Linux设备树(1)--概述

adtxl
2021-04-08 / 0 评论 / 1,243 阅读 / 正在检测是否收录...

1.为什么要用设备树

Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。

比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如exynos4412参考板的板级设备树文件就是"arch/arm/boot/dts/exynos4412-origen.dts"。这个文件可以通过$make dtbs命令编译成二进制的.dtb文件供内核驱动使用。

基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。

设备树由一系列被命名的节点(Node)和属性(Property),而节点本身可包含子节点。所谓属性,其实就是成对出现的名称和值。在设备树中,可描述的信息包括(原来这些信息大多被硬编码在内核中):

  • CPU的数量和类别
  • 内存基地址和大小
  • 总线和桥
  • 外设连接
  • 中断控制器和中断使用情况
  • GPIO控制器和GPIO使用情况
  • 时钟控制器和时钟使用情况

它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这颗树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform\_device、i2c\_client、spi\_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。

2.设备树的组成和结构

整个设备树牵涉面比较广,既增加了新的用于描述设备硬件信息的文本格式,又增加了编译这个文本的工具,同时Bootloader也需要支持将编译后的设备树传递给Linux内核。

2.1 DTS、DTC和DTB等

1. DTS

文件.dts是一种ASCII文本格式的设备树描述,一个.dst文件对应一个ARM设备,一般放置在arch/arm/boot/dts/目录中。
由于一个SOC可能对应多个设备,这些.dts文件中势必包含许多共同的部分,linux内核为了简化,把公用的部分提炼为.dtsi,类似于C语言的头文件。其它设备的.dts就包括这个.dtsi。

/include/ "vexpress-v2m.dtsi"

2. DTC(Device Tree Compiler)

DTC是将.dts编译为.dtb的工具。DTC的源代码位于内核的scripts/dtc目录中,在Linux内核使用了设备树的情况下,编译内核的时候主机工具DTC会被编译出来,对应于scripts/dtc/Makefile中"hostprogs-y := dtc"这一hostprogs的编译目标。也可以在ubuntu中单独安装

sudo apt-get install device-tree-compiler

在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SOC被选中后,哪些.dtb文件会被编译出来,

3. DTB(Device Tree Blob)

文件.dtb是.dts被DTC编译后的二进制格式的设备树描述,可由Linux内核解析,当然U-boot这样的bootloader也是可以识别.dtb的。

4. 绑定(Binding)

对于设备树的节点和属性是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。在这个.txt文件中,需要描述对应节点的兼容性、必需的属性和可选的属性。

这些文档位于内核的Documentation/devicetree/bindings目录下,其下又分为很多子目录。
Linux内核下的scripts/checkpatch.pl会运行一个检查,如果有人在设备树中新添加了compatible字符串,而没有添加相应的文档进行解释,checkpatch程序会报出警告:
UNDOCUMENTED_DT_STRINGGDT compatible string xxx appears un-documented
因此,程序员要养成及时写DT Binding文档的好习惯。

5. Bootloader

Uboot设备从v1.1.3开始支持设备树,其对ARM的支持则和ARM内核支持设备树同期完成。
为了使用设备树,需要在编译Uboot的时候在config文件中加入:
define CONFIG_OF_LIBFDT
在Uboot中,可以从NAND、SD或者TFTP等任意介质中将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在Uboot中运行fdt addr命令设置.dtb的地址
Uboot> fdt addr 0x71000000
fdt的其他命令就变得可以使用,如fdt resize、fdt print等
对于ARM来讲,可以通过bootz kernel_addr initrd_address dtb_address的命令来启动内核,即dtb_address作为bootz或者bootm的最后一次参数,第一个参数为内核映像的地址,第二个参数为initrd的地址,若不存在initrd,可以用‘-’符号代替。

2.2 根节点兼容性

Linux内核通过根节点“/”的兼容属性即可判断它启动的是什么设备。在真实项目中,这个顶层设备的兼容属性一般包括两个或者两个以上的兼容性字符串,首个兼容性字符串是板子级别的名字,后面一个兼容性是芯片级别的名字。
ARM Linux3.x在引入设备树之后,ARM Linux对不同的电路板会建立由DT_MACHINE_START和MACHINE_END包围起来的针对这个设备的一系列回调函数。其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性兼容关系。如果,Bootloader传递给内核的设备树中根节点的兼容属性出现在某设备的.dt_compat表中,相关的设备就与对应的兼容匹配,从而引发这一设备的一系列初始化函数被执行。

2.3 设备节点兼容性

在.dts文件的每一个设备节点中,都有一个兼容属性,兼容属性用于驱动和设备的绑定。兼容属性是一个字符串的列表,列表中的第一个字符串表征了节点代表的确切设备,形式为 "<manufacturer>,<model>" 其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。

使用设备树后,驱动需要与.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表

2.4 设备节点及label的命名

节点的命名方式,<name>[@<unit-address>]
<>中的内容是必选项,[]中的则是可选项。name是一个ASCII字符串,用于描述节点对应的设备类型,如3com Ethernet适配器对应的节点name宜为ethernet,而不是3com509。如果一个节点描述的设备有地址,而应该给出@unit-address。多个相同类型的设备节点的name可以一样,只要unit-address不同即可。
对于挂在内存空间的设备而言,@字符后跟的一般就是该设备在内存空间的基地址,例如

memory@80000000 {
       device_type = "memory";
       reg = <0 0x80000000 0 0xc0000000>;
};

上述节点的reg属性的开始位置与@后面的地址一样。

对于挂载在I2C总线上的外设而言,@后面一般跟的是从设备的I2C地址,例如

i2c0: i2c@58780000 {
    compatible = "socionext,uniphier-fi2c";
    status = "disabled";
    reg = <0x58780000 0x80>;
    #address-cells = <1>;
    #size-cells = <0>;
    interrupts = <0 41 4>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c0>;
    clocks = <&peri_clk 4>;
    resets = <&peri_rst 4>;
    clock-frequency = <100000>;
};

上述节点的reg属性标识的I2C从地址与@后面的地址一样。
我们还可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的。在上面的例子中,我们i2c0就是一个label,我们就可以通过&i2c0的形式访问这个节点。

例如,在arch/arm/boot/dts/omap5.dtsi中,第3组GPIO有gpio3这个label,如下所示

gpio3: gpio@48057000 {
        compatible = "ti,omap4-gpio";
        reg = <0x48057000 0x200>;
        interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
        ti,hwmods = "gpio3";
        gpio-controller;
        #gpio-cells = <2>;
        interrupt-controller;
        #interrupt-cells = <2>;
};

而hsusb2_phy这个USB的PHY复位GPIO用的是这组GPIO中的一个,所以它通过phandle引用了"gpio3",如下所示

/* HS USB Host PHY on PORT2 */
hsusb2_phy: hsusb2_phy {
        compatible = "usb-nop-xceiv"
        reset-gpios = <&gpio3 12 GPIO_ACTIVE_LOW>;    /* gpio3_76_HUB_RESET */
};

hsusb2_phy则通过&gpio3引用了这个节点,表明自己要使用这一组GPIO中的第12个GPIO。很显然,这种phandle引用其实表明硬件之间的一种关联性。
在上述代码中,引用了GPIO_ACTIVE_LOW这个类似C语言的宏。文件.dts的编译过程确实支持C的预处理,相应的.dts文件也包含GPIO_ACTIVE_NOW这个宏定义的头文件:

#include <dt-bindings/gpio/gpio.h>

对于ARM而言,dt-bindings头文件位于内核的include/dt-bindings目录中。
从内核的scripts/Makefile.lib这个文件可以看出,文件.dts的编译过程确实是支持C预处理的。

cmd_dtc = mkdir -p $(dir ${dtc-tmp}) ; \
    $(CPP) $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $< ; \
    $(DTC) -O dtb -o $@ -b 0 \
        $(addprefix -i,$(dir $<) $(DTC_INCLUDE)) $(DTC_FLAGS) \
        -d $(depfile).dtc.tmp $(dtc-tmp) ; \
    cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)

它是先做了$(CPP) $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $<,再做的.dtc编译。

2.5 地址编码

可寻址的设备使用如下信息在设备树中编码地址信息

reg
    #address-cells
    #size-cells

其中,reg的组织形式为reg = <address1 length1 [address2 length2] [address2 length2] [address3 length3]...>
其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length的意义则意味着从address到address+length-1的地址范围都属于该节点。若#size-cells=0,则length字段为空。
address和length字段是可变长的,父节点的#address-cells和#size-cells分别决定了子节点reg属性的address和length字段的长度。

/ {
    compatible = "socionext,uniphier-ld20";
    #address-cells = <2>;
    #size-cells = <2>;
    interrupt-parent = <&gic>;

    cpus {
        #address-cells = <2>;
        #size-cells = <0>;

        cpu-map {
            cluster0 {
                core0 {
                    cpu = <&cpu0>;
                };
                core1 {
                    cpu = <&cpu1>;
                };
            };

            cluster1 {
                core0 {
                    cpu = <&cpu2>;
                };
                core1 {
                    cpu = <&cpu3>;
                };
            };
        };
        cpu0: cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a72", "arm,armv8";
            reg = <0 0x000>;
            clocks = <&sys_clk 32>;
            enable-method = "psci";
            operating-points-v2 = <&cluster0_opp>;
            #cooling-cells = <2>;
        };

        cpu1: cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a72", "arm,armv8";
            reg = <0 0x001>;
            clocks = <&sys_clk 32>;
            enable-method = "psci";
            operating-points-v2 = <&cluster0_opp>;
            #cooling-cells = <2>;
        };

        cpu2: cpu@100 {
            device_type = "cpu";
            compatible = "arm,cortex-a53", "arm,armv8";
            reg = <0 0x100>;
            clocks = <&sys_clk 33>;
            enable-method = "psci";
            operating-points-v2 = <&cluster1_opp>;
            #cooling-cells = <2>;
        };

        cpu3: cpu@101 {
            device_type = "cpu";
            compatible = "arm,cortex-a53", "arm,armv8";
            reg = <0 0x101>;
            clocks = <&sys_clk 33>;
            enable-method = "psci";
            operating-points-v2 = <&cluster1_opp>;
            #cooling-cells = <2>;
        };
    };

如上面这个设备树节点,根节点的#address-cells = <2>;#size-cells = <2>;决定了serial、gpio、spi等节点的address和length字段的长度分别为2。
cpus节点的#address-cells = <2>;#size-cells = <0>;决定了cpu子节点的address为2,而length为空。

2.6 中断连接

设备树中还可以包含中断连接信息,对于中断控制器而言,它提供如下属性:
interrupt-controller--这个属性为空,中断控制器应该加上此属性表明自己的身份

2.7 GPIO、时钟、pinmux连接

除了中断以外,在ARM Linux中时钟、GPIO、pinmux都可以通过.dts中的节点和属性进行描述。

1. GPIO

例如,对于GPIO控制器而言,其对应的设备节点需声明gpio-controller属性,并设置#gpio-cells的大小,例如下面的gpio设备节点

gpio: gpio@55000000 {
        compatible = "socionext,uniphier-gpio";
        reg = <0x55000000 0x200>;
        interrupt-parent = <&aidet>;
        interrupt-controller;
        #interrupt-cells = <2>;
        gpio-controller;
        #gpio-cells = <2>;
        gpio-ranges = <&pinctrl 0 0 0>,
        <&pinctrl 96 0 0>,
        <&pinctrl 160 0 0>;
        gpio-ranges-group-names = "gpio_range0",
        "gpio_range1",
        "gpio_range2";
        ngpios = <205>;
        socionext,interrupt-ranges = <0 48 16>, <16 154 5>,
        <21 217 3>;
};

其中,#gpio-cells为2,第1个cell为GPIO号,第2个为GPIO的极性。为0的时候是高电平有效,为1的时候则是低电平有效。

2. 时钟

时钟和GPIO也是类似的,时钟控制器的节点被使用时钟的模块所引用:

       sysctrl@61840000 {
            compatible = "socionext,uniphier-ld20-sysctrl",
                     "simple-mfd", "syscon";
            reg = <0x61840000 0x10000>;

            sys_clk: clock {
                compatible = "socionext,uniphier-ld20-clock";
                #clock-cells = <1>;
            };

            sys_rst: reset {
                compatible = "socionext,uniphier-ld20-reset";
                #reset-cells = <1>;
            };

            watchdog {
                compatible = "socionext,uniphier-wdt";
            };

            pvtctl: pvtctl {
                compatible = "socionext,uniphier-ld20-thermal";
                interrupts = <0 3 4>;
                #thermal-sensor-cells = <0>;
                socionext,tmod-calibration = <0x0f22 0x68ee>;
            };
        };

使用

        eth: ethernet@65000000 {
            compatible = "socionext,uniphier-ld20-ave4";
            status = "disabled";
            reg = <0x65000000 0x8500>;
            interrupts = <0 66 4>;
            pinctrl-names = "default";
            pinctrl-0 = <&pinctrl_ether_rgmii>;
            clock-names = "ether";
            clocks = <&sys_clk 6>;
            reset-names = "ether";
            resets = <&sys_rst 6>;
            phy-mode = "rgmii";
            local-mac-address = [00 00 00 00 00 00];
            socionext,syscon-phy-mode = <&soc_glue 0>;

            mdio: mdio {
                #address-cells = <1>;
                #size-cells = <0>;
            };
        };

<&sys_clk 6>里的6这个index是与相应时钟驱动中clk的表的顺序对应的

3. pinmux

在设备树中,某个设备节点使用的pinmux的引脚群是通过phandle来指定的。例如,在arm/boot/dts/uniphier-pinctrl.dtsi中包含所有引脚群的描述

&pinctrl {
    pinctrl_aout: aout {
        groups = "aout";
        function = "aout";
    };

    pinctrl_ain1: ain1 {
        groups = "ain1";
        function = "ain1";
    };

    pinctrl_ain2: ain2 {
        groups = "ain2";
        function = "ain2";
    };

    pinctrl_ainiec1: ainiec1 {
        groups = "ainiec1";
        function = "ainiec1";
    };

    pinctrl_aout1: aout1 {
        groups = "aout1";
        function = "aout1";
    };

    pinctrl_aout2: aout2 {
        groups = "aout2";
        function = "aout2";
    };

    pinctrl_aout3: aout3 {
        groups = "aout3";
        function = "aout3";
    };

    pinctrl_aoutiec1: aoutiec1 {
        groups = "aoutiec1";
        function = "aoutiec1";
    };

    pinctrl_aoutiec2: aoutiec2 {
        groups = "aoutiec2";
        function = "aoutiec2";
    };
};
...

3. 由设备树引发的BSP和驱动变更

有了设备树后,不再需要大量的板级信息,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx中实施如下事情。

  • 注册platform,绑定resource,即内存、IRQ等板级信息
  • 注册i2c_board_info,指定IRQ等板级信息
  • 注册spi_board_info,指定IRQ等板级信息
  • 多个针对不同电路板的设备,以及相关的回调函数
  • 设备与驱动的匹配方式
    使用设备树后,驱动需要与在.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。新的驱动、设备的匹配变成了设备树节点的兼容属性和设备驱动中的OF匹配表的匹配。
  • 设备的平台数据属性化

4. 常用的OF API

  • of_machine_is_compatible判断根节点的兼容性
    int of_machine_is_compatible(const char *compat)
    此API判断目前运行的板子或者SoC的兼容性,它匹配的是设备树根节点下的兼容属性
  • of_device_is_compatible判断设备节点的兼容性
    int of_device_is_compatible(const struct device_node *device, const char *compat)
    此函数用于判断设备节点的兼容属性是否包含compat指定的字符串。这个API多用于一个驱动支持两个以上设备的时候。
    当一个驱动支持两个或多个设备的时候,这些不同.dts文件中设备的兼容属性都会写入驱动OF匹配表。因此驱动可以通过Bootloader传递给内核设备树中的真正节点的兼容属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。
  • 寻找节点

    struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)

    根据兼容属性,获得设备节点。遍历设备树中的设备节点,看看哪个节点的类型、兼容属性与本函数的输入参数匹配,在大多数情况下,from、type为NULL,则表示遍历了所有的节点。

  • 读取属性

    int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);
    int of_property_read_u16_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);
    int of_property_read_u32_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);
    int of_property_read_u64_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);

    读取设备节点np的属性名,为propname,属性类型为8、16、32、64位整型数组。对于32位处理器来讲,最常用的是of_property_read_u32_array()。
    在有些情况下,整型属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出更见简单的读单一整型属性的API,它们为int of_property_read_u8()等
    除了整型属性外,字符串属性也比较常用,其对应的API包括:

    int of_property_read_string(struct device_node *np, const char *propname, const char **out_string);
    int of_property_read_string_index(struct device_node *np, const char *propname, int index, const char **out_string);

    前者读取字符串属性,后者读取字符串数组属性中的第index个字符串。
    除整型、字符串以为的最常用属性类型就是布尔型,其对应的API为,

    static inline bool of_property_read_bool(const struct device_node *np, const char *propname);

    如果设备节点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。

  • 内存映射

    void __iomem *of_iomp(struct device_node *node, int index);

    上述API可以直接通过设备节点进行设备内存空间的ioremap(),index是内存段的索引。若设备节点的reg属性有多段,可通过index标识要ioremap()的是哪一段,在只有1段的情况,index为0。采用设备树后,一些设备驱动通过hcof_iomap()而不再通过传统的ioremap()进行映射,当然,传统的ioremap()的用户也不少。

    int of_address_toresource(struct device_node *dev, int index, struct resource *r);
    上述API通过设备节点获取与它对应的内存资源的resource结构体。其本质是分析reg属性以获取内存基地址、大小等信息并填充到struct resource *r参数指向的结构体中。
  • 解析中断

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

    通过设备树获得设备的中断号,实际上是从.dts中的interrupts属性里解析出中断号。若设备使用了多个中断,index指定中断的索引号。

  • 获取与节点对应的platform_device

    struct platform_device *of_kind_device_by_node(struct device_node *np)

    在可以拿到device_node的情况下。如果想反向获取对应的platform_device,可使用上述API。
    当然,在已知platform_device的情况下,想获取device_node则易如反掌,例如:

    static int sirfsoc_dma_probe(struct platform_device *op)
    {
      struct device_node *dn = op->dev.of_node;
      ...
    }
0

评论 (0)

取消