4、校验和(Checksum):简单累加和、CRC原理、CRC-32在固件中的应用

校验和,说白了就是给数据算个「指纹」。你想想看,固件在传输、存储过程中,万一某个 bit 翻了个跟头,整个系统可能就崩了。怎么知道数据有没有被改过?最简单的办法,就是算个和。

我最早接触校验和,是在做 8 位单片机的时候。那时候 Flash 空间金贵得很,能省则省。一个简单的累加和,就能挡住大部分意外。但后来做产品量产,发现累加和根本不够用——两个 bit 同时翻转,累加和居然还是对的。嗯,这就尴尬了。

4.1 简单累加和:入门级防护

简单累加和的思路很直白:把固件里每个字节加起来,只保留低 8 位或低 16 位,作为校验值。

// 简单累加和示例(8位)
uint8_t calc_checksum(uint8_t *data, uint32_t len) {
    uint8_t sum = 0;
    for (uint32_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

优点很明显:代码量小,执行快,几乎不占 RAM。但缺点也致命——碰撞率太高。数据里两个字节互换位置,累加和不变;一个字节加 1、另一个字节减 1,累加和也不变。

避坑提醒:我曾经在一个 Bootloader 项目里只用累加和做校验,结果量产时发现,有 0.1% 的芯片在 OTA 升级后程序跑飞。排查了三天,最后发现是 Flash 写入时地址线毛刺导致数据错位,但累加和居然通过了。从那以后,我再也不敢在正式产品里单独用累加和了。

累加和适合什么场景?我个人觉得,调试阶段、非关键数据、或者作为 CRC 的辅助校验,还是可以用的。但正式量产,千万别省这点功夫。

4.2 CRC 原理:为什么它更靠谱?

CRC(循环冗余校验)比累加和聪明在哪?它把数据当成一个巨大的二进制数,然后用一个固定的「除数」去做多项式除法。余数就是 CRC 值。

你可能会问:「除法有什么特别的?」关键在于,CRC 用的是模 2 除法,不进位、不借位。这就意味着,每一位的翻转都会影响最终结果,而且影响路径是「扩散」的。

核心思想:CRC 的本质是多项式除法。数据看作多项式系数,除数是生成多项式。余数就是校验值。

举个例子,CRC-8 的生成多项式是 x^8 + x^2 + x + 1(对应二进制 100000111)。计算时,数据后面补 8 个 0,然后做模 2 除法,余数就是 8 位 CRC。

为什么 CRC 能检测出「突发错误」?因为多项式除法的数学特性决定了:只要错误长度不超过生成多项式的阶数,CRC 就能 100% 检测出来。累加和可做不到这一点。

4.3 CRC-32:固件校验的工业标准

CRC-32 是 32 位的 CRC,生成多项式是 0x04C11DB7。它在固件领域有多普及?以太网帧、ZIP 压缩包、PNG 图片、甚至你手机里的 OTA 升级包,几乎都在用 CRC-32。

我做过一个统计:在 Cortex-M4 上,用查表法计算 1MB 固件的 CRC-32,大约需要 30ms(主频 168MHz)。这个速度对 Bootloader 来说完全够用。

CRC 类型 生成多项式 校验位宽 检测能力 典型应用
CRC-8 0x07 8 bit 检测 ≤8 位突发错误 1-Wire、SMBus
CRC-16 0x8005 16 bit 检测 ≤16 位突发错误 Modbus、USB
CRC-32 0x04C11DB7 32 bit 检测 ≤32 位突发错误 固件校验、以太网
经验之谈:我建议在固件校验中优先使用 CRC-32。原因有三:1) 32 位碰撞概率极低(约 1/40 亿);2) 硬件 CRC 外设几乎已成 MCU 标配;3) 与上位机工具兼容性好,不用自己造轮子。

4.4 CRC-32 在固件中的典型应用

实际固件里,CRC-32 通常用在三个地方:

  1. 固件头校验:在固件镜像头部存放 CRC-32 值,Bootloader 先校验头部,通过后再加载。
  2. 整包校验:OTA 下载完成后,对整个固件包算一次 CRC-32,与服务器下发的值比对。
  3. 运行时校验:系统启动后,后台线程定期对 Flash 中的固件算 CRC-32,检测是否被意外篡改。

下面是一个典型的查表法 CRC-32 实现,我项目中一直在用:

// CRC-32 查表法实现(多项式 0x04C11DB7)
uint32_t crc32_table[256];

void crc32_init_table(void) {
    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) ^ 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;
}
注意:CRC-32 的初始值和最终异或值,不同标准可能不同。比如 0xFFFFFFFF 初始值 + 0xFFFFFFFF 异或输出,是 PKZIP 标准。如果你和上位机工具对不上,先检查这两个参数。

4.5 知识体系总览

下面这张图,把校验和的核心脉络串起来了:

校验和知识体系 校验和(Checksum) 简单累加和 CRC 循环冗余校验 CRC-32 工业标准 优点:代码小、速度快 缺点:碰撞率高、易绕过 原理:模2多项式除法 检测能力:突发错误 ≤ 阶数 多项式:0x04C11DB7 应用:固件头/整包/运行时 选型建议:正式产品用 CRC-32,调试辅助用累加和 查表法实现,注意初始值和异或值标准

4.6 选型建议与避坑

说了这么多,到底怎么选?我个人的经验是:

  • 调试阶段:用累加和,快、省事,能发现大部分明显错误。
  • 正式量产:必须上 CRC-32。别省那几十毫秒的计算时间,省下来的时间可能都在售后返修上找补回来。
  • 安全敏感场景:CRC-32 只能防随机错误,防不了恶意篡改。需要上哈希(如 SHA-256)或签名。
一句话总结:累加和是「有没有错」,CRC 是「错在哪」,哈希是「谁改的」。固件校验用 CRC-32,性价比最高。

最后提醒一句:CRC-32 的查表法实现,记得把表放在 RAM 里还是 Flash 里? 我建议放 Flash,256 个 uint32_t 才 1KB,对现代 MCU 来说根本不是事。但如果你在 RAM 紧张的芯片上做 Bootloader,可以考虑运行时动态生成表,或者直接用逐位计算法(慢一些,但省空间)。