4. 数据发送与接收:发送邮箱管理、中断接收与轮询接收、FIFO机制与DMA传输

CAN通信说到底就两件事——把数据发出去,把数据收回来。但怎么发、怎么收,这里面的门道可不少。我刚开始接触CMS32的CAN模块时,觉得不就是写个寄存器嘛,结果调试起来才发现,发送邮箱的管理、接收方式的选择,每一个细节都能让你折腾半天。

这一章咱们就掰开揉碎,把发送和接收的几种方式讲清楚。你想想看,如果发送邮箱用不好,高优先级的报文可能被低优先级的堵住;如果接收方式选不对,CPU就得一直忙着处理CAN中断,别的活都干不了。

4.1 发送邮箱管理——别让报文堵在路上

CMS32的CAN控制器通常有3个发送邮箱。每个邮箱都可以独立配置,用来发送不同的CAN报文。说白了,就是三个独立的发送通道,谁准备好了谁就上总线。

核心要点:发送邮箱的优先级由报文ID决定,不是由邮箱编号决定。ID越小,优先级越高。

4.1.1 发送邮箱的工作流程

我习惯把发送邮箱比作三个快递员。每个快递员手里只能拿一个包裹,送完才能接下一个。具体流程是这样的:

  1. 准备数据:把报文ID、数据长度、数据内容写入邮箱寄存器
  2. 请求发送:置位发送请求位,告诉CAN控制器“这个邮箱要发”
  3. 仲裁发送:CAN控制器根据报文ID仲裁,优先级高的先上总线
  4. 发送完成:总线空闲且仲裁成功,报文发出,硬件自动清除发送请求位
  5. 状态检查:软件可以查询发送完成标志,或者等中断通知

嗯,这里要注意:如果三个邮箱同时请求发送,硬件会按报文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一直在轮询,那它啥也干不了。中断接收就不一样了,报文来了才处理,平时该干嘛干嘛。

中断接收的配置步骤:

  1. 使能CAN接收中断
  2. 配置中断优先级
  3. 编写中断服务函数
  4. 在中断服务函数中读取报文
/* 中断接收初始化 */
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是追求极致性能的选择。没有最好的方式,只有最适合你项目的方式。

CAN数据发送与接收知识体系 发送邮箱管理 接收方式 高级机制 3个独立邮箱 ID仲裁优先级 发送请求与完成标志 中断接收 轮询接收 FIFO缓冲接收 DMA传输 外设到内存直传 传输完成中断 选择建议 调试用轮询 → 通用用中断+FIFO → 高性能用DMA 发送邮箱管理 + 多种接收方式 + FIFO/DMA 构成CMS32 CAN通信的完整数据通路