1. 为什么CAN总线值得花时间搞懂嵌入式开发做久了你会发现一个现象很多通信协议你只需要会用就行比如UART、I2C、SPI配好寄存器、调通时序基本就完事了。但CAN总线不太一样它背后有一套完整的设计哲学不理解这套哲学你调出来的代码大概率只能跑在实验室的桌面上一上真实环境就各种翻车。我第一次接触CAN是在一个多节点控制的项目里当时用STM32的bxCAN外设觉得配置几个寄存器就能收发挺简单的。结果实际联调的时候总线时不时报错节点偶尔掉线数据偶尔丢失。排查了整整两天才发现问题根本不在代码逻辑上而在于我对CAN的仲裁机制、错误处理、终端电阻这些底层细节理解不到位。所以这篇内容我想把CAN总线从原理到STM32实战这条线完整串一遍。不管你是刚接触CAN的新手还是已经用过但总觉得理解不够透彻的开发者都能从中找到对自己有用的东西。我会尽量用生活化的类比来解释那些看起来枯燥的协议细节同时给出可以直接参考的代码配置和实操步骤。CAN总线的核心关键词其实就几个差分信号、非破坏性仲裁、位定时、错误帧、终端电阻。这几个词你要是真正理解了CAN对你来说就不再是黑盒。下面我按照“先搞懂为什么再搞懂怎么做”的思路一层一层往下拆。2. CAN总线的核心设计逻辑拆解2.1 为什么CAN用差分信号而不是单端信号要理解CAN首先得理解它为什么能在工业环境、汽车电子这种电磁干扰极强的场景下稳定工作。答案就在差分信号这四个字上。CAN总线使用两根线CAN_H和CAN_L。发送显性位逻辑0时CAN_H被拉到约3.5VCAN_L被拉到约1.5V两者压差约2V发送隐性位逻辑1时两根线都处于约2.5V压差接近0V。接收端不看单根线对地的绝对电压只看两根线之间的压差。这个设计的好处非常直接如果周围有电磁干扰干扰会同时耦合到CAN_H和CAN_L上两根线同时升高或降低相同的电压压差保持不变。接收端依然能正确判断逻辑值。这就像两个人抬一根扁担地面晃一下两个人一起晃扁担的相对位置没变东西不会掉。相比之下UART这种单端信号参考的是地线干扰直接叠加在信号上很容易导致误判。这也是为什么UART通常只在板级通信中使用而CAN可以拉出几十米甚至上百米的线缆。注意差分信号的前提是两根线要双绞。如果CAN_H和CAN_L没有绞在一起干扰耦合的程度不一致共模抑制效果会大打折扣。实际布线时务必使用双绞线最好是带屏蔽层的。2.2 非破坏性仲裁CAN最精妙的设计CAN总线上可以挂多个节点任何节点在总线空闲时都可以主动发送数据。那问题来了如果两个节点同时开始发送怎么办这就是CAN最精妙的地方——非破坏性仲裁。它的规则很简单节点在发送每一位的同时也在监听总线如果自己发的是隐性位1但总线上读到的是显性位0说明有别的节点在发显性位优先级更高这个节点就主动退出仲裁转为接收状态。赢得仲裁的节点完全感知不到冲突继续正常发送不需要重发。举个例子节点A的ID是0x100节点B的ID是0x200。转成二进制0x100 0001 0000 00000x200 0010 0000 0000。从最高位开始比较第3位A是0显性B是1隐性A赢得仲裁。ID值越小优先级越高因为显性位在前面的ID会更早“压住”总线。这个机制的好处是仲裁过程不浪费带宽不需要像以太网那样检测到冲突就全部退避重发。高优先级的消息延迟是确定性的这对汽车刹车信号、工业急停信号这类应用至关重要。2.3 位定时与采样点最容易踩坑的地方CAN的位定时Bit Timing是很多开发者最容易忽略、也最容易出问题的地方。CAN总线没有独立的时钟线所有节点必须通过各自的晶振和分频配置把自己的位时间调整到一致。如果两个节点的位定时偏差太大采样点对不上就会出现位错误。一个CAN位时间被分为四个段同步段Sync Seg固定1个Tq用于硬同步。传播段Prop Seg补偿总线上的物理延迟。相位缓冲段1Phase Seg1可编程用于补偿边沿相位误差。相位缓冲段2Phase Seg2可编程同样用于补偿。采样点位于Phase Seg1和Phase Seg2之间。采样点的位置通常建议设置在位时间的75%~87.5%之间。太靠前信号还没稳定就采样容易读错太靠后留给相位再同步的余量不够。TqTime Quantum是CAN位时间的基本单位由APB时钟分频得到。计算公式Tq (BRP 1) / F_APB 位时间 (1 PropSeg PhaseSeg1 PhaseSeg2) × Tq 波特率 1 / 位时间假设APB时钟为36MHz目标波特率500kbps位时间 2μs。如果取BRP3则Tq 4/36M ≈ 111ns位时间需要约18个Tq。可以分配为SyncSeg1PropSeg6PhaseSeg17PhaseSeg24总计18个Tq采样点在(167)/18 ≈ 77.8%的位置比较合理。实操心得同一个总线上所有节点的采样点位置应尽量一致偏差不要超过几个百分点。如果你发现总线在短距离能通、长距离就报错十有八九是采样点设置有问题。3. STM32 bxCAN外设配置全流程3.1 硬件连接与终端电阻在写代码之前先把硬件搞定。STM32的CAN外设需要外接一个CAN收发器比如常见的TJA1050、SN65HVD230等因为STM32的CAN_TX和CAN_RX是逻辑电平信号不能直接挂到差分总线上。典型连接方式STM32的CAN_TX → 收发器的TXDSTM32的CAN_RX → 收发器的RXD收发器的CAN_H → 总线CAN_H收发器的CAN_L → 总线CAN_L终端电阻是必须的。CAN总线两端各需要接一个120Ω的电阻跨接在CAN_H和CAN_L之间。这两个电阻的作用是消除信号反射保证信号完整性。如果总线很短比如小于10cm的板内通信有时候不接也能凑合跑但一旦线缆长度超过几十厘米不接终端电阻几乎必然出问题。我见过一个案例某开发者调试时总线只有两个节点线长不到20cm没接终端电阻也能通信。后来加到5个节点、线长2米通信就时断时续。补上两端的120Ω电阻后立刻稳定。所以不要心存侥幸终端电阻该接就接。3.2 STM32 CubeMX中的CAN配置用CubeMX配置CAN外设比较直观。以STM32F103系列为例基本步骤如下在Connectivity中选择CAN激活CAN外设。配置波特率参数。CubeMX会自动计算BRP、BS1、BS2等值你只需要输入目标波特率。使能CAN中断如果需要中断收发。配置过滤器Filter。关于过滤器这里多说一句。STM32的CAN过滤器有两种模式掩码模式和列表模式。掩码模式是指定ID的哪些位必须匹配、哪些位不关心列表模式是列出几个精确的ID。如果你只需要接收特定几个ID的数据用列表模式更简单如果需要接收一组ID用掩码模式更灵活。举个例子假设你只想接收ID为0x123的标准帧CAN_FilterTypeDef filter; filter.FilterBank 0; filter.FilterMode CAN_FILTERMODE_IDLIST; filter.FilterScale CAN_FILTERSCALE_32BIT; filter.FilterIdHigh 0x123 5; // 标准帧ID左移5位 filter.FilterIdLow 0; filter.FilterMaskIdHigh 0; filter.FilterMaskIdLow 0; filter.FilterFIFOAssignment CAN_RX_FIFO0; filter.FilterActivation ENABLE; HAL_CAN_ConfigFilter(hcan, filter);注意标准帧ID在过滤器寄存器中需要左移5位存放因为寄存器的高11位对应标准帧ID低5位是其他标志位。这个细节很多新手会搞错导致过滤器死活不生效。3.3 发送与接收代码实现发送一帧CAN数据的基本流程CAN_TxHeaderTypeDef txHeader; uint8_t txData[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; uint32_t txMailbox; txHeader.StdId 0x123; txHeader.ExtId 0; txHeader.IDE CAN_ID_STD; txHeader.RTR CAN_RTR_DATA; txHeader.DLC 8; txHeader.TransmitGlobalTime DISABLE; if (HAL_CAN_AddTxMessage(hcan, txHeader, txData, txMailbox) ! HAL_OK) { // 发送失败处理 }接收通常用中断方式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) { // 根据rxHeader.StdId处理不同消息 switch (rxHeader.StdId) { case 0x123: // 处理消息A break; case 0x456: // 处理消息B break; default: break; } } }这段代码看起来简单但实际使用中有几个容易忽略的点。第一HAL_CAN_AddTxMessage在发送邮箱满的时候会返回HAL_ERROR你需要处理这种情况比如等待或丢弃。第二接收回调中不要做耗时操作否则可能阻塞后续中断。第三如果总线上数据量大建议使用FIFO溢出中断来监控是否丢帧。4. CAN通信中的典型问题与排查方法4.1 总线无法通信的排查思路CAN调不通是最常见的问题排查时建议按照以下顺序逐项检查排查项检查方法常见问题终端电阻断电测量CAN_H与CAN_L之间电阻应为约60Ω两个120Ω并联收发器供电测量收发器VCC引脚电压是否正常波特率确认所有节点波特率一致一个节点不一致会导致全总线错误采样点用示波器观察位时间采样点偏差过大会导致位错误过滤器确认过滤器配置正确ID移位错误导致收不到数据引脚映射确认CAN_TX/RX引脚配置重映射未开启或引脚冲突我遇到最多的情况是波特率不一致。有一次一个项目两个节点一个配了500k一个配了250k结果总线上全是错误帧两个节点都通信不了。因为波特率不匹配时一个节点会把另一个节点的正常位判定为错误然后发送错误帧破坏总线。所以排查时第一步就应该确认所有节点的波特率完全一致。4.2 错误帧与总线关闭状态CAN协议有一套完整的错误检测和恢复机制。每个节点维护两个错误计数器发送错误计数器TEC和接收错误计数器REC。当错误计数超过一定阈值时节点会经历三种状态错误主动Error Active正常状态可以正常收发检测到错误时发送主动错误帧。错误被动Error PassiveTEC或REC超过127仍然可以通信但发送的是被动错误帧影响力减弱。总线关闭Bus OffTEC超过255节点自动脱离总线不再发送任何数据。总线关闭是很多开发者头疼的问题。一旦进入Bus Off节点就“沉默”了你需要手动恢复或者配置自动恢复。STM32的bxCAN支持自动恢复模式通过设置hcan.Instance-MCR | CAN_MCR_ABOM;可以在检测到128次连续11个隐性位后自动退出Bus Off状态。实操心得如果你的节点频繁进入Bus Off不要急着加自动恢复代码先查根因。常见原因包括波特率不匹配、终端电阻缺失、线缆过长、收发器损坏。自动恢复只是治标找到根因才能治本。4.3 数据丢失与FIFO溢出当总线上数据量较大时如果接收端处理不及时FIFO会溢出导致丢帧。STM32的CAN外设有两个接收FIFO每个FIFO可以存3帧。听起来很少但在500kbps波特率下一帧标准数据帧大约需要108~130位即约216~260μs。如果你的中断处理时间超过这个时间就可能丢帧。解决办法有几种一是提高中断优先级减少中断延迟二是在中断中只做数据拷贝把处理逻辑放到主循环三是使用DMA接收部分STM32型号支持。另外可以开启FIFO溢出中断在溢出时做统计帮助你判断系统是否过载。4.4 常见问题速查表现象可能原因解决方法完全无法通信终端电阻缺失、收发器损坏、波特率不一致逐项排查硬件和配置偶尔丢帧FIFO溢出、中断延迟过大优化中断处理、使用DMA频繁错误帧采样点偏差、线缆过长、干扰严重调整位定时、缩短线缆、加屏蔽进入Bus OffTEC超限、持续发送失败检查硬件、配置自动恢复只能单向通信收发器方向引脚、过滤器配置检查收发器STB引脚、过滤器设置短距离正常长距离异常终端电阻、线缆质量、采样点补终端电阻、换双绞线、调采样点5. 从入门到实战的进阶建议5.1 用逻辑分析仪抓CAN波形调试CAN时一台支持CAN解码的逻辑分析仪或者示波器能帮你省下大量时间。你可以直接看到总线上的帧内容、仲裁过程、错误帧位置。特别是当你不确定是硬件问题还是软件问题时抓一波波形基本就能定位。看波形时重点关注几个东西帧起始位是否干净、仲裁段是否有毛刺、ACK位是否被正确拉低、错误帧出现在哪个位置。如果ACK位没有被拉低说明没有其他节点正确接收可能是波特率或过滤器问题。5.2 CAN FD的过渡考虑传统CAN每帧最多8字节数据速率最高1Mbps。CAN FDFlexible Data Rate把数据段扩展到64字节数据段速率可以到5Mbps甚至更高。如果你现在选型的STM32不支持CAN FD但项目未来可能有大数据量需求建议提前考虑升级到支持CAN FD的型号比如STM32G4、STM32H7系列。不过CAN FD的收发器和传统CAN收发器不完全兼容总线上的所有节点都需要支持CAN FD才能发挥优势。如果总线上还有传统CAN节点CAN FD节点需要兼容模式运行速率优势就体现不出来了。5.3 实际项目中的分层设计在真实项目中不建议把CAN收发逻辑散落在各处。比较好的做法是做一层封装把CAN通信抽象成消息层。比如定义一个结构体表示CAN消息用消息ID做路由上层应用只关心消息内容不关心底层是哪个邮箱发的、用的哪个FIFO。typedef struct { uint32_t id; uint8_t data[8]; uint8_t len; } CanMessage_t; void Can_SendMessage(CanMessage_t *msg); void Can_RegisterHandler(uint32_t id, void (*handler)(CanMessage_t *));这样做的好处是将来如果从CAN迁移到CAN FD或者换一个芯片平台上层应用代码基本不用动只需要改底层驱动。我在几个项目中都用了这种分层方式后期维护成本明显降低。5.4 几个容易忽略的细节第一CAN总线的地线。虽然CAN是差分信号但收发器仍然需要共地参考。如果两个节点的地电位差太大共模电压超出收发器的容忍范围通信也会出问题。长距离通信时建议在总线中额外走一根地线。第二波特率与线长的关系。波特率越高允许的线长越短。500kbps下建议不超过100米1Mbps下不超过40米。这不是绝对值和线缆质量、终端电阻、节点数量都有关系但可以作为参考。第三节点数量。CAN总线上节点越多总线负载电容越大信号上升沿越慢。一般建议不超过110个节点实际项目中超过30个节点就要注意信号质量了。第四发送超时处理。HAL_CAN_AddTxMessage在邮箱满时会返回错误但如果你不处理数据就丢了。建议加一个发送队列或者至少做重试和统计。CAN总线看起来简单但真正用好需要对这些细节有深入理解。我自己的经验是每次遇到通信问题先回到原理层面想一想仲裁对不对、采样点偏不偏、终端电阻有没有、错误计数器什么状态。把这几个问题过一遍大部分问题都能定位到。
阅读完成 · 觉得有帮助?