STM32F10x 单片机移植Modbus

最近在做STM32F10x 单片机移植Modbus的工作。Modbus是串口通信中广泛使用的协议,为了将其移植到stm32f10x单片机中,我们通过其开源实现freeModbus 进行了协议分析。stm32f10x 是一款被广泛使用的单片机,具有能耗小、价格低、速度快和中断响应迅速等优点。

首先,本文主要对Modbus 协议分析,有少量涉及到stm32单片机的内容;其次,文章主要以从机作为分析对象。(主从机是通信的一种模式,主机用于发送指令或者查询数据,从机用于应答)。另外Modbus 只有从机开源。

文章中参考了一些网友的资料,如有需要,可以参考下面文章:

  1. STM32单片机嵌入式实战教程 (stm32单片机视频讲解)
  2. Freemodbus RTU在stm32上的移植分析    (包含Modbus 发送接收状态机)
  3. freeModbus 主机的改写 (包含主机部分代码)
  4. http://www.freemodbus.org/ (freeModbus 官网)

下面按照几个方面进行协议分析:

一、单片机上串口通信的数据帧

单片机上串口通信通过数据帧完成,但必须规定一帧的开始和结束才能够进行正常的数据读取。简单的方法就是在帧的两端定义特殊的开始字节和结束字节。但这样的话,要求数据部分不能出现与开始字节和结束字节相同的内容,且帧的开始结束都是由应用程序判断。Modbus使用更“硬件”的方法,它通过两个帧之间的时间间隔来判断一个帧,即如果两个字节相隔一段时间(如传输3.5个字符时间),则认为新的的数据是新的一帧。

二、Modbus 启动流程

我们以freeModbus 为例进行分析(从参考[4] 可以下载到freeModbus 的源代码)。源码中mb.c是Modbus 的主要接口函数,include 目录是头文件,tcp/ascii/rtu 是Modbus 支持的三种传输模式,rtu 是最常用的,本文只涉及rtu模式。

主函数main() 中包含三个部分:

eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );
eMBInit() 是初始化函数,使用“模式,端口,地址,波特率,校验”几个参数初始化系统硬件相关信息。

eStatus = eMBEnable(  );
使能函数,使得Modbus 整个协议栈工作起来。具体会做开启一些中断等工作。

for( ;; ){
( void )eMBPoll( );
}

这是从机的主要工作部分,系统经过简单的配置和使能后,就进入eMBpoll()这样的服务模式,类似于Linux 中I/O多路复用的epoll(),当有事件发生时则做出相应响应。

整个Modbus工作流程如下图,也可以看出初始化、使能和eMBPoll()服务三个部分。

untitled-flowchart

初始化:eMBInit()在mb.c中根据定义的是rtu 还是asccii等模式会给接口函数(函数指针)进行初始化,这样的好处是对应于不同的模式使用统一的接口函数。当模式为rtu时:

pvMBFrameStartCur = eMBRTUStart;     \\开始接收数据帧
pvMBFrameStopCur = eMBRTUStop;       \\停止接收数据帧
peMBFrameSendCur = eMBRTUSend;       \\数据帧已经发送
peMBFrameReceiveCur = eMBRTUReceive; \\数据帧接收完毕
pvMBFrameCloseCur = MB_PORT_HAS_CLOSE ? vMBPortClose : NULL;
pxMBFrameCBByteReceived = xMBRTUReceiveFSM;       \\接收到数据帧内一个Byte
pxMBFrameCBTransmitterEmpty = xMBRTUTransmitFSM;   \\接收到空
pxMBPortCBTimerExpired = xMBRTUTimerT35Expired;    \\发生T35 中断

其中,然后就进入了rtu模式下的初始化eMBRTUInit(),其中主要是对串口中断和时钟中断进行的初始化(xMBPortSerialInit()和xMBPortTimersInit())。串口中断会开启相应的RCC时钟,配置GPIO口,配置串口模式(速率等),这和一般的单片机串口通信初始化类似;时钟中断初始化也是要开启相应的RCC时钟,设置时钟模式。需要指出:

Modbus用3.5个字符传输时间间隔作为隔断数据的判断依据,即从时钟中断使能开始,3.5个字符传输时间发起一次中断,记作T35

但当速率超过19200时,则使用固定的时间作为中断依据,这些都在eMBRTUInit()源码中有注释。

使能:eMBEnable()是Modbus 的使能函数,主要是通过函数pvMBFrameStartCur()(rtu模式下指向eMBRTUStart())进行串口中断和时钟中断使能。使能就是使得相应功能能够正常开始工作了。

基于事件的服务模式:eMBPoll()用于循环询问是否有新来事件,有则处理。

三、基于事件驱动的服务模式

从eMBPoll() 源码可以看出:

eMBpoll() 使得整个系统处于一个事件驱动的工作模式,当有新事件到达则处理它,否则就一直循环询问。

进入mb.c 中的eMBpoll() 函数可以看到,首先使用

xMBPortEventGet( &eEvent )

获取可能的事件信息,可能的事件有:

  • EV_READY:准备事件,可以开始接收数据了;
  • EV_FRAME_RECEIVED:数据帧到达事件,如果是发给自己的数据帧,则发起一个EV_EXECUTE 的事件;
  • EV_EXECUTE:执行事件,根据已定义的功能码和功能函数对数据帧进行相应处理 (eException = xFuncHandlers[i].pxHandler( ucMBFrame, &usLength ););
  • EV_FRAME_SENT:数据帧已经发送状态

那么什么时候系统会产生事件呢:中断和状态改变。当发生中断时,中断处理函数可能修改系统所处状态,并发起事件。系统中可以使用xMBPortEventPost() 发起事件。

四、各种状态

系统设置了几类状态,如eMBErrorCode 类型的eStatus 表示整个系统的总体状态,有下面几种:

  • MB_ENOERR,            /*!< no error. */
  • MB_ENOREG,            /*!< illegal register address. */
  • MB_EINVAL,               /*!< illegal argument. */
  • MB_EPORTERR,        /*!< porting layer error. */
  • MB_ENORES,             /*!< insufficient resources. */
  • MB_EIO,                      /*!< I/O error. */
  • MB_EILLSTATE,          /*!< protocol stack in illegal state. */
  • MB_ETIMEDOUT        /*!< timeout error occurred. */

eMBState 表示Modbus 协议在系统中的状态,有以下几种:

  • STATE_ENABLED
  • STATE_DISABLED
  • STATE_NOT_INITIALIZED

另外还有数据接收流程和数据发送流程中的不同状态,数据接收状态eRcvState有:

  • STATE_RX_INIT,         /*!< Receiver is in initial state. */
  • STATE_RX_IDLE,        /*!< Receiver is in idle state. */
  • STATE_RX_RCV,         /*!< Frame is beeing received. */
  • STATE_RX_ERROR     /*!< If the frame is invalid. */

数据发送状态eMBSndState有:

  • STATE_TX_IDLE,      /*!< Transmitter is in idle state. */
  • STATE_TX_XMIT       /*!< Transmitter is in transfer state. */

接收状态eRcvState和发送状态eMBSndState都在mbrtu.h/c 中定义,并在mbrtu.c中实现了接收/发送过程的状态机(xMBRTUReceiveFSM和xMBRTUTransmitFSM)。

五、两类状态机

xMBRTUReceiveFSM()和xMBRTUTransmitFSM()两个函数在mbrtu.c中实现了接收和发送过程中的状态机,两种状态机的状态变化在参考[2]中有详细描述,这里引用文中的图。

中断时改变发送接收状态的唯一手段,所以我们必须知道有哪些中断会改变发送和接收状态,从两个状态机来看,无非两种:数据到达或者发送的串口中断和T35对应的时钟中断。

六、两类中断

Modbus 在启动时进行了串口中断初始化和时钟中断初始化,两类中断在有字节到达串口和过了T35时间时会发生中断。串口USART1的中断处理函数为USART1_IRQHandler(),时钟TIM2的中断函数为TIM2_IRQHandler()。前者在portserial.c 中,后者在porttimer.c 中。

下图是串口USART1的中断处理流程,流程中可能不完全包括对发送和接收状态的修改,具体需要在源码中查看。

uart-interrupt-and-fsm

当捕捉到USART1串口中断时,首先判断是接收中断还是发送中断。在接收第一个字节后,会将发送空闲状态(STATE_RX_IDLE)变化为接收状态(STATE_RX_RCV),此后只要不溢出接收缓冲区就一直接收,没接收一个字符,无论哪个状态都要调用vMBPortTimerEnable()重置时钟中断。接收数据首先要求发送处于发送空闲状态(STATE_RX_IDLE),然后逐个发送数据。

timer-interrupt

当时钟TIM2中断使能后,每过T35时间系统就会发起TIM2的中断,并由porttimer里的TIM2_IRQHandler() 函数处理,中断处理函数根据数据接收状态(eRcvState)发起不同的事件,如当系统处于初始化状态(STATE_RX_INIT)过了T35时间,表示系统可以进入准备状态(发起准备系统准备EV_READY)接收数据了;如果系统处于数据接收状态,则表示数据数据已经接收完毕(还记得串口中断中如果一直有连续数据进来,则接收一个字节就重新使能时钟中断);vMBPortTimersDisable()最后失效所有时钟中断(没关系当接收状态发生改变时,系统中断会被开启)。

 

以上赘言是个人总结和网络资料的汇编。