3. CAN报文发送实战:发送邮箱配置、标识符与数据长度设置、请求发送与发送完成中断处理、发送超时与错误处理

各位同学,今天咱们来啃一块硬骨头——CAN报文的发送。说实话,很多工程师在CAN通信上栽跟头,十有八九都出在发送环节。不是发不出去,就是发出去没人理,要么就是莫名其妙地总线错误。今天我就把CMS32上CAN发送的完整流程拆开揉碎了讲给你听。

核心要点:CAN发送的本质是“邮箱投递”。硬件给你准备了几个邮箱(Mailbox),你把报文填进去,贴上标签(标识符),告诉它长度,然后按一下“发送”按钮。剩下的,就看总线的仲裁结果了。

3.1 发送邮箱的配置

CMS32的CAN模块通常有3个发送邮箱。我个人习惯把它们看作三个独立的“发件窗口”。你可以同时准备三封信,但一次只能发一封,谁先抢到总线谁先走。

配置邮箱的第一步,是把它从“空闲”状态切换到“就绪”状态。这需要操作CAN控制器的一组寄存器。以CMS32为例,核心寄存器如下:

寄存器 功能 关键位
CAN_TxRQR 发送请求寄存器 TxRq[2:0]:对应3个邮箱的发送请求
CAN_TxSR 发送状态寄存器 TxOk[2:0]:发送成功标志;TxBusy[2:0]:发送忙标志
CAN_TxIER 发送中断使能寄存器 TxIE[2:0]:对应邮箱发送完成中断使能
CAN_TxMBX 发送邮箱数据寄存器组 包含ID、DLC、数据字节

配置流程其实很简单,我总结成三步:

  1. 选择邮箱:确定用哪个邮箱(0/1/2),一般从0开始。
  2. 清空旧数据:把邮箱对应的状态位清零,防止上次发送残留影响。
  3. 使能中断(可选):如果你不想轮询,就打开对应邮箱的发送完成中断。

我的经验:刚开始做项目时,我习惯把三个邮箱都配成一样的。后来发现,其实可以把邮箱0配成高优先级(用于紧急报文),邮箱1和2配成普通优先级。这样在总线负载高时,关键数据能优先发送。

3.2 标识符与数据长度设置

标识符(ID)就是报文的“身份证”。CAN总线上的所有节点,都是靠这个ID来决定要不要接收你的报文。CMS32支持标准帧(11位ID)和扩展帧(29位ID)。

设置ID时,有个坑我必须提醒你:ID的高位在前。也就是说,你往寄存器里写ID时,要把ID左移几位再填进去。具体移多少,得看芯片手册。CMS32的寄存器布局是这样的:

// 标准帧ID设置示例(11位ID = 0x123)
CAN_TxMBX[0].ID = 0x123 << 5;  // ID放在高11位,低5位为控制位

// 扩展帧ID设置示例(29位ID = 0x12345678)
CAN_TxMBX[0].ID = 0x12345678 << 3;  // 29位ID放在高29位,低3位为控制位

数据长度码(DLC)就简单多了。它告诉总线你这包数据有多长。取值范围0~8,单位是字节。注意:DLC不能超过8,否则接收方会认为你发的是无效帧。

DLC值 数据长度(字节) 说明
0 0 远程帧或空数据帧
1~8 1~8 正常数据帧
9~15 8 实际仍按8字节处理(部分控制器会报错)

避坑指南:我曾经在一个项目里,把DLC设成了9,结果接收方死活收不到完整数据。查了两天才发现,DLC超过8时,有些控制器会直接丢弃报文。所以,老老实实把DLC控制在0~8之间。

3.3 请求发送与发送完成中断处理

数据填好了,接下来就是“按发送按钮”。在CMS32上,这个按钮就是置位发送请求寄存器(TxRQR)的对应位。

// 请求发送邮箱0
CAN_TxRQR |= (1 << 0);  // 置位TxRq0位

就这么简单?对,硬件会自动处理后续的仲裁和发送。但问题来了:你怎么知道它发完了?

两种方式:

  • 轮询:死循环读TxBusy位,直到它变0。简单粗暴,但浪费CPU。
  • 中断:打开发送完成中断,让硬件告诉你“我发完了”。

我个人强烈推荐用中断方式。尤其是在低功耗场景下,轮询会阻止CPU进入休眠模式。中断的配置也很直接:

// 使能邮箱0发送完成中断
CAN_TxIER |= (1 << 0);  // 置位TxIE0位

// 中断服务函数(伪代码)
void CAN_Tx_IRQHandler(void) {
    if (CAN_TxSR & (1 << 0)) {  // 检查邮箱0是否发送完成
        // 发送成功,可以准备下一包数据了
        CAN_TxSR &= ~(1 << 0);  // 清除标志位
        // 这里可以唤醒等待发送的任务
    }
}

我的习惯:在中断服务函数里,我通常只做两件事:清标志位、发信号量。真正的数据处理放在任务级代码里。这样中断服务函数执行时间短,不会影响其他中断的响应。

3.4 发送超时与错误处理

报文发不出去怎么办?这是实际项目中最常见的问题。原因无非以下几种:

  • 总线忙:总线上有其他节点在发数据,你的报文仲裁失败。
  • 总线错误:物理层有问题,比如CAN_H和CAN_L短路、终端电阻缺失。
  • 邮箱被锁:上次发送还没完成,你又往同一个邮箱里填了新数据。

针对这些情况,我建议你实现一个“发送超时检测”机制。说白了,就是启动一个定时器,如果在规定时间内没收到发送完成中断,就认为发送失败。

// 发送超时检测伪代码
#define TX_TIMEOUT_MS 10  // 10ms超时

uint8_t CAN_SendMsg(uint32_t id, uint8_t *data, uint8_t len) {
    // 1. 检查邮箱是否空闲
    if (CAN_TxSR & (1 << 0)) {
        return CAN_BUSY;  // 邮箱正忙
    }
    
    // 2. 填充数据
    CAN_TxMBX[0].ID = id << 5;
    CAN_TxMBX[0].DLC = len;
    for (int i = 0; i < len; i++) {
        CAN_TxMBX[0].Data[i] = data[i];
    }
    
    // 3. 请求发送
    CAN_TxRQR |= (1 << 0);
    
    // 4. 等待发送完成或超时
    uint32_t tick_start = GetSysTick();
    while (!(CAN_TxSR & (1 << 0))) {
        if ((GetSysTick() - tick_start) > TX_TIMEOUT_MS) {
            // 超时!取消发送请求
            CAN_TxRQR &= ~(1 << 0);
            return CAN_TIMEOUT;
        }
    }
    
    return CAN_OK;
}

错误处理方面,CMS32的CAN控制器有错误状态寄存器(CAN_ESR)。你可以读取它来判断错误类型:

错误位 含义 处理建议
EW 错误警告(Error Warning) 记录日志,继续尝试发送
EP 错误被动(Error Passive) 降低发送频率,避免影响总线
BO 总线关闭(Bus Off) 停止发送,等待128个总线空闲位后自动恢复

避坑指南:我曾经在一个项目中,总线关闭后没有做任何处理,结果CAN模块一直处于离线状态,整个系统通信瘫痪。后来我加了一个“总线恢复”机制:检测到Bus Off后,主动复位CAN控制器,并等待128个空闲位。嗯,从那以后,再也没出过类似问题。

3.5 发送流程总览

说了这么多,咱们用一张图把整个发送流程串起来。这张图是我在实际项目中总结出来的,你照着这个流程走,基本不会出大问题。

CAN报文发送完整流程图 CAN报文发送完整流程图 配置发送邮箱 设置ID和DLC 填充数据字节 请求发送(置位TxRq) 等待发送完成 发送成功(TxOk置位) 超时/错误处理 成功 失败 说明:发送完成后,建议在中断中清除TxOk标志位,并释放信号量通知任务层

这张图把整个发送流程分成了7个步骤。你仔细看,从配置邮箱到请求发送,再到等待完成,最后分叉成成功和失败两条路。实际项目中,我建议你把“超时/错误处理”这个分支做得健壮一些,不要只是简单地重试,而是要根据错误类型采取不同的策略。

总结一下:CAN报文发送看似简单,但细节决定成败。邮箱配置要选对、ID和DLC要填准、中断处理要高效、超时错误要兜底。把这四点吃透了,你在CAN发送这块就算出师了。