4. CAN报文发送与接收:邮箱机制、发送流程、接收中断与FIFO管理
CAN通信这块,说白了就是两个动作:发和收。但怎么发得稳、收得全,这里面的门道可不少。我刚开始做座舱项目时,就因为在报文接收上栽了个跟头,导致车机黑屏——嗯,从那以后我对CAN的收发机制就再也不敢马虎了。
4.1 邮箱机制——CAN通信的“快递柜”
CAN控制器内部有一组硬件缓冲区,我们叫它“邮箱”。每个邮箱可以独立配置成发送邮箱或接收邮箱。你可以把它想象成一组快递柜:发送就是把报文塞进柜子,硬件自己会把它送出去;接收就是硬件把收到的报文放进柜子,你去取。
| 邮箱类型 | 方向 | 典型数量 | 用途 |
|---|---|---|---|
| 发送邮箱 | MCU → CAN总线 | 3~8个 | 存放待发送报文 |
| 接收邮箱 | CAN总线 → MCU | 2~16个 | 存放已接收报文 |
| FIFO邮箱 | 双向 | 深度可配置 | 批量接收/发送 |
每个邮箱都有自己的标识符(ID)和掩码寄存器。硬件收到报文后,会自动比对ID,匹配上了就存进对应的接收邮箱。这个过程不需要CPU干预——这才是CAN控制器的精髓。
关键点:邮箱数量是硬件固定的,但你可以通过软件灵活配置。比如把3个邮箱配成发送,5个配成接收,剩下的做FIFO。具体怎么分,看你的项目需求。
4.2 发送流程——轮询 vs 中断
发送报文有两种方式:轮询和中断。我个人的习惯是:低速报文用轮询,高速或周期性报文用中断。为什么?往下看。
4.2.1 轮询发送
轮询发送的流程很简单:
- 检查邮箱状态寄存器,确认邮箱是否为空(空闲)。
- 将报文数据写入邮箱的数据寄存器。
- 设置标识符、数据长度码(DLC)等控制位。
- 置位发送请求位(TXRQ),启动发送。
- 循环等待发送完成标志位(TXOK)置位。
- 清除标志位,释放邮箱。
// 轮询发送示例(伪代码)
void CAN_Send_Polling(uint32_t id, uint8_t *data, uint8_t len) {
// 等待邮箱空闲
while (CAN->TSR & CAN_TSR_TME0 == 0);
// 填充邮箱
CAN->sTxMailBox->TIR = (id << 21);
CAN->sTxMailBox->TDTR = len;
for (int i = 0; i < len; i++) {
CAN->sTxMailBox->TDLR = data[i] << (i * 8);
}
// 请求发送
CAN->sTxMailBox->TIR |= CAN_TI0R_TXRQ;
// 等待发送完成
while ((CAN->TSR & CAN_TSR_RQCP0) == 0);
// 清除标志
CAN->TSR |= CAN_TSR_RQCP0;
}
我的经验:轮询发送适合发送频率低(比如每秒几次)或者对实时性要求不高的报文。我曾经在一个项目中用轮询发送空调控制报文,每100ms发一次,CPU占用率几乎可以忽略。
4.2.2 中断发送
中断发送的流程:
- 配置邮箱,写入数据,置位发送请求。
- 使能发送完成中断。
- CPU去做其他事情。
- 硬件发送完成后,触发中断。
- 在中断服务函数中清除标志位,准备下一个报文。
// 中断发送——中断服务函数
void CAN_TX_IRQHandler(void) {
if (CAN->TSR & CAN_TSR_RQCP0) {
// 清除发送完成标志
CAN->TSR |= CAN_TSR_RQCP0;
// 通知主循环:邮箱已释放
g_can_tx_done = 1;
// 如果有待发送报文,继续发送
if (tx_queue_not_empty()) {
CAN_Send_From_Queue();
}
}
}
注意:中断服务函数里不要做复杂操作!我见过有人直接在中断里调用printf打印日志,结果系统直接卡死。中断里只做标志位操作和简单数据搬运,复杂逻辑放到主循环或任务里处理。
4.3 接收中断处理——别让报文“丢了”
接收中断是CAN通信中最常用的方式。为什么?因为报文什么时候来,你根本不知道。轮询接收会浪费大量CPU时间,而中断接收能做到“来了就处理”。
接收中断的处理流程:
- 硬件收到报文,匹配接收邮箱或FIFO。
- 触发接收中断(RXNE或FMP)。
- 在中断服务函数中读取报文。
- 将报文存入软件缓冲区(环形队列)。
- 清除中断标志位。
- 通知应用层处理。
// 接收中断服务函数
void CAN_RX_IRQHandler(void) {
CanRxMsg rx_msg;
// 读取报文
rx_msg.id = CAN->sFIFOMailBox->RIR >> 21;
rx_msg.dlc = CAN->sFIFOMailBox->RDTR & 0x0F;
for (int i = 0; i < rx_msg.dlc; i++) {
rx_msg.data[i] = (CAN->sFIFOMailBox->RDLR >> (i * 8)) & 0xFF;
}
// 释放FIFO(出栈)
CAN->RF0R |= CAN_RF0R_RFOM0;
// 存入环形队列
ring_buffer_push(&rx_queue, &rx_msg);
}
避坑指南:我曾经在一个项目中,接收中断里直接调用了一个耗时较长的处理函数。结果总线负载一高,中断嵌套导致栈溢出,系统直接复位。后来我把接收中断改成“只存不处理”,在主循环中统一处理,问题就解决了。
4.4 FIFO管理——批量接收的利器
FIFO(先进先出)是CAN控制器提供的一种硬件队列。它和邮箱最大的区别是:邮箱是一对一的,FIFO是一对多的。一个FIFO可以连续存储多个报文,你只需要按顺序读取就行。
| 特性 | 邮箱模式 | FIFO模式 |
|---|---|---|
| 存储深度 | 1个报文/邮箱 | 可配置(通常3~64个) |
| 匹配方式 | 每个邮箱独立ID过滤 | 共享ID过滤 |
| 读取方式 | 直接读取指定邮箱 | 按顺序读取(出栈) |
| 适用场景 | 特定ID报文 | 批量接收、高负载 |
FIFO的管理有几个关键点:
- 深度配置:根据总线负载和CPU处理能力来定。负载高、CPU慢,就配深一点;反之可以浅一些。
- 溢出处理:FIFO满了怎么办?硬件会丢弃新报文或覆盖旧报文。我建议开启溢出中断,在溢出时记录日志,方便排查问题。
- 读取时机:可以在中断中逐条读取,也可以在定时中断中批量读取。我个人更推荐后者——减少中断频率,降低系统抖动。
小技巧:FIFO的“水位线”功能很实用。你可以设置一个阈值,比如FIFO里存了5个报文时触发中断。这样既能批量处理,又不会让FIFO溢出。我在座舱项目中就用这个方式处理多媒体控制报文,效果很好。
4.5 知识结构总览
下面这张图把CAN报文收发的主要脉络串起来了。你可以对照着看,心里有个全局图景。
4.6 实战建议
说了这么多,最后给几个实在的建议:
- 发送优先级:CAN总线本身有仲裁机制,但多个发送邮箱之间也有优先级。通常邮箱号越小优先级越高。把关键报文放在小号邮箱里。
- 接收超时:如果某个报文超过预期时间没收到,要能检测到并做容错处理。我在项目中就遇到过CAN线接触不良导致报文丢失的情况,加了超时检测后问题很快定位。
- 调试手段:初期调试时,可以把所有收发报文都打印出来。但量产版本一定要关掉打印,否则CAN通信会被打印拖垮。
最后提醒:CAN控制器的寄存器操作一定要小心。我曾经因为写错了某个位,导致整个CAN模块无法正常工作。调试了整整两天才发现——嗯,从那以后我每次配置寄存器都会对照手册逐位确认。