zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux驱动开发中的中间件:设备树

2023-09-11 14:20:46 时间

Linux设备树

设备树的产生是为了解决内核源码的arch/arm目录下代码混乱和臃肿的问题(过去每个厂商出个板子就要提供外设硬件和平台硬件信息,这些信息以.c和.h文件的形式呈现)。在使用设备树之后,就使得每个硬件平台的硬件资源仅需要一个设备树文件来描述了,而不用在内核源码的arch/arm下以.c 或 .h 文件来定义。Linux内核则在启动过程中,通过解析设备树中的硬件资源来初始化某个具体的平台。

DTS、DTB、DTC三者的关系

DTS是设备树源码文件;
DTB是DTS编译后得到的二进制文件;
DTC则是将 .dts 文件编译成 .dtb 文件的编译工具。

编译DTS文件

在内核源码的 arch/arm/boot/dts/ 目录下的 Makefile 文件中添加 DTS 文件的信息,即将文件名添加进去,添加位置如下图。(先确定SOC的类型,我的是IMX6ULL,再写入板子的名字,名字是自定义的)
在这里插入图片描述
然后回到内核源码根目录,执行如下命令进行编译

make dtbs
或者
make all

设备树的头文件

设备树可以用 .h 文件作为头文件,并且它也有另一种头文件,即 .dtsi 文件,并且都是用 #include 来引用的。

一般 .dtsi 文件用于描述 SOC 的内部外设信息。

示例:在 imx6ull-alientek-emmc.dts 文件中就引用了如下两个头文件。
在这里插入图片描述
在 imx6ull.dtsi 文件中的描述内容(简化后只留下树节点)如下:

#include <dt-bindings/clock/imx6ul-clock.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include "imx6ull-pinfunc.h"
#include "imx6ull-pinfunc-snvs.h"
#include "skeleton.dtsi"

/ {
    aliases {};
    cpus 
    {
        cpu0: cpu@0 {};
    };

    intc: interrupt-controller@00a01000 {};

    clocks 
    {
        ckil: clock@0 {};
        osc: clock@1 {};
        ipp_di0: clock@2 {};
        ipp_di1: clock@3 {};
    };

    soc
    {
        busfreq {};
        pmu {};
        ocrams: sram@00900000 {};
        ocrams_ddr: sram@00904000 {};
        ocram: sram@00905000 {};
        dma_apbh: dma-apbh@01804000 {};
        gpmi: gpmi-nand@01806000{};
        
        aips1: aips-bus@02000000 
        {
            spba-bus@02000000 
            {
                spdif: spdif@02004000 {};
                ecspi1: ecspi@02008000 {};
                ecspi2: ecspi@0200c000 {};
                ecspi3: ecspi@02010000 {};
                ecspi4: ecspi@02014000 {};
                uart7: serial@02018000 {};
                uart1: serial@02020000 {};
                esai: esai@02024000 {};
                sai1: sai@02028000 {};
                sai2: sai@0202c000 {};
                sai3: sai@02030000 {};
                asrc: asrc@02034000 {};
            };
            tsc: tsc@02040000 {};
            pwm1: pwm@02080000 {};
            pwm2: pwm@02084000 {};
            pwm3: pwm@02088000 {};
            pwm4: pwm@0208c000 {};
            flexcan1: can@02090000 {};
            flexcan2: can@02094000 {};
            gpt1: gpt@02098000 {};
            gpio1: gpio@0209c000 {};
            gpio2: gpio@020a0000 {};
            gpio3: gpio@020a4000 {};
            gpio4: gpio@020a8000 {};
            gpio5: gpio@020ac000 {};
            snvslp: snvs@020b0000 {};
            fec2: ethernet@020b4000 {};
            kpp: kpp@020b8000 {};
            wdog1: wdog@020bc000 {};
            wdog2: wdog@020c0000 {};
            clks: ccm@020c4000 {};
            anatop: anatop@020c8000 
            {
                reg_3p0: regulator-3p0@120 {};
                reg_arm: regulator-vddcore@140 {};
                reg_soc: regulator-vddsoc@140 {};
            };
            usbphy1: usbphy@020c9000 {};
            usbphy2: usbphy@020ca000 {};
            tempmon: tempmon {};
            snvs: snvs@020cc000 {};
            snvs_poweroff: snvs-poweroff {};
            snvs_pwrkey: snvs-powerkey {};
        };

        epit1: epit@020d0000 {};
        epit2: epit@020d4000 {};
        src: src@020d8000 {};
        gpc: gpc@020dc000 {};
        iomuxc: iomuxc@020e0000 {};
        gpr: iomuxc-gpr@020e4000 {};
        mqs: mqs {};
        gpt2: gpt@020e8000 {};
        sdma: sdma@020ec000 {};
        pwm5: pwm@020f0000 {};
        pwm6: pwm@020f4000 {};
        pwm7: pwm@020f8000 {};
        pwm8: pwm@020fc000 {};
        
        aips2: aips-bus@02100000 
        {
            usbotg1: usb@02184000 {};
            usbotg2: usb@02184200 {};
            usbmisc: usbmisc@02184800 {};
            fec1: ethernet@02188000 {};
            usdhc1: usdhc@02190000 {};
            usdhc2: usdhc@02194000 {};
            adc1: adc@02198000 {};
            i2c1: i2c@021a0000 {};
            i2c2: i2c@021a4000 {};
            i2c3: i2c@021a8000 {};
            romcp@021ac000 {};
            mmdc: mmdc@021b0000 {};
            weim: weim@021b8000 {};
            ocotp: ocotp-ctrl@021bc000 {};
            csu: csu@021c0000 {};
            csi: csi@021c4000 {};
            lcdif: lcdif@021c8000 {};
            pxp: pxp@021cc000 {};
            qspi: qspi@021e0000 {};
            uart2: serial@021e8000 {};
            uart3: serial@021ec000 {};
            uart4: serial@021f0000 {};
            uart5: serial@021f4000 {};
            i2c4: i2c@021f8000 {};
            uart6: serial@021fc000 {};
        };

        aips3: aips-bus@02200000 
        {
            dcp: dcp@02280000 {};
            rngb: rngb@02284000 {};
            uart8: serial@02288000 {};
            epdc: epdc@0228c000 {};
            iomuxc_snvs: iomuxc-snvs@02290000 {};
            snvs_gpr: snvs-gpr@0x02294000 {};
        };
    };
};
  1. aliases 里存放的是设备树给片上外设取的别名;

  2. cpus 里存放cpu相关信息,主要是 CPU 的运行频率信息,SOC的运行频率信息,低功耗运行频率信息,时钟信息,时钟名字。

    operating-points
    fsl,soc-operating-points
    fsl,low-power-run
    clocks
    clock-names
  3. intc 则是存放和中断相关的信息,例如:中断控制器。

  4. clocks 存放与时钟相关的描述信息。

  5. soc 里存放了各种片上外设的信息,比如 ecspi14、uart18、usbphy12、i2c14 等都在这里面。

  6. chosen 子节点:其作用是为了让 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。
    在imx6ull-alientek/emmc.dts 中 chosen 节点内容如下所示:(设置输出路径为串口1)

    chosen {
        stdout-path = &uart1;
    };
    

    但是,chosen节点并没有配置 bootargs 参数,这个参数是由 uboot 来完成配置的。

设备树的节点

头文件描述的设备树结构大致如下所示:
在这里插入图片描述
设备树从根节点开始向上生长,生长到某一个具体的片上外设为止。每个节点记录了该节点共有的属性,组成一个层次分明的层次网络模型。

节点的属性

  1. compatible ,也叫”兼容性“属性,用于绑定设备和驱动,compatible 属性的值格式如下所示:

    "manufacturer,model"
    /*其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。*/
    
  2. model,该属性值是一个字符串,一般用于描述设备模块信息。

  3. status,该属性与设备状态有关。4种状态:okay,disabled,fail,fail-sss。

  4. #address-cells 和 #size-cells,这两个属性用于描述子节点的地址信息。#address-cells 属性值决定子节点 reg 属性中地址信息所占的字长(32 位),#size-cells 属性值决定子节点 reg 属性中长度信息所占的字长(32 位)

  5. reg,reg 属性的值是(address,length)对,用于描述设备地址空间资源信息,一般是某个外设的寄存器地址范围信息。

  6. ranges:ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成,该属性也可以为空。

    child-bus-address子总线地址空间的物理地址,由父节点的#address-cells
    确定此物理地址所占用的字长。
    parent-bus-address父总线地址空间的物理地址,同样由父节点的#address-cells
    确定此物理地址所占用的字长。
    length子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。
  7. name:用于记录节点名字,但已被弃用,有的老设备树文件可能有

  8. device_type:用于描述设备的 FCode,但是设备树没有 FCode。此属性只能用于 cpu 节点或者 memory 节点。

利用设备树进行设备匹配

在文件 arch/arm/include/asm/mach/arch.h 内有如下一个结构体和启动方法

/* machine_desc结构体 */
struct machine_desc {
        unsigned int            nr;             /* architecture number  */
        const char              *name;          /* architecture name    */
        unsigned long           atag_offset;    /* tagged list (relative) */
        const char *const       *dt_compat;     /* array of device tree
                                                 * 'compatible' strings */

        unsigned int            nr_irqs;        /* number of IRQs */

#ifdef CONFIG_ZONE_DMA
        phys_addr_t             dma_zone_size;  /* size of DMA-able area */
#endif

        unsigned int            video_start;    /* start of video RAM   */
        unsigned int            video_end;      /* end of video RAM     */

        unsigned char           reserve_lp0 :1; /* never has lp0        */
        unsigned char           reserve_lp1 :1; /* never has lp1        */
        unsigned char           reserve_lp2 :1; /* never has lp2        */
        enum reboot_mode        reboot_mode;    /* default restart mode */
        unsigned                l2c_aux_val;    /* L2 cache aux value   */
        unsigned                l2c_aux_mask;   /* L2 cache aux mask    */
        void                    (*l2c_write_sec)(unsigned long, unsigned);
        struct smp_operations   *smp;           /* SMP operations       */
        bool                    (*smp_init)(void);
        void                    (*fixup)(struct tag *, char **);
        void                    (*dt_fixup)(void);
        void                    (*init_meminfo)(void);
        void                    (*reserve)(void);/* reserve mem blocks  */
        void                    (*map_io)(void);/* IO mapping function  */
        void                    (*init_early)(void);
        void                    (*init_irq)(void);
        void                    (*init_time)(void);
        void                    (*init_machine)(void);
        void                    (*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
        void                    (*handle_irq)(struct pt_regs *);
#endif
        void                    (*restart)(enum reboot_mode, const char *);
};


/* 利用设备树启动Linux内核方法 */
#define DT_MACHINE_START(_name, _namestr)               \
static const struct machine_desc __mach_desc_##_name    \
 __used                                                 \
 __attribute__((__section__(".arch.info.init"))) = {    \
        .nr             = ~0,                           \
        .name           = _namestr,

在文件 arch/arm/mach-imx/mach-imx6ul.c 内有如下代码:

大致意思是利用设备树启动的方式启动Linux内核,然后匹配设备的属性,这些属性在该文件中被规定,并且在描述设备树的头文件中也被定义,只要这两者信息相同,则说明设备匹配成功。

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
        .map_io         = imx6ul_map_io,
        .init_irq       = imx6ul_init_irq,
        .init_machine   = imx6ul_init_machine,
        .init_late      = imx6ul_init_late,
        .dt_compat      = imx6ul_dt_compat,
MACHINE_END

举例:

在 imx6ull.dtsi 文件中根节点的compatible属性如下:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

在文件 arch/arm/mach-imx/mach-imx6ul.c 中有如下代码

static const char *imx6ul_dt_compat[] __initconst = {
        "fsl,imx6ul",
        "fsl,imx6ull",
        NULL,
};

系统会对这两者的内容进行比较,只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。就可以正常启动Linux内核。

设备匹配的过程

这个匹配的过程要用到另外几个文件内的函数:

函数 setup_arch 定义在文件 arch/arm/kernel/setup.c 中

函数 setup_machine_fdt 定义在文件 arch/arm/kernel/devtree.c 中

函数 of_flat_dt_match_machine 定义在文件 drivers/of/fdt.c 中

大概过程如下图所示:(该图来自正点原子)
在这里插入图片描述

向节点追加或修改内容

向设备树的某个节点中追加内容需要在 .dts 文件中追加。

举例:如下图,该图内容在 arch/arm/boot/dts/imx6ull-alientek-emmc.dts 文件中,内容是 I2C节点的相关属性,在该节点下有两个子节点:mag3110@0e 和 fxls8471@1e ,分别是飞思卡尔的磁力计和一个线性加速度传感器,它们的通讯协议都是I2C协议,所以追加在I2C节点上。在这里插入图片描述

设备树在系统中的体现

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device/tree 目录下根据节点名字创建不同文件夹。
在这里插入图片描述

Linux内核解析DTB文件

流程图来自正点原子
在这里插入图片描述

总结:

浅显的说,uboot引导linux内核启动,内核启动后就会加载设备树,让内核与各种硬件外设对接,然后我们编写驱动程序,驱动程序与设备树上的节点对接,从而控制某个开发板上的外设并让其运行起来。