3. 硬件抽象层基础:HAL模块结构、hw_module_t与hw_device_t结构体详解

说到Android HAL,很多人第一反应就是「那层封装」。没错,HAL就是硬件和框架之间的中间层。但具体长什么样?怎么组织的?今天咱们就把它拆开看看。

我个人习惯把HAL理解成一个「驱动仓库」。每个硬件模块就是一个仓库,仓库里有具体的设备操作接口。Android通过标准化的结构体来管理这一切——核心就是hw_module_t和hw_device_t。

3.1 HAL模块的整体结构

先看一张整体架构图,帮你建立直观印象:

Android HAL 模块结构总览 Android Framework (Java API) HAL Stub (hw_get_module / open) Camera Module hw_module_t hw_device_t open/close/ops Audio Module hw_module_t hw_device_t open/close/ops Sensor Module hw_module_t hw_device_t open/close/ops Linux Kernel Driver (Camera / Audio / Sensor ...) 每个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;

调用流程是这样的:

  1. 框架调用hw_get_module(module_id, &module),拿到模块指针。
  2. 然后调用module->methods->open(module, device_id, &device)。
  3. 你在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文件拿到设备操作接口?

  1. 查找.so: 根据module_id,在/vendor/lib/hw/或/system/lib/hw/下找对应的led.<variant>.so。
  2. 加载.so: dlopen打开动态库,把句柄存到module->dso。
  3. 定位符号: dlsym查找HAL_MODULE_INFO_SYM,拿到hw_module_t指针。
  4. 校验tag: 检查module->tag是否为HARDWARE_MODULE_TAG。
  5. 打开设备: 调用module->methods->open(),拿到hw_device_t。
  6. 类型转换: 框架把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匹配,切记。