简介:ISO 14229-1 统一诊断服务(UDS)标准文档,面向汽车电子诊断开发、测试工程师及车载网络协议学习者,用于解决诊断服务接口不统一、跨设备通讯协议理解困难的问题。资源包内含1个PDF文件,大小约3.6MB,内容围绕UDS应用层展开,涵盖概述、应用层接口服务、应用层协议及各功能单元诊断服务目录等核心章节。文档详细梳理了基于OSI七层模型的诊断服务架构,重点讲解应用层协议数据单元(A_PDU)、应用层协议控制信息(A_PCI)与服务标识符(SI)的组成关系,并给出请求服务、响应服务、确认服务及子服务的分类说明,同时附有SI取值范围的完整定义表,如0x10至0x3E为ISO 14229-1服务请求、0x50至0x7E为肯定响应、0x7F为否定响应标识等。目前已有536人学习浏览,适合需要系统掌握UDS协议框架、对照标准开展诊断功能开发与测试的读者参考查阅。
1. 从一份 PDF 说起:UDS 诊断协议到底在车上干什么
如果你手上正躺着一份 ISO 14229-1 的 PDF,翻了几页发现全是服务列表、状态机、字节定义,大概率会冒出同一个疑问:这东西到底怎么落到我手里的 ECU 上?我最初接触它时也这样,文档看得懂每个字,连起来不知道从哪下手。ISO 14229-1 定义的是统一诊断服务(UDS),它规定了诊断仪和 ECU 之间用什么服务、什么格式、什么时序来对话。你车上刷隐藏功能、读故障码、做下线检测、OTA 前的版本校验,背后跑的都是这套东西。它不绑定物理层,CAN、CAN FD、DoIP 都能承载,所以同一套服务逻辑能横跨不同总线。这份 PDF 适合谁?做 ECU 诊断协议栈的嵌入式工程师、写诊断上位机的软件工程师、做产线 EOL 工位的测试工程师,以及需要理解诊断报文为什么这么发的标定人员。接下来我不复述标准目录,而是按“怎么把 PDF 里的服务变成能跑通的代码和测试”这条线拆开讲。
2. 把 PDF 里的服务表翻译成可调用的诊断请求
2.1 先分清服务原语、寻址方式和会话状态
ISO 14229-1 的核心是一张服务表,每个服务有 SID(Service Identifier),比如 0x10 是诊断会话控制,0x22 是按标识符读数据,0x2E 是按标识符写数据,0x27 是安全访问,0x31 是例程控制。PDF 里会把这些服务按功能分成诊断与通信管理、数据传输、存储传输、输入输出控制、例程调用、上传下载几大类。你不需要背,但必须理解三个前置概念,否则后面代码一定写歪。
第一是寻址方式。物理寻址是一对一,功能寻址是一对多。功能寻址下 ECU 不回复否定响应,这个特性在广播唤醒和批量清码时很有用,但如果你用功能寻址发 0x22 读数据,等不到回复不是 ECU 坏了,是协议就这么定的。第二是会话状态。默认会话、编程会话、扩展会话,不同会话下允许的服务集合不同。PDF 里每个服务都会标注它支持哪些会话,你在默认会话下发 0x2E 写数据,ECU 回 0x7F 2E 7F(服务不支持于当前会话)是正常行为。第三是安全等级。0x27 安全访问分奇数种子请求和偶数密钥发送,种子和密钥算法由 ECU 供应商定义,标准只规定交互流程,不规定算法本身。很多人卡在这里,以为 PDF 里能找到算法,找不到的。
2.2 用 Python 构造一条 0x22 读数据的完整请求
下面这段代码不依赖任何商业诊断库,只用标准 socket 和 can 库的常见封装思路,展示从服务表到实际字节的映射。我一般会先写一个请求构造器,把 SID、子功能、数据参数按顺序拼出来,再交给底层发送。
# uds_request_builder.py # 仅演示 UDS 请求构造逻辑,底层发送需对接具体 CAN/DoIP 驱动 class UdsRequest: def __init__(self, sid: int): self.sid = sid self.payload = bytearray([sid]) def add_subfunction(self, sub: int, suppress_pos_resp: bool = False): # 子功能字节最高位为抑制肯定响应位,PDF 中标注为 SPRMIB if suppress_pos_resp: sub = sub | 0x80 self.payload.append(sub) return self def add_data(self, data: bytes): self.payload.extend(data) return self def to_bytes(self) -> bytes: return bytes(self.payload) def build_read_data_by_identifier(did: int) -> bytes: # 0x22 服务:按标识符读数据,DID 占两个字节,高字节在前 did_hi = (did >> 8) & 0xFF did_lo = did & 0xFF req = UdsRequest(0x22).add_data(bytes([did_hi, did_lo])) return req.to_bytes() def build_diagnostic_session_control(session_type: int) -> bytes: # 0x10 服务:子功能 0x01 默认会话,0x02 编程会话,0x03 扩展会话 req = UdsRequest(0x10).add_subfunction(session_type) return req.to_bytes() if __name__ == "__main__": print("读 DID 0xF190 请求:", build_read_data_by_identifier(0xF190).hex()) print("进入扩展会话请求:", build_diagnostic_session_control(0x03).hex())这段代码的关键点在于字节序和子功能位。DID 是两字节大端,0xF190 拆成 F1 90,拼在 0x22 后面变成 22 F1 90。0x10 服务的子功能字节如果置了 0x80,ECU 收到后执行但不回肯定响应,只会在出错时回否定响应。这个抑制位在刷写流程里用得很多,因为刷写时大量请求不需要逐条确认,减少总线负载。参数上你要注意,DID 不是随便选的,0xF190 通常是 VIN,0xF187 是零件号,0xF18C 是 ECU 序列号,这些在 PDF 的附录或各主机厂诊断规范里有定义,ISO 14229-1 本身只规定 0x22 的报文格式,不规定 DID 分配。
2.3 否定响应码是排查问题的第一手线索
ECU 回 0x7F 加原 SID 加 NRC 时,不要急着抓总线,先看 NRC 值。0x11 服务不支持,说明你 SID 写错了或者 ECU 根本不实现这个服务。0x12 子功能不支持,常见于会话类型传错。0x13 报文长度或格式不对,多半是 DID 少发了一个字节。0x22 条件不满足,通常是会话或安全等级没到位。0x31 请求超出范围,DID 不存在。0x33 安全访问被拒,密钥算错了。0x78 响应挂起,不是错误,是 ECU 告诉你请求收到了但还在处理,你需要等它后续发最终响应。0x78 在刷写和例程控制里非常常见,很多新手看到 0x78 就以为失败,其实要接着等。PDF 里每个服务都会列出它可能返回的 NRC 列表,你写上位机时最好把这张表做成字典,收到否定响应直接查表,比翻 PDF 快得多。
3. 在 CAN 总线上跑通一次会话切换与安全访问
3.1 从默认会话切到扩展会话的时序要求
会话切换不是发一条 0x10 03 就完事。PDF 里规定了 P2Server 和 P2Server 两个时间参数。P2Server 是 ECU 收到请求后到发出响应之间的最大时间,典型值 50ms。P2Server 是 ECU 发出 0x78 挂起响应后,到发出最终响应之间的最大时间,典型值 5000ms。你的上位机如果 50ms 内没收到响应就超时重发,可能把 ECU 搞乱。我一般会在发送请求后启动一个定时器,P2 时间内等响应,收到 0x78 就把等待窗口切到 P2*,收到最终响应再切回来。这个逻辑在 PDF 的时序图里有,但很多人只看服务定义不看时序章节,结果在真实总线上偶发超时,查半天以为是硬件问题。
3.2 安全访问 0x27 的种子密钥交互实现
安全访问是 UDS 里最容易翻车的环节。流程是:上位机发 0x27 加奇数子功能(比如 0x01)请求种子,ECU 回 0x67 加子功能加种子字节;上位机用种子算出密钥,发 0x27 加偶数子功能(0x02)加密钥;ECU 校验通过回 0x67 0x02,不通过回 0x7F 0x27 0x33。下面是一个模拟的密钥计算框架,实际算法要替换成 ECU 供应商提供的逻辑。
# security_access.py # 演示 0x27 安全访问的交互骨架,密钥算法为占位实现 class SecurityAccess: def __init__(self, send_func, recv_func): self.send = send_func self.recv = recv_func def request_seed(self, level: int = 0x01) -> bytes: # level 为奇数子功能,0x01 通常对应第一级安全等级 self.send(bytes([0x27, level])) resp = self.recv() if resp[0] == 0x7F: raise RuntimeError(f"请求种子被拒,NRC={resp[2]:#04x}") # 肯定响应格式:0x67 + 子功能 + 种子 return resp[2:] def compute_key(self, seed: bytes) -> bytes: # 占位算法:实际项目替换为供应商提供的密钥派生逻辑 # 常见做法是查表、异或固定常量、或基于 AES 的变换 key = bytearray(seed) for i in range(len(key)): key[i] = (key[i] ^ 0x5A) & 0xFF return bytes(key) def send_key(self, level: int, key: bytes) -> bool: # level 为偶数子功能,与请求种子时的奇数子功能配对 self.send(bytes([0x27, level]) + key) resp = self.recv() if resp[0] == 0x67 and resp[1] == level: return True return False def unlock(self, seed_level: int = 0x01, key_level: int = 0x02): seed = self.request_seed(seed_level) key = self.compute_key(seed) return self.send_key(key_level, key)这段代码里 request_seed 发的是 0x27 0x01,ECU 回 0x67 0x01 加种子。send_key 发的是 0x27 0x02 加密钥。注意子功能必须配对,0x01 对 0x02,0x03 对 0x04,以此类推。密钥算法是占位,真实项目里种子长度可能是 4 字节、8 字节甚至 16 字节,算法可能是简单的异或加偏移,也可能是基于对称加密的挑战响应。PDF 不规定算法,只规定交互格式,所以你在不同 ECU 上会看到完全不同的密钥逻辑。踩坑最多的地方是种子字节序和密钥字节序不一致,有的 ECU 种子按小端发,密钥按大端收,你不看供应商文档根本猜不到。
3.3 用 0x31 例程控制触发 ECU 内部动作
0x31 例程控制是 UDS 里最灵活的服务之一。子功能 0x01 启动例程,0x02 停止例程,0x03 请求例程结果。例程标识符 RID 由两字节定义,比如 0xFF00 常用于擦除内存,0xFF01 用于校验内存,0x0203 用于自检。PDF 里只规定 0x31 的报文格式,RID 的具体含义由主机厂定义。我一般会在上位机里维护一张 RID 表,把每个 RID 对应的操作、参数、预期响应时间写清楚。比如擦除例程可能耗时几秒,ECU 会先回 0x78 挂起,再回 0x71 加 RID 加结果。如果你不等 0x78 后的最终响应,直接发下一条请求,ECU 可能返回 0x7F 0x31 0x22 条件不满足,因为上一个例程还没结束。
4. 避坑与排查:UDS 调试中最容易翻车的五个点
4.1 功能寻址发请求收不到回复
现象:用功能寻址发 0x10 03,总线上看不到任何响应,以为 ECU 掉线了。原因:ISO 14229-1 明确规定功能寻址下 ECU 不发送肯定响应,只在出错时发否定响应。如果你发的是合法请求,ECU 执行了但不回你。解决:需要确认响应时改用物理寻址,或者接受功能寻址只用于广播场景。排查时先看请求是不是合法,再看寻址方式是不是用错了。
4.2 0x78 挂起响应被当成失败
现象:发 0x31 01 FF00 擦除例程,收到 0x7F 0x31 0x78,以为 ECU 拒绝了。原因:0x78 是响应挂起,表示请求已接受但处理未完成,不是否定响应。解决:收到 0x78 后继续等待,把超时窗口从 P2 切到 P2*,直到收到最终肯定或否定响应。很多诊断库默认把 0x7F 开头的都当否定响应,需要手动把 0x78 排除。
4.3 安全访问种子长度和密钥长度不匹配
现象:请求种子成功,发密钥后一直回 0x7F 0x27 0x33。原因:密钥长度和种子长度不一致,或者字节序搞反了。有的 ECU 种子 4 字节,密钥也是 4 字节,但种子按大端发,密钥按小端收。解决:抓总线看种子原始字节,对照供应商文档确认密钥生成规则和字节序。如果文档没写,用已知正确的诊断仪抓一次交互,对比你的密钥和它的密钥差在哪。
4.4 会话超时导致后续请求全部失败
现象:进入扩展会话后,隔了几秒发下一条请求,回 0x7F 0x22 0x22 条件不满足。原因:UDS 有 S3Server 会话超时,典型值 5000ms,超时后 ECU 自动回默认会话。解决:在会话期间定期发送 0x3E 0x80 保持会话,0x3E 是 tester present,子功能 0x80 抑制响应。发送间隔要小于 S3Server 超时时间,一般设 2000ms 发一次比较稳。
4.5 多帧传输时流控帧参数不匹配
现象:用 0x22 读长数据时,ECU 回了首帧,但后续收不到连续帧。原因:ISO 15765-2 的流控帧参数不匹配,块大小和间隔时间没协商好。解决:检查上位机发的流控帧里 BS 和 STmin 是否合理。BS 为 0 表示不再发流控帧,ECU 连续发完;BS 非 0 表示每发 BS 个连续帧后要再等流控帧。STmin 是连续帧之间的最小间隔,设太小 ECU 可能处理不过来,设太大传输效率低。常见做法是 BS 设 0,STmin 设 10ms 左右,具体看 ECU 能力。
5. 从能跑到好用:把 UDS 诊断做成可复用的测试资产
5.1 用配置表驱动诊断请求,而不是硬编码
当你调通几条服务后,下一步是把它们变成可维护的资产。我习惯把每个服务的 SID、子功能、DID、RID、预期响应、超时时间、会话要求、安全等级写进一张 YAML 或 CSV 配置表,上位机读表生成请求和校验逻辑。这样换一个 ECU 项目,只改配置表,不改代码。下面是一个配置表的结构示例。
| 服务名 | SID | 子功能 | 数据参数 | 会话要求 | 安全等级 | 超时(ms) | 预期响应 |
|---|---|---|---|---|---|---|---|
| 读VIN | 0x22 | 无 | DID=0xF190 | 默认/扩展 | 无 | 50 | 0x62 F1 90 + 17字节 |
| 进扩展会话 | 0x10 | 0x03 | 无 | 默认 | 无 | 50 | 0x50 03 |
| 安全访问一级 | 0x27 | 0x01/0x02 | 种子/密钥 | 扩展 | 无 | 50 | 0x67 02 |
| 擦除内存 | 0x31 | 0x01 | RID=0xFF00 | 编程 | 一级 | 5000 | 0x71 FF00 00 |
这张表的价值在于,它把 PDF 里的服务定义和你的项目特定参数分开了。SID 和报文格式来自 ISO 14229-1,DID、RID、会话要求、安全等级来自主机厂规范。你维护的是后者,前者是标准固定的。当标准更新时,你只需要检查 SID 和格式有没有变,项目参数不受影响。
5.2 用模拟 ECU 做回归测试
真实 ECU 不是随时能用的,尤其是编程会话和擦除例程,跑一次有风险。我一般会写一个轻量模拟 ECU,用 Python 的 socket 或虚拟 CAN 接口,按配置表响应请求。模拟 ECU 不需要实现全部服务,只实现你当前测试用例涉及的那几条。比如你测安全访问,模拟 ECU 就实现 0x27 的种子生成和密钥校验,种子可以固定,密钥算法和真实 ECU 一致。这样你可以在没有硬件的情况下跑通上位机逻辑,等硬件到位再切到真实总线。模拟 ECU 的另一个好处是能构造异常场景,比如故意回 0x78 挂起、故意回错误 NRC、故意延迟响应,验证上位机的超时和重试逻辑。
5.3 诊断日志的抓取和回放
调试 UDS 时,总线日志是最可靠的后悔药。我习惯在发送和接收的每个字节上打时间戳,记录方向、寻址方式、原始字节、解析后的服务名和 NRC。日志格式用 CSV 或 JSON Lines,方便后续用脚本分析。回放时把日志里的请求按时间顺序重新发一遍,对比响应是否一致。这个手段在排查偶发问题时特别有用,因为 UDS 的很多问题不是必现的,跟总线负载、ECU 状态、温度都可能有关系。你抓一次出问题的日志,回放十次,如果能复现,问题范围就缩小了。如果回放不复现,那问题可能在硬件或环境,不在协议逻辑。
5.4 一个具体技巧:用 0x3E 0x80 维持会话的同时做心跳检测
0x3E 是 tester present 服务,子功能 0x80 表示抑制肯定响应。你发 0x3E 0x80,ECU 收到后重置 S3Server 定时器,但不回任何报文。这个特性可以拿来做两件事:一是维持会话不超时,二是做心跳检测。如果你发 0x3E 0x80 后,下一条正常请求收到 0x7F 0x22 0x22,说明会话已经掉了,ECU 没收到你的心跳或者心跳间隔超过了 S3Server。我一般把心跳间隔设成 S3Server 的三分之一,比如 S3Server 是 5000ms,心跳就 1500ms 发一次。这样即使丢一两个心跳,会话也不会掉。注意 0x3E 0x80 在功能寻址下也能用,但功能寻址本身不回响应,所以你无法从响应判断心跳是否被收到,只能靠后续请求的 NRC 反推。
我自己的习惯是,每接一个新 ECU,先不急着跑复杂例程,而是用 0x10 03 进扩展会话,0x3E 0x80 维持,0x22 读几个已知 DID,0x27 过一遍安全访问,把这四条跑稳了再往下做。这四条覆盖了会话管理、心跳、读数据、安全访问,是 UDS 最基础的骨架。骨架不稳,后面刷写和例程控制一定出问题。希望帮到你。
本文还有配套的精品资源,点击获取