13、车载以太网与诊断:EthernetManager、车载诊断协议(OBD-II)、CAN总线数据读取、故障码解析

各位同学,今天我们来聊聊车载以太网和诊断这块硬骨头。说实话,我刚入行那会儿,觉得诊断就是插个OBD盒子读读故障码,简单得很。直到有一次,我在实车上调试一个多媒体系统,车机死活连不上诊断仪,查了三天才发现是以太网物理层的极性搞反了……嗯,从那以后我再也不敢小看这块内容了。

车载诊断,说白了就是车跟人之间的「对话」——车告诉你它哪里不舒服,你帮它对症下药。而以太网和CAN总线,就是这场对话的「电话线」。今天我们就从Android Automotive的角度,看看怎么用代码跟车「聊天」。

13.1 车载以太网:EthernetManager 实战

先说说以太网。你可能觉得奇怪,车里面为什么要用以太网?CAN总线不是挺好的吗?其实,随着ADAS、高清地图、OTA升级这些功能上来,CAN那点带宽(最高1Mbps)根本不够用。以太网一上来就是100Mbps、1Gbps,这才是现代车载应用的「高速公路」。

在Android Automotive中,以太网的管理是通过 EthernetManager 来做的。我个人习惯先检查设备是否支持以太网:

EthernetManager ethernetManager = (EthernetManager) getSystemService(Context.ETHERNET_SERVICE);
if (ethernetManager != null) {
    // 获取所有以太网接口
    List<EthernetInterface> interfaces = ethernetManager.getInterfaces();
    for (EthernetInterface iface : interfaces) {
        Log.d("Ethernet", "接口名称: " + iface.getName());
        Log.d("Ethernet", "MAC地址: " + iface.getMacAddress());
        Log.d("Ethernet", "连接状态: " + iface.isConnected());
    }
}

这里有个坑,我踩过。有些车机的以太网接口是「虚拟」的——它底层可能走的是USB转以太网,或者通过PCIe桥接。你调用 getInterfaces() 可能返回空列表。这时候别慌,先确认一下硬件是否真的接好了。

小技巧: 在Android Automotive中,以太网配置通常写在 /vendor/etc/ethernet.xml 里。你可以用adb pull出来看看,里面定义了接口名称、IP分配方式等。我调试时经常先看这个文件,能省不少时间。

13.2 车载诊断协议(OBD-II)基础

OBD-II,全称On-Board Diagnostics II,是1996年之后几乎所有乘用车都强制支持的诊断标准。你想想看,一辆车上有几十个ECU(电子控制单元),每个都可能出问题。OBD-II就是让这些ECU统一「说话」的协议。

OBD-II的物理层其实有好几种:

协议类型 物理层 速率 常见车型
ISO 15765-4 CAN总线 250/500 kbps 大多数2008年后车型
ISO 14230-4 K线 10.4 kbps 老款欧洲车
ISO 9141-2 K线 10.4 kbps 老款亚洲车
SAE J1850 VPW/PWM 10.4/41.6 kbps 老款美系车

现在的新车,99%都走CAN总线了。所以我们在Android Automotive上做诊断,核心就是通过CAN接口收发OBD-II报文。

核心概念: OBD-II的请求和响应都是通过CAN ID 0x7DF(请求)和0x7E8(响应)来传输的。请求格式是:02 01 0D 00 00 00 00 00,其中02是数据长度,01是服务ID(显示当前数据),0D是PID(车速)。响应格式类似,但第一个字节会加上0x40变成0x41。

13.3 CAN总线数据读取:从Socket到应用层

在Android上读取CAN总线数据,通常有两种方式:

  1. 通过SocketCAN —— Linux内核原生支持,Android Automotive也继承了这一点。
  2. 通过JNI调用底层库 —— 如果车机厂商提供了私有接口。

我个人更推荐SocketCAN,因为它标准、稳定,而且不需要厂商额外支持。来看看怎么用:

// 打开CAN socket
int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
struct sockaddr_can addr;
struct ifreq ifr;

// 假设CAN接口是can0
strcpy(ifr.ifr_name, "can0");
ioctl(s, SIOCGIFINDEX, &ifr);

addr.can_family = AF_CAN;
addr.can_ifindex = ifr.ifr_ifindex;
bind(s, (struct sockaddr *)&addr, sizeof(addr));

// 设置过滤器,只接收OBD-II相关的CAN ID
struct can_filter rfilter[2];
rfilter[0].can_id = 0x7E8;  // 发动机ECU响应
rfilter[0].can_mask = CAN_SFF_MASK;
rfilter[1].can_id = 0x7E9;  // 变速箱ECU响应
rfilter[1].can_mask = CAN_SFF_MASK;
setsockopt(s, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));

// 循环读取数据
struct can_frame frame;
while (1) {
    int nbytes = read(s, &frame, sizeof(struct can_frame));
    if (nbytes > 0) {
        // 解析CAN帧
        Log.d("CAN", "ID: 0x%X, DLC: %d, Data: %02X %02X ...",
              frame.can_id, frame.can_dlc,
              frame.data[0], frame.data[1]);
    }
}
注意: 在Android Automotive上,直接操作Socket需要系统权限。如果你的应用不是系统应用,可能需要通过 Vehicle HAL 或者厂商提供的诊断服务来间接访问CAN总线。我曾经在一个项目里因为权限问题折腾了两天,最后发现厂商在 sepolicy 里把CAN socket给禁用了……

13.4 故障码解析:从原始数据到可读信息

故障码,也就是DTC(Diagnostic Trouble Code),是OBD-II诊断的核心输出。一个标准的DTC由5个字符组成,比如 P0301

  • 第一位:系统类型(P=动力总成,B=车身,C=底盘,U=网络)
  • 第二位:代码类型(0=通用,1=制造商自定义)
  • 第三位:子系统(1=燃油/空气,2=燃油系统,3=点火系统……)
  • 第四五位:具体故障编号

举个例子,P0301 表示「动力总成-通用-点火系统-第1缸失火」。你想想看,如果没有这个标准,每个厂商自己定义一套编码,那维修师傅得背多少本手册?

在代码层面,解析故障码其实不复杂。我们通过OBD-II服务 03(读取已确认故障码)来获取:

// 请求:03 00 00 00 00 00 00 00
// 响应:43 01 03 01 00 00 00 00
// 解析:43是响应服务ID,01是故障码数量,03 01是第一个故障码

byte[] response = sendObdRequest(new byte[]{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
int dtcCount = response[1];
List<String> dtcList = new ArrayList<>();

for (int i = 0; i < dtcCount; i++) {
    int highByte = response[2 + i * 2] & 0xFF;
    int lowByte = response[3 + i * 2] & 0xFF;
    String dtc = decodeDTC(highByte, lowByte);
    dtcList.add(dtc);
}

private String decodeDTC(int high, int low) {
    char[] prefix = {'P', 'C', 'B', 'U'};
    char firstChar = prefix[(high >> 6) & 0x03];
    int secondDigit = (high >> 4) & 0x03;
    int thirdDigit = high & 0x0F;
    int fourthDigit = (low >> 4) & 0x0F;
    int fifthDigit = low & 0x0F;
    return String.format("%c%d%d%d%d", firstChar, secondDigit, thirdDigit, fourthDigit, fifthDigit);
}
避坑指南: 我曾经遇到过一个问题——读出来的故障码全是 P0000。查了半天,发现是CAN总线的终端电阻没接好,导致信号反射,数据全乱了。所以,如果你发现读到的数据不对劲,先检查物理层,别急着怀疑代码。

13.5 知识体系总览

下面这张图,是我自己总结的车载诊断知识体系。你把它存下来,以后做项目时对照着看,思路会清晰很多。

车载诊断知识体系 Android Automotive 诊断应用 诊断协议层 OBD-II 服务 (01-0A) UDS (ISO 14229) WWH-OBD 传输层 ISO 15765-2 (CAN) DoIP (ISO 13400) - 以太网 KWP2000 物理层 CAN (ISO 11898) 车载以太网 (100BASE-T1) K线 / LIN

从这张图你能看到,我们今天的课程其实覆盖了从物理层到应用层的完整链路。以太网和CAN是物理层和传输层的选择,OBD-II是协议层的标准,而EthernetManager和故障码解析则是应用层的实现。每一层都有它的坑,但只要你理解了整体架构,遇到问题就能快速定位。

好了,关于车载以太网和诊断的内容就讲到这里。记住,诊断不是「读个码就完事」——它涉及到硬件、协议、操作系统权限、数据解析等多个层面。下次你在车上读到 P0301 时,希望你能想起今天讲的CAN帧结构、OBD-II服务ID,以及那个让我折腾了两天的sepolicy权限问题。


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