4. HCS 配置语法详解:从结构到加载,手把手拆解
HCS,全称是 HDF Configuration Source。说白了,它就是鸿蒙驱动框架的“配置文件语言”。你写驱动,总得告诉系统你的硬件接在哪个引脚、用哪个中断、寄存器地址是多少吧?这些信息,HCS 就是用来干这个的。
我刚开始接触 HCS 时,觉得它不就是个配置文件嘛,能有多复杂?结果第一次写 sensor 驱动,就因为一个节点引用写错了,编译过了但加载死活不成功。查了两天才发现是路径写错了。嗯,从那以后,我对 HCS 的语法就再也不敢马虎了。
4.1 HCS 文件结构:树形组织的“硬件家谱”
HCS 文件的结构,其实就是一个树。根节点下面挂子节点,子节点下面再挂子节点。每个节点都可以有自己的属性。你想想看,这跟设备树(DeviceTree)是不是有点像?但 HCS 是鸿蒙自己的一套,语法更简洁,也更适合驱动框架的加载流程。
一个典型的 HCS 文件长这样:
root {
sensor_config {
accelerometer {
i2c_config {
bus = 1;
addr = 0x18;
reg_width = 1;
}
interrupt_config {
irq_gpio = 10;
irq_flags = 3;
}
}
gyroscope {
i2c_config {
bus = 2;
addr = 0x68;
reg_width = 2;
}
}
}
}
看到没?root 是顶层节点,下面挂 sensor_config,再下面挂具体的传感器。每个传感器节点里,又分 i2c_config 和 interrupt_config。这种结构,说白了就是把你硬件的“家谱”给画出来了。
#include 包含进来。这样维护起来清晰,不会一个文件几千行,找参数找到眼瞎。
4.2 节点与属性定义:核心语法就这么几条
HCS 的节点和属性定义,规则其实很少。我总结了一下,核心就三条:
- 节点: 用花括号
{}包裹,节点名就是标识符。 - 属性: 键值对,用
=赋值,分号;结尾。 - 数组: 用方括号
[]包裹,元素用逗号分隔。
举个例子:
display_config {
resolution = [1920, 1080]; // 数组属性
refresh_rate = 60; // 整型属性
panel_name = "lcd_ili9341"; // 字符串属性
support_te = true; // 布尔属性
power_seq {
step1 {
type = 0;
delay_ms = 10;
}
step2 {
type = 1;
delay_ms = 20;
}
}
}
这里要注意,字符串必须用双引号,布尔值用 true/false。整型支持十进制和十六进制(0x 开头)。数组嘛,我常用在分辨率、坐标、GPIO 列表这些场景。
root、include、delete 这些,用了编译直接报错。还有,字符串别漏了引号,漏了它会把你的字符串当成节点名去解析,然后告诉你“找不到这个节点”。
4.3 引用与继承:别重复造轮子
写驱动配置时,经常遇到多个设备共用一套配置参数。比如两个传感器,I2C 时序参数完全一样,只是地址不同。这时候,复制粘贴就太 low 了。HCS 提供了引用和继承机制。
4.3.1 引用:用 & 和 ref 指向已有节点
引用语法很简单:
common_i2c_timing : i2c_timing {
speed = 400000;
hold_time = 4;
setup_time = 2;
}
accelerometer {
i2c_config {
timing = &common_i2c_timing; // 引用上面定义的节点
addr = 0x18;
}
}
这里 &common_i2c_timing 就是引用。注意,被引用的节点前面要加个冒号 :,表示它是一个“可被引用的标签”。我习惯把这类公共配置单独放在一个 common.hcs 文件里,方便复用。
4.3.2 继承:用 : 实现“父子”关系
继承比引用更强大。它允许你从一个基础节点派生新节点,然后只修改差异部分。
base_sensor {
i2c_config {
bus = 1;
reg_width = 1;
speed = 100000;
}
power_config {
vdd_voltage = 1800000;
vddio_voltage = 1800000;
}
}
accelerometer : base_sensor {
i2c_config {
addr = 0x18; // 只补充地址,其他继承自 base_sensor
}
}
gyroscope : base_sensor {
i2c_config {
addr = 0x68;
speed = 400000; // 覆盖父节点的 speed
}
power_config {
vdd_voltage = 2800000; // 覆盖父节点的电压
}
}
看到没?accelerometer 和 gyroscope 都继承自 base_sensor。子节点里只写自己特有的属性,相同的属性自动从父节点继承。如果子节点定义了同名属性,就覆盖父节点的值。
4.4 编译与加载机制:HCS 是怎么变成驱动能用的数据的?
HCS 文件写好了,怎么变成驱动能读的数据?这个过程分两步:编译和加载。
4.4.1 编译:从 .hcs 到 .hcbs
HCS 文件不能直接给驱动用。它需要先被编译成二进制格式,后缀是 .hcbs。编译工具叫 hc-gen,在鸿蒙源码的 //drivers/hdf/tools/hc-gen 目录下。
编译命令很简单:
hc-gen -o output.hcbs input.hcs
这个工具会做三件事:
- 语法检查:看看你的 HCS 文件有没有语法错误。
- 解析引用和继承:把
&引用和:继承展开,生成一棵完整的配置树。 - 序列化:把树形结构转成二进制格式,方便驱动加载时解析。
我习惯在编译脚本里加一行 hc-gen 的调用,确保每次修改 HCS 后自动重新编译。不然你改了配置忘了编译,驱动加载的还是旧数据,排查起来很痛苦。
4.4.2 加载:驱动怎么拿到配置数据?
编译好的 .hcbs 文件,会被打包进系统镜像。驱动加载时,HDF 框架会做以下事情:
- 定位文件: 根据驱动模块名,在预定义的路径下找到对应的
.hcbs文件。 - 解析二进制: 把
.hcbs反序列化回内存中的树形结构。 - 绑定驱动: 把配置树的根节点和驱动入口绑定起来。
- 提供 API: 驱动可以通过
HdfGetNode()、HdfGetAttr()等 API 读取配置。
举个例子,驱动里读配置的代码大概是这样:
// 获取配置树的根节点
const struct DeviceResourceNode *root = HdfGetNode(dev, "sensor_config.accelerometer.i2c_config");
if (root == NULL) {
HDF_LOGE("Failed to get config node");
return HDF_FAILURE;
}
// 读取属性
uint32_t bus;
int32_t ret = HdfGetUint32(root, "bus", &bus, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("Failed to get bus");
return HDF_FAILURE;
}
uint32_t addr;
ret = HdfGetUint32(root, "addr", &addr, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("Failed to get addr");
return HDF_FAILURE;
}
HDF_LOGI("I2C config: bus=%d, addr=0x%x", bus, addr);
这里 HdfGetNode() 的参数是点分路径,从根节点开始,一层一层往下找。路径写错了,返回 NULL,驱动就初始化失败。所以,路径一定要和 HCS 文件里的结构严格对应。
Accelerometer,你代码里写 accelerometer,返回 NULL。还有,路径分隔符是点 .,不是斜杠 /。这两个细节,我 debug 了整整一个下午。
4.5 知识结构总览:一张图看懂 HCS
下面这张 SVG 图,把 HCS 的核心知识点串起来了。从文件结构到编译加载,一目了然。
4.6 避坑指南:我踩过的那些 HCS 的坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路。
| 问题 | 现象 | 原因 | 解决方法 |
|---|---|---|---|
| 路径大小写不匹配 | HdfGetNode 返回 NULL | HCS 节点名大小写敏感 | 严格保持 HCS 和代码中的大小写一致 |
| 字符串漏引号 | 编译报错:unexpected token | 字符串必须用双引号包裹 | 检查所有字符串属性是否加了引号 |
| 继承层级过深 | 属性值难以追踪 | 多层继承导致属性覆盖关系复杂 | 继承不超过两层,多用引用代替深层继承 |
| 忘记重新编译 .hcbs | 修改配置后驱动行为不变 | 驱动加载的是旧的 .hcbs 文件 | 在编译脚本中自动调用 hc-gen |
| 引用节点未加冒号标签 | 编译报错:undefined reference | 被引用的节点必须用 : 声明标签 |
给所有可能被引用的节点加上 : |
嗯,HCS 的语法其实不复杂,但细节很多。你只要把树形结构、节点属性、引用继承这三块搞明白,再记住编译加载的流程,写驱动配置基本不会出大问题。我刚开始学的时候,也是边写边踩坑,慢慢就熟练了。