14、车载诊断系统(OBD):OBD-II 协议基础、通过 VHAL 读取诊断数据、故障码处理

车载诊断系统,也就是我们常说的 OBD,是整车电子架构里最「接地气」的部分。为什么这么说?因为它是连接车辆底层硬件和上层应用的桥梁,也是法规强制要求的功能。我最早接触 OBD 是在一个商用车项目里,当时客户要求必须通过诊断接口读取发动机数据,结果发现不同厂商的协议实现五花八门,踩了不少坑。今天我们就从 OBD-II 协议基础讲起,再到 Android Automotive 里怎么通过 VHAL 拿诊断数据,最后聊聊故障码的处理逻辑。

14.1 OBD-II 协议基础

OBD-II 是第二代车载诊断标准,1996 年起在美国强制推行,后来欧洲、日本也都跟进了。说白了,它定义了一套统一的诊断接口和通信协议,让外部诊断仪能读取车辆的排放相关数据。

14.1.1 物理层与协议类型

OBD-II 的物理接口是标准的 16 针 DLC 连接器,但通信协议有好几种。我整理了一个表格,方便你对照:

协议类型 物理层 波特率 常见应用
ISO 9141-2 K-Line 10.4 kbps 早期欧洲车、部分亚洲车
ISO 14230 (KWP2000) K-Line 10.4 kbps 2000 年代欧洲车
SAE J1850 PWM 双线差分 41.6 kbps 福特、部分美系车
SAE J1850 VPW 单线可变脉宽 10.4 kbps 通用、克莱斯勒
ISO 15765 (CAN) CAN 总线 250/500 kbps 2008 年后几乎所有新车

现在的新车基本都用 CAN 总线了,也就是 ISO 15765 协议。我个人习惯把 CAN 诊断叫做「UDS over CAN」,因为 OBD-II 的很多诊断服务其实和 UDS(统一诊断服务)是兼容的。

14.1.2 诊断服务与 PID

OBD-II 定义了一组标准诊断服务,最常用的是模式 01(请求当前数据)和模式 03(请求故障码)。每个模式下面又有一堆 PID(参数 ID),用来标识具体的数据项。

举个例子,你想读发动机转速:

  • 发送请求:01 0C(模式 01,PID 0C)
  • 响应格式:41 0C XX YY(XX YY 是转速值,需要换算)
  • 换算公式:转速 = (XX * 256 + YY) / 4

嗯,这里要注意,不同 PID 的换算方式不一样。比如车速 PID 0D 就是直接读字节,不需要除 4。我在项目中遇到过有人把转速公式套到车速上,结果读出来的车速飙到 8000 km/h,笑死。

14.1.3 故障码格式

故障码(DTC)是 OBD 诊断的核心输出。标准格式是 5 位字符:

  • 第一位:系统类型(P=动力总成,B=车身,C=底盘,U=网络)
  • 第二位:故障类型(0=通用,1=制造商自定义)
  • 后三位:具体故障编号

比如 P0301 表示「1 缸失火」,P0420 表示「催化器效率低于阈值」。这些码是通用的,不管你是丰田还是大众,P0301 的含义都一样。

核心要点:OBD-II 的协议栈从物理层到应用层都有明确定义。作为车载系统开发者,你不需要关心底层 CAN 帧怎么拼,但必须理解 PID 请求/响应的格式,以及故障码的编码规则。

14.2 通过 VHAL 读取诊断数据

在 Android Automotive 系统里,应用层不能直接访问 CAN 总线。所有车辆数据都要通过 Vehicle HAL(VHAL)来中转。VHAL 定义了一组标准属性,其中就包括诊断相关的属性。

14.2.1 VHAL 诊断属性

VHAL 里和诊断相关的属性主要有这几个:

  • VEHICLE_PROPERTY_OBD2_LIVE_FRAME:实时诊断数据帧
  • VEHICLE_PROPERTY_OBD2_FREEZE_FRAME:冻结帧(故障发生时的快照)
  • VEHICLE_PROPERTY_DTC_INFO:故障码信息

每个属性都是一个 byte 数组,里面封装了 OBD-II 的原始响应数据。应用层拿到数据后,需要自己解析。

14.2.2 读取实时数据

假设你想读发动机转速和车速,代码大概长这样:

// 获取 VehiclePropertyManager
VehiclePropertyManager vpm = new VehiclePropertyManager(context);

// 读取 OBD2 实时帧
VehiclePropValue value = vpm.get(
    VehiclePropertyIds.OBD2_LIVE_FRAME,
    VehicleArea.GLOBAL
);

// 解析原始数据
byte[] rawData = value.getByteArrayValue();
// rawData 的格式:前两个字节是 PID,后面是数据
// 比如 rawData = {0x01, 0x0C, 0x0A, 0x1E}
// 表示 PID 0x0C(转速),数据 0x0A1E

int pid = rawData[1];
if (pid == 0x0C) {
    int rpm = (rawData[2] * 256 + rawData[3]) / 4;
    Log.d("OBD2", "Engine RPM: " + rpm);
}

这段代码看着简单,但实际项目里有个坑:VHAL 返回的 rawData 可能包含多个 PID 的数据。我曾经遇到过一个情况,一次请求返回了 6 个 PID 的数据,结果我只解析了前两个,后面的数据全丢了。所以建议你写一个循环,逐个 PID 解析。

14.2.3 读取故障码

故障码的读取稍微复杂一点。VHAL 的 DTC_INFO 属性返回的是一个字符串列表,每个字符串是一个故障码。但注意,这个列表可能包含历史故障码和当前故障码。

VehiclePropValue dtcValue = vpm.get(
    VehiclePropertyIds.DTC_INFO,
    VehicleArea.GLOBAL
);

List<String> dtcList = dtcValue.getStringValueList();
for (String dtc : dtcList) {
    // dtc 格式如 "P0301"
    if (dtc.startsWith("P")) {
        Log.d("DTC", "Powertrain DTC: " + dtc);
    }
}

嗯,这里有个细节:VHAL 返回的故障码是字符串,但底层 CAN 报文里其实是 2 个字节的编码。VHAL 实现层(通常是 HAL 端的 C++ 代码)负责把字节码转成字符串。如果你发现读出来的故障码格式不对,大概率是 HAL 层的转换逻辑写错了。

个人建议:在开发阶段,可以用 Android 的 CarDiagnosticManager 来调试。这个类封装了 VHAL 的诊断接口,比直接操作 VehiclePropertyManager 更方便。但生产环境里,我建议还是用 VPM 直接读,因为 CarDiagnosticManager 在某些 OEM 的 ROM 里可能被裁剪掉。

14.3 故障码处理

故障码处理是诊断系统的核心业务逻辑。说白了,就是拿到故障码之后,怎么判断严重程度、怎么通知用户、怎么记录日志。

14.3.1 故障码分类与优先级

故障码按严重程度可以分为三类:

  • 当前故障:故障正在发生,需要立即处理
  • 历史故障:故障曾经发生过,但现在已经恢复
  • 待定故障:故障条件满足过一次,但还没确认

在 VHAL 里,DTC_INFO 属性会带一个状态掩码,用来区分当前故障和历史故障。状态掩码的 bit 定义如下:

Bit 含义
0 测试失败(当前故障)
1 测试失败(本次驾驶循环)
2 待定故障
3 确认故障
4 未完成测试
5 测试未通过(上次驾驶循环)
6 历史故障
7 灯点亮请求

举个例子,如果状态掩码是 0x07(bit 0、1、2 都为 1),说明这是一个当前故障,且在本驾驶循环内测试失败过,同时还是待定状态。这种码需要重点关注。

14.3.2 故障码处理流程

我一般把故障码处理分成三步:

  1. 采集:通过 VHAL 轮询或监听事件,获取故障码列表
  2. 过滤:根据状态掩码,过滤出需要处理的故障码(比如只处理当前故障)
  3. 响应:根据故障码类型,执行对应的动作(点亮故障灯、记录日志、通知用户等)

这里有个常见的坑:不要在主线程里做故障码的解析和响应。VHAL 的读取操作可能涉及 IPC 通信,耗时不确定。我曾经在一个项目里看到有人直接在 UI 线程里读 DTC,结果界面卡了 2 秒多,用户体验极差。建议用后台线程或者协程来处理。

14.3.3 故障码的清除

故障码清除也是一个重要功能。VHAL 提供了 VEHICLE_PROPERTY_DTC_CLEAR 属性,设置这个属性可以清除所有故障码。

VehiclePropValue clearValue = new VehiclePropValue();
clearValue.setPropertyId(VehiclePropertyIds.DTC_CLEAR);
clearValue.setAreaId(VehicleArea.GLOBAL);
clearValue.setInt32Value(1);  // 1 表示清除

vpm.set(clearValue);

但注意,清除故障码是有条件的。按照 OBD-II 法规,只有在故障被修复后,才能清除故障码。如果你在故障还没修复的情况下强行清除,下次驾驶循环故障码又会冒出来。而且有些 OEM 会在 VHAL 层做校验,不允许随意清除。

避坑指南:我曾经在一个项目里,测试人员发现清除故障码后,仪表盘的故障灯灭了,但实际故障还在。后来排查发现,VHAL 实现层只清除了应用层的故障码列表,没有同步清除 ECU 里的故障码。结果下一次上电,ECU 又把故障码报上来了。所以清除故障码时,一定要确认底层 ECU 也同步清除了。

14.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

车载诊断系统(OBD)知识体系 OBD-II 协议基础 物理层/协议 诊断服务/PID 通过 VHAL 读取诊断数据 实时数据帧 故障码读取 故障码处理 分类与优先级 清除与响应 核心流程:采集 → 过滤 → 响应 通过 VHAL 轮询或事件监听获取故障码,根据状态掩码过滤,执行对应动作 关键点 1 OBD-II 协议是基础,必须理解 PID 和 DTC 格式 关键点 2 VHAL 是 Android 端访问诊断数据的唯一通道 关键点 3 故障码处理要区分当前/历史/待定状态 关键点 4 清除故障码必须同步底层 ECU,否则会复发

这张图把本章的三个核心模块串起来了。从左到右,从协议基础到 VHAL 读取,再到故障码处理,是一个完整的数据流。你写代码的时候,脑子里要有这张图,就知道每一步该做什么了。

好了,关于 OBD 诊断的内容就讲到这里。记住,诊断系统是车载软件里最「硬核」的部分之一,它直接关系到车辆的安全和法规合规。多动手写代码,多在实际车上测试,慢慢就能摸透里面的门道。

公众号:蓝海资料掘金营,微信deep3321