4. CAN通信软件编程:STM32 CAN外设初始化、发送与接收中断、过滤器配置

好,咱们进入实战环节。前面聊了那么多CAN协议的理论,说白了都是纸上谈兵。今天咱们就真刀真枪地干,把STM32的CAN外设用起来。我个人习惯,学任何外设,先看寄存器,再看库函数,最后看代码怎么跑。这样出了问题,你心里有底。

4.1 STM32 CAN外设初始化——别小看这一步

初始化这事儿,看着简单,其实坑不少。我刚开始做BMS项目时,CAN怎么都调不通,折腾了两天,最后发现是时钟配置错了。嗯,这里要注意,STM32的CAN外设挂载在APB1总线上,时钟频率通常是36MHz或42MHz,具体看你用的芯片型号。

初始化流程,我总结为三步走:

  1. 使能时钟——别漏了GPIO时钟和CAN时钟
  2. 配置GPIO——CAN_TX和CAN_RX引脚,要设为复用功能
  3. 设置CAN参数——波特率、工作模式、中断使能

来看一段实际代码,这是我项目里扒出来的:

void CAN_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    CAN_InitTypeDef CAN_InitStructure;
    CAN_FilterInitTypeDef CAN_FilterInitStructure;

    // 1. 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);

    // 2. 配置GPIO
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;  // CAN1_RX
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;  // CAN1_TX
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    // 3. CAN参数配置
    CAN_DeInit(CAN1);
    CAN_StructInit(&CAN_InitStructure);
    CAN_InitStructure.CAN_TTCM = DISABLE;
    CAN_InitStructure.CAN_ABOM = ENABLE;   // 自动离线管理
    CAN_InitStructure.CAN_AWUM = ENABLE;   // 自动唤醒
    CAN_InitStructure.CAN_NART = DISABLE;  // 自动重发
    CAN_InitStructure.CAN_RFLM = DISABLE;  // 接收FIFO锁定
    CAN_InitStructure.CAN_TXFP = DISABLE;  // 优先级由ID决定
    CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;
    CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
    CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq;
    CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq;
    CAN_InitStructure.CAN_Prescaler = 4;   // 36MHz / (1+8+7) / 4 = 500kbps
    CAN_Init(CAN1, &CAN_InitStructure);
}

小技巧:波特率计算其实很简单。APB1时钟36MHz,分频系数4,得到9MHz。再除以(1+8+7)=16个时间份额,就是500kbps。你想想看,BMS系统里500kbps是主流,兼容性好。

4.2 发送数据——别一股脑全发出去

CAN发送有三个邮箱,说白了就是三个发送缓冲区。你发数据时,硬件会自动选一个空闲的邮箱来用。但我建议你,别依赖硬件自动选择,自己管理发送队列更靠谱。

为什么?我在项目里遇到过,BMS上报电池状态时,如果突然来了一个紧急故障报文,你得保证它能插队发送。硬件自动选择是按邮箱编号来的,你控制不了优先级。

发送代码示例:

uint8_t CAN_SendMsg(uint32_t id, uint8_t *data, uint8_t len)
{
    CanTxMsg TxMessage;
    uint8_t mailbox;
    uint32_t timeout = 0;

    TxMessage.ExtId = 0;
    TxMessage.IDE = CAN_Id_Standard;  // 标准帧
    TxMessage.RTR = CAN_RTR_Data;     // 数据帧
    TxMessage.DLC = len;
    TxMessage.StdId = id;

    for(uint8_t i = 0; i < len; i++)
    {
        TxMessage.Data[i] = data[i];
    }

    mailbox = CAN_Transmit(CAN1, &TxMessage);

    // 等待发送完成,超时处理
    while(CAN_TransmitStatus(CAN1, mailbox) != CAN_TxStatus_Ok)
    {
        timeout++;
        if(timeout > 0xFFFF)
        {
            return 0;  // 发送超时
        }
    }
    return 1;
}

注意:发送超时一定要处理!我曾经在BMS项目中,因为CAN总线被某个节点拉死,发送一直不成功,程序就卡死在等待循环里。整个BMS死机,电池包保护都失效了。后来我加了个超时机制,问题解决。

4.3 接收中断——别让CPU傻等

接收数据有两种方式:轮询和中断。轮询就是CPU不停地问"有数据吗?有数据吗?",效率极低。中断是数据来了,硬件通知CPU去处理。你说哪个好?

在BMS系统里,我强烈建议用中断接收。电池数据随时可能变化,你不可能让CPU一直轮询CAN接收。而且中断响应快,不会丢帧。

中断配置代码:

void CAN_NVIC_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;

    NVIC_InitStructure.NVIC_IRQChannel = CAN1_RX0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // 使能FIFO0接收中断
    CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
}

中断服务函数:

void CAN1_RX0_IRQHandler(void)
{
    CanRxMsg RxMessage;
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

    // 根据ID处理不同报文
    switch(RxMessage.StdId)
    {
        case 0x100:  // BMS主控指令
            Process_MasterCmd(RxMessage.Data);
            break;
        case 0x200:  // 充电机状态
            Process_ChargerStatus(RxMessage.Data);
            break;
        case 0x300:  // 继电器状态
            Process_RelayStatus(RxMessage.Data);
            break;
        default:
            break;
    }
}

核心要点:中断服务函数里别做复杂处理。收到数据后,设个标志位,或者用消息队列把数据扔给主循环处理。我在BMS项目里吃过亏,中断里做CRC校验,耗时太长,导致后续报文丢失。

4.4 过滤器配置——别什么垃圾都收

CAN总线上报文很多,你不可能全收。过滤器就是帮你筛选的,只收你关心的报文。STM32的CAN有14个过滤器组,每个组可以配置成屏蔽位模式或列表模式。

我个人习惯用屏蔽位模式。举个例子,BMS系统里,所有电池相关的报文ID范围是0x100到0x1FF,我设一个屏蔽位,只收这个范围的报文。

过滤器配置代码:

void CAN_Filter_Config(void)
{
    CAN_FilterInitTypeDef CAN_FilterInitStructure;

    CAN_FilterInitStructure.CAN_FilterNumber = 0;
    CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
    CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
    CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;
    CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;
    CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0FF0;  // 只关心高11位
    CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0;
    CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
    CAN_FilterInit(&CAN_FilterInitStructure);
}

这里解释一下屏蔽位怎么用。你设的ID是0x000,屏蔽位是0x0FF0。意思是:ID的高11位中,只有bit11-bit4这8位参与比较,其他位忽略。说白了,就是只匹配ID的高8位。

模式 说明 适用场景
屏蔽位模式 匹配一组ID范围 接收多个相关报文
列表模式 精确匹配单个ID 只收特定报文

避坑指南:我曾经在调试时,过滤器怎么配都不对,收不到数据。后来发现是ID格式搞错了。标准帧ID是11位,扩展帧是29位。你配过滤器时,一定要搞清楚报文用的是哪种格式。嗯,这个坑我踩过,你别再踩了。

4.5 实战经验总结

说了这么多,我总结几条实战经验:

  • 初始化顺序不能乱:先时钟,再GPIO,最后CAN参数。顺序错了,外设起不来。
  • 波特率要统一:总线上所有节点的波特率必须一致。我见过有人把500k和250k混在一起,结果谁也收不到谁的数据。
  • 中断优先级要合理:CAN中断优先级别设太高,否则会抢占其他关键任务。BMS里,电池保护的中断优先级应该高于CAN接收中断。
  • 过滤器别太松:过滤器设得太宽,CPU会被无关报文打扰。设得太严,又可能漏掉重要报文。建议先分析好报文ID分布,再配过滤器。

好了,这一章的内容就到这儿。CAN外设的初始化、发送、接收和过滤器配置,说白了就是三板斧。你把这几个函数写好,CAN通信的基础就搭起来了。下一章咱们聊聊CANopen协议在BMS中的应用,那个更有意思。