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 故障码处理流程
我一般把故障码处理分成三步:
- 采集:通过 VHAL 轮询或监听事件,获取故障码列表
- 过滤:根据状态掩码,过滤出需要处理的故障码(比如只处理当前故障)
- 响应:根据故障码类型,执行对应的动作(点亮故障灯、记录日志、通知用户等)
这里有个常见的坑:不要在主线程里做故障码的解析和响应。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 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的三个核心模块串起来了。从左到右,从协议基础到 VHAL 读取,再到故障码处理,是一个完整的数据流。你写代码的时候,脑子里要有这张图,就知道每一步该做什么了。
好了,关于 OBD 诊断的内容就讲到这里。记住,诊断系统是车载软件里最「硬核」的部分之一,它直接关系到车辆的安全和法规合规。多动手写代码,多在实际车上测试,慢慢就能摸透里面的门道。