動移植排錯:從SDIO枚舉到休眠睡死的完整鏈路)
從SDIO枚舉到休眠睡死AIC8800DC驅(qū)動移植排錯全過程搞嵌入式Linux的兄弟應該都體會過那種驅(qū)動源碼明明拿到手編譯也過了但板子就是不識別網(wǎng)卡的絕望。我最近在一款主控平臺上移植AIC8800DC無線網(wǎng)卡驅(qū)動前前后后折騰了一周多問題從SDIO設備ID掃描失敗到驅(qū)動ko加載順序錯亂再到休眠喚醒后系統(tǒng)直接睡死每一步踩得都挺扎實。這篇就把整個排錯過程和相關機制完整梳理一遍給正在做SDIO WiFi驅(qū)動移植的朋友當個參考。AIC8800DC這顆芯片如果只看規(guī)格它支持WiFi 6、SDIO 3.0接口、還帶了藍牙協(xié)同性價比不錯常用于平板、電視盒子、工業(yè)網(wǎng)關這類需要無線連接但不想上USB網(wǎng)卡的場景。但支持SDIO和能被你的內(nèi)核正確枚舉出來是兩碼事實際移植時坑點非常多。本文主要面向Linux內(nèi)核驅(qū)動開發(fā)者、BSP工程師以及正在把AIC8800DC往新平臺或者舊內(nèi)核上搬的人。如果你剛接觸驅(qū)動移植這篇文章里的排查思路也可以直接套用到其他SDIO WiFi芯片上。1. 移植前先搞清楚驅(qū)動源碼目錄、固件路徑和加載方式很多人在移植初期就犯了方向性錯誤一拿到驅(qū)動壓縮包就急著改設備樹、調(diào)內(nèi)核配置結(jié)果編譯出來的模塊怎么都加載不上。其實AIC8800DC的驅(qū)動移植第一步不是改代碼而是先弄清楚源碼目錄結(jié)構、固件加載路徑和模塊加載方式。這三件事不搞明白后面所有排錯都像在黑屋里找開關。1.1 源碼放內(nèi)核樹內(nèi)還是樹外決定你后面的編譯方式AIC8800DC的驅(qū)動源碼通常包含兩個核心目錄一個是無線主控驅(qū)動一般叫aic8800_fdrv或者類似命名另一個是藍牙低功耗協(xié)同驅(qū)動常見叫aic8800_btlpm。如果你從廠商或者GitHub倉庫拿到的是完整驅(qū)動包里面一般還會帶一個aic_load_fw相關的工具或者腳本用于在驅(qū)動加載后把固件搬運到芯片里。這里第一個關鍵選擇是源碼是編進內(nèi)核in-tree還是編成外部模塊out-of-tree。AIC8800DC驅(qū)動官方一般默認支持out-of-tree編譯也就是直接進驅(qū)動根目錄執(zhí)行make生成一系列.ko文件然后手動insmod。這種方式的優(yōu)點是靈活不污染內(nèi)核源碼樹缺點是模塊依賴關系要自己維護尤其是insmod順序錯亂時你會看到一堆Unknown symbol報錯。如果你打算編進內(nèi)核那就要把整個驅(qū)動目錄放到kernel/drivers/net/wireless/下并補全Kconfig和Makefile的關聯(lián)規(guī)則。但實際上AIC8800DC驅(qū)動對內(nèi)核版本比較敏感編進內(nèi)核后一旦某個內(nèi)核API變化整個內(nèi)核都會編譯失敗。所以我個人建議移植階段老老實實用out-of-tree方式等全部功能驗證通過之后再考慮是否要固化到內(nèi)核源碼樹里。1.2 從Makefile反推模塊依賴避免加載順序坑驅(qū)動根目錄的Makefile里通常會寫明最終生成的模塊名和依賴關系。我拿到的那版驅(qū)動編譯后會生成類似aic8800_fdrv.ko、aic8800_btlpm.ko、aic_load_fw.ko這樣的模塊。這里有一個非常容易踩的坑這三個模塊存在加載順序要求大體上需要先加載aic_load_fw.ko負責固件下載再加載aic8800_fdrv.ko主控驅(qū)動最后才加載aic8800_btlpm.ko藍牙低功耗協(xié)同。如果順序反了或者只加載了主控驅(qū)動而沒加載固件下載模塊你會看到內(nèi)核日志里出現(xiàn)類似aic8800_fdrv: cant load firmware或者request_firmware failed的報錯。這個問題在我調(diào)試時出現(xiàn)過好幾次一度以為是固件文件路徑不對后來才發(fā)現(xiàn)是模塊加載順序?qū)е碌摹?.3 固件文件路徑request_firmware到底在找什么Linux內(nèi)核的request_firmware機制會按一定順序搜索固件文件。默認路徑一般是/lib/firmware如果內(nèi)核開啟了CONFIG_EXTRA_FIRMWARE_DIR還會附加搜索額外目錄。AIC8800DC驅(qū)動加載固件時通常會用芯片型號相關的文件名去請求比如aic8800_fw.bin、aic8800_tws.bin這類文件。排錯時最簡單的方法就是打開內(nèi)核的DYNAMIC_DEBUG或者直接查/sys/kernel/debug/dynamic_debug/control把firmware_class的調(diào)試開關打開就能看到內(nèi)核到底在哪個路徑下找哪個文件。另一個常用方法是用strace跟蹤insmod進程不過嵌入式板子上不一定有strace所以更通用的是臨時改驅(qū)動源碼在request_firmware調(diào)用前加一條printk把文件名打出來再和實際存放路徑比對。這里我踩過的一個具體坑是固件文件名帶了版本后綴比如aic8800_fw_v3.bin但驅(qū)動代碼里請求的是aic8800_fw.bin導致每次加載都報找不到固件。后來一查是廠商給的README和使用者下載的固件包版本不一致。這種低級錯誤往往比內(nèi)核API適配更消耗時間。2. SDIO設備ID掃描失敗枚舉不到網(wǎng)卡的完整排查鏈路AIC8800DC走SDIO接口時主控側(cè)的SDIO控制器需要先完成對設備的枚舉才能讓驅(qū)動bind上。如果你的內(nèi)核日志里壓根沒有出現(xiàn)mmc1: new high speed SDIO card之類的內(nèi)容說明問題出在設備ID掃描階段根本還沒到驅(qū)動加載那一步。這一章的排查鏈路是我這次移植中花時間最多的一段。2.1 SDIO設備識別機制CIS、廠商ID和設備ID是怎么讀出來的SDIO設備和USB設備類似也有廠商IDVendor ID和設備IDDevice ID但讀取機制完全不同。USB設備通過端點0查詢描述符而SDIO設備是在SD總線初始化階段通過CMD5IO_SEND_OP_COND進入SDIO模式后再讀取CISCard Information Structure來獲取元信息。CIS里包含了制造商IDManufacturer ID、設備IDDevice ID以及功能表等信息。AIC8800DC在SDIO枚舉時會報告一個廠商自定義的設備ID常見的是0x8800這個值具體以你自己手里芯片的實際值為準。驅(qū)動代碼里通常有一個sdiom_func或者sdio_device_id表里面寫著{SDIO_DEVICE(0xXXXX, 0x8800)}這樣的匹配項。如果SDIO控制器枚舉出來的設備ID和驅(qū)動表里的ID不一致內(nèi)核的sdio_bus匹配就會失敗導致probe函數(shù)根本不會被調(diào)用。2.2 掃描不到設備時的三個排查方向當你在內(nèi)核日志里看不到SDIO設備信息時不要急著查驅(qū)動先按下面三個方向排查。第一個方向是硬件層面確認SDIO_CLK、SDIO_CMD、SDIO_D0~D3這六根線是否都接對了。AIC8800DC的SDIO接口一般要求上拉電阻尤其是CMD和DATA線如果漏了上拉枚舉階段會隨機失敗。我這次遇到的情況是板子上D1數(shù)據(jù)線虛焊導致枚舉時CRC錯誤不斷日志里反復出現(xiàn)mmc1: Card stuck in wrong state的警告。第二個方向是主控SDIO控制器的配置。很多主控的SDIO接口默認以SD卡模式工作需要在內(nèi)核設備樹或者板級配置里明確把它配成SDIO模式。如果你的平臺用的是SDHCI控制器檢查設備樹里是否存在sdhci節(jié)點bus-width是否設置成4以及non-removable、keep-power-in-suspend這些屬性是否配置正確。有個容易被忽略的點是mmc-pwrseq如果WiFi芯片的供電由某個GPIO控制需要在設備樹里把電源序列配好否則芯片根本沒上電SDIO總線自然枚舉不到任何設備。第三個方向是電壓域匹配。AIC8800DC的IO電壓一般有兩種版本1.8V和3.3VSDIO接口的信號電平必須和主控側(cè)匹配。如果主控SDIO控制器輸出3.3V而芯片的IO域被配置成1.8V即使線序正確命令階段也會因為電平不匹配而失敗。排查方法是在設備樹中設置sdio節(jié)點的vqmmc或者iovsupply并實測量一下SDIO_CMD引腳的電壓。2.3 手動觸發(fā)SDIO重新掃描和日志驗證手段如果你懷疑是枚舉時序問題可以嘗試手動觸發(fā)SDIO總線重新掃描。在Linux下SDIO設備掛載在mmc總線上可以通過以下方式重新掃描echo mmc1:0001:1 /sys/bus/sdio/drivers/aic8800_fdrv/unbind echo mmc1:0001:1 /sys/bus/sdio/drivers/aic8800_fdrv/bind如果設備號不確定可以先查看/sys/bus/sdio/devices/目錄下有哪些設備條目。不過這里有個前提先得確保內(nèi)核里SDIO設備已經(jīng)被枚舉出來。如果/sys/bus/sdio/devices/下什么都沒有說明問題還在更底層需要回到mmc控制器層面排查。調(diào)試日志方面可以開啟內(nèi)核的MMC子系統(tǒng)調(diào)試信息echo 8 /proc/sys/kernel/printk echo file drivers/mmc/core/sdio_ops.c p /sys/kernel/debug/dynamic_debug/control這樣就能看到CMD5、CMD3、CMD7等命令的交互過程。如果日志顯示CMD5返回超時或者CRC錯誤大概率是硬件線序、供電或者時鐘問題。如果CMD5正常返回但后續(xù)讀取CIS失敗那就需要檢查CIS數(shù)據(jù)的解析邏輯看驅(qū)動和實際CIS中記錄的制造商ID是否匹配。我最終定位到的問題很有意思主控的SDIO控制器在枚舉階段讀到的設備ID是0x8800但驅(qū)動代碼里那張sdio_device_id表寫的是0x8801。這個ID不匹配導致驅(qū)動probe函數(shù)始終不被調(diào)用。改掉驅(qū)動里的設備ID表后設備立刻被識別并bind成功。這個案例也說明拿到驅(qū)動后第一步應該先跑一遍lspci如果是PCIe接口或者查看/sys/bus/sdio/devices/下的ID信息確認驅(qū)動表里的設備ID和實際芯片報告的ID一致能省下大量無頭緒的排查時間。3. 編譯加載階段的高頻報錯符號缺失、ko順序與probe失敗現(xiàn)場設備枚舉成功只是萬里長征第一步。接下來的編譯加載階段AIC8800DC驅(qū)動會暴露出一堆和內(nèi)核版本、編譯選項強相關的問題。這一章我按實際踩坑頻率排序把幾個最高頻的報錯場景和解決辦法整理出來。3.1 內(nèi)核版本差異導致的符號缺失AIC8800DC驅(qū)動的老版本代碼往往依賴一些在新內(nèi)核里已經(jīng)被移除或改名的函數(shù)。比如sdio_claim_irq、sdio_release_irq這些SDIO核心API在較老的內(nèi)核4.x早期里和新內(nèi)核5.15里的實現(xiàn)細節(jié)有差異。如果你的內(nèi)核版本比較新而驅(qū)動源碼還是兩三年前的版本編譯時很容易出現(xiàn)類似下面這種報錯ERROR: mmc_pm_flag_to_genr [drivers/net/wireless/aic8800/aic8800_fdrv.ko] undefined!這類報錯說明驅(qū)動代碼里引用了一個當前內(nèi)核沒有導出的符號。排查方法有兩種一是用grep在內(nèi)核源碼里搜這個符號確認它是否還存在、是否被導出二是直接改驅(qū)動代碼用功能等價的新API替換舊API。比如mmc_pm_flag_to_genr這個函數(shù)在新內(nèi)核里可能被內(nèi)聯(lián)到了mmc/core/sdio_io.c里不再單獨導出那就需要在驅(qū)動里自己實現(xiàn)一個相同邏輯的輔助函數(shù)。這里我的建議是移植前先明確目標內(nèi)核版本然后查看驅(qū)動的Git提交歷史如果有的話看廠商是否已經(jīng)適配過你用的內(nèi)核版本。如果沒有就做好自己適配的準備重點排查drivers/mmc/core/頭文件相關API的變動。3.2 模塊加載順序和probe失敗現(xiàn)場分析如果編譯通過在板子上加載模塊時你可能會看到Unknown symbol這類依賴缺失報錯。SDIO WiFi驅(qū)動往往不止一個模塊AIC8800DC驅(qū)動也一樣前面提到過aic_load_fw.ko和aic8800_fdrv.ko存在依賴關系。正確的加載順序一般是insmod aic_load_fw.ko insmod aic8800_fdrv.ko insmod aic8800_btlpm.ko如果順序不對可能出現(xiàn)Unknown symbol或者主控驅(qū)動加載時probe成功但無法向芯片下載固件。實際調(diào)試時建議用modprobe配合/etc/modules-load.d/下的配置文件來管理或者寫一個啟動腳本在insmod前后加日志打印方便定位是哪一步失敗。probe失敗時常見的內(nèi)核日志有兩種表現(xiàn)。一種是在調(diào)用sdio_claim_irq或者request_irq時失敗日志顯示中斷申請失敗或中斷號無效。這時要檢查設備樹里SDIO中斷的配置以及是否有其他驅(qū)動占用了同一個中斷號。另一種是firmware下載超時日志顯示aic8800_fdrv: download firmware timeout。這個問題多與SDIO時鐘頻率有關。AIC8800DC支持SDIO 3.0高速模式但早期調(diào)試時建議把時鐘頻率限制在50MHz以下也就是SDIO 2.0的默認速率等基本功能跑通后再逐步提高頻率。3.3 內(nèi)存與DMA相關SDIO高性能模式下的隱患AIC8800DC在高速吞吐時會啟用SDIO的DMA傳輸。如果你的主控平臺DMA分配有問題會出現(xiàn)dma_alloc_coherent failed或者系統(tǒng)內(nèi)存碎片化導致的分配失敗。這類問題在內(nèi)存較小的嵌入式平臺上尤其常見。排查思路是先看內(nèi)核的CMA配置CONFIG_CMA給DMA分配預留足夠大的連續(xù)內(nèi)存區(qū)域。一般建議CMA大小至少16MB如果板子內(nèi)存本身就小比如256MB以內(nèi)可以適當調(diào)高但要注意不要影響系統(tǒng)其他模塊的內(nèi)存使用。另一個小技巧是在設備樹SDIO節(jié)點里設置dma-coherent屬性如果平臺支持可以讓DMA操作走一致性緩存路徑減少cache同步帶來的性能損耗。我在實測中發(fā)現(xiàn)AIC8800DC驅(qū)動在高負載傳輸時偶爾出現(xiàn)data CRC error多半是SDIO時鐘頻率過高或者D0~D3走線過長導致的信號完整性問題。把時鐘頻率從150MHz降到100MHz問題就消失。這種問題在量產(chǎn)板上會非常隱蔽建議在驅(qū)動里預留一個模塊參數(shù)來控制SDIO時鐘頻率方便現(xiàn)場調(diào)優(yōu)。4. 休眠配置排錯從suspend到resume的完整檢查清單AIC8800DC驅(qū)動移植里最折磨人的不是枚舉也不是吞吐性能而是休眠喚醒。具體表現(xiàn)是系統(tǒng)執(zhí)行echo mem /sys/power/state進入休眠后看起來一切正常但按喚醒鍵后屏幕亮了WiFi卻連不上甚至整個系統(tǒng)直接睡死只能按復位鍵重啟。這一章我把自己調(diào)試休眠問題的完整檢查清單寫出來每條都是實測過的。4.1 為什么AIC8800DC一休眠系統(tǒng)就睡死SDIO WiFi芯片和USB WiFi芯片在休眠機制上有個顯著區(qū)別SDIO設備依賴主控側(cè)的SDIO控制器供電和時鐘而休眠時主控為了省電往往會關閉SDIO控制器的時鐘和電源。如果驅(qū)動沒有在suspend階段保存芯片上下文并正確配置喚醒源resume后芯片可能處于假死狀態(tài)。AIC8800DC的正常休眠流程應該是驅(qū)動收到suspend通知后通過SDIO命令讓芯片進入WLAN suspend模式同時配置GPIO喚醒腳然后釋放SDIO總線的獨占訪問。resume時驅(qū)動重新獲取SDIO總線恢復寄存器上下文再讓芯片退出suspend模式。整個鏈路中任何一個環(huán)節(jié)斷了都會導致睡死。我遇到的最典型案例是芯片的喚醒GPIO比如WL_WAKE_HOST沒有正確注冊為系統(tǒng)的wakeup source導致resume時內(nèi)核根本不知道有喚醒事件發(fā)生系統(tǒng)一直卡在suspend狀態(tài)。解決方法是在設備樹中給GPIO增加wakeup-source屬性并在驅(qū)動初始化時調(diào)用enable_irq_wake()。4.2 休眠配置常見坑位wowlan、電源域和GPIO控制AIC8800DC驅(qū)動的休眠配置主要集中在以下幾個地方。第一是設備樹里的keep-power-in-suspend屬性。這個屬性告訴SDIO控制器在系統(tǒng)休眠時不要切斷SDIO設備的電源。如果AIC8800DC的供電和主控是同一路電源不設置這個屬性suspend時電源被切斷resume時芯片需要重新下載固件但驅(qū)動可能沒有實現(xiàn)完整的重新初始化流程于是系統(tǒng)直接崩掉。第二是wowlanWake on Wireless LAN配置。AIC8800DC支持通過WiFi報文喚醒系統(tǒng)但這個功能需要在驅(qū)動里使能并且需要固件配合。如果你不需要WOWLAN功能建議在驅(qū)動配置里關閉它因為開著的狀態(tài)下suspend流程會多做很多事情反而更容易出問題。第三是電源域管理。很多主控方案里WiFi芯片的供電由一個PMIC的LDO控制而這個LDO在休眠時默認被關閉。如果AIC8800DC不支持完全斷電休眠即斷電后需要重新下載固件才能恢復就必須在設備樹里給SDIO節(jié)點的vmmc和vqmmc屬性綁定一個regulator并設置regulator-always-on或者regulator-boot-on確保休眠時供電不斷。4.3 用GPIO電平測量和內(nèi)核日志定位休眠問題當休眠問題發(fā)生時我建議先做兩個動作一是用萬用表量芯片的供電電壓和喚醒GPIO電平二是開內(nèi)核日志的動態(tài)調(diào)試打印驅(qū)動的suspend/resume回調(diào)執(zhí)行情況。GPIO電平測量非常直觀。系統(tǒng)進入suspend后量一下WL_WAKE_HOST這個引腳正常狀態(tài)應該是低電平觸發(fā)喚醒后它應該被芯片拉高。如果電平?jīng)]有變化說明芯片根本沒有發(fā)出喚醒信號問題在芯片側(cè)或者固件配置。如果電平變化了但系統(tǒng)沒有resume那問題在主控的中斷控制器或者電源管理單元需要檢查中斷是否注冊、是否被enable_irq_wake正確設置。內(nèi)核日志方面可以打開驅(qū)動的suspend/resume相關調(diào)試打印。很多驅(qū)動的suspend和resume回調(diào)里都有dev_dbg或pr_debug打印通過動態(tài)調(diào)試把它們打開echo file drivers/net/wireless/aic8800/* p /sys/kernel/debug/dynamic_debug/control echo file drivers/mmc/core/sdio_bus.c p /sys/kernel/debug/dynamic_debug/control然后在休眠前后對比日志看suspend回調(diào)是否完整執(zhí)行resume回調(diào)是否進入以及卡在哪個函數(shù)里。我遇到的睡死問題最后定位是在resume階段調(diào)用sdio_claim_host時死鎖。原因是suspend時沒有正確釋放SDIO總線鎖導致resume時同一個線程再次請求鎖直接死鎖。這種問題通過日志能看到resume回調(diào)打印了第一行但卡在sdio_claim_host這基本就是鎖問題。5. 這套排錯思路可以復用到哪些相似網(wǎng)卡和平臺AIC8800DC的移植排錯經(jīng)驗并不只對這一顆芯片有效。SDIO WiFi芯片的驅(qū)動架構在Linux內(nèi)核里高度相似很多坑其實是跨芯片通用的。最后這一章我把這些經(jīng)驗抽象一下希望你在做其他芯片移植時也能少走彎路。5.1 同類SDIO WiFi芯片的共性問題目前市面上常見的SDIO WiFi芯片比如Realtek的RTL8189系列、RTL8822系列Ampak的AP6212、AP6256以及AIC8800DC這類國產(chǎn)方案驅(qū)動移植時遇到的底層問題高度相似。SDIO設備ID不匹配、固件路徑錯誤、模塊加載順序、休眠喚醒死鎖這幾類問題幾乎每一顆芯片都存在。區(qū)別主要體現(xiàn)在廠商代碼風格和內(nèi)核適配程度上。有些廠商的驅(qū)動代碼維護得比較好比如RTL8822CS官方會同步更新內(nèi)核版本適配有些廠商的驅(qū)動代碼則比較陳舊需要使用者自己適配新內(nèi)核的API變化。遇到后者建議在動代碼之前先梳理一份內(nèi)核API差異清單把驅(qū)動用到的關鍵函數(shù)在新內(nèi)核里的狀態(tài)查一遍能省掉很多編譯錯誤。5.2 移植前做一個三件套體檢能省一半時間結(jié)合這次AIC8800DC的移植經(jīng)驗我強烈建議在正式動手前先花半小時做一個三件套體檢。第一件是確認硬件連接。對照芯片的參考原理圖確認SDIO線序、上拉電阻、供電域、喚醒GPIO、復位GPIO都正確。這一步如果出錯后面的所有軟件調(diào)試都是白費功夫。第二件是確認內(nèi)核基礎配置。檢查內(nèi)核是否支持SDIO總線、是否啟用了CONFIG_WLAN、CONFIG_CFG80211等關鍵項確保基礎無線子系統(tǒng)是完整的。第三件是確認固件和驅(qū)動版本匹配。核對固件文件名、版本號、加載路徑最好在干凈的文件系統(tǒng)上先驗證一次。只要這三件事不出錯移植過程至少能避開一半的坑。剩下的問題就是用本文里提到的排查鏈路和調(diào)試手段一個個解決了。我個人的體會是驅(qū)動移植這類工作最怕的不是技術難題而是沒有章法的亂試。每次改動只變一個變量日志完整留存硬件測量和軟件日志對照著看再復雜的問題也能拆解成一條清晰的排查路徑。希望這篇AIC8800DC的排錯筆記能讓你在SDIO WiFi驅(qū)動移植的路上少踩幾個坑尤其是那些讓系統(tǒng)睡死、讓人崩潰的坑。