Cortex-M3/M4 不可屏蔽硬件异常处理之非法内存访问

Cortex-M3/M4 不可屏蔽硬件异常处理之非法内存访问

记录使用 RT-Thread 开发 STM32F407 期间出现的硬件异常错误、排查思路及相关内容合集。

一、问题

  • 报错信息

image-20240807144700943

aht10_thread栈空间余量充足排除堆栈溢出,继续根据报错信息进行排除。

  • 最后报错 bus fault:SCB_CFS_BFSR:0x82 PRECISERR SCB->BFAR:BB000010

那么SCB是什么,SCB_CFS_BFSR、SCB->BFAR又是什么?

根据ARM32标准4GB内存空间分配如下,可见SBC为系统控制块其中包含了各个系统控制寄存器。

image-20240806151651562

查看程序可以找到Cotex-M4的SBC基地址定义,如下:

image-20240806160708914

PRECISERR SCB->BFAR:BB000010,代表在0xBB000010内存地址出现了非法访问,显然0xBB000010不在我们所能访问的内存访问内。 --> 结论PC值指向出错指令

image-20240806150110024

image-20240806161518623

scb_cfsr_bfsr:0x82 --> 1000 0010 对应MFSR/MMFSR寄存器 --> 结论PC值指向出错指令

image-20240806152952878

image-20240806153852603

image-20240806161208699

  • 查看PC寄存器及LR寄存器对应内容

关闭debug编译优化等级 -O0

复现错误,但是无法单步执行到具体位置,函数反复横跳,全速运行报错才会出现。

image-20240806180807209

直接根据报错信息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);

image-20240806190536875

image-20240806182802979

应用层关于lv_event_send的使用主要是物理按键按下时,执行页面的管理切换,以及采集数据完成时触发值改变事件来更改控件的显示值,由于页面管理的切换已经单独测试过,定位问题在采集数据部分。数据采集显示部分主要是参考这篇博客,采集线程周期进行数据采样,触发值改变事情,然后在回调函数中进行界面刷新,典型的生产者消费者同步通信。

查阅相关资料,发现LVGL实现机制是基于软件定时器的过程处理+事件处理,有点伪多线程的意思,一个定时器+多个flag事件位,也就是说LVGL内部函数执行过程中是不会被抢占的(简单的通过bool变量,但这其实是会出现问题的,若lv_task_handler()在不同地方多次调用,则无法保证原子),但是仍会被中断所打断。

image-20240806195253433
image-20240808114424157

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)

image-20240806175732234

rt_hw_hard_fault_exception内部会进行相关信息输出:…、bus_fault_track、…

image-20240806172759316

  • 函数多层调用时,什么情况下栈空间中的返回地址被破坏,跳转至某个随机的地方,即"程序跑飞"
  1. 使用未经初始化的指针或者指针错误地指向栈上空间,造成栈空间返回地址被破坏
  2. 局部变量越界访问,一般需要进行静态代码检测,来避免诸如此类问题
  3. 函数调用层次过深,导致线程栈空间被用完访问到其他空间,栈空间余量不够时RT-Thread会发出警告

三、结论

  1. 未能定位到具体问题,但学习掌握了程序出现硬件异常错误诸类问题的排查思路。
  2. 多线程开发时,各线程不应直接使用lv_event_send来影响LVGL线程的周期性。

四、参考

Cortex-M 硬件异常寄存器介绍

Cortex-M 硬件异常案例分析1

!Cortex-M 硬件异常案例分析2

.map文件分析

内存越界案例1-地址映射出错

内存越界案例2-原因不明、数组大小?

内存越界案例3-ulog格式化使用不当

如有侵权请联系作者本人。

  • 49
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值