实用指南:【OTA专题】3.实现简单的boot和APP程序逻辑
目录
1. __disable_irq():保证跳转过程的 “原子性”
2. NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000):保证 APP 中断响应的 “正确性”
目标:
1.实现Boot跳转App过程
2.学习代码跳转原理
实现步骤:
第一步:代码分区
前期就需要规定好Bootloader的资源占用空间。
Stm32F411Ceu6(512K flash)
分配:
Boot(分配32K)
APP(暂停其余Flash区域均为App)
Stm32F411Flash资源分配:

第二步:跳转代码
参考文章:
STM32单片机实现Bootloader跳转的关键步骤:zhuanlan.zhihu.com
Boot:
Boot_Manager.c
#include "Boot_Manager.h"
#include "main.h"
void jump_to_app(void)
{
uint32_t JumpAddress;
pFunction Jump_To_Application;
/* 检查栈顶地址是否合法 */
if(((*(__IO uint32_t *)APP_FLASH_ADDR) & 0x2FFE0000) == 0x20000000)
{
/* 屏蔽所有中断,防止在跳转过程中,中断干扰出现异常 */
__disable_irq();
NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x10000);
RCC_DeInit();
/* 用户代码区第二个 字 为程序开始地址(复位地址) */
JumpAddress = *(__IO uint32_t *) (APP_FLASH_ADDR + 4);
/* Initialize user application's Stack Pointer */
/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
__set_MSP(*(__IO uint32_t *) APP_FLASH_ADDR);
/* 类型转换 */
Jump_To_Application = (pFunction) JumpAddress;
/* 跳转到 APP */
Jump_To_Application();
}
}
Boot_Manager.h
#include "Boot_Manager.h"
#include "main.h"
void jump_to_app(void)
{
uint32_t JumpAddress;
pFunction Jump_To_Application;
/* 检查栈顶地址是否合法 */
if(((*(__IO uint32_t *)APP_FLASH_ADDR) & 0x2FFE0000) == 0x20000000)
{
/* 屏蔽所有中断,防止在跳转过程中,中断干扰出现异常 */
__disable_irq();
NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x8000);
RCC_DeInit();
/* 用户代码区第二个 字 为程序开始地址(复位地址) */
JumpAddress = *(__IO uint32_t *) (APP_FLASH_ADDR + 4);
/* Initialize user application's Stack Pointer */
/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
__set_MSP(*(__IO uint32_t *) APP_FLASH_ADDR);
/* 类型转换 */
Jump_To_Application = (pFunction) JumpAddress;
/* 跳转到 APP */
Jump_To_Application();
}
}
main.c
要恢复定时器默认中断,不然会卡在定时器相关中断里;

APP:
使用cubemx生成:






生成工程后,编译,LED被成功点亮
然后在keil中设置app的起始地址

main中进行相关的地址偏移

拓展:
1.这里为什么要屏蔽中断后重新设置向量表?

1. __disable_irq():保证跳转过程的 “原子性”
中断的本质是 “异步事件”,若跳转过程中触发中断,CPU 会去执行当前向量表中注册的中断服务函数(此时还是 Bootloader 的中断函数)。
1. 中断服务函数(ISR)依赖 “当前程序的运行环境”
中断服务函数(比如定时器中断、串口中断的处理函数)不是 “独立存在” 的,它需要依赖当前程序的 “运行上下文”,包括:
全局变量与数据结构:ISR 可能操作 Bootloader 自己的全局变量(如 “接收缓冲区”“状态标志”)。
栈空间:ISR 执行时,会把 CPU 寄存器压入当前程序的栈(Bootloader 有自己的栈,APP 也有自己的栈)。
外设配置:ISR 可能基于 Bootloader 对硬件的配置(如 UART 波特率、定时器分频系数)执行逻辑。
2. 跳转时若被中断打断,会出现 “环境不兼容”
当 Bootloader 执行Jump_To_Application()跳转时,APP 的运行环境已经开始初始化(比如栈指针已切换为 APP 的栈顶、内存布局已变为 APP 的全局变量),但中断向量表还没完全切换到 APP 的(或 APP 的中断逻辑还没初始化)。此时如果发生中断:
CPU 会根据Bootloader 的中断向量表,执行Bootloader 的 ISR。
但 Bootloader 的 ISR 是为 “Bootloader 的运行环境” 设计的:
它会操作Bootloader 的全局变量,但这些变量的地址可能已被 APP 的变量覆盖,导致数据混乱;
它会使用APP 的栈(因为栈指针已被
__set_MSP切换),但 Bootloader 的 ISR 对栈的操作逻辑(如局部变量大小、压栈深度)和 APP 的栈不匹配,可能导致栈溢出、破坏 APP 的栈数据;它会按照Bootloader 的外设配置操作硬件,但 APP 可能需要不同的外设配置,导致硬件行为异常。
举个直观的例子
假设:
Bootloader 用 UART1 接收 “固件升级数据”,中断服务函数会把数据存到
bootloader_rx_buf(地址0x20001000)。跳转到 APP 后,APP 用 UART1 接收 “用户指令”,数据要存到
app_rx_buf(地址0x20002000),且已将 UART1 波特率改为另一值。
若跳转时 UART1 触发中断:
CPU 执行Bootloader 的 UART ISR,它会往
bootloader_rx_buf(0x20001000)写数据,但此时0x20001000可能已经是 APP 的某个变量,数据被错误覆盖;同时,ISR 基于 Bootloader 的 UART 波特率处理数据,而实际硬件波特率已被 APP 修改,导致数据解析错误。
因此,
__disable_irq()屏蔽所有全局中断,确保跳转过程 “一气呵成”,不会被任何异步事件干扰。
2. NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000):保证 APP 中断响应的 “正确性”
STM32 的中断向量表默认位于Flash 起始地址(如
0x08000000),但如果 APP 不是从 Flash 起始地址启动(比如代码中 APP 的起始地址是APP_FLASH_ADDR,这里通过0x8000偏移指定 APP 的向量表位置),则需要重新配置向量表的基地址。该函数的作用是告诉 CPU:“后续中断发生时,去
Flash基地址 + 0x8000这个位置找中断向量表”(也就是 APP 自己的向量表)。若不重新设置,CPU 会继续使用 Bootloader 的向量表,导致 APP 的中断(如定时器中断、串口中断)无法找到正确的服务函数,进而引发异常。
这两步操作是 Bootloader 向 APP “交接控制权” 的关键保障:
屏蔽中断 → 确保跳转过程不被干扰;
重设向量表 → 确保APP 运行后中断能正确响应。
2.bootloader size比较大怎么办
可能原因
Bootloader 本质上就是负责 上电初始化+应用加载+升级机制。理论上它应该非常小(几KB到几十KB)。如果bootloader过大,通常有这些原因:
■ 功能加太多(UI、协议栈、复杂的文件系统等被塞进bootloader)。
■ 没有区分清楚 bootloader和application 的边界。
■ 链接脚本/工程配置不合理,把一些本该属于应用的代码/库链接进bootloader了。
■ 没有优化(比如没开编译优化、没裁剪库函数)。
应对方法
(1)精简功能
只保留 必须的功能:时钟初始化、Flash/IAP、通信接口(UART、USB、CAN、BLE等)。
把升级后的协议解析、UI、业务逻辑 放到application,不要留在bootloader。
(2)分层设计
Bootloader 只实现 最小升级能力,例如UART收发bin文件。
如果需要更复杂的升级协议(如OTA、加密校验),可以采用 两级 Bootloader:
Stage 1(Primary Bootloader):很小,放在固定地址(几十KB)
Stage 2(Secondary Bootloader):放在Flash应用区的一部分,负责复杂升级。
(3)优化体积
打开 -Os 或-02 编译优化。
使用printf 精简版(比如 iprintf 或自己实现 minimal log)。
去掉没用的库(特别是标准库、浮点printf)。
若协议栈太大(如BLE、USB Host),考虑放在application。
(4)调整内存分区
如果 bootloader 功能确实需要比较大:
在链接脚本中扩大 bootloader 占用区,比如从默认的16KB改到32KB、64KB。
只要应用程序的起始地址(App Flash 起始地址)跟bootloader 大小匹配即可。
3.为什么跳转前后用串口可以打印,RTT打印不了?
因为RTT它需要和Jlink上位机RTT_View进行握手(有一块缓冲区),跳转后会清除这块buffer丢失心跳,从而无法
继续打印,一般我们也没有这样的业务场景,这里不需要追求rtt在Bootloader和app的打印,用串口打印即可。

浙公网安备 33010602011771号