ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Linux MMC 驱动深入解析:从卡识别到 HS400 的完整调试指南

Linux MMC 驱动深入解析:从卡识别到 HS400 的完整调试指南 这一篇是整个 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.revext_csd 版本号决定了卡支持的特性quirks驱动针对某些卡的特殊处理。从命名上你应该能看出struct mmc_card 就是把一张硬件卡抽象成内核里的一个对象。它由 mmc_add_card 向内核注册由 mmc_remove_card 注销。整个生命周期管理得清楚不容易出问题真正出问题的是结构体字段被某个私有驱动写错之后随机崩溃。3. 核心代码路径mmc_attach_mmc 到 mmc_blk_probe3.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 这些节点。对于 eMMCboot 分区尤其重要因为系统 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/CMD18write 操作对应 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/HS400host 控制器支持对应的时序模式信号完整性要过关尤其是 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_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 之间的时间差确认每条命令的实际耗时从而判断是否超时、是否 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 或 128max_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 queueingCMDQ典型实现是 mtk 和某些平台的 emmc 控制器。CMDQ 的好处是能同时下发多条命令提高并发度但它的实现复杂度明显上升。如果你只是想稳定、稳妥不追求极限性能可以先不加 CMDQ 支持。因为 CMDQ 模式下队列深度、标签管理、错误恢复都变得更加复杂。一旦出错恢复路径要处理的情况非常多调试成本远高于性能收益。我的建议是先把无 CMDQ 的模式调稳定性能不够再考虑引入。结尾部分聊聊个人体会。我在实际调试中最大的体会是MMC type card 的驱动并不复杂复杂的是它跟硬件时序、电源管理、块层调度这些外部因素交错时的表现。你可能花三天时间查一个时好时坏的 bug最后发现是某个 GPIO 的上电时序晚了几毫秒。也可能纠结了很久的写入性能问题最后把 request queue 参数一调就解决。所以我一直建议项目里专门留一份 MMC 相关的调试 checklist把电压、时钟、dts 属性、ext_csd 状态、tracepoint 这些工具全部串起来而不是等出了问题再从头摸索。最后再分享一个小技巧无论遇到什么诡异现象先把频率降到最低、关掉高速模式、关掉 runtime pm能稳定出来再一项一项开回去。这个方法能帮你快速确定问题出在高速特性上还是出在基本通路里。希望这篇能帮你少走一些弯路。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表