4、编写第一个虚拟传感器:创建HAL模块、实现open/close/activate/batch等接口

好,咱们进入实战环节了。

前面几章我们把理论框架搭好了,HAL的架构、数据结构、加载流程都过了一遍。现在,是时候亲手写一个虚拟传感器了。

为什么叫「虚拟」?说白了,它不依赖任何真实硬件。数据是我们自己模拟的。这在开发初期特别有用——你想想看,硬件还没调通,驱动还没写好,但上层框架已经等着要数据了。这时候,一个虚拟传感器就能帮你把整个链路跑通。

我在项目中遇到过好几次这种情况:硬件延期了,但上层应用开发不能等。我们就靠虚拟传感器先跑起来,等硬件就绪了,替换掉底层实现就行。省心得很。

4.1 创建HAL模块的基本骨架

每个HAL模块,本质上就是一个动态库。它需要暴露两个关键符号:HAL_MODULE_INFO_SYMopen 函数。

嗯,这里要注意:HAL_MODULE_INFO_SYM 这个名字是固定的,AOSP的HAL加载器就认这个名字。你改个名字,系统就找不到你的模块了。

先看代码,我习惯这样写模块的入口结构体:

#include <hardware/hardware.h>
#include <hardware/sensors.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/* 模块ID,必须与Android.mk或Android.bp中定义的一致 */
static const char *sensor_module_id = SENSORS_HARDWARE_MODULE_ID;

/* 模块方法结构体 */
static struct hw_module_methods_t sensor_module_methods = {
    .open = sensor_device_open,
};

/* 模块结构体实例 —— 这就是 HAL_MODULE_INFO_SYM */
struct sensors_module_t HAL_MODULE_INFO_SYM = {
    .common = {
        .tag = HARDWARE_MODULE_TAG,
        .module_api_version = SENSORS_MODULE_API_VERSION_1_0,
        .hal_api_version = HARDWARE_HAL_API_VERSION,
        .id = sensor_module_id,
        .name = "Virtual Sensor HAL Module",
        .author = "Your Name",
        .methods = &sensor_module_methods,
        .dso = NULL,
        .reserved = {0},
    },
    .get_sensors_list = sensor_get_sensors_list,
};

看到没?HAL_MODULE_INFO_SYM 就是那个固定的符号名。它里面嵌套了一个 hw_module_t 结构体,这是所有HAL模块的通用头部。后面跟着的是传感器模块特有的字段,比如 get_sensors_list

个人习惯:我一般把模块ID定义成静态常量,而不是直接写在结构体里。这样万一要改,只改一处就行。别问我为什么,问就是吃过亏。

4.2 实现 open 接口

open 函数是HAL模块的入口。上层通过 hw_get_module 加载模块后,就会调用这个函数来打开设备。

说白了,这个函数要做的事情就是:分配一个 sensors_poll_device_t 结构体,把里面的函数指针都填上,然后返回给上层。

static int sensor_device_open(const struct hw_module_t *module,
                              const char __unused *name,
                              struct hw_device_t **device)
{
    struct sensors_poll_device_t *poll_device;

    poll_device = (struct sensors_poll_device_t *)
        calloc(1, sizeof(struct sensors_poll_device_t));
    if (!poll_device) {
        return -ENOMEM;
    }

    /* 填充通用设备头 */
    poll_device->common.tag = HARDWARE_DEVICE_TAG;
    poll_device->common.version = SENSORS_DEVICE_API_VERSION_1_0;
    poll_device->common.module = (struct hw_module_t *)module;
    poll_device->common.close = sensor_device_close;

    /* 填充传感器特有的操作接口 */
    poll_device->activate = sensor_activate;
    poll_device->setDelay = sensor_set_delay;
    poll_device->poll = sensor_poll;
    poll_device->batch = sensor_batch;
    poll_device->flush = sensor_flush;

    *device = &poll_device->common;

    return 0;
}

这里有个细节:calloc 会把分配的内存清零。我建议你养成这个习惯。有一次我用了 malloc,结果结构体里有个指针没初始化,导致上层调用时直接段错误。排查了半天,最后发现是野指针的问题。从那以后,我分配结构体一律用 calloc

4.3 实现 close 接口

close 接口相对简单。就是释放资源,清理状态。

static int sensor_device_close(struct hw_device_t *device)
{
    struct sensors_poll_device_t *poll_device =
        (struct sensors_poll_device_t *)device;

    if (poll_device) {
        /* 这里可以释放传感器相关的资源 */
        free(poll_device);
    }

    return 0;
}

嗯,看起来确实简单。但我要提醒你:如果你的传感器在运行过程中分配了其他资源(比如线程、内存缓冲区、文件描述符),一定要在 close 里释放干净。否则就会内存泄漏。

我曾经踩过的坑:有一个项目,虚拟传感器里开了一个线程来模拟数据上报。close的时候忘了关线程,结果模块卸载后线程还在跑,疯狂往一个已经释放的内存地址写数据。系统直接重启了。所以,close 里一定要做好清理工作。

4.4 实现 activate 接口

activate 是控制传感器开关的接口。上层调用 sensor_activate 时,会传入传感器句柄和使能标志。

static int sensor_activate(struct sensors_poll_device_t *dev,
                           int handle, int enabled)
{
    /* handle 是传感器的唯一标识,对应 get_sensors_list 返回的传感器 */
    if (handle != VIRTUAL_ACCELEROMETER_HANDLE) {
        return -EINVAL;
    }

    if (enabled) {
        /* 启动传感器:创建数据模拟线程、开启定时器等 */
        ALOGI("Virtual accelerometer activated");
        /* 这里可以启动一个定时器或线程来模拟数据 */
    } else {
        /* 停止传感器:停止数据模拟 */
        ALOGI("Virtual accelerometer deactivated");
        /* 停止定时器或线程 */
    }

    return 0;
}

你可能会问:handle 是怎么来的?这个我们在下一节讲 get_sensors_list 时会详细说。现在你只需要知道,每个传感器都有一个唯一的 handle,上层通过这个 handle 来操作具体的传感器。

4.5 实现 batch 接口

batch 接口是Android 4.4(KitKat)引入的。它用来设置传感器的批处理参数:采样率和最大报告延迟。

说白了,就是告诉传感器:「你按这个频率采样,但不用每次都上报,攒够一批再给我。」这样可以降低功耗。

static int sensor_batch(struct sensors_poll_device_t *dev,
                        int handle, int flags,
                        int64_t sampling_period_ns,
                        int64_t max_report_latency_ns)
{
    if (handle != VIRTUAL_ACCELEROMETER_HANDLE) {
        return -EINVAL;
    }

    /* sampling_period_ns: 采样周期,单位纳秒 */
    /* 比如 10ms = 10,000,000 ns,对应 100Hz */
    ALOGI("batch: handle=%d, period=%lld ns, latency=%lld ns",
          handle, (long long)sampling_period_ns,
          (long long)max_report_latency_ns);

    /* 保存采样率,后续 poll 接口按这个频率生成数据 */
    g_virtual_sensor.sampling_period_ns = sampling_period_ns;
    g_virtual_sensor.max_report_latency_ns = max_report_latency_ns;

    return 0;
}

这里有个容易忽略的点:sampling_period_ns 是上层期望的采样周期。但你的虚拟传感器不一定能精确满足这个值。比如上层要 100Hz,你只能做到 50Hz。这时候怎么办?

我的做法是:能精确匹配就精确匹配,匹配不了就取最接近的、比要求更快的那个值。因为上层通常能接受比预期更快的采样率,但不能接受更慢的。

4.6 实现 poll 接口

poll 是传感器数据上报的核心接口。上层会阻塞在这个函数上,等待传感器数据。

static int sensor_poll(struct sensors_poll_device_t *dev,
                       sensors_event_t *data, int count)
{
    /* 这里模拟一个加速度计数据 */
    /* 实际项目中,这里应该从硬件读取数据 */
    static int64_t timestamp = 0;

    if (count < 1) {
        return -EINVAL;
    }

    /* 模拟数据:简单的正弦波 */
    timestamp += g_virtual_sensor.sampling_period_ns;
    data->version = sizeof(sensors_event_t);
    data->sensor = VIRTUAL_ACCELEROMETER_HANDLE;
    data->type = SENSOR_TYPE_ACCELEROMETER;
    data->timestamp = timestamp;
    data->acceleration.x = 9.8 * sin(timestamp * 1e-9);
    data->acceleration.y = 9.8 * cos(timestamp * 1e-9);
    data->acceleration.z = 9.8;
    data->acceleration.status = SENSOR_STATUS_ACCURACY_HIGH;

    return 1; /* 返回上报的事件数量 */
}

注意看:poll 返回的是实际上报的事件数量。如果返回0,上层会认为没有新数据,继续等待。如果返回负数,表示出错。

关键点:在真实硬件中,poll 应该是阻塞的——没有数据就等着,有数据就返回。但在虚拟传感器里,我们可以直接模拟数据,所以不需要真的阻塞。不过,为了更贴近真实场景,我建议你用一个条件变量或信号量来实现阻塞等待,这样代码结构更清晰。

4.7 实现 get_sensors_list 接口

这个接口告诉上层:你的HAL模块支持哪些传感器。

static const struct sensor_t *sensor_get_sensors_list(
    struct sensors_module_t *module)
{
    static const struct sensor_t virtual_sensors[] = {
        {
            .name = "Virtual Accelerometer",
            .vendor = "Your Company",
            .version = 1,
            .handle = VIRTUAL_ACCELEROMETER_HANDLE,
            .type = SENSOR_TYPE_ACCELEROMETER,
            .maxRange = 39.2,       /* ±4g */
            .resolution = 0.01,     /* 0.01 m/s^2 */
            .power = 0.5,           /* 0.5 mA */
            .minDelay = 10000,      /* 最小采样间隔 10ms */
            .maxDelay = 1000000,    /* 最大采样间隔 1s */
            .fifoReservedEventCount = 0,
            .fifoMaxEventCount = 0,
            .stringType = SENSOR_STRING_TYPE_ACCELEROMETER,
            .requiredPermission = NULL,
            .maxDelay = 1000000,
            .flags = SENSOR_FLAG_CONTINUOUS_MODE,
            .reserved = {0},
        },
    };

    return virtual_sensors;
}

这里每个字段都有讲究。比如 minDelaymaxDelay 定义了传感器支持的采样率范围。上层会在这个范围内选择合适的采样率。如果你的虚拟传感器只支持固定频率,就把 minDelaymaxDelay 设成同一个值。

4.8 编译与测试

写完了代码,怎么验证它能不能跑?

我一般会写一个简单的测试程序,直接调用 hw_get_module 加载模块,然后调用各个接口看看返回值。

#include <hardware/hardware.h>
#include <hardware/sensors.h>

int main() {
    const struct hw_module_t *module = NULL;
    struct sensors_poll_device_t *device = NULL;

    /* 加载模块 */
    int ret = hw_get_module(SENSORS_HARDWARE_MODULE_ID, &module);
    if (ret != 0) {
        ALOGE("Failed to load sensor module: %d", ret);
        return -1;
    }

    /* 打开设备 */
    ret = module->methods->open(module, NULL,
                                 (struct hw_device_t **)&device);
    if (ret != 0) {
        ALOGE("Failed to open sensor device: %d", ret);
        return -1;
    }

    /* 激活传感器 */
    device->activate(device, VIRTUAL_ACCELEROMETER_HANDLE, 1);

    /* 读取数据 */
    sensors_event_t event;
    ret = device->poll(device, &event, 1);
    if (ret > 0) {
        ALOGI("Got event: x=%f, y=%f, z=%f",
              event.acceleration.x,
              event.acceleration.y,
              event.acceleration.z);
    }

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

    return 0;
}

把这个测试程序编译成可执行文件,push到设备上跑一下。如果能看到日志输出,说明你的虚拟传感器已经成功跑起来了。

调试小技巧:如果 hw_get_module 返回 -ENOENT,说明系统找不到你的HAL库。检查一下库文件是否放在了正确的路径(通常是 /vendor/lib/hw//system/lib/hw/),以及文件名是否匹配 sensors.<platform>.so 的命名规则。

好了,到这里你已经完成了第一个虚拟传感器的编写。虽然它还很简陋,但骨架已经搭好了。后续我们可以往里面添加更多功能:支持多种传感器类型、实现硬件 FIFO、添加校准功能等等。

下一章,我们会深入讲解如何让这个虚拟传感器支持事件上报和批量处理。到时候,你的传感器就能真正「动」起来了。