【Linux驱动】设备树简介 | 内核对设备树的处理

🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
图

🧲设备树简介

图
如上图所示,在总线驱动模型中,由platform_device结构体对象来提供不同类型的硬件资源:

  • 代码表某个开发板的board_XXX.c文件中,定义一个或多个platform_device结构体对象来给驱动程序提供硬件资源。
  • 不同开发板的硬件资源是不同的,所以多个开发板就会有多个board_XXX.c文件。

每一个board_XXX.c都需要进行编译,在修改platform_device中的某些资源后也需要重新编译,此时对Linux内核就存在两个影响:

  1. 操作复杂:每次修改或者增加后都需要重新编译,并且还要对驱动程序进行重新装载。
  2. 内核冗余:由于开发板的种类非常多,要想让内核能驱动这些开发板,内核中势必会存在大量的board_XXX_c文件。

对此,Linux之父Linus 大发雷霆:“this whole ARM thing is a f*cking pain in the ass”。认为这些东西都行垃圾,进行了改进,于是Linux内核开始引入了设备树

设备树是一个配置文件,该文件是给内核里的驱动程序指定硬件的信息的,比 如 LED 驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。

  • 设备树的优势在于,它并不属于内核,而是位于内核之外的,而且也不参与编译。

Linux内核在运行时,会从设备树文件中读取设备节点的硬件资源信息,并提供给相应的驱动程序,它完全起到了原本board_XXX.cplatform_device结构体对象的作用,而且还解决了总线模型中存在的问题。

🏹设备树语法

图
如上图所示,之所以叫设备,是因为这些设备节点挂载在系统总线上,形成一个树状结构。

  • root:表示根节点。
  • CPU:这些蓝色框中的设备节点是根节点的子节点。
  • I2C:这些黑色框中的设备节点是子节点的子节点。

某一条支路上的设备节点可以无限挂载下去,而且一个设备节点可以拥有多个子节点,每一个节点都表示一个设备,都包含着硬件资源信息。

怎么描述设备树呢?用设备树文件dts(device tree source),它需要编译成为dtb(device tree blob)文件,内核使用的就是dtb文件。

  • 我们要写的是dts文件,这才是设备树的根本。

图

如上图所示dts文件中的代码,这就是设备树文件,它的语法规则如下:

  • DTS文件布局:
/ {
	[property definitions];
	[child nodes];
};

和Linux文件系统一样,/表示根节点,后面跟一对大括号,以封号结束,表示整个设备树。

括号内包含:

  • property definitions(属性)
  • child nodes(子节点)

位于根节点中的属性就是用来描述根节点的。属性和子节点都在[]内,表示可有可无,并不是必须写的,数量也并不是固定的,可以有一个,也可以有多个。

node格式:

设备树中的设备节点,被称为node(基本单元)

[lable:] node_name[@unit-address] {
	[property definitions];
	[child nodes];
};

每一个设备节点的组成和根节点类似,也是由属性,子节点和大括号组成:

  • lable:表示该设备节点的标号,可以省略。
  • node_name:表示该设备节点的名称,不能省略。
  • @unit-addrsss:表示该设备节点的地址,可以省略。

就拿上图dts文件中设备节点uart来说,它的名称是uart,标号是uart0,地址是@fe001000,可以使用下面两种方式来修改uart@fe0010000这个node:

  1. 在根节点之外使用label引用node
&uart0 {
	//修改属性
};
  1. 在根节点之外使用绝对路径
&{/uart@fe001000} {
	//修改属性
};

从上面两个例子中可以看出,lable的好处是使用起来更方便,而且节点名称后的地址没有实际作用,可以看作是和设备名一起构造出的设备节点名称。

properties(属性)格式:

无论是根节点还是设备节点,都有属性,属性的描述也有一定的规则,简单来说就是property_name = value,也就是属性名称= 属性值

但是属性值value有多种类型:

  • interrupts = <17 0xc>:value值用<>括起来,17和0xc是两个32位的数据:

    • 可以是10进制的,也可以是16进制,重点是尖括号,表示是32位数据。
    • 之间使用空格隔开,可以有多个数据。
  • clock-frequency = <0x00000001 0x00000000>:value是一个64位的数据,需要用到0x000000010x00000000两个cell来表示。

  • compatible = "simple -bus", "A", "B":value有三个字符串,字符串之间用,隔开。

  • local-mac-address = [00 00 12 34 56 78]:value值有多个,全部都是16进制,用两个16进制数来表示一个字节,[]内的数字必然是16进制的。

  • example = <0xf00f0000 19>, "hello":value值有两种类型,之间用,隔开。

🏹常见节点和属性

根节点:

dts文件中必须要有一个根节点:

/dts-v1/
/ {
	//根节点属性
	modle = "fsl,mpc8572ds";
	compatible = "fsl,mpc8572ds", "smdk2410", "mini2440";
	#address-cells = <1>;
	#size-cells = <1>
};
  • model:表示使用该设备树文件的开发板是什么型号的,fsl,mpc8572ds表示这是飞思卡尔公司的mpc8572ds开发板。
  • compatible:表示兼容性,表示该设备树兼容fsl,mpc8572dssmdk2410mini2440三种驱动程序,
    • 由于这里是根节点,所以是内核驱动程序,普通设备节点就是该设备的驱动程序。
    • 启动时,会按照先fsl,mpc8572dssmdk2410,最后是mini2440的顺序去寻找驱动程序。

对于这两个属性的值,建议采样这样的形式manufacturer,model,即厂家名,模块名

  • #address-cellscell是一个32位的数值,该属性是说地址address要用多少个32位的数来表示。
  • #size_cells:表示大小size要用多少个32位的数来表示。
/ {
	#address-cells = <1>;
	#size-cells = <1>;
	memory {
		reg = <0x80000000 0x20000000>;
	};
};

上例中是描述根节点下一段内存的起始地址和大小,#address-cells大小是1,所以reg属性中用一个数0x80000000来表示这段内存的起始地址。#size-cells大小是1,所以reg中用一个数0x2000000来表示这段内存的大小。

  • reg:表示寄存器地址,在设备树里,可以用来描述一段空间,因为在ARM中,寄存器和内存是统一编址的,所以在访问寄存器和内存是方法上没有区别。
    • reg属性的值,是一系列address size

CPU节点:

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

	cpu0:cpu@0 {
		.....
	};
};

CPU节点一般不用我们设置,在dtsi文件中都定义好了,也就是说有人已经写好了,不用我们管。后面会介绍到dtsi文件。

memory节点:

前面在介绍#address-cells#size-cells的时候已经讲解过了,这里就不说了。

chosen节点:

chosen {
		//chosen属性
		bootargs = "root=/dev/sda2";
	};

这是一个虚拟节点,可以通过该节点向内核中传入一些参数,传入的就是属性值。


status属性:

&uart1 {
	status = "disabled";
};

上例中,可以通过status控制uart1是否使能,如果是disabled的话,就不会创建uart1设备节点。

status常用取值:

value 描述
“okay” 设备正常运行
“disabled” 设备不可操作,但是后面可以恢复工作
“fail” 发生了严重错误,需要修复

常用的主要是okaydisabled这两个值。

name属性:

name = "字符串",该属性是用来表示节点名字的,在匹配驱动程序时会用到,但是过时了,使用的不是很多。

device_type:

device_type = "字符串",该属性是用来表示节点类型的,也是在匹配驱动程序时会用到,也过时了,使用的不是很多。

🏹编译设备树文件

一般不会从头写dts文件,而是修改,修改完毕后需要重新编译成dtb文件,先看一下Linux-4.9.88/arch/arm/boot/dts目录下的100ask_imx6ull-14x14.dts设备树文件:

tu
如上图所示,除了有很多节点外,还使用C语言的语法#include包含了imx6ull.dtsi头文件。

  • dtsi设备树文件是别人写好的模板,我们只需要在这个基础上进行修改即可。

进入到内核目录Linux-4.9.88中:

  • 执行指令touch arch/arm/boot/dts/100ask_imx6ull-14x14.dts 修改一下设备树文件的时间。
  • 执行make dtbs V=1编译设备树文件。

使用V=1选项是为了查看编译过程中的打印信息:

图
如上图所示编译过程中的打印信息:

  • 先使用了gcc编译器对dts设备树文件进行了预处理,目的就是将使用C语言#include包含的dtsi头文件复制到dts文件中。
  • 然后再使用scripts/drc/dtc处理dts文件生成dtb文件。

如果只使用dtb编译工具,是不支持#include语法的,只能使用/inculde语法将dtsi文件包含进去。

  • 这也说明了,天下板子一大抄,我们写的dts文件继承了别人写好的dtsi文件。

增加设备树节点:

tu
如上图,在设备树文件中增加BigMiaomi_Node节点,属性为BigMiaomi_test = "A-Big-Miaomi"。然后使用make dtbs指令编译dts文件。编译好以后,将生成的dtb文件放到网络文件系统中。然后启动开发板,并且挂载网络文件系统。

图

如上图,将网络文件系统中的dtb文件拷贝到/boot目录下,覆盖原本的dtb文件,然后执行reboot指令重启开发板。
图

如上图所示,重启后在/sys/firmware/devicetree/base/目录下有多个文件,这里的每个文件都代表一个设备树中的节点,可以看到,我们增加的BigMiaomi_Node也在这里。

查看新增的节点文件,可以看到里面有节点属性BigMiaomi_test和节点名字name两个文件。

图
如上图,使用cat指令查看这两个文件,属性文件中存放的是属性值A-Big-Maiomi,节点名字文件中存放的是节点名字BigMiaomi_Node,和本喵在设备树文件中增加的节点相对应。

  • 对于value是字符串的属性,使用cat指令就可以查看。
  • 对于value是数值的属性,需要使用hemdump指令才能查看。

🧲内核对设备树的处理

设备树是给驱动程序提供资源的,起到总线驱动模型中platform_device结构体对象的作用,那么内核是如何做到的呢?是怎么处理设备树dts文件的呢?

图
如上图所示,设备树文件从dts文件类型开始,处理流程为:

  • dts在Linux服务器上被编译为dtb文件。
  • u-bootdtb文件传给内核。
  • 内核解析dtb文件,把每一个节点都转换为device_node结构体。
  • 对于某些device_node结构体,内核会将其转换为platform_device结构体。

图

如上图所示device_node结构体定义,设备树中的每一个节点都会被内核解释为这样的一个结构体对象,包含成员:

  • name:节点名称,来自name属性。
  • type:节点类型,来自device_type属性。
  • porperties:节点属性链表,包含节点中的多个属性。
  • parent:父节点。
  • child:字节点。

再看属性结构体struct property的定义:

  • name:属性名称。
  • length:属性值的长度(字节)。
  • value:属性值。

假设属性值是一个字符串,那么此时vaule中存放的就是字符串的首地址,通过length可以获得整个字符串。包括数字也是利用这二者求得的。

🏹会被转换为platform_device的节点

虽然所有节点都会被转换为device_node结构体,但是并不是所有节点都会被转换为platform_device结构体,会被转换为platform_device结构体提供硬件资源的节点要有以下特点:

图

用上面设备树文件为例来说明:

  • 根节点下含有compatible属性的节点。

上面设备树文件中的mytesti2cspi节点都会被转换为platform_device结构体,因为它们都包含compatible属性。

  • 必须是根节点的直系子节点符合该条件时才会转换。
  • 含有特定compatible属性值节点的子节点,该值有四种,只要符合一个就可以。
    • 这四个值是compatible = "simple-bus", "simple-mfd", "is", "arm,amba-bu"

上面设备树文件中mytest子节点mytest@0可以转换,因为mytest节点的compatible属性中有"simple-bus"属性,所以它的子节点mytest@0会被转换。

i2c节点的子节点at24c02不会被转换,因为它的父节点的compatible属性中没有那个四个值之一,同样的spi节点的子节点flash@0也不会被转换。

  • 这两个不会被转换的节点分别挂载在i2c总线和spi总线上,它们如何处理完全由父节点i2c控制器和spi控制器决定。

对于at24c02一般会被其父节点i2c处理成i2c_client结构体对象,对于flash@0一般会被其父节点spi处理成spi_device结构体对象。


将设备树中的某些节点转换为platform_device结构体对象后,该结构体中也有提供资源的resource数组,该数组中的资源值来自第一步转换后的device_node结构体中的属性链表properties

图
如上图所示,在platform_device中有一个dev成员,该成员也是一个结构体,里面含有device_node* of_node成员。该成员of_node就表示根节点。

  • 内核设备树文件中的根节点也处理成一个device_node结构体变量。

所以节点转换为platform_device结构后,可以从of_node根节点中找到设备树中任意一个device_node节点,获取它们属性链表中的任意一个属性,然后存放到platform_device结构体中的resource数组里。

  • 节点中的reg属性转换为platform_device后的资源类型是IORESOURCE_MEM类型。
  • 节点中的interrupts属性转换为platform_device后的资源类型是IORESOURCE_IRQ类型。

🏹匹配驱动程序

和总线驱动模型中一样,设备树中的节点被转换成platform_device结构体以后会插入到总线的Dev链表中,此时就会自动去Drv链表中匹配platform_driver,匹配成功后调用驱动程序中的probe函数。

tu
上图所示是用来匹配的paltform_match函数,之前在总线模型中,只讲解了使用1,3,4三步来匹配,没有讲解第二步,因为这一步是在使用了设备树后才会用到的。

第二步匹配过程:

图
如上图,由设备树节点转换后的platform_device结构体对象,通过dev成员中的of_node成员,可以找到插入Dev链表中新节点的device_node结构体对象,得到新节点的属性。

该结构体前面介绍过:

  • name来自设备树节点中的name属性。
  • type来自设备树节点中的device_type属性。
  • properties中存放节点的所有属性。

图
如上图所示,Drv链表中进行匹配的platform_driver结构体,它的device_driver成员就会包含一个数组of_match_table

图
如上图所示是该数组中存放的每个元素的类型定义,包含:

  • name:所支持节点的name属性。
  • type:所支持节点的device_type属性。
  • compatible:所支持节点的compatible属性。

此时Dev链表中有新插入节点的device_node结构体对象,Drv链表中有驱动程序中的device_driver结构体对象。

图
如上图所示,匹配顺序如下:

  1. 新节点中的compatible属性和驱动程序中的compatible进行匹配,成功则返回,失败则进行第二步。
  2. 新节点的device_type属性和驱动程序中的type进行匹配,成功则返回,失败则进行第三步。
  3. 新节点的name属性和驱动程序的name进行匹配,成功则返回,失败则说明设备树无法匹配成功。

而设备树中建议不再使用device_typename属性,所以基本上只使用设备节点的compatible属性来匹配platform_driver

  • 至此,加上总线驱动模型中的1,3,4步,匹配platform_deviceplatform_driver时一共有1,2,3,4步。

🏹使用没有转换的节点

设备树中所有节点都会转换为device_node结构体,但是并不是所有device_node都会转为为platform_device结构体,这些没有转换的结构体我们该怎么使用它们呢?

  • 没有转换的结构体如何使用device_node中的属性呢?

图
如上图所示of_find_node_by_path函数:

  • 功能:根据节点路径获取device_node结构体指针。
  • 形参:要获取节点的绝对路径。
  • 返回值:device_node结构体指针。

通过该函数可以得到没有转换为platform_device节点的device_node,该节点中包含prorerties属性列表。

图
如上图所示of_find_property函数:

  • 功能:获得指定节点np中名为name的属性。
  • 形参:np是指定节点的device_node结构体指针,name是要寻找属性的名称,lenp是属性长度,即它的值长度。
  • 返回值:得到是要寻找属性的struct property结构体指针。

虽然这样类似的函数有很多,但是使用这两个就可以使用没有转换为platform_device的节点了,其他函数在遇到的时候再讲解。

  • 根据节点路径名先获得该节点的device_node结构体指针。
  • 再根据该指针获得指定name的属性。

🧲总结

要明白引入设备树的原因,以及掌握书写设备树的基本语法,知道设备树文件中常见的节点和属性,了解内核对设备树文件的大致处理流程等知识。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2023-12-31 01:56:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-31 01:56:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-31 01:56:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-31 01:56:02       20 阅读

热门阅读

  1. RK3566 Android 11平台上适配YT8512C 100M PHY

    2023-12-31 01:56:02       40 阅读
  2. 配置git免密push

    2023-12-31 01:56:02       38 阅读
  3. 佛洛依德算法详解

    2023-12-31 01:56:02       31 阅读
  4. Modbus4j核心代码讲解

    2023-12-31 01:56:02       41 阅读
  5. honoka的键盘#洛谷

    2023-12-31 01:56:02       37 阅读
  6. 【STL】std::map使用小结

    2023-12-31 01:56:02       34 阅读
  7. TypeScript 类方法装饰器

    2023-12-31 01:56:02       35 阅读
  8. 什么是redis雪崩

    2023-12-31 01:56:02       33 阅读
  9. ELF Strip

    2023-12-31 01:56:02       27 阅读
  10. RabbitMQ消息队列常见面试题

    2023-12-31 01:56:02       36 阅读
  11. Git 命令

    2023-12-31 01:56:02       28 阅读
  12. 【Linux】修复 Linux 错误 - 地址已在使用中

    2023-12-31 01:56:02       31 阅读
  13. ❀My排序算法学习之选择排序❀

    2023-12-31 01:56:02       34 阅读