在嵌入式系统中,实现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。它需要模仿硬件复位后的行为:
-
从APP的向量表首地址读取栈顶指针(第一个字),将其设置到主栈指针(MSP)。
-
从APP的向量表第二个字读取复位处理函数地址,然后跳转过去执行。
-
设置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. 总结:正确做法
-
编写APP时:
-
在Keil中设置正确的ROM起始地址(如0x08040000)。
-
在
system_stm32h5xx.c(或其他系统初始化文件)中,注释掉设置VTOR的代码。 -
确保APP中不重复使能ICACHE(如果Bootloader已使能)。
-
-
编写Bootloader时:
-
编写跳转函数(如上面的汇编代码),传入APP向量表基地址。
-
在跳转前关闭所有中断,确保跳转过程不被干扰。
-
设置好VTOR、SP,然后跳转。
-
遵循这些原则,你的Bootloader就能稳定地启动任何位置的APP,且APP可以正常响应中断,实现复杂功能。