freeModbus代码解读及移植笔记(可编辑修改word版)

更新时间:2023-04-08 17:05:01 阅读量: 实用文档 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

1. FreeModbus 协议分析

协议必须首先调用初始化功能eMBinit()函数。后调用eMBEnable(),最后,在循环体或者单独一个任务中调用eMBPoll()函数。

2. 应用层协议

2.1. 系统的启动

2.1.1. eMBInit()函数的源码分析

以RTU 方式为例,首先,检查调用的地址是否合法。如不合法,返回错误。如果合法则继续执行,

首先,针对RTU 方式还是ASCII 方式,选择不同的编译模块。

对需要调用的函数指针进行复制。如果移植需要改变其他用途,则要修改相应的指针,包括

如下赋值:

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);具体初始化通讯端口。

2.1.2. eMBRTUInit

eMBRTUInit 这个函数主要干两件事:

第一,初始化串口:

if( xMBPortSerialInit(ucPort, ulBaudRate, 8, eParity ) != TRUE )

{

eStatus = MB_EPORTERR;

}

这个函数在portserial.c 中,需要用户在移植的时候根据自己的处理器编写。

第二,初始化计时器:首先要根据波特率计算一下是 3.5~5.0 个字节周期的时间,然后再调用xMBPortTimersInit( ( USHORT ) usTimerT35_50us),初始化计时器。这个函数在porttimer.c 中,需要用户在移植的时候根据自己的处理器编写。

2.1.

3. eMBEnable 源码分析

首先,看看Modbus 功能是否是被关闭的,如果不是被关闭(可能是没有被初始化或者已经打开),就返回错误。

如果是disable 状态,就干下面两件事:

l 调用pvMBFrameStartCur()。由于这是个函数指针,在模块eMBInit 中,指向了eMBRTUStart 函数

n 在源代码中有这样一段注释:,意思是,首先设置成STATE_RX_INIT,然后打开计时器,等待t3.5 以后,进入STATE_RX_IDLE 状态。

n 看源代码中,首先有设置Receiver 的状态,后调用vMBPortSerialEnable,设置接收状态,然后打开定时器。

n 当定时器中断后,自动调用中断服务程序,在中断服务程序中,只调用了pxMBPortCBTimerExpired,而这是一个函数指针,在RTU 方式初始化时,被指向了xMBRTUTimerT35Expired()函数。

n xMBRTUTimerT35Expired 函数在mbrtu.c 中,在这里,我们只看第一种方式,就是进入初始化状态,在t35 时间以后,只调用了一个xNeedPoll =

xMBPortEventPost( EV_READY );

n xMBPortEventPost 函数就是在事件队列里加了一个EV_RDY 事件。

l 然后,将eMB 状态改为使能状态,

l 初始化结束。

2.2. 总线侦听eMBPoll()

首先,判断系统是否被使能,如果没有,则返回错误值。

然后,检查是否有事件发生,如果有,则根据不同类型的事件响应:

l 如果是EV_RDY,表示系统刚刚进入侦听状态,则什么都不做;

l 如果状态为EV_FRAME_RECEIVED,也就是接收到完整的帧,做下面两件事情:

n 调用eStatus=peMBFrameReceiveCur( &ucRcvAddress,&ucMBFrame, &usLength)。这是一个函数指针,在eMBInit 中,被初始化指向eMBRTUReceive。

n eMBRTUReceive 这个函数首先校验帧的长度和CRC,然后从协议中解析出地址、数据和长度。

n 然后检查地址,如果是广播地址或者是本机地址,就调用xMBPortEventPost( EV- EXECUTE),将接收器的状态更改为EV_EXECUTE。

l 如果状态为EV_EXECUTE,就在函数列表中检查,有没有与命令字段相符合的函数来解析相应则执行该函数,否则返回非法功能代码。

2.3. 数据发送

发送数据通过指针eMBRTUSend,调用eMBRTUSend 函数。

2.3.1. eMBRTUSend 函数

这个函数的作用就是打包,将数据打包成帧。

l 首先,检查接收状态。因为MODBUS 是基于RS-485 半双工通讯,所以当正在接收数据时,不发送该帧。

l 如果总线空,就将数据打包,将地址和CRC 加入数据帧

l 将总线状态改为发送。

2.4. 功能注册

l 对于指定的功能代码,需要一个功能回调函数来处理,格式如下。

eMBException eMXXXXXX ( UCHAR *pucFrame, USHORT * usLen )

l 需要通过函数eMBRegisterCB(功能代码,函数名)加到处理代码中。具体源码分析从略。

2.4.1. prvvUARTTxReadyISR()

总线状态改为发送后,会在发送缓冲时,自动调用prvvUARTTxReadyISR()中断服务程序。prvvUARTTxReadyISR()只调用了一个函数,就是pxMBFrameCBTransmitterEmpty ()。2.4.2. pxMBFrameCBByteReceived() pxMBFrameCBTransmitterEmpty()是一个指针,指向了xMBRTUTransmitFSM 函数。3. 数据链路层协议

数据链路层是最基本的打包部分,将数据打包成帧,送到应用层。在数据链路层协议中,使用中断方式来接受。那么每次接收到字符就自动调用接收字符的ISR 程序。按照规定,应该将中断服务程序安装给prvvUARTRxISR(void)函数。实际上这个函数只调用了一个函数:pxMBFrameCBByteReceived(),这个指针调用了xMBRTUReceiveFSM 函数。

3.1. xMBRTUReceiveFSM()函数

函数首先检查是不是处于发送状态。如果处于发送状态,直接退出。

l 首先调用xMBPortSerialGetByte( ( CHAR * ) & ucByte),获取从串口读到的字符。

l 然后检查接受状态:

n 如果是错误状态或者处于初始化状态,那么直接等待,错过该帧。

n 如果是STATE_RX_IDLE 空闲状态,则将指针重置,将收到的第一个字节存储到缓冲区,

并将状态改为STATE_RX_RCV 状态。

n 如果处于接收状态,就判断,如果缓冲区未满,就将收到的字节放入缓冲区,否则改为错

误状态。

l 不管在任何状态,最后都开启了t35 计时器。在t35 结束的时候,通过指针调用了xMBRTUTimerT35Expired()函数。

l xMBRTUTimerT35Expired()函数检查状态,如果是接收状态那就表明,已经有t35 这么长的时间里,没有收到任新字节,当前的帧结束。在队列里增加一个EV_FRAME_RECEIVED 事件。

l 如果是错误状态,什么都不做。

l 然后关掉计时器,将状态改为空闲。

3.2. xMBRTUTransmitFSM()函数

xMBRTUTransmitFSM 首先判断总线是否忙,如果忙,则终止。如果不忙,则继续,根据发送状态变量:

l 如果当前为STATE_TX_IDLE(空闲)状态,则打开端口发送

l 如果当前状态为STATE_TX_XMIT,则进一步判断发送队列是否为空,

n 如果不空,则发送下一个字符

n 如果空,说明发送完成,关闭发送端口,改为侦听,并将状态改为空闲。

4. 传输控制

除了传输控制以外,还有传输控制的若干函数。通过下面几个指针来调用:pvMBFrameStopCur()

pvMBFrameCloseCur()

4.1. pvMBFrameStopCur()函数

pvMBFrameStopCur 是一个函数指针,在RTU 方式下,它指向eMBRTUStop()函数。该函数做下面几件事情:

l 关闭侦听和发送

freeModbus 的代码库还是很好用的,本人在wince 和C8051F410 下均移植成功(只用到RTU 模式)。但freeModbus 提供的文档比较少,只能对照着Modbus 协议一点点试着读懂源代码。下面是阅读代码期间的跟踪笔记:

1、eMBErrorCode 为枚举类型变量,代表错误码,共有8 个错误代号。常用的是MB_ENOERR,即没有错误。

2、eMBMode 枚举类型变量代表设备的工作模式,分别是MB_RTU、MB_ASCII 和MB_TCP。

3、eMBEventType 枚举类型变量定义了event 的类型,分别是EV_READY,代表Startup 启动完成;EV_FRAME_RECEIVED 代表接收到帧;EV_EXECUTE 代表执行功能函数;EV_FRAME_SENT 代表帧已发送。

4、eMBParity 枚举类型变量代表奇偶校验选项,分别是MB_PAR_NONE 无校验,MB_PAR_ODD 奇校验,和MB_PAR_EVEN 偶校验。

5、mb.c 文件中的静态变量ucMBAddress 存储设备地址,此变量在eMBInit 函数中初始化。

6、在C51Modbus 中将freeModbus 库中的源码进行了更改,例如尽量不使用函数指针,而是直接调用相关功能函数,根据eMBCurrentMode 中的工作模式,来判断调用哪个函数。在freeModbus 库中某些函数声明前加上reentrant,这是Keil 编译器特有的关键词。这样做带来的一个不足是:不能动态绑定函数,从而导致库代码失去可移植性。这样做是C51 编译

器与ANSI 标准不兼容的特殊性导致的。

7、ENTER_CRITICAL_SECTION()和EXIT_CRITICAL_SECTION()宏,实际上就是关闭和打开全局中断。

8、带xMBPort 前缀的函数都属于port layer 层,也就是独立于ModBus 协议栈。

9、freeModbus 库中函数名称的第一个字母表示返回值类型,例如e 表示返回enum 枚举类型;v 表示void 无返回值;x 表示BOOL 布尔类型。注意这条规则并不是总成立,

但主要函数基本上还是符合此规则的。第一个字母后的MB 代表是属于ModBus 协议栈的函数。

10、port.h 文件中宏#define F_MCU 定义了单片机的工作频率。需要用其值计算Uart0 定时器和Tick 定时器的重装入值。

11、在程序主函数main 中,使用协议栈的方法是:

eStatus = eMBInit( MB_RTU, 0x0A, 0, 9600, MB_PAR_EVEN );

/* Enable the Modbus Protocol Stack. */

eStatus = eMBEnable( );

for( ;; )

{

( void )eMBPoll( );

……

}

12、在port layer 层的xMBPortSerialInit 函数中,需要根据传入的波特率、奇偶校验、数据位长度设置来配置Uart0 及其使用的定时器。

13、在port layer 层的vMBPortSerialEnable 函数中配置接收和发送使能,由于在单片

机的寄存器SCON0 中只有接收使能控制位REN0,而没有发送使能控制位,所以在portserial.c 文件中又定义了一个TxEnable 变量,用来表示发送的使能状态。若同时关闭接收和发送,则要关闭Uart0 中断,即让ES0 = 0。

14、eMBRTUInit 函数中的变量usTimerT35_50us 代表如果50us 进行一次Tick 的话,T35 超时的Tick 次数。这个公式很重要:

usTimerT35_50us = ( 7UL * 220000UL ) / ( 2UL * ulBaudRate );

函数xMBPortTimersInit 要以变量usTimerT35_50us 为传入参数,对T35 超时定时器进行设置。

15、在mbrtu.c 文件中定义了两个状态变量,一个是接收状态变量eRcvState,为eMBRcvState 枚举类型,有4 个状态,在使能ModBus 协议栈后赋予STATE_RX_INIT,即初始状态;另一个是发送状态变量eSndState,为eMBSndState 枚举类型,有两个状态,初始化为发送idle 状态,即STATE_TX_IDLE。

16、mb.c 文件中的eMBState 状态变量为枚举类型,代表设备的工作状态,有3 种状态,分别是“未初始化”、“使能”和“禁止”状态。调用完eMBInit 函数后要调用eMBEnable 函数来使能ModBus 协议栈,在其中将eMBState 状态变量从“未初始化状态”变为“使能状态”,然后使能串口和打开T35 定时器。

17、如果T35 定时器超时并产生中断,则要调用xMBRTUTimerT35Expired 函数,其内部是一个状态机转换的switch,根据当前接收状态来通过xMBPortEventPost 发送事件通知,然后关闭T35 定时器,并将当前接收状态设置为STATE_RX_IDLE。

18、eMBException 枚举型变量表示Exception 的类型,共有10 种Exception,在ModBus 协议中有定义。

19、在eMBPoll( )中,首先通过xMBPortEventGet 函数取event,如果没有则退出,若有event 的话便根据event 类型进行相应处理。EV_READY 是在协议栈初始化后xMBRTUTimerT35Expired 函数发出来的,表示startup 完成;EV_FRAME_RECEIVED

是xMBRTUTimerT35Expired 函数在T35 超时后发出的,表示已经收到了一帧,需要进行成帧处理,调用eMBRTUReceive 函数;EV_EXECUTE 是在处理EV_FRAME_RECEIVED 过程中最后一步,如果此帧的地址符合本机地址,则发出EV_EXECUTE 事件,进行应用层的处理。

20、在eMBRTUReceive 函数中首先查看帧大小是否符合要求,然后进行CRC 校验。此函数的原型是:

eMBRTUReceive( UCHAR * pucRcvAddress, UCHAR ** pucFrame, USHORT * pusLength )

第一个参数是为了返回帧中的地址,也就是帧中第一个字节;第二个传入的参数以后要

当做数组来使用,所以用了指针的指针类型;第三个参数表示PDU 的长度,也就是帧中除去地址字节和CRC 校验字节后的长度。

21、在eMBPoll( )中处理EV_EXECUTE 事件,首先从PDU 中提取出FunctionCode,然后根据FunctionCode 找到相应的处理函数。xMBFunctionHandler 结构体类型变量xFuncHandlers 中定义了各个FunctionCode 对应的处理函数pxHandler,函数的第一个参数ucMBFrame 是PDU 的存储地址,第二个参数usLength 返回PDU 的长度。如果帧不是一个广播帧,则需要设备发出一个回复,如果前面有错误发生,则要回复一个错误报告帧。

22、在Keil 中程序需要使用大模式编译,否则会出现error c249: 'data': segment too large 的错误。

23、若使用波特率为9600,则t3.5= ( 11 * 3.5 ) / 9600 = 4.01 ms。不能使用8 位模式的Timer,因为11.0590MHz 主频在最大48 分频后,最长的超时时间为1.11ms,不能满足T35 的超时要求。

24、freeModbus 1.5 库,在使用过程中发现了一个bug,即如果在PDU 中发送的寄存器数据长度与要读写的寄存器的数量不符,只要CRC 校验正确,freeModbus 便不会检测出来。例如写多个寄存器命令中,标明写寄存器的数量为2,也就是后面接的数据长度为4,但随后的数据只为2 个字节,即一个寄存器的数据,freeModbus 不会发现此错误,同时会将后面的CRC 校验值认作是写第二个寄存器的数据。这个bug 实际上是通过Modbus 调试精灵1.024 的一个写多寄存器bug 发现的。

本文来源:https://www.bwwdw.com/article/uqol.html

Top