第五十二章 初识设备树
52.1 设备树介绍
设备树(Device Tree)是嵌入式系统和Linux内核中用于描述硬件的一种机制。
设备树概述
目的:描述硬件设备的特性、连接关系和配置信息。
优势:与平台无关,提高系统可移植性和可维护性。
传统方式(platform_device)
使用 platform_device结构体描述硬件设备。
包含设备名称、资源(内存地址、中断号等)、驱动程序信息。
struct platform_device { const char *name; struct resource *resource; int num_resources; // 其他字段...
};
传统方式的挑战
ARM Linux中存在大量杂乱且重复的平台相关配置代码。
维护困难,工作量增加。
Linus Torvalds对 ARM配置方式的不满引发广泛讨论。
设备树的引入
提供更灵活和可移植的硬件描述机制。
使用结构化数据格式描述设备节点、属性和连接关系。
/ { soc { uart@4000C000 { compatible = "arm,pl011"; reg = <0x4000C000 0x1000>; interrupts = <0 24 4>; }; };
};
设备树的优势
统一硬件描述方式,简化不同芯片和板级的支持。
提供硬件配置的可视化和可读性。
广泛应用于ARM架构,并扩展到其他架构和平台。
52.2 设备树基础知识
DTS(Device Tree Source)
定义:设备树源文件,采用文本语法描述硬件。
扩展名:.dts
作用:描述硬件层次结构、属性和连接关系。
/ { cpu { ... }; memory { ... }; // 其他设备...
};
DTSI(Device Tree Source Include)
定义:设备树源文件的包含文件,定义可重用片段。
扩展名:.dtsi
作用:提高设备树的可重用性和可维护性。
类比:类似于C语言中的头文件。
DTB(Device Tree Blob)
定义:设备树的二进制表示形式。
扩展名:.dtb
作用:被操作系统加载和解析,用于识别和管理硬件设备。
生成:由DTS或DTSI文件通过DTC编译而成。
DTC(Device Tree Compiler)
定义:设备树的编译器。
作用:将DTS和DTSI文件编译成DTB文件。
形式:命令行工具。
关系概述
编写:开发人员编写DTS和DTSI文件,描述硬件。
包含:DTSI文件可在多个DTS文件中包含和共享。
编译:使用DTC将DTS和DTSI文件编译成DTB文件。
加载:操作系统在启动时加载和解析DTB文件,管理硬件设备。
设备树文件存放路径:
ARM 体系结构:
ARM 体系结构下的设备树源文件通常存放在 arch/arm/boot/dts/目录中。
该目录是设备树源文件的根目录。如下图(图 55- 2)所示:
ARM64 体系结构:
ARM64 体系结构下的设备树源文件通常存放在 arch/arm64/boot/dts/目录及其子目录中。
该目录也是设备树源文件的根目录,并包含了针对不同 ARM64 平台和设备的子目录,如下图(图 55- 3)所示:
子目录结构:在 ARM64 的子目录中,同样按照硬件平台、设备类型或制造商组织和分类。
这些子目录的命名可能与特定芯片厂商(如 Qualcomm、NVIDIA、Samsung)有关,由 于 我 们 本 手 册 使 用 的 soc 是 瑞 芯 微 的 rk3568 , 所 以 匹 配 的 设 备 树 目 录 为arch/arm64/boot/dts/rockchip。
每个子目录中可能包含多个设备树文件,用于描述不同的硬件配置和设备类型,这里以 rockchip 目录内容所示:
52.3 设备树的编译
设备树编译
编译器:DTC(Device Tree Compiler)
源代码位置:Linux内核源码的scripts/dtc/目录
编译命令:dtc -I dts -O dtb -o output.dtb input.dts
设备树反编译
反编译器:DTC(同样使用)
反编译命令:dtc -I dtb -O dts -o output.dts input.dtb
创建测试文件 test.dts,内容仅为最基本的设备树框架
/dts-v1/;
/ {
};
使用编译命令
/path/to/dtc -I dts -O dtb -o test.dtb test.dts
生成test.dtb文件。
使用反编译命令
/path/to/dtc -I dtb -O dts -o 1.dts test.dtb
生成的1.dts文件内容与原test.dts相同。
第五十三章 设备树基本语法
53.1 设备树语法讲解 1
53.1.1 根节点
设备树根节点是整个设备树的起始点和顶层节点,用 /表示,并使用花括号{ }包含节点内容。
一个最简单的根节点示例如下:
/dts-v1/; // 可选的设备树版本信息
/ { // 根节点内容区域,可添加属性和配置
};
53.1.2 子节点
设备树中的子节点描述具体硬件或设备集合,格式如下:
/*节点标签 节点名称 节点地址*/
[label:] node-name@[unit-address] { [properties]; //属性定义[child nodes]; //子节点
};
节点标签(Label)(可选):
用于引用节点,格式为 [label:]。
节点名称(Node Name):
唯一标识节点,格式为 node-name。
单元地址(Unit Address)(可选):
区分设备实例,格式为 @[unit-address],可以是整数、十六进制值或字符串。
属性定义(Properties Definitions):
描述设备配置和特性,格式为键值对 [properties]。
子节点(Child Nodes):
进一步描述子组件或配置,可以嵌套更多子节点和属性。
53.1.3 reg 属性
reg 属性在设备树中用于指定设备的寄存器地址和大小,
reg 属性有两种格式:单个值格式和列表值格式。
当我们谈论寄存器大小的时候,谈论的是寄存器块的大小,以字节为单位。
单个值格式:
用于描述单个寄存器。
格式:reg = <address size>;
address 是起始寄存器地址,可以是整数或十六进制值。
size 是寄存器的大小(字节数)。
/*节点名称*/
my_device { compatible = "vendor,device"; //兼容性字符串,标识设备供应商、设备类型reg = <0x1000 0x4>; // 从地址 0x1000 开始的 4 字节寄存器
};
列表值格式:
用于描述多个寄存器区域。
格式:reg = <address1 size1 address2 size2 ...>;
每个 addressX 和 sizeX 分别表示一个寄存器区域的起始地址和大小。
/*节点名称*/
my_device { compatible = "vendor,device"; //兼容性字符串,设备供应商、设备类型reg = <0x1000 0x8 0x2000 0x4>; // 两个寄存器区域:0x1000-0x1007 和 0x2000-0x2003
};
53.1.4 address-cells 和 size-cells 属性
#address-cells 属性在设备树中用于指定多少个单元来表示地址,
#size-cells 属性在设备树中用于指定多少个单元来表示大小。
1个单元表示32位。
父节点的 address-cell属性和 size-cell属性表示在描述子节点的寄存器地址和大小时使用多少个单元。
#address-cells:
位于设备树根节点。指定地址单元的位数。告诉解析软件用多少位表示一个地址单元。
默认值为 2,表示使用两个单元表示地址。
#size-cells:
位于设备树根节点。指定大小单元的位数。告诉解析软件用多少位表示一个大小单元。
默认值为 1,表示使用一个单元表示大小。
/*节点名称*/
node1 { #address-cells = <1>; //地址单元 = 1,地址用一个单元表示#size-cells = <1>; //大小单元 = 1,大小用一个单元表示/*子节点名称*/node1-child { reg = <0x02200000 0x4000>; /*寄存器起始地址,寄存器大小*/};
};
/*节点名称*/
node1 { #address-cells = <2>; //2个单元表示地址 #size-cells = <0>; //不表示大小/*子节点*/node1-child { reg = <0x0000 0x0001>;//寄存器起始地址,寄存器大小(无大小) };
};
在设备树的 reg 属性中,地址和大小通常是成对出现的。
然而,在某些情况下,可能只需要指定地址而不需要指定大小,或者大小可能由其他机制隐含地确定。
当你看到 reg = <0x0000 0x0001> 并且 #size-cells = <0> 时,这实际上意味着在这个上下文中不显式指定大小。
53.1.5 model 属性
在设备树中,model 属性用于提供设备的型号或名称,作为设备的标识信息。
model 属性是可选的,但常被使用。该属性值为字符串,由设备厂商定义。
/*节点名称*/
my_device { compatible = "vendor,device"; //兼容性字符串,设备厂商,设备类型model = "My Device XYZ"; //设备名称
};
53.1.6 status 属性
在设备树中,status 属性指示设备或节点的状态。
其值包括:
"okay"(可用)、"disabled"(禁用)、"reserved"(保留)、"fail"(不可用)。
/*节点名称*/
my_device { compatible = "vendor,device"; //设备厂商、设备类型status = "okay"; //设备状态
};
53.1.7 compatible 属性
在设备树中,compatible 属性用于匹配设备与驱动程序。
它是一个关键属性,值可以是单个字符串或字符串列表。
/*节点名称*/
my_device { compatible = "vendor,device"; //兼容性字符串// 其他属性
};/*节点名称*/
my_device { compatible = ["vendor,device1", "vendor,device2"]; //兼容性字符串列表// 其他属性
};
该设备节点与 vendor 制造的 device1 型号设备兼容。
同时,该设备节点也与 vendor 制造的 device2 型号设备兼容。
53.2 设备树语法讲解 2
53.2.1 aliases 节点
aliases 节点在设备树根部定义设备别名,位于 /aliases 路径。
它包含属性,每个属性为设备提供别名,值指向设备树中的节点路径。
aliases { mmc0 = &sdmmc0; mmc1 = &sdmmc1; mmc2 = &sdhci; serial0 = "/simple@fe000000/serial@11c500";
};/*
mmc0、mmc1 和 mmc2 别名 分别指向 sdmmc0、sdmmc1 和 sdhci 节点。
serial0 别名 指向路径 /simple@fe000000/serial@11c500。
*/
别名简化设备引用,提高设备树可读性。
& 符号用于引用节点,别名只在设备树内部有效。
53.2.2 chosen 节点
chosen节点是设备树根部的特殊节点,路径为/chosen,存储系统引导和配置信息。
/*chosen节点*/
chosen {bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";//boot命令行参数
};
chosen节点通常包含以下属性:
bootargs 属性:存储引导内核时的命令行参数,如内核和设备树参数。
stdout-path 属性:指定标准输出设备路径。
firmware-name 属性:指定系统固件名称。
linux,initrd-start 和 linux,initrd-end:指定initrd的起始地址和initrd的结束地址。
其他自定义属性:存储特定于系统引导和配置的信息。
通过chosen节点,系统引导信息可传递给操作系统或引导加载程序,实现灵活可配置的系统引导流程。
53.2.3 device_type 节点
在设备树中,device_type属性用于描述设备类型,通常作为设备节点的一个属性存在。
它是一个字符串,帮助操作系统等软件识别设备。
常见的设备类型包括
cpu、memory、display、serial、ethernet、usb、i2c、spi、gpio和pwm等。
这些类型可自定义和扩展。根据设备类型,系统可加载适当驱动、配置资源和建立连接。
/*节点名称*/
/device { compatible = "example,device"; //兼容性字符串,设备厂商、设备类型device_type = "serial"; // 指定设备类型为串行通信设备
};
53.2.4 自定义属性
设备树中的自定义属性用于满足特定需求,提供额外信息、配置参数或元数据。
添加自定义属性时,需遵循命名约定,避免冲突。
例如,可自定义 pinnum属性指定管脚标号:
/*节点名称*/
my_device { compatible = "my_device"; //兼容性字符串pinnum = <0 1 2 3 4>; // 自定义属性,整数数组,表示管脚标号
};
这样,pinnum属性让操作系统等软件能识别并使用特定管脚标号。
第五十四章 实例分析:中断
54.1 中断相关属性
54.1.1 RK ft5x06 设备树节点
在iTOP-RK3568开发板的设备树中,gpio0 节点和 ft5x06触摸芯片节点定义了与中断相关的属性。
gpio0节点(在rk3568.dtsi中定义):
/*gpio0节点,节点地址*/
gpio0: gpio@fdd60000 { compatible = "rockchip,gpio-bank"; //兼容性字符串interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; // 中断号及类型 interrupt-controller; // 声明为中断控制器 #interrupt-cells = <2>; // 中断描述符的单元数
};
ft5x06触摸芯片节点(在topeet_rk3568_lcds.dtsi中定义):
/*节点名,节点地址*/
ft5x06: ft5x06@38 { compatible = "edt,edt-ft5306"; //兼容性字符串touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>; // 触摸中断GPIO及类型 interrupt-parent = <&gpio0>; // 指定中断父节点 interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>; // 中断号及类型 // 其他属性...
};
关键中断属性介绍:
interrupts 属性:定义设备使用的中断号及中断类型。
interrupt-controller 属性:声明节点为中断控制器。
#interrupt-cells 属性:指定中断描述符所需的单元数。
interrupt-parent 属性:指定设备中断的父中断控制器。
54.1.2 interrupts 属性
interrupts 属性在设备树中用于指定设备的中断信息,包括中断控制器类型、中断号和中断触发类型。
interrupts属性描述:
中断控制器类型:指定中断由哪种控制器管理,如GIC(通用中断控制器)SPI中断。
中断号:设备的唯一中断标识符,用于区分不同的中断源。
中断触发类型:中断信号的触发条件,如边沿触发(上升沿、下降沿、双边沿)或电平触发(高电平、低电平)。
在gpio0节点中:
gpio0: gpio@fdd60000 { ... interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; // GIC SPI中断,中断号33,高电平触发 ...
};
这里interrupts属性包含三个参数:
中断控制器类型(GIC_SPI)、
中断号(33)、
中断触发类型(IRQ_TYPE_LEVEL_HIGH)。
在ft5x06节点中:
ft5x06: ft5x06@38 { ... interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>; // 使用RK_PB5引脚的中断,低电平触发 interrupt-parent = <&gpio0>; // 指定中断父节点 ...
};
这里interrupts属性只有两个参数:
中断号(通过RK_PB5引用)和中断触发类型(IRQ_TYPE_LEVEL_LOW)。
这是因为 ft5x06节点的中断是通过GPIO引脚触发的,其 interrupt-parent属性已经指定了中断控制器(即gpio0),所以不需要再次指定中断控制器类型。
中断触发类型宏定义
IRQ_TYPE_*宏定义在内核源码中,用于表示不同的中断触发类型:
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1
#define IRQ_TYPE_EDGE_FALLING 2
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH 4
#define IRQ_TYPE_LEVEL_LOW 8
54.1.3 interrupt-controller 属性
interrupt-controller 属性在设备树中用于标识一个节点所代表的设备为中断控制器。
中断控制器是负责管理和分发中断信号的硬件或软件模块。
interrupt-controller属性描述:
用于标识节点为中断控制器。
该属性没有具体的值,只需在节点属性中出现即可。
/*节点标签 节点名 节点地址*/
gpio0: gpio@fdd60000 { compatible = "rockchip,gpio-bank"; //兼容性字符串// 其他属性... interrupt-controller; // 标识gpio0为中断控制器 // 中断控制器相关的其他属性...
};
在这个例子中,gpio0节点通过包含 interrupt-controller属性来表明它是一个中断控制器。
interrupt-controller属性允许操作系统和其他设备知道 gpio0能够接收和处理中断信号,
并根据需要配置和使用它。
54.1.4 interrupt-parent 属性
interrupt-parent 属性在设备树中用于建立中断信号源与中断控制器之间的关联。
interrupt-parent属性描述:
指定中断信号源所属的中断控制器。
属性值是一个引用,指向中断控制器节点的路径或标签。
/*中断节点,注意这是一个中断控制器*/
gpio0: gpio@fdd60000 { compatible = "rockchip,gpio-bank"; interrupt-controller; // 标识gpio0为中断控制器 // 其他属性...
}; /*中断节点,注意这是一个中断信号源*/
ft5x06: ft5x06@38 { compatible = "focaltech,ft5x06"; // 其他属性... interrupt-parent = <&gpio0>; // 指定ft5x06的中断信号由gpio0中断控制器处理 interrupts = </* 中断号、中断触发类型 */>; // 与gpio0中断控制器关联的中断信息
};
在这个例子中:
gpio0节点是一个中断控制器,通过包含interrupt-controller属性来表明。
ft5x06节点是一个中断信号源,通过interrupt-parent属性指定其中断信号由gpio0中断控制器处理。
interrupts属性将包含与gpio0中断控制器关联的中断号和其他相关信息。
如果中断控制器形成多级结构,interrupt-parent属性同样可以用于指定层次结构中的上级中断控制器。这样,操作系统就可以正确地处理和分发来自不同中断信号源的中断请求。
54.1.5 #interrupt-cells 属性
#interrupt-cells属性用于描述中断控制器中每个中断信号源所需的中断编号单元数量。
#interrupt-cells属性描述:
指定中断编号单元的数量。
值为整数,通常为正整数,如1、2或3,取决于中断控制器和设备要求。
54.2 中断实例编写
在上一个小节中对设备树中断要用到的属性进行了讲解,而在本小节将会编写一个在RK3568 上的 ft5x06 触摸中断设备树。
然后来查看内核源码目录下的“drivers/input/touchscreen/edt-ft5x06.c”文件,这是 ft5x06的驱动文件,找到 compatible 匹配值相关的部分,如下(图 57-3)所示:
这里的 compatible 匹配值都可以选择,作者选择的是 edt,edt-ft5206,选择其他 compatible也是可以的。
在内核源码目录下的“include/dt-bindings/pinctrl/rockchip.h”头文件中,定义了 RK 引脚名和 gpio 编号的宏定义。
可以看到 RK 已经将 GPIO 组和引脚编号写成了宏定义的形式,通过宏定义可以减少在编写设备树的过程中换算的时间,并且帮助大家进行理解。
至此,关于编写 ft5x06 设备树的前置内容就查找完成了,接下来进行设备树的编写。
/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 = <RK_PA5 IRQ_TYPE_EDGE_RISING>;//中断信号的配置,中断号、触发方式};
};
第 1 行: 设备树文件的头部,指定了使用的设备树语法版本。
第 3 行:用于定义 Rockchip 平台的引脚控制器相关的绑定。
第 4 行:用于定义中断控制器相关的绑定。
第 5 行:表示设备树的根节点开始。
第 6 行:指定了设备树的模型名称,描述为 "This is my devicetree!"。
第 9 行:指定了设备节点的兼容性字符串,表示该设备与 "edt,edt-ft5206" 兼容。
第 10 行:指定了中断的父节点,即中断控制器所在的节点。这里使用了一个引用(&gpio0)
来表示父节点。
第 11 行:指定了中断信号的配置。RK_PB5 表示中断信号的引脚编号,IRQ_TYPE_EDGE_RISING 表示中断类型为上升沿触发。
54.3 其他 SOC 设备树对比
无论使用的是瑞芯微 SOC 还是恩智浦、三星的 SOC,在设备树关于中断相关的描述都离不开四个属性:
中断配置interrupts、
中断控制器标识interrupts_controller、
中断控制器interrupts_parents、
中断编号单元数量interrupts_cells。
第五十五章 实例分析:时钟
时钟在硬件设备和系统中至关重要,用于同步和定时操作。
时钟分为时钟生产者和时钟消费者。
时钟生产者:
定义:生成和提供时钟信号的模块,如时钟控制器、PLL等。
设备树节点:以时钟节点形式表示。
关键属性:
#clock-cells:指定时钟编号的位数。0表示单个时钟,1表示多个。
#clock-frequency:指定时钟频率,单位为Hz。
clock-output-name:指定时钟输出信号的描述性名称。
/*单个时钟*/
osc24m: osc24m {compatible = "clock"; //兼容性字符串clock-frequency = <24000000>; //时钟频率,hzclock-output-names = "osc24m"; //时钟的名称#clock-cells = <0>; //时钟编号的位数
};/*多个时钟*/
clock: clock {#clock-cells = <1>;clock-output-names = "clock1", "clock2";
};
时钟消费者:
定义:依赖时钟信号的硬件设备或模块。
设备树节点:使用属性引用时钟生产者的时钟源。
关键属性:
assigned-clocks:指定使用的时钟源。
assigned-clock-rates:指定使用的时钟源频率。
clock-indices:指定时钟消费者使用的时钟源的索引值。
assigned-clock-parents:指定时钟源的父时钟源。
clocks/clock-names:也可用于指明时钟消费者节点所需的时钟源及其名称。
/*时钟消费者节点*/
cru: clock-controller@fdd20000 {#clock-cells = <1>;//时钟编号的个数,2个//明确要用的时钟源assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKVDEC_PRE>;//明确要用的时钟源频率assigned-clock-rates = <32768>, <300000000>; //指定时钟消费者节点所使用的时钟源的索引值//clock-indices = <0>, <1>, <2>;//这里明确指定了时钟源和时钟频率,因此使用索引值是多余的,};
/*时钟消费者节点*/
clock: clock {assigned-clocks = <&clkcon 0>, <&pll 2>; //指明引用的时钟源assigned-clock-parents = <&pll 2>; //指明时钟源的父时钟源assigned-clock-rates = <115200>, <9600>; //指明引用时钟源的时钟频率
};
上述设备树表示了一个名为 clock 的时钟消费者节点,具有以下属性:
assigned-clocks 属性指定了该节点使用的时钟源,引用了两个时钟源节点:clkcon 0 和 pll 2。
assigned-clock-parents 属性指定了这些时钟源的父时钟源,引用了 pll 2 时钟源节点。
assigned-clock-rates 属性指定了每个时钟源的时钟频率,分别是 115200 和 9600。
第五十六章 实例分析:CPU
56.1 cpus 节点
设备树的 cpus节点是描述系统中处理器信息的关键部分,它作为处理器拓扑结构的顶层,包含了所有处理器相关的子节点和属性。
cpus节点
作用:描述系统中的处理器拓扑和属性。
结构:容器节点,包含多个cpu@X子节点,其中X是处理器索引。
关键属性:
#address-cells:指定地址单元数量。
#size-cells: 指定大小单元数量。
cpu@X子节点
作用:描述单个处理器的属性。
命名:cpu@X,X为处理器索引。
关键属性:
device_type:指示设备类型为"cpu"。
compatible:指定处理器的兼容性信息。
clock-frequency(可选):指定处理器的时钟频率。
cache-size(可选):指定处理器的缓存大小。
/*单核CPU示例*/
cpus { #address-cells = <1>; //地址单元数#size-cells = <0>; //大小单元数cpu0: cpu@0 { device_type = "cpu"; //设备类型compatible = "arm,cortex-a7"; //兼容性字符串// 可添加其他属性,如clock-frequency, cache-size等 };
};
/*多核CPU示例*/
cpus { //CPUS节点#address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { //CPU@X子节点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"; // 可添加其他属性 };
};
拓扑关系节点(可选)
为了提供更详细的处理器拓扑信息,可以在cpus节点下添加以下节点:
cpu-map:描述处理器的映射关系。
socket:描述多处理器系统中的物理插槽。
cluster:描述处理器集群。
core:描述处理器核心。
thread:描述处理器线程。
56.2 cpu-map、socket、cluster 节点
cpu-map节点在设备树中扮演着描述大小核架构处理器映射关系的角色。
它位于cpus节点之下,并可以包含 cluster和 socket子节点来定义处理器的拓扑结构。
cpu-map节点概述
父节点:cpus
子节点:cluster(和可选的socket)
作用:描述处理器核心和集群的映射关系。
cluster节点
子节点:表示集群内的核心,通常通过引用具体的cpu节点来定义。
属性:cpu-map-mask属性,指定集群使用的核心。
cpus { #address-cells = <2>; #size-cells = <0>; cpu-map { socket{ //socket可省略cluster0 { core0 { cpu = <&cpu_l0>; }; core1 { cpu = <&cpu_l1>; }; core2 { cpu = <&cpu_l2>; }; core3 { cpu = <&cpu_l3>; }; }; }cluster1 { core0 { cpu = <&cpu_b0>; }; core1 { cpu = <&cpu_b1>; }; }; }; cpu_l0: cpu@0 { device_type = "cpu"; compatible = "arm,cortex-a53", "arm,armv8"; }; cpu_l1: cpu@1 { ... }; // 省略了与cpu_l0相同的属性 cpu_l2: cpu@2 { ... }; // 省略了与cpu_l0相同的属性 cpu_l3: cpu@3 { ... }; // 省略了与cpu_l0相同的属性 cpu_b0: cpu@100 { device_type = "cpu"; compatible = "arm,cortex-a72", "arm,armv8"; }; cpu_b1: cpu@101 { ... }; // 省略了与cpu_b0相同的属性
};
56.3 core、thread 节点
Core 和 Thread节点在设备树中用于描述处理器的核心和线程配置。
Core 和 Thread 节点概述
Core节点:描述处理器核心(CPU),每个核心能独立执行指令和任务。
Thread节点:描述在核心上执行的基本执行单元,即线程。每个核心可以支持多个线程。
cpus { #address-cells = <2>; #size-cells = <0>; // 通常需要,但在此示例中未直接使用 cpu-map { socket0 { cluster0 { core0 { thread0 { cpu = <&CPU0>; }; thread1 { cpu = <&CPU1>; }; }; core1 { thread0 { cpu = <&CPU2>; }; thread1 { cpu = <&CPU3>; }; }; }; cluster1 { core0 { thread0 { cpu = <&CPU4>; }; thread1 { cpu = <&CPU5>; }; }; core1 { thread0 { cpu = <&CPU6>; }; thread1 { cpu = <&CPU7>; }; }; }; }; socket1 { // 省略了与socket0相同的内部结构,但核心和线程引用不同 // ... (与socket0结构相同,但cpu引用为CPU8至CPU15) // 为简洁起见,这里不重复展开 }; }; // CPU节点定义(示例中未完全展开,仅展示几个作为参考) CPU0: cpu@0 { ... }; // 实际定义包括device_type和compatible等属性 CPU1: cpu@1 { ... }; // 省略了与CPU0相同的属性,但地址不同 // ... (继续至CPU15)
};
cpus节点是处理器描述的根节点。
#address-cells和#size-cells指定了设备树中地址和大小的编码方式(在此示例中,#size-cells未直接使用,但通常是必需的)。
cpu-map节点定义了CPU的映射关系。
socket节点表示物理插槽。
cluster节点表示核心集群。
core节点表示处理器核心。
thread节点表示在核心上执行的线程,并通过cpu属性引用具体的CPU节点。
第五十七章 实例分析:GPIO
57.1 GPIO 相关属性
57.1.1 RK ft5x06 设备树节点
在iTOP-RK3568开发板SDK源码中,gpio0节点和ft5x06触摸芯片节点分别定义在不同的设备树文件中:
gpio0节点定义在/arch/arm64/boot/dts/rockchip/rk3568.dtsi:
gpio0: gpio@fdd60000 { compatible = "rockchip,gpio-bank"; reg = <0x0 0xfdd60000 0x0 0x100>; interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>;
};
0x0:这通常表示寄存器块在物理地址空间中的起始偏移量(offset)。
在这个例子中,它是 0x0,意味着从基地址开始。
0xfdd60000:这是寄存器块的基地址(base address)。
它是设备在物理内存映射中的起始地址。
0x0:这个值的具体含义取决于硬件和设备树绑定的实现。
在某些情况下,它可能表示额外的偏移量或用于特定目的的标志。然而,在这个例子中,由于它是 0x0,并且紧跟在基地址之后,它可能不表示一个有意义的偏移量,而是保留或未使用的。
0x100:这通常表示寄存器块的大小(size)
ft5x06触摸芯片节点定义在/arch/arm64/boot/dts/rockchip/topeet_rk3568_lcds.dtsi:
ft5x06: ft5x06@38 { status = "disabled"; compatible = "edt,edt-ft5306"; reg = <0x38>; touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>; interrupt-parent = <&gpio0>; interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>; reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
};
接下来介绍设备树中GPIO常见的四个属性:
interrupts:指定GPIO中断的相关信息,如中断号和中断类型。
interrupt-controller:表明该节点是一个中断控制器。
#interrupt-cells:每个中断信号源所需的中断编号单元数量。
interrupt-parent:指定该节点中断的父中断控制器。
57.1.2 gpio-controller属性
gpio-controller 属性用于标记一个设备节点为 GPIO 控制器,GPIO控制器管理和控制 GPIO 引脚。
gpio-controller;
57.1.3 #gpio-cells
#gpio-cells 属性定义了 GPIO 引脚描述符的编码格式,即描述一个 GPIO 引脚所需的整数单元数。
#gpio-cells 属性指定了 GPIO 引脚描述符的构成,其值通常为 2,表示每个描述符包含两个整数。
在设备树中,一个 GPIO 引脚描述符可能如下所示(以 reset-gpios 为例):
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>; //控制器 编号 类型(触发方式)
&gpio0 是一个对 GPIO 控制器的引用,不属于 #gpio-cells 指定的两个整数单元。
RK_PB6 和 GPIO_ACTIVE_LOW 才是构成 GPIO 引脚描述符的两个整数。
由于 #gpio-cells 被设置为 2,系统知道每个 GPIO 引脚描述符应该包含两个这样的整数,从而能够正确解析和配置 GPIO 引脚。
57.1.4 gpio-ranges
gpio-ranges 属性在设备树中用于简化 GPIO 引脚的编码和访问。
它通过映射 GPIO 控制器的本地编号到实际的外部引脚编号来解决编号不一致的问题。
gpio-ranges 属性是一个列表,描述 GPIO 控制器本地编号与外部引脚编号之间的映射关系。
每个列表项包含三个整数值:
外部引脚编号的起始值。
GPIO 控制器内部本地编号的起始值。
映射的引脚数量(范围大小)。
gpio-ranges = <&pinctrl 0 0 32>;/*
&pinctrl 是对名为 pinctrl 的 GPIO 控制器节点的引用。
第一个 0 表示外部引脚编号从 0 开始。
第二个 0 表示 GPIO 控制器内部本地编号也从 0 开始。
32 表示映射了 32 个引脚。
*/
这意味着 GPIO 控制器的本地编号 0 到 31 直接映射到外部引脚编号 0 到 31。
57.1.5 gpio 引脚描述属性
在设备树节点 ft5x06@38 中,
reset-gpios 属性描述了 GPIO 引脚的信息。
由于该 GPIO 控制器节点(gpio0)的 #gpio-cells 属性被设置为 2,因此 reset-gpios 属性也包含两个值:
ft5x06: ft5x06@38 { reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>; // GPIO控制器 GPIO编号 GPIO类型// 注意:实际上 &gpio0 是对 GPIO 控制器的引用,不属于 #gpio-cells 指定的两个值
};
57.1.6 其他属性
ngpios属性:
该属性指定了GPIO控制器所支持的GPIO引脚总数。
ngpios = <18>;
gpio-reserved-ranges属性:
此属性定义了被保留的GPIO引脚范围。
每个范围由两个整数表示,分别代表起始引脚编号和保留的引脚数量。
gpio-reserved-ranges = <0 4>, <12 2>;
在这个例子中,有两个保留范围:从第0个引脚开始的4个引脚,以及从第12个引脚开始的2个引脚,这些引脚被其他设备或功能保留,不可用。
gpio-line-names属性:
该属性为GPIO引脚提供了名称,每个名称对应一个GPIO引脚,用于标识和识别每个引脚的作用或连接的设备。
gpio-line-names = "MMC-CD", "MMC-WP", "voD eth", "RST eth", "LED R", "LED G", "LED B", "col A", "col B", "col C", "col D", "NMI button", "Row A", "Row B", "Row C", "Row D", "poweroff", "reset";
57.2 中断实例编写
本小节将会编写一个在RK3568 上 LED 灯的中断设备树。
首先确定 LED 的引脚编号。
从上面的原理图可以得到 LED 灯的引脚网络标号为 Working_LEDEN_H_GPIO0_B7,对应的引脚为 GPIO0_B7。
然后来查看内核源码目录下的“drivers/drivers/leds/leds-gpio.c”文件,这是 led 的驱动文件,然后找到 compatible 匹配值相关的部分,
最后在内核源码目录下的“include/dt-bindings/pinctrl/rockchip.h”头文件中,定义了 RK 引脚名和 gpio 编号的宏定义,
在源码目录下的“include/dt-bindings/gpio/gpio.h”文件中定义了引脚极性设置宏定义,
其中 GPIO_ACTIVE_HIGH 表示将该引脚设置为高电平,GPIO_ACTIVE_LOW 表示将该引脚设置为低电平。
至此,我们关于编写 LED 设备树的前置内容就查找完成了,接下来进行设备树的编写。
/dts-v1/; //设备树版本#include "dt-bindings/pinctrl/rockchip.h"
#include "dt-bindings/gpio/gpio.h" /{model = "This is my devicetree!"; //设备树模型名称led@1 { //节点名@节点地址/实例编号compatible = "gpio-leds"; //兼容性字符串gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH> //LED设备所使用的GPIO引脚//GPIO控制器 GPIO编号 GPIO类型(活动电平)};
};
第五十八章 实例分析:pinctrl
58.1 pinmux 介绍
引脚复用(Pinmux)是指配置和管理系统中引脚功能的过程,允许单个引脚在不同功能(如GPIO、UART、SPI、I2C等)之间切换。
如何知晓引脚复用功能:
核心板原理图:通常标注了每个引脚的复用功能。例如,一个引脚可能可以复用为LCDC_D16、VOP_BT1120_D7、GMAC1_RXD0_M0等多个功能。
BGA引脚标号:在BGA封装中,引脚标号是唯一标识每个引脚的标识符,由芯片制造商定义,并在规格文档或数据手册中提供。例如,AG1是一个具体的引脚标号。
引脚标号图:芯片制造商提供引脚标号图,如RK3568的引脚标号图,显示了纵向和横向的标号,以及每个引脚对应的复用功能。
58.2 使用 pinctrl 设置复用关系
pinctrl(引脚控制)是设备树中的一个组件,它负责描述和配置硬件引脚的功能及连接方式。
启动过程中,pinctrl向操作系统和设备驱动程序提供引脚配置信息,确保引脚被正确初始化。
在设备树中,pinctrl采用了客户端和服务端的概念来管理引脚控制关系:
服务端:定义了一组可配置的引脚及其属性(如功能、方向、电气特性等)。
客户端:引用服务端定义的引脚配置,并将其应用于特定的硬件设备。
// 定义pinctrl服务端,包含一组引脚配置
&pinctrl { status = "okay"; // 定义一个引脚组,包含引脚号、功能等 my_pins_group: my_pins_group@0 { pins = <PIN1 PIN2 PIN3>; // 引脚号列表 function = <GPIO_FUNC>; // 引脚功能 // 可添加其他属性,如方向、驱动能力等 };
}; // 客户端设备引用pinctrl服务端配置
&uart0 { status = "okay"; // 引用pinctrl服务端定义的引脚组 pinctrl-0 = <&my_pins_group>; // 其他配置...
};
在这个例子中,&pinctrl节点定义了服务端,其中my_pins_group是一个引脚组,包含了三个引脚(PIN1、PIN2、PIN3)及其功能(GPIO_FUNC)。
然后,&uart0节点作为客户端,引用了这个引脚组配置(pinctrl-0 = <&my_pins_group>;),以便在UART设备中使用这些引脚。
58.2.1 客户端
pinctrl客户端属性示例:
单状态配置:
node { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1>;
}
在这个例子中,pinctrl-names定义了一个名为"default"的状态,
pinctrl-0指定了该状态下使用的引脚配置,引用了pinctrl_hog_1节点。
多状态配置:
node { pinctrl-names = "default", "wake up"; pinctrl-0 = <&pinctrl_hog_1>; pinctrl-1 = <&pinctrl_hog_2>;
}
这里定义了"default"和"wake up"两个状态,每个状态分别引用了不同的引脚配置。
组合配置:
node { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>;
}
在"default"状态下,同时引用了pinctrl_hog_1和pinctrl_hog_2两个引脚配置,表示这两个配置将共同作用于设备的引脚。
服务端配置差异性:
对于pinctrl服务端(即引脚控制器的配置),不同厂家的编写格式在客户端部分是相同的,但服务端部分会有所区别。这主要因为不同芯片的引脚控制器设计、引脚功能定义以及可用的配置选项存在差异。
以rk3568为例,其pinctrl服务端配置将详细定义每个引脚的功能、方向、电气特性等,并可能包含多个引脚组的定义,这些引脚组可以被客户端引用以实现特定的硬件配置。
58.2.2 服务端
这里以瑞芯微的 RK3568 为例进行 pinctrl 服务端的讲解,瑞芯微原厂 BSP 工程师为了方便用户通过 pinctrl 设置管脚的复用关系,将包含所有复用关系的配置写在了内核目录下的“arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi”设备树中。
在 pinctrl 节点中就是每个节点的复用功能,然后我们以 uart4 的引脚复用为例进行讲解,uart4 的 pinctrl 服务端内容如下,
其中<3 RK_PB1 4 &pcfg_pull_up>和<3 RK_PB2 4 &pcfg_pull_up>分别表示将 GPIO3 的 PB1 引脚设置为功能 4,将 GPIO3 的 PB2 也设置为功能 4,且电器属性都会设置为上拉。
通过查找原理图可以得到两个引脚在 BGA 封装位置分别为 AG1 和 AF2,
然后在 rk3568 的数据手册中找到引脚复用表对应的位置,
可以看到功能 4 对应串口 4 的发送端和接收端,pinctrl 服务端的配置和数据手册中的引脚复用功能是一一对应。
那如果要将 RK_PB1 和 RK_PB2 设置为 GPIO 功能要如何设置呢,从上图可以看到 GPIO 对应功能 0,所以可以通过以下 pinctrl 内容将设置 RK_PB1 和 RK_PB2 设置为 GPIO 功能(事实上如果不对该管脚进行功能复用该引脚默认就会设置为 GPIO 功能):
<3 RK_PB1 0 &pcfg_pull_up>,
<3 RK_PB2 0 &pcfg_pull_up>;
代表将 GPIO3 的 PB2 也设置为功能 0,且电器属性设置为上拉。
最后来看客户端对 uart4 服务端的引用,具体内容在内核源码目录“arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10-linux.dts”:
在客户端中引用服务端的引脚描述符,设备树可以将客户端和服务端的引脚配置关联起来。
这样,在设备树被解析和处理时,操作系统和设备驱动程序可以根据客户端的需求,查找并应用适当的引脚配置。
58.3 pinctrl 实例编写
通过 设备树的 pinctrl组件将 led 的控制引脚复用为 GPIO 模式。
设备树结构:
根据 sdk 源码目录下的“device/rockchip/rk356x/BoardConfig-rk3568-evb1-ddr4-v10.mk”默认配置文件可以了解到编译的设备树为 rk3568-evb1-ddr4-v10-linux.dts
注释原有 LED 节点:
Led 在 rk3568-evb.dtsi 设备树中已经被正常配置了。
这里虽然没有配置pinctrl,当一个引脚没有被复用为任何功能时,默认就是 GPIO 功能,所以这里没有 pinctrl led 功能也可以正常使用。
首先注释掉 leds 节点,
保存退出之后,然后进入到 rk3568-evb1-ddr4-v10.dtsi 设备树中,找到 rk_485_ctl 节点,
这是根节点的最后一个节点,而且也是用来控制一个 GPIO 的,我们完全可以仿照该节点,在该节点下方编写 led 控制节点,
my_led: led { //节点名称为 led,标签名为 my_led。compatible = "topeet,led"; //兼容性字符串gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>; //gpio引脚配置.GPIO控制器、编号、电平状态pinctrl-names = "default"; //引脚控制组的名称pinctrl-0 = <&rk_led_gpio>; //与引脚控制组关联的实际控制器//引用了名为 rk_led_gpio 的引脚控制器配置。
};
然后继续找到在同一设备树文件的 485 pinctrl 服务端节点,
然后在该节点下方仿写 led 控制引脚 pinctrl 服务端节点,
rk_led{ //节点名称rk_led_gpio:rk-led-gpio { //节点标签:子节点名称//这个属性是 Rockchip 特定的,用于描述引脚配置。rockchip,pins = <0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;};
};
至此,led 的控制引脚就通过 pinctrl 被复用为了 GPIO 功能,保存退出,重新编译内核。
第五十九章 dtb 文件格式讲解
设备树 Blob (DTB) 是设备树数据的平面二进制编码,用于在软件间交换设备树数据,如在启动时固件传递给操作系统内核。
DTB 由头部和三个部分组成:内存保留块、结构块和字符串块,按顺序排列。
设备树包括根节点、子节点和属性。
/*此代码展示了设备树的基本结构,包括根节点、CPU节点、别名节点、GPIO节点等,以及它们的属性。*//dts-v1/;
/ { model = "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"; };
};
pxBinaryViewerSetup 二进制分析软件。打开 deb 文件并设置大端模式之后如下,
59.1 Header
devicetree 的头布局由以下 C 结构定义。所有的头字段都是 32 位整数,以大端格式存储。
struct fdt_header {uint32_t magic; // 设备树头部的魔数.固定值 0xd00dfeed(大端)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; // 启动 CPU 的物理 IDuint32_t size_dt_strings; // 设备树字符串表的大小uint32_t size_dt_struct; // 设备树结构体(节点数据)的大小
};
magic 字段为魔数,固定值 0xd00dfeed(大端)。
totalsize 字段包含设备树数据结构的总大小(以字节为单位)。此大小应包含结构的所有部分:头部、内存保留块、结构块和字符串块,以及块之间或最后一个块之后的任何空闲空间间隙。
off_dt_struct 字段包含结构块相对于文件开头的的偏移量。
off_dt_strings 字段包含字符串块相对于文件开头的偏移量。
off_mem_rsvmap 字段包含内存保留块相对于文件开头的偏移量。
version 字段包含设备树数据结构的版本。
last_comp_version 向后兼容的设备树数据结构的最低版本。
boot_cpuid_phys 与设备树 CPU 节点的 reg 属性对应。启动CPU的物理ID。
size_dt_strings 设备树字符串块部分的字节长度。
size_dt_struct 设备树结构块部分的字节长度。
然后来查看二进制文件,其中 4 个字节表示一个单位,前十个单位分别代表上述的十个字段如下图。
59.2 内存保留块
内存保留块是用于客户端程序的保护和保留物理内存区域的列表。
这些保留区域不应被用于一般的内存分配,而是用于保护重要数据结构,以防止客户端程序覆盖这些数据。内存保留块的目的是确保特定的内存区域在客户端程序运行时不被修改或使用。
由于在示例设备树中没有设置内存保留块,所以相应的区域都为 0。
保留区域列表: 内存保留块是一个由一组 64 位大端整数对构成的列表。每对整数对应一个保留内存区域,其中包含物理地址和区域的大小(以字节为单位)。这些保留区域彼此不重叠。
保留区域的用途: 客户端程序不应访问内存保留块中的保留区域,除非引导程序提供的其他信息明确指示可以访问。引导程序可以使用特定的方式来指示客户端程序可以访问保留内存的部分内容。引导程序可能会在文档、可选的扩展或特定于平台的文档中说明保留内存的特定用途。
保留区域格式: 内存保留块中的每个保留区域由一个 64 位大端整数对表示。
每对由以下 C 结构表示:
/*内存保留区格式*/
struct fdt_reserve_entry {uint64_t address;uint64_t size;
};
其中的第一个整数表示保留区域的物理地址,第二个整数表示保留区域的大小(以字节为单位)。每个整数都以 64 位的形式表示,即使在 32 位架构上也是如此。
在 32 位 CPU 上,整数的高 32 位将被忽略。
59.3 结构块
结构块概述
结构块描述设备树的结构和内容,由令牌序列组成,令牌按线性树结构组织。
令牌类型
FDT_BEGIN_NODE (0x0001): 表示节点开始,后跟节点名称(空字符结尾)。
FDT_END_NODE (0x0002): 表示节点结束,无额外数据。
FDT_PROP (0x0003): 表示属性开始,后跟 struct { uint32_t len; uint32_t nameoff; } 和属性值。
FDT_NOP (0x0004): 可忽略的令牌,无额外数据。
FDT_END (0x0009): 表示结构块结束,无额外数据,位于结构块末尾。
树状结构
设备树以线性树表示,节点由 FDT_BEGIN_NODE 开始(节点开始),FDT_END_NODE 结束(节点结束)。属性和子节点在 FDT_END_NODE(节点结束) 之前表示,子节点嵌套在父节点内。
结构块结束
结构块以单个 FDT_END(结构块结束) 标记结束,位于末尾,之后字节应位于设备树 blob 标头中的size_dt_struct 字段(结构块字节长度)指定的偏移处。
最后对结构块开头的部分内容进行讲解,
通过使用结构块,设备树可以以一种层次化的方式组织和描述系统中的设备和资源。每个节点可以包含属性和子节点,从而实现更加灵活和可扩展的设备树表示。
59.4 字符串块
字符串块用于存储设备树中使用的所有属性名称。它由一系列以空字符结尾的字符串组成,这些字符串在字符串块中简单地连接在一起。
字符串块存储属性名称,字符串以空字符(\0)终止并连接形成连续字符序列。
每个字符串以空字符结尾,下一个字符串紧跟其后,形成连续字符序列。
属性名称通过偏移量引用字符串块中的字符串。偏移量为无符号整数,表示字符串位置。
字符串块无对齐约束,可出现在设备树 blob 的任何偏移处,位置灵活。
第六十章 dtb 展开成 device_node
60.1 dtb 展开流程
dtb 展开流程图如下,
1. 设备树源文件编写
根据设备树的基本语法和相关知识,编写符合规范的设备树源文件(DTS)。
2. 设备树编译
使用设备树编译器(dtc)将DTS文件编译成设备树二进制文件(DTB)。此过程包括语法和语义检查。
3. boot.img 镜像生成
对特定SOC(如瑞芯微),将内核镜像、DTB文件和其他资源文件打包成boot.img镜像。
4. U-Boot 加载
U-Boot引导加载程序将 boot.img中的内核和DTB文件加载到系统内存的指定地址。
5. 内核初始化
内核接管后,解析DTB文件,将其内容转换为内核可识别的数据结构。
6. 设备树展开
内核读取DTB文件,构建设备树数据结构(如设备节点、中断控制器等),用于管理和配置硬件资源。
最终设备树二进制文件dtb会被解析成 device_node,device_node 结构体定义在内核源码的“/include/linux/of.h”文件中。
struct device_node {const char *name; // 设备节点的名称const char *type; // 设备节点的类型phandle phandle; // 设备节点的句柄const char *full_name; // 设备节点的完整名称struct fwnode_handle fwnode; // 设备节点的固件节点句柄struct property *properties; // 设备节点的属性列表struct property *deadprops; // 已删除的属性列表struct device_node *parent; // 父设备节点指针struct device_node *child; // 子设备节点指针struct device_node *sibling; // 兄弟设备节点指针#if defined(CONFIG_OF_KOBJ)struct kobject kobj; // 内核对象(用于 sysfs)#endifunsigned long _flags; // 设备节点的标志位void *data; // 与设备节点相关的数据指针#if defined(CONFIG_SPARC)const char *path_component_name; // 设备节点的路径组件名称unsigned int unique_id; // 设备节点的唯一标识struct of_irq_controller *irq_trans; // 设备节点的中断控制器#endif
};
设备树结构体重要参数
name: 表示设备节点的唯一名称,用于在设备树中引用。
type: 提供设备节点的类型信息,反映其功能和类别。
properties: 指向属性列表指针,属性包含配置和参数信息,以键值对形式存在。
parent: 指向当前节点的父节点,用于在设备树中向上遍历。
child: 指向当前节点的第一个子节点,用于访问直接下级。
sibling: 指向当前节点的下一个兄弟节点,用于同级遍历。
通过 child 和 sibling 字段可以遍历所有子节点。
/*设备树property示例*/
struct property { char *name; // 属性名称 int length; // 属性值长度 void *value; // 属性值指针 struct property *next; // 下一个属性节点
};
60.2 dtb 解析过程源码分析
在Linux内核启动过程中,start_kernel()函数是内核的入口点,负责内核初始化和启动。
与设备树相关的初始化步骤主要通过 setup_arch() 函数进行。
start_kernel 中的 setup_arch
在 start_kernel() 函数中,关键的设备树初始化调用是:
setup_arch(&command_line);//负责kernel启动过程中架构相关(设备树)的初始化
这个函数位于 /arch/arm64/kernel/setup.c。
setup_arch 函数
setup_arch 函数中,与设备树相关的关键步骤包括:
1、映射设备树:使用 fixmap_remap_fdt() 将设备树从物理地址映射到内核虚拟地址空间。
2、验证和扫描设备树:调用 early_init_dt_scan() 来验证设备树的完整性和进行早期扫描。
3、设备树只读映射:完成早期修复后,将设备树映射为只读。
4、获取机器名称:通过 of_flat_dt_get_machine_name() 获取设备树的机器模型名称。
setup_machine_fdt 函数
设备树的加载通过 setup_machine_fdt(__fdt_pointer) 函数进行。
setup_machine_fdt(__fdt_pointer); // 加载机器的 FDT(平台设备树)
//加载指的是调用fixmap_remap_fdt()把dtb从物理地址映射到虚拟地址
early_init_dt_scan 函数
early_init_dt_scan() 函数验证设备树的完整性,然后扫描设备树节点。
/*验证设备树*/
bool status = early_init_dt_verify(params); //检查设备树头部,并计算CRC32校验值。
if (!status) return false;
/*扫描设备树节点*/
early_init_dt_scan_nodes();/*
从 /chosen 节点中检索信息(如命令行参数)。
初始化 {size,address}-cells 信息。
设置内存信息。
*/
在 early_init_dt_scan_nodes() 扫描设备树节点的过程中,使用 of_scan_flat_dt() 函数扫描设备树,并调用以下回调函数:
early_init_dt_scan_chosen():处理 /chosen 节点。
early_init_dt_scan_root(): 处理根节点,初始化 {size,address}-cells。
early_init_dt_scan_memory():处理内存信息。
unflatten_device_tree()函数
设备树的展开主要通过 unflatten_device_tree() 函数进行。
acpi_boot_table_init();// 解析 ACPI 表以进行可能的引导时配置 ACPI,高级配置和电源管理接口
if (acpi_disabled) // 趁ACPI表还不可用unflatten_device_tree(); // 展开设备树
bootmem_init(); // 引导内存的初始化
这些步骤共同完成了设备树在Linux内核启动过程中的初始化。
60.2.1 setup_machine_fdt(__fdt_pointer)
设备树加载流程:
Bootloader传递设备树地址:
Bootloader启动内核时,通过x0寄存器传递设备树二进制文件(DTB)在内存中的物理地址。
内核汇编代码(/arch/arm64/kernel/head.S)中,x0的值被复制到x21,然后存储到全局变量__fdt_pointer的地址中。
mov x21, x0 // x21=FDT
str_l x21, __fdt_pointer, x5 // Save FDT pointermov x21, x0
内核设置设备树:
setup_machine_fdt()函数(/arch/arm64/kernel/setup.c)使用__fdt_pointer作为参数,将设备树映射到内核虚拟地址空间,并进行初始化和验证。
设备树验证和扫描:
early_init_dt_scan()函数(drivers/of/fdt.c)验证设备树的完整性,扫描设备树节点。
检查设备树头部有效性,计算CRC32校验值,并保存设备树指针。
扫描/chosen节点以获取启动参数,初始化{size,address}-cells信息,并设置内存信息。
60.2.2 unflatten_device_tree
设备树解析函数 unflatten_device_tree() 主要负责将紧凑的设备树数据结构转换为树状结构,也就是负责设备树展开。
void __init unflatten_device_tree(void) { //遍历设备树,开辟设备树节点空间、初始化节点、管理树状关系__unflatten_device_tree(initial_boot_params,NULL, &of_root, early_init_dt_alloc_memory_arch, false); of_alias_scan(early_init_dt_alloc_memory_arch); //遍历设备树的别名unittest_unflatten_overlay_base(); //单元测试,验证展开后的设备树能否正常工作
}
第六十一章 device_node 转换成 platform_device
dtb 二进制文件展开成 device_node,这时候还并不能跟内核中的 platform_driver 进行对接,而为了让操作系统能够识别和管理设备,需要将设备节点转换为 platform_device。
平台节点->平台设备 -- 平台驱动
61.1 转换规则
规则一:
遍历根节点下包含 compatible属性的子节点,
为每个子节点创建一个对应的 platform_device。
规则二:
遍历compatible属性为"simple-bus"、"simple-mfd"或"isa"的节点及其子节点。
如果子节点包含compatible属性值,则创建一个对应的 platform_device。
规则三:
如果节点的compatible属性包含"arm"或"primecell",
则不将该节点转换为platform_device,而是识别为AMBA设备。高级微控制器总线架构
/*gpio节点包含兼容性属性compatibel,因此会被转换为platform_deivce*/
/ { gpio@22020101 { compatible = "led"; reg = <0x20220101 0x40>; status = "okay"; }; // 其他节点无compatible属性,不会被转换
};
/*node1节点包含compatible属性simple_bus,但其子节点无任意compatible属性,因此不被转换*/
/ { node1 { compatible = "simple-bus"; gpio@22020102 { reg = <0x20220102 0x40>; // 无compatible属性,不会被转换 }; }; // 其他节点分析略
};
/*cou1节点有simple-bus的兼容性属性,两个子节点也有兼容性属性arm,primecell,因此被识别为AMBA设备*/
/ { cpu1: cpu@1 { amba { compatible = "simple-bus"; dmac_peri: dma-controller@ff250000 { compatible = "arm,p1330", "arm,primecell"; // 其他属性略 }; dmac_bus: dma-controller@ff600000 { compatible = "arm,p1330", "arm,primecell"; // 其他属性略 }; }; }; // 其他节点分析略
};
61.2 转换流程源码分析
在Linux内核初始化过程中,
arch_initcall_sync()函数用于执行架构相关的初始化函数。
其中,of_platform_default_populate_init()函数负责解析设备树,并为每个设备节点创建对应的platform_device结构,然后注册到内核中。
of_platform_default_populate_init()函数会遍历设备树节点,对于特定的节点(如/firmware和具有特定compatible属性的节点),它会调用of_platform_populate()函数来填充平台设备。
of_platform_populate()函数使用默认的设备匹配表of_default_bus_match_table(包含simple-bus、simple-mfd、isa等兼容属性)来匹配设备驱动程序
核心函数of_platform_bus_create()会递归地遍历设备树节点,为每个节点创建platform_device。
在创建过程中它会检查节点的可用性、是否已填充、是否匹配给定的设备ID表等条件。
如果满足条件,它会调用of_platform_device_create_pdata()函数来分配并初始化platform_device结构。
of_platform_device_create_pdata()函数会检查设备节点的状态,分配platform_device结构,并设置其属性(如DMA掩码、总线类型、平台数据等)。
然后,它会调用of_device_add()函数将平台设备添加到设备模型中。
总的来说,就是
遍历节点、
通过设备匹配表匹配驱动、
检查节点可用性、
分配并初始化平台设备、
将平台设备添加到设备模型
第六十二章 设备树下 platform_device 和 platform_driver 匹配
device_node 到 platform_device 转换完成之后操作系统才能够识别和管理设备,从而与 platform_driver 进行匹配。
62.1 of_match_table
在Linux内核中,platform_device 和 platform_driver 通过名称或设备树进行匹配,以便加载驱动程序的 probe 初始化函数。
基本匹配机制:
platform_device 结构体中的 name 属性需要与 platform_driver 结构体中嵌套的 driver 结构体的 name 属性或 id_table 相同。
设备树匹配:
platform_driver 的 driver 结构体属性,有一个 of_match_table 属性,是一个of_device_id 结构体数组。
of_device_id 结构体用于定义匹配规则,包括 节点名称name、节点类型type、节点compatible 和 data 字段。
62.2 实验程序编写
rk3568-evb1-ddr4-v10-linux.dts 是讯为RK3568的顶层设备树,先进入该文件(当然也可以修改其他设备树),然后将根据需求编写的设备树节点添加到 rk3568-evb1-ddr4-v10-linux.dts 中,
/{topeet{#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus"; //兼容性字符串myLed{compatible = "my devicetree"; //兼容性字符串reg = <0xFDD60000 0x00000004>;//节点的寄存器信息,起始地址、大小};};
};
保存退出之后,重新编译内核源码,编译完成之后将生成的 boot.img 烧写到开发板。
进入到“/proc/device-tree”目录下,查看是否已经存在了 topeet 目录。
生成节点同名的目录。
编写完成的 platform_driver.c 代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_probe: Probing platform device\n");// 添加设备特定的操作// ... return 0;
}// 平台设备的移除函数
static int my_platform_remove(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_remove: Removing platform device\n");// 清理设备特定的操作// ... return 0;
}/*of_device_id数组,定义匹配规则*/
const struct of_device_id of_match_table_id[] = {{.compatible="my devicetree"},
};// 定义平台驱动结构体
static struct platform_driver my_platform_driver = {.probe = my_platform_probe, .remove = my_platform_remove, /*将 of_device_id定义的匹配规则,传递给 platform_driver的.driver属性*/.driver = {.name = "my_platform_device",.owner = THIS_MODULE, .of_match_table = of_match_table_id, //驱动的匹配规则},
};// 模块初始化函数
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("topeet");
之后,make生成 .ko模块驱动,加载模块后,注册平台驱动时,平台驱动就可以根据 设备节点的 compatible属性匹配对应的节点。
第六十三章 of 操作函数:获取设备树节点
of操作函数帮我我们在驱动程序中获取设备树节点的属性。
63.1 of 操作:获取设备树节点
Linux 内核提供 of 操作函数来从设备树获取属性,这些函数返回 device_node 结构体,描述设备树节点。
63.1.1 of_find_node_by_name 函数
of_find_node_by_name() 是 Linux 内核中查找设备树节点的函数。
#include <linux/of.h>/*根据节点名查找设备树中的节点*/
struct device_node *of_find_node_by_name(struct device_node *from,//起始节点,null表示根节点const char *name);//节点名
63.1.2 of_find_node_by_path 函数
of_find_node_by_path() 是 Linux 内核中用于通过节点路径查找设备树节点的函数。
#include <linux/of.h>
/*通过节点路径查找设备树节点*/
struct device_node *of_find_node_by_path(const char *path);//节点路径
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/printk.h> void example_find_node(void) { const char *path = "/topnode/myLed"; struct device_node *node = of_find_node_by_path(path); if (node) { printk(KERN_INFO "Found node: %s\n", path); // 可以在这里对找到的节点进行进一步操作 } else { printk(KERN_WARNING "Node not found: %s\n", path); }
}
66.1.3 of_get_parent 函数
of_get_parent() 函数用于在设备树中获取特定节点的父节点指针。
#include <linux/of.h>
/*获取特定节点的父节点*/
struct device_node *of_get_parent(const struct device_node *node);
63.1.4 of_get_next_child 函数
of_get_next_child() 函数在 Linux 内核中用于遍历设备树节点的子节点。
#include <linux/of.h>
/*根据当前节点和上一个子节点,返回下一个子节点的指针*/
struct device_node *of_get_next_child(const struct device_node *node, //当前节点struct device_node *prev);//上一个子节点,//为NULL则从当前节点第一个子节点开始
63.1.5 of_ find_ compatible_ node 函数
of_find_compatible_node() 在 Linux 内核用于根据兼容性字符串在设备树中查找节点。
#include <linux/of.h>
/*根据兼容性字符串查找子节点*/
struct device_node *of_find_compatible_node(struct device_node *from, //起始节点,NULL则从根节点开始const char *type, //设备类型字符串,null代表不匹配设备类型const char *compatible);//兼容性字符串
63.1.6 of_ find matching node_ and_ match 函数
of_find_matching_node_and_match() 函数在 Linux 内核中用于根据 of_device_id 匹配表在设备树中查找匹配的节点。
#include <linux/of.h>
/*根据 of_device_id匹配表查找节点*/
struct device_node *of_find_matching_node_and_match(struct device_node *from, //起始搜索节点,NULL代表从根节点开始const struct of_device_id *matches, //of_device_id匹配表的指针const struct of_device_id **match); //用于输出匹配到的 of_device_id条目的指针
#include <linux/of.h>
#include <linux/printk.h> // 定义 of_device_id 匹配表
static const struct of_device_id my_match_table[] = { { .compatible = "vendor,device" }, { /* sentinel,表示匹配表结束 */ }
}; void example_find_matching_node(void) { const struct of_device_id *match; struct device_node *np; // 从根节点开始查找匹配的节点 np = of_find_matching_node_and_match(NULL, my_match_table, &match); if (np) { // 找到匹配节点,进行处理 printk(KERN_INFO "Found matching node: %p, match: %p\n", (void *)np, (void *)match); // 使用完节点后,应该释放它 of_node_put(np); } else { // 未找到匹配节点 printk(KERN_WARNING "No matching node found\n"); }
} // 调用示例函数
example_find_matching_node();
第六十四章 of 操作函数实验:获取属性
64.1 of 操作:获取属性
64.1.1 of_find_property 函数
of_find_property() 函数在 Linux 内核设备树中用于查找指定节点下的属性。
#include <linux/of.h>
/*查找设备树指定节点下的属性*/
struct property *of_find_property(const struct device_node *np, //设备树节点const char *name, //属性名称int *lenp); //存储找到的属性值长度,不需要可NULL
#include <linux/of.h>
#include <linux/printk.h> void example_find_property(const struct device_node *np) { const char *prop_name = "my-property"; int len; struct property *prop; prop = of_find_property(np, prop_name, &len); if (prop) { // 找到属性,进行处理 printk(KERN_INFO "Found property '%s' with length %d\n", prop_name, len); // 可以进一步获取属性值,例如: // const void *value = prop->value; // 根据属性值的类型和长度进行处理... } else { // 未找到属性 printk(KERN_WARNING "Property '%s' not found in node\n", prop_name); }
} // 调用示例(假设已有一个有效的 device_node 指针 np)
example_find_property(np);
64.1.2 of_property_count_elems_of_size 函数
of_property_count_elems_of_size() 用于计算指定属性中元素的数量,基于元素的大小。
#include <linux/of.h>
/*计算设备树节点指定属性中元素的数量*/
int of_property_count_elems_of_size(const struct device_node *np, //设备树节点const char *propname, //属性名int elem_size);//单个元素大小
64.1.3 of_property_read_u32_index 函数
of_property_read_u32_index() 用于从指定节点的属性中读取特定索引位置的 32 位无符号整数值。
#include <linux/of.h> /*从指定节点的属性中读取特定位置的32位无符号整数值.成功返回0,失败返回错误码*/
int of_property_read_u32_index(const struct device_node *np, //设备节点const char *propname, //属性名u32 index, //索引位置u32 *out_value); //读取到的u32值
64.1.4 of_property_read_u64_index 函数
of_property_read_u64_index() 从特定节点的属性特定索引位置中读取64位无符号整数值。
#include <linux/of.h> /*从特定节点的特定属性的索引位置读取无符号64位值,成功返回0,失败返回错误码*/
static inline int of_property_read_u64_index(const struct device_node *np,//设备树节点const char *propname,//属性名u32 index, //索引位置u64 *out_value);//读取到的u64值
64.1.5 of_property_read_variable_u32_array 函数
这四个函数用于从设备树的指定属性中读取变长的数组数据。
它们分别支持读取 u8、u16、u32 和 u64 类型的数组。
int of_property_read_variable_u8_array(const struct device_node *np, //设备树节点const char *propname, //属性名u8 *out_values, //读取到的数组指针size_t SZ_min, //数组最小元素数量size_t SZ_max);//数组最大元素数量
int of_property_read_variable_u16_array(...);//一样的参数
int of_property_read_variable_u32_array(...);//一样的参数
int of_property_read_variable_u64_array(...);//一样的参数
64.1.6 of_property_read_string 函数
of_property_read_string()函数用于获取设备节点下指定名称的属性的字符串值。
/*获取设备节点指定属性的字符串值*/
static inline int of_property_read_string(const struct device_node *np, //设备节点const char *propname, //属性名const char **out_string); //存储字符串值
第六十五章 ranges 属性实验
65.1 platform_get_resource 获取设备树资源
由于设备树在系统启动的时候会根据兼容性字符串转化为 platform 设备,
因此可以在设备树源文件中,节点处添加ranges属性后,
使用platform_get_resource() 直接获取 platform_device 资源。
注意,platform_get_resource()是通过设备树节点的 ranges 属性,
得到转换成了 platform_device,平台设备的 resource资源的。
/*获取平台设备的资源属性*/
struct resource *platform_get_resource(struct platform_device *pdev, //平台设备unsigned int type, //资源类型unsigned int num);//资源索引号/*
pdev:指向你要查询的平台设备的指针。
type:你想要获取的资源类型,例如 IORESOURCE_MEM(内存资源)、IORESOURCE_IRQ(中断资源)等。
num:资源的索引号,因为同一个设备可能有多个相同类型的资源。
*/
/*驱动程序示例*/struct resource *myresources;// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_probe: Probing platform device\n");// 获取平台设备的资源myresources = platform_get_resource(pdev, IORESOURCE_MEM, 0);if (myresources == NULL) {// 如果获取资源失败,打印 value_compatible 的值printk("platform_get_resource is error\n");}printk("reg valus is %llx\n" , myresources->start);return 0;
}static int my_platform_remove(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_remove: Removing platform device\n");// 清理设备特定的操作// ... return 0;
}/*设备节点和驱动的匹配规则*/
const struct of_device_id of_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 = of_match_table_id, //绑定匹配规则},
};// 模块初始化函数
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);
65.2 ranges 属性
65.2.1 ranges 属性介绍
设备树是一种用于描述嵌入式系统中硬件组件及其连接关系的硬件描述语言。
在设备树中,ranges 属性用于描述子设备地址空间如何映射到父设备地址空间。
ranges 属性解释
格式:
ranges = <child-bus-address parent-bus-address length>;
<子设备起始地址 父设备起始地址 映射的大小>;
或 ranges;(空表示1:1映射)
组成部分:
child-bus-address:子设备空间的起始地址,由 #address-cells 属性决定字长。
parent-bus-address:父设备空间的起始地址,由父节点 #address-cells 决定字长。
length:映射的大小,由父节点的 #size-cells 属性决定字长。
/dts-v1/;
/ { compatible = "acme,coyotes-revenge"; #address-cells = <1>; #size-cells = <1>; external-bus { #address-cells = <2>; #size-cells = <1>; ranges = <0 0 0x10100000 0x10000 1 0 0x10160000 0x10000 2 0 0x30000000 0x30000000>; // 其他节点... }; // 其他节点...
};
由于 external-bus 节点的 #address-cells = <2>,因此子设备起始地址由两个值组成。
嵌入式系统中,不同的设备可能连接到相同的总线或总线控制器上,它们需要在物理地址空间中进行正确的映射,以便进行数据交换和通信。
例如,一个设备可能通过总线连接到主处理器或其他设备,而这些设备的物理地址范围可能不同。ranges 属性就是用来描述这种地址映射关系的。
65.2.2 设备分类
根据设备物理地址到虚拟地址的映射关系,设备分为内存映射型设备和非内存映射型设备。
内存映射型设备
内存映射型设备可直接通过内存地址访问,其寄存器、缓冲区等被映射到系统内存地址空间。CPU通过读写这些内存地址与设备通信。
CPU通过读写内存地址与设备交互。
非内存映射型设备
非内存映射型设备不能通过内存地址直接访问,通常使用I/O端口、专用总线或特定协议与CPU通信。
不能通过内存地址访问。使用特定接口和协议通信。需要特定驱动程序。
/*非内存映射型设备在设备树中的表示方法*/
/dts-v1/;
/ { compatible = "acme,coyotes-revenge"; #address-cells = <1>; #size-cells = <1>; external-bus { #address-cells = <2>; #size-cells = <1>; ranges = <0 0 0x10100000 0x10000 1 0 0x10160000 0x10000 2 0 0x30000000 0x30000000>; /*内存映射型设备*/ethernet@0,0 { compatible = "smc,smc91c111"; reg = <0 0 0x1000>; // 使用片选和寄存器大小 }; /*非内存映射型设备*/i2c@1,0 { compatible = "acme,a1234-i2c-bus"; #address-cells = <1>; #size-cells = <0>; reg = <1 0 0x1000>; // I2C 控制器配置 rtc@58 { compatible = "maxim,ds1338"; reg = <0x58>; // RTC 设备地址 }; }; };
};
非内存映射型设备通常不会有reg属性,因为它们不直接映射到内存地址空间中。
对于I2C从设备,可能会使用reg属性(但这里的reg属性指的是从设备在I2C总线上的地址,而不是内存地址)。
65.2.3 映射地址计算
在设备树中,ethernet@0节点被映射到外部总线(external-bus)上。
为了计算 ethernet@0的物理地址范围,我们需要考虑 external-bus节点的 ranges属性和ethernet@0节点的 reg属性。
查看ethernet@0的reg属性:
reg = <0 0 0x1000> 表示基地址为0(相对于external-bus),大小为0x1000字节。
查看external-bus的ranges属性:
第一个映射条目为“0 0 0x10100000 0x10000”,表示与ethernet@0(作为第一个子节点)相关联的地址范围为0x10100000到0x1010FFFF。
计算ethernet@0的物理地址范围:
起始地址 = 外部总线地址起始值 = 0x10100000
结束地址 = 起始地址 + reg属性中的大小 - 1 = 0x10100000 + 0xFFF = 0x10100FFF
因此,ethernet@0的物理地址范围为 0x10100000到 0x10100FFF。
子节点的reg中的地址是相对于父节点的偏移地址。
第六十六章 of 操作函数实验:获取中断资源
66.1 of 操作:获取中断资源
66.1.1 irq_of_parse_and_map 函数
irq_of_parse_and_map() 函数用于从设备树节点中解析中断号。
#include <linux/of_irq.h>/*从设备树节点解析和映射中断号.成功时返回映射后的中断号.失败返回错误码*/
unsigned int irq_of_parse_and_map(struct device_node *dev,//设备树节点int index);//要获取的中断号的索引
#include <linux/of_irq.h>
#include <linux/of.h> // 通常需要包含此头文件以获取设备树节点 // 假设 dev_ptr 是指向设备树节点的有效指针
struct device_node *dev_ptr = /* 获取设备树节点 */; // 获取第一个中断号(索引为0)
unsigned int irq = irq_of_parse_and_map(dev_ptr, 0); // 检查是否成功获取中断号
if (irq == NO_IRQ) { // 处理错误情况
} else { // 使用获取到的中断号
}
66.1.2 irq_get_trigger_type 函数
irqd_get_trigger_type() 函数用于获取中断的触发类型。
#include <linux/irq.h>
/*获取中断的触发类型.成功时返回中断的触发类型。*/
u32 irqd_get_trigger_type(struct irq_data *d);//指向中断数据结构体的指针
#include <linux/irq.h>
#include <linux/interrupt.h> // 可能需要这个头文件来获取 irqreturn_t 类型定义
#include <linux/printk.h> // 用于 pr_info 或其他打印函数 // 你的中断处理函数
irqreturn_t my_interrupt_handler(int irq, void *dev_id, struct irq_data *data)
{ // 获取中断触发类型 u32 trigger_type = irqd_get_trigger_type(data); // 打印触发类型(注意:这里的打印可能不会在所有中断上下文中都有效) // 在实际的内核代码中,你可能会使用更合适的日志记录机制 pr_info("Interrupt %d trigger type: 0x%x\n", irq, trigger_type); // 根据触发类型执行相应操作... // 返回 IRQ_HANDLED 表示中断已被处理 return IRQ_HANDLED;
} // 在其他地方(如驱动初始化代码中),你需要请求这个中断
int my_driver_init(void)
{ int result; int irq_number = /* 你要请求的中断号 */; // dev_id 可以是任何你想要传递给中断处理函数的指针,通常指向你的设备结构体 void *dev_id_ptr = /* 你的设备指针 */; // 请求中断,并注册你的中断处理函数 result = request_irq(irq_number, my_interrupt_handler, IRQF_SHARED | /* 其他标志 */, "my_driver", dev_id_ptr); if (result) { // 处理请求中断失败的情况 pr_err("Failed to request IRQ %d\n", irq_number); return -EBUSY; // 或其他错误代码 } // ... 其他初始化代码 ... return 0; // 表示成功
}
66.1.4 gpio_to_irq 函数
gpio_to_irq() 函数用于将 GPIO 编号转换为对应的中断号。
#include <linux/gpio.h>
/*将GPIO编号转换为中断号.成功时返回中断号,失败返回错误码*/
int gpio_to_irq(unsigned int gpio);
66.1.5 of_irq_get 函数
of_irq_get() 函数用于从设备节点的 "interrupts" 属性中获取对应的中断号。
#include <linux/of_irq.h>
/*从设备节点的interrupts属性获取中断号*/
int of_irq_get(struct device_node *dev, //设备节点int index); //要获取的中断号索引
66.1.6 platform_get_irq 函数
platform_get_irq()函数用于根据平台设备和索引号获取对应的中断号。
#include <linux/platform_device.h>
/*根据平台设备获取中断号*/
int platform_get_irq(struct platform_device *dev,//平台设备unsigned int num);//中断索引号
66.1.7 驱动程序编写
由于本章节要获取的是中断相关的资源,所以需要在设备树中添加有关中断的设备节点,
myirq {compatible = "my_devicetree_irq"; //节点的兼容性字符串interrupt-parent = <&gpio3>; //中断的中断控制器interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;//中断属性, GPIO引脚 中断类型
};
保存退出之后,重新编译内核源码,得到 boot.img 内核镜像之后烧写到开发板。
int num;
int irq;
struct irq_data *my_irq_data;
struct device_node *mydevice_node;
u32 trigger_type;
// 平台设备的初始化函数
static int my_platform_probe(struct platform_device *pdev)
{printk(KERN_INFO "my_platform_probe: Probing platform device\n");// 查找设备节点mydevice_node = of_find_node_by_name(NULL, "myirq");// 解析和映射中断irq = irq_of_parse_and_map(mydevice_node, 0);printk("irq is %d\n", irq);// 获取中断数据结构my_irq_data = irq_get_irq_data(irq);// 获取中断触发类型trigger_type = irqd_get_trigger_type(my_irq_data);printk("trigger type is 0x%x\n", trigger_type);// 将 GPIO 转换为中断号irq = gpio_to_irq(101);printk("irq is %d\n", irq);// 从设备节点获取中断号irq = of_irq_get(mydevice_node, 0);printk("irq is %d\n", irq);// 获取平台设备的中断号irq = platform_get_irq(pdev, 0);printk("irq is %d\n", irq);return 0;
}/*定义驱动规则*/
const struct of_device_id of_match_table_id[] = {{.compatible="my_devicetree_irq"}, };// 定义平台驱动结构体
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 = of_match_table_id, },
};// 模块初始化函数
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;
}
第六十七章 参考文档:设备树 bindings
当我们遇到非标准属性或无法理解的属性时,
Linux内核源码中的 Documentation/devicetree/bindings目录存储了设备树的bindings文档,这些文档详细说明了设备和驱动程序的配置方式。
设备树 bindings文档对开发人员至关重要,因为它们以可移植和硬件独立的方式描述了硬件属性、寄存器配置和中断信息,提供了在设备树中正确描述硬件和配置驱动程序的指南。
Documentation/devicetree/bindings
目录是 Linux 内核源码中的关键部分,它包含了多个子目录,每个子目录都专注于特定类型的设备和驱动程序的设备树配置文档。这些子目录包括:
arm
:针对 ARM 架构的设备和驱动程序的配置说明。clock
:时钟设备和时钟控制器的配置指南。dma
:直接内存访问(DMA)控制器和设备的配置文档。gpio
:通用输入输出(GPIO)控制器和设备的配置信息。i2c
:I2C 总线和设备的配置说明。interrupt-controller
:中断控制器的配置指南。media
:多媒体设备和驱动程序的配置信息。mfd
:多功能设备(MFD)子系统和设备的配置文档。networking
:网络设备和驱动程序的配置说明。power
:电源管理子系统和设备的配置指南。spi
:SPI 总线和设备的配置信息。usb
:USB 控制器和设备的配置文档。video
:视频设备和驱动程序的配置说明。
这些文档通常采用 .txt
或 .yaml
格式,详细阐述了设备树中属性的语法、可选值、使用示例以及最佳实践。通过阅读这些文档,开发人员能够准确理解设备树属性的含义和用法,从而正确配置和描述硬件平台和设备,确保硬件与软件之间的有效交互。