UDS 诊断协议入门:从 CAN 帧到 ISO 14229 的完整实战

CAN 抓包之后,下一步就是 UDS

如果你看过站内那篇《OBD-II 与 CAN 总线抓包分析完整指南》,大概率会有同一个困惑:抓到了一堆 0x7E0、0x7E8 的报文,看起来有规律,但完全读不懂它在说什么。

原因很简单:CAN 只负责「把字节送过去」,不规定这些字节是什么意思。真正让诊断仪能和 ECU 对话的那套「语言」,叫 UDS(Unified Diagnostic Services,ISO 14229)。

打个比方:CAN 是公路,ISO-TP 是货运规则(一车装不下怎么拆包),UDS 是包裹上的单据格式。读懂 UDS,你才算能听懂车在说什么。

本文从帧结构一路讲到能跑通的 Python 代码,全部是可以在自己车上复现的流程。

一、三层结构,先建立地图

层 标准 职责 你看到的东西
应用层 ISO 14229 (UDS) 定义服务、参数、正负响应 22 F1 90、62 F1 90 ...
传输层 ISO 15765-2 (ISO-TP) 长报文拆包/重组,流控 PCI 字节 0x0N / 0x1X / 0x2X / 0x3X
数据链路层 ISO 11898 (CAN) 仲裁、CRC、ACK 帧 ID、8 字节数据
物理层 — CAN_H / CAN_L 差分信号 OBD 口 6/14 脚

关键认知:UDS 报文不是「一条 CAN 帧」。超过 7 字节的响应必须靠 ISO-TP 拆成多帧,这也是一堆人卡住的地方——看到 10 14 开头就懵了。

二、ISO-TP 的四种帧

CAN 一帧最多 8 字节,第一个字节被拆成「高 4 位 = 帧类型,低 4 位 = 参数」。

类型 PCI 高 4 位 含义 示例
单帧 SF 0 低 4 位是数据长度(≤7) 02 10 03:长度 2,数据 10 03
首帧 FF 1 低 4 位 + 下一字节 = 12 位总长度 10 14 62 F1 ...:总长 0x014 = 20 字节
连续帧 CF 2 低 4 位是序号(1 开始,循环 1–F) 21 ...、22 ...
流控 FC 3 告诉对方「可以继续发」 30 00 00:继续,块大小 0,间隔 0

一条 20 字节的 VIN 响应,实际长这样:

7E8  10 14 62 F1 90 57 50 30    # 首帧:总长 20
7E0  30 00 00                    # 接收方发流控:继续发
7E8  21 41 4C 30 30 30 30 30    # 连续帧 1
7E8  22 30 30 30 30 30 30 30    # 连续帧 2

拼起来就是 62 F1 90 + 17 字节 VIN。手工拼当然可以,但代码里用 ISO-TP 库更省事。

三、诊断地址:谁在跟谁说话

OBD 口的诊断报文走固定的一组地址(11 位 ID):

用途 请求 响应
功能寻址(广播给所有 ECU) 0x7DF 各 ECU 自己回
发动机 / 主 ECU(物理寻址) 0x7E0 0x7E8
变速箱 0x7E1 0x7E9
ABS / ESP 0x7E2 0x7EA

功能寻址适合「让所有 ECU 一起做某件事」(比如 3E 00 保活),物理寻址适合精确读某个 ECU 的数据。注意:功能寻址的请求不会有流控,也不支持多帧请求。

四、UDS 报文结构

一条 UDS 请求 = 服务 ID(SID)+ 子功能/参数。

  • 正响应:SID + 0x40,后面跟解析后的数据。例如 10 03 → 50 03 ...
  • 负响应:固定 7F 开头,格式 7F <原SID> <NRC>。例如 7F 22 31 表示「读 DID 请求超出范围」。

常见 NRC(否定响应码)

NRC 含义 排查方向
0x10 generalReject 通用拒绝 报文内容不被接受
0x11 serviceNotSupported 这个 ECU 不支持该服务
0x12 subFunctionNotSupported 子功能号不对
0x13 incorrectMessageLength 长度不对,常见于多帧没拼对
0x22 conditionsNotCorrect 会话/车速/点火状态不满足
0x31 requestOutOfRange 参数越界(DID 不存在最常见)
0x33 securityAccessDenied 没做安全访问
0x78 responsePending 不是错误,ECU 还在算,继续等

0x78 值得单独说:ECU 处理耗时操作时会先回 7F xx 78,然后过几百毫秒再回正响应。不会处理 0x78 的诊断工具会把正常流程误判成失败。成熟的库(如 udsoncan)会自动等。

五、常用服务速查

SID 服务 典型用途
0x10 DiagnosticSessionControl 切会话:01 默认 / 02 编程 / 03 扩展 / 04 安全系统
0x11 ECUReset 复位 ECU
0x14 ClearDiagnosticInformation 清故障码
0x19 ReadDTCInformation 读故障码及其状态
0x22 ReadDataByIdentifier 最常用:按 DID 读数据(VIN、版本号、传感器值)
0x27 SecurityAccess 种子/密钥解锁
0x28 CommunicationControl 关闭/开启某类报文发送
0x2E WriteDataByIdentifier 写配置(高风险)
0x31 RoutineControl 执行例程:自检、学习、标定
0x3E TesterPresent 保活,防止 ECU 退回默认会话
0x34/0x36/0x37 RequestDownload / TransferData / Exit 刷写固件(极高风险)

记住一个规律:读服务(0x19 / 0x22)练手安全,写服务(0x2E / 0x31 / 0x34)可能把 ECU 写坏。初学阶段只碰读。

六、一次完整会话:读 VIN

在 Linux 上用 SocketCAN,先把 500 kbps 的 can0 起起来(绝大多数乘用车诊断 CAN 是 500k):

sudo ip link set can0 up type can bitrate 500000
candump can0

第 1 步:进入扩展会话

发送 7DF  [02] 10 03
接收 7E8  [06] 50 03 00 32 01 F4

解析:50 = 0x10 + 0x40,表示正响应;03 是当前会话;后面 4 字节是 P2/P2* 定时参数(这里 0x0032 = 50 ms,0x01F4 = 500 ms×10)。

第 2 步:读 VIN(DID = 0xF190)

发送 7E0  [03] 22 F1 90
接收 7E8  [10 14] 62 F1 90 57 50 30   # 首帧,20 字节
发送 7E0  [30 00 00]                   # 流控
接收 7E8  [21] 41 4C 30 30 30 30 30
接收 7E8  [22] 30 30 30 30 30 30 30

把 62 F1 90 后面的 17 个字节按 ASCII 拼起来,就是 VIN 码。这一步能跑通,说明你的硬件、波特率、ISO-TP 全链路都是对的。

七、用 Python 跑起来

手拼帧只适合验证,实际分析用 python-can + isotp + udsoncan 三件套。

pip install python-can isotp udsoncan
import can
import isotp
from udsoncan.connections import PythonIsoTpConnection
from udsoncan.client import Client
import udsoncan.configs

# 1) 打开 CAN
bus = can.Bus(interface="socketcan", channel="can0", bitrate=500000)

# 2) 配置 ISO-TP:物理寻址 7E0 -> 7E8
addr = isotp.Address(isotp.AddressingMode.Normal_11bits, txid=0x7E0, rxid=0x7E8)
stack = isotp.CanStack(bus=bus, address=addr, params={"blocksize": 0, "stmin": 0})

conn = PythonIsoTpConnection(stack)
conf = dict(udsoncan.configs.default_client_config)
conf["request_timeout"] = 3

with Client(conn, config=conf) as client:
    # 3) 进扩展会话
    client.change_session(0x03)
    print("会话已切换到扩展模式")

    # 4) 读 VIN
    r = client.read_data_by_identifier([0xF190])
    print("原始响应:", r.service_data)

    # 5) 读当前故障码
    dtc = client.get_dtc_by_status_mask(0xFF)
    print("DTC:", dtc.service_data)

几点实践说明:

  • udsoncan 会自动处理 0x78 和 ISO-TP 流控,比自己写状态机省太多。
  • 不同版本返回对象的字段名略有差异,建议先 print 出来再取。
  • Windows 上没有 SocketCAN,要用 interface="slcan"、"pcan" 或 "vector",并在 channel 里填串口名/通道号。
  • ICSim 这类纯 CAN 模拟器不支持 UDS——它只会伪造车速、门锁这些原始信号。要练 UDS,得用真实 ECU、台架,或者支持 UDS 的仿真工具。

八、安全访问 0x27 是怎么回事

很多服务(写配置、执行例程)要求先解锁。流程是标准的挑战-应答:

  1. 工具发 27 01(请求种子)
  2. ECU 回 67 01 <seed>
  3. 工具用某个算法把 seed 算成 key,发 27 02 <key>
  4. ECU 回 67 02 表示解锁成功,否则 7F 27 35(密钥无效)

算法由厂商定义,没有统一标准,常见做法是查表、异或固定值、或者基于 UDS 之外的私有算法。这也是为什么「破解」不同车型时,工作量和难度差异巨大。

从防御角度看,这一点很重要:把安全算法硬编码在 ECU 固件里,本质上只是增加门槛,不是安全边界。真正的防护要靠网关隔离、诊断防火墙和访问控制。

九、几个高频坑

现象 原因 处理
发出去没任何响应 波特率不对 / 用错了寻址方式 先 candump 看总线上有没有任何流量
只回 7F xx 11 该 ECU 不支持此服务 换 ECU 地址试,或确认车型是否走 UDS(老车可能是 KWP2000)
多帧响应读不全 没发流控,或块大小配错 用 ISO-TP 库自动处理,别手拼
会话总是掉回默认 超过 S3 定时(通常 5 s)没发保活 周期性发 3E 00
读 DID 回 7F 22 31 DID 不存在或该会话不可读 先 22 F1 86 读当前会话确认状态
操作后车辆报一堆故障 动了写服务 / 例程控制 断电重启,用 0x14 清码,必要时去 4S 恢复

十、风险与边界

这一节不是套话,是真会出事的部分:

  • 只读服务相对安全,写服务风险极高。0x2E 写错一个字节,可能让 ECU 进入不可用状态;0x31 执行错误例程,可能触发标定丢失。
  • 不要在行驶中操作。涉及制动、转向、动力系统的 ECU,任何异常都可能直接变成事故。
  • 只在自有车辆或书面授权的车辆上做。未经授权接入他人车辆诊断系统,可能触犯《网络安全法》与《刑法》第 285 条。
  • 刷写(0x34/0x36/0x37)不要碰,除非你有原厂工具和恢复方案。

十一、小结

把学习路径压缩成四步:

  1. 看懂帧:CAN 抓包 → 认出 0x7E0/0x7E8 → 理解 PCI 字节
  2. 跑通只读:10 03 切会话 → 22 F1 90 读 VIN
  3. 用库替代手拼:python-can + isotp + udsoncan
  4. 再谈写入:理解 0x27 与风险之后,才碰 0x2E/0x31

延伸阅读:


免责声明:本文仅用于自有车辆的诊断学习与授权范围内的安全研究。任何未授权的车辆接入、数据写入或功能篡改,均可能违法并造成人身财产损害,后果自负。

© 版权声明
THE END
喜欢就支持一下吧
点赞6赞赏 分享
相关推荐
评论 抢沙发

请登录后发表评论

    暂无评论内容