嵌入式SD/MMC控制器中断与DMA驱动开发实战解析
1. 项目概述与核心价值在嵌入式系统里尤其是那些需要频繁读写存储卡的应用场景比如工业数据采集、车载记录仪或者智能摄像头CPU要是总被数据搬运这种“体力活”占着那真正处理业务的“脑力活”就干不成了。这时候MMC/SD/SDIO控制器里的中断驱动和DMA直接内存访问机制就成了提升系统效率和实时性的关键法宝。简单说中断是让外设“有事就喊”DMA是让数据“自己跑腿”目的都是把CPU解放出来。最近在调TI的AM263P芯片它的MMC/SD/SDIO控制器设计得相当典型和精细寄存器层面的控制逻辑清晰但手册读起来容易陷入细节。我结合实际的驱动开发经验把其中关于中断处理流程、DMA响应模式以及核心的缓冲区管理机制这几个最硬核、也最容易踩坑的部分掰开揉碎了讲清楚。你会发现理解了控制器怎么通过MMC_STAT和MMC_IE寄存器玩转中断怎么通过SDMARREQN和SDMAWREQN信号与DMA控制器握手以及数据在缓冲区里如何“乒乓”运作不仅能写出更稳健的驱动还能在系统性能调优时心里更有底。这篇文章适合正在或即将进行嵌入式存储接口开发的工程师特别是使用TI Sitara系列或类似ARM核芯片的朋友。我会尽量用代码片段和时序图来解释寄存器操作避开纯理论堆砌聚焦在“怎么用”和“为什么这么用”上。2. 中断驱动操作从使能到处理的完整链条中断驱动的核心思想是变“轮询等待”为“事件通知”。对于MMC/SD/SDIO控制器这涉及到中断源的使能、事件状态的识别和清除这一套标准动作。手册里流程写得清楚但实际写代码时有几个顺序和细节问题一不留神就会导致中断不触发或者无法清除陷入死循环。2.1 中断使能与事件识别控制器内部有很多可以触发中断的事件比如命令完成CC、数据传输完成TC、缓冲区就绪BRR/BWR、以及各种错误CTO, DCRC等。并不是所有事件都会自动产生中断信号到CPU你需要先“订阅”它们。第一步是在MMC_IE寄存器里给关心的事件对应的位写1。比如你想在命令完成时收到中断就设置MMC_IE[0] CC_ENABLE 1。这里有个关键点必须在启动任何数据传输命令之前就配置好中断使能寄存器。如果先发了CMD再使能中断那么已经完成的事件可能不会补发中断信号。当一个使能了的事件发生时控制器的单根中断输出线连接到CPU的IRQ会被拉低断言。这时你的中断服务程序被调用。ISR里第一件也是最重要的事就是读取MMC_STAT寄存器。这个寄存器是个状态“快照”告诉你具体是哪个或哪些事件触发了这次中断。注意MMC_STAT寄存器是“读-清除”型的。但这里有两个特例卡中断CIRQ和错误中断ERRI。对于CIRQ位8它来自SDIO卡你不能直接写1清除它。正确做法是先在MMC_IE寄存器里屏蔽掉卡中断CIRQ_ENABLE位清0然后去操作SDIO卡本身的CCCR寄存器来清除中断源。对于ERRI位15它是一个聚合标志只有当MMC_STAT[31:16]这些具体的错误状态位都被清除后它才会自动清零。2.2 中断状态的清除与后续处理对于大多数普通事件如CC, TC, BRR等清除中断状态的流程是标准的读MMC_STAT确定中断来源。写MMC_STAT向对应事件位写1清除该状态位。这个操作同时会释放中断线如果该事件是当前唯一未处理的中断源。如果你清除后再读MMC_STAT对应位应该是0。这里有个非常重要的实操心得在清除状态位前最好把MMC_STAT的值保存到一个局部变量里。因为寄存器可能同时有多个位被置起你读一次之后如果直接写回这个值去清除可能会误清除掉在你“读”和“写”之间新发生的事件状态。更安全的做法是只清除你检测到的那几位。例如uint32_t status MMC_STAT; // 读取状态快照 if (status MMC_STAT_CC) { // 处理命令完成... MMC_STAT MMC_STAT_CC; // 仅清除CC位 } if (status MMC_STAT_TC) { // 处理传输完成... MMC_STAT MMC_STAT_TC; // 仅清除TC位 }中断处理完后通常需要根据事件类型进行后续操作。如果是命令完成可能需要解析响应寄存器MMC_RSPxx如果是缓冲区读就绪则需要从MMC_DATA寄存器读取数据。2.3 轮询模式作为备选方案当中断被禁用MMC_IE中对应位为0时控制器不会断言中断线。这时软件可以通过轮询MMC_STAT寄存器来检测事件。轮询模式虽然增加CPU开销但在一些对延迟要求极其苛刻、或者调试初期不想处理复杂中断嵌套的场景下它简单可靠。轮询时检测到状态位置1后同样需要写1清除它。但请注意在轮询模式下清除状态位不会影响中断线的状态因为中断本来就没使能。清除状态位主要是为了下一次轮询时能正确识别新的事件。3. DMA响应模式让数据搬运自动化如果说中断是把CPU从“等待”中解放出来那么DMA就是进一步把CPU从“搬运”中解放出来。AM263P的MMC/SD/SDIO控制器只支持DMA响应者模式。这意味着控制器本身不发起DMA传输而是作为DMA控制器的“从设备”通过请求信号告诉DMA控制器“我这里有数据要收”或者“我这里可以发数据了”。3.1 DMA模式的工作原理与使能条件控制器通过两根独立的信号线与DMA控制器交互SDMARREQNDMA读请求。当控制器从卡收到一个完整的数据块并存入缓冲区后会拉低此信号向DMA控制器请求“快来把数据读走”。SDMAWREQNDMA写请求。当控制器的发送缓冲区有足够空间容纳一个完整数据块时会拉低此信号向DMA控制器请求“快把要发送的数据写进来”。要使能DMA传输必须满足几个条件在发送数据传输命令如CMD17读单块CMD18读多块CMD24/25写块之前设置MMC_CMD[0] DE位为1。这个位是DMA使能开关必须在命令发出前打开。命令必须在MMC_CMD线上成功发出。对于接收读卡控制器缓冲区必须有足够空间一个块的长度由MMC_BLK[10:0] BLEN定义来接收下一个完整块。对于发送写卡缓冲区必须有足够空间接收来自DMA的一个完整块数据。3.2 DMA接收模式详解以从SD卡读取数据到系统内存为例。假设我们设置块长度BLEN512字节。控制器从卡上接收数据攒满512字节放入内部缓冲区。控制器断言SDMARREQN信号拉低向DMA控制器发出请求。DMA控制器开始从MMC控制器的MMC_DATA寄存器地址读取数据。关键点来了SDMARREQN信号在DMA控制器读取了第一个字32位后就会被释放拉高。这意味着每个数据块只产生一次DMA请求。DMA控制器可以以单次1-shot或突发burst模式读取剩余数据。它必须自己根据BLEN值计算需要读多少次。因为DMA访问是32位宽的所以总读取次数为Integer(BLEN / 4) 1。对于512字节就是128 1 129次32位读取。在DMA没有读完当前块的所有BLEN字节之前控制器内部会屏蔽新的DMA请求。即使卡又送来了一个新块只要缓冲区里旧块没被搬空就不会新的SDMARREQN。接收缓冲区设计上不会溢出。在多块传输且块大小较大时如果缓冲区快满了控制器会暂时停止提供给卡的时钟MMC_CLK直到DMA或CPU从缓冲区读走一个完整块时钟才会恢复。这是一种硬件流控机制保证了数据不会丢失。3.3 DMA发送模式详解以从系统内存写数据到SD卡为例。当控制器准备好接收一个数据块即发送缓冲区有至少BLEN字节的空闲空间时断言SDMAWREQN信号。DMA控制器开始向MMC_DATA寄存器写入数据。同样在写入第一个字后SDMAWREQN信号被释放。每个块也只产生一次写请求。DMA控制器需要负责写入完整BLEN字节的数据。如果DMA没有写满BLEN字节或者缓冲区空间不足以容纳下一个完整块新的DMA请求会被内部屏蔽。踩坑记录DMA传输的块大小BLEN必须与你在发送SD命令如CMD25时指定的块大小严格一致。有一次调试我命令里设的块大小是1024但DMA配置里误设成了512结果导致数据传输到一半就卡住了因为控制器一直在等DMA写满1024字节才往卡上发送而DMA早以为一个块结束了。排查了半天才发现是这两个地方不匹配。4. 缓冲区管理性能与稳定的核心数据缓冲区是这个控制器的“心脏”它负责在系统总线L4 Interconnect和卡总线之间进行速度匹配和临时存储。理解它的工作模式对优化传输性能、避免数据错误至关重要。4.1 缓冲区结构与访问规则控制器内部有一个主数据缓冲区辅以一个预取寄存器和一个后写缓冲区来提升性能。这两个辅助缓冲区对主机不可见。MMC_DATA寄存器是访问这个缓冲体系的唯一入口。这里有个反直觉但非常重要的点连续对MMC_DATA进行一次写和一次读你读到的不一定是你刚才写入的数据。因为写入操作是进入后写缓冲区而读取操作是从预取寄存器取数据。它们不是同一个物理存储单元。这在编程时需要特别注意不要依赖这种读写顺序来做数据校验。访问MMC_DATA寄存器有严格的状态限制读操作必须在缓冲区读使能状态MMC_PSTATE[11] BRE为1时进行否则会触发坏访问错误MMC_STAT[29] BADA。写操作必须在缓冲区写使能状态MMC_PSTATE[10] BWE为1时进行否则同样触发BADA错误且数据不会被写入。4.2 双缓冲与单缓冲模式这是缓冲区管理最精妙的部分直接影响了传输能否流水线化和最大吞吐量。模式的选择不是由软件配置的而是由要传输的数据块大小BLEN和缓冲区总大小的一半MEM_SIZE/2自动决定的。双缓冲模式BLEN MEM_SIZE/2当要传输的块大小小于等于缓冲区总容量的一半时控制器启用双缓冲。它将缓冲区划分为A、B两个部分Portion A/B以乒乓ping-pong方式工作。场景从卡读数据到系统。过程当DMA正在从A部分读取上一个块的数据时卡可以同时向B部分写入当前块的数据。当DMA读完A卡也写完B时角色互换DMA去读B卡去写A。优势实现了读和写的完全重叠隐藏了访问延迟最大化利用了总线带宽。这对于连续的多块传输性能提升巨大。单缓冲模式BLEN MEM_SIZE/2当块大小超过缓冲区容量的一半时整个缓冲区作为一个整体使用。限制此时不能同时进行从总线到缓冲区和从缓冲区到总线的传输。必须等一个方向的操作完全结束后才能开始另一个方向的操作。风险如果软件错误地试图在两个方向同时操作会触发BADA错误。内存大小、块长度与缓冲模式的关系下表清晰地展示了它们之间的关系假设寄存器MMC_CAPA[17:16] MBL和MEMSIZE决定了硬件能力内存大小 (字节)支持的最大块长 (字节)启用双缓冲的块长条件启用单缓冲的块长条件512512N/A (整个缓冲区就是一个块)BLEN 51210241024BLEN 512512 BLEN 102420482048BLEN 10241024 BLEN 204840962048BLEN 2048N/A (块长不会超过2048)核心技巧在系统设计初期如果已知需要频繁进行大块连续数据传输如视频录制应尽量选择块大小BLEN在512或1024字节并确保控制器的缓冲区内存足够大如2048字节以上这样就能充分利用双缓冲获得接近理论峰值的传输速率。如果被迫使用大块如2048字节且缓冲区只有2048字节那么性能会下降因为无法流水。4.3 传输方向配置的致命细节在启动任何数据传输之前必须正确配置MMC_CMD[4] DDIR位来指明传输方向0表示主机写卡发送1表示主机读卡接收。这个配置必须在发起传输命令之前就设置好。我遇到过因为忘记设置DDIR导致写命令被当成读命令执行或者读回来的数据全为0的诡异问题。这是一个非常低级的错误但一旦发生却很难排查因为命令和状态看起来都正常。5. 传输控制、错误处理与高级功能掌握了中断、DMA和缓冲区就掌握了数据通路的骨干。但要构建一个健壮的驱动还需要处理好传输的启动、停止、错误以及一些特殊协议。5.1 命令、响应与传输状态控制器支持MMC、SD、SDIO三种卡协议。软件在初始化阶段通过发送特定的命令序列如CMD0, CMD8, ACMD41等来识别卡类型。不同类型的卡其命令集和响应格式略有不同。响应数据存储在MMC_RSP10到MMC_RSP76这一组寄存器中。手册中的表格对应Table 13-178清晰地列出了不同响应类型R1, R1b, R2, R3等在寄存器中的映射关系。例如R2响应长响应的128位数据分布在MMC_RSP76,MMC_RSP54,MMC_RSP32,MMC_RSP10这四个寄存器中。特别要注意Auto CMD12响应会被存到MMC_RSP76这是为了避免在多块传输并发执行命令时覆盖掉存储在MMC_RSP10中的主命令响应。5.2 详尽的错误检测与处理控制器的错误报告机制非常完善通过MMC_STAT寄存器的高位[31:16]来标识具体错误并通过ERRI位[15]进行聚合。常见的错误包括CTO命令响应超时。CCRC命令CRC校验错误。DTO数据响应超时。DCRC数据CRC校验错误。DEB数据结束位错误。手册中详细描述了DCRC错误在几种超时场景下的触发条件如图13-135至13-138例如在等待R1b/R5b响应忙信号、等待写操作后的CRC状态、或读数据时的超时。处理错误时不仅要清除MMC_STAT中的错误位还要根据CC命令完成和TC传输完成位的状态来判断当前操作是否被中止并执行相应的重试或恢复流程。5.3 如何优雅地停止传输停止一个正在进行的传输尤其是多块或流式传输需要小心处理。控制器提供了Stop at block gap功能通过设置MMC_HCTL[16] SBGR为1启用。启用后控制器会在一个数据块传输的边自动暂停等待软件干预。这给了软件一个安全的窗口期去发送停止命令如CMD12。手册中的表格对应Table 13-180总结了不同卡类型和传输类型下的停止方法。核心原则是在块边界发送停止命令并等待TC传输完成标志置位。对于SDIO卡可能需要使用CMD52来中止I/O传输。重要提示控制器只会在块边界发送停止命令无论软件是什么时候把停止命令写入寄存器的。这意味着如果你在块中间请求停止控制器会先把当前块传完再执行停止操作。这保证了数据的一致性。5.4 时钟沿与性能调优控制器输出信号CMD和DAT的驱动边沿可以通过MMC_HCTL[2] HSPE位配置。下降沿驱动默认HSPE0。有利于保持时间hold time的余量。上升沿驱动HSPE1。有利于建立时间setup time的余量允许达到更高的总线频率是支持SDR单数据率高速模式所必需的。警告在使能了DDR双数据率模式MMC_CON[19] DDR 1时绝对不能使用上升沿驱动模式。因为DDR模式已经在时钟的上升沿和下降沿都采样数据改变驱动边沿会导致时序混乱。5.5 CE-ATA命令完成机制对于支持CE-ATA协议的设备如某些嵌入式存储控制器支持命令完成信号CCS的检测。当发送一个需要等待CCS的命令时设置MMC_CON[12] CEATA和MMC_CMD[2] ACEN为1主机在数据传输完成前不能发送新命令除非是发送“命令完成禁用令牌”。发送禁用令牌的寄存器操作序列需要严格遵守设置CEATA1HR1 清空MMC_ARG然后向MMC_CMD写入0x00000000。这个机制用于在需要时提前中止等待CCS的过程比如发送一个中止命令。6. 低层编程模型与初始化流程理论最终要落到代码上。AM263P手册提供了一个清晰的初始化流程图我们可以将其转化为更具体的编程步骤和注意事项。6.1 初始化流程拆解外围模块全局初始化时钟与电源通过PRCM模块使能控制器的主时钟OCP clock和适配器时钟CLKADPI。这是访问任何SD控制器寄存器的前提否则会访问失败。引脚复用在Control Module中将相关GPIO引脚配置为SD/MMC功能模式。中断与DMA如果使用中断或DMA需要配置VIM向量中断管理器和EDMA控制器。将SD控制器的中断线映射到CPU的IRQ并配置DMA通道的源/目标地址、传输量等。控制器软复位向SD_SYSCONFIG[1] SOFTRESET位写1。轮询SD_SYSSTATUS[0] RESETDONE位直到其变为1。复位期间不要访问其他功能寄存器。设置控制器能力从芯片的OTP或固件中读取能力参数并写入MMC_CAPA和MMC_CUR_CAPA寄存器。这些参数包括支持的最高电压、最大电流、最大块大小等告诉驱动本硬件支持什么。总线配置电压与电源配置SD_HCTL寄存器的SDVS电压选择和SDBP电源使能位。务必确认选择的电压在SD_CAPA寄存器支持的范围内否则SDBP可能无法成功置1。使能内部时钟设置SD_SYSCTL[0] ICE 1。设置初始低速时钟配置SD_SYSCTL[15:6] CLKD分频器产生一个低速时钟通常约400kHz或更低用于卡识别阶段。轮询SD_SYSCTL[1] ICS直到时钟稳定。配置总线宽度等根据卡类型配置SD_CON寄存器如是否开漏输出、是否8位总线、是否使能CE-ATA等。6.2 卡识别与选择流程精讲手册的图13-143和13-144是经典的SD/MMC卡初始化状态机。其核心逻辑是发CMD0GO_IDLE_STATE让所有卡进入空闲状态。发CMD8SEND_IF_COND询问卡是否支持SDHC/SDXC电压范围、检查模式。如果有响应CC1则是SD2.0或更高版本的卡如果超时CTO1则可能是SD1.x或MMC卡。发ACMD41SD_SEND_OP_COND对于SD卡发送应用命令先发CMD55来激活卡并获取OCR寄存器信息判断是否支持高容量SDHC/SDXC。发CMD1SEND_OP_COND对于MMC卡发送此命令激活卡。发CMD2ALL_SEND_CID获取卡的唯一CID号。发CMD3SEND_RELATIVE_ADDR为SD卡分配一个相对地址RCA。发CMD7SELECT/DESELECT_CARD通过RCA选中要操作的卡使其进入传输状态。调试心得卡识别阶段最容易出问题的是时序和电压。一定要确保在发CMD0之前已经有足够长时间的初始时钟至少74个周期并且电压已经稳定施加到卡上。识别过程中每次发送命令后都要检查CC命令完成和CTO命令超时标志并根据结果跳转到正确的状态分支。建议将这部分逻辑实现为一个清晰的状态机便于调试和排查问题。