Linux 設備樹詳解
Linux 操作系統早期是針對個人電腦設備而開發的操作系統,而個人電腦處理器產商較為單一(例如只有 Intel,AMD)同時個人電腦產商均使用 Intel 或 AMD 制造的處理器,業界形成了統一的總線/硬件接口標準,所以 Linux 系統只要遵循這些標準即可。
然而隨著 ARM 處理器在嵌入式微型設備上的廣泛應用,Linux 也支持了 ARM 處理器。但是由于 ARM 架構的授權機制使得任何產商都可以制造 ARM 處理器,各產商沒有統一標準,制造出的 ARM 處理器各不相同。
所以 Linux 系統源碼中添加了各產商 ARM 芯片描述代碼,才能支持各產商的芯片,這也導致 Linux 源碼包含了大量的 ARM 芯片描述代碼。
1. 前言
在 Linux 沒有設備樹之前 ARM 架構的板級芯片硬件細節通過 C 源碼的形式編寫在 “arch/arm/plat-xxx” 和 “arch/arm/mach-xxx” 形式命名的文件中,不同的硬件對應不同的文件,這些不可復用的文件參雜在 Linux 內核源碼目錄中。
為了從 Linux 內核源碼中去除芯片描述代碼就引入了設備樹,設備樹的本質是不再使用 C 源碼去描述芯片,而是使用設備樹DTS結構化腳本語法去描述各種芯片。
引入設備樹后不同的芯片還是需要對應不同的設備樹文件,所以設備樹文件也很多,那引入設備樹的意義在哪?在于(使用一種專屬文件去描述)可以做到芯片描述與內核源碼的分離,同時設備樹描述的硬件信息更結構化,更清晰易懂。
需要知道的是設備樹是集成在 OpenFrame 中的開源項目,并不是 Linux 的原創,現在知道 Linux 設備樹中 OF 操作函數命名的由來了吧。
2. 設備樹
設備樹 Device Tree,故名思意就是由各類設備組成的樹,設備樹文件叫做 DTS 即DeviceTree Source。DTS 文件采用樹形結構語法描述板級設備信息,比如芯片上 CPU 數量,DDR 內存基地址,SPI/I2C 接口連接的設備。
設備樹用于描述硬件設備,本質就是用于描述芯片外設的寄存器地址,而芯片外設寄存器都是連接在系統總線上的,所以設備樹樹的主干就是系統總線,如上圖。
3. 設備樹工具
現在把芯片的描述內容從 Linux 內核源碼中分離出來了,并且使用了 .dts
設備樹文件格式,所以芯片描述部分代碼不能再和 Linux 內核源碼一起編譯了,或是說不能再用 GCC 去編譯了。此時需要一個專門編譯器將設備樹文件編譯成二進制文件,這個編譯器叫做 DTC
,編譯后的二進制文件為 .dtb
格式。
總結:設備樹源碼文件 DTS,設備樹編譯器 DTC,DTS編譯后的二進制文件DTB。
3.1 DTC 編譯器
DTC 編譯器和 GCC 一樣由 C 語言編寫而成,其源碼位于內核的 “scripts/dtc” 目錄下,該目錄下默認是沒有 DTC 的可執行文件的,而是編譯 Linux 時再編譯出可執行文件,所以在 “scripts/dtc” 目錄下除了源碼還有一個 Makefile 文件,這個用于構建 DTC 源碼生成可執行文件。
從 Makefile 文件可以看出 DTC 編譯器源碼有 dtc.c,flattree.c,fstree.c,util.c ,ftdput.c 等文件。
3.2 DTC 使用
如果要使用 DTC 編譯 DTS 文件的話只需要進入到 Linux 源碼根目錄下,執行命令 make dtbs
或 make all
即可。執行后 Linux 根目錄下的主 Makefile 文件就會調用 “scripts/dtc” 目錄下的 DTC 可執行文件去編譯設備樹目錄下指定的 .dts
文件并生成 .dtb
文件。
如果只是編譯設備樹的話建議使用 make dtbs
命令,make dtbs
會編譯選中的所有設備樹文件。如果只要編譯指定的某個設備樹,比如全志 F1C200S 的,那可以執行 make suniv-f1c200s-lctech-pi.dtb
命令。
$ make suniv-f1c200s-lctech-pi.dtb
DTC arch/arm/boot/dts/suniv-f1c200s-lctech-pi.dtb
3.3 添加 DTS
打開設備樹文件所在的 “arch/arm/boot/dts/” 目錄,打開該目錄下的 Makefile 文件,可以看到全志 SUNIV 系列芯片相關的設備樹文件。
dtb-$(CONFIG_MACH_SUNIV) += \suniv-f1c100s-licheepi-nano.dtb \suniv-f1c200s-lctech-pi.dtb \suniv-f1c200s-popstick-v1.1.dtb
可知只要配置 CONFIG_MACH_SUNIV 選項為 y 后,使用全志 SUNIV 系列芯片的板子對應的 .dts
文件都會被編譯為 .dtb
文件。
比如要添加一個新的 SUNIV 系列芯片相關的設備樹文件, 只需要新建一個對應的 .dts
文件,再把對應的 .dtb
文件名添加到 dtb-$(CONFIG_MACH_SUNIV) 分支下即可,這樣編譯設備樹的時候就會將對應的 .dts
文件編譯為 .dtb
文件。
4. DTS 語法
設備樹文件和編程語言一樣有一套特定的編寫語法規則,編寫設備樹文件就要遵循這套規則,不建議自己編寫完整的設備樹,而是復制半導體產商提供的設備樹文件再修改定制即可。
4.1 頭文件 dtsi
和 C/C++ 一樣,DTS 設備樹也支持頭文件引用,設備樹頭文件后綴名為 .dtsi
,引用頭文件使用 #include
語句。
設備樹 .dts
可以引用 .dtsi
,.dts
,甚至 C 語言的頭文件 .h
,語法如下所示。
#include "suniv.dtsi"
#include "suniv.dts"
#include "suniv.h"
dtsi 的主要作用
實際應用中 .dtsi
文件用于描述芯片的核心信息(比如 CPU 架構,主頻)以及外設信息(比如 UART,USB,GPIO寄存器地址范圍)。芯片產商會把同一個系列芯片共有的外設信息提煉到一個 .dtsi
文件里,差異化部分的內容分布到具體芯片的 .dts
文件,這樣可減少代碼的冗余。
例如同一系列的芯片他們的 CPU 架構和 CPU 主頻肯定是相同的,那這部分信息就可以提煉到一個全系列芯片共有的 .dtsi
文件。
4.2 設備節點
普通的樹木由主干,枝條和葉子組成,而設備樹由根節點(主干),子節點(枝條),節點屬性(葉子)構成,每個節點屬性記錄著各類設備信息,并按所屬關系依層次排列,就像一棵樹木一樣。
根節點
設備樹根節點名稱固定使用 /
表示,節點范圍用 {}
括號標明,屬于根節點的屬性或子節點就放置在 {}
內部,如下所示。
/ {............
}
每個設備樹文件只有一個根節點,如果引用了別的包含根節點的設備樹文件,那這些根節點會合并為一個根節點,內容也會疊加合并為一份。
子節點
設備樹子節點名稱命名規則為 node-name@unit-address
,子節點范圍用 {}
括號標明,屬于子節點的屬性或子節點就放置在 {}
內部,如下所示。
/ {node-name@unit-address {............};
}
其中 node-name
是節點名字,為 ASCII 字符串,可以任意意命名,但是最好能夠體現節點的功能,比如 serial@1c25000
就表示這個節點是串口外設。
而 unit-address
是節點所代表設備的地址或寄存器首地址,如果某個節點沒有地址或者寄存器的話 unit-address
可忽略,比如 cpus,soc,cpu@0。
雖然 node-name
代表節點名字,但是完整的節點名字為 node-name@unit-address
如果要訪問節點要使用完整節點名字,或標簽。
子節點標簽
還可以給子節點命名標簽,規則為 label: node-name@unit-address
,其中 label
是節點的標簽,而 :
后面的是節點名字 node-name
,例如 uart0:serial@1c25000
其中 uart0
就是節點標簽。
引入 label 的目的是為了方便訪問節點,有了標簽可以通過 &label
來訪問節點,比如通過 &uart0
就可以訪問 serial@1c25000
這個節點,而不用輸入完整的節點名字。再比如節點 sram:sram@10000000
,節點 label
是 sram,而節點名字就很長了,為 sram@10000000
。所以通過 &sram
來訪問 sram@10000000
節點要方便得多。
設備樹實例
主要觀察實例中根節點以及子節點部分的命名以及規則,節點內部涉及到節點的屬性相關內容再下一小節講解。
/ {#address-cells = <1>;#size-cells = <1>;interrupt-parent = <&intc>;clocks {osc24M: clk-24M {#clock-cells = <0>;compatible = "fixed-clock";clock-frequency = <24000000>;clock-output-names = "osc24M";};};cpus {#address-cells = <1>;#size-cells = <0>;cpu@0 {compatible = "arm,arm926ej-s";device_type = "cpu";reg = <0x0>;};};soc {compatible = "simple-bus";#address-cells = <1>;#size-cells = <1>;ranges;sram-controller@1c00000 {compatible = "allwinner,suniv-f1c100s-system-control","allwinner,sun4i-a10-system-control";reg = <0x01c00000 0x30>;#address-cells = <1>;#size-cells = <1>;ranges;};}
}
節點屬性
節點屬性可以理解為編程語言的變量,可以存儲數據。賦值采用 key=value
對的形式,鍵值對的值可以為空或任意的字節流。
/ {node-name@unit-address {key=value};
}
節點屬性支持幾種常用的數據類型具體下:
(1) 字符串類型,例如 compatible = “arm,arm926ej-s” 設置 compatible 屬性的值為字符串 “arm,arm926ej-s”。
(2) 32 位無符號整數類型,例如 reg = <0x0> 設置 reg 屬性的值為整數 0。
(3) 字符串列表類型,例如 compatible = “licheepi,licheepi-nano”, “allwinner,suniv-f1c100s” 設置屬性 compatible 的值為 “licheepi,licheepi-nano” 字符串和 “allwinner,suniv-f1c100s” 字符串。
4.3 標準屬性
節點是由屬性組成,一個節點代表一個設備,不同的設備需要的屬性不同,我們可以自定義屬性,也可以使用 Linux 支持的標準屬性。
compatible 屬性
compatible 屬性叫 “兼容性” 屬性,兼容屬性對驅動至關重要,用于設備匹配驅動程序(換句話說是 Linux 內核根據該設備節點的兼容屬性為該設備分配一個設備驅動程序)。兼容屬性值是一個字符串列表,字符串列表用于選擇設備所要使用的驅動程序,具體格式如下。
compatible = "manufacturer,model";
其中 manufacturer 段表示廠商名稱,而 model 是指硬件模塊對應驅動程序的名稱,例如 compatible = “winbond,w25q128” 表示產商是 winbond(華邦),硬件模塊是 w25q128 (存儲芯片)。
我們知道屬性支持字符串列表數據類型,所以 compatible 可存屬性值列表,這樣設備就有多個兼容屬性值,設備將多個兼容值逐個的和 Linux 內核驅動程序匹配,直到有合適的驅動程序。
實例
驅動程序文件都會有一個 OF 匹配表,用來保存一些 compatible 值,如果設備節點的 compatible 屬性值和 OF 匹配表中的任何一個值相等,那么就表示設備可以使用這個驅動(那么這個節點就會引用相應的驅動文件)。
#ifdef CONFIG_OF
static const struct of_device_id i2c_nuvoton_of_match[] = {{.compatible = "nuvoton,npct501"},{.compatible = "winbond,wpct301"},{.compatible = "nuvoton,npct601", .data = OF_IS_TPM2},{},
};
MODULE_DEVICE_TABLE(of, i2c_nuvoton_of_match);
#endif
model 屬性
model 屬性用于描述設備名稱(這里的設備指的是具體設備產品比如電腦,而非芯片外設),該屬性屬于字符串數據類型,例如 model = “Lichee Pi Nano”。
status 屬性
status 屬性表示設備狀態,設備樹使用字符串描述設備狀態信息,所以 status 屬性的類型也是字符串,可選的狀態如下表所示:
status 值 | 描述 |
---|---|
“okay” | 說明設備是可操作(可讀可寫)的。 |
“disabled” | 說明設備當前不可操作(無法讀寫),但是狀態可再轉變為可操作的(比如熱插拔設備插入以后)詳細部分可以查看設備的綁定文檔。 |
“fail” | 說明設備不可操作(無法讀寫),設備出現了一系列的錯誤,并且狀態無法再轉變為可操作的。 |
“fail-sss” | 和 “fail” 相同,sss 部分用于表示檢測到的錯誤內容。 |
reg 屬性
reg 屬性的值一般是 <address,length>
對,該屬性一般用于描述設備地址空間資源信息或者設備地址(即寄存器)信息,比如描述 UART 寄存器地址范圍信息,或者 I2C 器件的設備地址。
其中 address
指的是起始地址,length
則指的是地址長度,reg 屬性可以同時存放多組 <address,length>
地址對,每個 <address length>
組合表示一個地址范圍,具體格式如下。
reg = <address1 length1 address2 length2 address3 length3 ...>;
下面 serial 設備節點描述了全志 F1C200S 芯片 UART0 相關信息,可以看到 reg 屬性中 UART0 的起始地址為 0x01c250,地址長度為 0x400。
uart0: serial@1c25000 {compatible = "snps,dw-apb-uart";reg = <0x01c25000 0x400>;interrupts = <1>;reg-shift = <2>;reg-io-width = <4>;clocks = <&ccu CLK_BUS_UART0>;resets = <&ccu RST_BUS_UART0>;status = "disabled";
};
但是在設置 reg 屬性時需要設置幾組 <address, length>
地址對,這如何確定?這就是下一節涉及的 #address-cells 和 #size-cells 屬性的作用。
#address-cells 和 #size-cells 屬性
這兩個屬性一般是配對使用的,所以一起講解,這兩個屬性的類型都是 32 位無符號整形,擁有子節點的設備節點用 #address-cells
和 #size-cells
屬性設置其子節點的 reg 屬性字長。
#address-cells
屬性指定 reg 屬性的 address 所占用的字長,#size-cells
屬性指定 reg 屬性的 length 所占用的字長。
#address-cells = <value>
#size-cells = <value>
注意上一節說過 reg 屬性可以同時存放多組 <address,length>
地址對,所以這里的占用字長指的是 reg 屬性 address 值或 length 值得個數。
所以 #address-cells
屬性指定 reg 屬性中 address 的個數,#size-cells
屬性指定 reg 屬性中 length 的個數。
實例 1
soc {......#address-cells = <1>;#size-cells = <1>;ranges;uart0: serial@1c25000 {compatible = "snps,dw-apb-uart";reg = <0x01c25000 0x400>;interrupts = <1>;reg-shift = <2>;reg-io-width = <4>;clocks = <&ccu CLK_BUS_UART0>;resets = <&ccu RST_BUS_UART0>;status = "disabled";};......
}
實例 2
soc {......#address-cells = <1>;#size-cells = <0>;ranges;uart0: serial@1c25000 {compatible = "snps,dw-apb-uart";reg = <0x01c25000>;interrupts = <1>;reg-shift = <2>;reg-io-width = <4>;clocks = <&ccu CLK_BUS_UART0>;resets = <&ccu RST_BUS_UART0>;status = "disabled";};......
}
ranges 屬性
ranges 是一個地址映射/轉換表,ranges 屬性每個項目由子地址,父地址和地址空間長度這三部分組成:
rangs = <child-bus-address parent-bus-address length>;
(1) child-bus-address,子總線地址空間的物理地址,由父節點的 #address-cells 屬性確定此物理地址所占用的字長。
(2) parent-bus-address,父總線地址空間的物理地址,同樣由父節點的#address-cells 屬性確定此物理地址所占用的字長。
(3) length,子地址空間的長度,由父節點的 #size-cells 屬性確定此地址長度所占用的字長。
注意 ranges 屬性值可以為空,例如 rangs;
這樣,如果 ranges 屬性值為空值,說明子地址空間和父地址空間完全相同,不需要進行地址轉換。
對于一些芯片來說,子地址空間和父地址空間完全相同,因此會在其設備樹中看到不少設備節點得 ranges 屬性為空值。
實例
sram-controller@1c00000 {......#address-cells = <1>;#size-cells = <1>;......sram_d: sram@10000 {......ranges = <0 0x00010000 0x1000>;......};
};
name 屬性
name 屬性用于記錄節點名字,name 屬性值為字符串數據類型。不過 name 屬性已經被棄用,只能在較老的設備樹文件中看見 name 屬性,所以了解即可。
device_type 屬性
該屬性用于描述設備的 FCode,屬性值為字符串數據類型,IEEE 1275 會用到此屬性。該屬性只能用于 cpu 節點或者 memory 節點,但是設備樹沒有 FCode,所以該屬性也被棄用,所以了解即可。
5. 內核兼容檢查
普通設備的 compatible 兼容屬性用于在 Linux 內核中匹配設備驅動程序,而根節點 /
下的 compatible 兼容屬性則用于 Linux 內核檢查當前設備樹對應的設備類型(注意這里的設備不是指芯片外設,而是完整的硬件產品)。
因為設備樹是和硬件設備綁定的,所以 Linux 內核檢查設備樹的類型就可以知道當前內核是否支持該硬件設備,如果支持則啟動 Linux 內核。
以下是全志 F1C200S 芯片的設備樹,可以看到根節點的兼容屬性為 “licheepi,licheepi-nano” 和 “allwinner,suniv-f1c100s”。
/ {model = "Lichee Pi Nano";compatible = "licheepi,licheepi-nano", "allwinner,suniv-f1c100s";aliases {mmc0 = &mmc0;serial0 = &uart0;spi0 = &spi0;};chosen {stdout-path = "serial0:115200n8";};reg_vcc3v3: vcc3v3 {compatible = "regulator-fixed";regulator-name = "vcc3v3";regulator-min-microvolt = <3300000>;regulator-max-microvolt = <3300000>;};
};
設備檢查原理
在 Linux 內核的源碼目錄 arch/arm/include/asm/mach/ 目錄下的 arch.h 文件中定義了 machine_desc 結構體宏定義 DT_MACHINE_START
,通過該宏定義可以根據芯片架構指定名稱定義一個專有名稱的 machine_desc 結構體,并初始化,宏定義如下。
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \__used \__section(".arch.info.init") = { \.nr = ~0, \.name = _namestr,#endif
宏定義 DT_MACHINE_START
并不完整,因為結構體缺少了 }
括號,實際上還有配套宏定義 MACHINE_END
,通過這兩個宏定義即可定義完整的結構體。
#define MACHINE_END \
};
在 Linux 內核的源碼目錄 arch/arm/mach-xxx 目錄下包含各類芯片的設備描述machine_desc 結構體定義,例如全志 F1C200S 芯片對應的 machine_desc 結構體定義如下。
static const char * const suniv_board_dt_compat[] = {"allwinner,suniv-f1c100s",NULL,
};DT_MACHINE_START(SUNIV_DT, "Allwinner suniv Family").dt_compat = suniv_board_dt_compat,
MACHINE_END
machine_desc 結構體成員變量 .dt_compat
保存著相應設備的兼容值,查看全志 F1C200S 設備樹根節點的 compatible 屬性值可知與 suniv_board_dt_compat 保存的兼容值相同,因此 Linux 內核支持該設備。
6. 節點追加內容
前面說過,芯片產商會把同一個系列芯片相同的外設信息提煉到一個 .dtsi
文件中,需要這部分信息的 .dts
設備樹文件就可以包含這個頭文件,這樣可減少代碼的冗余。
所以如果要添加或修改 .dtsi
頭文件中的節點內容,就不能直接在 .dtsi
文件修改,直接修改會影響包含該頭文件的其他設備樹,那該怎么辦呢。
通過前面子節點內容知道可以給子節點命名標簽(label),有了節點標簽后就可以在 .dts
設備樹文件中通過標簽訪問節點,此時即可體現子節點標簽的作用,通過標簽訪問節點的規則如下:
&label {}
比如現在要修改設備節點 serial@1c25000
的 status 屬性,那么在 .dts
文件直接通過它的 uart0
標簽訪問即可(如果要訪問的節點沒有標簽,先命名標簽)。
&uart0 {pinctrl-names = "default";pinctrl-0 = <&uart0_pe_pins>;status = "okay";
};
7. DTS與RootFS
Uboot 啟動 Linux 內核的同時會將設備樹 .dtb
文件傳遞給 Linux 內核,Linux 內核會解析出設備樹的節點信息,并根據節點名字在根文件系統 /proc/device-tree 目錄下創建不同文件夾,再將節點內容保存到這些文件夾下。
我們知道設備樹屬于樹狀層級結構,恰好文件系統目錄結構也是如此,節點類似文件夾,屬性類似于文件夾中的文件,所以 Linux 內核將解析的設備樹節點在根文件系統中表示為 文件夾,屬性表示為 文件。
例如 clocks,soc 屬于 /
的子節點,所以它們是文件夾的形式,compatible,#address-cells,size-cells 屬于 /
的屬性,所以它們是文件的形式,如下圖。
不僅根節點 /
如此,所有節點都是這樣,例如 soc 節點的子節點以文件夾的形式表示,屬性以文件的形式表示,如下圖。
8. 特殊節點
8.1 aliases 節點
aliases 節點的主要作用是給節點定義別名,定義別名的目的就是為了方便訪問節點,不過這不常用,現在更多是使用節點標簽來訪問節點。
8.2 chosen 節點
chosen 節點主要作用是用于 Uboot 向 Linux 內核傳遞數據,重點用于 bootargs 參數傳遞。 Uboot 會在 chosen 節點添加 bootargs 屬性,并且設置 bootargs 屬性值為 bootargs 環境變量的值。
詳細查看:
https://blog.csdn.net/WANGYONGZIXUE/article/details/115600699