配置指南)
1. 項目概述從“硬編碼”到“軟描述”的進化如果你是從單片機或者裸機開發(fā)轉(zhuǎn)向嵌入式Linux的那么第一次接觸“設備樹”這個概念時大概率會感到困惑。在傳統(tǒng)的單片機開發(fā)中我們習慣于在代碼里直接寫死硬件信息比如UART1的基地址是0x40013800使用USART1_IRQn這個中斷號連接的GPIO是PA9和PA10。這種方式簡單直接但問題也很明顯——代碼和具體的硬件板卡高度耦合。換一塊不同外設布局的核心板你就得去代碼里到處找這些“魔數(shù)”Magic Number并修改稍有不慎就會出錯維護起來簡直是噩夢。設備樹Device Tree的出現(xiàn)就是為了解決這個“硬編碼”的頑疾。它的核心思想是“描述”而非“編碼”。我們可以把設備樹理解為一個硬件資源的“配置文件”或“描述文件”它用一種結(jié)構(gòu)化的文本語言DTS將CPU、內(nèi)存、總線、外設控制器、GPIO、中斷等硬件拓撲和屬性清晰、獨立地描述出來。然后在系統(tǒng)啟動的早期由Bootloader將這個文本文件編譯成的二進制文件DTB傳遞給Linux內(nèi)核。內(nèi)核解析這個DTB動態(tài)地根據(jù)其中的描述去探測和初始化硬件從而驅(qū)動整個系統(tǒng)。所以當我們談論“設備樹文件 DTS”時我們實際上是在討論整個設備樹技術體系中最核心的“源代碼”部分。它是人類可讀、可編輯的硬件描述藍圖。掌握DTS文件的語法和編寫方法是深入理解嵌入式Linux驅(qū)動開發(fā)和系統(tǒng)移植的必經(jīng)之路。無論你是驅(qū)動工程師、系統(tǒng)移植工程師還是對底層感興趣的應用開發(fā)者讀懂并會修改DTS都能讓你對系統(tǒng)的掌控力提升一個檔次。接下來我們就拋開那些抽象的概念直接深入到DTS文件的語法細節(jié)和實戰(zhàn)技巧中去。2. DTS文件語法核心精講DTS文件看起來像一種自定義的配置文件但它其實有自己嚴謹?shù)恼Z法結(jié)構(gòu)。理解這些語法是編寫和調(diào)試設備樹的基礎。2.1 節(jié)點Node與屬性Property萬物皆節(jié)點設備樹是一種樹形結(jié)構(gòu)這棵樹由節(jié)點Node和屬性Property構(gòu)成。你可以把它想象成一個公司的組織架構(gòu)圖公司是根節(jié)點下面有研發(fā)部、市場部等子節(jié)點每個部門又有自己的屬性如人數(shù)、預算等。節(jié)點是設備樹的基本組成單元用花括號{}定義。每個節(jié)點都有一個名字node-name和一個可選的單元地址unit-address。// 定義一個名為 uart1 的節(jié)點單元地址為 fe001000 uart1: serialfe001000 { // 節(jié)點內(nèi)容屬性和子節(jié)點放在這里 };上面的uart1:是一個標簽label它相當于給這個節(jié)點起了一個別名方便在其他地方通過uart1來引用它這是避免硬編碼地址的關鍵機制。屬性是附著在節(jié)點上的“鍵值對”key-value pairs用于描述該節(jié)點的特性。值可以是多種類型字符串String:compatible “fsl,imx6ull-uart”, “fsl,imx6q-uart”;32位無符號整數(shù)u32:reg 0xfe001000 0x1000;字符串列表String List: 如上例的compatible多個字符串用逗號隔開。二進制數(shù)據(jù)Byte String:local-mac-address [00 11 22 33 44 55];布爾值Empty: 一個沒有值的屬性本身代表“真”例如dma-coherent;表示該設備支持一致性DMA。節(jié)點引用Phandle: 通過label引用其他節(jié)點例如interrupt-parent gic;。2.2 常用標準屬性詳解設備樹有大量預定義的屬性這里介紹幾個最核心、必會的compatible兼容性:這是設備樹中最重要的屬性沒有之一。它定義了設備與哪個驅(qū)動程序綁定。內(nèi)核啟動時會遍歷所有設備節(jié)點為每個節(jié)點的compatible屬性值在已注冊的驅(qū)動程序中尋找最匹配的那一個。值通常由“制造商,型號”組成越具體越優(yōu)先。compatible “ti,omap2-i2c”; // 首選匹配 // 如果找不到 omap2-i2c 的驅(qū)動會嘗試匹配更通用的 i2c 驅(qū)動 compatible “ti,omap2-i2c”, “i2c-bus”;reg寄存器區(qū)域: 描述設備占用的內(nèi)存地址空間。它的值通常是一個或多個(地址, 長度)對。地址和長度的具體含義需要參考其父節(jié)點的#address-cells和#size-cells屬性來決定。// 假設父節(jié)點定義了 #address-cells 1; #size-cells 1; reg 0xfe001000 0x1000; // 表示從地址 0xfe001000 開始長度為 0x1000 字節(jié)的區(qū)域。#address-cells和#size-cells: 這兩個屬性不是描述設備本身而是描述父節(jié)點如何解釋其子節(jié)點的reg屬性。它們定義了子節(jié)點reg屬性中“地址”和“長度”字段分別占用多少個32位數(shù)字cell。soc { #address-cells 1; // 子節(jié)點 reg 的地址字段占1個cell #size-cells 1; // 子節(jié)點 reg 的長度字段占1個cell serialfe001000 { reg 0xfe001000 0x1000; // 因此這個reg被解釋為地址0xfe001000 長度0x1000 }; };對于內(nèi)存節(jié)點#address-cells通常為264位系統(tǒng)#size-cells通常為2。interrupts和interrupt-parent: 描述設備的中斷信息。interrupt-parent指向該設備所屬的中斷控制器節(jié)點通常用標簽引用如gic。interrupts屬性則描述中斷號、觸發(fā)類型等具體格式由其中斷控制器決定。interrupt-parent gic; // 父中斷控制器是 gic interrupts 0 88 IRQ_TYPE_LEVEL_HIGH; // 含義需查GIC控制器文檔通常為 中斷類型 SPI/PPI, 中斷號, 觸發(fā)方式status: 描述設備狀態(tài)。常用值有“okay”或“ok”: 設備可操作?!癲isabled”: 設備存在但當前被禁用?!癴ail”,“failed”: 設備檢測到嚴重錯誤。“reserved”: 設備被保留給其他用途如安全世界。model和compatible(在根節(jié)點): 根節(jié)點的model描述板卡型號compatible描述板卡兼容的機器類型用于匹配內(nèi)核的Machine Description。/ { model “Freescale i.MX6 UltraLite 14x14 EVK Board”; compatible “fsl,imx6ull-14x14-evk”, “fsl,imx6ull”; };2.3 特殊節(jié)點與路徑根節(jié)點/: 設備樹的起點用單個斜杠表示。所有其他節(jié)點都是它的后代。/aliases節(jié)點: 定義全局別名方便通過短名訪問長路徑節(jié)點。aliases { serial0 uart1; // 現(xiàn)在可以通過 /aliases/serial0 或別名 serial0 找到 uart1 ethernet0 fec1; };/chosen節(jié)點: 不描述真實硬件而是由Bootloader向內(nèi)核傳遞運行時參數(shù)如內(nèi)核啟動參數(shù)(bootargs)、標準輸入輸出設備(stdout-path)。chosen { bootargs “consolettymxc0,115200 earlyconec_imx6q,0x30860000,115200”; stdout-path uart1; };/memory節(jié)點: 描述系統(tǒng)的物理內(nèi)存布局這是必須的節(jié)點。memory80000000 { device_type “memory”; reg 0x80000000 0x20000000; // 內(nèi)存從 0x80000000 開始大小為 512MB };節(jié)點路徑: 類似于文件系統(tǒng)路徑可以唯一標識一個節(jié)點。例如/soc/aips-bus02000000/uart02020000。注意屬性名中使用的破折號-是標準寫法而不是下劃線_。例如device_type、interrupt-parent。雖然內(nèi)核有時也能解析下劃線但遵循標準可以避免不必要的麻煩。3. DTS文件的組織、編譯與調(diào)試實戰(zhàn)一個復雜的系統(tǒng)其設備樹描述可能會非常龐大。直接寫在一個文件里會難以維護。因此設備樹提供了強大的模塊化機制。3.1 DTSI頭文件與模塊化設計.dtsi(Device Tree Source Include) 文件類似于C語言的頭文件.h。它用于存放可重用的部分通常是SoC系統(tǒng)級芯片級別的通用定義。一個具體的板卡.dts文件通過#include來包含一個或多個.dtsi文件然后在此基礎上進行覆蓋、修改和添加以描述這塊特定板卡的差異。典型結(jié)構(gòu)arch/arm64/boot/dts/vendor/soc-model.dtsi: 描述SoC內(nèi)部所有核心資源CPU集群、內(nèi)存控制器、各種總線、通用外設控制器如I2C0, SPI0, UART0等。這是由芯片廠商提供的。arch/arm64/boot/dts/vendor/board-model.dts: 描述具體板卡。它首先包含SoC的dtsi然后通過label引用并覆蓋dtsi中的節(jié)點使能或禁用某些外設status “okay”/“disabled”。修改外設的引腳復用pinctrl配置。添加SoC上沒有的、板級特有的設備如LED、按鍵、以太網(wǎng)PHY芯片、音頻編解碼器等。設置正確的時鐘頻率、電壓等板級參數(shù)。示例使能一個在dtsi中默認關閉的I2C控制器并添加一個板載EEPROM設備// 板卡 .dts 文件 #include “soc-model.dtsi” // 包含SoC定義 i2c1 { // 引用SoC dtsi中定義的 i2c1 節(jié)點 status “okay”; // 覆蓋狀態(tài)使能該控制器 clock-frequency 100000; // 設置I2C總線速度為100kHz eeprom50 { // 在使能的i2c1總線下添加一個子設備節(jié)點 compatible “atmel,24c02”; reg 0x50; // I2C設備地址 pagesize 8; }; };3.2 DTC從DTS到DTB的編譯工具鏈DTS/DTSI是人類可讀的文本文件需要編譯成二進制格式的DTB (Device Tree Blob) 文件才能被Bootloader和內(nèi)核使用。這個編譯器就是DTC (Device Tree Compiler)。編譯命令# 將 .dts 編譯為 .dtb dtc -I dts -O dtb -o my-board.dtb my-board.dts # 將 .dtb 反編譯為 .dts (用于分析和調(diào)試) dtc -I dtb -O dts -o my-board-decompiled.dts my-board.dtb在內(nèi)核源碼樹中通常不需要直接調(diào)用dtc使用make dtbs命令即可編譯所有配置的dts文件。常見編譯錯誤Syntax error: 語法錯誤檢查括號、分號、節(jié)點格式。Undefined label: 使用了未定義的節(jié)點標簽xxx檢查標簽名是否正確或?qū)墓?jié)點是否被禁用status “disabled”的節(jié)點標簽可能無法被引用。Duplicate node name: 重復的節(jié)點名確保同一父節(jié)點下子節(jié)點名含地址唯一。scripts/dtc/dtc: No such file or directory: 這通常是在內(nèi)核源碼外單獨使用dtc命令時dtc工具未安裝。需要安裝device-tree-compiler包Ubuntu/Debian或從內(nèi)核scripts/dtc/目錄下編譯生成。3.3 調(diào)試技巧如何查看與驗證設備樹設備樹出了問題驅(qū)動無法加載如何排查以下是一些必備的調(diào)試手段內(nèi)核啟動日志內(nèi)核在解析DTB時會打印大量信息。使用dmesg | grep -i device或dmesg | grep -i dts查看。重點關注錯誤和警告信息。查看/proc/device-tree這是一個神奇的目錄。系統(tǒng)啟動后內(nèi)核會將解析后的設備樹以目錄和文件的形式映射到這里。你可以用ls和cat命令直接查看節(jié)點的屬性和結(jié)構(gòu)。ls /proc/device-tree/ cat /proc/device-tree/model cat /proc/device-tree/soc/i2cfe001000/status注意這里顯示的是二進制屬性對于reg、interrupts等非文本屬性cat出來是亂碼需要用hexdump查看。使用fdtdump工具這個工具可以直觀地以十六進制和部分解析的形式查看DTB文件內(nèi)容比直接反編譯的dts更緊湊。fdtdump my-board.dtb | less驅(qū)動中的OFOpen FirmwareAPI在驅(qū)動代碼中可以使用內(nèi)核提供的OF API來讀取設備樹中的屬性這是驅(qū)動獲取硬件配置信息的標準方式。在驅(qū)動probe函數(shù)中添加打印可以確認是否成功讀取到預期值。// 示例在驅(qū)動中讀取 compatible 和 reg 屬性 pr_info(“Device compatible: %s\n”, of_device_get_match_data(pdev-dev)-compatible); struct resource *res platform_get_resource(pdev, IORESOURCE_MEM, 0); pr_info(“Registers at %pa, size %pa\n”, res-start, res-end);實操心得調(diào)試設備樹相關問題時一個非常有效的思路是“對比法”。找一份能正常工作的、相近板卡的DTS文件與你正在編寫的文件進行逐行對比尤其是compatible字符串、reg地址、中斷號、pinctrl配置等關鍵屬性。90%的問題都出在這些細節(jié)的差異上。4. 典型外設節(jié)點配置實例解析理論說再多不如看幾個實實在在的例子。我們以最常見的幾種外設為例拆解其DTS節(jié)點的標準寫法。4.1 UART串口控制器串口是調(diào)試和輸出的生命線其配置相對標準。uart1 { // 引用SoC dtsi中定義的uart1 status “okay”; pinctrl-names “default”; pinctrl-0 pinctrl_uart1; // 引用pinctrl節(jié)點配置TX、RX等引腳功能 // 通常不需要指定波特率波特率由用戶態(tài)程序設置 };關鍵點pinctrl-0引用的pinctrl_uart1必須在pinctrl子系統(tǒng)中正確定義它描述了引腳復用和電氣特性。這是設備樹中最容易出錯的地方之一必須與硬件原理圖嚴格對應。dma相關屬性如dmas,dma-names在需要DMA傳輸時才配置。4.2 I2C控制器及從設備I2C總線可以掛載多個設備設備樹需要描述總線和從設備。i2c1 { status “okay”; clock-frequency 400000; // 設置I2C總線頻率400kHz pinctrl-names “default”; pinctrl-0 pinctrl_i2c1; // 板載EEPROM eeprom50 { compatible “microchip,24lc02”, “atmel,24c02”; reg 0x50; // 7位I2C地址 pagesize 16; }; // 觸摸屏控制器 touchscreen38 { compatible “edt,edt-ft5x06”; reg 0x38; interrupt-parent gpio1; // 觸摸中斷引腳連接到的GPIO控制器 interrupts 9 IRQ_TYPE_EDGE_FALLING; // GPIO1_9下降沿觸發(fā) reset-gpios gpio1 8 GPIO_ACTIVE_LOW; // 復位引腳 }; };關鍵點I2C從設備節(jié)點必須位于I2C控制器節(jié)點之下。reg屬性是設備的7位I2C地址通常左移一位后的值即硬件地址本身。對于需要中斷或GPIO控制的從設備如觸摸屏需要在節(jié)點內(nèi)配置interrupts和xxx-gpios屬性。4.3 以太網(wǎng)控制器與PHY以太網(wǎng)配置涉及MAC控制器和外部PHY芯片兩者都需要描述。fec1 { // 以太網(wǎng)MAC控制器 status “okay”; pinctrl-names “default”; pinctrl-0 pinctrl_enet1; phy-mode “rmii”; // 指定PHY接口模式如 rmii, rgmii phy-handle ephy0; // 指向PHY設備節(jié)點 phy-reset-gpios gpio5 9 GPIO_ACTIVE_LOW; // PHY復位引腳 phy-reset-duration 100; // 復位時間單位ms mdio { // MDIO總線用于控制PHY #address-cells 1; #size-cells 0; ethphy0: ethernet-phy0 { // PHY設備 reg 0; // PHY在MDIO總線上的地址 max-speed 100; // 最大速率 // 可能還有PHY特定的屬性如LED配置等 }; }; };關鍵點phy-mode必須與硬件連接方式一致否則無法通信。phy-handle建立了MAC控制器和PHY設備的關聯(lián)。PHY的reg地址由硬件設計決定通過MDC/MDIO引腳的上拉下拉電阻配置。4.4 使用GPIO控制LED與按鍵對于簡單的板載設備可以直接在DTS中描述。/ { leds { // 可以創(chuàng)建一個自定義的父節(jié)點來組織LED compatible “gpio-leds”; led-0 { label “heartbeat”; gpios gpio1 0 GPIO_ACTIVE_HIGH; // 連接到GPIO1_0高電平點亮 linux,default-trigger “heartbeat”; // 內(nèi)核內(nèi)置的觸發(fā)器模式 }; led-1 { label “mmc0”; gpios gpio1 1 GPIO_ACTIVE_HIGH; linux,default-trigger “mmc0”; default-state “off”; }; }; gpio-keys { compatible “gpio-keys”; button-0 { label “Boot Button”; gpios gpio1 18 GPIO_ACTIVE_LOW; // 按鍵按下時GPIO為低電平 linux,code KEY_POWER; // 模擬的按鍵鍵值對應 /dev/input/eventX }; }; };關鍵點使用compatible “gpio-leds”和compatible “gpio-keys”可以讓內(nèi)核的對應子系統(tǒng)自動創(chuàng)建設備無需編寫專用驅(qū)動。linux,code指定了按鍵映射到輸入子系統(tǒng)的哪個鍵值可以在linux/input-event-codes.h中找到定義。5. 進階技巧與常見“坑點”避雷指南當你掌握了基礎語法后下面這些進階知識和“坑點”能讓你在實戰(zhàn)中更加游刃有余。5.1 屬性覆蓋與合并規(guī)則設備樹的一個強大特性是“覆蓋”。當.dts文件通過label引用.dtsi中的節(jié)點并添加新屬性時其規(guī)則是標量屬性字符串、整數(shù)等直接覆蓋。后定義的替換先定義的。列表屬性如interrupts,reg的多組值通常也是整體覆蓋。你不能只修改列表中的某一項。子節(jié)點如果標簽引用的是一個節(jié)點而非屬性那么在該引用塊內(nèi)定義的任何新子節(jié)點都會被添加到原節(jié)點下。如果子節(jié)點名含地址相同則會發(fā)生覆蓋。一個常見的“坑”試圖在板級DTS中只修改一個復合屬性的一部分。例如原DTSI中pinctrl-0 pad1 pad2 pad3;你在板級DTS中想只改成pad1 pad4你必須完整地重新定義整個列表而不能只“追加”或“修改”某一項。5.2 條件包含與預處理.dts/.dtsi文件在編譯前會被C預處理器cpp處理。這意味著你可以使用#ifdef,#if,#define等宏來進行條件編譯。這在區(qū)分硬件版本、配置可選功能時非常有用。// 在 dtsi 文件中 #ifdef CONFIG_HAVE_ETH1 fec2 { status “okay”; // ... 其他配置 }; #endif // 在板級 dts 文件中可以通過 /include/ 來包含一個定義了宏的文件 /include/ “my-board-config.dtsi” // 這個文件里可能定義了 #define CONFIG_HAVE_ETH1注意這里的CONFIG_HAVE_ETH1是DTS編譯環(huán)境的宏與內(nèi)核的Kconfig宏是兩套獨立系統(tǒng)雖然名字可以相同。通常通過Makefile或dtc的-D參數(shù)傳遞。5.3 調(diào)試與排查實戰(zhàn)記錄問題一驅(qū)動 probe 函數(shù)不執(zhí)行排查步驟查compatible首先確認DTS中的compatible字符串與驅(qū)動中of_device_id表里的字符串完全一致包括大小寫和逗號。查status確認節(jié)點狀態(tài)是“okay”而不是“disabled”或被注釋掉。查父節(jié)點狀態(tài)如果該設備掛載在某個總線如I2C、SPI下確??偩€控制器的status也是“okay”。查內(nèi)核日志dmesg | grep -E “of|device|probe”看是否有相關錯誤或跳過信息。查/proc/device-tree確認節(jié)點是否存在屬性是否正確。問題二外設寄存器訪問失敗返回 -EBUSY 或 -ENXIO排查步驟查reg屬性確認地址和長度是否正確。與芯片數(shù)據(jù)手冊的存儲器映射表核對。查pinctrl配置這是最最最常見的坑引腳復用配置錯誤會導致引腳功能不是你所期望的外設功能從而無法訪問。仔細核對原理圖和pinctrl配置。查時鐘和復位有些外設需要額外的時鐘和復位控制。檢查DTS中是否有clocks和resets屬性并確認引用的時鐘和復位控制器節(jié)點已使能。問題三中斷無法觸發(fā)排查步驟查interrupt-parent指向的中斷控制器節(jié)點是否正確。查interrupts屬性中斷號和觸發(fā)類型是否正確。需要查閱SoC數(shù)據(jù)手冊中關于中斷控制器的章節(jié)明確每個外設的中斷映射關系。查共享中斷如果中斷是共享的驅(qū)動中需要正確實現(xiàn)IRQF_SHARED標志。用cat /proc/interrupts命令查看你的設備中斷號是否被注冊以及中斷計數(shù)是否在增加。問題四設備樹編譯錯誤Undefined label原因你引用了一個標簽如uart1但DTC編譯器在上下文中找不到這個標簽的定義。解決檢查標簽名是否拼寫錯誤。檢查該標簽對應的節(jié)點是否被條件編譯#ifdef排除在外。重要檢查該節(jié)點是否被設置為status “disabled”。在某些舊版本或特定處理中被禁用的節(jié)點的標簽可能無法被引用。一個變通方法是不要禁用整個父節(jié)點而是只禁用你需要的那部分。5.4 從零開始為一塊新板卡編寫DTS假設你要為一塊基于NXP i.MX6ULL芯片的自制板卡編寫DTS。尋找參考在內(nèi)核源碼的arch/arm/boot/dts/32位或arch/arm64/boot/dts/64位目錄下找到你的SoC型號對應的.dtsi文件如imx6ull.dtsi和官方評估板.dts文件如imx6ull-14x14-evk.dts。以官方EVK的DTS為模板是最快的方式。建立骨架創(chuàng)建你的板卡DTS文件如my-custom-board.dts首先包含SoC的dtsi文件。// SPDX-License-Identifier: GPL-2.0 #include “imx6ull.dtsi” / { model “My Custom i.MX6ULL Board”; compatible “my,custom-board”, “fsl,imx6ull”; // 內(nèi)存配置 memory80000000 { device_type “memory”; reg 0x80000000 0x20000000; // 512MB RAM }; // 其他根節(jié)點屬性如 chosen, aliases };配置系統(tǒng)時鐘檢查并確認clocks節(jié)點下的振蕩器頻率是否正確根據(jù)你的板載晶振修改。使能外設根據(jù)你的板卡原理圖通過label引用SoC dtsi中的外設節(jié)點如uart1,i2c1,usdhc1將其status改為“okay”并配置正確的pinctrl。配置引腳復用這是最繁瑣但最關鍵的一步。你需要為每個使能的外設創(chuàng)建一個pinctrl_xxx節(jié)點通常在iomuxc節(jié)點下根據(jù)數(shù)據(jù)手冊的IOMUXC章節(jié)為每個用到的引腳設置正確的復用模式MX6UL_PAD_XXX_XXX__XXX、上下拉、驅(qū)動強度等。務必與原理圖一一對應。添加板載設備在對應的總線節(jié)點下如i2c1添加你的外部設備節(jié)點如EEPROM、傳感器等。編譯測試使用make dtbs編譯將生成的.dtb用Bootloader加載啟動。通過查看/proc/device-tree和內(nèi)核日志逐步調(diào)試直到所有硬件被正確識別。設備樹的學習是一個“先模仿后理解再創(chuàng)造”的過程。開始時多參考成熟平臺的代碼遇到問題時善用調(diào)試手段逐步積累經(jīng)驗。當你能夠獨立為一塊新板卡配通所有基礎外設時你對Linux底層系統(tǒng)的理解就已經(jīng)遠超大多數(shù)應用開發(fā)者了。這份能力將成為你深入嵌入式世界的堅實基石。