4、HCS配置语法:从结构到实战

HCS,全称是 HDF Configuration Source。说白了,它就是一套给驱动写配置的专用语言。我刚开始接触 OpenHarmony 驱动时,第一反应是:为什么不用 JSON 或者 XML?后来踩了几个坑才明白,HCS 的设计目标就是「轻量、可覆盖、适合嵌入式」。今天我们就把它彻底讲透。

4.1 HCS 文件结构:一棵有根的树

HCS 文件本质上是一棵属性树。每个文件只有一个根节点,下面挂子节点,子节点再挂属性或更小的子节点。你想想看,这跟设备树的思路很像,但语法更简洁。

核心结构:

  • 根节点:每个 HCS 文件有且只有一个,用 root {} 包裹。
  • 子节点:用 node_name {} 定义,可以无限嵌套。
  • 属性:键值对,写在节点内部。

来看一个实际例子。这是我之前在调试 I2C 控制器驱动时写的配置片段:

root {
    i2c_config {
        i2c0 {
            bus_number = 0;
            speed = 400000;  // 单位 Hz
            gpio_scl = 12;
            gpio_sda = 13;
        };
        i2c1 {
            bus_number = 1;
            speed = 100000;
            gpio_scl = 14;
            gpio_sda = 15;
        };
    };
};

嗯,这里要注意:每个属性后面必须跟分号,节点结束也要分号。我见过不少新手漏掉分号,编译直接报错,排查半天。

4.2 节点定义:路径与引用

每个节点都有一个路径。比如上面例子中,i2c0 的完整路径就是 root.i2c_config.i2c0。这个路径在后面的覆盖机制里非常关键。

节点名可以包含字母、数字和下划线,但不能以数字开头。我个人习惯用小写+下划线风格,跟内核代码风格保持一致。

小技巧:如果你想让某个节点可以被外部引用,可以在节点名前加 ref 关键字。比如 ref i2c0 {},这样其他 HCS 文件就能通过路径来覆盖它。

4.3 属性类型:不只是整数和字符串

HCS 支持的属性类型比你想的要多。我整理了一张表,方便你对照:

类型 关键字 示例 说明
整数 直接写数字 count = 10; 支持十进制、十六进制(0xFF)
布尔 true / false enable = true; 注意是小写
字符串 双引号 compatible = "hisi,i2c"; 不能换行
数组 方括号 irqs = [32, 33, 34]; 元素用逗号分隔
字节数组 单引号 + 十六进制 mac = [0x00, 0x11, 0x22]; 常用于 MAC 地址、UUID
节点引用 & 符号 parent = &i2c0; 指向另一个节点的路径

我曾经在配置 DMA 通道时,把数组写成了 channels = 1,2,3; 忘了加方括号,结果驱动读到的永远只有第一个值。这种坑,踩过一次就记住了。

4.4 引用与覆盖机制:HCS 的精髓

这是 HCS 最强大的地方,也是初学者最容易懵的地方。我尽量用大白话讲清楚。

引用(Reference):允许一个节点「继承」另一个节点的内容。语法是 ref 路径 {}。

覆盖(Override):在继承的基础上,修改部分属性。语法是 路径 : 属性名 = 新值;。

来看一个实际场景。假设你有一个默认配置文件 i2c_default.hcs:

root {
    i2c_default {
        speed = 100000;
        mode = "standard";
        timeout_ms = 1000;
    };
};

然后你的板级配置文件 board_i2c.hcs 想引用并覆盖:

root {
    i2c0 : ref i2c_default {
        speed = 400000;  // 只覆盖速度,其他属性继承
    };
};

最终 i2c0 节点的实际配置是:speed=400000、mode="standard"、timeout_ms=1000。看到了吗?你只改了一个值,其他自动继承。

注意:覆盖时,路径必须完全匹配。如果你写 i2c0 : ref i2c_default,但 i2c_default 的路径是 root.i2c_default,那就要写全路径。我曾经因为路径写错,导致覆盖没生效,驱动用了默认值,I2C 速率跑飞,调试了两天。

4.5 编译集成:HCS 如何变成 C 代码

HCS 文件不是直接给驱动用的。它需要经过一个编译工具(hc-gen)转换成 C 语言的头文件和源文件。流程是这样的:

  1. 编写:在 vendor/xxx/ 或 device/xxx/ 目录下创建 .hcs 文件。
  2. 编译:构建系统自动调用 hc-gen,生成 .hcb(二进制)和 .h(头文件)。
  3. 链接:驱动代码通过 #include 包含生成的头文件,调用 HDF 提供的 API 读取配置。

在 BUILD.gn 中,你需要这样声明 HCS 文件:

hcs_resources("my_driver_hcs") {
    sources = [
        "my_driver_config.hcs",
    ]
}

然后驱动代码里这样读:

#include "hdf_config.h"
// 通过 HDF 提供的接口读取配置树
struct DeviceResourceNode *node = HdfGetConfigNode("root.i2c_config.i2c0");
uint32_t speed;
HdfGetUint32(node, "speed", &speed, 100000);  // 默认值 100000

我的习惯:在 HCS 文件里给每个属性都写注释,标明单位、取值范围、默认值。这样半年后回来看代码,不用重新猜。

4.6 知识结构总览

下面这张图把 HCS 的核心知识点串起来了。你可以把它当作本章的「思维导图」:

HCS 配置语法知识结构图 HCS 配置语法核心知识体系 HCS 配置语法 文件结构 root {} → 子节点 → 属性 节点定义 路径、ref、命名规范 属性类型 整数/布尔/字符串/数组/引用 引用与覆盖 ref 继承 + 属性覆盖 编译集成 hc-gen → .hcb + .h → 驱动API 每个分支都是独立知识点,但通过覆盖机制串联

4.7 避坑指南

最后,分享几个我实际踩过的坑:

  • 路径大小写敏感:HCS 的路径是大小写敏感的。我见过有人把 I2C0 写成 i2c0,结果覆盖不生效。
  • 数组索引从 0 开始:覆盖数组元素时,用 array[0] = 10; 这种语法。
  • 不要用 Tab 缩进:HCS 解析器对 Tab 支持不好,建议统一用空格。我习惯 4 空格缩进。
  • 注释只能用 //:HCS 不支持 /* */ 块注释,只能用双斜杠。

我曾经犯过的错:在一个产品项目中,我把覆盖写成了 i2c0 : ref i2c_default,但 i2c_default 节点里有个 pinctrl 子节点。我以为覆盖会自动合并子节点,结果 HCS 是整节点替换——子节点全丢了。后来我改成显式引用子节点才解决。

好了,HCS 配置语法就讲到这里。记住:结构是树,属性是叶,覆盖是魂。把这三点吃透,HCS 对你来说就是小菜一碟。