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」的完整流程:

升级包结构设计 - 核心逻辑 升级包 (ZIP) manifest.json (元数据) partition_table.json (分区表) firmware.bin (固件镜像) 验证签名 & 校验和 解析分区映射表 解压 & 写入 Flash Flash 分区布局 Bootloader (只读) App 主分区 (可升级) App 备份分区 (回滚用) 配置分区 (持久化) 0x08000000 0x08010000 0x08090000 0x08110000 写入

这张图你看懂了吗?升级包解压后,bootloader 先读 manifest.json,验证签名和校验和。通过后,再读 partition_table.json,确定每个固件片段该写到哪里。最后,把 firmware.bin 解压并写入对应的 Flash 分区。整个过程环环相扣,少一步都不行。

3.5 避坑指南

最后,我把自己踩过的坑总结一下,你写代码时绕着走:

  • 签名和校验和不要混用:签名是防篡改,校验和是防传输错误。两个都要,缺一不可。
  • 分区表里不要写绝对路径:我见过有人把分区表写成 /home/user/firmware/app.bin,结果 bootloader 在裸机上跑,哪来的文件系统?用相对文件名就行。
  • 升级包版本号要语义化:比如 2.1.0,主版本号、次版本号、修订号。别用 v2finallast_one 这种,你会后悔的。
  • 预留扩展字段:在 manifest.json 里加一个 "extras": {} 字段,以后加新功能不用改结构。

嗯,升级包结构设计这块,核心就是这些。你只要把格式、元数据、分区表这三样东西搞扎实了,后面的升级流程就是水到渠成的事。