☰
STM32F103与TJA1050 CAN总线双机通信实战:硬件配置、过滤器设置与调试排错
2026/10/18 1:32:54 网站建设 项目流程

1. 为什么CAN总线在嵌入式项目里始终绕不开

搞过工业控制、汽车电子或者储能设备的朋友应该都有体会,设备之间的通信一旦上了距离和干扰强度,UART和I2C就开始力不从心了。UART点对点还行,多机通信要组网就得加一堆硬件握手;I2C总线挂几个设备就开始出现时序拉扯,稍微长一点的排线就通信失败。这时候CAN总线几乎是默认选项——差分信号抗干扰、多主架构天然支持优先级仲裁、硬件CRC校验省去大量软件校验逻辑,这些特性让它在电磁环境复杂的场景里活得非常滋润。

我这次拿STM32F103C8T6做双机通信实验,收发器用的是TJA1050。选这个组合的原因很直接:F103的CAN外设(bxCAN)功能完整,标准帧、扩展帧、过滤器组、中断收发都支持,资料也足够多;TJA1050是经典的高速CAN收发器,电路简单,和3.3V MCU配合只需要注意电平匹配问题。整套方案成本低、可复现性强,适合拿来吃透CAN通信的完整链路。

这篇文章我会从硬件接线、CubeMX配置、过滤器设置、收发代码、调试排错几个维度完整拆一遍。不管你是第一次接触CAN,还是之前调过但没调通,应该都能从里面找到有用的东西。我踩过的坑会重点标出来,尤其是过滤器配置和终端电阻这两个新手最容易翻车的地方。

2. 硬件层:TJA1050与STM32的接线细节和终端电阻的坑

2.1 TJA1050引脚功能与典型电路

TJA1050一共8个引脚,实际用到的核心引脚就四个:TXD(发送输入)、RXD(接收输出)、VCC(5V供电)、GND。另外CANH和CANL是差分总线输出,S引脚是模式选择(高速模式或静默模式),一般直接接地选高速模式。

这里有个非常容易忽略的点:TJA1050的VCC是5V供电,但它的TXD和RXD引脚电平是兼容3.3V的。TJA1050的数据手册里明确写了TXD输入高电平阈值最低是2.0V,所以STM32的3.3V推挽输出可以直接驱动TXD,不需要电平转换。RXD输出高电平大约是VCC减去一个二极管压降,大概4V左右,直接接到STM32的RX引脚(PA11)也没问题,因为F103的很多引脚是5V容忍的,PA11正好是其中之一。

典型接线如下:

TJA1050引脚连接到说明
TXD (pin 1)STM32 PA12 (CAN_TX)发送数据输入
RXD (pin 4)STM32 PA11 (CAN_RX)接收数据输出
VCC (pin 3)5V收发器供电
GND (pin 2)GND共地
CANH (pin 7)双绞线CANH差分高
CANL (pin 6)双绞线CANL差分低
S (pin 8)GND高速模式

两块板子之间的连接就是CANH接CANH,CANL接CANL,GND最好也连一根线做共地参考。如果两块板子各自独立供电,不共地的话差分信号没有参考点,通信会非常不稳定。

2.2 终端电阻:不是可选项,是必须项

CAN总线两端各需要一个120欧姆的终端电阻,这是CAN规范里明确要求的。它的作用是消除信号在总线末端的反射,保证差分信号的完整性。很多新手在实验台上用短线连接两块板子,觉得线这么短不加电阻也能通,确实有时候能通,但通信误码率会明显偏高,稍微加点干扰就丢帧。

我实测过:两块板子之间用20cm杜邦线连接,不加终端电阻的情况下,短帧通信勉强能跑,但一旦连续发送大量数据,错误帧计数器(CAN_ESR寄存器里的LEC和REC/TEC)就开始飙升。加上两个120欧姆电阻之后,同样的测试条件,连续跑几个小时都不出一帧错误。

具体接法就是在总线两端各并联一个120欧姆电阻,也就是一块板子的CANH和CANL之间焊一个120欧姆,另一块板子也焊一个。注意是两端各一个,不是每个节点都加。如果你总线上挂了三块以上的板子,只有物理位置最远的两个节点需要加终端电阻,中间节点不加。

提示:有些TJA1050模块出厂就自带了120欧姆终端电阻,用之前先用万用表量一下CANH和CANL之间的阻值。如果量出来是60欧姆左右,说明两端都有电阻;如果是120欧姆,说明只有一端有;如果是无穷大,说明两端都没有。这个检查步骤能帮你省掉至少半小时的排错时间。

2.3 双绞线的选择与布线注意事项

CANH和CANL必须用双绞线,这不是讲究,是差分信号的基本要求。双绞线能让两根线受到的干扰尽可能一致,在接收端做差的时候共模干扰就被抵消掉了。如果用两根分开的普通导线,抗干扰能力会大打折扣。

线材方面,短距离实验用普通的网线拆出来的双绞线对就够了,特性阻抗大概100欧姆左右,和CAN要求的120欧姆有偏差但影响不大。如果要做长距离通信(比如几十米以上),建议用专门的CAN总线电缆或者带屏蔽层的双绞线,屏蔽层单端接地。

布线的时候尽量远离电机、继电器、开关电源这些干扰源。如果实在避不开,交叉的时候尽量垂直交叉,不要平行走线。

3. CubeMX配置:CAN外设参数的计算与过滤器设置

3.1 波特率计算:从时钟树到CAN_BTR寄存器

CAN的波特率由三个参数决定:BRP(波特率预分频)、TS1(时间段1)、TS2(时间段2)。公式是:

波特率 = APB1时钟 / (BRP × (1 + TS1 + TS2))

STM32F103的CAN挂在APB1总线上,默认APB1时钟是36MHz。假设我们要配500kbps的波特率(这是工业上最常用的速率之一),计算过程如下:

36,000,000 / 500,000 = 72,也就是说 (BRP × (1 + TS1 + TS2)) 要等于72。

通常采样点建议在75%到87.5%之间。取TS1 = 5,TS2 = 3,那么1 + 5 + 3 = 9,BRP = 72 / 9 = 8。采样点位置 = (1 + TS1) / (1 + TS1 + TS2) = 6 / 9 = 66.7%,偏低了一点。

换个组合:TS1 = 6,TS2 = 2,1 + 6 + 2 = 9,BRP = 8,采样点 = 7 / 9 = 77.8%,这个就比较合适了。

在CubeMX里配置的时候,这些参数对应的是:

  • Prescaler (BRP) = 8
  • Time Seg1 (TS1) = 6
  • Time Seg2 (TS2) = 2
  • SJW (重新同步跳转宽度) = 1

CubeMX会自动帮你算出实际波特率,如果显示500kbps就说明配置正确。两块板子的波特率必须完全一致,差一点都不行,CAN控制器对位时间的容错非常小。

3.2 过滤器配置:CAN通信最容易翻车的地方

STM32F103的bxCAN有14个过滤器组(互联型有28个),每个过滤器组可以配置为掩码模式或列表模式,可以关联到FIFO0或FIFO1。过滤器的作用是硬件层面筛选报文,只有匹配的报文才会被存入接收FIFO并触发中断,不匹配的直接丢弃,不占用CPU资源。

新手最容易犯的错误是:不配置过滤器,或者配置了但模式选错,导致接收不到任何数据。默认情况下过滤器是关闭的,所有报文都被丢弃。你必须至少配置一个过滤器组并激活它。

我一般用掩码模式,配置一个过滤器组接收所有标准帧:

CAN_FilterTypeDef filter; filter.FilterBank = 0; // 使用过滤器组0 filter.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式 filter.FilterScale = CAN_FILTERSCALE_32BIT; // 32位 filter.FilterIdHigh = 0x0000; // ID高16位 filter.FilterIdLow = 0x0000; // ID低16位 filter.FilterMaskIdHigh = 0x0000; // 掩码高16位,全0表示不关心任何位 filter.FilterMaskIdLow = 0x0000; // 掩码低16位 filter.FilterFIFOAssignment = CAN_RX_FIFO0; // 关联到FIFO0 filter.FilterActivation = ENABLE; // 激活过滤器 filter.SlaveStartFilterBank = 14; // 从机起始过滤器组(互联型才用) HAL_CAN_ConfigFilter(&hcan, &filter);

掩码全0意味着所有位都不关心,也就是接收所有报文。如果你只想接收特定ID的报文,比如只接收ID为0x123的标准帧,可以这样配:

filter.FilterIdHigh = 0x123 << 5; // 标准帧ID左移5位 filter.FilterMaskIdHigh = 0x7FF << 5; // 掩码全1,要求每一位都匹配

这里有个细节:标准帧的11位ID在过滤器寄存器里是存在高16位的高11位,所以要左移5位。这个移位操作很多人会忘,导致过滤器匹配不上。

注意:如果你用的是STM32F103C8T6这类非互联型芯片,只有14个过滤器组(0到13),SlaveStartFilterBank这个参数随便填不影响。但如果你用的是STM32F105/107这类互联型芯片,有28个过滤器组,从机CAN的起始过滤器组要设对,否则过滤器不生效。

3.3 中断配置与FIFO管理

CAN接收中断有两个:FIFO0溢出中断和FIFO0消息挂起中断。我一般用FIFO0消息挂起中断(CAN_IT_RX_FIFO0_MSG_PENDING),每收到一帧报文就触发一次中断,在中断回调里读取报文。

HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);

对应的回调函数是:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); // 处理接收到的数据 }

FIFO0有3个邮箱深度,也就是说最多能缓存3帧未处理的报文。如果中断处理太慢,第4帧来的时候就会触发溢出。所以在中断回调里尽量只做数据拷贝,复杂处理放到主循环里做。

4. 收发代码实战:从发送函数到中断回调的完整链路

4.1 发送函数:邮箱选择与超时处理

CAN发送的核心是找一个空闲的发送邮箱,把报文头和数据填进去,然后请求发送。STM32F103有3个发送邮箱,HAL库提供了HAL_CAN_AddTxMessage函数来简化操作:

uint8_t CAN_SendMsg(uint32_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef txHeader; uint32_t txMailbox; txHeader.StdId = id; // 标准帧ID txHeader.ExtId = 0; // 扩展帧ID不用 txHeader.IDE = CAN_ID_STD; // 标准帧 txHeader.RTR = CAN_RTR_DATA; // 数据帧 txHeader.DLC = len; // 数据长度,0到8 txHeader.TransmitGlobalTime = DISABLE; if (HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailbox) != HAL_OK) { return 1; // 发送失败,没有空闲邮箱 } return 0; }

这个函数是非阻塞的,把报文放进邮箱就返回了,实际发送由CAN控制器硬件完成。如果三个邮箱都满了,函数会返回HAL_ERROR。在实际项目中,我一般会加一个重试机制:如果发送失败,等一小段时间再试,或者用一个软件队列缓存待发送的报文。

发送完成中断(CAN_IT_TX_MAILBOX_EMPTY)可以用来确认报文是否成功发到总线上。如果总线上没有其他节点应答(比如只有一块板子),发送会一直重试直到错误计数超限,最终进入错误被动状态。所以调试的时候一定要确保总线上至少有两个节点。

4.2 接收中断回调:数据解析与FIFO释放

接收回调里要做的事情很明确:读报文头、读数据、释放FIFO。HAL库的HAL_CAN_GetRxMessage函数会自动释放对应的FIFO邮箱,不需要手动操作。

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) != HAL_OK) { return; } // 根据ID做不同处理 switch (rxHeader.StdId) { case 0x100: // 处理控制指令 ProcessControlCmd(rxData, rxHeader.DLC); break; case 0x200: // 处理状态上报 ProcessStatusReport(rxData, rxHeader.DLC); break; default: break; } }

这里有个经验:在中断回调里不要做耗时操作,比如串口打印、Flash写入、长时间循环。这些操作会阻塞中断,导致后续报文丢失。我一般只把数据拷贝到一个环形缓冲区,然后在主循环里慢慢处理。

4.3 双机通信的完整测试流程

两块板子烧录好程序之后,测试流程建议这样走:

  1. 先确认硬件连接正确,CANH接CANH,CANL接CANL,终端电阻两端各一个。
  2. 用示波器或者逻辑分析仪看一下CANH和CANL上有没有波形。如果没有波形,检查TJA1050的供电和STM32的CAN引脚配置。
  3. 一块板子只发不收,另一块只收不发,用调试器看接收FIFO里有没有数据。
  4. 确认能收到数据之后,再改成双机互发互收。
  5. 逐步提高发送频率,观察错误计数器(CAN_ESR寄存器的TEC和REC)的变化。正常情况下TEC和REC应该保持在0附近,如果持续增长说明通信有问题。

我在测试的时候遇到过一个情况:两块板子单独收发都正常,但同时互发的时候就开始丢帧。排查后发现是接收中断优先级设得太低,被其他中断打断了。把CAN接收中断的抢占优先级调到最高之后问题就解决了。

5. 调试排错:CAN通信不上时怎么一步步定位

5.1 从物理层开始排查

CAN通信不上,第一步永远是查物理层。我习惯用万用表先量几个关键点:

  • CANH对地电压:正常应该在2.5V左右
  • CANL对地电压:正常也应该在2.5V左右
  • CANH和CANL之间的阻值:断电测量,应该是60欧姆左右(两个120欧姆并联)

如果CANH和CANL电压都是0V,说明收发器没有工作,检查VCC供电和S引脚电平。如果电压是5V和0V,说明TXD一直处于主导电平,可能是STM32的CAN_TX引脚配置错了,或者程序卡在某个地方一直发送。

如果阻值量出来是120欧姆,说明只有一端有终端电阻,另一端需要补上。如果阻值是无穷大,两端都没有终端电阻。

5.2 用回环模式验证MCU端配置

如果物理层没问题但通信还是不上,可以把CAN配置成回环模式(Loopback)来验证MCU端的配置是否正确。回环模式下,发送的报文不经过外部总线,直接在芯片内部回环到接收端。如果回环模式下能正常收发,说明MCU的CAN外设配置没问题,问题出在外部电路或者对方节点上。

在CubeMX里把CAN的Operating Mode改成Loopback,重新生成代码烧录测试。测试完之后记得改回Normal模式。

5.3 错误状态寄存器解读

STM32的CAN_ESR寄存器里包含了丰富的错误信息:

位段名称含义
LEC[2:0]最后错误代码0=无错误,1=填充错误,2=格式错误,3=ACK错误,4=位隐性错误,5=位显性错误,6=CRC错误,7=软件设置
TEC[7:0]发送错误计数发送错误累计值
REC[7:0]接收错误计数接收错误累计值
BOFF总线关闭标志1表示进入总线关闭状态

如果LEC显示3(ACK错误),说明发送的报文没有任何节点应答。可能的原因:总线上只有一个节点、对方节点没有上电、波特率不匹配、终端电阻缺失。

如果LEC显示6(CRC错误),说明接收到的报文CRC校验失败。可能的原因:波特率不匹配、总线干扰太大、终端电阻缺失、线太长。

如果TEC超过255,CAN控制器会进入总线关闭状态(Bus Off),停止发送和接收。这时候需要软件干预,重新初始化CAN外设才能恢复。

5.4 波特率不匹配的隐蔽表现

波特率不匹配是CAN调试中最隐蔽的问题之一。两块板子波特率差一点点,有时候能收到几帧,有时候一帧都收不到,错误计数器忽高忽低。用示波器看波形,每帧的位时间宽度会有细微差异,但肉眼很难分辨。

我的经验是:只要通信不稳定,先怀疑波特率。用示波器测量一帧报文里单个位的时间宽度,比如500kbps的位宽是2微秒,250kbps是4微秒。如果测量值和预期对不上,就去检查CubeMX里的时钟配置和CAN参数。

还有一个容易忽略的点:STM32F103的APB1时钟默认是36MHz,但如果你改了系统时钟配置,APB1的分频系数变了,CAN的输入时钟也会跟着变。比如系统时钟72MHz,APB1分频系数为2,APB1就是36MHz;如果分频系数改成4,APB1就是18MHz,同样的CAN参数配出来的波特率就减半了。

6. 进阶话题:从双机通信到多节点组网

6.1 多节点通信的ID分配策略

双机通信的时候ID随便设都行,但节点一多,ID分配就需要规划了。CAN标准帧有11位ID,范围是0x000到0x7FF,其中0x000到0x0FF通常保留给高优先级报文。我一般这样分配:

  • 0x100到0x1FF:控制指令,优先级较高
  • 0x200到0x2FF:状态上报,优先级中等
  • 0x300到0x3FF:参数配置,优先级较低
  • 0x400到0x4FF:诊断和调试信息

ID值越小优先级越高,因为CAN仲裁的时候是ID小的先发。所以紧急指令用低ID,普通数据用高ID。

6.2 总线负载率与实时性评估

CAN总线的负载率建议控制在30%到40%以下,超过50%之后延迟会明显增加。负载率的计算公式是:

负载率 = (每帧位数 × 每秒帧数) / 波特率

一帧标准数据帧大约有111位(包括帧起始、仲裁段、控制段、数据段、CRC段、ACK段、帧结束和帧间隔)。假设波特率500kbps,每秒发送1000帧,负载率 = (111 × 1000) / 500000 = 22.2%。

如果负载率太高,可以考虑:提高波特率、减少发送频率、合并报文(把多个信号打包到一帧里)、用扩展帧(扩展帧的位数更多,反而会增加负载,一般不推荐)。

6.3 错误处理与总线恢复

在实际项目中,CAN节点可能会因为干扰、电源波动等原因进入错误被动甚至总线关闭状态。一个健壮的CAN程序应该包含错误检测和自动恢复机制。

我一般会定期检查CAN_ESR寄存器的值,如果发现TEC或REC超过一定阈值(比如128),就主动采取措施:降低发送频率、记录错误日志、必要时重新初始化CAN外设。

总线关闭恢复的流程是:检测到BOFF标志置位后,等待一段时间(比如100ms),然后调用HAL_CAN_Start重新启动CAN外设。有些工程师会直接调用HAL_CAN_Init重新初始化,这样更彻底但耗时更长。

void CAN_ErrorRecovery(void) { uint32_t esr = hcan.Instance->ESR; if (esr & CAN_ESR_BOFF) { HAL_Delay(100); HAL_CAN_Stop(&hcan); HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); } }

这个恢复函数可以放在主循环里定期调用,或者放在错误中断里触发。

7. 我在这个项目里踩过的几个真实坑

第一个坑是过滤器没配。刚开始调的时候,发送正常但接收一直没反应,查了半天以为是硬件问题,后来才发现CubeMX里默认没有激活任何过滤器,所有报文都被硬件丢弃了。这个坑很隐蔽,因为发送端看起来一切正常,错误计数器也不涨,就是收不到数据。

第二个坑是终端电阻。我用的是两块现成的TJA1050模块,以为模块上自带了终端电阻,结果量了一下发现都没有。两块板子之间就靠杜邦线连着,短帧勉强能通,长帧必丢。后来各焊了一个120欧姆电阻上去,通信立刻稳定了。

第三个坑是中断优先级。CAN接收中断的优先级设得比串口中断低,结果串口打印的时候CAN中断被延迟,FIFO溢出丢帧。把CAN接收中断的抢占优先级调到最高之后问题解决。

第四个坑是波特率计算。我一开始用CubeMX默认的参数,没注意APB1时钟是36MHz,算出来的波特率是250kbps而不是我想要的500kbps。两块板子都烧的同样程序,所以能通,但和另一块用500kbps的板子就通不上。后来统一了波特率配置才解决。

这些坑说到底都是细节问题,但每一个都能让你卡上半天。CAN通信的调试就是这样,物理层、配置层、代码层任何一层出问题都会导致通信失败,而且现象往往很相似。我的建议是:按照物理层→回环模式→单机收发→双机收发的顺序逐步排查,每一步都确认无误再往下走,不要跳步。

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

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

立即咨询