设备树基础语法与实例分析


DTS和DTSI

.dts文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个.dts文件对应一个ARM的machine。
.dtsi文件作用:由于一个SOC可能有多个不同的电路板,而每个电路板拥有一个 .dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include *.dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。

DTB

DTC编译*.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。
目录:
Linux/kernel/arch/arm/boot/dts
Linux/kernel/arch/arm64/boot/dts

dtc

(编译内核源码时会编译出该工具,看 .config 中 CONFIG_DTC 选项是否编进内核,或者在 linux/kernel/scripts/dtc/ 中有源码,看 linux/kernel/scripts/ 目录下的makefile 会有:

subdir -S(CONFIG_DTC)+= dtc

编译设备树:

dtc -I dts -O dtb -o xxx.dtb xxx.dts

反编译设备树:

dtc -I dtb -O dts -o xxx.dts xxx.dtb

使用命令:make dtbs
可以一次性编译 Linux/kernel/arch/arm/boot/dts/ 目录下的选中的SOC的所有dtc,但是很多并非我们需要的。
在VS代码编辑器中可以安装 DeviceTree 插件。

一、语法篇

/dts-v1/;    #版本号,必须写,否则编译报错
/{           #根节点,一个设备树文件仅有一个
    [label]:node-name[@uint-address]{    /* label:标签,用于引用节点;node-name:节点名;uint-address:设备地址,没实际意义 */
        ......
        [child nodes]            /* 子节点,格式和节点一样 */
    };
    /* 同级的结点名不能相同 */
};

案例 1:

/dts-v1/;
/{
    uart: serial@02288000
};

/* uart 是节点的标签/别名, serial@02288000 是结点名称 */

reg 属性

描述地址信息,例如寄存器地址

 reg = <address1 length1 address2 length2 address3 length3 ......>
  /* address:寄存器地址,length:地址长度 */

案例2:

/dts-v1/;
/{
    uart: serial@02288000{
        reg = <0x2200000 0x4000
               0x3300000 0x4000>
    };
};

reg 限制(注意是子节点)

#address-cell /* 限制 子 节点中的地址数 /
#size-cell /
限制 子 节点中的长度数 */
案例3:

/dts-v1/;
/{
    node_1: node1@02288000{
        #address-cell = <1>
        #size-cell = <0>
        serial_1{
            reg = <0>;    /* 显然这个 0 对应的是地址,因为size-cell为0,代表没有长度信息 */
        };
    };
    node_2: node2@02299000{
        #address-cell = <1>
        #size-cell = <1>
        serial_1{
            reg = <0x2200000 0x4000>;    /* 一个地址,一个长度 */
        };
    };
    node_3: node3@02200000{
        #address-cell = <2>
        #size-cell = <0>
        serial_1{
            reg = <0x00 0x01>;    /* 两个地址 */
        };
    };
};

包含自写的设备树文件时,多个根节点会被合成一个;如果自写的设备树把 #address-cell 和 #size-cell 放在根节点的下一级,将会在合并后限制根节点的所有子节点。
解决这个问题需要在自己的设备树中添加一个无关节点,再把自己写的节点作为无关节点的子节点添加。

model 属性

字符串,可以描述设备的名字或用途

status 属性

status = "okay"       /* 有的厂商的设备树是 ok ,表示设备可用 */
status = "disable"    /* 不可用 */

compatible 属性

用来替换 driver device 分离后的 device 部分,用来与驱动匹配,匹配成功后执行驱动中的 probe 函数

compatible = "driver1", "driver2", ......;    /* 没有找到第一个驱动就继续找下一个... */

aliases 节点

定义别名,方便引用节点。
案例4:

/dts-v1/;
/{
    #address-cell = <1>
    #size-cell = <1>
    
    aliases{
        led_1 = &led;    /* 之后使用 led_1 相当于 gpio@20202010 */
    }
    
    led: gpio@20202010{
        compatible = "led";
        reg = <20202010 0x4000>;
        status = "okay";
    };
};

编译成 dtb 文件,再反编译回 dts 文件,可以看到:

/dts-v1/;
/{
    #address-cell = <1>
    #size-cell = <1>
    
    aliases{
        led_1 = "/gpio@20202010"
    }
    
    gpio@20202010{
        compatible = "led";
        reg = <20202010 0x4000>;
        status = "okay";
    };
};

案例5:

/dts-v1/;
/{
    #address-cell = <1>
    #size-cell = <1>
    
    aliases{
        led_1 = "/gpio@20202010"
    }
    
    gpio@20202010{
        compatible = "led";
        reg = <20202010 0x4000>;
        status = "okay";
    };
};

针对没有标签的节点,aliases 里面也可以直接赋予路径,效果和案例4相同。

chosen 节点

uboot 将该节点的参数传递给内核,重点是 bootargs 参数。
chosen 节点必须是根节点的子节点。
案例6:

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

chosen调用流程:

bootz命令
    do_bootz()
        do_bootm_states()
            boot_selected_os()
                boot_fn()
                    boot_prep_linux()    //启动Linux之前做一些其他处理,例如在在 bootargs 子节点存放 bootargs 环境变量
                        image_setup_linux()
                            image_setup_libfdt()
                                fdt_chosen()    //最终用该函数在 chosen 节点中添加 bootargs 属性

device_type 属性

属性值是字符串,只用于 cpu、memory 节点进行描述。
案例7:

cpu1: cpu@1{
    device_type = "cpu";
    ......
}

自定义属性

案例8:

/* 自定义一个管脚标号的属性 pinnum */
pinnum = <0 1 2 3 4>;

驱动可以拿到设备树中的任意节点的任意数据,至于有没有用,用来做什么,都是驱动开发的事情了,所以开放自定义属性合理。

二、实例分析篇

中断

三个处理对同一款SPI屏幕芯片的中断描述
中断类型的宏定义在:include\dt-bindings\interrupt-controller/irq.h

#define IRQ_TYPE_NONE			0
#define IRQ_TYPE_EDGE_RISING	1
#define IRQ_TYPE_EDGE_FALLING	2
#define IRQ_TYPE_EDGE_BOTH		(IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH		4
#define IRQ_TYPE_LEVEL_LOW		8

瑞芯微

/* 原厂中断节点 */
gpio0: gpio@fdd60000{
    compatible = "rockchip, gpio-bank";
    reg = <0x0 0xfdd60000 0x0 0x100>
    interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;    /* spi中断 33号中断线 高电平出发
    clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>;
    
    gpio-controller;                                  /* 表示该节点是 gpio 控制器 */
    #gpio-cell = <2>;
    gpio-ranges = <&pinctrl 0 0 32>;
    interrupt-controller;                             /* 表示该节点是 中断 控制器 */
    #interrupt-cell = <2>;
};

/* 开发者修改的中断节点 */
rt5x06: ft5x06@38{
    status = "disable";
    compatible = "edt, edt-ft5x06";
    reg = <0x38>;
    touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
    interrupt-parent = <&gpio0>;                        /* 引用了 gpio0 节点 */
    interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;           /* 受 interrupt-cell 限制,只能有两个属性 */
    reset-gpios = <&gpio RK_PB6 GPIO_ACTIVE_LOW>;
    touchscreen-size-x = <800>;
    touchscreen-size-y = <1200>;
    touch_type = <1>;
};

恩智浦

/* 原厂中断节点 */
gpio1: gpio@0209c000{
    compatible = "fsl, imx6ul-gpio", "fsl, imx35-gpio";
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;                                  /* 表示该节点是 gpio 控制器 */
    #gpio-cell = <2>;
    interrupt-controller;                             /* 表示该节点是 中断 控制器 */
    #interrupt-cell = <2>;
};
/* 开发者修改的中断节点 */
edt-ft5x06@38{
    compatible = "edt, edt-ft5306", "edt, edt-ft5x06", "edt, edt-ft5406";
    pinctrl-name = "default";
    pinctrl-0 = <&ts_int_pin &ts_reset_pin>;
    reg = <0x38>;
    interrupt-parent = <&gpio1>;                        /* 引用了 gpio1 节点 */
    interrups = <9 0>;                                  /* 发现这个和RK的不同,RK的interrupts 的属性是中断号 + 触发方式宏定义,这里的触发方式是数字,其实两者是一样的 */
    reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
    irq-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
    status = "disable";
};

三星

/* 原厂的中断节点 */
gpio_c: gpioc{
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};
/* 开发者修改的中断节点 */
ft5x06: ft5x06@38{
    compatible = "edt, edt-ft5x06";
    reg = <0x38>;
    pinctrl-name = "default";
#if defined(RGB_XXXXXXX) || defined(RGB_XXXXXX)    /* 类似C语言的预编译选项 */
    pinctrl-0 = <&tsxx_irq>;
    interrupt-parent = <&gpio_c>;
    interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
#endif
#if defined(RGB_XXXXXXX) || defined(RGB_XXXXXX)    /* 在两个条件中根据宏参数选择一个编译 */
    pinctrl-0 = <&gtxx_irq>;
    interrupt-parent = <&gpio_b>;
    interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
#endif
    reset-gpios = <&gpio_e 30 0>;
};

可以看到不同厂商的宏定义是自由编写的,我们需要记住的不是宏定义,而是宏定义代表的意义。
开发者添加自己的设备,就是引用厂商的节点,在厂商的基础上加上自己的修改。

其他写法

gic1: interrupt-controller@20220101{
    ......
    interrupt-controller;
};
gic2: interrupt-controller@20220102{
    ......
    interrupt-controller;
    interrupt-parent = <&gic1>        /* 中断控制器引用了中断控制器,表示的是中断控制器的级联 */
};
interrupt@38{
    ......
    interrupt-extended = <&gic1 9 1>, <&gic2 10 1>;    /* interrupt-extended 可以定义多组中断 */
};

总结:

  • 中断控制器中,必须有一个属性 #interrupt-cells 表示其他节点使用这个中断控制器需要几个 cell 来表示使用哪一个中断。
  • 中断控制器中,必须有一个属性 interrupt-controller 表示它是中断控制器。
  • 在设备树中使用中断,需要使用属性 interrupt-parent = <&中断控制器> 表示中断信号链接的是哪个中断控制器。
  • 声明引用的中断控制器后,需要用 interrupts 声明中断引脚和触发方式,至于 interrupts 中有几个 cell ,需要按照 interrupt-parent 的要求。

还有很多没见过的属性,保持疑问,接着理解。

实战:描述中断资源

将驱动中的描述device资源部分转换为设备树描述:

static struct resource my_device_resources[] = {
    [0] = {
        .start  = 0xFDD60000,
        .end    = 0XFDD60004,
        .flags  = IORESOURCE_MEM,
    },
    [1] = {
        .start  = 13,
        .end    = 13,
        .flags  = IORESOURCE_IRQ,
    },
};

在这里插入图片描述
中断管脚为 TP_INT_L_GPIO0_B5,GPIO控制器是 0(GPIO0)

/dts-v1/;
/{
    ft5x06@38{    /* 为了直观,直接写芯片名和设备地址即可 */
        compatible = "edt, edt-ft5206"; /* 驱动源码中的匹配表中compatible 成员的字符串,需要driver名和device名一样才能匹配到该设备 */
        interrupt-parent = <&gpio0>;    /* 看内核源码中 SOC 的 dtsi 文件中的gpio0中断控制器是否名为 gpio0 */
        interrupts = <13 >              /* 管脚对应的名称,对应关系可以在内核中的 dt-bindings/pinctrl/主板名.h 中找到;RK 中的宏定义:#define RK_PB5 13,不用13用PB5也可以,使用宏定义需要把 主板名.h 包含一下*/
                                        /* 再打开 dt-bindings/pinctrl/irq.h 可以看到各种触发方式的宏定义 */
    }
}

时钟

驱动解析设备树中时钟的信息,从而完成时钟的初始化和使用。
设备树中,时钟为 消费者 和 生产者。
#clock-cells 代表时钟的路数,为 0 时,代表有一路时钟输出;为 1 时,代表有多路时钟输出。

/* 消费者属性 */
osc24m: osc24m{
    ......
    clock-frequency = <24000000>;    /* 时钟的大小,这里频率为 24M */
    clock-output-names = "osc24m";   /* 定义输出时钟名 */
    #clock-cells = <0>;
};

clock: clock{
    #clock-cells = <1>;              /* 为 1 时有多路时钟输出 */
    clock-output-names = "clock1", "clock2";
};

/* 生产者属性 */
cru: clock-controller@fdd20000{
    #clock-cells = <1>;            /* 声明了输出多路时钟信号 */
    /* assigned-clocks 和 assigned-clock-rates 一般成对使用,当输出多路时钟时,为每路时钟编号 */
    assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKVDEC_PRE>;    /* 使用 pmucru 模块输出 CLK_RTC_32K 时钟信号;使用 cru 模块输出 ACLK_RKVDEC_PRE 时钟信号
    assigned-clock-rates = <32768>, <300000000>;                        /* 声明前面两路时钟的输出频率 */
};

clock-indices 属性指定索引号(index),如果不提供这个属性,那么 clock-output-names 和 index 的对应关系就是 0,1,2…如果这个关系不是线性的,可以通过 clock-indices定义映射。
在这里插入图片描述

CPU

cpus 节点
cpus 节点为物理 cpu 的布局。
cpu-map 节点
单核处理器不需要cpu-map节点,cpu-map 用于描述大小核架构的处理器中,其父节点必须为 cpus 节点,子节点必须是一个或多个 cluster 和 socket 节点。
socket 节点
socket 节点描述的是板卡上的 CPU 插槽。主板有几个插槽就有几个socket节点。其子节点必须为一个或多个 cluster 节点。当有多个CPU插槽时,socket节点的命名方式必须是 socketN(N = 0,1,2…)。
cluster 节点
cluster 节点描述的是 CPU 集群。RK3399 的 CPU 架构为双核 A73 + 四核 A53,其中四核 A53 和 双核 A73 各表示一个集群。集群命名必须为 clusterN(N = 0,1,2…)。
core 节点
core 节点用来描述 CPU,如果是单核CPU,则 core 节点就是cpus节点中的子节点。命名格式为 coreN(N = 0,1,2…)。
thread 节点
该节点必须是 core 的子节点,用来描述处理器的线程。命名格式为 threadN(N = 0,1,2…)。

实例:RK3399 的设备树,由于它是大小核架构的 CPU,所以可以用 cpu-map 节点,两个集群代表两个CPU,每个CPU又有多个核。
在这里插入图片描述

GPIO

  • GPIO控制器中,必须有一个属性 #gpio-cells,表示其他节点使用这个GPIO控制器需要几个cell来描述。
  • GPIO控制器中,必须有一个属性 gpio-controller 声明。
  • 使用GPIO需要用属性 data-gpios=<&gpio控制器标签 gpio引脚标号 高低电平>
    设置gpio属性,该属性也可为自定义属性。
    案例:
gpio1: gpio1{
    gpio-controller;
    #gpio-cells = <2>;    /* 需要两个属性来描述 gpio,两个属性是:gpio引脚标号  高低电平 */
};

data-gpios = <&gpio1 12 0>, <&gpio 15 0>;

ngpios
表示当前 GPIO 控制器下有多少个 pin 脚
gpio-reserved-ranges
用于指定保留的 pin 脚,例如 gpio-reserved-ranges <2 3> 表示当前 GPIO 控制器的 2,3,4 pin 脚为预留 pin,即第一个参数为起始 pin,第二个参数为 pin 脚数量。
gpio-line-names
用于给 GPIO 控制器的 pin 脚命名,控制器有多少 pin,就有多少个名字,名字用逗号隔开。
在这里插入图片描述
gpio-ranges

gpio-ranges = <&foo 0 128 12>;
解析:将当前GPIO控制器中的0~11号管脚对应到 foo GPIO控制器中的128~139号管脚,12 表示的是管脚数量

gpio-ranges通常和pinctrl使用,GPIO系统中有引脚号,Pinctrl子系统中也有自己的引脚号,2个号码要建立映射关系,当gpio和管脚编号不对应时就要用 gpio-ranges对应起来;

实战:用设备树点亮LED灯

在这里插入图片描述
根据电路图,确定几个信息:管脚输出低电平点亮,控制器是GPIO0,编号是PB7
这里用的 rk3568 的开发板做实验,瑞芯微的管教定义可以通过瑞芯微的内核文件:kernel/include/dt-bindings/pinctrl/rockchip.h 查看:

#define RK_PA0		0
#define RK_PA1		1
#define RK_PA2		2
#define RK_PA3		3
#define RK_PA4		4
#define RK_PA5		5
#define RK_PA6		6
#define RK_PA7		7
#define RK_PB0		8
#define RK_PB1		9
#define RK_PB2		10
#define RK_PB3		11
#define RK_PB4		12
#define RK_PB5		13
#define RK_PB6		14
#define RK_PB7		15
#define RK_PC0		16
#define RK_PC1		17
#define RK_PC2		18
#define RK_PC3		19
#define RK_PC4		20
#define RK_PC5		21
#define RK_PC6		22
#define RK_PC7		23
#define RK_PD0		24
#define RK_PD1		25
#define RK_PD2		26
#define RK_PD3		27
#define RK_PD4		28
#define RK_PD5		29
#define RK_PD6		30
#define RK_PD7		31

#define RK_FUNC_GPIO	0
#define RK_FUNC_0		0
#define RK_FUNC_1		1
#define RK_FUNC_2		2
#define RK_FUNC_3		3
#define RK_FUNC_4		4
#define RK_FUNC_5		5
#define RK_FUNC_6		6
#define RK_FUNC_7		7
#define RK_FUNC_8		8
#define RK_FUNC_9		9
#define RK_FUNC_10		10
#define RK_FUNC_11		11
#define RK_FUNC_12		12
#define RK_FUNC_13		13
#define RK_FUNC_14		14
#define RK_FUNC_15		15

gpio简单属性设置可以在 kernel/include/dt-bindings/pinctrl/rockchip.h 中查看:

/* Bit 0 express polarity */
#define GPIO_ACTIVE_HIGH 0
#define GPIO_ACTIVE_LOW 1

/* Bit 1 express single-endedness */
#define GPIO_PUSH_PULL 0
#define GPIO_SINGLE_ENDED 2

/* Bit 2 express Open drain or open source */
#define GPIO_LINE_OPEN_SOURCE 0
#define GPIO_LINE_OPEN_DRAIN 4

/*
 * Open Drain/Collector is the combination of single-ended open drain interface.
 * Open Source/Emitter is the combination of single-ended open source interface.
 */
#define GPIO_OPEN_DRAIN (GPIO_SINGLE_ENDED | GPIO_LINE_OPEN_DRAIN)
#define GPIO_OPEN_SOURCE (GPIO_SINGLE_ENDED | GPIO_LINE_OPEN_SOURCE)

/* Bit 3 express GPIO suspend/resume and reset persistence */
#define GPIO_PERSISTENT 0
#define GPIO_TRANSITORY 8

设备树编写:

led: led@1{
    compatible = "led";    /* 驱动名,这里只是点亮,不需要驱动也没事,一样可以输出低电平 */
    data-gpios = <&gpio0 RK_PB7 1>;    /* 引用了gpio0 控制器,需要按照 gpio0 控制器的规则编写 */
};

pinctrl

目的是为了统一各个芯片原厂的 pin 管理,所以 pinctrl 子系统的驱动由芯片原厂 BSP 工程师编写(包括设备树)。
pinctrl 子系统用来管理 GPIO 引脚,它主要完成了:

  • 引脚枚举与命名
  • 引脚复用
  • 引脚配置

pinctrl 客户端

客户端语法是固定的,所有平台都是相同的,主要包括两个属性:pinctrl-names 和 pinctrl-x(x 为数字 0,1,2…)
pinctrl-name 属性表示设备的状态,
pinctrl-x 表示第 x 个状态对应的引脚配置。

pinctrl-name="default";        /* 这里只有一个状态,default 为第0个状态 */
pinctrl-0=<&pinctrl_hog_1>;    /* 表示第0个状态default对应的引脚在 pinctrl_hog_1 节点上配置 */

pinctrl-name="default", "wake up";    /* default 为第0个状态, wake up 为第1个状态 */
pinctrl-0=<&pinctrl_hog_1>;           /* 状态0 对应的引脚配置在 pinctrl_hog_1 节点 */
pinctrl-1=<&pinctrl_hog_2>;           /* 状态1 对应的引脚配置在 pinctrl_hog_2 节点 */

pinctrl 服务端

pinctrl 服务端在不同平台有不同的语法。
瑞星微平台
这里拿 RK3568 举例:
在 pinctrl 节点内存在一个 pwm0子节点,pwm0 的 pwm0m0-pins 子节点对应 pinctrl 客户端的 pinctrl-x,rockchip,pins 是瑞星微 pinctrl pin 属性。
在这里插入图片描述
瑞星微 rockchip,pins 属性的第一个参数表示 GPIO组,第二个参数表示 pin 脚在该 GPIO 组的编号,第三个参数为引脚复用功能,第四个参数是 GPIO 驱动强度。
前两个参数之前的笔记已经见过很多次,但复用功能是第一次遇到,该参数值需要查数据手册或用户手册,比如上面的 <0 RK_PB7 1 &pcfg_pull_none> 中,复用功能为 1,通过查阅芯片 datasheet,可以知晓复用功能 1 对应的是 PWM_M0 功能。第四个参数暂时不去研究(基本都是填这个)。
在这里插入图片描述
上面提到不同平台 pin 属性的语法不同,我们可以查看内核 bindings 文档 (kernel/Documentation/devicetree/bindings/pinctrl)来了解设备树的语法,比如瑞星微 pin 属性的介绍:在这里插入图片描述
iMX 平台
iMX 平台 pins 属性相对比较复杂,该属性有六个参数,分别是:mux_reg、conf_reg、input_reg、mux_mode、input_val 和 CONFIG。
前五个参数是写在一起的(用 ‘_’ 连接,见下图),用来表示引脚复用功能,第六个参数用来设置引脚电气属性。
(前五个参数的对应关系我没搞明白,直接分析例子吧)
案例1:
在这里插入图片描述
MX6QDL_PAD_SD4_DAT0__SD4_DATA0 的作用是将引脚 “SD4_DAT0” 设置为 “SD4_DATA0” 复用功能。

总结

开发一个新模块时,要学会到设备树源码中去寻找相关的例子,找不到的可以去设备树的 /Documentation/devicetree/bangings 文档中找资料。

三、使用篇

Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备
树节点文件:

start_kernel()
    setup_arch()
        unflatten_device_tree()
            __unflatten_device_tree()
                unflatten_dt_node()    //解析DTB各个节点

DTB文件格式主要分为四个部分:头部、内存预留块、结构块、字符串块;自由空间块不一定存在。
在这里插入图片描述
头部

struct fdt_header {
	uint32_t magic;                // 该字段应该被设置成0xd00dfeed,字节序是大端字节序
	uint32_t totalsize;            // DTB文件的总大小
	uint32_t off_dt_struct;        // structure block相对于DTB文件头的偏移的字节数
	uint32_t off_dt_strings;       // strings block相对于DTB文件头的偏移的字节数
	uint32_t off_mem_rsvmap;       // memory reservation相对于DTB文件头的字节数
	uint32_t version;              // DTB的版本号
	uint32_t last_comp_version;    // DTB的上一个版本
	uint32_t boot_cpuid_phys;      // 保存系统CPU的ID号
	uint32_t size_dt_strings;      // strings block的大小
	uint32_t size_dt_struct;       // structure block的大小
};

Memory Reservation
指定的内存范围将会被保留,它不能被一般的内存分配函数使用,防止这块内存内的重要数据被内核破坏。memory reservation也可以用C语言的结构体表示,如下:

struct fdt_reserve_entry {
    uint64_t address;
    uint64_t size;
};

Structure Block
structure block由五个标识包含设备树的一些数据,形成一个树形结构,五个标识分别如下
FDT_BEGIN_NODE (0x00000001) :表示一个设备节点的开始。接下来跟着设备节点的名字,设备备节点名字如果有unit-address,需要加上去,节点名字最后以NULL结尾。如果结尾没有4字节对齐,需要填充0x00,保证4字节对齐。
FDT_END_NODE (0x00000002) :表示一个设备节点的结束。它的后面不用跟数据,通常是跟下一个标识,除了FDT_DROP。
示例:

/* DTS代码片段 */
cpus {
    cpu@0 {
        compatible = "mips,mips24KEc";
    };
};

/* DTB代码片段 */
000000b0              00 00 00 01  63 70 75 73 00 00 00 00  |........cpus....|
000000c0  00 00 00 01 63 70 75 40  30 00 00 00 00 00 00 03  |....cpu@0.......|
000000d0  00 00 00 0f 00 00 00 1b  6d 69 70 73 2c 6d 69 70  |........mips,mip|
000000e0  73 32 34 4b 45 63 00 00  00 00 00 02 00 00 00 02  |s24KEc..........|

从上面的代码可知,包含了两个device_node,分别是cpus和cpu@0,cpu@0是cpus的子节点,所以代码中有两个FDT_BEGIN_NODE (0x00000001),和FDT_END_NODE (0x00000002)。FDT_BEGIN后跟着节点名称。从DTB的组织方式也可以看出structure block是以树状层次结构布局的。
FDT_DROP (0x00000003) :表示一个属性的开始。后面需要跟一个属性名的长度和偏移量,再后面跟属性的值,属性的长度和偏移量可以用C语言结构体表示:

struct {
    uint32_t len;
    uint32_t nameoff;
}

len表示此结构后跟的属性值的长度,NULL也包含在内,nameoff表示属性名在strings block中偏移量。
Strings Block
string block包含了在设备树中出现的所有的属性名,所有的名字都是以NULL结尾,structure block通过nameoff来引用其中的属性名。strings block的结尾不需要4字节对齐。

设备树如何传递给内核?
在这里插入图片描述
路径:arch/arm64/kernel/setup.c
1、"cmdline_p = boot_command_line 记录了 uboot 传递内核的 command_line,大小是 4096.如果超过 4096 就要修改这个数组的大小。
setup_machine_fdt(__fdt_pointer);
__fdt_pointer 是 DTB 位于内存的地址。
在 arch/arm64/kernel/head.S 文件,找到:

preserve_boot_args:
    mov x21, x0    //x21 = FDT
    ...
    ...
    str_l x21, __fdt_pointer, x5    //保存FDT指针

启动内核之前,uboot 把DTB的地址传递到 x0,然后再启动内核。x0 存放DTB地址是规定。

设备树下的 device 和 driver 匹配

匹配优先级:name < id_table < of_match_table
案例:

/* 测试节点 */
/{
    test{                           /* 无关节点也需要 compatible 兼容性节点,不然编译到该节点会直接返回 */
        #address-cell=<1>;
        #size-cell=<1>;
        compatible="simple-bus";    /* 根据转换规则,无关节点的兼容性设置也必须是三个特殊值之一,这样有用的子节点才会被编译成平台设备 */
        my_led{
            compatible="my_driver";    /* 匹配的关键属性 */
            reg=<0xFDD60000 0X00000004>;
        };
    };
};

/* 驱动代码中的匹配部分 */
const struct of_device_id mydriver_id_table[] = {
    {.compatible = "my_driver"},    /* 必须和设备树节点中 compatible 属性值一样 */
    {}
};

加载驱动到开发板后将会在 sys/firmware/mydriver/ 目录中出现 myled 节点。

设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必
须先获取到这个设备的节点。
Linux 内核使用 device_node 结构体来描述一个节点:

/* include/linux/of.h */
struct device_node {
    const char *name; /* 节点名字 */
    const char *type; /* 设备类型 */
    phandle phandle;
    const char *full_name; /* 节点全名 */
    struct fwnode_handle fwnode;
    struct property *properties; /* 属性 */
    struct property *deadprops; /* removed 属性 */
    struct device_node *parent; /* 父节点 */
    struct device_node *child; /* 子节点 */
    struct device_node *sibling;
#if defined(CONFIG_OF_KOBJ)
    struct kobject kobj;
#endif
    unsigned long _flags;
    void *data;
#if defined(CONFIG_SPARC)
    const char *path_component_name;
    unsigned int unique_id;
    struct of_irq_controller *irq_trans;
#endif
};

查找节点有关的 OF 函数

struct device_node *of_find_node_by_name(struct device_node *from,
const char *name);
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, 
const char *compatible);
struct device_node *of_find_matching_node_and_match(struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match);
inline struct device_node *of_find_node_by_path(const char *path);

参数:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
name:要查找的节点名字
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值(of_find_compatible_node()中可为NULL)
compatible:要查找的节点所对应的 compatible 属性列表
matches:of_device_id 匹配表,也就是在此匹配表里面查找节点
match:找到的匹配的 of_device_id
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个
节点的全路径
返回值:找到的节点,如果为 NULL 表示查找失败
案例:(寻找上例中的节点)

#include <of.h>

int32_t mydriver_probe(struct platform_device *dev)
{
    ......
    struct device_node *my_mode = of_find_node_by_name(NULL, "myled");
    ......
}

查找父/子节点的 OF 函数

struct device_node *of_get_parent(const struct device_node *node);

返回值:返回输入节点的父节点的指针

struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);

node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为
NULL,表示从第一个子节点开始。

提取属性值的 OF 函数

(比较多,不需要全记住,有大概印象即可)

struct property {
    char *name; /* 属性名字 */
    int length; /* 属性长度 */
    void *value; /* 属性值 */
    struct property *next; /* 下一个属性 */
    unsigned long _flags;
    unsigned int unique_id;
    struct bin_attribute attr;
};
property *of_find_property(const struct device_node *np, const char *name, int *lenp);
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size);
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index,  u32 *out_value);

np:设备节点
name:属性名字
lenp:属性值的字节数
proname:需要统计元素数量的属性名字
elem_size:元素长度
index:要读取的值标号
out_value:读取到的值
of_find_property() 返回 得到的属性结构体指针
of_property_count_elems_of_size() 返回属性元素的数量
of_property_read_u32_index() 读取成功则返回0,否则为负

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,  u16 *out_values,  size_t sz);
int of_property_read_u32_array(const struct device_node *np, const char *propname,  u32 *out_values, size_t sz);
int of_property_read_u64_array(const struct device_node *np, const char *propname,  u64 *out_values, size_t sz);

np:设备节点
proname:要读取的属性名字
out_value:读取到的数组值,分别是 u8、u16、u32、u64
sz:读取的数组元素数量
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没
有要读取的数据,-EOVERFLOW 表示属性值列表太小。
这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据。
大多数的 reg 属
性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。

int of_property_read_u8(const struct device_node *np, const char *propname, u8 *out_value);
int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value);
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value);

np:设备节点
proname:要读取的属性名字
out_value:读取到的数组值
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没
有要读取的数据,-EOVERFLOW 表示属性值列表太小。

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

np:设备节点
proname: 要读取的属性名字
out_string:读取到的字符串值
返回值:0,读取成功,负值,读取失败。

int of_n_addr_cells(struct device_node *np);

返回值:获取到的#address-cells 属性值。
int of_n_size_cells(struct device_node *np);
返回值:获取到的#size-cells 属性值。

其他常用的 OF 函数

int of_device_is_compatible(const struct device_node *device, const char *compat);

device:设备节点
compat:要查看的字符串
返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible
属性中包含 compat 指定的字符串。

const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags);

dev:设备节点
index:要读取的地址标号
size:地址长度
flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。

u64 of_translate_address(struct device_node *dev, const __be32 *in_addr);

dev:设备节点
in_addr:要转换的地址
返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。

int of_address_to_resource(struct device_node *dev,  int index, struct resource *r);

dev:设备节点
index:地址资源标号
r:得到的 resource 类型的资源值
返回值:0,成功;负值,失败。
IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux
内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource
结构体描述的都是设备资源信息,resource 结构体定义在文件 include/linux/ioport.h 中,定义如
下:

struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    struct resource *parent, *sibling, *child;
};

对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示
结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,可选的资源标志
定义在文件 include/linux/ioport.h 中,如下所示:

#define IORESOURCE_BITS 0x000000ff 
#define IORESOURCE_TYPE_BITS 0x00001f00 
#define IORESOURCE_IO 0x00000100 
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300 
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000 
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000 
#define IORESOURCE_STARTALIGN 0x00080000 
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000 
#define IORESOURCE_MUXED 0x00400000 
#define IORESOURCE_EXCLUSIVE 0x08000000 
#define IORESOURCE_DISABLED 0x10000000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000

最 常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和
IORESOURCE_IRQ。

void __iomem *of_iomap(struct device_node *np, int index);

np:设备节点
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败
of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。
of_iomap() 本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段。

获取中断号的 OF 函数

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

dev:设备节点
index:索引号
返回值:对应的中断号
案例:获取一个gpio的终端号

/{
    test{
        myirq{
            compatible="my_irq";
            interrupt-parent = <&gpio0>;    /* 引用 gpio0 控制器的设备树 */
            interrupts=<RK_PB5 IRQ_TYPE_LEVEL_LOW>;
        };
    };
};

#include <linux/of.h>
#include <linux/of_irq.h>

int32_t my_irq_probe(struct platform_device *dev){
    strcut device_node *my_node = of_find_node_by_name(NULL, "myirq");
     int irq = irq_of_parse_and_map(my_node,0);
     pintk("irq is %d\n", irq);
     
     return 0;
}

其他中断相关函数:

struct irq_data *irq_get_irq_data(unsigned int irq);

输入中断号,返回 irq_data 结构体。

u32 irqd_get_trigger_type(struct irq_data *d);

从中断属性中获取中断标志位(中断触发方式)。
其他函数:

int gpio_to_irq(unsigned int gpio);

输入 gpio 编号获得其中断号。

int of_irq_get(struct device_node *dev, int index);

根据设备节点和索引号获得中断号。

int platform_get_irq(struct platform_device *dev, unsigned int num);

根据平台总线设备结构体和索引号获得中断号。
案例:

int32_t my_irq_probe(struct platform_device *dev){
    
    int irq = platform_get_irq(dev, 0);
      
    return 0;
}

获取 GPIO 的 OF 函数

int of_gpio_named_count(struct device_node *np, const char *propname);
int of_gpio_count(struct device_node *np);
int of_get_named_gpio(struct device_node *np, const char *propname, int index); 

np:设备节点
propname:要统计的 GPIO 属性
index:GPIO 索引,因为一个属性里面可能包含多个 GPIO,此参数指定要获取哪个 GPIO的编号,如果只有一个 GPIO 信息的话此参数为 0
of_gpio_named_count 和 of_gpio_count 返回统计到的GPIO数量
of_get_named_gpio 返回获取到的 GPIO 编号

四、附加篇

ranges 属性

两个格式:

ranges=<child-bus-address parent-bus-addree length>;
ranges; /* ranges 的值为空,那么将进行 1 :1 映射,是内存区域 */

child-bus-address:子地址物理空间的其实地址。由所在节点的 #address-cells 决定地址字长。
parent-bus-address:父地址物理空间的起始地址。由所在节点的父节点 的#address-cells 决定地址字长。
length:映射的大小,由所在节点的 #size-cell 属性决定地址的字长

案例:

soc{
    ...
    ranges=<0x0 0xe0000000 0x00100000>;
    /* 将地址0x0 ~ 0x0+0x10000000 映射到 0xe0000000+0x00100000 */
    ...
    serial {
        device_type = "serial";
        compatible = "ns16550";
        reg = <0x4600 0x100>;
        clock-frequency = <0>;
        interrupts = <0xA 0x8>;
        interrupt-parent = <&ipic>;
    };
}

reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。

经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。

按照 ranges 的两个格式,可以把设备分为内存映射型设备和非内存映射型设备。
内存映射型设备:CPU可以直接访问的设备。
当一个节点中含有:ranges;
那么其 reg 属性的地址就是 CPU 可以直接访问的。

非内存映射设备:CPU 不可直接访问的设备,需要通过外部总线转化地址。
即需要使用带参数的 ranges 格式来映射内存。
案例:某个以太网设备树中(高速设备一般走内存映射的传输方式)

/{
    ...
    #address-cell=<1>;        /* 注意这里是 1 !!!*/
    #size-cell=<1>;
    external-bus{
        #address-cell=<2>;    /* 注意这里是 2 !!!*/
        #size-cell=<1>;
        ranges=<0 0 0x10100000 0x100000    /* 以太网卡片选,不同的片选信号代表不同的地址域 */
                1 0 0x10160000 0x100000    /* 片选,I2C 控制 */
                2 0 0x30000000 0x30000000> /* 片选, NOR FLASH */
        ethernet@0,0{
            ...
            reg=<0 0 0x1000>;/* 前两个 0 也是地址,0x1000 是数字32,即32位,那么两个0分别组成64位地址的高32位和低32位 */
        };
        i2c@1,0{
            ...
            reg=<1 0 0x1000>;
        };
        flash@2,0{
            ...
            reg=<2 0 0x4000000>        
        }
    };
};

首先,因为所在节点的 #address-cell 是 2,所以用 2个数 来表示子地址,即可从左到右的前两位:0 0 ,两个数组成了地址的高32位和低32位。
其次,ranges 的父节点的 #address-cell 是1,所以 ranges 中 父地址物理空间的起始地址 只用一个数值来表示地址。

  • Ethernet 片选

(0<<32) | (0) 就是 ethernet 节点中的 reg 的64位地址,设其为 addr1
(0<<32) | (0) 就是 ranges 中<0 0 0x10100000 0x100000>前两位组成的64位地址,设其为 addr2
那么,地址范围就是:
(0x10100000 + (addr1-addr2) )~ (0x10100000+0x1000-1)
即:0x10100000 ~ 0x10100fff

  • IIC controller

(1<<32) | (0) 就是i2c节点中的reg的64位地址,设为addr1
(1<<32) | (0) 是 ranges 中<1 0 0x10160000 0x100000> 前两位组成的64位地址,设其为 addr2
那么,地址范围就是:
(0x10160000 + (addr1-addr2) )~ (0x10160000+0x1000-1)
即:0x10160000 ~ 0x10160fff

  • NOR FLASH

略……

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值