Bootloader 内存分配 · 入门篇

系列:Bootloader 内存分配 · 入门篇

平台:STM32F103C8T6(64 KB Flash / 20 KB RAM)

下一篇:进阶篇 — 三区 A/B 分区与 OTA

本文介绍 STM32 双区 Bootloader 方案:将 Flash 划分为 Bootloader 区与 Application 区,通过链接脚本、向量表偏移和跳转逻辑实现上电后从 Bootloader 进入 App 运行。

目录

  1. 为什么需要 Bootloader
  2. Flash 双区内存模型
  3. 链接脚本配置
  4. 向量表偏移 VTOR
  5. Bootloader 跳转 App
  6. 完整工程示例
  7. 编译与烧录
  8. 常见问题

1. 为什么需要 Bootloader

Bootloader 是芯片上电后首先运行的小程序,常见职责包括:

  • 通过 UART / USB / CAN 接收新固件并写入 Flash(ISP / OTA)
  • 校验 App 完整性后跳转到 App 运行
  • 提供出厂测试、序列号写入等能力

App 与 Bootloader 不能共用同一段 Flash,否则互相覆盖。解决方案是将 Flash 物理分区,各自链接到不同的起始地址。

2. Flash 双区内存模型

以 STM32F103C8(64 KB Flash)为例,典型划分如下:

0x0800_0000  ┌─────────────────────┐
             │  Bootloader  16 KB  │  上电入口
0x0800_4000  ├─────────────────────┤
             │  Application 48 KB  │  业务固件
0x0801_0000  └─────────────────────┘

2.1 统一地址定义 memory_map.h

Bootloader 与 Application 工程共用同一份地址宏定义,避免硬编码不一致:

#ifndef MEMORY_MAP_H
#define MEMORY_MAP_H

#define FLASH_BASE_ADDR     0x08000000U
#define BL_FLASH_SIZE       (16U * 1024U)
#define APP_FLASH_SIZE      (48U * 1024U)

#define BL_START_ADDR       FLASH_BASE_ADDR
#define APP_START_ADDR      (BL_START_ADDR + BL_FLASH_SIZE)
#define APP_VTOR_OFFSET     (APP_START_ADDR - FLASH_BASE_ADDR)

#endif

宏定义

说明

FLASH_BASE_ADDR

0x08000000

STM32 Flash 起始地址

BL_FLASH_SIZE

16 KB

Bootloader 占用空间

APP_FLASH_SIZE

48 KB

Application 可用空间

APP_START_ADDR

0x08004000

App 链接与烧录起始地址

APP_VTOR_OFFSET

0x4000

App 向量表相对 Flash 基址偏移

3. 链接脚本配置

链接脚本决定编译器将代码和数据放置到 Flash/RAM 的哪个地址。Bootloader 与 App 必须使用不同的链接脚本。

3.1 Bootloader — STM32F103C8Tx_BL.ld

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 16K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}

3.2 Application — STM32F103C8Tx_APP.ld

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08004000, LENGTH = 48K
  RAM   (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}

【关键】App 链接脚本中 FLASH 的 ORIGIN 必须等于 memory_map.h 中的 APP_START_ADDR,否则中断向量表与代码地址全部错位,运行后将 HardFault。

工程

FLASH ORIGIN

FLASH LENGTH

RAM

Bootloader

0x08000000

16 KB

20 KB(与 App 共用)

Application

0x08004000

48 KB

20 KB(与 BL 共用)

RAM 由两个工程共用同一段物理内存(0x20000000 起 20 KB)。跳转时 Bootloader 会重新设置 MSP,App 启动后使用自己的栈,互不冲突。

4. 向量表偏移 VTOR

Cortex-M 内核上电后默认从 0x08000000 读取向量表(栈顶指针 SP 与复位向量 PC)。App 链接在 0x08004000,必须在启动早期将 VTOR 指向 App 起始地址,否则所有中断仍会跳转到 Bootloader 区域的向量表,导致运行异常。

4.1 在 SystemInit 中设置

/* system_stm32f1xx.c — App 工程 */
#include "memory_map.h"

void SystemInit(void)
{
    SCB->VTOR = APP_START_ADDR;
}

4.2 或在 main() 最开始设置

int main(void)
{
    SCB->VTOR = APP_START_ADDR;
    __enable_irq();
    /* ... */
}

【说明】向量表第一个 word 为初始栈指针(MSP),第二个 word 为 Reset_Handler 地址。VTOR 设置后,中断发生时 CPU 从 App 区域的向量表取中断服务函数地址。

5. Bootloader 跳转 App

从 Bootloader 跳转到 App 不能简单调用函数,需要完成以下步骤:关闭中断、清除 SysTick、清除 NVIC 挂起、设置 VTOR、校验栈指针合法性、设置 MSP、跳转到 App 复位向量。

5.1 跳转函数 bl_jump.c

/* bl_jump.h / bl_jump.c */
#include "memory_map.h"
#include "stm32f1xx.h"

void 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)();
}

5.2 跳转步骤说明

步骤

操作

目的

1

读取 app_addr 处 SP 和 PC

获取 App 向量表前两个 word

2

__disable_irq()

防止跳转过程中中断干扰

3

关闭 SysTick

清除 Bootloader 遗留的定时器状态

4

清除 NVIC ICER/ICPR

取消所有挂起和使能的中断

5

SCB->VTOR = app_addr

将向量表指向 App 区域

6

校验 SP 是否在 RAM 范围

防止跳转到无效 App

7

__set_MSP(sp)

设置主栈指针

8

调用 pc 函数指针

跳转到 App Reset_Handler

5.3 Bootloader main.c

#include "bl_jump.h"
#include "memory_map.h"

int main(void)
{
    HAL_Init();

    /* 可选:简单校验 App 栈顶是否合法 */
    uint32_t sp = *(__IO uint32_t *)APP_START_ADDR;
    if ((sp & 0x2FFE0000U) == 0x20000000U) {
        jump_to_app(APP_START_ADDR);
    }

    /* 校验失败或未烧 App:进入 UART 升级模式 */
    uart_isp_mode();
    return 0;
}

上电流程:HAL 初始化 → 检查 App 区栈顶是否合法 → 合法则跳转 App,否则进入 ISP 升级模式等待接收固件。

6. 完整工程示例

6.1 目录结构

stm32-dual-bootloader/
├── Common/
│   ├── Inc/memory_map.h
│   └── Src/bl_jump.c
├── Bootloader/
│   ├── Core/Src/main.c
│   ├── STM32F103C8Tx_BL.ld
│   └── Drivers/          /* HAL,CubeMX 生成 */
├── Application/
│   ├── Core/Src/main.c
│   ├── STM32F103C8Tx_APP.ld
│   └── Drivers/
└── README.md

目录/文件

说明

Common/memory_map.h

Bootloader 与 App 共用的地址宏定义

Common/bl_jump.c

跳转 App 的公共实现

Bootloader/

16 KB 区工程,上电入口,含 ISP 逻辑

Application/

48 KB 区工程,业务固件

*_BL.ld / *_APP.ld

各自独立的链接脚本

6.2 Application main.c

#include "main.h"
#include "memory_map.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();

    char msg[] = "Hello from APP!\r\n";
    HAL_UART_Transmit(&huart1, (uint8_t *)msg, sizeof(msg) - 1, 100);

    while (1) {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        HAL_Delay(500);
    }
}

6.3 Bootloader ISP 框架(UART 接收示意)

void uart_isp_mode(void)
{
    uart_init(115200);
    uart_print("Bootloader ISP Mode\r\n");

    while (1) {
        if (receive_firmware_to_flash(APP_START_ADDR, APP_FLASH_SIZE) == 0) {
            uart_print("Update OK, rebooting...\r\n");
            HAL_Delay(100);
            jump_to_app(APP_START_ADDR);
        }
    }
}

receive_firmware_to_flash() 需实现协议解析、Flash 擦写与校验逻辑,属于 ISP/OTA 的具体实现,本文仅给出框架示意。

7. 编译与烧录

7.1 编译顺序

分别编译两个工程,各自使用对应的链接脚本:

# Bootloader
arm-none-eabi-gcc ... -T STM32F103C8Tx_BL.ld -o bootloader.elf

# Application
arm-none-eabi-gcc ... -T STM32F103C8Tx_APP.ld -o application.elf

7.2 烧录顺序

必须先烧录 Bootloader,再烧录 App。两者地址不同,可分开烧录:

# ST-Link / OpenOCD — 烧录 Bootloader
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
  -c "program bootloader.elf verify reset exit"

# 烧录 Application 到 0x08004000
openocd ... \
  -c "program application.bin 0x08004000 verify reset exit"

【注意】烧录 App 时必须指定起始地址 0x08004000,若误烧到 0x08000000 将覆盖 Bootloader。

7.3 验证方法

  1. 复位后 LED 闪烁(App 运行)→ 跳转成功
  2. 用调试器查看 0x08004000:前两 word 应为合法 SP 与 Reset_Handler
  3. 若出现 HardFault:检查 SCB->VTOR 与链接脚本 ORIGIN 是否一致
  4. 串口输出 Hello from APP! → App 正常初始化

8. 常见问题

问题

原因

解决方案

App HardFault

VTOR 未设置

App 内设置 SCB->VTOR = APP_START_ADDR

跳转后死机

SP 不在 RAM 范围

检查 App 链接脚本与栈配置

中断不触发

VTOR 仍指向 Bootloader 区

确认 App 的 SystemInit 已设置 VTOR

烧 App 后 Bootloader 没了

烧录地址错误

App 必须烧到 0x08004000

OTA 断电变砖

直接覆盖 App 区

见进阶篇 A/B 分区方案

跳转后外设异常

Bootloader 未清除外设状态

跳转前关闭 SysTick、清除 NVIC

App 能跑但中断错乱

VTOR 与链接地址不一致

核对 memory_map.h 与 .ld 文件

总结

双区 Bootloader 方案核心三步:

  1. 分区:Bootloader 与 App 使用不同 Flash 起始地址(16 KB + 48 KB)
  2. 链接:各自 .ld 文件 ORIGIN 与 memory_map.h 保持一致
  3. 跳转:Bootloader 清中断、设 VTOR、改 MSP/PC 进入 App

该方案适合 Flash 较小、升级需求简单的项目。若需要安全 OTA 与断电回滚,可采用进阶篇的三区 A/B 分区方案。

Logo

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

更多推荐