32单片机bootloader零基础入门篇

STM32 Bootloader 零基础入门与实战指南

对于很多单片机初学者来说,Bootloader(引导加载程序)听起来像是一个非常底层、神秘的高级功能。但实际上,只要你看透了它的本质,它的核心逻辑非常清晰。

这份文档将抛开所有晦涩难懂的学术名词,用最接地气的大白话,带你一步步搞懂并写出你的第一个 Bootloader。


模块一:核心概念 —— 到底什么是 Bootloader?

在开始写代码之前,我们必须先纠正一个初学者最容易产生的误区:加入 Bootloader 后,你其实是在给单片机写“两个完全独立的程序”。

你可以把单片机的内部存储空间(ROM/Flash,这里统称为 Flash 代码存储区)想象成一栋只有一层楼的长条形办公楼,房间号(内存地址)从 0x08000000 开始。

1. 没有 Bootloader 的普通单片机(只有一个程序)

  • 程序形态: 你只写了一个程序,生成了一个 main 函数。
  • 存储位置: 代码存放在办公楼的第一间房(起始地址 0x08000000)。
  • 启动流程: 单片机非常死板,开机上电后,永远只去第一间房敲门。它拿到了你的代码,开始执行你的 main 函数,直到断电。

2. 带有 Bootloader 的单片机(有两个程序)

为了实现“后续能通过串口/网络升级固件”的功能,我们把办公楼隔成了两半,分别租给两个团队:

  • 前台团队(Bootloader): 存放在前半截房间(0x08000000 开始)。它也有自己的 main 函数,主要工作是“开机检查”和“跳转”。
  • 业务团队(App): 存放在后半截房间(假设从 0x08010000 开始)。它也有自己的 main 函数,这才是你真正点灯、读传感器、跑业务的代码。

新的启动流程变成了:

  1. 开机上电: 单片机依然死板地直奔第一间房(0x08000000)。
  2. Bootloader 运行: Bootloader 团队被唤醒。它检查一下有没有接管到升级任务,如果没有,它就准备交班。
  3. 跳转 (Jump): Bootloader 告诉单片机:“正文在后半截楼,你去 0x08010000 找业务团队吧!”
  4. 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

  1. 点击魔术棒图标 Options for Target
  2. 进入 Target 标签页。
  3. 找到 Read/Only Memory Areas 中的 IROM1
  4. Start 地址改为 0x08010000
  5. 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)
{
    // 业务逻辑循环
}

}

模块四:烧录与验证测试

因为这是两个独立的项目,你需要分两次烧录来验证你的成果:

  1. 先烧录 App: 打开 App 工程,点击下载。
  • 现象:单片机没有任何反应(可能像变砖了一样)。这是正常的!因为开机地址 0x08000000 此时是空的,单片机找不到入口。
  1. 后烧录 Bootloader: 打开 Bootloader 工程,点击下载。
  • 现象:复位单片机。Bootloader 启动,瞬间完成安全检查和环境清理,随后成功跳转。你立刻就能看到 App 里的 LED 亮起!
1 个赞