Linux 驱动之设备树

Linux 驱动之设备树

参考视频地址

【北京迅为】嵌入式学习之Linux驱动(第七期_设备树_全新升级)_基于RK3568_哔哩哔哩_bilibili

本章总领


1.设备树基本知识

什么是设备树?

​ Linux之父Linus Torvalds在2011年3月17日的ARM Linux邮件列表中说道:This whole ARM thing is a f*cking pain in the ass。之后ARMLinux社区引入了设备树。为什么Linus Torvalds会爆粗口呢?

​ 在讲平台总线模型的时候,平台总线模型是把驱动分成了两个部分,一部分是device,一部分是driver,设备信息和驱动分离这个设计非常的好。device部分是描述硬件的。一般device部分的代码会放在内核源码中arch/arm/plat-xxx和arch/arm/mach-xxx下面。但是随着Linux支持的硬件越来越多,在内核源码下关于硬件描述的代码也越来越多。并且每修改一下就要编译一次内核。

​ 长此以往Linux内核里面就存在了大量"垃圾代码",而且非常多,这里说的"垃圾代码"是关于对硬件描述的代码。从长远看,这些代码对Linux内核本身并没有帮助,所以相当于Linux内核是"垃圾代码"。但是并不是说平台总线这种方法不好。

为了解决这个问题 ,设备树就被引入到了Linux上。使用设备树来剔除相对内核来说的"垃圾代码",既用设备树来描述硬件信息,用来替代原来的device部分的代码。虽然用设备树替换了原来的device部分,但是平台总线模型的匹配和使用基本不变。并且对硬件修改以后不必重新编译内核。直接需要将设备树文件编译成二进制文件,在通过bootloader传递给内核即可。所以设备树就是用来描述硬件资源的文件。

​ 设备树是描述硬件的文本文件,因为语法结构像树一样。所以叫设备树。

设备树的基本概念

基本名词解释

shell 复制代码
<1>DT:Device Tree //设备树
<2>FDT: Flattened Device Tree //开放设备树,起源于OpenFirmware (OF)
<3>dts: device tree source的缩写 //设备树源码
<4>dtsi: device tree source include的缩写 //通用的设备树源码
<5>dtb: device tree blob的缩写//编译设备树源码得到的文件
<6>dtc: device tree compiler的缩写 //设备树编译器

DTS, DTSI, DTB, DTC 之间的关系:

​ DTS和DTSI相当于源码文件,通过DTC这个编译器,编译生成DTB文件。

​ 以RK3588为例,设备树文件路径为:kernel/arch/arm64/boot/dts/rockchip

DTC编译器的使用

​ 以RK3588为例, DTC编译器源码路径:kernel/scripts/dtc; 如果正常编译完内核后,会在这个路径生成编译器dtc。

​ 如果你编译完内核代码后,进入到kernel/scripts/dtc路径,发现没有生成dtc编译器,那么检查kernel路径下.config配置文件,是否包含:CONFIG_DTC=y

如果没有,请将其加入到.config里面。(需要高版本的内核代码, 支持设备树)

编译设备树
shell 复制代码
dtc -I dts -O dtb -o xxx.dtb xxx.dts
反编译设备树
shell 复制代码
dtc -I dtb -O dts -o xxx.dts xxx.dtb
编译内核设备树

​ 进入到内核的顶层路径,执行make dtbs, 这种方法需要使能环境变量,暂时无法在我的rk3588内核上编译通过,会报错。

实验测试

​ 编写一个简单的设备树文件,代码路径:/home/topeet/Linux/my-test/40_dtc/my_device_tree.dts, 代码如下所示:

shell 复制代码
/dts-v1/;
/ {

};

​ 这个设备树很简单,只包含了根节点/,而根节点中没有任何子节点或属性。这个示例并 没有描述任何具体的硬件设备或连接关系,它只是一个最基本的设备树框架,在本小节只是为 了测试设备树的编译和反编译。

dts-v1 明确声明该文件使用设备树语法版本1 ,这是设备树源文件的强制要求,必须放在文件第一行,不能省略,否则编译报错。

编译my_device_tree.dts

shell 复制代码
/home/topeet/Linux/rk3588-linux/kernel/scripts/dtc/dtc -I dts -O dtb -o my_device_tree.dtb my_device_tree.dts

编译完成后,生成my_device_tree.dtb:

shell 复制代码
root@ubuntu:/home/topeet/Linux/my-test/40_dtc# ls
my_device_tree.dtb

反编译my_device_tree.dtb:

shell 复制代码
/home/topeet/Linux/rk3588-linux/kernel/scripts/dtc/dtc -I dtb -O dts -o re_my_device_tree.dts my_device_tree.dtb

反编译完成后,生成re_my_device_tree.dts:

shell 复制代码
root@ubuntu:/home/topeet/Linux/my-test/40_dtc# ls
re_my_device_tree.dts
VSCode 安装设备树插件

​ 搜索插件DeviceTree 并安装。


2.设备树语法

根结点

​ 根结点是设备树必须包含的结点,根结点的名字 "/",如下所示:

shell 复制代码
/dts-v1/;     // 第一行表示dts文件的版本
/{            // 根结点
  
};

子结点

格式

shell 复制代码
[label:] node-name[@unit-address] {
	[properties definitions]
	[child nodes]
};

properties definitions\] : 表示结点属性 \[child nodes\]:表示该结点的子结点 **举例**: ```shell node1{//子节点,节点名称为node1 node1_child{//子子节点,节点名称为node1_child }; }; ``` ​ 注意:同级节点下节点名称不能相同。不同级节点名称可以相同 **范例代码**: ```shell /dts-v1/; // 第一行表示dts文件的版本 /{ // 根结点 node1 { // 子结点1 node1-child { }; }; node2 { // 子结点2 node1-child { }; }; }; ``` #### 结点名称 ​ 在对节点进行命名的时候,一般要体现设备的类型,比如网口一般命名成ethernet,串口一般命名成uart ,对于名称一般要遵循下面的命名格式。 ​ 格式:\[标签\]:\<名称\>\[@\<设备地址\>\] 其中,\[标签\]和\[@\<设备地址\>\]是可选项,\[名称\]是必选项。另 外,这里的设备地址也没有实际意义,只是让节点名称更人性化 ,更方便阅读。 **举例**: uart: serial@02288000 其中, uart就是这个节点标签,也叫别名, serial@02288000 就是节点名称。 **范例代码**: ```shell /dts-v1/; // 第一行表示dts文件的版本 /{ // 根结点 node1 { // 子结点1 node1-child { }; }; node2 { // 子结点2 node1-child { }; }; led:gpio@02211000 { node1-child { }; }; }; ``` **reg属性** ​ reg属性可以来描述地址信息。比如存储器的地址。 ​ reg属性的格式如下: reg = \ **举例1** : reg = \<0x02200000 0x4000\>*;* **举例2** : reg = \<0x02200000 0x4000 0x02205000 0x4000 \>*;* #### #address-cell和#size-cells属性 ​ #address-cell和#size-cells用来描述子结点中的reg属性中的地址和长度信息。 **举例1:** ```shell node1 { #address-cells = <1>; // 子结点中reg 属性有一个地址 #size-cells = <0>; // 子结点中reg 属性没有长度 node1-child { reg = <0>; }; }; ``` **举例2:** ```shell node1 { #address-cells = <1>; // 子结点中reg 属性有一个地址 #size-cells = <1>; // 子结点中reg 属性有一个长度值 node1-child { reg = <0x02200000 0x4000>; }; }; ``` **举例3:** ```shell node1 { #address-cells = <2>; // 子结点中reg 属性有二个地址 #size-cells = <0>; // 子结点中reg 属性没有长度值 node1-child { reg = <0x00 0x01>; }; }; ``` *** ** * ** *** #### model属性 ​ model属性的值是一个字符串,一般用model描述一些信息.比如设备的名称,名字等。 **举例1:** ```shell model = "wm8969-audio"; ``` **举例2:** ```shell model = "This is Linux board" ``` *** ** * ** *** #### status属性 ​ status属性和设备的状态有关系,status的属性是字符串,属性值有以下几个状态可选: | 属性值 | 描述 | |----------|-----------------------------| | okay | 设备是可用状态 | | disabled | 设备是不可用状态 | | fail | 设备是不可用状态并且设备检测到了错误 | | fail-sss | 设备是不可用状态并且设备检测到了错误,sss是错误内容 | *** ** * ** *** #### compatible属性 ​ compatible属性是非常重要的一个属性。compatible是用来和驱动进行匹配的。匹配成功以后会执行驱动中的probe函数。 **举例:** ```shell compatible = "xunwei", "xunwei-board" //在匹配的时候会先使用第一个值"xunwei"进行匹配,如果没有就会使用第二个值"xunwei-board"进行匹配。 ``` *** ** * ** *** #### device_type属性 ​ 在某些设备树文件中,可以看到 device_type 属性,device_type 属性的值是字符串,只用于 cpu 节点或者 memory 节点进行描述。 **举例1:** ```shell memory@30000000 { device_type = "memory"; reg = <0x30000000 0x4000000>; }; ``` **举例2:** ```shell cpu1: cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a35", "arm,armv8"; reg = <0x0 0x1>; }; ``` *** ** * ** *** #### **自定义属性** ​ 设备树中规定的属性有时候并不能满足我们的需求,这时候我们可以自定义属性。 **举例:** 自定义一个管脚标号的属性 `pinnum`。 ```shell pinnum = <0 1 2 3 4>; ``` *** ** * ** *** #### 设备树特殊结点 ##### aliases ​ 特殊节点 `aliases` 用来定义别名。定义别名的目的就是为了方便引用结点点。当然,除了使用 `aliases` 来命名别名,也可以在对结点命名的时候添加标签来命名别名。 **举例:** ```shell aliases { mmc0 = &sdmmc0; mmc1 = &sdmmc1; mmc2 = &sdhci; serial0 = "/simple@fe000000/serial@llc500"; }; ``` **chosen** ​ 特殊节点 chosen 用来由 U-Boot 给内核传递参数。重点是 bootargs 参数。chosen 节点必须是根节点的子节点。 ```shell chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; }; ``` *** ** * ** *** #### 官方设备树文档路径 https://app.readthedocs.org/projects/devicetree-specification/downloads/pdf/latest/ #### 综合示例: ```shell /dts-v1/; /{ model = "This is Linux board"; #address-cells = <1>; #size-cells = <1>; aliases{ led1=&led; //给led取别名led1 led2=&ledB; //给ledB取别名led2 led3="/gpio@2211002"; //给"gpio@2211002"取别名led3 }; chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; }; cpu1: cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a35", "arm,armv8"; reg = <0x0 0x1>; }; node1 { #address-cells = <1>; #size-cells = <0>; gpio@2211001{ reg = <0x2211001>; }; }; node2 { node1-child { pinnum = <0 1 2 3 4>; }; }; led:gpio@2211000 { compatible = "led"; reg = <0x2211000 0x40>; status="okay"; }; ledB:gpio@2211001 { compatible = "led"; reg = <0x2211001 0x40>; status="okay"; }; ledC:gpio@2211002 { compatible = "led"; reg = <0x2211001 0x40>; status="okay"; }; }; ``` #### 实例分析--中断 ##### RK处理器中断节点实例: ```shell //RK原厂工程师编写 gpio0: gpio@fdd60000 { compatible = "rockchip,gpio-bank"; reg = <0x0 0xfdd60000 0x0 0x100>; interrupts = ; clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>; gpio-controller; #gpio-cells = <2>; gpio-ranges = <&pinctrl 0 0 32>; interrupt-controller; #interrupt-cells = <2>; }; ``` 第2行代码:**节点申明**: * `gpio0:` - 节点标签,允许其他节点通过 `&gpio0` 引用此节点 * `gpio@fdd60000` - 节点名称格式:设备类型@基地址 * 表示这是一个 GPIO 控制器,位于物理地址 `0xfdd60000` 第3行代码指定设备驱动为 `rockchip,gpio-bank` 第4行代码:**寄存器定义** * 地址格式:`<高32位 低32位 长度高32位 长度低32位>` * 基地址:`0x00000000fdd60000` (64位地址) * 地址范围长度:`0x100` (256字节) * 表示该GPIO控制器占用256字节的物理地址空间 第5行代码:**中断定义** * `GIC_SPI` - 中断类型:共享外设中断(SPI) * `33` - 硬件中断号 * `IRQ_TYPE_LEVEL_HIGH` - 触发类型:高电平触发 * 表示该GPIO控制器本身会产生中断(如端口状态变化) 第6行代码:**时钟依赖** * 引用两个时钟源: * `&pmucru PCLK_GPIO0` - GPIO0的外设时钟 * `&pmucru DBCLK_GPIO0` - GPIO0的调试时钟 * `pmucru`是时钟控制器的节点标签 第8行代码:**GPIO控制器申明** * 表明此节点是一个GPIO控制器 * 允许其他节点通过phandle引用其GPIO引脚 第9行代码:**GPIO单元格式** * 定义引用GPIO引脚时需要提供的参数数量 * `<2>` 表示需要两个参数: * 参数1:GPIO引脚号 * 参数2:GPIO标志(如激活状态) 第10行代码:**GPIO范围映射** * 映射到pinctrl控制器 `&pinctrl` * `0` - GPIO控制器的起始引脚号 * `0` - pinctrl的起始引脚号 * `32` - 映射的引脚数量 * 表示此GPIO控制器的0-31引脚对应pinctrl的0-31引脚 第11行代码:**中断控制器声明** * 表明此节点也是一个中断控制器 * 可以处理其GPIO引脚产生的中断 第12行代码:**中断单元格式** * 定义引用中断时需要提供的参数数量 * `<2>` 表示需要两个参数: * 参数1:GPIO引脚号 * 参数2:中断触发标志 此设备树节点功能总结:此节点定义了一个Rockchip平台的GPIO控制器,具有: 1. 地址空间:0xfdd60000 - 0xfdd60100 2. 支持32个GPIO引脚(0-31) 3. 既是GPIO控制器又是中断控制器 4. 依赖两个时钟源 5. 映射到pinctrl子系统 6. 使用双参数格式引用GPIO和中断 *** ** * ** *** ```shell // 开发人员编写的设备树节点 ft5x06:ft5x06@38 { status = "disabled"; compatible = "edt,edt-fts"; reg = <0x38>; touch_gpio = <&gpio0 5 IRQ_TYPE_EDGE_RISING>; interrupt-parent = <&gpio0>; interrupts = <5 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; touchscreen-size-x = <800>; touchscreen-size-y = <1280>; touch_type = <1>; }; ``` 接下来逐行分析下上面设备树节点: ft5x06: ft5x06@38 { **节点声明**: * `ft5x06:` - 节点标签,允许其他部分通过 `&ft5x06` 引用此节点 * `ft5x06@38` - 节点名称格式:设备类型@I2C地址 * 表示这是一个FT5x06系列触摸控制器,位于I2C总线地址0x38 status = "disabled"; *** ** * ** *** **设备状态**: * `"disabled"` 表示此设备默认不启用 * 可在系统启动时通过覆盖设备树或用户空间启用(改为`"okay"`) compatible = "edt,edt-fts"; **兼容性属性**: * 指定设备驱动为`edt,edt-fts` * 内核通过此字符串匹配触摸屏驱动程序 * 注意:虽然节点名为ft5x06,但兼容性指定为edt-fts系列 *** ** * ** *** reg = <0x38>; **I2C地址**: * 指定设备在I2C总线上的7位地址为0x38 * I2C驱动将使用此地址与设备通信 *** ** * ** *** touch_gpio = <&gpio0 5 IRQ_TYPE_EDGE_RISING>; **自定义触摸信号属性**: * 自定义属性(非标准) * 引用GPIO控制器`gpio0`的5号引脚 * 配置为上升沿触发(`IRQ_TYPE_EDGE_RISING`) *** ** * ** *** interrupt-parent = <&gpio0>; **中断父控制器**: * 指定中断控制器为`gpio0`(之前定义的GPIO控制器) * 表示此设备的中断信号连接到GPIO0控制器 *** ** * ** *** interrupts = <5 IRQ_TYPE_LEVEL_LOW>; **中断定义**: * 使用双参数格式(匹配`gpio0`的`#interrupt-cells = <2>`) * `5` - GPIO引脚号(GPIO0的第5号引脚) * `IRQ_TYPE_LEVEL_LOW` - 中断触发类型:低电平触发 *** ** * ** *** reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; **复位GPIO定义**: * 标准GPIO引用属性 * 引用GPIO控制器`gpio0`的6号引脚 * `GPIO_ACTIVE_LOW` - 低电平有效(复位时拉低) * 驱动将使用此引脚控制设备复位 *** ** * ** *** touchscreen-size-x = <800>; touchscreen-size-y = <1280>; **触摸屏尺寸**: * 标准触摸屏属性 * X方向分辨率:800像素 * Y方向分辨率:1280像素 * 驱动使用此信息校准坐标 *** ** * ** *** touch_type = <1>; **自定义触摸类型属性**: * 自定义属性(非标准) * 值`<1>`可能是设备特定配置(如协议版本) * 需要在驱动程序中解析此属性 *** ** * ** *** ##### 总结: 1. 在中断控制器中,必须有一个属性#interrupt-cells,表示其他节点如果使用这个中断控制器需要几个cell来表示使用哪一个中断。 2. 在中断控制前中,必须有一个属性interrupt-controller,表示他是中断控制器。 3. 在设备中使用中断,需要使用属性interrupt-parent=\<\&XXXX\>,表示中断信号链接的是哪个中断控制器,接着使用interrupts属性来表示中断引脚和触发方式。 ​ 注意:interrupt里有几个cell,是由interrupt-parent对应的中断控制器里面的#interrupt-cells属性决定。 ##### 其他写法: 级联中断控制器,gpio_intc 级联到gic ```shell // 主中断控制器(SoC级) gic: interrupt-controller@fee00000 { compatible = "arm,gic-v3"; #interrupt-cells = <3>; interrupt-controller; }; // 二级中断控制器(外设级) gpio_intc: interrupt-controller@fdd60000 { compatible = "arm,gic-v2m"; #interrupt-cells = <2>; interrupt-controller; interrupt-parent = <&gic>; // 级联到主GIC interrupts = <0 99 IRQ_TYPE_LEVEL_HIGH>; // 使用GIC的99号中断 }; ``` 使用interrupt-extended 来表示多组中断控制器 ```shell // 主中断控制器 gic1: interrupt-controller@fee00000 { compatible = "arm,gic-v3"; #interrupt-cells = <3>; interrupt-controller; }; // 级联中断控制器 gic2: interrupt-controller@f0800000 { compatible = "arm,gic-v2m"; #interrupt-cells = <2>; interrupt-controller; interrupt-parent = <&gic1>; interrupts = ; // 连接到GIC1的99号SPI中断 }; // 中断设备 interrupt@38 { compatible = "edt,edt-ft5206"; reg = <0x38>; interrupt-extended = <&gic1 0 9 IRQ_TYPE_EDGE_RISING>, // SPI中断9 <&gic2 10 IRQ_TYPE_EDGE_FALLING>; // 级联中断10 }; ``` ##### 实践---使用设备树描述中断 ​ 本小节将会编写一个在 RK3588 上的ft5x06 触摸中断设备树。首先确定ft5x06的中断引脚号,底板原理图如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ad00f2db973b404b988e46bb6e0502c4.png) ​ 由上图可知,触摸引脚网络标号为TP_INT_L,对应的SOC管脚为GPIO3_C0。 ​ 然后来查看内核源码目录下的"drivers/input/touchscreen/edt-ft5x06.c"文件,这是 ft5x06 的驱动文件,找到compatible匹配值相关的部分,如下所示: ```c static const struct of_device_id edt_ft5x06_of_match[] = { { .compatible = "edt,edt-ft5206", .data = &edt_ft5x06_data }, { .compatible = "edt,edt-ft5306", .data = &edt_ft5x06_data }, { .compatible = "edt,edt-ft5406", .data = &edt_ft5x06_data }, { .compatible = "edt,edt-ft5506", .data = &edt_ft5506_data }, { .compatible = "evervision,ev-ft5726", .data = &edt_ft5506_data }, /* Note focaltech vendor prefix for compatibility with ft6236.c */ { .compatible = "focaltech,ft6236", .data = &edt_ft6236_data }, { /* sentinel */ } }; ``` ​ 这里随便选择一个.compatible标签,我这里选择"edt,edt-ft5206"。 ​ 在内核源码目录下的"include/dt-bindings/pinctrl/rockchip.h"头文件中,定义了 RK 引脚名 和gpio 编号的宏定义,如下图所示: ```c #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 ``` ​ 可以看到RK已经将GPIO组和引脚编号写成了宏定义的形式, GPIO3_C0 对应的宏为:RK_PC0。有了以上信息后,我们就可以编写触摸屏中断的设备树,如下所示: ```shell /dts-v1/; #include "dt-bindings/pinctrl/rockchip.h" #include "dt-bindings/interrupt-controller/irq.h" /{ model = "This is my devicetree!"; ft5x06@38 { compatible = "edt,edt-ft5206"; interrupt-parent = <&gpio3>; interrupts = ; }; }; ``` ​ 第1行代码: 设备树文件的头部,指定了使用的设备树语法版本。 ​ 第3行代码:用于定义 Rockchip 平台的引脚控制器相关的绑定。 ​ 第4行代码:用于定义中断控制器相关的绑定。 ​ 第5行代码:表示设备树的根节点开始。 ​ 第6行代码:指定了设备树的模型名称,描述为 "This is my device tree!"。 ​ 第8行代码:指定了设备节点的兼容性字符串,表示该设备与 "edt,edt-ft5206" 兼容。 ​ 第9行代码:指定了中断的父节点,即中断控制器所在的节点。这里使用了一个引用(\&gpio3) 来表示父节点。 ​ 第10行代码:指定了中断信号的配置。RK_PC0表示中断信号的引脚编号,IRQ_TYPE_EDGE_RISING 表示中断类型为上升沿触发。 #### 实例分析--时钟 ​ 绝大部分的外设工作都需要时钟,时钟一般以时钟树的形式呈现。在ARM平台中可以使用设备树来描述时钟树,如时钟的结构、时钟的属性等。再由驱动来解析设备树中时钟树的信息,从而完成时钟的初始化和使用。 ​ 在设备树中,时钟分为**生产者(providers)**和**消费者(consumers)**。 ##### 生产者属性 ###### \*\*`#clock-cells` \*\* `#clock-cells` 属性代表时钟输出的路数: * 当 `#clock-cells` 值为 `0` 时,代表仅有 **1 路**时钟输出 * 当 `#clock-cells` 值大于等于 `1` 时,代表输出 **多路** 时钟 **举例1:单路时钟输出** osc24m: osc24m { compatible = "fixed-clock"; clock-frequency = <24000000>; // 24MHz时钟 clock-output-names = "osc24m"; // 时钟输出名称 #clock-cells = <0>; // 表示只有1路时钟输出 }; **举例2:多路时钟输出** clock: clock-controller { #clock-cells = <1>; // 表示有多路时钟输出 clock-output-names = "clock1", "clock2"; // 两路时钟名称 }; *** ** * ** *** ###### clock-output-names ​ clock-output-names 属性定义了输出时钟的名字。 **举例1:单路时钟输出** ```shell osc24m: osc24m { compatible = "fixed-clock"; clock-frequency = <24000000>; // 24MHz时钟 clock-output-names = "osc24m"; // 时钟输出名称 #clock-cells = <0>; // 表示只有1路时钟输出 }; ``` **举例2:多路时钟输出** ```shell clock: clock-controller { #clock-cells = <1>; // 表示有多路时钟输出 clock-output-names = "clock1", "clock2"; // 两路时钟名称 }; ``` ###### clock-frequency ​ clock-frequency 属性可以指定时钟的大小。 举例1: ```shell osc24m: osc24m { compatible = "fixed-clock"; clock-frequency = <24000000>; // 24MHz时钟 clock-output-names = "osc24m"; // 时钟输出名称 #clock-cells = <0>; // 表示只有1路时钟输出 }; ``` ###### assigned-clocks和assigned-clock-rates ​ assigned-clocks和assigned-clock-rates一般成对使用。当输出多路时钟时,为每路时钟进行编号。 举例: ```shell cru: clock-controller@fdd20000 { #clock-cells = <1>; assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKDEV_PRE>; assigned-clock-rates = <32768>, <300000000>; }; ``` ###### clock-indices ​ `clock-indices` 属性用于指定时钟输出的索引号(index)。如果不提供这个属性,那么 `clock-output-names` 和索引的对应关系默认是 0, 1, 2...(线性递增)。如果这种对应关系不是线性的,可以通过 `clock-indices` 属性来定义自定义的索引映射。 **举例1:标准索引映射** ```c scpi_dvfs: clocks@0 { #clock-cells = <1>; // 需要1个参数标识时钟 clock-indices = <0>, <1>, <2>; // 显式定义索引号 clock-output-names = "atlclk", "aplclk", "gpuclk"; // 三个时钟输出 }; ``` **举例2:非连续索引映射** ```c scpi_clk: clocks@1 { #clock-cells = <1>; // 需要1个参数标识时钟 clock-indices = <3>; // 定义索引号为3(非连续) clock-output-names = "pxlclk"; // 单个时钟输出 }; ``` ###### assigned-clock-parents ​ assigned-clock-parents 属性可以用来设置时钟的父时钟。 举例: ```c clock:clock { assigned-clock = <&clkcon 0>, <&pll 2>; assigned-clock-parents = <&pll 2>; assigned-clock-rates = <115200>, <9600>; }; ``` *** ** * ** *** ##### 消费者属性 ###### clock-name ​ clocks属性和clock-name属性用来指定使用的时钟源和消费者中时钟的名字。 举例: ```c clock:clock { clocks = <&cru CLK_VOP>; clock-names = "clk_vop",; }; ``` ​ 注:cru是clock reset unit的缩写,pmu是power management unit的缩写。 *** ** * ** *** **消费者时钟节点实例分析**: ```c gpio1: gpio@fe740000 { compatible = "rockchip.gpio-bank"; reg = <0x0 0xfe740000 0x0 0x100>; interrupts = ; clocks = <&cru PCLK_GPIO1>, <&cru DBCLK_GPIO1>; gpio-controller; #gpio-cells = <2>; gpio-ranges = <&pinctrl 0 32 32>; interrupt-controller; #interrupt-cells = <2>; }; spi0: spi@fe610000 { compatible = "rockchip,rk3066-spi"; reg = <0x0 0xfe610000 0x0 0x1000>; interrupts = ; #address-cells = <1>; #size-cells = <0>; clocks = <&cru CLK_SPI0>, <&cru PCLK_SPI0>; clock-names = "spick", "app_pclk"; dmas = <&dmac0 20>, <&dmac0 21>; dma-names = "tx", "rx"; pinctrl-names = "default", "high_speed"; pinctrl-0 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins>; pinctrl-1 = <&spi0m0_cs0 &spi0m0_cs1 &spi0m0_pins_hs>; status = "disabled"; }; ``` ​ 第1行和第5行代码:gpio1中有clocks 属性,配置2个时钟,模块cru提供的时钟PCLK_GPIO1。模块cru提供的时钟DBCLK_GPIO1。 ​ 第14行代码和第20行代码:spi0使用2个时钟源,分别是\<\&cru CLK_SPI0\>和\<\&cru PCLK_SPI0\>,并且给他们起了一个名字(第21行代码),分别为"spick"和"app_pclk"。 *** ** * ** *** ```c usb2phy0: usb2-phy@fe8a0000 { compatible = "rockchip,rk3568-usb2phy"; reg = <0x0 0xfe8a0000 0x0 0x10000>; interrupts = ; clocks = <&pmucru CLK_USBPHY0_REF>; clock-names = "phyclk"; #clock-cells = <0>; assigned-clocks = <&cru USB480M>; assigned-clock-parents = <&usb2phy0>; clock-output-names = "usb480m_phy"; rockchip,usbgrf = <&usb2phy0_grf>; status = "disabled"; u2phy0_host: host-port { #phy-cells = <0>; status = "disabled"; }; u2phy0_otg: otg-port { #phy-cells = <0>; status = "disabled"; }; }; ``` ​ 第1行,第8,第9行代码:usb2phy0时钟\<\&cru USB480M\>挂载在时钟\<\&usb2phy0\>下面, 并且输出的时钟名为:"usb480m_phy"(第10行代码)。 ​ 第5行,第6行代码,usb2phy0也使用时钟\<\&pmucru CLK_USBPHY0_REF\>,时钟名为"phyclk"。 *** ** * ** *** #### 实例分析--CPU **设备树中CPU节点介绍** 1. **cpus 节点** cpus 节点里面包含物理CPU的布局。也就是CPU的布局全部在此节点下描述。 2. **cpu-map 节点** 描述单核处理器不需要使用cpu-map节点,cpu-map节点主要用在描述大小核架构处理器中。cpu-map的节点名称必须是cpu-map,cpu-map节点的父节点必须是cpus节点。子节点必须是一个或者多个的cluster和socket节点。 3. **socket 节点** socket 节点描述的是主板上的CPU插槽。主板上有几个CPU插槽,就有几个socket节点。socket节点的子节点必须是一个或者多个cluster节点。当有多个CPU插槽时,socket节点的命名方式必须是socketN,N=0,1,2... 4. **cluster节点** cluster节点用来描述CPU的集群。比如RK3399的架构是双核A72+四核A53,双核A72是一个集群,用一个cluster节点来描述,四核A53也是一个集群,用一个cluster节点来描述。cluster节点的命名方式必须是clusterN,N=0,1,2...,cluster节点的子节点必须是一个或者多个的cluster节点或者一个或者多个的core节点。 5. **core节点** core节点用来描述一个cpu,如果是单核cpu,则core节点就是cpus节点的子节点。core节点的命名方式必须是coreN,N=0,1,2...,core节点的子节点必须是一个或者多个thread节点。 6. **thread节点** thread节点用来描述处理的线程。thread节点的命名方式必须是threadN,N=0,1,2... **举例1:单核CPU** ```c cpus { #address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { compatible = "arm, cortex-a7"; device_type = "cpu"; }; }; ``` **cpus 节点** ```c cpus { #address-cells = <1>; #size-cells = <0>; ... }; ``` * **作用**:系统CPU的父容器节点 * **属性** : * `#address-cells = <1>`:子节点地址字段使用1个32位单元 * `#size-cells = <0>`:子节点大小字段不使用任何单元 * **位置**:必须是根节点(/)的直接子节点 ##### **cpu0: cpu@0 节点** ```c cpu0: cpu@0 { compatible = "arm, cortex-a7"; device_type = "cpu"; }; ``` * **节点名称** :`cpu@0` 表示第0个CPU * **标签** :`cpu0`(可通过`&cpu0`引用) * **关键属性** : * `compatible = "arm, cortex-a7"`:指定CPU架构为ARM Cortex-A7 * `device_type = "cpu"`:声明设备类型为CPU(必需属性) **举例2:四核CPU** ```c cpus { #address-cells = <0x1>; #size-cells = <0x0>; cpu0: cpu@0 { device_type = "cpu"; compatible = "arm, cortex-a9"; }; cpu1: cpu@1 { device_type = "cpu"; compatible = "arm, cortex-a9"; }; cpu2: cpu@2 { device_type = "cpu"; compatible = "arm, cortex-a9"; }; cpu3: cpu@3 { device_type = "cpu"; compatible = "arm, cortex-a9"; }; }; ``` **举例3:四核A53+双核A72** ```c cpus { #address-cells = <2>; #size-cells = <0>; cpu-map { cluster0 { core0 { cpu = <&cpu_10>; }; core1 { cpu = <&cpu_11>; }; core2 { cpu = <&cpu_12>; }; core3 { cpu = <&cpu_13>; }; }; cluster1 { core0 { cpu = <&cpu_b0>; }; core1 { cpu = <&cpu_b1>; }; }; }; cpu_10: cpu@0 { device_type = "cpu"; compatible = "arm.context-a53", "arm.armv8"; }; cpu_11: cpu@1 { device_type = "cpu"; compatible = "arm.context-a53", "arm.armv8"; }; cpu_12: cpu@2 { device_type = "cpu"; compatible = "arm.context-a53", "arm.armv8"; }; cpu_13: cpu@3 { device_type = "cpu"; compatible = "arm.context-a53", "arm.armv8"; }; cpu_b0: cpu@100 { device_type = "cpu"; compatible = "arm.context-a72", "arm.armv8"; }; cpu_b1: cpu@101 { device_type = "cpu"; compatible = "arm.context-a72", "arm.armv8"; }; }; ``` **举例4:描述一个16核CPU,一个物理插槽,每个插槽中有2个集群,每个CPU里面有两个线程。** ```c cpus { #size-cells = <0>; #address-cells = <2>; cpu-map { socket0 { cluster0 { cluster0 { core0 { thread0 { cpu = <&&PU0>; }; thread1 { cpu = <&&PU1>; }; }; core1 { thread0 { cpu = <&&PU2>; }; thread1 { cpu = <&&PU3>; }; }; }; cluster1 { core0 { thread0 { cpu = <&&PU4>; }; thread1 { cpu = <&&PU5>; }; }; core1 { thread0 { cpu = <&&PU6>; }; thread1 { cpu = <&&PU7>; }; }; }; }; cluster1 { cluster0 { core0 { thread0 { cpu = <&&PU8>; }; thread1 { cpu = <&&PU9>; }; }; core1 { thread0 { cpu = <&&PU10>; }; thread1 { cpu = <&&PU11>; }; }; }; cluster1 { core0 { thread0 { cpu = <&&PU12>; }; thread1 { cpu = <&&PU13>; }; }; core1 { thread0 { cpu = <&&PU14>; }; thread1 { cpu = <&&PU15>; }; }; }; }; }; }; }; ``` *** ** * ** *** #### 实例分析--GPIO ```c gpio0: gpio@fdd60000 { compatible = "rockchip,gpio-bank"; reg = <0x0 0xfdd60000 0x0 0x100>; interrupts = ; clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>; gpio-controller; #gpio-cells = <2>; gpio-ranges = <&pinctrl 0 0 32>; interrupt-controller; #interrupt-cells = <2>; }; ft5x06: ft5x06@38 { status = "disabled"; compatible = "edt,edt-ft5306"; reg = <0x38>; touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>; interrupt-parent = <&gpio0>; interrupts = ; reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>; touchscreen-size-x = <800>; touchscreen-size-y = <1280>; touch_type = <1>; }; ``` 代码第1--12行是RK原厂工程师编写的。代码14--25 是驱动开发工程师编写的。 第7行代码,gpio0是一个GPIO控制器,第8行,后面引用这个GPIO管脚的,需要2个参数描述这个GPIO(对应第21行代码)。 *** ** * ** *** **总结:** 1. 在GPIO控制器中,必须有一个属性#gpio-cells,表示其他节点如果使用这个GPIO控制器需要几个cell来表示使用哪一个GPIO。 2. 在GPIO控制器中,必须有一个属性gpio-controller,表示他是GPIO控制器。 3. 在设备树中使用GPIO,需要使用属性data-gpios=\<\&gpio1 12 0\>来指定具体的GPIO引脚。data-gpios属性可以为自定义属性。 **举例(简化):** ```c gpio1: gpio1 { gpio-controller; #gpio-cells = <2>; }; [...] data-gpios = <&gpio1 12 0>, <&gpio1 15 0>; ``` **其他属性** 1. **`ngpios = <18>`** * **作用**:指定GPIO控制器管理的GPIO引脚总数 * **值说明** :`18` 表示该GPIO控制器有18个可用引脚(编号0-17) * **必要性**:必需属性,驱动程序需要此信息初始化GPIO芯片 2. **`gpio-reserved-ranges = <0 4>, <12 2>`** * **作用**:指定保留/不可用的GPIO范围 * **格式** :`<起始引脚 数量>` 对 * **值说明** : * `<0 4>`:保留0-3号引脚(共4个) * `<12 2>`:保留12-13号引脚(共2个) * **应用场景** : * 硬件设计上某些GPIO有特殊用途 * 防止驱动误用关键系统引脚 3. **`gpio-line-names`** * **作用**:为每个GPIO引脚指定用户友好的名称 * **格式**:字符串列表,按引脚顺序排列 * **值说明** :18个名称对应18个GPIO引脚: * 0: "MMC-CD"(SD卡检测) * 1: "MMC-WP"(SD卡写保护) * 2: "VDD eth"(以太网电源) * ...直到17: "reset"(复位引脚) 4.`gpio-ranges` ​ `gpio-ranges` 主要用于定义 GPIO 控制器管理的 GPIO 引脚与物理 SoC 引脚之间的映射关系。 **为什么需要 gpio-ranges?** 在复杂的 SoC 系统中: * 一个物理引脚可能被配置为 GPIO 或外设功能(如 UART、I2C) * GPIO 控制器看到的 GPIO 编号是"虚拟"的 * 需要将 GPIO 控制器的虚拟编号映射到物理引脚的实际位置 gpio-ranges 是一个三元组或四元组列表: ```c gpio-ranges = <&pinctrl_phandle gpio_offset pin_offset count>; 或者 gpio-ranges = <&pinctrl_phandle gpio_offset pin_offset count &pinctrl_phandle gpio_offset2 pin_offset2 count2>; ``` 参数说明: * \&pinctrl_phandle:指向引脚控制器节点的引用 * pin_offset:在引脚控制器中的起始物理引脚号 * gpio_offset:在 GPIO 控制器中的起始 GPIO 号 * count:要映射的连续引脚数量 **举例1:** ```c gpio-controller@00000000 { compatible = "foo"; reg = <0x00000000 0x1000>; gpio-controller; #gpio-cells = <2>; ngpios = <18>; gpio-reserved-ranges = <0 4>, <12 2>; gpio-line-names = "MMC-CD", "MMC-WP", "VDD eth", "RST eth", "LED R", "LED G", "LED B", "Col A", "Col B", "Col C", "Col D", "Row A", "Row B", "Row C", "Row D", "NMI button", "poweroff", "reset"; }; ``` ​ 第6行,`ngpios = <18>`表示一共18个GPIO引脚。 ​ 第7行,`<0 4>`表示保留引脚:0,1,2,3;`<12 2>` 表示保留GPIO引脚12,13 ​ 第18行,表示18个GPIO对应的名字。 **举例2:gpio-ranges 用法** ```c /* 引脚控制器 */ pinctrl: pinctrl@1000000 { compatible = "vendor,pinctrl"; reg = <0x1000000 0x1000>; }; /* GPIO 控制器 */ gpio0: gpio@2000000 { compatible = "vendor,gpio-controller"; reg = <0x2000000 0x1000>; gpio-controller; #gpio-cells = <2>; /* 映射关系 */ gpio-ranges = <&pinctrl 0 0 32>; // 将0~31 pin 映射到GPIO 控制器0~31 }; ``` *** ** * ** *** #### 引入pinmux概念 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/10ee20d2056b412685e86574282feb3e.png) ​ AE24这根GPIO管脚,有GPIO得功能GPIO0_A6_d,PCIE30X2_CLKREQn_M0, SATA_CP_POD,GPU_PWREN复用功能。 ​ AE24表示芯片上的物理坐标: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/bc4abf36578e426c9ed67c7a30e1bc41.png) ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/22c50df47283440eb28a6c6332b1cdb9.png) ##### pinmux工作方式 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7005d85150554b9ca9b95c496ecb4ba5.png) ##### **pinctrl简介** ​ Linux内核提供了pinctrl子系统,pinctrl是pin controller的缩写,目的是为了统一各芯片原厂的pin脚管理。所以一般pinctrl子系统的驱动是由芯片原厂的BSP工程师实现。有了pinctrl子系统以后,驱动工程师就可以通过配置设备树使用pinctrl子系统去设置管脚的复用以及管脚的电气属性。 #### pinctrl语法 ​ pinctrl的语法我们可以看作是由两个部分组成,以部分是客户端,一部分是服务器段。 **举例1:** ```c // client端: &i2c2 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; }; // service端 &iomuxc { pinctrl_i2c2: i2c2grp { fsl,pins = < MX6UL_PAD_UART5_TX_DATA__I2C2_SCL 0x4001b8b0 MX6UL_PAD_UART5_RX_DATA__I2C2_SDA 0x4001b8b0 >; }; }; ``` 1. **client端 (I2C2设备)** : * `pinctrl-names = "default"`:定义引脚控制状态名称 * `pinctrl-0 = <&pinctrl_i2c2>`:引用具体的引脚配置组 2. **service端 (iomuxc引脚控制器)** : * `pinctrl_i2c2: i2c2grp`:定义引脚配置组(标签为`pinctrl_i2c2`) * `fsl,pins`:指定具体的引脚配置(NXP i.MX平台特有属性) * `MX6UL_PAD_UART5_TX_DATA__I2C2_SCL`:将UART5_TX引脚复用为I2C2_SCL功能 * `MX6UL_PAD_UART5_RX_DATA__I2C2_SDA`:将UART5_RX引脚复用为I2C2_SDA功能 * `0x4001b8b0`:引脚电气属性配置值(包括上下拉、驱动强度等) > 此配置实现了I2C2控制器的引脚复用:将原本用于UART5的引脚重新配置为I2C2功能,并设置电气特性。 **举例2:** ```c pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1>; ``` **解析:** 使用`pinctrl-names`表示设备的状态。这里只有一个`default`状态,`default`为第0个状态。`pinctrl-0 = <&pinctrl_hog_1>`表示第0个状态`default`对应的引脚在`pinctrl_hog_1`节点中配置。 **举例3:** ```c pinctrl-names = "default", "wake_up"; pinctrl-0 = <&pinctrl_hog_1>; pinctrl-1 = <&pinctrl_hog_2>; ``` **解析:** 使用`pinctrl-names`表示设备的状态。这里有`default`和`wake_up`两个状态,`default`为第0个状态,`wake_up`为第1个状态。`pinctrl-0 = <&pinctrl_hog_1>`表示第0个状态`default`对应的引脚在`pinctrl_hog_1`节点中配置。`pinctrl-1`同理。 **举例4:** ```c pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>; ``` **解析:** 使用`pinctrl-names`表示设备的状态。这里只有一个`default`状态,`default`为第0个状态。`pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>`表示第0个状态`default`对应的引脚在`pinctrl_hog_1`和`pinctrl_hog_2`两个节点中配置。 **瑞芯微pinctrl示例:** ```c // RK3399 示例 led { pinctrl-names = "default"; pinctrl-0 = <&led1_cli>; }; led1_cli: led1-cli { rockchip,pins = <0 12 RK_FUNC_GPIO &pcfg_pull_up>; }; // RK3568 示例 &uart7 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart7m1_xfer>; }; uart7m1_xfer: uart7m1-xfer { rockchip,pins = /* uart7_rxm7l */ <3 RK_PC5 4 &pcfg_pull_up>, /* uart7_txm7l */ <3 RK_PC4 4 &pcfg_pull_up>; }; // 功能宏定义 #define RK_FUNC_GPIO 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 ``` 代码2\~5行,RK3399 pinctrl客户端的代码。 第8行代码,`<0 12 RK_FUNC_GPIO &pcfg_pull_up>`, 第1个参数0,表示GPIO0组,第2个参数表示GPIO0_12, 第三个参数`RK_FUNC_GPIO`表示这个管脚复用为GPIO功能,第4个参数表示电器特性,有以下几种可以选择: * `&pcfg_pull_up`:上拉电阻使能 * `&pcfg_pull_down`:下拉电阻使能 * `&pcfg_pull_none`:无上/下拉 * `&pcfg_output_high`:输出高电平 * `&pcfg_output_low`:输出低电平 第21行代码,`<3 RK_PC5 4 &pcfg_pull_up>`, 将GPIO3里面的C5 设置为**功能 4**(UART_RX),电器属性为上拉电阻使能。 第22行代码,`<3 RK_PC4 4 &pcfg_pull_up>`, 将GPIO3里面的C4 设置为功能 4(UART_TX),电器属性为上拉电阻使能。 **功能4**:根据RK3568手册,ALT4对应UART功能。 *** ** * ** *** #### 实践--pinctrl设置管脚复用关系 ​ 本小节将通过上面学到的 pinctrl 相关知识,将外接 led 灯的控制引脚复用为 GPIO 模式。首先来对 rk3588 的设备树结构进行以下介绍,根据 sdk 源码目录下的 "device/rockchip/rk3588/BoardConfig-rk3588-evb7-lp4-v10.mk" 默认配置文件可以了解到编译的设备树为 rk3588-evb7-lp4-v10-linux.dts,整理好的设备树之间包含关系列表如下所示: | 顶层设备树 | rk3588-evb7-lp4-v10-linux.dts | | | |:-----------|:------------------------------|-------------------|---------------------------| | **第二级设备树** | rk3588-evb7-lp4.dtsi | rk3588-linux.dtsi | topeet_rk3588_config.dtsi | | **第三级设备树** | rk3588.dtsi | | | | | rk3588-evb.dtsi | | | | | rk3588-rk806-single.dtsi | | | | | topeet_screen_lcds.dts | | | | | topeet_camera_config.dtsi | | | ​ 打开rk3588-evb7-lp4.dtsi,到根节点最后添加代码: ```c vbus5v0_typec: vbus5v0-typec { ...; }; //大概是在vbus5v0_typec设备树节点附近,添加如下代码 my_led:led { compatible = "topeet,led"; gpios = <&gpio2 RK_PC4 GPIO_ACTIVE_HIGH>; pinctrl-names = "default"; pinctrl-0 = <&rk_led_gpio>; }; ``` ​ 第1行:节点名称为led,标签名为my_led。 ​ 第2行:compatible属性指定了设备的兼容性标识,即设备与驱动程序之间的匹配规则。 在这里,设备标识为"topeet,led",表示该 LED 设备与名为 "topeet,led" 的驱动程序兼容。 ​ 第3行:gpios属性指定了与LED相关的GPIO(通用输入/输出)引脚配置。 ​ 第4行:pinctrl-names 属性指定了与引脚控制相关的命名。default表示状态 0 。 ​ 第5行:pinctrl-0属性指定了与pinctrl-names属性中命名的引脚控制相关联的实际引脚控 制器配置。\<\&rk_led_gpio\>表示引用了名为rk_led_gpio的引脚控制器配置。 ​ 然后继续找到在同一设备树文件的pinctrl服务端节点在该节点添加led控制引脚pinctrl服 务端节点,仿写完成的节点内容如下所示: ```c &pinctrl { rk_led { rk_led_gpio:rk-led-gpio { rockchip,pins = <2 RK_PC4 RK_FUNC_GPIO &pcfg_pull_none>; }; }; ...; } ``` ​ 接下来编译内核,如果没有报错,则说明我们添加的led设备树节点没有问题。 *** ** * ** *** #### 无设备树参考节点? 没有参考节点概率不大,如果真没有, 参考文档:`kernel/Documentation/devicetree/bindings` *** ** * ** *** ### 3.分析DTB格式 #### DTB文件格式 ```c /dts-v1/; / { model = "This is my devicetree!"; #address-cells = <1>; #size-cells = <1>; chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200"; }; cpu1: cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a35", "arm,armv8"; reg = <0x0 0x1>; }; aliases { led1 = "/gpio@22020101"; }; node1 { #address-cells = <1>; #size-cells = <1>; gpio@22020102 { reg = <0x20220102 0x40>; }; }; node2 { node1-child { pinnum = <01234>; }; }; gpio@22020101 { compatible = "led"; reg = <0x20220101 0x40>; status = "okay"; }; }; ``` ​ 上述设备树源码编译以后得到dtb文件,使用二进制查看软件打开得到内容。(二进制软件为Binary Viewer, 下载地址:[Binary Viewer - Download](https://binary-viewer.en.lo4d.com/windows)) ​ small header(头部),memory reservation block(内存预留块),structure block(结构块),strings block(字符串块)。free space(自由空间)不一定存在。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4f1db49058a64735b5bbf0f61d828821.png) ##### 1.header ```c struct fdt_header { uint32_t magic; uint32_t totalsize; uint32_t off_dt_struct; uint32_t off_dt_strings; uint32_t off_mem_rsvmap; uint32_t version; uint32_t last_comp_version; uint32_t boot_cpuid_phys; uint32_t size_dt_strings; uint32_t size_dt_struct; }; 注意,所有成员类型均为 u32。为大端模式。 ``` ###### 成员介绍 | 字段 | 十六进制数值 | 代表含义 | |:------------------|:---------|:---------------------------------------------------------------| | magic | D00DFEED | 固定值 | | totalsize | 000002A4 | 转换为十进制为676,表示文件大小为676字节 | | off_dt_struct | 00000038 | 结构块从00000038地址开始,结合size_dt_struct确定结构块存储范围 | | off_dt_strings | 0000024C | 字符串块从0000024C地址开始,结合size_dt_strings确定字符串块存储范围 | | off_mem_rsvmap | 00000028 | 内存保留块偏移地址为00000028,位于header之后、结构块之前 | | version | 00000011 | 11(十六进制) = 17(十进制),表示当前设备树结构版本为17 | | last_comp_version | 00000010 | 10 转换为十进制之后为16,表示向前兼容的设备树结构 版本为16 | | boot_cpuid_phys | 00000000 | 表示设备树的teg属性为0 | | size_dt_strings | 00000058 | 表示字符串块的大小为 00000058 ,和前面的 off_dt_strings 字符串块偏移值一起可以确定字符串块的 范围 | | size_dt_struct | 00000214 | 表示结构块的大小为00000214,和前面的off_dt_struct 结构块偏移值一起可以确定结构块的范围 | ##### 2.内存保留块 ​ 如果在 `dts` 文件中使用 `memreserve` 描述保留的内存,保留内存的大小就会在这部分保存。 `memreserve` 的使用方法: ```c /memreserve/

; ``` ​ 其中 `
` 和 `` 是 64 位 C 风格整数,例如: ```c /* Reserve memory region 0x10000000..0x10003fff */ /memreserve/ 0x10000000 0x4000; ``` ​ 在内存保留块的存储格式: ```c struct fdt_reserve_entry { uint64_t address; uint64_t size; }; ``` ##### 3.字符串块 字符串块用来存放属性的名字,比如 compatible、reg 等。通过分析 DTB 的头部,我们已经知道字符串块的位置,如 model 在 DTB 中的表示: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/780a664aa9404e49b1c1ddf38e19a843.png) ##### 4.结构块 ​ 结构块描述的是设备树的结构,也就是设备树的节点。那如何表示一个节点的开始和结束呢?使用 `0x00000001` 表示节点的开始,然后跟上节点名字(根节点的名字用 `0` 表示),然后使用 `0x00000003` 表示一个属性的开始(每表示一个属性,都要用 `0x00000003` 表示开始),使用 `0x00000002` 表示节点的结束,使用 `0x00000009` 表示根节点的结束(整个结构块的结束) 属性的名字和值用结构体表示: ```C struct { uint32_t len; uint32_t nameoff; } ``` * `len` 表示属性值的长度 * `nameoff` 表示属性名字在字符串块中的偏移 ​ 例子中以下节点在 DTB 中是如何表示的呢? ```c { model = "This is my devicetree!"; #address-cells = <1>; #size-cells = <1>; chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; }; ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/554633f30c8c454a9a8309e3609cf17e.png) *** ** * ** *** #### dtb展开成device_node ##### 设备树是如何传递给内核的 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/8251279ec5f34aa0891e8067a28d5363.png) 1. **编译阶段**:DTC将.dts编译为.dtb二进制格式 2. **加载阶段**:U-Boot将内核和.dtb加载到内存 3. **展开阶段**:内核解析.dtb,构建设备树数据结构 4. **使用阶段**:驱动程序通过设备树API获取硬件信息 ​ 由于内核并不认识dtb文件,需要将dtb文件展开为device_node结构体后,方可识别。 ​ `struct device_node`此结构体定义在Linux内核头文件:`include/linux/of.h`,它是内核中表示设备树节点的核心数据结构, 结构体如下所示: ```c struct device_node { const char *name; // 节点中的 name 属性 const char *type; // 节点中的 device_type 属性 phandle phandle; const char *full_name; // 节点的名字 struct fwnode_handle fwnode; struct property *properties; // 指向该设备节点下的第一个属性,其他属性与该属性链表相连 struct property *deadprops; struct device_node *parent; // 节点的父节点 struct device_node *child; // 节点的子节点 struct device_node *sibling; // 节点的同级节点,也可以叫兄弟节点 // ... 其他成员 }; ``` ​ `struct property`此结构体定义在Linux内核头文件:`include/linux/of.h`,结构体如下所示: ```c struct property { char *name; // 属性名字 int length; // 属性值的长度 void *value; // 属性值 struct property *next; // 指向该节点的下一个属性 // ... 其他成员 }; ``` 1. 每个字段的作用: * `name`:属性名称字符串(如"compatible", "reg"等) * `length`:属性值的字节长度 * `value`:指向属性值的指针 * `next`:指向同一节点的下一个属性,形成链表 2. 设备树节点的所有属性通过`next`指针连接成单向链表: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/dd828f50a9d14e95ba134315eb93097e.png) DTB展开为device_node,链表逻辑结构图: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/764fe200c8634e7383cf661cd9a7a26f.png) ##### 实例:dtb展开成device_node ​ 首先来到源码目录下的"/init/main.c"文件,找到其中的start_kernel函数,start_kernel函 数是 Linux 内核启动的入口点,它是Linux内核的核心函数之一,负责完成内核的初始化和启动过程,具体内容如下所示: ```c asmlinkage __visible void __init __no_sanitize_address start_kernel(void) { char*command_line; char*after_dashes; set_task_stack_end_magic(&init_task); //设置任务栈的魔数 smp_setup_processor_id(); //设置处理器ID debug_objects_early_init(); //初始化调试对象 cgroup_init_early(); //初始化cgroup(控制组) local_irq_disable(); //禁用本地中断 early_boot_irqs_disabled=true; //标记早期引导期间中断已禁用 /* *中断仍然被禁用。进行必要的设置,然后启用它们。 */ boot_cpu_init(); //初始化引导CPU page_address_init(); //设置页地址 pr_notice("%s",linux_banner); //打印Linux内核版本信息 setup_arch(&command_line); //架构相关的初始化 mm_init_cpumask(&init_mm); //初始化内存管理的cpumask(CPU掩码) setup_command_line(command_line); //设置命令行参数 setup_nr_cpu_ids(); //设置CPU个数 setup_per_cpu_areas(); //设置每个CPU的区域 smp_prepare_boot_cpu(); //准备启动CPU(架构特定的启动CPU钩子) boot_cpu_hotplug_init(); //初始化热插拔的引导CPU build_all_zonelists(NULL); //构建所有内存区域列表 page_alloc_init(); //初始化页面分配器 ........ } ``` ​ 代码第17行`setup_arch(&command_line);`该函数定义在内核源码的 `/arch/arm64/kernel/setup.c`文件中,具体内容如下所示: ```c void __init __no_sanitize_address setup_arch(char **cmdline_p) { ...; setup_machine_fdt(__fdt_pointer); // 设置机器的FDT(平台设备树) ...; if (acpi_disabled) unflatten_device_tree(); // 展开设备树 } ``` ​ 在setup_arch函数中与设备树相关的函数分别为第4行的`setup_machine_fdt(__fdt_pointer)`和第8行的`unflatten_device_tree()`,接下来将对上述两个函数进行详细的介绍。 ###### setup_machine_fdt(__fdt_pointer) ​ setup_machine_fdt(fdt_pointer)中的fdt_pointer是dtb二进制文件加载到内存的地址, 该地址由bootloader启动kernel时通过x0寄存器传递过来的,具体的汇编代码在内核源码目 录下的`/arch/arm64/kernel/head.S`文件中,具体内容如下所示: ```shell preserve_boot_args: mov x21, x0 //x21=FDT __primary_switched: str_l x21, __fdt_pointer, x5 //Save FDT pointer ``` ​ 第2行:将寄存器x0的值复制到寄存器x21。x0寄存器中保存了一个指针,该指针指向设 备树(Device Tree)。 ​ 第4行:将寄存器x21的值存储到内存地址__fdt_pointer中。 然后来看setup_machine_fdt函数,该函数定义在内核源码的"/arch/arm64/kernel/setup.c" 文件中,具体内容如下所示: ```c static void __init setup_machine_fdt(phys_addr_t dt_phys) { int size; //将设备树物理地址映射到内核虚拟地址空间 void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL); const char *name; if (dt_virt) //保留设备树占用的内存区域 memblock_reserve(dt_phys, size); if (!dt_virt || !early_init_dt_scan(dt_virt)) { pr_crit("\n" "Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n" "The dtb must be 8-byte aligned and must not exceed 2 MB in size\n" "\nPlease check your bootloader.", &dt_phys, dt_virt); while (true) cpu_relax(); } /* Early fixups are done, map the FDT as read-only now */ fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO); //获取设备树的机器名 name = of_flat_dt_get_machine_name(); if (!name) return; pr_info("Machine model: %s\n", name); dump_stack_set_arch_desc("%s (DT)", name); } ``` ​ 第5行代码:fixmap_remap_fdt()将设备树映射到内核虚拟地址空间中的fixmap区域。 ​ 第10行代码:如果映射成功,则使用memblock_reserve()保留设备树占用的物理内存区域。 ​ 第12行代码:调用函数early_init_dt_scan(dt_virt),该函数功能是检查设备树的有效性和完整性,如果设备 树无效或扫描失败,则会输出错误信息并进入死循环。,该函数定义在内核源 码的"drivers/of/fdt.c"目录下,具体内容如下所示: ```c bool __init early_init_dt_scan(void *params) { bool status; //验证设备树的兼容性和完整性 status = early_init_dt_verify(params); if (!status) return false; //扫描设备树节点 early_init_dt_scan_nodes(); return true; } ``` ​ 第5行代码:首先,调用early_init_dt_verify()函数对设备树进行兼容性和完整性验证。该函数可能会检 查设备树中的一致性标记、版本信息以及必需的节点和属性是否存在。如果验证失败,函数会 返回false。该函数的具体内容如下所示: ```c bool __init early_init_dt_verify(void *params) { if (!params) return false; /* 检查设备树头部的有效性 */ if (fdt_check_header(params)) return false; /* 设置指向设备树的指针为传入的参数 */ initial_boot_params = params; /* 计算设备树的CRC32校验值, 将结果保存到of_fdt_crc32中 */ of_fdt_crc32 = crc32_be(~0, initial_boot_params, fdt_totalsize(initial_boot_params)); return true; } ``` ​ 第7行代码,检测设备树DTB的`header`是否合法,检查设备树头部的有效性。fdt_check_header是一个用于检查设备树头部的函数, 如果设备树头部无效,则返回false,表示设备树不合法。 ​ 第11行代码,保存设备树指针。 ​ 第14行代码,计算设备树CRC32校验值。 ​ 然后继续回到early_init_dt_scan()函数中,如果设备树验证成功(即status为真),则调 用early_init_dt_scan_nodes()函数。这个函数的作用是扫描设备树的节点并进行相应的处理, 该函数的具体内容如下所示: ```c void __init early_init_dt_scan_nodes(void) { int rc = 0; /*从/chosen节点中检索各种信息 */ rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); if (!rc) pr_warn("No chosen node found, continuing without\n"); /* 初始化{size,address}-cells信息 */ of_scan_flat_dt(early_init_dt_scan_root, NULL); /* 设置内存信息,调用early_init_dt_add_memory_arch函数 */ of_scan_flat_dt(early_init_dt_scan_memory, NULL); } ``` ​ 函数early_init_dt_scan_nodes 被声明为__init,这表示它是在内核初始化阶段被调用,并 且在初始化完成后不再需要。该函数的目的是在早期阶段扫描设备树节点,并执行一些初始化 操作。 ​ 函数中主要调用了of_scan_flat_dt函数,该函数用于扫描平面设备树(flatdevicetree)。 平面设备树是一种将设备树以紧凑形式表示的数据结构,它不使用树状结构,而是使用线性结构,以节省内存空间。 具体来看,early_init_dt_scan_nodes 函数的执行步骤如下: ​ (1)of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line):从设备树的/chosen 节点中检索各种信息。/chosen节点通常包含了一些系统的全局配置参数,比如命令行参数。 early_init_dt_scan_chosen 是一个回调函数,用于处理/chosen 节点的信息。boot_command_line 是一个参数,表示内核启动时的命令行参数。 ​ (2)of_scan_flat_dt(early_init_dt_scan_root, NULL):初始化{size,address}-cells 信息。 {size,address}-cells 描述了设备节点中地址和大小的编码方式。early_init_dt_scan_root 是一个回 调函数,用于处理设备树的根节点。 ​ (3)of_scan_flat_dt(early_init_dt_scan_memory, NULL) : 设 置 内 存 信 息 , 并 调 用 early_init_dt_add_memory_arch 函数。这个步骤主要用于在设备树中获取内存的相关信息,并 将其传递给内核的内存管理模块。early_init_dt_scan_memory是一个回调函数,用于处理内存 信息。 ###### unflatten_device_tree() ​ 该函数用于解析设备树,将紧凑的设备树数据结构转换为树状结构的设备树,该函数定义 在内核源码目录下的"/drivers/of/fdt.c"文件中,具体内容如下所示: ```c void __init unflatten_device_tree(void) { /* 解析设备树 */ __unflatten_device_tree(initial_boot_params, NULL, &of_root, early_init_dt_alloc_memory_arch, false); /* 获取指向 "/chosen" 和 "/aliases" 节点的指针,以供全局使用 */ of_alias_scan(early_init_dt_alloc_memory_arch); /* 运行设备树的单元测试 */ unittest_unflatten_overlay_base(); } ``` ​ 该函数主要用于解析设备树,并将解析后的设备树存储在全局变量of_root中。 函数首先调用__unflatten_device_tree函数来执行设备树的解析操作。解析后的设备树将 使用of_root指针进行存储。 接下来,函数调用of_alias_scan函数。这个函数用于扫描设备树中的/chosen和/aliases节 点,并为它们分配内存。这样,其他部分的代码可以通过全局变量访问这些节点。 最后,函数调用unittest_unflatten_overlay_base函数,用于运行设备树的单元测试。 ​ 然后对__unflatten_device_tree这一设备树的解析函数进行详细的介绍,该函数的具体内容 如下所示: ```c void *__unflatten_device_tree(const void *blob, struct device_node *dad, struct device_node **mynodes, void *(*dt_alloc)(u64 size, u64 align), bool detached) { int size; void *mem; pr_debug(" -> unflatten_device_tree()\n"); if (!blob) { pr_debug("No device tree pointer\n"); return NULL; } pr_debug("Unflattening device tree:\n"); pr_debug("magic: %08x\n", fdt_magic(blob)); pr_debug("size: %08x\n", fdt_totalsize(blob)); pr_debug("version: %08x\n", fdt_version(blob)); if (fdt_check_header(blob)) { pr_err("Invalid device tree blob header\n"); return NULL; } /* 第一遍扫描,计算大小 */ size = unflatten_dt_nodes(blob, NULL, dad, NULL); if (size < 0) return NULL; size = ALIGN(size, 4); pr_debug(" size is %d, allocating...\n", size); /* 为展开的设备树分配内存 */ mem = dt_alloc(size + 4, __alignof__(struct device_node)); if (!mem) return NULL; memset(mem, 0, size); *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef); pr_debug(" unflattening %p...\n", mem); /* 第二遍扫描,实际展开设备树 */ unflatten_dt_nodes(blob, mem, dad, mynodes); if (be32_to_cpup(mem + size) != 0xdeadbeef) pr_warn("End of tree marker overwritten: %08x\n", be32_to_cpup(mem + size)); if (detached && mynodes) { of_node_set_flag(*mynodes, OF_DETACHED); pr_debug("unflattened tree is detached\n"); } pr_debug(" <- unflatten_device_tree()\n"); return mem; } ``` ​ 该函数的重点在两次设备树的扫描上,第一遍扫描的目的是计算展开设备树所需的内存大 小。 ​ 第28行:unflatten_dt_nodes函数的作用是递归地遍历设备树数据块,并计算展开设备树 所需的内存大小。它接受四个参数:blob(设备树数据块指针)、start(当前节点的起始地址, 初始为NULL)、dad(父节点指针)和mynodes(用于存储节点指针数组的指针,初始为NULL)。 第一遍扫描完成后,unflatten_dt_nodes函数会返回展开设备树所需的内存大小,然后在对大 小进行对齐操作,并为展开的设备树分配内存。 ​ 第二遍扫描的目的是实际展开设备树,并填充设备节点的名称、类型和属性等信息。 ​ 第47行:再次调用了unflatten_dt_nodes函数进行第二遍扫描。通过这样的过程,第二遍扫描会将设备树数据块中的节点展开为真正的设备节点,并填充节点的名称、类型和属性等信 息。这样就完成了设备树的展开过程。 最后我们来对unflatten_dt_nodes函数内容进行一下深究,unflatten_dt_nodes函数具体定 义如下所示: ```c static int unflatten_dt_nodes(const void *blob, void *mem, struct device_node *dad, struct device_node **nodepp) { struct device_node *root; //根节点 int offset = 0, depth = 0, initial_depth = 0; //偏移量、深度和初始深度 #define FDT_MAX_DEPTH 64 //最大深度 struct device_node *nps[FDT_MAX_DEPTH]; //设备节点数组 void *base = mem; //基地址,用于计算偏移量 bool dryrun = !base; //是否只是模拟运行,不实际处理 if (nodepp) *nodepp = NULL; //如果指针不为空,将其置为空指针 /* * 如果@dad有效,则表示正在展开设备子树。 * 在第一层深度可能有多个节点。 * 将@depth设置为1,以使fdt_next_node()正常工作。 * 当发现负的@depth时,该函数会立即退出。 * 否则,除第一个节点外的设备节点将无法成功展开。 */ if (dad) depth = initial_depth = 1; root = dad; //根节点为@dad nps[depth] = dad; //将根节点放入设备节点数组 for (offset = 0; offset >= 0 && depth >= initial_depth; offset = fdt_next_node(blob, offset, &depth)) { if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH)) continue; // 如果未启用CONFIG_OF_KOBJ并且节点不可用,则跳过该节点 if (!IS_ENABLED(CONFIG_OF_KOBJ) && !of_fdt_device_is_available(blob, offset)) continue; //填充节点信息,并将子节点添加到设备节点数组 if (!populate_node(blob, offset, &mem, nps[depth], &nps[depth+1], dryrun)) return mem - base; if (!dryrun && nodepp && !*nodepp) *nodepp = nps[depth+1]; //将子节点指针赋值给@nodepp if (!dryrun && !root) root = nps[depth+1]; //如果根节点为空,则将子节点设置为根节点 } if (offset < 0 && offset != -FDT_ERR_NOTFOUND) { pr_err("Error %d processing FDT\n", offset); return -EINVAL; } //反转子节点列表。一些驱动程序假设节点顺序与.dts文件中的节点顺序一致 if (!dryrun) reverse_nodes(root); return mem - base; //返回处理的字节数 } ``` ​ `unflatten_dt_nodes`函数的作用我们在上面已经讲解过了,这里重点介绍第31行的`fdt_next_node()`函数和第41行的`populate_node`函数。 ​ `fdt_next_node()`函数用来遍历设备树的节点。从偏移量为0开始,只要偏移量大于等于0 且深度大于等于初始深度,就执行循环。循环中的每次迭代都会处理一个设备树节点。 在每次迭代中,首先检查深度是否超过了最大深度`FDT_MAX_DEPTH`,如果超过了,则跳 过该节点。 ​ 如果未启用`CONFIG_OF_KOBJ`并且节点不可用(通过`of_fdt_device_is_available()`函数判 断),则跳过该节点。 ​ 随后调用`populate_node()`函数填充节点信息,并将子节点添加到设备节点数 组nps中。`populate_node()`函数定义如下所示: ```c static bool populate_node(const void *blob, int offset, void **mem, struct device_node *dad, struct device_node **pnp, bool dryrun) { struct device_node *np; //设备节点指针 const char *pathp; //节点路径字符串指针 unsigned int l, allocl; //路径字符串长度和分配的内存大小 pathp = fdt_get_name(blob, offset, &l); //获取节点路径和长度 if (!pathp) { *pnp = NULL; return false; } allocl = ++l; //分配内存大小为路径长度加一,用于存储节点路径字符串 np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl, __alignof__(struct device_node)); //分配设备节点内存 if (!dryrun) { char *fn; of_node_init(np); //初始化设备节点 np->full_name = fn = ((char *)np) + sizeof(*np); //设置设备节点的完整路径名 memcpy(fn, pathp, l); //将节点路径字符串复制到设备节点的完整路径名中 if (dad != NULL) { np->parent = dad; //设置设备节点的父节点 np->sibling = dad->child; //设置设备节点的兄弟节点 dad->child = np; //将设备节点添加为父节点的子节点 } } populate_properties(blob, offset, mem, np, pathp, dryrun); //填充设备节点的属性信息 if (!dryrun) { np->name = of_get_property(np, "name", NULL); //获取设备节点的名称属性 if (!np->name) np->name = ""; } *pnp = np; return true; } ``` ​ 在populate_node函数中首先会调用第18行的unflatten_dt_alloc函数分配设备节点内存。分配的内存大小为 sizeof(struct device_node) + allocl 字节,并使用 **alignof**(struct device_node) 对齐。然后调用populate_properties函数填充设备节点的属性信息。该函数会解 析设备节点的属性,并根据需要分配内存来存储属性值。 至此,关于dtb二进制文件的解析过程就讲解完成了,完整的源码分析流程图如下所示: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/03a5476be5534d9f82c17860d12ce5c7.png) #### device_node转化为platform_device ​ 设备树替换了平台总线模型当中对硬件资源描述的 device 部分。所以设备树也是对硬件资源进行描述的文件。在平台总线模型中,device 部分是用 platform_device 结构体来描述硬件资源的。所以内核最终会将内核认识的 device_node 树转换为 platform_device。但是并不是所有的节点都会被转换成 platform_device,只有满足要求的才会转换成 platform_device,转换成 platform_device 的节点可以在 `/sys/bus/platform/devices` 下查看。 ​ 节点要满足什么要求才会被转换成 platform_device 呢? ##### 转会规则 1. **根节点下包含 compatible 属性的子节点** * 例如: ```c / { mydevice { compatible = "vendor,device"; // ✅ 会被转换 }; }; ``` 2. **节点中 compatible 属性包含特定标识的节点** * 若节点的 compatible 属性包含以下值之一: * `"simple-bus"` * `"simple-mfd"` * `"isa"` * 则该节点下包含 compatible 属性的子节点会被转换 * 例如: ```c bus { compatible = "simple-bus"; // 标识符 #address-cells = <1>; #size-cells = <1>; child@0 { compatible = "vendor,child"; // ✅ 会被转换 }; }; ``` 3. **特殊排除规则** * 如果节点的 compatible 属性包含 `"arm,primecell"` 值 * 则该节点会被转换为 amba 设备(**不是** platform_device) * 例如: ```c uart0: serial@fe001000 { compatible = "arm,primecell", "arm,pl011"; // ❌ 转换为amba设备 reg = <0xfe001000 0x1000>; }; ``` *** ** * ** *** ​ 内核是如何将 device_node 转换为 platform_device 和上节课的转换规则是怎么来的。在内核启动的时候会执行 of_platform_default_populate_init 函数,这个函数是用 arch_initcall_sync 来修饰的。 arch_initcall_sync(of_platform_default_populate_init); ​ 所以系统启动的时候会调用 of_platform_default_populate_init 函数。 调用 参数 参数 参数 内部调用 参数 参数 参数 遍历节点 根据 设置 of_platform_default_populate_init of_platform_default_populate NULL NULL NULL of_platform_populate root (根节点) of_default_bus_match_table lookup.parent 对每个匹配节点调用 of_platform_bus_create of_platform_device_create_pdata of_device_alloc 设置platform_device资源 device_node属性 platform_device.resource of_default_bus_match_table {.compatible = 'simple-bus'} {.compatible = 'simple-mfd'} {.compatible = 'isa'} {.compatible = 'arm,amba-bus'} {} /\* NULL terminated list \*/ 关键函数说明: 1. **`of_platform_default_populate_init`** * 内核初始化时调用的入口函数 * 使用`arch_initcall_sync`修饰,在内核启动早期执行 2. **`of_platform_default_populate`** * 参数全为NULL表示使用默认值 * 实际调用`of_platform_populate` 3. **`of_platform_populate`** * 核心转换函数 * 参数: * `root`:设备树根节点 * `matches`:总线匹配表(`of_default_bus_match_table`) * `parent`:父设备(此处为NULL) 4. **`of_default_bus_match_table`** ```c static const struct of_device_id of_default_bus_match_table[] = { { .compatible = "simple-bus", }, { .compatible = "simple-mfd", }, { .compatible = "isa", }, #ifdef CONFIG_ARM_AMBA { .compatible = "arm,amba-bus", }, #endif {} /* 空值终止列表 */ }; ``` * 定义了哪些总线类型下的节点需要转换 5. **`of_platform_device_create_pdata`** * 为匹配的节点创建platform_device * 调用`of_device_alloc`分配设备资源 6. **`of_device_alloc`** * 从device_node提取资源信息 * 设置platform_device的resource数组 * 关键转换: * `reg`属性 → I/O内存资源 * `interrupts`属性 → IRQ资源 * `dma`属性 → DMA资源 ##### 资源转换示例: ```c // 设备树节点 serial@4000 { compatible = "ns16550a"; reg = <0x4000 0x100>; interrupts = <10 1>; }; ``` 转化如下: ```c // platform_device资源 static struct resource serial_resources[] = { [0] = { .start = 0x4000, // 寄存器起始地址 .end = 0x40FF, // 结束地址 (0x4000 + 0x100 - 1) .flags = IORESOURCE_MEM, }, [1] = { .start = 10, // 中断号 .end = 10, .flags = IORESOURCE_IRQ | IRQ_TYPE_EDGE_RISING, } }; ``` *** ** * ** *** #### 设备树下platform_device和platform_driver匹配 ​ 首先来对rk3588的设备树结构进行以下介绍,根据sdk源码目录下的"device/rockchip/r k3588/BoardConfig-rk3588-evb7-lp4-v10.mk"默认配置文件可以了解到编译的设备树为 rk3588 evb7-lp4-v10-linux.dts,整理好的设备树之间包含关系列表如下所示: | 顶层设备树 | rk3588-evb7-lp4-v10-linux.dts | | | |:-----------|:------------------------------|-------------------|---------------------------| | **第二级设备树** | rk3588-evb7-lp4.dtsi | rk3588-linux.dtsi | topeet_rk3588_config.dtsi | | **第三级设备树** | rk3588.dtsi | | | | | rk3588-evb.dtsi | | | | | rk3588-rk806-single.dtsi | | | | | topeet_screen_lcds.dts | | | | | topeet_camera_config.dtsi | | | ​ rk3588-evb7-lp4-v10-linux.dts 是顶层设备树,为了便于理解我们之后在该设备树下进行节 点的添加(当然这里也可以修改其他设备树),进入该设备树文件之后如下所示: ```c / { topeet { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; myLed{ compatible = "my_devicetree"; reg = <0xFEC30004 0x00000004>; }; }; }; ``` ​ 保存退出,重新编译内核文件。 ​ 修改设备驱动文件,代码路径:/home/topeet/Linux/my-test/44_devicetree_probe/platform_drv.c, 代码如下所示: ```c #include #include #include #include #include int my_platform_probe(struct platform_device *pdev) { printk(KERN_INFO "my_platform_probe: Probing platform device.\n"); return 0; } int my_platform_remove(struct platform_device *pdev) { printk("my_platform_driver: Removing platform device.\n"); return 0; } const struct platform_device_id mydriver_id_table = { .name = "my_platform_device", }; const struct of_device_id od_match_table_id[] = { {.compatible="my_devicetree"}, {} }; static struct platform_driver my_platform_driver = { .probe = my_platform_probe, .remove = my_platform_remove, .driver = { .name = "my_platform_device", .owner = THIS_MODULE, .of_match_table = od_match_table_id, }, .id_table = &mydriver_id_table, }; static int __init my_platform_driver_init(void) { int ret; ret = platform_driver_register(&my_platform_driver); if( ret ) { printk(KERN_ERR "Failed to register platform driver.\n"); return ret; } printk(KERN_INFO "my_platform_driver: Platform driver initialized.\n"); return 0; } static void __exit my_platform_driver_exit(void) { platform_driver_unregister(&my_platform_driver); printk(KERN_INFO "my_platform_driver: Platform driver exited.\n"); } module_init(my_platform_driver_init); module_exit(my_platform_driver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("YAN"); MODULE_VERSION("v1.0"); ``` ###### 查看设备树节点是否成功被加载到系统 ```shell ls /sys/firmware/devicetree/base/topeet/ '#address-cells' '#size-cells' compatible myLed name ``` ### 4.of操作函数 #### device_node结构体 ​ Linux 内核使用device_node结构体描述一个和结点,这个结构体定义在文件include/linux/of.h中: ```c struct device_node { const char *name; phandle phandle; const char *full_name; struct fwnode_handle fwnode; struct property *properties; struct property *deadprops; /* removed properties */ 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) unsigned int unique_id; struct of_irq_controller *irq_trans; #endif }; ``` #### of_ 函数操作集 ##### 1.节点查找函数 ###### of_find_node_by_name(from, name) ```c struct device_node *of_find_node_by_name(struct device_node *from, const char *name); ``` * 作用:通过节点名称查找设备树节点 * 参数: * `from`:起始节点(NULL 表示从根节点开始) * `name`:目标节点名称 * 返回值:成功返回节点指针,失败返回 NULL ###### of_find_node_by_path(path) ```c struct device_node *of_find_node_by_path(const char *path); ``` * 通过完整路径查找节点(如 `/soc/usb@fe800000`) ###### of_find_compatible_node(from, type, compatible) struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible); * 通过 compatible 属性查找节点 *** ** * ** *** ##### 2.属性操作函数 Linux内核使用property结构体来描述一个属性,这个结构体定义在文件:include/linux/of.h ```c struct property { char *name; int length; void *value; struct property *next; }; ``` ###### of_find_property(node, name, lenp) struct property *of_find_property(const struct device_node *np, const char *name, int *lenp); * 获取节点属性值 * `lenp`:返回属性长度 ###### of_property_read_xxx()系列 int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value); int of_property_read_string(struct device_node *np, const char *propname, const char **out_string); // 按索引的值index读取 int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index,u32 *out_value); * 读取不同类型的属性值(u8/u16/u32/u64/string/array) **of_property_read_u32和of_property_read_u32_index的区别:** | 特性 | `of_property_read_u32` | `of_property_read_u32_index` | |:----------|:-----------------------|:-----------------------------| | **读取目标** | 属性中的第一个值 | 属性中指定索引位置的值 | | **参数差异** | 不需要索引参数 | 需要明确指定索引位置 | | **适用场景** | 单值属性 | 多值数组属性中的特定元素 | | **返回值处理** | 只读取第一个值 | 可读取任意位置的指定值 | | **错误条件** | 属性不存在或长度不足4字节 | 索引越界或长度不足 | **使用场景区别** * **`of_property_read_u32`** 适用于单值属性: ```c clock-frequency = <50000000>; // 单个值 ``` ```c u32 clk_freq; of_property_read_u32(np, "clock-frequency", &clk_freq); ``` * **`of_property_read_u32_index`** 适用于多值数组中的特定元素: ```c reg = <0x40008000 0x1000>; // 两个值的数组 interrupts = <0 40 0x4>; // 三个值的数组 ``` ```c u32 irq_num; // 读取interrupts属性的第2个值(索引1) of_property_read_u32_index(np, "interrupts", 1, &irq_num); ``` *** ** * ** *** ###### of_property_count_elems_of_size ​ 该函数在设备树中的设备节点下查找指定名称的属性,并获取该属性中元素的数量。调用 该函数可以用于获取设备树属性中某个属性的元素数量,比如一个字符串列表的元素数量或一 个整数数组的元素数量等。 ```c #include int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size); ``` **函数参数**: ​ np:设备节点。 ​ propname:需要获取元素数量的属性名。 ​ elem_size:单个元素的尺寸。 **返回值**: ​ 如果成功获取了指定属性中元素的数量,则返回该数量;如果未找到属性或属性中没有元 素,则返回0。 ##### 3.节点遍历函数 ###### of_get_parent(node) ```c struct device_node *of_get_parent(const struct device_node *node); ``` * 获取父节点 ###### of_get_next_child(parent, prev) struct device_node *of_get_next_child(const struct device_node *parent, struct device_node *prev); * 遍历子节点(`prev = NULL` 开始) ###### of_get_next_available_child() * 获取下一个可用的子节点 *** ** * ** *** ##### 4.地址转化函数 ###### of_translate_address(node, in_addr) ```c u64 of_translate_address(struct device_node *np, const __be32 *addr); ``` * 将逻辑地址转换为物理地址 ###### of_iomap(node, index) ```c void __iomem *of_iomap(struct device_node *np, int index); ``` * 直接映射设备内存到虚拟地址空间 *** ** * ** *** ##### 5.中断相关函数 ###### of_irq_get(node, index) ```c int of_irq_get(struct device_node *np, int index); ``` * 获取中断号 ###### of_irq_to_resource_table() * 解析中断资源表 ###### gpio_to_irq() ```c int gpio_to_irq(unsigned int gpio) ``` **函数作用**: ​ 获取中断号。 **函数参数**: ​ gpio: gpio编号 **返回值**: ​ 成功返回对应中断号。 ###### irq_of_parse_and_map() ```c unsigned int irq_of_parse_and_map(struct device_node *dev, int index) ``` **函数作用**: ​ 从设备树节点的 `interrupts` 属性中解析并映射到对应的硬件中断号。 **函数参数**: ​ `*dev`: 目标设备节点 ​ `index`:要获取的中断在属性中的索引位置 **返回值**: ​ 成功:返回映射后的中断号 ​ 失败:返回0 ###### irqd_get_trigger_type() ```c u32 irqd_get_trigger_type(struct irq_data *d) ``` **函数作用**: ​ 从 `irq_data` 结构中获取中断触发类型标志 **函数参数**: ​ \*d: 指向 irq_data 结构的指针 **返回值**: ​ 成功 返回中断触发标志。 ​ 失败 返回0 ###### irq_get_irq_data() ```c struct irq_data *irq_get_irq_data(unsigned int irq) ``` **函数作用**: ​ 通过中断号获取对应的 `irq_data` 结构体 **函数参数**: ​ irq : 中断号 **返回值**: ​ 成功: 返回指向irq_data的指针。 ​ 失败:返回NULL。 **示例代码**: ```c #include // 1. 获取中断号 unsigned int irq = irq_of_parse_and_map(dev_node, 0); // 2. 获取irq_data结构 struct irq_data *irq_data = irq_get_irq_data(irq); if (!irq_data) { pr_err("无法获取irq_data\n"); return -ENODEV; } // 3. 获取中断触发类型 u32 trigger_type = irqd_get_trigger_type(irq_data); pr_info("中断触发类型: 0x%x\n", trigger_type); // 4. 根据类型处理中断 switch (trigger_type) { case IRQF_TRIGGER_RISING: pr_info("上升沿触发\n"); break; case IRQF_TRIGGER_FALLING: pr_info("下降沿触发\n"); break; case IRQF_TRIGGER_HIGH: pr_info("高电平触发\n"); break; case IRQF_TRIGGER_LOW: pr_info("低电平触发\n"); break; default: pr_warn("未知触发类型\n"); } ``` *** ** * ** *** #### ranges属性 ##### 1. **基本格式** ```c ranges = ; ``` 或 ```c ranges; // 空属性 ``` ##### 2. **字段说明** | 字段 | 说明 | 长度决定属性 | |:---------------------|:-----------|:---------------------------| | `child-bus-address` | 子地址空间的起始地址 | 由当前节点的 `#address-cells` 决定 | | `parent-bus-address` | 父地址空间的起始地址 | 由父节点的 `#address-cells` 决定 | | `length` | 映射区域的大小 | 由父节点的 `#size-cells` 决定 | ##### 3. **示例解析** ```c ranges = <0x0 0x20 0x100>; ``` * **含义** : * 子地址空间:`0x0` 到 `0x0 + 0x100`(`0x0-0x100`) * 父地址空间:`0x20` 到 `0x20 + 0x100`(`0x20-0x120`) * **映射关系** :子空间的 `0x0-0x100` 映射到父空间的 `0x20-0x120` ##### 4. **特殊值含义** | 属性值 | 含义 | |:----------------|:-----------------| | `ranges;` | 1:1 映射(内存区域直接映射) | | `ranges = < >;` | 无映射(地址空间不转换) | ##### 5. **关键属性依赖** ```c soc { #address-cells = <1>; // 父地址用1个32位数表示 #size-cells = <1>; // 长度用1个32位数表示 serial@4000 { #address-cells = <1>; // 子地址用1个32位数表示 #size-cells = <1>; // 子长度用1个32位数表示 ranges = <0x0 0x4000 0x1000>; // 含义: 子地址0x0-0x1000 → 父地址0x4000-0x5000 }; }; ``` ##### 6. **典型应用场景** ###### 场景1:内存映射外设 ```c // 父节点定义 soc { compatible = "simple-bus"; #address-cells = <2>; #size-cells = <2>; ranges; // 1:1映射 }; // 子节点(直接映射) uart0: uart@ff000000 { reg = <0x0 0xff000000 0x0 0x1000>; }; ``` ###### 场景2:地址转换(PCIe设备) ```c pcie_controller { #address-cells = <3>; #size-cells = <2>; // 子地址 → 父地址转换 ranges = <0x02000000 0 0xe0000000 0xc 0x20000000 0 0x20000000>; // 含义: // 子空间: PCIe内存空间 (0x02000000) // 父空间: 0xc20000000-0xc3fffffff }; ``` ###### 场景3:多级转换 ```c // 一级转换 soc { ranges = <0x0 0xf0000000 0x100000>; // 二级转换 i2c@1000 { ranges = <0x0 0x1000 0x100>; // 实际映射: // 子地址0x0 → soc地址0x1000 → 最终物理地址0xf0001000 }; }; ``` ##### 7. **字节序与数据格式** * 所有值均为 **大端序 (Big-Endian)** * 每个值占用32位(4字节) * 示例解析: ```c <0x00000000 0x20000000 0x00001000> // 等同于 <0x0 0x20 0x1000> // 简写形式 ``` ##### 8. **常见错误处理** ```c /* 错误示例1:长度不匹配 */ soc { #address-cells = <2>; // 需要2个地址值 ranges = <0x0 0x4000>; // 缺少长度值 → 编译错误 }; /* 错误示例2:未定义大小 */ serial@4000 { ranges = <0x0 0x4000 0x1000>; // 必须定义 #address-cells 和 #size-cells }; ``` *** ** * ** *** #### 参考资料 --- 设备树bindings文档 ​ 参考文档路径:`kernel/Documentation/devicetree/bindings` **bindings文档** ​ 设备节点里面除了一些标准的属性(课程中讲解的属性都是标准属性),但是当我们在接触一个新的节点的时候,有的属性不是标准属性,是芯片原厂自定义的属性,我们很难去看懂他是什么意思。这时候我们就可以去源码中查询bindings文档。一般在bindings中可以找到说明。 ​ bindings文档路径:内核源码下:Documentation/devicetree/bindings ​ 但是有的时候有些芯片在bindings中找不到文档,这时候可以去芯片原厂提供的资料中找下,如果也没有,可以咨询芯片供应商和FAE。

相关推荐
iceBin4 分钟前
uniapp打包安卓App热更新,及提示下载安装
android·前端
緈福的街口6 分钟前
【leetcode】20. 有效的括号
linux·算法·leetcode
杨充7 分钟前
高性能图片优化方案
android
望获linux19 分钟前
【Linux基础知识系列】第八篇-基本网络配置
linux·数据库·postgresql·操作系统·php·开源软件·rtos
墨狂之逸才20 分钟前
BindingAdapter名称的对应关系、命名规则和参数定义原理
android
现实与幻想~25 分钟前
Linux:shell脚本常用命令
linux·运维·服务器
德彪稳坐倒骑驴29 分钟前
Linux shell练习题
linux·运维·服务器
想睡hhh42 分钟前
Linux——初步认识Shell、深刻理解Linux权限
linux·运维·服务器·概念
hellokai42 分钟前
ReactNative介绍及简化版原理实现
android·react native
EchoZeal1 小时前
Linux远程连接主机——ssh命令详解
linux·运维·服务器·ssh