2. STM32 CAN外设入门:bxCAN控制器介绍、CAN引脚配置、波特率计算、过滤器配置、中断与轮询模式

好,咱们今天来啃一块硬骨头——STM32的CAN外设。说实话,我刚接触CAN总线那会儿,最头疼的就是这个bxCAN控制器。名字听着挺唬人,什么"基本扩展CAN",其实就是ST在F1/F4系列上标配的CAN模块。你把它想成一个独立的通信小管家就行,你只管把数据丢给它,它自己搞定总线仲裁、错误重发这些脏活累活。

2.1 bxCAN控制器长什么样?

bxCAN的核心结构,我习惯把它拆成三块来看:

  • 协议控制器:负责物理层到数据链路层的转换,说白了就是处理CAN帧的收发时序
  • 报文缓冲区:3个发送邮箱 + 2个接收FIFO(每个FIFO深3级),这是数据的中转站
  • 过滤器组:最多28个(F103系列),用来决定哪些报文能进你的接收缓冲区

我个人觉得,理解bxCAN的关键在于"邮箱"这个概念。发送邮箱就像三个并排的信箱,你把信塞进去,硬件自动帮你寄出去。接收FIFO则像两个收件箱,硬件收到信后按顺序放好,你按时去取就行。

核心要点:bxCAN支持CAN 2.0A和2.0B协议,标准帧11位ID,扩展帧29位ID。速率最高1Mbps,但实际项目中我一般用到500kbps或250kbps,稳定性更好。

STM32 bxCAN 控制器内部结构 CPU 接口 控制寄存器 状态寄存器 中断寄存器 bxCAN 核心模块 发送邮箱 0 TIR/TDT/TDL 发送邮箱 1 TIR/TDT/TDL 发送邮箱 2 TIR/TDT/TDL FIFO 0 3级深度 RIR/RDT/RDL FIFO 1 3级深度 RIR/RDT/RDL 过滤器 组0 组1 ... 组27 列表/掩码 16位/32位 CAN 总线接口 CAN_TX (PA12) CAN_RX (PA11) CAN收发器 (TJA1050等)

2.2 CAN引脚配置——别接错了

STM32的CAN引脚是固定的,不能随便映射。以F103为例:

  • CAN1_TX:PA12(默认),也可重映射到PB9、PD1
  • CAN1_RX:PA11(默认),也可重映射到PB8、PD0

我曾经犯过一个低级错误:把CAN_TX和CAN_RX接反了。结果总线死活没反应,查了半天才发现。记住:TX接收发器的TX,RX接收发器的RX,别交叉!

配置代码其实很简单,我一般这样写:

// CAN GPIO 初始化
void CAN_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
    
    // TX - 复用推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    // RX - 浮空输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

嗯,这里要注意:TX必须是复用推挽输出,RX必须是浮空输入。别问我为什么,CAN收发器就是这么要求的。

2.3 波特率计算——算不对就白搭

CAN的波特率计算,说白了就是分频。STM32的CAN挂在APB1总线上,时钟一般是36MHz(F103)或42MHz(F4系列)。

计算公式:

波特率 = CAN时钟 / (BRP + 1) / (tBS1 + tBS2 + 1)

其中:
- BRP:波特率预分频器(0~127)
- tBS1:时间段1(1~16)
- tBS2:时间段2(1~8)
- 同步段固定为1个时间量子

举个例子,我要配500kbps:

CAN时钟 = 36MHz
目标波特率 = 500kbps

36MHz / 500kbps = 72 个时间量子
72 = (BRP+1) * (tBS1 + tBS2 + 1)

取 BRP = 3,则 (BRP+1) = 4
72 / 4 = 18 = tBS1 + tBS2 + 1
取 tBS1 = 13, tBS2 = 4,则 13+4+1=18 ✓

我个人习惯:采样点设置在87.5%左右。公式是:采样点 = (1 + tBS1) / (1 + tBS1 + tBS2)。上面例子中采样点 = (1+13)/(1+13+4) = 14/18 ≈ 77.8%,其实偏低。我会调整成tBS1=14, tBS2=3,采样点=15/18=83.3%,更稳妥。

目标波特率 CAN时钟 BRP tBS1 tBS2 实际波特率 采样点
1Mbps 36MHz 1 8 8 1.0Mbps 50.0%
500kbps 36MHz 3 14 3 500kbps 83.3%
250kbps 36MHz 7 14 3 250kbps 83.3%
125kbps 36MHz 15 14 3 125kbps 83.3%

避坑指南:同一个CAN网络上的所有节点,波特率必须完全一致。我曾经调试一个系统,两个设备都配了500kbps,但一个采样点设成80%,另一个设成87.5%,结果偶尔丢帧。折腾了两天才发现是采样点不匹配。

2.4 过滤器配置——别让垃圾数据进来

过滤器是bxCAN最强大的功能之一。说白了,就是硬件级别的"白名单"。只有符合过滤条件的报文,才能进入接收FIFO。

过滤器有两种工作模式:

  • 标识符列表模式:只接收指定ID的报文,精确匹配
  • 标识符掩码模式:接收符合掩码范围的报文,模糊匹配

举个例子,我只想接收ID为0x321和0x322的报文:

// 过滤器配置 - 列表模式,接收0x321和0x322
void CAN_Filter_Config(void)
{
    CAN_FilterInitTypeDef CAN_FilterInitStructure;
    
    CAN_FilterInitStructure.CAN_FilterNumber = 0;       // 使用过滤器组0
    CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList;  // 列表模式
    CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 32位
    
    // 标准帧ID存储在[28:18]位
    CAN_FilterInitStructure.CAN_FilterIdHigh = (0x321 << 5) >> 16;  // 高16位
    CAN_FilterInitStructure.CAN_FilterIdLow = (0x321 << 5) | CAN_ID_STD;
    
    CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (0x322 << 5) >> 16;
    CAN_FilterInitStructure.CAN_FilterMaskIdLow = (0x322 << 5) | CAN_ID_STD;
    
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0;  // 关联到FIFO0
    CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
    
    CAN_FilterInit(&CAN_FilterInitStructure);
}

我建议:在项目初期,先用掩码模式把过滤器设成"全通"(掩码全0),等通信调通了再细化过滤规则。这样可以先排除过滤器配置错误导致的通信问题。

2.5 中断与轮询模式——什么时候用哪个?

这个问题,说白了就是"你愿不愿意等"。轮询模式就是CPU一直问"有消息吗?有消息吗?",中断模式就是"有消息了你叫我"。

轮询模式

// 轮询接收
uint8_t CAN_Polling_Receive(CAN_TypeDef* CANx, CanRxMsg* RxMessage)
{
    uint32_t timeout = 0xFFFF;
    
    // 等待FIFO非空
    while (CAN_MessagePending(CANx, CAN_FIFO0) == 0)
    {
        timeout--;
        if (timeout == 0) return 0;  // 超时返回
    }
    
    CAN_Receive(CANx, CAN_FIFO0, RxMessage);
    return 1;
}

中断模式

// 使能接收中断
CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);  // FIFO0消息挂起中断

// 中断服务函数
void USB_LP_CAN1_RX0_IRQHandler(void)
{
    CanRxMsg RxMessage;
    
    if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET)
    {
        CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
        // 处理接收到的数据
        
        CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0);
    }
}

我的经验:运动控制场景下,我强烈建议用中断模式。轮询会占用CPU时间,导致控制周期抖动。特别是当CAN报文频率高时(比如1ms发送一次),轮询模式基本不可用。中断模式虽然配置麻烦点,但实时性好太多。

不过话说回来,如果你只是做简单的数据采集,比如温度传感器每秒上报一次,轮询模式完全够用,代码还简单。别为了用中断而用中断,合适才是最好的。

好了,bxCAN的基础配置就这些。你把这些搞明白了,CAN通信的基本骨架就有了。下一节咱们开始写真正的CANopen协议栈,那才是重头戏。


公众号:蓝海资料掘金营,微信deep3321