☰
基于STM32F105的双CAN总线透传网关设计与实现
2026/10/18 0:07:46 网站建设 项目流程

简介:面向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/PB9;CAN2默认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通信关键参数:波特率、采样点与SJW

2.1 波特率计算:先把Tq搞明白

CAN的波特率不是直接填一个数,而是由APB1时钟、预分频器和位时间三段共同决定。公式是:

波特率 = APB1时钟 / (预分频值 × (1 + BS1 + BS2))

其中那个固定的“1”是同步段SYNC_SEG,CAN协议规定它必须占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%之内,属于比较稳的选择。如果总线距离远、干扰大,可以把采样点往后挪一点,比如BS1=14、BS2=3,采样点到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换成PB,Pin从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是500k,CAN2是250k,CAN1瞬间灌进来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.5V,CAN_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左右,刚好卡在接收阈值的临界区。后来换了SN65HVD230,3.3V供电,TXD高电平干净多了,错误帧彻底消失。建议新设计直接选带VIO引脚或者3.3V供电的收发器,别在高电平临界区上省成本。

第二个坑是跨速率转发时没有缓存,导致低速总线丢帧。当时CAN1是500k、CAN2是250k,高速端一口气发来一批报文,CAN2发送邮箱根本排不过来,丢了不少。加上环形缓冲区之后,转发丢帧率明显下降。但要注意,缓存只是缓解,如果持续满载,任何缓冲都会溢出。工程上最终要回到需求侧:瞬时峰值能不能在几百毫秒内发完?发不完就得在应用层协商降频或只转发关键帧。这个取舍必须提前跟甲方确认。

5. 写在最后的一点体会

这个双CAN桥接工程做下来,我的感受是STM32F105做CAN网关类应用性价比确实不错,原生双CAN省了外扩控制器,28组滤波器在ID比较多的时候也够用。但最花时间的往往不是写代码,而是排查总线物理层的问题,波形、电平、终端电阻这三个环节占了我整个调试周期的一大半。如果硬件上收发器选型合理、终端电阻规范、波特率计算严谨,软件部分其实很稳定,跑几天下来错误计数基本为零。最后再分享一个小技巧:调试时在代码里加一个心跳报文,每秒从CAN2发一条带有设备状态和数据计数器的帧,用CAN分析仪盯着看,一旦出现异常,从丢了哪一帧能很快反推出问题在哪一段,这个习惯帮我省了不少排查时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询