4、CRC校验实战:CRC-8/CRC-16/CRC-32原理、查表法实现、在理疗仪协议中的应用
各位同学,咱们今天聊点实在的——CRC校验。
说实话,我在做理疗仪通信协议那会儿,最头疼的就是数据传着传着就错了。你说病人正做着理疗呢,突然来一个误码,功率参数变了,那可不是闹着玩的。所以CRC校验,说白了就是给数据包加一把锁,确保它从A到B一路平安。
4.1 CRC到底是什么?
CRC的全称是循环冗余校验(Cyclic Redundancy Check)。名字挺唬人,其实原理不复杂。
你想想看,我们发送一串数据,比如“10110010”。接收方拿到后,怎么知道这串数据有没有被干扰过?CRC的做法是:发送方根据数据算出一个固定长度的校验码,附在数据后面一起发出去。接收方用同样的算法再算一遍,如果结果一致,说明数据没问题。
这个“算法”的核心,就是多项式除法。嗯,别被“多项式”三个字吓到,其实就是二进制除法,只不过把每一位当成多项式的系数。
核心思想:将数据看作一个二进制多项式,用约定的生成多项式去除,余数就是CRC校验码。
我在项目中遇到过一种情况:有人直接用累加和做校验,结果数据里同时翻转两位,累加和竟然没变。CRC就不会有这个问题,因为它对数据的变化非常敏感。
4.2 CRC-8、CRC-16、CRC-32的区别
这三种CRC,说白了就是校验码的长度不同,生成多项式也不同。我一般这么选:
| 类型 | 校验码长度 | 生成多项式(常见) | 适用场景 |
|---|---|---|---|
| CRC-8 | 8位(1字节) | 0x07(x⁸ + x² + x + 1) | 小数据包、传感器数据 |
| CRC-16 | 16位(2字节) | 0x8005(x¹⁶ + x¹⁵ + x² + 1) | 理疗仪通信帧、Modbus协议 |
| CRC-32 | 32位(4字节) | 0x04C11DB7 | 文件校验、网络传输 |
我个人习惯,理疗仪这种嵌入式设备,CRC-16就够用了。为什么?因为理疗仪的数据包一般不超过256字节,CRC-16的碰撞概率已经非常低。用CRC-32反而浪费带宽和计算时间。
我的经验:在理疗仪协议中,我通常用CRC-16/CCITT(多项式0x1021),因为它的硬件实现简单,很多MCU都有硬件CRC模块支持。
4.3 查表法实现——为什么快?
直接按位计算CRC,效率太低了。你想想看,每处理一个字节就要循环8次,处理100个字节就是800次循环。在低功耗MCU上,这简直是灾难。
查表法的思路很简单:提前把0~255这256个字节对应的CRC值算好,存成一张表。计算时直接查表,一个字节只需要一次查表操作。
我曾经在一个项目里,用直接计算法,CRC校验花了2毫秒。换成查表法后,0.1毫秒就搞定了。嗯,差距就是这么大。
下面我给出一个CRC-16查表法的实现,这是我在理疗仪项目里实际用过的代码:
// 生成CRC-16查表(多项式0x8005)
uint16_t crc16_table[256];
void crc16_init_table(void) {
uint16_t i, j, crc;
for (i = 0; i < 256; i++) {
crc = i << 8;
for (j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x8005;
else
crc = crc << 1;
}
crc16_table[i] = crc;
}
}
// 查表计算CRC-16
uint16_t crc16_calc(uint8_t *data, uint16_t len) {
uint16_t crc = 0x0000; // 初始值
uint16_t i;
for (i = 0; i < len; i++) {
crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
}
return crc;
}
注意:不同CRC标准,初始值可能不同。有的用0x0000,有的用0xFFFF。还有的输出结果要异或0xFFFF。一定要确认你用的协议标准。
4.4 在理疗仪协议中的应用
好,咱们来点实战的。理疗仪的通信协议,我一般这样设计数据帧:
| 帧头 | 地址 | 命令 | 数据长度 | 数据 | CRC-16 | 帧尾 |
|---|---|---|---|---|---|---|
| 0xAA 0x55 | 1字节 | 1字节 | 1字节 | N字节 | 2字节 | 0x0D 0x0A |
CRC校验的范围是从“地址”到“数据”结束,不包括帧头和帧尾。为什么?因为帧头和帧尾是固定的,不需要校验。我曾经见过有人把帧头也算进去,结果帧头变了CRC也跟着变,反而增加了误判的概率。
下面是一个完整的发送和校验示例:
// 发送数据帧(带CRC)
void send_frame(uint8_t addr, uint8_t cmd, uint8_t *data, uint8_t len) {
uint8_t buffer[256];
uint16_t crc;
uint8_t i, idx = 0;
// 帧头
buffer[idx++] = 0xAA;
buffer[idx++] = 0x55;
// 地址、命令、长度
buffer[idx++] = addr;
buffer[idx++] = cmd;
buffer[idx++] = len;
// 数据
for (i = 0; i < len; i++) {
buffer[idx++] = data[i];
}
// 计算CRC(从地址开始到数据结束)
crc = crc16_calc(&buffer[2], len + 3);
buffer[idx++] = (crc >> 8) & 0xFF;
buffer[idx++] = crc & 0xFF;
// 帧尾
buffer[idx++] = 0x0D;
buffer[idx++] = 0x0A;
// 发送
uart_send(buffer, idx);
}
// 接收校验
uint8_t check_frame(uint8_t *buffer, uint16_t len) {
uint16_t crc_recv, crc_calc;
// 检查帧头帧尾
if (buffer[0] != 0xAA || buffer[1] != 0x55) return 0;
if (buffer[len-2] != 0x0D || buffer[len-1] != 0x0A) return 0;
// 提取接收到的CRC
crc_recv = (buffer[len-4] << 8) | buffer[len-3];
// 计算CRC(从地址到数据结束)
crc_calc = crc16_calc(&buffer[2], len - 6);
return (crc_recv == crc_calc);
}
避坑指南:我曾经犯过一个错误——CRC计算时把帧尾也包含进去了。结果接收方怎么算都对不上。后来排查了半天才发现,帧尾的0x0D和0x0A被当成数据参与了CRC计算。记住:CRC只校验有效数据部分。
4.5 实际调试中的小技巧
调试CRC的时候,我一般会先用一个已知的数据做测试。比如发送“0x01 0x02 0x03”,用在线CRC计算器算好预期的CRC值,然后对比程序算出来的。如果对不上,八成是多项式或者初始值搞错了。
还有一个坑:字节序。有些MCU是大端,有些是小端。CRC计算时,数据字节的顺序一定要和协议定义一致。我习惯统一用小端,因为ARM Cortex-M系列默认就是小端。
我的建议:在理疗仪项目中,把CRC校验函数做成一个独立的模块,用单元测试覆盖。这样换MCU或者改协议时,只需要改这个模块,其他地方不用动。
好了,CRC校验这块就讲这么多。说白了,它就是数据通信的“安全员”,虽然不起眼,但少了它,整个系统就不可靠了。下一节咱们聊聊更高级的校验方式,以及如何在多机通信中避免冲突。