第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服务会被hwservicemanager或servicemanager管理。注册代码大致如下:
// 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实现。