STM32F4 OTA Bootloader 图文实战教程
一、概述
Bootloader 是一段运行在 MCU 上电后最先执行的引导程序。它的核心职责是:在必要时完成固件更新,在正常情况下将控制权移交给用户应用程序(APP)。OTA(Over-The-Air)升级则指设备在运行过程中,通过某种通信接口(如串口、以太网、Wi-Fi、CAN 等)接收新固件,并在下次重启时由 Bootloader 将新固件刷入 Flash,完成版本迭代。
本文以 STM32F407(1MB Flash)为例,讲解如何设计一个支持 OTA 的自定义 Bootloader,涵盖 Flash 分区规划、启动跳转、固件搬运、校验机制等关键环节。
二、STM32F4 的启动机制
2.1 启动模式
STM32F4 上电后,内核从地址 0x0000_0000 读取初始栈指针(SP),从 0x0000_0004 读取复位向量(PC)。这两个地址并非物理上独立的存储器,而是通过地址映射(Alias)指向实际存储区域,具体取决于 BOOT0 和 BOOT1 引脚电平:
| BOOT1 | BOOT0 | 启动源 | 0x0000_0000 映射到 |
|---|
| X | 0 | 主 Flash | 0x0800_0000 |
| 0 | 1 | 系统存储器 | 0x1FFF_0000(ST 出厂 Bootloader) |
| 1 | 1 | 嵌入式 SRAM | 0x2000_0000 |
在常规应用中,BOOT0 接地,芯片从内部 Flash 启动。此时 0x0800_0000 处存放的就是用户自定义 Bootloader。
2.2 中断向量表重映射
Cortex-M4 内核支持通过 VTOR(Vector Table Offset Register)寄存器将中断向量表偏移到任意 512 字节对齐的地址。APP 程序若不从 Flash 首地址运行,必须在初始化阶段执行:
1
| SCB->VTOR = APP_START_ADDR;
|
否则中断响应将指向 Bootloader 的中断向量表,导致程序跑飞。
三、Flash 分区规划
3.1 STM32F407 Flash 扇区分布
STM32F407ZG(1MB Flash)的扇区分布不均匀,前 4 个扇区各 16KB,第 5 个扇区 64KB,后 7 个扇区各 128KB:
| 扇区 | 起始地址 | 大小 |
|---|
| Sector 0 | 0x0800_0000 | 16 KB |
| Sector 1 | 0x0800_4000 | 16 KB |
| Sector 2 | 0x0800_8000 | 16 KB |
| Sector 3 | 0x0800_C000 | 16 KB |
| Sector 4 | 0x0801_0000 | 64 KB |
| Sector 5 | 0x0802_0000 | 128 KB |
| Sector 6 | 0x0804_0000 | 128 KB |
| Sector 7 | 0x0806_0000 | 128 KB |
| Sector 8 | 0x0808_0000 | 128 KB |
| Sector 9 | 0x080A_0000 | 128 KB |
| Sector 10 | 0x080C_0000 | 128 KB |
| Sector 11 | 0x080E_0000 | 128 KB |
关键约束:F4 系列 Flash 擦除的最小单位是"扇区",不能按字节或页擦除。因此分区设计必须以扇区边界对齐。
3.2 推荐分区方案(三区模型)
对于 1MB Flash 的 STM32F407,推荐采用"Bootloader + APP 运行区 + APP 备份区"的三区模型,兼顾空间利用率与升级可靠性:
| 分区 | 起始地址 | 结束地址 | 大小 | 占用扇区 |
|---|
| Bootloader | 0x0800_0000 | 0x0800_7FFF | 32 KB | Sector 0~1 |
| 参数区 | 0x0800_8000 | 0x0800_BFFF | 16 KB | Sector 2 |
| APP 运行区 | 0x0800_C000 | 0x0807_BFFF | 448 KB | Sector 3~7 |
| APP 备份区 | 0x0808_0000 | 0x080E_FFFF | 448 KB | Sector 8~10 |
| 保留 | 0x080F_0000 | 0x080F_FFFF | 64 KB | Sector 11 |

注:上图仅为示意,实际地址以上表为准。参数区用于存储 OTA 标志、版本号、CRC 校验值等元数据。
3.3 为什么需要备份区
若采用"Bootloader + 单 APP"的两区模型,新固件下载过程中一旦通信中断,旧 APP 可能已被破坏,设备将变砖。引入备份区后,升级流程变为:
- 新固件下载到备份区,不影响正在运行的 APP;
- 下载完成后校验备份区固件;
- Bootloader 将备份区固件搬运到 APP 运行区;
- 若搬运后 APP 运行区固件校验失败,可回滚到旧版本或停留在 Bootloader 等待重新升级。
四、Bootloader 工作流程
4.1 上电启动流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| 上电复位
|
v
初始化时钟、GPIO、USART(用于调试输出)
|
v
读取参数区 OTA 标志
|
+-- 标志 == NEED_UPDATE --+--> 校验备份区固件 CRC
| | |
| | v
| | CRC 通过?
| | / \
| | 是 否
| | | |
| | v v
| | 擦除 APP 区 报错,停留在 Bootloader
| | 搬运备份区 |
| | 到 APP 区 |
| | 清除 OTA 标志 |
| | 跳转 APP |
| v
+-- 标志 == NO_UPDATE ----+--> 校验 APP 区固件
| |
| v
| 校验通过?
| / \
| 是 否
| | |
| v v
| 跳转 APP 停留在 Bootloader
| 等待升级
v
|

4.2 跳转 APP 的关键机制
Bootloader 向 APP 跳转的本质是重新初始化内核的 SP 和 PC 寄存器。APP 编译后的二进制文件,其前 4 字节为栈顶地址,第 5~8 字节为 Reset_Handler 入口地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| typedef void (*app_func_t)(void);
void bootloader_jump_to_app(uint32_t app_addr)
{
uint32_t sp_val;
uint32_t pc_val;
app_func_t app_entry;
/* 读取 APP 的栈顶地址 */
sp_val = *(volatile uint32_t *)app_addr;
/* 读取 APP 的复位向量 */
pc_val = *(volatile uint32_t *)(app_addr + 4);
/* 简单校验:栈顶应在 SRAM 范围内 */
if ((sp_val < SRAM_BASE) || (sp_val > (SRAM_BASE + SRAM_SIZE))) {
return;
}
/* 关闭全局中断,防止跳转过程中被打断 */
__disable_irq();
/* 复位外设、关闭 SysTick、清除 pending 中断 */
HAL_DeInit();
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
/* 设置向量表偏移 */
SCB->VTOR = app_addr;
/* 设置主栈指针 */
__set_MSP(sp_val);
/* 跳转到 APP */
app_entry = (app_func_t)pc_val;
app_entry();
}
|
注意:跳转前务必关闭所有已初始化的外设中断,并将使用到的外设反初始化,否则 APP 中重新初始化时可能产生不可预期的行为。
五、核心代码实现
5.1 Flash 擦除与写入
STM32F4 的 Flash 编程支持按"双字(Double Word,64bit)“写入。擦除则必须以扇区为单位。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| #include "stm32f4xx_hal.h"
#define FLASH_SECTOR_TOTAL 12
static uint32_t s_flash_get_sector(uint32_t addr)
{
if (addr < 0x08004000U) return FLASH_SECTOR_0;
if (addr < 0x08008000U) return FLASH_SECTOR_1;
if (addr < 0x0800C000U) return FLASH_SECTOR_2;
if (addr < 0x08010000U) return FLASH_SECTOR_3;
if (addr < 0x08020000U) return FLASH_SECTOR_4;
if (addr < 0x08040000U) return FLASH_SECTOR_5;
if (addr < 0x08060000U) return FLASH_SECTOR_6;
if (addr < 0x08080000U) return FLASH_SECTOR_7;
if (addr < 0x080A0000U) return FLASH_SECTOR_8;
if (addr < 0x080C0000U) return FLASH_SECTOR_9;
if (addr < 0x080E0000U) return FLASH_SECTOR_10;
return FLASH_SECTOR_11;
}
uint8_t flash_erase_sector(uint32_t addr, uint32_t size)
{
HAL_StatusTypeDef status;
FLASH_EraseInitTypeDef erase_cfg;
uint32_t sector_err = 0;
uint32_t start_sector;
uint32_t end_sector;
uint32_t nb_sectors;
start_sector = s_flash_get_sector(addr);
end_sector = s_flash_get_sector(addr + size - 1);
nb_sectors = end_sector - start_sector + 1;
HAL_FLASH_Unlock();
erase_cfg.TypeErase = FLASH_TYPEERASE_SECTORS;
erase_cfg.Sector = start_sector;
erase_cfg.NbSectors = nb_sectors;
erase_cfg.VoltageRange = FLASH_VOLTAGE_RANGE_3; /* 2.7V~3.6V */
status = HAL_FLASHEx_Erase(&erase_cfg, §or_err);
HAL_FLASH_Lock();
return (status == HAL_OK) ? 0 : 1;
}
uint8_t flash_write_doubleword(uint32_t addr, uint64_t data)
{
HAL_StatusTypeDef status;
HAL_FLASH_Unlock();
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, data);
HAL_FLASH_Lock();
return (status == HAL_OK) ? 0 : 1;
}
uint8_t flash_write_buffer(uint32_t addr, uint8_t *buf, uint32_t len)
{
uint32_t i;
uint64_t dw;
uint8_t pad;
/* 按 8 字节对齐写入 */
for (i = 0; i < len; i += 8) {
dw = 0xFFFFFFFFFFFFFFFFULL;
if ((len - i) >= 8) {
memcpy(&dw, &buf[i], 8);
} else {
pad = len - i;
memcpy(&dw, &buf[i], pad);
}
if (flash_write_doubleword(addr + i, dw) != 0) {
return 1;
}
}
return 0;
}
|
5.2 OTA 参数管理
参数区存储升级相关的元数据,建议定义一个结构体并做版本管理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| #define OTA_PARAM_ADDR 0x08008000
#define OTA_MAGIC_WORD 0x5A5A1122
#define OTA_FLAG_NONE 0x00000000
#define OTA_FLAG_NEED 0xAA550001
typedef struct {
uint32_t magic; /* 魔术字,用于判断参数区是否已初始化 */
uint32_t ota_flag; /* OTA 标志 */
uint32_t app_version; /* 当前 APP 版本号 */
uint32_t backup_version; /* 备份区固件版本号 */
uint32_t app_crc; /* APP 区固件 CRC32 */
uint32_t backup_crc; /* 备份区固件 CRC32 */
uint32_t app_size; /* APP 区固件大小 */
uint32_t backup_size; /* 备份区固件大小 */
} ota_param_t;
static ota_param_t s_ota_param;
void ota_param_load(void)
{
memcpy(&s_ota_param, (void *)OTA_PARAM_ADDR, sizeof(ota_param_t));
if (s_ota_param.magic != OTA_MAGIC_WORD) {
/* 首次运行,初始化默认参数 */
memset(&s_ota_param, 0, sizeof(ota_param_t));
s_ota_param.magic = OTA_MAGIC_WORD;
s_ota_param.ota_flag = OTA_FLAG_NONE;
ota_param_save();
}
}
void ota_param_save(void)
{
flash_erase_sector(OTA_PARAM_ADDR, sizeof(ota_param_t));
flash_write_buffer(OTA_PARAM_ADDR, (uint8_t *)&s_ota_param, sizeof(ota_param_t));
}
void ota_param_set_flag(uint32_t flag)
{
s_ota_param.ota_flag = flag;
ota_param_save();
}
uint32_t ota_param_get_flag(void)
{
return s_ota_param.ota_flag;
}
|
5.3 固件搬运与校验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| #include "crc32.h"
#define APP_ADDR 0x0800C000
#define BACKUP_ADDR 0x08080000
#define APP_MAX_SIZE (448 * 1024)
uint8_t bootloader_do_update(void)
{
uint32_t crc_calc;
uint32_t fw_size;
uint8_t buf[1024];
uint32_t i;
uint32_t chunks;
uint32_t remain;
fw_size = s_ota_param.backup_size;
if (fw_size == 0 || fw_size > APP_MAX_SIZE) {
return 1; /* 固件大小非法 */
}
/* 1. 校验备份区固件 CRC */
crc_calc = crc32_calculate((uint8_t *)BACKUP_ADDR, fw_size);
if (crc_calc != s_ota_param.backup_crc) {
return 2; /* CRC 校验失败 */
}
/* 2. 擦除 APP 运行区 */
if (flash_erase_sector(APP_ADDR, APP_MAX_SIZE) != 0) {
return 3; /* 擦除失败 */
}
/* 3. 将备份区固件搬运到 APP 区(按 1KB 块搬运) */
chunks = fw_size / sizeof(buf);
remain = fw_size % sizeof(buf);
for (i = 0; i < chunks; i++) {
memcpy(buf, (void *)(BACKUP_ADDR + i * sizeof(buf)), sizeof(buf));
if (flash_write_buffer(APP_ADDR + i * sizeof(buf), buf, sizeof(buf)) != 0) {
return 4; /* 写入失败 */
}
}
if (remain > 0) {
memcpy(buf, (void *)(BACKUP_ADDR + chunks * sizeof(buf)), remain);
if (flash_write_buffer(APP_ADDR + chunks * sizeof(buf), buf, remain) != 0) {
return 4;
}
}
/* 4. 校验写入后的 APP 区 */
crc_calc = crc32_calculate((uint8_t *)APP_ADDR, fw_size);
if (crc_calc != s_ota_param.backup_crc) {
return 5; /* 搬运后校验失败 */
}
/* 5. 更新参数区 */
s_ota_param.app_version = s_ota_param.backup_version;
s_ota_param.app_crc = s_ota_param.backup_crc;
s_ota_param.app_size = s_ota_param.backup_size;
s_ota_param.ota_flag = OTA_FLAG_NONE;
s_ota_param.backup_crc = 0;
s_ota_param.backup_size = 0;
ota_param_save();
return 0;
}
|
5.4 Bootloader 主函数框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| int main(void)
{
HAL_Init();
SystemClock_Config();
/* 初始化调试串口,用于输出日志 */
debug_uart_init();
printf("Bootloader start...\r\n");
/* 加载 OTA 参数 */
ota_param_load();
printf("OTA flag: 0x%08X\r\n", s_ota_param.ota_flag);
if (s_ota_param.ota_flag == OTA_FLAG_NEED) {
printf("OTA update requested\r\n");
uint8_t ret = bootloader_do_update();
if (ret != 0) {
printf("OTA failed, err=%d\r\n", ret);
/* 停留在 Bootloader,可通过串口/Ymodem 等方式重新升级 */
bootloader_wait_for_upgrade();
}
}
/* 校验 APP 区 */
uint32_t sp = *(volatile uint32_t *)APP_ADDR;
if ((sp >= 0x20000000) && (sp <= 0x2001BFFF)) {
printf("Jump to APP @ 0x%08X\r\n", APP_ADDR);
bootloader_jump_to_app(APP_ADDR);
}
/* 跳转失败,停留在 Bootloader */
printf("No valid APP found\r\n");
bootloader_wait_for_upgrade();
}
|
六、APP 端需要做的修改
6.1 修改链接脚本(.ld 文件)
APP 的编译起始地址必须与分区规划中的 APP 运行区一致。以 GCC 链接脚本为例:
1
2
3
4
5
6
7
| /* 原默认起始地址 */
/* FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K */
/* 修改为 APP 运行区起始地址 */
FLASH (rx) : ORIGIN = 0x0800C000, LENGTH = 448K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
|
若使用 Keil,在 Options -> Target -> IROM1 中修改起始地址为 0x0800C000,大小为 0x00070000(448KB)。
6.2 中断向量表偏移
在 APP 的 main() 函数开头(或 SystemInit() 中)添加:
1
2
3
4
5
6
7
8
9
10
11
| #define APP_START_ADDR 0x0800C000
void system_init_app(void)
{
/* 设置向量表偏移 */
SCB->VTOR = APP_START_ADDR;
/* 继续初始化 HAL、时钟等 */
HAL_Init();
SystemClock_Config();
}
|
6.3 触发 OTA 升级
APP 中需要提供一个接口,用于在收到升级指令后设置 OTA 标志并复位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void app_request_ota_upgrade(uint32_t new_version, uint32_t new_crc, uint32_t new_size)
{
ota_param_t param;
memcpy(¶m, (void *)OTA_PARAM_ADDR, sizeof(ota_param_t));
param.ota_flag = OTA_FLAG_NEED;
param.backup_version = new_version;
param.backup_crc = new_crc;
param.backup_size = new_size;
/* 将参数写入参数区 */
flash_erase_sector(OTA_PARAM_ADDR, sizeof(ota_param_t));
flash_write_buffer(OTA_PARAM_ADDR, (uint8_t *)¶m, sizeof(ota_param_t));
/* 系统复位,进入 Bootloader 执行升级 */
HAL_NVIC_SystemReset();
}
|
七、完整的 OTA 升级流程
7.1 通信链路侧(以串口 Ymodem 为例)
- APP 运行中,通过串口收到上位机发送的"升级指令”;
- APP 解析指令,获取新固件版本号、CRC、大小等元数据;
- APP 调用
app_request_ota_upgrade() 设置标志并复位; - MCU 重启,Bootloader 检测到
OTA_FLAG_NEED; - Bootloader 等待上位机通过 Ymodem 发送固件,接收后写入备份区;
- 写入完成后,Bootloader 校验备份区 CRC;
- 校验通过,Bootloader 擦除 APP 区,将备份区固件搬运到 APP 区;
- 搬运完成再次校验,通过后清除 OTA 标志,跳转 APP;
- 新 APP 运行,升级完成。
若通信链路为以太网/Wi-Fi,则 APP 本身负责从服务器下载固件到备份区,下载完成后再设置 OTA 标志并复位,后续流程相同。
7.2 流程图



八、常见问题与排查
| 现象 | 可能原因 | 排查方法 |
|---|
| Bootloader 跳转后 APP 不运行 | VTOR 未正确设置 | 检查 APP 中是否执行了 SCB->VTOR = APP_ADDR |
| 跳转后 HardFault | SP 值非法或 APP 未正确烧录 | 检查 APP 首 4 字节是否在 SRAM 范围内;检查 APP 是否已正确写入 Flash |
| Flash 写入失败 | 未解锁 Flash;写入地址未对齐 | 确保调用 HAL_FLASH_Unlock();确保地址按双字对齐 |
| 擦除后参数丢失 | 参数区与 APP 区扇区重叠 | 检查分区表,确保参数区独占完整扇区 |
| OTA 后旧 APP 仍运行 | OTA 标志未清除或搬运失败 | 在 Bootloader 中添加日志,检查 ota_flag 值和搬运返回值 |
| CRC 校验不一致 | 下载过程中数据损坏 | 在传输层增加 ACK 确认机制;每包数据附带 CRC 校验 |
九、扩展方向
- A/B 双区热切换:不搬运固件,Bootloader 根据标志直接从 A 区或 B 区启动,升级时只需切换标志位,速度更快;
- 差分升级:使用 bsdiff/bspatch 算法生成差分包,减少传输流量,适合带宽受限场景;
- 安全启动:增加固件签名验证(RSA/ECDSA),防止刷入非法固件;
- 外部 Flash 方案:对于 Flash 空间不足的 MCU,可将备份区放到外部 SPI Flash(如 W25Q128),Bootloader 从中读取并写入内部 Flash。
十、参考资源
- STM32F4xx Reference Manual(RM0090)—— Flash 编程章节
- AN2606:STM32 微控制器系统存储器启动模式应用笔记
- AN3155:USART 协议用于 STM32 Bootloader
- GitHub:
skb666/stm32f4_iap —— STM32F4 双 APP 交替 IAP 方案
本教程基于公开技术文档与社区实践整理,具体实现需根据实际硬件环境和项目需求调整。Flash 操作具有破坏性,建议在调试阶段做好备份,避免误擦除 Bootloader 自身代码。