3. STM32 HAL库CAN外设配置:CAN初始化、过滤器设置、中断配置、波特率计算
好,咱们进入实战环节。这一章我带你把STM32的CAN外设从沉睡中唤醒。说白了,CAN外设就像一扇门,你得先把它打开、设好门槛、配好门卫,数据才能顺畅进出。
我个人习惯把CAN配置拆成四个模块:时钟与引脚、初始化结构体、过滤器、中断。波特率计算贯穿其中,咱们边配边算。
3.1 时钟与引脚:先让外设通电
任何外设的第一步都是开时钟。STM32的CAN外设挂在不同总线上——F1系列在APB1,F4/H7系列也在APB1,但有些型号有独立的CAN时钟源。
引脚配置方面,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 |
3.3 波特率计算:别让数学卡住你
波特率计算是CAN配置里最容易出错的环节。公式其实很简单:
波特率 = CAN时钟 / (Prescaler × (1 + BS1 + BS2))
其中1 + BS1 + BS2就是总的时间量子数。标准CAN要求这个值在8~25之间。
举个例子,假设CAN时钟是36MHz,目标波特率250kbps:
- 先算总Tq数:36MHz / 250kbps = 144
- 分解:144 = Prescaler × (1 + BS1 + BS2)
- 取Prescaler=6,则1+BS1+BS2=24
- 分配: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;
为什么会这样?因为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);
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初始化流程:
- 使能CAN和GPIO时钟
- 配置CAN_TX和CAN_RX引脚
- 调用
HAL_CAN_Init()初始化CAN外设 - 配置过滤器
- 使能中断
- 调用
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);
}
这张图把整个初始化流程串起来了。你按这个顺序走,基本不会漏掉什么。我在项目里用过不下十次这个流程,每次都能稳定跑起来。