4. 数据发送与接收:发送邮箱管理、中断接收与轮询接收、FIFO机制与DMA传输
CAN通信说到底就两件事——把数据发出去,把数据收回来。但怎么发、怎么收,这里面的门道可不少。我刚开始接触CMS32的CAN模块时,觉得不就是写个寄存器嘛,结果调试起来才发现,发送邮箱的管理、接收方式的选择,每一个细节都能让你折腾半天。
这一章咱们就掰开揉碎,把发送和接收的几种方式讲清楚。你想想看,如果发送邮箱用不好,高优先级的报文可能被低优先级的堵住;如果接收方式选不对,CPU就得一直忙着处理CAN中断,别的活都干不了。
4.1 发送邮箱管理——别让报文堵在路上
CMS32的CAN控制器通常有3个发送邮箱。每个邮箱都可以独立配置,用来发送不同的CAN报文。说白了,就是三个独立的发送通道,谁准备好了谁就上总线。
核心要点:发送邮箱的优先级由报文ID决定,不是由邮箱编号决定。ID越小,优先级越高。
4.1.1 发送邮箱的工作流程
我习惯把发送邮箱比作三个快递员。每个快递员手里只能拿一个包裹,送完才能接下一个。具体流程是这样的:
- 准备数据:把报文ID、数据长度、数据内容写入邮箱寄存器
- 请求发送:置位发送请求位,告诉CAN控制器“这个邮箱要发”
- 仲裁发送:CAN控制器根据报文ID仲裁,优先级高的先上总线
- 发送完成:总线空闲且仲裁成功,报文发出,硬件自动清除发送请求位
- 状态检查:软件可以查询发送完成标志,或者等中断通知
嗯,这里要注意:如果三个邮箱同时请求发送,硬件会按报文ID从小到大排序发送。ID最小的先走。
4.1.2 发送邮箱的代码实现
下面是一个发送邮箱的初始化与发送示例。我习惯把发送封装成一个函数,这样上层应用调用起来方便。
/* 发送邮箱初始化 */
void CAN_SendMailbox_Init(void)
{
/* 配置发送邮箱0:标准帧,ID=0x123,数据长度8字节 */
CAN->SFT[0] = 0x00; /* 标准帧模式 */
CAN->SID[0] = 0x0123; /* 报文ID */
CAN->SDLC[0] = 0x08; /* 数据长度 */
/* 配置发送邮箱1:扩展帧,ID=0x18DA00F1,数据长度8字节 */
CAN->SFT[1] = 0x01; /* 扩展帧模式 */
CAN->EID[1] = 0x18DA00F1; /* 扩展ID */
CAN->SDLC[1] = 0x08;
/* 配置发送邮箱2:标准帧,ID=0x100,数据长度4字节 */
CAN->SFT[2] = 0x00;
CAN->SID[2] = 0x0100;
CAN->SDLC[2] = 0x04;
}
/* 发送报文函数 */
uint8_t CAN_SendMessage(uint8_t mailbox, uint8_t *data, uint8_t len)
{
uint8_t i;
/* 检查邮箱是否空闲 */
if (CAN->STS & (0x01 << mailbox)) {
return 0xFF; /* 邮箱忙,返回错误 */
}
/* 写入数据 */
for (i = 0; i < len; i++) {
CAN->SDB[mailbox][i] = data[i];
}
/* 请求发送 */
CAN->SCR |= (0x01 << mailbox);
return 0x00; /* 发送请求成功 */
}
个人经验:我在项目中遇到过一个问题——连续发送多个报文时,第二个报文总是发不出去。后来发现是没检查邮箱状态就直接写数据了。记住:写数据之前一定要确认邮箱是空闲的,否则会覆盖正在发送的数据。
4.2 中断接收与轮询接收——两种接收方式的选择
接收报文有两种方式:中断接收和轮询接收。选哪个?这得看你的应用场景。
| 对比项 | 中断接收 | 轮询接收 |
|---|---|---|
| CPU占用 | 低,只在收到报文时响应 | 高,需要不断查询接收标志 |
| 实时性 | 高,报文到达立即处理 | 取决于轮询频率 |
| 实现复杂度 | 中等,需要配置中断向量 | 简单,只需读状态寄存器 |
| 适用场景 | 高速通信、实时控制 | 低速通信、简单测试 |
| 丢包风险 | 低,硬件FIFO缓冲 | 高,轮询间隔内可能丢包 |
4.2.1 中断接收——我推荐的方式
我个人习惯用中断接收。为什么?因为省心。你想想看,如果CPU一直在轮询,那它啥也干不了。中断接收就不一样了,报文来了才处理,平时该干嘛干嘛。
中断接收的配置步骤:
- 使能CAN接收中断
- 配置中断优先级
- 编写中断服务函数
- 在中断服务函数中读取报文
/* 中断接收初始化 */
void CAN_RecvInterrupt_Init(void)
{
/* 使能接收中断 */
CAN->IER |= 0x01; /* 使能接收完成中断 */
/* 配置中断优先级(假设使用NVIC) */
NVIC_SetPriority(CAN_IRQn, 2);
NVIC_EnableIRQ(CAN_IRQn);
}
/* CAN接收中断服务函数 */
void CAN_IRQHandler(void)
{
uint8_t i;
uint8_t rx_data[8];
uint32_t rx_id;
uint8_t rx_dlc;
/* 检查是否是接收中断 */
if (CAN->STS & 0x02) {
/* 读取报文ID */
if (CAN->RFT & 0x01) {
rx_id = CAN->REID; /* 扩展帧 */
} else {
rx_id = CAN->RSID; /* 标准帧 */
}
/* 读取数据长度 */
rx_dlc = CAN->RDLC & 0x0F;
/* 读取数据 */
for (i = 0; i < rx_dlc; i++) {
rx_data[i] = CAN->RDB[i];
}
/* 清除接收中断标志 */
CAN->STS &= ~0x02;
/* 处理接收到的报文(比如放入应用层队列) */
ProcessReceivedMessage(rx_id, rx_data, rx_dlc);
}
}
避坑指南:我曾经在中断服务函数里做了太多处理,导致下一个报文来了还没处理完,结果丢包了。记住:中断服务函数要短小精悍,只做必要的数据读取和标志清除,真正的处理放到主循环或者任务里去做。
4.2.2 轮询接收——调试时的好帮手
轮询接收虽然效率不高,但在调试阶段特别好用。我经常在刚开始调试CAN通信时先用轮询方式,确认硬件没问题了再切换到中断方式。
/* 轮询接收函数 */
uint8_t CAN_PollingReceive(uint8_t *id, uint8_t *data, uint8_t *len)
{
uint8_t i;
/* 检查是否有报文到达 */
if (!(CAN->STS & 0x02)) {
return 0xFF; /* 没有报文 */
}
/* 读取报文ID */
if (CAN->RFT & 0x01) {
*id = CAN->REID; /* 扩展帧 */
} else {
*id = CAN->RSID; /* 标准帧 */
}
/* 读取数据长度 */
*len = CAN->RDLC & 0x0F;
/* 读取数据 */
for (i = 0; i < *len; i++) {
data[i] = CAN->RDB[i];
}
/* 清除接收标志 */
CAN->STS &= ~0x02;
return 0x00; /* 接收成功 */
}
/* 主循环中调用 */
void main(void)
{
uint8_t id, data[8], len;
while (1) {
if (CAN_PollingReceive(&id, data, &len) == 0x00) {
/* 处理接收到的报文 */
printf("收到报文 ID=0x%02X, 长度=%d\n", id, len);
}
/* 其他任务 */
DoOtherTasks();
}
}
4.3 FIFO机制——给接收报文一个缓冲区
CMS32的CAN模块通常有接收FIFO。这个FIFO是个好东西,它能缓存多个接收到的报文。你想想看,如果CPU正在处理一个报文,突然又来了三个,没有FIFO的话,后面的报文就丢了。
FIFO的工作原理:
- 硬件自动将接收到的报文存入FIFO
- 软件从FIFO中按顺序读取
- FIFO满了之后,新报文会覆盖最旧的报文(或者丢弃,取决于配置)
关键参数:FIFO深度通常为3~8个报文。深度越大,缓冲能力越强,但读取延迟也越大。我一般设成4个,够用又不浪费。
4.3.1 FIFO的配置与使用
/* FIFO配置 */
void CAN_FIFO_Init(void)
{
/* 设置FIFO深度为4 */
CAN->FCR = 0x04; /* FIFO深度=4 */
/* 使能FIFO溢出覆盖模式(新报文覆盖最旧的) */
CAN->FCR |= 0x80;
/* 使能FIFO接收中断 */
CAN->IER |= 0x02;
}
/* 从FIFO读取报文 */
uint8_t CAN_ReadFromFIFO(uint8_t *id, uint8_t *data, uint8_t *len)
{
uint8_t i;
/* 检查FIFO是否为空 */
if (CAN->FSTS & 0x01) {
return 0xFF; /* FIFO空 */
}
/* 读取报文 */
if (CAN->FFT & 0x01) {
*id = CAN->FEID; /* 扩展帧 */
} else {
*id = CAN->FSID; /* 标准帧 */
}
*len = CAN->FDLC & 0x0F;
for (i = 0; i < *len; i++) {
data[i] = CAN->FDB[i];
}
/* 释放FIFO条目(告诉硬件我已经读完了) */
CAN->FSTS |= 0x02;
return 0x00;
}
我的习惯:用FIFO的时候,我一般会在中断里把FIFO里的报文全部读出来,放到一个更大的应用层环形缓冲区里。这样即使中断处理慢一点,也不容易丢包。
4.4 DMA传输——让数据自己跑起来
DMA传输是最高效的方式。CPU只需要告诉DMA控制器“把CAN接收到的数据搬到内存里”,然后就可以去干别的事了。DMA自己搞定一切,搬完了再通知CPU。
为什么会需要DMA?你想想看,如果CAN总线速率是1Mbps,每秒能收到几千个报文。如果每个报文都要CPU去读,那CPU啥也别干了。DMA就是来解决这个问题的。
4.4.1 DMA传输的配置
/* DMA传输配置 */
void CAN_DMA_Init(void)
{
/* 配置DMA通道0 */
DMA->CH0.CR = 0x00; /* 先复位 */
/* 源地址:CAN接收数据寄存器(固定地址) */
DMA->CH0.SAR = (uint32_t)&CAN->RDB[0];
/* 目的地址:内存缓冲区 */
DMA->CH0.DAR = (uint32_t)&can_rx_buffer[0];
/* 传输长度:8字节(一个CAN报文的数据部分) */
DMA->CH0.TCR = 8;
/* 配置传输模式:
* - 外设到内存
* - 源地址固定(CAN寄存器地址不变)
* - 目的地址递增
* - 传输完成产生中断
*/
DMA->CH0.CR = (0x01 << 4) | /* 外设到内存 */
(0x00 << 2) | /* 源地址固定 */
(0x01 << 1) | /* 目的地址递增 */
(0x01 << 0); /* 使能传输完成中断 */
/* 使能DMA通道 */
DMA->CH0.CR |= 0x80;
/* 将DMA与CAN接收事件关联 */
CAN->DMAREQ = 0x01; /* 接收完成触发DMA */
}
/* DMA传输完成中断 */
void DMA_CH0_IRQHandler(void)
{
/* 清除DMA中断标志 */
DMA->CH0.SR &= ~0x01;
/* 通知应用层:新的CAN报文已经到缓冲区了 */
can_rx_flag = 1;
/* 重新启动DMA传输(准备接收下一个报文) */
DMA->CH0.TCR = 8;
DMA->CH0.CR |= 0x80;
}
注意:DMA传输虽然高效,但配置起来比较麻烦。我踩过的坑是——DMA传输长度没设对,导致只搬了部分数据。记住:CAN报文数据长度是可变的(0~8字节),但DMA传输长度是固定的。我一般设成8字节,然后根据实际DLC判断有效数据。
4.5 四种接收方式的对比与选择
好了,四种方式都讲完了。咱们做个对比,方便你选型:
| 方式 | CPU负载 | 实时性 | 实现难度 | 推荐场景 |
|---|---|---|---|---|
| 轮询接收 | 高 | 低 | 低 | 调试、低速通信 |
| 中断接收 | 中 | 高 | 中 | 通用场景 |
| FIFO+中断 | 低 | 高 | 中 | 突发大量报文 |
| DMA+中断 | 极低 | 高 | 高 | 高速、大数据量 |
我个人建议:刚开始做项目时先用中断接收+FIFO,这个组合最稳妥。等系统稳定了,再考虑要不要上DMA。别一上来就搞DMA,调试起来够你喝一壶的。
总结一下:发送邮箱管理要记住“先检查再写入,先请求再等待”;接收方式的选择要权衡CPU负载和实时性;FIFO是防止丢包的好帮手;DMA是追求极致性能的选择。没有最好的方式,只有最适合你项目的方式。