設備樹(Device Tree),將這個詞分開就是“設備”和“樹”,描述設備樹的文件叫做 DTS(DeviceTree Source),這個 DTS 文件采用樹形結構描述板級設備,也就是開發板上的設備信息,比如CPU 數量、 內存基地址、 IIC 接口上接了哪些設備、 SPI 接口上接了哪些設備等等,如圖 所示:
在圖中,樹的主干就是系統總線, IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接到系統主線上的分支。IIC 控制器有分為 IIC1 和 IIC2 兩種,其中 IIC1 上接了 FT5206 和 AT24C02這兩個 IIC 設備, IIC2 上只接了 MPU6050 這個設備。 DTS 文件的主要功能就是按照圖所示的結構來描述板子上的設備信息, DTS 文件描述設備信息是有相應的語法規則要求的,稍后我們會詳細的講解 DTS 語法規則。
以LED驅動為例,如果你要更換LED所用的GPIO引腳,需要修改驅動程序源碼、重新編譯驅動、重新加載驅動。
在內核中,使用同一個芯片的板子,它們所用的外設資源不一樣,比如A板用GPIO A,B板用GPIO B。而GPIO的驅動程序既支持GPIO A也支持GPIO B,你需要指定使用哪一個引腳,怎么指定?在c代碼中指定。
隨著ARM芯片的流行,內核中針對這些ARM板保存有大量的、沒有技術含量的文件。
Linus大發雷霆:"this whole ARM thing is a f*cking pain in the ass"。
于是,Linux內核開始引入設備樹。設備樹并不是重新發明出來的,在Linux內核中其他平臺如PowerPC,早就使用設備樹來描述硬件了。
Linus發火之后,內核開始全面使用設備樹來改造,神人就神人。
/sys/firmware/devicetree目錄下是以目錄結構程現的dtb文件, 根節點對應base目錄, 每一個節點對應一個目錄, 每一個屬性對應一個文件。
這些屬性的值如果是字符串,可以用cat命令把它打印出來;對于數值,可以用hexdump把它打印出來。
一個單板啟動時,u-boot先運行,它的作用是啟動內核。U-boot會把內核和設備樹文件都讀入內存,然后啟動內核。在啟動內核時會把設備樹在內存中的地址告訴內核。
設備樹源文件擴展名為.dts,但是我們在前面移植 Linux 的時候卻一直在使用.dtb 文件,那么 DTS 和 DTB 這兩個文件是什么關系呢? DTS 是設備樹源碼文件, DTB 是將DTS 編譯以后得到的二進制文件。將.c 文件編譯為.o 需要用到 gcc 編譯器,那么將.dts 編譯為.dtb需要什么工具呢?需要用到 DTC 工具! DTC 工具源碼在 Linux 內核的 scripts/dtc 目錄下.另外dtsi就相當于C語言的.h文件的概念!
/dts-v1/; // 表示版本
[memory reservations] // 格式為: /memreserve/ <address> <length>;
/ {
[property definitions]
[child nodes]
};
設備樹中的基本單元,被稱為“node”,其格式為:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
label是標號,可以省略。label的作用是為了方便地引用node,比如:
/dts-v1/;
/ {
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};
可以使用下面2種方法來修改uart@fe001000這個node:
// 在根節點之外使用label引用node:
&uart0 {
status = “disabled”;
};
//在根節點之外使用全路徑:
&{/uart@fe001000} {
status = “disabled”;
};
簡單地說,properties就是“name=value”,value有多種取值方式。
Property格式1:
[label:] property-name = value;
Property格式2(沒有值):
[label:] property-name;
Property取值只有3種:
arrays of cells(1個或多個32位數據, 64位數據使用2個32位數據表示),
string(字符串),
bytestring(1個或多個字節)
cell指一個32位的數值,
address-cells:address要用多少個32位數來表示;
size-cells:size要用多少個32位數來表示。
比如一段內存,怎么描述它的起始地址和大小?
下例中,address-cells為1,所以reg中用1個數來表示地址,即用0x80000000來表示地址;size-cells為1,所以reg中用1個數來表示大小,即用0x20000000表示大小:
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
“compatible”表示“兼容”,對于某個LED,內核中可能有A、B、C三個驅動都支持它,可以這樣寫:
led {
compatible = “A”, “B”, “C”;
};
內核啟動時,就會為這個LED按這樣的優先順序為它找到驅動程序:A、B、C。
根節點下也有compatible屬性,用來選擇哪一個“machine desc”:一個內核可以支持machine A,也支持machine B,內核啟動后會根據根節點的compatible屬性找到對應的machine desc結構體,執行其中的初始化函數。
compatible的值,建議取這樣的形式:"manufacturer,model",即“廠家名,模塊名”。
比如:compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
注意:machine desc的意思就是“機器描述”,學到內核啟動流程時才涉及。
model屬性與compatible屬性有些類似,但是有差別。
compatible屬性是一個字符串列表,表示可以你的硬件兼容A、B、C等驅動;
model用來準確地定義這個硬件是什么。
比如根節點中可以這樣寫:
/ {
compatible = "samsung,smdk2440", "samsung,mini2440";
model = "jz2440_v3";
};
它表示這個單板,可以兼容內核中的“smdk2440”,也兼容“mini2440”。
從compatible屬性中可以知道它兼容哪些板,但是它到底是什么板?用model屬性來明確。
dtsi文件中定義了很多設備,但是在你的板子上某些設備是沒有的。這時你可以給這個設備節點添加一個status屬性,設置為“disabled”:
reg的本意是register,用來表示寄存器地址。
但是在設備樹里,它可以用來描述一段空間。反正對于ARM系統,寄存器和內存是統一編址的,即訪問寄存器時用某塊地址,訪問內存時用某塊地址,在訪問方法上沒有區別。
reg屬性的值,是一系列的“address size”,用多少個32位的數來表示address和size,由其父節點的#address-cells、#size-cells決定。
示例:
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
memory {
reg = <0x80000000 0x20000000>;
};
};
它的值是字符串,用來表示節點的名字。在跟platform_driver匹配時,優先級最低。
compatible屬性在匹配過程中,優先級最高。
它的值是字符串,用來表示節點的類型。在跟platform_driver匹配時,優先級為中。
compatible屬性在匹配過程中,優先級最高。
dts文件中必須有一個根節點:
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
};
根節點中必須有這些屬性:
#address-cells // 在它的子節點的reg屬性中, 使用多少個u32整數來描述地址(address)
#size-cells // 在它的子節點的reg屬性中, 使用多少個u32整數來描述大小(size)
compatible // 定義一系列的字符串, 用來指定內核中哪個machine_desc可以支持本設備
// 即這個板子兼容哪些平臺
// uImage : smdk2410 smdk2440 mini2440 ==> machine_desc
model // 咱這個板子是什么
// 比如有2款板子配置基本一致, 它們的compatible是一樣的
// 那么就通過model來分辨這2款板子
一般不需要我們設置,在dtsi文件中都定義好了:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
.......
}
};
芯片廠家不可能事先確定你的板子使用多大的內存,所以memory節點需要板廠設置,比如:
memory {
reg = <0x80000000 0x20000000>;
};
我們可以通過設備樹文件給內核傳入一些參數,這要在chosen節點中設置bootargs屬性:
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
單詞 aliases 的意思是“別名”,因此 aliases 節點的主要功能就是定義別名,定義別名的目的就是為了方便訪問節點。不過我們一般會在節點命名的時候會加上 label,然后通過&label來訪問節點,這樣也很方便,而且設備樹里面大量的使用&label 的形式來訪問節點。
打開 imx6ull.dtsi 文件, aliases 節點內容如下所示:
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
ethernet0 = &fec1;
ethernet1 = &fec2;
gpio0 = &gpio1;
gpio1 = &gpio2;
....
spi0 = &ecspi1;
spi1 = &ecspi2;
spi2 = &ecspi3;
spi3 = &ecspi4;
usbphy0 = &usbphy1;
usbphy1 = &usbphy2;
};
make dtbs
我們來修改下一個設備樹,在跟節點中增加一個測試節點
bootcmd以及bootargs配置為
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'mmc dev 1; fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000
imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000;
saveenv
設備樹描述了設備的詳細信息,這些信息包括數字類型的、字符串類型的、數組類型的,我們在編寫驅動的時候需要獲取到這些信息。比如設備樹使用 reg 屬性描述了某個外設的寄存器地址為 0X02005482,長度為 0X400,我們在編寫驅動的時候需要獲取到 reg 屬性的 0X02005482 和 0X400 這兩個值,然后初始化外設。 Linux 內核給我們提供了一系列的函數來獲取設備樹中的節點或者屬性信息,這一系列的函數都有一個統一的前綴“of_”,所以在很多資料里面也被叫做 OF 函數。這些 OF 函數原型都定義在 include/linux/of.h 文件中。
設備都是以節點的形式“掛”到設備樹上的,因此要想獲取這個設備的其他屬性信息,必須先獲取到這個設備的節點。 Linux 內核使用 device_node 結構體來描述一個節點,此結構體定義在文件 include/linux/of.h 中,定義如下:
struct device_node {
const char *name; /* 節點名字 */
const char *type; /* 設備類型 */
phandle phandle;
const char *full_name; /* 節點全名 */
struct fwnode_handle fwnode;
struct property *properties; /* 屬性 */
struct property *deadprops; /* removed 屬性 */
struct device_node *parent; /* 父節點 */
struct device_node *child; /* 子節點 */
struct device_node *sibling;
struct kobject kobj;
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
與查找節點有關的 OF 函數有 5 個,我們依次來看一下。
of_find_node_by_name 函數通過節點名字查找指定的節點,函數原型如下:
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
from:開始查找的節點,如果為 NULL 表示從根節點開始查找整個設備樹。
name:要查找的節點名字。
返回值: 找到的節點,如果為 NULL 表示查找失敗。
of_find_node_by_type 函數通過 device_type 屬性查找指定的節點,函數原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
from:開始查找的節點,如果為 NULL 表示從根節點開始查找整個設備樹。
type:要查找的節點對應的 type 字符串,也就是 device_type 屬性值。
返回值: 找到的節點,如果為 NULL 表示查找失敗。
of_find_compatible_node 函數根據 device_type 和 compatible 這兩個屬性查找指定的節點,
函數原型如下:
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible)
from:開始查找的節點,如果為 NULL 表示從根節點開始查找整個設備樹。
type:要查找的節點對應的 type 字符串,也就是 device_type 屬性值,可以為 NULL,表示
忽略掉 device_type 屬性。
compatible: 要查找的節點所對應的 compatible 屬性列表。
返回值: 找到的節點,如果為 NULL 表示查找失敗
of_find_matching_node_and_match 函數通過 of_device_id 匹配表來查找指定的節點,函數原
型如下:
struct device_node *of_find_matching_node_and_match(struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)
from:開始查找的節點,如果為 NULL 表示從根節點開始查找整個設備樹。
matches: of_device_id 匹配表,也就是在此匹配表里面查找節點。
match: 找到的匹配的 of_device_id。
返回值: 找到的節點,如果為 NULL 表示查找失敗
of_find_node_by_path 函數通過路徑來查找指定的節點,函數原型如下:
inline struct device_node *of_find_node_by_path(const char *path)
path:帶有全路徑的節點名,可以使用節點的別名,比如“/backlight”就是 backlight 這個
節點的全路徑。
返回值: 找到的節點,如果為 NULL 表示查找失敗
Linux 內核提供了幾個查找節點對應的父節點或子節點的 OF 函數,我們依次來看一下。
of_get_parent 函數用于獲取指定節點的父節點(如果有父節點的話),函數原型如下:
struct device_node *of_get_parent(const struct device_node *node)
node:要查找的父節點的節點。
返回值: 找到的父節點。
of_get_next_child 函數用迭代的查找子節點,函數原型如下:
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)
node:父節點。
prev:前一個子節點,也就是從哪一個子節點開始迭代的查找下一個子節點。可以設置為
NULL,表示從第一個子節點開始。
返回值: 找到的下一個子節點。
節點的屬性信息里面保存了驅動所需要的內容,因此對于屬性值的提取非常重要, Linux 內核中使用結構體 property 表示屬性,此結構體同樣定義在文件 include/linux/of.h 中,內容如下:
struct property {
char *name; /* 屬性名字 */
int length; /* 屬性長度 */
void *value; /* 屬性值 */
struct property *next; /* 下一個屬性 */
unsigned long _flags;
unsigned int unique_id;
struct bin_attribute attr;
};
Linux 內核也提供了提取屬性值的 OF 函數,我們依次來看一下
of_find_property 函數用于查找指定的屬性,函數原型如下
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
np:設備節點。
name: 屬性名字。
lenp:屬性值的字節數
返回值: 找到的屬性
of_property_count_elems_of_size 函數用于獲取屬性中元素的數量,比如 reg 屬性值是一個
數組,那么使用此函數可以獲取到這個數組的大小,此函數原型如下:
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size)
np:設備節點。
proname: 需要統計元素數量的屬性名字。
elem_size:元素長度。
返回值: 得到的屬性元素數量。
of_property_read_u32_index 函數用于從屬性中獲取指定標號的 u32 類型數據值(無符號 32位),比如某個屬性有多個 u32 類型的值,那么就可以使用此函數來獲取指定標號的數據值,此函數原型如下:
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)
np:設備節點。
proname: 要讀取的屬性名字。
index:要讀取的值標號。
out_value:讀取到的值
返回值: 0 讀取成功,負值,讀取失敗, -EINVAL 表示屬性不存在, -ENODATA 表示沒有
要讀取的數據, -EOVERFLOW 表示屬性值列表太小。
這 4 個函數分別是讀取屬性中 u8、 u16、 u32 和 u64 類型的數組數據,比如大多數的 reg 屬性都是數組數據,可以使用這 4 個函數一次讀取出 reg 屬性中的所有數據。這四個函數的原型如下:
int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz)
int of_property_read_u16_array(const struct device_node *np,
const char *propname,
u16 *out_values,
size_t sz)
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values,
size_t sz)
int of_property_read_u64_array(const struct device_node *np,
const char *propname,
u64 *out_values,
size_t sz)
np:設備節點。
proname: 要讀取的屬性名字。
out_value:讀取到的數組值,分別為 u8、 u16、 u32 和 u64。
sz: 要讀取的數組元素數量。
返回值: 0,讀取成功,負值,讀取失敗, -EINVAL 表示屬性不存在, -ENODATA 表示沒
有要讀取的數據, -EOVERFLOW 表示屬性值列表太小。
有些屬性只有一個整形值,這四個函數就是用于讀取這種只有一個整形值的屬性,分別用于讀取 u8、 u16、 u32 和 u64 類型屬性值,函數原型如下:
int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value)
int of_property_read_u16(const struct device_node *np,
const char *propname,
u16 *out_value)
int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value)
int of_property_read_u64(const struct device_node *np,
const char *propname,
u64 *out_value)
np:設備節點。
proname: 要讀取的屬性名字。
out_value:讀取到的數組值。
返回值: 0,讀取成功,負值,讀取失敗, -EINVAL 表示屬性不存在, -ENODATA 表示沒
有要讀取的數據, -EOVERFLOW 表示屬性值列表太小。
of_property_read_string 函數用于讀取屬性中字符串值,函數原型如下
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string)
np:設備節點。
proname: 要讀取的屬性名字。
out_string:讀取到的字符串值。
返回值: 0,讀取成功,負值,讀取失敗。
of_n_addr_cells 函數用于獲取#address-cells 屬性值,函數原型如下:
int of_n_addr_cells(struct device_node *np)
函數參數和返回值含義如下:
np:設備節點。
返回值: 獲取到的#address-cells 屬性值。
of_size_cells 函數用于獲取#size-cells 屬性值,函數原型如下:
int of_n_size_cells(struct device_node *np)
函數參數和返回值含義如下:
np:設備節點。
返回值: 獲取到的#size-cells 屬性值。
of_device_is_compatible 函數用于查看節點的 compatible 屬性是否有包含 compat 指定的字
符串,也就是檢查設備節點的兼容性,函數原型如下:
int of_device_is_compatible(const struct device_node *device,
const char *compat)
device:設備節點。
compat:要查看的字符串。
返回值: 0,節點的 compatible 屬性中不包含 compat 指定的字符串; 正數,節點的 compatible
屬性中包含 compat 指定的字符串
of_get_address 函數用于獲取地址相關屬性,主要是“reg”或者“assigned-addresses”屬性
值,函數屬性如下:
const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags)
dev:設備節點。
index: 要讀取的地址標號。
size:地址長度。
flags:參數,比如 IORESOURCE_IO、 IORESOURCE_MEM 等
返回值: 讀取到的地址數據首地址,為 NULL 的話表示讀取失敗
of_translate_address 函數負責將從設備樹讀取到的地址轉換為物理地址,函數原型如下
u64 of_translate_address(struct device_node *dev,
const __be32 *in_addr)
dev:設備節點。
in_addr:要轉換的地址。
返回值: 得到的物理地址,如果為 OF_BAD_ADDR 的話表示轉換失敗。
IIC、 SPI、 GPIO 等這些外設都有對應的寄存器,這些寄存器其實就是一組內存空間, Linux內核使用 resource 結構體來描述一段內存空間,“resource”翻譯出來就是“資源”,因此用 resource結構體描述的都是設備資源信息, resource 結構體定義在文件 include/linux/ioport.h 中,定義如下:
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
對于 32 位的 SOC 來說, resource_size_t 是 u32 類型的。其中 start 表示開始地址, end 表示
結束地址, name 是這個資源的名字, flags 是資源標志位,一般表示資源類型,可選的資源標志
定義在文件 include/linux/ioport.h 中,如下所示:
#define IORESOURCE_BITS 0x000000ff
#define IORESOURCE_TYPE_BITS 0x00001f00
#define IORESOURCE_IO 0x00000100
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_REG 0x00000300
#define IORESOURCE_IRQ 0x00000400
#define IORESOURCE_DMA 0x00000800
#define IORESOURCE_BUS 0x00001000
#define IORESOURCE_PREFETCH 0x00002000
#define IORESOURCE_READONLY 0x00004000
#define IORESOURCE_CACHEABLE 0x00008000
#define IORESOURCE_RANGELENGTH 0x00010000
#define IORESOURCE_SHADOWABLE 0x00020000
#define IORESOURCE_SIZEALIGN 0x00040000
#define IORESOURCE_STARTALIGN 0x00080000
#define IORESOURCE_MEM_64 0x00100000
#define IORESOURCE_WINDOW 0x00200000
#define IORESOURCE_MUXED 0x00400000
#define IORESOURCE_EXCLUSIVE 0x08000000
#define IORESOURCE_DISABLED 0x10000000
#define IORESOURCE_UNSET 0x20000000
#define IORESOURCE_AUTO 0x40000000
#define IORESOURCE_BUSY 0x80000000
大 家 一 般 最 常 見 的 資 源 標 志 就 是 IORESOURCE_MEM 、 IORESOURCE_REG 和IORESOURCE_IRQ 等。接下來我們回到 of_address_to_resource 函數,此函數看名字像是從設備樹里面提取資源值,但是本質上就是將 reg 屬性值,然后將其轉換為 resource 結構體類型,函數原型如下所示
int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r)
dev:設備節點。
index:地址資源標號。
r:得到的 resource 類型的資源值。
返回值: 0,成功;負值,失敗。
of_iomap 函數用于直接內存映射,以前我們會通過 ioremap 函數來完成物理地址到虛擬地址的映射,采用設備樹以后就可以直接通過 of_iomap 函數來獲取內存地址所對應的虛擬地址,不需要使用 ioremap 函數了。當然了,你也可以使用 ioremap 函數來完成物理地址到虛擬地址的內存映射,只是在采用設備樹以后,大部分的驅動都使用 of_iomap 函數了。 of_iomap 函數本質上也是將 reg 屬性中地址信息轉換為虛擬地址,如果 reg 屬性有多段的話,可以通過 index 參數指定要完成內存映射的是哪一段, of_iomap 函數原型如下:
void __iomem *of_iomap(struct device_node *np,
int index)
np:設備節點。
index: reg 屬性中要完成內存映射的段,如果 reg 屬性只有一段的話 index 就設置為 0。
返回值: 經過內存映射后的虛擬內存首地址,如果為 NULL 的話表示內存映射失敗。