3. CRC32 校验原理:多项式选择、查表法实现、在固件尾部的追加与验证
CRC32,全称是循环冗余校验。说白了,它就是一种哈希算法,能把任意长度的数据,算出一个固定长度的“指纹”。这个指纹通常就是 4 个字节,32 位。
我最早接触 CRC32,是在做 Bootloader 的时候。那时候要校验 Flash 里的固件是否完整,总不能每次启动都把整个固件读一遍再算一遍吧?太慢了。后来发现,把 CRC32 值追加在固件尾部,启动时只算一次,比对一下就行。这个思路,一直用到现在。
3.1 多项式选择:为什么是 0x04C11DB7?
CRC32 的核心,是一个二进制多项式。你可以把它理解成除法运算里的“除数”。不同的多项式,算出来的结果不一样,检错能力也不一样。
标准 CRC32 的多项式是:0x04C11DB7。这个多项式是 IEEE 802.3 标准里定义的,也是以太网、ZIP、PNG 等协议通用的。
为什么选它?因为它的检错能力经过了大量验证。它能检测出所有单比特错误、双比特错误、奇数个比特错误,以及长度不超过 32 位的突发错误。对于固件镜像来说,这已经足够了。
我个人习惯,只要不是特殊场景,一律用这个标准多项式。别自己发明多项式,容易踩坑。
标准 CRC32 参数:
- 多项式:0x04C11DB7(二进制:100000100110000010001110110110111)
- 初始值:0xFFFFFFFF
- 结果异或值:0xFFFFFFFF
- 输入输出反转:是(RefIn = true, RefOut = true)
3.2 查表法实现:空间换时间
直接按位计算 CRC32,效率很低。每处理一个字节,要循环 8 次。如果固件有 1MB,那就是 800 万次循环。在嵌入式系统里,这太奢侈了。
查表法,就是提前算好 256 个 CRC32 值,存成一个表。每次处理一个字节,直接从表里查,一次搞定。
这个表怎么来的?我简单说一下原理:
- 对于 0 到 255 的每个字节,把它左移 24 位,作为初始 CRC 值。
- 然后循环 8 次,每次判断最高位是否为 1。如果是,就左移一位再异或多项式;如果不是,只左移。
- 循环结束后,得到的就是这个字节对应的表项。
代码实现如下:
// 生成 CRC32 查表
uint32_t crc32_table[256];
void crc32_init_table(void) {
uint32_t poly = 0xEDB88320; // 反转后的多项式
for (uint32_t i = 0; i < 256; i++) {
uint32_t crc = i;
for (int j = 0; j < 8; j++) {
if (crc & 1) {
crc = (crc >> 1) ^ poly;
} else {
crc >>= 1;
}
}
crc32_table[i] = crc;
}
}
// 查表计算 CRC32
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;
}
你看,核心就两行:取索引,查表,异或。效率比按位计算快 8 倍。
我的经验:查表法虽然快,但表本身占 1KB 空间(256 * 4 字节)。在资源极少的 MCU 上,如果 Flash 空间紧张,可以考虑用半字节查表法,表只有 16 项,64 字节,速度也还行。
3.3 在固件尾部的追加与验证
这是实际项目中最常用的做法。把 CRC32 值追加到固件末尾,烧录到 Flash 里。启动时,Bootloader 读取固件,计算 CRC32,再跟尾部存储的值比对。
具体流程如下:
- 生成阶段:计算整个固件数据的 CRC32 值。
- 追加阶段:把 CRC32 值(4 字节)追加到固件末尾。
- 烧录阶段:把带 CRC32 的固件烧录到 Flash。
- 验证阶段:Bootloader 读取固件数据(不包括最后 4 字节),重新计算 CRC32,再跟最后 4 字节比对。
这里有个细节:追加的时候,CRC32 值本身要不要参与计算?答案是:不参与。计算范围只包括固件数据本身,不包括追加的校验值。
我曾经踩过的坑:有一次,我把 CRC32 值也算进了计算范围,结果算出来的值永远对不上。因为追加后的数据变了,CRC32 也跟着变,变成了一个死循环。记住:计算范围是固件数据,不包括最后 4 字节。
验证代码也很简单:
// 验证固件完整性
// flash_addr: 固件在 Flash 中的起始地址
// total_len: 固件数据长度 + 4 字节 CRC32
int verify_firmware(uint32_t flash_addr, uint32_t total_len) {
uint32_t data_len = total_len - 4; // 固件数据长度
uint32_t stored_crc = *(uint32_t *)(flash_addr + data_len); // 读取存储的 CRC32
uint32_t calc_crc = crc32_calc((uint8_t *)flash_addr, data_len); // 重新计算
if (stored_crc == calc_crc) {
return 0; // 校验通过
} else {
return -1; // 校验失败
}
}
3.4 整体流程可视化
下面这张图,展示了从生成到验证的完整流程:
3.5 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| CRC32 算出来跟工具不一致 | 多项式、初始值、反转设置不同 | 统一使用标准参数:0x04C11DB7,初始 0xFFFFFFFF,结果异或 0xFFFFFFFF,输入输出反转 |
| 查表法结果不对 | 表生成时多项式方向错误 | 标准多项式 0x04C11DB7 反转后是 0xEDB88320,查表法要用反转后的多项式 |
| 验证时 CRC32 总是对不上 | 计算范围包含了尾部 CRC32 | 计算时只算固件数据,不包括最后 4 字节 |
| Flash 读取时字节序不对 | 大端小端不匹配 | 确保存储和读取的字节序一致,通常用小端 |
我的建议:在开发阶段,先用 PC 工具算一遍 CRC32,跟嵌入式代码算的结果比对。确认一致后,再集成到 Bootloader 里。这一步能省掉很多调试时间。