3. STM32 HAL库CAN外设配置:CAN初始化、过滤器设置、中断配置、波特率计算

好,咱们进入实战环节。这一章我带你把STM32的CAN外设从沉睡中唤醒。说白了,CAN外设就像一扇门,你得先把它打开、设好门槛、配好门卫,数据才能顺畅进出。

我个人习惯把CAN配置拆成四个模块:时钟与引脚、初始化结构体、过滤器、中断。波特率计算贯穿其中,咱们边配边算。

3.1 时钟与引脚:先让外设通电

任何外设的第一步都是开时钟。STM32的CAN外设挂在不同总线上——F1系列在APB1,F4/H7系列也在APB1,但有些型号有独立的CAN时钟源。

注意:CAN时钟源的选择直接影响波特率精度。如果你用H7系列,记得检查RCC是否选择了HSE作为CAN时钟,别默认用了HSI导致波特率跑偏。

引脚配置方面,CAN需要两个引脚:CAN_TX和CAN_RX。以STM32F103为例,通常复用为PA11(CAN_RX)和PA12(CAN_TX)。

// 使能CAN时钟和GPIO时钟
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();

// 配置CAN_TX(PA12)为复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 配置CAN_RX(PA11)为浮空输入
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

嗯,这里要注意:CAN_TX必须配置为复用推挽输出,CAN_RX配置为浮空输入。我曾经见过有人把两个引脚都配成复用开漏,结果总线一直处于显性电平,整个网络都瘫痪了。

3.2 CAN初始化结构体:核心参数都在这里

HAL库把CAN初始化参数封装在CAN_InitTypeDef结构体中。咱们一个一个看。

参数 说明 我的建议值
Prescaler 分频系数,决定时间量子长度 根据波特率计算
Mode CAN_MODE_NORMAL / CAN_MODE_LOOPBACK / CAN_MODE_SILENT 调试用回环,正式用正常
SyncJumpWidth 同步跳转宽度,单位Tq 1~2,别太大
TimeSeg1 BS1段,单位Tq 通常8~16
TimeSeg2 BS2段,单位Tq 通常2~8
TimeTriggeredMode 时间触发模式 CANopen用不到,DISABLE
AutoBusOff 自动退出BusOff ENABLE,省心
AutoWakeUp 自动唤醒 DISABLE
AutoRetransmission 自动重发 ENABLE,CANopen需要
ReceiveFifoLocked FIFO锁定模式 DISABLE
TransmitFifoPriority 发送优先级由报文ID决定 ENABLE
核心原则:CANopen从站建议开启自动重发和自动BusOff恢复。这两个功能能让节点在总线干扰后自动恢复,不用人工复位。

3.3 波特率计算:别让数学卡住你

波特率计算是CAN配置里最容易出错的环节。公式其实很简单:

波特率 = CAN时钟 / (Prescaler × (1 + BS1 + BS2))

其中1 + BS1 + BS2就是总的时间量子数。标准CAN要求这个值在8~25之间。

举个例子,假设CAN时钟是36MHz,目标波特率250kbps:

  1. 先算总Tq数:36MHz / 250kbps = 144
  2. 分解:144 = Prescaler × (1 + BS1 + BS2)
  3. 取Prescaler=6,则1+BS1+BS2=24
  4. 分配:BS1=13,BS2=10,SJW=1
// 250kbps配置示例(CAN时钟36MHz)
hcan1.Init.Prescaler = 6;
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_13TQ;
hcan1.Init.TimeSeg2 = CAN_BS2_10TQ;
小技巧:我习惯把采样点设在75%~80%之间。BS1/(BS1+BS2)就是采样点位置。上面例子采样点=13/23≈56.5%,偏低。我会调整成BS1=16,BS2=7,采样点=16/23≈69.6%,更合理。

为什么会这样?因为CAN总线有传播延迟,采样点太靠前容易采到信号跳变沿,太靠后又可能错过有效数据。75%是个黄金比例。

3.4 过滤器设置:只收你想要的报文

CAN总线上的报文是广播的,所有节点都能收到。过滤器的作用就是让STM32只处理跟自己相关的报文,减轻CPU负担。

STM32的CAN过滤器有两种模式:

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

对于CANopen从站,我建议用掩码模式。因为CANopen的PDO和SDO报文ID是动态分配的,用掩码模式可以灵活过滤。

// 过滤器配置:接收所有标准帧,ID范围0x000~0x7FF
CAN_FilterTypeDef sFilterConfig;

sFilterConfig.FilterBank = 0;           // 使用过滤器组0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;    // 标准帧ID高16位
sFilterConfig.FilterIdLow = 0x0000;     // 标准帧ID低16位
sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码全0,接收所有
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
sFilterConfig.SlaveStartFilterBank = 14;

HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig);
避坑:FilterIdHigh和FilterIdLow的位分配跟你想的可能不一样。标准帧11位ID放在高16位的高11位,扩展帧29位ID跨两个16位寄存器。我曾经在这里栽过跟头,调试了一整天发现过滤器根本没生效。

3.5 中断配置:让CPU知道有数据来了

CAN有三种主要中断:

  • FIFO0消息挂起中断:收到报文时触发
  • FIFO0满中断:FIFO存满时触发
  • 错误中断:总线错误时触发

对于CANopen从站,我通常只开启FIFO0消息挂起中断。错误中断在调试阶段打开,正式产品里可以关掉,避免频繁中断影响实时性。

// 使能CAN中断
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

// 配置NVIC
HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);

中断服务函数里,我习惯只做一件事:把报文从FIFO搬到环形缓冲区,然后置一个标志位。真正的协议解析放在主循环或任务里做。为什么?因为中断里做复杂处理会阻塞其他中断,而且CANopen的PDO处理可能涉及对象字典查找,耗时不确定。

void CAN1_RX0_IRQHandler(void)
{
    HAL_CAN_IRQHandler(&hcan1);
}

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[8];
    
    if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK)
    {
        // 把报文放入环形缓冲区
        CAN_Frame_t frame;
        frame.id = rxHeader.StdId;
        frame.dlc = rxHeader.DLC;
        memcpy(frame.data, rxData, rxHeader.DLC);
        
        CAN_RingBuffer_Push(&frame);
    }
}

3.6 完整初始化流程

把上面所有步骤串起来,就是完整的CAN初始化流程:

  1. 使能CAN和GPIO时钟
  2. 配置CAN_TX和CAN_RX引脚
  3. 调用HAL_CAN_Init()初始化CAN外设
  4. 配置过滤器
  5. 使能中断
  6. 调用HAL_CAN_Start()启动CAN通信
// 完整初始化示例
void CAN_Init(void)
{
    // 1. 时钟和引脚配置(略,见3.1节)
    
    // 2. CAN初始化
    hcan1.Instance = CAN1;
    hcan1.Init.Prescaler = 6;
    hcan1.Init.Mode = CAN_MODE_NORMAL;
    hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
    hcan1.Init.TimeSeg1 = CAN_BS1_13TQ;
    hcan1.Init.TimeSeg2 = CAN_BS2_10TQ;
    hcan1.Init.TimeTriggeredMode = DISABLE;
    hcan1.Init.AutoBusOff = ENABLE;
    hcan1.Init.AutoWakeUp = DISABLE;
    hcan1.Init.AutoRetransmission = ENABLE;
    hcan1.Init.ReceiveFifoLocked = DISABLE;
    hcan1.Init.TransmitFifoPriority = ENABLE;
    HAL_CAN_Init(&hcan1);
    
    // 3. 过滤器配置(略,见3.4节)
    
    // 4. 中断配置(略,见3.5节)
    
    // 5. 启动CAN
    HAL_CAN_Start(&hcan1);
}
调试建议:第一次调试时先用回环模式(CAN_MODE_LOOPBACK),自己发自己收,确认收发通路正常。然后再切到正常模式接总线。这个习惯帮我省了不少排查时间。
CAN外设初始化流程图 1. 时钟与引脚配置 2. HAL_CAN_Init() 3. 过滤器配置 4. 中断配置 5. HAL_CAN_Start() 关键检查点 • 时钟使能了吗? • 引脚复用对吗? • 波特率算准了吗? 调试技巧 • 先用回环模式测试 • 用逻辑分析仪看波形 常见错误 • 过滤器位分配搞错 • 中断优先级太低

这张图把整个初始化流程串起来了。你按这个顺序走,基本不会漏掉什么。我在项目里用过不下十次这个流程,每次都能稳定跑起来。