freemodbus modbus TCP 学习笔记
1.前言
使用modbus有些时间了,期间使用过modbus RTU也使用过modbus TCP,通过博文和大家分享一些MODBUS TCP的东西。在嵌入式中实现TCP就需要借助一个以太网协议栈,在这里我选择最简单的uIP协议栈。uIP协议栈简单易用方便上手,相比于LwIP无论是移植还是使用难度都低些,这样就可以把更多的精力花在modbus tcp协议本身而不必花大量的时间研究以太网协议栈。modbus协议栈为freemodbus
【其他有用的博文】
【1】uIP学习笔记
【2】MODBUS协议整理——汇总
【工程代码】
示例代码托管于GitHub——【Github Clone】
如果有问题我会及时更新。
【使用说明】
【1】工具链为IAR 6.5
【2】从机IP为固定IP 192.168.1.15,请保证从机和路由器位于同一个网段中。
【3】modbus tcp的侦听端口号为502
2.MODBUS TCP注意点
2.1 主机和从机、服务端和客户端
图1 MODBUS请求响应模型
【在modbus协议中】
主机发送modbus请求,从机根据请求内容向主机返回响应。在modbus协议中,主机总是主动方,从机总是被动方。
【在网络应用中】
在网络应用中存在客户端和服务器端,客户端(例如浏览器)发送请求到服务器,服务器向客户端返回内容(例如HTML文本)。
【在modbus tcp中】
主机是客户端,而从机是服务器端。千万不要以为服务器端重要,主机也重要。
【在modbus rtu中】
主机(Master)是客户端(Client),而从机(Slave)是服务器端(Server)。
2.2 是否可以多主机
通过前面的分析,主机为客户端那么modbus tcp支持多个主机,在一个局域网中可存在多个主机和多个从机。从机的连接能力(连接主机的数量)由uIP的最大TCP连接个数决定。
2.3 modbus TCP协议简述
modbus TCP和modbus RTU基本相同,但是也存在一些区别
【1】从机地址变得不再重要,多数情况下忽略。从某种意义上说从机地址被IP地址取代
【2】CRC校验变得不再重要,甚至可以忽略。由于TCP数据包中已经存在校验,为了不重复造*,modbus TCP干脆取消了CRC校验。
modbus TCP和modbus RTU的区别可使用下图概括
图2 modbus TCP数据包和modbus RTU数据包比较
在modbus TCP中包含一个MBAP头,该头包含以下几个部分
区域 | 长度 |
描述 |
客户端 |
服务器 |
传输标志 |
2字节 |
MODBUS 请求和响应传输过程中 序列号 |
客户端生成 | 应答时复制该值 |
协议标志 | 2字节 |
Modbus协议默认为0 |
客户端生成 |
应答时复制该值 |
长度 |
2字节 |
剩余部分的长度 | 客户端生成 |
应答时由服务器端生成 |
单元标志 |
1字节 |
从机标志(从机地址) | 客户端生成 |
应答时复制该值 |
【注意】
【1】传输标志可理解为序列号,防止MODBUS TCP通信错位,例如后发生的响应先到了主机,而早发生的响应后到主机
【2】单元标志可理解为从机地址,此时已经不再重要
2.4 modbus tcp 和 TCP IP的关系
modbus TCP可以理解为发生在TCP上的应用层协议,既然是TCP协议那么一个完整的MODBUS TCP报文必然包括TCP首部,IP首部和Ethernet首部。
下面就通过uIP协议栈来实现modbus TCP
3.代码实现
3.1 侦听502端口
[cpp] view plain copy
- BOOL
- xMBTCPPortInit( USHORT usTCPPort )
- {
- BOOL bOkay = FALSE;
- USHORT usPort;
- if( usTCPPort == 0 )
- {
- usPort = MB_TCP_DEFAULT_PORT;
- }
- else
- {
- usPort = (USHORT)usTCPPort;
- }
- // 侦听端口 502端口
- uip_listen(HTONS(usPort));
- bOkay = TRUE;
- return bOkay;
- }
【代码说明】
【1】uip_listen(HTONS(usPort)) 侦听502端口,注意大小端变化。
3.2 uIP循环处理——porttcp.c
[cpp] view plain copy
- void uip_modbus_appcall(void)
- {
- if(uip_connected())
- {
- PRINTF("connected!\r\n");
- }
- if(uip_closed())
- {
- PRINTF("closed\r\n");
- }
- if(uip_newdata())
- {
- PRINTF("request!\r\n");
- // 获得modbus请求
- memcpy(ucTCPRequestFrame, uip_appdata, uip_len );
- ucTCPRequestLen = uip_len;
- // 向 modbus poll发送消息
- xMBPortEventPost( EV_FRAME_RECEIVED );
- }
- if(uip_poll())
- {
- if(bFrameSent)
- {
- bFrameSent = FALSE;
- // uIP发送Modbus应答数据包
- uip_send( ucTCPResponseFrame , ucTCPResponseLen );
- }
- }
- }
【代码说明】
【1】uip_newdata()返回为True表示存在新的数据
【2】复制uip_appdate中的数据到ucTCPRequestFrame,该变量为全局变量,通过该全部变量”中转“到modbus处理的缓冲区中。然后向modbus协议栈发送消息,消息内容为EV_FRAME_RECEIVED 。由于没有使用操作系统
【3】如果处理完成则通过uip_send发送响应。
【4】ucTCPRequestFrame和ucTCPResponseFrame均为全局数组,用于和modbus缓冲区交换数据。
static UCHAR ucTCPRequestFrame[MB_TCP_BUF_SIZE];
static USHORT ucTCPRequestLen;
static UCHAR ucTCPResponseFrame[MB_TCP_BUF_SIZE];
static USHORT ucTCPResponseLen;
3.3 modbus 接收处理
[cpp] view plain copy
- BOOL
- xMBTCPPortGetRequest( UCHAR ** ppucMBTCPFrame, USHORT * usTCPLength )
- {
- *ppucMBTCPFrame = &ucTCPRequestFrame[0];
- *usTCPLength = ucTCPRequestLen;
- /* Reset the buffer. */
- ucTCPRequestLen = 0;
- return TRUE;
- }
【代码说明】
【1】** ppucMBTCPFrame为一个指向数据的指针,而*ppucMBTCPFrame可以指向一个数组,在这里可把ucTCPRequestFrame复制给该变量,配合usTCPLength,那么从uIP接收到的内容就”转移“到freemodbus中。
3.4 modbus 发送处理
[cpp] view plain copy
- BOOL
- xMBTCPPortSendResponse( const UCHAR * pucMBTCPFrame, USHORT usTCPLength )
- {
- memcpy( ucTCPResponseFrame , pucMBTCPFrame , usTCPLength);
- ucTCPResponseLen = usTCPLength;
- bFrameSent = TRUE; // 通过uip_poll发送数据
- return bFrameSent;
- }
【代码说明】
【1】把传入的内容 pucMBTCPFrame复制给ucTCPResponseFrame,并设置bFrameSent为True,那么在下一次uip_poll时便会把响应发送会主机。
4.测试与分析
【1】连接从机
选择IP地址为192.168.1.15,端口号为502
图3 打开modbus tcp连接
【2】尝试读出保持寄存器
图4 读取保持寄存器
【3】抓包分析
请使用ip.addr == 192.168.1.15 表达式过滤报文,其中192.168.1.100为PC机,此处为modbus 主机
【简单分析】
【1】115行为modbus主机请求,此时传输标志为25.
【2】116行为modbus从机给出的TCP应答,TCP应答为TCP协议规定的内容,TCP应答中不包含modbus 响应
【3】117行为modbus从机响应,此时传输标志依然为25.
【4】118行为modbus主机 TCP应答,同16行。
图5 抓包分析