4、编写第一个虚拟传感器:创建HAL模块、实现open/close/activate/batch等接口
好,咱们进入实战环节了。
前面几章我们把理论框架搭好了,HAL的架构、数据结构、加载流程都过了一遍。现在,是时候亲手写一个虚拟传感器了。
为什么叫「虚拟」?说白了,它不依赖任何真实硬件。数据是我们自己模拟的。这在开发初期特别有用——你想想看,硬件还没调通,驱动还没写好,但上层框架已经等着要数据了。这时候,一个虚拟传感器就能帮你把整个链路跑通。
我在项目中遇到过好几次这种情况:硬件延期了,但上层应用开发不能等。我们就靠虚拟传感器先跑起来,等硬件就绪了,替换掉底层实现就行。省心得很。
4.1 创建HAL模块的基本骨架
每个HAL模块,本质上就是一个动态库。它需要暴露两个关键符号:HAL_MODULE_INFO_SYM 和 open 函数。
嗯,这里要注意: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。
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 里释放干净。否则就会内存泄漏。
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;
}
这里每个字段都有讲究。比如 minDelay 和 maxDelay 定义了传感器支持的采样率范围。上层会在这个范围内选择合适的采样率。如果你的虚拟传感器只支持固定频率,就把 minDelay 和 maxDelay 设成同一个值。
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、添加校准功能等等。
下一章,我们会深入讲解如何让这个虚拟传感器支持事件上报和批量处理。到时候,你的传感器就能真正「动」起来了。