第四章: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. 发送逻辑加入超时判断: 不要死等邮箱空闲,必要时抛弃旧报文或报警。

相关推荐
GQli20488 小时前
一天看懂一个原理图(day7)电源输入部分
单片机·嵌入式硬件
llilian_169 小时前
失真度测量仪校准 精准可靠的失真度校准检定测试仪筑牢检测根基 失真度检定装置
功能测试·单片机·嵌入式硬件·硬件工程
XiYang-DING9 小时前
【Java EE】UDP 编程核心类与方法
单片机·udp·java-ee
iCxhust10 小时前
点亮8086最小系统的LED
stm32·单片机·嵌入式硬件·51单片机·微机原理·8086最小系统·8088单板机
时空自由民.11 小时前
开环无感FOC与SPWM&SVPWM
单片机·嵌入式硬件
集芯微电科技有限公司11 小时前
替代TMUX1380A/TMUX1309A双向8:1单通道 4:1双通道控制多路复用器
人工智能·单片机·嵌入式硬件·生成对抗网络·计算机外设
Wallace Zhang11 小时前
SimpleFOC源码学习10(v2.3.2) - 电流传感器CurrentSense.cpp与CurrentSense.h
驱动开发·stm32·学习·电流环·simplefoc·foc电机控制
我要成为嵌入式大佬11 小时前
项目制作日记简介
单片机·嵌入式硬件
FreakStudio12 小时前
工控开发板从开箱到点亮 LED-恩智浦MCXE31B 实测:3 路 CAN + 以太网+自带调试器
python·单片机·嵌入式·大学生·面向对象·技术栈·并行计算·电子diy·电子计算机
猿来&如此12 小时前
【51单片机】开发板介绍
单片机·嵌入式硬件·51单片机