深入解析SD卡协议栈与Linux MMC驱动实现
1. 项目缘起:从一张“坏掉”的SD卡说起
前几天,我手头一个用了好几年的树莓派项目突然挂了,系统启动不了。第一反应是SD卡坏了——这几乎是嵌入式开发者和硬件爱好者的日常。我习惯性地把卡插到电脑上,Windows弹出了熟悉的“需要格式化”提示。但作为一个有点“轴”的工程师,我不甘心就这么格式化掉里面可能还有救的数据和配置。于是,我尝试用了一些底层的磁盘工具去读取扇区,发现有些扇区能读,有些返回CRC错误。这个现象让我突然意识到,我对SD卡的理解,可能一直停留在“一个即插即用的存储块设备”这个层面,而对于它底层是如何与主机通信、协议栈如何工作、错误是如何被检测和上报的,几乎一无所知。
这就像开车多年,却不知道发动机和变速箱是怎么协同工作的一样。SD卡,这个在我们手机、相机、开发板上无处不在的小东西,其内部运作远比我们想象的要复杂。它不仅仅是一个简单的存储芯片,而是一个集成了控制器、闪存管理和完整通信协议栈的微型计算机系统。我们通过文件系统(如FAT32、exFAT)操作它,但实际上,文件系统的命令(如打开、读写文件)需要经过操作系统驱动、主机控制器、SD物理协议等多层转换,最终才能变成电信号与卡内的控制器对话。
因此,我决定放下手头的“救砖”工作,先彻底搞懂SD协议本身。我找来了SD Association发布的物理层规范(Physical Layer Specification),并结合开源的Linux内核SD卡驱动(位于drivers/mmc/host/和drivers/mmc/core/)进行了一次深入的源码走读。这个过程不仅解答了我关于CRC错误的疑惑,更让我对整个存储栈的理解加深了一个维度。这篇文章,就是我这次探索的笔记和总结,我会带你从硬件信号开始,一步步向上,拆解SD协议栈的每一层,并分析Linux内核中对应的源码实现。无论你是正在调试SD卡驱动的嵌入式工程师,还是对硬件通信协议感兴趣的好奇者,相信都能有所收获。
2. SD协议栈全景:从物理引脚到应用命令
在深入代码之前,我们必须先建立SD协议栈的宏观视图。SD卡的通信是一个典型的分层模型,这与网络协议栈(如TCP/IP)的思想异曲同工。每一层负责特定的功能,下层为上层提供服务。理解这个分层,是读懂源码的关键。
2.1 协议栈的四层模型
一个完整的SD主机(比如你的手机主板或树莓派SoC)与SD卡之间的通信,可以划分为以下四个层次:
物理层(Physical Layer):这是最底层,定义了电气特性、引脚定义、时钟、电压和最基本的信号传输单元——命令(CMD)和数据(DAT)线。SD卡有SD和SPI两种模式,它们的物理层连接方式不同。我们常说的SDIO模式,就是基于SD物理层,用于连接Wi-Fi、蓝牙等IO设备。这一层在硬件上由主机的SD Host Controller和卡端的接口电路实现。
数据链路层(Data Link Layer):这一层负责在物理层提供的比特流基础上,构建可靠的帧传输。它的核心职责包括:
- 帧封装:为上层传来的命令和数据包添加起始位、CRC校验码和结束位,构成一个完整的传输帧。
- CRC校验与错误检测:发送方计算CRC,接收方验证CRC。这就是我遇到的“CRC错误”产生的地方。如果校验失败,接收方会通过设置状态位或直接拉低DAT线(在SD模式下)来报告错误。
- 流控与应答:通过特定的令牌(Token)和响应(Response)机制,确保命令和数据的同步。例如,主机发送一个读命令后,卡会先回送一个数据令牌,然后才是数据块。
传输层(Transport Layer):这一层管理着具体的读写事务。它定义了数据块(Block)传输的规则。关键概念包括:
- 块大小(Block Size):通常为512字节,但可以通过命令配置。
- 单块/多块传输:支持一次读写单个或多个连续块。
- 流控制:在SD模式下,通过DAT线上的“忙”信号来指示卡是否准备好接收或发送数据。
应用层(Application Layer):这是最上层,定义了主机与SD卡控制器“对话”的语义。核心是一套应用命令(Application Command),也就是我们常说的ACMD。例如:
ACMD41:发送主机容量支持信息,并获取卡的OCR(Operating Conditions Register)寄存器状态,用于完成卡的初始化。ACMD51:获取SCR(SD Configuration Register)寄存器,其中包含了卡是否支持高速度、是否支持CMD23(预定义多块传输)等重要信息。- 此外,像
CMD16(设置块大小)、CMD17/18(读单块/多块)、CMD24/25(写单块/多块)等,也属于应用层命令,它们直接表达了主机的意图。
这四层协议,在Linux内核的MMC/SD子系统中有着清晰的映射。物理层和数据链路层主要由Host Controller驱动(如sdhci-pci,sdhci-esdhc-imx等)负责,它操作具体的硬件寄存器来控制时钟、发送CMD、读写DAT。而传输层和应用层,则由MMC Core层(drivers/mmc/core/)来实现,它提供了一套统一的API,处理命令的构造、响应解析、错误重试、块设备请求的队列管理等。
2.2 SD模式 vs SPI模式:两种不同的“方言”
SD卡支持两种通信模式,这主要是在物理层和数据链路层有区别:
SD模式(默认、高速):
- 使用6线制:CLK(时钟)、CMD(命令/响应)、DAT0-DAT3(4条数据线,可并行传输)。
- 通信是全双工的,命令和数据有独立的通道。
- 协议复杂,但性能高,支持4位宽总线(High Speed)甚至更高速的UHS模式。
- 初始化流程相对复杂,需要经历卡识别模式(Identification Mode)和传输模式(Transfer Mode)的切换。
SPI模式(兼容、简单):
- 使用4线制:CS(片选)、CLK(时钟)、MOSI(主机出从机入)、MISO(主机入从机出)。
- 通信是半双工的,命令和数据共享同一对数据线。
- 协议简单,很多低端单片机(如STM32的SPI接口)都支持,易于实现。
- 性能较低,是SD模式的一种“兼容模式”,并非SD协议原生设计,但被广泛支持。
在Linux驱动中,模式的选择通常在Host Controller驱动初始化时确定。对于像STM32这类内置SDIO控制器但用SPI模式驱动的场景,实际上是通过软件模拟SPI时序,或者控制器本身支持将SDIO接口配置为SPI模式来工作的。
3. 深入Linux MMC子系统源码
有了协议栈的概念,我们打开Linux内核源码(以5.x版本为例),看看这些理论是如何落地的。MMC子系统是SD、MMC、eMMC等设备驱动的核心框架。
3.1 核心数据结构:struct mmc_host,struct mmc_card,struct mmc_command
驱动围绕几个核心结构体运转:
struct mmc_host:代表一个SD/MMC主机控制器。每个SoC上的SDIO控制器都会实例化一个mmc_host。它包含了控制器能力(如是否支持DMA、最大时钟频率)、操作函数集(struct mmc_host_ops,由具体Host驱动实现)、以及当前连接的卡(struct mmc_card)等信息。你可以把它理解为一个“SD读卡器硬件”的软件抽象。// 简化版,展示关键字段 struct mmc_host { struct device *parent; struct mmc_host_ops *ops; // 硬件操作函数集 unsigned int f_min, f_max; // 时钟频率范围 u32 ocr_avail; // 支持的电压范围 struct mmc_card *card; // 当前插入的卡 unsigned int caps; // 主机能力标志,如 MMC_CAP_4_BIT_DATA // ... 队列、锁、状态等大量管理字段 };struct mmc_card:代表一张插入的SD卡。它包含了从卡中读取的永久性信息,如CID(Card Identification Register,卡标识)、CSD(Card Specific Data Register,卡特定数据,包含容量、块大小、读写速度等信息)、SCR(SD配置寄存器)等。它还包含当前的操作状态,如当前电压、时钟频率、总线宽度等。struct mmc_card { struct mmc_host *host; // 所属主机 u32 ocr; // 操作条件寄存器(Operation Conditions Register) u32 cid[4]; // 卡标识 u32 csd[4]; // 卡特定数据 u32 scr[2]; // SD配置寄存器(仅SD卡有) unsigned int sd_bus_speed; // 当前SD总线速度 // ... 其他字段 };struct mmc_command:代表一次即将发送或已经完成的命令。它是主机与卡之间一次交互的载体。struct mmc_command { u32 opcode; // 命令码,如 CMD0, CMD2, ACMD41 u32 arg; // 命令参数 u32 resp[4]; // 响应数据(SD命令响应最长136位,存于4个32位整数) unsigned int flags; // 标志位,如 MMC_RSP_PRESENT(有响应), MMC_RSP_CRC(需要CRC校验) int error; // 命令执行错误码 // ... 数据指针、忙等待等字段 };当驱动需要发送一个命令时(比如初始化时发送
CMD8检查电压),它会填充一个mmc_command结构体,然后通过mmc_wait_for_req()等函数提交给Host驱动执行。
3.2 卡初始化流程源码走读
卡的初始化是协议交互最集中的体现。我们跟踪drivers/mmc/core/sd.c中的mmc_attach_sd()函数,这是SD卡初始化的入口。
上电与卡识别模式:主机控制器给卡上电,并设置一个低速时钟(通常<400kHz)。此时卡处于卡识别模式(Identification Mode),只响应部分基础命令(
CMD0,CMD8,CMD5等)。主机发送CMD0(GO_IDLE_STATE)让卡复位到空闲状态。发送
CMD8(SEND_IF_COND):这是一个非常重要的命令,用于验证卡是否支持SDHC/SDXC(高容量卡)以及主机提供的电压(2.7-3.6V)是否被接受。命令参数中包含了主机支持的电压模式和检查模式。如果卡支持,它会回送一个包含相同电压信息的响应。如果卡不支持CMD8(比如老式的SD卡),它不会响应,主机会根据超时来判断。发送
ACMD41(SD_SEND_OP_COND):这是初始化过程中最关键的一步。主机通过CMD55(APP_CMD)前缀告知卡,下一个命令是应用命令,然后发送ACMD41。ACMD41的参数包含了主机支持的高容量(HCS)标志和电压范围。卡在响应中,通过OCR寄存器的第31位(忙位,Busy)来告知初始化是否完成。主机需要轮询发送ACMD41,直到忙位被置为1。这个过程在源码中体现在一个循环里:// 简化逻辑 do { err = mmc_send_app_op_cond(host, ocr, &rocr); if (err) break; // 检查响应中的忙位 (rocr & MMC_CARD_BUSY) if (rocr & MMC_CARD_BUSY) break; // 等待一段时间再重试 mmc_delay(10); } while (time_before(jiffies, timeout));只有忙位置1,卡才真正准备好进入下一步。同时,卡也会在响应中设置HCS位,告知主机它是否是高容量卡(SDHC/SDXC)。
获取CID(
CMD2)和RCA(CMD3):初始化完成后,主机发送CMD2(ALL_SEND_CID)请求所有卡发送它们的CID寄存器(全球唯一标识)。然后主机为每张卡(在有多张卡的情况下)分配一个相对卡地址(RCA),并通过CMD3(SET_RELATIVE_ADDR)发送给卡。此后,通信就使用这个RCA地址,而不是广播。切换到传输模式:发送
CMD7(SELECT_CARD)并带上RCA,将选中的卡从识别模式切换到传输模式(Transfer Mode)。在此模式下,卡可以执行数据读写命令。获取CSD和SCR:主机发送
CMD9(SEND_CSD)获取卡的CSD寄存器,从中解析出容量、块大小、最大读写速度等关键信息。对于SD卡,还需要发送ACMD51(SEND_SCR)来获取SCR寄存器,以确定卡是否支持4位总线宽度、高速模式等高级特性。配置总线宽度和速度:根据SCR中的信息,主机可以通过
ACMD6(SET_BUS_WIDTH)将数据总线从默认的1位切换到4位(如果支持)。同时,主机控制器可以将时钟频率从初始化的低速提升到卡支持的最高速度(如25MHz、50MHz,甚至UHS模式的更高频率)。
整个初始化流程,在源码中是一系列条件判断和状态机跳转,完美对应了SD协议规范中定义的步骤。任何一个命令失败或响应不符合预期,都会导致初始化失败,这就是为什么有些卡在某些设备上无法识别的原因之一——可能是某个协商环节(如电压、总线模式)没有达成一致。
3.3 数据读写流程与块设备请求处理
初始化完成后,SD卡就作为一个块设备(/dev/mmcblk0)暴露给操作系统。当用户空间程序执行read()/write()系统调用时,请求最终会以struct bio的形式到达MMC子系统。
请求入队:MMC Core层提供了一个请求队列(
struct request_queue)。块层发来的读写请求(struct request)会被放入这个队列。每个请求可能包含多个连续的扇区(LBA,逻辑块地址)。请求预处理:MMC Core的队列线程(通常由
mmc_queue_thread()处理)会从队列中取出请求,并将其转换为一个或多个struct mmc_request。一个mmc_request包含一个struct mmc_command(用于发送读命令CMD17/18或写命令CMD24/25)和一个struct mmc_data(用于描述要传输的数据缓冲区、长度、方向等)。命令与数据传输:这个
mmc_request被提交给Host驱动(通过host->ops->request(host, mrq))。Host驱动负责:- 将命令码和参数写入控制器的命令寄存器。
- 配置DMA(如果支持)或准备PIO缓冲区。
- 启动传输,等待控制器产生完成中断或轮询状态位。
- 在传输过程中,处理CRC错误、超时等异常情况。
多块传输优化:为了提高效率,对于连续的多个块,应使用多块读写命令(
CMD18/25)。在发送多块读命令前,还可以先发送CMD23(SET_BLOCK_COUNT)来预定义要传输的块数,这有助于卡优化内部操作。是否支持CMD23,是在初始化阶段通过SCR寄存器获知的。完成与回调:传输完成后,Host驱动设置
mmc_request的完成状态,并唤醒等待的线程。MMC Core层检查结果,如果成功,则完成块设备层的请求;如果失败(例如CRC错误),则可能进行重试(retry)。重试逻辑是存储驱动稳定性的关键,过于激进的重试会降低性能,过于保守则可能导致本可恢复的错误被上报。
注意:CRC错误的处理。在数据链路层,每个命令帧和数据块都带有CRC校验码。如果Host控制器检测到CRC错误,它通常会在其状态寄存器中设置错误标志,并通过中断通知驱动。在Linux驱动中,这通常会触发错误处理路径(
mmc_error_log())。对于可恢复的错误(如偶尔的干扰),驱动可能会重试整个命令。但如果连续失败,则可能将卡标记为错误状态。我最初遇到的“部分扇区CRC错误”,很可能是因为SD卡闪存单元的某些块老化或损坏,导致存储的数据本身出错,控制器在读取时计算出的CRC与存储的CRC不匹配。
4. 实战调试:常见问题与源码级排查思路
理解了协议和源码,我们就可以像侦探一样,对SD卡相关的问题进行深度排查了。以下结合几个常见问题场景进行分析。
4.1 问题一:SD卡初始化失败(“无法识别”)
这是最头疼的问题之一。根据协议栈,我们可以分层排查:
物理层检查:
- 电压:用万用表测量VDD引脚电压是否在2.7-3.6V范围内?电压不稳或过低是常见原因。在源码中,主机驱动的
ocr_avail字段定义了支持的电压,初始化时ACMD41的参数会包含这个信息。 - 时钟:初始化早期时钟频率必须低于400kHz。检查Host驱动中初始时钟频率的设置(
host->f_min)。时钟信号是否干净?可以用示波器查看CLK引脚。 - 连接:检查焊点、插座是否接触良好。DAT0线是必须连接的,即使在1位模式下。CMD线是命令通道,更不能有问题。
- 电压:用万用表测量VDD引脚电压是否在2.7-3.6V范围内?电压不稳或过低是常见原因。在源码中,主机驱动的
协议交互分析(最有效的手段):
- 启用Linux内核的MMC调试日志:
echo 8 > /sys/module/mmc_core/parameters/debug。这会在内核日志(dmesg)中打印出所有发送的命令、参数和响应。 - 观察日志,看初始化流程卡在哪一步。
- 如果根本没有
CMD8的发送记录,可能是Host驱动配置不支持SDHC,或者卡处于某种异常状态。 - 如果
ACMD41的响应中忙位一直不为1,可能是卡本身有问题,或者电压不匹配。 - 如果
CMD9(获取CSD)失败,可能是通信已经建立,但卡内部状态异常。
- 如果根本没有
- 启用Linux内核的MMC调试日志:
源码对照:对照
mmc_attach_sd()函数,结合打印的日志,看是在哪个if (err)判断处失败的。错误码err的值(如-EIO,-ETIMEDOUT)能给出方向。
4.2 问题二:读写不稳定,偶尔出现I/O错误
这种问题通常在传输模式下出现。
- 电气干扰:长导线、劣质卡托、主板电源噪声都可能导致高速数据传输时出现位错误,从而引发CRC错误。尝试降低总线速度(可以在Host驱动中临时限制
host->f_max),看问题是否消失。 - 电源带载能力不足:SD卡在写入时,尤其是闪存擦除操作,瞬时电流较大。电源纹波过大会导致卡内部控制器复位或通信失败。确保电源电路有足够的电容滤波。
- 驱动配置问题:
- 总线宽度:如果配置了4位模式但硬件连接只有1位,必然失败。检查
host->caps是否包含MMC_CAP_4_BIT_DATA,以及卡在SCR寄存器中是否报告支持4位。 - 信号时序:高速模式下,信号建立时间和保持时间很关键。有些Host驱动提供可调的延迟配置(如DCM延迟)。对于特定板卡和特定卡,可能需要微调这些参数。相关代码通常在Host驱动的
->set_ios()回调函数中。
- 总线宽度:如果配置了4位模式但硬件连接只有1位,必然失败。检查
- 卡本身质量问题:使用
badblocks或f3等工具对卡进行全盘读写测试。如果坏块集中在某些区域,可能就是卡寿命将至。SD卡控制器会屏蔽坏块,但过多坏块会导致性能骤降和频繁错误。
4.3 问题三:SPI模式下的特殊问题
在单片机等资源受限环境中常用SPI模式。
- 片选(CS)信号管理:SPI协议要求,在一次完整的命令-响应事务期间,CS信号必须保持有效(低电平)。如果CS在数据传输中途被意外拉高,卡会认为事务结束,导致数据丢失。确保你的SPI驱动在发送命令和接收数据时,持有CS锁。
- 命令响应格式:在SPI模式下,命令响应是单字节的(如
0x01代表空闲状态),而不是SD模式下的多位响应。发送CMD0后应收到0x01。如果收到0xff(无响应),检查接线和卡是否上电。 - 初始化顺序差异:SPI模式的初始化命令序列与SD模式略有不同。例如,在发送
ACMD41之前,需要先发送CMD59(CRC_ON_OFF)来关闭CRC校验(参数为0),因为很多SPI实现不处理CRC。务必参考SD Physical Layer Spec中关于SPI模式的章节。
5. 进阶:从协议理解到性能优化与定制
当你掌握了协议和驱动框架后,就可以做一些更有意思的事情了。
5.1 性能调优点分析
- 时钟频率:这是最直接的杠杆。在卡和主机都支持的范围内,尽可能提高
host->clock。注意,初始化阶段和传输阶段可以使用不同频率。 - 总线宽度:将1位模式切换到4位模式,理论带宽提升4倍。确保硬件连接正确,并在初始化后正确发送
ACMD6。 - 多块传输与预定义:始终使用多块读写命令处理连续扇区。如果卡支持
CMD23(通过SCR获知),务必使用它来预定义块数,这可以减少命令交互开销。 - DMA vs PIO:使用DMA可以解放CPU,提升系统整体性能。检查Host驱动是否配置并正确使用了DMA。在
struct mmc_host的caps中,MMC_CAP_SDIO_IRQ等标志也与中断和DMA使用相关。 - 命令队列:更高级的eMMC和SD Express标准支持命令队列(Command Queue),允许主机发送多个命令后,由卡内部优化执行顺序。这需要主机控制器硬件和驱动支持。
5.2 实现一个简单的SD卡驱动骨架
如果你想在裸机或RTOS上实现一个最简SD卡驱动,流程可以高度简化,但必须包含以下核心步骤(以SPI模式为例):
- 硬件初始化:配置MCU的SPI外设为模式0(CPOL=0, CPHA=0),低速(如100-400kHz),MSB先行。
- 卡上电与复位:控制电源电路(如果有),然后发送至少74个时钟脉冲(只发CLK,不选片),接着拉低CS,发送
CMD0(0x40)进入SPI模式。 - 初始化循环:发送
CMD8(可选,用于检查)、CMD59(关闭CRC)、然后循环发送ACMD41(0x69)直到响应非0x01(表示初始化完成)。 - 读取容量信息:发送
CMD9读取CSD寄存器,解析其中的C_SIZE,READ_BL_LEN等字段,计算卡容量。 - 设置块大小:发送
CMD16,参数为512(或其他你想要的块大小)。 - 读写扇区:
- 读:发送
CMD17(单块)或CMD18(多块),参数为扇区地址(LBA)。等待卡返回数据起始令牌0xFE,然后接收512字节数据和2字节CRC。发送CMD12停止多块读。 - 写:发送
CMD24或CMD25,接着发送数据起始令牌0xFE、512字节数据、2字节CRC。卡会返回一个数据响应令牌,之后会持续拉低MISO线(忙状态),直到写入完成。
- 读:发送
这个骨架忽略了错误处理、多卡支持、高速模式切换等复杂内容,但足以让你理解协议栈是如何在底层一步步搭建起来的。每一步都对应着向特定的命令线(CMD)或数据线(DAT)发送特定的比特序列,而Linux内核驱动则用精妙的抽象和状态机封装了所有这些细节。
回过头来看我最初那张“坏掉”的SD卡,通过底层工具读取,发现CSD寄存器中的部分参数已经异常,这意味着卡内的控制器可能已经无法正确管理闪存阵列。协议栈层面的通信或许是正常的(能响应CMD),但应用层的数据已经不可靠。这次深入的协议与源码分析,虽然没有直接救回数据,却让我彻底明白了从fopen到闪存单元之间发生的所有故事。下次再遇到类似问题,我至少知道该从哪里入手,该观察哪些信号,该分析哪段日志。这或许就是底层技术的魅力所在:它不能解决所有问题,但能给你解决问题的清晰地图和可靠工具。
