在 STM32 的 HAL 库中,CAN 的收发看似简单,实则暗藏玄机。我们要实现的不是简单的"通了",而是"在 100% 总线负载下依然不丢包、不阻塞"。
1. 发送逻辑:别让"邮箱满"阻塞了你的主程序
1.1 基础发送函数解析
STM32 发送一个 CAN 报文,需要配置两个结构体:CAN_TxHeaderTypeDef(报文头)和 uint8_t aData[8](数据)。
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8];
uint32_t TxMailbox;
// 配置报文头
TxHeader.StdId = 0x123; // 标准 ID
TxHeader.RTR = CAN_RTR_DATA; // 数据帧
TxHeader.IDE = CAN_ID_STD; // 标准标识符
TxHeader.DLC = 8; // 数据长度为 8 字节
TxHeader.TransmitGlobalTime = DISABLE;
// 发送函数
if (HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox) != HAL_OK) {
// 如果发送失败(通常是因为 3 个邮箱都满了)
// 处理错误或重试
}
1.2 工业级优化:解决"发送阻塞"
在高速通讯中,如果你连续调用发送函数,而总线由于仲裁失败或物理故障暂时无法发送,3 个邮箱会瞬间爆满。此时 HAL_CAN_AddTxMessage 会返回错误。
工程策略:
-
不要在主循环里死等(while): 这样会导致整个系统 UI 或控制逻辑卡死。
-
利用发送中断: 开启
HAL_CAN_ActivateNotification(hcan, CAN_IT_TX_MAILBOX_EMPTY)。当一个邮箱发完空出来时,在回调函数里触发下一条数据的发送。 -
软件 FIFO: 建立一个应用层的"待发送队列"。应用层只负责把数据丢进队列,驱动层通过中断自动把队列里的数"喂"给邮箱。
2. 接收逻辑:中断是唯一正确的姿势
很多初学者喜欢在 main 函数里轮询 HAL_CAN_GetRxFifoFillLevel。这在工业场景是极度危险的。一旦 CPU 处理某个逻辑耗时稍长(比如写 Flash 或更新屏幕),FIFO 就会溢出。
2.1 开启接收中断
首先,必须开启接收通知:
// 在初始化完成后开启 FIFO0 的挂起中断
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
2.2 编写中断回调函数
当总线上有符合过滤规则的报文到来并进入 FIFO0 时,硬件会触发中断,HAL 库会调用 HAL_CAN_RxFifo0MsgPendingCallback。
关键点: 中断服务函数(ISR)要尽可能短!千万不要在中断里解析复杂的协议或打印 printf。
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
// 从 FIFO 中提取报文
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK) {
// 方案 A:直接处理(仅限逻辑非常简单的情况)
// 方案 B:推入环形缓冲区(推荐,工业标准)
CAN_Buffer_Push(&RxHeader, RxData);
}
}
3. 核心黑科技:环形缓冲区(Ring Buffer)
为了应对突发的大规模数据流入,我们需要在内存中开辟一块"蓄水池"。
3.1 为什么要用环形缓冲区?
-
解耦: 接收中断负责"生产"数据,主循环(或低优先级任务)负责"消费"数据。
-
削峰填谷: 总线可能在一秒内爆发 100 条报文,然后空闲三秒。缓冲区能保证数据不丢失,让 CPU 慢慢处理。
3.2 缓冲区结构示例
typedef struct {
CAN_RxHeaderTypeDef header;
uint8_t data[8];
} CAN_Message_Frame;
#define CAN_BUF_SIZE 64
CAN_Message_Frame CAN_RxBuffer[CAN_BUF_SIZE];
uint16_t head = 0; // 生产者索引
uint16_t tail = 0; // 消费者索引
void CAN_Buffer_Push(CAN_RxHeaderTypeDef *h, uint8_t *d) {
uint16_t next = (head + 1) % CAN_BUF_SIZE;
if (next != tail) { // 缓冲区未满
CAN_RxBuffer[head].header = *h;
memcpy(CAN_RxBuffer[head].data, d, 8);
head = next;
}
}
4. 健壮性设计:错误处理与"自动复活"
在工业现场,由于静电或电磁脉冲,CAN 总线可能会进入错误被动状态(Error Passive)甚至离线状态(Bus-Off)。
4.1 自动离线恢复
在第二章配置中,我们将 AutoBusOff 设为了 ENABLE。这意味着当硬件检测到总线恢复正常时,会自动重新加入网络。
4.2 错误监听
你应该开启错误中断 CAN_IT_ERROR。在错误回调中记录日志,或者通过 LED 灯闪烁告知现场人员:总线物理层出问题了!
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) {
uint32_t err = HAL_CAN_GetError(hcan);
// 常见的错误:ACK 错误(没人理你)、填充错误(干扰)、位错误
// 可以在这里通过串口打印错误码用于排查硬件问题
}
5. 本章工程总结:收发"三板斧"
要写出健壮的 STM32 CAN 驱动,请记住这三点:
-
配置并激活过滤器: 不要让无关报文进入 FIFO。
-
中断接收 + 环形缓冲区: 确保高负载下不丢包,且不阻塞中断。
-
发送逻辑加入超时判断: 不要死等邮箱空闲,必要时抛弃旧报文或报警。