一句话结论:MCU 硬件 CRC 外设算出来的"CRC-32",几乎从来不等于标准 ZLIB/PKZIP 的 CRC-32。 把它当成标准 CRC 用,对端校验时必然翻车。


一次真实的翻车现场

某个 OTA 升级项目,链路是这样的:

PC 打包工具 (标准 ZLIB CRC-32)
        ↓ 生成校验值写入固件头
链路对端 MCU (软件 ZLIB CRC-32)
        ↓ 转发固件
目标 MCU Bootloader (硬件 CRC 外设)
        ↓ 重读 Flash 校验
        ❌ 校验失败!

三方协作,两方用标准 ZLIB,一方用硬件 CRC——结果对不上。排查了几个小时,发现问题是:双方虽然都叫"CRC-32",但根本不是同一个算法。

实测数据触目惊心:

实现 测试向量 "123456789" 与实际固件头匹配?
PC 打包 (Python binascii.crc32) 0xCBF43926
链路对端 MCU (软件逐位 ZLIB) 0xCBF43926
目标 MCU (硬件 CRC 外设) 0x500E6FA8

同一段固件,两种算法算出了完全不同的值。Bootloader 侧的合法性检查必然失败。


认知篇:为什么硬件 CRC 不用标准反射形式

先纠正一个最常见的直觉误区——硬件 CRC 用的不是"容易让人误解的野路子算法",它用的恰恰是最"标准"的那个形式。

谁才是"标准"?

CRC-32 的多项式 0x04C11DB7 出自 ITU-T V.42 规范,并被 IEEE 802.3(以太网 FCS)采用。两种常见形式的关系:

  • 经典硬件 CRC(STM32F0 等经典外设):多项式 0x04C11DB7MSB-first 非反射处理 → 这就是 V.42 / 以太网规范里"按原样书写"的形式。
  • 软件 ZLIB / PKZIP 的 0xEDB88320 反射形式:在原版多项式基础上把位序翻转得到的变体,专门为了软件跑得快

所以真相是反过来的:硬件用的是"规范原版",软件用的才是"为性能优化的翻版"。 只是因为今天嵌入式开发者张口闭口"CRC-32 = zlib = 0xEDB88320"(Python 的 binascii.crc32、C 的 zlib 到处都是),大家潜意识里把反射变体当成了"标准",一看到硬件给的非反射值就觉得"它错了"。

为什么硬件选了 MSB-first 非反射?

  1. 它忠于规范本身的位序。 多项式 0x04C11DB7 在 V.42 / 以太网规范里就是"最高位先处理"(MSB-first)。一个 LFSR 移位寄存器按规范描述实现,自然就是非反射。

  2. 反射形式是被软件逼出来的优化。 0xEDB88320 这个反射多项式之所以流行,是因为它被查表法算 CRC 时用起来最快:一次处理一个字节、右移、查表,CPU 跑得飞起。但纯硬件移位寄存器里,左移和右移一样简单,硬件没有动力去反射。

  3. 外设的设计目的根本不是"校验 ZIP 包"。 这类 CRC 外设诞生时,定位是一个廉价、固定的完整性校验原语,给 Flash 自检、内部总线传输用,从没打算去兼容 PKZIP 的归档校验和。

  4. 硅面积最小化,砍掉了反射配置。 经典外设只有 DR / IDR / CR 三个寄存器,连 REV_IN / REV_OUT / POLY 配置位都没有。反射 / 多项式全硬接线,就是为了省硅。

对比:后续的新版 STM32 家族已经加了 REV_IN / REV_OUT 可配置位,可以当场切换成 ZLIB 反射形式——正是因为这坑太常见,原厂后来才补上配置能力。

"容易误解"的真正根源

软件约定反客为主造成的认知错位:

规范世界:  poly 0x04C11DB7, MSB-first    ← 硬件照此实现
软件世界:  poly 0xEDB88320, LSB-first    ← zlib 优化版,大家天天见

开发者脑子里装的是软件世界的样子,拿去套硬件外设,发现对不上 → 直觉是"硬件坏了"。其实两边都是合法标准,只是位序约定不同,数学上不构成等价。


一个 CRC 算法到底由什么定义

光说"CRC-32"四个字,完全不能唯一确定一个算法。必须锁定下面 6 个参数:

参数 含义
Polynomial 多项式(注意有"正向"和"反射"两种写法)
Init 初始值
RefIn 输入数据是否按位反转
RefOut 输出结果是否按位反转
XorOut 最终结果是否再异或一个常量
数据宽度 / 字节序 按字节喂,还是按 32-bit 字喂;字内字节顺序

两个最容易混淆的"CRC-32"对照:

参数 标准 CRC-32 / ZLIB 经典硬件 CRC
Polynomial 0x04C11DB7(反射 0xEDB88320 0x04C11DB7
Init 0xFFFFFFFF 0xFFFFFFFF
RefIn true(LSB-first) false(MSB-first)
RefOut true false
XorOut 0xFFFFFFFF 0x00000000(需手写)
喂数方式 字节流 32-bit 字(小端)
可配置? 不可配置,硬接线固定

关键结论:两者多项式"看起来一样",但 RefIn/RefOut 相反,算出的结果数学上不等价。看到硬件手册写"多项式 0x04C11DB7"就以为和标准 CRC-32 一致,是必定踩坑的起点。


五个最常见的坑

坑 1:反射 vs 非反射(最隐蔽也最致命)

软件 ZLIB 用反射多项式 0xEDB88320,逐字节 LSB-first 处理。经典硬件外设用正向多项式 0x04C11DB7,MSB-first 处理。两者对同一段数据的结果永远不同

坑 2:按 32-bit 字喂入时的字节序

硬件 CRC 通常吞 32-bit 字。小端 CPU 把内存 b0 b1 b2 b3 拼成字 b0 | b1<<8 | b2<<16 | b3<<24,硬件从字的 MSB(b3)先处理 → 实际字节处理顺序是 b3, b2, b1, b0,与字节流 b0, b1, b2, b3 相反

所以"按字喂"的硬件 CRC ≠ "按字节喂"的标准 CRC,即使多项式相同也差一个字节序

坑 3:最终异或常常要手写

很多硬件外设不自动做 XorOut。读出 CRC_DR 后必须自己 ^ 0xFFFFFFFF,否则和任何标准 CRC 都差一个常量。

坑 4:多项式 / 反射不可配置

经典外设把多项式和反射硬接线,寄存器里只有 RESET 位,没有 REV_IN / REV_OUT / POLYSIZE 等位域。想换算法?只能改软件

坑 5:“名字一样,结果不一样”

光"CRC-32"这一个名字下,常见的就有:

  • CRC-32 / ZLIB(PKZIP、ZIP、PNG、以太网 FCS)
  • CRC-32 / MPEG-2(init 0xFFFFFFFF, xorout 0x00000000
  • CRC-32C / Castagnoli(多项式 0x1EDC6F41,用于 iSCSI、SCTP)

看见"CRC-32"别直接当成 ZIP 那个,先确认是哪一家的定义


怎么验证你没踩坑

1) 必做自测向量。 标准 CRC-32 对 "123456789" 的结果恒为 0xCBF43926。任何"号称标准 CRC-32"的实现算不出这个值,就说明它不是标准 CRC-32——立刻报警。

2) 用 Python 把硬件行为复刻出来离线比对。 不要只靠肉眼读手册。下面这段代码可以模拟经典硬件 CRC 的行为:

def _stm_word(crc, word):
    crc ^= word
    for _ in range(32):
        crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if (crc & 0x80000000) else (crc << 1) & 0xFFFFFFFF
    return crc

def crc_hw(data):
    crc = 0xFFFFFFFF
    n = len(data); i = 0
    while i + 4 <= n:
        w = data[i] | (data[i+1]<<8) | (data[i+2]<<16) | (data[i+3]<<24)
        crc = _stm_word(crc, w); i += 4
    rem = n - i
    if rem:
        w = 0
        for j in range(rem): w |= data[i+j] << (8*j)
        crc = _stm_word(crc, w)
    return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF

3) 对真实产物做"三端比对"。 不要只验向量,要拿最终产物分别用生产方算法、校验方算法、标准库各算一遍,确认谁和头里存的校验值一致。

4) 给硬件外设写单元测试。 已知输入 → 已知输出,CI 里常驻,防止后续误改。


避坑清单(Before You Ship)

  • 别假设硬件 CRC = 标准 CRC-32。 先读参考手册的 CRC 章节,确认 RefIn / RefOut / Poly / XorOut / 数据宽度。
  • 整条链路"谁生产、谁校验"必须算法统一。 最稳的做法:所有参与方都用标准 ZLIB(软件实现),放弃硬件加速,换取确定性。
  • 若坚持用硬件 CRC: 让生产方和校验方都用同一个硬件外设,并且 PC 打包工具也要复刻该硬件算法,绝不能用 binascii.crc32 当标准去对拍。
  • 把 6 个参数 + 字节序写进代码注释,别只写一句"CRC-32"。
  • 保留自测向量"123456789" → 0xCBF43926)。
  • 用 CRC 目录确认参数身份:CRC RevEng 或在线 CRC 计算器,输入你的 6 个参数,看它对应哪个标准名。

修复建议

链路现状:两端是标准 ZLIB,目标 MCU 硬件 CRC 是非反射,对不上。

  • 方案 ①(推荐):目标 MCU 改用软件 ZLIB。 放弃硬件 CRC,改用逐位反射实现,保持接口不变 → 调用方零改动,三方全部统一成 ZLIB。
  • 方案 ②(不推荐):全链路改非反射。 需同时改 PC 打包工具和中间 MCU,且 OTA 镜像 CRC 变成非标准,以后不能再直接用 binascii.crc32 离线验包。

硬件外设的多项式 / 反射不可配置,所以只能改软件侧。方案①改动最小、风险最低、且保留标准 ZLIB 的通用性。


本文在撰写过程中使用了 AI 辅助工具进行资料整理与文字润色,核心观点、技术分析与实践经验均来自笔者个人的知识积累与实操验证。


延伸阅读:

  • CRC RevEng 目录:https://reveng.sourceforge.io/
  • Python zlib 文档:binascii.crc32 / zlib.crc32
  • STM32 参考手册 CRC 章节(对比各系列差异)
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐