4. CAN通信软件编程:STM32 CAN外设初始化、发送与接收中断、过滤器配置
好,咱们进入实战环节。前面聊了那么多CAN协议的理论,说白了都是纸上谈兵。今天咱们就真刀真枪地干,把STM32的CAN外设用起来。我个人习惯,学任何外设,先看寄存器,再看库函数,最后看代码怎么跑。这样出了问题,你心里有底。
4.1 STM32 CAN外设初始化——别小看这一步
初始化这事儿,看着简单,其实坑不少。我刚开始做BMS项目时,CAN怎么都调不通,折腾了两天,最后发现是时钟配置错了。嗯,这里要注意,STM32的CAN外设挂载在APB1总线上,时钟频率通常是36MHz或42MHz,具体看你用的芯片型号。
初始化流程,我总结为三步走:
- 使能时钟——别漏了GPIO时钟和CAN时钟
- 配置GPIO——CAN_TX和CAN_RX引脚,要设为复用功能
- 设置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中的应用,那个更有意思。