第四节:CRC32校验算法——固件升级的“守门员”

各位同学,今天我们来聊聊CRC32。说实话,这玩意儿在嵌入式领域太常见了。你随便找个固件升级的流程,十有八九都能看到它的影子。我最早接触CRC32是在做一款工业分拣机的时候,当时因为传输干扰导致固件写错,机器直接“死”在产线上。从那以后,我对CRC32就格外上心。

CRC32说白了就是一种校验算法。它能帮你判断数据在传输或存储过程中有没有被篡改。嗯,你可以把它想象成一个“数字指纹”。发送方算出一个值,接收方也算一个,两个一比对,就知道数据对不对了。

4.1 CRC32的原理——别被数学公式吓到

很多人一看到CRC的数学推导就头疼。其实你不用完全搞懂那些多项式除法。我个人的理解方式很简单:CRC就是把数据当成一个巨大的二进制数,然后除以一个固定的“除数”(生成多项式),余数就是CRC值。

CRC32用的生成多项式是固定的:

0x04C11DB7

这个多项式是国际标准。你想想看,全世界那么多设备都用同一个除数,就是为了保证兼容性。我在项目中遇到过用错多项式的情况,结果两边算出来的CRC永远对不上,排查了半天才发现是多项式写错了。

CRC32的计算过程大致分几步:

  • 初始化CRC寄存器为0xFFFFFFFF
  • 逐字节处理输入数据
  • 每处理一个字节,都要进行32次移位和异或操作
  • 最终结果再异或0xFFFFFFFF

为什么要异或两次0xFFFFFFFF?说白了就是为了把全0的数据也能算出一个非零的CRC值,避免漏检。

核心要点:CRC32不是加密算法,它不防恶意篡改,只防随机错误。如果有人故意改你的固件,CRC32是挡不住的。这一点后面讲安全防护时会再强调。

4.2 查表法实现——让单片机跑得快一点

直接按位计算CRC32,在PC上没问题。但在单片机上,尤其是那些主频只有几十兆的MCU,逐位计算太慢了。我曾经在一个项目里用直接法算一个256KB的固件,花了将近3秒钟——这显然不能忍。

查表法就是典型的“空间换时间”。我们预先算好所有可能的字节(0~255)对应的CRC值,存成一个256个元素的表。计算时每来一个字节,直接从表里查,一次搞定。

下面是我常用的查表法实现,C语言版本:

// 生成CRC32查表
void crc32_generate_table(uint32_t table[256]) {
    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;
        }
        table[i] = crc;
    }
}

// 查表计算CRC32
uint32_t crc32_compute(const uint8_t *data, size_t len, const uint32_t table[256]) {
    uint32_t crc = 0xFFFFFFFF;
    for (size_t i = 0; i < len; i++) {
        uint8_t index = (crc ^ data[i]) & 0xFF;
        crc = (crc >> 8) ^ table[index];
    }
    return crc ^ 0xFFFFFFFF;
}

注意看那个0xEDB88320,它其实就是0x04C11DB7的反射形式。因为很多硬件是按LSB first处理的,所以查表法里用的多项式是反射后的版本。嗯,这里容易搞混,我建议你直接复制上面这段代码,别自己手写多项式。

我的经验:查表法在STM32F103上算256KB数据只需要不到100毫秒。如果你用直接法,可能要好几秒。所以,能用查表就别用直接法。

4.3 硬件CRC加速——让外设帮你干活

查表法虽然快,但还是占CPU。如果你用的是带硬件CRC模块的MCU(比如STM32、NXP的LPC系列),那就可以把计算任务交给外设。CPU只需要把数据喂给CRC外设,然后读结果就行。

以STM32为例,硬件CRC的使用非常简单:

// 使能CRC外设时钟
RCC->AHBENR |= RCC_AHBENR_CRCEN;

// 复位CRC计算器
CRC->CR = CRC_CR_RESET;

// 逐字写入数据(注意是32位字)
for (int i = 0; i < len/4; i++) {
    CRC->DR = data[i];
}

// 读取结果
uint32_t crc_result = CRC->DR;

硬件CRC有多快?我实测过,STM32F4系列算1MB数据只需要几毫秒。而且CPU完全不用参与计算,可以去做别的事。这在做固件升级时特别有用——你可以在接收数据的同时,让硬件CRC模块同步计算校验值。

注意:不同厂家的硬件CRC模块,初始值和结果异或值可能不一样。比如STM32默认初始值是0xFFFFFFFF,结果不异或。而软件标准CRC32结果要异或0xFFFFFFFF。所以硬件算出来的值和软件算的可能不一样,需要做适配。我曾经在这个坑里栽过跟头,调试了一整天才发现是初始值没对上。

4.4 在固件升级中的应用——实战中的CRC32

好了,理论说完了,咱们看看CRC32在固件升级里到底怎么用。我参与过的分拣机固件升级流程大概是这样的:

  1. 固件打包:编译好的二进制文件,在末尾追加4字节的CRC32校验值
  2. 传输:通过CAN、UART或以太网发送给设备
  3. 接收校验:设备每收到一包数据,就计算局部CRC,最后算整体CRC
  4. 比对:设备算出的CRC和固件包里的CRC比对,一致则写入Flash
  5. 回读验证:写入完成后,再从Flash读出来算一次CRC,确保写入正确

这里有个细节:分块校验 vs 整体校验。我建议两者都用。每包数据算一个CRC,可以快速定位哪一包传错了。整体CRC则保证整个固件的完整性。

下面是一个简单的固件包头结构体示例:

typedef struct {
    uint32_t magic;          // 魔数,用于识别固件格式
    uint32_t firmware_size;  // 固件大小(不含CRC)
    uint32_t firmware_crc;   // 整个固件的CRC32
    uint32_t reserved;       // 保留
} firmware_header_t;

接收端收到包头后,先校验魔数,再根据firmware_size接收数据,最后用firmware_crc做整体校验。三步缺一不可。

避坑指南:我曾经遇到过一个问题——固件升级到一半突然断电。重启后设备不知道固件写到哪了,结果变砖了。后来我在Flash里加了一个“升级状态标志”,每次写入前先擦除标志,写入完成后再置位。这样重启后就能判断升级是否完成,没完成就回滚到旧固件。CRC在这里的作用是确保回滚的旧固件本身也是完整的。

4.5 总结与建议

CRC32虽然简单,但用好了能省很多麻烦。我个人的建议是:

  • 能用硬件CRC就用硬件,省CPU时间
  • 查表法作为备选,兼容没有硬件CRC的MCU
  • 永远不要只用CRC做安全防护,它只防随机错误,不防恶意攻击
  • 固件升级一定要做双重校验:传输时校验 + 写入后回读校验

好了,这一节就到这里。下一节我们会讲SHA256哈希算法,那才是真正能防篡改的东西。到时候你会看到,CRC32和SHA256配合使用,才是固件升级的完整方案。