4. Modbus协议基础:三种模式的区别与报文结构详解

说到Modbus,这可能是工业现场用得最多的协议了。我入行那会儿,第一个接触的串口项目就是Modbus RTU。说实话,当时看着那一串十六进制数,真有点懵。后来摸爬滚打这么多年,才真正理解它的设计精髓。

今天咱们就把Modbus的三种模式——RTU、ASCII、TCP,掰开揉碎了讲清楚。你想想看,搞懂了这些,以后拿着分析仪抓包,一眼就能看出问题在哪儿。

4.1 三种模式的核心区别

Modbus本质上就是个主从通信协议。主机发请求,从机回响应。但不同的物理层,报文格式差别很大。

特性 Modbus RTU Modbus ASCII Modbus TCP
物理层 RS-232/485 RS-232/485 以太网
数据表示 二进制(8位) ASCII字符(16进制文本) 二进制(封装在TCP帧内)
帧间隔 3.5字符时间 1秒超时 无(TCP流式)
校验方式 CRC-16 LRC(纵向冗余校验) TCP/IP协议栈校验
效率 低(约RTU的一半)
典型应用 PLC、变频器、仪表 老旧设备、调试场景 上位机、SCADA、网关

我个人习惯:能用RTU就用RTU。ASCII模式我只有在调试老设备时才用,比如一些90年代的温控仪表。为什么?因为ASCII模式每个字节拆成两个ASCII字符发,效率直接砍半。你想想看,9600波特率下,RTU能发1200字节/秒,ASCII只能发600字节/秒。

关键点:Modbus TCP没有校验字段!因为TCP/IP协议栈自己会做校验。很多新手拿着Modbus TCP报文找CRC,找了半天找不到,急得满头汗。嗯,这里要注意——TCP模式没有CRC,也没有地址字段(因为IP地址已经标识了设备)。

4.2 功能码分类:01-06和15-16

功能码就是告诉从机:你要干什么。Modbus的功能码很多,但最常用的就这几个。我在项目中遇到过无数次,90%的故障都出在这几个功能码的使用上。

4.2.1 位操作功能码

功能码 名称 操作对象 说明
01 (0x01) 读取线圈状态 DO(数字量输出) 读多个线圈的ON/OFF状态
02 (0x02) 读取离散输入 DI(数字量输入) 读多个输入点的状态
05 (0x05) 写单个线圈 DO 控制一个输出点的通断
15 (0x0F) 写多个线圈 DO 批量设置输出点

4.2.2 寄存器操作功能码

功能码 名称 操作对象 说明
03 (0x03) 读取保持寄存器 AO(模拟量输出/参数) 最常用的读操作,读参数、设定值
04 (0x04) 读取输入寄存器 AI(模拟量输入) 只读,读传感器值、采集数据
06 (0x06) 写单个寄存器 AO 修改一个参数或设定值
16 (0x10) 写多个寄存器 AO 批量写入参数

避坑指南:我曾经遇到一个项目,上位机用功能码03读数据,但设备手册写的是输入寄存器。结果读回来的全是0。折腾了两天,才发现应该用功能码04。记住:03读保持寄存器(可读可写),04读输入寄存器(只读)。这两个搞混了,数据肯定不对。

4.3 报文结构详解

报文结构是Modbus的核心。我建议你把这个部分多看几遍,因为抓包分析时,报文结构就是你的地图。

4.3.1 Modbus RTU报文结构

RTU报文由四部分组成:地址码 + 功能码 + 数据区 + CRC校验。每个字节都是二进制数据。

请求报文(主机→从机):
| 地址(1B) | 功能码(1B) | 起始地址(2B) | 数量(2B) | CRC低(1B) | CRC高(1B) |

示例:读取从机地址01,从寄存器0x0000开始,读2个保持寄存器
01 03 00 00 00 02 C4 0B

响应报文(从机→主机):
| 地址(1B) | 功能码(1B) | 字节数(1B) | 数据(NB) | CRC低(1B) | CRC高(1B) |

示例:返回4个字节的数据(2个寄存器)
01 03 04 00 0A 00 14 3A 0F

这里有个细节:RTU报文要求帧间间隔至少3.5个字符时间。如果间隔太短,从机可能把两帧当成一帧处理。我调试过一个485总线,数据总是乱码,最后发现是主机发送间隔太短,从机来不及处理。加了个10ms延时,问题就解决了。

4.3.2 Modbus ASCII报文结构

ASCII模式就是把每个字节拆成两个ASCII字符。报文以冒号":"开头,以回车换行"\r\n"结尾。

请求报文:
: 地址(2字符) 功能码(2字符) 数据(N字符) LRC(2字符) \r\n

示例:读取从机地址01,寄存器0x0000,读2个寄存器
: 01 03 00 00 00 02 F8 \r\n

响应报文:
: 地址(2字符) 功能码(2字符) 字节数(2字符) 数据(N字符) LRC(2字符) \r\n

示例:
: 01 03 04 00 0A 00 14 3A \r\n

注意:ASCII模式的LRC校验和RTU的CRC不一样。LRC是纵向冗余校验,计算方式是把所有字节(不包括起始冒号和结束符)相加,取补码。而CRC是循环冗余校验,计算复杂得多。我曾经用ASCII模式调试一个进口仪表,LRC算错了三次,后来发现是手册上写的校验范围不包括地址字节。这种坑,只有踩过才知道。

4.3.3 Modbus TCP报文结构

TCP模式去掉了地址和CRC,增加了MBAP头(Modbus Application Protocol Header)。

请求报文:
| 事务ID(2B) | 协议ID(2B) | 长度(2B) | 单元ID(1B) | 功能码(1B) | 数据(NB) |

示例:事务ID=0x0001,协议ID=0x0000,长度=0x0006,单元ID=0x01,读2个寄存器
00 01 00 00 00 06 01 03 00 00 00 02

响应报文:
| 事务ID(2B) | 协议ID(2B) | 长度(2B) | 单元ID(1B) | 功能码(1B) | 数据(NB) |

示例:
00 01 00 00 00 07 01 03 04 00 0A 00 14

解释一下MBAP头:事务ID用来匹配请求和响应,协议ID固定为0x0000,长度表示后面还有多少字节,单元ID相当于RTU的地址。说白了,TCP模式就是把RTU的地址和校验去掉,加上TCP的头部信息。

实战经验:我调试过一个Modbus TCP网关,上位机发请求后一直收不到响应。抓包一看,事务ID每次都是0x0000,但网关要求事务ID必须递增。改了一下代码,问题解决。所以啊,TCP模式虽然简单,但事务ID这个细节很容易被忽略。

4.4 三种模式的报文对比

咱们用一个实际例子来对比。假设要读取从机地址01,寄存器0x0000开始的2个保持寄存器,返回数据是0x000A和0x0014。

模式 请求报文 响应报文
RTU 01 03 00 00 00 02 C4 0B 01 03 04 00 0A 00 14 3A 0F
ASCII :010300000002F8\r\n :010304000A00143A\r\n
TCP 0001 0000 0006 01 03 00 00 00 02 0001 0000 0007 01 03 04 00 0A 00 14

你看,同样的数据,三种模式的报文长度差别很大。RTU最短,ASCII最长(因为每个字节变两个字符),TCP介于中间(多了MBAP头)。

我的建议:刚开始学Modbus,先用RTU模式。因为它的报文最直观,用分析仪一看就明白。ASCII模式现在用得少了,除非你遇到老设备。TCP模式适合上位机开发,但要注意网络延迟和丢包问题。

4.5 常见故障与排查思路

最后分享几个我踩过的坑,希望能帮你少走弯路。

  • CRC校验错误:RTU模式下最常见的问题。我遇到过CRC计算函数写错了,高低字节顺序反了。检查方法:用Modbus分析仪对比计算值。
  • 地址不匹配:从机地址设置成0x01,但主机发的是0x02。这种低级错误我犯过不止一次。
  • 寄存器地址偏移:很多设备手册上的地址是PLC地址(如40001),对应Modbus地址是0x0000。搞混了就会读到错误数据。
  • 波特率不匹配:串口通信最基础的坑。我建议调试时先用9600波特率,稳定后再改。
  • TCP连接超时:Modbus TCP默认端口是502。如果防火墙没开这个端口,连接就会超时。

好了,Modbus协议基础就讲到这里。下一章咱们会讲怎么用Modbus分析仪抓包,到时候这些报文结构知识就能派上用场了。记住一句话:搞懂了报文结构,Modbus调试就成功了一半。