UDS 诊断(ISO 14229)
比 OBD 强大得多的车厂级诊断协议:诊断会话、seed-key 安全访问、按 DID 读写数据、例程控制、读 DTC、否定响应 NRC,以及支撑一切的 ISO-TP 多帧传输。
比 OBD 强大得多的车厂级诊断协议:诊断会话、seed-key 安全访问、按 DID 读写数据、例程控制、读 DTC、否定响应 NRC,以及支撑一切的 ISO-TP 多帧传输。
0x10 会话控制、0x22 读数据、0x2E 写数据、0x27 安全访问、0x31 例程控制、0x19 读 DTC。正响应的 SID = 请求 SID + 0x40(如 0x22→0x62);否定响应固定为 0x7F。0x01 默认会话(Default,上电默认态,只能读基本数据);0x02 编程会话(Programming,用于刷写固件);0x03 扩展会话(Extended,解锁大多数诊断服务)。切换会话后需周期发送 TesterPresent(0x3E) 保活,否则会话超时(S3 定时器,通常 5 秒)回落到默认会话。0x27 + level,奇数子功能如 0x01 请求种子),ECU 返回随机 seed;诊断仪用只有车厂知道的算法把 seed 算成 key,回发(偶数子功能如 0x02 发送密钥);ECU 校验 key 通过后「解锁」。算法保密是安全的核心。0xF190 = VIN、0xF187 = 零件号)。ReadDataByIdentifier (0x22) 按 DID 读取,WriteDataByIdentifier (0x2E) 按 DID 写入。相比 OBD 的 1 字节 PID,DID 空间大得多(65536 个),车厂可自由定义。RoutineControl (0x31) 执行 ECU 内置例程(如擦除 Flash、自检、标定),子功能 0x01 启动、0x02 停止、0x03 查询结果,配合 2 字节 RID(Routine ID)。ReadDTCInformation (0x19) 读故障码,子功能 0x02 按状态掩码读 DTC 及其状态字节,比 OBD Mode 03 提供更丰富的故障信息(老化计数、测试状态等)。0x7F + 原始SID + NRC。NRC(Negative Response Code)说明原因:0x11 服务不支持、0x22 条件不满足、0x33 安全访问被拒、0x31 请求超出范围、0x78 请求已收到但仍在处理(responsePending,诊断仪需继续等待)。0x78 是最常见的「稍候」信号。| SID | 服务名 | 正响应 | 用途 |
|---|---|---|---|
0x10 | DiagnosticSessionControl | 0x50 | 切换默认/编程/扩展会话 |
0x11 | ECUReset | 0x51 | 软复位/硬复位 ECU |
0x14 | ClearDiagnosticInformation | 0x54 | 清除 DTC |
0x19 | ReadDTCInformation | 0x59 | 按状态读取故障码 |
| 0x22 | ReadDataByIdentifier | 0x62 | 按 DID 读数据 |
0x27 | SecurityAccess | 0x67 | seed-key 安全解锁 |
0x28 | CommunicationControl | 0x68 | 开关正常报文收发 |
0x2E | WriteDataByIdentifier | 0x6E | 按 DID 写数据 |
0x31 | RoutineControl | 0x71 | 启动/停止/查询例程 |
0x34/36/37 | Request/Transfer/RequestExitData | 0x74/76/77 | 固件刷写三部曲 |
0x3E | TesterPresent | 0x7E | 会话保活 |
0x7F | NegativeResponse | — | 否定响应载体 |
要写数据或刷固件,必须先切到扩展/编程会话,再通过 seed-key 解锁。整个握手如下:
seed-key 只是「挑战-应答」的访问控制,不是数据加密。算法一旦泄露(逆向 ECU 固件是常见手段),安全防线即被攻破。现代车厂正逐步转向基于非对称密码/证书的 SecOC(Secure Onboard Communication)与更严格的诊断认证(ISO 14229-1:2020 的 Authentication 0x29 服务)。
当 UDS 响应超过 7 字节,ECU 先发首帧(含总长度),诊断仪回流控帧许可,ECU 再连续发连续帧。序号 SN 从 1 循环到 15 再回 0。
FS(Flow Status):0=继续发(CTS)、1=等待、2=溢出中止。BS(Block Size):连续发多少个 CF 后需再等一个 FC,0 表示一次发完。STmin(Separation Time min):相邻 CF 的最小间隔,0x00–0x7F 表示 0–127 ms,0xF1–0xF9 表示 100–900 μs。接收方靠这三值控制发送方节奏,防止缓冲区溢出。
can-isotp 库自动处理 FF/CF/FC 分包与重组,上层只需读写整段 UDS 报文。
# uds_read_vin.py —— 用 isotp 传输层读取 VIN(DID 0xF190) import isotp import can bus = can.Bus(channel="can0", interface="socketcan") # 物理寻址:发 0x7E0,收 0x7E8 addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=0x7E0, rxid=0x7E8) stack = isotp.CanStack(bus, address=addr, params={"stmin": 10, "blocksize": 0}) def uds_request(payload: bytes) -> bytes: stack.send(payload) # 自动分帧 while stack.transmitting(): stack.process() import time time.sleep(0.05) stack.process() return stack.recv() # 自动重组 # ① 切扩展会话 uds_request(bytes([0x10, 0x03])) # ② 读 VIN:22 F1 90 resp = uds_request(bytes([0x22, 0xF1, 0x90])) if resp and resp[0] == 0x62: # 0x22+0x40 正响应 vin = resp[3:].decode("ascii", errors="ignore") print("VIN:", vin) elif resp and resp[0] == 0x7F: print(f"否定响应 NRC=0x{resp[2]:02X}")
不依赖库时,理解 PCI 字节如何切分报文很重要。下面演示手写首帧/连续帧的字节布局。
/* isotp_tx.c —— 把长报文拆成 ISO-TP 帧发送 */ #include <stdint.h> #include <string.h> void can_send(uint32_t id, uint8_t *d, uint8_t dlc); /* 平台相关 */ void isotp_send(uint32_t txid, const uint8_t *data, uint16_t len) { uint8_t f[8]; if (len <= 7) { /* 单帧 SF:PCI 高半字节=0 */ f[0] = len; /* 0x0L,L=长度 */ memcpy(&f[1], data, len); can_send(txid, f, 8); return; } /* 首帧 FF:PCI = 0x1 | 长度高4位, 次字节=长度低8位 */ f[0] = 0x10 | ((len >> 8) & 0x0F); f[1] = len & 0xFF; memcpy(&f[2], data, 6); /* 首帧带 6 字节数据 */ can_send(txid, f, 8); /* 等待接收方流控帧 FC(0x30) —— 此处省略等待逻辑 */ uint16_t pos = 6; uint8_t sn = 1; while (pos < len) { /* 连续帧 CF:PCI = 0x2 | 序号 */ f[0] = 0x20 | (sn & 0x0F); uint8_t n = (len - pos > 7) ? 7 : (len - pos); memcpy(&f[1], &data[pos], n); can_send(txid, f, 8); pos += n; sn = (sn + 1) & 0x0F; /* 序号 1..15 循环回 0 */ } }
某些耗时服务(如擦除 Flash、执行例程)会先返回 7F xx 78 表示「已收到,仍在处理」。诊断仪收到 0x78 时不能判定失败,而应重置 P2* 超时定时器继续等待最终响应。写工具时若忽略 0x78,会误报刷写失败——这是量产诊断软件最常见的坑之一。
非默认会话下若一段时间(S3 定时器,通常 5 s)无诊断报文,ECU 会自动回落到默认会话并重新上锁。诊断仪需周期发 0x3E 0x80(子功能 0x80 = 不需响应)保活。
# 后台线程每 2 秒发一次 TesterPresent 保活 import threading, time def keep_alive(): while keep_running: # 3E 80:子功能 0x80 = suppressPositiveResponse uds_request(bytes([0x3E, 0x80])) time.sleep(2) keep_running = True threading.Thread(target=keep_alive, daemon=True).start()
UDS(ISO 14229)是车厂级诊断协议,能力远超 OBD:读写任意数据、执行例程、刷写固件。请求首字节是 SID,正响应 SID+0x40,否定响应固定 0x7F + SID + NRC。核心服务:会话控制 0x10(默认/编程/扩展),扩展/编程会话需 0x3E TesterPresent 保活避免 S3 超时;安全访问 0x27 用 seed-key 挑战-应答解锁(奇数子功能请求 seed、偶数发送 key);0x22/0x2E 按 2 字节 DID 读写数据;0x31 RoutineControl 执行例程;0x19 读 DTC。所有超过 7 字节的报文都靠 ISO-TP(ISO 15765-2)分帧:单帧 SF(PCI 0x0x)、首帧 FF(0x1x 带总长)、连续帧 CF(0x2x 带 1–15 循环序号)、流控帧 FC(0x3x 含 FS/BS/STmin)。NRC 0x78(responsePending)意味「稍候」,必须继续等待而非报错。安全访问只是访问控制而非加密,现代车厂正转向 SecOC 与证书认证。