3、HAL模块编写:编写第一个HAL模块、实现open和close方法、注册模块

好,咱们现在开始动手写代码。

前面两章我们把HAL的架构和数据结构理了一遍。说实话,光看结构体定义确实有点枯燥。但这些东西就像盖房子的地基,不打好后面真会出问题。我记得刚入行那会儿,有个项目就是因为我没搞懂hw_module_thw_device_t的关系,结果open方法一直返回-ENODEV,查了两天才找到原因。

今天咱们的目标很明确:写一个真正的HAL模块。它不干别的,就是能打开、能关闭、能注册到系统里。麻雀虽小,五脏俱全。

3.1 模块的头文件定义

写HAL模块,第一步是定义头文件。这个头文件会暴露给上层框架使用。我个人习惯把模块ID、版本号、设备结构体都放在这里。

// hello.h
#ifndef ANDROID_HELLO_HAL_H
#define ANDROID_HELLO_HAL_H

#include <hardware/hardware.h>

#define HELLO_HARDWARE_MODULE_ID "hello"

// 模块版本号
#define HELLO_MODULE_API_VERSION_1_0 HARDWARE_MODULE_API_VERSION(1, 0)

// 设备结构体
struct hello_device_t {
    struct hw_device_t common;

    // 这里放你的设备特有方法
    int (*say_hello)(struct hello_device_t* dev, const char* name);
};

#endif

你想想看,这个头文件其实就干了三件事:

  • 告诉别人我这个模块叫"hello"
  • 定义版本号,方便以后升级
  • 定义设备结构体,里面嵌着hw_device_t
小提示:结构体里的common字段必须是第一个成员。这是HAL框架的硬性要求,因为上层会通过指针偏移来访问它。我见过有人把common放在第二个字段,结果系统直接崩溃。

3.2 实现open和close方法

接下来是重头戏。每个HAL模块都必须实现open方法。这个方法是框架和硬件之间的桥梁。

// hello.c
#include <hardware/hardware.h>
#include <cutils/log.h>
#include "hello.h"

// 设备关闭方法
static int hello_device_close(struct hw_device_t* dev) {
    if (dev == NULL) {
        ALOGE("hello_device_close: dev is NULL");
        return -EINVAL;
    }

    struct hello_device_t* hello_dev = (struct hello_device_t*)dev;
    // 在这里释放资源,比如关闭文件描述符、释放内存等
    ALOGI("hello_device_close: closing device");

    free(hello_dev);
    return 0;
}

// 设备打开方法
static int hello_device_open(const struct hw_module_t* module,
                             const char* id,
                             struct hw_device_t** device) {
    if (id == NULL || device == NULL) {
        ALOGE("hello_device_open: invalid parameters");
        return -EINVAL;
    }

    // 检查模块ID是否匹配
    if (strcmp(id, HELLO_HARDWARE_MODULE_ID) != 0) {
        ALOGE("hello_device_open: module ID mismatch: %s", id);
        return -EINVAL;
    }

    // 分配设备结构体内存
    struct hello_device_t* dev = malloc(sizeof(struct hello_device_t));
    if (dev == NULL) {
        ALOGE("hello_device_open: failed to allocate memory");
        return -ENOMEM;
    }

    // 清空结构体
    memset(dev, 0, sizeof(*dev));

    // 填充公共字段
    dev->common.tag = HARDWARE_DEVICE_TAG;
    dev->common.version = HELLO_MODULE_API_VERSION_1_0;
    dev->common.module = (struct hw_module_t*)module;
    dev->common.close = hello_device_close;

    // 填充设备特有方法
    dev->say_hello = hello_say_hello;

    // 返回设备指针
    *device = &dev->common;

    ALOGI("hello_device_open: device opened successfully");
    return 0;
}

这段代码里有个细节,我特别想强调一下:内存分配。你看,我们在open方法里用malloc分配了设备结构体,然后在close方法里用free释放。这个模式是固定的,千万别搞错。

我曾经在一个项目里犯过傻——在open里分配了内存,但close里忘了free。结果每次打开关闭设备,内存就泄漏一点。跑了一周后,系统直接OOM了。嗯,从那以后我每次写close都会再三检查资源释放。

3.3 注册模块到HAL框架

模块写好了,open和close也实现了。但系统怎么知道有这个模块呢?

答案是通过HAL_MODULE_INFO_SYM这个符号。它是HAL框架的入口点,每个模块都必须导出一个这样的结构体。

// 模块方法表
static struct hw_module_methods_t hello_module_methods = {
    .open = hello_device_open,
};

// 模块结构体实例
struct hello_module_t {
    struct hw_module_t common;
};

// 这是HAL框架要求的固定符号名
__attribute__((visibility("default")))
struct hw_module_t HAL_MODULE_INFO_SYM = {
    .tag = HARDWARE_MODULE_TAG,
    .version_major = 1,
    .version_minor = 0,
    .id = HELLO_HARDWARE_MODULE_ID,
    .name = "Hello HAL Module",
    .author = "Your Name",
    .methods = &hello_module_methods,
};

这里有几个关键点:

  • HAL_MODULE_INFO_SYM这个宏展开后就是HMI,是固定的符号名
  • __attribute__((visibility("default")))保证这个符号能被动态链接器找到
  • .methods指向我们刚才实现的hello_module_methods
注意:HAL_MODULE_INFO_SYM这个符号名不能改。我见过有人为了图省事,直接写了个struct hw_module_t my_module,结果框架死活找不到模块。说白了,这就是个约定,你得遵守。

3.4 完整的代码结构

把上面几块拼起来,一个完整的HAL模块就成型了。我整理了一个表格,方便你对照:

文件 作用 关键内容
hello.h 定义模块ID、版本号、设备结构体 hello_device_tHELLO_HARDWARE_MODULE_ID
hello.c 实现open、close、注册模块 hello_device_openhello_device_closeHAL_MODULE_INFO_SYM
Android.mk 编译脚本,生成.so文件 LOCAL_MODULE := hello.default

说到编译脚本,我顺便提一句。HAL模块的命名规则是模块名.变种,比如hello.default。这个.default表示这是默认实现。如果你有多个硬件变种,可以写成hello.vendor1hello.vendor2之类的。

3.5 验证你的模块

模块写完后,怎么验证它能不能用?

我一般会写一个简单的测试程序。用hw_get_module来加载模块,然后调用open方法。

// test_hello.c
#include "hello.h"
#include <hardware/hardware.h>

int main() {
    struct hello_device_t* dev = NULL;
    int ret;

    // 加载模块
    ret = hw_get_module(HELLO_HARDWARE_MODULE_ID,
                        (const struct hw_module_t**)&dev);
    if (ret != 0) {
        printf("Failed to load module: %d\n", ret);
        return -1;
    }

    // 调用设备方法
    if (dev->say_hello) {
        dev->say_hello(dev, "World");
    }

    // 关闭设备
    dev->common.close(&dev->common);

    return 0;
}

这个测试程序会走一遍完整的流程:加载模块 → 打开设备 → 使用设备 → 关闭设备。如果每一步都返回0,那你的模块就基本没问题了。

核心要点回顾:
  • 头文件定义模块ID和设备结构体
  • open方法负责分配内存、初始化设备、返回设备指针
  • close方法负责释放资源
  • HAL_MODULE_INFO_SYM是模块的入口点,必须导出
  • 模块命名规则:模块名.变种

好了,第一个HAL模块就写完了。说实话,这个模块除了能打印一句"Hello"之外,什么实际功能都没有。但它的骨架是完整的——你以后写任何HAL模块,不管是传感器、摄像头还是音频,结构都是这样。

下一章我们会在这个基础上,加入真正的硬件操作。比如读写寄存器、处理中断什么的。到时候你会发现,今天写的这些基础代码,其实一点都没白费。