周末,午休起来看到群里有上几十条消息,没细看、划掉。没多长时间又有几十条信息,这次仔细阅读他们究竟聊着啥。
群里大佬围绕着群友的问题展开长达6小时的讨论,提供排查思路。
又不是社会事实探讨,技术群里可曾能围绕一个问题聊数个小时!
10多天前,群里有个家伙Dino讨论了好几次请教如何从bootloader跳转到app,再从app跳转到bootloader的方法。前半部分,从bootloader跳转到app想必搞过STM32的同学都知道怎么个套路, 至于他所说的app跳转到bootloader我还是第一次听说,我一般采用软复位的方式, 一切交给上电时序。
折腾几日后群友完成了设计雏形,但代码的可移植性出现问题——切换Keil编译器AC5和AC6出现不一样的执行效果。
他问:“用AC6的-O0优化等级,从app跳转至bootloader无法正常运行,其他等级都正常,包括AC5的-O0也是正常的,可能是什么原因呢?”
(笔者注:聊天记录提问者口误,根据他聊天记录提供的源码可知真实需求:从app跳转到booloader,源码图片见下文)
群里的大佬在协助调试时思路清晰,也涉及好几个试错的步骤,对小白和刚入行的从业者特有借鉴意义,所以我特意把群聊天给截取了下来。
整个聊天记录特别长,我只节选部分内容分成2篇文章发布(如果在一篇文章发布,图片太多,影响加载速度,看官没耐心)。
本文是聊天记录第1篇文章:暂且将问题原因归结到Cortex-M4的双栈(PSP、MSP)设计上。
第2篇文章(整理中):找到根本原因——全局变量、局部变量存储APP跳转指针,会有什么缺陷?
至于有兴趣阅读完整聊天内容的看官,在我公众号发送:“chat1201”。
友情提示,原聊天内容图片特别多(10-20MB),长图加载可能会有些慢。
傻孩子:群主,ARM原厂技术支持,“裸机思维”的号主
Dino:提问者
斯宾诺沙小镜子:最终点明方向的大佬。
首先Dino向群主求助,此时他把原因暂时归结于AC6编译器存在缺陷。
群主建议他先排查是不是semihosting和栈溢出。
为什么要排查semihosting我不太清除。semihosting我在项目用过,它是一个调试出处方式(当然除了调试输出字符串外还能做啥我就不懂了),嵌入式工程师调试“大招”——打印调试,首先得实现串口初始化,然后重定向_putc()到串口IO上,编译时勾选keil的 microC库,如此一来调用printf可在串口上看到打印内容。
当然如果串口也懒得初始化,使用semihosting技术,printf的输出内容将从调试连接器传输到host(PC)端,调试器例如J-Link。
确认不是semihosting、也不是栈溢出。
确认当前加入的是HardFault异常。
按照我以前的经验,加入HardFault的情况大概率是数据溢出引起的, 在调试bootloader跳转时,忘记给PC地址+4 也会触发HardFault。
Keil的Fault Report(故障报告)对话框,对话框里被勾选的位置表示发生了相应的错误。
“FORCED”被勾选,暗示处理器当前处于 HardFault_Handler 异常里,CPU处于空转状态(停止)。
Debug Faults的内容是调试器触发的错误,与本例子无关。
当处理器暂停时,将设置为 “HALTED” 。当前程序已经加入Hard Fault,当然暂停咯。
遇到软件断点时设置 “BKPT” 。给调试器上报错误,当然也是断点暂停。
故障报告对话框显示故障的类型,用于故障的处理程序,相应的故障状态寄存器以及指示故障已发生的寄存器位。
“INVSTATE” 被勾选,表示一条指令执行失败,或者说执行一条不可识别的指令。
我不知道为什么Dino回去检查 0xE00EDF8 ,Memory Manage Faults和Bus Faults分明没有任何勾选项呀。
至于为什么发生 “INVSTATE” ,猜测是PC指针发生了偏移。
源码很精简,没有什么花里胡哨的(远程技术支持也方便),各个地方都给了注释。
既然他的目的是从app跳转得到bootloader,那么就要模拟CM4的复位过程。
CM4和CM3的复位过程应该差不多,中断向量表第1项地址存放的是栈地址,传递给MSP用, 第2项地址是复位异常处理(Reset_Handler),提问者C代码的__set_MSP()等同于 Reset_handler的前面两行汇编代码。
文稿贴的代码来源于FreeRTOS CM3通用代码。
// 来源于FreeRTOS
void Reset_Handler( void )
{
/* set stack pointer */
__asm volatile ( "ldr r0, =_estack" ); /* _estack SRAM起始地址 */
__asm volatile ( "mov sp, r0" );
/* copy .data section from flash to RAM */
/* zero out .bss section */
/* jump to board initialisation */
void _start( void );
_start();
}
const uint32_t * isr_vector[] __attribute__( ( section( ".isr_vector" ) ) ) =
{
( uint32_t * ) &_estack,
( uint32_t * ) &Reset_Handler, /* Reset -15 */
( uint32_t * ) &NMI_Handler, /* NMI_Handler -14 */
( uint32_t * ) &HardFault_Handler, /* HardFault_Handler -13 */
}
按照CM3/CM4的中断向量表设计要求,SRAM第1项是栈地址,仅存储数据,不跳转异常入口,第2项目是Reset_Handler异常处理入口,既然他把栈地址填写成APP_ADDRESS,则若要执行Reset_Handler应该是APP_ADDRESS加8,起初我很怀疑发生 “INVSTATE” 的原因是否和它有关。然而提问者说在其他优化模式下可以正常执行?我开始怀疑他的需求究竟是从app跳转到bootloader,还是从bootloader跳转到app。
如果APP_ADDRESS指的不是中断向量表,而是某APP的函数入口,他这么写,大致准确。
小镜子的问题帮我解答上面的疑惑。
小镜子:你的APP_ADDRESS究竟存储的是啥?为什么要取地址的值。
Dino:0x0800c000,需要的值在这个地址。
由此可推断APP_ADDRESS并不是我“疑惑”里猜测的系统复位地址,而是采用 “双bootloader设计”。
或许他的bootloader也是有两个,动态切换。
他取名有歧义,函数名写得有迷惑性,reset_handler改成 bootloader_entry避免引起歧义,handler也容易让人联想到是CM3/CM4的异常处理入口。
小镜子不只炸的,问出一句:“你在函数里用的是PSP,但是在reset里用的是MSP,不同的函数调用能切换过去吗?”
Dino好像悟出了什么。
小镜子推断Dino上了实时操作系统,否则一般裸机仅使用MSP栈。仅几行C代码,我很好奇小镜子是如何推断Dino使用的PSP栈。
小镜子:“猜的。”
目前为止所看到的证据碎片,可以组成自圆其说的逻辑,我推断是如下场景(待会被打脸):
1、系统上电,使用MSP
2、Reset_Handler根据0x0800c000的值选择跳转哪个bootloader,使用MSP
3、bootloader再次设置MSP栈顶部
4、启动实时操作系统,或者说启动APP,使用PSP
5、某种外部输入事件(比如升级bootloader)触发APP跳转bootloader
6、APP根据0x0800c000的值选择跳转哪个bootloader(聊天里的贴图),使用PSP
7、从步骤3继续执行
我预感到本次调试回是个很棒的案例,于是催促Dino快确认设置PSP是否能万却解决问题, 到时候发一篇文章记录。
我:“如果真解决了,请在群里吼一声”
Dino:“用 __set_PSP 就可以了”
好的,正在欢呼找到问题根源时,群主建议检查CONTROL寄存器的SPSEL值。CONTROL是系统寄存器,它的SPSEL直观的告知当前处理器使用的是哪个栈。SPSEL(CONTROL寄存器第1位)为1表示使用PSP,为0表示使用MSP。
Dino:“CONTROL里的值是0”。
显然他没上实时操作系统,用的是MSP栈,我前面推断的场景崩了,脸被打得莫名其妙。
嘿~有趣了,为什么没使用PSP栈,偏偏修改它的值,程序就能正常执行,什么天方夜谭!
感兴趣的看官翻阅到文章的开头,以及Dino在C代码里如何设计APP_Main变量的,通过反汇编后,自己解释原因。
未完待续:以上内容是16点-18点的聊天记录概要,余下18点-22点聊天记录安排整理中。
获取完整聊天记录发送:“chat1201”。