深入解析SD卡协议栈与Linux MMC驱动实现

发布时间:2026/7/30 10:49:50
深入解析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发送主机容量支持信息并获取卡的OCROperating Conditions Register寄存器状态用于完成卡的初始化。ACMD51获取SCRSD 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-DAT34条数据线可并行传输。通信是全双工的命令和数据有独立的通道。协议复杂但性能高支持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卡。它包含了从卡中读取的永久性信息如CIDCard Identification Register卡标识、CSDCard Specific Data Register卡特定数据包含容量、块大小、读写速度等信息、SCRSD配置寄存器等。它还包含当前的操作状态如当前电压、时钟频率、总线宽度等。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等。主机发送CMD0GO_IDLE_STATE让卡复位到空闲状态。发送CMD8SEND_IF_COND这是一个非常重要的命令用于验证卡是否支持SDHC/SDXC高容量卡以及主机提供的电压2.7-3.6V是否被接受。命令参数中包含了主机支持的电压模式和检查模式。如果卡支持它会回送一个包含相同电压信息的响应。如果卡不支持CMD8比如老式的SD卡它不会响应主机会根据超时来判断。发送ACMD41SD_SEND_OP_COND这是初始化过程中最关键的一步。主机通过CMD55APP_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。获取CIDCMD2和RCACMD3初始化完成后主机发送CMD2ALL_SEND_CID请求所有卡发送它们的CID寄存器全球唯一标识。然后主机为每张卡在有多张卡的情况下分配一个相对卡地址RCA并通过CMD3SET_RELATIVE_ADDR发送给卡。此后通信就使用这个RCA地址而不是广播。切换到传输模式发送CMD7SELECT_CARD并带上RCA将选中的卡从识别模式切换到传输模式Transfer Mode。在此模式下卡可以执行数据读写命令。获取CSD和SCR主机发送CMD9SEND_CSD获取卡的CSD寄存器从中解析出容量、块大小、最大读写速度等关键信息。对于SD卡还需要发送ACMD51SEND_SCR来获取SCR寄存器以确定卡是否支持4位总线宽度、高速模式等高级特性。配置总线宽度和速度根据SCR中的信息主机可以通过ACMD6SET_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。在发送多块读命令前还可以先发送CMD23SET_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线是命令通道更不能有问题。协议交互分析最有效的手段启用Linux内核的MMC调试日志echo 8 /sys/module/mmc_core/parameters/debug。这会在内核日志dmesg中打印出所有发送的命令、参数和响应。观察日志看初始化流程卡在哪一步。如果根本没有CMD8的发送记录可能是Host驱动配置不支持SDHC或者卡处于某种异常状态。如果ACMD41的响应中忙位一直不为1可能是卡本身有问题或者电压不匹配。如果CMD9获取CSD失败可能是通信已经建立但卡内部状态异常。源码对照对照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()回调函数中。卡本身质量问题使用badblocks或f3等工具对卡进行全盘读写测试。如果坏块集中在某些区域可能就是卡寿命将至。SD卡控制器会屏蔽坏块但过多坏块会导致性能骤降和频繁错误。4.3 问题三SPI模式下的特殊问题在单片机等资源受限环境中常用SPI模式。片选CS信号管理SPI协议要求在一次完整的命令-响应事务期间CS信号必须保持有效低电平。如果CS在数据传输中途被意外拉高卡会认为事务结束导致数据丢失。确保你的SPI驱动在发送命令和接收数据时持有CS锁。命令响应格式在SPI模式下命令响应是单字节的如0x01代表空闲状态而不是SD模式下的多位响应。发送CMD0后应收到0x01。如果收到0xff无响应检查接线和卡是否上电。初始化顺序差异SPI模式的初始化命令序列与SD模式略有不同。例如在发送ACMD41之前需要先发送CMD59CRC_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外设为模式0CPOL0 CPHA0低速如100-400kHzMSB先行。卡上电与复位控制电源电路如果有然后发送至少74个时钟脉冲只发CLK不选片接着拉低CS发送CMD00x40进入SPI模式。初始化循环发送CMD8可选用于检查、CMD59关闭CRC、然后循环发送ACMD410x69直到响应非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到闪存单元之间发生的所有故事。下次再遇到类似问题我至少知道该从哪里入手该观察哪些信号该分析哪段日志。这或许就是底层技术的魅力所在它不能解决所有问题但能给你解决问题的清晰地图和可靠工具。