第2章:HAL接口定义:HAL模块结构、hw_module_t结构体、hw_device_t结构体
好,咱们接着聊。上一章我们搞清楚了HAL到底是个什么角色,说白了就是Android系统和硬件之间的翻译官。那这一章,我们就来看看这个翻译官到底长什么样——它的身份证、它的工作证,都是怎么定义的。
我个人习惯,在开始写任何HAL代码之前,先把接口定义吃透。因为接口一旦定下来,后面改起来特别痛苦。我在项目中就吃过这个亏,后面会跟大家细说。
2.1 HAL模块结构:一切从这里开始
先看一张总图。每个HAL模块,在代码层面就是一个共享库(.so文件)。这个库里面,必须导出一个名为 HAL_MODULE_INFO_SYM 的符号。系统通过这个符号,找到模块的入口。
这个符号的类型,就是 hw_module_t。嗯,这里要注意,它不是指针,是实实在在的结构体变量。系统加载so库后,直接dlsym拿到这个变量的地址。
核心要点:每个HAL模块有且只有一个 hw_module_t 实例。它代表了整个硬件模块的抽象。
你想想看,一个硬件模块可能有多个设备实例。比如一个I2C控制器,可能挂载了多个传感器。但模块本身只有一个,就是那个驱动库。
2.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,比如 "audio"、"gps"
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 |
忘记初始化?系统直接拒绝加载,我踩过这个坑 |
module_api_version |
模块自己的版本号 | 升级接口时递增,向下兼容很重要 |
hal_api_version |
HAL框架版本,目前一般是1 | 别乱改,跟系统版本走 |
id |
唯一标识,比如"led"、"sensor" | 命名要规范,跟硬件匹配 |
methods |
指向 hw_module_methods_t 的指针 |
这是核心,后面细说 |
dso |
系统自动填充,你不用管 | 别手贱去改它 |
避坑指南:我曾经接手过一个项目,前任把 tag 写成了0xFFFFFFFF。结果每次加载都失败,查了两天才发现。记住,tag 必须等于 HARDWARE_MODULE_TAG,这个宏定义的值是 ('H' << 24 | 'W' << 16 | 'M' << 8 | 'T'),也就是0x484D5400。
2.3 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;
说白了,这个 open 函数就是用来创建设备实例的。系统调用它,传入模块指针和设备ID,你返回一个 hw_device_t 指针。
为什么需要 id 参数?因为一个模块可能管理多个同类设备。比如一个USB控制器,插了多个USB设备。你通过 id 来区分打开哪一个。
注意:这个 open 函数不是Linux的open系统调用。它是HAL层面的"打开",本质是分配并初始化一个 hw_device_t 结构体。
2.4 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;
这个结构体是所有具体设备结构体的"基类"。什么意思呢?比如你要实现一个LED设备,你会定义:
typedef struct led_device_t {
struct hw_device_t common; // 必须放在第一个字段!
int (*set_on)(struct led_device_t* dev, int on);
int (*set_brightness)(struct led_device_t* dev, int brightness);
} led_device_t;
这里有个C语言的小技巧:把 hw_device_t 作为第一个字段,这样 led_device_t* 和 hw_device_t* 的指针值是一样的。系统拿到 hw_device_t* 后,直接强转成 led_device_t* 就能用。
警告:千万不要改变字段顺序!common 必须是第一个。我曾经见过有人把自定义字段放在前面,结果系统调用 close 时崩溃了——因为它拿到的地址根本不是 close 函数指针的位置。
2.5 完整示例:一个虚拟LED模块
光说不练假把式。我写一个最简单的LED HAL模块,帮你把上面的概念串起来。
// led_hal.c
#include <hardware/hardware.h>
// 设备结构体
typedef struct led_device_t {
struct hw_device_t common;
int (*set_on)(struct led_device_t* dev, int on);
} led_device_t;
// open函数实现
static int led_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(*dev));
// 初始化common部分
dev->common.tag = HARDWARE_DEVICE_TAG;
dev->common.version = 1;
dev->common.module = (struct hw_module_t*)module;
dev->common.close = led_close;
// 初始化设备特有函数
dev->set_on = led_set_on;
*device = (struct hw_device_t*)dev;
return 0;
}
// 模块方法表
static struct hw_module_methods_t led_methods = {
.open = led_open,
};
// 模块实例——这就是HAL_MODULE_INFO_SYM
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = 1,
.hal_api_version = 1,
.id = "led",
.name = "Virtual LED Module",
.author = "Your Name",
.methods = &led_methods,
};
你看,整个流程很清晰:
- 定义设备结构体,第一个字段是
hw_device_t - 实现
open函数,在里面分配并初始化设备 - 定义模块方法表,指向
open - 定义模块实例
HAL_MODULE_INFO_SYM,填好所有字段
个人经验:我刚开始写HAL时,总把 HAL_MODULE_INFO_SYM 这个名字写错。记住,它是宏,不是变量名。你可以在代码里搜一下这个宏的定义,它其实就是 HMI 的展开。但别偷懒,直接用标准名字。
2.6 模块加载流程回顾
最后,我们串一下整个流程,看看系统是怎么用这些结构体的:
- 系统调用
hw_get_module(),传入模块ID(比如"led") - 系统根据ID找到对应的.so文件,用dlopen加载
- 用dlsym找到
HAL_MODULE_INFO_SYM符号,拿到hw_module_t* - 调用
module->methods->open(),传入模块指针和设备ID - 你的
open函数分配led_device_t,初始化后返回hw_device_t* - 系统拿到设备指针,强转成具体类型,开始调用设备函数
说白了,这一整套机制就是C语言面向对象编程的经典实现。模块是类,设备是对象。你想想看,是不是这个理?
本章小结:
hw_module_t:模块的身份证,每个so库一个hw_module_methods_t:打开设备的方法表hw_device_t:设备实例的基类,所有具体设备结构体必须把它放第一个字段- 记住初始化
tag,记住common放第一个,记住HAL_MODULE_INFO_SYM这个名字
下一章,我们会真正动手写一个完整的HAL模块,从头到尾走一遍。到时候你会发现,理解了这些结构体,写代码就是填空而已。