u-boot fdt的应用
简单的说,如果要使用Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把这些信息组织成Device Tree source file。通过DTC(Device Tree Compiler),可以将这些适合人类阅读的Device Tree source file变成适合机器处理的Device Tree binary file(有一个更好听的名字,DTB,device tree blob)。在系统启动的时候,boot program(例如:firmware、bootloader)可以将保存在flash中的DTB copy到内存(当然也可以通过其他方式,例如可以通过bootloader的交互式命令加载DTB,或者firmware可以探测到device的信息,组织成DTB保存在内存中),并把DTB的起始地址传递给client program(例如OS kernel,bootloader或者其他特殊功能的程序)。对于计算机系统(computer system),一般是firmware->bootloader->OS,对于嵌入式系统,一般是bootloader->OS。
1. u-boot对fdt(flattened device tree)的支持
实现:只要加入
#define CONFIG_OF_LIBFDT /* Device Tree support */
重新编译u-boot,就可以实现对device tree的支持。
2、dtb在uboot中的位置
dtb可以以两种形式编译到uboot的镜像中。
-
dtb和uboot的bin文件分离
- 如何使能
需要打开CONFIG_OF_SEPARATE宏来使能。 - 编译说明
在这种方式下,uboot的编译和dtb的编译是分开的,先生成uboot的bin文件,然后再另外生成dtb文件。
具体参考《[uboot] (第四章)uboot流程——uboot编译流程》。 - 最终位置
dtb最终会追加到uboot的bin文件的最后面。也就是uboot.img的最后一部分。
因此,可以通过uboot的结束地址符号,也就是_end符号来获取dtb的地址。
具体参考《[uboot] (第四章)uboot流程——uboot编译流程》。
- 如何使能
-
dtb集成到uboot的bin文件内部
- 如何使能
需要打开CONFIG_OF_EMBED宏来使能。 - 编译说明
在这种方式下,在编译uboot的过程中,也会编译dtb。 - 最终位置
注意:最终dtb是包含到了uboot的bin文件内部的。
dtb会位于uboot的.dtb.init.rodata段中,并且在代码中可以通过__dtb_dt_begin符号获取其符号。
因为这种方式不够灵活,文档上也不推荐,所以后续也不具体研究,简单了解一下即可。
- 如何使能
-
另外,也可以通过fdtcontroladdr环境变量来指定dtb的地址
可以通过直接把dtb加载到内存的某个位置,并在环境变量中设置fdtcontroladdr为这个地址,达到动态指定dtb的目的。
u-boot中如何获取dtb
1、整体说明
在u-boot初始化过程中,需要对dtb做两个操作:
- 获取dtb的地址,并且验证dtb的合法性
- 因为我们使用的dtb并没有集成到uboot的bin文件中,也就是使用的CONFIG_OF_SEPARATE方式。因此,在relocate uboot的过程中并不会去relocate dtb。因此,这里我们还需要自行为dtb预留内存空间并进行relocate。关于uboot relocate的内容请参考《[uboot] (番外篇)uboot relocation介绍》。
- relocate之后,还需要重新获取一次dtb的地址。
对应代码common/board_f.c
static init_fnc_t init_sequence_f[] = {
...
#ifdef CONFIG_OF_CONTROL
fdtdec_setup, // 获取dtb的地址,并且验证dtb的合法性
#endif
...
reserve_fdt, // 为dtb分配新的内存地址空间
...
reloc_fdt, // relocate dtb
...
}
后面进行具体函数的分析。
2、获取dtb的地址,并且验证dtb的合法性(fdtdec_setup)
对应代码如下:
lib/fdtdec.c
int fdtdec_setup(void)
{
#if CONFIG_IS_ENABLED(OF_CONTROL) // 确保CONFIG_OF_CONTROL宏是打开的
# ifdef CONFIG_OF_EMBED
/* Get a pointer to the FDT */
gd->fdt_blob = __dtb_dt_begin;
// 当使用CONFIG_OF_EMBED的方式时,也就是dtb集成到uboot的bin文件中时,通过__dtb_dt_begin符号来获取dtb地址。
# elif defined CONFIG_OF_SEPARATE
/* FDT is at end of image */
gd->fdt_blob = (ulong *)&_end;
//当使用CONFIG_OF_SEPARATE的方式时,也就是dtb追加到uboot的bin文件后面时,通过_end符号来获取dtb地址。
# endif
/* Allow the early environment to override the fdt address */
gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
(uintptr_t)gd->fdt_blob);
// 可以通过环境变量fdtcontroladdr来指定gd->fdt_blob,也就是指定fdt的地址。
#endif
// 最终都把dtb的地址存储在gd->fdt_blob中
return fdtdec_prepare_fdt();
// 在fdtdec_prepare_fdt中检查fdt的合法性
}
/* fdtdec_prepare_fdt实现如下 */
int fdtdec_prepare_fdt(void)
{
if (!gd->fdt_blob || ((uintptr_t)gd->fdt_blob & 3) ||
fdt_check_header(gd->fdt_blob)) {
puts("No valid device tree binary found - please append one to U-Boot binary, use u-boot-dtb.bin or define CONFIG_OF_EMBED. For sandbox, use -d <file.dtb>\n");
return -1;
// 判断dtb是否存在,以及是否有四个字节对齐。
// 然后再调用fdt_check_header看看头部是否正常。fdt_check_header主要是检查dtb的magic是否正确。
}
return 0;
}
验证dtb的部分可以参考《[kernel 启动流程] (第四章)第一阶段之——dtb的验证》。
3、为dtb分配新的内存地址空间(reserve_fdt)
relocate的内容请参考《[uboot] (番外篇)uboot relocation介绍》。
common/board_f.c中
static int reserve_fdt(void)
{
#ifndef CONFIG_OF_EMBED
// 当使用CONFIG_OF_EMBED方式时,也就是dtb集成在uboot中的时候,relocate uboot过程中也会把dtb一起relocate,所以这里就不需要处理。
// 当使用CONFIG_OF_SEPARATE方式时,就需要在这里地方进行relocate
if (gd->fdt_blob) {
gd->fdt_size = ALIGN(fdt_totalsize(gd->fdt_blob) + 0x1000, 32);
// 获取dtb的size
gd->start_addr_sp -= gd->fdt_size;
gd->new_fdt = map_sysmem(gd->start_addr_sp, gd->fdt_size);
// 为dtb分配新的内存空间
debug("Reserving %lu Bytes for FDT at: %08lx\n",
gd->fdt_size, gd->start_addr_sp);
}
#endif
return 0;
}
4、relocate dtb(reloc_fdt)
relocate的内容请参考《[uboot] (番外篇)uboot relocation介绍》。
common/board_f.c中
static int reloc_fdt(void)
{
#ifndef CONFIG_OF_EMBED
// 当使用CONFIG_OF_EMBED方式时,也就是dtb集成在uboot中的时候,relocate uboot过程中也会把dtb一起relocate,所以这里就不需要处理。
// 当使用CONFIG_OF_SEPARATE方式时,就需要在这里地方进行relocate
if (gd->flags & GD_FLG_SKIP_RELOC)
// 检查GD_FLG_SKIP_RELOC标识
return 0;
if (gd->new_fdt) {
memcpy(gd->new_fdt, gd->fdt_blob, gd->fdt_size);
// relocate dtb空间
gd->fdt_blob = gd->new_fdt;
// 切换gd->fdt_blob到dtb的新的地址空间上
}
#endif
return 0;
}
boot中对FDT的支持
如果要使用tag,需boot和kernel同时支持,那么用FDT取代TAG也同样如此.
uboot中对FDT的支持:
1) uboot代码中已经对fdt有支持,可以通过开启CONFIG_OF_LIBFDT配置,实现FDT支持,如此会省掉一部分工作。例如:fdt调试命令可以直接使用。
2) 如前所述dtb文件被烧录到flash上。在Uboot引导内核启动之前会将dtb拷贝到内存上,并对dtb的内容进行修改,这一步是为了支持对环境变量的动态修改。
以下介绍u-boot中如何修改dts文件中定义的内容:重点观察example_plat_version.dts文件里chosen节点中bootargs参数的修改,关键函数fdt_chosen:
int fdt_chosen(void *fdt, int force) { int nodeoffset; char *str; const char *path; nodeoffset= fdt_path_offset(fdt, "/chosen"); if(nodeoffset<0) { nodeoffset=fdt_add_subnode(fdt,0,"chosen"); if(nodeoffset<0) { return nodeoffset; } } str = getenv("bootargs"); path = fdt_set_prop(fdt,nodeoffset,"bootargs",str,strlen(str)+1); return 0; }
上述函数的fdt_path_offset(fdt,”chosen”)获取的是dts文件中的:
chosen{
bootargs = "console=ttyS0,115200 mem=100M root=/dev/mtdblock0 init=/linuxrc";
};
要想修改bootargs则调用fdt_setprop_string;
Ps:其中fdt_setprop_string等接口是FDT提供的标准接口,boot和kernel源码中都可调用。
3)将dtb从flash拷贝到dram上。假设拷贝到dram=0x200100的地址,则uboot跳转到kernel时再通过cpu的通用寄存器r2告知系统这个地址,这一点与tag没有分别。至此完成boot下对fdt的支持。
Uboot下调试FDT
Uboot支持标准的fdt命令修改dtb的内容。
1 举例:常用调试命令
1>fdt list :列出系统中所有节点。即上面dts文件中的节点。
#fdt list
/{
#address-cells = <0x1000000>;
#size-cells = <0x1000000>;
compatible = "example,**";
model = "example plat evm Board";
chosen{
};
aliases{
};
memory{
};
cpus{
};
apb@80000000{
};
};
2>列出aliases节点:
#fdt list /aliases
aliases{
serial0 = "/apb@80000000/uart@8005000";
nand = "/apb@90000000/nand@9001000";
i2c0 = "/ahb@70000000/i2c@7001000";
};
3>查看boot分区信息:
#fdt list nand
nand@9001000{
compatible = "example,nand";
#address-cells = <0x1000000>;
#size-cells = <0x1000000>;
partition@0{
};
partition@1{
};
};
#fdt list nand/partition@0
partition@0{
reg = <0x0 0x40000>;
label = "uboot";
};
2 修改dtb举例:
#fdt list /memory
memory{
device_type = "memory";
reg = <0x2000,0xe007>;
};
#fdt set /memory reg <0x200000 0x6e00000>
#
#fdt list /memory
memory{
device_type = "memory";
reg = <0x2000,0xe006>;
};
1>修改前 reg= <0x2000 0xe007>(字节序反了):实际表示linux 系统内存从0x200000开始大小为0x7e00000
2>修改后 reg=<0x2000 0xe006>:实际表示linux 系统内存从0x200000开始大小为0x6e00000
fdt调试和验证的工具方法:
驱动开发时与设备注册、设备树相关的调试方法,彼此间没有优先级之分,每种方法不一定是最优解,但可以作为一种debug查找问题的手段,快速定位问题原因。例如在芯片验证时,不同时钟频率下系统启动情况摸底时,U-Boot fdt命令可以方便快捷的帮助我们完成这个实验。
#1. dtc工具
dtc可以使用宿主机提供的亦可以使用kernel提供的。这个工具是将已编译的dtb文件反汇编。
#2. U-Boot fdt command
驱动代码在debug期间,若希望更改外设模块的设备树属性时,在不改变存储设备中dtb文件的前提下,进入到U-Boot的命令行界面,通过U-Boot的fdt命令来实现。例如修改外设时钟源、修改外设时钟名、status属性等。为了使U-Boot支持fdt命令需要打开CONFIG_OF_LIBFDT。
1、fdtdec_setup 函数
fdtdec_setup:如果u-boot中使用设备树,则需处理一些相关工作
第一篇文章查看.config配置文件知道关于设备树就有下面几个定义:
CONFIG_OF_CONTROL=y
CONFIG_OF_SEPARATE=y
CONFIG_OF_TRANSLATE=y
CONFIG_OF_LIBFDT=y
所以去掉宏定义之后的函数定义就是:
/* file: lib/fdtdec.c */
int fdtdec_setup(void)
{
int ret;
/* Allow the board to override the fdt address. */
gd->fdt_blob = board_fdt_blob_setup();
/* Allow the early environment to override the fdt address */
gd->fdt_blob = map_sysmem
(env_get_ulong("fdtcontroladdr", 16,
(unsigned long)map_to_sysmem(gd->fdt_blob)), 0);
ret = fdtdec_prepare_fdt();
if (!ret)
ret = fdtdec_board_setup(gd->fdt_blob);
return ret;
}
函数先是调用board_fdt_blob_setup来设置gd结构体的fdt_blob成员,简化宏定义之后函数如下:
/* file: lib/fdtdec.c */
__weak void *board_fdt_blob_setup(void)
{
void *fdt_blob = NULL;
/* FDT is at end of image */
fdt_blob = (ulong *)&_end;
return fdt_blob;
}
由前面的镜像组成可以知道u-boot设备树文件是放在u-boot.bin的末尾,所以取它的地址返回即可。 后面继续调用env_get_ulong从环境变量中获取u-boot设备树的地址,如果该环境变量没有被设置则返回原本的默认值(unsigned long)map_to_sysmem(gd->fdt_blob)。最终继续调用fdtdec_prepare_fdt函数来打印一些信息:
/* file: lib/fdtdec.c */
int fdtdec_prepare_fdt(void)
{
if (!gd->fdt_blob || ((uintptr_t)gd->fdt_blob & 3) ||
fdt_check_header(gd->fdt_blob)) {
#ifdef CONFIG_SPL_BUILD
puts("Missing DTB\n");
#else
puts("No valid device tree binary found - please append one to U-Boot binary, use u-boot-dtb.bin or define CONFIG_OF_EMBED. For sandbox, use -d <file.dtb>\n");
# ifdef DEBUG
if (gd->fdt_blob) {
printf("fdt_blob=%p\n", gd->fdt_blob);
print_buffer((ulong)gd->fdt_blob, gd->fdt_blob, 4,
32, 0);
}
# endif
#endif
return -1;
}
return 0;
}
节点node
{}包围起来的结构称之为节点,dts中最开头的/ {},称为根节点。
在节点中,以 key = value
代表节点属性。
树中每个表示一个设备的节点都需要一个 compatible 属性。
节点名 name
- 节点名称:每个节点名格式为:
<name>[@<unit_address>]
,其中:- :设备名,就是一个不超过31位的简单 ascii 字符串,节点的命名应该根据它所体现的是什么样的设备。
- <unit_address> :设备地址,用来唯一标识一共节点。没有指定<unit_address>时,同级节点命名必须是唯一的;但只要<unit_address>不同,多个节点也可以使用一样的通用名称。
下面是典型节点名的写法:
/ { model = "Freescale i.MX23 Evaluation Kit"; compatible = "fsl,imx23-evk", "fsl,imx23"; memory { reg = <0x40000000 0x08000000>; }; // 注意这里 apb@80000000 { ... }; }
上面的节点名是apb,节点路径是/apb@80000000 ,这点要注意,因为根据节点名查找节点的API的参数是不能有"@xxx"这部分的。
Linux中的设备树还包括几个特殊的节点:比如chosen,chosen节点不描述一个真实设备,而是用于firmware传递一些数据给OS,比如bootloader传递内核启动参数给内核
/include/ "zynq-7000.dtsi" / { model = "Zynq ZC702 Development Board"; compatible = "xlnx,zynq-zc702", "xlnx,zynq-7000"; ... chosen { bootargs = "console=ttyPS1,115200 earlyprintk"; }; };
引用
当我们找一个节点的时候,我们必须书写完整的节点路径,这样当一个节点嵌套比较深的时候就不是很方便。所以,设备树允许我们用下面的形式为节点标注引用(起别名),借以省去冗长的路径。
标号引用常常还作为节点的重写方式,用于修改节点属性。
- 格式:
- 声明别名:
别名 : 节点名
- 访问 : &
别名
- 声明别名:
编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的相同的属性会被重写(覆盖前值),使用引用可以避免移植者四处找节点,直接在板级.dts增改即可。
/include/ "imx53.dtsi" / { model = "Freescale i.MX53 Automotive Reference Design Board"; compatible = "fsl,imx53-ard", "fsl,imx53"; memory { reg = <0x70000000 0x40000000>; }; eim-cs1@f4000000 { #address-cells = <1>; #size-cells = <1>; compatible = "fsl,eim-bus", "simple-bus"; reg = <0xf4000000 0x3ff0000>; lan9220@f4000000 { compatible = "smsc,lan9220", "smsc,lan9115"; reg = <0xf4000000 0x2000000>; phy-mode = "mii"; interrupt-parent = <&gpio2>; // 直接使用引用 vdd33a-supply = <®_3p3v>; }; }; regulators { compatible = "simple-bus"; reg_3p3v: 3p3v { // 定义一个引用 compatible = "regulator-fixed"; regulator-name = "3P3V"; }; }; ... // 引用一个节点,新增/修改其属性。 ®_3p3v { regulator-always-on; }
1.2 设备树语法
在上一小节,我们将设备树的概念有基本的认知,下面更重要的就是DTS语法了,这里我么结合实际的代码区理解设备树语法。
1.2.1 #include语法
DTS中#include语法和C语言中类似,支持将包裹的文件直接放置在#include位置从而访问到其它文件的数据,如官方设备树内使用的
#include "nuc9xx.dtsi"
另外,也可以用来包含dts文件,如下
#include "imx6ull-14x14-evk.dts"
解析:
(1) 根节点
compatible: 内核通过root节点"/"的compatible属性来判断它启动的是哪个machine。
#address-cells : 子结点(reg属性)需要多少个cell描述地址。
#size-cells : 子结点(reg属性)需要多少个cell描述长度。
interrupt-parent : 标示该节点属于哪个中断控制器,如果没有该属性,则依附于父节点。
(2) cpus节点
#address-cells : 同上
#size-cells : 同上
(3) cpu节点
@unit-address: 可选项,设备地址,节点名相同时可以通过这个来区分不同节点。unit-address地址也经常在其对应的reg属性中给出。
reg : region,描述设备地址
格式: reg = <address1 length1 [address2 length2] [address3 length3]>
(4) serial节点
compatible: 外设节点上的compatible属性用于驱动和设备的绑定(匹配)
reg: 外设基地址和偏移量 ,比如:reg = <0x101f1000 0x1000 >
interrupts: 中断号和标识(上升沿,下降沿等), 里面多少个值要根据中断控制器的#interrupt-cells属性来决定。而#interrupt-cells属性值要由中断控制器的类型决定。
中断控制器类型:
GIC: Generic Interrupt Controller(通用中断控制器)
中断类型,中断号,标识(上升沿,下降沿等)
VIC : Vectored Interrupt Controller(向量中断控制器)
中断号
NVIC:Nested Vectored Interrupt Controller(内嵌向量中断控制器)
中断号,中断优先级
参考: Documentation\devicetree\bindings\interrupt-controller
(5) interrupt-controller节点
compatible: 中断控制器类型,查看上面路径下的文件来获知。
reg:同上
interrupt-controller:空属性,用来声明这个节点接收中断信号。
#interrupt-cells:标识该控制器需要几个cell来描述中断,其实就是决定了interrupts属性需要几个cell
(6) external-bus节点
ranges: 地址转换表,每一行都包含子地址、父地址、在子地址空间内的区域大小。
ranges属性值为空的话,表示1:1映射。
ranges属性值的格式 <local地址, parent地址, size>
local地址的个数取决于当前含有ranges属性的节点的#address-cells属性的值。
parent地址的个数取决于父节点的#address-cells的值。
size取决于当前含有ranges属性的节点的#size-cells属性的值。
(7) rtc节点
reg: i2c设备地址