本专栏争取每周三更新直到更新完成,期待大家的订阅关注,欢迎互相学习交流。
本文需要一些SD卡的前置知识,后续文章会介绍,这里先介绍一下FATFS文件系统。关于FATFS的文章分为上下两篇,上篇主要介绍什么是FAT文件系统以及FATFS的移植,下篇主要介绍FATFS的一些API函数。
目录
一、FATFS文件系统简介
1.1 FATFS引入
通常我们买来一张全新的SD卡时,如果我们想把它通过读卡器插入到电脑使用,我们需要先对其格式化,这是因为Windows系统使用的是FAT文件系统,我们格式化SD卡,实际也就是在SD卡中建立一个FAT文件系统,这样我们的电脑才能识别它。
FAT是一个专门为小型的嵌入式系统设计的,完全用标准C语言编写,完全免费开源的文件系统。FAT具有良好的硬件平台独立性,可以移植到C51、PIC、ARM等单片机上,而且只需要做一些简单的修改,使用起来非常方便。
1.2 FATFS特点
FATFS有以下特点
- Windows 兼容的 FAT 文件系统
- 与平台无关,移植简单
- 代码量少、效率高
- 支持多卷(物理驱动器或分区,最多 10 个卷)
- 多个 ANSI/OEM 代码页包括 DBCS
- 支持长文件名、ANSI/OEM 或 Unicode
- 支持 RTOS
- 支持多种扇区大小
- 只读、最小化的 API 和 I/O 缓冲区等
这里简单说明一下几个名词,仅供参考,大家可以自行搜索了解更多详细内容。
上面提到的卷指的是是硬盘上的存储空间,一个硬盘包括好多卷,一卷也可以跨越许多磁盘。卷又分为很多种,比如启动卷、跨区卷、带区卷等。此外还有簇,指的是磁盘文件存储管理的最小单位。
Unicode(Universal Multiple-Octet Coded Character Set)简称UCS,中文意思是统一码,或者叫万国码,可以简单理解为他就是一个数字映射表,每一个数字代表不同的字符或文字。
二、FATFS文件系统移植
2.1 FATFS源码文件简介
下面我们介绍一下如何移植FATFS。首先我们需要下载到它的源码,有需要的友友可以私信获取。下载完成后打开其中的“src”文件夹,其中是我们需要的源码。我们在移植FATFS模块的时候,一般只需要修改ffconf.h 和 diskio.c这两个文件。
- option文件夹是一些可选的外部C文件,包含了多语言支持需要的文件和转换函数,移植时我们通常会选择cc936.c文件,用来支持简体中文,它包含了简体中文GBK和Unicode互相转换的功能函数。
- diskio.c文件是FATFS移植的最关键文件,它为文件系统提供了最底层的接口返回问函数。
- diskio.h里面包含了FATFS用到的宏定义以及diskio.c文件内与底层硬件接口相关的函数声明。
- 00history.txt介绍了当前FATFS的版本更新情况。
- 00readme.txt介绍了文件夹中各个文件的功能。
- integer.h文件中是一些数据类型定义。
- ff.c文件时FATFS的核心,里面包含了FATFS的各个模块程序,时文件管理的实现方法,我们在移植时通常不需要对这个文件进行修改。
- ffconf.h是FATFS的配置文件,其中包含了对FATFS功能配置的宏定义。我们通过修改这些宏定义就可会议实现对FATFS功能的裁剪,达到自己想要的效果。
2.2 FATFS重要配置选项
上面在介绍源码文件时提到,FATFS模块的所有配置项都是存放在 ffconf.h 里面,我们这里再介绍一些重要的配置选项。
- _FS_TINY该宏定义时设置使用的是标准模式还是微小模式,我们通常设置为0,使用标准模式。
- _FS_READONLY该宏定义是选择是否使用只读模式,通常我们选择可读可写,设置为0。
- _FS_MINIMIZE该宏定义是选择是否裁剪掉一些函数,通常选择不裁剪,设置为0。
- _USE_STRFUNC该宏定义是设置是否支持字符串类操作,通常选择支持,设置为1。
- _USE_MKFS该宏定义是设置是否使用格式化,我们选择使用,设置为1。
- _USE_FASTSEEK该宏定义用来设定是否是能快速定位,通常选择使用,设置为1。
- _USE_LABEL该宏定义用来设置是否支持磁盘盘符读取与设置,通常选择支持,设置为1。使能后我们就可以通过相关函数来读取或者设置磁盘的名字。
- _CODE_PAGE该宏定义用来设置语言类型,通常我们选择简体中文,设置为936(也就是c936.c文件)。
- _USE_LFN该宏定义用来设置是否支持长文件名,取值范围是0到3。0表示不支持长文件名,1~3表示支持长文件名,但是存储的地方不一样。通常设置为3,可以通过ff_memalloc函数来动态分配长文件名的存储区域。
- _MAX_LFN该宏定义用来设置允许的最大文件名长度,我们设置为最大值255。
- _LFN_UNICODE该宏定义用来设置是否使用FATFS的字符编码,通常设置为不使用,设置为0。
- _VOLUMES该宏定义用来设置FATFS支持的逻辑涉笔数量。
- _MAX_SS该宏定义用来设置山区缓冲的最大值,一般设置为512。
2.3 FATFS移植步骤
想要将FATFS移植到STM32实际并不复杂,主要分为三步
- 下载FATFS源码
- 将源码添加到Keil工程
- 编写一些底层的接口函数
通常最后一步我们需要编写六个底层的接口函数,下面我们来详细地介绍一下它们。
2.3.1 disk_initialize函数
- 函数功能:初始化磁盘。
- 函数原型:DSTATUS disk_initialize(BYTE pdrv);
- 输入参数:prdrv是要初始化的逻辑驱动器号,也就是盘符,取值范围是0~9。
- 返回值:返回一个盘符状态作为结果。
- 注意事项:应用程序不应该在FATFS活动时调用此函数,否则卷上的FAT结构可能会损坏。
//初始化磁盘
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
u8 res=0;
switch(pdrv)
{
case SD_CARD://SD卡
res=SD_Init();//SD卡初始化
break;
case EX_FLASH://外部flash
W25QXX_Init();
FLASH_SECTOR_COUNT=2048*12;//W25Q1218,前12M字节给FATFS占用
break;
default:
res=1;
}
if(res)return STA_NOINIT;
else return 0; //初始化成功
}
2.3.2 disk_status函数
- 函数功能:返回当前磁盘驱动器的状态。
- 函数原型:DSTATUS disk_status (BYTE pdrv);
- 输入参数:pdrv是要确认的逻辑驱动器号,也就是盘符,取值范围是0~9。
- 返回值:有如下几个返回值
#define STA_NOINIT 0x01 /* Drive not initialized */
#define STA_NODISK 0x02 /* No medium in the drive */
#define STA_PROTECT 0x04 /* Write protected */
FATFS只使用前两个返回值。
//获得磁盘状态
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
return RES_OK;
}
2.3.3 disk_read函数
- 函数功能:从磁盘上读取扇区。
- 函数原型:DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
- 输入参数:pdrv:驱动器逻辑号;
buff:指向存储读取数据字节数组的指针;
sector:指定起始扇区的逻辑块上的地址;
count:指定要读取的扇区数; - 返回值:返回值结构体如下
/* 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;
//读扇区
//pdrv:磁盘编号0~9
//*buff:数据接收缓冲首地址
//sector:扇区地址
//count:需要读取的扇区数
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
u8 res=0;
if (!count)return RES_PARERR;//count不能等于0,否则返回参数错误
switch(pdrv)
{
case SD_CARD://SD卡
res=SD_ReadDisk(buff,sector,count);
while(res)//读出错
{
SD_Init(); //重新初始化SD卡
res=SD_ReadDisk(buff,sector,count);
//printf("sd rd error:%d\r\n",res);
}
break;
case EX_FLASH://外部flash
for(;count>0;count--)
{
W25QXX_Read(buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE);
sector++;
buff+=FLASH_SECTOR_SIZE;
}
res=0;
break;
default:
res=1;
}
//处理返回值,将SPI_SD_driver.c的返回值转成ff.c的返回值
if(res==0x00)return RES_OK;
else return RES_ERROR;
}
2.3.4 disk_write函数
- 函数功能:向磁盘写入一个或多个扇区。
- 函数原型:DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
- 输入参数:pdrv:要操作的逻辑驱动器号;
buff:指向要写入的数组的指针;
sector:指定起始扇区逻辑块上的地址;
count:指定要写入的扇区数,取值范围是1~128; - 返回值:与上面的读函数为同一个结构体。
//写扇区
//pdrv:磁盘编号0~9
//*buff:发送数据首地址
//sector:扇区地址
//count:需要写入的扇区数
#if _USE_WRITE
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to write */
)
{
u8 res=0;
if (!count)return RES_PARERR;//count不能等于0,否则返回参数错误
switch(pdrv)
{
case SD_CARD://SD卡
res=SD_WriteDisk((u8*)buff,sector,count);
while(res)//写出错
{
SD_Init(); //重新初始化SD卡
res=SD_WriteDisk((u8*)buff,sector,count);
//printf("sd wr error:%d\r\n",res);
}
break;
case EX_FLASH://外部flash
for(;count>0;count--)
{
W25QXX_Write((u8*)buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE);
sector++;
buff+=FLASH_SECTOR_SIZE;
}
res=0;
break;
default:
res=1;
}
//处理返回值,将SPI_SD_driver.c的返回值转成ff.c的返回值
if(res == 0x00)return RES_OK;
else return RES_ERROR;
}
#endif
2.3.5 disk_ioctl函数
- 函数功能:控制设备指定特性和一些除了读写外的杂项功能。
- 函数原型:DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
- 输入参数:pdrv:要操作的逻辑驱动器号;
cmd:命令代码;
buff:指向参数缓冲区的指针,取决于命令代码,不使用时指向一个NULL指针; - 返回值:与上面的读/写函数为同一个结构体。
- 注意事项:
这里贴一下一些命令的宏定义
/* Command code for disk_ioctrl fucntion */
/* Generic command (Used by FatFs) */
#define CTRL_SYNC 0 /* Complete pending write process (needed at _FS_READONLY == 0) */
#define GET_SECTOR_COUNT 1 /* Get media size (needed at _USE_MKFS == 1) */
#define GET_SECTOR_SIZE 2 /* Get sector size (needed at _MAX_SS != _MIN_SS) */
#define GET_BLOCK_SIZE 3 /* Get erase block size (needed at _USE_MKFS == 1) */
#define CTRL_TRIM 4 /* Inform device that the data on the block of sectors is no longer used (needed at _USE_TRIM == 1) */
/* Generic command (Not used by FatFs) */
#define CTRL_POWER 5 /* Get/Set power status */
#define CTRL_LOCK 6 /* Lock/Unlock media removal */
#define CTRL_EJECT 7 /* Eject media */
#define CTRL_FORMAT 8 /* Create physical format on the media */
/* MMC/SDC specific ioctl command */
#define MMC_GET_TYPE 10 /* Get card type */
#define MMC_GET_CSD 11 /* Get CSD */
#define MMC_GET_CID 12 /* Get CID */
#define MMC_GET_OCR 13 /* Get OCR */
#define MMC_GET_SDSTAT 14 /* Get SD status */
/* ATA/CF specific ioctl command */
#define ATA_GET_REV 20 /* Get F/W revision */
#define ATA_GET_MODEL 21 /* Get model name */
#define ATA_GET_SN 22 /* Get serial number */
//其他表参数的获得
//pdrv:磁盘编号0~9
//ctrl:控制代码
//*buff:发送/接收缓冲区指针
#if _USE_IOCTL
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
if(pdrv==SD_CARD)//SD卡
{
switch(cmd)
{
case CTRL_SYNC:
res = RES_OK;
break;
case GET_SECTOR_SIZE:
*(DWORD*)buff = 512;
res = RES_OK;
break;
case GET_BLOCK_SIZE:
*(WORD*)buff = SDCardInfo.CardBlockSize;
res = RES_OK;
break;
case GET_SECTOR_COUNT:
*(DWORD*)buff = SDCardInfo.CardCapacity/512;
res = RES_OK;
break;
default:
res = RES_PARERR;
break;
}
}else if(pdrv==EX_FLASH) //外部FLASH
{
switch(cmd)
{
case CTRL_SYNC:
res = RES_OK;
break;
case GET_SECTOR_SIZE:
*(WORD*)buff = FLASH_SECTOR_SIZE;
res = RES_OK;
break;
case GET_BLOCK_SIZE:
*(WORD*)buff = FLASH_BLOCK_SIZE;
res = RES_OK;
break;
case GET_SECTOR_COUNT:
*(DWORD*)buff = FLASH_SECTOR_COUNT;
res = RES_OK;
break;
default:
res = RES_PARERR;
break;
}
}else res=RES_ERROR;//其他的不支持
return res;
}
#endif
2.3.6 get_fattime函数
- 函数功能:获取当前时间。
- 返回值:返回以双字封装的当前时间,具体格式这里不再详细描述了。
- 注意事项:get_fattime函数必须返回一个合法的时间,如果系统不支持实时时钟,可以返回0。
通过上述步骤,我们就完成了对于FATFS的移植,FATFS 提供了很多 API 函数,这些函数 FATFS 的自带介绍文件里面都有详细的介绍。这里需要注意的是,在使用FATFS的时候,必须先通过f_mount函数注册一个工作区,才能开始后续 API 的使用。