3. HDF 核心概念:驱动对象、设备对象、服务接口与 HCS 解析
好,咱们进入 HDF 框架的核心地带。说实话,我刚接触 HDF 时,也被这几个概念绕得有点晕——驱动对象、设备对象、服务接口,还有那个 HCS 配置文件。它们到底什么关系?怎么配合?
我个人的理解是:驱动对象是“人”,设备对象是“岗位”,服务接口是“技能”,HCS 是“招聘简章”。你想想看,一个驱动要干活,总得知道自己管哪个设备、提供什么能力、怎么被系统发现吧?HDF 把这四件事拆得清清楚楚。
3.1 驱动对象(DriverObject)——驱动的“身份证”
每个 HDF 驱动在加载时,内核都会给它分配一个 DriverObject。这东西记录了驱动的名字、厂商、版本,以及最重要的——生命周期回调函数。
核心结构体:
struct DriverObject {
uint32_t objectId; // 对象ID,全局唯一
const char *driverName; // 驱动名称,如 "hdf_uart"
const char *vendorName; // 厂商名称
uint32_t version; // 驱动版本号
int32_t (*Bind)(struct HdfDeviceObject *device);
int32_t (*Init)(struct HdfDeviceObject *device);
void (*Release)(struct HdfDeviceObject *device);
struct DriverObject *next; // 链表指针,用于挂载多个驱动
};
这里我重点说三个回调:
- Bind:驱动和设备“配对”。系统读到 HCS 配置后,调用 Bind 把驱动对象绑定到具体的设备对象上。我在项目中遇到过 Bind 返回失败的情况,查了半天发现是 HCS 里的设备ID写错了。
- Init:真正的初始化。分配内存、注册中断、配置寄存器,都在这里做。
- Release:卸载时清理。嗯,这个很多人容易忘,我曾经就因为 Release 里没释放 DMA 缓冲区,导致系统反复加载卸载后内存泄漏。
我的习惯:在 Bind 里只做轻量级校验,别搞复杂逻辑。Init 才是干重活的地方。这样如果设备不存在,Bind 快速失败,不会拖慢系统启动。
3.2 设备对象(DeviceObject)——驱动的“工位”
设备对象描述的是硬件实例。一个 UART 控制器、一个 GPIO 引脚组,在 HDF 里都对应一个 HdfDeviceObject。它包含了设备号、资源信息、私有数据指针等。
struct HdfDeviceObject {
uint32_t deviceId; // 设备ID,与HCS中的device_id对应
uint32_t deviceClass; // 设备类别,如 DISPLAY、INPUT、SENSOR
struct DeviceResourceNode *property; // 指向HCS解析后的属性树
void *priv; // 私有数据,驱动自己用
struct HdfDeviceObject *next; // 链表指针
};
注意这个 property 字段——它指向 HCS 配置解析后的属性节点。说白了,你在 HCS 里写的那些 reg_base、irq_num,最终都挂在这里。驱动通过 DeviceResourceNode 接口读取配置,不用自己解析文本。
避坑指南:我曾经在适配一款传感器时,发现 Init 里读到的 property 是空指针。排查后发现 HCS 文件里设备节点的 match_attr 写错了,导致驱动找不到对应的配置块。记住:HCS 里的 match_attr 必须和驱动代码里调用 DeviceResourceGet 时传入的 key 完全一致。
3.3 服务接口(ServiceInterface)——驱动的“对外窗口”
驱动不能只闷头干活,还得给上层提供调用入口。HDF 的服务接口机制,说白了就是把驱动的能力暴露给用户态或其他内核模块。
服务接口的定义很简单:
struct HdfServiceInterface {
const char *serviceName; // 服务名称,如 "uart_service_0"
void *(*Create)(struct HdfDeviceObject *device);
int32_t (*Dispatch)(struct HdfDeviceIoService *service,
int32_t cmd,
struct HdfSBuf *data,
struct HdfSBuf *reply);
};
这里 Dispatch 是核心——上层通过它发送命令码(cmd)和数据(data),驱动处理后返回结果(reply)。我习惯把命令码定义成枚举,比如:
enum UartCmd {
UART_CMD_OPEN,
UART_CMD_CLOSE,
UART_CMD_SET_BAUD,
UART_CMD_SEND,
UART_CMD_RECV,
};
这样上层调用时语义清晰,驱动里用 switch-case 分发也方便。
服务注册流程:
- 在 Init 里调用
HdfDeviceRegisterService(device, serviceInterface) - 系统把服务名和接口指针注册到全局服务表
- 上层通过
HdfDeviceServiceGet("uart_service_0")获取服务句柄 - 调用
service->Dispatch(...)完成通信
3.4 HCS 配置解析——驱动的“说明书”
HCS(HDF Configuration Source)是 HDF 的专属配置文件格式。它用缩进和键值对描述硬件信息,比 DTS(Device Tree Source)更简洁,而且支持条件编译和宏展开。
一个典型的 HCS 片段长这样:
root {
uart_config {
match_attr = "hdf_uart_config";
device_id = 0;
reg_base = 0xFE010000;
reg_size = 0x1000;
irq_num = 32;
baud_rate = 115200;
data_bits = 8;
stop_bits = 1;
parity = "none";
};
};
驱动里怎么读?用 DeviceResourceGet 系列 API:
// 在 Init 中
struct DeviceResourceNode *node = device->property;
uint32_t regBase;
int32_t ret = DeviceResourceGetUint32(node, "reg_base", ®Base, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("Failed to get reg_base");
return HDF_FAILURE;
}
我建议:把所有 HCS 键名定义成宏,放在一个头文件里。比如 #define HCS_KEY_REG_BASE "reg_base"。这样如果配置项改名,只需要改一处,不会出现字符串散落在代码里到处找的尴尬。
3.5 四者关系全景图
说了这么多,咱们用一张图把关系理清楚:
从上图能看明白:HCS 是源头,驱动对象通过 Bind 绑定到设备对象,设备对象持有 HCS 解析后的属性,驱动初始化完成后注册服务接口,上层通过 Dispatch 与驱动交互。环环相扣,缺一不可。
3.6 关键对比:HDF vs 传统 Linux 驱动模型
为了帮你更快理解,我列个对比表:
| 概念 | HDF | 传统 Linux 驱动 |
|---|---|---|
| 驱动描述 | DriverObject(结构体 + 回调) | file_operations + platform_driver |
| 设备描述 | HdfDeviceObject(属性树挂载) | device_node(DTS 解析) |
| 配置方式 | HCS(缩进式,支持宏) | DTS(设备树,语法更严格) |
| 服务暴露 | ServiceInterface + Dispatch | ioctl / sysfs / uevent |
| 生命周期 | Bind → Init → Release | probe → remove |
说白了,HDF 把 Linux 驱动里那些“约定俗成”的东西,用一套标准框架固化下来了。你只要按这个套路走,代码结构自然清晰,跨平台移植也方便。
本章小结:
- DriverObject 是驱动的“身份证”,包含 Bind/Init/Release 三个核心回调
- DeviceObject 是硬件实例的抽象,通过 property 持有 HCS 配置
- ServiceInterface 是驱动的对外窗口,通过 Dispatch 处理上层请求
- HCS 是配置源头,用缩进和键值对描述硬件信息,驱动通过 DeviceResourceGet 读取