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、数据字节 |
配置流程其实很简单,我总结成三步:
- 选择邮箱:确定用哪个邮箱(0/1/2),一般从0开始。
- 清空旧数据:把邮箱对应的状态位清零,防止上次发送残留影响。
- 使能中断(可选):如果你不想轮询,就打开对应邮箱的发送完成中断。
我的经验:刚开始做项目时,我习惯把三个邮箱都配成一样的。后来发现,其实可以把邮箱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 发送流程总览
说了这么多,咱们用一张图把整个发送流程串起来。这张图是我在实际项目中总结出来的,你照着这个流程走,基本不会出大问题。
这张图把整个发送流程分成了7个步骤。你仔细看,从配置邮箱到请求发送,再到等待完成,最后分叉成成功和失败两条路。实际项目中,我建议你把“超时/错误处理”这个分支做得健壮一些,不要只是简单地重试,而是要根据错误类型采取不同的策略。
总结一下:CAN报文发送看似简单,但细节决定成败。邮箱配置要选对、ID和DLC要填准、中断处理要高效、超时错误要兜底。把这四点吃透了,你在CAN发送这块就算出师了。