4. CAN报文发送与接收:发送缓冲区管理与发送优先级、接收缓冲区管理与FIFO机制、中断使能与中断服务函数编写
好,咱们今天聊点实在的。CAN通信说白了,核心就两件事:把数据发出去,把数据收回来。但就是这两件事,里面门道不少。我刚开始调CAN的时候,总觉得不就是写个寄存器嘛,结果板子一跑起来,报文丢了、优先级乱了、中断进不去……各种问题。后来才明白,发送和接收的缓冲区管理,才是CAN通信的“心脏”。
4.1 发送缓冲区管理与发送优先级
先说说发送。CMS32的CAN模块,发送缓冲区不是只有一个“坑位”。它通常有3个独立的发送缓冲区,我习惯叫它们TX0、TX1、TX2。每个缓冲区都能独立存放一帧完整的报文。
关键点:这三个缓冲区不是简单的“轮流用”,它们有优先级之分。TX0优先级最高,TX1次之,TX2最低。硬件在发送时,会先看TX0有没有待发报文,有就发;没有再看TX1,最后看TX2。
你可能会问:“那我是不是把所有报文都塞进TX0就行了?”千万别这么干。我见过有人这么搞,结果高优先级报文把低优先级报文活活“饿死”了。正确的做法是:根据报文的重要性分配缓冲区。
| 缓冲区 | 优先级 | 推荐用途 |
|---|---|---|
| TX0 | 最高 | 紧急控制指令、故障报文 |
| TX1 | 中等 | 周期性状态数据、参数更新 |
| TX2 | 最低 | 诊断请求、非关键日志 |
发送流程其实不复杂,但有几个坑要注意。我一般这么写发送函数:
// 发送一帧CAN报文
uint8_t CAN_SendMsg(CAN_TypeDef* CANx, CanTxMsg* TxMsg) {
uint8_t mailbox = 0;
uint32_t tsr = CANx->TSR;
// 1. 检查哪个发送邮箱空闲
if (tsr & (1 << 26)) { // TX0空闲
mailbox = 0;
} else if (tsr & (1 << 27)) { // TX1空闲
mailbox = 1;
} else if (tsr & (1 << 28)) { // TX2空闲
mailbox = 2;
} else {
return CAN_BUSY; // 全满,返回忙
}
// 2. 写入标识符
if (TxMsg->IDE == CAN_ID_STD) {
CANx->sTxMailBox[mailbox].TIR = (TxMsg->StdId << 21);
} else {
CANx->sTxMailBox[mailbox].TIR = (TxMsg->ExtId << 3) | (1 << 2);
}
// 3. 写入数据长度和帧类型
CANx->sTxMailBox[mailbox].TDTR = TxMsg->DLC;
// 4. 写入数据
CANx->sTxMailBox[mailbox].TDLR = (TxMsg->Data[3] << 24) |
(TxMsg->Data[2] << 16) |
(TxMsg->Data[1] << 8) |
TxMsg->Data[0];
CANx->sTxMailBox[mailbox].TDHR = (TxMsg->Data[7] << 24) |
(TxMsg->Data[6] << 16) |
(TxMsg->Data[5] << 8) |
TxMsg->Data[4];
// 5. 请求发送
CANx->sTxMailBox[mailbox].TIR |= (1 << 0);
return CAN_OK;
}
我的经验:发送前一定要检查邮箱是否真的空闲。我曾经踩过一个坑:连续快速发送时,TSR寄存器还没更新,我就往同一个邮箱里写数据,结果把上一帧报文覆盖了。加个超时重试机制会稳妥很多。
4.2 接收缓冲区管理与FIFO机制
接收端的设计思路和发送端不太一样。CMS32的CAN模块有2个接收FIFO:FIFO0和FIFO1。每个FIFO可以存3帧报文。为什么用FIFO?说白了,就是为了应对“突发流量”。
你想想看,总线上可能同时有好几个节点发数据。如果CPU正在处理其他中断,来不及读接收寄存器,报文就丢了。FIFO就像一个缓冲池,先帮你存着,等CPU有空了再来取。
FIFO工作机制:
- FIFO0和FIFO1各自独立,深度为3
- 每个FIFO有对应的中断标志:FMP(报文等待)、FOV(溢出)、FULL(满)
- 读取顺序:先入先出,硬件自动维护指针
接收流程我习惯这样写:
// 从FIFO读取一帧报文
uint8_t CAN_ReceiveMsg(CAN_TypeDef* CANx, uint8_t fifo_num, CanRxMsg* RxMsg) {
CanRxFIFO* fifo;
// 选择FIFO
if (fifo_num == 0) {
fifo = &CANx->sFIFOMailBox[0];
} else {
fifo = &CANx->sFIFOMailBox[1];
}
// 1. 读取标识符
if (fifo->RIR & (1 << 2)) { // 扩展帧
RxMsg->IDE = CAN_ID_EXT;
RxMsg->ExtId = (fifo->RIR >> 3) & 0x1FFFFFFF;
} else {
RxMsg->IDE = CAN_ID_STD;
RxMsg->StdId = (fifo->RIR >> 21) & 0x7FF;
}
// 2. 读取数据长度
RxMsg->DLC = fifo->RDTR & 0x0F;
// 3. 读取数据
RxMsg->Data[0] = fifo->RDLR & 0xFF;
RxMsg->Data[1] = (fifo->RDLR >> 8) & 0xFF;
RxMsg->Data[2] = (fifo->RDLR >> 16) & 0xFF;
RxMsg->Data[3] = (fifo->RDLR >> 24) & 0xFF;
RxMsg->Data[4] = fifo->RDHR & 0xFF;
RxMsg->Data[5] = (fifo->RDHR >> 8) & 0xFF;
RxMsg->Data[6] = (fifo->RDHR >> 16) & 0xFF;
RxMsg->Data[7] = (fifo->RDHR >> 24) & 0xFF;
// 4. 释放FIFO(硬件自动更新指针)
CANx->RF0R |= (1 << 5); // 释放FIFO0
// CANx->RF1R |= (1 << 5); // 释放FIFO1
return CAN_OK;
}
重要提醒:读完报文后,一定要释放FIFO!我见过有人忘了这步,结果FIFO一直显示“满”,后续报文全部丢失。释放操作就是置位RF0R或RF1R寄存器的RFOM位。
4.3 中断使能与中断服务函数编写
轮询方式接收报文,CPU利用率太低了。实际项目中,我几乎都是用中断。CMS32的CAN中断源挺多的,但常用的就这几个:
| 中断源 | 标志位 | 触发条件 |
|---|---|---|
| 发送完成 | TXOK | 报文成功发送到总线 |
| 接收FIFO0 | FMP0 | FIFO0中有待读报文 |
| 接收FIFO1 | FMP1 | FIFO1中有待读报文 |
| 错误警告 | EWG | 错误计数达到警告阈值 |
| 总线关闭 | BOF | 节点进入总线关闭状态 |
中断使能的配置,我一般放在CAN初始化函数里:
// 使能CAN中断
void CAN_EnableInterrupt(CAN_TypeDef* CANx) {
// 使能发送完成中断
CANx->IER |= (1 << 0); // TMEIE
// 使能FIFO0接收中断
CANx->IER |= (1 << 1); // FMPIE0
// 使能FIFO1接收中断
CANx->IER |= (1 << 4); // FMPIE1
// 使能错误中断
CANx->IER |= (1 << 11); // EWGIE
CANx->IER |= (1 << 14); // BOFIE
// 在NVIC中使能CAN中断通道
NVIC_EnableIRQ(CAN_IRQn);
}
中断服务函数怎么写?我个人的习惯是:ISR里只做最轻量级的事,比如把数据从FIFO搬到环形缓冲区,然后置一个标志位。真正的数据处理,放在主循环里做。
// CAN中断服务函数
void CAN_IRQHandler(void) {
uint32_t status;
// 读取中断状态
status = CANx->ESR;
// 1. 处理接收中断(FIFO0)
if (CANx->RF0R & (1 << 3)) { // FMP0位
// 读取报文,存入环形缓冲区
CAN_ReceiveMsg(CANx, 0, &rx_msg);
RingBuf_Write(&rx_ring_buf, &rx_msg);
// 置位接收标志
rx_flag = 1;
}
// 2. 处理发送完成中断
if (CANx->TSR & (1 << 0)) { // TXOK0
// 清除标志
CANx->TSR |= (1 << 0);
// 可以在这里触发下一帧发送
tx_done_flag = 1;
}
// 3. 处理错误中断
if (status & (1 << 0)) { // EWGF
// 错误警告处理
error_count++;
}
if (status & (1 << 2)) { // BOFF
// 总线关闭处理
bus_off_flag = 1;
// 这里需要重新初始化CAN
}
}
避坑指南:我曾经在ISR里直接调用printf打印调试信息,结果程序卡死了。为什么?printf本身可能用了中断,在ISR里调用就形成了中断嵌套,堆栈溢出。记住:ISR里别做耗时操作,别调用不可重入函数。
最后,我画了一张图,把发送和接收的整个数据流串起来,方便你理解:
嗯,这张图基本把发送和接收的路径画清楚了。发送端从三个优先级不同的缓冲区出发,经过总线仲裁,最终送上CAN总线。接收端则先经过硬件滤波,符合条件的报文进入FIFO,然后触发中断,ISR把数据搬到环形缓冲区,主循环再慢慢处理。
记住一句话:发送看优先级,接收看FIFO,中断里别干重活。把这三点吃透了,CAN通信这块你就拿下了大半。