死机分析之一---死机日志建立
本文主要分析在死机时应该保存哪些信息,以便于后续的分析。本文以STM32F767也就是cortex_m7内核为为例。ARM官方文档《ARMv7-M Architecture Reference Manual》下载地址 https://developer.arm.com/documentation/ddi0403/latest/
死机日志相关代码可参考本人开源项目 https://gitee.com/cyytx/intelligent_lock 的stm32f7xx_it.c debug.c soft_watchdog.c 这几个文件。事实上当死机时,主要保存的信息有两方面,一个就是各种寄存器信息,第二就是软件相关信息,如FreeRTOS的堆栈信息,以及一些软件的运行状态信息。
保存寄存器信息
寄存相关信息可以参考《ARMv7-M Architecture Reference Manual》的B1.4 Registers 章节。以及**B3.2 System Control Space (SCS)**章节。
保存核心寄存器信息
核心寄存器就是 R0-R12,SP,LR,PC这15个,在加一个PSR。

那怎么保存呢?首先,在死机时,
首先就是进入死机中断函数时,硬件自动保存到栈中的信息:也就是 LR,PC,PSR,R0-R3,R12 这八个寄存器的值到栈中(MSP/PSP) 但是这里存在一个问题,就是要知道进入中断函数之前用的是PSP还是MSP呢,这里需要做个判断, 那怎么判断呢?其实可以根据LR的值来判断,因为LR的值是进入中断函数时,硬件保存栈帧后,会将LR设定为特定的值,这个值被称为EXC_RETURN在ARM芯片中,官方代码库的core_xx.h:中搜索EXC_RETURN,可以找到它的定义,如我在core_cm7.h中搜索EXC_RETURN,可以找到它的定义,如下:
#define EXC_RETURN_HANDLER (0xFFFFFFF1UL) /* return to Handler mode, uses MSP after return */
#define EXC_RETURN_THREAD_MSP (0xFFFFFFF9UL) /* return to Thread mode, uses MSP after return*/
#define EXC_RETURN_THREAD_PSP (0xFFFFFFFDUL) /* return to Thread mode, uses PSP after return */
#define EXC_RETURN_HANDLER_FPU (0xFFFFFFE1UL) /* return to Handler mode, uses MSP after return, restore floating-point state */
#define EXC_RETURN_THREAD_MSP_FPU (0xFFFFFFE9UL) /* return to Thread mode, uses MSP after return, restore floating-point state */
#define EXC_RETURN_THREAD_PSP_FPU (0xFFFFFFEDUL) /* return to Thread mode, uses PSP after return, restore floating-point state */
通过分析它,可以知道在最后四个bit中, 1,9,是使用MSP,D则是使用PSP。把它们转换为二进制,可以得到:

由此可以看到,可以利用倒数第三位的值来判断是使用MSP还是PSP,另外还要我们手动把硬件没有保存的R4-R11压入栈中。最后一起打印出来
所以我们可以用下面这样一段内联汇编代码实现判断是使用MSP还是PSP,并通过R0传递给C函数,以便于后续的分析。
__asm volatile (
"mov r2, lr \n" // 将 LR (EXC_RETURN) 保存到 R2,根据AAPCS,R2的是第三个参数
//tst 指令会执行 LR & 0x4 操作,并根据结果设置到PSR的 Z (Zero) 标志位。
//如果结果为 0 (LR & 0x4 == 0),则 Z 标志位被设置为 1。
//如果结果不为 0 (LR & 0x4 != 0),则 Z 标志位被设置为 0
"tst lr, #4 \n" // 测试 LR 的第 2 位, 0=MSP, 1=PSP
"ite eq \n" // 如果 Z 标志位为 1,则执行 eq 分支,否则执行 ne 分支
"moveq r1, #0xaa \n" // 如果是 MSP, R1 = 0xaa
"movne r1, #0xbb \n" // 如果是 PSP, R1 = 0xbb
"ite eq \n" // If-Then-Else Equal
"mrseq r0, msp \n" // 如果是 MSP, R0 = MSP
"mrsne r0, psp \n" // 如果是 PSP, R0 = PSP
// 此时 R0 包含了异常发生时的栈指针,它指向硬件自动压入的栈帧 (R0-R3, R12, LR, PC, PSR)。
// 手动将 R4-R11 压入栈中。
// stmdb: Store Multiple Decrement Before。
// 这会将 R4-R11 压入 R0 指向的栈中,并更新 R0 的值,使其指向新的栈顶(即 R4 的位置)。
"stmdb r0!, {r4-r11} \n"
// 调用 C 处理函数, 根据 AAPCS ,R0, R1, R2 作为参数1,2,
"bl xxx_Handler_C\n"
}
由此,可以通过确定的sp指针确定栈地址,从而打印栈中的信息。另外同时也可以把EXC_RETURN和是使用的哪个SP(MSP/PSP)打印出来
//定义栈帧结构体
typedef struct {
uint32_t r4;
uint32_t r5;
uint32_t r6;
uint32_t r7;
uint32_t r8;
uint32_t r9;
uint32_t r10;
uint32_t r11;
// 硬件自动压入的寄存器
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr; // EXC_RETURN value
uint32_t pc;
uint32_t psr;
} FullFaultStackFrame_t;
// 打印故障信息的C函数
void print_fault_info(void* fault_stack, uint32_t stack_type, uint32_t exc_return)
{
// 打印EXC_RETURN值和堆栈类型
crash_printf("EXC_RETURN = 0x%X\r\n", exc_return);
//打印使用哪个栈
if (stack_type == 0xaa)
{
crash_printf("Stack used: MSP\r\n", 0);
}
else
{
crash_printf("Stack used: PSP\r\n", 0);
}
//这里减去32是因我我在进入中断函数中,手动压入了R4-R11,又因为栈是向下生长的,所以栈顶指针需要加去32,展示原来栈顶的地址
crash_printf("\r\n=== SP addr: 0x%X ===\r\n", (uint32_t)fault_stack + 32);//打印栈地址
// 将堆栈指针转换为完整的异常栈帧结构
FullFaultStackFrame_t* stack_frame = (FullFaultStackFrame_t*)fault_stack;
// 打印异常发生时的寄存器值
crash_printf("\r\n=== Full Exception Stack Frame ===\r\n",0);
crash_printf("R0 = 0x%X\r\n", stack_frame->r0); // 通常包含函数参数或返回值
crash_printf("R1 = 0x%X\r\n", stack_frame->r1); // 通常包含函数参数
crash_printf("R2 = 0x%X\r\n", stack_frame->r2); // 通常包含函数参数
crash_printf("R3 = 0x%X\r\n", stack_frame->r3); // 通常包含函数参数
crash_printf("R4 = 0x%X\r\n", stack_frame->r4); // 手动保存的寄存器
crash_printf("R5 = 0x%X\r\n", stack_frame->r5);
crash_printf("R6 = 0x%X\r\n", stack_frame->r6);
crash_printf("R7 = 0x%X\r\n", stack_frame->r7);
crash_printf("R8 = 0x%X\r\n", stack_frame->r8);
crash_printf("R9 = 0x%X\r\n", stack_frame->r9);
crash_printf("R10 = 0x%X\r\n", stack_frame->r10);
crash_printf("R11 = 0x%X\r\n", stack_frame->r11);
crash_printf("R12 = 0x%X\r\n", stack_frame->r12); // IP (中间变量)
crash_printf("LR = 0x%X\r\n", stack_frame->lr); // 返回地址 (EXC_RETURN value)
crash_printf("PC = 0x%X\r\n", stack_frame->pc); // 异常发生的指令地址
crash_printf("PSR = 0x%X\r\n", stack_frame->psr); // 程序状态寄存器
屏蔽和系统相关寄存器
有些寄存器能够屏蔽中断和错误,这个如果长时间不撤销屏蔽可能导致看门狗超时之类导致死机。我之前就在FreeRTOS中遇到过在没 xTaskCreate 后不直接调用vTaskStartScheduler(),开始调度,而是做了其他外设的初始并使用了delay,但是由于在FreeRTOS没开始调度前是会xTaskCreate会导致部分中断屏蔽,delay一直没有值更新导致了看门狗超时。 屏蔽相关寄存器如下:
BASEPRI :Base Priority Mask Register(基本优先级屏蔽寄存器), 用于屏蔽优先级低于指定阈值的所有中断,实现优先级过滤
PRIMASK : Primary Interrupt Mask Register(主中断屏蔽寄存器),用于屏蔽所有可配置中断和异常,只允许 NMI 和 HardFault 发生。
FAULTMASK: Fault Mask Register(故障屏蔽寄存器),用于将处理器优先级提升到 HardFault 级别,屏蔽所有可屏蔽异常,只允许 NMI 发生 。

另外还有一个寄存器就是CONTROL 它可以设置或者用于记录内核当前使用的模式和哪个栈(msp/psp),以及浮点上下文活动状态,事实上我认为记录它意义不大,因为它记录的是现在的状况,目前已经进入了中断处理了,他的模式和栈都已经不是之前出问题的时候的了,出问题时的都记录在EXC_RETURN里面。不过它的浮点相关的或许有些帮助,也就记录下来。
所以总的代码可以这样写
crash_printf("CONTROL = 0x%X\r\n", __get_CONTROL()); // 控制寄存器
crash_printf("BASEPRI = 0x%X\r\n", __get_BASEPRI()); // 基础优先级寄存器
crash_printf("PRIMASK = 0x%X\r\n", __get_PRIMASK()); // 优先级屏蔽寄存器
crash_printf("FAULTMASK = 0x%X\r\n", __get_FAULTMASK());//故障屏蔽寄存器
故障错误相关寄存器
ARM Cortex-M 会提供一些故障状态寄存器,它们在系统发生异常时提供更详细的信息,这些信息非常有助于分析,这些寄存器具体可参考《ARMv7-M Architecture Reference Manual》的B3.2 System Control Space (SCS)一章,主要为下面这些寄存器。
CFSR : Configurable Fault Status Register(可配置故障状态寄存器),包含了内存管理故障 (MemManage Fault)、总线故障 (BusFault) 和使用故障 (UsageFault) 的详细状态信息。
MMFSR : MemManage Fault Status Register(内存管理故障状态寄存器),指示内存管理单元 (MPU) 相关的访问违规,例如访问了受保护的内存区域。
BFSR : BusFault Status Register(总线故障状态寄存器),指示总线操作中发生的错误,例如无效的内存地址访问或未对齐访问。
UFSR : UsageFault Status Register(使用故障状态寄存器),指示程序执行中的错误,例如执行了未定义的指令、尝试除以零或未对齐的数据访问。
HFSR : HardFault Status Register(硬故障状态寄存器),指示 HardFault 异常的来源,例如是否由其他故障升级而来。
DFSR : Debug Fault Status Register(调试故障状态寄存器),指示由调试事件引起的故障。
AFSR : Auxiliary Fault Status Register(辅助故障状态寄存器),提供芯片厂商特定的故障状态信息。
MMFAR : MemManage Fault Address Register(内存管理故障地址寄存器),当发生内存管理故障时,记录导致故障的内存地址。
BFAR : BusFault Address Register(总线故障地址寄存器),当发生总线故障时,记录导致精确数据访问故障的内存地址。
另外事实上,出现问题后,相关寄存器记录了错误信息,而它们其实也对应这具体的处理函数,所以当我们从代码侧看到触发了具体的中断函数后,可以倒推过来重点关注这些寄存器
| 故障状态寄存器 | 触发的 Handler 函数 | 备注 |
|---|---|---|
MMFSR |
MemManage_Handler |
内存管理单元 (MPU) 相关的访问违规。 |
BFSR |
BusFault_Handler |
总线操作中发生的错误。 |
UFSR |
UsageFault_Handler |
程序执行中的使用错误(如未定义指令、除零)。 |
HFSR |
HardFault_Handler |
HardFault 异常的来源,通常是其他故障升级而来或更严重的系统错误。 |
DFSR |
DebugMon_Handler |
由调试事件引起的故障。 |
AFSR |
HardFault_Handler (或相关故障) |
提供芯片厂商特定的辅助故障信息,通常在 HardFault 或其他故障中检查。 |
MMFAR |
MemManage_Handler |
记录内存管理故障的地址,需结合 MMFSR 的 MMARVALID 位判断有效性。 |
BFAR |
BusFault_Handler |
记录精确数据访问总线故障的地址,需结合 BFSR 的 BFARV |
它们都属于SCB(System Control Space)的寄存器,具体代码如下
// 打印主要fault寄存器值
crash_printf("\r\n=== Fault Status Info ===\r\n", 0);
crash_printf("CFSR = 0x%X\r\n", SCB->CFSR);
crash_printf("HFSR = 0x%X\r\n", SCB->HFSR);
crash_printf("DFSR = 0x%X\r\n", SCB->DFSR);
crash_printf("AFSR = 0x%X\r\n", SCB->AFSR);
crash_printf("MMFAR = 0x%X\r\n", SCB->MMFAR);
crash_printf("BFAR = 0x%X\r\n", SCB->BFAR);
建立软件看门狗
硬件看门狗会有个问题,就是它直接就重启了,没有机会给你保存日志,所以需要建立个软件看门狗,并且这个软件看门狗设置为最高优先级,软件看门狗要用timer来做,它本质上就是只需要一个计时功能和中断功能就可以,所以通常选择最简单的TIMER 如下,我的stm32f7的手册中就选择basic timer

如下以timer 7 作为看门狗
htim7.Instance = TIM7;
htim7.Init.Prescaler = uwPrescalerValue;// 预分频器
htim7.Init.CounterMode = TIM_COUNTERMODE_UP;// 计数模式为向上计数
//计数频率为2k,则周期为500us,超时时间为 500us * (Period+1)
htim7.Init.Period = WATCHDOG_TIMEOUT_SECONDS * 1000 * 2 - 1; //设置计数器周期,也就是超时时间
htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;//禁止自动重装载
---------省略-------------
HAL_NVIC_SetPriority(TIM7_IRQn, 0, 0);//作为看门狗,优先级设置为最高的0
HAL_NVIC_EnableIRQ(TIM7_IRQn);
如果是在FreeRTOS中就可以在 idle task 里面喂狗,如果系统卡死了,idle taks 一直得不到调用就会导致喂狗不及时导致计时器溢出产生中断,则在中断里执行死机日志的打印保存和重启。
void vApplicationIdleHook(void)
{
__HAL_TIM_SET_COUNTER(&htim7, 0);//软看门狗,定时器7清零
.........省略........
}
软件相关日志
本文以FreeRTOS作为例子进行分析
记录死机前任务调度记录
记录死机前的任务调度记录,有助于分析一些卡死导致看门狗超时,或者是其他错误时,帮助定位到具体的任务。
在任务切换函数vTaskSwitchContext 里面会有 调用 traceTASK_SWITCHED_IN();
void vTaskSwitchContext( void )
{
......
traceTASK_SWITCHED_IN();
}
所以我们只需要定义 traceTASK_SWITCHED_IN()为我们的函数,并使用一个循环数组一直记录切换的任务ID和切换时间,这样就能在死机时,只需打印taskSwitchHistory 数组就可以
#define traceTASK_SWITCHED_IN() record_task_switch(pxCurrentTCB) //定义追踪函数
#define MAX_TASK_SWITCH_RECORDS 16 //记录最后16次
// 任务切换记录结构,记录任务id 和时间
typedef struct {
UBaseType_t uxTaskNumber; // 任务编号
uint32_t timestamp; // 时间戳
} TaskSwitchRecord_t;
static TaskSwitchRecord_t taskSwitchHistory[MAX_TASK_SWITCH_RECORDS]; //用于记录的数组
// 记录任务切换
void record_task_switch(void* currentTCB)
{
uint8_t idx = taskSwitchIndex;
//MAX_TASK_SWITCH_RECORDS为2的幂次方,可以减少计算周期,在任务切换期间减少计算量很重要。
taskSwitchIndex = (taskSwitchIndex + 1) % MAX_TASK_SWITCH_RECORDS;
switchCount++;
// 使用任务句柄获取任务编号
taskSwitchHistory[idx].uxTaskNumber = uxTaskGetTCBNumber((TaskHandle_t)currentTCB);
taskSwitchHistory[idx].timestamp = xTaskGetTickCount();
}
记录所有任务名称,ID,栈地址以及最后一次的最大栈使用空间。
首先这些关于任务的最大栈使用空间的日志,其实可以常规打印,比如在每次进入休眠前做一个打印,这样的话可以帮助分析栈空间的使用情况,也可以帮助优化任务栈道大小。
1、在FreeRTOS中,uxTaskGetSystemState 函数可以获取所有任务的信息,信息类型如下,
typedef struct xTASK_STATUS
{
TaskHandle_t xHandle; //任务句柄
const char *pcTaskName; //任务名字
UBaseType_t xTaskNumber; //任务ID
eTaskState eCurrentState; //任务状态
UBaseType_t uxCurrentPriority; //任务当前优先级,互斥锁时可能会继承别人的优先级,基本情况下和uxBasePriority相同
UBaseType_t uxBasePriority; //任务基础优先级,就是创建任务时配置的优先级
uint32_t ulRunTimeCounter; //任务运行时间
StackType_t *pxStackBase; //任务栈基地址
configSTACK_DEPTH_TYPE usStackHighWaterMark;//任务栈高水位标记,可以用来计算栈使用空间。
} TaskStatus_t;
2、启动configRECORD_STACK_HIGH_ADDRESS 宏,让它记录栈顶地址,这样,我们就可以既知道栈基和栈顶了,同时也可以知道栈大小。
#if( configRECORD_STACK_HIGH_ADDRESS == 1 )
{
/* Also record the stack's high address, which may assist
debugging. */
pxNewTCB->pxEndOfStack = pxTopOfStack;
}
3、汇总这些信息并将它们打印就可以了,不过需要注意的是,在死机时的中断函数里面,不能调用FreeRTOS的API,比如uxTaskGetSystemState 它有进入临界区的操作,会导致断言失败,所以在死机时可以打印上次保留的信息,这样的信息其实也够了。
void printf_all_task_info_crash(void)
{
uint32_t task_count = 0;
uint32_t x;
if(uxArraySize > MAX_TASKS)
{
task_count = MAX_TASKS;
}else
{
task_count = uxArraySize;
}
;
DEBUG_LOG("TaskName ID Status Prio(C/B) StackStart MaxUsed StackEnd PeakUsage(%%)\r\n");
DEBUG_LOG("---------------- -- ------ --------- ---------- ---------- ---------- ------------\r\n");
for (x = 0; x < task_count; x++)
{
char cStatus = '?';
switch (pxTaskStatusArray[x].eCurrentState)
{
case eRunning:
cStatus = 'R';
break;
case eReady:
cStatus = 'r';
break;
case eBlocked:
cStatus = 'B';
break;
case eSuspended:
cStatus = 'S';
break;
case eDeleted:
cStatus = 'D';
break;
default:
cStatus = '?';
break;
};
configSTACK_DEPTH_TYPE peak_used_words = 0;
float usage_percentage = 0.0f;
if (total_stack_depth_words > 0) // 确保获取到了总堆栈大小
{
peak_used_words = total_stack_depth_words[x] - pxTaskStatusArray[x].usStackHighWaterMark;;
usage_percentage = ((float)peak_used_words * 100.0f) / (float)total_stack_depth_words[x];
}
DEBUG_LOG("%-16s %2lu %c %2lu/%2lu 0x%08lx %10lu 0x%08lx %7.2f%%\r\n",
// name_buffer,
pxTaskStatusArray[x].pcTaskName,
pxTaskStatusArray[x].xTaskNumber, // 打印任务ID
cStatus,
pxTaskStatusArray[x].uxCurrentPriority,
pxTaskStatusArray[x].uxBasePriority,
(unsigned long)pxTaskStatusArray[x].pxStackBase,
(unsigned long)peak_used_words*4, // usStackHighWaterMark 单位是 words
(unsigned long)pxTaskStatusArray[x].pxStackBase + total_stack_depth_words[x]*4, // total_stack_depth_words 单位是 words
usage_percentage);
}
ERROR_LOG("----------------------------------------------------------------------------------------\r\n");
}
实际日志展示
我这里直接做不喂狗,让软件看门狗直接超时,这个时候显示的死机日志如下详细可参考本人开源项目 https://gitee.com/cyytx/intelligent_lock 的stm32f7xx_it.c debug.c soft_watchdog.c 这几个文件。
TIM7_IRQHandler, WATCHDOG TIMEOUT DETECTED!
EXC_RETURN = 0xFFFFFFFD
Stack used: PSP
=== Current tick: 9999 ===
=== SP addr: 0x20000430 ===
=== Full Exception Stack Frame ===
R0 = 0x00000001
R1 = 0x00000000
R2 = 0x00001008
R3 = 0xA5A5A5A5
R4 = 0xA5A5A5A5
R5 = 0xA5A5A5A5
R6 = 0xA5A5A5A5
R7 = 0xA5A5A5A5
R8 = 0xA5A5A5A5
R9 = 0xA5A5A5A5
R10 = 0xA5A5A5A5
R11 = 0xA5A5A5A5
R12 = 0xA5A5A5A5
LR = 0x0805B6AB
PC = 0x0806A9DE
PSR = 0x81000000
=== System Control Registers ===
CONTROL = 0x00000000
BASEPRI = 0x00000000
PRIMASK = 0x00000000
FAULTMASK = 0x00000000
=== Fault Status Info ===
CFSR = 0x00000000
HFSR = 0x00000000
DFSR = 0x00000000
AFSR = 0x00000000
MMFAR = 0x00000000
BFAR = 0x00000000
=== Last Task Switch History ===
Task Number | Timestamp
------------|----------
2 | 9994
5 | 9940
2 | 9940
5 | 9943
2 | 9944
5 | 9956
2 | 9956
5 | 9962
2 | 9962
5 | 9975
2 | 9975
5 | 9978
2 | 9978
5 | 9991
2 | 9991
5 | 9994
=== Last Recorded All Task Information ===
[D:9999] TaskName ID Status Prio(C/B) StackStart MaxUsed StackEnd PeakUsage(%)
[D:9999] ---------------- -- ------ --------- ---------- ---------- ---------- ------------
[D:9999] MainTask 1 R 40/40 0x200252e0 2200 0x200262dc 53.76%
[D:9999] IDLE 2 r 0/ 0 0x20000264 56 0x20000464 10.94%
[D:9999] Tmr Svc 3 B 27/27 0x20000b0c 248 0x20000f0c 24.22%
[D:9999] app_task 4 B 29/29 0x200263f8 364 0x20026bf4 17.81%
[D:9999] DisplayTask 5 r 19/19 0x20026d38 168 0x20027534 8.22%
[D:9999] camera_task 6 B 20/20 0x20027610 260 0x20027e0c 12.72%
[D:9999] keyboardTask 7 S 28/28 0x20027e80 104 0x2002867c 5.09%
[D:9999] ledTask 8 B 1/ 1 0x200286f0 80 0x200288ec 15.75%
[D:9999] lock_ctrl_task 9 B 30/30 0x200289c0 292 0x20028dbc 28.63%
[D:9999] FP_Task 10 B 25/25 0x20028ee8 348 0x200296e4 17.03%
[D:9999] NfcTask 11 B 14/14 0x20029790 456 0x20029b8c 44.71%
[D:9999] BLE_Task 12 B 23/23 0x20029e10 192 0x2002a20c 18.82%
[D:9999] FaceTask 13 B 23/23 0x2002a338 356 0x2002b334 8.70%
[D:9999] swdraw 14 r 3/ 3 0x2002b5e8 40 0x2002d5e4 0.49%
[D:9999] LVGL_Task 15 r 18/18 0x2002d770 40 0x2002f76c 0.49%
[E:9999] ----------------------------------------------------------------------------------------
System will reset in 5 seconds...