1. 簡介
FatFS是一個適用於小型嵌入式系統的通用的FAT/exFAT文件系統模塊。FatFS是使用ANSI C(C89)進行編寫的,並且分出了磁盤I/O層,因此它是獨立於平臺的。不僅僅可以用於各種嵌入式平臺,同樣臺用於Linux、android、MacOS甚至windows平臺。
主要代碼文件:
- ff.h和ff.c是文件系統相關的代碼。
- diskio.h和diskio.c是磁盤I/O操作接口代碼。
- ffconf.h是文件系統版本配置代碼,通過配置來開啓關閉功能,可以根據需要提供更小的代碼空間佔用。
2. 用法
本文的內容及示例均是基於ff1.4版本進行的,並且在Windows平臺上,基於虛擬磁盤和真實U盤進行測試和驗證的。
2.1. 配置信息
以下設置爲示例使用設置,爲了更方便測試,沒有啓用Unicode及多分區功能。
- #define FF_CODE_PAGE 936 // 如果想支持中文使用此配置
- #define FF_LFN_UNICODE 0 // 配置是否使用unicode,0表示使用ANSI
- #define FF_MULTI_PARTITION 0 // 是否啓用多分區,默認不啓用
2.2. 磁盤操作接口
// 此函數主要完成磁盤設備的打開(虛擬磁盤文件的創建打開)
int assign_drives();
// 此函數返回磁盤是否寫保護狀態
DSTATUS disk_status (BYTE pdrv);
// 此函數的作用是完成獲取磁盤的容量、扇區大小(虛擬磁盤直接寫死即可)
DSTATUS disk_initialize (BYTE pdrv);
// 此函數主要負責讀取磁盤指定位置指定大小的內容
DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count);
// 此函數主要負責往磁盤指定位置寫入指定大小的內容
DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count);
// 此函數主要根據cmd來返回指定內容,如扇區大小、容量、對齊扇區大小
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
2.2.1. 真實磁盤操作接口實現
/*-----------------------------------------------------------------------*/
/* Initialize Disk Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber */
)
{
DSTATUS sta;
if (WaitForSingleObject(hMutex, 5000) != WAIT_OBJECT_0)
{
return STA_NOINIT;
}
get_status(pdrv);
sta = Stat[pdrv].status;
ReleaseMutex(hMutex);
return sta;
}
/*-----------------------------------------------------------------------*/
/* Get Disk Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber (0) */
)
{
DSTATUS sta;
sta = Stat[pdrv].status;
return sta;
}
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber (0) */
BYTE *buff, /* Pointer to the data buffer to store read data */
LBA_t sector, /* Start sector number (LBA) */
UINT count /* Number of sectors to read */
)
{
DWORD nc = 0, rnc;
DSTATUS res;
UINT uPartIdx = 0;
UINT uPartCnt = (count+127) / 128;
UINT uBuffSct = 128;
if (Stat[pdrv].status & STA_NOINIT || WaitForSingleObject(hMutex, 3000) != WAIT_OBJECT_0)
{
return RES_NOTRDY;
}
nc = (DWORD)count * Stat[pdrv].sz_sector;
memcpy(Buffer, buff, nc);
for (; uPartIdx < uPartCnt; uPartIdx++)
{
// 如果是最後1份,且是不完整的
if (uPartIdx == (count / 128))
{
uBuffSct = count % 128;
}
if (!LogReadFlash(Stat[pdrv].h_drive, sector, uBuffSct, Buffer, 512))
{
res = RES_ERROR;
}
else
{
memcpy(buff + uPartIdx*128*512, Buffer, nc);
res = RES_OK;
}
sector += uBuffSct;
}
ReleaseMutex(hMutex);
return res;
}
/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber (0) */
const BYTE *buff, /* Pointer to the data to be written */
LBA_t sector, /* Start sector number (LBA) */
UINT count /* Number of sectors to write */
)
{
DWORD nc = 0, rnc;
LARGE_INTEGER ofs;
DRESULT res;
if (Stat[pdrv].status & STA_NOINIT || WaitForSingleObject(hMutex, 3000) != WAIT_OBJECT_0)
{
return RES_NOTRDY;
}
res = RES_OK;
if (Stat[pdrv].status & STA_PROTECT)
{
res = RES_WRPRT;
}
else
{
nc = (DWORD)count * Stat[pdrv].sz_sector;
if (nc > BUFSIZE) res = RES_PARERR;
}
/* Physical drives */
if (pdrv >= MIN_READ_ONLY && pdrv <= MAX_READ_ONLY)
res = RES_WRPRT;
if (res == RES_OK)
{
UINT uPartIdx = 0;
UINT uPartCnt = (count+127) / 128;
UINT uBuffSct = 128;
memcpy(Buffer, buff, nc);
for (; uPartIdx < uPartCnt; uPartIdx++)
{
// 如果是最後1份,且是不完整的
if (uPartIdx == (count / 128))
{
uBuffSct = count % 128;
}
if (!LogWriteFlash(Stat[pdrv].h_drive, sector, uBuffSct, Buffer, 512))
{
res = RES_ERROR;
}
sector += uBuffSct;
}
}
ReleaseMutex(hMutex);
return res;
}
/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0) */
BYTE ctrl, /* Control code */
void *buff /* Buffer to send/receive data */
)
{
DRESULT res;
if ((Stat[pdrv].status & STA_NOINIT))
{
return RES_NOTRDY;
}
res = RES_PARERR;
switch (ctrl)
{
case CTRL_SYNC: /* Nothing to do */
res = RES_OK;
break;
case GET_SECTOR_COUNT: /* Get number of sectors on the drive */
*(LBA_t*)buff = Stat[pdrv].n_sectors;
res = RES_OK;
break;
case GET_SECTOR_SIZE: /* Get size of sector for generic read/write */
*(WORD*)buff = Stat[pdrv].sz_sector;
res = RES_OK;
break;
case GET_BLOCK_SIZE: /* Get internal block size in unit of sector */
*(DWORD*)buff = SZ_BLOCK;
res = RES_OK;
break;
case 200: /* Load disk image file to the RAM disk (drive 0) */
{
HANDLE h;
DWORD br;
if (pdrv == 0)
{
h = CreateFileW(buff, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0);
if (h != INVALID_HANDLE_VALUE)
{
if (ReadFile(h, RamDisk, SZ_RAMDISK * 1024 * 1024, &br, 0))
{
res = RES_OK;
}
CloseHandle(h);
}
}
}
break;
}
return res;
}
2.2.2. 虛擬磁盤操作接口實現
int get_status (
BYTE pdrv
)
{
volatile STAT *stat = &Stat[pdrv];
/* Get drive size */
stat->status = STA_NOINIT;
stat->sz_sector = 512;
stat->n_sectors = 15761504;
if (stat->sz_sector < FF_MIN_SS || stat->sz_sector > FF_MAX_SS)
return 0;
/* Get write protect status */
stat->status = 0;
if (pdrv >= MIN_READ_ONLY && pdrv <= MAX_READ_ONLY) stat->status = STA_PROTECT;
return 1;
}
/*--------------------------------------------------------------------------
Public Functions
---------------------------------------------------------------------------*/
#define FILENAME "e:\\Virtual.img"
/*-----------------------------------------------------------------------*/
/* Initialize Windows disk accesss layer */
/*-----------------------------------------------------------------------*/
int assign_drives (void)
{
BYTE pdrv = 0;
HANDLE h;
DWORD dwRet = 0;
OSVERSIONINFO vinfo = { sizeof (OSVERSIONINFO) };
hMutex = CreateMutex(0, 0, 0);
if (hMutex == INVALID_HANDLE_VALUE)
return 0;
Buffer = VirtualAlloc(0, BUFSIZE, MEM_COMMIT, PAGE_READWRITE);
if (!Buffer) return 0;
RamDisk = VirtualAlloc(0, SZ_RAMDISK * 0x100000, MEM_COMMIT, PAGE_READWRITE);
if (!RamDisk)
return 0;
Stat[0].h_drive = fopen(FILENAME, "w+");
if (!Stat[0].h_drive)
{
printf(__FILE__":%d\n", __LINE__);
printf(FILENAME" open err!\n");
return RES_NOTRDY;
}
wprintf(L"PD#%u <== %s", 0, "virtual disk");
if (get_status(0))
{
wprintf(L" (%uMB, %u bytes * %I64u sectors)\n", (UINT)((LONGLONG)Stat[pdrv].sz_sector * Stat[pdrv].n_sectors / 1024 / 1024), Stat[pdrv].sz_sector, (QWORD)Stat[pdrv].n_sectors);
}
else
{
wprintf(L" (Not Ready)\n");
}
Drives = 0;
return 0;
}
/*-----------------------------------------------------------------------*/
/* Initialize Disk Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber */
)
{
DSTATUS sta;
if (WaitForSingleObject(hMutex, 5000) != WAIT_OBJECT_0)
{
return STA_NOINIT;
}
get_status(pdrv);
sta = Stat[pdrv].status;
ReleaseMutex(hMutex);
return sta;
}
/*-----------------------------------------------------------------------*/
/* Get Disk Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber (0) */
)
{
DSTATUS sta;
sta = Stat[pdrv].status;
return sta;
}
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber (0) */
BYTE *buff, /* Pointer to the data buffer to store read data */
LBA_t sector, /* Start sector number (LBA) */
UINT count /* Number of sectors to read */
)
{
DWORD nc, rnc;
LARGE_INTEGER ofs;
DSTATUS res = 0;
DWORD ulSeek = 0;
unsigned long nRead = 0;
int nSeekRes = 0;
ulSeek = sector * 512;
nSeekRes = fseek(Stat[pdrv].h_drive, ulSeek, SEEK_SET);
nRead = fread(buff, 512, count, Stat[pdrv].h_drive);
if (nSeekRes || nRead == 0) {
printf(__FILE__":%d\n", __LINE__);
printf("read disk err!\n");
return RES_ERROR;
}
return res;
}
/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber (0) */
const BYTE *buff, /* Pointer to the data to be written */
LBA_t sector, /* Start sector number (LBA) */
UINT count /* Number of sectors to write */
)
{
DWORD nc = 0, rnc;
LARGE_INTEGER ofs;
DRESULT res;
DWORD ulSeek = 0;
unsigned long nRead = 0;
int nSeekRes = 0;
ulSeek = sector * 512;
res = RES_OK;
if (Stat[pdrv].status & STA_PROTECT)
{
res = RES_WRPRT;
}
else
{
nc = (DWORD)count * Stat[pdrv].sz_sector;
if (nc > BUFSIZE) res = RES_PARERR;
}
nSeekRes = fseek(Stat[pdrv].h_drive, ulSeek, SEEK_SET);
nRead = fwrite(buff, 512, count, Stat[pdrv].h_drive);
if (nSeekRes || nRead == 0)
{
printf(__FILE__":%d\n", __LINE__);
printf("read disk err!\n");
return RES_ERROR;
}
return res;
}
/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0) */
BYTE ctrl, /* Control code */
void *buff /* Buffer to send/receive data */
)
{
DRESULT res;
res = RES_PARERR;
switch (ctrl)
{
case CTRL_SYNC: /* Nothing to do */
res = RES_OK;
break;
case GET_SECTOR_COUNT: /* Get number of sectors on the drive */
*(LBA_t*)buff = Stat[pdrv].n_sectors;
res = RES_OK;
break;
case GET_SECTOR_SIZE: /* Get size of sector for generic read/write */
*(WORD*)buff = Stat[pdrv].sz_sector;
res = RES_OK;
break;
case GET_BLOCK_SIZE: /* Get internal block size in unit of sector */
*(DWORD*)buff = SZ_BLOCK;
res = RES_OK;
break;
case 200: /* Load disk image file to the RAM disk (drive 0) */
{
HANDLE h;
DWORD br;
if (pdrv == 0)
{
h = CreateFileW(buff, GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0);
if (h != INVALID_HANDLE_VALUE)
{
if (ReadFile(h, RamDisk, SZ_RAMDISK * 1024 * 1024, &br, 0))
{
res = RES_OK;
}
CloseHandle(h);
}
}
}
break;
}
return res;
}
2.3. FatFS常用接口
FatFS應用接口,主要分四大類:
2.3.1. 文件操作接口
- f_open -創建或打開文件,可設置不同讀寫屬性。
- f_close - 關閉打開的文件。- f_read - 讀取文件。
- f_write - 寫文件。
- f_lseek - 移動當前文件操作指針到指定位置。
- f_sync - 將緩存立即寫入設備,需要配置CTRL_SYNC控制碼。
- - f_forward - 帶回調函數的讀取文件內容接口。
- f_tell - 獲取當前文件指針。
- f_eof - 測試是否到文件末尾。
- f_size - 獲取文件大小。
- f_error - 測試是否出現錯誤。
2.3.2. 目錄操作接口
- f_opendir - 打開指定目錄。
- f_closedir - 關閉指定目錄。
- f_readdir - 讀取目錄的內容,可以判斷是否爲目錄以及相關屬性等。
- f_findfirst - 開始遍歷目錄,目錄可以使用通配符描述。
- f_findnext - 讀取下一個目標。
2.3.3. 文件和目錄管理接口
- f_stat - 檢測文件或子目錄是否存在。
- f_unlink - 刪除文件或子目錄。
- f_rename - 重命名或移動文件或子目錄。
- f_chmod - 改變文件或子目錄屬性。
- f_utime - 改變文件或子目錄時間戳。
- f_mkdir - 創建子目錄。
- f_chdir - 改變當前目錄。
- f_chdrive - 改變當前驅動器號。
- f_getcwd - 獲取當前驅動器號。
2.3.4. 卷管理和系統配置
- f_mount - 註冊和反註冊指定卷的工作區,其作用主要是完成卷(文件系統相關參數)和指定磁盤的對應關係。FatFS默認是使用磁盤號與文件系統相對應,也可以使用自定義卷名與文件系統相關聯。另外此函數還會調用磁盤初始化接口來完成磁盤的打開操作。
- f_mkfs - 對指定的卷(根據磁盤名或卷名)來進行指定類型的格式化操作。
- f_fdisk - 對指定的物理設備創建多個分區。
- f_getfree - 獲取指定卷的空閒空間。
- f_getlabel - 獲取卷標。
- f_setlabel - 設備卷票。
- f_setcp - 設置活動代碼頁。
3. 示例
示例完全基於ff1.4版本,只修改磁盤I/O接口,完全不修改ff.c代碼。
3.1. 格式化
- 初始化
調用assign_drives函數完成初始化,即打開指定設備(物理設備或邏輯設備)。 - 掛載
f_mount用來掛載,其作用主要是完成卷(文件系統相關參數)和指定磁盤的對應關係。 - 設置參數
n_fat默認爲1,Windows下通用設置爲2。align對齊默認是256。m_root默認爲0。au_size默認根據容量計算。 - 格式化
FatFs有一個全局變量FatFs[FF_VOLUMES],FF_VOLUMES默認爲8。所以FatFS默認只能掛載8個卷。如果想同時支持更多盤,FF_VOLUMES宏需要相應的修改。 - 代碼
// 3就是設備號,在FatFS中也被稱爲邏輯驅動器號,默認不填寫則爲0
TCHAR filePath[] = "3:\\hello.txt";
MKFS_PARM opt = {0};
FATFS fsEx = {0};
FRESULT fRet = FR_OK;
assign_drives('j', 3);
f_mount(&fsEx, "3:", 0);
opt.fmt = FM_EXFAT;
opt.n_fat = 2;
fRet = f_mkfs("3:", &opt, workBuff, sizeof(workBuff));
3.2. 讀寫文件
- 初始化
調用assign_drives函數完成初始化,即打開指定設備(物理設備或邏輯設備)。 - 掛載
f_mount用來掛載,其作用主要是完成卷(文件系統相關參數)和指定磁盤的對應關係。 - 打開文件
打開指定文件,返回一個文件屬性列表變量用來寫讀關閉文件。 - 寫文件
和Windows寫文件差不多,詳細示例 - 讀文件
和Windows讀文件操作類似,詳細示例 - 關閉文件
關閉文件的一個重要作用是保證將緩存寫入設備(直接設備或虛擬設備) - 示例
FIL fil = {0};
FRESULT res = FR_OK;
UINT bw = 0;
BYTE buff[1024] = {0};
res = f_open(&fil, pFilePath, FA_CREATE_NEW | FA_WRITE);
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
f_write(&fil, "Hello, World!\n", 15, &bw);
if (bw != 15)
{
printf("f_write hello.txt err\n");
f_close(&fil);
return 1;
}
f_close(&fil);
res = f_open(&fil, pFilePath, FA_READ | FA_OPEN_EXISTING);
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
memset(buff, 0, sizeof(buff));
res = f_read(&fil, buff, 15, &bw);
if (res)
{
printf("f_read err\n");
f_close(&fil);
return res;
}
printf("buff: %s", buff);
3.3. 讀寫目錄
- 初始化
調用assign_drives函數完成初始化,即打開指定設備(物理設備或邏輯設備)。 - 掛載
f_mount用來掛載,其作用主要是完成卷(文件系統相關參數)和指定磁盤的對應關係。 - 創建目錄
f_mkdir的參數和Windows類似,只需要目錄名,默認當前目錄。 - 打開目錄
f_opendir打開指定目錄,返回一個目錄標識供讀目錄使用。 - 讀取目錄
讀取目錄信息,包括是否目錄、各種目錄屬性。 - 關閉目錄
- 創建打開示例
UINT bw = 0;
FIL fil = {0};
TCHAR buff[1024] = {0};
DIR dir = {0};
FRESULT res = FR_OK;
res = f_mkdir("3:\\ABC");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_opendir(&dir, "3:\\ABC");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_mkdir("3:\\ABC\\A");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_opendir(&dir, "3:\\ABC\\A");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_mkdir("3:\\ABC\\B");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_opendir(&dir, "3:\\ABC\\B");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_mkdir("3:\\ABC\\B\\C");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
res = f_opendir(&dir, "3:\\ABC\\B\\C");
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
f_closedir(&dir);
res = f_open(&fil, "3:/ABC/B/a.txt", FA_CREATE_NEW | FA_WRITE);
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
f_write(&fil, "Mike say:Hello, World!\n", 24, &bw);
if (bw != 24)
{
printf("f_write hello.txt err\n");
f_close(&fil);
return 1;
}
f_close(&fil);
res = f_open(&fil, "3:/ABC/B/a.txt", FA_READ | FA_OPEN_EXISTING);
if (res)
{
printf("f_open hello.txt err\n");
return res;
}
memset(buff, 0, sizeof(buff));
res = f_read(&fil, buff, 24, &bw);
if (res)
{
printf("f_read err\n");
f_close(&fil);
return res;
}
printf("buff: %s", buff);
f_close(&fil);
- 遍歷目錄示例
FRESULT scan_files (
CHAR* path /* Pointer to the path name working buffer */
)
{
DIR dir;
FRESULT res;
int i;
if ((res = f_opendir(&dir, path)) == FR_OK)
{
i = strlen(path);
while (((res = f_readdir(&dir, &Finfo)) == FR_OK) && Finfo.fname[0])
{
if (Finfo.fattrib & AM_DIR)
{
AccDirs++;
*(path+i) = L'/';
strcpy(path+i+1, Finfo.fname);
printf("%s\n", path);
res = scan_files(path);
*(path+i) = L'\0';
if (res != FR_OK)
break;
}
else
{
AccFiles++;
AccSize += Finfo.fsize;
printf("%s\n", Finfo.fname);
}
}
f_closedir(&dir);
}
return res;
}
3.4. 多分區
多分區需要啓用宏FF_MULTI_PARTITION。
int TestMultiParition()
{
#if FF_MULTI_PARTITION
FRESULT fRet = FR_OK;
MKFS_PARM opt = {0};
LBA_t plist[] = {3000*2048, 100}; /* Divide the drive into two partitions */
/* {0x10000000, 100}; 256M sectors for 1st partition and left all for 2nd partition */
/* {20, 20, 20, 0}; 20% for 3 partitions each and remaing space is left not allocated */
f_fdisk(0, plist, workBuff); /* Divide physical drive 0 */
opt.fmt = FM_FAT32;
opt.n_fat = 2;
fRet = f_mkfs("0:", &opt, workBuff, sizeof(workBuff)); /* Create FAT volume on the logical drive 0 */
fRet = f_mkfs("1:", &opt, workBuff, sizeof(workBuff)); /* Create FAT volume on the logical drive 1 */
#endif
return 0;
}
4. 查看虛擬盤信息
我們可以通過虛擬盤將文件系統及相關信息寫入到Windows的文件中,用文件來模擬FatFS的移植。創建format.img文件,將diskio的相關接口均實現到此文件中。並且通過DiskGunius工具提供的打開虛擬磁盤文件,可以查看相關信息。DiskGunius支持FAT32和exFAT文件系統。
5. 注意
- 多盤需要考慮修改FatFs數組的大小。
- 讀寫文件時,需要考慮內存使用情況。