第四章:STM32 CAN基础收发编程

在 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 为什么要用环形缓冲区?

  1. 解耦: 接收中断负责"生产"数据,主循环(或低优先级任务)负责"消费"数据。

  2. 削峰填谷: 总线可能在一秒内爆发 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 驱动,请记住这三点:

  1. 配置并激活过滤器: 不要让无关报文进入 FIFO。

  2. 中断接收 + 环形缓冲区: 确保高负载下不丢包,且不阻塞中断。

  3. 发送逻辑加入超时判断: 不要死等邮箱空闲,必要时抛弃旧报文或报警。

相关推荐
椰羊~王小美2 小时前
嵌入式 和 单片机
java·单片机·嵌入式硬件
悠哉悠哉愿意3 小时前
【物联网学习笔记】TIM
笔记·单片机·嵌入式硬件·物联网·学习
豆包公子3 小时前
AUTOSAR CP故障诊断协议栈DEM(DTC故障管理)裸机实现-实践篇
单片机·嵌入式硬件·车载系统
Wave8454 小时前
STM32+ESP8266 智能手表实战:天气获取与阿里云时钟同步
stm32·阿里云·智能手表
汽车芯猿5 小时前
嵌入式 SHA-256 完全实现(附原码)(无 uint64_t,减少栈使用)
c语言·单片机
进击的小头5 小时前
第12篇:嵌入式核心外设科普:ADC_DAC模拟前端接口原理与典型应用
单片机·嵌入式硬件
水云桐程序员6 小时前
嵌入式系统开发 需要的环境配置
嵌入式硬件·物联网·硬件工程
CHANG_THE_WORLD6 小时前
PE文件解析器详细文档
stm32·单片机·嵌入式硬件
Z文的博客6 小时前
SLCAN工程搭建与实现教程(下)
stm32·单片机·嵌入式·can