寒假期间在家调试 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 刚复位的时候可以进入接收中断几次, 然后就再也不会进入中断了, 但是示波器测了也确实有串口的数据波形
“ 0 0 0 0 1024” 是遥控几个通道的值, 主 while 每隔一段时间打印, 如果摇杆扭动, 对应的值是会变化的;
为了 Debug, 在收到数据后, 在中断中发送 Recv 标志; 如图可见在物理按下 Reset 后会刷出 2-3 次 Recv, 然后就再没消息了。
对照 STM32F4 参考手册, 查询了几个关键寄存器的地址, RXNE=1 说明有数据等待接收, RXNEIE 却又一直是 0, 但是代码中确实有开启接收。
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 收完差不多长度的之后 就又不收了
不过轮询能一直收:
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 串口行为异常
虽然现在串口能正常接收了, 小车的轮子依照遥控器的油门已经欢快地转了起来, 但这美好的表面下还是有着隐隐的躁动:
A 通道的值不符合定义; B 通道数值异常。
接收到的 A 通道值可以超过定义的阈值, 并且变化不连续。 B 通道对应的是一个三段式开关, 应该有三个状态, 但是始终只能接受到两个状态。 此外, A 通道与 B 通道在二进制上是相邻的。
但是, 在示波器上, 可以看到 B 通道在三个状态之间切换时, 确实信号是有三种不同的对应状态的。
故障通道就是 “ 通道 3” 与 “ S1” 。
此外, 这个串口还死活没法开启 DMA。
虽然说通道变化时, 在示波器上确实看到波形变化了, 但是因为其他的通道都正常, 我还是怀疑是接收机本身的故障, 发送了错误的数据; 但是经过了一下午的查询, 也没有发现有相似情况发生。 考虑到两个通道是同一个字节分割开的, 我还检查了 N 遍解析代码是不是解析错误, 同时与剩下的 2-3 套参考代码进行对比, 但也没发现什么问题。 没办法, 这个问题只能先放在这里了, 想着看能不能等回学校用示波器解析一下串口波形。
第二个问题相对来说, 查起来更有思路: DMA 与串口, 必然会有相关的寄存器保存状态, 我们只要查 DMA 开启后是不是真的开启了, 开启后又是有没有真的收到过数据即可。
经过一番排查后, 发现如下现象:
DMA 在开启后, 第一次进入的 DMA 中断, 直接就进入了 DMA 的错误处理:
void HAL_DMA_IRQHandler (DMA_HandleTypeDef *hdma) { ... if ((tmpisr & (DMA_FLAG_TEIF0_4 << hdma->StreamIndex)) != RESET) { if (__HAL_DMA_GET_IT_SOURCE(hdma, DMA_IT_TE) != RESET) { hdma->Instance->CR &= ~(DMA_IT_TE); regs->IFCR = DMA_FLAG_TEIF0_4 << hdma->StreamIndex; hdma->ErrorCode |= HAL_DMA_ERROR_TE; } } ... }
DMA 发生了传输错误。 那么传输错误的原因呢? 最终发现居然是串口 ORE 了导致的 DMA 错误, 也就是串口溢出了。 这里我百思不得其解, 我都用 DMA 了, 理论上不是有数据你就自动给我从外设寄存器搬运到我的内存吗? 这一切理论上是全自动的, 除非是你 DMA 自己搬运慢了才会 ORE 呀。
这里在网上搜到了一种说法, 说是启动串口后, 在接收之前, 中间的时间差中收到了一些数据; 这些数据没有人取走, 造成的 ORE。
这个说法乍一听很有道理, 于是我也对代码做了一些调整, 使接收开始的位置与串口初始化的位置尽量靠近。 但是还是无济于事。 查看 HAL 库的代码后, 发现只要使用了 HAL 库, 其实这种说法本身就是站不住脚的, 因为 HAL 库已经考虑到这个情况了, 在开启 DMA 接收时已经清除了 ORE 标识。
HAL_StatusTypeDef HAL_UART_Receive_DMA (UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { ... tmp = (uint32_t *)&pData; HAL_DMA_Start_IT(huart->hdmarx, (uint32_t )&huart->Instance->DR, *(uint32_t *)tmp, Size); __HAL_UART_CLEAR_OREFLAG(huart); __HAL_UNLOCK(huart); SET_BIT(huart->Instance->CR1, USART_CR1_PEIE); SET_BIT(huart->Instance->CR3, USART_CR3_EIE); 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。
也怀疑过是不是串口参数的问题, 但是与三份代码与官方文档对比后也没发现错误; 最主要是数据也不是全错。
有一份代码是大疆某个官方车的开源代码, 有一份是遥控自带的示例代码, 还有一份是其他大学的开源代码, 配置无一例外都是 100k-8E1。
开始百思不得其解, 开始怀疑人生, 开始自闭, 开始怀疑大疆的遥控, 开始怀疑 HAL 库…… 最主要是在家又没别的仪器, 只能靠猜, 如果在学校兴许用示波器分析一下协议就知道问题了。
从跟朋友的聊天记录找到一个图, 全是搜索跟这相关的话题…… 这段时间浏览器标签页数量一直在 200 以上…… ( Firefox 牛逼! ! ! )
可能是老天看不下去了, 经过了一整天( 从起床开始一直查资料查到凌晨一两点然后睡觉, 第二天又继续) 查询, 终于在外网 StackOverflow 发现一个老外有近乎一样的环境与问题。 ( 他用的是 SBUS, 其实 DBUS 就是大疆拿 SBUS 搞得变种)
https://community.st.com/s/question/0D50X00009XkeWfSAJ/stm32f3-uart-dma-problem
他提到是数据长度的问题, 于是我尝试性的也将 8b 改成 9b, 问题解决, 不再 ORE 了, DMA 也能正常接收。
额外收获的是, 通道数据异常的问题也解决了, 道理其实很简单, 每个 Byte 之前会丢 1 个 bit, 数据自然会出错。
其实 CubeMX 也说清楚了, WordLength Include 了 Parity, 也就是包括了奇偶校验位的 1bit 长度。
总结: …… 我只关心那些个开源的代码, 是真的能用的吗? ? ? ? ?
20210202 故障 2: UCOS-III OS 无法启动
新的一天, 继续踩坑~
串口自从改成 9bit 长度后, 就几乎一切正常了( 我怀疑第一个故障是不是也与这个有关) 。
外设在裸机条件下基本稳定后, 开始移植操作系统。 这里因为一些原因, 我选择了 UCOS。
参考了正点原子的 “ STM32F4 UCOS 开发手册_V3.0.pdf” 与网上一个基于 HAL 库的移植教程后, 我下载了 micrium 的官方基于 MDK-Keil 的 F4 移植, 然后导入了我的工程。
跑马灯任务写好, 也正常编译了, 烧录进去, 叮! 没法工作。
其实也是预期之中的, 不出点岔子才奇怪了。
在调试状态下设置了一些断点, 发现是 OSStart() 后就 “ 跑飞” 了, 执行 OSStart() 后就没有进过 StartTask 了, OSStart 也没有返回过了。 于是跟着 OSStart 一直单步, 发现程序其实是没有 “ 跑飞” 的, 是一直在执行代码的, 只不过是卡在某个地方出不去了而已。 这个卡的地方还不是某个死循环之类的, 而是在好几个函数与汇编代码之间不停循环。
最后得到如下信息:
发现 PendSV 异常没被触发过, 但是 Systick 正常触发, 说明程序应该没有正常调度
程序一开始 OSStart 后是成功初始化了的, 也成功进入最高优先级的内部任务 TickTask 的, 并且发现是 Systick 正确地利用信号量唤醒了 TickTask
发现程序一直在优先级最高的 TickTask 切换不出去, 每次进入调度器后就退出了。 这里就是不合理的了, 理论上信号量 Pend 后应该要释放 CPU 进行调度, 调度就应该要到我们的跑马灯程序了, 但是调度程序因为什么原因并没有调度, 而是退出了。
继续跟进调度程序, 然后发现不执行调度的原因是 中断嵌套计数 OSIntNestingCtr>0, 说明 UCOS 认为这个时候在中断里面, 所以拒绝执行调度, 但是这个时候用 Keil 的调试工具看是没有任何 Active 的中断的( 终于学会使用 Keil 查看寄存器了) 。
最后查 OSIntNestingCtr 为啥会>0, 直接给 OSIntNestingCtr 加 Watch 下 Access 断点, 发现理论上时在进入中断时计数就会++, 退出时–, 从而计算有多少层中断嵌套; 接着查发现原因是在 OSInit 之前, Systick 中断就触发了, 进 systick 时那个计数不判断直接++了, 退出时却因为有判断发现 OS 还没跑起来, 就没有–直接退出了。
图为 UCOS 拒绝调度的位置
图为 UCOS 的 SysTick 对于 OSIntNestingCtr 的处理不平衡。
所以说要么不让 OSStart 之前触发 Systick, 要么 Systick 的回调就要多进行一个判断。
我认为这是一个很明显的 BUG, 但是官方移植、 正点原子…… 等等全部都按这个搞的, 为啥别人能正常调度?
带着疑问, 又打扰了一波学长, 学长查了最新的 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。
这里要先插播介绍一下软件的架构:
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 ) { #ifdef APP_UCOS_EN OSIntEnter(); #endif HAL_DMA_IRQHandler(&hdma_usart6_rx); #ifdef APP_UCOS_EN OSIntExit(); #endif } void USART6_IRQHandler (void ) { #ifdef APP_UCOS_EN OSIntEnter(); #endif HAL_UART_IRQHandler(&huart6); #ifdef APP_UCOS_EN OSIntExit(); #endif } void HAL_UART_RxCpltCallback (UART_HandleTypeDef *huart) { if (huart == (&RC_HUART)) { #ifdef APP_UCOS_EN OS_ERR err; #endif #ifdef APP_UCOS_EN 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); #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 ; 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 ); 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 ; RC_CtrlData.wheel = (RC_Origin_Buffer[16 ] | RC_Origin_Buffer[17 ] << 8 ) - 1024 ; #ifdef APP_UCOS_EN } #endif }
回到故障, 经过一番查询, HardFault 是有原因可查的, 具体资料可见 https://hhuysqt.github.io/hardfault/
经过一段时间的观察, 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。
查阅得到, 三个异常的原因都是 PC 值有问题, PC 寄存器指向了不该指向的位置。 在这里开始, 查资料关键词就可以以 CortexM 为主了, 因为这是 ARM 内核层面的异常, 已经不仅仅是 STM32 层面了。 PC 值, 也就是 Program Counter, R15 寄存器, 类似于 X86 中的 IP(EIP、 RIP), 用于指示下一条指令的位置。
接下来, 我们就要查 PC 到底是指向了哪里。 在 HardFault_Handler 的第一条语句打个断点, 然后引其复现。
查看几个关键寄存器的状态, PC=0x08002430, LR=0xFFFFFFE1, SP=0x20001228。 PC 指向的当然是断点的位置, 这个没的说的; LR 的值也就是 EXC_RETURN 的值, 拿去之前用到过的 “ Cortex™-M4 Devices Generic User Guide” 查询, 这个 LR 值表明发生错误的位置使用了 FPU, 是 Handler mode, 使用了 MSP 指针。
根据 UCOS 的任务模型, 这更加坚定了我们之前的判断, 发生错误的位置应该是 PendSV 中断。 PendSV 中断处理上下文切换, 在上下文切换时恢复了一个错误的 PC 值导致了 Fault, 这一切就说得通了。
只可惜, 此时的 Call Stack 已经被破坏, 已经无法验证发生异常的位置到底是在哪里了。
接下来可以想办法还原事故现场。 由于 Cortex-M 具有 Lazy Stacking 特性, 产生 Fault 时, 内核应该会自动保存事故现场。
查询 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, 不可能变成这样的地址) 。
在此图还可以看到, 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 了。 理论上我们可以根据这个查得到要恢复的任务是哪个。
可惜, 每个任务的 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 了。 手册可能没更新。
可以确认, 与内部任务也没有关系。 那是真不知道 PSP 那个 20003934 是哪来的了, 我们也没法进入 PendSV 去弄清楚发生了什么。 因为 Fault 发生是完全随机的, 有时要十几分钟, 有时 Reset 后 1 秒就 Fault 了, 所以也没法下断点, 可能点个几千次 Continue 都等不到复现, 最气的是真等到了还 tm 手欠继续按了个 Continue……
强行看一眼 PSP 的内存, 也没啥线索, 还特意往前挪了点字节。 这 PSP 指向的可一点不像个堆栈, 周围一大片 00000000, 我甚至怀疑 PSP 恢复了个错误的值, 也就是说写入 PSP 的地址并不是任务的 SP。 当然也有可能是堆栈溢出导致的。
机智的我又突然想起, 我们不是为了找接下来是啥任务吗? 我们直接按着汇编找 OSTCBCurPtr 不就好了吗? 查出来 0x20003910 也确实是个 SRAM 的地址, 结果再拿地址去查就懵逼了。 对着前面的已知 TCB( 包括我创建的和 UCOS 创建的) 的截图也没找到这个地址的 TCB。 不知道这个指针怎么来的, 莫名其妙……
这条路是有点走不通了, 说说堆栈溢出的事吧, 溢出那边如果没啥线索就只能反向的从 RCTask 找原因了。
首先说说 UCOS 怎么检测堆栈的, 手册里面介绍了很多方法, 有硬件的有软件的, 但是也没说怎么配置; 我这的实际情况是, 在这份代码的移植里, 应该是只有软件检测的, 软件检测是在 PendSV 中断里面触发的。
PendSV 有一句 BL OSTaskSwHook, 进入这个函数后里面有一个判断:
#if (OS_CFG_TASK_STK_REDZONE_EN > 0u) 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. 不开遥控的时候啥事没有, 也不会爆栈, 那么遥控的中断为什么会影响到堆栈?
起床了, 下午继续排查。 关了遥控好久都没事, 遥控一开半分钟就挂了, 这次得到一个全新的样本。
如图, 这次 HardFault 的 LR 是 0xFFFFFFED, 与之前不同的是, 这次这个值代表的是 PSP+Thread Mode, 也就是说明是在 Task 中发生的异常。
( 这次还是引发的 BusFault 的 PRECISERR)
拿到 DumpStack, 很关键, 这次 PC 和 LR 保存下来了: 0x080045E2
按照地址反汇编, 发现是 OS_StatTask( ) 中的报错, 但是向上翻翻, 发现刚刚退出了临界区, 刚刚恢复了中断。
不管是出问题的 LDR 还是上面的 CBZ 都不可能会碰 PC, PRECISERR 说的很清楚了是异常返回 Stack 的 PC 值错误触发的。
这行代码上面刚刚恢复中断, 恢复后可能就触发了 PendSV, 是 PendSV 返回时引发的异常。
再次观察 Keil 的报告, 得到几个信息。 BFAR 存的是 0xBD4643E6, 也就是出错的 PC 值。 此外, Fault 时 PendSV 并没有 Active, 此时的 EXC_RETURN 也确实说明的指向了 PSP+Thread Mode。 有点没看明白, 理论上应该还是之前的那个问题, 但是 fault 的位置却是 task。
这还有个更奇怪的样本, 引发的是 INVSTATE, Fault 的时候似乎 RCTask 和 LEDTask 还没创建? 我确认了一下 XCOM, 发现是收到了串口输出的, 按理说那两个 Task 应该已经启动了, 但是 keil 发现两个任务的 TCB 全部是 0, 就像是还没初始化一样( 但是 StartTask 是有的) 。
按照 MSP 指针去查 DumpStack, 居然全部是 0。