Cortex-M3/M4 不可屏蔽硬件异常处理之非法内存访问
记录使用 RT-Thread 开发 STM32F407 期间出现的硬件异常错误、排查思路及相关内容合集。
一、问题
aht10_thread栈空间余量充足排除堆栈溢出,继续根据报错信息进行排除。
那么SCB是什么,SCB_CFS_BFSR、SCB->BFAR又是什么?
根据ARM32标准4GB内存空间分配如下,可见SBC为系统控制块其中包含了各个系统控制寄存器。
查看程序可以找到Cotex-M4的SBC基地址定义,如下:
PRECISERR SCB->BFAR:BB000010,代表在0xBB000010内存地址出现了非法访问,显然0xBB000010不在我们所能访问的内存访问内。 -->
结论PC值指向出错指令
scb_cfsr_bfsr:0x82 --> 1000 0010 对应MFSR/MMFSR寄存器 -->
结论PC值指向出错指令
关闭debug编译优化等级 -O0
复现错误,但是无法单步执行到具体位置,函数反复横跳,全速运行报错才会出现。
直接根据报错信息PC
寄存器和LR
寄存器,查看.map映射,可以发现是在函数lv_obj_get_event_dsc
期间出现错误,上级调用为event_send_code
,最终能够给到应用层调用的只有lv_event_send
函数,故排除一切有调用lv_event_send的地方。
lv_res_t lv_event_send(lv_obj_t * obj, lv_event_code_t event_code, void * param);
static lv_res_t event_send_core(lv_event_t * e);
static lv_event_dsc_t * lv_obj_get_event_dsc(const lv_obj_t * obj, uint32_t id);
应用层关于lv_event_send
的使用主要是物理按键按下时,执行页面的管理切换,以及采集数据完成时触发值改变事件来更改控件的显示值,由于页面管理的切换已经单独测试过,定位问题在采集数据部分。数据采集显示部分主要是参考这篇博客,采集线程周期进行数据采样,触发值改变事情,然后在回调函数中进行界面刷新,典型的生产者消费者同步通信。
查阅相关资料,发现LVGL实现机制是基于软件定时器的过程处理+事件处理,有点伪多线程的意思,一个定时器+多个flag事件位,也就是说LVGL内部函数执行过程中是不会被抢占的(简单的通过bool变量,但这其实是会出现问题的,若lv_task_handler()在不同地方多次调用,则无法保证原子),但是仍会被中断所打断。
LVGL主函数内容如下:
while (1)
{
lv_task_handler();
rt_thread_mdelay(LV_DISP_DEF_REFR_PERIOD);
}
// lv_task_handler()函数简化如下:
uint32_t LV_ATTRIBUTE_TIMER_HANDLER lv_timer_handler(void) {...
static bool already_running = false;
if(already_running) {
TIMER_TRACE("already running, concurrent calls are not allow, returning");
return 1;
}
already_running = true;
...
do {
LV_GC_ROOT(_lv_timer_act) = _lv_ll_get_head(&LV_GC_ROOT(_lv_timer_ll));
while(LV_GC_ROOT(_lv_timer_act)) {
next = _lv_ll_get_next(&LV_GC_ROOT(_lv_timer_ll), LV_GC_ROOT(_lv_timer_act));
LV_GC_ROOT(_lv_timer_act) = next; /*Load the next timer*/
}
} while(LV_GC_ROOT(_lv_timer_act));
uint32_t time_till_next = LV_NO_TIMER_READY;
next = _lv_ll_get_head(&LV_GC_ROOT(_lv_timer_ll));
while(next) {
if(!next->paused) {
uint32_t delay = lv_timer_time_remaining(next);
if(delay < time_till_next)
time_till_next = delay;
}
next = _lv_ll_get_next(&LV_GC_ROOT(_lv_timer_ll), next); /*Find the next timer*/
}
...
already_running = false; /*Release the mutex*/
return time_till_next;
}
最终调试也未能定位到具体原因,个人推测原因如下:
页面管理模块使用lv_event_send发送事件不会出现问题,本质上是因为此时的lv_event_send上层调用keypad_read(),即输入设备的回调函数indev_drv.read_cb = keypad_read,它还是由LVGL线程内部软件定时器所管理的,是周期的,是能保证执行完整的,没人直接影响它
。
而采集模块部分内容则是由多个不同的线程像信号量的机制一样发送事件位给到LVGL线程,此时lv_event_send所触发的回调函数时机是不确定的,并且作为一个线程的LVGL此时是会被抢占的,如果上次回调执行的过程中有高优先级任务来进行抢占,高优先级任务再次进行lv_event_send时就可能破坏了LVGL其线程栈空间关于事件回调部分内容,导致出现非法内存访问
。
总的来说就是LVGL线程本身的"高内聚、低耦合"特性被我们所破坏,改进思路
是其他线程中不应直接使用lv_event_send来影响LVGL线程,各采集线程与其通信应采用生产者消费者异步通讯方式
,即创建缓存buff,且接收方LVGL线程可以按照自己的执行周期来从buff中取走数据,最终测试非法内存访问错误不再出现。
本文所触发的非法内存访问,多半就是这篇文章中的指针越界引起的core dump。原本单独的LVGL程序是周期的,即每个软定时器函数都会被执行完,现在虽然作为一个线程加入了整个程序之中,但其仍由自己的线程栈用于保存现场进行调度,本不应该出现问题。但是外部线程执行时,经常使用lv_event_send来直接影响它,由于LVGL本身是支持裸机,上文也提到它的保护机制是非常简单的只是一个flag标志,这就导致其自身内部代码可能缺乏一定必要的保护。文章中提到可以分析core文件,进行对比,但是在单片机开发中我并未找到此类方法,有懂的小伙伴还望告知。
二、提问
// 调用关系
DCD HardFault_Handler
_get_sp_done
_update_done
void rt_hw_hard_fault_exception(struct exception_info *exception_info)
static void hard_fault_track(void)
static void bus_fault_track(void)
rt_hw_hard_fault_exception
内部会进行相关信息输出:…、bus_fault_track、…
- 使用未经初始化的指针或者指针错误地指向栈上空间,造成栈空间返回地址被破坏
- 局部变量越界访问,一般需要进行静态代码检测,来避免诸如此类问题
- 函数调用层次过深,导致线程栈空间被用完访问到其他空间,栈空间余量不够时RT-Thread会发出警告
三、结论
- 未能定位到具体问题,但学习掌握了程序出现硬件异常错误诸类问题的排查思路。
- 多线程开发时,各线程不应直接使用lv_event_send来影响LVGL线程的周期性。
四、参考
如有侵权请联系作者本人。