4. HCS配置详解:HCS语法基础、节点与属性、设备树与HCS的映射、HCS加载与解析流程

HCS(HDF Configuration Source)是鸿蒙硬件驱动框架的配置语言。说白了,它就是一套专门描述硬件设备信息的“方言”。我刚开始接触时也觉得它跟设备树(DeviceTree)有点像,但用久了就会发现,HCS是为HDF量身定做的,更轻量,也更灵活。

这一章,我会带你从语法基础开始,一步步搞懂节点、属性、映射关系,最后再看HCS是怎么被加载和解析的。嗯,这些都是实战中绕不开的硬功夫。

4.1 HCS语法基础

HCS的语法非常简洁。它不像C语言那样有复杂的类型系统,也不像XML那样有冗长的标签。它的核心就两个东西:节点和属性。

先看一个最简单的例子:

root {
    uart0 {
        compatible = "hisi,uart";
        reg = 0x12000000;
        interrupts = 38;
        baudrate = 115200;
    };
};

这个例子描述了一个UART控制器。我来拆解一下:

  • root:根节点,所有设备节点都挂在这个下面。
  • uart0:子节点,代表一个具体的UART设备。
  • compatible:字符串属性,用来匹配驱动。这个很重要,驱动就是靠它找到对应设备的。
  • reg:整型属性,表示寄存器基地址。
  • interrupts:中断号。
  • baudrate:波特率,这里直接写死了。

语法规则其实就几条:

  • 节点用花括号 {} 包裹。
  • 属性用 = 赋值,分号 ; 结尾。
  • 字符串用双引号,整型直接写,支持十六进制(0x)。
  • 支持注释:// 单行,/* */ 多行。
我的习惯:写HCS文件时,我会把每个节点的compatible属性放在第一行。这样一眼就能看出这个节点是干什么的,排查问题也快。

4.2 节点与属性详解

节点和属性是HCS的基石。节点可以嵌套,属性可以有很多种类型。我们一个个来看。

4.2.1 节点类型

节点分两种:普通节点和引用节点。

  • 普通节点:就是上面那种,直接定义设备信息。
  • 引用节点:用 & 符号引用另一个节点,可以覆盖或扩展它的属性。

引用节点的用法:

root {
    uart0 {
        compatible = "hisi,uart";
        baudrate = 115200;
    };

    &uart0 {
        baudrate = 921600;  // 覆盖波特率
        dma-enable = 1;     // 新增DMA使能
    };
};

这个特性在板级适配时特别有用。比如公版芯片的HCS定义了一套默认配置,你的板子只需要引用并修改几个参数就行,不用把整个节点重写一遍。

4.2.2 属性类型

HCS支持多种属性类型,我整理了一个表格:

类型 写法示例 说明
字符串 compatible = "hisi,uart"; 用双引号包裹
整型 reg = 0x12000000; 十进制或十六进制
布尔型 dma-enable = 1; 1表示true,0表示false
数组 pins = [0, 1, 2, 3]; 用方括号包裹,逗号分隔
字符串数组 clocks = ["clk_uart0", "clk_uart1"]; 字符串的集合
键值对 gpio = { pin = 10; mode = 1; }; 用花括号包裹,内部是属性列表
注意:我曾经在一个项目里把数组的逗号写成了分号,结果HCS解析器直接报错。这种低级错误排查起来很费时间,所以写完后最好用工具校验一下格式。

4.3 设备树与HCS的映射

很多从Linux转过来的开发者会问:HCS和设备树(DeviceTree)到底什么关系?

我的理解是:HCS是鸿蒙版的设备树,但更精简。设备树用dts文件描述硬件,HCS用hcs文件描述硬件。它们的目标是一样的——把硬件信息从驱动代码里剥离出来。

但两者有几个关键区别:

对比项 设备树(DTS) HCS
语法风格 类似C语言,有标签、引用、宏 类似JSON,更简洁
编译工具 dtc(设备树编译器) hc-gen(HCS编译器)
运行时格式 dtb(二进制设备树) hcb(二进制HCS)
驱动匹配方式 compatible + of_match_table compatible + HDF驱动匹配机制
覆盖机制 dtbo(设备树覆盖) 引用节点 + 板级覆盖

在实际项目中,HCS和设备树可以共存。比如系统启动时,bootloader可能用设备树来初始化基本硬件,而HDF框架则用HCS来管理驱动。我做过的一个项目就是这样:内核启动用设备树,驱动加载用HCS,两者各司其职。

4.4 HCS加载与解析流程

HCS文件写好了,怎么被系统用起来?这个过程分为三步:编译、加载、解析。

下面这张图展示了完整的流程:

HCS加载与解析流程 HCS源文件 *.hcs hc-gen编译 生成hcb二进制 hcb二进制 烧录到文件系统 HDF框架加载 HdfDeviceObject解析 驱动匹配 compatible匹配 驱动初始化 Bind/Init/Release 说明: hc-gen编译后生成hcb文件,烧录到设备文件系统。HDF框架启动时读取hcb,解析成HdfDeviceObject树, 然后根据compatible属性匹配驱动,最后调用驱动的Bind/Init接口完成初始化。

流程看起来不复杂,但有几个细节值得注意:

4.4.1 编译阶段

hc-gen是HCS的编译器。它会做两件事:

  • 语法检查:检查节点、属性、引用的合法性。
  • 生成hcb:把文本格式的hcs编译成二进制格式的hcb。

编译命令很简单:

hc-gen -o output.hcb input.hcs

如果hcs文件有语法错误,hc-gen会报错并指出具体位置。我建议你在提交代码前,先用hc-gen跑一遍,避免低级错误。

4.4.2 加载阶段

HDF框架启动时,会从文件系统读取hcb文件。加载路径通常是 /vendor/etc/hdf/hdf.hcb 或 /system/etc/hdf/hdf.hcb,具体看产品配置。

加载过程的核心函数是 HdfDeviceObjectLoad()。它会:

  1. 打开hcb文件。
  2. 读取文件头,校验魔数和版本。
  3. 解析节点树,构建内存中的 HdfDeviceObject 结构。
  4. 建立节点间的父子关系和引用关系。
关键点:HDF框架不会一次性加载所有hcb文件。它采用按需加载策略——只有被引用的设备节点才会被解析。这样可以减少内存占用,加快启动速度。

4.4.3 解析阶段

解析阶段就是把hcb二进制数据还原成HDF框架能识别的数据结构。每个节点对应一个 HdfDeviceObject,每个属性对应一个 HdfDeviceAttr。

驱动开发者通过 HdfDeviceObject 的接口来读取属性:

// 读取字符串属性
const char *compatible = HdfDeviceGetAttrValue(device, "compatible");

// 读取整型属性
uint32_t reg = HdfDeviceGetAttrValueUint32(device, "reg");

// 读取数组属性
struct HdfDeviceAttr *pins = HdfDeviceGetAttr(device, "pins");

这里有个坑:属性名是大小写敏感的。我遇到过有人把 baudrate 写成 Baudrate,结果驱动读不到值,排查了半天才发现是大小写问题。

4.5 实战经验总结

最后,分享几个我在项目中积累的经验:

  • 善用引用节点:板级差异用引用节点覆盖,不要修改公版hcs文件。这样升级芯片驱动时,你的板级配置不会受影响。
  • 属性命名要规范:建议全小写,单词间用连字符。比如 dma-enable、clock-frequency。不要混用下划线和驼峰。
  • 加注释:HCS支持注释,别吝啬。特别是那些“魔法数字”,比如中断号、寄存器地址,加个注释说明来源,三个月后你自己也能看懂。
  • 用工具校验:hc-gen的语法检查很严格,写完后跑一遍,能省去很多调试时间。
我曾经踩过的坑:有一次我修改了hcs文件,但忘了重新编译生成hcb,结果烧录后驱动加载失败。排查了半天才发现hcb还是旧的。所以,修改hcs后一定要重新编译hcb,这是新手最容易犯的错误。