嵌入式Linux开发——设备树简介 - 给硬件画张像
嵌入式Linux开发——设备树简介 - 给硬件画张像仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里或者一起来尝试跑7.0的Linux欢迎各位大佬观摩喜欢的话点个⭐仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://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-xxx和arch/arm/plat-xxx文件夹里面全是描述板级信息的.c和.h文件。我们来想象一个具体的场景。假设你手里有一块开发板板子上有 CPU有内存有挂在 I2C 总线上的触摸屏和 EEPROM还有 SPI 接口上的 Flash 芯片。作为开发者你需要告诉 Linux 内核这些硬件都存在、它们接在哪条总线上、地址是多少。在旧的做法里你得写一个 C 文件来描述这块板子。比如针对 SMDK2440 开发板内核里就有这样一个文件/* arch/arm/mach-smdk2440.c (片段) */staticstructs3c2410fb_displaysmdk2440_lcd_cfg __initdata{.lcdcon5S3C2410_LCDCON5_FRM565|S3C2410_LCDCON5_INVVLINE|S3C2410_LCDCON5_INVVFRAME|S3C2410_LCDCON5_PWREN|S3C2410_LCDCON5_HWSWP,/* ... 其他配置 ... */};staticstructs3c2410fb_mach_infosmdk2440_fb_info __initdata{.displayssmdk2440_lcd_cfg,.num_displays1,.default_display0,/* ... 其他配置 ... */};staticstructplatform_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 编译器从哪里开始其实不用焦虑。我们的目标是理解设备树的基本概念学会看懂和编写简单的设备树文件。我们不需要一次性掌握所有复杂的语法和属性但我们会把整个框架搭起来。你要记住的是设备树开发的核心就是把硬件信息用树状结构描述出来然后让内核在启动时读取这张硬件地图。只要这一步通了剩下的就是具体的属性配置——怎么写节点、怎么设置属性值、怎么匹配驱动那些只是填充节点里的内容罢了。接下来的章节我们会从最基础的设备树语法开始一步步把知识体系搭起来。你会发现当你真正动手写设备树文件的时候那些抽象的概念会变得具体而实在。说句实话设备树这东西光看书是学不会的。你必须亲自写文件、编译、上板测试在这个过程中踩坑、填坑才能真正理解。所以我们接下来的风格会是少讲理论多写代码遇到问题就解决问题。准备好了吗我们开始吧。