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 = <>xx_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
略……