☰
STM32 CAN自动重发开关:NART与DAR位实测对比与场景选择
2026/9/25 1:44:49 网站建设 项目流程

做嵌入式这些年,几乎每次涉及CAN总线的项目评审,都会有人在配置表里问一句:发送失败,要不要让控制器自动重发?这个开关在STM32的bxCAN里叫NART位,在FDCAN里叫DAR位,名字不同,作用一样,但不同场景下的选择却直接决定系统稳定性。我自己踩过坑:电机控制器调试时,自动重发开着,总线上一个瞬时干扰,控制报文被老数据重发占据,应用层拿到的是过期转速,差点当成故障触发保护。从那以后,我对这个位都特别敏感。

这篇文章就用STM32实测数据,把这件“小事”彻底讲透:自动重发到底在干什么、开关各有什么代价、什么场景必须开、什么场景必须关,以及关掉之后应用层怎么接住这口锅。文章偏实战,适合正在调CAN节点、写应用层、做诊断和OTA升级的工程师,也适合刚接触CAN总线、想搞清楚协议底层行为的同学。

1. 自动重发的底层逻辑:CAN协议天生就“倔”

1.1 仲裁失败和错误帧之后的自动重试

CAN总线的数据帧设计,从一开始就不是“发一次就完事”。多个节点同时发送时,先进行非破坏性仲裁,优先级高的帧获胜,优先级低的节点自动停止发送,等总线空闲后重新尝试。这个过程不产生错误帧,也不增加错误计数器。而真正触发“重发机制”的是两种情况:一是帧在总线上因信号干扰、位填充错误、CRC校验失败等原因被任何节点判定为错误帧;二是发送节点发出后没有收到ACK。

无论是仲裁失败还是错误帧,CAN控制器的硬件行为是一致的:当前报文不会丢,而是留在发送邮箱里,等总线再次空闲或者错误恢复后,从SOF开始重新发送整帧。这个重发完全由硬件完成,不需要应用软件参与,所以它既快又“无脑”——哪怕应用层已经打算发送更新的数据,硬件也不知道,只会死心眼地把旧报文一遍遍发到成功为止。

1.2 错误计数器如何“记账”:TEC、REC、Error Passive和Bus Off

说到重发,就必须明白CAN节点内部的错误管理机制。每个节点都有发送错误计数器TEC和接收错误计数器REC,二者共同决定节点状态:

  • 错误主动(Error Active):TEC和REC都小于128,节点正常参与通信。
  • 错误被动(Error Passive):TEC或REC达到128~255,节点发送前需要插入延迟,且错误标志只能发隐性位,发送成功率明显下降。
  • 总线关闭(Bus Off):TEC超过255。此时节点完全退出总线,不能收发任何数据,直到检测到128次“总线连续11位隐性位”后才会重新恢复。

重发、错误计数和总线状态是绑在一起的。节点发送失败一次,TEC加8;发送成功一次,TEC减1。所以当自动重发开启时,如果总线持续存在干扰,节点并不是“温柔地”一遍遍重发,而是每失败一次错误计数就涨一截。实测下来,连续几十次失败就可能让节点进入Error Passive,再到Bus Off,整个过程可能不到100毫秒。这个隐患在测试时很容易被忽略,因为测试环境里总线往往非常干净。

2. STM32上开关自动重发的正确姿势

2.1 bxCAN和FDCAN的配置入口差异

STM32系列的CAN控制器主要分两类。F1、F2、F4、L4等用的是经典bxCAN,控制逻辑简单直接;G4、H7、U5等用的是FDCAN,功能更复杂,支持CAN FD。两者都有“禁止自动重发”的开关,但寄存器完全不同。

bxCAN中,CAN主控制寄存器MCR有两个关键位:NART(No Automatic Retransmission)和ABOM(Automatic Bus-Off Management)。NART为1时,禁止自动重发;为0时,允许硬件自动重发。ABOM为1时,节点进入Bus Off后硬件自动等待恢复;为0时,需要软件不停读状态,然后手动恢复。标准库里的初始化结构体一般同时给出这两个选项:

CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_ABOM = ENABLE; // 开启自动离线恢复 CAN_InitStructure.CAN_NART = ENABLE; // 禁止自动重发 CAN_InitStructure.CAN_TXFP = DISABLE; // 不启用先入先出 CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_9tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_6tq; CAN_InitStructure.CAN_Prescaler = 4; // 假设42MHz APB1,500kbps CAN_Init(CAN1, &CAN_InitStructure);

FDCAN则是在CCCR寄存器里有一个DAR位(Disable Automatic Retransmission)。HAL库没有直接把DAR封装成Init结构体成员,所以我通常在初始化之后手动写寄存器:

// 打开自动重发(默认) hfdcan1.Instance->CCCR &= ~FDCAN_CCCR_DAR; // 关闭自动重发 hfdcan1.Instance->CCCR |= FDCAN_CCCR_DAR;

注意:FDCAN在CAN FD模式下,自动重发机制和经典CAN模式有一些细节差异,后续单独展开。

2.2 开启和关闭对软件开发接口的直接影响

自动重发开和关,对应用代码的影响远不止一个初始化位。最核心的差异在于“发送函数返回后,数据到底发没发出去”:

  • 自动重发开启时,HAL_CAN_AddTxMessage或CAN_Transmit返回HAL_OK只代表报文成功挂进了硬件发送邮箱。如果此时总线上正好有干扰,硬件会一直重发,发送函数本身表现正常,应用层根本不知道发生了重发。只有当读取邮箱状态寄存器时,才能看到邮箱还没空、TEC在涨。
  • 自动重发关闭时,发送失败后硬件会立刻释放邮箱,并把错误标志置位。此时应用层可以通过发送状态查询明确知道失败,从而决定是否需要重新排队、换帧、丢弃。

也就是说,自动重发本质上是在“硬件帮你做重试决策”和“把重试决策权交还给应用层”之间做选择。前者实现简单,但控制粒度粗;后者需要应用层有逻辑,但系统整体更可控。后面实测数据会验证这一点。

3. 实测记录:四种场景下的真实表现

3.1 测试环境与测试方法

我搭了一套最小测试系统来量化对比自动重发开关的实际影响。主测节点用STM32F407,作为发送节点,运行在Normal模式,CAN波特率500kbps,标准帧,数据长度8字节,发送周期1ms。监听节点用周立功USBCAN-II分析仪接在总线上,记录每帧到达时间戳;同时用示波器挂在CAN_H和CAN_L上观察物理层。

干扰注入方式比较粗暴但有效:用另一个STM32的推挽输出GPIO,通过一个47欧电阻周期性地短接CAN_H和CAN_L之间的电平,主动制造位错误和帧错误。通过控制GPIO翻转时序,模拟“瞬时干扰”和“持续干扰”两种工况。发送节点的TEC、REC、重发次数、邮箱状态都通过串口上报,和USBCAN-II的时间戳对照。

测试分为四组:正常周期发送、单次2ms总线扰动、持续50ms总线故障、高总线负载(人为增加一个高优先级节点每秒刷大量报文)。每组分别跑自动重发开启和关闭两种情况,每组采集1000次发送数据。

3.2 场景一:总线无干扰时,开关几乎没有差别

正常总线环境下,两组数据基本一致。自动重发开启时,发送延迟中位数约为0.11ms,最大延迟0.13ms;自动重发关闭时,中位数0.11ms,最大延迟0.14ms。差异可以忽略,主要是时钟抖动和软件中断优先级造成。

这个结论其实不值得意外:无干扰时本来就不会触发重发,这个开关只是“保险丝”,平时不工作。但它启示我们,不要在无干扰环境里通过“看起来一切正常”来判断配置是否合理,必须做故障注入测试。

3.3 场景二:单次2ms扰动,自动重发改写了实时性

在总线上注入一个2ms的短时电平扰动,等效于破坏几帧数据。自动重发开启时,硬件会立刻重发,实测大部分情况下重发1~3次后成功,报文发送总延迟增大到0.4~0.8ms之间,比正常情况大了好几倍。由于发送邮箱一直被占住,后续周期报文被迫排队,间接影响了至少两个发送周期的实时性。

自动重发关闭时,故障期间的报文直接失败,应用层收到失败返回,邮箱立即释放,下一个周期报文可以照常发送。最终表现是:故障期间丢了一帧,但后续帧没有被堵车影响,发送节奏保持稳定。

这里是我认为最关键的实测观察:自动重发解决的是“报文是否最终交付”,但代价是“后续帧的实时性”。在实时控制系统中,迟到的老数据往往比丢弃更危险。

3.4 场景三:持续50ms总线故障,一个Bus Off一个扛住

持续故障是最极端的测试。自动重发开启时,节点不断重发,TEC快速上升,实测从正常到进入Error Passive大约需要几十次失败,大概不到10ms;再继续到TEC超过255触发Bus Off,总共约20ms。进入Bus Off后,该节点完全静默,直到故障消失并完成128次11位隐性位检测,恢复时间通常要几毫秒到几十毫秒。整体来看,从故障开始到节点恢复送数,中断时间约50~100ms。

自动重发关闭时,节点每次发送都失败,但TEC同样会因为发送失败而累加,只是没有了“发不出去就不撒手”的阻塞,应用层每毫秒都能正常收到失败回调。实测连续1000次发送全部失败,节点TEC上升到96~112附近后基本稳定,没有触发Bus Off,故障消失后的第一帧就能立即发出。

这个对比非常直观:自动重发关闭不会让节点“免疫”错误计数,但它把恢复主动权交还给了系统——应用层可以选择停止发送、降级、告警,而不是让硬件在一个死胡同里空转。

3.5 场景四:高总线负载下的排队延迟

把总线负载拉到95%左右(用另一个节点持续发送高优先级短帧),再来对比发送表现。自动重发开启时,报文在邮箱里等待,仲裁失败的次数变多,但因为重发会持续进行,最终每帧都能发出,最大发送延迟约3.2ms。自动重发关闭时,一旦发生仲裁失败,该报文直接失败,应用层需要立即重新入队,实测丢帧率约2%~5%,但如果应用层用“队列+重排队”策略,最终发送成功率也能做到接近100%,只是软件复杂度上去了。

这组数据说明:高负载下,硬件自动重发确实更省心,但不能保证“发送时刻”的确定性。

3.6 实测数据速查对比

把四组测试的典型结果汇总起来,方便按项目需求快速评估:

场景开启自动重发关闭自动重发
正常周期发送稳定,延迟约0.12ms稳定,延迟约0.12ms
单次2ms扰动重发1~3次,最终发出,延迟增至0.4~0.8ms丢一帧,后续实时性不中断
持续50ms故障TEC飙升,约20ms进入Bus Off,恢复后重建发送TEC升至约100后稳定,不Bus Off,故障消失后立即发送
95%总线负载最终全部发出,最大延迟约3.2ms直接失败2%~5%,需要软件重排队

4. 该不该开?按业务场景给出明确建议

4.1 强烈建议开启:车身控制、底盘通信、电机控制指令下发

如果报文承载的是控制指令,比如VCU给电机控制器下发扭矩、BMS给继电器下发闭合指令,这类场景的数据特点是“同一帧重复发送,接收方执行同一动作”,值的一致性比时间戳更重要。自动重开在这个场景就是“保险”,它能保证控制指令尽量被送达。

但这里有一个前提:接收方必须做“重复帧”处理。因为自动重发可能导致相同ID的报文在短时间内被接收两次,若接收方只按“收到就执行”来处理,同一个扭矩指令被执行两次虽然通常无害,但在某些递增型命令(如副驾座椅角度步进、车窗升降防夹逻辑)中就是逻辑漏洞。所以开启自动重发时,应用层最好加上帧计数或标志位去重。

4.2 强烈建议关闭:诊断通信、Bootloader升级、报文记录

诊断服务(ISO 14229)是典型的“请求-响应”模式,ECU收到诊断请求后执行,再回复响应。如果诊断请求的发送节点开启了自动重发,故障期间同一个诊断请求会被重复送到ECU,ECU可能连续执行两次服务,甚至扰乱诊断会话状态机。Bootloader刷写同理,AID/BLOCK数据的重发会导致Flash写入重复或地址错乱,所以我做OTA升级的CAN节点,底层强制关闭自动重发,由bootloader协议自行做超时和重传。

另外,数据记录和标定场景也不建议开。记录仪如果不停地重复收到同一条老报文,会干扰离线数据分析;标定工具更希望看到真实的丢帧和总线错误,而不是被硬件重发“修正”后的美好数据。

4.3 需要折中处理:布局诊断功能、OTA等多模式场景

实际项目经常是“同一个CAN节点,既要实时控制,也要诊断升级”,这种多模式场景不能简单地说开或关。我一般建议:“实时控制报文开启自动重发,诊断/升级系统期间切换为关闭”。

切换方式就是在CAN控制器初始化后、诊断会话建立前动态改寄存器。bxCAN的NART位在CAN运行时不能随意改,需要先进入初始化模式,配置完再退出;FDCAN的DAR位修改也一样,先设置CCCR的INIT位,再改DAR,最后清INIT位。

// bxCAN运行时切换NART CAN1->MCR |= CAN_MCR_INRQ; // 进入初始化模式,注意要等待INAK位置位 CAN1->MCR |= CAN_MCR_NART; // 关闭自动重发 CAN1->MCR &= ~CAN_MCR_INRQ; // 退出初始化模式

4.4 另一个容易踩的坑:CAN FD模式下的行为差异

FDCAN在CAN FD模式下,CCCR里的DAR位控制自动重发。但要注意,FD帧使用了更复杂的CRC和位时序,当总线上同时存在经典CAN和FD帧时,节点收到不支持的FD帧会发送错误帧。此时如果开启自动重发,节点对同一帧重复重发,会反复干扰整个总线的通信节奏。实测中,在混合网络上,一个配置错误的FD节点开启自动重发,比关闭状态更容易引发连锁错误,严重时会把其他节点拖入Bus Off。所以,如果你的网络里有FD和非FD设备混跑,建议所有节点全部关闭自动重发,或者确保所有节点都支持FD。

5. 实操代码与踩坑实录

5.1 使用HAL库配置并实现“受控重发”

下面这段代码的逻辑是:关闭硬件自动重发,发送失败后由应用层决定最多重试3次,每次退避时间递增。核心作用是避免“失败后立即盲目重发”导致的死循环。

CAN_TxHeaderTypeDef txHeader; uint8_t txData[8]; uint32_t txMailbox; uint8_t retryCount = 0; const uint8_t MAX_RETRY = 3; int CAN_SendWithRetry(uint32_t id, uint8_t *data, uint8_t len) { txHeader.IDE = CAN_ID_STD; txHeader.StdId = id; txHeader.DLC = len; txHeader.RTR = CAN_RTR_DATA; while (retryCount < MAX_RETRY) { if (HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailbox) == HAL_OK) { // 等待邮箱发送完成,超时200ms uint32_t tickStart = HAL_GetTick(); while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) < 3) { if (HAL_GetTick() - tickStart > 200) break; } retryCount = 0; return 0; } else { retryCount++; // 渐进退避,避免短促故障时连续失败打满错误计数 osDelay(1 + retryCount * 2); } } retryCount = 0; return -1; }

这个实现的核心思想是“把重发控制权拿到自己手里”。实测下来,在瞬时干扰场景下,这种受控重发的总发送成功率和硬件自动重发几乎一样,但最大延迟更可控,也不会在持续故障时把TEC憋到Bus Off。

5.2 开启自动重发时的发送确认:别被假成功骗了

如果你决定保持自动重发开启,有一个细节必须处理:不能只靠发送函数的返回值判断成功。很多工程师发现返回值是HAL_OK,就以为报文已经发到总线上了,实际上报文可能还在邮箱里重试。正确做法是在CAN TX中断里做确认。bxCAN的发送邮箱为空时,会触发TX Mailbox Empty中断;FDCAN则有TX Event FIFO机制,能精确记录每帧真正发送到总线的时间戳。

开启自动重发后,建议优先使用“邮箱空中断+成功标志”作为确认手段。例如:

void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) { if (hcan->Instance == CAN1) { txConfirmedFlag = 1; // 置位,通知应用层该帧已真正发出 } }

在实时性要求高的场景,应用层还可以结合发送时刻判断数据时效:如果从发送开始到邮箱真正空闲超过某个阈值(比如5ms),即使最终发送成功,也建议丢弃该帧并等待新鲜数据。

5.3 实操中遇到的典型问题与解决思路

问题一:关闭自动重发后,发送偶尔失败,但错误帧数量很低。排查后发现不是总线问题,而是我在中断里立即调用发送函数,但连续两帧发送间隔太短,上一帧还没结束,下一帧已经下发了。解决方法是发送前必须检查邮箱空标志,并确认上一帧发送完成,或者使用FDCAN的TX FIFO,让硬件自动排队。

问题二:开启自动重发后,Bus Off恢复时间过长。排查发现ABOM位没有置位,节点Bus Off后硬件不自动恢复,必须软件手动等待。处理方法是初始化时明确设置CAN_ABOM为ENABLE,并配上如下断线自动恢复逻辑:检测CAN_ESR寄存器的BOFF位,置位后清错误状态并重新初始化邮箱。

问题三:自动重发开启情况下,应用层协议(如J1939的传输协议)频繁出现异常。J1939等协议对单帧到达时序有严格要求,自动重发会导致重复帧或乱序。我当时把J1939报文所属的CAN控制器直接禁用自动重发,只对控制类报文单独走另一个通道或改用手工重发。

问题四:FDCAN上改了DAR位但没生效。这个位的修改必须在CCCR的INIT置位期间完成,否则写入被忽略。检查代码时发现我是在HAL_FDCAN_Start之后直接改寄存器,INIT位已经清零,当然没用。正确做法是调用HAL_FDCAN_ConfigRetransmission,或者手动进入Init模式后再写。

6. 常见问题速查表

问题现象自动重发状态排查方向
发送返回成功但接收方没收到开启检查邮箱空标志和TX中断,确认是否还在重发;用USBCAN观察总线错误
干扰后控制报文延迟明显变大开启重发次数过多,考虑关闭自动重发,应用层判断数据时效
关闭后报文频繁丢失,系统不稳定关闭确认应用层是否实现了丢帧重发;否则建议只对控制报文开启
节点进入Bus Off后很久才恢复开启检查ABOM位,开启自动离线恢复
修改NART/DAR位后配置无变化开启/关闭确认是否先进入初始化模式,改完再退出
诊断请求被执行两次开启诊断通道关闭自动重发,或协议层做请求去重

7. 一点补充:如何根据实际项目做最终决定

关于这个开关,我给不了“永远开”或“永远关”的结论,但可以提供一个判断方法:站在接收方的角度看问题。如果你的报文接收方“只认最新值”,比如电机转速、电池电压,这类报文关闭自动重发更合理,迟到的旧值可能把控制系统带偏;如果你的报文接收方“执行一次是一次”,比如继电器吸合、停车指令,这类报文开启自动重发更可靠,漏一次就是事故。

如果项目同时存在问题,那就按通道和ID做策略隔离。CAN控制器支持按FIFO过滤和优先级区分,你完全可以在同一个节点上,让高优先级控制帧走一个通道开启自动重发,诊断报文走另一个通道关闭自动重发。这个方法我用在多个量产项目上,效果稳定,唯一代价就是应用层要按ID管理发送策略,代码逻辑稍微复杂一些。

最后再分享一个我个人的小偏好:凡是需要长期无人值守运行的CAN节点,我倾向于默认关闭自动重发,把重发机制放在应用层,原因是它的错误行为可预测、可观测、可恢复。硬件自动重发虽然省事,但一旦出错,你很难知道它到底空转了多少次。测试时用前面那套故障注入方法,跑一轮数据,比自己纠结该开还是该关更有说服力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询