记得我第一次接触Linux驱动开发时,看到arch/arm目录下那些密密麻麻的板级文件简直头皮发麻。每个ARM开发板都要在mach-xxx目录下维护一套完整的硬件描述,这种开发方式存在三个致命问题:
代码冗余严重:同一个外设(比如I2C控制器)在不同板子上要重复定义
维护成本高:硬件稍有改动就要重新编译内核
可读性差:硬件信息散落在各种宏定义和初始化函数中
设备树的出现完美解决了这些问题。它就像一份标准化的硬件说明书,用节点和属性这种结构化语言来描述:
dts复制// 示例:一个典型的I2C控制器节点
i2c1: i2c@021a0000 {
compatible = "fsl,imx6ul-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
};
这个简单的节点告诉内核:
实际项目中,我遇到过因为寄存器范围定义错误导致驱动无法正常工作的情况。通过对比设备树中的reg属性和芯片手册,很快就能定位问题所在。
一个完整的设备树文件通常由三部分组成:
dts复制/dts-v1/; // 版本声明
#include "imx6ull.dtsi" // 包含SoC公共定义
/ {
model = "My Custom Board";
compatible = "mycompany,myboard";
// 硬件外设定义
i2c1: i2c@021a0000 {
status = "okay";
touchscreen@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>;
};
};
};
关键技巧:
.dtsi头文件存放SoC通用配置(如imx6ull.dtsi).dts中只定义板级特有配置#include实现层次化设计设备树中的属性就像表格里的字段,常见的有这些类型:
| 属性类型 | 示例 | 说明 |
|---|---|---|
| 字符串 | compatible = "fsl,imx6ul-i2c" |
驱动匹配的关键 |
| 32位无符号整数 | reg = <0x021a0000 0x4000> |
用尖括号包裹的十六进制数 |
| 字符串列表 | compatible = "a","b","c" |
多个驱动备选方案 |
| 二进制数据 | local-mac-address = [00 11 22] |
方括号包裹的字节序列 |
在调试GPIO控制器时,我发现phandle引用特别实用:
dts复制gpio5: gpio@020ac000 {
#gpio-cells = <2>;
};
sensor@0 {
interrupt-parent = <&gpio5>;
interrupts = <9 IRQ_TYPE_EDGE_FALLING>;
};
这种引用方式让硬件连接关系一目了然。
内核通过compatible属性来匹配驱动,这个匹配过程就像相亲:
设备树中的"自我介绍":
dts复制touchscreen@38 {
compatible = "edt,edt-ft5x06";
reg = <0x38>;
};
驱动中的"择偶标准":
c复制static const struct of_device_id ft5x06_of_match[] = {
{ .compatible = "edt,edt-ft5x06" },
{}
};
注册配对信息:
c复制static struct platform_driver ft5x06_driver = {
.driver = {
.of_match_table = ft5x06_of_match,
},
};
曾经有个坑:compatible字符串末尾多了空格,导致匹配失败。现在我会用printk打印出内核实际看到的字符串进行验证。
驱动中获取设备树数据的典型流程:
c复制static int probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
u32 reg;
// 1. 读取寄存器地址
of_property_read_u32(np, "reg", ®);
// 2. 获取中断号
int irq = platform_get_irq(pdev, 0);
// 3. 处理可选属性
if(of_property_read_bool(np, "wakeup-source")) {
device_init_wakeup(&pdev->dev, true);
}
}
对于GPIO配置,推荐使用新版的gpiodAPI:
c复制struct gpio_desc *reset_gpio;
reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
当设备树出现问题时,我常用的三板斧:
语法检查:
bash复制dtc -I dts -O dtb -o test.dtb test.dts
运行时查看:
bash复制ls /proc/device-tree/
cat /proc/device-tree/model
内核打印:
在start_kernel函数中dump设备树:
c复制early_init_dt_scan_nodes();
最近发现个实用技巧:在uboot中用fdt命令动态修改设备树:
code复制fdt set /i2c1/status "okay"
对于嵌入式设备,设备树优化能显著减少启动时间:
status = "disabled"关闭未使用的外设reg = <addr1 len1 addr2 len2>在某个项目中,通过精简设备树使启动时间缩短了200ms。关键是用time命令测量每个阶段的耗时:
code复制[ 0.123456] OF: parsing dtb took 45ms
设备树就像驱动开发的蓝图,掌握它就能让硬件乖乖听话。每次看到probe函数成功执行时,都觉得那些调试的夜晚值了。记住,好的设备树设计应该是:别人接手你的项目时,不看手册也能看懂硬件连接关系。