事发 2024 年 10 月,模块作为 CANopen 主站,以 5ms 间隔轮询读三个从站。以下全部来自当时的提交记录,时间线和判断都是原始的。
现象
某个从站随机失联,SDO 请求发出去没有任何应答。更麻烦的在后面:从站重新上线之后,还是不回。协议栈认为上一帧没发完,读循环整个卡死。掉线的从站不固定,间隔也不固定,有时几分钟,有时几小时,台架和现场同时出现。
取证链
先排除总线本身:示波器看波形,终端电阻、波特率、线缆都正常。掉线时抓总线流量,SDO 请求帧确实发出去了,从站就是不应答——问题不在总线,在我们这边。
突破口是"从站恢复后依然卡死"。正常协议栈超时重试,不该永远等下去。顺着接收路径往下翻,发现掉线发生时中断里有个状态被永久改写,接收路径从此走不通。再把驱动和 HAL 源码逐行对:发送失败的处理里有一行,把 CAN 控制器状态直接置成了 ERROR。HAL 一旦看到这个状态,后续收发全部拒绝执行。中断照常进,数据进不来——表现就是死在 IRQ 里。
根因
一共四层,从 HAL 到应用策略:
- 驱动层:RT-Thread CAN 驱动的发送失败分支里,
hcan->State被误置为HAL_CAN_STATE_ERROR,HAL 认为控制器进入错误状态,收发停摆。这是掉线的直接原因。 - 恢复缺失:从站重新上线时,驱动层没有任何恢复动作,错误状态一直挂着——这就是"上线也不回"的原因。
- 启动时序:我们应用启动时用 NMT 消息复位所有节点(Initialisation),本意是清干净再起。副作用是模块每次重启都会踢一遍电控,电控启动流程被反复打断。这个操作本来就不该存在。
- 策略错配:驱动的发送失败会触发 stopped 状态。对多数应用,“发送失败就停"是对的;我们是主站轮询,停了就不读了,策略不匹配。
修复
四步,每步单独验证:
- 删掉错误的状态置位,让 HAL 按自己的错误处理流程走。
- 从站上线事件里加 CAN 外设复位,清掉驱动的错误状态,收发恢复。
- 启动跳过 NMT 复位节点,直接进 Operational——模块重启不再踢电控。
- 去掉发送失败进 stopped 的分支:主站要持续读,不能因为一帧失败就停摆。
台架复测:三个节点的 SDO 轮询恢复连续读取,修复前必现的卡死不再出现。
验证标准
- 台架闭环:3 个从站、SDO 5ms 轮询,卡死不复现;修复分三天两版走完(2024-10-25 → 10-28)。
- 长期验证:此后两年的版本日志(到 V0.2.22,约 60 个版本)里,这类掉线零复发。CANopen 模块后续的 58 次提交全部是功能扩展——通信诊断对象字典、SDO 块传输固件更新——没有一条是返修。
事后记
这次教会我的不是"驱动有 bug”——第三方代码有 bug 是常态。是第 3、4 条:我们自己的代码里有两个"当时看没问题"的设计(启动 NMT 复位、发送失败即停),它们叠加在驱动的 bug 上,才把故障放大成批量掉线。排障排到最后,修的有一半是自己写的代码。