背景
通過上一講了解完設備樹DTS有關概念,我們這一講就來基於設備樹例程,學習設備樹的語法規則。
設備樹框架
1
個dts文件 + n
個dtsi文件,它們編譯而成的dtb文件就是真正的設備樹。
基於同樣的軟件分層設計的思想,由於一個SoC可能對應多個machine,如果每個machine的設備樹都寫成一個完全獨立的.dts文件,那么勢必相當一些.dts文件有重復的部分。
為了解決這個問題,Linux設備樹目錄把一個SoC公用的部分或者多個machine共同的部分提煉為相應的.dtsi文件。
這樣每個.dts就只有自己差異的部分,公有的部分只需要"include"相應的.dtsi文件, 以保證整個設備樹的管理更加有序。
以solidrun公司的hummingboard為例,其組成為imx6dl-hummingboard.dts |_imx6dl.dtsi | |_imx6qdl.dtsi |_imx6qdl-microsom.dtsi |_imx6qdl-microsom-ar8035.dtsi
此外,dts/dtsi兼容c語言的一些語法,能使用宏定義,也能包含.h文件
設備樹用樹狀結構描述設備信息,它有以下幾種特性:
- 每個設備樹文件都有一個根節點,每個設備都是一個節點。
- 節點由 節點名 + 屬性 組成。
- 節點間可以嵌套,形成父子關系,這樣就可以方便的描述設備間的關系。
- 每個設備的屬性都用一組key-value對(鍵值對)來描述。
- 每個屬性的描述用
;
結束
所以,一個設備樹的基本框架可以寫成下面這個樣子,一般來說,/表示板子,它的子節點node1表示SoC上的某個控制器,控制器中的子節點node2表示掛接在這個控制器上的設備(們)。
/ { //根節點
node1{ //node1是節點名,是/的子節點
key=value; //node1的屬性
...
node2{ //node2是node1的子節點
key=value; //node2的屬性
...
}
} //node1的描述到此為止
node3{
key=value;
...
}
}
以下是一顆最簡單的設備樹:
注意/dts-v1/;
是必須的,有時候正是因為忽略了它而引起了syntax error
且沒有其他提示。
/dts-v1/;
/ {
};
節點node
{}包圍起來的結構稱之為節點,dts中最開頭的/ {},稱為根節點。
在節點中,以key = value
代表節點屬性。
樹中每個表示一個設備的節點都需要一個 compatible 屬性。
節點名 name
- 節點名稱:每個節點名格式為:
<name>[@<unit_address>]
,其中:-
:設備名,就是一個不超過31位的簡單 ascii 字符串,節點的命名應該根據它所體現的是什么樣的設備。 - <unit_address> :設備地址,用來唯一標識一共節點。沒有指定<unit_address>時,同級節點命名必須是唯一的;但只要<unit_address>不同,多個節點也可以使用一樣的通用名稱。
-
下面是典型節點名的寫法:
/ {
model = "Freescale i.MX23 Evaluation Kit";
compatible = "fsl,imx23-evk", "fsl,imx23";
memory {
reg = <0x40000000 0x08000000>;
};
// 注意這里
apb@80000000 {
...
};
}
上面的節點名是apb,節點路徑是/apb@80000000 ,這點要注意,因為根據節點名查找節點的API的參數是不能有"@xxx"這部分的。
Linux中的設備樹還包括幾個特殊的節點:比如chosen,chosen節點不描述一個真實設備,而是用於firmware傳遞一些數據給OS,比如bootloader傳遞內核啟動參數給內核
/include/ "zynq-7000.dtsi"
/ {
model = "Zynq ZC702 Development Board";
compatible = "xlnx,zynq-zc702", "xlnx,zynq-7000";
...
chosen {
bootargs = "console=ttyPS1,115200 earlyprintk";
};
};
引用
當我們找一個節點的時候,我們必須書寫完整的節點路徑,這樣當一個節點嵌套比較深的時候就不是很方便。所以,設備樹允許我們用下面的形式為節點標注引用(起別名),借以省去冗長的路徑。
標號引用常常還作為節點的重寫方式,用於修改節點屬性。
- 格式:
- 聲明別名:
別名 : 節點名
- 訪問 : &
別名
- 聲明別名:
編譯設備樹的時候,相同的節點的不同屬性信息都會被合並,相同節點的相同的屬性會被重寫(覆蓋前值),使用引用可以避免移植者四處找節點,直接在板級.dts增改即可。
/include/ "imx53.dtsi"
/ {
model = "Freescale i.MX53 Automotive Reference Design Board";
compatible = "fsl,imx53-ard", "fsl,imx53";
memory {
reg = <0x70000000 0x40000000>;
};
eim-cs1@f4000000 {
#address-cells = <1>;
#size-cells = <1>;
compatible = "fsl,eim-bus", "simple-bus";
reg = <0xf4000000 0x3ff0000>;
lan9220@f4000000 {
compatible = "smsc,lan9220", "smsc,lan9115";
reg = <0xf4000000 0x2000000>;
phy-mode = "mii";
interrupt-parent = <&gpio2>; // 直接使用引用
vdd33a-supply = <®_3p3v>;
};
};
regulators {
compatible = "simple-bus";
reg_3p3v: 3p3v { // 定義一個引用
compatible = "regulator-fixed";
regulator-name = "3P3V";
};
};
...
// 引用一個節點,新增/修改其屬性。
®_3p3v {
regulator-always-on;
}
節點屬性 property
屬性一般由
key = value;
鍵值對構成。
Linux設備樹語法中定義了一些具有規范意義的屬性,包括:compatible, address, interrupt等,這些信息能夠在內核初始化找到節點的時候,自動解析生成相應的設備信息。
此外,還有一些Linux內核定義好的,一類設備通用的有默認意義的屬性,這些屬性一般不能被內核自動解析生成相應的設備信息,但是內核已經編寫的相應的解析提取函數,常見的有 "mac_addr","gpio","clock","power"。"regulator" 等等。
- 簡單的鍵-值對,它的值可以為空或者包含一個任意字節流。雖然數據類型並沒有編碼進數據結構,但在設備樹源文件中任有幾個基本的數據表示形式:
- 文本字符串(無結束符)可以用雙引號表示:
string-property = "a string"
- Cells是 32 位無符號整數,用尖括號限定:
cell-property = <0xbeef 123 0xabcd1234>
- 二進制數據用方括號限定:
binary-property = [01 23 45 67];
- 不同表示形式的數據可以使用逗號連在一起:
mixed-property = "a string", [01 23 45 67], <0x12345678>;
- 逗號也可用於創建字符串列表:
string-list = "red fish", "blue fish";
- 混合形式:上述幾種的混合形式
- 文本字符串(無結束符)可以用雙引號表示:
compatible 兼容性
如果一個節點是設備節點,那么它一定要有compatible(兼容性),因為這將作為驅動和設備(設備節點)的匹配依據,compatible(兼容性)的值可以有不止一個字符串以滿足不同的需求。(設備節點中對應的節點信息已經被內核構造成struct platform_device。驅動可以通過相應的函數從中提取信息。)
compatible屬性是用來查找節點的方法之一,另外還可以通過節點名或節點路徑查找指定節點。
而根節點的compatible也是非常重要的,一般在系統啟動以后,用於識別對應系統一些東西,並由此進行對應的初始化。
- 格式:
compatible = "<manufacturer>,<model>" [, "<manufacturer>,<model>"]
- manufacturer指定廠家名,model指定特定設備型號;后續的<manufacturer,model>指定兼容的設備型號(其中,后續的
<manufacturer>
可空,第二個model也可空)。
我們來看 compatible 是如何與 驅動捆綁在一起的:
可以看出,驅動中用於匹配的結構使用的compatible和設備樹中一模一樣,否則就可能無法匹配,這里另外的一點是struct of_device_id數組的最后一個成員一定是空,因為相關的操作API會讀取這個數組直到遇到一個空。
- manufacturer指定廠家名,model指定特定設備型號;后續的<manufacturer,model>指定兼容的設備型號(其中,后續的
1)先隨便在設備樹中出一個網卡設備,關鍵是找到 compatible
屬性中的 <model>
值。
// 文件節選於:arch/arm/boot/dts/vexpress-v2m-rs1.dtsi
ethernet@2,02000000 {
compatible = "smsc,lan9118", "smsc,lan9115";
reg = <2 0x02000000 0x10000>;
interrupts = <15>;
phy-mode = "mii";
reg-io-width = <4>;
smsc,irq-active-high;
smsc,irq-push-pull;
vdd33a-supply = <&v2m_fixed_3v3>;
vddvario-supply = <&v2m_fixed_3v3>;
};
2)在驅動中(為了方便讀者理解,這里在內核源碼根目錄下查找,實際上就是在driver目錄中),找到對應的.compatible
關鍵字所在的文件以及行數。
$ find . 2>/dev/null | grep lan9115
arch/arm/boot/dts/vexpress-v2m-rs1.dtsi:50: compatible = "smsc,lan9118", "smsc,lan9115";
arch/arm/boot/dts/vexpress-v2m.dtsi:49: compatible = "smsc,lan9118", "smsc,lan9115";
drivers/net/ethernet/smsc/smsc911x.c:2578: { .compatible = "smsc,lan9115", },
3)順藤摸瓜,找到所在行,也就找到了用來描述設備信息的結構體of_device_id
。
// 節選於 drivers/net/ethernet/smsc/smsc911x.c
#ifdef CONFIG_OF
static const struct of_device_id smsc911x_dt_ids[] = {
{ .compatible = "smsc,lan9115", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, smsc911x_dt_ids);
#endif
可以看出,驅動中用於匹配的結構使用的compatible和設備樹中一模一樣,且,字符串需要嚴格匹配。
注:這里另外的一點是
struct of_device_id
數組的最后一個成員一定是空,因為相關的操作API會讀取這個數組直到遇到一個空。
i2c和spi驅動還支持一種“別名匹配”的機制,就以pcf8523為例,假設某程序員在設備樹中的pcf8523設備節點中寫了compatible = "pcf8523";,顯然相對於驅動id_table中的"nxp,pcf8523",他遺漏了nxp字段,但是驅動卻仍然可以匹配上,因為別名匹配對compatible中字符串里第二個字段敏感。
驅動程序將直接和設備樹里的設備節點進行配對,是通過設備節點中的compatible(兼容性)來與設備節點進行配對的,具體的應用詳見 基於i2c子系統的驅動分析、 基於platform總線的驅動分析
address 地址屬性
有關節點的地址,比如i2c@021a0000,雖然它在名字后面跟了地址,但是正式的設置是在reg屬性中設置。
(幾乎)所有的設備都需要與CPU的IO口相連,所以其IO端口信息就需要在設備節點節點中說明。常用的屬性有:
#address-cells = <CNT>
,用來描述子節點"reg"屬性的地址表中用來描述首地址的cell的數量#size-cells = <CNT>
, 用來描述子節點"reg"屬性的地址表中用來描述地址長度的cell的數量。reg = <address ... length>
: address 代表基地址, length 代表長度。基址和長度的格式是可變的,addr由父節點的#address-cells
個uint32值組成,len由父節點的#size-cells
個uint32值組成。表明了設備使用的一個地址范圍。
例如:
aips-bus@02000000 { /* AIPS1 */
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
rtc: rtc@68 {
compatible = "stm,mt41t62";
reg = <0x68>;
};
};
};
/*
我們知道,aips-bus@02000000 是 i2c@021a0000 的父節點;i2c@021a0000 是 rtc@68 的父節點。
aips-bus@02000000的 #address-cells 和#size-cells均為1,所以 i2c@021a0000 中的 `reg` 格式為: `<address length>`
i2c@021a0000的 #address-cells 和#size-cells分別為1和0,所以 rtc@68 中的 `reg` 格式為: `<address>`
通俗來講,如果現在有 一個節點A的 #address-cells 和#size-cells分別為2和1;那么A的子節點B 的 `reg`格式為 `<address address length>`
*/
interrupts 中斷屬性
中斷產生設備用interrupts屬性描述中斷源(interrupt specifier),因為不同的硬件描述中斷源需要的數據量不同,所以interrupts屬性的類型也是
。為了明確表示一個中斷由幾個u32表示,又引入了#interrupt-cells屬性,#interrupt-cells屬性的類型是u32,假如一個中斷源需要2個u32表示(一個表示中斷號,另一個表示中斷類型),那么#interrupt-cells就設置成2。
有些情況下,設備樹的父節點不是中斷的父節點(主要是中斷控制器一般不是父節點),為此引入了interrupt-parent屬性,該屬性的類型是,用來引用中斷父節點(我們前邊說過,一般用父節點的標簽,這個地方說中斷父節點而不是中斷控制器是有原因的)。如果設備樹的父節點就是中斷父節點,那么可以不用設置interrupt-parent屬性。interrupts屬性和interrupt-parent屬性都是中斷產生設備節點的屬性,但是#interrupt-cells屬性不是,#interrupt-cells屬性是中斷控制器節點以及interrupt nexus節點的屬性,這兩類節點都可能是中斷父節點。
一個計算機系統中大量設備都是通過中斷請求CPU服務的,所以設備節點中就需要在指定中斷號。常用的屬性有:
interrupt-controller
: 一個空屬性用來聲明這個node接收中斷信號,即這個node是一個中斷控制器。#interrupt-cells
:是中斷控制器節點的屬性,用來標識這個控制器需要幾個單位做中斷描述符,用來描述子節點中interrupts
屬性使用了父節點中的interrupts
屬性的具體的哪個值。一般,如果父節點的該屬性的值是3,則子節點的interrupts一個cell的三個32bits整數值分別為:<中斷域 中斷 觸發方式>
,如果父節點的該屬性是2,則是<中斷 觸發方式>
interrupt-parent
:標識此設備節點屬於哪一個中斷控制器,如果沒有設置這個屬性,會自動依附父節點的interrupts
:一個中斷標識符列表,表示每一個中斷輸出信號。reg
: 在schips todo
這里重點說明一下,interrupts
屬性,在ARM GIC(Generic Interrupt Controller)中:
備注:ARM GIC 說明文檔位於:
Documentation/devicetree/bindings/arm/gic.txt
;此外,本人並沒有找到#interrupt-cells
為1個時的文檔說明。
當interrupt-cells
為3時,interrupts包含三個cells,如interrupts = <0 168 4>
:
第一個cell代表中斷類型:0 表示SPI中斷,1 表示PPI中斷。
第二個cell代表具體的中斷類型:、
- PPI中斷:私有外設中斷(Private Peripheral Interrupt),是每個CPU私有的中斷。最多支持16個PPI中斷,范圍【0 - 15】。
- SPI中斷類型:公用外設中斷(Shared Peripheral Interrupt),最多可以支持988個外設中斷,范圍【0 - 987】。
第三個cell代表中斷觸發標志:
- bits [ 3 :0 ] 觸發類型和級別標志:
1 = 低- 至- 高邊沿觸發
2 = 高- 到- 低邊沿觸發
4 = 活躍的高水平 - 敏感
8 = 低電平有效 - 敏感- bits [ 15 :8 ] PPI中斷cpu掩碼。每個位對應於每個位附加到GIC的8個可能的cpu。指示設置為"1"的位中斷被連接到該CPU 。只有有效的PPI中斷。
當interrupt-cells
為2時,interrupts包含2個cells,如interrupts = <2 4>
:
第一個cell代表具體的中斷類型:
- SGI中斷:軟件觸發中斷(Software Generated Interrupt),通常用於多核間通訊,最多支持16個SGI中斷,硬件中斷號從ID0~ID15。
- PPI中斷:私有外設中斷(Private Peripheral Interrupt),是每個CPU私有的中斷。最多支持16個PPI中斷,硬件中斷號從ID16~ID31。
- SPI中斷類型:公用外設中斷(Shared Peripheral Interrupt),最多可以支持988個外設中斷,硬件中斷號從ID32~ID1019。
第二個cell代表中斷觸發標志:
bits [ 3 :0 ] 觸發類型和級別標志:
1 = 低- 至- 高邊沿觸發
2 = 高- 到- 低邊沿觸發
4 = 活躍的高水平- 敏感
8 = 低電平有效- 敏感
bits [ 15 :8 ] PPI中斷cpu掩碼。每個位對應於每個位附加到GIC的8個可能的cpu。指示設置為"1"的位中斷被連接到該CPU 。只有有效的PPI中斷。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;//指定依附的中斷控制器是intc
serial@101f0000 { //子節點:串口設備
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
};
intc: interrupt-controller@10140000 { //intc中斷控制器
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;//定義為中斷控制器設備
#interrupt-cells = <2>;
};
}
GPIO 屬性
gpio也是最常見的IO口,常用的屬性有:
- "gpio-controller",用來說明該節點描述的是一個gpio控制器
- "#gpio-cells",用來描述gpio使用節點的屬性一個cell的內容,即 `屬性 = <&引用GPIO節點別名 GPIO標號 工作模式>
通過上面的屬性定義以后,就可以使用它,例如:
2 &spi_1 {
1 status = "okay";
388 cs-gpios = <&gpa2 5 GPIO_ACTIVE_HIGH>; // 使用 GPIO A2 第5個引腳,
1
2 w25q80bw@0 {
3 #address-cells = <1>;
4 #size-cells = <1>;
5 compatible = "w25x80";
6 reg = <0>;
7 spi-max-frequency = <1000000>;
8
9 controller-data {
10 samsung,spi-feedback-delay = <0>;
11 };
12
驅動自定義key屬性
針對具體的設備,有部分屬性很難做到通用,需要驅動自己定義好。
可以在設備樹中自定義key屬性,再在驅動中通過內核的屬性提取解析函數進行值的獲取。
比如:
/* 有關的 設備樹寫法 */
6 ethernet@2,02000000 {
5 compatible = "smsc,lan9118", "smsc,lan9115";
4 reg = <2 0x02000000 0x10000>;
3 interrupts = <15>;
2 phy-mode = "mii";
1 reg-io-width = <4>;
55 smsc,irq-active-high; // 自定義key
1 smsc,irq-push-pull; // 自定義key
2 vdd33a-supply = <&v2m_fixed_3v3>;
3 vddvario-supply = <&v2m_fixed_3v3>;
4 };
5
6 usb@2,03000000 {
7 compatible = "nxp,usb-isp1761";
8 reg = <2 0x03000000 0x20000>;
9 interrupts = <16>;
arch/arm/boot/dts/vexpress-v2m-rs1.dtsi
/* 有關的驅動寫法 */
2389 if (of_get_property(np, "smsc,irq-active-high", NULL))
1 config->irq_polarity = SMSC911X_IRQ_POLARITY_ACTIVE_HIGH;
2
3 if (of_get_property(np, "smsc,irq-push-pull", NULL))
4 config->irq_type = SMSC911X_IRQ_TYPE_PUSH_PULL;
5
6 if (of_get_property(np, "smsc,force-internal-phy", NULL))
7 config->flags |= SMSC911X_FORCE_INTERNAL_PHY;
8
9 if (of_get_property(np, "smsc,force-external-phy", NULL))
10 config->flags |= SMSC911X_FORCE_EXTERNAL_PHY;
11
12 if (of_get_property(np, "smsc,save-mac-address", NULL))
13 config->flags |= SMSC911X_SAVE_MAC_ADDRESS;
14
15 return 0;
drivers/net/ethernet/smsc/smsc911x.c
附錄:補充對於interrupt-parent的一些知識點
為什么會有interrupt-parent?
首先講講Linux設備管理中對中斷的設計思路演變。隨着linux kernel的發展,在內核中將interrupt controller抽象成irqchip這個概念越來越流行,甚至GPIO controller也可以被看出一個interrupt controller chip,這樣,系統中至少有兩個中斷控制器了。另外,在硬件上,隨着系統復雜度加大,外設中斷數據增加,在這種趨勢下,內核中原本的中斷源直接到中斷號的方式已經很難繼續發展了,為了解決這些問題,linux kernel的大牛們就創造了irq domain(中斷域)這個概念。domain在內核中有很多,除了irqdomain,還有power domain,clock 這些domain等等;所謂domain,就是領域,范圍的意思(即:任何的定義出了這個范圍就沒有意義了)。
實際上系統可以需要多個中斷控制器進行級聯,形成事實上的硬件中斷處理結構:
如上所述,系統中所有的interrupt controller會形成樹狀結構,對於每個interrupt controller都可以連接若干個外設的中斷請求(interrupt source,中斷源),interrupt controller會對連接其上的interrupt source(根據其在Interrupt controller中物理特性)進行編號(也就是HW interrupt ID了)。有了irq domain這個概念之后,這個編號僅僅限制在本interrupt controller范圍內。
有了這樣的設計,CPU(Linux 內核)就可以根據級聯的規則一級一級的找到想要訪問的中斷。當然,通常我們關心的只是內核中的中斷號,具體這個中斷號是怎么找到相應的中斷源的,我們作為程序員往往不需要關心。
在寫設備樹的時候,設備樹就是要描述嵌入式軟件開發中涉及的所有硬件信息。所以,設備樹就需要准確描述硬件上處理中斷的這種樹狀結構,如此,就有了我們的interrupt-parant這樣的概念:用來連接這樣的樹狀結構的上下級,用於表示這個中斷歸屬於哪個interrupt controller,比如,一個接在GPIO上的按鍵,它的組織形式就是:
中斷源--interrupt parent-->GPIO--interrupt parent-->GIC1--interrupt parent-->GIC2--...-->CPU
有了parant,我們就可以使用一級一級的偏移量來最終獲得當前中斷的絕對編號。
可以看出,在我板子上的dm9000的的設備節點中,它的"interrupt-parent"引用了"exynos4x12-pinctrl.dtsi"(被板級設備樹的exynos4412.dtsi包含)中的gpx0節點:
而在gpx0節點中,指定了#interrupt-cells = <2>;
,所以在dm9000中的屬性interrupts = <6 4>;
表示dm9000的的中斷在作為irq parant的gpx0中的中斷偏移量,即gpx0中的屬性interrupts
中的<0 22 0>
,通過查閱exynos4412的手冊知道,對應的中斷號是EINT[6]。