编写APP和Bootloader实现启动功能:为什么APP不能设置VTOR?

在嵌入式系统中,实现IAP(In-Application Programming)功能通常需要两个部分:Bootloader (引导加载程序)和应用程序 (APP)。Bootloader负责启动和升级,APP是用户功能的主体。当APP的代码存放在不同于Bootloader的Flash地址时(例如0x08040000),就需要正确处理向量表偏移,否则程序无法正常运行。

很多人在初次编写APP时,会使用STM32CubeMX生成的代码,其中默认包含了设置VTOR(向量表偏移寄存器)的语句。但在有Bootloader的情况下,这个设置必须注释掉。如果不注释,简单的点灯程序可能还能跑,但一旦程序复杂(比如使用了中断、外设初始化等),就会出现死机、无法启动等问题。本文将详细解释原因,并给出正确的实现方法。


1. 回顾:APP的编写要点

在Keil MDK中,通过修改工程的Target选项 ,可以设置APP的只读区域(RO)起始地址。例如,如果希望APP从0x08040000开始运行,就设置:

  • ROM1:Start = 0x08040000,Size = 0x200000(根据Flash大小调整)

这样,编译后的代码就会从0x08040000开始存放。

但是,仅仅修改地址还不够,还必须处理向量表 。STM32CubeMX生成的代码中,system_stm32h5xx.c(或类似文件)里通常有一段代码:

cs 复制代码
c

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif

这段代码的作用是将向量表定位到指定的基地址(FLASH_BASE通常为0x08000000)加上偏移。如果直接使用默认值(不注释),APP会将向量表设置到0x08000000 + 偏移 ,但APP的向量表实际存放在0x08040000,这就导致了向量表位置错误。中断发生后,CPU会跑到错误的地方取地址,程序必然崩溃。

因此,在APP中必须注释掉 这段代码,或者在预定义中不定义VECT_TAB_OFFSET,让VTOR保持Bootloader设置的值。


2. 为什么简单的任务还能跑?

有些同学可能会问:我注释掉VTOR设置后,点灯程序正常;如果不注释,点灯程序也能运行,为什么?

这是因为简单的点灯程序可能没有使用中断,或者即使使用了SysTick等中断,向量表偏移错误可能导致中断响应异常,但由于程序逻辑简单,可能看不出明显问题(比如LED闪烁频率不对,或者偶尔死机)。一旦程序复杂起来,比如使用了多个外设中断、DMA、USB等,中断响应异常就会导致系统彻底崩溃。

此外,Bootloader可能已经使能了某些外设(如ICACHE),APP如果再重新初始化这些外设,也可能导致冲突。下文会详细说明。


3. Bootloader的启动功能

Bootloader的任务之一就是启动APP。它需要模仿硬件复位后的行为:

  1. 从APP的向量表首地址读取栈顶指针(第一个字),将其设置到主栈指针(MSP)。

  2. 从APP的向量表第二个字读取复位处理函数地址,然后跳转过去执行。

  3. 设置VTOR,将向量表基地址指向APP的起始地址,这样APP运行时发生中断,CPU就能找到正确的处理函数。

3.1 Bootloader中的跳转代码(汇编示例)

下面是一段典型的跳转代码(jump.S):

cs 复制代码
assembly

; 函数名:start_app
; 参数:R0 = APP的向量表基地址(例如0x08040000)
start_app
    ; 设置VTOR = R0
    LDR R1, =0xE000ED08    ; VTOR寄存器地址(对于Cortex-M,VTOR地址为0xE000ED08)
    STR R0, [R1]            ; 写入VTOR

    ; 读取APP向量表的第一个字(栈顶指针),存入SP
    LDR R1, [R0]            ; R1 = *(uint32_t*)R0
    MOV SP, R1              ; 设置主栈指针

    ; 读取APP向量表的第二个字(复位地址),存入R1
    LDR R1, [R0, #4]        ; R1 = *(uint32_t*)(R0+4)

    ; 跳转到复位地址执行
    BX R1                   ; 跳转,并切换到Thumb模式(地址最低位应为1)

这段代码做了三件核心事:

  • 设置VTOR:让CPU知道新的向量表在0x08040000。

  • 设置栈指针:APP运行时需要自己的栈空间。

  • 跳转 :从APP的复位向量开始执行,也就是APP的Reset_Handler

3.2 为什么APP不能再设置VTOR?

Bootloader已经通过上述代码将VTOR正确指向了APP的向量表。如果APP在初始化时又执行了SCB->VTOR = FLASH_BASE | ...,就会覆盖Bootloader的设置,将VTOR改回默认值(0x08000000)或其他值。此时,APP的向量表实际在0x08040000,但VTOR指向0x08000000,中断自然找不到正确的入口。

更严重的是,如果Bootloader和APP都使能了ICACHE(指令缓存),APP再次初始化ICACHE可能会导致死机。ICACHE的使能只能做一次,第二次初始化可能因为状态不一致而卡死。因此,Bootloader和APP只能有一个使能ICACHE。通常做法是Bootloader使能ICACHE后,APP就不再操作ICACHE相关寄存器。


4. 实验验证

按照课程源码,进行以下实验:

  • 烧写未注释VTOR的APP:单独运行APP(地址0x08040000),由于向量表错误,程序无法运行,LED不闪烁。

  • 烧写Bootloader:Bootloader运行后,跳转到APP(地址0x08040000),且APP中已注释掉VTOR设置。LED闪烁,说明启动成功。

如果APP中未注释VTOR,Bootloader启动后,LED也可能闪烁?不一定。因为Bootloader设置了正确的VTOR,APP启动后立即又覆盖了VTOR,导致后续中断失效。如果APP在覆盖VTOR之前没有使用任何中断,或许能短暂运行,但一旦进入中断(如SysTick),就会死机。因此,必须注释掉


5. 常见疑问解答

Q:Bootloader中设置了VTOR,APP中还需要设置吗?

A:不需要。VTOR是一个全局寄存器,由Bootloader设置一次即可。APP应该信任Bootloader的设置,不要修改。

Q:如果APP有自己的中断,比如使用了USB,不设置VTOR能正确响应吗?

A:能,因为VTOR已经指向APP的向量表,中断自然能正确找到处理函数。前提是APP的向量表确实放在了该地址,且中断向量在表中正确填写。

Q:为什么Bootloader要读取APP向量表的第一字设置SP?APP自己启动时不是也会设置SP吗?

A:APP的启动代码(如Reset_Handler)在运行前,硬件已经自动从向量表加载了SP。但由于我们是通过软件跳转到APP,硬件不会自动做这件事,所以Bootloader必须手动设置SP,否则APP运行时栈指针错误,函数调用会崩溃。

Q:ICACHE的冲突具体怎么发生?

A:假设Bootloader使能了ICACHE,APP的SystemInit函数中可能又调用了SCB_EnableICache()之类的函数,这个函数内部会操作ICACHE控制寄存器,如果ICACHE已经使能,再次使能可能导致寄存器状态异常,甚至进入HardFault。因此,APP中应该避免重复使能。


6. 总结:正确做法

  1. 编写APP时

    • 在Keil中设置正确的ROM起始地址(如0x08040000)。

    • system_stm32h5xx.c(或其他系统初始化文件)中,注释掉设置VTOR的代码。

    • 确保APP中不重复使能ICACHE(如果Bootloader已使能)。

  2. 编写Bootloader时

    • 编写跳转函数(如上面的汇编代码),传入APP向量表基地址。

    • 在跳转前关闭所有中断,确保跳转过程不被干扰。

    • 设置好VTOR、SP,然后跳转。

遵循这些原则,你的Bootloader就能稳定地启动任何位置的APP,且APP可以正常响应中断,实现复杂功能。

相关推荐
Electron-er17 天前
汽车ECU重编程中的Bootloader设计原理:如何实现安全回滚?
autosar·uds·汽车电子·bootloader·功能安全·ecu刷写
STCNXPARM25 天前
Linux-ARM-Bootloader概述
linux·运维·arm开发·uboot·bootloader
一枝小雨1 个月前
【OTA专题】 20 上电立即跳转:加快MCU启动速度
stm32·单片机·嵌入式·ota·bootloader·加速启动
一枝小雨1 个月前
【OTA专题】17 打通Bootloader与App逻辑之间的通信
stm32·单片机·嵌入式·流程图·freertos·ota·bootloader
一枝小雨1 个月前
【OTA专题】18 OTA性能优化:优化bootloader存储空间与固件完整性校验(CRC)
stm32·单片机·性能优化·嵌入式·freertos·ota·bootloader
一枝小雨1 个月前
【OTA专题】15 实现App后台无感下载固件
stm32·单片机·嵌入式·ota·bootloader
橘色的喵1 个月前
嵌入式二级 Bootloader (SBL) 的设计与实现:基于裸机环境的安全固件管理
安全·bootloader·二级boot
Molesidy2 个月前
【Embedded Development】【bootloader】基于MCU的bootloader详细介绍以及基于MCU串口的IAP实战详细教程
单片机·嵌入式硬件·bootloader
一枝小雨2 个月前
【OTA专题】12 APP中移植EEprom、W25Q驱动
stm32·单片机·嵌入式·freertos·ota·bootloader