这几年在单片机的开发中经常用到FreeModbus,由于每个项目开发间隔时间长,选用的芯片型号不一样,故成贴记录本次利用STM32CubeMX与Keil2个工具移植的过程,减少下次移植的苦恼,以下内容从广大网友取经而来,如涉及侵权,敬请告知,立即整改。
/*更新纪要*/
/*20250322:将FreeModbus原CRC校验程序查表法改为STM32硬件CRC实现,在保证效率的前提下减少代码大小,详见:六、性能提升*/
/*20250401:在port.h中定义使用的串口为MB_UART为&hlpuart1和定时器MB_TIM为&htim7,portserial.c与porttimer.c文件中使用MB_UART与MB_TIM,方便在移植到其他芯片*/
一、STM32CubeMX的配置
开发时先实现FreeModbus的移植,后续再增加项目所需的其他功能,这里用了TIM7(这个定时器功能不能对外输出脉冲,正好用来给FreeModbus提供50uS的时钟)和LPUART1(开发使用的是NUCLEO-G474RE,MCU与调试器的虚拟串口连接的串口是LPUART1)。
1.系统配置
首先选择芯片,配置SYS模式和时钟树,选用Debug模式为Serial Wire,并时钟频率拉满,APB1频率为170MHz,对应2个外设TIM7和LPUART1的时钟也是170MHz在,这里需要记录一下,一会配置定时器还需要用来计算定时器周期。


2.定时器配置
在mbrtu.c文件的eMBRTUInit函数里具体说明了串口波特率和定时器设置的关系:
/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode
eMBRTUInit( UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
eMBErrorCode eStatus = MB_ENOERR;
ULONG usTimerT35_50us;
( void )ucSlaveAddress;
ENTER_CRITICAL_SECTION( );
/* Modbus RTU uses 8 Databits. */
if( xMBPortSerialInit( ucPort, ulBaudRate, 8, eParity ) != TRUE )
{
eStatus = MB_EPORTERR;
}
else
{
/* If baudrate > 19200 then we should use the fixed timer values
* t35 = 1750us. Otherwise t35 must be 3.5 times the character time.
*/
if( ulBaudRate > 19200 )
{
usTimerT35_50us = 35; /* 1800us. */
}
else
{
/* The timer reload value for a character is given by:
*
* ChTimeValue = Ticks_per_1s / ( Baudrate / 11 )
* = 11 * Ticks_per_1s / Baudrate
* = 220000 / Baudrate
* The reload for t3.5 is 1.5 times this value and similary
* for t3.5.
*/
usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );
}
if( xMBPortTimersInit( ( USHORT ) usTimerT35_50us ) != TRUE )
{
eStatus = MB_EPORTERR;
}
}
EXIT_CRITICAL_SECTION( );
return eStatus;
}
从上面代码的注释中可以看出,当波特率大于19200时,超时时间为1750us,当波特率小于19200时,超时时间为3.5个字符时间,项目使用115200的波特率,所以超时时间定为1750uS使用以下配置:
(1)进入TIM7配置界面,先得到50us的预分频(20kHz),即将预分频寄存器(PSC)设置为8500-1,计算方法如下:。
(2)再配置计数周期为1750uS,即将自动重装载寄存器(AutoReload Register)设置为35,计算方法如下:。
(3)自动重载预装载功能可以打开,如果后续串口波特率小于115200时需要用到。
- 启用自动重载预装载:要是开启了自动重载预装载功能,写入到 ARR 寄存器的值不会马上生效,而是先存于预装载寄存器。直到更新事件发生时,预装载寄存器的值才会被加载到影子寄存器,从而影响后续的计数操作。
- 禁用自动重载预装载:当禁用自动重载预装载功能时,写入到 ARR 寄存器的值会立刻被传送到影子寄存器,马上对计数操作产生影响。

(4)使能TIM7中断

3.LPUART1配置
(1)将通信模式配置为异步通信,参数为波特率115200,数据位8,检验NONE,停止位1

(2)使能LPUART中断

4.中断配置
TIM7的中断优先级必须比LPUART1低

5.项目参数配置
将代码生成规则设置成:将外设初始化为每个外设的一对 “. c/.h” 文件,然后生成代码。这样做方便后续进行移植改动初始化程序,不需要在main.c中有大的改动。

二、FreeModus源文件的添加
1.下载代码包
代码来于embedded-experts的官网(embedded-experts),该网页其中给出了源码的github仓库地址:https://github.com/cwalter-at/freemodbus,这里为方便大家下载我复制了一份在我的GitCode(FreeModbus_v1.6)。
2.复制相关代码文件到项目文件
(1)代码包文件树如下:
|-- Changelog.txt
|-- bsd.txt
|-- demo(案例,相关功能码的实现不完全,移植时复制一个空白的案例即BARE文件夹)
|-- doc 说明文档
|-- gpl.txt
|-- lgpl.txt
|-- modbus (存放源代码,需要复制的文件夹)
| |-- ascii(Modbus-ASCII协议源码)
| |-- functions (功能码对应的处理函数)
| |-- include(头文件)
| |-- mb.c(MODBUS协议栈的主文件框架,与具体的协议无关)
| |-- rtu(Modbus-RTU协议源码)
| `-- tcp(Modbus-TCP协议源码)
`-- tools
(2)确定项目使用的协议类型
Modbus的协议包含以下3种:
- Modbus-RTU
- Modbus-ASCII
- Modbus-TCP
- 本项目只使用了Modbus-RTU,后续根据协议类型修改相关代码。
(3)复制相关代码文件到项目文件中,并在Keil中添加,为方便识别将demo.c文件改名为STM32_FreeModus.c。
(4)重新设置头文件路径,将代码包的头文件包含进去。
三、代码的修改
修改内容及原因在代码中进行注释。
1.修改STM32_FreeModbus.c文件(原demo.c文件)
将main函数名改为slave,防止与项目中main.c文件里的main函数重复;
int
main( void )
{
eMBErrorCode eStatus;
eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );
/* Enable the Modbus Protocol Stack. */
eStatus = eMBEnable( );
for( ;; )
{
( void )eMBPoll( );
/* Here we simply count the number of poll cycles. */
usRegInputBuf[0]++;
}
}
/*该main函数与main.c里面的main函数重新,需要修改成下面代码
下面slave函数,在实际代码中并不运行,可直接删除。
*/
int
slave( void )
{
eMBErrorCode eStatus;
eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );
/* Enable the Modbus Protocol Stack. */
eStatus = eMBEnable( );
for( ;; )
{
( void )eMBPoll( );
/* Here we simply count the number of poll cycles. */
usRegInputBuf[0]++;
}
}
2.修改mbconfig.h文件
/*! \brief If Modbus ASCII support is enabled. */
#define MB_ASCII_ENABLED ( 1 )
/*! \brief If Modbus RTU support is enabled. */
#define MB_RTU_ENABLED ( 1 )
/*! \brief If Modbus TCP support is enabled. */
#define MB_TCP_ENABLED ( 0 )
/*取消不用的功能,源代码默认支持Modbus ASCII和Modbus RTU功能
这里我们只需要用到Modbus RTU功能,修改成下面代码
*/
/*! \brief If Modbus ASCII support is enabled. */
#define MB_ASCII_ENABLED ( 0 )
/*! \brief If Modbus RTU support is enabled. */
#define MB_RTU_ENABLED ( 1 )
/*! \brief If Modbus TCP support is enabled. */
#define MB_TCP_ENABLED ( 0 )
编译不报错,表示工程建立完成,可进行下一步修改。
3.修改port文件夹里的代码
port文件夹里的C文件需要我们根据不同平台进行修改
(1)port.h文件
#include <assert.h>
#include <inttypes.h>
#include "usart.h" //添加串口相关头文件
#include "tim.h" //添加定时器相关头文件
#include "crc.h" //添加CRC相关头文件,使用硬件CRC需要用到,不使用不需要添加
#define MB_UART &huart2 //定义使用的串口端口
#define MB_TIM &htim7 //定义使用的定时器
#define INLINE inline
#define PR_BEGIN_EXTERN_C extern "C" {
#define PR_END_EXTERN_C }
#define ENTER_CRITICAL_SECTION( ) __set_PRIMASK(1) //关总中断
#define EXIT_CRITICAL_SECTION( ) __set_PRIMASK(0) //开总中断
(2)portserial.c文件
/*
* FreeModbus Libary: BARE Port
* Copyright (C) 2006 Christian Walter <wolti@sil.at>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* File: $Id$
*/
#include "port.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
//static
void prvvUARTTxReadyISR( void );
//static
void prvvUARTRxISR( void );
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if (xRxEnable) //根据输入参数,使能串口接收(xRxEnable)和发送(xTxEnable),中断模式,项目使用LPUART1,故为&hlpuart1
{
__HAL_UART_ENABLE_IT(MB_UART, UART_IT_RXNE);
}
else
{
__HAL_UART_DISABLE_IT(MB_UART, UART_IT_RXNE);
}
if (xTxEnable)
{
__HAL_UART_ENABLE_IT(MB_UART, UART_IT_TXE);
}
else
{
__HAL_UART_DISABLE_IT(MB_UART, UART_IT_TXE);
}
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
return TRUE;//串口初始化在usart.c定义,在mian函数中完成,改为TURE,后续再修改成用该函数初始化,方便修改串口通信参数
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
if(HAL_UART_Transmit (MB_UART, (uint8_t *)&ucByte, 1, 0x01) != HAL_OK ) //添加发送一个字符
return FALSE ;
else
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
/* Return the byte in the UARTs receive buffer. This function is called
* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
*/
if(HAL_UART_Receive (MB_UART, (uint8_t *)pucByte, 1, 0x01) != HAL_OK ) //添加接收一位字符
return FALSE ;
else
return TRUE;
}
/* Create an interrupt handler for the transmit buffer empty interrupt
* (or an equivalent) for your target processor. This function should then
* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
* a new character can be sent. The protocol stack will then call
* xMBPortSerialPutByte( ) to send the character.
*/
/*当static用于修饰全局变量和函数时,它们的作用域会被限制在定义它们的源文件内,其他源文件无法访问这些全局变量和函数。这有助于避免命名冲突,提高代码的模块化程度。为方便在中断函数中调用这里取消掉。文件开头的声明也需要注释掉,不然编译还是不通过。*/
//static
void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
/* Create an interrupt handler for the receive interrupt for your target
* processor. This function should then call pxMBFrameCBByteReceived( ). The
* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
* character.
*/
/*当static用于修饰全局变量和函数时,它们的作用域会被限制在定义它们的源文件内,其他源文件无法访问这些全局变量和函数。这有助于避免命名冲突,提高代码的模块化程度。为方便在中断函数中调用这里取消掉。文件开头的声明也需要注释掉,不然编译还是不通过。*/
//static
void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
(3)porttimer.c文件
/*
* FreeModbus Libary: BARE Port
* Copyright (C) 2006 Christian Walter <wolti@sil.at>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* File: $Id$
*/
/* ----------------------- Platform includes --------------------------------*/
#include "port.h"
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- static functions ---------------------------------*/
//static
void prvvTIMERExpiredISR( void );
/* ----------------------- Start implementation -----------------------------*/
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
return TRUE; //串口初始化在usart.c定义,在mian函数中完成,改为TURE,后续再修改成用该函数初始化,方便修改串口通信参数
}
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
/*使能定时器7中断模式(更新中断),项目使用TIM7,故为&htim7*/
__HAL_TIM_CLEAR_IT(MB_TIM, TIM_IT_UPDATE); //清除定时器中断标志位
__HAL_TIM_ENABLE_IT(MB_TIM, TIM_IT_UPDATE); //使能定时器更新中断
__HAL_TIM_SET_COUNTER(MB_TIM, 0); //定时器计数器置0
__HAL_TIM_ENABLE(MB_TIM);//使能定时器
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
/*关闭定时器7*/
__HAL_TIM_DISABLE(MB_TIM);
__HAL_TIM_SET_COUNTER(MB_TIM, 0);
__HAL_TIM_DISABLE_IT(MB_TIM, TIM_IT_UPDATE);
__HAL_TIM_CLEAR_IT(MB_TIM, TIM_IT_UPDATE);
}
/* Create an ISR which is called whenever the timer has expired. This function
* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
* the timer has expired.
*/
/*当static用于修饰全局变量和函数时,它们的作用域会被限制在定义它们的源文件内,其他源文件无法访问这些全局变量和函数。这有助于避免命名冲突,提高代码的模块化程度。为方便在中断函数中调用这里取消掉。文件开头的声明也需要注释掉,不然编译还是不通过。*/
//static
void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
4.修改中断文件(stm32g4xx_it.c)
(1)添加三个函数声明,引用自portserial.c和porttimer.c
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */
/*将中断函数需要引用的三个文件进行声明*/
extern void prvvUARTTxReadyISR(void);
extern void prvvUARTRxISR(void);
extern void prvvTIMERExpiredISR( void );
/* USER CODE END PFP */
(2)修改串口中断函数,将相关流程连接至modbus中相关中断处理函数
/**
* @brief This function handles LPUART1 global interrupt.
*/
void LPUART1_IRQHandler(void)
{
/* USER CODE BEGIN LPUART1_IRQn 0 */
/* USER CODE END LPUART1_IRQn 0 */
HAL_UART_IRQHandler(&hlpuart1);
/* USER CODE BEGIN LPUART1_IRQn 1 */
if(__HAL_UART_GET_IT_SOURCE(&hlpuart1, UART_IT_RXNE)!= RESET)
{
prvvUARTRxISR();//接收中断
}
if(__HAL_UART_GET_IT_SOURCE(&hlpuart1, UART_IT_TXE)!= RESET)
{
prvvUARTTxReadyISR();//发送中断
}
HAL_NVIC_ClearPendingIRQ(LPUART1_IRQn);//清除LPUART1中断的挂起状态
/* USER CODE END LPUART1_IRQn 1 */
}
(3)修改定时器中断函数,将相关流程连接至modbus中相关中断处理函数
void TIM7_DAC_IRQHandler(void)
{
/* USER CODE BEGIN TIM7_DAC_IRQn 0 */
/* USER CODE END TIM7_DAC_IRQn 0 */
HAL_TIM_IRQHandler(&htim7);
/* USER CODE BEGIN TIM7_DAC_IRQn 1 */
if(__HAL_TIM_GET_FLAG(&htim7, TIM_FLAG_UPDATE)) // 更新中断标记被置位
{
__HAL_TIM_CLEAR_FLAG(&htim7, TIM_FLAG_UPDATE); // 清除中断标记
prvvTIMERExpiredISR(); // 通知modbus3.5个字符等待时间到
}
/* USER CODE END TIM7_DAC_IRQn 1 */
}
5.功能码处理程序
该代码参考了网上大神的作品,时间太过久远,忘记是参考谁的了,广大网友如有印象,烦请告知,我将在文中进行署名。
/*
* FreeModbus Libary: BARE Demo Application
* Copyright (C) 2006 Christian Walter <wolti@sil.at>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* File: $Id$
*/
/* ----------------------- Modbus includes ----------------------------------*/
#include "mb.h"
#include "mbport.h"
/* ----------------------- Defines ------------------------------------------*/
//输入寄存器起始地址
#define REG_INPUT_START 0x0001
//输入寄存器数量
#define REG_INPUT_NREGS 10
//保持寄存器起始地址
#define REG_HOLDING_START 0x0001
//保持寄存器数量
#define REG_HOLDING_NREGS 10
//线圈起始地址
#define REG_COILS_START 0x0001
//线圈数量
#define REG_COILS_SIZE 16
//开关寄存器起始地址
#define REG_DISCRETE_START 0x0001
//开关寄存器数量
#define REG_DISCRETE_SIZE 16
/* ----------------------- Static variables ---------------------------------*/
//输入寄存器内容
uint16_t usRegInputBuf[REG_INPUT_NREGS] = {0};
//输入寄存器起始地址
uint16_t usRegInputStart = REG_INPUT_START;
//保持寄存器内容
uint16_t usRegHoldingBuf[REG_HOLDING_NREGS] = {1,2,3,4,5,6,7,8,9,10};
//保持寄存器起始地址
uint16_t usRegHoldingStart = REG_HOLDING_START;
//线圈状态
uint8_t ucRegCoilsBuf[REG_COILS_SIZE / 8] = {0};
//开关输入状态
uint8_t ucRegDiscreteBuf[REG_DISCRETE_SIZE / 8] = {0};
/* ----------------------- Start implementation -----------------------------*/
int
slave( void )
{
eMBErrorCode eStatus;
eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );
/* Enable the Modbus Protocol Stack. */
eStatus = eMBEnable( );
for( ;; )
{
( void )eMBPoll( );
/* Here we simply count the number of poll cycles. */
usRegInputBuf[0]++;
}
}
/****************************************************************************
* 名 称:eMBRegInputCB
* 功 能:读取输入寄存器,对应功能码是 04 eMBFuncReadInputRegister
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 寄存器地址
* usNRegs: 要读取的寄存器个数
* 出口参数:
* 注 意:上位机发来的 帧格式是: SlaveAddr(1 Byte)+FuncCode(1 Byte)
* +StartAddrHiByte(1 Byte)+StartAddrLoByte(1 Byte)
* +LenAddrHiByte(1 Byte)+LenAddrLoByte(1 Byte)+
* +CRCAddrHiByte(1 Byte)+CRCAddrLoByte(1 Byte)
* 3 区
****************************************************************************/
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
if( ( usAddress >= REG_INPUT_START )
&& ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
{
iRegIndex = ( int )( usAddress - usRegInputStart );
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( UCHAR )( usRegInputBuf[iRegIndex] >> 8 );
*pucRegBuffer++ = ( UCHAR )( usRegInputBuf[iRegIndex] & 0xFF );
iRegIndex++;
usNRegs--;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
/****************************************************************************
* 名 称:eMBRegHoldingCB
* 功 能:对应功能码有:06 写保持寄存器 eMBFuncWriteHoldingRegister
* 16 写多个保持寄存器 eMBFuncWriteMultipleHoldingRegister
* 03 读保持寄存器 eMBFuncReadHoldingRegister
* 23 读写多个保持寄存器 eMBFuncReadWriteMultipleHoldingRegister
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 寄存器地址
* usNRegs: 要读写的寄存器个数
* eMode: 功能码
* 出口参数:
* 注 意:4 区
****************************************************************************/
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
if((usAddress >= REG_HOLDING_START) && \
((usAddress + usNRegs) <= (REG_HOLDING_START + REG_HOLDING_NREGS)))
{
iRegIndex = (int)(usAddress - usRegHoldingStart );
switch(eMode)
{
case MB_REG_READ://读 MB_REG_READ = 0
while(usNRegs > 0)
{
*pucRegBuffer++ = (uint8_t)(usRegHoldingBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (uint8_t)(usRegHoldingBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE://写 MB_REG_WRITE = 0
while(usNRegs > 0)
{
usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
}
}
else//错误
{
eStatus = MB_ENOREG;
}
return eStatus;
}
extern void xMBUtilSetBits( UCHAR * ucByteBuf, USHORT usBitOffset, UCHAR ucNBits,
UCHAR ucValue );
extern UCHAR xMBUtilGetBits( UCHAR * ucByteBuf, USHORT usBitOffset, UCHAR ucNBits );
/****************************************************************************
* 名 称:eMBRegCoilsCB
* 功 能:对应功能码有:01 读线圈 eMBFuncReadCoils
* 05 写线圈 eMBFuncWriteCoil
* 15 写多个线圈 eMBFuncWriteMultipleCoils
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 线圈地址
* usNCoils: 要读写的线圈个数
* eMode: 功能码
* 出口参数:
* 注 意:如继电器
* 0 区
****************************************************************************/
eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils,
eMBRegisterMode eMode )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//寄存器个数
int16_t iNCoils = ( int16_t )usNCoils;
//寄存器偏移量
int16_t usBitOffset;
//检查寄存器是否在指定范围内
if( ( (int16_t)usAddress >= REG_COILS_START ) &&
( usAddress + usNCoils <= REG_COILS_START + REG_COILS_SIZE ) )
{
//计算寄存器偏移量
usBitOffset = ( int16_t )( usAddress - REG_COILS_START );
switch ( eMode )
{
//读操作
case MB_REG_READ:
while( iNCoils > 0 )
{
*pucRegBuffer++ = xMBUtilGetBits( ucRegCoilsBuf, usBitOffset,
( uint8_t )( iNCoils > 8 ? 8 : iNCoils ) );
iNCoils -= 8;
usBitOffset += 8;
}
break;
//写操作
case MB_REG_WRITE:
while( iNCoils > 0 )
{
xMBUtilSetBits( ucRegCoilsBuf, usBitOffset,
( uint8_t )( iNCoils > 8 ? 8 : iNCoils ),
*pucRegBuffer++ );
iNCoils -= 8;
}
break;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
/****************************************************************************
* 名 称:eMBRegDiscreteCB
* 功 能:读取离散寄存器,对应功能码有:02 读离散寄存器 eMBFuncReadDiscreteInputs
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 寄存器地址
* usNDiscrete: 要读取的寄存器个数
* 出口参数:
* 注 意:1 区
****************************************************************************/
eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
//错误状态
eMBErrorCode eStatus = MB_ENOERR;
//操作寄存器个数
int16_t iNDiscrete = ( int16_t )usNDiscrete;
//偏移量
uint16_t usBitOffset;
//判断寄存器时候再制定范围内
if( ( (int16_t)usAddress >= REG_DISCRETE_START ) &&
( usAddress + usNDiscrete <= REG_DISCRETE_START + REG_DISCRETE_SIZE ) )
{
//获得偏移量
usBitOffset = ( uint16_t )( usAddress - REG_DISCRETE_START );
while( iNDiscrete > 0 )
{
*pucRegBuffer++ = xMBUtilGetBits( ucRegDiscreteBuf, usBitOffset,
( uint8_t)( iNDiscrete > 8 ? 8 : iNDiscrete ) );
iNDiscrete -= 8;
usBitOffset += 8;
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
6.启动modbus
(1)在main.c文件中添加下面2个头文件
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "mb.h"
#include "mbport.h"
/* USER CODE END Includes */
(2)初始化及使能modbus,并在主循环中开始侦听
/* USER CODE BEGIN 2 */
eMBInit( MB_RTU, 0x01, 1, 115200, MB_PAR_NONE);//初始化modbus,走modbusRTU,从站地址为0x01,端口为1。
eMBEnable( );//使能modbus
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
( void )eMBPoll( );//启动modbus侦听
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
四、编译项目
编译成功,下载到单片机中

五、系统调试
调试使用的是Modbus Poll工具,不过需要激活使用,这里试用了一下,一款很不错的软件。
1.设置好通信参数及端口
需要设置端口、波特率、数据位、奇偶校验、停止位、模式等。

2.定义读写要求
需要设置从站地址,功能码(测试功能码03),寄存器起始地址,寄存器数量,扫描速率等。

3.通信结果
(1)使用03功能码,正常读取出保存寄存器数据。

(2)加快扫描速率进行测试,没有报错

4.modbus相关功能码
| 功能码 | 名称 | 描述 |
| 1 | 读取线圈状态 | 读取一个或多个线圈(离散输出)的状态,返回值为 0 或 1,表示线圈的断开或闭合 |
| 2 | 读取离散输入状态 | 读取一个或多个离散输入的状态,返回值为 0 或 1,表示输入的断开或闭合 |
| 3 | 读取保持寄存器 | 读取一个或多个保持寄存器的值,用于获取设备的配置信息、测量值等 |
| 4 | 读取输入寄存器 | 读取一个或多个输入寄存器的值,通常用于获取只读的测量值或状态信息 |
| 5 | 写单个线圈 | 将单个线圈设置为指定的状态(0 或 1),用于控制设备的输出 |
| 6 | 写单个寄存器 | 将单个保持寄存器设置为指定的值,用于配置设备的参数 |
| 15 | 写多个线圈 | 同时将多个线圈设置为指定的状态,通过一次操作控制多个输出 |
| 16 | 写多个寄存器 | 同时将多个保持寄存器设置为指定的值,用于批量 |
六、优化改进
1.使用STM32硬件CRC实现MODBUS-CRC校验
采用STM32硬件CRC进行校验,所有的设置选项均要保持一致才能完成MODBUS-CRC校验。

如上图完成CRC设置后,找到mbcrc.c,将usMBCRC16( UCHAR * pucFrame, USHORT usLen )修改如下,其余内容删除或注释掉即可。
USHORT
usMBCRC16( UCHAR * pucFrame, USHORT usLen )
{
/*改用STM32硬件CRC进行校验*/
return ( USHORT )(HAL_CRC_Calculate(&hcrc, (ULONG*)pucFrame, usLen));
}
此外,在port.h文件中添加,编译成功后即可进行测试。
#include "crc.h" //添加CRC相关头文件
2.程序轻量化裁剪
为满足小内存芯片使用,可根据工作场景需要对程序进行裁剪,以下是对相关的文件进行修改
(1)修改mbconfig.h中的定义,取消不需要的通信模式和功能码定义,比如这里我只启用了启用 Modbus RTU 通信模式和11、03、06等3个功能码
```c
#ifndef _MB_CONFIG_H
#define _MB_CONFIG_H
#ifdef __cplusplus
PR_BEGIN_EXTERN_C
#endif
/* ----------------------- 宏定义 ------------------------------------------*/
/*! \defgroup modbus_cfg Modbus 协议栈配置分组
*
* 协议栈绝大部分功能模块均可按需裁剪关闭,
* 当单片机资源紧张、需要节省程序Flash空间时该特性尤为实用。<br>
*
* 本文件 mbconfig.h 包含全部协议栈功能开关配置项。
*/
/*! \addtogroup modbus_cfg
* @{
*/
/*! \brief 是否启用 Modbus ASCII 通信模式 */
#define MB_ASCII_ENABLED (0)
/*! \brief 是否启用 Modbus RTU 通信模式 */
#define MB_RTU_ENABLED (1)
/*! \brief 是否启用 Modbus TCP 以太网通信模式 */
#define MB_TCP_ENABLED (0)
/*! \brief Modbus ASCII 模式字符间隔超时时间(单位:秒)
*
* ASCII 协议没有固定的帧间隔超时,因此通过该宏自定义配置;
* 建议设置为现场网络通信预估最大延迟时长。
*/
#define MB_ASCII_TIMEOUT_SEC (1)
/*! \brief ASCII 模式开启串口发送前的等待延时
*
* 若定义该宏,协议栈会调用底层延时函数 vMBPortSerialDelay,
* 在打开串口发送引脚前插入一段延时。
* 部分单片机运行速度极快,接收完报文会立刻回复应答,
* 若主机串口接收开启速度较慢,会丢失应答帧,因此需要该延时缓冲。
*/
#ifndef MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS
#define MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS (0)
#endif
/*! \brief 协议栈支持的最大Modbus功能码处理数量
*
* 该数值必须大于当前开启的所有内置功能码 + 自定义功能码总数;
* 数值设置过小会导致新增功能码注册失败。
*/
#define MB_FUNC_HANDLERS_MAX (3)
/*! \brief “读取从站标识”功能的缓存分配字节长度
*
* 该宏限制从站自定义标识信息的最大存储长度;
* 配套接口 eMBSetSlaveID() 配置标识内容;
* 仅当 MB_FUNC_OTHER_REP_SLAVEID_ENABLED 置1时生效。
*/
#define MB_FUNC_OTHER_REP_SLAVEID_BUF (32)
/*! \brief 是否启用 0x11 读取从站标识 功能码 */
#define MB_FUNC_OTHER_REP_SLAVEID_ENABLED (1)
/*! \brief 是否启用 0x04 读输入寄存器 功能码 */
#define MB_FUNC_READ_INPUT_ENABLED (0)
/*! \brief 是否启用 0x03 读保持寄存器 功能码 */
#define MB_FUNC_READ_HOLDING_ENABLED (1)
/*! \brief 是否启用 0x06 写单个保持寄存器 功能码 */
#define MB_FUNC_WRITE_HOLDING_ENABLED (1)
/*! \brief 是否启用 0x10 批量写多个保持寄存器 功能码 */
#define MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED (0)
/*! \brief 是否启用 0x01 读线圈 功能码 */
#define MB_FUNC_READ_COILS_ENABLED (0)
/*! \brief 是否启用 0x05 写单个线圈 功能码 */
#define MB_FUNC_WRITE_COIL_ENABLED (0)
/*! \brief 是否启用 0x0F 批量写多个线圈 功能码 */
#define MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED (0)
/*! \brief 是否启用 0x02 读离散输入 功能码 */
#define MB_FUNC_READ_DISCRETE_INPUTS_ENABLED (0)
/*! \brief 是否启用 0x23 读写多个保持寄存器 功能码 */
#define MB_FUNC_READWRITE_HOLDING_ENABLED (0)
/*! @} */
#ifdef __cplusplus
PR_END_EXTERN_C
#endif
#endif
```
(2)修改STM32_FreeModbus.c中各类寄存器、线圈的数量,注释掉没有用到的功能码处理函数
5775

被折叠的 条评论
为什么被折叠?



