英飞凌TriCore异常问题排查定位详解跑飞死机
【英飞凌TriCore】异常问题排查定位详解(“跑飞”、“死机”)
本篇文章针对在英飞凌TriCore上,使用符合AutoSAR规范要求的基础软件时,当遇到的“死机”、“跑飞”等问题时,给出了一个比较系统的正向排查思路。我们知道,知道问题所在才能进一步解决问题,但是往往这种“死机”问题都给我们一种无法下手,锁定问题的痛苦。
本文由上到下以实际的“案例”从头到尾给读者展示了针对不同类别的“死机”问题,是如何一步一步定位到出现问题的地点,并进一步锁定问题,从而解决问题的整个过程,帮助读者能够掌握异常问题排查定位的手段,从而在自己经历的项目上不断应用总结,并形成一套符合当前公司调性的死机问题排查经验,最终达到在有限的问题状况了解下能够快速定位到实际的问题。
写在前边
作为一个嵌入式工程师,因为我们实现的逻辑代码往往是“嵌入”到现有的工程当中,这个现有的工程可能是芯片厂商针对某种应用方案Demo板提供的SDK,或者是一种无线通信的协议栈实现,亦或是我们自己使用芯片厂商提供的驱动搭配上实时操作系统实现的“基础工程”。这就无法避免需要对不是我们实现的代码进行阅读与调试。在添加“嵌入”的逻辑代码之后,往往不会一帆风顺,有时我们的代码测试结果没有如我们实现所想,更有甚者会导致“死机”、“跑飞”等问题。面对测试的结果与我们的实现有出入的时候,我们可以调整通过调整实现参数与逻辑来不断“试错”,帮助我们快速理解那些不是我们实现的代码。但是面对“死机”、“跑飞”等问题,我们没有办法像前面一样能够根据测试结果的反馈来不断调整我们的“试错”方向,那么”异常问题的排查定位”就作为我们必不可少的一个正向排查此类问题的方法(面对“死机”,“跑飞”等问题,如果我们“嵌入”的代码量较小,往往可以采用逐步注释这种反向方式查出“死机”等问题的源头)。下面是常见的排查方向。
本篇文章涉及的芯片为英飞凌的3系列芯片,使用的工程为符合AutoSAR架构的基础工程,“死机”和“跑飞”这种问题如果从正面查,往往得利用芯片或者“基础工程”提供的基础设施查起,本文涉及的主要包括:
- DET:开发错误追踪器(Default Error Tracer),符合AutoSAR规范的模块实现会根据DET规定的错误信息结构上报在运行过程中的错误(诸如传入参数无效,操作超时等)。
- ErrorHook:OS提供的错误回调函数,RTA-OS的API在运行时如果失败,会返回错误代码。如果启用了ErrorHook,则当任何API调用返回错误代码时,RTA-OS将调用ErrorHook这个回调函数,并传入错误代码。
- TRAP:TriCore的异常处理机制称为Trap系统,TriCore在硬件在运行过程中,会对内存的处理、指令取指执行等操作做监控,当检测到异常时,会根据当前的异常类型执行相应的异常动作,并提供异常现场信息供用户分析。
- 其他:还有一些用户代码实现的运行/错误log信息,也会很大程度的帮助我们查询死机问题。芯片提供的RSTSTAT寄存器等。 下面我们结合实例来分别介绍这四种异常问题的排查方法。
软件运行时检查:DET
这个排查方向是基于AutoSAR规定的每个模块(BSW所涉及的所有模块)在需要在运行时统一向这个DET模块汇报错误信息。每个模块原则上需要向上检验输入的参数等信息是否合规,向下则需要保证底层的状态是否正常,能否正确提供服务。现在的工程中DET模块对运行中的错误采取的策略紧紧是记录,因为这些问题往往会立即的导致某些模块无法工作,而工程师可以利用DET记录的信息进行排查,如下文所示的DET记录:
从上图可以看到,DET记录了某个模块的错误上报信息,其中包括ModuleId(模块ID)、InstanceId(模块实例ID)、ApiId(模块内服务ID)和ErrorId(错误ID)。我们首先根据ModuleId锁定是哪个模块上报的信息,我们在工程的代码中搜索“_MODULE_ID
“,即可得到当前工程中的模块ID信息。
如上图所示,是CanIf模块,因为我们工程就一个CanIf模块实例,所以我们直接查找CanIf模块中对应为5的SID,如下图。
可以看到是发送方面的问题,这也许对应了我们实际报文发不出去的情况, 下面我们再查对应的错误信息70是什么具体错误。
可以看到是CANIF_E_STOPPED错误。这个时候我们就可以搜索模块在代码中的那个部分上报了这个错误,如下图所示。
上图我是手改了错误汇报为真,所以向DET汇报了这个错误,实际的过程中我们汇报条件是(Pdu_mode_Lcl == (uint8)CANIF_OFFLINE)||(Ctrl_mode_Lcl == (uint8)CANIF_CS_STOPPED),可以看到有可能是控制器并没有在START状态,导致这个问题的原因可能是我们初始化CAN控制器的过程中遇到超时的问题导致CAN控制器初始化不完全或者是BswM中没有启动对应CAN通道,也有可能是当前的CanIf模块被请求为CANIF_OFFLINE,一般完成这部分模块配置的工程师获取到这个错误信息基本就能想起起自己在配置过程中哪里忘记配置了或者哪个参数配置的不合适。
OS错误回调:ErrorHook
ErrorHook是我们使用的OS提供的一个回调函数,用于记录OS在运行过程中的错误信息,下面是这个回调函数原始的样子。 FUNC(void, OS_ERRORHOOK_CODE) ErrorHook(StatusType Error) { ISRType ISRInError = 0; TaskType TaskInError = 0; StatusType Status_Type; OsTrace_ErrorHookLog_t* err; /*
- This code sample shows how to determine which Task or
- ISR was running when the error occurred. / ISRInError = GetISRID(); if (ISRInError != INVALID_ISR) { / The error occurred when ISR ‘ISRInError’ was running / } else { Status_Type = GetTaskID(&TaskInError); if (TaskInError != INVALID_TASK) { / The error occurred when Task ‘TaskInError’ was running / } else { / The error occurred during StartOS, Os_Cbk_Idle or ShutdownOS / } } err = &err_log[err_cnt % (uint8)ERR_LOG_MAX]; err->errCount = err_cnt; err->errType = Error; err->isr = ISRInError; err->task = TaskInError; err_cnt++; /
- This code sample shows how to determine which error
- was detected.
/
switch (Error){
case E_OS_ID:
/ Handle illegal identifier error /
break;
case E_OS_VALUE:
/ Handle illegal value error /
break;
case E_OS_STATE:
/ Handle illegal state error /
break;
case E_OS_LIMIT:
/ Terminate. /
REQUESTFAULTREACTION(E_OS_LIMIT);
// ShutdownOS(Error);
default:
/ Handle all other types of error */
break;
}
while(1)
{
}
}
可以看到程序运行到这里最后是一个while(1)的死循环,其主要记录了一个OS的错误类型,什么时候会触发那个类型的错误也比较宽泛,有兴趣的读者可以自行翻阅《RTA-
OS User
Guide.pdf》。下面我们举一个例子,我们首先在一个中断里添加一个While(1)循环,这会导致它打断的任务会一直结束不了,导致这个任务第二次调度的时候会导致E_OS_LIMIT(Task
Preemptability配置为均Full,Activations为1)。
下图是错误是的调用栈(调用栈信息的显示基于英飞凌平台提供的CSA上下文管理机制,当前的OS是会停用ISTACK,并根据调度任务优先级动态配置当前CPU优先级,所有的任务均以Timer的超时事件(1ms),并根据调度表中任务的周期进行调度,保证了任务调度的实时性)信息。
OS在英飞凌芯片处理进中断、出中断的基础上进行了改动,将所有中断统一到Os_ISRWrapperX处理函数中,在中断服务函数以及Task调度之前和之后加入了一些操作(诸如重新打开全局中断是能),使得OS的任务和各类中断能在一定意义上达到“平权”的状态。详细的英飞凌中断与Task和Function调用处理大家可以参照《TriCore TC1.6.2 core architecture manual》,相对于ARM我个人认为写的相对易懂。 OS还提供了Os_Cbk_StackOverrunHook和ProtectionHook等回调,他们会基于用户配置的栈信息进行溢出监控并会出现TRAP等异常时在关断
芯片异常中断:TRAP
Trap作为英飞凌TriCore提供的异常中断处理机制,它本身的处理往往异常简单,由硬件进行现场保护之后,就会根据当前异常中断大类在中断向量表查找对应处理函数入口地址并进行跳转,OS在这里做的TRAP中断向量表统一了八个大类的处理函数并将大类的信息传递给处理函数,下面是他的TRAP表。
asm(" .file "Os_traps.s"\n
\n
#==========================================\n\
_Os_TrapVectorTable\n\
#\n\
Call depth counting must be disabled when jumping to Os_ProtectionLog.\n\
This is in case the OS is reset in the Shutdown hook, because\n\
Os_longjmp will otherwise see a call depth underflow\n\
#==========================================\n
.section ".os_interrupt_code.ostraps", "ax", @progbits\n
.balign 256 # The vector table must be aligned such that the lowest 8 bits of its address are zero\n
.global Os_TrapVectorTable\n
Os_TrapVectorTable:\n\
—————-\n\
MMU (Virtual memory)\n\
—————-\n\
.global Os_memory_trap\n
Os_memory_trap:\n
mov %d4, 14 # Status for Os_UnhandledTrap\n
mov %d5, 0 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
Internal Protection [$TargetHT 127]\n\
—————-\n\
.balign 32\n
.global Os_protection_trap\n
Os_protection_trap:\n
mov %d4, 14 # Status for Os_UnhandledTrap\n
mov %d5, 1 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
Instruction (Illegal ops, address errors) [$TgtCore 110]\n\
—————-\n\
.balign 32\n
.global Os_instruction_trap\n
Os_instruction_trap:\n
mov %d4, 18 # Status for Os_UnhandledTrap\n
mov %d5, 2 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
Context call fault\n\
—————-\n\
.balign 32\n
.global Os_context_trap\n
Os_context_trap:\n
mov %d4, 18 # Status for Os_UnhandledTrap\n
mov %d5, 3 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
Bus errors\n\
—————-\n\
.balign 32\n
.global Os_bus_trap\n
Os_bus_trap:\n
mov %d4, 18 # Status for Os_UnhandledTrap\n
mov %d5, 4 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
Assertion (Arithmetic overflows)\n\
—————-\n\
.balign 32\n
.global Os_assert_trap\n
Os_assert_trap:\n
mov %d4, 18 # Status for Os_UnhandledTrap\n
mov %d5, 5 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
Syscall\n\
—————-\n\
.balign 32\n
.global Os_syscall_trap\n
Os_syscall_trap:\n
mov %d4, 18 # Status for Os_UnhandledTrap\n
mov %d5, 6 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n\
—————-\n\
NMI\n\
—————-\n\
.balign 32\n
.global Os_nmi_trap\n
Os_nmi_trap:\n
mov %d4, 18 # Status for Os_UnhandledTrap\n
mov %d5, 7 # Class for Os_UnhandledTrap\n
j Os_trap_fault\n
\n
\n
Os_trap_fault:\n
mov %d6, %d15 # TIN\n
mov.d %d7, %a11 # Return\n
mfcr %d15, $PSW\n
or %d15, 0x7f\n
mtcr $PSW, %d15\n
j Os_UnhandledTrap # Unrecoverable [$TargetHT 190] [$TargetHT 193]\n
\n
.size Os_TrapVectorTable, .-Os_TrapVectorTable");
下面是他的TRAP处理函数实现,这里边有我自己添加的一点私货,我们后边再具体说。
static unsigned int BootDataRaw[1024];
// 0xAF03F000
FUNC(void, OS_CODE)
Os_UnhandledTrap(StatusType status, uint32 klass, uint32 tin, uint32 addr)
{
unsigned int i = 0;
unsigned int PCXITemp = 0;
unsigned int PCXITempS = 0;
unsigned int PCXITempO = 0;
const Os_CoreConfiguration *os_current_core_const = &Os_const_coreconfiguration[OS_MFCR(0xfe1c)];
Os_ControlledCoreType *os_current_controlled_core = os_current_core_const->controlled;
Os_TrapInfo.Class = (uint8)klass;
Os_TrapInfo.TIN = (uint8)tin;
Os_TrapInfo.ReturnAddress = addr;
// PCXITemp = Ifx_Ssw_MFCR(CPU0_PCXI);
PCXITemp = OS_MFCR(0xFE00);
PCXITempS = PCXITemp & 0xFFFF;
PCXITempO = (PCXITemp » 16) & 0XF;
PCXITemp = PCXITempO « 28 | PCXITempS « 6;
for (i = 0u; i < 1024; i++)
{
BootDataRaw[i] = *((unsigned int *)(0xAF03F000) + i);
}
if (BootDataRaw[512] == 0x00 && BootDataRaw[513] == 0 && BootDataRaw[514] == 0)
{
BootDataRaw[512] = klass;
BootDataRaw[513] = tin;
BootDataRaw[514] = status;
BootDataRaw[515] = addr;
BootDataRaw[516] = *((unsigned int *)PCXITemp + 3);
Os_DisableAllInterrupts();
FlsLoader_Erase((uint32)0xAF03F000, 1);
FlsLoader_Write((uint32)0xAF03F000, 4096, (const uint8 )BootDataRaw);
Os_DisableAllInterrupts();
}
Os_ProtectionLog(status);
return;
} / UnhandledTrap */
英飞凌的TriCore将异常分为以下8个大类:
- Class 0 — MMU:对于那些包含内存管理单元(MMU)的实现,Class 0处理他们的异常。
- Class 1 — Internal Protection Traps:Class 1用于与内部保护系统相关的异常。它包括内存保护异常,MPR、MPR和 MPX,用于基于范围的保护系统,并且独立Class 0中于基于页面的VAP保护异常。
- Class 2 — Instruction Errors:Class 2用于指示各种类型的指令错误。指令错误包括指令操作码错误、指令操作数编码错误,或者在内存访问操作中操作数地址错误。
- Class 3 — Context Management:Class 3用于由上下文管理子系统检测到的异常条件,在涉及函数调用、中断、异常中断和返回的上下文保存和恢复操作执行(或尝试执行)过程中。
- Class 4 — System Bus and Peripheral Errors:系统总线和外设错误,它包含取值错误和数据访问(同步、异步)错误,以及协处理器操作错误等。
- Class 5— Assertion Traps:当用户配置了PSW.V或者PSW.SV会引发这个异常,这个也是用户代码assert的芯片补充支持。
- Class 6 — System Call:通过SYSCALL指令来触发一个系统调用。
- Class 7 — Non-Maskable Interrupt:引发不可屏蔽中断的原因取决于实现。
通常会有一个外部引脚,可以用来发出NMI信号,但它也可能在响应诸如看门狗定时器中断或即将发生的电源故障时被触发。
具体每个大类又有不同的子类(TIN),本篇文章就不赘述了,有兴趣的读者可以在《TriCore TC1.6.2 core architecture
manual》的Trap System章节进行了解。下面我们还是赶紧举个实例吧,总是说理论真的是非常无趣,哈哈。
从上面的截图可以看出,我们的程序发生了异常中断,并且发生的是Class 4,TIN 2的异常中断,经查可以知道是Data Access Synchronous Error.。
这个时候我们可以通过额外观察其余的寄存器来进一步定位信息,如果是DSE,我们可以观察DSTR和DEADD,前者帮我们细分数据同步访问错误,后者帮我们记录错误访问的地址。如果是DAE则可以观察DATR和DEADD,前者帮我们细分数据异步访问错误,后者帮我们记录错误访问的地址。
上图是当发生DSE的时候,我们观察DSTR的进一步错误分类是LBE(Load Bus Error),再根据DEADD的寄存器的信息0x04判断是想从0x04地址中Load数据发生的问题,实际上也是下面的代码引起的问题,跟问题描述一致。 PduR_PBConfigType TestA; PduR_Base = 0x04; TestA = *PduR_Base; 当发生了PSE异常的时候,我们可以通过观察PSTR获取更详细的错误信息,如下图。
从上图可以看出,为Fetch Bus Error,问题的描述确实与引发问题代码一致,如下: void (*function_ptr)(void); function_ptr = (void )NULL_PTR; / 0x80040000 */ // Change todo (function_ptr)(); 当然,如果能在有调试器连接的情况下,抓到TRAP异常那真的对问题能排查能够有很大的帮助,例如我们可以随意的在“案发现场”来查询核心和外设的寄存器值,调用栈信息等,可以直接锁定出问题的地方,并且在出问题的地方添加一些额外调试手段(如莫名其妙的结构体指针变为空时,可以通过在变量添加写断点来查找到底都有谁在运行过程中修改了这个指针),但是往往TRAP异常在实验室环境下都很难复现,甚至连实车上都是偶然发现的(这是因为ECU的很多处理逻辑跟对手件的报文,硬线信号都有直接的关系) ,所以我们不得不采取在发生TRAP中断之后将信息保存到Flash,待后边查的时候再把对应信息读取出来。前文提到的在Os_UnhandledTrap夹带的“私货”就是这部分功能的实现。 // PCXITemp = Ifx_Ssw_MFCR(CPU0_PCXI); PCXITemp = OS_MFCR(0xFE00); PCXITempS = PCXITemp & 0xFFFF; PCXITempO = (PCXITemp » 16) & 0XF; PCXITemp = PCXITempO « 28 | PCXITempS « 6; for (i = 0u; i < 1024; i++) { BootDataRaw[i] = *((unsigned int *)(0xAF03F000) + i); } if (BootDataRaw[512] == 0x00 && BootDataRaw[513] == 0 && BootDataRaw[514] == 0) { BootDataRaw[512] = klass; BootDataRaw[513] = tin; BootDataRaw[514] = status; BootDataRaw[515] = addr; BootDataRaw[516] = *((unsigned int *)PCXITemp + 3); Os_DisableAllInterrupts(); FlsLoader_Erase((uint32)0xAF03F000, 1); FlsLoader_Write((uint32)0xAF03F000, 4096, (const uint8 *)BootDataRaw); Os_DisableAllInterrupts(); } 这部分代码将错误的分类信息,以及发生错误时的PC指针以及调用栈的上一级PC指针存储到了英飞凌芯片中所谓EEPROM中的尾部,这部分空间原本是boot用,每次刷写的时候都会擦除,所以要记得在刷写新程序前获取这部分信息。第二级的PC指针用到了PCXI中存储的CSA上下文信息,感兴趣的同学可以参照《TriCore TC1.6.2 core architecture manual》中的Tasks and Functions章节。 最后我再补充说明一下TRAP异常,因为这种异常中断处理方式也并非TriCore专有,大部分的芯片核心都会有自己的异常中断机制,这种异常中断的机制不仅仅是让我们知道有这种异常导致了芯片“死机”就完了,而是为了让我们能够通过硬件提供的现场信息进行错误恢复从而再一次保证程序能够正常运行,但初衷是好的,因为恢复难度过大且也不是所有错误都能恢复,目前笔者还没有遇到过TRAP异常中断恢复的实现,有相关经历的读者可以在评论区分享一下。
其他
英飞凌的芯片提供了RSTSTAT,它会记录核心未被清除或上一次的重启原因,例如RSTSTAT.PORST位被置位,则表示上次重启的原因为Power-on
Reset(PORST引脚被拉低),或是RSTSTAT.CB3表示上一次的重启原因为软件触发的Application Reset。
还有当我们启用内部看门口的时候,我们的超时触发Alarm也会通过SMU最后导致芯片复位,我们可以通过SMU_AGi来观察汇报上来的fault
condition。
当然,还有一些软件的错误日志信息,例如ASSERT等,都会为我们查找死机问题提供帮助。
十六宿舍 原创作品,转载必须标注原文链接。
©2023 Yang Li. All rights reserved.
欢迎关注 ,大家喜欢的话,给个****👍****,更多关于嵌入式相关技术的内容持续更新中。