OBD诊断

OBD 最容易让人混淆的地方,是把那个 16 针插座、CAN 总线、诊断服务和扫描仪界面上的数值当成同一层东西。真到抓包时,只知道“插 OBD 读故障码”还不够,需要知道一个请求怎样变成总线上的字节,又怎样还原成转速、车速或故障信息。

下面用一组 经典 CAN、11 位标识符、正常寻址 下的离线报文串起接口与工作流程。字节是为说明字段人工构造的,不是某辆实车的抓包。

什么是 OBD?

OBD 是 On-Board Diagnostics,车载诊断系统。OBD-II/EOBD 的通用诊断围绕规定的监测与排放相关信息展开;常见的 16 针诊断连接器叫 DLC。厂商诊断工具能通过它执行更多功能,不意味着所有这些功能都属于通用 OBD 服务。

1996 年并不是全球统一的 OBD-II 起点;法规时间线会随地区、车型与动力类型变化,不能把某个市场的要求推广到全部车辆。

从 DLC 连接器到 CAN、ISO-TP,再到 OBD 服务和物理量

DBC 可以描述某些 CAN 信号编码,但诊断报文还涉及服务、参数和多帧重组,不能一概用“套 DBC”替代。

名称 解决的问题
DLC / SAE J1962 连接器 工具从哪里接入、电气接点在哪里
CAN 等底层通信 帧如何在网络上传输
ISO-TP 一条较长诊断消息如何拆成多帧并重组
OBD 诊断服务与 PID 请求哪类数据、数据如何解释
UDS 另一套诊断服务体系,可用于更广的 ECU 诊断功能

同一个插座可能接入不同协议,网关也可能限制可达范围。插上 DLC 不等于能直接看到整车所有内部网络。

接口针脚定义

DLC 正面示意,标出常见 CAN、地与电源针脚

图按车辆侧连接器正面、宽边朝上绘制。对着线束背面看时方向会反过来,实际接线仍以车型资料和连接器编号为准。

针脚 常见标准用途 备注
4 Chassis ground 车身地
5 Signal ground 信号地
6 / 14 CAN High / CAN Low 使用相应 CAN 诊断协议时
7 / 15 K line / L line 对应相关旧诊断协议,L line 并非总会使用
2 / 10 SAE J1850 Bus+ / Bus− 取决于实际支持的协议
16 车辆电源 不应当作 MCU 的 3.3 V/5 V 信号电源
其余位置 厂商用途等 不能从另一车型的接法直接照搬

1、3、11 针可能被厂商用于特定车型,但不能把这些用法当作通用映射。也不建议用“能点亮功率试灯”判断通信针脚:诊断时先确认原理图和测量方式,避免给信号线额外加载。标准连接器与 CAN OBD 的关系可参考 CSS Electronics 的 OBD2 说明。

OBD 如何工作?

ECU 根据传感器输入、内部状态和诊断监测逻辑生成数据。诊断工具请求后,ECU 返回对应信息。DTC 是诊断故障码,MIL 是故障指示灯;并不是任何一个 DTC 一出现,MIL 就立即按同一种方式点亮。

故障码通常描述监测到的异常类别或条件。看到某个传感器相关 DTC,不能直接得出“传感器坏了”:供电、线路、连接器、其他输入和监测前提都可能影响结果。

常见服务编号

下表统一使用十六进制。特别注意永久性故障码对应 0x0A,不要与 UDS 的 0x10 会话控制混淆。

服务 用途
01 当前动力系统数据
02 冻结帧数据
03 已存储的相关 DTC
04 清除相关诊断信息
05 特定旧协议的氧传感器监测结果
06 车载监测测试结果
07 当前或上一驾驶循环检测到的相关 DTC
08 特定系统或组件控制
09 车辆信息
0A 具有永久状态的相关 DTC

服务可用性取决于车辆及对应标准要求。读取故障和清除故障是不同操作:清除会改变诊断状态,不能把它当作测试通信是否正常的探针。

OBD-II 数据包:先拆开 CAN 与诊断负载

本节限定 11 位 CAN 示例。功能请求常使用 0x7DF,相关 ECU 响应通常位于 0x7E8~0x7EF;还存在 29 位 CAN 等其他配置,不能把这组 ID 当作所有诊断通信的固定地址。

读取发动机转速,服务 01、PID 0C:

1
2
请求 CAN ID 7DF:02 01 0C 00 00 00 00 00
响应 CAN ID 7E8:04 41 0C 1A F8 00 00 00

OBD 转速响应逐字节解析与计算公式

字段 请求 响应
ISO-TP 单帧长度 02,后面有 2 字节诊断数据 04,后面有 4 字节诊断数据
服务 01 41,本例的肯定响应
PID 0C 0C
参数数据 无 1A F8
剩余字节 填充 填充

PID 0C 的两个数据字节按大端组合,并乘以 0.25:

1
2
raw = 0x1A × 256 + 0xF8 = 6904
rpm = raw / 4 = 1726

这是该 PID 的编码公式,不能直接套到车速、温度或者厂商自定义信号上。PID 表与单位说明可以用于逐项核对。

先确认是否支持这个 PID

01 00 用于读取相应 PID 支持位图。得到 41 00 A B C D 后,四字节位图最高位对应 PID 01,最低位对应 PID 20。PID 0C 对应 bit 20,而不是简单拿十六进制 0C 当数组下标去取一个 bit。

1
2
3
4
bitmap = int.from_bytes(bytes.fromhex("00 10 00 00"), "big")
pid = 0x0C
supported = bool(bitmap & (1 << (32 - pid)))
print(supported) # True

这个人工位图只用来展示位序。实际车辆可能支持一批 PID,并通过范围边界位表示后续还有其他支持位图。没有响应时,还要区分未支持、寻址或协议不对、网关不可达、时序问题,不能只凭超时给出结论。

DTC 字节与显示文本

常见 OBD DTC 用两个字节表示。以 01 33 为例,高两位选择系统类别,其余位展开后得到 P0133。显示出来的五字符代码不是报文里直接保存的五个 ASCII 字符。

同样要区分已存储、pending、permanent 等状态。一个普通读取结果也不能代替维修手册中的故障判定条件和排查流程。

一段可以复算的离线解析

下载 can_decode.py:

1
python3 can_decode.py

其中转速例子的本地输出是:

1
OBD: 41 0c 1a f8 -> 1726 rpm

解析先使用 ISO-TP 的长度得到 41 0C 1A F8,再检查服务和 PID,最后计算物理量。不能先把所有 00 删掉再解析:零可能是合法数据,也可能是字段的一部分。

附件还包含 UDS 多帧重组例子。它只接受已经按连接与方向筛选好的经典 CAN 正常寻址数据,不负责从混合流量里识别会话,也不是可以直接发往车辆的诊断栈。

一次功能请求为什么会出现多个答案

7DF 请求的是一类诊断能力,并不指定“发动机控制器”。一条 01 00 可能同时收到 7E8、7E9 等响应。它们可以有不同的支持位图、响应时间和故障记录。工具如果只保存一个全局 supported_pid 集合,后面很容易向错误的 ECU 查询数据,或者把来自变速箱的回复标成发动机数据。

在本文的 11 位配置中,常见的对应关系如下。实际项目仍以网络配置为准。

用途 请求 CAN ID 响应 CAN ID
功能请求 7DF 多个相关 ECU 分别回复
物理请求,示例 ECU A 7E0 7E8
物理请求,示例 ECU B 7E1 7E9

长响应还有一个容易漏掉的细节:原来的请求虽然通过 7DF 发出,接下来给某个 ECU 的 FC 仍要发到它对应的物理接收地址。不能把所有 ECU 的流控都当作 7DF 广播。

两个 ECU 的数据交错到达,按地址对分别保存 ISO-TP 状态

这张图中的两个连接分别维护自己的传输状态。7E9 的单帧插在 7E8 的 CF1 与 CF2 之间,也不会改变连接 A 的缓存、总长度或下一 SN。如果两个连接都在发送多帧消息,各自的 CF1 都可以使用 21,因为 SN 只在各自这次 ISO-TP 传输内有意义。

29 位 ID 怎么看

对一种常见的正常固定寻址配置,诊断仪源地址为 F1,ECU 地址为 10:

1
2
3
4
5
功能请求:18DB33F1
物理请求:18DA10F1
物理响应:18DAF110
└─ 源地址
└─── 目标地址

18DA<TA><SA> 把目标、源地址放在 CAN ID 中。这个例子里的 33 是相应功能目标地址,不能替换成随便一个 ECU 地址就认为语义相同。

“29 位扩展 CAN 帧”和“ISO-TP 扩展寻址”是两个概念。后者还可能在 CAN 数据场里使用额外地址字节,使 PCI 起点和有效载荷容量发生变化。一个解析器只根据 len(can_id) > 3 就跳过数据场第一个字节,必然会把部分合法报文解析错。地址模式的具体配置可以对照 python-can-isotp addressing。

PID 支持位图:从位序到逐 ECU 建表

前面的单 bit 示例足够验证位序,但采集器需要处理完整位图和后续范围。令当前范围起点为 base,返回的四字节大端整数为 M,其中 i 取 1~32:

1
2
PID = base + i
supported(PID) = (M & (1 << (32 - i))) != 0

因此 01 00 的最高位是 PID 01,最低位是 PID 20;01 20 的最高位是 PID 21,最低位是 PID 40。范围末尾的位同时告诉工具相应的下一组支持信息是否可查询,不能把它当成普通传感器值读取。

下面采用人工位图 BE 3E B8 13,避免只挑“恰好一种 PID”掩盖位序错误:

1
2
3
4
请求:01 00
响应:41 00 BE 3E B8 13

支持项:01 03 04 05 06 07 0B 0C 0D 0E 0F 11 13 14 15 1C 1F 20

PID 位图的高低位顺序,以及 00、20 两个范围如何衔接

可以写成下面这个小函数,输入严格要求四字节,输出保留 PID 编号:

1
2
3
4
5
6
def supported_pids(base, data):
if len(data) != 4:
raise ValueError("bitmap must contain four bytes")
bitmap = int.from_bytes(data, "big")
return [base + i for i in range(1, 33)
if bitmap & (1 << (32 - i))]

实际采集时按 (通道, ECU 地址, 服务号, 范围起点) 保存结果。01 的支持情况不能代替 09 的车辆信息支持情况,刚连接时的超时也不能立即写成“不支持”。区分主动否定、没有响应、响应格式错误,后面分析才有意义。

物理量的公式为什么不能只写一个通用解码器

不同 PID 的数据长度、偏移和比例不相同。下面几个最常用的例子,足以覆盖容易写错的三类转换:大端整数、带偏移的无符号量和比例换算。

PID 参数 数据字节 换算 范围或单位
04 计算负荷 A 100 × A / 255 %
05 冷却液温度 A A − 40 °C
06 / 07 Bank 1 短期 / 长期燃油修正 A (A − 128) × 100 / 128 %
0C 发动机转速 A、B (256A + B) / 4 rpm
0D 车速 A A km/h
10 空气流量 A、B (256A + B) / 100 g/s
11 节气门位置 A 100 × A / 255 %

三个边界值值得亲手算一次:温度 00 是 −40 °C;燃油修正 80 是 0%;FF 是 99.21875%,并不是整整 100%。修正值的编码是以 128 为零点的偏移编码,不能先做 int8 有符号转换。

解码顺序也有要求。收到 41 0D 1A F8 时,即使后两个字节能算出一个转速,也不能接受为 PID 0C 的回复。至少检查以下三个条件再转换:响应 SID 与请求服务对应;PID 与正在等待的请求一致;参数长度符合该 PID 定义。

本文的附件把长度、单位和转换函数放在同一个表里,验证也由同一入口执行。以后增加一个 PID,应同时添加正常值、边界值和错误长度的样例。这样扩展的是数据字典,不是在调用端到处增加临时 if。

数据刷新率与查询频率不是一回事

诊断工具每秒查询 20 次,并不代表 ECU 每秒重新采样 20 次。返回值可能来自内部周期更新的缓存,而且不同 ECU 的周期不同。连续出现相同字节,既可能是数值稳定,也可能是重复读取同一次内部采样。

保存 CSV 时,建议同时保留 request_time、response_time、ECU、原始字节和解码值。只保存“2026-xx-xx,1726 rpm”会丢失请求延迟和来源;把回复时间当作传感器精确采样时刻,也会制造不存在的时间精度。

故障码:编码、状态与故障条件

两个字节的 OBD DTC 可以按位直接展开。设第一个字节为 A、第二个字节为 B:

1
2
3
4
5
6
A: b7 b6 | b5 b4 | b3 b2 b1 b0
系统类 第二位 第三位
B: b7 b6 b5 b4 | b3 b2 b1 b0
第四位 第五位

系统类:00=P,01=C,10=B,11=U

01 33 展开为 P0133,C1 23 展开为 U0123。后三个位置按十六进制显示;第二个位置由两 bit 得到 0~3。不要把整个两字节整数转成十进制,再往前拼一个 P。

1
2
3
def obd_dtc(a, b):
group = "PCBU"[a >> 6]
return f"{group}{(a >> 4) & 3}{a & 15:X}{b:02X}"

一条构造的服务 03 响应可以是:

1
2
3
ISO-TP 数据场:07 43 01 33 C1 23 00 00
应用负载 : 43 01 33 C1 23 00 00
DTC 记录 : P0133 U0123 空记录

这里要分清两种“零”:ISO-TP 长度之外的是 CAN 填充;负载内完整的 00 00 DTC 记录按相应格式解释为空记录。绝对不能把所有值为零的字节删掉,否则 01 00 这种合法记录也会被破坏。

Stored、pending、permanent 的差别

读取服务 肯定响应 SID 分析含义
03 43 读取相应已存储/确认的排放相关故障记录
07 47 读取相应当前或上一驾驶循环的 pending 信息
0A 4A 读取永久状态的相关 DTC

同一个代码出现在两种列表里,不是简单的“数据库重复”。它反映诊断监测状态。清除相关记录后,监测还没有运行,不等于故障已经验证消失;永久记录的消退还涉及监测确认条件,不能把服务 04 理解成擦除所有痕迹。

PID 01 的 MIL 和就绪状态

41 01 A B C D 中,A 的最高位表示 MIL 状态,低 7 位给出相应 DTC 数量。例如 A=82 表示 MIL 位为 1,计数为 2。这里得到的是状态字段,不是两字节 DTC 本身。

后续字段还表示监测是否支持、是否完成,并涉及点火类型对应的布局。支持某项监测与该项监测已完成是两件事。用固定掩码解析所有动力配置,很容易把“此车不支持该监测”显示成“监测未通过”。清码后出现多个未就绪项也有其状态背景,应该结合运行条件判断,不能直接当作新的故障。

冻结帧与实时值别放进同一个样本

冻结帧记录的是故障触发附近保存的环境数据。读取时要带相应帧编号。用第 0 帧的转速举例:

1
2
3
4
5
请求应用负载:02 0C 00
响应应用负载:42 0C 00 1A F8
│ │ └──── 参数 A、B
│ └─────── 冻结帧编号
└────────── PID

如果套用实时服务 01 的解析,把 00 1A 当成转速数据,就会得到 6.5 rpm,结果看起来“还能算”,字段却已经错位。本文解码函数把 frame 作为显式参数,要求响应中的帧编号也一致。

冻结帧的实际保留策略和可读取内容取决于支持情况;它未必保存你希望观察的每一个参数。分析报告应把当前工况和冻结时工况分开,不能拿停车后的实时车速去解释行驶中触发的故障。

服务 09 读取车辆信息:与 UDS 的 F190 对照

OBD 用 09 02 请求 VIN,UDS 常用 22 F1 90。下面使用教学标识 LTEST123456789012,只演示 17 字节字符串,不声称它通过真实 VIN 校验。

1
2
OBD 应用响应:49 02 01 + 17 字节标识
UDS 应用响应:62 F1 90 + 17 字节标识

本例 CAN 配置下,OBD 的 01 是该响应中的数据项计数。不能根据“两个响应都是 20 字节”,就让同一个服务头解析器处理它们。OBD 的完整传输如下:

1
2
3
4
5
Tester 7DF  02 09 02 55 55 55 55 55
ECU 7E8 10 14 49 02 01 4C 54 45
Tester 7E0 30 00 05 55 55 55 55 55
ECU 7E8 21 53 54 31 32 33 34 35
ECU 7E8 22 36 37 38 39 30 31 32

最后两帧是 CF1、CF2,不是 VIN 的第二、第三个数据项。ISO-TP 的长度和序号负责传输,应用层的数据项字段负责业务,两者分别校验。关于 FC、Block Size、连续帧回绕与超时,见 UDS 诊断。

ELM327 终端看到的不是原始 CAN 抓包

ELM 类适配器在串口文本与车辆协议之间做了一层转换。先确定当前显示模式,才能解释屏幕上的字节。以下是针对已确认 11 位、500 kbit/s CAN 的配置示例,不是所有车辆通用的自动探测流程,也不是本次实车执行记录:

1
2
3
4
5
6
7
8
9
AT Z       复位适配器
AT E0 关闭命令回显
AT L0 关闭额外换行
AT S1 显示字节间空格
AT SP 6 选择 11 位 / 500 kbit/s CAN
AT CAF1 自动添加/处理 CAN 格式字段
AT CFC1 自动发送相应流控
AT H1 显示报文头,便于区分 ECU
01 0C 发送服务和 PID

这些注释是本文解释,输入终端时只发送左侧命令。CAF1 下请求端通常由适配器添加 PCI;把 02 01 0C ... 整帧文本再交给它,可能相当于重复封装。另一方面,ATH1 会影响接收显示,让输出包含更多原始字段,但并不等于同时关闭发送端的自动格式处理。ELM327 原厂数据手册对 CAF、CFC、H 的组合有单独说明。

终端解析也需要状态机。命令回显、SEARCHING...、NO DATA、错误文本和最后的 > 提示符都不属于诊断负载。一次串口 read() 可能只有半行,也可能包含几行回复;等到 > 之前,应允许同一请求收到多个 ECU 的结果。

另外,NO DATA 是适配器的一次观察结果,不能直接转换成某个 ECU 的 NRC。前者可能根本没有收到可显示的帧,后者则来自一个完整的应用层否定响应。

采样吞吐量:瓶颈不只有 CAN 波特率

假设只做单帧请求、单帧响应,每个往返两帧;用每帧约 130 bit 作为包含帧开销与位填充的粗略预算值,500 kbit/s 总线的每次查询传输成本约为:

1
2 × 130 / 500000 = 0.00052 s = 0.52 ms

130 bit 不是每条经典 CAN 帧的固定长度;实际长度还随格式、DLC 和位填充变化。它适合做量级估算,不能替代分析仪测得的占用率。

如果查询 20 个 PID、希望每项 10 Hz,那么总查询率为 200 次/s,总帧率约 400 帧/s,仅本例诊断流量就约占 400 × 130 / 500000 = 10.4%。这还没有计算多 ECU 回复、流控、重试、其他总线流量及仲裁等待。

更实际的瓶颈可能在 ECU。若每次完整请求—响应平均耗时 20 ms,而同一目标只允许顺序处理请求,采集器上限约为 50 次/s。均匀轮询 20 个 PID,每项只能得到约 2.5 Hz。盲目缩短循环中的 sleep() 无法突破串行请求本身的限制。

总线预算与串行 ECU 往返共同限制轮询频率

优化前先量延迟分布:请求发出到首帧、首帧到末帧、末帧到适配器提示符各用了多久。针对高频参数单独设采集周期,低频的温度/状态降低查询频率;不要为了在界面上同时显示,就假设这些参数也在同一时刻采样。

服务 01 在相应配置中可以请求多个 PID,但请求容量、支持组合和响应解析都需要单独验证。批量请求能减少某些开销,并不自动保证返回值来自完全同步的物理采样。

离线实验:从混合轨迹重组到解码

下载 diagnostic_lab.py 和 diagnostic_trace.jsonl,放在同一目录。Python 3.10 及以上即可运行,不依赖车辆、CAN 设备或第三方库。

1
2
3
python3 diagnostic_lab.py
python3 diagnostic_lab.py --trace diagnostic_trace.jsonl
python3 diagnostic_lab.py --test

脚本本地运行得到以下输出。其中时间戳和报文是人工构造的;“测试通过”表示解析器通过这些输入的检查,不代表做过实车验证。

1
2
3
4
PID bitmap BE3EB813: 01 03 04 05 06 07 0B 0C 0D 0E 0F 11 13 14 15 1C 1F 20
RPM: 1726.0 rpm
DTC: P0133, U0123
OBD VIN: 49 02 01 4c 54 45 53 54 31 32 33 34 35 36 37 38 39 30 31 32 -> LTEST123456789012

轨迹里有一个多帧车辆信息响应,中途穿插另一 ECU 的转速单帧。解码器先按 link 区分连接,再进入各自状态机,因此不会把那条单帧混进 VIN。link 由采集配置明确给出;脚本没有假装能仅凭一条 CAN ID 自动推断任意厂商的寻址关系。

进一步可以改动附件,观察三个故障:把第一条 CF 的 21 改成 22,应触发序号错误;删去中间 FC,应触发不满足流控条件;把某个 CF 的时间戳推迟到超时之后,应终止该次重组。真实被动抓包如果存在漏采,结论应写“轨迹不完整或传输异常”,不能只凭缺一条 FC 就判定 ECU 违反协议。

OBD、厂商诊断与安全测试的边界

接口上存在通用 OBD 服务,不意味着能读取全部车内信号。普通 CAN 周期报文通常通过 DBC 的信号定义解释;诊断报文还涉及 ISO-TP、服务、数据标识与变长记录。两类数据即使来自同一接口,也不该塞进同一套未经区分的解码逻辑。

做网关或诊断代理的审计时,可以把一次请求沿链路追到底:手机/主机调用者身份,适配器连接权限,网关路由规则,目标 ECU 的服务权限。特别检查代理是否把多个客户端合并成同一个诊断身份、会话状态是否跨客户端复用、一个用户能否读取另一个用户请求的返回数据。这样得到的是可复核的访问控制问题,而不是笼统地写“OBD 没有认证”。

本文聚焦经典服务号与 CAN 示例。现代排放诊断还有其他协议配置,不能把这里的服务表当作所有年份、所有市场车辆的完整能力清单。实际测试记录里保留标准版本、目标配置与原始报文,比写一个包办所有车型的自动判断规则更可靠。

使用 OBD 分析通信数据

原来的“接工具、发请求、收响应、解码”还需要补几项记录:

阶段 应留下的信息
接入 连接器视角、协议、总线速率、工具型号与配置
请求 CAN ID、是否功能寻址、原始数据、发送时间
接收 每个响应 ECU 的 ID、方向、响应时间
重组 SF/FF/CF/FC、总长度、连续帧序号
解码 服务、PID/DID、大小端、比例与单位
结论 能确认的数值、未支持与超时等不确定项

若功能寻址触发多个 ECU 回复,不能把不同响应 ID 的连续帧拼到一起。抓到的“第一条回复”也不一定就是想分析的目标 ECU。

安全测试该看什么

测试重点应是诊断网关和服务的实际访问控制:入口能到哪些 ECU,默认状态能读取什么,敏感服务需要什么会话和认证条件,以及断线后状态怎样恢复。通用只读诊断可用,不代表写入或控制服务也应该开放。

对于适配器或车机转发服务,还要检查来自 USB、蓝牙、Wi-Fi 的请求是如何被映射到车辆网络的。把这些入口统一转成 CAN 消息之后,原始调用者身份是否仍被保留,是一个值得追的数据流。

更深入的会话、SecurityAccess 和服务响应结构,放在 UDS诊断 中继续分析。


OBD诊断
https://g1at.github.io/2024/03/14/OBD/
作者
g0at
发布于
2024年3月14日
许可协议