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分别是什么?
答案其实就在这一章里,你品,你细品。