资讯详情

嵌入式Linux mmc卡检测与初始化全链路解析

📅 2026/10/12 7:49:04 | 华诺云谱 👁 阅读
嵌入式Linux mmc卡检测与初始化全链路解析
1. 从卡插进去没反应说起mmc卡检测的完整链路做过嵌入式Linux存储驱动的人大概率都遇到过这样的场景板子通电SD卡插进去dmesg里安安静静/dev/mmcblk0死活不出来。这时候很多人第一反应是去翻设备树看cd-gpios配了没有但改完发现还是没动静。问题往往出在没搞清楚mmc子系统里卡检测这件事到底是怎么串起来的。Linux的mmc子系统把存储卡抽象成了三层host层对应SoC的mmc控制器驱动、core层drivers/mmc/core/负责协议流程和卡管理、card层具体的卡设备。卡检测和初始化这两件事横跨了这三层而且时序上非常讲究——先检测到卡的存在才能触发上电、识别、初始化这一整套流程。这篇文章要聊的就是这条链路从硬件层面的CDCard Detect信号到host驱动怎么上报再到core层怎么调度扫描工作队列最后卡是怎么被识别出来并注册成块设备的。我会把每个环节的关键函数、数据结构、以及实际调试中容易踩的坑都摊开讲。适合正在写mmc host驱动、或者被SD卡识别问题折磨过的嵌入式工程师参考。需要先明确一个前提mmc子系统的卡检测有两条路径一条是轮询检测polling一条是中断检测基于CD引脚的中断。这两条路径的代码走向完全不同调试时如果搞混了会浪费大量时间。下面我会分别拆解。2. 卡检测的两条路径轮询与中断的机制差异2.1 轮询检测mmc_rescan工作队列的调度逻辑轮询检测是mmc子系统的兜底机制。当host控制器没有CD引脚或者驱动没有配置CD中断时core层会启动一个延迟工作队列周期性地去摸一下卡槽里有没有卡。核心函数是mmc_rescan它被封装在一个delayed_work里。初始化时mmc_alloc_host会调用INIT_DELAYED_WORK(host-detect, mmc_rescan)把检测工作挂到host结构上。真正启动轮询是在mmc_start_host里void mmc_start_host(struct mmc_host *host) { host-f_init max(freqs[0], host-f_min); host-rescan_disable 0; ... mmc_detect_change(host, 0); }mmc_detect_change是关键入口它最终会调用_mmc_detect_change把host-detect这个delayed_work排进系统工作队列。延迟时间由参数delay决定轮询模式下通常传0或者一个较小的值。mmc_rescan的执行逻辑大致是这样的void mmc_rescan(struct work_struct *work) { struct mmc_host *host container_of(work, struct mmc_host, detect.work); ... if (host-rescan_disable) return; mmc_bus_get(host); /* 先检查卡是否还在 */ if (host-bus_ops !host-bus_dead) { if (!mmc_card_is_removable(host)) goto out; if (host-bus_ops-detect !host-bus_dead) host-bus_ops-detect(host); ... } /* 重新扫描 */ mmc_claim_host(host); ... mmc_rescan_try_freq(host, host-f_init); ... }这里有个细节值得注意mmc_rescan并不是无脑地重新初始化它先判断当前总线上是否已经挂了一张卡host-bus_ops非空如果卡还在就调用bus_ops-detect做一次确认如果卡已经不在了才会走完整的重新扫描流程。这个设计避免了每次轮询都对已识别的卡做一次完整的重新初始化节省了时间和功耗。轮询的周期由host-detect的延迟时间控制。在mmc_rescan的末尾如果卡是可移除的会再次调度自己if (host-caps MMC_CAP_NEEDS_POLL) mmc_schedule_delayed_work(host-detect, HZ);注意这里有个MMC_CAP_NEEDS_POLL标志。只有host控制器设置了这个capability轮询才会持续进行。很多SoC的mmc控制器默认不设这个标志因为轮询会持续唤醒CPU对功耗不友好。实际产品中如果硬件有CD引脚几乎都会用中断方式轮询只作为调试或特殊场景的备选。2.2 中断检测CD引脚如何触发mmc_detect_change中断检测依赖硬件上的CDCard Detect引脚。这个引脚在卡插入和拔出时会产生电平变化host驱动把它注册成一个GPIO中断在中断处理函数里调用mmc_detect_change通知core层。典型的host驱动代码长这样static irqreturn_t sdhci_cd_irq(int irq, void *dev_id) { struct sdhci_host *host dev_id; ... mmc_detect_change(host-mmc, 0); return IRQ_HANDLED; }mmc_detect_change的第二个参数是延迟时间。中断上下文里通常传0但有些驱动会传一个小的延迟比如20ms目的是做去抖。因为机械式卡槽的CD引脚在插拔瞬间会有抖动如果不去抖一次插卡可能触发多次中断导致core层反复扫描。去抖的实现方式有两种一种是在中断处理里用mdelay或msleep硬等但中断上下文不能睡眠所以更常见的做法是传一个延迟给mmc_detect_change让工作队列延迟执行天然避开了抖动窗口。另一种是在GPIO子系统层面配置debounce-interval由硬件或GPIO控制器做去抖。这里有个容易踩的坑CD引脚的极性。有些板子的CD引脚是低电平有效卡插入时拉低有些是高电平有效。如果设备树里cd-gpios的极性配反了会出现卡插着却报没卡卡拔了却报有卡的诡异现象。设备树里用GPIO_ACTIVE_LOW或GPIO_ACTIVE_HIGH来指定配错的话mmc_gpiod_request_cd拿到的电平就是反的。2.3 两种路径的选型依据与实测对比到底该用轮询还是中断这不是拍脑袋决定的取决于硬件设计和产品需求。对比维度轮询检测中断检测硬件要求无需CD引脚需要CD引脚并接到GPIO中断CPU占用周期性唤醒功耗高仅在插拔时触发功耗低响应速度取决于轮询周期有延迟几乎实时去抖处理天然规避周期远大于抖动需要额外去抖调试难度逻辑简单容易定位涉及GPIO/中断/极性环节多适用场景无CD引脚的板子、调试阶段量产产品、低功耗设备我实测下来的经验是调试阶段可以先用轮询把卡识别流程跑通确认host控制器和core层的交互没问题再切到中断模式。这样能把卡识别失败和CD中断没触发这两个问题解耦排查效率高很多。如果一上来就用中断一旦卡识别不了你根本分不清是CD信号没进来还是进来了但初始化流程挂了。另外提一句有些host控制器支持控制器内部的CD检测不经过GPIO子系统而是直接读控制器寄存器里的CD状态位。这种方式的检测逻辑在host驱动内部实现不走mmc_gpiod_request_cd那套调试时要注意区分。3. 卡初始化的核心流程从mmc_rescan_try_freq到卡注册3.1 上电与时钟配置初始化的第一步为什么是400kHz卡检测到之后接下来就是初始化。初始化的入口是mmc_rescan_try_freq它接收一个频率参数这个频率就是初始化阶段的工作频率。为什么初始化阶段要用400kHz这么低的频率这是SD/MMC协议规定的。协议里明确要求在卡进入数据传输模式之前主机必须以不超过400kHz的时钟频率与卡通信。原因有两个一是卡刚上电时内部电路还没稳定高频时钟可能导致通信失败二是识别阶段需要兼容各种速度等级的卡低频是最大公约数。代码里的体现是static int mmc_rescan_try_freq(struct mmc_host *host, unsigned freq) { host-f_init freq; ... mmc_go_idle(host); mmc_send_if_cond(host, host-ocr_avail); ... }host-f_init在mmc_start_host里被设为max(freqs[0], host-f_min)freqs[0]通常是400000400kHz。host驱动需要实现-set_ios回调core层通过它来配置时钟和电源。上电的顺序也很讲究先供电power_up再给时钟set_ios里配置clock最后发命令。如果顺序反了卡可能进入一个不确定的状态。我在调试一块自制板子时就遇到过因为上电和时钟顺序颠倒导致卡偶尔能识别、偶尔不能识别的问题后来严格按电-时钟-命令的顺序改就稳定了。3.2 卡识别命令序列CMD0、CMD8、ACMD41、CMD2、CMD3初始化阶段的核心是一串标准命令的交互。这些命令的发送顺序和响应处理是mmc core层最核心的逻辑之一。CMD0GO_IDLE_STATE让卡进入空闲状态。这是所有卡初始化的第一步无论SD卡还是MMC卡都要发。发送后卡进入idle状态准备接收后续命令。CMD8SEND_IF_COND这是SD 2.0规范引入的命令用来检测卡是否支持2.0及以上规范同时传递电压范围信息。如果卡返回响应说明是SD 2.0的卡如果超时无响应说明是SD 1.x或MMC卡。代码里通过mmc_send_if_cond的返回值来判断。ACMD41SD_APP_OP_COND这是SD卡特有的命令需要先发CMD55切换到APP命令模式用来查询卡的OCROperating Conditions Register并让卡完成上电初始化。这个命令需要循环发送直到卡返回的OCR里busy位清零表示卡上电完成。循环次数和超时时间都有讲究太短了卡还没准备好太长了浪费时间。CMD2ALL_SEND_CID获取卡的CIDCard Identification寄存器里面包含制造商、产品名、序列号等信息。这个命令的响应是136位的长响应。CMD3SEND_RELATIVE_ADDR让卡返回一个相对地址RCA后续通信就用这个RCA来寻址而不是每次都广播。对于SD卡这一步之后卡进入standby状态。这一串命令的代码主要在mmc_sd_init_card和mmc_attach_sd里。整个流程是static int mmc_sd_init_card(struct mmc_host *host, u32 ocr, struct mmc_card *oldcard) { ... err mmc_sd_get_cid(host, ocr, cid, rocr); ... card mmc_alloc_card(host, sd_type); ... err mmc_send_relative_addr(host, card-rca); ... err mmc_sd_get_csd(host, card); ... err mmc_select_card(card); ... err mmc_sd_setup_card(host, card, oldcard ! NULL); ... }每一步都有失败的可能而且失败原因各不相同。比如CMD8失败可能是卡太老ACMD41超时可能是供电不足CMD2失败可能是卡损坏。调试时看dmesg里哪一步报错基本就能定位问题方向。3.3 总线宽度与速度模式的协商从1-bit到4-bit/8-bit卡识别出来之后还没完。接下来要协商总线宽度和速度模式这直接决定了后续数据传输的带宽。总线宽度方面SD卡支持1-bit和4-bit模式MMC卡还支持8-bit。初始化时默认是1-bit模式识别完成后通过ACMD6SD卡或CMD6MMC卡切换到更宽的总线。切换的前提是host控制器和卡都支持。速度模式方面SD卡有Default Speed、High Speed、UHS-I等模式MMC卡有HS、HS200、HS400等。协商过程涉及读卡的扩展寄存器通过CMD6的Function Switch确认卡支持哪些模式然后host驱动通过-set_ios配置对应的时钟频率和时序。这里有个实际调试中的经验如果总线宽度切换失败卡可能还能工作但性能会大打折扣。我遇到过一块板子4-bit模式死活切不过去最后发现是PCB上DAT1~DAT3的走线有一根虚焊导致卡在切换时检测到数据线异常自动回退到1-bit。这种硬件问题光看软件日志很难发现得结合示波器量信号。速度模式切换失败则更隐蔽因为卡可能仍然以低速模式工作功能正常但吞吐量上不去。用cat /sys/kernel/debug/mmc0/ios可以看到当前实际生效的时钟频率和总线宽度这是排查性能问题的第一手信息。3.4 卡设备注册mmc_add_card与块设备节点的诞生所有初始化步骤完成后最后一步是把卡注册到系统里让它变成一个可用的块设备。核心函数是mmc_add_cardint mmc_add_card(struct mmc_card *card) { ... card-dev.bus mmc_bus_type; card-dev.type mmc_card_type; ... if (mmc_card_sd(card)) card-dev.class sdio_bus_type; // 实际是mmc_host class ... err device_add(card-dev); ... }device_add会触发总线匹配对于SD/MMC存储卡会匹配到mmc_blk驱动进而调用mmc_blk_probe在/dev下创建mmcblkX设备节点。同时如果卡上有分区还会创建mmcblkXpY这样的分区节点。从卡插入到/dev/mmcblk0出现整个链路串起来就是CD中断触发或轮询发现-mmc_detect_changemmc_rescan工作队列执行 -mmc_rescan_try_freq上电、配时钟 - 发CMD0/CMD8/ACMD41/CMD2/CMD3卡识别成功 -mmc_alloc_card- 总线宽度/速度协商mmc_add_card-device_add-mmc_blk_probe-/dev/mmcblk0这条链路上任何一环断了卡都出不来。调试时按这个顺序逐段确认比盲目改代码高效得多。4. 调试实战卡检测与初始化失败的排查链路4.1 用dmesg和debugfs定位卡在哪一步失败排查卡识别问题第一步永远是看dmesg。mmc core层在关键节点都有打印比如mmc0: new high speed SDHC card at address 0007 mmcblk0: mmc0:0007 SD32G 29.7 GiB如果卡识别失败通常会看到类似这样的错误mmc0: error -110 whilst initialising SD card mmc0: timeout waiting for hardware interrupt.-110是-ETIMEDOUT表示命令超时。这时候要往下看是哪条命令超时。可以在mmc_rescan_try_freq里加打印或者用dynamic_debug打开mmc core的调试信息echo file drivers/mmc/core/* p /sys/kernel/debug/dynamic_debug/control打开后每条命令的发送和响应都会打印出来能精确看到卡在哪条命令上卡住了。debugfs里还有几个有用的节点/sys/kernel/debug/mmc0/ios当前时钟频率、总线宽度、时序模式/sys/kernel/debug/mmc0/regshost控制器寄存器值需要host驱动支持/sys/kernel/debug/mmc0/clock时钟相关调试信息我一般会先看ios确认时钟和总线宽度是否符合预期。如果时钟频率是0或者明显不对问题就在host驱动的set_ios实现上而不是卡本身。4.2 CD引脚极性配反一个高频低级错误前面提过CD引脚极性这里展开说。设备树里配置CD引脚通常是这样的mmc0 { cd-gpios gpio1 5 GPIO_ACTIVE_LOW; ... };GPIO_ACTIVE_LOW表示低电平有效即卡插入时引脚为低。如果实际硬件是卡插入时引脚为高但设备树配了GPIO_ACTIVE_LOW就会出现卡插着报没卡的现象。怎么快速确认两个方法一是看/sys/kernel/debug/gpio找到CD对应的GPIO看它的当前值和方向。插卡和拔卡各看一次对比值的变化。二是在host驱动里加打印把mmc_gpiod_get_cd拿到的电平打出来。如果插卡时读到的是未插入那基本就是极性反了。这个错误之所以高频是因为很多原理图标注的CD信号实际经过一级反相器或者电平转换后极性就变了但设备树还是按原理图上的标注配。原理图上的标注和实际GPIO读到的电平一定要用万用表或示波器实测确认不能想当然。4.3 上电时序与电压切换vmmc和vqmmc的坑SD卡有两条供电线vmmc卡的主供电和vqmmcIO供电。在UHS-I模式下vqmmc需要在1.8V和3.3V之间切换这个切换时序如果不对卡会直接罢工。设备树里通常这样配mmc0 { vmmc-supply reg_3v3; vqmmc-supply reg_sd_io; ... };vqmmc的电压切换由mmc_set_signal_voltage触发最终调用到regulator框架。如果regulator驱动没实现set_voltage或者电压切换时间不够卡在UHS-I模式下就会初始化失败。我踩过的一个坑是vqmmc的regulator在设备树里配了但对应的PMIC驱动没有正确注册导致regulator_get拿到的是一个dummy regulator电压切换实际没生效。卡在Default Speed下能工作一切到High Speed就挂。后来在dmesg里看到vqmmc相关的regulator报错才定位到问题。排查这类问题可以看/sys/kernel/debug/regulator/regulator_summary确认vmmc和vqmmc的实际电压值和使能状态。4.4 时钟频率设置失败set_ios回调的实现陷阱host驱动的-set_ios回调是core层配置硬件的唯一入口时钟频率、总线宽度、电源状态都通过它下发。这个回调实现不好会引发各种诡异问题。常见的实现陷阱有时钟分频计算错误。SoC的mmc控制器通常有一个基础时钟比如200MHz通过分频器得到目标时钟。如果分频计算公式写错实际输出的时钟和预期差很多。比如目标400kHz实际输出了4MHz卡在初始化阶段就可能因为时钟太快而通信失败。时钟使能顺序错误。有些控制器要求先配置分频再使能时钟输出顺序反了会导致时钟毛刺卡收到异常时钟后进入错误状态。电源状态切换遗漏。set_ios里的ios-power_mode有MMC_POWER_OFF、MMC_POWER_UP、MMC_POWER_ON三种状态host驱动需要根据状态做对应的上电/下电操作。如果漏了某个状态的处理卡可能一直处于未上电状态。验证set_ios是否正确最直接的方法是在里面加打印把每次调用的ios-clock、ios-bus_width、ios-power_mode都打出来和预期值对比。同时用示波器量时钟引脚的实际频率软硬结合确认。5. 几个容易被忽略的细节与经验总结5.1mmc_rescan的并发保护rescan_disable的作用mmc_rescan可能被多个路径触发CD中断、轮询工作队列、系统PM回调。如果并发执行会导致重复初始化同一张卡引发竞态。core层用host-rescan_disable来防止这种情况。在系统suspend时mmc_suspend_host会把rescan_disable设为1阻止rescan执行resume时再设回0并触发一次检测。host驱动在实现suspend/resume回调时必须正确调用mmc_suspend_host和mmc_resume_host否则可能出现suspend期间CD中断触发rescan导致系统无法正常休眠。这个细节在调试电源管理问题时特别重要。我遇到过一块板子suspend后总是被SD卡的中断唤醒最后发现是host驱动的suspend回调里没有正确disable CD中断导致卡槽的微小振动都能触发中断。5.2 非可移除卡的检测MMC_CAP_NONREMOVABLE的处理有些产品里存储卡是焊在板子上的比如eMMC物理上不可移除。这种情况下CD检测逻辑要相应调整。host驱动通过设置MMC_CAP_NONREMOVABLE标志来告诉core层这张卡不可移除。设置后mmc_rescan里会跳过检查卡是否还在的逻辑因为卡永远在。同时轮询也不会启动因为没必要。如果eMMC被错误地配成了可移除卡会出现什么问题系统会周期性地去检测卡是否存在每次检测都可能触发一次重新初始化浪费CPU还可能导致数据访问异常。所以这个标志一定要配对。5.3 初始化失败后的重试机制与超时处理卡初始化失败后core层不是直接放弃而是有重试机制。mmc_rescan_try_freq会尝试多个频率从低到高如果某个频率下初始化失败会换下一个频率再试。这个设计是为了兼容不同速度等级的卡。重试的次数和频率列表在mmc_rescan里定义static const unsigned freqs[] { 400000, 300000, 200000, 100000 };如果所有频率都试过了还是失败才会放弃并打印错误信息。调试时如果看到卡反复重试最后失败说明问题比较底层可能是硬件连接或者供电问题而不是单纯的软件配置。超时处理方面每条命令都有超时时间由host-max_busy_timeout和命令本身的特性决定。如果卡在某个操作上卡死超时后core层会尝试恢复mmc_reset恢复失败才会报错。这个恢复机制在调试时容易被忽略有时候看到一次错误日志实际上系统已经自动恢复了卡还能正常工作。5.4 从实际项目中学到的三条经验第一条先确认硬件再调软件。卡识别问题里硬件原因供电不足、走线虚焊、CD引脚接错占了相当大的比例。在改代码之前先用万用表量供电、用示波器看时钟和CD信号能省下大量时间。第二条善用dynamic_debug和debugfs。mmc core层的调试信息非常丰富打开后能看到每条命令的收发细节。很多人不知道这个功能遇到问题只会加printk重新编译内核效率低很多。第三条理解时序不要只看代码。mmc初始化是一个严格的时序过程上电、时钟、命令、响应每一步都有时间要求。代码逻辑对了时序不对照样失败。调试时脑子里要有一条时间轴知道当前处在哪个阶段下一步该发生什么。卡检测和初始化这块代码量不算特别大但涉及的硬件细节和协议规范很多。把这条链路吃透不仅对调试SD/eMMC问题有帮助对理解Linux设备驱动模型里检测-识别-注册这套通用范式也很有价值。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑