本文通過為一個新machine寫一個設備樹來介紹設備樹相關的概念,以及如何來描述一個machine。
關於設備樹的技術細節描述,需要參考ePAPR文檔,ePAPR文檔中包含了大量的基礎語法之外的細節,如果你需要了解更多本文之外的設備樹細節,請參考ePAPR文檔。
基本數據格式
設備樹是一個由節點及屬性組成的簡單樹結構。屬性是基於key-value對的,節點則可以包含子節點以及屬性。
如,下面這個樹就是一個典型結構:
/ {
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
a-byte-data-property = [0x01 0x23 0x34 0x56];
child-node1 {
first-child-property;
second-child-property = <1>;
a-string-property = "Hello, world";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 {
};
};
};
這顆樹顯然沒什麼實際用途,因為它沒有描述任何有意義的內容,但是,我們通過它可以了解什麼是屬性,什麼是節點。這顆樹可以解讀如下:
- 一個根節點: "/"
- 兩個子節點: "node1" 和 "node2"
- node1下面又有兩個子節點:"child-node1" 和 "child-node2"
- 一堆的屬性分散與整顆樹的各個節點上
譯者注:可以這麼簡單理解:節點就是樹枝,屬性就是樹葉;樹枝上可以有再長樹枝也可以長樹葉,而樹葉上則不會再長樹枝。
屬性是基於key-value結構的,value可以為空或者特定格式的字符串內容。由於數據類型並不被編碼到最終的數據結構中,設備樹源代碼中僅能支持有限的幾種基本數據類型。
- text string(以null結束),以雙引號括起來,如:
- string-property = "a string"
- cells 是32位無符號整形數,以尖括號括起來,如
- cell-property = <0xbeef 123 0xabcd1234>
- binary data 以方括號括起來,如:
- binary-property = [0x01 0x23 0x45 0x67];
- 不同類型數據可以在同一個屬性中存在,以逗號分格,如:
- mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
- 多個字符串組成的列表也使用逗號分格,如:
- string-list = "red fish","blue fish";
基本概念
為了搞清楚設備樹該如何使用,我們將一步一步的為一個Sample Machine建立一顆設備樹。
Sample Machine
姑且想象有這麼一台機器,它由"Acme"出產,名為"Coyote's Revenge", 配置如下:
- 32bit ARM CPU
- 處理器的本地內存總線連接如下設備:串口,SPI總線控制器,I2C控制器,終端控制器以及外部總線橋
- 256MB SDRAM 位於地址:0x0
- 兩個串口,位於地址:0x101F1000 及 0x101F2000
- GPIO控制器位於地址:0x101F3000
- SPI控制器位於地址:0x10170000,在它下面連接了如下設備:
- 外部總線橋上接了如下設備:
- SMC91111 以太網控制器,位於地址:0x10100000
- I2C控制器,位於地址:0x10160000,在它下面連接了如下設備:
- Maxim DS1338實時時鐘,I2C地址為0x58
- 64MB NOR flash 位於地址:0x30000000
初始結構
第一步是要建立一個基本結構來使得這顆設備樹能描述對應的Machine
/ {
compatible = "acme,coyotes-revenge";
};
compatible這個屬性用於執行系統名,通常它是以 "廠商,型號" 這樣的字符串形式存在。它能准確的描述對應的設備的特征。
CPU描述
下一部就是描述每一個CPU了。在cpus節點中,每個CPU就是一個子節點。這種情況在多核的ARM Cortex A9系統中很常見。
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
};
每個cpu的子節點中有個compatible屬性,它描述了CPU的具體型號,形式通常也是 "廠商,型號"
當然,CPU的細節不是這麼一個compatible屬性能描述清楚的,後面我們會再逐步加入。
節點名
有必要為節點的命名講幾句。每個節點的名字都應該是這樣的形式: <name>[@<unit-address>]
尖括號是必須的,方括號是可選的。
<name> 是一個ASCII字符串,長度不超過31個字符。通常,節點名都是與設備的類型有關聯的。比如,3com的以太網卡,通常節點名就叫 ethernet 而不叫 3com509
<unit-address>被作為節點名的一部分時,用來描述設備的地址。通常,unit-address就是設備的寄存器地址,這個地址是被列舉在節點的reg屬性中的。在後文中我們會介紹reg屬性的內容。
同級別的兄弟節點的節點名必須唯一(不可重名),但如果name一致而address不一致則是正常情況(比如,serial@101f1000 與 serial@101f2000)。
關於節點的命名規則細節,請參考ePAPR文檔的2.2.1節。
設備
系統中的每個設備都對應著設備樹中的一個節點。好了,下一步我們就是為每個設備都加上對應的節點。
目前,我們僅為每個設備增加一個空節點,待後面介紹了中斷號及地址范圍的概念後再行補充。
/ {
compatible = "acme,coyotes-revenge";
cpus {
cpu@0 {
compatible = "arm,cortex-a9";
};
cpu@1 {
compatible = "arm,cortex-a9";
};
};
serial@101F0000 {
compatible = "arm,pl011";
};
serial@101F2000 {
compatible = "arm,pl011";
};
gpio@101F3000 {
compatible = "arm,pl061";
};
interrupt-controller@10140000 {
compatible = "arm,pl190";
};
spi@10115000 {
compatible = "arm,pl022";
};
external-bus {
ethernet@0,0 {
compatible = "smc,smc91c111";
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
rtc@58 {
compatible = "maxim,ds1338";
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
};
};
};
上面這棵樹中,系統中的每個設備都被添加了一個節點,而且節點的結構真實反應了設備是如何掛載在系統上的。比如,ethernet,i2c等節點是external-bus的子節點,rtc設備是i2c總線下的子節點。通常,設備樹的結構都是以CPU的視角來反應出來的。
上面這棵樹還有幾點不足,它缺少了設備的關鍵信息。這些信息將在後文中逐步添加上去。
關於上面這顆樹,我們還需要注意:
- 每個設備節點都都一個compatible屬性
- flash這個節點的compatible屬性有兩個字串,下面一節將介紹為什麼這麼寫。
- 在前文中曾提到:節點名反應的是設備類型而非設備型號。請參考ePAPR的2.2.2節中列出的常用節點名。
理解compatible屬性
每個設備節點都需要一個compatible屬性。compatible屬性是系統賴以查找對應的設備驅動程序的一個關鍵值,系統就是根據它的值來查找這個設備應該使用哪一個驅動的。
compatible屬性的值是一個字串表。第一個字串以"廠商,型號"的形式描述了准確的設備信息。後面一個字串則表示與它兼容的其他設備。
比如,Freescale MPC8349有一個串口設備是由National Semiconductor ns16550寄存器接口來實現的。所以對MPC8349的串口設備,它的compatible屬性我們就可以這樣寫:compatible = "fsl,mpc8349-uart", "ns16550",對於這樣的情況,fsl,mpc8349-uart准確描述了這個設備,而ns16550則說明了這個設備是與National Semiconductor ns16550寄存器接口兼容的。
注:ns16550它沒有廠商名這個信息,這是由於歷史原因。但所有新創建的compatible屬性都應該有廠商名這個前綴。
compatible屬性的這一特性,使得我們可以讓新設備使用系統中已有的舊驅動。
警告:不要在compatible屬性中使用通配符,如 "fsl,mpc83xx-uart" 或類似的。因為半導體廠商會不定期的更新他們的設計這會破壞你的通配符規則。選擇具有更好兼容性的方案才是正途。
地址是怎麼工作的
可尋址設備是通過使用以下屬性來將地址信息編碼到設備樹中的:
- reg
- #address-cells
- #size-cells
可尋址設備通過reg屬性來獲取寄存器相關的地址信息列表,reg屬性的形式如下:
reg = <address1 length1 [address2 length2] [address3 length3] ... >
每一組address length對應了設備所使用的一個地址區域。
address是一個list,其中包含一個或多個32位整數,我們把它叫做 cells。同理,length也是一個list,可以是多個cell或為空。
由於address和length的長度都是不固定的,所以有了#address-cells和#size-cells這兩個屬性。這兩個屬性被放到父節點中用於描述每個區域有幾個cell。簡單的說,就是reg屬性需要配合父節點的#address-cells和#size-cells來配合使用。為了弄明白它們是怎麼工作的,下面我們就來為這個設備加上地址相關的屬性,先從CPU開始。
CPU地址
CPU節點的地址用法是最簡單的。每個CPU被分配了唯一的ID號,而且這個沒有size。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
在上面的cpus節點中,#address-cells被設為了1,#size-cells則被設為了0,這就表示它的子節點中的reg屬性是一個32位整數的address,而且沒有size部分。
在上面的例子中,你可能注意到了節點名中的寄存器地址與reg數值一樣。
約定俗成的做法是,如果一個節點有reg屬性,則節點名中也需要包含unit-address這個部分,而unit-address的數值則是reg屬性的第一個address值。
內存映射設備
與cpu節點中的這種單地址不同,內存映射設備所分配的是一個地址范圍,而這個地址范圍則是由#size-cells和節點中的reg屬性的size區域來決定的。下面這個例子中,每個address是1個cell(32bit),且每個長度值也是一個cell。在32位系統中#size-cells通常就是這樣設置為1的。而早64位系統中,#address-cells和#size-cells則通常設置為2。
/ {
#address-cells = <1>;
#size-cells = <1>;
...
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
};
interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
};
...
};
每個設備被分配了一個基地址以及一個size。上面的例子中,gpio設備被分配了兩個地址段: 0x101f3000~0x1013fff 以及 0x101f4000~0x101f4fff。
有些設備在系統總線上的地址不連續。比如,一個設備可能通過不連續的片選線連接在外部總線上。
通過在父節點設置合適的#address-cells和#size-cells,地址映射機制可以准確的描述內存映射關系。下面的代碼中展示了一個不同片選信息在是如何使用的。
external-bus {
#address-cells = <2>
#size-cells = <1>;
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
上面的例子中,external-bus使用了兩個cell來描述address;一個表示片選號,另一個表示與片選基地址間的偏移量。length區域則用了一個cell來描述。在這個例子中,每個reg節點包含3個cell,分別是:片選號,偏移量,長度
非內存映射設備
還有一些設備在總線上並不是不是內存映射型的。他們可以有地址空間,但他們沒有通過CPU直接訪問地址空間的能力。取而代之的是他們的父設備驅動擁有間接訪問內存空間的能力。
以I2C設備(不是I2C總線哦)為例子,每個設備被分配了一個地址,沒有length這個字段。看起來實際上與cpu的地址分配很相似。
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
Ranges(地址變換)
上文已經討論過了我們如何分配地址給設備,但是這個所謂的地址僅僅是在設備節點中的本地地址。它無法描述該如何將這些本地的地址映射到CPU能訪問的地址空間中。
根節點中一定有描述CPU的地址空間。子節點所用的地址空間就來自與CPU的地址空間,所以不需要進行額外的地址映射。
如:serial@101f0000 就是直接分配的地址0x101f0000。
如果一個節點它不是跟節點下的子節點,那麼它就不能用CPU的地址空間。為了能將一個地址空間的地址映射到另一個地址空間,range屬性被創造出來了。
下面是在一個簡單的設備樹中增加range屬性。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
...
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
上面的例子中,ranges屬性就定義了一個地址轉換規格。在這個表中的每個節點表示一個地址轉換關系。
ranges屬性中每個字段的大小取決於當前節點的#address-cells,父節點的#address-cells以及當前節點的#size-cells。
比如上面的例子中,external-bus節點的地址長度是2,它的父節點的地址長度是1,size長度是1。所以ranges中的三個地址規則可以這樣解讀:
- CS0,偏移量為0的本地地址被映射到父節點地址空間的 0x10100000~0x1010ffff
- CS1,偏移量為1的本地地址被映射到父節點地址空間的 0x10160000~0x1016ffff
- CS2,偏移量為0的本地地址被映射到父節點地址空間的 0x30000000~0x31000000
更方便的是,如果父節點與子節點的地址空間完全匹配,則子節點可以只定義一個空的ranges屬性。
空ranges屬性所表示的意思就是子節點的地址空間與父節點地址空間是1:1的映射關系。
你可能會問:為什麼上面會寫1:1的映射關系?有些總線結構(如PCI總線)擁有自己獨立的地址空間,但需要向操作系統開放。還有些設備有DMA引擎,這就需要知道總線上的實際地址。還有些時候幾個設備因為使用同樣的物理地址空間而需要組合在一起。在硬件設計以及操作系統的大量特性上決定了地址映射是不是1:1的映射關系。
也可能還注意到了上文中的i2c@1,0節點中沒有ranges屬性。原因是I2C總線不像外部總線,它的地址空間並沒有映射到CPU的地址空間中去。實際上,CPU對rtc@58的訪問是通過i2c@1,0這個設備來間接達成的。沒有ranges屬性正表明了這個設備是不能直接訪問除父節點外的任何設備的特性。
中斷是怎樣工作的
不想地址空間映射表那樣是遵循設備樹的自然結構的(父傳子),中斷信號可以被machine中的任何設備產生或終止。終端信號獨立與設備樹將各個節點關聯起來。描述一個中斷需要4個屬性:
- interrupt-controller 這個屬性沒有值,他表示這個節點是一個接收中斷信號的設備
- #interrupt-cells 這個屬性是一個interrupt-controller節點的屬性,他說明了這個 interrupt-controller的每個中斷說明符(interrupt specifier)有幾個cells,類似#address-cells 與 #size-cells的作用
- interrupt-parent 這是一個設備節點的屬性,用於表明當前設備的中斷是屬於哪一個interrupt-controller的,如果沒有這個屬性,則繼承其父節點的interrupt-parent屬性
- interrupts 這是一個設備節點的屬性,他是 中斷說明符 列表,每一個 中斷說明符 表示此設備的一個中斷信號輸出。
中斷說明符是由一個或多個cell數據(#interrupt-cells )來描述一個設備是與哪一個終端信號輸入設備相連接的。多數設備都只有一個中斷信號輸出,如下面的例子,但也有可能存在一個設備有多個終端信號輸出的情況。中斷說明符的含義與具體的終端控制器( interrupt-controller)有關。每個終端控制器都可以決定它的輸入信號的中斷說明符有幾個cell數據。
下面的代碼中為我們的Coyote's Revenge添加了中斷連接:
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
interrupts = < 2 0 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
interrupts = < 3 0 >;
};
intc: interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;
#interrupt-cells = <2>;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = < 4 0 >;
};
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
interrupts = < 5 2 >;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = < 6 2 >;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
有些細節需要各位注意:
- 此machine僅有一個中斷控制器:interrupt-controller@10140000
- 標簽"intc:"被加到了中斷控制器的節點上,這個標簽在父節點上創建了一個phandle,這個phandle就是父節點的interrupt-parent。所以這個中斷控制器就成了系統所有子節點的默認終端控制器,只有當子節點明確的聲明了其interrupt-parent才會被覆蓋。
- 每個設備使用interrupt屬性來區分不同的終端輸入線。
- #interrupt-cells(在interrupt-controller@10140000節點中)的值是2,所以,每個中斷說明符由兩個cell數據組成。這個例子中用的是最常見的 中斷說明符 形式,第一個cell表示中斷線的序號,第二個cell表示終端類型的flag(表示高有效,低有效。。。),對於不同的終端控制器,需要閱讀對應的binding document來得知其 中斷說明符 的格式。
設備特定數據
在常用的屬性之外,我們還能為一個節點自行添加屬性及子節點。只要是系統需要的任何數據都能被我們按特定的規則來添加。
首先,新設備特定的屬性名需要使用 廠商前綴,這樣能避免與系統已有的標准屬性名稱沖突。
第二,每個屬性都應該在某個binding文檔中有相關的說明,這樣驅動作者才能知道如何使用這些屬性數據。
第三,將新的binding資料在
[email protected]中post出來,大家對binding的代碼審查將能規避大部分常識性錯誤。
特殊節點
aliases 節點
這個特殊節點用來引用一個長路徑,比如/external-bus/ethernet@0,0,它是長路徑的縮寫或叫別名。比如:
aliases {
ethernet0 = ð0;
serial0 = &serial0;
};
使用別名來標識一個設備是備受操作系統歡迎的做法。
chosen節點
chosen節點並不代表一個真正的設備,而是用來在Firmware與操作系統間傳遞數據,如啟動參數。
通常chosen節點在dts中被置空。
在我們這個例子中,被添加了如下的啟動參數:
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};