首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

SD存储卡

SD卡是目前移动设备上普遍使用的一种外部存储设备。全称为Secure Digital Memory Card(SD Card), 是闪迪、松下电器和东芝在MMC(Multi-Media Card)的基础上进行改进的存储卡, 与1999年联合发布。

下图1为三种常见的SD卡,最左边是标准的SD卡,在早期的数码相机、笔记本上见到的比较多。最右边是MicroSD卡,也称为TF卡,相比于标准的SD卡, 它的尺寸很小是目前手机等设备的主要扩展存储方式。至于中间的MiniSD卡,我并没有见过。

STM32为SD卡以及SDIO设备提供了一个SDIO的接口,它支持MMC,SD存储卡,SDIO卡,CE-ATA设备。至于这些不同的设备之间是什么关系, 我们暂时不考虑,这里只关心如何使用STM32的SDIO接口实现对SD存储卡的读写访问。先介绍SD卡的协议描述,然后介绍STM32中SDIO模块及相关寄存器, 最后我们将实现能够对SD存储卡进行读写的驱动。

图1 各种SD卡

1. SD卡协议简述

这里简述的协议主要参考的是2006年发布的2.0版本。 该文档描述的是,标准尺寸的SD存储卡的物理接口、总线的拓扑逻辑、以及命令协议。

SD存储卡的通信协议的物理层是建立在一个9针的接口(Clock, Command, 4xData, 3xPower lines)上的,最大的工作频率为50MHz。其容量有两种: 小于等于2G的卡称为标准容量卡(standard Capacity),大于2G小于等于32G的卡称为大容量卡(High Capacity)。

1.1 物理接口和拓扑逻辑

SD卡的接口和引脚定义如下表1所示,它支持SD和SPI两种通信模式。系统上电以后,宿主系统需要先发送一条复位指令(reset command), 设定SD卡的通信模式,在系统没有重新上电的情况下是不可以改变通信模式的。

SD卡总线是一种"一主多从","星型拓扑","同步通信"的总线。一主多从体现在,宿主系统可以同时挂载多个SD卡, 在不同的模式下连接形式有所不同。在SD模式下,各个SD卡的时钟可以共用一个信号,但是CMD和DAT[0:3]总线信号不可以共用, 应当为每个SD卡单独提供提供一套SD总线。在SPI模式下,时钟信号和数据信号都可以共用,但是需要为每个SD卡提供一个片选信号。 因此,各个SD卡都是直接与宿主连接和通信的,就是所谓的星型拓扑。 而同步通信是指,它在整个通信过程中需要由主设备(宿主系统)提供一个驱动时钟。

因为我们将用STM32提供的SDIO接口访问SD卡,这里主要关注SD通信模式。虽然SD模式下有4条数据线DAT[0:3],但刚上电后,只有DAT0用于数据传输。 完成SD卡的初始化工作后,宿主就可以根据需要综合考虑通信速率和引脚资源,修改数据线宽使用全部四条数据线。

表 1 SD卡引脚定义

Pin SD模式 SPI模式
名称 描述 名称 描述
1 CD/DAT3 Card Detect/数据线[Bit 3] CS 片选(低电平有效)
2 CMD 指令/响应 DI Data In
3 VSS1 VSS1
4 VDD 电源 VDD 电源
5 CLK 时钟 SCLK 时钟
6 VSS2 VSS2
7 DAT0 数据线[Bit 0] DO Data Out
8 DAT1 数据线[Bit 1] RSV 保留
9 DAT2 数据线[Bit 2] RSV 保留

1.2 总线协议

对于SD总线,其通信过程中涉及到三个要素:指令(command)、响应(response)和数据(data)。指令和响应都是在CMD线上进行传输的, 而数据则是在数据线上传输的。每次通信都是由宿主发送一个指令到SD卡开始,针对不同的指令SD卡可能有应答也可能没有, 但有数据传输的指令一定有响应。

SD卡的数据读写都是以块(block)的形式进行传输的。下图2中描述了读数据块操作的时序关系,宿主先向SD卡发送一条读指令, SD卡返回一个响应,然后SD卡就从数据线上发送数据块给宿主,数据块之后还会有一个CRC校验。 如果指令请求了多个数据块,那么SD卡会连续地发送数据,直到宿主发送了一条停止的指令为止。

图2 SD读数据块操作

下图3中描述了写数据块操作的时序关系。同样的宿主先向SD卡发送一条写指令,接收到SD卡返回的响应后,宿主再向总线上写数据和CRC校验。 SD卡需要把接收到的数据写到存储区中,同时需要进行CRC校验。在宿主发送完CRC校验后,SD卡完成CRC校验之前, SD卡都会通过DAT0数据线通知宿主其正处于busy状态。只有在busy状态解除后,才可以继续发送数据。

图3 SD写数据块操作

每条指令都以'0'开始,以'1'结束,一共有48位,其中第2位为'1',用于描述其是一条来自宿主的指令。 然后是CONTENT部分,有38位,记录了指令内容和寻址信息或者参数。 它被其后的7为CRC校验所保护,如果传输错误导致校验和不一致,可能会重新发送。根据SD卡回复的响应内容的不同,有48位和136位两种响应形式。 它们的起始位和结束位与指令一样,第二位为'0',用于描述其是一条来自SD卡的响应。然后就是CONTENT和CRC校验。

此外,有一点需要说明的是,在CMD和DAT线上都是先发送高位,再发送低位的,表现出来就是大端数据。 至于指令与响应之间的对应关系,以及数据格式,这里不再赘述, 读者可以参考文档的第4.7节和第3.6节

1.3 SD卡的寄存器

SD卡的结构和信息寄存器如下表2所示。

表 2 SD卡结构和寄存器

寄存器 Width 描述
CID 128 卡标识号(Card identification number), 参考第5.2节
RCA 16 卡相对地址(Relative card address):宿主系统中对于一个卡的寻址,动态变化的,在宿主初始化过程中确定。 参考第5.4节
DSR 16 驱动配置寄存器(Driver stage register):用于配置SD卡的输出驱动。 参考第5.5节
CSD 128 卡描述数据(Card Specific Data):用于描述卡工作条件的信息。 参考第5.3节
SCR 64 SD配置寄存器(SD configuration register):关于SD的一些特性信息。 参考第5.6节
OCR 32 操作条件寄存器(Operation conditions register):用于配置SD卡的输出驱动。 参考第5.1节
SSR 512 SD状态寄存器(SD status register):卡专有特性的一些信息。 参考第4.10.2节
CSR 32 卡状态寄存器(Driver stage register)。 参考第4.10.1节

2. STM32的SDIO接口

STM32F407中的SDIO是APB2外设总线与MMC,SD存储卡,SDIO卡,CE-ATA等设备之间的一个通用接口。 它只提供了SD通信模式,而且目前只支持一张SD卡。SDIO由SDIO适配器(SDIO adapter)和APB2接口两部分组成, 其结构如下图4所示。SDIO适配器提供了它所支持的各种卡的功能实现;APB2接口则用于访问SDIO适配器的寄存器,产生中断和DMA请求信号。

图4 SDIO结构框图

该模块涉及到SDIOCLK和PCLK2两个时钟。SDIOCLK由一个48MHz的时钟驱动, 这是一个和USB OTG共用的时钟,与系统主频率无关。PCLK2则是APB2总线上的时钟频率,它是由系统时钟分出的一个84MHz的时钟。 因为SD通信是一种同步通信方式,所以SDIO会通过SDIO_CK引脚产生一个0到25MHz的时钟给SD卡。 SDIO_CK与PCLK2之间必须满足如下的关系: $$ f_{PCLK2} ≥ 3/8 \times f_{SDIO\_CK} $$

SDIO一共有8个数据线,默认情况下SDIO_D0用于数据传输,初始化之后,可以修改总线位宽。对于SD或者SDIO卡, 可以配置使用SDIO_D0或者SDIO_D[3:0]进行数据传输。 所有的这些数据线都应工作在推挽模式下。

SDIO_CMD有两种工作模式,一种是用于初始化的开漏模式,另一种是用于指令传送的推挽模式。在我理解, 第一种工作模式,纯粹是为了兼容V3.31及更早版本的MMC卡而设计的,因为SD卡的初始化过程也是在推挽模式下进行的。

2.1 SDIO适配器

下图5为SDIO适配器的结构框图。它有五个部分组成:适配器寄存器块、控制单元、指令通道、数据通道、数据FIFO。寄存器块很好理解, 就是一组寄存器用于编程控制SD卡。

图5 SDIO适配器框图

控制单元则用于管理模块的电源和时钟。SDIO有Power-off, Power-up, Power-on三种供电状态,在Power-off和Power-up状态下, SDIO是不会有输出信号到总线上的。其时钟管理单元,是用来生成SDIO_CK时钟信号的。在power-off和power-up状态下不输出时钟信号, 如果配置了省电模式,那么当总线进入空闲状态(Idel state)时也将自动关闭时钟信号。

指令通道是指令和响应的通道。它维持了一个状态机CPSM,控制指令通道在几种状态之间切换,下图是指令发送的一个简图, 关于CPSM的详细信息还需要查看参考手册。 对于图6有一点需要强调的是,接收到SD卡的响应后需要间隔至少8个SDIO_CK的周期才可以发送下一条指令。

图6 SDIO指令传送简图

数据通道是STM32与SD卡之间的传送数据的通道。和指令通道一样它也维持了一个状态机称为DPSM。 STM32的SDIO机制支持流数据和块数据,而SD卡是块数据访问的。

数据FIFO则是发送和接受数据的一个缓存。它是有32个32位宽的先入先出队列构成的,特别的它由PCLK2时钟驱动。 本质上SD通信是一种半双工的通信方式,所以同一时间SDIO控制器只能处于接受或者发送的一个状态下, 因此这32个缓存队列对于收和发是复用的。

2.2 APB2接口

APB2接口用于产生中断和DMA请求,它是CPU访问SDIO适配器的寄存器和数据FIFO的通道。至于中断和DMA请求, 可以参考外部中断控制LED灯直接内存访问-DMA两篇文章。至于CPU访问寄存器和FIFO, 则完全是外设寄存器的操作。 这里不再做深入的介绍。

3. SD卡的驱动实现

参考在通用IO中介绍的访问外设寄存器的方法, 我们定义SDIO寄存器的结构体sdio_regs_t,并定义如下的宏用于访问SDIO。 这里打包了本文示例中所用的代码和工程文件。

        /* SDIO寄存器地址映射 */
        #define SDIO_BASE 0x40012C00
        /* SDIO寄存器指针访问 */
        #define SDIO ((sdio_regs_t *) SDIO_BASE)

3.1 SDIO接口的初始化过程

在于SD卡通信之前,我们需要先对STM32上的SDIO接口进行初始化,定义函数sdio_init_interface()如下:

        static void sdio_init_interface(void) {
            sdio_init_hw();

            sdio_init_clkcr(118, SDIO_BusWid_1);

            SDIO->POWER.bits.PWRCTRL = SDIO_Power_On;
            SDIO->CLKCR.bits.CLKEN = 1;
        }

首先,调用函数sdio_init_hw()中,配置引脚PC8, PC9, PC10, PC11复用做SDIO_D[0:3], 引脚PD2复用做SDIO_CMD,引脚PC12复用做SDIO_CK。同时打开了DMA2和APB2总线上的SDIO驱动时钟。

然后,通过函数sdio_init_clkcr()设置SDIO的时钟配置寄存器。该函数有clkdiv和buswid两个参数,这里分别赋值118和SDIO_BusWid_1。 clkdiv是由48MHz的时钟产生通信驱动时钟SDIO_CK的分频系数,存在关系\(f_{SDIO\_CK} = \frac{48\times 10^3}{clkdiv + 2}\)kHz。 根据SD卡协议, 我们知道刚上电时SD卡处于枚举状态(identification),此时的通信时钟频率最高就只能是400kHz,所以这里clkdiv赋予118,以产生其支持的最高频率。 buswid则用于设定总线宽度,而在刚上电时SD卡默认只有DAT0用于数据线,所以将之设定为SDIO_BusWid_1。

最后控制SDIO上电,同时SDIO_CK引脚输出驱动时钟。至此,就完成了宿主的初始化工作,下面可以通过SDIO发送指令到SD卡了。

3.2 SD卡的枚举过程

协议上说SD总线可以接多个设备,但目前STM32的SDIO接口同时只支持一个SD卡,所以这里实现的枚举过程实际上是针对一张SD卡的。 其流程如下图7所示,

图7 SD卡枚举过程

当宿主系统对SDIO上电(power on)之后,SD卡都处于idle的状态,并等待接收来自宿主的指令。 通过函数sdio_send_cmd()向SD卡发送指令GO_IDLE_STATE(CMD0)。它是一个SD卡的复位指令, 除非SD卡处于inactive状态,否则接收到该指令后都会进入idle状态。

        static enum SD_Error sdio_init_card(struct sd_card *card) {
            sdio_send_cmd(SD_CMD_GO_IDLE_STATE, 0, SDIO_Response_No);
            enum SD_Error e = sdio_check_resp0();
            if (SDE_OK != e)
                return e;

函数sdio_send_cmd()有三个参数:cmd, arg, res。其中cmd描述了发送指令的索引编号,arg则是该指令所需要的参数,res定义了指令的响应类型。 CMD0指令是不会有响应的,所以这里的res传参为SDIO_Response_No。在协议中定义了R1, R2, R3, R6, R7一共五种响应,这里我们把不响应定义为响应R0, 并通过函数sdio_check_resp0()查询SDIO状态寄存器的指令发送位(STA.cmdsent),判定是否发送是否超时。

SD卡上电后本来就处于idle状态,现在发送指令CMD0让其进入idle状态,这一操作看似多余,实际很重要。 因为,这一操作还会协商宿主与SD卡之间的通信是SD模式还是SPI模式。这里我们使用的是STM32的SDIO模块,所以协商结果就是SD模式。 但如果在发送CMD0时,数据线DAT3或者说是SPI模式下的片选信号线为低电平时,SD卡就会进入SPI模式。

V2.0版的协议要求,SD卡复位后必须发送指令SEND_IF_COND(CMD8),检测SD卡支持的协议和电压。我们在函数sdio_cmd8()中, 发送该指令,判定SD卡是否支持3.3V的电压。只有支持V2.0的SD卡才会返回一个R7的响应,否则是不会有响应返回的。 所以这里我们先判定sdio_cmd8()返回的错误类型,如果超时就认为是V1.1版的SD卡,如果正常则是V2.0版的SD卡。

            e = sdio_cmd8();
            if (SDE_CMD_RSP_TIMEOUT == e)
                card->cardtype = SDIO_STD_CAPACITY_SD_CARD_V1_1;
            else if (SDE_OK == e)
                card->cardtype = SDIO_STD_CAPACITY_SD_CARD_V2_0;
            else
                return e;

判定完SD卡支持的版本之后,我们需要向SD卡发送SD_SEND_OP_COND(ACMD41)。因为ACMD41是一个应用指令, 需要先发送一条APP_CMD(CMD55)指令,再发送ACMD41。

            bool ready = FALSE;
            uint32 count = 0, res = 0;
            while (!ready && (count < SD_MAX_VOLT_TRIAL)) {
                e = sdio_cmd55(card, 0);
                if (SDE_OK != e)
                    return e;
                e = sdio_acmd41();
                if (SDE_OK != e)
                    return e;
                res = SDIO->RESP1;
                ready = (((res >> 31) == 1) ? TRUE : FALSE);
                count++;
            }

            if (count >= SD_MAX_VOLT_TRIAL)
                return SDE_INVALID_VOLTRANGE;

这里的代码没有体现ACMD41的细节,它实际上是对OCR寄存器的操作,有些内容需要特别强调一下:

因为V2.0版本以后的SD卡可以支持2G以上容量,因而分为了标准容量和大容量两种卡。 ACMD41返回的数据中有一个CCS位标记了SD卡是否为大容量卡。

            if (SDIO_STD_CAPACITY_SD_CARD_V2_0 == card->cardtype)
               card->cardtype = (((res >> 30) & 1)
                              ? SDIO_HIGH_CAPACITY_SD_CARD
                              : SDIO_STD_CAPACITY_SD_CARD_V2_0);

接着,我们向SD卡发送指令ALL_SEND_CID(CMD2),索取枚举信息(card identification, CID)。SD卡接收到该指令后, 将CID寄存器中的数据返回给宿主,然后进入identification模式。

            // CMD2: 获取枚举信息CID
            sdio_send_cmd(SD_CMD_ALL_SEND_CID, 0, SDIO_Response_Long);
            e = sdio_check_resp2();
            if (SDE_OK != e)
                return e;
            card->cid.words[0] = SDIO->RESP1;
            card->cid.words[1] = SDIO->RESP2;
            card->cid.words[2] = SDIO->RESP3;
            card->cid.words[3] = SDIO->RESP4;

最后,我们发送SET_RELATIVE_ADDR(CMD3)到SD卡,为检测到的卡分配一个相对地址(RCA)。 此时,SD卡将进入stand-by状态,如果希望修改SD卡的相对地址,我们只需再发送一个CMD3到该卡即可。

            // CMD3
            sdio_send_cmd(SD_CMD_SET_REL_ADDR, 0, SDIO_Response_Short);
            uint16 rca = 0x01;
            e = sdio_check_resp6(SD_CMD_SET_REL_ADDR, &rca);
            if (SDE_OK != e)
                return e;
            card->rca = rca;

            return SDE_OK;
        }

至此完成了对SD卡的枚举操作,但此时的通信频率只有400kHz,只用了DAT0一条数据线,远没有发挥出SD通信的性能。 下面我们将做一些配置,尽可能的提高SDIO的性能。

3.3 SDIO的初始化操作

SD接口有4条数据线,一个字节可以在2个驱动时钟内传送。 它支持最高的通信频率为25MHz,但在枚举模式下最高只能工作在400kHz。在下面的初始化函数sdio_init()中,完成了对SDIO模块和SD卡的初始化操作。

        enum SD_Error sdio_init(struct sd_card *card) {
            sdio_init_interface();

            enum SD_Error e = sdio_init_card(card);
            if (SDE_OK != e)
                return e;           

            sdio_init_clkcr(0, SDIO_BusWid_1);

            e = sdio_load_card_info(card);
            if (SDE_OK != e)
                return e;

            e = sdio_sel(card);
            if (SDE_OK != e)
                return e;

            return sdio_en_widemode(card);
        }

在这个函数中调用的前两个函数分别是前面介绍的用于SDIO接口初始化SD卡枚举的函数。 这两个函数成功运行后,SD卡就进入了数据传送模式data transfer mode,在该模式下可以有更高的通信频率和更宽的数据总线。

第8行再次调用了函数sdio_init_clkcr()修改SD的通信频率为24MHz。此时尚不知道挂载的SD卡是否支持4路数据线, 所以我们仍然用1位宽的总线进行通信。在函数sdio_load_card_info()中,我们发送指令SEND_CSD(CMD9)来获取SD卡的描述数据, 包括数据块大小、容量等信息。

接着在函数sdio_sel()中,我们发送了一次CMD7的指令。CMD7用于控制一个指定的SD卡在stand-bytransfer状态之间切换。 函数sdio_init_card()成功执行后,SD卡就进入了stand-by状态,发送一次CMD7指令,SD卡进入transfer状态,再次发送将回到stand-by状态。 只有在transfer状态下的SD卡才可以通过数据线与宿主通信。

最后我们在函数sdio_en_widemode()中提高通信的数据线宽。在该函数中,我们先通过指令ACMD51获取SD卡的SCR寄存器,并判定是否支持宽数据线。 如果支持则通过指令ACMD6修改数据线宽,否则保持不变。

3.4 基于DMA的SD卡读写操作

完成SD卡的初始化操作以后,我们就可以通过指令CMD17读取一个数据块或者CMD18读取多个数据块, 也可以通过指令CMD24写一个数据块或者CMD25写多个数据块。这里我们以读写单个数据块为例进行介绍。

        enum SD_Error sdio_read_block(struct sd_card *card, uint32 bnum, uint8 *buf) {
            // 设置Block大小
            uint16 blocksize = 512;
            sdio_send_cmd(SD_CMD_SET_BLOCKLEN, blocksize, SDIO_Response_Short);
            enum SD_Error e = sdio_check_resp1(card, SD_CMD_SET_BLOCKLEN);
            if (SDE_OK != e)
                return e;

这是一个读单个数据块的函数,首先我们通过CMD16设定数据块的大小,一个数据块最大为512个字节。

            // 设置数据
            SDIO->DCTRL.all = 0;
            union sdio_dctrl dctrl;
            dctrl.all = SDIO->DCTRL.all;
            dctrl.bits.DBLOCKSIZE = SDIO_DataBlockSize_512b;
            dctrl.bits.DTDIR = SDIO_TransDir_ToSdio;
            dctrl.bits.DTMODE = SDIO_TransMode_Block;
            dctrl.bits.DTEN = 1;
            sdio_config_data(dctrl, SD_DATATIMEOUT, blocksize);

这里,我们修改SDIO接口的DCTRL寄存器,设定数据传输方向、数据块大小、传输模式等信息。

            // CMD17
            sdio_send_cmd(SD_CMD_READ_SINGLE_BLOCK, bnum, SDIO_Response_Short);
            e = sdio_check_resp1(card, SD_CMD_READ_SINGLE_BLOCK);
            if (SDE_OK != e)
                return e;

然后发送指令CMD17到SD卡请求读数据。

            // 开启DMA和中断
            sdio_enable_interrupts();
            SDIO->DCTRL.bits.DMAEN = 1;
            sdio_config_dma_rx((uint32 *)buf, blocksize);
            return e;
        }

最后,开启DMA和中断设置。写数据块的过程与读类似,只是修改SDIO接口的DCTRL寄存器时,需要设定数据方向为SDIO_TransDir_ToCard。 发送的指令从CMD17变更为CMD24即可。

4. 总结

虽然SD卡的协议是说SD总线通信是一个一主多从,星形连接的拓扑结构,但目前STM32的实现只支持一个SD卡设备。

SD卡支持SD和SPI两种通信模式,刚上电时需要由宿主系统发送一条CMD0的指令来协商通信模式,这里我们使用的是STM32的SDIO所以协商结果就是SD模式。 通过CMD0指令SD卡进入idle状态,发送指令CMD8判定SD卡类型,发送ACMD41判定SD卡的供电电压是否与宿主系统匹配、是否为大容量SD卡。 然后发送CMD2枚举SD卡的设备信息,发送CMD3分配SD卡相对地址。至此完成SD的枚举操作。

在枚举过程中总线的驱动频率最高位400kHz,默认情况下SD通信采用的1位的总线宽度。实际SD卡最高支持25MHz的驱动频率,4位的总线宽度。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1