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 引脚电平:

BOOT1BOOT0启动源0x0000_0000 映射到
X0主 Flash0x0800_0000
01系统存储器0x1FFF_0000(ST 出厂 Bootloader)
11嵌入式 SRAM0x2000_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 00x0800_000016 KB
Sector 10x0800_400016 KB
Sector 20x0800_800016 KB
Sector 30x0800_C00016 KB
Sector 40x0801_000064 KB
Sector 50x0802_0000128 KB
Sector 60x0804_0000128 KB
Sector 70x0806_0000128 KB
Sector 80x0808_0000128 KB
Sector 90x080A_0000128 KB
Sector 100x080C_0000128 KB
Sector 110x080E_0000128 KB

关键约束:F4 系列 Flash 擦除的最小单位是"扇区",不能按字节或页擦除。因此分区设计必须以扇区边界对齐。

3.2 推荐分区方案(三区模型)

对于 1MB Flash 的 STM32F407,推荐采用"Bootloader + APP 运行区 + APP 备份区"的三区模型,兼顾空间利用率与升级可靠性:

分区起始地址结束地址大小占用扇区
Bootloader0x0800_00000x0800_7FFF32 KBSector 0~1
参数区0x0800_80000x0800_BFFF16 KBSector 2
APP 运行区0x0800_C0000x0807_BFFF448 KBSector 3~7
APP 备份区0x0808_00000x080E_FFFF448 KBSector 8~10
保留0x080F_00000x080F_FFFF64 KBSector 11

STM32 Flash 分区示意图

注:上图仅为示意,实际地址以上表为准。参数区用于存储 OTA 标志、版本号、CRC 校验值等元数据。

3.3 为什么需要备份区

若采用"Bootloader + 单 APP"的两区模型,新固件下载过程中一旦通信中断,旧 APP 可能已被破坏,设备将变砖。引入备份区后,升级流程变为:

  1. 新固件下载到备份区,不影响正在运行的 APP;
  2. 下载完成后校验备份区固件;
  3. Bootloader 将备份区固件搬运到 APP 运行区;
  4. 若搬运后 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
初始化时钟、GPIOUSART(用于调试输出)
    |
    v
读取参数区 OTA 标志
    |
    +-- 标志 == NEED_UPDATE --+--> 校验备份区固件 CRC
    |                          |         |
    |                          |         v
    |                          |   CRC 通过?
    |                          |   /           \
    |                          |              
    |                          |  |             |
    |                          |  v             v
    |                          | 擦除 APP    报错,停留在 Bootloader
    |                          | 搬运备份区    |
    |                          |  APP      |
    |                          | 清除 OTA 标志 |
    |                          | 跳转 APP      |
    |                          v
    +-- 标志 == NO_UPDATE ----+--> 校验 APP 区固件
    |                                   |
    |                                   v
    |                             校验通过?
    |                             /           \
    |                                        
    |                            |             |
    |                            v             v
    |                         跳转 APP    停留在 Bootloader
    |                                       等待升级
    v

Bootloader 启动流程图

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, &sector_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(&param, (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 *)&param, sizeof(ota_param_t));

    /* 系统复位,进入 Bootloader 执行升级 */
    HAL_NVIC_SystemReset();
}

七、完整的 OTA 升级流程

7.1 通信链路侧(以串口 Ymodem 为例)

  1. APP 运行中,通过串口收到上位机发送的"升级指令”;
  2. APP 解析指令,获取新固件版本号、CRC、大小等元数据;
  3. APP 调用 app_request_ota_upgrade() 设置标志并复位;
  4. MCU 重启,Bootloader 检测到 OTA_FLAG_NEED
  5. Bootloader 等待上位机通过 Ymodem 发送固件,接收后写入备份区
  6. 写入完成后,Bootloader 校验备份区 CRC;
  7. 校验通过,Bootloader 擦除 APP 区,将备份区固件搬运到 APP 区;
  8. 搬运完成再次校验,通过后清除 OTA 标志,跳转 APP;
  9. 新 APP 运行,升级完成。

若通信链路为以太网/Wi-Fi,则 APP 本身负责从服务器下载固件到备份区,下载完成后再设置 OTA 标志并复位,后续流程相同。

7.2 流程图

Bootloader OTA 流程

MCU OTA 升级流程(机智云)

MCU OTA 工作流程(ST 社区)


八、常见问题与排查

现象可能原因排查方法
Bootloader 跳转后 APP 不运行VTOR 未正确设置检查 APP 中是否执行了 SCB->VTOR = APP_ADDR
跳转后 HardFaultSP 值非法或 APP 未正确烧录检查 APP 首 4 字节是否在 SRAM 范围内;检查 APP 是否已正确写入 Flash
Flash 写入失败未解锁 Flash;写入地址未对齐确保调用 HAL_FLASH_Unlock();确保地址按双字对齐
擦除后参数丢失参数区与 APP 区扇区重叠检查分区表,确保参数区独占完整扇区
OTA 后旧 APP 仍运行OTA 标志未清除或搬运失败在 Bootloader 中添加日志,检查 ota_flag 值和搬运返回值
CRC 校验不一致下载过程中数据损坏在传输层增加 ACK 确认机制;每包数据附带 CRC 校验

九、扩展方向

  1. A/B 双区热切换:不搬运固件,Bootloader 根据标志直接从 A 区或 B 区启动,升级时只需切换标志位,速度更快;
  2. 差分升级:使用 bsdiff/bspatch 算法生成差分包,减少传输流量,适合带宽受限场景;
  3. 安全启动:增加固件签名验证(RSA/ECDSA),防止刷入非法固件;
  4. 外部 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 自身代码。