動開發(fā)實戰(zhàn):從設(shè)備樹到固件燒錄的完整流程)
1. 嵌入式驅(qū)動開發(fā)到底在忙什么很多人一聽“嵌入式驅(qū)動開發(fā)”腦子里浮現(xiàn)的畫面要么是對著密密麻麻的寄存器手冊一行行敲代碼要么是抱著開發(fā)板反復插拔串口線看打印信息。說實話這兩種畫面都沒錯但它們只覆蓋了這份工作不到三成的日常。真正的嵌入式驅(qū)動開發(fā)更像是一個“翻譯官修理工偵探”的混合體你要把硬件的脾氣翻譯成操作系統(tǒng)能聽懂的話要在系統(tǒng)跑不起來的時候從一堆日志里找到那顆松掉的螺絲還要在硬件手冊語焉不詳?shù)牡胤娇拷?jīng)驗和實驗把真相推理出來。我干這行有些年頭了從早期的裸機驅(qū)動到后來的Linux設(shè)備樹時代踩過的坑能寫滿一個筆記本。這篇文章不打算寫成教科書而是想把我自己以及身邊同行每天真正在忙的事情拆開來講——從拿到一塊新板子開始到驅(qū)動跑通、固件燒錄、系統(tǒng)穩(wěn)定運行中間到底經(jīng)歷了哪些環(huán)節(jié)每個環(huán)節(jié)的核心技術(shù)點是什么哪些地方最容易翻車以及怎么用最短的時間把問題定位出來。無論你是剛?cè)胄械那度胧叫氯诉€是從應用層轉(zhuǎn)過來想了解底層的老手這些內(nèi)容都能幫你少走一些彎路。嵌入式驅(qū)動開發(fā)的核心關(guān)鍵詞其實就那么幾個嵌入式、驅(qū)動開發(fā)、Linux、設(shè)備樹、固件。這五個詞基本串起了整個工作流。嵌入式是場景驅(qū)動開發(fā)是手段Linux是操作系統(tǒng)平臺設(shè)備樹是硬件描述機制固件是最終交付物。把這五個環(huán)節(jié)打通你就能獨立負責一塊板子的底層適配了。2. 驅(qū)動開發(fā)的核心工作內(nèi)容拆解2.1 驅(qū)動工程師到底在寫什么代碼驅(qū)動開發(fā)的本質(zhì)是讓操作系統(tǒng)能夠控制硬件。Linux把設(shè)備分成三大類字符設(shè)備、塊設(shè)備和網(wǎng)絡(luò)設(shè)備。字符設(shè)備按字節(jié)流訪問比如串口、按鍵、LED塊設(shè)備按數(shù)據(jù)塊訪問比如eMMC、SD卡、NAND Flash網(wǎng)絡(luò)設(shè)備走socket接口比如以太網(wǎng)、WiFi模組。你寫的每一個驅(qū)動最終都要歸入這三類中的某一類然后向內(nèi)核注冊自己的操作函數(shù)集。以最常見的字符設(shè)備為例你需要實現(xiàn)file_operations結(jié)構(gòu)體里的open、read、write、ioctl、release等函數(shù)。open負責初始化硬件、申請資源read和write負責數(shù)據(jù)搬運ioctl負責處理那些不適合用讀寫表達的配置命令release負責釋放資源。聽起來簡單但實際寫起來光是資源管理就能讓人頭疼——中斷申請了沒釋放、內(nèi)存映射了沒取消、時鐘使能了沒關(guān)閉任何一個疏忽都會導致系統(tǒng)在反復加載卸載驅(qū)動后崩潰。我個人的習慣是每寫一個新驅(qū)動先在紙上畫出硬件的數(shù)據(jù)流圖數(shù)據(jù)從哪個寄存器進來經(jīng)過哪些緩沖最終到哪里去。這個圖不用很精確但必須把關(guān)鍵路徑標出來。有了這張圖代碼結(jié)構(gòu)基本就清晰了剩下的就是查手冊填細節(jié)。2.2 設(shè)備樹驅(qū)動工程師的硬件地圖設(shè)備樹是ARM Linux時代最重要的硬件描述機制。在設(shè)備樹出現(xiàn)之前每個板子的硬件信息都硬編碼在內(nèi)核的arch/arm/mach-xxx目錄下導致內(nèi)核里充斥著大量重復且難以維護的板級代碼。設(shè)備樹把硬件描述從內(nèi)核代碼中剝離出來用一套獨立的語法來描述CPU、內(nèi)存、總線、外設(shè)的拓撲結(jié)構(gòu)和屬性。一個典型的設(shè)備樹節(jié)點長這樣i2c1 { status okay; clock-frequency 100000; touchscreen38 { compatible focaltech,ft6236; reg 0x38; interrupt-parent gpio1; interrupts 5 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio1 6 GPIO_ACTIVE_LOW; }; };這段代碼描述了一個掛在I2C1總線上的觸摸屏控制器地址是0x38中斷引腳接在GPIO1的第5腳復位引腳接在GPIO1的第6腳。驅(qū)動代碼通過compatible屬性來匹配這個節(jié)點然后在probe函數(shù)里解析reg、interrupts、reset-gpios等屬性完成硬件初始化。設(shè)備樹最容易出問題的地方是引腳復用和時鐘配置。很多SoC的引腳功能是復用的同一個物理引腳既可以做UART的TX也可以做I2C的SCL還可以做GPIO。如果你在設(shè)備樹里沒有正確配置pinctrl驅(qū)動就會因為拿不到正確的引腳狀態(tài)而初始化失敗。時鐘也是類似外設(shè)的時鐘源、分頻系數(shù)、門控開關(guān)都需要在設(shè)備樹里描述清楚否則要么外設(shè)不工作要么功耗異常。注意設(shè)備樹修改后必須重新編譯并更新到板子上才能生效。很多新手改了dts文件卻忘記重新編譯dtb然后對著串口日志納悶為什么修改沒起作用。這個坑我踩過不止一次。2.3 從probe到remove驅(qū)動生命周期管理Linux驅(qū)動的生命周期圍繞probe和remove兩個回調(diào)展開。當內(nèi)核啟動或者設(shè)備熱插拔時總線匹配機制會根據(jù)設(shè)備樹中的compatible屬性找到對應的驅(qū)動然后調(diào)用驅(qū)動的probe函數(shù)。probe函數(shù)里要做的事情包括申請GPIO、注冊中斷、申請DMA通道、初始化硬件寄存器、向子系統(tǒng)注冊設(shè)備節(jié)點等。probe函數(shù)的返回值很關(guān)鍵。返回0表示初始化成功返回負數(shù)表示失敗內(nèi)核會記錄錯誤信息并可能嘗試其他驅(qū)動。我見過很多驅(qū)動在probe失敗時沒有正確釋放已經(jīng)申請的資源導致后續(xù)重試時資源沖突。正確的做法是使用devm_系列函數(shù)devm_gpio_request、devm_request_irq、devm_kzalloc等這些函數(shù)申請的資源會在驅(qū)動卸載或probe失敗時自動釋放能省掉大量手動清理的代碼。remove函數(shù)則負責在驅(qū)動卸載時釋放資源、關(guān)閉硬件。雖然有了devm機制后remove函數(shù)的工作量大大減少但有些資源還是需要手動處理比如DMA緩沖區(qū)的釋放、工作隊列的取消、定時器的刪除等。特別是工作隊列和定時器如果不在remove里取消驅(qū)動卸載后它們還在運行訪問已經(jīng)釋放的內(nèi)存就會導致內(nèi)核崩潰。2.4 固件驅(qū)動之外的另一個戰(zhàn)場固件這個詞在嵌入式領(lǐng)域有兩層含義。一層是指燒錄到設(shè)備里的完整系統(tǒng)鏡像包括bootloader、內(nèi)核、設(shè)備樹、根文件系統(tǒng)另一層是指某些外設(shè)自己運行的程序比如WiFi模組的固件、GPU的固件、觸摸屏的配置固件。驅(qū)動工程師經(jīng)常需要和這兩類固件打交道。系統(tǒng)固件的燒錄方式取決于存儲介質(zhì)。eMMC通常用USB下載工具或者SD卡啟動來燒錄SPI NAND需要用專用的燒錄器或者通過bootloader的TFTP功能NOR Flash則可以通過JTAG或者bootloader串口協(xié)議燒錄。無論哪種方式核心步驟都是讓設(shè)備進入燒錄模式、傳輸固件數(shù)據(jù)、校驗寫入結(jié)果、重啟驗證。外設(shè)固件的加載通常是驅(qū)動在probe階段完成的。驅(qū)動通過request_firmware接口從文件系統(tǒng)讀取固件文件然后通過I2C、SPI或USB接口寫入外設(shè)。這里有個常見的坑固件文件必須放在根文件系統(tǒng)的/lib/firmware目錄下而且內(nèi)核配置里要開啟CONFIG_FW_LOADER選項。如果固件加載失敗驅(qū)動通常會打印“firmware request failed”之類的錯誤這時候先檢查文件路徑和權(quán)限再檢查外設(shè)的供電和通信是否正常。3. 實操流程從零適配一塊新板子3.1 硬件確認與資料收集拿到一塊新板子第一件事不是寫代碼而是確認硬件資料是否齊全。你需要的東西包括原理圖、PCB布局圖、芯片數(shù)據(jù)手冊、SoC參考手冊、引腳復用表、時鐘樹圖。原理圖告訴你外設(shè)怎么連接的數(shù)據(jù)手冊告訴你寄存器怎么配置的參考手冊告訴你SoC內(nèi)部有哪些控制器可用。我習慣先把原理圖里所有需要驅(qū)動支持的外設(shè)列一個清單標注每個外設(shè)的接口類型I2C、SPI、UART、GPIO、USB等、地址、中斷號、供電要求。然后對照SoC的引腳復用表確認每個外設(shè)使用的引腳是否與其他功能沖突。這一步做扎實了后面調(diào)試能省一半時間。資料收集階段還有一個容易被忽視的事情確認芯片的硅版本。同一款芯片可能有多個版本不同版本的寄存器定義或已知問題可能不同。比如某些版本的I2C控制器在高負載下會丟中斷需要軟件規(guī)避。這些信息通常藏在芯片的勘誤手冊里不看的話調(diào)試時會被莫名其妙的問題折磨到懷疑人生。3.2 設(shè)備樹編寫與引腳配置設(shè)備樹編寫是板級適配的核心工作。我的做法是先寫一個最小可用的設(shè)備樹只保留CPU、內(nèi)存、串口和存儲控制器確保系統(tǒng)能啟動并進入控制臺。然后再逐個添加外設(shè)節(jié)點每添加一個就測試一個避免一次性改太多導致問題難以定位。引腳配置在設(shè)備樹里通過pinctrl子系統(tǒng)描述。以RK3568為例一個UART2的引腳配置大概是這樣的pinctrl { uart2 { uart2m0_xfer: uart2m0-xfer { rockchip,pins 0 RK_PB6 2 pcfg_pull_up, 0 RK_PB7 2 pcfg_pull_up; }; }; }; uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer; };這里的關(guān)鍵是rockchip,pins屬性的格式第一個參數(shù)是GPIO組號第二個是組內(nèi)引腳號第三個是功能復用編號第四個是電氣特性配置。功能復用編號必須查SoC的引腳復用表填錯了引腳就沒有輸出。提示調(diào)試引腳復用問題時可以用io命令直接讀寫GPIO控制器的寄存器觀察引腳的實際功能狀態(tài)。這比反復改設(shè)備樹重新編譯快得多。3.3 驅(qū)動移植與調(diào)試驅(qū)動移植有兩種策略如果SoC廠商的內(nèi)核源碼里已經(jīng)有類似外設(shè)的驅(qū)動優(yōu)先基于現(xiàn)有驅(qū)動修改如果完全沒有參考就從內(nèi)核主線里找一個同類型外設(shè)的驅(qū)動作為模板。比如你要適配一款新的觸摸屏可以先找內(nèi)核里已有的edt-ft5x06或者goodix驅(qū)動看它們的probe流程和寄存器操作方式然后根據(jù)新觸摸屏的數(shù)據(jù)手冊修改。調(diào)試驅(qū)動最有效的手段是打印日志。內(nèi)核的printk、dev_info、dev_dbg、dev_err等接口可以把信息輸出到串口控制臺。我通常會在probe函數(shù)的關(guān)鍵步驟前后加打印確認代碼執(zhí)行到了哪一步。如果probe根本沒有被調(diào)用那問題就在設(shè)備樹匹配上如果probe調(diào)用了但中途失敗那就根據(jù)失敗點的打印信息去查對應的硬件操作。另一個利器是sysfs和debugfs。很多子系統(tǒng)會在/sys或/sys/kernel/debug下暴露調(diào)試接口。比如I2C子系統(tǒng)有/sys/bus/i2c/devices/下的設(shè)備節(jié)點GPIO子系統(tǒng)有/sys/class/gpio/下的控制接口時鐘子系統(tǒng)有/sys/kernel/debug/clk/下的時鐘樹信息。通過這些接口你可以在不修改驅(qū)動代碼的情況下查看硬件狀態(tài)、讀寫寄存器、調(diào)整參數(shù)。3.4 固件燒錄與系統(tǒng)啟動驗證驅(qū)動調(diào)試完成后下一步是把整個系統(tǒng)固化到板子上。以eMMC為例典型的燒錄流程是用USB線連接板子和PC讓板子進入MaskROM模式用廠商提供的下載工具把bootloader、內(nèi)核、設(shè)備樹、根文件系統(tǒng)打包寫入eMMC。燒錄完成后重啟觀察串口輸出確認系統(tǒng)能正常啟動到登錄界面。系統(tǒng)啟動驗證要關(guān)注幾個關(guān)鍵點bootloader是否正常加載內(nèi)核、內(nèi)核是否正常初始化所有驅(qū)動、根文件系統(tǒng)是否正常掛載、網(wǎng)絡(luò)和存儲是否可用。如果啟動卡在某一步串口日志會給出線索。比如卡在“Waiting for root device”通常是根文件系統(tǒng)掛載參數(shù)不對或者存儲驅(qū)動有問題卡在“Kernel panic”通常是內(nèi)核配置或設(shè)備樹有嚴重錯誤。我個人的經(jīng)驗是第一次燒錄新板子時先用最簡單的根文件系統(tǒng)比如BusyBox做的initramfs確認內(nèi)核和基本驅(qū)動沒問題后再換成完整的根文件系統(tǒng)。這樣可以把問題范圍縮小避免文件系統(tǒng)的問題干擾對驅(qū)動問題的判斷。4. 常見問題與排查技巧實錄4.1 驅(qū)動probe失敗的典型原因驅(qū)動probe失敗是嵌入式開發(fā)中最常見的問題之一。根據(jù)我的經(jīng)驗原因大致可以歸為以下幾類問題現(xiàn)象可能原因排查方法probe根本沒被調(diào)用設(shè)備樹compatible不匹配檢查驅(qū)動of_match_table和設(shè)備樹compatible字符串是否一致probe返回-EPROBE_DEFER依賴的資源還沒準備好檢查時鐘、 regulator、GPIO等依賴是否已注冊probe返回-ENODEV硬件不存在或通信失敗用i2c-tools或spi-tools掃描總線確認設(shè)備應答probe返回-EINVAL設(shè)備樹屬性解析失敗檢查屬性名稱和格式是否正確用of_property_read確認probe成功但設(shè)備不工作寄存器配置錯誤讀回寄存器值對照數(shù)據(jù)手冊確認其中-EPROBE_DEFER是最容易被誤解的。這個返回值的意思是“我現(xiàn)在還不能初始化請稍后再試”內(nèi)核會把驅(qū)動放到延遲探測隊列里等依賴的資源就緒后重新調(diào)用probe。如果你看到日志里反復出現(xiàn)probe defer不要慌這是正常機制。但如果一直defer到超時那說明某個依賴永遠沒準備好需要去查那個依賴的驅(qū)動為什么沒加載。4.2 設(shè)備樹調(diào)試的實用技巧設(shè)備樹調(diào)試最大的痛點是修改后需要重新編譯、燒錄、重啟周期太長。有幾個技巧可以縮短這個周期第一使用設(shè)備樹覆蓋Device Tree Overlay。Overlay允許你在不重新編譯整個設(shè)備樹的情況下動態(tài)修改設(shè)備樹的某些節(jié)點。對于調(diào)試階段的引腳配置、時鐘頻率調(diào)整非常方便。第二利用/proc/device-tree目錄。系統(tǒng)啟動后內(nèi)核會把實際使用的設(shè)備樹展開到/proc/device-tree下你可以直接查看每個節(jié)點的屬性和值確認設(shè)備樹是否被正確解析。第三使用fdtdump和dtc工具反編譯dtb文件。有時候你懷疑燒錄的dtb不是最新的可以用fdtdump把板子上的dtb導出來和源碼編譯出的dtb對比確認是否一致。注意修改設(shè)備樹中的中斷觸發(fā)方式時要特別小心。邊沿觸發(fā)和電平觸發(fā)的配置不同如果配錯了中斷可能會丟失或者反復觸發(fā)。我遇到過把電平觸發(fā)配成邊沿觸發(fā)導致按鍵偶爾失靈的問題查了半天才定位到設(shè)備樹。4.3 固件加載失敗的排查思路固件加載失敗通常表現(xiàn)為驅(qū)動probe時報“firmware request failed”或者“failed to load firmware”。排查步驟可以按以下順序進行確認固件文件存在于/lib/firmware目錄下文件名和驅(qū)動請求的名稱完全一致包括大小寫。確認內(nèi)核配置了CONFIG_FW_LOADERy或m并且對應的文件系統(tǒng)已掛載。檢查固件文件的權(quán)限確保root用戶可讀。如果固件是通過外設(shè)接口傳輸?shù)臋z查外設(shè)的供電和通信是否正常。查看dmesg中是否有更詳細的錯誤信息比如“direct firmware load failed”后面通常會跟具體原因。有些外設(shè)的固件加載還依賴于特定的時序比如先拉高復位引腳、再釋放、然后等待一段時間才能開始傳輸固件。這些時序要求通常寫在數(shù)據(jù)手冊的“Power-Up Sequence”章節(jié)里不看的話很容易卡在固件加載這一步。4.4 系統(tǒng)穩(wěn)定性問題的定位方法驅(qū)動開發(fā)中最難纏的問題是系統(tǒng)偶發(fā)性崩潰或卡死。這類問題往往沒有固定的復現(xiàn)步驟日志也可能不完整。我的排查策略是首先開啟內(nèi)核的panic和oops記錄功能確保崩潰時能把調(diào)用棧打印到串口或者保存到存儲里。其次使用內(nèi)核的ftrace功能跟蹤函數(shù)調(diào)用看看崩潰前最后執(zhí)行的是哪個驅(qū)動的哪個函數(shù)。再次檢查是否有內(nèi)存泄漏或競態(tài)條件用kmemleak檢測內(nèi)存泄漏用lockdep檢測鎖的使用是否正確。還有一個容易被忽視的點是電源管理。很多SoC支持運行時電源管理外設(shè)在空閑時會被自動關(guān)閉時鐘或斷電。如果驅(qū)動沒有正確處理runtime PM的回調(diào)外設(shè)可能在系統(tǒng)進入低功耗狀態(tài)后無法正常喚醒。這類問題通常表現(xiàn)為系統(tǒng)休眠后外設(shè)失靈需要檢查驅(qū)動的suspend和resume回調(diào)是否完整。5. 驅(qū)動工程師的日常工具鏈與學習路徑5.1 必備工具與調(diào)試環(huán)境搭建嵌入式驅(qū)動開發(fā)的工具鏈可以分為幾類交叉編譯工具鏈、調(diào)試工具、分析工具、版本控制工具。交叉編譯工具鏈通常由SoC廠商提供比如ARM的gcc-arm-linux-gnueabihf或者aarch64-linux-gnu。選擇工具鏈時要注意glibc版本和內(nèi)核版本的兼容性版本不匹配可能導致編譯出的程序在板子上跑不起來。調(diào)試工具方面串口是必備的幾乎所有的啟動日志和內(nèi)核打印都通過串口輸出。JTAG調(diào)試器在驅(qū)動開發(fā)初期很有用可以單步跟蹤內(nèi)核啟動過程但日常開發(fā)中用的不多。網(wǎng)絡(luò)調(diào)試也很重要通過NFS掛載根文件系統(tǒng)可以避免反復燒錄通過SSH登錄板子可以方便地傳輸文件和執(zhí)行命令。分析工具包括perf用于性能分析ftrace用于函數(shù)跟蹤strace用于系統(tǒng)調(diào)用跟蹤i2c-tools和spi-tools用于總線調(diào)試。這些工具在排查具體問題時非常高效建議提前在板子上部署好。5.2 從應用層轉(zhuǎn)驅(qū)動開發(fā)的注意事項很多做應用層開發(fā)的朋友想轉(zhuǎn)驅(qū)動問我需要補哪些知識。我的建議是分三步走第一步補硬件基礎(chǔ)。不需要會畫板子但要能看懂原理圖知道GPIO、I2C、SPI、UART這些接口的基本工作原理和時序特征。推薦找一塊簡單的開發(fā)板從點燈和按鍵開始用sysfs接口操作GPIO感受一下硬件控制的過程。第二步補內(nèi)核基礎(chǔ)。理解內(nèi)核的模塊機制、字符設(shè)備框架、設(shè)備樹語法、中斷處理流程。可以從寫一個最簡單的hello world模塊開始然后逐步過渡到字符設(shè)備驅(qū)動、platform驅(qū)動、I2C驅(qū)動。第三步補調(diào)試技能。驅(qū)動開發(fā)的大部分時間不是在寫代碼而是在調(diào)試。學會看串口日志、用dev_dbg打印、用sysfs查看狀態(tài)、用示波器或邏輯分析儀抓時序這些技能比寫代碼本身更重要。5.3 持續(xù)學習與社區(qū)資源嵌入式Linux驅(qū)動開發(fā)的知識更新很快內(nèi)核每幾個月就發(fā)布一個新版本新的子系統(tǒng)和框架不斷涌現(xiàn)。保持學習的最好方式是訂閱內(nèi)核郵件列表、關(guān)注SoC廠商的BSP更新、參與開源社區(qū)的項目。我個人經(jīng)常逛的幾個地方內(nèi)核文檔目錄Documentation/下的驅(qū)動開發(fā)指南、SoC廠商的GitHub倉庫、以及一些活躍的嵌入式論壇。遇到問題時先搜一下有沒有人遇到過類似的情況往往能省下大量時間。另外建議養(yǎng)成寫筆記的習慣。每解決一個驅(qū)動問題就把問題現(xiàn)象、排查過程、最終原因和解決方法記錄下來。這些筆記積累多了就是你自己的一本調(diào)試手冊下次遇到類似問題時能快速定位。6. 一些踩坑之后的個人體會驅(qū)動開發(fā)這個活說到底是和硬件打交道而硬件是不講情面的。軟件寫錯了可以改硬件設(shè)計錯了要么飛線要么改板成本高得多。所以我在probe函數(shù)里養(yǎng)成了一個習慣每一步硬件操作之后都讀回寄存器確認狀態(tài)而不是盲目相信寫進去的值就是對的。這個習慣幫我提前發(fā)現(xiàn)了很多硬件問題比如供電不足導致寄存器寫入失敗、時鐘頻率不對導致通信超時等。還有一點不要過度依賴廠商提供的驅(qū)動。廠商的驅(qū)動往往是為了快速出貨寫的代碼質(zhì)量參差不齊有些甚至是從舊版本內(nèi)核直接移植過來的存在大量兼容性問題。拿到廠商驅(qū)動后先通讀一遍理解它的初始化流程和關(guān)鍵配置然后根據(jù)自己板子的實際情況做調(diào)整。該改的地方不要猶豫該重寫的地方也不要偷懶。最后保持耐心。驅(qū)動調(diào)試有時候就像破案線索藏在日志的某個角落里需要你一點點拼湊。遇到卡住的時候不妨先放一放去喝杯水回來再看往往會有新思路。我很多次都是在洗澡或者散步的時候突然想到問題可能出在哪里然后回去一試就通了。