第三章 硬件抽象层(HAL)基础:HAL架构设计、HAL接口定义、HAL模块加载机制
各位同学,今天我们来聊聊Android Automotive系统里一个绕不开的核心——硬件抽象层,也就是HAL。说实话,很多做应用开发的同事可能一辈子都碰不到HAL,但做系统移植和优化的,这关必须过。我当年刚入行时,就被HAL的加载机制坑过好几次,今天把这些经验分享给你们。
3.1 HAL架构设计:为什么要有这一层?
先问大家一个问题:为什么Google非要搞个HAL出来?直接让Framework调用驱动不香吗?
嗯,这里要注意。Android系统要跑在千奇百怪的硬件上。高通、MTK、展讯,每家芯片的传感器、音频、显示驱动都不一样。如果没有HAL这一层,Google每发布一个新版本,所有芯片厂商都得重写Framework层的代码——这谁受得了?
HAL说白了就是一个标准接口层。它把硬件厂商的实现和Android Framework彻底隔离开。Framework只认HAL定义的接口,至于底层是GPIO操作还是I2C通信,Framework不关心。
核心设计思想: Framework调用HAL接口,HAL调用硬件驱动。各司其职,互不干扰。
我个人习惯把HAL架构分成三层来看:
- 上层: Android Framework(Java/Native层),通过HAL Service或直接调用HAL接口
- 中间层: HAL模块本身,以共享库(.so)形式存在
- 底层: Kernel驱动,负责真正的硬件寄存器操作
我在项目中遇到过一种情况:某厂商为了赶工期,把大量业务逻辑写进了HAL层。结果Android版本升级时,HAL接口变了,整个底层代码几乎重写。这就是典型的架构设计失误。记住,HAL只做硬件抽象,不做业务逻辑。
3.2 HAL接口定义:规矩得立好
HAL接口怎么定义?Google其实给出了标准模板。每个HAL模块都有一个hw_module_t结构体,这是所有HAL模块的身份证。
来看一个典型的HAL接口定义:
// hardware/libhardware/include/hardware/hardware.h
typedef struct hw_module_t {
uint32_t tag; // 必须为 HARDWARE_MODULE_TAG
uint16_t module_api_version; // 模块API版本
uint16_t hal_api_version; // HAL API版本
const char *id; // 模块ID,如 "audio", "camera"
const char *name; // 模块名称
const char *author; // 作者信息
struct hw_module_methods_t *methods; // 模块方法表
void *dso; // 动态库句柄
uint32_t reserved[32-7]; // 保留字段
} hw_module_t;
你想想看,每个HAL模块都得先填好这个结构体。Framework加载HAL时,第一件事就是检查tag是否正确。如果不对,直接拒绝加载。我曾经调试过一个Bug,折腾了两天,最后发现是tag字段被意外改写了——这种低级错误最坑人。
模块方法表hw_module_methods_t也很关键:
typedef struct hw_module_methods_t {
int (*open)(const struct hw_module_t* module,
const char* id,
struct hw_device_t** device);
} hw_module_methods_t;
说白了,每个HAL模块只需要实现一个open函数。Framework调用open后,得到一个hw_device_t结构体,里面装着具体的设备操作函数指针。这种设计非常简洁——你只需要关注两个结构体:模块描述和设备描述。
个人建议: 定义HAL接口时,尽量保持接口函数的参数简单。我见过有人把十几个参数塞进一个函数里,调试起来简直噩梦。能用结构体传参就用结构体,后期扩展也方便。
3.3 HAL模块加载机制:到底是怎么跑起来的?
好,接口定义好了,那Framework怎么找到并加载这些.so文件呢?这就涉及到HAL模块加载机制了。说实话,这块逻辑不算复杂,但细节特别多。
加载流程大致如下:
- Framework调用
hw_get_module()函数 - 根据模块ID,在指定路径下查找对应的.so文件
- 使用
dlopen()打开动态库 - 通过
dlsym()找到符号HAL_MODULE_INFO_SYM - 验证
hw_module_t结构体的合法性 - 返回模块指针给调用者
这里有个关键点:查找路径。Android系统会按优先级依次搜索以下路径:
| 优先级 | 路径 | 说明 |
|---|---|---|
| 1 | /vendor/lib/hw/ |
厂商定制HAL,优先级最高 |
| 2 | /system/lib/hw/ |
系统默认HAL |
| 3 | /odm/lib/hw/ |
ODM分区HAL |
为什么会设计多个路径?嗯,这是为了支持分区覆盖。比如Google AOSP提供了一套默认的HAL实现,放在/system/lib/hw/下。芯片厂商可以把自己的优化版本放在/vendor/lib/hw/下,优先级更高,自动覆盖系统版本。
来看hw_get_module()的核心逻辑:
int hw_get_module(const char *id, const hw_module_t **module) {
int status;
const struct hw_module_t *hmi = NULL;
char prop[PATH_MAX] = {0};
char path[PATH_MAX] = {0};
// 1. 检查系统属性,是否指定了HAL路径
property_get("ro.hardware", prop, "default");
// 2. 构造.so文件名:<id>.<ro.hardware>.so
// 例如:audio.default.so, camera.msm8998.so
snprintf(path, sizeof(path), "%s/%s.%s.so",
HAL_LIBRARY_PATH, id, prop);
// 3. 加载动态库
status = load(path, &hmi);
if (status == 0) {
*module = hmi;
return 0;
}
// 4. 如果失败,尝试默认名称
snprintf(path, sizeof(path), "%s/%s.default.so",
HAL_LIBRARY_PATH, id);
status = load(path, &hmi);
// 5. 返回结果
*module = hmi;
return status;
}
避坑指南: 我曾经遇到过一个问题:系统属性ro.hardware被错误设置,导致HAL加载了错误的.so文件。比如明明是msm8998平台,属性值却是"default",结果永远加载不到正确的HAL。排查这种问题,先检查getprop ro.hardware的输出。
加载函数load()内部做了几件重要的事:
- 调用
dlopen()打开.so文件,设置RTLD_NOW标志(立即解析所有符号) - 调用
dlsym()查找HAL_MODULE_INFO_SYM符号 - 验证
tag是否为HARDWARE_MODULE_TAG - 将
dso指针指向动态库句柄(用于后续卸载)
这里有个小细节:RTLD_NOW vs RTLD_LAZY。Android HAL统一使用RTLD_NOW,意思是加载时就把所有外部符号解析完。如果某个符号找不到,dlopen()直接返回失败。这样做的好处是——问题早暴露,而不是等到运行时才崩溃。
核心要点总结:
- HAL架构的核心是接口标准化,Framework和硬件驱动解耦
- 每个HAL模块必须提供
hw_module_t和open()方法 - 加载机制依赖
dlopen/dlsym,路径优先级决定加载哪个.so - 系统属性
ro.hardware决定了HAL文件名的后缀
最后说一句,HAL这块的知识,光看书是不够的。我建议你们找一台真机,进/vendor/lib/hw/目录看看,里面有哪些.so文件?文件名怎么命名的?用dlopen手动加载一个试试?实践出真知,这话一点不假。
下一章我们会深入讲解HAL接口的具体实现,包括如何编写一个完整的HAL模块。到时候我会拿一个真实的传感器HAL做例子,带你们一步步走通整个流程。