3、HDF驱动模型基础:HDF驱动对象、HDF设备对象、HDF服务对象、驱动入口函数与加载流程

好,咱们今天来聊聊HDF驱动模型的核心骨架。说白了,就是搞清楚驱动在OpenHarmony里到底长什么样、怎么被系统找到、又是怎么跑起来的。

我刚开始接触HDF的时候,其实挺懵的。以前做Linux驱动,一个module_init就完事了。到了HDF这里,突然冒出来三个对象:驱动对象、设备对象、服务对象。我当时就想,搞这么复杂干嘛?后来踩了几个坑才明白——这恰恰是HDF的精髓所在。

3.1 HDF驱动对象:驱动的“身份证”

每个HDF驱动,都必须有一个驱动对象。你可以把它理解成驱动的“身份证”。系统要加载你,首先得找到这张身份证。

驱动对象用结构体 HdfDriverEntry 来描述。我习惯叫它“驱动入口描述符”。

struct HdfDriverEntry {
    uint32_t moduleVersion;      // 驱动版本号
    const char *moduleName;      // 驱动名字,必须唯一
    int32_t (*Bind)(struct HdfDeviceObject *device);    // 绑定服务
    int32_t (*Init)(struct HdfDeviceObject *device);    // 初始化硬件
    void (*Release)(struct HdfDeviceObject *device);    // 释放资源
};

这里面最重要的,就是三个函数指针:Bind、Init、Release。它们构成了驱动的生命周期。

关键点:moduleName 必须和配置文件里的名字一致。我曾经因为大小写写错,折腾了一下午才找到问题。嗯,血的教训。

3.2 HDF设备对象:驱动的“工作台”

设备对象 HdfDeviceObject 是驱动和硬件之间的桥梁。你可以把它想象成你的工作台——上面放着各种工具(资源)、图纸(配置信息)。

struct HdfDeviceObject {
    struct IDeviceIoService *service;   // 服务接口
    struct HdfDeviceObject *parent;     // 父设备
    void *priv;                         // 私有数据
    struct HdfDeviceResource *resource; // 硬件资源
    // ... 还有其他字段
};

我个人习惯把硬件相关的信息,比如GPIO号、I2C地址、中断号,都塞到 priv 里。这样代码结构清晰,也方便调试。

小技巧:在Bind阶段,你可以把设备对象保存到全局变量里。这样Init和Release阶段就能直接用了。我早期没这么做,结果每次都要从参数里取,代码写得很啰嗦。

3.3 HDF服务对象:驱动的“对外窗口”

服务对象 IDeviceIoService 是驱动暴露给上层的能力。上层应用或者框架层,通过这个对象来调用你的驱动。

说白了,这就是驱动的API接口。你提供什么功能,全看你怎么设计这个服务对象。

struct SensorService {
    struct IDeviceIoService ioService;  // 必须继承这个
    int32_t (*Open)(void);
    int32_t (*Close)(void);
    int32_t (*ReadData)(struct SensorData *data);
};

我做过一个温湿度传感器驱动,服务对象里就三个接口:打开、读取、关闭。简单明了。你想想看,上层调用方根本不需要知道底层I2C怎么操作,它只需要调用 ReadData 就行了。

3.4 驱动入口函数与加载流程

好,现在我们把三个对象串起来,看看驱动到底是怎么加载的。

加载流程分三步走:

  1. Bind阶段:系统调用你的Bind函数。这时候你要做的是:创建设备对象、绑定服务接口。说白了,就是搭架子。
  2. Init阶段:系统调用Init函数。这时候你要做的是:初始化硬件、注册中断、分配内存。说白了,就是真正干活。
  3. Release阶段:系统卸载驱动时调用。你要做的是:释放资源、关闭硬件。说白了,就是打扫战场。

注意:Bind和Init是分开的。Bind失败,Init不会执行。Init失败,Release会被调用。这个顺序一定要记清楚。我曾经在Init里分配了内存,结果Bind失败后忘了释放,导致内存泄漏。后来加了个标志位才解决。

下面这张图,是我自己画的加载流程。你看一眼就明白了。

HDF驱动加载流程图 系统启动 / 驱动加载 步骤1: 根据HCS配置查找驱动 步骤2: 调用 Bind() 绑定服务 成功? 失败 错误处理 / 返回失败 成功 步骤3: 调用 Init() 初始化硬件

你看,流程其实不复杂。关键是每个阶段该做什么,心里要有数。

3.5 实战:一个简单的GPIO驱动骨架

光说不练假把式。我给你写个最简单的GPIO驱动骨架,你感受一下。

// gpio_driver.c
#include "hdf_log.h"
#include "hdf_device_desc.h"

// 服务对象
struct GpioService {
    struct IDeviceIoService ioService;
    int gpioNum;
};

// Bind:绑定服务
static int32_t GpioBind(struct HdfDeviceObject *device)
{
    struct GpioService *service = NULL;
    
    service = (struct GpioService *)OsalMemCalloc(sizeof(struct GpioService));
    if (service == NULL) {
        HDF_LOGE("GpioBind: malloc failed");
        return HDF_FAILURE;
    }
    
    service->gpioNum = 123;  // 假设GPIO号
    device->service = &service->ioService;
    
    HDF_LOGI("GpioBind: success");
    return HDF_SUCCESS;
}

// Init:初始化硬件
static int32_t GpioInit(struct HdfDeviceObject *device)
{
    struct GpioService *service = (struct GpioService *)device->service;
    
    // 这里做真正的GPIO初始化
    HDF_LOGI("GpioInit: gpio %d initialized", service->gpioNum);
    
    return HDF_SUCCESS;
}

// Release:释放资源
static void GpioRelease(struct HdfDeviceObject *device)
{
    struct GpioService *service = (struct GpioService *)device->service;
    
    if (service != NULL) {
        OsalMemFree(service);
    }
    
    HDF_LOGI("GpioRelease: done");
}

// 驱动入口
struct HdfDriverEntry g_gpioDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_GPIO_DEMO",
    .Bind = GpioBind,
    .Init = GpioInit,
    .Release = GpioRelease,
};

// 注册驱动
HDF_INIT(g_gpioDriverEntry);

个人经验:我建议你在Bind阶段只做内存分配和服务绑定,别碰硬件。硬件初始化放到Init里。为什么?因为Bind失败时,系统不会调用Release。如果你在Bind里操作了硬件,万一失败,硬件状态就乱了。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • moduleName重复:系统里不能有两个同名的驱动。我曾经复制粘贴代码,忘了改名字,结果两个驱动互相打架。
  • Bind里别做耗时操作:系统加载驱动是有超时机制的。你在Bind里sleep 5秒,系统直接把你踢了。
  • Release要幂等:Release可能被调用多次。你释放过的资源,再释放一次不能崩溃。加个标志位判断一下。
  • 日志要打全:每个阶段都打日志。出问题的时候,看日志就知道卡在哪一步了。我习惯在Bind、Init、Release的入口和出口都打日志。

好了,HDF驱动模型的基础就这些。你把这个骨架跑通了,后面写传感器驱动就顺了。


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