嵌入式Linux设备树开发实战:从DTS编写到驱动匹配全链路
设备树把硬件描述从内核代码中剥离出来,驱动不再关心寄存器地址写死在哪,只管从DTB里读配置。本文覆盖DTS语法、编译流程、驱动匹配机制、内核API调用、调试技巧,给出兼容Linux 6.8的完整代码示例。
一、为什么需要设备树
Linux 3.x之前,ARM平台的硬件信息全部硬编码在arch/arm/mach-xxx/目录的board文件里。每加一块板子,就要往内核里塞一个新文件,把GPIO、I2C、SPI的寄存器地址和中断号逐个写死。到Linux 3.7的时候,arch/arm/下堆积了400多个board文件,维护成本失控制
设备树解决的核心问题硬件描述与驱动代码分支。板级信息放在独立的DTS文件里,编译成DTB二进制由bootloader传给内核,同一份内核镜像可以适配不同硬件配置。
整个流程:开发者编写DTS 。dtc编译器生成DTB 。U-Boot加载DTB到内存并传给内核 。内核解析DTB生成device_node。。Platform总线用compatible字符串匹配驱动。调用probe函数。
二、设备树核心概念
2.1 节点和属。
设备树的基本组成单元节点(node)属性(property),结构类似文件系统目录:
/ { // 根节。 node1 { // 子节。 prop1 = "abc"; // 字符串属。 prop2 = <0x1>; // 整数属。 };
};
- 节点:用
{}包裹,描述一个硬件设备或逻辑分组,支持嵌。- *属:key = value形式,描述节点的具体参数
2.2 标准属。
| 属性 | 类型 | 用途 | 示例 |
|---|---|---|---|
compatible |
字符串列表 | 驱动匹配标识,最关键的属性 | "vendor,dev" |
reg |
整数数组 | 设备寄存器地址和长度 | <0x1000 0x100> |
#address-cells |
整数 | 子节点reg中地址占几个u32 | <1> |
#size-cells |
整数 | 子节点reg中长度占几个u32 | <1> |
interrupts |
整数数组 | 中断号和触发方式 | <0 29 4> |
clocks |
引用+整数 | 设备使用的时钟 | <&clk 1> |
status |
字符串 | 设备状态 | "okay" / "disabled" |
phandle |
整数 | 节点唯一标识,供其他节点引用 | <1> |
*compatible属是驱动匹配的核心。它是一个字符串列表,内核按从左到右的顺序尝试匹配:
compatible = "vendor,mydev", "generic,mydev-class";
驱动先尝试匹配"vendor,mydev",匹配不上再尝试"generic,mydev-class"。
*reg属的解读依赖父节点的#address-cells和#size-cells。
soc {
#address-cells = <1>; // 地址2个u32
#size-cells = <1>; // 长度2个u32
mydev@1000 {
reg = <0x1000 0x100>; // 地址0x1000,长度0x100
};
};
2.3 特殊节点
| 节点 | 用途 | 典型内容 |
|---|---|---|
/chosen |
启动参数 | bootargs、stdout-path |
/aliases |
节点别名 | serial0 = &uart0 |
/memory |
物理内存布局 | reg = <0x0 0x40000000> |
/cpus |
CPU拓扑 | 各CPU核的频率、缓存信息 |
2.4 phandle引用
当一个节点需要引用另一个节点时,用&符号加标签(label)实现:
clk: oscillator {
compatible = "fixed-clock";
clock-frequency = <24000000>;
};
uart0 {
clocks = <&clk>; // 引用oscillator节点
};
编译器自动为clk标签分配phandle值,<&clk>展开为<0x1>(phandle值)。
三、设备树语法详解
3.1 基本语法规则
/dts-v1/; // DTS版本声明,必须放在第一。
/ { // 根节。 // 这是注释
string-prop = "hello"; // 字符串 int-prop = <42>; // 32位整。 int-array = <1 2 3>; // 整数数组
string-list = "a", "b"; // 字符串列。 byte-array = [00 01 02]; // 字节数组
bool-prop; // 布尔属性(存在即为true。 ref-prop = <&label>; // phandle引用
};
3.2 数据类型
| 类型 | 语法 | 说明 |
|---|---|---|
| 字符串 | "text" |
以null结尾的字符串 |
| 32位整数 | <0x1000> |
大端序,尖括号包裹 |
| 字符串列表 | "a", "b" |
逗号分隔 |
| 字节数组 | [00 01 ff] |
两位十六进制 |
| 引用 | <&label> |
phandle引用 |
| 布尔 | prop-name; |
无值,存在即真 |
3.3 完整DTS示例
下面是一个包含GPIO、I2C、SPI设备的板级描述:
/dts-v1/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
model = "MyBoard Rev1.0";
compatible = "vendor,myboard", "vendor,myboard-family";
// 启动参数
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2";
stdout-path = &uart0;
};
// 物理内存。GB起始地址0x40000000
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x40000000>;
};
// CPU配置
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a53";
reg = <0>;
clock-frequency = <1200000000>;
};
};
// SoC内部总线
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
// GPIO控制。 gpio0: gpio@2000000 {
compatible = "vendor,gpio";
reg = <0x2000000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
// I2C控制。 i2c0: i2c@3000000 {
compatible = "vendor,i2c";
reg = <0x3000000 0x1000>;
interrupts = <0 25 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_bus>;
clock-frequency = <100000>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
// I2C从设备:温度传感。 temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
};
};
// SPI控制。 spi0: spi@4000000 {
compatible = "vendor,spi";
reg = <0x4000000 0x1000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_bus>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
// SPI从设备:显示例 display@0 {
compatible = "vendor,lcd";
reg = <0>;
spi-max-frequency = <10000000>;
reset-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
};
};
// UART
uart0: serial@5000000 {
compatible = "vendor,uart";
reg = <0x5000000 0x1000>;
interrupts = <0 29 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_bus>;
status = "okay";
};
};
// 固定时钟
clk_bus: clk-bus {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <200000000>;
};
// LED
leds {
compatible = "gpio-leds";
led-heartbeat {
label = "heartbeat";
gpios = <&gpio0 10 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
3.4 #include。dtsi文件组织
设备树文件分三层。
| 文件类型 | 后缀 | 用途 |
|:---------|:-----|:-----|
| DTS | .dts | 板级描述,包含具体硬件配置 |
| DTSI | .dtsi | SoC级公共定义,被DTS包含 |
| DTSI | .dtsi | 厂商级公共定义,被SoC DTSI包含 |
典型包含关系。
// board-v2.dts
/dts-v1/;
#include "soc-v1.dtsi" // SoC公共定义
#include <dt-bindings/gpio/gpio.h> // 内核头文件
/ {
model = "MyBoard V2";
// 覆盖DTSI中的默认配置
&uart0 {
status = "okay"; // DTSI中默认disabled,这里启。 };
};
&label语法可以在包含DTSI后,对已有节点进行追加或覆盖属性,这是板级定制的主要手段。
四、设备树编译流程
4.1 dtc编译。
dtc(Device Tree Compiler)是设备树的标准编译工具。
# DTS编译为DTB
dtc -I dts -O dtb -o board.dtb board.dts
# DTB反编译为DTS(调试常用)
dtc -I dtb -O dts -o board.dts board.dtb
4.2 Makefile自动编译
Linux内核源码中,设备树通过Makefile自动编译。
# arch/arm64/boot/dts/vendor/Makefile
dtb-$(CONFIG_ARCH_VENDOR) += board-v1.dtb
dtb-$(CONFIG_ARCH_VENDOR) += board-v2.dtb
编译命令。
# 编译所有设备树
make dtbs
# 只编译指定目录的设备。make vendor/board-v1.dtb
4.3 反编译运行时DTB
在运行的系统上,可以直接读取内核实际使用的DTB。
# 反编译当前系统的设备。dtc -I dtb -O dts /sys/firmware/fdt 2>/dev/null
# 或者从proc读取
cat /proc/device-tree/compatible
4.4 /sys/firmware/devicetree/查看
内核把解析后的设备树导出到sysfs,每个节点对应一个目录,每个属性对应一个文件:
# 查看根节点compatible属。cat /sys/firmware/devicetree/base/compatible
# 查看某个节点的reg属性(二进制,用hexdump看)
hexdump -C /sys/firmware/devicetree/base/soc/i2c@3000000/reg
# 列出某节点下的所有子节点
ls /sys/firmware/devicetree/base/soc/
# 查看属性列。ls /sys/firmware/devicetree/base/soc/i2c@3000000/
五、驱动与设备树匹配机。
5.1 compatible字符串匹。
驱动通过of_device_id结构体声明自己支持的设备,compatible字段必须与DTS中的完全一致(区分大小写)。
static const struct of_device_id mydev_of_match[] = {
{ .compatible = "vendor,mydev", },
{ .compatible = "vendor,mydev-v2", .data = &mydev_v2_data },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);
MODULE_DEVICE_TABLE(of, ...)宏的作用是让depmod工具在模块安装时生成modules.ofmap文件,这样modprobe可以根据设备树的compatible自动加载对应驱动模块。
5.2 匹配流程
匹配优先级:of_match_table > id_table > name。在设备树场景下,of_match是主要匹配方式,后两种是遗留兼容机制。
5.3 驱动注册代码
static int mydev_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
dev_info(dev, "probe: compatible matched\n");
// 后续从np读取设备树属。 return 0;
}
static int mydev_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "remove\n");
return 0;
}
static struct platform_driver mydev_driver = {
.probe = mydev_probe,
.remove = mydev_remove,
.driver = {
.name = "mydev",
.of_match_table = mydev_of_match,
},
};
module_platform_driver(mydev_driver);
六、驱动中获取设备树信息
内核提供一整套of_*函数,让驱动从设备树节点中读取配置。
6.1 常用API速查
| API | 功能 | 返回回调 |
|---|---|---|
of_find_property(np, "name", NULL) |
查找属性是否存在 | struct property * |
of_property_read_u32(np, "name", &val) |
读取32位整数 | 0成功,负数失败 |
of_property_read_string(np, "name", &str) |
读取字符串 | 0成功,负数失败 |
of_property_read_u32_array(np, "name", arr, len) |
读取整数数组 | 0成功,负数失败 |
of_get_named_gpio(np, "name-gpios", 0) |
获取GPIO编号 | GPIO号或负数错误 |
of_parse_phandle(np, "clocks", 0) |
解析phandle引用 | struct device_node * |
of_address_to_resource(np, 0, &res) |
地址转resource | 0成功 |
of_iomap(np, 0) |
映射寄存器地址 | void __iomem * |
of_property_count_u32_elems(np, "name") |
统计整数数组元素个数 | 元素数或负数错误 |
6.2 读取基本类型
struct device_node *np = pdev->dev.of_node;
u32 freq;
const char *name;
// 读取整数
if (of_property_read_u32(np, "clock-frequency", &freq))
freq = 100000; // 默认。
// 读取字符号if (of_property_read_string(np, "label", &name))
name = "unknown";
// 读取整数数组
u32 pins[4];
int count = of_property_count_u32_elems(np, "pins");
if (count > 0 && count <= 4)
of_property_read_u32_array(np, "pins", pins, count);
6.3 获取GPIO
// 方式一:of_get_named_gpio(旧API。.8仍可用)
int gpio = of_get_named_gpio(np, "reset-gpios", 0);
if (!gpio_is_valid(gpio)) {
dev_err(dev, "reset-gpios not found\n");
return -EINVAL;
}
// 方式二:gpiod_get(推荐,基于描述符)
struct gpio_desc *reset_gpiod;
reset_gpiod = gpiod_get(dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(reset_gpiod)) {
dev_err(dev, "failed to get reset gpio\n");
return PTR_ERR(reset_gpiod);
}
gpiod_get方式不需要写-gpios后缀,内核自动拼接。DTS中写reset-gpios = <&gpio0 5 0>,代码里传"reset"即可。
6.4 解析phandle引用
// 解析clocks引用
struct device_node *clk_np;
clk_np = of_parse_phandle(np, "clocks", 0);
if (clk_np) {
u32 rate;
of_property_read_u32(clk_np, "clock-frequency", &rate);
of_node_put(clk_np); // 用完必须释放
}
6.5 地址映射
// 方式一:of_iomap(一步完成地址查找和映射)
void __iomem *base;
base = of_iomap(np, 0);
if (!base) {
dev_err(dev, "failed to map registers\n");
return -ENOMEM;
}
// 方式二:of_address_to_resource + devm_ioremap_resource(推荐)
struct resource res;
if (of_address_to_resource(np, 0, &res))
return -EINVAL;
base = devm_ioremap_resource(dev, &res);
if (IS_ERR(base))
return PTR_ERR(base);
devm_ioremap_resource的优势是驱动卸载时自动解除映射,不需要手动调用iounmap。
6.6 完整代码示例
下面是一个完整的Platform驱动,从设备树读取配置并初始化设备:
// mydev_dt.c - 从设备树读取配置的Platform驱动(Linux 6.8。#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_address.h>
#include <linux/gpio/consumer.h>
#include <linux/io.h>
#include <linux/interrupt.h>
struct mydev_priv {
void __iomem *base;
struct gpio_desc *reset_gpio;
struct gpio_desc *irq_gpio;
int irq;
u32 clock_freq;
u32 buffer_size;
};
static irqreturn_t mydev_irq_handler(int irq, void *dev_id)
{
struct mydev_priv *priv = dev_id;
pr_info("mydev: irq triggered\n");
return IRQ_HANDLED;
}
static int mydev_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct mydev_priv *priv;
struct resource res;
int ret;
if (!np) {
dev_err(dev, "no device tree node\n");
return -EINVAL;
}
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
platform_set_drvdata(pdev, priv);
/* 1. 读取寄存器地址并映*/
ret = of_address_to_resource(np, 0, &res);
if (ret) {
dev_err(dev, "failed to get register resource\n");
return ret;
}
priv->base = devm_ioremap_resource(dev, &res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
/* 2. 获取GPIO(描述符方式*/
priv->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(priv->reset_gpio)) {
dev_err(dev, "failed to get reset gpio\n");
return PTR_ERR(priv->reset_gpio);
}
/* 3. 读取整数属性(带默认值) */
if (of_property_read_u32(np, "clock-frequency", &priv->clock_freq))
priv->clock_freq = 100000000; // 默认100MHz
if (of_property_read_u32(np, "buffer-size", &priv->buffer_size))
priv->buffer_size = 4096;
/* 4. 获取中断 */
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0) {
dev_err(dev, "failed to get irq\n");
return priv->irq;
}
ret = devm_request_irq(dev, priv->irq, mydev_irq_handler,
IRQF_TRIGGER_FALLING, "mydev-irq", priv);
if (ret) {
dev_err(dev, "failed to request irq\n");
return ret;
}
/* 5. 硬件复位 */
gpiod_set_value(priv->reset_gpio, 1);
msleep(10);
gpiod_set_value(priv->reset_gpio, 0);
msleep(50);
dev_info(dev, "probe ok: freq=%u buf=%u irq=%d\n",
priv->clock_freq, priv->buffer_size, priv->irq);
return 0;
}
static int mydev_remove(struct platform_device *pdev)
{
struct mydev_priv *priv = platform_get_drvdata(pdev);
gpiod_set_value(priv->reset_gpio, 1);
dev_info(&pdev->dev, "remove ok\n");
return 0;
}
static const struct of_device_id mydev_of_match[] = {
{ .compatible = "vendor,mydev", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);
static struct platform_driver mydev_driver = {
.probe = mydev_probe,
.remove = mydev_remove,
.driver = {
.name = "mydev-dt",
.of_match_table = mydev_of_match,
},
};
module_platform_driver(mydev_driver);
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Device Tree Platform Driver Demo");
MODULE_LICENSE("GPL");
对应的DTS节点。
mydev@6000000 {
compatible = "vendor,mydev";
reg = <0x6000000 0x1000>;
interrupts = <0 30 4>;
reset-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
clock-frequency = <200000000>;
buffer-size = <8192>;
};
七、设备树调试技。
7.1 dtc编译检查语。
# 编译时检查语法错。dtc -I dts -O dtb board.dts > /dev/null
# 带详细错误信息dtc -I dts -O dtb -o /dev/null board.dts 2>&1
7.2 /sys/firmware/devicetree/查看运行时设备树
# 查看compatible属。cat /sys/firmware/devicetree/base/compatible
# 查看节点是否存在
ls /sys/firmware/devicetree/base/mydev@6000000/
# 查看reg属性(二进制)
hexdump -C /sys/firmware/devicetree/base/mydev@6000000/reg
# 查看status属。cat /sys/firmware/devicetree/base/mydev@6000000/status
7.3 fdtput/fdtdump工具
# 修改DTB中的属性。fdtput -t s board.dtb /mydev status "okay"
# 读取DTB中的属性。fdtget board.dtb /mydev compatible
# 导出DTB为可读文件fdtdump board.dtb
7.4 内核启动参数指定DTB
# U-Boot中指定DTB文件
setenv fdt_addr_r 0x44000000
setenv fdtfile vendor/board-v1.dtb
booti ${kernel_addr_r} - ${fdt_addr_r}
# 或在extlinux.conf中指。fdt /boot/dtbs/vendor/board-v1.dtb
7.5 动态调试
# 开启设备树解析调试日志
echo 'file drivers/of/*.c +p' > /sys/kernel/debug/dynamic_debug/control
# 查看已注册的platform_device
ls /sys/bus/platform/devices/
# 查看驱动匹配状态cat /sys/bus/platform/drivers/mydev-dt/bind
八、常见问题
Q1:驱动probe不被调用。
*最常见原因:compatible不匹配置 检查以下几点:
- DTS中的
compatible和驱动of_match_table中的字符串是否完全一致(区分大小写) - DTS节点是否在正确的总线节点下(需要被
of_platform_populate遍历到) - 节点
status是否为"okay"("disabled"的节点不会创建device。4. 驱动模块是否已加载(lsmod | grep mydev。
# 快速排查:查看内核是否识别到设备树节点
ls /sys/firmware/devicetree/base/ | grep mydev
# 查看驱动是否注册
ls /sys/bus/platform/drivers/ | grep mydev
Q2:of_property_read返回EINVAL。
*原因:属性名或类型不对
- 属性名拼写错误——DTS里写的是
clock-frequency,代码里写成了clock_freq,下划线和横线不要搞。2. 类型不匹配——DTS中reg = <0x1000>是整数,用of_property_read_string读会返回EINVAL - 属性不存在——用
of_find_property先检查属性是否存。
// 安全的读取方。if (of_property_read_u32(np, "clock-frequency", &freq)) {
dev_warn(dev, "clock-frequency not found, using default\n");
freq = 100000000;
}
Q3:DTB修改后不生效。
*原因:bootloader加载的还是旧DTB
- 只修改DTS文件不够,必须重新编译成DTB
- 编译后的DTB必须更新到bootloader能找到的位置(boot分区或固件分区)
- 有些平台需要更新
extlinux.conf中的fdt路径 - U-Boot环境变量可能缓存了旧的fdt地址,需要重启或清除环境变量
# 正确流程
dtc -I dts -O dtb -o board.dtb board.dts
cp board.dtb /boot/dtbs/vendor/
sync
reboot
九、总结
设备树的本质就是一份硬件配置清单,它把"板子上有什么?。驱动怎么。拆开。写DTS描述硬件,写驱动读DTS,内核负责中间的匹配和传递。
核心工作流:编写DTS 。dtc编译 bootloader加载 。内核解析 。compatible匹配 。probe中用of_* API读配置。
API速查看
| 操作 | API | 备注 |
|---|---|---|
| 检查属性存 | of_find_property(np, "name", NULL) |
返回非NULL表示存在 |
| 32位整数 | of_property_read_u32(np, "name", &val) |
失败返回负数 |
| 读字符串 | of_property_read_string(np, "name", &str) |
失败返回负数 |
| 读整数数 | of_property_read_u32_array(np, "name", arr, len) |
需预知长度 |
| 统计数组元素 | of_property_count_u32_elems(np, "name") |
返回元素个数 |
| 获取GPIO | devm_gpiod_get(dev, "name", flags) |
推荐描述符方 |
| 解析phandle | of_parse_phandle(np, "name", index) |
用完需of_node_put |
| 地址转resource | of_address_to_resource(np, index, &res) |
搭配devm_ioremap_resource |
| 直接映射寄存 | of_iomap(np, index) |
需手动iounmap |
| 读布尔属 | of_property_read_bool(np, "name") |
存在返回true |
本文首发于linuxros.cn,转载请注明出处。