前阵子调一块 STM32F103 的网关板遇到一个很典型的怪问题CAN 通信在桌面上怎么测都正常一进整机联调就乱套接收端一会儿说收不到一会儿又连续收到同一个 ID 重复好几帧。示波器挂上去一看总线占用率从正常的 20% 左右一路飙到接近 100%整个网络都被拖住了。查到最后问题出在 CAN 控制器默认打开的一个功能上——自动重发。这个功能到底该不该开圈子里争论一直不少。有人觉得 CAN 的可靠传输全靠它关掉等于自废武功也有人做实时控制时被它坑过一次之后就再也没开过。今天我结合在 STM32 上的实测数据把自动重发的原理、代价、配置方法和排坑经验一次性说清楚。正在做车载总线、伺服控制、仪器通信或者拿 STM32 做毕业设计的同学这篇文章应该能帮你省下不少弯路。1. 先想清楚自动重发到底是帮你还是坑你1.1 CAN错误处理机制里“自动重发”扮演的角色CAN 总线和我们平时用的 UART 不太一样它天然就是“多主”模式。任何一个节点在发送一帧数据后都必须等待其他节点在应答槽ACK Slot里给出一个显性位作为确认。如果发送节点没有收到这个确认或者传输过程中出现了位错误、填充错误、CRC 错误控制器就会认为这一帧发送失败。传统 CAN 控制器的默认做法是一旦发现发送失败就自动重新发送同一帧直到发送成功、或者被软件中止、或者节点进入 Bus-Off 状态。这个机制从 CAN 协议诞生时就存在目的是保证“报文最终能被送出去”尤其是面对瞬时干扰、偶发位错误的时候硬件自动重发可以大大提高可靠性。你可以把它理解成对讲机喊话你说了一句话对方没有回你“收到”你就再重复喊一遍。如果周围环境安静喊一遍对方就听到了如果环境很吵你就得一遍又一遍地喊直到对方回你为止。这种“不达目的不罢休”的策略在偶尔出错时确实管用但如果干扰一直存在你就会被困在重复喊话里其他该说的话全都排不上队。1.2 STM32 bxCAN/FDCAN是怎么实现这个开关的STM32 的 bxCAN 和 FDCAN 都提供了关闭自动重发的手段只是叫法不同。bxCAN 里对应的是 CAN_MCR 寄存器的 NART 位NART0 时开启自动重发NART1 时禁止自动重发。FDCAN 里对应的是 CCCR 寄存器的 DAR 位行为类似只是寄存器名字不一样HAL 库里的字段也有差异。用寄存器直接操作的话代码非常简单// STM32F1 bxCAN CAN1-MCR | CAN_MCR_NART; // 1禁止自动重发 CAN1-MCR ~CAN_MCR_NART; // 0开启自动重发用 STM32CubeMX HAL 库的话只需要在初始化结构体里改一个参数hcan.Instance CAN1; hcan.Init.Prescaler 9; hcan.Init.Mode CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_4TQ; hcan.Init.TimeSeg2 CAN_BS2_3TQ; hcan.Init.AutoRetransmission ENABLE; //改成 DISABLE 就是关闭 hcan.Init.AutoBusOff DISABLE; hcan.Init.AutoWakeUp DISABLE; hcan.Init.ReceiveFifoLocked DISABLE; hcan.Init.TransmitFifoPriority DISABLE; if (HAL_CAN_Init(hcan) ! HAL_OK) { Error_Handler(); }这里顺便说一下位时序。上面的配置里 APB1 时钟是 36MHzPrescaler 取 9BS14TQBS23TQSJW1TQ一个位时间就是 1438 个 TQ。算下来波特率是 36MHz / (9 × 8) 500kbps采样点位置在 (14)/8 62.5%。这个配置在 F103 上非常常用CubeMX 里也能直接拉出来。要注意一个细节自动重发不仅会发生在 ACK 错误时仲裁丢失也会触发重发。只要开启了自动重发低优先级帧在仲裁中输掉后控制器也会在总线空闲后自动把它重新发出去如果关闭了自动重发仲裁失败同样算“发送失败”帧会直接离开邮箱必须由软件决定是否重新提交。这个特性很多人容易忽略后面排查“丢帧”问题时会很关键。2. 开启自动重发这三个代价你算过吗2.1 错误帧会像滚雪球一样推高总线负载一条 CAN 总线上的带宽是固定的比如 500kbps 下一帧 8 字节标准帧大约要占用 222us。如果总线上出现了位错误发送节点会在错误帧结束后立刻重发同一帧。如果错误条件没有消失重发就会连续发生一个本来 222us 就能发完的帧可能会占据几毫秒甚至几十毫秒的总线时间。在只有一个节点出错时总线负载还只是升高但 CAN 是多主共享总线一个节点不断重发其他节点的帧就必须等待。最严重的时候总线看上去“忙”得不得了但实际传输的有效帧并没有增加反而全是重复数据。这就是典型的“重发风暴”。我见过有人把自动重发开着去抓总线日志结果发现某个 ID 的报文在错误期间重复了几十次总线负载报表直接“爆表”。如果不看底层数据很容易误判成“硬件故障”实际上是控制器在拼命重发把总线堵死了。2.2 最坏情况延迟无法对上层交代实时系统最怕的就是延迟不可控。比如一个控制周期是 1ms应用层每个周期发送一次电流指令。如果这一帧正好撞上总线干扰自动重发开启后它可能在进入成功发送前反复重试好几毫秒。对上层控制算法来说这一周期的指令已经“过期”了就算最后发送成功执行机构用的也是一帧旧数据。关闭自动重发后情况要简单得多发送不成功就立刻失败应用层知道这一周期数据没发出去可以决定是用上一次的值还是主动进入故障保护。这个“确定性”在运动控制、电池管理、安全通信里非常重要。很多刚从串口转 CAN 的开发者会有一个误区觉得只要 CAN 控制器还在自动重发数据就“没丢”。但实时系统的丢数据不只是“帧从总线上消失”还包括“数据没在规定时间内到达”。对时间敏感的应用来说晚到的数据比不到的数据更麻烦。2.3 应用层超时判断容易被“假成功”误导使用 HAL 库时很多人只关注HAL_CAN_AddTxMessage的返回值。这个函数返回HAL_OK只代表报文已经成功放进发送邮箱并不代表已经发送到总线上。如果你开启了自动重发邮箱里的帧可能一直在重发应用层却以为“发送成功了”。等到总线错误持续存在发送邮箱会被这样一帧“永远发不出去”的报文占满后续所有新的发送请求都会返回HAL_BUSY。此时应用层看到的是连续调用失败而不是一次清晰的“发送失败”状态。定位问题时很容易被误导到邮箱配置、中断优先级这些方向而忽略了真正的元凶是自动重发。更隐蔽的问题在接收端。如果发送节点一直重发同一个 ID接收端会收到大量重复帧。如果没有协议层去重接收端可能用重复数据覆盖新数据导致系统拿着旧状态做控制决策。这种“假成功”比直接丢帧更难排查。3. 实测数据同一条总线开与不开差多少3.1 测试环境和测量方法为了把问题量化我搭了一个非常简单的测试环境A 节点是 STM32F103C8T6 发送板B 节点是另一块同样芯片的干扰/接收板另外用逻辑分析仪直接抓总线波形统计负载。所有节点都没有外接 CAN 分析仪在线统计因为普通分析仪以正常模式挂在总线上会参与 ACK 应答反而会干扰故障注入。A 节点配置如下配置项数值发送芯片STM32F103C8T6APB136MHz波特率500kbps采样点62.5%帧格式标准帧ID0x123DLC8发送节奏定时器1ms触发一次邮箱满直接跳过干扰方式B节点每隔50ms切换到静默模式5ms观测工具20MHz采样逻辑分析仪示波器测GPIO时长B 节点切到静默模式后只接收报文不发送 ACK。这样 A 节点发送的任何一帧都无法被确认会触发 ACK 错误。这个做法的好处是干净、可重复比直接短路 CAN_H/CAN_L 安全得多也不会产生不可控的物理层波形。为了聚焦硬件自动重发本身A 节点的应用层没有做任何软件重发补偿。定时器到点后就直接调用发送接口如果邮箱满就跳过本次并计数。测试时长 5 分钟总计 300000 个发送周期。3.2 正常工况开与不开看不出差异先看没有干扰时的情况。总线环境干净A 节点正常每隔 1ms 发一帧开和关自动重发的表现几乎一模一样指标自动重发开启自动重发关闭平均总线负载22.3%22.2%峰值总线负载23%23%应用层成功发送周期数300000300000发送失败次数00接收端重复帧数00这个结果说明在总线质量良好、没有错误帧的环境下开与不开没有任何体验差异。这也解释了为什么很多人一直开着自动重发也没出过问题——平时总线太干净了错误重发的代价完全暴露不出来。3.3 人为干扰工况结果完全两样现在把 B 节点配置成每 50ms 静默 5ms。也就是说A 节点每 50ms 里大约有 5 个发送周期会落在“无 ACK”窗口内。同样跑 5 分钟数据差距非常明显指标自动重发开启自动重发关闭平均总线负载29.8%22.4%峰值总线负载98%26%A端成功发送的周期数276000270000A端因邮箱满跳过的周期数240000A端发送失败次数030000接收端识别到的重复帧数约1320000解读一下。关闭自动重发时落进静默窗口的那 30000 帧每一帧都只发送一次虽然接收端能看到这些波形但 A 节点因为没有收到 ACK判定为发送失败。总线负载几乎不变峰值也只有 26% 左右代价是 10% 的发送周期失败。开启自动重发时第一帧落进静默窗口后会立刻开始反复重发。5ms 的静默窗口内这一帧能重复二十多次接收端收到大量相同 ID 的重复帧总线在窗口内几乎被 100% 占满。等 B 节点恢复 ACK这一帧才终于发送成功但后续被打断的 4 个周期因为邮箱被占全部跳过应用层实际也没有把新数据发出去。所以从表面看开启自动重发“0 失败”但其实只是把旧数据反复刷屏真正新鲜的数据照样没发出去。接收端反而要花力气去重甚至在去重逻辑不健全时拿旧数据当新数据用。3.4 波形与总线占用率细节用逻辑分析仪看总线波形两边的差异非常直观。开启自动重发时静默窗口一开始同一个 ID 的报文像机关枪一样连续出现在总线上帧和帧之间的间隔几乎等于一帧本身的时间整个窗口内总线繁忙度接近 100%。只要 B 一直不 ACK这一帧就会无限重发下去直到外界恢复。关闭自动重发时静默窗口内的波形要“干净”得多。每个发送周期出现一帧之后总线就空在那里直到窗口结束正常周期报文才继续。接收端虽然能在窗口内收到这些失败的帧但从 A 节点的角度看它们都是无效帧不能作为有效数据参与控制逻辑。在实时控制场景里我宁可接收端偶尔“缺一帧”也不愿意接收端收到一帧延迟了 5ms 的“旧数据”。这是自动重发放不放在控制回路里的核心差异。4. 到底怎么选按应用场景做判断4.1 建议关闭自动重发的场景如果你做的是实时控制、伺服驱动、电池状态上报、或者任何对“数据时限”敏感的系统我建议直接关闭硬件自动重发。控制类数据本来就是周期性的这一周期发不出去下一周期会生成更新的数据重发旧数据没有任何控制意义反而占用总线带宽、延迟新数据。具体场景包括电机控制器的电流/速度环指令、机器人关节协调、电池管理系统的电流电压状态、主站和从站之间的周期同步报文。这类数据讲究“最新”不讲究“必达”丢了这帧就等下一帧千万不要让硬件帮你反复重发。另外如果通信协议本身就带了应用层重传机制比如诊断协议、文件传输协议、OTA 升级也建议关闭硬件自动重发。协议层有自己的超时重传和流控策略硬件自动重发会和它互相干扰反而让时序变得不可预测。4.2 建议保持开启的场景如果是非实时的数据采集、日志记录、故障录波、参数标定这类应用自动重发可以保留开启。这类数据对单帧延迟不敏感但要求最终不能丢。开启硬件自动重发后只要总线最终恢复正常报文就能被送出去简单可靠。比如一个温度传感器每隔 100ms 上报一次数据偶发一次总线干扰导致某帧发送失败自动重发可以等总线空闲后把这帧补上。虽然延迟了几毫秒但对温度监测来说完全没影响而且不用担心应用层要额外处理重发逻辑。OTA 升级、bootloader 下载这类场景也可以开启硬件自动重发前提是协议层能判断每一帧是否重复并且做好断点续传。不过更稳妥的做法是关闭硬件自动重发由协议栈的重传机制来保证完整性和顺序性。具体可以看我下面这个折中方案。4.3 折中方案应用层超时重发关闭硬件自动重发我现在的习惯是绝大多数项目都把硬件自动重发关掉在应用层实现一次“受控重发”。好处是重发次数、超时时间、去重逻辑都掌握在自己手里不会出现硬件无限重发堵塞总线的情况。伪代码大概是这样uint8_t CAN_SendWithRetry(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *txHeader, uint8_t data[], uint8_t maxRetry) { uint32_t mailbox; for (uint8_t i 0; i maxRetry; i) { if (HAL_CAN_AddTxMessage(hcan, txHeader, data, mailbox) ! HAL_OK) return 0; // 等待发送完成自设超时比如 1ms for (uint32_t t HAL_GetTick(); ;) { if (HAL_CAN_GetTxMailboxesFreeLevel(hcan) 0) return 1; if (HAL_GetTick() - t 1) break; } } return 0; }上面的函数只是一个简化示例。真正项目里我会把“发送完成”“发送失败”“超时”这三个状态分开处理并且根据业务等级决定是重发一次、丢弃本次、还是切换到故障模式。对于周期 1ms 的控制报文我通常把重试次数设为 0也就是调用失败就直接跳过让下一帧周期数据接管。5. 常见问题与排查技巧实录5.1 开了自动重发为什么总线占用率突然变高如果总线上原来正常的负载突然飙升先别急着怀疑硬件短路优先查是不是某个节点在反复重发。最直接的办法是看发送节点的错误状态寄存器。F103 的 bxCAN 里CAN_ESR 寄存器的 TEC 和 REC 分别表示发送错误计数和接收错误计数如果 TEC 持续上涨说明这个节点一直在产生错误。另一种更快的方法把 CAN 控制器临时配置为单次发送模式也就是关闭自动重发再观察总线负载。如果负载立刻降下来说明原来有“重发风暴”接下来要去查物理层问题比如终端电阻、CAN_H/CAN_L 接线、波特率是否匹配、是否有节点没接 ACK。5.2 关闭自动重发后偶发丢帧怎么查关了自动重发应用层会开始收到发送失败记录这是正常现象。真正要查的是“为什么会失败”。如果失败计数很低大概率只是仲裁丢失或者偶发干扰如果失败集中在某个时间段就要对应分析那段时候总线上有什么异常。建议在应用层给每帧数据加一个序列号或者时间戳。接收端用序列号检查有没有缺号发送端记录发送失败时的错误类型。只有把错误类型、错误时间、总线负载放在一起看才能定位到是物理层问题、波特率配置问题、还是 ID 优先级导致低优先级帧被持续推迟。5.3 发送邮箱一直满怎么定位开启自动重发时如果发送邮箱一直满最可能的原因就是邮箱里有一帧正在无限重发而应用层还在不断尝试添加新帧。此时优先检查是不是总线上长期没有 ACK比如只有一个节点在总线上或者接收节点进入了静默模式。如果确认是重发卡死可以通过HAL_CAN_AbortTxRequest中止当前发送请求释放邮箱。但注意 abort 只能作为超时保护不能当常规手段频繁 abort 会让本来就混乱的总线变得更难排查。正确做法是先解决总线错误再谈邮箱管理。5.4 怎样直观看到控制器在重发最简单的办法是拿逻辑分析仪抓总线波形。如果同一段时间内同一个 ID 的帧连续出现多次而应用层明明没有发送这么多新数据就能确定是控制器在自动重发。也可以在发送完成中断回调里翻转一个 GPIO用示波器看这个 GPIO 的脉冲间隔。正常情况下每一次发送完成后会有一个脉冲如果脉冲不是规律的而是出现连续多个紧密间隔说明帧在反复重发。我平时调试 CAN 板卡时会在最终样机上保留这样一个测试点联调时夹上示波器探头就能看出发送状态非常省事。我现在做 CAN 通信的第一习惯就是先把自动重发关掉再根据业务需求决定要不要在应用层补一次受控重发。等总线错误真正被解决了再回头考虑打开硬件自动重发也不迟。很多时候一个看似简单的“默认功能”会在极端场景下变成系统里最大的不可控变量。希望这篇实测和排坑记录能帮你少走一点弯路。
