资讯动态

STM32 CAN总线通信实战:从物理层到代码实现

发布时间:2026/10/21 1:08:08 来源:尧图企业网站定制
1. 为什么CAN总线在嵌入式领域一直没被替代搞嵌入式的人迟早会碰到CAN总线。不管你是在做汽车电子、工业控制、还是机器人项目只要涉及到多个控制器之间的通信CAN几乎是一个绕不开的选项。我最早接触CAN是在一个多电机协同控制的项目里当时用UART做多机通信线一多、距离一长通信就各种丢包和干扰后来换成CAN同样的硬件条件下稳定性直接上了一个台阶。从那以后但凡是多节点、有电磁干扰、或者对实时性有要求的场景我基本都会优先考虑CAN。这篇文章面向的是刚接触CAN总线的嵌入式开发者或者已经用过UART、I2C、SPI但还没碰过CAN的朋友。我会从CAN的物理层讲起把帧结构、仲裁机制、错误处理这些核心概念拆开揉碎然后落到STM32上的实际配置和代码实现。看完之后你应该能独立完成一个基于STM32的CAN通信节点并且知道调试时该从哪里下手。CAN总线全称是Controller Area Network最早由博世公司在1980年代为汽车内部网络设计。它的核心设计目标就三个多主通信、高可靠性、实时性强。这三个目标决定了CAN的几乎所有特性。比如多主通信意味着没有主从之分任何节点都可以主动发数据高可靠性体现在差分信号传输和CRC校验上实时性强则依赖于基于优先级的仲裁机制。理解了这三个设计目标后面很多看起来“奇怪”的设计就都能说得通了。CAN和UART最大的区别在于UART是点对点的CAN是多点的总线式结构。总线上挂多个节点所有节点共享同一对差分线。这就像一条走廊所有人都在走廊里说话但每个人只关心自己需要听的内容。这种结构省线、省接口但也带来了仲裁和冲突处理的问题而CAN用了一套非常优雅的机制来解决这些问题。2. CAN的物理层与帧结构从差分信号到数据帧2.1 差分信号为什么能抗干扰CAN总线使用两根线CAN_H和CAN_L。这两根线上的电压是反相的CAN_H高的时候CAN_L低接收端读的是两者的差值。这个差值在显性状态逻辑0时大约是2V在隐性状态逻辑1时接近0V。为什么要用差分因为干扰通常是共模的——两根线同时受到相同的干扰差值不变接收端就能正确还原信号。这就像两个人抬一根棍子旁边有人推他们只要推力对两个人是一样的棍子相对于两人的位置就不变。CAN总线的两端各需要一个120欧姆的终端电阻这个电阻的作用是匹配线缆的特性阻抗消除信号反射。我见过不少人调试CAN时通信不稳定最后发现就是终端电阻没接或者只接了一端。记住终端电阻必须接在总线的两个物理端点中间节点不需要接。CAN的位速率从几十kbps到1Mbps都有汽车上常用500kbps工业场景125kbps或250kbps也很常见。速率越高传输距离越短。1Mbps时总线长度不超过40米125kbps时可以到500米。这个对应关系不是随便定的而是由信号传播延迟和仲裁时间决定的。2.2 一帧CAN报文里到底装了什么CAN的标准帧和扩展帧结构略有不同但核心字段是一样的。我以标准数据帧为例按顺序拆解帧起始SOF1位显性电平告诉总线上所有节点“我要开始发了”。仲裁段包含11位标识符和RTR位。标识符决定优先级数值越小优先级越高。RTR位区分数据帧和远程帧。控制段包含IDE位、保留位和4位数据长度码DLC。DLC表示数据段有多少字节范围0到8。数据段0到8字节的实际数据。CRC段15位CRC校验值加1位界定符用于检测传输错误。ACK段发送节点发隐性位接收正确的节点拉低为显性表示确认收到。帧结束EOF7位隐性电平。扩展帧的标识符是29位仲裁段更长其他结构类似。标准帧适合节点数量少、标识符需求不复杂的场景扩展帧适合需要大量标识符的复杂系统比如汽车上不同ECU的报文区分。这里有个容易混淆的点CAN的标识符不是地址而是报文内容的标识。也就是说标识符描述的是“这帧数据是什么”而不是“这帧数据发给谁”。所有节点都可以根据标识符决定是否接收。这种设计让CAN天然支持广播和多播。2.3 仲裁机制为什么CAN不会撞车CAN最精妙的设计就是非破坏性仲裁。当多个节点同时开始发送时它们先发SOF然后逐位发送标识符。每个节点在发送每一位的同时也在监听总线。如果某个节点发的是隐性位1但总线上是显性位0它就知道自己输了仲裁立刻停止发送转为接收状态。赢的那个节点继续发不受任何影响。这个过程就像一群人同时想说话但规则是谁说的第一个字“优先级”最高谁就继续说其他人自动闭嘴。而且输的人不会打断赢的人所以叫“非破坏性”。仲裁结束后赢的节点继续发数据输的节点会在下一帧再尝试。标识符数值越小优先级越高。因为显性位0会覆盖隐性位1所以标识符前面0越多的报文优先级越高。实际项目中紧急报文比如刹车信号会分配最小的标识符普通状态报文用较大的标识符。3. STM32上的CAN外设配置从时钟到过滤器3.1 bxCAN外设的时钟与引脚配置STM32的CAN外设叫bxCANBasic Extended CAN支持CAN 2.0A和2.0B协议。以STM32F103为例CAN挂载在APB1总线上时钟频率通常是36MHz。配置的第一步是使能时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);引脚方面CAN_RX通常接在PB8或PA11CAN_TX接在PB9或PA12。具体用哪组要看你的芯片型号和板子设计。配置GPIO时要注意CAN_RX设为浮空输入或上拉输入CAN_TX设为复用推挽输出。GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure);这里有个细节如果你用的是STM32F4系列CAN引脚可能还需要配置复用功能编号GPIO_AF_CAN1。F1系列不需要直接设复用推挽就行。3.2 波特率计算一个容易算错的参数CAN的波特率由三个参数决定BRP波特率预分频、BS1时间段1、BS2时间段2。公式是波特率 APB1时钟 / (BRP × (1 BS1 BS2))其中BS1和BS2的单位是时间份额tq。举个例子APB1时钟36MHz想要500kbps36,000,000 / (BRP × (1 BS1 BS2)) 500,000假设BRP4那么1BS1BS218可以取BS115BS22。采样点位置在(1BS1)/(1BS1BS2)16/18≈88.9%这个位置比较理想推荐在75%到90%之间。实际配置代码CAN_InitStructure.CAN_Prescaler 4; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_15tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq;我踩过的坑BS1和BS2的宏定义在标准库和HAL库里名字不一样标准库是CAN_BS1_15tqHAL库是CAN_BS1_15TQ大小写有区别。另外采样点太靠前会导致对时钟偏差容忍度低太靠后则容易受相位延迟影响。88%左右是个比较稳妥的选择。3.3 过滤器配置让MCU只收该收的报文STM32的CAN外设有14个过滤器组互联型有28个每个过滤器组可以配置为屏蔽位模式或标识符列表模式。屏蔽位模式下你可以指定哪些位必须匹配、哪些位不关心列表模式下标识符必须完全匹配才能通过。实际项目中我通常这样分配把紧急报文放在过滤器组0用列表模式精确匹配把普通状态报文放在过滤器组1用屏蔽位模式匹配一段标识符范围。这样既能保证紧急报文不被漏掉又能灵活接收其他报文。CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x1234; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFFF; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0xFFFF; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);注意过滤器配置必须在CAN_Init之后、CAN_ITConfig之前完成。顺序错了过滤器不生效这个坑我踩过不止一次。4. 发送与接收的代码实现及调试技巧4.1 发送一帧CAN报文的完整流程发送流程分三步选一个空的发送邮箱、填报文内容、置位发送请求。标准库的代码大概长这样CanTxMsg TxMessage; TxMessage.StdId 0x123; TxMessage.ExtId 0x00; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC 8; TxMessage.Data[0] 0x01; // ... 填充其他数据 uint8_t mailbox CAN_Transmit(CAN1, TxMessage); while(CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok);最后那个while循环是等待发送完成。实际项目中不建议死等可以加超时或者用中断方式。CAN有3个发送邮箱如果三个都满了CAN_Transmit会返回CAN_TxStatus_NoMailBox这时候需要等一个邮箱空出来再发。发送优先级由标识符决定但如果三个邮箱都满了新报文会排队。如果队列也满了就只能丢弃。所以高优先级报文最好单独用一个邮箱避免被低优先级报文堵住。4.2 中断接收与FIFO管理接收方面STM32的CAN有两个接收FIFO每个FIFO有3级深度。中断方式下收到报文会触发FIFO挂起中断void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 处理数据 CAN_FIFORelease(CAN1, CAN_FIFO0); }这里的关键是每次读取后必须调用CAN_FIFORelease释放FIFO否则FIFO满了之后新报文会被丢弃。我见过有人调试时发现只能收3帧然后就收不到了就是因为忘了释放FIFO。另外如果FIFO溢出CAN外设会置位溢出标志。可以在中断里检查这个标志判断是否发生了数据丢失。实际项目中如果接收处理太慢导致FIFO频繁溢出可以考虑提高中断优先级或者用DMA搬运。4.3 用回环模式快速验证配置调试CAN的时候如果手头没有另一个CAN节点可以用回环模式Loopback自测。在CAN_InitStructure里把CAN_Mode设为CAN_Mode_LoopBack发送的报文会直接回到自己的接收FIFO不经过总线。这样可以先验证发送和接收的代码逻辑是否正确再去调硬件。CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack;回环模式验证通过后再改成CAN_Mode_Normal接实际总线。这个技巧能帮你快速区分是软件问题还是硬件问题。如果回环模式能收能发正常模式收不到那基本就是硬件接线、终端电阻或者波特率不匹配的问题。5. 实际项目中那些文档不会告诉你的坑5.1 终端电阻和线缆选择终端电阻必须是120欧姆两端各一个。我见过有人用100欧姆或者220欧姆短距离低速可能勉强能通但距离一长或者速率一高就不稳定。线缆方面CAN_H和CAN_L必须用双绞线绞距越密越好。如果用普通的平行线抗干扰能力会差很多。还有一个容易忽略的点CAN总线的地线。虽然CAN是差分传输但所有节点最好共地。如果节点之间地电位差太大共模电压可能超出收发器的承受范围导致通信异常甚至损坏接口。长距离布线时可以加共模电感或者隔离收发器。5.2 波特率不匹配的排查思路两个节点通信不上最常见的原因就是波特率不匹配。排查方法用示波器看CAN_H和CAN_L的波形测量一位的时间宽度。比如500kbps时一位是2微秒。如果测出来是4微秒那就是250kbps。另外采样点位置不一致也可能导致通信失败尤其是总线上节点多、线缆长的时候。我一般会先用回环模式确认自己的配置没问题然后用示波器确认总线上的实际波特率最后检查两个节点的采样点是否接近。这三步走下来基本能定位90%的通信问题。5.3 错误帧和总线关闭状态CAN节点有错误计数器发送错误或接收错误都会导致计数器增加。当发送错误计数器超过255时节点进入总线关闭Bus Off状态不再参与通信。这时候需要软件干预才能恢复。实际项目中如果某个节点频繁进入Bus Off通常是硬件问题终端电阻不对、线缆太长、波特率偏差太大、或者收发器损坏。软件上可以加一个恢复机制检测到Bus Off后延时一段时间重新初始化CAN外设。if (CAN_GetFlagStatus(CAN1, CAN_FLAG_BOF) ! RESET) { CAN_DeInit(CAN1); // 重新初始化 }但根本解决还是要从硬件上找原因。软件恢复只是兜底不能替代硬件排查。5.4 多节点通信时的优先级反转CAN的仲裁机制保证了高优先级报文先发但如果低优先级报文正在发送高优先级报文必须等它发完。一帧CAN报文最长大约130位标准帧8字节数据500kbps下大约260微秒。这个延迟在大多数场景下可以接受但如果你的系统对实时性要求极高就需要合理分配标识符把最紧急的报文设为最高优先级。另外如果总线上节点很多、负载率很高低优先级报文可能长时间发不出去。一般建议总线负载率不超过50%留出足够的余量应对突发流量。6. 从单节点到多节点一个最小系统的搭建思路6.1 硬件连接清单搭建一个最小的CAN通信系统你需要两个STM32开发板或者一个STM32加一个CAN分析仪、两个CAN收发器模块比如TJA1050或SN65HVD230、一个120欧姆电阻两个、若干杜邦线。接线方式STM32的CAN_TX接收发器的TXDCAN_RX接RXD收发器的CAN_H接总线CAN_HCAN_L接总线CAN_L两个120欧姆电阻分别接在总线的两端。如果你用的是CAN分析仪它通常内置了终端电阻可以通过跳线选择是否启用。用分析仪的好处是可以直接在电脑上看报文调试起来非常方便。6.2 软件框架的搭建我一般会把CAN通信封装成三个模块初始化模块、发送模块、接收模块。初始化模块负责配置GPIO、CAN外设和过滤器发送模块提供一个接口传入标识符和数据内部处理邮箱选择和发送等待接收模块用中断或轮询方式读取FIFO把数据放到一个环形缓冲区供上层应用读取。这种分层设计的好处是上层应用不需要关心CAN的底层细节只需要调用发送接口和读取接收缓冲区。换芯片或者换库的时候只需要改底层驱动上层代码不动。6.3 测试与验证步骤系统搭好后按这个顺序验证第一步用回环模式确认单个节点的发送和接收代码正确第二步两个节点都设成正常模式一个发一个收确认波特率和接线正确第三步增加节点数量观察总线负载和错误计数器第四步加入实际业务数据测试长时间运行的稳定性。每一步都要用示波器或者CAN分析仪观察波形和报文不要只看代码逻辑。CAN的问题很多时候是物理层的代码写得再对硬件不对也通不了。7. 进阶方向CAN FD和CANopenCAN FD是CAN的升级版数据段可以到64字节速率也可以更高。STM32的G4、H7等系列已经支持CAN FD。如果你的项目需要传大量数据CAN FD是个不错的选择。但要注意CAN FD和经典CAN在同一总线上不能直接混用需要所有节点都支持FD才行。CANopen是建立在CAN之上的应用层协议定义了通信对象、对象字典、网络管理等规范。工业控制领域用得很多比如伺服驱动器、PLC之间的通信。如果你要做工业设备CANopen值得花时间学一下。它把CAN的标识符分配、数据格式、状态机都标准化了不同厂家的设备可以互操作。不过对于大多数嵌入式项目来说先把经典CAN用熟理解仲裁、过滤、错误处理这些核心机制再去学CAN FD或CANopen会轻松很多。底层的东西是相通的上层协议只是换了一种组织数据的方式。我在实际项目中的体会是CAN总线的调试三分靠代码七分靠硬件。示波器和CAN分析仪是必备工具能帮你快速定位问题。另外不要等到系统集成的时候才去调CAN单独搭一个最小系统先把通信跑通后面会省很多时间。

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

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

免费获取报价 →
↑