35. SD卡—读写测试¶
本章参考资料:《STM32F10X-中文参考手册》、 《STM32F103增强型系列数据手册》以及SD简易规格文件《Physical Layer Simplified Specification V2.0》(版本号:2.00)。
阅读本章内容之前,建议先阅读SD简易规格文件。
35.1. SD卡简介¶
SD卡(Secure Digital Memory Card)在我们生活中已经非常普遍了, 控制器对SD卡进行读写通信操作一般有两种通信接口可选,一种是SPI接口,另外一种是SDIO接口。
SDIO全称是安全数字输入/输出接口,多媒体卡(MMC)、SD卡、SD I/O卡(专指使用SDIO接口的一些输入输出设备)都可使用SDIO接口通讯。 STM32F10x系列控制器有一个SDIO主机接口,它支持与上述使用SDIO接口的设备进行数据传输。
SD卡协会网站www.sdcard.org中提供了SD存储卡和SDIO卡系统规范。
随着科技发展,SD卡容量需求越来越大,SD卡发展到现在也是有几个版本的,关于SDIO接口的设备整体概括见图 SDIO接口的设备。
关于SD卡和SD I/O部分内容可以在SD协会网站获取到详细的介绍,比如各种SD卡尺寸规则、读写速度标示方法、应用扩展等等信息。 目前SD协议提供的SD卡规范版本最新是4.01版本,但STM32F10x系列控制器只支持SD卡规范版本2.0,即只支持标准容量SD和高容量SDHC标准卡, 不支持超大容量SDXC标准卡,所以可以支持的最高卡容量是32GB。
本章内容仅针对SD卡使用讲解,考虑到本开发板STM32芯片的硬件资源比较紧张,所以我们采用了SPI协议驱动SD卡的方案, 相对于使用SDIO驱动SD卡的方式节省了一些引脚资源,控制程序也较为简单,代价是传输速度不如使用SDIO接口的快(SDIO同步时钟频率较高, 且有较多的数据线)。若对SDIO驱动SD卡的方式感兴趣,可以参考我们F103指南者、F103霸道等开发板的教程。
35.2. SD卡物理结构¶
一张SD卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动器5个部分, 见图 SD卡物理结构。存储单元是存储数据部件, 存储单元通过存储单元接口与卡控制单元进行数据传输;电源检测单元保证SD卡工作在合适的电压下,如出现掉电或上状态时, 它会使控制单元和存储单元接口复位;卡及接口控制单元控制SD卡的运行状态,它包括有8个寄存器;接口驱动器控制SD卡引脚的输入输出。
35.2.1. SD卡的引脚及连接¶
SD卡使用9-pin的接口,其中3根电源线、1根时钟线、1根命令线和4根数据线,具体说明如下:
CLK:同步时钟线,由SDIO主机产生,即由STM32控制器输出;使用SPI模式时,该引脚与SPI总线的SCK时钟信号相连;
CMD:命令控制线,SDIO主机通过该线发送命令控制SD卡,如果命令要求SD卡提供应答(响应),SD卡也是通过该线传输应答信息; 使用SPI模式时,该引脚与SPI总线的MOSI信号相连,SPI主机通过它向SD卡发送命令及数据,但因为SPI总线的MOSI仅用于主机向从机输出信号,所以SD卡 返回应答信息时不使用该信号线;
D0-3:在SDIO模式下,它们均为数据线,传输读写数据,SD卡可将D0拉低表示忙状态;在SPI模式下,D0与SPI总线的MISO信号相连, SD卡通过该信号线向主机发送数据或响应,D3与总线的CS信号相连,SPI主机通过该信号线选择要通讯的SD卡。
VDD、VSS1、VSS2:电源和地信号,其中Micro SD卡不包含VSS2信号,即Micro SD卡仅有8根线。
SD卡与主机的信号连接见图 SD卡与SDIO及SPI总线的连接 ,SDIO总线与多张SD卡连接时, 可以共用CLK信号线,其它的如CMD、D0~D3信号线是每张SD卡独立的。对于STM32系列芯片来说,它的SDIO外设仅支持连接一个SDIO设备,即不可以同时连接2张或以上的SD卡
SD卡一般都支持SDIO和SPI这两种接口。另外,STM32F42x系列控制器的SDIO是不支持SPI通信模式的,如果需要用到SPI通信只能使用SPI外设。
SD卡总线拓扑参考图 SD卡与SDIO及SPI总线的连接。虽然可以共用总线, 但不推荐多卡槽共用总线信号,要求一个单独SD总线应该连接一个单独的SD卡。
35.2.2. SD卡的寄存器¶
SD卡总共有8个寄存器,用于设定或表示SD卡信息,参考表 SD卡寄存器。 这些寄存器只能通过对应的命令访问,对SD卡进行控制操作并不是像操作控制器GPIO相关寄存器那样一次读写一个寄存器, 它是通过命令来控制的,SD卡定义了64个命令(部分命令不支持SPI模式),每个命令都有特殊意义,可以实现某一特定功能, SD卡接收到命令后,根据命令要求对SD卡内部寄存器进行修改,程序控制中只需要发送组合命令就可以实现SD卡的控制以及读写操作。
每个寄存器位的含义可以参考SD简易规格文件《Physical Layer Simplified Specification V2.0》第5章内容。
35.3. SD卡命令控制说明¶
35.3.1. 控制时序¶
与SD卡的通信是基于命令和数据传输的。通讯由一个起始位(“0”),由一个停止位(“1”)终止。SD通信一般是主机发送一个命令(Command), 从设备在接收到命令后作出响应(Response),如有需要会有数据(Data)传输参与。
SD卡的基本交互是命令与响应交互,见图 命令与响应交互 ,图中的DataIn和DataOut线分别是SPI的MISO及MOSI信号。
SD数据是以块(Block)形式传输的,SDHC卡数据块长度一般为512字节,数据可以从主机到卡, 也可以是从卡到主机。数据块需要CRC位来保证数据传输成功,CRC位由SD卡系统硬件生成。 单个数据块的读写时序见图 单块读操作 及图 单块写操作。
读写操作都是由主机发起的,主机发送不同的命令表示读或写,SD卡接收到命令后先针对命令返回响应。在读操作中, SD卡返回一个数据块,数据块中包含CRC校验码;在写操作中,主机接收到命令响应后需要先发送一个标志(TOKEN)然后紧跟一个要写入的数据块, 卡接收完数据块后回反回一个数据响应及忙碌标志,当SD卡把接收到的数据写入到内部存储单元完成后,会停止发送忙碌标志,主机确认SD卡空闲后,可以发送下一个命令。
SD数据传输支持单块和多块读写,它们分别对应不同的操作命令,结束多块读写时需要使用命令来停止操作。
35.3.2. 命令¶
SD命令由主机发出,以广播命令和寻址命令为例,广播命令是针对与SD主机总线连接的所有从设备发送的,寻址命令是指定某个地址设备进行命令传输。
35.3.2.1. 命令格式¶
SD命令由主机发出,命令格式固定为48bit,都是通过CMD线连续传输的(数据线不参与),见图 SD命令格式。
SD命令的组成如下:
起始位和终止位:命令的主体包含在起始位与终止位之间,它们都只包含一个数据位,起始位为0,终止位为1。
传输标志:用于区分传输方向,该位为1时表示命令,方向为主机传输到SD卡,该位为0时表示响应,方向为SD卡传输到主机。
命令主体内容包括命令、地址信息/参数和CRC校验三个部分。
命令号:它固定占用6bit,所以总共有64个命令(代号:CMD0~CMD63),每个命令都有特定的用途, 部分命令不适用于SPI总线,或不适用于SD卡操作,只是专门用于MMC卡或者SD I/O卡。
地址/参数:每个命令有32bit地址信息/参数用于命令附加内容,例如,广播命令没有地址信息,这32bit用于指定参数, 而寻址命令这32bit用于指定目标SD卡的地址,当使用SDIO驱动多张SD卡时,通过地址信息区分控制不同的卡,使用SPI总线驱动时, 通过片选引脚来选择不同的卡,所以使用这些命令时地址可填充任意值。
CRC7校验:长度为7bit的校验位用于验证命令传输内容正确性,如果发生外部干扰导致传输数据个别位状态改变将导致校准失败, 也意味着命令传输失败,SD卡不执行命令。使用SDIO驱动时,命令中必须包含正确的CRC7校验值;而使用SPI驱动时, 命令中的CRC7校验默认是关闭的,即这CRC7校验位中可以写入任意值而不影响通讯,仅在发送CMD0命令时需要强制带标准的CRC7校验。
35.3.3. 响应¶
当SD卡接收到命令时,它会向主机发出相应的命令,不同的命令有不同的响应类型, 具体如前面表 SD部分命令描述 响应类型一列的说明, 这些响应多用于反馈SD卡的状态。SDIO协议一共有7个响应类型(代号:R1~R7),其中SD卡没有R4、R5类型响应。
特定的命令对应有特定的响应类型,比如当主机发送CMD9命令时,可以得到响应R2,使用SPI驱动时,响应通过MOSI信号线传输给主机。
以图 R1响应类型说明 中表示的R1响应为例, SD卡接收到大部分命令后它都是返回这个类型的响应,用于指示工作状态,它是一个长度为1字节的响应, 最高位固定为0,当其余位为1时,说明处于该位表示的状态中,主机根据该响应获得SD卡的反馈,然后处理。
各个位的说明如下:
in idle state:当该位为1时,表示SD卡处于空闲状态;
erase reset:因为接收到无需擦除操作的命令,擦除操作被复位;
illegal command:接收到一个无效的命令代码 ;
com crc error:接收到的上一个命令的CRC校验错误;
erase sequence error:擦除命令的控制顺序错误;
address error:读写的数据地址不对齐(数据地址需要按块大小对齐);
parameter error:命令的参数错误;
关于其它的响应类型可以查看《Simplified_Physical_Layer_Spec》文档的说明。
35.3.4. Token¶
使用SPI驱动SD卡时,需要使用一种名为Token的单字节标志对数据传输流程进行控制。
35.3.4.1. 数据响应Token¶
主机向SD卡写入数据时,每发送完一个数据块后,SD卡会返回一个数据响应Token,见前面的图 单块写操作 , 它的格式如下图 数据响应Token的格式 ,主机可以通过数据响应Token确认是否写入正常。
该Token格式中的Status长度为3个数据位,分别有如下几种含义:
010:数据被接受;
101:因为CRC校验失败,数据被拒绝;
110:因为写入错误,数据被拒绝。
35.4. SD卡的初始化流程及工作模式¶
驱动SD卡的时候,需要按照规定的初始化流程操作来使SD卡进入工作状态,见图 SD卡的上电时序。
图中的VDDmin~VDDmax是指SD卡正常工作的电压范围。主机给SD卡供电后,在SD卡电压爬升过程中, 电压小于VDDmin时SD卡不工作,当电压大于VDDmin后可以开始对SD卡进行初始化。 SD卡是通过命令控制进行初始化的,但主机在上电后向SD卡发送第一条之前,需要先向SD卡发送至少74个时钟信号, 对于SPI的驱动方式,在产生这些时钟信号期间需要保持片选信号CS为高电平。
发送时钟后,就可以向SD卡发送命令进行识别。SD卡的协议标准有多个版本, STM32控制器目前最高支持《Physical Layer Simplified SpecificationV2.0》定义的SD卡, 控制器对SD卡进行数据读写之前需要识别卡的种类:MMC卡、V1.0标准卡、V2.0标准卡、V2.0高容量卡或者不被识别卡, 见图 卡识别流程。因为版本众多,SD卡系统(包括主机和SD卡)定义了两种操作模式:卡识别模式和数据传输模式。 在系统复位后,主机处于卡识别模式,以寻找总线上可用的设备,区分出不同各类的卡; 同时,SD卡也处于卡识别模式,直到被主机识别到,之后SD卡就会进入数据传输模式。
以上的卡识别流程图说明如下:
(1) 流程图省略了SD卡上电后发送至少74个时钟的过程, 经过该过程后SD卡进入到本流程图的初始模式:空闲模式。
(2) 在空闲模式下,把与SD卡相连的CS片选信号拉低,然后发送CMD0命令使SD卡切换至使用SPI通讯模式, 主机得到SD卡的正常响应,表示SD卡切换为SPI模式后,才可以发送后续命令进行初始化;
(3) 发送CMD8命令,若SD卡响应为未知命令,表示它是V1版本的卡; 若SD卡正常响应该命令,表示它是V2版本的卡;
(4) 对于V1版本的卡,继续发送CMD1命令,若正常响应, 则完成卡识别流程,进入数据传输模式;
(5) 对于V2版本的卡,需要继续发送ACMD41命令,用以确认供电电压的支持范围,确认电压支持正常后, 发送CMD58命令以读取OCR寄存器的CCS位,接收SD卡的响应,若CCS=0,则表示该卡为SDSC卡(容量小于等于2GB), 若CCS=1,则表示该卡为SDHC卡(容量大于2GB);卡识别完成后,即进入数据传输模式,之后就可以使用读写命令向SD卡读写数据了
35.5. SD卡读写测试实验¶
SD卡广泛用于便携式设备上,比如数码相机、手机、多媒体播放器等。对于嵌入式设备来说是一种重要的存储数据部件。 类似于SPI Flash芯片数据操作,可以直接进行读写,也可以写入文件系统,然后使用文件系统读写函数,使用文件系统操作。 本实验是进行SD卡最底层的数据读写操作,直接使用SPI接口对SD卡进行读写,会损坏SD卡原本内容,导致数据丢失, 实验前请注意备份SD卡的原内容。由于SD卡容量很大,我们平时使用的SD卡都是已经包含有文件系统的,一般不会使用本章的操作方式编写SD卡的应用, 但它是SD卡操作的基础,对于原理学习是非常有必要的,在它的基础上移植文件系统到SD卡的应用将在下一章讲解。
35.5.1. 硬件设计¶
本开发板采用STM32的SPI外设接口驱动SD卡,与SPI驱动外部FLASH类似,其片选信号线CS使用普通的GPIO,采用软件控制, STM32与SD卡卡槽的硬件连接见图 SD卡硬件设计。
35.5.2. 软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等没有全部罗列出来,完整的代码请参考本章配套的工程。 有了相关SD卡驱动相关的知识基础,我们就可以着手开始编写SD卡驱动程序了,根据之前内容,可了解操作的大概流程:
初始化相关GPIO及SPI外设;
配置SD卡进入SPI模式,通过几个命令获取卡的空间大小等信息;
对SD卡进行进行读写的操作。
虽然看起来只有三步,但它们有非常多的细节需要处理。实际上,SD卡是非常常用外设部件,ST公司在其测试板上也有板子SD卡卡槽, 并提供了完整的驱动程序,我们直接参考移植使用即可。类似SDIO、USB这些复杂的外设,它们的通信协议相当庞大,要自行编写完整、 严谨的驱动不是一件轻松的事情,这时我们就可以利用ST官方例程的驱动文件,根据自己硬件移植到自己开发平台即可。
在“初识STM32HAL库”章节我们重点讲解了HAL库的源代码及启动文件和库使用帮助文档这两部分内容,实际上“Utilities”文件夹内容是非常有参考价值的, 该文件夹包含了基于ST官方实验板的驱动文件,比如LCD、SRAM、SD卡、音频解码IC等等底层驱动程序,另外还有第三方软件库, 如emWin图像软件库和FatFs文件系统。虽然,我们的开发平台跟ST官方实验平台硬件设计略有差别,但移植程序方法是完全可行的。 学会移植程序可以减少很多工作量,加快项目进程,更何况ST官方的驱动代码是经过严格验证的。
在“STM32Cube_FW_F4_V1.19.0\Drivers\BSP”文件路径下可以知道SD卡驱动文件。 为简化工程,本章的配置工程把上述文件中与SD卡配置相关的代码都整合到bsp_sdio_sdcard.c和bsp_sdio_sdcard.h文件中, 见图 SD卡驱动文件。另外,还自行编写了sdio_test.c和sdio_test.h文件,这两个文件包含了对SD卡进行读写测试的代码。
35.5.2.1. GPIO初始化和SPI配置¶
本实验使用STM32的SPI外设驱动SD卡,其控制原理与SPI驱动外部FLASH的一样。
SPI引脚相关的宏定义
类似地,工程中首先把引脚号、时钟等硬件相关的配置写入到驱动的头文件,见 代码清单:SD-1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #define SD_SPI SPI1
#define SD_SPI_CLK() __HAL_RCC_SPI1_CLK_ENABLE()
#define SD_SPI_SCK_PIN GPIO_PIN_5
#define SD_SPI_SCK_GPIO_PORT GPIOA
#define SD_SPI_SCK_GPIO_CLK() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SD_SPI_MISO_PIN GPIO_PIN_6
#define SD_SPI_MISO_GPIO_PORT GPIOA
#define SD_SPI_MISO_GPIO_CLK() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SD_SPI_MOSI_PIN GPIO_PIN_7
#define SD_SPI_MOSI_GPIO_PORT GPIOA
#define SD_SPI_MOSI_GPIO_CLK() __HAL_RCC_GPIOA_CLK_ENABLE()
#define SD_CS_PIN GPIO_PIN_8
#define SD_CS_GPIO_PORT GPIOA
#define SD_CS_GPIO_CLK() __HAL_RCC_GPIOA_CLK_ENABLE()
//#define SD_DETECT_PIN GPIO_PIN_0
//#define SD_DETECT_GPIO_PORT GPIOE
//#define SD_DETECT_GPIO_CLK() __HAL_RCC_GPIOE_CLK_ENABLE()
|
上述代码定义了使用的硬件SPI号,CLK、MOSI、MISO及CS引脚,其中CS引脚采用软件控制,所以硬件设计时也只是选择了一个普通的GPIO。 代码最后注释掉的SD_DETECT_PIN是用于检测SD卡是否接入到卡槽的,它通过检测该引脚电平的高低来实现判断,不过本开发板硬件设计上并没有连接卡槽的检测引脚, 所以检测引脚相关的代码被注释掉了。实际上不使用该引脚也可以检测SD卡是否接入,例如通过向SD卡发送命令,然后通过检测是否有正确的响应来判断SD卡是否接入正常。
GPIO初始化及SPI模式配置
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 | static void GPIO_Configuration(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStruct;
SD_SPI_CLK();
SD_SPI_SCK_GPIO_CLK();
// SD_DETECT_GPIO_CLK();
/*选择要控制的GPIO引脚*/
GPIO_InitStruct.Pin = SD_SPI_SCK_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SD_SPI_SCK_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = SD_SPI_MOSI_PIN;
HAL_GPIO_Init(SD_SPI_SCK_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = SD_SPI_SCK_PIN;
HAL_GPIO_Init(SD_SPI_MOSI_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = SD_CS_PIN;
HAL_GPIO_Init(SD_CS_GPIO_PORT, &GPIO_InitStruct);
// GPIO_InitStruct.Pin = SD_DETECT_PIN;
// GPIO_InitStruct.Mode =GPIO_MODE_INPUT;
// HAL_GPIO_Init(SD_DETECT_GPIO_PORT, &GPIO_InitStruct);
SpiHandle.Instance = SPI1;
SpiHandle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
SpiHandle.Init.Direction = SPI_DIRECTION_2LINES;
SpiHandle.Init.CLKPhase = SPI_PHASE_2EDGE;
SpiHandle.Init.CLKPolarity = SPI_POLARITY_HIGH;
SpiHandle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
SpiHandle.Init.CRCPolynomial = 7;
SpiHandle.Init.DataSize = SPI_DATASIZE_8BIT;
SpiHandle.Init.FirstBit = SPI_FIRSTBIT_MSB;
SpiHandle.Init.NSS = SPI_NSS_SOFT;
SpiHandle.Init.TIMode = SPI_TIMODE_DISABLE;
SpiHandle.Init.Mode = SPI_MODE_MASTER;
HAL_SPI_Init(&SpiHandle);
__HAL_SPI_ENABLE(&SpiHandle);
}
|
类似地,本函数按照SPI外设的要求,把CLK、MOSI引脚配置成复用推挽输出,MISO配置成浮空输入, 而软件控制的CS引脚配置为普通推挽输出,以便直接控制其输出高低电平。
在SPI外设模式的配置方面,最重要的是必须按照SD卡协议的要求使用SPI的模式3,即前面 SD卡寄存器 说明中的空闲时SCK时钟为高电平、 采样时刻为偶数边沿的配置,其余的SPI模式配置很好理解,跟SPI驱动FLASH是类似的,其中用于配置CRC校验的配置实际上并没有用到,所以把它配置成任意值都是可以的。
35.5.2.2. 相关类型定义¶
打开bsp_spi_sdcard.h文件可以发现有非常多的枚举类型定义、结构体类型定义以及宏定义,在源文件里将会用到,此处先提前介绍一下。
R1响应及数据响应Token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | typedef enum {
/**
* @brief SD 响应及错误标志
*/
SD_RESPONSE_NO_ERROR = (0x00),
SD_IN_IDLE_STATE = (0x01),
SD_ERASE_RESET = (0x02),
SD_ILLEGAL_COMMAND = (0x04),
SD_COM_CRC_ERROR = (0x08),
SD_ERASE_SEQUENCE_ERROR = (0x10),
SD_ADDRESS_ERROR = (0x20),
SD_PARAMETER_ERROR = (0x40),
SD_RESPONSE_FAILURE = (0xFF),
/**
* @brief 数据响应类型
*/
SD_DATA_OK = (0x05),
SD_DATA_CRC_ERROR = (0x0B),
SD_DATA_WRITE_ERROR = (0x0D),
SD_DATA_OTHER_ERROR = (0xFF)
} SD_Error;
|
这个SD_Error枚举类型定义的分成了两部分,第一部分是R1响应类型,见前面图 R1响应类型说明 及其说明, 这些枚举定义正是遵照R1响应的各个位表示的状态,当STM32接收到R1响应时,可以利用这些枚举定义进行比较判断;第二部分是数据响应Token, 见前面图 数据响应Token的格式 及其说明,把数据响应Token的最高3个无关的位设置成0, 然后组合起Status及其余两位的值得到一个字节数字,这正是代码中对应状态枚举定义的数值。
数据块开始和停止的Token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /**
* @brief Start Data tokens:
* Tokens (necessary because at nop/idle (and CS active) only 0xff is
* on the data/command line)
*/
/*!< Data token start byte, 单块读起始Token */
#define SD_START_DATA_SINGLE_BLOCK_READ 0xFE
/*!< Data token start byte, 多块读起始Token */
#define SD_START_DATA_MULTIPLE_BLOCK_READ 0xFE
/*!< Data token start byte, 单块写起始Token */
#define SD_START_DATA_SINGLE_BLOCK_WRITE 0xFE
/*!< Data token start byte, 多块写起始Token */
#define SD_START_DATA_MULTIPLE_BLOCK_WRITE 0xFC
/*!< Data toke stop byte, 多块写停止Token */
#define SD_STOP_DATA_MULTIPLE_BLOCK_WRITE 0xFD
|
这部分代码定义的是前面介绍的数据块开始和停止的Token,对于单块读写及多块的读数据Token均为“0xFE”, 而多块写入的开始Token值为“0xFC”,结束Token值为“0xFD”。
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 | /**
* @brief Commands: CMDxx = CMD-number | 0x40
*/
#define SD_CMD_GO_IDLE_STATE 0 /*!< CMD0 = 0x40 */
#define SD_CMD_SEND_OP_COND 1 /*!< CMD1 = 0x41 */
#define SD_CMD_SEND_CSD 9 /*!< CMD9 = 0x49 */
#define SD_CMD_SEND_CID 10 /*!< CMD10 = 0x4A */
#define SD_CMD_STOP_TRANSMISSION 12 /*!< CMD12 = 0x4C */
#define SD_CMD_SEND_STATUS 13 /*!< CMD13 = 0x4D */
#define SD_CMD_SET_BLOCKLEN 16 /*!< CMD16 = 0x50 */
#define SD_CMD_READ_SINGLE_BLOCK 17 /*!< CMD17 = 0x51 */
#define SD_CMD_READ_MULT_BLOCK 18 /*!< CMD18 = 0x52 */
#define SD_CMD_SET_BLOCK_COUNT 23 /*!< CMD23 = 0x57 */
#define SD_CMD_WRITE_SINGLE_BLOCK 24 /*!< CMD24 = 0x58 */
#define SD_CMD_WRITE_MULT_BLOCK 25 /*!< CMD25 = 0x59 */
#define SD_CMD_PROG_CSD 27 /*!< CMD27 = 0x5B */
#define SD_CMD_SET_WRITE_PROT 28 /*!< CMD28 = 0x5C */
#define SD_CMD_CLR_WRITE_PROT 29 /*!< CMD29 = 0x5D */
#define SD_CMD_SEND_WRITE_PROT 30 /*!< CMD30 = 0x5E */
#define SD_CMD_SD_ERASE_GRP_START 32 /*!< CMD32 = 0x60 */
#define SD_CMD_SD_ERASE_GRP_END 33 /*!< CMD33 = 0x61 */
#define SD_CMD_UNTAG_SECTOR 34 /*!< CMD34 = 0x62 */
#define SD_CMD_ERASE_GRP_START 35 /*!< CMD35 = 0x63 */
#define SD_CMD_ERASE_GRP_END 36 /*!< CMD36 = 0x64 */
#define SD_CMD_UNTAG_ERASE_GROUP 37 /*!< CMD37 = 0x65 */
#define SD_CMD_ERASE 38 /*!< CMD38 = 0x66 */
#define SD_CMD_READ_OCR 58 /*!< CMD58 */
#define SD_CMD_APP_CMD 55 /*!< CMD55 返回0x01*/
#define SD_ACMD_SD_SEND_OP_COND 41 /*!< ACMD41 返回0x00*/
|
以上代码定义的是驱动代码中使用到的SD命令,SD命令本质上也是一个数值, 控制时由SPI主机通过MOSI信号线发送到SD卡,SD卡接收后解释成该值对应的命令然后执行。
以下请配合前面的说明图 SD命令格式 来理解,根据SD协议,命令号是一个6位的数字, 如CMD0的命令号为0,CMD1的命令号为1,此处宏定义的正是各个命令的命令号, 如宏SD_CMD_GO_IDLE_STATE(宏值为0)是CMD0,宏SD_CMD_SEND_OP_COND(宏值为1)是CMD1命令。
在上述每行宏定义代码注释中的数字比较特别,如“CMD0 = 0x40”,实际上该注释表示的都是命令号与数值0x40运算后的结果, 即作“CMDxx = 命令号 | 0x40”运算,那此处的0x40是什么,为什么注释要这样写呢?这是因为在SPI模式下,数据是一个字节一个字节地发送出去的, 一个完整的SD命令包含起始位、传输标志、命令号、参数、CRC7校验以及终止位,一共是48位即6个字节。而起始位和传输标志分别固定为0和1, 这两位与紧接着的6位命令号刚好组成1个字节,由于SPI是高位先行,所以前面的起始位和传输标志占高2位,即0x40,再跟后面的6位命令号作或运算, 组成1个字节,然后就可以直接通过SPI发送出去了。
CSD、CID及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 | /**
* @brief Card Specific Data: CSD 寄存器
*/
typedef struct {
__IO uint8_t CSDStruct; /*!< CSD structure */
__IO uint8_t SysSpecVersion; /*!< System specification version */
__IO uint8_t Reserved1; /*!< Reserved */
/*此处省略大量内容,具体请直接查看工程代码...*/
__IO uint8_t FileFormat; /*!< File Format */
__IO uint8_t ECC; /*!< ECC code */
__IO uint8_t CSD_CRC; /*!< CSD CRC */
__IO uint8_t Reserved4; /*!< always 1*/
} SD_CSD;
/**
* @brief Card Identification Data: CID 寄存器
*/
typedef struct {
__IO uint8_t ManufacturerID; /*!< ManufacturerID */
__IO uint16_t OEM_AppliID; /*!< OEM/Application ID */
__IO uint32_t ProdName1; /*!< Product Name part1 */
__IO uint8_t ProdName2; /*!< Product Name part2*/
__IO uint8_t ProdRev; /*!< Product Revision */
__IO uint32_t ProdSN; /*!< Product Serial Number */
__IO uint8_t Reserved1; /*!< Reserved1 */
__IO uint16_t ManufactDate; /*!< Manufacturing Date */
__IO uint8_t CID_CRC; /*!< CID CRC */
__IO uint8_t Reserved2; /*!< always 1 */
} SD_CID;
/**
* @brief SD Card information
*/
typedef struct {
SD_CSD SD_csd;
SD_CID SD_cid;
uint32_t CardCapacity; /*!< Card Capacity */
uint32_t CardBlockSize; /*!< Card Block Size */
} SD_CardInfo;
|
以上代码定义了三个结构体,SD_CSD和SD_CID结构体用于存储从SD卡读取回来的CSD、CID寄存器的内容,根据这些寄存器的内容可以获知SD卡工作电压、 数据块的大小、卡的容量和序列号等信息,关于这两个寄存器的内容可查阅SD协议文档了解,与驱动最相关的信息是卡容量及数据块大小这两个信息, 而这些信息以及SD_CSD、SD_CID的内容又被封装进了SD_CardInfo结构体,驱动程序直接通过SD_CardInfo类型的结构体变量获取这些信息。
SD卡类型及全局变量
1 2 3 4 5 6 7 8 9 10 | /*C文件*/
uint8_t SD_Type = SD_TYPE_NOT_SD; //存储卡的类型
SD_CardInfo SDCardInfo; //用于存储卡的信息
/*H文件*/
//SD卡的类型
#define SD_TYPE_NOT_SD 0 //非SD卡
#define SD_TYPE_V1 1 //V1.0的卡
#define SD_TYPE_V2 2 //SDSC
#define SD_TYPE_V2HC 4 //SDHC
|
这部分代码列出的是c文件里定义的全局SD_Type变量及SDCardInfo变量,在卡识别流程时将会向SD_Type变量赋值为宏定义中的非SD卡、 V1.0卡、V2的SDSC卡以及V2的SDHC卡,便于后续程序驱动时使用。SDCardInfo变量则是前面说明的卡信息结构体的实例变量, 驱动程序可通过该变量获取卡容量、块大小等信息。
35.5.2.3. 发送命令¶
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 | /**
* @brief 发送SD命令
* @param Cmd: 要发送的命令
* @param Arg: 命令参数
* @param Crc: CRC校验码.
* @retval None
*/
void SD_SendCmd(uint8_t Cmd, uint32_t Arg, uint8_t Crc)
{
uint32_t i = 0x00;
uint8_t Frame[6];
Frame[0] = (Cmd | 0x40); /*!< Construct byte 1 */
Frame[1] = (uint8_t)(Arg >> 24); /*!< Construct byte 2 */
Frame[2] = (uint8_t)(Arg >> 16); /*!< Construct byte 3 */
Frame[3] = (uint8_t)(Arg >> 8); /*!< Construct byte 4 */
Frame[4] = (uint8_t)(Arg); /*!< Construct byte 5 */
Frame[5] = (Crc); /*!< Construct CRC: byte 6 */
for (i = 0; i < 6; i++) {
SD_WriteByte(Frame[i]); /*!< Send the Cmd bytes */
}
}
|
这个SD_SendCmd发送命令的函数包含命令号cmd、命令参数Arg以及校验码Crc三个参数,它们组成一个完整的6个字节命令, 在函数的末尾通过一个for循环调用SD_WriteByte函数把这些命令数据发送到SD卡。
组成命令的这6个字节内容说明如下:
Frame[0]:它的值是命令号cmd跟起始位(固定为0)、传输标志(固定为1)的组合,所以其值为“cmd | 0x40”;
Frame[1]~Frame[4]:这是按字节分开存储的命令参数Arg;
Frame[5]:这是7位的Crc校验码与终止位(固定为1)的组合。
因为在SPI模式下仅CMD0命令需要校验CRC,其余命令均忽略,所以在整个工程中并没有编写计算CRC校验码的函数,每次发送命令时手动填写CRC校验码的值。 例如,发送CMD0命令时,由于它不包含参数,所以它的6个字节命令编码为“0x40,0x00,0x00,0x00,0x00,0x95”, 调用这个SD_SendCmd函数的示例是 “SD_SendCmd(SD_CMD_GO_IDLE_STATE, 0, 0x95)”, 其中SD_CMD_GO_IDLE_STATE是CMD0命令的宏, 即命令号,0是命令的参数,而0x95就是该命令的7位CRC校验加上最后一位终止位的结果。另外, 发送读取单个数据块的命令时的例子如“ SD_SendCmd(SD_CMD_READ_SINGLE_BLOCK, ReadAddr, 0xFF)”, 语句中的SD_CMD_READ_SINGLE_BLOCK是读取单个数据块的命令号CMD17,ReadAddr是将要读取的数据块的起始地址,该地址的单位是块(即512字节), 它将作为命令参数被发送出去,0xFF则是CRC加上最后一位终止位的结果,由于这个命令不需要检查CRC, 所以实际上这个参数的前7位即CRC可以是任意值,只要它的终止位为1即可,0xFF即可达到该要求。
35.5.2.4. 接收响应¶
本工程中只应用到了响应类型为R1的SD命令,处理R1响应的函数见 代码清单:SD-9。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /**
* @brief 获取SD卡的的响应
* @param 要检查的响应类型
* @retval SD响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_GetResponse(uint8_t Response)
{
uint32_t Count = 0xFFF;
/*!< 检查是否接收到 response 表示的响应 */
while ((SD_ReadByte() != Response) && Count) {
Count--;
}
if (Count == 0) {
/*!< 检查超时 */
return SD_RESPONSE_FAILURE;
} else {
/*!< 得到response表示的响应 */
return SD_RESPONSE_NO_ERROR;
}
}
|
这个SD_GetResponse函数接收一个输入参数Response,该参数将用于与SD_ReadByte函数接收到的响应进行比较, 若响应与参数相等,则返回成功SD_RESPONSE_NO_ERROR,否则返回失败。
在应用SD_GetResponse函数时,这个Response参数可以直接输入 代码清单:SD-4 中枚举类型SD_Error定义的R1枚举值, 如SD_GetResponse(SD_IN_IDLE_STATE)可检测响应是否表示SD卡处于空闲状态。
当向SD卡写入数据时,SD卡会返回数据响应,其相应的处理见 代码清单:SD-10。
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 | /**
* @brief 获取SD卡的数据响应.
* @param None
* @retval 返回响应状态 status: 读取到的数据响应 xxx0<status>1
* - status 010: 接受数据
* - status 101: CRC校验错误,拒绝数据
* - status 110: 写入错误,拒绝数据
* - status 111: 其它错误,拒绝数据
*/
uint8_t SD_GetDataResponse(void)
{
uint32_t i = 0;
uint8_t response, rvalue;
while (i <= 64) {
/*!< 读取响应 */
response = SD_ReadByte();
/*!< 屏蔽无关的数据位(前三位xxx) */
response &= 0x1F;
switch (response) {
case SD_DATA_OK: {
rvalue = SD_DATA_OK;
break;
}
case SD_DATA_CRC_ERROR:
return SD_DATA_CRC_ERROR;
case SD_DATA_WRITE_ERROR:
return SD_DATA_WRITE_ERROR;
default: {
rvalue = SD_DATA_OTHER_ERROR;
break;
}
}
/*!< 数据正常,退出循环 */
if (rvalue == SD_DATA_OK)
break;
/*!< Increment loop counter */
i++;
}
/*!< 等待空数据 */
while (SD_ReadByte() == 0);
/*!< 返回响应 */
return response;
}
|
这个处理数据响应的函数SD_GetDataResponse与R1响应的处理类似,它循环调用SD_ReadByte读取SD返回的响应, 把接收到的响应与 代码清单:SD-5 定义的数据响应枚举类型SD_DATA_OK、 SD_DATA_CRC_ERROR及SD_DATA_WRITE_ERROR进行比较, 直到检测到SD_DATA_OK(即响应正常)或检测超时才退出,最后把检测结果以函数返回值返回。
35.5.2.5. SD卡初始化¶
SD卡初始化过程主要是切换SD卡至SPI模式以及完成卡的上电识别流程, 即前面的图 卡识别流程,请结合图来理解下面的代码。
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 | /*bsp_spi_sdcard.h文件*/
// 片选信号CS输出低电平
#define SD_CS_LOW() GPIO_ResetBits(SD_CS_GPIO_PORT, SD_CS_PIN)
// 片选信号CS输出高电平
#define SD_CS_HIGH() GPIO_SetBits(SD_CS_GPIO_PORT, SD_CS_PIN)
//空 字节数据
#define SD_DUMMY_BYTE 0xFF
/*bsp_spi_sdcard.c文件*/
/**
* @brief 初始化 SD/SD 卡
* @param None
* @retval SD 响应:
* - SD_RESPONSE_FAILURE: 初始化失败
* - SD_RESPONSE_NO_ERROR: 初始化成功
*/
SD_Error SD_Init(void)
{
uint32_t i = 0;
/*!< 初始化 SD_SPI */
GPIO_Configuration();
/*!< CS高电平 */
SD_CS_HIGH();
/*!< CS高电平期间,发送 空 字节数据 0xFF, 10 次 */
/*!< Rise CS and MOSI for 80 clocks cycles */
for (i = 0; i <= 9; i++) {
/*!< 发送 空 字节数据 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
}
if (SD_GoIdleState() == SD_RESPONSE_FAILURE)
return SD_RESPONSE_FAILURE;
//获取卡的类型,最多尝试10次
i=10;
do
{
SD_GetCardType();
} while (SD_Type == SD_TYPE_NOT_SD || i-- > 0);
//不支持的卡
if (SD_Type == SD_TYPE_NOT_SD)
return SD_RESPONSE_FAILURE;
return SD_GetCardInfo(&SDCardInfo);
}
|
该函数的执行流程如下:
(1) 调用前面定义的GPIO_Configuration函数初始化GPIO及SPI的工作模式;
(2) 控制片选信号引脚, 使之持续输出高电平;
(3) 使用for循环调用前面定义的SD_WriteByte函数发送10次SD_DUMMY_BYTE,这个发送过程产生80个时钟信号, 完成了SD卡初始化产生至少74个时钟的要求,注意此处的SD_DUMMY_BYTE必须为0xFF,不能为其它值;
(4) 程序后面调用的SD_GoIdleState、SD_GetCardType及SD_GetCardInfo内部细节稍后详解, 此处仅说明它们的功能;
(5) 调用SD_GoIdleState函数使SD卡进入空闲模式以及使之切换到SPI通讯方式, 并返回该函数的执行结果;
(6) 在do-while循环里调用SD_GetCardType函数最多10次,该函数主要功能为识别SD卡的类型, 区分出是否SD卡、V1.0卡,V2.0的SDSC卡以及V2.0的SDHC卡,识别的结果存储在全局变量SD_Type中;
(7) 若识别结果为后三种可支持的卡类型, 使用return的形式调用SD_GetCardInfo函数读取SD卡的CSD、CID寄存器,并计算SD卡的容量和数据块大小。
进入空闲及SPI模式(SD_GoIdleState函数)
下面讲解初始化过程调用的SD_GoIdleState函数实现,见 代码清单:SD-12。
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 | /**
* @brief 让SD卡进入空闲模式.
* @param None
* @retval SD卡响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_GoIdleState(void)
{
/*!< 片选CS低电平 */
SD_CS_LOW();
/*!< 发送 CMD0 (SD_CMD_GO_IDLE_STATE) 让SD卡切换至SPI模式 */
SD_SendCmd(SD_CMD_GO_IDLE_STATE, 0, 0x95);
/*!< 等待R1响应返回的状态为SD_IN_IDLE_STATE */
if (SD_GetResponse(SD_IN_IDLE_STATE)) {
/*!< 响应不是空闲状态,失败返回 */
return SD_RESPONSE_FAILURE;
}
/*!< 片选CS高电平 */
SD_CS_HIGH();
/*!< 发送 空 字节 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
/*初始化成功返回*/
return SD_RESPONSE_NO_ERROR;
}
|
SD_GoIdleState函数执行流程如下:
(1) 拉低CS片选引脚,调用SD_SendCmd发送CMD0命令(SD_CMD_GO_IDLE_STATE), SD卡接收到这样的控制后,会切换成使用SPI模式, 注意由于CMD0命令是SDIO及SPI驱动共用的命令,且此时并未进入SPI模式,默认还是需要CRC校验码的,因为发送命令时以0x95作为CRC校验码;
(2) 调用SD_GetResponse函数检测SD卡是否返回SD_IN_IDLE_STATE已进入空闲状态的响应, 若检测不到该响应则初始化失败返回;
(3) 在函数结束返回前,调用了函数SD_WriteByte发送空字节SD_DUMMY_BYTE,在这里仅起到延时的作用, 没有什么特别的意义,后面将会经常有这样的操作,不再赘述。
获取SD卡的类型(SD_GetCardType函数)
SD卡成功进入空闲及SPI模式后,可以开始卡识别的核心流程,见 代码清单:SD-13。
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 | /**
* @brief 获取SD卡的版本类型,并区分SDSC和SDHC
* @param 无
* @retval SD响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_GetCardType(void)
{
uint32_t i = 0;
uint32_t Count = 0xFFF;
uint8_t R7R3_Resp[4];
uint8_t R1_Resp;
SD_CS_HIGH();
/*!< 发送空字节延时 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 片选信号CS低电平 */
SD_CS_LOW();
/*!< 发送 CMD8 命令,带0x1AA检查参数*/
SD_SendCmd(SD_CMD_SEND_IF_COND, 0x1AA, 0xFF);
/*!< 等待R1响应 */
while (( (R1_Resp = SD_ReadByte()) == 0xFF) && Count) {
Count--;
}
if (Count == 0) {
/*!< 等待超时 */
return SD_RESPONSE_FAILURE;
}
//响应 = 0x05 非V2.0的卡
if (R1_Resp == (SD_IN_IDLE_STATE|SD_ILLEGAL_COMMAND)) {
/* 激活SD卡 */
do {
/*!< 片选信号CS高电平 */
SD_CS_HIGH();
/*!< 发送空字节延时 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 片选信号CS低电平 */
SD_CS_LOW();
/*!< 发送CMD1完成V1 版本卡的初始化 */
SD_SendCmd(SD_CMD_SEND_OP_COND, 0, 0xFF);
/*!< Wait for no error Response (R1 Format) equal to 0x00 */
} while (SD_GetResponse(SD_RESPONSE_NO_ERROR));
//V1版本的卡完成初始化
SD_Type = SD_TYPE_V1;
//不处理MMC卡
//初始化正常
}
//响应 0x01 V2.0的卡
else if (R1_Resp == SD_IN_IDLE_STATE) {
/*!< 读取CMD8 的R7响应 */
for (i = 0; i < 4; i++) {
R7R3_Resp[i] = SD_ReadByte();
}
/*!< 片选信号CS高电平 */
SD_CS_HIGH();
/*!< 发送空字节延时 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 片选信号CS低电平 */
SD_CS_LOW();
//判断该卡是否支持2.7-3.6V电压
if (R7R3_Resp[2]==0x01 && R7R3_Resp[3]==0xAA) {
//支持电压范围,可以操作
Count = 200;
//发卡初始化指令CMD55+ACMD41
do {
//CMD55,以强调下面的是ACMD命令
SD_SendCmd(SD_CMD_APP_CMD, 0, 0xFF);
// SD_IN_IDLE_STATE
if (!SD_GetResponse(SD_RESPONSE_NO_ERROR))
return SD_RESPONSE_FAILURE; //超时返回
//ACMD41命令带HCS检查位
SD_SendCmd(SD_ACMD_SD_SEND_OP_COND, 0x40000000, 0xFF);
if (Count-- == 0)
return SD_RESPONSE_FAILURE; //重试次数超时
} while (SD_GetResponse(SD_RESPONSE_NO_ERROR));
//初始化指令完成,读取OCR信息,CMD58
//-----------鉴别SDSC SDHC卡类型开始-----------
Count = 200;
do {
/*!< 片选信号CS高电平 */
SD_CS_HIGH();
/*!< 发送空字节延时 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 片选信号CS低电平 */
SD_CS_LOW();
/*!< 发送CMD58 读取OCR寄存器 */
SD_SendCmd(SD_CMD_READ_OCR, 0, 0xFF);
} while ( SD_GetResponse(SD_RESPONSE_NO_ERROR) || Count-- == 0);
if (Count == 0)
return SD_RESPONSE_FAILURE; //重试次数超时
//响应正常,读取R3响应
/*!< 读取CMD58的R3响应 */
for (i = 0; i < 4; i++) {
R7R3_Resp[i] = SD_ReadByte();
}
//检查接收到OCR中的bit30(CCS)
//CCS = 0:SDSC CCS = 1:SDHC
if (R7R3_Resp[0]&0x40) { //检查CCS标志
SD_Type = SD_TYPE_V2HC;
} else {
SD_Type = SD_TYPE_V2;
}
//-----------鉴别SDSC SDHC版本卡的流程结束-----------
}
}
/*!< 片选信号CS高电平 */
SD_CS_HIGH();
/*!< 发送空字节延时 0xFF */
SD_WriteByte(SD_DUMMY_BYTE);
//初始化正常返回
return SD_RESPONSE_NO_ERROR;
}
|
这个函数比较长,但它实际是完全按照图 卡识别流程 走的,说明如下:
(1) 发送CMD8命令,并检测其响应,根据它的响应为0x05和0x01分成V1.0版本的卡和V2.0版本的卡两个分支, 其中0x05实质是卡空闲状态与无法识别该命令的响应代码,0x01则是卡空闲状态的响应代码;
(2) 无法识别CMD8命令的SD卡,即响应为0x05,初始化过程什么简单,在该分支,主机向SD卡继续发送CMD1命令, 若接收到正常响应,则初始化成功,全局变量SD_Type被赋予宏SD_TYPE_V1,表示它被检测成V1.0版本的SD卡;
(3) 在正常响应CMD8命令的分支,首先需要继续SD卡返回的CMD8后续响应信息,这是长度为4个字节的R7响应, 代码中把它存储在R7R3_Resp变量并判断其表示的支持电压范围是否正常,若正常继续后面的流程,否则直接退出函数且SD_Type保持默认值SD_TYPE_NOT_SD,表示非SD卡;
(4) 接着代码中发送ACMD41命令确认SD卡供电电压窗口,按照SD协议,需要发送多次该命令直至得到正常响应, 其中ACMD命令是特殊命令,所以在发送它前还发送了表示特殊命令的CMD55;
(5) 发送CMD58命令以读取OCR寄存器的内容, 通过判断OCR寄存器中的CCS位来区分SDSC卡和SDHC卡,最终判断结果存储在变量SD_Type中。
总之,经过这个函数处理后,完成了SD卡的识别流程,SD_Type变量中存储了SD卡的类型。
获取SD卡信息
初始化SD卡后,可以通过读取其CSD及CID寄存器获取信息,见 代码清单:SD-14。
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 | /**
* @brief 读取卡的CSD寄存器
* 在SPI模式下,读取CSD的方式与读取数据块类似
* @param SD_csd: 存储CSD寄存器的SD_CSD结构体指针
* @retval SD响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_GetCSDRegister(SD_CSD* SD_csd)
{
uint32_t i = 0;
SD_Error rvalue = SD_RESPONSE_FAILURE;
uint8_t CSD_Tab[16];
/*!< 片选信号CS低电平*/
SD_CS_LOW();
/*!< 发送 CMD9 (CSD register) 或 CMD10(CID register)命令 */
SD_SendCmd(SD_CMD_SEND_CSD, 0, 0xFF);
/*!< 等待 R1 响应 (0x00 is no errors) */
if (!SD_GetResponse(SD_RESPONSE_NO_ERROR)) {
//等待数据块Token
if (!SD_GetResponse(SD_START_DATA_SINGLE_BLOCK_READ)) {
for (i = 0; i < 16; i++) {
/*!< 存储 CSD 寄存器的值到 CSD_Tab */
CSD_Tab[i] = SD_ReadByte();
}
}
/*!< 读取 CRC 校验字节 (此处不校验,但SD卡有该流程,需要接收) */
SD_WriteByte(SD_DUMMY_BYTE);
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 设置返回值,表示成功接收到寄存器数据 */
rvalue = SD_RESPONSE_NO_ERROR;
}
/*!< 片选信号CS高电平*/
SD_CS_HIGH();
/*!< 发送 dummy 空字节: 8 时钟周期的延时 */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 字节 0 处理*/
SD_csd->CSDStruct = (CSD_Tab[0] & 0xC0) >> 6;
SD_csd->SysSpecVersion = (CSD_Tab[0] & 0x3C) >> 2;
SD_csd->Reserved1 = CSD_Tab[0] & 0x03;
/*!< 字节 1 处理*/
SD_csd->TAAC = CSD_Tab[1];
/*!< 字节 2 处理*/
SD_csd->NSAC = CSD_Tab[2];
/*!< 以下省略3~15字节的处理......*/
/*!< 返回接收状态 */
return rvalue;
}
|
这个函数SD_GetCSDRegister的主要流程如下:
(1) 拉低片选信号,调用SD_SendCmd向SD卡发送CMD9命令,SD卡接收到该命令后,将会返回一个字节的R1响应、 一个字节的单块数据读取Token、16个字节的CSD寄存器内容、2个字节的CRC校验码;
(2) 调用SD_GetResponse等待SD卡的R1响应;
(3) 接收到R1响应后,再调用SD_GetResponse检查SD卡是否返回单块数据读取的Token, 该宏SD_START_DATA_SINGLE_BLOCK_READ表示的Token值为0xFE;
(4) 接收到Token, 使用for循环调用SD_ReadByte函数接收长度为16个字节的CSD寄存器的值;
(5) 在16个字节的CSD寄存器内容之后,SD卡还返回2个字节的CRC校验码, 此处不进行校验,但还是要让SD卡完成发送流程,所以调用SD_WriteByte函数2次以产生时钟驱动SD卡输出校验码;
(6) 接收完寄存器的内容后,SD_GetCSDRegister函数根据CSD寄存器的定义, 把内容整理至SD_CSD结构体中,方便在其它程序直接通过该结构体访问特定的CSD信息。
在本工程中,还有一个SD_GetCIDRegister函数用于读取CID寄存器的值,该函数的处理过程与这个SD_GetCSDRegister函数类似, 区别仅为发送的命令不同(读取CID时发送CMD10)以及接收到的内容根据CID寄存器的定义整理到SD_CID结构体中,本教程不再详细讲解。
为了方便使用,程序里还把SD_GetCSDRegister、SD_GetCIDRegister函数封装到 SD_GetCardInfo函数中, 并根据读取得的CSD信息计算出卡的容量与数据块大小,见 代码清单:SD-15。
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 | /**
* @brief 获取SD卡的信息.
* @param SD_CardInfo 结构体指针
* @retval SD响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_GetCardInfo(SD_CardInfo *cardinfo)
{
SD_Error status = SD_RESPONSE_FAILURE;
//读取CSD寄存器
status = SD_GetCSDRegister(&(cardinfo->SD_csd));
//读取CID寄存器
status = SD_GetCIDRegister(&(cardinfo->SD_cid));
if ((SD_Type == SD_TYPE_V1) || (SD_Type == SD_TYPE_V2)) {
//块数目基数: CSize + 1
cardinfo->CardCapacity = (cardinfo->SD_csd.DeviceSize + 1) ;
//块数目 = 块数目基数*块数目乘数。块数目乘数: 2的 (C_SIZE_MULT + 2)次方
cardinfo->CardCapacity *= (1 << (cardinfo->SD_csd.DeviceSizeMul + 2));
// 块大小:2的READ_BL_LEN 次方
cardinfo->CardBlockSize = 1 << (cardinfo->SD_csd.RdBlockLen);
//卡容量 = 块数目*块大小
cardinfo->CardCapacity *= cardinfo->CardBlockSize;
} else if (SD_Type == SD_TYPE_V2HC) { //SDHC卡
cardinfo->CardCapacity = (uint64_t)(cardinfo->SD_csd.DeviceSize + 1) * 512 * 1024;
cardinfo->CardBlockSize = 512;
}
/*!< 返回SD响应 */
return status;
}
|
这个函数调用了SD_GetCSDRegister和 SD_GetCIDRegister读取了CSD及CID寄存器的内容, 然后利用CSD寄存器相关的域计算出块大小CardBlockSize和容量CardCapacity。
在计算SD卡块大小和容量的处理中,代码根据SD卡的类型分成了两个分支,其基本计算公式为:SD卡容量 = BLOCKNR * BLOCK_LEN, 其中BLOCKNR为含有的数据块数目,BLOCK_LEN为数据块长度,计算得到的SD卡容量单位为字节。
对于V1.0或SDSC卡的具体计算公式,比较复杂,其说明如下:
BLOCKNR = (C_SIZE+1) * MULT
MULT = 2C_SIZE_MULT+2
BLOCK_LEN = 2READ_BL_LEN
其中:
C_SIZE即代码中的SD_csd.DeviceSize;
C_SIZE_MULT即代码中的SD_csd.DeviceSizeMul;
READ_BL_LEN即代码中的SD_csd.RdBlockLen;
对于SDHC卡则简单得多:
SD卡容量 = (C_SIZE+1) * 512KByte
C_SIZE即代码中的SD_csd.DeviceSize;
35.5.2.6. SD卡数据操作¶
SD卡数据操作一般包括数据读取、数据写入以及存储区擦除,数据读取和写入都可以分为单块操作和多块操作, 由于应用中很少用到擦除操作,本工程仅实现了读写功能。
数据读取操作
读取数据操作与读取CSD寄存器时类似,见 代码清单:SD-16。
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 | /**
* @brief 从SD卡读取一个数据块
* @param pBuffer: 指针,用于存储读取到的数据
* @param ReadAddr: 要读取的SD卡内部地址
* @param BlockSize: 块大小
* @retval SD响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_ReadBlock(uint8_t* pBuffer, uint64_t ReadAddr, uint16_t BlockSize)
{
uint32_t i = 0;
SD_Error rvalue = SD_RESPONSE_FAILURE;
//SDHC卡块大小固定为512,且读命令中的地址的单位是sector
if (SD_Type == SD_TYPE_V2HC) {
BlockSize = 512;
ReadAddr /= 512;
}
/*!< 片选CS低电平*/
SD_CS_LOW();
/*!< 发送 CMD17 (SD_CMD_READ_SINGLE_BLOCK) 以读取一个数据块 */
SD_SendCmd(SD_CMD_READ_SINGLE_BLOCK, ReadAddr, 0xFF);
/*!< 检查R1响应 */
if (!SD_GetResponse(SD_RESPONSE_NO_ERROR)) {
/*!< 检查读取单个数据块的Token */
if (!SD_GetResponse(SD_START_DATA_SINGLE_BLOCK_READ)) {
/*!< 读取一个数据块的数据 : NumByteToRead 个数据 */
for (i = 0; i < BlockSize; i++) {
/*!< 接收一个字节到pBuffer */
*pBuffer = SD_ReadByte();
/*!< 指针加1*/
pBuffer++;
}
/*!< 读取 CRC 校验字节 (此处不校验,但SD卡有该流程,需要接收) */
SD_ReadByte();
SD_ReadByte();
/*!< 设置返回值,表示成功接收到寄存器数据*/
rvalue = SD_RESPONSE_NO_ERROR;
}
}
/*!< 片选信号CS高电平 */
SD_CS_HIGH();
/*!< 发送 dummy 空字节: 8 时钟周期的延时 */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 返回接收状态 */
return rvalue;
}
|
数据读取操作SD_ReadBlock函数有三个形参,分别用于存储读取到的数据的指针、 数据读取起始目标地址和单块长度。SD_ReadBlock函数执行流程如下:
(1) 函数的开头首先就根据SD卡的类型做了重要的处理,因为对于SDHC卡,数据块(扇区)的大小固定为512字节, 而且又由于后续发送SD卡读命令中的地址参数是数据块的编号(扇区号),所以若SD卡类型为SDHC卡时, 程序把输入参数ReadAddr从以字节单位转为扇区为单位,即:ReadAddr /= 512;
(2) 拉低片选信号,调用SD_SendCmd向SD卡发送CMD17命令以及命令参数(即要读取的地址ReadAddr), 此命令的参数对于SDSC卡是实际的数据地址, 对于SDHC卡是扇区的编号,所以ReadAddr在第一个步骤作了区分处理。SD卡接收到该命令后,将会返回一个字节的R1响应、 一个字节的单块数据读取Token、一个从ReadAddr地址开始的数据块内容、2个字节的CRC校验码;
(3) 调用SD_GetResponse等待SD卡的R1响应;
(4) 接收到R1响应后,再调用SD_GetResponse检查SD卡是否返回单块数据读取的Token, 该宏SD_START_DATA_SINGLE_BLOCK_READ表示的Token值为0xFE;
(5) 接收到Token, 使用for循环调用SD_ReadByte函数接收长度为BlockSize个字节的数据;
(6) 读取完数据后,SD卡还返回2个字节的CRC校验码,此处不进行校验, 但还是要让SD卡完成发送流程,所以调用SD_WriteByte函数2次以产生时钟驱动SD卡输出校验码。
对于高容量的SD卡要求块大小必须为512字节,程序员有责任保证目标读取地址与块大小的字节对齐问题。
数据写入操作
数据写入操作与读取操作什么类型,只是发送的命令与数据传输方向的差异,见 代码清单:SD-17。
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 | /**
* @brief 往SD卡写入一个数据块
* @param pBuffer: 指针,指向将要写入的数据
* @param WriteAddr: 要写入的SD卡内部地址
* @param BlockSize: 块大小
* @retval SD响应:
* - SD_RESPONSE_FAILURE: 失败
* - SD_RESPONSE_NO_ERROR: 成功
*/
SD_Error SD_WriteBlock(uint8_t* pBuffer, uint64_t WriteAddr, uint16_t BlockSize)
{
uint32_t i = 0;
SD_Error rvalue = SD_RESPONSE_FAILURE;
//SDHC卡块大小固定为512,且写命令中的地址的单位是sector
if (SD_Type == SD_TYPE_V2HC) {
BlockSize = 512;
WriteAddr /= 512;
}
/*!< 片选CS低电平*/
SD_CS_LOW();
/*!< 发送 CMD24 (SD_CMD_WRITE_SINGLE_BLOCK) 以写入一个数据块 */
SD_SendCmd(SD_CMD_WRITE_SINGLE_BLOCK, WriteAddr, 0xFF);
/*!< 检查R1响应 */
if (!SD_GetResponse(SD_RESPONSE_NO_ERROR)) {
/*!< 发送 dummy 空字节 */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 发送 一个数据块起始Token表示 开始传输数据 */
SD_WriteByte(0xFE);
/*!< 写入一个数据块的数据 */
for (i = 0; i < BlockSize; i++) {
/*!< 发送指针指向的字节 */
SD_WriteByte(*pBuffer);
/*!< 指针加1 */
pBuffer++;
}
/*!< 两个字节的空CRC校验码,默认不验证 */
SD_ReadByte();
SD_ReadByte();
/*!< 读取SD卡数据响应 */
if (SD_GetDataResponse() == SD_DATA_OK) {
rvalue = SD_RESPONSE_NO_ERROR;
}
}
/*!< 片选信号CS高电平 */
SD_CS_HIGH();
/*!< 发送 dummy 空字节: 8 时钟周期的延时 */
SD_WriteByte(SD_DUMMY_BYTE);
/*!< 返回接收状态 */
return rvalue;
}
|
数据写入操作SD_WriteBlock函数有三个形参,分别是要写入的数据指针、写入起始目标地址和单块长度。函数执行流程如下:
(1) 类似地,函数的开头根据SD卡的类型做了重要的处理,对于SDHC卡, 把输入参数WriteAddr从以字节单位转为扇区为单位,即:WriteAddr/= 512;
(2) 拉低片选信号,调用SD_SendCmd向SD卡发送CMD24命令以及命令参数(即要写入的地址WriteAddr), 此命令的参数对于SDSC卡是实际的数据地址,对于SDHC卡是扇区的编号,所以WriteAddr在第一个步骤作了区分处理。
(3) 接收到R1响应后,再调用SD_WriteByte向SD卡发送单块数据开始的Token:0xFE, SD卡接收到此Token值后,将把后续接收到的字节理解成要写入的数据;
(4) 使用for循环调用SD_WriteByte函数写入长度为BlockSize个字节的数据;
(5) 写入完数据后,还继续向SD卡发送2个字节的CRC校验码,此处代码中的SD_ReadByte函数等效于SD_WriteByte(0xFF), 虽然SD卡在SPI模式下默认不进行校验,但还是要完成发送流程,所以发送了两个0xFF。
对于高容量的SD卡要求块大小必须为512字节,程序员有责任保证目标读取地址与块大小的字节对齐问题。
本工程中的驱动还包含有SD_ReadMultiBlocks和SD_WriteMultiBlocks函数用于连续向SD卡读写多个数据块,其程序实现是直接发送了多次单块数据读写的命令,十分类似,此处就不介绍了。
另外,由于SD卡的写入命令会自动对存储区域进行预擦除,所以写入操作前没必要像SPI-FLASH例程那样先擦除再写入,而没有这个需求的话, SD的擦除操作就很少用到,所以本驱动就没有去实现手动的擦除操作了。
35.5.2.7. 测试函数¶
测试SD卡部分的函数存放在sdcard_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 | void SD_Test(void)
{
LED2_ON;
/* SD初始化*/
if ((Status = SD_Init()) != SD_RESPONSE_NO_ERROR) {
LED1_ON;
printf("SD卡初始化失败,请确保SD卡已正确接入开发板,或换一张SD卡测试!\n");
} else {
printf("SD卡初始化成功!\n");
}
if (Status == SD_RESPONSE_NO_ERROR) {
LED2_ON;
/*single block 读写测试*/
SD_SingleBlockTest();
LED2_ON;
/*muti block 读写测试*/
SD_MultiBlockTest();
}
}
|
测试程序以开发板上LED灯指示测试结果,同时打印相关测试结果到串口调试助手。测试程序先调用SD_Init函数完成SD卡初始化, 该函数具体代码参考 代码清单:SD-11 ,如果初始化成功就可以进行数据操作测试。
单块读写测试
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 | /**
* @brief 测试对SD卡的单块读写操作
* @param None
* @retval None
*/
void SD_SingleBlockTest(void)
{
/*------------------- 块 读写 --------------------------*/
/* 向数组填充要写入的数据*/
Fill_Buffer(Buffer_Block_Tx, BLOCK_SIZE, 0x320F);
if (Status == SD_RESPONSE_NO_ERROR) {
/* 把512个字节写入到SD卡的0地址 */
Status = SD_WriteBlock(Buffer_Block_Tx, 0x00, BLOCK_SIZE);
}
if (Status == SD_RESPONSE_NO_ERROR) {
/* 从SD卡的0地址读取512字节 */
Status = SD_ReadBlock(Buffer_Block_Rx, 0x00, BLOCK_SIZE);
}
/* 校验读出的数据是否与写入的数据一致 */
if (Status == SD_RESPONSE_NO_ERROR) {
TransferStatus1 = Buffercmp(Buffer_Block_Tx, Buffer_Block_Rx, BLOCK_SIZE);
}
if (TransferStatus1 == PASSED) {
LED2_ON;
printf("Single block 测试成功!\n");
} else {
LED1_ON;
printf("Single block 测试失败,请确保SD卡正确接入开发板,或换一张SD卡测试!\n");
}
}
|
SD_SingleBlockTest函数主要编程思想是首先填充一个块大小的存储器,通过写入操作把数据写入到SD卡内, 然后通过读取操作读取数据到另外的存储器,然后在对比存储器内容得出读写操作是否正确。
SD_SingleBlockTest函数一开始调用Fill_Buffer函数用于填充存储器内容,它只是简单实用for循环赋值方法给存储区填充数据, 它有三个形参,分别为存储区指针、填充字节数和起始数选择,这里的起始数选择参数对本测试没有实际意义。 SD_WriteBlock函数和SD_ReadBlock函数分别执行数据写入和读取操作, 具体可以参考 代码清单:SD-17 和 代码清单:SD-16。 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 33 | int main(void)
{
/* 配置系统时钟为72 MHz */
SystemClock_Config();
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
DEBUG_USART_Config();
/* 初始化LED灯 */
LED_GPIO_Config();
/* 初始化独立按键 */
Key_GPIO_Config();
/*初始化USART1*/
DEBUG_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_PIN) == KEY_ON) {
printf("\r\n开始进行SD卡读写实验\r\n");
SD_Test();
}
}
}
|
测试过程中有用到LED灯、独立按键和调试串口,所以需要对这些模块进行初始化配置。在无限循环中不断检测按键状态, 如果有被按下就执行SD卡测试函数。由于本实验尚未移植文件系统,所以运行后会破坏原SD卡存储的内容,所以实验前请注意备份SD卡的数据。
35.5.3. 下载验证¶
把Micro SD卡插入到开发板右侧的卡槽内,使用USB线连接开发板上的“USB TO UART”接口到电脑, 电脑端配置好串口调试助手参数。编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板发过来的提示信息, 按下开发板左下边沿的K1按键,开始执行SD卡测试,测试结果在串口调试助手可观察到,板子上LED灯也可以指示测试结果。