作为一名嵌入式Linux开发者,我深刻体会到设备树(Device Tree)在硬件描述中的重要性。设备树本质上是一种描述硬件资源的数据结构,它采用树状组织形式,将系统中的各种硬件设备及其连接关系清晰地呈现出来。
在早期的Linux内核中,硬件信息通常直接硬编码在内核源码中,导致内核臃肿且难以维护。设备树的引入彻底改变了这一局面,它实现了硬件描述与内核代码的分离,使得同一内核可以支持多种硬件平台。
设备树的工作原理可以类比为建筑蓝图:.dts文件就像设计师绘制的图纸,而.dtb则是经过编译后的施工图。内核在启动时读取.dtb文件,就像施工队按照图纸建造房屋一样配置硬件。
实际项目中,我建议将公共硬件描述抽取到.dtsi文件中,通过#include方式引用。例如IMX6ULL处理器的基本配置可以放在imx6ull.dtsi中,各开发板特定的配置再通过各自的.dts文件补充。
设备树生态系统包含几种关键文件类型:
在i.MX6ULL平台上,典型的文件结构如下:
code复制arch/arm/boot/dts/
├── imx6ull.dtsi # SoC基础定义
├── imx6ull-14x14-evk.dts # 评估板定义
└── imx6ull-myboard.dts # 自定义板定义
编译设备树需要配置好交叉编译环境。以i.MX6ULL为例,编译命令如下:
bash复制# 编译所有设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
# 编译特定设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx6ull-myboard.dtb
编译过程实际上经历了两个阶段:
在实际项目中,我通常会创建一个Makefile来自动化这个过程:
makefile复制DTB_FILES := $(patsubst %.dts,%.dtb,$(wildcard *.dts))
all: $(DTB_FILES)
%.dtb: %.dts
dtc -I dts -O dtb -o $@ $<
设备树节点的完整语法结构如下:
code复制[label:] node-name[@unit-address] {
[properties]
[child nodes]
};
以一个LED节点为例,我们分析其关键属性:
dts复制leds {
compatible = "gpio-leds";
status = "okay";
user_led {
label = "heartbeat";
gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "off";
};
};
关键属性解析:
compatible:驱动匹配的关键字,格式通常为"manufacturer,model"status:控制节点是否启用,"okay"或"disabled"reg:描述设备寄存器地址和大小#address-cells和#size-cells:指定子节点reg属性的地址和长度字段数量dts复制/ {
model = "My Board";
compatible = "my,board";
#address-cells = <1>;
#size-cells = <1>;
};
dts复制aliases {
serial0 = &uart1;
ethernet0 = &fec1;
};
dts复制memory {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
pinctrl子系统负责管理SoC的引脚复用和电气特性配置。在设备树中,我们需要为每个外设定义对应的引脚组。
以i.MX6ULL的UART1为例:
dts复制&iomuxc {
pinctrl_uart1: uart1grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b0
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b0
>;
};
};
配置值0x1b0b0分解说明:
在设备树中定义GPIO节点时,需要注意以下几点:
示例配置:
dts复制gpio-keys {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_buttons>;
button1 {
label = "USER BUTTON";
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
linux,code = <KEY_1>;
debounce-interval = <10>;
};
};
传统方式直接操作寄存器,虽然简单但存在严重问题:
c复制static int __init led_init(void)
{
void __iomem *base;
struct device_node *np;
np = of_find_node_by_path("/mydevice");
base = of_iomap(np, 0);
// 直接操作寄存器
writel(0x12345678, base + REG_CTRL);
return 0;
}
主要缺点:
现代Linux驱动应该采用Platform驱动模型,其核心结构包括:
典型实现框架:
c复制static const struct of_device_id mydrv_of_match[] = {
{ .compatible = "my,device" },
{ }
};
static struct platform_driver mydrv_driver = {
.probe = mydrv_probe,
.remove = mydrv_remove,
.driver = {
.name = "mydrv",
.of_match_table = mydrv_of_match,
},
};
module_platform_driver(mydrv_driver);
优势对比:
| 特性 | 传统方式 | Platform驱动 |
|---|---|---|
| 符合规范 | × | √ |
| 热插拔支持 | × | √ |
| 资源管理 | 手动 | 自动 |
| 代码复用性 | 低 | 高 |
| 维护成本 | 高 | 低 |
Linux GPIO子系统提供了一套完整的API:
c复制#include <linux/gpio/consumer.h>
struct gpio_desc *gpiod;
int value;
// 获取GPIO描述符
gpiod = gpiod_get(dev, "led", GPIOD_OUT_LOW);
// 设置输出电平
gpiod_set_value(gpiod, 1);
// 获取输入电平
value = gpiod_get_value(gpiod);
// 释放资源
gpiod_put(gpiod);
设备树中定义GPIO的推荐方式:
dts复制mydevice {
compatible = "my,device";
led-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>;
button-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;
};
驱动中获取GPIO的正确方式:
c复制static int mydrv_probe(struct platform_device *pdev)
{
struct gpio_desc *led, *button;
led = gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
button = gpiod_get(&pdev->dev, "button", GPIOD_IN);
// 使用gpiod_set_value()等API操作GPIO
}
内核通过compatible属性匹配驱动,匹配过程如下:
匹配表定义示例:
c复制static const struct of_device_id mydrv_ids[] = {
{ .compatible = "company,dev-v1" },
{ .compatible = "company,dev-v2" },
{ /* sentinel */ }
};
为保持向后兼容,建议采用以下命名约定:
code复制compatible = "company,dev-v3", "company,dev-v2", "company,dev";
驱动会按照顺序尝试匹配,先尝试最具体的版本,再尝试较旧的版本。
分析硬件连接:
编写设备树:
dts复制/ {
mydevice {
compatible = "my,device";
reg = <0x02000000 0x1000>;
interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_mydevice>;
};
};
验证设备树:
bash复制dtc -I dtb -O dts -o extracted.dts /proc/device-tree
创建驱动框架:
c复制static int mydrv_probe(struct platform_device *pdev)
{
// 获取设备树资源
// 初始化硬件
// 注册字符设备
return 0;
}
实现文件操作:
c复制static const struct file_operations mydrv_fops = {
.owner = THIS_MODULE,
.read = mydrv_read,
.write = mydrv_write,
.open = mydrv_open,
.release = mydrv_release,
};
添加Makefile配置:
makefile复制obj-$(CONFIG_MYDRV) += mydrv.o
典型部署命令:
bash复制# 加载设备树覆盖
echo mydevice.dtbo > /sys/kernel/config/device-tree/overlays/load
# 加载驱动模块
insmod mydrv.ko
# 验证设备节点
ls -l /dev/mydevice
驱动未加载:
资源获取失败:
GPIO工作异常:
设备树调试:
bash复制# 查看解析后的设备树
ls /proc/device-tree/
# 查看特定属性
hexdump -C /proc/device-tree/mydevice/reg
GPIO调试:
bash复制# 查看GPIO状态
cat /sys/kernel/debug/gpio
# 手动控制GPIO
echo 1 > /sys/class/gpio/gpio10/value
驱动调试:
c复制// 在驱动中添加调试打印
dev_dbg(&pdev->dev, "Probe called\n");
延迟初始化:
对于不立即需要的资源,可以使用probe_defer机制:
c复制if (!device_ready)
return -EPROBE_DEFER;
中断优化:
电源管理:
c复制static const struct dev_pm_ops mydrv_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(mydrv_suspend, mydrv_resume)
};
DMA缓存处理:
c复制void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
在多年的嵌入式开发实践中,我发现严格遵守Linux驱动模型虽然初期学习成本较高,但能显著提高代码质量和可维护性。特别是在产品需要长期维护和升级时,规范的驱动设计能节省大量调试时间。对于刚接触设备树的开发者,我建议从简单的GPIO驱动开始,逐步掌握pinctrl、中断、DMA等复杂功能的设计方法。