摘要
本報告旨在為嵌入式Linux開發者詳細梳理設備樹(Device Tree, DT)在系統啟動中的完整解析流程。報告將從引導加載程序(Bootloader)如何準備和傳遞設備樹二進制文件(DTB)開始,逐步深入到內核如何將其轉化為可操作的內存數據結構,如何與設備驅動程序進行匹配與綁定,最終闡述驅動程序如何利用這些信息在sysfs
文件系統中創建可供用戶空間訪問的設備節點。這份報告旨在解答從靜態的硬件描述到用戶可見的動態設備文件這一過程中的所有核心技術問題。
引言:設備樹的核心作用與演變
1.1 設備樹的起源與必要性
設備樹最初源于SPARC和PowerPC平臺的Open Firmware項目,其設計初衷是為了在不修改操作系統內核的情況下,支持多種不同的硬件配置 。在ARM架構中,設備樹的引入尤其關鍵,因為它徹底改變了傳統的“一個板子一個內核”的困境 。
在設備樹出現之前,Linux內核的硬件描述是硬編碼在所謂的board-file
(如arch/arm/mach-xxx/board-yyy.c
)中的 。這意味著,每當硬件發生微小改動,例如更改了一個I2C外設的地址,就需要重新編譯整個內核鏡像 。這種緊耦合的設計使得內核開發難以擴展,也極大地增加了新板卡移植的復雜性。
設備樹的出現標志著一種從硬編碼到數據驅動的嵌入式軟件開發范式轉變。在舊的ARM啟動機制中,引導加載程序使用一種名為ATAGS
(一個鏈表結構)的機制,只能傳遞內存大小、內核命令行等少量基本信息 。而其他所有非自發現(non-discoverable)的硬件信息,如I2C控制器地址、GPIO引腳配置等,都必須預先硬編碼在內核源碼的
board-file
里 。
與此形成鮮明對比的是,設備樹二進制文件(DTB)包含了完整的硬件拓撲結構。引導加載程序只需將DTB的物理地址傳遞給內核(在ARM上,這一地址通常被加載到R2寄存器中),內核就能在運行時自主解析整個硬件配置 。這種轉變將硬件描述從內核源碼中移除,放入獨立的文件(
.dts
)中,使得同一個內核鏡像能夠通過加載不同的DTB文件來支持多個不同的板卡 。這種解耦機制是現代Linux內核在嵌入式領域取得成功,特別是實現通用主線內核支持的關鍵。
1.2 設備樹的基本構成
設備樹本質上是一種描述硬件的數據結構,采用樹形或非循環圖的格式 。其基本結構由節點(nodes)和屬性(properties)組成 。每個節點都代表一個設備或總線,并可以包含任意數量的命名屬性和子節點 。
根節點(
/
):設備樹的頂層節點,沒有名稱和地址 。節點命名:每個節點都遵循
node-name@unit-address
的命名慣例 。node-name
描述設備的類型,unit-address
則指定設備在其父總線地址空間中的基地址 。核心屬性:
#address-cells
和#size-cells
:這兩個屬性定義了子節點reg
屬性中地址和大小字段的單元格(cell,即32位值)數量 。這是一種可擴展的地址表示機制。compatible
(兼容性字符串):這是連接硬件描述與設備驅動程序的關鍵契約 。它是一個字符串列表,通常由vendor,device
組成,其中最具體的字符串排在最前面,最通用的排在最后 。驅動程序會使用這個字符串來聲明自己支持哪些硬件 。phandle
與aliases
:設備樹允許節點之間通過phandle
進行引用 。這種引用通常通過&label
的形式來實現,其中label
是節點的標簽 。此外,/aliases
節點為常用的節點路徑提供了簡短的別名,便于訪問 。
第一章:啟動加載階段:DTB的準備與傳遞
2.1 DTB的生成與存儲
設備樹的生命周期始于其源文件。硬件開發者通常會編寫設備樹源文件(.dts
),并引用包含SoC級別通用定義的設備樹包含文件(.dtsi
) 。這種
.dtsi
文件通過#include
指令實現模塊化和重用,使得不同板卡可以共享SoC的硬件描述 。
這些.dts
和.dtsi
文件由設備樹編譯器(Device Tree Compiler, dtc
)處理,dtc
將人類可讀的文本格式轉換為緊湊的、面向機器的二進制格式,即設備樹二進制文件(DTB,文件后綴為.dtb
)。生成的DTB文件通常與Linux內核鏡像(如
zImage
或uImage
)一同存儲在系統的非易失性存儲介質(如SD卡、eMMC或NAND Flash)的啟動分區中 。
2.2 U-Boot的職責與DTB的修改
引導加載程序(例如U-Boot)是DTB解析過程的第一階段參與者 。它的主要職責是:
將內核鏡像和DTB文件從存儲介質加載到RAM中 。
在內存中對DTB進行必要的運行時修改,例如更新
chosen
節點中的bootargs
(內核命令行參數)、設置RAM的基地址和大小,或者配置網絡接口的MAC地址等 。加載完成后,U-Boot將最終的DTB準備好,并將其物理內存地址傳遞給內核 。
這些修改至關重要,它確保了內核接收到的硬件描述是最新、最準確的,能夠反映引導加載程序在啟動時根據用戶配置或探測結果所做的任何調整。
2.3 DTB的關鍵傳遞機制
在ARM架構中,U-Boot將DTB的物理地址傳遞給內核的機制是一個設計精巧的關鍵環節 。在跳轉到內核入口點之前,U-Boot會執行以下操作:
將CPU的
r0
寄存器設置為0。將
r1
寄存器設置為機器類型ID(舊機制)。將
r2
寄存器設置為DTB在系統RAM中的物理地址 。
然后,U-Boot跳轉到內核的入口點,內核從此接管控制權 。下表對比了設備樹機制與舊的ATAGS機制的差異,突出了前者在靈活性和可擴展性方面的優勢。
表1:ATAGS與設備樹傳遞機制對比
特性 | ATAGS (舊機制) | 設備樹 (現代機制) |
數據結構 | 一個鏈表,由一系列標簽(tag)組成 | 一個扁平化、樹狀的二進制數據塊(DTB) |
傳遞方式 | 引導加載程序通過r2 寄存器傳遞一個指向ATAGS鏈表頭部的指針 | 引導加載程序通過 |
描述內容 | 僅限于少量基本信息,如內存大小、命令行參數等 | 包含完整的硬件拓撲、外設地址、中斷、GPIO等所有非自發現的硬件信息 |
優點 | 簡單、開銷小 | 硬件描述與內核代碼完全解耦,支持通用內核鏡像 |
缺點 | 缺乏靈活性,板級硬件信息需要硬編碼在內核源碼中 | 需要額外的dtc 工具編譯,DTB文件需要占用一定內存空間 |
適用范圍 | 已被棄用,不建議用于新平臺 | 現代嵌入式Linux的首選,ARM等多種架構強制使用 |
第二章:內核早期啟動:DTB的初步解析
3.1 內核入口點與DTB的接收
在U-Boot將控制權交給Linux內核后,內核的啟動代碼會開始執行。在start_kernel()
函數被調用之前,內核的架構特定啟動代碼(如ARM上的head.S
文件)會從r2
寄存器中讀取DTB的物理地址 。這個地址隨后會被保存到一個全局變量中,供內核后續使用 。
這個階段非常關鍵,因為此時內存管理單元(MMU)尚未啟用,內核只能訪問物理地址 。這意味著內核無法進行復雜的動態內存分配和構建復雜的C數據結構。然而,它需要一些關鍵信息才能完成MMU的初始化,例如RAM的地址和大小,以及
bootargs
中的內核命令行參數 。
3.2 of_scan_flat_dt()
與libfdt
庫
為了在MMU啟用之前的受限環境中獲取必要信息,內核使用一個輕量級的庫來直接解析DTB的扁平化二進制數據 。這個庫就是
libfdt
(Flattened Device Tree library)。
libfdt
被集成在內核源碼中,提供了一系列低級API來檢查、讀取和修改DTB二進制文件 。
內核的啟動代碼會調用of_scan_flat_dt()
函數來遍歷DTB,并使用回調函數來提取關鍵信息 。例如:
early_init_dt_scan_chosen()
:用于解析/chosen
節點下的內核命令行參數bootargs
。early_init_dt_scan_memory()
:用于確定系統RAM的地址和大小 。
這種設計表明內核對DTB的解析并非一次性完成,而是分為兩個階段:早期掃描(pre-MMU)和后期解壓(post-MMU)。這個設計是啟動過程中內存和尋址限制所決定的。在MMU啟用前,內核無法進行復雜的動態內存分配,因此需要libfdt
這種能夠在物理內存中直接操作DTB二進制數據的工具。
3.3 DTB的“解壓”:從扁平化到樹形結構
當內核完成了MMU的初始化和虛擬內存的配置后,它就可以進行更復雜的內存操作。此時,一個關鍵的函數unflatten_device_tree()
會被調用 。這個函數的核心任務是將靜態的DTB二進制數據轉換成一個動態的、鏈表連接的內存數據結構:
struct device_node
樹 。
unflatten_device_tree()
的執行標志著DTB的靜態描述階段結束,進入了DT在內存中作為動態數據結構的活躍階段。這種兩階段解析過程是內核為了在有限制的早期啟動環境中獲取關鍵信息,同時在成熟的運行時環境中構建高效、易于訪問的數據結構之間做出的權衡。
第三章:DT的內存表示與驅動程序的接口
4.1 struct device_node
:DT在內存中的實體
DTB經過unflatten_device_tree()
的轉換后,在內核中被表示為一系列相互連接的struct device_node
實例 。
struct device_node
是DT中每個節點的內存表示,它包含了指向其父節點、子節點、兄弟節點和屬性列表的指針,從而構成了完整的樹形結構 。
驅動程序不會直接操作DTB二進制數據,而是通過一套標準的of_*
家族API來與這個struct device_node
樹進行交互 。這些API包括:
of_get_next_child()
:用于遍歷一個節點的所有子節點 。of_find_compatible_node()
:用于根據兼容性字符串在DT樹中查找匹配的節點 。of_get_property()
:用于讀取一個節點中特定屬性的值 。
這些高級API將復雜的樹形遍歷和數據解析細節封裝起來,為驅動程序提供了一個簡潔、高效的接口。
4.2 DT與內核設備模型的整合
設備樹的解析為Linux設備模型提供了設備實例化的數據來源。Linux設備模型的核心思想是圍繞著總線(bus
)、設備(device
)和驅動(driver
)這三元組來組織的 。
DT中的節點本身并不是Linux設備模型中的“設備”,而只是對硬件的靜態描述 。內核在解析DT后,會根據這些靜態數據來動態創建
struct device
實例。對于SoC上那些無法被自動枚舉的內存映射設備,內核為此創建了一個虛擬總線——platform_bus
。
of_platform_populate()
函數是連接DT解析和Linux設備模型的關鍵橋梁 。它會遍歷DT樹,找到那些具有
compatible
屬性的節點,并為它們創建對應的platform_device
實例,然后將這些設備注冊到platform_bus
上 。對于其他總線類型(如I2C、SPI),其總線驅動的
probe
函數會負責為其子節點創建相應的設備實例(如i2c_client
),并注冊到各自的總線上 。
這個過程將設備描述從靜態數據轉換為了內核設備模型中的動態實例,從而實現了從設計藍圖到可操作實體的轉變。
第四章:驅動程序綁定與設備實例
5.1 設備與驅動的匹配模型
在Linux設備模型中,一個設備只有在找到與其匹配的驅動程序時,才會被“激活”并進行初始化 。當一個新的設備實例(例如一個
platform_device
)被注冊到總線上時,內核的驅動核心會自動遍歷所有已注冊的驅動程序,尋找與該設備匹配的項 。
platform_bus
上的匹配過程由platform_match()
函數實現 。該函數會嘗試多種匹配方式,其中最重要的一種就是基于設備樹的兼容性匹配 。
5.2 DT與驅動的“兼容”橋梁
設備樹驅動綁定機制的核心是compatible
字符串 。這是一種在設備和驅動之間建立“契約”的機制:
DT端:設備樹節點通過
compatible
屬性來聲明其所代表的硬件類型 。該屬性是一個字符串列表,由最具體的兼容性字符串到最通用的兼容性字符串排列 。驅動端:驅動程序通過定義一個名為
of_match_table
的const struct of_device_id
數組來聲明自己支持的compatible
字符串 。這個數組通常通過MODULE_DEVICE_TABLE(of,...)
宏導出,供內核驅動核心識別 。
匹配過程由of_match_device()
函數執行 。當一個
platform_device
被注冊時,內核會調用此函數,將設備的compatible
字符串列表與所有已注冊驅動的of_match_table
進行比較,如果找到了匹配項,則認為匹配成功 。
表2:DT節點與驅動匹配示例
模塊 | 關鍵元素 | 示例內容 | 作用 |
設備樹 | DT節點路徑 | &i2c1 {... my_device@4a {... } } | 定義一個在I2C總線1上,地址為0x4a的設備 |
compatible 屬性 | compatible = "acme-inc,my-device"; | 聲明該設備的硬件兼容性字符串 | |
驅動程序 | of_match_table | static const struct of_device_id my_device_of_match = { {.compatible = "acme-inc,my-device", }, { } }; MODULE_DEVICE_TABLE(of, my_device_of_match); | 聲明該驅動程序支持“acme-inc,my-device”這個兼容性字符串 |
probe() 函數 | static int my_device_probe(struct platform_device *pdev) {... } | 匹配成功后,內核調用的初始化函數 |
上表展示了compatible
字符串如何成為連接設備樹和驅動程序的“魔術字符串”。一個在設備樹中聲明的設備節點,只有當其compatible
字符串與某個驅動程序的of_match_table
中的一項完全匹配時,才會被內核成功綁定。
5.3 probe()
函數的調用與驅動的參與
當內核找到匹配的設備與驅動后,它會自動調用驅動程序提供的probe()
函數 。例如,對于
platform_driver
,其platform_driver
結構體中的probe()
函數會被調用,并將對應的platform_device
實例作為參數傳入 。
probe()
函數是驅動程序參與設備初始化的起點 。在該函數中,驅動程序會:
獲取DT節點:從傳入的
platform_device
實例中,通過dev->of_node
指針獲取到對應的struct device_node
。讀取屬性:使用
of_*
家族API(例如of_get_property()
、of_get_next_child()
)從DT節點中讀取硬件配置信息 。這些信息可能包括內存映射地址(reg
)、中斷號(interrupts
)、GPIO引腳配置(gpios
)等 。初始化硬件:利用讀取到的信息,驅動程序對硬件進行具體的初始化和配置 。例如,I2C控制器驅動會根據DT中的
clock-frequency
屬性設置總線速度 。注冊設備:完成初始化后,驅動程序會向內核注冊更高層級的設備接口,例如字符設備、塊設備或網絡設備 。
第五章:從DT節點到/sys
目錄的最終映射
6.1 kobject
基礎設施與設備模型的基石
/sys
文件系統是一個虛擬文件系統,它的主要功能是將內核中的數據結構和關系以目錄和文件的形式,層次化地暴露給用戶空間 。
sysfs
的核心是kobject
。
kobject
是Linux內核中用于描述和管理對象的通用基礎設施 。每個
kobject
都代表內核中的一個對象,并在/sys
中對應一個目錄。struct device
(如platform_device
)是內嵌了kobject
的更高級抽象 。當
struct device
被注冊時,其內嵌的kobject
也會被注冊,從而在sysfs
中自動創建相應的目錄結構 。
6.2 probe()
函數中的/sys
目錄生成
當驅動程序的probe()
函數成功返回后,內核設備模型會注冊這個設備實例 。這個注冊過程會自動觸發
kobject
的創建和注冊,從而在/sys/devices/platform/...
或/sys/bus/i2c/devices/...
等目錄下創建對應的設備目錄 。這些目錄的名稱通常基于設備的
name
和id
。
6.3 驅動程序如何創建屬性文件
設備目錄創建后,驅動程序可以在其中添加屬性文件,以暴露設備的運行時狀態或提供配置接口 。這些屬性文件由
struct device_attribute
結構體定義,其中包含了文件的名稱、訪問權限以及兩個關鍵的回調函數:show()
和store()
。
show()
:當用戶空間應用程序通過cat
等命令讀取/sys
下的屬性文件時,內核會調用這個函數 。驅動程序會執行相應的邏輯,將設備的實時狀態或信息寫入內核提供的緩沖區,然后返回給用戶空間 。store()
:當用戶寫入屬性文件時,內核會調用這個函數 。驅動程序會接收用戶寫入的數據,并據此配置或控制硬件 。
值得注意的是,用戶在/sys
下看到的設備節點并非DT節點的直接鏡像,而是內核設備模型在運行時生成的抽象。DT節點是靜態的硬件描述,而/sys
文件是動態的軟件接口。DTB的原始二進制數據可以在/sys/firmware/devicetree/base
或/proc/device-tree
下以只讀形式訪問 。而
/sys/devices/...
下的文件則是由設備驅動程序在probe()
階段動態創建的。/sys
接口的背后是show()
和store()
回調函數實現的具體設備控制邏輯,這些邏輯根據DT中提供的靜態數據(如寄存器地址、中斷號)來操作硬件 。因此,
/sys
是內核提供給用戶空間的標準化接口,而DT則是驅動程序實現這些接口所需的“設計藍圖”。
結論與展望
7.1 完整流程回顧
設備樹在Linux內核啟動過程中扮演著至關重要的角色,其解析流程可概括為以下步驟:
DTB的準備:開發者使用
dtc
工具將.dts
源文件編譯成DTB二進制文件,并將其與內核鏡像一同存儲在啟動介質中 。DTB的傳遞:引導加載程序(如U-Boot)將DTB加載到RAM中,進行必要的運行時修改,并將DTB的物理地址通過特定的CPU寄存器(如ARM上的R2)傳遞給內核 。
早期解析:內核在早期啟動階段(pre-MMU),利用
libfdt
庫調用of_scan_flat_dt()
函數,提取內存布局和bootargs
等關鍵信息 。構建數據結構:MMU啟用后,
unflatten_device_tree()
函數將DTB的扁平化二進制數據轉換成可供內核高效訪問的struct device_node
樹形數據結構 。設備實例化:內核設備模型遍歷
device_node
樹,根據節點的compatible
屬性創建對應的platform_device
或其他總線設備實例 。驅動綁定與
probe
:內核驅動核心將新創建的設備實例與已注冊的驅動程序的of_match_table
進行匹配。匹配成功后,內核調用驅動的probe()
函數 。sysfs
文件創建:在probe()
函數中,驅動程序根據DT信息初始化硬件,并利用kobject
基礎設施在/sys
目錄下創建設備目錄和屬性文件,從而向用戶空間暴露可讀寫的設備接口 。
7.2 DT的未來與意義
設備樹機制的引入是嵌入式Linux發展的一個里程碑,它將硬件描述從內核源碼中分離,使開發者能夠使用同一個內核二進制文件來支持不同的硬件平臺 。這種從硬編碼到數據驅動的范式轉變極大地提高了內核的可維護性和可移植性。展望未來,設備樹仍然是現代嵌入式系統不可或缺的一部分。例如,設備樹Overlay(DTO)機制允許在運行時動態地加載和應用硬件配置,使得系統能夠支持熱插拔設備或可配置的擴展板 。設備樹的持續演進和廣泛應用確保了Linux在快速變化的嵌入式硬件生態中保持其強大的適應性和靈活性。