linux设备树基础知识:
-
设备树的应用范围?
在新版本的linux中。ARM相关的驱动全部采用了设备树,有设备树存在的linux版本,基本上改开发板上的所有linux驱动都会基于设备树开发 -
什么是设备树?
设备树的主干就是系统总线
设备树的主干下有大树枝,大树枝包括I2C控制器,GPIO控制器,SPI控制器等
设备树的大树枝下有小树枝,小树枝包括,I2C1,I2C2等(例如,I2C控制器分为,I2C1,I2C2)
设备树的小树枝下要树叶,树叶包括,AT24C02设备(AT24C02是I2C从设备) -
描述设备树的文件是什么?
描述设备树的文件叫DTS文件(device tree source),就是我们通常说的设备树,我们说的修改设备树,其实就是来修改该文件
该DTS文件采用树形结构描述板级设备,***** 非常关键: 系统最终会根据DTS文件中的节点和属性来生成对应的文件节点,这样最终文件结构就是设备树中描述的树形结构 -
在以前没有通过设备树来描述ARM架构中的板级信息,那么是如何实现的?
使用arch.arm/mach-xxx,arch/arm/plat-xxx文件夹下的文件来描述对应平台下的板级信息
这种方式会造成在linux内核中添加了大量无用,冗余的板级信息文件
***** 关键点:
后来ARM社区引入了PowerPC等架构中的设备树方法,将这些描述板级硬件信息的内容全部从linux中分离出来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名.dts -
设备树文件的组成
一个SOC(片上芯片),可以做出好多开发板,不同开发板之间会存在共同相同和不同部分,将一个SOC中的共同部分信息提取出来做成一个通用文件,其他文件直接引用该通用文件即可,
然后每个开发板将自己的差异化信息写在一个定制文件中,这样就包含了该开发板上的所有设备的硬件信息
一个开发板的设备树 = 该开发板所在SOC的通用文件 + 对应自己的定制文件 例如 imx6ul.dtsi + imx6ull-alitek-emmc.dts
通用文件: .dtsi后缀,描述SOC级信息,(想想也是,这里是存放一个SOC中设备硬件信息的文件),它的作用就相当于该SOC对应的的头文件,
当我们要修改设备树文件时,最后修改自己的板级文件(imx6ull-alitek-emmc.dts),而不是去修改soc设备树文件(imx6ul.dtsi),因为你不能保证你修改的东西在整个soc中通用,你只能保证该修改对你的开发板有用
定制文件: 描述自己把这个开发板上特有的硬件设备信息 -
DTS,DTB,DTC的关系
DTS: 设备树源文件, 类似于linux中的 .c文件
DTB: 二进制文件 类似于linux中的 可执行文件 .elf
DTC: 编译DTS文件的工具 类似于linux中的 gcc编译工具
通过DTC编译工具将DTS设备树源文件编译生成设备树的二进制可执行文件如何添加一个新开发板的设备树?
- 首先确定该开发板属于那个SOC,然后找到该SOC的.dtsi文件
- 创建一个定制文件.dts,先包含.dtsi文件,然后添加新开发板上的新设备硬件信息
- 在arch/arm/boot/dts/Makefile中的所属SOC下,添加生成同名的.dtb文件的语句,这样在编译生成设备树文件时会生成该新开发板对应的.dtb文件
-
DTS语法
1> 在dts设备树文件中,可以通过#include 来引用.h,.dtsi,和.dts文件,(最后将会被别人引用的设备树文件定义成.dtsi文件)
2> 语法详解查看文件 myfirst.dts.txt文件中的注释
3> 框架
#include 该开发板所在SOC下的通用文件
/{
节点1{
属性键值对子节点{ 属性键值对 } }; }
-
设备树文件中的根节点
每个设备树文件只有一个根节点
当a.dts引b.dtsi时,这两个文件中的根节点会合并成一个根节点,因为存在这个机制,所以设备树文件 = 定制文件 + 头文件 ,最终形成应该完整的设备树
关键点: *****
节点是由一堆的属性组成的,节点就是具体的设备,不同的设备需要的属性不同
属性都是键值对,值可以为空或任意的字节流 标准属性 看文档吧 -
当开发板上新增了一个设备,该怎么处理?
需要将新设备的硬件信息同步修改到该开发板的设备树文件
例如,新增了一个fxls8471设备到开发板上的I2C1节点下
1. 首先确定该设备是不是SOC共同设备,如果不是就不能加该设备硬件信息添加到SOC的通用文件.dtsi中
2. 找到该开发板对应的设备树文件.dts,然后在I2C1节点下在创建一个对应fxls8471设备的子节点 -
编译DTS文件的两种方法
首先都需要进入到linux内核源码的根目录下
方法1: make all 分析Makefile文件会发现,这个命令是编译linux源码中所有的东西,包括zImage,.ko驱动模块以及设备树
方法2: make dtbs 这个命令只是编译设备树文件
流程: 在linux内核源码的根目录下 输入make dtbs
因为在顶层Makefile中设置的系统框架是arm 故会加载编译arch/arm/Makefile文件
在arch/arm/Makefile文件中有如下代码
PHONY += dtbs dtbs_installdtbs: prepare scripts $(Q)$(MAKE) $(build)=$(boot)/dts ==> 静默编译arch/arm/boot/dts下的makefile文件 在arch/arm/boot/dts下的makefile文件中有如下定义(还有许多) dtb-$(CONFIG_ARCH_ALPINE) += \ alpine-db.dtb ==> 如果宏定义CONFIG_ARCH_ALPINE存在,而且在arch/arm/boot/dts目录下存在alpine-db.dts文件就编译alpine-db.dts文件生成alpine-db.dtb文件
-
设备树在系统中的体现
前面一直说,通过设备树定义设备节点,最终在文件系统中,设备关系也会成为树形结构
/proc/device-tree 就是设备树中的根节点"/"的所有属性和子节点信息 ==> 你在设备树的根节点下定义了什么字节点,这里就会有该子节点对应的同名文件,属性也是一样
==> 传说中的一切皆文件的一种具体表现 ,这些文件中记录的信息就是我们在设备树中定义的信息 -
特殊节点
在根节点"/“中有两个特殊的子节点: aliases和chosen
aliases子节点 aliases的含义就是"别名”
通常定义形式如下
aliases {
can0 = &flexcan1; ==> 这里就是将 &flexcan1节点的别名设置为can0
… …
}
但是实际引用节点时,我们不怎么会使用别名,而是直接使用&label的形式(&flexcan1)来引用该节点chosen子节点
chosen子节点并不是一个真实的设备,该节点的主要目的是为了实现uboot传递数据到linux内核,前面说了 uboot会将环境变量bootargs中的值作为参数传递给linux内核
在我们imx6ill-alientek-emmc.dts文件中定义的chosen节点如下
chosen {
stdout-path = &uart1
}我们在文件系统中的 /proc/device-tree/chosen中发现该目录下有以下文件
bootargs name stdout-path
cat bootargs ==>发现bootargs文件的内容就是我们在uboot中设置的bootargs环境变量的值
问题如下,问题1: 我们在设备树中的chosen中没有定义bootargs属性,但是现实的文件系统中却存在bootargs文件
问题2: bootargs文件中的内容为什么和我们在uboot中设置的bootargs环境变量的值一致
得出结论,bootargs文件中的内容和uboot有关,而且不是在linux内核中实现bootargs文件的,而是uboot过程中想chosen节点中添加了bootargs属性
uboot知道bootargs环境变量的值和设备树执行文件.dtb的位置,所以就有可能是uboot将bootargs环境变量作为chosen节点的属性添加到设备树中代码实现
在uboot源码中全局搜索"chosen"字符串,来寻找相关操作
~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “chosen” ./
结果:
./common/fdt_support.c:227: nodeoffset = fdt_find_or_add_subnode(fdt, 0, “chosen”); //fdt来发现or新增chosen子节点 差不多就是这里了
int fdt_chosen(void fdt)
{
… … …
/ find or create “/chosen” node. /
nodeoffset = fdt_find_or_add_subnode(fdt, 0, “chosen”);
|
/*
* fdt_find_or_add_subnode() - find or possibly add a subnode of a given node 查找或可能添加给定节点的子节点
*
* @fdt: pointer to the device tree blob 指向设备树blob的指针
* @parentoffset: structure block offset of a node 节点的结构块偏移量
* @name: name of the subnode to locate 要定位的子节点的名称
*
* fdt_subnode_offset() finds a subnode of the node with a given name.
* If the subnode does not exist, it will be created.
*/str = getenv("bootargs"); //获取环境变量bootargs的值 if (str) { //给设备树fdt下的chosen节点(nodeoffset)添加属性名称为 bootargs 的属性,属性值为booatargs环境变量的值(str) err = fdt_setprop(fdt, nodeoffset, "bootargs", str, strlen(str) + 1); ... ... } return fdt_fixup_stdout(fdt, nodeoffset);
}
开始方向追踪
(1) ./common/fdt_support下的fdt_chosen()~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “fdt_chosen” ./
==>
./common/image-fdt.c:507: if (fdt_chosen(blob) < 0) {
(2) ./common/image-fdt.c下的image_setup_libfdt()
int image_setup_libfdt(bootm_headers_t *images, void *blob,int of_size, struct lmb *lmb)
{
if (fdt_chosen(blob) < 0) {
printf(“ERROR: /chosen node create failed\n”);
goto err;
}
… …
}~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “image_setup_libfdt” ./
==>
./common/image.c:1376: ret = image_setup_libfdt(images, *of_flat_tree, of_size, lmb);
int image_setup_linux(bootm_headers_t *images)
{... ... if (IMAGE_ENABLE_OF_LIBFDT && of_size) { ret = image_setup_libfdt(images, *of_flat_tree, of_size, lmb); if (ret) return ret; } return 0;
}
(3) ./common/image.c下的image_setup_linux()~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “image_setup_linux” ./
==>
./arch/arm/lib/bootm.c:211: if (image_setup_linux(images)) {
static void boot_prep_linux(bootm_headers_t *images)
{
char *commandline = getenv(“bootargs”);if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len) {
#ifdef CONFIG_OF_LIBFDT
debug(“using: FDT\n”);
if (image_setup_linux(images)) {
printf(“FDT creation failed! hanging…”);
hang();
}
#endif
… …
}
(4) ./arch/arm/lib/bootm.c 下的 boot_prep_linux()~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “boot_prep_linux” ./
==>
./arch/mips/lib/bootm.c:338: boot_prep_linux(images);
int do_bootm_linux(int flag, int argc, char * const argv[],bootm_headers_t *images)
{if (flag & BOOTM_STATE_OS_PREP) { boot_prep_linux(images); return 0; } ... ...
}
(5) ./arch/arm/lib/bootm.c 下的 do_bootm_linux() //到这里这个函数是不是就熟悉了? 前面uboot分析时,说过uboot的流程~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “do_bootm_linux” ./
==>
./common/bootm_os.c:438: [IH_OS_LINUX] = do_bootm_linux,
static boot_os_fn *boot_os[] = { //不同的os对应不同的操作函数
#ifdef CONFIG_BOOTM_LINUX
[IH_OS_LINUX] = do_bootm_linux,
#endif
… …
};
int boot_selected_os(int argc, char * const argv[], int state, bootm_headers_t *images, boot_os_fn *boot_fn)
{
arch_preboot_os();
boot_fn(state, argc, argv, images); ==> do_bootm_linux(state, argc, argv, images);... ...
}
(6) ./common/bootm_os.c 下的 boot_selected_os()~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “boot_selected_os” ./
==>
./common/bootm.c:684: ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],int states, bootm_headers_t images, int boot_progress)
{
/ Now run the OS! We hope this doesn’t return */
if (!ret && (states & BOOTM_STATE_OS_GO))
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
images, boot_fn);... ... ...
}
(7) ./common/bootm_os.c 下的 do_bootm_states()~/linux/IMX6ULL/uboot/uboot-imx-rel_imx_4.1.15_2.1.0_ga_alientek$ grep -rn “do_bootm_states” ./
==>
./cmd/bootm.c:639: ret = do_bootm_states(cmdtp, flag, argc, argv,int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
… …
ret = do_bootm_states(cmdtp, flag, argc, argv,
BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
BOOTM_STATE_OS_GO,
&images, 1);return ret;
}
(8) ./cmd/bootm.c:639 下的 do_bootz()==>跟到这里 差不多了 ,因为是bootz命令才会调用do_bootz函数 所以 实际上是在bootz命令时实现的将bootargs环境变量作为chosen节点的属性
-
绑定信息文档 -->设备树操作的参考书
在Linux内核源码中有详细的.txt文档来描述如何添加节点,这些.txt文档称为绑定文档 路径为 /Documentation/devicetree/bindings -
OF操作函数
因为设备树最终是被驱动文件所使用的,而驱动文件在实现驱动的时候必须要获取设备的信息,设备的信息是定义在设备树中的
故OF操作函数是驱动文件获取设备树信息的api接口
demo:
/*
练习.dts文件的编写
*/
#include “imx6ull.dtsi” //引用该开发板对应的SOC的通用文件
/ {
//定义该.dts文件的根节点
/*
compatible:标准属性(“兼容性"属性) ,该属性的值可以上为一个字符串列表
在这里,他是不仅仅是兼容性属性更是根节点的兼容性属性
普通节点的兼容性属性的作用是: 为该设备节点匹配对应的驱动程序
根节点的兼容性属性的作用是: 作为Linux内核来判断是否支持该设备,如果支持就启动内核,不然就无法启动内核
这里定义的根节点 compatible属性值为一个字符串列表,通常第一个字符串值表示该硬件设备的名称"fsl,imx6ull-alientek-evk”
第二个字符串值表示该硬件设备所属的SOC名称"fsl,imx6ull"
********** 对于普通compatible属性值来说形式为: “该设备的开发厂商,该设备要匹配的驱动程序名称”
例如"fsl,imx6ull-alientek-evk",
*/
compatible = "fsl,imx6ull-alientek-evk","fsl,imx6ull";
cpus { //定义了根节点下的子节点 cpus (相当于设备树中的大树枝)
/*
标准属性#address-cells,#size-cells: 这两个属性的值都为u32,可以用在任何拥有子节点的设备中(关键点****,说明它只能作为,树枝/树干的属性不能作为树叶的属性)
#address-cell属性值决定了子节点中reg属性中起始地址信息所占用的字长(看清楚单位是字长,1字长 = 4byte)
#size-cells属性值决定了子节点中reg属性中地址长度信息所占的字长
*/
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 { //定义了节点cpus下的子节点&cpu0(cpu@0),(相当于小树枝)
/*
设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点
设备节点的常用定义形式
label: node-name@unit-address
label: 节点标签,它的作用是为了方便访问节点
&label == node-name@unit-address,相当于直接访问这个节点,
比如通过 &cpu0就可以访问"cpu@0"这个节点
node-name: 该设备节点的名称 字符串类型
unit-address: 该设备的起始地址,如果该节点没有地址就写0
*/
compatible = "arm,cortex-a7"; //表示&cpuo这个设备节点的兼容性属性,它的驱动文件是cortex-a7,开发厂商是arm
/*
device_type:标准属性 属性值为字符串
该属性被DTS语法抛弃,此属性现在只能用于cpu节点或者memory节点
*/
device_type = "cpu";
reg = <0>;
};
soc { //定义soc节点,(大树枝),在linux设备树中会通过soc节点来管理SOC的内部外设(例如uart,i2c控制器等),因此需要创建一个soc节点来管理这些soc内部外设代表的子节点
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
/*
ranges 标准属性(地址转换表) 属性值可为空或者这种类型的数字矩阵(子总线地址空间的物理地址,父总线地址空间的物理地址,子地址空间的长度)
如果属性值为空表示子地址空间和父地址空间完全相同,故不需要地址转换
子总线地址空间的物理地址和父总线地址空间的物理地址由该节点的父节点的#address-cells属性来决定
子总线空间地址长度由该节点的父节点的#size-cells属性来决定
例如: 父节点soc中设置 #address-cells = <1>; #size-cells = <1> ;ranges = <0x0 0xe0000000 0x00100000>
然后子节点serial中设置reg = <0x4600 0x100>
那么我们原本定义serial设备的起始地址为0x4600,寄存器长度为0x100,在经过地址转换后,serial设备可以从0xe0004600处开始读写
*/
ranges;
};
};
}