4.2 设备树:把硬件拓扑写成数据


4.2 设备树:把硬件拓扑写成数据

本节摘要:设备树是用数据描述板载硬件的标准格式。本节从一段真实的板级描述入手讲语法(节点、属性、引用),走通从源码到设备Blob的编译流程,再看驱动侧如何用资源接口取出地址与中断,最后把第 2、3 章所有写死的常量一次性替换干净。

一份设备树源文件读起来像一份硬件清单的族谱:根节点代表整块板子,子节点代表总线与外设,节点属性写明寄存器区、中断、时钟、引脚配置。本章主线里那盏 LED 与那个按键,最终都会在这棵树上各占一个节点。先看一段浓缩了全部常见要素的源码:

/* 板级描述源文件片段(节选自一块 ARM 开发板) */ /dts-v1/; / { compatible = "vendor,eval-board"; #address-cells = <1>; #size-cells = <1>; leds { compatible = "vendor,board-led"; reg = <0xfdd20000 0x100>; /* 寄存器基址与长度 */ led-gpio = <18>; /* 自定义属性:第 18 号引脚 */ }; keys { compatible = "vendor,board-key"; interrupt-parent = <&gic>; /* 中断走到哪个控制器 */ interrupts = <0 42 4>; /* 中断号与触发类型 */ }; i2c0: i2c@ff020000 { compatible = "vendor,i2c-ctrl"; reg = <0xff020000 0x1000>; clock-frequency = <400000>; /* 四百千赫兹快速模式 */ #address-cells = <1>; #size-cells = <0>; sensor@48 { compatible = "vendor,tmp-sensor"; reg = <0x48>; /* I2C 从地址 */ }; }; };

语法三件套:节点、属性、引用

节点是大括号包起的一个硬件单元,名字惯例是"设备名加单元地址",如 i2c@ff020000 里的艾特符号后就是其总线地址。属性是键值对:值可以是字符串、数字列表或字节序列。三组属性最常打照面:compatible 是匹配凭据(4.1 节的契约文本);reg 是寄存器区描述(基址加长度,长度含义由 #size-cells 约定);interrupts 是中断声明(中断父节点、编号、触发类型)。

引用用艾特加标签写成,interrupt-parent = <&gic> 意思是"我的中断由标签为 gic 的控制器接管"。引用机制让树里不必重复抄写控制器节点——一个标签,处处引用,这正是"树"的拓扑表达力:设备挂在控制器下、中断通向控制器、时钟取自时钟单元,物理连接关系一图讲清。

节点标签 i2c0: 这类前缀是给人用的快捷名。树里还有一个隐含规则值得点破:sensor@48 的地址单元数由父节点 #address-cells 决定——I2C 总线上单元地址是从地址(一个单元),而内存总线上是物理地址(一到两个单元)。cell 声明就是树里的"度量衡制度",父子之间必须对齐。

编译与加载:从源码到内核

设备树源文件不直接被内核读取,而是编译成紧凑的二进制Blob:

$ dtc -I dts -O dtb -o board.dtb board.dts # 源码编译为二进制 $ dtc -I dtb -O dts -o dump.dts board.dtb # 反向反编译,排错常用 $ fdtdump board.dtb | head -20 # 查看二进制内容

启动时引导程序把Blob连同内核一起加载,内核启动早期将其展开成内部设备树结构,再按节点逐个创建平台设备(或 I2C、SPI 设备)注册进相应总线——4.1 节"设备树展开成平台设备"那一格说的就是这一步。改板子、换外设,只改树源文件重编译,内核与驱动代码纹丝不动。

排错时有两个系统接口是你的老朋友:运行中的系统会把整棵设备树以文本形式暴露在系统目录下,随时可查;每个设备的 sys 条目也能反向找到它的树节点。第 7 章的"probe 不来"实录里,第一步就是核对这两个地方。

设备树到驱动的信息流转

设备树到驱动的信息流转

驱动侧:把写死的常量全部换掉

4.1 节的 probe 已经用资源接口拿过寄存器,这里补齐中断与自定义属性的取法,LED 驱动的"去硬编码"就此收官:

#include <linux/of.h> #include <linux/platform_device.h> static int led_probe(struct platform_device *pdev) { struct resource *res; void __iomem *base; int gpio, irq, ret; /* 寄存器区:树里的 reg 属性 */ res = platform_get_resource(pdev, IORESOURCE_MEM, 0); base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(base)) return PTR_ERR(base); /* 中断号:树里的 interrupts 属性,控制器已解析 */ irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; /* 自定义属性:led-gpio,取不到给默认值 */ if (of_property_read_u32(pdev->dev.of_node, "led-gpio", &gpio)) gpio = 18; pr_info("board-led: 寄存器已映射,引脚 %d,中断 %d\n", gpio, irq); return 0; }

对比一下第 2 章的写法:基地址从写死变成树提供,中断号从写死变成树提供,引脚号从写死变成树提供(还有缺省兜底)。同一份驱动二进制,放进设备树描述不同的两块板,无需重编译——这份可移植性就是当初那场"解耦手术"的复利。

💡 关键直觉:设备树属性分两层——标准属性(reg、interrupts)由总线框架解析成资源对象,驱动用资源接口取;自定义属性存在节点上,驱动用属性接口按名取。分清"框架代收"与"自助提取",取数代码就不会写错层。

树也会长坏:三棵歪树示例

引用悬空interrupt-parent 指向不存在的标签,编译期就报错——这类问题最友好,出不了门。

cell 度量衡错配:父节点声明单地址单元,子节点 reg 却写了两个数,反编译看着正常、解析全乱——树能编过,设备地址离奇。排查靠逐层核对 cell 声明。

compatible 大小写或前缀不一致:树里"Vendor,board-led"、匹配表里"vendor,board-led",肉眼难辨、匹配必败。这就是 4.1 节警告过的那类"安静失败",第 7 章的实录会给完整破案过程。

本节要点回顾

  • 树是硬件说明书:节点表设备、属性表参数、引用表连接,编译成Blob随内核启动。
  • 标准属性框架代收:reg 与 interrupts 变成资源对象,probe 里按索引取。
  • 自定义属性自助提取:of_ 属性接口按名读取,配缺省值增强容错。
  • 改板不改码:板级差异全部沉淀在树里,驱动二进制跨板复用。

通用平台设备走通了,但树里那个挂在 I2C 控制器下的传感器节点还没人管——下一节把总线模型套到 I2C 与 SPI 上,看真实总线上的驱动长什么样。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U