资讯动态

STM32F105双CAN例程详解:筛选器分配与中断转发实现

发布时间:2026/10/21 19:08:46 来源:尧图企业网站定制
简介一套基于STM32F105微控制器的CAN总线通信例程核心设计为CAN1接口负责接收总线报文CAN2接口负责发送数据非常适合嵌入式入门开发者学习也可为工业自动化、汽车电子或物联网节点间的可靠通信提供参考。压缩包共收录201个文件整体大小仅2.12MB以C语言源文件和H头文件为主同时包含工程配置、编译链接产物、备份文件及少量文本说明属于典型的标准外设库工程可直接导入开发环境查看和重新编译。目前已有984人学习下载。代码涵盖CAN控制器初始化、接收及发送邮箱管理、报文滤波器设置、中断服务程序设计、发送错误与重传处理等关键环节并附带可烧录验证的十六进制文件与调试映射文件便于边看边调。通过该例程可以深入理解双CAN接口如何独立配置位时钟与波特率并保持同步掌握多主站总线中仲裁、错误检测与实时收发的完整流程也能学会在中断与轮询机制之间做出合理选择是一份兼具教学价值与实用价值的参考资料。1. 双CAN例程看似简单实则考验硬件分配的STM32F105工程用STM32F105做CAN1接收、CAN2发送听起来就是把两个CAN控制器初始化一遍的简单工程。实际拆开代码之后你会发现这类例程的难点根本不在收发本身而在F105双CAN外设的共享资源分配28组筛选器怎么切给两个控制器、两个CAN的中断向量为什么要分开使能、以及为什么网上那么多F103单CAN示例一移植过来就翻车。这份工程正好把最小可运行的收发骨架搭好了没用协议栈也不做复杂路由非常适合刚接触双CAN的开发者拿来打底也是之后做CAN中继器或网关的一个干净起点。2. 双CAN硬件架构F105的CAN2为什么不能脱离CAN1单独使用2.1 时钟、复位与中断向量之间的关系无论从哪里拿到这个工程第一件事不是去看main.c里的业务逻辑而是先翻启动文件把与CAN有关的向量名全部列出来。F105属于互联型产品它的中断向量和F103不完全一样尤其是CAN2相关的向量在F103上根本没有。与这个工程直接相关的向量通常是这几个CAN1发送共用USB_HP_CAN1_TX_IRQHandler、CAN1接收0共用USB_LP_CAN1_RX0_IRQHandler以及CAN1_RX1_IRQHandler、CAN1_SCE_IRQHandler。CAN2侧还有一组独立的接收、发送、错误向量。注意CAN1的TX和RX0向量与USB功能是共用的如果你的工程里同时开启了USB设备要考虑向量冲突这是F105项目里常见的隐藏问题。时钟上CAN1和CAN2都挂在APB1总线上但使能位分开代码里通常这样写RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1 | RCC_APB1Periph_CAN2, ENABLE);很多人在这个位置只写了CAN2以为CAN2是独立的外设结果后面初始化CAN2时寄存器完全写不进去。把CAN1的时钟一并打开是F105双CAN驱动的第一个约定。实际项目中我一般把CAN1的接收中断作为唯一的数据入口在中断里做转发到CAN2这样两个CAN的中断向量不用都做复杂处理但如果后续要扩展CAN2的接收就必须单独配置CAN2对应的中断通道不能只在CAN1的ISR里顺带处理否则NVIC里根本没使能CAN2的向量标志位来了也进不了中断。2.2 筛选器组的分配逻辑28组滤波器如何切给两个CANF105的bxCAN提供了28组筛选器这28组由CAN1和CAN2共享。共享的含义是并不存在物理上“CAN1专属筛选器”或“CAN2专属筛选器”这种隔离而是通过配置把某一组筛选器关联到CAN1或CAN2的FIFO。在标准外设库的CAN_FilterInit函数里决定归属的核心参数有四个CAN_FilterNumber、CAN_FilterFIFOAssignment、CAN_FilterMode、CAN_FilterScale。其中CAN_FilterFIFOAssignment不仅决定进入哪个FIFO还决定这一组筛选器挂在哪个CAN名下。典型分配做法是低编号组013给CAN1高编号组1427给CAN2两组之间不要重叠。在“CAN1收、CAN2发”的场景里CAN2没有接收需求理论上不需要给CAN2单独分配筛选器但如果你怕后面调试麻烦也可以预留一组给CAN2并保持激活状态。这里还有一个寄存器级细节CAN_FMR里有一个筛选器初始化位只有当所有CAN都处于软件初始化状态时筛选器寄存器才能被正常改写。如果忘记了把CAN1退出发送模式筛选器可能写不进现象就是初始化代码执行成功了但接收就是不通。关于筛选器模式掩码模式适合放行一簇ID列表模式适合精确匹配单个ID。这个工程用掩码模式把掩码设为全0效果等于接收总线上的全部帧转发时再根据业务决定丢弃哪些走的是一个典型的“接收网关”思路。做二次开发时只需要把掩码改成目标ID就成了一个单ID接收的节点。2.3 波特率计算F103迁移到F105最先踩的坑双CAN工程的波特率参数比单CAN工程更容易错因为F105的APB1时钟是36MHz而很多F103工程的APB1时钟也是36MHz看起来一样但F103的CAN外设可以来自APB1而F105的CAN时钟电路略有差异导致很多人复制过来的预分频值直接不能用。标准外设库里的波特率公式是波特率 APB1时钟 / ((1 BS1 BS2) * Prescaler)如果板子主频72MHzAPB1通常是36MHz想要500Kbps可以让1 BS1 BS2 18个时间单元Prescaler取4即36MHz除以72。这个组合在CAN_InitTypeDef里对应CAN_BS1_10tq、CAN_BS2_7tq、CAN_Prescaler4。算完以后可以先在代码里打印CAN_GetPrescaler之类的寄存器值核对别急着接到总线上。3. 核心代码拆解一条CAN帧从进入到转发出去的完整链路3.1 引脚与时钟初始化顺序错了后面全是黑的硬件初始化顺序决定了后面调试是否顺利。这个工程中CAN1引脚固定复用为PA11RX和PA12TXCAN2引脚复用为PB12RX和PB13TX。初始化代码通常长这样void CAN_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 先开GPIO时钟再开外设时钟这是标准库的习惯顺序 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1 | RCC_APB1Periph_CAN2, ENABLE); // CAN1引脚 PA11( RX), PA12( TX) GPIO_InitStructure.GPIO_Pin GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // CAN2引脚 PB12( RX), PB13( TX) GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_13; GPIO_Init(GPIOB, GPIO_InitStructure); }引脚模式必须是复用推挽GPIO_Mode_AF_PP不能设成普通的推挽输出CAN控制器才能接管引脚。GPIO_Speed取50MHz通常没问题CAN通信速率本身不高但引脚翻转速度不够会影响波形边沿。如果你用的是某些把CAN引脚和串口共用映射的板子还要检查板级原理图有没有跳线冲突否则软件怎么配置都收不到数据。3.2 CAN控制器初始化与筛选器配置接收门槛只对CAN1生效控制器初始化直接决定波特率和工作模式。双CAN工程里两个CAN都要调一次CAN_Init但参数可以完全一致。示例配置如下CAN_InitTypeDef CAN_InitStructure; CAN_DeInit(CAN1); CAN_DeInit(CAN2); 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; // 36MHz / ((1 10 7) * 4) 500Kbps CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_10tq; CAN_InitStructure.CAN_BS2 CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure); CAN_Init(CAN2, CAN_InitStructure);注意CAN_SJW是同步跳转宽度处理时钟偏差一般取1tq够了。BS1是传播段加相位缓冲段1BS2是相位缓冲段2它们直接影响采样点位置。对500Kbps而言把10tq和7tq组合起来采样点大约在(110)/(1107)61%的位置这是CAN通信中比较常用的值。如果总线上有别的设备最好算清楚对方的采样点避免边沿误差。筛选器配置比控制器配置更容易写错。只做发送的CAN2其实不需要筛选器真正决定数据进不进FIFO的是CAN1的筛选器所以代码里会出现这样一段CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; // 使用筛选器组0 CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; // 标准ID高16位 CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; // 掩码全0 全收 CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO0; // 挂到CAN1的FIFO0 CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);掩码全0表示“掩码位不参与比较”也就是说任何ID都能通过。如果想要只收某一个ID把掩码设为0xFFFF把FilterIdHigh/Low填成目标ID即可。32位长度下标准帧的ID只占高16位中的11位扩展帧的ID占整个32位配置时容易漏掉这种位布局差异我一般会直接按下表对应填值。3.3 CAN2发送往邮箱里塞帧的几个关键参数标准CAN的发送路径是构造一条CanTxMsg结构体调用CAN_Transmit把它压入三个发送邮箱中的一个再由硬件按照优先级自动移出到总线上。这个工程把发送放在CAN2上所以每次转发都是一次发送流程。典型代码uint8_t CAN2_SendData(uint32_t id, uint8_t *buf, uint8_t len) { CanTxMsg txMsg; txMsg.StdId id; // 标准帧ID txMsg.ExtId 0; // 扩展ID用不到就填0 txMsg.IDE CAN_Id_Standard; // 使用标准帧 txMsg.RTR CAN_RTR_Data; // 数据帧 txMsg.DLC len; // 数据长度0到8 for (uint8_t i 0; i len; i) { txMsg.Data[i] buf[i]; } uint8_t mailbox CAN_Transmit(CAN2, txMsg); return (mailbox ! CAN_TxStatus_Failed) ? 1 : 0; }CAN_Transmit的返回值有三种如果返回0到2表示成功占用某个邮箱返回CAN_TxStatus_Failed时表示三个邮箱都满此时需要等待或丢弃。这个返回值是判断发送是否成功的第一依据比只看函数返回后有没有立刻发出去可靠得多。DLC最好控制在8以内超长帧在标准CAN里不受支持。如果ID超过0x7FF那就应该走扩展帧把IDE改成CAN_Id_Extended并使用ExtId字段。3.4 中断接收与转发FIFO到邮箱的接力逻辑CAN1接收中断是整条链路的入口。中断处理的基本原则是能少做事就少做事不能在中断里长时间等待发送邮箱否则后续帧会因FIFO溢出被丢弃。工程里最简单的转发写法如下void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg rxMsg; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, rxMsg); // 这里直接调用CAN2发送不等待发送结果 // 如果发送失败可以选择丢弃这帧或交给主循环补偿 uint8_t ok CAN2_SendData(rxMsg.StdId, rxMsg.Data, rxMsg.DLC); if (!ok) { // 邮箱满时最简单的策略是丢掉避免中断卡死 } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }注意这个中断函数的名称必须和启动文件里的向量名完全一致写成CAN1_RX0_IRQHandler这种简化名在链接时不会报错但中断进不来。F105的启动文件里CAN1的RX0向量名是USB_LP_CAN1_RX0_IRQHandler这是很多第一次接触F105的开发者踩得最凶的坑。当CAN2的发送邮箱满时我一般不会在中断里用while死等CAN_TransmitStatus那会把整个中断堵住。更好的做法是先把帧丢进一个环形队列在主循环里统一发送队列满了再丢弃或者置一个错误标志。这个工程直接调用发送函数虽然简单但适合低负载场景后续改造空间很大。4. 编译运行与联调让双CAN例程在你的板子上真正跑通4.1 工程目录结构与启动文件选择这个工程是典型的STM32标准外设库结构目录大致如下STM32f105 CAN1收 CAN2发/ ├─ Libraries/ │ ├─ CMSIS/ │ └─ STM32F10x_StdPeriph_Driver/ ├─ User/ │ ├─ main.c │ ├─ stm32f10x_it.c │ └─ stm32f10x_conf.h ├─ Hardware/ │ ├─ can.c │ └─ can.h └─ Project/ ├─ 工程文件.uvprojx └─ JLinkSettings.ini在MDK里打开工程后第一件事是确认器件型号和启动文件。F105属于互联型产品线必须使用startup_stm32f10x_cl.s启动文件。有人会用startup_stm32f10x_hd.s替代程序能编译过但中断向量表对不上CAN2和USB相关的向量全部错位跑起来就是各种诡异问题。在C/C选项卡里还需要定义STM32F10X_CL否则标准外设库的条件编译不会打开互联型产品的支持。4.2 接线与收发器连接只连芯片引脚是不够的CAN控制器引脚输出的是差分信号必须先经过CAN收发器才能接到总线。常见接法如下外设功能芯片引脚接往收发器CAN1_RXPA11收发器1的RXDCAN1_TXPA12收发器1的TXDCAN2_RXPB12收发器2的RXDCAN2_TXPB13收发器2的TXD收发器的CANH和CANL分别接总线上的CAN_H、CAN_L在总线两端各接一个120Ω终端电阻。这个电阻必须有否则帧波形反射严重高速率时几乎必然丢帧甚至无法通信。收发器在与MCU连接时要注意电平匹配如果收发器是5V供电它的TXD输出到3.3V的STM32引脚时需要确认是否兼容很多收发器本身就做成了3.3V兼容但个别老型号需要加电阻分压。4.3 两种验证方式双板对测与单板自环验证整条收发链路的办法我推荐优先做双板对测。准备两块刷了同一份固件的板子把两板的CAN1总线并在一起CAN2总线也并在一起然后在一端接上USB转CAN分析仪。用分析仪往CAN1总线上发一帧标准帧比如ID 0x123加8字节数据两块板子的CAN1都会收到各自从CAN2转发出去。再在CAN2总线侧接另一路分析工具观察到了帧链路就通了。如果手头只有一块板也可以做单板自环。只要在CAN1总线侧用工具发帧再用示波器或USB转CAN工具挂在CAN2的PB12/PB13上观察。如果没有额外分析工具可以把工程临时改动初始化时先让CAN1自己发一帧测试报文然后看CAN2的发送引脚有没有动作。这个变通办法虽然验证不了“外部收到”但足以确认中断转发逻辑没有断。5. 避坑指南双CAN调试中我踩过的5个具体问题5.1 CAN2发送一直返回失败邮箱始终满现象CAN1收发正常但调用CAN_Transmit(CAN2, txMsg)时频繁返回CAN_TxStatus_Failed或者偶尔发送一次成功后后续全部失败。原因最常见的是RCC只开了CAN2的时钟没有开CAN1时钟。F105双CAN模块内部是一个整体CAN2的运行依赖CAN1的时钟域。其次GPIO的端口的复用功能没有配置正确CAN2的TX引脚PB13如果还是默认上拉状态波形送不出去。解决初始化里把CAN1和CAN2的时钟位一起打开严格按照外部收发器时序给TX引脚配置成复用推挽。如果这两步都确认了再用CAN_TransmitStatus查一下邮箱实际状态别只看CAN_Transmit的返回值。5.2 两个CAN都在接收帧只出现在其中一个现象给CAN1和CAN2都配置了筛选器后总线上的帧全部进了CAN1CAN2一个都收不到或者反过来CAN2全收CAN1收不到。原因28组筛选器是共享的两个CAN配置了相同的筛选器组号或者CAN_FilterFIFOAssignment写错了把本应挂在CAN2的筛选器挂到了CAN1的FIFO上。筛选器本身没有“归属”关系只有在哪里登记数据就从哪里进。解决给两个CAN分配不同的筛选器组比如低14组给CAN1高14组给CAN2并在CAN_FilterInit前用CAN_FMR确认筛选器启动模式。接收中断里也要分别处理对应的FIFO标志不能把两个CAN的中断处理混在同一个函数里。5.3 中断函数写了却不触发程序像死了一样现象主循环正常运行CAN1的接收中断标志位也确实置位了但断点打不进中断处理函数。原因第一函数名和启动文件里的向量名不一致最常见的是把USB_LP_CAN1_RX0_IRQHandler写成了CAN1_RX0_IRQHandler第二NVIC里没有使能对应的中断通道第三启动文件选错型号向量表位置偏移了。解决打开startup_stm32f10x_cl.s把CAN相关向量名全部列出来逐个与工程里的中断函数名比对。编译完以后打开map文件搜索IRQHandler字符确认它确实被链接到了向量表对应的位置。NVIC配置里除了使能CAN1_RX0_IRQn还要把优先级分组设置好否则中断嵌套时也可能出现进不来的现象。5.4 总线上有波形但对面收不到数据现象用示波器能看到CAN_H和CAN_L之间有明显的差分信号但另一块板子或者分析仪就是收不到帧有时候能看到错误帧波形。原因多半是波特率参数和对方不一致。F105的APB1时钟为36MHz大家从F103工程里复制过来时经常把Prescaler照搬而F103工程的APB1可能是不同的外设时钟配置导致实际波特率偏离目标值。另一个常见原因是终端电阻没接或者只接了一端。解决核算一遍波特率公式然后把采样点也确认好。标准库初始化里改变CAN_BS1和CAN_BS2后CAN_SJW也要重新检查。测试时在CAN_H和CAN_L之间直接并一个120Ω电阻两边都并联或者使用标准总线结构不要只靠收发器内部电阻。5.5 高速率长测试后开始丢帧FIFO溢出现象低速500Kbps下测试一切正常连续发数据半小时后开始丢包换成1Mbps后刚开始正常过一会儿丢帧越来越严重。原因中断里做转发时直接调用CAN2_SendData如果CAN2三个邮箱刚好全满这一帧就丢弃了。此时CAN1的FIFO还在继续接收FIFO溢出后也会把后续帧丢掉。简单轮询发送的结果就是负载越高丢帧越不可控。解决不要在中断里等待邮箱可用。改进方法有两种一是用CAN_TXFP把发送优先级设为顺序发送配合三个邮箱做多帧缓冲二是引入环形队列中断里只入队主循环统一发送。对于更高的吞吐可以把接收FIFO的溢出中断打开对溢出次数做统计而不是任由数据静默丢失。6. 二次开发把它改造成带路由功能的双CAN转发网关6.1 三个可落地的改造方向第一个方向是加ID路由。当前筛选器全收任何一帧都会从CAN2转发出去。如果在接收中断里增加一张路由表只把特定ID的帧转发给CAN2其他ID直接丢弃或转到CAN1这就成了一个简单的双CAN网关节点。第二个方向是数据缓冲。当前实现里接收中断直接调用发送函数邮箱满时丢帧不可控。我一般会在工程里加一个环形队列把收到的帧先存进去主循环里再做发送。队列长度取64条即可覆盖大多数中继场景队列满了再统计丢帧次数。第三个方向是错误管理。F105的CAN错误状态寄存器能区分被动错误和总线关闭可以在检测到总线错误时自动恢复或者把错误帧的计数通过串口打印出来。做设备联调时这个错误统计非常有用能告诉你丢帧到底是因为总线干扰还是因为协议层处理不过来。6.2 一个具体技巧用路由表替代全量转发下面这个结构体数组可以放在can.c里接收中断中先查表再决定转发的目标比起全量转发更灵活typedef struct { uint32_t id; uint8_t enable; // 1:转发 0:丢弃 uint8_t dest_can; // 1:走CAN1 2:走CAN2 } CanRouteItem; const CanRouteItem route_table[] { {0x123, 1, 2}, // ID 0x123转CAN2 {0x321, 1, 1}, // ID 0x321转CAN1 {0x456, 0, 2}, // ID 0x456直接丢弃 }; uint8_t CanRoute_Find(uint32_t id, uint8_t *dest) { uint8_t count sizeof(route_table) / sizeof(route_table[0]); for (uint8_t i 0; i count; i) { if (route_table[i].id id) { if (route_table[i].enable) { *dest route_table[i].dest_can; return 1; } return 0; } } return 0; }在接收中断的转发逻辑里把原来直接调用CAN2_SendData的地方改成先查表根据查到的目标决定调用CAN1还是CAN2。这样改动不需要动寄存器配置只是把数据流控制从硬件筛选器挪到软件层后续增加协议转换、DBC映射或者帧格式重组都在这张表上扩展。从那以后我每次拿到双CAN工程第一件事都是先打开启动文件核对向量名和筛选器组分配确认基础资源不打架再动业务逻辑这个习惯帮我少熬了很多夜。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑