3、HDF核心数据结构:HdfDriverEntry、HdfDeviceObject、HdfDevice、IUnknown接口

好,咱们今天来啃HDF驱动框架里最核心的几个数据结构。说实话,我刚接触鸿蒙驱动开发那会儿,看着这几个结构体也是一头雾水——名字长得像,功能又重叠,稍不留神就搞混了。

但后来我悟了:这几个结构体,其实就是驱动在HDF世界里的“身份证”、“户口本”和“工作证”。搞懂了它们,你写驱动时就知道该往哪儿填代码了。

3.1 HdfDriverEntry——驱动的“身份证”

每个驱动都得有一个HdfDriverEntry。它告诉HDF框架:你是谁?你从哪来?你要干啥?

说白了,这就是驱动的入口描述符。框架加载驱动时,第一个找的就是它。

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);     // 释放
};

你看,就5个成员。但每个都关键。

成员 作用 我踩过的坑
moduleVersion 版本号,用于兼容性检查 曾经忘了赋值,结果框架一直报版本不匹配
moduleName 驱动名字,HDF用它来匹配设备 名字必须和hcs配置里的一致,大小写敏感
Bind 绑定阶段,把驱动接口挂到设备对象上 这个阶段别做资源申请,容易出问题
Init 初始化阶段,真正干活的地方 我习惯在这里做硬件初始化
Release 卸载时清理资源 别忘了释放中断和DMA缓冲区
我的习惯:Bind里只做接口挂载,Init里做资源申请和硬件初始化。这样职责清晰,出了问题也好排查。

3.2 HdfDeviceObject——驱动的“户口本”

HdfDeviceObject是驱动和HDF框架之间的通信桥梁。每个驱动实例都有一个对应的HdfDeviceObject。

你想想看,驱动要访问硬件资源、要获取配置信息、要跟其他驱动通信——这些信息都存在HdfDeviceObject里。

struct HdfDeviceObject {
    struct IDeviceIoService  *service;       // 服务接口
    struct HdfDevice        *device;         // 指向HdfDevice
    struct HdfDriver        *driver;         // 指向驱动描述
    void                    *priv;           // 私有数据
    struct HdfObject        *object;         // 对象基类
    ...
};

这里我要特别强调priv指针。它是个void*,你可以把驱动的私有数据结构挂上去。我在项目里经常这么用:

// 定义私有数据结构
struct MyGpioDevice {
    uint32_t baseAddr;
    uint32_t irqNum;
    struct OsalMutex mutex;
};

// 在Init里挂载
int32_t MyGpioInit(struct HdfDeviceObject *device) {
    struct MyGpioDevice *priv = OsalMemCalloc(sizeof(struct MyGpioDevice));
    priv->baseAddr = 0x20000000;
    priv->irqNum = 42;
    device->priv = priv;  // 挂上去
    return HDF_SUCCESS;
}
注意:priv指向的内存一定要在Release里释放!我曾经漏过一次,结果内存泄漏查了两天。

3.3 HdfDevice——驱动的“工作证”

HdfDevice是HDF框架内部管理的设备节点。它比HdfDeviceObject更底层,包含了设备在框架中的完整生命周期信息。

说实话,普通驱动开发者很少直接操作HdfDevice。它更多是框架内部用的。但理解它有助于你理解HDF的设备管理机制。

struct HdfDevice {
    struct HdfObject            object;          // 对象基类
    struct HdfDeviceNode        *node;           // 设备节点
    struct HdfDeviceObject      *deviceObject;   // 关联的HdfDeviceObject
    struct HdfDriver            *driver;         // 驱动描述
    struct HdfDevice            *parent;         // 父设备
    struct HdfDevice            *child;          // 子设备链表
    ...
};

你看,HdfDevice里包含了HdfDeviceObject的指针。它们的关系是这样的:

  • HdfDevice:框架视角的设备节点,管理设备树
  • HdfDeviceObject:驱动视角的设备对象,提供接口
  • HdfDriverEntry:驱动入口,定义生命周期回调
HDF核心数据结构关系图 HdfDriverEntry moduleVersion moduleName Bind / Init / Release HdfDeviceObject service priv device / driver HdfDevice deviceObject parent / child driver 创建 包含 关键关系 HdfDriverEntry 定义驱动生命周期 → 框架创建 HdfDeviceObject HdfDeviceObject 包含 priv 供驱动使用 → 内部关联 HdfDevice 管理设备树 Bind阶段 Init阶段 Release阶段 驱动生命周期:Bind(挂接口)→ Init(初始化)→ Release(清理)

3.4 IUnknown接口——驱动的“服务窗口”

IUnknown是HDF里实现驱动间通信的核心接口。它本质上是一个多态接口基类,类似于COM里的IUnknown。

为什么要搞这么复杂?你想想看,一个GPIO驱动要给I2C驱动提供服务,总不能直接调用对方函数吧?那样耦合太深了。IUnknown就是解决这个问题的:通过接口查询,动态获取服务。

struct IUnknown {
    int32_t (*QueryInterface)(struct IUnknown *unknown, int32_t ver, void **out);
    int32_t (*AddRef)(struct IUnknown *unknown);
    int32_t (*Release)(struct IUnknown *unknown);
};

三个方法:

  • QueryInterface:查询指定版本的接口
  • AddRef:增加引用计数
  • Release:减少引用计数,减到0就释放
实际用法:驱动A想调用驱动B的服务,先通过HDF框架拿到驱动B的HdfDeviceObject,然后调用QueryInterface获取B的服务接口指针。这样A和B就解耦了。

3.5 实战:写一个完整的驱动骨架

光说不练假把式。咱们把上面四个结构体串起来,写一个完整的驱动骨架:

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

#define HDF_LOG_TAG my_gpio_driver

// 私有数据结构
struct MyGpioDevice {
    uint32_t baseAddr;
    uint32_t irqNum;
};

// Bind:挂接口
static int32_t MyGpioBind(struct HdfDeviceObject *device) {
    HDF_LOGI("MyGpioBind: enter");
    // 这里只做接口挂载,不做资源申请
    return HDF_SUCCESS;
}

// Init:初始化
static int32_t MyGpioInit(struct HdfDeviceObject *device) {
    HDF_LOGI("MyGpioInit: enter");

    struct MyGpioDevice *priv = OsalMemCalloc(sizeof(struct MyGpioDevice));
    if (priv == NULL) {
        return HDF_ERR_MALLOC_FAIL;
    }

    // 从hcs配置读取硬件信息
    priv->baseAddr = 0x20000000;
    priv->irqNum = 42;

    // 挂到device上
    device->priv = priv;

    // 硬件初始化
    // ...

    HDF_LOGI("MyGpioInit: success");
    return HDF_SUCCESS;
}

// Release:释放
static void MyGpioRelease(struct HdfDeviceObject *device) {
    HDF_LOGI("MyGpioRelease: enter");

    if (device->priv != NULL) {
        OsalMemFree(device->priv);
        device->priv = NULL;
    }

    HDF_LOGI("MyGpioRelease: done");
}

// 驱动入口
struct HdfDriverEntry g_myGpioDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "my_gpio_driver",
    .Bind = MyGpioBind,
    .Init = MyGpioInit,
    .Release = MyGpioRelease,
};

// 注册驱动
HDF_INIT(g_myGpioDriverEntry);
我的经验:写驱动时,先把骨架搭好——Bind、Init、Release三个函数都写上,哪怕Init里只打印一行日志。这样框架加载时就能看到你的驱动了,后续再慢慢填充功能。

3.6 避坑指南

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

  • moduleName必须唯一:HDF框架用名字来匹配驱动和设备,重名会导致加载失败。我习惯用"厂商_模块_功能"的命名方式。
  • Bind里别做资源申请:Bind阶段框架还没准备好,这时候申请资源容易出问题。资源申请放Init里。
  • Release里别忘了清理:priv、中断、DMA、定时器——一个都不能少。我吃过亏,后来写了个清理清单,每次对照着检查。
  • IUnknown的引用计数要小心:多线程环境下,AddRef和Release必须成对出现,否则要么内存泄漏,要么野指针崩溃。

嗯,这几个数据结构就讲到这里。你把这些搞明白了,HDF驱动开发就算入门了。剩下的就是多写、多调试、多踩坑——经验就是这么积累出来的。