尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Modbus RTU与TCP帧结构、CRC校验及寻址差异详解

Modbus RTU与TCP帧结构、CRC校验及寻址差异详解 1. 从一条产线调试的翻车现场说起去年帮朋友处理一条包装线的通讯故障PLC 侧用的是 FX3U 加 485ADP-MB 模块从站是一台 E5CC 温控器走的是 Modbus RTU。现场折腾了一下午读回来的温度值始终是 0x8000 这种明显不对的数值。后来换了一台带网口的网关把同样的寄存器地址、同样的功能码搬到 Modbus TCP 上五分钟就通了。当时朋友问了我一句话“同样是读一个寄存器为什么换个传输层差别这么大”这个问题其实问到了 Modbus 的本质。Modbus 本身只是一套应用层的问答约定——主站发什么、从站回什么、寄存器怎么编址、异常怎么报这些规则在 RTU 和 TCP 上是一模一样的。真正变的是这套问答被“装进”了哪一层信封里。RTU 把它塞进串行帧靠 CRC 保证完整性TCP 把它塞进 MBAP 报文头靠 TCP 自己保证可靠性。理解了这一层你就能明白为什么有些代码在 RTU 上跑得好好的搬到 TCP 上就报错也能明白为什么抓包时看到的字节流长得完全不一样。这篇东西我打算把 Modbus RTU 和 Modbus TCP 从帧结构、校验方式、寻址逻辑到实际调试踩坑一层一层拆开讲。适合刚接触 Modbus 的入门者也适合那些“会用但说不清为什么”的老手。看完你至少能做到两件事一是拿到一段报文能自己拆出功能码和数据二是遇到 RTU 转 TCP 的网关配置时知道该盯哪几个参数。2. 同一套问答两种信封核心差异拆解2.1 应用层不变的那部分PDU 才是灵魂先把一个概念钉死Modbus 真正定义“业务逻辑”的部分叫PDUProtocol Data Unit协议数据单元。它只有两个字段——功能码和数据域。比如你要读从站 1 的保持寄存器 40001 开始的 2 个寄存器PDU 就是03 00 00 00 0203是功能码读保持寄存器后面四个字节是起始地址 0x0000 和数量 0x0002。注意这里的地址 0x0000 对应的是“40001”这个编号。Modbus 的寄存器编号是 1 基的而报文里是 0 基的所以 40001 在报文里就是 0x000040002 是 0x0001以此类推。这个偏移量是新手最容易翻车的地方后面还会细说。PDU 这一层RTU 和 TCP 完全共用。也就是说你写业务逻辑的那部分代码——功能码怎么组、寄存器怎么算、异常码怎么解析——理论上可以原封不动地在两种传输方式之间迁移。这也是为什么很多 Modbus 库会把 PDU 的编解码单独抽出来传输层只是外面套的一层壳。2.2 RTU 的信封地址 PDU CRCModbus RTU 的完整帧叫ADUApplication Data Unit应用数据单元结构是这样的字段长度说明从站地址1 字节0x01~0xF70 是广播PDU1~252 字节功能码 数据CRC 校验2 字节低字节在前高字节在后举个例子主站读从站 1 的 40001 开始的 2 个寄存器完整 RTU 帧是01 03 00 00 00 02 C4 0B拆开看01是从站地址03 00 00 00 02是 PDUC4 0B是 CRC。CRC 是低字节在前所以实际计算出来的 CRC 值如果是 0x0BC4在报文里就写成C4 0B。这个字节序问题坑过无数人我见过有人把 CRC 高低字节写反结果从站一直不回抓包看帧明明“长得对”就是没响应。RTU 帧与帧之间靠至少 3.5 个字符时间的静默间隔来分隔。这个设计很巧妙——串行线上没有“帧头帧尾”这种标记只能靠时间间隔判断一帧从哪开始到哪结束。波特率越高3.5 个字符时间越短。9600 波特率下一个字符11 位1 起始 8 数据 1 校验 1 停止约 1.146ms3.5 个字符就是约 4ms。所以 RTU 对时序是有要求的如果主站发完一帧立刻又发下一帧中间没有足够静默从站可能把两帧粘成一帧处理直接报 CRC 错。2.3 TCP 的信封MBAP PDU没有 CRCModbus TCP 的 ADU 结构换成了MBAP 报文头 PDU字段长度说明事务标识符2 字节主站自增用于匹配请求和响应协议标识符2 字节固定 0x0000长度2 字节后续字节数单元标识符 PDU单元标识符1 字节类似 RTU 的从站地址PDU1~252 字节功能码 数据同样的读寄存器请求TCP 帧是00 01 00 00 00 06 01 03 00 00 00 02拆开00 01是事务标识符00 00是协议标识符00 06是长度后面 6 个字节单元标识符 1 PDU 501是单元标识符03 00 00 00 02是 PDU。没有 CRC因为 TCP 协议本身在传输层已经做了校验和重传应用层再叠一层 CRC 就是重复劳动。这里有个细节值得说MBAP 里的“长度”字段是从单元标识符开始算的不包括事务标识符、协议标识符和长度字段本身。很多人第一次手搓 TCP 报文时会把长度算错导致从站解析越界。记住这个公式长度 1单元标识符 PDU 长度。2.4 端口 502 和那个容易混淆的 502 错误Modbus TCP 默认端口是502。这个数字在热搜里经常和“502 Bad Gateway”混在一起出现但两者毫无关系。502 Bad Gateway 是 HTTP 状态码表示网关从上游服务器收到了无效响应Modbus 的 502 只是一个 TCP 端口号纯粹是历史分配的结果。我之所以专门提这个是因为搜索热词里出现了“502 bad gateway”和“unexpected status 502 bad gateway”这类词很多新手在搜 Modbus TCP 资料时会被这些 HTTP 报错内容干扰。如果你在调试 Modbus TCP 时看到“502”先确认它是端口号还是 HTTP 错误码——前者是你主动连的后者是某个 Web 服务返回的场景完全不同。3. CRC 校验RTU 的命门也是新手的噩梦3.1 CRC-16/MODBUS 到底怎么算Modbus RTU 用的是CRC-16/MODBUS变体参数是多项式 0x8005反向表示为 0xA001初始值 0xFFFF输入和输出都反射结果异或 0x0000。听起来很绕但实际计算逻辑并不复杂核心就是一个循环def crc16_modbus(data: bytes) - int: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc拿前面的例子验证一下对01 03 00 00 00 02计算得到的结果应该是 0x0BC4。因为 RTU 报文里 CRC 低字节在前所以写入报文时是C4 0B。你可以用这段代码自己跑一遍确认输出是0x0BC4。这里有个容易搞混的点多项式 0x8005 和 0xA001 是同一个东西的两种写法。0x8005 是正常表示最高位对应 x^150xA001 是反向表示最低位对应 x^0。因为 Modbus CRC 是反射算法实际代码里用的是 0xA001。你不需要纠结推导过程记住“初始 0xFFFF、多项式 0xA001、结果不异或”这三条就够了。3.2 为什么 CRC 低字节在前这是 Modbus 协议的历史遗留。RTU 基于串行通信早期实现里 CRC 是逐字节发送的协议规定先发低字节。所以你在报文里看到的顺序是“低字节、高字节”而不是我们习惯的“高字节、低字节”。这个规则和寄存器数据的大端序是相反的——寄存器数据是高字节在前CRC 是低字节在前。同一个协议里两种字节序并存确实反直觉但这就是规范。我踩过的坑有一次用某个国产 PLC 的 Modbus 库它内部把 CRC 算好后自动按低字节在前填充我没注意又手动交换了一次结果从站一直不回。抓包看帧CRC 字段是0B C4而正确应该是C4 0B。这种错误很隐蔽因为帧长度、功能码、数据都对只有最后两个字节反了。3.3 CRC 错误的排查思路当你遇到从站不响应或返回异常时CRC 错误是首要怀疑对象。排查顺序建议这样确认波特率、数据位、校验位、停止位一致。RTU 常见配置是 9600/8/N/1 或 19200/8/E/1主从必须完全一致。校验位不一致会导致每个字节都错CRC 自然全错。确认 CRC 字节序。用工具算一遍对比报文里的两个字节。确认帧间隔。如果主站发得太快从站可能把两帧粘在一起导致 CRC 校验的是拼接后的数据。确认线路质量。485 线如果没接终端电阻、走线太长、和动力线捆在一起误码率会飙升CRC 错误会随机出现而不是稳定复现。提示CRC 错误是“结果”不是“原因”。稳定复现的 CRC 错误通常是配置或字节序问题随机出现的 CRC 错误通常是线路或干扰问题。先分清是哪种再往下查。4. 从 RTU 到 TCP迁移时真正要改的东西4.1 寻址逻辑单元标识符和从站地址的对应在 RTU 里从站地址是帧的第一个字节范围 1~247。在 TCP 里对应的字段叫单元标识符位置在 MBAP 头之后、PDU 之前。功能上它和从站地址等价但用法上有区别。纯 TCP 网络里每个 Modbus 设备通常有独立 IP单元标识符往往固定为 0x01 或 0xFF因为“找哪个设备”已经由 IP 决定了。但在RTU over TCP或Modbus 网关场景下单元标识符就重要了——网关后面挂多个 RTU 从站网关根据单元标识符把请求转发给对应的串行从站。这时候单元标识符必须和 RTU 从站地址一致。我见过一个典型故障网关配置里单元标识符设成了 0xFF但后面挂的温控器地址是 0x01结果网关把请求广播出去温控器不响应广播通常不回复主站超时。改成 0x01 后立刻正常。所以迁移时第一件事就是确认单元标识符和实际从站地址的映射关系。4.2 事务标识符TCP 独有的请求响应匹配机制RTU 是严格的一问一答主站发一帧等从站回一帧不存在“响应错位”的问题。TCP 虽然也是请求响应模式但理论上可以并发多个请求事务标识符就是用来匹配“哪个响应对应哪个请求”的。实际实现里大多数 Modbus TCP 客户端还是串行发请求——发一个、等一个、再发下一个。但事务标识符仍然要正确填充因为有些从站或网关会校验它。通常的做法是每发一个请求就自增一次响应回来时比对事务标识符是否一致。如果你手搓报文时把事务标识符固定成 0x0000大部分设备也能工作但遇到严格的实现可能会被拒绝。4.3 超时和重试策略的差异RTU 的超时通常按波特率和帧长估算。比如 9600 波特率下一帧 8 字节约 9ms 传输时间加上从站处理时间超时设 100~300ms 比较合理。设太短会误判超时设太长会拖慢轮询周期。TCP 的超时逻辑不同。TCP 本身有重传机制如果网络丢包TCP 会自己重传应用层感知到的是“响应变慢”而不是“丢包”。所以 Modbus TCP 的应用层超时通常设得比 RTU 长500ms~2s 都常见。但也不能太长否则一个从站掉线会拖垮整个轮询循环。我的经验是RTU 超时设 200ms 起步TCP 超时设 1s 起步然后根据实际网络质量调整。如果 TCP 网络稳定可以降到 500ms如果走无线或跨网段可能要设到 3s。5. 实操手搓一段报文并验证5.1 用 Python 构造 RTU 请求并计算 CRC下面这段代码完整演示了从构造 PDU 到拼出 RTU 帧的过程import struct def crc16_modbus(data: bytes) - int: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc def build_rtu_frame(slave_addr: int, func_code: int, start_addr: int, quantity: int) - bytes: pdu struct.pack(BHH, func_code, start_addr, quantity) frame_without_crc struct.pack(B, slave_addr) pdu crc crc16_modbus(frame_without_crc) # CRC 低字节在前 frame frame_without_crc struct.pack(H, crc) return frame frame build_rtu_frame(1, 3, 0, 2) print(frame.hex( ).upper()) # 输出: 01 03 00 00 00 02 C4 0B注意struct.pack(BHH, ...)里的表示大端序因为 Modbus 的寄存器地址和数量都是大端。而 CRC 用H小端序打包实现低字节在前。这两个字节序的差异是手搓报文时最容易出错的地方。5.2 构造对应的 TCP 请求同样的业务请求TCP 帧的构造def build_tcp_frame(transaction_id: int, unit_id: int, func_code: int, start_addr: int, quantity: int) - bytes: pdu struct.pack(BHH, func_code, start_addr, quantity) length 1 len(pdu) # 单元标识符 PDU mbap struct.pack(HHHB, transaction_id, 0x0000, length, unit_id) return mbap pdu frame build_tcp_frame(1, 1, 3, 0, 2) print(frame.hex( ).upper()) # 输出: 00 01 00 00 00 06 01 03 00 00 00 02对比两段输出你会发现 PDU 部分03 00 00 00 02完全一样区别只在信封RTU 是01 ... C4 0BTCP 是00 01 00 00 00 06 01 ...。这就是“同一套问答差在哪一层”的最直观体现。5.3 用工具验证Modbus Poll 和抓包实际调试时我一般用两个工具交叉验证。Modbus Poll用来模拟主站发请求配置好功能码、地址、数量后它能直接显示从站返回的原始报文。Wireshark用来抓 TCP 包过滤条件写tcp.port 502能看到完整的 MBAP PDU 字节流。如果是 RTUWireshark 抓不了串口得用串口调试助手或者Modbus Slave配合虚拟串口。我常用的组合是一台机器跑 Modbus Slave 模拟从站另一台跑 Modbus Poll 模拟主站中间用虚拟串口对连。这样可以在没有真实硬件的情况下验证报文格式和 CRC 计算是否正确。注意虚拟串口工具在 Windows 上比较多Linux 下可以用socat创建一对伪终端。但虚拟串口不模拟真实线路的时序和干扰所以 CRC 时序相关的坑还是得在真实硬件上验证。6. 常见问题与排查速查表6.1 从站不响应先查这五项排查项RTU 检查点TCP 检查点物理连接485 A/B 是否接反终端电阻是否接网线、IP 是否可达ping 是否通地址匹配从站地址是否与报文首字节一致单元标识符是否与网关映射一致通讯参数波特率、数据位、校验位、停止位端口是否 502协议标识符是否 0x0000帧格式CRC 是否正确字节序是否低前MBAP 长度字段是否正确时序帧间隔是否满足 3.5 字符时间事务标识符是否匹配这张表我一般贴在工位上遇到问题从上往下过一遍八成能定位。6.2 返回异常码功能码 0x80如果从站返回的帧里功能码是0x83即0x03 | 0x80说明从站拒绝了请求后面跟一个字节的异常码。常见异常码0x01非法功能码。从站不支持这个功能。0x02非法数据地址。寄存器地址超出从站支持范围。0x03非法数据值。数量参数不合法比如请求读 200 个寄存器但从站最多支持 125 个。0x04从站设备故障。从站内部错误通常需要检查从站本身。我遇到最多的是 0x02尤其是跨品牌设备时。比如某温控器的保持寄存器从 0x0000 开始但另一个品牌的从 0x0001 开始地址偏移没对齐就会报 0x02。解决办法是查从站手册的寄存器映射表确认起始地址。6.3 RTU 转 TCP 网关的典型坑网关是 RTU 和 TCP 混用场景的核心设备也是故障高发区。几个我踩过的坑坑一网关的单元标识符映射配错。网关后面挂多个 RTU 从站时每个从站地址要在网关里注册。如果网关配置里只配了一个单元标识符所有请求都会转发到同一个从站其他从站永远收不到。坑二网关的响应超时设得太短。RTU 从站处理请求需要时间网关如果等 100ms 没收到就从站就返回超时主站会收到异常。把网关超时设到 500ms 以上通常能解决。坑三网关的 TCP 连接数限制。有些低端网关只支持 1~2 个 TCP 连接多个主站同时连会互相踢掉。如果现场有多个上位机要么换网关要么做连接复用。坑四字节序转换。部分网关在 RTU 和 TCP 之间转换时会做字节序处理导致寄存器数据高低字节颠倒。这个要看网关手册有些可以配置有些是固定的。遇到数据明显不对比如温度值变成 256 倍先怀疑字节序。6.4 那个“7-zip CRC 错误”是怎么回事热搜里出现了“装显卡驱动报 7-zip crc错误”这其实是另一个场景的 CRC——压缩包的 CRC 校验失败和 Modbus 的 CRC 只是同名不同物。7-zip 用 CRC 校验压缩数据的完整性如果下载的驱动包损坏解压时就会报 CRC 错误。解决办法是重新下载或者用7z t命令测试压缩包完整性。我提这个是因为很多新手搜“CRC 错误”时会同时看到 Modbus 和压缩包两类结果容易混淆。记住Modbus 的 CRC 是通讯帧校验压缩包的 CRC 是文件完整性校验两者算法可能都是 CRC-16 或 CRC-32但应用场景完全不同。7. 我个人的几条实战心得调试 Modbus 这些年有几个习惯帮我省了大量时间。第一个是永远先抓包再改代码。很多人一遇到通讯失败就开始改程序改了半天发现是线接反了。抓包能看到实际发出的字节一眼就能判断是报文问题还是物理问题。RTU 用串口助手抓TCP 用 Wireshark 抓这是最快定位问题的手段。第二个是把常用请求做成模板。读保持寄存器、读输入寄存器、写单个寄存器、写多个寄存器这四个功能码覆盖了 90% 的场景。我把它们的报文模板和 CRC 计算脚本存成一个小工具需要时改几个参数就能生成报文不用每次从头算。第三个是寄存器地址一定查手册不要猜。不同品牌的设备同一个物理量可能映射到完全不同的寄存器地址。温度、湿度、设定值、报警状态每个都要对着手册确认。我见过有人凭经验填地址结果读回来的是完全无关的数据还以为是通讯问题。第四个是RTU 和 TCP 的代码尽量共用 PDU 层。如果你的项目同时支持两种传输方式把 PDU 的编解码抽成独立模块传输层只负责加信封和拆信封。这样业务逻辑只写一遍减少出错概率。很多成熟的 Modbus 库就是这么设计的比如 Python 的 pymodbus你可以参考它的分层思路。最后说一个关于 502 端口的小技巧。如果你在本地调试 Modbus TCP发现连不上 502 端口先确认服务端是否真的在监听。Linux 下用ss -tlnp | grep 502Windows 下用netstat -ano | findstr 502。有时候服务端绑定的不是 0.0.0.0 而是 127.0.0.1外部就连不上。这个和 HTTP 的 502 错误完全是两码事别被搜索结果带偏了。
返回列表