资讯动态

STM32 CAN总线实战:从差分信号到bxCAN过滤器配置

发布时间:2026/10/21 1:08:08 来源:尧图企业网站定制
1. 为什么CAN总线在嵌入式领域始终占有一席之地如果你拆开过任何一辆近十年生产的汽车或者接触过工业自动化产线上的PLC控制柜大概率会看到两根绞在一起、末端各带一个120欧姆电阻的线——那就是CAN总线。很多刚入行的朋友第一次接触CAN往往是因为项目里要对接某个电机驱动器、BMS电池管理模块或者车载OBD接口翻遍手头的教程发现全是显性电平隐性电平位填充这类术语看完更懵了。我自己第一次用CAN是在一个多轴运动控制项目上当时用STM32F103对接三个伺服驱动器一开始想当然地按UART的思路去接TX/RX结果折腾了一下午一个字节都没收到。后来才明白CAN的收发器把TX/RX转换成了差分信号物理层逻辑和串口完全不是一回事。这篇文章就是想把当年踩过的坑、绕过的弯连同CAN从物理层到STM32固件配置的完整链路一次性讲清楚。CAN全称Controller Area Network中文叫控制器局域网最早是上世纪八十年代由博世公司为汽车电子设计的一种多主总线协议。它的核心价值在于多节点共享一条总线、无需主机轮询、自带优先级仲裁和错误检测。这三点决定了它在电磁环境复杂、节点数量多、实时性要求高的场景里比RS485和UART都更合适。STM32几乎全系列都集成了bxCAN外设Basic Extended CAN配合一颗几块钱的收发器芯片就能跑起来这也是它成为嵌入式工程师必修课的原因。这篇文章适合三类人看一是刚学完STM32基础外设、想进阶到总线通信的初学者二是从UART/I2C转过来、被CAN的仲裁机制绕晕的开发者三是需要在项目里快速搭起CAN通信、不想啃几百页参考手册的实战派。我会从物理层的硬件连接讲起一路走到STM32的过滤器配置和收发代码中间穿插示波器实测波形和常见故障排查尽量让每个环节都能直接抄作业。2. 差分信号、终端电阻与CAN收发器的硬件真相2.1 为什么CAN用两根线而不是一根CAN总线物理层用的是差分信号CAN_H和CAN_L两根线。所谓差分就是接收端判断的是两根线之间的电压差而不是某根线对地的绝对电压。这样做的好处非常直接外界电磁干扰通常会同时耦合到两根线上形成共模干扰而差分接收端只关心差值共模部分被自然抵消掉了。汽车里点火线圈、电机、继电器到处都在制造噪声单端信号早就被淹没了差分是唯一靠谱的选择。具体电平定义是这样的当总线处于隐性状态逻辑1时CAN_H和CAN_L都大约在2.5V压差接近0V当处于显性状态逻辑0时CAN_H被拉到约3.5VCAN_L被拉到约1.5V压差约2V。注意这里有个反直觉的点显性电平对应逻辑0隐性对应逻辑1。这和UART里低电平起始位有点像但CAN的仲裁机制正是建立在显性覆盖隐性这个物理特性上的——只要有一个节点发显性位总线就是显性这个特性后面讲仲裁时会反复用到。提示如果你用万用表量CAN_H和CAN_L对地电压隐性时都应该是2.5V左右。如果某一根明显偏离比如CAN_H只有1V那基本可以判断收发器损坏或者总线短路。2.2 终端电阻不是可选项是必须项CAN总线两端各需要一个120欧姆的终端电阻这个电阻的作用是消除信号反射。信号在电缆中传播时如果遇到阻抗不连续的地方比如线缆末端开路能量会反射回来和原始信号叠加造成波形畸变甚至误码。120欧姆这个值不是随便定的它约等于双绞线的特性阻抗匹配之后反射最小。实际接线时两个120欧姆电阻分别接在总线最远的两端并联在CAN_H和CAN_L之间。如果你用万用表在断电状态下量CAN_H和CAN_L之间的电阻正常应该读到约60欧姆两个120欧姆并联。如果读到120欧姆说明只有一端接了电阻如果读到无穷大说明两端都没接如果读到接近0说明短路了。这个量60欧姆的技巧是我调试时最常用的第一步能快速判断总线物理层是否正常。很多新手会问短距离通信能不能省掉终端电阻我的经验是哪怕只有几十厘米的线只要通信速率上到500kbps以上省掉电阻后示波器上就能看到明显的振铃。低速长距离场景下影响小一些但既然电阻成本几乎为零没有理由省。2.3 收发器芯片怎么选、怎么接STM32的bxCAN外设输出的是TTL电平的CAN_TX和CAN_RX信号不能直接挂到总线上中间必须经过收发器芯片。常见的收发器有TJA1050、SN65HVD230、MCP2551等。TJA1050是5V供电的经典款SN65HVD230是3.3V供电和STM32电平更匹配我个人更推荐后者省掉电平转换的麻烦。接线逻辑很简单STM32的CAN_TX接收发器的TXDCAN_RX接收发器的RXD收发器的CAN_H和CAN_L接到总线。这里有个容易翻车的点CAN_TX和CAN_RX不能接反接反了收发器根本不工作而且不会报错现象就是发不出去也收不到。另外收发器的供电和地一定要和STM32共地否则差分信号没有参考通信必然失败。收发器型号供电电压最高速率特点TJA10505V1Mbps经典稳定需注意电平匹配SN65HVD2303.3V1Mbps与STM32直连推荐MCP25515V1Mbps老牌芯片货源广TJA10425V5Mbps支持CAN FD高速场景选型时还要注意一点如果项目对EMC要求高优先选带待机模式和斜率控制的收发器比如TJA1042它可以通过STB引脚进入低功耗通过SPLIT引脚稳定共模电压。这些功能在实验室里看不出差别但到了车载或工业现场就是能不能过认证的关键。3. 从帧结构到仲裁CAN协议里最容易被误解的机制3.1 一帧CAN报文到底长什么样标准CAN数据帧CAN 2.0A的结构可以拆成这么几段帧起始SOF、仲裁段11位ID RTR、控制段IDE 保留位 4位DLC、数据段0到8字节、CRC段、ACK段、帧结束。扩展帧CAN 2.0B把ID扩展到29位多了一个SRR和IDE位来区分。初学者最容易搞混的是DLC和数据长度的关系。DLC是4位取值0到8表示数据段字节数。理论上DLC可以填9到15但标准规定数据段最多8字节填超过8的值行为未定义实际项目中千万别这么干。另外RTR位区分数据帧和远程帧远程帧没有数据段用来请求其他节点发送数据但现在用得越来越少大多数项目直接用数据帧。我建议新手先用CAN分析仪抓一帧真实报文对着位定义逐段看一遍比看十页手册都管用。比如一帧标准数据帧ID是0x123数据是01 02 03 04抓出来大概是这个结构SOF | ID(11bit) | RTR | IDE | r0 | DLC(4bit) | Data(0-8B) | CRC | ACK | EOF 0 | 0x123 | 0 | 0 | 0 | 4 | 01 02 03 04| ... | ... | ...3.2 仲裁机制为什么ID越小优先级越高CAN最精妙的设计就是非破坏性仲裁。总线上多个节点同时开始发送时它们先发SOF然后逐位发ID。每个节点在发每一位的同时也在监听总线如果自己发的是隐性1但监听到显性0说明有更高优先级的节点在发这个节点立刻退出仲裁转为接收状态而且已经发出的位不会丢失。因为显性覆盖隐性所以ID的二进制值越小显性位出现得越早优先级越高。比如ID 0x100二进制1 0000 0000和ID 0x20010 0000 0000同时发送第几位就能分出胜负0x100的第9位是0显性0x200的第9位是1隐性所以0x100赢。这就是为什么紧急报文比如刹车信号通常分配最小的ID。这里有个实战经验规划ID时一定要留出优先级梯度。我见过一个项目把所有节点ID随便分配结果某个高频发送的传感器ID特别小把总线上其他关键报文全堵住了。正确的做法是按实时性要求分层比如0x000到0x0FF给安全相关0x100到0x1FF给控制指令0x200以上给状态上报。3.3 位定时与采样点波特率不是随便设的CAN没有时钟线所有节点靠位定时同步。一个位时间被分成四个段同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段1PHASE_SEG1、相位缓冲段2PHASE_SEG2。采样点位于PHASE_SEG1结束处通常建议设在位时间的75%到87.5%之间。以STM32为例波特率计算公式是BaudRate Fpclk1 / (Prescaler * (1 BS1 BS2))其中BS1对应PHASE_SEG1BS2对应PHASE_SEG2。假设APB1时钟是36MHz想跑500kbps可以取Prescaler4BS115BS22那么36MHz / (4 * (1 15 2)) 36M / 72 500kbps采样点位置 (1 BS1) / (1 BS1 BS2) 16 / 18 ≈ 88.9%稍微偏高但可接受。如果想让采样点落在87.5%可以调成BS114BS23此时(114)/(1143)15/18≈83.3%。实际调试时如果通信距离长、线缆质量差采样点适当提前一点比如80%容错性更好。注意总线上所有节点的波特率和采样点必须一致否则会出现位错误。我遇到过两个节点一个设87.5%一个设75%短距离能通线一长就疯狂报错查了半天才发现是采样点不匹配。4. STM32 bxCAN外设的初始化与过滤器配置实战4.1 时钟、GPIO和CAN外设的初始化顺序STM32的bxCAN挂在APB1总线上初始化顺序不能乱。我的习惯是先开GPIO和CAN时钟再配GPIO复用然后复位CAN外设最后配CAN参数。以STM32F103为例CAN_TX用PA12CAN_RX用PA11这两个脚要配成复用推挽输出和上拉输入。// 1. 开时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 2. 配GPIO GPIO_InitTypeDef gpio; gpio.GPIO_Pin GPIO_Pin_12; // CAN_TX gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_11; // CAN_RX gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio); // 3. 复位CAN CAN_DeInit(CAN1); // 4. 配CAN参数 CAN_InitTypeDef can; CAN_StructInit(can); can.CAN_TTCM DISABLE; can.CAN_ABOM ENABLE; // 自动离线恢复 can.CAN_AWUM ENABLE; // 自动唤醒 can.CAN_NART DISABLE; // 自动重传 can.CAN_RFLM DISABLE; // 接收FIFO不锁定 can.CAN_TXFP DISABLE; // 发送优先级由ID决定 can.CAN_Mode CAN_Mode_Normal; can.CAN_SJW CAN_SJW_1tq; can.CAN_BS1 CAN_BS1_15tq; can.CAN_BS2 CAN_BS2_2tq; can.CAN_Prescaler 4; CAN_Init(CAN1, can);这里几个参数值得展开说。**ABOMAuto Bus-Off Management**建议打开当CAN进入总线关闭状态错误计数超过255时能自动恢复否则需要手动干预。**NARTNo Automatic Retransmission**建议关闭也就是允许自动重传这样丢帧概率低但在总线负载极高时可能加剧拥堵需要根据场景权衡。TXFP决定发送优先级是按ID还是按请求顺序如果项目里有紧急报文建议关闭TXFP让ID小的先发。4.2 过滤器CAN最绕但最重要的部分STM32的CAN过滤器是新手最大的拦路虎。它一共有14个过滤器组互联型有28个每个组可以配成掩码模式或列表模式可以关联到FIFO0或FIFO1。过滤器的本质是对收到的报文ID做匹配匹配通过才存入FIFO否则直接丢弃。掩码模式的逻辑是(收到的ID 掩码) (过滤ID 掩码)。比如你想接收ID范围0x100到0x1FF的所有标准帧可以设过滤ID0x100掩码0x700这样只要高3位是001的ID都能通过。列表模式则是精确匹配过滤ID里列出的值必须完全相等才通过。CAN_FilterInitTypeDef filter; filter.CAN_FilterNumber 0; filter.CAN_FilterMode CAN_FilterMode_IdMask; filter.CAN_FilterScale CAN_FilterScale_32bit; filter.CAN_FilterIdHigh 0x100 5; // 标准ID左移5位 filter.CAN_FilterIdLow 0x0000; filter.CAN_FilterMaskIdHigh 0x700 5; filter.CAN_FilterMaskIdLow 0x0000; filter.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; filter.CAN_FilterActivation ENABLE; CAN_FilterInit(filter);这里有个巨坑标准ID在过滤器寄存器里要左移5位因为寄存器的高11位才是ID低5位是RTR、IDE等标志。我当年就是忘了移位结果过滤器死活不通过抓包能看到总线有数据但FIFO里就是没有。扩展ID的移位更复杂要拆成高低两段建议直接参考手册里的位域图。另一个经验是过滤器数量有限要省着用。如果项目里要接收的ID很多且不连续优先用掩码模式圈定范围再在软件里二次过滤。实在不够用可以把FIFO设成不锁定模式RFLM1新报文覆盖旧报文避免溢出。4.3 发送与接收的中断处理发送比较简单填好报文结构体选一个空的发送邮箱调用CAN_Transmit即可。STM32有三个发送邮箱如果都满了会返回CAN_TxStatus_NoMailBox这时候要么等要么丢弃。我一般会在发送前检查邮箱状态或者用发送中断来驱动。CanTxMsg tx; tx.StdId 0x123; tx.ExtId 0; tx.IDE CAN_Id_Standard; tx.RTR CAN_RTR_Data; tx.DLC 4; tx.Data[0] 0x01; tx.Data[1] 0x02; tx.Data[2] 0x03; tx.Data[3] 0x04; uint8_t mailbox CAN_Transmit(CAN1, tx); if (mailbox CAN_TxStatus_NoMailBox) { // 邮箱满处理 }接收推荐用中断。FIFO0和FIFO1各有自己的中断向量在中断里调用CAN_Receive把报文取出来然后清中断标志。注意必须把FIFO里的报文全部取完再退出中断否则残留的报文会反复触发中断。void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg rx; while (CAN_MessagePending(CAN1, CAN_FIFO0) 0) { CAN_Receive(CAN1, CAN_FIFO0, rx); // 处理rx.StdId和rx.Data } CAN_FIFORelease(CAN1, CAN_FIFO0); }实测下来如果总线负载率超过70%接收中断会非常频繁这时候要考虑用DMA或者提高中断优先级。我做过一个项目总线负载85%用中断接收时主循环几乎跑不动后来改成DMA搬运才缓解。5. 调试现场那些手册上不会写的CAN故障排查5.1 通信完全不通时的排查链路遇到CAN不通我一般按这个顺序查先量电阻再看波形最后查配置。量电阻就是前面说的断电测CAN_H和CAN_L之间是不是60欧姆。如果不对先解决终端电阻问题。电阻正常的话上电用示波器看CAN_H和CAN_L正常应该有明显的差分波形隐性时两条线都在2.5V附近。如果波形正常但通信不通大概率是配置问题。重点查三处波特率是否一致、过滤器是否放行、收发器TX/RX是否接反。我遇到过最隐蔽的一次是收发器的使能引脚没拉高芯片一直处于待机模式波形看着有但就是不通查了两天才发现。还有一种情况是只有一个节点时自发自收失败。CAN需要至少两个节点才能完成ACK应答单节点发送时因为没有其他节点拉低ACK位会一直重传并最终进入错误状态。测试时要么接两个节点要么把模式设成环回模式Loopback自测。5.2 偶发丢帧和错误帧的定位思路偶发丢帧比完全不通更难查因为它时好时坏。我的经验是先看错误计数器。STM32的CAN外设有发送错误计数TEC和接收错误计数REC可以通过CAN_GetLastErrorCode和CAN_GetErrorCounters读取。如果TEC持续增长说明发送端有问题REC增长则是接收端。常见原因有几个终端电阻只接了一端短距离能通但长距离反射严重波特率有微小偏差两个节点晶振精度不够累积误差导致采样错误线缆太长或分支太多CAN总线的分支长度建议不超过30厘米主干长度和波特率成反比500kbps下主干不超过100米。现象可能原因排查方法完全无波形收发器未供电/使能量收发器VCC和STB引脚波形畸变振铃终端电阻缺失量总线电阻是否为60欧姆偶发错误帧波特率偏差/采样点不一致核对两节点位定时参数高负载丢帧FIFO溢出改用DMA或提高中断优先级单节点无法发送无ACK应答接第二节点或设环回模式5.3 总线负载率与实时性的平衡CAN总线的实时性不是无限的。一帧标准数据帧大约108到130位取决于数据长度和位填充按500kbps算一帧耗时约216到260微秒。如果总线上有10个节点每个节点每10毫秒发一帧负载率就是10 * 250us / 10ms 25%还算健康。但如果某个节点每1毫秒发一帧8字节数据光它一个就占25%再加上其他节点很容易超过70%的警戒线。我的经验是总线负载率控制在50%以下留出余量应对突发。如果实在降不下来可以考虑几个方向提高波特率但受线缆长度限制、减少发送频率、合并报文把多个信号打包进一帧8字节、或者用CAN FD数据段提速到2Mbps以上但需要收发器和MCU都支持。提示计算负载率时别忘了位填充。CAN协议规定连续5个相同极性位后要插入一个相反位最坏情况下每5位插1位所以实际位数可能比理论值多20%。粗略估算时按理论值乘1.1到1.2比较稳妥。6. 从能通到好用CAN应用层的几个设计习惯6.1 ID规划与信号打包项目一旦超过三四个节点ID规划就必须提前做。我的习惯是用一张表格管理列出每个节点的发送ID、接收ID、周期、数据含义。ID分配上把安全相关的放低段高优先级状态上报放高段。比如0x000到0x0FF给急停、故障0x100到0x1FF给控制指令0x200到0x2FF给传感器数据0x300以上给调试信息。信号打包也有讲究。一帧8字节如果每个信号都单独发一帧总线很快就被占满。合理的做法是把同一节点、同一周期的信号打包进一帧。比如一个电机驱动器可以把位置、速度、电流、温度打包成一帧用位域或者字节偏移来解析。打包时注意字节对齐小端还是大端要统一否则对接时数据全是乱的。6.2 心跳、超时与故障降级CAN本身有错误检测和自动重传但它不保证应用层的可靠性。一个节点死机了它不再发心跳其他节点怎么知道所以应用层必须自己做心跳机制。常见做法是每个节点周期性发一帧心跳报文其他节点收到后刷新对应的超时计数器超过一定时间没收到就判定该节点离线触发降级策略。降级策略要根据场景设计。比如一个多轴系统某个轴离线了是让整个系统停机还是让其他轴继续运行这取决于安全评估。我的建议是安全相关的节点离线必须停机非安全节点离线可以降级运行并报警。心跳周期一般设100毫秒到1秒超时时间设3到5倍心跳周期避免偶发丢帧误判。6.3 用分析仪做回归测试项目开发阶段我强烈建议配一个CAN分析仪便宜的USB-CAN盒子一两百块就能买到。它的价值不只是抓包更重要的是做回归测试。每次改完代码用分析仪录一段总线数据对比改动前后的报文周期、ID、数据是否符合预期。我见过太多改了一个地方结果把另一个功能搞挂的情况有分析仪录制的基线数据回归测试效率能提高好几倍。分析仪还能帮你发现隐藏问题比如某个节点实际发送周期是10.3毫秒而不是设计的10毫秒或者某帧数据的某个字节偶尔跳变。这些问题在功能测试时可能看不出来但到了现场就是隐患。养成改代码必抓包对比的习惯能省掉大量后期调试时间。7. 写在最后的一点个人体会CAN总线这东西看手册觉得复杂真正上手跑通一次之后就会发现它的设计其实非常优雅。仲裁机制、错误检测、自动重传这些特性都是为恶劣环境和实时性量身定做的。我在实际项目里最大的体会是物理层的问题占故障的一半以上很多看似软件配置的错误根子都在接线、终端电阻或者收发器上。所以每次遇到通信问题先别急着改代码拿万用表和示波器把物理层确认一遍往往能少走很多弯路。另一个建议是尽早引入CAN分析仪。它就像调试串口时的串口助手没有它你只能盲猜有了它总线上跑的是什么一目了然。刚开始可能觉得多花了一笔钱但省下的调试时间绝对值得。等你把STM32的bxCAN跑通、能稳定收发之后再去研究CAN FD或者CANopen协议栈会发现底层逻辑都是相通的上手会快很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑