Bootloader 内存分配进阶:三区 A/B 分区与 OTA 实战(STM32)

系列:Bootloader 内存分配 · 进阶篇
平台:STM32F103RCT6(256 KB Flash / 48 KB RAM)
工具链:GCC ARM + CMake / STM32CubeIDE 均可
前置:已理解双区模型(Bootloader + App)、链接脚本与 VTOR 基本概念


目录

  1. 进阶目标与整体架构
  2. Flash 三区内存映射
  3. Cortex-M 向量表与 VTOR 深度解析
  4. Boot 元数据与槽位切换
  5. OTA 升级完整流程
  6. CRC 校验与简易签名
  7. 回滚与故障恢复
  8. 完整工程目录与源码
  9. 编译、烧录与调试
  10. 从双区迁移到三区
  11. 常见问题与排错

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 系统数据流

上电

Bootloader

读取 Boot Meta

CRC/签名 OK?

跳转 Active Slot

尝试另一 Slot

Application 运行

OTA 下载

写入 App2

重启进 Bootloader

校验并切换 Active


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 状态机

正常运行

收到 OTA 命令

下载完成

CRC 通过

CRC 失败

写 pending_slot

硬件复位

校验 pending

切换 active_slot 并跳转

Idle

Downloading

Verifying

PendingSwitch

Reboot

BootloaderRun

ActiveSwitch

Running

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 验证流程(无硬件加密模块时):

  1. 编译后在 PC 端用 Python 对 (header 不含 signature + image) 计算 HMAC-SHA256
  2. 将 signature 写回头部
  3. 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 测试流程

  1. 设备运行 App1 v1.0.0
  2. PC 端 python tools/pack_ota.py app2.bin --version 1.1.0
  3. 通过 UART 发送 OTA 包
  4. 设备重启,Bootloader 校验 App2 并切换
  5. 串口打印 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 迁移步骤

  1. 重新规划链接脚本:原 App 起始地址若与 App1 相同(0x08008000),App1 无需改代码,仅新增 App2 链接脚本
  2. 增加 Boot Meta 区:确保不与现有 App 重叠(可能需要缩小 App 槽 4 KB)
  3. Bootloader 增加槽位逻辑:原 jump_to_app(APP_START) 改为 bl_select_slot()
  4. App 增加 OTA 模块:写入 inactive 槽而非自覆盖
  5. 出厂流程:仍只烧 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 · 作者可自由转载注明出处

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐