简介面向STM32嵌入式开发者的CAN总线通信例程资源演示在STM32F105上通过CAN1接口接收数据、CAN2接口发送数据的完整实现适合需要学习多路CAN控制器独立应用的工程师或学生。整包共201个文件、大小仅2.12MB以C源文件和头文件为主体配齐Keil MDK工程文件、启动代码、编译生成的hex/axf固件、map映射表及调试列表既能对照源码理解细节也可直接烧录到开发板观察CAN1与CAN2的收发行为。目前已有981人学习。例程覆盖CAN控制器初始化、位时序与波特率配置、报文滤波器规则、收发邮箱管理、中断服务程序设计以及发送仲裁、错误检测与重传机制等关键知识点适合作为双CAN通道工程模板帮助开发者快速掌握STM32标准库与HAL库下CAN通信的实战要点并积累多接口实时通信的调试思路。 接了个现场设备联调的小活儿两条独立CAN总线上的设备需要互通数据A总线发过来的报文要原样送到B总线反过来B总线的也要送过去。市面上的CAN中继模块价格不便宜协议还是黑盒出了事儿只能找厂家所以干脆用手头现成的STM32F105开发板自己搭一个。核心逻辑一句话就能讲清CAN1收CAN2发。但实际落地时我发现这句话背后的配置细节比想象中多得多从滤波器分配、波特率对齐到总线波形验证每一步都有讲究。这篇文章就把这个工程从思路到踩坑完整记录下来给同样要做双CAN透传或者CAN网关的朋友做个参考。1. 项目思路双CAN桥接到底要做什么1.1 为什么选STM32F105而不是F103先说结论如果手里只有F103双CAN接口确实也是够用的F103同样带两个CAN控制器。但F105这颗料在双CAN场景下更顺手最明显的区别是滤波器组数量翻了一倍F103是14组F105直接给到28组。滤波器多了两条总线上挂多个节点ID时就能分得更细不会出现滤波器不够用、只能靠“全接收软件过滤”这种笨办法兜底的情况。另一个不算坑但必须知道的差异是F105的两个CAN控制器不是完全独立的。CAN1是主控角色CAN2作为从设备两者共享一块256字节的报文存储区。这意味着初始化CAN2的时候某些寄存器比如过滤器分配相关的FMR得通过CAN1的地址空间去操作代码上会比F103多一点绕弯。这个细节很多参考例程不会专门讲自己写的时候容易卡住。1.2 应用场景不只有透传一种玩法“CAN1收、CAN2发”听起来是简单透传实际工程里至少能拆出三类需求第一类是纯透传A总线上任何报文都往B总线上转发反过来也一样。这种需求适合用中继逻辑最简单但要注意两条总线的波特率最好一致否则跨速率转发时存在缓存溢出风险。第二类是选择性转发只把指定ID的报文送过去。比如采集A总线上某个温度传感器节点的数据转发给B总线上做显示那就在过滤器里精确锁定ID别的一概不收。第三类是协议转换CAN1这边用J1939协议解析CAN2那边按CANopen标准重新组帧。这种已经属于网关的范畴通常在中断里收、在主循环里处理协议栈再通过发送邮箱发到另一条总线。我这个工程以第二类为主兼顾第一类所以过滤器配置成了“接收指定ID 其余屏蔽”。后面代码部分会展开。1.3 硬件搭接收发器、终端电阻与电平匹配软件好写硬件先得稳住。STM32F105的CAN控制器引脚是这样的CAN1默认PA11(RX)/PA12(TX)重映射到PB8/PB9CAN2默认PB12(RX)/PB13(TX)重映射到PB5/PB6。我这个工程里全部走默认引脚没开重映射省得后面画板子时把重映射脚搞混。收发器选型是个容易被低估的环节。CAN控制器和收发器是两回事F105内部只有CAN协议控制器真正把差分信号送上总线的是一颗外置收发器。TJA1050这类老经典是5V供电的而F105的IO是3.3V虽然大多数情况下TXD输入能兼容3.3V逻辑但收发器的RXD输出回灌到MCU时电平匹配不够干净极端情况下会引入误码。我实际测试中直接换成了3.3V供电的SN65HVD230和F105的IO电平对齐后面再没因为电平问题出过怪毛病。终端电阻也要单独说。很多人以为板子上随便焊一个120Ω就行但CAN总线的终端电阻是加在物理总线两端各一颗120Ω不是每个节点都加。如果只有两块板子连在一起测试两头各放一颗如果是从总线中间某个节点引线出来调试那这个中间节点是不该加终端电阻的。我犯过的错误是两头各加了一颗之后又在一端多并联了一颗120Ω导致总线差分电压被拉低波形幅值明显不够通信时好时坏。2. CAN通信关键参数波特率、采样点与SJW2.1 波特率计算先把Tq搞明白CAN的波特率不是直接填一个数而是由APB1时钟、预分频器和位时间三段共同决定。公式是波特率 APB1时钟 / (预分频值 × (1 BS1 BS2))其中那个固定的“1”是同步段SYNC_SEGCAN协议规定它必须占1个时间量子Tq。F105的系统时钟如果从外部8MHz晶振倍频到72MHz那么APB1总线时钟是36MHz。我这个工程按500kbps来配计算过程如下Tq 预分频值 × (1 / 36MHz) 4 / 36MHz ≈ 111ns位时间 (1 13 4) 18个Tq波特率 36MHz / (4 × 18) 500kbps对应到标准外设库的初始化结构体就是CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_13tq; CAN_InitStructure.CAN_BS2 CAN_BS2_4tq; CAN_InitStructure.CAN_Prescaler 4;如果目标是250kbps预分频改成8其他不变即可36MHz / (8 × 18) 250kbps。建议先算清楚再写代码别拿着例程的预分频值直接套APB1时钟不同结果差很多。2.2 采样点与SJW的工程选型位时间里的BS1和BS2比例决定采样点位置。采样点就是控制器在一个位周期内读取电平的时刻太靠前容易把噪声当有效信号太靠后留给相位缓冲的余量就少。采样点 (1 BS1) / (1 BS1 BS2)我配的13/4组合采样点 (1 13) / 18 ≈ 77.8%。这个位置在CANopen推荐范围75%到87.5%之内属于比较稳的选择。如果总线距离远、干扰大可以把采样点往后挪一点比如BS114、BS23采样点到83%左右。SJW同步跳跃宽度是重同步时最多能跨越的Tq数量取值1到4。SJW太小遇到节点间晶振频率偏差较大时同步不过去会产生错误帧SJW太大虽然同步能力强但采样窗口会被挤占。实测下来如果总线上全是MCU节点、晶振精度在50ppm以内SJW取1就够用如果混了非标设备或者线缆特别长我习惯取2或3。这个参数很多人忽略但恰恰是总线“偶发错误帧”的常见元凶。2.3 过滤器设置ACCCode与ACCMask到底怎么用有些资料会把过滤器寄存器叫“验收码”和“验收屏蔽码”对应的就是ST库里的FilterId和FilterMask。原理其实一句话验收码寄存器存你期望的ID验收屏蔽寄存器对应位为1表示“这一位我不关心”为0表示“这一位必须匹配”。全0掩码就是精确匹配一个ID全1掩码就是放行所有报文。F105的32位过滤器模式下一个滤波器可以覆盖标准帧整个11位ID加上扩展帧的18位数据用高位对齐的方式塞进CAN_FxR0和CAN_FxR1掩码对应放在CAN_FxR2和CAN_FxR3。实践中如果只想收ID 0x123的报文可以这样配CAN_FilterInitStructure.CAN_FilterIdHigh 0x0123 3; // 11位ID高位 CAN_FilterInitStructure.CAN_FilterIdLow 0; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFF8; // 只比对高13位低3位不关心 CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000;注意这里ID在32位寄存器里是左移3位存放的所以赋值时要右移3位初学容易在这块把ID写错。更省事的办法是先用全掩码验证通信确认链路通了再收紧过滤规则否则调试时还会怀疑是过滤器吃了报文。3. 工程代码实现从初始化到转发逻辑3.1 时钟与GPIO初始化首先是使能时钟。F105上CAN1、CAN2的时钟都在APB1上加上GPIOA、GPIOB和AFIO时钟一个都不能少。我用标准外设库初始化代码是这样的RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1 | RCC_APB1Periph_CAN2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);GPIO配置要区分TX和RX。TX引脚是复用推挽输出RX引脚是带上拉的输入。我用的引脚是CAN1的PA11/PA12和CAN2的PB12/PB13配置方式如下GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; // PA11 CAN1_RX GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // TX GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; // PA12 CAN1_TX GPIO_Init(GPIOA, GPIO_InitStructure);CAN2那组引脚同样处理只是PA换成PBPin从11/12换成12/13。这里有个经验TX和RX搞反是常见的低级错误。查板子原理图是最快的别凭记忆我报废过一块转接板。3.2 CAN1接收模式配置初始化CAN1时除了波特率参数还要设置模式。普通模式下CAN_InitStructure.CAN_Mode CAN_Mode_Normal如果只是联调可以先设成CAN_Mode_LoopBack回环模式验证控制器本身有没有问题再切回Normal接总线。接收这边我给的是掩码过滤先放行全部报文方便链路调试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_FilterInit(CAN_FilterInitStructure);掩码全部为0表示每一位都必须匹配而ID也全部是0那能匹配上的只有ID为0的报文。等等这里正好是个容易混淆的点。ST库里的掩码和前面讲的“验收屏蔽”语义不太一样库里的掩码位为0表示“必须匹配”为1表示“不关心”。想要放行所有报文掩码寄存器应该全为1不是全为0。我在实际写的时候用的是CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFFF; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0xFFFF;这个细节特别重要很多刚接触的人就在这栽跟头——以为是全0放行结果一条报文都收不到。我把这个坑单独提出来就是想告诉各位拿到库函数先确认掩码的语义不同厂家甚至不同版本的库是反着定义的。接收方式我最终选了中断。主循环里轮询CAN_MessagePending也可以但CAN1接收、CAN2发送这种转发场景报文到达的时机不可控轮询可能因为主循环正在做别的事情丢报文。中断配置如下CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);3.3 CAN2发送配置与转发逻辑CAN2这边只做发送配置比CAN1简单。波特率参数、模式跟CAN1保持一致不需要配过滤器。不过因为F105的CAN2和CAN1共享过滤器存储区有一个寄存器我建议主动设置一下那就是CAN_FMR里的CAN2SB位。它决定从第几组过滤器开始分配给CAN2。虽然理论上只发送不需要过滤器但不设这个位后面一旦需要CAN2接收就会发现怎么配都收不到。我的习惯是一开始就分好CAN1-FMR | CAN_FMR_FINIT; // 进入初始化模式 CAN1-FMR ~(((uint32_t)0x3F) 8); // 清掉CAN2SB CAN1-FMR | ((uint32_t)14) 8; // CAN2SB 14过滤器14~27归CAN2 CAN1-FMR ~CAN_FMR_FINIT; // 退出初始化模式主循环的转发逻辑用伪代码描述就是CAN1收到报文原样填入CAN2的发送结构体然后调用CAN_Transmit。这里有个细节发送函数的返回值有三种成功、邮箱未释放、非法参数。写代码时不能只看是否调用成功还要等发送邮箱空出来CanTxMsg TxMessage; uint8_t retry_cnt 0; // 从CAN1 FIFO0读取报文填充TxMessage结构体 // ... 赋值Stdid/ExtId/IDE/RTR/DLC/Data ... while (CAN_Transmit(CAN2, TxMessage) CAN_TxStatus_Failed) { if (retry_cnt 100) break; // 防止死等 }如果两条总线的波特率不一致这里还要考虑缓存。比如CAN1是500kCAN2是250kCAN1瞬间灌进来100帧报文CAN2发得慢满了就得丢。这种场景我在工程里加了个简单的环形缓冲区和丢弃策略新报文优先覆盖旧报文保证最新状态能送过去。3.4 中断里收、主循环里发把接收放在中断里发送放在主循环目的是缩短关中断的时间。中断服务函数里就做三件事从FIFO读报文、拷贝到全局变量、置一个标志位。不要做协议解析更不要在那里调CAN_Transmit死等。实测下来500k波特率满负载情况下这种结构能扛住高频报文偶发丢帧也可以通过加大缓冲区解决。中断服务函数名字要和启动文件对应F105上CAN1接收FIFO0的中断向量叫USB_LP_CAN1_RX0_IRQn函数名是void USB_LP_CAN1_RX0_IRQn_handler(void) // 具体函数名以工程模板为准 { if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxBuffer); forwarding_flag 1; CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }4. 调试实录与常见问题排查4.1 示波器看波形判断通信好坏代码写完之后真正的考验在总线上。我建议调试CAN时一定接示波器看波形比看逻辑分析仪的数据更直观。CAN总线在空闲时CAN_H和CAN_L都是2.5V左右差分电压接近0这是隐性电平。显性位时CAN_H拉到约3.5VCAN_L降到约1.5V差分约2V。如果示波器抓到幅值只有几百毫伏甚至更低先怀疑终端电阻是不是两端电阻没接或者接的位置不对。如果波形边沿圆钝、上升沿有明显拖尾可能是线缆太长、支线太多或者收发器驱动能力不足。如果抓到的帧中间有异常压缩的显性位多半是有节点在发错误帧那就需要看总线错误计数器和错误码寄存器。4.2 常见问题速查表现象可能原因排查与解决方法一块板收不到任何报文过滤掩码配反掩码全1放行所有报文确认通了再收紧规则两块板都收不到GPIO的TX/RX接反对照原理图确认特别是用转接板时波特率不一致出现连续错误帧两个节点的BRP/BS1/BS2设置不同用公式反推波特率逐项核对偶发错误帧通信时好时坏终端电阻缺失或过多总线两端各一颗120Ω中间节点不加上电一会儿就通信全断CAN控制器进入Bus-Off检查CAN_ESR寄存器排查是否总线短路插上CAN分析仪没反应上位机驱动兼容问题检查设备管理器识别状态尝试旧版本驱动显性电平幅值偏低收发器供电不足或电平不匹配改用3.3V供电收发器测量TXD/RXD电平这几类问题我几乎都遇到过一遍。特别是CAN分析仪的驱动换了台Win11电脑折腾了半个多小时最后是换了旧版驱动解决的新版本反而在Win11上稳定性很差。4.3 两个实际踩过的坑第一个坑是收发器电平不匹配。我一开始用的是手头一块5V供电的TJA1050板子F105的TXD输出3.3V高电平理论上能识别但总线在长时间运行后偶发错误帧用示波器看TXD引脚发现高电平只有2.8V左右刚好卡在接收阈值的临界区。后来换了SN65HVD2303.3V供电TXD高电平干净多了错误帧彻底消失。建议新设计直接选带VIO引脚或者3.3V供电的收发器别在高电平临界区上省成本。第二个坑是跨速率转发时没有缓存导致低速总线丢帧。当时CAN1是500k、CAN2是250k高速端一口气发来一批报文CAN2发送邮箱根本排不过来丢了不少。加上环形缓冲区之后转发丢帧率明显下降。但要注意缓存只是缓解如果持续满载任何缓冲都会溢出。工程上最终要回到需求侧瞬时峰值能不能在几百毫秒内发完发不完就得在应用层协商降频或只转发关键帧。这个取舍必须提前跟甲方确认。5. 写在最后的一点体会这个双CAN桥接工程做下来我的感受是STM32F105做CAN网关类应用性价比确实不错原生双CAN省了外扩控制器28组滤波器在ID比较多的时候也够用。但最花时间的往往不是写代码而是排查总线物理层的问题波形、电平、终端电阻这三个环节占了我整个调试周期的一大半。如果硬件上收发器选型合理、终端电阻规范、波特率计算严谨软件部分其实很稳定跑几天下来错误计数基本为零。最后再分享一个小技巧调试时在代码里加一个心跳报文每秒从CAN2发一条带有设备状态和数据计数器的帧用CAN分析仪盯着看一旦出现异常从丢了哪一帧能很快反推出问题在哪一段这个习惯帮我省了不少排查时间。本文还有配套的精品资源点击获取