STM32F4(CortexM4)+UCOS-III 玄学排查实况

寒假期间在家调试RM工程车的底盘期间一直发生各种玄学故障感觉这单片机与我有仇在我手里总是出现玄学情况特采用记叙的方式将过程思路记录下来

不过前面有一部分故障的排查是有计划记录下这些过程之前进行的我会尽量还原当时的思路但是更多的可能还只能起来一个记录故障的作用

背景

硬件平台正点原子STM32F4探索者板

外设

  • UART1PC通讯1152008N1无流控发送LOG
  • UART6接大疆的D-BUS遥控接收机1000009E1无流控UART电平反相
  • CAN总线接4个电机数字控制转矩电流
  • TIMER 1000hz定时器PID提供时钟

使用STM32CubeMX生成Keil工程均为最新版本编译器使用的ARM Compiler 6使用UCOS-III作为RTOS

20210130

故障1DBUS串口行为异常

此时还是裸机还没移植UCOS

MCU刚复位的时候可以进入接收中断几次然后就再也不会进入中断了但是示波器测了也确实有串口的数据波形

1.png

1.1.png

0 0 0 0 1024是遥控几个通道的值while每隔一段时间打印如果摇杆扭动对应的值是会变化的

为了Debug在收到数据后在中断中发送Recv标志如图可见在物理按下Reset后会刷出2-3Recv然后就再没消息了


对照STM32F4参考手册查询了几个关键寄存器的地址RXNE=1说明有数据等待接收RXNEIE却又一直是0但是代码中确实有开启接收

2.png

3.png

4.png

CubeMX也是正常设置了中断的这里是USART2是因为截图时为了避免是串口问题6换到了2但结果是一样的

求助了一波学长学长指导我先后检查了几个地方换个串口波特率校验位那些对应对了吗那个rxbuffer开小一点先用1一个字节一个字节接是否是因为程序卡死轮训接收试试

不过无济于事现象依旧一样中间学长还确认了波特率等信息但是也被我忽略了

MR 2021/1/30 21:05:10
一样

MR 2021/1/30 21:05:13
检查贵了

MR 2021/1/30 21:05:15
过了

MR 2021/1/30 21:05:23
复位后接收到的数据也是对的
Lee 2021/1/30 21:05:30
那个rxbuffer开小一点先用1一个字节一个字节接

MR 2021/1/30 21:06:08
好 我试试

MR 2021/1/30 21:09:35
不行

MR 2021/1/30 21:10:09
之前是18个收一次 然后复位后能收3个

MR 2021/1/30 21:10:48
现在是复位后能连收一长串的1字节 

MR 2021/1/30 21:10:52
但是总数基本不变

MR 2021/1/30 21:11:13
收完差不多长度的之后 就又不收了

不过轮询能一直收

5.png

Lee 2021/1/30 21:25:57
你接收的那些数据是对的吗

Lee 2021/1/30 21:26:22
发送端先用电脑

MR 2021/1/30 21:27:10
绝大部分都对 有一个开关不对 不知道为什么

Lee 2021/1/30 21:27:28
你先用电脑发送试试

MR 2021/1/30 21:27:31
好

Lee 2021/1/30 21:30:10
还有把其他外设代码注释掉

关键部分在学长的提醒下我把其他的外设也就是CAN和定时器全部注释了最终程序正常触发中断接收

然后调整定时器的中断优先级后最终所有外设都能正常开启

总结在遇到玄学问题时尽量精简故障场景方便定位故障点有时候看起来毫无关系的部分可能就是关键突破点

但是我依然困惑定时器1Khz并不算太快特别是对于F4168Mhz主频来说中断内部也没有太占时间的工作这影响会有那么大吗

20210201

故障1-续DBUS串口行为异常

虽然现在串口能正常接收了小车的轮子依照遥控器的油门已经欢快地转了起来但这美好的表面下还是有着隐隐的躁动

  1. A通道的值不符合定义B通道数值异常

接收到的A通道值可以超过定义的阈值并且变化不连续B通道对应的是一个三段式开关应该有三个状态但是始终只能接受到两个状态此外A通道与B通道在二进制上是相邻的

但是在示波器上可以看到B通道在三个状态之间切换时确实信号是有三种不同的对应状态的

6.png

故障通道就是通道3S1

  1. 此外这个串口还死活没法开启DMA

虽然说通道变化时在示波器上确实看到波形变化了但是因为其他的通道都正常我还是怀疑是接收机本身的故障发送了错误的数据但是经过了一下午的查询也没有发现有相似情况发生考虑到两个通道是同一个字节分割开的我还检查了N遍解析代码是不是解析错误同时与剩下的2-3套参考代码进行对比但也没发现什么问题没办法这个问题只能先放在这里了想着看能不能等回学校用示波器解析一下串口波形

第二个问题相对来说查起来更有思路DMA与串口必然会有相关的寄存器保存状态我们只要查DMA开启后是不是真的开启了开启后又是有没有真的收到过数据即可

经过一番排查后发现如下现象

DMA在开启后第一次进入的DMA中断直接就进入了DMA的错误处理

//位于HALstm32f4xx_hal_dma.c文件中

void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
{
  ...

  /* Transfer Error Interrupt management ***************************************/
  if ((tmpisr & (DMA_FLAG_TEIF0_4 << hdma->StreamIndex)) != RESET)
  {
    if(__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_TE) != RESET)
    {
      /* Disable the transfer error interrupt */
      hdma->Instance->CR  &= ~(DMA_IT_TE);

      /* Clear the transfer error flag */
      regs->IFCR = DMA_FLAG_TEIF0_4 << hdma->StreamIndex;

      /* Update error code */
      hdma->ErrorCode |= HAL_DMA_ERROR_TE;
    }
  }

    ...
}

DMA发生了传输错误那么传输错误的原因呢最终发现居然是串口ORE了导致的DMA错误也就是串口溢出了这里我百思不得其解我都用DMA理论上不是有数据你就自动给我从外设寄存器搬运到我的内存吗这一切理论上是全自动的除非是你DMA自己搬运慢了才会ORE

这里在网上搜到了一种说法说是启动串口后在接收之前中间的时间差中收到了一些数据这些数据没有人取走造成的ORE

这个说法乍一听很有道理于是我也对代码做了一些调整使接收开始的位置与串口初始化的位置尽量靠近但是还是无济于事查看HAL库的代码后发现只要使用了HAL其实这种说法本身就是站不住脚的因为HAL库已经考虑到这个情况了在开启DMA接收时已经清除了ORE标识

//位于HALstm32f4xx_hal_uart.c文件中

HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
    ...

    /* Enable the DMA stream */
    tmp = (uint32_t *)&pData;
    HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);

    /* Clear the Overrun flag just before enabling the DMA Rx request: can be mandatory for the second transfer */
    __HAL_UART_CLEAR_OREFLAG(huart);

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the UART Parity Error Interrupt */
    SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);

    /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
    SET_BIT(huart->Instance->CR3, USART_CR3_EIE);

    /* Enable the DMA transfer for the receiver request by setting the DMAR bit
    in the UART CR3 register */
    SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

于是我决定不使用DMA回到之前的中断方式看看会不会还有ORE的情况发生

BTWHAL库确实是方便HAL_UART_Receive_DMA函数把DMA改成IT即可变为中断接收其他什么都不用动

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart == (&huart6))
    {
        if(huart->ErrorCode | HAL_UART_ERROR_ORE)
        {
            errorCount++;
        }
    }
}

修改后发现使用中断方式时虽然能正常收数据了但其实还是有ORE错误的

那么这就奇怪了100k的波特率再加上14ms的发送间隔对于STM32F4实在不是个什么大事但在中断中即使什么都不做收到数据后就立即开启下一次中断然后就返回也会产生ORE

也怀疑过是不是串口参数的问题但是与三份代码与官方文档对比后也没发现错误最主要是数据也不是全错

7.png

有一份代码是大疆某个官方车的开源代码有一份是遥控自带的示例代码还有一份是其他大学的开源代码配置无一例外都是100k-8E1

开始百思不得其解开始怀疑人生开始自闭开始怀疑大疆的遥控开始怀疑HAL……最主要是在家又没别的仪器只能靠猜如果在学校兴许用示波器分析一下协议就知道问题了

8.png

从跟朋友的聊天记录找到一个图全是搜索跟这相关的话题……这段时间浏览器标签页数量一直在200以上……Firefox牛逼

可能是老天看不下去了经过了一整天从起床开始一直查资料查到凌晨一两点然后睡觉第二天又继续查询终于在外网StackOverflow发现一个老外有近乎一样的环境与问题他用的是SBUS其实DBUS就是大疆拿SBUS搞得变种

https://community.st.com/s/question/0D50X00009XkeWfSAJ/stm32f3-uart-dma-problem

他提到是数据长度的问题于是我尝试性的也将8b改成9b问题解决不再OREDMA也能正常接收

额外收获的是通道数据异常的问题也解决了道理其实很简单每个Byte之前会丢1bit数据自然会出错

9.png

其实CubeMX也说清楚了WordLength IncludeParity也就是包括了奇偶校验位的1bit长度

总结……我只关心那些个开源的代码是真的能用的吗

20210202

故障2UCOS-III OS无法启动

新的一天继续踩坑~

串口自从改成9bit长度后就几乎一切正常了我怀疑第一个故障是不是也与这个有关

外设在裸机条件下基本稳定后开始移植操作系统这里因为一些原因我选择了UCOS

参考了正点原子的STM32F4 UCOS开发手册_V3.0.pdf与网上一个基于HAL库的移植教程后我下载了micrium的官方基于MDK-KeilF4移植然后导入了我的工程

10.png

跑马灯任务写好也正常编译了烧录进去没法工作

其实也是预期之中的不出点岔子才奇怪了

在调试状态下设置了一些断点发现是OSStart()后就跑飞执行OSStart()后就没有进过StartTaskOSStart也没有返回过了于是跟着OSStart一直单步发现程序其实是没有跑飞是一直在执行代码的只不过是卡在某个地方出不去了而已这个卡的地方还不是某个死循环之类的而是在好几个函数与汇编代码之间不停循环

最后得到如下信息

  1. 发现PendSV异常没被触发过但是Systick正常触发说明程序应该没有正常调度

  2. 程序一开始OSStart后是成功初始化了的也成功进入最高优先级的内部任务TickTask并且发现是Systick正确地利用信号量唤醒了TickTask

  3. 发现程序一直在优先级最高的TickTask切换不出去每次进入调度器后就退出了这里就是不合理的了理论上信号量Pend后应该要释放CPU进行调度调度就应该要到我们的跑马灯程序了但是调度程序因为什么原因并没有调度而是退出了

  4. 继续跟进调度程序然后发现不执行调度的原因是 中断嵌套计数OSIntNestingCtr>0说明UCOS认为这个时候在中断里面所以拒绝执行调度但是这个时候用Keil的调试工具看是没有任何Active的中断的终于学会使用Keil查看寄存器了

  5. 最后查OSIntNestingCtr为啥会>0直接给OSIntNestingCtrWatchAccess断点发现理论上时在进入中断时计数就会++退出时–从而计算有多少层中断嵌套接着查发现原因是在OSInit之前Systick中断就触发了systick时那个计数不判断直接++了退出时却因为有判断发现OS还没跑起来就没有–直接退出了

11.png

图为UCOS拒绝调度的位置

12.png
13.png

图为UCOSSysTick对于OSIntNestingCtr的处理不平衡

所以说要么不让OSStart之前触发Systick要么Systick的回调就要多进行一个判断

我认为这是一个很明显的BUG但是官方移植正点原子……等等全部都按这个搞的为啥别人能正常调度

14.png
15.png

带着疑问又打扰了一波学长学长查了最新的UCOS-III源码发现官方在最新版本进行了改动中断管理统一使用OSIntEnterOSIntExit里面进行了判断

在移植最新版UCOS-III发现最新版本变化还挺大不但变更了授权为Apache在架构上也有大变化

移植的文件也有变化这里建议参考 http://www.armbbs.cn/forum.php?mod=viewthread&tid=96918 的移植

总结要用就用最新版过时教程害死人

20210205

这个故障从25号一直搞到今天20实在是复杂以及难搞感觉比之前的都要恶心唯一的好是100%能复现但是排查的过程中确实学到了很多很多东西虽然写下这个文字时依然还没解决所以也是今天才萌生要记录下来这一系列坑的想法一是备忘二是看能不能帮到同样遭遇的有缘人三是写一遍也能帮我理一下说不定哪个盲点就被我漏掉了毕竟我现在真没任何思路了😂

故障3随机HardFault

UCOS移植成功后就一直在撸码想着移植点LogShell啊什么的高级东西上去然后把软件工程那套应用应用弄出个比较好点的工程结构之前的裸机代码全写在同一个main.c里了一个源码1k多行搞着搞着想着接上去测一下嘿还转的挺好……欸等等怎么不转了灯也不闪了又跑飞了

进调试状态搞一搞又复现了发现是HardFault

这里要先插播介绍一下软件的架构

16.png

main()创建StartTask再在StartTask中创建剩下三个真正的工作任务后StartTask结束自己

ChassisTask只由定时器唤醒计算PID并输出给电机后PendRCTask只由遥控的DBUS DMA完毕的中断唤醒负责解析DMA收到的字节为遥控信息另外一个LEDTask除了跑马灯还要打印调试LOG之后自己Delay500ms等待唤醒所有的ISRPost信号量来唤醒Task

此外经过一番定位发现错误与ChassisTask底盘任务无关只与RCTask有关注释ChassisTask故障依然存在再注释RCTask故障不存在单跑马灯连续运行1个小时都正常为了避免是任务调度本身的问题还特意测试了设置两个不同的LEDTask两个Task按照不同的频率跑马两个不同的LED结果也是故障不存在

此外还发现只要遥控器不开机此时接收机就不会产生波形到单片机上就不会产生中断RCTaskSemaphore就会一直Pend这时也不会产生Fault但一旦开启接收机一定随机的时间后就一定会产生Fault

RCTask相关代码如下还包括了相关ISR

void DMA2_Stream1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA2_Stream1_IRQn 0 */
#ifdef APP_UCOS_EN
    OSIntEnter();
#endif
  /* USER CODE END DMA2_Stream1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_usart6_rx);
  /* USER CODE BEGIN DMA2_Stream1_IRQn 1 */
#ifdef APP_UCOS_EN
    OSIntExit();
#endif
  /* USER CODE END DMA2_Stream1_IRQn 1 */
}

void USART6_IRQHandler(void)
{
  /* USER CODE BEGIN USART6_IRQn 0 */
#ifdef APP_UCOS_EN
    OSIntEnter();
    //CPU_SR_ALLOC();
    //CPU_CRITICAL_ENTER();
#endif
  /* USER CODE END USART6_IRQn 0 */
  HAL_UART_IRQHandler(&huart6);
  /* USER CODE BEGIN USART6_IRQn 1 */
#ifdef APP_UCOS_EN
    //CPU_CRITICAL_EXIT();
    OSIntExit();
#endif
  /* USER CODE END USART6_IRQn 1 */
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart == (&RC_HUART))
  {
#ifdef APP_UCOS_EN    
    OS_ERR err;
    //CPU_SR_ALLOC();
    //CPU_CRITICAL_ENTER();
#endif
    // memcpy(RC_Origin_Buffer, RC_UART_Buffer, 18);
#ifdef APP_UCOS_EN      
    //CPU_CRITICAL_EXIT();
    OSTaskSemPost(&RCTaskTCB, OS_OPT_POST_1, &err);
#else
     HAL_UART_Receive_DMA(&RC_HUART, RC_UART_Buffer, 18);
#endif

  }
}

void rc_task(void *p_arg)
{
#ifdef APP_UCOS_EN
    OS_ERR err;
    p_arg = p_arg;

    while (1)
    {
        OSTaskSemPend(0, OS_OPT_PEND_BLOCKING, NULL, &err); // Waiting for RC UART Interrupt
#endif
        RC_CtrlData.ch1 = (RC_Origin_Buffer[0] | RC_Origin_Buffer[1] << 8) & 0x07FF;
        RC_CtrlData.ch1 -= 1024;
        RC_CtrlData.ch2 = (RC_Origin_Buffer[1] >> 3 | RC_Origin_Buffer[2] << 5) & 0x07FF;
        RC_CtrlData.ch2 -= 1024;
        RC_CtrlData.ch3 = (RC_Origin_Buffer[2] >> 6 | RC_Origin_Buffer[3] << 2 | RC_Origin_Buffer[4] << 10) & 0x07FF;
        RC_CtrlData.ch3 -= 1024;
        RC_CtrlData.ch4 = (RC_Origin_Buffer[4] >> 1 | RC_Origin_Buffer[5] << 7) & 0x07FF;
        RC_CtrlData.ch4 -= 1024;

        /* prevent remote control zero deviation */
        if (RC_CtrlData.ch1 <= 5 && RC_CtrlData.ch1 >= -5)
        {
            RC_CtrlData.ch1 = 0;
        }
        if (RC_CtrlData.ch2 <= 5 && RC_CtrlData.ch2 >= -5)
        {
            RC_CtrlData.ch2 = 0;
        }
        if (RC_CtrlData.ch3 <= 5 && RC_CtrlData.ch3 >= -5)
        {
            RC_CtrlData.ch3 = 0;
        }
        if (RC_CtrlData.ch4 <= 5 && RC_CtrlData.ch4 >= -5)
        {
            RC_CtrlData.ch4 = 0;
        }

        RC_CtrlData.sw1 = ((RC_Origin_Buffer[5] >> 4) & 0x000C) >> 2;
        RC_CtrlData.sw2 = (RC_Origin_Buffer[5] >> 4) & 0x0003;

        if ((abs(RC_CtrlData.ch1) > 660) || \
                (abs(RC_CtrlData.ch2) > 660) || \
                (abs(RC_CtrlData.ch3) > 660) || \
                (abs(RC_CtrlData.ch4) > 660))
        {
            memset(&RC_CtrlData, 0, sizeof(struct RC_Ctl_t));
#ifdef APP_UCOS_EN
            continue;
#else
            return;
#endif
        }

        RC_CtrlData.x = RC_Origin_Buffer[6] | (RC_Origin_Buffer[7] << 8); // x axis
        RC_CtrlData.y = RC_Origin_Buffer[8] | (RC_Origin_Buffer[9] << 8);
        RC_CtrlData.z = RC_Origin_Buffer[10] | (RC_Origin_Buffer[11] << 8);

        RC_CtrlData.press_l = RC_Origin_Buffer[12];
        RC_CtrlData.press_r = RC_Origin_Buffer[13];

        RC_CtrlData.kb.key_code = RC_Origin_Buffer[14] | RC_Origin_Buffer[15] << 8; // keyboard code
        RC_CtrlData.wheel = (RC_Origin_Buffer[16] | RC_Origin_Buffer[17] << 8) - 1024;
#ifdef APP_UCOS_EN
    }
#endif
}

回到故障经过一番查询HardFault是有原因可查的具体资料可见 https://hhuysqt.github.io/hardfault/

17.png

18.png

经过一段时间的观察HardFault每次都是FORCED置位代表HardFault是上访来的上访的原因通常有三种一个是MemoryManageFaultIACCVIOL一个是BusFaultPRECISERR一个是UsageFaultINVSTATE图中这次复现是PRECISERR好家伙三种fault都尝了个遍此外每次发生HardFault的时机是随机的并且通过NVIC可以看到PendSVActive是置位的也就是说应该是在PendSV异常里面发生的错误

那么IACCVIOLPRECISERRINVSTATE到底是个啥意思呢这个可以通过查阅ARMCortex™-M4 Devices Generic User Guide文档得到可以在网上下载到公开的PDF

19.png

查阅得到三个异常的原因都是PC值有问题PC寄存器指向了不该指向的位置在这里开始查资料关键词就可以以CortexM为主了因为这是ARM内核层面的异常已经不仅仅是STM32层面了PC也就是Program CounterR15寄存器类似于X86中的IP(EIPRIP)用于指示下一条指令的位置

接下来我们就要查PC到底是指向了哪里HardFault_Handler的第一条语句打个断点然后引其复现

20.png

查看几个关键寄存器的状态PC=0x08002430LR=0xFFFFFFE1SP=0x20001228PC指向的当然是断点的位置这个没的说的LR的值也就是EXC_RETURN的值拿去之前用到过的Cortex™-M4 Devices Generic User Guide查询这个LR值表明发生错误的位置使用了FPUHandler mode使用了MSP指针

21.png

根据UCOS的任务模型这更加坚定了我们之前的判断发生错误的位置应该是PendSV中断PendSV中断处理上下文切换在上下文切换时恢复了一个错误的PC值导致了Fault这一切就说得通了

只可惜此时的Call Stack已经被破坏已经无法验证发生异常的位置到底是在哪里了

接下来可以想办法还原事故现场由于Cortex-M具有Lazy Stacking特性产生Fault内核应该会自动保存事故现场

22.png

查询ARM的文档dai0298 - Cortex-M4(F) Lazy Stacking and Context Switching - Application Note 298可以得知不管使用了FPU没有一定会保存R0-R3, R12, LR, PC, xPSR寄存器的值

根据之前得到的信息我们在左侧寄存器中拿到MSP寄存器的地址0x20001228并使用KeilMemory窗口进行查询发现前面5个值确实对应此时的R0-R3, R12LRPC确实是非法的值代码段映射在0x08000000不可能变成这样的地址

23.png

在此图还可以看到MSP堆栈中有许多的FFFFFFED的值这个值非常像一个EXC_RETURN的值如果是查询文档得到该值表示想返回的是使用PSP堆栈的线程模式在我们的模型中也就是返回Task由于我们之前推断出是PendSV中发生的错误那么该值表明很可能是PendSV切换完上下文想恢复Task的运行————这一切都符合PendSV的逻辑再次增强了前面推断的说服力

当然也可能是其他中断留下的又突然发现这好像是有点问题的呀如果是EXC_RETURN为啥留下了这么多呢堆栈按理说push多少就要pop多少呀这留在这里岂不是不平衡了这玩意不应该留的吧但是也暂时没法深究

PendSV为啥会发生异常呢搜到一个类似的情况 https://blog.csdn.net/_xiao/article/details/78475195但是好像我的情况要更复杂一些我们保存的PCLR已经被破坏了按照这篇文章我检查了PendSV优先级为240也是正确的

接下来我看到左边PSP的值是0x20003934既然任务的堆栈是PSP那么根据PendSV的流程这个地址应该是切换到的接下来要执行的任务的SP理论上我们可以根据这个查得到要恢复的任务是哪个

24.png

可惜每个任务的SP好像都跟他不搭边这地址到底是啥我也没头绪ChassisTask此时已经被注释StartTask已经自行Suspend所以只有这俩有影响

再看看 PendSV到底做了一些什么该程序来自uCOS-III源码中Cortex-M ARMv7-M的官方移植

;********************************************************************************************************
;                                       HANDLE PendSV EXCEPTION
;                                   void OS_CPU_PendSVHandler(void)
;
; Note(s) : 1) PendSV is used to cause a context switch.  This is a recommended method for performing
;              context switches with Cortex-M.  This is because the Cortex-M auto-saves half of the
;              processor context on any exception, and restores same on return from exception.  So only
;              saving of R4-R11 & R14 is required and fixing up the stack pointers. Using the PendSV exception
;              this way means that context saving and restoring is identical whether it is initiated from
;              a thread or occurs due to an interrupt or exception.
;
;           2) Pseudo-code is:
;              a) Get the process SP
;              b) Save remaining regs r4-r11 & r14 on process stack;
;              c) Save the process SP in its TCB, OSTCBCurPtr->OSTCBStkPtr = SP;
;              d) Call OSTaskSwHook();
;              e) Get current high priority, OSPrioCur = OSPrioHighRdy;
;              f) Get current ready thread TCB, OSTCBCurPtr = OSTCBHighRdyPtr;
;              g) Get new process SP from TCB, SP = OSTCBHighRdyPtr->OSTCBStkPtr;
;              h) Restore R4-R11 and R14 from new process stack;
;              i) Perform exception return which will restore remaining context.
;
;           3) On entry into PendSV handler:
;              a) The following have been saved on the process stack (by processor):
;                 xPSR, PC, LR, R12, R0-R3
;              b) Processor mode is switched to Handler mode (from Thread mode)
;              c) Stack is Main stack (switched from Process stack)
;              d) OSTCBCurPtr      points to the OS_TCB of the task to suspend
;                 OSTCBHighRdyPtr  points to the OS_TCB of the task to resume
;
;           4) Since PendSV is set to lowest priority in the system (by OSStartHighRdy() above), we
;              know that it will only be run when no other exception or interrupt is active, and
;              therefore safe to assume that context being switched out was using the process stack (PSP).
;
;           5) Increasing priority using a write to BASEPRI does not take effect immediately.
;              (a) IMPLICATION  This erratum means that the instruction after an MSR to boost BASEPRI
;                  might incorrectly be preempted by an insufficient high priority exception.
;
;              (b) WORKAROUND  The MSR to boost BASEPRI can be replaced by the following code sequence:
;
;                  CPSID i
;                  MSR to BASEPRI
;                  DSB
;                  ISB
;                  CPSIE i
;********************************************************************************************************

OS_CPU_PendSVHandler
    CPSID   I                                                   ; Cortex-M7 errata notice. See Note #5
    MOV32   R2, OS_KA_BASEPRI_Boundary                          ; Set BASEPRI priority level required for exception preemption
    LDR     R1, [R2]
    MSR     BASEPRI, R1
    DSB
    ISB
    CPSIE   I

    MRS     R0, PSP                                             ; PSP is process stack pointer
    STMFD   R0!, {R4-R11, R14}                                  ; Save remaining regs r4-11, R14 on process stack

    MOV32   R5, OSTCBCurPtr                                     ; OSTCBCurPtr->StkPtr = SP;
    LDR     R1, [R5]
    STR     R0, [R1]                                            ; R0 is SP of process being switched out

                                                                ; At this point, entire context of process has been saved
    MOV     R4, LR                                              ; Save LR exc_return value
    BL      OSTaskSwHook                                        ; Call OSTaskSwHook() for FPU Push & Pop

    MOV32   R0, OSPrioCur                                       ; OSPrioCur   = OSPrioHighRdy;
    MOV32   R1, OSPrioHighRdy
    LDRB    R2, [R1]
    STRB    R2, [R0]

    MOV32   R1, OSTCBHighRdyPtr                                 ; OSTCBCurPtr = OSTCBHighRdyPtr;
    LDR     R2, [R1]
    STR     R2, [R5]

    ORR     LR,  R4, #0x04                                      ; Ensure exception return uses process stack
    LDR     R0,  [R2]                                           ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;
    LDMFD   R0!, {R4-R11, R14}                                  ; Restore r4-11, R14 from new process stack
    MSR     PSP, R0                                             ; Load PSP with new process SP

    MOV32   R2, #0                                              ; Restore BASEPRI priority level to 0
    MSR     BASEPRI, R2
    BX      LR                                                  ; Exception return will restore remaining context

    ALIGN                                                       ; Removes warning[A1581W]: added <no_padbytes> of padding at <address>

    END

可以看出PendSV中并没有直接修改PCLR的值而是通过修改SP然后通过exception return利用lazy stackingCPU自动从SP中读到接下来任务的的PCLR来达到修改PC的目的

也就是说Fault应该发生在PendSV最后的BX LR的时候CPU发现PSP中保存的PC是错误的然后才引发Fault

那么理论上此时的PSP应该确实是要恢复的任务的StkPtr

查看任务结构体os_tcb的定义StkPtr是结构体第一个成员所以tcb的指针也可以说是指向了该任务的StkPtr成员所以先MOV32 R1, OSTCBHighRdyPtr拿到OSTCBHighRdyPtr的地址然后LDR R2, [R1]拿到OSTCBHighRdyPtr的值也就是tcb的地址存到R2OSTCBHighRdyPtr是个指针指向tcb所以OSTCBHighRdyPtr的值就是tcb的地址然后LDR R0, [R2]tcb指针指向的值也就是第一个成员的值存到R0第一个成员是该任务的StackPtr指针所以R0存的也就是接下来任务的SP地址最后MSR PSP, R0PSP指向接下来任务的SP

所以这个PSP理论上也是从接下来那个任务的TCB里面load出来的但却没法在我们已知的tcb里面找到属实离谱我也搞不懂发生了什么等下是不是ucos的内置任务

20210221

写了一天已经12点半了跨个日吧

接下来思路1.查一下ucos的内置任务 2.刚刚突然发现LEDTask其实又爆栈了往上翻有张TCB情况的截图这个等下说随机爆栈这个事其实我10号的时候就知道了但是一直没理他

查了一下手册UCOS-III内部任务有IdleTaskTickTask还可能有StatTaskTmrTaskIntQTask几个看你开没开可选功能的任务分别分布在os_core.cos_tick.cos_stat.cos_tmr.cos_int.c几个文件里

实际情况是IntQTask没找到os_int.c都不见了TickTask新版应该是取消了好像是整合进SysTickISR手册可能没更新

25.png

可以确认与内部任务也没有关系那是真不知道PSP那个20003934是哪来的了我们也没法进入PendSV去弄清楚发生了什么因为Fault发生是完全随机的有时要十几分钟有时Reset1秒就Fault所以也没法下断点可能点个几千次Continue都等不到复现最气的是真等到了还tm手欠继续按了个Continue……

26.png

强行看一眼PSP的内存也没啥线索还特意往前挪了点字节PSP指向的可一点不像个堆栈周围一大片00000000我甚至怀疑PSP恢复了个错误的值也就是说写入PSP的地址并不是任务的SP当然也有可能是堆栈溢出导致的

27.png

机智的我又突然想起我们不是为了找接下来是啥任务吗我们直接按着汇编找OSTCBCurPtr不就好了吗查出来0x20003910也确实是个SRAM的地址结果再拿地址去查就懵逼了对着前面的已知TCB包括我创建的和UCOS创建的的截图也没找到这个地址的TCB不知道这个指针怎么来的莫名其妙……

这条路是有点走不通了说说堆栈溢出的事吧溢出那边如果没啥线索就只能反向的从RCTask找原因了

首先说说UCOS怎么检测堆栈的手册里面介绍了很多方法有硬件的有软件的但是也没说怎么配置我这的实际情况是在这份代码的移植里应该是只有软件检测的软件检测是在PendSV中断里面触发的

PendSV有一句BL OSTaskSwHook进入这个函数后里面有一个判断

#if (OS_CFG_TASK_STK_REDZONE_EN > 0u)
                                                                /* Check if stack overflowed.                           */
    stk_status = OSTaskStkRedzoneChk((OS_TCB *)0u);
    if (stk_status != OS_TRUE) {
        OSRedzoneHitHook(OSTCBCurPtr);
    }
#endif

如果发现堆栈不对劲就会引发一个Software Exception自杀除去HardFault这个SW_Exception我也遇到过好几次现象跟HardFault差不多都是随机的

说实话我现在严重怀疑PC被破坏和前面TCB指针和PSP指针的乱指与堆栈溢出有关系反正一发生堆栈溢出就会践踏无辜的内存内存被莫名其妙的覆盖自然就出现莫名奇妙的现象嘛说得通哈哈~~嵌入式两大玄学堆栈溢出中断重入🤣🤣🤣

那么啥叫不对劲UCOS的任务堆栈实际上就是开在堆区的全局变量在创建任务时会将size和指针传进去同时还会传一个size/10的值进去这个size/10的值就是报警值也就是上限那为啥/10就报警呢剩下90%都不用嘛我之前还疑惑过后面才意识到堆栈的起始位置其实是ptr+size从最后面往前用的因为堆栈是向下的嘛所以其实是留了10%留给中断等特殊情况用的

那发现堆栈会溢出后就肯定要记录堆栈的情况呀如果是真不够了就要加内存下面是12号记录的堆栈的原始数据

…… 省略1K条一样的

StartTask      used/free:119/393  usage:23%  SP:0x200041b4 StkBase:0x20003b48 StkTop:0x20004348
LEDTask        used/free:112/400  usage:21%  SP:0x20001894 StkBase:0x200011f4 StkTop:0x200019f4
RCTask         used/free:85/427  usage:16%  SP:0x20003a2c StkBase:0x20003334 StkTop:0x20003b34
ChassisTask    used/free:73/951  usage:7%  SP:0x20000f14 StkBase:0x20000010 StkTop:0x20001010

StartTask      used/free:119/393  usage:23%  SP:0x200041b4 StkBase:0x20003b48 StkTop:0x20004348
LEDTask        used/free:112/400  usage:21%  SP:0x20001894 StkBase:0x200011f4 StkTop:0x200019f4
RCTask         used/free:84/428  usage:16%  SP:0x20003a2c StkBase:0x20003334 StkTop:0x20003b34
ChassisTask    used/free:73/951  usage:7%  SP:0x20000f14 StkBase:0x20000010 StkTop:0x20001010

StartTask      used/free:119/393  usage:23%  SP:0x200041b4 StkBase:0x20003b48 StkTop:0x20004348
LEDTask        used/free:112/400  usage:21%  SP:0x20001894 StkBase:0x200011f4 StkTop:0x200019f4
RCTask         used/free:85/427  usage:16%  SP:0x20003a2c StkBase:0x20003334 StkTop:0x20003b34
ChassisTask    used/free:73/951  usage:7%  SP:0x20000f14 StkBase:0x20000010 StkTop:0x20001010

Halt: RCTask Stack Overflow
StkPtr: 0x20000F14

就那么一瞬间突然爆的前面RCTask都是稳定的堆栈情况说实话也应是这个情况这玩意一直在做一样的事情应该是趋于稳定的才对最差也是平稳增长的可能因为堆栈不平衡push多了pop少了导致越用越多\越用越少然后就突然爆栈了最奇怪的地方最后打印的StkPtr应该是RCTask在爆栈时的SP居然跟ChassisTaskSP是一样的

另外还有一个数据

StkPtr 0x2000242C
StkLimitPtr 0x200001A8
StkBasePtr 0X20000010
StkSize 1024
StkUsed 73
StkFree 951

StkBasePtr + StkSize*CPU_STK(4) = 0x20001010

这个忘记是啥时候啥情况存的了按计算应该是从0x20001010用到0X20000010中间要在0x200001A8触发报警才对结果SP居然变成0x2000242C反向用的堆栈……

这就很疑问

1.堆栈怎么会是突然爆掉的呢看不懂

2.这玩意时间也是随机的出事的堆栈也是随机的有时是ChassisTask有时是RCTask而在上面今天调试的这个现场里面虽然是HardFault但是LEDTask按照TCB的情况也是爆栈了的

3.不开遥控的时候啥事没有也不会爆栈那么遥控的中断为什么会影响到堆栈


起床了下午继续排查关了遥控好久都没事遥控一开半分钟就挂了这次得到一个全新的样本

28.png

如图这次HardFaultLR0xFFFFFFED与之前不同的是这次这个值代表的是PSP+Thread Mode也就是说明是在Task中发生的异常

这次还是引发的BusFaultPRECISERR

29.png

拿到DumpStack很关键这次PCLR保存下来了0x080045E2

30.png

按照地址反汇编发现是OS_StatTask中的报错但是向上翻翻发现刚刚退出了临界区刚刚恢复了中断

31.png

  1. 不管是出问题的LDR还是上面的CBZ都不可能会碰PCPRECISERR说的很清楚了是异常返回StackPC值错误触发的

  2. 这行代码上面刚刚恢复中断恢复后可能就触发了PendSVPendSV返回时引发的异常

32.png

再次观察Keil的报告得到几个信息BFAR存的是0xBD4643E6也就是出错的PC此外FaultPendSV并没有Active此时的EXC_RETURN也确实说明的指向了PSP+Thread Mode有点没看明白理论上应该还是之前的那个问题但是fault的位置却是task


这还有个更奇怪的样本引发的是INVSTATEFault的时候似乎RCTaskLEDTask还没创建我确认了一下XCOM发现是收到了串口输出的按理说那两个Task应该已经启动了但是keil发现两个任务的TCB全部是0就像是还没初始化一样但是StartTask是有的

33.png

按照MSP指针去查DumpStack居然全部是0