第三章 诊断通信机制:请求与响应模型、物理寻址与功能寻址、定时参数、网络层与传输层接口

各位同学,咱们今天聊聊诊断通信的核心机制。说白了,就是ECU之间怎么“说话”,怎么“听懂”对方的话。我刚开始接触UDS时,觉得不就是发个请求收个响应嘛,有啥难的?结果第一次调功能寻址,整车十几个ECU同时回我,CAN总线直接爆了……嗯,从那以后我再也不敢小看这些基础概念了。

3.1 请求与响应模型:一问一答的规矩

UDS的通信模型,本质上就是“客户端-服务器”模式。诊断仪(Tester)是客户端,ECU是服务器。Tester发一个请求,ECU回一个响应。就这么简单?其实不然。

这里有个关键点:一个请求,必须对应一个响应。但响应分两种:

  • 肯定响应:ECU说“我收到了,并且干完了”。格式是 [SID + 0x40] + 数据。比如请求读取 DID 0xF190,SID是0x22,肯定响应就是0x62 + 数据。
  • 否定响应:ECU说“我收到了,但干不了”。格式是 0x7F + 请求SID + NRC。NRC就是错误码,比如0x31表示请求超出范围。

我个人习惯:在写代码时,永远先处理否定响应。因为肯定响应只有一种,否定响应却有几十种。你想想看,如果ECU回了0x7F 0x22 0x31,你却当成肯定响应去解析,那程序直接就崩了。

举个例子,读取VIN码(DID 0xF190)的完整流程:

// Tester发送请求
CAN ID: 0x7E0  // 物理寻址,指向特定ECU
数据: 0x22 0xF1 0x90

// ECU响应(肯定)
CAN ID: 0x7E8
数据: 0x62 0xF1 0x90 0x4C 0x46 0x56 0x33 0x32 0x30 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38

// 或者ECU响应(否定)
CAN ID: 0x7E8
数据: 0x7F 0x22 0x31  // 请求超出范围

3.2 物理寻址与功能寻址:点对点 vs 一对多

这是诊断通信里最容易搞混的地方。我当年面试新人,十个有八个分不清。

物理寻址:就是点对点通信。Tester发一个请求,只有一个ECU响应。CAN ID是固定的,比如0x7E0发,0x7E8收。每个ECU有自己唯一的物理请求ID和物理响应ID。

功能寻址:就是一对多通信。Tester发一个请求,所有支持该功能的ECU都会响应。CAN ID是广播式的,比如0x7DF。所有ECU都监听这个ID,收到后各自处理并响应。

避坑指南:我曾经在一个项目中,用功能寻址去刷写所有ECU的固件。结果20个ECU同时回响应,CAN总线负载率直接飙到90%以上,丢帧严重。后来我改成物理寻址逐个刷写,才解决问题。功能寻址适合查询类操作,不适合大数据量传输。

什么时候用物理寻址?什么时候用功能寻址?我总结了一个经验:

场景 推荐寻址方式 原因
读取单个ECU的故障码 物理寻址 只需要一个ECU的数据
广播“进入扩展会话” 功能寻址 所有ECU都需要切换会话
刷写固件 物理寻址 避免总线冲突,保证数据完整性
读取所有ECU的软件版本 功能寻址 一次请求,获取所有信息

3.3 定时参数:P2、P3 这些数字到底啥意思?

定时参数,说白了就是“等多久”的问题。UDS协议里定义了好几个时间参数,我刚开始看规范时,P2、P2*、P3、P4……头都大了。其实核心就两个:

  • P2:Tester发完请求后,等待ECU响应的最长时间。默认是50ms。如果ECU在50ms内没回响应,Tester就认为超时了。
  • P2*:ECU告诉Tester“我还没准备好,再等等”。ECU可以发一个NRC 0x78(待处理)来延长等待时间。P2*一般是5秒。
  • P3:两次连续请求之间的最小间隔。默认是0ms,但实际项目中我一般设10ms,给ECU一点喘息时间。

注意:P2和P2*是ECU侧的时间,P3是Tester侧的时间。很多新手把P3当成ECU的响应时间,结果调了半天发现是Tester发太快了。

我记得有一次,客户反馈说ECU偶尔不响应诊断请求。我抓了CAN log一看,Tester在收到NRC 0x78后,只等了2秒就重发了。而ECU的P2*设的是5秒。嗯,这就是典型的Tester侧超时配置错误。

实际项目中,这些时间参数怎么配?我建议:

  • P2:50ms是标准值,但如果CAN总线负载高,可以放宽到100ms。
  • P2*:5秒是标准值,但刷写场景下可能需要10秒甚至更长。
  • P3:10ms足够,但如果是高速CAN(500kbps),可以设0ms。

3.4 网络层与传输层接口:数据怎么打包和解包?

UDS的应用层只管“发什么数据”,网络层和传输层管“怎么发数据”。在CAN总线上,一帧最多发8个字节。如果诊断数据超过8个字节,就需要分包和重组。

ISO 15765-2(也就是常说的CAN-TP)定义了四种传输协议:

  • 单帧(SF):数据长度 ≤ 7字节(CAN 2.0)或 ≤ 63字节(CAN FD)。直接发。
  • 首帧(FF):数据长度 > 7字节。第一帧告诉对方“我要发这么多数据”。
  • 连续帧(CF):后续的数据帧,每帧带一个序列号。
  • 流控帧(FC):接收方告诉发送方“你慢点发”或“你可以发了”。

举个例子,刷写一个256字节的固件块:

// Tester发送首帧(FF)
CAN ID: 0x7E0
数据: 0x10 0x0C 0x34 0x00 0x01 0x00 0x00 0x00
// 0x10表示首帧,0x0C表示总长度256字节(0x0100),后面是请求SID和数据

// ECU回流控帧(FC)
CAN ID: 0x7E8
数据: 0x30 0x00 0x00
// 0x30表示流控帧,0x00表示“继续发”,0x00表示块大小(0=无限制)

// Tester发送连续帧(CF)
CAN ID: 0x7E0
数据: 0x21 0x11 0x22 0x33 0x44 0x55 0x66 0x77
// 0x21表示连续帧,序列号=1

// ... 继续发送直到所有数据发完

我个人习惯:在实现网络层时,一定要处理好流控帧的“块大小”和“最小间隔时间”。有些ECU处理能力弱,你发太快它会丢帧。我曾经遇到一个ECU,块大小设成1才能正常工作,设成2就丢数据。后来发现是它的接收缓冲区太小了。

网络层接口,说白了就是三个函数:

  • N_UsData.request:应用层调用,告诉网络层“我要发这些数据”。
  • N_UsData.confirm:网络层回调,告诉应用层“数据发完了”或“发送失败”。
  • N_UsData.indication:网络层回调,告诉应用层“收到数据了”。

这三个接口,是UDS协议栈和CAN驱动之间的桥梁。我建议你在写代码时,把网络层单独封装成一个模块,不要和应用层混在一起。这样以后换CAN硬件,只需要改网络层,应用层不用动。

好了,这一章的内容就这些。下一章咱们聊聊诊断会话和安全访问,这两个是UDS里最常用的功能,也是面试必考题。