STM32 Bootloader 零基础入门与实战指南
对于很多单片机初学者来说,Bootloader(引导加载程序)听起来像是一个非常底层、神秘的高级功能。但实际上,只要你看透了它的本质,它的核心逻辑非常清晰。
这份文档将抛开所有晦涩难懂的学术名词,用最接地气的大白话,带你一步步搞懂并写出你的第一个 Bootloader。
模块一:核心概念 —— 到底什么是 Bootloader?
在开始写代码之前,我们必须先纠正一个初学者最容易产生的误区:加入 Bootloader 后,你其实是在给单片机写“两个完全独立的程序”。
你可以把单片机的内部存储空间(ROM/Flash,这里统称为 Flash 代码存储区)想象成一栋只有一层楼的长条形办公楼,房间号(内存地址)从 0x08000000 开始。
1. 没有 Bootloader 的普通单片机(只有一个程序)
- 程序形态: 你只写了一个程序,生成了一个
main函数。 - 存储位置: 代码存放在办公楼的第一间房(起始地址
0x08000000)。 - 启动流程: 单片机非常死板,开机上电后,永远只去第一间房敲门。它拿到了你的代码,开始执行你的
main函数,直到断电。
2. 带有 Bootloader 的单片机(有两个程序)
为了实现“后续能通过串口/网络升级固件”的功能,我们把办公楼隔成了两半,分别租给两个团队:
- 前台团队(Bootloader): 存放在前半截房间(
0x08000000开始)。它也有自己的main函数,主要工作是“开机检查”和“跳转”。 - 业务团队(App): 存放在后半截房间(假设从
0x08010000开始)。它也有自己的main函数,这才是你真正点灯、读传感器、跑业务的代码。
新的启动流程变成了:
- 开机上电: 单片机依然死板地直奔第一间房(
0x08000000)。 - Bootloader 运行: Bootloader 团队被唤醒。它检查一下有没有接管到升级任务,如果没有,它就准备交班。
- 跳转 (Jump): Bootloader 告诉单片机:“正文在后半截楼,你去
0x08010000找业务团队吧!” - App 运行: 单片机跑到
0x08010000,唤醒了 App 程序,开始执行 App 的main函数。至此,Bootloader 彻底“休眠”,由 App 完全接管单片机。
模块二:Bootloader 代码实战(前台团队的交接仪式)
Bootloader 的核心任务就是“跳转”。但在跳转之前,作为一个负责任的前台,它必须做两件事:安全检查 和 打扫工位(清理历史包袱)。
新建一个普通的 STM32 工程作为 Bootloader 工程,在 main.c 中添加以下代码:
#include “stm32f1xx_hal.h”
// 定义 App 存放的起始地址(后半截楼的门牌号)
#define APP_ADDRESS 0x08010000
// 定义一个函数指针类型,用于执行最终的跳转动作
typedef void (*pFunction)(void);
// 核心跳转函数
void Jump_To_App(void)
{
pFunction JumpToApplication;
uint32_t JumpAddress;
/* * 第 1 步:安全检查 (检查 RAM)
* 作用:检查 APP_ADDRESS 里存放的栈顶指针,是否指向了合法的 RAM 区域(0x20000000开头)。
* 大白话:开盲盒检查一下后半截楼是不是空的。如果里面有合法的程序,才允许跳过去。
*/
if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
{
/* * 第 2 步:打扫工位 (关闭中断与外设)
* 作用:告诉CPU不要响应打扰,并将所有外设寄存器恢复到刚上电的默认状态。
* 目的:防止 Bootloader 开启的定时器或串口中断在 App 里突然触发,导致死机。
*/
__disable_irq();
HAL_DeInit();
// 如果开启了SysTick定时器,建议一并关闭
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
/* * 第 3 步:获取 App 的入口地址并准备跳转
* STM32 规定,程序的第 2 个数据(地址 + 4)存放的就是真正的代码入口(Reset_Handler)
*/
JumpAddress = *(__IO uint32_t*) (APP_ADDRESS + 4);
JumpToApplication = (pFunction) JumpAddress;
/* * 第 4 步:设置 App 的主堆栈指针 (MSP)
* STM32 规定,程序的第 1 个数据(起始地址)存放的就是栈顶指针
*/
__set_MSP(*(__IO uint32_t*) APP_ADDRESS);
/* * 第 5 步:正式跳转!单片机彻底离开 Bootloader
*/
JumpToApplication();
}
else
{
// 如果走到这里,说明后半截楼没有合法的 App 程序
// 这里可以写一个 LED 狂闪的逻辑,提示用户“固件丢失”
}
}
int main(void)
{
HAL_Init();
// (可选)可以加点料:比如让 LED 闪烁 3 次,代表 Bootloader 运行过
// ... 代码略 ...
// 执行跳转
Jump_To_App();
while (1)
{
// 正常情况下永远不会执行到这里
}
}
模块三:App 代码实战(业务团队的入住准备)
现在,我们要写真正干活的业务代码了。再新建一个 完全独立 的 STM32 工程作为 App。
因为 App 搬家到了后半截楼,所以我们需要修改两个关键配置,否则它无法正常工作。
修改 1:更改 Flash 起始地址(告诉编译器代码放哪)
在 Keil MDK 中,你需要把代码的烧录地址改到 0x08010000:
- 点击魔术棒图标
Options for Target。 - 进入
Target标签页。 - 找到
Read/Only Memory Areas中的IROM1。 - 将
Start地址改为0x08010000。 - 将
Size改为剩余的 Flash 大小(例如芯片总计 64K,用掉 64K 即0x10000,填0x10000即可)。
修改 2:设置中断向量表偏移(非常关键!)
App 运行后,如果发生了中断(比如按键按下了),CPU 默认还是会去 0x08000000 找中断处理函数,这就全乱套了。我们需要在代码最开头告诉 CPU:“我的中断向量表跟着搬家了!”
打开 App 工程的 main.c,在 main() 函数的最开头添加偏移代码:
int main(void)
{
/* * 关键配置:将中断向量表偏移到 0x08010000
* 必须放在所有初始化代码的最前面!
*/
SCB->VTOR = 0x08010000;
// 开启中断(因为Bootloader跳过来之前把中断全关了,这里要重新打开)
__enable_irq();
HAL_Init();
SystemClock_Config();
// 接下来就是你正常的业务代码了,比如点亮一个常亮的 LED
// ... 代码略 ...
while (1)
{
// 业务逻辑循环
}
}
模块四:烧录与验证测试
因为这是两个独立的项目,你需要分两次烧录来验证你的成果:
- 先烧录 App: 打开 App 工程,点击下载。
- 现象:单片机没有任何反应(可能像变砖了一样)。这是正常的!因为开机地址
0x08000000此时是空的,单片机找不到入口。
- 后烧录 Bootloader: 打开 Bootloader 工程,点击下载。
- 现象:复位单片机。Bootloader 启动,瞬间完成安全检查和环境清理,随后成功跳转。你立刻就能看到 App 里的 LED 亮起!