Linux and the Device Tree
————————-
The Linux usage model for device tree data
本文閱讀翻譯自linux核心的說明文件usage-model.txt
本文講述linux如何使用device tree,關於device tree資料型別的詳細描述可以參考文件:
或者,無法FQ的話,
The “Open Firmware Device Tree”, or simply Device Tree (DT)是一種描述硬體的資料結構或者說語言, 更具體地說,DT是一種作業系統可讀的硬體描述語言,這樣作業系統就不必把硬體詳細資訊硬編碼到程式碼裡。
從結構上來講,DT是一種樹形結構,或者說一種帶有命名節點的非迴圈(acyclic)圖。每一個節點,可能有任意個命名屬性和任意資料的鍵值對。也有一種機制,可以讓一個節點和樹形結構之外的節點建立連線。
從概念上來講,有一種名為繫結(bindings)的通用的約定來描述,DT的資料如何來描述物理硬體的資訊,資料匯流排、中斷線、GPIO連線和外圍裝置。
在實際操作中,硬體資訊應該儘可能的用已經存在的“繫結”來描述,以便最大化的利用現有的程式碼,但是實際上“屬性”和“節點”都是簡單的文字字串,它可以很容易的用來擴充套件現有的“繫結”或者建立一種新的“繫結”。不管如何,建立一種新的“繫結”需要非常小心,在這之前,一定要先做做功課,瞭解現有的“繫結”有哪些。現在就有兩種關於i2c匯流排的描述方法,這是因為一種新的i2c裝置描述方法建立之前,沒有去調查i2c裝置已經如何在系統中描述。
1 History
DT最初被OPEN Firmware建立,作為一種從OPEN Firmware傳輸資料到客戶端程式(比如一個作業系統)的傳輸方法的一部分。作業系統利用DT來實時的獲取硬體拓撲結構,從而可以支援多數的硬體裝置,而不用將硬體資訊硬編碼到程式碼中。(假設驅動程式可用於所有裝置)
由於Open Firmware被廣泛應用有PowerPC和SPARC平臺,linux早已長期使用裝置樹(Device Tree)來支援這些體系結構。
在2005年,當PowerPC linux開始一次重大的程式碼清理以合併對32-bit和64bit的支援。當時決定所有的PowerPC平臺都需要支援DT,而不管它是否使用了Open Firmware。為了達到這一目標,一個被稱為FDT(Flattened Device Tree)的DT版本被設計出來,它可以以二進位制的形式傳給kernel,而不需要一個真正的Open Firmware。uboot、kexec或者其他bootloaders也都通過修改,支援了傳遞二進位制形式的DT檔案(DTB)和在啟動階段修改dtb檔案。DT也新增到了PowerPC的引導啟動包裝器( arch/powerpc/boot/*)中,所以dtb檔案也可以被打包到kernel image檔案中來支援在啟動階段沒有DT的韌體
在一段時間以後,FDT已經應用到了所有的架構。在本文編寫的時候,6個主線架構( arm, microblaze, mips, powerpc, sparc, and x86)和一個非主線架構(nios)都在一定程度支援了DT。
2 Data Model
如果你還沒有讀過DT的使用說明(https://elinux.org/Device_Tree_Usage),那麼趕緊去讀吧!
請讀完之後,在繼續閱讀本文。
2.1 High Level View
理解DT,最重要的事情是,DT就是一種簡單的,用來描述硬體資訊用的資料資料結構。它沒有什麼魔力,更沒有什麼魔法來解決所有的硬體配置問題。它能做的,就是提供一種語言,讓板級硬體配置和linux核心(或其他支援DT的作業系統)中支援的裝置驅動去耦。使用DT,能讓對板卡和裝置的支援變成資料驅動;在啟動過程中的一些決策將基於傳入kernel的資料,而不是硬編碼到核心本身。
理想情況下,資料驅動的平臺啟動方式能夠減少kernel中的程式碼拷貝,能夠用簡簡單單一個kernel image來支援很多的不同硬體裝置。
linux使用DT資料有三個主要的目的:
1)平臺識別
2)執行時配置
3)裝置資訊管理
2.2 平臺識別
首先,kernel使用DT資料來識別具體的機器型號。在完美的世界裡,由於所有的平臺細節都能由DT以一種一致且可靠的方式完美描述,特定的平臺型別對於kernel來說就不那麼重要了。但是硬體不那麼完美,所以kernel必須在啟動的早期就能識別機器的型號,從而能夠有機會執行特定機器對應的特定程式碼。
在大多數情況下,機器型號識別都是硬體無關的,kernel將根據機器的核心CPU或者Soc來選擇setup程式碼。以ARM為例,函式setup_arch( arch/arm/kernel/setup.c)呼叫函式 setup_machine_fdt( arch/arm/kernel/devicetree.c )來搜尋裝置描述表( machine_desc),選擇出與DT資料最匹配的裝置型號。它通過將DT資料的根節點的 `compatible` 屬性與 machine_desc結構體的 dt_compat欄位來進行比較,來找到最匹配的裝置型號。
`compatible`屬性包含一個有序的字串列表,字串以準確的機器名字為起始,然後跟著的是一個可選的板卡列表,按照匹配性的高低來排列。比如說,Ti BeagleBoard以及其後續版本 BeagleBoard xM board的根`compatible`屬性可能可以這樣來寫:
compatible = “ti,omap3-beagleboard”, “ti,omap3450”, “ti,omap3”;
compatible = “ti,omap3-beagleboard-xm”, “ti,omap3450”, “ti,omap3”;
這裡的”ti,omap3-beagleboard-xm”指定了準確的裝置型號,它也表明它與 OMAP 3450 SoC相容,屬於ti的omap3系列Soc。 您會注意到,這個compatible列表是從最特定的(確切的板)到最不特定的(SoC族)排序的。
精明的讀者可能會指出,BeagleBoard xM board也可以宣告與原始版本的BeagleBoard相匹配相容的。然而,在板卡級別這樣做必須非常小心謹慎,因為通常情況下,即使在同一產品線中,從一個板到另一個板之間也會有很大程度的變化,當一個板聲稱與另一個板相容時,很難確切地確定這意味著什麼。從更高層次看,寧可謹慎行事,也不要聲稱一個板卡與另一個板卡相容。值得注意的例外是,一個板卡是另一個的載板時,比如一個附加在載板的CPU模組。
關於“ compatible”,還有一個值得注意事項是, compatible屬性中使用任何字串都必須根據它所指示的內容進行記錄,並將“ compatible”字串歸檔。(Documentation/devicetree/bindings)
仍然以ARM為例,對於每一項 machine_desc,核心都會判斷其 dt_compat是否出現在“ compatible”屬性中。如果在,則該machine_desc將作為裝置啟動的一個候選。在搜尋了整個machine_desc描述表之後,函式 setup_machine_fdt返回最匹配的machine_desc。如果沒有匹配的machine_desc,則函式返回NULL。
這種方案背後的原因是,大多數情況下,一個machine_desc可以支援很多種類的板卡,如果他們使用同樣的Soc,或者同系列的Socs。然兒,總會有一些例外情況,特定的板卡需要特殊的啟動程式碼,而這些啟動程式碼在通常版本下是沒有用的。當然,也可以在通用的程式碼中顯示地檢查板卡來處理特殊情況,但是這樣會迅速的讓程式碼變得ugly和不可維護,如果特殊情況不止一兩處的話。
相反,相容性(“ compatible”)列表通過在dt_compat列表中指定“不相容”值來支援廣泛的通用板卡集。在上面的例子中,通用板卡的支援,可以把“ compatible”屬性宣告為 “ti,omap3” or “ti,omap3450″。如果在最初的 beagleboard板卡上發現有問題,需要在早期啟動階段,載入一些特殊的程式碼來解決問題。 那麼可以新增一個新的machine_desc,來實現這些程式碼,並且其屬性只與 “ti,omap3-beagleboard”匹配。
PowerPC使用一種稍微不同的方案,它從每個machine_desc呼叫.probe()鉤子,並使用返回TRUE的第一個。然而,這種方法無法實現相容性(“ compatible”)列表的優先順序,因此,新的體系結構可能應該避免使用這種方法。
2.3 Runtime configuration
在大部分情況下,DT是韌體和核心之間資料通訊的唯一方法,因此也被用來傳輸執行時配置資料,比如核心引數字串和initrd映象的位置。
這種資料大部分被包含在/chosen節點下,在linux啟動中,執行時配置引數,經常如下面的程式碼所示:
chosen { bootargs = “console=ttyS0,115200 loglevel=8”;
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>; };
bootargs屬性包含核心引數,以及initrd-*屬性定義了initrd映象的地址和大小。值得注意的是,initrd-end是initrd映象之後的第一個地址,所以這裡也與通常的結構體資源語義不同。chosen節點還可以為平臺特定的配置資料,包含任意數量的額外屬性(可選)。
在早期的引導階段,在分頁初始化完成之前,架構設定程式碼使用不同的回撥函式呼叫of_scan_flat_dt函式來解析裝置樹資料(device tree data)。of_scan_flat_dt函式掃描裝置樹,然後使用幫助程式提取早期引導所需的資訊。舉例來說,early_init_dt_scan_chosen()函式一般被用來解析包含核心引數的“chosen”節點,early_init_dt_scan_root()函式用來初始化DT地址空間模型,early_init_dt_scan_memory()函式用來確定可用RAM的大小和地址。
在ARM架構下,setup_machine_fdt()函式負責在選擇了正確的machine_desc之後,對裝置樹進行早期的掃描。
2.4 Device population
在完成了板卡識別,以及早期配置資料解析之後, 核心初始化就可以以正常的方式進行了。在這個過程的某些時候,unflatten_device_tree()函式被用來將DT資料轉換成執行時效率更高的形式。這時,machine-specific的配置回撥函式也會被呼叫,比如ARM架構的machine_desc .init_early()函式,.init_irq() 和 .init_machine() 回撥函式。 本節的其餘部分將使用來自ARM實現的示例,但是在使用DT時,所有架構都將做幾乎相同的事情。
顧名思義,.init_early()函式的作用是,執行一些板卡特定的,需要在啟動過程的早期執行的配置項,.init_irq()函式是用於配置中斷處理。使用DT並不會實質性的改變這些函式的行為。如果提供了DT,則.init_early() 和 .init_irq()函式能夠呼叫任意DT獲取函式(of_* in include/linux/of*.h)來從平臺獲取額外的資料。
在DT使用過程中,最有趣的回撥函式是.init_machine(),它主要負責用平臺有關的資料,管理linux裝置模型。過去,這個函式在嵌入式平臺,是通過在板級支援.c檔案中,定義一系列的靜態時鐘結構體,platform_devices以及其他資料,並大量的註冊來完成的。在使用了DT資料之後,就不再為每個平臺硬編碼描述靜態裝置,裝置列表可以通過解析DT資料獲取,然後動態申請裝置結構體。
最簡單的例子是 .init_machine() 函式只負責註冊platform_devices塊,platform_device是Linux中的一個概念,用於表示記憶體或者I/O對映裝置(硬體無法檢測到這些裝置)以及“複合”或“虛擬”裝置(稍後將詳細介紹)。但是DT中並沒有platform_device的這樣的術語,platform_device大致對應於裝置樹的根節點以及簡單記憶體對映匯流排節點的子節點。
現在是時候舉出一個例子了,下面是NVIDIA Tegra板卡的DT的一部分:
/{ compatible = "nvidia,harmony", "nvidia,tegra20"; #address-cells = <1>; #size-cells = <1>; interrupt-parent = <&intc>; chosen { }; aliases { }; memory { device_type = "memory"; reg = <0x00000000 0x40000000>; }; soc { compatible = "nvidia,tegra20-soc", "simple-bus"; #address-cells = <1>; #size-cells = <1>; ranges; intc: interrupt-controller@50041000 { compatible = "nvidia,tegra20-gic"; interrupt-controller; #interrupt-cells = <1>; reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >; }; serial@70006300 { compatible = "nvidia,tegra20-uart"; reg = <0x70006300 0x100>; interrupts = <122>; }; i2s1: i2s@70002800 { compatible = "nvidia,tegra20-i2s"; reg = <0x70002800 0x100>; interrupts = <77>; codec = <&wm8903>; }; i2c@7000c000 { compatible = "nvidia,tegra20-i2c"; #address-cells = <1>; #size-cells = <0>; reg = <0x7000c000 0x100>; interrupts = <70>; wm8903: codec@1a { compatible = "wlf,wm8903"; reg = <0x1a>; interrupts = <347>; }; }; }; sound { compatible = "nvidia,harmony-sound"; i2s-controller = <&i2s1>; i2s-codec = <&wm8903>; }; };
在.init_machine()函式呼叫的時候,Tegra板卡支援程式碼需要檢視這些DT資料,然後決定為哪些節點建立platform_devices。然後,從裝置樹(Device Tree),並不能直接看出節點所代表的裝置型別,甚至是否代表裝置都不能判斷。“/chosen”,“/aliases”,“/memory”節點是資訊性的節點,並不描述裝置(儘管記憶體也可能被考慮為裝置)。/soc節點的子節點是記憶體對映裝置,但是“codec@1a”是一個i2c裝置,而“sound”節點不代表裝置, 而是其他裝置如何連線在一起來建立音訊子系統。我知道每個裝置是什麼,是因為我對板卡硬體設計很熟悉,但是linux kernel是如何知道為每個節點做什麼呢?
訣竅是kernel從裝置樹的根節點開始,尋找帶有“compatible”屬性的節點。首先,通常假設,節點帶有的“compatible”屬性表述它代表了哪種裝置;第二,可以這樣假定,任何裝置樹(Device Tree)的根下的節點都直接連線到處理器匯流排,或者是其他系統裝置(不能以其他方式描述)。 對於每個這種節點,Linux分配並註冊一個platform_device,而該裝置又可能繫結到一個platform_driver。
為什麼對這些節點使用platform_device是一個安全的假設?因為,對於linux裝置模型,幾乎所有匯流排型別都假設它的裝置是匯流排控制器的子裝置。例如,每個i2c_client都是i2c_master的子裝置。每一個spi_device是SPI匯流排的子裝置。USB、PCI、MDIO等都是如此。同樣的層級結構在DT中是如此,I2C裝置節點只會以i2c匯流排結點的子節點的形式出現。SPI、MDIO、USB等匯流排也是如此。唯一不需要特定的父裝置的是platform_device(和amba_devices,稍後會詳細介紹),它位於linux的裝置樹的/sys/devices。因此,如果一個DT節點在裝置樹(Device Tree)的根路徑下, 那麼很可能需要將其註冊為platform_device。
linux板卡支援程式碼呼叫of_platform_populate(NULL, NULL, NULL, NULL)函式,來啟動DT的根裝置發現。所有的引數都是NULL,因為從DT的根路徑開始查詢,所以不需要提供起始的節點(第一個NULL),父裝置的結構體(最後一個NULL),而且我們也不需要一個匹配表。 對於只需要註冊裝置的板卡,.init_machine()可以完全為空,除了呼叫of_platform_populate()以外。
在Tegra的例子裡,這是指”/soc”和“/sound”節點,但是“/soc”節點的子節點呢? 它們不應該也註冊為平臺裝置嗎? 以linux對裝置樹的支援,通常的方式是,子裝置的註冊在父裝置驅動的.probe函式中完成。所以i2c匯流排驅動將為每一個子節點,註冊一個i2c_client,一個spi匯流排驅動也把子節點註冊為spi裝置,其他的匯流排型別也是如此。根據這種模型,可以為繫結到”/soc”的節點編寫驅動,並把它的子節點都註冊為platform_devices。板卡支援程式碼將分配和註冊一個Soc裝置,一個(理論上的)Soc裝置驅動可以繫結到Soc裝置,在回撥函式.probe中將 /soc/interrupt-controller, /soc/serial, /soc/i2s, /soc/i2c註冊為 platform_device。很容易,對吧!
實際上,將一些platform_devices的子節點註冊為更多的platform_devices是一種常見的模式, 裝置樹支援程式碼反映了這一點,並簡化了上面的示例。of_platform_populate()的第二個引數是of_device_id表,任何匹配該表中的條目的節點也將註冊其子節點。在Tegra的例子裡,程式碼如下所示:
static void __init harmony_init_machine(void) { /* ... */ of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL); }
“simple-bus”表示一種簡單的記憶體對映匯流排,定義在 ePAPR 1.0規範中,所以 of_platform_populate() 函式,可以假設“simple-bus”相容的節點始終會被遍歷到。然而,我們將它作為引數傳入, 這樣board support程式碼就可以始終覆蓋預設行為。
【 需要新增關於新增i2c/spi/etc子裝置的討論】
附錄 A: AMBA devices
ARM Primecells是一種附加在ARM AMBA匯流排上的裝置,它包含一些對硬體檢測和電源控制的支援。 在Linux中,結構體amba_device和amba_bus_type用於表示Primecell裝置。 然而,棘手的一點是,AMBA匯流排上的所有裝置不都是primecell,對於Linux系統而言, amba_device和platform_device例項通常是同一匯流排上的兄弟節點。在使用裝置樹時,這會給of_platform_populate()帶來問題,因為它必須決定是將每個節點註冊為platform_device還是amba_device。 不幸的是,這會使裝置模型的建立變得有點複雜,但是解決方案並不複雜。如果一個節點與“arm,amba-primecell”相容,那麼of_platform_populate()函式將把它註冊為amba_device,而非platform_device。