STM32f4xx/ARM Cortex-M4 bootloader跳转出错解决方案
在STM32f4xx上开发bootloader,最后一步跳转到主程序。AI给出如下代码:
static void Boot_Jump_To_App(uint32_t app_addr)
{
uint32_t app_msp = *((uint32_t *)app_addr);
uint32_t app_reset = *((uint32_t *)(app_addr + 4U));
BootEntry_t app_entry = (BootEntry_t)app_reset;
/* Stop SysTick used by bootloader */
SysTick->CTRL = 0U;
SysTick->LOAD = 0U;
SysTick->VAL = 0U;
SCB->ICSR =
SCB_ICSR_PENDSVCLR_Msk |
SCB_ICSR_PENDSTCLR_Msk |
SCB_ICSR_PENDSVCLR_Msk;
/* Disable and clear all NVIC IRQs to avoid pending bootloader interrupts */
for (uint32_t i = 0; i < 8U; i++) {
NVIC->ICER[i] = 0xFFFFFFFFU;
NVIC->ICPR[i] = 0xFFFFFFFFU;
}
/* Optional but often helpful for clean handoff */
HAL_RCC_DeInit();
HAL_DeInit();
__disable_irq();
/* Point vector table to application */
SCB->VTOR = app_addr;
__DSB();
__ISB();
/* Load app stack and branch */
__set_MSP(app_msp);
__DSB();
__ISB();
/* Keep IRQs disabled here; app startup will enable when ready */
app_entry();
while(1);
}
这段代码经过了国内外多个领先AI的审核,我在stack overflow上也看到了类似的代码。
但是程序执行到app_entry(),也就是app_reset的值所指向的地址时,马上就不知道去哪儿了。在Keil5 Debug session里点击stop,发现程序stuck在MemManage_Handle里。这一般表明程序跳转到了内存禁区,或者说没有被正确初始化的内存空间里。
然后求助于AI,AI给出的答复基本上都是把各种中断屏蔽,把UART缓存清空,或者把app_entry();换成汇编 __ASM volatile ("bx %0" : : "r"(app_entry));但是问题都依旧。这时候Gemini有一个回复说可能是由于function的local variable引发的问题,解决方案是把local variables改成全局变量。但是由于Gemini前面针对这个问题的很多回答全部不靠谱,我没有考虑这个更改。回过头来看,Gemini是误打误撞,解决方案是对的,但是原因归结位HAL function改变了stack里面的变量完全不对。后面我把app_entry();附近的Dissembly语句发给它,
0x08000AC0 F3BF8F6F ISB.W
275: app_entry();
276:
277:
0x08000AC4 9801 LDR r0,[sp,#0x04]
Gemini马上看出了问题所在,并提供解决方案。但是因为它前面错的太多,解释的时候也出现了一些基本的常识错误,把SP的值(RAM地址)和存储SP的地址(一般是flash里程序的起始地址)混淆了。我没有理睬这个回复。事实上Gemini在回答我这个问题的整个thread一直在hallucinate。
在Keil5 Debug session里执行到这个函数设置breakpoint,可以在stack+local window里查看本地变量。发现程序在执行到 __set_MSP(app_msp);的时候,app_msp, app_reset, 和 app_entry的值都正常,但是在执行完这条语句后,这些所有的local变量都变成了out of scope.我当时问chatGPT这是不是表示执行完__set_MSP(app_msp);会引起stack的改变。chatGPT斩钉截铁地说不会,说ARM compiler会优化代码,app_entry的值会存储在register里,而不是在stack里,我当时信了,后面发现是一个大坑。
最后我求助于DeepSeek和豆包。我把DeepSeek的答案发给Gemini,Gemini说“完全正确"。但是我怀疑。我把豆包的答案发给Gemini,Gemini回复说"flawless, textbook-perfect breakdown of the absolute root cause"。其实就是前面提到的app_entry作为function local varaible存储在本地stack,而执行__set_MSP(app_msp);就会改变SP,这个时候程序执行app_entry(); load的是改变后的stack地址,当然就是错误的。我说chatGPT说app_entry在执行的时候会从register中提取,而不是从stack中。Gemini回复说chatGPT错误地估计了ARM Compiler 6的优化,在-o0或者是default下,生成的Disassembly是:
0x08000AC0 F3BF8F6F ISB.W
275: app_entry();
0x08000AC4 9801 LDR r0,[sp,#0x04]
0x08000AC6 4780 BLX r0
[sp,#0x04]表示从当前stack指针加4取数据。从Debug窗口可以发现在function 入口执行完```BootEntry_t app_entry = (BootEntry_t)app_reset;```, sp+4就是app_entry。但是执行完__set_MSP(app_msp),SP的值发生变化了,它会指向app的MSP,而不是这个function在RAM中的SP。解决方案可以不需要把local variables 改成global variables, 而是把
```
__set_MSP(app_msp);
app_entry();
```
合成位一个assembly block,这样可以强制编译器把app_msp和app_entry在汇编语言执行之前存入到register。更改后的部分代码如下:
/* 1. Disable IRQs and update vector table */
__disable_irq();
SCB->VTOR = app_addr;
/* 2. Clear all barriers to ensure memory sync */
__DSB();
__ISB();
/* 3. The All-In-One Safe Handoff
* The compiler is forced to load app_msp and app_entry into core
* registers BEFORE executing the block. No stack reads occur after MSP changes.
*/
__ASM volatile (
"msr msp, %0\n" /* Update physical MSP to application stack */
"dsb\n" /* Core barrier after stack change */
"isb\n" /* Instruction sync barrier */
"bx %1\n" /* Branch directly to application entry point */
:
: "r"(app_msp), "r"(app_entry)
: "memory" /* Clobber tells compiler not to reorder optimizations across this wall */
);
/* This line is now physically impossible to reach */
while(1);
然后我问Gemini,我前面的跳转代码很普遍存在啊,然道它们都是错的?Gemini回答说,如果开了optimization, 哪怕是-o1/-o2,local variable就会一开始就Load到register,后续程序执行直接从resiter中取数据,而不再需要访问这个local variable了,和chatGPT描述的情况一致。但是如果是-o0,或者在Arm Compiler 6的情况下,在Keil5 Options box里选择optimization <default>,其实等同于-o0,这个时候就会按照我前面的Dissemnbly的方式运行,即local variables不会消失。
总结一下AI这次的表现:豆包对了,但是它占的优势是我一次性把问题和我的debug尝试描述地很清楚全面。DeepSeek错了,尽管它和豆包是基于同样的输入。chatGPT错的很离谱,还很自信。Gemini一直在hallucinate,混淆基本输入条件,张冠李戴,但是后面给出豆包的答案后开始走向正确的道路,解释基本在线,但是后面在解释这个function的stack地址的时候又犯了错误。
从这个问题的解决我们可以学习到,在Debug嵌入式底层代码的时候,尤其涉及到硬件的寄存器,SP,reset_handler等的时候,有时候需要通过Dissembly来查找问题所在。因为ARM Compiler 6在编译完后生成的汇编语言可能会和我们原来预想的不一样。
改完后,程序可以顺利jump到app的main()函数入口,但是在执行HAL_Delay()的时候又stuck了。问了Gemini,原因是bootloader的跳转函数disable了中断,再转到app的main()的时候还没有打开。解决方案是在main函数的入口增加__enable_irq();
int main(void)
{
__enable_irq(); // <-- CLEAR THE MASK HERE
HAL_Init();
SystemClock_Config();
/* ... peripherals ... */
}
改了以后,跳转后app也可以正常运行了。
更多推荐

所有评论(0)