4. HDF驱动框架基础

驱动初始化,说白了就是让内核认识你的硬件,并准备好干活。很多新手一上来就盯着硬件寄存器调,结果驱动加载就崩了——其实问题往往出在入口函数没写对。这一章,我们就来拆解 OpenHarmony 驱动框架里最核心的四个函数:DriverEntry、Bind、Init 和 Release。

4.1 驱动入口:DriverEntry 到底干了什么?

DriverEntry 是驱动的第一个入口点,类似 C 程序的 main 函数。但注意,它不负责硬件初始化,而是做两件事:

  • 注册驱动对象到 HDF(Hardware Driver Foundation)
  • 声明驱动对外提供的服务接口

我个人习惯在 DriverEntry 里只放注册逻辑,绝不碰硬件。为什么?因为此时设备可能还没上电,或者总线还没枚举完。你想想看,一上来就去读寄存器,大概率读到 0xFF 或者挂死。

// 一个典型的 DriverEntry 实现
static int32_t MyDriverEntry(struct HdfDriverEntry *driverEntry)
{
    // 注册驱动对象,HDF 框架会管理它的生命周期
    return HdfDriverRegister(driverEntry);
}

// 驱动入口结构体定义
struct HdfDriverEntry g_myDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_MY_DRIVER",
    .Bind = MyDriverBind,
    .Init = MyDriverInit,
    .Release = MyDriverRelease,
};

关键点:DriverEntry 返回后,HDF 框架会立即调用 Bind 函数。如果 Bind 失败,整个驱动加载流程就终止了。

4.2 Bind 函数:搭起驱动与设备的桥梁

Bind 函数负责把驱动对象和设备对象绑定在一起。说白了,就是告诉框架:“我这个驱动,管的是哪个设备”。

我在项目中遇到过一种情况:Bind 里分配了资源,但 Init 失败了,结果资源没释放,导致内存泄漏。嗯,这里要注意——Bind 和 Init 的失败处理要分开写。

// Bind 函数典型实现
static int32_t MyDriverBind(struct HdfDeviceObject *deviceObject)
{
    // 为设备对象分配私有数据
    deviceObject->priv = (struct MyDevice *)OsalMemAlloc(sizeof(struct MyDevice));
    if (deviceObject->priv == NULL) {
        return HDF_ERR_MALLOC_FAIL;
    }
    // 初始化设备私有数据
    memset(deviceObject->priv, 0, sizeof(struct MyDevice));
    return HDF_SUCCESS;
}
函数 调用时机 典型失败原因
Bind DriverEntry 之后立即调用 内存分配失败、设备树匹配失败
Init Bind 成功之后调用 硬件初始化失败、中断申请失败
Release 驱动卸载或设备移除时 资源未完全释放、回调未注销

4.3 Init 函数:真正的硬件初始化战场

Init 函数才是真正操作硬件的地方。在这里,你要完成:

  • 寄存器配置
  • 中断申请
  • DMA 缓冲区分配
  • 设备状态初始化

我曾经在 Init 里踩过一个坑:申请中断时用了错误的 IRQ 号,结果驱动加载成功了,但一触发中断就 panic。排查了两天才发现是设备树里的中断号写错了。所以,我建议在 Init 里加一段硬件自检逻辑,哪怕只是读一个状态寄存器确认设备是否在线。

static int32_t MyDriverInit(struct HdfDeviceObject *deviceObject)
{
    struct MyDevice *dev = (struct MyDevice *)deviceObject->priv;
    int32_t ret;

    // 1. 映射寄存器地址
    dev->regBase = OsalIoRemap(MY_DEVICE_BASE_ADDR, MY_DEVICE_REG_SIZE);
    if (dev->regBase == NULL) {
        return HDF_ERR_IO;
    }

    // 2. 申请中断
    ret = OsalRegisterIrq(dev->irqNum, MY_DEVICE_IRQ_FLAGS, MyIrqHandler, "my_device", dev);
    if (ret != HDF_SUCCESS) {
        OsalIoUnmap(dev->regBase);
        return ret;
    }

    // 3. 硬件自检:读设备 ID 寄存器
    uint32_t devId = OSAL_READL(dev->regBase + MY_DEVICE_ID_REG);
    if (devId != EXPECTED_DEVICE_ID) {
        OsalUnregisterIrq(dev->irqNum, dev);
        OsalIoUnmap(dev->regBase);
        return HDF_ERR_DEVICE_ID;
    }

    return HDF_SUCCESS;
}

警告:Init 函数中每申请一个资源,都要有对应的释放路径。否则一旦中间步骤失败,前面的资源就泄漏了。我见过最夸张的案例,一次 Init 失败泄漏了 4 块内存和 2 个中断。

4.4 Release 函数:优雅退场的艺术

Release 函数是 Init 的逆操作。它负责在驱动卸载时释放所有资源。很多开发者不重视 Release,觉得驱动卸载不常见。但你要知道,在 OpenHarmony 的动态加载场景下,驱动可能因为热插拔、电源管理等原因被频繁卸载和加载。

Release 的编写原则很简单:Init 里申请了什么,Release 里就释放什么。顺序要反过来,就像堆栈一样。

static void MyDriverRelease(struct HdfDeviceObject *deviceObject)
{
    struct MyDevice *dev = (struct MyDevice *)deviceObject->priv;
    if (dev == NULL) {
        return;
    }

    // 1. 先注销中断
    OsalUnregisterIrq(dev->irqNum, dev);

    // 2. 取消寄存器映射
    if (dev->regBase != NULL) {
        OsalIoUnmap(dev->regBase);
        dev->regBase = NULL;
    }

    // 3. 释放私有数据
    OsalMemFree(dev);
    deviceObject->priv = NULL;
}

4.5 调用链全景:从加载到卸载

下面这张图展示了完整的驱动生命周期调用链。我建议你把它存下来,调试时对照着看。

驱动初始化与卸载调用链 驱动生命周期调用链 加载阶段 卸载阶段 DriverEntry Bind Init Bind 失败 → 终止 Init 失败 → 回滚 卸载请求 Release 资源回收 注意:Init 失败时,框架会自动调用 Release 清理已分配的资源 但前提是你的 Release 函数能正确处理部分初始化的状态

4.6 避坑指南:我踩过的几个坑

  1. Bind 里做硬件操作——设备可能还没就绪,读寄存器全是 0xFF。Bind 只做软件层面的绑定。
  2. Init 失败不释放 Bind 的资源——框架不会自动帮你清理 Bind 里分配的内存,你得自己在 Release 里处理。
  3. Release 里忘记检查指针是否为空——如果 Init 中途失败,某些资源可能还没分配,Release 里一上来就释放空指针,直接 panic。
  4. DriverEntry 里写复杂逻辑——DriverEntry 是在内核启动早期调用的,此时很多子系统还没初始化。越简单越好。

调试小技巧:在 DriverEntry、Bind、Init 和 Release 的入口和出口各加一条 HDF_LOGI 日志,带上函数名和返回值。这样驱动加载失败时,看一眼日志就知道卡在哪一步了。我自己的项目里一直保留着这个习惯,排查问题效率翻倍。

4.7 小结

驱动初始化的四个函数,各有各的职责。DriverEntry 是注册入口,Bind 是搭桥,Init 是干活,Release 是善后。记住一个原则:谁申请,谁释放;先申请,后释放。把这个链条理清了,驱动加载阶段的 80% 问题都能快速定位。