手头正好在调一块新板子的MMC子系统,从bootloader起来之后uImage加载完成,到rootfs挂在SD卡上,中间最让人心里没底的就是这一步:SD卡控制器到底有没有被正确初始化。运气好的时候一条dmesg就带过了,运气不好的时候连个中断都等不到。而这一切的起点,就是那个看起来低调但扛着整个存储栈核心职责的函数——dw_mci_probe。不论你是做嵌入式Linux方案、写板级驱动,还是单纯想把内核的MMC子系统吃透,这个函数都是绕不开的一关。这篇文章我就从源码实现、设备树配合、常见失败场景和排查手段几个维度,把整个过程掰开揉碎讲清楚。
1. dw_mci_probe 在MMC驱动体系里到底扮演什么角色
1.1 从硬件控制器到块设备之间的那个桥梁
先把视角拉高一点。当你往板子上插一张SD卡,系统里的软件链路大概是这样的:MMC控制器硬件位于SoC内部,它负责发送CMD、处理DATA线、产生中断,在此基础上Linux内核的MMC子系统分了三层——mmc core处理卡的状态机和协议细节,host controller driver负责驱动具体的控制器芯片,而最上层的块设备层则把卡抽象成/dev/mmcblk0这样的设备节点。
dw_mci_probe这个名字,对应的是DesignWare系列MMC/SDIO控制器在Linux内核中的probe入口。这一类控制器在手机主控、机顶盒、网络设备和工控板里出镜率极高,因为DesignWare的IP被大量SoC厂商买去集成。dw_mci_probe被调用的时机,是在设备驱动模型把设备树节点和驱动匹配上之后,内核自动进入探测流程。换句话说,这函数跑完且返回成功,存储控制器才算真正“活”了。
从功能上看,dw_mci_probe不是只做初始化寄存器那么单薄。它需要完成资源申请、时钟使能、中断注册、DMA能力探测、MMC host注册、请求队列初始化等一系列动作。任何一个环节失败,整个MMC栈都起不来,表现出来就是系统完全看不到SD卡,或者驱动加载报错、进程卡在dmesg的某个error后面。
1.2 驱动模型匹配之前的准备工作
在probe被执行之前,内核的设备驱动模型已经做了一轮匹配。设备树里的节点,比如sdmmc或者mmc,它包含了reg、interrupts、clocks、pinctrl这些描述性信息。驱动侧则通过of_match_table声明自己支持哪些兼容字符串。两者匹配成功后,内核分配一个struct platform_device,接着就会去调用驱动的probe回调,于是dw_mci_probe登场。
很多人刚开始接触这块的时候,以为probe就是“把驱动加载进去”,这种理解过于笼统。我习惯把probe看成一次“资源集齐仪式”:本驱动要正常工作,所有依赖的资源必须在这个阶段申请到,而且申请不到时不能直接报错退出,而是要返回-EPROBE_DEFER,告诉内核“先别急,我依赖的东西还没好,等会儿再叫我”。比如时钟控制器还没ready、电源域还没up,那这次probe就会被推迟重试。理解了这层机制,后面调试-EPROBE_DEFER相关的启动日志时就不会一头雾水。
也正是因为probe阶段承担了这么多职责,dmesg里开关驱动的日志顺序、.probe的实现质量,直接决定了后续SD卡枚举是顺利还是磕绊。接下来我们逐行看dw_mci_probe内部到底做了什么。
2. 源码视角下的dw_mci_probe核心流程
2.1 私有数据结构与devm资源管理
打开drivers/mmc/host/dw_mmc.c,第一眼看到的probe实现通常长这样:通过devm_kzalloc分配一个struct dw_mci作为私有数据结构,然后把这个结构挂在platform_set_drvdata上。这里的“devm”前缀是设备资源管理机制,它的好处是如果probe中途fail,已经申请的内存、时钟、GPIO、中断都会自动释放,不用每一步都手写错误处理goto标签。
到这一步有个特别容易踩的坑:struct dw_mci里包含了大量硬件相关成员,比如寄存器基地址、DMA描述符基址、fifo地址、当前请求列表、中断屏蔽状态等等。你如果只把这个私有结构当成一个“存储配置的地方”,后面写中断处理函数和完结请求的路径时就会特别被动。我见过有同事在probe里把flag设置得挺好,但忘了初始化cmd和data状态机的标志位,结果第一笔读写请求进来就直接死锁。
顺带说一句,dw_mci结构里还有个use_dma标志,它是probe期间通过读取设备树参数或DMA能力探测来决定的。这直接决定后续是走IDMAC方式还是PIO方式,选错的话要么数据搬运慢得离谱,要么DMA传输一直报错。
2.2 时钟与复位:probe成功的首要关卡
一个MMC控制器没有时钟,所有寄存器访问都会卡死,这是嵌入式开发里的常识,但实际操作中还是经常看到人们在probe失败后选择一头扎进中断代码里排查,最后发现只是某个时钟源没配对。
dw_mci_probe里通常会有类似devm_clk_get或clk_get来获取biu和ciu两个时钟。所谓biu是总线接口单元时钟,负责控制器内部寄存器和总线接口逻辑,ciu是卡接口单元时钟,负责产生MMC总线上的时钟信号。这两个时钟的父时钟源、频率是否匹配,直接决定了SD卡能跑多快、时序对不对。
我在一个基于ARM SoC的项目里就遇到过这种诡异现象:dmesg显示dwmmcprobe成功,但mmc卡识别不了,频率只能勉强跑在400kHz的初始识别速率下,一切到high speed就报数据超时。后来查清楚是ciu时钟被配置成了某个不可分频的固定源,驱动不管怎么设置mmc->f_max,实际总线频率都上不去。
另外,复位也是probe里的高频检查点。很多SoC的MMC控制器带rstc复位控制,probe阶段需要先解除复位,否则寄存器都处于复位状态,写什么都没反应。这类问题典型特征是dmesg里没有报错,但寄存器读出来全部是0或固定值。
2.3 寄存器映射与中断申请:把硬件资源握在手里
probe接下来做的事情,是把设备树里的reg映射成虚拟地址。Linux内核提供devm_platform_ioremap_resource,它会自动根据平台设备的资源索引完成request_mem_region和ioremap。映射之后,驱动就用dw_mci_reg之类的接口访问寄存器。
紧接着是中断申请,通常用的是devm_request_irq或平台相关的platform_get_irq。MMC控制器中断是整个驱动的心脏,所有命令完成、数据传输完成、错误标志、卡检测事件,全靠中断通知CPU。申请中断之后,probe阶段还会做中断屏蔽的初始配置,一般会把所有中断屏蔽掉,等卡初始化时再逐个打开。
这里有一个值得注意的细节:DesignWare控制器支持两种中断处理风格。一种是单一中断号,所有事件都在一个IRQ里处理;另一种是分开的cmd、data、card detect等独立中断。如果你的平台接了多个中断源,probe里没有分别申请,可能卡能初始化但事件响应丢一半。别问为什么设备偶尔能识别但读写就超时,往往是这里埋下的病根。
2.4 IDMAC控制器初始化与FIFO配置
对于支持DMA的DesignWare控制器,dw_mci_probe里还会涉及IDMAC的初始化。IDMAC是控制器内置的DMA引擎,驱动需要分配描述符表、把描述符地址写到控制器的DMAC寄存器里,并且在DMA传输过程中维护一个描述符链。
这一步如果配置不对,轻则数据错乱,重则控制器直接hang住。我在实际项目里看到过一个很典型的错误:为DMA描述符分配的缓冲区没有做cache一致性维护,CPU写好了描述符但DMA看到的是旧数据,导致每次传输位置都不对。后来在probe初始化里强制做了dma_map_single并妥善处理了dma_unmap,问题才消失。
同时FIFO深度也会在probe阶段被读取并记录。DesignWare控制器的FIFO深度和总线宽度寄存器能告诉你一次突发传输最多能搬多少数据,驱动要根据这个值设置合适的max_blk_count和max_seg_size。如果FIFO配置和实际硬件不符,大块数据读写时会导致FIFO溢出或欠载。
2.5 MMC host分配、注册与槽位初始化
资源准备妥当之后,probe的重头戏就是创建MMC主机。驱动会调用mmc_alloc_host分配一个struct mmc_host,设置好ops指针、最大频率、最小频率、总线宽度标志,然后再调用mmc_add_host把它注册到MMC核心框架里。
mmc_add_host注册完成那一刻,MMC核心会立刻开始卡检测流程。所以probe里注册host之前,必须确保控制器已经处于可以正常接收命令的状态。这就是为什么有些驱动会在mmc_add_host之前先调一个dw_mci_init_slot之类的函数,完成供电、卡检测GPIO注册、请求队列初始化。
请求队列的概念容易被忽略。MMC驱动并不仅仅是处理“发一个命令、收一个响应”这么简单,它要同时支持多个未完成的IO请求、命令排队、错误恢复重试等机制。dw_mci_init_slot里通常会注册mmc_request的提交接口,后续的读写请求都会进入到这个队列统一调度。probe阶段忘了初始化队列,后果就是每次提交request都crash,而且crash位置往往在很后面,让人完全摸不着头脑。
3. 设备树配置如何左右probe的大局
3.1 从of_match到与硬件频率的匹配逻辑
设备树节点是驱动probe的资源来源,但很多开发者把设备树理解成“写着玩儿的配置”,这是完全错误的。一个典型的MMC节点至少包含compatible、reg、interrupts、clocks、clock-names,还可能携带bus-width、cap-mmc-highspeed、cap-sd-highspeed、sd-uhs-sdr104、non-removable、cd-gpios等属性。
其中compatible要和驱动的of_match_table严格对应。如果你把snps,dwmmc写成了snps,dw-mmc,哪怕只差一个小横线,驱动都匹配不上,probe压根不会执行。我在帮同事review设备树patch时就见过好几次这类低级但极端难查的问题。
clocks和clock-names则与probe里的clk_get一一绑定。设备树里通常给biu和ciu各分配一个时钟源,并用clock-names标识。如果设备树少了ciu时钟定义,probe里的clk_get就可能拿不到时钟,从而选择跳过部分时钟配置,系统照样能跑但性能一塌糊涂。
3.2 卡检测机制:轮询、映射GPIO和SDIO带内中断的取舍
卡检测是MMC驱动里最容易被设备树配置带偏的环节。DesignWare控制器本身有一个CD引脚用来检测卡是否插入,但很多SoC没有把这一物理信号直接连接到控制器,而是通过一个GPIO来检测。
设备树中的cd-gpios属性告诉驱动使用GPIO来做卡检测,而broken-cd则告诉驱动“别依赖硬件检测,老老实实轮询”。这两种方式各有利弊。GPIO检测响应快,但占用一个引脚,而且需要去抖处理;轮询机制简单稳妥,但延迟高,插入一张卡可能要等几百毫秒甚至更久才被系统发现。
还有一种情况是SDIO设备,它支持带内中断,卡自己通过DAT1线拉低来表示中断事件。这种情况下设备树需要设置cap-sdio-irq属性。SDIO的中断处理和普通卡检测完全是两套路径,probe阶段如果没把相关flag正确设置,SDIO设备可能出现中断风暴或者完全收不到中断。
3.3 总线宽度、时序参数与高速模式的设备树表达
bus-width属性决定了驱动将配置几条数据线。常见的值是1、4、8。如果你硬件上只连了4根数据线,但设备树写了bus-width = <8>,控制器在初始化时可能尝试8位模式,而设备不支持,最终降级到1位模式,速度大打折扣。反过来,硬件支持8位但你只写4位,那就会白白浪费一倍的吞吐能力。
高速模式参数则包括max-frequency、cap-mmc-hw-reset等。尤其max-frequency直接影响probe初始化时mmc->f_max的设定。如果这个值被设置成远高于物理时钟源能力,实际的时钟分频后达不到目标值,SD卡协商到某种高速模式后反而会因为时序偏移导致数据错误。这种情况调试起来特别恶心,因为dmesg看起来一切正常,卡也识别了,但读写频繁超时。
所以在调整设备树时,别先急着把max-frequency调大。先确认SoC的时钟树能不能输出你想要的频率,再看信号完整性是否扛得住。我在高速模式下踩过不少坑,最后发现老老实实把频率从200MHz降到150MHz,稳定性立刻上来了。
4. 从probe出发的调试实例与避坑心得
4.1 现象一:probe返回EPROBE_DEFER卡在启动流程里
在一个模拟项目X的开发板上,A同学发现自己写的驱动dmesg里出现dwmmc相关的-EPROBE_DEFER信息,系统启动流程在SD卡挂载这一步反复重试,最后超时进入救援模式。
第一步不要慌,先看/sys/kernel/debug/devices_deferred或者直接在dmesg里搜deferred,就能看到是哪个设备在等哪个资源。大多数情况下是clk或power-domain还没ready。A同学排查后确认,是某个PMIC的时钟控制驱动加载太晚,MMC驱动抢先probe,于是依赖的时钟源还没注册。解决办法是给MMC驱动的probe加上合理的-EPROBE_DEFER返回机制,同时检查各驱动的加载顺序依赖。
另外,如果你的内核配置了CONFIG_DEBUG_FS,可以通过cat /sys/kernel/debug/devices_deferred查看等待列表。列表里会明确写出等待的资源和设备名,比对着dmesg猜要高效太多。
4.2 现象二:probe成功但卡无法识别,控制器寄存器全为0
还有一次,B同学反馈自己板子上dw_mci_probe打印成功,MMC host也注册了,但就是检测不到任何卡。后来发现是ciu时钟虽然拿到了,但probe里没有先clk_prepare_enable。时钟没使能,控制器内部逻辑根本没有运转,寄存器读出来自然是全0。
这个案例的教训是:clk_get成功只能说明“时钟树里存在这个时钟”,并不代表时钟已经使能。很多新手拿到一个时钟句柄就直接用,结果所有寄存器操作都静默失败。probe里必须确保调用clk_prepare_enable,并且驱动卸载时对应clk_disable_unprepare。
如果你用的是devm_clk_get,它只会管理时钟句柄的自动释放,不会替你enable。这个细节真得刻在脑子里。
4.3 现象三:SDIO设备中断不触发或异常频繁
在一个采用SDIO接口的WiFi模组项目中,C同学发现WiFi驱动能加载,但网络吞吐量极低,而且/proc/interrupts里MMC的中断计数疯涨。排查发现是SDIO带内中断的触发条件配置错了,控制器把DAT1线的每个边沿都当成中断事件,而实际的SDIO协议要求的是电平中断。
进入dw_mci中断处理函数一看,发现中断状态寄存器里不断出现SDIO_INT标志,但上层SDIO函数没有任何实际请求。最后通过设备树增加适当的属性并调整控制器寄存器配置,把SDIO中断模式改成正确的电平触发,问题解决。这个案例提醒我们,SDIO中断不是普通的GPIO中断,它依赖控制器特定的采样窗口和去抖电路,probe阶段更要核对控制器版本对应的参考手册。
4.4 避坑清单:几个流传甚广的误解
网上常有人说“dw_mci_probe修改起来很危险,尽量只改设备树”,这个说法不完全对。设备树能解决的是资源描述和参数选择的问题,无法解决驱动源代码里的逻辑bug。比如控制器FIFO深度在某个芯片版本上和默认值不同,光靠设备树是改不透的,必须在驱动或平台数据里处理。
还有人说“MMC驱动只要probe成功就可以不管了”,这也是大错特错。probe成功只是拿到了入场券,真正的难点在中断处理、DMA描述符回收、错误恢复和电源管理。很多时候probe期间看不出问题,运行几天后偶发一次CRC错误,才翻回去检查DMA缓冲区的对齐方式,发现是某处代码写了kmalloc而不是dma_alloc_coherent。
我再分享一个具体的优化建议:如果你的平台SD卡读写性能明显偏低,可以检查probe阶段是否正确设置了mmc->caps中的MMC_CAP_SD_HIGHSPEED或MMC_CAP_UHS_SDR104。如果只是默认的普通模式,无论硬件多好,读写速度都会限制在一个很低的水准。同时配合设备树里的max-frequency,把控制器的实际工作频率提上去,才谈得上性能调优。
4.5 中断与DMA协作的独门调试技巧
在调试DMA和中断配合时,我最推荐的做法是先在probe里临时禁用DMA,改用PIO模式跑一遍基本读写。如果PIO模式完全正常、切换DMA后问题复现,那十有八九问题出在DMA描述符管理或cache一致性上,而不是控制器本身。这个方法虽然粗暴,但能快速缩小排查范围,比对着寄存器反复猜高效得多。
实际操作中还可以在中断处理函数的入口打印INT_STATUS和STATUS寄存器的原始值。DesignWare控制器的状态寄存器里包含FIFO空满、数据线忙、命令忙等关键信息,一眼望去就能分辨是卡侧问题还是控制器侧问题。很多开发者只盯着一个错误标志debug,最后绕了远路。
用tracepoint或trace_mmc_request_done这类内核事件钩子,也能够在不影响实时性的前提下记录请求生命周期。我建议在probe注册host之后先跑一轮mmc_test测试模块,把各种尺寸的读写请求都过一遍,确保数据完整性没问题再继续开发上层业务。
5. 从probe到稳定存储的最后一公里
探针函数只是万里长征第一步。我自己做过的几个项目里,最深的体会是:一个稳定好用的MMC驱动并不取决于probe写得多么花哨,而是取决于probe阶段有没有把该做的硬件能力探测做扎实,以及后续的错误恢复路径是否完备。很多产品在实验室里跑得好好的,一到现场就偶发SD卡掉线,最后查来查去,问题恰恰出在probe阶段没有正确设置卡检测的中断触发方式,导致热插拔事件被漏掉。
还有一件值得做的小事:在probe成功之后,立即打印一份“能力摘要”日志,把控制器版本、FIFO深度、时钟频率、DMA模式、总线宽度这些关键信息全部记录下来。这样后续无论是做性能调优还是排查现场问题,都能有一个可靠的基础信息参考。
如果你手头也正在调dw_mci_probe相关的驱动,我特别推荐先仔细读一遍drivers/mmc/host/dw_mmc.c里的probe实现,再对照自己板子的原理图,把每个资源都梳理清楚。这个过程虽然枯燥,但绝对值得。等哪一天SD卡在极端温度、频繁热插拔、高吞吐压力下依然稳定工作,你就能真正体会到这个函数背后承载的分量。