FreeModbus开源协议栈的(六)FreeModbus状态机和事件总结

从FreeModbus源码中能够发现有很多状态机,了解这些状态机能更快的理解FreeModbus源码流程。下面逐个介绍各个状态机的流程和驱动机制。由于modbus有3种封包模式ASCII,RTU,TCP,这里就以最常用的RTU为例,从源码进行分析,此例程为源码中的win32例程,不过不影响探究流程。分析有不对的地方,欢迎指正

 

1. FreeModbus 串口接收状态机

先上一张串口接收状态机,下面代码也是围绕这张图展开的。

什么是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个状态,可以根据自己需要在添加吧。

什么是freemodbus的从机和主机_c语言_02

 

  • 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)

什么是freemodbus的从机和主机_单片机_03

 可以发现第一个字节和第二个字节之间相差了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,此时我们还是可以做些事情的。

什么是freemodbus的从机和主机_单片机_04

 上面就不多说了,到添加主模式功能后,如果有问题的话再更改

  • 上面第二步,如果不是发送给当前设备的数据,则状态机一直保持不变, 直到下一次发送或者接收一帧有效数据后,方能再次流转下去。
  • 上面第三步,返回的数据可能是主机请求从机的数据,也可能是从机响应主机的数据,用于主机确保从机收到校验信息。