34. 基于SD卡的FatFs文件系统¶
34.1. FatFs文件系统介绍¶
- FatFs文件系统的源码可以从FatFs官网下载:
FatFs 源码下载连接:http://elm-chan.org/fsw/ff/archives.html
上一章我们已经全面介绍了SD卡的识别和简单的数据读写,也进行了简单的读写测试,不过像这样直接操作SD卡存储单元,在实际应用中是不现实的。 SD卡一般用来存放文件,所以我们需要加载文件系统到里面。 类似于串行Flash芯片,我们移植FatFs文件系统到SD卡内。
对于FatFs文件系统的介绍和具体移植过程参考 “基于FLASH的FatFs文件系统移植”章节,这里就不做过多介绍,重点放在SD卡与FatFs接口函数编写上。 与串行Flash的FatFs文件系统移植例程相比,基于SD卡的FatFs文件系统部分代码只有diskio.c文件有所不同,其他的不用修改。
提示
有关FatFs文件系统介绍、以及API函数的介绍已经在本书第24章 《基于FLASH的FatFs文件系统移植》 做了详细的介绍,有需要的请返回24章查看,这里就不再重复介绍了。
34.2. FatFs新建工程¶
因为在上一章节的例程中,我们已经配置好了关于SD卡的函数,所以我们在这里直接将第32章的例程拿来修改即可。
- 对于 e2 studio 开发环境:
拷贝一份我们之前的 e2s 工程 “32_SDHI_SDCard_RW”, 然后将工程文件夹重命名为 “33_SDHI_SDCard_FatFs”,最后再将它导入到我们的 e2 studio 工作空间中。
- 对于 Keil 开发环境:
拷贝一份我们之前的 Keil 工程 “32_SDHI_SDCard_RW”, 然后将工程文件夹重命名为 “33_SDHI_SDCard_FatFs”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。
接下来,为了方便,我们直接拷贝第24章“基于FLASH的FatFs文件系统移植”配套例程“24_Flash_FatFs”里的FatFs文件夹过来。 这样就不用重复配置 FatFs 模块了,只需修改 diskio.c 文件。
e2 studio 工程文件结构如下:
32_SDHI_SDCard_FatFs
├─ ......
└─ src
├─ led
│ ├─ bsp_led.c
│ └─ bsp_led.h
├─ debug_uart
│ ├─ bsp_debug_uart.c
│ └─ bsp_debug_uart.h
├─ sdhi_sdcard
│ ├─ bsp_sdhi_sdcard.c
│ └─ bsp_sdhi_sdcard.h
├─ FatFs
│ ├─ diskio.c
│ ├─ diskio.h
│ ├─ ff.c
│ ├─ ff.h
│ ├─ ffconf.h
│ ├─ ffsystem.c
│ └─ ffunicode.c
└─ hal_entry.c
Keil 工程文件结构如下:
32_SDHI_SDCard_FatFs
├─ ......
├─ FatFs //需要手动添加到Keil工程上
│ ├─ diskio.c
│ ├─ diskio.h
│ ├─ ff.c
│ ├─ ff.h
│ ├─ ffconf.h
│ ├─ ffsystem.c
│ └─ ffunicode.c
└─ src
├─ led
│ ├─ bsp_led.c
│ └─ bsp_led.h
├─ debug_uart
│ ├─ bsp_debug_uart.c
│ └─ bsp_debug_uart.h
├─ sdhi_sdcard
│ ├─ bsp_sdhi_sdcard.c
│ └─ bsp_sdhi_sdcard.h
└─ hal_entry.c
34.3. FatFs接口函数¶
FatFs文件系统与存储设备的连接函数在diskio.c文件中,主要有5个函数需要我们编写的,这也是本章节的重点。
34.3.1. 宏定义和存储设备状态获取函数¶
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 | /* Definitions of physical drive number for each drive */
//#define DEV_RAM 0 /* Example: Map Ramdisk to physical drive 0 */
//#define DEV_MMC 1 /* Example: Map MMC/SD card to physical drive 1 */
//#define DEV_USB 2 /* Example: Map USB MSD to physical drive 2 */
#define DEV_FLASH 0
#define DEV_SDCARD 1
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
switch (pdrv) {
case DEV_FLASH :
//QSPI_Flash_WaitForWriteEnd(); //等待Flash芯片内部操作完成
stat = RES_OK;
return stat;
case DEV_SDCARD :
stat = RES_OK;
return stat;
}
return STA_NOINIT;
}
|
FatFs支持同时挂载多个存储设备,通过定义为不同编号以区别。 SD卡一般定义为编号0,编号1预留给串行Flash芯片使用。使用宏定义方式给出SD卡块大小, 方便修改。实际上,SD卡块大小一般都是设置为512字节的,不管是标准SD卡还是高容量SD卡。
disk_status函数要求返回存储设备的当前状态,对于SD卡一般返回SD卡插入状态,这里直接返回正常状态。
34.3.2. 存储设备初始化函数¶
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 | /*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
sdmmc_status_t status;
switch (pdrv) {
case DEV_FLASH :
//QSPI_Flash_Init();
stat = RES_OK;
return stat;
case DEV_SDCARD :
/* 初始化SD卡:打开SDHI外设模块 */
SDCard_Init();
/* 检查SD卡是否插入 */
R_SDHI_StatusGet(&g_sdmmc0_ctrl, &status);
if (!status.card_inserted)
{
/* 等待卡插入中断 */
while (!g_card_inserted)
{
printf("请插入SD卡\r\n");
R_BSP_SoftwareDelay(1000, BSP_DELAY_UNITS_MILLISECONDS);
}
printf("\r\n检测到SD卡已插入\r\n");
}
/* 设备应在检测到VDD最小值后1ms内准备好接受第一个命令。参考SD物理层简化规范6.00版第6.4.1.1节“卡的通电时间”。 */
R_BSP_SoftwareDelay(1U, BSP_DELAY_UNITS_MILLISECONDS);
/* Initialize the SD card. This should not be done until the card is plugged in for SD devices. */
R_SDHI_MediaInit(&g_sdmmc0_ctrl, &my_sdmmc_device);
stat = RES_OK;
return stat;
}
return STA_NOINIT;
}
|
该函数用于初始化存储设备,一般包括相关GPIO初始化、外设环境初始化、中断配置等等。 对于SD卡,直接调用SD_Init函数实现对SD卡初始化, 如果函数返回SD_OK说明SD卡正确插入,并且控制器可以与之正常通信。
34.3.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 30 31 32 | /*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
switch (pdrv) {
case DEV_FLASH :
// translate the arguments here
//QSPI_Flash_BufferRead(buff, sector<<12, count<<12); //1 sector == 4096 bytes
res = RES_OK;
return res;
case DEV_SDCARD :
R_SDHI_Read(&g_sdmmc0_ctrl, buff, sector, count); //1 sector == 512 bytes
while (g_transfer_complete == 0);
g_transfer_complete = 0;
res = RES_OK;
return res;
}
return RES_PARERR;
}
|
disk_read函数用于从存储设备指定地址开始读取一定的数量的数据到指定存储区内。对于SD卡,最重要是使用SD_ReadMultiBlocks函数读取多块数据到存储区。 这里需要注意的地方是SD卡数据操作是使用DMA传输的,并设置数据尺寸为32位大小,为实现数据正确传输,要求存储区是4字节对齐。在某些情况下, FatFs提供的buff地址不是4字节对齐,这会导致DMA数据传输失败,所以为保证数据传输正确,可以先判断存储区地址是否是4字节对齐, 如果存储区地址已经是4字节对齐,无需其他处理,直接使用SD_ReadMultiBlocks函数执行多块读取即可。如果判断得到地址不是4字节对齐, 则先申请一个4字节对齐的临时缓冲区,即局部数组变量scratch,通过定义为DWORD类型可以使得其自动4字节对齐,scratch所占的总存储空间也是一个块大小, 这样把一个块数据读取到scratch内,然后把scratch存储器内容拷贝到buff地址空间上就可以了。
SD_ReadMultiBlocks函数用于从SD卡内读取多个块数据,它有四个形参,分别为存储区地址指针、起始块地址、块大小以及块数量。为保证数据传输完整, 还需要调用SD_WaitReadOperation函数和SD_GetStatus函数检测和保证传输完成。
34.3.4. 存储设备数据写入函数¶
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 | /*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
#if FF_FS_READONLY == 0
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
DRESULT res;
uint32_t write_addr;
switch (pdrv) {
case DEV_FLASH :
write_addr = sector << 12;
//QSPI_Flash_SectorErase(write_addr);
//QSPI_Flash_BufferWrite(buff, write_addr, count<<12);
res = RES_OK;
return res;
case DEV_SDCARD :
R_SDHI_Write(&g_sdmmc0_ctrl, buff, sector, count);
while (g_transfer_complete == 0);
g_transfer_complete = 0;
res = RES_OK;
return res;
}
return RES_PARERR;
}
#endif
|
disk_write函数用于向存储设备指定地址写入指定数量的数据。对于SD卡,执行过程与disk_read函数是非常相似,也必须先检测存储区地址是否是4字节对齐, 如果是4字节对齐则直接调用SD_WriteMultiBlocks函数完成多块数据写入操作。如果不是4字节对齐,申请一个4字节对齐的临时缓冲区, 先把待写入的数据拷贝到该临时缓冲区内,然后才写入到SD卡。
SD_WriteMultiBlocks函数是向SD卡写入多个块数据,它有四个形参,分别为存储区地址指针、起始块地址、块大小以及块数量, 它与SD_ReadMultiBlocks函数执行相互过程。最后也是需要使用相关函数保存数据写入完整才能够退出disk_write函数。
34.3.5. 其他控制函数¶
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 | /*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
switch (pdrv) {
case DEV_FLASH :
switch (cmd) {
case GET_SECTOR_COUNT: /* 扇区数量:1024*4096/1024/1024 = 4(MB) */
*(DWORD *)buff = 1024;
break;
case GET_SECTOR_SIZE: /* 扇区大小 */
*(WORD *)buff = 4096;
break;
case GET_BLOCK_SIZE: /* 同时擦除扇区个数 */
*(DWORD *)buff = 1;
break;
}
res = RES_OK;
return res;
case DEV_SDCARD :
switch (cmd)
{
// 扇区大小。仅当FF_MAX_SS > FF_MIN_SS时,才需要此命令。
case GET_SECTOR_SIZE: /* 扇区大小:512(Byte) */
*(WORD *)buff = (WORD)my_sdmmc_device.sector_size_bytes;
break;
// 擦除块大小(以扇区为单位)
case GET_BLOCK_SIZE: /* 同时擦除扇区个数 */
*(DWORD *)buff = my_sdmmc_device.erase_sector_count;
break;
// 可用扇区数
case GET_SECTOR_COUNT: /* 扇区数量 */
*(DWORD *)buff = my_sdmmc_device.sector_count;
break;
}
res = RES_OK;
return res;
}
return RES_PARERR;
}
|
disk_ioctl 函数有三个形参,pdrv为设备物理编号,cmd为控制指令,包括发出同步信号、获取扇区数目、获取扇区大小、获取擦除块数量等等指令,buff为指令对应的数据指针。
对于SD卡,为支持格式化功能,需要用到获取扇区数量(GET_SECTOR_COUNT)指令和获取块尺寸(GET_BLOCK_SIZE)。 另外,SD卡扇区大小为512字节,串行Flash芯片一般设置扇区大小为4096字节,所以需要用到获取扇区大小(GET_SECTOR_SIZE)指令。
至此,基于SD卡的FatFs文件系统移植就已经完成了, 接下来就编写FatFs基本的文件操作检测移植代码是否可以正确执行。
34.4. FatFs功能测试¶
主要的测试包括格式化测试、文件写入测试和文件读取测试三个部分,主要程序都在bsp_sdhi_sdcard.c文件中实现。
34.4.1. 变量定义¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | MKFS_PARM f_opt = {
.fmt = FM_FAT32, //格式选项
.n_fat = 0, //FATs大小
.align = 0, //数据区域对齐(扇区)
.n_root = 0, //根目录条目数
.au_size = 0, //群集大小(字节)
};
FATFS fs; /* FatFs文件系统对象 */
FIL fnew; /* 文件对象 */
UINT fnum; /* 文件成功读写数量 */
FRESULT res_sd; /* 文件操作结果 */
BYTE ReadBuffer[1024]={0}; /* 读缓冲区 */
BYTE WriteBuffer[] = /* 写缓冲区*/
"感谢您选用野火启明瑞萨RA开发板 [FatFs读写测试文件.txt]";
BYTE work[FF_MAX_SS]; /* Work area (larger is better for processing time) */
|
FATFS是在ff.h文件定义的一个结构体类型,针对的对象是物理设备,包含了物理设备的物理编号、扇区大小等等信息, 一般我们都需要为每个物理设备定义一个FATFS变量。
FIL也是在ff.h文件定义的一个结构体类型,针对的对象是文件系统内具体的文件,包含了文件很多基本属性,比如文件大小、 路径、当前读写地址等等。如果需要在同一时间打开多个文件进行读写,才需要定义多个FIL变量,不然一般定义一个FIL变量即可。
FRESULT是也在ff.h文件定义的一个枚举类型,作为FatFs函数的返回值类型,主要管理FatFs运行中出现的错误。 总共有19种错误类型,包括物理设备读写错误、找不到文件、没有挂载工作空间等等错误。这在实际编程中非常重要, 当有错误出现是我们要停止文件读写,通过返回值我们可以快速定位到错误发生的可能地点。如果运行没有错误才返回FR_OK。
fnum是个32位无符号整形变量,用来记录实际读取或者写入数据的数组。
buffer和textFileBuffer分别对应读取和写入数据缓存区,都是8位无符号整形数组。
34.4.2. hal_entry入口函数¶
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 | /* 用户头文件包含 */
#include "led/bsp_led.h"
#include "debug_uart/bsp_debug_uart.h"
//#include "sdhi_sdcard/bsp_sdhi_sdcard.h" //这里不需要直接包含
//FatFs
#include "FatFs/ff15/ff.h"
MKFS_PARM f_opt = {
.fmt = FM_FAT32, //格式选项
.n_fat = 0, //FATs大小
.align = 0, //数据区域对齐(扇区)
.n_root = 0, //根目录条目数
.au_size = 0, //群集大小(字节)
};
FATFS fs; /* FatFs文件系统对象 */
FIL fnew; /* 文件对象 */
UINT fnum; /* 文件成功读写数量 */
FRESULT res_sd; /* 文件操作结果 */
BYTE ReadBuffer[1024]={0}; /* 读缓冲区 */
BYTE WriteBuffer[] = /* 写缓冲区*/
"感谢您选用野火启明瑞萨RA开发板 [FatFs读写测试文件.txt]";
BYTE work[FF_MAX_SS]; /* Work area (larger is better for processing time) */
void hal_entry(void)
{
/* TODO: add your own code here */
LED_Init(); // LED 初始化
Debug_UART4_Init(); // SCI4 UART 调试串口初始化
printf("这是一个基于SD卡的FatFs使用演示例程\r\n");
printf("打开串口助手查看打印的信息\r\n\r\n");
/*----------------------- 格式化测试 ---------------------------*/
/* 尝试挂载外部FLASH FAT文件系统 */
res_sd = f_mount(&fs, "1:", 1);
/* 如果没有文件系统就格式化SD卡创建文件系统 */
if(res_sd == FR_NO_FILESYSTEM)
{
printf("》SD卡还没有文件系统,即将进行格式化...\r\n");
/* 格式化 */
res_sd = f_mkfs("1:", NULL, work, sizeof(work));
if(res_sd == FR_OK)
{
printf("》SD卡已成功格式化文件系统。\r\n");
/* 格式化后,先取消挂载 */
res_sd = f_mount(NULL, "1:", 1);
/* 重新挂载 */
res_sd = f_mount(&fs, "1:", 1);
}
else
{
LED1_ON; //红灯亮
printf("《《格式化失败。》》\r\n");
while(1);
}
}
else if(res_sd != FR_OK)
{
LED1_ON; //红灯亮
printf("!!SD卡挂载文件系统失败。(%d)\r\n", res_sd);
printf("!!可能原因:SD卡初始化不成功。\r\n");
while(1);
}
else
{
printf("》文件系统挂载成功,可以进行读写测试\r\n");
}
/*----------------------- 文件系统测试:写测试 -----------------------------*/
/* 打开文件,如果文件不存在则创建它 */
printf("\r\n****** 即将进行文件写入测试... ******\r\n");
// 创建新文件。如果该文件存在,则覆盖。 |
// 可以写文件
res_sd = f_open(&fnew, "1:FatFs读写测试文件.txt", FA_CREATE_ALWAYS | FA_WRITE); //FatFs读写测试文件 FatFs_files
if ( res_sd == FR_OK )
{
printf("》打开/创建FatFs读写测试文件.txt文件成功,向文件写入数据。\r\n");
/* 将指定存储区内容写入到文件内 */
res_sd = f_write(&fnew, WriteBuffer, sizeof(WriteBuffer), &fnum);
if(res_sd == FR_OK)
{
printf("》文件写入成功,写入字节数据:%d\n", fnum);
printf("》向文件写入的数据为:\r\n%s\r\n", WriteBuffer);
}
else
{
LED1_ON; //红灯亮
printf("!!文件写入失败:(%d)\n", res_sd);
}
/* 不再写,关闭文件 */
f_close(&fnew);
}
else
{
LED1_ON; //红灯亮
printf("!!打开/创建文件失败。\r\n");
while(1);
}
/*------------------- 文件系统测试:读测试 ------------------------------------*/
printf("\r\n****** 即将进行文件读取测试... ******\r\n");
res_sd = f_open(&fnew, "1:FatFs读写测试文件.txt", FA_OPEN_EXISTING | FA_READ);
if(res_sd == FR_OK)
{
printf("》打开文件成功。\r\n");
res_sd = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum);
if(res_sd == FR_OK)
{
printf("》文件读取成功,读到字节数据:%d\r\n", fnum);
printf("》读取得的文件数据为:\r\n%s \r\n", ReadBuffer);
}
else
{
LED1_ON; //红灯亮
printf("!!文件读取失败:(%d)\n", res_sd);
}
}
else
{
LED1_ON; //红灯亮
printf("!!打开文件失败。\r\n");
while(1);
}
/* 不再读,关闭文件 */
f_close(&fnew);
/* 不再使用文件系统,取消挂载文件系统 */
f_mount(NULL, "1:", 1);
printf("****** 测试结束 ******\r\n");
while(1);
#if BSP_TZ_SECURE_BUILD
/* Enter non-secure code */
R_BSP_NonSecureEnter();
#endif
}
|
最后是hal_entry函数,关于LED和串口调试以及SD卡数据读写部分不再赘述,在本代码中只讲解重要部分。
程序的开头首先初始化LED灯和调试串口,用来指示程序进程。
FatFs的第一步工作就是使用f_mount函数挂载工作区。f_mount函数有三个形参,第一个参数是指向FATFS变量指针,如果赋值为NULL可以取消物理设备挂载。 第二个参数为逻辑设备编号,使用设备根路径表示,与物理设备编号挂钩, 我们定义SD卡物理编号为1,所以这里使用“1:”。 第三个参数可选0或1,1表示立即挂载,0表示不立即挂载,延迟挂载。 f_mount函数会返回一个FRESULT类型值,指示运行情况。
如果f_mount函数返回值为FR_NO_FILESYSTEM,说明SD卡没有FAT文件系统。 我们就必须对SD卡进行格式化处理。使用f_mkfs函数可以实现格式化操作。 f_mkfs函数有三个形参,第一个参数为逻辑设备编号; 第二参数可选0或者1,0表示设备为一般硬盘,1表示设备为软盘。 第三个参数指定扇区大小, 如果为0,表示通过中disk_ioctl函数获取。 格式化成功后需要先取消挂载原来设备,再重新挂载设备。
在设备正常挂载后,就可以进行文件读写操作了。使用文件之前,必须使用f_open函数打开文件,不再使用文件必须使用f_close函数关闭文件, 这个跟电脑端操作文件步骤类似。f_open函数有三个形参,第一个参数为文件对象指针。 第二参数为目标文件,包含绝对路径的文件名称和后缀名。 第三个参数为访问文件模式选择,可以是设置文件的读模式、 写模式,也可以是创建新文件模式,或者总是创建新文件模式、打开文件、总是打开文件模式等。比如对于写测试, 使用FA_CREATE_ALWAYS和FA_WRITE组合模式,就是总是新建一个文件并开启写模式。
f_close函数用于不再对文件进行读写操作关闭文件,f_close函数只要一个形参,为文件对象指针。f_close函数运行可以确保缓冲区完全写入到文件内。
成功打开文件之后就可以使用f_write函数和f_read函数对文件进行写操作和读操作。这两个函数用到的参数是一致的,只不过一个是数据写入,一个是数据读取。 f_write函数第一个形参为文件对象指针,使用与f_open函数一致即可。第二个参数为待写入数据的首地址,对于f_read函数就是用来存放读出数据的首地址。 第三个参数为写入数据的字节数,对于f_read函数就是欲读取数据的字节数。第四个参数为32位无符号整形指针,这里使用fnum变量地址赋值给它, 在运行读写操作函数后,fnum变量指示成功读取或者写入的字节个数。
最后,不再使用文件系统时,使用f_mount函数取消挂载。
34.4.3. 下载验证¶
保证开发板相关硬件连接正确,用USB线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。 程序开始运行后在串口调试助手可看到格式化测试、写文件检测和读文件检测三个过程;最后如果所有读写操作都正常, LED绿灯闪烁,如果在运行中FatFs出现错误,则LED红灯亮。正确执行例程程序后可以使用读卡器将SD卡在电脑端打开, 我们可以在SD卡根目录下看到 “FatFs读写测试文件.txt” 文件,这与程序设计是相吻合的。