bootloader介绍
bootloader翻译过来是引导加载程序,我一开始把这个和OTA搞混了,简单来说他就是上电后最先执行的一段程序,这个和硬件上给BOOT0接1不太一样,一般只有芯片被锁才会给boot0接1通过串口擦除或烧录程序 附上git工程bootloader
写这个bootloader有什么用处呢,首先它可以不烧录就能够切换程序,其次能够接收数据实现OTA功能(这个是我后面会说)之类的,目前我能想到的就是这两个。
芯片上电后的过程
1. 硬件复位与 Boot 引脚采样 (Hardware Reset)
- 上电/复位 :当 NRST 引脚检测到复位信号或电源电压达到阈值时,CPU 内核复位。
- 采样 Boot 引脚 :硬件电路采样 BOOT0 和 BOOT1 引脚的电平状态。
- 根据电平组合,决定从哪个存储区域开始执行代码:
- Main Flash (0x0800 0000):正常用户程序启动。
- System Memory (0x1FFF xxxx):进入厂家固化的 ISP Bootloader。
- SRAM (0x2000 0000):从内存启动(常用于调试)。
- 映射地址 :如果从 Main Flash 启动,硬件会将 Flash 的起始地址映射到 0x0000 0000(别名区),以便 CPU 取指。
这里一般就指向0x08000000
2. 获取初始栈指针 (MSP) 和复位向量
CPU 从启动地址(通常是 0x0800 0000)读取前两个字(Word,32位):
- CPU 将这个值加载到 MSP (Main Stack Pointer) 寄存器。栈用于存放局部变量、函数调用上下文等。
- CPU 将这个地址加载到 PC (Program Counter) 寄存器。
- 此时,PC 指向了启动文件中的 Reset_Handler 函数。
注意 :这两个值构成了中断向量表 (Vector Table) 的前两项。
这里就是常说的指向向量中断表,每个程序的都不一样,所以得确保跳转之后指向的是与之相对应的,后面我会有代码解释
- 如果使用了 C++ 或特定的 C 库特性,编译器会生成初始化数组。
3. 执行 Reset_Handler (汇编阶段)
Reset_Handler 通常位于启动文件(如 startup_stm32f10x.s)中,是一段汇编代码。它主要做以下几件事:
关闭中断 :确保在初始化完成前不会响应任何中断,防止不可预知的行为。
系统时钟初始化 (SystemInit) :
- 调用 C 语言函数 SystemInit()(位于 [system_stm32f1xx.c](javascript:void(0)))。
- 配置 PLL、HSE/HSI、分频系数等,将系统时钟提升到目标频率(如 72MHz)。
- 配置 Flash 等待周期(Wait States),确保 CPU 能正确读取 Flash。
数据段初始化 (.data 和 .bss) :
- .data 段拷贝 :将存储在 Flash 中的已初始化全局变量/静态变量复制到 SRAM 中。因为 SRAM 断电后数据丢失,所以初始值必须存在 Flash 里,上电后搬移到 SRAM。
- .bss 段清零 :将未初始化的全局变量/静态变量所在的 SRAM 区域清零。C 标准规定未初始化的全局变量默认值为 0。
- 启动代码会遍历这些数组,调用全局对象的构造函数(C++)或特定的初始化函数。
- 对于纯 C 工程,这一步可能为空或非常简短。
这里在代码里主要就是关中断,systick
4. 调用 C 库初始化 (__libc_init_array / __init_array)
- 如果使用了 C++ 或特定的 C 库特性,编译器会生成初始化数组。
- 启动代码会遍历这些数组,调用全局对象的构造函数(C++)或特定的初始化函数。
- 对于纯 C 工程,这一步可能为空或非常简短。
5. 跳转到 main() 函数
- 完成上述所有初始化后,Reset_Handler 最后会调用 C 标准库入口函数 __main(注意不是 main)。
- __main 是编译器生成的辅助代码,负责最终的环境设置(如堆初始化),然后正式跳转到用户编写的 main() 函数。
- 从此,程序进入用户应用逻辑,开始无限循环或任务调度。
原理简述
现在上电过程讲完了,能看到正常没有bootloader的话,他会指向0x08000000这个地址,从这个地址开始执行程序,如果想用bootloader,我们可以把bootloader写在这个首地址,把APP即应用功能写在别的地址分块,从而能够实现多功能
就像这个表格
| 0x08000000~0x08003FFF | 16 KB | Bootloader | 上电首先执行,负责跳转到有效 APP |
| 0x08004000~0x08007FFF | 16 KB | APP1(主应用) | 正常功能固件 |
| 0x08008000~0x0800BFFF | 16 KB | APP2(备份/升级应用) | 可作 OTA 备份或应急功能 |
| 0x0800C000~0x0800FFFF | 16 KB | APP3(测试/诊断应用) | 特殊功能,如出厂诊断 |
| 0x08010000~ … | 剩余空间 | 保留 / 配置参数区 | 存放标志位、版本信息等 |
无 Bootloader 时 :直接从 0x08000000 执行单一程序。
有 Bootloader 时 :
Bootloader 先运行,根据标志(按键/升级标志/CRC等)决定跳转到哪个 APP。
每个 APP 编译时需设置独立的 链接地址(VECT_TAB_OFFSET) ,并重新映射中断向量表到自身地址。
代码解析
现在看一下boot的程序
/* USER CODE BEGIN 0 */
// app的程序在芯片flash的位置
#define APP_ADDRESS 0x08002000U
typedef void (*pFunction)(void);
void JumpToApplication(void) {
// 1. 检查应用程序地址是否有效
// 应用程序的栈顶地址位于其起始地址的第一个字
if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
// 2. 禁用所有中断(防止 Bootloader 中断干扰 App)
__disable_irq();
// 3. 关闭 SysTick、清除挂起的中断(干净的执行环境)
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
for (uint32_t i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFF; // 禁用所有中断
NVIC->ICPR[i] = 0xFFFFFFFF; // 清除挂起标志
}
// 2. 将中断向量表重定向到应用程序地址
SCB->VTOR = APP_ADDRESS;
// 3. 获取应用程序的栈顶地址和复位处理函数地址
pFunction app_reset_handler = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4));
__set_MSP(*(__IO uint32_t*)APP_ADDRESS);
// 4.重新使能中断
//__enable_irq();
// 5. 跳转到应用程序
app_reset_handler();
}else{
// 没有识别到app
}
}
/* 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();
/* USER CODE BEGIN 2 */
uint32_t startTime = HAL_GetTick();
uint32_t timeout = 2000; // 2秒超时
// 在2秒内轮询检测按键状态
while ((HAL_GetTick() – startTime) < timeout) {
// 检测 KEY0 是否被按下 (低电平有效)
if (HAL_GPIO_ReadPin(Key0_GPIO_Port, Key0_Pin) == GPIO_PIN_RESET) {
// 可选:添加简单的去抖动延迟,例如 10-20ms
HAL_Delay(20);
// 再次确认按键仍然处于按下状态
if (HAL_GPIO_ReadPin(Key0_GPIO_Port, Key0_Pin) == GPIO_PIN_RESET) {
JumpToApplication();
}
}
// 短暂延迟以降低 CPU 占用率,非必须但推荐
HAL_Delay(10);
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(Led0_GPIO_Port, Led0_Pin);
HAL_GPIO_TogglePin(Led1_GPIO_Port, Led1_Pin);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
这个就是boot程序的主函数,主要实现的功能就是在上电后的两秒内如果按下按键key0,会跳到另外一个程序,另外一个程序是两个灯常量,如果上电后两秒内不按,就不跳程序,运行该程序,两个灯亮灭亮灭
来解释一下这个跳转函数
// app的程序在芯片flash的位置
#define APP_ADDRESS 0x08002000U
typedef void (*pFunction)(void);
void JumpToApplication(void) {
// 1. 检查应用程序地址是否有效
// 应用程序的栈顶地址位于其起始地址的第一个字
if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
// 2. 禁用所有中断(防止 Bootloader 中断干扰 App)
__disable_irq();
// 3. 关闭 SysTick、清除挂起的中断(干净的执行环境)
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
for (uint32_t i = 0; i < 8; i++) {
NVIC->ICER[i] = 0xFFFFFFFF; // 禁用所有中断
NVIC->ICPR[i] = 0xFFFFFFFF; // 清除挂起标志
}
// 2. 将中断向量表重定向到应用程序地址
SCB->VTOR = APP_ADDRESS;
// 3. 获取应用程序的栈顶地址和复位处理函数地址
pFunction app_reset_handler = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4));
__set_MSP(*(__IO uint32_t*)APP_ADDRESS);
// 4.重新使能中断
//__enable_irq();
// 5. 跳转到应用程序
app_reset_handler();
}else{
// 没有识别到app
}
}
一开始定义了一个地址,是APP存放在flash的地址,如何确定烧录地址待会会在下面讲,现在只需要知道我们已经把这个APP烧录在一个起始地址为0x08002000的 加个u是告诉编译器这是无符号整数 运行这个跳转程序的时候,会先检查一下
判断条件解释
解释一下 (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000)
这个就是取一下栈顶指针,再根据这个指针取一下值,而MSP也就是栈顶指针指向SRAM,SRAM起始地址通常是0x2000 0000,SRAM的大小一般为20k,即0x2000 5000,栈指针一般指向最高地址(即地址数字最大的地址),即这个取出来的值是0x2000 5000 他与0x2FFE0000进行与操作后就是0x20000000,这保证APP地址的MSP有效且正确
*(__IO uint32_t*)APP_ADDRESS)这个取出来就是MSP 而MSP就是指向SRAM栈底地址的地址
关中断,清中断,重定义中断向量表
这里需要说的就是SCB->VTOR = APP_ADDRESS
这是操作寄存器,把新的中断向量表存到寄存器里
重定义启动入口
读取一下复位中断服务程序的地址,这个在第二个字,第一个字是指向SRAN的栈顶地址,
开中断
这一步讲一下,我查了一下正规操作在跳转的过程中不需要开中断,可以一直关中断,这个原因是,关掉中断之后,等跳转到APP的地址的时候,会运行APP程序的MAIN函数,MAIN函数里的init函数会自己把这些中断,systick之类的打开
同时这个跳转过程一直关中断还能保证安全性
再跳转到应用程序
keil操作解释

这里可以设置你烧录程序的起始地址和大小,这个得和flash对上,flash一般擦除是按照扇区,一个扇区就是2kb,注意对其即可,旁边的SRAM不用动,这个是boot的设置

这个是APP的设置,会发现IRAM也就是SRAM是一样的,我一开始想这会不会导致MSP一样,后面查了一下
| 不同 App 的 MSP 会一样吗? | 如果它们是为目标相同的芯片编译的,初始 MSP 值通常是一样的 (都指向 SRAM 顶部)。如果目标芯片 SRAM 大小不同,则不一样。 |
| MSP 一样会导致冲突吗? | 不会 。因为它们是分时工作的。SRAM 在同一时间只被一个 App 使用。 |
| Bootloader 会强制让它们一样吗? | 不会 。Bootloader 只是忠实地读取每个 App 自己在 Flash 里声明的 MSP 值,并加载到 CPU 寄存器中。 |
结论: 你不需要担心 MSP 是否一样。只要每个 App 都能独立正常运行,Bootloader 就能正确地将它们一个个启动起来。 MSP 相同是嵌入式系统中的常态,而非异常。
看来应该没什么事,我们只要做好flash管理就好了
中断向量偏移表重定向
我问了deepseek说,跳转到APP的时候,APP可能会在HAL_init这个函数里把SCB->VTOR也就是中断向量偏移表给重定向,他这个本来应该是0x00000000,跳转的时候我设为了0x08002000,APP的main重定向可能会把他重新设为00000000,但是我自己调试的时候发现不会出现这个现象,跳转后APP的main函数里并不会对中断向量偏移进行操作
又问了下deepseek 他说F4/F7系列可能会重新设定 = = 我也不知道真的假的 这块是F103系列的
实验现象
开机后2s内不按key0 灯闪烁 reset后2s内按下key0 灯常亮




