嵌入式MMC/SD/SDIO主机控制器驱动开发:从初始化到DMA传输实战

📅 2026/7/19 7:37:55 👁️ 阅读次数 📝 编程学习
嵌入式MMC/SD/SDIO主机控制器驱动开发:从初始化到DMA传输实战

1. 项目概述:深入嵌入式存储接口的驱动核心

在嵌入式系统开发中,存储设备接口编程是连接硬件与软件的关键环节。MMC、SD和SDIO作为广泛应用的存储与扩展接口标准,其主机控制器(Host Controller)的初始化与数据传输流程是驱动开发的核心。理解其工作原理,对于实现稳定高效的数据交换至关重要。控制器初始化通常遵循标准流程:首先配置接口与功能时钟,确保控制器基础时钟信号稳定;随后进行软件复位,清除控制器内部状态;接着设置硬件能力寄存器,定义电压支持、总线宽度等关键参数;最后配置空闲与唤醒模式,优化系统功耗。完成初始化后,控制器进入数据传输阶段,支持DMA(直接内存访问)与轮询两种模式,分别适用于高吞吐量场景和简单控制需求。DMA模式通过中断或轮询机制管理数据块传输,能显著降低CPU负载,提升系统整体性能。这些流程涉及对MMCi.MMCHS_SYSCONFIG、MMCi.MMCHS_CAPA、MMCi.MMCHS_HCTL等一系列寄存器的精确操作。掌握MMC/SD/SDIO主机控制器的编程模型,是开发可靠存储驱动、进行SDIO设备扩展以及实现CE-ATA协议通信的基础,广泛应用于移动设备、工业控制等嵌入式领域。

对于刚接触底层驱动的开发者来说,面对动辄数百页的技术参考手册和一堆以“MMCHS_”开头的寄存器,很容易感到无从下手。我最初调试MMC控制器时,也曾在时钟配置和命令超时问题上卡了好几天。这篇文章,我将结合自己的踩坑经验,把TI OMAP/AM系列芯片中MMC/SD/SDIO主机控制器(MMCHS)的初始化与数据传输流程掰开揉碎,从最基础的时钟使能,到复杂的DMA传输和CE-ATA协议支持,提供一个可以直接“抄作业”的编程指南。我会重点解释每个步骤“为什么”要这么做,以及在实际操作中可能遇到的“坑”和应对技巧。

2. 控制器初始化流程全解析

控制器初始化是驱动工作的基石,一个稳定可靠的初始化流程能避免后续数据传输中各种诡异的问题。这个过程可以拆解为几个逻辑清晰的步骤,我们逐一深入。

2.1 第一步:时钟配置——为控制器注入“脉搏”

在访问任何MMCHS控制器寄存器之前,必须首先为其提供时钟信号,这包括接口时钟和功能时钟。接口时钟用于寄存器总线访问,而功能时钟则是控制器内部逻辑(如状态机、FIFO控制器)工作的核心。

为什么必须先配置时钟?现代SoC的功耗管理非常精细,外设模块在非活动状态下其时钟域可能被关闭以节省功耗。如果试图在时钟关闭时访问其寄存器,轻则读取到全0或全1的无效数据,重则可能导致总线挂起或系统异常。因此,使能时钟是唤醒并激活控制器的第一步。

具体操作与寄存器详解:以TI OMAP平台为例,时钟管理通常由PRCM模块负责。我们需要操作两个关键寄存器:

  1. PRCM.CM_ICLKEN1_CORE:接口时钟使能寄存器。找到对应MMCHS控制器的位(例如MMCHS1对应第24位),将其置1。
  2. PRCM.CM_FCLKEN1_CORE:功能时钟使能寄存器。同样,将对应控制器的使能位置1。

注意:不同型号的SoC,PRCM模块的寄存器地址和位定义可能不同。务必查阅你所用芯片的《系统控制模块》或《时钟与电源管理》章节的文档。我曾在一个项目中将AM335x的寄存器配置直接套用到AM437x上,结果因为时钟域划分不同导致控制器根本无法响应,排查了很久。

实操心得:在编写驱动时,我习惯将时钟配置函数封装起来,并在函数开头加入对寄存器地址和位域的宏定义检查,防止误用。同时,在使能时钟后,最好加入一个微秒级的延时(例如udelay(10)),等待时钟域稳定。虽然手册不一定要求,但这能规避一些偶发的时序问题。

2.2 第二步:软件复位——让控制器从“干净状态”开始

时钟就绪后,下一步是对控制器进行软件复位。这个操作会将控制器内部的大部分逻辑(除少数保持寄存器外)恢复到上电默认状态,确保没有残留的异常状态影响初始化。

复位流程详解:

  1. 发起复位:向MMCi.MMCHS_SYSCONFIG寄存器的SOFTRESET位(通常为bit 1)写入1。
  2. 等待复位完成:循环读取MMCi.MMCHS_SYSSTATUS寄存器的RESETDONE位(通常为bit 0),直到其变为1。复位过程可能需要数个功能时钟周期。

关键寄存器操作示例:假设控制器实例为MMCHS1,基地址为0x4809C000

// 1. 发起软件复位 volatile uint32_t *sysconfig_reg = (uint32_t*)(0x4809C010); *sysconfig_reg |= (1 << 1); // 设置SOFTRESET位 // 2. 等待复位完成 volatile uint32_t *sysstatus_reg = (uint32_t*)(0x4809C014); uint32_t timeout = 10000; // 超时计数,防止死循环 while (((*sysstatus_reg & 0x1) == 0) && (timeout-- > 0)) { // 空循环等待 } if (timeout == 0) { // 复位超时,处理错误 }

常见问题与排查:

  • 复位超时:如果RESETDONE位迟迟不变1,首先检查第一步的时钟配置是否正确且已稳定。其次,确认你访问的是正确的控制器实例(例如MMCHS1, MMCHS2)。最后,查阅芯片勘误表,有些早期芯片的软复位流程有特殊要求。
  • 复位后寄存器值未恢复默认值:软件复位主要复位控制逻辑,某些配置寄存器(如MMCHS_CAPA)可能保持原值。最稳妥的做法是,在复位后,对所有需要配置的寄存器进行显式的写入操作,而不是依赖默认值。

2.3 第三步:硬件能力与默认配置——告诉控制器“你能做什么”

复位完成后,我们需要根据硬件实际支持和系统需求,对控制器进行基础配置。这分为两部分:声明硬件能力和设置初始工作参数。

2.3.1 设置硬件能力

MMCi.MMCHS_CAPA寄存器是只读的,它反映了控制器物理上支持的能力,例如最高支持电压(VS18, VS30)、是否支持8位数据总线等。软件需要读取此寄存器以了解硬件限制。而MMCi.MMCHS_CUR_CAPA寄存器则是软件可写的,用于设置当前要使用的能力子集。例如,即使控制器支持3.0V和1.8V,我们可能只选择使用1.8V进行通信以实现低功耗。

配置要点:

  • 电压选择:根据目标卡(SD卡、MMC卡)和支持的电压范围来设置。SD卡初始化通常从3.0V开始,识别后可能切换到1.8V以进入高速模式。
  • 总线宽度:在初始化初期,通常先配置为1位数据线模式。待卡识别完成后,再根据卡的能力切换到4位或8位模式以提升速度。

2.3.2 总线与主机初始配置

这是初始化阶段最关键的配置之一,涉及MMCi.MMCHS_HCTLMMCi.MMCHS_SYSCTLMMCi.MMCHS_CON等寄存器。

  • MMCi.MMCHS_HCTL
    • SDVS:设置输出到卡的总线电压(如1.8V或3.0V)。
    • DTW:设置数据总线宽度(1-bit, 4-bit, 8-bit)。
    • SDBP:总线电源使能。这里有个大坑:此位控制的是控制器内部总线驱动器的电源,并非直接给卡供电。卡的供电通常由外部PMIC(电源管理芯片)控制。即使将此位置1,如果外部电源未打开,通信也会失败。
  • MMCi.MMCHS_SYSCTL
    • ICE&CEN:内部时钟使能和输出时钟使能。必须先使能内部时钟(ICE),等待其稳定(通过ICS位判断),再使能输出到卡的时钟(CEN)。
    • CLKD:时钟分频器。这是决定通信频率的关键。时钟频率 = 输入功能时钟频率 / (CLKD+ 1)。初始化阶段,SD/MMC规范要求时钟频率不能超过400kHz,通常设置为100-400kHz。
  • MMCi.MMCHS_CON
    • OD:开漏模式。在初始化初期,总线通常配置为开漏模式,以便多卡检测和兼容性。识别完成后,可切换到推挽模式以获得更好的信号完整性。

初始化配置示例(1.8V, 1-bit, 400kHz):

// 配置HCTL: 1.8V, 1-bit总线,总线电源使能 *((volatile uint32_t*)(0x4809C128)) = (0x1 << 9) | (0x0 << 1) | (0x1 << 8); // 假设SDVS=001(1.8V), DTW=0(1-bit), SDBP=1 // 配置SYSCTL: 使能内部时钟,设置分频值 uint32_t input_clk = 96000000; // 假设功能时钟96MHz uint32_t target_clk = 400000; // 目标400kHz uint32_t clkd = (input_clk / target_clk) - 1; *((volatile uint32_t*)(0x4809C12C)) = (clkd << 6) | (1 << 2) | (1 << 0); // 设置CLKD, CEN=1, ICE=1 // 等待内部时钟稳定 while (((*((volatile uint32_t*)(0x4809C12C)) >> 1) & 0x1) == 0); // 配置CON: 开漏模式 *((volatile uint32_t*)(0x4809C02C)) |= 0x1;

2.4 第四步:初始化流发送与唤醒配置

2.4.1 发送初始化时钟流

在向卡发送任何命令之前,必须发送至少74个时钟周期的空闲信号(CMD线为高)。MMCHS控制器提供了INIT位来简化此操作。

操作流程:

  1. MMCi.MMCHS_CON[1] INIT位置1。
  2. MMCi.MMCHS_CMD寄存器写入0(发送一个空操作命令)。
  3. 控制器会自动在CMD线上输出至少74个时钟周期的高电平。完成后,需要将INIT位清零。

这里有一个至关重要的细节:规范要求初始化流的持续时间至少为1ms。如果控制器时钟配置得较高(比如几十MHz),74个周期远小于1ms。因此,在设置INIT位之前,必须先将时钟频率降低到80kHz或以下,以确保74个周期的时间跨度大于等于1ms。这是一个非常容易忽略的步骤,如果直接用400kHz的时钟发初始化流,时间可能不够,导致某些卡无法正确复位。

2.4.2 配置唤醒与空闲模式

对于移动设备,功耗管理至关重要。MMCi.MMCHS_SYSCONFIG寄存器中的SIDLEMODEAUTOIDLE字段用于配置控制器在系统空闲时的行为。

  • SIDLEMODE:决定在系统请求空闲时,控制器是强制空闲、仅功能时钟关闭,还是保持全速运行。
  • AUTOIDLE:使能后,当总线空闲时,控制器内部时钟门控会自动生效以节省功耗。
  • 对于SDIO卡,还可以配置ENAWAKEUPIWE位,使卡产生的中断能够唤醒系统。

功耗配置建议:在初始化阶段,可以先不使能AUTOIDLE,以保证调试阶段稳定性。在驱动完全正常工作后,再根据产品功耗需求开启这些省电特性。

3. 卡识别与枚举流程实战

控制器初始化妥当后,下一步就是识别挂在总线上的卡设备。这个过程是一系列标准命令的交互,目的是获取卡的类型(MMC, SD, SDIO)、版本、容量和支持的特性。

3.1 卡识别状态机与命令序列

卡识别是一个典型的状态机过程,其核心流程图在技术手册中已有描述。我将其转化为更易理解的步骤,并附上关键点解析:

  1. CMD0 (GO_IDLE_STATE):发送复位命令,使所有卡进入空闲状态。这是一个无响应广播命令。
  2. CMD8 (SEND_IF_COND):这是SD卡规范2.0引入的,用于询问卡是否支持2.0+的电压范围。如果卡响应,说明它是SD2.0或更高版本的卡;如果无响应或响应错误,则可能是SD1.x卡、MMC卡或SDIO卡,或者总线上无卡。
  3. CMD5 (IO_SEND_OP_COND):此命令用于SDIO卡。如果总线上是SDIO卡,它会响应并报告I/O操作条件。
  4. ACMD41 (SD_SEND_OP_COND)/CMD1 (MMC_SEND_OP_COND)
    • 如果之前CMD8有响应,则向SD卡发送ACMD41(注意:ACMD41是应用特定命令,前面需要先发送CMD55告知卡下一个是应用命令)。
    • 如果之前CMD8无响应,且CMD5也无响应,则尝试向MMC卡发送CMD1。 这些命令携带主机支持的电压范围(OCR),卡会返回其接受的电压范围。软件需要协商一个双方都支持的电压。
  5. CMD2 (ALL_SEND_CID)/CMD3 (SEND_RELATIVE_ADDR)
    • 对于MMC卡:发送CMD2获取卡的唯一CID(Card Identification),然后发送CMD3为卡分配一个相对地址(RCA)。
    • 对于SD卡:发送CMD2获取CID,然后发送CMD3,卡会自己选择一个RCA并返回。
  6. CMD7 (SELECT/DESELECT_CARD):通过RCA地址选中一个卡,使其进入传输状态。之后就可以对其进行数据读写操作了。如果总线上有多张卡,需要通过CMD7切换选中对象。

编程实现要点:每个命令的发送,都遵循“命令传输流程”:检查命令线是否空闲(CMDI),填充参数寄存器(ARG),配置命令寄存器(CMD,包含命令索引、响应类型、是否等待响应完成等),然后触发发送。之后需要等待命令完成中断(CC)或轮询状态,检查是否有错误(CTO,CCRC,CEB等),最后从响应寄存器(RSP10,RSP32等)读取卡返回的数据。

3.2 关键命令的寄存器配置示例

以发送CMD8(检查SD2.0卡)为例,展示详细的寄存器操作:

// 假设我们已经将时钟配置为400kHz,总线为开漏模式 // 1. 确保命令线空闲 while ((*((volatile uint32_t*)(MMCHS_BASE + 0x024)) & 0x1) != 0); // 检查PSTATE.CMDI // 2. 配置命令参数:CMD8的参数用于指定支持的电压和检查模式,这里我们使用0x1AA(支持2.7-3.6V,检查模式0xAA) *((volatile uint32_t*)(MMCHS_BASE + 0x120)) = 0x1AA; // 写入MMCHS_ARG // 3. 配置命令寄存器: // - 命令索引 = 8 // - 响应类型 = 0x2 (48-bit response with CRC) // - 使能CRC检查 (CCCE) // - 使能索引检查 (CICE) // - 命令类型为普通命令 (无数据) uint32_t cmd_reg_value = (8 << 24) | (0x2 << 16) | (1 << 11) | (1 << 10); *((volatile uint32_t*)(MMCHS_BASE + 0x10C)) = cmd_reg_value; // 写入MMCHS_CMD,触发发送 // 4. 等待命令完成(以轮询为例) uint32_t timeout = 100000; uint32_t stat; do { stat = *((volatile uint32_t*)(MMCHS_BASE + 0x130)); // 读取MMCHS_STAT if (stat & 0xFFFF0000) { // 检查错误位(CTO, CCRC, CEB, CIE等) // 处理命令错误 break; } } while (((stat & 0x1) == 0) && (timeout-- > 0)); // 等待CC位为1 if (timeout > 0 && (stat & 0x1)) { // 命令成功完成,读取响应 uint32_t response = *((volatile uint32_t*)(MMCHS_BASE + 0x114)); // 读取MMCHS_RSP10 // 检查响应中的电压接受位和检查模式回显位 if ((response & 0xFFF) == 0x1AA) { // 卡支持2.7-3.6V,且是SD2.0+卡 } else { // 卡不支持该电压或不是SD2.0+卡 } } // 5. 清除状态位(写1清零) *((volatile uint32_t*)(MMCHS_BASE + 0x130)) = stat;

实操心得:超时处理与错误恢复在识别流程中,必须为每个命令设置合理的超时机制。对于CMD8CMD5这类探测命令,超时可以设短一些(如10ms),因为无卡或卡类型不匹配时本就不会响应。对于ACMD41/CMD1这类需要卡进行内部初始化的命令,超时应设置得较长(如500ms-1s),特别是对于大容量卡。一旦某个命令超时或出错,完整的识别流程应该被重置(软件复位控制器,重新从CMD0开始),而不是继续执行后续命令。

4. 数据传输模式深度剖析

卡成功识别并进入传输状态后,就可以进行数据读写操作了。MMCHS控制器支持三种主要的数据传输模式:带中断的DMA模式、轮询的DMA模式以及非DMA的轮询模式(PIO模式)。选择哪种模式取决于系统对性能、CPU占用率和实现复杂度的权衡。

4.1 DMA传输模式(带中断)

这是高性能应用的首选模式。控制器通过DMA引擎直接在存储卡和系统内存之间搬运数据,极大减轻了CPU负担。

4.1.1 工作流程与寄存器配置

  1. 前期准备

    • 配置DMA控制器:设置源/目标地址(卡数据端口地址或内存地址)、传输数据量、地址递增模式等。这通常涉及SoC的DMA控制器,而非MMCHS本身。
    • 配置MMCi.MMCHS_BLK:定义块大小(BLEN)和块数量(NBLK)。对于多块传输,NBLK大于1。
    • 配置MMCi.MMCHS_CON:设置数据传输方向(读/写)、传输模式(单块/多块)、是否使能自动发送CMD12停止命令(对于多块读)。
    • 使能MMCHS控制器中与DMA和数据传输相关的中断,如传输完成(TC)、数据错误(DEB)、CRC错误(DCRC)、超时(DTO)等。
  2. 启动传输

    • 发送数据读写命令(如CMD17读单块,CMD18读多块,CMD24写单块,CMD25写多块)。命令的发送流程与识别阶段相同。
    • 命令发送后,控制器检测到数据阶段,会根据MMCHS_BLK的配置自动发起DMA请求。
  3. 中断处理

    • 传输完成中断 (TC):所有数据块传输完毕。对于多块读且使能了Auto-CMD12的情况,控制器会自动发送停止命令。
    • 数据错误中断 (DEB,DCRC,DTO):在数据传输阶段发生错误。此时应软件复位数据线(设置MMCHS_SYSCTL[26] SRD位),并报告错误。
    • DMA中断:来自DMA控制器的传输完成或错误中断,需结合MMCHS状态判断。

4.1.2 核心注意事项

  • 缓冲区对齐:DMA传输通常对内存缓冲区的地址对齐有要求(如32字节对齐)。使用非对齐缓冲区可能导致性能下降或DMA错误。
  • 数据线状态检查:在发送新的数据命令前,务必检查MMCi.MMCHS_PSTATE[1] DATI位,确保数据线空闲。否则命令会被拒绝。
  • Auto-CMD12的使用:对于多块读操作,强烈建议使能Auto-CMD12。这样在最后一个数据块传输完成后,控制器会自动发送CMD12终止传输,避免软件在精确时刻发送停止命令的时序难题。使能方法是在发送读多块命令(CMD18)前,设置MMCi.MMCHS_CMD寄存器中的ACEN位。
  • 中断清除:进入中断服务程序后,需要读取MMCHS_STAT寄存器来清除中断标志位(写1清零)。注意,有些状态位(如TC)在读取后会自动清零,而有些需要显式写入。

4.2 DMA传输模式(轮询)

此模式与中断模式类似,区别在于CPU通过不断轮询MMCHS_STAT寄存器来检查传输状态,而不是等待中断。其流程为:

  1. 配置DMA和MMCHS寄存器(同中断模式)。
  2. 发送数据命令。
  3. 循环轮询MMCHS_STAT寄存器,检查TC位或错误位。
  4. 传输完成或出错后,关闭DMA通道。

适用场景与优缺点

  • 优点:实现简单,无需编写中断服务程序,适合在简单的实时操作系统或无操作系统的裸机环境中快速实现。
  • 缺点:CPU被长时间占用在忙等待上,浪费功耗和计算资源,不适合高并发或低功耗系统。
  • 技巧:在轮询循环中,可以加入短暂的延时或让出CPU(如果系统支持),避免完全的死循环消耗。

4.3 非DMA模式(PIO轮询)

在这种模式下,CPU直接通过读写MMCi.MMCHS_DATA寄存器来搬运每一个数据字(通常是4字节)。控制器通过状态位BRR(缓冲区读就绪)和BWR(缓冲区写就绪)来指示数据寄存器的状态。

工作流程

  1. 发送数据命令。
  2. 对于读操作:轮询BRR位,当其为1时,从MMCHS_DATA寄存器读取一个数据字。重复直到完成BLEN指定字节数的读取。
  3. 对于写操作:轮询BWR位,当其为1时,向MMCHS_DATA寄存器写入一个数据字。重复直到写完所有数据。
  4. 轮询TC位判断传输完成。

性能考量与使用建议: PIO模式的性能最低,因为每个数据字都需要CPU介入。其吞吐量严重受限于CPU读取状态寄存器和读写数据寄存器的速度。仅建议在以下情况使用

  • 调试阶段,用于验证控制器和卡的基本通信是否正常。
  • 传输非常小的数据量(如SDIO的CMD52CMD53字节操作)。
  • 系统资源极度受限,无法使用DMA。

一个PIO读数据的代码片段示例:

uint32_t block_size = 512; // 块大小,字节 uint32_t words_to_read = (block_size + 3) / 4; // 计算需要读取的32位字数 uint32_t *data_buffer = ...; // 数据缓冲区 // 发送读单块命令CMD17... // ... 命令发送成功,进入数据阶段 for (int i = 0; i < words_to_read; i++) { // 等待数据就绪 while ((*((volatile uint32_t*)(MMCHS_BASE + 0x024)) & (1 << 2)) == 0); // 轮询PSTATE.BRR // 读取数据 data_buffer[i] = *((volatile uint32_t*)(MMCHS_BASE + 0x120)); // 读取MMCHS_DATA } // 等待传输完成 while ((*((volatile uint32_t*)(MMCHS_BASE + 0x130)) & (1 << 1)) == 0); // 轮询STAT.TC

5. 高级功能与疑难问题排查

除了基本的初始化和数据传输,MMCHS控制器还支持一些高级功能,并且在开发过程中会遇到各种问题。这里分享一些关键点和排查思路。

5.1 CE-ATA协议支持

CE-ATA是MMC协会为硬盘驱动器类设备定义的命令队列协议。MMCHS控制器提供了硬件支持以优化CE-ATA命令的执行。

关键配置差异:

  1. 模式使能:在发送CE-ATA命令前,需要设置MMCi.MMCHS_CON[12] CEATA位为1。
  2. 命令传输:发送命令时,必须设置MMCi.MMCHS_CMD[2] ACEN位为1。
  3. 数据传输限制:CE-ATA模式只支持有限传输,不支持无限传输。MMCHS_BLK寄存器中的块数(NBLK)必须设置为一个有限值。
  4. 命令并发限制:在CE-ATA数据传输过程中,不允许发送其他命令(除了CCSD命令)。这需要驱动软件严格管理命令队列。

使用场景:CE-ATA主要用于连接嵌入式硬盘(eMMC中的HS200/HS400模式也使用类似机制)。如果你的设备只是读写标准SD卡或MMC卡,则无需关心此模式。

5.2 SDIO挂起与恢复流程

SDIO卡支持挂起-恢复机制,允许主机暂时停止与SDIO功能的数据传输,以让SDIO卡处理其他事务(如响应内部中断),然后再恢复传输。

挂起流程要点:

  1. 在数据传输过程中,设置MMCi.MMCHS_HCTL[16] SBGR位,请求在下一个块间隙停止。
  2. 等待MMCHS_STAT[2] BGE位变为1,表示传输已在块间隙暂停。
  3. 向SDIO卡发送挂起命令(CMD52with Suspend)。
  4. 保存当前的MMCHS_BLK寄存器值(记录传输进度)。

恢复流程要点:

  1. 恢复之前保存的MMCHS_BLK寄存器值。
  2. 软件复位数据线(设置SRD位并等待清零)。
  3. 向SDIO卡发送恢复命令(CMD52with Resume)。
  4. 继续数据传输。

注意事项:挂起-恢复是SDIO协议特有的复杂特性,实现时需要仔细处理状态保存和时序。许多简单的SDIO设备驱动可能不实现此功能。

5.3 常见问题排查速查表

在实际开发中,你可能会遇到以下问题。这里提供一个快速排查的思路:

问题现象可能原因排查步骤与解决方案
控制器无响应,读写寄存器失败1. 时钟未使能。
2. 软件复位未完成或卡死。
3. 寄存器地址映射错误。
1. 确认PRCM中接口和功能时钟已开启。
2. 检查SYSSTATUS.RESETDONE位,确保复位完成。增加超时判断和打印。
3. 核对芯片数据手册,确认控制器基地址是否正确。
卡识别失败(CMD8/CMD5无响应)1. 总线电压未正确提供。
2. 初始化时钟流时间不足1ms。
3. 上拉电阻未配置或错误。
4. 卡本身损坏或兼容性问题。
1. 用万用表测量卡座的VDD引脚,确认PMIC已输出正确电压(如3.0V)。
2.重点检查:发送初始化流(INIT)前,是否已将时钟频率降至80kHz或以下?
3. 检查SoC的I/O Pad配置,确保CMD和DATA线已启用内部上拉电阻。
4. 更换一张已知良好的卡测试。
命令超时(CTO错误)1. 时钟频率过高(初始化阶段)。
2. 卡未正确插入或接触不良。
3. 命令响应类型配置错误。
1. 初始化阶段确保时钟≤400kHz。
2. 检查硬件连接。
3. 确认MMCHS_CMD寄存器中的RESP_TYPE字段与所发命令要求的响应类型一致(如CMD8是R7,48位带CRC)。
CRC错误(CCRC/DCRC错误)1. 总线干扰,信号完整性差。
2. 开漏/推挽模式切换时机不当。
3. 软件在数据未就绪时读取响应寄存器。
1. 检查PCB布线,确保CMD/DATA线等长,远离干扰源。可尝试降低时钟频率。
2. 确保在卡识别完成(收到RCA后)再尝试将总线模式从开漏切换到推挽。
3. 确保在命令完成(CC)中断发生或状态位置起后,再读取MMCHS_RSPxx寄存器。
DMA传输数据错乱或中断不触发1. DMA缓冲区地址或长度未对齐。
2. DMA传输方向配置错误。
3. MMCHS或DMA控制器中断未使能或未正确清除。
4. 块大小(BLEN)配置错误。
1. 确保内存缓冲区地址满足DMA对齐要求(如32字节对齐)。
2. 确认MMCHS的CON寄存器传输方向与DMA配置方向匹配(读卡对应DMA从外设到内存)。
3. 检查MMCHS_IE/MMCHS_ISE以及DMA通道的中断使能位。在ISR中正确读取状态寄存器以清除中断源。
4. 确保MMCHS_BLK.BLEN与发送的命令中指定的块大小、以及实际DMA传输长度一致。
高频率下数据传输不稳定1. 时序裕量不足。
2. 电源噪声。
3. 未使用延迟链(Delay Line)校准。
1. 尝试在MMCHS_SYSCTL中调整DTO(数据超时)值,给予更多响应时间。
2. 检查电源纹波,确保卡供电干净稳定。
3. 对于支持可调输出延迟(Tuning)的高速模式(如SDR104, HS200),必须执行延迟校准流程,这通常涉及发送CMD19和调整MMCHS_AC12等寄存器。

调试建议

  1. 从最简配置开始:始终使用1位数据总线宽度、较低时钟频率(如400kHz)、开漏模式开始调试,待基本识别和读写稳定后,再逐步提高频率、切换推挽模式、增加数据位宽。
  2. 善用状态寄存器MMCHS_STAT寄存器是调试的宝库。在任何命令或数据传输失败后,第一时间完整地读取并打印该寄存器的值,其中的错误位(CEB,CCRC,CTO,DEB,DCRC,DTO)能给出明确的失败方向。
  3. 逻辑分析仪是关键:如果软件排查困难,使用逻辑分析仪抓取CMD和DATA线上的实际波形是终极手段。可以直观地看到命令发送、响应、数据流以及CRC,很容易发现时序问题、信号质量问题或协议错误。

6. 性能优化与实战经验

当驱动基本功能稳定后,下一步就是考虑优化性能和稳定性。这里分享一些从实际项目中总结的经验。

6.1 时钟频率与分频器计算

数据传输速率直接受限于SDCLK的频率。MMCHS控制器的输出时钟由功能时钟分频得到:SDCLK = MMC_FCLK / (CLKD + 2)。注意,有些版本的手册或芯片可能是/(CLKD+1),务必以你所用芯片的数据手册为准。

计算示例:假设功能时钟MMC_FCLK = 96 MHz,目标SDCLK为25 MHz(SD高速模式上限)。

  • 理论分频值:CLKD = 96 / 25 - 2 ≈ 1.84
  • 取整后:CLKD = 2
  • 实际SDCLK:96 / (2+2) = 24 MHz

注意CLKD寄存器字段通常只有有限的位宽(如6位),这意味着分频值有上限,从而限制了最低输出频率。在初始化阶段需要很低频率(如100kHz)时,要确保输入功能时钟足够低,或者控制器支持额外的预分频器。

6.2 DMA描述符链与双缓冲区

对于持续的大数据量传输(如视频录制),使用简单的单次DMA可能不够高效。可以采用以下策略:

  1. DMA描述符链:预先在内存中构建一个描述符链表,每个描述符指向一个数据缓冲区并指定下一个描述符的地址。配置DMA控制器工作在链表模式。这样,当一次传输完成,DMA会自动加载下一个描述符继续传输,无需CPU频繁干预来重配DMA,减少了中断延迟和总线占用。
  2. 双缓冲区(Ping-Pong Buffer):分配两个缓冲区A和B。当DMA正在从卡读取数据到缓冲区A时,CPU可以处理之前已满的缓冲区B中的数据。当A满、B空时,两者角色交换。这有效地隐藏了数据搬运时间,实现了流水线操作,能最大化吞吐量。

6.3 错误处理与重试机制

一个健壮的驱动必须有完善的错误处理机制。

  1. 分层重试

    • 命令级重试:对于命令超时(CTO)或CRC错误(CCRC),可以立即重试发送该命令1-2次。
    • 数据级重试:对于数据传输错误(DCRC,DTO),应软件复位数据线(SRD),然后重试整个数据块传输。
    • 操作级重试:如果重试多次仍失败,可以考虑降低时钟频率后重试整个读写操作。
    • 卡级恢复:如果持续失败,最彻底的方法是发送CMD0使卡复位,然后重新执行识别流程。
  2. 状态保存与恢复:在发生错误并进行重试或复位前,尽可能保存当前的配置状态(如时钟频率、总线宽度、选中的RCA等)。在恢复时,可以快速恢复到错误前的状态,而不是完全从头开始。

6.4 与操作系统驱动的整合要点

在Linux等操作系统中,MMC/SD驱动通常以三层结构存在:核心层(MMC Core)、主机控制器驱动层(如omap_hsmmc.c)和块设备驱动层。

  • 主机控制器驱动职责:你编写的初始化、识别、数据传输代码,最终会封装成主机控制器驱动,向MMC核心层注册一个struct mmc_host对象,并提供request回调函数。核心层会将上层的读写请求转化为命令和数据传输,调用你的回调函数。
  • 关键数据结构:你需要填充mmc_hostcaps字段(告知核心层控制器支持的能力,如DMA、高容量、8位总线等),以及ops字段(包含控制器具体的操作函数指针,如request,set_ios,get_ro等)。
  • set_ios的实现:这是驱动中非常重要的一个函数,核心层通过它来设置总线的时钟频率、电压、总线宽度和电源状态。你的实现需要根据参数去配置MMCHS_SYSCTL.CLKDMMCHS_HCTL.SDVS/DTW等寄存器。
  • 中断处理:在中断服务程序中,你需要快速读取MMCHS_STAT,判断中断类型(命令完成、数据传输完成、错误等),然后调用MMC核心层提供的API(如mmc_request_done)来通知请求处理完毕,并可能唤醒等待的线程。

编写符合操作系统框架的驱动,虽然初期工作量较大,但能获得更好的可维护性、电源管理支持以及社区维护的优势。理解上述MMCHS控制器的底层操作,是写好上层主机控制器驱动的基础。