3. 设备树基础语法(二):数据类型与地址单元
好,咱们接着聊设备树的语法。上一节我们把骨架搭起来了,这一节要往里面填“肉”——也就是各种数据类型。
说实话,我刚接触设备树那会儿,最头疼的就是这些数据类型。尤其是 phandle 和 #address-cells,看着就头大。但后来我发现,只要搞懂了它们背后的设计思路,其实一点都不难。
3.1 设备树支持的数据类型
设备树的数据类型,说白了就六种。我列个表给你看,一目了然:
| 数据类型 | 关键字/写法 | 说明 | 示例 |
|---|---|---|---|
| 32位整数 | u32 | 最常用的类型,地址、大小、标志位都用它 | reg = <0x10000000 0x1000>; |
| 64位整数 | u64 | 用两个 u32 拼起来表示 | reg = <0x00000000 0x80000000>; |
| 字符串 | string | 用双引号括起来 | compatible = "ti,am335x-gpio"; |
| 引用句柄 | phandle | 指向另一个节点的指针 | interrupt-parent = <&intc>; |
| 布尔值 | bool | 存在即为 true,不存在为 false | status = "disabled"; 或直接写属性名 |
| 字节串 | bytestring | 用方括号括起来的十六进制字节 | local-mac-address = [00 11 22 33 44 55]; |
3.2 逐个拆解,讲点实在的
3.2.1 u32 —— 最常用的“小兵”
u32 就是 32 位无符号整数。设备树里几乎所有数值都用它。比如描述一个 GPIO 控制器的寄存器基地址:
gpio0: gpio@44e07000 {
compatible = "ti,omap4-gpio";
reg = <0x44e07000 0x1000>;
interrupts = <96>;
};
这里 0x44e07000 是基地址,0x1000 是地址范围。两个都是 u32。
3.2.2 u64 —— 两个 u32 拼出来的“大块头”
有些 64 位 SoC,地址空间很大,一个 u32 装不下。这时候就用两个 u32 拼成一个 u64。
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0x00000001 0x00000000>;
};
上面这个例子,表示两段内存:第一段从 0x80000000 开始,大小 2GB;第二段从 0x100000000 开始,大小 4GB。注意看,每个地址和大小都是两个 u32 拼起来的。
#address-cells 和 #size-cells 必须设置为 2。否则编译器会按 u32 解析,结果全乱套。我曾经在调试一个 FPGA 加速卡时踩过这个坑,查了两天才发现是 cells 设错了。
3.2.3 string —— 最直观的类型
字符串用双引号括起来,多个字符串可以用逗号分隔。最常见的用途就是 compatible 属性:
compatible = "ti,am335x-gpio", "ti,omap4-gpio";
内核匹配驱动时,会从左到右依次尝试。第一个匹配上了就用,没匹配上就试下一个。所以一般把最具体的型号放前面,通用的放后面。
3.2.4 phandle —— 设备树的“指针”
phandle 是设备树里最巧妙的设计之一。它用来引用另一个节点。比如一个 UART 设备要连接某个中断控制器:
uart0: serial@44e09000 {
compatible = "ti,am335x-uart";
reg = <0x44e09000 0x2000>;
interrupt-parent = <&intc>;
interrupts = <72>;
};
intc: interrupt-controller@48200000 {
compatible = "ti,am335x-intc";
reg = <0x48200000 0x1000>;
interrupt-controller;
#interrupt-cells = <1>;
};
这里的 &intc 就是一个 phandle,指向了 intc 这个节点。编译时,设备树编译器会把 &intc 替换成对应的整数句柄值。
3.2.5 bool —— 有就是 true
布尔值在设备树里很简单:属性存在就是 true,不存在就是 false。比如:
interrupt-controller;
这个属性没有值,只要写了,就表示这个节点是一个中断控制器。内核驱动里检查 of_property_read_bool() 返回 true 就说明有这个属性。
3.2.6 bytestring —— 原始字节数据
字节串用方括号括起来,每个字节用空格隔开。最典型的应用是 MAC 地址:
local-mac-address = [00 11 22 33 44 55];
也可以用来存放一些固件参数、校准数据等。我在一个项目里就用字节串存放过 WiFi 模块的校准参数,省去了单独烧录 EEPROM 的麻烦。
3.3 #address-cells 和 #size-cells —— 地址单元的“尺子”
这两个属性,可以说是设备树里最容易让人迷糊的地方。我尽量讲清楚。
#address-cells 表示用几个 u32 来表示一个地址。
#size-cells 表示用几个 u32 来表示一个地址范围的大小。
看个例子就明白了:
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
gpio0: gpio@44e07000 {
reg = <0x44e07000 0x1000>;
};
uart0: serial@44e09000 {
reg = <0x44e09000 0x2000>;
};
};
这里 #address-cells = <1>,所以每个地址用一个 u32 表示。
#size-cells = <1>,所以每个大小也用一个 u32 表示。
那如果换成 64 位地址空间呢?
soc {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <1>;
dma@100000000 {
reg = <0x00000001 0x00000000 0x1000>;
};
};
看到没?地址用了两个 u32:0x00000001 0x00000000,拼起来就是 0x100000000。大小还是用一个 u32:0x1000。
#address-cells 设成了 2,但子节点的 reg 里只写了一个 u32 的地址。结果设备树编译不报错,但内核解析出来的地址完全不对。调试了整整一天才发现是 cells 和 reg 不匹配。所以记住:父节点的 cells 决定了子节点 reg 的格式,必须严格对应。
3.4 知识体系总览
说了这么多,我画个图帮你理一理思路:
3.5 组合使用,才是真功夫
实际项目中,这些数据类型很少单独出现。它们会组合在一起,描述一个完整的硬件设备。比如一个带 DMA 的以太网控制器:
ethernet@4a100000 {
compatible = "ti,cpsw";
reg = <0x4a100000 0x1000>;
interrupts = <40 41 42>;
interrupt-parent = <&intc>;
status = "okay";
local-mac-address = [00 00 00 00 00 00];
phy-mode = "rgmii";
#address-cells = <1>;
#size-cells = <0>;
phy0: ethernet-phy@0 {
reg = <0>;
};
};
你看,这里 u32、string、phandle、bool、bytestring 全用上了。#address-cells 和 #size-cells 也配合得恰到好处。
嗯,数据类型这部分就讲到这里。记住一句话:设备树的数据类型,本质上是为描述硬件服务的。搞懂了硬件需要什么信息,数据类型自然就选对了。
公众号:蓝海资料掘金营,微信deep3321