1、ARM Linux社區為什么要引入設備樹
Linux之父Linus Torvalds閑來無事,在翻看ARM Linux代碼的時候,有一天終于忍不住了。他在2011年3月17日的ARM Linux郵件列表中說道:“This whole ARM thing is a f*cking pain in the ass”。這句話迫使ARM Linux社區引入了設備樹。
Linus Torvalds為什么會發飆呢?而ARM Linux社區的牛人為什么又乖乖地聽話了?你得首先理解Linux設備驅動框架中一個非常好的設計:設備信息和驅動分離。
為了說明設備信息和驅動分離的概念,這里用一個簡單的模擬代碼來解釋:
【例-1】實現一個代碼,把要使用的信息簡單寫死在代碼中:
int add() /*模擬驅動代碼*/
{
return 3+5; /*模擬設備信息*/
}
優點:簡單
缺點:一旦加數和被加數發生變化就得改代碼
改進設計如下:
【例-2】實現一個代碼,把要使用的信息和操作代碼分離開來:
struct dev{
int id;
int x;
int y;
}; /*模擬設備信息結構*/
strcut drv{
int id;
int (*add)(struct dev *info);
}; /*模擬驅動結構*/
int add(struct dev *info) /*模擬驅動代碼*/
{
return info->x + info->y; /*模擬設備信息-通過參數傳遞進來*/
}
struct drv drv = {
.id = 1,
.add = add,
};
/*模擬設備信息*/
struct dev dev = {
.id = 1,
.x = 3,
.y = 5,
};
/*模擬總線初始化匹配設備信息和驅動代碼*/
int bus()
{
if(dev.id == drv.id){
return drv.add(&dev);
}
...
}
優點:不管加數和被加數怎么變化,不需要修改代碼,僅需要修改信息
缺點:結構比較復雜
那這個設備信息和驅動分離的設計跟驅動有什么關系呢?熟悉硬件編程的同學都知道,硬件一般的構成可以使用下圖簡單表述:
操作外設的驅動代碼邏輯,只要硬件是一樣的,就不會變化。但是外設掛到不同的主機上,可能會存在I/O地址的變化,如果有中斷也是一樣的,中斷號也可能不同。這些I/O地址和中斷號就是設備信息,使用這些信息來操作控制硬件的代碼就是驅動。
如果采用【例-1】的設計方式,那么同一個硬件外設接到不同的主機,或是換了地址線/中斷線,設備信息就變化了,得去修改驅動。但是采用【例-2】的方式進行設計,問題就迎刃而解:不管同樣的外設硬件接到哪里或是那個平臺,其驅動代碼邏輯并不需要改動,而僅僅需要改變下設備信息,主要的就是I/O地址和中斷號。
說了這么半天,跟引入設備樹有什么關系呢?華清教學使用的開發板(A8/A9)都使用DM9000網卡芯片。DM9000驅動是開源的,在主線內核源碼中就有。我們每次基于A8/A9板子移植的時候,DM9000驅動并沒有修改過,僅僅是選配了下,主要的工作是在板級文件中添加了設備信息。DM9000驅動使用的是platform框架,所以添加了一份DM9000網卡芯片的platform_device信息。問題來了,如果使用C代碼的形式來描述設備信息,則在內核源碼中,將會有多份DM9000的platform_device設備信息,造成了內核代碼冗余。
解決這個問題的辦法就是引入設備樹,改造【例-2】來說明設備樹的作用。
【例-3】實現一個代碼,不僅把要使用的信息和操作代碼分離開來,而且信息不是C代碼編寫的,而是文本配置文件保存的:
struct dev{
int id;
int x;
int y;
}; /*模擬設備信息結構*/
strcut drv{
int id;
int (*add)(struct dev *info);
}; /*模擬驅動結構*/
int add(struct dev *info) /*模擬驅動代碼*/
{
return info->x + info->y; /*模擬設備信息-通過參數傳遞進來*/
}
struct drv drv = {
.id = 1,
.add = add,
};
/*模擬設備樹-一個特殊的配置文件,xxx.dtbs的文本文件*/
/{
......
Dm9000{
x = 3;
y = 5;
};
......
};
/*模擬總線初始化匹配設備信息和驅動代碼*/
int bus()
{
/*模擬設備樹初始化處理*/
讀文件(xxx.dtbs);
解析文件內容(根據設備樹的規則來解析);
生成struct dev設備信息;
if(dev.id == drv.id){
return drv.add(&dev);
}
...
}
如果像【例-3】這樣,就可以解決大量設備信息的代碼冗余問題。
推而廣之,系統的軟硬件信息都可以使用設備樹來描述。這樣的話,ARM Linux社區就不會因為支持板子和驅動越來越多造成內核源碼中出現很多冗余代碼(主要是板級文件),僅僅需要移植者,把系統的軟硬件信息通過設備樹提供出來,選配一下內核代碼,就可以了。
2、設備樹的概述
2.1、參考資料
內核源碼目錄Documentation\devicetree設備樹說明文檔
內核源碼drivers/of/源碼分析
2.2、基本概念
設備樹是描述軟/硬件信息的,包含節點和屬性的一個樹形結構。節點用以歸類描述了一個硬件信息或是軟件信息(好比文件系統的目錄)。節點內描述了一個或多個屬性,屬性是鍵值對,描述具體的軟/硬信息。簡單形式如下:
/{
node{
property=value;
...
child_node{
child_property=value;
...
};
...
};
...
};
說明如下:
/:根節點,節點使用“{};”的語法描述作用范圍
node:根節點下的一個子節點
child_node:node節點下的一個子節點
property:node節點內描述的屬性,value就是屬性的值(任意字節數據,可以是整型、字符串、數組、等等)。描述行以“;”結束
2.3、存儲形式
在《ARM Linux社區為什么要引入設備樹》中,已經討論過設備樹的使用方式。簡而言之:內核初始化時,以配置的文件形式讀取設備樹文件的內容,并解析后生成相應的軟/硬件信息,以供相應的內核代碼使用。
編寫設備樹文件是以.dts的文本文件存儲的,主要是為了修改、添加編輯方便。
那么問題來了,如果純文本解析的話,顯然比較慢且麻煩。譬如如果屬性值是一個I/O地址:0x80000000,如果是字符串的形式存儲,那么“0x80000000”就是一個字符串,內核代碼解析這個信息的時候還得轉換成整型數,不僅僅是慢,無形設備樹文件大小還會增加不少,還得增加更多的初始化代碼。
所以.dts的設備樹文件,在內核使用前需要轉換一次,主要是把繁復的語法形式及屬性值轉換成字節數據(特殊的數據結構),而非符號。.dts文件轉換后是.dtb的二進制文件。
3、節點
3.1、命名
節點的命名以字母、數字、_、等等符號構成。常見的命令方式如下:
A、以“設備名”為節點名,范例:
DM9000命名如下:
/{
...
dm9000{
...
};
...
};
B、以“設備@I/O地址”為“節點名@I/O地址”,范例:
DM9000在主機端的I/O地址為0x8000 0000,可以命名如下:
/{
...
dm9000@80000000{
...
};
...
};
C、以“設備類型@I/O地址”為“節點名@I/O地址”,范例:
DM9000在主機端的I/O地址為0x8000 0000,可以命名如下:
/{
...
ethernet@80000000{
...
};
...
};
3.2、節點路徑
A、
/{
...
dm9000{
...
};
...
};
節點名:dm9000
節點路徑:/dm9000
B、
/{
...
dm9000@80000000{
...
};
...
};
節點名:dm9000
節點路徑:/dm9000@80000000
C、
/{
...
ethernet@80000000{
...
};
...
};
節點名:ethernet
節點路徑:/ethernet@80000000
3.3、節點引用
/{
aliases {
demo = &demo;
};
...
demo:demo@80000000{
...
};
...
};
節點名:demo
節點路徑:/demo@80000000
引用路徑:demo(等價/demo@80000000,解決路徑名過程的問題)
設備樹中引用節點“/demo@80000000”的范例:
&demo{
...
};
3.4、節點查找
有時候,分享內核代碼或是編寫內核代碼的時候,可能會涉及使用查找節點函數。內核提供很多內核函數來查找(解析設備樹)一個指定節點:
A、路徑查找
/*
* 功能:通過路徑查找指定節點
* 參數:
* const char *path - 節點路徑,可以是路徑,也可以是路徑引用
* 返回值:
* 成功:得到節點對象的首地址;失敗:NULL
*/
struct device_node *of_find_node_by_path(const char *path);
B、節點名查找
/*
* 功能:通過節點名查找指定節點
* 參數:
* struct device_node *from - 指向開始路徑的節點,如果為NULL,則從根節點開始
* const char *name- 節點名
* 返回值:
* 成功:得到節點對象的首地址;失敗:NULL
*/
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
C、通過compatible屬性查找
/*
* 功能:通過compatible屬性查找指定節點
* 參數:
* struct device_node *from - 指向開始路徑的節點,如果為NULL,則從根節點開始
* const char *type - 節點類型,可以為NULL
* const char *compat - 指向節點的compatible屬性的值(字符串)的首地址
* 返回值:
* 成功:得到節點對象的首地址;失。篘ULL
*/
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compat);
設備ID表結構
struct of_device_id {
char name[32]; /*設備名*/
char type[32]; /*設備類型*/
char compatible[128]; /*用于與設備樹compatible屬性值匹配的字符串*/
const void *data; /*私有數據*/
};
/*
* 功能:通過compatible屬性查找指定節點
* 參數:
* struct device_node *from - 指向開始路徑的節點,如果為NULL,則從根節點開始
* const struct of_device_id *matches - 指向設備ID表
* 注意ID表必須以NULL結束
* 范例: const struct of_device_id mydemo_of_match[] = {
{ .compatible = "fs4412,mydemo", },
{}
};
* 返回值:
* 成功:得到節點對象的首地址;失。篘ULL
*/
struct device_node *of_find_matching_node(struct device_node *from,
const struct of_device_id *matches);
D、查找子節點
/*
* 功能:查找指定節點的子節點
* 參數:
* const struct device_node *node - 指向要查找子節點的父節點
* const char *name - 子節點名
* 返回值:
* 成功:得到子節點對象的首地址;失。篘ULL
*/
struct device_node *of_get_child_by_name(const struct device_node *node,
const char *name);
3.5、節點內容合并
有時候,一個硬件設備的部分信息不會變化,但是部分信息是可能會變化的,就出現了節點內容合并。即:先編寫好節點,僅僅描述部分屬性值;使用者后加一部分屬性值。
在同級路徑下,節點名相同的“兩個”節點實際是一個節點。
/*參考板的已經編寫好的node節點*/
/{
node{
item1=value;
};
};
/*移植者添加的node節點*/
/{
node{
item2=value;
};
};
等價于:
/{
node{
item1=value;
item2=value;
};
};
3.6、節點內容替換
有時候,一個硬件設備的部分屬性信息可能會變化,但是設備樹里面已經描述了所有的屬性值,使用者可以添加已有的屬性值,以替換原有的屬性值,就出現了節點內容替換。
另外,節點的內容即使不會變化,但是可能不會使用。
在同級路徑下,節點名相同的“兩個”節點實際是一個節點。
內容替換的常見形式之一:
/*參考板的已經編寫好的node節點*/
/{
node{
item=value1;
};
};
/*移植者添加的node節點*/
/{
node{
item=value2;
};
};
等價于:
/{
node{
item=value2;
};
};
內容替換的常見形式之二:
/*參考板的已經編寫好的node節點*/
/{
node{
item=value;
status = "disabled";
};
};
/*移植者添加的node節點*/
/{
node{
status = "okay";
};
};
等價于:
/{
node{
item=value;
status = "okay";
};
};
3.7、節點內容引用
有時候,一個節點需要使用到別的節點的屬性值,就需要引用的概念。有時候在設備樹編寫時,要替換節點屬性值,或是合并節點的屬性值,也會使用引用。
A、引用節點完成屬性值的替換及合并:
/*參考板的已經編寫好的node節點*/
/{
node:node@80000000{
item1=value;
status = "disabled";
};
};
/*移植者添加的node節點*/
&node{
item2=value;
status = "okay";
};
等價于:
/{
node : node@80000000{
item1=value;
item2=value;
status = "okay";
};
};
B、節點引用另一個節點:
/*參考板的已經編寫好的node節點*/
/{
node:node@80000000{
item=value;
};
};
/*移植者添加的demo節點*/
/{
demo{
item=<&node>;
};
};
說明:
demo節點的屬性item引用了節點的node的屬性值,具體怎么使用node節點的屬性值,在屬性章節進行討論。