设备树详解与实践
1. 概述
1.1 功能简介
设备树(Device Tree)是一种描述硬件资源的数据结构,用于将硬件配置信息从 Linux 内核中解耦。RK3576 使用设备树来描述 SoC 内部资源(GPIO、I2C、SPI、UART 等)以及板级外设的连接关系。设备树文件通过编译生成 DTB(Device Tree Blob),由 U-Boot 传递给内核。
1.2 应用场景
- 板级适配:为新硬件平台编写设备树,描述外设连接关系
- 外设驱动开发:在设备树中配置 GPIO/I2C/SPI 等接口的 pinmux、时序参数
- 内核配置解耦:通过修改设备树而非内核代码来适配不同硬件版本
- Overlay 机制:动态加载设备树覆盖片段,实现热插拔外设支持
1.3 工作原理
设备树采用树形结构描述硬件:根节点(/)下包含 CPU 节点、内存节点、SoC 总线节点等,总线下挂载各外设控制器节点。RK3576 的设备树采用分层结构:SoC 级 .dtsi 定义通用 IP 模块,板级 .dts 引用 .dtsi 并添加板级具体配置。U-Boot 在启动时会将 DTB 加载到内存中,内核通过 DTB 获取硬件信息并初始化对应的驱动。
原理类内容优先引用 RK 官方文档,此处仅做要点提炼。 详见 RK 官方文档:[《Rockchip_Developer_Guide_Linux_Software_CN.pdf》]
2. 硬件准备
2.1 硬件清单
| 组件 | 型号/规格 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | C-3576BQ 核心板 + M4-R2 底板 | 1 | |
| 调试串口线 | USB 转 TTL(3.3V) | 1 | 波特率 1500000 |
| 电源适配器 | 5V 电源适配器(Type-C 或 6pin 输入) | 1 | 空载最小 1000mA |
| 外设模块 | 示例:I2C 传感器 / SPI 模块 | 若干 | 根据实际需要 |
2.2 硬件确认
# 查看当前设备树信息
cat /proc/device-tree/model
# 查看设备树完整结构
ls /proc/device-tree/
# 查看设备树中某个节点的属性
cat /proc/device-tree/soc/serial@xxx/compatible3. 软件准备
3.1 设备树工具链
| 工具 | 用途 | 安装方法 |
|---|---|---|
dtc | 设备树编译器/反编译器 | sudo apt-get install device-tree-compiler |
dtc -I dtb -O dts | 反编译 DTB 为可读的 DTS | SDK 内置或在主机安装 |
dtc -I dts -O dtb | 编译 DTS 为 DTB | 同上 |
fdtget | 查看 DTB 属性 | sudo apt-get install libfdt-dev |
fdtput | 修改 DTB 属性 | 同上 |
3.2 内核配置
确保内核已启用设备树支持(默认已开启):
CONFIG_OF=y
CONFIG_OF_FLAT_TREE=y
CONFIG_OF_EARLY_FLATTREE=y
CONFIG_OF_DYNAMIC=y如需 Overlay 支持,还需启用:
CONFIG_OF_OVERLAY=y
CONFIG_OF_CONFIGFS=y4. 操作步骤
4.1 设备树语法基础
4.1.1 基本结构
/dts-v1/;
/ {
model = "shimetapi RK3576 Board";
compatible = "rockchip,rk3576";
chosen {
stdout-path = &uart2;
bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0"; /* 示例,以实际 SDK 为准 */
};
memory {
device_type = "memory";
reg = <0x0 0x0 0x0 0x80000000>; /* 4GB RAM(示例值) */
};
};以上节点中的寄存器地址、中断号、时钟标识均为 RK 平台通用示例,具体数值以实际发布的 RK3576 SDK 设备树为准。
4.1.2 常用属性
| 属性 | 说明 | 示例 |
|---|---|---|
compatible | 设备兼容性标识 | "rockchip,rk3576-uart" |
reg | 寄存器地址/大小 | <0x0 0xfe660000 0x0 0x100>(示例) |
interrupts | 中断号 | <GIC_SPI 112 IRQ_TYPE_LEVEL_HIGH>(示例) |
clocks | 时钟引用 | <&cru PCLK_UART2>(示例) |
pinctrl | Pinmux 配置 | pinctrl-0 = <&uart2m0_xfer> |
status | 设备状态 | "okay" 或 "disabled" |
#address-cells | 地址字段单元格数 | <2>(64 位地址) |
#size-cells | 大小字段单元格数 | <2>(64 位大小) |
4.1.3 标签与引用
// 定义标签(label)
uart2: serial@fe660000 { /* 地址为示例,以实际 SDK 为准 */
compatible = "rockchip,rk3576-uart";
reg = <0x0 0xfe660000 0x0 0x100>; /* 示例 */
interrupts = <GIC_SPI 112 IRQ_TYPE_LEVEL_HIGH>; /* 示例 */
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>; /* 示例 */
clock-names = "baudclk", "apb_pclk";
pinctrl-names = "default";
pinctrl-0 = <&uart2m0_xfer>;
status = "disabled";
};
// 使用 & 引用标签修改节点
&uart2 {
status = "okay";
};4.2 RK3576 设备树结构
4.2.1 文件层次
RK3576 的设备树文件采用分层引用结构(文件名以实际 SDK 为准):
rk3576.dtsi # SoC 级定义(所有 IP 模块、时钟、中断)
├── rk3576-pinctrl.dtsi # Pinmux 定义(所有 IOMUX 配置)
├── rk3576-power.dtsi # 电源域定义
└── rk3576-vop.dtsi # VOP 显示控制器定义
rk3576-evb.dtsi # EVB 板级定义(PMIC、DDR、外设)
├── rk3576-xxx.dts # 具体板级 DTS 文件
└── rk3576-xxx-linux.dts # Linux 专用配置4.2.2 SoC 级 dtsi 结构(rk3576.dtsi)
#include <dt-bindings/...>
/ {
compatible = "rockchip,rk3576";
// CPU 核心(8 核:4×A72 + 4×A53,RK3576 大小核架构)
cpus {
#address-cells = <2>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a72";
reg = <0x0 0x0>;
enable-method = "psci";
clocks = <&scmi_clk 0>;
operating-points-v2 = <&cpu0_opp_table>;
};
// cpu1~cpu3 (A72) 和 cpu4~cpu7 (A53) 类似...
};
// 内存控制器(LPDDR4/LPDDR5)
dmc: dmc {
compatible = "rockchip,rk3576-dmc";
// DDR 配置...
};
// SoC 总线
soc: soc@fda00000 {
compatible = "simple-bus";
#address-cells = <2>;
#size-cells = <2>;
ranges;
// 中断控制器
gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0xfd400000 0x0 0x10000>,
<0x0 0xfd460000 0x0 0x100000>;
};
// 时钟控制器
cru: clock-controller@fdd20000 {
compatible = "rockchip,rk3576-cru";
reg = <0x0 0xfdd20000 0x0 0x1000>;
#clock-cells = <1>;
#reset-cells = <1>;
};
// NPU 节点(RK3576 内置,6 TOPS)
rknpu: rknpu@fde40000 {
compatible = "rockchip,rknpu";
reg = <0x0 0xfde40000 0x0 0x10000>;
// NPU 配置...
};
// 各外设控制器:UART、I2C、SPI、GPIO、GMAC、USB 等
// ...
};
};以上节点地址为 RK 平台通用结构示例,具体寄存器基地址与中断号以实际发布的 RK3576 SDK 设备树为准。 RK3576 为 8 核 CPU(4×Cortex-A72 + 4×Cortex-A53),详见核心板规格书(S-02)。
4.2.3 内核设备树路径
# 内核设备树源文件路径(文件名与实际 SDK 版本一致)
ls kernel/arch/arm64/boot/dts/rockchip/rk3576*.dts
ls kernel/arch/arm64/boot/dts/rockchip/rk3576*.dtsi4.3 GPIO 节点配置
4.3.1 GPIO 控制器节点
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru PCLK_GPIO0>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller;
#interrupt-cells = <2>;
};寄存器基地址为示例,以实际 SDK 为准。RK3576 提供 GPIO0~GPIO4 共 5 组 GPIO。
4.3.2 GPIO 使用示例
// 在设备树中引用 GPIO(以 M4-R2 40PIN 引脚为例)
my_device {
compatible = "my,device";
// GPIO3_A1(40PIN Pin19,SPI MOSI 复用),低电平有效
enable-gpios = <&gpio3 RK_PA1 GPIO_ACTIVE_LOW>;
// GPIO4_C7(40PIN Pin32,PWM0),高电平有效
reset-gpios = <&gpio4 RK_PC7 GPIO_ACTIVE_HIGH>;
};RK3576 GPIO 编号规则:每组 GPIO 有 32 个 pin,编号为 0-31。GPIO4_C7 表示 GPIO4 的第 C 组第 7 个 pin,即 4 * 32 + 2 * 8 + 7 = 151。
M4-R2 底板 40PIN 引脚定义见 40PIN 引脚定义。
4.3.3 Pinmux 配置(pinctrl)
Pinmux 用于配置 GPIO 引脚的功能复用,在 rk3576-pinctrl.dtsi 中定义:
pinctrl: pinctrl {
compatible = "rockchip,rk3576-pinctrl";
rockchip,grf = <&grf>;
rockchip,pmu = <&pmugrf>;
#address-cells = <2>;
#size-cells = <2>;
ranges;
// 功能组定义
uart2 {
uart2m0_xfer: uart2m0-xfer {
rockchip,pins =
<0 RK_PD4 2 &pcfg_pull_up>, // GPIO0_D4(M4-R2 调试串口 TX),func 2
<0 RK_PD5 2 &pcfg_pull_up>; // GPIO0_D5(M4-R2 调试串口 RX),func 2
};
};
};说明:M4-R2 底板原理图(S-04)中调试串口网络为
UART0_TX_M0_DEBUG(GPIO0_D4_D)/UART0_RX_M0_DEBUG(GPIO0_D5_D),与规格书标注的 UART2(RX2/TX2)命名不一致,具体以实际物料与 SDK 配置为准(待板上验证)。以上 pinmux 配置为示例写法。
4.4 I2C 节点配置
4.4.1 I2C 控制器节点
i2c1: i2c@fe5a0000 {
compatible = "rockchip,rk3576-i2c", "rockchip,rk3399-i2c";
reg = <0x0 0xfe5a0000 0x0 0x1000>;
clocks = <&cru CLK_I2C1>, <&cru PCLK_I2C1>;
clock-names = "i2c", "pclk";
interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&i2c1m0_xfer>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};寄存器和中断号均为示例,以实际 SDK 为准。M4-R2 40PIN 引脚(Pin3/Pin5)提供 I2C 接口,详细说明见 I2C 驱动开发。
4.4.2 挂载 I2C 从设备
&i2c1 {
status = "okay";
clock-frequency = <400000>; // 400kHz (Fast Mode)
// 挂载 I2C 温度传感器
temp_sensor: temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio3>;
interrupts = <RK_PB1 IRQ_TYPE_LEVEL_LOW>;
};
};4.5 SPI 节点配置
4.5.1 SPI 控制器节点
spi1: spi@fe610000 {
compatible = "rockchip,rk3576-spi", "rockchip,rk3066-spi";
reg = <0x0 0xfe610000 0x0 0x1000>;
clocks = <&cru CLK_SPI1>, <&cru PCLK_SPI1>;
clock-names = "spiclk", "apb_pclk";
interrupts = <GIC_SPI 52 IRQ_TYPE_LEVEL_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&spi1m1_cs0 &spi1m1_pins>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};寄存器和中断号均为示例,以实际 SDK 为准。M4-R2 40PIN 提供 SPI 接口(Pin19 MOSI=GPIO3_A1、Pin21 MISO=GPIO3_A2、Pin23 SCK=GPIO3_A0、Pin24 CE0=GPIO3_A3、Pin26 CE1=GPIO2_D7),详细说明见 SPI 驱动开发。
4.5.2 挂载 SPI 设备
&spi1 {
status = "okay";
max-freq = <50000000>; // 50MHz
spi_flash: flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
m25p,fast-read;
};
};4.6 设备树编译与反编译
4.6.1 编译 DTS → DTB
# 方法一:使用内核编译系统
cd <SDK_PATH>/kernel
make ARCH=arm64 rockchip_linux_defconfig
make ARCH=arm64 dtbs
# 编译单个 DTB
make ARCH=arm64 rk3576-xxx.dtb
# 方法二:使用 dtc 直接编译
dtc -I dts -O dtb -o output.dtb input.dts4.6.2 反编译 DTB → DTS
# 反编译 DTB 为 DTS
dtc -I dtb -O dts -o output.dts input.dtb
# 反编译并保留符号
dtc -I dtb -O dts -@ -o output.dts input.dtb
# 查看 DTB 内容
fdtdump input.dtb4.7 设备树编译流程(RK3576 SDK)
RK3576 SDK 中,内核编译 DTB 的流程:
.dts源文件经过 C 预处理器处理(解析#include)dtc编译器将预处理后的 DTS 编译为.dtbscripts/mkimg --dtb工具将.dtb打包为dtb.img(resource.img)dtb.img与kernel.img一起打包进boot.img
4.8 Overlay 机制
4.8.1 原理
设备树 Overlay(覆盖)允许在系统运行时动态加载设备树片段,实现热插拔外设支持。Overlay 文件是 .dtbo 格式,它与基础 DTB 合并后生效。
4.8.2 编写 Overlay
// my-overlay.dts
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&i2c1>;
__overlay__ {
status = "okay";
clock-frequency = <100000>;
my_sensor: my-sensor@48 {
compatible = "my,company-sensor";
reg = <0x48>;
};
};
};
};4.8.3 编译 Overlay
# 编译 Overlay DTS → DTBO
dtc -I dts -O dtb -@ -o my-overlay.dtbo my-overlay.dts4.8.4 加载 Overlay
# 方法一:通过 configfs 加载
mkdir /sys/kernel/config/device-tree/overlays/my-overlay
cat my-overlay.dtbo > /sys/kernel/config/device-tree/overlays/my-overlay/dtbo
# 方法二:使用 U-Boot 加载(需 U-Boot 支持)
load mmc 1:1 ${fdt_addr_r} /overlays/my-overlay.dtbo
fdt apply ${fdt_addr_r}4.8.5 查看已加载的 Overlay
# 查看已加载的 Overlay
ls /sys/kernel/config/device-tree/overlays/
# 卸载 Overlay
rmdir /sys/kernel/config/device-tree/overlays/my-overlay4.9 SPL 设备树
RK3576 的 SPL(Secondary Program Loader)也使用设备树,用于 DDR 初始化、存储设备识别等早期启动阶段。SPL 设备树有特殊标记 u-boot,dm-spl 和 u-boot,dm-pre-reloc,只有带这些标记的节点才会被编译进 SPL 的 DTB。
// U-Boot 设备树中标记 SPL 使用的节点
&uart2 {
u-boot,dm-pre-reloc; // 在 SPL 阶段初始化
status = "okay";
};
&sdmmc0 {
u-boot,dm-spl; // 在 SPL 阶段使用
status = "okay";
};5. 代码详解
5.1 内核中设备树的解析
Linux 内核在启动早期通过 unflatten_device_tree() 将 DTB 解析为设备树结构体:
// 在 start_kernel() 中调用
setup_arch()
-> setup_machine_fdt(__fdt_pointer)
-> early_init_dt_scan()
-> of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line)
-> of_scan_flat_dt(early_init_dt_scan_root, NULL)
-> of_scan_flat_dt(early_init_dt_scan_memory, NULL)
-> unflatten_device_tree()5.2 驱动中获取设备树属性
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct gpio_desc *reset_gpio;
u32 val;
int ret;
// 读取整型属性
ret = of_property_read_u32(np, "my,property", &val);
if (ret) {
dev_err(dev, "failed to read my,property\n");
return ret;
}
// 读取 GPIO
reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
return PTR_ERR(reset_gpio);
// 读取中断
int irq = irq_of_parse_and_map(np, 0);
return 0;
}6. 常见问题
6.1 DTB 与内核版本不匹配
现象: 内核启动时提示 FDT: __unflatten_device_tree() failed 或设备无法工作
解决方案:
# 使用内核自带的 dtc 编译器
cd <SDK_PATH>/kernel
make ARCH=arm64 scripts
scripts/dtc/dtc --version6.2 设备树节点未生效
现象: 修改设备树后,驱动未加载或设备未出现
解决方案:
# 确认编译后的 DTB 包含修改
dtc -I dtb -O dts -o /tmp/check.dts boot.img.dtb
grep -A5 "my_device" /tmp/check.dts6.3 Pinmux 冲突
现象: 多个外设配置了相同的 GPIO 引脚,导致功能异常
解决方案:
- 检查硬件原理图,确认引脚分配不冲突
- 在设备树中确保每个 GPIO 引脚只被一个外设使用
- 使用
pinctrl-names区分不同状态下的 pinmux 配置
相关文档
- 内核配置与裁剪 — 内核配置
- Linux_SDK编译 — SDK 编译流程
- 核心板参数 — 硬件事实基线
- GPIO驱动与控制 — GPIO 开发
