4. CAN通信栈模块概览
CAN通信的核心,说白了就是报文怎么发出去、怎么收进来。很多初学者把CAN驱动当成一个黑盒子,调通了就完事。但真正做产品,你得搞清楚里面的收发机制——不然遇到丢帧、优先级反转、FIFO溢出,你连从哪下手都不知道。
我个人习惯把CAN收发拆成三个维度来看:发送侧的邮箱调度、接收侧的FIFO处理、以及过滤机制。今天咱们就一个一个掰开聊。
4.1 发送邮箱管理:谁先走,谁等着?
CAN控制器内部通常有多个发送邮箱(也叫发送缓冲器)。以STM32的bxCAN为例,它有3个发送邮箱。你可能会问:才3个,够用吗?
嗯,这里要注意——邮箱数量是硬件决定的,但怎么用,是软件决定的。
4.1.1 邮箱的三种状态
每个发送邮箱在任意时刻处于以下三种状态之一:
| 状态 | 含义 | 典型场景 |
|---|---|---|
| 空 | 邮箱空闲,可写入新报文 | 系统初始化后,所有邮箱均为空 |
| 挂起 | 报文已写入,等待总线仲裁 | 高优先级报文正在等待低优先级报文释放总线 |
| 发送中 | 报文正在总线上传输 | 该邮箱对应的报文赢得仲裁,正在逐位发送 |
我刚开始做CAN驱动时,犯过一个低级错误:往一个非空邮箱里写数据,结果把正在发送的报文覆盖了。后来养成习惯,每次写邮箱前先检查状态寄存器。
4.1.2 发送优先级策略
当多个邮箱同时有报文等待发送时,控制器怎么决定谁先走?
有两种模式:
- 固定优先级:邮箱编号越小,优先级越高。比如邮箱0 > 邮箱1 > 邮箱2。简单粗暴,但容易导致低编号邮箱被“饿死”。
- 轮询模式:按邮箱编号循环发送,每个邮箱轮流获得发送机会。公平,但实时性差。
我在项目中遇到过一个问题:某个节点发送紧急报警报文,但因为它被分配到了邮箱2,前面两个邮箱一直在发周期性的状态报文,报警报文迟迟发不出去。后来我把报警报文固定到邮箱0,问题解决。
4.1.3 软件发送流程
一个标准的发送流程,我一般这样写:
// 伪代码:CAN报文发送
uint8_t CAN_SendMessage(uint32_t id, uint8_t* data, uint8_t len) {
// 1. 找一个空邮箱
uint8_t mailbox = CAN_FindEmptyMailbox();
if (mailbox == NO_MAILBOX) {
return BUSY; // 所有邮箱都满了
}
// 2. 写入ID、数据、长度
CAN_TxMailbox[mailbox].ID = id;
CAN_TxMailbox[mailbox].DLC = len;
for (int i = 0; i < len; i++) {
CAN_TxMailbox[mailbox].Data[i] = data[i];
}
// 3. 请求发送
CAN_TxMailbox[mailbox].TXRQ = 1;
return OK;
}
注意:CAN_FindEmptyMailbox() 这个函数,我建议不要只查邮箱状态,还要考虑优先级。比如先查邮箱0,再查邮箱1,最后查邮箱2——这样能保证高优先级邮箱优先被使用。
4.2 接收FIFO处理:别让数据淹了
接收侧和发送侧不一样。发送是主动的,接收是被动的。你永远不知道总线上下一个报文什么时候来,来多少。
所以CAN控制器用FIFO(先进先出队列)来缓冲接收到的报文。以bxCAN为例,它有2个接收FIFO,每个FIFO深度为3个报文。
4.2.1 FIFO的工作原理
FIFO的工作流程很简单:
- 总线上的报文通过硬件过滤后,被存入FIFO。
- CPU从FIFO头部读取最早到达的报文。
- 读取完成后,释放该报文占用的FIFO空间。
但有个坑:如果CPU读取速度跟不上报文到达速度,FIFO会溢出。一旦溢出,新来的报文会被丢弃。
4.2.2 FIFO溢出处理策略
遇到FIFO溢出,你有三种选择:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 丢弃新报文 | FIFO满时,新报文直接丢弃 | 周期性报文,丢一帧影响不大 |
| 覆盖旧报文 | FIFO满时,新报文覆盖最旧的报文 | 状态类报文,最新数据最重要 |
| 软件缓冲 | 在RAM中开辟更大的环形缓冲区 | 关键报文,一帧都不能丢 |
我个人习惯用第三种——软件缓冲。硬件FIFO只有3个深度,太浅了。我在RAM里开一个256字节的环形缓冲区,中断里只做快速拷贝,主循环里慢慢处理。
4.2.3 接收中断 vs 轮询
接收报文有两种方式:
- 中断方式:FIFO非空时触发中断,CPU立即读取。实时性好,但中断频率高时CPU负载大。
- 轮询方式:主循环定期检查FIFO状态。实现简单,但实时性差。
我的经验是:总线负载率低于30%时,轮询就够了。高于30%,必须用中断。高于70%,中断里只能做最轻量的操作——读取FIFO、存入缓冲区、清中断标志,然后立刻退出。
4.3 硬件过滤与软件过滤:谁来决定报文该收?
CAN总线上所有节点都能收到所有报文。但每个节点只关心跟自己相关的报文。过滤机制就是干这个的。
4.3.1 硬件过滤:又快又省心
硬件过滤是CAN控制器内部的一个功能模块。它根据预设的ID列表或ID范围,决定哪些报文能进入FIFO,哪些直接丢弃。
以bxCAN为例,它有28个过滤器组。每个过滤器组可以配置为:
- 列表模式:精确匹配一个或两个ID。比如只收ID=0x123的报文。
- 掩码模式:匹配一个ID范围。比如收ID=0x100~0x1FF的所有报文。
配置示例:
// 配置过滤器0:掩码模式,只收ID=0x123的报文
CAN_FilterInitTypeDef filter;
filter.FilterNumber = 0;
filter.FilterMode = CAN_FilterMode_IdMask;
filter.FilterScale = CAN_FilterScale_32bit;
filter.FilterIdHigh = 0x123 << 5; // 标准ID左移5位
filter.FilterIdLow = 0;
filter.FilterMaskIdHigh = 0x7FF << 5; // 掩码:只比较11位ID
filter.FilterMaskIdLow = 0;
filter.FilterFIFOAssignment = CAN_FIFO0;
filter.FilterActivation = ENABLE;
CAN_FilterInit(&filter);
硬件过滤的好处是:不占用CPU时间。报文在进入FIFO之前就被过滤掉了,CPU根本不知道有这回事。
4.3.2 软件过滤:灵活但费CPU
软件过滤,说白了就是先把所有报文都收进来,然后在软件里判断要不要处理。
什么时候用软件过滤?
- 硬件过滤器数量不够用(比如28个过滤器不够你配的)
- 过滤条件太复杂(比如要根据报文数据段内容过滤)
- 需要动态修改过滤规则
软件过滤的典型实现:
void CAN_ReceiveHandler(void) {
CanRxMsg rxMsg;
CAN_Receive(CAN_FIFO0, &rxMsg);
// 软件过滤:只处理ID在0x200~0x2FF范围内的报文
if ((rxMsg.ID >= 0x200) && (rxMsg.ID <= 0x2FF)) {
ProcessMessage(&rxMsg);
}
// 其他ID的报文直接丢弃
}
但注意:软件过滤意味着所有报文都进了FIFO。如果总线上报文很多,FIFO很容易溢出。我建议能用硬件过滤的,尽量用硬件过滤。
4.3.3 硬件+软件混合过滤
实际项目中,我常用混合策略:
- 用硬件过滤收下所有“可能有用”的报文(比如一个较宽的ID范围)
- 用软件过滤做精确筛选
这样既减少了FIFO压力,又保留了灵活性。
4.4 本章知识体系
下面这张图总结了CAN报文收发机制的核心逻辑:
这张图把本章的三个核心模块串起来了。你想想看:发送邮箱管的是“怎么发出去”,接收FIFO管的是“怎么收进来”,过滤机制管的是“收哪些”。三者配合好了,CAN通信才算真正跑得稳。