第三章 诊断通信机制:请求与响应模型、物理寻址与功能寻址、定时参数、网络层与传输层接口
各位同学,咱们今天聊聊诊断通信的核心机制。说白了,就是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里最常用的功能,也是面试必考题。