1、文件系統
文件系統負責管理和存儲文件信息的軟件機構,在磁盤上組織文件的方法。
對於 SPI Flash 芯片或者 SD 卡之類的大容量設備,我們需要一種高效的方式來管理它的存儲內容。這些管理方式即為文件系統,它是為了存儲和管理數據,而在存儲介質建立的一種組織結構,這些結構包括操作系統引導區、目錄和文件。 常見的 windows 下的文件系統格式包括 FAT32、 NTFS、 exFAT。 在使用文件系統前,要先對存儲介質進行格式化。格式化先擦除原來內容,在存儲介質上新建一個文件分配表和目錄。這樣,文件系統就可以記錄數據存放的物理地址,剩余空間。
使用文件系統時, 數據都以文件的形式存儲。寫入新文件時,先在目錄中創建一個文件索引,它指示了文件存放的物理地址,再把數據存儲到該地址中。當需要讀取數據時,可以從目錄中找到該文件的索引,進而在相應的地址中讀取出數據。具體還涉及到邏輯地址、簇大小、不連續存儲等一系列輔助結構或處理過程。
文件系統的存在使我們在存取數據時,不再是簡單地向某物理地址直接讀寫,而是要遵循它的讀寫格式。如經過邏輯轉換,一個完整的文件可能被分開成多段存儲到不連續的物理地址,使用目錄或鏈表的方式來獲知下一段的位置。SPI Flash 芯片驅動只完成了向物理地址寫入數據的工作,而根據文件系統格式的邏輯轉換部分則需要額外的代碼來完成。實質上,這個邏輯轉換部分可以理解為當我們需要寫入一段數據時,由它來求解向什么物理地址寫入數據、以什么格式寫入及寫入一些原始數據以外的信息(如目錄)。這個邏輯轉換部分代碼我們也習慣稱之為文件系統。
2、FatFs 文件系統簡介
上面提到的邏輯轉換部分代碼(文件系統)即為FatFs 文件系統要點,文件系統龐大而復雜,它需要根據應用的文件系統格式而編寫,而且一般與驅動層分離開來,很方便移植,所以工程應用中一般是移植現成的文件系統源碼。
常用的文件系統:FAT/FATFS(小型嵌入式系統)、 NTFS (WINDOWS)、CDFS(光盤)、exFAT(內存)。
FatFs 是面向小型嵌入式系統的一種通用的 FAT 文件系統。它完全是由 AISI C 語言編寫並且完全獨立於底層的 I/O 介質。因此它可以很容易地不加修改地移植到其他的處理器當中,如 8051、 PIC、 AVR、 SH、 Z80、 H8、 ARM 等。 FatFs 支持 FAT12、 FAT16、FAT32 等格式利用文件系統的各種函數, 所以可以利用FatFs對 SPI Flash 芯片以“文件”格式進行讀寫操作。
FATFS優點:免費開源,專門為小型嵌入式系統設計,c編寫,支持FAT12, FAT16 與 FAT32,支持多種存儲媒介,有獨立的緩沖區,可對多個文件進行讀寫,可裁剪的文件系統(極為重要)。
FATFS的特點:
- Windows兼容的FAT文件系統(支持FAT12/FAT16/FAT32)與平台無關,移植簡單
- 代碼量少、效率高
- 多種配置選項
- 支持多卷(物理驅動器或分區, 最多10個卷)
- 多個ANSI/OEM代碼頁包括DBCS
- 支持長文件名、 ANSI/OEM 或Unicode
- 支持RTOS
- 支持多種扇區 大小
- 只讀、最小化的API和I/O緩沖區等
FatFs 文件系統的源碼可以從 fatfs 官網下載:http://elm-chan.org/fsw/ff/00index_e.html
3、FatFs 的目錄結構
在移植 FatFs 文件系統到開發板之前,我們先要到 FatFs 的官網獲取源碼, 最新版本為R0.11a,官網有對 FatFs 做詳細的介紹,有興趣可以了解。
解壓之后可看到里面有 doc 和src 這兩個文件夾。 doc 文件夾里面是一些使用幫助文檔; src 才是 FatFs 文件系統的源碼。
3.1、doc文件夾
打開 doc 文件夾,可看到如下圖的文件目錄:
其中 en 和 ja 這兩個文件夾里面是編譯好的 html 文檔,講的是 FATFS 里面各個函數的使用方法,這些函數都是封裝得非常好的函數,利用這些函數我們就可以操作 SPI Flash 芯片。這兩個文件夾的唯一區別就是 en 文件夾下的文檔是英文的, ja 文件夾下的是日文的。 img 文件夾包含 en 和 ja 文件夾下文件需要用到的圖片,還有四個名為 app.c 文件,內容都是 FatFs 具體應用例程。 00index_e.html 和00index_j.html 是一些關於 FATFS 的簡介,至於另外兩個文件可以不看。
3.2、src文件夾
打開 src 文件夾,可看到如下圖文件目錄:
option 文件夾下是一些可選的外部 c 文件,包含了多語言支持需要用到的文件和轉換函數。
00history.txt 介紹了 FatFs 的版本更新情況。
00readme.txt 說明了當前目錄下 diskio.c 、 diskio.h、 ff.c、 ff.h、 integer.h 的功能。
diskio.c 文件是 FatFs 移植最關鍵的文件,它為文件系統提供了最底層的訪問 SPI Flash芯片的方法, FatFs 有且僅有它需要用到與 SPI Flash 芯片相關的函數。
diskio.h 定義了FatFs 用到的宏,以及 diskio.c 文件內與底層硬件接口相關的函數聲明。
源碼文件功能簡介如下:
integer.h:文件中包含了一些數值類型定義。
diskio.c:包含底層存儲介質的操作函數,這些函數需要用戶自己實現,主要添加底層驅動函數。
ff.c:FatFs 核心文件,文件管理的實現方法。該文件獨立於底層介質操作文件的函數,利用這些函數實現文件的讀寫。
cc936.c:本文件在 option 目錄下,是簡體中文支持所需要添加的文件,包含了簡體中文的 GBK 和 Unicode 相互轉換功能函數。
ffconf.h:這個頭文件包含了對 FatFs 功能配置的宏定義,通過修改這些宏定義就可以裁剪 FatFs 的功能。如需要支持簡體中文,需要把 ffconf.h 中的_CODE_PAGE的宏改成 936 並把上面的 cc936.c 文件加入到工程之中。
建議閱讀這些源碼的順序為: integer.h --> diskio.c --> ff.c 。
閱讀文件系統源碼 ff.c 文件需要一定的功底,建議讀者先閱讀 FAT32 的文件格式,再去分析 ff.c 文件。若僅為使用文件系統,則只需要理解 integer.h 及 diskio.c 文件並會調用ff.c 文件中的函數就可以了。
4、FatFs 程序結構圖
用戶應用程序需要由用戶編寫,想實現什么功能就編寫什么的程序,一般我們只用到f_mount()、 f_open()、 f_write()、 f_read()就可以實現文件的讀寫操作。
FatFs 組件是 FatFs 的主體,文件都在源碼 src 文件夾中,其中 ff.c、 ff.h、 integer.h 以及diskio.h 四個文件我們不需要改動,只需要修改 ffconf.h 和 diskio.c 兩個文件。
底層設備輸入輸出要求實現存儲設備的讀寫操作函數、存儲設備信息獲取函數等等。
5、 FatFs 底層設備驅動函數
FatFs 文件系統與底層介質的驅動分離開來,對底層介質的操作都要交給用戶去實現,它僅僅是提供了一個函數接口而已。下表 為 FatFs 移植時用戶必須支持的函數。通過表25-1 我們可以清晰知道很多函數是在一定條件下才需要添加的,只有前三個函數是必須添加的。我們完全可以根據實際需求選擇實現用到的函數。
前三個函數是實現讀文件最基本需求。接下來三個函數是實現創建文件、修改文件需要的。為實現格式化功能,需要在 disk_ioctl 添加兩個獲取物理設備信息選項。我們一般只有實現前面六個函數就可以了,已經足夠滿足大部分功能。
為支持簡體中文長文件名稱需要添加 ff_convert 和 ff_wtoupper 函數,實際這兩個已經在 cc936.c 文件中實現了,我們只要直接把 cc936.c 文件添加到工程中就可以了。
后面六個函數一般都不用。如真有需要可以參考 syscall.c 文件(src\option 文件夾內)。
底層設備驅動函數是存放在 diskio.c 文件,我們的目的就是把 diskio.c 中的函數接口與SPI Flash 芯片驅動連接起來。總共有五個函數,分別為設備狀態獲取(disk_status)、設備初始化(disk_initialize)、扇區讀取(disk_read)、扇區寫入(disk_write)、其他控制(disk_ioctl)。
6、 FatFs測試
6.1、Flash
FatFs 屬於軟件組件,不需要附帶其他硬件電路。我們使用 SPI Flash 芯片作為物理存儲設備,其硬件電路在上一章已經做了分析,這里就直接使用。
FatFs 移植步驟
將 FatFs 組件文件添加到工程中,需要添加 ff.c、 diskio.c 和cc936.c 三個文件,FatFs_test.c為用戶測試程序。
如果現在編譯工程,可以發現有兩個錯誤,一個是來自 diskio.c 文件,提示有一些頭文件沒找, diskio.c 文件內容是與底層設備輸入輸出接口函數文件,不同硬件設計驅動就不同,需要的文件也不同;另外一個錯誤來自 cc936.c 文件,提示該文件不是工程所必需的,這是因為 FatFs 默認使用日語,我們想要支持簡體中文需要修改 FatFs 的配置,即修改 ffconf.h 文件。至此,將 FatFs 添加到工程的框架已經操作完成,接下來要做的就是修改文件和 ffconf.h 文件。
(1)、ffconf.h文件配置
ffconf.h 文件是 FatFs 功能配置文件,我們可以對文件內容進行修改,使得 FatFs 更符合我們的要求。 ffconf.h 對每個配置選項都做了詳細的使用情況說明。
下面只列出修改的配置,其他配置采用默認即可:
//ffconf.h #define _USE_MKFS 1 #define _CODE_PAGE 936 #define _USE_LFN 2 #define _VOLUMES 2 #define _MIN_SS 512 #define _MAX_SS 4096 _USE_MKFS:格式化功能選擇,為使用 FatFs 格式化功能,需要把它設置為 1。 _CODE_PAGE:語言功能選擇,並要求把相關語言文件添加到工程宏。為支持簡體中文文件名需要使用“936”。 _USE_LFN:長文件名支持,默認不支持長文件名,這里配置為 2,支持長文件名,並指定使用棧空間為緩沖區。 _VOLUMES:指定物理設備數量,這里設置為 2,包括預留 SD 卡和 SPI Flash 芯片。 _MIN_SS、_MAX_SS:指定扇區大小的最小值和最大值。 SD 卡扇區大小一般都為 512 字節, SPI Flash 芯片扇區大小一般設置為 4096 字節,所以需要把_MAX_SS 改為 4096。
(2)、底層設備驅動函數
/*-----------------------------------------------------------------------*/ /* Low level disk I/O module skeleton for FatFs (C)ChaN, 2014 */ /*-----------------------------------------------------------------------*/ /* If a working storage control module is available, it should be */ /* attached to the FatFs via a glue function rather than modifying it. */ /* This is an example of glue functions to attach various exsisting */ /* storage control modules to the FatFs module with a defined API. */ /*-----------------------------------------------------------------------*/ #include "diskio.h" /* FatFs lower layer API */ #include "ff.h" #include "main.h" //#include "usbdisk.h" /* Example: Header file of existing USB MSD control module */ //#include "atadrive.h" /* Example: Header file of existing ATA harddisk control module */ //#include "sdcard.h" /* Example: Header file of existing MMC/SDC contorl module */ /*為每個設備定義一個物理編號*/ /* Definitions of physical drive number for each drive 每個驅動器的物理驅動器號的定義*/ #define ATA 0 /* Example: Map ATA harddisk to physical drive 0 將一個硬盤映射到物理驅動器0*/ #define MMC 1 /* Example: Map MMC card to physical drive 1 將MMC卡映射到物理驅動器1*/ #define USB 2 /* Example: Map USB to physical drive 2 將USB映射到物理驅動器2*/ #define SD 3 /* Example: Map SD to physical drive 3 將SD卡映射到物理驅動器3*/ #define Flash 4 /* Example: Map FLASH to physical drive 4 將Flash映射到物理驅動器4*/ /*-----------------------------------------------------------------------*/ /* Get Drive Status 獲取設備狀態 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv :Physical drive nmuber to identify the drive 設備物理編號,通過物理驅動器編號來識別驅動器*/ DSTATUS disk_status (BYTE pdrv) { DSTATUS status = STA_NOINIT & 0x00; //int result; switch(pdrv) { case ATA: //result = ATA_disk_status();//獲取設備狀態 break; case MMC: //result = MMC_disk_status(); break; case USB: //result = USB_disk_status(); break; case SD: //result = SD_disk_status(); break; case Flash : //result = FLASH_disk_status(); //SPI Flash狀態檢測:讀取SPI Flash 設備ID if(FLASH_ID == FLASH_Read_Jedec_ID()) //設備ID讀取結果正確 { status = STA_NOINIT & 0x00; } else //設備ID讀取結果錯誤 { status = STA_NOINIT; } break; default: status = STA_NOINIT; break; } return status; } /*-----------------------------------------------------------------------*/ /* Inidialize a Drive 設備初始化 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv :Physical drive nmuber to identify the drive 設備物理編號,通過物理驅動器編號來識別驅動器*/ DSTATUS disk_initialize(BYTE pdrv) { DSTATUS status = STA_NOINIT & 0x00; //int result; switch(pdrv) { case ATA: //result = ATA_disk_initialize();//設備初始化 break; case MMC: //result = MMC_disk_initialize(); break; case USB: //result = USB_disk_initialize(); break; case SD: //result = SD_disk_initialize(); break; case Flash: //result = FLASH_disk_initialize(); SPI_1_Config_Init(); int i=500; while(--i); Flash_PowerOn_Mode(); //喚醒Flash status = disk_status(Flash); //獲取SPI Flash芯片狀態 break; default: status = STA_NOINIT; break; } return status; } /*-----------------------------------------------------------------------*/ /* Read Sector(s) 讀扇區:讀取扇區內容到指定存儲區 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv:Physical drive nmuber to identify the drive 設備物理編號(0...)*/ /*BYTE *buff:Data buffer to store read data 數據緩存區*/ /*DWORD sector:Sector address in LBA 扇區首地址*/ /*UINT count:Number of sectors to read 扇區個數(1...128)*/ DRESULT disk_read(BYTE pdrv,BYTE *buff, DWORD sector,UINT count) { DRESULT status = STA_NOINIT & 0x00; //int result; switch (pdrv) { case ATA: //result = ATA_disk_read(buff, sector, count);//讀取扇區內容到指定存儲區 break; case MMC: //result = MMC_disk_read(buff, sector, count); break; case USB: //result = USB_disk_read(buff, sector, count); break; case SD: //result = DS_disk_read(buff, sector, count); break; case Flash: //result = FLASH_disk_read(buff, sector, count); /*開發板使用的 SPI Flash 芯片型號為 W25Q128FV,每個扇區大小為 4096 個字節(4KB),總共有 16M 字節空間,為兼 容后面實驗程序,我們只將后部分 10MB 空間分配給 FatFs 使用,前部分 6MB 空間用於其他實驗需要,即 FatFs 是從 6MB 空間開始,為實現這個效果需要將所有的讀寫地址都偏移 1536 個扇區空間。 對於 SPI Flash 芯片,主要是使用 SPI_FLASH_BufferRead()實現在指定地址讀取指定長度的數據,它接收三個參數, 第一個參數為指定數據存放地址指針。第二個參數為指定數據讀取地址,這里使用左移運算符,左移12位實際是乘以4096, 這與每個扇區大小是息息相關的。第三個參數為讀取數據個數,也是需要使用左移運算符。*/ sector = sector + 1536; //扇區偏移6MB,外部Flash文件系統空間放在SPI Flash后面10MB空間 FLASH_Read_Buffer(sector <<12, count<<12,buff); status = RES_OK; break; default: status = RES_PARERR; break; } return status; } /*-----------------------------------------------------------------------*/ /* Write Sector(s) 寫扇區:將數據寫入指定扇區空間上 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv: Physical drive nmuber to identify the drive 設備物理編號(0...)*/ /*const BYTE *buff: Data to be written 寫入數據的緩存區*/ /*DWORD sector: Sector address in LBA 扇區首地址*/ /*UINT count: Number of sectors to write 扇區個數(1...128)*/ #if _USE_WRITE DRESULT disk_write(BYTE pdrv,const BYTE *buff,DWORD sector,UINT count) { DRESULT status = STA_NOINIT & 0x00; uint32_t write_addr; //int result; if(!count) //扇區個數為0 { status = STA_NOINIT; return status; } switch (pdrv) { case ATA: //result = ATA_disk_write(buff, sector, count);//將數據寫入指定扇區空間上 break; case MMC: //result = MMC_disk_write(buff, sector, count); break; case USB: //result = USB_disk_write(buff, sector, count); break; case SD: //result = SD_disk_write(buff, sector, count); break; case Flash: //result = FLASH_disk_write(buff, sector, count); /*扇區偏移6MB,外部Flash文件系統空間放在Flash后面10MB空間 */ sector = sector + 1536; write_addr = sector<<12; FLASH_Erase_Sector(write_addr); //寫入數據前先擦除 FLASH_Write_Buffer(write_addr,count<<12,(uint8_t *)buff); status = RES_OK; break; default: status = STA_NOINIT; break; } return status; } #endif /*-----------------------------------------------------------------------*/ /* Miscellaneous Functions 其他控制 */ /*-----------------------------------------------------------------------*/ /*BYTE pdrv:Physical drive nmuber (0..) 設備物理編號 */ /*BYTE cmd:Control code 控制指令,包括發出同步信號、獲取扇區數目、獲取扇區大小、獲取擦除塊數量等等指令*/ /*void *buff:Buffer to send/receive control data 寫入或者讀取數據地址指針*/ #if _USE_IOCTL DRESULT disk_ioctl (BYTE pdrv,BYTE cmd,void *buff) { DRESULT status = STA_NOINIT & 0x00; int result; switch (pdrv) { case ATA: break; case MMC: break; case USB: break; case SD: break; case Flash: switch (cmd) { case GET_SECTOR_COUNT: //扇區數量:2560*4096/1024/1024=10(MB) *(DWORD * )buff = 2560; break; case GET_SECTOR_SIZE : //扇區大小 *(WORD * )buff = 4096; break; case GET_BLOCK_SIZE : //同時擦除扇區個數 *(DWORD * )buff = 1; break; default: status = STA_NOINIT; break; } break; default: status = STA_NOINIT; break; } return status; } #endif /* get_fattime 函數用於獲取當前時間戳,在 ff.c 文件中被調用。 FatFs 在文件創建、被修改時會記錄時間,這里我們直接使用賦值方法 設定時間戳。為更好的記錄時間,可以使用控制器 RTC 功能,具體要求返回值格式為: bit31:25 ——從 1980 至今是多少年,范圍是 (0..127) ; bit24:21 ——月份,范圍為 (1..12) ; bit20:16 ——該月份中的第幾日,范圍為(1..31) ; bit15:11——時,范圍為 (0..23); bit10:5 ——分,范圍為 (0..59); bit4:0 ——秒/ 2,范圍為 (0..29) */ __weak DWORD get_fattime(void) { //返回當前時間戳 return ( (DWORD)(2015 - 1980) << 25) //Year 2015 | ((DWORD)1 << 21) //Month 1 | ((DWORD)1 << 16) //Mday 1 | ((DWORD)0 << 11) //Hour 0 | ((DWORD)0 << 5) //Min 0 | ((DWORD)0 >> 1); //Sec 0 }
FatFs 的第一步工作就是使用 f_mount 函數掛載工作區。 f_mount 函數有三個形參,第一個參數是指向 FATFS 變量指針,如果賦值為 NULL 可以取消物理設備掛載。第二個參 數為邏輯設備編號,使用設備根路徑表示,與物理設備編號掛鈎,在代碼中我們定義 SPI Flash 芯片物理編號為 1,所以這里使用“1:”。第三個參數可選 0 或 1, 1 表示立即掛載, 0 表示不立即掛載,延遲掛載。 f_mount 函數會返回一個 FRESULT 類型值,指示運行情況。
FRESULT f_mount ( FATFS* fs, /* Pointer to the file system object (NULL:unmount)*/ const TCHAR* path, /* Logical drive number to be mounted/unmounted */ BYTE opt /* 0:Do not mount (delayed mount), 1:Mount immediately */ )
如果 f_mount 函數返回值為 FR_NO_FILESYSTEM,說明沒有 FAT 文件系統,比如新出廠的 SPI Flash 芯片就沒有 FAT 文件系統。我們就必須對物理設備進行格式化處理。使用 f_mkfs 函數可以實現格式化操作。 f_mkfs 函數有三個形參,第一個參數為邏輯設備編號;第二參數可選 0 或者 1, 0 表示設備為一般硬盤, 1 表示設備為軟盤。第三個參數指定扇區大小,如果為 0,表示通過代碼中 disk_ioctl 函數獲取。格式化成功后需要先取消掛載原來設備,再重新掛載設備。
FRESULT f_mkfs ( const TCHAR* path, /* Logical drive number */ BYTE sfd, /* Partitioning rule 0:FDISK, 1:SFD */ UINT au /* Size of allocation unit in unit of byte or sector */ )
在設備正常掛載后,就可以進行文件讀寫操作了。使用文件之前,必須使用 f_open 函數打開文件,不再使用文件必須使用 f_close 函數關閉文件,這個跟電腦端操作文件步驟類似。 f_open 函數有三個形參,第一個參數為文件對象指針。第二參數為目標文件,包含絕對路徑的文件名稱和后綴名。第三個參數為訪問文件模式選擇,可以是打開已經存在的文件模式、讀模式、寫模式、新建模式、總是新建模式等的或運行結果。比如對於寫測試,使用 FA_CREATE_ALWAYS 和 FA_WRITE 組合模式,就是總是新建文件並進行寫模式。f_close 函數用於不再對文件進行讀寫操作關閉文件, f_close 函數只要一個形參,為文件對象指針。 f_close 函數運行可以確保緩沖區完全寫入到文件內。
FRESULT f_open ( FIL* fp, /* Pointer to the blank file object */ const TCHAR* path, /* Pointer to the file name */ BYTE mode /* Access mode and file open mode flags */ )
FRESULT f_close ( FIL *fp /* Pointer to the file object to be closed */ )
成功打開文件之后就可以使用 f_write 函數和 f_read 函數對文件進行寫操作和讀操作。這兩個函數用到的參數是一致的,只不過一個是數據寫入,一個是數據讀取。 f_write 函數第一個形參為文件對象指針,使用與 f_open 函數一致即可。第二個參數為待寫入數據的首地址,對於 f_read 函數就是用來存放讀出數據的首地址。第三個參數為寫入數據的字節數,對於 f_read 函數就是欲讀取數據的字節數。第四個參數為 32 位無符號整形指針,這里使用fnum 變量地址賦值給它,在運行讀寫操作函數后, fnum 變量指示成功讀取或者寫入的字節個數。
FRESULT f_write ( FIL* fp, /* Pointer to the file object */ const void *buff, /* Pointer to the data to be written */ UINT btw, /* Number of bytes to write */ UINT* bw /* Pointer to number of bytes written */ )
FRESULT f_read ( FIL* fp, /* Pointer to the file object */ void* buff, /* Pointer to data buffer */ UINT btr, /* Number of bytes to read */ UINT* br /* Pointer to number of bytes read */ )
最后,不再使用文件系統時,使用 f_mount 函數取消掛載。
注意:使用文件系統時,引腳初始化需要放在系統中