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

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

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

背景

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

外设:

  • UART1与PC通讯(115200,8N1,无流控)发送LOG
  • UART6接大疆的D-BUS遥控接收机(100000,9E1,无流控,UART电平反相)
  • CAN总线接4个电机,数字控制转矩电流
  • TIMER 1000hz定时器,为PID提供时钟

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

20210130

故障1:DBUS串口行为异常

(此时还是裸机,还没移植UCOS)

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

1.png

1.1.png

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

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


对照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并不算太快,特别是对于F4的168Mhz主频来说;中断内部也没有太占时间的工作,这影响会有那么大吗?

20210201

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

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

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

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

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

6.png

故障通道就是“通道3”与“S1”。

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

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

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

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

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

//位于HAL库stm32f4xx_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标识。

//位于HAL库stm32f4xx_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的情况发生。

BTW,HAL库确实是方便,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,问题解决,不再ORE了,DMA也能正常接收。

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

9.png

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

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

20210202

故障2:UCOS-III OS无法启动

新的一天,继续踩坑~

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

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

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

10.png

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

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

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

最后得到如下信息:

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

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

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

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

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

11.png

图为UCOS拒绝调度的位置

12.png
13.png

图为UCOS的SysTick对于OSIntNestingCtr的处理不平衡。

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

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

14.png
15.png

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

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

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

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

20210205

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

故障3:随机HardFault

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

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

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

16.png

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

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

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

此外,还发现,只要遥控器不开机,此时接收机就不会产生波形,到单片机上就不会产生中断,RCTask的Semaphore就会一直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是上访来的;上访的原因通常有三种,一个是MemoryManageFault的IACCVIOL,一个是BusFault的PRECISERR,一个是UsageFault的INVSTATE,图中这次复现是PRECISERR。好家伙,三种fault都尝了个遍。此外,每次发生HardFault的时机是随机的,并且通过NVIC可以看到PendSV的Active是置位的,也就是说应该是在PendSV异常里面发生的错误。

那么IACCVIOL和PRECISERR和INVSTATE到底是个啥意思呢?这个可以通过查阅ARM的“Cortex™-M4 Devices Generic User Guide”文档得到,可以在网上下载到公开的PDF。

19.png

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

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

20.png

查看几个关键寄存器的状态,PC=0x08002430,LR=0xFFFFFFE1,SP=0x20001228。PC指向的当然是断点的位置,这个没的说的;LR的值也就是EXC_RETURN的值,拿去之前用到过的“Cortex™-M4 Devices Generic User Guide”查询,这个LR值表明发生错误的位置使用了FPU,是Handler 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,并使用Keil的Memory窗口进行查询,发现前面5个值确实对应此时的R0-R3, R12,LR和PC确实是非法的值(代码段映射在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,但是好像我的情况要更复杂一些,我们保存的PC和LR已经被破坏了。按照这篇文章,我检查了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中并没有直接修改PC或LR的值,而是通过修改SP,然后通过exception return利用lazy stacking让CPU自动从SP中读到接下来任务的的PC和LR,来达到修改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的地址存到R2(OSTCBHighRdyPtr是个指针,指向tcb,所以OSTCBHighRdyPtr的值就是tcb的地址),然后LDR R0, [R2],把tcb指针指向的值,也就是第一个成员的值存到R0;第一个成员是该任务的StackPtr指针,所以R0存的也就是接下来任务的SP地址;最后MSR PSP, R0,把PSP指向接下来任务的SP。

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

20210221

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

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

查了一下手册,UCOS-III内部任务有IdleTask与TickTask,还可能有StatTask,TmrTask,IntQTask几个看你开没开可选功能的任务,分别分布在os_core.c,os_tick.c,os_stat.c,os_tmr.c,os_int.c几个文件里。

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

25.png

可以确认,与内部任务也没有关系。那是真不知道PSP那个20003934是哪来的了,我们也没法进入PendSV去弄清楚发生了什么。因为Fault发生是完全随机的,有时要十几分钟,有时Reset后1秒就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,居然跟ChassisTask的SP是一样的?

另外还有一个数据:

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

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

(这次还是引发的BusFault的PRECISERR)

29.png

拿到DumpStack,很关键,这次PC和LR保存下来了:0x080045E2

30.png

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

31.png

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

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

32.png

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


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

33.png

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