4、CANFD报文发送与接收:发送流程(TxFIFO/TxBuffer)、接收流程(RxFIFO/RxBuffer)、硬件过滤与软件过滤
好,咱们今天聊点实在的。CANFD的报文收发,说白了就是MCU和总线之间怎么“递纸条”。你写好了数据,怎么塞给CAN控制器?总线上一堆报文飞过来,你怎么只拿自己想要的?
我刚开始做车载项目那会儿,总觉得收发就是调个API完事。结果有一次,报文发不出去,查了三天,最后发现是TxBuffer的优先级配错了。嗯,从那以后,我再也不敢小看这个流程了。
4.1 发送流程:TxFIFO与TxBuffer
CANFD的发送,核心就两个机制:TxFIFO 和 TxBuffer。它们都是你放待发报文的地方,但用法完全不同。
| 特性 | TxFIFO | TxBuffer |
|---|---|---|
| 存储结构 | 先进先出队列 | 独立邮箱槽位 |
| 发送顺序 | 严格按写入顺序 | 按优先级(ID或槽位号) |
| 应用场景 | 周期报文、诊断流 | 高优先级事件报文 |
| 覆盖风险 | 满时丢最新报文 | 满时丢最旧报文(可配置) |
TxFIFO 的发送流程:
- 应用程序将报文写入FIFO的写入指针位置。
- 硬件自动将FIFO内容搬运到发送移位寄存器。
- 总线空闲时,移位寄存器将报文逐位发送到CAN总线。
- 发送完成后,硬件更新FIFO的释放指针。
TxBuffer 的发送流程:
- 应用程序选择一个空闲的TxBuffer槽位,写入报文。
- 硬件根据每个TxBuffer的优先级(通常是报文ID越小优先级越高)进行仲裁。
- 优先级最高的TxBuffer内容被送入发送移位寄存器。
- 发送完成后,该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零延迟接收 |
| 溢出处理 | 覆盖最旧报文 | 覆盖自身槽位 |
接收流程的典型步骤:
- CAN收发器将总线差分信号转为数字电平。
- CAN控制器进行位同步、CRC校验、填充位解码。
- 通过硬件过滤(ID + IDE + DLC等条件)的报文,进入RxFIFO或RxBuffer。
- 硬件置位接收中断标志或状态寄存器。
- 中断服务程序(或轮询)读取报文,存入软件缓冲区。
- 应用程序从软件缓冲区取走报文。
小技巧:我建议你在中断服务程序里只做“搬运工”——把报文从硬件寄存器搬到内存队列,不要在里面做协议解析。否则中断时间太长,容易丢帧。解析工作交给后台任务去做。
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的收发全流程串起来了。你对照着看,会更清楚。
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通信的基本功就算打牢了。下次遇到报文发不出去或者收不到的情况,你就知道从哪儿查起了。