MCUboot 裸机移植实战:Flash 分区、固件升级与回退
MCUboot 的双槽交换机制支持固件签名验证、测试升级和失败回退。裸机移植需要配合镜像格式规划 Flash 布局,并实现存储访问、启动交接及 App 升级接口。
示例固定为 MCUboot v2.2.0、单镜像、ECDSA P-256 椭圆曲线签名、Mbed TLS 密码学库和 Swap using offset 交换算法。配置、链接和签名步骤使用同一组分区参数。更换版本或算法后,需要重新核对依赖与接口。
代码采用通用硬件接口,不绑定芯片或开发板。Port_*、Download_*、App_* 由工程实现,代码中的每个非空行均附注释。示例用于说明接入方法,尚未在目标硬件上完成编译、烧录和断电验证。
目录
- 镜像、分区与升级状态
- 系统架构与下载方式
- [Flash 分区与 Offset 交换](#Flash 分区与 Offset 交换)
- [工程组织与 App 链接布局](#工程组织与 App 链接布局)
- [MCUboot 核心与移植接口](#MCUboot 核心与移植接口)
- [Bootloader 启动与交接](#Bootloader 启动与交接)
- [App 升级请求与提交](#App 升级请求与提交)
- 下载协议与会话状态
- 密钥生成与镜像签名
- 首次烧录与启动检查
- 测试升级、确认与断电恢复
- 排错与交付检查
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, §or) |
找到包含分区内偏移 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 构建与接口验证
- 编译器实际读到的是本文自己的 MCUboot 与 Mbed TLS 配置。
- 链接结果只有选定的验签与交换实现,公钥符号来源唯一。
- Flash 分区可打开,扇区表和几何参数一致,越界访问会失败。
- 能在专用测试区域完成擦除、对齐写入、任意长度读取和校验,且真实失败会向上传递。
- 密码学内存池在验签前就绪;能够验证已知合法镜像并拒绝被篡改镜像。
- 编译与链接无未处理错误,关键失败路径能够保留诊断信息并停止当前操作。
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 自定义数据格式兼容回退版本。需要防降级或抗故障注入时,另行接入并验证相应安全能力,不能把本教程的功能测试当作安全认证。