第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,
};

你看,整个流程很清晰:

  1. 定义设备结构体,第一个字段是 hw_device_t
  2. 实现 open 函数,在里面分配并初始化设备
  3. 定义模块方法表,指向 open
  4. 定义模块实例 HAL_MODULE_INFO_SYM,填好所有字段

个人经验:我刚开始写HAL时,总把 HAL_MODULE_INFO_SYM 这个名字写错。记住,它是宏,不是变量名。你可以在代码里搜一下这个宏的定义,它其实就是 HMI 的展开。但别偷懒,直接用标准名字。

2.6 模块加载流程回顾

最后,我们串一下整个流程,看看系统是怎么用这些结构体的:

  1. 系统调用 hw_get_module(),传入模块ID(比如"led")
  2. 系统根据ID找到对应的.so文件,用dlopen加载
  3. 用dlsym找到 HAL_MODULE_INFO_SYM 符号,拿到 hw_module_t*
  4. 调用 module->methods->open(),传入模块指针和设备ID
  5. 你的 open 函数分配 led_device_t,初始化后返回 hw_device_t*
  6. 系统拿到设备指针,强转成具体类型,开始调用设备函数

说白了,这一整套机制就是C语言面向对象编程的经典实现。模块是类,设备是对象。你想想看,是不是这个理?

本章小结:

  • hw_module_t:模块的身份证,每个so库一个
  • hw_module_methods_t:打开设备的方法表
  • hw_device_t:设备实例的基类,所有具体设备结构体必须把它放第一个字段
  • 记住初始化 tag,记住 common 放第一个,记住 HAL_MODULE_INFO_SYM 这个名字

下一章,我们会真正动手写一个完整的HAL模块,从头到尾走一遍。到时候你会发现,理解了这些结构体,写代码就是填空而已。