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 值,存成一个表。每次处理一个字节,直接从表里查,一次搞定。

这个表怎么来的?我简单说一下原理:

  1. 对于 0 到 255 的每个字节,把它左移 24 位,作为初始 CRC 值。
  2. 然后循环 8 次,每次判断最高位是否为 1。如果是,就左移一位再异或多项式;如果不是,只左移。
  3. 循环结束后,得到的就是这个字节对应的表项。

代码实现如下:

// 生成 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,再跟尾部存储的值比对。

具体流程如下:

  1. 生成阶段:计算整个固件数据的 CRC32 值。
  2. 追加阶段:把 CRC32 值(4 字节)追加到固件末尾。
  3. 烧录阶段:把带 CRC32 的固件烧录到 Flash。
  4. 验证阶段: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 整体流程可视化

下面这张图,展示了从生成到验证的完整流程:

CRC32 固件校验流程 CRC32 固件校验流程 1. 计算固件 CRC32 2. 追加 4 字节到尾部 3. 烧录到 Flash 4. 读取固件数据 5. 重新计算 CRC32 6. 比对尾部 CRC32 7. 一致则启动,否则报错 注意事项 • 计算范围不包括 CRC32 • 初始值 0xFFFFFFFF • 结果异或 0xFFFFFFFF

3.5 常见问题与避坑

问题 原因 解决方法
CRC32 算出来跟工具不一致 多项式、初始值、反转设置不同 统一使用标准参数:0x04C11DB7,初始 0xFFFFFFFF,结果异或 0xFFFFFFFF,输入输出反转
查表法结果不对 表生成时多项式方向错误 标准多项式 0x04C11DB7 反转后是 0xEDB88320,查表法要用反转后的多项式
验证时 CRC32 总是对不上 计算范围包含了尾部 CRC32 计算时只算固件数据,不包括最后 4 字节
Flash 读取时字节序不对 大端小端不匹配 确保存储和读取的字节序一致,通常用小端

我的建议:在开发阶段,先用 PC 工具算一遍 CRC32,跟嵌入式代码算的结果比对。确认一致后,再集成到 Bootloader 里。这一步能省掉很多调试时间。