作为一个常年和Linux内核打交道的嵌入式驱动开发者,我最近半年几乎每天都在和mmc core层打交道。起因是手里一块板子的SD卡偶发识别失败,每次看日志都像在猜谜,一会儿是CMD8超时,一会儿是CMD55收到了意外响应,折腾了整整两周才定位到是core层的初始化时序被某个补丁搅乱了。也正因为这次踩坑,我把drivers/mmc/core/下面的代码从头到尾读了一遍,越读越觉得这一层被太多人低估了。
如果你去搜Linux SD卡/SDIO驱动开发的资料,十有八九都在讲drivers/mmc/host/下面的控制器驱动,也就是怎么写platform driver、怎么配DMA、怎么处理中断。很少有人认真讲中间的core层。但这个认知其实是错的。真正决定一个SD卡能不能被稳定识别、一次读写请求能不能被正确拆成若干条CMD、UHS-I模式要切换到哪种电压时序的地方,恰恰就在core层。这篇我专门把“3-core”这一层讲透,内容全部围绕实际调试和源码阅读时的观察展开,适合刚入门内核驱动、以及被卡识别问题折磨过的同学参考。
1. 先说清楚:core层在整个SD/MMC驱动栈里到底扮演什么角色
1.1 三层架构:host、core、block
Linux的MMC子系统从上到下大致可以分成三层:最下面是host层,负责物理控制器相关的操作,比如寄存器读写、描述符管理、DMA传输;最上面是block层,它把内核块设备层的请求翻译成MMC子系统能理解的mmc_request;夹在中间的core层,则是整个子系统的协议中枢和状态管家。
host层一般对应厂商的SoC内部MMC控制器驱动,这一层的代码在drivers/mmc/host/下面,打开之后能看到形形色色的驱动文件,但它们共同遵守的接口标准,都是core层定义的那一份struct mmc_host_ops。也就是说,core层不关心你底下用的是某A厂商的控制器还是某B厂商的IP,它只认mmc_host_ops里的那几个回调。
block层则位于core之上,平时看到/dev/mmcblk0这种设备节点,就是block层通过mmc_blk驱动注册出来的。block层负责把文件系统发下来的IO请求组织成req,再调用core层提供的接口提交下去,最后拿回结果。
你可能要问,既然有host和block,core还有什么存在的必要?答案很简单:SD卡是一个带有完整协议状态机的可插拔外设,总得有人负责卡上电之后的识别流程、命令重试机制、时序协商、休眠唤醒这些策略性工作。controller不行,太底层;block层不行,它只管数据。真正能把这一整套串起来的只有core。
1.2 core层具体管了哪些事
我梳理过drivers/mmc/core/目录下几十个文件后,发现core层的职责大致可以归纳为五块:
- 卡探测与枚举:识别SD卡还是SDIO设备,读取CID、CSD、SCR等寄存器,建立
mmc_card对象。 - 总线状态管理:维护host的总线宽度、时钟频率、信号电压,以及电源开断。
- 命令发送与响应解析:封装CMD、ACMD的发送流程,处理CRC错误、超时、非法命令等异常。
- 请求调度与排队:协调
mmc_request的投递、完成通知、并发切换。 - 特殊功能支持:比如UHS模式切换、HS200/HS400 tuning、eMMC的BKOPS、SDIO的IRQ。
这里面的每一项单拎出来都能写一篇文章,但实战中大家最先遇到也最常踩坑的,是卡枚举流程和请求调度这两块。后面我会各用一个大章节展开。
1.3 一个驱动开发者最容易犯的层次错位问题
我见过不少同学在调试SD卡识别失败时,一上来就钻进host层查中断状态寄存器,查mbox、查cd脚配置,查了半天没发现问题。等大家坐在一起撸代码才发现,真正异常出现在core层:mmc_rescan扫描时用的初始频率选错了,导致卡压根没有响应CMD8。
这种“层次错位”的排查方式最浪费时间。正确的做法是先定位问题出在哪一层:如果mmc_request结构里命令都发出去了但没响应或者响应异常,优先查core层的命令序列和时序;如果响应正常但数据CRC错乱,再往下查host层的DMA和中断。也就是说,core层是整个子系统的“腰”,后腰没力气,前面腿再壮也跑不动。
2. core层的心脏:卡状态机与协议解析
2.1 为什么SD卡要有一台状态机
现代SD卡本质上是一个带微控制器的存储设备,它内部维护了一套标准的状态机。从inactive到idle,再到ready、ident、stby、tran,每次状态迁移都依赖外部发送特定命令。core层的职责,就是在合适的时机发送合适的命令,引导卡完成状态跃迁。
举个最典型的例子:卡上电之后,host要先发送CMD0让卡进入idle状态,然后发送CMD8检查卡是否支持SD Spec 2.0,再发送ACMD41通过HCS位协商工作电压,之后卡会进入ready状态。这一整套连环动作如果顺序错乱,卡就会一直停留在某个中间态。
2.2 OCR、CSD、SCR这些寄存器解析是core的分内事
SD卡可以通过命令读取一系列寄存器。OCR(工作条件寄存器)反映了卡的电压范围和上电状态;CSD(卡特定数据寄存器)包含容量、最大读/写块长度、最大时钟频率等参数;SCR(SD配置寄存器)则声明了卡支持的版本和安全能力,是从ACMD51里读出来的。
这些寄存器为什么必须由core层解析?因为block层算容量、host层调时钟都需要用到。比如mmc_read_csd会解析CSD,再用mmc_fixup_device对特定厂商卡做兼容性修正。我之前调试的一张杂牌卡,就是因为CSD里的read_blkbits字段解析出来是异常值,导致上层申请的buffer大小和卡实际返回的数据长度对不上,每次多读16字节就超时。后来在core层对该厂商卡的字段做了强制纠正,问题立即解决。
2.3 一个host_ops部件如何把这些信息传给上层
core层读取到这些寄存器之后,通常会填充到struct mmc_card的字段里。也就是说,只要你写host层驱动,你不必关心CSD第50位到第52位是什么意思,只需要调用core提供的mmc_read_csd、mmc_decode_cid这些函数。这种“让协议的归协议,让硬件的归硬件”的隔离,正是core层最大的价值。
另外,SDIO设备在core层也有自己的一套流程:先通过CMD5去探测IO设备是否存在,再读取CCCR和FBR信息。SDIO的枚举流程和SD卡不完全一样,但依然跑在同一套mmc_attach_bus的框架里。core层会用mmc_attach_sdio和mmc_attach_sd分派不同的识别逻辑。这块内容适合读sdio_ops.c,你会在里面看到很多有趣的IO读写命令构造过程。
3. 上手维护前必须认识的几个核心数据结构
3.1 mmc_host:把一个控制器抽象成“实体”
struct mmc_host是所有MMC/SD控制器驱动都要实例化并注册的核心对象。它里面至少有这几类信息:
- 硬件能力标志:
caps和caps2,表示是否支持SDIO IRQ、是否支持UHS、是否支持HS400等。 - 当前运行状态:总线频率、总线位宽、时序模式、信号电压。这些字段会被core层用来和卡协商出最终运行参数。
- 统计与调试信息:比如错误计数、传输统计。
每个host控制器在系统里都有一个编号,从mmc0开始。你在/sys/class/mmc_host/mmc0/目录下能看到设备、设备号等信息。调试时,cat /sys/kernel/debug/mmc0/ios可以直接看到当前控制器的工作状态,这是堪称神奇的功能。
3.2 mmc_card、mmc_command、mmc_data、mmc_request
把一次完整的SD命令交互拆开看,struct mmc_command描述一条命令,struct mmc_data描述与之关联的数据传输,struct mmc_request则把命令、数据打包成一个完整的请求。
mmc_command里有几个关键的字段:opcode(命令号)、arg(命令参数)、resp数组(存放响应)、flags(用于说明期望什么类型的响应)。mmc_request则把命令和数据通过指针关联起来:cmd指向要发送的命令,data指向数据块参数,mrq指针会在core层内部使用。
我最想提醒的一点是:cmd、data和mrq的生命周期管理是core层最容易出错的地方。我在读代码时看到过不少驱动通过kzalloc临时分配请求结构体,结果在中断上下文里使用的时候发起释放导致空指针。core层和host层的契约里写得很清楚:请求对象必须存活到mmc_request_done被调用之后。
3.3 别忘了mmc_pwrseq和调节器
还有一个容易被忽略的东西:struct mmc_pwrseq。它是core层为了支持外接电源时序而引入的,近似于“管理卡供电顺序”的抽象层。很多做Wifi模组(通过SDIO连接)的板子,复位脚和电源脚需要按严格时序操作,直接放在host层会非常别扭,而放到mmc_pwrseq里,启动时会由core层统一触发。
这套设计解释了为什么有些板子的Wifi模组识别不稳定,但换一个平台却正常。说到底就是pwrseq时序没有按core层规定的节奏来。调试支持wifi模组的SDIO设备时,建议优先查mmc_pwrseq_ops的实现是否遵守了厂商手册里的上电间隔。
4. core层与host层之间的那道契约:host_ops
4.1 request回调:所有数据通路的咽喉
struct mmc_host_ops定义了一大堆函数指针,但最核心的其实是request。core层把所有要发的命令和要传的数据整理好,放进一个mmc_request结构里,然后调用host->ops->request(host, mrq),请求就落到了host驱动手里。
request回调有两个重要特性:
- 它可以异步返回。也就是说,host驱动拿到请求后返回,等DMA传输完成再由中断处理函数调用
mmc_request_done通知core层。 - 它必须在合理时间内完成命令投递,不能在函数里做耗时的忙等。我在优化某数据通路时,发现host驱动的
request回调里有个udelay(1000)等待fifo清空,导致core层创建的异步上下文被无谓阻塞,整条IO队列陷入停顿。去掉这行忙等、改为中断触发,吞吐量立刻翻倍。
4.2 set_ios:电压、时钟、位宽的“风向标”
set_ios是core层用来调整host输出参数的回调。struct mmc_ios里面包含四个核心字段:clock(时钟频率)、bus_width(总线宽度)、power_mode(供电模式)、timing(时序模式)。
每次core层切换到新的模式,比如从默认SD模式切到UHS-I SDR104,它会先通过mmc_set_clock把时钟降低,再设置电压,等电压稳定后再提升时钟。如果你在host层只实现了简单的寄存器写操作,没有遵循“先降频、后加压、再升频”的时序,卡非常容易出现完全无响应或CRC错误。
我早年在公司调某主控平台第一次支持UHS-I卡时,就犯了这种错误。当时只看规格书把set_ios里电压切换寄存器写对了,但忽略了core层在mmc_set_signal_voltage中设置的电压切换等待时间,导致卡在进入高电压模式后直接死掉。后来参考core层标准实现加了重试机制,每次切换失败自动回退到默认电压档,问题才彻底解决。
4.3 get_cd、get_ro、execute_tuning:不显眼却致命
除了request和set_ios,get_cd(检查卡是否存在)、get_ro(检查写保护开关)、execute_tuning(执行信号调优)这几个回调也值得花时间关注。
get_ro很容易被写错。有些host驱动默认返回0表示“可写”,但core层的语义却是“只读标志为1时禁止调用写命令”。我见过一个驱动因为反了语义,导致系统始终认为写保护是打开的,几乎所有写请求都返回WP_VIOLATION。排查到最后居然是一个return 1和return 0的问题,实在让人哭笑不得。
execute_tuning则是UHS/HS200/HS400模式下的关键回调。Tuning的过程简单说就是发送一串CMD19/CMD21的调谐块,调整采样点,直到信号能稳定接收为止。很多卡在低温或特定PCB走线较长时,tuning会失败或者tuning到错误的采样点,这个时候日志里会看到频繁的Data CRC错误。core层提供了一个mmc_execute_tuning的入口,调试时可以在host驱动里加打印,也可以直接看/sys/kernel/debug/mmc0/的tuning计数。
5. 顶层的枚举流程:从总线扫描到卡第一次亮出身份
5.1 mmc_rescan到底在忙什么
SD卡是热插拔外设,core层通过mmc_rescan来处理卡插入事件。mmc_rescan会先看host的card是否已经有对象,没有就尝试各种初始频率去探测总线。
它会依次尝试mmc_rescan_try_freq里预处理好的几个速度档位:从最慢的400kHz初始频率开始,逐步向更高频率探测。为什么从最慢开始?因为卡刚上电时对时钟频率有严格限制,太高频率的命令会被卡忽略或者引发紊乱。这个逻辑处理不当的host驱动,会造成“换个卡就能识别,换另一个品牌的卡就不识别”的诡异现象。
当core层在最低频发出了CMD0并收到响应后,它会调用mmc_attach_mmc或者mmc_attach_sd来建立卡的协议连接。根据卡对CMD8/ACMD41的应答不同,core层最终会认定这是一张SD卡还是一个MMC/eMMC设备,并决定采用哪套初始化流程。这一步很重要,因为很多人以为/dev/mmcblk0是“SD卡设备”,其实eMMC、SD卡在core层走的是不同分支。
5.2 枚举过程中的关键命令序列
标准的SD卡枚举序列大概是这样的:
CMD0:让卡进入idle状态。CMD8:发送接口条件,确认卡是否支持高容量(SDHC/SDXC)。ACMD41:用SD_OP_COND查询/激活卡的电压窗口。CMD2:读取卡的CID寄存器,获得唯一卡标识。CMD3:请求获取相对卡地址(RCA)。CMD9:读取卡CSD寄存器。CMD7:选中某张卡,进入transfer状态。
我建议你在阅读mmc_sd_init_card、mmc_init_card这些核心函数时,把上面这个命令序列牢牢记住。绝大多数识别问题,最终都能归结为命令序列中某一步的响应不符合预期。比如ACMD41的busy位一直在置位,说明卡还处于上电初始化阶段,core层会循环等待;如果循环超时,就会报Card did not respond to voltage select。
5.3 调试枚举问题时的正确姿势
遇到卡识别不稳定,我的习惯是先看dmesg开启mmc debug输出,然后依次确认:
mmc0: new SD card是否出现,没出现说明卡没完成枚举。- 如果出现但后续IO失败,再查看
ios状态里的时钟、电压、位宽是否符合预期。 - 多卡交叉验证,区分是“卡的问题”还是“时序问题”。
在实际调试时,我还喜欢直接在core层临时加点打印,比如在mmc_rescan_try_freq里打出每次尝试的频率和cmd结果。这种临时调试代码看起来“不入流”,但比用逻辑分析仪抓信号快速得多。定位到大致嫌疑后,再去抓波形确认,效率最高。
6. 一次块读请求在core层走过的路
6.1 从block层到core层:请求怎么被一步步包装
当应用层发出read()系统调用之后,文件系统会生成块设备IO请求,经过mmc_blk层进入core。mmc_blk_rw_rq_prep函数会把一个struct request转换成core层可识别的mmc_request,里面包含了mmc_command(比如CMD17读单块或CMD18读多块)、mmc_data以及mmc_command用于结束(stop)的CMD12。
这一步说白了就是完成“通用块IO”到“SD总线硬件操作”的翻译。为什么特别强调CMD12?因为多块读传输结束后,SD卡需要一条stop命令来终止传输状态。如果你的host层驱动没有正确触发CMD12,卡就会一直停留在传输状态,下一次CMD18请求直接超时。这种问题很多体现在Request timeout日志中。
6.2 mmc_wait_for_req:同步等待背后其实有异步机制
mmc_wait_for_req是core层最常用的同步请求封装。它的内部实现是:将请求挂到host->mrq之后,唤醒一个工作队列,等待完成信号。很多初学者以为core层真的“阻塞”在那里,其实底层是利用了workqueue和complete机制实现的调度睡眠。
如果你深入看mmc_start_req和mmc_wait_for_req的源码,会发现它们还承担了“前一个请求的善后工作”:只有前一个请求通过mmc_request_done完成回调返回之后,下一个请求才会登记到host上。这种串行化设计保证了同一时刻只有一个mrq在处理。对于支持多块、支持CMD23(设置块计数)的卡,core层还会用到mmc_blk_prep_packed等手段做聚合。
6.3 错误重试与stop transmission机制
core层在命令失败时,会看情况执行错误恢复。比如写操作途中数据CRC出错,core会尝试发CMD12停止传输、发送CMD13查询卡状态,甚至执行mmc_blk_reset对整个host状态进行重置。这些策略没有写死在block层,而是由core层统一管理,这样才能确保任何host驱动都能套用一套通用的容错模板。
不过这套错误恢复也偶尔会“误伤”。我遇到过一张全盘性坏块的卡片,写失败后core层反复重置队列,导致系统卡顿。真正的解决方案是在block层提前判断坏块并隔离,而不是每次心疼core层反复重试。也就是说,core层的重试策略是“兜底”而不是“万灵药”。
7. 实战期最容易踩的坑与排查经验
7.1 别在request回调里做printk和耗时慢操作
调试时在request回调里加printk可能是最爽也是最危险的做法。一条大量数据读写请求可能每几十微秒到达一次,如果printk放在这里,console输出会瞬间霸占CPU,导致core层谁都无法继续调度,最后变成看火焰图时一片耸立的中断风暴。
建议优先使用ftrace、perf和内核自带的mmc debugfs接口,不要用暴力打印。就算一定要用打印,也加个计数器,每1000次只打一次。这句叮嘱是我在吞过无数苦头之后才总结出来的。
7.2 卡识别不稳定先查voltage switching,别乱调卡检测引脚
很多人以为“识别不到卡”就是get_cd的状态不对,三板斧换gpio、改设备树。但在UHS-I SD卡上,识别失败很多时候是由电压切换引起的。core层在mmc_attach_sd过程中发现卡支持SDR50或SDR104时,会触发电压切换,从3.3V切换到1.8V。如果host的电源芯片没有真正完成切换,卡在后续时钟频率拉升时会直接“失聪”。
我调试过一块板子,每次冷启动后第一次识别失败,重新modprobe又好了。最后追踪下来,是电源芯片的电压切换完成信号没有在规格要求时间内拉高,core层判定切换失败,命令发出去就石沉大海。这个场景里改get_cd引脚根本无用,还是在core层看mmc_set_signal_voltage的返回值最靠谱。
7.3 用mmc-utils和debugfs对着core层做体检
最后分享一个我长期使用的调试组合拳。先在板子上用mmc extcsd read /dev/mmcblk0、mmc status get这类命令确认卡本身状态,再到/sys/kernel/debug/mmc0/目录下面看ios、clock、caps等信息,基本能把问题和core层的行为对上。
比如你怀疑卡工作在过高频率,那就看cat /sys/kernel/debug/mmc0/ios里clock字段是不是已经跑到200MHz;怀疑电压切换没生效,就看signal voltage字段。这套操作不需要打开驱动源码敲打印,比什么gdb都快。其实“调试驱动”很多时候并不是真的在调“驱动”,而是在调core层与硬件之间的“习惯匹配度”。把core层读透了,很多所谓疑难杂症会变得非常简单。
8. 最后再补充一点学习路线的建议
如果你打算认真把SD卡/SDIO驱动开发这条路走通,我不建议一开始就抱着某个vendor的host控制手册啃。正确顺序应该是先把mmc_ops.c、sd_ops.c、sdio_ops.c里那些命令构造函数通读一遍,再顺着mmc_rescan往上,看block层是怎么把一次文件系统读请求一路送到host控制器寄存器里的。
对我来说,读完core层之后最明显的感受是:以前看到[ 123.456789] mmc0: Timeout waiting for hardware interrupt这种日志会非常慌,现在反而很平静。因为我知道,这条日志在绝大多数时候只是core层某个重试机制被触发的表象,真正要查的是触发它之前的命令序列、电压状态以及host层回调的返回值。这就是core层知识带来的“解题直觉”。希望这篇能帮你省掉一些不必要的弯路。