3、CAN报文收发实战:使用STM32CubeMX配置CAN、标准帧与扩展帧发送、中断接收与轮询接收
好,咱们直接进入正题。上一章我们把CAN的理论基础讲完了,这一章就是真刀真枪地干。我会带着你,一步步用STM32CubeMX把CAN配置出来,然后写代码实现标准帧和扩展帧的发送,再分别用中断和轮询两种方式把数据收回来。
说实话,CAN通信这块,很多人卡就卡在配置上。CubeMX虽然方便,但有些细节不注意,板子就是跑不起来。我当年第一次调CAN,折腾了整整一个下午,最后发现是GPIO的复用功能没选对……嗯,这种坑咱们今天必须绕过去。
3.1 用STM32CubeMX配置CAN外设
打开CubeMX,选好你的芯片型号。我习惯用STM32F103系列做演示,因为它的CAN控制器(bxCAN)非常典型,其他系列配置思路基本一样。
第一步,把CAN1的外设使能。在Pinout & Configuration标签页里,找到Connectivity -> CAN1,勾选Activate。
第二步,配置GPIO。CAN需要两个引脚:CAN_TX和CAN_RX。在F103上,默认是PA11(CAN_RX)和PA12(CAN_TX)。你可以在Pinout视图里直接点这两个引脚,把它们的功能选为CAN1_RX和CAN1_TX。这里有个关键点——GPIO的复用功能要选对,我见过有人把PA11配成了普通输入,结果CAN死活收不到数据。
第三步,配置CAN参数。在CAN1的Configuration页面里,重点看这几个参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Prescaler (分频系数) | 9 | 配合后面的时间段,把CAN时钟调到1MHz |
| Time Quanta in Bit Segment 1 | 13 | 采样点位置,一般设13~15 |
| Time Quanta in Bit Segment 2 | 2 | 相位缓冲段2 |
| SJW (同步跳转宽度) | 1 | 用于时钟同步,一般设1 |
上面这组配置,配合72MHz的APB1时钟,算出来的波特率是:72MHz / (9 * (1+13+2)) = 500kbps。这是工业上最常用的CAN波特率,兼容性最好。
第四步,配置中断。在NVIC Settings标签页里,把CAN1 RX0中断和CAN1 SCE中断都使能。RX0是接收中断,SCE是状态错误中断。调试阶段建议都打开,方便排查问题。
配置完这些,点Generate Code生成代码。CubeMX会帮你把HAL库的初始化函数都写好,我们只需要在用户代码区里填业务逻辑就行。
3.2 标准帧与扩展帧的发送
CAN报文发送,核心就一个函数:HAL_CAN_AddTxMessage。这个函数会把报文放到发送邮箱里,硬件自动发出去。
先看标准帧的发送。标准帧的ID是11位的,范围0x000~0x7FF。我写个例子:
CAN_TxHeaderTypeDef txHeader;
uint8_t txData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t txMailbox;
// 配置标准帧头
txHeader.StdId = 0x123; // 标准ID,11位
txHeader.ExtId = 0; // 扩展ID,标准帧不用
txHeader.IDE = CAN_ID_STD; // 标准帧标志
txHeader.RTR = CAN_RTR_DATA; // 数据帧,不是远程帧
txHeader.DLC = 8; // 数据长度,最大8字节
// 发送
if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, txData, &txMailbox) != HAL_OK)
{
// 发送失败处理
Error_Handler();
}
扩展帧呢?就是把IDE改成CAN_ID_EXT,然后填ExtId。扩展ID是29位的,范围0x00000000~0x1FFFFFFF。注意,ExtId虽然是个32位变量,但只有低29位有效。
txHeader.StdId = 0; // 扩展帧不用标准ID
txHeader.ExtId = 0x12345678; // 扩展ID,29位
txHeader.IDE = CAN_ID_EXT; // 扩展帧标志
txHeader.RTR = CAN_RTR_DATA;
txHeader.DLC = 8;
HAL_CAN_AddTxMessage(&hcan1, &txHeader, txData, &txMailbox);
发送完成后,txMailbox会返回用的是哪个邮箱(0、1或2)。如果你需要确认发送完成,可以用HAL_CAN_GetTxMailboxesFreeLevel检查邮箱状态,或者使能发送完成中断。
3.3 中断接收
中断接收是实际项目中最常用的方式。CPU不用一直等着,有数据来了自动进中断处理,效率高。
首先,在CubeMX生成的代码里,中断已经使能了。我们需要做的是在main.c里启动CAN接收中断:
// 启动CAN,并开启中断接收
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
然后,重写中断回调函数。HAL库提供了一个弱定义的HAL_CAN_RxFifo0MsgPendingCallback,我们在用户代码里覆盖它:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
if (hcan->Instance == CAN1)
{
// 从FIFO0读取报文
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK)
{
// 判断是标准帧还是扩展帧
if (rxHeader.IDE == CAN_ID_STD)
{
// 标准帧,ID在rxHeader.StdId里
printf("收到标准帧,ID: 0x%03X, 数据: ", rxHeader.StdId);
}
else
{
// 扩展帧,ID在rxHeader.ExtId里
printf("收到扩展帧,ID: 0x%08X, 数据: ", rxHeader.ExtId);
}
// 打印数据
for (int i = 0; i < rxHeader.DLC; i++)
{
printf("%02X ", rxData[i]);
}
printf("\r\n");
}
}
}
这里有个细节要注意:HAL_CAN_GetRxMessage必须在回调函数里调用,而且调用一次只能取一帧。如果FIFO里积压了多帧,这个回调会被连续触发多次,每次取一帧。你想想看,如果中断里处理时间太长,会不会丢帧?嗯,所以回调里尽量只做数据拷贝,别做复杂运算。
3.4 轮询接收
轮询接收适合对实时性要求不高,或者数据量很小的场景。说白了就是CPU主动去问:「有数据没?有数据没?」
轮询的核心是检查FIFO里有没有待处理的报文:
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
while (1)
{
// 检查FIFO0是否有待处理报文
if (HAL_CAN_GetRxFifoFillLevel(&hcan1, CAN_RX_FIFO0) > 0)
{
// 有数据,读取
if (HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK)
{
// 处理报文,和中断里一样
// ...
}
}
// 做其他事情
// ...
HAL_Delay(10); // 轮询间隔,别太频繁
}
轮询的缺点很明显:如果主循环在忙别的事,报文来了可能不能及时处理。但优点也简单——代码逻辑清晰,调试方便。我刚开始学CAN的时候,先用轮询把收发调通,再改成中断,这样出问题了容易定位。
HAL_CAN_GetRxFifoFillLevel返回的是FIFO里当前有多少帧待处理。如果连续多帧进来,你得循环调用HAL_CAN_GetRxMessage直到FIFO为空。否则,FIFO满了之后新报文会被丢弃,这就是所谓的「溢出」。
3.5 中断 vs 轮询:怎么选?
我个人的建议是:
- 中断接收:项目量产、实时性要求高、数据量大的场景。比如电机控制,电流环周期只有几十微秒,必须用中断。
- 轮询接收:调试阶段、数据量极小(比如几秒一帧)、或者主循环本身就很空闲的场景。
还有一种混合用法:中断里只把数据拷贝到环形缓冲区,主循环里再慢慢解析。这样既保证了实时性,又不会因为中断处理太久影响其他任务。我在一个多电机同步的项目里就是这么干的,效果很好。
好了,这一章的内容就到这。下一章我们会讲CAN的过滤器配置,这可是CAN通信里最灵活也最容易出错的地方。到时候我会把我在项目里踩过的过滤器坑,一个一个给你讲清楚。