3. CRC校验原理:数学基础、常见标准与多项式选择
CRC校验,说白了就是给数据算一个“指纹”。这个指纹很小,但能帮你判断数据在传输或存储过程中有没有被改过。我在Bootloader里做固件升级时,CRC几乎是必用的——Flash写入后读回来对不对?跳转前校验一下,心里就有底了。
这一章我们不讲太深的数学推导,但核心原理你得吃透。不然选错了多项式,或者实现有bug,板子变砖了都不知道问题出在哪。
3.1 CRC的数学本质:模2除法
CRC的数学基础是多项式除法,而且是模2除法。什么叫模2?就是加法不进位,减法不借位,说白了就是异或(XOR)运算。
举个例子:
1101
+ 1011
------
0110 (模2加法,没有进位)
CRC的计算过程是这样的:
- 把原始数据看作一个二进制多项式(比如0b1011就是x³ + x + 1)
- 在数据后面补上多项式阶数个0(比如CRC-8就补8个0)
- 用选定的生成多项式做模2除法
- 余数就是CRC校验值
关键点:CRC不是哈希,它没有“雪崩效应”。两个相近的数据,CRC值也可能相近。所以CRC适合检测随机错误,不适合做安全校验(比如防篡改)。
我记得刚入行时,有个同事把CRC和MD5搞混了,非要用CRC-32做固件签名验证。结果呢?攻击者随便改几个字节,CRC值还能对上。嗯,那件事之后我们专门给团队做了次培训。
3.2 常见CRC标准:CRC-8/16/32
实际工程中,我们不会自己发明多项式。业界已经定义好了标准,直接用就行。下面是我常用的几个:
| 标准 | 多项式(十六进制) | 初始值 | 输出异或 | 典型应用 |
|---|---|---|---|---|
| CRC-8 | 0x07 | 0x00 | 0x00 | 1-Wire、SMBus |
| CRC-16-IBM | 0x8005 | 0x0000 | 0x0000 | Modbus、USB |
| CRC-16-CCITT | 0x1021 | 0xFFFF | 0x0000 | XMODEM、蓝牙 |
| CRC-32 | 0x04C11DB7 | 0xFFFFFFFF | 0xFFFFFFFF | Ethernet、ZIP、PNG |
我的建议:Bootloader里做固件校验,CRC-32是首选。8位太弱,16位勉强够用,32位能覆盖大部分Flash容量(4GB以内)。如果你的MCU有硬件CRC模块,记得优先用硬件——速度能差几十倍。
3.3 多项式选择原则:别乱选,有讲究
多项式不是随便挑的。选错了,CRC的检错能力会大打折扣。我见过有人用0x01当多项式——那还不如不加校验,因为余数永远都是0。
选多项式时,你得关注这几个指标:
- HD(汉明距离):能检测出多少个比特错误。HD=4意味着最多能保证检测出3个比特的错误。
- 本原多项式:这种多项式生成的CRC周期最长,检错能力最强。
- 奇偶性:好的多项式通常包含x+1因子,能检测所有奇数个错误。
举个例子,CRC-32(0x04C11DB7)的HD在数据长度≤12KB时是4,超过12KB会降到3。所以如果你的固件有1MB,CRC-32并不能保证检测出所有4比特错误——但实际工程中,这已经够用了。
避坑指南:我曾经在一个项目里用了CRC-16-IBM做128KB固件校验,结果发现它只能检测HD=2的错误。后来换成CRC-32,问题才解决。所以,小数据用短CRC,大数据用长CRC,这是铁律。
3.4 CRC计算流程:从数据到校验值
下面这张图展示了CRC-32的完整计算流程。我建议你把它存下来,写代码时对照着看。
实际代码实现时,我推荐用查表法。预计算256个表项,每个字节进来直接查表,比逐位计算快8倍。下面是一个CRC-32查表法的核心代码:
// CRC-32 查表法实现
uint32_t crc32_table[256];
void crc32_init_table(void) {
uint32_t crc;
for (int i = 0; i < 256; i++) {
crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320; // 反射多项式
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++) {
uint8_t index = (crc ^ data[i]) & 0xFF;
crc = (crc >> 8) ^ crc32_table[index];
}
return crc ^ 0xFFFFFFFF;
}
注意:上面的多项式0xEDB88320是0x04C11DB7的反射版本。因为很多MCU是小端模式,反射多项式算起来更快。如果你的平台是大端,直接用0x04C11DB7就行。
3.5 工程中的CRC校验策略
在Bootloader里,CRC校验不是算一次就完事了。我一般会分两层:
- 分区内CRC:每个Flash分区末尾存一个CRC-32值,覆盖整个分区数据。写入时算一次,跳转前再算一次,对比一致才放行。
- 全局CRC:所有分区数据合并算一个CRC,用于固件完整性验证。这个通常在固件打包时由上位机生成,Bootloader只做校验。
你想想看,如果只做全局CRC,分区损坏了你还得全量重刷。有了分区内CRC,哪个分区坏了就刷哪个,省时省力。
我曾经踩过的坑:有一次我忘了考虑CRC的初始值和输出异或。上位机用标准库算出来的CRC,和Bootloader算出来的对不上。查了两天才发现,上位机默认初始值是0xFFFFFFFF,我代码里写的是0x00000000。所以,CRC参数必须对齐,差一个比特都不行。
3.6 小结:CRC选型速查表
最后给你一张速查表,做Bootloader时直接对着选:
| 场景 | 推荐CRC | 理由 |
|---|---|---|
| 小数据块(<256字节) | CRC-8 | 够用,省空间 |
| Modbus通信 | CRC-16-IBM | 协议标准,兼容性好 |
| 固件分区校验 | CRC-32 | 检错能力强,覆盖范围大 |
| 安全敏感场景 | 不要用CRC | 用SHA-256或HMAC |