Chapter 11 · CAN 总线 / 汽车电子

UDS 诊断(ISO 14229)

比 OBD 强大得多的车厂级诊断协议:诊断会话、seed-key 安全访问、按 DID 读写数据、例程控制、读 DTC、否定响应 NRC,以及支撑一切的 ISO-TP 多帧传输。

课程进度69%

关键概念

UDS (Unified Diagnostic Services, ISO 14229)
统一诊断服务,是车厂在生产线、售后、刷写固件时使用的标准诊断协议。OBD-II 只暴露排放相关的只读数据,而 UDS 提供了读写任意数据、执行例程、刷写 ECU、安全访问等完整能力。它定义在应用层(ISO 14229-1),在 CAN 上通过 ISO 15765(ISO-TP)承载。
SID (Service Identifier)
UDS 请求的第一个字节,标识要调用哪个服务。如 0x10 会话控制、0x22 读数据、0x2E 写数据、0x27 安全访问、0x31 例程控制、0x19 读 DTC。正响应的 SID = 请求 SID + 0x40(如 0x22→0x62);否定响应固定为 0x7F
DiagnosticSessionControl (0x10)
切换诊断会话。0x01 默认会话(Default,上电默认态,只能读基本数据);0x02 编程会话(Programming,用于刷写固件);0x03 扩展会话(Extended,解锁大多数诊断服务)。切换会话后需周期发送 TesterPresent(0x3E) 保活,否则会话超时(S3 定时器,通常 5 秒)回落到默认会话。
SecurityAccess (0x27) — seed & key
安全访问,防止未授权者随意读写敏感数据或刷写固件。流程:诊断仪请求 seed(0x27 + level,奇数子功能如 0x01 请求种子),ECU 返回随机 seed;诊断仪用只有车厂知道的算法把 seed 算成 key,回发(偶数子功能如 0x02 发送密钥);ECU 校验 key 通过后「解锁」。算法保密是安全的核心。
DID (Data Identifier) 与 0x22 / 0x2E
DID 是 2 字节标识符,指定一个数据项(如 0xF190 = VIN、0xF187 = 零件号)。ReadDataByIdentifier (0x22) 按 DID 读取,WriteDataByIdentifier (0x2E) 按 DID 写入。相比 OBD 的 1 字节 PID,DID 空间大得多(65536 个),车厂可自由定义。
RoutineControl (0x31) 与 ReadDTC (0x19)
RoutineControl (0x31) 执行 ECU 内置例程(如擦除 Flash、自检、标定),子功能 0x01 启动、0x02 停止、0x03 查询结果,配合 2 字节 RID(Routine ID)。ReadDTCInformation (0x19) 读故障码,子功能 0x02 按状态掩码读 DTC 及其状态字节,比 OBD Mode 03 提供更丰富的故障信息(老化计数、测试状态等)。
否定响应(0x7F + NRC)
当 ECU 无法执行请求时返回 0x7F + 原始SID + NRC。NRC(Negative Response Code)说明原因:0x11 服务不支持、0x22 条件不满足、0x33 安全访问被拒、0x31 请求超出范围、0x78 请求已收到但仍在处理(responsePending,诊断仪需继续等待)。0x78 是最常见的「稍候」信号。
ISO-TP (ISO 15765-2) 传输层
CAN 单帧最多 8 字节(经典 CAN),而 UDS 报文常常几十上百字节。ISO-TP 负责把长报文拆成多帧:单帧 SF(≤7 字节,PCI 高半字节 0);首帧 FF(PCI 0x1x,携带总长度);连续帧 CF(PCI 0x2x,带 0–15 循环序号);流控帧 FC(PCI 0x3x,接收方回给发送方,含 FS/BS/STmin)。

UDS 常用服务(SID)一览

SID服务名正响应用途
0x10DiagnosticSessionControl0x50切换默认/编程/扩展会话
0x11ECUReset0x51软复位/硬复位 ECU
0x14ClearDiagnosticInformation0x54清除 DTC
0x19ReadDTCInformation0x59按状态读取故障码
0x22ReadDataByIdentifier0x62按 DID 读数据
0x27SecurityAccess0x67seed-key 安全解锁
0x28CommunicationControl0x68开关正常报文收发
0x2EWriteDataByIdentifier0x6E按 DID 写数据
0x31RoutineControl0x71启动/停止/查询例程
0x34/36/37Request/Transfer/RequestExitData0x74/76/77固件刷写三部曲
0x3ETesterPresent0x7E会话保活
0x7FNegativeResponse否定响应载体

seed-key 安全访问握手

要写数据或刷固件,必须先切到扩展/编程会话,再通过 seed-key 解锁。整个握手如下:

SecurityAccess 0x27 seed-key 四步握手 ═══════════════════════════════════════════════════════════ 诊断仪 (0x7E0) ECU (0x7E8) │ │ ① │ ── 10 03 ─────────────────────────► │ 切扩展会话 │ ◄──────────────────── 50 03 ...... │ 正响应 │ │ ② │ ── 27 01 ──────────────────────► │ 请求 seed(level1) │ ◄──────── 67 01 3A F2 91 5C ────── │ 返回 4 字节 seed │ │ │ [ 本地算法: key = f(seed, 车厂密钥) ] │ │ │ ③ │ ── 27 02 8B 1D E0 44 ───────────► │ 发送 key │ ◄──────────────────── 67 02 ─────── │ 解锁成功 │ │ ④ │ ── 2E F1 90 ...(写 VIN)───────────► │ 现在可写敏感数据 │ ◄──────────────────── 6E F1 90 ─── │ 写入成功 若 key 错误 → 7F 27 35 (invalidKey) 连续错误多次 → 7F 27 36 (exceedNumberOfAttempts),锁定延时
seed-key 不是加密

seed-key 只是「挑战-应答」的访问控制,不是数据加密。算法一旦泄露(逆向 ECU 固件是常见手段),安全防线即被攻破。现代车厂正逐步转向基于非对称密码/证书的 SecOC(Secure Onboard Communication)与更严格的诊断认证(ISO 14229-1:2020 的 Authentication 0x29 服务)。

ISO-TP 多帧传输时序

当 UDS 响应超过 7 字节,ECU 先发首帧(含总长度),诊断仪回流控帧许可,ECU 再连续发连续帧。序号 SN 从 1 循环到 15 再回 0。

读 VIN(0x22 F190) 的 ISO-TP 多帧接收(响应 20 字节) ═══════════════════════════════════════════════════════════ 诊断仪 (0x7E0) ECU (0x7E8) │ ── 03 22 F1 90 (单帧请求) ───────► │ │ │ │ ◄─ 首帧 FF: 10 14 62 F1 90 57 44 42 │ 10=FF, 014=总长20 │ │ │ │ │ │ └─ 长度 0x014 = 20 字节 │ │ └──── PCI 高半字节 1 = 首帧 │ │ │ │ ── 流控帧 FC: 30 00 0A 00... ───► │ 30=FC │ │ │ └─ STmin=10ms(帧间隔) │ │ │ └──── BS=0 (一次全发,无需再控)│ │ └─────── FS=0 ClearToSend 允许 │ │ │ │ ◄─ 连续帧 CF: 21 42 41 31 32 33 34 35 │ 21=CF,SN=1 │ ◄─ 连续帧 CF: 22 36 37 38 39 30 41 42 │ 22=CF,SN=2 │ ◄─ 连续帧 CF: 23 43 44 00 00 00 00 00 │ 23=CF,SN=3 │ │ │ [ 拼接: 62 F1 90 + 17字节 VIN ] │ │ VIN = "WDB..." (ASCII 解码) │
流控帧三要素

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。接收方靠这三值控制发送方节奏,防止缓冲区溢出。

用 python-can + isotp 发起 UDS 请求

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}")

手动构造 ISO-TP 分包(理解底层)

不依赖库时,理解 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 */
  }
}
0x78 responsePending 必须正确处理

某些耗时服务(如擦除 Flash、执行例程)会先返回 7F xx 78 表示「已收到,仍在处理」。诊断仪收到 0x78 时不能判定失败,而应重置 P2* 超时定时器继续等待最终响应。写工具时若忽略 0x78,会误报刷写失败——这是量产诊断软件最常见的坑之一。

会话保活 TesterPresent

非默认会话下若一段时间(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 与证书认证。

⚒️ 配套开发者工具箱 · JSON / 正则 / 时间戳 / Cron 等 17 款免费在线工具