再遇 HardFault

书接上回:STM32F4(CortexM4)+UCOS-III 玄学排查实况。上文中一直留下了一个未完的结尾,其实是一直没有结尾。我们研究了很久,最终也没有研究清楚产生HardFault的原因;最后阴差阳错地,我们决定将HAL库更换为标准外设库(当时芯片是STM32F4),再看看会不会有这种情况。后面重新在标准外设库下移植了我们的程序和uCOS,居然奇迹般地就好了。后面也没有敢再来研究这个问题。

好巧不巧,今年(2022年)的某个项目,我们又一次上了uCOS III,也又一次地遇到了HardFault。希望这一次,可以与这个问题斗争到底,将他彻底地拿下。一方面,我们使用的是STM32G0系列,是一个较晚推出的新系列产品,不再提供标准外设库了,另一方面,这次我也没有其他琐事的骚扰,可以安心排查了。这一次,也有一个优势,可以100%复现了,并且是可控环境下的复现,更加方便调试了。

经单步调试的排查,确定这次的HardFault是在创建任务后,OS从StartTask切换到NewTask的过程中,在某个CPU_CRITICAL_EXIT();后发生的。凭借着上次的经验,直接在PendSV_Handler下断点,可以验证的确在退出临界区后,触发了PendSV。这一切似乎与上次隐隐约约有一定的关系啊。PendSV中发生异常,多半是上下文切换后,从PendSV回到Task时,无法跳转到正确的内存导致的。

CortexM系列处理器拥有LazyStacking特性,一部分上下文是由CPU直接保存的。这部分数据应该也是保存在PSP(Process SP)中的(待验证)。另一部分由OS完成保存。那么我们在PendSV中停下,看看NewTask的堆栈信息到底是怎样的。

Snipaste_2022-03-03_21-17-38.png

可以看到现在PendSV已经保存完毕了StartTask的上下文信息了。

由于我们已经提前知道异常引发的时机了,若PendSV运行完毕必定引发异常,为了方便调试,我们在此处暂停程序的运行,静态查看NewTask内存数据即可。

Snipaste_2022-03-03_21-31-11.png

栈底是0x20001400,我们给了256个CPU_STK(4 Byte)的栈空间,所以栈顶是0x20001800. 在Keil中,我们直接查看0x20001800附近的内存。

Snipaste_2022-03-03_21-31-54.png

有了内存,我们还需要知道内存数据是怎么组织的。因为我们此处现场不复杂,NewTask刚刚创建,还没有运行,因此我们可以直接参考创建任务的函数实现,因为创建任务的时候必定要初始化堆栈,否则无法切换到NewTask。这里因为篇幅限制,将相关函数精简了,只保留大意。

void  OSTaskCreate ( ... )
{

OS_TaskInitTCB(p_tcb); /* Initialize the TCB to default values */
/* -------------- CLEAR THE TASK'S STACK -------------- */
if (((opt & OS_OPT_TASK_STK_CHK) != 0u) || /* See if stack checking has been enabled */
((opt & OS_OPT_TASK_STK_CLR) != 0u)) { /* See if stack needs to be cleared */
if ((opt & OS_OPT_TASK_STK_CLR) != 0u) {
p_sp = p_stk_base;
for (i = 0u; i < stk_size; i++) { /* Stack grows from HIGH to LOW memory */
*p_sp = 0u; /* Clear from bottom of stack and up! */
p_sp++;
}
}
}

p_sp = OSTaskStkInit(p_task,
p_arg,
p_stk_base,
p_stk_limit,
stk_size,
opt);

p_tcb->Prio = prio; /* Save the task's priority */

p_tcb->StkPtr = p_sp; /* Save the new top-of-stack pointer */
p_tcb->StkLimitPtr = p_stk_limit; /* Save the stack limit pointer */
p_tcb->ExtPtr = p_ext; /* Save pointer to TCB extension */
p_tcb->Opt = opt; /* Save task options */


OSTaskCreateHook(p_tcb); /* Call user defined hook */

OS_TRACE_TASK_CREATE(p_tcb);
OS_TRACE_TASK_SEM_CREATE(p_tcb, p_name);
/* -------------- ADD TASK TO READY LIST -------------- */
CPU_CRITICAL_ENTER();
OS_PrioInsert(p_tcb->Prio);
OS_RdyListInsertTail(p_tcb);

OSTaskQty++; /* Increment the #tasks counter */

if (OSRunning != OS_STATE_OS_RUNNING) { /* Return if multitasking has not started */
CPU_CRITICAL_EXIT();
return;
}

CPU_CRITICAL_EXIT();

OSSched();
}

CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,
void *p_arg,
CPU_STK *p_stk_base,
CPU_STK *p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
{
CPU_STK *p_stk;


(void)opt; /* 'opt' is not used, prevent warning */

p_stk = &p_stk_base[stk_size]; /* Load stack pointer */
/* Align the stack to 8-bytes. */
p_stk = (CPU_STK *)((CPU_STK)(p_stk) & 0xFFFFFFF8u);
/* Registers stacked as if auto-saved on exception */
*(--p_stk) = (CPU_STK)0x01000000u; /* xPSR */
*(--p_stk) = (CPU_STK)p_task; /* Entry Point */
*(--p_stk) = (CPU_STK)OS_TaskReturn; /* R14 (LR) */
*(--p_stk) = (CPU_STK)0x12121212u; /* R12 */
*(--p_stk) = (CPU_STK)0x03030303u; /* R3 */
*(--p_stk) = (CPU_STK)0x02020202u; /* R2 */
*(--p_stk) = (CPU_STK)p_stk_limit; /* R1 */
*(--p_stk) = (CPU_STK)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(--p_stk) = (CPU_STK)0xFFFFFFFDuL; /* R14: EXEC_RETURN; See Note 4 */
*(--p_stk) = (CPU_STK)0x11111111uL; /* R11 */
*(--p_stk) = (CPU_STK)0x10101010uL; /* R10 */
*(--p_stk) = (CPU_STK)0x09090909uL; /* R9 */
*(--p_stk) = (CPU_STK)0x08080808uL; /* R8 */
*(--p_stk) = (CPU_STK)0x07070707uL; /* R7 */
*(--p_stk) = (CPU_STK)0x06060606uL; /* R6 */
*(--p_stk) = (CPU_STK)0x05050505uL; /* R5 */
*(--p_stk) = (CPU_STK)0x04040404uL; /* R4 */

return (p_stk);
}

这里面我们最感兴趣的便是OSTaskStkInit函数了。该函数实现也一并附上了。

可以看出,从栈顶依次往下,应该是xPSR、PC、LR、R12、R3、R2等(详细见代码),同时初始化时还给了特殊的初值,方便了我们寻找。结合着我们之前从内存中保存的数据,可以发现一些细节:1. 栈顶有一个0xB的数据在,我们的xPSR寄存器并没有保存在最顶上,而是往后挪了一个位置。2. PC的位置居然是0x20001400,这是一个指向RAM的地址。很明显,要么是PC被错误的写入了,要么是PC被修改了。

在跟踪任务堆栈如何被创建之前,我们可以让PendSV继续运行一小段,以确保其他寄存器都正确的保存了。

Snipaste_2022-03-03_21-52-12.png

可以看到,R4-R12等由OS手动还原的寄存器都已经正确设置了。

接下来,我们就可以只关心0x200017F8这一个地址了,也就是堆栈中保存PC的地址,观察他是如何创建的。

Snipaste_2022-03-03_21-57-56.png

可以看到,我们的初始化函数拿到的就是错误的地址。那么错误的地址是谁给的呢?

在上级函数OSTaskCreate中,地址的变量名是”p_task”。这个变量来自我们给的函数参数。

Snipaste_2022-03-04_11-28-10.png

查看我们给OSTaskCreate传入的参数。……,居然是个笔误,给他传了个任务堆栈的地址。修改为任务的函数指针,问题解决。

没想到,这次是个这么低级的错误导致的。