22、车载以太网与 SOME/IP:车载以太网基础、SOME/IP 协议、在 Android 中集成 SOME/IP

各位同学,今天我们来聊聊车载通信里一个绕不开的话题——车载以太网和 SOME/IP。

说实话,我刚从手机转行做车载那会儿,最不适应的就是通信协议。手机里跑的是 USB、Wi-Fi、蓝牙,到了车上,全变成了 CAN、LIN、FlexRay,还有我们今天的主角——车载以太网。你想想看,一辆智能汽车里,摄像头、雷达、激光雷达、座舱娱乐系统,这些动辄几百兆甚至上 G 的数据流,靠传统的 CAN 总线根本扛不住。所以,车载以太网就登场了。

22.1 车载以太网基础

车载以太网,说白了就是「针对汽车环境做了优化的以太网」。它和咱们办公室用的以太网同根同源,但做了不少「瘦身」和「加固」。

我习惯把车载以太网的核心特点归纳为三点:

  • 轻量化物理层:传统以太网用 4 对线,车载以太网只用 1 对非屏蔽双绞线。重量轻、成本低,还省空间。我记得第一次看到 BroadR-Reach 的波形图时,心里还嘀咕「这玩意儿真能抗干扰?」后来在实车上跑了一轮 EMC 测试,才服气了。
  • 高带宽:目前主流是 100BASE-T1(100Mbps)和 1000BASE-T1(1Gbps)。对于摄像头数据流,100Mbps 勉强够用,1Gbps 才算是「舒坦」。我建议新项目直接上 1000BASE-T1,别省那点成本。
  • 低延迟与确定性:车载以太网支持时间敏感网络(TSN),能保证关键数据在微秒级内送达。这对 ADAS 和自动驾驶来说,是生死攸关的事。

下面这张图展示了车载以太网在整车网络中的位置。你可以看到,它连接了 ADAS、座舱、网关和车身控制模块,是整车的「数据主动脉」。

车载以太网整车网络架构 中央网关 车载以太网骨干 ADAS 域控制器 摄像头 / 雷达 / 激光雷达 车身域控制器 车门 / 灯光 / 空调 座舱域控制器 中控屏 / 仪表 / 音响 T-Box / 网关 4G/5G / V2X 1000BASE-T1 1000BASE-T1 1000BASE-T1 1000BASE-T1

核心要点:车载以太网不是「新造轮子」,而是「改造轮子」。它保留了 TCP/IP 协议栈的通用性,同时通过物理层和链路层的优化,满足了车规级要求。

22.2 SOME/IP 协议

有了以太网这个「高速公路」,还得有「交通规则」对吧?SOME/IP 就是这套规则。

SOME/IP 的全称是 Scalable service-Oriented MiddlewarE over IP。名字很长,但核心就两个词:面向服务中间件

我刚开始接触 SOME/IP 时,总觉得它和 DDS 很像。后来在项目里踩过坑才明白,SOME/IP 更「轻」,更「紧耦合」——它和 AUTOSAR 深度绑定,适合车内确定性较高的场景。而 DDS 更「重」,更「松」,适合车路协同这类动态环境。

22.2.1 服务发现(Service Discovery)

SOME/IP 的服务发现机制,我习惯叫它「谁提供、谁使用、谁找到谁」。

  • Service Provider:提供服务的 ECU,比如座舱域提供「车辆状态服务」。
  • Service Consumer:使用服务的 ECU,比如仪表盘需要读取车速。
  • SD 报文:通过 Offer Service、Find Service、Subscribe/Subscribe Ack 等报文,完成服务的注册、发现和订阅。

举个例子。仪表盘上电后,会广播一个 Find Service 报文:「谁提供车速服务?」座舱域收到后,回复 Offer Service:「我提供,我的 IP 是 192.168.1.10,端口是 12345。」然后仪表盘发送 Subscribe 报文,座舱域回复 Subscribe Ack,通信链路就建立了。

避坑指南:我曾经在一个项目里遇到仪表盘偶尔收不到车速数据的问题。排查了半天,发现是服务发现阶段的超时时间设置得太短。座舱域启动慢,仪表盘已经超时退出了。后来我把 SD 的重试次数从 3 次改成 5 次,问题就解决了。

22.2.2 远程过程调用(RPC)与事件通知

SOME/IP 支持三种通信模式:

模式 说明 典型场景
Method(方法调用) 请求-响应模式,类似 HTTP 的 GET/POST 获取车辆 VIN 码
Event(事件通知) 服务端主动推送数据 车速变化、车门状态
Field(属性访问) 类似全局变量,可读可写可通知 空调温度设定值

我个人最常用的是 Event 模式。你想想看,车速每 10ms 变化一次,如果用 Method 模式让仪表盘不断轮询,网络带宽和 CPU 都扛不住。用 Event 模式,座舱域主动推送,仪表盘只管接收,效率高得多。

22.3 在 Android 中集成 SOME/IP

好,到了最实战的部分。Android Automotive 系统里怎么集成 SOME/IP?

我参与的第一个车载项目,就是在 Android 中集成 SOME/IP。当时团队选的是开源库 vsomeip,它是 GENIVI 社区维护的 C++ 实现。嗯,这里要注意,Android 原生是不带 vsomeip 的,需要我们自己编译和集成。

22.3.1 编译 vsomeip 库

首先,把 vsomeip 源码放到 Android 源码的 external/ 目录下。然后写一个 Android.bp 文件:

cc_library_shared {
    name: "libvsomeip",
    srcs: [
        "src/*.cpp",
    ],
    export_include_dirs: ["include"],
    shared_libs: [
        "libboost_system",
        "libboost_thread",
    ],
    cflags: ["-DUSE_SOMEIP_CFG"],
    vendor: true,
}

编译命令很简单:

source build/envsetup.sh
lunch aosp_car_x86_64-userdebug
mmm external/vsomeip/

编译成功后,会在 out/target/product/.../vendor/lib/ 下生成 libvsomeip.so

注意:vsomeip 依赖 Boost 库。Android 原生不带 Boost,需要自己移植。我建议用 external/boost/ 的方式集成,别用预编译库,否则 ABI 兼容性问题会让你头疼。

22.3.2 配置 SOME/IP 服务

vsomeip 通过 JSON 配置文件来定义服务和接口。下面是一个典型的配置:

{
    "unicast": "192.168.1.10",
    "logging": {
        "level": "debug",
        "console": true
    },
    "applications": [
        {
            "name": "VehicleServiceApp",
            "id": "0x1001"
        }
    ],
    "services": [
        {
            "service": "0x1234",
            "instance": "0x5678",
            "method": "0x0001",
            "event": "0x8001",
            "reliable": true
        }
    ]
}

这个配置定义了一个服务 ID 为 0x1234、实例 ID 为 0x5678 的服务,包含一个 Method(0x0001)和一个 Event(0x8001)。

22.3.3 编写 Android 服务端代码

在 Android 中,我习惯把 SOME/IP 服务封装在一个 SystemService 里。这样上层 App 可以通过 Binder 调用,底层通信对 App 透明。

public class VehicleSomeipService extends SystemService {

    private static final String TAG = "VehicleSomeipService";
    private long mNativeHandle;

    public VehicleSomeipService(Context context) {
        super(context);
    }

    @Override
    public void onStart() {
        publishBinderService("vehicle_someip", new Binder() {
            // Binder 接口实现
        });
    }

    @Override
    public void onBootPhase(int phase) {
        if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
            // 初始化 vsomeip 原生层
            mNativeHandle = nativeInit("VehicleServiceApp", "/vendor/etc/vsomeip.json");
            nativeStart(mNativeHandle);
        }
    }

    // JNI 方法
    private static native long nativeInit(String appName, String configPath);
    private static native void nativeStart(long handle);
}

JNI 层调用 vsomeip 的 C++ API:

extern "C" JNIEXPORT jlong JNICALL
Java_com_example_VehicleSomeipService_nativeInit(JNIEnv *env, jobject thiz,
                                                  jstring app_name, jstring config_path) {
    const char *app = env->GetStringUTFChars(app_name, nullptr);
    const char *cfg = env->GetStringUTFChars(config_path, nullptr);

    std::shared_ptr<vsomeip::application> app_ptr = vsomeip::runtime::get()->create_application(app);
    app_ptr->init(cfg);

    env->ReleaseStringUTFChars(app_name, app);
    env->ReleaseStringUTFChars(config_path, cfg);

    return reinterpret_cast<jlong>(new std::shared_ptr<vsomeip::application>(app_ptr));
}

22.3.4 客户端调用

App 端通过 Binder 获取服务代理,然后调用:

IVehicleSomeip service = IVehicleSomeip.Stub.asInterface(
        getSystemService("vehicle_someip"));
int speed = service.getVehicleSpeed();  // 底层走 SOME/IP Method 调用

嗯,这里要注意,Binder 调用是同步的,如果 SOME/IP 网络延迟高,会卡住 UI 线程。我建议在异步线程里调用,或者用回调方式。

经验之谈:Android 集成 SOME/IP 的关键在于「桥接」。上层用 Binder 封装,下层用 JNI 对接 vsomeip。中间加一层线程池做异步处理,这样既保证了 Android 的稳定性,又利用了 SOME/IP 的实时性。

22.4 小结

车载以太网和 SOME/IP 是智能汽车通信的基石。车载以太网提供了物理层和链路层的「高速公路」,SOME/IP 则定义了应用层的「交通规则」。在 Android Automotive 中集成 SOME/IP,核心是做好 JNI 桥接和 Binder 封装。

说实话,这部分内容在项目初期很容易被忽视。很多团队把精力都放在 UI 和功能上,等到联调时才发现通信延迟高、丢包、服务发现失败……那时候再回头改,代价就大了。我建议你在项目早期就把 SOME/IP 的测试环境搭起来,哪怕只跑一个简单的「Hello World」服务,也能提前暴露很多问题。


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