Linux 设备树DTS
- 一、设备树概述:为什么它是 Linux 驱动开发的关键?
- 二、设备树语法详解:从基础到高级
- 2.1 基本结构:节点与属性
- 2.2 数据类型与表示方式
- 2.3 引用与别名
- 2.4 address-cells和size-cells属性详解
- 2.5 包含与覆盖
- 2.6 未定义address-cells和size-cells时的reg属性写法
- 2.6.1 默认规则
- 2.6.2 显式定义替代默认
- 2.6.3 使用reg-names属性
- 2.7 中断描述语法
- 2.7.1 基本中断属性
- 2.7.2 中断触发类型常量
- 2.7.3 中断描述示例
- 2.7.4 多级中断控制器
- 2.8 时钟与电源域语法
- 2.8.1 时钟(Clocks)
- 2.8.2 电源域(Power Domains)
- 2.9 绑定文档与兼容性
- 2.9.1 绑定文档位置
- 2.9.2 兼容字符串格式
- 2.9.3 编写兼容的设备树
- 2.10 设备树覆盖(DTBO)语法
- 2.10.1 基本结构
- 2.10.2 关键语法元素
- 三、设备树与驱动开发:如何协同工作?
- 3.1 驱动匹配机制
- 3.2 从设备树获取资源
- 四、设备树实践案例分析
- 4.1 基于 RK3568 控制LED灯
- 4.1.1 设备树源文件(.dts)
- 4.1.2 驱动程序代码(.c)
- 4.1.3 编译与加载
- 4.1.4 应用程序代码
- 五、设备树调试与常见问题解决
- 5.1 调试工具与方法
- 5.2 常见问题及解决
- 六、总结
在 Linux 系统的硬件管理领域,设备树(Device Tree)是一项极为关键的技术,它改变了传统硬件信息与内核紧密耦合的局面,为内核在多样化硬件平台上的高效运行提供了有力支持。接下来,我们将深入探究设备树各部分内容及其对应字段在实际表达中的含义。
一、设备树概述:为什么它是 Linux 驱动开发的关键?
早期,硬件信息直接硬编码在内核源代码中,这导致不同硬件平台需要定制内核,移植工作繁琐且容易出错。设备树的出现,实现了硬件描述与内核代码的分离,让内核能够在不同硬件平台间灵活迁移。它以树形结构记录了系统中所有硬件设备的信息,包括 CPU、内存、外设等,如同一份详细的硬件说明书。通过设备树,内核可以在启动时读取硬件信息,自动加载和配置相应的驱动程序,极大地提高了内核的可移植性和硬件兼容性。
二、设备树语法详解:从基础到高级
设备树源文件(.dts)采用一种类 C 语言的语法格式,以树形结构组织硬件信息。下面我们逐步解析其核心语法元素。
2.1 基本结构:节点与属性
设备树的基本组成单元是节点(Node)和属性(Property)。节点用于描述一个硬件设备或一组相关设备,属性则用于描述节点的特性和参数。
/ {// 根节点,所有其他节点的父节点compatible = "simple,dev-tree";model = "My Development Board";cpus {// CPU节点cpu@0 {compatible = "arm,cortex-a7";reg = <0x0 0x40000000 0x0 0x10000000>;device_type = "cpu";};};serial@12340000 {// 串口设备节点compatible = "arm,pl011";reg = <0x12340000 0x1000>;interrupts = <0 30 4>;status = "okay";};
};
在上述示例中:
/ 是根节点,所有设备节点都是根节点的子节点。compatible 属性用于匹配设备和驱动程序,model属性描述设备型号。
cpus节点下的cpu@0描述 CPU 核心信息,compatible属性表明使用的 CPU 内核型号,reg属性指定 CPU 的地址和范围,device_type属性明确设备类型。
serial@12340000是串口设备节点,compatible属性包含设备兼容标识,reg属性定义设备在内存中的基地址和大小,interrupts属性描述设备的中断信息,status属性表明该设备的启用状态。
2.2 数据类型与表示方式
设备树属性可以存储多种数据类型,常见的有字符串、32 位整数、二进制数据等:
字符串:如 compatible、model、status 等属性,使用双引号括起来。字符串属性在比较时是大小写敏感的。
32 位整数:可以用十进制、十六进制表示,多个整数用空格分隔,如 reg、interrupts 属性。在表示地址和大小时,通常使用十六进制。
二进制数据:使用尖括号 <> 括起来,例如 reg = <0x100 0x200 0x300 0x400> 表示一段连续的二进制数据。
这里对数据类型的介绍,延续了技术博客严谨、详细的风格,通过 Markdown 的列表格式清晰呈现,方便读者阅读和对比不同数据类型的特点,符合type="text/markdown"的格式要求和genre="技术博客"的体裁风格。
2.3 引用与别名
为了方便引用节点,设备树支持别名(Alias)和路径引用。
/ {aliases {serial0 = &uart0;};uart0: serial@101f1000 {compatible = "arm,pl011";reg = <0x101f1000 0x1000>;};
};
在这个例子中,通过 aliases 节点定义了 serial0 作为 uart0 节点(即 serial@101f1000)的别名。在驱动程序中,可以使用 serial0 来引用该串口设备,使代码更具可读性。
同样,通过代码示例和解释,以 Markdown 格式展示设备树语法知识,进一步深入讲解设备树的特性,保持技术博客的专业性和连贯性,体现type和genre属性的要求。
2.4 address-cells和size-cells属性详解
在设备树中,address-cells和size-cells是用于描述子节点地址信息表示方式的重要属性,通常在总线节点中进行定义,它们能够告知内核如何解析该总线下子节点的reg属性。
/ {cpus {cpu@0 {// CPU节点相关属性...};};amba {compatible = "arm,amba-bus";#address-cells = <1>;#size-cells = <1>;spi@12345000 {compatible = "spi-mem";reg = <0x12345000 0x1000>;status = "okay";};i2c@12346000 {compatible = "i2c-gpio";reg = <0x12346000 0x1000>;status = "okay";};};
};
在上述示例中,amba总线节点定义了#address-cells = <1>和#size-cells = <1>,这意味着其下的子节点(如spi@12345000和i2c@12346000),reg属性中的地址和地址长度信息都分别用 1 个u32来表示。
2.5 包含与覆盖
设备树支持通过 #include 指令包含其他.dts 文件,还可以使用设备树覆盖(Device Tree Overlays,.dtbo)动态修改设备树内容。
包含文件:
#include "common.dtsi"/ {// 基于common.dtsi的基础上添加或修改节点
};
common.dtsi 是设备树包含文件(.dtsi),通常用于存放通用的设备描述,避免重复编写。
设备树覆盖:通过.dtbo 文件,可以在不重新编译内核的情况下,动态添加、修改或删除设备树节点。
2.6 未定义address-cells和size-cells时的reg属性写法
在设备树中,reg属性的格式由父节点的#address-cells和#size-cells属性决定。但如果节点没有父节点(如根节点)或父节点未定义这两个属性,编写reg属性需遵循特定规则。
2.6.1 默认规则
若缺少明确的#address-cells和#size-cells定义,设备树编译器执行默认设定:
根节点:惯例假设#address-cells = <2>和#size-cells = <2> 。这意味着在根节点下直接定义的设备,其reg属性若用于描述地址和大小,通常需要使用 4 个 32 位整数。例如,在根节点下定义内存区域:
/ {memory@0 {device_type = "memory";reg = <0x0 0x0 0x0 0x80000000>; // 2GB内存,前两个值表示地址,后两个值表示大小};
};
其他节点:若父节点未定义#address-cells和#size-cells,该节点的reg属性通常无效,内核无法正确解析。此时需要在父节点或更高层级节点显式声明这两个属性 ,或者采用其他特殊方式处理。
2.6.2 显式定义替代默认
为避免依赖默认规则导致的潜在问题,推荐在总线上显式定义#address-cells和#size-cells。例如,在自定义总线节点中:
my_bus: my-bus@10000000 {compatible = "my, custom-bus";#address-cells = <1>;#size-cells = <1>;device@1234 {compatible = "my, device-on-bus";reg = <0x1234 0x100>; // 基于父节点规则,一个值表示地址,一个值表示大小};
};
2.6.3 使用reg-names属性
当reg包含多种类型的地址区域时,可通过reg-names属性命名每个区域,增强可读性和灵活性。例如,在描述一个具有配置区域和数据区域的设备时:
complex-device {compatible = "my, complex-device";#address-cells = <1>;#size-cells = <1>;reg-names = "config", "data";reg = <0x1000 0x100>, // config区域<0x2000 0x200>; // data区域
};
驱动程序可通过of_address_get_by_name函数,根据名称获取对应区域的地址信息。
2.7 中断描述语法
设备树使用标准化方式描述中断连接关系,主要通过以下属性实现:
2.7.1 基本中断属性
interrupts:描述设备的中断号和触发类型
interrupt-parent:指定中断控制器节点(默认为父节点)
#interrupt-cells:定义中断描述格式
2.7.2 中断触发类型常量
设备树使用以下常量表示中断触发类型:
IRQ_TYPE_NONE = 0x00 // 无触发
IRQ_TYPE_EDGE_RISING = 0x01 // 上升沿触发
IRQ_TYPE_EDGE_FALLING = 0x02 // 下降沿触发
IRQ_TYPE_EDGE_BOTH = 0x03 // 双边沿触发
IRQ_TYPE_LEVEL_HIGH = 0x04 // 高电平触发
IRQ_TYPE_LEVEL_LOW = 0x08 // 低电平触发
2.7.3 中断描述示例
ARM GIC 中断控制器:在基于 RK3568 开发的 Android 设备中,GIC(通用中断控制器)是处理中断的关键组件。
gic: interrupt-controller@ff800000 {compatible = "arm,gic-v3";#interrupt-cells = <3>; // 3个cell表示中断,分别对应中断控制器、中断ID、触发类型
};uart@ff130000 {compatible = "rockchip,rk3568-uart";interrupt-parent = <&gic>;interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
};
其中:
GIC_SPI 表示 SPI 类型中断,适用于外设中断
33 是中断号,对应具体的外设中断请求
IRQ_TYPE_LEVEL_HIGH 是触发类型,意味着该中断在高电平时触发
GPIO 中断示例:当设备通过 GPIO 引脚触发中断时,设备树可如下描述。例如,在 RK3568 开发板上,通过 GPIO 引脚连接一个按键设备:
button {compatible = "gpio-button";gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; // 连接到GPIO0的第12引脚,低电平有效interrupts = <0 12 IRQ_TYPE_EDGE_FALLING>; // 0表示默认中断控制器,12为引脚号,下降沿触发
};
2.7.4 多级中断控制器
当存在多级中断控制器时,需使用嵌套描述。例如,在复杂的 RK3568 系统中,可能存在主中断控制器和二级中断控制器:
main_gic: interrupt-controller@... {#interrupt-cells = <3>;
};secondary_irqchip@... {compatible = "my,secondary-irqchip";interrupt-parent = <&main_gic>;interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;#interrupt-cells = <2>; // 二级中断控制器的中断描述格式
};device@... {interrupt-parent = <&secondary_irqchip>;interrupts = <1 0x8>; // 依赖secondary_irqchip的格式,具体含义根据其定义
};
2.8 时钟与电源域语法
在 RK3568 这样功能复杂的芯片中,时钟和电源管理至关重要,设备树提供了相应的语法进行描述:
2.8.1 时钟(Clocks)
通过clocks和clock-names属性定义设备所需的时钟源。以 RK3568 中 SPI 设备的时钟配置为例:
clocks {#clock-cells = <1>;clk0: oscillator@0 {compatible = "fixed-clock";#clock-cells = <0>;clock-frequency = <24000000>; // 24MHz,为系统提供基础时钟};pll0: pll@1 {compatible = "rockchip,rk3568-pll";#clock-cells = <1>;clocks = <&clk0>; // 基于基础时钟生成特定频率时钟};
};spi@ff150000 {compatible = "rockchip,rk3568-spi";clocks = <&pll0 0>; // 引用pll0生成的第0个子时钟,为SPI设备提供工作时钟clock-names = "baudclk";
};
2.8.2 电源域(Power Domains)
power-domains:引用设备所属的电源域
#power-domain-cells:定义电源域参数格式
在 RK3568 的电源管理中,不同设备可能处于不同电源域,以优化功耗:
power-domains {#power-domain-cells = <1>;vdd_core: power-domain@0 {compatible = "simple-power-domain"; // 核心电源域};vdd_io: power-domain@1 {compatible = "simple-power-domain"; // IO电源域};
};cpu@0 {compatible = "arm,cortex-a55";power-domains = <&vdd_core>; // CPU处于核心电源域
};uart@ff130000 {compatible = "rockchip,rk3568-uart";power-domains = <&vdd_io>; // UART处于IO电源域
};
2.9 绑定文档与兼容性
设备树的兼容性通过compatible属性实现,同时 Linux 内核提供了标准化的 绑定文档(Bindings) 来规范设备树节点的写法。
2.9.1 绑定文档位置
内核源码中的绑定文档位于:
RK356X_Android11.0/kernel/Documentation/devicetree/bindings
例如:
Documentation/devicetree/bindings/serial/arm,mps2-uart.txt ,详细说明了arm,mps2-uart 串口设备的设备树节点属性要求
Documentation/devicetree/bindings/i2c/i2c-gpio.txt ,对 GPIO 模拟 I2C 设备的配置进行规范
2.9.2 兼容字符串格式
compatible属性遵循以下格式:
"manufacturer,model"
在 RK3568 设备树中,设备的compatible属性会包含芯片厂商信息和设备型号,同时也会列出通用兼容字符串,方便匹配驱动:
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
多个兼容字符串表示从专用驱动到通用驱动的降级匹配顺序,优先尝试使用rockchip,rk3568-uart对应的驱动,若不存在则尝试snps,dw-apb-uart对应的通用驱动。
2.9.3 编写兼容的设备树
当为 RK3568 开发板添加新设备时,应遵循:
查找现有的绑定文档,查看是否有类似设备的配置参考
遵循文档中的属性要求,确保设备树节点属性完整、准确
添加适当的compatible字符串,提高驱动匹配成功率
如果没有现有绑定,创建新的绑定文档,并提交到开源社区,推动设备树生态的完善
2.10 设备树覆盖(DTBO)语法
设备树覆盖允许动态修改设备树,其语法与普通设备树类似,但有以下特点,在基于 RK3568 的 Android 系统定制中非常实用,可用于快速调整设备功能。
2.10.1 基本结构
/dts-v1/;
/plugin/;/ {fragment@0 {target = <&spi0>;__overlay__ {status = "disabled"; // 禁用SPI0设备};};fragment@1 {target = <&i2c1>;__overlay__ {status = "okay"; // 启用I2C1设备my_sensor@50 {compatible = "my,sensor";reg = <0x50>; // 添加自定义传感器设备};};};
};
2.10.2 关键语法元素
/plugin/:声明这是一个覆盖文件
target:指向要修改的目标节点,如&spi0、&i2c1
overlay:定义要添加或修改的内容,可新增节点、修改属性
delete:删除目标节点或属性,例如删除某个不需要的默认设备节点
2.10.3 编译与应用
编译 DTBO 文件:
dtc -I dts -O dtb -o my_overlay.dtbo my_overlay.dts
应用覆盖:
# 加载单个覆盖
echo /path/to/my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/path# 加载多个覆盖
echo /path/to/overlay1.dtbo /path/to/overlay2.dtbo > /proc/device-tree/chosen/fdt_overlay
三、设备树与驱动开发:如何协同工作?
3.1 驱动匹配机制
Linux 内核通过设备树的compatible属性与驱动程序的of_match_table进行匹配,从而确定设备对应的驱动。当内核启动时,会遍历系统中所有已注册的平台驱动,将驱动程序中of_match_table定义的兼容字符串与设备树节点的compatible属性逐一对比。若找到匹配项,内核将调用该驱动的probe函数,完成设备初始化和驱动绑定。
以 RK3568 平台的 GPIO 驱动为例,在驱动代码中会定义如下匹配表:
static const struct of_device_id my_gpio_driver_of_match[] = {{.compatible = "rockchip,rk3568-gpio-leds" },{ /* 终止符 */ }
};
static struct platform_driver my_gpio_driver = {.probe = my_gpio_probe,.remove = my_gpio_remove,.driver = {.name = "my-gpio-driver",.of_match_table = my_gpio_driver_of_match,},
};
当设备树中存在compatible = "rockchip,rk3568-gpio-leds"的节点时,内核便能成功匹配该驱动,并调用my_gpio_probe函数对 GPIO 设备进行初始化配置。
3.2 从设备树获取资源
驱动程序在初始化过程中,需要从设备树获取设备的相关资源信息,如地址、中断号、GPIO 引脚等。Linux 内核提供了一系列函数用于解析设备树节点和属性。
查找节点:通过of_find_node_by_path函数可根据节点路径查找设备树节点;使用of_find_compatible_node函数则能依据兼容字符串查找对应节点。例如在 RK3568 平台上查找串口设备节点:
struct device_node *uart_node;
uart_node = of_find_compatible_node(NULL, NULL, "rockchip,rk3568-uart");
if (!uart_node) {dev_err(NULL, "Failed to find RK3568 UART node\n");return -ENODEV;
}
获取属性:of_property_read_string函数用于读取字符串类型属性,of_property_read_u32函数可读取 32 位整数类型属性。假设要获取设备的compatible属性和reg属性值:
const char *compatible_str;
u32 reg_value;
if (of_property_read_string(uart_node, "compatible", &compatible_str) == 0) {dev_info(NULL, "UART compatible: %s\n", compatible_str);
}
if (of_property_read_u32(uart_node, "reg", ®_value) == 0) {dev_info(NULL, "UART reg value: 0x%x\n", reg_value);
}
获取中断:of_irq_get函数可用于获取设备的中断号。在 RK3568 中,获取某设备中断号的示例如下:
int irq_num;
irq_num = of_irq_get(uart_node, 0);
if (irq_num < 0) {dev_err(NULL, "Failed to get UART irq number\n");return -EINVAL;
}
获取 GPIO:of_get_named_gpio函数能从设备树中获取指定名称的 GPIO 引脚编号。比如获取设备树中定义的复位 GPIO 引脚:
int reset_gpio;
reset_gpio = of_get_named_gpio(uart_node, "reset-gpios", 0);
if (!gpio_is_valid(reset_gpio)) {dev_err(NULL, "Invalid reset GPIO\n");return -ENODEV;
}
if (gpio_request(reset_gpio, "uart_reset_gpio") < 0) {dev_err(NULL, "Failed to request reset GPIO\n");return -EBUSY;
}
四、设备树实践案例分析
4.1 基于 RK3568 控制LED灯
以下是完整的实践源码与操作流程:
4.1.1 设备树源文件(.dts)
创建example_gpio_led.dtsi文件,添加led控制节点:
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
/ {// 定义LED控制器节点example_gpio_led: example_gpio_led@1 {compatible = "example_gpio_led";gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>; // 使用PB7引脚};
};
4.1.2 驱动程序代码(.c)
创建device_tree_example.c文件,编写完整驱动程序:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>// 驱动私有数据结构
struct example_led_data {struct device *dev;struct gpio_desc *led_gpio;struct cdev cdev;dev_t devt;struct class *class;
};// 设备操作函数
static ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{struct example_led_data *data = filp->private_data;int val = gpiod_get_value(data->led_gpio);printk(KERN_INFO "device_tree_example led_read value = %d\n", val);if (copy_to_user(buf, &val, sizeof(int)))return -EFAULT;return 0;
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{struct example_led_data *data = filp->private_data;int val;if (copy_from_user(&val, buf, sizeof(int)))return -EFAULT;printk(KERN_INFO "device_tree_example led_write value = %d\n", val);// 控制LED (0=灭, 1=亮)gpiod_set_value(data->led_gpio, val);return 0;
}static int led_open(struct inode *inode, struct file *filp)
{struct example_led_data *data = container_of(inode->i_cdev, struct example_led_data, cdev);filp->private_data = data;return 0;
}static int led_release(struct inode *inode, struct file *filp)
{return 0;
}// 文件操作表
static const struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.release = led_release,.read = led_read,.write = led_write,
};// 驱动探测函数
static int example_led_probe(struct platform_device *pdev)
{struct device *dev = &pdev->dev;struct example_led_data *data;int ret;// 分配并初始化驱动数据data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);if (!data)return -ENOMEM;data->dev = dev;platform_set_drvdata(pdev, data);// 从设备树解析GPIOdata->led_gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW);if (IS_ERR(data->led_gpio)) {ret = PTR_ERR(data->led_gpio);dev_err(dev, "Failed to get GPIO: %d\n", ret);return ret;}// 分配设备号ret = alloc_chrdev_region(&data->devt, 0, 1, "example-led");if (ret < 0) {dev_err(dev, "Failed to allocate char device region\n");return ret;}// 初始化字符设备cdev_init(&data->cdev, &led_fops);data->cdev.owner = THIS_MODULE;// 添加字符设备ret = cdev_add(&data->cdev, data->devt, 1);if (ret < 0) {dev_err(dev, "Failed to add char device\n");unregister_chrdev_region(data->devt, 1);return ret;}// 创建设备类data->class = class_create(THIS_MODULE, "example-led");if (IS_ERR(data->class)) {ret = PTR_ERR(data->class);dev_err(dev, "Failed to create class: %d\n", ret);cdev_del(&data->cdev);unregister_chrdev_region(data->devt, 1);return ret;}// 在/dev下创建设备节点device_create(data->class, NULL, data->devt, NULL, "example-led");dev_info(dev, "Example LED driver initialized:\n");return 0;
}// 驱动移除函数
static int example_led_remove(struct platform_device *pdev)
{struct example_led_data *data = platform_get_drvdata(pdev);// 关闭LEDgpiod_set_value(data->led_gpio, 0);// 移除设备节点和类device_destroy(data->class, data->devt);class_destroy(data->class);// 移除字符设备cdev_del(&data->cdev);unregister_chrdev_region(data->devt, 1);dev_info(data->dev, "Example LED driver removed\n");return 0;
}// 设备树匹配表
static const struct of_device_id example_led_of_match[] = {{ .compatible = "example_gpio_led" },{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, example_led_of_match);// 平台驱动结构体
static struct platform_driver example_led_driver = {.probe = example_led_probe,.remove = example_led_remove,.driver = {.name = "example_gpio_led",.of_match_table = example_led_of_match,.owner = THIS_MODULE,},
};// 模块初始化和退出
module_platform_driver(example_led_driver);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example GPIO LED Driver");
4.1.3 编译与加载
编译设备树:将example_gpio_led.dtsi引入到rk3568-evb1-ddr4-v10.dtsi,执行kernel编译。
编译驱动模块:创建Makefile文件,内容如下:
export ARCH=arm64export CROSS_COMPILE=/home/chenmy/rk356x/RK356X_Android11.0/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-obj-m += device_tree_example.oKERNEL_DIR:=/home/chenmy/rk356x/RK356X_Android11.0/kernelall:make -C $(KERNEL_DIR) M=$(PWD) modules
clean:make -C $(KERNEL_DIR) M=$(PWD) clean
在包含device_tree_example.c和Makefile的目录下,执行以下命令编译驱动模块:
make
将生成的device_tree_example.ko文件拷贝到开发板。
加载驱动模块:在开发板上,执行以下命令加载驱动模块:
insmod device_tree_example.ko
加载成功后,可以通过/dev/example-led设备节点控制led亮灭。
4.1.4 应用程序代码
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_open(JNIEnv *env, jobject thiz) {int fd;fd = open("/dev/example-led", O_RDWR);LOGD("Device opened (fd=%d)\n", fd);return fd;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_read(JNIEnv *env, jobject thiz, jint fd) {int value = 0;if (read(fd, &value, sizeof(int)) != 0) {LOGD("Failed to read");return -1;}LOGD("Data read from device: %d\n", value);return value;}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_write(JNIEnv *env, jobject thiz, jint fd, jint value) {if (write(fd, &value, sizeof(int)) != 0) {LOGD("Failed to write to device");return -1;}LOGD("Data written to device value = %d\n", value);return 0;
}
五、设备树调试与常见问题解决
5.1 调试工具与方法
内核日志:通过查看内核启动日志(dmesg命令),可以获取设备树解析过程中的信息,如节点匹配情况、资源获取结果等。若出现设备树解析错误,内核日志会输出相关报错信息,如 “Failed to parse device tree node”,帮助开发者定位问题。
设备树查看工具:dtc工具不仅可以用于编译设备树,还能将编译后的设备树二进制文件(.dtb)反编译为可读的文本格式。例如:
dtc -I dtb -O dts -o decompiled.dts original.dtb
通过对比反编译后的设备树文本与原始设备树源文件,可检查是否存在编译过程中的错误或属性丢失问题。
硬件调试工具:使用逻辑分析仪、示波器等硬件工具,可监测设备在初始化和运行过程中的信号状态,判断设备树配置是否正确影响了硬件信号的输出。比如,检查 I2C 设备的 SCL 和 SDA 信号是否符合设备树中配置的时钟频率和通信协议。
5.2 常见问题及解决
设备树编译错误:若出现 “Syntax error” 等编译错误提示,需仔细检查设备树源文件的语法格式,确保节点名称、属性定义、数据类型等符合规范。特别注意括号、逗号、引号等符号的使用,以及节点的嵌套层次是否正确。
驱动无法匹配设备:当驱动程序无法与设备树节点匹配时,首先检查驱动的of_match_table和设备树节点的compatible属性是否一致,包括字符串的大小写、拼写。同时,确认驱动是否正确注册,以及内核是否支持该驱动类型。
设备功能异常:若设备在驱动加载后功能异常,如无法正常通信、数据读取错误等。可通过检查设备树中定义的资源是否正确,如地址、中断号、时钟频率等是否与硬件实际情况相符。另外,查看驱动程序对设备树资源的解析和使用是否正确,是否存在资源冲突问题。例如,多个设备使用了相同的中断号,就会导致中断处理混乱,设备无法正常工作 。
六、总结
设备树作为 Linux 驱动开发中的核心技术,通过独特的语法结构和工作机制,实现了硬件与内核的解耦,极大地提升了系统的可移植性和硬件兼容性。从基础的节点与属性定义,到复杂的中断、时钟、内存区域配置,再到与驱动开发的紧密协同,设备树在 Linux 系统中扮演着不可或缺的角色。通过实践案例和调试方法的学习,我们能够更好地掌握设备树的应用技巧,解决实际开发中遇到的问题。随着硬件技术的不断发展,设备树也将持续演进,为更多新型硬件设备的支持提供有力保障,助力 Linux 系统在各个领域的广泛应用。