第3章 硬件抽象层(HAL)基础:HAL接口定义、HIDL与AIDL的演进、标准HAL实现示例

说到Android Automotive的移植,HAL是绕不开的一道坎。我刚开始接触车载系统时,就被这层抽象搞得有点晕——明明底层硬件千奇百怪,上层应用却要统一调用。说白了,HAL就是给系统内核和Android框架之间搭了一座桥。

3.1 HAL接口定义:到底在抽象什么?

HAL的全称是Hardware Abstraction Layer。它的核心任务就一个:让Android框架不关心底层硬件是谁家的。你想想看,车载系统里可能有高通、瑞萨、恩智浦的芯片,每家传感器、显示、音频的实现都不一样。但上层应用不能跟着变来变去,对吧?

所以HAL定义了一套标准接口。比如你要读取车速,框架就调用getVehicleSpeed(),至于底层是走CAN总线还是走串口,那是HAL实现者的事。

关键点:HAL接口是契约。厂商必须实现这些接口,框架只管调用。谁破坏了契约,系统就崩给你看。

我在项目中遇到过一件事:某家供应商把HAL接口里的返回值类型改了,从int改成了float。结果上层服务直接崩溃,排查了两天才找到原因。嗯,从那以后我养成了一个习惯——接口定义一旦确定,绝不动它

3.2 HIDL与AIDL的演进:从混乱到统一

Android的HAL接口定义语言经历过一次大变革。早期用的是HIDL(HAL Interface Definition Language),后来逐渐迁移到AIDL(Android Interface Definition Language)。为什么会这样?

我个人的理解是:HIDL太重了。它专门为HAL设计,语法独特,学习成本高。而且每次接口变更都要重新生成一堆文件,维护起来很痛苦。我记得有一次改了一个接口参数,结果忘了更新对应的types.hal,编译直接报错,折腾了大半天。

AIDL就不一样了。它本来就是Android IPC的标准语言,现在扩展到HAL层,语法统一,工具链成熟。说白了,Google想把事情简化——你写HAL接口就跟写普通的AIDL服务一样,不用再学一套新东西。

特性 HIDL AIDL(HAL版)
设计初衷 专为HAL设计 从IPC扩展到HAL
语法复杂度 较高,有独立类型系统 较低,与Android IPC一致
工具链 hidl-gen aidl + 新插件
稳定性 成熟但臃肿 正在快速演进
推荐度 遗留项目使用 新项目首选

我的建议:如果你现在开始做新项目,直接用AIDL。别走回头路。HIDL虽然还能用,但Google已经在逐步淘汰它了。

3.3 标准HAL实现示例:以Vehicle HAL为例

讲理论太枯燥,我们直接看代码。车载系统里最典型的HAL就是Vehicle HAL,它负责读取车辆状态(车速、转向、车门等)。

下面是一个简化版的Vehicle HAL接口定义(AIDL版本):

// IVehicle.aidl
package android.hardware.automotive.vehicle;

interface IVehicle {
    /**
     * 获取车辆属性值
     * @param propId 属性ID,比如车速、转速
     * @return 属性值
     */
    VehiclePropValue getProperty(int propId);

    /**
     * 设置车辆属性值
     * @param propId 属性ID
     * @param value 要设置的值
     */
    void setProperty(int propId, VehiclePropValue value);

    /**
     * 订阅属性变化
     * @param propIds 要订阅的属性列表
     * @param listener 回调接口
     */
    void subscribe(int[] propIds, IVehicleCallback listener);
}

接口定义好了,接下来是实现。我习惯把实现类命名为VehicleHalService

// VehicleHalService.cpp
#include "IVehicle.h"

class VehicleHalService : public IVehicle {
public:
    VehiclePropValue getProperty(int propId) override {
        // 这里实际会去读CAN总线或硬件寄存器
        // 为了演示,我们返回一个模拟值
        VehiclePropValue result;
        result.propId = propId;
        result.value = 42;  // 模拟车速42km/h
        return result;
    }

    void setProperty(int propId, VehiclePropValue value) override {
        // 写入硬件
        // 比如设置空调温度
        writeToHardware(propId, value);
    }

    void subscribe(int[] propIds, IVehicleCallback listener) override {
        // 启动一个线程,轮询硬件变化
        // 有变化时回调listener
        startPollingThread(propIds, listener);
    }
};

注意:上面的代码是极度简化的。真实项目中,你需要处理线程安全、内存管理、错误码、超时重试等一堆细节。我曾经因为忘了加互斥锁,导致两个线程同时写同一个寄存器,结果车辆仪表盘乱跳——还好只是在测试车上。

最后一步,注册这个服务。在Android启动时,HAL服务会被hwservicemanagerservicemanager管理。注册代码大致如下:

// main.cpp
int main() {
    // 创建服务实例
    std::shared_ptr<IVehicle> service = 
        std::make_shared<VehicleHalService>();

    // 注册到服务管理器
    android::status_t status = 
        defaultServiceManager()->addService(
            String16("android.hardware.automotive.vehicle.IVehicle"), 
            service);

    if (status != android::OK) {
        // 注册失败,记录日志
        ALOGE("Failed to register Vehicle HAL service");
        return -1;
    }

    // 进入事件循环
    android::IPCThreadState::self()->joinThreadPool();
    return 0;
}

3.4 避坑指南:HAL开发中的常见问题

做HAL开发,有几个坑我踩过,分享给你:

  • 接口版本管理:HAL接口一旦发布,就不能随便改。如果要加新功能,必须升版本号。我曾经因为直接修改了接口,导致旧版客户端崩溃。
  • 性能瓶颈:HAL调用是有开销的。别在循环里频繁调用getProperty,能缓存就缓存。我见过一个项目,每10ms读一次车速,结果CPU占用率飙到30%。
  • 死锁问题:HAL实现里不要调用框架层的服务。比如在getProperty里又去调别的HAL接口,很容易死锁。记住:HAL是单向依赖的
  • 日志要打全:HAL运行在底层,出问题很难排查。我习惯在每个接口的入口和出口都打日志,带上参数和返回值。虽然啰嗦,但调试时能救命。

一个小技巧:开发阶段可以用模拟HAL来测试上层逻辑。写一个假的VehicleHalService,返回固定值或随机值。这样你不需要真车就能调试大部分功能。我每次做项目都会先搭一套模拟HAL环境。

好了,这一章的内容就到这里。HAL是Android Automotive移植的基石,理解它、用好它,后面的工作会顺畅很多。下一章我们聊聊更具体的——如何为你的车载硬件编写第一个HAL实现。