Bootloader 内存分配进阶:三区 A/B 分区与 OTA 实战
Bootloader 内存分配进阶:三区 A/B 分区与 OTA 实战
系列:Bootloader 内存分配 · 进阶篇
平台:STM32F103RCT6(256 KB Flash / 48 KB RAM)
工具链:GCC ARM + CMake / STM32CubeIDE 均可
前置:已理解双区模型、链接脚本与 VTOR 基本概念
本文在双区 Bootloader 基础上,介绍三区 A/B 分区方案:通过 Bootloader + App1(运行槽)+ App2(下载/备份槽)实现安全 OTA、槽位切换、CRC 校验与故障回滚。
目录
- 进阶目标与整体架构
- Flash 三区内存映射
- Cortex-M 向量表与 VTOR 深度解析
- Boot 元数据与槽位切换
- OTA 升级完整流程
- CRC 校验与简易签名
- 回滚与故障恢复
- 完整工程目录与源码
- 编译、烧录与调试
- 从双区迁移到三区
- 常见问题与排错
1. 进阶目标与整体架构
1.1 双区的局限
|
问题 |
说明 |
|
擦写中断 |
升级过程中断电,App 区可能半写入,设备变砖 |
|
无回滚 |
新固件有 bug 时无法快速恢复旧版本 |
|
单点故障 |
只有一个 App 槽,校验失败即无法启动业务 |
1.2 三区 A/B 模型
将 Flash 划分为 Bootloader + App1(运行槽)+ App2(下载/备份槽):
0x0800_0000 ┌──────────────────────┐
│ Bootloader 32KB │ 上电入口,校验与跳转
0x0800_8000 ├──────────────────────┤
│ App1 (Slot A) │ 当前运行固件
│ 112KB │
0x0802_4000 ├──────────────────────┤
│ App2 (Slot B) │ OTA 下载区 / 备份区
│ 112KB │
0x0804_0000 └──────────────────────┘
(剩余空间可用于参数区、日志等)
1.3 系统数据流
上电 → Bootloader → 读取 Boot Meta → CRC/签名校验
├─ 校验通过 → 跳转 Active Slot → Application 运行
└─ 校验失败 → 尝试另一 Slot → 跳转
OTA 下载 → 写入 App2(inactive 槽)→ 重启进 Bootloader
→ 校验并切换 Active → 跳转新固件
2. Flash 三区内存映射
2.1 统一头文件 memory_map.h
所有工程(Bootloader / App1 / App2)必须引用同一份地址定义,避免链接地址与跳转地址不一致。
#ifndef MEMORY_MAP_H
#define MEMORY_MAP_H
#include <stdint.h>
#define FLASH_BASE_ADDR 0x08000000U
#define BL_FLASH_SIZE (32U * 1024U)
#define APP_SLOT_SIZE (112U * 1024U)
#define BL_START_ADDR FLASH_BASE_ADDR
#define APP1_START_ADDR (BL_START_ADDR + BL_FLASH_SIZE)
#define APP2_START_ADDR (APP1_START_ADDR + APP_SLOT_SIZE)
#define APP1_VTOR_OFFSET (APP1_START_ADDR - FLASH_BASE_ADDR)
#define APP2_VTOR_OFFSET (APP2_START_ADDR - FLASH_BASE_ADDR)
#define BOOT_META_ADDR 0x0803F000U
#define BOOT_META_SIZE (4U * 1024U)
#define BOOT_MAGIC 0xB007DA7AU
typedef enum {
SLOT_APP1 = 0,
SLOT_APP2 = 1,
} app_slot_t;
#endif
|
宏定义 |
值 |
说明 |
|
BL_FLASH_SIZE |
32 KB |
Bootloader 区 |
|
APP_SLOT_SIZE |
112 KB |
每个 App 槽大小 |
|
APP1_START_ADDR |
0x08008000 |
Slot A 起始地址 |
|
APP2_START_ADDR |
0x08024000 |
Slot B 起始地址 |
|
BOOT_META_ADDR |
0x0803F000 |
Boot 元数据区(Flash 末尾 4 KB) |
2.2 链接脚本对照
/* Bootloader — STM32F103RCTx_FLASH.ld */
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (xrw): ORIGIN = 0x20000000, LENGTH = 48K
/* App1 — STM32F103RCTx_APP1.ld */
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 112K
/* App2 — STM32F103RCTx_APP2.ld */
FLASH (rx) : ORIGIN = 0x08024000, LENGTH = 112K
【注意】App1 与 App2 源码相同,仅链接脚本 ORIGIN 不同。编译时通过 -T 参数或 CMake 变量切换。
3. Cortex-M 向量表与 VTOR 深度解析
3.1 向量表结构
|
偏移 |
内容 |
|
+0x00 |
初始 SP(栈顶) |
|
+0x04 |
Reset_Handler 地址 |
|
+0x08 |
NMI_Handler |
|
+0x0C |
HardFault_Handler |
|
... |
其他中断向量 |
Bootloader 位于 0x08000000,向量表天然正确。App 链接在 0x08008000 或 0x08024000,硬件默认仍从 0x08000000 取向量,必须在 App 中重定位 VTOR。
3.2 VTOR 寄存器
/* CMSIS: SCB->VTOR,bit[29:7] 为向量表基址(512 字节对齐) */
#define SCB_VTOR_Msk (0x1FFFFF80UL)
static inline void vtor_set(uint32_t base)
{
SCB->VTOR = base & SCB_VTOR_Msk;
}
/* system_stm32f1xx.c — App 工程 */
void SystemInit(void)
{
#if defined(APP_SLOT1)
SCB->VTOR = APP1_START_ADDR;
#elif defined(APP_SLOT2)
SCB->VTOR = APP2_START_ADDR;
#endif
}
3.3 Bootloader 跳转前的清理清单
void bl_jump_to_app(uint32_t app_addr)
{
uint32_t sp = *(__IO uint32_t *)app_addr;
uint32_t pc = *(__IO uint32_t *)(app_addr + 4U);
__disable_irq();
SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0;
for (uint32_t i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFFU;
NVIC->ICPR[i] = 0xFFFFFFFFU;
}
SCB->VTOR = app_addr;
if ((sp & 0x2FFE0000U) != 0x20000000U) return;
__set_MSP(sp);
((void (*)(void))pc)();
}
|
步骤 |
操作 |
目的 |
|
1 |
__disable_irq() |
关闭全局中断 |
|
2 |
关闭 SysTick |
清除定时器状态 |
|
3 |
清除 NVIC ICER/ICPR |
取消所有中断使能与挂起 |
|
4 |
SCB->VTOR = app_addr |
向量表指向 App 区域 |
|
5 |
校验 SP 在 RAM 范围 |
防止跳转到无效固件 |
|
6 |
__set_MSP + 跳转 PC |
进入 App Reset_Handler |
4. Boot 元数据与槽位切换
4.1 元数据结构
元数据单独占用 Flash 末尾 4 KB 页,与 App 区隔离:
typedef struct __attribute__((packed)) {
uint32_t magic; /* BOOT_MAGIC */
uint8_t active_slot; /* SLOT_APP1 or SLOT_APP2 */
uint8_t pending_slot; /* OTA 完成后待切换槽,0xFF=无 */
uint16_t reserved;
uint32_t app1_size;
uint32_t app1_crc32;
uint32_t app2_size;
uint32_t app2_crc32;
uint32_t boot_count; /* 启动计数,用于回滚判定 */
uint32_t crc_meta; /* 本结构体 CRC(不含此字段) */
} boot_meta_t;
|
字段 |
说明 |
|
active_slot |
当前运行槽(SLOT_APP1 / SLOT_APP2) |
|
pending_slot |
OTA 完成后待切换的槽,0xFF 表示无 |
|
app1/app2_size |
各槽有效固件字节数 |
|
app1/app2_crc32 |
各槽固件 CRC32 |
|
boot_count |
启动计数,配合回滚机制 |
|
crc_meta |
元数据自身完整性校验 |
4.2 读写实现要点
- boot_meta_load():从 BOOT_META_ADDR 读取,校验 magic 与 crc_meta
- 首次使用:magic 无效时初始化默认值(active=SLOT_APP1)并写入
- boot_meta_save():擦除元数据页 → 半字编程写入 → 更新 crc_meta
4.3 槽位切换策略
|
策略 |
流程 |
特点 |
|
A — 指针切换(推荐) |
OTA 写 inactive 槽 → 校验 → 更新 active_slot → 重启 |
不拷贝 Flash,速度快 |
|
B — 拷贝切换 |
OTA 写 App2 → 校验 → 拷贝覆盖 App1 → active=SLOT_APP1 |
兼容只认 App1 的旧 Bootloader |
本工程示例采用策略 A(指针切换)。
5. OTA 升级完整流程
5.1 状态机
Idle(正常运行)
→ Downloading(收到 OTA 命令,写入 inactive 槽)
→ Verifying(下载完成,计算 CRC)
→ PendingSwitch(CRC 通过,写 pending_slot)
→ Reboot(NVIC_SystemReset)
→ BootloaderRun(校验 pending,切换 active_slot)
→ Running(跳转新固件)
5.2 App 侧 OTA 接收框架
/* ota_receiver.c — 运行在 active App 内 */
int ota_begin(uint32_t firmware_size)
{
boot_meta_load(&meta);
g_ota.write_addr = inactive_slot_addr(&meta); /* 写非 active 槽 */
flash_erase_range(g_ota.write_addr, APP_SLOT_SIZE);
return 0;
}
int ota_write_chunk(const uint8_t *data, uint32_t len)
{
flash_program(g_ota.write_addr + g_ota.received, data, len);
g_ota.running_crc = crc32_update(g_ota.running_crc, data, len);
g_ota.received += len;
return 0;
}
int ota_finish(void)
{
meta.pending_slot = target; /* 标记待切换槽 */
boot_meta_save(&meta);
NVIC_SystemReset(); /* 重启,由 Bootloader 完成切换 */
return 0;
}
5.3 Bootloader 启动决策
bl_select_slot() 决策逻辑:
- 若有 pending_slot:校验 pending 槽 CRC → 通过则切换为 active
- 校验当前 active 槽 → 通过则跳转
- active 失败:尝试另一槽(fallback)
- 两槽均无效:进入 UART 救砖模式
int main(void)
{
HAL_Init();
boot_meta_load(&meta);
app_slot_t slot = bl_select_slot(&meta);
if (slot <= SLOT_APP2) {
bl_jump_to_app(slot_addr(slot));
}
recovery_mode_run(); /* 救砖:UART YMODEM */
return 0;
}
6. CRC 校验与简易签名
6.1 CRC32 实现
uint32_t crc32_compute(const uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFU;
for (uint32_t i = 0; i < len; i++) {
crc = (crc >> 8) ^ crc32_table[(crc ^ data[i]) & 0xFFU];
}
return crc ^ 0xFFFFFFFFU;
}
采用标准 IEEE 802.3 多项式 0xEDB88320 查表法实现。
6.2 固件头部(版本 + 签名)
#define FW_HEADER_MAGIC 0x46574D52U /* 'FWMR' */
typedef struct __attribute__((packed)) {
uint32_t magic;
uint32_t version; /* 如 0x00010203 = v1.2.3 */
uint32_t image_size;
uint32_t image_crc32;
uint32_t build_timestamp;
uint8_t signature[32]; /* HMAC-SHA256,可选 */
} firmware_header_t;
简易 HMAC 验证流程:
- PC 端对(header 不含 signature + image)计算 HMAC-SHA256
- 将 signature 写回头部,生成 .signed 固件
- Bootloader 内置相同密钥校验(生产环境应使用 Secure Boot + OTP 存密钥)
# tools/sign_firmware.py
sig = hmac.new(SECRET, header + image, hashlib.sha256).digest()
7. 回滚与故障恢复
7.1 启动计数回滚
新固件切换后,App 在 main() 早期调用 app_boot_confirm() 清零 boot_count;若连续 N 次启动未确认,Bootloader 自动回滚到另一槽。
/* App — 启动确认 */
void app_boot_confirm(void)
{
boot_meta_load(&meta);
meta.boot_count = 0;
boot_meta_save(&meta);
}
/* Bootloader — 递增计数,达阈值则回滚 */
if (meta->boot_count >= 3) {
meta->active_slot = 切换另一槽;
meta->boot_count = 0;
}
7.2 救砖模式(Recovery)
Bootloader 在双槽均无效时进入 UART 救砖:支持 YMODEM 接收 .bin 直写 App1,或通过 SWD 重新烧录(开发阶段)。
void recovery_mode_run(void)
{
uart_print("Bootloader Recovery Mode\r\n");
while (1) {
if (ymodem_receive(APP1_START_ADDR, APP_SLOT_SIZE) == 0) {
/* 更新 meta 并重启 */
NVIC_SystemReset();
}
}
}
8. 完整工程目录与源码
8.1 工程树
stm32-ab-bootloader/
├── Common/
│ ├── Inc/ memory_map.h, boot_meta.h, crc32.h, bl_jump.h
│ └── Src/ boot_meta.c, crc32.c, bl_jump.c
├── Bootloader/
│ ├── Core/Src/main.c
│ └── STM32F103RCTx_FLASH.ld
├── Application/
│ ├── Core/Src/main.c, ota_receiver.c
│ ├── STM32F103RCTx_APP1.ld
│ └── STM32F103RCTx_APP2.ld
├── tools/ sign_firmware.py, pack_ota.py, flash_all.sh
└── CMakeLists.txt
8.2 Application main.c 示例
int main(void)
{
HAL_Init();
SystemClock_Config();
app_boot_confirm(); /* 防止 Bootloader 回滚 */
GPIO_Init();
uart_init(115200);
#if defined(APP_SLOT1)
uart_print("Running on APP1\r\n");
#elif defined(APP_SLOT2)
uart_print("Running on APP2\r\n");
#endif
while (1) {
ota_poll_uart();
HAL_Delay(100);
}
}
8.3 CMake 构建片段
# App1:-DAPP_SLOT1 + STM32F103RCTx_APP1.ld
target_compile_definitions(app1.elf PRIVATE APP_SLOT1)
target_link_options(app1.elf PRIVATE
-T${CMAKE_SOURCE_DIR}/Application/STM32F103RCTx_APP1.ld)
# App2:同源码,-DAPP_SLOT2 + APP2.ld
target_compile_definitions(app2.elf PRIVATE APP_SLOT2)
target_link_options(app2.elf PRIVATE
-T${CMAKE_SOURCE_DIR}/Application/STM32F103RCTx_APP2.ld)
9. 编译、烧录与调试
9.1 首次烧录顺序
cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-arm-none-eabi.cmake
cmake --build build
# 1. 烧录 Bootloader
openocd ... -c "program build/bootloader.elf verify reset exit"
# 2. 烧录 App1(出厂固件)
openocd ... -c "program build/app1.bin 0x08008000 verify reset exit"
# 3. 初始化 Boot Meta
python tools/init_meta.py --app1 build/app1.bin
9.2 OTA 测试流程
- 设备运行 App1 v1.0.0
- PC 端 pack_ota.py 打包 app2.bin --version 1.1.0
- 通过 UART 发送 OTA 包
- 设备重启,Bootloader 校验 App2 并切换
- 串口打印 Running on APP2
9.3 调试要点
|
现象 |
可能原因 |
排查 |
|
HardFault 进 App |
VTOR 未设置 / SP 非法 |
查 SystemInit、链接脚本 ORIGIN |
|
跳转后无输出 |
UART 未重新初始化 |
App main() 内重新 init 外设 |
|
OTA 后仍跑旧版 |
meta 未更新 / pending 未处理 |
读 BOOT_META_ADDR 内容 |
|
CRC 失败 |
下载丢包 / size 不一致 |
对比 PC 端与设备端 CRC |
【内存查看】0x08000000 Bootloader 向量表 | 0x08008000 App1 向量表 | 0x0803F000 Boot Meta
10. 从双区迁移到三区
10.1 迁移步骤
- 重新规划链接脚本:新增 App2.ld,确认 App1 起始地址
- 增加 Boot Meta 区:确保不与 App 重叠(可能需缩小槽 4 KB)
- Bootloader 增加槽位逻辑:jump_to_app() 改为 bl_select_slot()
- App 增加 OTA 模块:写入 inactive 槽而非自覆盖
- 出厂流程:仍只烧 App1,App2 留空,Meta 默认 active=SLOT_APP1
10.2 兼容性对照
|
双区 |
三区 |
|
APP_START_ADDR |
APP1_START_ADDR + APP2_START_ADDR |
|
单 app.ld |
APP1.ld + APP2.ld |
|
OTA 直写 App 区 |
OTA 写 inactive 槽 |
|
无 meta |
boot_meta_t 管理槽位 |
|
无回滚 |
boot_count + 双槽 fallback |
11. 常见问题与排错
|
问题 |
解答 |
|
App1 和 App2 必须编译两份吗? |
源码一份,编译两次,分别指定 APP_SLOT1/2 宏和对应链接脚本 |
|
两个槽能链接到同一地址吗? |
不能,物理地址不同,向量表必须对应各自 ORIGIN |
|
RAM 要不要分区? |
一般不分,跳转前清 NVIC/SysTick,App 重新初始化 HAL |
|
F103C8(64KB)能做三区吗? |
空间极紧(约32+16+16KB),仅适合演示;推荐≥256KB Flash |
|
与 MCUboot 的关系? |
本文是裸机轻量方案;量产可迁移 MCUboot,分区思想一致 |
附录 A:Flash 擦写辅助函数
int flash_erase_range(uint32_t addr, uint32_t size)
{
/* F103 每页 2KB,逐页擦除 */
while (page_addr < end) {
HAL_FLASHEx_Erase(&erase, &err);
page_addr += 0x800U;
}
}
int flash_program(uint32_t addr, const uint8_t *data, uint32_t len)
{
/* 半字(16位)编程 */
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr + i, half);
}
附录 B:推荐阅读
- ARM Cortex-M3 TRM — 向量表与 VTOR 章节
- STM32F103 Reference Manual — Flash 模块
- AN2606 / AN3155 — STM32 内置 Bootloader 协议
- Bootloader 内存分配 · 入门篇(双区模型)
总结
|
主题 |
进阶要点 |
|
三区划分 |
Bootloader + App1 + App2,inactive 槽接收 OTA |
|
VTOR |
App 必须重定位;跳转前清 NVIC/SysTick |
|
Boot Meta |
持久化 active/pending 槽、CRC、启动计数 |
|
OTA |
下载 → 校验 → pending → 重启 → 切换 |
|
可靠性 |
双槽 fallback + boot_count 回滚 + UART 救砖 |
掌握以上内容后,可在产品级 STM32 项目上实现安全的 A/B OTA,并根据 Flash 容量灵活调整槽位大小。
更多推荐

所有评论(0)