Modbus 协议逐字节拆解:一帧报文 + 一份 RS-485 现场踩坑大全
博客的第一篇技术文,从 Modbus 开始。它是工控通讯里的”普通话”——几乎所有 PLC、仪表、变频器、传感器都认它。这篇文章分两部分:先把一帧报文逐字节 + 图解看懂;再把我在 RS-485 现场踩过、见过的坑,按物理层 / 时序层 / 协议层分类列全。
一句话理解 Modbus
Modbus 是主从(Master / Slave)+ 一问一答的协议:只有主站发起请求,从站被动应答。
1 | sequenceDiagram |
学 Modbus 核心就三件事:数据放在哪(数据模型)、怎么点名(功能码 + 地址)、报文长什么样(帧结构)。
数据模型:四种”区”
| 数据区 | 读写 | 位宽 | 传统地址 | 典型用途 |
|---|---|---|---|---|
| 线圈 Coils | 读 / 写 | 1 bit | 0xxxx | 开关量输出(继电器、DO) |
| 离散输入 Discrete Inputs | 只读 | 1 bit | 1xxxx | 开关量输入(按钮、DI) |
| 输入寄存器 Input Registers | 只读 | 16 bit | 3xxxx | 采集量(温度、AI) |
| 保持寄存器 Holding Registers | 读 / 写 | 16 bit | 4xxxx | 参数、设定值(最常用) |
实际项目里 90% 的时间你在和”保持寄存器”打交道。
RTU 帧结构:图解 + 逐字节
Modbus RTU 一帧的骨架:
1 | ┌───────────┬──────────┬──────────────────┬───────────┐ |
以”读从站 1 号、保持寄存器从地址 0 开始的 2 个寄存器“为例,主站发:
1 | 01 03 00 00 00 02 C4 0B |
| 字节 | 值 | 含义 |
|---|---|---|
| 1 | 01 |
从站地址 = 1 |
| 2 | 03 |
功能码 = 读保持寄存器 |
| 3–4 | 00 00 |
起始地址 = 0 |
| 5–6 | 00 02 |
寄存器数量 = 2 |
| 7–8 | C4 0B |
CRC(低字节在前) |
两个容易翻车的细节:
- 地址和数量是大端(高字节在前):
00 02= 2 - CRC 是低字节在前:真实值是
0x0BC4,发送顺序却是C4 0B。”数据大端、CRC 小端”的不一致,是新手第一个坑
从站的响应
从站读到 2 个寄存器,值 0x000A(10) 和 0x0064(100),回复:
1 | 01 03 04 00 0A 00 64 DB DA |
| 字节 | 值 | 含义 |
|---|---|---|
| 1 | 01 |
从站地址 |
| 2 | 03 |
功能码(正常回显) |
| 3 | 04 |
后面数据字节数 = 4 |
| 4–5 | 00 0A |
寄存器 1 = 10 |
| 6–7 | 00 64 |
寄存器 2 = 100 |
| 8–9 | DB DA |
CRC |
CRC 校验
Modbus RTU 用 CRC-16/MODBUS:多项式 0xA001,初值 0xFFFF。C 实现(后面通讯库直接用):
1 |
|
拿它算 01 03 00 00 00 02,得 0x0BC4,发送 C4 0B——和报文对得上。
写操作 & 异常响应
写单个保持寄存器(0x06),把地址 1 写成 0x0003:
1 | 请求:01 06 00 01 00 03 98 0B |
如果从站处理不了,会回异常响应:功能码最高位置 1(0x03→0x83),后跟异常码:
1 | 01 83 02 C0 F1 ← 83=读保持寄存器出错, 02=非法数据地址 |
主站怎么判断收到的是数据还是异常?流程如下:
1 | flowchart TD |
常见异常码:01 非法功能码、02 非法数据地址(最常见)、03 非法数据值、04 从站故障、06 从站忙。
Modbus TCP:换了个信封
到以太网上,去掉 CRC(TCP 保证可靠),前面加 7 字节 MBAP 头:
1 | ┌─────────────── MBAP 头(7 字节)──────────────┬───── PDU ─────┐ |
从”功能码”往后(PDU),RTU 和 TCP 完全一样。所以写库时应把 PDU 的组包/解包做成公共层,RTU、TCP 只是外面套不同的”信封”。
RS-485 现场踩坑大全
协议看懂只是第一步。真正让人熬夜的,是 485 总线上那些”时好时坏”的毛病。按层次分类列一遍:
一、时序层(最隐蔽,最容易”偶发”)
1. RTU 的 3.5 字符帧间隔
RTU 没有起止符,靠”总线静默 ≥ 3.5 个字符时间”来判断一帧结束:
1 | 帧1 最后一字节 帧2 第一字节 |
- 现象:偶发”解析失败””长度不对”,尤其高波特率下
- 原因:115200 下 3.5 字符时间才约 0.3 ms,普通串口/USB 转串口根本保证不了这么精确的间隔,两帧被”粘”在一起,或一帧被拆成两段
- 解决:接收端用”空闲超时”重组帧(收到数据后启动定时器,超过约 1.75~3.5 字符时间没有新字节就认为一帧结束);低速场景可适当放宽;对时序敏感的场合直接上带硬件 T35 定时的 MCU 串口
2. 收发方向切换(半双工的命门)
RS-485 是半双工,靠 DE/RE 引脚切换收发方向:
- 现象:从站响应的最后一个字节或 CRC 经常丢,或者主站收到自己发的数据
- 原因:发送完成后过早把方向切回接收——此时 UART 移位寄存器里最后一个字节还没真正发到线上,就被截断了
- 解决:一定要等发送完成中断(TC / TXE,不是 TXE 缓冲空) 再切方向;切换点前后各留一点死区时间。这是自己写 485 驱动时最经典的一个坑
3. USB 转串口的 latency timer
- 现象:用电脑 + USB 转串口调试时莫名丢帧、响应慢,换到嵌入式设备上反而正常
- 原因:FTDI 等芯片默认 latency timer 16 ms,会把零散字节攒一批再上报,彻底打乱帧间隔
- 解决:把 latency timer 调到 1~2 ms(驱动高级设置里改);这个坑排查半天,改一个参数就好
二、物理层(接线和电阻)
4. 终端电阻(120Ω)
1 | 主站 从站1 从站2 从站N |
- 现象:短距离好好的,拉长或提速后误码、丢包
- 原因:阻抗不匹配产生信号反射;缺失、或中间也乱接终端电阻都会出问题
- 解决:只在总线物理两端各接一个 120Ω,中间节点不接
5. 偏置 / 失效保护电阻(failsafe bias)
- 现象:总线空闲时从站偶尔收到乱码、假帧
- 原因:所有节点都不发送时,A/B 之间电平悬空、状态不确定,可能被噪声解析成起始位
- 解决:在总线上加一组上/下拉偏置电阻,保证空闲时是确定的”1”(很多带失效保护的收发器已内置)
6. 共地问题
- 现象:通讯时好时坏,严重时收发器烧毁
- 原因:485 虽是差分,但仍需地参考。多设备不共地、或地电位差过大,共模电压超出收发器范围
- 解决:单独走一根信号地;跨设备/远距离用隔离型 485 收发器或磁隔离
7. A/B(D+/D-)接反、拓扑不对
- 现象:完全通讯不上,或整条总线不稳
- 原因:A/B 接反;不同厂家 A/B 定义还可能相反;用了星型/长分支而不是手拉手菊花链,产生反射
- 解决:接反了对调即可(不会烧);严格走菊花链,分支尽量短
三、协议 / 配置层
8. 地址偏移:40001 还是 0?
组态里填 40001,报文里发的却是 0——那个”4”只表示保持寄存器区,真实偏移是 地址 - 40001。
- 现象:整体读不到、或数据错位一个寄存器
- 解决:跨品牌对接先问清对方文档给的是”传统地址”还是”协议地址”
9. 32 位数 / 浮点的字序
一个 32 位整数或 float 占两个寄存器,谁在前 Modbus 没强制规定,于是 ABCD、CDAB、BADC、DCBA 四种字序现场都能遇到。
- 现象:整数/浮点数值离谱,但又不像是通讯错误
- 解决:先怀疑字序,四种挨个试;库里要把字序做成可配置项
10. 串口参数不匹配 / 地址冲突 / 广播
- 参数:波特率、校验位、停止位有一个不对就全乱码——最基础也最常见,先查这个
- 地址冲突:两个从站同地址,总线互相打架,表现为随机错乱
- 广播(地址 0):写广播时从站不响应,别傻等超时
小结
Modbus 协议本身很简单:地址 + 功能码 → 一问一答 → 校验。但工程上 80% 的时间花在上面那些 RS-485 现场坑里——时序(帧间隔、收发切换)、物理(终端/偏置/共地)、配置(地址/字序/参数)。把这份清单存下来,现场排障能少熬很多夜。
接下来我会开源一个用 C/C++ 写的多协议通讯库,第一个实现 Modbus RTU/TCP。这篇里提到的 PDU 公共层、CRC、字序处理、帧间隔重组、方向切换时序,都会变成实实在在的代码。进展同步在这里。
现场还踩过哪些这里没列到的坑?欢迎评论区或加我微信一起补充。