基于EncEthernet的FreeModbus-TCP
在stm32上的移植与测试
DanielLee_USTB 2013-3-27
QQ:382899443
昨天移植好了modbus-RTU,今晚开始在EncEthernet上的free modbus-TCP的移植,使用的开发板为火牛开发板,stm32f103+enc28j60网络方案。主流的TCP/IP协议栈包括uIP、LwIP等,EncEthernet协议栈是一款比较简单的协议栈,由厂家提供在stm32的开发板已经移植好,所以就直接使用,其他的协议的移植方法应该都大同小异。
一、相关知识
Modbus TCP/IP数据帧除了TCP已经有的包头外,还有modbus TCP协议数据单元(ADU),包括MBAP帧头以及与RTU数据内容相同的应用数据单元(PDU),地址码除外。
MBAP报文头定义
可以看出来,MBAP报文头主要添加了以下附加信息,为了识别是请求还是响应而设置的事务元标识符、为了判断协议类型设置的协议标识符、为了区分可变长度数据帧结束的数据帧长度、还有用于标识从站地址,与RTU不同的是,从地址放在了MBAP帧头里。
二、代码移植
前两天已经基于BARE工程移植好RTU模式,仿照相应思路实现TCP的一些函数功能,在mbtcp.c中可以发现,包括TCP初始化(xMBTCPPortInit)、TCP启动(eMBTCPStart)、TCP停止(eMBTCPStop)、TCP接收一个数据包(xMBTCPPortGetRequest)、TCP发送一个数据包(xMBTCPPortSendResponse)等,为了实现free-modbus与EncEthernet对接,在port文件夹下建立porttcp.c文件,在其中包含头文件ip_arp_udp_tcp.h。
(1) xMBTCPPortInit( ucTCPPort )
这是以太网TCP端口初始化函数,怎么觉得参数有点少呢,绑定TCP端口至少需要mac地址、ip地址以及端口地址吧,这里面只与端口有关,看来只能把他们隐藏了。
于是加入
enc28j60Init(mymac);
enc28j60PhyWrite(PHLCON,0x476);
init_ip_arp_udp_tcp(mymac,myip,mywwwport);
这个几个函数作为TCP端口初始化。
(2) eMBTCPStart
其实在EncEthernet中只要进行了协议栈的初始化,就已经启动了协议栈,可直接使用。
(3) eMBTCPStop
这个函数是作为TCP端口关闭的函数,其实在modbus中调用它的是eMBClose,而在协议中没有调用eMBClose把modbus给关掉,所以这个函数不用去实现。
(4) xMBTCPPortGetRequestTCP接收一个数据包
调用enc28j60PacketReceive(BUFFER_SIZE, buf)进行判断,当然希望移植的modbus除了能处理modbus-TCP包外还能对一些正常数据包进行响应,比如arp请求、ping命令等,所以在之后添加了包头验证,当确定传入包是有数据的TCP/IP包才返回TRUE。
(5) xMBTCPPortSendResponse TCP 发送一个数据包
封装xMBTCPPortSendResponse发送写好的TCP包即可。
接口部分移植完毕,下面软件仿真一下看看modbusTCP运行的流程。
先看main函数:
eMBErrorCode eStatus;
SystemInit();
SPI_Enc28j60_Init();
eStatus = eMBTCPInit(502 );
eStatus =eMBEnable( );
for( ;; ){
( void)eMBPoll( );
usRegInputBuf[0]++;
}
与modbus-RTU模式类似,只是多了对ENC28J60SPI端口的初始化函数,使用eStatus = eMBTCPInit( 502 )对TCP/IP协议栈进行初始化,进入到eMBPoll()。到这里的时候发现在 if( xMBPortEventGet(&eEvent ) == TRUE )条件一直为假,也就是事件队列没有事件,RTU模式下是定时器触发的,TCP模式下到底应该谁去触发它呢? TCP模式没有控制状态转换状态机有木有!没有东西修改队列检索事件的函数xMBPortEventGet,就不能被eMBPoll周期性地调用!这可是个大问题。
由于网络数据包这里并不是用的中断方式,所以只能有以下解决方法:在eMBTCPStart中加上一个xMBPortEventPost(EV_FRAME_RECEIVED),那么这样进入到eMBPoll的时候就会调用eMBTCPReceiveàxMBTCPPortGetRequest去读取数据包,如果是modbus-TCP包的话就把返回MB_ENOERR,再对MBAP帧头进行判断,查看协议类型,跳过MBAP帧头,传递了正确的数据包后进入EV_EXECUTE状态就会调用相应的函数进行处理,如果不是广播帧则返回处理后的TCP数据包,调用完xMBTCPPortSendResponse再发送事件xMBPortEventPost(EV_FRAME_RECEIVED)即可。如果不是想要的TCP数据包,在xMBTCPPortGetRequest里面也加入xMBPortEventPost(EV_FRAME_RECEIVED),进入下一轮查询,这样可以响应ARP、PING命令的数据包等。
测试下ping命令,设置开发板ip地址为222.28.40.18,与我的电脑接在同一个路由器上,可以看到返回的响应了!
Ping 目标板响应
三、运行流程
不过还不能高兴的太早,能测试通ping只能是TCP/IP的功能,那么接收以太网发来的modbus-TCP帧后是如何处理的呢?接下来分析一下,既然modbus-TCP发出来的数据包时TCP/IP包对modbus数据帧的封装,那么在调用peMBFrameReceiveCur(xMBTCPPortGetRequest)获得的数据包是带各种包头的,要把带以太网帧头、IP头、TCP头去掉才行,除此以外还需要实现TCP协议的三次握手功能,这还得在xMBTCPPortGetRequest中修改,主要去判断数据长度、是否为 arp请求、是否为空ip包、对ping命令做出响应、实现TCP协议的三次握手功能、以及获得modbus-TCP的MBAP帧头。
通过对代码的整体分析可以得出数据流在modbus协议栈是这样流动的,如图所示:
Freemodbus-TCP中数据流向
数据接收:
由于在eMBPoll中数据是逐渐调用的,这里不妨从数据接收的源头开始理清思路。当一帧数据到来之后,通过enc28j60PacketReceive接收到了一个完整的TCP包,包括以太网头、IP头、TCP头以及modbusTCP/IP 应用数据单元ADU。接着被xMBTCPPortGetRequest调用去掉了各种header,然后经过eMBTCPReceive(peMBFrameReceiveCur)对MBAP的分析,将指向MBAP报文头的指针传到eMBPoll:EV_FRAME_RECEIVED中。
数据处理:
在eMBPoll :EV_EXECUTE状态下调用对应的处理函数,这里以读入寄存器状态为例,调用的是eMBFuncReadInputRegister,对命令内容进行分析译码,调用eMBRegInputCB 填好请求的数据。
数据发送:
如果不是广播包的话,需要对数据进行回复,调用eMBTCPSend(peMBFrameSendCur)填好MBAP帧头的长度信息,再传给xMBTCPPortSendResponse enc28j60PacketSend填好header以及校验发送出去。
在EV_FRAME_RECEIVED接收完数据后,发现一个问题,按RTU的思路还要比较接收地址是不是给我们的地址,但是我们之前并没有制定我们自己的设备地址ucMBAddress,在xMBTCPPortInit初始化的时候只绑定了端口,就给地址设置带来不便,只有在TCP端口初始化像设定mac地址、ip地址那样设置从地址了,我们移植的原则是尽量不去修改下载的源码,一切修改都在接口的实现中完成。寻根溯源,在eMBTCPReceive看到这样的定义:
eMBTCPReceive: *pucRcvAddress =MB_TCP_PSEUDO_ADDRESS;
#defineMB_TCP_PSEUDO_ADDRESS 255 原来设备从地址居然是一个宏定义!协议栈是这样解释的:
/* Modbus TCP does not useany addresses. Fake the source address such
* that the processing partdeals with this frame.*/
原来modbus-TCP根本不需要从地址,因为已经有IP地址以及MAC地址进行了绑定!
四、通信测试
移植分析完毕,把程序下载到stm32板子中进行测试。
(1)测试工具
上位机采用TCP&UDP测试工具建立服务器
以及抓包工具wireshark
Wireshark 抓包工具
(2)测试内容
由于EncEthernet原协议TCP端口号只支持了一个字节,而modbus-TCP要用到502端口,超出了这一范围,稍加改动就可以正常与上位机连接了。
在wireshark中可以捕获到TCP协议三次握手连接以及断开过程如下图所示:
TCP三次握手建立连接
使用上位机TCP&UDP测试工具发送读取寄存器指令00 0000 00 00 00 ff 04 00 00 00 01
使用上位机发送的TCP数据帧
来读取GPIOA的数据,可是没有数据返回,先Debug一下看看板子有没有收到这个数据包,从结果看来确实收到了这个数据包,原来是数据发送有问题,修改xMBTCPPortSendResponse函数,先发送一个ACK应答包,再发送数据,就可以了!注意数据的格式。在wireshark中抓包如下图所示:
Wireshark收到的Modbus-TCP数据帧
上位机收到的modbus-TCP帧数据
更改上位机指令,使其发送读取3个寄存器(GPIOA-GPIOC)的值:
更改指令读取3个寄存器返回的数据
至此,经过了三天时间modbus-TCP模式终于能够正常通信了,真高兴!其实无论是移植EncEthernet,或是uIP、Lwip基本思路都是一样的,弄清楚数据接收以及发送的整个过程很重要,可以很好的帮助理解协议栈是如何运行的。