概述
設備樹(Device tree)是一套用來描述硬件屬相的規則。ARM Linux采用設備樹機制源於2011年3月份Linux創始人Linus Torvalds發的一封郵件,在這封郵件中他提倡ARM平台應該參考其他平台如PowerPC的設備樹機制描述硬件。因為在此之前,ARM平台還是采用舊的機制,在kernel/arch/arm/plat-xxx目錄和kernel/arch/arm/mach-xxx目錄下用代碼描述硬件,如注冊 platform設備,聲明設備的resource等。因為這些代碼都是用來描述芯片平台及板級差異的,所以對於內核來講都是垃圾代碼。因為嵌入式平台中很多公司的芯片采用的都是ARM架構,隨著Android的成功,這些代碼越來越多。據說常見的平台如s3c2410板級目錄下邊的代碼有數萬行,難怪Linux Torvalds會說“this whole ARM thing is a fucking pain in the ass”。
內核中關於設備樹的文檔位於kernel/Documentation/devicetree/目錄。設備樹是Power.org組織定義的一套規范,規范文檔可以在官網上找到,目前最新的版本是https://www.power.org/documentation/epapr- version-1-1/。內核中設備樹相關的函數都是以of開頭的,我推測原因是設備樹機制是源於IEEE 1275 Open Firmware standard規范的,相關的代碼都是繼承下來的。如果想快速了解下設備樹怎麼用,可以參考http://devicetree.org /Device_Tree_Usage。
設備樹是從軟件使用的角度描述硬件的,不是從硬件設計的角度描述的。我們在寫設備樹時沒有必要按照硬件邏輯生搬硬套,也不要指望通過閱讀設備樹弄清楚硬件是如何設計的。對於軟件可以自動識別的硬件,如USB設備,PCI設備,也是沒有必要通過設備樹描述的。
我個人覺得規范內容是可以分為兩個層次的。第一層是關於設備樹組織形式的,如設備樹結構,節點名字的構成等,第一個層次是基礎,是理解第二個層次的前提。第二層是關於設備樹內容的,如多核CPU怎樣描述,一個具體的設備如何描述。第二層可以看成是第一層的具體應用。相對來說第二層內容更多,更具體,根據描述的內容不同,定義規范的方式也有差別,比如關於CPU,內存,中斷這些基礎的內容,是在epapr中說明的,而關於外設的規范是在專門的地方說明的。
DTS(Device tree syntax,另一種說法是Device tree source)是設備樹源文件,為了方便閱讀及修改,采用文本格式。DTC(Device tree compiler)是一個小工具,負責將DTS轉換成DTB(Device tree blob)。DTB是DTS的二進制形式,供機器使用。使用中,我們首先根據硬件修改DTS文件,然後在編譯的時候通過DTC工具將DTS文件轉換成 DTB文件,然後將DTB文件燒寫到機器上(如emmc,磁盤等存儲介質)。系統啟動時,fastboot(或者類似的啟動程序,如Uboot)在啟動內核前將DTB文件讀到內存中,跳轉到內核執行的同時將DTB起始地址傳給內核。內核通過起始地址就可以根據DTB的結構解析整個設備樹。說設備樹的規范可以分成兩個層次,是針對DTS的,關於DTB的結構不在此范圍內。DTB僅僅是為了方便機器使用而對DTS的轉換而已(也可以說DTS僅是為了方便人類使用而對DTB的一種描述)。
設備樹首先是一個樹形結構,並且是一棵樹。除了根節點外其他子節點都有唯一的父節點,節點下可以有子節點和屬性(子節點可以看成是樹枝,屬性可以看成是葉子)。屬性由名字和值組成(名字是必須的,但是值不是必須的,如果只要根據是否存在這個屬性就可以表示我們想要的功能,那麼可以不需要有值)。
下邊是我們從內核代碼中截取的一個DTS片段。“/”表示根節點。“model = "Newflow AM335x NanoBone"”是根節點下邊的屬性。“cpus”是根節點的一個子節點。“cpu0-supply = <&dcdc2_reg>”是“cpu@0”子節點下的屬性。節點下的屬性用來表示節點的特性,子節點和父節點具有一定的從屬關系。真實的硬件不可能是這樣規則的樹形結構,所以設備樹僅是軟件開發人員為了描述硬件而做的一個近似表示而已,連抽象都算不上。
/ {
model = "Newflow AM335x NanoBone";
compatible = "ti,am33xx";
cpus {
cpu@0 {
cpu0-supply = <&dcdc2_reg>;
};
};
memory {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
leds {
compatible = "gpio-leds";
led@0 {
label = "nanobone:green:usr1";
gpios = <&gpio1 5 0>;
default-state = "off";
};
};
};
memory&chosen節點
根節點那一節我們說過,最簡單的設備樹也必須包含cpus節點和memory節點。memory節點用來描述硬件內存布局的。如果有多塊內存,既可以通過多個memory節點表示,也可以通過一個memory節點的reg屬性的多個元素支持。舉一個例子,假如某個64位的系統有兩塊內存,分別是
• RAM: 起始地址 0x0, 長度 0x80000000 (2GB)
• RAM: 起始地址 0x100000000, 長度 0x100000000 (4GB)
對於64位的系統,根節點的#address-cells屬性和#size-cells屬性都設置成2。一個memory節點的形式如下(還記得前幾節說過節點地址必須和reg屬性第一個地址相同的事情吧):
memory@0 {
device_type = "memory";
reg = <0x000000000 0x00000000 0x00000000 0x80000000
0x000000001 0x00000000 0x00000001 0x00000000>;
};
兩個memory節點的形式如下:
memory@0 {
device_type = "memory";
reg = <0x000000000 0x00000000 0x00000000 0x80000000>;
};
memory@100000000 {
device_type = "memory";
reg = <0x000000001 0x00000000 0x00000001 0x00000000>;
};
chosen節點也位於根節點下,該節點用來給內核傳遞參數(不代表實際硬件)。對於Linux內核,該節點下最有用的屬性是bootargs,該屬性的類型是字符串,用來向Linux內核傳遞cmdline。規范中還定義了stdout-path和stdin-path兩個可選的、字符串類型的屬性,這兩個屬性的目的是用來指定標准輸入輸出設備的,在linux中,這兩個屬性基本不用。
memory和chosen節點在內核初始化的代碼都位於 start_kernel()->setup_arch()->setup_machine_fdt()->early_init_dt_scan_nodes() 函數中(位於drivers/of/fdt.c),復制代碼如下(本節所有代碼都來自官方內核4.4-rc7版本):
1078 void __init early_init_dt_scan_nodes(void)
1079 {
1080 /* Retrieve various information from the /chosen node */
1081 of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
1082
1083 /* Initialize {size,address}-cells info */
1084 of_scan_flat_dt(early_init_dt_scan_root, NULL);
1085
1086 /* Setup memory, calling early_init_dt_add_memory_arch */
1087 of_scan_flat_dt(early_init_dt_scan_memory, NULL);
1088 }
of_scan_flat_dt函數掃描整個設備樹,實際的動作是在回調函數中完成的。第1081行是對chosen節點操作,該行代碼的作用是將節點下的bootargs屬性的字符串拷貝到boot_command_line指向的內存中。boot_command_line是內核的一個全局變量,在內核的多處都會用到。第1084行是根據根節點的#address-cells屬性和#size-cells屬性初始化全局變量 dt_root_size_cells和dt_root_addr_cells,還記得前邊說過如果沒有設置屬性的話就用默認值,這些都在 early_init_dt_scan_root函數中實現。第1087行是對內存進行初始化,復制early_init_dt_scan_memory 部分代碼如下:
893 /**
894 * early_init_dt_scan_memory - Look for an parse memory nodes
895 */
896 int __init early_init_dt_scan_memory(unsigned long node, const char *uname,
897 int depth, void *data)
898 {
899 const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
900 const __be32 *reg, *endp;
901 int l;
902
903 /* We are scanning "memory" nodes only */
904 if (type == NULL) {
905 /*
906 * The longtrail doesn't have a device_type on the
907 * /memory node, so look for the node called /memory@0.
908 */
909 if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
910 return 0;
911 } else if (strcmp(type, "memory") != 0)
912 return 0;
913
914 reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l);
915 if (reg == NULL)
916 reg = of_get_flat_dt_prop(node, "reg", &l);
917 if (reg == NULL)
918 return 0;
919
920 endp = reg + (l / sizeof(__be32));
921
922 pr_debug("memory scan node %s, reg size %d,\n", uname, l);
923
924 while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
925 u64 base, size;
926
927 base = dt_mem_next_cell(dt_root_addr_cells, ®);
928 size = dt_mem_next_cell(dt_root_size_cells, ®);
929
930 if (size == 0)
931 continue;
932 pr_debug(" - %llx , %llx\n", (unsigned long long)base,
933 (unsigned long long)size);
934
935 early_init_dt_add_memory_arch(base, size);
936 }
937
938 return 0;
939 }
第914行可以看出linux內核不僅支持reg屬性,也支持linux,usable-memory屬性。對於 dt_root_addr_cells和dt_root_size_cells的使用也能看出根節點的#address-cells屬性和#size- cells屬性都是用來描述內存地址和大小的。得到每塊內存的起始地址和大小後,在第935行調用 early_init_dt_add_memory_arch函數,復制代碼如下:
983 void __init __weak early_init_dt_add_memory_arch(u64 base, u64 size)
984 {
985 const u64 phys_offset = __pa(PAGE_OFFSET);
986
987 if (!PAGE_ALIGNED(base)) {
988 if (size < PAGE_SIZE - (base & ~PAGE_MASK)) {
989 pr_warn("Ignoring memory block 0x%llx - 0x%llx\n",
990 base, base + size);
991 return;
992 }
993 size -= PAGE_SIZE - (base & ~PAGE_MASK);
994 base = PAGE_ALIGN(base);
995 }
996 size &= PAGE_MASK;
997
998 if (base > MAX_MEMBLOCK_ADDR) {
999 pr_warning("Ignoring memory block 0x%llx - 0x%llx\n",
1000 base, base + size);
1001 return;
1002 }
1003
1004 if (base + size - 1 > MAX_MEMBLOCK_ADDR) {
1005 pr_warning("Ignoring memory range 0x%llx - 0x%llx\n",
1006 ((u64)MAX_MEMBLOCK_ADDR) + 1, base + size);
1007 size = MAX_MEMBLOCK_ADDR - base + 1;
1008 }
1009
1010 if (base + size < phys_offset) {
1011 pr_warning("Ignoring memory block 0x%llx - 0x%llx\n",
1012 base, base + size);
1013 return;
1014 }
1015 if (base < phys_offset) {
1016 pr_warning("Ignoring memory range 0x%llx - 0x%llx\n",
1015 if (base < phys_offset) {
1016 pr_warning("Ignoring memory range 0x%llx - 0x%llx\n",
1017 base, phys_offset);
1018 size -= phys_offset - base;
1019 base = phys_offset;
1020 }
1021 memblock_add(base, size);
1022 }
從以上代碼可以看出內核對地址和大小做了一系列判斷後,最後調用memblock_add將內存塊加入內核。