Bootloader核心原理与简单完成:从零写一个bootloader

2026-07-04 02:16:25      新服速递

目录

0 相关阅读

1 为什么需要boot loader?

2 写一个最简单的bootloader

原理与思想

步骤

bootloader工程

1. 选择工程代码烧录的区域

2. 定义APP的工程代码存放区域(方便后续跳转)

3. printf重定向和关闭外设函数

4.【重点】跳转APP程序函数

5. main()函数(bootloader主逻辑)

APP工程

1. 选择工程代码烧录区域

2. main()函数

最终效果:

3 常见问题

1. 从bootloader跳转到APP需要几步?

2. 在bootloader的跳转函数中为什么要设置MSP=app初始栈顶指针?

1. 栈的基本作用

2. 为什么必须重新设置MSP?

问题场景:

具体问题:

总结:

3. Bootloader跳转APP时需要注意什么?

1. 如果Bootloader里面有freertos,则跳转App之前需要关闭 systick定时器和关中断。在App中需要先反向初始化外设、时钟,然后再初始化外设和时钟,再开启中断。

2. 在跳转APP后应该在app的所有初始化(时钟)之前,先deinit,再init所有外设,像锁相环这种外设,不是你修改一下参数,就能重新整定的,它需要重新回到激励,重新设置参数

0 相关阅读下面的文章是我之前写的相关博客,可配合本文食用:

STM32启动流程与bootloader全面解析:从上电复位到进入main函数

揭秘:基于Bootloader的IAP如何实现程序更新

1 为什么需要boot loader?简化固件更新:Bootloader 允许通过串行接口(如UART、USB、SPI等)在不使用编程器的情况下更新单片机的固件。这使得开发和维护过程更加便捷,尤其是对于那些已经部署在现场的设备。

分离应用和编程逻辑:通过使用 Bootloader,可以将应用程序代码与编程和启动逻辑分开。这样可以简化应用程序的开发,因为开发者不需要处理底层的启动和初始化细节。

安全性增强:Bootloader 可以集成安全机制,如加密和签名验证,以确保只有经过验证和授权的固件能够被写入和执行。这有助于防止恶意代码的注入和固件篡改。

硬件初始化:在一些复杂的单片机应用中,Bootloader 可以处理初始的硬件配置和初始化工作,如配置时钟、初始化外设等,然后将控制权交给主应用程序。

多应用支持:Bootloader 可以支持多应用程序管理,允许在单片机上运行多个独立的应用程序,并在需要时选择启动不同的应用。

复原机制:如果在固件更新过程中出现错误,Bootloader 可以提供复原机制,如保持一个稳定的备份版本或进入安全模式,以确保设备不会因为更新失败而变砖。

2 写一个最简单的bootloader我们现在来写一个最简单的bootloader:

程序内容只有三步:取出 app 的地址 -> 设置MSP寄存器的数值为APP地址(初始化APP栈顶指针)-> 跳转到APP

配置cubemx的过程略过。(简单配置一下时钟,再配置一个串口1为异步即可)

原理与思想Bootloader 和 APP 是两个工程,两个工程都有自己的启动文件。bootloader如果存在,就会先进bootloader的启动文件,然后到bootloader的main()函数,执行完bootloader的流程,然后跳转到APP的Reset_Handler,执行APP的启动文件,再进APP的main()函数。

Flash布局 (0x08000000)

├── Bootloader区 (0x08000000 - 0x08019000)

│ ├── Bootloader的向量表

│ ├── Bootloader的启动代码

│ └── Bootloader的主逻辑

└── 应用程序区 (0x08019000 - (0x08019000+0x67000))

├── 应用程序的向量表(已偏移)

├── 应用程序的启动代码

└── 应用程序的主逻辑

步骤bootloader工程1. 选择工程代码烧录的区域

2. 定义APP的工程代码存放区域(方便后续跳转)

#define APP_FLASH_ADDR 0x08019000

3. printf重定向和关闭外设函数

#ifdef __GNUC__

#define PUTCHAR_PROTOTYPE int _io_putchar(int ch)

#else

#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)

#endif /* __GNUC__*/

/******************************************************************

*@brief Retargets the C library printf function to the USART.

*@param None

*@retval None

******************************************************************/

PUTCHAR_PROTOTYPE

{

HAL_UART_Transmit(&huart1, (uint8_t *)&ch,1,0xFFFF);

// SEGGER_RTT_PutChar(0,ch);

return ch;

}

// 关闭外设函数

void DisablePeripherals(void)

{

// turn off RTC timer

__HAL_RCC_RTC_DISABLE();

// disable irq

__disable_irq();

}

4.【重点】跳转APP程序函数

typedef void (*pFunction)(void);

static pFunction JumpToApplication;

void JumpToApp(void)

{

uint32_t jumpAddr, armAddr;

// read the first 4 bytes of App

armAddr = *(uint32_t*)APP_FLASH_ADDR;

for (uint16_t i = 0;i < 1000;++i)

{

printf("bootloader running[%d]...\r\n",i);

}

// 1.the range of ram's addr is 0x20000000~0x2001FFFF

// 2.This indicates that the application's entry point address is within the valid range of RAM.

// 3.the first 4 bytes of APP_FLASH_ADDR indicates App's initial stack top pointer(SP)

// 4.__IO == volatile

if (((*(__IO uint32_t*)APP_FLASH_ADDR) & 0x2FFE0000) == 0x20000000)

{

// 获取应用程序的入口地址(即应用程序的复位中断服务函数的地址)

jumpAddr = *(__IO uint32_t*)(APP_FLASH_ADDR + 4);

// 将函数指针 = 复位中断服务函数地址

JumpToApplication = (pFunction)jumpAddr;

// 设置栈顶指针为应用程序栈顶指针的初始值

__set_MSP(*(__IO uint32_t*)APP_FLASH_ADDR);

// 跳转到应用程序复位中断服务函数,开始执行

JumpToApplication();

}

}

步骤拆解:

① 读取 APP 的栈顶指针:APP_FLASH_ADDR是 APP 在 Flash 中的起始地址,对应 APP 中断向量表的第 1 个元素(栈顶指针,见笔记MCU启动:从上电到运行main函数完整流程中的向量表结构)。

② 验证 APP 有效性:(*(__IO uint32_t*) APP_FLASH_ADDR)&0x2FFE0000 == 0x20000000是关键检查:

STM32 的 SRAM 地址范围通常是0x20000000 ~ 0x2001FFFF(假设 SRAM 大小为 128KB,可以看参考手册的地址映射图)。

*(__IO uint32_t*) APP_FLASH_ADDR: 读取应用程序起始地址(APP_FLASH_ADDR)处的第一个字(初始栈指针值)。

& 0x2FFE0000: 这是一个掩码,用于检查地址是否落在有效的RAM范围内。

掩码 0x2FFE0000 的二进制形式:0010 1111 1111 1110 0000 0000 0000 0000

目的是忽略地址的低位(如对齐位或保留位)低位都是偏移量不需要关心,只需要检查高位是否匹配RAM基地址。

例子:

经过 & 0x2FFE0000 操作后,一个有效的、指向 RAM 区域的栈指针,其高位部分必须恰好等于 0x20000000。

(0x20001234 & 0x2FFE0000) = 0x20000000 -> 有效

(0x2001FFFF & 0x2FFE0000) = 0x20000000 -> 有效

(0x20020000 & 0x2FFE0000) = 0x20020000 -> 无效 (不在SRAM有效地址范围内)

(0x08001234 & 0x2FFE0000) = 0x00000000 -> 无效 (不在SRAM有效地址范围内)

(0x00000000 & 0x2FFE0000) = 0x00000000 -> 无效 (不在SRAM有效地址范围内)

== 0x20000000: 验证 masked 后的地址是否等于 0x20000000(STM32的RAM起始地址)。

③ 获取 APP 入口地址:APP_FLASH_ADDR + 4是 APP 向量表的第 2 个元素(复位向量),存储的是 APP 的入口函数地址(即 APP 启动文件中的Reset_Handler)。

④ 设置栈指针并跳转:

__set_MSP(...):将主栈指针(MSP)设置为 APP 的栈顶指针(APP 运行需要自己的栈空间)。

JumpToApplication():通过函数指针调用 APP 入口地址,完成跳转(此后 Bootloader 失去控制权)。

5. main()函数(bootloader主逻辑)

int main(void)

{

// 设置中断向量表的偏移,我们将bootloader放在0x08000000的位置

// 所以中断向量表偏移到0x08000000

SCB->VTOR = 0x08000000 | 0x0;

/* 系统的一些初始化 */

HAL_Init();

SystemClock_Config();

MX_GPIO_Init();

MX_USART1_UART_Init();

/* 系统的一些初始化 */

// 关闭外设

DisablePeripherals();

// 跳转到应用程序复位中断服务函数

JumpToApp();

while (1)

{}

}

执行流程:

① 初始化硬件:包括 HAL 库、系统时钟、GPIO、UART(为调试打印做准备)。

② 配置向量表:SCB->VTOR = 0x8000000指定 Bootloader 自己的中断向量表在0x08000000(Bootloader 运行时用自己的向量表响应中断)。

③ 调用JumpToApp()尝试跳转:若成功,不会返回;若失败(如 APP 无效),则进入死循环。

APP工程1. 选择工程代码烧录区域之前我们在 Bootloader 中定义了 APP 的起始地址:#define APP_FLASH_ADDR 0x8019000(对应 Bootloader 占用0x08000000~0x08018FFF,共 100KB)。

因此,这个 APP 必须被烧写到 Flash 的0x08019000地址开始的区域(需在编译时通过链接脚本配置 APP 的 Flash 起始地址,确保与 Bootloader 的定义一致)。

若烧写地址错误(比如烧到0x08000000),会覆盖 Bootloader 工程烧写在 flash 上的代码,导致整个系统无法启动。

2. main()函数

int main(void)

{

// 设置向量表偏移并使能全局中断

SCB->VTOR = FLASH_BASE | 0x00019000;

__enable_irq();

/* 系统的一些初始化 */

HAL_Init();

SystemClock_Config();

MX_GPIO_Init();

MX_USART1_UART_Init();

/* 系统的一些初始化 */

while (1)

{

printf("hello world! at [%d] tick\r\n", HAL_GetTick());

HAL_Delay(10);

}

}

重点:中断向量表的 “重定向”(即重新设置中断向量表的偏移)

//设置中断向量表偏移,并使能全局中断

SCB->VTOR = FLASH_BASE | 0x19000;

__enable_irq();

这是 APP 代码中最核心的配置,必须结合 Bootloader 和中断向量表的原理理解:

SCB->VTOR:是 Cortex-M 内核中 “中断向量表偏移寄存器”,用于指定当前使用的中断向量表在 Flash 中的起始地址。

FLASH_BASE:STM32 Flash 的基地址(0x08000000)。

0x19000:偏移量,恰好对应 APP 在 Flash 中的起始地址(0x08000000 + 0x19000 = 0x08019000)。

这句代码的作用:告诉 CPU“现在使用 APP 自己的中断向量表(位于0x08019000),而非 Bootloader 的向量表(位于0x08000000)”。

为什么必须配置?Bootloader 运行时,会将SCB->VTOR设置为自己的向量表地址(0x08000000);当 Bootloader 跳转到 APP 后,若不重新配置SCB->VTOR,CPU 会继续使用 Bootloader 的向量表,导致 APP 的中断(如串口中断、定时器中断)无法正确响应(因为向量表中没有 APP 的中断服务函数地址)。

__enable_irq():Bootloader 在跳转前调用了__disable_irq()(禁用全局中断),避免跳转过程被中断干扰。因此 APP 启动后,需要重新使能全局中断,确保自己的中断功能正常。

最终效果:

运行完bootloader后跳转到应用程序的复位中断复位函数,开始运行应用程序的工程。

3 常见问题1. 从bootloader跳转到APP需要几步?三步:

取出 app 的地址

设置MSP寄存器的数值为APP地址(初始化APP栈顶指针)

跳转到APP

2. 在bootloader的跳转函数中为什么要设置MSP=app初始栈顶指针?1. 栈的基本作用首先理解栈在ARM Cortex-M中的重要性:

函数调用时的局部变量存储

中断发生时的上下文保存

函数参数传递

返回地址保存

2. 为什么必须重新设置MSP?问题场景:

// Bootloader运行时的栈情况

Bootloader栈空间: 0x20001000 - 0x20001FFF (4KB)

当前栈指针: 0x20001500 (已经使用了一部分)

// 如果直接跳转,不重置MSP:

应用程序期望的栈空间: 0x20002000 - 0x20002FFF (4KB)

但实际栈指针还是: 0x20001500 ← 这会导致严重问题!

具体问题:栈空间重叠污染

Bootloader栈数据会污染应用程序栈空间

应用程序的局部变量可能覆盖Bootloader的栈数据

栈溢出风险

应用程序不知道Bootloader已经使用了多少栈空间

可能很快耗尽剩余的栈空间,导致硬件错误

中断处理问题

中断发生时,上下文会保存在错误的栈位置

可能导致数据损坏或程序崩溃

总结:设置MSP为应用程序的初始栈顶指针是必需的,因为:

栈空间隔离:确保应用程序使用自己独立的栈空间

避免污染:防止Bootloader栈数据影响应用程序

符合架构规范:模拟硬件复位时的标准行为

稳定性保障:避免栈溢出和内存冲突导致的崩溃

这就像给应用程序一个"干净的开始",确保它在预期的内存环境中正常运行。

3. Bootloader跳转APP时需要注意什么?1. 如果Bootloader里面有freertos,则跳转App之前需要关闭 systick定时器和关中断。在App中需要先反向初始化外设、时钟,然后再初始化外设和时钟,再开启中断。

2. 在跳转APP后应该在app的所有初始化(时钟)之前,先deinit,再init所有外设,像锁相环这种外设,不是你修改一下参数,就能重新整定的,它需要重新回到激励,重新设置参数

打印机脱机状态怎么解除?3步一键修复教程 - 驱动精灵
“晖”字是什么意思?正确读音、注音及书写笔顺详解