- 1. 一、错误检测的三层架构
- 2. 二、字符层:奇偶校验
- 3. 2.1 原理
- 4. 2.2 计算示例
- 5. 2.3 奇偶校验的局限
- 6. 三、帧层(一):LRC 校验 —— Modbus ASCII 模式
- 7. 3.1 LRC 是什么
- 8. 3.2 Modbus ASCII 帧结构
- 9. 3.3 LRC 算法
- 10. 3.4 LRC 的 C 语言实现
- 11. 3.5 LRC 的局限
- 12. 四、帧层(二):CRC-16 校验 —— Modbus RTU 模式
- 13. 4.1 CRC-16 Modbus 的数学定义
- 14. 4.2 CRC-16 Modbus 算法流程(逐位计算法)
- 15. 4.3 CRC-16 Modbus C 语言实现
- 16. 4.4 查表法 —— 嵌入式设备上的标准实现
- 17. 4.5 验证你的 CRC 实现
- 18. 4.6 CRC 的检错能力
- 19. 五、应用层:异常响应码
- 20. 5.1 异常响应的帧格式
- 21. 5.2 标准异常码速查表
- 22. 5.3 异常码的实际调试用法
- 23. 六、超时机制:主站的最后一道防线
- 24. 七、完整排障流程
来源:Modbus中文网(modbus.cn) —— 国内领先的Modbus通信协议技术社区
本文:Modbus 通讯错误的检测与诊断:从奇偶校验到异常码的完整体系 · 作者:modbus技术团队 · 发布于 2026-07-01
摘要:Modbus 协议的错误检测分为三个层次——字符级的奇偶校验、帧级的 LRC/CRC 校验、以及应用层的异常响应码和超时机制。本文逐一拆解每种检测方式的工作原理、算法实现(含可运行的 C/Python 代码)和实际调试用途,并给出完整的异常码速查表和排障流程。关键词:Modbus CRC、LRC 校验、奇偶校验、Modbus 异常码、Modbus 错误检测、CRC16 Modbus。
在 RS-485 总线上传输一个 Modbus 帧,就像在嘈杂的车间里喊话——喊出去的是 01 03 00 00 00 01,对端收到的可能变成了 01 03 00 00 00 03。一个比特翻转,温度读数就从 25°C 变成 26°C,或者某台阀门「为什么不动」的原因就埋在这里。
Modbus 从 1979 年诞生就带着一套分层错误检测机制:每个字符用奇偶校验挡住比特错误,整帧数据用 CRC(RTU 模式)或 LRC(ASCII 模式)兜底,应用层再通过异常响应码告诉主站「你的请求有问题」。这三层机制配合超时重发策略,构成了 Modbus 在工业现场存活近五十年不倒的可靠性基础。
坦率说,Modbus 的错误检测不是密码学级别的——CRC16 不能防篡改,LRC 对连续偶数位错误可能漏检。但在一个 9600bps、几十米 RS-485 总线的物理环境下,这些机制已经够用了。它们的价值不在于理论完美,而在于实现简单、计算开销低——一个 8 位单片机用不到 100 字节的代码就能完成 CRC16 计算。
一、错误检测的三层架构
Modbus 串行通信的错误检测分成三个层级,从上到下覆盖整个通信栈:
应用层:异常响应码 + 超时重发
↓
帧层 :CRC-16(RTU)或 LRC(ASCII)
↓
字符层:奇偶校验(Even/Odd/None)
字符层保护每个字节的传输正确性——一个 UART 帧里的 8 个数据位加上一个校验位,硬件自动完成校验。
帧层保护整条 Modbus 消息的完整性——从站地址到最后一个数据字节,全部参与计算。如果帧校验不通过,从设备静默丢弃这条消息,不回复任何内容。
应用层处理「物理层没问题,但逻辑上有问题」的情况——比如你请求的功能码设备不支持、寄存器地址不存在、数据值超出范围。这些错误通过异常响应码告知主站。
三层各司其职,但调试的时候工程师更容易看到应用层的异常码,而忽略了物理层的奇偶校验错误——因为奇偶校验错误在从设备硬件层面就被丢弃了,你根本看不到。
二、字符层:奇偶校验
2.1 原理
奇偶校验是在每个 UART 字符帧中附加一个校验位,使得整个帧中「1」的总数为奇数(奇校验,Odd)或偶数(偶校验,Even)。
Modbus 的字符帧定义因模式而异:
| 模式 | 起始位 | 数据位 | 校验位 | 停止位 | 总位数 |
|---|---|---|---|---|---|
| RTU(有校验) | 1 | 8 | 1 | 1 | 11 |
| RTU(无校验) | 1 | 8 | 0 | 2 | 11 |
| ASCII | 1 | 7 | 1 | 1 | 10 |
注意 RTU 无校验时用 2 个停止位来凑总位数——这是硬性要求,很多人在配置串口参数时选了 No Parity 但忘了把停止位从 1 改成 2,结果通信不稳定但又不至于完全不通,非常难查。
2.2 计算示例
取一个字节:11000101。其中 1 的个数是 4(偶数)。
- 偶校验 → 校验位 = 0,保持 1 的总数为偶数(4 个)
- 奇校验 → 校验位 = 1,使 1 的总数为奇数(5 个)
硬件 UART 在发送时自动计算并填入校验位,接收时自动校验。如果接收端校验不通过,UART 硬件会标记一个奇偶校验错误(Parity Error),但不会自动丢弃数据——要不要丢弃由软件决定。
2.3 奇偶校验的局限
奇偶校验只能检测奇数个比特错误。如果传输中恰好翻转了 2 个比特,奇偶性不变,校验通过,但数据已经错了。在 RS-485 上,电磁干扰导致单比特翻转的概率远高于多比特同时翻转,所以奇偶校验在实际中还是有用的——但它绝不是万能的。
调试建议:如果你怀疑线路质量有问题,用逻辑分析仪抓 UART 帧看每个字符的奇偶校验错误标记。串口助手(如 SSCOM)可以显示奇偶错误次数,这个计数器如果一直在涨,说明你的物理线路有电磁干扰问题——可能是电缆不屏蔽、和变频器走到一个桥架里了、或者终端电阻没接导致信号反射。
三、帧层(一):LRC 校验 —— Modbus ASCII 模式
3.1 LRC 是什么
纵向冗余校验(Longitudinal Redundancy Check,LRC)是 Modbus ASCII 模式使用的帧校验算法。它是一个 8 位(1 字节)的校验码,位于帧的末尾,校验范围是从站地址到数据区的所有字节,不包括帧头(冒号 :)和帧尾(回车换行符 CR/LF)。
3.2 Modbus ASCII 帧结构
: 01 03 21 02 00 02 D7 CR LF
│ └─────────┬────────────┘ │
帧头 LRC 校验范围 LRC码
一个完整的 Modbus ASCII 请求帧:
:0103020002F8rn
3.3 LRC 算法
算法极其简单——三步:
- 将地址码到数据区的所有字节相加求和
- 取和的低 8 位(模 256)
- 取其补码(256 减去这个值,或者按位取反后加 1)
示例:报文 : 01 03 21 02 00 02
求和:0x01 + 0x03 + 0x21 + 0x02 + 0x00 + 0x02 = 0x29
低 8 位:0x29
补码:256 - 0x29 = 0xD7
LRC 校验码 = D7
完整帧为:: 01 03 21 02 00 02 D7 rn
3.4 LRC 的 C 语言实现
/**
* 计算 Modbus ASCII LRC 校验码
* buf: 需要校验的数据(从地址码到数据区的所有字节)
* len: 字节数
* 返回值: 1 字节 LRC 校验码
*/
unsigned char LRC(unsigned char *buf, unsigned short len)
{
unsigned char lrc = 0;
while (len--) {
lrc += *buf++;
}
// 取补码:256 - lrc,等效于 (-lrc)
return (unsigned char)(-lrc);
}
用 Python 验证:
def calc_lrc(data: bytes) -> int:
"""计算 Modbus ASCII LRC 校验码"""
return (256 - (sum(data) & 0xFF)) & 0xFF
# 测试
msg = bytes([0x01, 0x03, 0x21, 0x02, 0x00, 0x02])
lrc = calc_lrc(msg)
print(f"LRC: 0x{lrc:02X}") # 输出: LRC: 0xD7
3.5 LRC 的局限
LRC 只能检测到一定比例的错误。如果两个字节的同一个比特位同时翻转(比如字节 A 的第 3 位从 0 变 1,字节 B 的第 3 位从 1 变 0),求和结果不变,LRC 校验通过。这也是为什么 Modbus RTU 使用更强的 CRC-16 而不是 LRC——RTU 模式用于二进制数据传输,对可靠性要求更高,而 ASCII 模式主要用于调试和兼容老设备。
四、帧层(二):CRC-16 校验 —— Modbus RTU 模式
4.1 CRC-16 Modbus 的数学定义
Modbus RTU 使用 CRC-16 算法,参数如下:
| 参数 | 值 |
|---|---|
| 宽度 | 16 位 |
| 生成多项式 | 0x8005(x¹⁶ + x¹⁵ + x² + 1) |
| 实际运算多项式 | 0xA001(0x8005 位反转形式) |
| 初始值 | 0xFFFF |
| 输入字节反射 | 否 |
| 输出 CRC 反射 | 是(最终结果高低字节交换) |
| 输出异或值 | 0x0000 |
这里的「位反转」是 CRC 参数体系中很容易搞混的概念。Modbus 的计算实际上是按位处理的、从 LSB 方向移位,使用反转多项式 0xA001,计算结果自然就是反转的——所以最终不需要再做一次全局反转,只需要高低字节交换。发送时低字节在前,高字节在后。
4.2 CRC-16 Modbus 算法流程(逐位计算法)
逐位计算虽然慢,但能让人看清每一步发生了什么:
- 预置 16 位 CRC 寄存器为
0xFFFF - 将报文的第一个字节与 CRC 寄存器的低 8 位异或,结果存回 CRC 寄存器
- CRC 寄存器右移 1 位,最高位补 0,检查移出的最低位
- 移出位 = 1 → CRC 寄存器与 0xA001 异或;移出位 = 0 → 不做操作
- 重复步骤 3~4,共 8 次(处理完一个字节的 8 个位)
- 重复步骤 2~5,处理报文中下一个字节
- 所有字节处理完成后,CRC 寄存器的低字节在前、高字节在后,即为 CRC-16 校验码
4.3 CRC-16 Modbus C 语言实现
/**
* 计算 Modbus RTU CRC-16 校验码(逐位计算法)
* buf: 需要校验的数据(从地址码到数据区的所有字节)
* len: 字节数
* 返回值: 16 位 CRC 值(低字节在前)
*/
unsigned short CRC16_Modbus(unsigned char *buf, unsigned short len)
{
unsigned short crc = 0xFFFF;
unsigned short i, j;
for (i = 0; i < len; i++) {
crc ^= buf[i]; // 步骤 2
for (j = 0; j < 8; j++) { // 步骤 3~5 循环 8 次
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001; // 移出位为 1
} else {
crc >>= 1; // 移出位为 0
}
}
}
// 注意:Modbus CRC 发送时低字节在前
return crc;
}
4.4 查表法 —— 嵌入式设备上的标准实现
逐位计算每个字节要做 8 次循环,对嵌入式 MCU 开销太大。工程上用的是查表法,预先计算 256 个字节的 CRC 值,每字节只做一次查表 + 一次异或 + 一次移位。
高位表查表法(最常用):
/* CRC 高位表 */
static const unsigned char auchCRCHi[] = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
/* ... 完整 256 项,此处省略 */
};
/* CRC 低位表 */
static const unsigned char auchCRCLo[] = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
/* ... 完整 256 项,此处省略 */
};
unsigned short CRC16_Modbus_Table(unsigned char *buf, unsigned short len)
{
unsigned char crcHi = 0xFF;
unsigned char crcLo = 0xFF;
unsigned short index;
while (len--) {
index = crcLo ^ *buf++;
crcLo = crcHi ^ auchCRCHi[index];
crcHi = auchCRCLo[index];
}
return (crcHi << 8) | crcLo;
}
完整的高低位表共 512 字节,可直接从 Modbus 协议规范附录中抄过来,Modbus.org V1.1b3 规范的第 40 页给出了完整的示例表。
4.5 验证你的 CRC 实现
用已知报文测试:
报文: 01 03 00 00 00 01
正确的 CRC: 84 0A(低字节在前)
验证步骤:
crc = 0xFFFF
crc ^= 0x01 → 0xFFFE
bit0=0, crc>>1 → 0x7FFF
bit1=1, (0x3FFF) ^ 0xA001 → 0x9FFE
...
最终 crc = 0x0A84
发送时低字节在前: 84 0A
整个报文发送序列:01 03 00 00 00 01 84 0A
你也可以用 Modbus Poll 或 Modbus 小程序直接算 CRC,和你的代码实现交叉验证。
4.6 CRC 的检错能力
CRC-16 可以检测到以下错误类型:
- 所有单比特错误
- 所有双比特错误(在报文不超过 32767 位的条件下)
- 所有奇数个比特错误
- 所有突发错误,长度 ≤ 16 位
- 99.998% 的更长突发错误
简单说,在 Modbus 帧通常不超过 256 字节的前提下,CRC-16 可以捕捉几乎所有物理层传输错误。用 CRC 校验通过的数据帧,基本可以信任。
五、应用层:异常响应码
物理层和帧校验都通过了,从设备收到了一个完整有效的 Modbus 帧——但逻辑上是错的。这时候从设备不是沉默,而是返回一个异常响应,告诉主站「你让我做的事我做不了」。
5.1 异常响应的帧格式
正常响应和异常响应的区别只有一个:功能码的最高位。
- 正常响应:功能码 = 原始功能码(如 0x03 读保持寄存器)
- 异常响应:功能码 = 原始功能码 + 0x80(如 0x83),然后跟一个字节的异常码
示例:主站请求读寄存器 01 03 00 00 00 01 84 0A
正常响应:01 03 02 00 7B F9 8D(读回 2 字节数据 00 7B)
异常响应(假设寄存器不存在):01 83 02 C0 F1
83= 0x03(原功能码)+ 0x8002= 异常码,表示「非法数据地址」
5.2 标准异常码速查表
| 异常码 | 名称 | 含义 | 常见原因 |
|---|---|---|---|
| 0x01 | Illegal Function | 非法的功能码 | 设备不支持该功能码(如给只读设备发写指令) |
| 0x02 | Illegal Data Address | 非法的数据地址 | 寄存器地址超出范围,或起始地址+数量越界 |
| 0x03 | Illegal Data Value | 非法的数据值 | 写入的值超出了寄存器的允许范围 |
| 0x04 | Slave Device Failure | 从设备故障 | 从设备在执行操作时发生了不可恢复的错误 |
| 0x05 | Acknowledge | 确认(正在处理中) | 请求已接受但需要较长时间处理,主站应等待 |
| 0x06 | Slave Device Busy | 从设备忙 | 设备正在处理另一个命令,暂时无法响应 |
| 0x07 | Negative Acknowledge | 否定确认 | 从设备不能执行该功能(通常是非特定错误) |
| 0x08 | Memory Parity Error | 内存奇偶校验错误 | 扩展文件区一致性校验失败 |
0x01 到 0x04 是最容易遇到的,0x05 到 0x08 在常规 Modbus 设备上比较少见——很多设备厂商只实现了前 4 个异常码。
5.3 异常码的实际调试用法
「为什么设备不回数据?」排除物理层和 CRC 问题后,看异常码:
收到 0x01(非法功能):你发了一个设备不支持的功能码。最常见的场景是——你用 0x03(读保持寄存器)去读了一个只能通过 0x04(读输入寄存器)访问的模拟量输入。去翻设备手册的寄存器映射表,确认该地址对应的访问功能码。
收到 0x02(非法地址):你的起始地址加上请求数量超出了设备支持的地址范围。比如设备只有 10 个保持寄存器(地址 0-9),你请求了从地址 0 开始的 12 个寄存器——跨过了边界。解决方法:减小每次请求的寄存器数量,或者在读之前先用 0x11(报告从站 ID)确认设备类型。
收到 0x03(非法数据值):你要写入的值对设备没有意义。比如设备支持 0-100 的数据范围,你写入了 200。或者写入时的数据字节数与功能码要求的长度不匹配。
完全没有响应:不是异常码,而是什么都没有。可能的原因:CRC 错误(从设备静默丢弃)、帧间隔(3.5 个字符时间)没有正确实现、设备地址不匹配。这时候需要用逻辑分析仪抓总线,确认从设备收到了什么、UART 硬件上有没有奇偶校验错误标记。
六、超时机制:主站的最后一道防线
Modbus 规范要求主站配置一个超时时间。在以下情况下,超时会触发:
- 主站发出请求后,从站在规定时间内没有任何响应
- 从站检测到帧错误(CRC/LRC 失败),静默丢弃帧,主站收不到回复
- 从站地址不存在——报文发到了空地址上
超时时间的设置有一个约束:必须大于从设备最长可能的响应时间。计算方式为:
超时时间 > 传输延迟 × 2 + 从站处理时间
在 9600bps 下,一个典型的 Modbus RTU 请求(读 1 个寄存器)约 8 字节 = 64 位,传输时间 = 64 / 9600 ≈ 6.7ms。响应约 7 字节 = 56 位,传输时间 ≈ 5.8ms。加上从站处理时间(假设最慢 50ms),双向总时间约 6.7 + 50 + 5.8 ≈ 62.5ms。留一倍余量,超时设 100-200ms 比较合理。
如果总线上有多个从站,要把轮询最慢的设备(比如有些老 PLC 处理一个请求要 100ms)考虑进去。Modbus 规范建议的默认超时是 1 秒——这个值对于现代设备偏大,但作为初始值启动调试是稳妥的。
七、完整排障流程
当你面对「Modbus 通信失败」时,按这个顺序逐层排查:
| 层 | 检查项 | 工具 | 判断方法 |
|---|---|---|---|
| 物理层 | RS-485 接线(A/B、共地、终端电阻) | 万用表 | 导通、总线空闲电平 ≥ 200mV |
| 字符层 | 串口参数(波特率、数据位、校验位、停止位) | 逻辑分析仪 | 实测位宽 = 1/baud |
| 字符层 | 奇偶校验错误 | 逻辑分析仪 / 串口助手 | 奇偶错误计数 = 0 |
| 帧层 | CRC Check | Modbus 小程序 / 自写代码 | 接收 CRC = 计算 CRC |
| 帧层 | 帧间隔(≥ 3.5 字符时间) | 逻辑分析仪 | 帧间空闲 ≥ 3.6ms @ 9600bps |
| 应用层 | 从站地址 | 设备手册 / 拨码开关 | 请求地址 = 从站实际地址 |
| 应用层 | 功能码 + 地址范围 | 设备寄存器映射表 | 功能码和寄存器地址在设备支持范围内 |
| 应用层 | 异常响应码 | 串口助手 / 抓包 | 功能码最高位为 1 时检查异常码 |
| 应用层 | 超时 | 抓包看时间戳 | 响应时间 < 超时配置 |
特别注意:用 「01 03 00 00 00 01」 这类标准 Modbus 请求去交互时,不要直接用 ASCII 字符串发送——必须发送二进制帧。初学者在串口助手里用 ASCII 模式发了 0x31 0x30 0x33 0x30 0x30 0x30 0x30 0x30 0x30 0x31(ASCII 字符串的 “010300000001”),而不是 01 03 00 00 00 01 这 6 个字节。这个错误几乎每个学 Modbus 的人都犯过。
CRC 校验码的正确计算是 Modbus 通信可靠的基石。但工程现场的问题九成不在 CRC 本身——在接线、在参数、在地址范围越界、在功能码选错。CRC 更像一个安全网,在你说「数据应该是对的」的时候给你信心。当数据确实不对的时候,先查前面几层。
把这篇文章里的代码放到你的工程里交叉验证一下,别指望网上随便抄一段 CRC 代码就能直接用——我见过太多因为字节序搞反导致 CRC 永远校验不通过的案例。有兴趣可以翻一下 Modbus.org V1.1b3 规范的附录部分,那里有 CRC 查找表的完整数据和高低位两种实现方式的官方示例。
有问题再聊。
1