第28问:CRC校验——从原理到C语言实现
CRC校验,说白了就是给数据算个「指纹」。
我在嵌入式项目里用过不下十次CRC。从I2C通信到OTA升级包校验,几乎每个需要可靠传输的地方,都能看到它的身影。你可能会问:为什么不用简单的累加和?嗯,这个问题我当年也问过,直到有一次产品在现场出现了数据错乱,累加和愣是没检测出来——从那以后,我对CRC就格外上心了。
CRC的核心思想
CRC的全称是循环冗余校验。它的本质是:把数据看作一个巨大的二进制数,然后用一个固定的「除数」去做模2除法,得到的余数就是CRC值。
这里的关键是「模2除法」。它和我们小学学的除法不一样——它不考虑进位,说白了就是按位异或。你想想看,硬件实现起来就特别简单,几个异或门就能搞定。
核心公式:
发送数据 = 原始数据 × 2^n + CRC值
其中n是CRC的位数(8、16、32)。接收方用同样的多项式去除,余数为0则数据正确。
多项式——CRC的灵魂
每个CRC算法都有一个多项式。比如:
| 类型 | 多项式(十六进制) | 多项式(二进制) |
|---|---|---|
| CRC-8 | 0x07 | 0000 0111 |
| CRC-16/IBM | 0x8005 | 1000 0000 0000 0101 |
| CRC-32 | 0x04C11DB7 | 0000 0100 1100 0001 0001 1101 1011 0111 |
我个人习惯把多项式记成「生成多项式」。它决定了CRC的检错能力。多项式选得好,能检测出所有单比特错、双比特错、奇数个比特错,甚至突发错误。
小经验:我在做Modbus协议时,用的就是CRC-16/IBM。这个多项式在工业现场表现很稳定,误判率极低。
CRC的计算过程
咱们一步步来,别急。
- 初始化:CRC寄存器置为全1或全0(不同标准不一样)
- 逐字节处理:每个字节与CRC寄存器高8位异或
- 逐位运算:对每个比特,判断最高位是否为1
- 移位与异或:左移一位,如果最高位是1,就异或多项式
- 输出:处理完所有字节后,CRC寄存器取反(有些标准需要)
听起来有点绕?我画个图你就明白了。
CRC-8的C语言实现
先来个最简单的。CRC-8常用于1-Wire总线、SMBus等场合。
// CRC-8 查表法实现
// 多项式:0x07(x^8 + x^2 + x + 1)
static uint8_t crc8_table[256];
void crc8_init_table(void)
{
for (int i = 0; i < 256; i++) {
uint8_t crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 0x80)
crc = (crc << 1) ^ 0x07;
else
crc <<= 1;
}
crc8_table[i] = crc;
}
}
uint8_t crc8_calc(uint8_t *data, uint32_t len)
{
uint8_t crc = 0xFF; // 初始值全1
for (uint32_t i = 0; i < len; i++) {
crc = crc8_table[(crc ^ data[i]) & 0xFF];
}
return crc ^ 0xFF; // 输出取反
}
注意:我曾经在某个项目里忘了初始化查表,结果CRC算出来全是错的。排查了整整半天才发现——嗯,这种低级错误犯一次就够了。
CRC-16的C语言实现
CRC-16在工业通信里用得最多。Modbus、Profibus都用它。
// CRC-16/IBM 查表法
// 多项式:0x8005
static uint16_t crc16_table[256];
void crc16_init_table(void)
{
for (int i = 0; i < 256; i++) {
uint16_t crc = i << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x8005;
else
crc <<= 1;
}
crc16_table[i] = crc;
}
}
uint16_t crc16_calc(uint8_t *data, uint32_t len)
{
uint16_t crc = 0xFFFF;
for (uint32_t i = 0; i < len; i++) {
crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF];
}
return crc;
}
你看,CRC-16和CRC-8的框架几乎一样。只是寄存器和多项式换成了16位的。这就是查表法的好处——一旦理解了核心逻辑,换什么多项式都只是改几个参数的事。
CRC-32的C语言实现
CRC-32是重量级选手。ZIP、PNG、以太网都用它。检错能力极强。
// CRC-32 查表法
// 多项式:0x04C11DB7
static uint32_t crc32_table[256];
void crc32_init_table(void)
{
for (int i = 0; i < 256; i++) {
uint32_t crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 0x80000000)
crc = (crc << 1) ^ 0x04C11DB7;
else
crc <<= 1;
}
crc32_table[i] = crc;
}
}
uint32_t crc32_calc(uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < len; i++) {
crc = (crc << 8) ^ crc32_table[((crc >> 24) ^ data[i]) & 0xFF];
}
return crc ^ 0xFFFFFFFF;
}
性能提示:查表法比直接计算快8倍左右。在STM32这类MCU上,CRC-32查表法处理1KB数据只需要几十微秒。如果你的MCU有硬件CRC模块(比如STM32的CRC外设),那就直接用硬件——又快又省心。
三种CRC的对比
| 特性 | CRC-8 | CRC-16 | CRC-32 |
|---|---|---|---|
| 校验值长度 | 1字节 | 2字节 | 4字节 |
| 检错能力 | 一般 | 良好 | 极强 |
| 典型应用 | 1-Wire、小数据包 | Modbus、工业总线 | 以太网、文件校验 |
| 查表内存开销 | 256字节 | 512字节 | 1024字节 |
| 处理速度 | 最快 | 较快 | 较慢(但可接受) |
避坑指南
我踩过的坑,你最好别踩。
- 初始值别搞错:不同标准初始值不同。CRC-8/MAXIM用0x00,CRC-8/DALLAS用0xFF。搞错了算出来全不对。
- 输出要不要取反:有些标准最后要异或0xFF或0xFFFF。查一下你用的协议规范。
- 字节序问题:多字节CRC要注意大小端。我在一个项目里,PC端算的CRC和MCU端对不上,最后发现是字节序反了。
- 查表法要预初始化:别在每次计算时都重新建表。初始化一次,后面直接用。
我的建议:如果你刚开始用CRC,先拿CRC-16练手。它比CRC-8强,又比CRC-32简单。等熟悉了再升级到CRC-32。我在一个OTA项目里,就是用CRC-16做包内校验,CRC-32做整体固件校验——两层保险,稳得很。
CRC这东西,说白了就是「用空间换时间,用计算换可靠」。在嵌入式系统里,它是最实用的错误检测手段之一。你想想看,一个查表法CRC-16,512字节的表格,就能检测出几乎所有常见错误——这笔买卖,划算。
公众号:蓝海资料掘金营,微信deep3321