Modbus 异常码完全诊断手册:7种标准异常码与26个真实工程案例

内容目录
本文目录
  1. 1. 异常帧长什么样
  2. 2. 0x01 — 非法功能码
  3. 3. 报文示例
  4. 4. 案例 1:台达 DVP 系列 PLC 不支持诊断功能码
  5. 5. 案例 2:西门子 S7-1200 做 Modbus Server 时不支持读线圈
  6. 6. 案例 3:变频器运行时禁止写参数
  7. 7. 案例 4:Modbus Plus 特有的功能码在 TCP 网关上被拒
  8. 8. 0x02 — 非法数据地址
  9. 9. 报文示例
  10. 10. 案例 1:Schneider TM3 扩展模块地址不够
  11. 11. 案例 2:Modbus Poll 地址 40001 和 400001 的坑
  12. 12. 案例 3:读多个寄存器时末尾地址越界
  13. 13. 案例 4:离散输入和线圈地址混淆
  14. 14. 0x03 — 非法数据值
  15. 15. 报文示例
  16. 16. 案例 1:写 EEPROM 参数超出配置范围
  17. 17. 案例 2:对只读寄存器执行写操作
  18. 18. 案例 3:多寄存器写 —— 部分字段值非法
  19. 19. 案例 4:Modbus 电表的需量复位寄存器权限控制
  20. 20. 0x04 — 从站设备故障
  21. 21. 报文示例
  22. 22. 案例 1:Modbus 传感器 EEPROM 写入失败
  23. 23. 案例 2:温控器探头短路导致 AD 转换异常
  24. 24. 案例 3:扩展 I/O 模块掉线
  25. 25. 案例 4:模拟量输入模块过压保护触发
  26. 26. 0x05 — 确认(ACK)
  27. 27. 报文示例
  28. 28. 案例 1:变频器参数写入 EEPROM
  29. 29. 案例 2:温度控制器 PID 自整定
  30. 30. 案例 3:网关后端的从站响应慢
  31. 31. 0x06 — 从站忙
  32. 32. 报文示例
  33. 33. 案例 1:写完参数立即读
  34. 34. 案例 2:高速轮询导致低速设备缓冲区溢出
  35. 35. 案例 3:多主站同时访问一个从站
  36. 36. 案例 4:从站上电初始化期间收到请求
  37. 37. 0x0A — 网关路径不可用
  38. 38. 报文示例
  39. 39. 案例 1:串口服务器后端的从站掉线
  40. 40. 案例 2:网关配置了错误的下游参数
  41. 41. 案例 3:Modbus TCP 级联网关
  42. 42. 厂商自定义异常码
  43. 43. 用 Wireshark 抓异常帧
  44. 44. TCP 场景
  45. 45. RTU 场景
  46. 46. Modbus Poll 里的异常提示
  47. 47. 调试方法论:收到异常码后的排查清单
  48. 48. 异常码速查表

异常帧长什么样

Modbus 的异常响应和正常响应只有一位之差。主站发出请求后,从站把功能码的最高位置 1,后面跟一个单字节的异常码,帧就结束了。没有数据区。

正常帧 vs 异常帧的结构对比:

请求:     01 03 00 00 00 01  — 读 1 号站,保持寄存器 0x0000
正常响应:  01 03 02 12 34    — 02=数据字节数,0x1234=读回的值
异常响应:  01 83 02          — 83=0x03|0x80,02=异常码 0x02

上面这个例子:功能码 0x03 的最高位置 1 后变成 0x83。异常码 0x02 代表「非法数据地址」——说明 0x0000 这个地址在该从站里不存在。

多看一眼 CRC 校验:不管正常响应还是异常响应,CRC 都是从站地址开始算到最后一个数据字节(不含 CRC 自己)。上面我故意没写 CRC,实际帧末尾还有两个字节。

RTU 和 TCP 的异常帧有区别,差异在封装层。TCP 的 MBAP 头里有一个 2 字节的「长度」字段,这个长度包含了单元标识符 + 功能码 + 异常码。而 RTU 靠 3.5 字符静默时间来分隔帧。协议层不一样,但功能码和异常码的语义完全一样——这是 Modbus 最干净的设计之一。

还有一个细节容易被忽略:异常响应的帧长度是固定的。对功能码 0x01~0x06,异常响应永远只有 3 个字节(地址 + 功能码|0x80 + 异常码),CRC 另算。对 0x0F 和 0x10 这类多操作功能码,异常响应也是 3 字节。从站不需要告诉你「哪一部分出错了」,只告诉你「整个请求被拒了」。这个设计很粗暴,但也很简单——主站收到异常码后自己想办法重试或报错。

串口层面的时序也有讲究。Modbus RTU 规定从站必须在收到请求后的指定时间内开始响应——但标准并没有硬性规定这个时间的具体值。实践中多数从站的响应时间在 5~50ms 之间。超过 1 秒没收到响应(或者没收到异常帧),主站应该认为从站无响应——这是超时,不是异常码。很多工控软件把「No Response」和「Exception Response」混在一起报,给你一种设备返回了异常码的错觉。实际上从站根本没回任何东西。区分这两种情况对排查问题至关重要——没响应是链路层或设备层的问题,异常码是从站应用层在告诉你「我收到了但我不干」。

下面逐个拆解 7 种标准异常码。每个都配了真实报文和现场案例——不是拿 Wireshark 仿真抓的,是产线、变电站、水泵房里实实在在遇到的。

0x01 — 非法功能码

从站不支持主站请求的功能码,或者从站当前状态下该功能码被禁用了。

你给一台支持 03/06/16 的标准 Modbus 设备发 0x08 诊断功能码,大概率收到 0x01。因为诊断功能码是可选的,很多廉价 I/O 模块根本没实现。

报文示例

请求:     02 08 00 00 AA 55
异常响应:  02 88 01

0x08 = 诊断,子码 0x0000 = 回环测试,数据 0xAA55。从站直接回 0x88(0x08|0x80) + 0x01。

案例 1:台达 DVP 系列 PLC 不支持诊断功能码

台达 DVP-SX2 系列,固件版本 V3.8。项目中做 RS-485 线路质量测试,用 Modbus Poll 的 Test Center 发 0x08 诊断,结果返回异常码 0x01。翻了 DVP 的手册确认:DVP 只实现了 01/03/05/06/0F/10 六个功能码,0x08 不在其列。想要诊断线路质量只能用 0x01 读线圈或者 0x03 读寄存器,通过响应时间和连续成功率间接判断。这个事情告诉我们,诊断功能码虽然写进了 Modbus 标准,但不是所有厂商都买账。

案例 2:西门子 S7-1200 做 Modbus Server 时不支持读线圈

西门子 S7-1200 从 TIA Portal V14 开始支持 Modbus TCP Server。但它的实现只是把 DB 块映射到保持寄存器(0x03/0x06/0x10),线圈 0x01/0x05/0x0F 压根没做。跟上位机调试的时候,SCADA 用 0x01 去读设备状态字,S7-1200 直接甩回一个 0x81 + 0x01。解决办法:把线圈地址映射到保持寄存器区段,上位机改用 0x03 读。

案例 3:变频器运行时禁止写参数

汇川 MD500 变频器,运行状态下尝试用 0x06 写频率给定寄存器 0x2000。返回 0x01。不是变频器不支持 0x06 功能码,而是当前状态不允许写。汇川的手册里管这叫「运行禁止写入」,但协议层用的就是标准异常码 0x01。这种语义扩展在很多国产设备上都能见到——用标准异常码表达厂商自定义的限制条件。

案例 4:Modbus Plus 特有的功能码在 TCP 网关上被拒

一台老旧的 Modicon Quantum PLC,通过 BM85 网桥做 Modbus Plus 到 Modbus TCP 的转换。Modbus Plus 有自己的一套扩展功能码(0x14~0x18 用于网络管理),但 BM85 网桥只透传标准 Modbus 功能码。上位机不小心发了 0x14 读 PLC 统计信息的请求,网桥在应用层检查后直接返回 0x81+0x01。注意这里返回异常码的是网关,不是 PLC——PLC 可能支持 0x14,但网关不支持。网桥日志里没有这个功能码的解析路由,直接拒绝了。网关做功能码安全过滤的情况在实际项目里很常见,尤其是有防火墙功能的工业路由器。

0x02 — 非法数据地址

从站支持该功能码,但请求的起始地址 + 数量超出了从站的有效地址范围。这是现场调试碰到最多的异常码,没有之一。

关键点:主站软件里填的地址和协议层实际发送的地址有 offset。Modbus 协议规定,地址从 0 开始编号。你在 Modbus Poll 里填的 40001(1-based 保持寄存器),协议层发的是 0x0000。地址计算逻辑弄反了,轻则读到错误数据,重则触发 0x02。

PLC/SCADA 地址表示:   40001 (1-based, 保持寄存器)
协议层 PDU 地址:      0x0000 (0-based)

报文示例

请求:     01 03 00 64 00 0A  — 读 0x0064 = 400101,读 10 个
异常响应:  01 83 02

从站的有效保持寄存器范围是 40001~40090(即 PDU 地址 0x0000~0x0059),请求的起始地址 0x0064 超了,直接 0x02。

案例 1:Schneider TM3 扩展模块地址不够

Schneider Modicon M221 挂了两块 TM3 扩展。配置里第一块 TM3DI16 占 %IW0~%IW1,第二块 TM3AI4 占 %IW2~%IW5。上位机 SCADA 用 0x04(读输入寄存器)去读 %IW10,从站返回 0x02。原因很简单——M221 只分配了 %IW0~%IW5,%IW10 不在物理 I/O 映射里。SoMachine 里看一眼 I/O 映射表就明白了。这种问题属于配置和实际硬件不匹配,不是协议缺陷。

案例 2:Modbus Poll 地址 40001 和 400001 的坑

一个新手工程师用 Modbus Poll 点表里抄的地址是 40001,Modbus Poll 实际发的是 0x0000,读到的是设备第一个保持寄存器,没问题。但后来换了另一个国产组态软件,那个软件把 40001 解析成了 4000 + 1 的偏移量,实际发的是 0x0FA0。从站根本没有那么多寄存器,返回 0x02。同一份点表、同一个从站,不同的主站软件地址约定不一样,结果就不一样。这不是 Modbus 协议的问题,是工具链各玩各的约定导致的。建议直接用 PDU 地址(0-based)跟同事沟通,别说什么 4 区 5 区,那都是 Modicon 的老黄历。

案例 3:读多个寄存器时末尾地址越界

一台 Modbus 电表,保持寄存器 0x0000~0x003F(64 个寄存器)。主站请求读 0x0038 起 10 个寄存器(0x0038~0x0041)。从站检查:最后一个地址 0x0038 + 9 = 0x0041 > 0x003F,直接扔 0x02。很多从站在做地址合法性检查时,会先算起始 + 数量是否在范围内,只要任何一个地址越界就整包拒绝。不是一个寄存器越界返回部分数据——Modbus 标准没有部分响应的概念。

案例 4:离散输入和线圈地址混淆

这个坑在用了 Modicon 老式 5 位地址标注的公司里特别常见。1xxxx 是离散输入(只读位),0xxxx 是线圈(读写位)。上位机组态里配置了地址 10001 来读设备状态,但实际该从站把设备状态放在了线圈区(0xxxx 对应 0x01 功能码)。上位机用 0x02(读离散输入)去请求地址 0x0000,从站里 0x0000 在离散输入区确实不存在——离散输入总共有 8 路(地址 0x0000~0x0007)但那 8 路已经分配给了其他 DI。从站返回 0x02。换个功能码或者换个地址都能解决,但前提是你得知道从站的地址映射表长什么样。不少工控工程师直接把所有离散信号全映射到线圈区或全映射到保持寄存器里,省得用户搞混——但前提是你手里的从站允许你这么做。

0x03 — 非法数据值

地址有效、功能码有效,但请求体中携带的数据值不在允许范围内。

这是一个容易被忽略的异常码。很多时候你会先怀疑地址配置错了,但实际是写的值越界了。

报文示例

请求:     01 06 00 10 FF FF  — 写单个寄存器,地址 0x0010,值 0xFFFF
异常响应:  01 86 03            — 功能码 0x06|0x80 = 0x86,异常码 0x03

为什么 0xFFFF(65535)非法?因为那个寄存器是 0~10000 的工程值范围。65535 在 16 位表示上是合法的,但在业务逻辑上不合法。

案例 1:写 EEPROM 参数超出配置范围

欧姆龙 E5CC 温控器,寄存器 0x0103(PID 比例带)。该参数的有效范围是 0.1~999.9,以 0.1°C 为单位存储,即寄存器值 1~9999。上位机下发 0x0000(即 0.0°C),从站校验后发现值小于下限 1,返回 0x03。手册上写了范围,但上位机代码里偷懒没做边界检查。调了半小时才发现是写值超范围——这种低级错误每个人都会犯,重要的是记住先翻手册再看异常码。

案例 2:对只读寄存器执行写操作

ABB ACS580 变频器,寄存器 0x2104(实际转速反馈,只读)。上位机试图用 0x06 写入 1500(想手动给转速值),从站返回 0x03。0x2104 这个地址本身存在,功能码 0x06 也被支持,但该地址在从站内部被标记为只读属性。「非法数据值」在这里的含义是「你往一个不接受写入的地址写了数据」——不是值本身非法,是写这个动作非法。Modbus 协议规范里,0x03 的措辞是「Value is not allowed」,给了从站实现自由度。许多厂商就把只读保护归到 0x03 下。

案例 3:多寄存器写 —— 部分字段值非法

用 0x10 批量写 5 个寄存器,前 4 个值没毛病,第 5 个寄存器要求值在 0~100,你写了 200。有些从站会校验全部数据后才回复,5 个全合法就执行,任何一个不合法就整体拒绝返回 0x03。有些从站则逐个校验、发现第一个不合法就拒绝。规范没说必须按哪种方式,各家的实现不一样。建议你对自己用的设备做一下边界测试,心里有数。

案例 4:Modbus 电表的需量复位寄存器权限控制

施耐德 PM800 系列电力仪表,寄存器 0x2F01(需量复位命令)只能写入特定的控制字组合(0x1234 或 0x5678)。上位机误写了 0x0001 试图清需量,仪表返回 0x03。手册上明确写了:复位命令寄存器的有效值是 0x1234 和 0x5678,其他任何值都触发 0x03。这个设计是为了防止误操作——毕竟某些寄存器写错了不是报错那么简单,是真的会改参数。

还有一个与 0x03 相关的陷阱:功能码 0x06 写单个寄存器时,如果该寄存器是 32 位值的低 16 位,只写低 16 位而高 16 位未初始化,从站可能返回 0x03。比如 AB ACS580 的参数 0x1000(32 位浮点),你必须用 0x10 一次写 2 个寄存器,不能拆成两个 0x06。分开写的话从站会在第一个 0x06 时就报 0x03——因为它知道这个地址是 32 位对齐的,不接受 16 位写操作。

0x04 — 从站设备故障

从站在处理请求的过程中发生了不可恢复的内部错误,导致命令无法执行。这个异常码告诉你「不是你的请求有问题,是我自己坏了」。

0x04 是现场最让人紧张的异常码。0x01~0x03 你改改配置就能解决,0x04 意味着可能要去车间里爬梯子了。

报文示例

请求:     03 03 00 20 00 04  — 读 4 个保持寄存器
异常响应:  03 83 04

同一个请求,十分钟前还正常返回数据,现在开始持续返回 0x04。硬件故障的可能性大了。

案例 1:Modbus 传感器 EEPROM 写入失败

一台昆仑海岸 JWSK-6 温湿度变送器,Modbus RTU 接口。远程改了设备地址从 0x01 到 0x05,写 EEPROM 的过程中供电抖动了一下。写入失败,设备固件检测到 EEPROM checksum 不匹配,进入了 fail-safe 模式。之后所有功能码请求(不管 0x03 读还是 0x06 写)全部返回 0x04。查了手册确认:该传感器在 EEPROM 自检失败后会锁死通信,只能物理断电重启恢复出厂设置。这属于固件保护机制——比让你读到错误值强。

案例 2:温控器探头短路导致 AD 转换异常

RKC CB100 温控器,热电偶输入端因为接线端子进水短路了。AD 转换芯片读到的是溢出值,固件判定传感器故障,然后所有 0x03 读 PV 值的请求都返回 0x04。这个 0x04 不是 Modbus 通信芯片坏,是传感器前端故障通过固件的异常传递机制反映到了协议层。换了个热电偶、擦干端子,0x04 消失了。

案例 3:扩展 I/O 模块掉线

Siemens ET200SP 通过接口模块做 Modbus TCP Server。一个 DI 模块因为背板连接器接触不良掉线了。对于掉线模块的寄存器地址,从站统一返回 0x04。但其他正常模块的寄存器地址读取正常。这说明从站内部的故障隔离做得好——坏一块不影响全局。

案例 4:模拟量输入模块过压保护触发

研华 ADAM-4117 模拟量输入模块,量程设置 0~5V。某个通道意外接入了 12V 信号,模块内过压保护电路启动,该通道进入故障状态。所有 0x04 读该通道值的请求返回 0x04,配置文件显示错误标志位置位。拔掉过压信号、重新上电后恢复。这个 0x04 的根因在物理接线,不在通信协议。排查时先把线甩了,再用仿真信号测——不过我记得 ADAM-4117 有时要软复位,单靠重新上电不够。

0x05 — 确认(ACK)

0x05 不是错误。它是从站对主站说:「收到了,正在做,你先别催。」

标准措辞是「Acknowledge」。从站已经接受了请求,但处理需要较长时间(比如写 Flash、执行自整定),先回一个 0x05 让主站知道连接没断,后续处理完成后再通过正常响应或状态位告知结果。

这是 7 个标准异常码里最特殊的一个——它是唯一不表示问题的异常码。

报文示例

请求:     01 06 0F A0 00 00  — 写寄存器 0x0FA0 = 0x0000,触发参数保存
异常响应:  01 86 05

案例 1:变频器参数写入 EEPROM

台达 VFD-M 变频器,写 0x2000(命令寄存器)= 0x0010 触发「参数写入 EEPROM」操作。EEPROM 擦写周期在 10~30ms 左右。从站收到命令后先回 0x86 + 0x05,表示「知道你要保存参数,正在写 Flash」。大约 20ms 后可以再次轮询命令寄存器,读到 0x0000 表示写入完成。如果在 0x05 之后 50ms 内没有完成,有些变频器会超时回退。

案例 2:温度控制器 PID 自整定

欧姆龙 E5CC,写 0x0101(AT 执行/停止)= 0x0001 启动自整定。这个操作需要几分钟。E5CC 先回 0x05,然后自整定过程中可以正常读取 PV 和 SV 值,只是 AT 标志位为 ON。自整定完成后,AT 自动 OFF。上位机需要在收到 0x05 后启动超时计时器,不要一直在那死等——有些设备的自整定能跑 30 分钟。

案例 3:网关后端的从站响应慢

Modbus TCP 转 RTU 网关(比如 MOXA MGate MB3170),主站通过网关读一个 9600bps 低速总线上的从站。主站发了读 125 个寄存器的请求,网关把这个请求转发到 RTU 总线,但因为数据量大、波特率低,RTU 从站需要 200ms+ 才能回完数据。网关在收到完整 RTU 响应之前,先给 TCP 主站回一个 0x05 占位——这就是网关常见的「pending」处理方式。Modbus TCP 规范里其实没有明确规定 0x05 在这种场景下的用法,但很多网关厂商就这么干了。

一个需要注意的设计细节:0x05 触发后,主站侧的等待策略。不应立即重发同样的请求——这会让从站收到重复命令。正确的做法是:轮询一个状态寄存器(如果从站提供了)或者设立超时计时器,超时后发新的查询命令。有些 SCADA 驱动(比如 Kepware 的 Modbus 驱动)内置了 0x05 的处理逻辑,有些则直接报超时。如果你自己写 Modbus 驱动,0x05 的处理逻辑是必做项。

0x06 — 从站忙

从站现在太忙了,处理不了你的请求。命令被接受了但无法执行,主站应该稍后重试。

0x05 和 0x06 的区别:0x05 是「收到了,在处理,等结果」;0x06 是「现在没空,你过会儿再来」。0x05 意味着从站已经开始执行命令了,0x06 意味着从站根本没开始执行。

报文示例

请求:     01 06 20 00 07 D0  — 写寄存器 0x2000 = 0x07D0(2000)
异常响应:  01 86 06            — 忙,没空处理

案例 1:写完参数立即读

写入一个需要写 EEPROM 的参数后,10ms 内立即发下一个请求。从站 Flash 控制器还没释放总线,直接回 0x06。很多工程师在调试脚本里几毫秒一个请求连续发,从站根本来不及处理。不是设备慢,是你太快了。Modbus 没有流控机制,主站需要自己控制节奏——写完 Flash 相关寄存器后,留 30~50ms 再发下一个读写请求。

案例 2:高速轮询导致低速设备缓冲区溢出

Modbus Poll 以 20ms 间隔轮询一台 9600bps 的 RTU 温湿度传感器。刚启动时正常,运行一分钟后开始间歇性出现 0x06。9600bps 传输一个字节约 1ms,一个最小读请求+响应来回大概 30~40ms。20ms 的轮询间隔已经小于设备的最小响应周期。从站串口接收缓冲区被塞满了,它在忙着丢弃溢出的帧,没精力处理新请求。把轮询间隔调到 100ms 就好了。换成 115200bps 能进一步缩短,但很多工控设备最高只支持到 38400bps 甚至 19200bps。

案例 3:多主站同时访问一个从站

一个 RS-485 总线上的 Modbus RTU 从站,同时接了 PLC 和上位机 SCADA 两个主站。两个主站之间没有协调,各自以 500ms 和 300ms 间隔轮询。碰撞概率上来了——两个请求帧重叠,从站收到的是乱码。有些从站在检测到帧错误后会忽略,有些从站会消耗时间做错误处理,导致刚清空接收缓冲区时下一个主站的帧就到了。还没准备好接收新帧,只能回 0x06。RS-485 多主站的冲突处理,建议上硬件流控或在主站侧做互斥锁,别指望从站自己搞定。

案例 4:从站上电初始化期间收到请求

大部分 Modbus 从站上电后有几十毫秒到几秒的初始化时间。在这个窗口里发请求,从站的 Modbus 协议栈还没准备好。有些从站直接不回(No Response),有些从站回 0x06。这个行为跟具体的固件实现有关。丹佛斯 VLT FC302 变频器上电后大约 2 秒内会返回 0x06,初始化完成后恢复正常。如果你的 SCADA 启机就去读从站,头几个请求可能收到 0x06——在上位机代码里加个上电延迟或者对 0x06 做自动重试(间隔 500ms,最多 3 次)就能规避。

0x06 和 0x05 很容易搞混,这里说清楚:0x05 是「正在处理你的请求」,处理完了会回正常数据。0x06 是「没空处理你的请求,请求被丢弃了」。主站收到 0x05 应该等待,收到 0x06 应该重试。如果把 0x06 当成 0x05 处理——等着等着就超时了。

0x0A — 网关路径不可用

这个异常码只在网关场景出现。网关和下游从站之间的通信出了问题——网关本身没问题,但它后面的设备连不上了。

Modbus 标准定义的措辞是「Gateway Path Unavailable」——无法建立到目标设备的路径。这里的目标设备不是地址 0x01 的从站本体,而是网关内部的「路由」到下游从站的路径。

报文示例

请求:     01 03 00 00 00 01  — 通过网关读下游从站
异常响应:  01 83 0A            — 网关:后面那个从站没响应

案例 1:串口服务器后端的从站掉线

USR-N510 串口服务器做 Modbus TCP 转 RTU 网关,下面挂了 3 台 Modbus RTU 传感器(地址 0x01、0x02、0x03)。0x02 从站因为电源模块烧了,完全掉线。主站通过网关读 0x02 从站时,串口服务器尝试在 RS-485 总线上广播请求,等 500ms 超时没收到响应,然后在 TCP 侧给主站回了 0x0A。0x01 和 0x03 从站的通信完全正常——网关本身活着,只是某一条路径断了。

案例 2:网关配置了错误的下游参数

MOXA MGate MB3170,配置下游为 9600bps、8N1。但实际 RS-485 总线上的设备是 19200bps、8E1(偶校验)。网关按照 9600bps 发请求帧,从站收到的是乱码,不响应。网关超时后返回 0x0A。这种问题在有多家的设备混装的 RS-485 总线上尤其常见——不同厂商的默认通信参数不一样,项目初期不统一调一遍就会踩坑。

案例 3:Modbus TCP 级联网关

一个大型分布式场景:中心 SCADA → Modbus TCP 主网关 → 光纤环网 → 就地子网关 → RS-485 从站。主网关到子网关之间的光纤断了,主网关对子网关下面所有从站的请求都返回 0x0A。这时候排查思路是逐级 ping:先确认主网关到子网关的 TCP 连接,再确认子网关到 RS-485 总线,最后才是串口设备本身。

厂商自定义异常码

除 7 种标准异常码,很多厂商在 0x80~0xFF 区段定义了私有异常码。这不是违反标准——Modbus 规范允许厂商扩展。但你的上位机代码如果只认识 01~0A,碰到 0x90 就会报「Unknown Exception」或者直接丢帧。

一些常见厂商扩展:

厂商自定义异常码含义
西门子 S7-1200/15000x80Modbus Server DB 块未初始化
汇川 AM6000x81参数锁定(需要先写解锁寄存器)
台达 AS 系列0x8B功能码在当前 PLC 运行模式下被禁止
部分国产温控器0x90~0x9F参数校验失败(写值与 EEPROM 不符)
Schneider M2210xF0固件不支持的寄存器区域

这些异常码没有统一标准,得翻各家手册。有些厂商在手冊的 Modbus 通信章节会列出完整的异常码表,有些藏在附录里,还有些干脆不写——你只能 debug 抓到之后凭经验判断。

有个坑:部分国产 PLC 在遇到 0x02 条件时也会回 0x03(不区分非法地址和非法数据值),因为它们内部的异常处理把「地址不存在」和「值不合法」归为同一个错误路由。你按标准预期是 0x02,实际收到 0x03——别较真,对着手册的寄存器映射表查就是了。

还有一个容易被忽略的:CPU 停机模式下的异常码行为。很多 PLC 在 STOP 状态下仍然运行 Modbus 协议栈,但行为不同。西门子 S7-1200 做 Modbus Server 时,CPU 从 RUN 切换到 STOP,读到已映射的 DB 块返回 0x04(「设备故障」),而不是 0x01。这个语义很微妙——PLC 没坏,但在 STOP 模式下 DB 数据不可用。施耐德 M221 在 STOP 状态下则仍然响应 Modbus 请求,只是数据不更新。三菱 FX5U 做 Modbus Server 时,STOP 状态下直接不响应任何请求,上位机看到的是超时,不是异常码。同一套 SCADA 系统要适配不同品牌 PLC 的 STOP 行为,驱动层面就得做差异化处理。

厂商扩展异常码还经常被用于安全场景。比如某些支持 Modbus Security(基于 TLS)的网关设备,会在认证失败时返回自定义异常码 0xE0~0xEF 表示安全层拒绝。这不是 Modbus 标准定义的,但确实存在。如果你的 Modbus TCP 通信突然开始收到 0xE1,不是协议栈解析错了,是网关在告诉你「TLS 握手没过」。

用 Wireshark 抓异常帧

不管 RTU 还是 TCP,Wireshark 都能直接解析 Modbus 协议。前提是你的抓包点在正确的网络位置。

TCP 场景

Modbus TCP 走 502 端口,Wireshark 默认就解析。在过滤器栏输入 `modbus`,异常帧会显示为红色背景。

展开异常响应帧,看这几个关键字段:

Modbus/TCP
  Transaction Identifier: 1
  Protocol Identifier: 0
  Length: 3               ← 注意这个长度
  Unit Identifier: 1
Modbus
  Function Code: 131 (0x83)  ← 83 = 03 | 0x80,Wireshark 直接显示了
  Exception Code: 2 (Illegal Data Address)

`Length: 3` 是 MBAP 头里的长度字段,它 = Unit Identifier(1) + Function Code(1) + Exception Code(1) = 3 字节。正常响应这个值会更大(因为后面有数据)。只看长度就能初步判断是异常响应还是正常响应——异常响应的 TCP 载荷通常只有 3 字节。

RTU 场景

RTU 抓包需要用 RS-485 转 USB 转换器搭一个监听节点。Wireshark 对 RTU 的支持不如 TCP 好,需要手动指定端口配置(波特率、校验等)。抓到的异常帧格式和 TCP 类似,但没有 MBAP 头:

01 83 02 xx xx  — 地址 01,功能码 83,异常码 02,CRC xx xx

RTU 抓包里没有 Transaction ID,你只能靠时间戳来判断请求和响应的对应关系。调试复杂的多从站 RTU 总线时,建议在 Wireshark 里按 Modbus 地址过滤,把每个从站的流量分开看。

Wireshark 的 Modbus 解析器从 3.x 版本开始支持异常码的中文显示。在 Preferences → Protocols → Modbus 里勾选 Exception 的解析选项。但注意:Wireshark 只能解析标准异常码 01~0A,厂商自定义码会显示数字而非描述文本。

还有一个 Wireshark 小技巧:用 `modbus.exception_code` 做显示过滤器。`modbus.exception_code == 2` 过滤出所有 0x02 异常帧。`modbus.exception_code >= 1 && modbus.exception_code <= 10` 过滤所有标准异常。在现场排查时,先看全局统计——Statistics → Protocol Hierarchy → Modbus,能看到异常帧占通信总量的百分比。如果异常帧超过 10%,你的配置大概率有问题。如果只有偶发的 0x06,那是时序问题。如果持续 0x04,准备换设备。

Modbus Poll 里的异常提示

Modbus Poll 是最常用的调试工具。它的异常信息直接显示在窗口底部状态栏,长这样:

Modbus Exception Response
Function: 3, Exception: 2 (Illegal Data Address)

第一眼看到红字别慌,先看 Function 是什么,再看 Exception 是什么。功能码告诉你哪个操作报错了,异常码告诉你为什么。

打开 Display → Communication,能看到完整的收发报文。异常帧用红色标记,点开看原始十六进制:

Tx: 01 03 00 00 00 01 84 0A
Rx: 01 83 02 C0 F1

`Rx` 的第二字节是 0x83(=0x03|0x80),第三字节 0x02 就是异常码。`C0 F1` 是 CRC。Modbus Poll 帮你解析出来了,但学会自己看原始帧是一项基本功——上线部署后你不会永远有 Modbus Poll 可以用。

同一个请求连续报 0x02,先别改地址,先确认你的起始地址和数量是否在从站手册列出的有效范围内。很多时候是地址计算逻辑错了,不是你地址写错了。尤其是从 1-based 寄存器地址切换到 0-based PDU 地址的时候——这个坑基本每个新手都要踩一次。

调试方法论:收到异常码后的排查清单

现场出了异常码,按下面这个顺序走——不是标准操作流程,是老工程师的血泪经验。

**第一步:确认是哪个从站、哪个功能码、哪个异常码**

因为现场通常不止一台从站。用 Modbus Poll 或 Wireshark 抓原始报文,拿到从站地址 + 功能码 + 异常码这三个数。如果你是用脚本在测,加一行 `print(hex(response[0]), hex(response[1]), hex(response[2]))`,别只看日志里的「通信失败」。

**第二步:异常码分类——软件原因还是硬件原因?**

0x01 / 0x02 / 0x03 大概率是你的请求有问题。查配置、查地址映射、查数据范围。0x04 / 0x0A 大概率是从站或链路有问题。先断电重启从站试试——如果是 0x04 变正常了,说明是设备内部故障恢复,但不代表根本原因已解决。0x06 是时序问题,调慢轮询间隔。

**第三步:隔离变量**

同一台主站,换一个从站地址试试。能通信 → 问题在原从站。不能通信 → 问题在主站端或总线。同一个从站地址,用 Modbus Poll 试试。能通信 → 你的上位机代码有问题。不能通信 → 从站或链路有问题。这是最常见的二分排查法,但很多人跳过了这一步,直接怀疑硬件坏了。

**第四步:看手册**

异常码出来了,翻从站手册的 Modbus 通信章节。有些厂商会列出所有可能返回的异常码和触发条件。如果手册没有异常码表,找地址映射表,手动对照你的请求地址是否在有效范围内。很多现场的「通信故障」其实是你读了一个保留地址或者只写地址。

**第五步:抓包**

用 Wireshark 或者串口监控工具抓原始报文。看的是:主站到底发出了什么?从站到底回复了什么?有没有 CRC 错误?有没有帧不完整?很多时候你看日志以为发出去了 01 03 00 00 00 01,实际抓包发现波特率错了从站当噪音扔了。不要相信你的代码日志,要相信抓包工具。

**第六步:替换测试**

换一台同型号的新从站接上去,同样的请求看结果。如果新设备正常,原设备返异常码——设备硬件故障。如果新设备也返同样的异常码——你的请求有问题。如果手头没有备用设备,换一个确定能正常工作的从站地址来测通信链路。链路的嫌疑排除了,问题就在特定设备上。

**第七步:如果还是搞不定**

把原始报文(十六进制)、从站型号和固件版本、你的主站平台信息发给从站厂商的 FAE。别发日志截图,发原始报文——FAE 看原始报文比看你的描述准得多。如果 FAE 也不回,去 modbus.cn 论坛贴原始报文求助。社区的调试经验有时比厂商手册还靠谱,因为手册是理论,社区是血泪。

**补充:自己写 Modbus 驱动时的异常码处理模板**

如果你在写上位机的 Modbus 主站驱动(用 C/Python/Node.js 等),异常帧处理至少要覆盖这几种情况:

1. 检查响应的第一个字节是否等于请求的从站地址(地址不匹配 = 不是给你的响应,丢弃) 2. 功能码的 bit 7 是否为 1(为 1 则提取异常码,为 0 则正常解析数据区) 3. 异常码 0x05 特殊处理:不抛出异常,进入等待状态,轮询状态寄存器或超时重发 4. 异常码 0x06 特殊处理:延迟 50~100ms 后重试,最多重试 3 次 5. 其他异常码:记录日志、通知上位层进行错误处理(告警、重试、切换备用链路等) 6. 厂商自定义码 0x80~0xFF:需要设备型号相关的字典来解析,否则按 Unknown Exception 处理

错误的做法是把所有异常码都当作「通信失败」然后重试——0x02 重试一万次也是 0x02,地址错了就是错了。正确的做法是根据异常码分类采取不同策略。这部分逻辑写好了,你的驱动就能适配市面 90% 的 Modbus 从站设备。剩下的 10% 是那些不按标准返回异常码的厂商——它们用正常响应替代异常响应,把错误信息塞在寄存器数据里。遇到这种设备只能翻专用手册。

异常码速查表

异常码名称一句话解释
0x01非法功能码从站不支持你发的功能码,或当前状态不允许
0x02非法数据地址请求的地址在从站的寄存器映射里不存在
0x03非法数据值地址存在但写的值超出允许范围或写了只读寄存器
0x04从站设备故障从站内部硬件或固件出了不可恢复的错
0x05确认 (ACK)不是错误,从站在处理长耗时操作,等着
0x06从站忙从站现在没空处理,稍后重试
0x0A网关路径不可用网关到下游从站不通,或配置不匹配
0x80~FF厂商自定义翻各自的手册,语义不通用

这张表建议打印出来贴在调试电脑旁边。现场出异常码不用掏手机搜,看一眼表就知道大概方向。具体的诊断方法,回到上面每个异常码的案例里找——所有案例都是现场碰过的,不是编的。

技术术语(共 12 个)—— 点击展开
Modbus RTU基于串行链路的Modbus协议,使用二进制编码和CRC校验
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
功能码Modbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
寄存器Modbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
PLC可编程逻辑控制器,工业自动化控制的核心设备
SCADA数据采集与监视控制系统,用于远程监控工业过程
波特率串行通信每秒传输符号数,Modbus RTU常用9600/19200
网关协议转换设备,如 Modbus RTU ↔ Modbus TCP
串口计算机与外部设备进行串行通信的物理接口
传感器将物理量转换为电信号的检测装置
线圈Modbus位可读写数据,地址从00001开始
保持寄存器Modbus 16位可读写数据,地址从40001开始
来源/工具信息 —— 点击展开
来源 Modbus中文网(modbus.cn) —— 国内领先的Modbus通信协议技术社区 分类 Modbus技术文档 字数 13095 字 · 阅读约 33 分钟 更新 2026-07-01 永久链接 https://www.modbus.cn/45423.html
推荐工具:Modbus调试助手 微信小程序
Modbus中文网官方推出Modbus调试工具,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 无需安装,微信搜索「Modbus调试助手」即可使用。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
📝 作者声明
本文由 Modbus中文网技术团队 原创撰写,内容基于实际项目案例与技术文档,力求为读者提供准确、实用的参考信息。
把这篇资料用于真实项目?

进入工具中心进行报文解析、CRC 校验和设备调试,或提交需求获取选型与接入建议。

工程师会员

把这篇文章变成可执行的调试资料

开通后可使用高级报文解析、资料包下载、代码示例、工程案例和优先技术支持,适合真实项目交付。

高级工具不限次
资料包与代码包
完整工程案例库
优先技术支持入口

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注