第10篇:双镜像机制 -- 活动槽与下载槽的协同工作
本文基于 X-CUBE-SBSFU v2.6.2,硬件平台 NUCLEO-G474RE,加密方案 ECC384 + AES256-CBC + SHA384。
本文面向零基础读者,深入解释 SBSFU 双镜像(2_Images)配置的核心机制。
1. 为什么需要双镜像?
在嵌入式系统中,固件更新是一个高风险操作。我们先来看看不同方案在面对断电、传输错误等场景时的表现。
1.1 单镜像的致命缺陷
单镜像方案只有一个固件存储区域。更新流程只能是:
Step 1: 设备进入 Bootloader 模式(停止正常运行)
Step 2: 擦除唯一的固件存储区
Step 3: 接收新固件 → 写入 → 验证
Step 4: 跳转执行新固件
这带来了三个致命问题:
问题一:下载期间"死机"
擦除旧固件后、写入新固件之前,设备内没有可执行的程序。如果此时掉电,设备将永远无法正常启动,俗称"变砖"(bricked)。对于部署在偏远地区的 IoT 设备,这意味着一笔昂贵的现场维护成本。
问题二:下载时无法执行业务逻辑
从擦除到写入完成,设备处于"Loader 模式",不能执行任何用户任务。对一个需要 7x24 小时运行的工业控制设备,几分钟的停机时间可能不可接受。
问题三:不支持固件验证和回滚
如果新固件有 bug,没有旧固件可以退回去。单镜像方案中没有"上一版本"可供恢复。
1.2 双镜像如何解决这些问题
双镜像方案的核心思想是:下载和运行分离。
┌──────────────────────────────────────────────────────┐
│ Flash 存储区 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ Active Slot │ │ Swap Area │ │Download Slot│ │
│ │ (运行中) │ │ (交换缓存) │ │ (新固件) │ │
│ │ 216 KB │ │ 8 KB │ │ 216 KB │ │
│ └─────────────┘ └─────────────┘ └────────────┘ │
│ ↑ ↑ │
│ │ │ │
│ 正常执行用户程序 后台下载新固件 │
│ (不能擦除/覆盖) (不影响当前运行) │
└──────────────────────────────────────────────────────┘
- 下载期间不中断业务:UserApp 从 Active Slot 运行,新固件写入 Download Slot,两者在物理 Flash 上完全隔离。
- 断电安全:如果在下载过程中断电,Download Slot 中的数据可能损坏,但 Active Slot 中的旧固件完好无损。下次上电后可以重新下载。
- 支持回滚:新固件安装后如果验证失败,可以将 Active Slot 中的旧固件换回来。
回顾 readme.txt 中的关键说明:
To download a PartialUserApp.sfb, it is mandatory to have previously installed the UserApp.sfb identified as reference into the device.
这说明双镜像机制不仅支持完整更新,还支持"有依赖关系的差分更新"。
2. 双镜像的内存布局
2.1 实际地址映射
从 mapping_fwimg.h 中提取的真实地址分配:
c
/* Active slot #1 (216 kbytes) */
#define SLOT_ACTIVE_1_START 0x08010000
#define SLOT_ACTIVE_1_END 0x08045FFF
/* swap (8 kbytes) */
#define SWAP_START 0x08046000
#define SWAP_END 0x08047FFF
/* Dwl slot #1 (216 kbytes) */
#define SLOT_DWL_1_START 0x08048000
#define SLOT_DWL_1_END 0x0807DFFF
/* Slots not configured (本配置中全部为0) */
#define SLOT_ACTIVE_2_START 0x00000000
#define SLOT_ACTIVE_3_START 0x00000000
#define SLOT_DWL_2_START 0x00000000
#define SLOT_DWL_3_START 0x00000000
2.2 可视化布局
STM32G474RE Flash: 512 KB (0x08000000 - 0x0807FFFF)
0x08000000 ┌──────────────────────────┐
│ SE Core + SBSFU │ ← 安全引导代码(64KB)
0x08010000 ├──────────────────────────┤
│ │
│ Active Slot #1 │ ← 当前运行的固件
│ (216 KB) │ 经过签名验证+解密
│ 0x08010000-0x08045FFF │ 用户程序从这里执行
│ │
0x08046000 ├──────────────────────────┤
│ Swap Area (8 KB) │ ← 原子交换的临时缓冲区
│ 0x08046000-0x08047FFF │ 存放交换过程中被搬移的页面
0x08048000 ├──────────────────────────┤
│ │
│ Download Slot #1 │ ← 新固件下载区
│ (216 KB) │ 用户程序通过YMODEM写入
│ 0x08048000-0x0807DFFF │ 收到的是加密的.sfb文件
│ │
0x0807E000 ├──────────────────────────┤
│ (8 KB 保留/未使用) │ ← 对齐预留
0x08080000 └──────────────────────────┘ ← Flash 结束
2.3 各区域功能详解
| 区域 | 起始地址 | 大小 | 功能 | 保护级别 |
|---|---|---|---|---|
| Active Slot | 0x08010000 | 216 KB | 存储已安装且通过验证的固件。SBSFU 每次启动时验证其签名,验证通过后才跳转执行。 | 正常(不可写保护,因为 Swap 时需要擦除和重写) |
| Download Slot | 0x08048000 | 216 KB | 接收新固件的目标区域。UserApp 在运行期间通过 YMODEM 协议将 .sfb 文件写入这里。 | 无保护(UserApp 可以自由写入) |
| Swap Area | 0x08046000 | 8 KB | 固件安装时的交换缓冲区。类似于"交换两个变量的中间变量",保证交换过程即使断电也能恢复。 | 仅 SBSFU 访问 |
3. 固件安装流程(Swap 方式详解)
当 UserApp 完成下载并复位后,SBSFU 进入安装流程。以下是每一步的详细分解。
3.1 整体流程概览
UserApp 下载完成 → SFU_APP_InstallAtNextReset() → NVIC_SystemReset()
↓
SBSFU 启动
↓
检测 Download Slot 中有新固件(FwImageState == NEW)
↓
[SE] 验证新固件头 ECDSA 签名
↓
[SE] 解密新固件(AES256-CBC)
↓
[SE] 验证新固件完整性(SHA384 对比 FwTag)
↓
执行 Swap:将 Download Slot 内容交换到 Active Slot
↓
验证交换后的 Active Slot 固件
↓
跳转执行新固件
3.2 Swap 过程逐步详解
Swap 是双镜像机制中最精妙的设计。它不是简单地"把 Download Slot 复制到 Active Slot",而是通过分页的原子交换来保证断电安全。
为什么不能直接覆盖?
问题场景:直接覆盖 Active Slot
━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1: 擦除 Active Slot 第 0 页 ← 旧固件已经损坏
Step 2: 写入新固件第 0 页 ← 掉电!!!
结果:Active Slot 破损,Download Slot 内容还在但无法单独启动
Swap 的分页交换策略
Swap 的每一步都保证"至少有一个完整固件存在于某个槽中"。使用 Swap Area 作为 8KB 的中间缓冲区:
初始状态:
Active Slot: [A0][A1][A2][A3]...[An] (旧固件,每页 4KB)
Swap Area: [空]
Download Slot: [D0][D1][D2][D3]...[Dn] (新固件)
交换过程(逐页进行):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1: 页 0 的交换
1a. 将 Active Slot 页 0 复制到 Swap Area
Active: [A0][A1][A2]... Swap: [A0] DWL: [D0][D1][D2]...
状态标记:ACTIVE_PAGE0→SWAP (如果此时断电,Active 完整)
1b. 将 Download Slot 页 0 复制到 Active Slot 页 0 的位置
(先擦除 Active 页 0,再写入 D0)
Active: [D0][A1][A2]... Swap: [A0] DWL: [D0][D1][D2]...
状态标记:DWL_PAGE0→ACTIVE
1c. 将 Swap Area 内容复制到 Download Slot 页 0
(先擦除 DWL 页 0,再写入 A0)
Active: [D0][A1][A2]... Swap: [A0] DWL: [A0][D1][D2]...
状态标记:SWAP→DWL_PAGE0
Step 2: 页 1 的交换
2a. Active 页 1 → Swap Area
2b. DWL 页 1 → Active 页 1
2c. Swap → DWL 页 1
Active: [D0][D1][A2]... Swap: [A1] DWL: [A0][A1][D2]...
... 重复直到所有页交换完成 ...
最终状态:
Active Slot: [D0][D1][D2][D3]...[Dn] (新固件)
Swap Area: [空/任意]
Download Slot: [A0][A1][A2][A3]...[An] (旧固件)
4. Swap 机制的原子性保证
4.1 状态标记机制
SBSFU 在每一小步操作前后都会更新状态标记 。这些标记存储在 Flash 的特定位置(固件头中的 FwImageState 字段),确保断电后恢复执行时能够识别当前进度。
关键概念:状态标记必须是原子的,且存储在非易失性存储器中。SBSFU 使用 Flash 中专门的区域存储交换进度。
4.2 断电恢复场景分析
场景 A:在 Step 1a 之后断电(Active 页0 → Swap Area 完成,尚未擦除 Active 页0)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
上电后 SBSFU 检测到:
- Swap Area 中有数据(状态标记指示"正在交换")
- Active Slot 页 0 未被擦除
恢复策略:继续执行 Step 1b,将 DWL 页 0 写入 Active 页 0
最终结果:Active 完整,交换正常完成
场景 B:在 Step 1b 之后断电(DWL 页 0 已写入 Active 页 0,尚未写回 A0 到 DWL)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
上电后 SBSFU 检测到:
- Active 页 0 已更新为 D0
- Swap Area 中仍有 A0
- 状态标记指示"需要将 Swap 写回 DWL"
恢复策略:执行 Step 1c,将 Swap Area 中的 A0 写入 DWL 页 0
最终结果:交换完整(Active 有 D0,DWL 有 A0)
场景 C:在 Step 1c 之后断电(页 0 交换完全完成)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
上电后 SBSFU 检测到:
- 页 0 交换完成标记
- 继续页 1 的交换
恢复策略:从页 1 开始继续交换
最终结果:所有页交换完成
核心原则:任何时候,旧固件的所有数据页要么在 Active Slot 中,要么在 Download Slot 中,要么在 Swap Area 中。永远不会出现"一页数据同时丢失"的情况。Swap Area 充当了一页数据的"安全中转站"。
4.3 SFU_NO_SWAP 编译选项
如果定义了 SFU_NO_SWAP,则跳过 Swap 机制,直接将新固件写入 Active Slot。这种模式下没有断电保护,但节省了 Swap Area 的空间。本项目使用默认的 Swap 方式。
4.1 Trailer 结构大小公式
Swap Area 底部的 Trailer 区域存储交换进度和状态。其大小计算遵循以下公式:
Trailer_Size = (HeaderSize × 2) + (32 × 2) + ((N1 + N2) × Flash_Write_Len)
其中:
HeaderSize = SE_FW_HEADER_TOT_LEN (232 字节,本方案)
32 × 2 = Clean 标记 (32B) + Image Resume (32B) --- 两段 0x55 编码
N1 = Active Slot 的 Flash 页数 (108)
N2 = Download Slot 的 Flash 页数 (108)
Flash_Write_Len = FLASH_IF_MIN_WRITE_LEN (8 字节,G4 双字编程)
实际计算:
= 232×2 + 32×2 + (108+108)×8
= 464 + 64 + 216×8
= 464 + 64 + 1728
= 2256 字节 ≈ 2.2KB
Trailer 占据了 Swap Area (8KB) 中的最后约 2.2KB,其余约 5.8KB 用于逐页缓冲。这个大小经过精心计算,确保即使是最坏情况(全部 216 页都需要记录交换状态),也有足够的存储空间。
Clean 标记为什么是 0x55?
0xFF = Flash 擦除后的自然状态(全 1)
0x00 = 全 0,可能与其他"已编程为 0"的状态混淆
0x55 = 0b01010101,交替的 0/1 位模式
优势:
· 可以明确区分"从未写入"(0xFF) vs "已编程为 Clean"(0x55)
· 0xAA (0b10101010) 也常用,但 0x55 在噪声环境下的误判概率更低
· 任何中间状态(部分编程)既不是 0xFF 也不是 0x55
→ SBSFU 可判定为"Swap 被中断,需要恢复"
5. 固件状态管理(FwImageState)
5.1 固件头的 FwImageState 字段
每个固件槽的头部包含 FwImageState[3][32],这是一个 3x32 字节的数组,存储了 3 个槽位中各状态的副本。这些状态定义了固件的生命周期阶段。
5.2 六种固件状态
c
// 固件状态定义(位于 se_def_metadata.h)
FWIMG_STATE_INVALID // 状态无效(初始值或验证失败)
FWIMG_STATE_NEW // 新固件已下载,等待安装
FWIMG_STATE_INSTALLING // 正在安装(交换进行中)
FWIMG_STATE_INSTALLED // 已安装,等待用户验证
FWIMG_STATE_SELFTEST // 自检模式(首次启动)
FWIMG_STATE_VALID // 验证通过,正常运行
FWIMG_STATE_VALID_ALL // 所有固件验证通过(多槽位场景)
5.3 状态转换图
下载新固件到 DWL Slot
STATE_EMPTY / ──────────────────────────────────→ STATE_NEW
STATE_INVALID │
↑ │ SBSFU 检测到后开始安装
│ ↓
│ STATE_INSTALLING
│ │
│ Swap 完成 │
│ ↓
│ STATE_INSTALLED
│ │
│ 用户程序显式验证│
│ ↓
│ STATE_SELFTEST
│ │
│ ┌──────────────────┤
│ │ │
│ 超时未验证 自检通过
│ │ │
│ ↓ ↓
│ STATE_INVALID STATE_VALID
│ │ │
│ │ │
└──────────────────────────────┘ │
回滚:SBSFU 执行反向 Swap │
正常运行
6. 固件验证与回滚
6.1 ENABLE_IMAGE_STATE_HANDLING 编译开关
这是控制固件状态管理功能的关键编译开关。默认情况下在 UserApp 示例中被注释掉(从 readme.txt 可知)。
当启用时:
- 新固件安装后处于
SELFTEST状态 - 用户程序必须显式调用"固件验证"功能
- 验证通过 → 状态变为
VALID - 超时未验证(如看门狗触发复位)→ 状态变为
INVALID→ 触发回滚
当禁用时:
- 安装完成后直接认为固件有效
- 没有自检和验证环节
- 没有自动回滚功能
6.2 验证流程代码
从 fw_update_app.c 中的 FW_VALIDATE_RunMenu() 函数:
c
void FW_VALIDATE_RunMenu(void)
{
// 仅当 ENABLE_IMAGE_STATE_HANDLING 已定义时此功能生效
// 否则输出 "Feature not supported !"
// 验证子菜单:
// 0 - 验证所有固件
// 1 - 验证 SLOT_ACTIVE_1
// 2 - 验证 SLOT_ACTIVE_2 (本配置不适用)
// 3 - 验证 SLOT_ACTIVE_3 (本配置不适用)
// 核心调用: SE_APP_ValidateFw() 或 FW_VALIDATE_ValidateFw()
se_retCode = SE_APP_ValidateFw(&se_Status, slot_number);
}
static SE_ErrorStatus FW_VALIDATE_ValidateFw(SE_StatusTypeDef *peSE_Status, uint32_t SlotNumber)
{
// 获取当前固件状态
e_ret_status = FW_VALIDATE_GetActiveFwState(peSE_Status, current_slot, ¤t_fw_state);
if (e_ret_status == SE_SUCCESS)
{
// 只有处于 SELFTEST 状态的固件才能被验证
if (current_fw_state != FWIMG_STATE_SELFTEST)
{
printf("Firmware not is SELF_TEST state\r\n");
e_ret_status = SE_ERROR;
}
else
{
if (next_fw_state == FWIMG_STATE_VALID_ALL)
FWIMG_STATE[current_slot - SLOT_ACTIVE_1] = FWIMG_STATE_TAG_VALID_ALL;
else
FWIMG_STATE[current_slot - SLOT_ACTIVE_1] = FWIMG_STATE_TAG_VALID;
e_ret_status = SE_SUCCESS;
}
}
return e_ret_status;
}
注意状态标记的存储方式:FWIMG_STATE 实际上是一个位于 RAM 中的数组,用于快速访问。真实的状态存储在 Flash 固件头中。在受保护的配置下(SFU_MPU_PROTECT_ENABLE 或 SFU_SECURE_USER_PROTECT_ENABLE),这个 RAM 数组受 MPU 保护,只有通过 SE 接口才能修改。
6.3 回滚流程
当 SBSFU 启动检测到当前固件状态为 INVALID 时:
SBSFU 检测到 Active Slot 固件状态 = INVALID
↓
触发反向 Swap:将 Download Slot 中的旧固件换回 Active Slot
↓
验证交换后的 Active Slot 固件(旧固件)的签名
↓
擦除 Download Slot 内容(清除无效的新固件)
↓
跳转到旧固件执行
7. 从用户程序触发固件下载
sfu_app_new_image.c 提供了三个核心 API,它们被编译在 UserApp 的上下文中:
7.1 SFU_APP_GetDownloadAreaInfo() -- 获取下载槽信息
c
void SFU_APP_GetDownloadAreaInfo(uint32_t DwlSlot, SFU_FwImageFlashTypeDef *pArea)
{
pArea->DownloadAddr = SlotStartAdd[DwlSlot];
// DwlSlot = SLOT_DWL_1 → 返回 0x08048000
pArea->MaxSizeInBytes = (uint32_t)SLOT_SIZE(DwlSlot);
// 返回 216 * 1024 = 221184 字节
pArea->ImageOffsetInBytes = SFU_IMG_IMAGE_OFFSET;
// 固件头偏移,通常是 4096 (4KB 对齐)
}
SlotStartAdd[] 和 SLOT_SIZE() 宏定义在 sfu_fwimg_regions.h 中,它们的值来自链接器映射文件(mapping_fwimg.h),确保 UserApp 下载的数据写入正确的物理地址。
7.2 SFU_APP_InstallAtNextReset() -- 标记安装请求
c
HAL_StatusTypeDef SFU_APP_InstallAtNextReset(uint8_t *fw_header)
{
#if !defined(SFU_NO_SWAP)
if (fw_header == NULL)
return HAL_ERROR;
// 将固件头写入 Swap Area 的起始位置
if (WriteInstallHeader(fw_header) != HAL_OK)
return HAL_ERROR;
return HAL_OK;
#else
return HAL_OK; // 无 Swap 模式,不需要额外操作
#endif
}
static HAL_StatusTypeDef WriteInstallHeader(uint8_t *pfw_header)
{
// 擦除 Swap Area 的固件头区域(SFU_IMG_IMAGE_OFFSET 字节)
ret = FLASH_If_Erase_Size((void *)SlotStartAdd[SLOT_SWAP], SFU_IMG_IMAGE_OFFSET);
if (ret == HAL_OK)
{
// 将新固件的头部写入 Swap Area
ret = FLASH_If_Write((void *)SlotStartAdd[SLOT_SWAP], pfw_header, SE_FW_HEADER_TOT_LEN);
}
return ret;
}
这个函数将新固件的头部信息写入 Swap Area (0x08046000),作为"安装请求"的标记。SBSFU 在下次启动时检查 Swap Area:
- 如果 Swap Area 中有有效的固件头 → 说明有固件等待安装
- SBSFU 读取这个头部,获取新固件的元数据(版本、大小、哈希等)
- 从 Download Slot 读取加密固件,解密并验证
- 执行 Swap 安装
8. 多个下载槽的理论扩展
X-CUBE-SBSFU 理论上支持最多 3 个 Active Slot + 3 个 Download Slot。这在以下场景中很有用:
8.1 多固件场景
示例:一个 IoT 设备包含
Slot Active 1 + DWL 1 → 主控 MCU 固件
Slot Active 2 + DWL 2 → 无线通信模块固件(如 LoRa/WiFi 协议栈)
Slot Active 3 + DWL 3 → 协处理器固件
每个槽位可以独立更新,某个模块的固件升级不影响其他模块。
8.2 当前项目配置
本示例项目(2_Images)只配置了 1 个 Active Slot 和 1 个 Download Slot:
c
// mapping_fwimg.h 中未配置的槽位全部为 0
#define SLOT_ACTIVE_2_START 0x00000000
#define SLOT_ACTIVE_2_END 0x00000000
#define SLOT_ACTIVE_3_START 0x00000000
// ...
fw_update_app.c 中的多镜像功能检查代码验证了这一点:
c
if (several_dwl_area == 1)
{
printf(" -- !!Only 1 download area configured - feature not available!! \r\n\n");
return;
}
如果需要启用更多槽位,需要:
- 修改
mapping_fwimg.h和对应的链接器脚本 - 调整 Flash 分区大小(总 Flash 只有 512KB,多槽位意味着每个槽位更小)
- 重新生成所有密钥(每个槽位对应不同的
fwid) - 重新编译全部 3 个工程
总结
双镜像机制是 SBSFU 安全固件更新体系中最核心的设计。它通过 Active Slot、Download Slot 和 Swap Area 三个区域的分工协作,以及精细的原子交换状态标记,实现了:
- 下载与运行并行:用户程序从 Active Slot 运行,新固件写入 Download Slot,互不干扰
- 断电安全:任何时刻断电,Active Slot 中始终有一个完整的固件,设备不会变砖
- 自动回滚:新固件安装后可验证,验证失败自动恢复到上一个版本
- 差分更新支持:PartialUserApp.sfb 描述了新固件与旧固件的差异,减少传输数据量
这些特性的核心代价是 Flash 空间利用率降低了约一半(Active Slot 和 Download Slot 大小相同)。但对于安全关键的应用场景,这个代价是完全值得的。
下一篇文章将详细解析固件从 .bin 到 .sfb 的完整打包流程。
本文基于 X-CUBE-SBSFU v2.6.2 实际源代码撰写,所有地址映射和代码片段均来自项目文件 mapping_fwimg.h 和 sfu_app_new_image.c。