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,累加和也不变。
累加和适合什么场景?我个人觉得,调试阶段、非关键数据、或者作为 CRC 的辅助校验,还是可以用的。但正式量产,千万别省这点功夫。
4.2 CRC 原理:为什么它更靠谱?
CRC(循环冗余校验)比累加和聪明在哪?它把数据当成一个巨大的二进制数,然后用一个固定的「除数」去做多项式除法。余数就是 CRC 值。
你可能会问:「除法有什么特别的?」关键在于,CRC 用的是模 2 除法,不进位、不借位。这就意味着,每一位的翻转都会影响最终结果,而且影响路径是「扩散」的。
举个例子,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 位突发错误 | 固件校验、以太网 |
4.4 CRC-32 在固件中的典型应用
实际固件里,CRC-32 通常用在三个地方:
- 固件头校验:在固件镜像头部存放 CRC-32 值,Bootloader 先校验头部,通过后再加载。
- 整包校验:OTA 下载完成后,对整个固件包算一次 CRC-32,与服务器下发的值比对。
- 运行时校验:系统启动后,后台线程定期对 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;
}
0xFFFFFFFF 初始值 + 0xFFFFFFFF 异或输出,是 PKZIP 标准。如果你和上位机工具对不上,先检查这两个参数。
4.5 知识体系总览
下面这张图,把校验和的核心脉络串起来了:
4.6 选型建议与避坑
说了这么多,到底怎么选?我个人的经验是:
- 调试阶段:用累加和,快、省事,能发现大部分明显错误。
- 正式量产:必须上 CRC-32。别省那几十毫秒的计算时间,省下来的时间可能都在售后返修上找补回来。
- 安全敏感场景:CRC-32 只能防随机错误,防不了恶意篡改。需要上哈希(如 SHA-256)或签名。
最后提醒一句:CRC-32 的查表法实现,记得把表放在 RAM 里还是 Flash 里? 我建议放 Flash,256 个 uint32_t 才 1KB,对现代 MCU 来说根本不是事。但如果你在 RAM 紧张的芯片上做 Bootloader,可以考虑运行时动态生成表,或者直接用逐位计算法(慢一些,但省空间)。