3、HAL模块编写:编写第一个HAL模块、实现open和close方法、注册模块
好,咱们现在开始动手写代码。
前面两章我们把HAL的架构和数据结构理了一遍。说实话,光看结构体定义确实有点枯燥。但这些东西就像盖房子的地基,不打好后面真会出问题。我记得刚入行那会儿,有个项目就是因为我没搞懂hw_module_t和hw_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_t、HELLO_HARDWARE_MODULE_ID |
| hello.c | 实现open、close、注册模块 | hello_device_open、hello_device_close、HAL_MODULE_INFO_SYM |
| Android.mk | 编译脚本,生成.so文件 | LOCAL_MODULE := hello.default |
说到编译脚本,我顺便提一句。HAL模块的命名规则是模块名.变种,比如hello.default。这个.default表示这是默认实现。如果你有多个硬件变种,可以写成hello.vendor1、hello.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模块,不管是传感器、摄像头还是音频,结构都是这样。
下一章我们会在这个基础上,加入真正的硬件操作。比如读写寄存器、处理中断什么的。到时候你会发现,今天写的这些基础代码,其实一点都没白费。