| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 嵌入式 -> 【cortex-m3/m4/m7常见死机、跑飞、异常、hardfault等查找方法】 -> 正文阅读 |
|
[嵌入式]【cortex-m3/m4/m7常见死机、跑飞、异常、hardfault等查找方法】 |
死机是所有软件从业者无法回避的坑,而死机问题导致的原因千奇百怪,对于可以稳定复现现场的问题,还是比较好处理的,最可怕的情形是你怎么也复现不了,但是在客户那偶发。在此对笔者解决过的死机问题做个分享,若有谬误,请指正。 总的思路为根据堆栈和寄存器,定位到出现异常的语句,然后从逻辑上分析导致其异常可能的原因。
在查找此类问题时,通用寄存器,堆栈,简单的汇编指令是需要必备的基础知识,以我们最常用的M3/M4内核做个简单介绍。具体知识可以自行学习cortex-M3权威手册等资料。 寄存器简介 ?????? (cortex-M3权威手册第三章) CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。
R8‐R12 也被称为高组寄存器。这是因为只有很少的 16 位 Thumb 指令能访问它们, 32 位的指令则不受限制。它们也是 32 位字长,且复位后的初始值是不可预料的。
R13 是堆栈指针。在 CM3 处理器内核中共有两个堆栈指针:
用例程中时)。 堆栈指针用于访问堆栈,并且 PUSH 指令和 POP 指令默认使用 SP。寄存器的 PUSH 和 POP 操作永远都是 4 字节对齐的
常写做LR,用于在调用子程序时存储返回地址。
常写做PC,因为 CM3 内部使用了指令流水线,读 PC 时返回的值是当前指令的地址+4。
程序状态寄存器在其内部又被分为三个子状态寄存器:
通过 MRS/MSR 指令,这 3 个 PSRs 即可以单独访问,也可以组合访问(2 个组合, 3 个组合都可以)。当使用三合一的方式访问时,应使用名字“xPSR”或者“PSR”。
控制寄存器用于定义特权级别,还用于选择当前使用哪个堆栈指针。
0=选择主堆栈指针 MSP(复位后缺省值) 1=选择进程堆栈指针 PSP 在线程或基础级(没有在响应异常——译注),可以使用 PSP。在 handler 模式下,只允许使用 MSP,所以此时不得往该位写 1。
1=用户级的线程模式 Handler 模式永远都是特权级的。 栈内存操作?????? 在 Cortex‐M3 中,除了可以使用 PUSH 和 POP 指令来处理堆栈外,内核还会在异常处理 的始末自动地执行 PUSH 与 POP 操作。 笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由 SP 给出。寄存器的数据通 过 PUSH 操作存入堆栈,以后用 POP 操作从堆栈中取回。 ?????? Cortex‐M3 使用的是“向下生长的满栈”模型。堆栈指针 SP 指向最后一个被压入堆栈的 32 位数值。在下一次压栈时, SP 先自减 4,再存入新的数值。 POP 操作刚好相反:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4。 虽然 POP 后被压入的数值还保存在栈中,但它已经无效了,因为为下次的 PUSH 将覆盖它的值。 异常与中断(cortex-M3权威手册第七章) 所有能打断正常执行流的事件都称为异常。 Cortex‐M3 在内核水平上搭载了一个异常响应系统, 支持为数众多的系统异常和外部中断。其中,编号为 1-15 的对应系统异常,大于等于 16 的则全是外部中断。除了个别异常的优先级被定死外, 其它异常的优先级都是可编程的。 下表列出了 Cortex‐M3 可以支持的所有异常。有一定数量的系统异常是用于 fault 处理的,它们可以由多种错误条件引发。 NVIC 还提供了一些 fault 状态寄存器,以便于 fault 服务例程找出导致异常的具体原因。
优先级对于异常来说很关键的,它会影响一个异常是否能被响应,以及何时可以响应。优先级的数值越小,则优先级越高。 CM3 支持中断嵌套,使得高优先级异常会抢占(preempt)低优先级异常。有 3 个系统异常:复位, NMI 以及硬 fault,它们有固定的优先级,并且它们的优先级号是负数,从而高于所有其它异常。所有其它异常的优先级则都是可编程的(但不能编程为负数)。
当一个发生的异常被 CM3 内核接受,对应的异常 handler 就会执行。为了决定 handler 的入口地址, CM3 使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该异常 handler 的入口地址。向量表的存储位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 处必须包含一张向量表,用于初始时的异常分配。 举个例子,如果发生了异常 3(SVC),则 NVIC 会计算出偏移移量是 3x4=0x0C,然后从那里取出服务例程的入口地址并跳入。 0 号异常的功能则是个另类,它并不是什么入口地址,而是给出了复位后 MSP 的初值。
被归类为硬 fault。
硬 fault 是上文讨论的总线 fault、存储器管理 fault 以及用法 fault 上访的结果。如果这些 fault 的服务例程无法执行,它们就会成为上访(escalation)成硬 fault。 中断/异常的响应序列(cortex-M3权威手册第九章) 当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:
响应异常的第一个行动,就是自动保存现场的必要部分:依次把xPSR, PC, LR, R12以及 R3‐R0由硬件自动压入适当的堆栈中:如果当响应异常时,当前的代码正在使用PSP,则压入 PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用 主堆栈。
从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。
将由MSP负责对堆栈的访问。
并且在异常返回时使用。 后面会有详细解释。
当异常服务例程执行完毕后,需要很正式地做一个“异常返回”动作序列,从而恢复先前的系统状态,才能使被中断的程序得以继续执行。从形式上看,有3种途径可以触发异常返回序列,如表9.2所示;不管使用哪一种,都需要用到先前储的LR的值。
先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指针的值也改回去。
伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若 中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始
前面已经讲到,在进入异常服务程序后, LR的值被自动更新为特殊的EXC_RETURN,这 是一个高28位全为1的值,只有[3:0]的值有特殊含义,如表9.3所示。当异常服务例程把这个 值送往PC时,就会启动处理器的中断返回序列。 合法的EXC_RETURN值及其功能:
如果主程序在线程模式下运行,并且在使用 MSP时被中断 ,则在服务例程中LR=0xFFFF_FFF9(主程序被打断前的LR已被自动入栈)。 如果主程序在线程模式下运行 ,并且在使用 PSP时被中断 ,则在服务例程中LR=0xFFFF_FFFD(主程序被打断前的LR已被自动入栈)。 如果主程序在Handler模式下运行,则在服务例程中LR=0xFFFF_FFF1(主程序被打断前的LR已被自动入栈)。 ?????? 自编写示例(keil+RTT)
真实案例一、RT-Tread assert断言。? ? ? ? ? ??程序仿真,进入如下断言。
搜索代码中的调用进入中断函数osIntEnter(rt_interrupt_enter函数的封装),发现在USB枚举特殊字符串回调函数中,当序号不是0x80时,直接退出,没有释放该标志。 二、中断服务函数中调用RTT操作系统导致异常。以太网设备,在客户处偶发设备无响应的问题(keil+GD32F307+LWIP+RTT) 由于难以复现,无法在仿真调试的环境下查找,此时可以通过客户处异常现场,读取当前寄存器和堆栈数据的方式查找问题(注意你的程序应与客户处设备的程序完全一致,否则很多地址对应不上,所以代码管理很重要)。 前提:
再看rt_thread_ready_table和线程地址双向链表rt_thread_priority_table,均没有可调度线程。 但是正常情况下,idle线程是一直处于就绪状态下的,因此上面的数据至少应包含idle线程,可以基本确认,问题出自此方面。 通过查找map文件找到idle线程结构体,分析其当前状态标志位stat,值为2,查看结构体定义注释,表示处于挂起状态RT_THREAD_SUSPEND:
问题定位,原因是因为在中断服务程序中调用延迟函数rt_thread_sleep,导致的异常,RT-Thread的使用指导中有明确的的说明,在中断中是不允许操作线程相关的调度功能的。那么为什么中断中调用延迟函数会导致异常呢,我们从idle中断被挂起及线程优先级数组均为初始值来分析: 在上电时,初始化函数rt_system_scheduler_init会把双向链表每个节点都连接到该节点本身 在线程退出时,会调用rt_schedule_remove_thread初始化当前线程的节点链表,而这个函数会在下面的情况调用:
由于初始化函数只会执行一次,如果是打印机重启导致变量复位,那么可以通过被初始化成0的变量当前值来确定是否复位了,通过map文件和当前ram数据,查找多个初始化为0的全局变量,如历史记录数据,值均为非0,可以基本排除复位原因。 那么原因很可能是在idle线程执行过程中,被以太网中断打断,以太网中断在某些条件下调用延迟函数,延迟函数中有操作线程状态的函数调用。
?三、电压跌落导致sRam写操作失败导致的异常? ? ? ? 观察上图的PC指针(指向当前代码运行地址)和SP指针(栈顶地址),完全是非法地址。 此时通过指令savebin c\:ram.bin 0x20000000 0xc000将RAM区读出来,对其中的栈区信息与map文件比较,发现代码应该是运行到了上电外设IO初始化时,给外部一个模块(可以当做一个大功率器件)供电的IO初始化后面,猜测与模块供电有关,但是上面的寄存器值无法解释。
该关中断寄存器会把正常中断和异常中断都关掉,若此时产生异常,则无法进入hardfault异常中断。 修改为使用PRIMASK寄存器开关中断后,抓取到的寄存器信息如下: ?????? PC指针指向了RAM区,SP指针的第四字节位于系统栈区,但是高字节应该是0x2000 0000(sRam起始地址),此处无法对应上。 ?????? 而IPSR信息显示,此时进入了Hardfault,但是监控的Hardfault函数位于Flash中,与PC指针对应不上。 ?????? 此时再看代码,会发现中断向量初始化函数位于外设IO初始化函数之后,而查看BOOT的map文件,发现Hardfault正是位于0x2000 23BA处,原因定位,产生异常时,由于中断向量还没有指向监控程序的中断向量位置,寄存器中存储的是BOOT的中断向量表,因此产生异常跳转时,PC指针为BOOT的Hardfault函数地址。 ?????? 调整代码,将中断向量初始化函数移到最开始,再次死机时,PC指针指向了监控程序的Hardfault函数。 ?????? 至此,只有SP寄存器的地址无法解释,怀疑由于它的异常导致死机。
通道1黄色表笔:3.8V_EN 通道2绿色表笔:3.3VMCU供电电压 通道1黄色表笔:3.8V_EN, 通道3蓝色表笔:模块使能开机电流 模块电源开启时模块开机电流接近10A,造成3.3V电压跌落(如图1所示)。电压 跌至2.1V后,回弹到3.7V最后趋于3.3V稳定状态。 查看MCU数据手册 正常工作电压为2.6V,询问芯片厂商,复位电压为1.8V。因此怀疑是由于电压 突降,导致此时寄存器数据写入时部分BIT位没有写入成功,导致后面的访问异常。 |
|
嵌入式 最新文章 |
基于高精度单片机开发红外测温仪方案 |
89C51单片机与DAC0832 |
基于51单片机宠物自动投料喂食器控制系统仿 |
《痞子衡嵌入式半月刊》 第 68 期 |
多思计组实验实验七 简单模型机实验 |
CSC7720 |
启明智显分享| ESP32学习笔记参考--PWM(脉冲 |
STM32初探 |
STM32 总结 |
【STM32】CubeMX例程四---定时器中断(附工 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 | -2025/1/9 1:07:37- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |