1.前言
FATFS(File Allocation Table File System)是一个轻量级的文件系统,被广泛应用于嵌入式系统和小型存储设备中。它由Chan提供,并在嵌入式系统中得到了广泛的应用和支持。

FatFS官网地址为:FatFs - Generic FAT Filesystem Module

本文主要分享关于 FatFS 文件系统在SD卡/SD nand上的移植使用,本文所采用的硬件环境如下:

控制器:国民技术 N32G4FR,Cortex-M4内核控制器
SD nand:创世 CSNP4GCR01-AMW,4Gb SD nand



2. FATFS文件系统介绍
2.1 文件系统的概念和作用

在计算机系统中,文件系统是用于组织和管理存储介质上的文件和目录的一种结构。文件系统提供了对文件的读取、写入、删除、重命名等操作,以及对目录的创建、删除、遍历等操作。它使得用户和应用程序能够方便地访问和管理存储设备上的数据。

2.2 FATFS的特点和优势
FATFS是一个开源的文件系统,具有以下特点和优势:

1.轻量级和高效性:FATFS是一个轻量级的文件系统,适用于资源受限的嵌入式系统和小型存储设备。它的代码量相对较小,占用的存储空间较少,并且具有较高的运行效率。

2.跨平台兼容性:FATFS可以在多个操作系统和平台上运行,包括嵌入式系统、Windows、Linux等。这使得开发人员可以方便地将FATFS应用于不同的硬件平台和操作系统环境中。

3.易于集成和使用:FATFS的源代码结构清晰,具有简单的API接口,易于集成到嵌入式系统的应用程序中。开发人员可以通过简单的函数调用来实现文件的读写、目录的创建和遍历等操作。

4.支持多种存储介质:FATFS支持多种存储介质,包括SD卡、SPI Flash、硬盘等。它可以根据不同的存储介质进行配置和适配,并提供统一的文件系统接口。

5.支持多种文件操作:FATFS支持常见的文件操作,如文件的打开、关闭、读取、写入、定位等。它还支持目录的创建、删除、重命名和遍历,以及文件和目录的属性管理。

6.支持长文件名和短文件名:FATFS同时支持长文件名(Long File Name,LFN)和短文件名(Short File Name,SFN)。这使得文件系统更加灵活和兼容,能够处理各种不同的文件命名规则。

7.支持文件系统的格式化和检查:FATFS提供了格式化和检查文件系统的功能。开发人员可以通过相应的API函数来格式化存储介质并创建文件系统,也可以进行文件系统的检查和修复。这使得文件系统能够保持良好的状态,提高数据的可靠性和完整性。

8.支持文件的读写缓存:FATFS允许开发人员配置读写缓存,以提高文件的读写性能。通过使用读写缓存,可以减少对存储介质的频繁访问,提高文件的读写速度。

9.具有错误处理和容错机制:FATFS提供了错误处理和容错机制,能够检测和处理各种错误情况。例如,当存储介质出现错误或不可用时,FATFS能够进行相应的错误处理和恢复,保证文件系统的稳定性和可靠性。

3.相关源码获取
3.1 FatFs文件源码下载

移植FatFs,第一步当然就是获取对应的移植文件了, FatFs的源文件在其官网上即可直接下载,地址:FatFs - Generic FAT Filesystem Module



3.2 国民技术N32软件开发套件获取
国民技术作为一款国产的IC,对标stm32,性能上也还是很不错了,不过其开发资料下载会有点麻烦,不过有幸你看到这里,就不会感觉很麻烦了,其开发资料下载方法如下:

1.打开电脑的文件管理器,输入以下内容:ftp://download.nationstech.com ,之后回车访问




2.此远程文件夹就是国民技术所有IC的全部资料了,之后便可根据自己手上所使用的IC,下载对应的资料,此处我们使用的是N32G4FR系列芯片,进入1-Microcontrollers 目录,选择对应的压缩包,单击鼠标右键,选择 复制到文件夹 进行下载



4. 文件架构说明
4.1 FatFs文件架构说明

从官网上下载FatFs文件解压后,主要有两个目录:



documents:主要存放关于FatFs有关离线本地文档,文档内容和官网一致,等同于一个本地的副本,可以很好解决官网访问速度过慢的问题

source:里面存放了FatFs有关的源码

4.1.1 本地离线文档访问方式
使用本地浏览器打开 documents 目录下的 00index_e.html 文件,即可访问一个和官网完全一样的网页文档!且文档内的超链接也可直接点击跳转!

此文档非常重要,在FatFs文件使用中,很多API的接口,我们都需要参考此文档才行!




4.1.2 源码文件介绍
进入source 目录我们即可看到 fatfs 所有源码文件,7个文件,非常的精简,其各自功能如下:



综上,其实完成FatFs文件的移植,主要是修改 diskio.c 和 ffconf.h 这两个文件就可以了,如此简单!

在 diskio.c 文件内实现物理存储器的读写实体访问,在ffconf.h 内实现FatFs文件系统子功能的开关,之后就可以使用FatFs文件系统了!

4.2 国民技术N32文件架构说明
N32 资料压缩包下载后,内容如下,里面包含了对应系列芯片的所有资料,手册、应用笔记、软硬件资料等等。



其中我们关注最核心的几个文件,路径如下:

芯片包路径如下:N32G4FR_V3.0.0\6-软件开发套件(Software Development Kit)

例程Demo路径如下:N32G4FR_V3.0.0\6-软件开发套件(Software Development Kit)\Nationstech.N32G4FR_Library.2.1.0\projects\n32g4fr_EVAL\examples

5.FatFs移植
5.1 SD卡/SD nand读写实现

SD卡和SD nand只是封装上存在差异,软件上没有区别,因此后续不作区分

我们需要在SD nand上移植FatFs文件系统,肯定首先需要实现对SD nand的基本读写访问,之后再在读写访问的基础上给它穿上文件系统这层外套了,这就像一件一件穿衣服一样。

关于SD nand的读写操作,我们可以直接打开N32对应配套的SDIO例程,例程已完成了对SD卡的读写测试,我们直接拿过来测一下,可以跑起来说明硬件上就没有什么问题,关于例程的优化可以后续再慢慢进行。

例程路径:N32G4FR_V3.0.0\6-软件开发套件(Software Development Kit)\Nationstech.N32G4FR_Library.2.1.0\projects\n32g4fr_EVAL\examples\SDIO



对应例程硬件配置如下:

    1、SystemClock:144MHz
    2、DMA通道:DMA2_CH4
    3、SDIO 配置:
            D0   -->   PC8          50MHz,AF_PP
            D1   -->   PC9          50MHz,AF_PP
            D2   -->   PC10         50MHz,AF_PP
            D3   -->   PC11         50MHz,AF_PP
            CLK   -->  PC12         50MHz,AF_PP
            CMD   -->  PD2          50MHz,AF_PP

            分频系数:178    (SDIOCLK = HCLK, SDIO_CK = HCLK/(2 + 分频系数))
            上升沿有效
            禁用旁路
            禁用时钟保持
            总线位宽4bit
    
    4、USART1配置:
            TX  -->  PA9            50MHz,AF_PP
            波特率:115200
            数据位:8bit
            停止位:1bit
            无校验

    5、测试步骤与现象
        a,测试前请先安装好TF卡
        b,编译下载代码复位运行
        c,从串口看打印信息,验证结果

例程正确运行后,串口打印结果如下:




备注:
如果例程运行不起来,硬件也确认没有问题,尝试修改下SD卡识别过程中的速度,具体内容如下:

sdio_sdcard.c文件 710 行,SDIO_InitStructure.ClkDiv = 178; 修改为 SDIO_InitStructure.ClkDiv = 179;



为什么修改这里呢?

因为我觉得这里Demo里面可能有错误,此IC主频为144MHz,看时钟树,对应此处HCLK应该也是144MHz,那么ClkDiv的值应该配置为179时,SDIO_CK才为400KHz,Demo里面的配置应该是800KHz!





实测SDIO CLK亦是如此,确实是800k,而对于部分SD卡,识别过程最大仅支持400K,因此建议可以尝试修改此分频系数值,排除此原因!

通过main.c文件的demo可知,关于SDIO访问SD nand的核心是如下几个函数:

初始化SD nand

	Status = SD_Init(0, 3, 4);
    if (Status != SD_OK)
    {
        printf("SD Card initialization failed!\r\n");
        return testResult;
    }

写SD nand,同时注意写区分多块写和单块写操作

#ifdef MUL_BLOCK_RW
    Status = SD_WriteMultiBlocks(Buf_TX, 0x01, BLOCK_SIZE, 4);
#else
    Status = SD_WriteBlock(Buf_TX, 0x00, BLOCK_SIZE);
#endif
    
    Status = SD_WaitWriteOperation();
    while (SD_GetStatus() != SD_TRANSFER_OK);
    if (Status != SD_OK)
    {
        printf("SD Card write block failed!\r\n");
        return testResult;
    }

读SD nand,注意读也区分多块读和单块读
#ifdef MUL_BLOCK_RW
    Status = SD_ReadMultiBlocks(Buf_RX, 0x00, BLOCK_SIZE, 4);
#else
    Status = SD_ReadBlock(Buf_RX, 0x00, BLOCK_SIZE);
#endif
    
    Status = SD_WaitReadOperation();
    while (SD_GetStatus() != SD_TRANSFER_OK);
    if (Status != SD_OK)
    {
        printf("SD Card read block failed!\r\n");
        return testResult;
    }

5.2 FatFs移植
5.2.1 添加源文件

1.拷贝FatFs至工程路径



2.将相关源文件添加进工程



3.添加相关头文件路径



4.打开diskio.c文件,采用SD nand读写及初始化接口完成对应 diskio.c 文件内函数的实现,对应FatFs文档内的 Media Access Interface,实现下述接口即可使用FatFs。



5.2.2 添加设备号
首先,包含对应sd nand相关程序的头文件,并定义对应的物理设备号

#include "ff.h"			/* Obtains integer types */
#include "diskio.h"		/* Declarations of disk functions */
#include "./sdio_sdcard/sdio_sdcard.h"

/* 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_SD    0

5.2.3 修改 disk_status() 函数
在线文档链接 disk_status

disk_status() 用于获取存储设备的状态,函数原型如下:

#define FF_VOLUMES		1
/* Number of volumes (logical drives) to be used. (1-10) */

传入参数:
pdrv:用于标识目标设备的物理驱动器号,在单驱动系统中始终为零。
返回值:
DSTATUS:返回驱动器的状态,FatFs仅关心 STA_NOINIT 和 STA_PROTECT
STA_NOINIT: 表示驱动器尚未初始化
STA_NODISK:表示驱动器中没有介质
STA_PROTECT:表示介质被写保护,如果 STA_NODISK 状态位为1,则此位无效
注意,每个状态占据一个bit位,返回值为三个数据位的或。

/* Disk Status Bits (DSTATUS) */

#define STA_NOINIT		0x01	/* Drive not initialized */
#define STA_NODISK		0x02	/* No medium in the drive */
#define STA_PROTECT		0x04	/* Write protected */

根据SD nand操作接口,修改实现如下:

DSTATUS disk_status (
    BYTE pdrv		/* Physical drive nmuber to identify the drive */
)
{
    int ret  = 0, result = 0;
  
    if (pdrv == DEV_SD) {
        ret = SD_GetStatus();
        if(ret == SD_TRANSFER_ERROR)
            result |= STA_NODISK;
        else
            result = 0;
    }

    return result;
}

5.2.4 修改 disk_initialize() 函数
在线文档链接 disk_initialize

disk_initialize() 此函数初始化存储设备并使其准备好进行通用读/写,当函数成功时,返回值中的STA_NOINIT标志被清除,函数原型如下:

DSTATUS disk_initialize (
  BYTE pdrv            /* [IN] Physical drive number */
);

传入参数:
pdrv:用于标识目标设备的物理驱动器号,在单驱动系统中始终为零。
返回值:
DSTATUS:该函数返回当前驱动器状态标志作为结果。参考disk_status() 函数
根据SD nand操作接口,修改实现如下:

/*-----------------------------------------------------------------------*/
/* Inidialize a Drive                                                    */
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize (
    BYTE pdrv				/* Physical drive nmuber to identify the drive */
)
{
    int ret;

    ret = SD_Init(0, 3, 4);
    if (ret != SD_OK)
        return STA_NOINIT;
    else
        return 0;
}

5.2.5 修改 disk_read() 函数


在线文档链接 disk_read

disk_read() 用于从存储设备中读取数据,函数原型如下:

DRESULT disk_read (
  BYTE pdrv,     /* [IN] Physical drive number */
  BYTE* buff,    /* [OUT] Pointer to the read data buffer */
  LBA_t sector,  /* [IN] Start sector number */
  UINT count     /* [IN] Number of sectros to read */
);

传入参数:

pdrv:用于标识目标设备的物理驱动器号,在单驱动系统中始终为零。
buff:指向存储读取数据的字节数组的指针。
sector:起始扇区号,注意此处数据类型LBA_t是DWORD或QWORD的别名,具体取决于配置选项。
count:读取的扇区数。


返回值:

DRESULT
RES_OK : 成功
RES_ERROR:错误
RES_PARERR:无效参数
RES_NOTRDY:设备尚未初始化

/* Results of Disk Functions */
typedef enum {
	RES_OK = 0,		/* 0: Successful */
	RES_ERROR,		/* 1: R/W Error */
	RES_WRPRT,		/* 2: Write Protected */
	RES_NOTRDY,		/* 3: Not Ready */
	RES_PARERR		/* 4: Invalid Parameter */
} DRESULT;

根据SD nand操作接口,修改实现如下:

/*-----------------------------------------------------------------------*/
/* 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 */
)
{
    int ret;

    if (count == 1) {
        ret = SD_ReadBlock(buff, sector * 512, 512);
    } else {
        ret = SD_ReadMultiBlocks(buff, sector * 512, 512, count);
    }
    if (ret != SD_OK)   goto error;
    ret = SD_WaitReadOperation();
    if (ret != SD_OK)   goto error;
    while (SD_GetStatus() != SD_TRANSFER_OK);
    if (ret != SD_OK)   goto error;

    return RES_OK;
error:
    return RES_ERROR;
}

关于 disk_read 有以下注意事项:

1.对通用存储设备(如存储卡、硬盘和光盘)的读/写操作是以称为扇区的数据字节块单位进行的,FatFs 支持 512 到 4096 字节范围内的扇区大小。当 FatFs 配置为固定扇区大小(FF_MIN_SS == FF_MAX_SS,这是大多数情况)时,通用读/写功能必须仅在此扇区大小下工作。当 FatFs 配置为可变扇区大小(FF_MIN_SS < FF_MAX_SS )时,在disk_initialize函数成功后使用disk_ioctl函数查询介质的扇区大小。

2.此外,关于 buff 传递的内存地址,它不一定是内存对齐的,这个需要大家在使用的时候注意。

5.2.6 修改 disk_write() 函数


在线文档链接 disk_write

disk_write() 用于往存储设备中写入数据,函数原型如下:

DRESULT disk_write (
  BYTE pdrv,        /* [IN] Physical drive number */
  const BYTE* buff, /* [IN] Pointer to the data to be written */
  LBA_t sector,     /* [IN] Sector number to write from */
  UINT count        /* [IN] Number of sectors to write */
);

传入参数:

pdrv:用于标识目标设备的物理驱动器号,在单驱动系统中始终为零。
buff:指向存储被写入数据的字节数组的指针。
sector:起始扇区号,注意此处数据类型LBA_t是DWORD或QWORD的别名,具体取决于配置选项。
count:读取的扇区数。


返回值:

DRESULT
RES_OK : 成功
RES_ERROR:错误
RES_WRPRT:设备处于写保护状态
RES_PARERR:无效参数
RES_NOTRDY:设备尚未初始化

/* Results of Disk Functions */
typedef enum {
	RES_OK = 0,		/* 0: Successful */
	RES_ERROR,		/* 1: R/W Error */
	RES_WRPRT,		/* 2: Write Protected */
	RES_NOTRDY,		/* 3: Not Ready */
	RES_PARERR		/* 4: Invalid Parameter */
} DRESULT;

根据SD nand操作接口,修改实现如下:

/*-----------------------------------------------------------------------*/
/* 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 */
)
{
    int ret;

    if (count == 1) {
        ret = SD_WriteBlock((uint8_t *)buff, sector * 512, 512);
    } else {
        ret = SD_WriteMultiBlocks((uint8_t *)buff, sector * 512, 512, count);
    }
    if (ret != SD_OK)   goto error;
    ret = SD_WaitWriteOperation();
    if (ret != SD_OK)   goto error;
    while (SD_GetStatus() != SD_TRANSFER_OK);
    if (ret != SD_OK)   goto error;

    return RES_OK;
error:
    return RES_ERROR;
}

#endif

关于 disk_write() 有以下注意事项:

1.buff 参数和disk_read一样,不一定是字节对齐的,需要注意
2.多个扇区的写请求通常不建议拆解成单个的扇区写操作,这回降低传输速率,但是我们在使用的时候也需要注意对应的SD卡/SD nand所支持的最大连续写入扇区数限制!
3.调用disk_write()函数不要求写操作完成后才返回,可以在写过程或数据丢入缓存后返回,但是需要注意返回后,buff指针所指向的内容将变成非法的,不能再使用。写完成操作完成,可在调用 disk_ioctl() 函数CTRL_SYNC 命令时完成。通过延迟写入的实现,可增大文件系统的吞吐量(加快读写速度的点子!!!)\


5.2.7 修改 disk_ioctl() 函数


在线文档链接 disk_ioctl

disk_ioctl() 用于控制设备的除了读写之外的其他特定功能,函数原型如下:

DRESULT disk_ioctl (
  BYTE pdrv,     /* [IN] Drive number */
  BYTE cmd,      /* [IN] Control command code */
  void* buff     /* [I/O] Parameter and data buffer */
);

传入参数:

pdrv:用于标识目标设备的物理驱动器号,在单驱动系统中始终为零。
cmd:命令代码。
buff:指针参数,具体取决于 cmd 参数。


返回值:

DRESULT
RES_OK : 成功
RES_ERROR:错误
RES_PARERR:无效参数
RES_NOTRDY:设备尚未初始化

/* Results of Disk Functions */
typedef enum {
	RES_OK = 0,		/* 0: Successful */
	RES_ERROR,		/* 1: R/W Error */
	RES_WRPRT,		/* 2: Write Protected */
	RES_NOTRDY,		/* 3: Not Ready */
	RES_PARERR		/* 4: Invalid Parameter */
} DRESULT;

关于命令参数,分为标准命令参数以及可选命令参数,当 FF_FS_READONLY == 1 和 FF_MAX_SS == FF_MIN_SS时, disk_ioctl 函数不需要实现也可运行,此处我们先简单实现标准命令参数。

标准命令参数有与以下几种:

命令 描述
CTRL_SYNC 同步命令,确保设备写操作完成。
GET_SECTOR_COUNT 获取有效扇区大小,此数据将被存放在一个LBA_t类型的buff指针指向的数据中。此命令被f_mkfs和f_fdisk函数使用。f_mkfs函数用于创建文件系统,f_fdisk函数用于对磁盘进行分区。在执行这两个函数时,需要确定要创建的文件系统或分区的大小,所以需要获取磁盘上可用扇区的数量。
GET_SECTOR_SIZE 获取扇区大小,也就是读写操作的最小数据单位,使用WORD类型buff指针指向的数据存储。有效扇区大小有512、1024、2048、4096。此命令只有在FF_MAX_SS>FF_MIN_SS时被调用,如果FF_MAX_SS=FF_MIN_SS,此命令不会被调用,此时读写必须按照每扇区FF_MAX_SS字节访问。
GET_BLOCK_SIZE 获取擦除块大小,单位为扇区!使用DWORD类型的buff指针指向的数据存储。允许的值范围在 1 ~ 32768,但值必须时2的幂,即2的指数倍。 如果无法确定擦除块大小或介质不是闪存存储器则返回1。此命令在f_mkfs函数未指定块大小时使用。当创建文件系统时,如果没有指定擦除块大小,f_mkfs函数会尝试根据获取到的擦除块大小来对齐数据区域,以提高闪存的性能。
CTRL_TRIM 通知磁盘 I/O 层或存储设备不再需要扇区块上的数据,可以将其擦除。扇区块在由 buff 指向的 LBA _ t 数组{ < Start LBA > ,< End LBA > }中指定。这是一个与 ATA 设备相同的命令。如果不支持此函数或不支持闪存设备,则不对此命令执行任何操作。FatFs 不会检查结果代码,即使扇区块未擦除,文件函数也不会受到影响。在删除集群链并在 f _ mkfs 函数中调用此命令。当 FF _ USE _ TRIM == 1时,它必须实现。



以下是可选命令,大家亦可自行查看官方文档,此处不再实现。



根据SD nand操作接口,修改实现如下:

/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions                                               */
/*-----------------------------------------------------------------------*/

DRESULT disk_ioctl (
    BYTE pdrv,		/* Physical drive nmuber (0..) */
    BYTE cmd,		/* Control code */
    void *buff		/* Buffer to send/receive control data */
)
{
    switch (cmd) {
        case CTRL_SYNC:
        break;
        case GET_SECTOR_COUNT:
            *(LBA_t *)buff = (SDCardInfo.CardCapacity / SDCardInfo.CardBlockSize);
        break;
        case GET_SECTOR_SIZE:
            *(WORD *)buff = SDCardInfo.CardBlockSize;
        break;
        case GET_BLOCK_SIZE:
            *(DWORD *)buff = 1;
        break;
        case CTRL_TRIM:
        break;
    }

    return RES_OK;
}

5.2.8 修改 ffconf.h 取消 get_fattime 实现
get_fattime()函数用于获取当前时间,此函数不影响fatfs访问sd nand操作,因此我们暂时先屏蔽,后续再实现

进入ffconf.h 文件,修改 FF_FS_NORTC 宏为 1

#define FF_FS_NORTC		1
#define FF_NORTC_MON	5
#define FF_NORTC_MDAY	14
#define FF_NORTC_YEAR	2023
/* The option FF_FS_NORTC switches timestamp feature. If the system does not have
/  an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the
/  timestamp feature. Every object modified by FatFs will have a fixed timestamp
/  defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time.
/  To enable timestamp function (FF_FS_NORTC = 0), get_fattime() function need to be
/  added to the project to read current time form real-time clock. FF_NORTC_MON,
/  FF_NORTC_MDAY and FF_NORTC_YEAR have no effect.
/  These options have no effect in read-only configuration (FF_FS_READONLY = 1). */

5.2.9 接口测试
在使用使用通用文件操作接口(指 f_open、f_read、f_write、f_close)访问SD nand之前,我们可以先测试下我们刚刚实现的 diskio.c 内的接口是否可用。因为这是通用文件操作底层所依赖的接口,如果这些接口出问题,通用文件操作接口肯定是跑不起来的!

修改 main 函数如下:

#include "diskio.h"
int main(void)
{
    int res;
    
    bsp_uart_init();
    printf("fatfs test!\r\n");
    
    Memset(Buf_RX, 0x00, Buf_Len);
    Fill_Buffer(Buf_TX, Buf_Len, 0x00);
    
    res = disk_initialize(0);
    printf("disk inital res:%d\r\n", res);
    SD_Info(&SDCardInfo);
    
    res = disk_read(0, Buf_RX, 1, 4);
    printf("disk read res:%d\r\n", res);
    dataShow(Buf_RX, Buf_Len);
    
    res = disk_write(0, Buf_TX, 1, 4);
    printf("disk write res:%d\r\n", res);
    dataShow(Buf_TX, Buf_Len);
    
    res = disk_read(0, Buf_RX, 1, 4);
    printf("disk read2 res:%d\r\n", res);
    dataShow(Buf_RX, Buf_Len);
	
	while(1);
}

5.2.10 调用通用文件操作接口进行读写测试
测试通过,接下来,我们便可调用通用的文件操作接口,通过FatFs完成对SD nand的读写访问了。

在进行读写访问之前,我们首先需要格式化SD nand,这是因为新的SD nand上是没有数据的,也就没有文件系统,因此我们需要先对齐进行格式化。此操作与U盘插入电脑上点击格式化操作类似。


    // 初始化文件系统
    res = f_mount(&fs, "0:", 1);
    if (res != FR_OK) {
        printf("fatfs mount error! ret = %d\r\n", res);
    }
    
    if(res == FR_NO_FILESYSTEM) {  //FR_NO_FILESYSTEM值为13,表示没有有效的设备
        // 格式化SD卡
        res = f_mkfs("0:", 0, buffer, sizeof(buffer));
        if (res != FR_OK) {
            printf("fatfs mkfs error! ret:%d\r\n", res);
        }

        res = f_mount(NULL, "0:", 1);
        printf("fatfs unmount ret:%d!\r\n", res);
        res = f_mount(&fs, "0:", 1);
        if (res != FR_OK) {
            printf("2: fatfs mount error! ret = %d\r\n", res);
        }
    }

5.2.10.1 f_mount() 函数
f_mount() 函数用于挂载文件系统以及取消卸载文件系统,函数原型如下:

FRESULT f_mount (
  FATFS*       fs,    /* [IN] Filesystem object */
  const TCHAR* path,  /* [IN] Logical drive number */
  BYTE         opt    /* [IN] Initialization option */
);

参数:

fs:指向一个文件系统对象指针,当传入为 NULL 时,表示卸载文件系统
path:一个非空字符串,内容为特定的逻辑物理设备号。
opt:可选项,0:不立即挂载(第一次访问时挂载);1:立即挂载


返回值:

FR_OK
FR_INVALID_DRIVE
FR_DISK_ERR
FR_NOT_READY
FR_NOT_ENABLED
FR_NO_FILESYSTEM
使用 f_mount 即可实现挂载和卸载,当然卸载也可以使用f_unmount函数,原型如下:

FRESULT f_unmount (
  const TCHAR* path   /* [IN] Logical drive number */
);

上述代码,首先调用f_mount挂载设备,当检测到设备内没有文件系统时调用f_mkfs格式化设备。

5.2.10.2 f_mkfs() 函数
f_mkfs函数原型如下:

FRESULT f_mkfs (
  const TCHAR* path,   /* [IN] Logical drive number */
  const MKFS_PARM* opt,/* [IN] Format options */
  void* work,          /* [-]  Working buffer */
  UINT len             /* [IN] Size of working buffer */
);

参数:

path:一个非空字符串,内容为特定的逻辑物理设备号。
opt:指定格式选项结构MKFS_PARM持有格式选项。如果给出空指针,它会以默认值为函数提供每个选项。该结构有五个成员,顺序如下:
BYTE fmt:指定 FAT 类型标志FM_FAT、FM_FAT32、FM_EXFAT和这三者的按位或FM_ANY的组合。未启用 exFAT 时忽略FM_EXFAT 。这些标志指定要创建的 FAT 卷类型。如果指定了两种或多种类型,将根据卷大小和au_size选择其中一种。标志FM_SFD指定以 SFD 格式在驱动器上创建卷。默认值为FM_ANY。
BYTE n_fat:指定 FAT/FAT32 卷上的 FAT 副本数。此成员的有效值为 1 或 2。默认值 (0) 和任何无效值都为 1。如果 FAT 类型为 exFAT,则此成员无效。
UINT n_align:以扇区为单位指定卷数据区(文件分配池,通常是闪存介质的擦除块边界)的对齐方式。该成员的有效值介于 1 和 32768 之间(包括 2 的幂)。如果给出零(默认值)或任何无效值,该函数将使用 disk_ioctl 函数从较低层获取块大小。
DWORD au_size:以字节为单位指定分配单元(簇)的大小。对于 FAT/FAT32 卷,有效值为扇区大小和 128 * 扇区大小(包括在内)之间的 2 的幂,或者对于 exFAT 卷,最大为 16 MB。如果给出零(默认值)或任何无效值,则该函数使用默认分配单元大小,具体取决于卷大小。
UINT n_root:指定 FAT 卷上的根目录条目数。此成员的有效值最大为 32768 并与扇区大小/32 对齐。默认值 (0) 和任何无效值都为 512。如果 FAT 类型为 FAT32 或 exFAT,则此成员无效。
work:格式化过程中的工作缓冲区指针
len:缓冲区大小,单位字节。大小最小为FF_MAX_SS,大量的工作缓冲区减少了向驱动器写入事务的数量,因此格式化过程将很快完成。


返回值:

FR_OK
FR_DISK_ERR
FR_NOT_READY
FR_WRITE_PROTECTED
FR_INVALID_DRIVE
FR_MKFS_ABORTED
FR_INVALID_PARAMETER
FR_NOT_ENOUGH_CORE


5.2.10.3 读写测试
SD nand格式好了之后,就可以调用通用文件操作函数接口(f_open、f_read、f_write、f_close)对齐进行读写了,修改 main 函数如下:

#include "ff.h"
#include <string.h>

BYTE buffer[FF_MAX_SS];
/**
 * @brief   Main program
 */
int main(void)
{
	FATFS fs;
    FIL file;
    FRESULT res;
    
    bsp_uart_init();

    printf("fatfs test!\r\n");
    
    // 初始化文件系统
    res = f_mount(&fs, "0:", 1);
    if (res != FR_OK) {
        printf("fatfs mount error! ret = %d\r\n", res);
    }
    
    if(res == FR_NO_FILESYSTEM) {  //FR_NO_FILESYSTEM值为13,表示没有有效的设备
        // 格式化SD卡
        res = f_mkfs("0:", 0, buffer, sizeof(buffer));
        if (res != FR_OK) {
            printf("fatfs mkfs error! ret:%d\r\n", res);
        }

        res = f_mount(NULL, "0:", 1);
        printf("fatfs unmount ret:%d!\r\n", res);
        res = f_mount(&fs, "0:", 1);
        if (res != FR_OK) {
            printf("2: fatfs mount error! ret = %d\r\n", res);
        }
    }

    // 创建一个新文件
    res = f_open(&file, "test.txt", FA_CREATE_ALWAYS | FA_WRITE);
    if (res != FR_OK) {
        printf("fatfs open error!\r\n");
    }

    // 写入数据到文件
    const char* data = "Hello, FatFs!";
    UINT bytes_written;
    res = f_write(&file, data, strlen(data), &bytes_written);
    if (res != FR_OK) {
        printf("fatfs write error!\r\n");
    }

    // 关闭文件
    res = f_close(&file);
    if (res != FR_OK) {
        printf("fatfs close error!\r\n");
    }

    // 打开文件并读取数据
    res = f_open(&file, "test.txt", FA_READ);
    if (res != FR_OK) {
        printf("2: fatfs open error!\r\n");
    }

    // 读取文件数据
    char read_data[50];
    UINT bytes_read;
    res = f_read(&file, read_data, sizeof(read_data), &bytes_read);
    if (res != FR_OK) {
        printf("2: fatfs read error!\r\n");
    }

    // 关闭文件
    res = f_close(&file);
    if (res != FR_OK) {
        printf("2: fatfs close error!\r\n");
    }

    // 打印读取的数据
    read_data[bytes_read] = '\0';  // 添加字符串结束符
    printf("Read data: %s\n", read_data);
    
    while (1) {
    }
}

测试结果如下:


6. 结束语
至此,FatFs已经移植完成,需要注意的是,在此次移植过程中,以分享移植流程、思路为主,对于部分函数接口的实现比较粗糙,在实际项目中需要结合本文中对相应接口的描述,对其进行优化以确保产品的稳定性,不过相信那些都不是什么难题。

以上就是关于FatFs移植的全部流程了,欢迎大家讨论!共同进步!