CRC 校验原理与计算
CRC(Cyclic Redundancy Check,循环冗余校验)是错误检测技术。
CRC校验的基本原理
CRC基于多项式除法的数学原理。它将数据视为一个大的二进制数,对应一个多项式,然后用一个预定的生成多项式进行模2除法运算,得到的余数就是CRC校验码。
核心概念
模2运算:在CRC中使用的是模2运算,其特点是:
- 加法和减法都等同于异或(XOR)运算
- 不考虑进位和借位
- 1 + 1 = 0, 1 + 0 = 1, 0 + 0 = 0
生成多项式:这是CRC算法的关键,不同的CRC标准使用不同的生成多项式。例如:
- CRC-16: x^16 + x^15 + x^2 + 1 (二进制:11000000000000101)
- CRC-32: x^32 + x^26 + x^23 + … + x^2 + x + 1
CRC计算过程
发送端计算步骤
-
准备数据:假设原始数据为M(x),生成多项式为G(x),G(x)的最高次幂为r
-
左移数据:将M(x)左移r位,相当于M(x) × 2^r,在数据末尾添加r个0
-
模2除法:用左移后的数据除以生成多项式G(x),得到余数R(x)
-
附加校验码:将余数R(x)附加到原始数据M(x)后面,形成要传输的数据
接收端验证步骤
-
接收数据:接收包含原始数据和CRC校验码的完整数据
-
执行除法:用相同的生成多项式G(x)对接收到的数据进行模2除法
-
检查余数:如果余数为0,说明数据传输正确;否则说明出现错误
具体计算示例
让我通过一个简单的例子来演示CRC计算:
假设:
- 原始数据:1101(对应多项式 x^3 + x^2 + 1)
- 生成多项式:1011(对应 x^3 + x + 1),r = 3
计算过程:
-
数据左移3位:1101000
-
进行模2除法:
1110 (商,我们不需要)
-------
1011 | 1101000
1011
----
1100
1011
----
1110
1011
----
1010
1011
----
001 (余数)
-
CRC校验码为:001
-
最终传输的数据:1101001
常见CRC标准
不同的应用场景使用不同的CRC标准:
- CRC-8:用于短消息
- CRC-16:用于Modbus、USB等
- CRC-32:用于以太网、ZIP文件等
- CRC-64:用于更高可靠性要求的场合
Modbus CRC-16 特点
Modbus使用的是CRC-16/MODBUS标准,具有以下特征:
- 生成多项式:0x8005(二进制:1000000000000101,对应 x^16 + x^15 + x^2 + 1)
- 初始值:0xFFFF
- 结果异或值:0x0000
- 输入/输出反转:是
- 校验值用于验证:"123456789"的CRC-16/MODBUS结果是0x4B37
实际Modbus消息示例
假设我们有一个Modbus RTU读取保持寄存器的请求:
从机地址:01
功能码: 03(读保持寄存器)
起始地址:0000
寄存器数:0002
完整消息(不含CRC):01 03 00 00 00 02
Modbus CRC-16 计算步骤
方法1:逐字节计算法(易理解)
def calculate_modbus_crc(data):
# 初始化CRC为0xFFFF
crc = 0xFFFF
# 对每个字节进行处理
for byte in data:
# CRC与当前字节异或
crc ^= byte
# 对该字节的8个位进行处理
for _ in range(8):
# 检查最低位
if crc & 0x0001:
# 如果最低位是1,右移并与多项式异或
crc = (crc >> 1) ^ 0xA001 # 0xA001是0x8005的反转
else:
# 如果最低位是0,只右移
crc = crc >> 1
return crc
# 计算示例消息的CRC
data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x02]
crc_result = calculate_modbus_crc(data)
print(f"CRC-16: {crc_result:04X}") # 结果:C40B
详细计算过程演示
让我手工演示前两个字节的计算:
处理第1个字节(0x01):
初始CRC = 0xFFFF (1111111111111111)
字节值 = 0x01 (0000000000000001)
CRC ^= 0x01 → 0xFFFE (1111111111111110)
8次位处理:
第1次:最低位=0,右移 → 0x7FFF
第2次:最低位=1,右移并异或0xA001 → 0xBFFF ^ 0xA001 = 0x1FFE
第3次:最低位=0,右移 → 0x0FFF
第4次:最低位=1,右移并异或0xA001 → 0x87FF ^ 0xA001 = 0x27FE
第5次:最低位=0,右移 → 0x13FF
第6次:最低位=1,右移并异或0xA001 → 0x89FF ^ 0xA001 = 0x29FE
第7次:最低位=0,右移 → 0x14FF
第8次:最低位=1,右移并异或0xA001 → 0x8A7F ^ 0xA001 = 0x2A7E
处理完0x01后,CRC = 0x2A7E
处理第2个字节(0x03):
当前CRC = 0x2A7E
字节值 = 0x03
CRC ^= 0x03 → 0x2A7D
继续8次位处理...
最终得到新的CRC值
方法2:查表法(实际应用)
实际应用中,为了提高效率,通常使用预计算的CRC表:
# CRC-16/MODBUS查找表(部分)
CRC_TABLE = [
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
# ... 完整表有256个值
]
def calculate_modbus_crc_table(data):
crc = 0xFFFF
for byte in data:
# 取CRC低8位与数据字节异或,作为表索引
index = (crc ^ byte) & 0xFF
# CRC右移8位,与表值异或
crc = (crc >> 8) ^ CRC_TABLE[index]
return crc
完整的Modbus RTU帧
对于我们的示例消息:
原始数据:01 03 00 00 00 02
计算得CRC:0x0BC4(低字节在前)
完整帧:01 03 00 00 00 02 C4 0B
注意:Modbus发送CRC时,低字节在前,高字节在后。
接收端验证
接收端收到完整帧后有两种验证方法:
方法1:重新计算CRC
received_frame = [0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B]
# 对除了最后两个CRC字节外的数据计算CRC
data = received_frame[:-2]
calculated_crc = calculate_modbus_crc(data)
received_crc = (received_frame[-1] << 8) | received_frame[-2]
if calculated_crc == received_crc:
print("数据正确")
else:
print("数据错误")
方法2:对整个帧计算CRC
# 对包含CRC的整个帧计算,正确时结果应为0
result = calculate_modbus_crc(received_frame)
if result == 0:
print("数据正确")
实际应用建议
- 使用查表法:预计算的CRC表大大提高计算速度
- 注意字节序:Modbus CRC是小端序(低字节在前)
- 硬件支持:许多微控制器有硬件CRC模块,可以进一步加速
- 错误处理:CRC错误时,通常请求重发而不是尝试纠错
为什么0xA001?
你可能注意到代码中用的是0xA001而不是0x8005:
- 0x8005 = 1000 0000 0000 0101(正常表示)
- 0xA001 = 1010 0000 0000 0001(位反转后)
Modbus使用位反转算法(LSB first),所以实际运算时用反转后的多项式值。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)