STM32F4 Bootloader(IAP)开发-基于Ymodem协议
在嵌入式开发中,STM32 设备的固件升级是高频需求 —— 如果每次升级都要拆设备、接调试器,不仅效率低,还可能损坏硬件。而 Bootloader(引导加载程序) 就是解决这个问题的核心:它能让设备上电后自主选择 “升级模式” 或 “运行应用模式”,实现无需调试器的 IAP(在应用编程)升级。
本文结合实际项目代码,从原理到实战,手把手拆解 STM32 Bootloader 的开发逻辑,帮你彻底掌握这一核心技能。
一、Bootloader 是什么?它的核心价值在哪?
简单说,Bootloader 是 STM32 上电后第一个运行的程序,相当于设备的 “启动管家”,主要负责两件事:
- 模式选择:开机时检测按键,按下指定按键(如 KEY1)进入「IAP 升级模式」,否则尝试跳转到「用户 APP 模式」;
- APP 管理:在升级模式中提供固件下载、校验、写 Flash 功能;在正常模式中检测 APP 合法性,确保只有合法固件才能运行。
核心价值:
- 无需拆设备:通过串口 / BLE 等接口远程升级,适用于已安装的设备;
- 降低维护成本:批量设备可通过脚本自动升级,无需人工干预;
- 提高可靠性:支持 APP 合法性校验,避免固件损坏导致设备变砖。
二、底层原理:STM32 启动流程与 Flash 分区
要开发 Bootloader,必须先搞懂 STM32 的启动逻辑和 Flash 存储规则。
1. STM32 启动的两个关键规则
STM32 上电后,CPU 会先从 Flash 起始地址(0x08000000) 读取两个核心数据:
- 前 4 字节:栈指针(SP)的初始值(必须是合法的 RAM 地址);
- 后 4 字节:程序入口地址(PC)(即
main函数的地址)。
这意味着:
- Bootloader 必须存放在 Flash 起始地址(0x08000000),才能被优先执行;
- 用户 APP 必须存放在其他地址,且其起始地址的前 8 字节需符合上述格式(否则无法被正常跳转)。
2. Flash 分区规划(核心!避免地址重叠)
Bootloader 和 APP 不能共用同一块 Flash 区域,否则会相互覆盖。需根据 STM32 的 Flash 容量合理划分,以 512KB Flash 的 STM32F4 为例:
在这里我划分bootloader的启动代码内存大小为0xc000,也就是48kb大小,范围是0x8000000到0x800C000,所以app工程的flash大小为464kb,如下图所示:
iap工程:
![]()
app工程:

代码定义:用宏统一管理 APP 起始地址,避免硬编码:

三、实战拆解:Bootloader 核心代码逻辑
以下代码基于 STM32 HAL 库开发,涵盖 “开机模式判断”“APP 跳转”“IAP 菜单交互” 三大核心功能,逐行解析关键逻辑。
1. 初始化阶段:硬件与外设准备
上电后首先初始化硬件:
int main(void)
{
/* USER CODE BEGIN 1 */
SCB->VTOR = FLASH_BASE;
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_SPI1_Init();
MX_TIM2_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
delay_init();
Key_Port_Init();
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
//lcd
LCD_Init();
LCD_Fill(0, 0, LCD_W, LCD_H, BLACK);
delay_ms(10);
LCD_Set_Light(50);
//开机启动时如果按下KEY1, 进入boot中IAP升级模式
if(HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == 0)
{
// 延时判断是否真的按下
delay_ms(500);
if(HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == 0)
{
//go in boot menu
FLASH_If_Init();
Main_Menu();
}
}
}
2. 开机模式判断:按键触发 IAP 或跳转 APP
这是 Bootloader 的 “大脑”,决定设备上电后进入哪种模式:
在上电的时候,我通过是否按下按键,来判断是否进入bootloader下载模式,如果不进入,再检测是否之前已经下载过app文件,如果下载过,则直接进入app的flash内存段运行代码,
3. IAP 主菜单:实现固件下载与管理
进入 IAP 模式后,通过 Main_Menu 函数提供命令行交互,支持固件下载、跳转等功能:
void Main_Menu(void)
{
uint8_t key = 0;
SerialPutString("\r\n======================================================================");
SerialPutString("\r\n= (C) COPYRIGHT 2011 STMicroelectronics =");
SerialPutString("\r\n= =");
SerialPutString("\r\n= STM32F4xx In-Application Programming Application (Version 1.0.0) =");
SerialPutString("\r\n= =");
SerialPutString("\r\n= By MCD Application Team =");
SerialPutString("\r\n======================================================================");
SerialPutString("\r\n\r\n");
/* Test if any sector of Flash memory where user application will be loaded is write protected */
if (FLASH_If_GetWriteProtectionStatus() == 0)
{
FlashProtection = 1;
}
else
{
FlashProtection = 0;
}
while (1)
{
SerialPutString("\r\n================== Main Menu ============================\r\n\n");
SerialPutString(" Download Image To the STM32F4xx Internal Flash ------- 1\r\n\n");
SerialPutString(" Upload Image From the STM32F4xx Internal Flash ------- 2\r\n\n");
SerialPutString(" Execute The New Program ------------------------------ 3\r\n\n");
if(FlashProtection != 0)
{
SerialPutString(" Disable the write protection ------------------------- 4\r\n\n");
}
SerialPutString("==========================================================\r\n\n");
/* Receive key */
key = GetKey();
if (key == 0x31)
{
/* Download user application in the Flash */
SerialDownload();
}
else if (key == 0x32)
{
/* Upload user application from the Flash */
// SerialUpload();
SerialPutString("This function is disabled! Please Use 1\r");
}
else if (key == 0x33) /* execute the new program */
{
//user code here
SysTick->CTRL = 0X00;//禁止SysTick
SysTick->LOAD = 0;
SysTick->VAL = 0;
__disable_irq();
//set JumpAddress
JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
/* Jump to user application */
Jump_To_Application = (pFunction) JumpAddress;
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
Jump_To_Application();
}
else if ((key == 0x34) && (FlashProtection == 1))
{
/* Disable the write protection */
switch (FLASH_If_DisableWriteProtection())
{
case 1:
{
SerialPutString("Write Protection disabled...\r\n");
FlashProtection = 0;
break;
}
case 2:
{
SerialPutString("Error: Flash write unprotection failed...\r\n");
break;
}
default:
{
}
}
}
else
{
if (FlashProtection == 0)
{
SerialPutString("Invalid Number ! ==> The number should be either 1, 2 or 3\r");
}
else
{
SerialPutString("Invalid Number ! ==> The number should be either 1, 2, 3 or 4\r");
}
}
}
}
四、APP 工程适配:确保能被 Bootloader 正确跳转
Bootloader 跳转后,APP 需做两件关键适配,否则会卡死或中断失效:
1. 重映射中断向量表
STM32 默认从 Flash 起始地址(0x08000000)读取中断向量表,但 APP 存放在 0x0800C000,必须手动重映射:

2. 重新开启全局中断
Bootloader 跳转前通过 __disable_irq() 关闭了全局中断,APP 需重新开启,否则所有中断(如 UART、定时器)无法响应:
APP 中如何正确开启中断?
在 APP 工程的初始化阶段(通常是 main() 函数的早期,如外设初始化前或初始化后),通过以下函数清除全局中断屏蔽,恢复中断功能:
方法 1:使用 CMSIS 标准函数(推荐)
直接调用 __enable_irq();(与 Bootloader 中的 __disable_irq(); 对应),该函数会将 PRIMASK 寄存器清 0,允许所有可屏蔽中断响应:
int main(void)
{
// 1. 首先初始化 MCU 核心(如时钟:SystemClock_Config(),必须在开中断前,避免时钟异常)
HAL_Init();
SystemClock_Config(); // 关键:先配置时钟,再处理中断和外设
// 2. 开启全局中断(清除 Bootloader 遗留的中断屏蔽)
__enable_irq(); // 这一步必须加!
// 3. 后续初始化外设(UART、GPIO、定时器等,按需使能对应中断)
MX_USART1_UART_Init();
MX_TIM2_Init();
// ... 其他外设初始化
// 4. 主循环
while (1)
{
// APP 业务逻辑
}
}
方法 2:直接操作 PRIMASK 寄存器(等效)
若不使用 CMSIS 函数,也可直接通过汇编指令或寄存器操作清 0 PRIMASK(本质与 __enable_irq(); 一致):
// 方式 1:汇编指令(兼容所有 Cortex-M 内核)
__asm volatile ("cpsie i"); // "cpsie i" = Clear PRIMASK, Enable IRQ
// 方式 2:直接写寄存器(Cortex-M3/M4/M7 适用)
__set_PRIMASK(0); // PRIMASK=0 表示不屏蔽任何可屏蔽中断
关键注意事项
-
开中断的时机:必须在时钟初始化之后
SystemClock_Config()是 MCU 所有外设(包括 NVIC 中断控制器)的基础,若在时钟未配置前开中断,可能导致中断控制器工作异常。因此固定顺序为:HAL_Init() → SystemClock_Config() → __enable_irq() → 外设初始化。 -
APP 需单独配置自身的中断优先级
Bootloader 中的中断优先级配置(如HAL_NVIC_SetPriority())不会继承到 APP 中,APP 需根据自身需求重新配置外设中断优先级(否则中断可能因优先级错误无法响应)。 -
SysTick 需重新初始化
Bootloader 中已通过SysTick->CTRL = 0X00;关闭了 SysTick,APP 若依赖 SysTick(如HAL_Delay()函数),需在HAL_Init()后自动恢复(HAL_Init()会默认初始化 SysTick),无需额外操作。
如果忘记开中断会怎样?
- 现象:APP 能正常执行 “无中断依赖” 的逻辑(如单纯的 LED 闪烁、串口主动发送数据),但无法响应任何中断事件(如串口接收数据、按键中断触发、定时器溢出);若 APP 有 “等待中断标志” 的逻辑(如
while (HAL_UART_GetState(&huart1) != HAL_UART_STATE_READY);),会直接卡死在循环中。 - 排查:通过调试器查看
PRIMASK寄存器值,若为 1 则说明中断被屏蔽,需补充__enable_irq();。
五、效果演示
-
进入 IAP 模式:此时串口会打印一下信息

-
进入下载模式:通过写1发送给上位机,此时会进入等待发送bin文件状态

- 下载已经编写好的bin文件,通过ymodem协议发送单片机下载

-
出现 APP Flag Set Successfully!就表示已经下载成功
-
之后我们的单片机就会出现我们app工程所想要的演示的工程啦

六、总结:Bootloader 开发流程梳理
- 规划 Flash 分区:确定 Bootloader 和 APP 的地址范围,用宏统一管理;
- 开发 Bootloader 核心:
- 初始化硬件与外设;
- 实现开机模式判断(按键触发 IAP / 自动跳转 APP);
- 开发 IAP 菜单(下载、跳转、写保护控制);
- 适配 APP 工程:
- 重映射中断向量表;
- 开启全局中断;
- 写入 APP 合法标志;
- 测试验证:
- 测试 IAP 升级流程(下载固件 → 跳转 → 功能验证);
- 测试异常场景(无 APP、固件损坏、写保护开启)。
通过本文的拆解,你应该能掌握 STM32 Bootloader 的核心逻辑和开发要点。实际项目中,可根据需求扩展功能(如 BLE 无线升级、固件版本管理),但核心流程和原理是相通的。希望这篇文章能帮你少走弯路,高效开发出稳定的 Bootloader 功能!
更多推荐



所有评论(0)