第2章 HDF驱动框架入门:核心概念、驱动模型分层、HCS解析与入口函数实现

好,咱们正式开始啃HDF这块硬骨头。很多刚接触鸿蒙南向开发的朋友,一上来就被HDF的各种概念砸晕了。其实没那么玄乎,HDF说白了就是一套帮你把驱动写得更规范、更好维护的框架。我最早接触它的时候,也觉得文档绕,但真正上手写过两个驱动之后,就发现它的设计确实有道理。

2.1 HDF核心概念:你必须要搞懂的三个东西

HDF里最核心的概念,我个人总结就三个:驱动对象、设备对象、服务接口。你把这仨理清了,后面就顺了。

概念 一句话解释 我习惯把它比作
驱动对象 驱动代码的实体,负责初始化和卸载 一个“工人”,知道怎么干活
设备对象 硬件资源的抽象,包含寄存器地址、中断号等 工人的“工具箱”,里面装着工具
服务接口 驱动暴露给上层调用的函数指针集合 工人的“服务菜单”,你能点啥服务

驱动对象通过设备对象拿到硬件资源,然后通过服务接口对外提供服务。这三者通过HDF框架绑定在一起。嗯,这里要注意:驱动对象和设备对象是一对一的关系吗?不一定。一个驱动对象可以管理多个设备对象,比如同一个I2C控制器驱动,可以挂多个不同地址的从设备。

2.2 驱动模型分层:为什么非要分三层?

HDF的驱动模型分了清晰的层级。你想想看,如果不分层,所有代码揉在一起,换一个芯片平台就得重写整个驱动,那得多痛苦?

HDF的分层设计,我个人觉得是它最聪明的地方。它把驱动拆成了三层:

  • 驱动实现层:最底层,直接操作寄存器。这部分跟硬件强相关,换芯片就得改。
  • 驱动管理层:中间层,负责驱动注册、设备匹配、生命周期管理。这部分是框架提供的,基本不用动。
  • 驱动服务层:最上层,封装成统一接口给应用或服务调用。这部分是面向业务的,跟具体硬件解耦。

我在项目中遇到过一个问题:有个同事把寄存器操作和业务逻辑全写在一个文件里,结果换了个传感器型号,改代码改到崩溃。后来按HDF的分层思想重构,驱动实现层只留读写寄存器,业务逻辑放服务层,再换传感器只需要改底层文件,上层完全不动。

核心原则:驱动实现层只关心“怎么读写硬件”,驱动服务层只关心“提供什么功能”。中间的管理层,交给HDF框架。

HDF驱动模型分层结构图 HDF驱动模型分层结构 驱动服务层 对外提供统一接口(读/写/配置) 驱动管理层 驱动注册、设备匹配、生命周期管理 驱动实现层 寄存器操作、硬件时序、中断处理 业务逻辑 与硬件解耦 框架提供 基本不用改 硬件相关 换平台必改

2.3 驱动配置文件HCS解析:驱动怎么知道硬件信息?

驱动要干活,总得知道寄存器地址、中断号、时钟频率这些信息吧?这些信息从哪来?就是HCS文件。HCS全称是HDF Configuration Source,是一种树形结构的配置文件。

我第一次看HCS文件时,觉得它跟设备树有点像,但又不完全一样。HCS是鸿蒙自己的一套配置描述语言,语法更简洁,解析效率也更高。

一个典型的HCS文件长这样:

root {
    sensor_config {
        board_config {
            i2c_bus = 1;        // I2C总线号
            i2c_addr = 0x76;    // 设备地址
            irq_gpio = 12;      // 中断引脚
            reg_base = 0x40005000; // 寄存器基地址
            reg_size = 0x1000;      // 寄存器范围
        }
        driver_config {
            poll_interval = 100;    // 轮询间隔(ms)
            power_mode = 1;         // 电源模式
        }
    }
}

这里有几个要点:

  • root是根节点,所有配置都在它下面。
  • 节点可以嵌套,用花括号表示层级。
  • 属性用key = value;的形式,分号结尾。
  • 支持注释,用//或/* */。

小技巧:我习惯把硬件相关的配置(寄存器地址、中断号)和软件相关的配置(轮询间隔、工作模式)分开放在不同的节点下。这样换硬件平台时,只需要改board_config,driver_config基本不用动。

HCS文件写好后,会被编译成HCB二进制文件,运行时由HDF框架解析。驱动通过DeviceResourceIface接口来读取配置值。具体怎么读,我们下一节讲入口函数时会看到。

2.4 驱动入口函数实现:从哪开始写代码?

好了,概念讲完了,咱们来写点真东西。每个HDF驱动都必须实现一个入口函数,这是驱动的起点。入口函数的名字有固定格式:

// 驱动入口函数
int32_t HdfSensorDriverBind(struct HdfDeviceObject *deviceObject)
{
    // 绑定服务接口
    return HDF_SUCCESS;
}

int32_t HdfSensorDriverInit(struct HdfDeviceObject *deviceObject)
{
    // 初始化硬件、读取配置、注册中断
    return HDF_SUCCESS;
}

void HdfSensorDriverRelease(struct HdfDeviceObject *deviceObject)
{
    // 释放资源、注销中断
}

这三个函数是HDF驱动的标准入口:

函数 调用时机 我一般在这里做什么
Bind 驱动加载时最先调用 绑定服务接口,把驱动的功能函数注册到框架
Init Bind成功后调用 读取HCS配置、初始化硬件、申请资源
Release 驱动卸载时调用 释放内存、关闭设备、注销中断

我曾经踩过一个坑:在Init里申请了DMA缓冲区,但在Release里忘了释放。结果驱动反复加载卸载几次后,内存泄漏导致系统卡死。从那以后,我每次写Release函数都会对照Init,确保每个申请都有对应的释放。

下面是一个完整的入口函数示例,包含了读取HCS配置:

// 从HCS读取配置
static int32_t ReadSensorConfig(const struct DeviceResourceNode *node, struct SensorConfig *config)
{
    struct DeviceResourceIface *iface = DeviceResourceGetIfaceInstance();
    if (iface == NULL) {
        return HDF_ERR_INVALID_PARAM;
    }
    
    // 读取I2C地址
    if (iface->GetUint32(node, "i2c_addr", &config->i2cAddr, 0) != HDF_SUCCESS) {
        HDF_LOGE("Failed to read i2c_addr");
        return HDF_FAILURE;
    }
    
    // 读取中断GPIO号
    if (iface->GetUint32(node, "irq_gpio", &config->irqGpio, 0) != HDF_SUCCESS) {
        HDF_LOGE("Failed to read irq_gpio");
        return HDF_FAILURE;
    }
    
    return HDF_SUCCESS;
}

// 驱动初始化
int32_t HdfSensorDriverInit(struct HdfDeviceObject *deviceObject)
{
    if (deviceObject == NULL || deviceObject->property == NULL) {
        return HDF_ERR_INVALID_PARAM;
    }
    
    // 读取HCS配置
    struct SensorConfig config;
    int32_t ret = ReadSensorConfig(deviceObject->property, &config);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("Read config failed");
        return ret;
    }
    
    // 初始化I2C控制器
    ret = InitI2cController(config.i2cAddr);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("I2C init failed");
        return ret;
    }
    
    // 注册中断
    ret = RegisterIrq(config.irqGpio);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("IRQ register failed");
        return ret;
    }
    
    HDF_LOGI("Sensor driver init success");
    return HDF_SUCCESS;
}

注意:Init函数里如果某一步失败了,一定要记得回滚已经成功的操作。比如I2C初始化成功了,但中断注册失败了,你得把I2C关掉再返回错误。否则硬件资源就泄漏了。

最后,还需要一个驱动入口描述符,告诉HDF框架你的入口函数在哪:

struct HdfDriverEntry g_sensorDriverEntry = {
    .moduleVersion = 1,
    .moduleName = "HDF_SENSOR",
    .Bind = HdfSensorDriverBind,
    .Init = HdfSensorDriverInit,
    .Release = HdfSensorDriverRelease,
};

// 注册驱动
HDF_INIT(g_sensorDriverEntry);

HDF_INIT这个宏会把驱动入口注册到HDF框架的驱动链表中。驱动加载时,框架会根据moduleName在HCS中查找匹配的设备节点,然后调用对应的Bind、Init函数。

嗯,到这里,HDF驱动框架的核心脉络应该清楚了。你想想看,从配置文件到入口函数,整个链路其实很清晰:HCS描述硬件资源,驱动入口读取配置并初始化硬件,最后通过服务接口对外提供功能。这套机制,说白了就是让驱动开发变得有章可循,不再是一堆散乱的寄存器操作。