Bootloader + OTA 升级框架笔记

Bootloader + OTA 升级框架笔记

支持 UARTCAN 两种通信方式的 STM32F407 嵌入式 Bootloader 与 OTA 固件升级框架。


一、基本概念

什么是 Bootloader

Bootloader 是芯片上电后最早运行的引导程序,负责:

  • 硬件初始化(时钟、外设)
  • 检查是否有固件升级请求
  • 验证 APP 固件合法性
  • 跳转到 APP 或进入 OTA 升级模式

什么是 RAM 和 Flash

特性 Flash RAM (SRAM)
用途 存储程序代码和常量 运行时变量和栈
容量 (F407) 1MB 128KB
持久性 掉电不丢失 掉电丢失
写入方式 需擦除后编程 (字对齐) 直接读写
速度 较慢 (需等待状态) 快 (单周期访问)

什么是 OTA

OTA (Over-The-Air) 指通过通信接口(UART/CAN/蓝牙/WiFi)远程升级固件,无需物理连接调试器。本框架使用有线方式(UART/CAN),但传输层抽象设计允许扩展到无线模块。


二、Flash 内存分区(STM32F407)

STM32F407 内部 Flash 共 1MB,分为 12 个扇区:

扇区 起始地址 大小 用途
0 0x08000000 16KB Bootloader
1 0x08004000 16KB Bootloader
2 0x08008000 16KB Bootloader
3 0x0800C000 16KB 配置区(魔数/标志)
4 0x08010000 64KB APP
5 0x08020000 128KB APP
6 0x08040000 128KB APP
7 0x08060000 128KB APP
8 0x08080000 128KB OTA 下载暂存区
9 0x080A0000 128KB OTA 下载暂存区
10 0x080C0000 128KB OTA 下载暂存区
11 0x080E0000 128KB OTA 下载暂存区
复制代码
+------------------+ 0x08000000
| Bootloader (48KB)| 扇区 0~2
+------------------+ 0x0800C000
| Config   (16KB)  | 扇区 3
+------------------+ 0x08010000
| APP       (448KB)| 扇区 4~7
+------------------+ 0x08080000
| OTA Temp  (512KB)| 扇区 8~11
+------------------+ 0x08100000

设计要点:

  • Bootloader 48KB 留有充足的代码扩展空间
  • 配置区独占一个扇区,擦除配置区不影响 Bootloader 或其他区域
  • OTA 下载区 (512KB) >= APP 区 (448KB),确保能容纳完整固件
  • APP 和 OTA 下载区按扇区边界对齐,简化擦除操作

三、启动流程(魔数机制)

复制代码
上电复位
    │
    ▼
┌──────────────┐
│ 初始化时钟    │
│ 初始化传输层  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ 读取配置区    │──── 魔数无效? ──→ 初始化配置区(首次启动)
│ (0x0800C000) │
└──────┬───────┘
       │ 魔数 = 0x424F4F54 ("BOOT")
       ▼
┌──────────────┐
│ boot_mode == │── 是 ──→ 进入 OTA 模式
│ OTA?         │
└──────┬───────┘
       │ 否
       ▼
┌──────────────┐
│ APP 合法?    │──── 否 ──→ 进入 OTA 模式 (等待升级)
│ (检查SP+PC)  │
└──────┬───────┘
       │ 是
       ▼
┌──────────────┐
│ 跳转到 APP   │
└──────────────┘

魔数机制说明:

  • 配置区魔数 0x424F4F54("BOOT" 的 ASCII),标识配置区已被正确初始化
  • 芯片首次上电时 Flash 为空 (0xFF),魔数不匹配,Bootloader 自动初始化配置区
  • boot_mode 字段控制启动路径:0xAA55AA55 = OTA 模式,0x00000000 = 正常启动

四、OTA 核心状态机

三个命令(Host → Bootloader)

命令 代码 说明
CMD_OTA_START 0x01 开始 OTA,携带固件大小、CRC32、版本号
CMD_OTA_DATA 0x02 传输固件数据块
CMD_OTA_ABORT 0x03 中止当前传输,状态机回 IDLE

四个状态码(Bootloader → Host)

状态码 代码 说明
STATUS_ACK 0x10 数据帧接收成功,等待下一帧
STATUS_NAK 0x11 帧错误(CRC/格式/状态不允许)
STATUS_BUSY 0x12 设备忙(正在执行 Flash 操作)
STATUS_COMPLETE 0x14 OTA 完成,固件校验通过,设备即将复位

完整升级时序

复制代码
Host (上位机)                        Bootloader (STM32)
     │                                      │
     │                                      │  [IDLE]
     │── CMD_OTA_START ────────────────────→│
     │   (fw_size, crc32, version)          │  擦除 OTA 下载区
     │←─ STATUS_READY ─────────────────────│  [START_RCVD]
     │                                      │
     │── CMD_OTA_DATA (chunk 0) ───────────→│  写入 Flash
     │←─ STATUS_ACK ───────────────────────│  [DOWNLOADING]
     │                                      │
     │── CMD_OTA_DATA (chunk 1) ───────────→│  写入 Flash
     │←─ STATUS_ACK ───────────────────────│
     │         ... (循环 N 次) ...          │
     │                                      │
     │── CMD_OTA_DATA (chunk N, 最后一块) ─→│  写入 Flash
     │←─ STATUS_COMPLETE ──────────────────│  [COMPLETE]
     │                                      │
     │                                      │  CRC32 校验通过
     │                                      │  拷贝到 APP 区
     │                                      │  系统复位...
     │                                      │
     │                                      │  [IDLE] 重新启动
     │                                      │  跳转到新 APP

帧格式:

复制代码
| STX(0xAA) | CMD/STATUS(1B) | LEN(2B,BE) | DATA(NB) | CRC16(2B) | ETX(0x55) |
     1B            1B             2B        0~1024B      2B          1B
  • CRC16 采用 MODBUS 多项式,校验范围:STX 到 DATA 末尾
  • LEN 为大端序,最大 1024 字节
  • 单帧最大长度 = 4(头) + 1024(数据) + 3(尾) = 1031 字节

五、传输层架构(核心设计)

统一接口

c 复制代码
typedef struct transport_ops {
    int32_t (*init)(void *params);
    int32_t (*deinit)(void);
    int32_t (*send)(const uint8_t *data, uint32_t len, uint32_t timeout);
    int32_t (*recv)(uint8_t *buf, uint32_t len, uint32_t timeout);
    void    (*flush)(void);
} transport_ops_t;

上层(OTA 状态机)只调用 transport_send() / transport_recv(),不感知底层是 UART 还是 CAN。

切换协议方法

c 复制代码
/* 运行时切换传输层, 自动 deinit 旧层、init 新层 */
transport_switch(&transport_uart);   /* 切换到 UART */
transport_switch(&transport_can);    /* 切换到 CAN */

条件编译保护

c 复制代码
/* boot_config.h */
#define TRANSPORT_UART_ENABLED    // 启用 UART
//#define TRANSPORT_CAN_ENABLED   // 启用 CAN (二选一或共存)
  • 启用多个传输层时,DEFAULT_TRANSPORT 宏选定默认使用的协议
  • 编译时如果未启用任何传输层,#error 编译报错
  • 每个传输层的 .c 文件内使用 #ifdef TRANSPORT_XXX_ENABLED 守卫

六、串口传输层详解

文件: Bootloader/Transport/transport_uart.c

  • 基于 STM32 HAL 库 HAL_UART_Transmit / HAL_UART_Receive 阻塞收发
  • 配置参数通过 uart_config_t 传入(UART 句柄 + 波特率)
  • 默认波特率 115200,8N1 无流控
  • 收发都带超时参数,防止死等
  • flush() 清空 UART DR 寄存器,丢弃残留数据

不使用 HAL 时: 文件内含裸机寄存器实现(通过 #else 分支),直接操作 USART SR/DR 寄存器。


七、Flash 擦写操作

文件: Bootloader/Core/flash_ops.c

核心操作:

函数 说明
flash_erase_sector(sector) 擦除单个扇区(F407 扇区 0~11)
flash_erase_range(addr, size) 擦除地址范围(自动计算覆盖扇区)
flash_write_words(addr, data, words) 按 32-bit 对齐写入
flash_write_bytes(addr, data, len) 字节数组写入(内部处理对齐和补齐)
flash_read(addr, buf, len) 直接内存映射读取
flash_calc_crc32(addr, len) 计算 Flash 区域的 CRC32

注意事项:

  • Flash 写入前必须擦除(擦除后为 0xFF)
  • STM32F4 支持 Word (32-bit) 编程,写入地址必须 4 字节对齐
  • 擦除/写入期间 Flash 不可读,需等待 BSY 标志清除
  • 所有 Flash 操作函数内部自动 unlock/lock,调用者无需手动操作

八、APP 验证与跳转

文件: Bootloader/Core/app_validator.c

验证 APP 合法性

c 复制代码
int app_validate(uint32_t app_addr, uint32_t app_size, uint32_t expected_crc);

验证项:

  1. 栈指针 (MSP) 检查 --- 初始 SP 值必须落在 SRAM 范围内 (0x20000000 ~ 0x20020000)
  2. 复位向量检查 --- Reset_Handler 地址必须在 APP Flash 范围内 (0x08010000 ~ 0x08080000)
  3. Thumb 模式检查 --- Reset_Handler 的 bit0 必须为 1(Cortex-M 要求)
  4. CRC32 校验(可选)--- 全固件完整性检查

跳转到 APP

c 复制代码
void app_jump(uint32_t app_addr) __attribute__((noreturn));

跳转步骤:

  1. cpsid i --- 关闭全局中断
  2. SCB->VTOR = app_addr --- 重设向量表基址(APP 中断生效)
  3. MSR MSP, sp_init --- 设置 APP 栈指针
  4. BX reset_handler --- 跳转到 APP 的 Reset_Handler

九、APP 端触发升级

文件: App/ota_trigger.c

APP 在运行时可通过以下方式触发 OTA:

c 复制代码
/* 仅设置标志, 不立即复位 */
ota_trigger_set_flag();   // 写入 boot_mode = BOOT_MODE_OTA 到配置区
// ... 等待合适的时机 ...
NVIC_SystemReset();       // 手动触发复位

/* 或一步到位:设置标志并立即复位 */
ota_trigger_request();    // 设置标志 + 软复位, 函数不返回

工作原理:

  1. APP 调用 ota_trigger_request()
  2. 函数写入配置区 boot_mode = BOOT_MODE_OTA (0xAA55AA55)
  3. 通过 SCB->AIRCR 触发系统软复位
  4. Bootloader 启动后检测到 OTA 标志,进入 OTA 模式

十、链接脚本

Bootloader 链接脚本 (Bootloader/linker_stm32f407_boot.ld)

区域 起始地址 大小
FLASH 0x08000000 48KB
SRAM 0x20000000 128KB
  • 栈大小: 2KB, 无堆(Bootloader 不需要动态内存)

APP 链接脚本 (App/linker_stm32f407_app.ld)

区域 起始地址 大小
FLASH 0x08010000 448KB
SRAM 0x20000000 128KB
  • 栈大小: 4KB, 堆大小: 1KB
  • APP 的向量表位于 0x08010000,需在 startup 中设置 SCB->VTOR = 0x08010000

十一、移植清单

将本框架移植到其他 STM32 芯片时,需要修改以下内容:

1. boot_config.h --- Flash 分区

参数 F407 值 说明
FLASH_BASE 0x08000000 Flash 基址(F1/F2/F4 相同)
FLASH_SIZE 1024*1024 芯片 Flash 总容量
FLASH_SECTOR_COUNT 12 扇区个数
BOOTLOADER_SIZE 48KB 根据需要调整
APP_START_ADDR 0x08010000 APP 起始地址
APP_SIZE 448KB APP 最大容量
OTA_DOWNLOAD_SIZE 512KB OTA 区容量 (>= APP_SIZE)

2. flash_ops.c --- 扇区表

修改 sector_table[] 数组,填入目标芯片的扇区分布。

3. app_validator.c --- SRAM 范围

修改 SRAM_BASESRAM_SIZE 宏。

4. 链接脚本

修改 ORIGINLENGTH 值。

5. 传输层 init()

根据目标芯片的外设基地址和 HAL 库配置调整。


十二、整体架构总结

复制代码
┌──────────────────────────────────────────────────────┐
│                    上位机 (Host)                      │
│              (UART/CAN 发送 OTA 帧)                  │
└─────────────────────┬────────────────────────────────┘
                      │ UART 或 CAN
┌─────────────────────▼────────────────────────────────┐
│                  传输层 (Transport)                   │
│  ┌─────────────────────────────────────────────────┐ │
│  │ transport.h       统一接口 (init/send/recv)      │ │
│  │ transport_uart.c  UART 实现                     │ │
│  │ transport_can.c   CAN 实现 (分段传输)           │ │
│  └─────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│               OTA 状态机 (ota_state_machine)          │
│  ┌─────────────────────────────────────────────────┐ │
│  │ 3 条命令: START / DATA / ABORT                  │ │
│  │ 4 个状态: IDLE → START_RCVD → DOWNLOADING       │ │
│  │           → VERIFYING → COMPLETE                │ │
│  └─────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│                  Flash 操作层 (flash_ops)             │
│  ┌─────────────────────────────────────────────────┐ │
│  │ erase / write / read / crc32                    │ │
│  └─────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│           APP 校验与跳转 (app_validator)              │
│  ┌─────────────────────────────────────────────────┐ │
│  │ SP 检查 / PC 检查 / Thumb 检查 / CRC32 校验     │ │
│  │ 跳转: SCB->VTOR → MSP → BX ResetHandler        │ │
│  └─────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────┤
│                   配置区 (Config)                     │
│  ┌─────────────────────────────────────────────────┐ │
│  │ 扇区 3: 魔数 | boot_mode | OTA参数 | APP标志    │ │
│  └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│                     APP 区域                          │
│  ┌─────────────────────────────────────────────────┐ │
│  │ ota_trigger.c: 设置 OTA 标志 + 软复位           │ │
│  └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

代码文件索引:

文件 作用
Bootloader/Config/boot_config.h 全局配置、Flash 分区、协议常量
Bootloader/Core/flash_ops.h/c Flash 擦除/写入/读取/CRC32
Bootloader/Core/ota_state_machine.h/c OTA 帧解析、状态机、CRC16
Bootloader/Core/app_validator.h/c APP 校验 (SP/PC/CRC) 与跳转
Bootloader/Core/bootloader.h/c 主启动流程 + OTA 主循环
Bootloader/Transport/transport.h/c 抽象接口 + 协议切换
Bootloader/Transport/transport_uart.h/c UART 传输层
Bootloader/Transport/transport_can.h/c CAN 传输层 (分段协议)
Bootloader/linker_stm32f407_boot.ld Bootloader 链接脚本
App/ota_trigger.h/c APP 端触发 OTA
App/linker_stm32f407_app.ld APP 链接脚本