MCUboot裸机移植实战

MCUboot 裸机移植实战:Flash 分区、固件升级与回退

MCUboot 的双槽交换机制支持固件签名验证、测试升级和失败回退。裸机移植需要配合镜像格式规划 Flash 布局,并实现存储访问、启动交接及 App 升级接口。

示例固定为 MCUboot v2.2.0、单镜像、ECDSA P-256 椭圆曲线签名、Mbed TLS 密码学库和 Swap using offset 交换算法。配置、链接和签名步骤使用同一组分区参数。更换版本或算法后,需要重新核对依赖与接口。

代码采用通用硬件接口,不绑定芯片或开发板。Port_*、Download_*、App_* 由工程实现,代码中的每个非空行均附注释。示例用于说明接入方法,尚未在目标硬件上完成编译、烧录和断电验证。

目录

  1. 镜像、分区与升级状态
  2. 系统架构与下载方式
  3. [Flash 分区与 Offset 交换](#Flash 分区与 Offset 交换)
  4. [工程组织与 App 链接布局](#工程组织与 App 链接布局)
  5. [MCUboot 核心与移植接口](#MCUboot 核心与移植接口)
  6. [Bootloader 启动与交接](#Bootloader 启动与交接)
  7. [App 升级请求与提交](#App 升级请求与提交)
  8. 下载协议与会话状态
  9. 密钥生成与镜像签名
  10. 首次烧录与启动检查
  11. 测试升级、确认与断电恢复
  12. 排错与交付检查

1. 镜像、分区与升级状态

1.1 MCUboot 处理的对象

在覆盖式升级中,新固件写入运行区后,旧固件通常不再保留。交换升级会保留旧镜像,供新版本试运行失败并复位后回退。

MCUboot 使用"镜像"描述可验证的固件文件,使用"槽"描述存放镜像的 Flash 分区。镜像是内容,槽是存储位置,两者需要分别理解。

术语 含义 在本例中的作用
Bootloader 设备复位后执行的引导程序 调用 MCUboot 核心,再交接给 App
App 应用程序,负责设备业务 接收升级文件、运行自检并确认新版本
Primary Slot(主槽) 用于运行 App 的镜像分区 升级前运行旧镜像,交换后运行新镜像
Secondary Slot(候选槽) 接收新镜像的分区 交换后保存旧镜像,供试运行失败时回退
Image Header(镜像头) 记录镜像尺寸、版本、标志等信息的头部 帮助核心解析镜像,头部预留长度由工程确定
TLV(Type-Length-Value) 按"类型、长度、内容"保存的数据条目 保存摘要、签名等镜像元数据
Trailer(槽尾状态区) 位于槽末尾的运行时记录 保存升级请求、确认状态及交换恢复信息

单镜像表示升级一份 App;Primary 和 Secondary 是这份 App 的两个存储槽,不代表同时升级两个独立应用。本文也不单独划分 Scratch(交换临时分区),Offset 算法使用候选槽中的额外扇区完成搬运。

1.2 下载、安装与确认

一次测试升级包含以下阶段。表中的状态名称用于说明流程,不是要求下载程序直接写入同名 Flash 字段。

阶段 完成的操作 后续行为
下载 将完整签名镜像写入候选位置,并检查传输及写入结果 尚未申请安装
Pending(待升级) 调用公共 API 提交测试升级请求 复位后由 MCUboot 验证并安装
Test(试运行) 新镜像交换到主槽,旧镜像留在候选槽 新 App 自检,尚未确认时复位会触发回退
Confirmed(已确认) 新 App 自检通过并成功写入确认状态 后续启动保留新镜像
Revert(回退) 未确认的新 App 复位后,恢复旧镜像 重新启动旧版本

App 启动后,MCUboot 不会在后台监视业务。自检、确认和故障复位由 App 实现;若 App 卡死且始终不复位,Bootloader 就没有重新处理回退的机会。

2. 系统架构与下载方式

2.1 系统角色

固件制作、文件传输和设备启动分别由不同模块负责。传输完成只表示文件已接收,升级结果还需检查安装和 App 确认状态。

参与方 负责的工作 输出或完成条件
发布端 编译 App,使用私钥签名 可下载的签名镜像及版本记录
上位机 下发文件、处理重试、查询设备状态 分别报告传输、安装及 App 确认结果
Bootloader 初始化底层接口,调用 MCUboot,完成启动交接 验证通过后启动所选镜像,失败时进入恢复或停止路径
App 执行业务、接收升级文件或请求下载模式 新版本自检通过后提交确认

2.2 下载程序的位置

MCUboot 的 bootutil 核心库负责镜像处理。下载程序可放在 App,也可放在 Bootloader;MCUboot 仓库还提供其他恢复与传输组件,但不在本文接入范围内。

方式 处理流程 工程影响
App 接收 App 下载到候选槽,提交 Pending 后复位 App 需要下载协议和 Flash 写入接口;擦写是否影响业务取决于存储器及驱动
Bootloader 接收 App 保存进入下载模式的请求并复位,Bootloader 接收文件 Bootloader 需要通信与下载逻辑,接收期间 App 不运行

本例由 App 接收固件。文件完整写入后提交测试升级,复位进入 MCUboot;交换完成后由新 App 自检并确认。两种方式共用镜像格式与安装规则,第 7 节说明请求入口和提交时机。

3. Flash 分区与 Offset 交换

3.1 交换算法与运行地址

App 按主槽的运行位置链接后,候选槽中的文件不能仅靠更换分区名称就直接运行。Swap 会实际读取、擦除和写入 Flash,将新旧镜像交换位置;链接地址保持不变,回退同样需要搬运数据。

安装方式 空间与执行方式 选择时需要考虑的条件
Swap using scratch 使用独立临时分区交换双槽内容 临时区容量至少覆盖参与交换的最大扇区
Swap using offset 使用候选槽内额外扇区交换 候选镜像从第二扇区开始,需满足扇区布局约束
Overwrite 新镜像覆盖主槽,不保留可交换回去的旧镜像 不提供本文的双槽试运行回退
Direct-XIP 直接从所选槽执行镜像 镜像需要满足对应槽的执行地址要求,并另行配置回退策略

采用 Offset 交换时,App 始终按主槽位置构建。它省去独立 Scratch 分区,但仍需要额外的交换空间。

3.2 分区定义

区域 用途 约束
Bootloader 启动代码、MCUboot、验签公钥、移植层 不属于应用升级写入范围
Primary 当前运行镜像与末尾状态区 App 按这里的运行位置链接
Secondary 候选镜像、交换空间及末尾状态区 测试升级期间保存恢复所需的数据
App 自定义数据区(可选) 由 App 按需保存配置参数、运行记录等 独立于两个镜像槽,升级时保留其中的数据

App 自定义数据区由应用根据需要划分,MCUboot 不要求必须存在该区域,也不负责管理其中的数据格式。若新旧 App 共用这些数据,格式变更应兼容回退后的旧版本;下载会话如需持久化,也应单独约定存放位置。

分区表至少记录设备编号、设备内偏移、分区大小、擦除扇区分布、最小编程单位和擦除值。设备内偏移、分区内偏移和 CPU 地址应分别命名,接口中也应明确使用哪一种地址。

3.3 Offset 容量与对齐参数

Offset 算法使用 Secondary 的一个额外扇区参与交换,因此新候选镜像从 Secondary 的第二个扇区开始下载。下载器计算写入地址时需要计入这一偏移。

擦除扇区是 Flash 可独立擦除的区域;最小编程对齐表示写入偏移和长度需要满足的字节粒度,两者并不相同。容量计算采用以下示例参数,实际工程应使用存储器的规格值:

参数 示例值 含义
擦除扇区大小 E 4096 字节 两个槽都由相同大小的扇区组成
最小编程对齐 A 8 字节 写入偏移、长度按 8 字节满足底层约束
Primary 大小 P 262144 字节,64 个扇区 包含镜像与尾部保留区域
Secondary 大小 S 266240 字节,65 个扇区 比 Primary 多一个扇区
候选镜像起始偏移 4096 字节 相对于 Secondary 起点
镜像头预留 H 512 字节 与 App 链接和 imgtool 参数一致
最大扇区数 65 覆盖两个槽中扇区数较多的那个

Trailer 中的 magic 是用于识别状态是否有效的固定字节序列,image_ok 是镜像确认标志;交换进度由专用状态记录保存。它们与镜像签名 TLV 分别管理,下载范围不得覆盖槽尾状态区。尾部保留空间应按所选版本、算法和对齐计算,并按擦除扇区向上取整。

按这组参数,每个槽尾部预留一个 4096 字节扇区:Primary 中完整签名镜像不超过 0x3F000 字节;Secondary 从偏移 0x1000 写起,到末尾保留扇区之前结束。如果增加功能或调整参数导致 Trailer 超出一个扇区,必须相应扩大保留区、缩小镜像上限。其他布局应重新计算镜像容量上限。

Offset 允许两槽等大,但会影响可用容量;本例让 Secondary 比 Primary 多一个扇区。本版本 Offset 要求相容的扇区布局,文档要求各槽扇区大小一致。外部存储也必须满足这些约束,不能只比较总容量。算法与空间要求见 v2.2.0 设计文档。

4. 工程组织与 App 链接布局

4.1 构建目标与共享配置

两个目标各自拥有启动文件、入口函数、链接布局和输出文件。它们可以放在同一仓库,也可以共用驱动源码,并分别配置各自的链接地址。

目标 必须包含 输出
Bootloader 启动代码、MCUboot 核心、Flash 后端、密码学、公钥、启动交接 可在设备复位后执行的 Bootloader
App 业务代码、公共升级 API 所需实现、同一分区定义与 Flash 后端 不带 MCUboot 镜像头的原始 app.bin,随后签名

工程可按下表组织,第三方源码保留原仓库结构:

工程路径 放什么
third_party/mcuboot/ 固定版本的原始仓库,保留许可证
port/include/mcuboot_config/ mcuboot_config.h、mcuboot_logging.h、mcuboot_assert.h
port/include/flash_map_backend/ flash_map_backend.h
port/include/sysflash/ sysflash.h,定义镜像槽与分区 ID
port/include/ port_api.h、port_mbedtls_config.h、分区参数头文件
port/src/ Flash 适配、日志、看门狗、复位、启动交接实现
boot/ Bootloader 入口、keys.c、生成的 public_key.c
app/ App 入口、下载状态机和自检确认逻辑

两个目标能够独立构建后,可在专用测试区域验证 Flash 后端,再接入镜像验证和交换。分阶段联调便于判断错误来自存储驱动、镜像处理还是启动交接。

Bootloader 和 App 应共享分区参数及升级配置。镜像槽编号、写入对齐、交换算法和 Trailer 布局需要一致,否则 App 写出的状态可能无法被 Bootloader 正确解释。

4.2 镜像布局与链接位置

签名镜像与 Flash 槽的布局如下。签名镜像是传输文件,Trailer 是设备端维护的槽尾状态;本例不把 Trailer 填充进下载文件。

部分 内容与位置
镜像头预留区域 位于文件开头,本例预留 512 字节;v2.2.0 的基础头结构为 32 字节,512 并非固定格式要求
App 正文 编译链接得到的程序内容,紧接头部预留区域
TLV 区域 保存摘要、签名等条目;启用受保护 TLV 后,相应条目也参与摘要计算
槽尾 Trailer 位于 Flash 槽末尾,由设备更新升级状态,与上述文件内容之间保留容量边界

对本文固定位置运行的 App,可把布局约定写成:App 正文存放起点 = Primary 镜像起点 + 镜像头预留 H。具体入口是否位于正文起点,以及如何找到入口,由目标的可执行格式和启动约定决定,不能从这个等式推导成一次通用函数跳转。

本例的原始 app.bin 不包含预留镜像头,签名时使用 --pad-header 前置 512 字节;App 链接时必须已经按添加头部后的运行位置布局。签名工具不会自动重定位已经链接好的绝对地址。

原始二进制若已包含预留头部,应省略 --pad-header,避免重复插入。链接脚本、二进制导出和签名步骤需要遵循同一约定。

候选文件虽然先写到 Secondary,仍按最终 Primary 的运行布局构建。交换只移动字节,不会修改 App 内部的链接地址。

5. MCUboot 核心与移植接口

MCUboot 的通用核心通过配置头文件和 Flash 后端连接到工程。接入工作包括选择核心源文件、配置密码学依赖、注册公钥,以及实现分区访问和运行环境接口;以下分别说明文件位置、配置取值和接口要求。

5.1 固定源码和依赖

本文使用 MCUboot 提交 2d61c318933819a0f4954fb2a5a957a62c6128ce,即 v2.2.0。在工程目录执行以下命令获取源码和依赖。

sh 复制代码
git clone --branch v2.2.0 --depth 1 https://github.com/mcu-tools/mcuboot.git third_party/mcuboot # 获取固定版本,避免跟随主分支变化。
git -C third_party/mcuboot submodule update --init ext/mbedtls # 只获取本教程所需的密码学依赖,并使用仓库记录的提交。
git -C third_party/mcuboot rev-parse HEAD # 记录实际源码提交,用于后续复现和排错。

该版本记录的 Mbed TLS 子模块提交为 8df2f8e7b9c7bb9390ac74bb7bace27edca81a2b,其头文件版本为 3.0.0。下文密码学文件清单以它为准。其他 Mbed TLS 版本的依赖关系可能不同,精简文件清单需要随版本调整;产品还应持续跟踪所用依赖的安全更新。

5.2 MCUboot 源文件选择

保留整个 boot/bootutil/include/ 和 boot/bootutil/src/ 目录,按下表选择参与构建的 C 文件。硬件差异由移植层适配,核心状态机保持原实现。

下表路径都相对于 third_party/mcuboot/boot/bootutil/src/:

文件 本文是否编译 为什么
loader.c 是 实现 boot_go()、镜像槽管理和启动决策
image_validate.c 是 检查镜像格式、哈希和签名等验证条件
image_ecdsa.c 是 对接 ECDSA 验签
tlv.c 是 遍历和读取镜像 TLV
bootutil_misc.c 是 镜像状态、Trailer 读写等公共内部工具
bootutil_public.c 是 提供 pending、confirmed 等公共控制 API
swap_misc.c 是 交换流程共用的辅助逻辑
swap_offset.c 是 Offset 交换与中断恢复实现,不能漏掉
caps.c 是 返回本次构建的能力信息
fault_injection_hardening.c 是 提供故障注入加固(FIH)调用框架,启动代码见第 6 节
image_rsa.c、image_ed25519.c 否 本文没有选择这些签名类型
swap_move.c、swap_scratch.c 否 本文只选择 Offset 交换
ram_load.c 否 本文不采用 RAM-load 模式
encrypted.c、encrypted_psa.c 否 本文不启用镜像加密
boot_record.c 否 本文不启用测量启动或启动信息共享
fault_injection_hardening_delay_rng_mbedtls.c 否 本文 FIH 设为 OFF,不需要 HIGH 档的随机延迟实现

bootutil_priv.h、bootutil_misc.h、swap_priv.h 等是内部头文件,应保留供源码包含,不能当作 C 文件编译。

v2.2.0 的 boot/bootutil/CMakeLists.txt 通用清单没有列入 swap_offset.c。 因此选择 Offset 时,必须在自己的构建清单中显式加入它;只打开模式宏仍会留下缺失实现。可对照 该版本通用清单 和 模拟器构建清单。

5.3 头文件搜索路径

搜索路径 用途
third_party/mcuboot/boot/bootutil/include 找到 bootutil/bootutil.h 等公开头文件
third_party/mcuboot/boot/bootutil/src 找到内部头文件;一般只给核心库使用
port/include 找到本文定义的配置、分区和移植头文件
third_party/mcuboot/ext/mbedtls/include 找到密码学公开接口

搜索路径应指向 port/include,因为源码使用的是 #include <mcuboot_config/mcuboot_config.h> 这样的完整相对路径。

搜索路径中若同时存在多份同名配置,编译器可能选中其他示例工程的文件。接入时可查看头文件包含跟踪,确认 MCUboot 和 Mbed TLS 都使用当前工程的配置。

5.4 MCUboot 配置示例

以下放入 port/include/mcuboot_config/mcuboot_config.h。其中的数值与第 3 节存储参数对应,更换分区后需同步修改。

c 复制代码
#ifndef PORT_MCUBOOT_CONFIG_H /* 避免同一翻译单元重复包含配置。 */
#define PORT_MCUBOOT_CONFIG_H /* 建立本配置文件的包含保护。 */
#include "port_api.h" /* 声明 Port_WatchdogFeed 等工程适配函数。 */
#define MCUBOOT_IMAGE_NUMBER 1 /* 只管理一组 Primary 与 Secondary 镜像槽。 */
#define MCUBOOT_SIGN_EC256 /* 选择 ECDSA P-256 签名,镜像必须用匹配私钥签名。 */
#define MCUBOOT_USE_MBED_TLS /* 使用 Mbed TLS 实现哈希和 ECDSA 验证。 */
#define MCUBOOT_SWAP_USING_OFFSET /* 选择 Offset 交换,候选镜像从 Secondary 第二扇区写入。 */
#define MCUBOOT_VALIDATE_PRIMARY_SLOT /* 正常启动时也验证 Primary 镜像。 */
#define MCUBOOT_USE_FLASH_AREA_GET_SECTORS /* 使用 flash_area_get_sectors 查询分区扇区表。 */
#define MCUBOOT_MAX_IMG_SECTORS 65 /* 示例最大槽有 65 个扇区,状态和扇区数组必须覆盖它。 */
#define MCUBOOT_BOOT_MAX_ALIGN 8 /* 示例最大编程对齐为 8 字节,影响 Trailer 格式。 */
#define MCUBOOT_HAVE_LOGGING /* 启用日志,并要求提供 mcuboot_logging.h。 */
#define MCUBOOT_HAVE_ASSERT_H /* 使用工程提供的 mcuboot_assert.h,控制断言失败路径。 */
#define MCUBOOT_FIH_PROFILE_OFF /* 教学基线暂不启用故障注入加固,不能据此宣称具备抗攻击能力。 */
#define MCUBOOT_WATCHDOG_FEED() Port_WatchdogFeed() /* 把核心的喂狗位置连接到工程实现。 */
#define MCUBOOT_CPU_IDLE() Port_CpuIdle() /* 把核心的空闲钩子连接到工程实现。 */
#endif /* 结束 MCUboot 配置文件的包含保护。 */

这些宏有两种含义。MCUBOOT_IMAGE_NUMBER、MCUBOOT_MAX_IMG_SECTORS 等是数值;MCUBOOT_HAVE_LOGGING、MCUBOOT_SIGN_EC256 等通常按"是否定义"判断。关闭存在性开关应删除定义或使用 #undef,不要写成 #define 宏名 0。 对 #ifdef 而言,值为 0 依旧属于已定义。

5.5 配置项含义与取值依据

配置 当前取值的作用 调整时的影响
MCUBOOT_IMAGE_NUMBER=1 一份 App 的双槽升级 多镜像需要分别定义各镜像槽,还要处理依赖与一致升级,不能只把 1 改成 2
MCUBOOT_SIGN_EC256 使用 P-256 ECDSA 验证镜像真实性 改算法必须同时更换验签源文件、密码学模块、公钥和签名参数;不配置签名不能等同于安全启动
MCUBOOT_USE_MBED_TLS 明确选择当前密码学后端 不能同时开启冲突的后端;更换后端需要重新配依赖与接口
MCUBOOT_SWAP_USING_OFFSET 实际搬运双槽内容,保留旧镜像以支持回退 改成其他安装模式必须重做分区、候选偏移和测试,不是简单开关
MCUBOOT_VALIDATE_PRIMARY_SLOT 每次正常启动都检查 Primary 的有效性 不选择可减少启动校验开销,但不能假定正常启动路径仍执行同等验证
MCUBOOT_USE_FLASH_AREA_GET_SECTORS 使用返回 flash_sector 的扇区查询接口 不选时可能走旧版接口路径;新移植明确实现并选用这条路径更容易核对
MCUBOOT_MAX_IMG_SECTORS=65 足以容纳示例 Secondary 的 65 个扇区 太小会导致扇区枚举/交换失败;盲目增大会增加状态区及内存需求,签名容量检查也需同步
MCUBOOT_BOOT_MAX_ALIGN=8 设置 Trailer 使用的最大对齐 必须覆盖实际要求;本版本 Swap 支持范围受源码约束,常用取值为 8、16、32,不应任填扇区大小
MCUBOOT_HAVE_LOGGING 输出验证与交换原因,便于首次联调 不选择时核心日志被屏蔽;选择后必须实现全部日志宏
MCUBOOT_HAVE_ASSERT_H 让核心包含工程的 mcuboot_config/mcuboot_assert.h 不定义时才使用标准 assert.h;不能根据宏名误判,不应依靠被关闭的断言替代输入检查
MCUBOOT_FIH_PROFILE_OFF 关闭额外的故障注入加固,保留统一调用接口 LOW/MEDIUM/HIGH 增加不同防护;HIGH 还需随机延迟来源,选用后需复核编译器行为与安全目标
MCUBOOT_WATCHDOG_FEED() 在核心预设位置服务看门狗 不能覆盖所有长耗时驱动操作;擦除与验签最长耗时仍要测量
MCUBOOT_CPU_IDLE() 给等待/空闲路径一个工程钩子 可以实现为空操作,但不能未经分析就让处理器永久休眠或关闭恢复所需外设

5.6 未启用功能及适用条件

下表列出与本示例相关的替代模式和扩展功能。PSA Crypto 是统一的密码学接口规范;表中的 PSA 路径使用这套接口,而本例直接使用 Mbed TLS 软件接口。其他配置项的依赖可通过对应版本的源码条件分支核对。

宏或功能组 本文状态 开启后的含义与本文不选原因
MCUBOOT_SWAP_USING_MOVE 不定义 改用 Move 算法,额外空间和搬运步骤不同
MCUBOOT_SWAP_USING_SCRATCH 不定义 改用独立 Scratch 交换,需要额外分区与相应后端
MCUBOOT_OVERWRITE_ONLY 不定义 用新内容覆盖旧内容,不能按本文的双镜像交换方式回退
MCUBOOT_OVERWRITE_ONLY_FAST 不定义 Overwrite 的优化选项,不是 Offset 的加速开关
MCUBOOT_DIRECT_XIP、MCUBOOT_DIRECT_XIP_REVERT 不定义 从槽直接执行及其回退策略,要求另外的运行地址与存储条件
MCUBOOT_RAM_LOAD 不定义 把镜像装入 RAM 运行,内存布局和启动交接不同
MCUBOOT_SIGN_RSA、MCUBOOT_SIGN_RSA_LEN 不定义 选择 RSA 及密钥长度时使用;本文公钥和签名都不是 RSA
MCUBOOT_SIGN_ED25519、MCUBOOT_SIGN_EC384 不定义 其他签名算法;本版本 EC384 还要求对应 PSA 路径
MCUBOOT_USE_TINYCRYPT、MCUBOOT_USE_PSA_CRYPTO 不定义 替代密码学后端,不能与本文后端混用
MCUBOOT_HW_KEY 不定义 将可信公钥哈希等信任依据交由硬件/平台读取接口提供
MCUBOOT_BUILTIN_KEY 不定义 使用密码学后端中的内建密钥标识与接口;不等同于普通 keys.c 公钥数组
MCUBOOT_ENCRYPT_RSA/KW/EC256/X25519 均不定义 这些是镜像解密方案,不是签名算法选择;涉及解密密钥和额外源文件
MCUBOOT_ENC_IMAGES 不手工定义 属于加密路径的配置关系;只写它不能构成完整加密方案
MCUBOOT_HW_ROLLBACK_PROT 不定义 启用基于可信安全计数器的防降级,需要实现持久计数器接口
MCUBOOT_DOWNGRADE_PREVENTION 不定义 是特定模式下的软件版本降级限制,不能当作本文 Swap 的通用硬件防回滚替代品
MCUBOOT_MEASURED_BOOT、MCUBOOT_DATA_SHARING 不定义 增加测量记录或启动数据共享,需要对应输出区域与接口
FIH LOW/MEDIUM/HIGH 均不定义 本文已明确 OFF,不能同时定义多个档位

测试升级允许在新版本运行失败后回退旧镜像;防降级策略则用于限制低版本镜像的安装。本文配置支持测试升级回退,未接入可信计数器,因此不提供防降级保护,镜像版本号仅用于版本标识。

5.7 Mbed TLS 源文件与内存配置

密码学后端是核心调用的算法实现库。Mbed TLS 负责 SHA-256 摘要计算和 ECDSA P-256 验签;SHA-256 将数据转换为固定长度摘要,ECDSA 则利用公私钥关系验证签名。签名验证不对 App 正文加密。

MCUBOOT_USE_MBED_TLS 只选择 MCUboot 调用哪种后端,不会自动把 Mbed TLS 源文件和内存分配器加入工程。

针对前述固定依赖,将下列文件加入密码学静态库,路径相对于 ext/mbedtls/library/。其中 ASN.1 是公钥、签名等结构化数据使用的表示规则;本例需要相应的解析与编码模块。

文件 用途
sha256.c 镜像 SHA-256 摘要
asn1parse.c、asn1write.c ASN.1 数据处理;该版本 ECDSA 配置检查要求解析与写入模块
bignum.c 椭圆曲线计算的大整数基础
ecp.c、ecp_curves.c 椭圆曲线运算与 P-256 曲线参数
ecdsa.c ECDSA 签名格式处理与验签
platform.c、platform_util.c 内存函数转接、清零等公共工具
memory_buffer_alloc.c 从预分配缓冲区提供动态分配服务

保持这些文件在原仓库目录中,内部头文件也保留。验签使用专用静态内存池,在没有通用堆的裸机环境中提供内存分配。

在 port/include/port_mbedtls_config.h 中定义:

c 复制代码
#ifndef PORT_MBEDTLS_CONFIG_H /* 防止重复处理密码学配置。 */
#define PORT_MBEDTLS_CONFIG_H /* 建立密码学配置包含保护。 */
#define MBEDTLS_PLATFORM_C /* 编译平台抽象层,提供内存函数转接。 */
#define MBEDTLS_PLATFORM_MEMORY /* 允许安装自定义 calloc/free 实现。 */
#define MBEDTLS_PLATFORM_NO_STD_FUNCTIONS /* 不默认依赖系统堆等标准平台函数。 */
#define MBEDTLS_PLATFORM_EXIT_ALT /* 把内存分配器检测到内部损坏时的退出行为交给工程。 */
#define MBEDTLS_MEMORY_BUFFER_ALLOC_C /* 使用预分配缓冲区作为密码学内存池。 */
#define MBEDTLS_BIGNUM_C /* 开启 ECDSA 所需的大整数运算。 */
#define MBEDTLS_ECP_C /* 开启椭圆曲线基础运算。 */
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED /* 仅启用本文签名所用的 P-256 曲线。 */
#define MBEDTLS_ECP_NIST_OPTIM /* 启用此类曲线的模约减优化。 */
#define MBEDTLS_ECDSA_C /* 开启 ECDSA 实现,供 MCUboot 验证签名。 */
#define MBEDTLS_ASN1_PARSE_C /* 解析公钥和签名中的 ASN.1 数据。 */
#define MBEDTLS_ASN1_WRITE_C /* 满足此版本 ECDSA 模块的配置依赖。 */
#define MBEDTLS_SHA256_C /* 计算 MCUboot 镜像所需的 SHA-256 摘要。 */
#define MBEDTLS_SHA224_C /* 此固定版本要求与 SHA256 模块成对配置,并不改变镜像使用 SHA-256。 */
#define MBEDTLS_ECP_MAX_BITS 256 /* 将椭圆曲线运算规模限制到本文所需的 256 位。 */
#define MBEDTLS_MPI_MAX_SIZE 32 /* 将大整数输入规模按 256 位即 32 字节配置。 */
#endif /* 结束密码学配置包含保护。 */

对 MCUboot 和 Mbed TLS 的所有相关源文件(各自经预处理后形成编译器的翻译单元)统一设置编译定义 MBEDTLS_CONFIG_FILE="port_mbedtls_config.h",并统一头文件搜索路径。这里引号必须实际传给编译器;不同构建系统的转义方式不同。该版本 mbedtls/build_info.h 会引入配置检查;出现报错时,应按检查结果补齐或调整配置依赖。

未选择的密码学功能 原因
TLS、X.509、网络模块 此路径验证的是固件签名,不建立 TLS 连接,也不解析证书链
RSA、其他曲线、ECDH 与本文 P-256 固件验签路径无关
PSA 和硬件加速替代实现 本例使用 Mbed TLS 软件接口;切换后需重做构建依赖
镜像解密所需 AES 等模块 本文只签名,不加密固件正文
熵源与随机数生成模块 本文普通验签路径不生成密钥或签名,且未启用 FIH 随机延迟;引入这些用途时必须补上真实随机源
MBEDTLS_HAVE_ASM 不预设处理器和编译器,先使用通用实现

在第一次验签之前初始化内存池,且验签期间不能重置或复用这个缓冲区:

c 复制代码
#include <stddef.h> /* 提供 max_align_t,确保缓冲区具备通用基础类型对齐。 */
#include <mbedtls/memory_buffer_alloc.h> /* 声明 Mbed TLS 静态内存池初始化函数。 */
#include <mbedtls/platform.h> /* 声明密码学失败退出回调的注册函数。 */
#include "port_api.h" /* 声明不得返回的 Port_CryptoAbort(int status) 工程函数。 */
#define PORT_CRYPTO_HEAP_BYTES (32U * 1024U) /* 示例先预留 32 KiB,最终应根据目标运行峰值调整。 */
_Alignas(max_align_t) static unsigned char crypto_heap[PORT_CRYPTO_HEAP_BYTES]; /* 分配独立、对齐的验签工作区。 */
void Port_CryptoInit(void) /* 工程函数:在启动验证前调用一次。 */
{ /* 开始初始化密码学内存服务。 */
    (void)mbedtls_platform_set_exit(Port_CryptoAbort); /* 固定版本此注册函数返回 0,设置内部损坏时的停止处理。 */
    mbedtls_memory_buffer_alloc_init(crypto_heap, sizeof(crypto_heap)); /* 将此缓冲区注册为密码学分配器。 */
} /* 结束初始化;缓冲区因静态存储期会一直有效。 */

示例为内存池预留 32 KiB,实际容量需根据验签峰值、Bootloader 其他内存和栈占用确定。这个数值尚未经目标硬件测量;分配不足时,应报告验证失败并停止本次启动。Port_CryptoAbort(int status) 用于分配器检测到内部损坏等致命情况,应记录状态并停止,不得返回;普通分配不足通过正常错误路径处理。这里使用 C11 对齐语法,编译器应支持 C11 或提供等价写法。

MBEDTLS_PLATFORM_NO_STD_FUNCTIONS 不意味着完全不需要 C 运行库。memcpy、memset、整数类型等基础支持仍要提供;这里明确接管的是内存分配和致命退出路径。没有启用密码学内部打印调试、自测、文件系统或网络功能,后续增加随机数相关功能时,应提供符合要求的真实随机源。

5.8 接入可信公钥

公钥来自第 9 节的 imgtool getpub,输出到 boot/public_key.c,将生成文件加入 Bootloader。私钥只放在签名端。

另建 boot/keys.c,把生成的公钥注册给 MCUboot:

c 复制代码
#include <bootutil/sign_key.h> /* 提供 bootutil_key 结构以及 MCUboot 期望的符号声明。 */
extern const unsigned char ecdsa_pub_key[]; /* 引用 imgtool 生成的 ECDSA 公钥字节数组。 */
extern const unsigned int ecdsa_pub_key_len; /* 引用 imgtool 生成的公钥长度常量。 */
const struct bootutil_key bootutil_keys[] = /* 定义 MCUboot 使用的可信公钥表。 */
{ /* 开始公钥表初始化。 */
    { .key = ecdsa_pub_key, .len = &ecdsa_pub_key_len }, /* 注册一把公钥及其长度地址。 */
}; /* 结束公钥表。 */
const int bootutil_key_cnt = 1; /* 告诉验证逻辑公钥表中只有一项。 */

生成文件的符号应以实际输出为准;工程中应只保留这一份公钥注册实现,避免与仓库测试 keys.c 冲突。产品使用独立生成的密钥,测试密钥仅用于开发验证。

5.9 分区 ID 与镜像槽映射

本例采用分区 ID 1、2;它们是软件编号,不是 Flash 地址。在 port/include/sysflash/sysflash.h 中定义映射:

c 复制代码
#ifndef PORT_SYSFLASH_H /* 避免重复定义镜像槽映射。 */
#define PORT_SYSFLASH_H /* 建立映射头文件的包含保护。 */
#define FLASH_AREA_IMAGE_PRIMARY(image) (((image) == 0) ? 1 : 255) /* 镜像 0 的主槽 ID 为 1,其他索引返回无效 ID。 */
#define FLASH_AREA_IMAGE_SECONDARY(image) (((image) == 0) ? 2 : 255) /* 镜像 0 的备槽 ID 为 2,其他索引返回无效 ID。 */
#endif /* 结束映射定义;Offset 模式不需要 Scratch 分区。 */

本文固定单镜像,合法镜像索引只有 0。flash_area_id_from_multi_image_slot(0, 0) 应得到 1,flash_area_id_from_multi_image_slot(0, 1) 应得到 2;反向映射也必须一致,非法索引返回错误。接口调用时应区分槽索引 0/1 和分区 ID 1/2。

5.10 Flash 后端类型与声明

建立 port/include/flash_map_backend/flash_map_backend.h,包括标准整数类型、struct flash_area、struct flash_sector、属性访问器和下一小节的函数声明。可以参考 v2.2.0 模拟器接口形状,实际读写仍需对接目标存储驱动。

下面是数据结构示例;自定义实现可调整内部布局,只要核心使用的访问接口一致。属性访问器可写成 static inline,结构成员的含义见逐行注释:

c 复制代码
#include <stdint.h> /* 提供固定宽度整数类型,避免地址和长度类型随编译器变化。 */
struct flash_area /* 描述一个逻辑 Flash 分区。 */
{ /* 开始分区描述。 */
    uint8_t fa_id; /* 分区 ID,对应上述映射中的 1 或 2。 */
    uint8_t fa_device_id; /* 存储设备 ID,用于选择实际驱动。 */
    uint32_t fa_off; /* 分区起点相对于该存储设备起点的字节偏移。 */
    uint32_t fa_size; /* 分区总字节数,包含算法保留空间和 Trailer。 */
}; /* 结束分区描述。 */
struct flash_sector /* 描述分区内部的一个真实擦除扇区。 */
{ /* 开始扇区描述。 */
    uint32_t fs_off; /* 扇区起点相对于分区起点的字节偏移。 */
    uint32_t fs_size; /* 该扇区的真实擦除大小。 */
}; /* 结束扇区描述。 */

需要提供的属性访问器:flash_area_get_id() 返回 fa_id,flash_area_get_device_id() 返回 fa_device_id,flash_area_get_off() 返回 fa_off,flash_area_get_size() 返回 fa_size;flash_sector_get_off()、flash_sector_get_size() 分别返回 fs_off、fs_size。访问器不进行 CPU 地址换算。

5.11 Flash 接口实现要求

接口 输入/输出约定 实现要点
flash_area_open(id, &area) 按 ID 返回分区描述;成功 0 返回有效生命周期的描述指针,不能返回栈上对象;非法 ID 失败
flash_area_close(area) 释放分区使用关系 无资源需要释放时可为空操作,不应擦除任何内容
flash_area_read(area, off, dst, len) off 相对分区;成功 0 允许核心读取头部和单字节状态,不能强迫所有读请求满足写入对齐
flash_area_write(area, off, src, len) off 相对分区;成功 0 检查边界和编程对齐,等待写入完成,再向上层报告成功
flash_area_erase(area, off, len) 擦除请求范围;成功 0 按真实扇区执行;擦除范围必须保持在请求所属分区内
flash_area_align(area) 返回最小写对齐字节数 本文示例为 8,不是擦除大小 4096;还要核对驱动额外的编程约束
flash_area_erased_val(area) 返回擦除后的单字节值 常见为 0xFF,必须以实际设备为准
flash_area_get_sectors(id, &count, sectors) 输入 count 为容量,输出为实际数量 按偏移递增枚举分区所有扇区;容量不足应失败,不能越界写数组
flash_area_get_sector(area, off, &sector) 找到包含分区内偏移 off 的扇区 返回分区内扇区起点与大小,Offset 相关路径会使用它
flash_area_id_from_image_slot(slot) 当前单镜像槽到分区 ID 槽 0 返回 1,槽 1 返回 2,非法槽失败
flash_area_id_from_multi_image_slot(image, slot) 镜像索引与槽到 ID 本文只接受 image=0,再按槽映射
flash_area_id_to_multi_image_slot(image, id) 分区 ID 反查槽 本文 ID 1 返回槽 0,ID 2 返回槽 1;未知值失败
flash_device_base(device, &base) 对可内存映射设备返回基址 用于需要 CPU 地址的交接等路径;不可映射设备不能伪造可执行地址

接口原型以固定版本头文件为准,返回值必须反映底层操作结果。若驱动在擦写尚未完成或已经失败时返回成功,核心会继续更新交换进度,导致恢复状态与 Flash 内容不一致。

Flash 接口接收的是分区内偏移。地址计算使用以下约定:

名称 含义 本例的计算方式
off 请求位置相对于分区起点的偏移 由调用者传入
fa_off 分区起点相对于存储设备起点的偏移 来自分区表
设备内访问偏移 驱动访问设备中的位置 fa_off + off
CPU 映射地址 处理器直接访问内存映射存储时使用的地址 设备映射基址加设备内访问偏移

不可内存映射的设备通过驱动命令访问,无需构造 CPU 地址。读写擦除都要检查边界,计算时也要避免无符号加法溢出:

c 复制代码
#include <stdbool.h> /* 提供 bool、true 和 false。 */
#include <stdint.h> /* 提供 uint32_t 作为分区偏移和长度类型。 */
static bool Port_FlashRangeValid(uint32_t area_size, uint32_t off, uint32_t len) /* 检查半开区间是否完全位于分区内。 */
{ /* 开始边界检查。 */
    if (off > area_size) /* 先排除起点越界,确保后面的减法不会下溢。 */
    { /* 处理非法起点。 */
        return false; /* 起点超出分区范围,拒绝该访问。 */
    } /* 结束起点检查。 */
    return len <= (area_size - off); /* 使用减法比较长度,避免 off + len 发生溢出。 */
} /* 结束边界检查函数;实际驱动还要检查设备总范围和对齐。 */

最后一包数据长度不一定满足写对齐。下载层应缓存凑齐,或用擦除值填充最后一个编程单元;填充也不能超过镜像写入上限。驱动还应按页写入限制拆分请求,并正确处理缓存一致性、超时和错误状态。具体拆分方式和错误处理取决于所用存储器及驱动。

5.12 日志、断言和看门狗

开启日志后,建立 mcuboot_config/mcuboot_logging.h。下面假设 Port_Log(level, format, ...) 接受与格式串兼容的可变参数,内部用安全的格式化方式输出;日志失败不应改写固件状态。

c 复制代码
#ifndef PORT_MCUBOOT_LOGGING_H /* 防止日志适配宏重复定义。 */
#define PORT_MCUBOOT_LOGGING_H /* 建立日志头文件包含保护。 */
#include "port_api.h" /* 声明 Port_Log,保证可变参数函数有正确原型。 */
#define MCUBOOT_LOG_ERR(...) Port_Log(0, __VA_ARGS__) /* 将错误日志映射到工程级别 0。 */
#define MCUBOOT_LOG_WRN(...) Port_Log(1, __VA_ARGS__) /* 将警告日志映射到工程级别 1。 */
#define MCUBOOT_LOG_INF(...) Port_Log(2, __VA_ARGS__) /* 将信息日志映射到工程级别 2。 */
#define MCUBOOT_LOG_DBG(...) Port_Log(3, __VA_ARGS__) /* 将调试日志映射到工程级别 3。 */
#define MCUBOOT_LOG_SIM(...) Port_Log(4, __VA_ARGS__) /* 提供模拟/诊断日志宏,避免接口缺项。 */
#define MCUBOOT_LOG_MODULE_DECLARE(module) /* 本文日志实现不需要模块声明,此宏有意为空。 */
#define MCUBOOT_LOG_MODULE_REGISTER(module) /* 本文日志实现不需要模块注册,此宏有意为空。 */
#endif /* 结束日志适配定义。 */

Port_WatchdogFeed() 要与 Bootloader 看门狗状态一致。若当前目标未启用看门狗,可实现为空函数;若已启用,应测量单次擦除和验签的最长不可服务时段,必要时调整驱动或超时。核心的喂狗钩子只能覆盖预设调用位置,驱动内部的长时间阻塞仍需单独处理。

本版本定义 MCUBOOT_HAVE_ASSERT_H 后包含的是工程自己的 mcuboot_config/mcuboot_assert.h;不定义才走标准 assert.h。本文提供自己的断言适配,文件内容如下:

c 复制代码
#ifndef PORT_MCUBOOT_ASSERT_H /* 防止重复定义断言适配。 */
#define PORT_MCUBOOT_ASSERT_H /* 建立断言头文件包含保护。 */
#include "port_api.h" /* 声明不得返回的 Port_AssertFail 函数。 */
#ifdef assert /* 如果之前的标准头已经定义 assert,先处理重定义问题。 */
#undef assert /* 只取消旧宏定义,不改变任何固件数据。 */
#endif /* 结束旧断言宏检查。 */
#define assert(condition) ((condition) ? (void)0 : Port_AssertFail(#condition, __FILE__, __LINE__)) /* 条件只求值一次,失败时记录表达式和位置并停止。 */
#define ASSERT(condition) assert(condition) /* 同时提供核心使用的另一种断言名称。 */
#endif /* 结束断言适配定义。 */

Port_AssertFail(const char *condition, const char *file, int line) 应输出必要诊断并停止当前危险操作,不得返回。此适配不因 NDEBUG 而静默关闭;常规输入错误仍应由显式检查和错误码处理,以保留可诊断的失败路径。

5.13 App 公共 API 的链接依赖

App 需要 boot_set_pending_multi()、boot_set_confirmed_multi() 等公共 API 时,仅包含 bootutil_public.h 不够:必须链接实现文件及其依赖、Flash 后端和同一配置。

为了首次接入时不漏依赖,可以把第 5.2 节选择的源文件构建成静态库,Bootloader 与 App 各自链接该库,并启用工具链的函数/数据分节与未引用节回收(分别放置函数和数据,由链接器丢弃未使用的节)。这样 App 不调用的启动流程有机会被链接器裁掉,实际体积以链接映射文件为准。App 不调用 boot_go() 来"跳回升级模式"。

若之后单独裁剪 App 的公共 API 库,应根据实际未解析符号收敛依赖,核对 bootutil_public.c 所引用的其他实现。同时核对 App 与 Bootloader 的槽编号、算法、最大扇区数、对齐和 Trailer 解释保持一致。

5.14 构建与接口验证

  1. 编译器实际读到的是本文自己的 MCUboot 与 Mbed TLS 配置。
  2. 链接结果只有选定的验签与交换实现,公钥符号来源唯一。
  3. Flash 分区可打开,扇区表和几何参数一致,越界访问会失败。
  4. 能在专用测试区域完成擦除、对齐写入、任意长度读取和校验,且真实失败会向上传递。
  5. 密码学内存池在验签前就绪;能够验证已知合法镜像并拒绝被篡改镜像。
  6. 编译与链接无未处理错误,关键失败路径能够保留诊断信息并停止当前操作。

6. Bootloader 启动与交接

启动入口依次完成初始化、镜像处理和执行权交接。port_api.h 是工程提供的声明集合;所有 Port_* 接口都需要实际实现,失败处理和启动交接必须保证不返回正常流程。

FIH 是 Fault Injection Hardening,即故障注入加固,用于降低异常干扰导致关键检查被跳过的风险。FIH_CALL 配合 FIH_SUCCESS 等宏传递和检查结果。启动代码沿用这套调用形式,与核心接口保持一致;当前 OFF 配置不代表已经实现了抗故障注入保护。

c 复制代码
#include <bootutil/bootutil.h> /* 声明 boot_go 和 boot_rsp。 */
#include <bootutil/fault_injection_hardening.h> /* 提供与 FIH 配置一致的调用和比较宏。 */
#include "port_api.h" /* 声明初始化、失败处理和 App 交接函数。 */
int main(void) /* Bootloader 的程序入口,由本工程启动代码调用。 */
{ /* 开始启动处理。 */
    struct boot_rsp rsp = {0}; /* 清空输出结构,避免误用未初始化启动信息。 */
    FIH_DECLARE(result, FIH_FAILURE); /* 默认按验证失败处理,直到核心明确返回成功。 */
    Port_BootInit(); /* 初始化核心所需存储、日志及看门狗;失败时内部停止或进入恢复路径。 */
    Port_CryptoInit(); /* 在任何验签之前初始化密码学内存池。 */
    FIH_CALL(boot_go, result, &rsp); /* 恢复未完成交换、处理升级并验证选中的镜像。 */
    if (FIH_NOT_EQ(result, FIH_SUCCESS)) /* 只允许核心成功结果进入 App 启动路径。 */
    { /* 处理无可启动镜像或启动验证失败。 */
        Port_BootFailure(); /* 进入工程定义的安全失败/恢复状态,此函数不得返回。 */
    } /* 结束失败分支。 */
    Port_StartImage(&rsp); /* 按目标执行环境完成 App 交接,此函数不得返回。 */
    Port_BootFailure(); /* 防御交接函数意外返回,禁止继续运行到未知地址。 */
} /* 结束入口定义;正常执行不会从 main 返回。 */

rsp.br_flash_dev_id 指明设备,rsp.br_image_off 是镜像头在该设备内的偏移,rsp.br_hdr 指向镜像头信息。镜像头起点不是直接可调用的 C 函数地址。 Port_StartImage() 应依据镜像格式与目标启动约定定位入口,并处理执行环境、异常/中断状态、缓存和外设所有权。

处理器的入口格式、状态寄存器和执行环境不同,因此启动指令由目标移植层实现,MCUboot 核心只提供所选镜像的信息。

若加入 Bootloader 下载入口,应明确未完成交换时的优先级:恢复所需的槽数据尚未整理完毕时,不能允许接收程序擦除 Secondary。接收入口应检查槽状态,存在恢复数据时拒绝冲突的下载请求。

7. App 升级请求与提交

7.1 App 下载与升级提交

App 先检查当前镜像不处于待确认的试运行状态,且没有未完成的交换/升级会话,再允许新的下载。否则擦除 Secondary 可能抹掉试运行期间需要回退的旧镜像。首次直接装入 Primary 的镜像可能没有有效 Trailer magic,不能仅看一个 image_ok 字节就把它判断为未确认试运行;应结合完整槽状态与首次安装约定判断。

候选镜像完整写入并读回检查后,调用公共 API 标记测试升级。下面函数只负责最后的提交,不执行下载;Download_ReadyToCommit() 必须检查长度、完整性、写入完成、读回结果及会话状态。

c 复制代码
#include <bootutil/bootutil_public.h> /* 声明公共升级控制 API。 */
#include "download.h" /* 声明工程下载状态检查函数。 */
#include "port_api.h" /* 声明系统复位接口。 */
int App_CommitDownloadedImage(void) /* 提交已经写完的候选镜像,返回失败原因。 */
{ /* 开始升级提交。 */
    if (!Download_ReadyToCommit()) /* 拒绝未完整写入或读回检查失败的下载会话。 */
    { /* 处理尚未满足提交条件的情况。 */
        return -1; /* 不写 pending 标记,保留失败供上层处理。 */
    } /* 结束下载状态检查。 */
    int rc = boot_set_pending_multi(0, 0); /* 镜像索引 0、permanent=0,申请可回退的测试升级。 */
    if (rc != 0) /* 确认 Trailer 更新是否真正成功。 */
    { /* 处理状态写入失败。 */
        return rc; /* 保留 MCUboot 错误码,不宣称升级提交成功。 */
    } /* 结束提交结果检查。 */
    Port_SystemReset(); /* 让设备重新进入 Bootloader;该工程接口不得正常返回。 */
    return -2; /* 仅处理异常返回,不能把未复位状态当作升级完成。 */
} /* 结束升级提交函数。 */

第二个参数为 0 表示测试升级,为 1 表示永久升级。本文需要自检失败后回退,因此选择 0。pending 只是请求,真正的签名验证与安装仍在 boot_go() 中进行。

7.2 Bootloader 下载入口

如果不希望 App 内置完整下载逻辑,可以让 App 写入一个"进入接收模式"的工程标记,再复位。Bootloader 读取并消费该标记,进入接收程序。

两类请求分别处理如下。工程请求的存放位置、复位后是否保留、有效性检查和清除时机需要单独约定,不占用 MCUboot Trailer。

请求 写入时机 读取方与含义
进入下载模式 尚未接收固件,App 收到下载指令时 Bootloader 进入下载程序
Pending 候选镜像完整写入并检查通过后 MCUboot 开始验证与安装候选镜像

8. 下载协议与会话状态

下载命令与设备内部会话状态分别定义。命令描述上位机的请求,状态用于约束设备当前允许执行的操作;它们均属于工程协议,不是 MCUboot 核心的状态枚举。

8.1 协议命令

最小下载流程使用以下命令,命令编码和传输介质由工程选择:

命令 必须检查 完成动作
BEGIN 镜像长度、产品匹配、是否允许覆盖候选区 建立会话,按规则清理 Secondary 的旧状态与内容
DATA 会话 ID、偏移、长度、分包校验与重复包 按候选起始偏移写入;重复包不得破坏已完成数据
END 是否完整、总长度、整体校验、Flash 读回结果 关闭写入,再调用 pending API,准备复位
ABORT 会话合法性、当前是否已提交 停止接收,保持未提交数据不能触发升级
QUERY 当前会话和可恢复进度 返回状态,便于上位机重试和诊断

本文新候选数据的分区内写入位置为 一个扇区的偏移 + 文件偏移。BEGIN 擦除 Secondary 之前,必须确认其中没有当前试运行所需的旧镜像,也没有中断交换所需的恢复数据。

CRC(循环冗余校验)用于发现传输中的常见错误;SHA-256 摘要可用于比对文件内容,但二者本身都不能证明发布者身份。可信来源由镜像签名验证。接收到的长度、偏移等都属于不可信输入,需要先检查再访问存储。

下载续传由下载协议及其持久化会话实现,MCUboot 只负责镜像安装阶段的交换恢复。初版可以在下载中断后重新接收整个文件,并在完整写入、校验通过后才提交 pending。

8.2 会话状态与失败处理

会话状态 处理内容 转移条件
空闲 等待 BEGIN 参数合法,且槽状态允许下载
准备 检查容量、擦除允许写入的区域 擦除成功后开始接收
接收 检查分包偏移,按编程粒度缓冲并写入 完整接收后进入验证
验证 检查总长度、整体校验和 Flash 读回结果 通过后允许提交
提交 调用测试 Pending API 成功后应答并复位
失败 记录错误,停止写入和提交 由上位机明确重试或中止

上位机在重连后查询运行版本和确认结果,再报告升级完成。仅收到传输应答或新版本心跳,还不能证明 App 已成功写入确认状态。

9. 密钥生成与镜像签名

9.1 密钥与签名工具

私钥和公钥都是密钥数据。私钥保存在发布端,用于镜像签名;设备端只保留配对的公钥,用于验证签名。.pem 是保存密钥的文件格式,私钥文件还可以另设口令加密。imgtool getpub 输出的 C 数组只是公钥数据的代码表示,不改变它的用途。

在独立 Python 环境中,从工程根目录执行以下命令,安装仓库中的 imgtool 并生成密钥。root-ec-p256.pem 是示例签名私钥文件,正式密钥应由发布流程保护。

sh 复制代码
python -m pip install ./third_party/mcuboot/scripts # 安装与固定 MCUboot 源码配套的 imgtool。
python ./third_party/mcuboot/scripts/imgtool.py keygen -k root-ec-p256.pem -t ecdsa-p256 # 生成 P-256 私钥,仅首次建立签名身份时执行。
python ./third_party/mcuboot/scripts/imgtool.py getpub -k root-ec-p256.pem -o boot/public_key.c # 导出可编译的公钥,随后加入 Bootloader 并与 keys.c 连接。

后续版本继续使用与设备公钥配对的私钥签名;更换私钥需要配套的密钥更新方案,否则已有设备无法验证新镜像。设备中只编入公钥,私钥保留在签名端。

9.2 生成不带整槽填充的升级文件

下面参数对应第 3 节抽象布局。app-v1.bin 与 app-v2.bin 是分别编译得到的 App 正文文件。

sh 复制代码
python ./third_party/mcuboot/scripts/imgtool.py sign -k root-ec-p256.pem --align 8 --header-size 0x200 --pad-header --slot-size 0x40000 --max-sectors 65 --version 1.0.0 app-v1.bin app-v1.signed.bin # 为首次安装的 V1 添加镜像头、版本、哈希和签名。
python ./third_party/mcuboot/scripts/imgtool.py sign -k root-ec-p256.pem --align 8 --header-size 0x200 --pad-header --slot-size 0x40000 --max-sectors 65 --version 1.1.0 app-v2.bin app-v2.signed.bin # 为后续测试升级生成 V2,使用相同运行布局与签名身份。
python ./third_party/mcuboot/scripts/imgtool.py verify -k root-ec-p256.pem app-v2.signed.bin # 在签名端检查生成文件,设备端仍必须独立验签。
参数 本文取值及原因
-k 选择与 Bootloader 公钥配对的签名私钥
--align 8 与示例写入对齐一致;不能误填擦除扇区大小
--header-size 0x200 预留 512 字节头部,必须匹配第 4 节链接约定
--pad-header 输入不含预留头部,所以工具负责前置它
--slot-size 0x40000 本示例用于签名容量检查的保守槽跨度,与 Primary 相同;不代表 Secondary 实际分区变成这个大小
--max-sectors 65 与核心配置同步,让工具的容量/Trailer 估算使用相同扇区上限
--version 标识 V1/V2,便于确认运行版本;不是自动启用硬件防降级
未用 --pad 不生成带整槽填充和 Trailer magic 的文件,状态交给设备端 API 写入
未用 --confirm 候选镜像必须先试运行,由新 App 自检后确认
未用 --overwrite-only 与本文 Offset 交换模式保持一致

此版本工具的通用帮助会把 --slot-size 描述为槽大小,异长槽时通常取 Secondary 大小。但它不替下载器管理 Offset 起始扇区。本例不填充文件,使用较小的 0x40000 做保守容量检查,并额外要求生成文件实际长度不超过 0x3F000;设备下载上限也按第 3 节执行。不能把一个按 Secondary 全长填充的文件再从第二扇区写入,否则会越过分区末尾。

所有数字只适用于本文几何示例。更换参数时同时修改分区、核心配置、签名参数和下载上限;imgtool 成功不代表目标存储布局一定正确。

10. 首次烧录与启动检查

首次烧录先写入包含正确公钥的 Bootloader,再把 app-v1.signed.bin 写到 Primary 镜像起点。首次初始化存储时,两个槽未使用的区域与 Trailer 应处于正确的擦除状态,不保留上一次实验的交换标记。

这是首次初始化设备的步骤。正常现场升级不能无条件擦除两个镜像槽,否则会失去正在运行或用于回退的镜像。

复位后检查 V1 能通过验证并运行,完成必要自检。接着把 V2 写到 Secondary 第二扇区起点,使用测试 pending 请求,再进入第 11 节验证完整流程。

记录本次 Bootloader 和 App 的版本、分区参数、镜像摘要及启动日志,后续测试使用同一套构建配置。

11. 测试升级、确认与断电恢复

11.1 自检与确认条件

新 App 在关键初始化和必要业务自检通过后调用确认接口。若在入口处立即确认,后续初始化失败便无法再利用试运行状态回退。

c 复制代码
#include <bootutil/bootutil_public.h> /* 声明确认当前镜像的公共 API。 */
#include "app_health.h" /* 声明本工程对当前版本的必要健康检查。 */
int App_ConfirmAfterSelfTest(void) /* 自检后确认当前镜像;由 App 启动流程调用一次。 */
{ /* 开始确认流程。 */
    if (!App_RequiredSelfTestPassed()) /* 检查关键功能是否已达到允许保留新版本的条件。 */
    { /* 处理自检尚未通过或已经失败。 */
        return -1; /* 不写确认标记,保留下一次复位回退的可能。 */
    } /* 结束自检判断。 */
    return boot_set_confirmed_multi(0); /* 确认镜像 0,并把实际 Flash 状态写入结果交给调用者。 */
} /* 结束确认函数;调用者必须检查返回值。 */

未确认状态下复位,MCUboot 根据状态执行回退;已经确认后,后续业务故障不会自动触发这种试运行回退。App 若卡死而始终不复位,MCUboot 也无法重新获得执行权,因此故障复位策略要一起验证。

11.2 交换中断后的恢复机制

Offset 按扇区分步搬运数据,并将进度写入 Flash 状态区。断电后这些记录仍然保留,Bootloader 再次启动时据此恢复未完成的交换,无需保留此前的 RAM 状态。

中断发生的位置决定了重启后的处理方式:

中断发生时刻 预期处理
下载尚未提交 pending 不安装半份候选文件;下载重试或续传由下载协议处理
正在交换、尚未完成安装 根据交换状态继续恢复;不能直接跳入只搬了一半的 App
新 App 已开始试运行但尚未确认 再次启动时按状态执行回退;它与"继续上次未完成交换"不同

恢复机制依赖 Flash 驱动、存储几何参数和状态写入顺序正确,也受到存储器掉电行为的影响。硬件损坏或驱动错误报告操作成功都可能破坏这一前提,因此仍需在目标硬件上进行断电测试,检查真实擦写过程中的恢复结果。

11.3 测试项目与通过条件

顺序 操作 通过条件
1 正常装入 V1,连续复位 每次稳定启动 V1,镜像验证正常
2 下载 V2 后不提交 pending,复位 仍运行 V1,半成品不被安装
3 完整下载 V2,提交测试升级 实际搬运后启动 V2,旧 V1 保留用于回退
4 故意让 V2 不确认,然后复位 回退并启动 V1
5 再次升级 V2,自检通过后确认,再多次复位 始终保留 V2,不重复回退
6 分别测试错密钥、修改正文、截断文件 无效候选不被启动,原有效镜像仍可用或进入明确的恢复路径
7 下载中随机掉电,尤其是末包与 pending 前后 不把半份镜像误认为成功提交;允许重试
8 交换和回退期间反复随机掉电/复位 多次恢复后能进入完整有效镜像,不启动半搬运镜像
9 故意制造写入失败、擦除超时、读错误 错误被报告,不继续把当前步骤记录为成功
10 测试最大镜像、越界包、错误对齐和重复包 不越界、不覆盖 Trailer 或 App 自定义数据区,错误输入被拒绝

测试表第 8 项要覆盖扇区擦除、数据编程以及状态写入附近,既测试正向升级,也测试回退。记录固件版本、掉电位置、启动日志和最终运行版本;有限次数通过只能证明这些测试场景,不能表述为任何电气条件下都绝对可靠。

App 自定义数据若存在格式升级,还需要验证旧版本能否读取升级后的数据。测试升级期间可将不可逆的数据迁移延后到确认之后,或保留旧格式副本;仅追加字段是否兼容,仍取决于旧版本的解析方式。

12. 排错与交付检查

现象 优先核对
找不到头文件或函数 搜索路径、Offset 源文件、App 公共 API 实现及静态库链接顺序
验签失败 公私钥配对、签名算法、文件完整性、密码学内存池和配置是否一致
pending 后不升级 API 返回值、真实槽 ID、候选是否从第二扇区写入、旧 Trailer 状态
验证通过却启动失败 App 链接位置、头部是否重复、目标启动交接契约
刚升级又退回旧版 自检是否完成、确认是否成功、确认前是否发生复位
交换中反复复位 看门狗时限、擦写耗时、驱动错误、状态区与几何配置

交付材料应包含版本与分区记录、签名流程、目标硬件测试结果。交付前还需确认私钥未进入设备、Bootloader/公钥具备需要的写保护、App 自定义数据格式兼容回退版本。需要防降级或抗故障注入时,另行接入并验证相应安全能力,不能把本教程的功能测试当作安全认证。

继续查阅:MCUboot v2.2.0 源码、移植约定、设计与恢复状态、imgtool 参数实现。

相关推荐
想喝奶茶啾啾1 小时前
AMBA CHI架构以及规范中使用的术语 翻译笔记(1)
笔记·arm·chi
北京盛世宏博2 小时前
多协议以太网温湿度变送器:Modbus TCP/UDP/SNMP 协议应用场景详解
单片机·tcp/ip·udp·传感器·温湿度
HRTOS2 小时前
HRTOS 内核 BUG 修复:任务等待对象未初始化导致的异常唤醒问题
单片机·嵌入式硬件·算法·51单片机
@MMiL2 小时前
SImulink: PowerGui下的信号FFT分析
单片机
List<String> error_P3 小时前
UART串口通信示波器波形测试
嵌入式硬件·示波器
番茄灭世神3 小时前
PN学堂GD32教程第12篇——SPI与Nor Flash
c语言·stm32·单片机·嵌入式·gd32·总线协议
狂奔蜗牛(bradley)3 小时前
EtherCAT 主站拓扑判定的三级防护:从去抖到振荡检测
人工智能·嵌入式硬件·fpga开发·架构
HRTOS3 小时前
HRTOS 测试框架完善:为中断验证增加中断来源校验机制
经验分享·单片机·嵌入式硬件·51单片机
FPC_小西4 小时前
您知道嵌入式开发中八大常用接口与总线有哪些吗?
人工智能·单片机·嵌入式硬件·pcb工艺·hdi高密度互联