簡單的說,如果要使用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設備地址