3. 硬件抽象层基础:HAL模块结构、hw_module_t与hw_device_t结构体详解
说到Android HAL,很多人第一反应就是「那层封装」。没错,HAL就是硬件和框架之间的中间层。但具体长什么样?怎么组织的?今天咱们就把它拆开看看。
我个人习惯把HAL理解成一个「驱动仓库」。每个硬件模块就是一个仓库,仓库里有具体的设备操作接口。Android通过标准化的结构体来管理这一切——核心就是hw_module_t和hw_device_t。
3.1 HAL模块的整体结构
先看一张整体架构图,帮你建立直观印象:
这张图很直观:每个硬件类型对应一个HAL模块。模块里藏着两个核心结构体——hw_module_t描述模块本身,hw_device_t描述具体设备实例。框架通过hw_get_module()找到模块,再通过module->methods->open()打开设备。
3.2 hw_module_t:模块的身份证
先看定义。这个结构体在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,如 "camera"、"audio"
const char *name; // 模块名称
const char *author; // 作者
struct hw_module_methods_t *methods; // 操作方法
void *dso; // 动态库句柄
uint32_t reserved[32-7]; // 保留字段
} hw_module_t;
说白了,这就是模块的「身份证」。每个字段都有讲究:
| 字段 | 含义 | 我的经验 |
|---|---|---|
tag |
魔数,必须为HARDWARE_MODULE_TAG |
用来校验结构体是否被正确初始化 |
id |
模块唯一标识,如"camera" | 命名要规范,框架靠它找模块 |
methods |
指向hw_module_methods_t |
里面只有一个open函数指针 |
dso |
动态库句柄 | 系统自动填充,别手动改 |
reserved |
保留字段,用于扩展 | 必须清零,否则兼容性会出问题 |
HAL_MODULE_INFO_SYM宏。这个宏会自动生成一个名为HMI的hw_module_t变量,省去手动命名的麻烦。
3.3 hw_device_t:设备的操作手柄
有了模块,还得有设备。hw_device_t就是代表一个具体的硬件设备实例:
typedef struct hw_device_t {
uint32_t tag; // 必须为 HARDWARE_DEVICE_TAG
uint32_t version; // 设备版本
struct hw_module_t *module; // 所属模块
uint32_t reserved[12]; // 保留
int (*close)(struct hw_device_t *device); // 关闭设备
} hw_device_t;
你想想看,这个结构体其实很轻量。真正干活的是它的子结构体——每个硬件类型都会在hw_device_t基础上扩展自己的操作接口。
举个例子,Camera HAL的设备结构体:
typedef struct camera_device_t {
hw_device_t common; // 必须放在第一个字段
int (*set_preview_window)(struct camera_device_t *,
struct preview_stream_ops *);
int (*start_preview)(struct camera_device_t *);
int (*stop_preview)(struct camera_device_t *);
int (*take_picture)(struct camera_device_t *);
// ... 更多操作
} camera_device_t;
hw_device_t common。这样做的目的是——你可以把子结构体指针强制转换成hw_device_t*,实现多态。这是C语言里经典的「继承」手法。
3.4 模块与设备的关联:open函数
模块和设备怎么连起来的?靠hw_module_methods_t里的open函数:
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;
调用流程是这样的:
- 框架调用
hw_get_module(module_id, &module),拿到模块指针。 - 然后调用
module->methods->open(module, device_id, &device)。 - 你在
open函数里分配设备结构体,初始化硬件,返回给框架。
我曾经在一个项目里踩过坑——open函数里忘了给device->tag赋值。结果框架校验tag时直接返回-EINVAL,查了半天才发现。嗯,这种低级错误,犯一次就记住了。
3.5 实战:定义一个简单的LED HAL模块
光说不练假把式。咱们手写一个LED HAL模块,把上面的概念串起来:
// led.h
typedef struct led_device_t {
hw_device_t common;
int (*set_on)(struct led_device_t *dev, int on);
int (*set_brightness)(struct led_device_t *dev, int level);
} led_device_t;
// led.c
static int led_device_open(const struct hw_module_t *module,
const char *id,
struct hw_device_t **device) {
led_device_t *dev = malloc(sizeof(led_device_t));
memset(dev, 0, sizeof(led_device_t));
dev->common.tag = HARDWARE_DEVICE_TAG;
dev->common.version = 0;
dev->common.module = (struct hw_module_t *)module;
dev->common.close = led_device_close;
dev->set_on = led_set_on;
dev->set_brightness = led_set_brightness;
*device = &dev->common;
return 0;
}
static struct hw_module_methods_t led_module_methods = {
.open = led_device_open,
};
hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = 1,
.hal_api_version = 1,
.id = "led",
.name = "LED HAL Module",
.author = "Your Name",
.methods = &led_module_methods,
};
HAL_MODULE_INFO_SYM这个符号名是固定的。Android的hw_get_module函数通过dlsym查找这个符号来定位模块入口。如果你改了名字,框架就找不到你的模块了。
3.6 模块加载流程:从.so到设备指针
最后梳理一下完整流程。框架怎么从.so文件拿到设备操作接口?
- 查找.so: 根据
module_id,在/vendor/lib/hw/或/system/lib/hw/下找对应的led.<variant>.so。 - 加载.so:
dlopen打开动态库,把句柄存到module->dso。 - 定位符号:
dlsym查找HAL_MODULE_INFO_SYM,拿到hw_module_t指针。 - 校验tag: 检查
module->tag是否为HARDWARE_MODULE_TAG。 - 打开设备: 调用
module->methods->open(),拿到hw_device_t。 - 类型转换: 框架把
hw_device_t*转换成具体的子类型(如led_device_t*),开始调用硬件操作。
你看,整个过程其实不复杂。核心就是两个结构体、一个open函数、一个固定符号名。搞懂了这些,HAL模块的骨架你就拿下了。
hw_get_module总是返回-ENOENT。查了半天,原来是.so文件命名不符合规范——平台是ranchu,但文件名写成了led.default.so,而系统期望的是led.ranchu.so。命名规则一定要和ro.product.board或ro.hardware匹配,切记。