最近逛开源鸿蒙的社区,看到好几个新手帖都在问差不多的问题:“RK3568 板子有那么多设备树,到底该选哪个?”“为什么我在 x86 机器上跑 OpenHarmony,老报missing hcs services: hns, vmcompute, vfpext?”
这两个问题表面上看风马牛不相及,但深挖下去,它们全都指向同一个根:HDF 和 HCS。
HDF(HarmonyOS Driver Foundation)是 OpenHarmony 的驱动框架,HCS 是这套框架的配置描述语言。你要是搞不定这俩,别说自己适配外设了,连系统为什么起不来、某个设备为什么 probe 失败都无从查起。这篇就结合我在 RK3568 和 x86 平台上摸爬滚打的经验,把 HDF 和 HCS 的定位、语法、加载流程、选板思路和报错排查一次讲清。这篇内容同样收录在《万物智能之开源鸿蒙 OpenHarmony 系统实战开发系列教程》里,作为驱动与配置专题的第一篇。
1. HDF:先搞清楚它在系统里的位置
1.1 一句话讲清 HDF 的作用
HDF 是 OpenHarmony 的驱动框架,对上给应用层和系统服务层提供统一的驱动访问接口,对下管理各种硬件设备驱动。
你可以把它理解成一套“驱动界的物业公司”:硬件驱动是各个商户(GPIO、I2C、SPI、Display、Sensor……),HDF 是物业,负责登记商户信息、安排入驻、处理投诉、统一门禁。开发者不需要关心每家商户内部怎么装修,只需要通过物业前台(IO Service)就能找到想要的商户(设备)。
在 OpenHarmony 的架构里,HDF 处于内核态和用户态之间的驱动管理层。它不是一个简单的内核模块,而是一整套跨内核的驱动开发、加载、管理和访问的规范。
1.2 HDF 的六大核心能力
我把 HDF 的实际能力拆成六块,这是理解它的入口:
驱动模型抽象:HDF 规定每个驱动必须实现
Bind、Init、Release三个接口,分别对应驱动绑定、初始化和释放。无论你写的是字符设备、块设备还是总线设备驱动,都必须按这套模型来。统一配置解析:驱动需要哪些参数、设备挂在哪条总线上、中断号是多少,这些一律通过 HCS 配置来描述。HDF 框架启动时会统一解析配置,避免每个驱动自己造一套解析逻辑。
设备管理:HDF 维护一张设备清单,启动时扫描配置,逐个创建设备对象(
HdfDeviceObject),并绑定对应的驱动。服务管理:HDF 的驱动可以对外暴露成服务,用户态程序通过
HdfIoService按服务名访问,和传统 Linux 下 open + ioctl 的老套路相比,结构清晰得多。消息机制:HDF 提供了一套跨进程、跨内核态/用户态的消息发送接口,比如
HdfMessage,驱动和应用可以通过它收发自定义消息。电源管理与平台驱动支持:HDF 还包含平台驱动框架,统一管理 GPIO、PWM、I2C、SPI、UART 这类常见平台设备。
1.3 HDF 和 Linux 设备模型的区别
很多从 Linux 驱动开发转过来的人会有个疑问:Linux 已经有 platform bus、device tree、driver model 了,OpenHarmony 为什么还要再造一套 HDF?
关键在于多内核形态。OpenHarmony 标准系统不仅能跑在 Linux 内核上,还要支持 LiteOS-A 这种轻量内核。Linux 的驱动模型是无法直接迁移到 LiteOS 上的。HDF 的定位就是一个抽象层,把驱动的访问逻辑、配置逻辑、生命周期管理和底层内核解耦。你在 HDF 框架下的驱动代码,可以复用一大部分到不同内核上,这是纯 Linux 驱动做不到的。
打个比方:Linux 驱动是“入乡随俗”,到了哪个内核就得讲哪个内核的语言;HDF 是“自带翻译”,你只要按 HDF 的规矩写,它帮你翻译给下层内核听。
2. HCS:比 JSON 更“抠门”的配置语言
2.1 HCS 的语法长什么样
HCS 的全称是 HDF Configuration Source,是用来描述 HDF 设备树的配置语言,最终会编译成二进制 HCB(HDF Configuration Binary)文件,供框架在启动时读取。
HCS 的语法有点像 JSON 的“瘦身版”,没有括号也没用,用的是层层缩进加花括号。下面是一个典型的 HCS 片段:
root { sensor_config { sensor_light { match_attr = "hdf_light_sensor"; sensorName = "bsoh_light"; vendorName = "bsoh"; deviceName = "bsoh_light"; busType = 1; reg = 0x38; irq = 5; } } }这里root是根节点,sensor_config是子节点,sensor_light是具体设备节点,里面的match_attr、sensorName、vendorName、reg都是属性,属性值可以是字符串、整数、整数数组、bool 值等。
这里最容易忽略的是match_attr。它就是 HDF 驱动和 HCS 节点之间的“暗号”。驱动代码里通过HDF_MODULE_NAME指定模块名,通过HDF_INIT注册驱动,然后框架根据配置里的match_attr,去驱动列表里找有没有匹配的处理函数。
2.2 模板与继承:批量生成节点的利器
如果你管过一堆 Sensor 驱动,你会发现不同传感器节点有大量重复字段,比如busType、reg、vendorName。HCS 提供了模板机制来解决这个问题。
root { template SensorTemplate { busType = 1; reg = 0; irq = 0; match_attr = ""; } sensor_config { sensor_light : SensorTemplate { sensorName = "bsoh_light"; match_attr = "hdf_light_sensor"; reg = 0x38; } sensor_prox : SensorTemplate { sensorName = "bsoh_prox"; match_attr = "hdf_prox_sensor"; reg = 0x39; irq = 7; } } }定义了一个SensorTemplate模板,之后的sensor_light和sensor_prox都通过: SensorTemplate继承模板中的字段。子节点可以覆盖模板中的默认值,比如sensor_prox重新指定了irq = 7,模板里没写的字段就默认沿用。这有点像面向对象里的继承,能省掉大批重复配置。我常用的做法是:先定义一个“基础板级模板”,各个外设节点针对自己的差异只写改动项。
2.3 引用、删除、包含:HCS 的“编程”能力
HCS 不只是简单的键值对,它还提供了一些接近编程语言的特性:
- include:把一个 hcs 文件包含到另一个 hcs 文件中,适合拆分配置模块。
- delete:从模板或公共配置中删除某个节点。比如你不用声卡,在板级配置里直接把声卡节点 delete 掉,非常方便。
- reference(引用):允许一个节点引用另一个节点的路径值。比如一个 GPIO 控制器节点里定义了一个
gpio_chip,外设节点可以通过引用直接关联,避免手动写错端口号。
实体示例:
root { platform { gpio_controller { gpio_chip = [0, 32]; } led { // 引用 gpio_controller 里的 gpio_chip gpio = &platform/gpio_controller/gpio_chip; } } }这些特性让 HCS 具备了模块化和复用能力,也是为什么官方推荐大规模驱动配置时,不要写成一大坨,而要分层、分文件、分模板去管理。
2.4 从 hcs 到 hcb 的编译过程
HCS 文件和.dts文件一样,源码是不能直接被内核或者用户态框架使用的,需要先编译成二进制。HCS 的编译器是hc-gen,在 OpenHarmony 的编译工具链里自带。
编译动作一般长这样:
hc-gen -o /output/path/hdf_default.hcb -b /input/path/hdf_default.hcs-o指定输出,-b指定输入 hcs 文件。编译产物是.hcb文件,框架启动时通过DevmgrService加载默认配置路径下的 hcb。
在标准系统的编译脚本里,hcs 到 hcb 的转换通常被封装进了 build 流程,你会看到类似hdf_config、hdf_default.hcs、hdf_default.hcb这些文件出现在中间产物目录中。想手动验证配置合不合法,也可以自己单独跑 hc-gen。
3. HDF 和 HCS 怎么配合:从配置到驱动的完整链路
3.1 驱动加载路径的源码级复盘
要真正理解 HDF 和 HCS 的关系,得跟着启动流程走一遍。我简化成 5 步:
- 系统启动时,HDF 核心框架(
HdfCore或者叫Devmgr)先初始化。 - 框架找到预设的 hcb 配置节区,通常由
device_info节点开始解析。 - 解析出所有
deviceNode,每个节点里至少包含:moduleName:驱动模块名,对应编译出来的.ko或者静态编译的 module 名。serviceName:对外暴露的服务名。matchAttr:和驱动匹配的字符串。devicePolicy:服务策略。
- 框架根据
moduleName去找对应驱动,调用驱动的Bind接口绑定设备,再调用Init接口初始化。 - 初始化成功后,驱动就注册到 HDF 的服务管理器里,应用层可以通过
serviceName获取服务,开始正常通信。
你写一个 HDF 驱动时,入口文件长这样:
#include "hdf_device_desc.h" #include "hdf_log.h" static int32_t MyDriverBind(struct HdfDeviceObject *device) { HDF_LOGI("MyDriver bind success"); return HDF_SUCCESS; } static int32_t MyDriverInit(struct HdfDeviceObject *device) { HDF_LOGI("MyDriver init success"); return HDF_SUCCESS; } static void MyDriverRelease(struct HdfDeviceObject *device) { HDF_LOGI("MyDriver release"); } struct HdfDriverEntry g_myDriverEntry = { .moduleVersion = 1, .moduleName = "my_driver", .Bind = MyDriverBind, .Init = MyDriverInit, .Release = MyDriverRelease, }; HDF_INIT(g_myDriverEntry);上面对应的 HCS 里就得写:
root { device_info { match_attr = "hdf_manager"; device_myDriver { policy = 1; priority = 50; preload = 0; permission = 0660; moduleName = "my_driver"; serviceName = "my_driver_service"; deviceMatchAttr = "my_driver"; } } }moduleName = "my_driver"对应 C 文件里的.moduleName = "my_driver",deviceMatchAttr = "my_driver"是为了在多个设备间做匹配选择。这一串名字任何一个对不上,驱动就加载失败,而日志往往不给力。我踩过的坑里,有一半是这里的大小写或下划线写岔了。
3.2 matchAttr 匹配机制的细节
HDF 驱动加载时,核心匹配逻辑就是拿 HCS 节点的match_attr去驱动管理器里找对应的deviceMatchAttr。
源码层面可以看hdf_device.c和hdf_driver_loader.c里的实现,大致流程是:
- 解析配置里的每个
deviceNode。 - 根据
moduleName去已加载的驱动列表里找入口。 - 找到后,比较
deviceNode->deviceMatchAttr和驱动里设置的match_attr。 - 匹配成功后,调用
Bind创建并绑定设备对象。
这套机制的好处是:一个驱动模块可以服务于多个设备节点。比如一个通用 GPIO 按键驱动,可以同时处理三个按键节点,每个按键节点对应不同的 GPIO 号和上报键值,驱动内部通过deviceMatchAttr区分。这要比传统的 Linux platform_driver 靠of_match_table匹配设备树节点的方式更显式、更直观。
3.3 服务策略:device、BUILTIN、延迟加载
HCS 里每个设备节点都有policy字段,表示服务策略:
| policy 值 | 枚举含义 | 说明 |
|---|---|---|
| 0 | SERVICE_POLICY_DISABLED | 不对外提供服务,驱动加载但外部不可访问 |
| 1 | SERVICE_POLICY_BUILTIN | 服务随驱动加载即发布,用户态可直接通过 serviceName 获取 |
| 2 | SERVICE_POLICY_GUEST | 服务需要额外权限控制,一般用于权限敏感设备 |
| 3 | SERVICE_POLICY_CAPACITY | 按能力验证后才发布服务,用得少 |
| 4 | SERVICE_POLICY_BUILTIN_ONLY | 仅向内核态提供服务,用户态不能用 |
另外还有preload字段,0 代表随框架启动预加载,1 代表延迟加载,需要在代码里显式调HdfLoadDevice才会加载。这个对系统启动耗时很敏感的场景很重要:不必要的驱动不要预加载,加快开机时间。
4. 实战:RK3568 一堆设备树,到底怎么选
4.1 dts 和 hcs 不是一套东西
说回那个高频问题:OpenHarmony 的 RK3568 目录下有一堆.dts、.dtsi,到底选哪个?
首先要明确:dts/dtsi 是 Linux 内核的设备树,和 OpenHarmony 的 HCS 是两套完全独立的配置体系。
dts/dtsi:给 Linux 内核用,描述硬件资源(寄存器地址、中断、时钟、pinctrl),内核驱动按这个去初始化硬件。hcs/hcb:给 HDF 用,描述 HDF 驱动的设备节点、匹配属性、服务策略,以及部分 HDF 驱动的私有参数。
它们对应的加载者和使用者不同,但都围绕同一块开发板。你选对标定板卡型号后,dts 和 hcs 要么已经在编译流程里配对好了,要么需要手动指定。
4.2 选板级 dts 的 3 条路径
在OpenHarmony 源码里,RK3568 的设备树文件一般位于:
kernel/linux/linux-5.10/arch/arm64/boot/dts/rockchip/目录下会有大量.dts和.dtsi。真正的板级入口是你最终编译烧录时生效的那个文件。选择路径我总结为三条:
第一条:看板卡厂商名和型号。如果你是 Rockchip 官方的 RK3568 EVB1 开发板,入口大概率是rk3568-evb1-linux.dts这类文件。第三方板卡比如 ROC-RK3568-PC,会有对应的rk3568-rock-3a.dts或rk3568-nvr-demo-v10.dts。先按型号对号入座。
第二条:看构建脚本里指定的 dts。OpenHarmony 产品编译时,device/board 目录下的config或gni文件会引用具体的 dts 名。比如你在device/board/hihope/rk3568/里找一个BoardConfig.mk或者product相关脚本,里面通常会有KERNEL_DTS_NAME或类似的变量,这就是最终编译用的 dts。
第三条:看实际执行的编译日志。编译内核时会打印Building kernel with dts: rk3568-evb1-linux.dts之类的字样。拿不准的时候,编译一次,日志会告诉你最终用的是哪个。
我个人的建议是:别一上来就改 dts。先把板卡厂商给的基础 dts 跑到系统起来,外设驱动用 HCS 逐步加。很多新手上来就把 dts 改得面目全非,结果内核都起不来,还找不到是 cores 配置错了还是 pinctrl 冲突。
4.3 改 hcs 的常见误区
RK3568 平台下,HCS 配置文件一般放在:
vendor/厂商名/产品名/hdf_config/以某个第三方 RK3568 产品为例,常见是:
vendor/hihope/rk3568/hdf_config/里面会有hdf_default.hcs、hdf_test.hcs、device_info.hcs等文件。改完 hcs 之后,默认的编译流程会帮你生成新的 hcb,不需要手动跑 hc-gen。
但是这里有几个高频坑:
- 坑 1:只改了 dts,没改 hcs,或者反过来。同一个外设,内核侧要配置寄存器地址、中断,HDF 侧也要配置对应的服务节点。两套配置必须一致,否则驱动加载成功但访问不到正确资源。
- 坑 2:
match_attr冲突。全局不能重复。如果两个设备的match_attr一样,后加载的驱动可能匹配错设备。 - 坑 3:
moduleName对应的驱动没有编译进内核。HCS 里写了某个moduleName,但驱动没编进去,框架加载时直接报“驱动不存在”,日志里表现为设备节点创建失败。检查BUILD.gn是否把驱动加入hdf_driver列表。
5. 报错排查:missing hcs services 这类问题怎么处理
5.1 理解“missing hcs services”报错
你会在 x86 平台镜像或虚拟化场景下看到类似missing hcs services: hns, vmcompute, vfpext的报错。
这类报错的意思直译是:HDF 框架启动时,尝试从 hcb 配置里加载某些服务,但没找到对应的服务定义。
为什么会“missing”?最常见的两个原因:
- 配置里声明了某些服务节点,但对应的 hcs 源文件没有被打进最终 hcb 里。比如新加了一个模组,
device_info.hcs里加了device_hns { ... },但BUILD.gn里的sources没把hns.hcs加进去,导致 hc-gen 编译时漏掉了这个节点。 - 服务名和驱动注册的服务名对不上。比如配置里
serviceName = "vmcompute",但驱动里定义的实际服务名是vmcompute_service或别的,框架找不到指定名称的服务,就会报 missing。
注意,有些hns、vmcompute、vfpext这类词,其实是内核态/用户态某些子系统注册到 HDF 的扩展服务。比如在虚拟化场景下,Hypervisor 与 VM 之间的计算节点服务,OpenHarmony 的 HDF 会以服务形式暴露出来。x86 平台上如果镜像裁剪掉对应模块,就会出现 missing。
5.2 排查流程
遇到 missing hcs services,我一般按下面四步走,效率最高:
第一步:定位报错的上下文。看完整 dmesg 或 hilog 日志,确认 missing 发生在 HDF 核心初始化阶段,还是某个厂商驱动加载阶段。如果是 x86 通用镜像,优先怀疑镜像裁剪或虚拟化相关组件缺失;如果是自己的板级镜像,优先怀疑 hcs 路径和编译配置。
第二步:确认 hcb 里到底有没有对应的服务节点。从编译产物里找到 hdf_default.hcb,用 hc-gen 反解析回文本对比:
hc-gen -o output.hcs -i hdf_default.hcb或者直接查编译日志,看 hc-gen 处理了哪些 hcs。如果目标 hcs 没出现,就是编译配置漏了。
第三步:检查服务注册方。搜代码里serviceName = "hns"或HdfIoServiceBind("hns"),看是否有对应实现。如果只配置不实现,说明拉了一个空配置,要么补实现,要么删配置。
第四步:检查驱动是否被静态编译/动态加载。HDF 的服务可以由内核态驱动发布,也可以由用户态宿主进程(host)发布。如果你跑在 x86 的精简环境里,很多驱动被裁剪掉了,那么 hcs 里仍然残留对应节点,就会在框架启动时找不到服务。这就是 x86 上出现这个报错的高频原因。
5.3 快速自检清单
遇到missing hcs services或者其他 HCS 相关启动失败,先对照这张表自查:
| 检查项 | 方法 | 常见结果 |
|---|---|---|
| hcs 文件是否在编译源列表里 | 查BUILD.gn的 sources | 遗漏新加的 hcs |
| hcb 是否生成了 | 查编译产物路径 | hc-gen 失败/未执行 |
| serviceName 是否唯一 | 全局搜 serviceName | 重复服务名导致绑定歧义 |
| moduleName 是否和驱动一致 | 对比 C 里的 moduleName | 大小写下划线不一致 |
| 对应驱动模块是否编译 | 查内核/用户态镜像 | 镜像裁剪导致驱动缺失 |
| 节点 priority 是否异常 | 查 hcs 的 priority 字段 | 过高或过低影响加载顺序 |
6. x86 上的 OpenHarmony:驱动适配有哪些坑
6.1 x86 和 RK 的差异
很多人在电脑上装 OpenHarmony x86 版,跑起来后开始琢磨“为什么没有声音”“为什么网卡不工作”。
核心原因就是:x86 平台的硬件形态和 ARM 开发板差异巨大。RK3568 的板级资源都在设备树里按寄存器地址写死,而 x86 平台大量设备走的是 PCIe、ACPI 这类动态发现机制。HDF 的 HCS 配置在 x86 平台也依然存在,但很多节点是空的或者仅描述服务关系,底层的硬件发现交给内核的 PCI/ACPI 子系统。
所以你直接用 RK3568 的 hcs 思路套到 x86 上,很容易发现:声卡设备节点不存在,网卡设备节点不存在。不是 HDF 不工作,而是 x86 平台下 HDF 更多承担服务管理职能,板级硬件描述大量依赖内核自身能力。
6.2 常见适配问题
在 x86 上跑 OpenHarmony,和 HDF/HCS 直接相关的问题主要有三个:
- hcb 路径不对。x86 镜像中 hdf 配置默认路径和 ARM 开发板默认路径可能不同,检查
vendor下的产品配置,确认 hcb 被正确拷贝到系统镜像的/system/etc/hdfconfig/或对应目录。 - 用户态宿主进程缺失。HDF 在 x86 上有很多服务是由用户态进程注册的,比如部分系统服务、虚拟化助手。如果镜像裁剪掉了宿主进程,HDF 启动时就会因为找不到对应服务报
missing hcs services。 - 依赖的内核接口不一致。有些 HDF 驱动实现会直接操作寄存器或 GPIO,在 ARM 上很正常,搬上 x86 后没有对应地址映射,加载直接失败。这种情况只能走“平台化改造”,把硬件相关代码用 x86 的 PCI/ACPI 方式重写。
我的建议是:如果只是学习 HDF、HCS 框架,优先用 RK3568 这类标准开发板,资料多、参考实现多、出问题容易查。x86 平台适合做系统验证和框架测试,不适合做驱动开发入门。
7. 编译和调试 HDF 驱动时的几个实用习惯
7.1 日志先走 hilog
调试 HDF 驱动时,我的习惯是优先用HDF_LOGI、HDF_LOGE而不是printf或者printk。因为这些日志会走统一的 hilog 通道,配合hilog | grep HDF可以快速过滤出 HDF 框架的日志流。
你在内核日志里可能还会看到hdf前缀的devmgr相关打印,这些是框架核心的日志。我强烈建议把HDF_LOG_LEVEL临时调到 debug,看驱动加载时的详细匹配过程。具体方法不同版本不一样,一般是在内核 cmdline 或 hdf 配置文件里加 debug 开关。调一次日志级别,往往能少猜半小时问题。
7.2 改配置的成本最低,别急着改代码
遇到驱动行为不对,先回头审 HCS。很多问题不是代码逻辑错了,而是配置里的reg、irq、busType和硬件上实际接线对不上。
特别是 RK3568 这类 SoC,同一路 I2C 可以复用多个引脚,同一组 GPIO 可以配置多种功能。HCS 里写的是“软件视角的设备”,dts/pinctrl 里定义的是“硬件视角的复用”。两者联动出问题,现象千奇百怪,比如初始化成功但读写超时、中断不触发等。
我踩过的一次比较典型的坑是:HCS 里reg = 0x38的地址写到了 0x39,驱动 init 全成功,但一开读取,总线上根本没应答,显示器件就是不亮。排查到最后,用逻辑分析仪抓 I2C 波形,才发现地址偏移了一位。这个教训不值钱,但排查过程很贵。
7.3 善用 HDF 的测试框架
OpenHarmony 提供了一套 HDF 测试框架,硬件驱动的开发者可以在test目录下写驱动单测,跑hdf_test来验证。如果只是验证 HCS 配置的语法,hc-gen 也支持只解析不输出的模式,写配置脚本时可以利用这一点,在 CI 里提前拦截错误。
8. 一点个人体会
写 HDF 驱动和 HCS 配置,本质上是在两套思维之间反复横跳:一套是内核侧的资源描述(dts/pinctrl),一套是系统侧的服务描述(hcs/hdf)。这两套思维缺一不可,又容易互相干扰。
我在最初接触 OpenHarmony 驱动开发时,也犯过“选错设备树、改错 hcs、找不到服务”三连错。后来养成一个习惯:拿到一块新板子,先不动任何代码,把 dts 和 hcs 两份配置完整读一遍,搞清楚每个外设在两套体系里分别叫什么、挂在哪,再开始动手。这个习惯帮我省下大量 debug 时间。
如果你现在正被 RK3568 的device_info.hcs绕晕,或者在 x86 镜像里被 missing services 卡住,回到根上把 HDF 框架的服务模型和 HCS 的编译加载流程理一遍,90% 的困惑会自然消失。
后面我会继续更新 HDF 驱动从零编写、hcs 模板工程化拆分、以及 RK3568 常见外设适配的实战记录。有具体问题,也可以直接在评论区聊。