ZZBX / NOTES ← 全部笔记
AutoSAR · CanSM · CanIf · BusOff

BusOff 恢复总是
比配置慢 25 毫秒

快恢复配置 50 ms,实测 75 ms;慢恢复配置 200 ms,实测 235 ms。控制器完全按标准在做事。多出来的那一截在别处:一个状态机不肯承认总线已经恢复,除非真的有一帧报文发出去 —— 而那一帧,要等应用自己的周期定时器轮到它。

2026-11-07 ◆ 约 10 分钟 ◆ AutoSAR CP / CanSM ◆ 25 毫秒没有着落
50 → 75毫秒,快恢复
200 → 235毫秒,慢恢复
1它在等的那一帧
40毫秒,那帧的周期

一段话讲清根因

根因:启用 CanSMBorTxConfirmationPolling 之后,CanSM 不把「控制器脱离 BusOff」当作恢复完成。状态 CanSM_FullCom_S_Bus_off_Check 会一直轮询 CanIf_GetTxConfirmationState(),直到返回 CANIF_TX_RX_NOTIFICATION —— 也就是至少有一帧报文真的发出去了。这一帧只能等应用自己的周期报文轮到发送时机。最快的周期是 40 ms,所以平均多等 20 ms、最坏 40 ms。50 + 25 = 75,235 - 200 = 35,两个偏差都落在这同一个区间里。修复:在 FULLCOM_S_BUS_OFF_CHECK 状态期间用 5 ms 周期主动调一次 CanIf_Transmit,把最坏等待从 40 ms 压到 5 ms。

这件事值得记下来,不是因为那个修法 —— 修法只有四行。而是因为这个偏差看起来像时序容差,实际上量的是另一件事。一旦知道恢复被一次发送确认卡着,那 25 ms 就不再是噪声,而变成一个可以预测、可以给上界、可以测的数字。

两个对不上的数字

配置很普通,测试也很普通,对不上的是结果:

阶段配置实测偏差
快恢复50 ms75 ms+25 ms
慢恢复200 ms235 ms+35 ms

关于这张表有两件事需要说清楚,因为大多数记录就错在这里。

第一,偏差不是常数。25 和 35 不是一条直线上的两个点。两个都落在 0–40 ms 里,而 40 ms 是总线上最快的那条周期报文(这个项目里是一条 40 ms 的 HCM_L)。等多久取决于恢复完成的那一刻,落在这 40 ms 窗口的哪个位置:如果刚好在报文该发之前完成,几乎不用等;如果刚好错过,就要等满 40。两次采样无法区分「固定偏移」和「均匀分布」 —— 而这里是后者。

第二,也是真正影响工程判断的一点:50 ms 的恢复上带 ±20 ms 的抖动,就是 40% 的不确定度。如果网络要求某个节点必须在 50 ms 内回来,那它有一半的概率回不来 —— 不是因为配置错了,而是因为恢复在等一个配置管不到的数据面事件。只看到一次 75 ms 就下结论「协议栈慢了 25 ms」的人,会把一天时间花在调错的参数上。

快慢恢复本该长什么样

在怀疑任何东西之前,先把标准规定的行为说准 —— 因为控制器那一半是固定的,问题不在那里:

  • 发送错误计数器 TEC 每发送失败 +8,每成功 -1。超过 255 时控制器进入 BusOff,完全把自己从总线上摘下来。
  • 要脱离 BusOff,必须连续观察到 128 × 11 个隐性位。如果短路还在,这些位永远不会出现,控制器就一直重试、一直恢复不了。
  • 快恢复以很短的固定间隔重试。每次尝试立刻失败就记一次;达到配置的次数(通常是 5 次)之后,控制器放弃快速重试。
  • 慢恢复接着用更长的间隔接管,避免对一条彻底坏掉的总线持续冲击。延迟递增的目的是让真正有故障的节点不再和健康节点抢总线。

这些全部是控制器侧的、确定性的、以位时间为单位的行为。它解释不了 25 ms 的偏移。也就是说,偏移必然来自控制器之上的软件 —— 而它之上唯一持有定时器的东西,是 CanSM。

慢的那一截不在控制器里

沿着「恢复应该完成」的那一刻走一遍调用链。这是普华栈里的路径,其他实现的形状也一样:

CanSM_MainFunction()                       ← 周期任务
  └─ CanSM_BsmSFullCom()                   ← FULL_COM 模式下的主状态机
       └─ CanSM_FullCom_S_Bus_off_Check()  ← ★ 恢复判定就在这里
            if (CANIF_TX_RX_NOTIFICATION ==
                CanIf_GetTxConfirmationState(controllerID))   ← 唯一的出口条件
            {
                /* 认为总线真的可用,退出 BUS_OFF_CHECK */
                CanSM_FullCom_S_Restart_CC(...);              ← 重启控制器
            }
            /* 否则:什么也不做,等下一次 CanSM_MainFunction */

把这个出口条件逐字读一遍。它问的不是控制器有没有退出 BusOff —— 控制器早就自己退出了,在硬件层。它问的是有没有发生过一次发送确认。这是两个不同的问题,而它们之间的差距,就是那 25 ms。

这个区分就是全文的核心。控制器恢复了是一个关于节点内部状态的事实;总线可用是一个关于那根线的断言,而它唯一的证据是有一帧过去了。这样配置的 CanSM 不接受推断,只接受证据。

这条配置到底换来了什么

这个行为由 CanSMBorTxConfirmationPolling 打开。它不改变任何等待时长,它改变的是什么算作完成。

配置恢复条件风险
FALSE 控制器退出 BusOff 即算恢复 如果总线还短路、或还有故障节点在霸占,会立刻被踢回 BusOff
TRUE 控制器退出 BusOff 并且 有一帧被确认 恢复时间从此依赖应用的发送调度

设计意图是合理的。反复重进 BusOff 的控制器会浪费总线时间,更糟的是,它会让一条真正有故障的总线永远无法安静下来。在宣布恢复之前要求「这根线能通」的证据,把一个重试风暴变成「一次干净的尝试 + 一次测量」。

代价是一次归属权的转移,而这一点在评审时很容易被漏掉:恢复时间不再单纯是 CanSM 配置的属性。它是 CanSM 配置加上应用里最快周期发送间隔的函数。而写下 CanSM 配置文件的人,看不到第二项。

这个结构性教训值得带到别的项目上:任何一个让状态机等待数据面事件的配置开关,都在耦合两个本来独立设计的子系统。这个耦合在两份配置文件里都看不见,它只以一个「对不上的数字」的形式出现 —— 而这正是这次问题浮现的方式。

满足这个检查的两条路

CanIf_GetTxConfirmationState() 读的是一个数组元素 CanIf_TxConfirmationState[]。这个数组只有两个地方会被置成 CANIF_TX_RX_NOTIFICATION,而两者的区别就是整个修法所在:

置位点前置条件延迟
CanIf_TxConfirmation 帧真的发出去了,控制器给出了确认 受发送调度约束
CanIf_TransmitHandle 使能 CANIF_TX_OFFLINE_ACTIVE_SUPPORT 只要有人调 CanIf_Transmit 就立刻

第二行才是关键,而它很容易被一眼扫过去。CanIf_TransmitHandle 是在 CanIf_Transmit 内部被调用的。所以只要调一次 CanIf_Transmit,就足以把状态推进 —— 你不需要那一帧真的到总线上。这正是下面那个重试之所以可行的原因:它给这个检查提供了一个更便宜的第二触发源,从而把它和应用的发送调度解耦。

必须在自己栈上核实的限定:CANIF_TX_OFFLINE_ACTIVE_SUPPORT 是一个配置开关,它的具体行为因实现而异。这里的主张很窄、也可以验证:开启它之后,CanIf_Transmit 能走到 CanIf_TransmitHandle,从而在没有真实总线确认的情况下把确认状态置上。你的 CanIf 是不是这样、以及在控制器 offline 时是否也这样,要用断点确认,不能靠文档。

断点:没人发帧,就永远等不到

到这里故障就很清楚了。BusOff 期间,这个检查停在等待状态。应用还在产生信号、还在请求发送 —— 但在 CanIf 这一层,这些请求产生不了确认,因为控制器根本没法把东西放到线上。唯一能满足这个检查的是一帧成功发送,而总线坏着的时候没有任何一帧能成功。

t0  TEC > 255 → 控制器进入 BusOff,脱离总线
t1  硬件等待 128 × 11 隐性位 → 恢复成功,回到 Error Active
t2  CanSM 进入 FULLCOM_S_BUS_OFF_CHECK,开始轮询 TxConfirmationState
t3  应用仍在调发送,但 CanIf 层不产生确认 → 状态不变
t4  ★ 只能等周期报文轮到发送时机
      最快周期 40 ms → 平均等 20 ms,最坏 40 ms
t5  那一帧发出去 → CanIf_TxConfirmation → 状态置 NOTIFICATION
t6  CanSM 判定恢复完成 → Restart_CC → 通信恢复

→ 实测 = 配置 + 等待时间
   快恢复:50 + 25 = 75 ms
   慢恢复:200 + 35 = 235 ms

注意,这段等待不是任何人写出来的延时。它是一个采样问题:状态机只能在一帧发送发生的那一刻观察到确认,所以「恢复时钟」的分辨率就是帧周期。40 ms 的报文意味着 40 ms 粒度的恢复时钟。这就是为什么偏差落在 0–40 而不是别处。

还有一个比「平均值」更麻烦的二阶后果。如果最快的那条周期报文所在的 IPduGroup 恰好在 BusOff 期间被关掉了 —— 也就是另一篇文章里那个修法 —— 那就根本没有帧被调度,等待不是 40 ms 而是无界。这种情况下面单独一节讲,因为它是真的死锁,不是舍入误差。

修复:5 毫秒重试

既然这个检查可以通过调用 CanIf_Transmit 来满足,而唯一的问题是没人在正确的时刻调它,那么修法就是刻意去调 —— 但只在需要它的那个窗口里调:

/* 5 ms 周期任务 */
void HCM_LR_TransmitRetry(void)
{
  /* ★ 只在 BUS_OFF_CHECK 期间动作,正常通信时完全沉默 */
  if (CanSM_GetCurrentSubState(CANSM_CONTROLLER_0)
        == CANSM_FULLCOM_S_BUS_OFF_CHECK)
  {
    /* 调一次即可推进 CanIf_TxConfirmationState
       —— 不需要这一帧真的到总线上 */
    (void)CanIf_Transmit(HCM_LR_TX_PDU_ID, &HCM_LR_TxPduInfo);
  }
}

两个性质让这是工程手段而不是打补丁。状态门控意味着它在正常运行时贡献零总线负载 —— 除了故障恢复期间,它一直休眠。5 ms 周期把最坏等待从 40 ms 压到 5 ms,于是恢复时间变成 50 + ≤5,而不是 50 + 0–40。

修复前修复后
快恢复实测75 ms50 ms
最坏额外等待40 ms5 ms
50 ms 预算上的抖动±20 ms±2.5 ms
正常运行时开销—0

实测回到 50 ms,也就是配置值本身 —— 这才是有用的那种确认:你看到的数字就是配置文件里的数字,而制造旧数字的机制是消失了,不只是被掩盖了。

为什么是 5 ms 不是 1 ms

下一个自然会问「为什么不再快一点」。三个理由,第三个是决定性的:

  • 收益递减。残余误差就是重试周期本身。从 40 ms 降到 5 ms 消掉 35 ms 抖动;从 5 ms 降到 1 ms 再消掉 4 ms。在 50 ms 的预算上,第二次收益不到 8%。
  • 过采样已有余量。相对 40 ms 的帧周期,5 ms 是 8 倍过采样。即使以后变型车里最快的帧变成 10 ms,5 ms 仍然有 2 倍余量。
  • 每次调用都有代价。CanIf_Transmit 不是免费的:它要穿过 PDU 路径、碰发送缓冲、可能和真实流量争用。BUS_OFF_CHECK 期间这无所谓,但这个周期同时也是「万一状态门控写错了,你要付多少」的下界。5 ms 的下界是个有界的错误;1 ms 的下界在故障期间就是个 CPU 预算问题。

还有一个「保持慢一点更正确」的论证:这个重试是一次探针,而一个打得太频繁的探针就不再是探针,而是流量。目的是尽快知道总线能不能用,不是一直去戳它。

和「关组」方案之间的死锁

这一节是我最希望评审仔细读的部分,因为这里是两个各自正确的修法组合成一个挂死的地方。它也是本文中属于推理而非实测的部分,所以请当作一个需要在自己栈上验证的假设。

另一篇的修法是:BusOff 时 BswM 调 Com_IpduGroupControl(group, FALSE) 停掉发送 I-PDU,避免泄漏的 Transmiting 标志把它们永久堵死。修法本身没问题,也验证过有效。但看看它对本文这条恢复路径做了什么:

t0  BusOff → BswM 关掉 TX 组(ActiveEnable = FALSE)
t1  控制器硬件恢复(128 × 11 隐性位)→ Error Active
t2  CanSM 进入 FULLCOM_S_BUS_OFF_CHECK,等一次发送确认
t3  组是关的 → Com 不再把任何 I-PDU 交给 PduR → 没有帧被调度
t4  ★ 没有帧 → 没有 TxConfirmation → 状态永远不变
t5  CanSM 永远停在 BUS_OFF_CHECK
t6  BswM 收不到「已恢复」事件 → 不会去重开组
    → 回到 t3,闭环

→ 控制器是好的、总线是好的、协议栈活着,但这个节点永远回不来

这个环是:CanSM 等一帧,帧等组,组等 BswM,BswM 等 CanSM。四个等待,零进展。这个环里没有一环单独看是坏的 —— 这正是这类挂死能通过代码评审的原因。

规避方式做法代价
重试帧不放在被关的组里 留一个最小常开组,只放探针帧 多一个组要维护;探针帧必须是总线上所有节点都能接受的
重试绕过 Com 直接调 CanIf_Transmit,不经过 Com 绕过 Com 自己的记账;要手工构造 PDU,需仔细评审
不关探针所在的组 只关那些帧不参与恢复的组 要求你知道恢复路径依赖哪些帧 —— 而这正是问题所在

第二行才是这套实现实际做的事,也解释了为什么两个修法在这个项目里能共存。但那是有人做出的设计决定,不是任何一个修法自带的性质。如果后来有人加了一个经过 Com 路径的重试,死锁就会回来 —— 而且是静默地回来,只在 BusOff 下发生,而那恰好是测得最少的条件。

一般化的形式:每当一条恢复路径等待某个数据面事件,就要检查系统里还有什么会压制这个事件。两个各自看起来局部的修法,可以经由故障处理层形成一个环。最便宜的防御是把这个环保显式地写下来 —— 上面那六行时间线比任何数量的测试都有用,因为测试只有在恰好覆盖了这个组合时才会发现它。

怎么测才算测过

单次恢复时间不是对这个行为的测量 —— 它只是一个样本,而你真正要刻画的是这个分布的宽度。所以应该这样测:

  1. 用 VH6501 或等效的故障注入通道确定性地制造 BusOff,不要手工短接线。短接能制造 BusOff,但控制不了时机,而时机正是决定偏差的东西。
  2. 至少注入 20 次,记录每一次的恢复时长。20 个样本是谈分布的最低门槛;50 个更好,自动化之后不额外花时间。
  3. 报 P50 和 P95,不要报平均值。平均值掩盖尾部,而尾部才是打破时序要求的东西。均值 60 ms、P95 88 ms 的系统,不是「60 ms 的系统」。
  4. 拿两组数字对照:配置值,以及「配置值 + 最快帧周期」。修复前 P95 应接近 50 + 40 = 90 ms;修复后应接近 50 + 5 = 55 ms。在测量之前把两个数字都预测出来,结果才算证据,而不只是观察。
  5. 同时测快慢切换点:第 5 次快恢复应该是最后一次。如果计数差一,你就有两个问题,而其中一个不是本文这个。

还有一件值得做的事:把重试计数留在一个可读的调试变量里。等这个故障最终出现在客户 trace 里时,知道探针到底被触发了几次,能立刻告诉你恢复路径有没有被走进去。

这套修法不解决什么

不解决的为什么
BusOff 本身的发生 重试让恢复变快,没有让总线变健康。如果短路是真的,恢复得快也没意义
中断期间丢掉的报文 物理事实。要不要补发是应用层的决定
轮询本身的取舍 重试缩短了等待,没有消除「恢复依赖发送路径」这个耦合
Bor 设计不同的其他栈 上面的链条是普华实现;状态名乃至轮询行为都因实现而异
慢恢复参数的取值 200 ms 对这个网络是不是对的数字,是系统问题,不是协议栈问题

把通用教训说一次,说直白:当一条恢复路径等待数据面的证据,恢复时间就不再是恢复配置的属性。它变成恢复配置、发送调度、以及一切能压制发送的东西的共同属性。三个所有者,通常没有一个知道另外两个的存在。

常见问题

为什么 CAN BusOff 快恢复实测比配置多 25 ms?

因为开了 CanSMBorTxConfirmationPolling 之后,CanSM 不把「控制器退出 BusOff」当作恢复完成。CanSM_FullCom_S_Bus_off_Check 会一直轮询 CanIf_GetTxConfirmationState,直到返回 CANIF_TX_RX_NOTIFICATION —— 也就是至少有一帧真的发出去了。这一帧只能来自应用的周期调度。最快周期 40 ms 时,额外等待平均 20 ms、最坏 40 ms,所以 50 + 25 = 75 正好落在这个窗口里。

CanSMBorTxConfirmationPolling 这个配置到底在做什么?

它把「控制器退出 BusOff」和「总线可用」拆成两个条件。硬件在 128 × 11 个隐性位后回到 Error Active,这只能证明这个节点愿意重新说话,不能证明线上是干净的。开启这个开关后,CanSM 额外要求一次成功的发送确认作为活体检测。意图是合理的;代价是恢复时间不再由 CanSM 配置单独决定。

CanIf_TxConfirmationState 是在哪里被置位的?

两个地方。CanIf_TxConfirmation 是正常路径:帧真的发出去了,控制器给了确认。另一个是 CanIf_TransmitHandle,需要使能 CANIF_TX_OFFLINE_ACTIVE_SUPPORT,它在 CanIf_Transmit 内部被调用。第二条路径正是重试能生效的原因 —— 调一次 CanIf_Transmit 就能推进状态,不需要真实的总线确认。

为什么快恢复慢 25 ms,慢恢复却慢 35 ms?

两个都落在同一个 0–40 ms 窗口里 —— 一个最快帧周期。等待时间取决于恢复完成时相对于那帧下一次发送时机的位置,是均匀分布而不是固定值。25 和 35 是两个采样点,不是规律。要刻画它需要 20 次以上注入和 P50/P95:修复前 P95 应接近配置值 + 40 ms,修复后接近配置值 + 5 ms。

5 ms 重试应该怎么实现?

建一个 5 ms 周期任务,调 CanIf_Transmit 发一条短报文,并由「CanSM 处于 FULLCOM_S_BUS_OFF_CHECK」这个条件门控。门控比周期更重要:没有它,你在正常运行时凭空增加总线负载。有了它,除了故障恢复期间成本为零。实测效果:快恢复从 75 ms 回到配置的 50 ms。

BusOff 时关闭 IPduGroup 和这个 5 ms 重试会冲突吗?

可能会死锁。如果关掉的组正是唯一会发帧的那一组,CanSM 就永远看不到确认,永远不退出 BUS_OFF_CHECK,于是 BswM 也永远收不到恢复事件、不会重开组。环是这样的:CanSM 等帧,帧等组,组等 BswM,BswM 等 CanSM。规避方式:把探针放在被关的组之外,或者让重试直接调 CanIf_Transmit,绕过 Com 的组开关。

Harry Zhao
汽车嵌入式 · AutoSAR BSW / 诊断工具链

12 年汽车电子底层软件经验,从 ECU 底层驱动做到 AutoSAR CP 平台架构:Com / CanIf / CanSM 集成、编译流程、测试脚本、 Git 代码服务器、Jenkins CI/CD,以及 TC397 / S32K314 芯片级功能安全。 这里记录的调试过程都来自真实供应商项目,而它们大多始于一个谁都解释不清的现象。

示例数据全部为合成,不含任何真实车型、供应商或客户标识。