前言
Bootloader、IAP、OTA、ISP

一、简介
Bootloader(引导加载程序)是嵌入式系统中运行于 MCU(微控制器)复位后、应用程序执行前的核心底层软件,是 MCU 启动流程的 "第一道关卡",其核心作用是初始化硬件、管理程序镜像的加载与更新,是实现嵌入式系统灵活升级、可靠启动的关键组件。
注:这里以STM32为例,以boot0/1选择启动方式为主flash启动,在flash开始时就是用户自定义的bootloader开始运行,再调整中断向量表,再移动MSP到Flash的App区域。
+---------------------+
| 上电启动 |
+---------------------+
|
V
+---------------------+
| 初始化时钟、GPIO、串口 |
+---------------------+
|
V
+---------------------+
| 检测升级触发条件 |
| (如按键/USB信号) |
+---------------------+
|------------------- 是 -------------------+
| |
否 |
| V
V +---------------------+
+---------------------+ | 进入Bootloader模式 |
| 检查APP有效性 | | (等待固件传输) |
| (CRC校验或标志位) | +---------------------+
+---------------------+ |
| |
有效 V
| +---------------------+
V | 接收数据并写入Flash |
+---------------------+ | (分块校验+烧录) |
| 跳转到应用程序(APP) | +---------------------+
+---------------------+ |
| |
+------------------- 完成 ------------------+
|
V
+---------------------+
| 复位或跳转到新APP |
+---------------------+
类似的bootloader:BIOS UBoot GRUB



1.1 初识 Bootloader:单片机的 "开机管家"
1. 什么是 Bootloader?
Bootloader 是 MCU 上电 / 复位后、主应用程序运行前执行的一段专用程序,类比电脑开机时的 BIOS/UEFI------ 电脑不会直接进入操作系统,需先经 BIOS 初始化硬件;MCU 的 Bootloader 也同理,仅规模更小、目标更专一:
- 核心功能:初始化基础硬件,决策 "启动主应用" 或 "进入固件升级模式";
- 存储特性:固化在 Flash 的指定区域,通常带写保护,避免被应用程序误破坏,一旦损坏可能导致设备 "无法启动"。
2. 为什么需要 Bootloader?
简单项目可直接运行应用程序(编译→烧录→运行),但面向量产、需维护的产品,Bootloader 是刚需:
- 解决远程升级痛点:产品部署后,无需拆机、无需专用烧录工具(如 J-Link/ST-Link),即可修复 bug 或新增功能;
- 提升产品灵活性:支持 "先发布、后升级",通过固件迭代优化功能;
- 增强系统可靠性:应用程序损坏时,可通过 Bootloader 恢复,避免设备 "变砖"。
1.2 Bootloader 核心工作原理
Bootloader 的运行逻辑围绕 "启动应用" 和 "固件升级" 两大核心目标展开,流程清晰且环环相扣:
1. 启动触发
MCU 上电 / 复位后,CPU 从 Flash 固定地址(复位向量表位置)取第一条指令;若 Bootloader 部署在该地址,则优先执行 Bootloader 代码。
2. 极简硬件初始化
Bootloader 仅初始化核心外设,满足基础功能即可,无需全量初始化:
- 时钟系统:确保 CPU 和核心外设正常运行;
- GPIO:检测升级触发引脚(如按键)、控制状态指示灯;
- 通信接口:UART/CAN/USB 等,用于接收升级固件或指令。
3. 核心决策:跑应用还是升固件?
初始化完成后,Bootloader 通过预设逻辑判断下一步操作:
- 检测 GPIO 引脚:上电时长按指定按钮,触发升级模式;
- 读取标志位:读取 Flash 共享区域的升级请求标志(由应用程序写入);
- 监听通信接口:短时间内(1-2 秒)等待升级指令,超时则启动应用;
- 校验应用程序:检测应用区 CRC / 校验和,异常则强制进入升级模式。
4. 跳转至应用程序(以 ARM Cortex-M 为例)
若判定启动应用,需完成关键的 "控制权移交":
- 读取应用向量表第一个字,设置主堆栈指针(MSP);
- 读取向量表第二个字,获取应用程序的复位处理函数(Reset_Handler)地址;
- 通过函数指针跳转到 Reset_Handler 执行;
- 重定向中断向量表:修改 NVIC 的 VTOR 寄存器,确保应用中断正常响应(可由 Bootloader 或应用程序完成)。
5. 固件升级(IAP)流程
若判定进入升级模式,Bootloader 执行完整的固件更新逻辑:
- 初始化通信接口,与上位机 / 服务器建立交互;
- 按自定义协议接收固件数据块(如 "握手→传数据→应答");
- 数据校验:每接收一块数据,通过 CRC 等校验确保完整性;
- 擦除 Flash:按扇区擦除应用程序存储区(Flash 写前需擦除);
- 写入 Flash:将校验通过的数据块写入指定地址;
- 整体验证:全量数据写入后,校验整个固件的合法性;
- 复位设备:发送升级完成信号,触发软件复位,重启后加载新应用。
1.3 Bootloader、IAP、OTA:理清三者关系
三者常被混用,但核心含义不同,且存在明确的依赖关系:
- Bootloader:实现固件加载 / 升级的基础程序,是 "能力载体";
- IAP(In-Application Programming):在设备运行中擦写程序存储器的技术 / 过程,是 "升级方式";
- OTA(Over-The-Air):通过无线链路(Wi-Fi / 蓝牙 / NB-IoT)交付固件的方式,是 "传输形式"。
核心关联:OTA 依赖 IAP 完成固件烧录,而 IAP 的可靠实现几乎离不开 Bootloader 的支撑。
1.4 动手开发简易 Bootloader:关键要点
开发 Bootloader 的核心是 "精简、稳定",重点关注以下环节:
1. Flash规划
需精确划分 Flash 区域,通过修改链接脚本指定各区域地址:
- Bootloader 区:如 0x8000000 开始,占用固定大小(如 48KB);
- 应用程序区:如 0x800C800 开始,为主要业务代码区;
- 共享区域:存储升级标志、版本号等交互数据。
示例(Keil 配置):直接指定应用程序起始地址为 0x800C800,Bootloader 默认从 0x8000000 开始。
2. 通信协议设计
需定义简洁、可靠的交互指令,示例(串口协议):
- 0x21/0x22:查询服务器固件版本;
- 0x24/0x25:申请获取 OTA 固件内容。
3. 核心代码实现(仅供参考)
(1)Bootloader 主逻辑
int main(void)
{
// 初始化核心外设:时钟、GPIO、串口
SystemInit();
GPIO_Init();
UART_Init();
// 判断是否进入升级模式
if(CheckIfUpdateRequest() == 1)
{
EnterUpdateMode(); // 进入升级模式
}
else
{
JumpToApplication(); // 跳转到应用程序
}
while(1);
}
(2)应用跳转函数(Cortex-M 示例)
#define APP_ADDRESS (uint32_t)0x800C800 // 应用程序起始地址
typedef void (*pFunction)(void); // 函数指针类型
void JumpToApplication(void)
{
uint32_t jumpAddress;
pFunction applicationEntry;
// 校验应用程序向量表有效性
if (((*(volatile uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000)
{
// 设置应用程序主堆栈指针
__set_MSP(*(volatile uint32_t*)APP_ADDRESS);
// 获取复位处理函数地址
jumpAddress = *(volatile uint32_t*)(APP_ADDRESS + 4);
applicationEntry = (pFunction)jumpAddress;
// 跳转到应用程序
applicationEntry();
}
else
{
// 应用程序无效,报错(如闪烁LED)
while(1) { GPIO_Toggle(LED_PIN); Delay(500); }
}
}
(3)升级模式逻辑
核心步骤:循环监听通信指令→接收固件数据块→CRC 校验→擦除 Flash→写入数据→全量校验→复位。
1.5 从 IAP 到 OTA:进阶升级能力
实现基础 IAP 后,若需无线升级,需扩展 OTA 能力,核心关注四点:
1. 无线通信栈集成
在 Bootloader(或专用下载器程序)中集成 Wi-Fi/BLE/NB-IoT 等通信协议栈;若 Bootloader 空间有限,可采用 "两阶段 Bootloader":第一阶段极简(仅初始化基础硬件),第二阶段加载带通信能力的程序。
2. 安全性保障
- 固件签名验签:服务器用私钥签名固件,设备用公钥验证,防止恶意固件;
- 传输加密:采用 TLS/DTLS 加密传输固件,避免数据被窃听 / 篡改;
- 安全存储:密钥需存储在 MCU 安全区域,防止泄露。
3. 健壮性设计
- 断点续传:记录传输进度,中断后从断点继续;
- 双备份区(Dual-Bank):固件分两个区域存储,升级失败时回滚到旧版本;
- 版本管理与回滚:记录版本号,新版本异常时自动切回稳定版本;
- 防掉电:升级过程中记录状态,断电后可恢复升级。
4. 后端支撑
需搭建固件管理服务器:存储固件包、管理设备列表、推送升级任务,形成 "服务器→无线链路→设备 Bootloader" 的完整 OTA 闭环。
1.6 Bootloader 开发避坑指南
Bootloader 是设备的 "最后防线",开发时需规避以下问题:
- 大小控制:精简代码,避免引入冗余库,给应用程序预留足够空间;
- 稳定性优先:充分测试边界场景(通信中断、Flash 擦写失败、掉电),确保无致命 bug;
- 看门狗处理:擦写 Flash 等耗时操作时,定时 "喂狗",避免看门狗超时复位;
- 调试与日志:预留 UART 输出关键日志,或通过 JTAG/SWD 单步调试(注意调试器对硬件初始化的影响);
- Flash 写保护:启用 Bootloader 区写保护,防止被应用程序误修改;
- 版本管理:给 Bootloader 和应用程序添加版本号,便于追溯问题和兼容性校验。
1.7 总结
Bootloader 看似是 MCU 启动流程中的 "小段程序",却是嵌入式设备具备 "可升级、可恢复" 能力的核心。从简单的串口 IAP 到复杂的安全 OTA,Bootloader 的设计需兼顾精简性、稳定性和安全性。掌握其原理与开发要点,不仅能解决产品迭代维护的核心痛点,也是衡量嵌入式工程师能力的重要标尺。
二、bootloader实战:制作基于串口的IAP
注:硬件测试平台:立创天空星STM32F407VET6
STM32F407VET6操作的是扇区,且扇区大小不一致


2.1 使用STM32CubeMX创建工程
选择STM32F407VET6,打开外部高速时钟RCC,打开SYS的SWD下载模式,使用STLink下载,开启串口一的异步通信,不开启串口中断,记得将外部高速时钟改为8MHz。

2.2 开始编写代码
重定向printf

cpp
#include <stdio.h>
int fputc(int ch, FILE* f)
{
HAL_UART_Transmit(&huart1, (const uint8_t*)&ch, 1, 1000);
return ch;
}
2.3 宏定义

cpp
/* USER CODE BEGIN PD */
//STM32F407VET6 的 FLASH 操作完全以扇区为最小擦除 / 管理单位(无 "页" 的概念)
//其扇区大小并非统一值,而是按 "差异化划分" 设计(前 4 个小扇区、1 个中等扇区、7 个大扇区)
//STM32F407VET6 扇区大小 & 地址范围(精准版)
//扇区编号 起始地址 结束地址 大小 十六进制大小 备注
//FLASH_SECTOR_0 0x08000000 0x08003FFF 16 KB 0x4000 适合存放 Bootloader(小容量、频繁更新)
//FLASH_SECTOR_1 0x08004000 0x08007FFF 16 KB 0x4000
//FLASH_SECTOR_2 0x08008000 0x0800BFFF 16 KB 0x4000
//FLASH_SECTOR_3 0x0800C000 0x0800FFFF 16 KB 0x4000
//FLASH_SECTOR_4 0x08010000 0x0801FFFF 64 KB 0x10000 中等扇区
//FLASH_SECTOR_5 0x08020000 0x0803FFFF 128 KB 0x20000 适合存放 APP 固件(大容量)
//FLASH_SECTOR_6 0x08040000 0x0805FFFF 128 KB 0x20000
//FLASH_SECTOR_7 0x08060000 0x0807FFFF 128 KB 0x20000
//FLASH_SECTOR_8 0x08080000 0x0809FFFF 128 KB 0x20000
//FLASH_SECTOR_9 0x080A0000 0x080BFFFF 128 KB 0x20000
//FLASH_SECTOR_10 0x080C0000 0x080DFFFF 128 KB 0x20000
//FLASH_SECTOR_11 0x080E0000 0x080FFFFF 128 KB 0x20000
//把这个bootloader可以看为也是一个App,不过作用有点特殊而已,主要作用是接受固件,写固件到flash,然后将MSP(SP)主栈指针指向新的App
//立创天空星STM32F407VET6: 主频:168MHz flash:512KB SRAM:196KB
#define SRAM_App_Start_Addr ((uint32_t)0x20010000) //初始运行用户自定义的bootloader 指定SRAM起始地址
#define SRAM_App_Size (0xC000) //如果App较小 App在SRAM运行时可以限定一下其占SRAM的范围
#define SRAM_App_End_Addr ((uint32_t)(SRAM_App_Start_Addr+SRAM_App_Size)) //初始运行用户自定义的bootloader 指定SRAM结束地址 这是运行bootloader需要的SRAM限定空间
#define Flash_Start_Addr ((uint32_t)0x08000000) //指定Flash起始地址:0x08000000
#define Bootloader_Size (0xFFFF) //大小,64KB,1个sector:16 KB,Bootloader直接使用前4个sector
#define Application_Start_Addr (Flash_Start_Addr + Bootloader_Size+1 + 0xFFFF+1) //App的起始地址:0x08020000 把app放在bootloader后面的0x08020000 App直接使用2个sector
#define Update_CMD "update" //擦除并升级指令 开始升级
#define Application_Size (1024 * 128 * 2) //App大小256KB
#define Application_End_Addr ((uint32_t)(Application_Start_Addr+Application_Size)) //App的结束地址:0x0805FFFF
#define AppFirmwareImageBuf_Size (256) //App固件接收缓冲区,一包是256B
#define IAP_UART_Port huart1
/* USER CODE END PD */
2.4 全局变量

cpp
/* USER CODE BEGIN PV */
uint8_t AppFirmwareImageBuf[AppFirmwareImageBuf_Size] = {0}; //一片/一包固件 建议不要在栈上开辟数组
IAP_FUN Jump_To_App; //函数指针
uint8_t bootloader_flag = 0; //是否升级标志
uint8_t time_out_flag = 0; //超时标记
/* USER CODE END PV */
2.5 函数内部
2.5.1 判断是否更新App
10s判断接收命令,30s发送bin文件,我未使用特殊的上位机,使用sscom中每隔100ms发送256字节模式发送bin文件,先擦除App空间,然后,一定要注意,各芯片操作flash的方式有所不同,有些操作的是页或者扇区,页或者扇区的大小不一定相同

cpp
printf("bootloader is running\r\n");
printf("Let's do it\r\n");
//判断是否更新
for(uint8_t i = 0; i < 10; i++)
{
printf("在十秒内输入: update 即更新App 计时: %d 秒\r\n", (i+1));
//上电后阻塞10秒等待接收升级指令,连续10秒未收到则跳转App
//如果10秒内收到,则接收App数据并擦写写入
HAL_UART_Receive(&IAP_UART_Port, AppFirmwareImageBuf, AppFirmwareImageBuf_Size, 1000); //阻塞等待升级命令
if(strstr((const char *)AppFirmwareImageBuf, Update_CMD) != NULL) //在发来的数据里查找update字符串
{
FLASH_EraseInitTypeDef EraseInitStruct; //需要擦除的地方
uint32_t SectorError; //Sector操作返回值
HAL_FLASH_Unlock(); //解锁
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS; //操作sector 指定擦除类型(扇区擦除 / 全片擦除)
EraseInitStruct.Banks = FLASH_BANK_1; //全片擦除时指定要擦除的 FLASH 存储体(Bank) F407 只有FLASH_BANK_1(1MB FLASH 仅占 Bank1);
EraseInitStruct.Sector = FLASH_SECTOR_5; //扇区擦除时指定起始扇区(从哪个扇区开始擦) F407 取值:FLASH_SECTOR_0 ~ FLASH_SECTOR_11(共 12 个扇区)
EraseInitStruct.NbSectors = 2; //扇区擦除时指定要擦除的扇区数量(连续擦除多少个) 取值范围:1 ≤ NbSectors ≤ (总扇区数 - 起始扇区编号)(F407 总扇区 12,比如起始扇区 5 → 最大可设 7)
EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; //指定芯片供电电压范围(决定 FLASH 擦除的并行度 / 速度)
if(HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK) //擦除App空间
{
HAL_FLASH_Lock(); //上锁
printf("------------Erase fail at:0x%x\r\n",SectorError);
return -1;
}
HAL_FLASH_Lock(); //上锁
bootloader_flag = 1; //升级启动
printf("------------Erase OK\r\n");
break; //跳出等待
}
}
2.5.2 是否进入bootloader的边接收写入flash阶段
30s内要接收到数据,接收256字节数据,同时,再写入flash(间隔会有100ms,必须要间隔时间来写入Flash),然后循环接收直到无接收到数据就退出

cpp
/* USER CODE BEGIN PTD */
typedef void (*IAP_FUN)(void);
/* USER CODE END PTD */
cpp
//是否进入升级
if(bootloader_flag == 1)
{
HAL_StatusTypeDef ret = HAL_ERROR; //存储判断返回值
uint32_t CurrentAppAddr = 0; //当前写的地址
uint32_t Data_Word = 0; //一个字的缓存值
uint8_t packet_count = 0; //记录接收多少包
printf("------------ready to receive bin, please send in 30s\r\n");
CurrentAppAddr = Application_Start_Addr; //以App起始地址开始
ret = HAL_UART_Receive(&IAP_UART_Port, AppFirmwareImageBuf, AppFirmwareImageBuf_Size, 30*1000); //用串口工具发bin文件 30S内必须要接收到
if(ret == HAL_TIMEOUT)
{
printf("------------time out, end wait to receive bin\r\n");
return -1;
} else if (ret == HAL_OK) {
//收到则循环接收,每秒阻塞接收256字节,并写入Flash
while(1)
{
HAL_FLASH_Unlock();
for(uint8_t i = 0; i < AppFirmwareImageBuf_Size/4; i++)
{
Data_Word = *((uint32_t *)(&AppFirmwareImageBuf[i << 2])); //i << 2 等价于 i * 4(左移 n 位 = 乘以 2^n,2^2=4)即步进4个字节
if(CurrentAppAddr < Application_End_Addr)
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, CurrentAppAddr, Data_Word) == HAL_OK) //写入一个字的数据
{
CurrentAppAddr += 4;
}else{
HAL_FLASH_Lock();
printf("------------write flash fail at: 0x%x\r\n", CurrentAppAddr);
return -1;
}
}
} //接收完一包
HAL_FLASH_Lock();
printf("------------write 256 btye OK: 0x%x\t%d\r\n", CurrentAppAddr, ++packet_count);
ret = HAL_UART_Receive(&IAP_UART_Port, AppFirmwareImageBuf, AppFirmwareImageBuf_Size, 2*1000); //2秒内阻塞式等待
if(ret == HAL_TIMEOUT) //没有数据发来就默认发完bin文件就跳转到启动App
{
time_out_flag++;
if(time_out_flag == 2) //第二次超时就退出
{
printf("------------End write OK\r\n");
goto START_APP; //写入完App后尝试跳转
}
}
}
}
}else{ //bootloader_flag =0
2.5.3 是否进入跳转App
校验写入的App是不是对应这个bootloader的App(写入到0x08020000到0x08020003地址的四个字节是不是SP栈顶指针在之前约定好的范围内(SRAM里的某一个地址))

cpp
}else{ //bootloader_flag =0
START_APP:
printf("------------start user app\r\n");
HAL_Delay(10);
/*
校验App有效性:Flash中App起始地址存储的是App的中断向量表(第一项就是栈顶指针MSP(SP) 第二项"复位地址")
向量表第1个4字节:App的栈顶地址(MSP),需落在芯片合法SRAM区间内
*/
printf("------------Application_Start_Addr:%0x\r\n", *(unsigned int *)Application_Start_Addr); //没有App的情况下去访问的值为0xFFFFFFFF(擦除状态)
//由KEIL做出来的bin文件,在编译出的时候在魔法棒里会调整整个App程序的链接Flash的入口地址以及SRAM起始地址,比如这里面的Application_Start_Addr:0x08020000
//在这里面的宏定义Application_Start_Addr是一个宏定义的32位数据,是App空间的起始(写入Flash)地址
//去访问Application_Start_Addr(0x08020000)这个地址上的值,以unsigned int*访问,即访问到32位的数据,这个数据就是以后App在SRAM运行起来的栈顶指针(地址)
//然后在下面去判断这个App在SRAM运行起来的栈顶指针(地址) 是否是符合:bootloader和做固件的时候是一同规定的App栈顶指针在限定的SRAM里面所处于的范围
//由于后续App的固件大小不确定,然后编译出来的bin固件文件里MSP里装的栈顶指针(地址),这个栈顶实在编译和链接Flash/SRAM过程当中决定的,这个栈顶只能在魔法棒的SRAM起始地址和其大小限定的那个范围里
//如果属于这个限定的范围内就成功完成校验,然后跳转
if( ((*(unsigned int *)Application_Start_Addr) >= SRAM_App_Start_Addr) &&
((*(unsigned int *)Application_Start_Addr) <= SRAM_App_End_Addr)
)
{
//关中断 防止被打断 在App里记得需要开启中断
__disable_irq();
//Application_Start_Addr +4 放的是中断向量表的第二项"复位地址" (第一项就是栈顶指针MSP(SP))
__set_MSP(*(unsigned int *)Application_Start_Addr); //手动指定MSP到新App起始地址
Jump_To_App = (IAP_FUN)*(unsigned int *)(Application_Start_Addr + 4); //将复位函数指针移到(新App起始地址+4) Jump_To_App就是函数指针
Jump_To_App(); //调用复用函数
}else{
printf("the value of MSP error\r\n");
}
}
最后下载烧录bootloader
源码
cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include <string.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
typedef void (*IAP_FUN)(void);
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
//STM32F407VET6 的 FLASH 操作完全以扇区为最小擦除 / 管理单位(无 "页" 的概念)
//其扇区大小并非统一值,而是按 "差异化划分" 设计(前 4 个小扇区、1 个中等扇区、7 个大扇区)
//STM32F407VET6 扇区大小 & 地址范围(精准版)
//扇区编号 起始地址 结束地址 大小 十六进制大小 备注
//FLASH_SECTOR_0 0x08000000 0x08003FFF 16 KB 0x4000 适合存放 Bootloader(小容量、频繁更新)
//FLASH_SECTOR_1 0x08004000 0x08007FFF 16 KB 0x4000
//FLASH_SECTOR_2 0x08008000 0x0800BFFF 16 KB 0x4000
//FLASH_SECTOR_3 0x0800C000 0x0800FFFF 16 KB 0x4000
//FLASH_SECTOR_4 0x08010000 0x0801FFFF 64 KB 0x10000 中等扇区
//FLASH_SECTOR_5 0x08020000 0x0803FFFF 128 KB 0x20000 适合存放 APP 固件(大容量)
//FLASH_SECTOR_6 0x08040000 0x0805FFFF 128 KB 0x20000
//FLASH_SECTOR_7 0x08060000 0x0807FFFF 128 KB 0x20000
//FLASH_SECTOR_8 0x08080000 0x0809FFFF 128 KB 0x20000
//FLASH_SECTOR_9 0x080A0000 0x080BFFFF 128 KB 0x20000
//FLASH_SECTOR_10 0x080C0000 0x080DFFFF 128 KB 0x20000
//FLASH_SECTOR_11 0x080E0000 0x080FFFFF 128 KB 0x20000
//把这个bootloader可以看为也是一个App,不过作用有点特殊而已,主要作用是接受固件,写固件到flash,然后将MSP(SP)主栈指针指向新的App
//立创天空星STM32F407VET6: 主频:168MHz flash:512KB SRAM:196KB
#define SRAM_App_Start_Addr ((uint32_t)0x20010000) //初始运行用户自定义的bootloader 指定SRAM起始地址
#define SRAM_App_Size (0xC000) //如果App较小 App在SRAM运行时可以限定一下其占SRAM的范围
#define SRAM_App_End_Addr ((uint32_t)(SRAM_App_Start_Addr+SRAM_App_Size)) //初始运行用户自定义的bootloader 指定SRAM结束地址 这是运行bootloader需要的SRAM限定空间
#define Flash_Start_Addr ((uint32_t)0x08000000) //指定Flash起始地址:0x08000000
#define Bootloader_Size (0xFFFF) //大小,64KB,1个sector:16 KB,Bootloader直接使用前4个sector
#define Application_Start_Addr (Flash_Start_Addr + Bootloader_Size+1 + 0xFFFF+1) //App的起始地址:0x08020000 把app放在bootloader后面的0x08020000 App直接使用2个sector
#define Update_CMD "update" //擦除并升级指令 开始升级
#define Application_Size (1024 * 128 * 2) //App大小256KB
#define Application_End_Addr ((uint32_t)(Application_Start_Addr+Application_Size)) //App的结束地址:0x0805FFFF
#define AppFirmwareImageBuf_Size (256) //App固件接收缓冲区,一包是256B
#define IAP_UART_Port huart1
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint8_t AppFirmwareImageBuf[AppFirmwareImageBuf_Size] = {0}; //一片/一包固件 建议不要在栈上开辟数组
IAP_FUN Jump_To_App; //函数指针
uint8_t bootloader_flag = 0; //是否升级标志
uint8_t time_out_flag = 0; //超时标记
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
//重定向
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&IAP_UART_Port, (const uint8_t *)&ch, 1, 1000);
return ch;
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* 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_USART2_UART_Init();
/* USER CODE BEGIN 2 */
printf("bootloader is running\r\n");
printf("Let's do it\r\n");
//判断是否更新
for(uint8_t i = 0; i < 10; i++)
{
printf("在十秒内输入: update 即更新App 计时: %d 秒\r\n", (i+1));
//上电后阻塞10秒等待接收升级指令,连续10秒未收到则跳转App
//如果10秒内收到,则接收App数据并擦写写入
HAL_UART_Receive(&IAP_UART_Port, AppFirmwareImageBuf, AppFirmwareImageBuf_Size, 1000); //阻塞等待升级命令
if(strstr((const char *)AppFirmwareImageBuf, Update_CMD) != NULL) //在发来的数据里查找update字符串
{
FLASH_EraseInitTypeDef EraseInitStruct; //需要擦除的地方
uint32_t SectorError; //Sector操作返回值
HAL_FLASH_Unlock(); //解锁
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS; //操作sector 指定擦除类型(扇区擦除 / 全片擦除)
EraseInitStruct.Banks = FLASH_BANK_1; //全片擦除时指定要擦除的 FLASH 存储体(Bank) F407 只有FLASH_BANK_1(1MB FLASH 仅占 Bank1);
EraseInitStruct.Sector = FLASH_SECTOR_5; //扇区擦除时指定起始扇区(从哪个扇区开始擦) F407 取值:FLASH_SECTOR_0 ~ FLASH_SECTOR_11(共 12 个扇区)
EraseInitStruct.NbSectors = 2; //扇区擦除时指定要擦除的扇区数量(连续擦除多少个) 取值范围:1 ≤ NbSectors ≤ (总扇区数 - 起始扇区编号)(F407 总扇区 12,比如起始扇区 5 → 最大可设 7)
EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; //指定芯片供电电压范围(决定 FLASH 擦除的并行度 / 速度)
if(HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK) //擦除App空间
{
HAL_FLASH_Lock(); //上锁
printf("------------Erase fail at:0x%x\r\n",SectorError);
return -1;
}
HAL_FLASH_Lock(); //上锁
bootloader_flag = 1; //升级启动
printf("------------Erase OK\r\n");
break; //跳出等待
}
}
//是否进入升级
if(bootloader_flag == 1)
{
HAL_StatusTypeDef ret = HAL_ERROR; //存储判断返回值
uint32_t CurrentAppAddr = 0; //当前写的地址
uint32_t Data_Word = 0; //一个字的缓存值
uint8_t packet_count = 0; //记录接收多少包
printf("------------ready to receive bin, please send in 30s\r\n");
CurrentAppAddr = Application_Start_Addr; //以App起始地址开始
ret = HAL_UART_Receive(&IAP_UART_Port, AppFirmwareImageBuf, AppFirmwareImageBuf_Size, 30*1000); //用串口工具发bin文件 30S内必须要接收到
if(ret == HAL_TIMEOUT)
{
printf("------------time out, end wait to receive bin\r\n");
return -1;
} else if (ret == HAL_OK) {
//收到则循环接收,每秒阻塞接收256字节,并写入Flash
while(1)
{
HAL_FLASH_Unlock();
for(uint8_t i = 0; i < AppFirmwareImageBuf_Size/4; i++)
{
Data_Word = *((uint32_t *)(&AppFirmwareImageBuf[i << 2])); //i << 2 等价于 i * 4(左移 n 位 = 乘以 2^n,2^2=4)即步进4个字节
if(CurrentAppAddr < Application_End_Addr)
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, CurrentAppAddr, Data_Word) == HAL_OK) //写入一个字的数据
{
CurrentAppAddr += 4;
}else{
HAL_FLASH_Lock();
printf("------------write flash fail at: 0x%x\r\n", CurrentAppAddr);
return -1;
}
}
} //接收完一包
HAL_FLASH_Lock();
printf("------------write 256 btye OK: 0x%x\t%d\r\n", CurrentAppAddr, ++packet_count);
ret = HAL_UART_Receive(&IAP_UART_Port, AppFirmwareImageBuf, AppFirmwareImageBuf_Size, 2*1000); //2秒内阻塞式等待
if(ret == HAL_TIMEOUT) //没有数据发来就默认发完bin文件就跳转到启动App
{
time_out_flag++;
if(time_out_flag == 2) //第二次超时就退出
{
printf("------------End write OK\r\n");
goto START_APP; //写入完App后尝试跳转
}
}
}
}
}else{ //bootloader_flag =0
START_APP:
printf("------------start user app\r\n");
HAL_Delay(10);
/*
校验App有效性:Flash中App起始地址存储的是App的中断向量表(第一项就是栈顶指针MSP(SP) 第二项"复位地址")
向量表第1个4字节:App的栈顶地址(MSP),需落在芯片合法SRAM区间内
*/
printf("------------Application_Start_Addr:%0x\r\n", *(unsigned int *)Application_Start_Addr); //没有App的情况下去访问的值为0xFFFFFFFF(擦除状态)
//由KEIL做出来的bin文件,在编译出的时候在魔法棒里会调整整个App程序的链接Flash的入口地址以及SRAM起始地址,比如这里面的Application_Start_Addr:0x08020000
//在这里面的宏定义Application_Start_Addr是一个宏定义的32位数据,是App空间的起始(写入Flash)地址
//去访问Application_Start_Addr(0x08020000)这个地址上的值,以unsigned int*访问,即访问到32位的数据,这个数据就是以后App在SRAM运行起来的栈顶指针(地址)
//然后在下面去判断这个App在SRAM运行起来的栈顶指针(地址) 是否是符合:bootloader和做固件的时候是一同规定的App栈顶指针在限定的SRAM里面所处于的范围
//由于后续App的固件大小不确定,然后编译出来的bin固件文件里MSP里装的栈顶指针(地址),这个栈顶实在编译和链接Flash/SRAM过程当中决定的,这个栈顶只能在魔法棒的SRAM起始地址和其大小限定的那个范围里
//如果属于这个限定的范围内就成功完成校验,然后跳转
if( ((*(unsigned int *)Application_Start_Addr) >= SRAM_App_Start_Addr) &&
((*(unsigned int *)Application_Start_Addr) <= SRAM_App_End_Addr)
)
{
//关中断 防止被打断 在App里记得需要开启中断
__disable_irq();
//Application_Start_Addr+4 放的是中断向量表的第二项"复位地址" (第一项就是栈顶指针MSP(SP))
__set_MSP(*(unsigned int *)Application_Start_Addr); //手动指定MSP到新App起始地址
Jump_To_App = (IAP_FUN)*(unsigned int *)(Application_Start_Addr + 4); //将复位函数指针移到(新App起始地址+4) Jump_To_App就是函数指针
Jump_To_App(); //调用复用函数
}else{
printf("the value of MSP error\r\n");
}
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
printf("------------defualt bootloader is running and no app\r\n");
HAL_Delay(150);
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
三、bootloader实战:制作对应基于串口的IAP的App固件(bin文件)
3.1 同样使用STM32CubeMX创建一个工程
其中开启一个GPIO接上LED灯,通过控制反转电平速度,和开启串口一的打印信息,来观察是否App更新成功
3.2 编写App代码
Bootloader 执行Jump_To_App()后,CPU 会直接跳转到 APP 的复位中断服务函数 (即 APP 中断向量表中Application_Start_Addr + 4指向的地址),后续将完全由 APP 接管系统控制权。
Bootloader:等待升级指令 → 擦除0x08020000~0x0805FFFF扇区 → 接收 APP 固件并写入 Flash → 校验栈顶地址 → 关闭中断 → 设置 MSP → 跳转到 APP 复位地址。
APP:复位函数执行(汇编)→ 初始化栈 / 全局变量 → main()中重映射向量表 → 恢复全局中断 → 初始化硬件 → 执行业务逻辑。
cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int fputc(int ch, FILE* f)
{
HAL_UART_Transmit(&huart1, (const uint8_t*)&ch, 1, 1000);
return ch;
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
//STM32 的中断响应完全依赖SCB->VTOR寄存器指向的向量表基地址,
//而 IAP 跳转后 APP 的中断向量表地址与默认值不匹配,且任何延迟都可能触发中断寻址错误,直接导致程序崩溃。
SCB->VTOR = (uint32_t)(0x08020000); //中断向量表地址重映射
__enable_irq();
/* 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();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_2);
printf("This is App1 . 1000 ms set LED\r\n");
HAL_Delay(1000);
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
3.3 调整链接规则

3.4 产生bin文件
cpp
fromelf --bin -o "$L@L.bin" "#L


3.5 编译产生

四、bootloader实战:效果演示


工程地址: