嵌入式驱动学习第四周——设备树

前言

   掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树。本篇博客重点介绍一下设备树与设备树语法。

   嵌入式驱动学习专栏将详细记录博主学习驱动的详细过程,未来预计四个月将高强度更新本专栏,喜欢的可以关注本博主并订阅本专栏,一起讨论一起学习。现在关注就是老粉啦!

1. 设备树简介

1.1 设备树介绍

   描述设备树的文件叫DTS,该文件采用树形结构描述板级设备即开发板上的设备信息:CPU数量,内存基地址,IIC接口上接了哪些设备,如下所示:

在这里插入图片描述

   设备树是一种描述硬件的数据结构,在Linux3.x版本上才开始使引入,采用了设备树之后,许多硬件的细节可以直接通过它传递给Linux,而不再需要在内核中进行大量的冗余编码,它通过bootloader将硬件资源传给内核,使得内核和硬件资源描述相对独立。

   ARM 社区引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。

1.2 dtb、dts、dtc、dtsi文件的关系

   这四个代表四种不同的文件格式,可以类比到C语言中的相关知识来理解。

   我们都知道,C语言编写.c文件的时候需要添加C库.h文件来添加我们所需要用到的函数,宏等。然后要用编译器将.c文件编译成计算机能理解的二进制文件。

   同样的,在设备树中,dts是设备树源码,相当于.c文件,是我们编写和能看懂的文件,然后需要添加.dtsi文件来得到一些板级信息,相当于.h文件。最后要将这个文本文件编译成计算机理解的二进制文件,即用dtc编译工具编译成.dtb这个二进制文件。总结下来,对应关系如下所示:

.dts   -->  .c文件
.dtsi  -->   .h文件
.dtb   -->   .exe文件
dtc   -->   编译器

   dtc的源码存放于scripts/dtc目录中,对应于该目录下Makefile中hostprogs-y:=dtc这一编译目标

生成dts文件对应的dtb文件
dtc -I dts -O dtb -o xxx.dtb xxxdts
反过来生成dts文件
dtc -I dtb -O dts -o xxx.dts xxxdtb

1.3 编译设备树

   进入到Linux源码根目录下,然后执行如下指令进行编译:

make dtbs	(这个指令只编译设备树)
或者
make all (这个指令是编译所有的东西,包括.ko,zImage)

1.4 设备树特点

在这里插入图片描述

   设备树可以用树状结构描述硬件资源,如上图所示,在根节点/下,挂载本地总线的SPI总线,UART总线等的树干为根节点的子节点。若是SPI下的设备不止一个,那么又可以从这根树枝下分出枝干

   设备树可以复用,例如多个硬件平台都使用i.MX6ULL作为主控芯片, 那么我们可以将i.MX6ULL芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀, 其他设备树文件直接使用“# includexxx”引用即可。

2. 设备树语法

   设备树文件存放地址:

源码地址/arch/arm/boot/boot/dts

   此处打开正点原子的imx6ull-alientek-emmc.dts文件来学习一下设备树语法,此处提一个事情,设备树语法中的注释用/* ... */表示

2.1 基本构成

   首先来看以下一段:

#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"

   这一段首先是包含头文件,设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。 imx6ull.dtsi由NXP官方提供,是一个imx6ull平台“共用”的设备树文件。.dts 文件引用 C 语言中的.h 文件,甚至也可以引用.dts 文件。

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

	key {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "atkmini-key";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;
		key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_FALLING>;
		status = "okay";
	};

	gpioled {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "atkmini-gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_led>;
		led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
		status = "okay";
	};

	mini {
		#address-cells = <1>;
		#size-cells = <0>;
		compatible = "atkmini-led";
		status = "okay";
		reg = < 0x020c406c 0x04		/* CCM_CCGR1_BASE         */
				0x020e0068 0x04		/* SW_MUX_GPIO1_IO03_BASE */
				0x020e02f4 0x04		/* SW_PAD_GPIO1_IO03_BASE */
				0x0209c000 0x04		/* GPIO1_DR_BASE          */
				0x0209c004 0x04>;		/* GPIO1_GDIR_BASE        */
	};
	chosen {
		stdout-path = &uart1;
	};

	dht11 {
		compatible = "alientek, dht11";
		pinctrl-names = "default";
                pinctrl-0 = <&pinctrl_dht11>;
		dht11-gpio = <&gpio1 1 GPIO_ACTIVE_LOW>;
		status = "okay";
	};

	ds18b20 {
		compatible = "alientek, ds18b20";
		pinctrl-names = "default";
                pinctrl-0 = <&pinctrl_ds18b20>;
		ds18b20-gpio = <&gpio1 1 GPIO_ACTIVE_LOW>;
		status = "okay";
	};

	memory {
		reg = <0x80000000 0x20000000>;
	};

	reserved-memory {
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;

		linux,cma {
			compatible = "shared-dma-pool";
			reusable;
			size = <0x8000000>;
			linux,cma-default;
		};
	};
	/* 以下内容省略
}

   这一段是设备树节点,每个{}就是一个节点,最外面的/{...}就是根节点,每个设备树只有一个根节点。但是如果打开imx6ull.dtsi文件可以发现它也有一个根节点,虽然imx6ull-alientek-emmc.dts引用了imx6ull.dtsi文件, 但这并不代表imx6ull-alientek-emmc.dts设备树有两个根节点,因为不同文件的根节点最终会合并为一个

   然后我们可以看到根节点内部也有很多{...},比如ds18b20 {...}memory {}这些都是根节点的子节点。

   最后来看下一段:

&cpu0 {
	arm-supply = <&reg_arm>;
	soc-supply = <&reg_soc>;
	dc-supply = <&reg_gpio_dvfs>;
};

&clks {
	assigned-clocks = <&clks IMX6UL_CLK_PLL4_AUDIO_DIV>;
	assigned-clock-rates = <722534400>;
};

&csi {
	status = "okay";

	port {
		csi_ep: endpoint {
			remote-endpoint = <&camera_ep>;
		};
	};
};

   这一部分是设备树节点的追加内容,最明显的特点就是添加了一个&符号。该符号表示向已经存在的子节点追加数据,这些已经存在的节点可以是本文件中的,也可以是#include中定义的。

2.2 节点的格式

   知道了设备树的组成后,来具体看看一个节点如何定义:

node-name@unit-address {
	属性1 = ""
	属性2 = ""
	属性3 = ""
	子节点
}

   node-name是节点名称,长度为1~31个字符,最好使用大写或小写字母开头,且能描述设备类别。根节点是一个特殊的节点,其用/指代。

   @unit-address是指定单元地址,@可理解为分隔符,unit-address的值要与节点“reg”属性的第一个地址一致,如果没有reg节点,可以省略,这时就要求同级设备树下,节点名唯一。因此要么节点名唯一,要么节点名重复单单元地址不同,总之就是node-name@unit-address这个整体要求同级唯一。

   还有一种方式就是添加了节点标签:

label:node-name@unit-address
比如:
cpu0:cpu@0

   引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。此外还有一个很重要的作用就是对接点进行扩展,当其他位置需要引用时可以使用节点标签来向该节点中追加内容。

2.3 节点属性

   设备树源码中常用的几种数据形式:字符串、32位无符号整数

2.3.1 compatible属性

   属性值类型:字符串

   一般compatible属性的格式如下所示,manufacturer 表示厂商,model 一般是模块对应的驱动名字。

compatible = "manufacturer,model"
例如:
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

   设备树中的每一个代表了一个设备的节点都要有一个compatible属性。 compatible是系统用来决定绑定到设备的设备驱动的关键。 compatible属性是用来查找节点的方法之一。

   一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。如下:

static const struct of_device_id imx6ull_of_match {
	{.compatible = "fsl,imx6ull-14x14-evk"},
	{ //Sentinel }
};

static struct platform_driver imx6ull_driver = {
	.driver = {
		.name   = "xxx",
		.of_match_table = imx6ull_of_match,
	},
	.probe = imx6ull_probe,
	.remove = imx6ull_remove,
};

2.3.2 model属性

   属性值类型:字符串

   一般 model 属性描述设备模块信息,比如名字什么的。

model = "wm8960-audio";

2.3.3 status属性

   属性值类型:字符串

   该属性是设备的状态信息,可选状态如表所示:

status值描述
“okay”表示设备可操作
“disable”表示设备当前是不可操作的,但在未来可以变为可操作,比如热插拔设备插入后
“fail”表明设备不可操作,设备检测到了一系列错误,且设备不大可能变得可操作
“fail-sss”含义与"fail"相同,后面的sss是检测到的错误内容

2.3.4 #address-cells和#size-cell

   属性值类型:整数

   #address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值。具体的结合下面的reg属性讲解。

2.3.5 reg属性

   属性值类型:整数(表示地址)

   reg的形式如下:

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

   #address-cells控制address的数量,#size-cells控制length的数量。如下:

spi4 {
	compatible = "spi-gpio";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_spi4>;
	status = "disabled";
	gpio-sck = <&gpio5 11 0>;
	gpio-mosi = <&gpio5 10 0>;
	num-chipselects = <1>;
	#address-cells = <1>;
	#size-cells = <0>;

	gpio_spi: gpio_spi@0 {
		compatible = "fairchild,74hc595";
		gpio-controller;
		#gpio-cells = <2>;
		reg = <0>;
		registers-number = <1>;
		registers-default = /bits/ 8 <0x57>;
		spi-max-frequency = <100000>;
	};
};

   父节点设置了#address-cells = <1>以及#size-cells = <0>,于是在子节点中就是reg<0>,表示只设置了起始地址,没有设置地址长度。

2.3.6 ranges属性

   属性值类型:任意数量的 <子地址、父地址、地址长度>编码

   比如对于#address-cells和#size-cells都为1的话,以ranges=<0x0 0x10 0x20>为例,表示将子地址的从0x0~(0x0 + 0x20)的地址空间映射到父地址的0x10~(0x10 + 0x20)

   可以为空,如下所示:

soc {
	...
	ranges;
	...
}

   不为空时如下所示:

soc {
	compatible = "simple-bus";
	#address-cells = <1>;
	#size-cells = <1>;
	ranges = <0x0 0xe0000000 0x00100000>;
	
	serial {
		device_type = "serial";
		compatible = "ns16550";
		reg = <0x4600 0x100>;
		clock-frequency = <0>;
		interrupts = <0xA 0x8>;
		interrupt-parent = <&ipic>;
	};
};

   节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。

   serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。

2.3.7 name和device_type

   属性值类型:字符串

   这两个属性值已经被抛弃了。

2.3.8 特殊节点

2.3.8.1 aliases子节点

   aliases子节点的作用就是为其他节点起一个别名,如下所示:

aliases {
    can0 = &flexcan1;
    can1 = &flexcan2;
    ethernet0 = &fec1;
    ethernet1 = &fec2;
    gpio0 = &gpio1;
    gpio1 = &gpio2;
    gpio2 = &gpio3;
    gpio3 = &gpio4;
    gpio4 = &gpio5;
    i2c0 = &i2c1;
    i2c1 = &i2c2;
    /*----------- 以下省略------------*/
}

   以can0 = &flexcan1;为例。flexcan1是一个节点的名字, 设置别名后我们可以使用can0来指代flexcan1节点,与节点标签类似。 在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。 在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。 也可以使用别名“一步到位”找到节点。

2.3.8.2 chosen子节点

   chosen子节点不代表实际硬件,它主要用于给内核传递参数,此外这个节点还用作uboot向linux内核传递配置参数的“通道”, 我们在Uboot中设置的参数就是通过这个节点传递到内核的, 这部分内容是uboot和内核自动完成的。

3. 获取设备树节点信息

3.1 查找节点

3.1.1 根据节点路径:

   就和windows下查找文件一样,我们也可以通过节点路径查找节点。

/*
 * @description: 根据节点路径查找节点
 * @param-path : 指定节点在设备树中的路径
 * @return     : 返回device_node结构体指针,如果查找失败返回NULL,否则返回device_node类型的结构体指针,保存设备节点的信息。
 */
struct device_node *of_find_node_by_path(const char *path)

   得到device_node结构体之后我们就可以使用其他of 函数获取节点的详细信息。

3.1.2 根据节点类型和compatible属性寻找节点函数

/*
 * @description     : 根据节点类型和compatible属性寻找节点函数
 * @param-from      : 指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL表示从根节点开始查找
 * @param-type      : 要查找节点的类型,这个类型就是device_node-> type
 * @param-compatible: 要查找节点的compatible属性
 * @return          : device_node类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL
 */
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)

3.1.3 其他方式

  常用的为以上几种,如果想看其他的话可以看野火的文档。

3.2 获取属性值

   找到一个设备节点就会返回这个设备节点对应的结构体指针(device_node*)。这个过程可以理解为把设备树中的设备节点“获取”到驱动中。“获取”成功后我们再通过一组of函数从设备节点结构体(device_node)中获取我们想要的设备节点属 性信息。其of函数存放在以下目录下:

内核源码/include/linux/of.h

3.2.1 device_node结构体

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;
#if defined(CONFIG_OF_KOBJ)
    struct  kobject kobj;
#endif
    unsigned long _flags;
    void    *data;
#if defined(CONFIG_SPARC)
    const char *path_component_name;
    unsigned int unique_id;
    struct of_irq_controller *irq_trans;
#endif
};
  • name: 节点中属性为name的值
  • type: 节点中属性为device_type的值
  • full_name: 节点的名字,在device_node结构体后面放一个字符串,full_name指向它
  • properties: 链表,连接该节点的所有属性
  • parent: 指向父节点
  • child: 指向子节点
  • sibling: 指向兄弟节点

3.2.2 获取节点属性

/*
 * @description: 寻找指定属性
 * @param-np   : 设备节点
 * @param-name : 属性名字
 * @param-lenp : 属性值的字节数
 * @return     : 找到的属性
property *of_find_property(const struct device_node *np, const char *name, int *lenp)

   属性的property结构体:

struct property {
	char	*name;		// 属性名字
	int	length;			// 属性长度
	void	*value;		// 属性值
	struct property *next;	// 下一个属性
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

3.2.3 其他of函数

   太多了,可以看原子的资料,或者查看这位博主的博客:Linux 学习笔记: 设备树—常用OF操作函数

4. Linux设备树调试

   我们可以在Linux下查看设备树信息:

cd /proc/device-tree
ls

在这里插入图片描述

   Linux内核在启动时会解析设备树的各个节点信息,并在/proc/device-tree目录下根据节点名字创建不同的文件或文件夹

   如果要查看其下面有哪些属性和节点,cd进去即可,比如我要看最后一个spi4:

cd spi4
ls

在这里插入图片描述

参考资料

[1] 【正点原子】I.MX6U嵌入式Linux驱区动开发指南 第四十三章
[2] 【野火】嵌入式Linux驱动开发实战指南——基于I.MX6ULL系列
[3] Device Tree -----设备树
[4] Linux 学习笔记: 设备树—常用OF操作函数

  • 30
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嵌入式驱动是指针对特定硬件平台,开发设备驱动程序的技术。这些驱动程序可以控制各种外围设备,例如传感器、执行器、通信接口等。嵌入式驱动程序通常需要直接操作硬件,因此需要深入了解硬件结构和编程技术。 以下是一个嵌入式驱动学习实战的教程: 1. 首先,了解嵌入式系统的基本概念和发展历史。可以学习嵌入式系统的硬件、软件、通信接口、操作系统等方面的知识。 2. 掌握嵌入式系统的基础编程语言,例如C语言和汇编语言。这些语言是嵌入式系统开发的基础,需要熟练掌握。 3. 学习硬件体系结构和编程技术。这包括处理器架构、内存管理、中断处理、GPIO控制等方面的知识。 4. 学习嵌入式系统的设备驱动程序开发技术。这包括字符设备驱动、块设备驱动、网络设备驱动等方面的知识。 5. 掌握常见的嵌入式系统开发工具,例如编译器、调试器、仿真器等。这些工具可以帮助开发人员调试和测试驱动程序。 6. 进行实战练习,例如使用开发板开发一个简单的设备驱动程序。可以从最基础的GPIO控制开始,逐步扩展到其他设备驱动程序的开发。 7. 学习嵌入式系统的调试技术,例如使用调试器进行单步调试和断点调试。这些技术可以帮助开发人员快速定位和解决问题。 总之,嵌入式驱动学习需要系统化的知识结构和实战经验。通过不断学习和实践,可以逐步掌握嵌入式驱动开发的技术和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值