☰
Linux MMC 驱动深入解析:从卡识别到 HS400 的完整调试指南
2026/10/21 10:07:30 网站建设 项目流程

这一篇是整个 Linux MMC 驱动系列里我拖得最久的一篇,也是我实际项目里踩坑最多的一段。前五篇我们把 host 控制器注册、MMC core 框架、SD 卡初始化链路、块设备层对接这些内容基本过了一遍,按道理说框架已经清楚了。但你会发现,真正到了嵌入式板子上调 eMMC 或者 MMC 类型卡的时候,情况完全不是“照着代码读一遍”就能搞定的。这也是我为什么要在第六篇专门把 MMC type card 这条分支单独拉出来讲透——从卡检测、类型识别、ext_csd 解析,到 block 设备挂载,再到 HS200/HS400 的切换和调试排障,整条链路里面藏着大量“文档上不会写、但代码里就是如此”的细节。

这篇适合谁看?已经在看 MMC 驱动源码、想把 eMMC 初始化流程彻底搞明白的驱动开发者,或者正在调板子、发现卡识别不稳定、速度上不去、老是在某个状态机卡住的人。说句实话,纯读内核文档是学不会 MMC 驱动的,真正能让你少熬夜的,是那些在代码里翻来覆去比对后得到的经验值。这篇就把我积累的这些值都放出来。

1. MMC type card 在 Linux 驱动栈中的位置

1.1 三层四层,type card 到底属于哪一层

很多刚开始读 MMC 驱动的人会被层数搞晕。旧内核里习惯叫“三层”:host 层、core 层、card 层。后来 4.x 之后内核把 card 层的块设备逻辑抽到了 block 层,实际变成四层:host 控制器层、core 通用层、card 类型层、block 层。但 MMC type card 这个概念,不太等于某一层,而是一条横跨 core 和 block 的完整数据路径。

具体说,当你插入一张 MMC 卡(注意不只是传统的 MMC 卡,还包括绝大多数 eMMC 板载存储),内核首先由 host 层触发检测,进入 core 层的 mmc_rescan,然后在 core 里根据卡的协议特征区分出是 MMC_TYPE_MMC、MMC_TYPE_SD 还是 MMC_TYPE_SDIO。type card 这个词,强调的就是 MMC 类型(区别于 SD 类型)的卡。它在 core 层对应的是 mmc_attach_mmc 这个分支,在 block 层对应的是 mmc_blk_probe 这个驱动绑定逻辑。

所以你在代码里找 MMC type card 的入口,第一站就是 drivers/mmc/core/mmc.c 里的 mmc_attach_mmc,第二站就是 drivers/mmc/core/block.c。而真正决定卡会不会被识别成 MMC 类型的,是它响应命令时的协议特征。

1.2 几种卡类型的区分,别把 SD 当 MMC

在实际项目中,最容易被忽略的就是“明明插的是 SD 卡,却走进了 SD 分支;插的是 eMMC,却没进 MMC 分支”。这背后其实是协议层的差异决定的。我把典型的类型区分整理一下:

卡类型内核分支入口典型场景识别关键点
MMC_TYPE_MMCmmc_attach_mmceMMC 板载存储、传统 MMC 卡响应 CMD1 时 OCR 状态、无法通过 CMD8/SD 识别流程
MMC_TYPE_SDmmc_attach_sdSD、microSD 卡CMD8 能拿到有效的 R7 响应
MMC_TYPE_SDIOmmc_attach_sdioWiFi/BT 组合模块CMD5 能得到 SDIO OCR 响应
MMC_TYPE_HOSTmmc_attach_busloopback 自测、虚拟卡host 自己注册时指定

SD 卡在协议上兼容了部分 MMC 命令,但 SD 卡会先响应 CMD8,然后再走 ACMD41 这个流程。MMC 类型卡不一样,它不认识 CMD8 或对 CMD8 响应方式不同,内核只能回到 CMD1 去拉 OCR、看电压匹配。内核代码里这个“退回”逻辑也非常有意思,它不会一上来就断定你是 MMC,而是依次尝试 SD、SDIO、MMC 三个分支。这个顺序本身就是卡识别过程里最值得注意的细节。

1.3 为什么单独把 type card 拉出来讲

我的实际体会是,MMC 类型卡和 SD 卡的分水岭不仅仅是协议上的,更是工程调试上的。SD 卡插拔频繁,要考虑卡检测中断、写保护开关、热插拔。MMC 类型卡特别是 eMMC,不插拔、固定焊在板子上,要考虑的是电源上电时序、启动时 boot partition 的处理、HS200/HS400 的高速切换、以及系统休眠唤醒时的状态保持。

这些差异直接决定你在调试时看什么文件、查什么寄存器、加什么打印。如果你拿 SD 卡的调试经验去套 eMMC,大概率会被“启动时偶尔失败、休眠唤醒后挂在 IO 上”这类问题折磨到怀疑人生。

2. 从卡插入到类型确定的完整流程

2.1 检测起点:mmc_rescan 的状态机

卡检测的起点是 mmc_rescan。这个函数在 core.c 里,无论是轮询方式还是 gpio 中断方式,最终都会调用到它。

大概的执行路径是:

  • 判断 host 是不是在恢复、是不是已经在 rescan 中;
  • 如果 host 没有卡,尝试 mmc_attach_* 系列;
  • 如果已有卡且检测到移除,调用 mmc_detach_bus 和 mmc_remove_card;
  • 如果已有卡且还在,则可能进入重新初始化流程。

这里一个典型的坑是:mmc_rescan 内部有延时和状态判断,如果在中断上下文频繁触发,可能出现重复检测。内核里通过 host->rescan_disable 和 host->detect_change 配合,把检测请求延迟到 workqueue 里执行。你自己调试时如果发现 dmesg 里反复出现“Card removed during transfer”,可以重点查一下是不是中断触发的 detect 逻辑导致了重复扫描。

我个人建议你在改驱动时,千万不要直接在中断回调里调用 mmc_detect_change,而要用它自带的工作队列机制。原因是 MMC 初始化过程中的命令交互非常耗时,一次 card detect 往往要执行好几条命令,每条命令都涉及 complete 等待和超时计时,放在中断上下文里直接把系统调度搞死。

2.2 OCR、CID、CSD 这些寄存器怎么读

卡类型确定之后,真正让内核“认识”这张卡靠的是一组寄存器读取。MMC 卡最关键的三个寄存器是 OCR、CID、CSD。

OCR 寄存器表示卡支持的电压范围和工作状态。内核通过 mmc_send_op_cond 来发 CMD1,并从响应中解析 OCR。代码里这一步会反复发送 CMD1,直到 busy 位被清零,然后再把 host 支持的电压范围和卡支持的电压范围做交集,只有交集非空才会继续。

CID 寄存器是卡的唯一标识,里面包含厂商 ID、产品名、生产时间等。CMD2 用来获取 CID,之后所有卡都会用这组数据做区分。CSD 寄存器描述卡的容量、最大传输速度、块大小、读/写时序参数,通过 CMD9 读取。

读取这些寄存器的辅助函数在 mmc_ops.c 里都能找到,比如 mmc_send_cid、mmc_send_csd。我调试时最常用的操作是:

cat /sys/block/mmcblk0/device/cid cat /sys/block/mmcblk0/device/csd cat /sys/block/mmcblk0/device/ocr

如果这三项能读出来,说明卡的基本命令通路是通的,剩下的问题基本都在高速模式切换或者传输阶段。

2.3 关键结构体与初始化关系

卡一旦被识别,就会创建一个 struct mmc_card 来承载整个生命周期。这个结构体你一定要熟悉,因为后面几乎所有调试都绕不开它。

关键字段我列一下:

  • type:卡类型,文章开头提到的那几种;
  • state:卡的当前状态,包括卡是否在线、是否就绪;
  • ocr:从 OCR 寄存器读出的电压信息;
  • cid/csd/ext_csd:寄存器信息缓存;
  • sector_count:容量,从 CSD 或者 ext_csd 计算得出;
  • csd.capacity:块数;
  • ext_csd.rev:ext_csd 版本号,决定了卡支持的特性;
  • quirks:驱动针对某些卡的特殊处理。

从命名上你应该能看出,struct mmc_card 就是把一张硬件卡抽象成内核里的一个对象。它由 mmc_add_card 向内核注册,由 mmc_remove_card 注销。整个生命周期管理得清楚,不容易出问题;真正出问题的是结构体字段被某个私有驱动写错之后,随机崩溃。

3. 核心代码路径:mmc_attach_mmc 到 mmc_blk_probe

3.1 真正分清 MMC 与 SD 的分水岭

mmc_attach_mmc 位于 drivers/mmc/core/mmc.c,已经发展为 mmc_init_card 和 mmc_attach_bus 的组合流程。关键的初始化步骤大致如下。

第一步是 mmc_attach_bus,设置 host 的总线操作函数,也就是把 host->bus_ops 指向 mmc_ops。这时候总线起草能力已经绑定到 mmc 类型。

第二步是 mmc_init_card,这个函数内部做了大量工作:

  • 再次发送 CMD1,确认卡就绪;
  • 读取 CID;
  • 读取 CSD;
  • 读取 ext_csd;
  • 根据 ext_csd 支持的能力设置卡的属性;
  • 如果有必要,执行卡进入 high speed 模式的 switch 操作。

在整个过程中,对 MMC 类型卡影响最大的是 ext_csd。这可以说是 MMC/eMMC 特有的一把钥匙。通过 ext_csd,驱动能知道卡是否支持 HS200、HS400、DDR、是否支持 CMDQ、boot 分区大小、安全写保护区域等。

我可以负责任地说,调试 eMMC 启动失败时,你在 ext_csd 上花的时间一定是最多的。比如 read_ext_csd 失败,那你根本看不到卡支持什么模式,所有高速切换都无从谈起。

3.2 从 card 到 block 的对接

初始化完成之后,mmc_add_card 会把卡注册到设备模型里,生成 /sys/block/mmcblkX 对应的设备。重点在 mmc_driver 和 mmc_blk_probe 的匹配过程。

简单理解,mmc_add_card 把一个 mmc_card 设备添加到总线上,然后 mmc_blk 驱动通过 mmc_bus_match 判断设备类型是 MMC/SD,就会调用 mmc_blk_probe。

mmc_blk_probe 里面的动作很关键:

  • 分配 struct mmc_blk_data;
  • 初始化工作队列、请求队列;
  • 从卡的分区表里读取分区信息;
  • 为每个分区注册一个 gendisk;
  • 最终你会看到 /dev/mmcblk0、/dev/mmcblk0boot0、/dev/mmcblk0boot1 这些节点。

对于 eMMC,boot 分区尤其重要,因为系统 bootloader 通常在 boot0 或 boot1 里。我在实际项目里见过因为 boot 分区没设置到位,导致系统升级后 bootloader 损坏又无法从备份分区启动的案例。这类问题建议你直接用 mmc bootpart enable 操作来恢复,我在后面的排查章节会细说。

3.3 request 到 command 的映射

块设备层发起读写请求后,最终要变成一组 MMC 命令在总线上跑。这里的转化路径是:

bio request → mmc_blk_rw_rq_prep → 构造 mmc_request → mmc_wait_for_req → host 层执行 → 中断完成回调

mmc_request 里最核心的成员是 cmd、stop、data,分别对应命令、停止命令、数据传输块。对于 MMC 类型卡而言,read 操作通常对应 CMD17/CMD18,write 操作对应 CMD24/CMD25。CMD18 和 CMD25 是多块传输,需要在结束的时候发 CMD12 停止传输。

这里我强调一个工程上容易出问题的地方:多块传输的结束方式分为 open-end 和预定义块数,MMC 卡通常支持预定义块数,也就是你在 data 里设置 blocks,硬件会按块数自动停止。但如果 host 控制器驱动没有按这个逻辑设置,容易传着传着多传或少传一块,然后 CRC 错误满天飞。

操作单块命令多块命令停止命令
读CMD17CMD18CMD12
写CMD24CMD25CMD12

从实际性能角度讲,单块命令只适合小文件或元数据访问,真正吞吐量上来还得靠多块命令加 request 合并,不然性能连普通 U 盘都比不过。

4. 设备树配置与实际移植经验

4.1 一个能跑起来的 dts 节点该怎么写

默认情况下,如果 host 控制器驱动支持标准设备树绑定,你把 MMC 设备节点加到 dts 里之后,Linux 就能枚举卡。但枚举和“稳定工作”完全是两回事。

我建议至少保证这样的节点配置:

&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>; };

有几个属性的理解值得展开说一下。

bus-width 是数据线数量,eMMC 一般至少 4 位,常见 8 位。错了会导致数据线上信号错位,症状是最开始能识别,但读取大块数据时不稳定。

non-removable 表示这是板载存储,内核不会轮询检测卡移除。千万别小看这个属性,如果没配,系统会周期性地给 eMMC 发检测命令,影响休眠唤醒。

cap-mmc-highspeed 使能 MMC 的 high speed mode,如果缺失,eMMC 大概率被限制在 26MHz 或者更低的默认频率。

4.2 HS200/HS400 的时序选择

HS200 和 HS400 是 eMMC 的两种高速模式。HS200 是单数据速率 8 位接口下跑 200MHz 时钟;HS400 是 DDR 模式 8 位接口,同样是 200MHz 时钟,但等效吞吐翻倍。

要把这两种模式跑起来,光是 dts 里写 mmc-hs200-1_8v 是不够的,还需要:

  • 卡本身在 ext_csd 中声明支持 HS200/HS400;
  • host 控制器支持对应的时序模式;
  • 信号完整性要过关,尤其是 HS400 对信号质量非常敏感;
  • 必要的时候需要做 tuning,也就是通过发送 tuning block 来寻找最佳采样点。

实际调试中,最常见的问题是把频率提上去之后 CRC 错误暴增或者干脆卡住。这时候先别急着改驱动,用示波器看一眼信号眼图,再看 host 是否支持 DLL delay 调节。很多国产 SoC 的 MMC host 在 HS400 下需要配合特定 DLL 配置,不开的话采样点不对,必挂。

4.3 双电压与信号电平的关系

eMMC 引脚上有一路主电源 vmmc(通常是 3.3V),一路 IO 电源 vqmmc(可能是 1.8V 或 3.3V)。

高速模式(HS200/HS400)一般要求 1.8V 信号电平,所以在切高速之前,内核会先通过 host 的 start_signal_voltage_switch 回调把 vqmmc 从 3.3V 切换到 1.8V。如果切换失败,或者稳压器不支持动态调压,内核就会放弃高速模式,退回 legacy 模式。

我见过一种奇葩情况:dts 里配置了 mmc-hs400-1_8v,但 vqmmc 对应的 regulator 没有配置 set_voltage 操作,内核调 switch 时返回 -EINVAL,然后整个初始化流程回退。如果你看到 dmesg 里出现 “signal voltage switch failed”,先查 regulator 而不是卡。

5. 常见问题排查与调试实录

5.1 启动卡在 mmc0: error -110

-error 110 是 ETIMEDOUT,也就是命令超时。这个错误我遇到最多,但原因却五花八门。

最常见的原因是 vmmc 或 vqmmc 没供电。有些板子的 eMMC 电源由一个 GPIO 控制,dts 里 if 没正确把 regulator 和 GPIO 关联,或者时序上供电晚于 host 时钟,卡自然不回命令。

第二个常见原因是 CMD1 一直 busy,卡上电后需要时间完成内部初始化。标准做法是反复发送 CMD1 直到 busy 位释放,但如果你板子的时钟频率太低,卡在内部初始化期间响应窗口就会变长,可能导致 host 超时。

第三个原因比较隐蔽:host 控制器时钟没开启。MMC host 控制器本身需要两路时钟,一路是卡时钟,一路是控制器接口时钟。如果接口时钟没使能,发命令后控制器根本没动作,也会表现为超时。

排查思路我一般遵循:示波器量时钟 → 量 vmmc/vqmmc → 看 CMD 线是否有时序 → 再查 dmesg。这条路径省了我不少时间。

5.2 CID 能读出但 CSD 失败怎么办

这种情况很诡异:说明卡的命令通路基本是通的,CMD2 能响应,但 CMD9 读不到正确的 CSD。

如果 CSD 返回 CRC 错误,先怀疑频率。MMC 类型卡在识别阶段数据传输模式还没建立,过高的时钟会让数据线上的信号质量变差,CRC 错误的概率自然上升。你可以先把 host 的 max_bus_clk 调低,比如从 200MHz 降到 52MHz 甚至 26MHz,测一下能不能稳定读出来。

另一个原因是 1.8V 信号切换时序没对齐。有些卡的 CSD 读取要求在信号电压切换之后进行,如果你提前读取,会返回乱码。可以看一下 host->ios.timing 和 ios.signal_voltage 的值,确认当前状态。

我最后加一个经验:CSD 偶尔失败一次不要紧,但反复失败一定不要忽略。它往往预示着硬件链路不稳定,在高温或者低电压下会演变成大规模读写失败。

5.3 用 mmc-utils 工具快速定位问题

调试 MMC 类型卡,内核日志只是第一步,真正好用的是 mmc-utils 这套用户态工具。

常用命令如下:

# 查看 ext csd,重点看卡支持特性 mmc extcsd read /dev/mmcblk0 # 读取卡状态 mmc status get /dev/mmcblk0 # 设置 boot 分区 mmc bootpart enable 1 1 /dev/mmcblk0 # 设置 boot bus mmc bootbus set single_backward x1 x8 /dev/mmcblk0

调试的时候我会重点看 extcsd 里的 DEVICE_TYPE、BOOT_INFO、RPMB_SIZE、CACHE_SIZE、ERASE_GROUP_DEF 这些字段。比如 DEVICE_TYPE 显示不支持 HS400,但 dts 里写了 mmc-hs400-1_8v,那工作模式自然切不过去,白费功夫。

bootpart enable 这个命令值得单独强调。很多板子第一次烧录 bootloader 前,eMMC 的 BOOT_BUS_WIDTH 和 PARTITION_ACCESS 配置不是期望值,导致 bootloader 启动不了。这时候用 mmc bootpart enable 1 1 把 boot 分区对齐到物理分区,再配合 bootbus 设置,能解决一批“代码烧进去了但起不来”的问题。

5.4 用 tracepoint 看命令级耗时

如果你想把问题定位到“哪条命令慢了”这个粒度,最简单的方法是用内核的 MMC tracepoint。

内核开启以下配置后,可以在 ftrace 里看到完整的 mmc 请求流程:

CONFIG_MMC_DEBUG=y CONFIG_FTRACE=y CONFIG_TRACEPOINTS=y

然后:

cd /sys/kernel/debug/tracing echo 'mmc:*' > set_event echo 1 > tracing_on cat trace_pipe

通过 trace,你能看到 mmc_request_start 和 mmc_request_done 之间的时间差,确认每条命令的实际耗时,从而判断是否超时、是否 retry。这个手段比 dmesg 里的“某某命令超时”要精细得多,因为它能看到是哪一步慢的,甚至能发现多块命令之间的间隙过大导致的性能问题。

6. 一些容易被忽略但很实用的点

6.1 块层合并对性能的影响

很多人调试 MMC 性能时,只盯着 host 控制器频率,忽略了块层的 bio 合并机制。MMC 驱动通过 mmc_blk_set_queue 初始化请求队列时,会设置 queue 的合并参数,比如 max_segments、max_segment_size。这些参数如果没有和 host 的 SGMII/DMA 能力匹配,就会产生大量碎片化请求,吞吐打折。

一个直观表现是:顺序读 100MB/s,顺序写只有 40MB/s,而且 dmesg 里没有任何错误。这种时候去看 blktrace,往往是因为每次写请求很小,没有合并成大的 segment。

合理的做法是确认 host 控制器支持 multi-block DMA 传输,并调整 queue 的参数,让内核一次下发尽量大的请求。比如设置 max_segments 为 64 或 128,max_segment_size 为 1MB,都能有效提升顺序读写性能。

6.2 电源域和 runtime pm 的配合

MMC host 控制器的电源管理是 MMC 类型卡低功耗设计的重头戏。特别是板载 eMMC,系统休眠时要把时钟关掉、电源可以保持也可以关断,但唤醒时不能直接访问卡,必须先重新初始化链路。

内核里通过 mmc_runtime_suspend/mmc_runtime_resume 来实现。有些平台会出现休眠唤醒后第一个 IO 访问超时,就是因为唤醒流程中 host 的时钟恢复和卡的状态重建没有同步。

我调试下来的经验是:先确认 host 是否有 PM runtime 回调,再确认卡在恢复时是否重新执行了 init。如果唤醒后直接调用 mmc_blk_read,卡可能还处于休眠状态或者状态寄存器里的电源状态不对,导致首个请求失败。大部分平台会在 runtime_resume 里调用 mmc_power_up + mmc_init_card,这一步不能省。

6.3 关于 CMDQ 的一些经验

较新版本内核在 MMC 类型卡上支持 command queueing(CMDQ),典型实现是 mtk 和某些平台的 emmc 控制器。CMDQ 的好处是能同时下发多条命令,提高并发度,但它的实现复杂度明显上升。

如果你只是想稳定、稳妥,不追求极限性能,可以先不加 CMDQ 支持。因为 CMDQ 模式下队列深度、标签管理、错误恢复都变得更加复杂。一旦出错,恢复路径要处理的情况非常多,调试成本远高于性能收益。我的建议是:先把无 CMDQ 的模式调稳定,性能不够再考虑引入。

结尾部分,聊聊个人体会。

我在实际调试中最大的体会是,MMC type card 的驱动并不复杂,复杂的是它跟硬件时序、电源管理、块层调度这些外部因素交错时的表现。你可能花三天时间查一个时好时坏的 bug,最后发现是某个 GPIO 的上电时序晚了几毫秒。也可能纠结了很久的写入性能问题,最后把 request queue 参数一调就解决。所以我一直建议项目里专门留一份 MMC 相关的调试 checklist,把电压、时钟、dts 属性、ext_csd 状态、tracepoint 这些工具全部串起来,而不是等出了问题再从头摸索。最后再分享一个小技巧:无论遇到什么诡异现象,先把频率降到最低、关掉高速模式、关掉 runtime pm,能稳定出来再一项一项开回去。这个方法能帮你快速确定问题出在高速特性上,还是出在基本通路里。希望这篇能帮你少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询