37. SDIO—SD卡读写测试

本章参考资料:《STM32F10X-中文参考手册》、《STM32F103增强型系列数据手册》、 库帮助文档《stm32f10x_stdperiph_lib_um.chm》以及SD简易规格文件 《Physical Layer Simplified SpecificationV2.0》(版本号:2.00)。

阅读本章内容之前,建议先阅读SD简易规格文件。

37.1. SDIO简介

SD卡(Secure Digital Memory Card)在我们生活中已经非常普遍了,控制器对SD卡进行读写通信操作一般有两种通信接口可选, 一种是SPI接口,另外一种就是SDIO接口。SDIO全称是安全数字输入/输出接口,多媒体卡(MMC)、SD卡、SD I/O卡都有SDIO接口。 STM32F10x系列控制器有一个SDIO主机接口,它可以与MMC卡、SD卡、SD I/O卡以及CE-ATA设备进行数据传输。MMC卡可以说是SD卡的前身, 现阶段已经用得很少。SD I/O卡本身不是用于存储的卡,它是指利用SDIO传输协议的一种外设。比如Wi-Fi Card, 它主要是提供Wi-Fi功能,有些Wi-Fi模块是使用串口或者SPI接口进行通信的,但Wi-Fi SDIO Card是使用SDIO接口进行通信的。 并且一般设计SD I/O卡是可以插入到SD的插槽。CE-ATA是专为轻薄笔记本硬盘设计的硬盘高速通讯接口。

多媒体卡协会网站www.mmca.org中提供了有MMCA技术委员会发布的多媒体卡系统规范。

SD卡协会网站www.sdcard.org中提供了SD存储卡和SDIO卡系统规范。

CE-ATA工作组网站www.ce-ata.org中提供了CE_ATA系统规范。

随之科技发展,SD卡容量需求越来越大,SD卡发展到现在也是有几个版本的, 关于SDIO接口的设备整体概括见图 SDIO接口的设备

SDIO接口的设备

关于SD卡和SD I/O部分内容可以在SD协会网站获取到详细的介绍,比如各种SD卡尺寸规则、读写速度标示方法、应用扩展等等信息。

本章内容针对SD卡使用讲解,对于其他类型卡的应用可以参考相关系统规范实现,所以对于控制器中针对其他类型卡的内容可能在本章中简单提及或者被忽略, 本章内容不区分SDIO和SD卡这两个概念。即使目前SD协议提供的SD卡规范版本最新是4.01版本,但STM32F10x系列控制器只支持SD卡规范版本2.0, 即只支持标准容量SD和高容量SDHC标准卡,不支持超大容量SDXC标准卡,所以可以支持的最高卡容量是32GB。

37.2. SD卡物理结构

一张SD卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器5个部分,见图 SD卡物理结构。 存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证SD卡工作在合适的电压下, 如出现掉电或上状态时,它会使控制单元和存储单元接口复位;卡及接口控制单元控制SD卡的运行状态,它包括有8个寄存器;接口驱动器控制SD卡引脚的输入输出。

SD卡物理结构

SD卡总共有8个寄存器,用于设定或表示SD卡信息,参考表 SD卡寄存器。这些寄存器只能通过对应的命令访问, 对SD卡进行控制操作并不是像操作控制器GPIO相关寄存器那样一次读写一个寄存器的,它是通过命令来控制, SDIO定义了64个命令,每个命令都有特殊意义,可以实现某一特定功能,SD卡接收到命令后, 根据命令要求对SD卡内部寄存器进行修改,程序控制中只需要发送组合命令就可以实现SD卡的控制以及读写操作。

SD卡寄存器

每个寄存器位的含义可以参考SD简易规格文件《Physical Layer Simplified Specification V2.0》第5章内容。

37.3. SDIO总线

37.3.1. 总线拓扑

SD卡一般都支持SDIO和SPI这两种接口,本章内容只介绍SDIO接口操作方式,如果需要使用SPI操作方式可以参考SPI相关章节。 另外,STM32F10x系列控制器的SDIO是不支持SPI通信模式的,如果需要用到SPI通信只能使用SPI外设。

SD卡总线拓扑参考图 SD卡总线拓扑。虽然可以共用总线, 但不推荐多卡槽共用总线信号,要求一个单独SD总线应该连接一个单独的SD卡。

SD卡总线拓扑

SD卡使用9-pin接口通信,其中3根电源线、1根时钟线、1根命令线和4根数据线,具体说明如下:

  • CLK:时钟线,由SDIO主机产生,即由STM32控制器输出;

  • CMD:命令控制线,SDIO主机通过该线发送命令控制SD卡,如果命令要求SD卡提供应答(响应),SD卡也是通过该线传输应答信息;

  • D0-3:数据线,传输读写数据;SD卡可将D0拉低表示忙状态;

  • VDD、VSS1、VSS2:电源和地信号。

在之前的I2C以及SPI章节都有详细讲解了对应的通信时序,实际上,SDIO的通信时序简单许多,SDIO不管是从主机控制器向SD卡传输, 还是SD卡向主机控制器传输都只以CLK时钟线的上升沿为有效。SD卡操作过程会使用两种不同频率的时钟同步数据,一个是识别卡阶段时钟频率FOD, 最高为400kHz,另外一个是数据传输模式下时钟频率FPP,默认最高为25MHz,如果通过相关寄存器配置使SDIO工作在高速模式,此时数据传输模式最高频率为50MHz。

对于STM32控制器只有一个SDIO主机,所以只能连接一个SDIO设备, 开发板上集成了一个Micro SD卡槽和SDIO接口的WiFi模块,要求只能使用其中一个设备。 SDIO接口的WiFi模块一般集成有使能线,如果需要用到SD卡需要先控制该使能线禁用WiFi模块。

37.3.2. 总线协议

SD总线通信是基于命令和数据传输的。通讯由一个起始位(“0”),由一个停止位(“1”)终止。SD通信一般是主机发送一个命令(Command), 从设备在接收到命令后作出响应(Response),如有需要会有数据(Data)传输参与。

SD总线的基本交互是命令与响应交互,见图 命令与响应交互

命令与响应交互

SD数据是以块(Black)形式传输的,SDHC卡数据块长度一般为512字节,数据可以从主机到卡, 也可以是从卡到主机。数据块需要CRC位来保证数据传输成功。CRC位由SD卡系统硬件生成。 STM32控制器可以控制使用单线或4线传输,本开发板设计使用4线传输。图 多块写入操作 为主机向SD卡写入数据块操作示意。

多块写入操作

SD数据传输支持单块和多块读写,它们分别对应不同的操作命令,多块写入还需要使用命令来停止整个写入操作。 数据写入前需要检测SD卡忙状态,因为SD卡在接收到数据后编程到存储区过程需要一定操作时间。SD卡忙状态通过把D0线拉低表示。

数据块读操作与之类似,只是无需忙状态检测。

使用4数据线传输时,每次传输4bit数据,每根数据线都必须有起始位、终止位以及CRC位,CRC位每根数据线都要分别检查,并把检查结果汇总然后在数据传输完后通过D0线反馈给主机。

SD卡数据包有两种格式,一种是常规数据(8bit宽),它先发低字节再发高字节,而每个字节则是先发高位再发低位,4线传输示意如图 8位宽数据包传输

8位宽数据包传输

4线同步发送,每根线发送一个字节的其中两个位,数据位在四线顺序排列发送,DAT3数据线发较高位,DAT0数据线发较低位。

另外一种数据包发送格式是宽位数据包格式,对SD卡而言宽位数据包发送方式是针对SD卡SSR(SD状态)寄存器内容发送的, SSR寄存器总共有512bit,在主机发出ACMD13命令后SD卡将SSR寄存器内容通过DAT线发送给主机。宽位数据包格式示意见图 宽位数据包传输

宽位数据包传输

37.3.3. 命令

SD命令由主机发出,以广播命令和寻址命令为例,广播命令是针对与SD主机总线连接的所有从设备发送的,寻址命令是指定某个地址设备进行命令传输。

37.3.3.1. 命令格式

SD命令格式固定为48bit,都是通过CMD线连续传输的(数据线不参与),见图 SD命令格式

SD命令格式

SD命令的组成如下:

  • 起始位和终止位:命令的主体包含在起始位与终止位之间,它们都只包含一个数据位,起始位为0,终止位为1。

  • 传输标志:用于区分传输方向,该位为1时表示命令,方向为主机传输到SD卡,该位为0时表示响应,方向为SD卡传输到主机。

命令主体内容包括命令、地址信息/参数和CRC校验三个部分。

  • 命令号:它固定占用6bit,所以总共有64个命令(代号:CMD0~CMD63),每个命令都有特定的用途, 部分命令不适用于SD卡操作,只是专门用于MMC卡或者SD I/O卡。

  • 地址/参数:每个命令有32bit地址信息/参数用于命令附加内容,例如,广播命令没有地址信息, 这32bit用于指定参数,而寻址命令这32bit用于指定目标SD卡的地址。

  • CRC7校验:长度为7bit的校验位用于验证命令传输内容正确性, 如果发生外部干扰导致传输数据个别位状态改变将导致校准失败,也意味着命令传输失败,SD卡不执行命令。

37.3.3.2. 命令类型

SD命令有4种类型:

  • 无响应广播命令(bc),发送到所有卡,不返回任务响应;

  • 带响应广播命令(bcr),发送到所有卡,同时接收来自所有卡响应;

  • 寻址命令(ac),发送到选定卡,DAT线无数据传输;

  • 寻址数据传输命令(adtc),发送到选定卡,DAT线有数据传输。

另外,SD卡主机模块系统旨在为各种应用程序类型提供一个标准接口。在此环境中,需要有特定的客户/应用程序功能。为实现这些功能, 在标准中定义了两种类型的通用命令:特定应用命令(ACMD)和常规命令(GEN_CMD)。要使用SD卡制造商特定的ACMD命令如ACMD6, 需要在发送该命令之前发送CMD55命令,告知SD卡接下来的命令为特定应用命令。CMD55命令只对紧接的第一个命令有效, SD卡如果检测到CMD55之后的第一条命令为ACMD则执行其特定应用功能,如果检测发现不是ACMD命令,则执行标准命令。

37.3.3.3. 命令描述

SD卡系统的命令被分为多个类,每个类支持一种“卡的功能设置”。表 SD部分命令描述 列举了SD卡部分命令信息, 更多详细信息可以参考SD简易规格文件说明,表中填充位和保留位都必须被设置为0。

虽然没有必须完全记住每个命令详细信息,但越熟悉命令对后面编程理解非常有帮助。

SD部分命令描述

37.3.4. 响应

响应由SD卡向主机发出,部分命令要求SD卡作出响应,这些响应多用于反馈SD卡的状态。SDIO总共有7个响应类型(代号:R1~R7), 其中SD卡没有R4、R5类型响应。特定的命令对应有特定的响应类型,比如当主机发送CMD3命令时,可以得到响应R6。与命令一样, SD卡的响应也是通过CMD线连续传输的。根据响应内容大小可以分为短响应和长响应。短响应是48bit长度, 只有R2类型是长响应,其长度为136bit。各个类型响应具体情况如表 SD卡响应类型

除了R3类型之外,其他响应都使用CRC7校验来校验,对于R2类型是使用CID和CSD寄存器内部CRC7。

SD卡响应类型

37.4. SD卡的操作模式及切换

37.4.1. SD卡的操作模式

SD卡有多个版本,STM32控制器目前最高支持《Physical Layer Simplified Specification V2.0》定义的SD卡, STM32控制器对SD卡进行数据读写之前需要识别卡的种类:V1.0标准卡、V2.0标准卡、V2.0高容量卡或者不被识别卡。

SD卡系统(包括主机和SD卡)定义了两种操作模式:卡识别模式和数据传输模式。在系统复位后,主机处于卡识别模式, 寻找总线上可用的SDIO设备;同时,SD卡也处于卡识别模式,直到被主机识别到,即当SD卡接收到SEND_RCA(CMD3)命令后, SD卡就会进入数据传输模式,而主机在总线上所有卡被识别后也进入数据传输模式。在每个操作模式下, SD卡都有几种状态,参考表 SD卡状态与操作模式 ,通过命令控制实现卡状态的切换。

SD卡状态与操作模式

37.4.2. 卡识别模式

在卡识别模式下,主机会复位所有处于“卡识别模式”的SD卡,确认其工作电压范围,识别SD卡类型, 并且获取SD卡的相对地址(卡相对地址较短,便于寻址)。在卡识别过程中,要求SD卡工作在识别时钟频率FOD的状态下。 卡识别模式下SD卡状态转换如图 卡识别模式状态转换图

卡识别模式状态转换图

主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。主机也可以发送GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并不会复位。

主机在开始与卡通信前,需要先确定双方在互相支持的电压范围内。SD卡有一个电压支持范围,主机当前电压必须在该范围可能才能与卡正常通信。 SEND_IF_COND(CMD8)命令就是用于验证卡接口操作条件的(主要是电压支持)。卡会根据命令的参数来检测操作条件匹配性,如果卡支持主机电压就产生响应, 否则不响应。而主机则根据响应内容确定卡的电压匹配性。CMD8是SD卡标准V2.0版本才有的新命令,所以如果主机有接收到响应,可以判断卡为V2.0或更高版本SD卡。

SD_SEND_OP_COND(ACMD41)命令可以识别或拒绝不匹配它的电压范围的卡。ACMD41命令的VDD电压参数用于设置主机支持电压范围,卡响应会返回卡支持的电压范围。 对于对CMD8有响应的卡,把ACMD41命令的HCS位设置为1,可以测试卡的容量类型,如果卡响应的CCS位为1说明为高容量SD卡,否则为标准卡。 卡在响应ACMD41之后进入准备状态,不响应ACMD41的卡为不可用卡,进入无效状态。ACMD41是应用特定命令,发送该命令之前必须先发CMD55。

ALL_SEND_CID(CMD2)用来控制所有卡返回它们的卡识别号(CID),处于准备状态的卡在发送CID之后就进入识别状态。之后主机就发送SEND_RELATIVE_ADDR(CMD3)命令, 让卡自己推荐一个相对地址(RCA)并响应命令。这个RCA是16bit地址,而CID是128bit地址,使用RCA简化通信。卡在接收到CMD3并发出响应后就进入数据传输模式, 并处于待机状态,主机在获取所有卡RCA之后也进入数据传输模式。

37.4.3. 数据传输模式

只有SD卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以将主机SD时钟频率设置为FPP,默认最高为25MHz, 频率切换可以通过CMD4命令来实现。数据传输模式下,SD卡状态转换过程见图 数据传输模式卡状态转换

数据传输模式卡状态转换

CMD7用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态, 必须选择一个RCA地址目标卡使其进入传输状态才可以进行数据通信。同时通过CMD7命令也可以让已经被选择的目标卡返回到待机状态。

数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。 卡处于传输状态下可以使用表 SD部分命令描述 中面向块的读写以及擦除命令对卡进行数据读写、擦除。 CMD12可以中断正在进行的数据通信,让卡返回到传输状态。CMD0和CMD15会中止任何数据编程操作, 返回卡识别模式,这可能导致卡数据被损坏。

37.5. STM32的SDIO功能框图

STM32控制器有一个SDIO,由两部分组成:SDIO适配器和AHB接口,见图 SDIO功能框图。SDIO适配器提供SDIO主机功能, 可以提供SD时钟、发送命令和进行数据传输。AHB接口用于控制器访问SDIO适配器寄存器并且可以产生中断和DMA请求信号。

SDIO功能框图

SDIO使用两个时钟信号,一个是SDIO适配器时钟(SDIOCLK=HCLK=72MHz),另外一个是AHB总线时钟的二分频(HCLK/2,一般为36MHz)。 适配器寄存器和FIFO使用AHB总线一侧的时钟(HCLK/2),控制单元、命令通道和数据通道使用SDIO适配器一侧的时钟(SDIOCLK)。

SDIO_CK是SDIO接口与SD卡用于同步的时钟信号。它使用SDIOCLK作为SDIO_CK的时钟来源, 可以通过设置BYPASS模式直接得到,这时SDIO_CK = SDIOCLK=HCLK。若禁止BYPASS模式, 可以通过配置时钟寄存器的CLKDIV位控制分频因子,即SDIO_CK=SDIOCLK/(2+CLKDIV)= HCLK/(2+CLKDIV)。 配置时钟时要注意,SD卡普遍要求SDIO_CK时钟频率不能超过25MHz。

STM32控制器的SDIO是针对MMC卡和SD卡的主设备,所以预留有8根数据线,对于SD卡最多用四根数据线。

SDIO适配器是SD卡系统主机部分,是STM32控制器与SD卡数据通信中间设备。SDIO适配器由五个单元组成, 分别是控制单元、命令路径单元、数据路径单元、寄存器单元以及FIFO,见图 SDIO适配器框图

SDIO适配器框图

37.5.1. 控制单元

控制单元包含电源管理和时钟管理功能,结构如图 SDIO适配器控制单元。 电源管理部件会在系统断电和上电阶段禁止SD卡总线输出信号。时钟管理部件控制CLK线时钟信号生成。一般使用SDIOCLK分频得到。

SDIO适配器控制单元

37.5.2. 命令路径

命令路径控制命令发送,并接收卡的响应,结构见图 SDIO适配器命令路径

SDIO适配器命令路径

关于SDIO适配器状态转换流程可以参考图 卡识别模式状态转换图 , 当SD卡处于某一状态时,SDIO适配器必然处于特定状态与之对应。 STM32控制器以命令路径状态机(CPSM)来描述SDIO适配器的状态变化,并加入了等待超时检测功能, 以便退出永久等待的情况。CPSM的描述见图 CPSM状态机描述图

CPSM状态机描述图

37.5.3. 数据路径

数据路径部件负责与SD卡相互数据传输,内部结构见图 SDIO适配器数据路径

SDIO适配器数据路径

SD卡系统数据传输状态转换参考图 数据传输模式卡状态转换 , SDIO适配器以数据路径状态机(DPSM)来描述SDIO适配器状态变化情况。 并加入了等待超时检测功能,以便退出永久等待情况。发送数据时,DPSM处于等待发送(Wait_S)状态,如果数据FIFO不为空, DPSM变成发送状态并且数据路径部件启动向卡发送数据。接收数据时,DPSM处于等待接收状态,当DPSM收到起始位时变成接收状态, 并且数据路径部件开始从卡接收数据。DPSM状态机描述见图 DPSM状态机描述图

DPSM状态机描述图

37.5.4. 数据FIFO

数据FIFO(先进先出)部件是一个数据缓冲器,带发送和接收单元。控制器的FIFO包含宽度为32bit、 深度为32字的数据缓冲器和发送/接收逻辑。其中SDIO状态寄存器(SDIO_STA)的TXACT位用于指示当前正在发送数据, RXACT位指示当前正在接收数据,这两个位不可能同时为1。

  • 当TXACT为1时,可以通过AHB接口将数据写入到传输FIFO。

  • 当RXACT为1时,接收FIFO存放从数据路径部件接收到的数据。

根据FIFO空或满状态会把SDIO_STA寄存器位值1,并可以产生中断和DMA请求。

37.5.5. 适配器寄存器

适配器寄存器包含了控制SDIO外设的各种控制寄存器及状态寄存器,内容较多, 可以通过SDIO提供的各种结构体来了解,这些寄存器的功能都被整合到了结构体或ST标准库之中。

37.6. SDIO初始化结构体

标准库函数对SDIO外设建立了三个初始化结构体,分别为SDIO初始化结构体SDIO_InitTypeDef、SDIO命令初始化结构体SDIO_CmdInitTypeDef和SDIO数据初始化结构体SDIO_DataInitTypeDef。 这些结构体成员用于设置SDIO工作环境参数,并由SDIO相应初始化配置函数或功能函数调用,这些参数将会被写入到SDIO相应的寄存器,达到配置SDIO工作环境的目的。

初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。 初始化结构体定义在stm32f10x_sdio.h文件中,初始化库函数定义在stm32f10x_sdio.c文件中,编程时我们可以结合这两个文件内注释使用。

SDIO初始化结构体用于配置SDIO基本工作环境,比如时钟分频、时钟沿、数据宽度等等。它被SDIO_Init函数使用。

代码清单:SDIO-1 SDIO初始化结构体
1
2
3
4
5
6
7
8
typedef struct {
    uint32_t SDIO_ClockEdge;              // 时钟沿
    uint32_t SDIO_ClockBypass;           // 旁路时钟
    uint32_t SDIO_ClockPowerSave;        // 节能模式
    uint32_t SDIO_BusWide;                // 数据宽度
    uint32_t SDIO_HardwareFlowControl;  // 硬件流控制
    uint8_t SDIO_ClockDiv;                // 时钟分频
} SDIO_InitTypeDef;

各结构体成员的作用介绍如下:

(1) SDIO_ClockEdge:主时钟SDIOCLK产生CLK引脚时钟有效沿选择,可选上升沿或下降沿, 它设定SDIO时钟控制寄存器(SDIO_CLKCR)的NEGEDGE位的值,一般选择设置为高电平。

(2)SDIO_ClockBypass:时钟分频旁路使用,可选使能或禁用,它设定SDIO_CLKCR寄存器的BYPASS位。如果使能旁路,SDIOCLK直接驱动CLK线输出时钟; 如果禁用,使用SDIO_CLKCR寄存器的CLKDIV位值分频SDIOCLK,然后输出到CLK线。一般选择禁用时钟分频旁路。

(3) SDIO_ClockPowerSave:节能模式选择,可选使能或禁用,它设定SDIO_CLKCR寄存器的PWRSAV位的值。如果使能节能模式, CLK线只有在总线激活时才有时钟输出;如果禁用节能模式,始终使能CLK线输出时钟。

(4) SDIO_BusWide:数据线宽度选择,可选1位数据总线、4位数据总线或8为数据总线,系统默认使用1位数据总线, 操作SD卡时在数据传输模式下一般选择4位数据总线。它设定SDIO_CLKCR寄存器的WIDBUS位的值。

(5) SDIO_HardwareFlowControl:硬件流控制选择,可选使能或禁用,它设定SDIO_CLKCR寄存器的HWFC_EN位的值。 硬件流控制功能可以避免FIFO发送上溢和下溢错误。

(6) SDIO_ClockDiv:时钟分频系数,它设定SDIO_CLKCR寄存器的CLKDIV位的值, 设置SDIOCLK与CLK线输出时钟分频系数:

CLK线时钟频率=SDIOCLK/([CLKDIV+2])。

37.7. SDIO命令初始化结构体

SDIO命令初始化结构体用于设置命令相关内容,比如命令号、命令参数、响应类型等等。它被SDIO_SendCommand函数使用。

代码清单:SDIO-2 SDIO命令初始化接口
1
2
3
4
5
6
7
typedef struct {
    uint32_t SDIO_Argument; // 命令参数
    uint32_t SDIO_CmdIndex; // 命令号
    uint32_t SDIO_Response; // 响应类型
    uint32_t SDIO_Wait;     // 等待使能
    uint32_t SDIO_CPSM;     // 命令路径状态机
} SDIO_CmdInitTypeDef;

各个结构体成员介绍如下:

(1) SDIO_Argument:作为命令的一部分发送到卡的命令参数, 它设定SDIO参数寄存器(SDIO_ARG)的值。

(2) SDIO_CmdIndex:命令号选择, 它设定SDIO命令寄存器(SDIO_CMD)的CMDINDEX位的值。

(3) SDIO_Response:响应类型,SDIO定义两个响应类型:长响应和短响应。根据命令号选择对应的响应类型。 SDIO定义了四个32位的SDIO响应寄存器(SDIO_RESPx,x=1..4),短响应只用到SDIO_RESP1。

(4) SDIO_Wait:等待类型选择,有三种状态可选,一种是无等待状态,超时检测功能启动;一种是等待中断, 另外一种是等待传输完成。它设定SDIO_CMD寄存器的WAITPEND位和WAITINT位的值。

(5) SDIO_CPSM:命令路径状态机控制,可选使能或禁用CPSM。 它设定SDIO_CMD寄存器的CPSMEN位的值。

37.8. SDIO数据初始化结构体

SDIO数据初始化结构体用于配置数据发送和接收参数,比如传输超时、数据长度、传输模式等等。它被SDIO_DataConfig函数使用。

代码清单:SDIO-3 SDIO数据初始化结构体
1
2
3
4
5
6
7
8
typedef struct {
    uint32_t SDIO_DataTimeOut;    // 数据传输超时
    uint32_t SDIO_DataLength;     // 数据长度
    uint32_t SDIO_DataBlockSize;  // 数据块大小
    uint32_t SDIO_TransferDir;    // 数据传输方向
    uint32_t SDIO_TransferMode;   // 数据传输模式
    uint32_t SDIO_DPSM;           // 数据路径状态机
} SDIO_DataInitTypeDef;

各结构体成员介绍如下:

(1) SDIO_DataTimeOut:设置数据传输以卡总线时钟周期表示的超时周期,它设定SDIO数据定时器寄存器(SDIO_DTIMER)的值。 在DPSM进入Wait_R或繁忙状态后开始递减,直到0还处于以上两种状态则将超时状态标志置1.

(2) SDIO_DataLength:设置传输数据长度, 它设定SDIO数据长度寄存器(SDIO_DLEN)的值。

(3) SDIO_DataBlockSize:设置数据块大小,有多种尺寸可选, 不同命令要求的数据块可能不同。它设定SDIO数据控制寄存器(SDIO_DCTRL)寄存器的DBLOCKSIZE位的值。

(4) SDIO_TransferDir:数据传输方向,可选从主机到卡的写操作, 或从卡到主机的读操作。它设定SDIO_DCTRL寄存器的DTDIR位的值。

(5) SDIO_TransferMode:数据传输模式,可选数据块或数据流模式。 对于SD卡操作使用数据块类型。它设定SDIO_DCTRL寄存器的DTMODE位的值。

(6) SDIO_DPSM:数据路径状态机控制,可选使能或禁用DPSM。 它设定SDIO_DCTRL寄存器的DTEN位的值。要实现数据传输都必须使能SDIO_DPSM。

37.9. SD卡读写测试实验

SD卡广泛用于便携式设备上,比如数码相机、手机、多媒体播放器等。对于嵌入式设备来说是一种重要的存储数据部件。类似于SPI Flash芯片数据操作, 可以直接进行读写,也可以写入文件系统,然后使用文件系统读写函数,使用文件系统操作。本实验是进行SD卡最底层的数据读写操作,直接使用SDIO对SD卡进行读写, 会损坏SD卡原本内容,导致数据丢失,实验前请注意备份SD卡的原内容。由于SD卡容量很大,我们平时使用的SD卡都是已经包含有文件系统的, 一般不会使用本章的操作方式编写SD卡的应用,但它是SD卡操作的基础,对于原理学习是非常有必要的,在它的基础上移植文件系统到SD卡的应用将在下一章讲解。

37.9.1. 硬件设计

STM32控制器的SDIO引脚是被设计固定不变的,开发板设计采用四根数据线模式。对于命令线和数据线须需要加一个上拉电阻。

SD卡硬件设计

37.9.2. 软件设计

这里只讲解核心的部分代码,有些变量的设置,头文件的包含等没有全部罗列出来,完整的代码请参考本章配套的工程。有了之前相关SDIO知识基础, 我们就可以着手开始编写SD卡驱动程序了,根据之前内容,可了解操作的大概流程:

  • 初始化相关GPIO及SDIO外设;

  • 配置SDIO基本通信环境进入卡识别模式,通过几个命令处理后得到卡类型;

  • 如果是可用卡就进入数据传输模式,接下来就可以进行读、写、擦除的操作。

虽然看起来只有三步,但它们有非常多的细节需要处理。实际上,SD卡是非常常用外设部件,ST公司在其测试板上也有板子SD卡卡槽, 并提供了完整的驱动程序,我们直接参考移植使用即可。类似SDIO、USB这些复杂的外设,它们的通信协议相当庞大,要自行编写完整、 严谨的驱动不是一件轻松的事情,这时我们就可以利用ST官方例程的驱动文件,根据自己硬件移植到自己开发平台即可。

在“初识STM32标准库”章节我们重点讲解了标准库的源代码及启动文件和库使用帮助文档这两部分内容,实际上“Utilities”文件夹内容是非常有参考价值的, 该文件夹包含了基于ST官方实验板的驱动文件,比如LCD、SRAM、SD卡、音频解码IC等等底层驱动程序, 另外还有第三方软件库,如emWin图像软件库和FatFs文件系统。虽然,我们的开发平台跟ST官方实验平台硬件设计略有差别,但移植程序方法是完全可行的。 学会移植程序可以减少很多工作量,加快项目进程,更何况ST官方的驱动代码是经过严格验证的。

在ST固件库“STM32F10x_StdPeriph_Lib_V3.5.0UtilitiesSTM32_EVALCommon”文件夹下可以找到SD卡驱动文件, 见图 ST官方实验板SD卡驱动文件。 我们需要stm32_eval_sdio_sd.c和stm32_eval_sdio_sd.h两个文件的完整内容。 另外还可以参考目录“STM32F10x_StdPeriph_Lib_V3.5.0ProjectSTM32F10x_StdPeriph_ExamplesSDIOuSDCard”下中的示例代码编写测试, 为简化工程,本章配置工程代码是将这些与SD卡相关的的内容都添加到stm32_eval_sdio_sd.c文件中,具体可以参考工程文件。

ST官方实验板SD卡驱动文件

我们把stm32_eval_sdio_sd.c和stm32_eval_sdio_sd.h两个文件拷贝到我们的工程文件夹中, 并将其对应改名为bsp_sdio_sdcard.c和bsp_sdio_sdcard.h,见图 SD卡驱动文件。 另外,添加的sdio_test.c和sdio_test.h文件包含了SD卡读、写、擦除测试代码。

SD卡驱动文件

本实验中讲解的代码,极大部分是从ST提供的这个SDIO驱动示例整理而来。

37.9.2.1. GPIO初始化和DMA配置

SDIO用到CLK线、CMD线和4根DAT线,使用之前必须初始化相关的GPIO,并设置复用模式为SDIO的类型。而SDIO外设又支持生成DMA请求, 使用DMA传输可以提高数据传输效率,因此在SDIO的控制代码中,可以把它设置为DMA传输模式或轮询模式, ST标准库提供SDIO示例中针对这两个模式做了区分处理。由于应用中一般都使用DMA传输模式,所以接下来代码分析都以DMA传输模式介绍。

SDIO模式、地址及时钟分频配置相关的宏定义

代码清单:SDIO-4 SDIO模式、地址及时钟分频配置相关的宏定义(bsp_sdio_sdcard.h文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/*宏定义*/
//SDIO_FIOF地址=SDIO地址+0x80至 sdio地址+0xfc
#define SDIO_FIFO_ADDRESS                ((uint32_t)0x40018080)
/**
* @brief  SDIO 初始化时钟频率 (最大400KHz )
*/
#define SDIO_INIT_CLK_DIV                ((uint8_t)0xB2)
/**
* @brief  SDIO 数据传输时钟频率 (最大25MHz )
*/
/*!< SDIOCLK = HCLK, SDIO_CK = HCLK/(2 + SDIO_TRANSFER_CLK_DIV) */
#define SDIO_TRANSFER_CLK_DIV            ((uint8_t)0x01)


/* 通过注释,选择SDIO传输时使用的模式,可选DMA模式及普通模式, */
#define SD_DMA_MODE                       ((uint32_t)0x00000000)
/*#define SD_POLLING_MODE                 ((uint32_t)0x00000002)*/

代码中主要定义了如下内容:

(1) 定义了SDIO外设的FIFO地址,SDIO进行传输时,数据会被存储在FIFO,该FIFO的大小为32字节, 即从0x40018080至0x400180fc(可从《STM32参考手册》的SDIO寄存器说明中查询到),把FIFO的起始地址定义成宏后方便后面配置DMA传输时使用;

(2) 定义卡识别模式和数据传输模式下的时钟分频因子。SDIO_CK引脚的时钟信号在卡识别模式时要求不超过400KHz, 而在识别后的数据传输模式时则希望有更高的速度(最大不超过25MHz),所以会针对这两种模式配置SDIOCLK的时钟。 代码中的SDIO_INIT_CLK_DIV分频因子用于卡识别模式,SDIO_TRANSFER_CLK_DIV用于数据传输模式。把两种模式的分频因子代入公式计算:

SDIOCLK = HCLK=72MHz, SDIO_CK = HCLK/(2 +CLK_DIV)

可得卡识别模式SDIO_CK时钟为400KHz,数据传输模式SDIO_CK时钟为24MHz。

(3) 定义SDIO传输使用DMA还是普通的轮询模式。宏SD_DMA_MODE和SD_POLLING_MODE可用于选择是否使用DMA, 只要定义了其中一个宏并把另一个注释掉即可选择宏对应的模式,上述代码中选择了DMA模式。

GPIO初始化

代码清单:SDIO-5 GPIO初始化(bsp_sdio_sdcard.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* 函数名:GPIO_Configuration
* 描述  :初始化SDIO用到的引脚,开启时钟。
* 输入  :无
* 输出  :无
* 调用  :内部调用
*/
static void GPIO_Configuration(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure;

    /*!< 使能端口时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD , ENABLE);

/*!< 配置 PC.08, PC.09, PC.10, PC.11,PC.12 引脚: D0, D1, D2, D3,CLK 引脚 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 |
GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    /*!< 配置 PD.02 CMD 引脚 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    /*!< 使能 SDIO AHB 时钟 */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_SDIO, ENABLE);

    /*!< 使能 DMA2 时钟 */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);
}

由于SDIO对应的IO引脚都是固定的,所以这里没有使用宏定义方式给出,直接使用GPIO引脚,该函数初始化引脚之后还使能了SDIO和DMA2时钟。

DMA传输配置

代码清单:SDIO-6 DMA传输配置(bsp_sdio_sdcard.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//SDIO_FIOF地址=SDIO地址+0x80至 sdio地址+0xfc
#define SDIO_FIFO_ADDRESS                ((uint32_t)0x40018080)

/*
* 函数名:SD_DMA_RxConfig
* 描述  :为SDIO接收数据配置DMA2的通道4的请求
* 输入  :BufferDST:用于装载数据的变量指针
*       : BufferSize: 缓冲区大小
* 输出  :无
*/
void SD_DMA_RxConfig(uint32_t *BufferDST, uint32_t BufferSize)
{
    DMA_InitTypeDef DMA_InitStructure;

    DMA_ClearFlag(DMA2_FLAG_TC4 | DMA2_FLAG_TE4 |
                DMA2_FLAG_HT4 | DMA2_FLAG_GL4);//清除DMA标志位

    /*!< 配置前先禁止DMA */
    DMA_Cmd(DMA2_Channel4, DISABLE);

    /*!< DMA2 传输配置 */
    //外设地址,fifo
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SDIO_FIFO_ADDRESS;
    //目标地址
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)BufferDST;
    //外设为源地址
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    //除以4,把字转成字节单位
    DMA_InitStructure.DMA_BufferSize = BufferSize / 4;
    //外设地址不自增
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    //存储目标地址自增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    //外设数据大小为字,32位
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
    //外设数据大小为字,32位
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
    //不循环
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    //通道优先级高
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    //非 存储器至存储器模式
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

    DMA_Init(DMA2_Channel4, &DMA_InitStructure);

    /*!< 使能DMA通道 */
    DMA_Cmd(DMA2_Channel4, ENABLE);
}

/*
* 函数名:SD_DMA_RxConfig
* 描述  :为SDIO发送数据配置DMA2的通道4的请求
* 输入  :BufferDST:装载了数据的变量指针
    BufferSize:  缓冲区大小
* 输出  :无
*/
void SD_DMA_TxConfig(uint32_t *BufferSRC, uint32_t BufferSize)
{
    DMA_InitTypeDef DMA_InitStructure;

    DMA_ClearFlag(DMA2_FLAG_TC4 | DMA2_FLAG_TE4 |
                DMA2_FLAG_HT4 | DMA2_FLAG_GL4);

    /*!< 配置前先禁止DMA */
    DMA_Cmd(DMA2_Channel4, DISABLE);

    /*!< DMA2 传输配置 */
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SDIO_FIFO_ADDRESS;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)BufferSRC;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//外设为写入目标
    DMA_InitStructure.DMA_BufferSize = BufferSize / 4;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA2_Channel4, &DMA_InitStructure);

    /*!< 使能DMA通道 */
    DMA_Cmd(DMA2_Channel4, ENABLE);
}

SD_DMA_RxConfig函数用于配置DMA的SDIO接收请求参数,并指定接收存储器地址和大小。SD_DMA_TxConfig函数用于配置DMA的SDIO发送请求参数, 并指定发送存储器地址和大小。这两个函数在SDIO数据传输时会被调用来对DMA进行配置,使得传输过程采用DMA搬运数据。函数有两个输入参数, BufferSRC在接收时用于指定接收到的数据存储的内存地址,发送时用于指定要对外发送的数据的内存地址;而BufferSize用于指定数据的大小, 它是32个字节的,所以在配置DMA传输的数据大小时,要把它由字的单位转换成字节,也就是1/4大小;另外,DMA配置中的外设地址在接收和发送时都是SDIO的FIFO, 在代码中使用了宏SDIO_FIFO_ADDRESS来定义。接收和发送函数非常类似,只是数据的方向和来源不一样而已,对于DMA相关配置的详细解释可以参考DMA章节内容。

37.9.2.2. 相关类型定义

打开bsp_sdio_sdcard.h文件可以发现有非常多的枚举类型定义、结构体类型定义以及宏定义,把所有的定义在这里罗列出来不太现实, 这部分代码内容请直接打开工程查看,针对这些内容在此处简要介绍如下:

  • 枚举类型定义:有SD_Error、SDTransferState和SDCardState三个。SD_Error是列举了控制器可能出现的错误、 比如CRC校验错误、通信等待超时、FIFO上溢或下溢、擦除命令错误等等。这些错误类型部分是控制器系统寄存器的标志位, 部分是通过命令的响应内容得 到的。SDTransferState定义了SDIO传输状态,有传输正常状态、传输忙状态和传输错误状态。 SDCardState定义卡的当前状态,比如准备状态、识别状态、待机状态、传输状态等等, 具体状态转换过程参考图 卡识别模式状态转换图 和图 数据传输模式卡状态转换

  • 结构体类型定义:有SD_CSD、SD_CID、SD_CardStatus以及SD_CardInfo。 SD_CSD定义了SD卡的特定数据(CSD)寄存器位, 一般提供R2类型的响应可以获取得到CSD寄存器内容。SD_CID结构体类似SD_CSD结构体,它定义SD卡CID寄存器内容, 也是通过R2响应类型获取得到。SD_CardStatus结构体定义了SD卡状态,有数据宽度、卡类型、速度等级、擦除宽度、 传输偏移地址等等SD卡状态。SD_CardInfo结构体定义了SD卡信息,包括了SD_CSD类型和SD_CID类型成员, 还有定义了卡容量、卡块大小、卡相对地址RCA和卡类型成员。

  • 宏定义内容:包含有命令号定义、SDIO传输方式、SD卡插入状态以及SD卡类型定义。 参考表 SD部分命令描述 列举了描述了部分命令,文件中为每个命令号定义一个宏, 比如将复位CMD0定义为SD_CMD_GO_IDLE_STATE,这与表 SD部分命令描述 中缩写部分是类似的, 所以熟悉命名用途可以更好理解SD卡操作过程。 SDIO数据传输可以选择是否使用DMA传输,前面提到的SD_DMA_MODE和SD_POLLING_MODE就定义在这里, 两种方式只能二选一使用,为提高系统性能,一般使用DMA传输模式。接下来还定义了检测SD卡是否正确插入的宏SD_PRESENT和SD_NOT_PRESENT, ST官方的原SD卡驱动是以一个输入引脚电平判断SD卡是否正确插入,由于我们的硬件没有使用该引脚, 所以我们的程序里把ST驱动中原来的引脚检测部分代码删除掉了,但保留了SD_PRESENT和SD_NOT_PRESENT两个宏定义。 最后定义SD卡具体的类型,有V1.1版本标准卡、V2.0版本标准卡、高容量SD卡以及其他类型卡,前面三个是常用的类型。

在bsp_sdio_sdcard.c文件也有部分宏定义,这部分宏定义只能在该文件中使用。这部分宏定义包括命令超时时间定义、OCR寄存器位掩码、R6响应位掩码等等, 这些定义更多是为提取特定响应位内容而设计的掩码。

因为类型定义和宏定义内容没有在本文中列举出来,读者有必要使用KEIL工具打开本章配套例程理解清楚。同时了解bsp_sdio_sdcard.c文件中定义的多个不同类型变量。

接下来我们就开始根据SD卡识别过程和数据传输过程理解SD卡驱动函数代码。这部分代码内容也是非常庞大,不可能全部在文档中全部列出,对于部分函数只介绍其功能。

37.9.2.3. SD卡初始化

SD卡初始化过程主要是卡识别和相关SD卡状态获取。整个初始化函数可以实现图 SD卡初始化和识别流程 中的功能。

SD卡初始化和识别流程

SD卡初始化函数

代码清单:SDIO-7 SD_Init函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/*
* 函数名:NVIC_Configuration
* 描述  :SDIO 优先级配置为最高优先级。
* 输入  :无
* 输出  :无
*/
static void NVIC_Configuration(void)
{
    NVIC_InitTypeDef NVIC_InitStructure;

    /* Configure the NVIC Preemption Priority Bits */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    NVIC_InitStructure.NVIC_IRQChannel = SDIO_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/**
* 函数名:SD_Init
* 描述  :初始化SD卡,使卡处于就绪状态(准备传输数据)
* 输入  :无
* 输出  :-SD_Error SD卡错误代码
*         成功时则为 SD_OK
* 调用  :外部调用
*/
SD_Error SD_Init(void)
{
    /*重置SD_Error状态*/
    SD_Error errorstatus = SD_OK;

    NVIC_Configuration();

    /* SDIO 外设底层引脚初始化 */
    GPIO_Configuration();

    /*对SDIO的所有寄存器进行复位*/
    SDIO_DeInit();

    /*上电并进行卡识别流程,确认卡的操作电压  */
    errorstatus = SD_PowerON();

    /*如果上电,识别不成功,返回“响应超时”错误 */
    if (errorstatus != SD_OK) {
        /*!< CMD Response TimeOut (wait for CMDSENT flag) */
        return (errorstatus);
    }

    /*卡识别成功,进行卡初始化    */
    errorstatus = SD_InitializeCards();

    if (errorstatus != SD_OK) {  //失败返回
        /*!< CMD Response TimeOut (wait for CMDSENT flag) */
        return (errorstatus);
    }

    /* 配置SDIO外设
    * 上电识别,卡初始化都完成后,进入数据传输模式,提高读写速度
    */

    /* SDIOCLK = HCLK, SDIO_CK = HCLK/(2 + SDIO_TRANSFER_CLK_DIV) */
    SDIO_InitStructure.SDIO_ClockDiv = SDIO_TRANSFER_CLK_DIV;

    /*上升沿采集数据 */
    SDIO_InitStructure.SDIO_ClockEdge = SDIO_ClockEdge_Rising;

    /* Bypass模式使能的话,SDIO_CK不经过SDIO_ClockDiv分频 */
    SDIO_InitStructure.SDIO_ClockBypass = SDIO_ClockBypass_Disable;

    /* 若开启此功能,在总线空闲时关闭sd_clk时钟 */
    SDIO_InitStructure.SDIO_ClockPowerSave = SDIO_ClockPowerSave_Disable;

    /* 暂时配置成1bit模式 */
    SDIO_InitStructure.SDIO_BusWide = SDIO_BusWide_1b;

    /* 硬件流,若开启,在FIFO不能进行发送和接收数据时,数据传输暂停 */
    SDIO_InitStructure.SDIO_HardwareFlowControl = SDIO_HardwareFlowControl_Disable;

    SDIO_Init(&SDIO_InitStructure);

    if (errorstatus == SD_OK) {
        /* 用来读取csd/cid寄存器 */
        errorstatus = SD_GetCardInfo(&SDCardInfo);
    }

    if (errorstatus == SD_OK) {
        /* 通过cmd7  ,rca选择要操作的卡 */
    errorstatus = SD_SelectDeselect((uint32_t) (SDCardInfo.RCA << 16));
    }

    if (errorstatus == SD_OK) {
        /* 最后为了提高读写,开启4bits模式 */
        errorstatus = SD_EnableWideBusOperation(SDIO_BusWide_4b);
    }

    return (errorstatus);
}

该函数的部分执行流程如下:

(1) 配置NVIC,SD卡通信用到SDIO中断,如果用到DMA传输还需要配置DMA中断。 中断服务函数SDIO_IRQHandler定义在stm32f10x_it.c文件的, 为了移植方便,您也可以把它直接定义在bsp_sdio_sdcard.c文件中,中断服务函数定义在哪个文件问题都不大, 只要定义正确就可以的,编译器会自动寻找,只是在移植的时候,要注意别漏掉该函数。

(2) 执行GPIO_Configuration函数,其功能是对底层SDIO引脚进行初始化以及开启相关时钟, 该函数在之前已经讲解。

(3) SDIO_DeInit函数用于解除初始化SDIO接口,它是与GPIO_Configuration函数相反功能,关闭相关时钟,关闭SDIO电源, 让SDIO接近上电复位状态。恢复复位状态后再进行相关配置,可以防止部分没有配置的参数采用非默认值而导致错误,这是ST官方驱动常用的一种初始化方式。

(4) 调用SD_PowerON函数,它用于查询卡的工作电压和时钟控制配置,并返回SD_Error类型错误, 该函数是整个SD识别精髓,有必要详细分析。

SD_POWERON函数

代码清单:SDIO-8 SD_POWERON函数
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/*
* 函数名:SD_PowerON
* 描述  :确保SD卡的工作电压和配置控制时钟
* 输入  :无
* 输出  :-SD_Error SD卡错误代码
*         成功时则为 SD_OK
* 调用  :在 SD_Init() 调用
*/
SD_Error SD_PowerON(void)
{
    SD_Error errorstatus = SD_OK;
    uint32_t response = 0, count = 0, validvoltage = 0;
    uint32_t SDType = SD_STD_CAPACITY;

    /******************************************************************/
    /* 上电初始化
    * 配置SDIO的外设
    * SDIOCLK = HCLK, SDIO_CK = HCLK/(2 + SDIO_INIT_CLK_DIV)
    * 初始化时的时钟不能大于400KHz
    */
    /* HCLK = 72MHz, SDIOCLK = 72MHz, SDIO_CK = HCLK/(178 + 2) = 400 KHz

    SDIO_InitStructure.SDIO_ClockDiv = SDIO_INIT_CLK_DIV;

    SDIO_InitStructure.SDIO_ClockEdge = SDIO_ClockEdge_Rising;

    /* 不使用bypass模式,直接用HCLK进行分频得到SDIO_CK */
    SDIO_InitStructure.SDIO_ClockBypass = SDIO_ClockBypass_Disable;

    /* 空闲时不关闭时钟电源 */
    SDIO_InitStructure.SDIO_ClockPowerSave = SDIO_ClockPowerSave_Disable;

    /* 初始化的时候暂时先把数据线配置成1根 */
    SDIO_InitStructure.SDIO_BusWide = SDIO_BusWide_1b;

    /* 禁止能硬件流控制 */
    SDIO_InitStructure.SDIO_HardwareFlowControl = SDIO_HardwareFlowControl_Disable;

    SDIO_Init(&SDIO_InitStructure);

    /* 开启SDIO外设的电源 */
    SDIO_SetPowerState(SDIO_PowerState_ON);

    /* 使能 SDIO 时钟 */
    SDIO_ClockCmd(ENABLE);
    /****************************************************************/
    /* 下面发送一系列命令,开始卡识别流程
    * CMD0: GO_IDLE_STATE(复位所以SD卡进入空闲状态)
    * 没有响应
    */
    SDIO_CmdInitStructure.SDIO_Argument = 0x0;
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_GO_IDLE_STATE;

    /* 没有响应 */
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_No;

    /* 关闭等待中断 */
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;

    /* CPSM在开始发送命令之前等待数据传输结束 */
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    /* 检测是否正确接收到cmd0 */
    errorstatus = CmdError();

    /* 命令发送出错,返回 */
    if (errorstatus != SD_OK) {
        /* CMD 响应超时  */
        return (errorstatus);
    }
    /****************************************************************/
    /* CMD8: SEND_IF_COND
    * 发送 CMD8 检查SD卡的电压操作条件
    *
    * 参数: - [31:12]: 保留 (要被设置为 '0')
    *       - [11:8] : 支持的电压 (VHS) 0x1 (范围: 2.7-3.6 V)
    *       - [7:0]  : 校验模式 (推荐 0xAA)
    * 响应类型: R7
    */
    /* 接收到命令sd会返回这个参数 */
    SDIO_CmdInitStructure.SDIO_Argument = SD_CHECK_PATTERN;

    SDIO_CmdInitStructure.SDIO_CmdIndex = SDIO_SEND_IF_COND;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    /*检查是否接收到命令*/
    errorstatus = CmdResp7Error();

    /* 有响应则card遵循sd协议2.0版本 */
    if (errorstatus == SD_OK) {
        /* SD Card 2.0 ,先把它定义会sdsc类型的卡 */
        CardType = SDIO_STD_CAPACITY_SD_CARD_V2_0;

        /* 这个变量用作ACMD41的参数,用来询问是sdsc卡还是sdhc卡 */
        SDType = SD_HIGH_CAPACITY;
    } else {  /* 无响应,说明是1.x的或mmc的卡 */
        /* 发命令 CMD55 */
        SDIO_CmdInitStructure.SDIO_Argument = 0x00;
        SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_APP_CMD;
        SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
        SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
        SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
        SDIO_SendCommand(&SDIO_CmdInitStructure);
        errorstatus = CmdResp1Error(SD_CMD_APP_CMD);
    }

    /* CMD55
    * 发送cmd55,用于检测是sd卡还是mmc卡,或是不支持的卡
    * CMD 响应: R1
    */
    SDIO_CmdInitStructure.SDIO_Argument = 0x00;
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_APP_CMD;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    /* 是否响应,没响应的是mmc或不支持的卡 */
    errorstatus = CmdResp1Error(SD_CMD_APP_CMD);
    /*****************************************************************/
    /* 若 errorstatus 为 Command TimeOut, 说明是MMC 卡
    * 若 errorstatus 为 SD_OK ,说明是SD card: SD 卡 2.0 (电压范围不匹配)
    * 或 SD 卡 1.x
    */
    if (errorstatus == SD_OK) { //响应了cmd55,是sd卡,可能为1.x,可能为2.0
        /*下面开始循环地发送sdio支持的电压范围,循环一定次数*/

        /* SD CARD
        * 发送 ACMD41 SD_APP_OP_COND ,带参数 0x80100000
            */
        while ((!validvoltage) && (count < SD_MAX_VOLT_TRIAL)) {
            /* 在发送ACMD命令前都要先向卡发送CMD55
            * 发送 CMD55 APP_CMD , RCA 为 0
            */
            SDIO_CmdInitStructure.SDIO_Argument = 0x00;
            SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_APP_CMD;
            SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
            SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
            SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
            SDIO_SendCommand(&SDIO_CmdInitStructure);

            errorstatus = CmdResp1Error(SD_CMD_APP_CMD);

            if (errorstatus != SD_OK) {
                return (errorstatus);
            }

            /* ACMD41
        * 命令参数由支持的电压范围及HCS位组成,HCS位置一来区分卡是SDSC还是SDHC
            * 0:SDSC
            * 1:SDHC
            * 响应:R3,对应的是OCR寄存器
            */
            SDIO_CmdInitStructure.SDIO_Argument = SD_VOLTAGE_WINDOW_SD | SDType;
            SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SD_APP_OP_COND;
            SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
            SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
            SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
            SDIO_SendCommand(&SDIO_CmdInitStructure);

            errorstatus = CmdResp3Error();

            if (errorstatus != SD_OK) {
                return (errorstatus);
            }

            /* 若卡需求电压在SDIO的供电电压范围内,会自动上电并标志pwr_up位
            * 读取卡寄存器,卡状态
            */
            response = SDIO_GetResponse(SDIO_RESP1);

            /* 读取卡的ocr寄存器的pwr_up位,看是否已工作在正常电压 */
            validvoltage = (((response >> 31) == 1) ? 1 : 0);
            count++;        /* 计算循环次数 */
        }

        if (count >= SD_MAX_VOLT_TRIAL) {  /* 循环检测超过一定次数还没上电 */
        errorstatus = SD_INVALID_VOLTRANGE; /* SDIO不支持card的供电电压 */
            return (errorstatus);
        }

        /*检查卡返回信息中的HCS位*/
        /* 判断ocr中的ccs位 ,如果是sdsc卡则不执行下面的语句 */
        if (response &= SD_HIGH_CAPACITY) {
            CardType = SDIO_HIGH_CAPACITY_SD_CARD; /* 把卡类型从初始化的sdsc型改为sdhc型 */
        }

    }/* else MMC Card */

    return (errorstatus);
}

SD_POWERON函数执行流程如下:

(1) 配置SDIO_InitStructure结构体变量成员并调用SDIO_Init库函数完成SDIO外设的基本配置, 注意此处的SDIO时钟分频,由于处于卡识别阶段,其时钟不能超过400KHz。

(2) 调用SDIO_SetPowerState库函数控制SDIO的电源状态, 给SDIO提供电源,并调用SDIO_ClockCmd库函数使能SDIO时钟。

(3) 发送命令给SD卡,首先发送CMD0,复位所有SD卡, CMD0命令无需响应,所以调用CmdError函数检测错误即可。CmdError函数用于无需响应的命令发送检测,带有等待超时检测功能, 它通过不断检测SDIO_STA寄存器的CMDSENT位即可知道命令发送成功与否。如果遇到超时错误则直接退出SD_PowerON函数。如果无错误则执行下面程序。

(4) 发送CMD8命令,检测SD卡支持的操作条件,主要就是电压匹配,CMD8的响应类型是R7,使用CmdResp7Error函数可获取得到R7响应结果, 它是通过检测SDIO_STA寄存器相关位完成的,并具有等待超时检测功能。如果CmdResp7Error函数返回值为SD_OK,即CMD8有响应, 可以判定SD卡为V2.0及以上的高容量SD卡,如果没有响应可能是V1.1版本卡或者是不可用卡。

(5) 使用ACMD41命令判断卡的具体类型。因为是A类命令,所以在发送ACMD41之前必须先发送CMD55, CMD55命令的响应类型的R1。如果CMD55命令都没有响应说明是MMC卡或不可用卡。 在正确发送CMD55之后就可以发送ACMD41,并根据响应判断卡类型,ACMD41的响应号为R3,CmdResp3Error函数用于检测命令正确发送并带有超时检测功能, 但并不具备响应内容接收功能,需要在判定命令正确发送之后调用SDIO_GetResponse函数才能获取响应的内容。实际上,在有响应时,SDIO外设会自动把响应存放在SDIO_RESPx寄存器中, SDIO_GetResponse函数只是根据形参返回对应响应寄存器的值。通过判定响应内容值即可确定SD卡类型。

(6) 执行SD_PowerON函数无错误后就已经确定了SD卡类型,并说明卡和主机电压是匹配的,SD卡处于卡识别模式下的准备状态。 退出SD_PowerON函数返回SD_Init函数,执行接下来代码。判断执行SD_PowerON函数无错误后,执行下面的SD_InitializeCards函数进行与SD卡相关的初始化,使得卡进入数据传输模式下的待机模式。

SD_InitializeCards函数

代码清单:SDIO-9 SD_InitializeCards函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
* 函数名:SD_InitializeCards
* 描述  :初始化所有的卡或者单个卡进入就绪状态
* 输入  :无
* 输出  :-SD_Error SD卡错误代码
*         成功时则为 SD_OK
*调用:在 SD_Init() 调用,在调用power_on()上电卡识别完毕后,调用此函数进行卡初始化
*/
SD_Error SD_InitializeCards(void)
{
    SD_Error errorstatus = SD_OK;
    uint16_t rca = 0x01;

    if (SDIO_GetPowerState() == SDIO_PowerState_OFF) {
        errorstatus = SD_REQUEST_NOT_APPLICABLE;
        return (errorstatus);
    }

    /* 判断卡的类型 */
    if (SDIO_SECURE_DIGITAL_IO_CARD != CardType) {
        /* Send CMD2 ALL_SEND_CID
        * 响应:R2,对应CID寄存器
        */
        SDIO_CmdInitStructure.SDIO_Argument = 0x0;
        SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_ALL_SEND_CID;
        SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Long;
        SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
        SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
        SDIO_SendCommand(&SDIO_CmdInitStructure);

        errorstatus = CmdResp2Error();

        if (SD_OK != errorstatus) {
            return (errorstatus);
        }

        /* 将返回的CID信息存储起来 */
        CID_Tab[0] = SDIO_GetResponse(SDIO_RESP1);
        CID_Tab[1] = SDIO_GetResponse(SDIO_RESP2);
        CID_Tab[2] = SDIO_GetResponse(SDIO_RESP3);
        CID_Tab[3] = SDIO_GetResponse(SDIO_RESP4);
    }
/********************************************************************/
    if (   (SDIO_STD_CAPACITY_SD_CARD_V1_1 == CardType)
        ||(SDIO_STD_CAPACITY_SD_CARD_V2_0 == CardType)
        ||(SDIO_SECURE_DIGITAL_IO_COMBO_CARD == CardType)
        ||(SDIO_HIGH_CAPACITY_SD_CARD == CardType) ) { /* 使用的是2.0的卡 */
        /* 发送 CMD3 SET_REL_ADDR ,带参数 0
        * 要求各个SD卡返回自身的RCA地址.
        * 响应:R6,对应RCA寄存器
        */
        SDIO_CmdInitStructure.SDIO_Argument = 0x00;
        SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_REL_ADDR;
        SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
        SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
        SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
        SDIO_SendCommand(&SDIO_CmdInitStructure);

        /* 把接收到的卡相对地址存起来 */
        errorstatus = CmdResp6Error(SD_CMD_SET_REL_ADDR, &rca);

        if (SD_OK != errorstatus) {
            return (errorstatus);
        }
    }
    /*******************************************************************/
    if (SDIO_SECURE_DIGITAL_IO_CARD != CardType) {
        RCA = rca;

        /* Send CMD9 SEND_CSD with argument as card's RCA
        * 响应:R2  对应寄存器CSD(Card-Specific Data)
        */
        SDIO_CmdInitStructure.SDIO_Argument = (uint32_t)(rca << 16);
        SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SEND_CSD;
        SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Long;
        SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
        SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
        SDIO_SendCommand(&SDIO_CmdInitStructure);

        errorstatus = CmdResp2Error();

        if (SD_OK != errorstatus) {
            return (errorstatus);
        }

        CSD_Tab[0] = SDIO_GetResponse(SDIO_RESP1);
        CSD_Tab[1] = SDIO_GetResponse(SDIO_RESP2);
        CSD_Tab[2] = SDIO_GetResponse(SDIO_RESP3);
        CSD_Tab[3] = SDIO_GetResponse(SDIO_RESP4);
    }
    /******************************************************************/
    /*全部卡初始化成功 */
    errorstatus = SD_OK;

    return (errorstatus);
}

SD_InitializeCards函数执行流程如下:

(1) 判断SDIO电源是否启动, 如果没有启动电源返回错误。

(2) SD卡不是SD I/O卡时会进入if判断,执行发送CMD2,CMD2是用于通知所有卡通过CMD线返回CID值, 执行CMD2发送之后就可以使用CmdResp2Error函数获取CMD2命令发送情况, 发送无错误后即可以使用SDIO_GetResponse函数获取响应内容,它是个长响应,我们把CMD2响应内容存放在CID_Tab数组内。

(3) 发送CMD2之后紧接着就发送CMD3,用于指示SD卡自行推荐RCA地址,CMD3的响应为R6类型,CmdResp6Error函数用于检查R6响应错误, 它有两个形参,一个是命令号,这里为CMD3,另外一个是RCA数据指针,这里使用rca变量的地址赋值给它,使得在CMD3正确响应之后rca变量即存放SD卡的RCA。 R6响应还有一部分位用于指示卡的状态,CmdResp6Error函数通用会对每个错误位进行必要的检测,如果发现有错误存在则直接返回对应错误类型。 执行完CmdResp6Error函数之后返回到SD_InitializeCards函数中,如果判断无错误说明此刻SD卡已经处于数据传输模式。

(4) 发送CMD9给指定RCA的SD卡使其发送返回其CSD寄存器内容, 这里的RCA就是在CmdResp6Error函数获取得到的rca。最后把响应内容存放在CSD_Tab数组中。

执行SD_InitializeCards函数无错误后SD卡就已经处于数据传输模式下的待机状态,退出SD_InitializeCards后会返回前面的SD_Init函数, 执行接下来代码,以下是SD_Init函数的后续执行过程:

(1) 重新配置SDIO外设,提高时钟频率,之前的卡识别模式都设定CMD线时钟为小于400KHz, 进入数据传输模式可以把时钟设置为小于25MHz,以便提高数据传输速率。

(2) 调用SD_GetCardInfo函数获取SD卡信息, 它需要一个指向SD_CardInfo类型变量地址的指针形参,这里赋值为SDCardInfo变量的地址。 SD卡信息主要是CID和CSD寄存器内容,这两个寄存器内容在SD_InitializeCards函数中都完成读取过程并将其分别存放在CID_Tab数组和CSD_Tab数组中, 所以SD_GetCardInfo函数只是简单的把这两个数组内容整合复制到SDCardInfo变量对应成员内。正确执行SD_GetCardInfo函数后, SDCardInfo变量就存放了SD卡的很多状态信息,这在之后应用中使用频率是很高的。

(3) 调用SD_SelectDeselect函数用于选择特定RCA的SD卡,它实际是向SD卡发送CMD7。执行之后, 卡就从待机状态转变为传输模式,可以说数据传输已经是万事俱备了。

(4) 扩展数据线宽度,之前的所有操作都是使用一根数据线传输完成的,使用4根数据线可以提高传输性能,调用可以设置数据线宽度, 函数只有一个形参,用于指定数据线宽度。在SD_EnableWideBusOperation函数中,调用了SDEnWideBus函数使能使用宽数据线, 然后传输SDIO_InitTypeDef类型变量并使用SDIO_Init函数完成使用4根数据线配置。

至此,SD_Init函数已经全部执行完成。如果程序可以正确执行,接下来就可以进行SD卡读写以及擦除等操作。虽然bsp_sdio_sd.c文件看起来非常长, 但在SD_Init函数分析过程就已经涉及到它差不多一半内容了,另外一半内容主要就是读、写或擦除相关函数。

37.9.2.4. SD卡数据操作

SD卡数据操作一般包括数据读取、数据写入以及存储区擦除。数据读取和写入都可以分为单块操作和多块操作。

擦除函数

代码清单:SDIO-10 SD_Erase函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* @brief  控制SD卡擦除指定的数据区域
* @param  startaddr: 擦除的开始地址
* @param  endaddr: 擦除的结束地址
* @retval SD_Error: SD返回的错误代码.
*/
SD_Error SD_Erase(uint32_t startaddr, uint32_t endaddr)
{
    SD_Error errorstatus = SD_OK;
    uint32_t delay = 0;
    __IO uint32_t maxdelay = 0;
    uint8_t cardstate = 0;

    /*!< 检查SD卡是否支持擦除操作 */
    if (((CSD_Tab[1] >> 20) & SD_CCCC_ERASE) == 0) {
        errorstatus = SD_REQUEST_NOT_APPLICABLE;
        return (errorstatus);
    }
    //延时,根据时钟分频设置来计算
    maxdelay = 120000 / ((SDIO->CLKCR & 0xFF) + 2);

    if (SDIO_GetResponse(SDIO_RESP1) & SD_CARD_LOCKED) { //卡已上锁
        errorstatus = SD_LOCK_UNLOCK_FAILED;
        return (errorstatus);
    }

    if (CardType == SDIO_HIGH_CAPACITY_SD_CARD) { //sdhc卡
        //在sdhc卡中,地址参数为块地址,每块512字节,而sdsc卡地址为字节地址
        //所以若是sdhc卡要对地址/512进行转换
        startaddr /= 512;
        endaddr /= 512;
    }

    /*!<  ERASE_GROUP_START (CMD32)设置擦除的起始地址,
    erase_group_end(CMD33)设置擦除的结束地址, */
    if ((SDIO_STD_CAPACITY_SD_CARD_V1_1 == CardType) ||
        (SDIO_STD_CAPACITY_SD_CARD_V2_0 == CardType) ||
        (SDIO_HIGH_CAPACITY_SD_CARD == CardType)) {
        /*!< 发送命令 CMD32 SD_ERASE_GRP_START ,带参数startaddr  */
        SDIO_CmdInitStructure.SDIO_Argument = startaddr;
        SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SD_ERASE_GRP_START;
        SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;  //R1
        SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
        SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
        SDIO_SendCommand(&SDIO_CmdInitStructure);

        errorstatus = CmdResp1Error(SD_CMD_SD_ERASE_GRP_START);
        if (errorstatus != SD_OK) {
            return (errorstatus);
        }

        /*!< 发送命令 CMD33 SD_ERASE_GRP_END ,带参数endaddr  */
        SDIO_CmdInitStructure.SDIO_Argument = endaddr;
        SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SD_ERASE_GRP_END;
        SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
        SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
        SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
        SDIO_SendCommand(&SDIO_CmdInitStructure);

        errorstatus = CmdResp1Error(SD_CMD_SD_ERASE_GRP_END);
        if (errorstatus != SD_OK) {
            return (errorstatus);
        }
    }

    /*!< 发送 CMD38 ERASE 命令,开始擦除*/
    SDIO_CmdInitStructure.SDIO_Argument = 0;
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_ERASE;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    errorstatus = CmdResp1Error(SD_CMD_ERASE);

    if (errorstatus != SD_OK) {
        return (errorstatus);
    }

    for (delay = 0; delay < maxdelay; delay++) {
    }

    /*!< 等待SD卡的内部时序操作完成 */
    errorstatus = IsCardProgramming(&cardstate);

    while ((errorstatus == SD_OK)&&((SD_CARD_PROGRAMMING == cardstate) ||
                                (SD_CARD_RECEIVING == cardstate))) {
        errorstatus = IsCardProgramming(&cardstate);
    }

    return (errorstatus);
}

SD_Erase函数用于擦除SD卡指定地址范围内的数据。该函数接收两个参数,一个是擦除的起始地址,另外一个是擦除的结束地址。 对于高容量SD卡都是以块大小为512字节进行擦除的,所以保证字节对齐是程序员的责任。SD_Erase函数的执行流程如下:

(1) 检查SD卡是否支持擦除功能,如果不支持则直接返回错误。为保证擦除指令正常进行, 要求主机一个遵循下面的命令序列发送指令:CMD32->CMD33->CMD38。如果发送顺序不对,SD卡会设置ERASE_SEQ_ERROR位到状态寄存器。

(2) SD_Erase函数发送CMD32指令用于设定擦除块开始地址, 在执行无错误后发送CMD33设置擦除块的结束地址。

(3) 发送擦除命令CMD38,使得SD卡进行擦除操作。SD卡擦除操作由SD卡内部控制完成,不同卡擦除后是0xff还是0x00由厂家决定。 擦除操作需要花费一定时间,这段时间不能对SD卡进行其他操作。

(4)通过IsCardProgramming函数可以检测SD卡是否处于编程状态(即卡内部的擦写状态),需要确保SD卡擦除完成才退出SD_Erase函数。 IsCardProgramming函数先通过发送CMD13命令SD卡发送它的状态寄存器内容,并对响应内容进行分析得出当前SD卡的状态以及可能发送的错误。

数据写入操作

数据写入可分为单块数据写入和多块数据写入,这里只分析单块数据写入,多块的与之类似。SD卡数据写入之前并没有硬性要求擦除写入块, 这与SPI Flash芯片写入是不同的。ST官方的SD卡写入函数包括扫描查询方式和DMA传输方式,我们这里只介绍DMA传输模式。

代码清单:SDIO-11 SD_WriteBlock函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* @brief  向sd卡写入一个BLOCK的数据(512字节)
* @note   本函数使用后需要调用如下两个函数来等待数据传输完成
*          - SD_WaitWriteOperation(): 确认DMA已把数据传输到SDIO接口
*          - SD_GetStatus(): 确认SD卡内部已经把数据写入完毕
* @param  writebuff: 指向要写入的数据
* @param  WriteAddr: 要把数据写入到sd卡的地址
* @param  BlockSize: 块大小,sdhc卡为512字节
* @retval SD_Error: 返回的sd错误代码
*/
SD_Error SD_WriteBlock(uint8_t *writebuff, uint32_t WriteAddr, uint16_t BlockSize)
{
    SD_Error errorstatus = SD_OK;

    TransferError = SD_OK;
    TransferEnd = 0;
    StopCondition = 0;

    SDIO->DCTRL = 0x0;

    if (CardType == SDIO_HIGH_CAPACITY_SD_CARD) {
        BlockSize = 512;
        WriteAddr /= 512;
    }

    /*----以下这段是在st驱动库上添加的 , 没有这一段容易卡死在DMA检测中 ---*/
    /* 设置块BLOCK的大小,cmd16,
    * 若是sdsc卡,可以用来设置块大小,
    * 若是sdhc卡,块大小为512字节,不受cmd16影响
    */
    SDIO_CmdInitStructure.SDIO_Argument = (uint32_t) BlockSize;
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_BLOCKLEN;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    errorstatus = CmdResp1Error(SD_CMD_SET_BLOCKLEN);

    if (SD_OK != errorstatus) {
        return (errorstatus);
    }
    /********************************************************/

    /*!< 发送 CMD24 WRITE_SINGLE_BLOCK 写入 */
    SDIO_CmdInitStructure.SDIO_Argument = WriteAddr;    //写入地址
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_WRITE_SINGLE_BLOCK;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;   //r1
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    errorstatus = CmdResp1Error(SD_CMD_WRITE_SINGLE_BLOCK);

    if (errorstatus != SD_OK) {
        return (errorstatus);
    }

    //配置sdio的写数据寄存器
    SDIO_DataInitStructure.SDIO_DataTimeOut = SD_DATATIMEOUT;
    SDIO_DataInitStructure.SDIO_DataLength = BlockSize;
    SDIO_DataInitStructure.SDIO_DataBlockSize = (uint32_t) 9 << 4;  //512字节
    SDIO_DataInitStructure.SDIO_TransferDir = SDIO_TransferDir_ToCard;//写数据
    SDIO_DataInitStructure.SDIO_TransferMode = SDIO_TransferMode_Block;
    SDIO_DataInitStructure.SDIO_DPSM = SDIO_DPSM_Enable;   //开启数据通道状态机
    SDIO_DataConfig(&SDIO_DataInitStructure);

    SDIO_ITConfig(SDIO_IT_DATAEND, ENABLE);  //数据传输结束中断
    SD_DMA_TxConfig((uint32_t *)writebuff, BlockSize); //配置dma,跟rx类似
    SDIO_DMACmd(ENABLE);   // 使能sdio的dma请求

    return (errorstatus);
}

SD_WriteBlock函数用于向指定的目标地址写入一个块的数据,它有三个形参,分别为指向待写入数据的首地址的指针变量、目标写入地址和块大小。 块大小一般都设置为512字节。SD_WriteBlock写入函数的执行流程如下:

(1) SD_WriteBlock函数开始时将SDIO 数据控制寄存器 (SDIO_DCTRL)清零, 复位之前的传输设置。

(2) 对SD卡进行数据读写之前,都必须发送CMD16指定块的大小,对于标准卡,要写入BlockSize长度字节的块;对于SDHC卡, 写入固定为512字节的块。接下来就可以发送块写入命令CMD24通知SD卡要进行数据写入操作,并指定待写入数据的目标地址。

(3) 利用SDIO_DataInitTypeDef结构体类型变量配置数据传输的超时、块数量、 数据块大小、数据传输方向等参数并使用SDIO_DataConfig函数完成数据传输环境配置。

(4) 调用SDIO_ITConfig函数使能SDIO数据结束传输结束中断,传输结束时, 会跳转到SDIO的中断服务函数运行。

(5) 调用前面讲解的SD_DMA_TxConfig函数,配置使能SDIO数据向SD卡的数据传输的DMA请求,该函数可以参考 代码清单:SDIO-6。 为使SDIO发送DMA请求,需要调用SDIO_DMACmd函数使能。对于高容量的SD卡要求块大小必须为512字节,程序员有责任保证数据写入地址与块大小的字节对齐问题。

执行完以上代码后,SDIO外设会自动生成DMA发送请求,将指定数据使用DMA传输写入到SD卡内。

写入操作等待函数

SD_WaitWriteOperation函数用于检测和等待数据写入完成,在调用数据写入函数之后一般都需要调用,SD_WaitWriteOperation函数适用于单块及多块写入函数。

代码清单:SDIO-12 SD_WaitWriteOperation函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief  本函数会一直等待到DMA传输结束
*         在 SDIO_WriteBlock() 和 SDIO_WriteMultiBlocks() 函数后必须被调用以确保DMA数据传输完成
* @param  None.
* @retval SD_Error: 返回的sd错误代码.
*/
SD_Error SD_WaitWriteOperation(void)
{
    SD_Error errorstatus = SD_OK;
    //等待dma是否传输结束
    while ((SD_DMAEndOfTransferStatus() == RESET) &&
        (TransferEnd == 0) && (TransferError == SD_OK)) {
    }

    if (TransferError != SD_OK) {
        return (TransferError);
    }

    /*!< 清除标志 */
    SDIO_ClearFlag(SDIO_STATIC_FLAGS);

    return (errorstatus);
}

上述代码调用库函数SD_DMAEndOfTransferStatus一直检测DMA的传输完成标志,当DMA传输结束时,该函数会返回SET值。 另外,while循环中的判断条件使用的TransferEnd和TransferError是全局变量,它们会在SDIO的中断服务函数根据传输情况被设置, 传输结束后,根据TransferError的值来确认是否正确传输,若不正确则直接返回错误代码。SD_WaitWriteOperation函数最后是清除相关标志位并返回错误。 由于这个函数里的while循环的存在,它会确保DMA的传输结束。

数据读取操作

同向SD卡写入数据类似,从SD卡读取数据可分为单块读取和多块读取。这里仅介绍单块读操作函数,多块读操作类似。

代码清单:SDIO-13 SD_ReadBlock函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* @brief  从sd卡读取一个BLOCK的数据(512字节)
* @note   本函数使用后需要调用如下两个函数来等待数据传输完成
*          - SD_ReadWaitOperation(): 确认DMA已从SDIO传输到数据到内存
*          - SD_GetStatus(): 确认SD卡传输完成
* @param  writebuff: 指向要接收数据的缓冲区
* @param  WriteAddr: 要把数据写入到sd卡的地址
* @param  BlockSize: 块大小,sdhc卡为512字节
* @retval SD_Error: 返回的sd错误代码
*/
SD_Error SD_ReadBlock(uint8_t *readbuff, uint32_t ReadAddr, uint16_t BlockSize)
{
    SD_Error errorstatus = SD_OK;

    TransferError = SD_OK;
    TransferEnd = 0;   //传输结束标置位,在中断服务置1
    StopCondition = 0;

    SDIO->DCTRL = 0x0;


    if (CardType == SDIO_HIGH_CAPACITY_SD_CARD) {
        BlockSize = 512;
        ReadAddr /= 512;
    }
    /*******************add,没有这一段容易卡死在DMA检测中*************/
    /* Set Block Size for Card,cmd16,
    * 若是sdsc卡,可以用来设置块大小,
    * 若是sdhc卡,块大小为512字节,不受cmd16影响
    */
    SDIO_CmdInitStructure.SDIO_Argument = (uint32_t) BlockSize;
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_BLOCKLEN;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;   //r1
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    errorstatus = CmdResp1Error(SD_CMD_SET_BLOCKLEN);

    if (SD_OK != errorstatus) {
        return (errorstatus);
    }
    /*********************************************************/
    SDIO_DataInitStructure.SDIO_DataTimeOut = SD_DATATIMEOUT;
    SDIO_DataInitStructure.SDIO_DataLength = BlockSize;
    SDIO_DataInitStructure.SDIO_DataBlockSize = (uint32_t) 9 << 4;
    SDIO_DataInitStructure.SDIO_TransferDir = SDIO_TransferDir_ToSDIO;
    SDIO_DataInitStructure.SDIO_TransferMode = SDIO_TransferMode_Block;
    SDIO_DataInitStructure.SDIO_DPSM = SDIO_DPSM_Enable;
    SDIO_DataConfig(&SDIO_DataInitStructure);

    /*!< 发送 CMD17 READ_SINGLE_BLOCK 命令*/
    SDIO_CmdInitStructure.SDIO_Argument = (uint32_t)ReadAddr;
    SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_READ_SINGLE_BLOCK;
    SDIO_CmdInitStructure.SDIO_Response = SDIO_Response_Short;
    SDIO_CmdInitStructure.SDIO_Wait = SDIO_Wait_No;
    SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
    SDIO_SendCommand(&SDIO_CmdInitStructure);

    errorstatus = CmdResp1Error(SD_CMD_READ_SINGLE_BLOCK);

    if (errorstatus != SD_OK) {
        return (errorstatus);
    }

    SDIO_ITConfig(SDIO_IT_DATAEND, ENABLE);
    SDIO_DMACmd(ENABLE);
    SD_DMA_RxConfig((uint32_t *)readbuff, BlockSize);

    return (errorstatus);
}

数据读取操作与数据写入操作编程流程是类似,只是数据传输方向改变,使用到的SD命令号也有所不同而已。SD_ReadBlock函数有三个形参, 分别为数据读取存储器的指针、数据读取起始目标地址和单块长度。SD_ReadBlock函数执行流程如下:

(1) 将SDIO外设的数据控制寄存器 (SDIO_DCTRL)清零, 复位之前的传输设置。

(2) 对SD卡进行数据读写之前,都必须发送CMD16指定块的大小, 对于标准卡,读取BlockSize长度字节的块;对于SDHC卡,固定读取512字节的块。

(3) 利用SDIO_DataInitTypeDef结构体类型变量配置数据传输的超时、块数量、数据块大小、 数据传输方向等参数并使用SDIO_DataConfig函数完成数据传输环境配置。

(4) 向SD卡发送单块读数据命令CMD17, SD卡在接收到命令后就会通过数据线把数据传输到SDIO的数据FIFO内。

(5) 调用SDIO_ITConfig函数使能相关中断数据结束中断, 当数据传输完成时会进入SDIO的中断服务函数。

(6) 最后调用SD_ DMA_RxConfig函数,配置使能SDIO从SD卡读取数据的DMA请求,该函数可以参考 代码清单:SDIO-6。 为使SDIO发送DMA请求,需要调用SDIO_DMACmd函数使能,配置完成后,SD卡发出的数据将会传输到STM32的SDIO外设,而SDIO外设激发DMA请求,把数据搬运到内存中。

对于高容量的SD卡要求块大小必须为512字节,程序员有责任保证目标读取地址与块大小的字节对齐问题。

读取操作等待函数

SD_WaitReadOperation函数用于等待数据读取操作完成,只有在确保数据读取完成了我们才可以放心使用数据。SD_WaitReadOperation函数适用于单块及多亏读取函数。

代码清单:SDIO-14 SD_WaitReadOperation函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief  本函数会一直等待到DMA传输结束
*          SDIO_ReadMultiBlocks() 函数后
    必须被调用以确保DMA数据传输完成
* @param  None.
* @retval SD_Error: 返回的sd错误代码.
*/
SD_Error SD_WaitReadOperation(void)
{
    SD_Error errorstatus = SD_OK;
    //等待dma传输结束
    while ((SD_DMAEndOfTransferStatus() == RESET) &&
            (TransferEnd == 0) && (TransferError == SD_OK)) {
    }

    if (TransferError != SD_OK) {
        return (TransferError);
    }

    return (errorstatus);
}

本代码与写入等待函数类似,利用SD_DMAEndOfTransferStatus函数及TransferEnd和TransferError全局变量确认是否传输完成, 并检查传输是否正常结束,若不正常则直接返回错误代码。SD_WaitReadOperation函数最后是清除相关标志位并返回错误。 由于这个函数里的while循环的存在,它会确保DMA的传输结束。

37.9.2.5. SDIO中断服务函数

在进行数据传输操作时都会使能相关标志中断,用于跟踪传输进程和错误检测。中断服务函数的stm32f10x_it.c文件,在移植SDIO驱动时要注意添加。

代码清单:SDIO-15 SDIO中断服务函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* 函数名:SDIO_IRQHandler
* 描述  :在SDIO_ITConfig()这个函数开启了sdio中断 ,
    数据传输结束时产生中断
* 输入  :无
* 输出  :无
*/
void SDIO_IRQHandler(void)
{
    /* SDIO中断相关的处理* /
    SD_ProcessIRQSrc();
}


/*
* 函数名:SD_ProcessIRQSrc
* 描述  :数据传输结束中断
* 输入  :无
* 输出  :SD错误类型
*/
SD_Error SD_ProcessIRQSrc(void)
{
    if (StopCondition == 1) { //发送读取、多块读写命令时置1
        SDIO->ARG = 0x0;   //命令参数寄存器
        SDIO->CMD = 0x44C;    // 命令寄存器: 0100  01    001100
        //            [7:6]   [5:0]
        //        CPSMEN  WAITRESP CMDINDEX
        //    开启命令状态机 短响应   cmd12 STOP_ TRANSMISSION
        TransferError = CmdResp1Error(SD_CMD_STOP_TRANSMISSION);
    } else {
        TransferError = SD_OK;
    }
    SDIO_ClearITPendingBit(SDIO_IT_DATAEND); //清中断
    SDIO_ITConfig(SDIO_IT_DATAEND, DISABLE); //关闭sdio中断使能
    TransferEnd = 1;
    return (TransferError);
}

SDIO中断服务函数SDIO_IRQHandler会直接调用SD_ProcessIRQSrc函数执行。SD_ProcessIRQSrc函数首先判断全局变量StopCondition变量是否为1, 该全局变量在SDIO的多块读写函数中被置1(前面分析的单块读写函数中StopCondition均为0),因为根据SD卡的要求,多块读写命令由CMD12结束, SD卡在接收到该命令时才停止多块的传输,此处正是根据StopCondition的情况控制是否发送CMD12命令,它发送命令时直接采用往寄存器写入命令和参数的方式。

中断服务函数的其它部分,根据传输情况设置全局变量TransferError和TransferEnd。

至此,我们已经介绍了SD卡初始化、SD卡数据操作的基础功能函数以及SDIO相关中断服务函数内容,利用这个SDIO驱动,可以编写一些简单的SD卡读写测试程序。

37.9.2.6. 测试函数

测试SD卡部分的函数是我们自己编写的,存放在sdio_test.c文件中。

SD卡测试函数

代码清单:SDIO-16 SD_Test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void SD_Test(void)
{
    LED_BLUE;
    /*---------------------------- SD Init -------------------------- */
    /* SD卡使用SDIO中断及DMA中断接收数据,中断服务程序位于bsp_sdio_sd.c文件尾

    if ((Status = SD_Init()) != SD_OK) {
        LED_RED;
printf("SD卡初始化失败,请确保SD卡已正确接入开发板,或换一张SD卡测试!\n");
    } else {
        printf("SD卡初始化成功!\n");
    }
    if (Status == SD_OK) {
        LED_BLUE;
        /*擦除测试*/
        SD_EraseTest();

        LED_BLUE;
        /*single block 读写测试*/
        SD_SingleBlockTest();

        LED_BLUE;
    }
}

测试程序以开发板上LED灯指示测试结果,同时打印相关测试结果到串口调试助手。测试程序先调用SD_Init函数完成SD卡初始化, 该函数具体代码参考 代码清单:SDIO-7 ,如果初始化成功就可以进行数据操作测试。

SD卡擦除测试

代码清单:SDIO-17 SD_EraseTest
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void SD_EraseTest(void)
{
    /*------------------- 块擦除 -------------------------------*/
    if (Status == SD_OK) {
        /* 擦除 NumberOfBlocks 个块每个块长度为512字节 */
        Status = SD_Erase(0x00, (BLOCK_SIZE * NUMBER_OF_BLOCKS));
    }

    if (Status == SD_OK) {
        Status = SD_ReadMultiBlocks(Buffer_MultiBlock_Rx, 0x00,
                                    BLOCK_SIZE, NUMBER_OF_BLOCKS);

        /* 等待传输完成 */
        Status = SD_WaitReadOperation();

        /* 检查传输是否正常*/
        while (SD_GetStatus() != SD_TRANSFER_OK);
    }

    /* 校验数据 */
    if (Status == SD_OK) {
        EraseStatus = eBuffercmp(Buffer_MultiBlock_Rx, MULTI_BUFFER_SIZE);
    }

    if (EraseStatus == PASSED) {
        LED_GREEN;
        printf("SD卡擦除测试成功!\n");
    } else {
        LED_BLUE;
        printf("SD卡擦除测试失败!\n");
        printf("温馨提示:部分SD卡不支持擦除测试,若SD卡能通过下面的single \
                读写测试,即表示SD卡能够正常使用。\n");
    }
}

SD_EraseTest函数主要编程思路是擦除一定数量的数据块,接着读取已擦除块的数据,把读取到的数据与0xff或者0x00比较,得出擦除结果。

SD_Erase函数用于擦除指定地址空间,源代码参考 代码清单:SDIO-10 ,它接收两个参数指定擦除空间的起始地址和终止地址。 如果SD_Erase函数返回正确,表示擦除成功则执行数据块读取;如果SD_Erase函数返回错误,表示SD卡擦除失败,并不是所有卡都能擦除成功的,部分卡虽然擦除失败, 但数据读写操作也是可以正常执行的。这里使用多块读取函数SD_ReadMultiBlocks,它有四个形参,分别为读取数据存储器、读取数据目标地址、块大小以及块数量, 函数后面都会跟随等待数据传输完成相关处理代码。接下来会调用eBuffercmp函数判断擦除结果,它有两个形参,分别为数据指针和数据字节长度, 它实际上是把数据存储器内所有数据都与0xff或0x00做比较,只有出现这两个数之外就报错退出。

单块读写测试

代码清单:SDIO-18 SD_SingleBlockTest函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void SD_SingleBlockTest(void)
{
    /*------------------- 块 读写 --------------------------*/
    /* 向数组填充要写入的数据*/
    Fill_Buffer(Buffer_Block_Tx, BLOCK_SIZE, 0x320F);

    if (Status == SD_OK) {
        /* 把512个字节写入到SD卡的0地址 */
        Status = SD_WriteBlock(Buffer_Block_Tx, 0x00, BLOCK_SIZE);
        /* 检查传输 */
        Status = SD_WaitWriteOperation();
        while (SD_GetStatus() != SD_TRANSFER_OK);
    }
    if (Status == SD_OK) {
        /* 从SD卡的0地址读取512个字节 */
        Status = SD_ReadBlock(Buffer_Block_Rx, 0x00, BLOCK_SIZE);
        /* 检查传输 */
        Status = SD_WaitReadOperation();
        while (SD_GetStatus() != SD_TRANSFER_OK);
    }
    /* 校验读出的数据是否与写入的数据一致 */
    if (Status == SD_OK) {
        TransferStatus1 = Buffercmp(Buffer_Block_Tx,
                                    Buffer_Block_Rx, BLOCK_SIZE);
    }
    if (TransferStatus1 == PASSED) {
        LED_GREEN;
        printf("Single block 测试成功!\n");
    } else {
    LED_RED;
    printf("Single block 测试失败,请确保SD卡正确接入开发板,或换一张SD卡测试!\n");
    }
}

SD_SingleBlockTest函数主要编程思想是首先填充一个块大小的存储器,通过写入操作把数据写入到SD卡内,然后通过读取操作读取数据到另外的存储器, 然后在对比存储器内容得出读写操作是否正确。

SD_SingleBlockTest函数一开始调用Fill_Buffer函数用于填充存储器内容,它只是简单实用for循环赋值方法给存储区填充数据,它有三个形参, 分别为存储区指针、填充字节数和起始数选择,这里的起始数选择参数对本测试没有实际意义。SD_WriteBlock函数和SD_ReadBlock函数分别执行数据写入和读取操作, 具体可以参考 代码清单:SDIO-11代码清单:SDIO-13。 Buffercmp函数用于比较两个存储区内容是否完全相等,它有三个形参,分别为第一个存储区指针、 第二个存储区指针和存储器长度,该函数只是循环比较两个存储区对应位置的两个数据是否相等,只有发现存在不相等就报错退出。

SD_MultiBlockTest函数与SD_SingleBlockTest函数执行过程类似,这里就不做详细分析。

主函数

代码清单:SDIO-19 main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    /* 初始化LED灯 */
    LED_GPIO_Config();
    LED_BLUE;
    /* 初始化独立按键 */
    Key_GPIO_Config();

    /*初始化USART1*/
    USART_Config();

    printf("\r\n欢迎使用野火  STM32 开发板。\r\n");

    printf("在开始进行SD卡基本测试前,请给开发板插入32G以内的SD卡\r\n");
    printf("本程序会对SD卡进行 非文件系统 方式读写,会删除SD卡的文件系统\r\n");
    printf("实验后可通过电脑格式化或使用SD卡文件系统的例程恢复SD卡文件系统\r\n");
    printf("\r\n 但sd卡内的原文件不可恢复,实验前务必备份SD卡内的原文件!!!\r\n");

    printf("\r\n 若已确认,请按开发板的KEY1按键,开始SD卡测试实验....\r\n");

    /* Infinite loop */
    while (1) {
        /*按下按键开始进行SD卡读写实验,会损坏SD卡原文件*/
        if (  Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON) {
            printf("\r\n开始进行SD卡读写实验\r\n");
            SD_Test();
        }
    }
}

测试过程中有用到LED灯、独立按键和调试串口,所以需要对这些模块进行初始化配置。在无限循环中不断检测按键状态,如果有被按下就执行SD卡测试函数。 由于本实验尚未移植文件系统,所以运行后会破坏原SD卡存储的内容,所以实验前请注意备份SD卡的数据。

37.9.3. 下载验证

把Micro SD卡插入到开发板右侧的卡槽内,使用USB线连接开发板上的“USB TO UART”接口到电脑,电脑端配置好串口调试助手参数。 编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板发过来的提示信息,按下开发板左下边沿的K1按键,开始执行SD卡测试, 测试结果在串口调试助手可观察到,板子上LED灯也可以指示测试结果。