3、CAN总线软件:CAN协议栈分层、CAN报文发送与接收、CAN滤波器配置

好,咱们进入正题。这一章聊的是CAN总线的软件层面。很多工程师硬件调通了,一上软件就抓瞎。其实,搞懂CAN协议栈的分层、收发流程和滤波器配置,你就能解决80%的通信问题。

3.1 CAN协议栈分层:别被“分层”吓到

CAN协议栈,说白了就是一套通信规则。它不像TCP/IP那么复杂,但分层思想是一样的。我个人习惯把它分成三层:

  • 应用层:你写的业务代码。比如“我要发一个车速信号”。
  • 数据链路层:CAN控制器干的活。负责组帧、校验、应答。
  • 物理层:CAN收发器干的活。负责电平转换、差分信号。

你想想看,你写代码时根本不用管CAN_H和CAN_L上到底是3.5V还是1.5V。这就是分层的好处——各司其职。

核心要点: 应用层只关心“发什么”,数据链路层关心“怎么发”,物理层关心“用什么发”。

我在项目中遇到过一位同事,死活调不通通信。他一直在应用层找问题,结果最后发现是CAN收发器的供电电压不对。嗯,这就是没理解分层——物理层出问题,应用层代码写得再漂亮也没用。

3.2 CAN报文发送与接收:从代码到总线

报文收发,是咱们和CAN控制器打交道的核心。我以常用的STM32的bxCAN为例,讲讲实际流程。

3.2.1 发送流程:三步走

  1. 填充报文:把ID、数据长度、数据内容填到发送邮箱。
  2. 请求发送:置位发送请求位。
  3. 等待仲裁:CAN控制器自动处理总线仲裁,发送成功或失败都有状态位。

代码示例(伪代码,但逻辑通用):

// 配置发送报文
CAN_TxHeaderTypeDef TxHeader;
TxHeader.StdId = 0x123;      // 标准ID
TxHeader.DLC = 8;            // 数据长度
TxHeader.IDE = CAN_ID_STD;   // 标准帧

uint8_t TxData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};

// 请求发送
HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox);

这里有个坑。我曾经在项目里发现,连续快速发送报文时,总线会丢帧。为什么?因为发送邮箱只有3个。你发得太快,邮箱满了,新报文就进不去了。解决办法是:发送前检查邮箱是否空闲,或者用发送完成中断来触发下一帧。

避坑指南: 我曾经在高速发送时忘了检查发送邮箱状态,结果丢了一堆报文。后来我养成了习惯:每次发送前,先读一下 CAN_TSR 寄存器,确认有空闲邮箱再发。

3.2.2 接收流程:中断还是轮询?

接收报文,我建议用中断。轮询太浪费CPU了,尤其是总线负载高的时候。

接收中断的配置很简单:

// 使能接收中断
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);

// 中断回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef RxHeader;
    uint8_t RxData[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData);
    // 处理接收到的数据
}

你想想看,如果总线上一秒钟有1000帧报文,你用轮询,CPU全耗在查标志位上了。用中断,CPU只在有报文时才处理,效率高得多。

注意: 中断服务函数里不要做耗时操作,比如打印日志、复杂计算。我曾经见过有人直接在中断里调用 printf,结果系统直接卡死。正确的做法是:中断里只把数据搬到缓冲区,用信号量或标志通知主循环处理。

3.3 CAN滤波器配置:只收你想要的

滤波器,是CAN控制器的一个硬件功能。它就像一个门卫,只让符合规则的报文进入FIFO。不匹配的报文,直接丢弃,CPU根本不知道。

为什么要用滤波器?因为总线上报文太多了。你想想看,一个动力CAN总线上可能有几十个节点,每秒几千帧报文。如果CPU每帧都处理,早累死了。

3.3.1 两种模式:列表模式和掩码模式

模式 说明 适用场景
列表模式 只接收ID完全匹配的报文 只关心几个固定ID
掩码模式 接收ID符合掩码规则的报文 关心一组ID(比如0x100~0x1FF)

我个人习惯:如果只监控3-5个ID,用列表模式,简单粗暴。如果是一组ID,比如所有诊断报文(0x7DF~0x7EF),用掩码模式,省配置空间。

3.3.2 实战配置:以STM32为例

配置滤波器,其实就是配置几个寄存器。我直接给个例子:

// 配置滤波器0:接收ID为0x321和0x322的报文
CAN_FilterTypeDef sFilterConfig;

sFilterConfig.FilterBank = 0;                    // 使用滤波器组0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; // 列表模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_16BIT; // 16位滤波器

// 配置两个ID
sFilterConfig.FilterIdHigh = 0x321 << 5;        // 标准ID左移5位
sFilterConfig.FilterIdLow = 0x322 << 5;
sFilterConfig.FilterMaskIdHigh = 0x0000;          // 列表模式下掩码无效
sFilterConfig.FilterMaskIdLow = 0x0000;

sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 关联到FIFO0
sFilterConfig.FilterActivation = ENABLE;           // 使能滤波器

HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);

这里有个细节:标准ID是11位,但寄存器里要左移5位。为什么?因为CAN标准帧的ID在报文里占11位,寄存器里是从高位对齐的。我第一次配置时忘了左移,结果死活收不到报文。嗯,这种细节,手册上写得清楚,但你不踩坑就记不住。

经验之谈: 调试滤波器时,我建议先把滤波器全关掉(旁路模式),确认硬件收发没问题。然后再一个一个打开滤波器,验证过滤效果。这样出了问题,你能快速定位是硬件问题还是软件配置问题。

3.3.3 掩码模式的高级玩法

掩码模式里,掩码位为1表示“必须匹配”,为0表示“忽略”。举个例子:

  • ID = 0x100,掩码 = 0x7F0(二进制:0111 1111 0000)
  • 结果:只匹配ID的高7位(0x10?),低4位任意
  • 所以:0x100~0x10F都能通过

我曾经在项目里用这个特性,一次性过滤了所有诊断报文。诊断请求ID是0x7DF,响应ID是0x7EF,我配一个掩码就全搞定了。省了十几个滤波器组,你说爽不爽?

小技巧: 如果你不确定掩码怎么配,可以用“先全1,再慢慢减”的方法。全1就是列表模式,然后逐步把不需要精确匹配的位改成0。这样不容易出错。

3.4 总结一下

这一章的内容,说白了就是三件事:

  • 分层:让你知道问题出在哪一层。
  • 收发:让你知道报文怎么进出控制器。
  • 滤波:让你只处理你关心的报文。

我见过太多工程师,一上来就调滤波器,结果连报文都发不出去。我的建议是:先裸收发,再上滤波。一步一步来,别想一口吃成胖子。

下一章,咱们聊聊CAN总线的常见故障和诊断方法。到时候我会分享一些我在现场排故的真实案例,保证让你有收获。