Linux:设备树学习篇(1)

2022-11-15 16:17:40 浏览数 (1)

Linux:设备树学习篇(1)

  • 1. 什么是设备树
  • 2. 为什么要使用设备树来替代传统的总线设备驱动模型
  • 3. DTS、DTB 和 DTC
  • 4. DTS 语法
    • 4.1 .dtsi 头文件
    • 4.2 设备节点
    • 4.3 标准属性
  • 5. DTS 编译
    • 5.1 内核编译设备树
    • 5.2 dtc 工具编译设备树

1. 什么是设备树

设备树是一种数据结构,它通过特有的语法格式描述片上片外的设备信息。由BootLoader传递给kernel,kernel进行解析后形成和驱动程序关联的dev结构供驱动代码使用。

描述设备树的文件叫做 DTS(DeviceTree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。

树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS 文件的主要功能就是按照图所示的结构来描述板子上的设备信息。

2. 为什么要使用设备树来替代传统的总线设备驱动模型

传统的总线设备驱动是将设备信息描述在C代码中,当需要修改驱动相关的硬件信息时,就得修改具体的代码文件,再全编译内核。整个操作繁琐且不利于代码的维护和移植。

设备树的方式将驱动和设备完全分离开。将驱动程序设计成硬件无关的类型,一切设备资源(比如memory,interrupt,clk,pinctrl)在设备树文件中定义。内核来适配驱动和设备信息。将有效的设备信息通过参数传递给驱动的probe函数,再进行具体硬件的初始化。这样当硬件出现变更时(各公司基于芯片公版单独设计PCB等情况),只需要去修改对应的设备树文件,而完全不用去更改驱动代码。驱动的通用性也会大大提供。这样多个系列芯片只需要共用同一套驱动代码,差分设备树文件就可以。

3. DTS、DTB 和 DTC

设备树的代码文件是dts文件和dtsi文件。

  1. dts 是设备树源码文件;
  2. dtsi文件类似include头文件,可以被dts文件包含;
  3. dtb是将 dts编译以后得到的二进制文件。

dts文件会被dtc(设备树编译器)编译为dtb(device tree block)的二进制文件。该文件会被烧写到内存的特定地址(由BootLoader指定,原则上随意,只要不覆盖了boot和kernel的内容就好)。再由BootLoader将地址通过参数传递给kernel。kernel根据dtb文件的特定格式解析出有效的设备信息,从而传递给驱动代码。

dtc 工具源码在 Linux 内核的 scripts/dtc 目录下:

kernel/msm-5.4/scripts/dtc$ cat Makefile

代码语言:javascript复制
# SPDX-License-Identifier: GPL-2.0
# scripts/dtc makefile

hostprogs-$(CONFIG_DTC) := dtc
ifeq ($(DTC_EXT),)
always          := $(hostprogs-y)
endif

dtc-objs        := dtc.o flattree.o fstree.o data.o livetree.o treesource.o 
                   srcpos.o checks.o util.o
dtc-objs         = dtc-lexer.lex.o dtc-parser.tab.o

# Source files need to get at the userspace version of libfdt_env.h to compile
HOST_EXTRACFLAGS  = -I $(srctree)/$(src)/libfdt

ifeq ($(shell pkg-config --exists yaml-0.1 2>/dev/null && echo yes),)
ifneq ($(CHECK_DTBS),)
$(error dtc needs libyaml for DT schema validation support. 
        Install the necessary libyaml development package.)
endif
HOST_EXTRACFLAGS  = -DNO_YAML
else
dtc-objs         = yamltree.o
HOSTLDLIBS_dtc  := $(shell pkg-config yaml-0.1 --libs)
endif

# Generated files need one more search path to include headers in source tree
HOSTCFLAGS_dtc-lexer.lex.o := -I $(srctree)/$(src)
HOSTCFLAGS_dtc-parser.tab.o := -I $(srctree)/$(src)

# dependencies on generated files need to be listed explicitly
$(obj)/dtc-lexer.lex.o: $(obj)/dtc-parser.tab.h

DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译出 DTC 这个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:make all 或者 make dtbs。

4. DTS 语法

4.1 .dtsi 头文件

和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。与此同时,.dts 文件也可以引用 C 语言中的.h 文件,甚至也可以引用.dts 文件。

一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。

4.2 设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设 备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。 kernel4.14archarmbootdtsspear300.dtsi

代码语言:javascript复制
/include/ "spear3xx.dtsi"

/ {
	ahb {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "simple-bus";
		ranges = <0x60000000 0x60000000 0x50000000
			  0xd0000000 0xd0000000 0x30000000>;

		pinmux@99000000 {
			compatible = "st,spear300-pinmux";
			reg = <0x99000000 0x1000>;
		};

		clcd@60000000 {
			compatible = "arm,pl110", "arm,primecell";
			reg = <0x60000000 0x1000>;
			interrupts = <30>;
			status = "disabled";
		};

		fsmc: flash@94000000 {
			compatible = "st,spear600-fsmc-nand";
			#address-cells = <1>;
			#size-cells = <1>;
			reg = <0x94000000 0x1000	/* FSMC Register */
			       0x80000000 0x0010	/* NAND Base DATA */
			       0x80020000 0x0010	/* NAND Base ADDR */
			       0x80010000 0x0010>;	/* NAND Base CMD */
			reg-names = "fsmc_regs", "nand_data", "nand_addr", "nand_cmd";
			status = "disabled";
		};

		sdhci@70000000 {
			compatible = "st,sdhci-spear";
			reg = <0x70000000 0x100>;
			interrupts = <1>;
			status = "disabled";
		};

		shirq: interrupt-controller@0x50000000 {
			compatible = "st,spear300-shirq";
			reg = <0x50000000 0x1000>;
			interrupts = <28>;
			#interrupt-cells = <1>;
			interrupt-controller;
		};

		apb {
			#address-cells = <1>;
			#size-cells = <1>;
			compatible = "simple-bus";
			ranges = <0xa0000000 0xa0000000 0x10000000
				  0xd0000000 0xd0000000 0x30000000>;

			gpio1: gpio@a9000000 {
				#gpio-cells = <2>;
				compatible = "arm,pl061", "arm,primecell";
				gpio-controller;
				reg = <0xa9000000 0x1000>;
				interrupts = <8>;
				interrupt-parent = <&shirq>;
				status = "disabled";
			};

			kbd@a0000000 {
				compatible = "st,spear300-kbd";
				reg = <0xa0000000 0x1000>;
				interrupts = <7>;
				interrupt-parent = <&shirq>;
				status = "disabled";
			};
		};
	};
};

设备树的基本单元是节点,由根节点(/)和其子节点(name@addr)组成,子节点也可以有子节点,形成一个树状结构。 上例中:

  1. “/”是根节点,每个设备树文件只有一个根节点。
  2. ahb 是子节点,在设备树中节点命名格式如下:
代码语言:javascript复制
node-name@unit-address

“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。

另一种格式:

代码语言:javascript复制
label: node-name@unit-address

引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。

4.3 标准属性

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

  1. compatible 属性 compatible 属性也叫“兼容性”属性,可以理解为节点的兼容ID,它的值是一个字符串列表,内核通过它和驱动进行匹配。根节点的compatible更多表示该设备树所支持的平台类型。字符串列表用于选择设备所要使用的驱动程序,compatible 属性的值格式如下所示:
代码语言:javascript复制
"manufacturer,model"

manufacturer 表示厂商,model 一般是模块对应的驱动名字。

  1. model 属性 model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字。

例如:kernel4.14archarmbootdtssun8i-a33-et-q8-v1.6.dts

代码语言:javascript复制
/ {
	model = "Q8 A33 Tablet";
	compatible = "allwinner,q8-a33", "allwinner,sun8i-a33";
};

compatible = “allwinner,q8-a33”, “allwinner,sun8i-a33”;则表示当前设备树支持allwinner的q8-a33平台和sun8i-a33平台。当内核运行对应arch目录下的mach平台文件时,会匹配到这个设备树,然后进行加载。

  1. status 属性 status 属性是和设备状态有关的,status 属性值也是字符串,字符串是设备的 状态信息。
  1. #address-cells 和#size-cells 属性 这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg 属性的格式一为:
代码语言:javascript复制
reg = <address1 length1 address2 length2 address3 length3……>

每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长度,#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用的字长。

代码语言:javascript复制
fsmc: flash@94000000 {
	compatible = "st,spear600-fsmc-nand";
	#address-cells = <1>;
	#size-cells = <1>;
	reg = <0x94000000 0x1000	/* FSMC Register */
	       0x80000000 0x0010	/* NAND Base DATA */
	       0x80020000 0x0010	/* NAND Base ADDR */
	       0x80010000 0x0010>;	/* NAND Base CMD */
	reg-names = "fsmc_regs", "nand_data", "nand_addr", "nand_cmd";
	status = "disabled";
};

在fsmc中reg的第一个地址是0x94000000,它的大小是0x1000;第二个地址是0x80000000,它的大小是0x0010;第三个地址是0x80020000 ,它的大小是0x0010;第四个地址是0x80010000 ,它的大小是0x0010。代表这个设备占了四块寄存器地址空间,每块的起始地址和偏移量都在reg中列举出来了。

  1. reg 属性 reg 属性的值一般是(address,length)对。一般用于描述设备地址空间资源信息,都是某个外设的寄存器地址范围信息。
  2. ranges 属性 ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
代码语言:javascript复制
apb {
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "simple-bus";
	ranges = <0xa0000000 0xa0000000 0x10000000
		  0xd0000000 0xd0000000 0x30000000>;

	gpio1: gpio@a9000000 {
		#gpio-cells = <2>;
		compatible = "arm,pl061", "arm,primecell";
		gpio-controller;
		reg = <0xa9000000 0x1000>;
		interrupts = <8>;
		interrupt-parent = <&shirq>;
		status = "disabled";
	};

	kbd@a0000000 {
		compatible = "st,spear300-kbd";
		reg = <0xa0000000 0x1000>;
		interrupts = <7>;
		interrupt-parent = <&shirq>;
		status = "disabled";
	};
};

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

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

length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。

如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换。

  1. aliases属性 aliases的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不使用别名引用节点要使用绝对路径,类似/soc/serial0。主要供设备树外部使用,设备内部引用节点使用label即可。在节点命名的时候会加上 label,然后通过&label来访问节点,这样比较方便,而且设备树里面大量的使用&label 的形式来访问节点。
  2. chosen 属性 chosen并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,比如启动参数bootargs,不代表实际的设备。其父节点必须是根节点。一般.dts 文件中 chosen 节点通常为空或者内容很少。

如kernel4.14archarmbootdtssun8i-r16-bananapi-m2m.dts

代码语言:javascript复制
/dts-v1/;
#include "sun8i-a33.dtsi"

#include <dt-bindings/gpio/gpio.h>

/ {
	model = "BananaPi M2 Magic";
	compatible = "sinovoip,bananapi-m2m", "allwinner,sun8i-a33";

	aliases {
		i2c0 = &i2c0;
		i2c1 = &i2c1;
		i2c2 = &i2c2;
		serial0 = &uart0;
		serial1 = &uart1;
	};

	chosen {
		stdout-path = "serial0:115200n8";
	};
...
  1. memory节点 memory节点是设备树文件的必备节点,device_type属性表明该节点的类型是memory,它定义了系统物理内存的layout,即起始地址和长度。memory节点可以有多个,代表多段内存。
代码语言:javascript复制
/ {
	memory {
		reg = <0x00000000 0x20000000>;
	};
...
  1. 绑定信息文档 设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为:kernel/Documentation/devicetree/bindings。
  2. 设备树常用 OF 操作函数 设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的, 我们在编写驱动的时候需要获取到这些信息。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 kernel/include/linux/of.h 文件中。

5. DTS 编译

5.1 内核编译设备树

在kernel/arch/arm/boot/dts/目录下的Makefile中加入该dts文件的编译选项,内核目录下make dtbs就可得到对应的dtb二进制文件。

5.2 dtc 工具编译设备树

在ubuntu使用 dtc 工具编译设备树:

  1. dtc 工具安装:
代码语言:javascript复制
sudo apt-get install device-tree-compiler
  1. 编译设备树:
代码语言:javascript复制
dtc -I dts -O dtb -o xxx.dtb xxx.dts
  1. 设备树反汇编
代码语言:javascript复制
dtc -I dtb -O dts -o xxx.dts xxx.dtb

0 人点赞