设备树是 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,用 OsalMemAllocOsalMutex。为什么?因为同一份驱动还想在 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 = 1024solutionY = 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.hcsinput_config.hcslcd_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 结构体初始化相反,手滑加个分号,报错信息不一定指到那一行。

每个文件一个 rootroot 里必须有 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。

preload0 开机加载,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 失败时不要先改应用。按这个顺序问:户口本里有没有这个 serviceNamepolicy 是不是 2?preload 是不是 0(或者有没有人先 GetService 过)?Image 里 grep -a 得到这个字符串吗?这四问能消掉大半「应用层问题」。

加载策略跟 policy 是两件事。preload 决定什么时候把驱动拉起来,policy 决定拉起来之后服务给谁看。两个都写成你以为的「默认」,框架的默认并不总是你以为的那个。模板里 policy = 0preload = 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 还是 vendorkhdf 子文件改动没清 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 是否重跑了内核 actionboard 目录不在内核 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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐