长线 RS-485 上电机启停、变频器噪声容易造成 bit 翻转。Modbus RTU 强制使用 CRC16:对「地址 + 功能码 + 数据」整段计算 16 位冗余,附在帧尾;接收方重算不一致则 静默丢弃(多数从站不回异常)。现场「HEX 发了没应答」里,有相当一部分是 CRC 字节序或计算范围错了,不是 485 线断了。
必记三条
1. 多项式实现常用反射形式 0xA001(对应 0x8005)。
2. 初值 `0xFFFF`,对「除 CRC 外整帧」逐字节更新。
3. 结果 低字节先上线(little-endian):例如算得 0x0BC4,线上顺序是 C4 0B。
校验范围 不含 CRC 自身,也不含 Modbus ASCII 的 : 与 \r\n(那是另一套 LRC)。
标准测试向量(务必写进单测)
| 数据(HEX) | CRC 低→高 | 完整发送帧 |
|---|---|---|
| 01 03 00 00 00 02 | C4 0B | 01 03 00 00 00 02 C4 0B |
| 01 06 00 01 00 03 | 自算核对 | 写单寄存器示例 |
你的 MCU / Python / JS 实现必须先过第一行向量,再谈查表优化。
移位法 vs 查表法 | 方法 | 优点 | 缺点 | 适用 | |---|---|---|---| | 移位法 | 代码短、无大表 | 高频帧时 CPU 忙 | 51 / 资源极紧 | | 查表法 | 每字节 O(1) | 约 512 字节表 | 从站 / 上位机主流 | 上位机与网页工具内部多为查表法;两端用 同一测试向量 对齐,比争论「谁的算法正宗」更有效。
串口现场对照(本站)
1. 打开 https://www.serialpro.top/ ,HEX 模式,波特率与从站一致(常见 9600 8N1)。
2. 发送完整帧:01 03 00 00 00 02 C4 0B(空格仅编辑用,勿把空格字节发出)。
3. 有正确应答 → CRC 与电气基本过关,再对寄存器表。
4. 无应答:
- 先换回环/短线,排除 485 A/B、终端、供电;
- 再自算 CRC:是否误成高字节在前 0B C4;
- 是否把 CRC 算进了校验范围;
- 是否多发了 \r\n(文本模式残留)。
5. 应答功能码变 83 等异常码:CRC 往往已对,问题在地址/寄存器映射。
实现检查清单
- [ ] 单测向量 01 03 00 00 00 02 → C4 0B
- [ ] 线上顺序 CRC_L 然后 CRC_H
- [ ] 异常应答帧同样带 CRC
- [ ] 写多寄存器时「字节数」字段纳入 CRC 计算
- [ ] HEX 工具忽略空白;不要把空格 0x20 算进帧
和乱码、超时怎么分工 | 现象 | 先查 | |---|---| | HEX 噪声无结构 | 波特率 / 共地 / 485 电平 | | 帧结构像但从不回 | 地址、DE 方向、CRC | | 偶发 CRC 错 | 终端 120Ω、EMI、偏置 | | 固定错一字节 | 端序或漏算字段 | 把 CRC 在上下位机用同一套测例钉死,再谈复杂寄存器表——这是 Modbus 串口联调里 性价比最高 的一步。本站适合现场快速发标准帧;算法细节可在固件里对照本文向量。完整帧格式见《Modbus RTU 帧格式详解》。
// 快速高效的 Modbus CRC16 查表法 JavaScript 引擎
const CRC16_TABLE_HI = [
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
// ... 完整 256 字节的 CRC 计算预存表(在 utils.ts 中已被完整实现)
];
function calculateCRC16(buffer) {
let crcHi = 0xFF;
let crcLo = 0xFF;
for (let i = 0; i < buffer.length; i++) {
const idx = crcLo ^ buffer[i];
crcLo = crcHi ^ CRC16_TABLE_HI[idx]; // 快速查表
crcHi = CRC16_TABLE_LO[idx];
}
return (crcHi << 8) | crcLo; // 组装 16 位循环校验字
}