本文主要分析在死机时应该保存哪些信息,以便于后续的分析。本文以STM32F767也就是cortex_m7内核为为例。ARM官方文档《ARMv7-M Architecture Reference Manual》下载地址 https://developer.arm.com/documentation/ddi0403/latest/
死机日志相关代码可参考本人开源项目 https://gitee.com/cyytx/intelligent_lockstm32f7xx_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 记录内存管理故障的地址,需结合 MMFSRMMARVALID 位判断有效性。
BFAR BusFault_Handler 记录精确数据访问总线故障的地址,需结合 BFSRBFARV

它们都属于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_lockstm32f7xx_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...