1. UDS协议概述:从分层到安全访问

大家好,我是你们的讲师。今天咱们正式进入UDS的世界。说实话,UDS这玩意儿,刚接触时觉得它就是一堆ID和字节,枯燥得很。但干得越久,越觉得它设计得精巧。咱们这一章,先把地基打牢。

UDS,全称 Unified Diagnostic Services,统一诊断服务。它是汽车电子世界里,ECU和诊断仪之间沟通的“普通话”。你想想看,车上几十上百个ECU,来自不同供应商,如果没有统一标准,那调试和维修就是一场噩梦。

1.1 UDS协议栈分层

UDS不是孤零零的协议,它跑在OSI模型上。我个人习惯把UDS协议栈分成三层来看:

  • 应用层:这就是UDS核心,定义服务ID(SID)和具体功能。比如0x10(诊断会话控制)、0x27(安全访问)。
  • 会话层/表示层:ISO 15765-3 定义了如何把UDS消息打包。说白了,就是告诉你怎么把“我想读数据”这个请求,变成一串能发出去的字节。
  • 传输层:ISO 15765-2(DoCAN) 负责把数据拆包、组包。CAN报文一次只能发8字节,超过8字节怎么办?这里就用到多帧传输了。

核心要点:应用层只管“做什么”,传输层只管“怎么传”。分层解耦,这是嵌入式系统设计的黄金法则。

下面这张图,是我自己画的一个UDS协议栈分层结构,帮你快速建立整体认知。

UDS协议栈分层结构 应用层 UDS核心服务 (ISO 14229-1) 会话层 / 表示层 消息打包 (ISO 15765-3) 传输层 拆包/组包 (ISO 15765-2 DoCAN) 注意:实际项目中,这三层通常集成在同一个驱动模块中,但逻辑上必须清晰分离。

实战小贴士:我在调试Bootloader时,经常遇到“发请求没响应”的问题。90%的情况不是应用层逻辑错了,而是传输层多帧处理有bug。所以,先确认物理层和传输层通不通,再查应用层。

1.2 UDS服务分类

UDS服务,按功能可以分为六大类。我整理了一个表格,方便你对照记忆。

服务类别 典型SID 功能描述 我常用的场景
诊断和通信管理 0x10, 0x3E 会话控制、保持连接 刷写前先切到编程会话
数据传输 0x22, 0x2E 读写DID数据 读VIN码、写配置参数
存储数据传输 0x34, 0x36, 0x37 固件下载、上传 刷写Bootloader/APP
输入输出控制 0x2F 强制控制物理输出 测试阶段驱动继电器
远程激活 0x31 执行例程 擦除Flash、校验CRC
上传/下载 0x35, 0x36 大数据块传输 批量数据迁移

这里有个坑,我必须要说。0x3E(Tester Present)这个服务,很多人不重视。但在我参与的一个项目中,就因为刷写过程中没发0x3E,ECU超时退出了编程会话,导致刷写失败。嗯,从那以后,我写Bootloader时,都会在刷写循环里定时发送0x3E。

1.3 诊断会话控制 (0x10)

ECU不是任何时候都允许你刷写的。它有不同的“工作模式”,UDS里叫诊断会话。通过0x10服务,诊断仪可以切换这些会话。

常见的会话有三种:

  • 默认会话 (Default Session):上电后的初始状态。只允许读一些基本信息,比如VIN码。不能刷写,不能做安全访问。
  • 编程会话 (Programming Session):刷写固件的专用模式。在这个模式下,ECU会关闭一些非必要的通信,专心处理刷写请求。
  • 扩展会话 (Extended Session):用于一些高级诊断功能,比如读写标定参数、执行特殊例程。

注意:从默认会话切换到编程会话,通常需要先做安全访问(0x27)。否则ECU会拒绝切换。我曾经见过一个新手,直接发0x10 02,结果ECU回了NRC 0x33(安全访问拒绝),他愣了半天没反应过来。

会话切换的流程,我画了个简单的状态图。

诊断会话状态切换 默认会话 编程会话 扩展会话 0x10 02 0x10 03 超时 / 0x10 01 注意:编程会话通常需要先通过安全访问(0x27)才能进入。

1.4 诊断安全访问 (0x27)

安全访问,说白了就是一把锁。防止非授权设备随意刷写或读取敏感数据。它的流程是“挑战-应答”机制。

  1. 请求种子 (Seed):诊断仪发0x27 01(请求种子)。ECU返回一串随机数,比如4个字节。
  2. 计算密钥 (Key):诊断仪用特定算法,把种子算成密钥。
  3. 发送密钥 (Key):诊断仪发0x27 02,带上计算出的密钥。
  4. 验证结果:ECU内部用同样的算法计算,比对密钥。匹配则解锁,返回肯定响应;不匹配则返回NRC 0x35(无效密钥)。

关键点:算法是OEM或供应商自定义的。有的用简单的异或,有的用AES。我见过最狠的,种子和密钥算法里还混入了DID值和当前时间戳。

这里有个常见的坑。安全访问有次数限制。比如连续3次输错密钥,ECU会锁定一段时间,或者直接要求下电重启。我曾经在测试时,因为算法实现有个字节序错误,连续输错5次,结果ECU锁了10分钟。那十分钟,我盯着示波器,心里那个悔啊。

安全访问的报文格式,我写个例子给你看。

// 请求种子 (诊断仪 -> ECU)
// ID: 0x7E0 (物理请求)
// 数据: 0x27 0x01
// 解释: 请求安全访问,级别为01

// 肯定响应 (ECU -> 诊断仪)
// ID: 0x7E8 (物理响应)
// 数据: 0x67 0x01 0xAA 0xBB 0xCC 0xDD
// 解释: 0x67 = 0x27 + 0x40 (肯定响应标识)
//       0x01 = 安全级别
//       0xAA 0xBB 0xCC 0xDD = 种子 (4字节)

// 发送密钥 (诊断仪 -> ECU)
// ID: 0x7E0
// 数据: 0x27 0x02 0x12 0x34 0x56 0x78
// 解释: 0x12 0x34 0x56 0x78 = 计算出的密钥

// 肯定响应 (ECU -> 诊断仪)
// ID: 0x7E8
// 数据: 0x67 0x02
// 解释: 解锁成功!

避坑指南:我曾经在写Bootloader时,把种子和密钥的字节序搞反了。ECU是小端,我按大端算了密钥。结果死活解不了锁。后来用CANoe抓了报文,一对比才发现。所以,写代码前,一定先确认好字节序和算法细节。

好了,这一章的内容就到这里。UDS的分层、服务分类、会话控制和安全访问,是后续所有刷写操作的基础。把这些概念吃透,后面讲0x34、0x36这些刷写服务时,你才能游刃有余。