4、HAL层开发实战:编写HAL模块、实现Stub接口、注册到ServiceManager

好,咱们今天来点真格的。

前面几章我们把HAL的理论框架聊透了,什么HAL是硬件抽象层啊,什么Stub是桩接口啊,什么ServiceManager是服务大管家啊……嗯,这些概念你肯定都记住了。但说实话,光记住没用,你得亲手写一遍才能真正理解。

我记得我第一次做HAL开发时,对着AOSP的源码看了三天,觉得自己全会了。结果一动手,编译报错、找不到符号、ServiceManager注册失败……折腾了一整天才跑通。所以今天这章,我带你一步步走一遍,把坑都踩平了。

4.1 整体架构:HAL模块到底长什么样?

先给你画个图,让你心里有个谱。

HAL模块整体架构 App / Framework (Java层) JNI 层 (C/C++) HAL Service (我们自己写的模块) Stub 接口实现 注册到 ServiceManager 图:HAL模块在Android系统中的位置与交互流程

说白了,我们今天的任务就是:写中间那个“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_thw_device_t是Android HAL框架定义的基础结构体,我们必须在自己的结构体里把它们作为第一个成员。这是规矩,不能乱。

💡 我的习惯:头文件里的函数指针命名,我一般用动词_名词的格式,比如get_temperatureset_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.变种。变种可以是defaultgoldfishranchu等,对应不同的硬件平台。

⚠️ 避坑指南:我曾经把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 完整流程回顾

好,咱们把今天的内容串起来,看看整个流程长什么样:

  1. 写头文件:定义temperature_module_ttemperature_device_t,声明函数指针。
  2. 写实现:实现temperature_device_openget_temperatureset_threshold,定义HAL_MODULE_INFO_SYM
  3. 写编译脚本:Android.mk,注意模块命名规则和安装路径。
  4. 写注册代码:在系统服务中调用hw_get_module加载模块,打开设备。
  5. 编译烧录mmm hardware/libhardware/modules/temperature/,然后make snod打包。

嗯,其实就这么几步。但每一步都有坑,我当年踩过的坑都写在上面了,你照着做应该能一次跑通。

最后说一句:HAL开发说白了就是“定义接口 -> 实现接口 -> 注册接口”这三板斧。不管你是做温度传感器、指纹识别还是摄像头,套路都一样。把今天这个例子吃透了,后面遇到任何HAL模块你都能举一反三。


公众号:蓝海资料掘金营,微信deep3321