CRC 用来检出传输或存储错误。它把比特串解释为二元多项式,利用模 2 除法生成冗余位;校验通过表示“没有检出错误”,不保证数据绝对正确,也不提供身份认证。
本文先保留小多项式的手算方法,再给出可直接运行的 CRC-16/MODBUS 位运算与完整查表实现。RTU 帧的其余语义仍需另行检查。
1. 模 2 除法与手算例子
跳转到“1. 模 2 除法与手算例子”在 上,加减法都等价于 XOR,不发生进位或借位。对次数为 的生成多项式 ,最简单的零初值模型计算:
因此 能被 整除。例如原笔记的数据 1101,生成多项式 1011,补三个零后为 1101000:
1101000 XOR 1011000 = 01100000110000 XOR 0101100 = 00111000011100 XOR 0010110 = 00010100001010 XOR 0001011 = 0000001商是 1111,余数是 001,发送码字为 1101001。原稿写成 1110 的商需要纠正。这个例子解释代数原理;实际 CRC 还规定初始值、输入/输出反射、最终异或和字节处理方向,不能直接把所有 CRC 都理解为这一个补零算法。
CRC-8/16/32/64 只说明宽度,不是完整算法名。同一宽度存在多个参数组合;宽度更大也不能脱离消息长度与错误模型而直接等同于“可靠性更高”。选型必须写完整参数或协议名称。
2. CRC-16/MODBUS 参数与三个“顺序”
跳转到“2. CRC-16/MODBUS 参数与三个“顺序””| 项目 | 值或解释 |
|---|---|
| 宽度 | 16 bit |
| 生成多项式 | ;省略最高项的正常表示为 0x8005 |
| 初始值 / 最终异或 | 0xFFFF / 0x0000 |
| 反射形式的右移反馈值 | 0xA001;它是16位表示 0x8005 的位反转 |
| 常用核对值 | ASCII 123456789 → 数值 0x4B37 |
| RTU CRC 字段 | 低字节先发,高字节后发 |
右移算法已经体现反射规则,不要再额外翻转最终16位结果。CRC 字段的低字节在前,与寄存器数据的高字节在前是两回事;UART 在线路上发送一个字节时的位序又是第三层。
请求 01 03 00 00 00 02 的 CRC 数值为 0x0BC4,线上附加字节是 C4 0B。原稿代码注释的 C40B 混淆了数值和打印出的字节序列。串行规范附录 B
3. 完整 Python 实现
跳转到“3. 完整 Python 实现”下面仅接收字节数据,查找表由程序生成全部256项,不留下不能运行的省略号。crc_valid() 检查总长度与 CRC;它不是功能码、地址和响应匹配检查器。
def as_bytes(data): if not isinstance(data, (bytes, bytearray, memoryview)): raise TypeError("expected bytes, bytearray or memoryview") return bytes(data)
def crc16_modbus(data): crc = 0xFFFF for value in as_bytes(data): crc ^= value for _ in range(8): crc = (crc >> 1) ^ (0xA001 if crc & 1 else 0) return crc
def make_crc_table(): table = [] for value in range(256): crc = value for _ in range(8): crc = (crc >> 1) ^ (0xA001 if crc & 1 else 0) table.append(crc) return tuple(table)
CRC_TABLE = make_crc_table()
def crc16_modbus_table(data): crc = 0xFFFF for value in as_bytes(data): crc = (crc >> 8) ^ CRC_TABLE[(crc ^ value) & 0xFF] return crc
def add_rtu_crc(address_and_pdu): payload = as_bytes(address_and_pdu) if not 2 <= len(payload) <= 254: raise ValueError("address plus PDU length must be 2..254") return payload + crc16_modbus(payload).to_bytes(2, "little")
def crc_valid(frame): frame = as_bytes(frame) if not 4 <= len(frame) <= 256: return False expected = int.from_bytes(frame[-2:], "little") return crc16_modbus(frame[:-2]) == expected
if __name__ == "__main__": request = bytes.fromhex("01 03 00 00 00 02") frame = add_rtu_crc(request) assert crc16_modbus(b"123456789") == 0x4B37 assert crc16_modbus(request) == 0x0BC4 assert frame.hex(" ") == "01 03 00 00 00 02 c4 0b" assert crc16_modbus_table(request) == crc16_modbus(request) assert crc_valid(frame) assert crc16_modbus(frame) == 0 print(frame.hex(" "))对这个参数组合,完整正确帧继续计算 CRC 的余数为零,因此可以用全帧余数法交叉核对。这个“零余数”不能不加区分地套到所有 CRC 变体。
第一个字节的逐步核对
跳转到“第一个字节的逐步核对”初始 FFFF 与 01 异或后为 FFFE,八次右移/条件异或应为:
7FFF → 9FFE → 4FFF → 87FE → 43FF → 81FE → 40FF → 807E随后处理第二字节 03,先得到 807D,再执行相同的八轮。原稿第二轮起的中间值错误,不能作为手算参考。
4. 从 CRC 正确到事务正确
跳转到“4. 从 CRC 正确到事务正确”通过 CRC 后仍要检查站号、功能码、异常码、长度、数量、字节计数,以及与当前未完成请求是否一致。写响应还要核对回显的地址、数量或值。单纯 ack = 1 不足以证明业务动作已经执行。
CRC 错误帧通常丢弃;RTU 没有“CRC 错误重传请求”这一通用应答。由主站超时策略决定是否重试,且写命令是否可安全重试取决于设备语义。CRC 既不纠错,也不能阻止有意构造的修改。
位运算适合体积小、吞吐量低的实现;查表用存储空间换计算量,实际收益应在目标 MCU 上测量。使用硬件 CRC 前必须核对宽度、多项式、初值、反射、最终异或及输入排列;例如 STM32F103 的固定32位 CRC 单元不能直接当成可任意配置的 CRC-16/MODBUS 单元。ST RM0008 CRC章节
实现至少用已知向量、全部单字节输入、空数据 CRC、帧长边界和逐位翻转测试核对;正确向量全部通过仍不代表整个串口协议或现场布线已经通过验收。
相关笔记:地址映射、FreeModbus 从站移植。原始来源:CRC 原文。