news 2026/2/8 9:34:27

设备树节点与属性:手把手教程(小白指南)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设备树节点与属性:手把手教程(小白指南)

设备树节点与属性:从零开始的实战指南(新手也能懂)

你有没有遇到过这种情况:明明写好了驱动代码,烧录进开发板后却“纹丝不动”?串口没输出、GPIO 控制不了、I²C 传感器读不到数据……最后翻来覆去查了半天,问题竟然出在一个.dts文件里的一行配置?

在现代嵌入式 Linux 开发中,设备树(Device Tree)就是那个藏在幕后、掌控一切硬件命脉的关键角色。它不像 C 代码那样直接执行逻辑,但它决定了内核“知道有哪些外设”、“该加载哪个驱动”、“资源怎么分配”。可以说——不懂设备树,就别谈会写 Linux 驱动

本文不玩虚的,也不堆术语。我们将以一个真实开发场景为线索,带你一步步看懂设备树的结构、理解它的规则,并最终学会如何手动添加一个外设节点。无论你是刚接触嵌入式的新人,还是已经摸过几块开发板但一直对.dts文件心存畏惧的老兵,这篇都能让你豁然开朗。


为什么我们需要设备树?一个故事讲明白

想象一下:你是一家芯片公司的工程师,负责设计一款叫MySoC-1000的处理器。这款芯片性能很强,支持 UART、SPI、I2C、PWM 等一堆外设控制器。

现在有两个客户买了你的芯片:

  • 客户 A 做了一块评估板,上面接了个 SSD1306 OLED 屏;
  • 客户 B 做了个工业网关,挂了两个温湿度传感器和一个 RS485 模块。

问题是:这两块板子用的是同一款 CPU,但外设完全不同。如果让 Linux 内核“内置”所有可能的硬件信息,那得编译多少个内核版本?每换一块板子就要改一次代码?显然不现实。

于是,设备树出现了。

设备树的本质,是一份描述“这块板子上到底连了啥”的说明书

你可以把内核想象成一个通用大脑,而设备树就是告诉这个大脑:“嘿,你现在跑在一块带 OLED 和 TMP102 的板子上。”
不需要重新编译大脑,只要换个说明书就行。

这就是所谓的硬件抽象化 + 配置外部化—— 也是设备树最核心的价值。


设备树长什么样?先看一棵“树”

设备树本质上是一棵以/为根的树形结构,由节点属性构成。

最基本的骨架

/ { model = "My Embedded Board"; compatible = "mycompany,myboard"; cpus { #address-cells = <1>; #size-cells = <0>; cpu@0 { compatible = "arm,cortex-a53"; reg = <0>; }; }; memory@80000000 { device_type = "memory"; reg = <0x80000000 0x40000000>; /* 1GB */ }; };

这段代码虽然短,但包含了设备树最基本的元素:

  • /是根节点。
  • modelcompatible描述了整个开发板的信息。
  • cpusmemory是标准节点,必须存在。
  • 节点内部可以嵌套子节点,形成层级关系。
  • 每个键值对就是一个属性(property),比如reg = <0x80000000 0x40000000>;表示内存起始地址和大小。

这棵树不是随便画的,它是对真实硬件拓扑的建模。就像电路图有主控、总线、外设一样,设备树也有对应的层次结构。


节点命名规则:别被@&吓到

刚开始看.dts文件时,很多人会被这些符号搞晕:

spi@7e806000 { status = "okay"; }
&spi0 { status = "okay"; }

它们其实各有用途。

1. 正常定义一个新节点

格式是:

[label:] node-name[@unit-address]
  • node-name:节点名,建议小写+连字符,如i2c,uart,gpio-leds
  • @unit-address:单元地址,通常是该设备寄存器的物理基地址
  • label:标签,用于后续引用(类似变量名)

举个例子:

uart1: serial@10000000 { compatible = "snps,dw-apb-uart"; reg = <0x10000000 0x100>; interrupts = <0 32 4>; clocks = <&clkc 13>; status = "okay"; };

这里我们做了几件事:

  • 给节点打了标签uart1,后面可以用&uart1引用它;
  • 名称是serial@10000000,说明这是一个串口控制器,位于地址0x10000000
  • reg明确指出寄存器范围:从0x10000000开始,占 256 字节;
  • interrupts指定中断号(SPI 中断 32)、触发方式等;
  • clocks引用了另一个叫clkc的时钟控制器节点的第 13 号输出。

注意:reg的格式是由父节点中的#address-cells#size-cells决定的。常见组合如下:

#address-cells#size-cellsreg 示例
<1><1><0x10000000 0x100>
<2><1><0x0 0x10000000 0x100>(高位+低位地址)

一般情况下都是<1>, <1>,除非遇到 PCIe 这种复杂总线。


2. 修改已有节点:用&引用

有时候你不希望从头写一个节点,而是想修改 SoC 公共文件里已经定义好的内容。这时候就要用&

比如,在rk3568.dtsi中已经有:

spi0: spi@fc004000 { reg = <0xfc004000 0x1000>; interrupts = <GIC_SPI 69 IRQ_TYPE_LEVEL_HIGH>; #address-cells = <1>; #size-cells = <0>; status = "disabled"; };

但在你的板子上,你要启用 SPI 并挂一个 OLED 屏。你不需要复制整个节点,只需这样写:

&spi0 { status = "okay"; oled_display: oled@0 { compatible = "solomon,ssd1306fb-spi"; reg = <0>; spi-max-frequency = <1000000>; reset-gpios = <&gpio5 RK_PB7 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio5 RK_PB6 GPIO_ACTIVE_LOW>; }; };

这里的&spi0就是在“打补丁”,把原来的状态改成"okay",然后加上子设备。

这种机制极大提升了复用性:SoC 厂商提供.dtsi定义所有控制器,板级开发者只关心自己接了什么外设。


属性详解:哪些字段最关键?

每个节点都靠属性来“说话”。下面这几个是最常用、也最容易出错的。

compatible:决定谁能管你

这是最重要的属性,没有之一。

compatible = "vendor,device-model";

例如:

compatible = "ti,tmp102"; compatible = "bosch,bme280-i2c"; compatible = "st,stmpe610";

内核启动时会遍历所有平台设备,拿着这个字符串去匹配驱动注册表里的.of_match_table。只有匹配成功,才会调用probe()函数。

而且支持多级兼容:

compatible = "fsl,imx8mq-i2c", "fsl,imx35-i2c";

意思是:“优先按 i.MX8MQ 的方式处理,不行就退化成 i.MX35 的老方法”,实现向后兼容。

⚠️ 提示:如果你新加了一个设备但驱动没加载,第一件事就是检查compatible是否拼错或未被任何驱动支持。


reg:我在哪?有多大?

描述设备的寄存器地址空间。

reg = <地址 地址长度>;

对于挂在 APB/AHB 总线上的外设,通常只有一个地址段:

uart@10000000 { reg = <0x10000000 0x100>; };

如果是内存映射设备(如 framebuffer),可能是多个区域:

gpu@deadbeef { reg = <0xdeadbeef 0x1000>, <0xcafebabe 0x2000>; };

记住:reg的 cell 数量由父节点的#address-cells#size-cells控制!


interruptsinterrupt-parent

中断系统非常关键,尤其当你做按键、ADC、DMA 外设的时候。

interrupts = <类型 编号 触发方式>;

ARM GIC 下常见格式:

interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>; // SPI 中断,编号 32,高电平触发

或者简写为:

interrupts = <0 32 4>; // type=0(SPI), irq=32, flags=4(level-high)

如果没有指定interrupt-parent,默认继承父节点。如果你想指定特定中断控制器,可以显式声明:

interrupt-parent = <&gpio_intc>;

gpios/clocks/pinctrl:三大关联属性

这三个属性都不是本地定义,而是通过phandle引用其他节点。

GPIO 示例
reset-gpios = <&gpio5 7 GPIO_ACTIVE_HIGH>;

分解一下:

  • &gpio5:指向名为gpio5的 GPIO 控制器节点;
  • 7:使用该控制器的第 7 号引脚;
  • GPIO_ACTIVE_HIGH:表示高电平有效(宏定义为 0);

驱动中可以通过:

int gpio = of_get_named_gpio(np, "reset-gpio", 0);

获取实际的 GPIO 编号并请求控制权。

Pinctrl 引脚复用配置

很多 SoC 引脚是多功能的,需要明确设置工作模式。

pinctrl-names = "default"; pinctrl-0 = <&pinctrl_oled_default>; &iomuxc { pinctrl_oled_default: oledgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0x10b0 MX6UL_PAD_UART1_TX_DATA__UART1_DCE_RX 0x17071 >; }; };

pinctrl-0指向一组引脚配置,内核会在 probe 前自动应用。

Clock 引用
clocks = <&clkc IMX8MQ_CLK_I2C1>; clock-names = "i2c";

告诉驱动:“请帮我打开 I2C1 的时钟”。


实战:手把手添加一个 I²C 温度传感器

假设你在自己的板子上焊接了一个 TI 的 TMP102 温度传感器,连接到 I2C1,地址是0x48

我们要做的就是:让内核知道这个设备的存在,并加载正确的驱动

第一步:找到 I2C 控制器节点

查看你的.dts.dtsi文件,确认 I2C1 是否已启用:

&i2c1 { clock-frequency = <100000>; status = "okay"; }

确保状态是"okay",否则不会初始化这条总线。

第二步:添加子设备节点

&i2c1块内加入:

tmp102@48 { compatible = "ti,tmp102"; reg = <0x48>; };

就这么简单!不需要写地址范围,因为 I2C 设备只有一个字节的设备地址。

保存后重新编译 dtb:

make ARCH=arm dtbs

重启开发板,看看日志:

dmesg | grep tmp102

你应该看到类似输出:

i2c i2c-1: new_device: client registered successfully thermal thermal_zone0: sensor 'tmp102'

恭喜!你刚刚完成了一次完整的设备树配置。


常见坑点与调试技巧

设备树看似简单,但一不小心就会掉进坑里。以下是几个高频问题及应对方法。

❌ 问题1:驱动不加载,probe 不触发

排查步骤

  1. 检查compatible是否拼写正确;
  2. 查看dmesg是否提示 “no matching node found”;
  3. 使用of_node打印当前设备节点信息;
  4. 确认.dtb已更新并被 U-Boot 正确加载。

❌ 问题2:GPIO 引脚无效或行为异常

原因

  • pinctrl没配置,引脚功能不对;
  • gpios引用错误控制器或编号越界;
  • 极性设置反了(ACTIVE_HIGH vs ACTIVE_LOW)。

解决

使用cat /sys/kernel/debug/pinctrl/*/pinmux-pins查看当前引脚状态。

❌ 问题3:I2C 设备扫描不到

i2cdetect -y -r 1

如果显示UU,说明设备已被驱动占用;如果是--,说明没响应。

可能原因:

  • 地址写错(7位 vs 8位);
  • 上拉电阻缺失;
  • 电源未供上;
  • 设备树中reg写成了十进制而非十六进制。

🛠️ 小技巧:可用fdtdumpdtc -I dtb -O dts反编译运行时加载的.dtb,确认最终生效的配置。


如何写出高质量的设备树?五个最佳实践

  1. 拆分.dtsi.dts

SoC 公共部分放.dtsi,板级定制放.dts,提高可维护性。

  1. 善用标签和引用

dts &uart1 { status = "okay"; }

比手动复制一大段代码更安全、简洁。

  1. 遵循上游绑定文档

查阅Documentation/devicetree/bindings/目录下的文档,使用标准属性名和格式。

  1. 启用 overlay 支持动态扩展

对于 Raspberry Pi HAT、USB 扩展卡等热插拔场景,使用CONFIG_OF_OVERLAY=y,运行时加载.dtbo

  1. 定期验证语法

编译时加上检查:

bash make dtbs_check

或手动运行:

bash dtc -I dts -O dtb -o test.dtb your_file.dts

静默通过才算合格。


结语:掌握设备树,你就掌握了硬件入口

设备树不是魔法,它是嵌入式 Linux 中一种极为实用的设计哲学:把硬件描述从代码中剥离出来,变成可配置的数据

当你能熟练阅读一份.dts文件,看出其中的 CPU、内存、外设连接关系;当你能在不改内核的情况下成功接入一个新的传感器;当你通过dmesg看到“probe success”那一刻——你就真正迈进了驱动开发的大门。

下次再遇到“外设不工作”的问题,别急着怀疑代码,先去看看那棵静静躺在内存里的“树”吧。也许答案,就在/soc/i2c@7e804000的某个角落。

如果你在实践中遇到了具体的设备树难题,欢迎留言讨论。我们可以一起分析.dts片段,找出隐藏的 bug。毕竟,每一个成功的 probe,都值得庆祝。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/8 1:50:34

Cloudy模糊效果库完整指南:为Jetpack Compose提供跨平台模糊支持

Cloudy模糊效果库完整指南&#xff1a;为Jetpack Compose提供跨平台模糊支持 【免费下载链接】Cloudy ☁️ Jetpack Compose blur effect library, which falls back onto a CPU-based implementation to support older API levels. 项目地址: https://gitcode.com/gh_mirror…

作者头像 李华
网站建设 2026/2/4 6:03:47

LED阵列汉字显示实验硬件基础:行扫描机制全面讲解

从零点亮汉字&#xff1a;深入理解LED点阵的行扫描机制你有没有想过&#xff0c;那些街头巷尾滚动播放“开业大吉”“欢迎光临”的红色LED屏&#xff0c;是怎么用几块小芯片就实现中文显示的&#xff1f;它们没有操作系统&#xff0c;没有图形界面库&#xff0c;甚至连内存都少…

作者头像 李华
网站建设 2026/2/8 5:51:43

Steam Deck Tools实战指南:Windows系统高效掌控游戏掌机

Steam Deck Tools实战指南&#xff1a;Windows系统高效掌控游戏掌机 【免费下载链接】steam-deck-tools (Windows) Steam Deck Tools - Fan, Overlay, Power Control and Steam Controller for Windows 项目地址: https://gitcode.com/gh_mirrors/st/steam-deck-tools 你…

作者头像 李华
网站建设 2026/2/4 16:07:35

终极Rust存档工具:5步掌握UE游戏进度编辑

终极Rust存档工具&#xff1a;5步掌握UE游戏进度编辑 【免费下载链接】uesave-rs 项目地址: https://gitcode.com/gh_mirrors/ue/uesave-rs 还在为复杂的Unreal Engine游戏存档格式而烦恼吗&#xff1f;想要轻松备份、修改游戏进度却无从下手&#xff1f;这款基于Rust语…

作者头像 李华
网站建设 2026/2/5 11:51:24

ZyPlayer免费跨平台播放器:3小时快速上手完整指南

ZyPlayer免费跨平台播放器&#xff1a;3小时快速上手完整指南 【免费下载链接】ZyPlayer 跨平台桌面端视频资源播放器,免费高颜值. 项目地址: https://gitcode.com/gh_mirrors/zy/ZyPlayer 想要在Windows、macOS或Linux系统上享受免费高颜值的视频播放体验吗&#xff1f…

作者头像 李华
网站建设 2026/2/6 18:27:49

基于Qwen3-VL的视觉代理技术详解:自动操作PC与移动GUI的新范式

基于Qwen3-VL的视觉代理技术详解&#xff1a;自动操作PC与移动GUI的新范式 在智能助手还停留在“回答问题”阶段时&#xff0c;真正的AI进化已经开始——它正学会“动手做事”。想象一下&#xff1a;你只需说一句“帮我登录邮箱并查一下今天的会议安排”&#xff0c;AI就能自主…

作者头像 李华