编写设备树DTS

    xiaoxiao2021-03-25  107

    第一步就是要为这个模型机构建一个基本结构,这是一个有效的设备树最基本的结构。在这个阶段你需要唯一的标识该机器。

    /{

      compatible= " Marvell ,armada38x"

    };compatible 指定了系统的名称。它包含了一个“<制造商>,<型号>”形式的字符串。重要的是要指定一个确切的设备,并且包括制造商的名子,以避免命名空间冲突。由于操作系统会使用 compatible 的值来决定如何在机器上运行,所以正确的设置这个属性变得非常重要。


     

    接下来就应该描述每个 CPU 了。先添加一个名为“cpus”的容器节点,然后为每个 CPU 分别添加子节点。具体到我们的情况是一个 ARM 的 双核 Cortex A9 系统

    /{

      compatible= " Marvell ,armada38x";

      cpus{

         cpu@0{

              compatible = "arm,cortex-a9";

            };

           cpu@1{

              compatible = "arm,cortex-a9";

            };

          };

    };每个 cpu 节点的 compatible 属性是一个“<制造商>,<型号>”形式的字符串,并指定了确切的 cpu,就像顶层的 compatible 属性一样


    节点名称:现在应该花点时间来讨论命名约定了。每个节点必须有一个“<名称>[@<设备地址>]”形式的名字。

      1).名称> 就是一个不超过31位的简单 ascii 字符串。通常,节点的命名应该根据它所体现的是什么样的设备。比如一个 3com 以太网适配器的节点就应该命名为 ethernet,而不应该是 3com509。

      2).如果该节点描述的设备有一个地址的话就还应该加上设备地址(unit-address)。通常,设备地址就是用来访问该设备的主地址,并且该地址也在节点的 reg 属性中列出

      3).同级节点命名必须是唯一的,但只要地址不同,多个节点也可以使用一样的通用名称(例如 serial@101f1000 和 serial@101f2000)

     

    设备:系统中每个设备都表示为一个设备树节点。所以接下来就应该为这个设备树填充设备节点。现在,知道我们讨论如何进行寻址和中断请求如何处理之前这些新节点将一直为空。 

    / {     compatible = "acme,coyotes-revenge";

        cpus {         cpu@0 {             compatible = "arm,cortex-a9";         };         cpu@1 {             compatible = "arm,cortex-a9";         };     };

        serial@101F0000 {         compatible = "arm,pl011";     };

        serial@101F2000 {         compatible = "arm,pl011";     };

        gpio@101F3000 {         compatible = "arm,pl061";     };

        interrupt-controller@10140000 {         compatible = "arm,pl190";     };

        spi@10115000 {         compatible = "arm,pl022";     };

        external-bus {         ethernet@0,0 {             compatible = "smc,smc91c111";         };

            i2c@1,0 {             compatible = "acme,a1234-i2c-bus";             rtc@58 {                 compatible = "maxim,ds1338";             };         };

            flash@2,0 {             compatible = "samsung,k8f1315ebm", "cfi-flash";         };     }; };在此树中,已经为系统中的每个设备添加了节点,而且这个·层次结构也反映了设备与系统的连接方式。例如,外部总线上的设备就是外部总线节点的子节点,i2c 设备就是 i2c 总线节点的子节点。通常,这个层次结构表现的是 CPU 视角的系统视图。现在这棵树还是无效的,因为它缺少关于设备之间互联的信息。稍后将添加这些信息。

    在这颗树中,应该注意这些事情:

                  每个设备节点都拥有一个 compatible 属性,

                  闪存(flash)节点的 compatible 属性由两个字符串构成。欲知为何,请阅读下一节

     

    理解 compatible 属性:

                 1).树中每个表示一个设备的节点都需要一个 compatible 属性。compatible 属性是操作系统用来决定使用哪个设备驱动来绑定到一个设备上的关键因素。

                 2).compatible 是一个字符串列表,之中第一个字符串指定了这个节点所表示的确切的设备,该字符串的格式为:"<制造商>,<型号>"。剩下的字符串的则表示其它与之相兼容的设备。

                       3).Freescale MPC8349 片上系统(SoC)拥有一个实现了美国国家半导体 ns16550 的寄存器接口的串行设备,那么 MPC8349 的串行设备的 compatible 属性就应该是:compatible = "fsl,mpc8349-uart", "ns16550"。在这里,mpc8349-uart 指定了确切的设备,而 ns16550 则说明这是与美国国家半导体 ns16550 UART 的寄存器级兼容

                 4).不要使用带通配符的 compatible 值,比如“fsl,mpc83xx-uart”或类似情况。芯片提供商无不会做出一些能够轻易打破你通配符猜想的变化,这时候在修改已经为时已晚了。相反,应该选择一个特定的芯片然后是所有后续芯片都与之兼容。

    如何编址:

              1>可编址设备使用以下属性将地址信息编码进设备树

           ■ reg

           ■ #address-cells        ■ #size-cells

        2>每个可编址设备都有一个元组列表的 reg,元组的形式为:reg = <地址1 长度1 [地址2 长度2] [地址3 长度3] ... >。每个元组都表示一个该设备使用的地址范围。每个地址值是一个或多个 32 位整型数列表,称为 cell。同样,长度值也可以是一个 cell 列表或者为空。

        3>由于地址和长度字段都是可变大小的变量,那么父节点的 #address-cells 和 #size-cells 属性就用来声明各个字段的 cell 的数量。换句话说,正确解释一个 reg 属性需要用到父节点的 #address-cells 和 #size-cells 的值。要知道这一切是如何运作的,我们将给模型机添加编址属性,就从 CPU 开始。

          CPU 编址:

                

    CPU 节点表示了一个关于编址的最简单的例子。每个 CPU 都分配了一个唯一的 ID,并且没有 CPU id 相关的大小信息。     cpus {         #address-cells = <1>;         #size-cells = <0>;         cpu@0 {             compatible = "arm,cortex-a9";             reg = <0>;         };         cpu@1 {             compatible = "arm,cortex-a9";             reg = <1>;         };     };在 cpu 节点中,#address-cells 设置为 1,#size-cells 设置为 0。这意味着子节点的 reg 值是一个单一的 uint32,这是一个不包含大小字段的地址,为这两个 cpu 分配的地址是 0 和 1。cpu 节点的 #size-cells 为 0 是因为只为每个 cpu 分配一个单独的地址。你可能还会注意到 reg 的值和节点名字是相同的。按照惯例,如果一个节点有 reg 属性,那么该节点的名字就必须包含设备地址,这个设备地址就是 reg 属性里第一个地址值

        内存映射设备:   

    与 cpu 节点里单一地址值不同,应该分配给内存映射设备一个地址范围。#size-cells 声明每个子节点的 reg 元组中长度字段的大小。在接下来的例子中,每个地址值是 1 cell(32 位),每个长度值也是 1 cell,这是典型的 32 位系统。64 位的机器则可以使用值为 2 的 #address-cells 和 #size-cells 来获得在设备树中的 64 位编址。 / {     #address-cells = <1>;     #size-cells = <1>;

        ...

        serial@101f0000 {         compatible = "arm,pl011";         reg = <0x101f0000 0x1000 >;     };

        serial@101f2000 {         compatible = "arm,pl011";         reg = <0x101f2000 0x1000 >;     };

        gpio@101f3000 {         compatible = "arm,pl061";         reg = <0x101f3000 0x1000                0x101f4000 0x0010>;     };

        interrupt-controller@10140000 {         compatible = "arm,pl190";         reg = <0x10140000 0x1000 >;     };

        spi@10115000 {         compatible = "arm,pl022";         reg = <0x10115000 0x1000 >;     };

        ... };每个设备都被分配了一个基址以及该区域的大小。这个例子中为 GPIO 分配了两个地址范围:0x101f3000...0x101f3fff 和 0x101f4000..0x101f400f。

     

    一些挂在总线上的设备有不同的编址方案。例如一个带独立片选线的设备也可以连接至外部总线。由于父节点会为其子节点定义地址域,所以可以选择不同的地址映射来最恰当的描述该系统。下面的代码展示了设备连接至外部总线并将其片选号编码进地址的地址分配

    external-bus {         #address-cells = <2>         #size-cells = <1>;

            ethernet@0,0 {             compatible = "smc,smc91c111";             reg = <0 0 0x1000>;         };

            i2c@1,0 {             compatible = "acme,a1234-i2c-bus";             reg = <1 0 0x1000>;             rtc@58 {                 compatible = "maxim,ds1338";             };         };

            flash@2,0 {             compatible = "samsung,k8f1315ebm", "cfi-flash";             reg = <2 0 0x4000000>;         };     };外部总线的地址值使用了两个 cell,一个用于片选号;另一个则用于片选基址的偏移量。而长度字段则还是单个 cell,这是因为只有地址的偏移部分才需要一个范围量。所以,在这个例子中,每个 reg 项都有三个 cell:片选号、偏移量和长度。

    由于地址域是包含于一个节点及其子节点的,所以父节点可以自由的定义任何对于该总线来说有意义的编址方案。那些在直接父节点和子节点以外的节点通常不关心本地地址域,而地址应该从一个域映射到另一个域

    chosen 节点:

    chosen 节点并不代表一个真正的设备,只是作为一个为固件和操作系统之间传递数据的地方,比如引导参数。chosen 节点里的数据也不代表硬件。通常,chosen 节点在 .dts 源文件中为空,并在启动时填充。

    chosen {         bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";     };

    问题一:以前的Linux Kernel如何描述硬件,现在又如何描述呢? 在以前的内核版本中: 1)内核包含了对硬件的全部描述; 2)bootloader会加载一个二进制的内核镜像,并执行它,比如uImage或者zImage; 3)bootloader会提供一些额外的信息,成为ATAGS,它的地址会通过r2寄存器传给内核;     ATAGS包含了内存大小和地址,kernel command line等等; 4)bootloader会告诉内核加载哪一款board,通过r1寄存器存放的machine type integer; 5)U-Boot的内核启动命令:bootm  6)Barebox变量:bootm.image (?)

    现今的内核版本使用了Device Tree: 1)内核不再包含对硬件的描述,它以二进制的形式单独存储在另外的位置:the device tree blob 2)bootloader需要加载两个二进制文件:内核镜像和DTB     内核镜像仍然是uImage或者zImage;     DTB文件在arch/arm/boot/dts中,每一个board对应一个dts文件; 3)bootloader通过r2寄存器来传递DTB地址,通过修改DTB可以修改内存信息,kernel command line,以及潜在的其它信息; 4)不再有machine type; 5)U-Boot的内核启动命令:bootm  6)Barebox变量:bootm.image,bootm.oftree 有些bootloader不支持Device Tree,或者有些专门给特定设备写的版本太老了,也不包含。为了解决这个问题,CONFIG_ARM_APPENDED_DTB被引进。     它告诉内核,在紧跟着内核的地址里查找DTB文件;     由于没有built-in Makefile rule来产生这样的内核,因此需要手动操作:         cat arch/arm/boot/zImage arch/arm/boot/dts/myboard.dtb > my-zImage         mkimage ... -d my-zImage my-uImage     (cat这个命令,还能够直接合并两个mp3文件哦!so easy!) 另外,CONFIG_ARM_ATAG_DTB_COMPAT选项告诉内核去bootloader里面读取ATAGS,并使用它们升级DT。 ============================================= 问题二:现在Linux Kernel使用的Device Tree到底是个什么东东? 引用the Power.org Standard for Embedded Power Architecture Platform Requirements (ePAPR)的定义: 1)ePAPR使用device tree的概念描述硬件。boot程序会加载device tree到client program's memory中,并将device tree的指针传递给client; 2)device tree是一个树形数据结构with nodes,用来描述系统的physical devices; 3)一个ePAPR-complient device tree描述的设备信息不能被client program读取; From Source to binary 1)在ARM系统中,所有的DTS文件放置在arch/arm/boot/dts中:     .dts文件为板级定义     .dtsi文件为SoC级定义 2)Device Tree Compiler工具,将源代码编译成二进制形式;     它的源代码放置在scripts/dtc中 3)编译器会产生DTB文件,bootloader会加载这个DTB文件,内核在boot时去解析它; 4)arch/arm/boot/dts/Makefile会指定产生哪个DTB文件; 上图是pdf里面自带的例子,我再从arch/arm/boot/dts/am33xxx.dtsi中摘录了两个:                                 uart0: serial@44e09000 {                         compatible = "ti,omap3-uart";                         ti,hwmods = "uart1";                         clock-frequency = <48000000>;                         reg = <0x44e09000 0x2000>;                         interrupts = <72>;                         status = "disabled";                 };                 uart1: serial@48022000 {                         compatible = "ti,omap3-uart";                         ti,hwmods = "uart2";                         clock-frequency = <48000000>;                         reg = <0x48022000 0x2000>;                         interrupts = <73>;                         status = "disabled";                 }; 对比图片中的注释,就能够知道对于uart0这个外设: Node name: serial Unit Address: 0x44e09000 compatible: 定义了设备的programming model,允许操作系统识别对应的程序驱动; clock-frequency: 48000000,晶振频率为24MHz,这应该是PLL倍频后的输出(?); reg: 寄存器的地址和寄存器长度,uart0的地址起始为0x44e09000,长度为0x2000; interrupts: 中断号; status: 状态值,初始的时候为disabled,即禁用它; ============================================= 问题三:Device Tree的编写规则是怎样的? 1)Device Tree inclusion不一定要做成monolithic,它们可以分散在不同的文件中,互相包含; 2).dtsi文件是被包含的,.dts文件才是最终的Device Trees; 3).dts文件包含了板级信息; 4)including工作主要是将including file覆盖到included file上面; 5)inclusion使用DT操作符/include/,或者在某些少量的内核发布版中,由于DTS是使用了C preprocessor,因此推荐#include。 由这张图可见,如果included file中的某项,被including file文件定义了,则会使用后者的定义,也就是使用更上层更新的定义;如果没有被定义,则添加进入。 1)bindings是device tree里面可已包含的specific types and classes of devices。 2)compatible特征描述了节点编译的specific binding; 3)当为一个设备创建新的device tree时,应该创建a binding来描述设备的全部细节。 ============================================= 问题四:在哪里可以找到Device Tree的文档呢?-------这里就是可以用来参考写法 1)所有可被内核识别的Device Tree bindings在文档Documentation/devicetree/bindings里面; 2)每个binding文档描述了哪些properties可以被接受,可以使用哪些值,哪些特征是必须的,哪些是可选的; 3)所有新的Device Tree bindings必须让代码维护者审核,提交到devicetree@vger.kernel.org上。这用来保证它们的正确性和一致性。

    例如,

    打开里面的txt文件就是例子:

    ============================================= 问题五:Device Tree中的常见关键字含义是什么? Device Tree organization: top-level nodes 在设备的最顶层节点上,一般可以发现如下这些: cpus:描述了系统的CPU memory:定义了RAM的地址和大小 chosen:定义了boot时被系统固件选择或定义的参数;可用来传递kernel command line; aliases:定义了certain nodes的shotcuts; 一个或多个总线定义; 一个或多个板上设备定义; 下面是am33xx.dtsi中的定义: / {         compatible = "ti,am33xx";         interrupt-parent = <&intc>;         aliases {                 i2c0 = &i2c0;                 i2c1 = &i2c1;                 i2c2 = &i2c2;                 serial0 = &uart0;                 serial1 = &uart1;                 serial2 = &uart2;                 serial3 = &uart3;                 serial4 = &uart4;                 serial5 = &uart5;                 d_can0 = &dcan0;                 d_can1 = &dcan1;                 usb0 = &usb0;                 usb1 = &usb1;                 phy0 = &usb0_phy;                 phy1 = &usb1_phy;                 ethernet0 = &cpsw_emac0;                 ethernet1 = &cpsw_emac1;         };         cpus {                 #address-cells = <1>;                 #size-cells = <0>;                 cpu@0 {                         compatible = "arm,cortex-a8";                         device_type = "cpu";                         reg = <0>;                                                  /*                          * To consider voltage drop between PMIC and SoC,                          * tolerance value is reduced to 2% from 4% and                          * voltage value is increased as a precaution.                          */                         operating-points = <                                 /* kHz    uV */                                 720000  1285000                                 600000  1225000                                 500000  1125000                                 275000  1125000                         >;                         voltage-tolerance = <2>; /* 2 percentage */                         clocks = <&dpll_mpu_ck>;                         clock-names = "cpu";                                                  clock-latency = <300000>; /* From omap-cpufreq driver */                 };         };         pmu {                 compatible = "arm,cortex-a8-pmu";                 interrupts = <3>;         };         /*          * The soc node represents the soc top level view. It is used for IPs          * that are not memory mapped in the MPU view or for the MPU itself.          */         soc {                 compatible = "ti,omap-infra";                 mpu {                         compatible = "ti,omap3-mpu";                         ti,hwmods = "mpu";                 };         };     /* ...... */ }; 从上面的代码里面可以找出四个compatible,分别是: top: compatible = "ti,am33xx"; cpu0: compatible = "arm,cortex-a8"; pmu: compatible = "arm,cortex-a8-pmu"; soc: compatible = "ti,omap-infra"; 怎样使用compatible呢? 方法一是用来匹配DT_MACHINE结构体中的dt_compat域,方法二是使用of_machine_is_compatible函数。 在总线中,一般要定义compatile、#address-cells、#size-cells、ranges,比如:        i2c0: i2c@44e0b000 {                 compatible = "ti,omap4-i2c";                 #address-cells = <1>;                 #size-cells = <0>;                 ti,hwmods = "i2c1";                 reg = <0x44e0b000 0x1000>;                 interrupts = <70>;                 status = "disabled";         }; ============================================= 问题六:有关DT的策略,你应该了解的~ DT是一种硬件描述,而不是一种配置。 它应该描述硬件的构成,和硬件工作的方式; 它不应该描述那种硬件配置你更加喜欢; 例如:     你可以在DT中描述是否硬件配置支持DMA;     但是你不要在DT中描述你是否想要DMA。 1)DT独立于OS,它也需要非常稳定; 2)最初的设想是,DTBs由生产厂家烧写进芯片中,用户直接安装系统就好了; 3)当Device Tree binding被定义,并且在DTBs使用之后,它就不能再改变,但可以扩展; 4)这意味着Device Tree binding是内核的二进制程序接口(ABI),它需要same care; 5)但是内核开发者意识到了这个很难达到,并且会减慢驱动程序的集成:     ARM Kernel Mini-submit discussions放松了这些限制;     未来在Kernel Summit时会有additional discussion; Basic guidelines for binding design:

    1) A precise compatible string is better than a vague one.

    转载请注明原文地址: https://ju.6miu.com/read-22102.html

    最新回复(0)