一、Linux设备树的简介
设备树的诞生主要是为了解决RAM架构CPU日益增多,对应的设备种类更多的情况;很多设备描述无关紧要,按照传统的设备描述驱动方法会造成Linux系统的冗余;所以外设属性的描述独立于Linux就成为了DTS。
DTS、DTB和DTC:
DTS是设备树的源码文件;DTB则是将DTS编译以后得到的可以被系统识别的二进制文件。从DTS到DTB需要专用的编译器来编译,也就是所谓的DTC。在Linux的源码目录里,通过命令
make dtbs就可以编译出设备树。
如何在Linux源码中添加自己的dts文件并编译出dtb:
在Linux内核源码目录下,路径./arch/arm/boot/dts/下,有个Makefile文件,找到你使用的SOC对应的配置选项(每种RAM有多种SOC半导体CPU),选项内容如(dtb-$(CONFIG_SOC_XXXX));在对应选项中添加自己写的dts对应的xxx.dtb,然后SOC选项配置为y,就可以编译出来了。
xxx.dtsi头文件:每一块soc都有一个公共的设备树,也就是dtsi,具体的板子的具体外设则叫dts。
二、Linux设备树的语法规则(基础篇)
1、xxx.dtsi头文件与引用:
与C语言一样,设备树文件也有语法 #include <xxx.h> 或 #include
<xxx.dtsi>;用于引用C语言的头文件或其他的设树头文件。编写设备树头文件的时候,最好使用后缀.dtsi。
一般来说,xxx.dtsi文件是SOC(核心板)用于描述其内部的外设信息的,包括CPU和时钟树;基于此SOC的开发板编写的dts文件都要引用这个xxx.dtsi文件。
//
2、设备节点的概念与规范:
① /{} 根节点,每个完整的设备树有且只有一个根节点,如果设备树文件所引用的头文件里也有根节点的话,跟节点会自动合并为一个。
②
node-name@unit-address节点命名格式:node-name是节点名字,为ASCII字符串;unit-address用于表示设备的地址或寄存器首地址,如果某个节点没有地址或寄存器的话,可以不要。
③
节点的标签label:node-name@unit-address:标签+:后才是节点的定义;引入标签的目的就是为了方便访问节点,可以直接通过&label来访问这个节点。
④ 每个节点下面,都有属性定义,格式:键 = 值。节点下面也可以嵌套子节点。
//
3、常用的标准属性:
① compatible属性(重要)
基本格式:compatible = "manufacturer,model","manufacturer,model",......;
内容含义:manufacturer表示驱动或设备的厂商,model表示该设备需要使用的驱动模块;这个属性值与Linux内核驱动的OF匹配表如果有匹配项,则会使用对应的驱动程序来驱动设备。
② model属性
基本格式:model = "xxxx";
内容含义:一般model属性用于描述设备的模块信息,比如模块名称啥的。
③ status属性
基本格式:status = "xxx";
内容含义:与设备运行状态有关,一般的,设备有如下几个运行状态;每个状态的具体含义需要参照设备的手册说明
④ #address-cells和#size-cells属性
基本格式:#address-cells = <number>; #size-cells = <number>;
内容含义:首先,对于拥有子节点的节点属性值,此值才有用,因为他们控制的是子节点的!!#address-cells决定了子节点属性中reg这个属性的地址信息所占用的字长(32位/字);#size-cells决定了子节点属性中,reg这个属性的地址长度值所占用的字长。
⑤ reg属性
基本格式:reg = <address lenght>,<address lenght>,......;
内容含义:一般用于描述某个外设的物理寄存器的地址占用信息。
⑥ ranges属性
基本格式:ranges = <child-bus-address parent-bud-address length>; 也可以为空
内容含义:child-bus-address表示子总线地址空间的物理地址,由父节点的#address-cells决定字长;parent-bus-address表示父总线地址空间的物理地址,同样由父节点的#address-cells决定字长;length表示子地址空间的物理地址长度,由父节点的#size-cells确定此地址长度所占用的字长。如果此项目为空,标志该节点设备的子地址与父地址空间完全相同,不需要转换(可以理解为相对地址与绝对地址的转换)。
⑦ device_type属性
基本格式:device_type = "xxx";
内容含义:这个属性值只有CPU和memory节点会使用,其他设备节点已经弃用了。
//
4、根节点的compatible属性与Linux启动原理:
①
基本原理:根节点所定义的compatible属性值,第一个值代表板子的类型,第二个值代表使用的SOC;Linux内核在启动时,会读取这个属性值和内核里的DT_MACHINE_START里的各种设备信息做比对,如果找到值相同的则启动内核。
② Linux内核的匹配过程:
//
5、设备树中的特殊节点:
① aliases 子节点:这个是专门用来给其他设备节点定义别名的,但一般我们会在每个设备节点上定义label,所以可以不用
②
chosen子节点:这个节点并不是一个真实的设备,closen节点主要是为了uboot向linux内核传递数据,重点是bootargs参数。u-boot引导linux内核启动前会往这个节点传递数据,证据如下:
//
6、设备树添加设备节点的参考文档:在Linux内核源码目录下,./Documentation/devicetree/bindings下,那些目录包含了各种外设不同SOC或板子添加设备节点的说明和例子。
//
7、Linux解析DTB文件的大概流程:Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree
目录下生成相应的设备树节点文件。
这里基于正点原子的NXP写一个简单的例子:
/ {
compatible = "fsl,imx6ull-alientek-evk","fsl,imx6ull";
cpus {
#address-cells = <1>
#size-cells = <0>
//cpu节点
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
//soc的内部外设父节点
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
ranges;
//ocram 节点
ocram: sram@00900000 {
compatible = "fsl,lpm-sram";
reg = <0x00900000 0x20000>;
};
//aips1 节点
aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus","simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "fsl,imx6ul-ecspi","fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
status = "disabled";
};
};
//aips2 节点
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus","simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
usbotg1: usb@02184000 {
compatible = "fsl,imx6ul-usb","fsl,imx27-usb";
reg = <0x02184000 0x4000>;
status = "disabled";
};
};
//aips3 节点
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus","simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;
rngb: rngb@02284000 {
compatible = "fsl,imx6sl-rng","fsl,imx-rng","imx-rng";
reg = <0x02284000 0x4000>;
};
};
};
}
三、Linux内核驱动下,获取设备树参数的OF函数
设备树建立好了以后,需要编写Linux设备驱动程序;而设备驱动程序需要获取设备树上的那些属性值来驱动具体的设备。Linux内核给我们提供了一系列函数来获取设备树节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”;这些OF函数原型都定义在include/linux/of.h文件中。
设备节点描述结构体:
设备都是以节点的形式“挂”到设备输上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux内核使用结构体device_node来描述一个节点,结构体定义在include/linux/of.h中,定义如下:
struct device_node {
const char *name; /* 节点名字 */
const char *type; /* 设备类型 */
phandle phandle;
const char *full_name; /* 节点全名 */
struct fwnode_handle fwnode;
struct property *properties; /* 属性 */
struct property *deadprops; /* removed 属性 */
struct device_node *parent; /* 父节点 */
struct device_node *child; /* 子节点 */
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data; /* 提供该驱动开发人员,用于传递自定义数据 */
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
//
Linux用于查找设备树指定节点的5个函数:
#include <linux/of.h>
1、struct device_node *of_find_node_by_name(struct device_node *from,const char
*name);
参数解释:
*from:查找设备节点的起始节点,如果为NULL则自动从根节点开始查找
*name:要查找的节点名字。
返回值:返回找到的目标节点,如果为NULL表示查找失败。
2、struct device_node *of_find_node_by_type(struct device_node *from,const char
*type);
参数解释:
*from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。
*type:要查找的节点对应的type字符串,也就是device_type属性值。
返回值:找到的节点,如果为NULL表示查找失败。
3、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、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;
返回值:找到的节点,如果为NULL表示查找失败。
of_device_id结构体原型:(思想:对传递的参数进行封装)
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
5、struct device_node *of_find_node_opts_by_path(const char *path, const char
**opts);
参数解释:
*path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是backlight这个节点的全路径。这里的路径是指设备节点在设备树上的路径;
**opts:这个一般为NULL;
返回值:找到的节点,如果为NULL表示查找失败。
//
Linux查找设备节点的父子节点的函数:
#include <linux/of.h>
1、struct device_node *of_get_parent(const struct device_node *node);
参数解释:
*node:需要查找其父节点的节点
返回值:返回被查找节点的父节点,返回NULL说明查找失败或没有父节点
2、struct device_node *of_get_next_child(const struct device_node *node,struct
device_node *prev);
参数解释:
*node:需要查找子节点的设备节点
*prev:上一个被查找出来的子节点,设备为NULL表示从第一个子节点开始查找
返回值:返回被查找出来的下一个子节点
//
Linux获取设备节点的属性值链表节点OF函数:
#include <linux/of.h>
属性值表示结构体:
Linux内核使用结构体property来描述一个设备的节点的一个属性,每个设备节点的属性值以property结构体单向链表的形式存在。
结构体原型:
struct property {
char *name; /* 属性的名字 */
int length; /* 属性的长度 */
void *value; /* 属性的值 */
struct property *next; /* 下一个属性节点 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
2、struct property *of_find_property(const struct device_node *np, const char
*name, int *lenp);
参数解释:
*np:目标设备节点
*name:目标属性的键名
*lenp:获取到的属性值的字节数,可以为NULL
返回值:返回属性值描述结构体,NULL则表示没有或失败
3、int of_property_count_elems_of_size(const struct device_node *np, const char
*propname, int elem_size); //获取对应属性的值的数组个数
参数解释:
*np: 目标的设备节点;
*propname:需要统计元素数量的属性名字;
*elem_size:元素值中,数据单个数组的字节数
返回值:对应属性值的数组的个数
4、int of_property_read_u32_index(const struct device_node *np, const char
*propname, u32 index, u32 *out_value); //以u32数据格式读取属性的值
参数解释:
*np:设备节点;
*propname:要读取的属性名字;
*index:要读取的值标号,reg属性值中的哪一个;
*out_value:读取到的具体值;
返回值:0读取成功,负值,读取失败。
5、int
of_property_read_u8_array、of_property_read_u16_array、of_property_read_u32_array、of_property_read_u64_array函数:
函数原型格式:以数组的格式读取属性值
int of_prooerty_read_uxx_arrray(const struct device_node *np, const char
*propname, uxx *out_values, size_t sz);
参数解释:
*np:目标设备节点;
*proname:目标属性的名字;
*out_value:用于装载读取到的对应的数据数组值
sz:指定要读取的数组元素个数
返回值:返回0,代表操作成功,负值,读取失败;
6、of_property_read_u8、of_property_read_u16、of_property_read_u32、of_property_read_u64函数:
函数原型结构:单独以对应的数据格式读取属性的值
static inline int of_property_read_uxx(const struct device_node *np, const
char *propname, uxx *out_value)
{
return of_property_read_uxx_array(np,propname,out_value,1);
}
参数解释:
*np:目标设备节点;
*propname:目标属性名字;
*out_value:用于装载读取到的对应格式的属性值;
返回值:返回0,代表操作成功,负值,读取失败;
7、int of_property_read_string(struct device_node *np, const char *propname,
const char **out_string);
参数解释:
*np:目标设备节点
*propname:目标要读取的属性的名字;
**out_string:返回的字符串格式的属性值;
返回值:返回0,代表操作成功,负值,读取失败
8、int of_n_addr_cells(struct device_node *np);
//专门获取节点的#address-cells属性值,本质上是定位到父节点并获取"#address-cells"的值
参数解释:
*np:目标的设备节点
返回值:获取到的#address-cells的值,负值代表获取失败
9、int of_n_size_cells(struct device_node *np); //专门获取节点的#size-cells的属性值
参数解释:
*np:目标的设备节点
返回值:获取到的#size-cells的值
//
其他常用的OF函数:
#include <linux/of.h>
1、int of_device_is_compatible(const struct device_node *device, const char
*compat); //查看设备节点的compatible属性是否包含compat指定的字符串//
参数解释:
*device:目标设备节点;
*compat:指定对比的字符串
返回值:0=节点中compatible不包含字符串compat;1=包含
2、const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,
unsigned int *flags); //用于获取地址相关属性
参数解释:
*dev:目标设备节点;
index:要读取的地址标号,reg属性值中的哪一段;
*size:地址长度;
*flags:参数,比如IORESOURCE_IO、IORESOURCE_MEM等;
返回值:读取到的地址数据首地址
3、u64 of_translate_address(struct device_node *dev, const __be32 *in_addr);
//将从设备树节点读取到的地址转换为物理地址
参数解释:
*dev:目标设备节点;
*in_addr:需要转换的地址;
返回值:转换得到的物理地址,如果为OF_BAD_ADDR表示转换无效
4、int of_address_to_resource(struct device_node *node, int index, struct
resource *r); //此函数从设备树里提取内存资源值结构体resource,其实就是对设备节点的reg属性值进行转换
参数解释:
此函数获取的resource结构体描述了设备节点对应的SOC的内存资源,结构体原型如下:
struct resource {
resource_size_t start; /* u32类型,表示内存资源的起始地址 */
resource_size_t end; /* u32类型,表示内存资源的结束地址 */
const char *name; /* 资源的名字 */
unsigned long flags; /* 表示资源所属类型 */
struct resource *parent, *sibling, *child;
};
资源类型有:
#define IORESOURCE_BITS 0x000000ff
#define IORESOURCE_TYPE_BITS 0x00001f00
#define IORESOURCE_IO 0x00000100
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000
#define IORESOURCE_STARTALIGN 0x00080000
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000
#define IORESOURCE_MUXED 0x00400000
#define IORESOURCE_EXCLUSIVE 0x08000000
#define IORESOURCE_DISABLED 0x10000000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000
函数参数接口:
*node:目标设备节点
index:地址资源标号,指定reg属性值中的哪一段
*r:获取到的资源描述结构体
返回值:0=成功
5、void __iomem *of_iomap(struct device_node *node, int index);
//将设备节点的reg属性值里的指定index的物理地址转换为虚拟地址
#include <linux/of_address.h>
参数解释:
*node:目标的设备节点;
index:指定的地址标号,reg属性值的哪一个
返回值:返回一个对应的虚拟地址
//
个人理解总结:
1、从各种OF函数的源码实现分析来看,Linux内核对设备树的描述和设备树上节点的父子关系以及具体的属性值都是用链表或哈希表嵌套实现的。
2、其实设备树只是提供具体外部设备的属性信息,Linux下编写设备驱动的方式依旧不变;只是可以通过内核提供的OF函数接口读取属性信息来驱动对应的外设。
四、Linux驱动pinctrl和gpio子系统
pinctrl驱动子系统:
Linux提供的一套方便驱动GPIO的驱动工具。在设备树上按照pinctrl规定的规则描述一个GPIO设备节点,提供GPIO及其电器属性值;在驱动内核源码里,Linux自动读取这些属性值来初始化GPIO寄存器,无需我们关心。(不同厂家的SOC具体的语法规则不同,但整体步骤相似)
pinctrl子系统主要工作内容如下:
① 获取设备树中的pin信息;
② 根据获取到的pin信息来设置pin的复用功能
③ 根据获取到的pon信息来设置pin的电气特性
///
设备树中添加pinctrl节点:
①
创建对应的节点(以NXP为例):在节点iomux里的子节点imx6ul-evk下面独立在创建一个imx6ul-evk的子节点,名字必须是pinctrl_开头。
② 添加“fsl,pins”属性:这个属性名根据不同SOC有不同,但绝对不可以自己随便给!!
③ 在“fls,pins”里添加具体的GPIO属性信息:就是指定具体的GPIO寄存器地址以及配置的值
///
gpio操作子系统:
pinctrl用来配置GPIO口具体的复用功能和电气属性,那么gpio子系统则是用来具体操作gpio口的(将GPIO与具体的功能外设联系起来);gpio子系统提供了API方便驱动开发者使用GPIO口。
在设备树上添加gpio节点:
① 在/目录下添加一个节点:名字随意
② 在新添加的节点上,添加引用的pinctrl信息:pinctrl-names="xxx"; pinctrl-0=<&pinctrl_xxx>;
③ 节点上添加gpio的属性信息(表示使用那个gpio以及有效电平):gpios=<&gpiox pin
GPIO_ACTIVE_LOW/GPIO_ACTIVE_HIGHT>;
///
与gpio子系统相关的OF函数:
1、int of_gpio_named_count(struct device_node *np,const char* propname);
//获取目标节点定义的gpio的个数,其实是读取了"#gpio-cells"
参数解释:
*np:目标设备节点
*propname:要统计的GPIO属性
返回值:正值,统计到的gpio定义数量
2、int of_gpio_count(struct device_node *np); //获取目标节点定义的“gpios”这个属性值的数量
参数解释:
*np:目标访问的设备节点
返回参数:正值,统计到的gpio的数量
3、int of_get_named_gpio(struct device_node *np,const char *propname,int
index); //用于获取gpio节点对应驱动的编号,gpio子系统提供的API需要使用到这个编号!!
参数解释:
*np:目标设备节点
*propname:包含要获取GPIO信息的属性名,根据具体的自定义
index:GPIO索引,因为一个属性里面,可能包含多个GPIO,此参数指定要获取哪个GPIO编号(编号从0开始)
返回值:正值,获取到的编号
///
gpio子系统提供的API函数:(专门获取设备树上gpio节点信息)
1、int gpio_request(unsigned gpio,const char *label); //向Linux内核申请使用目标gpio口
参数解释:
gpio:要申请的gpio标号,需要使用函数of_get_named_gpio来获取
*label:给这个gpio起个名字;
返回值:0=申请成功
2、void gpio_free(unsigned gpio); //释放不再使用的gpio
参数解释:
gpio:要释放资源的gpio的标号
返回值:无
3、int gpio_direction_input(unsigned gpio); //将目标gpio配置为数字量输入功能
参数解释:
gpio:要设置为输入的gpio标号
返回值:0=设置成功
4、int gpio_direction_output(unsigned gpio,int value);
//将目标gpio配置为数字量输出,并设置输出电平
参数解释:
gpio:目标的gpio标号;
value:初始化电平值
返回值:0=配置成功
5、#define gpio_get_value __gpio_get_value(unsigned gpio);
//这个宏定义用于获取指定的gpio当前的电平
参数解释:
gpio:目标的gpio对应的标号
返回值:非负数=获取到的gpio的值
6、#define gpio_set_value __goio_set_value(unsigned gpio,int value);
//这个宏用于控制gpio口的输出电平
参数解释:
gpio:要控制的gpio标号
value:控制输出的电平值
返回值:无
///