CRC 校验的数学本质
CRC(循环冗余校验)本质上是将数据视为二进制多项式,用约定的生成多项式做模2除法,余数即为校验值。例如 Modbus CRC16 使用多项式 x^16 + x^15 + x^2 + 1,即 0x8005(高位省略),实际计算时通过左移或右移方式处理。
Modbus CRC16 的算法参数
- 多项式:0x8005(反转后为0xA001)
- 初始值:0xFFFF
- 输入/输出反转:均反转(即处理时低位在前)
- 结果异或:0x0000
以上参数决定了 Modbus 在 RTU 模式下的标准 CRC16。
两种实现方式:直接计算 vs 查表
直接计算适合理解原理,但逐位循环效率低。查表法用空间换时间,适合实时通信。
直接计算示例(右移版)
uint16_t crc16_modbus_direct(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
while (len--) {
crc ^= *data++;
for (int i = 0; i < 8; i++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
这里使用反转多项式 0xA001,与直接使用0x8005左移算法等价,但更贴近单片机小端处理。
查表法实现
生成查找表(在PC或初始化时调用)
uint16_t crc16_table[256];
void crc16_init(void) {
for (int i = 0; i < 256; i++) {
uint16_t crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xA001;
else
crc >>= 1;
}
crc16_table[i] = crc;
}
}
查表计算函数
uint16_t crc16_modbus_table(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
while (len--) {
uint8_t idx = (crc ^ *data++) & 0xFF;
crc = (crc >> 8) ^ crc16_table[idx];
}
return crc;
}
注意:查表函数与生成表的移位方向必须一致。这里表内保存的是单字节参与计算后的结果,因此直接异或高字节即可。
实用例程:Modbus 报文生成与校验
Modbus RTU 帧末尾附加两个字节 CRC,先低字节后高字节。以下是一个完整示例:
#include <stdint.h>
uint16_t crc16_modbus(uint8_t *data, uint16_t len); // 上面任意实现
void build_frame(uint8_t addr, uint8_t func, uint8_t *payload, uint8_t plen, uint8_t *frame) {
uint8_t len = 2 + plen; // 地址、功能码 + 数据
frame[0] = addr;
frame[1] = func;
for (int i = 0; i < plen; i++) {
frame[2 + i] = payload[i];
}
uint16_t crc = crc16_modbus(frame, len);
frame[len] = crc & 0xFF; // 低字节
frame[len + 1] = crc >> 8; // 高字节
}
在接收端,对整帧(含CRC)做一次CRC计算,结果为0则校验通过,这是一种常见做法。
配置步骤(移植到新MCU)
- 定义索引表类型,确认
uint16_t至少16位。 - 在初始化阶段调用
crc16_init()生成表。 - 将
crc16_modbus_table封装到协议层。 - 发送时计算CRC并按“低前高后”填充。
- 接收时对包括CRC的整帧进行校验,结果应为0。
如果没有特殊要求,可跳过生成表,直接将预生成的静态表存入代码区,节省启动时间。
注意事项
- 字节序:Modbus 规定 CRC 低字节在前,发送顺序不可颠倒。
- 多字节数据:数据流按实际字节顺序输入,不要对16位数做字节序转换。
- 查表法生成的表是全局的,若在中断中使用,需保证初始化完成。
- 某些单片机可用硬件CRC外设,但参数需与Modbus匹配(如多项式、初值、输入/输出反转),否则结果不一致。
总结
CRC 是协议层的“安全带”,Modbus CRC16 通过标准参数和查表优化,可在任何单片机上高效实现。掌握其数学原理和实现细节,能帮助你更好地调试通信故障。