2026年正点原子开发板移植(3)——设备树基础:从硬编码噩梦到硬件描述分离
2026年正点原子开发板移植3——设备树基础从硬编码噩梦到硬件描述分离为什么要谈设备树老实说设备树这个概念刚接触的时候真的让人头大。一堆花括号、各种莫名其妙的属性、那个compatible到底在匹配什么东西、引脚复用配置里的那些十六进制数是什么鬼——如果你也是这种感受放心我们当年都是这么过来的。但问题是你绕不开它。在U-Boot移植过程中设备树就是那本硬件使用说明书。CPU怎么知道你的eMMC接在哪个引脚上怎么知道你用的是这个PHY芯片而不是那个PHY芯片怎么知道你的LCD屏幕分辨率是1024x600而不是800x480答案全在设备树里。更妙的是设备树解决了嵌入式开发史上一个巨大的痛点硬编码。在设备树出现之前硬件配置是写死在代码里的。你想换个eMMC容量改代码重新编译。你想换个PHY芯片地址改代码重新编译。你想维护同一款芯片的十种不同板型恭喜你你要维护十份几乎完全相同的代码只有几个数字不同。这在工程上简直是灾难。猜猜设备树在历史的来源去搜下Linus对就那个Linux Maintainer老大哥咋喷ARM社区到狗血领头的从硬编码到设备树一场革命让我给你讲个故事。在2005年左右做嵌入式开发是什么体验假设你的板子上有一个I2C设备地址是0x50。你要做的事情是找到板级初始化代码通常在arch/arm/mach-xxx/下面的某个文件找到I2C设备的注册函数硬编码设备地址到代码里重新编译整个内核烧录测试然后有一天硬件工程师跑过来说嘿我们换个I2C设备地址改成0x51了。你就得把上面这套流程再来一遍。更惨的是如果你维护的是同一款芯片的不同板型你就得维护多份几乎相同的代码只有几个参数不同。这就是为什么那个年代的Linux内核源码里arch/arm/mach-*目录下的文件数量爆炸式增长。ARM Linux早期使用的是ATAG机制——参数标签列表。这玩意儿的功能很有限基本上只能传内存大小、命令行参数这些基础信息。复杂的硬件描述别想了。2008年PowerPC架构已经成功迁移到了设备树机制效果很好。2010年PowerPC维护者Grant Likely提议ARM也跟进从ATAG转向设备树描述硬件。这在当时引起了不小的争议因为这意味着要改动大量的板级代码。但历史证明这个决定是正确的。2011到2012年间ARM Linux开始大规模迁移到设备树ATAG机制逐步被废弃。设备树的核心思想很简单把硬件描述从代码中分离出来用一种专门的格式DTS文件来描述。硬件不变设备树就不变硬件变了只需要改设备树不需要改代码。同一个内核镜像配上不同的设备树就可以运行在不同的板子上——这在以前是不可想象的。U-Boot对设备树的支持是从v1.1.3开始的通过CONFIG_OF_LIBFDT选项启用。但有趣的是U-Boot和Linux在设备树方面并不是完全同步的。某些板级绑定在两个项目中仍然存在差异为了解决这个问题U-Boot维护了一个从Linux内核同步的设备树子目录通过devicetree-rebasing机制保持更新。你在U-Boot源码里看到的*-u-boot.dtsi文件就是专门用来添加U-Boot特定配置的不会和Linux内核的设备树冲突。DTS文件结构.dts vs .dtsi好了历史课结束。我们来聊聊DTS文件本身。首先你要搞清楚两个概念.dts文件和.dtsi文件。.dts是设备树源文件Device Tree Source.dtsi是设备树包含文件Device Tree Source Include。这个区别就像C语言里的.c和.h——.dtsi是用来被包含的公共定义.dts是具体的板级配置。来看我们的板子文件// SPDX-License-Identifier: (GPL-2.0 OR MIT) // // Copyright (C) 2016 Freescale Semiconductor, Inc. /dts-v1/; #include imx6ull.dtsi #include imx6ull-aes.dtsi #include imx6ull-14x14-evk-u-boot.dtsi / { model Awesome Embedded Studio IMX6ULL (i.mx NXP); compatible fsl,imx6ull-14x14-evk, fsl,imx6ull; };这个文件的结构非常典型。第一行/dts-v1/;声明我们使用的是设备树语法版本1这是现在通用的格式。然后是三个#include分别包含了imx6ull.dtsii.MX6ULL芯片的基础设备树定义包含CPU、内存控制器、各种外设的基本信息imx6ull-aes.dtsi我们板子特定的硬件配置imx6ull-14x14-evk-u-boot.dtsiU-Boot特定的配置然后是根节点/里面定义了model和compatible两个属性。model就是一个人类可读的描述告诉你这是什么板子compatible就重要了它是驱动匹配的关键。你会发现这个.dts文件非常简洁只有45行。真正的硬件配置都在imx6ull-aes.dtsi里。这就是良好的分层设计.dts文件只定义板子级别的信息具体的硬件配置放在.dtsi里这样可以方便地复用。常用属性详解设备树里的节点和属性看起来很神秘但其实每个属性都有明确的用途。我们来拆解几个最常用的。compatible设备身份的身份证compatible属性可能是设备树里最重要的属性了。它用于驱动程序和设备之间的匹配。比如compatible fsl,imx6ull-14x14-evk, fsl,imx6ull;这个属性有两个值fsl,imx6ull-14x14-evk和fsl,imx6ull。驱动程序会按照从左到右的顺序尝试匹配先找最具体的第一个找不到就用更通用的第二个。这个机制叫compatible向后兼容它的意思就是这个板子首先是一块imx6ull-14x14-evk其次它也是一块imx6ull。对于I2C设备比如我们的WM8960音频编解码器codec: wm89601a { #sound-dai-cells 0; compatible wlf,wm8960; reg 0x1a; ... };这里的compatible wlf,wm8960告诉驱动这是一个Wolfson Microelectronics现在是Cirrus Logic生产的WM8960芯片。驱动程序会根据这个字符串来查找对应的驱动代码。reg地址和大小reg属性用于描述设备的寄存器地址或内存映射范围。比如我们板子的内存定义memory80000000 { device_type memory; reg 0x80000000 0x20000000; };这里的reg 0x80000000 0x20000000表示内存的起始地址是0x80000000512MB大小是0x20000000512MB。i.MX6ULL的DDR内存控制器把外部SDRAM映射到了这个地址空间这是芯片的物理设计决定的。你可能会问80000000和reg里的地址有什么区别80000000是节点的单元地址unit-address它必须和reg里的第一个地址匹配。这个约定是为了让设备树编译器DTC能够快速定位节点也方便人类阅读。对于I2C设备reg的含义稍有不同codec: wm89601a { compatible wlf,wm8960; reg 0x1a; ... };这里reg 0x1a表示这个设备在I2C总线上的地址是0x1A。注意I2C地址只有7位或10位所以reg只需要一个值。status开关设备status属性可能是最简单的属性了它只有两个常用值okay和disabled。csi { status disabled; ... }; ov5640 { status disabled; ... };在我们的板子上摄像头接口CSI和OV5640摄像头都被禁用了。为什么因为我们可能还没有接摄像头或者调试时不想让它干扰。当你要启用这些设备时只需要把status改成okay就可以了不用修改任何代码。这个机制在硬件调试时非常实用。你可以先把所有不确定的设备都禁用然后一个一个启用逐个排查问题。#address-cells和#size-cells地址解码器这两个属性可能是设备树里最让人困惑的了但理解它们之后你会发现设备树的设计真的很精妙。/ { #address-cells 1; #size-cells 1; ... };#address-cells定义了子节点的reg属性中地址信息占用多少个32位单元cell。#size-cells定义了长度信息占用多少个cell。在根节点下#address-cells 1和#size-cells 1表示子节点的reg属性里地址占1个cell大小占1个cell。比如内存节点memory80000000 { reg 0x80000000 0x20000000; };这里0x80000000是地址1个cell0x20000000是大小1个cell。但对于某些总线地址和大小可能需要多个cell来表示。比如64位系统可能需要2个cell来表示地址/ { #address-cells 2; #size-cells 1; ... };这样子节点的reg就会是0x0 0x80000000 0x20000000这种格式高位地址在前。对于I2C总线这样的简单总线#size-cells通常是0因为I2C设备没有地址范围的概念i2c2 { #address-cells 1; #size-cells 0; codec: wm89601a { reg 0x1a; }; };这里#size-cells 0表示I2C设备的reg里没有大小信息只有地址。引脚复用配置pinctrl子系统引脚复用Pin Multiplexing是嵌入式系统里最容易让人头疼的问题之一。i.MX6ULL这样的芯片引脚数量远远多于实际封装的管脚数量所以一个物理引脚往往可以复用为多种功能。比如UART1_TX_DATA这个引脚可以作为UART1的发送引脚也可以配置成普通GPIO或者其它外设的信号。在设备树里引脚复用配置是通过pinctrl子系统来描述的。来看一个例子pinctrl_uart1: uart1grp { fsl,pins MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1 MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1 ; };这里定义了一个叫pinctrl_uart1的引脚配置组里面有两个引脚。每个引脚配置由两部分组成引脚名称和配置值。MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX这个宏定义在imx6ul-pinfunc.h里展开后是一个32位整数高16位是引脚编号低16位是复用功能选择。__前面是物理引脚名称PAD后面是复用功能MUX。后面的0x1b0b1是引脚电气配置。这个32位数的每一位都有含义位0-11上拉/下拉配置、驱动强度、开漏使能、速度等位12-15保留位位16-23额外配置如施密特触发器位24-31保留位具体到0x1b0b1二进制0001 1011 0000 1011 00010x1b0部分配置了引脚的电气特性最后的0xb1配置了上拉、驱动等这些数值是怎么来的答案是芯片数据手册Datasheet。NXP的i.MX6ULL参考手册里有一张巨大的表列出了每个引脚的所有配置选项和对应的寄存器值。你没看错这些数字不是瞎填的每一个位都有数据手册依据。引脚配置还有一个重要的概念状态切换。比如我们的eMMC配置usdhc2 { pinctrl-names default, state_100mhz, state_200mhz; pinctrl-0 pinctrl_usdhc2_8bit; pinctrl-1 pinctrl_usdhc2_8bit_100mhz; pinctrl-2 pinctrl_usdhc2_8bit_200mhz; ... };这里定义了三种状态默认状态default、100MHz状态state_100mhz和200MHz状态state_200mhz。pinctrl-0/1/2分别对应这三个状态的引脚配置。为什么要这样因为eMMC在不同的工作频率下需要不同的引脚电气特性。低速时用默认配置中速时切换到100MHz配置高速时切换到200MHz配置。驱动程序会根据实际工作频率自动切换状态这是硬件优化的一个重要手段。你可能注意到了每个引脚配置后面都有一个类似0x1b0b1这样的数字。这个数字是引脚的电气特性配置包括驱动强度、上拉下拉、转换速率等。不同频率下这个数值是不同的因为高速信号需要更严格的时序控制。比如在默认状态下eMMC引脚配置是0x17059而在200MHz状态下是0x170f9。这两个数值的差异主要在驱动强度和转换速率上高速模式下需要更强的驱动和更快的转换速率。时钟配置CCM和PLL原理嵌入式系统的时钟管理是一个深不见底的话题但我们先从设备树的角度来看看是怎么描述的。i.MX6ULL的时钟系统非常复杂有一个叫CCMClock Controller Module的模块里面有多个PLLPhase-Locked Loop锁相环和各种分频器。PLL负责把外部晶振通常是24MHz倍频到更高的频率然后分频器再把这些高频时钟分配给各个外设。我们的设备树里有这样一段clks { assigned-clocks clks IMX6UL_CLK_PLL3_PFD2, clks IMX6UL_CLK_PLL4_AUDIO_DIV; assigned-clock-rates 320000000, 786432000; };这段话的意思是把PLL3_PFD2的频率设置为320MHz把PLL4_AUDIO_DIV的频率设置为786.432MHz。为什么是786.432MHz这个奇怪的数字因为音频采样率如44.1kHz、48kHz需要精确的时钟分频786.432MHz是音频时钟树的一个常用频率它可以精确分频出各种音频采样率。时钟配置的一个重要原则是不是所有外设都需要最高的时钟频率。过高的时钟频率会增加功耗和EMI电磁干扰所以应该根据实际需求设置合适的频率。比如SAI2音频接口的配置sai2 { assigned-clocks clks IMX6UL_CLK_SAI2_SEL, clks IMX6UL_CLK_SAI2; assigned-clock-parents clks IMX6UL_CLK_PLL4_AUDIO_DIV; assigned-clock-rates 0, 12288000; ... };这里SAI2_SEL选择时钟源PLL4_AUDIO_DIVSAI2设置分频后的频率12.288MHz。注意第一个assigned-clock-rates是0表示自动选择也就是只选择时钟源但不设置具体频率。你可能还注意到了assigned-clock-parents这个属性。i.MX6ULL的时钟系统是多级的每个时钟可能从多个源头获取信号。比如SAI2可以选择从PLL、OSC、或者其它分频器获取时钟。assigned-clock-parents就是用来选择时钟源的。时钟配置中最让人头疼的部分可能是时钟ID。IMX6UL_CLK_PLL3_PFD2这样的宏定义在imx6ul-clock.h里每个ID对应CCM里的一个具体时钟。这些ID是芯片设计时定义的你无法更改只能在驱动代码里查找对应关系。实战分析我们的imx6ull-aes.dts设备树现在我们来完整分析一下我们板子的设备树看看它是如何描述硬件的。首先来看.dts文件/dts-v1/; #include imx6ull.dtsi #include imx6ull-aes.dtsi #include imx6ull-14x14-evk-u-boot.dtsi / { model Awesome Embedded Studio IMX6ULL (i.mx NXP); compatible fsl,imx6ull-14x14-evk, fsl,imx6ull; }; clks { assigned-clocks clks IMX6UL_CLK_PLL3_PFD2, clks IMX6UL_CLK_PLL4_AUDIO_DIV; assigned-clock-rates 320000000, 786432000; }; csi { status okay; }; ov5640 { status okay; }; /delete-node/ sim2; usdhc2 { pinctrl-names default, state_100mhz, state_200mhz; pinctrl-0 pinctrl_usdhc2_8bit; pinctrl-1 pinctrl_usdhc2_8bit_100mhz; pinctrl-2 pinctrl_usdhc2_8bit_200mhz; bus-width 8; non-removable; status okay; };这个文件有几个值得注意的技巧。首先是csi、ov5640这样的语法这叫节点引用node reference。符号表示引用在包含文件里定义的节点然后在大括号里添加或覆盖属性。这是一种非常优雅的修改方式不需要复制整个节点定义只需要修改你关心的属性。其次是/delete-node/ sim2;这个语法。SIM卡接口在我们的板子上不存在所以直接删除这个节点。设备树编译器会从最终的二进制设备树DTB里移除这个节点就像它从未存在过一样。最后是usdhc2节点。这里重写了pinctrl-names和pinctrl-0/1/2属性覆盖了imx6ull-aes.dtsi里的定义。这是为了启用8位宽度的eMMC接口bus-width 8。non-removable表示eMMC是焊在板子上的不可热插拔驱动程序可以据此优化行为。接下来看.dtsi文件这里包含了大部分硬件配置/ { aliases { spi5 {/spi-4}; }; chosen { stdout-path uart1; }; memory80000000 { device_type memory; reg 0x80000000 0x20000000; }; ... };aliases节点定义了设备的别名。spi5 {/spi-4}是一个特殊的语法引用了一个绝对路径节点/spi-4。这个节点是用GPIO模拟的SPI总线因为i.MX6ULL的硬件SPI不够用。chosen节点是启动参数的传递渠道。stdout-path uart1表示控制台输出到UART1这样U-Boot的早期启动信息就能通过串口打印出来。这对于调试非常重要没有它你就看不到启动日志。然后是各种外设节点比如网络接口fec1 { pinctrl-names default; pinctrl-0 pinctrl_enet1; phy-mode rmii; phy-handle ethphy0; phy-reset-gpios gpio5 7 GPIO_ACTIVE_LOW; phy-reset-duration 200; phy-reset-post-delay 200; phy-supply reg_peri_3v3; status okay; };fec1是i.MX6ULL的第一个以太网控制器Fast Ethernet Controller。phy-mode rmii表示使用RMII接口与PHY芯片通信这是i.MX6ULL常用的网络接口方式MII需要更多引脚。phy-handle ethphy0引用了MDIO总线上的PHY设备描述。phy-reset-gpios定义了PHY复位信号GPIO5_7低电平有效。phy-reset-duration和phy-reset-post-delay定义了复位时序PHY芯片需要正确的复位序列才能正常工作。phy-supply reg_peri_3v3表示PHY芯片由3.3V外设电源供电。再看MDIO总线和PHY设备定义fec2 { pinctrl-names default; pinctrl-0 pinctrl_enet2; phy-mode rmii; phy-handle ethphy1; phy-reset-gpios gpio5 8 GPIO_ACTIVE_LOW; phy-reset-duration 200; phy-reset-post-delay 200; phy-supply reg_peri_3v3; status okay; mdio { #address-cells 1; #size-cells 0; ethphy0: ethernet-phy2 { compatible ethernet-phy-id0022.1560; reg 2; micrel,led-mode 1; clocks clks IMX6UL_CLK_ENET_REF; clock-names rmii-ref; }; ethphy1: ethernet-phy1 { compatible ethernet-phy-id0022.1560; reg 1; micrel,led-mode 1; clocks clks IMX6UL_CLK_ENET2_REF; clock-names rmii-ref; }; }; };注意mdio节点定义在fec2节点里面。MDIO是管理数据输入输出总线用于配置PHY芯片。#address-cells 1和#size-cells 0表示MDIO总线上的设备只有地址没有地址范围。ethphy0和ethphy1是两片KSZ8091RNB PHY芯片compatible ethernet-phy-id0022.1560中的0022.1560是PHY的ID号IEEE OUI为00:22型号为15:60。reg 2和reg 1是PHY在MDIO总线上的地址。micrel,led-mode 1是Micrel现在是MicrochipPHY特有的属性配置LED指示灯行为。clocks clks IMX6UL_CLK_ENET_REF定义了PHY的参考时钟源。RMII接口需要一个50MHz的参考时钟这个时钟可以由MAC网络控制器提供也可以由外部晶振提供。这里选择由MAC提供。与正点原子设备树的对比正点原子ALIENTEK是国内知名的嵌入式开发板厂商他们的i.MX6ULL开发板也使用设备树。对比一下我们的设备和他们的设备树你会发现一些有趣的设计差异。首先正点原子的设备树文件命名通常是imx6ull-alientek-emmc.dts而我们是imx6ull-aes.dts。命名风格不同但遵循同样的规则芯片名-板型名.dts。在结构上正点原子的设备树倾向于把更多配置写在.dts文件里而我们将大部分硬件配置放在.dtsi里。这两种方式没有优劣之分只是组织风格的差异。我们的方式更强调板级配置与芯片级配置分离他们的方式更强调一个文件看清板子全貌。在时钟配置上正点原子的设备树通常设置更多的时钟固定频率clks { assigned-clocks clks IMX6UL_CLK_PLL3_PFD2, clks IMX6UL_CLK_PLL4_AUDIO_DIV; assigned-clock-rates 320000000, 786432000; };这部分我们是一致的。但正点原子的某些板型还会固定更多外设时钟如UART、I2C等。我们的策略是让驱动程序自动选择时钟这样更灵活功耗也更好控制。在引脚配置上正点原子的设备树倾向于使用更宽松的电气特性如更大的驱动强度、更快的转换速率而我们倾向于根据实际信号要求选择合适的配置。例如我们的eMMC 200MHz配置是0x170f9而正点原子可能用0x1b0f9更强的驱动。这种差异不会导致功能问题但在EMI测试时可能会有区别。一个显著差异是SIM卡接口。正点原子的某些板型支持SIM卡所以他们的设备树里保留了sim2节点sim2 { pinctrl-names default; pinctrl-0 pinctrl_sim2; assigned-clocks clks IMX6UL_CLK_SIM_SEL; assigned-clock-parents clks IMX6UL_CLK_SIM_PODF; assigned-clock-rates 240000000; ... };我们的板子不需要SIM卡功能所以直接删除了sim2节点。这种按需启用的策略让设备树更简洁也避免了不必要的外设初始化。设备树对移植的好处到这里你应该能体会到设备树对嵌入式系统移植的巨大价值了。让我总结一下首先是代码复用。有了设备树同一个U-Boot镜像可以运行在多个不同的板子上只要给它们配上不同的设备树文件。这在产品线维护时是巨大的优势——你不需要为每个板型维护一套独立的代码。其次是调试效率。硬件配置修改不需要重新编译代码只需要修改设备树然后重新编译DTB。DTB的编译速度比完整的代码编译快得多这大大缩短了调试周期。然后是可维护性。设备树用结构化的方式描述硬件比硬编码的板级初始化函数更易读、更易维护。新人接手项目时看设备树比看一大板级初始化代码要轻松得多。最后是社区协作。设备树已经成为Linux和U-Boot的标准硬件描述方式这意味着你可以直接使用社区贡献的设备树或者把自己的设备树贡献回社区。正点原子、NXP官方、以及其他开发者的设备树都可以作为参考这大大降低了开发门槛。写在最后设备树的学习曲线确实陡峭这是不争的事实。一堆宏定义、莫名其妙的十六进制数、复杂的时钟树、怎么也记不住的属性名称——刚接触时谁都会有点崩溃。但好消息是设备树是一个投入一次长期受益的技能。一旦你理解了它的基本原理后面遇到任何新的芯片或板子你都能快速上手。因为设备树的核心思想——硬件描述与代码分离——是通用的。在下一篇文章里我们将深入到实际的移植过程中。你会看到如何从零开始为一个新板子编写设备树如何验证设备树配置是否正确以及如何调试设备树相关的问题。那将是一个真正的从原理到实践的过程。但在此之前我建议你做一件事情打开我们项目的patches/uboot-imx/charlies_board.patch仔细读一下imx6ull-aes.dtsi里的每个节点和属性。对照着i.MX6ULL的参考手册和板子原理图尝试理解每个配置的含义。这个过程可能有点枯燥但你会发现设备树其实是一份非常精确的硬件说明书——它描述的每一条信息都能在硬件上找到对应的实体。准备好了吗让我们继续深入U-Boot的世界。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2413753.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!