搜索
bottom↓
回复: 39

freemodbus源码分析详解

  [复制链接]

出0入0汤圆

发表于 2016-1-7 10:48:15 | 显示全部楼层 |阅读模式
本帖最后由 fakeCode 于 2016-1-7 11:09 编辑

由于文章是直接转载的,有网友反应看着比较累,我转了一个PDF文档,建议用WEB视图打开。



freemodbus源码分析详解
我这里为了方便代码浏览,用了VS2013DEMO自然用WIN32的,选用哪个DEMO进行分析也并不影响我们对FREEMODBUS的解剖。
代码组织结构

首先是modbus这个大文件下的文件:

  • ascii目录的文件是用于实现MODBUS ASCII模式的,这个在modbus里是可选实现代码比较简单,看完RTU的分析我相信你对比着自己也就看明白ASCII模式了,这将不是本文的重点。
  • funcions是与RTU的执行功能码相关的代码,主要就是读、写寄存器开关线圈之类的,根据你自己的需要在去实现里面回调,按照相应参数去执行相应功能。
  • includefreemodbus的一些定义,这里先不作分析,在看源代码的时候我们再去看每个数据结构的相关定义。
  • rtu这个文件夹就是RTU模式的实现了,本文分析重点之一。
  • port这个是移植相关port.h是移植需要的函数声明。portevent.c这个是事件队列的实现,freemodbus只是用了一个消息作为队列简单赋值处理,portother.c是一相从字节里取位等与MODBUS没多大关系的函数,portserial.c是串口移植相关函数,porttimer.c是定时器相关移植(由于RTU方式依赖时间来判断帧头帧尾),移植相关可以参见我的另一篇博文(译自官方文档)freemodbus RTU/ASCII 官方移植文档
  • mb.c这个就是modbus的应用层实现,本文分析重点之一。
Source Files目录中的demo.cpp是例程,stdafx.cppWIN32的预编译文件与modbusfree无关。
流程分析
win32main()分析,不感兴趣直接跳到mb.c一节
一切还是从main开始吧。

int
_tmain( int argc, _TCHAR * argv[] )
{
   int             iExitCode;
   TCHAR           cCh;
   BOOL            bDoExit;

   const UCHAR     ucSlaveID[] = {0xAA, 0xBB, 0xCC };

   if( eMBInit( MB_RTU, 0x01, 1, 115200, MB_PAR_EVEN ) != MB_ENOERR )
   {
       _ftprintf( stderr, _T( "%s: can't initialize modbusstack!\r\n" ), PROG );
       iExitCode = EXIT_FAILURE;
   }
   else if( eMBSetSlaveID( 0x34, TRUE, ucSlaveID, 3 ) != MB_ENOERR )
   {
       _ftprintf( stderr, _T( "%s: can't set slave id!\r\n" ), PROG);
       iExitCode = EXIT_FAILURE;
   }
   else
   {
       /* Create synchronization primitives and set the current state
        * of the thread to STOPPED.
        */
       InitializeCriticalSection( &hPollLock );
       eSetPollingThreadState( STOPPED );

       /* CLI interface. */
       _tprintf( _T( "Type 'q' for quit or 'h' for help!\r\n" ) );
       bDoExit = FALSE;
       do
       {
            _tprintf( _T( "> " ));
            cCh = _gettchar(  );
           switch ( cCh )
            {
            case _TCHAR( 'q' ):
                bDoExit = TRUE;
                break;
            case _TCHAR( 'd' ):
                eSetPollingThreadState(SHUTDOWN );
                break;
            case _TCHAR( 'e' ):
                if( bCreatePollingThread(  ) != TRUE )
                {
                    _tprintf( _T( "Can'tstart protocol stack! Already running?\r\n" ) );
                }
                break;
            case _TCHAR( 's' ):
                switch (eGetPollingThreadState(  ) )
                {
                case RUNNING:
                    _tprintf( _T("Protocol stack is running.\r\n" ) );
                    break;
                case STOPPED:
                    _tprintf( _T( "Protocolstack is stopped.\r\n" ) );
                    break;
                case SHUTDOWN:
                    _tprintf( _T("Protocol stack is shuting down.\r\n" ) );
                    break;
                }
                break;
            case _TCHAR( 'h' ):
                _tprintf( _T( "FreeModbusdemo application help:\r\n" ) );
                _tprintf( _T( "  'd' ... disable protocol stack.\r\n" ));
                _tprintf( _T( "  'e' ... enabled the protocol stack\r\n") );
                _tprintf( _T( "  's' ... show current status\r\n" ) );
                _tprintf( _T( "  'q' ... quit applicationr\r\n" ) );
                _tprintf( _T( "  'h' ... this information\r\n" ) );
                _tprintf( _T( "\r\n") );
                _tprintf( _T( "Copyright2006 Christian Walter <wolti@sil.at>\r\n" ) );
                break;
            default:
                if( cCh != _TCHAR('\n') )
                {
                    _tprintf( _T( "illegalcommand '%c'!\r\n" ), cCh );
                }
                break;
            }

            /* eat up everything untill returncharacter. */
            while( cCh != '\n' )
            {
                cCh = _gettchar(  );
            }
       }
       while( !bDoExit );

       /* Release hardware resources. */
       ( void )eMBClose(  );
       iExitCode = EXIT_SUCCESS;
   }
   return iExitCode;
}

eMBInit( MB_RTU,0x01, 1, 115200, MB_PAR_EVEN ) != MB_ENOERR 初始化modbus协议栈,如果实始化失败则打印错误信息并退出,否则打印命令提示符,要求输入指令。

       do
       {
            _tprintf( _T( "> " ));
            cCh = _gettchar(  );
            switch ( cCh )
            {
            case _TCHAR( 'q' ):
                bDoExit = TRUE;
                break;
            case _TCHAR( 'd' ):
                eSetPollingThreadState(SHUTDOWN );
                break;
            case _TCHAR( 'e' ):
                if( bCreatePollingThread(  ) != TRUE )
                {
                    _tprintf( _T( "Can'tstart protocol stack! Already running?\r\n" ) );
                }
                break;
            case _TCHAR( 's' ):
                switch (eGetPollingThreadState(  ) )
                {
                case RUNNING:
                    _tprintf( _T("Protocol stack is running.\r\n" ) );
                    break;
                case STOPPED:
                    _tprintf( _T("Protocol stack is stopped.\r\n" ) );
                    break;
                case SHUTDOWN:
                    _tprintf( _T("Protocol stack is shuting down.\r\n" ) );
                   break;
                }
                break;
            case _TCHAR( 'h' ):
                _tprintf( _T( "FreeModbusdemo application help:\r\n" ) );
                _tprintf( _T( "  'd' ... disable protocol stack.\r\n" ));
               _tprintf( _T( "  'e' ... enabled the protocol stack\r\n") );
                _tprintf( _T( "  's' ... show current status\r\n" ) );
                _tprintf( _T( "  'q' ... quit applicationr\r\n" ) );
                _tprintf( _T( "  'h' ... this information\r\n" ) );
                _tprintf( _T( "\r\n") );
                _tprintf( _T( "Copyright2006 Christian Walter <wolti@sil.at>\r\n" ) );
                break;
            default:
                if( cCh != _TCHAR('\n') )
                {
                    _tprintf( _T( "illegalcommand '%c'!\r\n" ), cCh );
                }
                break;
            }

            /* eat up everything untill returncharacter. */
            while( cCh != '\n' )
            {
                cCh = _gettchar(  );
            }
       }
       while( !bDoExit );

如果用户输入e,则会调用bCreatePollingThread( )启动协议栈线程。那么我们跟进bCreatePollingThread()去看看。

BOOL
bCreatePollingThread( void )
{
   BOOL            bResult;

   if( eGetPollingThreadState(  ) ==STOPPED )
   {
       if( ( hPollThread = CreateThread( NULL, 0, dwPollingThread, NULL, 0,NULL ) ) == NULL )
       {
            /* Can't create the polling thread.*/
            bResult = FALSE;
       }
       else
       {
            bResult = TRUE;
       }
   }
   else
   {
       bResult = FALSE;
   }

   return bResult;
}

先是确认一下线程状态,然后创建并启动线程函数dwPollingThread(),

DWORD           WINAPI
dwPollingThread( LPVOID lpParameter )
{
   eSetPollingThreadState( RUNNING );

   if( eMBEnable(  ) == MB_ENOERR )
   {
       do
       {
            if( eMBPoll(  ) != MB_ENOERR )
                break;
       }
       while( eGetPollingThreadState(  )!= SHUTDOWN );
   }

   ( void )eMBDisable(  );

   eSetPollingThreadState( STOPPED );

   return 0;
}

从这里就跟MCU\ARM上应用freemodbus一样一样的了,无法是先使能协议栈,然后循环调用eMBPoll( ),同时用eGetPollingThreadState()检测线程状态。eMBPoll( void )就是我们的重点咯,我们现在已经进入mb.c这个文件啦,这个是freemodbus实现的modbus应用层,虽然代码里面对数据链路层以及应用层分的不是很清晰,但这个mb.c是完完全全的应用层了。
mb.c

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 )
   {
       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:
            break;

       case EV_FRAME_RECEIVED:
            eStatus = peMBFrameReceiveCur(&ucRcvAddress, &ucMBFrame, &usLength );
            if( eStatus == MB_ENOERR )
            {
                /* Check if the frame is forus. 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 handlersregistered. Abort. */
                if(xFuncHandlers.ucFunctionCode == 0 )
                {
                    break;
                }
               else if(xFuncHandlers.ucFunctionCode == ucFunctionCode )
                {
                    eException =xFuncHandlers.pxHandler( ucMBFrame, &usLength );
                    break;
                }
            }

            /* If the request was not sent tothe 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;
}

eMBPoll()就是一个状态机。它只有下面四种状态:

typedef enum
{
   EV_READY,                  /*!< Startup finished. */
    EV_FRAME_RECEIVED,          /*!< Frame received. */
   EV_EXECUTE,                /*!< Execute function. */
   EV_FRAME_SENT              /*!< Frame sent. */
} eMBEventType;

从注释中可以看出,分别是启动完成,帧接收完成,执行功能码,执行帧发送。
这个状态机通过xMBPortEventGet( &eEvent ) 获取事件状态,而事件状态的投递方是谁呢?这里我们先不关注(咱们自上向下分析吧)。我们先分析一下这个状态机的流程。
由于我在写这篇文章之前做过功课,所以比较清楚,这里大家过一下就可以了。
在整个协议栈运行的最初肯定是EV_READY态,然后过了一个3.5T(这个就是modbus的帧头帧尾确认时间啦,不清楚?去翻翻协议吧,我当然不建议你去读国人写的那些“modbus协议整理之类的葵花宝典,而是建议你去modbus官网下载。找不到下载链接?看这里Modbus Specifications and Implementation Guides,点那个I Accept就可以进去啦。)如果这个时候接收到一个完整的帧那么就会进入EV_FRAME_RECEIVED态,至于是谁负责去接收和检验帧我们后面再去理,你要记住我们还在应用层里打转转。

                /* Check if theframe is for us. If not ignore the frame. */
                if( ( ucRcvAddress ==ucMBAddress ) || ( ucRcvAddress == MB_ADDRESS_BROADCAST ) )
                {
                    ( void )xMBPortEventPost(EV_EXECUTE );
                }

EV_READY态如果检测收到的地址跟从机地址(freemodbus的开源版本只支持从机,如果你想要主机的可以参考一下FreeModbus_Slave-Master-RTT-STM32)匹配,或是广播地址就自己给自己投递一个EV_EXECUTE 事件。

        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 handlersregistered. Abort. */
                if(xFuncHandlers.ucFunctionCode == 0 )
                {
                    break;
                }
                else if(xFuncHandlers.ucFunctionCode == ucFunctionCode )
                {
                    eException =xFuncHandlers.pxHandler( ucMBFrame, &usLength );
                    break;
                }
            }

            /* If the request was not sent tothe 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;

EV_EXECUTE的第一段就是执行相应的功能码回调,也就是读写寄存器或者是打开线圈什么的,实现上就是执行mbfunctions里面的代码,因为在协议栈初始化的时候这些文件里面的函数都被值给了xFuncHandlers[],去看看xFuncHandlers[]的定义吧。

/* An array of Modbus functions handlers which associates Modbus function
*codes with implementing functions.
*/
static xMBFunctionHandlerxFuncHandlers[MB_FUNC_HANDLERS_MAX] = {
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED> 0
   {MB_FUNC_OTHER_REPORT_SLAVEID, eMBFuncReportSlaveID},
#endif
#if MB_FUNC_READ_INPUT_ENABLED > 0
   {MB_FUNC_READ_INPUT_REGISTER, eMBFuncReadInputRegister},
#endif
#if MB_FUNC_READ_HOLDING_ENABLED > 0
   {MB_FUNC_READ_HOLDING_REGISTER, eMBFuncReadHoldingRegister},
#endif
#ifMB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED > 0
   {MB_FUNC_WRITE_MULTIPLE_REGISTERS, eMBFuncWriteMultipleHoldingRegister},
#endif
#if MB_FUNC_WRITE_HOLDING_ENABLED > 0
   {MB_FUNC_WRITE_REGISTER, eMBFuncWriteHoldingRegister},
#endif
#if MB_FUNC_READWRITE_HOLDING_ENABLED> 0
   {MB_FUNC_READWRITE_MULTIPLE_REGISTERS,eMBFuncReadWriteMultipleHoldingRegister},
#endif
#if MB_FUNC_READ_COILS_ENABLED > 0
   {MB_FUNC_READ_COILS, eMBFuncReadCoils},
#endif
#if MB_FUNC_WRITE_COIL_ENABLED > 0
   {MB_FUNC_WRITE_SINGLE_COIL, eMBFuncWriteCoil},
#endif
#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED> 0
   {MB_FUNC_WRITE_MULTIPLE_COILS, eMBFuncWriteMultipleCoils},
#endif
#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED> 0
   {MB_FUNC_READ_DISCRETE_INPUTS, eMBFuncReadDiscreteInputs},
#endif
};

看到这里你就明白了xFuncHandlers不过是一个功能码和功能回调函数的对应表,eMBFuncWriteHoldingRegister()就是写保持寄存器回调。我们还是接着看EV_EXECUTE,第一段里面需要注意if( xFuncHandlers.ucFunctionCode == 0 )这一句是用来在结束遍历表, freemodbus提供了一个eMBRegisterCB()eMBRegisterCB函数专门用来注册功能码和与之相应的回调,但是对于不响应的功能码freemodbus通过xFuncHandlers.ucFunctionCode = 0;将其直接置0
EV_EXECUTE第二段就是对主机作出回应。讲到这里接收处理就讲完了。在mb.c中我们可以看到这一层并不对EV_FRAME_SENT作处理。
mbrtu.c分析
mb.c里面我们留了一个疑惑,是谁在投递事件?或者说是谁在改变mb.c里面状态机的状态?
如果是RTU模式,那么就是这mbrtu.c里面的这个函数了

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;
}

这个函数是被vMBPortTimerPoll()被调用的,vMBPortTimerPoll()又是被xMBPortEventGet()调用的,这里我们看一下vMBPortTimerPoll()是在什么情况下调用xMBRTUTimerT35Expired

void
   vMBPortTimerPoll(  )
{

   /* Timers are called from the serial layer because we have no high
   * res timer in Win32. */
   if( bTimeoutEnable )
   {
       DWORD           dwTimeCurrent =GetTickCount(  );

       if( ( dwTimeCurrent - dwTimeLast ) > dwTimeOut )
       {
            bTimeoutEnable = FALSE;
            ( void)pxMBPortCBTimerExpired(  );
       }
   }
}

可以看到就当系统的tickCount间隔达到一定时间时就调用xMBRTUTimerT35Expired()(pxMBPortCBTimerExpiredeMBInit()中被赋值为xMBRTUTimerT35Expired),简单点说吧,就相当于单片机定时器中断函数,定时执行xMBRTUTimerT35Expired()函数。
回到mbrtu.c中来吧,跟踪的第一要点是不能迷路,方向感要好!
xMBRTUTimerT35Expired就是根据eRcvState的不同状态来投递不同的事件给mb.c中的eMBPoll()这个状态机。而eRcvState又是怎么来的呢?在xMBRTUReceiveFSM()中我们看到了它。

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;
}

这里不兜圈子,直接告诉你xMBRTUReceiveFSM会在串口接收函数中被调用(虽然在这个WIN32例程中并没有中断例程)。我们这里主要分析一下xMBRTUReceiveFSM的流程。
首先xMBRTUReceiveFSM会进入STATE_RX_INIT态,这个时候它调用vMBPortTimersEnable开启定时器,当达到3.5T时间后xMBRTUTimerT35Expired会让 eRcvState =STATE_RX_IDLE,这样xMBRTUReceiveFSM会进入STATE_RX_IDLE态,在STATE_RX_IDLE态一旦通过xMBPortSerialGetByte收到了一个字符,那么就会 进入STATE_RX_RCV态,在这里就是持续的接收字符同时进行两种检测,一种是如果接收的字符超过了MB_SER_PDU_SIZE_MAXRTU帧的最大值)就会进入STATE_RX_ERROR态,另一种就是检测是否超时,vMBPortTimersEnable()就是用来清零定时器的。如果超时则会由xMBRTUTimerT35Expiredmb.c状态机投递一个EV_FRAME_RECEIVED帧结束事件,这个时候帧数据就会被交给mb.c中的状态机去处理。在xMBRTUTimerT35Expired退出前会再次将xMBRTUReceiveFSM的状态置为STATE_RX_IDLE空闲态。
至此从上到下整个接收流程都理清楚了。那么我再看一看发送流程吧,这个比较轻松。

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
       {
            xNeedPoll = xMBPortEventPost(EV_FRAME_SENT );
            /* Disable transmitter. Thisprevents another transmit buffer
             * empty interrupt. */
            vMBPortSerialEnable( TRUE, FALSE );
            eSndState = STATE_TX_IDLE;
       }
       break;
   }

   return xNeedPoll;
}

xMBRTUTransmitFSMeMBInit()中被赋值给了pxMBFrameCBTransmitterEmpty,而pxMBFrameCBTransmitterEmpty又被xMBPortSerialPoll调用,最后xMBPortSerialPollxMBPortEventGet中被调用。
xMBRTUTransmitFSM只有两个状态。

typedef enum
{
   STATE_TX_IDLE,             /*!< Transmitter is in idle state. */
   STATE_TX_XMIT              /*!< Transmitter is in transfer state. */
} eMBSndState;

在没有发送任务的时候,它是处理STATE_TX_IDLE态,在modbus协议栈初始化的时候它就是这个态,而这个STATE_TX_XMIT发送态则是用来将要发送的数据推送到发送缓冲的(这里你可以用你的串口中断来做,但我觉得用DMA会更好一些),发送完数据后又返回到STATE_TX_IDLE态,但是STATE_TX_XMIT是谁让它进入的呢?

eMBErrorCode
eMBRTUSend( UCHAR ucSlaveAddress, constUCHAR * pucFrame, USHORT usLength )
{
   eMBErrorCode    eStatus =MB_ENOERR;
   USHORT          usCRC16;

    ENTER_CRITICAL_SECTION(  );

   /* Check if the receiver is still in idle state. If not we where to
    * slow with processing the received frame and the master sent another
    * frame on the network. We have to abort sending the frame.
    */
   if( eRcvState == STATE_RX_IDLE )
   {
       /* First byte before the Modbus-PDU is the slave address. */
       pucSndBufferCur = ( UCHAR * ) pucFrame - 1;
       usSndBufferCount = 1;

       /* Now copy the Modbus-PDU into the Modbus-Serial-Line-PDU. */
       pucSndBufferCur[MB_SER_PDU_ADDR_OFF] = ucSlaveAddress;
       usSndBufferCount += usLength;

       /* Calculate CRC16 checksum for Modbus-Serial-Line-PDU. */
       usCRC16 = usMBCRC16( ( UCHAR * ) pucSndBufferCur, usSndBufferCount );
       ucRTUBuf[usSndBufferCount++]= ( UCHAR )( usCRC16 & 0xFF );
       ucRTUBuf[usSndBufferCount++] = ( UCHAR )( usCRC16 >> 8 );

       /* Activate the transmitter. */
       eSndState = STATE_TX_XMIT;
       vMBPortSerialEnable( FALSE, TRUE );
   }
   else
   {
       eStatus = MB_EIO;
   }
   EXIT_CRITICAL_SECTION(  );
   return eStatus;
}


这个eMBRTUSend就是用来将xMBRTUTransmitFSM置为STATE_TX_XMIT的函数,同时它还使能串口发送功能。eMBRTUSend本身却是在eMBPoll()的EV_EXECUTE状态的第二段被调用的,就是当收到功能码时我们回应给主机的这一部分。

            /* If the request wasnot 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;

其中的peMBFrameSendCur()就是eMBRTUSend(),在eMBInit我们将eMBRTUSend赋值给了peMBFrameSendCur()。 现在咱们终于绕出来了,发送流程也介绍清楚了。
写到这里,我估计你可能会有一些疑惑,在这个例程中真正完成发送和接收串口的代码在哪里?

BOOL
xMBPortEventGet( eMBEventType * eEvent )
{
   BOOL            xEventHappened =FALSE;

   if( xEventInQueue )
   {
       *eEvent = eQueuedEvent;
       xEventInQueue = FALSE;
       xEventHappened = TRUE;
   }
   else
   {        
       /* Poll the serial device. The serial device timeouts if no
        * characters have been received within for t3.5 during an
        * active transmission or if nothing happens within a specified
        * amount of time. Both timeouts are configured from the timer
        * init functions.
        */
       ( void )xMBPortSerialPoll( );  

       /* Check if any of the timers have expired. */
       vMBPortTimerPoll(  );

   }
   return xEventHappened;
}

其实它们就在xMBPortSerialPoll里,换句话说,每次当mb.c的状态机调用xMBPortEventGet()都在进行串口操作,要么是发送要么是接收。

BOOL
xMBPortSerialPoll(  )
{
   BOOL            bStatus = TRUE;
   DWORD           dwBytesRead;
   DWORD           dwBytesWritten;
   DWORD           i;

   while( bRxEnabled )
   {
       /* buffer wrap around. */
       if( uiRxBufferPos >= BUF_SIZE )
            uiRxBufferPos = 0;

       if( ReadFile( g_hSerial, &ucBuffer[uiRxBufferPos],
                      BUF_SIZE - uiRxBufferPos,&dwBytesRead, NULL ) )
       {
            if( dwBytesRead == 0 )
            {
                /* timeout with no bytes. */
                break;
            }
            else if( dwBytesRead > 0 )
            {
                vMBPortLog( MB_LOG_DEBUG, _T("SER-POLL" ),
                           _T("detected end of frame (t3.5 expired.)\r\n" ) );
                for( i = 0; i < dwBytesRead;i++ )
                {
                    /* Call the modbus stackand let him fill the buffers. */
                    ( void )pxMBFrameCBByteReceived(  );
                }
            }
       }
       else
       {
            vMBPortLog( MB_LOG_ERROR, _T("SER-POLL" ), _T( "I/O error on serial device: %s" ),
                        Error2String(GetLastError ( ) ) );
            bStatus = FALSE;
       }
   }
   if( bTxEnabled )
   {
       while( bTxEnabled )
       {
            ( void)pxMBFrameCBTransmitterEmpty(  );
            /* Call the modbus stack to let himfill the buffer. */
       }
       dwBytesWritten = 0;
        if( !WriteFile
            ( g_hSerial, &ucBuffer[0],uiTxBufferPos, &dwBytesWritten, NULL )
            || ( dwBytesWritten !=uiTxBufferPos ) )
       {
            vMBPortLog( MB_LOG_ERROR, _T("SER-POLL" ), _T( "I/O error on serial device: %s" ),
                        Error2String(GetLastError ( ) ) );
            bStatus = FALSE;
       }
   }

   return bStatus;
}

xMBPortSerialPoll依据bRxEnabled bTxEnabled 来区分到底是发送还是接收。
我看到有些人说freemodbus只能通过阻塞方式发送和接收串口数据很显然是错误的,它可以用普通串口中断或者是串口DMA来做。
写第一版的时候忘了分析一下事件队列,虽然说是叫事件队列,其实就是很简单的对一个变量进行了封装,提供了抽象接口,代码也只有这么几行:

/* ----------------------- Variables----------------------------------------*/
static eMBEventType eQueuedEvent;
static BOOL     xEventInQueue;

/* ----------------------- Startimplementation -----------------------------*/
BOOL
xMBPortEventInit( void )
{
   xEventInQueue = FALSE;
   return TRUE;
}

BOOL
xMBPortEventPost( eMBEventType eEvent )
{
   xEventInQueue = TRUE;
   eQueuedEvent = eEvent;
   return TRUE;
}

BOOL
xMBPortEventGet( eMBEventType * eEvent )
{
   BOOL            xEventHappened =FALSE;

   if( xEventInQueue )
   {
       *eEvent = eQueuedEvent;
       xEventInQueue = FALSE;
       xEventHappened = TRUE;
   }
   else
   {        
       /* Poll the serial device. The serial device timeouts if no
        * characters have been received within for t3.5 during an
        * active transmission or if nothing happens within a specified
        * amount of time. Both timeouts are configured from the timer
        * init functions.
        */
       ( void )xMBPortSerialPoll( );  

       /* Check if any of the timers have expired. */
       vMBPortTimerPoll(  );

   }
   return xEventHappened;
}


xMBPortEventPost这个投递事件的函数只是将事件枚举赋值给这个模块的变量,同时将xEventInQueue置为真表示队列中有数据,xMBPortEventGet的逻辑稍微复杂一点,它会在eMBPoll状态机中被反复调用,它首先将xEventInQueue置为FALSE,然后如果队列中有数据将就队列中的数字赋给eMBPoll传入的指针,没有事件的话就进行一下串口的接收和发送处理。
注意freemodbus并没有用到T1.5(同一帧内两个字符之间的最大时间间隔)检测,你可以去看源代码里面xMBRTUTimerT15Expired这个函数仅仅只是声明了,我个人猜测是因为T1.5这个时间粒度太小(波特率为19200,按协议t1.5取为750us),一般的MCU根本没精力去做这个检测。

最后提一下asc模式,在eMBInit()函数中我们看到如果你的初时化时候的选择MB_ASCII作为参数,与modbus协议相关的回调和状态都会被替换成maasiic中的内容,顺着这条藤去摸一下ASC模式的瓜应该不难。

#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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入0汤圆

 楼主| 发表于 2016-1-7 10:51:08 | 显示全部楼层
本来觉得移植就够了,移植网上有现成的一大堆,分析不分析的网上也一大堆了,但是为了自己重写和近期项目需要能有把握地进行修改还是写了篇博客以作备忘,欢迎拍砖,哈哈。

出0入0汤圆

发表于 2016-1-7 10:53:25 | 显示全部楼层
字太小了,看的累

出0入0汤圆

 楼主| 发表于 2016-1-7 11:10:51 | 显示全部楼层
powermeter 发表于 2016-1-7 10:53
字太小了,看的累

传了个PDF上去,本来是MARKDOWN写的,拷到WORD里变小了,在文章的开头。

出0入0汤圆

发表于 2016-1-7 11:42:00 | 显示全部楼层
辛苦。谢谢。

在用,就是移植上用了,代码没细看。

出0入0汤圆

发表于 2016-1-7 11:53:17 | 显示全部楼层
感谢分享!!!

出0入0汤圆

发表于 2016-1-7 12:15:57 | 显示全部楼层
这个不错,移植了但是没有细看呢。

出0入0汤圆

 楼主| 发表于 2016-1-7 13:42:39 | 显示全部楼层
buliaoqqlp 发表于 2016-1-7 12:15
这个不错,移植了但是没有细看呢。


细看一下,用得心里才安,即便是不重写。另外freemodbus本身限制很多,免不了将来要重写。

出0入0汤圆

 楼主| 发表于 2016-1-7 21:56:04 | 显示全部楼层
Excellence 发表于 2016-1-7 11:42
辛苦。谢谢。

在用,就是移植上用了,代码没细看。

也是自己要用,为用得放心就看了看,谈不上辛苦!

出0入0汤圆

 楼主| 发表于 2016-1-7 21:56:47 | 显示全部楼层

不错就点收藏吧,哈哈。

出0入0汤圆

发表于 2016-1-8 08:45:49 | 显示全部楼层
感谢分享!!!

出0入0汤圆

 楼主| 发表于 2016-1-8 08:52:34 | 显示全部楼层

不谢,自己写下来也是怕好记性不如烂笔头。

出0入0汤圆

发表于 2016-1-10 16:34:33 | 显示全部楼层
先收藏,有空研究下

出0入0汤圆

 楼主| 发表于 2016-1-10 17:55:13 | 显示全部楼层
mll2015 发表于 2016-1-10 16:34
先收藏,有空研究下

哈哈,记得好久以前阿莫这个论坛是没有收藏功能的。那时找个贴子麻烦死了。

出0入0汤圆

发表于 2016-4-25 13:38:46 | 显示全部楼层
顶一下            

出0入0汤圆

发表于 2016-4-25 14:30:25 | 显示全部楼层
mark一下

出0入4汤圆

发表于 2016-4-25 14:57:35 | 显示全部楼层
已收藏~~~~

出0入0汤圆

发表于 2016-6-17 17:57:04 | 显示全部楼层
不错不错

出0入0汤圆

发表于 2016-6-17 22:24:48 | 显示全部楼层
fakeCode 发表于 2016-1-10 17:55
哈哈,记得好久以前阿莫这个论坛是没有收藏功能的。那时找个贴子麻烦死了。 ...

www.hex55.com是你的博客吗? 我能把你里面所有文章都转载吗? 当然,一开始必须注意转载,并标上你的博客网址!
其实也是纪录,主要是作一个备份,不晓得哪天你的独立博客关门了,不就全歇菜了;

不知可否?



出0入0汤圆

 楼主| 发表于 2016-6-22 17:05:34 | 显示全部楼层
kinsno 发表于 2016-6-17 22:24
www.hex55.com是你的博客吗? 我能把你里面所有文章都转载吗? 当然,一开始必须注意转载,并标上你的博 ...

可以转载,欢迎转载。

出0入0汤圆

发表于 2016-6-22 17:18:58 | 显示全部楼层
还没用过呢,了解下。

出0入0汤圆

发表于 2016-7-19 08:38:20 | 显示全部楼层
近期着手写个PC端调试PLC的Modbus软件,感谢你的分享。
PS: 请问,FreeModbus里有带VS2010的工程么,在哪里下载。

出0入0汤圆

 楼主| 发表于 2016-7-20 14:41:58 | 显示全部楼层
automation0406 发表于 2016-7-19 08:38
近期着手写个PC端调试PLC的Modbus软件,感谢你的分享。
PS: 请问,FreeModbus里有带VS2010的工程么,在哪里 ...

没有MFC,VS工程,但是网上有开源的QT工程,名为qmodbus,在VS下编译方法详见:
qmodbus在visual-studio中的编译

出0入0汤圆

发表于 2016-8-4 21:59:30 | 显示全部楼层
你好  请问   xMBPortEventGet( eMBEventType * eEvent )中以下代码是你自己写的吗  我从徐凯那个帖子里下载的代码里没有这个,移植之后也能用啊
  else
    {        
        /* Poll the serial device. The serial device timeouts if no
         * characters have been received within for t3.5 during an
         * active transmission or if nothing happens within a specified
         * amount of time. Both timeouts are configured from the timer
         * init functions.
         */
        ( void )xMBPortSerialPoll(  );  

        /* Check if any of the timers have expired. */
        vMBPortTimerPoll(  );

    }

xMBPortSerialPoll
vMBPortTimerPoll
没找到这两个函数

出0入0汤圆

 楼主| 发表于 2016-8-5 09:19:26 | 显示全部楼层
464050032 发表于 2016-8-4 21:59
你好  请问   xMBPortEventGet( eMBEventType * eEvent )中以下代码是你自己写的吗  我从徐凯那个帖子里下 ...

不是我写的,是官方源码里面的。

出0入0汤圆

 楼主| 发表于 2016-8-5 09:21:10 | 显示全部楼层
本帖最后由 fakeCode 于 2016-8-5 09:22 编辑
464050032 发表于 2016-8-4 21:59
你好  请问   xMBPortEventGet( eMBEventType * eEvent )中以下代码是你自己写的吗  我从徐凯那个帖子里下 ...


这两个函数是在PC上实现的函数,一个是串口处理,另一个是作定时检查,注释写得很明白。

出0入0汤圆

发表于 2016-8-5 11:26:18 | 显示全部楼层
空研究下

出0入0汤圆

发表于 2016-8-5 14:50:30 | 显示全部楼层
fakeCode 发表于 2016-8-5 09:21
这两个函数是在PC上实现的函数,一个是串口处理,另一个是作定时检查,注释写得很明白。 ...

好的 我再仔细看看
谢谢啦

出0入0汤圆

发表于 2016-8-30 11:05:11 | 显示全部楼层
楼主辛苦了

出0入0汤圆

发表于 2016-8-30 11:51:25 | 显示全部楼层
Mark freemodebus

出0入0汤圆

发表于 2016-8-30 12:14:28 | 显示全部楼层
楼主辛苦了

出0入0汤圆

发表于 2017-7-12 10:47:01 | 显示全部楼层
看一下,谢谢分享

出0入0汤圆

发表于 2018-1-4 23:57:34 | 显示全部楼层
研究一下,提高一下自己写代码的质量。

出0入0汤圆

发表于 2018-9-8 10:26:17 | 显示全部楼层
hex55我打开看到少儿不宜的内容........

出0入0汤圆

 楼主| 发表于 2018-9-8 12:59:18 | 显示全部楼层
eyancool 发表于 2018-9-8 10:26
hex55我打开看到少儿不宜的内容........

域名过期了。

出0入0汤圆

发表于 2018-9-8 13:51:38 | 显示全部楼层
http://www.hex55.com/

这是部车,动车

出0入0汤圆

发表于 2018-12-19 20:24:46 | 显示全部楼层
谢谢分享,学习了

出0入0汤圆

发表于 2019-7-12 09:35:17 | 显示全部楼层
感谢楼主分享

出0入0汤圆

发表于 2019-8-15 21:00:19 | 显示全部楼层
没有移植 DMA 的吗 ? 一个字节一个字节的接收浪费 CPU。

出0入0汤圆

发表于 2019-8-15 23:28:10 | 显示全部楼层
感谢分享!!!
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-27 05:30

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表