CAN FD 与经典 CAN 混合组网实战:一开 BRS,老节点就”广播错误帧”——我把混跑这件事从帧格式啃到采样点
老规矩,先说结论:CAN FD 和经典 CAN “物理层兼容”不等于”能混跑”。 只要总线上出现任何一帧 FDF(EDL)位置隐性的 FD 帧,经典节点就会把它判成格式错误,然后主动错误帧一广播,整帧 FD 报文当场作废。所以混跑这件事,第一位的不是”把 FD 跑通”,而是先回答一个问题:这条总线上到底有没有经典节点? 有,就只有两条路——物理隔离,或者全体升级。本文就是我给一套储能设备做”采集通道上 FD”时,把混跑从帧格式、收发器、采样点一路啃到 TDC 的全过程。
一、问题现场:仲裁段好好的,一开 BRS 就开始”下雨”
项目背景很典型:一套储能柜,BMS、PCS、采集网关挂在同一条 CAN 上,仲裁段 500 kbps 跑得好好的。后来电芯数据量上来了,单帧 8 字节不够用,一帧要搬 24 个电芯的电压,只能拆成 6 帧发,总线负载一下冲到 60% 以上,实时性开始扛不住。
于是决定上 CAN FD:仲裁段还是 500 kbps,数据段切到 2 Mbps,单帧直接搬 64 字节。理论收益很清楚,一帧顶过去八帧,负载立马降下来。
板子改完,上电测试——然后就是噩梦:
- 抓包工具里满屏的错误帧,几乎每一帧 FD 报文后面都跟着一串错误标志;
- 老节点(几台只支持经典 CAN 的从机)的错误计数器哗哗往上冲,几分钟就进了 Error Passive,再过一会儿直接 Bus-Off 掉线;
- 而如果我把 BRS 关掉(FD 帧但数据段不加速),情况并没有好转多少——依然有节点报错。
这里有个关键信息,是后来才想明白的:问题不在”2 Mbps 太快”,而在”这条总线上有经典节点”。只要 FD 帧出现在它们面前,快慢都一样会炸。所以第一步,得先把”FD 帧到底哪里动了”这件事说清楚。
二、先把帧格式对齐:FD 到底改了哪几个位
CAN FD 的设计目标之一是”仲裁段尽量兼容经典 CAN”,所以它很克制地只动了控制场那几个保留位,没有推倒重来。把两种帧并排看,改动一目了然:
| 字段 | 经典 CAN | CAN FD | 影响 |
|---|---|---|---|
| SOF / 标识符 | 11 或 29 位 | 同 | 仲裁机制完全一致,可共存竞争 |
| RTR(替代位) | 数据帧=显性,远程帧=隐性 | 变为 RRS,恒显性 | 远程帧在 FD 里实质废弃 |
| r0(保留位) | 显性 | 变为 FDF/EDL:FD 帧=隐性 | 就是这一位,把经典节点逼疯了 |
| r1(保留位) | 显性 | 变为 res,恒显性 | — |
| 新增位 | 无 | BRS(位速率切换)、ESI(错误状态指示) | BRS=隐性才切高速数据段 |
| DLC | 4 位,0~8 → 0~8 字节 | 4 位,9~15 映射到 12/16/20/24/32/48/64 字节 | 一帧最大 64 字节 |
| 数据场 | ≤8 字节 | ≤64 字节 | 带宽收益来源 |
| CRC | 15 位 | 17 位(≤16 字节)/ 21 位(>16 字节) | 更长的载荷需要更强的校验 |
| 填充位计数 | 无 | 新增 SBC(3 位格雷码计数 + 1 位奇偶校验) | ISO 版新增,提升错误检出 |
| ESI 语义 | 无 | 显性=发送方 Error Active,隐性=Error Passive | 接收方可直接看出对端”病了” |
DLC 那行值得单独记住,因为它是”FD 帧没法直译成经典帧”的第二个证据:
| DLC 值 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|
| 数据字节数 | 12 | 16 | 20 | 24 | 32 | 48 | 64 |
写代码时如果 DLC 还是按 len 直接填,长度超过 8 就会填错。正确做法是过一张映射表,后面代码里我会给。
还有个容易忽略的分类:ISO CAN FD 和 non-ISO CAN FD。2012 年 Bosch 最初版 CAN FD 里,CRC 没有”填充位计数”这个字段;2015 年 ISO 11898-1:2015 补上了 SBC 和额外的固定填充位,两者不兼容。好消息是绝大多数控制器都同时支持两种模式,坏消息是如果总线上有人跑 non-ISO、有人跑 ISO,两边会互相判错。所以组网前必须把”跑哪种”当成一条硬约束写进设计文档,别指望默认值一致。
三、关键结论:为什么”经典节点收到 FD 帧”必然出错
这一节是本文最值钱的部分,也是我踩得最深的坑。
经典节点收到 FD 帧时,它的解码器是这么”看”的:
- 读完标识符后,它期待的是 RTR 位。FD 帧在 RTR 位置放的是 RRS=显性,对经典节点来说恰好等于”数据帧”——它没觉得有问题,继续往下走;
- 再往下是 IDE 位,也是兼容的;
- 然后是 r0 位。经典协义规定 r0 是保留位、必须显性,而 FD 帧在这里放的是 FDF=隐性(表示”我是 FD 帧”);
- 经典节点读到”该显性却来了隐性”——立刻判 Form Error(格式错误)或者填充错误;
- 按 CAN 的错误处理规则,检测到错误的节点要主动广播错误帧:6 个连续显性位 + 8 个隐性位,这 6 个显性位故意破坏总线上的电平,让所有节点都知道”出错了”;
- 结果就是:正在发的这帧 FD 报文,被现场搅成一堆错误。发送方的 TEC 每被搅一次 +8,接收方的 REC 也在涨。如果总线上有多个经典节点,错误帧会被重复放大,总线瞬间乱成一锅粥。
所以出现了一个流传很广、但站不住脚的说法:“不切速率就行——只要 FD 帧不发 BRS=1,数据段保持低速,经典节点就能正常接收。”
这句话是错的。 原因就在第 3 步:让经典节点报错的是 FDF/EDL 这一个位本身,跟 BRS 没关系。FD 帧里 FDF=隐性是”声明自己是 FD 帧”,无论后面数据段跑多快、多慢,这个位已经让经典节点判定格式非法了。我用示波器实测验证过:关掉 BRS 之后,错误帧数量确实少了(因为高速段那部分不再产生边沿问题),但错误依然存在,一个都没少根。
更进一步的误区是”换个支持 CAN FD 的收发器/SBC 就能混跑”。这里要把两个概念分清:
| 概念 | 作用 | 能不能解决混跑 |
|---|---|---|
| 收发器支持 CAN FD | 物理层能跟上高速数据段的边沿 | 不能,解决的是”信号完整性”,不是”协议兼容” |
| CAN FD passive(部分 PN 收发器/SBC 特性) | 仅在睡眠/待机等低功耗监听模式下,识别到 FDF=1 就忽略整帧、不累加内部错误计数,从而不被 FD 流量误唤醒 | 基本不能——它管的是”别乱醒”,工作模式下节点照样会报错 |
CAN FD passive 这个特性我特意查了 NXP/TI 的说明,本质是从”选择性唤醒(partial networking)”衍生出来的:在低功耗监听模式下,收发器看到 FD 帧就直接忽略、不递增内部错误计数器,避免”FD 流量把节点从睡眠里炸醒”。TI 那份文档写得很直白——CAN FD passive 只在 sleep 模式启用,节点被正常唤醒、进入工作模式后,它照样参与到总线的正常通信里,该报的错一个不少。所以别指望换个带这个字样的器件就把混跑问题”解决”了。
把这三条串起来,混跑的判据就非常干脆了:
判定规则:某条物理总线(或者说某个广播域)上,只要存在任何一个只能用经典 CAN 收发的节点,这条总线上就不允许出现任何 FDF=1 的帧。
注意”物理总线”这个词——同一条铜线上的广播域,才是问题的边界。这也是下一节架构选型的出发点。
四、三种可行架构,以及我怎么选的
既然”有经典节点就不能有 FD 帧”,那么现实里就只有三条路。我把它们做成一张决策表,方便对着项目情况直接查:
| 架构 | 怎么搭 | 适用场景 | 代价 |
|---|---|---|---|
| A. 双总线 + 网关桥接 | 经典节点一条总线,FD 节点另一条总线,中间用一块网关板(双控制器)做协议转换 | 最推荐,尤其是存量设备不能动的情况 | 多一块网关板、多一路软件转发、转发帧需要”分帧/重组” |
| B. 广播用经典、FD 只走”全 FD 段” | 拓扑上把 FD 节点串成独立的一段,广播/心跳用经典帧;只有”全 FD 节点”之间的点对点通道才切 BRS | 系统里 FD 节点和新节点可以物理分区 | 拓扑受限,收发器仍要与”每一段是否有经典节点”逐条核对 |
| C. 全体升级 | 所有节点换成 FD 控制器 | 新项目、所有节点可控 | 成本和周期最大,但一劳永逸 |
我最后选的是 A。理由很简单:BMS 和 PCS 那几台老设备是甲方的存量设备,一根线都不能动;而网关板我们自己就能设计,升级成本最低。
网关的职责边界必须写清楚,否则会变成”两个网络的错误放大器”:
- 经典段 → FD 段:直接把经典帧原样转发(无损),FD 段接收毫无问题;
- FD 段 → 经典段:这是最关键的一条——大载荷必须分帧。经典 CAN 单帧最多 8 字节,一个 64 字节的 FD 帧直接塞过去,要么被静默截断、要么报错。正规做法是走 ISO 15765-2(CAN-TP) 的分帧/重组;如果不想上 TP,就必须在业务层把单帧约束成 ≤8 字节,从源头避免。
- 两个网络的状态必须分开统计:经典段的 TEC/REC 和 FD 段的错误计数绝不能混成一个数。这一点我在代码里做了硬隔离,后面会讲。
有一件事我特意做过验证:网关的两个控制器,工作模式必须彼此独立。经典段那个控制器跑纯经典模式,FD 段那个跑 FD 模式,千万不要图省事用一个控制器同时”监听两种帧”——FD 控制器的经典兼容模式是用来”收经典帧”的,它收 FD 帧没有任何问题,但反过来经典节点收 FD 帧就是刚才说的灾难现场。
五、就算全是 FD 节点,数据段照样有五个坑
双总线做完,混跑问题解决了。但事情还没完——在纯 FD 段上跑 2 Mbps 数据段,我又踩了一遍坑。这一节的所有数字,我都用一个自己写的小计算器复算过(文末给脚本),你可以对着自己的时钟和波特率直接算。
坑一:只改波特率,不改收发器
这是最经典的一个。我的测试板上一开始装的是沿用经典 CAN 时代的老收发器,表现极其”有迷惑性”:
仲裁段 500 kbps 完全正常,一切正常;数据段一开 BRS,就全乱。
因为仲裁段慢,老收发器带得动;数据段一快,它就跟不上了。问题在于:很多标着”CAN 收发器”的器件,其实只能跑 1 Mbps。
我查了几款常见器件的环回延时(TXD→总线 + 总线→RXD,走一圈),做成一张表:
| 型号 | 标称上限数据段 | 环回延时 | 2 Mbps 下占位时间 | 5 Mbps 下占位时间 |
|---|---|---|---|---|
| TJA1050(二代) | 1 Mbps | 255 ns(上限口径) | 51% | 127% |
| TJA1051(三代) | 5 Mbps | 220 ns(上限口径) | 44% | 110% |
| TJA1042 | 5 Mbps | 225 ns(上限口径) | 45% | 112% |
| TJA1044 | 5 Mbps | 75 ns(典型值) | 15% | 38% |
| TJA1057 | 8 Mbps | 50 ns(典型值) | 10% | 25% |
判读口诀很简单:环回延时占数据位时间的比例,≤1/3 很舒服,≤1/2 需要开 TDC 兜底,>1/2 这个器件就”跟不上”这个数据段了。
看表格最后两列——TJA1050 那种老器件,在 2 Mbps 就已经吃掉一半位时间,在 5 Mbps 直接超过 100%,连一个位都包不住。这就是我”仲裁正常、一开 BRS 就乱码”的物理原因。
注意:这张表里”上限口径”和”典型值”不要混用。半导体手册里 TXD→总线显性和总线→RXD 一般是分两个方向、各给典型值和上限值。选型时按上限值算余量,否则量产的板子会翻车。
顺带说一个好消息和一个坏消息。好消息是收发器是硬件层的事,改起来最便宜(TJA1044 之类的器件很常见、也不贵)。坏消息是物理层也要跟着改:2 Mbps 以上的数据段对桩线、终端、共模扼流圈的要求,比 500 kbps 严得多——我上期在《端接拓扑与点对多点走线策略》里讲的那套东西,在 FD 数据段上是”必须做”,不是”建议做”。
坑二:数据段沿用仲裁段的采样点
采样点是第二个大坑。经典 CAN 上大家都很熟:500 kbps 配 87.5% 采样点,这是行业惯例。于是一个很自然的想法是——数据段也用 87.5% 呗。
不行。 因为采样点的作用是”在信号稳定之后、还要留出足够的时间做重同步”。数据段的位时间短了一个数量级,留给相位缓冲段 2(Phase_Seg2)的时间被压得极扁,87.5% 会直接导致采不到稳定电平。
正确的推荐区间是这样的:
| 应用 | 推荐采样点 | 理由 |
|---|---|---|
| 经典 CAN(≤1 Mbps) | 75%~87.5% | CiA 惯例,兼顾传播延时与重同步余量 |
| CAN FD 仲裁段 | 80%~87.5% | 与经典一致,因为它就是在经典速率下竞争 |
| CAN FD 数据段(2 Mbps) | 70%~80% | 位时间短,要更早采样,给重同步留余量 |
| CAN FD 数据段(5 Mbps) | 60%~70% | 位时间极短,继续提前 |
还有个更硬的要求:同一条总线上所有节点的采样点差异不能大于 5%。超过这个值,会出现”某些数据图样下才报错”的间歇性错误——这种错误极难查,因为它不随温度、不随负载,只随报文内容变化。
以之前那台板子为例:控制器时钟 80 MHz,仲裁段 500 kbps、数据段 2 Mbps,理想的分配是——
仲裁段 500 kbps:BRP=8(tq=100ns),20 TQ/位,TSEG1=15,TSEG2=4 → 采样点 80.0%
数据段 2000 kbps:BRP=2(tq=25ns), 20 TQ/位,TSEG1=14,TSEG2=5 → 采样点 75.0%
注意两行是两套独立参数,必须分开写寄存器。有人图省事把仲裁段的 TQ 分配复制到数据段,采样点就崩了。
一个容易把人绕进去的细节:BRP 这个”分频系数”在不同厂商寄存器里的定义差 1——有的寄存器写进去的
NBRP实际分频是NBRP+1,有的直接就是分频值。这篇里我统一把 BRP 当作分频系数本身(即tq = BRP / f_clock)。对寄存器的时候,一定以自己芯片的手册为准,必要时代进去算一遍tq反推。
坑三:TDC 忘了开,或者 TDCO 拍脑袋
这是数据段最”高级”、也最容易漏的一步:发送延迟补偿(TDC,Transmitter Delay Compensation)。
先讲清楚它为什么需要。CAN 的发送方在发每一位之后,要回读总线(通过收发器的 RXD)确认自己发的位真的上去了。这个回读是有延时的——信号从 TXD 到总线、再从总线回到 RXD,要绕一圈收发器,延时就是上面那张表里的环回延时。
在 500 kbps 下,一位 2000 ns,150 ns 的环回延时不算什么。但在 2 Mbps 下,一位只有 500 ns,环回延时可能超过位时间的一半——等信号回到控制器,它已经”看过”这一位、甚至已经进入下一位了,于是回读和发送对不上,误判成位错误(Bit Error)。
TDC 的解决办法是引入第二采样点(SSP):数据段不用常规采样点,而是等到”信号绕一圈回来、稳定之后”再采。公式很短:
SSP = TDCV + TDCO
- TDCV:控制器自动测量的环回延时(拿 BRS 位的下降沿做基准,测到后面某个确定性边沿),单位是 mtq(控制器最小时间量子);
- TDCO:软件配置的偏移量,作用是把 SSP 推到数据位时间的 70%~90% 这个稳定区间里,避开边沿抖动。
配置思路是”先测量、再回填“:先把 TDCO 设 0,让控制器测出 TDCV,再把 TDCO 设成让 SSP 落在目标位置的值。以一个典型场景举例(控制器时钟 20 MHz、数据段 2 Mbps、目标 SSP 80%):
1 mtq = 1/20 MHz = 50 ns
1 位时间 = 1/2 Mbps = 500 ns
目标 SSP 位置 = 80% → 需要的总位移 = 500 × 80% = 400 ns = 8 mtq
第 1 步:TDCO 先置 0,控制器在数据段自动测出 TDCV
(假设实测环回延时 150 ns → TDCV ≈ 3 mtq)
第 2 步:回填 TDCO = 目标总位移 − TDCV = 8 − 3 = 5 mtq
此时 SSP 落在 400 ns 处,正好是位时间的 80%
合法性:收发器延时 + TDCO = 150 + 5×50 = 400 ns < 6 个数据位时间(3000 ns) ✓
合计 8 mtq ≤ 127 mtq ✓
上面这段是按官方应用笔记整理的通用流程。具体到寄存器名(TDCR / TDCF / SSP 之类)和 mtq 的定义,一定以自己控制器的手册为准——我用的那颗,mtq 就取 CAN 时钟周期,而有的控制器取的是数据段 tq,两者差得不少。
还有两条合法性约束,配错了控制器不报错、但补偿无效:
- 收发器延时 + TDCO 的合计必须小于 6 个数据位时间;
- 合计必须 ≤127 mtq,超了就会被钳到 127 mtq,补偿就不够了。
对应到 STM32 的 FDCAN(HAL 库有现成接口),配置大概是这样的:
/* STM32H7 FDCAN: 仲裁段 500k / 数据段 2M,启用 TDC */
FDCAN_InitTypeDef f = {0};
f.ClockDivider = FDCAN_CLOCK_DIV1;
f.FrameFormat = FDCAN_FRAME_FD_BRS; /* 允许 FD + 位速率切换 */
f.Mode = FDCAN_MODE_NORMAL;
f.AutoRetransmission = ENABLE;
f.TransmitPause = ENABLE; /* 防止连续发送饿死低优先级 */
f.ProtocolException = DISABLE;
f.NominalPrescaler = 8; /* 分频系数(tq=100ns@80MHz) */
f.NominalSyncJumpWidth = 4;
f.NominalTimeSeg1 = 15; /* 采样点 (1+15)/20 = 80% */
f.NominalTimeSeg2 = 4;
f.DataPrescaler = 2; /* tq=25ns */
f.DataSyncJumpWidth = 3;
f.DataTimeSeg1 = 14; /* 采样点 (1+14)/20 = 75% */
f.DataTimeSeg2 = 5;
f.StdFiltersNbr = 1;
f.ExtFiltersNbr = 0;
f.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION;
if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK) { Error_Handler(); }
/* --- 关键:启用发送延迟补偿 --- */
/* 先清零偏移、打开 TDC 让它自动测量 TDCV,再按 SSP 目标位置回填 TDCO */
HAL_FDCAN_ConfigTxDelayCompensation(&hfdcan1, 0, 0);
HAL_FDCAN_EnableTxDelayCompensation(&hfdcan1);
/* 采样若干帧后,读回实测 TDCV,按需要回填 TDCO(= 目标位移 - TDCV) */
uint32_t tdcv = (FDCAN1->TDCR & FDCAN_TDCR_TDCV) >> FDCAN_TDCR_TDCV_Pos;
/* 目标:SSP 落在数据位时间 75~80%;TDCO 单位为 mtq,超 6 个数据位时间要降速率 */
HAL_FDCAN_ConfigTxDelayCompensation(&hfdcan1, tdcv + 20U, 0U);
FDCAN_ConfigTxDelayCompensation() / HAL_FDCAN_EnableTxDelayCompensation() 这组接口是 HAL 里现成的,记得两件事:一是要打开它,二是打开后原采样点会被 SSP 取代,不是叠加。我第一次看到”SSP 要落在 80%、原采样点也在 80%”的时候,脑子里第一反应是”那不就变成 160% 了”——不是这个意思,TDC 开启后就是只用 SSP 比较。
坑四:时钟容差,从 0.5% 收紧到 0.1%
经典 CAN 对节点时钟的要求是 ±0.5%,而且这条约束在 1 Mbps 下还能靠 SJW 撑一撑。FD 的数据段把这根线拉到了 0.1% 量级。
这意味着什么?意味着内部 RC 振荡器、陶瓷谐振器这类”省成本”的时钟源,在 FD 数据段上直接出局。必须用晶振(20 ppm 级别)。0.1% = 1000 ppm,看起来还有很大余量,但别忘了:
- 两个节点的时钟偏差是叠加的;
- 温度漂移、老化都算在里面;
- 数据段位时间只有几百 ns,1% 的偏差就是几 ns——在 2 Mbps 下这已经接近一位的 2%。
所以我的建议是:只要上 FD 数据段,就按”全节点必须用晶振”来设计,别在网上找”内部振荡器也能跑 FD”的说法赌运气。
坑五:工具链和 DBC 跟不上,人会先被吓到
最后这个是”人”的坑,但杀伤力不小。我第一次抓包看到满屏错误帧时,差点直接去改硬件——后来才发现,是分析仪自己只支持经典 CAN,它把 FD 帧全判成错误了。总线其实是好的。
上 FD 之前,这几样东西都要先确认:
- 分析仪/USB-CAN 盒子支持 CAN FD,并且支持你所用的模式(ISO / non-ISO);
- DBC 文件里把每个 FD 帧的长度标对(它可能是 12/16/20/24/32/48/64 字节,不是 8);
- 抓包软件的列里能看到 FDF/BRS/ESI 这些标志位——ESI 特别有用,它是发送方主动声明的”我是不是已经 Error Passive 了”,不用你自己去加起来算;
- 老老实实先开 BRS=0 验证”格式对不对”,再开 BRS=1 验证”速率对不对”。这两步分开做,能省掉你一半的排查时间。
六、代码:把”两个网络、两个真相”分开统计
混跑场景里最忌讳的一件事,是用一个错误计数器去描述两个网络。我写的这套监控逻辑,核心思想就一句话:每个网络一套独立的错误状态,谁也不许污染谁。
先说 FDCAN 里一个非常实用的寄存器设计:它的 Protocol Status Register(PSR)把仲裁段的最后错误码(LEC)和数据段的最后错误码(DLEC)分开存。这一点比经典 bxCAN 强——bxCAN 只有一个 LEC,你没法区分”错误发生在仲裁段还是数据段”。有了 LEC/DLEC,”我一开 BRS 就乱码”这个现象,在寄存器里就是一句话:LEC 正常、DLEC 报错。这一条判据,直接把我从”怀疑控制器”拽回了”怀疑收发器”。
/* 错误监控:按"仲裁段 / 数据段"分开归一,不去猜 */
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint32_t tec; /* 发送错误计数 */
uint32_t rec; /* 接收错误计数 */
bool err_passive; /* RESI: 已进入 Error Passive */
bool bus_off; /* 已脱总线 */
uint8_t last_lec_arb; /* 仲裁段最后错误码 */
uint8_t last_lec_data; /* 数据段最后错误码(只在 FD 帧有意义) */
uint32_t cnt_err; /* 累计错误事件 */
uint32_t cnt_fd_frame; /* 累计收到的 FD 帧 */
uint32_t cnt_cls_frame; /* 累计收到的经典帧 */
} can_health_t;
static can_health_t g_fd_health; /* FD 段(全 FD 节点) */
static can_health_t g_cls_health; /* 经典段(含老节点) */
static const char *lec_name(uint8_t lec)
{
static const char *n[8] = {
"NoErr", "Stuff", "Form", "Ack", "Bit1", "Bit0", "CRC", "SwSet"
};
return n[lec & 0x07U];
}
/* 从 FDCAN PSR 里把两段错误分别抠出来 */
void can_health_sample(FDCAN_HandleTypeDef *h, can_health_t *hlt)
{
uint32_t psr = h->Instance->PSR;
hlt->last_lec_arb = (uint8_t)((psr & FDCAN_PSR_LEC) >> FDCAN_PSR_LEC_Pos);
hlt->last_lec_data = (uint8_t)((psr & FDCAN_PSR_DLEC) >> FDCAN_PSR_DLEC_Pos);
/* RESI=1 表示已进入 Error Passive(协议层降级预警) */
hlt->err_passive = ((psr & FDCAN_PSR_RESI) != 0U);
/* BO 位表示 Bus-Off */
hlt->bus_off = ((psr & FDCAN_PSR_BO) != 0U);
FDCAN_ErrorCountersTypeDef ec;
if (FDCAN_GetErrorCounters(h, &ec) == HAL_OK) {
hlt->tec = ec.TxErrorCnt;
hlt->rec = ec.RxErrorCnt;
}
/* BO 期间要主动恢复:置 INIT 位再清,等价于一次软复位 */
if (hlt->bus_off) {
h->Instance->CCCR |= FDCAN_CCCR_INIT;
h->Instance->CCCR &= ~FDCAN_CCCR_INIT;
hlt->cnt_err++;
}
}
配合通信中断里的错误回调,把”错误发生在哪个阶段”直接落到日志:
/* 错误通知回调:FDCAN 把"仲裁段错误"和"数据段错误"分成两类事件 */
void HAL_FDCAN_ErrorStatusCallback(FDCAN_HandleTypeDef *hfdcan,
uint32_t ErrorStatusITs)
{
if ((ErrorStatusITs & FDCAN_IT_DATA_PROTOCOL_ERROR) != 0U) {
/* 数据段出错 —— 高度怀疑收发器跟不上 / TDC 没配好 */
g_fd_health.cnt_err++;
can_health_sample(hfdcan, &g_fd_health);
return;
}
if ((ErrorStatusITs & FDCAN_IT_ARB_PROTOCOL_ERROR) != 0U) {
/* 仲裁段出错 —— 高度怀疑终端电阻 / 共模电压 / 波特率不匹配 */
g_fd_health.cnt_err++;
can_health_sample(hfdcan, &g_fd_health);
return;
}
if ((ErrorStatusITs & FDCAN_IT_BUS_OFF) != 0U) {
can_health_sample(hfdcan, &g_fd_health); /* 内部会走恢复流程 */
}
}
再往上,把状态打包成能被远程读到的数字(JSON,整数运算,不碰浮点、不碰 %f):
/* 健康上报:整数运算,零 malloc,禁用 %f —— 组帧前先做定点/整数化 */
static int fmt_u32(char *p, uint32_t v) /* 简易无符号十进制 */
{
char t[11]; int n = 0, i;
if (v == 0U) { p[0] = '0'; return 1; }
while (v > 0U && n < 10) { t[n++] = (char)('0' + (v % 10U)); v /= 10U; }
for (i = 0; i < n; i++) { p[i] = t[n - 1 - i]; }
return n;
}
/* 输出形如:{"fd":{"tec":0,"rec":0,"ep":0,"bo":0,"lecarb":0,"lecdata":5},...}
* lecdata=5(Bit0) 且 lecarb=0 → 判据:数据段位错误,查收发器 / TDC */
void can_health_report(char *out, size_t cap)
{
/* 实现在此略去字符串拼接细节;要点是:
* 1) 两个网络各自出字段,永不相加;
* 2) 只上报"能定性的数字",不上报浮点;
* 3) 一个字段一个含义,LEC 用编码值,不上传字符串。 */
(void)out; (void)cap;
}
这份状态一上云,判读就变成三条规则,不需要工程师到现场:
| 现象 | 指向 |
|---|---|
ep/bo 上升,且 lecdata 报 Bit0/Bit1 |
数据段问题:收发器太慢、TDC 没配、采样点不对 |
lecarb 报 Stuff/Form/Ack,lecdata 正常 |
仲裁段问题:终端电阻、共模电压、波特率、桩线 |
lecarb 报 Form/Stuff,且这条总线上有经典节点 |
有人在往经典段发 FD 帧,回头查网关过滤 |
七、六个实测坑(速查)
把上面所有内容压成一张速查表,方便对着自查:
| # | 现象 | 根因 | 处置 |
|---|---|---|---|
| 1 | 一开 BRS 就乱码,关掉就正常 | 收发器不支持该数据段速率 | 换 FD 收发器(环回延时 ≤1/3 位时间),按上限值算余量 |
| 2 | 2 Mbps 下间歇性丢帧,随报文内容变化 | 数据段采样点沿用仲裁段的 87.5% | 数据段改 70%~80%;全网节点采样点差 ≤5% |
| 3 | 单机自发自收正常,多节点上线就位错误 | 未启用 TDC | 打开 TDC,先测 TDCV 再按 SSP 70%~90% 回填 TDCO |
| 4 | 高频误码,常温偶发,高温必错 | 时钟容差不足(用了内部 RC / 陶瓷谐振) | 换晶振,FD 数据段按 0.1% 量级设计 |
| 5 | 抓包满屏错误帧,但总线实际是好的 | 分析仪/工具不支持 FD,或 DBC 长度标错 | 先确认工具支持 FD 与 ISO/non-ISO 模式,DBC 长度改对 |
| 6 | 网关转发后经典段疯狂报错 | 64 字节 FD 帧直接塞进经典段(>8 字节被截断/报错) | 上 CAN-TP 分帧重组,或业务层约束单帧 ≤8 字节 |
有一条我想单独强调:坑 1 和坑 3 经常同时出现。收发器跟不上会引发数据段位错误,而位错误又会被误认为”TDC 没配好”。所以排查顺序应该是先换对硬件,再配 TDC——反过来做,你会陷入”配了 TDC 还是错、怀疑 TDC 没用”的死循环。
八、混合网络落地自查清单
混跑和 FD 数据段这两件事,按这张单子过一遍再发板发固件:
- ☐ 已把”每个广播域里有哪些节点、各自支持经典/FD”列成表格,写进设计文档
- ☐ 已确认:任何含经典节点的广播域内,不存在 FDF=1 的帧(含网关转发路径)
- ☐ 已明确 ISO CAN FD 还是 non-ISO CAN FD,且全网一致(包括抓包工具)
- ☐ 所有 FD 节点已换用支持目标数据段速率的收发器,且按手册上限值核算环回延时
- ☐ 收发器环回延时占数据位时间 ≤1/3(最差 ≤1/2),否则必须启用 TDC
- ☐ 仲裁段采样点 80%~87.5%、数据段采样点 70%~80%,两段参数分开配置
- ☐ 全网节点采样点差异 ≤5%(用计算器逐节点复算过,不是抄参考设计)
- ☐ 数据段速率 ≥2.5 Mbps 时,TDC 已启用且 TDCO 按 SSP 70%~90% 回填
- ☐ 已复核”收发器延时 + TDCO < 6 个数据位时间”且 “≤127 mtq”
- ☐ 所有 FD 节点使用晶振(不依赖内部 RC / 陶瓷谐振),时钟容差按 0.1% 量级评估
- ☐ 物理层按高速数据段标准处理:终端 120Ω、CANH/CANL 等长、桩线尽量短、共模扼流圈就位
- ☐ 网关的 FD→经典方向已做分帧(CAN-TP)或单帧 ≤8 字节约束
- ☐ 抓包工具支持 FD 且能显示 FDF/BRS/ESI,DBC 长度已按 12~64 字节更新
- ☐ 固件侧经典段与 FD 段的错误计数、状态机完全隔离,且能分别上报
- ☐ 已用 PSR 的 LEC / DLEC 分别验证过”仲裁段正常、数据段正常”,而不是只看一个总错误数
- ☐ 已做过”仅发经典帧 / 仅发 FD 帧 / 混发”三组总线负载与错误率对照测试
九、写在最后
这套东西做完,最大的感受是:CAN FD 的坑,八成不在”FD 有多快”,而在”它和经典 CAN 的关系没想清楚”。
回头看,那次”满屏错误帧”的现场,真正把我困住的不是硬件,而是两个想当然的认知:
第一个是“物理层兼容就等于能混跑”。CAN FD 很聪明地把仲裁段保留了、只动了几个保留位,于是很容易让人觉得”它就是经典 CAN 的加强版,插上去就能用”。但恰恰是那个表示”我是 FD 帧”的 FDF 位,成了经典节点的”死亡开关”。物理兼容,协议不兼容——这两件事必须分开谈。
第二个是“BRS 关了就能兼容”。这个说法流传太广了,我从网上看到过、从同行的经验里听到过。实测下来,错的就是错的:FD 帧之所以是 FD 帧,靠的是 FDF 位,不是 BRS 位。想要兼容,只有”不发 FD 帧”这一条路。
而真正让我省时间的两件事,都不是改硬件:一个是把”每个广播域有没有经典节点”当成架构级的硬约束先定下来,它直接决定了后面所有设计(双总线、网关、分帧);另一个是用 PSR 的 LEC/DLEC 把”错误发生在哪个阶段”分离开,这一条判据把”怀疑控制器”的弯路直接掐掉了。
顺带说个更现实的体会:CAN FD 这波升级,最难的往往不是新设备,是那些”不能动的老设备”。存量系统里,很多时候你没法让所有节点一起换。这时候双总线 + 网关这条路虽然要多写一点转发逻辑,但它把”老设备不用动”这件事保住了——这在工程上比”设计得多优雅”重要得多。
相关阅读:
- CAN 总线错误帧排查实战:一条”隔几小时报一次总线-off”的产线设备
- RS485/Modbus 抗干扰实战:一条”隔几小时丢一帧”的产线总线
- 端接拓扑与点对多点走线策略实战:一条挂了 6 片器件的共享时钟
- 信号完整性与串阻源端匹配实战:一条 25MHz 时钟线上的振铃
附:本文用到的位时序/延迟补偿计算器已放在草稿目录 drafts/_tools/canfd_timing.py,纯 Python 零依赖。用法:python canfd_timing.py dual --clock 80 --arb 500 --data 2000(双相位反解)、tdc --clock 80 --data 2000 --loopdelay 150(TDC/SSP)、budget --data 2000 --loopdelay 150(收发器预算)、table(常见收发器对照)。