英語

Linux 和裝置樹

Linux 對裝置樹資料的使用模型

作者:

Grant Likely <grant.likely@secretlab.ca>

本文描述了 Linux 如何使用裝置樹。裝置樹資料格式的概述可以在 devicetree.org 的裝置樹使用頁面上找到[1]

“開放韌體裝置樹”,或簡稱裝置樹 (DT),是一種用於描述硬體的資料結構和語言。更具體地說,它是硬體的描述,可以被作業系統讀取,因此作業系統不需要硬編碼機器的細節。

在結構上,DT 是一棵樹,或一個具有命名節點的無環圖,節點可以有任意數量的命名屬性,封裝任意資料。還存在一種機制,用於在自然樹結構之外建立一個節點到另一個節點的任意連結。

從概念上講,定義了一組通用的使用約定,稱為“繫結”,用於描述資料應該如何在樹中出現,以描述典型的硬體特徵,包括資料匯流排、中斷線、GPIO 連線和外圍裝置。

儘可能地,硬體使用現有的繫結來描述,以最大限度地利用現有的支援程式碼,但是由於屬性和節點名稱只是文字字串,因此很容易透過定義新的節點和屬性來擴充套件現有繫結或建立新的繫結。但是,在沒有事先研究現有內容的情況下建立新的繫結時要小心。目前,對於 i2c 匯流排有兩種不同的、不相容的繫結,這是因為在建立新的繫結之前,沒有調查 i2c 裝置在現有系統中是如何被列舉的。

1. 歷史

DT 最初由開放韌體建立,作為將資料從開放韌體傳遞到客戶端程式(如作業系統)的通訊方法的一部分。作業系統使用裝置樹在執行時發現硬體的拓撲結構,從而在沒有硬編碼資訊的情況下支援大多數可用硬體(假設所有裝置都有可用的驅動程式)。

由於開放韌體通常用於 PowerPC 和 SPARC 平臺,因此 Linux 對這些架構的支援長期以來一直使用裝置樹。

2005 年,當 PowerPC Linux 開始進行重大清理併合並 32 位和 64 位支援時,決定要求所有 powerpc 平臺都支援 DT,無論它們是否使用開放韌體。為此,建立了一種稱為扁平裝置樹 (FDT) 的 DT 表示形式,可以將其作為二進位制 blob 傳遞給核心,而無需真正的開放韌體實現。U-Boot、kexec 和其他引導載入程式被修改為既支援傳遞裝置樹二進位制檔案 (dtb) 也支援在引導時修改 dtb。DT 也被新增到 PowerPC 引導包裝器 (arch/powerpc/boot/*) 中,以便 dtb 可以與核心映象一起包裝,以支援引導現有的非 DT 感知韌體。

此後不久,FDT 基礎設施被推廣為可供所有架構使用。在編寫本文時,6 個主線架構(arm、microblaze、mips、powerpc、sparc 和 x86)和 1 個主線外架構 (nios) 具有某種程度的 DT 支援。

2. 資料模型

如果您還沒有閱讀裝置樹使用[1] 頁面,那麼現在就去閱讀它。沒關係,我會等你的....

2.1 高階檢視

最重要的是要理解 DT 只是一個描述硬體的資料結構。它沒有什麼神奇之處,也不會神奇地解決所有硬體配置問題。它所做的是提供一種語言,將硬體配置與 Linux 核心(或任何其他作業系統)中的板卡和裝置驅動程式支援分離。使用它可以使板卡和裝置支援成為資料驅動的;基於傳遞到核心中的資料而不是基於每臺機器硬編碼的選擇來進行設定決策。

理想情況下,資料驅動的平臺設定應該減少程式碼重複,並使其更容易使用單個核心映象支援各種硬體。

Linux 使用 DT 資料用於三個主要目的

  1. 平臺識別,

  2. 執行時配置,和

  3. 裝置填充。

2.2 平臺識別

首先,核心將使用 DT 中的資料來識別特定的機器。在一個完美的世界中,特定的平臺對核心來說並不重要,因為所有的平臺細節都會透過裝置樹以一致和可靠的方式完美地描述出來。然而,硬體並不完美,因此核心必須在早期啟動期間識別機器,以便有機會執行特定於機器的修復程式。

在大多數情況下,機器身份無關緊要,核心將根據機器的核心 CPU 或 SoC 選擇設定程式碼。例如,在 ARM 上,arch/arm/kernel/setup.c 中的 setup_arch() 將呼叫 arch/arm/kernel/devtree.c 中的 setup_machine_fdt(),該函式搜尋 machine_desc 表並選擇最匹配裝置樹資料的 machine_desc。它透過檢視根裝置樹節點中的“compatible”屬性,並將其與 struct machine_desc 中的 dt_compat 列表進行比較來確定最佳匹配(如果您好奇,它在 arch/arm/include/asm/mach/arch.h 中定義)。

“compatible”屬性包含一個排序的字串列表,首先是機器的確切名稱,然後是它相容的可選板卡列表,從最相容到最不相容排序。例如,TI BeagleBoard 及其後續產品 BeagleBoard xM 板的根相容屬性可能分別如下所示

compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";

其中“ti,omap3-beagleboard-xm”指定了確切的型號,它還宣告它與 OMAP 3450 SoC 和 omap3 系列 SoC 相容。您會注意到該列表是從最具體(確切的板卡)到最不具體(SoC 系列)排序的。

精明的讀者可能會指出,Beagle xM 也可以宣告與原始 Beagle 板卡相容。但是,應該注意不要在板卡級別這樣做,因為通常從一個板卡到另一個板卡,即使在同一產品線中,也存在高度的變化,並且很難確定當一個板卡宣告與另一個板卡相容時意味著什麼。對於頂層,最好謹慎行事,不要宣告一個板卡與另一個板卡相容。一個值得注意的例外是當一個板卡是另一個板卡的載體時,例如連線到載板的 CPU 模組。

關於 compatible 值的另一個注意事項。在 compatible 屬性中使用的任何字串都必須記錄它表示什麼。在 Documentation/devicetree/bindings 中新增 compatible 字串的文件。

同樣在 ARM 上,對於每個 machine_desc,核心會檢視 dt_compat 列表中的任何條目是否出現在 compatible 屬性中。如果有一個匹配,那麼該 machine_desc 是驅動機器的候選者。在搜尋完整個 machine_desc 表之後,setup_machine_fdt() 會根據每個 machine_desc 與 compatible 屬性中的哪個條目匹配,返回“最相容”的 machine_desc。如果沒有找到匹配的 machine_desc,則返回 NULL。

這個方案背後的原因是觀察到,在大多數情況下,如果大量的板卡都使用相同的 SoC 或相同的 SoC 系列,則單個 machine_desc 可以支援大量的板卡。然而,總會有一些例外情況,其中特定的板卡需要特殊的設定程式碼,這在通用情況下是沒有用的。可以透過在通用設定程式碼中顯式檢查有問題的板卡來處理特殊情況,但是如果它不僅僅是幾個案例,這樣做很快就會變得醜陋和/或難以維護。

相反,compatible 列表允許通用的 machine_desc 透過在 dt_compat 列表中指定“不太相容”的值來為廣泛的通用板卡集提供支援。在上面的示例中,通用的板卡支援可以宣告與“ti,omap3”或“ti,omap3450”相容。如果在原始 beagleboard 上發現了一個需要在早期引導期間進行特殊解決方法程式碼的錯誤,那麼可以新增一個新的 machine_desc,它實現解決方法並且僅匹配“ti,omap3-beagleboard”。

PowerPC 使用稍微不同的方案,它從每個 machine_desc 呼叫 .probe() 鉤子,並且使用第一個返回 TRUE 的鉤子。然而,這種方法沒有考慮到 compatible 列表的優先順序,並且可能應該避免用於新的架構支援。

2.3 執行時配置

在大多數情況下,DT 將是韌體向核心傳遞資料的唯一方法,因此也用於傳入執行時和配置資料,如核心引數字串和 initrd 映象的位置。

大多數資料都包含在 /chosen 節點中,當引導 Linux 時,它看起來像這樣

chosen {
        bootargs = "console=ttyS0,115200 loglevel=8";
        initrd-start = <0xc8000000>;
        initrd-end = <0xc8200000>;
};

bootargs 屬性包含核心引數,initrd-* 屬性定義了 initrd blob 的地址和大小。請注意,initrd-end 是 initrd 映象之後的第一個地址,因此這與 struct resource 的通常語義不匹配。chosen 節點還可以選擇性地包含任意數量的附加屬性,用於特定於平臺的配置資料。

在早期引導期間,架構設定程式碼使用不同的輔助回撥多次呼叫 of_scan_flat_dt() 以在設定分頁之前解析裝置樹資料。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 裝置填充

在識別出板卡並解析完早期配置資料後,核心初始化可以以正常方式進行。在此過程中的某個時刻,呼叫 unflatten_device_tree() 將資料轉換為更有效的執行時表示形式。這也是機器特定的設定鉤子將被呼叫的時間,例如 ARM 上的 machine_desc .init_early()、.init_irq() 和 .init_machine() 鉤子。本節的其餘部分使用 ARM 實現中的示例,但是所有架構在使用 DT 時都會執行幾乎相同的事情。

顧名思義,.init_early() 用於需要在引導過程早期執行的任何機器特定設定,.init_irq() 用於設定中斷處理。使用 DT 不會實質性地改變這兩個函式的行為。如果提供了 DT,那麼 .init_early() 和 .init_irq() 都可以呼叫任何 DT 查詢函式(include/linux/of*.h 中的 of_*)來獲取有關平臺的其他資料。

在 DT 上下文中,最有趣的鉤子是 .init_machine(),它主要負責用有關平臺的資料填充 Linux 裝置模型。歷史上,這在嵌入式平臺上是透過在板卡支援 .c 檔案中定義一組靜態時鐘結構、platform_devices 和其他資料,並在 .init_machine() 中大量註冊來實現的。當使用 DT 時,可以從解析 DT 中獲得裝置列表,並動態分配裝置結構,而不是為每個平臺硬編碼靜態裝置。

最簡單的情況是 .init_machine() 僅負責註冊一個 platform_devices 塊。platform_device 是 Linux 使用的概念,用於硬體無法檢測到的記憶體或 I/O 對映裝置,以及用於“複合”或“虛擬”裝置(稍後會詳細介紹)。雖然 DT 沒有“平臺裝置”術語,但平臺裝置大致對應於樹的根目錄中的裝置節點以及簡單的記憶體對映匯流排節點的子節點。

現在是展示示例的好時機。這是 NVIDIA Tegra 板卡的裝置樹的一部分

/{
      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。然而,檢視樹,並不立即清楚每個節點代表什麼型別的裝置,甚至不清楚節點是否代表裝置。/chosen、/aliases 和 /memory 節點是不描述裝置的告知性節點(儘管可以說記憶體可以被認為是一個裝置)。/soc 節點的子節點是記憶體對映裝置,但是 codec@1a 是一個 i2c 裝置,並且 sound 節點不代表一個裝置,而是代表其他裝置如何連線在一起以建立音訊子系統。我知道每個裝置是什麼,因為我熟悉板卡設計,但是核心如何知道如何處理每個節點?

訣竅是核心從樹的根目錄開始,並查詢具有“compatible”屬性的節點。首先,通常假設任何具有“compatible”屬性的節點都代表某種裝置,其次,可以假設樹的根目錄中的任何節點要麼直接連線到處理器匯流排,要麼是無法以任何其他方式描述的各種系統裝置。對於這些節點中的每一個,Linux 分配並註冊一個 platform_device,而 platform_device 又可以繫結到一個 platform_driver。

為什麼對這些節點使用 platform_device 是一個安全的假設?嗯,對於 Linux 建模裝置的方式,幾乎所有的 bus_types 都假設其裝置是匯流排控制器的子裝置。例如,每個 i2c_client 都是 i2c_master 的子裝置。每個 spi_device 都是 SPI 匯流排的子裝置。USB、PCI、MDIO 等也是如此。在 DT 中也發現了相同的層次結構,其中 I2C 裝置節點僅作為 I2C 匯流排節點的子節點出現。SPI、MDIO、USB 等也是如此。唯一不需要特定型別的父裝置的裝置是 platform_devices(和 amba_devices,稍後會詳細介紹),它會很高興地位於 Linux /sys/devices 樹的底部。因此,如果 DT 節點位於樹的根目錄中,那麼將其註冊為 platform_device 可能是最好的選擇。

Linux 板卡支援程式碼呼叫 of_platform_populate(NULL, NULL, NULL, NULL) 以啟動在樹的根目錄中發現裝置的過程。這些引數都是 NULL,因為當從樹的根目錄開始時,不需要提供起始節點(第一個 NULL),父 struct device(最後一個 NULL),並且我們還沒有使用匹配表(尚未)。對於只需要註冊裝置的板卡,.init_machine() 可以完全為空,除了 of_platform_populate() 呼叫之外。

在 Tegra 示例中,這解釋了 /soc 和 /sound 節點,但是 SoC 節點的子節點呢?它們不也應該註冊為平臺裝置嗎?對於 Linux DT 支援,通用行為是由父裝置的裝置驅動程式在驅動程式 .probe() 時註冊子裝置。因此,i2c 匯流排裝置驅動程式將為每個子節點註冊一個 i2c_client,SPI 匯流排驅動程式將註冊其 spi_device 子節點,其他 bus_types 也是如此。根據該模型,可以編寫一個驅動程式,該驅動程式繫結到 SoC 節點,並簡單地為其每個子節點註冊 platform_devices。板卡支援程式碼將分配並註冊一個 SoC 裝置,(理論上的)SoC 裝置驅動程式可以繫結到 SoC 裝置,並在其 .probe() 鉤子中註冊 /soc/interrupt-controller、/soc/serial、/soc/i2s 和 /soc/i2c 的 platform_devices。很簡單,對吧?

實際上,事實證明,將某些 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”在裝置樹規範中被定義為一個屬性,意味著一個簡單的記憶體對映匯流排,因此可以編寫 of_platform_populate() 程式碼,使其僅假設 simple-bus 相容節點將始終被遍歷。但是,我們將其作為引數傳遞,以便板卡支援程式碼始終可以覆蓋預設行為。

[需要新增有關新增 i2c/spi/etc 子裝置的討論]

附錄 A:AMBA 裝置

ARM Primecells 是一種連線到 ARM AMBA 匯流排的裝置,它包括對硬體檢測和電源管理的一些支援。在 Linux 中,struct amba_device 和 amba_bus_type 用於表示 Primecell 裝置。然而,棘手的是,並非 AMBA 總線上的所有裝置都是 Primecells,並且對於 Linux 來說,amba_device 和 platform_device 例項通常是同一匯流排段的兄弟裝置。

當使用 DT 時,這會給 of_platform_populate() 帶來問題,因為它必須決定是將每個節點註冊為 platform_device 還是 amba_device。不幸的是,這使裝置建立模型稍微複雜了一點,但是解決方案並不太具有侵入性。如果一個節點與“arm,primecell”相容,那麼 of_platform_populate() 將其註冊為 amba_device 而不是 platform_device。