设备树节点与属性:标准属性、自定义属性与节点操作

设备树这东西,说白了就是给内核描述硬件的一本"说明书"。每个硬件模块对应一个节点,节点里塞满了各种属性。今天我就带你把这套规则彻底捋清楚。

我记得刚接触设备树那会儿,最头疼的就是搞不懂哪些属性是必须的,哪些可以自己定义。后来踩了几个坑,才慢慢摸出门道。

2.1 节点命名规则

每个设备树节点都得有个名字。格式很简单:node-name@unit-address。

  • node-name:描述设备类型,比如 uart、i2c、gpio
  • unit-address:设备的基地址或寄存器偏移,用来区分同名节点
// 正确示例
uart@101f0000 {
    compatible = "ns16550";
    reg = <0x101f0000 0x1000>;
};

// 错误示例 —— 两个节点同名且无地址区分
uart {
    ...
};
uart {
    ...
};
我曾经踩过的坑:两个 SPI 控制器都叫 spi,但忘了加地址后缀。内核解析时直接报错,说节点重复。从那以后,我只要看到总线上的设备,一定带上 @address。

2.2 标准属性详解

标准属性是设备树规范里定死的,内核解析器就认这些名字。我挑几个最常用的说。

2.2.1 compatible —— 灵魂属性

这个属性告诉内核:"我是谁,我能兼容谁"。格式是 "厂商,型号",越具体越靠前。

compatible = "ti,am335x-uart", "ns16550";

内核匹配驱动时,会从左往右试。第一个匹配上了就用,没匹配上就试第二个。我个人习惯把最精确的型号放第一位,通用型号放后面兜底。

2.2.2 reg —— 地址与大小

描述设备的寄存器空间。格式是 <地址 长度>,可以有多个地址段。

reg = <0x101f0000 0x1000>;
// 或者多段
reg = <0x101f0000 0x1000>, <0x101f1000 0x200>;

你想想看,如果 #address-cells 和 #size-cells 没配对,reg 解析出来全是错的。我见过有人把 #size-cells 设成 0,结果内核读到的地址完全不对。

2.2.3 interrupts —— 中断绑定

描述设备用哪个中断号、触发方式。需要配合 interrupt-parent 使用。

interrupt-parent = <&gic>;
interrupts = <0 42 4>;
// 0=SPI, 42=中断号, 4=高电平触发
小技巧:中断号不要直接写硬件号,要减去 32(SPI 基值)。我刚开始做的时候直接写了硬件号 74,结果中断死活不触发。查了半天才发现是号没对齐。

2.2.4 其他常用标准属性

属性名 作用 示例
status 设备状态:okay / disabled / fail status = "okay";
clock 时钟源绑定 clocks = <&clk_uart>;
pinctrl 引脚复用配置 pinctrl-0 = <&uart0_pins>;
dmas DMA 通道绑定 dmas = <&edma 12>;

2.3 自定义属性

标准属性不够用怎么办?自己定义。设备树允许你随便加属性,只要驱动能读懂就行。

// 自定义属性示例
my-device {
    compatible = "my,driver";
    reg = <0x2000000 0x100>;
    my,baud-rate = <115200>;      // 自定义波特率
    my,data-bits = <8>;           // 自定义数据位
    my,parity = "none";           // 自定义校验
};

我个人习惯给自定义属性加厂商前缀,比如 my,,避免跟未来标准属性冲突。驱动里用 of_property_read_u32() 就能读出来。

实战经验:我在一个项目里需要配置 LCD 的时序参数,标准属性里没有 h-front-porch 这种东西。我就自己定义了 panel,h-front-porch、panel,v-sync-len 等属性,驱动里一次性读出来,非常灵活。

2.4 节点引用与覆盖

设备树支持用 &label 引用已有节点,然后追加或覆盖属性。这个功能在板级文件中用得特别多。

// 原始 dtsi 文件
uart0: uart@101f0000 {
    compatible = "ns16550";
    reg = <0x101f0000 0x1000>;
    status = "disabled";
};

// 板级 dts 文件
&uart0 {
    status = "okay";
    clock-frequency = <48000000>;
};

这样做的好处是:SoC 厂商提供通用的 .dtsi,板级文件只改自己需要的部分。不用把整个节点复制一遍。

2.4.1 覆盖规则

  • 追加属性:如果原节点没有该属性,直接加上
  • 覆盖属性:如果原节点已有,新值替换旧值
  • 删除属性:用 /delete-property/ 或 /delete-node/
// 删除属性示例
&uart0 {
    /delete-property/ clock-frequency;
};

// 删除节点示例
&uart0 {
    /delete-node/;
};
注意:覆盖时不能改变 reg 的地址段数量,否则内核解析会出问题。我见过有人覆盖 reg 时少写了一个地址段,结果驱动读到的寄存器空间完全错位。

2.5 知识结构总览

下面这张图把设备树节点与属性的核心逻辑串起来了。你对照着看,思路会更清晰。

设备树节点与属性知识结构 设备树根节点 子节点 uart@101f0000 子节点 i2c@2000000 子节点 gpio@3000000 标准属性 compatible reg interrupts 自定义属性 my,baud-rate my,data-bits panel,h-front-porch 节点操作 &label 引用 属性覆盖 /delete-node/ 节点命名:node-name@unit-address 引用覆盖:&uart0 { status = "okay"; }; 最终生成:设备树源文件 .dts

2.6 实战建议

说了这么多,我总结几条实际干活时的经验:

  1. 命名规范要统一:整个项目用同一套命名风格,别一会儿驼峰一会儿下划线
  2. 标准属性优先:能用标准属性就别自己造,内核工具链支持更好
  3. 自定义属性加前缀:避免未来跟新标准属性冲突
  4. 引用覆盖别乱改 reg:地址段数量一变,驱动直接崩
  5. 多用 status 控制:调试时把不用的设备设成 disabled,省得干扰
我的习惯:每次写完设备树,我都会用 dtc -I dts -O dtb 编译一遍,再用 dtc -I dtb -O dts 反编译回来。看看反编译结果跟原文件是否一致,能发现很多隐藏问题。

嗯,设备树的节点和属性就这些核心内容。你把这些规则吃透了,后面写驱动、调帧率的时候,设备树这块就不会再卡你。