2. HDF驱动模型核心概念
好,咱们今天聊聊HDF驱动模型里最核心的几个概念。说实话,我刚接触HDF那会儿,也被这些对象绕得有点晕。驱动对象、设备对象、服务对象,还有入口出口函数——它们到底是什么关系?
我个人的理解是:驱动对象是“人”,设备对象是“工具”,服务对象是“接口”。你想想看,一个工人(驱动对象)操作一把电钻(设备对象),通过电钻的开关(服务对象)对外提供打孔服务。嗯,这个比喻虽然糙了点,但道理是通的。
2.1 驱动对象(Driver Object)
驱动对象,说白了就是驱动在HDF框架里的“身份证”。每个驱动在加载时,框架都会为它创建一个HdfDriverObject实例。这个对象里存了什么?驱动的名字、入口函数指针、私有数据,还有它绑定的设备列表。
我记得第一次看HDF源码时,发现驱动对象其实是个很轻量的结构体。它不负责具体硬件操作,更像一个“调度中心”。
核心要点:驱动对象是HDF管理驱动的单元。一个驱动对象对应一个.ko文件(或静态编译的驱动模块)。
// 驱动对象的核心结构(简化版)
struct HdfDriverObject {
const char *driverName; // 驱动名字
int (*bind)(struct HdfDeviceObject *device); // 绑定函数
int (*init)(struct HdfDeviceObject *device); // 初始化函数
void (*release)(struct HdfDeviceObject *device); // 释放函数
void *priv; // 私有数据
};
这里要注意:bind、init、release这三个函数指针,就是咱们后面要讲的驱动入口和出口函数。它们由驱动开发者实现,HDF框架在合适的时机调用。
2.2 设备对象(Device Object)
设备对象,代表的是“一个具体的硬件设备实例”。比如你的开发板上有两个I2C控制器,那就会有两个设备对象。每个设备对象都挂载在某个驱动对象下面。
我曾经在项目中踩过一个坑:以为一个驱动对象只能对应一个设备对象。后来调试时发现,HDF支持一个驱动管理多个同类型设备。比如GPIO驱动,可以同时控制多组GPIO控制器。每个控制器就是一个独立的设备对象。
我的习惯:在写驱动时,我会把设备对象的私有数据(priv字段)用来存放硬件资源信息,比如寄存器基地址、中断号、时钟句柄等。这样每个设备实例都有自己的“小仓库”。
// 设备对象的核心结构(简化版)
struct HdfDeviceObject {
struct HdfDriverObject *driver; // 所属驱动对象
struct HdfDeviceObject *next; // 链表指针,用于挂载同驱动下的其他设备
const char *deviceName; // 设备名字
const char *deviceMatchAttr; // 匹配属性(用于设备树匹配)
void *priv; // 私有数据
struct HdfServiceObject *service;// 服务对象(后面讲)
};
你可能会问:驱动对象和设备对象是怎么关联起来的?答案是——通过bind函数。在驱动加载时,HDF框架会调用驱动的bind函数,把设备对象和驱动对象“绑”在一起。
2.3 服务对象(Service Object)
服务对象,是驱动对外暴露的“接口层”。用户态程序不能直接操作设备对象,但可以通过服务对象来调用驱动的能力。说白了,服务对象就是驱动和上层应用之间的“翻译官”。
HDF支持两种服务发布方式:
- 用户态服务:通过HDF的IPC机制,用户态程序可以像调用本地函数一样调用驱动服务
- 内核态服务:其他内核模块可以直接获取服务对象指针,调用其方法
避坑指南:我曾经在项目里把服务对象设计得过于复杂,一个服务接口塞了十几个函数。后来发现,服务接口应该尽量精简。我的建议是:每个服务对象只暴露5-8个核心接口,复杂的业务逻辑放在上层封装。
// 服务对象的典型定义
struct HdfServiceObject {
struct HdfDeviceObject *device; // 所属设备对象
const char *serviceName; // 服务名字
struct HdfObject *object; // 服务实例(实际的功能实现)
int (*dispatch)(struct HdfObject *object, int cmd, ...); // 消息分发函数
};
服务对象和设备对象的关系是一对一的。一个设备对象可以发布一个服务对象,但一个服务对象只能属于一个设备对象。嗯,这个关系要记清楚。
2.4 驱动入口与出口函数
每个HDF驱动都必须实现三个关键函数:bind、init、release。它们就是驱动的“生老病死”管理函数。
| 函数 | 调用时机 | 主要职责 | 返回值 |
|---|---|---|---|
bind |
驱动加载时,最先调用 | 绑定设备对象,初始化服务对象 | 成功返回0,失败返回负数 |
init |
bind成功后调用 |
初始化硬件、申请资源、注册中断 | 成功返回0,失败返回负数 |
release |
驱动卸载时调用 | 释放资源、注销中断、关闭硬件 | 无返回值(void) |
我个人习惯的写法是:bind里只做“轻量级”工作,比如分配服务对象、设置回调函数。真正的硬件初始化放到init里。为什么?因为bind失败时,驱动还能尝试重新绑定;但init失败时,驱动就直接退出了。把重试逻辑放在bind里更安全。
// 一个典型的驱动入口出口实现
static int MyDriverBind(struct HdfDeviceObject *device)
{
// 1. 分配私有数据
device->priv = kzalloc(sizeof(struct MyDevicePriv), GFP_KERNEL);
if (!device->priv) {
return -ENOMEM;
}
// 2. 创建服务对象(可选)
// 这里只做准备工作,不操作硬件
return 0;
}
static int MyDriverInit(struct HdfDeviceObject *device)
{
struct MyDevicePriv *priv = (struct MyDevicePriv *)device->priv;
// 1. 获取硬件资源(寄存器地址、中断号等)
// 2. 初始化硬件(复位、配置时钟等)
// 3. 注册中断处理函数
// 4. 注册服务到HDF框架
return 0;
}
static void MyDriverRelease(struct HdfDeviceObject *device)
{
struct MyDevicePriv *priv = (struct MyDevicePriv *)device->priv;
// 1. 注销服务
// 2. 释放中断
// 3. 关闭硬件
// 4. 释放私有数据内存
kfree(priv);
device->priv = NULL;
}
重要提醒:release函数必须能处理“init只执行了一半”的情况。比如init里申请了三个资源,但在申请第二个时失败了,release要能正确释放第一个资源。我曾经因为没处理好这个,导致内存泄漏,排查了两天才找到原因。
2.5 三个对象的关系总结
咱们用一张图来理清关系(嗯,虽然HTML里不能画图,但我用文字描述一下):
- 驱动对象(HdfDriverObject):管理驱动的生命周期,包含bind/init/release函数指针
- 设备对象(HdfDeviceObject):代表具体的硬件实例,挂载在驱动对象下
- 服务对象(HdfServiceObject):对外暴露的接口,挂载在设备对象下
它们的关系是:1个驱动对象 → N个设备对象 → 每个设备对象可挂载0或1个服务对象。
你想想看,这种分层设计的好处是什么?驱动对象负责“管理”,设备对象负责“实例”,服务对象负责“通信”。各司其职,互不干扰。我在做项目时,经常利用这种分层来解耦:驱动层只关心硬件操作,服务层只关心接口协议,上层应用只关心业务逻辑。
我的小技巧:在调试时,可以通过/sys/kernel/hdf目录下的文件查看驱动对象、设备对象和服务对象的注册情况。这个目录是HDF框架自动生成的调试接口,非常有用。
好了,这一章的核心概念就讲到这里。下一章咱们会深入bind函数的实现细节,包括如何解析设备树、如何匹配硬件资源。到时候我会分享一个我在项目里遇到的设备树匹配失败的案例,保证让你印象深刻。