4. 第一个HDF驱动:创建HDF驱动项目、驱动入口函数与用户态通信
好,咱们今天正式动手写第一个HDF驱动。说实话,很多初学者看到HDF那套框架代码会觉得头大——又是Bind又是Init,还有Release,感觉比普通Linux驱动复杂不少。但你别被吓到,这套流程其实很固定,说白了就是「三段式」生命周期管理。
我个人习惯是先把骨架搭起来,再往里填肉。咱们今天就干这件事:从零创建一个HDF驱动项目,把三个入口函数写明白,然后部署到开发板上,最后让用户态程序跟内核驱动说上话。
4.1 HDF驱动项目的目录结构
先看看一个标准的HDF驱动长什么样。我以Hi3516DV300平台为例,你在其他芯片上做也是一样的套路。
// 驱动源码目录建议
drivers/hdf_core/adapter/khdf/linux/platform/
└── my_hdf_driver/
├── Makefile
├── my_hdf_driver.c // 驱动主文件
├── my_hdf_driver.h // 头文件
└── hcs/
└── my_hdf_driver.hcs // 配置文件
嗯,这里要注意:HDF驱动必须配一个.hcs配置文件,这是HDF框架用来做设备匹配和资源管理的核心。没有它,驱动加载不起来。
| 文件 | 作用 | 是否必须 |
|---|---|---|
my_hdf_driver.c |
驱动主逻辑,包含Bind/Init/Release | 是 |
my_hdf_driver.h |
对外接口声明、数据结构定义 | 推荐 |
my_hdf_driver.hcs |
设备配置、资源描述 | 是 |
Makefile |
编译脚本,链接到HDF框架 | 是 |
4.2 驱动入口函数:Bind、Init、Release
HDF驱动的生命周期就靠这三个函数撑着。我刚开始接触时也觉得奇怪——为什么Linux驱动一个probe就够了,HDF要搞三个?后来踩过坑才明白,这是为了把「资源绑定」和「业务初始化」分开,方便做热插拔和电源管理。
4.2.1 Bind:绑定服务接口
Bind阶段干的事很简单:把你的驱动服务注册到HDF框架的服务管理器中。说白了就是告诉系统「我来了,我能提供这些接口」。
// my_hdf_driver.c
#include "hdf_device_desc.h"
#include "hdf_log.h"
#define HDF_LOG_TAG my_hdf_driver
// 服务接口结构体
struct MyDriverService {
struct IDeviceIoService ioService;
int (*DoSomething)(void);
};
// 实际业务函数
static int MyDriverDoSomething(void)
{
HDF_LOGI("MyDriver: DoSomething called");
return 0;
}
// Bind函数:注册服务
static int32_t MyDriverBind(struct HdfDeviceObject *device)
{
struct MyDriverService *service = NULL;
if (device == NULL) {
HDF_LOGE("MyDriverBind: device is NULL");
return HDF_ERR_INVALID_PARAM;
}
service = (struct MyDriverService *)OsalMemAlloc(sizeof(struct MyDriverService));
if (service == NULL) {
HDF_LOGE("MyDriverBind: failed to alloc service");
return HDF_ERR_MALLOC_FAIL;
}
service->ioService.Dispatch = NULL; // 后面讲通信时会用到
service->DoSomething = MyDriverDoSomething;
device->service = &service->ioService;
HDF_LOGI("MyDriverBind: success");
return HDF_SUCCESS;
}
4.2.2 Init:初始化硬件与资源
Init阶段才是真正的初始化。这时候设备已经就绪,你可以操作寄存器、申请中断、创建内核线程了。
// Init函数:初始化硬件
static int32_t MyDriverInit(struct HdfDeviceObject *device)
{
(void)device;
HDF_LOGI("MyDriverInit: enter");
// 这里做真正的初始化工作
// 比如:映射寄存器地址、配置GPIO、注册中断
// int ret = MyDriverHardwareInit();
// if (ret != HDF_SUCCESS) {
// HDF_LOGE("MyDriverInit: hardware init failed");
// return ret;
// }
HDF_LOGI("MyDriverInit: success");
return HDF_SUCCESS;
}
4.2.3 Release:释放资源
Release是驱动的「善后」函数。驱动卸载时调用,释放所有申请的资源。很多新手会忽略这个函数,结果卸载驱动后内存泄漏,系统越来越慢。
// Release函数:释放资源
static void MyDriverRelease(struct HdfDeviceObject *device)
{
struct MyDriverService *service = NULL;
if (device == NULL || device->service == NULL) {
HDF_LOGE("MyDriverRelease: invalid param");
return;
}
service = (struct MyDriverService *)device->service;
// 释放硬件资源
// MyDriverHardwareDeinit();
OsalMemFree(service);
device->service = NULL;
HDF_LOGI("MyDriverRelease: success");
}
4.3 驱动入口注册与HCS配置
三个函数写好了,怎么告诉HDF框架「这就是我的驱动」?需要注册一个入口描述符。
// 驱动入口描述符
struct HdfDriverEntry g_myDriverEntry = {
.moduleVersion = 1,
.moduleName = "my_hdf_driver",
.Bind = MyDriverBind,
.Init = MyDriverInit,
.Release = MyDriverRelease,
};
// 宏注册
HDF_INIT(g_myDriverEntry);
同时,HCS配置文件里要声明这个设备:
// my_hdf_driver.hcs
root {
platform {
my_hdf_driver {
match_attr = "my_hdf_driver";
// 可以加自定义属性
// gpio_num = 10;
// irq_num = 38;
}
}
}
match_attr是HDF用来匹配设备和驱动的关键字段,必须跟驱动代码里的一致。
4.4 驱动编译与部署
编译HDF驱动有两种方式:编译进内核镜像,或者编译成独立模块。我建议前期调试用独立模块,方便快速迭代。
# Makefile
include $(LITEOSTOPDIR)/config.mk
MODULE_NAME := my_hdf_driver
LOCAL_SRCS := my_hdf_driver.c
LOCAL_INCLUDE := \
-I$(LITEOSTOPDIR)/drivers/hdf_core/framework/include \
-I$(LITEOSTOPDIR)/drivers/hdf_core/adapter/khdf/linux/include
LOCAL_CFLAGS := -fstack-protector-strong
include $(HDF_DRIVER)
编译命令:
# 编译整个内核(包含驱动)
hb build -f
# 或者单独编译驱动模块
make -C out/kernel/linux-5.10/ M=$(PWD)/drivers/hdf_core/adapter/khdf/linux/platform/my_hdf_driver modules
部署到开发板:
# 通过NFS挂载或scp拷贝
scp my_hdf_driver.ko root@192.168.1.100:/lib/modules/
# 加载驱动
insmod /lib/modules/my_hdf_driver.ko
# 查看驱动是否加载成功
cat /proc/hdf/driver_list
验证方法:执行cat /proc/hdf/driver_list,如果看到my_hdf_driver出现在列表中,说明Bind和Init都执行成功了。如果没出现,检查dmesg看错误日志。
4.5 用户态与内核态通信
驱动写好了,用户态程序怎么调用它?HDF提供了一套标准的IO通信机制——通过Dispatch接口实现。
4.5.1 内核端:实现Dispatch
在Bind函数里,我们留了个service->ioService.Dispatch = NULL,现在把它补上:
// 命令码定义
enum MyDriverCmd {
CMD_DO_SOMETHING = 0,
CMD_GET_STATUS,
};
// Dispatch函数:处理用户态请求
static int32_t MyDriverDispatch(struct HdfDeviceIoClient *client,
int cmdId,
struct HdfSBuf *data,
struct HdfSBuf *reply)
{
(void)client;
switch (cmdId) {
case CMD_DO_SOMETHING:
HDF_LOGI("Dispatch: CMD_DO_SOMETHING");
// 从data中读取参数
// 执行操作
// 结果写入reply
return HDF_SUCCESS;
case CMD_GET_STATUS:
HDF_LOGI("Dispatch: CMD_GET_STATUS");
// 读取状态并返回
return HDF_SUCCESS;
default:
HDF_LOGE("Dispatch: unknown cmd %d", cmdId);
return HDF_ERR_NOT_SUPPORT;
}
}
// 修改Bind,注册Dispatch
static int32_t MyDriverBind(struct HdfDeviceObject *device)
{
// ... 前面的代码不变 ...
service->ioService.Dispatch = MyDriverDispatch; // 注册分发函数
// ...
}
4.5.2 用户态:打开设备并发送命令
用户态程序通过HDF提供的用户态API来操作驱动:
// user_app.c
#include "hdf_io_service_if.h"
int main(void)
{
struct HdfIoService *service = NULL;
struct HdfSBuf *data = NULL;
struct HdfSBuf *reply = NULL;
int ret;
// 1. 打开驱动服务
service = HdfIoServiceBind("my_hdf_driver");
if (service == NULL) {
printf("Failed to bind service\n");
return -1;
}
// 2. 准备数据缓冲区
data = HdfSBufObtainDefaultSize();
reply = HdfSBufObtainDefaultSize();
// 3. 发送命令
ret = service->dispatcher->Dispatch(
service->dispatcher,
CMD_DO_SOMETHING,
data,
reply);
if (ret != HDF_SUCCESS) {
printf("Dispatch failed: %d\n", ret);
}
// 4. 清理
HdfSBufRecycle(data);
HdfSBufRecycle(reply);
HdfIoServiceRecycle(service);
return 0;
}
BUILD.gn里加上deps = ["//drivers/hdf_core/framework/model/network/hdf_io_service:libhdf_io_service"]。我第一次编译时忘了加,链接报错找了半天。
4.6 本章知识结构图
下面这张图把整个流程串起来了,从驱动创建到用户态通信,一目了然:
4.7 常见问题与避坑
- 驱动加载失败:检查HCS配置里的
match_attr是否跟驱动代码里的moduleName一致。我遇到过两次都是因为拼写错误。 - 用户态无法绑定服务:确认驱动已经加载成功(
cat /proc/hdf/driver_list),并且用户态程序有权限访问HDF设备节点。 - Dispatch收不到数据:检查SBuf的读写顺序——先写后读,别搞反了。我曾经在用户态先读后写,数据一直为空,排查了半天。
- Release没被调用:驱动卸载时如果还有用户态程序持有服务引用,Release不会执行。先关掉所有用户态程序再卸载驱动。
好了,第一个HDF驱动就这么写出来了。从Bind到Init再到Release,加上用户态通信,这套流程在后续所有HDF驱动开发中都会反复用到。你把这个骨架跑通了,后面加功能就是往里面填代码的事。