Duke's Blog

Learning is a life long journey.

Subscribe

解决 CDBUS Bridge 小概率死机的问题

问题描述

使用 CDPNP 贴片机的时候 CDBUS Bridge 会非常小概率死机,如果每次使用贴片机 4 小时,平均贴片 5 次会遇到 1 次死机。 如果每个礼拜贴片 2 次,相当于半个多月会遇到 1 次死机。(Windows 下概率据说要高一点点。)

怀疑是 USB HAL 部分的代码 bug 导致。 因为 STM32 的 HAL 有很多 bug,譬如 SPI DMA 传输结束偶尔会丢 callback.

历史经验

我以前在 Ingenic 工作的时候,经常要解决嵌入式 Linux 小概率死机的问题, 譬如 Android 平板的 USB 插拔几千次会死机 1 次,又譬如 Hi-Fi 音频播放器连续播放几天会死机一次。

解决这些问题得到的经验是:

  1. 想一切办法尽量提高死机或 bug 复现概率。
  2. 加调试打印,死机或判断出问题的时候要打印出函数调用关系(backtrace)。

如果不提高 bug 复现概率,会很难抓到 bug 的相关打印, 而且还要根据 bug 当前触发的打印,进一步定位和增加打印内容,要重复很多次,耗时会很久。 而且,即便找到和解决了问题,也很难通过测试来确认问题真的被修复。 与之相反的做法是:稍微改变一下无关代码,让 bug 出现概率更低、甚至消失,从而不去解决问题, 这样日后再次遇到问题,就会 N 多问题混在一起,更加没有可能去解决。

打印函数调用关系,是为了通过极少次数的 bug 复现的打印,来推断问题出自何处。 经常一次死机的 log 就可以定位到问题点。

准备调试工具

之所以拖了很久没有解决 CDBUS Bridge 死机的 bug,有一个原因是它使用的是 Cortex M0+ 的芯片,和 M3 不同的是, 进入异常的时候,无法自动打印出函数调用关系,因为 M0+ 内核为了节约成本,有些调试相关的寄存器 CPU 自身无法读取, 只能通过 JTAG/SWD 读取。

之前抓到一次 log,显示进入了 hardfault 异常,没有其它多余信息,所以很难定位问题。 由于事情太多,又不熟 STM32 的 USB 代码,所以当时没有第一时间解决,但一直放在较高优先级的计划安排中。 (早期 CDBUS Bridge 使用 STM32F1 的时候,死机时会自动打印函数调用关系,使用 gcc 自带的 backtrace.)

由于我从事嵌入式 Linux 开发之后,就告别了 JTAG 调试,因为单步调试在很多场景都会受到限制(譬如 halt 的时候,电机驱动不再换相直接烧毁电机线圈), 而且断点信息很难保存下来和代码一起分享给其它参与者。而 log 打印则非常灵活和高效,是终极调试方案。

但是由于 M0+ 的硬件限制,只能借助 JTAG 来显示函数调用关系。

JTAG/SWD

由于我平时使用 Linux 桌面系统,所以代码是 STM32CubeMX 生成的 Makefile 工程,直接敲一下 make 就能得到 hex 固件,非常方便和适合开源项目。 编辑代码我习惯使用 eclipse,仅用来编辑和阅读代码,不负责编译和调试。

下载程序使用命令行的开源工具 st-flash(IAP 更新代码则可以用 CDBUS GUI 工具)。

这个开源工具其实名为 stlink ( https://github.com/texane/stlink ), 这两天又想起来,它自带了 jtag server 工具:st-util.

启动 gdb server(和烧录代码一样使用 ST-LINK v2 硬件、SWD 接口):
$ st-util

连接 gdb:
$ arm-none-eabi-gdb -ex "target remote localhost:4242" /path/to/xxx.elf

输入 continuec 恢复运行程序,首次执行设备会重启。

Ctrl + C 可以挂起 MCU.

MCU 挂起后,输入 where 可以打印 backtrace, 查看函数调用关系。

info locals 可以查看所有局部变量。

p xxx 可以查看指定的全局变量,可显示结构体内部数据。

还有其它很多命令,譬如读写指定内存地址的数据,gdb 还自带了命令行版本的图形界面 TUI,有兴趣可以自己查一下。

首次打印 backtrace,显示的函数关系不全,提示 "Backtrace stopped: Cannot access memory at address 0x20023fdc". 从代码安装最新 git 版本的 stlink 就解决了(因为 stm32g0b1 比较新)。

已经想不起来十多年前我刚在 Linux 下开发 stm32 的时候为何要使用 openocd 了。

提高死机复现概率

提高复现概率也尝试了很多方法,第一次有效果是:

借用 Windows 电脑,在 CDPNP 网页界面中,首先打开摄像头,拍摄画面内容比较丰富的电路板(让传输的 jpg 图片大一些), 然后在网页调试终端,用定时器每 0.1 秒移动一下 X 轴(移动速度预先设置为 2 档,步进 0.1mm)。

等吸嘴快移动到最右边,我再用键盘长按左键抢控制权,把吸嘴移动到最左边(大概每左移两三次,会右移一次)。

移动的比较慢,左右来回两次差不多就会死机一次。

半个小时不到,抓到两次死机:

Your browser may not support avif images!

由于两次死机都是访问链表的时候死掉,怀疑是 USB 接收电脑数据的时候,偶尔越界写 rx buffer 窜改了其它数据,导致内存数据错乱。 (APP_RX_DATA_SIZE 定义的是 2K 大小,而我提供的 rx buffer 只有 512 字节,虽然以前大概查过 APP_RX_DATA_SIZE 定义这么大没什么用。)

第二种效果非常好的方法是:

回到 Linux 电脑,改用 CDBUS GUI 工具,同时打开 CDPNP 机器的两个摄像头,且都拍摄比较复杂的图案。 此时,构建一个 CDBUS 数据帧(使用 echo 命令),带 CRC 共 10 bytes,发送目标是不存在的设备地址,数据重复很多次(使用 cat 命令),生成一个 5MBytes 左右的文件。 然后用 dd 命令把这个数据传输到 CDBUS Bridge 的串口节点。(串口节点同时被 CDBUS GUI 和 dd 程序打开和读写。)
(由于数据量太大,CPU 比较慢,来不及处理,会打印很多 rx_lost 错误,临时屏蔽掉该行打印。由 rx_lost 导致的 crc error 打印相对较少则没有屏蔽。)

基本上十来秒就会死机一次:

问题分析

由于第 3 次依然是链表出错,于是打开链表检查和打印,打开 LIST_DEBUG 定义即可(这就是打印调试的便捷之处)。
(会顺便打开 gcc 自带的 backtrace,需要增加一些编译参数,否则编译不过,临时屏蔽掉 unwind 相关代码即可。)

第 4 次果然链表检查提示出错,链表 len 成员和实际 nodes 数量不符。

由于很多次提示 cdbus_uart.cframe_free_head 链表出错,以及提示的链表操作函数是 list_get, 而非 list_get_it, 引起了我的注意。 我打开 cdbus_uart.c 文件,找到出错的 169 行:

list_get_it 最终也是通过 inline function 调用 list_get 函数,所以 backtrace 显示 list_get 函数也是正常的, 不能说明我没有使用 list_get_it 中断安全的版本。 (很多时候,backtrace 显示的内容会比较精减,要进一步查看出错位置的汇编,也很难和 c 文件对应的很好,是因为编译器优化导致,可以考虑临时设置成不优化或小优化等级,或仅部分函数或文件改优化等级。查看出错汇编和 c 程序对应关系,使用命令:arm-none-eabi-objdump -S xxx.elf.)

不过,以上还是引起了我的注意,毕竟是在找 bug,任何有可能出错的地方都要再三检查一下,不想一检查,真的发现我没有定义 CDUART_IRQ_SAFE. 因为我用 cdbus_uart 生成的两个虚拟设备,都是用于数据中转,仅在 main 循环中调用,没有涉及中断调用,所以没有开 CDUART_IRQ_SAFE. 然而,我漏掉一件事情:cdbus_uartcdctl_fast 代码共用同一个 frame_free_head 链表,cdctl_fast 会在中断里面操作 frame_free_head 链表。 如果 cdbus_uart 在主循环中正在操作 frame_free_head 链表,cdctl_fast 来了中断也操作了同一个链表,那么链表肯定就会错乱,至此破案。

同样的方法,又找到另外一处最近新写的代码没有以 irq safe 方式访问 frame_free_head 链表:
local_irq_save_restore 分别是关闭和打开全局中断使能。)

看来各种 free_head 很容易被忽视,全面检查了一遍其它各种 free_head,没有问题,多次使用同样的测试方法,长时间不再死机。

看来这次是错怪 STM HAL 代码了,复盘想了一下,CDPNP 用户复现概率高也不是因为 Windows 操作系统, 而是用户的贴片机配置的是高帧率的新版本摄像头,而我自己暂时用的还是老版本摄像头,数据量不同,相当于我同时打开两个老摄像头。 (我之所以还没用上新版本摄像头,是因为新摄像头要做 CDPNP 升级包赠送老客户,因为事情多,拖了比较久,已经联络了部分老客户,打算一个月内全部通知到位。)

使用 Eclipse 单步调试

首先要下载安装 Eclipse IDE for Embedded C/C++ Developers 版本的 Eclipse

然后在 Eclipse 直接打开现有的 Makefile 工程,Toolchain 选择 Arm Cross GCC

然后在臭虫图标的倒三角图标的下拉列表选择 Debug Configurations 菜单

然后双击 GDB Hardware Debugging 会自动在其下增加一个 xxx Default

然后指定一下 gdb 工具的路径,和 gdb server 的端口(下面不同工具默认端口不同,启动 gdb server 的时候会打印端口信息;其它 table 还可以指定是否每次启动调试都烧录一下代码之类的)

在外部启动 gdb server,譬如使用 st-util 命令(来自 st-flash 烧录工具)

又或者是:

$ pyocd gdbserver --target gd32e230f6

pyocd 没有默认支持的芯片,只要在芯片官网下载 keil 支持包,然后指定一下包所在路径即可,不用安装或修改 pyocd 配置(烧录也是一样,调试 stm32、gd32、at32 等等我使用的都是 st-link v2 硬件):

$ pyocd gdbserver --target at32f405cbt7 --pack=/path/to/ArteryTek.AT32F402_405_DFP.2.1.0.pack

最后点编译按钮就可以完成编译,然后点臭虫图标,或者是上图最右下角的 Debug 按钮即可进入调试模式。(留意 gdbserver 终端的打印,开始烧录程序要等几秒。)

Comments:


Please visit the original link: /stm32g0-hardfault-debug-cn