1. 为什么还要折腾CAN总线
前阵子帮一个做工业控制的朋友调一块板子,需求很朴素:两块STM32F103的板子之间要互传数据,一块负责采集传感器状态,另一块负责驱动执行机构,中间隔着两三米,车间里变频器、继电器一堆,电磁环境相当糟糕。一开始图省事上了UART,结果跑起来之后数据时不时就飞了,加校验、加重传、加超时,代码越堆越厚,稳定性还是看运气。后来换成I2C,距离一长直接趴窝,总线电容一上来波形就烂得没法看。折腾了两天,最后还是老老实实回到CAN总线上来。
CAN这个东西,在汽车电子和工业控制里属于老江湖了。它天生就是为多节点、强干扰、长距离的场景设计的,差分信号传输、硬件级仲裁、自动重传、错误检测,这些机制不是软件补丁能比的。STM32F103这颗芯片,基本上算是国内嵌入式圈子里最普及的型号之一,它自带的bxCAN外设功能相当完整,配合一颗TJA1050收发器,就能搭出一套成本极低但相当可靠的CAN通信链路。
这篇内容我打算把双机CAN通信从硬件到软件完整走一遍,包括TJA1050的接线细节、终端电阻怎么算、STM32的CAN外设怎么配、过滤器怎么设、收发代码怎么写、出问题了怎么排查。适合手上有两块F103板子、想跑通CAN通信但被各种细节卡住的人,也适合之前只用过串口、想了解一下CAN到底怎么回事的朋友。我会尽量把每个参数背后的原因讲清楚,而不是甩一堆代码让你自己猜。
2. 硬件层:TJA1050与CAN物理层拆解
2.1 CAN总线的差分信号到底是怎么回事
很多人第一次接触CAN,看到CAN_H和CAN_L两根线就懵了,不知道这俩线到底在传什么。其实核心就一句话:CAN传的是两根线之间的电压差,不是某根线对地的电压。这种差分传输的好处在于,外界干扰通常是同时耦合到两根线上的(共模干扰),而接收端只看差值,共模部分就被抵消掉了。这就是为什么CAN在车间那种恶劣环境下还能稳住,而单端的UART早就扛不住了。
具体到电平,CAN总线有两个状态:显性(Dominant)和隐性(Recessive)。显性状态下,CAN_H大约3.5V,CAN_L大约1.5V,差值约2V;隐性状态下,两根线都大约2.5V,差值接近0V。注意这里说的是典型值,实际会随供电和收发器不同略有浮动。显性用逻辑0表示,隐性用逻辑1表示。这里有个关键点:显性会覆盖隐性,也就是说如果总线上有一个节点发显性、另一个发隐性,最终总线呈现显性。这个特性正是CAN仲裁机制的物理基础,后面讲仲裁的时候会用到。
TJA1050这颗收发器的作用,就是把STM32的CAN_TX和CAN_RX这两个单端逻辑信号,转换成总线上的差分信号,反过来也一样。它内部有个差分驱动器和差分接收器,供电5V,和3.3V的STM32连接时,TX/RX引脚电平是兼容的,这点可以放心。TJA1050还有个Rs引脚(第8脚),用来控制斜率,接地就是高速模式,接电阻可以降低斜率减少EMI,一般跑500kbps以下、线又不长的话,直接接地就行。
2.2 TJA1050接线与终端电阻的坑
接线这块,我见过太多人栽在终端电阻上。先把标准接法说清楚:TJA1050的TXD接STM32的CAN_TX(一般是PA12或PB9,取决于重映射),RXD接CAN_RX(PA11或PB8),VCC接5V,GND共地。CAN_H和CAN_L分别接到总线上。然后重点来了——终端电阻。
CAN总线两端各需要一个120欧姆的终端电阻,两个加起来并联后是60欧姆,这是总线特性阻抗的匹配值。为什么需要它?因为信号在电缆末端如果阻抗不匹配,会反射回来,和原信号叠加,导致波形畸变、误码。120欧姆不是随便定的,它对应双绞线的特性阻抗。我实测过,如果两个终端电阻都不接,短距离低速下可能还能凑合跑,但一旦速率上到250kbps以上或者线长超过几米,误码率立刻飙升。如果只接一个,波形也会不对称。
注意:终端电阻必须接在总线的两个物理末端,不是每个节点都接。两个节点的情况下,就是每块板子各接一个。三个以上节点时,只有最远的两端接,中间节点不接。我见过有人在每个节点都焊了120欧姆,结果总线上挂了四五个电阻,并联后阻抗低得离谱,收发器直接驱动不动,通信全挂。
还有个细节,TJA1050的CAN_H和CAN_L之间,有些模块会自带一个120欧姆的贴片电阻,用之前一定要确认。如果模块自带,你外面再并一个,就变成60欧姆了,也是错的。判断方法很简单,断电状态下用万用表量CAN_H和CAN_L之间的电阻,如果是120欧姆左右,说明这个节点带了终端电阻;如果是无穷大或者很大,说明没带。
2.3 供电与隔离的取舍
TJA1050是5V供电,STM32F103一般是3.3V。如果你的板子上只有3.3V,那就需要额外一路5V。这里有个常见误区:有人觉得TJA1050的TXD/RXD是5V电平,直接接3.3V的STM32会不会烧。实际上TJA1050的TXD是输入脚,内部有上拉,STM32推挽输出3.3V高电平,TJA1050识别为高电平是没问题的,因为它的输入高电平阈值典型值在2V左右。RXD是输出脚,TJA1050输出高电平时接近5V,这个直接接STM32的CAN_RX引脚,理论上超过了3.3V,但STM32的很多IO是5V容忍的(具体看数据手册,PA11/PA12是5V容忍的),所以一般直连也能用。不过如果你追求稳妥,可以在RXD上加一个电平转换或者分压,但实测下来大多数情况直连没问题。
至于隔离,车间环境如果地电位差比较大,或者有强共模干扰,建议上隔离型收发器或者加光耦/数字隔离器。但那是另一个话题了,先用TJA1050把基本链路跑通再说。
3. STM32 bxCAN外设配置全流程
3.1 时钟、GPIO与CAN外设初始化
STM32F103的CAN外设挂在APB1总线上,时钟使能别忘了。GPIO这边,CAN_TX和CAN_RX要配成复用推挽输出和上拉输入(或者复用输入),具体取决于你用的是默认引脚还是重映射。默认是PA11(CAN_RX)和PA12(CAN_TX),重映射后是PB8和PB9。重映射需要开AFIO时钟并调用重映射函数。
初始化顺序我习惯这样:先开GPIO和AFIO时钟,配好引脚;再开CAN时钟;然后配置CAN的位时序;接着配置过滤器;最后使能CAN。这个顺序不是死规定,但先配引脚再开外设比较符合直觉,也避免外设使能后引脚还没准备好的尴尬。
位时序这块是重点,也是最容易配错的地方。CAN的位时间由三段组成:同步段(SYNC_SEG,固定1个Tq)、时间段1(BS1)、时间段2(BS2)。采样点位于BS1结束、BS2开始的位置。Tq是时间份额,由APB1时钟分频得到。STM32F103的APB1一般是36MHz(系统72MHz时)。假设我们要跑500kbps,位时间就是2微秒。如果分频系数设为4,那Tq就是4/36MHz约等于111纳秒,一个位时间需要2us/111ns约等于18个Tq。这18个Tq要分给SYNC_SEG(1)、BS1、BS2。采样点一般建议在75%到87.5%之间。取BS1=14,BS2=3,加上SYNC_SEG=1,总共18,采样点在(1+14)/18约等于83.3%,比较合理。
用代码表示就是:
CAN_InitTypeDef CAN_InitStructure; 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; // 发送优先级由标识符决定 CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_14tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq; CAN_InitStructure.CAN_Prescaler = 4; CAN_Init(CAN1, &CAN_InitStructure);这里几个参数值得说一下。ABOM自动离线管理建议开,因为CAN控制器在错误累积到一定程度会进入离线状态,开了ABOM后它会自动恢复,不然你得手动干预。NART自动重传建议关(也就是允许重传),这样发送失败会自动重发,提高可靠性。TXFP这个参数决定发送优先级是按标识符还是按请求顺序,一般用标识符优先级,因为CAN本身的仲裁就是按标识符来的,保持一致。
3.2 过滤器配置:别让无关报文打断你的CPU
过滤器是很多人忽略但极其重要的一环。CAN总线上可能跑着各种ID的报文,如果你不过滤,每个报文都触发中断,CPU会被大量无关中断拖垮。STM32F103有14个过滤器组,可以配置成屏蔽位模式或标识符列表模式,还可以指定关联到FIFO0还是FIFO1。
双机通信的场景下,假设A机发ID为0x123的报文,B机发ID为0x456的报文。那A机的过滤器应该只接收0x456,B机只接收0x123。配置成32位屏蔽位模式,屏蔽寄存器全1表示所有位都要匹配,这样只有完全相同的ID才能通过。
CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x456 << 5; CAN_FilterInitStructure.CAN_FilterIdLow = 0; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0xFFFF; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0xFFFF; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure);注意那个左移5位,因为标准帧的ID是11位,在32位过滤器里是存在高16位的高11位,所以要左移5位对齐。这个坑我踩过,当时ID怎么都对不上,查了半天才发现是移位问题。如果你用的是扩展帧,29位ID的存放方式又不一样,需要仔细看参考手册。
提示:过滤器组0在有些情况下会被默认占用,如果发现过滤器不生效,可以换一个过滤器组试试。另外,CAN_FilterInit函数内部会进入初始化模式,配置完再退出,这个不用手动管。
3.3 发送与接收:中断还是轮询
发送这块,STM32提供了三个发送邮箱。发送时选择一个空邮箱,填入标识符、数据长度、数据内容,然后请求发送。可以轮询等待发送完成,也可以用中断。双机通信数据量不大的话,轮询其实够用,但中断更优雅,不会阻塞主循环。
uint8_t CAN_SendMsg(uint32_t id, uint8_t* data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId = id; TxMessage.ExtId = 0; 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(CAN1, &TxMessage); if(mailbox == CAN_TxStatus_NoMailBox) return 1; // 没有空邮箱 return 0; }接收这边,我强烈建议用中断。CAN接收中断在FIFO0或FIFO1有报文时触发,在中断里把报文读出来放到缓冲区,主循环再处理。这样不会丢报文,也不会阻塞。
void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 处理RxMessage.StdId和RxMessage.Data // 记得清中断标志 }中断服务函数的名字要注意,FIFO0的中断是USB_LP_CAN1_RX0_IRQHandler,FIFO1是CAN1_RX1_IRQHandler,别搞混了。还有,接收中断标志在CAN_Receive里会自动清除,但如果你用的是FIFO溢出中断之类的,要单独处理。
4. 双机通信实战:从点亮LED到数据互传
4.1 硬件搭建与上电前检查
两块板子,各自接一个TJA1050模块,CAN_H接CAN_H,CAN_L接CAN_L,GND最好也连起来(如果两边供电独立的话,共地能减少共模干扰)。终端电阻各接一个,或者确认模块自带。上电之前,用万用表量一下CAN_H和CAN_L之间的电阻,应该是60欧姆左右(两个120并联)。如果量出来是120,说明只接了一个;如果是30左右,说明接了四个;如果是无穷大,说明一个都没接。这一步花不了一分钟,但能省掉后面几小时的抓瞎。
供电方面,两块板子可以各自独立供电,但一定要共地。我试过两边用不同的电源适配器,地没连,结果通信时好时坏,后来把地连上就稳了。如果实在不方便共地,那就得考虑隔离方案了。
4.2 发送端代码结构与调试技巧
发送端的逻辑很简单:按键按下或者定时器触发,就往总线上发一帧数据。我一般会先写一个最简单的测试:上电后每隔500毫秒发一帧固定数据,用逻辑分析仪或者CAN分析仪抓一下,确认波形和ID都对。这一步能排除掉大部分硬件和配置问题。
调试的时候,如果没有CAN分析仪,可以用另一块板子做接收端,在接收中断里翻转一个LED。LED闪烁就说明收到了,不闪就说明链路有问题。这个方法虽然土,但非常有效。我手边常备一个USB-CAN的小工具,几十块钱,能直接看到总线上的报文,调试效率高很多。没有的话,两块板子互相对发也能定位问题。
发送端还有一个细节:如果发送失败(比如总线没有其他节点应答),CAN控制器会一直重传,直到错误计数超限进入错误被动甚至离线状态。所以如果你只接了一块板子,没有接收方,发送会失败并可能进入错误状态。这时候ABOM的作用就体现出来了,它会自动恢复。但最好的做法还是确保总线上至少有两个正常节点。
4.3 接收端过滤器与中断处理
接收端的过滤器配置前面讲过了,这里补充一个实际调试中的经验:如果你发现接收中断进不去,先检查过滤器是否使能、FIFO分配是否正确、中断是否使能。这三步任何一步漏了,中断都不会来。我习惯在初始化完成后,打印一下CAN寄存器的状态,确认CAN_MSR、CAN_TSR这些寄存器的值符合预期。
中断处理函数里,尽量只做数据搬运,不要做耗时操作。比如收到数据后,把数据存到一个环形缓冲区,置一个标志位,主循环看到标志位再处理。如果在中断里直接做复杂运算或者打印,容易导致后续报文丢失。
#define RX_BUF_SIZE 16 typedef struct { uint32_t id; uint8_t data[8]; uint8_t len; } CanMsg_t; CanMsg_t rxBuf[RX_BUF_SIZE]; volatile uint8_t rxHead = 0; volatile uint8_t rxTail = 0; void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); uint8_t next = (rxHead + 1) % RX_BUF_SIZE; if(next != rxTail) { // 缓冲区未满 rxBuf[rxHead].id = RxMessage.StdId; rxBuf[rxHead].len = RxMessage.DLC; for(uint8_t i = 0; i < RxMessage.DLC; i++) rxBuf[rxHead].data[i] = RxMessage.Data[i]; rxHead = next; } // 如果满了就丢弃,或者置溢出标志 }这个环形缓冲区的写法很基础,但足够用。注意rxHead和rxTail的volatile修饰,因为它们在中断和主循环里都被访问。
4.4 通信协议设计:别只发裸数据
双机通信如果只是发几个字节,那随便怎么发都行。但一旦数据种类多了,就需要一个简单的协议。我一般会在数据区的前两个字节放一个命令字或者数据类型,后面跟实际数据。比如0x01表示传感器数据,0x02表示控制命令,0x03表示心跳。这样接收端根据命令字来解析,扩展起来方便。
数据长度方面,CAN一帧最多8字节,如果数据超过8字节,需要分包。分包的话,可以在第一个字节放一个序号或者帧类型,接收端组包。不过双机通信数据量一般不大,8字节通常够用。如果确实要传长数据,建议用ISO-TP或者自己写一个简单的分包重组逻辑。
注意:CAN的标准帧ID是11位,范围0到0x7FF。扩展帧是29位。双机通信一般用标准帧就够了,ID分配上尽量避开0x000和0x7FF这些特殊ID,虽然标准里没有严格禁止,但有些工具和驱动会特殊处理。
5. 常见问题排查与避坑经验
5.1 通信完全不通的排查顺序
遇到CAN不通,别急着改代码,按这个顺序查:第一,量终端电阻,确认是60欧姆左右;第二,量CAN_H和CAN_L的静态电压,隐性状态下都应该是2.5V左右,如果差很多,检查收发器供电和接线;第三,用示波器或者逻辑分析仪看CAN_TX引脚有没有波形,如果没有,说明STM32根本没发出来,检查CAN初始化是否成功、发送邮箱是否可用;第四,看CAN_RX引脚有没有波形,如果没有,说明收发器没收到总线上的信号,检查TJA1050的TXD/RXD是否接反。
我遇到过最坑的一次是TXD和RXD接反了,因为模块上的丝印和实际芯片引脚对不上。后来养成习惯,接线前先看芯片数据手册的引脚定义,不信模块丝印。还有一次是两块板子的CAN_H和CAN_L接反了,CAN_H接了对方的CAN_L,这种差分线接反,通信肯定不通,但静态电压量起来又差不多,很容易忽略。
5.2 能收到但偶尔丢帧怎么办
偶尔丢帧通常有几个原因:一是总线负载太高,报文太密集,接收FIFO溢出。STM32的CAN只有两个接收FIFO,每个3级深度,如果短时间内大量报文涌入,来不及读就会溢出。解决办法是提高中断优先级,或者在中断里尽快把数据搬走。二是波特率不匹配,虽然能收到一些帧,但采样点偏差导致部分帧错误。这时候要检查两端的位时序配置是否完全一致,包括分频、BS1、BS2、SJW。三是终端电阻问题,前面说过了。
还有一种情况是发送端重传导致的。如果接收端偶尔因为处理不过来没有应答,发送端会重传,重传的帧和原帧ID相同,接收端如果没做去重,就会收到重复数据。可以在协议里加一个序号来去重。
5.3 错误状态与总线关闭的恢复
CAN控制器有错误计数器,发送错误和接收错误分别计数。当发送错误计数超过255时,控制器进入总线关闭状态,这时候它不再参与总线通信。如果开了ABOM,它会自动检测总线空闲并恢复。如果没开,就需要手动清除CAN_MCR的INRQ位重新初始化。
我建议在代码里加一个错误检测,定期读CAN_ESR寄存器,如果发现错误计数在增长,就打印出来。这样能在问题恶化之前发现苗头。比如错误计数缓慢增长,可能是终端电阻不匹配或者线太长;突然暴增,可能是接线松动或者强干扰。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无通信 | 终端电阻缺失、TXD/RXD接反、CAN_H/L接反 | 量电阻、查接线、看波形 |
| 偶尔丢帧 | FIFO溢出、波特率偏差、干扰 | 提高中断优先级、核对位时序、加屏蔽 |
| 错误计数增长 | 终端电阻不匹配、线太长、干扰 | 量电阻、缩短线长、加共模电感 |
| 总线关闭 | 错误累积超限、无其他节点应答 | 开ABOM、确保至少两个节点 |
| 收到重复帧 | 发送端重传、接收端未去重 | 协议加序号、检查应答 |
5.4 几个容易被忽略的细节
第一个是CAN_TX和CAN_RX的GPIO模式。CAN_TX要配成复用推挽输出,CAN_RX要配成上拉输入或者浮空输入。我见过有人把CAN_RX配成推挽输出,结果收发器输出和STM32输出打架,通信时好时坏。第二个是CAN外设的时钟,APB1时钟如果没使能,CAN初始化会失败,但有些库函数不会报错,只是后续操作无效。第三个是过滤器的激活,配置完过滤器一定要调用CAN_FilterInit并确保CAN_FilterActivation是ENABLE,否则过滤器不生效,所有报文都会进来。
还有一点,如果你用的是HAL库而不是标准库,配置方式会有所不同,但原理是一样的。HAL库的CAN配置结构体字段名不一样,但位时序的计算逻辑完全相同。我建议不管用哪个库,都要理解位时序的计算,因为这是CAN通信的根基,配错了后面全白搭。
6. 一些实测数据与个人体会
我在两块F103板子上跑500kbps,线长大概3米,普通双绞线,终端电阻各120欧姆,连续跑了一个下午,大概发了几十万帧,丢帧率为零。后来把线换成普通的杜邦线,非双绞,同样条件,丢帧率明显上升,偶尔会出现错误帧。所以线材这块,能用双绞就用双绞,哪怕是网线里拆出来的一对,也比杜邦线强很多。
波特率方面,500kbps在3米距离下很稳,1Mbps在同样距离下也能跑,但对线材和终端电阻更敏感。如果距离超过10米,建议降到250kbps或125kbps。CAN的速率和距离是反比关系,这个没有捷径,物理规律摆在那里。
TJA1050这颗收发器,便宜、好用、资料多,但它的ESD防护和共模抑制能力一般。如果应用环境特别恶劣,可以考虑带隔离的收发器,或者在外面加TVS和共模电感。不过对于大多数实验室和一般工业场景,TJA1050完全够用。
最后说一个调试习惯:每次改完CAN配置,先用回环模式(Loopback)自测一下,确认发送和接收逻辑没问题,再切到正常模式接总线。回环模式下,STM32自己发自己收,不需要外部收发器和总线,能快速验证软件配置。这个习惯帮我省了很多时间,尤其是过滤器配置这种容易出错的地方。
回环模式的配置很简单,把CAN_Mode改成CAN_Mode_LoopBack就行,其他不变。自测通过后再改回CAN_Mode_Normal。注意回环模式下TJA1050不参与,所以硬件问题测不出来,但软件问题基本都能暴露。