1. 项目概述
在嵌入式系统开发中,存储和扩展IO是绕不开的话题。无论是运行Linux的工控主板,还是跑着RTOS的智能手表,MMC、SD和SDIO这三种接口标准几乎无处不在。它们不仅仅是简单的存储卡,更是系统启动、数据存储和功能扩展的基石。然而,当你真正动手为一块新的SoC编写底层驱动时,往往会发现,数据手册里那些密密麻麻的寄存器描述和时序图,读起来就像天书。命令怎么发?响应怎么收?总线宽度和时钟频率如何切换?这些问题不搞清楚,你的系统可能连卡都识别不了,更别提稳定读写数据了。
我最近在为一个基于TI AM335x的项目调试SD卡启动功能,就深陷在MMCHS控制器的寄存器海洋里。官方手册提供了详尽的寄存器列表和命令序列,但如何将这些冰冷的十六进制数值转化为可工作的驱动代码,中间隔着巨大的鸿沟。比如,手册告诉你发送CMD2获取CID,但不会告诉你为什么要在识别阶段用开漏模式,也不会提醒你电压协商失败后该如何回退。这些实战中的“坑”,往往需要耗费大量时间才能填平。
本文的目的,就是结合TI MMCHS控制器的具体实例,为你彻底拆解MMC/SD/SDIO主机控制器的编程核心。我们将不满足于罗列命令,而是深入每个操作背后的硬件原理和设计意图。从最基础的上电、时钟初始化,到复杂的卡识别、高速模式切换,再到最终的数据块读写,我会一步步展示每个阶段需要配置哪些寄存器、为什么要这样配置,并分享我在调试过程中积累的宝贵经验和避坑指南。无论你是在为新的硬件平台移植驱动,还是想深入理解存储协议,这篇文章都将提供从理论到实践的完整路径。
2. 控制器基础与初始化流程拆解
在开始对着寄存器“填数字”之前,我们必须先理解MMC/SD/SDIO通信的基本模型。你可以把它想象成一种主从式的问答协议。主机(我们的SoC)通过CMD线发送命令,从设备(存储卡)通过同一根线回复响应。数据交换则通过1根、4根或8根DAT线进行。整个通信的节奏,完全由主机输出的时钟信号CLK来同步。
2.1 核心寄存器组概览
以TI的MMCHS控制器为例,其寄存器空间可以被划分为几个功能明确的组,理解这个分组对编程至关重要:
系统控制与状态寄存器:这是控制器的“总开关”。
MMCHS_SYSCONFIG:控制模块的时钟门控、空闲模式和软件复位。例如,SOFTRESET位写1可以触发整个控制器的软复位,这在驱动加载或出错恢复时是第一步。MMCHS_SYSSTATUS:主要用来查询RESETDONE位,确保软件复位操作确实已完成,避免在控制器还在复位过程中进行配置。MMCHS_SYSCTL:这是整个初始化的核心。它控制内部时钟ICE的启停、输出给卡的时钟CEN的开关,以及最重要的——时钟分频器CLKD。卡在不同阶段(如识别阶段和传输阶段)需要不同的时钟频率,全靠这个寄存器来配置。
总线与电源控制寄存器:负责物理层的连接。
MMCHS_CON:配置总线模式(开漏/推挽)、数据线宽度(1/4/8位)、初始化序列发送等。识别阶段必须配置为开漏模式,这是协议规定的。MMCHS_HCTL:控制总线电源SDBP的开关和电压选择SDVS。必须先选好电压(如3.3V),再上电。MMCHS_PWCNT:设置上电后的稳定延时。有些卡或板级电路需要电源稳定一段时间后才能通信,这个计数器就是干这个的。
命令与数据传输寄存器:协议交互的直接窗口。
MMCHS_ARG:命令参数寄存器。几乎所有命令都需要附带参数,比如卡地址、块长度、读写起始地址等,都放在这里。MMCHS_CMD:命令发射寄存器。写入这个寄存器就意味着向卡发送命令。它的位域定义了命令索引INDX、响应类型RSP_TYPE、是否启用CRC检查CCCE、是否启用索引检查CICE、是否有数据阶段DP、数据传输方向DDIR、是否启用DMADE等。这是最需要仔细配置的寄存器之一。MMCHS_RSP10~MMCHS_RSP76:响应寄存器。卡对命令的回复就存放在这里,根据响应类型(48位或136位),数据分布在不同寄存器中。MMCHS_BLK:定义数据块的长度BLEN和本次传输的块数量NBLK。MMCHS_DATA:数据缓冲区的人口。在非DMA模式下,读写的数据都通过这个寄存器进出;在DMA模式下,控制器会直接与内存交互。
状态与中断寄存器:驱动程序的“眼睛”和“耳朵”。
MMCHS_STAT:中断状态寄存器。任何事件发生,如命令完成CC、传输完成TC、缓冲区可读BRR/可写BWR,或各种错误(超时CTO/DTO、CRC错误CCRC/DCRC等),都会在这里置位对应的位。MMCHS_PSTATE:当前状态寄存器。可以轮询CMDI和DATI位来判断命令线和数据线是否空闲,避免冲突。MMCHS_IE和MMCHS_ISE:中断使能寄存器。IE决定哪些事件能设置状态位,ISE决定哪些状态位能触发硬件中断信号。通常两者配置为相同的值。
2.2 上电与时钟初始化:一切的基础
驱动加载的第一步,不是急着发命令,而是给控制器一个稳定的工作环境。这个过程往往被忽略,但却是后续所有操作稳定的前提。
步骤一:解除复位并等待稳定首先,我们需要确保控制器不在复位状态。通过向MMCHS_SYSCONFIG寄存器的SOFTRESET位写1,然后轮询MMCHS_SYSSTATUS的RESETDONE位,直到它变为1。这个等待是必须的,我曾在早期调试中因为没等复位完成就配置时钟,导致配置不生效,卡在第一步。
步骤二:配置并启动内部时钟接下来,需要给控制器提供工作时钟。通过设置MMCHS_SYSCTL寄存器:
- 将
ICE(Internal Clock Enable) 位置1,启动内部时钟。 - 轮询
ICS(Internal Clock Stable) 位,直到它变为1。这一步非常关键,时钟不稳定就去操作其他寄存器,行为是不可预测的。我建议在这里加一个超时判断,比如循环检查1000次后若ICS仍为0,则报错返回,这能帮你快速定位是时钟源本身的问题还是控制器故障。 - 设置时钟分频器
CLKD。在卡识别阶段,协议规定时钟频率不能超过400kHz。假设你的控制器输入时钟FUNC_96M_CLK是96MHz,那么分频值应为96MHz / 400kHz = 240。所以CLKD应设置为240。注意,有些手册规定CLKD为0或1时是旁路,从2开始才是2分频,所以实际写入的值可能是240 - 1 = 239或直接是240,务必查阅你的具体手册。TI的MMCHS控制器这里CLKD写入5代表分频值为6,最终时钟为96MHz / 6 = 16MHz,但在识别阶段会通过其他方式进一步降速,需要结合MMCHS_CON的INIT位产生的80个时钟周期来满足大于1ms的初始化时间要求。 - 最后,将
CEN(Clock Enable) 位置1,时钟信号才会真正输出到卡的CLK引脚上。
步骤三:总线电源上电在输出时钟之前或之后,需要给卡供电。通过MMCHS_HCTL寄存器:
- 根据你的硬件设计支持的电压(通常为3.3V),设置
SDVS(SD Bus Voltage Select) 字段。例如,对于3.3V,TI控制器设置为0x7。 - 将
SDBP(SD Bus Power) 位置1,开启电源。 - 重要经验:开启电源后,必须等待一段时间(通常几毫秒)让卡的电容器充电和电压稳定。可以利用
MMCHS_PWCNT寄存器设置一个���于时钟周期的延时,或者简单地进行毫秒级的软件延时。我曾经遇到因为上电后立即操作导致CMD0无响应的问题,加上10ms延时后就解决了。
注意:初始化序列(发送至少74个时钟脉冲)通常可以通过设置
MMCHS_CON寄存器的INIT位为1来自动完成。控制器会自动在CMD线上产生至少80个时钟周期的空闲高电平。确保在这样做之前,时钟频率CLKD的设置能满足“80个时钟周期 > 1ms”的要求。
3. 卡识别与初始化的详细步骤解析
卡上电并收到初始化时钟后,就进入了识别模式。这个阶段的目标是让主机识别出卡的类型(MMC/SD/SDIO)、版本、容量,并给卡分配一个相对地址(RCA)。整个过程是一系列标准命令的对话。
3.1 发送CMD0:进入空闲状态
CMD0是复位命令,其索引为0,没有响应。它的作用是让卡进入空闲状态,无论卡之前处于什么状态。这是一个广播命令(所有卡都接收)。
寄存器配置要点:
MMCHS_CON:在识别阶段,总线必须处于开漏模式,以便多个卡可以同时响应。需要设置OD位为1。MMCHS_CMD:命令索引INDX设为0。由于无响应,RSP_TYPE设为0。通常不需要CRC和索引检查,所以CCCE和CICE可以设为0。命令类型CMD_TYPE为普通命令(0)。最终,MMCHS_CMD的值可能类似于0x00000000。MMCHS_ARG:对于CMD0,参数通常为0。
操作后检查:发送CMD0后,应轮询MMCHS_STAT寄存器的CC(Command Complete) 位,等待其置1。同时检查CTO(Command Timeout) 位,确保没有超时。如果超时,可能是硬件连接问题(如CMD线断路)或卡未正确供电。
3.2 发送CMD8:验证SD卡版本(仅SD/SDIO)
这是SD卡规范V2.00引入的,用于区分SD卡V2.00或更高版本与旧版本卡或MMC卡。MMC卡会对此命令无响应(报命令超时错误)。
命令参数设计:CMD8的参数用于询问卡支持的电压范围和检查模式。例如,参数可以设置为0x000001AA。其中,低8位0xAA是检查模式,卡会在响应中回显这个值;[11:8]位设置为0001,表示主机支持2.7-3.6V电压范围。
寄存器配置与响应处理:
- 配置
MMCHS_CMD:索引INDX设为8,响应类型RSP_TYPE设为48位带忙响应(R7),即0x2。启用CRC检查CCCE和索引检查CICE。命令值可能为0x081A0000(具体位域组合需参考手册)。 - 配置
MMCHS_ARG为0x000001AA。 - 写入
MMCHS_CMD发送命令。 - 等待
CC位置位,然后读取MMCHS_RSP10获取响应。 - 解析响应:如果卡是SD V2.00+,它会返回一个R7响应,其中包含回显的检查模式和电压信息。如果发生超时(
CTO置位),则可能是MMC卡或SD V1.x卡。
实战技巧:处理CMD8的超时不是错误,而是一种识别机制。你的驱动代码应该捕获超时状态,并将其作为判断卡类型的一个分支点。
3.3 发送CMD55+ACMD41:激活SD卡
对于SD卡,需要通过应用特定命令(ACMD)来激活。ACMD41是发送给SD卡,使其进入准备状态并完成电压协商的关键命令。但ACMD41不能直接发送,必须先发送CMD55(前缀命令)告诉卡“下一个命令是应用命令”。
循环发送流程:
- 发送CMD55:设置
MMCHS_CMD发送索引为55的命令,响应类型为48位R1。参数MMCHS_ARG中应放入你希望对话的卡的地址(RCA),在初始识别阶段,这个地址为0(广播)。 - 发送ACMD41:紧接着发送CMD55之后,发送索引为41的命令。ACMD41的参数
MMCHS_ARG的高位(HCS位)用于指示主机是否支持高容量卡(SDHC/SDXC)。例如,参数0x40FF8000表示支持高容量卡,并请求卡报告其工作电压范围。 - 等待与重试:卡可能不会立即准备好。主机需要在一个循环中重复发送CMD55+ACMD41序列,直到ACMD41的响应表明卡已结束上电流程(OCR寄存器的忙位被清除)。每次循环之间应加入数十毫秒的延时。
- 解析响应:从ACMD41的响应(在
MMCHS_RSP10中)可以读出卡的OCR寄存器内容,从而知道卡支持的电压和是否上电完成。
为MMC卡发送CMD1:对于MMC卡,对应的激活命令是CMD1,其流程与ACMD41类似,也是通过参数进行电压协商,并循环发送直到卡准备好。
3.4 获取卡标识信息:CMD2, CMD3, CMD9
一旦卡被激活,主机需要获取卡的详细信息并为其分配地址。
- CMD2 - 获取CID:这是一个广播命令,要求所有卡发送它们的唯一标识符CID。响应是136位的R2类型。主机需要从
MMCHS_RSP10,RSP32,RSP54,RSP76四个寄存器中读取完整的CID。CID包含了制造商ID、产品名称、序列号、生产日期等唯一信息。 - CMD3 - 发布相对地址(RCA):主机通过此命令为卡分配一个本地使用的短地址(RCA)。参数
MMCHS_ARG中放入你想要的地址(例如0x00010000)。卡接受后,会在响应中返回这个RCA。从此之后,除了少数广播命令,主机与特定卡的通信都必须使用这个RCA。 - CMD9 - 获取CSD:这是一个寻址命令(需在
MMCHS_ARG中指定卡的RCA)。CSD(Card Specific Data)寄存器包含了卡的关键操作参数,如:- 卡容量(对于标准容量卡,通过
C_SIZE,C_SIZE_MULT,READ_BL_LEN计算)。 - 支持的最大数据传输速率(
TRAN_SPEED)。 - 数据块的最大长度(
READ_BL_LEN,WRITE_BL_LEN,通常为512字节)。 - 读/写操作所需的等待时间(
TAAC,NSAC)。
- 卡容量(对于标准容量卡,通过
解析CSD的实战意义:CSD中的TRAN_SPEED字段决定了卡能跑多快。例如,值0x32表示25MHz,0x5A表示50MHz。驱动必须根据这个值,在后续切换到传输模式后,合理设置控制器的时钟分频器CLKD,以匹配甚至低于卡支持的最高频率,确保通信稳定。同时,READ_BL_LEN指明了卡内部组织数据的基本块大小,主机发起的读写操作块长度必须是这个值的整数倍,现代卡通常都是512字节。
4. 切换至传输模式与性能优化
完成识别并获取CSD后,卡仍处于Stand-by状态。通过发送CMD7(选择/取消选择卡命令)并带上卡的RCA作为参数,可以将卡切换到Transfer状态。只有在这个状态下,才能进行数据读写操作。
4.1 总线模式切换:从开漏到推挽
在识别阶段,为了兼容性,总线(CMD和DAT线)通常工作在开漏模式。进入传输模式后,为了获得更好的信号完整性和速度,需要切换到推挽模式。这是通过将MMCHS_CON寄存器的OD位清零来实现的。
4.2 提升传输性能:高速模式与宽总线
现代卡(MMC V4.x以上,SD V2.0以上)支持更高的传输速率(High Speed,最高52MHz)和更宽的数据总线(4位或8位)。但这需要主机显式地启用。
对于MMC卡(使用CMD6切换):
- 切换到高速模式:通过CMD6命令写入扩展CSD寄存器(
EXT_CSD)的特定字节。例如,将EXT_CSD[185](HS_TIMING)设置为1。CMD6的参数构造有固定格式:(0x3 << 24) | (byte_address << 16) | (byte_value << 8)。所以设置为高速模式的参数可能是0x03B90100(byte_address=185,byte_value=1)。 - 切换到8位总线模式:同样使用CMD6,修改
EXT_CSD[183](BUS_WIDTH)��设置为8位总线时,byte_value为2。参数可能是0x03B70200。 - 配置控制器:在成功发送CMD6并等待卡完成切换(通过检查响应中的忙位或等待一段时间)后,主机控制器需要同步更新自己的配置:
- 将
MMCHS_CON寄存器的DW8位置1,告知控制器使用8位DAT线。 - 根据卡支持的频率(从CSD获知)和系统时钟,重新计算并设置
MMCHS_SYSCTL中的CLKD分频值,将输出时钟提升到更高频率(如48MHz)。注意,频率切换前需要先关闭输出时钟(CEN清零),配置好CLKD后再重新开启。
- 将
对于SD/SDIO卡(使用ACMD6切换):
- 切换到4位总线模式:先发送CMD55(前缀命令),再发送ACMD6。ACMD6的参数低两位表示总线宽度:
0x0为1位,0x2为4位。 - 配置控制器:成功后,将
MMCHS_HCTL寄存器的DTW位设置为1(4位模式)。高速模式的切换对于SD卡,通常是在识别阶段通过CMD8和ACMD41的协商隐含确定的,或者通过CMD6(非ACMD6)进行更详细的性能查询。
关键注意事项:
- 顺序很重要:通常建议先切换总线宽度,再切换高速模式。因为有些卡在高速模式下对信号完整性要求更高,宽总线可能加剧挑战。
- 稳定性优先:在提高频率后,务必进行简单的读写测试(如读写卡的第一个扇区)。如果出现CRC错误或超时,应逐步降低频率直到稳定。在我的项目中,一块标称支持50MHz的卡,在48MHz下工作稳定,但尝试52MHz时就频繁出错。
- 电源考量:高速和宽总线模式会增大瞬时电流。确保你的电源电路能提供足够稳定、干净的电流,否则可能导致随机性的写入失败或卡死。
5. 数据读写传输的完整实现
数据传输是存储控制的最终目的。MMC/SD协议支持单块和多块读写。多块传输效率更高,因为它减少了命令开销。
5.1 写数据流程详解
假设我们要写入一个4KB的数据(8个512字节的块)。
- 选择卡:确保目标卡已被CMD7选中(处于Transfer状态)。
- 设置块长度:发送CMD16。参数
MMCHS_ARG设置为块长度,通常是512(0x200)。MMCHS_CMD配置为发送CMD16,响应类型为48位R1。这一步对于标准容量卡是必须的,高容量卡(SDHC/SDXC)的块长度固定为512字节,但发送CMD16通常也被接受或忽略。 - 设置块数量:对于多块写入,发送CMD23。参数
MMCHS_ARG设置为要写入的块数,本例中为8(0x00000008)。 - 启动多块写入:发送CMD25。这是多块写入命令。
MMCHS_CMD配置:索引INDX=25,响应类型R1(48位),需要设置DP位表示有数据,MSBS位表示多块,BCE位表示已通过CMD23设置了块计数,DE位根据是否使用DMA来设置。MMCHS_BLK寄存器需要配置:BLEN=512,NBLK=8。格式通常为(NBLK << 16) | BLEN,即0x00080200。MMCHS_ARG:写入起始块的地址(对于标准容量卡是字节地址,对于高容量卡是块索引)。
- 数据搬运与结束:
- 非DMA模式:控制器会通过置位
MMCHS_STAT的BWR(Buffer Write Ready)中断或状态位,通知主机数据缓冲区已空,可以写入下一个数据块。主机将数据写入MMCHS_DATA寄存器(注意字节序和访问宽度)。写完指定块数后,控制器会自动发送停止命令(如果使能了Auto CMD12)或需要主机手动发送CMD12停止传输。 - DMA模式:这是更高效的方式。在发送CMD25之前,先配置好DMA控制器,将源内存地址、传输总字节数(块数*块长度)等信息告知DMA。然后使能
MMCHS_CMD的DE位。控制器会在需要数据时直接通过DMA请求从内存读取,无需CPU干预。传输完成后,会产生TC(Transfer Complete)中断。
- 非DMA模式:控制器会通过置位
5.2 读数据流程详解
读流程与写流程对称。
- 选择卡:CMD7。
- 设置块长度:CMD16(可选,对于高容量卡可省略,但建议保留以兼容)。
- 设置块数量:CMD23。
- 启动多块读取:发送CMD18(多块读命令)。寄存器配置与CMD25类似,但需要将
DDIR位设置为1,表示数据方向为从卡到主机(读)。 - 数据接收:
- 非DMA模式:等待
BRR(Buffer Read Ready)状态,然后从MMCHS_DATA寄存器读取数据。 - DMA模式:配置DMA目的地址,使能
DE位。数据会自动搬运到内存。
- 非DMA模式:等待
5.3 高容量卡(SDHC/SDXC, eMMC)寻址差异
这是新手最容易踩坑的地方。标准容量卡(SDSC, <= 2GB)使用字节寻址。你在CMD17/18/24/25中指定的参数是32位的字节地址。
而高容量卡(SDHC: 2GB-32GB, SDXC: 32GB-2TB)和eMMC使用块寻址。你指定的参数是32位的块索引(每个块固定512字节)。这意味着:
- 你不能请求从非512字节对齐的地址读取。
- 你不能读取或写入小于512字节的数据。如果你只需要修改一个文件的前100字节,你也必须将包含这100字节的整个512字节块读出来,修改后再将整个块写回去。
在驱动中处理:在获取CSD后,需要解析CSD_STRUCTURE版本。如果是V2.0(对于SD卡),或从EXT_CSD中确认卡是高容量类型,则在后续所有数据读写命令中,将软件层的“字节地址”参数右移9位(除以512)转换为“块索引”,再写入MMCHS_ARG寄存器。
6. 错误处理、调试与实战经验
再严谨的流程也会遇到问题。一个健壮的驱动必须有完善的错误处理和调试手段。
6.1 常见错误状态解析
MMCHS_STAT寄存器是你的第一道防线。发生任何异常,首先读取它。
CTO(Command Timeout):命令超时。最常见的原因:物理连接问题(断线、接触不良)、卡未上电、卡处于错误状态、时钟频率过高或不稳定。排查步骤:检查硬件;降低时钟频率重试;发送CMD0复位卡。CCRC(Command CRC Error) /DCRC(Data CRC Error):CRC校验错误。表明传输过程中数据被破坏。可能原因:时钟频率过高导致采样错误;信号完整性差(过长走线、无端接);电源噪声大。排查步骤:降低时钟频率;检查PCB布线;加强电源滤波。CIE(Command Index Error):命令索引错误。卡响应的命令索引与发送的不符。比较罕见,可能表示严重的通信混乱或卡故障。DTO(Data Timeout):数据超时。卡在数据阶段无响应。可能原因:卡读写速度跟不上主机时钟;卡内部操作(如擦除)未完成;使用了不支持的块长度或地址。BADA(Bad Access):错误的数据缓冲区访问。在缓冲区未就绪时(BRE/BWE为0)尝试读写MMCHS_DATA寄存器。这通常是驱动程序的逻辑错误,比如没有正确等待状态就操作数据。
6.2 调试技巧与工具
- 逻辑分析仪是神器:抓取CMD、DAT、CLK线上的实际波形。你可以清晰地看到命令和响应的内容、数据流、以及时序关系。这是定位CRC错误、超时问题的终极手段。检查时钟边沿是否在数据稳定中心采样,CMD线上升/下降时间是否过慢。
- 善用状态寄存器:在关键步骤(如发送命令后、读写数据前后)打印或记录
MMCHS_PSTATE和MMCHS_STAT的值。CMDI和DATI位能告诉你总线是否忙。 - 分阶段验证:不要试图一口气写完所有功能。先确保CMD0有响应,再试CMD8/CMD1,然后读CID/CSD,最后再尝试单块读写,成功后再进行多块和高速模式测试。
- 电压与时钟检查:用万用表确认给卡的供电电压是否准确稳定。用示波器测量CLK输出频率是否与寄存器配置值相符,波形是否干净。
6.3 性能优化考量
- 使用DMA:这能极大释放CPU负担,提升吞吐量。确保DMA源/目标地址对齐(通常32位对齐最佳),并配置正确的突发传输大小。
- 合理设置块大小:虽然协议支持1-2048字节的块,但512字节是通用且高效的。对于大文件传输,使用多块命令(CMD18/CMD25)并配合CMD23预定义块数,比多次发送单块命令快得多。
- 中断 vs 轮询:对于低延迟或CPU负载敏感的应用,使用中断(配置
MMCHS_IE和MMCHS_ISE)是更好的选择。对于简单的初始化过程,轮询可能更直接。 - 时钟频率管理:在初始化阶段用低速(400kHz),识别后根据CSD信息切换到卡支持的最高稳定频率。在系统进入低功耗模式时,可以考虑降低SD卡时钟频率或关闭时钟输出。
编写MMC/SD/SDIO驱动是一个对细节要求极高的过程,它融合了硬件知识、协议理解和软件设计。从理解每个寄存器的含义,到组织正确的命令序列,再到处理各种边界情况和错误,每一步都需要耐心和严谨。希望这篇基于实战的指南,能帮你搭建起清晰的知识框架,少走一些我当年走过的弯路。当你看到dd if=/dev/mmcblk0 of=test.bin成功读出数据的那一刻,所有这些繁琐的配置和调试都值了。