STM32 BOOT 未引出场景下的串口升级方案
解决"BOOT0/BOOT1 引脚未引出、但需要通过串口升级固件"这类问题的技术方案。 以 STM32F407 + USART1 + HAL 库为实例,已实测验证可行。
1. 适用场景
- BOOT0(甚至 BOOT1)引脚未引出到板边,无法通过硬件拨码/跳线进入系统 bootloader;
- 需要现场通过串口(或 USB DFU 等)升级固件;
- 希望在应用运行中通过一条指令/命令触发升级;
- 应用里启用了独立看门狗(IWDG),需要兼顾升级过程不被看门狗打断。
方案思路:软件跳转 + 标志 + 复位 + 上电早期跳转,不依赖 BOOT0 引脚。
2. 背景与原理
2.1 什么是系统 bootloader(System Bootloader)
STM32 出厂时在芯片内部固化了一段 ROM 引导程序(System Bootloader),存放在 System Memory(系统存储器) 中。它不占用用户的 Flash,可以擦写整个 Flash(包括用户自己的 bootloader 区)。
2.2 BOOT 引脚与启动模式,以及为什么能软件跳转
复位时,硬件根据 BOOT0 / BOOT1 的组合决定从哪块存储启动:
| BOOT1 | BOOT0 | 启动模式 | 启动空间 |
|---|---|---|---|
| x | 0 | 主 Flash | 0x08000000(别名到 0x00000000) |
| 0 | 1 | System Memory(系统 bootloader) | 0x1FFF0000 |
| 1 | 1 | 内嵌 SRAM | 0x20000000 |
注:BOOT0 是物理引脚;BOOT1 的来源因系列而异——STM32F1 是独立引脚(PB2),STM32F4 则由选项字节
nBOOT1位决定(不再占用引脚)。
BOOT0 / BOOT1 只在复位瞬间被采样。运行中的程序可以直接跳转到 System Memory 的执行入口,等效于复位后从系统 bootloader 启动,从而绕开 BOOT0 引脚。这一点在 AN2606 中明确说明。
2.3 System Memory 地址(关键)
不同系列地址不同,务必以 AN2606 为准。常见速查:
| 系列 | System Memory 基址 | 备注 |
|---|---|---|
| STM32F0 | 0x1FFFEC00 / 0x1FFFD800 等 | 分具体型号 |
| STM32F1 | 0x1FFFF000 | F10x |
| STM32F2 / F4 | 0x1FFF0000 | 本文所用 |
| STM32F3 | 0x1FFFD800 | |
| STM32F7 | 0x1FF00000 | |
| STM32G0 / G4 | 0x1FFF0000 | |
| STM32L0 / L1 | 0x1FF00000 | |
| STM32L4 | 0x1FFF0000 | |
| STM32H7 | 0x1FF09800 | 注意:入口 ≠ 基址 0x1FFF0000 |
2.4 串口协议要点(AN3155)
- 数据帧:8 数据位 + 偶校验(even parity) + 1 停止位;
- 自动波特率检测:bootloader 用 SysTick 测量首个
0x7F同步字节的帧长来推算波特率; - 波特率范围 1200 ~ 115200,115200 是上限(最敏感),9600 更稳定;
- 实际波特率偏差需 < 2.5%;
- 握手机制:host 发
0x7F→ bootloader 回0x79(ACK);命令字节 + 其按位取反(XOR 校验),错误回0x1F(NACK)。
3. 方案设计
3.1 为什么不能"直接在运行中跳转"
在运行中的应用里直接跳转会有三个问题:
- IWDG 一旦启动,软件无法关闭(只能靠复位停止)。跳转后 bootloader 运行期间没人喂狗,约数秒后(取决于超时配置)芯片被复位回主 Flash,升级被打断。
- 跳转前外设状态不干净:DMA、中断、PLL、USART 等都还在运行,bootloader 期望的是复位后的默认状态,脏状态可能导致自动波特率检测异常。
- 跳转前时钟、Flash 延迟等配置残留,可能影响 bootloader 内部的 SysTick 波特率测量。
3.2 正确流程:标志 + 复位 + 早期跳转
跳转必须发生在 HAL_Init() / SystemClock_Config() / MX_IWDG_Init() / 任何外设初始化之前。
3.3 为什么用 RTC 备份寄存器存标志
标志必须在软件复位后依然存在,否则复位后无法得知要进入 bootloader。可选方案对比:
| 方案 | 跨软复位保持 | 实现难度 | 说明 |
|---|---|---|---|
| 普通全局变量 | ❌ 复位清零 | 简单 | 不可用 |
| no-init RAM 变量 | ✅ | 较复杂 | 需在散文件/linker 单独声明 no-init 段,不同工具链(Keil/GCC)配置各异,易出错 |
| RTC 备份寄存器 | ✅ | 最简单 | 硬件原生支持,无需改 linker |
| 内部 Flash 固定位置 | ✅ | 复杂 | 需擦写、速度慢、有磨损寿命 |
选 RTC 备份寄存器(RTC_BKP_DRx)的理由:
- 跨软复位保持:备份寄存器位于独立备份域,
NVIC_SystemReset()这类软复位不会清零,正好满足"跨复位传递标志"的需求; - 掉电才丢失、窗口极短:只要 VDD 供电(或 VBAT 接电池)就保持,仅 VDD 掉电且无 VBAT 时才丢失。而本方案流程是"设标志 → 立即复位 → 检测到即清除",窗口极短,不影响正常使用;
- 零 linker 改动:读写只需三步(使能 PWR 时钟 → 解除备份域写保护
HAL_PWR_EnableBkUpAccess()→ 读写寄存器),不用动散文件/链接脚本,最省事、也不容易出错。
注:STM32F407 有 20 个备份寄存器(
RTC_BKP_DR0~RTC_BKP_DR19),任选一个即可(本例用BKP0R),注意别与工程里已有的 RTC 用途冲突。
3.4 完整交互时序
4. 核心实现(完整可复制)
4.1 头文件
void jump_to_system_bootloader(void);
void request_system_bootloader(void);
uint8_t bootloader_requested(void);
void clear_bootloader_request(void);
4.2 实现
#include "stm32f4xx_hal.h"
#define BOOTLOADER_MAGIC 0x5AA5C33C /* 任意唯一魔数 */
/**
* 跳转到系统 bootloader。注意:此函数不会返回。
*/
void jump_to_system_bootloader(void)
{
void (*sysmem_boot_jump)(void);
volatile uint32_t addr = 0x1FFF0000; /* F407 System Memory 基址 */
/* 1. 时钟回到复位默认态(HSI) */
HAL_RCC_DeInit();
/* 2. 关闭 SysTick */
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
/* 3. 关闭全局中断 */
__disable_irq();
/* 4. 将 System Memory 重映射到 0x0000 0000,保证向量表正确 */
__HAL_RCC_SYSCFG_CLK_ENABLE();
SYSCFG->MEMRMP = 0x01;
/* 5. 关键顺序:先取跳转地址,再设 MSP(设 MSP 后不能再访问栈) */
sysmem_boot_jump = (void (*)(void))(*(volatile uint32_t *)(addr + 4));
__set_MSP(*(volatile uint32_t *)addr);
/* 6. 跳转 */
sysmem_boot_jump();
/* 不应到达 */
while (1);
}
/**
* 请求进入系统 bootloader:写备份寄存器标志 + 软件复位。
* 供运行中的命令(AT 指令、按键、上位机指令等)调用。
*/
void request_system_bootloader(void)
{
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
RTC->BKP0R = BOOTLOADER_MAGIC;
HAL_NVIC_SystemReset();
}
/**
* 复位后检查是否有进入 bootloader 的请求(备份寄存器跨软复位保持)。
*/
uint8_t bootloader_requested(void)
{
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
return (RTC->BKP0R == BOOTLOADER_MAGIC);
}
/**
* 清除请求标志。
*/
void clear_bootloader_request(void)
{
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
RTC->BKP0R = 0;
}
4.3 入口检查 main.c(放在 main() 第一条)
int main(void)
{
/* 复位后优先检查是否请求进入系统 bootloader */
if (bootloader_requested())
{
clear_bootloader_request();
jump_to_system_bootloader(); /* 不会返回 */
}
/* …… 之后的正常初始化:HAL_Init / SystemClock_Config / MX_IWDG_Init / ... …… */
}
5. 踩坑记录
坑 1:__set_MSP() 的顺序(会导致跳转后 HardFault)
错误写法(先设 MSP、后取跳转地址):
__set_MSP(*(volatile uint32_t *)addr); /* 先切栈 */
sysmem_boot_jump = ...(addr + 4); /* 再读 addr(volatile,从栈读)→ 垃圾值 */
__set_MSP() 会切换栈指针,之后不能再访问任何局部变量。addr 是 volatile,编译器每次从栈上重读,切栈后读到的是垃圾,跳到错误地址直接 HardFault。
正确写法:先取跳转地址,最后再设 MSP。
坑 2:IWDG 复位打断升级
跳转后 bootloader 运行期间无人喂狗,IWDG 超时复位回主 Flash。表现为:连接能握手、能读到 Chip ID,但后续命令 timeout/NACK 交替。
解决:用"标志 + 复位 + 早期跳转",让跳转发生在 MX_IWDG_Init() 之前(复位已停掉 IWDG)。
坑 3:自动波特率敏感,GETID 被 NACK
0x7F 同步能过(Activating OK),但 GETID 命令 NACK/timeout 交替。原因:
- 115200 接近 bootloader 波特率上限,量化误差大;
- 部分 USB-TTL 适配器(尤其 CH340 类)晶振精度不足。
解决:
- 降波特率到 9600 测试;
- 换 PL2303GT / FT232 等更可靠的适配器。
坑 4:跳转前状态不干净
在运行中直接跳转(外设/DMA/中断/PLL 未复位)会导致 bootloader 行为异常。ST 官方建议"复位后、初始化前跳转"(即本方案)。
6. 移植到其他 STM32 系列
- 改地址:替换
jump_to_system_bootloader()中的addr(见 2.3 表,以 AN2606 为准); - 改引脚:确认目标系列系统 bootloader 使用的 USART 引脚(F407 的 USART1 为 PA9/PA10;H7/F7 等需查 AN2606 对应表格);
- 改重映射寄存器:
- F4:
SYSCFG->MEMRMP - F0:
SYSCFG->CFGR1 - 其他系列查对应参考手册的 SYSCFG/内存重映射章节;
- F4:
- 标志存储:若目标芯片无 RTC 备份寄存器,可改用 no-init RAM 变量(需在 linker/散文件里单独声明 no-init 段),或用内部 Flash 固定位置。
7. 参考资料
- AN2606:STM32 microcontroller system memory boot mode(系统 bootloader 地址、引脚、时序)
- AN3155:USART protocol used in the STM32 bootloader(串口协议、校验、波特率)
- RM0090:STM32F405/415、407/417 参考手册(IWDG、SYSCFG、时钟)