
做BSP的兄弟应该都有体会Android平台上的SD卡问题看起来只是“插卡”这件小事真正追下去会发现它横跨设备树、内核驱动、vold存储框架、文件系统好几个层面。我这次处理的MTK平台Android 8.1项目起因是测试反馈两个问题一是大于128GB的UHS-I卡插上去只能读不能挂载二是连续读写大文件时卡顿明显偶尔还会在休眠唤醒之后丢失存储。这两个现象综合起来指向同一个方向——SD卡的驱动能力配置与实际硬件能力不匹配需要从驱动层做一轮系统修改。这里说的“驱动能力修改”不是简单改一两个宏而是要对SD卡控制器的能力位做全面梳理识别阶段允许什么总线速度、UHS模式要不要全部放开、电压切换是否正常、卡检测用GPIO还是轮询、文件系统层是否能支持exFAT。把这些点理清楚问题才能根治。我会先把这个项目需求拆开讲再把每一处修改的代码位置、参数含义和验证方法放出来文末附上排障速查表适合做BSP、驱动和系统集成的同学参考对操作过Android存储的老手来说也可以对照排查。实际项目里需求方不会在意你改的是设备树还是vold他们只会看到“插卡能不能用、快不快”。所以改动之前先把链路走一遍确定瓶颈在哪一层比急着动代码重要得多。1. 项目概况SD卡驱动能力修改到底在改什么1.1 这次改动的目标与触发原因先交代一下背景。该项目基于某硬件平台的Android 8.1方案外置存储一直用的是标准SD卡槽需求方最初提的规格只是“支持常见的FAT32格式卡”。结果量产前测试发现市面上的新卡容量越做越大128GB已经很普遍256GB和512G的卡也开始进入评测清单。测试同学在新卡上做拷贝验证前几次还正常换到另一张256G卡后就直接弹“存储设备异常”的提示格式化也没用。从这个现象反推问题极大概率不是卡本身损坏而是系统某层对SD卡的容量或分区格式支持不到位。当时我拿到手先做了一轮快速验证插上卡之后adb shell进去看dmesg发现mmc1初始化是成功的卡的类型和容量都读到了但vold日志里卡在“mount”这一步报的是不认识的文件系统。这样一来问题范围立刻缩小驱动识别没有错错在后半段文件系统挂载环节。再看第二个问题连续读写大文件卡顿。测试同学用同一张卡连续往里面拷贝几个电影文件速度从最初的四五十兆每秒一路掉到几兆中间还伴随“设备已断开”的短暂弹窗。这个现象通常指向UHS模式协商不稳定或者驱动在高速模式下频繁重校准。把几个现象拼在一起改动方向就出来了既要调底层的SD控制器能力位又要补文件系统层的exFAT支持还要优化挂载后的读写参数。这就是一次典型的“SD卡驱动能力修改”项目。1.2 Android 8.1上SD卡的完整链路先完整过一遍SD卡在Android 8.1上的工作链路这样后面定位问题才有全局视野。物理上SD卡通过主控平台的第一路MSDC控制器接入SoCMSDC控制器负责SD总线的时钟、命令和数据处理再往上走内核的mmc核心层负责协议解析、卡初始化、UHS模式协商而后是块设备层经过mmcblkX设备节点交给vold守护进程vold负责分区表解析、文件系统检测和挂载最终把容量呈现给应用层。Android 8.1这个版本比较特殊外置存储的权限模型已经收敛到传统模型vold通过sdcardfs对外暴露但物理卡本身还是走FAT/exFAT这条老路线。很多做应用开发的同事对sdcardfs熟悉对物理卡链路反而不熟这也可以理解。但如果你要做驱动层面的修改就必须把mmc core、块设备、vold三层的关系吃透因为在不同层次改同一件事效果可能完全不同。举个例子你以为“容量支持不够”是驱动上限其实很可能只是FAT32分区表不支持超过2TB的寻址你以为“读写慢”是驱动时钟跑太低其实可能是vold挂载时用了旧的fsck参数。这些坑在下面都会展开说。链路示意图就不画了一张dmesg的输出就能看到各阶段的状态后面验证章节会给出具体判断方法。2. 动手前先定位源码分布与修改思路2.1 MTK平台SD卡驱动的层级结构MTK平台在Linux内核里对应SD卡控制器的驱动叫mtk-msdc源码通常位于kernel/drivers/mmc/host/mtk-msdc.c。这名字可能随SDK版本有细微差异有的分支叫mtk-msdc.c有的叫msdc.c本质上都是同一个东西负责MSDC控制器的DMA、中断、时钟门控、GPIO引脚复用等底层操作。设备树节点一般在kernel/arch/arm64/boot/dts/对应项目目录下的dts或dtsi文件里节点名通常是msdc1因为msdc0一般给了eMMCmsdc1给SD卡部分带读卡器扩展的方案还有msdc2。很多初次改SD卡的同学会把目光直接盯在mtk-msdc.c文件里觉得驱动逻辑都在C代码里其实恰好相反。MTK的设备树节点已经把大部分能力位暴露出来了改dts往往比改C代码更安全、更直接。比如总线宽度、最大时钟频率、支持的UHS模式、卡检测引脚、是否启用SDIO这些在dts里都有对应属性。真正需要动C代码的场景通常是某个行为不符合预期比如SD卡识别失败后的重试逻辑、电压切换时序、tuning重试次数这些才需要回溯到mtk-msdc.c或者mmc核心层。这里有一个非常关键的经验MTK平台的内核版本和分支差异很大Android 8.1对应的内核版本可能是4.4也可能是4.9同一个dts属性在不同内核版本里解析规则并不完全一致。动手之前先确认项目内核版本再去看对应的Documentation/devicetree/bindings/mmc/文档不要凭网上旧帖子的经验硬套。2.2 从需求到落点先判断该动哪一层收到需求之后我习惯先画一个简单的“需求—层级”映射避免改错地方。如果问题是“大容量卡识别不到完整容量”优先怀疑分区表和文件系统而不是驱动能力如果问题是“识别都识别不了”那才是驱动和设备树该管的事如果问题是“能识别但读写极慢”大概率是UHS模式没有协商成功或者驱动把卡降级到了默认速度如果问题是“休眠唤醒后丢卡”那就是电源管理和时钟门控的问题和识别逻辑关系反而不大。我这次项目的两个症状“大于128GB无法挂载”和“连续读写卡顿”正好跨了文件系统与UHS协商两个层面。所以我的修改分三步走第一步改dts把msdc1节点的能力位补全让它能协商到SDR104模式第二步把内核的exFAT支持打开解决分区挂载问题第三步在vold挂载层面检查fsck和挂载参数确保exFAT能用合适的簇大小读写。三步各有明确的验证点前一步没过就继续往后排查不会眉毛胡子一把抓。判断“该动哪一层”还有一个很实用的起点先看一眼dmesg。mmc1初始化阶段会打印出卡类型、速度模式、时序、容量等关键信息哪怕只是把dmesg里“mmc1: mmc_sd_init_card”这一段贴到搜索框里都能筛掉一大半低级问题。这个习惯建议所有做驱动的同事都养成后面验证章节我会给出具体的日志判断点。3. 核心修改三处最常动的驱动位置与参数详解3.1 设备树节点的能力开关先看设备树。MTK平台的msdc1节点在去掉厂商私有配置后一个相对完整的写法是这样的msdc1 { pinctrl-names default, state_uhs; pinctrl-0 mmc1_pins_default; pinctrl-1 mmc1_pins_uhs; bus-width 4; max-frequency 200000000; cap-sd-highspeed; sd-uhs-sdr12; sd-uhs-sdr25; sd-uhs-sdr50; sd-uhs-sdr104; no-sdio; no-mmc; cd-gpios pio 12 GPIO_ACTIVE_LOW; status okay; };逐个拆一下这些属性的意思。bus-width4是SD卡协议决定的SD卡物理接口最多四线数据所以这个值在SD卡上不能乱改写1或者8都会出问题。max-frequency是控制器允许跑到的最高时钟SD卡协议中SDR104模式最高208MHz这里写200MHz是常见做法留了一点余量如果你确定卡的体质不错想再激进一点可以写208M但我不推荐因为高速模式对PCB走线和信号完整性要求高强行超频只会加大识别失败概率。sd-uhs-sdr12、sdr25、sdr50、sdr104这几个属性表示允许启用哪些UHS-I模式。值得注意的是大部分项目默认只开了sdr50没有打开sdr104。而SDR104才是UHS-I卡支持的最高速度理论带宽能到104MB/s。我这次就是把sdr104打开之后连续读大文件的速度才明显提上来。如果你的硬件设计有问题比如走线太长、时序不满足打开sdr104反而会初始化失败到时候再通过降级开关回到sdr50即可驱动不会直接罢工而是自动做能力协商。cd-gpios是卡检测引脚的声明对应电路中用于检测SD卡是否插入的GPIO。有的板子没有这个引脚或者GPIO占用冲突就需要改用轮询模式。轮询模式在dts里对应broken-cd属性加了broken-cd之后驱动会按固定间隔检测卡是否存在代价是卡热插拔的响应变慢而且会增加一点待机功耗。能用GPIO检测就不要上轮询这是硬件设计时就要定的驱动层面只能做补救。另外要提醒一句dts节点改完之后很多同学只编译kernel结果发现改动不生效原因在于产品项目普遍会做DTS overlay机制最终的dtb可能是由底层dtsi和产品overlay叠加出来的。改完底层dtsi后要么确认overlay没有覆盖相关节点要么找到实际生效的那个dts文件再改否则排查一天都不一定能找到问题。这个坑踩过一次印象非常深刻。3.2 mmc核心与mtk-msdc驱动的行为调整dts改完之后如果还需要调整驱动的内部行为才会去动mtk-msdc.c和mmc核心层。比如我这次遇到的“休眠唤醒后丢卡”根因和时钟门控有关。MSDC控制器的时钟在不工作的时候会被门控掉以省电但如果门控时序与SD卡进入休眠的状态不同步唤醒时SD卡会响应超时最终被驱动判成remove。这种问题在C代码里通常表现为mmc1 error -110也就是请求超时。处理这类问题的思路一般有两个方向一是修改驱动里的时钟门控条件比如在suspend时强制保持SD卡时钟输出或者延迟门控时间二是调整驱动的runtime PM策略让SD卡在低功耗状态下不进入真正断电的深度休眠。这两个方向没有标准答案要看具体平台的power domain设计。我这次的平台通过把msdc1的runtime PM延迟从100ms调大到2s基本消除了休眠唤醒丢卡代价是待机电流多了几个毫安属于可以接受的工程权衡。再强调一个容易被忽略的行为调整点协议兼容性导致的降级重试。很多卡在主控上第一次协商UHS模式失败驱动会尝试降低速度重新协商这个过程在代码里叫retune。默认的重试次数和超时时间可能不够导致一部分体质普通的卡直接被判无法识别。建议在mtk-msdc.c里把retune相关的计数器日志打开先观察几次失败时的具体步骤再决定是增加重试次数还是扩大超时窗口不要盲目加大参数否则卡初始化时间会变得很长体验反而更差。3.3 文件系统层的配合修改驱动层把卡识别出来了不代表Android就能用。Android 8.1外置SD卡的默认文件系统策略是这样的vold检测到分区表后会尝试用blkid检测分区文件系统类型如果检测到FAT32就挂载为FAT32如果检测到exFAT且内核支持就挂载为exFAT如果内核没有exFAT支持vold会提示用户格式化或直接报错。这就引入了最常见的一个“假驱动问题”卡识别了容量也看得到但挂载失败用户看到的就是不能用。MTK的Android 8.1 SDK里exFAT支持往往以内核Kconfig项出现比如CONFIG_EXFAT_FS。有的方案默认不开有的开了但注册成FUSE方式性能远不如内核原生支持。我这次的做法是在kernel defconfig里把exFAT的内核支持打开同时确认vold侧的mkfs.exfat、fsck.exfat等工具都已经放在文件系统镜像里。如果只有驱动支持、没有用户态工具格式化也会失败等于半套方案。还要注意FAT32与exFAT的边界问题FAT32分区最大只支持2TB单卷文件最大4GBexFAT几乎没有这些限制但碎片化管理和多平台兼容性略差。如果你的需求是支持512G或1T的卡实际上驱动MMC层面并没有硬性上限真正决定上限的是分区表是不是MBR——MBR只能寻址2TB。所以这种需求通常要把vold里分区创建逻辑从MBR改成GPT别把时间浪费在改驱动上。4. 编译、烧录与一次完整的验证流程4.1 修改后的编译注意事项dts和defconfig改完之后编译流程要特别小心。dts的变化编译进dtbdefconfig的变化决定内核配置两者都不只是重编kernel那么简单。MTK平台一般用mk脚本整体打包boot.img内部会先生成dtb.img再打包。如果你只改了dts要确保最终烧录镜像里的dtb确实是新编译出来的版本方法是在编译完后检查out目录下的dtb.img时间戳或者直接解包boot.img看dtb内容。编译exFAT支持时有一个不太容易注意的点如果你是通过CONFIG_EXFAT_FSm把它编成模块而不是编进内核镜像那么要确认这个ko文件被打进了系统的vendor分区或rootfs并且有配套的加载逻辑。很多项目为了省空间把文件系统驱动编成模块结果跑到现场发现ko没打包vold就永远等不到文件系统支持。我这次的方案是直接编进内核少一个模块加载环节稳定优先。另外不要忽略预编译产物。MTK项目里lk、preloader这些阶段也涉及SD卡初始化——系统启动早期要从SD卡或eMMC找刷机资源时用的不是linux内核里的驱动而是另一套代码。如果你的修改是让系统“能认出大容量卡”大概率不需要动这层但如果目标是让设备从TF卡启动那就必须同步改preloader里的SD卡初始化代码否则会出现“内核认了、preloader不认识”的奇怪状态。这个区分很多新手容易搞混把两种场景放在这里可以对号入座。4.2 用dmesg和mmc debugfs确认改动生效驱动改动是否生效最直接的证据就是dmesg。插卡之后在dmesg里会看到SD卡主机被识别为SD卡然后打印卡的最大时钟、SDR模式、总线宽度、容量等字段。关键看两点第一速度模式是不是已经协商到SDR104插UHS-I卡时如果显示的还是default speed或high speed说明UHS协商没成功要么是dts开关没生效要么是卡不支持第二容量字段是否和卡面标称一致如果标称512G但内核只识别到128G那就要回到分区表的问题而不是驱动。mmc子系统还提供debugfs接口路径一般在/sys/kernel/debug/mmc1/下面。进入该目录后可以看到ios信息里面会列出当前时钟频率、总线宽度、时序模式、电压等级等变量。建议在每次修改后把插卡瞬间的ios内容记录下来前后对比就能看出哪一段被限制了。比如时钟频率一直卡在50M说明sdr104没有打开总线宽度一直显示1-bit说明bus-width属性没有生效。靠日志定位比靠猜快得多。还有一个非常实用的技巧如果你的内核编译时开了CONFIG_MMC_DEBUG可以通过mmc debugfs打开协议级日志能看到SD卡响应每个初始化命令的细节包括CMD0、CMD8、ACMD41这些关键握手。对于那种“特定品牌卡识别不了”的兼容性问题这一步几乎是必备排障手段。配合逻辑分析仪和示波器验证信号质量基本能确定80%以上问题出在硬件还是软件。4.3 性能与兼容性验证清单驱动改完不能只测一张卡就宣布搞定。我的做法是准备一个卡库至少包含三类卡一张较新的大容量UHS-I卡一张普通的class 10卡一张杂牌低速卡。新卡用来验证SDR104和exFAT大文件读写普通卡用来验证降级路径是否平滑也就是新卡拔掉换旧卡时驱动能否自动协商到合适速度杂牌卡用来验证协议兼容性尤其是那些对UHS支持不规范的卡最容易在retune阶段出问题。测试项目上我会用dd读写固定大小文件同时记录耗时另外配合一个随机小文件拷贝测试因为对用户体验来说大量小文件的卡顿比单一顺序读写更明显。dd命令的count块大小建议分别用4K、1M和100M覆盖小文件、常规媒体、大文件三种场景。最后做一轮休眠唤醒测试在SD卡持续读写时按下休眠键唤醒后检查挂载点是否还在并立即执行一次写操作确保没有变成只读状态。每轮测试完成之后把dmesg和debugfs的ios信息都归档一次。别嫌麻烦等某天在现场碰到“返回同样问题但环境不同”的情况这些记录就是排查的底气。我见过太多人现场抓瞎就是因为没有留下基线数据前后版本对比不出来。5. 常见问题与排查速查表5.1 改完不识别或者识别极慢这类问题排在第一位因为它最让人崩溃。不识别分两种情况插卡完全没有反应和插卡要等几十秒才出现挂载。前者优先查dts里的cd-gpios是否接对、GPIO是否被占用以及pinctrl的default状态是否把引脚复用成了其他功能后者优先查UHS-tuning流程——SD卡在进入高速模式前要发CMD19做tuning如果卡响应慢或者信号质量差tuning会反复失败内核会启动降级重试从而拖慢初始化。不要一上来就怀疑mtk-msdc.c的C代码大部分“插上没反应”都是dts overlay覆盖问题。这里给一个排查顺序简单但有效先烧回上一个已知正常的boot镜像排除是烧录包本身的问题再看dmesg里的mmc1初始化段有没有error关键字再检查/sys/kernel/debug/mmc1/ios里的状态最后才去翻C代码。这个顺序能帮我们把90%的问题快速收敛到具体层次。如果log里出现mmc1: error -110这类超时通常是时钟或电压问题出现CMD/ACMD timeout则多半是信号完整性问题要往硬件方向查。5.2 性能不升反降改完sdr104之后有人会发现性能反而比之前更慢了。这可能吗可能。UHS-I模式对信号质量要求更高卡在SDR104模式下如果频繁触发retune每次retune都会打断读写整体吞吐反而下降。现象就是顺序读速度忽高忽低甚至有几次掉到个位数。这种情况下优先检查主控到卡槽之间的走线长度、串阻和地回路能不能改硬件实在改不了就得在软件上妥协——把sdr104关掉退回sdr50损失一半理论带宽但换来稳定。另一个常见的性能陷阱是exFAT的簇大小。簇太小大文件碎片化严重读写效率极低簇太大小文件浪费空间且随机写性能糟糕。我实测过512G的卡用128KB簇顺序读写和随机小文件能达到一个较好的平衡点如果主要存视频这类大文件可以加大到256KB。但簇大小是在SD卡格式化时决定的改驱动改不到这个参数要改的是vold格式化工具mkfs.exfat的参数或者让测试同学用特定工具去格式化卡。5.3 休眠唤醒后丢卡休眠唤醒丢卡大概率不是SD卡真的坏了而是驱动在suspend时把控制器时钟门控了SD卡在低功耗状态下时钟消失后内部状态机混乱唤醒后再发命令就得不到正确响应。前面把runtime PM延迟调大解决了问题但还有一种情况是改了dts里的pm-ignore-notify之类的属性反而掩盖了问题而不是解决。排查时用dmesg在唤醒瞬间抓日志看是否有command timeout或card removed字样有的话基本坐实是门控时机问题没有的话再怀疑硬件接触不良。这种问题比较难复现现场可能几小时出现一次。建议把相关日志打点提前比如在mtk-msdc.c的suspend/resume回调里加一段dump记录suspend时的clock状态和resume后的第一个命令结果。先让bug能稳定留下来再谈修复。5.4 兼容性问题的系统性排查“为什么他家的卡能用我这张就不能用”是驱动工程师最常被问的问题。SD卡虽然都符合SD规范但在实际实现上有些卡对tuning容忍度低有些卡的ACMD41响应时序偏慢有些卡对电源切换时间敏感。对付这类兼容性问题不要指望一个“万能补丁”能解决全部。系统性的做法是准备多张不同主控的卡逐一记录它们在当前驱动下的初始化日志和失败点然后对比差异找出共性。靠一两次试错来猜问题是效率最低的路径。另外可以关注一下内核版本里的quirk机制。很多SD卡兼容性问题在Linux内核社区已经有已知quirks比如某些卡需要在初始化后延时、某些卡不能进SDR104。这些quirk通常以卡名或CID片段匹配如果你正好遇到这种已知问题合入对应patch会比从头调试快得多。6. 几点个人经验与建议做SD卡驱动改动我的核心体会是“少改代码多改配置”。MTK平台把大量能力都暴露在dts里能用dts表达的能力尽量不要进到C代码里动函数逻辑。因为dts改动可以被不同项目的overlay覆盖或者继承代码改动则会影响所有使用该驱动的项目一旦回归出问题责任范围会大很多。每次改动最好保持“单点最小化”一个patch只解决一个问题不要顺手把其他参数也改了不然问题复现时根本分不清是谁造成的。第二个体会是验证要贴近真实用户场景。实验室里能通过的dd读写不代表用户日常使用没问题。我习惯在最终验证时放一批真实照片、视频、安装包在卡上模拟用户在文件管理器里连续拷贝、删除、打开的操作这样跑半天下来比跑十轮dd更能暴露问题。SD卡这类外设的兼容性测试样本量越大踩坑越少。第三个建议是文档记录。每次修改把改动原因、涉及文件、验证结果、基线数据都写清楚。特别要记录修改之前的状态——如果现场出了问题需要回退准确的回退点能帮你节省一天时间。我见过太多团队因为“不知道上一个版本是怎么调的”而推倒重来其实当初写一段commit message就能避免。最后别忽略硬件的影响。驱动层面能做的优化是有限的如果板子的PCB走线、卡座选型、串阻匹配有问题再多的软件参数也无法弥补。遇到反复调不通的情况该找硬件同事看原理图就看原理图该上示波器就上示波器。SD卡驱动只是整个系统的一个关节它转不转还得看周围的东西配不配合。