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)。 - 支持注释:
//单行,/* */多行。
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; }; |
用花括号包裹,内部是属性列表 |
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文件写好了,怎么被系统用起来?这个过程分为三步:编译、加载、解析。
下面这张图展示了完整的流程:
流程看起来不复杂,但有几个细节值得注意:
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()。它会:
- 打开hcb文件。
- 读取文件头,校验魔数和版本。
- 解析节点树,构建内存中的
HdfDeviceObject结构。 - 建立节点间的父子关系和引用关系。
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的语法检查很严格,写完后跑一遍,能省去很多调试时间。