Freemodbus实现一主多从
目录
引言
利用Freemodbus从机开源库实现一主多从,主机是串口模拟的上位机,从机1是一块GD32的开发板,控制项(寄存器){热敏电阻、湿敏电阻、LED1、LED2};从机2是自己焊的一块STM32洞洞板,控制项(寄存器){热敏电阻、湿敏电阻、LED1、LED2};
使用到的器件,GD32开发板(自带RS485转TTL电路),STM32洞洞板,USB转RS485模块,RS485转TTL模块,ST-Link下载器。
从机1(gd32)和从机2(stm32)的代码以及Freemodbus库都放在了下方的链接:
通过网盘分享的文件:代码包.zip
链接: https://pan.baidu.com/s/10x1x7Z0F80pnuMuBRs9zdw?pwd=GD32 提取码: GD32
1、Freemodbus简介
我的这篇文章介绍了Modbus协议内容:RS485电气协议和Modbus软件协议-CSDN博客,Modbus协议本身简单,但是想用软件实现是很复杂的,而Freemodbus是一个开源的库(只有从机代码开源),这个库已经实现了Modbus通信协议的大部分内容,比如3.5个字符空闲中断等等。我们只需要学会移植这个库,实现自己的逻辑代码即可。
FreeMODBUS Downloads - Embedded Experts这是下载Freemodbus库的官方网址,代码在GitHub上,国内可以使用加速器访问,比如Steam加速器(现在叫Watt Toolkit)将里面的GitHub加速勾选就可以访问了。
1.1 如何使用库文件
如下图所示就是下载好的库文件,我们只关心两个文件夹,demo和modbus
那怎么在我们自己的工程中添加库文件呢?下图是软件架构图,APP是业务应用层;Mid是中间层,主要是移植的库文件中实现具体协议内容的那部分文件;DRV就是驱动层,细分成两部分,一部分就是普通片上外设的驱动实现,一部分就是Modbus需要使用的片上外设的驱动实现,485电路也是串口通信,那就需要串口初始化,3.5个字符的空闲时间需要用定时器 ,那就需要定时器驱动初始化。

1.2 工程建立详解
FreeModbus库\freemodbus-master\demo\BARE\port将这个路径下的四个文件,复制放到我们工程文件的Drv_modbus文件夹中

FreeModbus库\freemodbus-master\modbus将这个路径下的,下图中选中的文件,复制到工程文件的Mid_modbus文件夹下。
由于我们只会用到Modbus读写寄存器的三个功能,03、06、16,所以打开我们的工程文件,打开Mid_modbus文件,打开functions文件夹,只留下图中的两个.c文件即可,下图中的第一个就是功能实现代码,第二个是一些通用接口函数的实现。
1.3 Keil内需要的初步设置
把这些文件添加进Keil工程内之后,记得包含这些新添加的文件夹的路径和头文件的途径。之后还需要对库文件的代码进行一些设置:
想办法找到并打开Freemodbus库文件的mbconfig.h这个头文件,由于我们使用RTU报文,所以只需使能MB_RTU_ENABLED的(1),其它两个失能为0。

然后这部分代码是功能使能区,由于我们只使用03、06、16三个功能,所以只需使能下图中的三个1。

之后再编译只会显示一个错误,说有一个函数eMBRegHoldingCB没有实现定义,这就是我们自己需要实现的函数,后面再讲。我们在工程Mid_midbus下新加modbus_slave的.c和.h文件,用于实现eMBRegHoldingCB这个函数。
2、Freemodbus-RTU相关代码详解
我写这部分的思路就是,先用已经写好的GD32的代码,把上位机主机串口发送字符帧,从机返回字符帧这个过程,手动过一遍代码,这样就可以知道Freemodbus库文件各函数的调用关系。
还是要解释一下各个文件,不然之后跳来跳去容易混乱,portserial.c串口驱动代码和串口中断服务函数的实现,当然还有一些其它调用函数;porttimer.c是定时器中断驱动及其中断服务函数的实现,同样也有一些其它调用函数;portevent.c提供从机事件状态,比如没有接收到主机字符帧是一个状态码,接收完一包数据又是一个状态码....
mbcrc.c就是crc循环校验的实现,可以不用细看;mbrtu.c很重要,代码可以细看,是RTU报文内容的实现;mbfuncholding.c是功能码的视线;mbutils.c是一些通用接口函数的实现;modbus_slave.c是我们自己加入的文件,用于实现eMBRegHoldingCB这个函数。
modbus_app.c是我们自己实现的业务层代码文件。
值得一提的是,freemodbus库文件中的驱动文件有三个,但是却只有一个port.h文件,因为这三个.c文件的函数声明被集成放在了一个port.h文件中。

2.1 主机没有发送数据时的程序流程-modbus部分




提示:整个第二章都是用的GD32的代码。
(1)main.c中的main函数调用初始化函数
AppInit();
这个函数又调用modbus应用层初始化函数
ModbusAppInit();
这个函数又调用从机初始化函数
ModbusSlaveInit(&mbInstace);
(2)从机初始化函数内会调用两个函数
void ModbusSlaveInit(ModbusSlaveInstance_t *mbInstance)
{
eMBInit(MB_RTU, mbInstance->slaveAddr, 0, mbInstance->baudRate, MB_PAR_NONE);//串口号在portserial中已经写固定了,所以写啥都无所谓
eMBEnable();
g_modbusFuncCb = mbInstance->cb;//结构体直赋值
}
【2.1】eMBInit(MB_RTU, mbInstance->slaveAddr, 0, mbInstance->baudRate, MB_PAR_NONE);
函数的参数中,串口号给的是0,这一点之后解释。
/* ----------------------- Start implementation -----------------------------*/
eMBErrorCode
eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
eMBErrorCode eStatus = MB_ENOERR;
/* check preconditions */
//从机地址范围是1~247,这里做检查判断
if( ( ucSlaveAddress == MB_ADDRESS_BROADCAST ) ||
( ucSlaveAddress < MB_ADDRESS_MIN ) || ( ucSlaveAddress > MB_ADDRESS_MAX ) )
{
eStatus = MB_EINVAL;
}
else
{
ucMBAddress = ucSlaveAddress;//全局变量接收表示从机地址的字节
switch ( eMode )
{
#if MB_RTU_ENABLED > 0
case MB_RTU://下面这些代码主要是给函数指针变量赋值
pvMBFrameStartCur = eMBRTUStart;
pvMBFrameStopCur = eMBRTUStop;
peMBFrameSendCur = eMBRTUSend;
peMBFrameReceiveCur = eMBRTUReceive;
pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
pxMBFrameCBByteReceived = xMBRTUReceiveFSM;
pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;
pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;
eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );
break;
#endif
#if MB_ASCII_ENABLED > 0
case MB_ASCII:
pvMBFrameStartCur = eMBASCIIStart;
pvMBFrameStopCur = eMBASCIIStop;
peMBFrameSendCur = eMBASCIISend;
peMBFrameReceiveCur = eMBASCIIReceive;
pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
pxMBFrameCBByteReceived = xMBASCIIReceiveFSM;
pxMBFrameCBTransmitterEmpty = xMBASCIITransmitFSM;
pxMBPortCBTimerExpired = xMBASCIITimerT1SExpired;
eStatus = eMBASCIIInit( ucMBAddress, ucPort, ulBaudRate, eParity );
break;
#endif
default:
eStatus = MB_EINVAL;
}
if( eStatus == MB_ENOERR )
{
if( !xMBPortEventInit( ) )
{
/* port dependent event module initalization failed. */
eStatus = MB_EPORTERR;
}
else
{
eMBCurrentMode = eMode;//全局变量接收报文类型码
eMBState = STATE_DISABLED;//注意现在这个全局变量已被赋值
}
}
}
return eStatus;
}
在函数 eMBInit中会调这个函数
eStatus = eMBRTUInit( ucMBAddress, ucPort, ulBaudRate, eParity );
/* ----------------------- 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;
}
在函数xMBPortSerialInit内可以看到几个参数前面加了void,意在告诉编译器我不使用这些参数,不用给我报警告提示,也意在告诉阅读代码的读者,我故意忽略这些参数的,并不是我忘记使用了,这也是最开始 eMBInit函数中,串口号给0的原因,因为调用到这,根本不会用到,我已经在驱动函数中把串口号写死了,所以参数位置写啥都可以。
不得不说多看看这些开源库代码还是很有好处的,虽然很难,但是能学到很多东西,比如说看到现在,你也该发现了,库代码中有很多变量前缀什么uc, us, eMB, xMB, vMB等等,冠以uc开头的变量是表示这个变量是无符号char类型的,uc就是unsigned short;eMB表示此变量或函数与Modbus协议有关,并且类型或返回值是枚举类型;xMB表示返回值或类型是BOOL类型;vMB表示是void类型等等。
就说eMBRTUInit这个函数,其使用串口和定时器初始化的方式也很巧妙,用的if和else。
定时器初始化函数xMBPortTimersInit( USHORT usTim1Timerout50us )的参数表示50us的倍数,定时器驱动初始化中把分频系数设为了5999,也就是说频率是20kHZ,那么周期就是50us,如果参数给2,则重装载值为2 - 1,那2*50 = 100us, 100us进入一次中断。为什么要这样设计呢?这和Modbus协议规定有关,因为它规定接收完一包数据之后需要有3.5T时间的空闲位(假设传输一个字符所需时间为T)。

当波特率大于19200时,重装载值为35,35*50=1750us;当为其它波特率时则采用计算的方式,波特率表示每秒传输的二进制位数,波特率为9600那么传输一个bit位需要1000/9600ms,而传输一个字节包括起始位、数据位、停止位 共10bit,那么传输一个字符所需的时间就是10*1000/9600ms, 传输3.5个字符就是3.5*10*1000/9600,转换为50us的倍数就是3.5*10*1000*1000/9600/50us,为了避免浮点数的运算,写成7*10*1000*1000/2/9600/50,也就是
7*10*20*1000/(2*9600)=7*200000/(2*9600),我们写的驱动代码中是没有奇偶校验位的,但是库代码中意思是有奇偶校验位,即传输一个字符是11bit,因为差别不大所以也就没有改。
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 );
}
#define ENTER_CRITICAL_SECTION( ) //这个是进出临界的宏定义,没定义的话空着就可以了;
//不同平台这个宏定义可以设计成不同的
#define EXIT_CRITICAL_SECTION( )
【2.2】 ModbusSlaveInit(。。。)调用eMBEnable();
eMBErrorCode
eMBEnable( void )
{
eMBErrorCode eStatus = MB_ENOERR;
if( eMBState == STATE_DISABLED )//之前初始化已经设置变量值了,所以if成立
{
/* Activate the protocol stack. */
pvMBFrameStartCur( );//调用这个函数指针变量。
eMBState = STATE_ENABLED;
}
else
{
eStatus = MB_EILLSTATE;
}
return eStatus;
}
// pvMBFrameStartCur( );//调用这个函数指针变量。
//实际就是调用执行这个函数
void
eMBRTUStart( void )
{
ENTER_CRITICAL_SECTION( );
/* Initially the receiver is in the state STATE_RX_INIT. we start
* the timer and if no character is received within t3.5 we change
* to STATE_RX_IDLE. This makes sure that we delay startup of the
* modbus protocol stack until the bus is free.
*/
eRcvState = STATE_RX_INIT;
vMBPortSerialEnable( TRUE, FALSE );//使能串口接收中断,注意是从机,也就是单片机接收
vMBPortTimersEnable( );//使能定时器更新中断、使能定时器EXIT_CRITICAL_SECTION( );
}
(3)while(1)内循环调用的函数说明
【3.1】 由于之前初始化已经使能了定时器跟新中断,时间肯定是<=1.75ms进入一次中断服务函数,所以单片机上电复位后的几乎瞬间就会执行定时器中断服务函数,理解这一点很重要,之后会用到。
【3.2】 单片机每隔1ms调用函数void ModbusTask(void), 而这个函数又会调用(void)eMBPoll();
void ModbusTask(void)
{
/******************************************************************************************
在 C 语言中,前面加上 (void) 的作用是显式地表明你不关心函数的返回值。具体来说:
①避免警告:如果 eMBPoll() 函数有返回值但你没有使用这个返回值,某些编译器可能会发出警告,提示你忽略了返回值。
通过将其强制转换为 (void),可以告诉编译器你明确意识到这个返回值并决定不使用它,从而避免警告。
②代码可读性:使用 (void) 可以提高代码的可读性,让其他开发者清楚地知道你故意忽略了这个返回值,而不是无意中遗漏。
******************************************************************************************/
(void)eMBPoll();
}
【3.3】 函数eMBPoll();由于主机没有发送数据,所以到第一个case就会break出去。
eMBErrorCode
eMBPoll( void )
{
static UCHAR *ucMBFrame;//这里不用数组形式表示,因为长度是未知的
static UCHAR ucRcvAddress;
static UCHAR ucFunctionCode;
static USHORT usLength;
static eMBException eException;
int i;
eMBErrorCode eStatus = MB_ENOERR;
eMBEventType eEvent;
/* Check if the protocol stack is ready. */
if( eMBState != STATE_ENABLED )//初始化已经ENABLE了,所以这个if不成立
{
return MB_EILLSTATE;
}
/* Check if there is a event available. If not return control to caller.
* Otherwise we will handle the event. */
if( xMBPortEventGet( &eEvent ) == TRUE )
{
switch ( eEvent )
{
case EV_READY://由于主机没有发送命令报文,所以到这个case就会跳出去
break;
case EV_FRAME_RECEIVED:
eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
if( eStatus == MB_ENOERR )
{
/* Check if the frame is for us. If not ignore the frame. */
if( ( ucRcvAddress == ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
{
( void )xMBPortEventPost( EV_EXECUTE );
}
}
break;
case EV_EXECUTE:
ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];
eException = MB_EX_ILLEGAL_FUNCTION;
for( i = 0; i < MB_FUNC_HANDLERS_MAX; i++ )
{
/* No more function handlers registered. Abort. */
if( xFuncHandlers[i].ucFunctionCode == 0 )
{
break;
}
else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
{
eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
break;
}
}
/* If the request was not sent to the broadcast address we
* return a reply. */
if( ucRcvAddress != MB_ADDRESS_BROADCAST )
{
if( eException != MB_EX_NONE )
{
/* An exception occured. Build an error frame. */
usLength = 0;
ucMBFrame[usLength++] = ( UCHAR )( ucFunctionCode | MB_FUNC_ERROR );
ucMBFrame[usLength++] = eException;
}
if( ( eMBCurrentMode == MB_ASCII ) && MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS )
{
vMBPortTimersDelay( MB_ASCII_TIMEOUT_WAIT_BEFORE_SEND_MS );
}
eStatus = peMBFrameSendCur( ucMBAddress, ucMBFrame, usLength );
}
break;
case EV_FRAME_SENT:
break;
}
}
return MB_ENOERR;
}
2.2 主机发送报文:01 06 00 02 00 01
(1)01是从机地址,06是功能码{写单个控制项}, 0002是控制项的地址,0001是数据内容,表示LED亮,如果是0000就表示熄灭LED。
由于初始化的时候是使能了定时器更新中断和定时器的,那么承接前言,单片机上电复位几乎一瞬间就会调用定时器中断服务函数。
void TIMER3_IRQHandler(void)
{
if (timer_interrupt_flag_get(TIMER3, TIMER_INT_FLAG_UP) == SET)
{
timer_interrupt_flag_clear(TIMER3, TIMER_INT_FLAG_UP);
timer_counter_value_config(TIMER3, 0);
prvvTIMERExpiredISR();
}
}
中断服务函数里面会执行函数指针,也就是下面这个函数:
BOOL
xMBRTUTimerT35Expired( void )
{
BOOL xNeedPoll = FALSE;switch ( eRcvState )
{
/* Timer t35 expired. Startup phase is finished. */
case STATE_RX_INIT:
xNeedPoll = xMBPortEventPost( EV_READY );
break;/* A frame was received and t35 expired. Notify the listener that
* a new frame was received. */
case STATE_RX_RCV:
xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );
break;/* An error occured while receiving the frame. */
case STATE_RX_ERROR:
break;/* Function called in an illegal state. */
default:
assert( ( eRcvState == STATE_RX_INIT ) ||
( eRcvState == STATE_RX_RCV ) || ( eRcvState == STATE_RX_ERROR ) );
}vMBPortTimersDisable( );
eRcvState = STATE_RX_IDLE;return xNeedPoll;
}
静态全局枚举类型变量eRcvState 未初始化默认为0,即STATE_RX_INIT,执行第一个case,然后退出switch,失能定时器中断和定时器,eRcvState赋值为STATE_RX_IDLE。
(1)由于初始化的时候,串口使能了接收中断,失能发送中断,所以每来一个字节,触发一次串口接收中断服务函数:
void USART1_IRQHandler(void)
{
//判断接收还是发送中断
if (RESET != usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_RBNE))
{
//接收完成中断
prvvUARTRxISR();
usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_RBNE_ORERR);
}
if(RESET != usart_interrupt_flag_get(g_uartHwInfo.uartNo, USART_INT_FLAG_TC))
{
//发送完成中断
prvvUARTTxReadyISR();
usart_interrupt_flag_clear(g_uartHwInfo.uartNo, USART_INT_FLAG_TC);
}
}
函数prvvUARTRxISR();会调用函数指针变量pxMBFrameCBByteReceived();也就是执行这个函数:
BOOL
xMBRTUReceiveFSM( void )
{
BOOL xTaskNeedSwitch = FALSE;
UCHAR ucByte;assert( eSndState == STATE_TX_IDLE );
/* Always read the character. */
( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );switch ( eRcvState )
{
/* If we have received a character in the init state we have to
* wait until the frame is finished.
*/
case STATE_RX_INIT:
vMBPortTimersEnable( );
break;/* In the error state we wait until all characters in the
* damaged frame are transmitted.
*/
case STATE_RX_ERROR:
vMBPortTimersEnable( );
break;/* In the idle state we wait for a new character. If a character
* is received the t1.5 and t3.5 timers are started and the
* receiver is in the state STATE_RX_RECEIVCE.
*/
case STATE_RX_IDLE:
usRcvBufferPos = 0;
ucRTUBuf[usRcvBufferPos++] = ucByte;
eRcvState = STATE_RX_RCV;/* Enable t3.5 timers. */
vMBPortTimersEnable( );
break;/* We are currently receiving a frame. Reset the timer after
* every character received. If more than the maximum possible
* number of bytes in a modbus frame is received the frame is
* ignored.
*/
case STATE_RX_RCV:
if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
{
ucRTUBuf[usRcvBufferPos++] = ucByte;
}
else
{
eRcvState = STATE_RX_ERROR;
}
vMBPortTimersEnable( );
break;
}
return xTaskNeedSwitch;
}
/* Always read the character. */
( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );
这条语句表示总是会在一开始就获取字节数据。
由于一开始的定时器中断已经把eRcvState赋值为STATE_RX_IDLE,所以自然会跳动这个case, 将接受到的第一个字节保存到数组【0】里面,然后usRcvBufferPos++为1,注意变量++是先使用后++。 将eRcvState = STATE_RX_RCV; break之前再调用定时器使能函数,在使能函数里面不仅会使能定时器和定时器跟新中断,还会对计数器清零,这样保证在接收完主机发送的报文之前不会产生3.5T的空闲。
之后再接收下一个字节,case会跳到STATE_RX_RCV,将接收到的字节保存到数组中,并且对定时器计数器清零,避免产生中断。
就这样将数据全部接收完后,不会再产生串口接收中断,也就不会再对计数器清零,这样定时器计数器到了3.5T的时间就会产生更新中断,由于eRcvState已经被赋值为STATE_RX_RCV,就会执行这个case:
case STATE_RX_RCV:
xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );
break;
跳出Switch后固定执行:
vMBPortTimersDisable( );
eRcvState = STATE_RX_IDLE;
(2)这个时候在每ms执行一次的eMBPoll();函数中,就会执行下述代码,类似状态机,在这个case中,函数指针变量peMBFrameReceiveCur的作用是把从串口中端侧接收到的:代表从机地址的字节数据、存放接收到的数据的数组地址(以元素1开始为首地址)、字节数据减3后的数字(减去从机地址1字节,CRC校验码2字节)分别拷贝给ucRcvAddress、ucMBFrame、usLength。
case EV_FRAME_RECEIVED:
eStatus = peMBFrameReceiveCur( &ucRcvAddress, &ucMBFrame, &usLength );
if( eStatus == MB_ENOERR )
{
/* Check if the frame is for us. If not ignore the frame. */
if( ( ucRcvAddress == ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
{
( void )xMBPortEventPost( EV_EXECUTE );
}
}
break;
第二次执行的时间自然就会跳转到case EV_EXECUTE:
ucFunctionCode = ucMBFrame[MB_PDU_FUNC_OFF];由于从串口中断侧接收到的数组地址是从1开始的,那么用于保存数据的数组ucMBFrame的第0个元素实际上就是功能码。
遍历到对应的功能码,就执行函数指针变量。
else if( xFuncHandlers[i].ucFunctionCode == ucFunctionCode )
{
eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength );
break;
}
06功能码实际执行的函数就是这个:
eMBException
eMBFuncWriteHoldingRegister( UCHAR * pucFrame, USHORT * usLen )
{
USHORT usRegAddress;
eMBException eStatus = MB_EX_NONE;
eMBErrorCode eRegStatus;
if( *usLen == ( MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN ) )
{
usRegAddress = ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_ADDR_OFF] << 8 );
usRegAddress |= ( USHORT )( pucFrame[MB_PDU_FUNC_WRITE_ADDR_OFF + 1] );
//usRegAddress++; modify
/* Make callback to update the value. */
eRegStatus = eMBRegHoldingCB( &pucFrame[MB_PDU_FUNC_WRITE_VALUE_OFF],
usRegAddress, 1, MB_REG_WRITE );
/* If an error occured convert it into a Modbus exception. */
if( eRegStatus != MB_ENOERR )
{
eStatus = prveMBError2Exception( eRegStatus );
}
}
else
{
/* Can't be a valid request because the length is incorrect. */
eStatus = MB_EX_ILLEGAL_DATA_VALUE;
}
return eStatus;
}
主机发送的字节数据加上两个校验码字节刚好是8个字节,之前在拷贝长度的时候已经减去了3,即减去了1个从机地址码字节,2个校验码字节,所以:
if( *usLen == ( MB_PDU_FUNC_WRITE_SIZE + MB_PDU_SIZE_MIN ) )成立。
//usRegAddress++; modify这里把这个注释掉了,其实modbus中规定主机发送的控制项地址应该比实际的地址少1,比如我们举的例子是发送:01 06 00 02 00 01,其中0002是控制项地址,按照规定我们应该发送01 06 00 01 00 01,然后代码用usRegAddress++;得到实际的地址,但是我们把库文件中的这条代码注释掉,我们发送的时候发送实际地址也是可以使用的,至于Modbus为啥有这种规定我也不知道,可能和PLC有关,毕竟是西门子公司发明的Modbus。
然后调用这个需要我们自己实现的函数,MB_PDU_FUNC_WRITE_VALUE_OFF值是3,时刻记住当初从串口中断拷贝的数组地址是从1开始的,所示这里是3,实际就是从“寄存器个数高字节”地址起。usRegAddress是上几条语句获得的控制项地址字节(高位低位),1表示只有一个控制项(因为给的功能码是写单寄存器),MB_REG_WRITE表示是主机向从机发送报文数据。
eRegStatus = eMBRegHoldingCB( &pucFrame[MB_PDU_FUNC_WRITE_VALUE_OFF],
usRegAddress, 1, MB_REG_WRITE );
后面的内容可已试着自己分析了, 不管是Freemodbus库文件中的代码还是自己写的逻辑代码,都使用到了大量的结构体和函数指针变量,需要有扎实的C语言基本功才能读懂。
2.3 其它
其它的从机返回数据,主机发送写多寄存器报文,主机发送读多寄存器功能报文,都可以试着自己分析一下。
eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, const UCHAR * pucFrame, USHORT usLength )
值得一提的是,在函数eMBRTUSend中,手动加入了以下代码。
//函数xMBRTUTransmitFSM是在中断函数USART1_IRQHandler中调用的
//TC上电默认为0进不了中断,手册436,图17-3
//插入代码 启动第一次发送,这样才可以进入发送完成中断,在中断中调用xMBRTUTransmitFSM函数 modify
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );
pucSndBufferCur++;
usSndBufferCount--;
因为从机串口发送中断采用的是TC标志位,而不是是TXE标志位,有什么区别呢?

假如串口发送中断采用TXE标志位会怎样。处理最后一个字节,发送数据寄存器中的数据空了,TXE标志位硬件置1,调用函数xMBRTUTransmitFSM(),xMBPortSerialPutByte()将数据从移位寄存器发送到数组中,退出中断。
此时串口发送中断还是使能的,由于发送数据寄存器中已经没有数据了,自然TXE置位1,调用函数xMBRTUTransmitFSM(),执行下面的代码,失能定时器发送,使能定时器接收,而如果移位寄存器中最后一个字节的数据还没有全移动数组中,那么就会导致最后一个字节发送错误,由于最后一个字节是校验码,那么就会导致错误。所以采用TC标志位。
else
{
xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
/* Disable transmitter. This prevents another transmit buffer
* empty interrupt. */
vMBPortSerialEnable( TRUE, FALSE );
eSndState = STATE_TX_IDLE;
}
那么采用TC标志位和我们在 eMBRTUSend函数中加的代码有什么关系呢?由于TC标志位是需要先发送数据,等TXE置1,移位寄存器空了之后才会置1。而发送数据的函数又是在发送中断中调用的,而没有TC标志位又根本不会触发发送中断。所以需要在eMBRTUSend,先手动发送一个字节数据,触发TC标志位,相当于“启动中断”的作用。

特别提示,有一个地方容易理解错:
那就是mbfuncholding.c中的函数eMBFuncReadHoldingRegister的这段代码:
pucFrameCur = &pucFrame[MB_PDU_FUNC_OFF];
*usLen = MB_PDU_FUNC_OFF;
/* First byte contains the function code. */
*pucFrameCur++ = MB_FUNC_READ_HOLDING_REGISTER;
*usLen += 1;
/* Second byte in the response contain the number of bytes. */
*pucFrameCur++ = ( UCHAR ) ( usRegCount * 2 );
*usLen += 1;
/* Make callback to fill the buffer. */
eRegStatus = eMBRegHoldingCB( pucFrameCur, usRegAddress, usRegCount, MB_REG_READ );
//这个地方坑了我好久啊,我草还是代码看少了,我说咋一直不对
//最后一个*pucFrameCur++之后,地址已经是指向“数据内容1高字节这个位置了”
//也就是说传给函数eMBRegHoldingCB的参数pucFrameCur地址不再是最初的 pucFrameCur = &pucFrame[0];
//而是&pucFrame[2]
//这也就是为什么modbus_app.c中的函数ReadRegsCb
//是吧获取到的内容字节数据保存在buf[2*i]而不是buf[2*(i+1)]中
/* If an error occured convert it into a Modbus exception. */
3、总结




4、演示视频
【Modbus协议实现一主多从】 https://www.bilibili.com/video/BV1cnwHeoEFY/?share_source=copy_web&vd_source=c9e86bee3ac4892e3670e748240943c6
更多推荐




所有评论(0)