首页|资源下载
登录|注册

您现在的位置是:首页 > 技术阅读 >  听,比特在说话:stm32从HardFault里走出来

听,比特在说话:stm32从HardFault里走出来

时间:2024-01-05

噪音

“啪~ 哔——”数分钟后又是“啪~ 哔——”,整个上午办公室角落持续传来单调节奏。吴解脱下降噪耳机,以打水为由头绕到发出声响的位置,探究是什么情况。

白蔡桌面上摆放着一台可调稳压电源,“啪~”的一声是关闭可调电源总开关发出的。搞不懂可调电源厂家产品经理怎么想的,没多大功率的电源,为什么要选择像空气开关一样的机械。就如同工厂里的紧急停止开关一样,推上去阻力很大,掰下来轻轻松松,弹簧瞬间打到机箱外壳,发出很大的噪音“啪~”。

推上电源,桌面上的电路板蜂鸣器随即发出长长的“哔——”,电脑窗口字幕哗啦啦向上滑动,白蔡在设备上一顿操作屏幕内容才安静下来。他复制屏幕的内容做下笔记,笔记本上已经有好几段类似的结构,白蔡看眼Keil停止的位置,准备再次给稳压电源供电。

白蔡嘴巴嘟哝着:“每次不太一样。”

吴解打断他:“早上忙啥呀,休息时间也不站起来一下溜达溜达。”

吴解实际上已经看清除他在忙啥,就看白蔡是否愿意虚心请教,若不问,吴解不介意继续戴着耳机事不关己己不劳心,不做好为人师之事。

白蔡:“吴哥呀,我是打算问你来着,不过不知从何谈起,找不到规律。”

吴解指着笔记本屏幕:“那就从笔记本内容谈起,粘贴一坨打印字符表示什么,你在跟踪什么?”

白蔡:“系统掉入Hard Fault,我在很多怀疑的地方添加printf打印函数,试图跟踪到究竟是哪行代码引起的Hard Fault,实操情况呢~似乎找不到故障点。”

低效且有效的人海战术,吴解:“哦!找不到故障点?Hard Fault应该能看的东西蛮多东西的。说说你是怎么分析的?”

白蔡面露难色,鼠标在两次日志末尾滑动,企图让对方理解她的意思:“额,分析不来,每次陷入Hard Fault死机前输出的内容未必一样,emo中。”

吴解翻动手机找着《CM3权威指南》准备发给他补基础:“你对Cortex-M3/M4响应异常过程了解多少?直白地说‘现场保护的过程是怎么样的’?中断发生时,处理器是如何自动把相关寄存器压入栈的?”

白蔡伸出小拇指,不太自信:“一丢丢,额~从R0到R16所有寄存器依次入栈,SP寄存器向下移动?大概这些吧。”

白蔡的情况不太懂Cortex-M3/M4的架构,不妨碍面试背诵八股文。

吴解深吸一口气:“好吧,简单给你说一下,从Hard Fault逆推是哪行代码引发异常的”。

HardFault入栈顺序

吴解先给他讲平台的大背景:“首先要明确一个概念,Cortex-M3/M4有两个栈寄存器,主栈MSP和任务栈PSP,复位后自动使用MSP,裸机开发绝大多数全程使用MSP,实时操作系统考虑到安全,所有任务(Task)使用PSP。”

SP属于影子寄存器,它对用户几乎是透明的,它可能表示MSP也可能表示PSP,若想直接访问MSP、PSP则要直接调用API:__set_PSP、__get_MSP。提醒一下,因为Cortext-M3/M4没有MMU,所以处于PSP的照样有机会越界访问MSP。

白蔡:“那么任务调度时候用的是哪个栈。”

吴解:“问出这个表明你在思考,而不只是听。‘调度算法’是一个异常中断服务,所有异常中断服务都处于Handler mode,自动切换成MSP。任务算法执行过程本质是修改PSP成另一个Task的栈,待异常中断服务结束后,切换回PSP,所有任务都工作在Thread mode。”

“异常”不等同于“中断”,中断是异常的子集。

吴解:“当Hard Fault异常发生时候,处理器会自动保存8个寄存器到当前栈,注意,只有8个,而不是你刚才说的全部。8个寄存器是现场保护、恢复现场要做的工作之一。”

白蔡疑惑:“为什么只有8个而不是全部。”

吴解两手一摊,模仿Emoji微笑表情包:“得问处理器设计工程师咯,猜测为了提升入栈速度,第二为了节省栈RAM空间。”

小蔡想起第2个问题:“等一下,你刚刚说Cortex-M3/M4有两个栈,那发生Hard Fault时8个寄存器究竟保存在用到哪个栈。”

吴解:“‘当前栈’,之前用哪个发生异常后就使用哪个。”

白蔡白他一眼,听君一席话,如听一席话。

吴解看着他的白眼,补充解释:“ARM支持中断嵌套,如果在任务模式陷入Hard Fault,8个寄存器压如PSP栈;若正在处理外部中断服务时 陷入Hard Fault,则压入MSP栈;处理器设计工程师当然有其他途径告诉咱们所谓的‘当前栈’在哪,确认的方式可访问EXC_RETURN值,它存储在LR寄存器中。”

EXC_RETURN全称叫Exception return behavior(异常行为返回值)。EXC_RETURN的bit2等于1时使用main stack、等于0时使用process stack。

吴解从白蔡的电脑登录公司内网打开FreeRTOS源码,找到startup.c:“典型的RTOS在实现Hard Fault时,首先确定之前是哪个栈。'tst lr ,#4' 就是判断bit2的状态,再根据结果将栈地址存储到r0。”

下面是节选FreeRTOS基于Crotex-M3架构下处理Hard Fault的处理过程。    

__asm volatile ( " tst lr, #4 \n" /* 读取bit2 */ " ite eq \n" " mrseq r0, msp \n" /* 如果bit4=1,则msp存入r0 */ " mrsne r0, psp \n" /* 如果bit4=0,则psp存入r0 */ );

白蔡:“8个被压如栈的寄存器都是什么?能提供什么信息?”

吴解打开《Analyzing HardFaults on Cortex-M CPU》:“你瞧这张图,每个寄存器栈4字节,第一个地址是R0接着R1、R2、R3、R12、LR、PC、xPSR。”

白蔡眼镜发光,有方向了:“得到栈地址,栈地址往后第7项目(字)是PC寄存器,是不是就找到发生异常的指令了!妙哉。”

吴解拿起水杯抿上一口,准备离开:“Bingo,如果有有空可以看看CmBacktrace模块,rt-thread的开发者朱天龙写的一个根据Hard Fault实现的backtrace。”

手动触发一次Hard Fault

接下来在FreeRTOS上演示如何触发一次Hard Fault:函数fun1向一个非法地址0xffffffff发起写操作,从而主动引起Hard Fault。

void fun1(){ volatile int *k = (volatile uint32_t *)0xffffffff; *k = 0x123456; return 0;}

gdb窗口显示停留的断点位置准备向0xffffffff地址写入0x123456,对应下面的反汇编代码,此时PC指向0x3cac,此时SP表示PSP,值是0x20008c80。

好,执行后陷入中断,注意到了吗,SP的值跳转了十万八千里,变成0x203fffe0,正常压栈猛地来个4M大小,显然影子寄存器SP指向的位置切换了。

首先根据LR寄存器的值(EXC_RETURN)去判断之前使用的是MSP还是PSP;

第2步将他存储在R0寄存器里;

第3步列出r0后面一些列内容;

第4步比较7个项的内容是啥,是不是指向了0x3cb2地址。

生产环境下没脸上调试器,没gdb怎么办?

别怕,FreeRTOS已经很贴心的帮开发者实现了相关功能prvGetRegistersFromStack,即使不在gdb环境下也能在串口输出寄存器信息,根据地址找到对应汇编代码。

从地址到代码

白蔡面露难色:“你好像高估我了,看不懂汇编代码。下载到板子的程序都是bin文件,没有源码调试信息,就算拿到地址我也不知道对应哪行C代码。”

吴解也嘲笑自己半桶水:“巧了,我也看不懂,君子性非异也,善假于物也。”

白蔡:“假于谁?”

吴解:“于add2line呗。”

add2line傻瓜化的转换地址到对应函数,甚至定位到具体的行(条件是编译加上-g选项)。

$ arm-none-eabi-addr2line -e build/RTOSDemo.axf -a -f 0x3cac0x00003cacfun1~/work/FreeRTOS/Demo/wml-m3/main_blinky.c:142

白蔡:“吴哥,HardFault的处理过程哪里能得到权威的资料,网上的资料好乱。”

吴解出示网文链接:“当然是找ARM原厂下载咯,扫描二维码,关注公众号,发送 “hardfault” 获取。”

参考

  • 《Analyzing HardFaults on Cortex-M CPU》

  • 《ARM v7-M Architecture Reference Manual》




推荐阅读