做嵌入式这几年,CAN总线几乎成了我工作中最熟悉的陌生人。说熟悉,是因为从车身电子到工业现场,到处都是它的身影;说陌生,是因为很多同行在单节点回环测试时一切正常,一旦组了多节点总线,不是随机丢帧,就是整个网络在一阵抖动后彻底瘫掉。检查了半天才发现,问题往往不是出在软件流程上,而是位时序、采样点和仲裁逻辑这些基础概念没有吃透。这篇笔记是我在TI嵌入式平台上做CAN开发时的经验整理,包含物理层设计、波特率计算、报文解析、CAN FD升级和故障排查链路,适合刚接触CAN的工程师,也适合那些已经能跑demo但想知道"为什么这样配置更稳"的开发者。
1. 物理层决定上限:差分信号、120欧终端和总线拓扑
1.1 为什么CAN要设计成差分传输
很多人第一次看CAN收发器输出,都会盯着CAN_H和CAN_L两个引脚发呆:明明是两根线,为什么不直接用一根线传高低电平?答案就藏在实际的电磁环境里。车内或者工业现场的电缆往往和电机线、继电器线走在一起,共模干扰非常强。CAN采用差分传输后,外部干扰同时叠加到CAN_H和CAN_L上,接收端关心的是两者之差,共模噪声便被抵消掉了。
隐性状态下,CAN_H和CAN_L都约为2.5V,差分电压接近0V;显性状态下,CAN_H升到3.5V左右,CAN_L降到1.5V左右,差分电压约2V。总线空闲时所有节点输出隐性电平,谁要发送,就把总线拉成显性。这个"线与"特性直接决定了后面要讲的仲裁机制。
在实际测试中,我习惯用示波器两只探头分别夹CAN_H和CAN_L,打开数学通道看差分波形。如果只量单端电压,很容易被共模噪声误导。正常的显性差分幅度应该在1.5V到3V之间,如果低于1.2V,节点可能识别不了,通信就会间歇性出错。
1.2 120欧终端电阻:两颗电阻背后的阻抗逻辑
CAN总线要求电缆两端各接一个120欧电阻,很多初学者不理解,以为随便焊两颗电阻就完事。实际上CAN收发器设计时是假定总线特征阻抗约为120欧,你在物理末端并联电阻,是为了让信号到达线缆末端时不产生反射。如果反射严重,波形会出现振铃,接收端的采样点可能采到错误的电平。
两个120欧终端电阻并联后,从总线任意一点看进去是60欧。这也是现场排查硬件问题时最快的手段:断电后在总线两端分别量阻抗,正常应该看到约60欧。如果只有60欧中的一个,你会量到120欧,说明缺了一个终端电阻。更隐蔽的情况是终端电阻被一个带开关的节点误接成双端都接,导致等效阻抗低于60欧,虽然短距离也能工作,但总线负载被加重,长时间运行容易热到出问题。
TI的收发器生态里,SN65HVD230是3.3V供电的经典款,支持1Mbps,适合和3.3V MCU直连;SN65HVD251则是5V供电,和老的PCA82C250引脚兼容。做CAN FD或者更高频率应用时,TCAN1042这类收发器支持5Mbps甚至更高,而且带TXD显性超时保护,防止芯片被一条常显性的故障线锁死。选型时不要只看速率,还要注意VIO引脚是否支持你的MCU电平,很多收发器在逻辑接口侧已经做了电平转换。
1.3 拓扑和分支长度:CAN网络到最后都是"线"的艺术
CAN总线的拓扑设计最容易埋雷。理想情况是:一根主干线,从一端到另一端,所有节点用尽量短的分支线(stub)挂上去。主干线两端接终端电阻。这种结构下信号从源端出发,一路到两个末端,反射最少。
但实际项目里,总有工程师图方便把节点串成一个菊花链,或者让分支线长达一两米。低速场合可能还能跑,但在500kbps以上,每根分支线都是一截阻抗不连续的短截线,会产生反射。反射在分支末端来回弹跳,降低噪声容限。我在项目中给自己定的经验值:分支线尽量控制在0.3米以内,超过1米就必须重新规划节点位置或使用CAN hub进行星型转接。
另一个和拓扑相关的坑是接地。CAN总线是差分传输,但不代表不需要地线。节点之间的地电位差如果过大,即使差分信号正常,共模电压也可能超出收发器的容忍范围(很多收发器是-2V到7V)。长距离布线时,我建议要么在每个节点间拉一根可靠的地线,要么直接用带隔离的收发器方案,避免地环路和共模电压累积把通信拖垮。
2. 位时间拆解:波特率、SJW与采样点的完整计算
2.1 一个bit为什么被切成四段
很多人配置CAN波特率时,只知道填一个数字进去,比如500k,却不知道控制器内部其实把每一个bit时间切成了若干段(Time Quantum,Tq)。位时间通常由四段组成:同步段(SYNC_SEG)、传播段(PROP_SEG)、相位缓冲段1(PS1)和相位缓冲段2(PS2)。
同步段固定为1个Tq,用来检测总线上的跳变沿,重新同步所有节点。传播段用来补偿信号在线缆和收发器上的物理延迟,想象一下一个节点发出去的位,另一个节点收到后又要回复,信号一来一回的时间必须被容纳。相位缓冲段1和2分布在采样点两侧,PS1用于吸收信号较晚到达的偏差,PS2用于给出位结束前的缓冲,采样点就落在PS1和PS2的边界上。
从这个结构就能看出,波特率不是简单的一个"频率"概念,它背后的位时间结构决定了你通信的鲁棒性。同样的500kbps,有人用40个Tq实现,有人用16个Tq实现,抗干扰能力完全不同。
2.2 以40MHz时钟和500kbps目标为例,手算一遍
我拿一个使用40MHz外设时钟的TI MCU来实际算一遍。目标波特率500kbps,一个bit的时长就是2微秒。如果预分频系数BRP取4,那么Tq = BRP / 40MHz = 100纳秒,一个bit需要20个Tq。
接下来分配这20个Tq。同步段固定1个Tq。传播段需要考虑线缆延迟:假设总线长度10米,信号在线缆中的速度约为光速的60%,约5ns/m,那么一来一回大约100纳秒;加上两个收发器的环路延迟约100纳秒,传播段最少需要200纳秒,即2个Tq。剩下17个Tq分给PS1和PS2,我习惯把采样点放在75%左右,所以PS1取12个Tq,PS2取5个Tq。最终位时间 = 1 + 2 + 12 + 5 = 20个Tq,采样点位置 = (1 + 2 + 12) / 20 = 75%。
用TI SysConfig配置时,这个计算过程可以被自动化,但我仍然建议自己手算一遍,因为实际项目里你会遇到各种奇怪的时钟源,比如PLL后得到38.4MHz这类非整数频率。手算才能快速判断该用哪个预分频值。如果40MHz时钟直接以BRP=2、总Tq=20配置,就能得到1Mbps,同一套位时间结构直接翻倍,非常方便。
2.3 SJW和采样点的工程经验
SJW全称是Synchronization Jump Width,即重新同步时,控制器最多允许相位缓冲段移动多少个Tq。它决定了节点抵抗晶振偏差和总线干扰的能力。SJW太小,从节点跟不上主节点的时钟漂移;SJW太大,又会把真正的总线跳变误判为噪声,反而引入抖动。
工程上我一般这样取:SJW至少为1,最多不能超过PS2,常见配置取1到4个Tq。如果系统里用了便宜的晶振(精度100ppm级别)或者工作温度变化很大,SJW建议取大一点,比如2到4;如果用的是高精度晶振或TCXO,SJW取1到2就够。
采样点的选择也很有讲究。太早,信号还没稳定,你采到一个毛刺;太晚,位时间快结束,遇到下一个跳变沿时几乎没有余量。低速CAN(125k到250k)我常用75%;高速CAN(500k到1M)用75%到80%比较稳。某些汽车OEM协议会规定精确的采样点,比如85%,这时候就要反推各个段的Tq分配。无论如何,采样点不要低于60%,否则线缆延长一点就容易出错。
3. 仲裁机制与ID分配:为什么优先级越高ID数字越小
3.1 仲裁本质上是一场"听总线说话"的淘汰赛
CAN没有中心调度器,所有节点都能在总线空闲时同时发起发送。之所以不会乱套,靠的是逐位仲裁。总线上,显性电平(0)会覆盖隐性电平(1),所以当一个节点发送隐性1,而另一个节点发送显性0时,总线实际电平是0。发送隐性的那个节点发现"自己发的1,总线上却是0",就知道有更高优先级的节点在抢总线,立刻退出发送,转入接收状态。
这个过程从帧起始的SOF开始,紧接着就是ID逐位比较。一个11位ID的标准帧,相当于11轮淘汰赛,直到剩下唯一一个发送者。因此,ID数值越小,二进制中高位的0出现得越早,优先级就越高。这也是为什么CAN协议里0x000是最高优先级,0x7FF是最低优先级。
理解了这个机制,就明白了两件事。第一,CAN帧一旦开始发送,是不会被更高优先级消息"打断"的,只能等当前帧发送完成,这是CAN实时性设计的基础。第二,如果你把两个节点的ID配置得完全相同,而它们又在同一时刻抢总线,仲裁会一直平局,直到数据场阶段才可能靠内容区分,这是一种错误用法,会让低ID还是高ID都无法决出胜负,最终产生错误帧。
3.2 ID规划:低号段给谁,高号段给谁
因为ID越小优先级越高,实际项目里就必须把最紧急、对延迟最敏感的消息放在低号段。我一般会这样规划:
| ID范围 | 消息类型 | 说明 |
|---|---|---|
| 0x000~0x0FF | 安全关键控制 | 刹车、转向、扭矩请求等 |
| 0x100~0x1FF | 动力与能量管理 | 电池状态、电机转速、VCU控制 |
| 0x200~0x2FF | 车身舒适 | 车门、车窗、空调、灯光 |
| 0x300~0x3FF | 诊断与标定 | 故障码、读写参数 |
| 0x400~0x7FF | 低速普通信息 | 软件版本、统计数据 |
还要考虑扩展帧29位ID的情况。标准11位ID放在扩展帧的前面部分,高11位相同的前提下,扩展帧和标准帧的仲裁顺序还跟RTR、IDE这些位有关。这部分细节容易让人头晕,我建议优先保持系统中某一类消息的ID格式统一,不要混用标准帧和扩展帧,能省掉很多调试时间。
3.3 验收滤波与负载率估算
ID除了决定优先级,还承担着"路由"功能。如果总线上有20个节点,每个节点都处理所有消息,CPU开销和软件复杂度会暴涨。CAN控制器里的验收滤波单元(比如TI MCAN模块的标准ID过滤元素)会先在硬件层过滤,只有ID满足条件的帧才进入接收FIFO。
过滤规则分掩码和范围两种。掩码方式里,mask位为0表示这一位必须匹配期望ID,mask位为1表示这一位不关心。举个例子,期望接收0x520,掩码设为0x7F0,那么高7位精确匹配0x520而低4位任意,相当于能收0x520到0x52F这一组ID。这个技巧在诊断多实例会话时很实用。
负载率估算也建议在方案阶段就做。总线上每秒实际传输的位数除以波特率就是负载率。假设标准帧平均120位(包含填充位),500kbps下每秒1000帧,负载率就是120000/500000 = 24%。我一般要求正常工况控制在60%以内,剩下40%留给诊断、标定、OTA刷写这些突发流量。一旦长期超过60%,总线延迟会显著增加,低优先级消息可能出现饿死现象。
4. 报文解析实战:字节序、掩码和一个真实数据帧的拆解
4.1 一个典型CAN数据帧的离线解读
拿到一段CAN报文,很多人第一反应是拿十六进制数据硬猜。这里我演示一个典型的解析过程。假设从总线上抓到一个标准帧,ID=0x0CF00400,DLC=8,数据为00 14 88 20 00 38 00 00。
先看ID。0x0CF00400是商用车J1939协议中很常见的EEC1(Electronic Engine Controller 1)报文。再看数据场。这个协议的转速信号定义在byte1和byte2,分辨率0.25 RPM/bit,Motorola字节序。把0x14和0x88拼接成0x1488,即5256,乘以0.25得到1314 RPM。车速信号如果定义在byte3,分辨率1 km/h,那么0x20就是32 km/h。byte5是冷却液温度,0x38等于56,减去偏移量40得到16°C,冷车状态完全合理。
我经常对初学者说:不要先写代码,先手工把一帧数据算一遍。只有手算对了,你写的代码才有依据。如果手算和代码结果对不上,问题大概率出在字节序上。
4.2 字节序陷阱:大端、小端和"看似没序"的位域
CAN报文的数据场按字节传输,但一个超过8位的信号可能跨字节存放。Motorola格式(也叫大端)下,高字节在前,0x14 0x88表示0x1488;Intel格式(也叫小端)下,低字节在前,同样的字节序列表示0x8814,换算过来是34836,再乘0.25就变成了8709 RPM,差了六倍多。这种错误很难一眼发现,因为数据看起来"挺正常"。
更隐蔽的是位序。DBC文件里定义的start_bit在不同工具中可能表示LSB或MSB位置,同一个信号用Vector工具和用某些开源工具解析,结果可能相差很大。我的建议是:项目初期就用一个已知物理量的报文做基准测试,把这个报文的所有字节序和位序确认清楚,形成文档;后续所有信号解析都以它为模板。另外,有符号信号要注意符号扩展,不能简单把16位无符号值硬转,否则负温度、负扭矩会被解析成很大的正数。
我踩过最痛的一次坑,是把一个16位小端符号信号当成无符号解析,结果负扭矩显示成65000多,整个台架测试被这个假数据带偏了方向。后来排查到DBC文件里的符号定义,才意识到问题出在最低的人工解析。
4.3 用脚本和DBC把"天书"变成物理量
如果是小批量调试,手写C或Python解析就够了。但项目里报文一多,建议直接用DBC文件描述信号定义,再用工具解析。Python生态里的cantools库支持加载DBC,一句db.get_message_by_name('EEC1').decode(data)就能得到所有物理量。我一般还会配合pandas把整个离线log文件解析成表格,直接看趋势曲线,排查偶发异常非常方便。
解析之外,过滤也很关键。TI MCAN的接收过滤可以按ID掩码只放行关心的报文,软件层面再做一层DLC和数据范围校验。我会在解析函数入口检查DLC是否等于预期值,防止被上位机格式错误的数据串扰。数据范围校验也建议加上,比如转速信号解析出9000 RPM,明显超出物理上限,就应当打印告警而不是直接使用。
5. CAN FD登场:升级前必须重新理解的差异
5.1 可变速率解决了什么问题
经典CAN2.0的数据场最多8字节,速率通常限制在1Mbps以内。汽车控制器里的标定、固件刷写动辄几百KB,一台车几十个ECU,刷写时间成了很大的痛点。CAN FD的核心改进就是把数据场扩展到最多64字节,并且允许数据阶段使用比仲裁阶段高得多的波特率。
注意"仲裁阶段"和"数据阶段"是两个速率。CAN FD帧里有一个BRS位,置1表示从BRS后的数据段开始切换到高速率,直到CRC结束再切回仲裁速率。这样设计的好处是:ID仲裁仍然使用较低速率,保证和其他节点的兼容性和远距离传输能力;而大批量数据用高速率传输,节省总线时间。我在实际项目中常配仲裁段500kbps、数据段2Mbps,同一根总线,有效吞吐比经典CAN提升明显。
5.2 从经典CAN迁移到CAN FD的兼容性边界
很多人的第一反应是"那我把所有节点升级成CAN FD就行"。但CAN FD帧对经典CAN控制器而言是无法识别的。如果总线上有一个经典CAN节点,它看到FD帧的速率突变和更长的数据场,会产生错误帧甚至进入bus-off。这意味着CAN FD要么全网络匹配,要么就不能用FD格式,只能以经典CAN格式通信。
TI的MCAN模块默认能同时收发经典CAN帧和CAN FD帧,但如果软件配置里没有正确打开CAN FD使能选项,发送FD帧时会被直接拒绝。所以迁移前需要确认三件事:所有控制器的CAN FD功能是否使能、所有收发器的带宽是否够快、所有节点的DLC处理逻辑是否支持8字节以上的数据。第三个点常常被忽略,很多旧代码用数组uint8_t data[8]固定存放,DLC大于8时会溢出。
5.3 硬件层面:不只是换个控制器那么简单
经典CAN时代,很多节点用一个几块钱的收发器就很稳定。但CAN FD把数据阶段速率提高到5Mbps甚至8Mbps后,信号边沿变得非常陡,反射和振铃的影响被放大。普通收发器的TXD如果出现故障被拉死,整个总线都会被锁住,所以CAN FD收发器通常都要求带TXD显性超时保护。
选型时我会注意这几个参数:是否支持CAN FD、TXD显性超时是否内置、总线引脚最大耐压、共模输入范围。TI TCAN1042系列是5V供电;TCAN334系列是3.3V供电,速率同样支持到5Mbps。数据阶段高于5Mbps时,建议启用控制器的TDC(Transmitter Delay Compensation)功能,用来补偿收发器反馈回环的延迟,否则高速采样点会漂移。
一个对比表可以帮助决策:
| 项目 | 经典CAN | CAN FD |
|---|---|---|
| 数据场最大长度 | 8字节 | 64字节 |
| 典型最高速率 | 1Mbps | 5Mbps~8Mbps |
| 仲裁速率和数据速率 | 同一个 | 可分离 |
| CRC计算范围 | 数据场 | 数据场+填充计数器 |
| 经典节点接收FD帧 | 不兼容 | 不兼容 |
| 收发器要求 | 普通 | 要求快速边沿和TXD超时保护 |
6. 用TI SDK把最小工程跑起来:回环验证与寄存器快照
6.1 初始化外设之前,先确认三件事
无论你用TI的MSPM0 SDK、C2000的C2000Ware还是AM64x的MCU+ SDK,初始化的坑都差不多。第一是时钟。CAN模块挂在哪个时钟树上,外设时钟源选没选对,预分频是否在合理范围。很多"初始化失败"其实就是外设时钟没使能或者时钟源被改错了。
第二是引脚复用。新款MCU几乎所有引脚都有多种功能,CANRX和CANTX必须显式配置成外设功能,同时注意接口电平。TI的SysConfig图形化配置工具会生成对应的PINMUX代码,但我仍然建议在代码里加一个GPIO_getPinConfiguration回读,确认配置生效。因为有些板卡上有跳线或者外部电路会覆盖引脚功能。
第三是收发器使能。很多CAN收发器上有STB或EN引脚,用于静音或待机模式。如果这个引脚悬空或者被默认拉高,总线就收不到你的数据。确认初始化正常后,先做一个物理层测试:用示波器量TXD引脚有没有方波输出,量收发器侧的CAN_H/CAN_L有没有差分变化,一层一层向上排查。
6.2 内环自测:不接总线先把软件调对
TI SDK里的CAN例程通常会提供Loopback(回环)模式。所谓Loopback,就是控制器发出的帧不经过总线,在芯片内部直接绕回接收路径。这个模式的价值在于:它能验证你的位时序配置、发送接收中断、DMA路径、滤波配置是否正确,而不受硬件连接干扰。
推荐一个标准的自测流程:配置回环模式,初始化MCAN后发送一帧已知ID固定数据,在接收中断里把收到的帧和发送值比对。如果一致,说明外设链路是通的;如果不一致,先查位时序寄存器的值,再查滤波配置——很多人回环下收不到帧,就是因为过滤把发送的ID给屏蔽了。
我在回环模式下还会同时开几个不同ID的消息循环发送,每个ID附带自增计数,接收端校验计数是否连续。这样能顺带验证FIFO是否溢出、中断是否积压。回环跑上几分钟不出错,再切到普通模式接总线。这个流程看起来慢,但比直接上总线排错快得多。
6.3 寄存器快照:让外设在现场开口说话
现场出现问题,与其盲目改代码,不如先让外设自己"交代"状态。TI MCAN模块有几个关键寄存器:ECR(Error Counter Register)里TEC是发送错误计数,REC是接收错误计数;PSR(Protocol Status Register)里LEC是最近错误代码,BO表示是否处于bus-off状态。
我会在CAN通信的定时器中断里,每隔100ms采样一次这几个寄存器,和正常状态对比。如果TEC或REC持续增长,说明总线上有异常正在累积;如果LEC出现CRC错误,多半是位时序或物理层问题;如果BO置位,说明错误计数已经超过256,这时候控制器会主动脱离总线,必须软件触发恢复流程。
有一个小技巧:把寄存器快照做成一个诊断指令。通过串口输入某个命令,设备立刻返回当前波特率配置、ECR、PSR以及最近一次收发帧的ID和时间戳。很多现场问题可以靠这一条指令远程缩小范围,不用扛着示波器到处跑。TI的官方例程形式上是can_ex1_loopback这类基础工程,但我会在此基础上把"状态快照"加进去,长期来看收益很大。
7. 实战中反复出现的三类故障:完整排查链路复盘
7.1 "can not open com port"这类接入层报错
USB-CAN分析仪在Windows上偶尔会报can not open com port,这类报错看起来像是设备问题,其实大部分是软件和驱动层面的问题。我的排查顺序是:先打开设备管理器,看USB设备是否被正确识别。如果显示未知设备,重新安装厂商驱动;如果显示正常但软件打不开,可能是其他工具占用了同一个串口号,比如你同时开了一个串口助手。
接下来看波特率设置,工具软件里选择的CAN通道波特率必须和实际网络一致。如果某台分析仪还支持多通道,要确认当前选的通道序号对应哪路CAN。另外要注意供电,部分USB-CAN模块在笔记本电脑只插USB时会供电不足,表现为打开成功但示波器看总线没有信号或者电压偏低,这种情况换一个带独立供电的USB HUB就解决了。
7.2 初始化失败和偶发bus-off的定位过程
CAN初始化失败,先确认是软件返回失败还是通信失败。软件返回失败通常是参数不合法,比如位时序里某个段的值超过寄存器位数限制,或者采样点计算出来的Seg1值太小。通信失败则要按物理层排查:供电是否到位、终端电阻是否在总线两端、CAN_H和CAN_L是否接反、示波器能不能抓到显性差分波形。
偶发bus-off是最折磨人的一种故障。bus-off不是瞬间发生的,错误计数器从0涨到256是一个逐渐累积的过程。通常先出现错误帧,然后进入error passive,再往后才bus-off。所以排查的核心是抓住"第一个错误帧"出现时的场景。我会开启控制器的协议状态中断,记录LEC字段的值。如果大量CRC错误,重点查波特率偏移和线缆长度;如果大量bit错误,重点查收发器、触点接触、总线电平是否被拉偏。
7.3 一个真实案例:波特率偏移1%引发的随机掉线
分享一个我踩过的大坑。有一个项目,单节点回环测试完全正常,接两个节点后偶发丢帧,接四个节点后两三个小时就整个网络bus-off一次。示波器抓波形,总线上电平也算干净,终端电阻也在。最后我在每个节点上打印ECR,发现只有某个节点发送时REC持续增长。
查到最后是那个节点的位时序预分频寄存器写错了一位。目标500kbps,实际波特率变成了506kbps左右,偏差约1.2%。单节点回环测试时,发送和接收是同一个控制器,自己发的帧自己收,永远同步,根本不会报错。一旦上总线,别的节点必须用500kbps去采样它506kbps的信号,短报文勉强能对上,报文稍微长一点,相位积累超过采样余量就产生CRC错误。
这个案例给我的教训是:回环测试只证明"控制器内部链路通",不能证明"波特率真的准"。验证波特率最直接的办法是开启发送功能后,用示波器量TXD或者CAN_H/CAN_L上的位时间和标准值对比。一个bit的实际时长差一点都能看出来。另一个办法是故意让两个节点互发大量随机数据,观察错误计数器是否长时间保持0,这个压力测试比回环自测可靠得多。
故障排查到最后,决定效率的往往是是不是有系统的方法。先隔离变量,再逐层验证,最后用错误计数器定位问题节点。CAN总线是个集体协议,一个节点配置错误,整个网络都会跟着遭殃。这也是为什么我现在接新项目时,第一件事就是先做一个节点自检固件,把位时序回读、回环发送、错误计数器快照、总线电平等信息全部做成可查询的指令。哪个现场出了问题,一条指令就能拿到第一手证据,而不是靠猜。