在嵌入式开发中,STM32 设备的固件升级是高频需求 —— 如果每次升级都要拆设备、接调试器,不仅效率低,还可能损坏硬件。而 Bootloader(引导加载程序) 就是解决这个问题的核心:它能让设备上电后自主选择 “升级模式” 或 “运行应用模式”,实现无需调试器的 IAP(在应用编程)升级

        本文结合实际项目代码,从原理到实战,手把手拆解 STM32 Bootloader 的开发逻辑,帮你彻底掌握这一核心技能。

      

一、Bootloader 是什么?它的核心价值在哪?

简单说,Bootloader 是 STM32 上电后第一个运行的程序,相当于设备的 “启动管家”,主要负责两件事:

  1. 模式选择:开机时检测按键,按下指定按键(如 KEY1)进入「IAP 升级模式」,否则尝试跳转到「用户 APP 模式」;
  2. 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 表示不屏蔽任何可屏蔽中断

关键注意事项

  1. 开中断的时机:必须在时钟初始化之后
    SystemClock_Config() 是 MCU 所有外设(包括 NVIC 中断控制器)的基础,若在时钟未配置前开中断,可能导致中断控制器工作异常。因此固定顺序为:
    HAL_Init() → SystemClock_Config() → __enable_irq() → 外设初始化

  2. APP 需单独配置自身的中断优先级
    Bootloader 中的中断优先级配置(如 HAL_NVIC_SetPriority())不会继承到 APP 中,APP 需根据自身需求重新配置外设中断优先级(否则中断可能因优先级错误无法响应)。

  3. 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();

五、效果演示

  1. 进入 IAP 模式:此时串口会打印一下信息

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

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

  4. 出现 APP Flag Set Successfully!就表示已经下载成功

  5. 之后我们的单片机就会出现我们app工程所想要的演示的工程啦

六、总结:Bootloader 开发流程梳理

  1. 规划 Flash 分区:确定 Bootloader 和 APP 的地址范围,用宏统一管理;
  2. 开发 Bootloader 核心
    • 初始化硬件与外设;
    • 实现开机模式判断(按键触发 IAP / 自动跳转 APP);
    • 开发 IAP 菜单(下载、跳转、写保护控制);
  3. 适配 APP 工程
    • 重映射中断向量表;
    • 开启全局中断;
    • 写入 APP 合法标志;
  4. 测试验证
    • 测试 IAP 升级流程(下载固件 → 跳转 → 功能验证);
    • 测试异常场景(无 APP、固件损坏、写保护开启)。

通过本文的拆解,你应该能掌握 STM32 Bootloader 的核心逻辑和开发要点。实际项目中,可根据需求扩展功能(如 BLE 无线升级、固件版本管理),但核心流程和原理是相通的。希望这篇文章能帮你少走弯路,高效开发出稳定的 Bootloader 功能!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐