STM32F105双CAN驱动详解:bxCAN滤波器配置与位时序优化

STM32F105双CAN驱动详解:bxCAN滤波器配置与位时序优化 简介面向嵌入式开发者的 STM32F105 CAN 通信代码示例包适合需要快速验证 CAN 总线收发功能或希望在裸机项目中加入 CAN 外设驱动的开发者参考能帮助绕过繁琐的寄存器配置阶段直接聚焦报文收发逻辑。压缩包内共 2 个文件包括 1 个 C 源文件和 1 个头文件整体大小仅约 2KB没有复杂工程依赖属于便于逐行通读、按需裁剪和快速移植的轻量级代码片段。代码围绕 CAN 控制器初始化、接收滤波器设置、消息框配置、发送与接收处理流程展开可作为理解 STM32F105 CAN 通信链路的基础模板也可在调试收发异常时用作对照参考。对于工业控制、车载电子或物联网设备中的控制器局域网通信需求该示例能提供清晰的模块划分与接口思路避免开发者从零摸索。目前该包已有 237 人学习/下载对正在学习 CAN 总线或准备开展 STM32F105 通信验证的嵌入式初学者和工程师而言是一个投入成本极低的入门与排查资源。1. 拆开 CAN4stm32f105.zip双 CAN 外设的初印象拿到这个压缩包先别急着解压跑例程。CAN4stm32f105.zip这个名字里最有价值的信息是「F105」而不是「CAN」——STM32F105 的 CAN 外设和大多数人练过手的 F103 不一样它属于bxCAN 双 CAN 控制器但两个 CAN 实例共享一套滤波器组和中断向量这在初始化顺序上有一个非常容易踩的坑不先初始化 CAN1CAN2 根本进不了配置态。压缩包里的can.c和can.h是 ST 标准外设库风格的驱动核心职责是把 bxCAN 的初始化、滤波器配置、发送请求和接收中断封装成可直接调用的接口。这篇博文将直接对着源文件逐段拆位时序里的 BS1/BS2 采样点怎么算、标识符屏蔽模式怎么命中过滤规则、发送失败后状态寄存器该怎么读。读完你会得到一份能直接移植到 IAR 或 Keil 工程的 CAN 驱动理解而不是一份只会点灯的例程说明。适合正在调 CAN 通信但不满足于「能通就完事」的嵌入式工程师。2. bxCAN 外设结构双 CAN 实例、邮箱与位时序参数2.1 双 CAN 实例的共享资源边界STM32F105 的 CAN 外设编号从 1 到 2核心是 bxCAN 模块每个实例有独立的3 个发送邮箱和2 个接收 FIFOFIFO0/FIFO1但两个实例共享28 个滤波器组F103 只有 14 个。这是 F105 相对 F103 一个非常实用的升级滤波器组可以在 CAN1 和 CAN2 之间按需分配而不是像 F103 那样滤波器资源只属于单个 CAN。分配关系挂在CAN_FMR滤波器主控制寄存器的CAN2SB位段上。CAN2SB[5:0]的值表示 CAN2 滤波器组的起始序号等于 0 表示所有滤波器都归 CAN1等于 14 表示 CAN1 用 0~13、CAN2 用 14~27。这个位段在标准库里的写入口是CAN_FilterInitStructure.CAN_FilterNumber但那个成员只决定当前操作哪一个滤波器真正决定归属关系的是CAN2SB。初始化顺序必须是 CAN1 先完成初始化并进入正常模式CAN2 才能配置滤波器这在can.c的初始化流程里如果没处理好会出现 CAN2 的滤波器配置写不进去的情况。2.2 位时序从 CAN_InitTypeDef 反推采样点can.h里定义的结构体CAN_InitTypeDef中CAN_SJW、CAN_BS1、CAN_BS2和CAN_Prescaler四个成员直接决定波特率和采样点。bxCAN 的位时间由三部分组成同步段固定 1 Tq、传播时间段并入 BS1、相位缓冲段 BS1 和 BS2。ST 库把传播时间段合并进 BS1 计算所以一个位时间的 Tq 总数是1 BS1 BS2波特率公式如下// CAN_SJW1, CAN_BS113, CAN_BS22, CAN_Prescaler6, APB136MHz uint32_t apb1_clock 36000000; uint32_t tq apb1_clock / 6; // 6 M Tq/s uint32_t baudrate tq / (1 13 2); // 375 kbps uint32_t sample_point (1 13) * 100 / (1 13 2); // 87.5%这段代码里CAN_Prescaler会先对 APB1 总线时钟做分频得到 Tq 时钟随后用总 Tq 数去除即得实际波特率。(1BS1)的占比就是采样点通常建议 500 kbps 以下配置在 75%80% 之间1 Mbps 时靠近 75% 更稳。can.c示例工程如果给的是 500 kbps、采样点 87.5%在短线上可能没问题但如果总线有分支或线缆较长BS113, BS22这种参数会对信号上升沿太敏感遇到反射容易报位错误。工程中我倾向于把 BS1 降为 11、BS2 升为 4采样点落在 75%抗干扰性明显好一些。2.3 邮箱状态机与发送优先级发送侧的核心是三个邮箱CAN_TIxR寄存器里的TXRQ位一旦置 1即表示该邮箱有发送请求。bxCAN 硬件会自动按优先级由高到低的顺序发送优先级规则依次是先比较标识符数值越小优先级越高标识符相同则比较邮箱编号。CAN_TxMailBoxesStatus结构体里的CAN_TxStatus_Ok、CAN_TxStatus_Failed和CAN_TxStatus_Pending三个枚举就是给应用层判断发送结果的。发送失败不需要立即把邮箱清空标准库接口CAN_CancelTransmit的作用其实是清除TXRQ位、终止当前请求。真正判断发送是否成功要去看CAN_TSR寄存器的TXOK位。很多初学者误以为CAN_Transmit返回CAN_TxStatus_Ok就代表数据已经发到总线上了实际上这个返回值只说明数据成功放进了邮箱并发出请求真正上总线成功要等发送完成中断里读TXOK。can.c里的发送函数如果没区分这两层调完立即返回Ok但总线上抓不到波形就要先检查是不是中断服务程序里清标志时把TXOK提前清了。3. 滤波器配置与标识符命中逻辑3.1 32 位屏蔽位模式的掩码计算F105 的滤波器组可以配置为 32 位模式或 16 位模式一个滤波器组在 32 位模式下对应一条报文过滤规则16 位模式下可以拆成两条独立规则。can.c里常用的是 32 位屏蔽位模式涉及两个关键寄存器CAN_FFA1RFIFO 关联和CAN_FM1R模式选择而真正决定报文能不能进 FIFO 的是CAN_FxR1标识符寄存器和CAN_FxR2屏蔽寄存器。屏蔽寄存器的规则是某一位为 0 表示必须匹配为 1 表示不关心。例如要让 ID 为0x181的标准帧进入 FIFO0配置如下CAN_FilterInitStructure.CAN_FilterIdHigh (uint16_t)0x0181 5; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x07FF 5; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE;代码中CAN_FilterIdHigh左移 5 位是因为 32 位模式下 RTR、IDE 和 ID 一起排布在寄存器高 16 位区域标准帧的 11 位 ID 从位 21 开始存放左移是为了对齐到硬件期望的位置。CAN_FilterMaskIdHigh中0x07FF的作用是让 ID 的 11 位全部置 0必须精确匹配而扩展帧的 18 位可省略。若想作为范围过滤比如接收0x181到0x185之间的所有 ID可以把掩码置为~0x0007对应的位段即低 3 位不关心。3.2 扩展 ID 与标准 ID 的匹配差异很多调 CAN 的工程师会忽略一个细节CAN_FilterIdHigh里的数据格式标准帧和扩展帧在滤波器寄存器中的位分布不一样。标准帧的 ID 占 11 位扩展帧的 ID 占 29 位所以同一个滤波器要接收两种帧型必须显式区分CAN_ID_EXT和CAN_ID_STD如果不区分硬件会把标准帧的 11 位存入扩展帧的 ID 区域导致永远匹配不上。处理方式是让滤波器在屏蔽位模式下只关注 ID 本身而把 IDE 位也纳入匹配// 只接收标准帧扩展帧直接丢弃 CAN_FilterInitStructure.CAN_FilterIdHigh (uint16_t)((stId 5) 0xFFFF); CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x7FF 5; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0004; // IDE1 时屏蔽为不关心这段配置里CAN_FilterMaskIdLow 0x0004将协议相关位的屏蔽位置为 1表示对 IDE 位不关心。注意掩码寄存器中位 3 是 RTR、位 2 是 IDE如果置 0 表示强制匹配当前帧是标准数据帧才能过远程帧会被拦掉。工程上常用的做法是数据帧和远程帧分开处理让滤波器只放行数据帧远程帧应答由协议层单独维持可以降低中断触发频率。3.3 标识符列表模式的适用场景屏蔽位模式适合「一组 ID 都要收」的场景但如果需求是「只收少数几个离散 ID」列表模式更合适。16 位列表模式一个滤波器组可以存两条 ID32 位列表模式只能存一条。使用列表模式时不再有掩码寄存器概念完全精确匹配。CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdList; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh (uint16_t)((0x181 5) 0xFFFF); CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO1;在 ID 列表模式下CAN_FilterIdHigh保存第一条 IDCAN_FilterIdLow保存第二条 ID。由于是精确匹配不需要算掩码误配概率低。缺点是滤波器资源消耗大一条规则占一组滤波器而屏蔽位模式一组可以覆盖一段连续地址空间。CANopen 这类协议通常固定使用 11 位标准帧节点地址分布在0x180 NodeID用屏蔽位模式把功能码和节点地址组合成一个掩码可以做到一个滤波器组收完 PDO 和 SDO 的所有组合这是我在多个工程里验证过的高效做法。4. 发送接收例程拆解从 can.c 的代码路径看中断与状态位4.1 发送请求与完成确认的完整链路can.c的发送函数通常分三层应用层传入一个CanTxMsg结构体库函数CAN_Transmit负责找空邮箱、填充数据、置TXRQ位硬件发送完成后置TXOK和Rqcp位触发发送完成中断中断里调用CAN_TransmitStatus读取状态并清标志。整理后的发送流程如下void CAN_SendData(CAN_TypeDef *CANx, uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; TxMessage.StdId id; // 标准帧 ID TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; // 数据帧 TxMessage.DLC len; memcpy(TxMessage.Data, data, len); CAN_Transmit(CANx, TxMessage); }CAN_Transmit内部会遍历 3 个邮箱找到TME发送邮箱空标志为空的那个填充CAN_TIxR和CAN_TDxR寄存器。如果三个邮箱都满函数返回CAN_TxStatus_Failed这是发送失败的第一个信号。注意CAN_Transmit返回Ok不代表总线 ACK 成功只能确定数据已经进入邮箱真正的成功标志是中断里读CAN_TSR的TXOK24/12/01位。代码里建议把TXOK中断和TERR传输错误中断分开处理避免在中断服务程序里做重发逻辑导致嵌套过深。4.2 接收 FIFO 溢出与中断优先级设计接收侧走的是 FIFO0 / FIFO1中断入口是USB_LP_CAN1_RX0_IRQHandler和CAN1_RX1_IRQHandler。F105 的向量表里把 CAN1 接收中断和 USB LP 中断合并了如果工程同时开了 USB这个中断向量会被共用裸机例程里没问题上 RTOS 后要特别注意中断里不要调用可能会阻塞的系统调用。void CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 根据 RxMessage.StdId 分发到应用层 }CAN_Receive会从 FIFO 头部取出一条报文并将RFOM位置 1释放该邮箱。这里有个非常隐蔽的坑如果中断响应不及时FIFO 溢出后硬件会丢弃新报文同时置FOVR溢出标志和RFL位。但如果你在中断里先CAN_ClearFlag再CAN_Receive可能把RFOM也清掉造成 FIFO 读指针错乱。正确顺序是先CAN_Receive读出数据再清溢出标志。中断优先级方面接收中断建议设为高于发送完成中断。原因是发送完成中断晚一点处理没关系邮箱可以等但 FIFO 只有 3 个深度满了就丢帧。F105 的 CAN 控制器没有 DMA 接口接收全部靠中断这一点在热词里经常有人问「CAN 总线一般中断接收还是 DMA 接收」答案是bxCAN 不支持 DMA 搬运只能中断但可以在中断里只做数据拷贝协议解析丢到主循环。如果想降低中断频率可以把多个报文滤波器映射到同一个 FIFO在中断里循环CAN_MessagePending读空。4.3 错误管理位错误、帧错误与 CRC 错误的现场恢复CAN 的错误机制分为发送错误计数TEC和接收错误计数REC各 8 位达到阈值会改变总线状态。can.c中错误处理往往只写了CAN_GetFlagStatus判断但不区分错误类型实际排障时需要看CAN_ESR寄存器的LEC[2:0]位段000 - 无错误 001 - 位填充错误 010 - 格式错误 011 - ACK 错误 100 - 隐性位错误 101 - 显性位错误 110 - CRC 错误ACK 错误是最常见的总线上只有自己在发没有其他节点应答。显性位错误通常意味着两个节点同时发送且 ID 冲突或总线被短路到地。CRC 错误多见于波特率不匹配——接收方采样点不对读错位后 CRC 校验失败。CAN_ESR的BOFF位为 1 表示进入 Bus Off 状态此时控制器不再参与总线通信。恢复策略有两个级别硬件自动恢复进入 Bus Off 后硬件等待 128 个 11 位隐性位后自动回 Error Active或软件干预在 Bus Off 中断里重新初始化 CAN。工程上建议不要立即重新初始化先读CAN_ESR如果错误计数还在上升说明总线存在持续故障应先关中断、停止发送等总线空下来再恢复。5. 收发验证技巧与 CANopen 协议层的预置钩子拿示波器或逻辑分析仪确认一根 CAN 报文是否发出去是调试最直接的验证手段。但我更推荐先用两个 STM32F105 节点做回环A 节点发送B 节点接收B 收到后回一帧A 再收回来。can.c示例如果支持环回模式初始化的CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack可以直接在内部把 TX 输出反馈到 RX 输入不需要外部收发器就能验证协议栈逻辑。采样点校准有一个实用方法在总线上同时挂三个节点分别配置为 75%、80%、87.5% 采样点用 CAN 分析仪连续压力测试 10 万帧观察哪个节点先报 CRC 错误。这个实验比单纯理论计算更能暴露线缆长度和收发器传播延迟的问题。CAN_SJW同步跳转宽度也不要一直用默认的 1 Tq总线上有 5 个以上节点时我一般会设置CAN_SJW CAN_SJW_2tq能让接收方在时钟偏差累积时更快恢复同步减少位错误。CANopen 协议层在 F105 上预留的时候can.c的收发接口建议再做一层薄封装发送函数入参从裸 ID 换成 COB-ID。CANopen 的 COB-ID 实际是功能码左移 7 位加上节点号比如初始化 SDO 服务器要监听0x600 NodeID心跳报文是0x700 NodeID。在can.c的滤波器初始化处用一条屏蔽位规则把[0x580, 0x59F]SDO 请求和[0x600, 0x61F]SDO 应答收进来其余 PDO 报文用第二组滤波器做列表模式离散接收能够有效降低中断负载。J1939 的地址声明报文地址0xEE00就需要 29 位扩展帧支持此时can.h中的CAN_Id_Extended宏需要配合 32 位滤波器模式掩码位段要覆盖到扩展 ID 的 18 位低位这是移植时最容易被忽略的差异点。最后由CAN_ESR观察 Bus Off 触发的实际现场发送高优先级报文被 ACK 错误打断TEC 计数持续累加到 256 后进 Bus Off。恢复策略不要直接CAN_Init重来先读CAN_ESR的LEC确认是 ACK 错误还是位错误再决定是调整波特率还是检查收发器供电。本文还有配套的精品资源点击获取