【Embedded Development】【bootloader】基于MCU的bootloader详细介绍以及基于MCU串口的IAP实战详细教程

前言

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 为例)

若判定启动应用,需完成关键的 "控制权移交":

  1. 读取应用向量表第一个字,设置主堆栈指针(MSP);
  2. 读取向量表第二个字,获取应用程序的复位处理函数(Reset_Handler)地址;
  3. 通过函数指针跳转到 Reset_Handler 执行;
  4. 重定向中断向量表:修改 NVIC 的 VTOR 寄存器,确保应用中断正常响应(可由 Bootloader 或应用程序完成)。

5. 固件升级(IAP)流程

若判定进入升级模式,Bootloader 执行完整的固件更新逻辑:

  1. 初始化通信接口,与上位机 / 服务器建立交互;
  2. 按自定义协议接收固件数据块(如 "握手→传数据→应答");
  3. 数据校验:每接收一块数据,通过 CRC 等校验确保完整性;
  4. 擦除 Flash:按扇区擦除应用程序存储区(Flash 写前需擦除);
  5. 写入 Flash:将校验通过的数据块写入指定地址;
  6. 整体验证:全量数据写入后,校验整个固件的合法性;
  7. 复位设备:发送升级完成信号,触发软件复位,重启后加载新应用。

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 是设备的 "最后防线",开发时需规避以下问题:

  1. 大小控制:精简代码,避免引入冗余库,给应用程序预留足够空间;
  2. 稳定性优先:充分测试边界场景(通信中断、Flash 擦写失败、掉电),确保无致命 bug;
  3. 看门狗处理:擦写 Flash 等耗时操作时,定时 "喂狗",避免看门狗超时复位;
  4. 调试与日志:预留 UART 输出关键日志,或通过 JTAG/SWD 单步调试(注意调试器对硬件初始化的影响);
  5. Flash 写保护:启用 Bootloader 区写保护,防止被应用程序误修改;
  6. 版本管理:给 Bootloader 和应用程序添加版本号,便于追溯问题和兼容性校验。

1.7 总结

Bootloader 看似是 MCU 启动流程中的 "小段程序",却是嵌入式设备具备 "可升级、可恢复" 能力的核心。从简单的串口 IAP 到复杂的安全 OTA,Bootloader 的设计需兼顾精简性、稳定性和安全性。掌握其原理与开发要点,不仅能解决产品迭代维护的核心痛点,也是衡量嵌入式工程师能力的重要标尺。


二、bootloader实战:制作基于串口的IAP

注:硬件测试平台:立创天空星STM32F407VET6

STM32F407VET6操作的是扇区,且扇区大小不一致

STM32F405/415, STM32F407/417, STM32F427/437 and STM32F429/439 advanced Arm<Sup>®</Sup>-based 32-bit MCUs - Reference manual

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实战:效果演示

基于STM32的串口IAP实验_哔哩哔哩_bilibili


工程地址:

Molesidy/STM32_UART_IAP

相关推荐
长安第一美人4 小时前
php出现zend_mm_heap corrupted 或者Segment fault
开发语言·嵌入式硬件·php·zmq·工业应用开发
沐欣工作室_lvyiyi4 小时前
基于单片机的两轮自平衡循迹小车(论文+源码)
单片机·嵌入式硬件·小车·两轮自平衡
清风6666665 小时前
基于单片机的8路抢答器设计与实现
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
点灯小铭5 小时前
基于单片机的智能污水有害气体电子鼻检测系统
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
SystickInt7 小时前
32 DMA实现ROM与RAM通信
stm32·单片机·嵌入式硬件
俊昭喜喜里9 小时前
STM32开发板电源设计( DCDC 电路和 LDO 电路 )
stm32·单片机·嵌入式硬件
m0_555762909 小时前
方案再再对比
单片机
boneStudent10 小时前
Day20:串口基本配置与收发
stm32·单片机·嵌入式硬件
紫阡星影10 小时前
基于Arduino模拟烟雾监测系统
单片机·嵌入式硬件·arduino