4、固件包格式设计:固件头结构、校验和算法(CRC32/MD5)、版本号管理

好,咱们进入第四章。这一章我打算聊聊固件包格式设计。说实话,这是整个升级系统里最容易被忽视、但出事概率最高的环节。你想想看,固件包要是格式乱了,校验没过,版本号搞混了——轻则升级失败,重则设备变砖。我在项目里见过太多次了。

4.1 固件头结构——给固件办个身份证

固件头是什么?说白了,就是固件包的“身份证”。它放在固件二进制文件的最前面,告诉单片机:你是谁、多大、用什么算法校验、从哪开始才是真正的程序代码。

我个人习惯把固件头设计成固定长度,比如64字节或128字节。这样解析起来简单,不用猜。下面是我常用的一个结构体:

typedef struct {
    uint32_t magic;          // 魔数,比如 0xA5A5A5A5
    uint32_t header_crc;     // 固件头自身的CRC32
    uint32_t image_size;     // 固件体大小(不含头)
    uint32_t image_crc;      // 固件体的CRC32
    uint8_t  hw_version[4];  // 硬件版本号
    uint8_t  sw_version[4];  // 软件版本号
    uint8_t  reserved[40];   // 保留字段,对齐到64字节
} firmware_header_t;

这里有个细节——魔数。我建议选一个不常见的值,比如0xA5A5A5A5或者0xFEEDBEEF。为什么?因为Flash擦除后全是0xFF,如果魔数不对,直接判定无效,省得后面解析一堆垃圾数据。我在项目中遇到过客户把别的型号的固件包刷进来,魔数不对,程序直接拒绝,救了他们一命。

关键点:固件头一定要包含校验自身完整性的字段(header_crc)。否则固件头在传输过程中损坏了,你连“它坏了”这件事都检测不到。

4.2 校验和算法——CRC32还是MD5?

校验算法这块,我踩过坑,所以多说几句。

CRC32:硬件支持好,计算快,适合嵌入式环境。很多MCU内置CRC计算模块,几微秒就算完了。缺点是抗碰撞能力一般,但用于固件完整性校验完全够用。

MD5:128位摘要,碰撞概率极低。但计算量比CRC32大不少,在低端MCU上可能耗时几十毫秒甚至上百毫秒。不过,如果你做的是医疗仪器或者安全等级高的设备,MD5更稳妥。

我个人怎么选?看场景:

  • 普通工业仪表:CRC32就够了,简单高效
  • 医疗、金融、安全相关:用MD5,甚至可以考虑SHA256
  • OTA升级包:我建议双重校验——固件头用CRC32(快),固件体用MD5(稳)

嗯,这里要注意:千万别只用累加和(checksum)。我曾经接手过一个项目,原工程师用8位累加和做校验,结果固件里两个字节互换位置,累加和居然没变。设备升级后直接死机,排查了两天才发现是校验算法太弱。

实战技巧:计算CRC32时,记得用标准多项式0x04C11DB7。很多国产MCU的硬件CRC模块默认多项式不一样,要手动配置。我吃过这个亏,固件包在PC上算出来是对的,下载到设备里校验就不通过——折腾了一下午才发现是多项式没对上。

4.3 版本号管理——别让版本号成为灾难

版本号管理,看起来简单,做起来全是坑。我见过最离谱的:某设备固件版本号用字符串"V1.2.3",结果升级时比较版本号,字符串比较"V1.9.0"比"V1.10.0"大——因为'9' > '1'。你说冤不冤?

我推荐用四段式版本号,每个字段用一个字节存储:

typedef struct {
    uint8_t major;   // 主版本号,重大功能变更
    uint8_t minor;   // 次版本号,功能新增
    uint8_t patch;   // 修订号,Bug修复
    uint8_t build;   // 构建号,每次编译递增
} version_t;

这样比较版本号就很简单了:

int compare_version(version_t *a, version_t *b) {
    if (a->major != b->major) return a->major - b->major;
    if (a->minor != b->minor) return a->minor - b->minor;
    if (a->patch != b->patch) return a->patch - b->patch;
    return a->build - b->build;
}

关于版本号策略,我总结了几条铁律:

  • 向下兼容:主版本号不同,通常意味着不兼容,升级要谨慎
  • 强制升级 vs 可选升级:安全补丁必须强制,功能更新可以可选
  • 硬件版本绑定:固件头里一定要带硬件版本号,防止固件刷错硬件平台

血的教训:我曾经给一批设备做远程升级,固件包没检查硬件版本号。结果A型号的固件刷到了B型号上——屏幕驱动反了,按键映射全乱。最后只能派人去现场一台台刷回来。从那以后,我坚持在固件头里放硬件版本号,升级前必须比对。

4.4 完整的固件包布局

把上面这些串起来,一个完整的固件包长这样:

偏移地址 内容 大小 说明
0x0000 固件头 64字节 包含魔数、版本号、校验值等
0x0040 固件体 N字节 实际的二进制程序代码
0x0040+N 固件体MD5 16字节 对整个固件体的MD5摘要

升级流程大致是:

  1. 读取固件头,校验魔数
  2. 校验固件头自身的CRC32
  3. 比对硬件版本号,确认兼容
  4. 比对软件版本号,确认是否需要升级
  5. 读取固件体,计算CRC32或MD5,与固件头里的值比对
  6. 全部通过,开始写入Flash
  7. 写入完成后,再读出来校验一遍(双重保险)

你看,每一步都有校验,每一步都在防错。嵌入式系统最怕的就是“差不多”——差不多能跑、差不多能升级。但“差不多”的设备,迟早会出问题。

最后一个小建议:固件包里留一些保留字段。我一般留40字节左右。为什么?因为产品迭代过程中,你可能会想加签名、加加密密钥、加设备序列号——有保留字段,就不用改固件头结构,兼容性更好。这是经验之谈。