HDF 和 HCS 在开源鸿蒙系统里干什么?—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
设备树是 Linux 认硬件的办法。今天看开源鸿蒙自己那套驱动框架:触摸、音频、显示很大一部分走这边,和设备树各进一张镜像。
你要是从 Linux 那边过来,第一次碰开源鸿蒙的驱动,最容易犯的错不是写不出 probe,而是改完配置,板上完全没反应。设备树你改了,HCS 你也改了,全量编译 exit 0,镜像刷进去,触摸还是按 800×1280 走,分辨率明明已经写成 1024×600。
这不是编译器跟你作对。是开源鸿蒙把「硬件被谁认出来」拆成了两套世界:一套仍是 Linux 的设备树,一套叫 HDF。两套各有一份配置、各进一张镜像、各有一份缓存。你改的那份,很可能根本没被编进去,或者编进去了你刷的是另一张分区。
设备树还要继续用。HDF 和 DTS 是一对,缺一边,后面触摸、音频、显示会改错地方。
官方模型先放在这,对着看比空背术语快:


1. 先把场景说清楚:谁在找谁
传统 Linux 嵌入式里,驱动是 .c 加 Kconfig 加 DTS,用户态 open("/dev/input/event0")。应用认的是节点路径。节点在,应用就能干活。
开源鸿蒙标准系统底下确实还是 Linux 5.10,/dev 也还在。可桌面、Ability、ArkTS Kit 并不靠你随手 open 一个字符设备过日子。图形要找 composer,输入要找多模输入,音频要找 ADM。这些子系统认的是 HDI——Hardware Device Interface,一套稳定的接口,不认芯片型号,也不认你给节点起的名字。
HDI 下面那一层,就是 HDF(Hardware Driver Foundation)。它干三件很具体的事:
第一,按名单加载驱动。名单不在 DTS 里,在 HCS 里。框架拿着 device_info.hcs 逐个对 moduleName,对上了就调你的 Bind 和 Init。
第二,把驱动包装成服务。每个 DeviceNode 可以对外发布一个服务。内核里的别的驱动能 GetService,用户态的 host 进程也能 Bind。发不发布、谁看得见,由一个叫 policy 的整数决定。这个整数写错,是用户态「驱动明明起来了却拿不到」的第一号元凶。
第三,给用户态和内核态一条统一的消息路。不必每人自己 invent 一套 ioctl 编号。用户态 Dispatch,内核态 HdfDeviceSendEvent 往回推。
它的宣传语是「一次编写、多内核部署」。LiteOS 上能跑的 HDF 驱动,理想状态下搬到 Linux 上只换 OSAL。RK3568 这块板跑的是标准系统,内核是 Linux,所以你实际看到的是 Linux 原生驱动和 HDF 并存。并存不是文档里的小字,是每天会咬人的结构:同一颗 GT911,内核的 goodix 驱动和 HDF 的触摸模型都想占 I2C2 的 0x5d。谁先 probe 谁赢,另一个开始 NACK。同一条 I2C 怎么拆,下文单独讲。
官方概述在这,写得比我规范,但例子偏通用芯片:驱动子系统基础
2. 对着官方图,把五块积木认回家
图从上往下,我按你改代码时真正会进的目录说。
HDI 是门面。drivers/interface/ 里一堆 IDL,编译出的是服务端骨架和客户端桩。应用同学调 @ohos.multimodalInput、@ohos.multimedia.audio,最后会落到这里。你要是南向,很少直接改 IDL,但要知道:HDI 稳定,底下换芯片不能把接口改没。
HDF 框架在 drivers/hdf_core/framework/。加载、Host、服务管理、消息、配置解析,都在这。你写驱动时实现的 Bind / Init / Release,就是交给它调的。它不管你的芯片发哪几个寄存器,它管「这个模块叫什么名字、什么时候加载、服务发给谁」。
OSAL 是操作系统抽象。驱动里不要直接 kmalloc、不要直接 mutex_lock,用 OsalMemAlloc、OsalMutex。为什么?因为同一份驱动还想在 LiteOS 上编过。RK3568 上 OSAL 的实现落到 Linux 内核 API,对你来说就是多包一层。bring-up 阶段有人图省事直接调内核 API,短期能跑,后面一开 CFI 或者一换编译选项就炸。能走 OSAL 就走 OSAL。
平台驱动是腿。GPIO、I2C、SPI、UART、ADC、PWM、RTC、MMC,这些不叫「外设模型」,叫「把板子上的总线和脚交给别人用」。外设模型通过框架 API 向 I2C 管理器要一次 transfer,不必自己填 i2c_msg。HCS 里 HDF_PLATFORM_I2C_MANAGER 那种条目,就是在登记这些腿。
外设模型是身子。Input、Display、Audio、Camera、Sensor。模型规定「触摸要上报坐标」「显示要给 composer 提供层」「音频要走 ADM 的 render/capture」。芯片差异尽量关在模型底下的芯片驱动里。上层 Kit 认模型,不认 GT911 还是 FT5406。
仓库可以记这张地图,改的时候少逛错目录:
drivers/hdf_core/framework/ 框架、平台、模型、hc-gen
drivers/hdf_core/adapter/khdf/linux/ 接到 Linux 内核的那一层
drivers/peripheral/ Display / Input / Audio / Camera 的 HAL
drivers/interface/ HDI 接口定义
vendor/rk/rk3568_evb/hdf_config/khdf/ 编进内核的 HCS
vendor/rk/rk3568_evb/hdf_config/uhdf/ 打进 vendor.img 的 HCS
平台驱动管「脚和总线」,外设模型管「这类器件对系统长什么样」。PWM 风扇只是一个占空比,走平台 PWM 就够;电容触摸要进桌面滑动,必须走 Input 模型。别把超声波硬塞进 Sensor 模型——你没有标准 Kit 要接它,外设测试应用 open 一个 /dev/hcsr04 更直接。内核原生、HDF 平台、HDF 外设,三条路怎么选,下一篇写。
3. 一次触摸,从手指走到 ArkTS
空讲分层容易飘。走一遍触摸。
手指点到屏上,GT911 拉低 INT。这根脚在 HDF 的 input_config.hcs 里写成 intGpio = 23(GPIO0_C7)。khdf 里的触摸芯片驱动收到中断,按 I2C2 去读坐标寄存器。I2C 交易不是芯片驱动自己开的,它找 HDF_PLATFORM_I2C_MANAGER 要一次 transfer。管理器再落到 Linux 的 i2c_adapter。
坐标读回来,芯片驱动交给 Input 模型。模型按 solutionX = 1024、solutionY = 600 做缩放——注意,这里的分辨率是 HCS 里的,不是 DTS 里的。你只改 DTS 的 touchscreen-size-x,HDF 接管时根本不看那两个属性。这就是很多人「DTS 改了触摸还是歪」的原因。
模型上报给多模输入,再给窗口,最后 ArkTS 的点击回调到了。整条链上,应用没 open 过 /dev/input/event0。你用 cat /proc/bus/input/devices 仍可能看到一个 input 设备,那是模型在内核侧登记的,给兼容路径用,不是应用的主路。
音频更明显:HDF ADM 起来之后,板上没有 /dev/snd,也没有 /proc/asound。你按 ALSA 的习惯去找 card0,会以为声卡没驱动。其实四个节点在 /dev/hdf_audio_*。走错框架,诊断全废。
显示是第三条典型 HDF 路。composer 在用户态,panel 入口在内核,中间还夹着 DRM/KMS。这里只要记住:显示不完全是「写好 DTS 就出桌面」,HDF 的 panel 配置找不到 compatible 时,/dev/dri/card0 都可能不建。
4. khdf 和 uhdf:两套配置,进两张镜像
这是最值钱的分工,也是「我改了怎么没反应」的答案所在。
HCS 源码在板级 vendor/rk/rk3568_evb/hdf_config/ 下分成两棵树:
hdf_config/
├── khdf/
│ ├── hdf.hcs # 顶层,几乎全是 #include
│ ├── device_info/
│ ├── input/
│ ├── lcd/
│ ├── audio/
│ └── platform/
└── uhdf/
├── device_info.hcs
└── (用户态 host 用的配置)
khdf 跟内核一起跑。触摸芯片驱动、部分音频、部分显示 panel 入口,读的是这套。hc-gen 把它编成二进制,再转成一个 C 数组,链进内核 Image。所以改 khdf = 改 boot_linux.img(显示相关还要改 resource 分区里的 dtb)。
uhdf 给用户态 host 用。composer、部分平台适配跑在进程里,读 vendor 分区里的 hdf_default.hcb。改 uhdf = 改 vendor.img。
两套可以长得很像,字段名都叫 device_info,缓存却互不相认。你改了 khdf 的 input_config.hcs,图省事只刷了 vendor,板上触摸分辨率纹丝不动。日志也正常,因为内核里跑的还是上一份 hex。
管道画清楚:
HCS 源码(你改的是这些 .hcs)
│
┌────────────┴────────────┐
│ │
khdf/hdf.hcs uhdf/*.hcs
│ │
hc-gen hc-gen
│ │
hdf_hcs.hcb hdf_default.hcb
│ │
hdf_hcs_hex.c 拷进 vendor.img
(unsigned char 数组) /vendor/etc/hdfconfig/
│
链进 vmlinux / Image
│
boot_linux.img
khdf 的 make 规则精简后是这样。注意看依赖:
HCB_FLAGS := -b -i -a
HCS_OBJ := hdf_hcs_hex.o
$(CONFIG_GEN_HEX_SRC): $(LOCAL_HCS_ROOT)/%_hcs_hex.c: $(HCS_DIR)/%.hcs | $(HC_GEN)
$(Q)echo gen hdf built-in config
$(Q)$(HC_GEN) $(HCB_FLAGS) -o $(subst _hex.c,,$(@)) $<
obj-$(CONFIG_DRIVERS_HDF) += $(HCS_OBJ)
依赖项写的是顶层 hdf.hcs。#include 进去的 device_info.hcs、input_config.hcs、lcd_config.hcs 不在依赖里。make 判断「要不要重生成 hex」,只看 hdf.hcs 的时间戳。你改子文件、顶层一行没动,make 认为没变,继续拿上次的 hdf_hcs_hex.c 去 cc。链接成功,exit 0,镜像是旧配置。
这不是理论。触摸从 FT5406 切到 GT911 的时候,HCS 改了,重建烧录,板上仍按 FT5406 的复位脚去拉。hcb 停留在两天前。从那以后,板级 build_kernel.sh 编内核前必须无条件删 hcb,不能指望 make 变聪明。
# 编内核前:khdf 的 hcb / hex 缓存无条件清
rm -f vendor/rk/rk3568_evb/hdf_config/khdf/hdf.hcb
rm -f vendor/rk/rk3568_evb/hdf_config/khdf/hdf_hcs.hcb
find out/kernel -name 'hdf_hcs_hex.c' -o -name 'hdf_hcs_hex.o' -o -name 'hdf.hcb' \
| xargs --no-run-if-empty rm -f
uhdf 是另一对文件,别和上面删混:
rm -f out/rk3568_evb/gen/vendor/rk/rk3568_evb/hdf_config/uhdf/hdf_default.hcb
rm -f out/rk3568_evb/packages/phone/vendor/etc/hdfconfig/hdf_default.hcb
rm -f out/rk3568_evb/packages/phone/images/vendor.img
编完不要只看 exit 码。Image 是二进制,但 HCS 里的字符串还在里面,grep -a 能直接搜:
# 主机:新字符串能搜到,才算 khdf 真进了内核
grep -a "HDF_TOUCH_GT911" out/kernel/OBJ/linux-5.10/arch/arm64/boot/Image | head
grep -a "main_touch" out/kernel/OBJ/linux-5.10/arch/arm64/boot/Image | head
# 把 solutionX 从 800 改成 1024 之后,旧的 800 不该再作为触摸配置出现
搜不到就别刷。刷了也是上一份。ninja 对 board 目录经常不重跑,和这个坑叠在一起,能让人连续两天以为「HCS 语法错了」。最小重建那篇会拆 checkpoint。
5. HCS 本身:它不是 C,也不是 DTS
HCS 是 HDF 自己的配置语言,key-value 树。设计目的很明确:把配置从驱动代码里拆出去。驱动里不要写死总线号、不要写死复位脚,启动时用 match_attr 把对应节点找回来。换板换脚,理论上只改 HCS。
顶层 hdf.hcs 几乎全是 include,真正内容在子文件:
#include "device_info/device_info.hcs"
#include "platform/adc_config_linux.hcs"
#include "platform/pwm_config.hcs"
#include "platform/rk3568_uart_config.hcs"
#include "input/input_config.hcs"
#include "camera/camera_config.hcs"
#include "audio/audio_config.hcs"
#include "lcd/lcd_config.hcs"
root {
module = "rockchip,rk3568_chip";
}
几个语法点,写的时候少被 hc-gen 骂:
属性必须属于一个节点,必须以分号结束。节点用花括号,后面没有分号。这和 C 结构体初始化相反,手滑加个分号,报错信息不一定指到那一行。
每个文件一个 root。root 里必须有 module,给这份配置贴标签。
template 定义模板,foo :: templateName { ... } 继承后再覆盖字段。device_info.hcs 里大段都是这套写法,看起来像面向对象,其实就是少抄几遍 priority = 100。
match_attr 是全局唯一字符串。驱动启动时拿这个字符串去配置树里找节点。device_info.hcs 的 deviceMatchAttr 必须和配置节点的 match_attr 逐字符相同。差一个下划线,驱动 Init 时拿到空配置,表现为「脚是随机值」或者直接 Init 失败。
include 拼树。delete 只能删 include 进来的节点,不能删本文件自己写的。
触摸这块,分辨率、总线、复位脚都在 input_config.hcs,不在 DTS:
root {
input_config {
touchConfig {
touch0 {
boardConfig {
match_attr = "touch_device1";
inputAttr {
inputType = 0; /* 0 = touch */
solutionX = 1024;
solutionY = 600;
devName = "main_touch";
}
busConfig {
busType = 0; /* 0 = i2c */
busNum = 2; /* I2C2 */
}
pinConfig {
rstGpio = 88; /* GPIO2_D0,LVDS 配 GT911 */
intGpio = 23; /* GPIO0_C7 */
}
}
}
}
}
}
solutionX/Y 必须跟屏的逻辑分辨率一致。LVDS 和 RGB 都是 1024×600 横屏;MIPI 那块常见 800×1280 竖屏,这两个数字要一起改,只改屏不改触摸,划起来整块玻璃是歪的。HDF 触摸的几何在 HCS,不在 DTS。
背光若走 HDF PWM,lcd_config.hcs 指 PWM 号。RK3568 这块底板常见 PWM4:
root {
backlightConfig {
pwmBacklightConfig {
match_attr = "pwm_bl_dev";
pwmDevNum = 4;
pwmMaxPeriod = 25000;
backlightDevName = "hdf_pwm";
minBrightness = 0;
defBrightness = 127;
maxBrightness = 255;
}
}
}
PWM 号写错,表现为屏有时序、有 HDMI 那样的影像,就是背光不亮。你拿示波器去量 PWM4 没波形,量到别的 PWM 上才有,就是这份配置指错了。别先怀疑 panel 驱动。
HCS 和 DTS 的分工可以记一句人话:
DTS 告诉 Linux 内核「这块硅有哪些控制器、哪些脚、哪些时钟」。
HCS 告诉 HDF「哪个模型用哪条总线、哪根脚、什么分辨率、服务叫什么」。
两份都要,而且不要让它们抢同一颗从设备。
6. device_info.hcs:户口本,不是注释
框架加载谁、以什么策略发布服务,全看这份文件。它不负责时序,不负责坐标,只负责「人」。模板通常长这样:
root {
device_info {
match_attr = "hdf_manager";
template host {
hostName = "";
priority = 100;
template device {
template deviceNode {
policy = 0;
priority = 100;
preload = 0;
permission = 0664;
moduleName = "";
serviceName = "";
deviceMatchAttr = "";
}
}
}
}
}
字段一个一个钉死。写错的代价不是编译失败,是运行时静默缺席。
hostName 是容器名。一类驱动放一个 Host。平台放 platform_host,输入放 input_host。划分 Host 的原则是耦合:两个驱动互相 GetService,放一起更省事;完全无关就分开,一个挂了别把另一类拖死。用户态 Host 往往是一个进程,内核态 Host 是框架里的一组对象。
priority 取值 0 到 200,越小越先加载。先比 Host,再比 Host 里的设备。平台 Host 填 50、输入 Host 填 100,是因为触摸 Init 时要向 I2C 管理器发消息,管理器必须已经在。你要是图个整洁把 platform_host 调到 200,触摸会在 I2C 还没起来时失败,日志只说 transfer failed,不说「你把加载顺序写反了」。
policy 下一节单独讲。这里先记:用户态要拿服务,必须是 2。
preload:0 开机加载,1 快启第二阶段,2 第一次 GetService 再加载。显示、触摸、音频用 0。不要把触摸设成 2 还指望开机就能划解锁。
permission 是设备节点权限。开发期 0666 能少踩 SELinux 和 hap 权限的坑;交付再收紧。现在 permissive 也别太得意,hap 自己的 ACL 还能把你挡在节点外面,那是签名和权限另说。
moduleName 必须和驱动注册的名字逐字符相同。驱动里是 HDF_TOUCH_GT911,户口本写成 HDF_TOUCH_gt911,框架加载时找不到,设备缺席。dmesg 里经常只有一句很淡的 load driver failed。先对这份户口本,再怀疑芯片虚焊。
serviceName 是 GetService / Bind 用的名字。用户态写 "hdf_input_host0",户口本写成 "hdf_input_host",Bind 返回空指针。两边对着抄,不要凭记忆。
deviceMatchAttr 去配置树里找私有配置。必须对上 match_attr。
一段能用的平台 + 触摸登记如下。结构来自板级,名字按教程代号 rk3568_evb:
platform :: host {
hostName = "platform_host";
priority = 50;
device_gpio :: device {
device0 :: deviceNode {
policy = 2;
priority = 10;
permission = 0644;
moduleName = "HDF_PLATFORM_GPIO_MANAGER";
serviceName = "HDF_PLATFORM_GPIO_MANAGER";
}
device1 :: deviceNode {
policy = 0;
priority = 10;
moduleName = "linux_gpio_adapter";
deviceMatchAttr = "linux_gpio_adapter";
}
}
device_i2c :: device {
device0 :: deviceNode {
policy = 2;
priority = 50;
moduleName = "HDF_PLATFORM_I2C_MANAGER";
serviceName = "HDF_PLATFORM_I2C_MANAGER";
deviceMatchAttr = "hdf_platform_i2c_manager";
}
device1 :: deviceNode {
policy = 0;
moduleName = "linux_i2c_adapter";
deviceMatchAttr = "linux_i2c_adapter";
}
}
}
input_host :: host {
hostName = "input_host";
priority = 100;
device_touch :: device {
device0 :: deviceNode {
policy = 2;
preload = 0;
permission = 0666;
moduleName = "HDF_TOUCH_GT911";
serviceName = "hdf_input_host0";
deviceMatchAttr = "touch_device1";
}
}
}
注意 GPIO、I2C 都拆成了两个 DeviceNode:一个是管理器,policy = 2,对外发布;一个是 Linux 适配,policy = 0,不发布服务,只给同 Host 的管理器当腿。你要是给 adapter 也写成 policy 2,用户态能 Bind 到一个它不该直接用的对象,调用约定全乱。
7. policy = 0 / 1 / 2,用户态看得见看不见就看这个
官方枚举比三个值多,本教程日常只用前三个:
typedef enum {
SERVICE_POLICY_NONE = 0, /* 不发布服务 */
SERVICE_POLICY_PUBLIC = 1, /* 只对内核态发布 */
SERVICE_POLICY_CAPACITY = 2, /* 内核态 + 用户态都发布 */
SERVICE_POLICY_FRIENDLY = 3, /* 不发布,可被订阅 */
SERVICE_POLICY_PRIVATE = 4, /* 私有,不可订阅 */
} ServicePolicy;
0:谁 GetService 都没有。纯适配层,比如上面的 linux_i2c_adapter。它的存在是给管理器用的,不是给应用用的。
1:内核态驱动之间能拿到。看门狗这类「内核自己喂、用户态不必插手」可以走 1。你要是把触摸写成 1,内核日志里 Init 成功,hdc 里 HdfIoServiceBind 却永远 NULL。应用同学开始怀疑 hap 权限、怀疑 SELinux、怀疑签名,查三天,最后发现是一个整数。
2:内核态和用户态都发布。触摸、GPIO 管理器、UART、ADC,凡是 HDI 或者测试程序要找的,都是 2。
用户态拿服务的最小样子:
#include "hdf_io_service_if.h"
int OpenInputHost(void)
{
struct HdfIoService *svc = HdfIoServiceBind("hdf_input_host0");
if (svc == NULL) {
/* 十有八九:policy 不是 2,或 serviceName 写错,或 khdf 根本没编进内核 */
HDF_LOGE("bind hdf_input_host0 failed");
return -1;
}
/* 后面用 svc->dispatcher->Dispatch(...) 发消息 */
HdfIoServiceRecycle(svc);
return 0;
}
Bind 失败时不要先改应用。按这个顺序问:户口本里有没有这个 serviceName?policy 是不是 2?preload 是不是 0(或者有没有人先 GetService 过)?Image 里 grep -a 得到这个字符串吗?这四问能消掉大半「应用层问题」。
加载策略跟 policy 是两件事。preload 决定什么时候把驱动拉起来,policy 决定拉起来之后服务给谁看。两个都写成你以为的「默认」,框架的默认并不总是你以为的那个。模板里 policy = 0、preload = 0,继承时你忘了覆盖 policy,设备会开机加载,但不对外。看起来「驱动在 dmesg 里有,用户态没有」,就是这个组合。
8. 驱动侧你要写的三个函数
HDF 驱动不是 module_init + platform_driver。入口是一张表:
#include "hdf_device_desc.h"
#include "hdf_log.h"
static int32_t SampleBind(struct HdfDeviceObject *deviceObject)
{
static struct IDeviceIoService service = {
.Dispatch = SampleDispatch,
};
if (deviceObject == NULL) {
return HDF_ERR_INVALID_OBJECT;
}
deviceObject->service = &service;
return HDF_SUCCESS;
}
static int32_t SampleInit(struct HdfDeviceObject *deviceObject)
{
const struct DeviceResourceNode *node = NULL;
uint32_t busNum = 0;
if (deviceObject == NULL) {
return HDF_ERR_INVALID_OBJECT;
}
node = deviceObject->property;
if (node == NULL) {
HDF_LOGE("sample: no property, check deviceMatchAttr");
return HDF_ERR_INVALID_OBJECT;
}
if (HdfDeviceResourceGetUint32(node, "busNum", &busNum) != HDF_SUCCESS) {
HDF_LOGE("sample: busNum missing");
return HDF_FAILURE;
}
HDF_LOGI("sample init, busNum=%u", busNum);
return HDF_SUCCESS;
}
static void SampleRelease(struct HdfDeviceObject *deviceObject)
{
(void)deviceObject;
}
static struct HdfDriverEntry g_sampleEntry = {
.moduleVersion = 1,
.moduleName = "HDF_SAMPLE_MISC",
.Bind = SampleBind,
.Init = SampleInit,
.Release = SampleRelease,
};
HDF_INIT(g_sampleEntry);
HDF_INIT 把这张表挂到框架的入口链表。moduleName 必须等于户口本里的 moduleName。
调用顺序是 先 Bind 后 Init。Bind 的职责是把服务接口挂到 deviceObject->service 上,让框架能发布。Init 的职责是读配置、申请总线、注册中断。很多人把申请资源写进 Bind,结果 Init 还没跑、配置还是空的,申请出来的脚是 0。
deviceObject->property 就是 deviceMatchAttr 对上的那棵配置子树。它是空的,百分之九十是 attr 字符串没对上,不是 hc-gen 坏了。
Dispatch 是用户态消息进来的门。policy=2 时才会被用户态走到。内核态驱动互调走 GetService,不走 Dispatch。
这只是骨架。触摸、显示、音频的模型会替你把上报格式规定死,芯片驱动填的是读寄存器和复位时序。HDF 驱动的「main」是这三个函数,不是 module_init。
9. 同一条 I2C,不要两边 bind
这是 HDF 和 Linux 并存之后,最具体的一条纪律。
GT911 挂在 I2C2,地址 0x5d。内核树里有 goodix 驱动,DTS 里要是写了:
gt911@5d {
compatible = "goodix,gt911";
reg = <0x5d>;
status = "okay";
};
内核就会去 probe。与此同时,HDF 触摸模型按 HCS 的 busNum = 2 也去找 0x5d。两条路径没有协商,只有先到先得。
后果有三种,都见过。
第一种:内核先到,i2cdetect 上 0x5d 显示 UU(被驱动占用)。HDF 每一次 transfer 都 NACK,hilog 刷 i2c transfer failed。你以为芯片坏了,其实芯片活得好好的,只是被内核牵着走。
第二种:两边交替复位。HDF 拉一下 RST,内核又按自己的时序拉一下。坐标乱跳,偶发丢中断。
第三种:内核节点写了但驱动没编进来,HDF 独占成功,看起来「没问题」。过两周有人在 defconfig 里打开 CONFIG_TOUCHSCREEN_GOODIX=y,触摸突然全死。你去比 HCS,HCS 没变。变的是内核多了一个竞争者。
正确写法是:内核节点留着当备查,status 必须 disabled,总线留给 HDF。
&i2c2 {
status = "okay";
clock-frequency = <100000>; /* 这条总线上屏线和相机分支负载大,400kHz 容易 NACK */
/* 备查用,禁止 okay。HDF_TOUCH_GT911 独占 0x5d */
gt911_lvds: gt911@5d {
compatible = "goodix,gt911";
reg = <0x5d>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PC7 IRQ_TYPE_LEVEL_LOW>;
reset-gpios = <&gpio2 RK_PD0 GPIO_ACTIVE_HIGH>;
status = "disabled";
};
/* RGB 屏的 FT5406 同理,HDF 独占 0x38 */
ft5406_rgb: touchscreen@38 {
compatible = "edt,edt-ft5406";
reg = <0x38>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>;
reset-gpios = <&gpio3 RK_PB1 GPIO_ACTIVE_LOW>;
status = "disabled";
};
};
超声波、RFID 反过来。它们不进桌面、不进 Kit,外设测试应用 open /dev/hcsr04、/dev/nfc 就够。DTS okay,内核驱动 probe,HCS 里不要再给同一地址登记一个 HDF 设备。
判断用这三句,比背表格快:
要进开源鸿蒙标准子系统——桌面触摸、composer、ADM、相机 Kit——走 HDF。
只要一个 /dev 节点给外设测试应用读——走内核原生。
两者都想要——选一边,另一边 status disabled,或者根本不要在 HCS 里登记。
I2C 频率也是实打实的坑。公版参考把 i2c2 写成 400kHz,FT5406 在这条总线上全部 NACK,返回 -6。降到 100kHz 立刻 ACK。原因不是芯片「只支持 100k」,是这条 FPC 加分支负载大,边沿不够干净。HDF 和内核抢总线之前,先保证电气上能 ACK。i2cdetect -y -r 2 是听诊器,不是装饰。
10. 哪些走 HDF,哪些不要硬塞
结合这块 RK3568 的实际分工,给你一个不会过时的名单。
电容触摸 GT911、FT5406:HDF Input 模型。上层要滑动解锁、要点图标。
显示 panel 和 composer:HDF Display 模型加 DRM。第 16 章起整篇都是它。
音频:HDF ADM。不要找 /dev/snd。
相机:HDF / HDI 加 V4L2 / ISP,第 59 章。底层传感器仍可能是内核 V4L2 子设备,上面必须进相机框架,应用才能走 Kit。
超声波、RFID、红外避障、直流电机、步进电机、矩阵键盘(gpio-matrix-keypad)、LED(leds-gpio):内核原生。这些没有标准 Kit 要接,硬塞 Sensor 模型只会多写一堆 HCS,外设测试应用还是得 open 节点。
ADC 按键、PWM 风扇:可以走 HDF 平台(路径 B),也可以走内核 iio / sysfs。本教程里风扇用 sysfs 演示足够,ADC 按键用内核 adc-keys 更省事,因为桌面本来就认 input 事件。选一条,写进设备树或 HCS,不要两条同时 okay。
11. 改完怎么验收,命令按这个顺序跑
先确认当前启动盘。by-name 永远指 eMMC,你以为自己在刷 SD,可能写到了另一块介质。刷之前再防一次:
hdc shell "cat /proc/partitions"
hdc shell "mount | grep ' on / '"
hdc shell "cat /proc/version"
再看 HDF 用户态节点。policy=2 的服务,通常会在 /dev/hdf_* 出现:
hdc shell "ls /dev/hdf_*"
hdc shell "ls /proc/hdf/ 2>/dev/null"
触摸起来之后,多模输入侧能看到设备。名字不一定叫 gt911,可能叫 main_touch,以 HCS 的 devName 为准:
hdc shell "cat /proc/bus/input/devices"
hdc shell "ls /sys/class/input/"
内核 goodix / edt 不应该再绑 I2C。下面两条期望是「没有 driver 目录」或 No such file:
hdc shell "ls /sys/bus/i2c/devices/2-005d/driver"
hdc shell "ls /sys/bus/i2c/devices/2-0038/driver"
扫总线。UU 表示被占用,5d / 38 表示有应答但没绑驱动,-- 表示没人在:
hdc shell "i2cdetect -y -r 2"
HDF 接管成功时,0x5d 常常是 UU——占用者应当是 HDF 的 I2C 路径,而不是 goodix。结合上面 ls .../driver 一起看,不要看见 UU 就高兴,也不要看见 UU 就害怕。
用户态 host 的话在 hilog,内核 khdf 的话在 dmesg。两边都要看,只看一边会漏:
hdc shell "hilog -x"
hdc shell "hilog | grep -iE 'hdf|touch|gt911|ft5406'"
hdc shell "dmesg | grep -iE 'hdf|gt911|ft5406|touch|i2c'"
刷完 khdf 如果行为没变,回到主机做这三件事,再决定要不要再刷一遍:
# 1. Image 里有没有新字符串
grep -a "touch_device1" out/kernel/OBJ/linux-5.10/arch/arm64/boot/Image | head
# 2. hex 源是不是今天生成的
ls -l vendor/rk/rk3568_evb/hdf_config/khdf/hdf_hcs_hex.c \
out/kernel/OBJ/linux-5.10/drivers/hdf/khdf/hdf_hcs_hex.c 2>/dev/null
# 3. 你刷的分区是不是 boot_linux,路径是不是 /dev/block/
hdc shell "ls -l /dev/block/mmcblk1p5 /dev/block/mmcblk0p5"
dd 必须写 /dev/block/mmcblkXpY。写成 /dev/mmcblkXpY,这个路径常常不是块设备,dd 会在 /dev 下新建一个普通文件,返回成功,md5 也对,重启还是旧内核。卷首语里写过,这里再钉一次。
12. 现象 → 原因
下面这张表当听诊器,不要当阅读顺序。现象对上了,按「先查」那一列跑命令,原因栏是我走过的那几条,不是穷尽。
| 现象 | 先查 | 常见原因 |
|---|---|---|
| 改了 `input_config.hcs` 的 1024×600,划屏仍按 800×1280 走 | Image 里 `grep -a` 新数字;刷的是 boot_linux 还是 vendor | khdf 子文件改动没清 hcb,链的是旧 `hdf_hcs_hex.c` |
| 驱动 Init 成功,用户态 Bind 失败 | `policy`、`serviceName` | policy=1 或名字少写一个 0 |
| `i2cdetect` 0x5d 是 UU,HDF 报 I2C 失败 | `/sys/bus/i2c/devices/2-005d/driver` | 内核 goodix 抢了总线,DTS 没 disabled |
| 只刷了 vendor,触摸没变 | 改的是 khdf 还是 uhdf | 触摸在 khdf,要刷 boot_linux;显示几何还要刷 resource |
| 开机没有 hdf 设备节点 | `preload`、`moduleName`、`CONFIG_DRIVERS_HDF_*=y` | 模块没编进内核,或 preload=2 还没人 GetService |
| 改了 `device_info.hcs` 全量编译却行为旧 | ninja 是否重跑了内核 action | board 目录不在内核 DEPS 里,要删 checkpoint |
| 两个 Host 互相找不到服务 | 两边的 priority | 被依赖的 Host 后加载 |
| `property` 为空,Init 读不到 busNum | `deviceMatchAttr` 和 `match_attr` | 字符串差一个字符 |
| FT5406 全部 NACK,返回 -6 | `i2cdetect -y -r 2`;DTS `clock-frequency` | 400kHz 在长 FPC 上跑不稳,降到 100kHz |
| 音频「没声卡」 | `ls /dev/hdf_audio_*`,不要找 `/dev/snd` | ADM 不创建 ALSA 节点,属正常 |
| 背光不亮,时序是对的 | PWM 号、`lcd_config.hcs` 的 `pwmDevNum` | HDF 背光指错 PWM,示波器量 PWM4 |
「删 checkpoint」会和 hcb 缓存叠在一起。你清了 hcb,ninja 却因为 board 目录不在 DEPS 里根本没重跑内核,hcb 清了也白清。
核对一遍:HDF 是开源鸿蒙自己的驱动框架,RK3568 上它和 Linux 原生驱动并存,并存就要谈所有权。khdf 编进内核,uhdf 进 vendor。hc-gen 的 make 规则只盯顶层 hdf.hcs,改 include 的子文件必须删 hcb。用户态要看见服务,policy 必须是 2。触摸、音频、显示走 HDF;超声波、RFID 走内核原生。GT911、FT5406 的内核节点保持 disabled。
想确认 make 会不会撒谎:在 input_config.hcs 加一个 debugTag = "hcs-cache-probe";,不清 hcb 编一次内核,grep -a hcs-cache-probe 那份 Image。再清 hcb 编一次。两次结果不一样,缓存这件事就算看见了。
系列第 13 篇 · 芯片:瑞芯微 RK3568 · OpenHarmony 4.1(API 11) · Linux 5.10
更多推荐

所有评论(0)