1. 这不是“驱动代码阅读课”,而是一场嵌入式Linux底层通信的解剖实验
你打开内核源码树,cd到drivers/mmc/目录下,看到core/、host/、card/三个子目录,第一反应可能是:“哦,又是分层设计”。但真正动手改过SD卡热插拔逻辑、调过eMMC启动失败、抓过SDIO WiFi模块初始化时序的人会立刻意识到:这里根本不是教科书里干净利落的“协议栈分层”,而是一张由状态机、中断风暴、时钟抖动、电压切换和硬件怪癖共同编织的网。我第一次在某款国产SoC上调试SDIO蓝牙模块时,卡在CMD53数据传输超时整整三天——不是因为代码写错了,而是因为host控制器的DMA描述符缓存一致性没处理好,而这个细节在Documentation/mmc/文档里只用一行带过。这篇分析不讲“MMC协议标准第几章第几条”,也不堆砌函数调用图;它还原的是一个真实嵌入式工程师蹲在示波器前,一边看CLK线波形一边翻寄存器手册、一边改dts节点一边查内核日志的全过程。核心关键词就三个:SD/MMC协议栈、host控制器驱动、card设备驱动——它们不是并列关系,而是三层咬合的齿轮:最底层是SoC厂商提供的host controller IP(比如Synopsys DesignWare、Cadence HS400、三星Exynos SDHCI),中间是内核通用MMC core(drivers/mmc/core/),最上层才是具体卡设备的行为实现(如sd.c、mmc.c、sdio.c)。你调不通一张卡,问题可能在任意一层,但90%的失败案例,根源都藏在host与core的接口胶水层里。这篇文章适合两类人:一类是刚接手BSP开发、面对客户反馈“SD卡偶尔识别失败”的工程师,需要快速建立排查路径;另一类是想深入理解Linux设备模型如何承载复杂时序协议的内核学习者。它不承诺让你“三天掌握MMC驱动”,但能确保你下次看到mmcblk0: error -110时,不再盲目重启,而是知道该先看哪行dmesg、该改哪个clk_phase参数、该怀疑哪段DMA映射逻辑。
2. 协议栈的三重门:从物理信号到块设备的逐层通关
2.1 第一重门:物理层信号与Host Controller的硬约束
SD/MMC协议栈的起点从来不是代码,而是PCB走线和SoC数据手册。当你把一张SD卡插入插槽,最先被激活的不是内核线程,而是SoC内部的host controller IP核。这个IP核不是万能的,它有明确的物理能力边界,这些边界直接决定了上层软件能做什么、不能做什么。以最常见的SDHCI(Secure Digital Host Controller Interface)为例,其寄存器组里藏着几个决定性的“硬开关”:
Clock Divider vs. Programmable Clock:老式SDHCI控制器(如v1.x)只能通过整数分频器生成时钟,这意味着你无法精确输出400kHz的卡初始化时钟(需要分频值=主频/400k,结果必须是整数)。而新控制器(v3.0+)支持可编程时钟,允许小数分频,这对UHS-I模式下的100MHz时钟精度至关重要。我曾在一个基于旧版SDHCI的项目中,因分频误差导致卡在初始化阶段反复发送ACMD41,最终发现是clock divider寄存器配置值计算错误——主频200MHz,要得到400kHz,理论分频值应为500,但寄存器实际只接受16位值,最大511,而500在范围内,问题出在代码里误用了无符号除法导致溢出。
Voltage Switching Support:SD卡支持1.8V和3.3V两种I/O电压,切换过程不是简单写个寄存器就能完成。它要求host controller必须具备独立的1.8V电源域,并在切换前完成严格的时序:先拉低CMD线,等待至少5ms,再切换电源,再等待至少1ms,最后释放CMD。这个时序如果由软件纯延时实现,在高负载系统中极易失败。真正可靠的方案是host driver必须调用
mmc_regulator_set_vqmmc()并确认regulator框架返回成功,而非自己写udelay()。某次调试中,我们跳过了regulator检查,直接操作GPIO模拟电源切换,结果在高温环境下批量出现卡识别失败——因为GPIO切换速度远快于真实电源稳定时间。Data Timeout Counter:这是最容易被忽视的“隐形杀手”。SD卡在数据传输时,如果host未在规定时间内收到响应,会触发timeout中断。这个timeout值不是固定常量,而是由卡的CSD寄存器中
TAAC(Time to Access Card)字段决定,单位是时钟周期。内核在卡识别阶段读取CSD后,会将TAAC值乘以当前时钟频率,再转换为jiffies填入host控制器的timeout寄存器。但如果host driver在初始化时错误地将timeout寄存器清零(某些厂商SDK模板代码有此bug),则所有数据传输都会立即超时。我们曾在一个客户现场抓取到连续的mmc0: cmd timeout日志,最终定位到host driver的probe函数里有一行多余的writel(0, host->base + SDHCI_TIMEOUT_CONTROL)。
提示:不要迷信dts里的
max-frequency属性。它只是告诉core“这个host理论上能跑多快”,但实际能否达到,取决于上述物理约束是否全部满足。很多“降频后工作正常”的问题,本质是规避了某个未被满足的硬约束。
2.2 第二重门:MMC Core的抽象与妥协——当通用性撞上硬件特异性
内核的drivers/mmc/core/目录是整个协议栈的中枢神经,但它并非完美抽象。它的设计哲学是“为大多数host提供统一接口”,代价是对少数硬件特性的支持变得笨重甚至别扭。理解它的行为逻辑,关键在于抓住三个核心对象及其交互:
struct mmc_host:这是host controller在内核中的化身。它不包含任何硬件操作函数,而是通过
.ops指针指向一个struct mmc_host_ops结构体,里面全是回调函数:.get_cd()检测卡存在、.set_ios()配置电压/时钟/总线宽度、.request()提交命令请求。这个设计看似优雅,实则埋下隐患——不同厂商的host ops实现差异巨大。例如,.set_ios()在Synopsys DW_MMC中需处理复杂的时钟相位(clk_phase)和信号采样点(sample_clk)配置,而在TI OMAP的SDHCI中,这些参数可能被固化在IP核里不可修改。当你的板子换用新SoC时,最可能出问题的就是这个.ops的适配层。struct mmc_request (mrq):这是命令请求的载体。一个典型的mrq包含一个主命令(如CMD17读单块)和一个可选的数据传输(data)。但SD协议要求某些命令必须成对出现,比如CMD53(SDIO块读写)必须紧跟一个CMD52(SDIO功能选择)。Core层负责将这些逻辑组合成mrq,但host driver必须保证在
.request()回调中,严格按顺序向硬件发送命令,并在收到响应后正确解析状态。我们曾遇到SDIO WiFi模块在高吞吐时丢包,最终发现是host driver在处理CMD52响应时,错误地将R1响应的READY_FOR_DATA位当作数据就绪信号,而实际上该位在此上下文中无意义,真正标志数据就绪的是CMD53的R1响应。State Machine与Workqueue:Core层用一个有限状态机管理卡的生命周期(detect -> init -> ready -> suspend)。这个状态机运行在workqueue上,而非中断上下文,目的是避免阻塞。但这也带来问题:当卡热插拔事件发生时,
mmc_detect_work被调度,它会调用mmc_rescan()重新枚举。如果此时上层应用(如udev)正在读写/dev/mmcblk0,mmc_rescan()会尝试mmc_power_off(),而该函数会等待所有pending request完成。如果某个request因硬件故障卡死,整个rescan就会无限等待,导致系统假死。解决方案不是禁用热插拔,而是host driver必须在.request()中实现超时机制,并在超时时主动调用mmc_request_done()通知core放弃该请求。
注意:
mmc_host的f_max字段常被误认为是性能瓶颈。其实它只是限制core层不会向host提交超过此频率的请求。真正的瓶颈往往在host driver的.request()实现效率上——比如是否使用了高效的DMA描述符链,是否避免了不必要的寄存器轮询。
2.3 第三重门:Card Driver的“人格化”——为什么同一张卡在不同板子上表现迥异
到了drivers/mmc/card/层,代码开始展现“人格化”特征。sd.c、mmc.c、sdio.c不是简单的协议翻译器,而是针对不同类型存储卡和外设的“行为建模师”。它们的差异,直接解释了为何同一张Sandisk UHS-I卡,在A板上能跑满90MB/s,在B板上却卡在20MB/s:
SD Card的“双模”困境:一张SDHC卡支持两种工作模式:默认的SD模式(最高25MB/s)和高速的UHS-I模式(最高104MB/s)。进入UHS-I模式需要一系列握手:先发CMD6查询功能,再发ACMD6设置总线宽度为4-bit,最后发ACMD41指定UHS-I电压。但这个过程充满陷阱。
sd.c中的mmc_sd_init_uhs_card()函数会尝试所有UHS-I模式(SDR12/SDR25/SDR50/DDR50),并记录每种模式下的实际性能。然而,如果host controller的信号完整性不佳(如PCB走线长度不匹配),SDR50模式下CLK和DAT线的时序偏移会导致大量CRC错误,此时core会自动降级回SDR25。但降级逻辑在旧版内核中存在bug:它只检查一次CRC错误率,如果错误率在阈值边缘波动,就会陷入“升级-失败-降级-再升级”的死循环,表现为dmesg里刷屏的mmc0: new high speed SDHC card at address 0001。修复方法是在host driver的.execute_tuning()回调中,强制执行更严格的信号质量测试。eMMC的“启动分区”迷雾:eMMC设备有特殊的启动分区(Boot Partition),用于存放bootloader。访问它需要发送特定的EXT_CSD寄存器写入命令(CMD6)。
mmc.c中的mmc_boot_partition_access()函数封装了这一逻辑,但它依赖host controller支持“boot mode”。某些低成本SoC的SDHCI IP核阉割了boot mode支持,导致即使EXT_CSD配置正确,也无法从boot partition启动。此时dmesg只会显示mmc0: new MMC card at address 0001,却没有任何boot partition信息。诊断方法是直接读取EXT_CSD的BOOT_BUS_WIDTH字段(地址177),如果返回0xFF,说明host未启用boot mode。SDIO的“中断风暴”治理:SDIO设备(如WiFi、蓝牙)通过CMD52/CMD53与host通信,但真正的实时性保障靠的是中断。
sdio.c中的sdio_irq_thread()是一个高优先级内核线程,专门处理SDIO中断。然而,如果WiFi模块固件有bug,持续拉低中断线,这个线程就会100%占用一个CPU核心。top命令会显示ksoftirqd/0或sdio_irq进程占满CPU。这不是内核bug,而是SDIO规范允许的“中断共享”特性被滥用。解决方案是host driver必须在.enable_sdio_irq()回调中,实现硬件级的中断防抖(debounce),例如配置寄存器使中断在信号稳定10us后再触发。
3. Host Controller驱动的“七寸”:寄存器操作背后的魔鬼细节
3.1 寄存器映射的“表象与真相”
所有host controller驱动的起点,都是ioremap()将物理地址映射为虚拟地址。但新手常犯的致命错误是:把寄存器手册里的“Offset”直接当成ioremap()后的偏移量。这是大忌。以ARM平台为例,SoC的SDHCI控制器物理地址可能是0x12300000,而寄存器手册写的“Base Address”是0x0000,这并不意味着readl(host->base + 0x0000)就读到了SDHCI_VENDOR_SPECIFIC_REGISTER。真相是:host->base是ioremap()返回的虚拟地址,而寄存器手册的Offset是相对于该物理基址的偏移。所以正确的读写是readl(host->base + 0x0028)(假设Vendor Register Offset是0x28)。但更深层的陷阱在于字节序(Endianness)。某些SoC(如部分MIPS架构)的SDHCI控制器寄存器是Big-Endian,而CPU是Little-Endian。此时readl()读出的32位值,其字节顺序与寄存器手册图示相反。我曾在一个MIPS项目中,因忽略此点,将SDHCI_HOST_CONTROL寄存器的LED Control位(bit 0)误置为bit 31,导致SD卡指示灯行为诡异。解决方案是查阅SoC TRM(Technical Reference Manual),确认寄存器端序,并在driver中使用readl_be()或手动字节交换。
3.2 中断处理的“原子性”幻觉
request_irq()注册中断处理函数是标配,但mmc_host的中断处理远比普通设备复杂。SD/MMC协议要求在命令传输过程中,host必须能同时响应多种中断源:命令完成(CMD_DONE)、数据传输完成(DATA_END)、超时(TIMEOUT)、卡插入/弹出(CARD_INSERTION/REMOVAL)。这些中断可能在同一毫秒内密集到来。许多host driver采用“单一中断号+状态寄存器轮询”的模式,即在irq_handler中读取SDHCI_INT_STATUS寄存器,然后根据置位的bit分别处理。这看似高效,却隐藏着竞态风险:如果两个中断几乎同时发生,第一个中断处理完后清除了INT_STATUS,第二个中断的status bit可能已被覆盖丢失。真正健壮的做法是:在irq_handler中只做最轻量的工作——记录中断发生,然后唤醒一个tasklet或workqueue进行后续处理。dw_mmc.c驱动就是典范,它的dw_mci_irq()只做tasklet_schedule(&host->tasklet),所有繁重的命令解析、数据搬运都在dw_mci_tasklet_func()中完成。这样既保证了中断响应的及时性,又避免了在中断上下文中做耗时操作(如DMA映射、内存拷贝)引发的系统不稳定。
3.3 DMA与PIO的“生死抉择”
现代host controller几乎都支持DMA传输,但并非所有场景都适用。mmc_host的caps字段有MMC_CAP_DMA标志,但这只是“能力声明”,是否启用DMA由core层根据请求大小和host状态动态决定。关键决策点在mmc_start_req()函数中:如果请求数据长度大于host->max_segs * host->max_seg_size,core会强制使用DMA;否则可能回退到PIO(Programmed I/O)。PIO模式下,CPU需要逐字节读写SDHCI_BUFFER寄存器,这在高吞吐时会严重拖慢系统。但DMA也有坑:DMA缓冲区必须是物理连续的,且地址需满足host controller的对齐要求(通常是4字节或128字节)。dma_alloc_coherent()分配的内存满足连续性,但若host driver未正确设置dma_mask(如dma_set_mask(host->dev, DMA_BIT_MASK(32))),在64位系统上可能分配到高位物理地址,导致DMA传输失败。我们曾在一个ARM64项目中,因忘记设置dma_mask,导致eMMC写入大文件时随机出现DMA transfer timeout,最终发现DMA地址落在了host controller无法寻址的区域。
提示:调试DMA问题的黄金组合是
cat /proc/dma确认DMA通道分配,dmesg | grep -i dma查看DMA初始化日志,以及用perf record -e 'dma:*'捕获DMA事件。不要只盯着mmc日志。
4. 实战排错:从“卡不识别”到“性能归零”的全链路追踪
4.1 现象:系统启动时“mmc0: no card detected”,但物理卡确已插入
这是最基础也最棘手的问题。排查必须遵循“从外到内”原则:
物理层验证:用万用表测量SD卡插槽的CD#(Card Detect)引脚电压。标准设计是:卡插入时CD#接地(0V),弹出时通过上拉电阻为高电平(3.3V)。如果CD#始终为高,说明机械开关损坏或PCB短路;如果始终为低,说明开关粘连或焊接短路。我们曾遇到一个案例,CD#引脚在PCB上与GND铺铜意外连通,导致系统永远认为“卡已插入”,但实际无法通信。
Host Driver初始化日志:
dmesg | grep -i "mmc\|sdhci"。关键线索是:sdhci: Secure Digital Host Controller Interface driver:表明host driver已加载。mmc0: SDHCI controller on xxx [mem 0x...@0x...]:表明platform_device已匹配,probe()函数执行。- 如果没有第二行,问题在设备树(dts):检查
&sdhci0节点是否status = "okay",clocks、clock-names、vmmc-supply等必需属性是否齐全。特别注意vmmc-supply,它指向一个regulator,如果该regulator未在系统中定义或enable失败,probe()会直接返回错误,且日志可能只显示Failed to get vmmc regulator。
CD检测逻辑验证:如果host driver已加载,但日志显示
mmc0: no card detected,需检查.get_cd()回调。有些host driver(如sdhci-of-at91.c)会读取一个GPIO作为CD信号,而该GPIO在dts中可能被错误配置为input-pull-up,导致即使卡插入,GPIO仍读为高。解决方案是在dts中为CD GPIO添加bias-pull-down属性。
4.2 现象:卡能识别,但dd if=/dev/zero of=/dev/mmcblk0 bs=1M count=100写入速度仅5MB/s,远低于标称
性能瓶颈排查是系统工程,需逐层排除:
| 排查层级 | 关键命令/工具 | 正常现象 | 异常原因 |
|---|---|---|---|
| Host Controller能力 | cat /sys/kernel/debug/mmc0/ios | clock: 50000000 Hz(50MHz) | clock: 0 Hz表明.set_ios()未正确设置时钟 |
| 总线宽度 | cat /sys/kernel/debug/mmc0/ios | bus_width: 4 | bus_width: 1表明未启用4-bit模式,检查dts中bus-width = <4>及host driver是否支持 |
| UHS-I模式 | `dmesg | grep -i "uhs|sdr"` | mmc0: new high speed SDHC card at address 0001+UHS-I SDR25 |
| DMA效率 | cat /proc/interrupts | grep mmc | mmc0中断计数随IO增长 | 中断计数恒定,说明DMA未启用或失效,检查host->dma_ops是否为NULL |
最隐蔽的性能杀手是时钟相位(Clock Phase)不匹配。SD卡数据在CLK上升沿采样,但host controller的采样点可能因PCB走线延迟而偏移。dw_mmc驱动通过clk_phase参数微调采样点。我们曾在一个项目中,将clk_phase从默认的0改为180,写入速度从12MB/s跃升至85MB/s。调整方法是在dts中为&dwmmc0节点添加rockchip,clk-phase = <180>,然后重新编译内核。
4.3 现象:dmesg持续刷屏mmc0: cmd timeout,系统完全无法使用SD卡
这是典型的“软硬件协同失败”。cmd timeout意味着host controller在规定时间内未收到卡的响应。根因必在host driver或硬件:
第一步:确认超时值是否合理。在host driver的
.request()函数中,找到设置timeout寄存器的代码。例如SDHCI中,writel(timeout, host->base + SDHCI_TIMEOUT_CONTROL)。timeout值应为CSD[TAAC] * current_clock_freq。打印current_clock_freq和CSD[TAAC],计算理论timeout,再对比寄存器实际写入值。如果相差巨大,说明CSD读取错误或时钟频率获取错误。第二步:抓取物理信号。用示波器观察CMD线。正常CMD超时前,应看到主机发出CMD命令(如CMD17),然后等待约100ms,卡才响应。如果CMD线上只有主机发出的脉冲,没有卡的响应脉冲,问题在卡供电或硬件连接;如果能看到卡的响应脉冲,但host未捕获,问题在host的CMD接收电路或驱动中断配置。
第三步:检查中断屏蔽。
SDHCI_INT_ENABLE寄存器控制哪些中断被使能。如果CMD_DONE位未置1,则即使卡响应了,host也不会触发中断,最终超时。在probe()函数中,确保writel(SDHCI_INT_CMD_COMPLETE | ..., host->base + SDHCI_INT_ENABLE)被执行。
经验:90%的
cmd timeout问题,根源在host driver的.set_ios()实现。它必须在改变时钟频率后,等待足够长的稳定时间(通常udelay(1000)),再发送命令。跳过此延时,是新手最常犯的错误。
5. 工具链与调试技巧:让内核成为你的“透明实验室”
5.1 内核调试接口的深度挖掘
Linux内核为MMC子系统提供了丰富的debugfs接口,它们是无需重新编译内核就能获取底层信息的利器:
/sys/kernel/debug/mmc0/:这是主入口。其中:ios:实时显示当前I/O状态(时钟、电压、总线宽度、功率模式)。status:显示卡的当前状态寄存器(OCR、CID、CSD、SCR等),相当于在线读取卡的“身份证”。host_caps:列出host controller支持的所有能力(MMC_CAP_4_BIT_DATA,MMC_CAP_UHS等),验证dts配置是否生效。card_caps:列出已识别卡的实际能力,与host_caps对比,可知是否启用了所有可用特性。
/sys/kernel/debug/mmc0/regs:直接dump host controller的所有寄存器值。这是诊断硬件问题的终极手段。例如,当怀疑CMD线故障时,读取SDHCI_COMMAND寄存器,看主机是否真的发出了命令;读取SDHCI_INT_STATUS,看是否有CMD_TIMEOUT位被置位。/sys/kernel/debug/mmc0/clk:显示时钟树信息。cat clk会输出类似mmc_clk: 50000000 Hz, parent: pll_a,确认时钟源是否正确,频率是否达标。
技巧:将debugfs挂载为
/debug后,可以用watch -n 1 'cat /debug/mmc0/ios'实时监控I/O状态变化,比翻dmesg日志直观百倍。
5.2 动态跟踪:ftrace在MMC世界里的魔法
当静态日志无法定位问题时,ftrace是打开内核黑盒的钥匙。针对MMC,最有效的跟踪点是:
mmc_cmdq_complete: 跟踪命令队列完成事件。mmc_request_start: 跟踪每个请求的开始。mmc_request_done: 跟踪每个请求的结束。
启用方法:
echo 1 > /sys/kernel/debug/tracing/events/mmc/mmc_request_start/enable echo 1 > /sys/kernel/debug/tracing/events/mmc/mmc_request_done/enable echo function_graph > /sys/kernel/debug/tracing/current_tracer echo 1 > /sys/kernel/debug/tracing/tracing_on # 执行你的测试命令,如 dd ... echo 0 > /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace_pipe输出会清晰显示每个mmc_request_start和mmc_request_done的调用栈及耗时。如果发现mmc_request_done耗时异常长(>100ms),说明问题在host driver的.request()实现中;如果mmc_request_start和mmc_request_done之间间隔巨大,说明问题在卡本身或物理层。
5.3 设备树(DTS)的“魔鬼在细节”
DTS是连接硬件与驱动的桥梁,一个字符的错误就能让整个MMC子系统瘫痪。常见陷阱:
vmmc-supply与vqmmc-supply混淆:vmmc-supply为卡提供I/O电源(3.3V或1.8V),vqmmc-supply为host controller的I/O电源(通常也是3.3V)。两者必须指向正确的regulator节点。如果vmmc-supply指向了vqmmc,卡将得不到正确电压,导致识别失败。non-removable属性的误用:对于焊死的eMMC,应设置non-removable;,这会禁用CD检测,强制core进行初始化。但如果错误地加在SD卡插槽节点上,系统将永远无法响应热插拔。cap-sd-highspeed与cap-mmc-highspeed:这两个属性告诉core“host支持高速模式”,但它们不等于“卡会进入高速模式”。卡是否进入高速,取决于卡自身的CSD寄存器和ACMD41协商结果。因此,即使dts中未设置这些属性,只要卡和host都支持,协商仍会发生。
最后一个实战心得:当一切看起来都正确,但问题依旧存在时,拔掉SD卡,用示波器测量插槽的VCC引脚电压。我们曾在一个项目中,发现VCC电压因电源芯片负载能力不足,在卡初始化瞬间跌落到2.8V,低于SD卡最低工作电压3.0V,导致初始化失败。这个电压跌落,任何软件日志都不会体现。