36. SDIO—SD卡读写测试¶
本章参考资料:《STM32F4xx参考手册》、《STM32F4xx规格书》、 库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》以及SD简易规格文件《Physical Layer Simplified Specification V2.0》(版本号:2.00)。
特别说明,本书内容是以STM32F42x系列控制器资源讲解。
阅读本章内容之前,建议先阅读SD简易规格文件。
36.1. SDIO简介¶
SD卡(Secure Digital Memory Card)在我们生活中已经非常普遍了,控制器对SD卡进行读写通信操作一般有两种通信接口可选, 一种是SPI接口,另外一种就是SDIO接口。SDIO全称是安全数字输入/输出接口,多媒体卡(MMC)、SD卡、SD I/O卡都有SDIO接口。 stm32f4xx系列控制器有一个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接口的设备。
关于SD卡和SD I/O部分内容可以在SD协会网站获取到详细的介绍,比如各种SD卡尺寸规则、读写速度标示方法、应用扩展等等信息。
本章内容针对SD卡使用讲解,对于其他类型卡的应用可以参考相关系统规范实现,所以对于控制器中针对其他类型卡的内容可能在本章中简单提及或者被忽略, 本章内容不区分SDIO和SD卡这两个概念。即使目前SD协议提供的SD卡规范版本最新是4.01版本,但stm32f4xx系列控制器只支持SD卡规范版本2.0, 即只支持标准容量SD和高容量SDHC标准卡,不支持超大容量SDXC标准卡,所以可以支持的最高卡容量是32GB。
36.2. SD卡物理结构¶
一张SD卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器5个部分,见图 SD卡物理结构。 存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证SD卡工作在合适的电压下, 如出现掉电或上状态时,它会使控制单元和存储单元接口复位;卡及接口控制单元控制SD卡的运行状态,它包括有8个寄存器;接口驱动器控制SD卡引脚的输入输出。
SD卡总共有8个寄存器,用于设定或表示SD卡信息,参考表 SD卡寄存器。这些寄存器只能通过对应的命令访问, 对SD卡进行控制操作并不是像操作控制器GPIO相关寄存器那样一次读写一个寄存器的,它是通过命令来控制, SDIO定义了64个命令,每个命令都有特殊意义,可以实现某一特定功能,SD卡接收到命令后, 根据命令要求对SD卡内部寄存器进行修改,程序控制中只需要发送组合命令就可以实现SD卡的控制以及读写操作。
每个寄存器位的含义可以参考SD简易规格文件《Physical Layer Simplified Specification V2.0》第5章内容。
36.3. SDIO总线¶
36.3.1. 总线拓扑¶
SD卡一般都支持SDIO和SPI这两种接口,本章内容只介绍SDIO接口操作方式,如果需要使用SPI操作方式可以参考SPI相关章节。 另外,stm32f42x系列控制器的SDIO是不支持SPI通信模式的,如果需要用到SPI通信只能使用SPI外设。
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模块。
36.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位宽数据包传输。
4线同步发送,每根线发送一个字节的其中两个位,数据位在四线顺序排列发送,DAT3数据线发较高位,DAT0数据线发较低位。
另外一种数据包发送格式是宽位数据包格式,对SD卡而言宽位数据包发送方式是针对SD卡SSR(SD状态)寄存器内容发送的, SSR寄存器总共有512bit,在主机发出ACMD13命令后SD卡将SSR寄存器内容通过DAT线发送给主机。宽位数据包格式示意见图 宽位数据包传输。
36.3.3. 命令¶
SD命令由主机发出,以广播命令和寻址命令为例,广播命令是针对与SD主机总线连接的所有从设备发送的,寻址命令是指定某个地址设备进行命令传输。
36.3.3.1. 命令格式¶
SD命令格式固定为48bit,都是通过CMD线连续传输的(数据线不参与),见图 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卡不执行命令。
36.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命令,则执行标准命令。
36.4. SD卡的操作模式及切换¶
36.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卡状态与操作模式 ,通过命令控制实现卡状态的切换。
36.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之后也进入数据传输模式。
36.4.3. 数据传输模式¶
只有SD卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以将主机SD时钟频率设置为FPP,默认最高为25MHz, 频率切换可以通过CMD4命令来实现。数据传输模式下,SD卡状态转换过程见图 数据传输模式卡状态转换。
CMD7用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态, 必须选择一个RCA地址目标卡使其进入传输状态才可以进行数据通信。同时通过CMD7命令也可以让已经被选择的目标卡返回到待机状态。
数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。 卡处于传输状态下可以使用表 SD部分命令描述 中面向块的读写以及擦除命令对卡进行数据读写、擦除。 CMD12可以中断正在进行的数据通信,让卡返回到传输状态。CMD0和CMD15会中止任何数据编程操作, 返回卡识别模式,这可能导致卡数据被损坏。
36.5. STM32的SDIO功能框图¶
STM32控制器有一个SDIO,由两部分组成:SDIO适配器和APB2接口,见图 SDIO功能框图。SDIO适配器提供SDIO主机功能, 可以提供SD时钟、发送命令和进行数据传输。APB2接口用于控制器访问SDIO适配器寄存器并且可以产生中断和DMA请求信号。
SDIO使用两个时钟信号,一个是SDIO适配器时钟(SDIOCLK=48MHz),另外一个是APB2总线时钟(PCLK2,一般为90MHz)。
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适配器框图。
36.5.1. 控制单元¶
控制单元包含电源管理和时钟管理功能,结构如图 SDIO适配器控制单元。 电源管理部件会在系统断电和上电阶段禁止SD卡总线输出信号。时钟管理部件控制CLK线时钟信号生成。一般使用SDIOCLK分频得到。
36.5.2. 命令路径¶
命令路径控制命令发送,并接收卡的响应,结构见图 SDIO适配器命令路径。
关于SDIO适配器状态转换流程可以参考图 卡识别模式状态转换图 , 当SD卡处于某一状态时,SDIO适配器必然处于特定状态与之对应。 STM32控制器以命令路径状态机(CPSM)来描述SDIO适配器的状态变化,并加入了等待超时检测功能, 以便退出永久等待的情况。CPSM的描述见图 CPSM状态机描述图。
36.5.3. 数据路径¶
数据路径部件负责与SD卡相互数据传输,内部结构见图 SDIO适配器数据路径。
SD卡系统数据传输状态转换参考图 数据传输模式卡状态转换 , SDIO适配器以数据路径状态机(DPSM)来描述SDIO适配器状态变化情况。 并加入了等待超时检测功能,以便退出永久等待情况。发送数据时,DPSM处于等待发送(Wait_S)状态,如果数据FIFO不为空, DPSM变成发送状态并且数据路径部件启动向卡发送数据。接收数据时,DPSM处于等待接收状态,当DPSM收到起始位时变成接收状态, 并且数据路径部件开始从卡接收数据。DPSM状态机描述见图 DPSM状态机描述图。
36.5.4. 数据FIFO¶
数据FIFO(先进先出)部件是一个数据缓冲器,带发送和接收单元。控制器的FIFO包含宽度为32bit、 深度为32字的数据缓冲器和发送/接收逻辑。其中SDIO状态寄存器(SDIO_STA)的TXACT位用于指示当前正在发送数据, RXACT位指示当前正在接收数据,这两个位不可能同时为1。
当TXACT为1时,可以通过APB2接口将数据写入到传输FIFO。
当RXACT为1时,接收FIFO存放从数据路径部件接收到的数据。
根据FIFO空或满状态会把SDIO_STA寄存器位值1,并可以产生中断和DMA请求。
36.5.5. 适配器寄存器¶
适配器寄存器包含了控制SDIO外设的各种控制寄存器及状态寄存器,内容较多, 可以通过SDIO提供的各种结构体来了解,这些寄存器的功能都被整合到了结构体或ST标准库之中。
36.6. SDIO初始化结构体¶
标准库函数对SDIO外设建立了三个初始化结构体,分别为SDIO初始化结构体SDIO_InitTypeDef、SDIO命令初始化结构体SDIO_CmdInitTypeDef和SDIO数据初始化结构体SDIO_DataInitTypeDef。 这些结构体成员用于设置SDIO工作环境参数,并由SDIO相应初始化配置函数或功能函数调用,这些参数将会被写入到SDIO相应的寄存器,达到配置SDIO工作环境的目的。
初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。 初始化结构体定义在stm32f4xx_sdio.h文件中,初始化库函数定义在stm32f4xx_sdio.c文件中,编程时我们可以结合这两个文件内注释使用。
SDIO初始化结构体用于配置SDIO基本工作环境,比如时钟分频、时钟沿、数据宽度等等。它被SDIO_Init函数使用。
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])。
36.7. SDIO命令初始化结构体¶
SDIO命令初始化结构体用于设置命令相关内容,比如命令号、命令参数、响应类型等等。它被SDIO_SendCommand函数使用。
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位的值。
36.8. SDIO数据初始化结构体¶
SDIO数据初始化结构体用于配置数据发送和接收参数,比如传输超时、数据长度、传输模式等等。它被SDIO_DataConfig函数使用。
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。
36.9. SD卡读写测试实验¶
SD卡广泛用于便携式设备上,比如数码相机、手机、多媒体播放器等。对于嵌入式设备来说是一种重要的存储数据部件。类似于SPI Flash芯片数据操作, 可以直接进行读写,也可以写入文件系统,然后使用文件系统读写函数,使用文件系统操作。本实验是进行SD卡最底层的数据读写操作,直接使用SDIO对SD卡进行读写, 会损坏SD卡原本内容,导致数据丢失,实验前请注意备份SD卡的原内容。由于SD卡容量很大,我们平时使用的SD卡都是已经包含有文件系统的, 一般不会使用本章的操作方式编写SD卡的应用,但它是SD卡操作的基础,对于原理学习是非常有必要的,在它的基础上移植文件系统到SD卡的应用将在下一章讲解。
36.9.2. 软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等没有全部罗列出来,完整的代码请参考本章配套的工程。有了之前相关SDIO知识基础, 我们就可以着手开始编写SD卡驱动程序了,根据之前内容,可了解操作的大概流程:
初始化相关GPIO及SDIO外设;
配置SDIO基本通信环境进入卡识别模式,通过几个命令处理后得到卡类型;
如果是可用卡就进入数据传输模式,接下来就可以进行读、写、擦除的操作。
虽然看起来只有三步,但它们有非常多的细节需要处理。实际上,SD卡是非常常用外设部件,ST公司在其测试板上也有板子SD卡卡槽, 并提供了完整的驱动程序,我们直接参考移植使用即可。类似SDIO、USB这些复杂的外设,它们的通信协议相当庞大,要自行编写完整、 严谨的驱动不是一件轻松的事情,这时我们就可以利用ST官方例程的驱动文件,根据自己硬件移植到自己开发平台即可。
在“初识STM32标准库”章节我们重点讲解了标准库的源代码及启动文件和库使用帮助文档这两部分内容,实际上“Utilities”文件夹内容是非常有参考价值的, 该文件夹包含了基于ST官方实验板的驱动文件,比如LCD、SDRAM、SD卡、音频解码IC等等底层驱动程序, 另外还有第三方软件库,如emWin图像软件库和FatFs文件系统。虽然,我们的开发平台跟ST官方实验平台硬件设计略有差别,但移植程序方法是完全可行的。 学会移植程序可以减少很多工作量,加快项目进程,更何况ST官方的驱动代码是经过严格验证的。
在“STM32F4xx_DSP_StdPeriph_Lib_V1.5.1”文件下可以知道SD卡驱动文件, 见图 ST官方实验板SD卡驱动文件。 我们需要stm324x9i_eval_sdio_sd.c和stm324x9i_eval_sdio_sd.h两个文件的完整内容。另外还需要stm324x9i_eval.c和stm324x9i_eval.h两个文件的部分代码内容,为简化工程,本章配置工程代码是将这两个文件需要用到的内容移植到stm324x9i_eval_sdio_sd.c文件中,具体可以参考工程文件。
我们把stm324x9i_eval_sdio_sd.c和stm324x9i_eval_sdio_sd.h两个文件拷贝到我们的工程文件夹中,并将其对应改名为bsp_sdio_sd.c和bsp_sdio_sd.h, 见图 SD卡驱动文件。另外,sdio_test.c和sdio_test.h文件包含了SD卡读、写、擦除测试代码。
36.9.2.1. GPIO初始化和DMA配置¶
SDIO用到CLK线、CMD线和4根DAT线,使用之前必须初始化相关GPIO,并设置为复用模式。SDIO可以生成DMA请求, 使用DMA传输可以提高数据传输效率。SDIO可以设置为轮询模式或DMA传输模式,SD卡驱动代码针对这两个模式做了区分处理, 一般使用DMA传输模式,使用接下来代码分析都以DMA传输模式介绍。
GPIO初始化和DMA配置这部分代码从stm324x9i_eval.c和stm324x9i_eval.h两个文件中移植而来。
DMA及相关配置宏定义
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 | #define SDIO_FIFO_ADDRESS ((uint32_t)0x40012C80)
/**
* @brief SDIO Intialization Frequency (400KHz max)
*/
#define SDIO_INIT_CLK_DIV ((uint8_t)0x76)
/**
* @brief SDIO Data Transfer Frequency (25MHz max)
*/
#define SDIO_TRANSFER_CLK_DIV ((uint8_t)0x0)
#define SD_SDIO_DMA DMA2
#define SD_SDIO_DMA_CLK RCC_AHB1Periph_DMA2
#define SD_SDIO_DMA_STREAM3 3
#ifdef SD_SDIO_DMA_STREAM3
#define SD_SDIO_DMA_STREAM DMA2_Stream3
#define SD_SDIO_DMA_CHANNEL DMA_Channel_4
#define SD_SDIO_DMA_FLAG_FEIF DMA_FLAG_FEIF3
#define SD_SDIO_DMA_FLAG_DMEIF DMA_FLAG_DMEIF3
#define SD_SDIO_DMA_FLAG_TEIF DMA_FLAG_TEIF3
#define SD_SDIO_DMA_FLAG_HTIF DMA_FLAG_HTIF3
#define SD_SDIO_DMA_FLAG_TCIF DMA_FLAG_TCIF3
#define SD_SDIO_DMA_IRQn DMA2_Stream3_IRQn
#define SD_SDIO_DMA_IRQHANDLER DMA2_Stream3_IRQHandler
#elif defined SD_SDIO_DMA_STREAM6
#define SD_SDIO_DMA_STREAM DMA2_Stream6
#define SD_SDIO_DMA_CHANNEL DMA_Channel_4
#define SD_SDIO_DMA_FLAG_FEIF DMA_FLAG_FEIF6
#define SD_SDIO_DMA_FLAG_DMEIF DMA_FLAG_DMEIF6
#define SD_SDIO_DMA_FLAG_TEIF DMA_FLAG_TEIF6
#define SD_SDIO_DMA_FLAG_HTIF DMA_FLAG_HTIF6
#define SD_SDIO_DMA_FLAG_TCIF DMA_FLAG_TCIF6
#define SD_SDIO_DMA_IRQn DMA2_Stream6_IRQn
#define SD_SDIO_DMA_IRQHANDLER DMA2_Stream6_IRQHandler
#endif /* SD_SDIO_DMA_STREAM3 */
|
使用宏定义编程对程序在同系列而不同型号主控芯片移植起到很好的帮助,同时简化程序代码。 数据FIFO起始地址可用于DMA传输地址;SDIOCLK在卡识别模式和数据传输模式下一般是不同的, 使用不同分频系数控制。SDIO使用DMA2外设,可选择stream3和stream6。
GPIO初始化
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 | void SD_LowLevel_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* GPIOC and GPIOD Periph clock enable */
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD,
ENABLE);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource8, GPIO_AF_SDIO);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource9, GPIO_AF_SDIO);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_SDIO);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_SDIO);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_SDIO);
GPIO_PinAFConfig(GPIOD, GPIO_PinSource2, GPIO_AF_SDIO);
/* Configure PC.08, PC.09, PC.10, PC.11 pins: D0, D1, D2, D3 pins */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 |
GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* Configure PD.02 CMD line */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_Init(GPIOD, &GPIO_InitStructure);
/* Configure PC.12 pin: CLK pin */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* Enable the SDIO APB2 Clock */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SDIO, ENABLE);
/* Enable the DMA2 Clock */
RCC_AHB1PeriphClockCmd(SD_SDIO_DMA_CLK, ENABLE);
}
|
由于SDIO对应的IO引脚都是固定的,所以这里没有使用宏定义方式给出,直接使用GPIO引脚,该函数初始化引脚之后还使能了SDIO和DMA2时钟。
DMA传输配置
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 | /* 配置使用DMA发送 */
void SD_LowLevel_DMA_TxConfig(uint32_t *BufferSRC, uint32_t BufferSize)
{
DMA_InitTypeDef SDDMA_InitStructure;
DMA_ClearFlag(SD_SDIO_DMA_STREAM,SD_SDIO_DMA_FLAG_FEIF |
SD_SDIO_DMA_FLAG_DMEIF|SD_SDIO_DMA_FLAG_TEIF|
SD_SDIO_DMA_FLAG_HTIF | SD_SDIO_DMA_FLAG_TCIF);
/* DMA2 Stream3 or Stream6 disable */
DMA_Cmd(SD_SDIO_DMA_STREAM, DISABLE);
/* DMA2 Stream3 or Stream6 Config */
DMA_DeInit(SD_SDIO_DMA_STREAM);
SDDMA_InitStructure.DMA_Channel = SD_SDIO_DMA_CHANNEL;
SDDMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SDIO_FIFO_ADDRESS;
SDDMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)BufferSRC;
SDDMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
SDDMA_InitStructure.DMA_BufferSize = 1;
SDDMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
SDDMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
SDDMA_InitStructure.DMA_PeripheralDataSize =DMA_PeripheralDataSize_Word;
SDDMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
SDDMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
SDDMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
SDDMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
SDDMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
SDDMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC4;
SDDMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_INC4;
DMA_Init(SD_SDIO_DMA_STREAM, &SDDMA_InitStructure);
DMA_ITConfig(SD_SDIO_DMA_STREAM, DMA_IT_TC, ENABLE);
DMA_FlowControllerConfig(SD_SDIO_DMA_STREAM, DMA_FlowCtrl_Peripheral);
/* DMA2 Stream3 or Stream6 enable */
DMA_Cmd(SD_SDIO_DMA_STREAM, ENABLE);
}
/* 配置使用DMA接收 */
void SD_LowLevel_DMA_RxConfig(uint32_t *BufferDST, uint32_t BufferSize)
{
DMA_InitTypeDef SDDMA_InitStructure;
DMA_ClearFlag(SD_SDIO_DMA_STREAM,SD_SDIO_DMA_FLAG_FEIF|
SD_SDIO_DMA_FLAG_DMEIF|SD_SDIO_DMA_FLAG_TEIF|
SD_SDIO_DMA_FLAG_HTIF|SD_SDIO_DMA_FLAG_TCIF);
/* DMA2 Stream3 or Stream6 disable */
DMA_Cmd(SD_SDIO_DMA_STREAM, DISABLE);
/* DMA2 Stream3 or Stream6 Config */
DMA_DeInit(SD_SDIO_DMA_STREAM);
SDDMA_InitStructure.DMA_Channel = SD_SDIO_DMA_CHANNEL;
SDDMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SDIO_FIFO_ADDRESS;
SDDMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)BufferDST;
SDDMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
SDDMA_InitStructure.DMA_BufferSize = 1;
SDDMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
SDDMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
SDDMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
SDDMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
SDDMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
SDDMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
SDDMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
SDDMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
SDDMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC4;
SDDMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_INC4;
DMA_Init(SD_SDIO_DMA_STREAM, &SDDMA_InitStructure);
DMA_ITConfig(SD_SDIO_DMA_STREAM, DMA_IT_TC, ENABLE);
DMA_FlowControllerConfig(SD_SDIO_DMA_STREAM, DMA_FlowCtrl_Peripheral);
/* DMA2 Stream3 or Stream6 enable */
DMA_Cmd(SD_SDIO_DMA_STREAM, ENABLE);
}
|
SD_LowLevel_DMA_TxConfig函数用于配置DMA的SDIO发送请求参数,并指定发送存储器地址和大小。 SD_LowLevel_DMA_RxConfig函数用于配置DMA的SDIO接收请求参数,并指定接收存储器地址和大小。 实际上,仔细分析代码可以发现BufferSize参数在这里是没有被用到,一般在使用DMA参数时都是指定传输的数量, DMA控制器在传输指定的数量后自动停止,但对于SD卡来说,可以生成硬件控制流, 在传输完目标数量数据后即控制传输停止,所以这里调用DMA_FlowControllerConfig函数使能SD卡作为DMA传输停止的控制, 这样BufferSize参数无需用到也可以正确传输。对于DMA相关配置可以参考DMA章节内容。
36.9.2.2. 相关类型定义¶
打开bsp_sdio_sd.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宏定义选择DMA传输,SD_POLLING_MODE使用普通扫描和传输,只能二选一使用。为提高系统性能,一般使用DMA传输模式, ST官方的SD卡驱动对这两个方式做了区分出来,下面对SD卡操作都是以DMA传输模式为例讲解。接下来还定义了检测SD卡是否正确插入的宏, ST官方的SD卡驱动是以一个输入引脚电平判断SD卡是否正确插入,这里我们不使用,把引脚定义去掉(不然编译出错), 保留SD_PRESENT和SD_NOT_PRESENT两个宏定义。最后定义SD卡具体的类型,有V1.1版本标准卡、V2.0版本标准卡、高容量SD卡以及其他类型卡,前面三个是常用的类型。
在bsp_sdio_sd.c文件也有部分宏定义,这部分宏定义只能在该文件中使用。这部分宏定义包括命令超时时间定义、OCR寄存器位掩码、R6响应位掩码等等, 这些定义更多是为提取特定响应位内容而设计的掩码。
因为类型定义和宏定义内容没有在本文中列举出来,读者有必要使用KEIL工具打开本章配套例程理解清楚。同时了解bsp_sdio_sd.c文件中定义的多个不同类型变量。
接下来我们就开始根据SD卡识别过程和数据传输过程理解SD卡驱动函数代码。这部分代码内容也是非常庞大,不可能全部在文档中全部列出,对于部分函数只介绍其功能。
36.9.2.3. SD卡初始化¶
SD卡初始化过程主要是卡识别和相关SD卡状态获取。整个初始化函数可以实现图 SD卡初始化和识别流程 中的功能。
SD卡初始化函数
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 | SD_Error SD_Init(void)
{
__IO SD_Error errorstatus = SD_OK;
/**************配置SDIO中断 DMA中断**********************/
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 = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init (&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = SD_SDIO_DMA_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_Init (&NVIC_InitStructure);
/**********************************************************/
/* SDIO Peripheral Low Level Init */
SD_LowLevel_Init();
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);
}
/*!< Configure the SDIO peripheral */
/*!< SDIO_CK = SDIOCLK / (SDIO_TRANSFER_CLK_DIV + 2) */
/*!< on STM32F4xx devices, SDIOCLK is fixed to 48MHz */
SDIO_InitStructure.SDIO_ClockDiv = SDIO_TRANSFER_CLK_DIV;
SDIO_InitStructure.SDIO_ClockEdge = SDIO_ClockEdge_Rising;
SDIO_InitStructure.SDIO_ClockBypass = SDIO_ClockBypass_Disable;
SDIO_InitStructure.SDIO_ClockPowerSave = SDIO_ClockPowerSave_Disable;
SDIO_InitStructure.SDIO_BusWide = SDIO_BusWide_1b;
SDIO_InitStructure.SDIO_HardwareFlowControl = SDIO_HardwareFlowControl_Disable;
SDIO_Init(&SDIO_InitStructure);
/*----------------- Read CSD/CID MSD registers ------------------*/
errorstatus = SD_GetCardInfo(&SDCardInfo);
if (errorstatus == SD_OK) {
/*----------------- Select Card --------------------------------*/
errorstatus = SD_SelectDeselect((uint32_t) (SDCardInfo.RCA << 16));
}
if (errorstatus == SD_OK) {
errorstatus = SD_EnableWideBusOperation(SDIO_BusWide_4b);
}
return (errorstatus);
}
|
该函数的部分执行流程如下:
(1) 配置NVIC,SD卡通信用到SDIO中断,如果用到DMA传输还需要配置DMA中断。 注意中断服务函数不是定义在stm32f4xx_it.c文件的,是直接定义在bsp_sdio_sd.c文件中, 中断服务函数定义在个文件问题都不大,只要定义正确就可以的,编译器会自动寻找。
(2) 执行SD_LowLevel_Init函数,其功能是对底层SDIO引脚进行初始化以及开启相关时钟, 该函数在之前已经讲解。
(3) (3) SDIO_DeInit函数用于解除初始化SDIO接口,它只是简单调用SD_LowLevel_DeInit函数。 而SD_LowLevel_DeInit函数是与SD_LowLevel_Init函数相反功能,关闭相关时钟,关闭SDIO电源, 让SDIO接近上电复位状态。恢复复位状态后再进行相关配置,可以防止部分没有配置的参数采用非默认值而导致错误, 这是ST官方驱动常用的一种初始化方式。
(4) 调用SD_PowerON函数,它用于查询卡的工作电压和时钟控制配置,并返回SD_Error类型错误, 该函数是整个SD识别精髓,有必要详细分析。
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 | SD_Error SD_PowerON(void)
{
__IO SD_Error errorstatus = SD_OK;
uint32_t response = 0, count = 0, validvoltage = 0;
uint32_t SDType = SD_STD_CAPACITY;
/*!< Power ON Sequence --------------------------------------------*/
SDIO_InitStructure.SDIO_ClockDiv = SDIO_INIT_CLK_DIV;
SDIO_InitStructure.SDIO_ClockEdge = SDIO_ClockEdge_Rising;
SDIO_InitStructure.SDIO_ClockBypass = SDIO_ClockBypass_Disable;
SDIO_InitStructure.SDIO_ClockPowerSave = SDIO_ClockPowerSave_Disable;
SDIO_InitStructure.SDIO_BusWide = SDIO_BusWide_1b;
SDIO_InitStructure.SDIO_HardwareFlowControl =SDIO_HardwareFlowControl_Disable;
SDIO_Init(&SDIO_InitStructure);
/*!< Set Power State to ON */
SDIO_SetPowerState(SDIO_PowerState_ON);
/*!< Enable SDIO Clock */
SDIO_ClockCmd(ENABLE);
/*!< CMD0: GO_IDLE_STATE -----------------------------------------*/
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;
SDIO_CmdInitStructure.SDIO_CPSM = SDIO_CPSM_Enable;
SDIO_SendCommand(&SDIO_CmdInitStructure);
errorstatus = CmdError();
if (errorstatus != SD_OK) {
return (errorstatus);
}
/*!< CMD8: SEND_IF_COND -------------------------------------------*/
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();
if (errorstatus == SD_OK) {
CardType = SDIO_STD_CAPACITY_SD_CARD_V2_0; /*!< SD Card 2.0 */
SDType = SD_HIGH_CAPACITY;
} else {
/*!< 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 */
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 is Command TimeOut, it is a MMC card */
/*!< If errorstatus is SD_OK it is a SD card: SD card 2.0
(voltage range mismatch)or SD card 1.x */
if (errorstatus == SD_OK) {
/*!< SD CARD */
/*!< Send ACMD41 SD_APP_OP_COND with Argument 0x80100000 */
while ((!validvoltage) && (count < SD_MAX_VOLT_TRIAL)) {
/*!< SEND CMD55 APP_CMD with RCA as 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);
}
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);
}
response = SDIO_GetResponse(SDIO_RESP1);
validvoltage = (((response >> 31) == 1) ? 1 : 0);
count++;
}
if (count >= SD_MAX_VOLT_TRIAL) {
errorstatus = SD_INVALID_VOLTRANGE;
return (errorstatus);
}
if (response &= SD_HIGH_CAPACITY) {
CardType = SDIO_HIGH_CAPACITY_SD_CARD;
}
}/*!< else MMC Card */
return (errorstatus);
}
|
SD_POWERON函数执行流程如下:
(1) 配置SDIO_InitStructure结构体变量成员并调用SDIO_Init库函数完成SDIO外设的基本配置, 注意此处的SDIO时钟分频,由于处于卡识别阶段,其时钟不能超过400KHz。
(2) 调用SDIO_SetPowerState函数控制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命令判断卡的具体类型。在发送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函数
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 | 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 */
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_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) ) {
/*!< Send CMD3 SET_REL_ADDR with argument 0 */
/*!< SD Card publishes its 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 */
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; /*!< All cards get intialized */
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函数分析过程就已经涉及到它差不多一半内容了,另外一半内容主要就是读、写或擦除相关函数。
36.9.2.4. SD卡数据操作¶
SD卡数据操作一般包括数据读取、数据写入以及存储区擦除。数据读取和写入都可以分为单块操作和多块操作。
擦除函数
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 | SD_Error SD_Erase(uint64_t startaddr, uint64_t endaddr)
{
SD_Error errorstatus = SD_OK;
uint32_t delay = 0;
__IO uint32_t maxdelay = 0;
uint8_t cardstate = 0;
/*!< Check if the card coomnd class supports erase command */
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) {
startaddr /= 512;
endaddr /= 512;
}
/*!< ERASE_GROUP_START (CMD32) and 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) ) {
/*!< Send CMD32 SD_ERASE_GRP_START with argument as addr */
SDIO_CmdInitStructure.SDIO_Argument =(uint32_t)startaddr;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SD_ERASE_GRP_START;
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_START);
if (errorstatus != SD_OK) {
return (errorstatus);
}
/*!< Send CMD33 SD_ERASE_GRP_END with argument as addr */
SDIO_CmdInitStructure.SDIO_Argument = (uint32_t)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);
}
}
/*!< Send 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++) {
}
/*!< Wait till the card is in programming state */
errorstatus = IsCardProgramming(&cardstate);
delay = SD_DATATIMEOUT;
while ((delay > 0) && (errorstatus == SD_OK) &&
((SD_CARD_PROGRAMMING == cardstate)||(SD_CARD_RECEIVING == cardstate))) {
errorstatus = IsCardProgramming(&cardstate);
delay--;
}
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传输模式。
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 | SD_Error SD_WriteBlock(uint8_t *writebuff, uint64_t WriteAddr,
uint16_t BlockSize)
{
SD_Error errorstatus = SD_OK;
TransferError = SD_OK;
TransferEnd = 0;
StopCondition = 0;
SDIO->DCTRL = 0x0;
#if defined (SD_DMA_MODE)
SDIO_ITConfig(SDIO_IT_DCRCFAIL | SDIO_IT_DTIMEOUT | SDIO_IT_DATAEND |
SDIO_IT_RXOVERR | SDIO_IT_STBITERR, ENABLE);
SD_LowLevel_DMA_TxConfig((uint32_t *)writebuff, BlockSize);
SDIO_DMACmd(ENABLE);
#endif
if (CardType == SDIO_HIGH_CAPACITY_SD_CARD) {
BlockSize = 512;
WriteAddr /= 512;
}
/* Set Block Size for Card */
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);
}
/*!< Send CMD24 WRITE_SINGLE_BLOCK */
SDIO_CmdInitStructure.SDIO_Argument = (uint32_t)WriteAddr;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_WRITE_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_WRITE_SINGLE_BLOCK);
if (errorstatus != SD_OK) {
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_ToCard;
SDIO_DataInitStructure.SDIO_TransferMode = SDIO_TransferMode_Block;
SDIO_DataInitStructure.SDIO_DPSM = SDIO_DPSM_Enable;
SDIO_DataConfig(&SDIO_DataInitStructure);
return (errorstatus);
}
|
SD_WriteBlock函数用于向指定的目标地址写入一个块的数据,它有三个形参,分别为指向待写入数据的首地址的指针变量、目标写入地址和块大小。 块大小一般都设置为512字节。SD_WriteBlock写入函数的执行流程如下:
(1) SD_WriteBlock函数开始将SDIO 数据控制寄存器 (SDIO_DCTRL)清理, 复位之前的传输设置。
(2) 来调用SDIO_ITConfig函数使能相关中断,包括数据CRC失败中断、 数据超时中断、数据结束中断等等。
(3) 调用SD_LowLevel_DMA_TxConfig函数,配置使能SDIO数据向SD卡的数据传输的DMA请求, 该函数可以参考代码清单 37 6。为使SDIO发送DMA请求,需要调用SDIO_DMACmd函数使能。对于高容量的SD卡要求块大小必须为512字节,程序员有责任保证数据写入地址与块大小的字节对齐问题。
(4) 对SD卡进行数据读写之前,都必须发送CMD16指定块的大小, 对于标准卡,要写入BlockSize长度字节的块;对于SDHC卡,写入512字节的块。接下来就可以发送块写入命令CMD24通知SD卡要进行数据写入操作,并指定待写入数据的目标地址。
(5) 利用SDIO_DataInitTypeDef结构体类型变量配置数据传输的超时、 块数量、数据块大小、数据传输方向等参数并使用SDIO_DataConfig函数完成数据传输环境配置。
执行完以上代码后,SDIO外设会自动生成DMA发送请求,将指定数据使用DMA传输写入到SD卡内。
写入操作等待函数
SD_WaitWriteOperation函数用于检测和等待数据写入完成,在调用数据写入函数之后一般都需要调用,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 24 25 26 27 28 29 30 | SD_Error SD_WaitWriteOperation(void)
{
SD_Error errorstatus = SD_OK;
uint32_t timeout;
timeout = SD_DATATIMEOUT;
while ( (DMAEndOfTransfer == 0x00) && (TransferEnd == 0) &&
(TransferError == SD_OK) && (timeout > 0) ) {
timeout--;
}
DMAEndOfTransfer = 0x00;
timeout = SD_DATATIMEOUT;
while (((SDIO->STA & SDIO_FLAG_TXACT)) && (timeout > 0)) {
timeout--;
}
if (StopCondition == 1) {
errorstatus = SD_StopTransfer();
StopCondition = 0;
}
if ((timeout == 0) && (errorstatus == SD_OK)) {
errorstatus = SD_DATA_TIMEOUT;
}
/*!< Clear all the static flags */
SDIO_ClearFlag(SDIO_STATIC_FLAGS);
if (TransferError != SD_OK) {
return (TransferError);
} else {
return (errorstatus);
}
}
|
该函数开始等待当前块数据正确传输完成,并添加了超时检测功能。然后不停监测SDIO_STA寄存器的TXACT位, 以等待数据写入完成。对于多块数据写入操作需要调用SD_StopTransfer函数停止数据传输, 而单块写入则不需要。SD_StopTransfer函数实际是向SD卡发送CMD12,该命令专门用于停止数据传输, SD卡系统保证在主机发送CMD12之后整块传输完后才停止数据传输。SD_WaitWriteOperation函数最后是清除相关标志位并返回错误。
数据读取操作
同向SD卡写入数据类似,从SD卡读取数据可分为单块读取和多块读取。这里这介绍单块读操作函数,多块读操作类似理解即可。
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 | SD_Error SD_ReadBlock(uint8_t *readbuff, uint64_t ReadAddr,
uint16_t BlockSize)
{
SD_Error errorstatus = SD_OK;
TransferError = SD_OK;
TransferEnd = 0;
StopCondition = 0;
SDIO->DCTRL = 0x0;
#if defined (SD_DMA_MODE)
SDIO_ITConfig(SDIO_IT_DCRCFAIL | SDIO_IT_DTIMEOUT | SDIO_IT_DATAEND |
SDIO_IT_RXOVERR | SDIO_IT_STBITERR, ENABLE);
SDIO_DMACmd(ENABLE);
SD_LowLevel_DMA_RxConfig((uint32_t *)readbuff, BlockSize);
#endif
if (CardType == SDIO_HIGH_CAPACITY_SD_CARD) {
BlockSize = 512;
ReadAddr /= 512;
}
/* Set Block Size for Card */
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);
}
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);
/*!< Send 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);
}
return (errorstatus);
}
|
数据读取操作与数据写入操作编程流程是类似,只是数据传输方向改变,使用到的SD命令号也有所不同而已。SD_ReadBlock函数有三个形参, 分别为数据读取存储器的指针、数据读取起始目标地址和单块长度。SD_ReadBlock函数执行流程如下:
(1) 将SDIO外设的数据控制寄存器 (SDIO_DCTRL)清零, 复位之前的传输设置。
(2) 调用SDIO_ITConfig函数使能相关中断,包括数据CRC失败中断、数据超时中断、数据结束中断等等。 然后调用SD_LowLevel_DMA_RxConfig函数,配置使能SDIO从SD卡的读取数据的DMA请求, 该函数可以参 考代码清单 37 6。为使SDIO发送DMA请求,需要调用SDIO_DMACmd函数使能。 对于高容量的SD卡要求块大小必须为512字节,程序员有责任保证目标读取地址与块大小的字节对齐问题。
(3) 对SD卡进行数据读写之前,都必须发送CMD16指定块的大小,对于标准卡,读取BlockSize长度字节的块; 对于SDHC卡,读取512字节的块。
(4) 利用SDIO_DataInitTypeDef结构体类型变量配置数据传输的超时、块数量、数据块大小、 数据传输方向等参数并使用SDIO_DataConfig函数完成数据传输环境配置。
(5) 最后控制器向SD卡发送单块读数据命令CMD17, SD卡在接收到命令后就会通过数据线把数据传输到控制器数据FIFO内,并自动生成DMA传输请求。
读取操作等待函数
SD_WaitReadOperation函数用于等待数据读取操作完成,只有在确保数据读取完成了我们才可以放心使用数据。 SD_WaitReadOperation函数也适用于单块读取函数和多块读取函数的。
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 | SD_Error SD_WaitReadOperation(void)
{
SD_Error errorstatus = SD_OK;
uint32_t timeout;
timeout = SD_DATATIMEOUT;
while ((DMAEndOfTransfer == 0x00) && (TransferEnd == 0) &&
(TransferError == SD_OK) && (timeout > 0)) {
timeout--;
}
DMAEndOfTransfer = 0x00;
timeout = SD_DATATIMEOUT;
while (((SDIO->STA & SDIO_FLAG_RXACT)) && (timeout > 0)) {
timeout--;
}
if (StopCondition == 1) {
errorstatus = SD_StopTransfer();
StopCondition = 0;
}
if ((timeout == 0) && (errorstatus == SD_OK)) {
errorstatus = SD_DATA_TIMEOUT;
}
/*!< Clear all the static flags */
SDIO_ClearFlag(SDIO_STATIC_FLAGS);
if (TransferError != SD_OK) {
return (TransferError);
} else {
return (errorstatus);
}
}
|
该函数开始等待当前块数据正确传输完成,并添加了超时检测功能。然后不停监测SDIO_STA寄存器的RXACT位,以等待数据读取完成。对于多块数据读取操作需要调用SD_StopTransfer函数停止数据传输,而单块写入则不需要。该函数最后是清除相关标志位并返回错误。
36.9.2.5. SDIO中断服务函数¶
在进行数据传输操作时都会使能相关标志中断,用于跟踪传输进程和错误检测。如果是使用DMA传输,也会使能DMA相关中断。为简化代码,加之SDIO中断服务函数内容一般不会修改,将中断服务函数放在bsp_sdio_sd.c文件中,而不是放在常用于存放中断服务函数的stm32f4xx_it.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 | void SDIO_IRQHandler(void)
{
/* Process All SDIO Interrupt Sources */
SD_ProcessIRQSrc();
}
SD_Error SD_ProcessIRQSrc(void)
{
if (SDIO_GetITStatus(SDIO_IT_DATAEND) != RESET) {
TransferError = SD_OK;
SDIO_ClearITPendingBit(SDIO_IT_DATAEND);
TransferEnd = 1;
} else if (SDIO_GetITStatus(SDIO_IT_DCRCFAIL) != RESET) {
SDIO_ClearITPendingBit(SDIO_IT_DCRCFAIL);
TransferError = SD_DATA_CRC_FAIL;
} else if (SDIO_GetITStatus(SDIO_IT_DTIMEOUT) != RESET) {
SDIO_ClearITPendingBit(SDIO_IT_DTIMEOUT);
TransferError = SD_DATA_TIMEOUT;
} else if (SDIO_GetITStatus(SDIO_IT_RXOVERR) != RESET) {
SDIO_ClearITPendingBit(SDIO_IT_RXOVERR);
TransferError = SD_RX_OVERRUN;
} else if (SDIO_GetITStatus(SDIO_IT_TXUNDERR) != RESET) {
SDIO_ClearITPendingBit(SDIO_IT_TXUNDERR);
TransferError = SD_TX_UNDERRUN;
} else if (SDIO_GetITStatus(SDIO_IT_STBITERR) != RESET) {
SDIO_ClearITPendingBit(SDIO_IT_STBITERR);
TransferError = SD_START_BIT_ERR;
}
SDIO_ITConfig(SDIO_IT_DCRCFAIL | SDIO_IT_DTIMEOUT | SDIO_IT_DATAEND |
SDIO_IT_TXFIFOHE | SDIO_IT_RXFIFOHF | SDIO_IT_TXUNDERR
SDIO_IT_RXOVERR | SDIO_IT_STBITERR, DISABLE);
return (TransferError);
}
|
SDIO中断服务函数SDIO_IRQHandler会直接调用SD_ProcessIRQSrc函数执行。SD_ProcessIRQSrc函数通过多个if判断语句分辨中断源,并对传输错误标志变量TransferError赋值以指示当前传输状态。最后禁用SDIO中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void SD_SDIO_DMA_IRQHANDLER(void)
{
/* Process DMA2 Stream3 or DMA2 Stream6 Interrupt Sources */
SD_ProcessDMAIRQ();
}
void SD_ProcessDMAIRQ(void)
{
if (DMA2->LISR & SD_SDIO_DMA_FLAG_TCIF) {
DMAEndOfTransfer = 0x01;
DMA_ClearFlag(SD_SDIO_DMA_STREAM,
SD_SDIO_DMA_FLAG_TCIF|SD_SDIO_DMA_FLAG_FEIF);
}
}
|
SD_SDIO_DMA_IRQHANDLER函数是DMA传输中断服务函数,它直接调用SD_ProcessDMAIRQ函数执行。SD_ProcessDMAIRQ函数主要是判断DMA的传输完成标志位。
至此,我们已经介绍了SD卡初始化、SD卡数据操作的基础功能函数以及SDIO相关中断服务函数内容,很多时候这些函数已经足够我们使用了。接下来我们就编写一些简单的测试程序验证移植的正确性。
36.9.2.6. 测试函数¶
测试SD卡部分的函数是我们自己编写的,存放在sdio_test.c文件中。
SD卡测试函数
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 | 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;
/*muti block 读写测试*/
SD_MultiBlockTest();
}
}
|
测试程序以开发板上LED灯指示测试结果,同时打印相关测试结果到串口调试助手。测试程序先调用SD_Init函数完成SD卡初始化, 该函数具体代码参考 代码清单:SDIO-18 ,如果初始化成功就可以进行数据操作测试。
SD卡擦除测试
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)
{
/*------------------- Block Erase -------------------------------*/
if (Status == SD_OK) {
/* Erase NumberOfBlocks Blocks of WRITE_BL_LEN(512 Bytes) */
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);
/* Check if the Transfer is finished */
Status = SD_WaitReadOperation();
/* Wait until end of DMA transfer */
while (SD_GetStatus() != SD_TRANSFER_OK);
}
/* Check the correctness of erased blocks */
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做比较,只有出现这两个数之外就报错退出。
单块读写测试
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)
{
/*------------------- Block Read/Write --------------------------*/
/* Fill the buffer to send */
Fill_Buffer(Buffer_Block_Tx, BLOCK_SIZE, 0x320F);
if (Status == SD_OK) {
/* Write block of 512 bytes on address 0 */
Status = SD_WriteBlock(Buffer_Block_Tx, 0x00, BLOCK_SIZE);
/* Check if the Transfer is finished */
Status = SD_WaitWriteOperation();
while (SD_GetStatus() != SD_TRANSFER_OK);
}
if (Status == SD_OK) {
/* Read block of 512 bytes from address 0 */
Status = SD_ReadBlock(Buffer_Block_Rx, 0x00, BLOCK_SIZE);
/* Check if the Transfer is finished */
Status = SD_WaitReadOperation();
while (SD_GetStatus() != SD_TRANSFER_OK);
}
/* Check the correctness of written data */
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函数执行过程类似,这里就不做详细分析。
主函数
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 | int main(void)
{
/* 禁用WiFi模块 */
BL8782_PDN_INIT();
/* 初始化LED灯 */
LED_GPIO_Config();
LED_BLUE;
/* 初始化独立按键 */
Key_GPIO_Config();
/*初始化USART1*/
Debug_USART_Config();
printf("\r\n欢迎使用野火 STM32 F429 开发板。\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_PIN) == KEY_ON) {
printf("\r\n开始进行SD卡读写实验\r\n");
SD_Test();
}
}
}
|
开发板板载了SDIO接口的WiFi模块,可以认为是个SD I/O卡,因为STM32F42x系统控制器只有一个SDIO,为使用SD卡,需要把WiFi模块的使能端拉低, 禁用WiFi模块,BL8782_PDN_INIT函数就是实现该功能。 测试过程中有用到LED灯、独立按键和调试串口,所以需要对这些模块进行初始化配置。 在无限循环中不断检测按键状态,如果有被按下就执行SD卡测试函数。
36.9.3. 下载验证¶
把Micro SD卡插入到开发板右侧的卡槽内,使用USB线连接开发板上的“USB TO UART”接口到电脑,电脑端配置好串口调试助手参数。 编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板发过来的提示信息,按下开发板左下边沿的K1按键,开始执行SD卡测试, 测试结果在串口调试助手可观察到,板子上LED灯也可以指示测试结果。