掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、NXP的 I.MX8系列等。我们所使用的Linux版本为 4.1.15,其支持设备树,所以正点原子I.MX6U ALPHA 开发板的所有 Linux 驱动都是基于设备树的。
最原始的驱动框架,需要手动加载驱动,手动注册设备节点;
新的字符设备驱动主要是优化手动注册设备节点的问题,手动加载驱动后可以自动注册字符设备节点;
而设备树对驱动的影响主要是在加载驱动进行初始化时,对硬件外设的一系列配置工作。
有个专栏可以参考:Linux设备树详解(一) 基础知识_linux 设备树-CSDN博客
通俗理解,就是硬件做好了一块板子,然后就可以用设备树文件来描述这块板子上的硬件资源信息,相当于一个小数据库,然后软件去拿这些数据,就能获取对应的硬件信息了,从而不必再自己去定义这些硬件信息了。
Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如exynos4412参考板的板级设备树文件就是"arch/arm/boot/dts/exynos4412-origen.dts"。这个文件可以通过
$make dtbs
命令编译成二进制的.dtb文件供内核驱动使用。基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。
本质上,Device Tree改变了原来用code方式将HW配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核在linux kernel中,Device Tree的设计目标就是如此。
在devie tree中,可描述的信息包括:
1、CPU的数量和类别
2、内存基地址和大小
3、总线和桥
4、外设连接
5、中断控制器和中断的使用情况
6、GPIO控制器和GPIO使用情况
7、clock控制器和clock使用情况
它基本就是一棵电路板上的CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核来识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给内核,内核会将这些资源绑定给展开的相应设备
DTS基本知识
dts
硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts,一般在Linux源码中存在大量的dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个ARM的machie。
dtsi
值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,然后类似于C语言的方式可以include到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置。
dtc
dtc是编译dts的工具,可以在Ubuntu系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具;
scripts/dtc/Makefile 文件内容如下:
示例代码 43.2.1 scripts/dtc/Makefile 文件代码段 1 hostprogs-y := dtc 2 always := $(hostprogs-y) 3 4 dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \ 5 srcpos.o checks.o util.o 6 dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o ......
可以看出,DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:
make all
或者:
make dtbs
“make all”命令是编译 Linux 源码中的所有东西,包括 zImage,.ko 驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令。
dtb
dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过Bootloader引导程序加载到内核。所以Bootloader需要支持设备树才行;Kernel也需要加入设备树的支持;
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,有如下内容:
示例代码 43.2.2 arch/arm/boot/dts/Makefile 文件代码段 381 dtb-$(CONFIG_SOC_IMX6UL) += \ 382 imx6ul-14x14-ddr3-arm2.dtb \ 383 imx6ul-14x14-ddr3-arm2-emmc.dtb \ ...... 400 dtb-$(CONFIG_SOC_IMX6ULL) += \ 401 imx6ull-14x14-ddr3-arm2.dtb \ 402 imx6ull-14x14-ddr3-arm2-adc.dtb \ 403 imx6ull-14x14-ddr3-arm2-cs42888.dtb \ 404 imx6ull-14x14-ddr3-arm2-ecspi.dtb \ 405 imx6ull-14x14-ddr3-arm2-emmc.dtb \ 406 imx6ull-14x14-ddr3-arm2-epdc.dtb \ 407 imx6ull-14x14-ddr3-arm2-flexcan2.dtb \ 408 imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \ 409 imx6ull-14x14-ddr3-arm2-lcdif.dtb \ 410 imx6ull-14x14-ddr3-arm2-ldo.dtb \ 411 imx6ull-14x14-ddr3-arm2-qspi.dtb \ 412 imx6ull-14x14-ddr3-arm2-qspi-all.dtb \ 413 imx6ull-14x14-ddr3-arm2-tsc.dtb \ 414 imx6ull-14x14-ddr3-arm2-uart2.dtb \ 415 imx6ull-14x14-ddr3-arm2-usb.dtb \ 416 imx6ull-14x14-ddr3-arm2-wm8958.dtb \ 417 imx6ull-14x14-evk.dtb \ 418 imx6ull-14x14-evk-btwifi.dtb \ 419 imx6ull-14x14-evk-emmc.dtb \ 420 imx6ull-14x14-evk-gpmi-weim.dtb \ 421 imx6ull-14x14-evk-usb-certi.dtb \ 422 imx6ull-alientek-emmc.dtb \ 423 imx6ull-alientek-nand.dtb \ 424 imx6ull-9x9-evk.dtb \ 425 imx6ull-9x9-evk-btwifi.dtb \ 426 imx6ull-9x9-evk-ldo.dtb 427 dtb-$(CONFIG_SOC_IMX6SLL) += \ 428 imx6sll-lpddr2-arm2.dtb \ 429 imx6sll-lpddr3-arm2.dtb \ ......
可以看出,当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到I.MX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。示例代码 43.2.2 中第 422 和 423 行就是我们在给正点原子的 I.MX6U-ALPHA 开发板移植Linux 系统的时候添加的设备树。
DTS 语法
虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts文件上进行修改。但是 DTS 文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改.dts文件。大家不要看到要学习新的语法就觉得会很复杂,DTS 语法非常的人性化,是一种 ASCII文本文件,不管是阅读还是修改都很方便。本节我们就以 imx6ull-alientek-emmc.dts 这个文件为例来讲解一下 DTS 语法。
.dtsi 头文件
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 imx6ull-alientek
emmc.dts 中有如下所示内容:
示例代码 43.3.1.1 imx6ull-alientek-emmc.dts 文件代码段 12 #include <dt-bindings/input/input.h> 13 #include "imx6ull.dtsi"
第 12 行,使用“#include”来引用“input.h”这个.h 头文件。
第 13 行,使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。
看到这里,大家可能会疑惑,不是说设备树的扩展名是.dtsi 吗?为什么也可以直接引用 C语言中的.h 头文件呢?这里并没有错,.dts 文件引用 C 语言中的.h 文件,甚至也可以引用.dts 文件,打开 imx6ull-14x14-evk-gpmi-weim.dts 这个文件,此文件中有如下内容:
示例代码 43.3.1.2 imx6ull-14x14-evk-gpmi-weim.dts 文件代码段 #include "imx6ull-14x14-evk.dts"
可以看出,示例代码 43.3.1.2 中直接引用了.dts 文件,因此在.dts 设备树文件中,可以通过“#include”来引用.h、.dtsi 和.dts 文件。只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息的,内容如下:
示例代码 43.3.1.3 中第 54~89 行就是 cpu0 这个设备节点信息,这个节点信息描述了I.MX6ULL 这颗 SOC 所使用的 CPU 信息,比如架构是 cortex-A7,频率支持 996MHz、792MHz、528MHz、396MHz 和 198MHz 等等。在 imx6ull.dtsi 文件中不仅仅描述了 cpu0 这一个节点信息,I.MX6ULL 这颗 SOC 所有的外设都描述的清清楚楚,比如 ecspi1~4、uart1~8、usbphy1~2、i2c1~4等等,关于这些设备节点信息的具体内容我们稍后在详细的讲解。
设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下是从imx6ull.dtsi 文件中缩减出来的设备树文件内容:
第 1 行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi和 imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点的内容会合并成一个根节点。
第 2、6 和 17 行,aliases、cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
其中“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。
但是我们在示例代码 43.3.2.1 中我们看到的节点命名却如下所示:
cpu0:cpu@0
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:
label: node-name@unit-address
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “intc:interrupt-controller@00a01000”,节点 label 是 intc,而节点名字就很长了,为“interrupt:controller@00a01000”。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方便很多!
第 10 行,cpu0 也是一个节点,只是 cpu0 是 cpus 的子节点。
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
①、字符串
compatible = "arm,cortex-a7";
上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
②、32 位无符号整数
reg = <0>;
上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
③、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。
compatible 属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible 属性的值格式如下所示:
"manufacturer,model"
其中manufacturer 表示厂商,model一般是模块对应的驱动名字。比如imx6ull-alientek-emmc.dts 中sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点,I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的 WM8960,sound 节点的 compatible 属性值如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件 imx-wm8960.c 中有如下内容:
示例代码 43.3.3.1 imx-wm8960.c 文件代码段 632 static const struct of_device_id imx_wm8960_dt_ids[] = { 633 { .compatible = "fsl,imx-audio-wm8960", }, 634 { /* sentinel */ } 635 }; 636 MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids); 637 638 static struct platform_driver imx_wm8960_driver = { 639 .driver = { 640 .name = "imx-wm8960", 641 .pm = &snd_soc_pm_ops, 642 .of_match_table = imx_wm8960_dt_ids, 643 }, 644 .probe = imx_wm8960_probe, 645 .remove = imx_wm8960_remove, 646 };
第 632~635 行的数组 imx_wm8960_dt_ids 就是 imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的 compatible 属性值与此相等,那么这个节点就会使用此驱动文件。
第 642 行,wm8960 采用了 platform_driver 驱动模式,关于 platform_driver 驱动后面会讲解。此行设置.of_match_table 为 imx_wm8960_dt_ids,也就是设置这个 platform_driver 所使用的OF 匹配表。
model 属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
status 属性
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的状态信息,可选的状态如表 43.3.3.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 属性的格式为:
reg = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长度,#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用的字长,比如:
第 3,4 行,节点 spi4 的#address-cells = <1>,#size-cells = <0>,说明 spi4 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。
第 8 行,子节点 gpio_spi: gpio_spi@0 的 reg 属性值为 <0>,因为父节点设置了#address cells = <1>,#size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没有设置地址长度。
第 14,15 行,设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>,说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。
第 19 行,子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0x02280000,length= 0x4000,相当于设置了起始地址为 0x02280000,地址长度为 0x40000。
不太明白,地址和长度不都是1个字长吗?难道还有超过1个字长的地址或者长度?
参考:终于搞懂Linux 设备树中的#address-cells,#size-cells 和reg 属性-CSDN博客
再看个例子:
alphaled { #address-cells = <1>; #size-cells = <1>; compatible = "atkalpha-led"; status = "okay"; reg = < 0X020C406C 0X04 /* CCM_CCGR1_BAE */ 0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */ 0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */ 0X0209C000 0X04 /* GPIO1_DR_BASE */ 0X0209C004 0X04>; /* GPIO1_GDIR_BASE */ };
这里
address-cells = 1
,size-cells = 1
, 表示reg
属性中,地址信息的长度是1
个字长,地址长度信息也是1
个字长。可见,大部分情况下,都是1个字长。
有没有不是1个字长的?
比如:
pci@1,0 { #address-cells = <3>; #size-cells = <2>; compatible = "intel,ce4100-pci", "pci"; device_type = "pci"; bus-range = <1 1>; reg = <0x0800 0x0 0x0 0x0 0x0>; ... }
这里
address-cells = 3
,size-cells = 2
, 表示reg
属性中,地址信息的长度是3
个字长,地址长度信息是2
个字长,即是:address = 0x0800 0x0 0x0, length = 0x00 0x00,具体含义具体对待。此时,地址和长度均无法通过1个字长来描述。
reg 属性
reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在 imx6ull.dtsi 中有如下内容:
上述代码是节点 uart1,uart1 节点描述了 I.MX6ULL 的 UART1 相关信息,重点是第 326 行的 reg 属性。其中 uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、#sizecells = <1>,因此 reg 属性中 address=0x02020000,length=0x4000。查阅《I.MX6ULL 参考手册》可知,I.MX6ULL 的 UART1 寄存器首地址为 0x02020000,但是 UART1 的地址长度(范围)并没有 0x4000 这么多,这里我们重点是获取 UART1 寄存器首地址。
ranges 属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges 是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在imx6ull.dtsi中找到大量的值为空的 ranges 属性,如下所示:
第 142 行定义了 ranges 属性,但是 ranges 属性值为空。
ranges 属性不为空的示例代码如下所示:
第 5 行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
第 10 行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,
寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,
0xe0004600=0x4600+0xe0000000。
name 属性
name 属性值为字符串,name 属性用于记录节点名字,name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。
device_type 属性
device_type 属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:
关于标准属性就讲解这么多,其他的比如中断、IIC、SPI 等使用的标准属性等到具体的例程再讲解。
特殊节点
/aliases 子节点
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。例如:定义 flexcan1 和 flexcan2 的别名是 can0 和 can1。
aliases { can0 = &flexcan1; can1 = &flexcan2; };
单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
/memory 子节点
所有设备树都需要一个memory设备节点,它描述了系统的物理内存布局。如果系统有多个内存块,可以创建多个memory节点,或者可以在单个memory节点的reg属性中指定这些地址范围和内存空间大小。
例如:一个64位的系统有两块内存空间:RAM1: 起始地址是0x0,地址空间是 0x80000000;RAM2: 起始地址是0x10000000,地址空间也是0x80000000;同时根节点下的 #address-cells = <2>和#size-cells = <2>,这个memory节点描述为:
memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x80000000 0x00000000 0x10000000 0x00000000 0x80000000>; };
或者:
memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x80000000>; }; memory@10000000 { device_type = "memory"; reg = <0x00000000 0x10000000 0x00000000 0x80000000>; };
更多待补充。
/chosen 子节点
chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。例如:
chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; };
更多待补充。
Linux内核如何确认是否匹配设备?
根节点 compatible 属性
每个节点都有 compatible 属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts 文件中根节点的 compatible 属性内容如下所示:
可以看出,compatible 有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。前面我们说了,设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,那么根节点中的 compatible属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。接下来我们就来学习一下 Linux 内核在使用设备树前后是如何判断是否支持某款设备的。这里的设备说的是开发板吧?不是说外设设备。
使用设备树之前设备匹配方法
在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。Linux 内核是支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START和MACHINE_END来定义一个 machine_desc 结构体来描述这个设备,比如在文件 arch/arm/mach-imx/mach-mx35_3ds.c 中有如下定义:
上述代码就是定义了“Freescale MX35PDK”这个设备,其中 MACHINE_START和MACHINE_END 定义在文件 arch/arm/include/asm/mach/arch.h 中,内容如下:
根据 MACHINE_START 和 MACHINE_END 的宏定义,将示例代码 43.3.4.2 展开后如下所示:
从示例代码 43.3.4.3 中可以看出,这里定义了一个 machine_desc 类型的结构体变量 __mach_desc_MX35_3DS , 这 个 变 量 存 储 在 “ .arch.info.init ” 段 中 。 第 4 行 的 MACH_TYPE_MX35_3DS 就 是 “ Freescale MX35PDK ” 这 个 板 子 的 machine id 。 MACH_TYPE_MX35_3DS 定义在文件 include/generated/mach-types.h 中,此文件定义了大量的 machine id,内容如下所示:
第 287 行就是 MACH_TYPE_MX35_3DS 的值,为 1645。
前面说了,uboot 会给 Linux 内核传递 machine id 这个参数,Linux 内核会检查这个 machine id,其实就是将 machine id 与示例代码 43.3.4.3 中的这些 MACH_TYPE_XXX 宏进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动 Linux 内核。
使用设备树以后的设备匹配方法
当 Linux 内 核 引 入 设 备 树 以 后 就 不 再 使 用 MACHINE_START 了 , 而 是 换 为 了DT_MACHINE_START。
可以看出,DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同,在 DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machine id 来检查 Linux 内核是否支持某个设备了。
打开文件 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:
machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性,示例代码 43.3.4.5 中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里面有"fsl,imx6ul"和"fsl,imx6ull"这两个兼容值。只要某个设备(板子)根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。imx6ull-alientek-emmc.dts 中根节点的 compatible 属性值如下:
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
其中“fsl,imx6ull”与 imx6ul_dt_compat 中的“fsl,imx6ull”匹配,因此 I.MX6U-ALPHA 开发板可以正常启动 Linux 内核。如果将 imx6ull-alientek-emmc.dts 根节点的 compatible 属性改为其他的值,比如:
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ullll"
重新编译 DTS,并用新的 DTS 启动 Linux 内核,结果如图 43.3.4.1 所示的错误提示:
当我们修改了根节点 compatible 属性内容以后,因为 Linux 内核找不到对应的设备,因此Linux 内核无法启动。在 uboot 输出 Starting kernel…以后就再也没有其他信息输出了。
向节点追加或修改内容
产品开发过程中可能面临着频繁的需求更改,一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471 要接到 I.MX6U-ALPHA 开发板的 I2C1 接口上,那么相当于需要在 i2c1 这个节点上添加一个 fxls8471 子节点。先看一下 I2C1 接口对应的节点,打开文件 imx6ull.dtsi 文件,找到如下所示内容:
示例代码 43.3.5.1 就是 I.MX6ULL 的 I2C1 节点,现在要在 i2c1 节点下创建一个子节点,这个子节点就是 fxls8471,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点,如下所示:
第 947~950 行就是添加的 fxls8471 这个芯片对应的子节点。但是这样会有个问题!i2c1 节点是定义在 imx6ull.dtsi 文件中的,而 imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码 43.3.5.2 这样写肯定是不行的。
这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向i2c1 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 I.MX6ULL 的板子。I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alientek-emmc.dts,因此我们需要在imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:
第 1 行,&i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ull.dtsi 中的“i2c1: i2c@021a0000”。
第 2 行,花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。
打开 imx6ull-alientek-emmc.dts,找到如下所示内容:
示例代码 43.3.5.4 就是向 i2c1 节点添加/修改数据,比如第 225 行的属性“clock-frequency”就表示 i2c1 时钟为 100KHz。“clock-frequency”就是新添加的属性。
第 228 行,将 status 属性的值由原来的 disabled 改为 okay。
第 230~234 行,i2c1 子节点 mag3110,因为 NXP 官方开发板在 I2C1 上接了一个磁力计芯片 mag3110,正点原子的 I.MX6U-ALPHA 开发板并没有使用 mag3110。
第 236~242 行,i2c1 子节点 fxls8471,同样是因为 NXP 官方开发板在 I2C1 上接了 fxls8471这颗六轴芯片。
因为示例代码 43.3.5.4 中的内容是 imx6ull-alientek-emmc.dts 这个文件内的,所以不会对使用 I.MX6ULL 这颗 SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。
练习:创建小型模板设备树
上一节已经对 DTS 的语法做了比较详细的讲解,本节我们就根据前面讲解的语法,从头到 尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为 了掌握设备树的语法。在实际产品开发中,我们是不需要完完全全的重写一个.dts 设备树文件,一般都是使用 SOC 厂商提供好的.dts 文件,我们只需要在上面根据自己的实际情况做相应的修改即可。在编写设备树之前要先定义一个设备,我们就以 I.MX6ULL 这个 SOC 为例,我们需要在设备树里面描述的内容如下:
①、I.MX6ULL 这个 Cortex-A7 架构的 32 位 CPU。
②、I.MX6ULL 内部 ocram,起始地址 0x00900000,大小为 128KB(0x20000)。
③、I.MX6ULL 内部 aips1 域下的 ecspi1 外设控制器,寄存器起始地址为 0x02008000,大 小为 0x4000。
④、I.MX6ULL 内部 aips2 域下的 usbotg1 外设控制器,寄存器起始地址为 0x02184000,大 小为 0x4000。
⑤、I.MX6ULL 内部 aips3 域下的 rngb 外设控制器,寄存器起始地址为 0x02284000,大小 为 0x4000。
为了简单起见,我们就在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点“/”的基础的框架,新建一个名为 myfirst.dts 文件,在里面输入如下所示内容:
设备树框架很简单,就一个根节点“/”,根节点里面只有一个 compatible 属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。
添加 cpus 节点
首先添加 CPU 节点,I.MX6ULL 采用 Cortex-A7 架构,而且只有一个 CPU,因此只有一个 cpu0 节点,完成以后如下所示:
第 4~14 行,cpus 节点,此节点用于描述 SOC 内部的所有 CPU,因为 I.MX6ULL 只有一个 CPU,因此只有一个 cpu0 子节点。
添加 soc 节点
像 uart,iic 控制器等等这些都属于 SOC 内部外设,因此一般会创建一个叫做 soc 的父节点 来管理这些 SOC 内部外设的子节点,添加 soc 节点以后的 myfirst.dts 文件内容如下所示:
第 17~22 行,soc 节点,soc 节点设置#address-cells = <1>,#size-cells = <1>,这样 soc子节点的 reg 属性中起始地占用一个字长,地址空间长度也占用一个字长。
添加 ocram 节点
根据第②点的要求,添加 ocram 节点,ocram 是 I.MX6ULL 内部 RAM,因此 ocram 节点应 该是 soc 节点的子节点。ocram 起始地址为 0x00900000,大小为 128KB(0x20000),添加ocram节点以后 myfirst.dts 文件内容如下所示:
第 24~27 行,ocram 节点,第 24 行节点名字@后面的 0x00900000 就是 ocram 的起始地址。
第 26 行的 reg 属性也指明了 ocram 内存的起始地址为 0x00900000,大小为 0x20000。
添加 aips1、aips2 和 aips3 这三个子节点
I.MX6ULL 内部分为三个域:aips1~3,这三个域分管不同的外设控制器,aips1~3 这三个域 对应的内存范围如表 43.4.1 所示:
我们先在设备树中添加这三个域对应的子节点。aips1~3 这三个域都属于 soc 节点的子节点,完成以后的 myfirst.dts 文件内容如下所示:
第 30~36 行,aips1 节点。
第 39~45 行,aips2 节点。
第 48~54 行,aips3 节点。
添加 ecspi1、usbotg1 和 rngb 这三个外设控制器节点
最后我们在 myfirst.dts 文件中加入 ecspi1,usbotg1 和 rngb 这三个外设控制器对应的节点,其中 ecspi1 属于 aips1 的子节点,usbotg1 属于 aips2 的子节点,rngb 属于 aips3 的子节点。最终的 myfirst.dts 文件内容如下:
第 38~44 行,ecspi1 外设控制器节点。
第 56~60 行,usbotg1 外设控制器节点。
第 72~75 行,rngb 外设控制器节点。
至此,myfirst.dts 这个小型的模板设备树就编写好了,基本和 imx6ull.dtsi 很像,可以看做是 imx6ull.dtsi 的缩小版。在 myfirst.dts 里面我们仅仅是编写了 I.MX6ULL 的外设控制器节点, 像 IIC 接口,SPI 接口下所连接的具体设备我们并没有写,因为具体的设备其设备树属性内容不同,这个等到具体的实验在详细讲解。
设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device
tree 目录下根据节点名字创建不同文件夹,如图 43.5.1 所示:
图 43.5.1 就是目录/proc/device-tree 目录下的内容,/proc/device-tree 目录下是根节点“/”的
所有属性和子节点,我们依次来看一下这些属性和子节点。
根节点“/”各个属性
在图 43.5.1 中,根节点属性属性表现为一个个的文件(图中细字体文件),比如图 43.5.1 中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设备树中就是根节点的 5个属性。既然是文件那么肯定可以查看其内容,输入 cat 命令来查看model和 compatible 这两个文件的内容,结果如图 43.5.2 所示:
从图 43.5.2 可以看出,文件 model 的内容是“Freescale i.MX6 ULL 14x14 EVK Board”,文 件compatible 的内容为“fsl,imx6ull-14x14-evkfsl,imx6ull”。打开文件 imx6ull-alientek-emmc.dts查看一下,这不正是根节点“/”的 model 和 compatible 属性值嘛!
根节点“/”各子节点
图 43.5.1 中各个文件夹(图中粗字体文件夹)就是根节点“/”的各个子节点,比如“aliases”、 “backlight”、“chosen”和“clocks”等等。大家可以查看一下 imx6ull-alientek-emmc.dts 和 imx6ull.dtsi 这两个文件,看看根节点的子节点都有哪些,看看是否和图 43.5.1 中的一致。
/proc/device-tree 目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进 入/proc/device-tree/soc 目录中就可以看到 soc 节点的所有子节点,如图 43.5.3 所示:
和根节点“/”一样,图 43.5.3 中的所有文件分别为 soc 节点的属性文件和子节点文件夹。
大家可以自行查看一下这些属性文件的内容是否和 imx6ull.dtsi 中 soc 节点的属性值相同,也可以进入“busfreq”这样的文件夹里面查看 soc 节点的子节点信息。
Linux 内核解析 DTB 文件Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备 树节点文件。接下来我们简单分析一下 Linux 内核是如何解析 DTB 文件的,流程如图 43.7.1 所示:
从图 43.7.1 中可以看出,在 start_kernel 函数中完成了设备树节点解析的工作,最终实际工
作的函数为 unflatten_dt_node。
设备树常用 OF 操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存 器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。
查找节点的 OF 函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定 义在文件 include/linux/of.h 中,定义如下:
与查找节点有关的 OF 函数有 5 个,我们依次来看一下。
1、of_find_node_by_name 函数
of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值:找到的节点,如果为 NULL 表示查找失败。
2、of_find_node_by_type 函数
of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值:找到的节点,如果为 NULL 表示查找失败。
3、of_find_compatible_node 函数
of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示 忽略掉 device_type 属性。
compatible:要查找的节点所对应的 compatible 属性列表。
返回值:找到的节点,如果为 NULL 表示查找失败
4、of_find_matching_node_and_match 函数
of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点,函数原型如下:
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)
函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
match:找到的匹配的 of_device_id。
返回值:找到的节点,如果为 NULL 表示查找失败
5、of_find_node_by_path 函数
of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
返回值:找到的节点,如果为 NULL 表示查找失败
查找父/子节点的 OF 函数
Linux 内核提供了几个查找节点对应的父节点或子节点的 OF 函数,我们依次来看一下。
1、of_get_parent 函数
of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
函数参数和返回值含义如下:
node:要查找的父节点的节点。
返回值:找到的父节点。
2、of_get_next_child 函数
of_get_next_child 函数用迭代的方式查找子节点,函数原型如下:
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
函数参数和返回值含义如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为
NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。
提取属性值的 OF 函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内 核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,内容如下:
1、of_find_property 函数
of_find_property 函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np, const char *name, int *lenp)
函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。
2、of_property_count_elems_of_size 函数
of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个
数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size)
函数参数和返回值含义如下:
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值:得到的属性元素数量。
3、of_property_read_u32_index 函数
of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32
位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:
int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有
要读取的数据,-EOVERFLOW 表示属性值列表太小。
4、
of_property_read_u8_array 函数
of_property_read_u16_array 函数
of_property_read_u32_array 函数
of_property_read_u64_array 函数
这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属
性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型如下:
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
sz:要读取的数组元素数量。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没
有要读取的数据,-EOVERFLOW 表示属性值列表太小。
5、
of_property_read_u8 函数
of_property_read_u16 函数
of_property_read_u32 函数
of_property_read_u64 函数
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用
于读取 u8、u16、u32 和 u64 类型属性值,函数原型如下:
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没
有要读取的数据,-EOVERFLOW 表示属性值列表太小。
6、of_property_read_string 函数
of_property_read_string 函数用于读取属性中字符串值,函数原型如下:
int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值:0,读取成功,负值,读取失败。
7、of_n_addr_cells 函数
of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#address-cells 属性值。
8、of_n_size_cells 函数
of_size_cells 函数用于获取#size-cells 属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells 属性值。
其他常用的 OF 函数
1、of_device_is_compatible 函数
of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字
符串,也就是检查设备节点的兼容性,函数原型如下:
int of_device_is_compatible(const struct device_node *device, const char *compat)
函数参数和返回值含义如下:
device:设备节点。
compat:要查看的字符串。
返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible
属性中包含 compat 指定的字符串。
2、of_get_address 函数
of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性
值,函数原型如下:
const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
函数参数和返回值含义如下:
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。
3、of_translate_address 函数
of_translate_address 函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
4、of_address_to_resource 函数
IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux
内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource结构体描述的都是设备资源信息,resource 结构体定义在文件 include/linux/ioport.h 中,定义如下:
对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,可选的资源标志定义在文件 include/linux/ioport.h 中,如下所示:
大 家 一 般 最 常 见 的 资 源 标 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和
IORESOURCE_IRQ 等。接下来我们回到 of_address_to_resource 函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型,函数原型如下所示
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
函数参数和返回值含义如下:
dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值
返回值:0,成功;负值,失败。5、of_iomap 函数
of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地
址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:
void __iomem *of_iomap(struct device_node *np, int index)
函数参数和返回值含义如下:
np:设备节点。
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。
关于设备树常用的 OF 函数就先讲解到这里,Linux 内核中关于设备树的 OF 函数不仅仅只 有前面讲的这几个,还有很多 OF 函数我们并没有讲解,这些没有讲解的 OF 函数要结合具体的驱动,比如获取中断号的 OF 函数、获取 GPIO 的 OF 函数等等,这些 OF 函数我们在后面的驱动实验中再详细的讲解。
实战:设备树下的 LED 驱动实验
上一章我们详细的讲解了设备树语法以及在驱动开发中常用的 OF 函数,本章我们就开始第一个基于设备树的 Linux 驱动实验。
本章实验重点内容如下:
①、在 imx6ull-alientek-emmc.dts 文件中创建相应的设备节点。
②、编写驱动程序(在第四十二章实验基础上完成),获取设备树中的相关属性值。
③、使用获取到的有关属性值来初始化 LED 所使用的 GPIO。
修改设备树文件
在根节点“/”下创建一个名为“alphaled”的子节点,打开 imx6ull-alientek-emmc.dts 文件,在根节点“/”最后面输入如下所示内容:
第 2、3 行,属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长(cell),地址长度也占用一个字长(cell)。
第 4 行,属性 compatbile 设置 alphaled 节点兼容性为“atkalpha-led”。
第 5 行,属性 status 设置状态为“okay”。
第 6~10 行,reg 属性,非常重要!reg 属性设置了驱动里面所要使用的寄存器物理地址,比 如第 6 行的“0X020C406C 0X04”表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地 址为 0X020C406C,长度为 4 个字节。
设备树修改完成以后输入如下命令重新编译一下 imx6ull-alientek-emmc.dts:
make dtbs
编译完成以后得到 imx6ull-alientek-emmc.dtb,使用新的 imx6ull-alientek-emmc.dtb 启动 Linux 内核。Linux 启动成功以后进入到/proc/device-tree/目录中查看是否有“alphaled”这个节 点,结果如图 44.3.1.1 所示:
如果没有“alphaled”节点的话请重点查看下面两点:
①、检查设备树修改是否成功,也就是 alphaled 节点是否为根节点“/”的子节点。
②、检查是否使用新的设备树启动的 Linux 内核。
可以进入到图 44.3.1 中的 alphaled 目录中,查看一下都有哪些属性文件,结果如图 44.3.1.2 所示:
大家可以查看一下 compatible 、 status 等属性值是否和我们设置的一致。设备树准备好以后就可以编写驱动程序了
工程创建好以后新建 dtsled.c 文件,在 dtsled.c 里面输入如下内容:
示例代码 44.3.2.1 dtsled.c 文件内容 1 #include <linux/types.h> 2 #include <linux/kernel.h> 3 #include <linux/delay.h> 4 #include <linux/ide.h> 5 #include <linux/init.h> 6 #include <linux/module.h> 7 #include <linux/errno.h> 8 #include <linux/gpio.h> 9 #include <linux/cdev.h> 10 #include <linux/device.h> 11 #include <linux/of.h> 12 #include <linux/of_address.h> 13 #include <asm/mach/map.h> 14 #include <asm/uaccess.h> 15 #include <asm/io.h> 16 /*************************************************************** 17 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 18 文件名 : dtsled.c 19 作者 : 左忠凯 20 版本 : V1.0 21 描述 : LED 驱动文件。 22 其他 : 无 23 论坛 : www.openedv.com 24 日志 : 初版 V1.0 2019/7/9 左忠凯创建 25 ***************************************************************/ 26 #define DTSLED_CNT 1 /* 设备号个数 */ 27 #define DTSLED_NAME "dtsled" /* 名字 */ 28 #define LEDOFF 0 /* 关灯 */ 29 #define LEDON 1 /* 开灯 */ 30 31 /* 映射后的寄存器虚拟地址指针 */ 32 static void __iomem *IMX6U_CCM_CCGR1; 33 static void __iomem *SW_MUX_GPIO1_IO03; 34 static void __iomem *SW_PAD_GPIO1_IO03; 35 static void __iomem *GPIO1_DR; 36 static void __iomem *GPIO1_GDIR; 37 38 /* dtsled 设备结构体 */ 39 struct dtsled_dev{ 40 dev_t devid; /* 设备号 */ 41 struct cdev cdev; /* cdev */ 42 struct class *class; /* 类 */ 43 struct device *device; /* 设备 */ 44 int major; /* 主设备号 */ 45 int minor; /* 次设备号 */ 46 struct device_node *nd; /* 设备节点 */ 47 }; 48 49 struct dtsled_dev dtsled; /* led 设备 */ 50 51 /* 52 * @description : LED 打开/关闭 53 * @param - sta : LEDON(0) 打开 LED,LEDOFF(1) 关闭 LED 54 * @return : 无 55 */ 56 void led_switch(u8 sta) 57 { 58 u32 val = 0; 59 if(sta == LEDON) { 60 val = readl(GPIO1_DR); 61 val &= ~(1 << 3); 62 writel(val, GPIO1_DR); 63 }else if(sta == LEDOFF) { 64 val = readl(GPIO1_DR); 65 val|= (1 << 3); 66 writel(val, GPIO1_DR); 67 } 68 } 69 70 /* 71 * @description : 打开设备 72 * @param – inode : 传递给驱动的 inode 73 * @param – filp : 设备文件,file 结构体有个叫做 private_data 的成员变量 74 * 一般在 open 的时候将 private_data 指向设备结构体。 75 * @return : 0 成功;其他 失败 76 */ 77 static int led_open(struct inode *inode, struct file *filp) 78 { 79 filp->private_data = &dtsled; /* 设置私有数据 */ 80 return 0; 81 } 82 83 /* 84 * @description : 从设备读取数据 85 * @param – filp : 要打开的设备文件(文件描述符) 86 * @param - buf : 返回给用户空间的数据缓冲区 87 * @param - cnt : 要读取的数据长度 88 * @param – offt : 相对于文件首地址的偏移 89 * @return : 读取的字节数,如果为负值,表示读取失败 90 */ 91 static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) 92 { 93 return 0; 94 } 95 96 /* 97 * @description : 向设备写数据 98 * @param - filp : 设备文件,表示打开的文件描述符 99 * @param - buf : 要写给设备写入的数据 100 * @param - cnt : 要写入的数据长度 101 * @param – offt : 相对于文件首地址的偏移 102 * @return : 写入的字节数,如果为负值,表示写入失败 103 */ 104 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 105 { 106 int retvalue; 107 unsigned char databuf[1]; 108 unsigned char ledstat; 109 110 retvalue = copy_from_user(databuf, buf, cnt); 111 if(retvalue < 0) { 112 printk("kernel write failed!\r\n"); 113 return -EFAULT; 114 } 115 116 ledstat = databuf[0]; /* 获取状态值 */ 117 118 if(ledstat == LEDON) { 119 led_switch(LEDON); /* 打开 LED 灯 */ 120 } else if(ledstat == LEDOFF) { 121 led_switch(LEDOFF); /* 关闭 LED 灯 */ 122 } 123 return 0; 124 } 125 126 /* 127 * @description : 关闭/释放设备 128 * @param – filp : 要关闭的设备文件(文件描述符) 129 * @return : 0 成功;其他 失败 130 */ 131 static int led_release(struct inode *inode, struct file *filp) 132 { 133 return 0; 134 } 135 136 /* 设备操作函数 */ 137 static struct file_operations dtsled_fops = { 138 .owner = THIS_MODULE, 139 .open = led_open, 140 .read = led_read, 141 .write = led_write, 142 .release = led_release, 143 }; 144 145 /* 146 * @description : 驱动入口函数 147 * @param : 无 148 * @return : 无 149 */ 150 static int __init led_init(void) 151 { 152 u32 val = 0; 153 int ret; 154 u32 regdata[14]; 155 const char *str; 156 struct property *proper; 157 158 /* 获取设备树中的属性数据 */ 159 /* 1、获取设备节点:alphaled */ 160 dtsled.nd = of_find_node_by_path("/alphaled"); 161 if(dtsled.nd == NULL) { 162 printk("alphaled node can not found!\r\n"); 163 return -EINVAL; 164 } else { 165 printk("alphaled node has been found!\r\n"); 166 } 167 168 /* 2、获取 compatible 属性内容 */ 169 proper = of_find_property(dtsled.nd, "compatible", NULL); 170 if(proper == NULL) { 171 printk("compatible property find failed\r\n"); 172 } else { 173 printk("compatible = %s\r\n", (char*)proper->value); 174 } 175 176 /* 3、获取 status 属性内容 */ 177 ret = of_property_read_string(dtsled.nd, "status", &str); 178 if(ret < 0){ 179 printk("status read failed!\r\n"); 180 } else { 181 printk("status = %s\r\n",str); 182 } 183 184 /* 4、获取 reg 属性内容 */ 185 ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10); 186 if(ret < 0) { 187 printk("reg property read failed!\r\n"); 188 } else { 189 u8 i = 0; 190 printk("reg data:\r\n"); 191 for(i = 0; i < 10; i++) 192 printk("%#X ", regdata[i]); 193 printk("\r\n"); 194 } 195 196 /* 初始化 LED */ 197 #if 0 198 /* 1、寄存器地址映射 */ 199 IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]); 200 SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]); 201 SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]); 202 GPIO1_DR = ioremap(regdata[6], regdata[7]); 203 GPIO1_GDIR = ioremap(regdata[8], regdata[9]); 204 #else 205 IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); 206 SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1); 207 SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2); 208 GPIO1_DR = of_iomap(dtsled.nd, 3); 209 GPIO1_GDIR = of_iomap(dtsled.nd, 4); 210 #endif 211 212 /* 2、使能 GPIO1 时钟 */ 213 val = readl(IMX6U_CCM_CCGR1); 214 val &= ~(3 << 26); /* 清楚以前的设置 */ 215 val |= (3 << 26); /* 设置新值 */ 216 writel(val, IMX6U_CCM_CCGR1); 217 218 /* 3、设置 GPIO1_IO03 的复用功能,将其复用为 219 * GPIO1_IO03,最后设置 IO 属性。 220 */ 221 writel(5, SW_MUX_GPIO1_IO03); 222 223 /* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */ 224 writel(0x10B0, SW_PAD_GPIO1_IO03); 225 226 /* 4、设置 GPIO1_IO03 为输出功能 */ 227 val = readl(GPIO1_GDIR); 228 val &= ~(1 << 3); /* 清除以前的设置 */ 229 val |= (1 << 3); /* 设置为输出 */ 230 writel(val, GPIO1_GDIR); 231 232 /* 5、默认关闭 LED */ 233 val = readl(GPIO1_DR); 234 val |= (1 << 3); 235 writel(val, GPIO1_DR); 236 237 /* 注册字符设备驱动 */ 238 /* 1、创建设备号 */ 239 if (dtsled.major) { /* 定义了设备号 */ 240 dtsled.devid = MKDEV(dtsled.major, 0); 241 register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME); 242 } else { /* 没有定义设备号 */ 243 alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号*/ 244 dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */ 245 dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */ 246 } 247 printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor); 248 249 /* 2、初始化 cdev */ 250 dtsled.cdev.owner = THIS_MODULE; 251 cdev_init(&dtsled.cdev, &dtsled_fops); 252 253 /* 3、添加一个 cdev */ 254 cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT); 255 256 /* 4、创建类 */ 257 dtsled.class = class_create(THIS_MODULE, DTSLED_NAME); 258 if (IS_ERR(dtsled.class)) { 259 return PTR_ERR(dtsled.class); 260 } 261 262 /* 5、创建设备 */ 263 dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME); 264 if (IS_ERR(dtsled.device)) { 265 return PTR_ERR(dtsled.device); 266 } 267 268 return 0; 269 } 270 271 /* 272 * @description : 驱动出口函数 273 * @param : 无 274 * @return : 无 275 */ 276 static void __exit led_exit(void) 277 { 278 /* 取消映射 */ 279 iounmap(IMX6U_CCM_CCGR1); 280 iounmap(SW_MUX_GPIO1_IO03); 281 iounmap(SW_PAD_GPIO1_IO03); 282 iounmap(GPIO1_DR); 283 iounmap(GPIO1_GDIR); 284 285 /* 注销字符设备驱动 */ 286 cdev_del(&dtsled.cdev);/* 删除 cdev */ 287 unregister_chrdev_region(dtsled.devid, DTSLED_CNT);/*注销设备号*/ 288 289 device_destroy(dtsled.class, dtsled.devid); 290 class_destroy(dtsled.class); 291 } 292 293 module_init(led_init); 294 module_exit(led_exit); 295 MODULE_LICENSE("GPL"); 296 MODULE_AUTHOR("zuozhongkai");
dtsled.c 文件中的内容和第四十二章的 newchrled.c 文件中的内容基本一样,只是 dtsled.c 中
包含了处理设备树的代码,我们重点来看一下这部分代码。
第 46 行,在设备结构体 dtsled_dev 中添加了成员变量 nd,nd 是 device_node 结构体类型指
针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般
在设备结构体中添加 device_node 指针变量来存放这个节点。
第 160~166 行,通过 of_find_node_by_path 函数得到 alphaled 节点,后续其他的 OF 函数要
使用 device_node。
第 169~174 行,通过 of_find_property 函数获取 alphaled 节点的 compatible 属性,返回值为
property 结构体类型指针变量,property 的成员变量 value 表示属性值。
第 177~182 行,通过 of_property_read_string 函数获取 alphaled 节点的 status 属性值。
第 185~194 行,通过 of_property_read_u32_array 函数获取 alphaled 节点的 reg 属性所有值,
并且将获取到的值都存放到 regdata 数组中。第 192 行将获取到的 reg 属性值依次输出到终端
上。
第 199~203 行,使用“古老”的 ioremap 函数完成内存映射,将获取到的 regdata 数组中的
寄存器物理地址转换为虚拟地址。
第 205~209 行,使用 of_iomap 函数一次性完成读取 reg 属性以及内存映射,of_iomap 函数
是设备树推荐使用的 OF 函数。
后记:
我之前还以为设备树会让设备用另外一种方式来初始化,其实该怎么初始化还是怎么初始化,只不过,初始化时所需要的那些数据,我们可以通过设备树来获取。
于是,我不禁想,这和我自己定义一个头文件,然后把设备所需的信息都定义进去,有啥区别,感觉头文件定义还直接方便一些。
也许更通用吧。