一段话讲清根因
根因:Com 用 Com_TxIPduRunTimeState[pduId].Transmiting 表示「这个 I-PDU 正在发送」。置 TRUE 在发送主函数里,清回 FALSE 只在 Com_TxConfirmation 里。BusOff 期间控制器脱离总线,那一帧的确认回调没有回来,于是 Transmiting 永远停在 TRUE,发送主函数每次都判定「上一帧还没发完」,从此跳过这个 PDU。修复:BusOff 进入时用 BswM 通过 Com_IpduGroupControl 关掉该组 I-PDU(ActiveEnable = FALSE),BusOff 恢复时必须重新打开,否则整组永久静默。
这不是某一家供应商代码的 bug,而是 Classic Platform Com 发送机制的结构性特征:发送权由一个标志发放,由一次回调归还。任何吞掉回调的情况 —— BusOff、发送途中模式切换、下层静默丢帧 —— 都会让这个标志泄漏。BusOff 只是最容易被人为复现的那一种。
现象:少了一条报文
复现方式刻意做得很粗暴:把 PCAN 侧的 CANH 与 CANL 短接,然后让应用发一条事件型报文。接下来发生的不是一串总线错误,而是安静 —— 那条报文在总线上再也不出现了,撤掉短接没有用,控制器完成 BusOff 恢复也没有用。
这个故障贵就贵在别的都没坏。其他周期报文照常在跑,应用没挂,CANoe 里总线上一直有流量。看 trace 的人看到一条繁忙的总线少了一个 ID,很自然会往时序或滤波上想,而不是往「某个状态机卡住了」上想。
| 观察到的 | 它排除了什么 |
|---|---|
| 总线仍在跑,其他报文正常 | 控制器死了、驱动停了、整个栈挂了 |
| 撤掉短接也不恢复 | 单纯的物理层瞬态干扰 |
| 只少了那一个 ID | 整组被关、通信模式被切 |
| 应用还在调发送接口 | 应用层不再产生信号 |
最后一行最关键:应用并没有停,它一直在请求发送。请求被接口接受了,然后在下一层被悄悄丢掉。「被接受但没发生」是状态标志的典型特征,不是缓冲区的特征 —— 缓冲区满了会返回忙,标志卡了则一切返回值都正常。
定位:状态位卡在 TRUE
挂上调试器,读这个 I-PDU 的运行时状态。一个布尔量就把故事讲完了:
Com_TxIPduRunTimeState[IPduId].Transmiting == TRUE ← 一直是 TRUE
Com_TxIPduRunTimeState[IPduId].ActiveEnable == TRUE ← 组是开着的
应用侧:Com_SendSignal() 返回 E_OK,信号确实写进了 Com 的运行缓冲
总线侧:该 ID 从短接那一刻起,一帧都没有再出现
注意这个组合:组是开着的、信号写进去了、报文还是不发。这三件事同时成立,就把范围收缩到一个点上 —— 发送主函数里的那个前置判断。
在普华生成的代码里,这条路径长这样。重要的是结构,不是具体命名 —— Vector、ETAS 等实现的同一个状态会换个前缀:
Com_MainFunctionTx() ← 周期任务
for each Tx I-PDU:
Com_MainFuncTxDirect(IPduId) ← 事件型走这条分支
if (Com_TxIPduRunTimeState[IPduId].ActiveEnable == TRUE)
{
if (Com_TxIPduRunTimeState[IPduId].Transmiting == FALSE) ← ★ 卡在这里
{
if (事件待发 / 周期到期)
{
ret = PduR_ComTransmit(...)
if (ret == E_OK)
Com_TxIPduRunTimeState[IPduId].Transmiting = TRUE; ← 置位
}
}
/* Transmiting == TRUE → 什么都不做,静默跳过 */
}
那个 else 分支在源码里连注释都没有 —— 它是「什么都不做」。没有计数器、没有日志、没有 DTC。这正是这个故障表现如此安静的原因:决定「不发」的那段代码不留任何痕迹。
机制:这个位凭什么能清
要看清 BusOff 是怎么弄坏它的,先看这个标志两侧有多不对称:置位有多个入口,清除只有一个。
| 位置 | 触发条件 | |
|---|---|---|
| 置 TRUE | Com_MainFuncTxDirect |
有事件待发或周期到期,且标志为 FALSE |
| 置 TRUE(重发) | 主函数的重发分支 | ComTxModeNumberOfRepetitions 未用完 |
| 清 FALSE | Com_TxConfirmation |
下层确认这一帧发完 |
三个入口,一个出口。而这个出口依赖一次来自硬件的回调,要依次穿过 CAN 驱动自己的主函数、CanIf_TxConfirmation、PduR_IfTxConfirmation,最后才到 Com_TxConfirmation。这条链上任何一处丢了帧,就没有确认。这个标志本质上是一份没有到期时间的租约。
这也正是那篇《一条报文的三条路径》在这里变得有用的原因:如果你以为「确认」是发送调用的一部分,这个故障根本无法推理 —— 在那个心智模型里,一次发送要么成功要么报错,不存在「被接受了但永远没完成」这第三种状态。
断链:BusOff 期间发生了什么
BusOff 是 CAN 控制器在发送错误计数器超过 255 之后,主动把自己从总线上摘下来。从那一刻到恢复完成,它不发任何帧。微妙之处在于已经交给下层的那几帧会怎样:
t0 Com 判定发送 → PduR_ComTransmit → CanIf_Transmit → Can_Write
Can_Write 返回 E_OK(帧进了发送邮箱)
Com 立刻置 Transmiting = TRUE ← 租约发出
t1 CANH / CANL 短接 → 发送错误 → TEC +8 → 持续累积
TEC > 255 → 进入 BusOff,控制器脱离总线
t2 邮箱里的帧发不出去,被丢弃 / 在 Flush 中清掉
★ 没有产生 TxConfirmation ← 租约再也没被归还
t3 BusOff 恢复(128 × 11 个隐性位),控制器回到 Error Active
Com 侧:Transmiting 仍然是 TRUE
→ Com_MainFuncTxDirect 每次进来都看到「还没发完」,直接跳过
→ 该 I-PDU 永久静默
t4 撤掉短接、重新上电都不算「重置 Com 的运行时状态」
→ 只有 Com 重新初始化,或状态位被显式清掉,才会恢复
关键是 t2 这一行。注意 t0 时 Can_Write 返回的 E_OK 不代表帧发出去了,它代表帧被收进了发送邮箱。这个区别在正常运行中完全不可见,而在 BusOff 期间它变成了整个故障本身。
必须诚实地说明一点:不同栈在这里的行为并不一致。有些 CanIf 实现会在 BusOff 或控制器模式切换时,主动对排队帧回调 E_NOT_OK 确认 —— 那样 Com 的 Transmiting 会被正常清掉,就不会有这个故障。所以这不是标准规定的必然行为,而是具体实现的选择。判断方法很简单:在 Com_TxConfirmation 里放一个计数,短接总线前后对比,看它有没有继续增长。涨不上去,就是本文这种情况。
修复:关组,然后记得开回来
修复并不直接去动那个状态标志。它做的事是把「总线根本不能用」的那段时间里对标志正确性的依赖去掉:BusOff 时告诉 Com 别再试着发这一组,恢复时告诉它可以继续。机制是 Com_IpduGroupControl,由 BswM 响应 CanSM 的 BusOff 状态来驱动。
/* BusOff 进入:关掉 TX 所在 IPduGroup */
void BswM_CanSM_CurrentState(Network, CurrentState)
{
if (CurrentState == CANSM_BSWM_BUS_OFF)
{
/* FALSE = 停止该组,组内所有 I-PDU 的 ActiveEnable 置 FALSE */
Com_IpduGroupControl(COM_IPDU_GROUP_TX, FALSE);
}
else if (CurrentState == CANSM_BSWM_NO_COMMUNICATION ||
CurrentState == CANSM_BSWM_FULL_COMMUNICATION)
{
/* ★ 恢复:必须显式打开,否则整组永久静默 */
Com_IpduGroupControl(COM_IPDU_GROUP_TX, TRUE);
}
}
有两个细节决定这套方案到底成不成立。第一是第二个参数。AUTOSAR 的签名是 Com_IpduGroupControl(IpduGroupId, Initialize)。Initialize = TRUE 会重新初始化组内 I-PDU —— 计数器、期限,以及在多数实现里连同运行时状态(含发送标志)一起重置;FALSE 只做启停,不碰这些状态。如果你的目的是清掉泄漏的标志,回到可用状态时通常需要 TRUE。这一点必须按自己用的实现去验证,因为规范没有把重置范围完全钉死。
第二是组的划分。如果所有 I-PDU 挤在一个组里,BusOff 时关掉它,连你可能还想留着做诊断的报文也一起停了,重开也变成全有或全无。给发送单独分一组,或者按关键性拆开,反应才是有比例的。
然后是最容易漏的一步:恢复分支。BusOff 恢复不是一个会让人注意到的事件 —— 总线就那么自己好起来了。如果没有任何东西去重新打开这个组,控制器是健康的、总线是安静的、报文依然不在。表现和原来的 bug 一模一样,原因完全不同。这是这套修法最常见的实现错误。
三个方案,为什么选关组
| 方案 | 做法 | 代价 |
|---|---|---|
| A · 关组 | BswM 在 BusOff 时关 TX 组,恢复时重开 | 恢复时序要自己保证;要求组划分合理 |
| B · 靠超时 | 让 Com 或 PduR 的发送超时机制自己清状态位 | CP Com 没有「超时清发送态」的标准机制;能不能用看实现 |
| C · 改 CanIf | BusOff 时对所有排队帧主动回调 E_NOT_OK |
改供应商 BSW;认证与回归成本最高 |
C 在技术上最干净:按契约,每一次被接受的发送都应该得到确认,所以下层也应该把失败确认为失败。但在供应商管理的 BSW 栈里,改 CanIf 意味着提变更请求、重新鉴定、再对每一个用这套栈的项目做回归。成本不在代码里,在流程里。
这里选 A,理由只有一个:它完全落在项目自己的配置空间里。BswM 规则和 IPduGroup 成员关系是项目产物,不需要重新鉴定供应商的 Com 或 CanIf。代价是恢复时序从此归你管,而且你必须测 —— 特别是「BusOff 恢复后这一组必须回来」这个场景。
关于 B 要补一句:它诱人是因为看起来像白送的安全网。但在依赖它之前,先确认这套机制在你用的栈里真的存在,而且真的清了发送状态,而不只是报了超时。一个只置了 DTC 却没清标志的超时机制,只是把症状从「静默」变成「有记录地静默」—— 好一点,但没修好。
台架上怎么复现和确认
不要靠等真实 BusOff 来测这个 —— 你会等很久,然后还不确定到底发生了什么。直接短接总线:
-
在 PCAN 接头上把
CANH与CANL短接,或者用 VH6501 / CANoe 的故障注入通道确定性地制造 BusOff。 - 从应用触发一条事件型报文 —— 按按钮、改信号,任何能让这个 PDU 进入待发状态的操作。
-
读
Com_TxIPduRunTimeState[pduId].Transmiting。它应该停在TRUE不动。 -
在短接前后各统计一次
Com_TxConfirmation的调用次数。如果发送还在尝试、而这个计数不再增长,就是断链了。 - 撤掉短接。报文回来了说明只是瞬态;回不来就是标志泄漏了。这是决定性的一步,也是最容易被跳过的一步。
修复到位后,重跑同一套流程,观察相反的迹象:BusOff 时组被关闭、该 PDU 连尝试都不再尝试(ActiveEnable 变 FALSE),恢复后组和报文都回来,而且不需要重新上电。
恢复路径要测得和故障路径一样狠。这套修法一半的价值在「总线恢复后才运行」的那段代码里,而那恰恰是正常测试中跑得最少的一段。
把 BusOff 本身也说清楚
既然 BusOff 是触发器,就值得把它本身说准 —— 因为它的时序正好解释了为什么这个故障不总是在同一个帧数上复现:
-
发送错误计数器(
TEC)每发送失败一次+8,每接收出错+1,每成功发送一次-1。 -
TEC > 255时控制器进入 BusOff:完全停止收发,把自己从总线上摘下来。 -
恢复需要连续观察到
128 × 11个隐性位。如果短接还在,这些位永远不会出现,控制器就一直重试、一直恢复不了。 - 恢复之后如果短时间内再次 BusOff,会进入慢恢复:重试前的等待时间递增(通常以约 100 ms 为单位),避免反复冲击一条已经坏掉的总线。
最后一点解释了为什么这个故障看起来时有时无:标志会不会泄漏,取决于 TEC 越过 255 的那一刻哪一帧正在飞。在控制器脱离之前就已经正常完成的那一帧会正常确认;脱离那一刻还在邮箱里的那一帧不会。同样的测试,不同的帧数,不同的结果。
这套修法不解决什么
说清边界比给出修法更重要,因为这个修法很容易被过度声称:
| 不解决的 | 为什么 |
|---|---|
| BusOff 期间丢掉的报文 | 物理事实,总线当时发不了。要不要补发是应用层决定,不是 Com 的事 |
| BusOff 本身的成因 | 关组让协议栈在总线故障中活下来,并没有阻止短路 |
| 其他同形状的标志 | 任何「只靠回调清除」的状态位都有同一种失效模式;这个修法只覆盖发送路径 |
| 因其他原因丢失的确认 | 发送途中模式切换、队列溢出、PduR 配错,效果相同,需要各自处理 |
值得提炼的通用教训是:每当一个状态机靠回调来回收权限,就要问一句「回调不来会怎样」。在这套栈里,答案是「权限变成永久的」—— 而代码里、日志里、总线 trace 里,没有任何地方说了这件事。这个问题在设计评审时问出来很便宜,在台架上出故障之后再回答很贵。
源码与背景:本文的调试记录来自一个使用普华 AutoSAR CP 的供应商项目。以上标识均为通用写法,不含任何真实车型、供应商或客户标识。
常见问题
为什么 CAN BusOff 会导致 AUTOSAR Com 的报文永久发不出去?
因为 Com 的发送状态位 Transmiting 只能由发送确认回调清除。Com 把帧交给 PduR 后把它置 TRUE,唯一把它清回 FALSE 的地方是 Com_TxConfirmation。BusOff 期间控制器脱离总线,帧发不出去,确认回调也可能根本不产生,于是这个标志永远停在 TRUE,发送主函数每次都判定「上一帧还没发完」,从此跳过这个 I-PDU。这个状态位是一份没有到期时间的租约。
Com_TxIPduRunTimeState 的 Transmiting 是谁置位、谁清除的?
置位在发送主函数里:Com_MainFunctionTx 判断某个 I-PDU 到期或事件待发、且标志为 FALSE 时,调用 PduR_ComTransmit,返回 E_OK 后把它置 TRUE。清除只有一个入口:Com_TxConfirmation(pduId),由 PduR 在收到 CanIf 的完成上报后调用。两侧不对称 —— 多个入口、一个出口 —— 出口断一次,标志就永久泄漏。
为什么周期报文看起来正常,只有事件型报文卡死?
通常不是周期报文没问题,而是它的故障被掩盖了。多数实现里周期和事件 I-PDU 共用同一个 Transmiting 判断,所以两者都会停。区别在可观测性:周期报文停了只是 trace 里少了一个心跳,容易被归因成总线负载或采样时机;事件报文停了就是明显不存在。所以排查时事件型报文是最灵敏的探针。
BusOff 恢复之后为什么报文还是不发?
两种原因,必须分开。一是 BusOff 期间标志已经泄漏 —— 恢复只恢复了控制器,没有恢复 Com 的运行时状态;二是用了关组方案但忘了重新打开,整组 ActiveEnable 保持 FALSE。第二种是这套修法最常见的实现错误,表现和原始 bug 完全一致。
除了 BusOff 时关闭 IPduGroup,还有别的修法吗?
三条。BswM 关组(本文):不动供应商代码,但恢复时序归你管。依赖发送超时:CP Com 没有「超时清发送态」的标准行为,能不能用完全取决于你的实现。改 CanIf 让排队帧在 BusOff 时回调 E_NOT_OK 确认:技术上最干净,认证与回归成本最高。
怎么快速确认现场故障是不是这个原因?
三步。第一,在调试器里看该 I-PDU 的发送标志是否恒为 TRUE,这是最快的判据。第二,在 PduR_ComTransmit 和 Com_TxConfirmation 两处各放一个计数,正常应一一对应;发送一直涨而确认不动就是断链。第三,短接 CANH 与 CANL 再撤掉:不恢复说明是标志泄漏,不是物理层瞬态。