Bootloader核心原理与简单实现:从零写一个bootloader
目录
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 ApparmAddr = *(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 == volatileif (((*(__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的位置// 所以中断向量表偏移到0x08000000SCB->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栈数据影响应用程序
-
符合架构规范:模拟硬件复位时的标准行为
-
稳定性保障:避免栈溢出和内存冲突导致的崩溃
这就像给应用程序一个"干净的开始",确保它在预期的内存环境中正常运行。