4. 固件镜像格式设计

在TRAVEO的固件升级体系中,镜像格式是升级流程能否可靠执行的基础。Bootloader需要在不依赖外部工具的情况下,仅凭镜像自身携带的信息完成合法性判断、版本比较、完整性校验和写入决策。因此,镜像格式设计的目标不是“把固件塞进一个文件”,而是让镜像成为一个自描述、可验证、可追溯的数据结构。

镜像的整体布局

一个典型的TRAVEO升级镜像由头部、元数据区、固件负载、签名区和尾部校验区组成。头部固定长度,位于镜像起始位置,Bootloader上电后首先读取该区域。头部之后是元数据区,用于承载版本描述、构建信息、目标硬件标识等可变长内容。固件负载是实际写入Flash的应用程序二进制。签名区存放对头部、元数据和负载的签名值。尾部校验区则提供对整个镜像的快速完整性校验。

TRAVEO 固件镜像布局与校验链 头部 Header(固定 256 字节) magic | header_ver | image_type | fw_version | load_addr | image_len | entry_point | crc32 | sig_offset | meta_offset 元数据区 Metadata(TLV 可变长) build_time | git_hash | target_hw_id | toolchain_ver | dependency_min_ver 固件负载 Firmware Payload 应用程序二进制,按 load_addr 映射写入 Flash 签名区 Signature(ECDSA P-256,64 字节) 对 Header + Metadata + Payload 的哈希值签名 尾部 CRC32(覆盖 Header + Metadata + Payload) 签名覆盖范围 CRC 覆盖范围

头部字段的设计要点

头部是Bootloader最先解析的区域,字段设计必须兼顾固定偏移和可扩展性。以下是一个可供参考的头部结构定义:

typedef struct {
    uint32_t magic;          // 0x5452564F "TRVO"
    uint16_t header_ver;     // 头部格式版本,当前为 0x0100
    uint16_t image_type;     // 0x0001=应用, 0x0002=Bootloader, 0x0003=标定数据
    uint32_t fw_version;     // 主.次.修订.构建,如 0x01020304
    uint32_t load_addr;      // 负载写入 Flash 的目标起始地址
    uint32_t image_len;      // 固件负载字节数
    uint32_t entry_point;    // 跳转入口地址
    uint32_t crc32;          // 头部+元数据+负载的 CRC32
    uint32_t sig_offset;     // 签名区相对镜像起始的偏移
    uint32_t meta_offset;    // 元数据区相对镜像起始的偏移
    uint32_t meta_len;       // 元数据区长度
    uint8_t  reserved[212];  // 保留至 256 字节
} image_header_t;

magic字段用于快速识别合法镜像,避免Bootloader对随机数据做无意义解析。header_ver允许后续在不破坏旧Bootloader的前提下扩展头部。fw_version采用32位编码,高8位为主版本、次8位为次版本、再8位为修订号、低8位为构建号,便于Bootloader用一次整数比较完成版本判断。load_addr和image_len共同界定写入范围,Bootloader在擦写前必须校验该范围是否落在应用分区内,防止越界写入。

版本号与回滚策略

版本号不仅用于显示,更用于升级决策。Bootloader通常维护一个当前运行版本寄存器或备份区。当收到新镜像时,比较fw_version与当前版本:若新版本更高,允许升级;若相同,可根据策略拒绝或允许重刷;若更低,则进入回滚判断。回滚策略需要额外字段支持,例如在元数据中增加min_allowed_version,表示允许降级的最低版本。若新镜像版本低于该值,Bootloader直接拒绝,防止恶意降级到存在已知漏洞的旧固件。

校验字段与签名区

CRC32用于快速完整性校验,计算范围覆盖头部、元数据和固件负载,但不包括签名区本身。Bootloader在接收完镜像后先做CRC校验,若失败则丢弃,不进入签名验证,以节省非对称运算时间。签名区采用ECDSA P-256,签名对象是头部、元数据和负载拼接后的SHA-256哈希值。公钥预先烧录在Bootloader的只读区域或OTP中,Bootloader使用该公钥验证签名。只有CRC和签名都通过,镜像才被允许写入。

CRC32用于快速排除传输错误,签名用于确认来源可信。两者职责不同,不可互相替代。建议先CRC后签名,避免对损坏镜像执行昂贵的椭圆曲线验签。

元数据区的TLV组织

元数据区采用TLV(Type-Length-Value)格式,便于按需扩展而不影响头部固定布局。每个条目由1字节类型、2字节长度和变长值组成。例如:

Type 0x01: build_time   "2025-03-21T14:30:00"
Type 0x02: git_hash     "a3f8c21e"
Type 0x03: target_hw_id 0x0001 (CYT4BF)
Type 0x04: toolchain    "GCC 10.3.1"
Type 0x05: min_ver     0x01020000

Bootloader不需要解析全部元数据,只提取target_hw_id和min_ver等关键字段做匹配判断。其余字段供上位机工具和售后追溯使用。这种设计让镜像格式在保持Bootloader简洁的同时,具备工程可追溯性。

镜像打包工具链

镜像打包通常由上位机工具完成,输入是编译链接生成的ELF或BIN文件,输出是带头部、元数据和签名的完整升级镜像。工具链的核心步骤包括:从ELF提取负载和入口地址;计算负载长度和CRC;生成头部和元数据;对头部、元数据和负载做SHA-256并调用私钥签名;将各段按布局拼接输出。以下是一个打包命令的示例:

trav_pack --input app.bin \
          --elf app.elf \
          --version 1.2.3.4 \
          --hw-id 0x0001 \
          --load-addr 0x08010000 \
          --key private_p256.pem \
          --output app_v1.2.3.4.img

打包完成后,建议用配套的解析工具对镜像做一次回读校验,确认头部字段、CRC和签名均可正确解析。这一步能在产线烧录前发现打包参数错误,避免批量返工。

私钥必须离线保管,不得放入代码仓库或CI明文变量。签名工具应在受控环境中运行,并对每次签名操作记录镜像哈希和版本号,形成可审计的发布记录。

设计检查清单

  • 头部长度固定,字段偏移在文档中明确,保留区足够后续扩展。
  • magic值唯一且不易与Flash空白或随机数据混淆。
  • load_addr与image_len的组合经过分区边界检查。
  • CRC32覆盖范围明确排除签名区,避免循环依赖。
  • 签名算法和曲线在Bootloader与打包工具中保持一致。
  • 元数据TLV解析具备未知类型跳过能力,不因新增字段导致旧Bootloader失败。
  • 版本比较逻辑包含相同版本和降级场景的明确策略。

镜像格式一旦发布并被Bootloader固化,后续修改成本很高。因此在设计阶段就应把版本兼容、回滚控制、签名验证和元数据扩展纳入考虑,使镜像格式能够支撑TRAVEO产品整个生命周期的固件治理需求。