Bootloader核心原理与简单实现:从零写一个bootloader

目录

[0 相关阅读](#0 相关阅读)

[1 为什么需要boot loader?](#1 为什么需要boot loader?)

[2 写一个最简单的bootloader](#2 写一个最简单的bootloader)

原理与思想

步骤

bootloader工程

[1. 选择工程代码烧录的区域](#1. 选择工程代码烧录的区域)

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

[3. printf重定向和关闭外设函数](#3. printf重定向和关闭外设函数)

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

[5. main()函数(bootloader主逻辑)](#5. main()函数(bootloader主逻辑))

APP工程

[1. 选择工程代码烧录区域](#1. 选择工程代码烧录区域)

[2. main()函数](#2. main()函数)

最终效果:

[3 常见问题](#3 常见问题)

[1. 从bootloader跳转到APP需要几步?](#1. 从bootloader跳转到APP需要几步?)

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

[1. 栈的基本作用](#1. 栈的基本作用)

[2. 为什么必须重新设置MSP?](#2. 为什么必须重新设置MSP?)

问题场景:

具体问题:

总结:

[3. Bootloader跳转APP时需要注意什么?](#3. Bootloader跳转APP时需要注意什么?)

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

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


0 相关阅读

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

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

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

1 为什么需要boot loader?

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

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

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

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

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

  6. 复原机制:如果在固件更新过程中出现错误,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的工程代码存放区域(方便后续跳转)
cpp 复制代码
#define APP_FLASH_ADDR 0x08019000
3. printf重定向和关闭外设函数
cpp 复制代码
#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程序函数
cpp 复制代码
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主逻辑)
cpp 复制代码
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()函数
cpp 复制代码
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);
    }
}
  • 重点:中断向量表的 "重定向"(即重新设置中断向量表的偏移)
cpp 复制代码
//设置中断向量表偏移,并使能全局中断
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需要几步?

三步:

  1. 取出 app 的地址

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

  3. 跳转到APP

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

1. 栈的基本作用

首先理解栈在ARM Cortex-M中的重要性:

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

  • 中断发生时的上下文保存

  • 函数参数传递

  • 返回地址保存

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

问题场景:
cpp 复制代码
// Bootloader运行时的栈情况
Bootloader栈空间: 0x20001000 - 0x20001FFF (4KB)
当前栈指针: 0x20001500 (已经使用了一部分)

// 如果直接跳转,不重置MSP:
应用程序期望的栈空间: 0x20002000 - 0x20002FFF (4KB)
但实际栈指针还是: 0x20001500 ← 这会导致严重问题!
具体问题:
  1. 栈空间重叠污染

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

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

  2. 栈溢出风险

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

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

  3. 中断处理问题

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

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

总结:

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

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

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

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

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

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

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

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

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

相关推荐
充哥单片机设计10 小时前
【STM32项目开源】基于STM32的智能路灯控制系统
stm32·单片机·嵌入式硬件
大聪明-PLUS10 小时前
通过 Telnet 实现自动化
linux·嵌入式·arm·smarc
啃硬骨头13 小时前
MC33PT2000控制详解七:软件代码设计1-图形化设置
单片机·嵌入式硬件
充哥单片机设计15 小时前
【STM32项目开源】基于STM32的智能语音分类垃圾桶
stm32·单片机·嵌入式硬件
张人玉16 小时前
C# UDP 服务端与客户端2.0
单片机·udp·c#
大聪明-PLUS20 小时前
ARM Cortex-M:内存保护单元 (MPU) 发布
linux·嵌入式·arm·smarc
清风66666620 小时前
基于51单片机宠物喂食系统设计
数据库·单片机·毕业设计·51单片机·课程设计·宠物
客官、打尖还是住店20 小时前
STM32简介
stm32·单片机·嵌入式硬件
GilgameshJSS21 小时前
STM32H743-ARM例程13-SDIO
c语言·arm开发·stm32·嵌入式硬件·学习