SDIO-SD卡
文章目录
- SDIO-SD卡
- SD卡结构
- SDIO总线
- 命令
- SD卡的操作模式
- [STM32 的 SDIO 功能框图](#STM32 的 SDIO 功能框图)
- SDIO结构体
-
- SDIO初始化结构体
- [SDIO 命令初始化结构体](#SDIO 命令初始化结构体)
- [SDIO 数据初始化结构体](#SDIO 数据初始化结构体)
- SD卡的读写实验
-
- 准备工作
- [void SD_EraseTest(void)](#void SD_EraseTest(void))
- [void SD_MultiBlockTest(void)](#void SD_MultiBlockTest(void))
- 比较函数和填充函数补充
- 测试程序
- main.c
SD卡结构
物理结构

- 存储单元是存储数据部件,存储单元通过存储单元接口与卡控制单元进行数据传输;
- 电源检测单元保证SD卡工作在合适的电压下,如出现掉电或上状态时,它会使控制单元和存储单元接口复位;
- 卡及接口控制单元控制SD卡的运行状态,它包括有8个寄存器;
- 接口驱动器控制SD卡引脚的输入输出
- SD卡总共有8个寄存器,用于设定或表示SD卡信息。
SD卡寄存器列表

SDIO总线
SDIO总线拓扑
SD卡与SDIO接口示意图

推荐一个单独SD总线应该连接一个单独的SD卡
SD卡使用9-pin接口通信,其中3根电源线、1根时钟线、1根命令线和4根数据线,具体如下:
• CLK:时钟线,由SDIO主机产生,即由STM32控制器输出;
• CMD:命令控制线,SDIO主机通过该线发送命令控制SD卡,如果命令要求SD卡提供应答,SD卡也是通过该线传输应答信息;
• D0-3:数据线,传输读写数据;SD卡可将D0拉低表示忙状态;
• VDD、VSS1、VSS2:电源和地信号;
SDIO总线
SDIO不管是从主机控制器向SD卡传输,还是SD卡向主机控制器传输都只以CLK时钟线的上升沿为有效。

开始通讯的时候要发送一个起始位"0",最后由一个停止位"1"停止。
SD通讯一般是主机发送一个命令,从设备接收到命令后做出响应(有些命令没有响应可能)如果有数据传输,DATA线也需要用
SDIO总线协议

补充:
- SD数据是以块(Black)形式传输的,SDHC卡数据块长度一般为512字节
- 数据块需要CRC位来保证数据传输成功。
- CRC位由SD卡系统硬件生成。
- STM32控制器可以控制使用单线或4线传输,本开发板(野火指南者)设计使用4线传输
协议:
SD数据传输支持单块和多块读写,它们分别对应不同的操作命令,多块写入还需要使用命令来停止整个写入操作。
数据写入前需要检测SD卡忙状态,因为SD卡在接收到数据后编程到存储区过程需要一定操作时间。SD卡忙状态通过把D0线拉低表示。
常规数据传输

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

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

命令的类型
• 无响应广播命令 (bc),发送到所有卡,不返回任务响应;
• 带响应广播命令 (bcr),发送到所有卡,同时接收来自所有卡响应;
• 寻址命令 (ac),发送到选定卡,DAT 线无数据传输;
• 寻址数据传输命令 (adtc),发送到选定卡,DAT 线有数据传输。
在标准中定义了两种类型的通用命令:特定应用命令 (ACMD) 和常规命令 (GEN_CMD)。要使用 SD 卡制造商特定的 ACMD 命令如 ACMD6,需要在发送该命令之前发送 CMD55 命令,告知 SD 卡接下来的命令为特定应用命令。CMD55 命令只对紧接的第一个命令有效,SD 卡如果检测到 CMD55 之后的第一条命令为 ACMD 则执行其特定应用功能,如果检测发现不是 ACMD 命令,则执行标准命令。
命令集

七个响应类型:
-
R1 Response (Normal Response): R1响应是最基本的响应,包含一个字节的状态位,用于指示命令是否成功执行。
-
R1b Response (Normal with Busy): 类似于R1,但在命令执行完成后,卡会持续将忙位(busy)发送给主机,直到卡准备好执行下一个命令。
-
R2 Response (CID or CSD Register Response): 包含两个字节的数据,用于读取CID(Card ID)或CSD(Card Specific Data)寄存器内容。
-
R3 Response (OCR Register Response): 包含四个字节的数据,用于读取OCR(Operating Conditions Register)寄存器内容。
-
R6 Response (Published RCA Response): 包含一个字节的状态位和一个字节的相对卡地址(RCA),用于获取卡的相对地址。
-
R7 Response (Card Interface Condition Response): 包含一个字节的状态位和一个字节的回应信息,用于卡初始化阶段。
-
Data Response (for Data Transfer Commands): 在读/写数据时,卡会响应数据响应,用于指示数据是否成功接收。
注意:
- 短响应是 48bit长度,只有 R2 类型是长响应,其长度为 136bit。
- 除了 R3 类型之外,其他响应都使用 CRC7 校验来校验,对于 R2 类型是使用 CID 和 CSD 寄存器内部 CRC7。
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 之后也进入数据传输模式
数据传输模式

CMD7 用来选定和取消指定的卡,卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态,必须选择一个 RCA 地址目标卡使其进入传输状态才可以进行数据通信。同时通过 CMD7 命令(选择卡命令)也可以让已经被选择的目标卡返回到待机状态。数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处于传输状态下可以使用表 SD 部分命令描述 中面向块的读写以及擦除命令对卡进行数据读写、擦除。CMD12可以中断正在进行的数据通信,让卡返回到传输状态。CMD0 和 CMD15 会中止任何数据编程操作,返回卡识别模式,这可能导致卡数据被损坏。
STM32 的 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。
内部适配器

控制单元

电源管理部件会在系统断电和上电阶段禁止 SD 卡总线输出信号。时钟管理部件控制 CLK 线时钟信号生成。一般使用 SDIOCLK 分频得到。
命令路径
命令路径控制命令发送,并接收卡的响应。

CPSM 状态机描述图
STM32 控制器以命令路径状态机 (CPSM) 来描述 SDIO适配器的状态变化,并加入了等待超时检测功能,以便退出永久等待的情况。
由stm32自己控制无需十分了解

数据路径
数据路径部件负责与 SD 卡相互数据传输

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

数据 FIFO
数据 FIFO(先进先出) 部件是一个数据缓冲器,带发送和接收单元。控制器的 FIFO 包含宽度为32bit、深度为 32 字的数据缓冲器和发送/接收逻辑。其中 SDIO 状态寄存器 (SDIO_STA) 的 TXACT位用于指示当前正在发送数据,RXACT 位指示当前正在接收数据,这两个位不可能同时为 1。• 当 TXACT 为 1 时,可以通过 AHB 接口将数据写入到传输 FIFO。• 当 RXACT 为 1 时,接收 FIFO 存放从数据路径部件接收到的数据。根据 FIFO 空或满状态会把 SDIO_STA 寄存器位值 1,并可以产生中断和 DMA 请求。
SDIO结构体
SDIO初始化结构体
c
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;
SDIO 命令初始化结构体
c
typedef struct {
uint32_t SDIO_Argument; // 命令参数
uint32_t SDIO_CmdIndex; // 命令号
uint32_t SDIO_Response; // 响应类型
uint32_t SDIO_Wait; // 等待使能
uint32_t SDIO_CPSM; // 命令路径状态机
} SDIO_CmdInitTypeDef;
SDIO 数据初始化结构体
c
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;
SD卡的读写实验
准备工作
c
#include "sdio/sdio_test.h"
#include "./led/bsp_led.h"
#include "./sdio/bsp_sdio_sdcard.h"
#include "./usart/bsp_usart.h"
/* Private typedef -----------------------------------------------------------*/
typedef enum {FAILED = 0, PASSED = !FAILED} TestStatus;
/* Private define ------------------------------------------------------------*/
#define BLOCK_SIZE 512 /* Block Size in Bytes */
#define NUMBER_OF_BLOCKS 10 /* For Multi Blocks operation (Read/Write) */
#define MULTI_BUFFER_SIZE (BLOCK_SIZE * NUMBER_OF_BLOCKS)
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
uint8_t Buffer_Block_Tx[BLOCK_SIZE], Buffer_Block_Rx[BLOCK_SIZE];
uint8_t Buffer_MultiBlock_Tx[MULTI_BUFFER_SIZE], Buffer_MultiBlock_Rx[MULTI_BUFFER_SIZE];
volatile TestStatus EraseStatus = FAILED, TransferStatus1 = FAILED, TransferStatus2 = FAILED;
SD_Error Status = SD_OK;
/* Private function prototypes -----------------------------------------------*/
static void SD_EraseTest(void);
static void SD_SingleBlockTest(void);
void SD_MultiBlockTest(void);
static void Fill_Buffer(uint8_t *pBuffer, uint32_t BufferLength, uint32_t Offset);
static TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint32_t BufferLength);
static TestStatus eBuffercmp(uint8_t* pBuffer, uint32_t BufferLength);
/* Private functions---------------------------------------------------------*/
void SD_EraseTest(void)
c
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");
}
}
-
擦除操作:
cStatus = SD_Erase(0x00, (BLOCK_SIZE * NUMBER_OF_BLOCKS));
这里调用了
SD_Erase
函数,擦除了从地址0x00开始,总共BLOCK_SIZE * NUMBER_OF_BLOCKS
大小的数据块。Status
用于存储函数执行的状态。 -
读取操作:
cStatus = SD_ReadMultiBlocks(Buffer_MultiBlock_Rx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
如果擦除操作成功,接下来进行读取操作。这里调用了
SD_ReadMultiBlocks
函数,从地址0x00开始读取BLOCK_SIZE
大小的数据块,总共读取NUMBER_OF_BLOCKS
个块。 -
等待读取操作完成:
cStatus = SD_WaitReadOperation();
等待读取操作完成,确保数据已被成功读取。
-
检查擦除的块的正确性:
cEraseStatus = eBuffercmp(Buffer_MultiBlock_Rx, MULTI_BUFFER_SIZE);
通过比较读取的数据块与期望的擦除状态,判断擦除是否成功。
-
根据检查结果输出信息:
cif (EraseStatus == PASSED) { LED_GREEN; printf("SD卡擦除测试成功!\n"); } else { LED_BLUE; printf("SD卡擦除测试失败!\n"); printf("温馨提示:部分SD卡不支持擦除测试,若SD卡能通过下面的single读写测试,即表示SD卡能够正常使用。\n"); }
根据擦除测试的结果,点亮相应的LED并输出信息。
void SD_MultiBlockTest(void)
c
{
/*--------------- Multiple Block Read/Write ---------------------*/
/* Fill the buffer to send */
Fill_Buffer(Buffer_MultiBlock_Tx, MULTI_BUFFER_SIZE, 0x0);
if (Status == SD_OK)
{
/* Write multiple block of many bytes on address 0 */
Status = SD_WriteMultiBlocks(Buffer_MultiBlock_Tx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
/* Check if the Transfer is finished */
Status = SD_WaitWriteOperation();
while(SD_GetStatus() != SD_TRANSFER_OK);
}
if (Status == SD_OK)
{
/* Read block of many bytes from address 0 */
Status = SD_ReadMultiBlocks(Buffer_MultiBlock_Rx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
/* 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)
{
TransferStatus2 = Buffercmp(Buffer_MultiBlock_Tx, Buffer_MultiBlock_Rx, MULTI_BUFFER_SIZE);
}
if(TransferStatus2 == PASSED)
{
LED_GREEN;
printf("Multi block 测试成功!");
}
else
{
LED_RED;
printf("Multi block 测试失败,请确保SD卡正确接入开发板,或换一张SD卡测试!");
}
}
这是一个多块数据读写测试的C语言函数。让我解释一下主要的步骤:
-
填充发送缓冲区:
cFill_Buffer(Buffer_MultiBlock_Tx, MULTI_BUFFER_SIZE, 0x0);
这里调用了
Fill_Buffer
函数,用0x0填充了Buffer_MultiBlock_Tx
缓冲区,该缓冲区用于发送数据。 -
多块数据写操作:
cStatus = SD_WriteMultiBlocks(Buffer_MultiBlock_Tx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
如果填充缓冲区成功,接下来进行多块数据写入操作。调用了
SD_WriteMultiBlocks
函数,将填充的数据写入从地址0x00开始的多个数据块。 -
等待写操作完成:
cStatus = SD_WaitWriteOperation(); while(SD_GetStatus() != SD_TRANSFER_OK);
等待写入操作完成,确保数据已成功写入SD卡。
-
多块数据读操作:
cStatus = SD_ReadMultiBlocks(Buffer_MultiBlock_Rx, 0x00, BLOCK_SIZE, NUMBER_OF_BLOCKS);
如果写入操作成功,接下来进行多块数据读取操作。调用了
SD_ReadMultiBlocks
函数,从地址0x00开始读取多个数据块。 -
等待读操作完成:
cStatus = SD_WaitReadOperation(); while(SD_GetStatus() != SD_TRANSFER_OK);
等待读取操作完成,确保数据已成功读取。
-
检查写入的数据的正确性:
cTransferStatus2 = Buffercmp(Buffer_MultiBlock_Tx, Buffer_MultiBlock_Rx, MULTI_BUFFER_SIZE);
通过比较写入的数据和读取的数据,判断写入是否成功。
-
根据检查结果输出信息:
cif(TransferStatus2 == PASSED) { LED_GREEN; printf("Multi block 测试成功!"); } else { LED_RED; printf("Multi block 测试失败,请确保SD卡正确接入开发板,或换一张SD卡测试!"); }
根据多块数据读写测试的结果,点亮相应的LED并输出信息。
这段代码主要用于测试SD卡的多块数据读写功能,并通过比较写入和读取的数据来验证操作的正确性。如果测试成功,LED变绿并输出成功信息,否则LED变红并输出失败信息。
比较函数和填充函数补充
c
/**
* @brief Compares two buffers.
* @param pBuffer1, pBuffer2: buffers to be compared.
* @param BufferLength: buffer's length
* @retval PASSED: pBuffer1 identical to pBuffer2
* FAILED: pBuffer1 differs from pBuffer2
*/
TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint32_t BufferLength)
{
while (BufferLength--)
{
if (*pBuffer1 != *pBuffer2)
{
return FAILED;
}
pBuffer1++;
pBuffer2++;
}
return PASSED;
}
/**
* @brief Fills buffer with user predefined data.
* @param pBuffer: pointer on the Buffer to fill
* @param BufferLength: size of the buffer to fill
* @param Offset: first value to fill on the Buffer
* @retval None
*/
void Fill_Buffer(uint8_t *pBuffer, uint32_t BufferLength, uint32_t Offset)
{
uint16_t index = 0;
/* Put in global buffer same values */
for (index = 0; index < BufferLength; index++)
{
pBuffer[index] = index + Offset;
}
}
/**
* @brief Checks if a buffer has all its values are equal to zero.
* @param pBuffer: buffer to be compared.
* @param BufferLength: buffer's length
* @retval PASSED: pBuffer values are zero
* FAILED: At least one value from pBuffer buffer is different from zero.
*/
TestStatus eBuffercmp(uint8_t* pBuffer, uint32_t BufferLength)
{
while (BufferLength--)
{
/* In some SD Cards the erased state is 0xFF, in others it's 0x00 */
if ((*pBuffer != 0xFF) && (*pBuffer != 0x00))
{
return FAILED;
}
pBuffer++;
}
return PASSED;
}
/*********************************************END OF FILE**********************/
测试程序
c
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();
}
}
main.c
c
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();
}
}