4、HAL层开发实战:编写HAL模块、实现Stub接口、注册到ServiceManager
好,咱们今天来点真格的。
前面几章我们把HAL的理论框架聊透了,什么HAL是硬件抽象层啊,什么Stub是桩接口啊,什么ServiceManager是服务大管家啊……嗯,这些概念你肯定都记住了。但说实话,光记住没用,你得亲手写一遍才能真正理解。
我记得我第一次做HAL开发时,对着AOSP的源码看了三天,觉得自己全会了。结果一动手,编译报错、找不到符号、ServiceManager注册失败……折腾了一整天才跑通。所以今天这章,我带你一步步走一遍,把坑都踩平了。
4.1 整体架构:HAL模块到底长什么样?
先给你画个图,让你心里有个谱。
说白了,我们今天的任务就是:写中间那个“HAL Service”模块,然后让它左边实现Stub接口,右边注册到ServiceManager。这样上层App通过JNI就能调用到我们的硬件服务了。
4.2 第一步:编写HAL模块的头文件
我习惯先写头文件。为什么?因为头文件就是契约,定义了接口长什么样,后面实现起来心里有底。
假设我们要做一个虚拟温度传感器的HAL模块。功能很简单:读取当前温度、设置温度阈值。
// hardware/libhardware/include/hardware/temperature.h
#ifndef ANDROID_TEMPERATURE_INTERFACE_H
#define ANDROID_TEMPERATURE_INTERFACE_H
#include <hardware/hardware.h>
__BEGIN_DECLS
/* 模块ID,ServiceManager通过这个字符串找到我们 */
#define TEMPERATURE_HARDWARE_MODULE_ID "temperature"
/* 设备ID,同一个模块下可以有多个设备 */
#define TEMPERATURE_DEVICE_ID "temperature_sensor"
/* 温度模块的结构体 */
struct temperature_module_t {
struct hw_module_t common;
/* 这里可以放模块级别的函数指针,比如初始化 */
};
/* 温度设备的结构体 */
struct temperature_device_t {
struct hw_device_t common;
/* 读取当前温度,单位:摄氏度 */
int (*get_temperature)(struct temperature_device_t* dev, float* temp);
/* 设置温度阈值,超过阈值触发回调 */
int (*set_threshold)(struct temperature_device_t* dev, float threshold);
};
__END_DECLS
#endif // ANDROID_TEMPERATURE_INTERFACE_H
嗯,这里要注意:hw_module_t和hw_device_t是Android HAL框架定义的基础结构体,我们必须在自己的结构体里把它们作为第一个成员。这是规矩,不能乱。
动词_名词的格式,比如get_temperature、set_threshold。这样上层调用时一眼就能看出是干什么的。
4.3 第二步:实现HAL模块的Stub接口
头文件写好了,接下来是实现。这里就是所谓的“Stub接口”——说白了,就是给上层一个桩,让上层能调得动我们的硬件。
我直接给你看完整的实现代码,然后一行行解释。
// hardware/libhardware/modules/temperature/temperature.c
#include <hardware/temperature.h>
#include <string.h>
#include <stdlib.h>
#include <log/log.h>
/* 设备打开函数 */
static int temperature_device_open(const struct hw_module_t* module,
const char* name,
struct hw_device_t** device) {
// 我曾经在这里犯过一个低级错误:忘了检查name是否匹配
// 结果上层传了个"temperature_sensor_v2",我直接返回了NULL
// 导致ServiceManager注册失败,查了一下午……
if (strcmp(name, TEMPERATURE_DEVICE_ID) != 0) {
ALOGE("Device name mismatch: %s", name);
return -EINVAL;
}
struct temperature_device_t* dev = malloc(sizeof(struct temperature_device_t));
if (dev == NULL) {
return -ENOMEM;
}
memset(dev, 0, sizeof(*dev));
/* 填充hw_device_t的公共字段 */
dev->common.tag = HARDWARE_DEVICE_TAG;
dev->common.version = 1;
dev->common.module = (struct hw_module_t*)module;
dev->common.close = NULL; // 简单实现,不处理close
/* 挂载我们的Stub函数 */
dev->get_temperature = temperature_get_temperature;
dev->set_threshold = temperature_set_threshold;
*device = &dev->common;
return 0;
}
/* Stub函数:获取温度 */
static int temperature_get_temperature(struct temperature_device_t* dev,
float* temp) {
// 这里模拟读取硬件寄存器
// 真实项目中你会操作/dev/i2c-1或者/dev/gpio之类的节点
*temp = 25.6f; // 假装读到了25.6度
ALOGI("get_temperature: %.1f°C", *temp);
return 0;
}
/* Stub函数:设置阈值 */
static int temperature_set_threshold(struct temperature_device_t* dev,
float threshold) {
// 这里模拟写入硬件寄存器
ALOGI("set_threshold: %.1f°C", threshold);
return 0;
}
/* 模块的公共方法表 */
static struct hw_module_methods_t temperature_module_methods = {
.open = temperature_device_open,
};
/* 模块实例,注意这个符号名是固定的 */
struct temperature_module_t HAL_MODULE_INFO_SYM = {
.common = {
.tag = HARDWARE_MODULE_TAG,
.version_major = 1,
.version_minor = 0,
.id = TEMPERATURE_HARDWARE_MODULE_ID,
.name = "Temperature HAL Module",
.author = "Your Name",
.methods = &temperature_module_methods,
},
};
核心要点:
HAL_MODULE_INFO_SYM这个宏展开后是HMI,是HAL框架查找模块的入口点。名字不能改,改了框架就找不到你的模块了。temperature_device_open是模块的“工厂函数”,上层通过它拿到设备句柄。- Stub函数里目前是模拟数据,真实项目中你会在这里操作硬件寄存器。
4.4 第三步:编写Android.mk编译脚本
代码写完了,怎么编译?Android系统里用Android.mk或者Android.bp。我习惯用Android.mk,因为更直观。
# hardware/libhardware/modules/temperature/Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := temperature.default
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_SRC_FILES := temperature.c
LOCAL_SHARED_LIBRARIES := liblog libhardware
LOCAL_MODULE_TAGS := optional
include $(BUILD_SHARED_LIBRARY)
注意LOCAL_MODULE的名字:temperature.default。这个命名规则是模块ID.变种。变种可以是default、goldfish、ranchu等,对应不同的硬件平台。
LOCAL_MODULE_RELATIVE_PATH写成了lib/hw,结果编译出来的.so文件放到了/system/lib/hw/下面。但Android HAL框架默认搜索路径是/vendor/lib/hw/和/system/lib/hw/,如果你的设备是Treble架构,必须放到/vendor分区下。否则ServiceManager死活找不到你的模块。
4.5 第四步:注册到ServiceManager
模块编译好了,怎么让上层知道它的存在?这就轮到ServiceManager出场了。
在Android 8.0之后,Google推荐使用HIDL方式注册服务。但咱们这里讲的是传统HAL方式,也就是通过hw_get_module来加载模块。这种方式在低版本Android和某些嵌入式场景中仍然广泛使用。
注册过程其实很简单,就是写一个小的守护进程,或者直接在某个系统服务里调用hw_get_module。
// 在某个系统服务中(比如temperature_service.cpp)
#include <hardware/temperature.h>
void init_temperature_service() {
temperature_module_t* module = NULL;
temperature_device_t* device = NULL;
// 1. 加载HAL模块
int ret = hw_get_module(TEMPERATURE_HARDWARE_MODULE_ID,
(const hw_module_t**)&module);
if (ret != 0) {
ALOGE("Failed to load temperature module: %d", ret);
return;
}
// 2. 打开设备
ret = module->common.methods->open(
(const hw_module_t*)module,
TEMPERATURE_DEVICE_ID,
(hw_device_t**)&device);
if (ret != 0) {
ALOGE("Failed to open temperature device: %d", ret);
return;
}
// 3. 注册到ServiceManager(传统方式)
// 注意:这里需要你自己实现一个ServiceManager的注册逻辑
// 或者通过defaultServiceManager()注册到binder服务中
ALOGI("Temperature HAL module loaded and device opened successfully!");
// 4. 测试调用
float temp = 0;
device->get_temperature(device, &temp);
ALOGI("Initial temperature: %.1f°C", temp);
}
你可能会问:“hw_get_module到底是怎么找到我的.so文件的?”
好问题。它的搜索路径是这样的:
| 优先级 | 搜索路径 | 说明 |
|---|---|---|
| 1 | /vendor/lib/hw/模块ID.变种.so |
Treble架构首选路径 |
| 2 | /system/lib/hw/模块ID.变种.so |
传统路径 |
| 3 | /odm/lib/hw/模块ID.变种.so |
ODM定制路径 |
变种名称由系统属性ro.hardware决定。比如你的设备是ro.hardware=ranchu,那么hw_get_module会先找temperature.ranchu.so,找不到再找temperature.default.so。
adb shell ls /vendor/lib/hw/ | grep temperature查看.so文件是否存在。然后用adb shell logcat -s temperature看我们的ALOGI日志有没有打印出来。
4.6 完整流程回顾
好,咱们把今天的内容串起来,看看整个流程长什么样:
- 写头文件:定义
temperature_module_t和temperature_device_t,声明函数指针。 - 写实现:实现
temperature_device_open、get_temperature、set_threshold,定义HAL_MODULE_INFO_SYM。 - 写编译脚本:Android.mk,注意模块命名规则和安装路径。
- 写注册代码:在系统服务中调用
hw_get_module加载模块,打开设备。 - 编译烧录:
mmm hardware/libhardware/modules/temperature/,然后make snod打包。
嗯,其实就这么几步。但每一步都有坑,我当年踩过的坑都写在上面了,你照着做应该能一次跑通。
最后说一句:HAL开发说白了就是“定义接口 -> 实现接口 -> 注册接口”这三板斧。不管你是做温度传感器、指纹识别还是摄像头,套路都一样。把今天这个例子吃透了,后面遇到任何HAL模块你都能举一反三。
公众号:蓝海资料掘金营,微信deep3321