FreeModbus开源协议栈的(六)FreeModbus状态机和事件总结
从FreeModbus源码中能够发现有很多状态机,了解这些状态机能更快的理解FreeModbus源码流程。下面逐个介绍各个状态机的流程和驱动机制。由于modbus有3种封包模式ASCII,RTU,TCP,这里就以最常用的RTU为例,从源码进行分析,此例程为源码中的win32例程,不过不影响探究流程。分析有不对的地方,欢迎指正
1. FreeModbus 串口接收状态机
先上一张串口接收状态机,下面代码也是围绕这张图展开的。
上面工作后会循环执行3->5
,当发生3->4->5
,说明主机发送数据太快了。不过发生异常后,仍然会循环执行3->5
1.1.系统后设置工作状态为STATE_RX_INIT
首先先选择一种工作模式,三种工作模式由宏来控制,这里使能MB_RTU_ENABLED
宏即可,关闭另外2个宏即可。可以看到RTU相关的方法都注册到mb.c中了。
//mb.c
eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity )
{
eMBErrorCode eStatus = MB_ENOERR;
//此处省略n行代码
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
针对接收状态,需要关注xMBRTUReceiveFSM(),eMBRTUReceive()
- xMBRTUReceiveFSM() 此接口为接受单个字符的状态机,当串口接收到一个字符都会调用此接口。下面先看一下调用流程.这段代码是win32例程中的,实际上可以把xMBRTUReceiveFSM接收中断服务程序中。这里win32从文件中读取数据
xMBPortSerialPoll( )
{
BOOL bStatus = TRUE;
DWORD dwBytesRead;
DWORD dwBytesWritten;
DWORD i;
while( bRxEnabled ) //rtu初始化时,会先初始化成接收状态
{
/* buffer wrap around. */
if( uiRxBufferPos >= BUF_SIZE )//如果uiRxBufferPos 超过了BUF_SIZE,也就是说已经接收了很多数据,超过了缓存大小,则重置接收指针。
uiRxBufferPos = 0;
if( ReadFile( g_hSerial, &ucBuffer[uiRxBufferPos],
BUF_SIZE - uiRxBufferPos, &dwBytesRead, NULL ) )
{//上面从文件中读取数据,所以事先知道读多大的数据。
....
{
vMBPortLog( MB_LOG_DEBUG, _T( "SER-POLL" ),
_T( "detected end of frame (t3.5 expired.)\r\n" ) );
for( i = 0; i < dwBytesRead; i++ ) //这里做for循环接收每一个字节
{
/* Call the modbus stack and let him fill the buffers. */
( void )pxMBFrameCBByteReceived( );
}
上面只是win32的例程,不过在单片机上它们大都是这样的。直接调用
void vMBPortUARTRxISR(void) //某单片机的接收中断处理函数
{
ucBuffer[uiRxBufferPos++] = USARTX->DR; //保存本次数据
pxMBFrameCBByteReceived();
}
在字符接收中断服务调用之前,rtu启动前会打开接收功能,并打开定时器检测帧间隔功能。注意这里由于一开始没有发送字符的话,所以超时
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; //这里将接收状态,初始化为STATE_RX_INIT
vMBPortSerialEnable( TRUE, FALSE );//串口接收功能打开,发送功能关闭
vMBPortTimersEnable( );//定时器打开,对帧间隔进行检查。
EXIT_CRITICAL_SECTION( );
}
//下面这个方法当有接收串口中断产生时,就会调用。
BOOL
xMBRTUReceiveFSM( void )
{
BOOL xTaskNeedSwitch = FALSE;
UCHAR ucByte;
assert( eSndState == STATE_TX_IDLE );
/* Always read the character. *///依次从接收缓存中ucBuffer获取一个字符
( 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;
}
1.2 发生超时中断状态设置为STATE_RX_IDLE
定时器使能后,3.5个字符时间
后就超时发生超时中断。紧接着调用超时函数,注意此时eRcvState = STATE_RX_INIT
,在最后会把接受状态机设置为eRcvState = STATE_RX_IDLE
.
BOOL
xMBRTUTimerT35Expired( void )
{
BOOL xNeedPoll = FALSE;
switch ( eRcvState )
{
/* Timer t35 expired. Startup phase is finished. */
case STATE_RX_INIT: //代码会走到这里,把pool状态机设置为EV_READY
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;//设置eRcvState的状态位STATE_RX_IDLE
return xNeedPoll;
}
BOOL
xMBRTUReceiveFSM( void )
{
.....
case STATE_RX_IDLE: //此时状态为STATE_RX_IDLE
usRcvBufferPos = 0;
ucRTUBuf[usRcvBufferPos++] = ucByte;//将串口接收到的数据放到ucRTUBuf缓存中
eRcvState = STATE_RX_RCV; //状态机设置为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://下一次产生接收中断后设置为STATE_RX_RCV
if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
{
ucRTUBuf[usRcvBufferPos++] = ucByte;
}
else
{
eRcvState = STATE_RX_ERROR;//如果用户发送太快,缓冲区溢出,设置错误状态。
}
vMBPortTimersEnable( );//重新设置定时器。
break;
}
}
1.4 当数据发送完毕后,调用定时器超时方法,设置为STATE_RX_IDLE
如上面代码当接收完最后一个字符后,主机不在发送字符,此时定时器已经重新设置了。当定时器超时后,会判断当前接收状态机状态,如果接受状态机为STATE_RX_RCV
,会重新设置接收状态机状态为STATE_RX_IDLE
.
BOOL
xMBRTUTimerT35Expired( void )
{
......
case STATE_RX_RCV:
xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );//产生接收一帧的事件
break;
......
vMBPortTimersDisable( ); //这里会停止定时器,因为好长时间没人发送数据过来,
eRcvState = STATE_RX_IDLE;//设置eRcvState的状态位STATE_RX_IDLE
到了上面这一步,此时从机接收了完整的一帧。然后紧接着会调用对应功能码的方法来解析此帧数据,解析后即可执行相应的动作。
- eMBRTUReceive()
eMBErrorCode
eMBPoll( void )
{
.......
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:
.....
break;
}
eMBErrorCode
eMBRTUReceive( UCHAR * pucRcvAddress, UCHAR ** pucFrame, USHORT * pusLength )
{
BOOL xFrameReceived = FALSE;
eMBErrorCode eStatus = MB_ENOERR;
ENTER_CRITICAL_SECTION( );
assert( usRcvBufferPos < MB_SER_PDU_SIZE_MAX );
/* Length and CRC check */
if( ( usRcvBufferPos >= MB_SER_PDU_SIZE_MIN )
&& ( usMBCRC16( ( UCHAR * ) ucRTUBuf, usRcvBufferPos ) == 0 ) )
{
/* Save the address field. All frames are passed to the upper layed
* and the decision if a frame is used is done there.
*/
*pucRcvAddress = ucRTUBuf[MB_SER_PDU_ADDR_OFF];
/* Total length of Modbus-PDU is Modbus-Serial-Line-PDU minus
* size of address field and CRC checksum.
*/
*pusLength = ( USHORT )( usRcvBufferPos - MB_SER_PDU_PDU_OFF - MB_SER_PDU_SIZE_CRC );
/* Return the start of the Modbus PDU to the caller. */
*pucFrame = ( UCHAR * ) & ucRTUBuf[MB_SER_PDU_PDU_OFF];
xFrameReceived = TRUE;
}
else
{
eStatus = MB_EIO;
}
EXIT_CRITICAL_SECTION( );
return eStatus;
}
2. FreeModbus 串口发送状态机
目前freemodbus发送状态机很简单,只有2个状态,可以根据自己需要在添加吧。
- 1.从机接收到主机发送的命令后,都会返回响应帧。这数据可能是主机真正需要的数据,也可能是返回主机确认的。
- 2.数据发送完毕,即待发送数据字节数为0,发送完毕。
BOOL
xMBRTUTransmitFSM( void )
{
BOOL xNeedPoll = FALSE;
assert( eRcvState == STATE_RX_IDLE );
switch ( eSndState )
{
/* We should not get a transmitter event if the transmitter is in
* idle state. */
case STATE_TX_IDLE://当没有数据的情况下,打开接收中断,准备接收主机数据
/* enable receiver/disable transmitter. */
vMBPortSerialEnable( TRUE, FALSE );
break;
case STATE_TX_XMIT:
/* check if we are finished. */
if( usSndBufferCount != 0 )
{
xMBPortSerialPutByte( ( CHAR )*pucSndBufferCur );//将数据发送出去,例如直接赋值给串口寄存器。
pucSndBufferCur++; /* next byte in sendbuffer. */
usSndBufferCount--;
}
else
{ //当数据发送完毕,触发EV_FRAME_SENT 给MBPOLL.
xNeedPoll = xMBPortEventPost( EV_FRAME_SENT );
/* Disable transmitter. This prevents another transmit buffer
* empty interrupt. */
vMBPortSerialEnable( TRUE, FALSE );//关闭发送中断,使能接收中断。
eSndState = STATE_TX_IDLE;//重新设置状态机为STATE_TX_IDLE
}
break;
}
return xNeedPoll;
}
3. FreeModbus 帧间隔超时检测机制
超时等待是为了确保数据帧的有效性,且检测帧间隔只发生在接收数据时。如下所示为无校验,1个起始,1个停止位,8bit数据
的串口发送的0xaa,0xaa,0xaa数据。(为了便于画图,选择0xaa)
可以发现第一个字节和第二个字节之间相差了2个字符时间,不会发生超时中断。但第二个字节和第三个字节之间的间隔时间为4个字符,接收机以为数据已经发送完毕了,此时也会发生超时中断,导致接收端功能码解析函数实际收到的数据只有0xaa,0xaa两个数据,这在后续的帧检验时就会由于CRC校验失败,舍弃此帧导致数据丢失。
- 如何计算一个字符时间? 其实一个字符传输时间,很容易计算,如串口传输波特率为9600,无校验,1个起始,1个停止位,则传输一个bit为1/9600,那么传输一个字节需要的时间为10/9600s ,即1s可以传输960个字符,一个字符时间为1041.6us.那么3.5T字符时间为3645.8us,在计算后最好使用示波器确定一下。
下面是是接受状态机和超时响应函数,内容都在代码里
BOOL
xMBRTUReceiveFSM( void )
{
switch ( eRcvState )
{
......
case STATE_RX_RCV:
if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
{
ucRTUBuf[usRcvBufferPos++] = ucByte; //从串口接收缓存中取出数据
}
else
{
eRcvState = STATE_RX_ERROR; //如果超过了接收缓存,则会报错,舍弃当前帧。
}
//这里能够看到每一个字节接收后,都会重置定时器,以备下一次传输使用。
vMBPortTimersEnable( );
break;
}
.......
}
确保超时函数能够及时执行,还需要确保定时器设置的是否正确。
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;
}
4. FreeModbus poll事件机制
到这里就是最外层大循环处理事件的机制了。下图包含了主从状态机
【1】从机经历1->2->3->4
【2】当为主机时,接收从机响应数据时,仍然会走1->2->3->4。当主机发送请求帧时走5->4,发送之前,会先把请求帧打包好,然后调用rtu注册过来的发送函数,激发发送状态机,当数据发送完后,设置状态机为EV_FRAME_SENT,此时我们还是可以做些事情的。
上面就不多说了,到添加主模式功能后,如果有问题的话再更改。
- 上面第二步,如果不是发送给当前设备的数据,则状态机一直保持不变, 直到下一次发送或者接收一帧有效数据后,方能再次流转下去。
- 上面第三步,返回的数据可能是主机请求从机的数据,也可能是从机响应主机的数据,用于主机确保从机收到校验信息。