ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CAN FD 双比特率与 MCP2518FD 位定时、TDC、FIFO 过滤器

CAN FD 双比特率与 MCP2518FD 位定时、TDC、FIFO 过滤器

简介:《CANFD芯片MCP2518数据手册》是一份面向汽车电子、工业自动化与嵌入式开发者的中文数据手册,适合需要为单片机扩展CAN FD通道、核对MCP2518FD寄存器与接口设计的工程师和学生查阅。压缩包共1个PDF文件,约1.48MB,内容涵盖芯片特性、引脚分配、典型应用、框图、FIFO与滤波器配置、SPI通信、低功耗模式、封装及电气参数等,可帮助读者理解CAN 2.0B与CAN FD混合模式下的报文收发、时间戳、中断和健康诊断机制。已有7032人学习下载,便于在方案选型、硬件原理图设计、驱动调试和问题排查时快速定位关键参数,也可作为对照官方手册梳理CAN FD控制器知识点的参考资料。

1. 结论:CAN FD 的「快」不在时钟,而在一帧里的两套时间线

不少人第一次接触 CAN FD,直觉是「把位时钟拉到 2 Mbps 就完事了」。把 CANFD 芯片 MCP2518FD 的中文手册翻到帧格式那页会发现不是这么回事:同一帧里仲裁段仍跑 1 Mbps,进数据段才切到 8 Mbps,靠 BRS 位开关,切过去还要切回来。这解决的是一个实际矛盾——仲裁要所有节点共用同一时间基准才能比出谁先发,数据搬运又要尽量高的带宽。

搞懂这条时间线为什么必须存在、切换点由谁控制、收发器延迟怎么补,比记住 8 Mbps 这个数字有用。后面从位定时寄存器拆到 FIFO 与过滤器,把手册里像寄存器清单的部分还原成设计取舍。

2. 双比特率:仲裁段 1 Mbps、数据段 8 Mbps 是怎么被切开的

2.1 从帧结构看:BRS 位为什么是整帧的分界线

CAN FD 帧前半段和经典 CAN 几乎一样:帧起始、11 位或 29 位 ID、RRS、IDE,然后 FDF 位把 FD 帧和经典帧区分开。过了 FDF 进入控制段,依次是保留位、BRS、ESI、DLC,BRS 就是那条分界线。BRS 采到隐性,从它之后的位开始切换到数据比特率;采到显性,整帧老老实实走标称速率。数据段结束、CRC 界定符之前,还有一次切回标称速率的动作。

仲裁段不能加速,根子在物理层。仲裁靠「显性覆盖隐性」的线与机制,多个节点同时开口时,谁发显性谁留下。一个位时间内,信号必须从发送节点出发,走完整条总线,再回到每个接收节点的采样点并稳定下来。1 Mbps 时位时间是 1 微秒,40 米总线往返传播就要 400 纳秒上下,再叠上收发器环路延迟和振荡器容差,余量已经很薄。仲裁段若也提到 8 Mbps,位时间只剩 125 纳秒,物理上根本立不住。

数据段只有一个节点在发,没有仲裁,接收方只要在自己的采样点采到稳定电平即可,位时间就能大幅压缩。代价是对时钟偏差和收发器延迟变得极度敏感,这也是后面必须引入 TDC 的原因。

2.2 两套独立的位定时寄存器:C1NBTCFG 与 C1DBTCFG

MCP2518FD 给了两个完全独立的位定时寄存器,标称段一套、数据段一套,互不干扰。字段位宽本身就是一条线索:

字段C1NBTCFG(标称段)C1DBTCFG(数据段)作用
BRP8 位8 位CAN 时钟预分频
TSEG18 位5 位传播段 + 相位缓冲 1
TSEG27 位4 位相位缓冲 2
SJW7 位4 位重新同步跳转宽度

标称段的 TSEG1 有 8 位、TSEG2 有 7 位,能表达几百个时间份额;数据段的 TSEG1 只剩 5 位、TSEG2 只剩 4 位。原因很直接:数据段位时间被压到极短,一个位时间里能塞下的时间份额本来就少,用 8 位字段是浪费硅面积。这个裁剪反过来也是一种保护——它让数据段的位时间不可能被配置得过长。

按常规公式,位时间等于(1 + TSEG1 + TSEG2) × tq,采样点位置等于(1 + TSEG1) / (1 + TSEG1 + TSEG2),tq 由 BRP 对 CAN 时钟分频得到。下面这段只演示位域怎么拼装,具体数值要用手册公式按实际时钟核算:

/* 40 MHz CAN 时钟,标称段目标 1 Mbps(位时间 40 tq),采样点约 80% */ #define C1NBTCFG_1M_80PCT ((0u << 24) | /* BRP:分频值,编码方式以手册为准 */ \ (31u << 16) | /* TSEG1 = 31 个 tq */ \ (8u << 8) | /* TSEG2 = 8 个 tq */ \ (8u << 0)) /* SJW = 8 个 tq */ /* 同一路时钟,数据段目标 8 Mbps(位时间 5 tq),采样点约 80% */ #define C1DBTCFG_8M_80PCT ((0u << 24) | /* BRP */ \ (3u << 16) | /* TSEG1 = 3 个 tq */ \ (1u << 8) | /* TSEG2 = 1 个 tq */ \ (1u << 0)) /* SJW = 1 个 tq */ static void can_set_bitrate_regs(uint32_t nbtcfg, uint32_t dbtcfg) { /* 关键顺序:先进配置模式,等 OPMOD 回读一致,再动位定时 */ c1con_reqop(0x4); /* REQOP = 100,配置模式 */ while ((C1CON & 0xE0u) != 0x80u) /* OPMOD 回读为配置模式才继续 */ ; C1NBTCFG = nbtcfg; C1DBTCFG = dbtcfg; c1con_reqop(0x0); /* 回正常 CAN FD 模式 */ }

代码里最值得盯的不是数值,而是那两行等待。位定时寄存器只在配置模式下可写,写REQOP只是「请求」,真正生效要等OPMOD回读一致。跳过这个握手直接写,寄存器值会静默丢弃,现象是总线上完全没反应,查半天以为是硬件问题。

2.3 TDC:收发器环路延迟为什么必须补偿

8 Mbps 时一个位时间是 125 纳秒。高速 CAN FD 收发器从 TX 引脚到 RX 引脚的环路延迟,量级通常在几十到一百多纳秒,这个数字足以吃掉一个位时间甚至更多。发送节点在数据段需要回读自己发出的位来做错误检测,一旦环路延迟超过一位,回读到的就是「上一位」的值,位错误标志立刻被点亮,错误计数飞涨,节点很快被踢进错误被动。

C1TDC 就是为这件事准备的。它包含TDCMOD<1:0>(TDC 模式)、TDCO<6:0>(偏移量)、TDCV<5:0>(测得的延迟值,只读),外加EDGFLTEN(边沿滤波使能)。工作方式是:在数据段开始时测量发送到接收的实际延迟,把二次采样点(SSP)从默认位置往后推这段延迟,保证回读落在正确的位置上。自动模式下这颗芯片自己测、自己补;手动模式下软件写TDCO。

为什么仲裁段不用 TDC?1 Mbps 位时间 1 微秒,延迟占比太小,采错的风险可以忽略;数据段位时间缩到 125 纳秒,同一个延迟就变成致命量级。同一颗芯片里两套时间线配两套补偿策略,逻辑上是自洽的。

提示:TDC 的自动测量依赖数据段真的发出过长报文。如果现场全是 8 字节以内的短帧,数据段停留时间太短,测量样本可能不足,建议先用长帧跑一遍让 TDC 收敛,再回到业务流量。

2.4 模式切换与发送带宽共享

C1CON 里的REQOP<2:0>是请求、OPMOD<2:0>是回读,可取的值覆盖正常 CAN FD、正常 CAN 2.0、休眠、仅监听、配置、内部环回、外部环回、受限工作。改任何与协议时序相关的配置,都要走「请求 → 回读 → 写 → 请求 → 回读」这个闭环。

同一个寄存器里还有TXBWS<3:0>,发送带宽共享。它限制这颗控制器在总线高负载时抢占发送的份额,避免一个节点用高优先级 ID 把总线占死。这个字段在只跑几帧的台架上完全看不出作用,一到真实车载网络里,就是「某个节点一上电整条总线抖动」这类玄学问题的分水岭。

3. FIFO、TXQ 与 TEF:31 个 FIFO 之外为什么还要单造一个队列

3.1 FIFO 的头尾指针与用户地址:C1FIFOUA 到底指向哪

这颗控制器有 31 个 FIFO,每个都能独立配置成发送或接收,配置全在C1FIFOCONx里,状态在C1FIFOSTAx,数据入口是C1FIFOUAx。三件套各管一件事:CON管「这个 FIFO 多大、装什么、怎么发」,STA管「现在轮到第几个报文对象、有没有溢出、有没有中止」,UA直接给出「下一个该读或该写的 RAM 地址」。

C1FIFOCONx的关键位域:FSIZE<4:0>定这个 FIFO 装几个报文对象,PLSIZE<2:0>定每个对象的数据区大小(2 的幂),TXEN决定方向,TXREQ是发送请求,UINC是头尾指针递增,FRESET把 FIFO 清空,TXPRI<4:0>是发送优先级,TXAT<1:0>是重发尝试次数。C1FIFOSTAx里的FIFOCI<4:0>告诉你当前指针落在第几个对象上,RXOVIF是溢出标志,TXERR/TXABT/TXLABT对应发送出错、中止、仲裁丢失。

UINC是最容易被忽略的一位。软件从 RAM 里读完一个接收对象后,必须写一次UINC让尾指针前移,否则下次读到的还是同一块数据。这个动作不是「刷新缓存」,它是 FIFO 的出队操作,漏写一次,接收队列就永远卡在第一个报文上。

/* 把 FIFO1 配成发送 FIFO:8 个报文对象,每个对象 8 字节数据区 */ #define FIFO1_CON_INIT ((3u << 29) | /* PLSIZE = 3 -> 2^3 = 8 字节 */ \ (3u << 24) | /* FSIZE = 3 -> 4 个对象(编码为对象数减一) */ \ (1u << 7)) /* TXEN = 1,发送方向 */ static void fifo1_send(const uint8_t *payload, uint8_t dlc) { volatile uint32_t *ua = (uint32_t *)C1FIFOUA1; /* 下一个可写对象地址 */ write_canfd_object(ua, payload, dlc); /* 写 ID/数据/控制字 */ C1FIFOCON1 |= (1u << 9); /* TXREQ = 1,触发发送 */ C1FIFOCON1 |= (1u << 8); /* UINC = 1,头指针前移 */ }

这段里FSIZE和PLSIZE的编码方式直接决定 RAM 占用,写错不会报错,只会在收发数据时对不上号。TXREQ和UINC的顺序也有讲究:先请求发送,再推指针,否则第二个报文会覆盖掉第一个还没被硬件取走的数据。

3.2 TXQ:以 ID 最小为出队规则的硬件优先队列

普通发送 FIFO 是严格先进先出,谁先写进去谁先发。真实车载网络里经常出现另一种需求:同一时刻攒了七八帧待发,希望 ID 数值小的先出去,因为小 ID 通常代表更高优先级的事件。软件排序当然能做,但那意味着每次入队都要遍历一遍、搬一次内存,中断上下文里做这件事很别扭。

TXQ 就是把这个排序搬进硬件。它是唯一的、特殊的发送 FIFO,有独立的C1TXQCON/C1TXQSTA/C1TXQUA,出队规则不看写入顺序,只看报文 ID 数值,最小的先走。软件只管往队列里塞,硬件负责摘。

发送优先级其实是两层叠加的。第一层是各发送 FIFO 之间的TXPRI<4:0>,数值决定谁先拿到发送机会;第二层才是 TXQ 内部的 ID 排序。只配了 TXQ 却忘了给其他发送 FIFO 设TXPRI,两层规则就会打架,现象是高优先级帧偶尔被低优先级帧压在后面,抓包看时序完全没规律。

3.3 TEF 与 23 位 SEQ:发送完成的「后悔药」怎么留痕

报文发出去之后,软件其实并不知道它什么时候真的上了总线。TXREQ被清掉只说明硬件取走了数据,仲裁输没输、有没有重发、最终哪一刻发成功,都得另找地方对账。TEF(发送事件 FIFO)就是这个账本:每成功发送一帧,就往 TEF 里写一条事件记录,包含报文 ID、发送时刻的 32 位时间戳,以及一个序列号。

这颗芯片相对前代的一个改动,是把报文对象和 TEF 对象里的SEQ字段从 7 位扩到 23 位。7 位只能数到 127,一轮密集发送很快就回绕,软件分不清「这是第 3 帧」还是「这是第 131 帧」。23 位能数到八百多万,足够覆盖绝大多数诊断和标定场景的连续发送窗口。配合 32 位自由运行时基计数器(C1TBC)打的时间戳,一条事件记录就能回答「哪一帧、什么时候、发没发成」。

/* TEF 事件对象在报文 RAM 中的字段示意,具体偏移以手册为准 */ struct tef_event { uint32_t sid; /* 标准 ID 位段 */ uint32_t eid; /* 扩展 ID 位段 */ uint32_t seq; /* 23 位发送序列号,软件写入、硬件回填 */ uint32_t tbc; /* 发送时刻的 32 位时基计数 */ uint8_t data[8]; /* 与发送报文前 8 字节一致,便于比对 */ };

C1TEFCON里的TEFTSEN决定是否给事件打时间戳,FSIZE<4:0>决定事件 FIFO 深度,TEFOVIE/TEFFIE/TEFHIE/TEFNEIE分别是溢出、满、半满、非空中断。C1TEFSTA的TEFOVIF一旦置位,说明有事件被覆盖丢失,对账就断了。这类溢出在调试阶段几乎必现——中断没及时清、TEF 深度又给小了,血泪经验是先把深度开够再谈优化。

4. 过滤器与屏蔽器:32 个对象怎么在一次比较里完成分流

4.1 C1FLTOBJ 与 C1MASK:EID/SID 位域为什么这样交错

32 个过滤器对象,每个配一个屏蔽器。C1FLTOBJx存「要匹配成什么样」,C1MASKx存「哪些位参与比较」。屏蔽器置 1 的位必须和过滤器对象一致才算命中,置 0 的位随便。

这两个寄存器的位域排布很有意思:标准 ID 的 11 位和扩展 ID 的 18 位被拼进同一个 32 位字里,EXIDE位决定按哪种帧格式解释。当EXIDE = 1,走扩展帧,11 位 SID 加 18 位 EID 拼成完整的 29 位标识符;当EXIDE = 0,走标准帧,SID 那 11 位照常匹配,而原本放 EID 的那段位宽被挪作他用。

这个「一段硬件两种解释」的做法不是省寄存器那么简单。它让同一套过滤器阵列既能按 ID 分流,也能按数据内容分流,硬件复用率拉满,代价是软件配置时要非常清楚当前对象处于哪种解释模式。配错EXIDE,过滤器不报错,只是永远匹配不上,接收 FIFO 一直是空的。

4.2 标准 ID + 前 18 个数据位:CAN FD 特有的过滤维度

手册里明确写了,标准帧模式下过滤器可以匹配「标准 ID + 前 18 个数据位」。这个能力在经典 CAN 时代基本没人用,到 CAN FD 才变得有意义。

原因在载荷长度。CAN FD 一帧最多 64 字节,很多上层协议会把「信号标识」放在数据区最前面两个字节,后面才是真正的采样值。如果没有数据位过滤,所有帧都涌进同一个接收 FIFO,单片机被中断叫醒之后还得逐帧拆包、判断这是哪路信号,再分发到不同缓冲区。CPU 做这件事的开销,在 8 Mbps 满载时相当可观。

有了前 18 位过滤,分流动作前移到硬件:ID 加数据前缀一起参与匹配,命中哪个过滤器就进哪个 FIFO。软件这边变成「读 FIFO 就知道是什么信号」,中断服务程序可以写得很短。18 位这个数字不是随便定的——它刚好是标准帧 ID 之外剩余的位宽,把硬件位段用满,一个比特都不浪费。

4.3 时间戳与中断:32 位 TBC 与三个中断引脚的取舍

C1TBC是 32 位自由运行时基计数器,C1TSCON里的TBCPRE<9:0>是 10 位预分频,TBCEN使能,TSRESET复位,TSEOF决定是否在帧起始时刻锁存。40 MHz CAN 时钟、预分频取 1 时,时间戳分辨率是 25 纳秒。接收报文可以带时间戳,TEF 里的发送事件也可以带,两边共用一个时基,软件算收发时延不用做时钟域换算。

中断这块,芯片给了 INT、INT0、INT1 三个引脚,INT0/INT1 还能配成普通 GPIO,IOCON里的PM1/PM0控制推挽还是开漏,INTOD/TXODSOF分别控制中断引脚和 TXCAN/SOF 引脚的开漏模式。C1INT是一整排中断使能位,C1VEC里的ICODE<6:0>告诉软件当前是哪个中断源,FILHIT<4:0>直接给出命中的过滤器编号,RXCODE<6:0>/TXCODE<6:0>给出具体的 FIFO 编号。

这套设计的思路是「少量引脚 + 代码寄存器」:三个引脚不可能一对一映射到 31 个 FIFO,所以中断服务程序进来第一件事是读C1VEC,靠ICODE和FILHIT定位,再决定读哪个 FIFO。相比每个 FIFO 一根中断线,引脚数从三十多根压到三根,代价是中断响应多了一次寄存器读。在 SPI 接口本来就要读寄存器的前提下,这个代价几乎为零。

5. 避坑与常见问题排查:五个把我按在地上摩擦的现场

5.1 现象:SPI 读回全是 0xFF 或全 0x00

现象很直白,上电后读C1CON或OSC寄存器,返回不是全 1 就是全 0,换个地址结果一样。

原因通常有三层。最外层是片选和时钟极性:这颗芯片只支持 SPI 模式 0,0 和 1,1,模式 2 或 3 下数据完全错位。中间层是上电时序:内部 LDO 和 POR 还没稳,OSC寄存器里的OSCRDY、SCLKRDY、PLLRDY三个就绪位没置起来,此时访问其他寄存器没有意义。最内层是 SFR 的字节序,32 位寄存器低字节在低地址,如果按大端发,读回来的值高低字节是反的,看起来像乱码。

解决办法是先只读OSC寄存器,轮询那三个就绪位全部置 1,再去碰别的寄存器;SPI 初始化时把模式固定成 0,0;字节序按小端逐字节发。这三步做完,读回值基本就正常了。

5.2 现象:位定时改了但总线上速率没变

写C1NBTCFG和C1DBTCFG之后示波器一测,位宽还是老样子,寄存器读回来却是新值。

原因是没有进入配置模式。这两个寄存器只在OPMOD回读为配置模式时可写,直接写会被硬件丢弃,但 SPI 写操作本身不报错,读回来甚至可能返回写入值,形成「写成功但没生效」的假象。另一个变种是写了REQOP = 100却没轮询OPMOD,配置模式还没切过去就接着写位定时。

解决就是老老实实走握手:写REQOP,循环读C1CON直到OPMOD那三位等于 100,再写位定时,最后写回正常模式并再等一次。这段等待加个超时保护,超时就报错而不是死循环。

5.3 现象:一切到 8 Mbps 数据段就进错误被动

标称段 1 Mbps 收发正常,数据段一开到 8 Mbps,错误计数器往上窜,节点很快进错误被动,严重时总线直接关闭。

原因几乎都出在数据段的时间余量上。一是采样点位置不对,8 Mbps 下位时间只有 5 个 tq,采样点稍微偏一点就采到边沿;二是没开 TDC,收发器环路延迟超过一个位时间,发送节点回读自己的位时采错;三是收发器本身跟不上,标称支持 5 Mbps 的器件硬跑 8 Mbps,波形已经塌了。

解决顺序是:先把数据段降到 5 Mbps 验证链路,确认没问题再往上走;打开 TDC 自动模式,让芯片自己测延迟;采样点收到 75% 到 80% 之间;最后拿示波器看总线波形,眼图张开度不够就换收发器或缩短总线。

5.4 现象:接收 FIFO 里数据错位、DLC 对不上

读出来的报文 ID 是对的,数据区内容却像错了一段,DLC 也和发送端不一致。

原因在FSIZE/PLSIZE和 DLC 的配合。PLSIZE决定每个报文对象在 RAM 里占多大,实际数据长度由 DLC 决定,如果PLSIZE配的比实际 DLC 小,硬件会截断;配得太大则浪费 RAM,还可能让相邻对象地址算错。另一个高频原因是UINC漏写——读完一个对象没推尾指针,下次读到的还是同一块内存,看起来像「数据没更新」。

解决办法是按最长的业务帧倒推PLSIZE,把 31 个 FIFO 的 RAM 占用加起来核一遍总量别超 2 KB;读接收 FIFO 的代码里,UINC写在读完数据之后、退出中断之前,一个对象一次,不多不少。

5.5 现象:发送挂死,TXREQ 一直是 1 不清零

写TXREQ之后这一位赖着不走,TEF 里没有新事件,中断也不来。

原因有几种:TXEN没置 1,FIFO 根本没被配成发送方向;总线没接终端电阻或没有其他节点应答,帧发不出去一直重试;C1FIFOSTA里的TXERR已经置位,但错误中断被屏蔽了,软件不知道;TXAT配成禁止重发后仲裁一输就中止,TXABT亮了没人清。

排查路径是顺着状态位走:先读C1FIFOSTA看TXERR/TXABT/TXLABT,再看C1TREC里的TEC/REC和TXBO/TXBP/RXBP,最后确认总线物理层。软件侧则要保证错误中断不被整体屏蔽,TXATIF这种标志读一次清一次,否则中断会反复触发把 CPU 拖死。

6. 设计取舍:2 KB 报文 RAM 与 32 位寄存器布局背后的账

把手册翻到最后会发现,这颗控制器的所有「不够用」都是算过账的。2 KB 报文 RAM 听着不少,但 31 个 FIFO 加上 TXQ、TEF 共享这块空间,每个报文对象除了数据区还有 ID、控制字、时间戳、序列号这些头部开销,按常见实现估算每个对象 8 字节头,一个装 64 字节数据的对象要占 72 字节。32 个这样的对象就是 2304 字节,超了。

PLSIZE 编码单对象数据区单对象占用(按 8 字节头估算)2 KB 最多能放
38 字节16 字节128(受 FIFO 数量上限约束)
532 字节40 字节51
664 字节72 字节28

所以真实项目里不可能「全开最大」,必须按用途分配:几路高实时性的开 8 字节深队列,一两路大数据量的开 64 字节浅队列,TEF 留出足够深度防止溢出。这张预算表比任何配置代码都先动手,配错了后面全是玄学。

另一个取舍是寄存器组织。SFR 统一 32 位宽、低字节在低地址,配合最高 20 MHz 的 SPI,一次突发能搬 4 字节,地址连续时吞吐很可观;代价是所有位域都得按 32 位对齐摆放,小字段被塞进大字的边角。CRC 命令和 ECC 保护的 RAM 也是同一个思路——安全关键场景里,SPI 走线长、RAM 可能被翻转,宁可多花几个寄存器做校验,也不让错误悄悄传到总线上。

从那以后我每次拿到一颗新的 CAN FD 控制器,都先做两件事:画一张 RAM 预算表,把每个 FIFO 的对象数和数据区大小写死;再写一段只读OSC就绪位的上电自检。这两步走过去,后面调位定时和过滤器才有意义。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进