1. 设备树基础
大家好,我是你们的嵌入式系统讲师。今天咱们来聊聊设备树——这个在Linux内核开发中绕不开的话题。
说实话,我刚开始接触嵌入式Linux那会儿,设备树还没普及。那时候大家还在用板级文件,每个板子一个board-xxx.c,代码里写满了GPIO号、中断号、时钟频率。后来我参与一个项目,要同时支持三款硬件略有差异的板子,那叫一个痛苦——改一个外设,得同步修改三份文件,稍不留神就漏了。
嗯,设备树就是来解决这个问题的。
一句话总结:设备树是一种描述硬件资源的数据结构,它把硬件配置信息从内核源码中剥离出来,变成一份独立的、可读性强的描述文件。
1.1 什么是设备树
设备树,全称Device Tree,简称DT。它本质上是一个树形结构,用来描述硬件平台上的各种设备信息。
你想想看,一个嵌入式板子上有哪些东西?CPU、内存、I2C控制器、SPI控制器、GPIO、中断控制器、时钟源……这些硬件资源怎么连接、地址范围是多少、中断号是多少,以前都硬编码在C代码里。设备树把这些信息抽出来,写成一种类似JSON的文本格式。
举个例子,一个简单的设备树节点长这样:
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256MB RAM */
};
i2c0: i2c@44e0b000 {
compatible = "ti,omap4-i2c";
reg = <0x44e0b000 0x1000>;
interrupts = <70>;
status = "okay";
};
};
看到没?每个设备就是一个节点,节点里用属性来描述它的特征。compatible告诉内核用哪个驱动,reg描述地址和大小,interrupts指定中断号。清晰明了。
我个人习惯:写设备树时,先把硬件手册翻一遍,把外设的基地址、中断号、时钟关系列个表格,再动手写dts文件。这样不容易漏东西。
1.2 为什么需要设备树
这个问题我问过不少刚入行的朋友。他们的第一反应是:「以前没有设备树不也跑得好好的吗?」
对,但那是过去。咱们来看看没有设备树的日子是什么样的。
在Linux 2.6到3.x早期,ARM架构的板级支持代码是这样的:
static struct resource i2c_resources[] = {
{
.start = 0x44e0b000,
.end = 0x44e0bfff,
.flags = IORESOURCE_MEM,
},
{
.start = 70,
.end = 70,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device i2c_device = {
.name = "omap4-i2c",
.id = 0,
.num_resources = ARRAY_SIZE(i2c_resources),
.resource = i2c_resources,
};
每个板子都要写这么一堆。我记得有一次,客户换了一颗内存颗粒,容量从256MB变成512MB。就改一个内存大小,我得去翻board-xxx.c,找到meminfo结构体,改完重新编译整个内核。你说烦不烦?
设备树带来的好处,我总结了几点:
- 硬件描述与内核代码解耦——换硬件改dts文件就行,不用动C代码
- 同一内核镜像支持多款板子——启动时根据设备树动态适配
- 减少内核源码中的冗余代码——以前每个板子一个文件,现在一个dts搞定
- 社区统一标准——PowerPC、ARM、RISC-V都用同一套机制
我曾经踩过一个坑:设备树里reg属性的地址写错了,导致外设驱动申请内存资源失败。查了两天才发现是dts里少写了一个0。从那以后,我每次写完dts都会用dtc -I dts -O dtb反编译检查一遍地址值。
1.3 设备树在Linux启动流程中的角色
设备树不是只在编译时有用。它在系统启动过程中扮演着关键角色。咱们来走一遍启动流程,看看设备树在哪里发挥作用。
下面这张图是我画的设备树在启动流程中的位置:
流程其实不复杂。我拆开来讲:
- Bootloader加载DTB——U-Boot从存储介质(eMMC、SD卡、NAND Flash)读取
.dtb文件,放到内存某个地址。这一步我建议用fdt addr命令确认地址对不对。 - 传递DTB地址——U-Boot跳转到内核时,通过ARM的r2寄存器(或其他架构的对应机制)把DTB的物理地址传给内核。内核启动代码第一时间读取这个地址。
- 内核早期解析——内核调用
unflatten_device_tree(),把扁平的DTB二进制数据解析成一颗device_node树。这棵树会一直保留在内核内存中,供后续使用。 - 驱动匹配与初始化——内核遍历设备树,为每个
compatible属性匹配对应的驱动。匹配成功后调用驱动的probe()函数,驱动从设备树中读取reg、interrupts等资源,完成初始化。
我建议:调试启动问题时,在U-Boot里用fdt list命令查看设备树内容,确认它是否被正确加载。很多启动失败的原因就是DTB文件损坏或地址传错了。
这里有个关键点——设备树是「一次解析,多次使用」。内核在启动早期就把设备树解析好了,后面所有驱动都通过of_系列API(比如of_property_read_u32、of_iomap)来读取设备树信息。这样做的好处是:驱动代码里不需要硬编码任何硬件地址,全部从设备树动态获取。
举个例子,一个I2C驱动读取设备树资源的代码:
static int my_i2c_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct resource *res;
void __iomem *base;
int irq;
/* 从设备树读取内存资源 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(dev, res);
/* 从设备树读取中断号 */
irq = platform_get_irq(pdev, 0);
/* 从设备树读取自定义属性 */
u32 clock_freq;
of_property_read_u32(dev->of_node, "clock-frequency", &clock_freq);
/* ... 初始化硬件 ... */
return 0;
}
看到没?驱动里没有写死任何地址。换板子?换dts文件就行,驱动代码一行都不用改。这就是设备树的精髓。
我曾经遇到一个诡异的问题:某款板子在启动时内核崩溃,卡在of_platform_populate函数里。查了半天,发现是设备树里一个节点的reg属性长度写错了,导致内核访问了非法地址。所以,写设备树时一定要对照硬件手册,地址和长度一个字节都不能错。
好了,设备树的基础概念就讲到这里。总结一下:设备树就是硬件的「身份证」,它告诉内核「我有哪些设备、它们在哪里、怎么用」。没有它,内核就得靠一堆板级文件来猜,那效率太低了。
下一节咱们会深入设备树的语法和编译方法,到时候我会带大家手写一个完整的dts文件。嗯,敬请期待。
可可精品资料库