一、设备树的核心作用
硬件抽象描述
用树状结构描述硬件拓扑,替代传统硬编码的board-*.c
文件。驱动与硬件解耦
同一内核可适配不同硬件,通过更换.dtb
文件实现兼容。资源分配与冲突避免
明确指定外设的寄存器地址、中断号、时钟等资源。动态配置支持
运行时加载不同设备树,无需重新编译内核。
二、实际案例解析
以下代码包含嵌入式系统中常见的硬件配置,包含了 CPU、内存、串口、GPIO 控制器等设备
/dts-v1/;
/ {
model = "Example Board";
compatible = "example,board", "generic-armv7";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait";
};
memory@0 {
device_type = "memory";
reg = <0x0 0x40000000>; /* 1GB memory starting at address 0x0 */
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
reg = <0>;
compatible = "arm,cortex-a9";
clock-frequency = <800000000>; /* 800 MHz */
};
};
serial@10000000 {
compatible = "arm,pl011";
reg = <0x10000000 0x1000>;
interrupts = <1 0>;
clocks = <&clocks 1>;
clock-names = "uartclk";
status = "okay";
};
gpio@20000000 {
compatible = "arm,pl061";
reg = <0x20000000 0x1000>;
interrupts = <2 0>;
gpio-controller;
#gpio-cells = <2>;
status = "okay";
};
clocks {
#address-cells = <1>;
#size-cells = <0>;
compatible = "simple-bus";
uartclk: clock@1 {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <1843200>;
};
};
};
代码解释
1. 文件头和根节点
/dts-v1/;
/ {
model = "Example Board";
compatible = "example,board", "generic-armv7";
#address-cells = <1>;
#size-cells = <1>;
/dts-v1/
:表示使用设备树版本 1。model
:指定设备的型号名称。compatible
:是一个字符串列表,用于告诉内核该设备与哪些设备兼容,内核可以根据这个属性来匹配合适的驱动程序。#address-cells
和#size-cells
:用于指定在reg
属性中地址和大小所占用的单元格数量。
2. chosen
节点
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait";
};
chosen
节点用于传递内核启动参数,bootargs
属性指定了内核启动时的命令行参数。
3. memory
节点
memory@0 {
device_type = "memory";
reg = <0x0 0x40000000>; /* 1GB memory starting at address 0x0 */
};
device_type
:指定设备类型为内存。reg
:指定内存的起始地址和大小,这里表示从地址0x0
开始的 1GB 内存。
4. cpus
节点和 cpu
子节点
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
reg = <0>;
compatible = "arm,cortex-a9";
clock-frequency = <800000000>; /* 800 MHz */
};
};
cpus
节点是 CPU 设备的容器节点。cpu@0
表示第一个 CPU,compatible
属性指定了 CPU 的型号,clock-frequency
指定了 CPU 的时钟频率。
5. serial
节点
serial@10000000 {
compatible = "arm,pl011";
reg = <0x10000000 0x1000>;
interrupts = <1 0>;
clocks = <&clocks 1>;
clock-names = "uartclk";
status = "okay";
};
compatible
:指定串口设备的兼容型号为arm,pl011
。reg
:指定串口设备的寄存器基地址和大小。interrupts
:指定串口设备的中断号。clocks
:指定串口设备使用的时钟源。status
:表示设备的状态,"okay"
表示设备可用。
6. gpio
节点
gpio@20000000 {
compatible = "arm,pl061";
reg = <0x20000000 0x1000>;
interrupts = <2 0>;
gpio-controller;
#gpio-cells = <2>;
status = "okay";
};
gpio-controller
:表示该设备是一个 GPIO 控制器。#gpio-cells
:指定在使用 GPIO 时需要的参数数量。
7. clocks
节点和 clock
子节点
clocks {
#address-cells = <1>;
#size-cells = <0>;
compatible = "simple-bus";
uartclk: clock@1 {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <1843200>;
};
};
clocks
节点是时钟设备的容器节点。uartclk
是一个固定频率的时钟源,clock-frequency
指定了时钟频率。
编译设备树文件
要将 .dts
文件编译成 .dtb
文件(设备树二进制文件),可以使用 dtc
(设备树编译器)工具,命令如下:
dtc -I dts -O dtb -o example.dtb example.dts
其中,-I dts
表示输入文件格式为 .dts
,-O dtb
表示输出文件格式为 .dtb
,-o example.dtb
指定输出文件名,example.dts
是输入的设备树源文件。
案例1:GPIO按键驱动
1.1 设备树定义
// 按键节点定义
gpio-keys {
compatible = "gpio-keys"; // 匹配内核驱动
#address-cells = <1>;
#size-cells = <0>;
button@1 {
label = "USER Button";
linux,code = <KEY_ENTER>; // 对应键盘的ENTER键
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>; // GPIO1_IO18,低电平触发
debounce-interval = <50>; // 消抖时间50ms
};
};
1.2 驱动中解析设备树
// 驱动代码片段
static int gpio_keys_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct gpio_keys_button *buttons;
struct gpio_keys_platform_data *pdata;
// 解析设备树中的按键数量
int nbuttons = of_get_child_count(dev->of_node);
// 分配内存存储按键配置
buttons = devm_kcalloc(dev, nbuttons, sizeof(*buttons), GFP_KERNEL);
// 遍历子节点
struct device_node *child;
int i = 0;
for_each_child_of_node(dev->of_node, child) {
u32 code;
of_property_read_u32(child, "linux,code", &code); // 读取键值
buttons[i].code = code;
// 获取GPIO编号
buttons[i].gpio = of_get_gpio(child, 0);
// 注册中断
irq = gpio_to_irq(buttons[i].gpio);
request_irq(irq, button_isr, IRQF_TRIGGER_FALLING, "gpio-key", NULL);
i++;
}
// 注册输入设备
input_register_device(input_dev);
}
案例2:I2C温度传感器(LM75)
2.1 设备树定义
&i2c1 {
clock-frequency = <100000>; // I2C速率100kHz
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
lm75@48 {
compatible = "nxp,lm75"; // 匹配内核驱动drivers/hwmon/lm75.c
reg = <0x48>; // I2C设备地址
vsupply = <®_3v3>; // 电源依赖
};
};
2.2 驱动中获取设备树参数
// 驱动代码片段
static int lm75_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct device_node *np = dev->of_node;
// 读取设备树中的电源配置
struct regulator *vcc;
vcc = devm_regulator_get(dev, "vsupply");
regulator_enable(vcc); // 开启传感器电源
// 获取I2C地址
u8 i2c_addr = client->addr; // 0x48
// 初始化传感器
i2c_smbus_write_byte_data(client, LM75_REG_CONF, 0x00);
}
案例3:内存映射型外设(UART)
3.1 设备树定义
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart"; // 匹配驱动drivers/tty/serial/imx.c
reg = <0x02020000 0x4000>; // 寄存器物理地址范围
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>; // 中断号
clocks = <&clks IMX6UL_CLK_UART1_IPG>, // 时钟依赖
<&clks IMX6UL_CLK_UART1_SERIAL>;
clock-names = "ipg", "per";
status = "okay";
};
3.2 驱动中映射寄存器
// 驱动代码片段
static int imx_uart_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
// 获取寄存器物理地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 映射到内核虚拟地址
base = devm_ioremap_resource(&pdev->dev, res);
// 配置波特率(从设备树或默认值)
u32 baudrate;
if (of_property_read_u32(pdev->dev.of_node, "current-speed", &baudrate))
baudrate = 115200;
// 写入寄存器
writel(baud_reg, base + UART_UBIR);
}
三、设备树调试技巧
查看设备树节点
# 树形结构查看 dtc -I fs /proc/device-tree # 查看特定属性值 hexdump -C /proc/device-tree/soc/i2c@021a0000/clock-frequency
2. 内核调试日志
dmesg | grep -i "of_" # 查看设备树解析过程
3. 验证驱动匹配
# 检查设备与驱动是否匹配成功 cat /sys/kernel/debug/devices_deferred