第2章 HDF驱动框架入门:核心概念、驱动模型分层、HCS解析与入口函数实现
好,咱们正式开始啃HDF这块硬骨头。很多刚接触鸿蒙南向开发的朋友,一上来就被HDF的各种概念砸晕了。其实没那么玄乎,HDF说白了就是一套帮你把驱动写得更规范、更好维护的框架。我最早接触它的时候,也觉得文档绕,但真正上手写过两个驱动之后,就发现它的设计确实有道理。
2.1 HDF核心概念:你必须要搞懂的三个东西
HDF里最核心的概念,我个人总结就三个:驱动对象、设备对象、服务接口。你把这仨理清了,后面就顺了。
| 概念 | 一句话解释 | 我习惯把它比作 |
|---|---|---|
| 驱动对象 | 驱动代码的实体,负责初始化和卸载 | 一个“工人”,知道怎么干活 |
| 设备对象 | 硬件资源的抽象,包含寄存器地址、中断号等 | 工人的“工具箱”,里面装着工具 |
| 服务接口 | 驱动暴露给上层调用的函数指针集合 | 工人的“服务菜单”,你能点啥服务 |
驱动对象通过设备对象拿到硬件资源,然后通过服务接口对外提供服务。这三者通过HDF框架绑定在一起。嗯,这里要注意:驱动对象和设备对象是一对一的关系吗?不一定。一个驱动对象可以管理多个设备对象,比如同一个I2C控制器驱动,可以挂多个不同地址的从设备。
2.2 驱动模型分层:为什么非要分三层?
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描述硬件资源,驱动入口读取配置并初始化硬件,最后通过服务接口对外提供功能。这套机制,说白了就是让驱动开发变得有章可循,不再是一堆散乱的寄存器操作。