当前位置: 首页 > article >正文

【嵌入式Linux】Linux设备树详解

设备树是是Linux中一种用于描述硬件配置的数据结构,它在系统启动时提供给内核,以便内核能够识别和配置硬件资源。设备树在嵌入式Linux系统中尤其重要,因为这些系统通常不具备标准的硬件配置,需要根据实际的硬件配置来动态配置内核。在Linux中,设备树源文件的扩展名为.dts,其二进制编码文件为.dtb,将.dts编译成.dtb需要使用DTC工具,位于Linux内核的scripts/dtc文件夹下

基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile

// 从381行开始
dtb-$(CONFIG_SOC_IMX6UL) += \
imx6ul-14x14-ddr3-arm2.dtb \
imx6ul-14x14-ddr3-arm2-emmc.dtb \
....
dtb-$(CONFIG_SOC_IMX6ULL) += \
imx6ull-14x14-ddr3-arm2-emmc.dtb \
imx6ull-14x14-ddr3-arm2-flexcan2.dtb \
imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \
imx6ull-14x14-ddr3-arm2-wm8958.dtb \
imx6ull-14x14-evk.dtb \
imx6ull-14x14-evk-btwifi.dtb \
imx6ull-14x14-evk-emmc.dtb \
imx6ull-14x14-evk-gpmi-weim.dtb \

当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到IMX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果使用了这一款SOC搓了一块板子,那么我们只需要新建一个该板子对应的.dts,然后将对应的.dtb文件名添加到dtb-$(CONFIG_SOC_IMX6ULL)下,这样在使用make编译设备树的时候就会将对应的.dts编译为二进制的.dtb

一般dtb文件会和根文件目录以及uboot一同烧录进启动存储设备中。(待补充)

如何编写DTS

虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts文件上进行修改。但是我们肯定需要修改.dts文件,因此DTS 文件语法我们还是要学习一遍

.dtsi头文件

和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。比如我们使用的正点原子imx6ull使用的设备树是imx6ull-alientek-emmc.dts,而imx6ull-alientek-emmc.dts里又include了imx6ull.dtsi,以下是从imx6ull.dtsi 文件中缩减出来的设备树文件内容:

/ {
	aliases {
		can0 = &flexcan1;
	};

	cpus {
		#address-cells = <1>;
		#size-cells = <0>;

 		cpu0: cpu@0 {
 			compatible = "arm,cortex-a7";
 			device_type = "cpu";
 			reg = <0>;
 		};
 	};

 	intc: interrupt-controller@00a01000 {
 		compatible = "arm,cortex-a7-gic";
 		#interrupt-cells = <3>;
 		interrupt-controller;
 		reg = <0x00a01000 0x1000>,
 			  <0x00a02000 0x100>;
 	};
 }

第 1 行,“/”是根节点,每个设备树文件只有一个根节点。如果有多个dts或dtsi,那么这几个设备树文件的根节点会合并为一个根节点。aliases、cpus 和 intc 是三个子节点。

在设备树中节点命名格式为:label:node-name@unit-address,其中“label”是节点标签,主要是方便访问,“node-name”是节点名字,为 ASCII 字符串,“unit-address”一般表示设备的地址或寄存器首地址,如果没有则可以省去,nodename和unit-addr共同构成了节点名字。intc: interrupt-controller@00a01000就是这种命名格式,而label的存在使得使用&cpu0就可以访问cpu@0这个节点

每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中有如下几种数据形式:
1.字符串

compatible = “arm,cortex-a7”;

2.32位无符号整数,可以是一组值也可以是单个值

reg = <0 0x123456 100>;

3.字符串列表,使用‘,’分隔字符串

compatible = “fsl,imx6ull-gpmi-nand”, “fsl, imx6ul-gpmi-nand”;

标准属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性

1. compatible 属性
compatible 属性也叫做“兼容性”属性,compatible 属性的值是一个字符串列表,这个属性的作用是告诉内核,当前的设备节点应该由哪个驱动来处理。内核会根据compatible属性中列出的字符串,去查找与之匹配的驱动程序。如果找到了匹配的驱动,内核就会尝试加载并初始化该驱动,以便操作相应的硬件设备,其格式如下:

compatible = “manufacturer,model”

其中 manufacturer 表示厂商,一般是芯片制造厂商,model 一般是模块对应的驱动名字,比如 imx6ull-alientek-emmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点,I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜出品的 WM8960,sound 节点的 compatible 属性值如下:

compatible = “fsl,imx6ul-evk-wm8960”,“fsl,imx-audio-wm8960”;

其中fsl表示厂商是飞思卡尔,“imx6ul-evk-wm8960”是驱动模块的名字,这个属性会遍历字符串列表中符合的驱动程序名称,然后再Linux内核中查找该驱动程序,如果第一个没有则第二个,第二个没有则第三个,直到找到位置。

在内核中,驱动程序通常会提供一个匹配表,这个表中列出了该驱动支持的所有compatible字符串。当内核解析设备树时,它会检查每个设备节点的compatible属性,并与驱动程序的匹配表进行对比,以确定是否应该由该驱动程序来处理该设备节点,比如在文件 imx-wm8960.c 中有如下内容

// 从632行开始
static const struct of_device_id imx_wm8960_dt_ids[] = {
	{ .compatible = "fsl,imx-audio-wm8960", },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);

static struct platform_driver imx_wm8960_driver = {
	.driver = {
		.name = "imx-wm8960",
		.pm = &snd_soc_pm_ops,
		.of_match_table = imx_wm8960_dt_ids,
	},
	.probe = imx_wm8960_probe,
	.remove = imx_wm8960_remove,
};

imx_wm8960_dt_idss 就是 imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”,可以和imx6ull-alientek的dts中sound节点中的compatible 属性的字符串相匹配,那么这个节点就会使用此驱动文件。

2. model属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比
如:

model = “wm8960-audio”;

3.status 属性
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的状态信息,可选的状态如表:

描述
“okay”表明设备是可操作的。
“disabled”表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档。
“fail”表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
“fail-sss”含义和“fail”相同,后面的 sss 部分是检测到的错误内容

4.#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,格式如下:

reg = <address1 length1 address2 length2 address3 length3……>

每个“address length”组合表示一个子节点设备或其寄存器的地址范围,其中 address 是起始地址,length 是地址长度,addressx表示第x个设备的起始地址,lengthx表示第x个设备的长度,当#address-cells=<1>的时候表示reg中的每一个address的长度是1字长,同样,#size-cells=<1>表示每个length的长度是1字长。

这部分看不懂没关系,后面会有详解

5.reg 属性
reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息

6.ranges属性
ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度
这三部分组成,目的是将:

  • child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
  • parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
  • length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长

如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。以ranges = <0x0 0xe0000000 0x00100000>;为例,此属性值指定了一个 1024KB(0x00100000)的地址范围子,地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000,range会将子空间映射到父空间中,这样,我们在写程序的时候就直接以0x0为起点进行编程就好,地址映射会自动映射到父空间的对应位置

特殊属性

chosen属性
chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。在chosen中会存储放着uboot中的bootargs数据,由uboot引导内核启动时传递进来。

aliases属性
单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。别名也可以用过在声明节点时候在@之前声明

设备树的工作方式

在DTS文件中,每个节点都有 compatible 属性,但根节点 compatible 属性值得拿出来单独说一下,通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,第二个值描述了设备所使用的 SOC。我们一般会将设备树和uboot一起烧录到启动设备中,uboot在引导Linux内核启动时,会将设备树dtb文件的首地址传给Linux内核,Linux内核检查设备树的根节点compatible 就可以获知当前设备的信息

以IMX6ULL设备为例子,其设备树arch/arm/mach-imx/mach-imx6ul.c的根节点信息如下:

/ {
	model = "Freescale i.MX6 ULL 14x14 EVK Board";
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

Linux内核中存储着其支持的板子和芯片的信息,Linux内核都用MACHINE_STARTMACHINE_END来定义一个machine_desc 结构体来描述这个设备,Linux内核会获取设备树文件dtb的根节点compatible 属性,然后遍历它支持的machine_desc 结构体,用于确认自己是否支持当前设备。该结构体位于arch/arm/include/asm/mach/arch.h,源码如下

#define DT_MACHINE_START(_name, _namestr)		\
static const struct machine_desc __mach_desc_##_name	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= ~0,				\
	.name		= _namestr,

#endif

可以看到,要实现该架构体需要两个参数:以IMX6ULL设备为例子,这个设备的machine_desc 结构体实现位于文件 arch/arm/mach-imx/mach-imx6ul.c

static const char *imx6ul_dt_compat[] __initconst = {
	"fsl,imx6ul",
	"fsl,imx6ull",
	NULL,
};

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
	.map_io		= imx6ul_map_io,
	.init_irq	= imx6ul_init_irq,
	.init_machine	= imx6ul_init_machine,
	.init_late	= imx6ul_init_late,
	.dt_compat	= imx6ul_dt_compat,
MACHINE_END

machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性。只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。而mach-imx6ul.c的根节点的compatible属性中的"fsl,imx6ull"显然与之匹配,那么Linux就可以确认该设备树dtb是自己支持的,从而将设备树加载进来。

Linux内核设备树与machine_desc匹配详解

上一小节说了Linux Kernel通过将uboot提供的设备树的compatible属性和自己的machine_desc 一 一对比,从而确定内核是否支持当前设备树。那内核具体是怎么匹配的呢?

Linux 内核调用 start_kernel 函数来启动内核,start_kernel 函数会调用setup_arch 函数来匹配 machine_descsetup_arch 函数定义在文件 arch/arm/kernel/setup.c 中,源码如下:

// 913行开始
void __init setup_arch(char **cmdline_p)
{
	const struct machine_desc *mdesc;

	setup_processor();
	mdesc = setup_machine_fdt(__atags_pointer);
	if (!mdesc)
		mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
	machine_desc = mdesc;
	machine_name = mdesc->name;
	dump_stack_set_arch_desc("%s", mdesc->name);

其中setup_machine_fdt函数就是用于获取匹配的 machine_desc的, 参数就是 atags 的首地址,也就是 uboot 传递给 Linux 内核的 dtb 文件首地址,setup_machine_fdt 函数的返回值就是找到的最匹配的 machine_desc

函数 setup_machine_fdt 定义在文件 arch/arm/kernel/devtree.c

// 204行开始
const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
	const struct machine_desc *mdesc, *mdesc_best = NULL;
	...
	if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
		return NULL;

	mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

找到匹配的 machine_desc 的过程就是用设备树根节点的compatible 属性值和 Linux 内核中 machine_desc.dt_compat 的值比较,看看那个相等,如果相等的话就表示找到匹配的 machine_desc,反之则获取下一个machine_desc结构体。

从源码可知, 函数of_flat_dt_match_machine就是用来对比设备树和machine_desc结构体的函数,其包含两个参数:,参数 mdesc_best是默认的 machine_desc, arch_get_next_mach是一个函数,该函数的工作就是获取 Linux 内核中下一个 machine_desc 结构体。我们进入of_flat_dt_match_machine这个函数再看看,源码如下:

	dt_root = of_get_flat_dt_root();
	while ((data = get_next_compat(&compat))) {
		score = of_flat_dt_match(dt_root, compat);
		if (score > 0 && score < best_score) {
			best_data = data;
			best_score = score;
		}
	}

首先是用of_get_flat_dt_root()来获取设备树根节点,然后使用wile循环查找每一个machine_desc,使用of_flat_dt_match函数将根节点 compatible 属性的值和每个 machine_desc 结构体中.dt_compat 的值进行比较,直至找到匹配的那个machine_desc

如何向设备树中添加或修改内容

添加或者修改内容应该就是向.dtsi文件中新增节点等等,比如如第一版硬件上有一个 IIC 接口的六轴芯片 MPU6050,第二版硬件又要把这个 MPU6050 更换为 MPU9250 等。我们就要同步的修改设备树文件。或者假设现在有个六轴芯片
fxls8471,fxls8471 要接到 I.MX6U-ALPHA 开发板的 I2C1 接口上,那么相当于需要在 i2c1 这个节点上添加一个 fxls8471 子节点。最简单的处理方法就是在imx6ull.dtsi文件下的 i2c1 下直接添加一个名为 fxls8471 的子节点

i2c1: i2c@021a0000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
	reg = <0x021a0000 0x4000>;
	interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_I2C1>;
	status = "disabled";
	//fxls8471 子节点
	fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
	};
};

但是这样会有个问题,其他所有使用到 I.MX6ULL这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备。所以我们应该在对应板子的dts文件中去改,而不是直接改这个设备树头文件。I.MX6U-ALPHA 开发板使用的设备树文件为imx6ull-alientek-emmc.dts,因此我们需要在
imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:

&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";

	mag3110@0e {
		compatible = "fsl,mag3110";
		reg = <0x0e>;
		position = <2>;
	};

 	fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
		position = <0>;
		interrupt-parent = <&gpio5>;
		interrupts = <0 8>;
	};
};

相较于源文件,我们改了:

  • 添加新属性clock_frequency,用于指定i2c1时钟频率
  • status属性从disable改为okay
  • 添加新的i2c1 子节点 fxls8471,用于支持我们新加的设备

这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。

Linux内核的设备树解析过程

Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件。
在这里插入图片描述
Linux Kernel在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工作的函数为 unflatten_dt_node。

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。具体添加一个硬件的节点的时候需要什么属性会有对应的文档说明,具体路径为Linux 源码目录/Documentation/devicetree/bindings

设备树常用 OF 操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获
取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中

1.查找节点的 OF 函数

Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h

struct device_node {
	const char *name;
	const char *type;
	phandle phandle;
	const char *full_name;
	struct fwnode_handle fwnode;

	struct	property *properties;
	struct	property *deadprops;	/* removed properties */
	struct	device_node *parent;
	struct	device_node *child;
	struct	device_node *sibling;
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
};

与查找节点有关的 OF 函数有 5 个:

// 通过函数名字查找节点,from用于表示从哪个设备节点开始查找
extern struct device_node *of_find_node_by_name(struct device_node *from,const char *name);

// 通过设备类型查找结点
extern struct device_node *of_find_node_by_type(struct device_node *from,const char *type);

// 根据设备类型以及兼容性comaptible查找节点
extern struct device_node *of_find_compatible_node(struct device_node *from, 
		const char *type, 
		const char *compat);

// 通过of_device_id匹配表查找指定节点
extern struct device_node *of_find_matching_node_and_match(
	struct device_node *from,
	const struct of_device_id *matches,
	const struct of_device_id **match);

// 通过路径查找结点
static inline struct device_node *of_find_node_by_path(const char *path)
2.查找父/子节点的OF函数
// 用于获取指定节点的父节点
extern struct device_node *of_get_parent(const struct device_node *node);
extern struct device_node *of_get_next_parent(struct device_node *node);

// 使用迭代的方法查找子节点,从某个节点出发找遍历子节点
extern struct device_node *of_get_next_child(const struct device_node *node,
					     struct device_node *prev);

// 通过名字获取子节点
extern struct device_node *of_get_child_by_name(const struct device_node *node,
					const char *name);

of_get_next_child函数是Linux内核中用于遍历设备树(Device Tree)的一个API。它允许你迭代地访问一个特定设备节点下的所有子节点。这个函数在驱动开发中非常有用,特别是当你需要处理一个设备节点下的所有子设备时。例如,在一个总线驱动中,你可能会使用这个函数来枚举所有连接到该总线的设备。通过不断地调用of_get_next_child并传入前一个返回的子节点,你可以遍历一个设备节点下的所有子节点。

3.提取属性值的OF函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,源码如下:

struct property {
	char *name;  // 属性名称
	int	length;  // 属性长度
	void *value;	// 属性值
	struct property *next;  // 下一个属性,内核中属性以链表的方式联系
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

同样,获取属性值的of函数有:

// 查找指定属性
extern struct property *of_find_property(const struct device_node *np,
					 const char *name,
					 int *lenp);

// 获取属性中元素的数量,比如像reg等属性可能是一个数组
extern int of_property_count_elems_of_size(const struct device_node *np,
				const char *propname, int elem_size);

// 从属性中获取指定标号的u32类型数据,比如某个属性中还有多个u32类型值,那么可以通过该函数获取指定下标的u32属性值
extern int of_property_read_u32_index(const struct device_node *np,
				       const char *propname,
				       u32 index, u32 *out_value);

4.其他常用OF函数

http://www.kler.cn/a/401938.html

相关文章:

  • MySQL超详细安装配置教程(亲测有效)
  • 面试题: Spring中的事务是如何实现的?
  • Android开发实战班 - Activity 生命周期
  • etcd defrag
  • 高效工具推荐:基于WebGPU的Whisper Web结合内网穿透远程使用指南
  • 为何数据库推荐将IPv4地址存储为32位整数而非字符串?
  • 【算法设计与分析实训】第1关:求序列的最大字段和
  • 高阶云服务-ELB+AS
  • Android CPU核分配关联进程
  • Java网络编程1 - 介绍网络编程、网络编程三要素
  • STM32设计防丢防摔智能行李箱-分享
  • ReactNative的环境搭建
  • POI和easyExcel的讲解和使用
  • 最少前缀操作问题--感受不到动态规划,怎么办怎么办
  • 动态Tab导航
  • STM32G4的数模转换器(DAC)功能介绍
  • Linux-shell实例手册-服务操作
  • 基于YOLOv8深度学习的智慧农业猪行为检测系统研究与实现(PyQt5界面+数据集+训练代码)
  • SpringSecurity+jwt+captcha登录认证授权总结
  • ARM CCA机密计算安全模型之简介
  • 网络IPC:套接字汇总整理
  • 2411rust,编译时自动检查配置
  • 贴代码框架PasteForm特性介绍之select,selects,lselect和reload
  • Python入门(9)--类与对象基础
  • 30. 并发编程
  • Go 编译代码-分平台编译