在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也可以正常运行了。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐