4. CAN FD物理层

好,咱们今天聊点实在的。CANFD的报文收发,说白了就是MCU和总线之间怎么“递纸条”。你写好了数据,怎么塞给CAN控制器?总线上一堆报文飞过来,你怎么只拿自己想要的?

我刚开始做车载项目那会儿,总觉得收发就是调个API完事。结果有一次,报文发不出去,查了三天,最后发现是TxBuffer的优先级配错了。嗯,从那以后,我再也不敢小看这个流程了。

4.1 发送流程:TxFIFO与TxBuffer

CANFD的发送,核心就两个机制:TxFIFO 和 TxBuffer。它们都是你放待发报文的地方,但用法完全不同。

特性 TxFIFO TxBuffer
存储结构 先进先出队列 独立邮箱槽位
发送顺序 严格按写入顺序 按优先级(ID或槽位号)
应用场景 周期报文、诊断流 高优先级事件报文
覆盖风险 满时丢最新报文 满时丢最旧报文(可配置)

TxFIFO 的发送流程:

  1. 应用程序将报文写入FIFO的写入指针位置。
  2. 硬件自动将FIFO内容搬运到发送移位寄存器。
  3. 总线空闲时,移位寄存器将报文逐位发送到CAN总线。
  4. 发送完成后,硬件更新FIFO的释放指针。

TxBuffer 的发送流程:

  1. 应用程序选择一个空闲的TxBuffer槽位,写入报文。
  2. 硬件根据每个TxBuffer的优先级(通常是报文ID越小优先级越高)进行仲裁。
  3. 优先级最高的TxBuffer内容被送入发送移位寄存器。
  4. 发送完成后,该TxBuffer状态变为空闲,触发中断。

我的经验:我个人习惯把周期报文(比如车速、转速)放在TxFIFO里,因为顺序固定,好调试。而紧急报文(比如安全气囊触发、故障快照)必须用TxBuffer,并且把它的CAN ID设得很小,确保它总能抢到总线。

避坑指南:我曾经在一个项目中,把TxFIFO深度设成了2,结果诊断刷写时连续发报文,FIFO满了,后面的报文直接丢了。后来我改成深度8,并且加了软件队列做缓冲,问题才解决。记住:TxFIFO满了不会阻塞,它只会静默丢弃。

4.2 接收流程:RxFIFO与RxBuffer

接收和发送是对称的,但逻辑更复杂。因为发送是你主动的,接收是被动的——总线上的报文你都得看一眼,再决定要不要。

RxFIFO:所有通过硬件过滤的报文,都会先进入RxFIFO。你按顺序读出来就行。适合大多数常规报文。

RxBuffer(专用接收缓冲):某些高端CAN控制器(比如NXP的S32K系列、瑞萨的RH850系列)允许你为特定ID分配专用接收缓冲。这个ID的报文来了,直接进专用槽,不经过FIFO。

特性 RxFIFO RxBuffer
存储方式 队列,先进先出 独立槽位,一一对应
读取方式 顺序读取 直接读取指定槽位
适用场景 多ID混合接收 关键ID零延迟接收
溢出处理 覆盖最旧报文 覆盖自身槽位

接收流程的典型步骤:

  1. CAN收发器将总线差分信号转为数字电平。
  2. CAN控制器进行位同步、CRC校验、填充位解码。
  3. 通过硬件过滤(ID + IDE + DLC等条件)的报文,进入RxFIFO或RxBuffer。
  4. 硬件置位接收中断标志或状态寄存器。
  5. 中断服务程序(或轮询)读取报文,存入软件缓冲区。
  6. 应用程序从软件缓冲区取走报文。

小技巧:我建议你在中断服务程序里只做“搬运工”——把报文从硬件寄存器搬到内存队列,不要在里面做协议解析。否则中断时间太长,容易丢帧。解析工作交给后台任务去做。

4.3 硬件过滤与软件过滤

这是CANFD接收的灵魂。总线上一秒钟可能有几千帧报文,你的MCU不可能全收。怎么办?过滤。

硬件过滤:由CAN控制器内部的过滤寄存器完成,不消耗CPU。它基于报文ID(标准帧11位,扩展帧29位)进行匹配。

硬件过滤有两种模式:

  • 掩码模式(Mask Mode):你设置一个ID值和一个掩码。掩码为1的位必须匹配,为0的位忽略。比如ID=0x100,掩码=0x7FF,那就只收ID=0x100的报文。如果掩码=0x700,那就收ID范围0x000~0x0FF的所有报文。
  • 列表模式(List Mode):你列出几个允许通过的ID,只有这些ID的报文能进。适合精确控制。
过滤方式 优点 缺点
掩码模式 灵活,一个过滤器覆盖一段ID范围 可能误收不需要的ID
列表模式 精确,只收指定ID 过滤器数量有限(通常8~32个)

软件过滤:硬件过滤之后,报文已经进了RxFIFO。如果你还想进一步筛选,可以在软件里再做一次过滤。比如,你硬件上收了ID范围0x100~0x1FF的所有报文,但软件里只处理ID=0x100和0x101,其他的直接丢弃。

你可能会问:为什么不直接在硬件里配精确?因为硬件过滤器数量有限。我做过一个项目,需要同时收50个不同ID的报文,但硬件只有8个过滤器。怎么办?硬件用掩码模式收一个大范围,软件再逐个筛选。

我的经验:硬件过滤和软件过滤要配合使用。硬件过滤做“粗筛”,把明显不需要的报文挡在门外,减轻CPU负担。软件过滤做“精筛”,处理那些硬件无法精确匹配的场景。我曾经见过一个团队,硬件过滤器一个没用,全靠软件收所有报文,结果CPU负载飙到80%。加了硬件过滤后,直接降到15%。

注意:硬件过滤的配置必须在CAN控制器初始化阶段完成,运行中修改要小心。有些控制器在修改过滤寄存器时,需要先进入初始化模式,这会短暂断开总线连接。我建议你在系统启动时一次性配好,不要动态改。

4.4 收发流程全景图

下面这张图,把CANFD的收发全流程串起来了。你对照着看,会更清楚。

CANFD报文收发流程全景图 CANFD 报文收发流程全景图 发送路径 应用程序写入报文 TxFIFO / TxBuffer 发送仲裁(优先级) CAN总线 接收路径 CAN总线报文 硬件过滤(掩码/列表) RxFIFO / RxBuffer 软件过滤(可选) 应用程序读取 发送与接收 独立并行工作 关键要点 • 发送:TxFIFO按顺序,TxBuffer按优先级。周期报文用FIFO,紧急报文用Buffer。 • 接收:硬件过滤做粗筛,软件过滤做精筛。硬件过滤不消耗CPU,必须优先使用。 • 中断:只做数据搬运,不做协议解析。解析交给后台任务。 • 过滤配置:一次性在初始化阶段配好,运行中尽量不改。 • 溢出处理:TxFIFO满丢新报文,RxFIFO满丢旧报文。软件要做好防丢机制。

4.5 代码示例:TxFIFO发送与RxFIFO接收

光说不练假把式。下面给一个简化的代码片段,展示TxFIFO发送和RxFIFO接收的核心逻辑。以STM32H7的FDCAN外设为例。

// TxFIFO 发送示例
void CANFD_SendMessage(FDCAN_HandleTypeDef *hfdcan, uint32_t id, uint8_t *data, uint8_t len)
{
    FDCAN_TxHeaderTypeDef txHeader;
    
    txHeader.Identifier = id;
    txHeader.IdType = FDCAN_STANDARD_ID;      // 标准帧
    txHeader.TxFrameType = FDCAN_DATA_FRAME;   // 数据帧
    txHeader.DataLength = len;
    txHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
    txHeader.BitRateSwitch = FDCAN_BRS_OFF;    // 不切换速率
    txHeader.FDFormat = FDCAN_CLASSIC_CAN;     // 经典CAN模式
    txHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
    txHeader.MessageMarker = 0;
    
    // 写入TxFIFO
    if (HAL_FDCAN_AddMessageToTxFifo(hfdcan, &txHeader, data) != HAL_OK)
    {
        // 处理FIFO满的情况
        Error_Handler();
    }
}

// RxFIFO 接收示例(中断回调)
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
    FDCAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[64];
    
    if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE)
    {
        // 从RxFIFO0读取报文
        if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK)
        {
            // 软件过滤:只处理ID=0x100和0x101
            if (rxHeader.Identifier == 0x100 || rxHeader.Identifier == 0x101)
            {
                // 将数据放入软件队列,供后台任务处理
                Queue_Send(&app_rx_queue, rxHeader.Identifier, rxData, rxHeader.DataLength);
            }
            // 其他ID的报文直接丢弃
        }
    }
}

提示:上面代码里,我在中断回调中做了软件过滤。你可能会问:为什么不直接在硬件里配?因为硬件过滤器数量有限,而且有些ID是动态变化的。软件过滤虽然多花几个CPU周期,但灵活得多。我个人习惯是:硬件过滤收一个范围,软件过滤做精确匹配。

好了,CANFD的收发流程就聊到这儿。你想想看,其实核心就三件事:怎么发、怎么收、怎么过滤。把这三点搞明白,CANFD通信的基本功就算打牢了。下次遇到报文发不出去或者收不到的情况,你就知道从哪儿查起了。