STM32F105双CAN实战:CAN1收CAN2发配置与避坑指南

STM32F105双CAN实战:CAN1收CAN2发配置与避坑指南 简介这是一份面向STM32F105开发者的CAN总线通信例程核心实现CAN1接口接收数据、CAN2接口发送数据适用于需要掌握STM32双CAN独立收发配置的嵌入式学习与项目调试场景。压缩包共包含201个文件整体大小2.12MB以C语言源码和头文件为主h/c文件约113个同时提供Keil工程文件uvproj/uvopt、编译产物hex/axf以及crf、o、lst等中间文件便于查看完整工程结构或直接烧录验证。例程中可学习到CAN外设的时钟使能、波特率与位时间配置、接收滤波器设置、收发邮箱状态判断以及中断服务程序的编写思路并可通过CAN1接收与CAN2发送的实例了解双CAN并行通信时的资源分配与同步处理方法。目前已有981人学习该资源对于正在调试STM32F105 CAN通信或准备移植相关协议的开发者这套精简工程能提供直观的代码参考与排错依据。 搞嵌入式的小伙伴应该都遇到过这种需求一块板上既要接收一路CAN数据又要往另一路CAN总线上发数据。手里正好有一片STM32F105自带两个CAN控制器这活儿本来应该很顺手——可实际调起来踩坑的点比想象中多。这个项目就是典型的“CAN1收、CAN2发”双CAN独立工作场景我把整个配置思路和调试过程梳理一下给后面做网关、做协议转换的朋友当个参考。F105这块芯片很有意思它比F103多了内置USB PHY和双CAN做小型车载网关、工业协议转换器特别合适。很多人一看到“双CAN”就以为是把CAN1的配置复制一份给CAN2实际跑起来才发现根本不是那么回事。CAN2在F105里是一个“从属外设”它挂在CAN1的下面时钟、过滤器都有依赖关系。这篇文章我会从时钟树、引脚映射、过滤器归属、收发改动这几个方面一层层拆开讲最后附上我调试时踩过的坑。1. 项目拆解为什么非要用F105来做“一收一发”1.1 F105的双CAN资源到底特殊在哪F105和F103虽然都是72MHz主频、同一套Cortex-M3内核但F105的外设组合更适合做CAN网关。它内置两个CAN控制器CAN1和CAN2。CAN1是主控制器CAN2是从控制器。这里说的“主从”不是软件概念而是硬件层面的依赖关系CAN2的时钟源来自CAN1的时钟域使用CAN2之前必须使能CAN1的时钟否则CAN2根本起不来。另一个容易忽略的点是过滤器资源的分配。F105内部有28个过滤器这28个过滤器不是简单分成“14个给CAN1、14个给CAN2”而是通过FMR寄存器里的CAN2SB位段来划分起点。默认情况下过滤器0到13归CAN1过滤器14到27归CAN2。如果代码里没有初始化这个分割点或者初始化顺序写反了很容易出现“CAN1收发正常CAN2却收不到任何帧”的诡异现象。1.2 这种“一收一发”适合什么场景CAN1收、CAN2发最典型的应用就是网关转发一路CAN网络比如车内动力总成总线的数据需要先收进来经过过滤、协议转换或ID重映射再发到另一路CAN网络比如车身总线。两个网络之间波特率可能不同一个500kbps一个250kbps这就必须靠MCU做桥梁。另一个常见场景是数据采集和模拟CAN1负责接收真实设备发来的报文CAN2接一个可调电阻或按键模块模拟某条总线的故障注入或特定工况报文。两边物理隔离互不干扰一台设备就能顶两块USB-CAN卡的工作。这个项目标题虽然是“CAN1收 CAN2发”但反过来“CAN1发 CAN2收”的用法也完全一样只是把下面的代码对称调换而已。2. 时钟树与引脚规划先别急着写代码2.1 CAN2是“从属外设”时钟使能有讲究F105的APB1外设时钟最高是36MHz。CAN1和CAN2挂在APB1总线上但CAN2本身没有独立的时钟门控它复用CAN1的时钟域。所以在初始化时必须同时使能两个外设的时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN2, ENABLE);这里有个很容易踩的坑很多人认为既然只用到CAN2发送那就只使能CAN2的时钟结果CAN2的初始化函数一直返回ERROR。原因就是CAN2的时钟依赖CAN1哪怕CAN1一个报文都不发也必须先把CAN1的时钟打开。我见过更隐蔽的情况CAN2配置看起来正常波特率也对但发送时TA位置位非常慢甚至超时。排查到最后发现CAN1虽然没被使用但它的初始化函数没有调用导致CAN2处于“时钟域异常”状态。所以保险做法是只要项目里用了双CAN就把两个外设的初始化流程都走完哪怕CAN1只初始化不收发。2.2 引脚映射与复用配置F105的CAN引脚映射有几组可选项目里常用的是默认复用功能CAN1_TXPA12CAN1_RXPA11CAN2_TXPB6CAN2_RXPB5这里的RX引脚要配置成上拉输入TX引脚配置成复用推挽输出。很多人第一次调CAN不通信检查一通发现TX引脚配成了GPIO_Mode_AF_OD复用开漏而CAN控制器本身是差分输出开漏模式下电平拉不起来波形自然不对。GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // CAN1 RX PA11 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // CAN1 TX PA12 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // CAN2 RX PB5 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); // CAN2 TX PB6 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);如果你用的板子把CAN引脚引到了别的复用脚记得查参考手册的AFIO映射表。F105的CAN2还支持重映射到PB12/PB13用这个功能的时候要额外开启AFIO时钟不然引脚电平一直不对。2.3 波特率计算采样点和同步跳跃宽度CAN波特率不是随便填的它由APB1时钟、预分频器、BS1、BS2共同决定。F105的APB1最高36MHz如果你的系统时钟配置成72MHzAPB1分频系数为2那么CAN外设时钟就是36MHz。目标波特率500kbps位时间 1/500k 2μs。如果预分频为4TQ 36MHz / 4 9MHz单个TQ约111ns。2μs / 111ns ≈ 18TQ。位时间由1个同步段 BS1 BS2组成同步段固定为1 TQ所以BS1 BS2 17。查寄存器范围BS1可以设13TQBS2设4TQ这样采样点 (1 13) / 18 ≈ 77.8%是非常理想的位置。CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_Prescaler 4; CAN_InitStructure.CAN_BS1 CAN_BS1_13tq; CAN_InitStructure.CAN_BS2 CAN_BS2_4tq; CAN_InitStructure.CAN_SJW CAN_SJW_1tq;这里必须单独提一下SJW同步跳跃宽度。SJW的作用是当总线上的边沿出现相位误差时控制器把采样点往前或往后挪动的最大宽度。一般建议设成1TQ就够了网络上很多教程把它设成4TQ反而让抗干扰能力变差。因为SJW越大采样点抖动范围越大在总线上多个节点时这种「让」出来的宽度容易掩盖真正的同步问题。对F105来说波特率计算最容易出错的地方在于APB1时钟到底是多少。如果你用了别人的工程模板系统时钟可能跑在108MHz甚至120MHzAPB1分频后变成54MHz。这时候把F103上那套“APB136MHz、预分频9、BS18、BS27”的500kbps配置照搬过来实际波特率会偏到天上去。所以每次换芯片、换时钟树务必重新算一遍别抄老代码。3. 过滤器与中断接收让CAN1老老实实收数据3.1 过滤器分配CAN1和CAN2共用一套过滤器F105的28个过滤器是双CAN共用的。项目要求CAN1收、CAN2发接收侧只有CAN1需要过滤器因此可以把大部分过滤器都划分给CAN1但必须保证CAN2也至少有一个过滤器可用否则CAN2虽然能发送但一旦总线上有其他帧进入它的接收路径可能触发错误中断。初始化过滤器前建议先设置CAN2的过滤器起始位置CAN_FilterInitTypeDef CAN_FilterInitStructure; // 先设置过滤器分割0~13归CAN114~27归CAN2 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 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_ConfigFilter(CAN_FilterInitStructure);上面的代码把过滤器0用于CAN1掩码全0表示接收所有帧。如果你只想接收某个ID比如0x123屏蔽位要相应配置ID高16位和低16位分别填入对应位置掩码位相应位置0即可。不少初学者在这里把屏蔽位的含义搞反了——掩码位为0是必须匹配为1是忽略。3.2 接收中断的两种处理思路CAN1收到报文后有两种常见处理方式中断方式和轮询方式。网关项目里我推荐中断因为CAN报文的到达时间不确定轮询在任务繁忙时容易丢帧。void CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 这里把报文存入环形缓冲区或者直接丢给CAN2发送 CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }注意FMP0这个中断标志有个特点它反映的是FIFO0中待读报文的个数当FIFO0读空后硬件会自动清掉该中断标志。所以在中断里先调用CAN_Receive再调用ClearITPendingBit这样能保证FIFO里的报文全部被读完不会出现“读了一帧但中断一直挂着”的情况。如果你想同时接收两路CAN的数据中断服务函数的写法要特别小心FIFO0和FIFO1可以分别产生FMP0和FMP1中断但是否进同一中断函数取决于你在NVIC里怎么配置。建议把CAN1的RX0和RX1中断都统一到一个处理函数里先用if判断是哪个FIFO触发的再去读取避免两个中断入口竞争同一份报文缓冲区。3.3 扩展帧和远程帧的处理很多CAN项目只处理标准帧但实际工业设备里扩展帧很常见。CAN1在接收时判断标准帧和扩展帧的依据是IDE标志位。如果过滤器设置成32位模式那么标准帧和扩展帧都能匹配到只是掩码的计算方式不同。我在项目里见过一个奇葩问题设备发的都是扩展帧MCU过滤器却只配了标准帧的ID结果一帧都收不到还把过滤器删了重写了好几遍。这种问题用CAN分析仪一抓就能看出来但如果是自己写代码就很容易忽略。接收端最好在代码里显式判断RxMessage.IDE不同帧类型走不同处理分支特别是网关转发时发送端的IDE要和接收端保持一致否则对端设备会解析失败。4. 发送通路搭建CAN2把数据发出去4.1 发送邮箱机制和请求流程CAN2发送用三个发送邮箱调用CAN_Transmit时硬件会自动选择一个空闲邮箱装载报文然后发出发送请求。如果三个邮箱都满了CAN_Transmit会返回CAN_TxStatus_NoMailBox。uint8_t CAN2_SendData(uint32_t id, uint8_t ide, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; TxMessage.StdId id; TxMessage.IDE ide; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len; for (uint8_t i 0; i len; i) { TxMessage.Data[i] data[i]; } return CAN_Transmit(CAN2, TxMessage); }发送完成后硬件会置位对应的传输完成标志。比较稳妥的发送方式是在发送前检查上一次的完成状态uint8_t CAN2_SendDataBlocking(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId id; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len; for (uint8_t i 0; i len; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CAN2, TxMessage); // 等待发送完成极限超时时间根据波特率调整 uint32_t timeout 0xFFFF; while (CAN_TransmitStatus(CAN2, mailbox) ! CAN_TxStatus_Ok timeout--) { } return (CAN_TransmitStatus(CAN2, mailbox) CAN_TxStatus_Ok) ? 0 : 1; }在实时性要求高的系统里“阻塞等发送完成”要慎用。CAN2发送时如果总线上有其他节点也在发送会发生仲裁。低优先级报文在极端情况下可能一直发不出去如果代码里无脑循环等TA位MCU就被卡死了。建议用发送中断或者至少加上超时保护。4.2 发送失败问题不一定在代码CAN2发送失败经常被误判成“MCU配置问题”。实际上CAN是差分总线必须有至少两个节点通信才能正常工作。如果你只用一个开发板做自发自收必须把终端电阻放在CANH和CANL之间同时把通信模式设为回环模式CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack;项目里如果从回环模式切到正常模式很多人忘了修改这一行导致代码发不出去。正常模式下CAN控制器发出报文后没有收到ACK就会不断重试最终进入总线关闭状态Bus-Off表现就是TXD引脚有波形但总线上抓不到完整报文。另外CAN收发器芯片的供电电压也要检查。F105的CAN控制器输出是3.3V电平外接TJA1050这类5V收发器时要注意电平适配。很多板子直接把3.3V的MCU TXD接到5V收发器波形会有轻微畸变短距离没问题距离一长就出现偶发错帧。保险做法是用支持3.3V供电的收发器比如SN65HVD230。5. 调试实录与避坑手册5.1 双CAN同时使用时的三个经典坑我把实际调试中遇到的典型问题整理成一个速查表方便后面的人直接对照排查故障现象可能原因排查思路CAN2初始化返回ERROR没有使能CAN1时钟确认RCC_APB1Periph_CAN1已使能CAN2能发不能收过滤器全部划分给CAN1CAN2无过滤器检查FMR寄存器中CAN2SB的起始过滤器编号双CAN同时用时CAN1偶发丢帧中断优先级配置不当CAN1 RX中断被打断把接收中断设为最高可抢占优先级或使用FIFO1互补总线有ACK错误发送超时总线上只有本机一个节点或终端电阻缺失用CAN分析仪或另一个节点应答检查120Ω电阻报文内容正确但ID不对标准帧和扩展帧混淆检查发送端的IDE标志确认接收端掩码方向双CAN项目特别是网关型应用建议一开始就用两个独立的CAN分析仪分别监控CAN1和CAN2总线。有些开发者只监控了一路发向另一路的报文成了“黑盒”定位问题全靠猜浪费时间。5.2 用波形和日志快速定位问题调CAN最直观的手段是逻辑分析仪或示波器抓TXD引脚波形。CAN空闲时总线为隐性高电平有一帧数据时能看到显性位低电平和隐性位高电平交替变化。如果抓到的波形全都是高电平说明控制器根本没有发送问题在代码逻辑或时钟。如果波形有低电平但很窄且重复出现相同图案大概率是总线缺少ACK应答控制器在反复重发。我习惯在调试阶段给每个CAN保留一个调试日志串口在中断收帧后打印ID和数据。打印函数不要放在CAN中断里直接调用因为串口打印速度远低于CAN接收速度会互相影响。正确做法是把收到的报文放进环形缓冲区主循环再通过串口输出。这个习惯帮我在很多项目里省下了数小时定位时间。5.3 过滤器掩码配置的现场教训有一次我把CAN1的过滤器设成只接收ID为0x123的帧掩码位配的是0xFFFF全忽略结果总线上任何帧都能进来。后来看了参考手册才反应过来掩码位为0表示“必须相等”为1表示“不关心”。很多新手在这里卡住是因为他们把掩码理解成了“把某位屏蔽掉”。实际正好相反——屏蔽位为1是忽略该位。具体来说32位掩码模式下想要只接收标准ID 0x123二进制0001 0010 0011过滤器的ID寄存器要填入0x0123高位和0x0000低位掩码寄存器要把所有相关位置0其余不关心的位置1。如果掩码全0那就只匹配0x123一个ID如果掩码全1那就是接收全部报文。理解了这个原理过滤器配置就不再是玄学。5.4 两个CAN模块互相通信的自测方法如果你手里暂时没有CAN分析仪可以用F105自带的双CAN做一次自测CAN1发CAN2收。把CAN1的发送ID设为0x111CAN2的过滤器掩码设置成接收0x111两个控制器共用同一对收发器总线。然后在主循环里用CAN1定时发送在CAN2的接收中断里计数。这个方法能同时验证两个CAN通道的时钟、引脚、过滤器、中断是否全部正常。自测时要注意如果两个CAN控制器接在同一物理总线上波特率必须一致且总线上必须有一个节点处于正常模式。你可以把CAN1配成正常模式CAN2配成回环模式但两者的波特率还是要匹配。个人经验小结调CAN这种底层通信我最大的感受是先查时钟再查配置先量波形再看寄存器。F105这个双CAN架构本身很成熟只要把时钟使能和过滤器归属这两条命脉理顺CAN1收、CAN2发跑起来其实很稳。最后再分享一个习惯调试时把每次的波特率计算过程包括APB1时钟、预分频、BS1、BS2、采样点全部写进代码注释里。这样后面换芯片、换频率或者同事接手时只需要对照注释重新算一遍不用重新推导一遍。这个细节看起来不起眼但在项目周期长、人员流动大的环境里能省下大把沟通时间。本文还有配套的精品资源点击获取