5、硬件抽象层(HAL)设计:Vehicle HAL 接口定义,VHAL 属性(Property)与区域(Area)模型

各位同学,今天我们来聊聊 Android Automotive OS 里最硬核的一层——硬件抽象层,也就是 Vehicle HAL。说实话,我当年刚接触这个模块时,觉得它就是个“翻译官”,把车上的硬件信号转成 Android 能懂的语言。但做着做着才发现,这里面的门道远比想象的多。

咱们先看一张整体架构图,帮你快速建立全局观:

Vehicle HAL 架构分层 Car Service (Java) Vehicle HAL (C++ / HIDL / AIDL) 接口定义:IVehicle.hal / VehicleProperties.aidl VHAL 实现(OEM 定制) 属性(Property) + 区域(Area)模型 示例:车速、车门、空调、电池 车辆硬件(CAN / LIN / 以太网) 接口层 实现层 物理层

5.1 Vehicle HAL 接口定义——到底在定义什么?

Vehicle HAL 的接口定义,说白了就是定了一套“车机对话的协议”。Android 系统通过这套协议,向车辆硬件问问题、发指令。反过来,硬件也通过这套协议把状态告诉系统。

我个人习惯把 Vehicle HAL 接口分成三大块:

  • 属性(Property)操作接口:获取属性值、设置属性值、订阅属性变化
  • 事件回调接口:硬件主动上报事件,比如车门开了、车速变了
  • 诊断与控制接口:用于调试、诊断、工厂模式等特殊场景

在 AOSP 里,接口定义最早是用 HIDL 写的,后来慢慢迁移到 AIDL。嗯,这里要注意,不同 Android 版本用的接口语言不一样,但核心逻辑没变。

核心接口方法(简化版):

// 获取属性值
get(VehiclePropValue request) → VehiclePropValue

// 设置属性值
set(VehiclePropValue value) → StatusCode

// 订阅属性变化
subscribe(IVehicleCallback callback, 
          List<SubscribeOptions> options) → StatusCode

// 取消订阅
unsubscribe(IVehicleCallback callback, 
            int propId) → StatusCode

你看,就这么几个方法,但背后承载了整个车机系统的数据交互。我在项目中遇到过,有些 OEM 厂商觉得接口太少不够用,就自己加私有接口。结果呢?升级系统版本时全崩了。所以我建议,尽量用标准接口,别搞特殊化。

5.2 VHAL 属性(Property)模型——万物皆属性

VHAL 里最核心的概念就是“属性”。你可以把属性理解成车辆上每一个可观测、可控制的“点”。

举个例子:

  • 车速是一个属性(VehicleSpeed)
  • 驾驶员侧车窗位置是一个属性(WindowPosDriver)
  • 空调温度设定是一个属性(HvacTemperatureSet)

每个属性都有一个唯一的 ID,叫 propId。AOSP 里预定义了几百个标准属性,从 0x0100 到 0xFFFF。但 OEM 也可以自定义属性,范围是 0x10000 以上。

避坑指南:我曾经见过一个项目,OEM 把自定义属性的 ID 写到了标准属性范围内,结果和系统升级后的新标准属性冲突了。车机直接死循环重启。所以自定义属性一定要用 0x10000 以上的 ID,这是血的教训。

属性的数据结构长这样:

struct VehiclePropValue {
    int32_t propId;           // 属性 ID
    int32_t areaId;           // 区域 ID(后面讲)
    int32_t status;           // 状态:可用/不可用/错误
    int64_t timestamp;        // 时间戳
    union {
        int32_t int32Values[];
        float floatValues[];
        int64_t int64Values[];
        bytes stringValue;
    };
};

这里有个细节:status 字段。很多初学者会忽略它,但实际项目中,硬件可能暂时拿不到数据(比如传感器初始化中),这时候 status 就能告诉上层“数据暂时不可用”。我建议你在实现时,一定要正确处理 status,别硬塞一个错误值上去。

5.3 区域(Area)模型——同一个属性,不同位置

区域模型是 VHAL 里一个非常巧妙的设计。你想想看,一辆车有四个车门、四个车窗、前后两排空调。如果每个车门都定义一个独立的属性,那属性数量会爆炸。

区域模型的做法是:一个属性 + 多个区域

比如车窗位置属性 WINDOW_POS,它支持的区域有:

区域 ID 含义
0x01 驾驶员侧
0x02 副驾驶侧
0x04 左后侧
0x08 右后侧

你看,一个属性 ID 配合不同的 areaId,就能表示四个车窗的位置。而且区域 ID 是按位设计的,可以组合使用。比如 0x01 | 0x02 = 0x03 表示前排两个车窗一起控制。

注意:区域 ID 不是随便定义的。AOSP 里有一套标准区域定义,比如 GLOBAL(整个车辆)、ZONE(前后排)、SEAT(具体座位)、DOOR(具体车门)。OEM 可以扩展,但必须遵循位掩码的设计原则。我曾经见过有人把区域 ID 定义成 1、2、3、4,结果想组合控制时发现根本没法按位运算,只能一个个发指令,效率极低。

区域模型还有一个好处:订阅时可以精确到某个区域。比如我只想监听驾驶员侧车窗的变化,不用收其他三个车窗的事件。这在性能优化上很有用。

5.4 属性与区域的组合——实际项目中的用法

咱们来看一个实际例子。假设你要实现空调温度控制:

// 属性定义
const int32_t HVAC_TEMPERATURE_SET = 0x1520;

// 区域定义(按位)
const int32_t AREA_ROW1_LEFT  = 0x01;  // 主驾
const int32_t AREA_ROW1_RIGHT = 0x02;  // 副驾
const int32_t AREA_ROW2_LEFT  = 0x04;  // 左后
const int32_t AREA_ROW2_RIGHT = 0x08;  // 右后

// 设置主驾空调温度到 22.5 度
VehiclePropValue value;
value.propId = HVAC_TEMPERATURE_SET;
value.areaId = AREA_ROW1_LEFT;
value.floatValues[0] = 22.5f;
status_t ret = mHal->set(value);

这段代码看起来简单,但实际项目中坑不少。我遇到过最典型的问题是:有些 OEM 把区域 ID 和属性 ID 搞混了,在 set 的时候传错了参数,结果主驾调温度,副驾出冷风。嗯,调试这种问题特别痛苦,因为硬件层面看起来一切正常。

所以我的建议是:在 VHAL 实现层,一定要加日志,把每次 set/get 的 propId、areaId、value 都打印出来。这样出了问题,看一眼日志就知道是上层传错了,还是硬件执行错了。

5.5 属性变更通知——事件驱动的核心

VHAL 不只是被动响应请求,它还能主动上报事件。比如车速变了、车门开了、电池电量低了,这些都需要硬件主动告诉系统。

事件通知的流程是这样的:

  1. Car Service 调用 subscribe() 订阅某个属性
  2. VHAL 实现层监听硬件变化
  3. 硬件状态变化时,VHAL 调用 IVehicleCallback.onPropertyEvent()
  4. Car Service 把事件分发给对应的应用

这里有个性能相关的点:事件频率控制。我记得有个项目,车速属性每秒上报 100 次,Car Service 那边直接 CPU 飙到 80%。后来我们加了一个采样率控制,车速变化超过 1km/h 才上报,CPU 立马降到 5%。

经验总结:实现属性变更通知时,一定要考虑三点:

  • 最小变化阈值(变化多少才上报)
  • 最大上报频率(每秒最多报几次)
  • 批量上报(多个属性变化合并成一个回调)

5.6 小结

好了,这一章的内容就这些。咱们回顾一下:

  • Vehicle HAL 接口定义了车机对话的协议,核心就 get、set、subscribe、unsubscribe 四个方法
  • 属性模型把车辆上每个可观测的点抽象成一个 propId,配合 status 字段表示数据质量
  • 区域模型用位掩码的方式,让一个属性可以覆盖多个物理位置,既节省 ID 又支持组合控制
  • 事件通知是 VHAL 的主动能力,但要注意频率控制,别把系统搞崩了

下一章咱们会深入讲属性的具体分类和配置方法,包括那些让人头疼的 change mode 和 access mode。到时候我会拿几个实际项目里的坑出来聊,保证让你印象深刻。


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