【无标题】
Bootloader 内存分配进阶:三区 A/B 分区与 OTA 实战(STM32)
系列:Bootloader 内存分配 · 进阶篇
平台:STM32F103RCT6(256 KB Flash / 48 KB RAM)
工具链:GCC ARM + CMake / STM32CubeIDE 均可
前置:已理解双区模型(Bootloader + App)、链接脚本与VTOR基本概念
目录
- 进阶目标与整体架构
- Flash 三区内存映射
- Cortex-M 向量表与 VTOR 深度解析
- Boot 元数据与槽位切换
- OTA 升级完整流程
- CRC 校验与简易签名
- 回滚与故障恢复
- 完整工程目录与源码
- 编译、烧录与调试
- 从双区迁移到三区
- 常见问题与排错
1. 进阶目标与整体架构
1.1 双区的局限
双区方案(Bootloader + App)在 OTA 时存在明显风险:
| 问题 | 说明 |
|---|---|
| 擦写中断 | 升级过程中断电,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 系统数据流
2. Flash 三区内存映射
2.1 统一头文件 memory_map.h
所有工程(Bootloader / App1 / App2)必须引用同一份地址定义,避免链接地址与跳转地址不一致。
#ifndef MEMORY_MAP_H
#define MEMORY_MAP_H
#include <stdint.h>
/* ---- Flash 基址 ---- */
#define FLASH_BASE_ADDR 0x08000000U
/* ---- 分区大小 ---- */
#define BL_FLASH_SIZE (32U * 1024U) /* Bootloader: 32 KB */
#define APP_SLOT_SIZE (112U * 1024U) /* 每个 App 槽: 112 KB */
/* ---- 分区起始地址 ---- */
#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)
/* ---- 向量表偏移(相对 Flash 基址)---- */
#define APP1_VTOR_OFFSET (APP1_START_ADDR - FLASH_BASE_ADDR)
#define APP2_VTOR_OFFSET (APP2_START_ADDR - FLASH_BASE_ADDR)
/* ---- Boot 元数据区(放在 Flash 末尾 4KB)---- */
#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 /* MEMORY_MAP_H */
2.2 链接脚本对照
Bootloader — STM32F103RCTx_FLASH.ld
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 48K
}
App1 — STM32F103RCTx_APP1.ld
MEMORY
{
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 112K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 48K
}
App2 — STM32F103RCTx_APP2.ld
MEMORY
{
FLASH (rx) : ORIGIN = 0x08024000, LENGTH = 112K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 48K
}
注意:App1 与 App2 源码相同,仅链接脚本
ORIGIN不同。编译时通过-T参数或 CMake 变量切换。
3. Cortex-M 向量表与 VTOR 深度解析
3.1 向量表结构
Cortex-M3 复位后从地址 0x0800_0000 取:
| 偏移 | 内容 |
|---|---|
| +0x00 | 初始 SP(栈顶) |
| +0x04 | Reset_Handler 地址 |
| +0x08 | NMI_Handler |
| +0x0C | HardFault_Handler |
| … | 其他中断向量 |
Bootloader 位于 0x0800_0000,其向量表天然正确。
App 链接在 0x0800_8000,硬件仍从 0x0800_0000 取向量 — 必须在 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;
}
App 在 main() 之前通过 SystemInit() 设置:
/* 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 跳转前的清理清单
跳转 App 前必须完成以下步骤,否则 App 可能 HardFault 或中断异常:
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);
/* 1. 关闭全局中断 */
__disable_irq();
/* 2. 关闭 SysTick */
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
/* 3. 清除所有 NVIC 使能与挂起 */
for (uint32_t i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFFU;
NVIC->ICPR[i] = 0xFFFFFFFFU;
}
/* 4. 设置 VTOR(App 的 SystemInit 会再次设置,此处双保险) */
SCB->VTOR = app_addr;
/* 5. 校验 SP 在 RAM 范围内 */
if ((sp & 0x2FFE0000U) != 0x20000000U) {
return; /* SP 非法,拒绝跳转 */
}
/* 6. 设置 MSP 并跳转 */
__set_MSP(sp);
((void (*)(void))pc)();
}
4. Boot 元数据与槽位切换
4.1 元数据结构
元数据单独占用 Flash 末尾 4 KB 页,与 App 区隔离:
/* boot_meta.h */
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; /* App1 有效固件字节数 */
uint32_t app1_crc32;
uint32_t app2_size;
uint32_t app2_crc32;
uint32_t boot_count; /* 启动计数,用于回滚判定 */
uint32_t crc_meta; /* 本结构体 CRC(不含此字段) */
} boot_meta_t;
4.2 读写实现(STM32F1 Flash)
/* boot_meta.c */
#include "boot_meta.h"
#include "memory_map.h"
#include "stm32f1xx_hal.h"
#include "crc32.h"
static boot_meta_t s_meta_cache;
static uint32_t meta_calc_crc(const boot_meta_t *m)
{
return crc32_compute((const uint8_t *)m,
sizeof(boot_meta_t) - sizeof(uint32_t));
}
int boot_meta_load(boot_meta_t *out)
{
const boot_meta_t *flash_meta = (const boot_meta_t *)BOOT_META_ADDR;
if (flash_meta->magic != BOOT_MAGIC) {
/* 首次使用:默认 App1 为 active */
memset(&s_meta_cache, 0, sizeof(s_meta_cache));
s_meta_cache.magic = BOOT_MAGIC;
s_meta_cache.active_slot = SLOT_APP1;
s_meta_cache.pending_slot = 0xFF;
s_meta_cache.crc_meta = meta_calc_crc(&s_meta_cache);
boot_meta_save(&s_meta_cache);
}
memcpy(out, (void *)BOOT_META_ADDR, sizeof(boot_meta_t));
if (meta_calc_crc(out) != out->crc_meta) {
return -1; /* 元数据损坏 */
}
return 0;
}
int boot_meta_save(const boot_meta_t *in)
{
boot_meta_t tmp = *in;
tmp.crc_meta = meta_calc_crc(&tmp);
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_PAGES,
.PageAddress = BOOT_META_ADDR,
.NbPages = 1,
};
uint32_t page_err;
if (HAL_FLASHEx_Erase(&erase, &page_err) != HAL_OK) {
HAL_FLASH_Lock();
return -1;
}
const uint16_t *src = (const uint16_t *)&tmp;
for (uint32_t i = 0; i < sizeof(tmp); i += 2) {
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,
BOOT_META_ADDR + i, src[i / 2]) != HAL_OK) {
HAL_FLASH_Lock();
return -1;
}
}
HAL_FLASH_Lock();
return 0;
}
4.3 槽位切换策略
策略 A — 指针切换(推荐,速度快)
OTA 完成后只更新 active_slot,不拷贝 Flash:
1. 新固件下载到 App2
2. 校验 App2 CRC
3. meta.active_slot = SLOT_APP2
4. 重启 → Bootloader 跳 App2
策略 B — 拷贝切换(兼容旧 Bootloader 只认 App1)
1. 新固件下载到 App2
2. 校验通过后,Bootloader 将 App2 拷贝覆盖 App1
3. meta.active_slot = SLOT_APP1
4. 重启 → 始终从 App1 启动
本工程示例采用 策略 A。
5. OTA 升级完整流程
5.1 状态机
5.2 App 侧 OTA 接收框架
/* ota_receiver.c — 运行在 active App 内 */
#include "memory_map.h"
#include "boot_meta.h"
#include "crc32.h"
#include "stm32f1xx_hal.h"
#define OTA_CHUNK_SIZE 256
typedef struct {
uint32_t total_size;
uint32_t received;
uint32_t write_addr; /* 始终写入非 active 槽 */
uint32_t running_crc;
} ota_ctx_t;
static ota_ctx_t g_ota;
static uint32_t inactive_slot_addr(const boot_meta_t *meta)
{
return (meta->active_slot == SLOT_APP1) ? APP2_START_ADDR : APP1_START_ADDR;
}
int ota_begin(uint32_t firmware_size)
{
boot_meta_t meta;
boot_meta_load(&meta);
if (firmware_size == 0 || firmware_size > APP_SLOT_SIZE) {
return -1;
}
g_ota.total_size = firmware_size;
g_ota.received = 0;
g_ota.write_addr = inactive_slot_addr(&meta);
g_ota.running_crc = 0xFFFFFFFFU;
/* 擦除 inactive 槽对应页 */
flash_erase_range(g_ota.write_addr, APP_SLOT_SIZE);
return 0;
}
int ota_write_chunk(const uint8_t *data, uint32_t len)
{
if (g_ota.received + len > g_ota.total_size) {
return -1;
}
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)
{
boot_meta_t meta;
boot_meta_load(&meta);
uint32_t crc = g_ota.running_crc ^ 0xFFFFFFFFU;
app_slot_t target = (meta.active_slot == SLOT_APP1) ? SLOT_APP2 : SLOT_APP1;
if (target == SLOT_APP1) {
meta.app1_size = g_ota.total_size;
meta.app1_crc32 = crc;
} else {
meta.app2_size = g_ota.total_size;
meta.app2_crc32 = crc;
}
meta.pending_slot = (uint8_t)target;
boot_meta_save(&meta);
/* 请求重启,由 Bootloader 完成最终切换 */
NVIC_SystemReset();
return 0;
}
5.3 Bootloader 启动决策
/* main.c — Bootloader */
#include "memory_map.h"
#include "boot_meta.h"
#include "crc32.h"
#include "bl_jump.h"
static uint32_t slot_addr(app_slot_t slot)
{
return (slot == SLOT_APP1) ? APP1_START_ADDR : APP2_START_ADDR;
}
static int slot_verify(app_slot_t slot, uint32_t size, uint32_t expect_crc)
{
if (size == 0 || size > APP_SLOT_SIZE) {
return -1;
}
uint32_t calc = crc32_compute((const uint8_t *)slot_addr(slot), size);
return (calc == expect_crc) ? 0 : -1;
}
static app_slot_t bl_select_slot(boot_meta_t *meta)
{
/* 若有 pending,先校验 pending 槽 */
if (meta->pending_slot <= SLOT_APP2) {
app_slot_t p = (app_slot_t)meta->pending_slot;
uint32_t sz = (p == SLOT_APP1) ? meta->app1_size : meta->app2_size;
uint32_t crc = (p == SLOT_APP1) ? meta->app1_crc32 : meta->app2_crc32;
if (slot_verify(p, sz, crc) == 0) {
meta->active_slot = (uint8_t)p;
meta->pending_slot = 0xFF;
meta->boot_count = 0;
boot_meta_save(meta);
return p;
}
}
/* 校验 active 槽 */
app_slot_t a = (app_slot_t)meta->active_slot;
uint32_t sz = (a == SLOT_APP1) ? meta->app1_size : meta->app2_size;
uint32_t crc = (a == SLOT_APP1) ? meta->app1_crc32 : meta->app2_crc32;
if (slot_verify(a, sz, crc) == 0) {
return a;
}
/* active 失败,尝试另一槽 */
app_slot_t fb = (a == SLOT_APP1) ? SLOT_APP2 : SLOT_APP1;
sz = (fb == SLOT_APP1) ? meta->app1_size : meta->app2_size;
crc = (fb == SLOT_APP1) ? meta->app1_crc32 : meta->app2_crc32;
if (slot_verify(fb, sz, crc) == 0) {
meta->active_slot = (uint8_t)fb;
boot_meta_save(meta);
return fb;
}
/* 两槽均无效 — 停留 Bootloader,等待 UART 救砖 */
return (app_slot_t)0xFF;
}
int main(void)
{
HAL_Init();
boot_meta_t meta;
boot_meta_load(&meta);
app_slot_t slot = bl_select_slot(&meta);
if (slot <= SLOT_APP2) {
bl_jump_to_app(slot_addr(slot));
}
/* 救砖模式:UART YMODEM / 串口命令 */
recovery_mode_run();
return 0;
}
6. CRC 校验与简易签名
6.1 CRC32 实现
/* crc32.c */
static const uint32_t crc32_table[256] = {
/* 标准 IEEE 802.3 多项式 0xEDB88320 表 — 省略 256 项,见工程源码 */
};
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;
}
uint32_t crc32_update(uint32_t crc, const uint8_t *data, uint32_t len)
{
for (uint32_t i = 0; i < len; i++) {
crc = (crc >> 8) ^ crc32_table[(crc ^ data[i]) & 0xFFU];
}
return crc;
}
6.2 固件头部(进阶:版本 + 签名)
在 App 二进制开头增加固定头部,Bootloader 先校验头部再校验全镜像:
/* firmware_header.h */
#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 或 ECDSA 截断,可选 */
} firmware_header_t;
简易 HMAC 验证流程(无硬件加密模块时):
- 编译后在 PC 端用 Python 对
(header 不含 signature + image)计算 HMAC-SHA256 - 将 signature 写回头部
- Bootloader 内置相同密钥(生产环境应使用 Secure Boot + 密钥存 OTP)
# tools/sign_firmware.py
import hmac, hashlib, struct
SECRET = b"your-32-byte-secret-key-here!!!"
def sign_firmware(bin_path, version):
with open(bin_path, "rb") as f:
image = f.read()
image_crc = zlib.crc32(image) & 0xFFFFFFFF
header = struct.pack("<IIII", 0x46574D52, version, len(image), image_crc)
sig = hmac.new(SECRET, header + image, hashlib.sha256).digest()
with open(bin_path + ".signed", "wb") as f:
f.write(header + sig + image)
7. 回滚与故障恢复
7.1 启动计数回滚(Watchdog 配合)
新固件切换后,App 在 main() 早期向 Boot Meta 写入「确认标志」;若连续 N 次启动未确认,Bootloader 自动回滚:
/* App main.c — 启动确认 */
void app_boot_confirm(void)
{
boot_meta_t meta;
boot_meta_load(&meta);
meta.boot_count = 0; /* 清零 = 已稳定运行 */
boot_meta_save(&meta);
}
/* Bootloader — 每次从未确认状态启动时递增 */
void bl_increment_boot_count(boot_meta_t *meta)
{
meta->boot_count++;
if (meta->boot_count >= 3) {
/* 连续 3 次未能确认 → 回滚到另一槽 */
meta->active_slot = (meta->active_slot == SLOT_APP1) ? SLOT_APP2 : SLOT_APP1;
meta->boot_count = 0;
meta->pending_slot = 0xFF;
}
boot_meta_save(meta);
}
7.2 救砖模式(Recovery)
Bootloader 在双槽均无效时进入 UART 救砖:
- 支持 YMODEM 接收
.bin直写 App1 - 或通过 SWD 重新烧录(开发阶段)
void recovery_mode_run(void)
{
uart_init(115200);
uart_print("Bootloader Recovery Mode\r\n");
while (1) {
if (ymodem_receive(APP1_START_ADDR, APP_SLOT_SIZE) == 0) {
/* 更新 meta 并重启 */
boot_meta_t meta = { .magic = BOOT_MAGIC, .active_slot = SLOT_APP1 };
meta.app1_size = ymodem_last_size;
meta.app1_crc32 = crc32_compute((uint8_t *)APP1_START_ADDR, meta.app1_size);
meta.crc_meta = meta_calc_crc(&meta);
boot_meta_save(&meta);
NVIC_SystemReset();
}
}
}
8. 完整工程目录与源码
8.1 工程树
stm32-ab-bootloader/
├── Common/
│ ├── Inc/
│ │ ├── memory_map.h
│ │ ├── boot_meta.h
│ │ ├── crc32.h
│ │ ├── firmware_header.h
│ │ └── bl_jump.h
│ └── Src/
│ ├── boot_meta.c
│ ├── crc32.c
│ └── bl_jump.c
├── Bootloader/
│ ├── Core/
│ │ ├── Inc/main.h
│ │ └── Src/main.c
│ ├── Drivers/ /* STM32 HAL,CubeMX 生成 */
│ └── STM32F103RCTx_FLASH.ld
├── Application/
│ ├── Core/
│ │ ├── Inc/main.h
│ │ └── Src/
│ │ ├── main.c
│ │ └── ota_receiver.c
│ ├── Drivers/
│ ├── STM32F103RCTx_APP1.ld
│ └── STM32F103RCTx_APP2.ld
├── tools/
│ ├── sign_firmware.py
│ ├── pack_ota.py
│ └── flash_all.sh
├── CMakeLists.txt
└── README.md
8.2 bl_jump.c
#include "bl_jump.h"
#include "stm32f1xx.h"
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 < 8U; i++) {
NVIC->ICER[i] = 0xFFFFFFFFU;
NVIC->ICPR[i] = 0xFFFFFFFFU;
}
SCB->VTOR = app_addr;
if ((sp & 0x2FFE0000U) != 0x20000000U) {
return;
}
__set_MSP(sp);
((void (*)(void))pc)();
}
8.3 Application main.c 示例
#include "main.h"
#include "memory_map.h"
#include "boot_meta.h"
#include "ota_receiver.h"
/* 编译 App1 时: -DAPP_SLOT1 ; 编译 App2 时: -DAPP_SLOT2 */
int main(void)
{
HAL_Init();
SystemClock_Config();
/* 确认启动成功,防止 Bootloader 回滚 */
app_boot_confirm();
GPIO_Init(); /* LED、UART 等 */
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 命令处理 */
ota_poll_uart();
HAL_Delay(100);
}
}
8.4 CMake 构建片段
# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(stm32_ab_bootloader C ASM)
set(MCU_FLAGS -mcpu=cortex-m3 -mthumb)
# ---- Bootloader ----
add_executable(bootloader.elf
Bootloader/Core/Src/main.c
Common/Src/bl_jump.c
Common/Src/boot_meta.c
Common/Src/crc32.c
# ... HAL 源文件
)
target_include_directories(bootloader.elf PRIVATE Common/Inc Bootloader/Core/Inc)
target_compile_options(bootloader.elf PRIVATE ${MCU_FLAGS})
target_link_options(bootloader.elf PRIVATE
-T${CMAKE_SOURCE_DIR}/Bootloader/STM32F103RCTx_FLASH.ld
${MCU_FLAGS} -Wl,--gc-sections
)
# ---- Application Slot 1 ----
add_executable(app1.elf Application/Core/Src/main.c ...)
target_compile_definitions(app1.elf PRIVATE APP_SLOT1)
target_link_options(app1.elf PRIVATE
-T${CMAKE_SOURCE_DIR}/Application/STM32F103RCTx_APP1.ld
)
# ---- Application Slot 2(同源码,不同链接脚本与宏)----
add_executable(app2.elf Application/Core/Src/main.c ...)
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 首次烧录顺序
# 1. 编译
cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/gcc-arm-none-eabi.cmake
cmake --build build
# 2. 烧录 Bootloader
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
-c "program build/bootloader.elf verify reset exit"
# 3. 烧录 App1(首次出厂固件)
openocd ... -c "program build/app1.bin 0x08008000 verify reset exit"
# 4. 写入 Boot Meta(或通过 Bootloader 首次启动自动初始化)
python tools/init_meta.py --app1 build/app1.bin
9.2 OTA 测试流程
- 设备运行 App1 v1.0.0
- PC 端
python tools/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 |
使用 ST-Link Utility 查看内存:
0x08000000 Bootloader 向量表
0x08008000 App1 向量表(前两 word = SP, Reset)
0x0803F000 Boot Meta
10. 从双区迁移到三区
10.1 迁移步骤
- 重新规划链接脚本:原 App 起始地址若与 App1 相同(
0x08008000),App1 无需改代码,仅新增 App2 链接脚本 - 增加 Boot Meta 区:确保不与现有 App 重叠(可能需要缩小 App 槽 4 KB)
- Bootloader 增加槽位逻辑:原
jump_to_app(APP_START)改为bl_select_slot() - App 增加 OTA 模块:写入 inactive 槽而非自覆盖
- 出厂流程:仍只烧 App1,App2 留空,Meta 默认
active=SLOT_APP1
10.2 兼容性对照
| 双区 | 三区 |
|---|---|
APP_START_ADDR |
APP1_START_ADDR |
单 -T app.ld |
APP1.ld + APP2.ld |
| OTA 直写 App 区 | OTA 写 inactive 槽 |
| 无 meta | boot_meta_t 管理槽位 |
| 无回滚 | boot_count + 双槽 fallback |
11. 常见问题与排错
Q1:App1 和 App2 必须编译两份吗?
源码一份即可,编译两次,分别指定 APP_SLOT1/APP_SLOT2 宏和对应链接脚本。生成的 bin 内容除链接地址外相同。
Q2:能否两个槽都链接到同一地址?
不能。两个槽物理地址不同,向量表与符号地址必须对应各自 ORIGIN。
Q3:RAM 要不要分区?
一般不分。跳转前 Bootloader 清理 NVIC/SysTick,App 重新初始化 HAL 即可。注意不要依赖 Bootloader 留在 RAM 中的数据。
Q4:F103C8(64 KB Flash)能做三区吗?
空间极紧(32+16+16 KB),仅适合演示。生产推荐 ≥ 256 KB Flash 的型号(F103RC、F401、F411 等)。
Q5:与 MCUboot、FreeRTOS+OTA 的关系?
本文是裸机轻量方案,便于理解原理。量产可迁移到 MCUboot 等成熟框架,分区思想一致。
附录 A:Flash 擦写辅助函数
/* flash_ops.c */
#include "stm32f1xx_hal.h"
int flash_erase_range(uint32_t addr, uint32_t size)
{
HAL_FLASH_Unlock();
uint32_t page_addr = addr;
uint32_t end = addr + size;
while (page_addr < end) {
FLASH_EraseInitTypeDef erase = {
.TypeErase = FLASH_TYPEERASE_PAGES,
.PageAddress = page_addr,
.NbPages = 1,
};
uint32_t err;
if (HAL_FLASHEx_Erase(&erase, &err) != HAL_OK) {
HAL_FLASH_Lock();
return -1;
}
page_addr += 0x800U; /* F103 每页 2 KB */
}
HAL_FLASH_Lock();
return 0;
}
int flash_program(uint32_t addr, const uint8_t *data, uint32_t len)
{
HAL_FLASH_Unlock();
for (uint32_t i = 0; i < len; i += 2) {
uint16_t half = data[i] | ((i + 1 < len) ? (data[i + 1] << 8) : 0);
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr + i, half) != HAL_OK) {
HAL_FLASH_Lock();
return -1;
}
}
HAL_FLASH_Lock();
return 0;
}
附录 B:推荐阅读
- ARM Cortex-M3 TRM — 向量表与 VTOR 章节
- STM32F103 Reference Manual — Flash 模块 FMCR
- AN2606 / AN3155 — STM32 内置 Bootloader 协议(可参考 UART ISP)
- STM32 双区 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 容量灵活调整槽位大小。
文档版本:v1.0 · 平台 STM32F103RCT6 · 作者可自由转载注明出处
更多推荐



所有评论(0)