4、升级包制作与管理:固件版本号规范、升级包签名校验、差分升级(增量包)原理、升级包存储策略

各位同行,今天我们来聊聊升级包的制作与管理。说实话,这块内容看着简单,但坑特别多。我见过不少项目,升级功能写好了,结果因为版本号乱标、升级包没签名,现场设备刷成了砖。嗯,咱们今天就把这些细节掰开揉碎了讲清楚。

4.1 固件版本号规范:别让版本号成为灾难

版本号这东西,说白了就是个标识。但你要是随便写,后期维护起来能让你崩溃。我个人习惯用三段式:主版本.次版本.修订号

  • 主版本:架构或协议有重大变更时加1。比如从Modbus RTU换成Modbus TCP,主版本必须变。
  • 次版本:新增功能或接口变化时加1。比如加了OTA功能,次版本升级。
  • 修订号:修复bug或小优化时加1。比如修复了某个内存泄漏,修订号递增。

我在项目中遇到过一件事:某厂商把版本号写成"V2024.03.15",看着像日期。结果同一天发布了两个补丁,版本号完全一样,现场根本分不清哪个是最新的。你想想看,这多可怕?

版本号规范建议:
格式:MAJOR.MINOR.PATCH-BUILD
示例:2.1.3-20240315
说明:主版本2,次版本1,修订号3,构建日期2024年3月15日

另外,我建议在固件头部固定位置写入版本号字符串。这样不管是上位机还是Bootloader,都能直接读取。代码里可以这样写:

// 固件版本信息结构体
typedef struct {
    uint8_t  major;      // 主版本
    uint8_t  minor;      // 次版本
    uint16_t patch;      // 修订号
    uint32_t build_date; // 构建日期(Unix时间戳)
    uint8_t  reserved[8];// 预留
} firmware_version_t;

// 放在固定地址,比如0x08000100
const firmware_version_t g_fw_version __attribute__((section(".version"))) = {
    .major = 2,
    .minor = 1,
    .patch = 3,
    .build_date = 1710489600, // 2024-03-15
};

4.2 升级包签名校验:防止被篡改的最后一道防线

升级包如果不做签名校验,就等于把设备的安全拱手让人。我曾经见过一个案例:黑客伪造了一个升级包,里面植入了挖矿程序,结果整个站点的PLC都被感染了。嗯,从那以后,我再也不敢省略签名这一步。

签名校验的核心逻辑很简单:

  1. 签名端:用私钥对固件哈希值加密,生成签名文件。
  2. 验证端:用公钥解密签名,比对固件哈希值是否一致。

实际项目中,我推荐使用ECDSA(椭圆曲线数字签名算法)。相比RSA,它的密钥更短,计算更快,特别适合资源受限的嵌入式设备。

我的经验:
公钥一定要烧死在芯片的OTP(一次性可编程)区域,或者放在Bootloader的只读段。千万别放在文件系统里,否则别人直接替换公钥,你的签名校验就形同虚设了。

签名校验的流程,我画了个图,你一看就明白:

升级包签名校验流程图 签名端(服务器) 固件文件 → SHA-256哈希 私钥加密哈希 → 生成签名 打包:固件 + 签名 + 公钥证书 传输 验证端(设备) 提取固件 → 计算SHA-256哈希 公钥解密签名 → 得到原始哈希 比对两个哈希值是否一致 一致 → 升级成功 | 不一致 → 拒绝升级

4.3 差分升级(增量包)原理:只传变化的部分

差分升级,说白了就是只传输固件中变化的部分。你想想看,一个完整的固件可能10MB,但每次升级可能只改了100KB的代码。如果每次都全量传输,带宽和流量都浪费了。

差分升级的原理,我简单说一下:

  • bsdiff算法:比较新旧两个固件,生成一个差分补丁文件。这个补丁里只包含新增、修改、删除的数据块。
  • 设备端还原:用旧固件 + 差分补丁,通过算法还原出新固件。

我在项目中遇到过一个问题:差分升级虽然省流量,但还原过程需要消耗大量内存和CPU。有个设备只有256KB RAM,结果还原到一半内存溢出了。后来我改用块级差分,把固件分成4KB一个块,只传输变化的块,这样内存占用就小多了。

避坑指南:
差分升级对固件版本有严格要求。如果设备当前版本和差分补丁的基准版本不一致,还原出来的固件就是错的。我曾经因为版本号搞混,导致一批设备升级后全部死机。所以,差分升级前一定要校验当前固件的版本号。

差分补丁的生成命令示例:

# 生成差分补丁(在服务器端执行)
bsdiff old_firmware.bin new_firmware.bin patch.bin

# 应用差分补丁(在设备端执行)
bspatch old_firmware.bin new_firmware.bin patch.bin

4.4 升级包存储策略:放哪里、怎么放、放多久

升级包下载到设备后,存哪里?这是个好问题。我见过三种主流策略:

存储策略 原理 优点 缺点
双备份区 固件分A区和B区,升级时写入备用区 升级失败可回滚,安全性高 需要双倍Flash空间
临时存储区 升级包先存到临时区,校验通过后再覆盖 节省空间,实现简单 升级过程中断电可能变砖
外部存储 升级包存到SD卡或U盘 容量大,可离线升级 速度慢,依赖外部硬件

我个人最推荐双备份区。虽然多占一倍Flash,但安全第一。你想想看,一个SCADA站点可能有几百台设备,如果升级失败导致全部离线,那损失可不是一块Flash芯片能比的。

双备份区的存储布局示例:

Flash地址空间:
0x08000000 - 0x0801FFFF: Bootloader(128KB)
0x08020000 - 0x0809FFFF: 固件A区(512KB)
0x080A0000 - 0x0811FFFF: 固件B区(512KB)
0x08120000 - 0x0813FFFF: 配置参数区(128KB)
0x08140000 - 0x0815FFFF: 升级包临时区(128KB)
我的习惯:
升级包下载完成后,先做完整性校验(CRC32或SHA-256),再做签名校验。两关都过了,才允许写入固件区。另外,升级包临时区在升级完成后要立即擦除,防止被恶意读取。

嗯,关于升级包制作与管理,今天就聊这么多。核心就三点:版本号要规范、签名校验不能省、存储策略要安全。你把这些做好了,远程升级这块基本就稳了。


专注资料整理