3、诊断协议基础(UDS on CAN):UDS服务概述、诊断会话控制(0x10)、安全访问(0x27)、例程控制(0x31)

各位同学,欢迎来到第三章。

前面我们聊了CAN通信的基础,也搭好了CANoe的仿真环境。但说实话,光能收发报文,那只是车联网的“皮毛”。真正让ECU“听话”的,是诊断协议。今天我们就来啃一啃UDS on CAN这块硬骨头。

UDS,全称是Unified Diagnostic Services,统一诊断服务。它定义了一套标准化的“指令集”,让测试设备(比如我们的CANoe)能和ECU进行深度对话。说白了,就是ECU的“管理后台”。

我个人习惯把UDS服务分成三类:控制类(比如切换模式、复位)、读写类(读写数据、内存)、执行类(运行例程)。今天我们先挑三个最核心、也最常用的服务来解剖:0x10、0x27、0x31。

3.1 UDS服务概述:一个请求,一个响应

UDS的通信模型很简单,就是“请求-响应”。

测试器(Tester)发一个请求报文,ECU收到后,必须回复一个响应报文。如果请求成功,就回肯定响应;如果失败,就回否定响应(NRC,Negative Response Code)。

这里有个关键点:UDS报文是承载在CAN报文的数据场里的。通常一个UDS服务请求,会占用CAN报文的8个字节中的前几个字节。

一个典型的UDS请求报文格式是这样的:

| 字节0 | 字节1 | 字节2 | ... | 字节7 |
| PCI (协议控制信息) | SID (服务ID) | 子功能/数据 | ... |

举个例子,请求诊断会话控制(0x10)到默认会话(0x01):

CAN ID: 0x7E0  // 物理请求ID
数据: 02 10 01 00 00 00 00 00
// 02: 表示后续有2个有效字节
// 10: 服务ID
// 01: 子功能,表示切换到默认会话

ECU的响应报文格式类似:

CAN ID: 0x7E8  // 物理响应ID
数据: 06 50 01 00 32 01 F4 00
// 06: 表示后续有6个有效字节
// 50: 肯定响应,服务ID + 0x40
// 01: 子功能,确认切换到默认会话
// 00 32 01 F4: 会话参数(比如P2超时时间等)

嗯,这里要注意:肯定响应的SID = 请求SID + 0x40。比如请求0x10,肯定响应就是0x50。如果ECU回复0x7F,那就是否定响应,后面会跟一个字节的NRC。

我的小技巧: 在CANoe的Trace窗口里,我习惯把UDS报文单独过滤出来看。右键点击报文,选择“Add as Filter”,只显示0x7E0和0x7E8这两个ID,这样诊断交互一目了然。

3.2 诊断会话控制(0x10):ECU的“工作模式”

ECU不是永远都在“全功能”状态的。为了安全、省电,它设计了不同的会话模式。0x10服务就是用来切换这些模式的。

常见的会话模式有:

子功能 会话名称 说明
0x01 默认会话 (Default Session) 上电后的初始模式,功能受限,通常只允许读一些基本信息
0x02 编程会话 (Programming Session) 用于刷写软件,通信速率可能改变,安全等级最高
0x03 扩展会话 (Extended Session) 用于诊断、标定,比默认会话开放更多服务

你想想看,为什么要有这些模式?

我在项目中遇到过一件事:有一次,一个ECU在默认会话下,死活不肯执行0x31例程。我查了半天手册,才发现——0x31服务在默认会话下是被禁用的。必须先切到扩展会话(0x03),才能执行。从那以后,我养成了一个习惯:做任何诊断操作前,先确认当前会话模式

切换会话的流程很简单:

  1. Tester发送0x10 + 子功能(比如0x03)
  2. ECU回复0x50 + 子功能 + 会话参数(P2时间、P2*时间等)
  3. 如果ECU不支持该会话,会回复NRC 0x12(子功能不支持)或0x22(条件不满足)
避坑指南: 我曾经在测试中,连续快速发送了两次0x10请求。结果ECU直接不理我了,回复了NRC 0x21(忙)。后来才知道,会话切换不是瞬间完成的,ECU内部需要时间处理。所以,发送下一个请求前,一定要等肯定响应回来。我一般会在CANoe里加一个100ms的延时。

3.3 安全访问(0x27):ECU的“门锁”

有些关键操作,比如刷写软件、解锁安全相关的配置,ECU不会让你随便碰。这时候就需要0x27服务——安全访问。

它的原理是“挑战-应答”。

流程是这样的:

  1. 请求种子: Tester发送0x27 + 子功能(比如0x01表示请求种子)
  2. ECU返回种子: ECU回复0x67 + 子功能 + 种子数据(通常是4个字节的随机数)
  3. 计算密钥: Tester根据种子,用约定的算法计算出密钥
  4. 发送密钥: Tester发送0x27 + 子功能+1(比如0x02)+ 密钥数据
  5. ECU验证: ECU内部用同样的算法计算密钥,比对。如果一致,回复0x67 + 子功能+1;否则回复NRC 0x35(无效密钥)

说白了,就是ECU给你出了一道题,你必须用正确的“公式”算出答案,才能开门。

这里有个坑:种子和密钥的算法是OEM(主机厂)保密的。不同项目、不同ECU,算法可能完全不同。有的用简单的异或,有的用AES加密,有的甚至用查表法。

我的经验: 在CANoe里模拟安全访问时,我通常会用CAPL写一个函数,专门计算密钥。比如:
// 伪代码示例
byte CalculateKey(byte seed[4]) {
  // 假设算法是:密钥 = 种子 ^ 0x5A5A5A5A
  dword key = (seed[0] << 24) | (seed[1] << 16) | (seed[2] << 8) | seed[3];
  key = key ^ 0x5A5A5A5A;
  // 返回密钥
  return key;
}
当然,实际项目中算法要复杂得多。但思路是一样的:先拿到种子,再算密钥,最后发回去
小提示: 有些ECU有“安全访问失败次数限制”。比如连续3次输错密钥,ECU会锁定一段时间(比如10秒),期间不再响应0x27请求。所以,测试时一定要确保算法正确,否则会被“关在门外”。

3.4 例程控制(0x31):让ECU“干活”

0x31服务,是让ECU执行一段预定义的“程序”。比如:擦除内存、检查软件完整性、执行自检等。

它有三个子功能:

子功能 名称 说明
0x01 启动例程 (Start Routine) 让ECU开始执行某个例程
0x02 停止例程 (Stop Routine) 强制停止正在执行的例程
0x03 请求例程结果 (Request Routine Results) 查询例程的执行结果(比如成功/失败,以及返回数据)

举个例子,启动一个“擦除内存”的例程:

请求: 02 31 01 FF 00  // 启动例程,例程ID = 0xFF00
响应: 06 71 01 FF 00 00 00 00  // 肯定响应,例程启动成功,状态为0x00(未完成?)

注意,例程ID(RID)是2个字节的,范围从0x0000到0xFFFF。具体每个ID代表什么功能,由OEM定义。

我遇到过最头疼的情况是:例程执行时间很长。比如擦除一个大的Flash块,可能需要几秒钟。这时候,ECU会在响应里告诉你“我还在忙”。

CANoe里怎么处理?我一般会用定时器轮询的方式:

  1. 发送0x31 01启动例程
  2. 收到肯定响应后,启动一个定时器,每隔100ms发送一次0x31 03(请求结果)
  3. 直到ECU返回“例程执行完成”的状态
注意: 有些例程是“不可中断”的。比如正在擦写Flash时,你发送0x31 02(停止例程),ECU可能会回复NRC 0x24(请求序列错误)。所以,启动例程前,一定要确认当前状态是否允许执行该例程

好了,今天的内容就到这里。我们讲了UDS的三大核心服务:0x10(会话控制)、0x27(安全访问)、0x31(例程控制)。

下一章,我们会继续深入,聊聊UDS的读写服务(0x22、0x2E)和传输层(ISO 15765-2)。到时候,我们会用CANoe实际搭建一个完整的诊断仿真环境。

记住:诊断是ECU的灵魂,而CANoe是你解剖灵魂的手术刀。好好练,别怕犯错。