“为啥我写的驱动设备不理我?”——自定义设备驱动在 HarmonyOS/OpenHarmony 的开发与注册全流程(HDF + HCS 实战)
本文介绍了在鸿蒙系统中开发设备驱动的关键步骤与注意事项,重点围绕HDF(Huawei Driver Foundation)框架和HCS(Harmony Config System)配置展开。首先,作者强调了驱动开发的核心是"被内核/框架信任并稳定接管硬件",而非简单地通过编译。文章提供了完整的驱动开发流程,从驱动模板创建、设备描述文件编写到编译加载,并以GPIO灯驱动为示例展示
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
先把心里话摆在前面:驱动不是“能编过就算赢”,而是“被内核/框架信任并稳定接管硬件”。在 HarmonyOS/OpenHarmony 生态里,这件事的关键字是 HDF(Huawei Driver Foundation) 和 HCS(Harmony Config System):前者是驱动框架与 API,后者是驱动与设备的“身份证”。本文严格按照你的大纲,带你走一遍可落地的三步:驱动模板 → 设备描述文件 → 编译与加载;并围绕你关心的技术点 hcs 配置 与 HDF API 给出完整样例和踩坑清单。
说明:不同发行分支/芯片平台在目录与 API 名上可能有细微差异,下文以通用、主干思路与常见路径示例,遇到你本仓差异时按实际工程替换路径/宏即可。
一、驱动模板:从“空壳”到“可绑定可初始化”
1.1 HDF 快速心智图
- Host(设备宿主进程/上下文):同一 Host 中可挂多个驱动实例。常见 host 有
platform、i2c_host、spi_host等。 - Driver(驱动模块):实现 Bind/Init/Release 三件套,通过
HdfDriverEntry注册到 DevMgr。 - Device(设备实例):由 HCS 描述,匹配到具体 Driver 后由框架创建
HdfDeviceObject传给你。 - Service(对外服务句柄,可选):你可以把方法表挂到
deviceObject->service,供用户态/其他驱动通过HdfIoService调用。
一句话:DevMgr 读 HCS → 找到 moduleName 匹配的 DriverEntry → 调用 Bind/Init → 设备上线。
1.2 目录建议
/drivers/peripheral/my_led/
├── src/my_led_driver.c
├── include/my_led_driver.h
├── config/my_led.hcs
├── BUILD.gn
└── README.md
1.3 最小可用 Driver 模板(Platform 设备为例)
这个示例实现一个“GPIO 灯”驱动:启动时解析 HCS 里的
gpioNum/activeLow,导出一个简单服务MY_LED_TURN与MY_LED_STATUS。
// src/my_led_driver.c
#include "hdf_device_desc.h"
#include "hdf_log.h"
#include "device_resource_if.h"
#include "hdf_base.h"
#include "osal_mem.h"
#include "hdf_sbuf.h"
// 如接入真实 GPIO,还需包含对应 BUS/Pin 控制头文件,略
#define HDF_LOG_TAG my_led
enum {
IO_CMD_LED_TURN = 1, // data: uint8_t on/off
IO_CMD_LED_STATUS = 2, // resp: uint8_t current
};
struct MyLedDev {
struct HdfDeviceObject *hdfDev;
uint32_t gpioNum;
bool activeLow;
uint8_t state; // 0/1
};
static int32_t MyLedParseConfig(struct MyLedDev *dev, struct HdfDeviceObject *obj)
{
if (dev == NULL || obj == NULL) return HDF_ERR_INVALID_PARAM;
const struct DeviceResourceNode *node = obj->property;
const struct DeviceResourceIface *iface = DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE);
if (iface == NULL || iface->GetUint32 == NULL) {
HDF_LOGE("no resource iface");
return HDF_FAILURE;
}
(void)iface->GetUint32(node, "gpioNum", &dev->gpioNum, 0);
uint32_t al = 0;
(void)iface->GetUint32(node, "activeLow", &al, 0);
dev->activeLow = (al != 0);
HDF_LOGI("parsed gpio=%u activeLow=%d", dev->gpioNum, dev->activeLow);
return HDF_SUCCESS;
}
// 真实项目这里操作 GPIO,这里用占位函数模拟
static void MyLedApplyPin(struct MyLedDev *dev, uint8_t on)
{
dev->state = on ? 1 : 0;
// 若 activeLow,则 on=0 拉低;此处略
HDF_LOGI("LED %s (gpio=%u)", on ? "ON":"OFF", dev->gpioNum);
}
/* ========== 可选:对外服务 ========== */
static int32_t MyLedDispatch(struct HdfDeviceIoClient *client, int cmd,
struct HdfSBuf *data, struct HdfSBuf *reply)
{
if (client == NULL || client->device == NULL) return HDF_ERR_INVALID_PARAM;
struct MyLedDev *dev = (struct MyLedDev *)client->device->priv;
if (dev == NULL) return HDF_FAILURE;
switch (cmd) {
case IO_CMD_LED_TURN: {
if (data == NULL) return HDF_ERR_INVALID_PARAM;
uint8_t on = 0;
if (!HdfSbufReadUint8(data, &on)) return HDF_ERR_IO;
MyLedApplyPin(dev, on);
return HDF_SUCCESS;
}
case IO_CMD_LED_STATUS: {
if (reply == NULL) return HDF_ERR_INVALID_PARAM;
if (!HdfSbufWriteUint8(reply, dev->state)) return HDF_ERR_IO;
return HDF_SUCCESS;
}
default:
return HDF_ERR_NOT_SUPPORT;
}
}
static struct HdfDeviceIoService g_ledService = {
.Dispatch = MyLedDispatch,
.Open = NULL,
.Release = NULL,
};
/* ========== HDF 生命周期 ========== */
static int32_t MyLedBind(struct HdfDeviceObject *obj)
{
if (obj == NULL) return HDF_FAILURE;
struct MyLedDev *dev = (struct MyLedDev *)OsalMemCalloc(sizeof(*dev));
if (dev == NULL) return HDF_ERR_MALLOC_FAIL;
dev->hdfDev = obj;
obj->priv = dev;
// 对外发布服务(可选)
obj->service = &g_ledService;
HDF_LOGI("bind ok");
return HDF_SUCCESS;
}
static int32_t MyLedInit(struct HdfDeviceObject *obj)
{
if (obj == NULL || obj->priv == NULL) return HDF_FAILURE;
struct MyLedDev *dev = (struct MyLedDev *)obj->priv;
int32_t ret = MyLedParseConfig(dev, obj);
if (ret != HDF_SUCCESS) return ret;
// 这里做 GPIO 请求/复用/默认状态设定,略
MyLedApplyPin(dev, 0);
HDF_LOGI("init ok");
return HDF_SUCCESS;
}
static void MyLedRelease(struct HdfDeviceObject *obj)
{
if (obj == NULL || obj->priv == NULL) return;
struct MyLedDev *dev = (struct MyLedDev *)obj->priv;
// 释放 GPIO/中断等资源,略
OsalMemFree(dev);
obj->priv = NULL;
HDF_LOGI("release ok");
}
/* ========== DriverEntry ========== */
struct HdfDriverEntry g_myLedEntry = {
.moduleVersion = 1,
.moduleName = "HDF_MY_LED", // **与 HCS moduleName 对应**
.Bind = MyLedBind,
.Init = MyLedInit,
.Release = MyLedRelease,
};
HDF_INIT(g_myLedEntry);
要点复盘
HdfDriverEntry.moduleName必须与 HCS 中的moduleName完全一致。- 在
Bind里挂obj->service = &你的 service才能被用户态/其他模块通过HdfIoServiceBind访问。 - 设备属性从
obj->property读,使用DeviceResourceIface的GetUint32/GetString/...。
二、设备描述文件(HCS):把“驱动能力”绑定到“具体那块硬件”
2.1 HCS 是什么?
HCS(Harmony Config System)用类 JSON 的 DSL描述系统组件与设备树。对 HDF 来说,HCS 决定了 DevMgr 在启动时要创建哪些设备实例、它们属于哪个 host,用哪个 driver(moduleName),并把自定义属性传给驱动。
2.2 常见文件与位置
device_info.hcs:总入口,包含 host 与设备节点。- 也可拆分模块独立
xxx.hcs并在device_info.hcs里#include。
工程常见路径(示意):
/vendor/<your_vendor>/<board>/hdf_config/device_info.hcs
/drivers/peripheral/my_led/config/my_led.hcs # 你自己的模块
2.3 样例 HCS(platform host 上挂一个 LED 设备)
// config/my_led.hcs
root {
my_led :: host {
hostName = "platform"; // 宿主:平台 Host(也可以是 i2c_host/spi_host 等)
priority = 50;
device my_led_dev { // 设备实例
deviceId = 0x1001; // 业务自定义,驱动里一般用不到
policy = 2; // 0/1/2:按需加载策略(不同分支略有差异)
preload = 0; // 1=开机预载
moduleName = "HDF_MY_LED"; // **与 g_myLedEntry.moduleName 一致**
serviceName = "my_led_service"; // 对外服务名(HdfIoServiceBind 使用)
// 设备自定义属性 —— 驱动里通过 DeviceResourceIface 读取
gpioNum = 12;
activeLow = 1;
}
}
}
如果你的平台遵循“集中入口”方式,在 device_info.hcs 里包含它:
#include "my_led.hcs"
2.4 驱动如何拿到这些属性?
见前面 MyLedParseConfig(),关键点:
const struct DeviceResourceNode *node = obj->property;
const struct DeviceResourceIface *iface = DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE);
iface->GetUint32(node, "gpioNum", &dev->gpioNum, 0);
支持的常见读取:GetString、GetUint8/16/32、GetBool、GetArray* 等(视分支)。
三、编译与加载:把 .c 变成可执行的“设备能力”
3.1 BUILD.gn(示意)
不同分支组件名不完全一致,以下给常见写法,把你的源编成 HDF 可加载的 共享库驱动(user-mode):
# drivers/peripheral/my_led/BUILD.gn
import("//build/ohos.gni")
ohos_shared_library("my_led_driver") {
sources = [
"src/my_led_driver.c",
]
include_dirs = [
"include",
"//drivers/hdf/frameworks/include", # HDF 头
"//drivers/hdf/frameworks/core/common/include",
]
deps = [
"//drivers/hdf/frameworks/core/manager:devmgr", # DevMgr 能力
"//drivers/hdf/frameworks/core/shared:hdf_core", # 基础 HDF 能力
]
cflags = [ "-DHDF_LOG_TAG=my_led" ]
part_name = "drivers_peripheral" # 归属部件(示意,按你项目部件填)
subsystem_name = "drivers" # 子系统
}
内核态(kernel-mode)驱动则会用到
kernel_module/ohos_kernel_module,并与内核 Kconfig/Makefile 相关;本文以用户态 HDF 驱动为例,更易上手、易调试。
3.2 把你的部件纳入系统构建
在对应部件的 bundle.json / 组件清单里加入 target,或在板级 productdefine/common/component 打开你的组件开关。不同仓/发行略有差异,核心就是:让 hb build 能找到并编进镜像。
3.3 HCS 的打包路径
构建系统会把 HCS 部署到 /etc/hdf/(或同等目录)下,DevMgr 启动时读取。你只需保证:
- 你的
my_led.hcs被device_info.hcsinclude; - 最终产物里能看到
.../etc/hdf/my_led.hcs(或合并后的大 HCS)。
3.4 构建与烧录
hb set # 选好 product/board
hb build -f # 全量构建
# 烧录或以仿真环境运行,具体依你的板卡方式
3.5 加载与日志验证
-
开机自动加载:如果
preload=1或策略允许,DevMgr 会在解析到你的节点后调用Bind/Init。 -
日志:串口或
hilog里 grep 你的 TAG:hilog -x | grep my_led你应该能看到:
I/my_led: bind ok I/my_led: parsed gpio=12 activeLow=1 I/my_led: init ok
3.6 运行时访问(用户态示例)
用户态进程可通过 HdfIoService 与你的驱动通信(前提是你在 Bind 挂了 obj->service 且 HCS 里设置了 serviceName)。
// user/test_my_led.c —— 简易验证程序
#include "hdf_io_service_if.h"
#include "hdf_sbuf.h"
#include <stdio.h>
int main(void)
{
struct HdfIoService *svc = HdfIoServiceBind("my_led_service");
if (svc == NULL) { printf("bind service fail\n"); return -1; }
struct HdfSBuf *data = HdfSbufObtainDefaultSize();
struct HdfSBuf *reply = HdfSbufObtainDefaultSize();
// 开灯
HdfSbufWriteUint8(data, 1);
int ret = svc->dispatcher->Dispatch(&svc->object, 1 /*IO_CMD_LED_TURN*/, data, reply);
printf("turn ret=%d\n", ret);
HdfSbufRecycle(data); HdfSbufRecycle(reply);
// 读状态
data = HdfSbufObtainDefaultSize(); reply = HdfSbufObtainDefaultSize();
ret = svc->dispatcher->Dispatch(&svc->object, 2 /*STATUS*/, data, reply);
uint8_t st = 0; HdfSbufReadUint8(reply, &st);
printf("status=%u\n", st);
HdfSbufRecycle(data); HdfSbufRecycle(reply);
HdfIoServiceRecycle(svc);
return 0;
}
如果你要提供 HDI(接口定义语言) 给 ArkTS/上层调用,建议再加一层 IDL + 代码生成,让接口类型安全、升级更平滑。
四、常见总线场景的差异点(I²C/SPI/UART)
- Platform 设备:最简单,常用于虚拟设备/聚合控制器;无额外总线绑定。
- I²C/SPI:在 HCS 里除了
moduleName外,还需要标注总线号/片选/地址等属性(如bus=1, addr=0x3C)。驱动里通过对应总线的 HDF 接口获取I2cDev/SpiDev句柄再读写。 - UART:注意波特率/数据位/校验位等串口参数也放 HCS;打开串口后用异步回调或轮询方式收发。
核心变化只是HCS 的属性字段与驱动里拿句柄的 API变了,Bind/Init/Release + 解析属性的范式不变。
五、排障与“玄学去除剂”
-
Bind/Init 没被调用
- 检查
moduleName与 HCS 是否完全一致(大小写也要一致)。 - 看
device_info.hcs是否包含了你的my_led.hcs。 hostName是否存在,并且该 host 在你的镜像里启用了。
- 检查
-
属性读取总是默认值
- 确认 HCS 文法、数字是否无引号(
gpioNum = 12而不是"12")。 DeviceResourceIface是否从HDF_CONFIG_SOURCE取,字段名拼写是否一致。
- 确认 HCS 文法、数字是否无引号(
-
用户态绑定服务失败 (
HdfIoServiceBind返回 NULL)- HCS 里的
serviceName是否设置; Bind是否把obj->service指向了你的HdfDeviceIoService;policy/preload导致设备未加载,尝试把preload=1验证。
- HCS 里的
-
多实例设备撞车
- 给每个设备实例不同的
serviceName与deviceId,HCS 里拆成多个device {}节点。
- 给每个设备实例不同的
-
升级后 ABI 报错/符号缺失
- 检查分支的 HDF 头/库变化,优先使用公共 API(
HdfDeviceObject、HdfSBuf、DeviceResourceIface、HdfIoService)。 - 不要依赖未公开的内部符号。
- 检查分支的 HDF 头/库变化,优先使用公共 API(
六、上线前的工程化清单
- 日志 TAG:每个关键路径(Bind/Init/属性解析/总线打开/错误码)都有
HDF_LOGI/E。 - 资源释放:
Release里关闭中断、释放总线、free 内存。 - 并发安全:服务
Dispatch中可能多线程进入,必要时加OsalMutex。 - 边界检查:SBuf 读写返回值逐个判断,避免越界。
- HCS 版本化:大改动时为不同板卡/产品准备独立 HCS,避免“通用一份把谁都带崩”。
- 自测工具:提供一个
user/test_xxx小工具做回归。
七、把样板改造成你的驱动:一页速用指南
- 复制模板:把本文
my_led_driver.c改名为你的设备名,保留 Bind/Init/Release、Dispatch框架。 - 定接口:把
IO_CMD_*与HdfDeviceIoService方法根据业务改掉。 - 写 HCS:在
config/your_device.hcs中设好hostName/moduleName/serviceName与你的属性。 - BUILD.gn 加入构建,确保被部件/产品引用。
- hb build + 烧录,hilog 验证。
- 用户态小工具或 ArkTS HDI 调通后再逐步接入业务应用。
结语
驱动这活儿,最怕“糊里糊涂能跑”,一换板/一升级就挂。把 HDF 生命周期、HCS 绑定、对外服务这三件事按上面的模板走一遍,你会发现:驱动是可重复生产的工程品,不是玄学。
…
(未完待续)
更多推荐




所有评论(0)