2. UDS物理层与数据链路层:CAN总线基础、CAN帧结构、CAN-ID分配策略、网络层定时参数

好,咱们进入第二章。这一章讲的是UDS的根基——物理层和数据链路层。说白了,就是诊断报文在CAN总线上怎么跑起来的。很多新手一上来就研究应用层的服务,结果报文发不出去,或者发出去没响应,回头一看,原来是CAN帧结构搞错了,ID分配冲突了。嗯,我当年也踩过这个坑。

2.1 CAN总线基础:为什么是CAN?

你想想看,汽车上那么多ECU,发动机、变速箱、ABS、气囊……它们之间要通信。用点对点连线?那线束比车还重。所以需要一条总线,大家挂上去,谁有话谁说。CAN总线就是干这个的。

CAN(Controller Area Network)是德国Bosch公司在80年代发明的。它有几个特点,我挑重要的说:

  • 多主通信:任何节点都可以主动发消息,没有主从之分。这一点和LIN总线完全不同。
  • 差分信号:CAN_H和CAN_L两根线,抗干扰能力强。我在做EMC测试时深有体会,CAN总线在辐射骚扰下依然稳如老狗。
  • 非破坏性仲裁:多个节点同时发消息怎么办?谁的ID小(优先级高),谁继续发,其他的自动退让。这个机制非常巧妙。
  • 错误检测与重发:CRC校验、位填充、应答机制……出错会自动重发。我曾经在一条有问题的线束上抓过波形,CRC错误帧满天飞,但CAN控制器自己就处理了,应用层几乎无感。

重要:UDS诊断协议在CAN总线上运行时,通常使用CAN 2.0A标准(11位标识符),速率常见为250 kbps或500 kbps。速率越高,单帧能传的数据越多,但总线长度要相应缩短。

2.2 CAN帧结构:数据帧、远程帧、错误帧、过载帧

CAN总线上一共有四种帧类型。咱们做诊断,最常用的是数据帧。远程帧偶尔用来请求数据,但UDS里不常用。错误帧和过载帧是硬件自动处理的,咱们不用操心。

数据帧的结构,我习惯把它拆成几个部分来看:

字段 长度 说明
SOF 1 bit 帧起始,同步信号
仲裁场 11 bit (CAN 2.0A) 包含CAN-ID,决定优先级
控制场 6 bit 包含IDE、R0、DLC(数据长度码)
数据场 0~8 byte 实际要传输的数据,诊断报文就放在这里
CRC场 16 bit 循环冗余校验,保证数据完整性
ACK场 2 bit 应答位,接收节点拉低表示收到
EOF 7 bit 帧结束

这里有个关键点:DLC(数据长度码)。它告诉接收方数据场有多少个字节。UDS诊断报文通常不超过8字节,所以DLC最大就是8。如果报文超过8字节,就需要用到传输层协议(ISO 15765-2),这个我们后面会讲。

个人经验:我在调试一个项目时,发现ECU总是回复错误响应。抓了半天波形,发现是DLC设置错了——我发了8字节数据,但DLC写成了4。接收方只读了前4个字节,后面的数据全丢了。嗯,这种低级错误,排查起来最费时间。

2.3 CAN-ID分配策略:物理寻址 vs 功能寻址

这是UDS诊断里最核心的概念之一。CAN-ID决定了这条报文是发给谁的,以及谁有权回复。

咱们先看两种寻址方式:

  • 物理寻址(Physical Addressing):一对一的通信。诊断仪发一个请求,只有指定的那个ECU会响应。CAN-ID里包含了目标ECU的地址。
  • 功能寻址(Functional Addressing):一对多的通信。诊断仪发一个请求,总线上所有支持该功能的ECU都会响应。比如“读取所有ECU的故障码”,用功能寻址一次搞定。

那CAN-ID具体怎么分配呢?ISO 15765-3和ISO 11898给出了推荐方案,但各家OEM(主机厂)有自己的标准。我以最常见的11位CAN-ID为例,给你一个典型的分配策略:

用途 CAN-ID (11位) 说明
诊断请求(物理寻址) 0x7DF 诊断仪发给ECU,目标地址在数据场中
诊断响应(物理寻址) 0x7E8 ~ 0x7EF ECU回复给诊断仪,每个ECU一个ID
诊断请求(功能寻址) 0x7DF 诊断仪发给所有ECU
诊断响应(功能寻址) 0x7E8 ~ 0x7EF 每个ECU用自己的物理地址回复

你可能会问:为什么请求ID都是0x7DF?因为ISO标准里规定,0x7DF是功能寻址的请求ID。物理寻址的请求ID,其实也是0x7DF,但区别在于数据场的第一个字节(目标地址)不同。这个我们到网络层再细说。

避坑指南:我曾经在一个项目中,两个ECU的物理响应ID配成了同一个(都是0x7E8)。结果诊断仪一发请求,两个ECU同时回复,CAN总线上就冲突了。虽然CAN有仲裁机制,但数据会乱掉。所以,每个ECU的物理响应ID必须是唯一的。

2.4 网络层定时参数:N_As、N_Ar、N_Bs、N_Br、N_Cs、N_Cr

好,到了这一章最难啃的部分——定时参数。ISO 15765-2(网络层)定义了一套定时器,用来控制诊断报文的发送、接收和超时处理。说白了,就是规定“你多久必须回我”、“我等多久算超时”。

我整理了一张表,把常用的定时参数列出来:

参数 定义 典型值 说明
N_As 发送节点从请求发送到帧实际发出 0~1000 ms 取决于总线负载和优先级
N_Ar 接收节点从收到帧到开始处理 0~1000 ms ECU的CPU处理时间
N_Bs 发送节点等待接收节点的流控帧 0~1000 ms 用于多帧传输时的流控
N_Br 接收节点发送流控帧的时间 0~1000 ms 接收方准备好后发流控
N_Cs 发送节点等待接收节点的连续帧 0~1000 ms 多帧传输中,等待下一帧
N_Cr 接收节点发送连续帧的时间 0~1000 ms 接收方连续发送数据

这些参数看起来枯燥,但实际调试时非常关键。我举个例子:

假设诊断仪发了一个请求,ECU需要回复一个很长的数据(比如读取DTC故障码列表)。这个数据超过8字节,所以需要拆成多帧发送。第一帧(FF)告诉接收方总共有多少字节,然后接收方发一个流控帧(FC),告诉发送方“我准备好了,你发吧”。接着发送方一帧一帧地发连续帧(CF)。

在这个过程中,N_Bs就是发送方等待流控帧的时间。如果ECU处理慢,N_Bs设得太短,发送方就会超时,认为ECU没准备好,直接放弃。我遇到过一台老款ECU,它的N_Bs需要500ms以上,但诊断仪默认设了200ms,结果死活连不上。后来我把N_Bs改成1000ms,问题就解决了。

核心要点:定时参数不是随便设的。OEM会在诊断规范里给出具体值。作为开发人员,你要确保你的诊断仪和ECU的定时参数匹配。否则,轻则通信不稳定,重则完全无法通信。

2.5 实战:用Python模拟CAN帧发送

光说不练假把式。咱们用Python写一段代码,模拟发送一个UDS诊断请求(比如0x10 0x03,诊断会话控制)。这里用python-can库,假设你已经装好了。

import can

# 创建一个CAN总线接口(假设是PCAN或虚拟通道)
bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=500000)

# 构造一个UDS诊断请求:0x10 0x03(扩展诊断会话)
# CAN-ID = 0x7DF(功能寻址)
# DLC = 8(数据场长度)
# 数据场:0x02 0x10 0x03 0x00 0x00 0x00 0x00 0x00
# 注意:第一个字节0x02表示后续数据长度(2字节),这是ISO 15765-2的规范

msg = can.Message(
    arbitration_id=0x7DF,
    data=[0x02, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00],
    is_extended_id=False  # 使用11位标准ID
)

# 发送报文
try:
    bus.send(msg)
    print("诊断请求已发送:0x10 0x03")
except can.CanError:
    print("发送失败!检查总线连接。")

# 接收响应(假设ECU回复ID为0x7E8)
response = bus.recv(timeout=1.0)
if response is not None:
    print(f"收到响应:ID=0x{response.arbitration_id:X}, 数据={response.data.hex()}")
else:
    print("超时,未收到响应。")

这段代码很简单,但包含了几个关键点:

  • CAN-ID用0x7DF,这是功能寻址的请求ID。
  • 数据场的第一个字节是单帧长度(0x02表示后面有2个有效字节)。
  • 接收时,ECU会用0x7E8回复(物理寻址)。

个人习惯:我调试时喜欢先用虚拟CAN总线(比如socketcan或PCAN的虚拟通道)跑通流程,再连真实硬件。这样能避免把真实ECU搞出问题。你想想看,万一发了个错误报文把ECU锁死了,还得断电重启,多麻烦。

2.6 本章小结

这一章我们讲了CAN总线的基础、帧结构、ID分配策略和网络层定时参数。说白了,就是诊断报文在物理上怎么传输的。下一章我们会进入传输层(ISO 15765-2),看看超过8字节的数据怎么拆包、组包。嗯,那才是UDS协议的精髓所在。

最后留个思考题:如果总线上有两个ECU,它们的物理响应ID分别是0x7E8和0x7E9,诊断仪用功能寻址(0x7DF)发了一个请求,两个ECU都会回复吗?它们的回复ID分别是什么?

答案其实就在这一章里,你品,你细品。