一. 设备树是一种描述硬件设备的方法,描述设备数的文件叫DTS(Device Tree Source),DTS采用树形的结构描述板级设备
-
在设备树出现之前,板级硬件设备的相关信息都被编译进Linux内核中,导致内核代码臃肿且难以维护,所以引入了设备树,将内核与硬件设备代码解耦。
-
.dtsi文件(板级公共文件,使用时类似头文件)描述的是如SOC级信息:SOC有几个CPU、主频是多少、各个外设的控制信息等;.dts文件描述的是板级信息:IIC设备、SPI设备等;一个完整设备树文件是由1个dts+n个dtsi文件组成。
二. DTS、DTB和DTC:
- DTB是二进制文件,DTS是源文件,使用DTC工具(scripts/dtc目录下)将DTS编译成DTB
- 当制作了一块开发板,就需要根据板级设备制作.dts文件,并在arch/arm/boot/dts/Makefile文件下找到对应SOC,将.dts文件名添加上去
- .dts文件基本不会重头写完,大都是在SOC厂商提供的.dts文件上进行修改
三. DTS语法:
- 参考 《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》
- 设备树是采用树形结构描述板子的设备信息的文件,每个设备都是一个节点,称为设备节点;每个节点都通过一些属性信息(键值对)来描述节点信息
- 节点命名格式:node-name@unit-address
- node-name是设备节点名字;unit-address是设备节点的地址,若没有可以不写
- 如:cpu@0;interrupt-controller@00a01000
- 节点标签:label: node-name@unit-address,使用节点标签 &label可以方便访问设备节点
- 节点属性:不同设备有不同的节点属性,用户可以自定义属性,但有很多属性都是标准属性
-
compatible(兼容) 属性,用于将设备和驱动绑定;格式为 “manufacturer,model”,manufacturer表示驱动厂商,model表示驱动的名字;如compatible = ul-evk-wm8960",“fsl,imx-audio-wm8960”;fal表示的是飞思卡尔厂商,而后面则表示驱动名字;设备会根据compatible属性在Linux内核查找相对应的驱动
-
model属性,用于描述设备信息,如名字
-
status属性:表示设备的状态信息
okay 表明设备是可操作的 disabled 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档 fail 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作 fail-sss 含义和fail相同,后面的 sss 部分是检测到的错误内容 -
#address-cells 和**#size-cells** 属性,可以在任何拥有子节点的设备中,用于描述子节点的地址信息属性。
- #address-cells 和#size-cells 表明了子节点应该如何编写reg属性值,reg属性格式为reg = <address1 length1 address2 length2 address3 length3……>;
- #address-cells表明起始地址address所占用的字长,#size-cells表明地址长度length所占用的字长。
-
reg属性,描述设备的地址空间信息,一般是外设的寄存器地址范围。
-
ranges属性,格式为 (child-bus-address,parent-bus-address,length),是一个地址映射转换表;若为空,则代表父地址空间和子地址空间完全相同。
四. 根节点compatible属性
- Linux内核会根据根节点的compatible属性,判断是否支持该设备;支持则会启动Linux内核,否则启动失败
- 如在arch/arm/mach-imx/mach-imx6ul.c下,machine_desc 结构体中有个**.dt_compat** 成员变量,此成员变量保存着本设备兼容属性;只要根节点compatible属性和 .dt_compat 中任一值相等,则表示Linux内核支持该设备。
五. 向节点追加或修改内容
-
在 .dts文件中通过**&label引用.dtsi文件中的节点,然后在这个节点增加或修改**内容,如
&i2c1{ /*要添加或修改的内容*/ }
六. 创建小型模板设备树,熟悉DTS语法
/*
简单实现如下内容
1. I.MX6ULL 这个Cortex-A7架构的 32 位CPU
2. I.MX6ULL 内部 ocram,起始地址 0x00900000,大小为 128KB(0x20000)
3. aips1 = <0x02000000 0x100000>
4. aips2 = <0x02100000 0x100000>
5. aips3 = <0x02200000 0x100000>
6. I.MX6ULL内部aips1域下的ecspi1 外设控制器,寄存器起始地址为0x02008000,大小为0x4000
7. I.MX6ULL内部aips2域下的usbotg1外设控制器,寄存器起始地址为0x02184000,大小为0x4000
8. I.MX6ULL内部aips3域下的rngb 外设控制器,寄存器起始地址为0x02284000,大小为0x4000
*/
/{
#address-cells = <1>;
#size-cells = <0>;
//CPU0节点
cpu0:cpu@0{
compatible = "arm,Cortex-A7";
device_type = "cpu";
reg = <0>;
};
//soc节点
soc{
compatible = "fal,IMx6ULL";
ranges; //表示父子地址空间完全相同
//ocram子节点
ocram:sram@00900000{
compatible = "fal,lpm-sram";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x00900000 0x20000>;
};
//aips1子节点
aips1:aips1@02000000{
compatible = "fal,aips-bus","simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
//ecspi1 外设控制器子节点
ecspi1:ecspi@02008000{
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
status = "disabled";
};
};
//aips2子节点
aips2:aips2@02100000{
compatible = "fal,aips-bus","simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
//usbotg1 外设控制器子节点
usbotg1:usb@02184000{
compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";
reg = <0x02184000 0x4000>;
status = "disabled";
};
};
//aips3子节点
aips3:aips3@02200000{
compatible = "fal,aips-bus","simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;
//rngb 外设控制器子节点
rngb:rngb@02284000{
compatible = "fsl,imx6sl-rng", "fsl,imx-rng";
reg = <0x02284000 0x4000>;
status = "disabled";
};
};
};
}
七. 设备树在系统中的体现
- Linux内核启动的时候会解析各个设备节点的信息,并根据设备节点名字在根文件系统的/proc/devicetree 目录下创建文件夹
- #address-cells、#size-cells、compatible、model和name等文件,是根节点的属性信息
- 其余文件夹对应的就是,根节点下的各个子节点
-
特殊节点:aliases和chosen
- aliases子节点(在.dtsi文件)的意思为别名,该节点的主要功能就是给其他节点定义别名,方便访问;像是标签**&label**也是为了方便访问。
- chosen子节点主要是为了uboot向Linux内核传递数据!里面有 bootargs属性!!!这个属性和uboot环境变量bootargs一模一样,因为 uboot向chosen节点添加并设置了bootarges属性
八. 绑定信息文档
- Linux 内核源码中有详细的.txt 文档描述了在设备树中如何添加一个硬件设备的节点,叫做绑定文档,路径为内核源码目录下的 /Documentation/devicetree/bindings
九. 设备树OF操作函数
-
设备树中描述了节点设备的详细信息,在编写设备驱动的时候需要获得这些信息;Linux内核在include/linux/of.h文件中,定义了许多OF函数,用于获取节点设备的详细信息。
-
Linux使用device_node结构体描述一个设备节点,该结构体中有一表示属性信息的结构体指针变量property
-
/******************** 查找节点的OF函数 ********************/ /* from表示开始查找到节点,NULL则表示从根节点开始查找 name表示要查找节点的名字 返回值为查找到的节点地址,类型为device_node* ;返回NULL表示失败*/ struct device_node *of_find_node_by_name(struct device_node*from, const char *name); /*函数通过 device_type 属性查找指定的节点*/ struct device_node *of_find_node_by_type(struct device_node *from, const char *type); /*函数根据 device_type 和 compatible 这两个属性查找指定的节点*/ struct device_node *of_find_compatible_node(struct device_node *from, const char *type,const char *compatible); /*函数通过 of_device_id 匹配表来查找指定的节点 matches:of_device_id 匹配表,在此匹配表里面查找节点 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); /*通过路径来查找指定的节点 path为带有全路径的节点名,可以使用 节点的别名 */ inline struct device_node *of_find_node_by_path(const char *path);
-
/*用于获取指定节点的父节点 参数为要查找父节点的节点 返回值为找到的父节点,没有则返回NULL*/ struct device_node *of_get_parent(const struct device_node *node); /*迭代的查找子节点 参数node为父节点;prev表示从哪一个节点开始迭代查找子节点,NULL表示从第一个子节点开始 返回值为找到的下一个子节点*/ struct device_node *of_get_next_child(const struct device_node *node,struct device_node *prev);
-
/*通过 属性名字 查找指定的属性 np为设备节点;name为属性名字;lenp为属性值的字节数(大小?) 返回值为属性property结构体指针,即找到的属性地址*/ property *of_find_property(const struct device_node *np,const char *name,int *lenp);
-
/*获取属性中元素的数量 proname: 需要统计元素数量的属性名字;elem_size:指定元素长度??? 返回值为属性中元素的数量*/ int of_property_count_elems_of_size(const struct device_node *np, const char *propname,int elem_size);
-
/*用于从 指定属性 中获取 指定标号 的 u32类型数据值 proname:要读取的属性名字;index:要读取的指定值标号;out_value:读取到的值 返回值:0成功,0<失败,-EINVAL属性不存在,-ENODATA未读取到,-EOVERFLOW溢出*/ int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value)
-
/*下面四个函数分别是读取属性中u8、u16、u32和u64类型的 数组数据 const struct device_node *np 为设备节点 const char *propname 为要读取的属性名字 u8/u16/u32/u64 *out_value 为读取到的数组地址 size_t sz 为要读取的数组元素数量 返回值:0成功,0<失败,-EINVAL属性不存在,-ENODATA未读取到,-EOVERFLOW溢出*/ int of_property_read_u8_array(......); int of_property_read_u16_array(......); int of_property_read_u32_array(......); int of_property_read_u64_array(......); /*下面四个函数分别是读取属性中u8、u16、u32和u64类型的 数据*/ int of_property_read_u8(const struct device_node *np,const char *propname,u8 *out_value); int of_property_read_u16(......); int of_property_read_u32(......); int of_property_read_u64(......);
-
/*读取属性中字符串值;返回值:0成功,<0失败*/ int of_property_read_string(struct device_node *np,const char *propname,const char **out_string);
-
int of_n_addr_cells(struct device_node *np); //获取#address-cells 属性值 int of_n_size_cells(struct device_node *np); //获取#size-cells 属性值
-
/*查看节点的 compatible属性 是否有包含 compat 指定的字符串,即检查设备节点的兼容性 返回值:0不包含,>0包含*/ int of_device_is_compatible(const struct device_node *device,const char *compat);
-
/*函数用于获取地址相关属性,主要是reg或者assigned-addresses属性值 index为要读取的地址标号,size为读取的地址长度,flags为参数 返回值:读取到的地址数据首地址,NULL失败*/ const __be32 *of_get_address(struct device_node *dev,int index,u64 *size,unsigned int *flags); /*将从设备树读取到的地址转换为物理地址 返回值:得到的物理地址,OF_BAD_ADDR表示转换失败*/ u64 of_translate_address(struct device_node *dev,const __be32 *in_addr);
-
IIC、SPI、GPIO等外设,都有自已的一组寄存器即内存空间;Linux内核在文件include/linux/ioport.h中,用结构体resource描述一段内存空间。
/*本质上是将 reg 属性值,转换为 resource 结构体类型 dev:设备节点;index:地址资源标号;r:得到的 resource 类型的资源值; 返回值:0成功,<0失败*/ int of_address_to_resource(struct device_node *dev, int index, struct resource *r);
-
of_iomap 函数来获取内存地址所对应的虚拟地址;本质是将reg属性值,转换为虚拟地址,若reg有多段,则可以用index参数指定完成地址映射的是哪一段。
/*返回值:内存映射后的虚拟地址首地址,NULL失败*/ void __iomem *of_iomap(struct device_node *np, int index);
以上是我在学习过程中的总结,不当之处请在评论区指出。