3. 升级包结构设计:升级包格式、元数据定义与分区映射表
好,咱们直接切入正题。升级包的结构设计,说白了就是你要把「新固件」打包成什么样子,才能让设备安全、可靠地吃下去。我见过太多团队,功能写得飞起,结果升级包结构一塌糊涂,最后设备变砖。嗯,这一节咱们就把这块硬骨头啃下来。
3.1 升级包格式:ZIP 还是 TAR?
我个人习惯用 ZIP。为什么?因为生态好。几乎所有的嵌入式系统,不管是 Linux 还是 RTOS,都能找到现成的 ZIP 解压库。TAR 当然也行,但 TAR 不带压缩,你得额外套一层 gzip,麻烦。
但这里有个坑——不要用 ZIP 的「存储」模式。我遇到过有人直接把固件扔进 ZIP 包,压缩率设为 0,美其名曰「省 CPU」。结果呢?包体巨大,传输慢,还容易出错。你想想看,嵌入式设备的 Flash 和 RAM 都金贵,能压一点是一点。
我建议的打包策略是这样的:
- 固件镜像:用 deflate 算法压缩,压缩率选中等(比如 level 6),兼顾速度和体积。
- 元数据文件:不压缩,直接存储。因为元数据要快速读取,压缩反而增加解析开销。
- 分区映射表:同样不压缩,但要用 JSON 或二进制格式,别用 XML,太啰嗦。
核心原则:升级包的结构要「一眼能看懂」。一个合格的升级包,解压后应该只有 3-5 个文件,每个文件职责清晰。别搞成俄罗斯套娃。
3.2 元数据定义:签名、校验和、硬件ID
元数据是升级包的「身份证」。没有它,设备根本不知道这个包能不能用、该不该用。我早期做项目时,就吃过没加硬件 ID 的亏——同一款芯片,不同批次的外设地址不一样,结果刷错了直接黑屏。
元数据文件我通常命名为 manifest.json,结构如下:
{
"version": "2.1.0",
"hardware_id": "STM32H743_RevB",
"checksum": {
"algorithm": "SHA256",
"value": "a1b2c3d4e5f6..."
},
"signature": {
"algorithm": "ECDSA_P256",
"value": "3045022100..."
},
"partition_map": "partition_table.json",
"firmware_files": [
{"name": "bootloader.bin", "offset": 0x08000000, "size": 65536},
{"name": "app.bin", "offset": 0x08010000, "size": 524288}
]
}
这里有几个关键点,我得跟你掰扯清楚:
3.2.1 校验和:别用 CRC32
CRC32 太弱了。我见过有人用 CRC32 做固件校验,结果传输过程中数据被篡改,CRC 竟然没检测出来。为什么?因为 CRC32 是线性校验,攻击者可以轻松构造碰撞。现在最低标准是 SHA256,你要是资源紧张,至少也得用 SHA1(虽然 SHA1 也快退役了)。
3.2.2 签名:别裸奔
签名是防篡改的最后一道防线。我建议用 ECDSA(椭圆曲线签名),密钥长度短,适合嵌入式。RSA 也行,但 2048 位的 RSA 签名在 Cortex-M4 上要算好几秒,用户体验很差。
我的经验:签名验证一定要在 bootloader 里做,不要在 app 里做。因为 app 可能已经被篡改了,你拿被篡改的代码去验证签名,那不是搞笑吗?
3.2.3 硬件 ID:精确到版本
硬件 ID 不能只写芯片型号,要写 PCB 版本。比如 STM32H743_RevB。我踩过的坑是:同一款芯片,RevA 和 RevB 的 GPIO 复用功能不一样,刷错版本直接烧外设。所以,硬件 ID 要精确到「这个固件是为哪块板子编译的」。
3.3 分区映射表:谁该住哪里
分区映射表是升级包的「地图」。它告诉 bootloader:每个固件片段该写到 Flash 的哪个位置。我见过最蠢的设计是把分区表硬编码在 bootloader 里,结果 Flash 布局一改,bootloader 也得跟着升级,死循环。
正确的做法是:分区映射表放在升级包里。这样 bootloader 只需要解析这个表,就能知道怎么写 Flash。结构如下:
{
"partitions": [
{
"name": "bootloader",
"type": "readonly",
"start_addr": 0x08000000,
"size": 0x10000,
"firmware_file": "bootloader.bin"
},
{
"name": "app_primary",
"type": "updatable",
"start_addr": 0x08010000,
"size": 0x80000,
"firmware_file": "app.bin"
},
{
"name": "app_backup",
"type": "backup",
"start_addr": 0x08090000,
"size": 0x80000,
"firmware_file": null
},
{
"name": "config",
"type": "persistent",
"start_addr": 0x08110000,
"size": 0x10000,
"firmware_file": null
}
]
}
这里要注意几个字段:
- type:标记分区属性。readonly 表示只读(比如 bootloader),updatable 表示可升级,backup 是备份区,persistent 是配置区(升级时不擦除)。
- firmware_file:对应升级包里的文件名。如果为 null,表示这个分区不参与本次升级。
- start_addr 和 size:必须和芯片的 Flash 布局严格对齐。我建议在编译时自动生成这个表,别手写,手写必出错。
警告:分区映射表里的地址,一定要做边界检查。我曾经遇到过:升级包里的分区表写错了 size,结果 bootloader 把 app 写到了 bootloader 的区域,直接覆盖了启动代码。从那以后,我在 bootloader 里加了一道「地址合法性校验」,任何超出 Flash 范围的写入请求,直接拒绝。
3.4 整体结构图
说了这么多,咱们用一张图把整个升级包的结构串起来。下面这张 SVG 图展示了升级包从「打包」到「解包」再到「写入 Flash」的完整流程:
这张图你看懂了吗?升级包解压后,bootloader 先读 manifest.json,验证签名和校验和。通过后,再读 partition_table.json,确定每个固件片段该写到哪里。最后,把 firmware.bin 解压并写入对应的 Flash 分区。整个过程环环相扣,少一步都不行。
3.5 避坑指南
最后,我把自己踩过的坑总结一下,你写代码时绕着走:
- 签名和校验和不要混用:签名是防篡改,校验和是防传输错误。两个都要,缺一不可。
- 分区表里不要写绝对路径:我见过有人把分区表写成
/home/user/firmware/app.bin,结果 bootloader 在裸机上跑,哪来的文件系统?用相对文件名就行。 - 升级包版本号要语义化:比如
2.1.0,主版本号、次版本号、修订号。别用v2、final、last_one这种,你会后悔的。 - 预留扩展字段:在 manifest.json 里加一个
"extras": {}字段,以后加新功能不用改结构。
嗯,升级包结构设计这块,核心就是这些。你只要把格式、元数据、分区表这三样东西搞扎实了,后面的升级流程就是水到渠成的事。