Vehicle HAL 详解:属性定义与配置、属性订阅与回调、VHAL 实现示例(模拟车速)

各位同学,今天我们来啃一块硬骨头——Vehicle HAL。说实话,这个模块是车载系统里最容易被忽视但又最关键的一环。我见过不少项目,应用层写得花里胡哨,结果底层 VHAL 没搞好,整个系统跑起来就像个瘸腿的机器人。嗯,咱们今天就把这块彻底讲透。

一、VHAL 到底是什么?

Vehicle HAL,全称 Vehicle Hardware Abstraction Layer。说白了,它就是汽车硬件和 Android 系统之间的翻译官。你想想看,汽车上的传感器、ECU、CAN 总线,它们说的都是「车语」。而 Android 系统只懂「Android 语」。VHAL 就是那个两头跑、两头翻译的中间人。

我个人习惯把 VHAL 比作一个「属性仓库」。仓库里存着所有车辆状态——车速、转速、车门状态、空调温度等等。上层应用想读什么,就来仓库查;想控制什么,就来仓库写。VHAL 负责把这些读写请求翻译成硬件能理解的指令。

核心要点:VHAL 不直接操作硬件,它操作的是「属性」(Property)。每个属性代表一个车辆信号或控制项。真正的硬件通信,由 VHAL 内部实现去完成。

应用层 (App) CarService Vehicle HAL 属性仓库 (Property Store) 车速 | 转速 | 车门 | 空调 | 灯光 | 电池 | ... 硬件抽象层 (CAN / LIN / Ethernet / ...) ECU | 传感器 | 执行器 | 网关 读写属性 硬件通信

二、属性定义与配置

在 VHAL 的世界里,一切皆属性。每个属性都有一个唯一的 ID,一套配置参数。我记得第一次接触 VHAL 时,光看属性定义文件就看了三天——密密麻麻几百个属性,头都大了。

2.1 属性 ID 与命名规范

Android 官方定义了一套标准的属性 ID,放在 hardware/interfaces/automotive/vehicle/ 目录下。每个属性都是一个 32 位整数,按功能模块划分区域:

属性范围 功能模块 示例
0x0100 - 0x01FF 车辆信息 VIN码、车型、年份
0x0200 - 0x02FF 动力系统 车速、转速、油门
0x0300 - 0x03FF 车身控制 车门、车窗、天窗
0x0400 - 0x04FF 空调系统 温度、风量、模式
0x0500 - 0x05FF 灯光系统 近光、远光、转向灯
0x0600 - 0x06FF 电池/充电 电量、电压、充电状态

2.2 属性配置参数

每个属性除了 ID,还有一堆配置项。我挑几个最重要的说说:

  • changeMode:属性变化模式。有 STATIC(静态)、ON_CHANGE(变化时通知)、CONTINUOUS(持续上报)。车速一般用 CONTINUOUS,因为它在不断变化。
  • access:访问权限。READ(只读)、WRITE(只写)、READ_WRITE(读写)。车速是只读的,车窗控制是读写的。
  • type:数据类型。INT32、FLOAT、STRING、BOOLEAN 等。车速用 INT32 或 FLOAT。
  • minValue / maxValue:取值范围。车速 0-300 km/h。
  • unit:单位。车速用 km/h。

我的经验:属性配置一定要和硬件团队对齐。我曾经遇到过一个坑——硬件上报的车速单位是 mph,VHAL 里配的是 km/h,结果上层应用显示的数字直接翻倍。排查了整整两天才发现是单位不匹配。

2.3 属性定义示例

来看一个实际的定义,这是车速属性的配置:

// 在 vehicle_types.h 中定义
enum VehicleProperty {
    // ...
    VEHICLE_PROPERTY_PERF_VEHICLE_SPEED = 0x0201,
    // ...
};

// 在 vehicle_config.json 中配置
{
    "propertyId": 0x0201,
    "propertyName": "PERF_VEHICLE_SPEED",
    "changeMode": "CONTINUOUS",
    "access": "READ",
    "type": "FLOAT",
    "minValue": 0.0,
    "maxValue": 300.0,
    "unit": "km/h",
    "description": "当前车速"
}

三、属性订阅与回调

上层应用怎么知道车速变了?靠轮询?那太傻了。VHAL 提供了一套订阅机制——应用说「我对车速感兴趣」,VHAL 就会在车速变化时主动通知它。

3.1 订阅流程

订阅的流程其实很简单,就三步:

  1. 应用通过 CarService 调用 subscribeProperty(),传入属性 ID 和回调接口。
  2. CarService 把订阅请求转发给 VHAL。
  3. VHAL 记录下这个订阅,当属性值变化时,调用回调接口通知 CarService,再传到应用。

嗯,这里要注意一点——订阅是有频率限制的。你不可能让 VHAL 每毫秒都上报一次车速,那会把系统撑爆。一般车速的订阅频率在 10-50Hz 之间。

3.2 回调接口

VHAL 的回调接口长这样:

// IVehicleCallback.aidl
interface IVehicleCallback {
    /**
     * 属性值变化时回调
     * @param values 变化的属性值列表
     */
    oneway void onPropertyEvent(in VehiclePropValue[] values);
    
    /**
     * 属性设置完成时回调
     * @param value 设置后的属性值
     * @param status 操作状态
     */
    oneway void onPropertySet(in VehiclePropValue value, in int32_t status);
}

应用层拿到回调后,就可以更新 UI 了。比如车速仪表盘,每次收到车速变化,就刷新指针位置。

注意:回调是在 Binder 线程中执行的,不要在里面做耗时操作。我曾经见过一个同事在回调里写数据库,结果把整个系统卡死了。正确的做法是:回调里只做数据传递,真正的处理放到主线程或工作线程。

四、VHAL 实现示例:模拟车速

理论说完了,咱们来点实际的。我带你手写一个模拟车速的 VHAL 实现。为什么用模拟?因为真车上跑 VHAL 需要硬件环境,咱们先用模拟器把逻辑跑通。

4.1 创建 VHAL 服务

首先,创建一个继承自 VehicleHal 的服务类:

public class MockVehicleHal extends VehicleHal {
    private static final String TAG = "MockVehicleHal";
    
    // 模拟车速属性
    private static final int PROP_SPEED = 0x0201;
    
    // 当前车速
    private float mCurrentSpeed = 0.0f;
    
    // 模拟线程
    private HandlerThread mSimThread;
    private Handler mSimHandler;
    
    // 订阅者列表
    private final Map<Integer, List<IVehicleCallback>> mSubscribers = new HashMap<>();
    
    public MockVehicleHal() {
        // 启动模拟线程
        mSimThread = new HandlerThread("SpeedSimulator");
        mSimThread.start();
        mSimHandler = new Handler(mSimThread.getLooper());
        
        // 开始模拟车速
        startSpeedSimulation();
    }
    
    @Override
    public void onCreate() {
        Log.d(TAG, "MockVehicleHal created");
    }
    
    @Override
    public void onDestroy() {
        mSimThread.quitSafely();
    }
}

4.2 实现属性读取

当 CarService 来读取属性时,VHAL 需要返回当前值:

@Override
public VehiclePropValue get(VehiclePropKey key) {
    VehiclePropValue value = new VehiclePropValue();
    value.prop = key.prop;
    value.areaId = key.areaId;
    value.timestamp = System.currentTimeMillis();
    
    switch (key.prop) {
        case PROP_SPEED:
            value.value = new VehiclePropValue.Value();
            value.value.floatValues = new float[]{mCurrentSpeed};
            break;
        default:
            throw new IllegalArgumentException("Unknown property: " + key.prop);
    }
    
    return value;
}

4.3 实现属性订阅

订阅的实现就是维护一个回调列表:

@Override
public void subscribe(IVehicleCallback callback, VehiclePropKey key, int samplingRate) {
    int propId = key.prop;
    
    if (!mSubscribers.containsKey(propId)) {
        mSubscribers.put(propId, new ArrayList<>());
    }
    
    List<IVehicleCallback> callbacks = mSubscribers.get(propId);
    if (!callbacks.contains(callback)) {
        callbacks.add(callback);
        Log.d(TAG, "Subscribed to property: 0x" + Integer.toHexString(propId));
    }
}

@Override
public void unsubscribe(IVehicleCallback callback, VehiclePropKey key) {
    int propId = key.prop;
    List<IVehicleCallback> callbacks = mSubscribers.get(propId);
    if (callbacks != null) {
        callbacks.remove(callback);
        if (callbacks.isEmpty()) {
            mSubscribers.remove(propId);
        }
    }
}

4.4 模拟车速变化

核心逻辑来了——模拟车速从 0 加速到 120,再减速回来:

private void startSpeedSimulation() {
    mSimHandler.post(new Runnable() {
        boolean accelerating = true;
        
        @Override
        public void run() {
            // 更新车速
            if (accelerating) {
                mCurrentSpeed += 2.0f;
                if (mCurrentSpeed >= 120.0f) {
                    accelerating = false;
                }
            } else {
                mCurrentSpeed -= 2.0f;
                if (mCurrentSpeed <= 0.0f) {
                    accelerating = true;
                }
            }
            
            // 通知订阅者
            notifySpeedChanged();
            
            // 每 100ms 更新一次(10Hz)
            mSimHandler.postDelayed(this, 100);
        }
    });
}

private void notifySpeedChanged() {
    // 构建属性值
    VehiclePropValue propValue = new VehiclePropValue();
    propValue.prop = PROP_SPEED;
    propValue.areaId = 0;
    propValue.timestamp = System.currentTimeMillis();
    propValue.value = new VehiclePropValue.Value();
    propValue.value.floatValues = new float[]{mCurrentSpeed};
    
    // 通知所有订阅者
    List<IVehicleCallback> callbacks = mSubscribers.get(PROP_SPEED);
    if (callbacks != null) {
        for (IVehicleCallback callback : callbacks) {
            try {
                callback.onPropertyEvent(new VehiclePropValue[]{propValue});
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to notify callback", e);
            }
        }
    }
}

完整流程总结:

  1. MockVehicleHal 启动后,后台线程开始模拟车速变化。
  2. 应用通过 CarService 订阅车速属性。
  3. 每次车速变化,VHAL 调用回调通知所有订阅者。
  4. 应用收到回调,更新 UI 显示。

五、避坑指南

做 VHAL 开发,有几个坑我踩过,分享给大家:

  • 线程安全:VHAL 的方法可能被多个线程同时调用,一定要加锁。我习惯用 synchronized 或者 ReentrantReadWriteLock
  • 内存泄漏:订阅回调时,如果应用销毁了但没取消订阅,VHAL 还持有它的引用,就会泄漏。建议在应用端用 Lifecycle 管理订阅生命周期。
  • 性能问题:CONTINUOUS 模式的属性上报频率不要太高。车速 10-20Hz 足够了,没必要搞到 100Hz。
  • 边界值处理:车速为负?转速为 0?这些边界情况一定要处理。我曾经遇到过车速传感器故障,上报了 -1 km/h,结果仪表盘显示了一个诡异的负数。

我的习惯:在 VHAL 里加一个「健康检查」接口。每隔几秒检查一下各个传感器的状态,如果发现异常值,就上报一个错误事件。这样上层应用可以及时处理异常,而不是傻傻地显示错误数据。

好了,关于 Vehicle HAL 的核心内容就讲到这里。属性定义、订阅回调、模拟实现,这三块搞懂了,VHAL 开发的基本功就算打牢了。剩下的就是在真车上调试、优化,那又是另一番天地了。


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