欢迎光临
我们一直在努力

bootloader STM32 按键APP跳转

bootloader介绍

bootloader翻译过来是引导加载程序,我一开始把这个和OTA搞混了,简单来说他就是上电后最先执行的一段程序,这个和硬件上给BOOT0接1不太一样,一般只有芯片被锁才会给boot0接1通过串口擦除或烧录程序 附上git工程bootloader

写这个bootloader有什么用处呢,首先它可以不烧录就能够切换程序,其次能够接收数据实现OTA功能(这个是我后面会说)之类的,目前我能想到的就是这两个。

芯片上电后的过程

1. 硬件复位与 Boot 引脚采样 (Hardware Reset)

  • 上电/复位 :当 NRST 引脚检测到复位信号或电源电压达到阈值时,CPU 内核复位。
  • 采样 Boot 引脚 :硬件电路采样 BOOT0 和 BOOT1 引脚的电平状态。
  • 根据电平组合,决定从哪个存储区域开始执行代码:
    • Main Flash (0x0800 0000):正常用户程序启动。
    • System Memory (0x1FFF xxxx):进入厂家固化的 ISP Bootloader。
    • SRAM (0x2000 0000):从内存启动(常用于调试)。
  • 映射地址 :如果从 Main Flash 启动,硬件会将 Flash 的起始地址映射到 0x0000 0000(别名区),以便 CPU 取指。

这里一般就指向0x08000000

2. 获取初始栈指针 (MSP) 和复位向量

CPU 从启动地址(通常是 0x0800 0000)读取前两个字(Word,32位):

  • 第一个字 (地址 0x0800 0000) : 初始主栈指针 (Initial MSP) 。
    • CPU 将这个值加载到 MSP (Main Stack Pointer) 寄存器。栈用于存放局部变量、函数调用上下文等。
  • 第二个字 (地址 0x0800 0004) : 复位异常处理程序地址 (Reset Handler Address) 。
    • CPU 将这个地址加载到 PC (Program Counter) 寄存器。
    • 此时,PC 指向了启动文件中的 Reset_Handler 函数。

    注意 :这两个值构成了中断向量表 (Vector Table) 的前两项。

    这里就是常说的指向向量中断表,每个程序的都不一样,所以得确保跳转之后指向的是与之相对应的,后面我会有代码解释

    • 如果使用了 C++ 或特定的 C 库特性,编译器会生成初始化数组。

    3. 执行 Reset_Handler (汇编阶段)

    Reset_Handler 通常位于启动文件(如 startup_stm32f10x.s)中,是一段汇编代码。它主要做以下几件事:

    关闭中断 :确保在初始化完成前不会响应任何中断,防止不可预知的行为。

    系统时钟初始化 (SystemInit) :

    • 调用 C 语言函数 SystemInit()(位于 [system_stm32f1xx.c](javascript:void(0)))。
    • 配置 PLL、HSE/HSI、分频系数等,将系统时钟提升到目标频率(如 72MHz)。
    • 配置 Flash 等待周期(Wait States),确保 CPU 能正确读取 Flash。

    数据段初始化 (.data 和 .bss) :

    • .data 段拷贝 :将存储在 Flash 中的已初始化全局变量/静态变量复制到 SRAM 中。因为 SRAM 断电后数据丢失,所以初始值必须存在 Flash 里,上电后搬移到 SRAM。
    • .bss 段清零 :将未初始化的全局变量/静态变量所在的 SRAM 区域清零。C 标准规定未初始化的全局变量默认值为 0。
    • 启动代码会遍历这些数组,调用全局对象的构造函数(C++)或特定的初始化函数。
    • 对于纯 C 工程,这一步可能为空或非常简短。

    这里在代码里主要就是关中断,systick

    4. 调用 C 库初始化 (__libc_init_array / __init_array)

    • 如果使用了 C++ 或特定的 C 库特性,编译器会生成初始化数组。
    • 启动代码会遍历这些数组,调用全局对象的构造函数(C++)或特定的初始化函数。
    • 对于纯 C 工程,这一步可能为空或非常简短。

    5. 跳转到 main() 函数

    • 完成上述所有初始化后,Reset_Handler 最后会调用 C 标准库入口函数 __main(注意不是 main)。
    • __main 是编译器生成的辅助代码,负责最终的环境设置(如堆初始化),然后正式跳转到用户编写的 main() 函数。
    • 从此,程序进入用户应用逻辑,开始无限循环或任务调度。

    原理简述

    现在上电过程讲完了,能看到正常没有bootloader的话,他会指向0x08000000这个地址,从这个地址开始执行程序,如果想用bootloader,我们可以把bootloader写在这个首地址,把APP即应用功能写在别的地址分块,从而能够实现多功能

    就像这个表格

    地址范围大小存放内容说明
    0x08000000~0x08003FFF 16 KB Bootloader 上电首先执行,负责跳转到有效 APP
    0x08004000~0x08007FFF 16 KB APP1(主应用) 正常功能固件
    0x08008000~0x0800BFFF 16 KB APP2(备份/升级应用) 可作 OTA 备份或应急功能
    0x0800C000~0x0800FFFF 16 KB APP3(测试/诊断应用) 特殊功能,如出厂诊断
    0x08010000~ … 剩余空间 保留 / 配置参数区 存放标志位、版本信息等

    无 Bootloader 时 :直接从 0x08000000 执行单一程序。

    有 Bootloader 时 :

    Bootloader 先运行,根据标志(按键/升级标志/CRC等)决定跳转到哪个 APP。

    每个 APP 编译时需设置独立的 链接地址(VECT_TAB_OFFSET) ,并重新映射中断向量表到自身地址。

    代码解析

    现在看一下boot的程序

    /* USER CODE BEGIN 0 */
    // app的程序在芯片flash的位置
    #define APP_ADDRESS 0x08002000U
    typedef void (*pFunction)(void);
    void JumpToApplication(void) {
    // 1. 检查应用程序地址是否有效
    // 应用程序的栈顶地址位于其起始地址的第一个字
    if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
    // 2. 禁用所有中断(防止 Bootloader 中断干扰 App)
    __disable_irq();
    // 3. 关闭 SysTick、清除挂起的中断(干净的执行环境)
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL = 0;
    for (uint32_t i = 0; i < 8; i++) {
    NVIC->ICER[i] = 0xFFFFFFFF; // 禁用所有中断
    NVIC->ICPR[i] = 0xFFFFFFFF; // 清除挂起标志
    }
    // 2. 将中断向量表重定向到应用程序地址
    SCB->VTOR = APP_ADDRESS;

    // 3. 获取应用程序的栈顶地址和复位处理函数地址
    pFunction app_reset_handler = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4));
    __set_MSP(*(__IO uint32_t*)APP_ADDRESS);
    // 4.重新使能中断
    //__enable_irq();
    // 5. 跳转到应用程序
    app_reset_handler();
    }else{
    // 没有识别到app

    }
    }
    /* USER CODE END 0 */

    /**
    * @brief The application entry point.
    * @retval int
    */

    int main(void)
    {

    /* USER CODE BEGIN 1 */

    /* USER CODE END 1 */

    /* MCU Configuration——————————————————–*/

    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* USER CODE BEGIN Init */

    /* USER CODE END Init */

    /* Configure the system clock */
    SystemClock_Config();

    /* USER CODE BEGIN SysInit */

    /* USER CODE END SysInit */

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    /* USER CODE BEGIN 2 */
    uint32_t startTime = HAL_GetTick();
    uint32_t timeout = 2000; // 2秒超时

    // 在2秒内轮询检测按键状态
    while ((HAL_GetTick() startTime) < timeout) {
    // 检测 KEY0 是否被按下 (低电平有效)
    if (HAL_GPIO_ReadPin(Key0_GPIO_Port, Key0_Pin) == GPIO_PIN_RESET) {
    // 可选:添加简单的去抖动延迟,例如 10-20ms
    HAL_Delay(20);
    // 再次确认按键仍然处于按下状态
    if (HAL_GPIO_ReadPin(Key0_GPIO_Port, Key0_Pin) == GPIO_PIN_RESET) {
    JumpToApplication();
    }
    }
    // 短暂延迟以降低 CPU 占用率,非必须但推荐
    HAL_Delay(10);
    }

    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
    HAL_GPIO_TogglePin(Led0_GPIO_Port, Led0_Pin);
    HAL_GPIO_TogglePin(Led1_GPIO_Port, Led1_Pin);
    HAL_Delay(500);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    }

    这个就是boot程序的主函数,主要实现的功能就是在上电后的两秒内如果按下按键key0,会跳到另外一个程序,另外一个程序是两个灯常量,如果上电后两秒内不按,就不跳程序,运行该程序,两个灯亮灭亮灭

    来解释一下这个跳转函数

    // app的程序在芯片flash的位置
    #define APP_ADDRESS 0x08002000U
    typedef void (*pFunction)(void);
    void JumpToApplication(void) {
    // 1. 检查应用程序地址是否有效
    // 应用程序的栈顶地址位于其起始地址的第一个字
    if (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000) {
    // 2. 禁用所有中断(防止 Bootloader 中断干扰 App)
    __disable_irq();
    // 3. 关闭 SysTick、清除挂起的中断(干净的执行环境)
    SysTick->CTRL = 0;
    SysTick->LOAD = 0;
    SysTick->VAL = 0;
    for (uint32_t i = 0; i < 8; i++) {
    NVIC->ICER[i] = 0xFFFFFFFF; // 禁用所有中断
    NVIC->ICPR[i] = 0xFFFFFFFF; // 清除挂起标志
    }
    // 2. 将中断向量表重定向到应用程序地址
    SCB->VTOR = APP_ADDRESS;

    // 3. 获取应用程序的栈顶地址和复位处理函数地址
    pFunction app_reset_handler = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4));
    __set_MSP(*(__IO uint32_t*)APP_ADDRESS);
    // 4.重新使能中断
    //__enable_irq();
    // 5. 跳转到应用程序
    app_reset_handler();
    }else{
    // 没有识别到app

    }
    }

    一开始定义了一个地址,是APP存放在flash的地址,如何确定烧录地址待会会在下面讲,现在只需要知道我们已经把这个APP烧录在一个起始地址为0x08002000的 加个u是告诉编译器这是无符号整数 运行这个跳转程序的时候,会先检查一下

    判断条件解释

    解释一下 (((*(__IO uint32_t*)APP_ADDRESS) & 0x2FFE0000) == 0x20000000)

    这个就是取一下栈顶指针,再根据这个指针取一下值,而MSP也就是栈顶指针指向SRAM,SRAM起始地址通常是0x2000 0000,SRAM的大小一般为20k,即0x2000 5000,栈指针一般指向最高地址(即地址数字最大的地址),即这个取出来的值是0x2000 5000 他与0x2FFE0000进行与操作后就是0x20000000,这保证APP地址的MSP有效且正确

    *(__IO uint32_t*)APP_ADDRESS)这个取出来就是MSP 而MSP就是指向SRAM栈底地址的地址

    关中断,清中断,重定义中断向量表

    这里需要说的就是SCB->VTOR = APP_ADDRESS

    这是操作寄存器,把新的中断向量表存到寄存器里

    重定义启动入口

    读取一下复位中断服务程序的地址,这个在第二个字,第一个字是指向SRAN的栈顶地址,

    开中断

    这一步讲一下,我查了一下正规操作在跳转的过程中不需要开中断,可以一直关中断,这个原因是,关掉中断之后,等跳转到APP的地址的时候,会运行APP程序的MAIN函数,MAIN函数里的init函数会自己把这些中断,systick之类的打开

    同时这个跳转过程一直关中断还能保证安全性

    再跳转到应用程序

    keil操作解释

    在这里插入图片描述

    这里可以设置你烧录程序的起始地址和大小,这个得和flash对上,flash一般擦除是按照扇区,一个扇区就是2kb,注意对其即可,旁边的SRAM不用动,这个是boot的设置

    在这里插入图片描述

    这个是APP的设置,会发现IRAM也就是SRAM是一样的,我一开始想这会不会导致MSP一样,后面查了一下

    疑问解释
    不同 App 的 MSP 会一样吗? 如果它们是为目标相同的芯片编译的,初始 MSP 值通常是一样的 (都指向 SRAM 顶部)。如果目标芯片 SRAM 大小不同,则不一样。
    MSP 一样会导致冲突吗? 不会 。因为它们是分时工作的。SRAM 在同一时间只被一个 App 使用。
    Bootloader 会强制让它们一样吗? 不会 。Bootloader 只是忠实地读取每个 App 自己在 Flash 里声明的 MSP 值,并加载到 CPU 寄存器中。

    结论: 你不需要担心 MSP 是否一样。只要每个 App 都能独立正常运行,Bootloader 就能正确地将它们一个个启动起来。 MSP 相同是嵌入式系统中的常态,而非异常。

    看来应该没什么事,我们只要做好flash管理就好了

    中断向量偏移表重定向

    我问了deepseek说,跳转到APP的时候,APP可能会在HAL_init这个函数里把SCB->VTOR也就是中断向量偏移表给重定向,他这个本来应该是0x00000000,跳转的时候我设为了0x08002000,APP的main重定向可能会把他重新设为00000000,但是我自己调试的时候发现不会出现这个现象,跳转后APP的main函数里并不会对中断向量偏移进行操作

    又问了下deepseek 他说F4/F7系列可能会重新设定 = = 我也不知道真的假的 这块是F103系列的

    实验现象

    开机后2s内不按key0 灯闪烁 reset后2s内按下key0 灯常亮

    赞(0)
    未经允许不得转载:171主机测评 » bootloader STM32 按键APP跳转
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址