嵌入式Linux开发——设备树简介 - 给硬件画张像

0 阅读9分钟

嵌入式Linux开发——设备树简介 - 给硬件画张像

仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!

仓库地址:github.com/Awesome-Emb…

静态网页:awesome-embedded-learning-studio.github.io/imx-forge/

前言:我们为什么要折腾这个

第一次接触设备树的时候,我其实颇为恐惧(真的)。看着那些稀奇古怪的节点名、属性值,还有满屏的花括号,完全不知道这东西跟驱动开发有什么关系。这种感觉就像是你第一次走进一家全是机械零件的工厂——齿轮太多,反而不知道该从哪个开始转。

但后来我发现,设备树其实没那么可怕。它的核心思想非常简单:把硬件描述从代码里剥离出来。只要理解了这一点,后面的事情就顺理成章了。

现在我们坐下来,好好聊聊设备树到底是什么,以及为什么它是你在嵌入式 Linux 开发路上绕不过去的一道坎。

Linus 的愤怒:ARM 内核源码为什么那么大

事情得从 2011 年说起。那时候 ARM Linux 的发展遇到了一个尴尬的问题:内核源码变得越来越大,大到让 Linus Torvalds 都忍不住爆了粗口。

"This whole ARM thing is a f*cking pain in the ass." (这该死的 ARM 事儿真是一团糟。)

不是开玩笑,而是 Linus 在邮件列表里真的说出来的(可以查一下,太出名了)。那么问题来了,为什么 ARM 会让他这么头疼?

答案在于一个叫做"板级信息硬编码"的历史遗留问题。

在 3.x 版本之前的 Linux 内核里,ARM 架构处理硬件描述的方式非常直接——直接写在 C 代码里。内核里充斥着大量的 arch/arm/mach-xxxarch/arm/plat-xxx 文件夹,里面全是描述板级信息的 .c.h 文件。

我们来想象一个具体的场景。假设你手里有一块开发板,板子上有 CPU,有内存,有挂在 I2C 总线上的触摸屏和 EEPROM,还有 SPI 接口上的 Flash 芯片。作为开发者,你需要告诉 Linux 内核这些硬件都存在、它们接在哪条总线上、地址是多少。

在旧的做法里,你得写一个 C 文件来描述这块板子。比如针对 SMDK2440 开发板,内核里就有这样一个文件:

/* arch/arm/mach-smdk2440.c (片段) */
static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
    .lcdcon5 = S3C2410_LCDCON5_FRM565 |
              S3C2410_LCDCON5_INVVLINE |
              S3C2410_LCDCON5_INVVFRAME |
              S3C2410_LCDCON5_PWREN |
              S3C2410_LCDCON5_HWSWP,
    /* ... 其他配置 ... */
};

static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
    .displays        = &smdk2440_lcd_cfg,
    .num_displays    = 1,
    .default_display = 0,
    /* ... 其他配置 ... */
};

static struct platform_device *smdk2440_devices[] __initdata = {
    &s3c_device_ohci,
    &s3c_device_lcd,
    &s3c_device_wdt,
    &s3c_device_i2c0,
    &s3c_device_iis,
};

你看,上面这段代码只是在描述一块板子上的 LCD 配置和设备列表。这看起来很合理,直到你意识到:这仅仅是使用 2440 芯片的 SMDK2440一种 板子。

使用 2440 芯片的板子有成百上千种,每换一块板子,哪怕只是改了一个电阻的连接,理论上你就需要修改内核源码,重新编译。随着 ARM 芯片爆发式增长,Linux 内核源码库里迅速塞满了这种"一次性"的板级代码。

这种做法让内核变得极其臃肿。就像你花大价钱去吃海鲜自助,结果进去发现全是炒面和凉菜,你肯定想骂人。Linus 看到 ARM 社区向主线内核提交了海量的、重复的板级描述文件时,他的反应和你差不多。

这次"震怒"改变了 ARM Linux 的历史。ARM 社区被迫引入了 PowerPC 等架构早已采用的技术:设备树

硬编码时代的问题:每块板子都要写专门的 C 代码

我们现在来仔细拆解一下,为什么硬编码的方式这么不可持续。

首先,代码重复是一个巨大的问题。同一个 SOC 芯片会被用在几十种不同的板子上,每种板子都要写一份几乎相同的 C 代码来描述这个 SOC 本身的硬件信息——比如它有几个 UART、每个 UART 的寄存器基地址是什么、GPIO 控制器的地址在哪里。这些信息是芯片固有的,不应该因为板子不同而改变,但旧的做法让你每次都要复制一份。

其次,维护成本高得离谱。假设你发现某个驱动的板级初始化代码写错了,你需要修复所有使用这个驱动的板子的代码。如果有上百块板子,你就得改上百个文件。而且这些文件散落在不同的 mach-xxx 目录下,很难保证都改对了。

更糟糕的是,每次你想换一块板子测试,都得重新编译整个内核。你不能只编译板级描述部分,因为它和其他内核代码纠缠在一起。这在开发调试阶段简直是个灾难。

还有一个容易被忽视的问题:代码和配置混在一起,让代码审查变得困难。当你看到一段内核代码时,你很难分辨哪些是通用的驱动逻辑,哪些是某块板子特有的配置。这种混乱会让新手望而却步——我当初就是看到满屏的板级代码,完全不知道该从哪里下手。

设备树的本质:一张"硬件说明书"

设备树的引入,本质上是一次"代码与配置分离"的设计革命。

我们可以把设备树理解为一张"硬件说明书"。以前,内核开发工程师像是一个不仅要会开车,还得自己造车的司机——每换一辆车,都要把车的结构参数写进引擎控制代码里。有了设备树之后,事情变了。我们把所有硬件描述从 C 代码里剥离出来,写在一个独立的文件里。这个文件就是 DTS (Device Tree Source)

它的结构是一棵树。这棵树的主干是系统总线,树枝上挂着各种控制器(I2C、SPI、GPIO),树叶则是具体的外设芯片(比如 I2C 总线下挂着的 FT5206 触摸屏、AT24C02 存储芯片)。

但"说明书"这个比喻有一个地方是错的:真正的说明书是给人看的,而设备树是给内核里的驱动模型看的。它的格式是严格树状的,每个节点对应一个物理设备,子节点挂在父节点代表的总线下面。这不是给人读的散文,是给机器读的结构化数据。

在这个体系下,文件有了明确的分工。.dts 文件描述板级信息,比如"我的板子上 I2C1 接了 FT5206"。.dtsi 文件描述 SOC 级信息,就像 C 语言的头文件 .h,它描述的是芯片本身的共性——比如"这款 SOC 有 4 个 UART,每个 UART 的寄存器基地址是什么"。

一个 SOC 可以造出无数种板子,但我们只需要把通用的 SOC 信息提取到 .dtsi 里,具体的 .dts 文件直接 include 进去就行了。这不仅解决了代码膨胀,还让代码结构变得清晰。

关于设备树的事情——咱们的仓库笔者提交了之前的笔者读文档的时候随手写的教程,可以访问仓库中的document/tutorial/driver/device_tree目录查看。

与之前教程的衔接:从硬编码到设备树

如果你跟着我们的教程一路走过来,你应该还记得在 00_chardev_base 章节里,我们写驱动的时候用的是硬编码方式。

那时候我们直接在驱动代码里写死了寄存器地址、中断号这些硬件信息。这样做对于学习驱动框架本身来说没问题,但对于实际项目来说,这种方式有很大的局限性。每次硬件信息变化,你都得修改驱动代码,重新编译。而且驱动代码变得非常臃肿,业务逻辑和硬件描述混在一起。

现在我们要学习更现代的方式:用设备树来描述硬件,让驱动代码只关心业务逻辑。这样你的驱动可以更加通用,硬件信息变了也不用动驱动代码,只需要改设备树文件就好。

这一步转变有点像从面向过程编程到面向对象编程的跨越。一开始你会觉得多写一个设备树文件很麻烦,但等你习惯了之后,你会发现这种分离带来的好处是巨大的。

我们要做什么

看完了这些概念,你可能会觉得压力山大——又是 DTS、又是 DTB、还有 DTC 编译器,从哪里开始?

其实不用焦虑。我们的目标是:理解设备树的基本概念,学会看懂和编写简单的设备树文件。我们不需要一次性掌握所有复杂的语法和属性,但我们会把整个框架搭起来

你要记住的是:设备树开发的核心,就是把硬件信息用树状结构描述出来,然后让内核在启动时读取这张"硬件地图"。只要这一步通了,剩下的就是具体的属性配置——怎么写节点、怎么设置属性值、怎么匹配驱动,那些只是填充节点里的内容罢了。

接下来的章节,我们会从最基础的设备树语法开始,一步步把知识体系搭起来。你会发现,当你真正动手写设备树文件的时候,那些抽象的概念会变得具体而实在。

说句实话,设备树这东西,光看书是学不会的。你必须亲自写文件、编译、上板测试,在这个过程中踩坑、填坑,才能真正理解。所以我们接下来的风格会是:少讲理论,多写代码,遇到问题就解决问题。

准备好了吗?我们开始吧。