拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式CAN总线实战指南:从原理到STM32配置与问题排查

嵌入式CAN总线实战指南:从原理到STM32配置与问题排查

1. 为什么CAN总线是嵌入式工程师绕不开的一道坎

搞嵌入式这行,你可以不会写设备树,可以没碰过RTOS,但只要你的产品涉及板间通信、汽车电子、工业控制,CAN总线就是那道你迟早要迈过去的门槛。我见过太多人,STM32的GPIO玩得飞起,串口收发也熟得不行,一到CAN就卡壳——要么初始化配错波特率,要么发不出数据,要么总线一挂上就报错误帧,折腾半天找不到北。

这份“报告6686”其实是一份典型的嵌入式CAN总线知识梳理,核心就是帮嵌入式开发者把CAN从“听说过”变成“能用起来”。它解决的不是什么高深问题,而是最实际的痛点:CAN初始化怎么配、报文怎么收发、过滤器怎么设置、总线出问题了怎么排查。适合谁看?刚入行的嵌入式新人、从纯MCU转向汽车电子或工业控制的开发者、以及面试前需要快速复习CAN八股文的同学。

我自己的经历是,第一次接触CAN是在一个车载控制器项目上,当时用STM32F103的CAN外设,波特率500K,结果两块板子怎么都通不上。示波器一测,波形乱七八糟,后来才发现是终端电阻没接。这种坑,文档里不会写,但实际项目中一踩一个准。所以这篇博文,我打算把CAN总线的核心知识、实操配置、常见问题和排查经验,按照一个嵌入式从业者的视角,从头到尾捋一遍。

2. CAN总线核心原理拆解:从物理层到协议层

2.1 CAN总线的物理层长什么样

CAN总线物理层其实很简单,两根线:CAN_H和CAN_L,差分信号传输。差分的好处是抗干扰能力强,工业现场电磁环境恶劣,单端信号早就被干扰得不成样子了,差分信号靠两根线的电压差来判断逻辑,共模干扰会被抵消掉。

显性电平(逻辑0)时,CAN_H约3.5V,CAN_L约1.5V,压差2V左右;隐性电平(逻辑1)时,两根线都在2.5V附近,压差接近0V。这个“显性”和“隐性”的概念很关键,因为CAN的仲裁机制就是靠显性电平覆盖隐性电平来实现的——谁先发出显性位,谁就抢到总线。

终端电阻是必须的,标准是120欧姆,接在总线两端。我见过有人只在一端接电阻,短距离低速还能凑合,一旦速率上到500K或者线缆拉长到几十米,通信就时好时坏。为什么是120欧姆?因为CAN总线的特性阻抗大约是120欧姆,终端电阻用来匹配阻抗,消除信号反射。你可以把它理解成水管末端的堵头,不堵住的话水会反弹回来形成驻波。

注意:终端电阻不是随便接的,必须接在总线的两个物理端点。如果节点是手拉手串联的,电阻接在首尾两个节点上;如果是星型连接,那问题就大了,CAN不推荐星型拓扑。

2.2 CAN帧结构:一张表看懂标准帧和扩展帧

CAN协议有标准帧(11位ID)和扩展帧(29位ID)两种格式。标准帧用在大多数常规场景,扩展帧用在需要更多ID空间的场合,比如J1939协议就是29位ID。

字段标准帧扩展帧说明
帧起始SOF1位1位显性位,同步用
仲裁段11位ID + RTR29位ID + SRR + IDE + RTR决定优先级
控制段IDE + r0 + DLCIDE + r1 + r0 + DLCDLC表示数据长度0-8
数据段0-8字节0-8字节实际载荷
CRC段15位CRC + 界定符同左校验
ACK段ACK槽 + 界定符同左接收方拉低确认
帧结束7位隐性同左EOF

仲裁段是CAN最精妙的设计。总线上多个节点同时发送时,谁发的ID数值小(显性位多),谁就赢。输的那个节点会自动退让,下一轮再发。这就像一群人同时说话,谁声音大谁先说,但CAN是靠ID优先级来定,不是靠音量。

DLC是数据长度码,4位,取值0-8。注意,经典CAN一帧最多8字节数据,CAN FD可以到64字节,但那是另一个话题了。很多新手会问“为什么只能发8字节”,这是协议设计时的权衡,8字节对于大多数控制指令足够了,而且帧短意味着实时性好,总线占用时间短。

2.3 CAN的位定时与波特率计算

波特率配置是CAN初始化最容易出错的地方。CAN的位时间分成四段:同步段(Sync_Seg)、传播段(Prop_Seg)、相位缓冲段1(Phase_Seg1)、相位缓冲段2(Phase_Seg2)。

以STM32为例,CAN时钟来自APB1,假设APB1是36MHz,目标波特率500K,那么位时间总长度是36M/500K=72个时钟周期。这72个周期要分配到上述四段中。通常Sync_Seg固定1个周期,Prop_Seg + Phase_Seg1 + Phase_Seg2 = 71。

采样点位置很关键,一般在75%左右比较稳。计算公式是:(Sync_Seg + Prop_Seg + Phase_Seg1) / 总位时间。如果Prop_Seg=1,Phase_Seg1=52,Phase_Seg2=18,那么采样点在(1+1+52)/72=75%,比较合理。

实际配置时,STM32的CAN_BTR寄存器里,BRP是预分频,TS1和TS2分别对应Phase_Seg1和Phase_Seg2,SJW是同步跳转宽度。我一般用下面的参数:BRP=4,TS1=15,TS2=4,SJW=1,这样位时间=4*(1+15+4)=80个时钟周期,36M/80=450K,不太对。重新算:要500K,位时间=72个时钟周期,BRP=4的话,TQ总数=72/4=18,TS1+TS2+1=18,取TS1=13,TS2=4,采样点=(1+13)/18=77.8%,可以。

提示:如果你懒得算,可以用STM32CubeMX自动生成,但建议至少手动算一遍,面试常问,而且出了问题你能知道从哪查。

3. 嵌入式CAN开发实操:从初始化到收发

3.1 硬件连接与电平匹配

先确认硬件。CAN收发器常用的是TJA1050、SN65HVD230、MCP2551等。MCU的CAN_TX和CAN_RX接到收发器的TXD和RXD,收发器的CAN_H和CAN_L接到总线。注意TJA1050是5V供电,SN65HVD230是3.3V,别搞混了。

如果两块板子通信,最简单的方式是CAN_H接CAN_H,CAN_L接CAN_L,两端各接一个120欧姆电阻。如果只有一块板子想自测,可以进入回环模式(Loopback),MCU自己发自己收,不需要外部收发器。

我踩过的坑:有一次用3.3V的MCU直接接5V的TJA1050,结果CAN_TX电平不够,收发器识别不到。后来加了电平转换芯片才解决。所以选型时一定要看收发器的VIL/VIH参数,确保和MCU电平匹配。

3.2 STM32 CAN初始化代码逐行解析

下面以STM32 HAL库为例,给出一段可复用的CAN初始化代码:

CAN_HandleTypeDef hcan; void CAN_Init(void) { hcan.Instance = CAN1; hcan.Init.Prescaler = 4; // 预分频,APB1=36M,TQ=4/36M hcan.Init.Mode = CAN_MODE_NORMAL; // 正常模式 hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_13TQ; // Phase_Seg1 + Prop_Seg hcan.Init.TimeSeg2 = CAN_BS2_4TQ; // Phase_Seg2 hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.AutoBusOff = DISABLE; hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE; // 自动重传 hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); } // 配置过滤器 CAN_FilterTypeDef filter; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; filter.FilterMaskIdLow = 0x0000; // 全通过 filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; filter.SlaveStartFilterBank = 14; if (HAL_CAN_ConfigFilter(&hcan, &filter) != HAL_OK) { Error_Handler(); } HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); }

这段代码里,Prescaler、TimeSeg1、TimeSeg2决定了波特率。AutoRetransmission我一般开ENABLE,因为CAN总线仲裁失败或错误时会自动重发,省得应用层处理。过滤器配置成掩码模式,Mask全0表示不关心任何位,所有报文都收进FIFO0。

3.3 发送与接收:中断和轮询怎么选

发送用HAL_CAN_AddTxMessage,接收可以用轮询HAL_CAN_GetRxMessage,也可以用中断。我推荐中断方式,因为CAN报文到达是异步的,轮询会浪费CPU。

// 发送 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; HAL_CAN_AddTxMessage(&hcan, &txHeader, txData, &txMailbox); // 接收回调 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); // 处理rxData }

中断回调里不要做耗时操作,把数据拷到缓冲区,置个标志位,主循环里处理。我见过有人在回调里直接跑PID算法,结果CAN中断频繁触发,系统直接卡死。

注意:CAN发送邮箱只有3个,如果连续发送大量报文,AddTxMessage会返回HAL_ERROR。这时候要么等邮箱空,要么用发送完成回调来排队。

4. CAN总线常见问题与排查技巧实录

4.1 通信不上:从硬件到软件的排查顺序

CAN通信不上是最常见的问题,我一般按以下顺序排查:

  1. 测终端电阻:断电,万用表测CAN_H和CAN_L之间电阻,应该是60欧姆左右(两个120欧姆并联)。如果是120欧姆,说明只接了一个;如果是无穷大,说明一个都没接。
  2. 测电平:上电,测CAN_H和CAN_L对地电压,隐性时都应该在2.5V左右。如果偏差大,检查收发器供电。
  3. 看波形:示波器差分探头看CAN_H-CAN_L,应该有明显的差分方波。如果没有波形,检查MCU的CAN_TX是否有输出。
  4. 查波特率:两块板子的波特率必须完全一致,采样点也要接近。我遇到过一块板子采样点80%,另一块60%,短距离能通,长距离就丢帧。
  5. 查过滤器:如果发送正常但收不到,大概率是过滤器配置问题。先把过滤器设成全通过,确认能收到再慢慢调。

4.2 错误帧与总线关闭:Bus-Off怎么恢复

CAN节点有错误计数器,发送错误超过255时进入Bus-Off状态,节点自动脱离总线。这时候需要软件干预恢复。

STM32的HAL库可以开启AutoBusOff,自动恢复。但自动恢复有个问题:如果总线短路导致持续错误,节点会不断尝试恢复又不断Bus-Off,影响总线。所以我一般用自动恢复,但在应用层加个计数,频繁Bus-Off就报警。

手动恢复的代码:

if (HAL_CAN_GetError(&hcan) & HAL_CAN_ERROR_BOF) { HAL_CAN_Stop(&hcan); HAL_CAN_Start(&hcan); }

4.3 常见问题速查表

现象可能原因排查方法
完全无通信终端电阻缺失、接线反了测电阻、查CAN_H/L
偶发丢帧波特率偏差、采样点不对示波器测位时间
收不到特定ID过滤器配置错误改全通过测试
发送失败邮箱满、总线Bus-Off查错误码、加延时
距离短就出错线缆阻抗不匹配、分支太长换双绞线、缩短分支
上电就Bus-OffCAN_H/L短路、收发器损坏断电测短路

4.4 实操心得:那些文档不会告诉你的细节

第一个心得:CAN总线的地线一定要接。虽然CAN是差分信号,但共模电压范围有限,如果两个节点地电位差太大,收发器会损坏。我见过一个项目,两块板子分别供电,地没连,结果通信时好时坏,后来把地连上就稳了。

第二个心得:双绞线不是随便扭两下就行,节距要均匀,一般20-30 twists per meter。线缆阻抗要120欧姆,别拿普通杜邦线凑合,短距离低速还行,一上速度就完蛋。

第三个心得:CAN分析仪是个好东西。早期我调试CAN全靠示波器和打印,效率极低。后来买了个USB-CAN分析仪,能直接看报文、统计错误帧、发模拟数据,排查问题快十倍。推荐入门级的就行,几百块钱,比浪费的时间值。

第四个心得:面试常问的CAN八股文,其实就那几个:CAN为什么用差分、仲裁机制怎么工作、标准帧和扩展帧区别、波特率怎么算、Bus-Off怎么恢复。把这几条吃透,基本够用。

5. CAN在嵌入式项目中的典型应用场景

5.1 汽车电子:ECU之间的通信骨架

汽车上CAN总线是标配,发动机ECU、变速箱ECU、ABS、仪表盘都挂在CAN上。车速、转速、水温这些信号通过CAN广播,谁需要谁收。诊断接口OBD-II也是CAN,读故障码、刷ECU都走CAN。

汽车CAN通常是500K波特率,ID分配有规范,比如0x000-0x0FF是高优先级安全相关,0x100-0x4FF是车身控制。做汽车嵌入式开发,不懂CAN基本没法干活。

5.2 工业控制:PLC与远程IO的可靠连接

工业现场PLC和远程IO模块之间常用CANopen协议,底层就是CAN。CANopen定义了对象字典、PDO、SDO这些概念,但底层还是CAN帧。工业环境干扰大,CAN的差分传输和CRC校验保证了可靠性。

我做过一个项目,PLC通过CAN控制20个远程IO节点,线缆拉了80米,波特率250K,跑了两年没出过通信故障。关键就是终端电阻接对、线缆用屏蔽双绞线、屏蔽层单端接地。

5.3 储能与BMS:电池管理系统的通信总线

储能BMS里,主控和从控之间用CAN通信,从控采集电池电压温度,通过CAN上报主控。BMS的CAN通常用扩展帧,因为节点多,11位ID不够分。波特率一般250K或500K,要求高实时性和高可靠性。

这个场景对CAN的错误处理要求高,因为电池数据丢了可能导致过充过放。所以BMS的CAN驱动一般会做应用层重传和超时检测,不能只依赖CAN底层的自动重传。

6. 进阶方向:从经典CAN到CAN FD与时间触发

6.1 CAN FD:更大数据量和更高波特率

经典CAN最多8字节,500K波特率,在汽车OTA升级、大数据量传输场景下不够用了。CAN FD(Flexible Data-rate)把数据段扩展到64字节,数据段波特率可以到5M甚至更高,仲裁段还是保持低速以保证兼容性。

CAN FD的帧格式和经典CAN不同,多了FDF、BRS、ESI这几个位。如果你用STM32G4或H7系列,硬件支持CAN FD,配置时注意数据段波特率要单独设置。

6.2 时间触发CAN:确定性通信的尝试

TTCAN(Time-Triggered CAN)在CAN基础上加了时间调度,每个节点按时间槽发送,避免仲裁带来的不确定性。这个在航空和高端汽车里有用,但普及度不高,因为CAN FD和以太网已经覆盖了大部分需求。

6.3 嵌入式Linux下的CAN开发

如果你做嵌入式Linux项目,CAN设备在/dev下是can0、can1这样的网络接口。用ip命令配置:

ip link set can0 type can bitrate 500000 ip link set can0 up

收发用socketcan,和UDP socket类似。candump看报文,cansend发报文。Linux下的CAN开发比裸机简单,因为内核帮你处理了大部分底层细节,但调试时还是要懂位定时和错误处理。

7. 我个人在CAN开发中的几点体会

CAN总线这东西,入门不难,精通不易。协议本身不复杂,但实际项目中的问题往往出在硬件和细节上。我的建议是,先拿两块开发板和两个收发器,把通信跑通,然后故意制造一些错误——拔掉终端电阻、改错波特率、短接CAN_H和CAN_L——看看现象是什么,这样印象最深。

另外,别只盯着MCU的CAN外设,收发器的选型和电路设计同样重要。我见过太多项目,软件调了半天,最后发现是收发器选错了型号。还有,CAN分析仪真的值得买,省下的调试时间远超它的价格。

最后说个面试技巧:如果面试官问你CAN,别只背协议,结合你实际做过的项目讲,比如“我在XX项目里用CAN做了XX,遇到了XX问题,最后怎么解决的”,这样比干巴巴背八股文强得多。

返回列表