動(dòng)深入解析:從卡識(shí)別到 HS400 的完整調(diào)試指南)
這一篇是整個(gè) Linux MMC 驅(qū)動(dòng)系列里我拖得最久的一篇也是我實(shí)際項(xiàng)目里踩坑最多的一段。前五篇我們把 host 控制器注冊(cè)、MMC core 框架、SD 卡初始化鏈路、塊設(shè)備層對(duì)接這些內(nèi)容基本過了一遍按道理說框架已經(jīng)清楚了。但你會(huì)發(fā)現(xiàn)真正到了嵌入式板子上調(diào) eMMC 或者 MMC 類型卡的時(shí)候情況完全不是“照著代碼讀一遍”就能搞定的。這也是我為什么要在第六篇專門把 MMC type card 這條分支單獨(dú)拉出來講透——從卡檢測(cè)、類型識(shí)別、ext_csd 解析到 block 設(shè)備掛載再到 HS200/HS400 的切換和調(diào)試排障整條鏈路里面藏著大量“文檔上不會(huì)寫、但代碼里就是如此”的細(xì)節(jié)。這篇適合誰看已經(jīng)在看 MMC 驅(qū)動(dòng)源碼、想把 eMMC 初始化流程徹底搞明白的驅(qū)動(dòng)開發(fā)者或者正在調(diào)板子、發(fā)現(xiàn)卡識(shí)別不穩(wěn)定、速度上不去、老是在某個(gè)狀態(tài)機(jī)卡住的人。說句實(shí)話純讀內(nèi)核文檔是學(xué)不會(huì) MMC 驅(qū)動(dòng)的真正能讓你少熬夜的是那些在代碼里翻來覆去比對(duì)后得到的經(jīng)驗(yàn)值。這篇就把我積累的這些值都放出來。1. MMC type card 在 Linux 驅(qū)動(dòng)棧中的位置1.1 三層四層type card 到底屬于哪一層很多剛開始讀 MMC 驅(qū)動(dòng)的人會(huì)被層數(shù)搞暈。舊內(nèi)核里習(xí)慣叫“三層”host 層、core 層、card 層。后來 4.x 之后內(nèi)核把 card 層的塊設(shè)備邏輯抽到了 block 層實(shí)際變成四層host 控制器層、core 通用層、card 類型層、block 層。但 MMC type card 這個(gè)概念不太等于某一層而是一條橫跨 core 和 block 的完整數(shù)據(jù)路徑。具體說當(dāng)你插入一張 MMC 卡注意不只是傳統(tǒng)的 MMC 卡還包括絕大多數(shù) eMMC 板載存儲(chǔ)內(nèi)核首先由 host 層觸發(fā)檢測(cè)進(jìn)入 core 層的 mmc_rescan然后在 core 里根據(jù)卡的協(xié)議特征區(qū)分出是 MMC_TYPE_MMC、MMC_TYPE_SD 還是 MMC_TYPE_SDIO。type card 這個(gè)詞強(qiáng)調(diào)的就是 MMC 類型區(qū)別于 SD 類型的卡。它在 core 層對(duì)應(yīng)的是 mmc_attach_mmc 這個(gè)分支在 block 層對(duì)應(yīng)的是 mmc_blk_probe 這個(gè)驅(qū)動(dòng)綁定邏輯。所以你在代碼里找 MMC type card 的入口第一站就是 drivers/mmc/core/mmc.c 里的 mmc_attach_mmc第二站就是 drivers/mmc/core/block.c。而真正決定卡會(huì)不會(huì)被識(shí)別成 MMC 類型的是它響應(yīng)命令時(shí)的協(xié)議特征。1.2 幾種卡類型的區(qū)分別把 SD 當(dāng) MMC在實(shí)際項(xiàng)目中最容易被忽略的就是“明明插的是 SD 卡卻走進(jìn)了 SD 分支插的是 eMMC卻沒進(jìn) MMC 分支”。這背后其實(shí)是協(xié)議層的差異決定的。我把典型的類型區(qū)分整理一下卡類型內(nèi)核分支入口典型場(chǎng)景識(shí)別關(guān)鍵點(diǎn)MMC_TYPE_MMCmmc_attach_mmceMMC 板載存儲(chǔ)、傳統(tǒng) MMC 卡響應(yīng) CMD1 時(shí) OCR 狀態(tài)、無法通過 CMD8/SD 識(shí)別流程MMC_TYPE_SDmmc_attach_sdSD、microSD 卡CMD8 能拿到有效的 R7 響應(yīng)MMC_TYPE_SDIOmmc_attach_sdioWiFi/BT 組合模塊CMD5 能得到 SDIO OCR 響應(yīng)MMC_TYPE_HOSTmmc_attach_busloopback 自測(cè)、虛擬卡host 自己注冊(cè)時(shí)指定SD 卡在協(xié)議上兼容了部分 MMC 命令但 SD 卡會(huì)先響應(yīng) CMD8然后再走 ACMD41 這個(gè)流程。MMC 類型卡不一樣它不認(rèn)識(shí) CMD8 或?qū)?CMD8 響應(yīng)方式不同內(nèi)核只能回到 CMD1 去拉 OCR、看電壓匹配。內(nèi)核代碼里這個(gè)“退回”邏輯也非常有意思它不會(huì)一上來就斷定你是 MMC而是依次嘗試 SD、SDIO、MMC 三個(gè)分支。這個(gè)順序本身就是卡識(shí)別過程里最值得注意的細(xì)節(jié)。1.3 為什么單獨(dú)把 type card 拉出來講我的實(shí)際體會(huì)是MMC 類型卡和 SD 卡的分水嶺不僅僅是協(xié)議上的更是工程調(diào)試上的。SD 卡插拔頻繁要考慮卡檢測(cè)中斷、寫保護(hù)開關(guān)、熱插拔。MMC 類型卡特別是 eMMC不插拔、固定焊在板子上要考慮的是電源上電時(shí)序、啟動(dòng)時(shí) boot partition 的處理、HS200/HS400 的高速切換、以及系統(tǒng)休眠喚醒時(shí)的狀態(tài)保持。這些差異直接決定你在調(diào)試時(shí)看什么文件、查什么寄存器、加什么打印。如果你拿 SD 卡的調(diào)試經(jīng)驗(yàn)去套 eMMC大概率會(huì)被“啟動(dòng)時(shí)偶爾失敗、休眠喚醒后掛在 IO 上”這類問題折磨到懷疑人生。2. 從卡插入到類型確定的完整流程2.1 檢測(cè)起點(diǎn)mmc_rescan 的狀態(tài)機(jī)卡檢測(cè)的起點(diǎn)是 mmc_rescan。這個(gè)函數(shù)在 core.c 里無論是輪詢方式還是 gpio 中斷方式最終都會(huì)調(diào)用到它。大概的執(zhí)行路徑是判斷 host 是不是在恢復(fù)、是不是已經(jīng)在 rescan 中如果 host 沒有卡嘗試 mmc_attach_* 系列如果已有卡且檢測(cè)到移除調(diào)用 mmc_detach_bus 和 mmc_remove_card如果已有卡且還在則可能進(jìn)入重新初始化流程。這里一個(gè)典型的坑是mmc_rescan 內(nèi)部有延時(shí)和狀態(tài)判斷如果在中斷上下文頻繁觸發(fā)可能出現(xiàn)重復(fù)檢測(cè)。內(nèi)核里通過 host-rescan_disable 和 host-detect_change 配合把檢測(cè)請(qǐng)求延遲到 workqueue 里執(zhí)行。你自己調(diào)試時(shí)如果發(fā)現(xiàn) dmesg 里反復(fù)出現(xiàn)“Card removed during transfer”可以重點(diǎn)查一下是不是中斷觸發(fā)的 detect 邏輯導(dǎo)致了重復(fù)掃描。我個(gè)人建議你在改驅(qū)動(dòng)時(shí)千萬不要直接在中斷回調(diào)里調(diào)用 mmc_detect_change而要用它自帶的工作隊(duì)列機(jī)制。原因是 MMC 初始化過程中的命令交互非常耗時(shí)一次 card detect 往往要執(zhí)行好幾條命令每條命令都涉及 complete 等待和超時(shí)計(jì)時(shí)放在中斷上下文里直接把系統(tǒng)調(diào)度搞死。2.2 OCR、CID、CSD 這些寄存器怎么讀卡類型確定之后真正讓內(nèi)核“認(rèn)識(shí)”這張卡靠的是一組寄存器讀取。MMC 卡最關(guān)鍵的三個(gè)寄存器是 OCR、CID、CSD。OCR 寄存器表示卡支持的電壓范圍和工作狀態(tài)。內(nèi)核通過 mmc_send_op_cond 來發(fā) CMD1并從響應(yīng)中解析 OCR。代碼里這一步會(huì)反復(fù)發(fā)送 CMD1直到 busy 位被清零然后再把 host 支持的電壓范圍和卡支持的電壓范圍做交集只有交集非空才會(huì)繼續(xù)。CID 寄存器是卡的唯一標(biāo)識(shí)里面包含廠商 ID、產(chǎn)品名、生產(chǎn)時(shí)間等。CMD2 用來獲取 CID之后所有卡都會(huì)用這組數(shù)據(jù)做區(qū)分。CSD 寄存器描述卡的容量、最大傳輸速度、塊大小、讀/寫時(shí)序參數(shù)通過 CMD9 讀取。讀取這些寄存器的輔助函數(shù)在 mmc_ops.c 里都能找到比如 mmc_send_cid、mmc_send_csd。我調(diào)試時(shí)最常用的操作是cat /sys/block/mmcblk0/device/cid cat /sys/block/mmcblk0/device/csd cat /sys/block/mmcblk0/device/ocr如果這三項(xiàng)能讀出來說明卡的基本命令通路是通的剩下的問題基本都在高速模式切換或者傳輸階段。2.3 關(guān)鍵結(jié)構(gòu)體與初始化關(guān)系卡一旦被識(shí)別就會(huì)創(chuàng)建一個(gè) struct mmc_card 來承載整個(gè)生命周期。這個(gè)結(jié)構(gòu)體你一定要熟悉因?yàn)楹竺鎺缀跛姓{(diào)試都繞不開它。關(guān)鍵字段我列一下type卡類型文章開頭提到的那幾種state卡的當(dāng)前狀態(tài)包括卡是否在線、是否就緒ocr從 OCR 寄存器讀出的電壓信息cid/csd/ext_csd寄存器信息緩存sector_count容量從 CSD 或者 ext_csd 計(jì)算得出csd.capacity塊數(shù)ext_csd.revext_csd 版本號(hào)決定了卡支持的特性quirks驅(qū)動(dòng)針對(duì)某些卡的特殊處理。從命名上你應(yīng)該能看出struct mmc_card 就是把一張硬件卡抽象成內(nèi)核里的一個(gè)對(duì)象。它由 mmc_add_card 向內(nèi)核注冊(cè)由 mmc_remove_card 注銷。整個(gè)生命周期管理得清楚不容易出問題真正出問題的是結(jié)構(gòu)體字段被某個(gè)私有驅(qū)動(dòng)寫錯(cuò)之后隨機(jī)崩潰。3. 核心代碼路徑mmc_attach_mmc 到 mmc_blk_probe3.1 真正分清 MMC 與 SD 的分水嶺mmc_attach_mmc 位于 drivers/mmc/core/mmc.c已經(jīng)發(fā)展為 mmc_init_card 和 mmc_attach_bus 的組合流程。關(guān)鍵的初始化步驟大致如下。第一步是 mmc_attach_bus設(shè)置 host 的總線操作函數(shù)也就是把 host-bus_ops 指向 mmc_ops。這時(shí)候總線起草能力已經(jīng)綁定到 mmc 類型。第二步是 mmc_init_card這個(gè)函數(shù)內(nèi)部做了大量工作再次發(fā)送 CMD1確認(rèn)卡就緒讀取 CID讀取 CSD讀取 ext_csd根據(jù) ext_csd 支持的能力設(shè)置卡的屬性如果有必要執(zhí)行卡進(jìn)入 high speed 模式的 switch 操作。在整個(gè)過程中對(duì) MMC 類型卡影響最大的是 ext_csd。這可以說是 MMC/eMMC 特有的一把鑰匙。通過 ext_csd驅(qū)動(dòng)能知道卡是否支持 HS200、HS400、DDR、是否支持 CMDQ、boot 分區(qū)大小、安全寫保護(hù)區(qū)域等。我可以負(fù)責(zé)任地說調(diào)試 eMMC 啟動(dòng)失敗時(shí)你在 ext_csd 上花的時(shí)間一定是最多的。比如 read_ext_csd 失敗那你根本看不到卡支持什么模式所有高速切換都無從談起。3.2 從 card 到 block 的對(duì)接初始化完成之后mmc_add_card 會(huì)把卡注冊(cè)到設(shè)備模型里生成 /sys/block/mmcblkX 對(duì)應(yīng)的設(shè)備。重點(diǎn)在 mmc_driver 和 mmc_blk_probe 的匹配過程。簡單理解mmc_add_card 把一個(gè) mmc_card 設(shè)備添加到總線上然后 mmc_blk 驅(qū)動(dòng)通過 mmc_bus_match 判斷設(shè)備類型是 MMC/SD就會(huì)調(diào)用 mmc_blk_probe。mmc_blk_probe 里面的動(dòng)作很關(guān)鍵分配 struct mmc_blk_data初始化工作隊(duì)列、請(qǐng)求隊(duì)列從卡的分區(qū)表里讀取分區(qū)信息為每個(gè)分區(qū)注冊(cè)一個(gè) gendisk最終你會(huì)看到 /dev/mmcblk0、/dev/mmcblk0boot0、/dev/mmcblk0boot1 這些節(jié)點(diǎn)。對(duì)于 eMMCboot 分區(qū)尤其重要因?yàn)橄到y(tǒng) bootloader 通常在 boot0 或 boot1 里。我在實(shí)際項(xiàng)目里見過因?yàn)?boot 分區(qū)沒設(shè)置到位導(dǎo)致系統(tǒng)升級(jí)后 bootloader 損壞又無法從備份分區(qū)啟動(dòng)的案例。這類問題建議你直接用 mmc bootpart enable 操作來恢復(fù)我在后面的排查章節(jié)會(huì)細(xì)說。3.3 request 到 command 的映射塊設(shè)備層發(fā)起讀寫請(qǐng)求后最終要變成一組 MMC 命令在總線上跑。這里的轉(zhuǎn)化路徑是bio request → mmc_blk_rw_rq_prep → 構(gòu)造 mmc_request → mmc_wait_for_req → host 層執(zhí)行 → 中斷完成回調(diào)mmc_request 里最核心的成員是 cmd、stop、data分別對(duì)應(yīng)命令、停止命令、數(shù)據(jù)傳輸塊。對(duì)于 MMC 類型卡而言read 操作通常對(duì)應(yīng) CMD17/CMD18write 操作對(duì)應(yīng) CMD24/CMD25。CMD18 和 CMD25 是多塊傳輸需要在結(jié)束的時(shí)候發(fā) CMD12 停止傳輸。這里我強(qiáng)調(diào)一個(gè)工程上容易出問題的地方多塊傳輸?shù)慕Y(jié)束方式分為 open-end 和預(yù)定義塊數(shù)MMC 卡通常支持預(yù)定義塊數(shù)也就是你在 data 里設(shè)置 blocks硬件會(huì)按塊數(shù)自動(dòng)停止。但如果 host 控制器驅(qū)動(dòng)沒有按這個(gè)邏輯設(shè)置容易傳著傳著多傳或少傳一塊然后 CRC 錯(cuò)誤滿天飛。操作單塊命令多塊命令停止命令讀CMD17CMD18CMD12寫CMD24CMD25CMD12從實(shí)際性能角度講單塊命令只適合小文件或元數(shù)據(jù)訪問真正吞吐量上來還得靠多塊命令加 request 合并不然性能連普通 U 盤都比不過。4. 設(shè)備樹配置與實(shí)際移植經(jīng)驗(yàn)4.1 一個(gè)能跑起來的 dts 節(jié)點(diǎn)該怎么寫默認(rèn)情況下如果 host 控制器驅(qū)動(dòng)支持標(biāo)準(zhǔn)設(shè)備樹綁定你把 MMC 設(shè)備節(jié)點(diǎn)加到 dts 里之后Linux 就能枚舉卡。但枚舉和“穩(wěn)定工作”完全是兩回事。我建議至少保證這樣的節(jié)點(diǎn)配置mmc0 { status okay; bus-width 8; non-removable; cap-mmc-highspeed; mmc-hs200-1_8v; mmc-hs400-1_8v; vmmc-supply vcc_3v3; vqmmc-supply vcc_1v8; pinctrl-names default, state_uhs; pinctrl-0 mmc0_pins_default; pinctrl-1 mmc0_pins_uhs; };有幾個(gè)屬性的理解值得展開說一下。bus-width 是數(shù)據(jù)線數(shù)量eMMC 一般至少 4 位常見 8 位。錯(cuò)了會(huì)導(dǎo)致數(shù)據(jù)線上信號(hào)錯(cuò)位癥狀是最開始能識(shí)別但讀取大塊數(shù)據(jù)時(shí)不穩(wěn)定。non-removable 表示這是板載存儲(chǔ)內(nèi)核不會(huì)輪詢檢測(cè)卡移除。千萬別小看這個(gè)屬性如果沒配系統(tǒng)會(huì)周期性地給 eMMC 發(fā)檢測(cè)命令影響休眠喚醒。cap-mmc-highspeed 使能 MMC 的 high speed mode如果缺失eMMC 大概率被限制在 26MHz 或者更低的默認(rèn)頻率。4.2 HS200/HS400 的時(shí)序選擇HS200 和 HS400 是 eMMC 的兩種高速模式。HS200 是單數(shù)據(jù)速率 8 位接口下跑 200MHz 時(shí)鐘HS400 是 DDR 模式 8 位接口同樣是 200MHz 時(shí)鐘但等效吞吐翻倍。要把這兩種模式跑起來光是 dts 里寫 mmc-hs200-1_8v 是不夠的還需要卡本身在 ext_csd 中聲明支持 HS200/HS400host 控制器支持對(duì)應(yīng)的時(shí)序模式信號(hào)完整性要過關(guān)尤其是 HS400 對(duì)信號(hào)質(zhì)量非常敏感必要的時(shí)候需要做 tuning也就是通過發(fā)送 tuning block 來尋找最佳采樣點(diǎn)。實(shí)際調(diào)試中最常見的問題是把頻率提上去之后 CRC 錯(cuò)誤暴增或者干脆卡住。這時(shí)候先別急著改驅(qū)動(dòng)用示波器看一眼信號(hào)眼圖再看 host 是否支持 DLL delay 調(diào)節(jié)。很多國產(chǎn) SoC 的 MMC host 在 HS400 下需要配合特定 DLL 配置不開的話采樣點(diǎn)不對(duì)必掛。4.3 雙電壓與信號(hào)電平的關(guān)系eMMC 引腳上有一路主電源 vmmc通常是 3.3V一路 IO 電源 vqmmc可能是 1.8V 或 3.3V。高速模式HS200/HS400一般要求 1.8V 信號(hào)電平所以在切高速之前內(nèi)核會(huì)先通過 host 的 start_signal_voltage_switch 回調(diào)把 vqmmc 從 3.3V 切換到 1.8V。如果切換失敗或者穩(wěn)壓器不支持動(dòng)態(tài)調(diào)壓內(nèi)核就會(huì)放棄高速模式退回 legacy 模式。我見過一種奇葩情況dts 里配置了 mmc-hs400-1_8v但 vqmmc 對(duì)應(yīng)的 regulator 沒有配置 set_voltage 操作內(nèi)核調(diào) switch 時(shí)返回 -EINVAL然后整個(gè)初始化流程回退。如果你看到 dmesg 里出現(xiàn) “signal voltage switch failed”先查 regulator 而不是卡。5. 常見問題排查與調(diào)試實(shí)錄5.1 啟動(dòng)卡在 mmc0: error -110-error 110 是 ETIMEDOUT也就是命令超時(shí)。這個(gè)錯(cuò)誤我遇到最多但原因卻五花八門。最常見的原因是 vmmc 或 vqmmc 沒供電。有些板子的 eMMC 電源由一個(gè) GPIO 控制dts 里 if 沒正確把 regulator 和 GPIO 關(guān)聯(lián)或者時(shí)序上供電晚于 host 時(shí)鐘卡自然不回命令。第二個(gè)常見原因是 CMD1 一直 busy卡上電后需要時(shí)間完成內(nèi)部初始化。標(biāo)準(zhǔn)做法是反復(fù)發(fā)送 CMD1 直到 busy 位釋放但如果你板子的時(shí)鐘頻率太低卡在內(nèi)部初始化期間響應(yīng)窗口就會(huì)變長可能導(dǎo)致 host 超時(shí)。第三個(gè)原因比較隱蔽host 控制器時(shí)鐘沒開啟。MMC host 控制器本身需要兩路時(shí)鐘一路是卡時(shí)鐘一路是控制器接口時(shí)鐘。如果接口時(shí)鐘沒使能發(fā)命令后控制器根本沒動(dòng)作也會(huì)表現(xiàn)為超時(shí)。排查思路我一般遵循示波器量時(shí)鐘 → 量 vmmc/vqmmc → 看 CMD 線是否有時(shí)序 → 再查 dmesg。這條路徑省了我不少時(shí)間。5.2 CID 能讀出但 CSD 失敗怎么辦這種情況很詭異說明卡的命令通路基本是通的CMD2 能響應(yīng)但 CMD9 讀不到正確的 CSD。如果 CSD 返回 CRC 錯(cuò)誤先懷疑頻率。MMC 類型卡在識(shí)別階段數(shù)據(jù)傳輸模式還沒建立過高的時(shí)鐘會(huì)讓數(shù)據(jù)線上的信號(hào)質(zhì)量變差CRC 錯(cuò)誤的概率自然上升。你可以先把 host 的 max_bus_clk 調(diào)低比如從 200MHz 降到 52MHz 甚至 26MHz測(cè)一下能不能穩(wěn)定讀出來。另一個(gè)原因是 1.8V 信號(hào)切換時(shí)序沒對(duì)齊。有些卡的 CSD 讀取要求在信號(hào)電壓切換之后進(jìn)行如果你提前讀取會(huì)返回亂碼。可以看一下 host-ios.timing 和 ios.signal_voltage 的值確認(rèn)當(dāng)前狀態(tài)。我最后加一個(gè)經(jīng)驗(yàn)CSD 偶爾失敗一次不要緊但反復(fù)失敗一定不要忽略。它往往預(yù)示著硬件鏈路不穩(wěn)定在高溫或者低電壓下會(huì)演變成大規(guī)模讀寫失敗。5.3 用 mmc-utils 工具快速定位問題調(diào)試 MMC 類型卡內(nèi)核日志只是第一步真正好用的是 mmc-utils 這套用戶態(tài)工具。常用命令如下# 查看 ext csd重點(diǎn)看卡支持特性 mmc extcsd read /dev/mmcblk0 # 讀取卡狀態(tài) mmc status get /dev/mmcblk0 # 設(shè)置 boot 分區(qū) mmc bootpart enable 1 1 /dev/mmcblk0 # 設(shè)置 boot bus mmc bootbus set single_backward x1 x8 /dev/mmcblk0調(diào)試的時(shí)候我會(huì)重點(diǎn)看 extcsd 里的 DEVICE_TYPE、BOOT_INFO、RPMB_SIZE、CACHE_SIZE、ERASE_GROUP_DEF 這些字段。比如 DEVICE_TYPE 顯示不支持 HS400但 dts 里寫了 mmc-hs400-1_8v那工作模式自然切不過去白費(fèi)功夫。bootpart enable 這個(gè)命令值得單獨(dú)強(qiáng)調(diào)。很多板子第一次燒錄 bootloader 前eMMC 的 BOOT_BUS_WIDTH 和 PARTITION_ACCESS 配置不是期望值導(dǎo)致 bootloader 啟動(dòng)不了。這時(shí)候用 mmc bootpart enable 1 1 把 boot 分區(qū)對(duì)齊到物理分區(qū)再配合 bootbus 設(shè)置能解決一批“代碼燒進(jìn)去了但起不來”的問題。5.4 用 tracepoint 看命令級(jí)耗時(shí)如果你想把問題定位到“哪條命令慢了”這個(gè)粒度最簡單的方法是用內(nèi)核的 MMC tracepoint。內(nèi)核開啟以下配置后可以在 ftrace 里看到完整的 mmc 請(qǐng)求流程CONFIG_MMC_DEBUGy CONFIG_FTRACEy CONFIG_TRACEPOINTSy然后cd /sys/kernel/debug/tracing echo mmc:* set_event echo 1 tracing_on cat trace_pipe通過 trace你能看到 mmc_request_start 和 mmc_request_done 之間的時(shí)間差確認(rèn)每條命令的實(shí)際耗時(shí)從而判斷是否超時(shí)、是否 retry。這個(gè)手段比 dmesg 里的“某某命令超時(shí)”要精細(xì)得多因?yàn)樗芸吹绞悄囊徊铰纳踔聊馨l(fā)現(xiàn)多塊命令之間的間隙過大導(dǎo)致的性能問題。6. 一些容易被忽略但很實(shí)用的點(diǎn)6.1 塊層合并對(duì)性能的影響很多人調(diào)試 MMC 性能時(shí)只盯著 host 控制器頻率忽略了塊層的 bio 合并機(jī)制。MMC 驅(qū)動(dòng)通過 mmc_blk_set_queue 初始化請(qǐng)求隊(duì)列時(shí)會(huì)設(shè)置 queue 的合并參數(shù)比如 max_segments、max_segment_size。這些參數(shù)如果沒有和 host 的 SGMII/DMA 能力匹配就會(huì)產(chǎn)生大量碎片化請(qǐng)求吞吐打折。一個(gè)直觀表現(xiàn)是順序讀 100MB/s順序?qū)懼挥?40MB/s而且 dmesg 里沒有任何錯(cuò)誤。這種時(shí)候去看 blktrace往往是因?yàn)槊看螌懻?qǐng)求很小沒有合并成大的 segment。合理的做法是確認(rèn) host 控制器支持 multi-block DMA 傳輸并調(diào)整 queue 的參數(shù)讓內(nèi)核一次下發(fā)盡量大的請(qǐng)求。比如設(shè)置 max_segments 為 64 或 128max_segment_size 為 1MB都能有效提升順序讀寫性能。6.2 電源域和 runtime pm 的配合MMC host 控制器的電源管理是 MMC 類型卡低功耗設(shè)計(jì)的重頭戲。特別是板載 eMMC系統(tǒng)休眠時(shí)要把時(shí)鐘關(guān)掉、電源可以保持也可以關(guān)斷但喚醒時(shí)不能直接訪問卡必須先重新初始化鏈路。內(nèi)核里通過 mmc_runtime_suspend/mmc_runtime_resume 來實(shí)現(xiàn)。有些平臺(tái)會(huì)出現(xiàn)休眠喚醒后第一個(gè) IO 訪問超時(shí)就是因?yàn)閱拘蚜鞒讨?host 的時(shí)鐘恢復(fù)和卡的狀態(tài)重建沒有同步。我調(diào)試下來的經(jīng)驗(yàn)是先確認(rèn) host 是否有 PM runtime 回調(diào)再確認(rèn)卡在恢復(fù)時(shí)是否重新執(zhí)行了 init。如果喚醒后直接調(diào)用 mmc_blk_read卡可能還處于休眠狀態(tài)或者狀態(tài)寄存器里的電源狀態(tài)不對(duì)導(dǎo)致首個(gè)請(qǐng)求失敗。大部分平臺(tái)會(huì)在 runtime_resume 里調(diào)用 mmc_power_up mmc_init_card這一步不能省。6.3 關(guān)于 CMDQ 的一些經(jīng)驗(yàn)較新版本內(nèi)核在 MMC 類型卡上支持 command queueingCMDQ典型實(shí)現(xiàn)是 mtk 和某些平臺(tái)的 emmc 控制器。CMDQ 的好處是能同時(shí)下發(fā)多條命令提高并發(fā)度但它的實(shí)現(xiàn)復(fù)雜度明顯上升。如果你只是想穩(wěn)定、穩(wěn)妥不追求極限性能可以先不加 CMDQ 支持。因?yàn)?CMDQ 模式下隊(duì)列深度、標(biāo)簽管理、錯(cuò)誤恢復(fù)都變得更加復(fù)雜。一旦出錯(cuò)恢復(fù)路徑要處理的情況非常多調(diào)試成本遠(yuǎn)高于性能收益。我的建議是先把無 CMDQ 的模式調(diào)穩(wěn)定性能不夠再考慮引入。結(jié)尾部分聊聊個(gè)人體會(huì)。我在實(shí)際調(diào)試中最大的體會(huì)是MMC type card 的驅(qū)動(dòng)并不復(fù)雜復(fù)雜的是它跟硬件時(shí)序、電源管理、塊層調(diào)度這些外部因素交錯(cuò)時(shí)的表現(xiàn)。你可能花三天時(shí)間查一個(gè)時(shí)好時(shí)壞的 bug最后發(fā)現(xiàn)是某個(gè) GPIO 的上電時(shí)序晚了幾毫秒。也可能糾結(jié)了很久的寫入性能問題最后把 request queue 參數(shù)一調(diào)就解決。所以我一直建議項(xiàng)目里專門留一份 MMC 相關(guān)的調(diào)試 checklist把電壓、時(shí)鐘、dts 屬性、ext_csd 狀態(tài)、tracepoint 這些工具全部串起來而不是等出了問題再從頭摸索。最后再分享一個(gè)小技巧無論遇到什么詭異現(xiàn)象先把頻率降到最低、關(guān)掉高速模式、關(guān)掉 runtime pm能穩(wěn)定出來再一項(xiàng)一項(xiàng)開回去。這個(gè)方法能幫你快速確定問題出在高速特性上還是出在基本通路里。希望這篇能幫你少走一些彎路。