【Leo的手記】Linux設(shè)備驅(qū)動程序手記5-設(shè)備樹

由著之前的平臺設(shè)備中的platform_device的實現(xiàn)我們可以得知,在平臺設(shè)備總線的抽象下,設(shè)備驅(qū)動程序被分離為了設(shè)備驅(qū)動業(yè)務(wù)和設(shè)備描述信息。而對于主要用于進(jìn)行設(shè)備信息描述的platform_device,如果全部按照內(nèi)核模塊的方式以內(nèi)核源碼的形式放置于Linux內(nèi)核中,則由于不同且多樣的板級硬件實現(xiàn),Linux內(nèi)核驅(qū)動的源碼將飛速膨脹并嚴(yán)重拉低內(nèi)核源碼質(zhì)量。而對于主要包含設(shè)備信息描述的platform_device,將其作為程序代碼的組成部分實在是沒有必要。因此,LInux內(nèi)核引入了設(shè)備樹,通過設(shè)備樹,Linux可以方便地自行組織相應(yīng)的設(shè)備結(jié)構(gòu)而無需將代碼固化到內(nèi)核代碼中。需要注意的是設(shè)備樹并非Linux內(nèi)核所獨(dú)有,在多種實時嵌入式操作系統(tǒng),Bootloader等項目代碼中也都引入了設(shè)備樹。例如U-Boot同樣引入了設(shè)備樹進(jìn)行設(shè)備的描述。
1. 設(shè)備樹
設(shè)備樹顧名思義,它是一種樹的數(shù)據(jù)結(jié)構(gòu)。其包含一個根節(jié)點(diǎn),并對于每一個子節(jié)點(diǎn),都有其父節(jié)點(diǎn)(根節(jié)點(diǎn)除外,因為根節(jié)點(diǎn)在常見的系統(tǒng)解析時是被解析為通過一全局變量指向的數(shù)據(jù)結(jié)構(gòu))。系統(tǒng)通過樹狀結(jié)構(gòu),描述了系統(tǒng)的各個設(shè)備以及設(shè)備之間的依賴關(guān)系。需要注意的是,對于設(shè)備的描述不一定非得是物理設(shè)備,也可以是邏輯設(shè)備或者是數(shù)據(jù)結(jié)構(gòu)抽象。
通過設(shè)備樹,不僅精簡了系統(tǒng)的代碼,而且更集中高效地組織了系統(tǒng)設(shè)備的描述信息,將系統(tǒng)的設(shè)備移植成本和裁剪精簡成本大大降低。
我們一般說編寫設(shè)備樹,是指編寫設(shè)備樹的源文件,設(shè)備樹的源文件是以設(shè)備樹的語法邏輯編寫的以dts為拓展名的文本文件。在系統(tǒng)內(nèi)核編譯時,會將所指定的設(shè)備樹源文件編譯為設(shè)備樹的二進(jìn)制文件,其拓展名為dtb。在制作系統(tǒng)啟動鏡像時,往往需要將內(nèi)核鏡像文件和設(shè)備樹文件放在一起。
2. 設(shè)備樹的語法
2.1 設(shè)備樹結(jié)構(gòu)的定義
對于樹狀結(jié)構(gòu),設(shè)備樹的語法主要是需要提供對于節(jié)點(diǎn)的描述。
設(shè)備樹通過如下語法描述設(shè)備的節(jié)點(diǎn)。
其中,每一個節(jié)點(diǎn),都由節(jié)點(diǎn)名稱后跟著一對花括號所定義。節(jié)點(diǎn)中包含了對當(dāng)前節(jié)點(diǎn)屬性的描述,以及子節(jié)點(diǎn)的描述。
需要注意的是,對于任何一條完整的語義(包含節(jié)點(diǎn)描述,屬性描述),都需要以花括號結(jié)尾。
而對于節(jié)點(diǎn)名node-name,可以在其前方指定label作為別名,中間以半角分號分隔。而我們也可以在node-name后指定該節(jié)點(diǎn)的地址。該地址需要和節(jié)點(diǎn)的reg屬性起始地址匹配。
根節(jié)點(diǎn)作為頂層節(jié)點(diǎn),其節(jié)點(diǎn)名固定為/,同Linux文件系統(tǒng)結(jié)構(gòu)的根目錄路徑。
2.2 設(shè)備樹引用
我們可以在根節(jié)點(diǎn)外部引用一些節(jié)點(diǎn)并修改其中的部分屬性。
通過使用&加別名或全路徑的方式,可以進(jìn)行節(jié)點(diǎn)的引用。
在設(shè)備樹中,通常需要包含硬件廠商所提供的設(shè)備樹文件。該類設(shè)備樹文件由廠商負(fù)責(zé)編寫好各硬件資源的定義。在我們使用時,只需要進(jìn)行裁剪等。我們通常不會直接修改廠商提供的設(shè)備樹源碼。而因此,他們所提供的源碼也通常以dtsi為拓展名,意為設(shè)備樹的包含頭文件。該文件在設(shè)備樹中可以通過#include <>的預(yù)處理指令進(jìn)行包含(需要注意的是,在Linux內(nèi)核對設(shè)備樹的編譯過程中會調(diào)用gcc編譯器的-E選項進(jìn)行預(yù)處理,因此其支持C語言的預(yù)處理指令。但并不代表所有的項目代碼都一定使用這種編譯流程。)。而我們在包含相應(yīng)的頭文件后,便可以通過引用節(jié)點(diǎn)的方式對設(shè)備進(jìn)行定制。
2.3 設(shè)備樹的常用節(jié)點(diǎn)
2.3.1 根節(jié)點(diǎn)
一個設(shè)備樹結(jié)構(gòu)必須要有一個根節(jié)點(diǎn)。
2.3.2 cpu節(jié)點(diǎn)
用于對單核或多核處理器的描述。其不同的子節(jié)點(diǎn)用于描述不同的cpu核心。
2.3.3 memory節(jié)點(diǎn)
用于描述系統(tǒng)內(nèi)存資源的節(jié)點(diǎn)。
2.3.4 chosen節(jié)點(diǎn)
虛擬節(jié)點(diǎn),用于指定內(nèi)核啟動參數(shù),標(biāo)準(zhǔn)輸出路徑和標(biāo)準(zhǔn)輸入路徑等信息。
2.4 設(shè)備樹屬性的定義
定義一個節(jié)點(diǎn)的屬性,使用如下格式
其中屬性值property-value包括三種大類型,分別是字符串,32位整數(shù)和字節(jié)。
分別用雙引號",尖括號和方括號定義。具體如下
字符串 :name = "LeoBunny";
字符串列表:compatible = "LeoBunny","LeoIsaacBunny";
32位整數(shù)列表(64位整數(shù)使用兩個32位整數(shù)表示,0x開頭表示16進(jìn)制,無前綴默認(rèn)10進(jìn)制):reg = < 0x12345678 100 >;
單字節(jié)列表(16進(jìn)制表示):data = [12 34 56 78];
組合:sample = "LeoBunny",<0x12345678>,[9a bc];
此外,可以僅僅定義屬性的名稱,默認(rèn)代表布爾類型。作為標(biāo)志。
也可以使用其他節(jié)點(diǎn)的句柄作為屬性值。
2.5 常用的屬性
2.5.1 #address-cells,#size-cells
在設(shè)備樹的語法中,默認(rèn)一個cell是代表32位數(shù)據(jù)。但是設(shè)備樹支持32位和64位系統(tǒng),因此對于地址,數(shù)據(jù)長度等的表示,則需要指定一個字段到底需要多長的位數(shù)對齊進(jìn)行表示。
#address-cells :指定地址需要用多少個cell進(jìn)行表示
#size-cells :指定長度需要用多少個cell進(jìn)行表示
例如
上述設(shè)備樹描述了對于memory節(jié)點(diǎn),使用1個cell進(jìn)行地址的表示,1個cell進(jìn)行長度的表示。
2.5.2 compatible
兼容屬性,用于指定該設(shè)備兼容何種驅(qū)動,用于匹配平臺驅(qū)動。優(yōu)先級最高。
對于根節(jié)點(diǎn)的compatible屬性,用于選擇對應(yīng)的machine desc結(jié)構(gòu)體,進(jìn)行平臺初始化。
而對于設(shè)備節(jié)點(diǎn),則根據(jù)兼容屬性查找對應(yīng)的平臺驅(qū)動程序。
例如
從前向后依次匹配。
2.5.3 model
用于指定該設(shè)備的具體型號描述。
需要注意的是對于compatible屬性和model屬性,官方給出的命名參考都是"廠商名,產(chǎn)品名"
2.5.4 status
用于指定設(shè)備的狀態(tài)。有如下取值
"okay" : 表示設(shè)備良好并使用設(shè)備。
"disabled" : 表示設(shè)備不可用。
"reserved" : 表示設(shè)備可以運(yùn)行但是不建議使用,通常該類設(shè)備被諸如平臺固件等其他的軟件組件所控制。
“fail” : 表示設(shè)備不可用,并且檢測到了嚴(yán)重錯誤。
"fail-sss" : 同上,sss指示具體的錯誤類型
可以結(jié)合status屬性和節(jié)點(diǎn)引用實現(xiàn)對硬件資源的裁剪。
2.5.5 reg
表示寄存器地址,是一系列的“地址 長度“對。長度由其父節(jié)點(diǎn)的#address-cells和#size-cells決定。
例同#address-cells與#size-cells。
此外reg,也可以用于指定資源標(biāo)號,此時的reg不再使用地址長度對而是僅僅指定標(biāo)號。例如多核cpu系統(tǒng)中的cpu標(biāo)號。
2.5.6 name (過時,不建議使用)
指定設(shè)備名稱,用于匹配平臺驅(qū)動,優(yōu)先級最低。
2.5.7 device_type (過時,不建議使用)
指定設(shè)備類型,用于匹配平臺驅(qū)動,優(yōu)先級中。
3. Linux對設(shè)備樹的解析
在內(nèi)核編譯的過程中,設(shè)備樹源文件會被編譯為dtb文件,dtb文件隨著系統(tǒng)的加載被載入內(nèi)存,之后Linux內(nèi)核將其每一個節(jié)點(diǎn)都解析為device_node結(jié)構(gòu)。然后根據(jù)指定的規(guī)則,將某些device_node結(jié)構(gòu)體轉(zhuǎn)換為platform_device結(jié)構(gòu)體。
device_node結(jié)構(gòu)體的原型如下
根節(jié)點(diǎn)將會被保存在全局變量of_root中。相關(guān)內(nèi)核代碼如下(base.c 外部聲明位于of.h)
對于設(shè)備樹的樹狀結(jié)構(gòu),其很好地描述了設(shè)備間的依賴關(guān)系。而對于平臺設(shè)備驅(qū)動模型。往往并不需要使用總線模型對一些設(shè)備的子節(jié)點(diǎn)進(jìn)行處理,這些節(jié)點(diǎn)應(yīng)該交由其父節(jié)點(diǎn)所指定的驅(qū)動程序進(jìn)行處理。因此并非所有的設(shè)備樹節(jié)點(diǎn)都會被轉(zhuǎn)換為platform_device。
以下設(shè)備樹節(jié)點(diǎn)將會被轉(zhuǎn)換為platform_device。
根節(jié)點(diǎn)下,包含有compatible屬性的子節(jié)點(diǎn)。
在某個節(jié)點(diǎn)的compatible屬性中包含有“simple-bus”,"simple-mfd","isa","arm,amba-bus"其中之一,則其包含有compatible屬性的子節(jié)點(diǎn)將會被轉(zhuǎn)換為platform_device。
3.1 Linux匹配設(shè)備樹節(jié)點(diǎn)與platform_driver
對于能夠支持設(shè)備樹的platform_driver,需要定義其of_match_table成員。
of_match_table為struct of_device_id數(shù)組。該數(shù)組必須以空項結(jié)尾。而struct of_device_id原型如下
對于某個可以轉(zhuǎn)換為platform_device的設(shè)備樹節(jié)點(diǎn)。其匹配流程如下。
遍歷整個of_match_table
檢查of_match_table中的某項的compatible成員是否和設(shè)備樹節(jié)點(diǎn)中的compatible屬性匹配,如果匹配則匹配成功。
如果不匹配,檢查type成員是否和設(shè)備的type屬性匹配。
如果不匹配,檢查name成員是否和設(shè)備的name屬性匹配。
而對于匹配成功的設(shè)備節(jié)點(diǎn),platform_driver則需要獲取其屬性來得到資源定義。
3.2 Linux獲取設(shè)備樹屬性
對于轉(zhuǎn)換成platform_device的設(shè)備節(jié)點(diǎn),其reg屬性將會被轉(zhuǎn)換為IORESOURCE_MEM類型的資源。而interrupts將會被轉(zhuǎn)換為IORESOURCE_IRQ類型的資源。
而對于非官方指定的標(biāo)準(zhǔn)屬性或者是不會被轉(zhuǎn)換為platform_device的設(shè)備節(jié)點(diǎn)屬性,Linux內(nèi)核提供了一系列方法用于讀取其屬性。
這一系列的操作通??梢杂刹檎夜?jié)點(diǎn),查找節(jié)點(diǎn)屬性,獲取節(jié)點(diǎn)屬性值三步完成。
3.2.1 查找設(shè)備樹節(jié)點(diǎn)
of_find_node_by_path方法
該方法可以根據(jù)設(shè)備樹路徑獲取設(shè)備節(jié)點(diǎn)。
of_find_node_by_name方法
該方法可以根據(jù)設(shè)備節(jié)點(diǎn)名稱找到節(jié)點(diǎn)。其中from參數(shù)指定從哪一個節(jié)點(diǎn)開始尋找。如果傳入NULL則從根節(jié)點(diǎn)開始(下同)。
of_find_node_by_type方法
該方法可以根據(jù)類型(即設(shè)備節(jié)點(diǎn)的device_type屬性)查找節(jié)點(diǎn)。
of_find_compatible_node方法
該方法可以根據(jù)compatible屬性查找節(jié)點(diǎn)。
of_find_node_by_phandle方法
phandle是dts文件被編譯為dtb文件時,為每一個節(jié)點(diǎn)指派的唯一的數(shù)字ID,可以使用這些數(shù)字ID來查找device_node。
of_get_parent方法
獲取某一節(jié)點(diǎn)的父節(jié)點(diǎn)
of_get_next_parent方法
返回結(jié)果同of_get_parent,區(qū)別在于其在執(zhí)行的過程中將參數(shù)node作為參數(shù)調(diào)用了一次of_node_put方法減少了node節(jié)點(diǎn)的引用計數(shù)。
of_get_next_child方法
查找下一個子節(jié)點(diǎn),node傳入父節(jié)點(diǎn),prev表示上一個子節(jié)點(diǎn),prev傳入NULL表示尋找第一個子節(jié)點(diǎn)。
of_get_next_available_child方法
與of_get_next_child方法的區(qū)別在于它通過判斷status屬性只查找可用的節(jié)點(diǎn),如果節(jié)點(diǎn)的status屬性為"disabled",會跳過。
of_get_child_by_name方法
根據(jù)名字獲取節(jié)點(diǎn)的子節(jié)點(diǎn),node表示父節(jié)點(diǎn)。
3.2.2 獲取節(jié)點(diǎn)屬性
of_find_property方法
np:要獲取屬性的節(jié)點(diǎn)。
name:屬性的名稱。
lenp:用來保存屬性長度的變量指針。
struct property原型
name :屬性名。
length :屬性長度。
value :屬性內(nèi)容(指針)。
3.2.3 獲取屬性值
of_get_property方法
np:要獲取屬性的節(jié)點(diǎn)。
name:屬性的名稱。
lenp:用來保存屬性長度的變量指針。
of_property_count_elems_of_size方法
獲取當(dāng)前屬性按照屬性的元素長度計算得到的元素個數(shù)。需要注意的是上文中所說的屬性長度均以字節(jié)為單位。
np :節(jié)點(diǎn)。
propname :屬性名。
elem_size :單位長度。
(1) 讀取整數(shù)
of_property_read_u32方法
讀取32位整數(shù)。
np : 節(jié)點(diǎn)。
propname :屬性值。
out_value :用于存放輸出值的指針。
of_property_read_u64方法
讀取64位整數(shù),參數(shù)定義同上。
of_property_read_u32_index方法
讀取指定索引處的32位整數(shù)。index參數(shù)為索引號。
of_property_read_u64_index方法
讀取指定索引處的64位整數(shù)。
of_property_read_variable_u8_array方法
用u8數(shù)組的方式獲取參數(shù)的全部數(shù)據(jù)。sz_min和sz_max指定長度的范圍,如果長度沒有落在由其指定的范圍內(nèi),則不返回屬性值。
of_property_read_variable_u16_array方法
of_property_read_variable_u32_array方法
of_property_read_variable_u64_array方法
of_property_read_string方法
讀取到的字符串屬性的指針會被存放在out_string指向的字符串的指針中。
4. 實例代碼
我們在原有的設(shè)備樹中添加如下的設(shè)備節(jié)點(diǎn)。
重新編譯設(shè)備樹并將設(shè)備樹替換原有的設(shè)備樹。
然后編寫platform_driver對應(yīng)的驅(qū)動代碼。如下
編譯,在系統(tǒng)啟動后加載內(nèi)核模塊,得到如下的輸出。

可以看到,驅(qū)動程序在加載時匹配到了設(shè)備樹生成的設(shè)備。也能獲得其對應(yīng)的資源。
需要注意的是,對于自動轉(zhuǎn)換為IORESOURCE_MEM和IORESOURCE_IRQ的資源,會通過系統(tǒng)的處理轉(zhuǎn)化為系統(tǒng)對應(yīng)的資源表示形式。這也說明了為什么打印的Reg和IRQ和設(shè)備樹中所編寫的有所區(qū)別。而通過直接獲取資源得到的數(shù)值則同設(shè)備樹源文件中的內(nèi)容。