跳转到内容
新建笔记

CRC-16/MODBUS:原理、字节顺序与完整实现

CRC 用来检出传输或存储错误。它把比特串解释为二元多项式,利用模 2 除法生成冗余位;校验通过表示“没有检出错误”,不保证数据绝对正确,也不提供身份认证。

本文先保留小多项式的手算方法,再给出可直接运行的 CRC-16/MODBUS 位运算与完整查表实现。RTU 帧的其余语义仍需另行检查。

1. 模 2 除法与手算例子

跳转到“1. 模 2 除法与手算例子”

在 GF(2)\mathrm{GF}(2) 上,加减法都等价于 XOR,不发生进位或借位。对次数为 rr 的生成多项式 G(x)G(x),最简单的零初值模型计算:

R(x)=xrM(x) mod G(x),T(x)=xrM(x)+R(x)R(x)=x^rM(x)\bmod G(x),\qquad T(x)=x^rM(x)+R(x)

因此 T(x)T(x) 能被 G(x)G(x) 整除。例如原笔记的数据 1101,生成多项式 1011,补三个零后为 1101000:

1101000 XOR 1011000 = 0110000
0110000 XOR 0101100 = 0011100
0011100 XOR 0010110 = 0001010
0001010 XOR 0001011 = 0000001

商是 1111,余数是 001,发送码字为 1101001。原稿写成 1110 的商需要纠正。这个例子解释代数原理;实际 CRC 还规定初始值、输入/输出反射、最终异或和字节处理方向,不能直接把所有 CRC 都理解为这一个补零算法。

CRC-8/16/32/64 只说明宽度,不是完整算法名。同一宽度存在多个参数组合;宽度更大也不能脱离消息长度与错误模型而直接等同于“可靠性更高”。选型必须写完整参数或协议名称。

2. CRC-16/MODBUS 参数与三个“顺序”

跳转到“2. CRC-16/MODBUS 参数与三个“顺序””
项目值或解释
宽度16 bit
生成多项式x16+x15+x2+1x^{16}+x^{15}+x^2+1;省略最高项的正常表示为 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

下面仅接收字节数据,查找表由程序生成全部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 原文。