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。

相关推荐
踢球的打工仔8 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人8 小时前
安卓socket
android
快乐的学习8 小时前
开源相关术语及提交commit关键字总结
驱动开发·开源
t198751289 小时前
在Ubuntu 22.04系统上安装libimobiledevice
linux·运维·ubuntu
skywalk81639 小时前
linux安装Code Server 以便Comate IDE和CodeBuddy等都可以远程连上来
linux·运维·服务器·vscode·comate
晚风吹人醒.10 小时前
缓存中间件Redis安装及功能演示、企业案例
linux·数据库·redis·ubuntu·缓存·中间件
Hard but lovely11 小时前
linux: pthread库的使用和理解
linux
这儿有一堆花13 小时前
Kali Linux:探测存活到挖掘漏洞
linux·运维·服务器
松涛和鸣13 小时前
从零开始理解 C 语言函数指针与回调机制
linux·c语言·开发语言·嵌入式硬件·排序算法
安卓理事人14 小时前
安卓LinkedBlockingQueue消息队列
android