3. STM32 HAL库与CAN外设初始化:从CubeMX到实战

好,咱们进入第三章。这一章我打算手把手带你走一遍STM32的CAN外设初始化流程。说白了,就是让芯片的CAN控制器能正常收发报文。很多初学者卡在这一步,要么波特率算不对,要么过滤器设置错了导致收不到数据。别急,咱们一个一个来拆。

3.1 STM32CubeMX配置CAN外设

我个人习惯先用CubeMX生成工程框架,再手动微调。这样省事,也不容易漏掉时钟使能这类基础配置。

打开CubeMX,选好你的芯片型号(比如STM32F103C8T6或STM32F407VET6)。在Pinout & Configuration界面,找到Connectivity分类下的CAN1(或CAN2)。勾选激活,界面会变成这样:

  • Mode:选Normal模式。Loopback是回环测试用,Silent是只听不发,咱们做从站NMT通信,用Normal。
  • Basic Parameters:这里设置波特率预分频器、时间段1、时间段2和同步跳转宽度。具体怎么算,我后面单独讲。
  • Advanced Parameters:自动重传、自动唤醒这些,保持默认就行。我一般只关掉自动重传,方便调试时看到总线错误。
我的小习惯: 在CubeMX里把CAN的RX和TX引脚手动确认一下。有时候自动分配会跑到复用功能冲突的引脚上,尤其是STM32F1系列,踩过这个坑的人不少。

3.2 CAN过滤器设置——别让无关报文打扰你

CAN总线上的报文很多,但你的从站只关心特定ID的报文。过滤器就是干这个的——把不想要的报文挡在门外,减轻CPU负担。

STM32的CAN控制器有28个过滤器组(F1系列是14个)。每个过滤器组可以配置为:

模式 说明 适用场景
标识符列表模式 只接收与预设ID完全匹配的报文 从站只监听自己的NMT、PDO、SCO对象
标识符掩码模式 接收与预设ID按掩码匹配的报文 需要接收一组连续ID的报文(如多个PDO)
32位模式 一个过滤器组处理一个32位ID(标准+扩展) 标准CAN 2.0A/B混合使用
16位模式 一个过滤器组拆成两个16位过滤器 只使用标准ID(11位)时更高效

我在项目中做CANopen从站时,通常这样配:

  • 用32位掩码模式,一个过滤器组就够了。
  • 掩码设为0xFFFFFFF0,意思是只看高28位,低4位忽略。
  • 这样就能同时接收NMT(ID 0x000)、SYNC(ID 0x080)、EMCY(ID 0x080 + 节点ID)等关键报文。
曾经踩过的坑: 有一次我把过滤器掩码设成了0xFFFFFF00,结果NMT报文(ID 0x000)被误过滤掉了,从站一直收不到主站的状态切换命令。排查了整整半天才发现是掩码太严格了。记住:CANopen的NMT对象是0x000,别把它当无效ID过滤掉。

3.3 中断优先级配置——别让CAN中断被其他任务饿死

CAN通信对实时性有要求,尤其是NMT状态机切换和紧急报文处理。中断优先级配不好,轻则丢帧,重则总线离线。

在CubeMX的NVIC设置里,找到CAN1 RX0中断(或CAN1 RX1中断,看你用哪个FIFO)。我建议:

  • 抢占优先级:设为1或2(数值越小优先级越高)。别设成0,因为0通常留给系统滴答定时器或硬故障处理。
  • 子优先级:设为0。
  • 如果同时用了CAN的TX中断和SCE(状态改变)中断,也一并使能,优先级保持一致。

为什么会这样配?你想想看,CAN中断来了,你要在中断服务函数里把报文读到FIFO,然后置一个标志位,让主循环去处理。如果中断被更高优先级的任务长时间阻塞,FIFO可能溢出,报文就丢了。CANopen的NMT报文丢了,从站可能一直卡在Pre-operational状态上不来。

经验之谈: 我习惯在CAN中断服务函数里只做最轻量的事——读DR寄存器、清标志位、存数据到环形缓冲区。复杂的协议解析(比如拆解NMT命令)放到主循环或任务里做。这样中断占用时间短,不会影响其他实时任务。

3.4 CAN波特率计算——算不对就通不了

波特率是CAN通信的基础。同一总线上所有节点必须用相同的波特率,否则谁也收不到谁的数据。

STM32的CAN波特率计算公式:

波特率 = APB时钟频率 / (预分频器 × (时间段1 + 时间段2 + 1))

其中:

  • APB时钟频率:F1系列通常是36MHz(APB1),F4系列是42MHz或84MHz。
  • 预分频器:BRP寄存器值,范围1~1024。
  • 时间段1:BS1,范围1~16个时间量子。
  • 时间段2:BS2,范围1~8个时间量子。
  • 同步段:固定为1个时间量子,公式里已经隐含了。

举个例子,我要配250kbps的波特率,APB1时钟是36MHz:

36MHz / (预分频器 × (BS1 + BS2 + 1)) = 250kbps
36,000,000 / (预分频器 × (BS1 + BS2 + 1)) = 250,000
预分频器 × (BS1 + BS2 + 1) = 144

我常用的组合:

目标波特率 APB1时钟 预分频器 BS1 BS2 采样点位置
1 Mbps 36 MHz 4 6 2 87.5%
500 kbps 36 MHz 8 6 2 87.5%
250 kbps 36 MHz 16 6 2 87.5%
125 kbps 36 MHz 32 6 2 87.5%
采样点位置建议: CAN总线推荐采样点在85%~90%之间。我习惯用87.5%,也就是BS1=6、BS2=2。这个位置对总线长度和节点数量变化有较好的容忍度。如果你用的是F4系列,APB1时钟是42MHz或84MHz,记得重新算,别直接套F1的参数。

3.5 核心逻辑:CAN初始化流程总览

说了这么多,咱们用一张图把整个初始化流程串起来。这张图我画了好几次才满意,你一看就明白每一步在干什么。

STM32 CAN外设初始化流程 STM32 CAN外设初始化流程 1. 使能CAN时钟 2. 配置CAN RX/TX引脚 3. 设置波特率参数 4. 配置过滤器组 5. 使能CAN中断 6. 启动CAN控制器 关键提醒 • 过滤器掩码别设太严 • 中断优先级别设最高 • 采样点推荐87.5% • 先配时钟再配GPIO

这张图把6个步骤串起来了。你按这个顺序走,基本不会漏。我刚开始做CAN开发时,经常忘了使能CAN时钟就急着配GPIO,结果引脚死活拉不出波形。后来养成了习惯:先看时钟树,再动外设。

3.6 实战代码片段:HAL库初始化示例

下面是我在一个实际项目中用过的初始化代码,基于STM32F103,APB1时钟36MHz,目标波特率250kbps。你直接参考这个结构改参数就行。

/* CAN句柄定义 */
CAN_HandleTypeDef hcan1;

/* CAN过滤器配置结构体 */
CAN_FilterTypeDef sFilterConfig;

/* CAN初始化函数 */
void MX_CAN1_Init(void)
{
  hcan1.Instance = CAN1;
  hcan1.Init.Prescaler = 16;           /* 预分频器 */
  hcan1.Init.Mode = CAN_MODE_NORMAL;   /* 正常模式 */
  hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;  /* 同步跳转宽度 */
  hcan1.Init.TimeSeg1 = CAN_BS1_6TQ;   /* BS1 = 6个时间量子 */
  hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;   /* BS2 = 2个时间量子 */
  hcan1.Init.TimeTriggeredMode = DISABLE;
  hcan1.Init.AutoBusOff = DISABLE;
  hcan1.Init.AutoWakeUp = DISABLE;
  hcan1.Init.AutoRetransmission = DISABLE;  /* 关掉自动重传,方便调试 */
  hcan1.Init.ReceiveFifoLocked = DISABLE;
  hcan1.Init.TransmitFifoPriority = DISABLE;
  
  if (HAL_CAN_Init(&hcan1) != HAL_OK)
  {
    Error_Handler();  /* 初始化失败,进错误处理 */
  }
  
  /* 配置过滤器:32位掩码模式,接收所有ID */
  sFilterConfig.FilterBank = 0;
  sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
  sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
  sFilterConfig.FilterIdHigh = 0x0000;
  sFilterConfig.FilterIdLow = 0x0000;
  sFilterConfig.FilterMaskIdHigh = 0x0000;
  sFilterConfig.FilterMaskIdLow = 0x0000;  /* 掩码全0,接收所有报文 */
  sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
  sFilterConfig.FilterActivation = ENABLE;
  sFilterConfig.SlaveStartFilterBank = 14;  /* 双CAN时使用 */
  
  if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  
  /* 使能CAN中断 */
  HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
  
  /* 启动CAN */
  HAL_CAN_Start(&hcan1);
}
注意: 上面代码中过滤器掩码设成了全0,意思是接收所有报文。这在调试阶段很方便,但正式产品里一定要根据CANopen的COB-ID范围设置合理的掩码,否则CPU会被无关报文淹没。我见过一个项目,总线上有几十个节点,从站没配过滤器,结果CPU 30%的时间都在处理无用报文。