再遇 HardFault

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

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

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

CortexM系列处理器拥有LazyStacking特性一部分上下文是由CPU直接保存的这部分数据应该也是保存在PSPProcess 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我们给了256CPU_STK4 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函数了该函数实现也一并附上了

可以看出从栈顶依次往下应该是xPSRPCLRR12R3R2详细见代码同时初始化时还给了特殊的初值方便了我们寻找结合着我们之前从内存中保存的数据可以发现一些细节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传入的参数……居然是个笔误给他传了个任务堆栈的地址修改为任务的函数指针问题解决

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