设备树(DeviceTree)
描述一个硬件平台的板级细节,设备树可以被 bootloader(uboot)传递到内核,内核从中获取设备树中的硬件信息。
Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。
在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。
引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。
可描述的信息
- CPU数量和类别
- 总线和桥
- 中断控制器和中断使用情况
- 内存及地址和大小
- 外设连接
- GPIO控制器和GPIO使用情况
- CLOCK控制器和CLOCK使用情况
几个缩写理解
- DTS:是指 .dts 格式的文件,是一种 ASII 文本格式的设备树描述,也是我们要编写的设备树源码,一般一个 .dts 文件对应一个硬件平台,位于 Linux 源码的 /arch/arm/boot/dts 目录下。
- DTC:是指编译设备树源码的工具,一般情况下,需要手动安装这个编译工具。
- DTB:是设备树源码编译生成的文件。
- .dts:设备树源文件。
- .dtsi:设备树头文件。
- .dtb:设备树可执行文件。
设备树原理
(.dts为板级定义,.dsti为Soc级定义,一个Soc可以有多个不同的电路板,多个.dts的共同部分保保存至.dtsi中,类似.h文件)
语法结构
1个根节点“/”,根节点下一系列子节点,子节点也包含一系列子节点,各节点有一系列属性;
属性的值类型
- 空属性
- 字符串,使用双引号
- 字符串列表,字符串之间逗号隔开
- u32/u64(cells),使用尖括号
- 二进制数,字节序列,使用方括号
- 各种组合都会隔开
结构理解
- { }包围起来的称之为节点,key=value代表节点属性,每一个设备节点都需要一个compatible属性;
- 节点名:格式< name > [@unit_addr] 设备地址不同,节点名可相同;如果节点没有reg属性,则必须省略,@unit-addr。例如:apb@8000000{ };
例外,chosen节点用于firmware传递数据给OS(bootloader传参内核):chosen{ bootargs = “console=ttyPS1,115200…”;}; - 引用(取别名)
(1) 声明:别名:节点名
(2) 访问:<&别名>
(3) 作用:查找方便,编译时,相同节点不同属性信息被合并,相同节点相同属性会被重写(覆盖)
可在板级.dts文件中增改:&uart0{ status = “okay”; }; - 节点属性property
一般由key=value 键值对组成,节点属性类型挺多的
节点属性类型(标准属性)
规范定义的属性:compatible、addr、interrupt,内核初始化启动自动解析生成
内核定好的,通用的,有默认意义的属性,内核使用提取函数解析,“mac_addr”、“gpio”“clock”、“power”、“regulator”
compatible兼容性
设备节点一定要有该属性,这将作为驱动和设备的匹配依据,其值可不只有一个,匹配上了,可通过相应函数提取信息;
格式:compatible = “< manufacturer >,< model >”[,"< m >,< m >"];
厂家名,特定的设备型号,[ ]可选
- 驱动中用于匹配的结构使用的compatible和设备树中的一模一样
- 别名匹配对compatible中字符串里的第二个字段敏感
address地址属性
虽然i2c@021a0000,名字后面跟了地址,但正式设置地址在reg属性中
- #address-cells=< CNT >,描述子节点中首地址的cell数量
- #size-cells=< CNT >,描述子节点reg中地址长度的cell数量
- reg=< addr length >:addr基地址,length为长度,两者个数可变,addr由父节点的#address-cells个u32值组成;lenth由父节点的#size-cells个u32值组成
例如:节点A的#address-cells为2,#size-cells为1,则节点A的子节点B的reg为’<addr,addr,length>’
interrupt中断属性
一个计算机系统大量设备都是通过中断请求CPU服务的,因此设备节点中就需要指定中断号
- interrupt-controller:一个空属性,声明此node接收中断号,此node是中断控制器
- #interrupt-cells:中断控制器节点的属性,用于描述子节点中interrupt属性使用了父节点中interrupt属性的具体哪个值
如父节点的该属性值为3,则interrupt一个cell为<中断域 中断 触发方式>,如父节点的该属性值为2,则是<中断 触发方式> - interrupt-parent:标识此设备节点属于哪一中断控制器,如未设置,则主动依附父节点
(1) 当#interrupt-cells为3时,interrupt=<0,168,4>
第一个cell为中断类型,0为SPI中断,1为PPI中断
第二个cell代表具体的类型中断号:PPI:CPU私有外设中断[0-15];SPI:公用外设中断[0-987]
第三个cell代表中断触发标志
bit[3:0] 上升沿1 下降沿2 高电平4 低电平8
bit[15:8] PPI中断CPU掩码
(2) 当#interrupt-cells为2时,interrupt=<2,4>
第一个cell为中断类型及中断号
ID0-ID15:SGI中断(软件触发中断) ID16-ID31:PPI ID32-ID1019:SPI
第二个cell同上cells为3时第三个cell含义
gpio属性
- gpio-controller:用来说明该节点描述的是一个gpio控制器
- #gpio-cells:用于描述gpio使用节点的属性一个cell的内容,即’属性=<&引用GPIO节点别名 GPIO标号 工作模式>’
驱动自定义key属性
基本设备节点类型
所有设备树文件均要包含一个根文件,并且所有设备树文件均应在根节点下存在以下节点:
- 1个/cpus节点
- 至少一个/memory节点
Root node
devicetree有一个单独的根节点,所有其他设备节点都是它的后代。根节点的完整路径为/。
/aliases节点
设备树文件可能具有一个别名节点(/aliases),该节点定义一个或多个别名属性。别名节点应位于设备树的根节点,并且具有节点名称/别名。/aliases节点的每个属性都定义了一个别名。属性名称指定别名。属性值指定设备树中节点的完整路径。例如,属性serial0 = "/simple-bus@fe000000/serial@llc500"定义了别名serial0
/memory节点
所有设备树都需要内存设备节点,并描述系统的物理内存布局。如果系统具有多个范围的内存,则可以创建多个内存节点,或者可以在单个内存节点的reg属性中指定范围。
例如:
memory {
reg = <0x40000000 0x10000000>; 起始地址0x40000000 长度0x10000000(32MB)
};
/chosen 节点
/chosen 节点不代表系统中的实际设备,而是描述了在运行时由系统固件选择或指定的参数。它应该是根节点的子节点。
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
/cpus 节点
所有设备树均需要/cpus/cpu节点。它并不代表系统中的真实设备,而是作为代表系统cpu的子cpu节点的容器。
dtc命令
手动编译
dtc -I dts -O dtb -o B.dtb A.dts #编译
dtc -I dtb -O dts -o A.dts B.dtb #反编译
自动编译
make dtbs
常用设备树API
设备树节点结构体
struct device_node //结构体描述设备树节点
关注: const char *name; //节点名
const char *type; //设备类型
phandle phandle;
const char *full_name; //全路径节点名
struct fwnode_handle fwnode;
struct property *properties; //属性结构体
struct device_node *parent; //父节点
struct device_node *child; //子节点
查找节点API
-
of_find_compatible_node(struct device_node *from,const char *type, const char *compatible);
@from:指向开始路径节点,NULL为从根节点开始;@type:设备类型;@compat:设备树中该节点compatible属性值首地址 -
of_find_matching_node(struct device_node *from,const struct of_device_id *matches);
@matches:指向设备ID表of_device_id,ID表必须以NULL结束 -
of_find_node_by_path(const char *path);
@path:全路径节点名或引用 -
of_find_node_by_name(struct device_node *from, const char *name);
@name:节点名查找
提取属性API
-
提取指定属性的值:of_find_propety(const struct device_node *np,const char *name,int *lenp);
@np:设备节点指针;@name:属性名称;@lenp:属性值字节数 -
提取属性值中数据的数量:of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size);
@elem_size:每个数据的单位(字节数) -
提取属性值中指定标号的32位数据值:of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value);
@index:标号;@out:输出参数 -
提取属性值中的字符串:f_property_read_string(const struct device_node *np, const char *propname, const char **out_string);
@out_string:输出参数,执行字符串
提取addr属性API
-
提取默认属性’#address_cells’和’#size_cells’
int of_n_addr_cells(struct device_node *np);
int of_n_size_cells(struct device_node *np); -
提取I/O地址:of_get_address(struct device_node *dev, int index, u64 *size,unsigned int *flags);
@size:I/O口地址长度 -
提取resource:int of_address_to_resource(struct device_node *dev, int index,struct resource *r);
-
提取gpio属性,得到gpio口编号:int of_get_named_gpio(struct device_node *np, const char *propname, int index);
-
提取中断号:int of_irq_get(struct device_node *np, int index);
@index:中断的编号;@返回值:中断号
设备树加载
准备 uboot传参两种方式
- tag传递参数列表给Linux内核,只需知道内核的地址
bootm < uImage_addr > - dtb镜像,使用设备树
bootm < uImage_addr > < initrd_addr > < dtb_addr >
加载过程
- kernel 入口处获取uboot传来的dtb镜像基地址
- 通过early_init_dt_scan( )函数来获取kernel初始化时需要的bootargs和cmd_line等系统引导参数
- 调用unflatten_device_tree( )函数来解析dtb文件,构建一个由device_node结构连接而成的单向链表,并使用全局变量of_allnodes保存链表头指针
- 内核调用OF的API,获取of_allnodes链表信息来初始化内核其他子系统、设备等
设备树转换
dtb格式
- alignment gap:对齐间隙
- memory reserve map:描述保留的内存部分,被保留不应覆盖
- device_tree structure:属性的value部分
- device_tree strings:dtb中大量重复字符串,比如model,compatible等等,为节省空间,将这些字符串统一放在某个地址,需要时使用索引查看,一般为属性的key部分
dtb_header
整个dtb的大致描述信息
struct fdt_header {
fdt32_t magic; /* magic word FDT_MAGIC 魔数,固定值,大端为0xd00dfeed,小端为0xedfe0dd0*/
fdt32_t totalsize; /* total size of DT block 设备树大小,实际所占内存空间*/
fdt32_t off_dt_struct; /* offset to structure struct部分所在内存相对头部的偏移*/
fdt32_t off_dt_strings; /* offset to strings strings部分所在内存相对头部的偏移*/
fdt32_t off_mem_rsvmap; /* offset to memory reserve map memory reserve map部分所在内存相对头部的偏移*/
fdt32_t version; /* format version 设备树版本*/
fdt32_t last_comp_version; /* last compatible version */
/* version 2 fields below */
fdt32_t boot_cpuid_phys; /* Which physical CPU id we're
booting on */
/* version 3 fields below */
fdt32_t size_dt_strings; /* size of the strings block strings部分大小*/
/* version 17 fields below */
fdt32_t size_dt_struct; /* size of the structure block struct部分大小*/
};
设备树转换过程
dtb—>device_node
- setup_machine_fdt( ); //根据传入的dtb首地址完成一些初始化操作,总览信息
- arm_memblock_init( ); //内存相关函数,为设备树保留相应的内存空间,保证不被覆盖
- unflatten_device_tree( ); //对设备树的具体解析
device_node—>platform_device
- decide_node转换为platform_device的条件:
- 节点中必须有compatible属性;
- 只对根中的第一级节点(/xx)注册成platform_device,对它们的子节点并不处理;
- 当一个节点的compatible属性有(“simple-bus”、“simple-mfd”、“isa”、“arm,amba-bus”)中特殊值之一,并且自己已成功注册platform_device,则其子节点(含compatible属性)的也可以转换为platform_device;
- 根节点 / 是不会处理的。
- platform_device 与 devie_node的关联
struct platform_device中含有struct device,struct device中含有struct device_node*:
- 以platform_device.device->device_node关联
- resources部分 I/O(reg属性)、中断(interrupt属性)—>(转换)—>struct resources
注意:并不是所有节点全都转换成platform_device平台总线设备的,总线I2C、SPI节点下的子节点不转换,而应该给对应的总线驱动来处理,这点下节讨论!
--有参考,侵删--