页面加载中,请稍候
来源:一口Linux发布时间:2023-02-17302浏览
询问 AI在之前使用 S3C2440 开发板移植 Linux 3.4.2 内核时,修改了很多关于 c 文件去适配开发板,和开发板相关的文件放在arch/arm/mxch-xxx目录下,因此 linux 内核 arm 架构下添加了很多开发板的适配文件:

这些 c 文件仅仅用来适配某款开发板,对于 Linux 内核来说并没有提交什么新功能,但是每适配一款新的开发板就需要一堆文件,导致 Linux 内核越来越臃肿:

终于 Linus 忍不住天天 merge 这些鬼东西,向 arm 社区发出了一封邮件,第一句话就足矣表现不满:"This whole ARM thing is a f*cking pain in the ass"。

因此,Arm 社区开始引入之前 powerPC 架构就采用的设备树,将描述这些板级信息的文件与 Linux 内核代码分离,Linux 4.x 版本几乎都支持设备树,所有开发板的设备树文件统一放在arch/arm/boot/dts目录中。
设备树全称 Device Tree,是一种数据结构,用来描述板级设备信息,比如 CPU 数量、外设基地址、总线设备等,如图:

(1)DTS:设备树描述文件为.dts格式,这个也是我们重点需要掌握编写的。(2)DTSI
为了减少冗余,设备树头文件格式为.dtsi文件,可以被不同的.dts文件引用。
比如 imx6ull 有野火、正点原子、米尔、百问网等很多款开发板,这些开发板肯定需要一个 dts 文件来描述,但是关于 imx6ull 芯片级别的描述,就不需要每个文件都去描述一下,而是大家都引用 NXP 官方提供的.dtsi描述文件即可。
这样既最大化的降低了设备描述文件的冗余程序,也极大的降低了开发者适配新开发板的工作量。
(3)DTC
编写.dtc文件使用设备树语法,则需要一个特定的编译器来编译,称为dtc工具,源码在 Linux 内核的scripts/dtc目录下。

(4)DTB:设备树源码.dts、.dtsi文件最终经过 dtc 编译器,会生成.dtb文件。
(1)简单粗暴,编译内核
make
(2)编译全部设备树文件
makedtbs
(3)编译指定的设备树文件
make<xxx.dtb>
/dts-v1/
设备树是由一个个节点组成的,每个节点相当于树上的一片叶子,节点的结构和约定如下。
(1)节点名称
node-name@unit-address
node-name指明了节点的名称,长度应该为 1-31 个字符,命名应该以小写或者大写字母开头,支持的字符如下表:

节点名称的unit-address表示设备地址或者寄存器首地址(具体节点具体分析,不一定是绝对地址),必须与节点 reg 属性中指定的首地址匹配。
eg. imx6ull.dtsi 中描述的 uart1 控制器节点:

该节点 label 为 uart1,节点名称为 serial,设备地址(寄存器首地址)为 02020000,正是 imx6ull uart1 外设寄存器的首地址。

(2)路径名称
通过指定从根节点到所有子节点到所需节点的完整路径,可以唯一地标识设备树中的节点。
指定设备路径的约定如下:
/node-name-1/node-name-2/node-name-N
(3)属性
设备树中的每个节点都有用来描述节点信息的属性。属性由名称和值两部分组成,属性名称的可用字符如下表:

属性值是一个由零个或多个字节组成的数组,其中包含与属性相关联的信息,支持的数据类型如下:


(4)节点标签
节点标签用:隔开,为了方便访问节点,可用直接通过node-lable来访问节点,示例如下:
node-label:node-name@unit-address{
};
DTSpec 为设备节点指定一组标准属性,如下。
(1)compatible
compatible 属性值由 string list 组成,定义了设备的兼容性,推荐格式为manufacturer,model,manufacturer 描述了生产商,model 描述了型号。
compatible="fsl,imx6ull-14x14-evk","fsl,imx6ull";
开发板上的音频芯片采用的欧胜 WM8960,sound 节点的 compatible 属性值如下:
compatible="fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960"
使用的时候,sound 这个设备首先使用第一个兼容值在 Linux 内核中查找,看看能不能找到对应的驱动文件;如果没有找到的话,就使用第二个兼容值查找。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值核 OF 匹配表中的任何一个值相等,那么就表示这个设备可以使用这个驱动。
比如在文件imx-wm8960.c文件中:
staticconststructof_device_idimx_wm8960_dt_ids[]={
{.compatible="fsl,imx-audio-wm8960",},
{/*sentinel*/}
};
这个数组就是imx-wm8960.c这个驱动文件的匹配表。
(2)model
model 属性值是一个 string,指明了设备的厂商和型号,推荐格式为manufacturer,model。
model="Freescalei.MX6ULL14x14EVKBoard";
(3)phandle
phandle 属性值是一个 u32,为设备树中唯一的节点指定一个数字标识符,用于其它节点指明关系。
(4)status
status 属性值是一个 string,表示设备的运行状态,可用值如下表:

(5)#address-cells 和 #size-cells
#address-cells and #size-cells 属性值是一个 u32,可以用在任何拥有子节点的设备中,并描述子设备节点应该如何寻址。
#address-cells属性定义子节点 reg 属性中地址字段所占用的字长,也就是占用 u32 单元格的数量。
#size-cells属性定义子节点 reg 属性值的长度所占用的 u32 单元格的数量。
(6)reg
reg 属性值是一个 prop-encoded-array,用来描述设备地址空间资源信息,一般是某个外设的寄存器地址范围信息,包括起始地址和地址长度。
reg=<address1length1address2length2address3length3……>
其中 address 是起始地址,length 是地址长度。#address-cells表明 address 这个数据所占用的字长,#size-cells表示 length 这个数据所占用的字长。
比如:
spi4{
compatible="spi-gpio";
pinctrl-names="default";
pinctrl-0=<pinctrl_spi4>;
status="okay";
gpio-sck=<gpio5110>;
gpio-mosi=<gpio5100>;
num-chipselects=<1>;
#address-cells=<1>;
#size-cells=<0>;
gpio_spi:gpio_spi@0{
compatible="fairchild,74hc595";
gpio-controller;
#gpio-cells=<2>;
reg=<0>;
registers-number=<1>;
registers-default=/bits/8<0x57>;
spi-max-frequency=<100000>;
};
};
#address-cells = <1>表示子节点中 reg 属性的 address 占用 1 个 u32 数据,#size-cells 表示子节点中 reg 属性的 length 不占用空间,没有。
在子节点 gpio_spi 中:reg 属性值设置为<0>,相当于设置了起始地址,而没有设置地址长度。
(7)virtual-reg
(8)ranges
(9)dma-ranges
(1)根节点
树是由树根开始的,在设备树中称之为根节点,路径为/,根节点不需要节点名称,所有子节点都是挂在根节点上的,可以看到最简单的根节点如下:
/{
};
根节点的属性有:

(2)aliases
aliases 节点用来定义别名,为了内核方便访问节点。
(3)chosen
chosen 节点是为了uboot 向 Linux 内核传递数据,重点是 bootargs 参数,一般.dts 文件中 chosen 节点通常为空或者内容很少。
此处关于 uboot 如何通过设备树传参给 kernel,可以单独写篇文章,待补充...
(1)向根节点追加内容
/{
//要补充的内容
};
(2)向子节点追加内容
node-label{
//追加内容
};

直接查看arch/arm/boot/dts/Makefile文件,在该文件中查找即可,比如 imx6ull 相关部分如下:
dtb-$(CONFIG_SOC_IMX6ULL)+=\
imx6ull-14x14-ddr3-arm2.dtb\
imx6ull-14x14-ddr3-arm2-adc.dtb\
imx6ull-14x14-ddr3-arm2-cs42888.dtb\
imx6ull-14x14-ddr3-arm2-ecspi.dtb\
imx6ull-14x14-ddr3-arm2-emmc.dtb\
imx6ull-14x14-ddr3-arm2-epdc.dtb\
imx6ull-14x14-ddr3-arm2-flexcan2.dtb\
imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb\
imx6ull-14x14-ddr3-arm2-lcdif.dtb\
imx6ull-14x14-ddr3-arm2-ldo.dtb\
imx6ull-14x14-ddr3-arm2-qspi.dtb\
imx6ull-14x14-ddr3-arm2-qspi-all.dtb\
imx6ull-14x14-ddr3-arm2-tsc.dtb\
imx6ull-14x14-ddr3-arm2-uart2.dtb\
imx6ull-14x14-ddr3-arm2-usb.dtb\
imx6ull-14x14-ddr3-arm2-wm8958.dtb\
imx6ull-14x14-evk.dtb\
imx6ull-14x14-evk-btwifi.dtb\
imx6ull-14x14-evk-emmc.dtb\
imx6ull-14x14-evk-gpmi-weim.dtb\
imx6ull-14x14-evk-usb-certi.dtb\
imx6ull-9x9-evk.dtb\
imx6ull-atk-emmc.dtb\
imx6ull-9x9-evk-btwifi.dtb\
imx6ull-9x9-evk-ldo.dtb
可以看到所有 imx6ull 的开发板,其中我们移植的开发板为imx6ull-atk-emmc.dtb,本文就以该设备树文件为例,讲述设备树语法。

查看文件arch/arm/boot/dts/skeleton.dtsi,内容非常简洁,只定义了根节点:

不同的 imx6ull 开发板都是使用 imx6ull 这颗处理器芯片,而 imx6ull soc 芯片级的描述是固定的,通常这个也是由芯片厂商提供。
查看imx6ull.dtsi文件,整体框架如下:

接下来我们逐个分析。
该文件引用的skeleton.dtsi文件中,已经定义了根节点,如果再次定义根节点,其中的内容将作为对根节点的补充。
在该描述文件中,挂在根节点上的子节点有:aliases、cpus、intc、clocks、soc。
(1)aliases 节点
aliases 节点用来定义一个或多个别名属性,按照约定,该节点应该在根节点上。

(2)cpus 节点所有的设备树都需要 cpus 节点,用来描述系统的 CPU 信息。

i.MX6ULL 是单核处理器,因此只有一个/cpus/cpu*子节点,用来表示某一个具体 CPU 核的信息,其中有以下属性:
(3)intc 节点
(4)clocks 节点
(5)soc 节点
soc 节点中,描述了 i.MX6ULL 片上的总线和全部外设:


aips2:aips-bus@02100000{
compatible="fsl,aips-bus","simple-bus";
#address-cells=<1>;
#size-cells=<1>;
reg=<0x021000000x100000>;
ranges;
//一堆外设子节点,省略...
};
aips2 节点的属性有:
i2c 控制器是挂在 aips2 总线上的,对应到设备树中,i2c 控制器节点挂在 aips2 节点上,描述代码如下:

以 i2c1 节点为例,标签是 i2c1,节点名称是 i2c,寄存器起始地址是 0x021a0000,有如下属性:

imx6ull-atk-emmc.dts这个文件的大概框架如下。
/dts-v1/;
框架如下:

其中 chosen 节点是 uboot 用来向内核传递参数,内容如下:

根节点之后,使用引用符来对imx6ull.dtsi文件中定义的子节点进行补充,用来描述开发板的具体配置,这个也是主要需要适配修改的文件。

在 NXP 官方开发板上,磁力计 mag3110 是接在 i2c1 总线控制器上的,对应到设备树中,磁力计节点挂在 i2c1 控制器节点上,如下。

可以看到,i2c1 节点的补充描述中,就描述了 i2c1 控制器上所连接的设备。
i2c1 节点的补充属性有:
i2c 控制器上接了两个设备,一个是 mag3110 磁力计,一个是 fxls8471 加速度计。注意,在描述节点时,@后面的地址变为了 i2c 总线的设备地址,mag3110 的 i2c 从机地址是 0e,fxls8471 的 i2c 从机地址是 1e。

至此,imx6ull 设备树分析完成,完整的思维导图文档在这里:【腾讯文档】imx6ull 设备树[1]。
Linux 内核启动的时候会解析设备树 dtb 文件,所以启动以后可以在根文件系统中看到设备树的节点信息,在/proc/device-tree目录中:

这里 device-tree 目录是一个软链接,实际指向/sys/firmware/devicetree/base目录。
在 device-tree 目录中,首先可以看到设备树根节点下的所有一级子节点。
(1)属性是以文件的方式给出,可以直接查看。
比如查看根节点的 model 属性:

(2)节点以目录的方式给出。
比如 soc 子节点的内容如下:

在设备树中添加一个新的节点时,添加的格式在 Linux 内核源码中有详细的.txt 文档描述,这些 txt 文档就称为绑定文档。
绑定文档在/Documentation/devicetree/bindings路径中:

比如我们在开发板的 i2c 上新添加了一个设备,需要在设备树的 i2c 节点下新添加一个节点,就可以查看i2c/i2c-imx.txt文档:

Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀of_,所以也称为 OF 函数,声明在文件include/linux/of.h文件中。
Linux 内核使用 device_node 结构体来描述一个设备树节点,定义在文件include/linux/of.h文件中。
structdevice_node{
constchar*name;
constchar*type;
phandlephandle;
constchar*full_name;
structfwnode_handlefwnode;
structproperty*properties;
structproperty*deadprops;/*removedproperties*/
structdevice_node*parent;
structdevice_node*child;
structdevice_node*sibling;
structkobjectkobj;
unsignedlong_flags;
void*data;
#ifdefined(CONFIG_SPARC)
constchar*path_component_name;
unsignedintunique_id;
structof_irq_controller*irq_trans;
#endif
};
(1)通过节点名字查找节点
externstructdevice_node*of_find_node_by_name(structdevice_node*from,
constchar*name);
参数意义如下:
返回值为找到的节点,NULL 为查找失败。
(2)通过节点类型查找节点
externstructdevice_node*of_find_node_by_type(structdevice_node*from,
constchar*type);
type 参数指定要查看节点对应的 type 字符串,也就是 device_type 属性值。
(3)通过 device_type 和 compatible 查找节点
externstructdevice_node*of_find_compatible_node(structdevice_node*from,
constchar*type,constchar*compat);
(4)通过 of_device_id 匹配表来查找节点
externstructdevice_node*of_find_matching_node_and_match(
structdevice_node*from,
conststructof_device_id*matches,
conststructof_device_id**match);
(5)通过路径来查找节点
staticinlinestructdevice_node*of_find_node_by_path(constchar*path)
{
returnof_find_node_opts_by_path(path,NULL);
}
这里的 path 必须要是绝对路径。
(1)获取父节点
externstructdevice_node*of_get_parent(conststructdevice_node*node);
(2)迭代查找子节点
externstructdevice_node*of_get_next_child(conststructdevice_node*node,
structdevice_node*prev);
prev 参数是前一个子节点,如果为 NULL 表示从第一个子节点开始。
在节点描述类型 device_node 中,有这样一项用来描述属性值:
structproperty*properties;
property 结构体类型定义如下:
structproperty{
char*name;
intlength;
void*value;
structproperty*next;
unsignedlong_flags;
unsignedintunique_id;
structbin_attributeattr;
};
(1)查找指定节点的属性
externstructproperty*of_find_property(conststructdevice_node*np,
constchar*name,
int*lenp);
参数 name 指属性名字,lenp 指属性值的字节数。
(2)获取属性中元素的数量
externintof_property_count_elems_of_size(conststructdevice_node*np,
constchar*propname,intelem_size);
参数 propname 是需要统计元素数量的属性名字,参数 elem_size 是元素的长度。
返回值是获取到的属性元素数量。
eg. reg 属性的值通常是一个数组,使用此函数可以获取的数组的大小。
(3)从属性中获取指定索引的 u32 类型数据值
externintof_property_read_u32_index(conststructdevice_node*np,
constchar*propname,
u32index,u32*out_value);
参数 out_value 用来返回获取到的值。
返回值用来表示是否获取成功。
(4)从属性中获取数组值
externintof_property_read_u8_array(conststructdevice_node*np,
constchar*propname,u8*out_values,size_tsz);
externintof_property_read_u16_array(conststructdevice_node*np,
constchar*propname,u16*out_values,size_tsz);
externintof_property_read_u32_array(conststructdevice_node*np,
constchar*propname,
u32*out_values,
size_tsz);
externintof_property_read_u64_array(conststructdevice_node*np,
constchar*propname,
u64*out_values,
size_tsz);
eg. reg 属性的值通常是一个数组,使用这个函数可以一次读取出一个数组,也就是 reg 属性的全部值。
(5)从属性中获取布尔值/整形值
/**
*of_property_read_bool-Findfromaproperty
*@np:devicenodefromwhichthepropertyvalueistoberead.
*@propname:nameofthepropertytobesearched.
*
*Searchforapropertyinadevicenode.
*Returnstrueifthepropertyexistfalseotherwise.
*/
staticinlineboolof_property_read_bool(conststructdevice_node*np,
constchar*propname)
{
structproperty*prop=of_find_property(np,propname,NULL);
returnprop?true:false;
}
staticinlineintof_property_read_u8(conststructdevice_node*np,
constchar*propname,
u8*out_value)
{
returnof_property_read_u8_array(np,propname,out_value,1);
}
staticinlineintof_property_read_u16(conststructdevice_node*np,
constchar*propname,
u16*out_value)
{
returnof_property_read_u16_array(np,propname,out_value,1);
}
staticinlineintof_property_read_u32(conststructdevice_node*np,
constchar*propname,
u32*out_value)
{
returnof_property_read_u32_array(np,propname,out_value,1);
}
staticinlineintof_property_read_s32(conststructdevice_node*np,
constchar*propname,
s32*out_value)
{
returnof_property_read_u32(np,propname,(u32*)out_value);
}
(6)从属性中获取字符串
externintof_property_read_string(structdevice_node*np,
constchar*propname,
constchar**out_string);
(7)获取#address-cells 和#size-cells 属性值
externintof_n_addr_cells(structdevice_node*np);
externintof_n_size_cells(structdevice_node*np);
地址相关操作的函数定义在include/linux/of_address.h文件中。
(1)获取地址相关属性
staticinlineconst__be32*of_get_address(structdevice_node*dev,intindex,
u64*size,unsignedint*flags);
参数 index 是要读取的地址标号,size 是地址长度,flag 是是参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等。
返回值是读取到的数据首地址,为 NULL 则表示读取失败。
(2)将从设备树读取到的地址转换为物理地址
externu64of_translate_address(structdevice_node*np,const__be32*addr);
返回值为转换得到的地址,如果为 OF_BAD_ADDR 的话表示转换失败。
(3)将地址转换为 resources 资源
GPIO、IIC、SPI 这些外设都有对应的寄存器,这些寄存器就是一段内存空间,Linux 内核使用 resource 结构体来描述一段内存空间,定义在文件include/linux/ioport.h中。
/*
*Resourcesaretree-like,allowing
*nestingetc..
*/
structresource{
resource_size_tstart;
resource_size_tend;
constchar*name;
unsignedlongflags;
structresource*parent,*sibling,*child;
};
其中:
资源类型同样定义在文件include/linux/ioport.h中,如下:
/*
*IOresourceshavethesedefinedflags.
*/
#defineIORESOURCE_BITS0x000000ff/*Bus-specificbits*/
#defineIORESOURCE_TYPE_BITS0x00001f00/*Resourcetype*/
#defineIORESOURCE_IO0x00000100/*PCI/ISAI/Oports*/
#defineIORESOURCE_MEM0x00000200
#defineIORESOURCE_REG0x00000300/*Registeroffsets*/
#defineIORESOURCE_IRQ0x00000400
#defineIORESOURCE_DMA0x00000800
#defineIORESOURCE_BUS0x00001000
#defineIORESOURCE_PREFETCH0x00002000/*Nosideeffects*/
#defineIORESOURCE_READONLY0x00004000
#defineIORESOURCE_CACHEABLE0x00008000
#defineIORESOURCE_RANGELENGTH0x00010000
#defineIORESOURCE_SHADOWABLE0x00020000
#defineIORESOURCE_SIZEALIGN0x00040000/*sizeindicatesalignment*/
#defineIORESOURCE_STARTALIGN0x00080000/*startfieldisalignment*/
#defineIORESOURCE_MEM_640x00100000
#defineIORESOURCE_WINDOW0x00200000/*forwardedbybridge*/
#defineIORESOURCE_MUXED0x00400000/*Resourceissoftwaremuxed*/
#defineIORESOURCE_EXCLUSIVE0x08000000/*Userlandmaynotmapthisresource*/
#defineIORESOURCE_DISABLED0x10000000
#defineIORESOURCE_UNSET0x20000000/*Noaddressassignedyet*/
#defineIORESOURCE_AUTO0x40000000
#defineIORESOURCE_BUSY0x80000000/*Driverhasmarkedthisresourcebusy*/
常用的资源是 IORESOURCE_MEM、IORESOURCE_REG、IORESOURCE_IRQ 三个。
OF 函数中,可以将 reg 属性给出的地址转换为 resource 资源:
externintof_address_to_resource(structdevice_node*dev,intindex,
structresource*r);
(4)虚拟地址映射
之前使用 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后,可以直接通过 of_iomap 来完成物理地址到虚拟地址的映射。
void__iomem*of_iomap(structdevice_node*node,intindex);
参数 index 是 reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话,index 就设置为 0。
(1)检查设备兼容性
externintof_device_is_compatible(conststructdevice_node*device,
constchar*);
第二个参数用来指定要查看的字符串,该函数会检查指定的字符串是否在节点的 compatible 属性中。
至此,设备树基本知识学习完毕,又是一篇万字长文。

参考资料
[1]【腾讯文档】imx6ull 设备树:https://docs.qq.com/mind/DR29WT0R4YWVEeUh2(点击阅读原文即可查看思维导图)
end
一口Linux
关注,回复【1024】海量Linux资料赠送
新闻来源:一口Linux,文中所述为作者独立观点,不代表icspec立场。更多精彩资讯请下载icspec App。如对本稿件有异议,请联系微信客服specltkj。
暂无评论哦,快来评论一下吧!

2026-06-04

2026-06-01