【OTA专题】3.实现简单的boot和APP程序逻辑
目录
目标:
1.实现Boot跳转App过程
2.学习代码跳转原理
实现步骤:
第一步:代码分区
第二步:跳转代码
Boot:
APP:
拓展:
1.这里为什么要屏蔽中断后重新设置向量表?
1. __disable_irq():保证跳转过程的 “原子性”
2. NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000):保证 APP 中断响应的 “正确性”
2.bootloader size比较大怎么办
可能原因
应对方法
(1)精简功能
(2)分层设计
(3)优化体积
(4)调整内存分区
3.为什么跳转前后用串口可以打印,RTT打印不了?
目标:
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的打印,用串口打印即可。