第25章 串行FLASH文件系統FatFs
全套200集視頻教程和1000頁PDF教程請到秉火論壇下載:www.firebbs.cn
野火視頻教程優酷觀看網址:http://i.youku.com/firege
本章參考資料:《00index_e.html》,這是FatFs官方的編譯好的HTML文檔,里面有FatFs所有函數的介紹和函數的應用示例,學習FatFs看這個官方的文檔即可。
圖 261 FatFs參考資料
25.1 文件系統
即使讀者可能不了解文件系統,讀者也一定對"文件"這個概念十分熟悉。數據在PC上是以文件的形式儲存在磁盤中的,這些數據的形式一般為ASCII碼或二進制形式。在上一章我們已經寫好了SPI Flash芯片的驅動函數,我們可以非常方便的在SPI Flash芯片上讀寫數據。如需要記錄本書的書名"零死角玩轉STM32-F429系列",可以把這些文字轉化成ASCII碼,存儲在數組中,然后調用SPI_FLASH_BufferWrite函數,把數組內容寫入到SPI Flash芯片的指定地址上,在需要的時候從該地址把數據讀取出來,再對讀出來的數據以ASCII碼的格式進行解讀。
但是,這樣直接存儲數據會帶來極大的不便,如難以記錄有效數據的位置,難以確定存儲介質的剩余空間,以及應以何種格式來解讀數據。就如同一個巨大的圖書館無人管理,雜亂無章地存放着各種書籍,難以查找所需的文檔。想象一下圖書館的采購人員購書后,把書籍往館內一扔,拍拍屁股走人,當有人來借閱某本書的時候,就不得不一本本地查找。這樣直接存儲數據的方式對於小容量的存儲介質如EEPROM還可以接受,但對於SPI Flash芯片或者SD卡之類的大容量設備,我們需要一種高效的方式來管理它的存儲內容。
這些管理方式即為文件系統,它是為了存儲和管理數據,而在存儲介質建立的一種組織結構,這些結構包括操作系統引導區、目錄和文件。常見的windows下的文件系統格式包括FAT32、NTFS、exFAT。在使用文件系統前,要先對存儲介質進行格式化。格式化先擦除原來內容,在存儲介質上新建一個文件分配表和目錄。這樣,文件系統就可以記錄數據存放的物理地址,剩余空間。
使用文件系統時,數據都以文件的形式存儲。寫入新文件時,先在目錄中創建一個文件索引,它指示了文件存放的物理地址,再把數據存儲到該地址中。當需要讀取數據時,可以從目錄中找到該文件的索引,進而在相應的地址中讀取出數據。具體還涉及到邏輯地址、簇大小、不連續存儲等一系列輔助結構或處理過程。
文件系統的存在使我們在存取數據時,不再是簡單地向某物理地址直接讀寫,而是要遵循它的讀寫格式。如經過邏輯轉換,一個完整的文件可能被分開成多段存儲到不連續的物理地址,使用目錄或鏈表的方式來獲知下一段的位置。
上一章的SPI Flash芯片驅動只完成了向物理地址寫入數據的工作,而根據文件系統格式的邏輯轉換部分則需要額外的代碼來完成。實質上,這個邏輯轉換部分可以理解為當我們需要寫入一段數據時,由它來求解向什么物理地址寫入數據、以什么格式寫入及寫入一些原始數據以外的信息(如目錄)。這個邏輯轉換部分代碼我們也習慣稱之為文件系統。
25.2 FatFs文件系統簡介
上面提到的邏輯轉換部分代碼(文件系統)即為本章的要點,文件系統龐大而復雜,它需要根據應用的文件系統格式而編寫,而且一般與驅動層分離開來,很方便移植,所以工程應用中一般是移植現成的文件系統源碼。
FatFs是面向小型嵌入式系統的一種通用的FAT文件系統。它完全是由AISI C語言編寫並且完全獨立於底層的I/O介質。因此它可以很容易地不加修改地移植到其他的處理器當中,如8051、PIC、AVR、SH、Z80、H8、ARM等。FatFs支持FAT12、FAT16、FAT32等格式,所以我們利用前面寫好的SPI Flash芯片驅動,把FatFs文件系統代碼移植到工程之中,就可以利用文件系統的各種函數,對SPI Flash芯片以"文件"格式進行讀寫操作了。
FatFs文件系統的源碼可以從fatfs官網下載:
http://elm-chan.org/fsw/ff/00index_e.html
25.2.1 FatFs的目錄結構
在移植FatFs文件系統到開發板之前,我們先要到FatFs的官網獲取源碼,最新版本為R0.11a,官網有對FatFs做詳細的介紹,有興趣可以了解。解壓之后可看到里面有 doc 和 src 這兩個文件夾,見圖 251。doc 文件夾里面是一些使用幫助文檔; src 才是FatFs文件系統的源碼。
圖 251 FatFs文件目錄
25.2.2 FatFs幫助文檔
打開 doc 文件夾,可看到如圖 252的文件目錄:
圖 252 doc文件夾的文件目錄
其中 en 和 ja 這兩個文件夾里面是編譯好的html文檔,講的是FATFS里面各個函數的使用方法,這些函數都是封裝得非常好的函數,利用這些函數我們就可以操作SPI Flash芯片。有關具體的函數我們在用到的時候再講解。這兩個文件夾的唯一區別就是 en 文件夾下的文檔是英文的,ja 文件夾下的是日文的。img文件夾包含en和ja文件夾下文件需要用到的圖片,還有四個名為app.c文件,內容都是FatFs具體應用例程。00index_e.html和00index_j.html是一些關於FATFS的簡介,至於另外兩個文件可以不看。
25.2.3 FATFS源碼
打開 src 文件夾,可看到如圖 253的文件目錄:
圖 253 src文件夾的文件目錄
option 文件夾下是一些可選的外部c文件,包含了多語言支持需要用到的文件和轉換函數。
diskio.c文件是FatFs移植最關鍵的文件,它為文件系統提供了最底層的訪問SPI Flash芯片的方法,FatFs有且僅有它需要用到與SPI Flash芯片相關的函數。diskio.h定義了FatFs用到的宏,以及diskio.c文件內與底層硬件接口相關的函數聲明。
00history.txt介紹了FatFs的版本更新情況。
00readme.txt說明了當前目錄下 diskio.c 、diskio.h、ff.c、ff.h、integer.h的功能。
src文件夾下的源碼文件功能簡介如下:
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文件中的函數就可以了。本章主要講解如何把FATFS文件系統移植到開發板上,並編寫一個簡單讀寫操作范例。
25.3 FatFs文件系統移植實驗
25.3.1 FatFs程序結構圖
移植FatFs之前我們先通過FatFs的程序結構圖了解FatFs在程序中的關系網絡,見圖 254。
圖 254 FatFs程序結構圖
用戶應用程序需要由用戶編寫,想實現什么功能就編寫什么的程序,一般我們只用到f_mount()、f_open()、f_write()、f_read()就可以實現文件的讀寫操作。
FatFs組件是FatFs的主體,文件都在源碼src文件夾中,其中ff.c、ff.h、integer.h以及diskio.h四個文件我們不需要改動,只需要修改ffconf.h和diskio.c兩個文件。
底層設備輸入輸出要求實現存儲設備的讀寫操作函數、存儲設備信息獲取函數等等。我們使用SPI Flash芯片作為物理設備,在上一章節已經編寫好了SPI Flash芯片的驅動程序,這里我們就直接使用。
25.3.2 硬件設計
FatFs屬於軟件組件,不需要附帶其他硬件電路。我們使用SPI Flash芯片作為物理存儲設備,其硬件電路在上一章已經做了分析,這里就直接使用。
25.3.3 FatFs移植步驟
上一章我們已經實現了SPI Flash芯片驅動程序,並實現了讀寫測試,為移植FatFs方便,我們直接拷貝一份工程,我們在工程基礎上添加FatFs組件,並修改main函數的用戶程序即可。
1) 先拷貝一份SPI Flash芯片測試的工程文件(整個文件夾),並修改文件夾名為"SPI—FatFs文件系統"。將FatFs源碼中的src文件夾整個文件夾拷貝一份至"SPI—FatFs文件系統\USER\"文件夾下並修改名為"FATFS",見圖 255。
圖 255 拷貝FatFs源碼到工程
2) 使用KEIL軟件打開工程文件(..\SPI—FatFs文件系統\Project\RVMDK(uv5)\ BH-F429.uvprojx),並將FatFs組件文件添加到工程中,需要添加有ff.c、diskio.c和cc936.c三個文件,見圖 256。
圖 256 添加FatFS文件到工程
3) 添加FATFS文件夾到工程的include選項中。打開工程選項對話框,選擇"C/C++"選項下的"Include Paths"項目,在彈出路徑設置對話框中選擇添加"FATFS"文件夾,見圖 257。
圖 257 添加FATFS路徑到工程選項
4) 如果現在編譯工程,可以發現有兩個錯誤,一個是來自diskio.c文件,提示有一些頭文件沒找,diskio.c文件內容是與底層設備輸入輸出接口函數文件,不同硬件設計驅動就不同,需要的文件也不同;另外一個錯誤來自cc936.c文件,提示該文件不是工程所必需的,這是因為FatFs默認使用日語,我們想要支持簡體中文需要修改FatFs的配置,即修改ffconf.h文件。至此,將FatFs添加到工程的框架已經操作完成,接下來要做的就是修改diskio.c文件和ffconf.h文件。
25.3.4 FatFs底層設備驅動函數
FatFs文件系統與底層介質的驅動分離開來,對底層介質的操作都要交給用戶去實現,它僅僅是提供了一個函數接口而已。表 251為FatFs移植時用戶必須支持的函數。通過表 251我們可以清晰知道很多函數是在一定條件下才需要添加的,只有前三個函數是必須添加的。我們完全可以根據實際需求選擇實現用到的函數。
前三個函數是實現讀文件最基本需求。接下來三個函數是實現創建文件、修改文件需要的。為實現格式化功能,需要在disk_ioctl添加兩個獲取物理設備信息選項。我們一般只有實現前面六個函數就可以了,已經足夠滿足大部分功能。
為支持簡體中文長文件名稱需要添加ff_convert和ff_wtoupper函數,實際這兩個已經在cc936.c文件中實現了,我們只要直接把cc936.c文件添加到工程中就可以了。
后面六個函數一般都不用。如真有需要可以參考syscall.c文件(src\option文件夾內)。
表 251 FatFs移植需要用戶支持函數
函數 |
條件(ffconf.h) |
備注 |
disk_status |
總是需要 |
底層設備驅動函數 |
disk_write |
_FS_READONLY == 0 |
|
disk_ioctl (GET_SECTOR_COUNT) |
_USE_MKFS == 1 |
|
disk_ioctl (GET_SECTOR_SIZE) |
_MAX_SS != _MIN_SS |
|
disk_ioctl (CTRL_TRIM) |
_USE_TRIM == 1 |
|
ff_convert |
_USE_LFN != 0 |
Unicode支持,為支持簡體中文,添加cc936.c到工程即可 |
ff_cre_syncobj |
_FS_REENTRANT == 1 |
FatFs可重入配置,需要多任務系統支持(一般不需要) |
ff_mem_alloc |
_USE_LFN == 3 |
長文件名支持,緩沖區設置在堆空間(一般設置_USE_LFN = 2 ) |
底層設備驅動函數是存放在diskio.c文件,我們的目的就是把diskio.c中的函數接口與SPI Flash芯片驅動連接起來。總共有五個函數,分別為設備狀態獲取(disk_status)、設備初始化(disk_initialize)、扇區讀取(disk_read)、扇區寫入(disk_write)、其他控制(disk_ioctl)。
接下來,我們對每個函數結合SPI Flash芯片驅動做詳細講解。
宏定義
代碼清單 251 物理編號宏定義
1 /* 為每個設備定義一個物理編號 */
2 #define ATA 0 // 預留SD卡使用
3 #define SPI_FLASH 1 // 外部SPI Flash
這兩個宏定義在FatFs中非常重要,FatFs是支持多物理設備的,必須為每個物理設備定義一個不同的編號。
SD卡是預留接口,在講解SDIO接口相關章節后會用到,可以實現使用讀寫SD卡內文件。
設備狀態獲取
代碼清單 252設備狀態獲取
1 DSTATUS disk_status (
2 BYTE pdrv /* 物理編號 */
3 )
4 {
5
6 DSTATUS status = STA_NOINIT;
7
8 switch (pdrv) {
9 case ATA: /* SD CARD */
10 break;
11
12 case SPI_FLASH:
13 /* SPI Flash狀態檢測:讀取SPI Flash 設備ID */
14 if (sFLASH_ID == SPI_FLASH_ReadID()) {
15 /* 設備ID讀取結果正確 */
16 status &= ~STA_NOINIT;
17 } else {
18 /* 設備ID讀取結果錯誤 */
19 status = STA_NOINIT;;
20 }
21 break;
22
23 default:
24 status = STA_NOINIT;
25 }
26 return status;
27 }
disk_status函數只有一個參數pdrv,表示物理編號。一般我們都是使用switch函數實現對pdrv的分支判斷。對於SD卡只是預留接口,留空即可。對於SPI Flash芯片,我們直接調用在SPI_FLASH_ReadID()獲取設備ID,然后判斷是否正確,如果正確,函數返回正常標准;如果錯誤,函數返回異常標志。SPI_FLASH_ReadID()是定義在bsp_spi_flash.c文件中,上一章節已做了分析。
設備初始化
代碼清單 253 設備初始化
1 DSTATUS disk_initialize (
2 BYTE pdrv /* 物理編號 */
3 )
4 {
5 uint16_t i;
6 DSTATUS status = STA_NOINIT;
7 switch (pdrv) {
8 case ATA: /* SD CARD */
9 break;
10
11 case SPI_FLASH: /* SPI Flash */
12 /* 初始化SPI Flash */
13 SPI_FLASH_Init();
14 /* 延時一小段時間 */
15 i=500;
16 while (--i);
17 /* 喚醒SPI Flash */
18 SPI_Flash_WAKEUP();
19 /* 獲取SPI Flash芯片狀態 */
20 status=disk_status(SPI_FLASH);
21 break;
22
23 default:
24 status = STA_NOINIT;
25 }
26 return status;
27 }
disk_initialize函數也是有一個參數pdrv,用來指定設備物理編號。對於SPI Flash芯片我們調用SPI_FLASH_Init()函數實現對SPI Flash芯片引腳GPIO初始化配置以及SPI通信參數配置。SPI_Flash_WAKEUP()函數喚醒SPI Flash芯片,當SPI Flash芯片處於睡眠模式時需要喚醒芯片才可以進行讀寫操作。
最后調用disk_status函數獲取SPI Flash芯片狀態,並返回狀態值。
讀取扇區
代碼清單 254 扇區讀取
1 DRESULT disk_read (
2 BYTE pdrv, /* 設備物理編號(0..) */
3 BYTE *buff, /* 數據緩存區 */
4 DWORD sector, /* 扇區首地址 */
5 UINT count /* 扇區個數(1..128) */
6 )
7 {
8 DRESULT status = RES_PARERR;
9 switch (pdrv) {
10 case ATA: /* SD CARD */
11 break;
12
13 case SPI_FLASH:
14 /* 扇區偏移6MB,外部Flash文件系統空間放在SPI Flash后面10MB空間 */
15 sector+=1536;
16 SPI_FLASH_BufferRead(buff, sector <<12, count<<12);
17 status = RES_OK;
18 break;
19
20 default:
21 status = RES_PARERR;
22 }
23 return status;
24 }
disk_read函數有四個形參。pdrv為設備物理編號。buff是一個BYTE類型指針變量,buff指向用來存放讀取到數據的存儲區首地址。sector是一個DWORD類型變量,指定要讀取數據的扇區首地址。count是一個UINT類型變量,指定扇區數量。
BYTE類型實際是unsigned char類型,DWORD類型實際是unsigned long類型,UINT類型實際是 unsigned int類型,類型定義在integer.h文件中。
開發板使用的SPI Flash芯片型號為W25Q128FV,每個扇區大小為4096個字節(4KB),總共有16M字節空間,為兼容后面實驗程序,我們只將后部分10MB空間分配給FatFs使用,前部分6MB空間用於其他實驗需要,即FatFs是從6MB空間開始,為實現這個效果需要將所有的讀寫地址都偏移1536個扇區空間。
對於SPI Flash芯片,主要是使用SPI_FLASH_BufferRead()實現在指定地址讀取指定長度的數據,它接收三個參數,第一個參數為指定數據存放地址指針。第二個參數為指定數據讀取地址,這里使用左移運算符,左移12位實際是乘以4096,這與每個扇區大小是息息相關的。第三個參數為讀取數據個數,也是需要使用左移運算符。
扇區寫入
代碼清單 255 扇區輸入
1 DRESULT disk_write (
2 BYTE pdrv, /* 設備物理編號(0..) */
3 const BYTE *buff, /* 欲寫入數據的緩存區 */
4 DWORD sector, /* 扇區首地址 */
5 UINT count /* 扇區個數(1..128) */
6 )
7 {
8 uint32_t write_addr;
9 DRESULT status = RES_PARERR;
10 if (!count) {
11 return RES_PARERR; /* Check parameter */
12 }
13
14 switch (pdrv) {
15 case ATA: /* SD CARD */
16 break;
17
18 case SPI_FLASH:
19 /* 扇區偏移6MB,外部Flash文件系統空間放在SPI Flash后面10MB空間 */
20 sector+=1536;
21 write_addr = sector<<12;
22 SPI_FLASH_SectorErase(write_addr);
23 SPI_FLASH_BufferWrite((u8 *)buff,write_addr,count<<12);
24 status = RES_OK;
25 break;
26
27 default:
28 status = RES_PARERR;
29 }
30 return status;
31 }
disk_write函數有四個形參,pdrv為設備物理編號。buff指向待寫入扇區數據的首地址。sector,指定要讀取數據的扇區首地址。count指定扇區數量。對於SPI Flash芯片,在寫入數據之前需要先擦除,所以用到扇區擦除函數(SPI_FLASH_SectorErase)。然后就是在調用數據寫入函數(SPI_FLASH_BufferWrite)把數據寫入到指定位置內。
其他控制
代碼清單 256 其他控制
1 DRESULT disk_ioctl (
2 BYTE pdrv, /* 物理編號 */
3 BYTE cmd, /* 控制指令 */
4 void *buff /* 寫入或者讀取數據地址指針 */
5 )
6 {
7 DRESULT status = RES_PARERR;
8 switch (pdrv) {
9 case ATA: /* SD CARD */
10 break;
11
12 case SPI_FLASH:
13 switch (cmd) {
14 /* 扇區數量:2560*4096/1024/1024=10(MB) */
15 case GET_SECTOR_COUNT:
16 *(DWORD * )buff = 2560;
17 break;
18 /* 扇區大小 */
19 case GET_SECTOR_SIZE :
20 *(WORD * )buff = 4096;
21 break;
22 /* 同時擦除扇區個數 */
23 case GET_BLOCK_SIZE :
24 *(DWORD * )buff = 1;
25 break;
26 }
27 status = RES_OK;
28 break;
29
30 default:
31 status = RES_PARERR;
32 }
33 return status;
34 }
disk_ioctl函數有三個形參,pdrv為設備物理編號,cmd為控制指令,包括發出同步信號、獲取扇區數目、獲取扇區大小、獲取擦除塊數量等等指令,buff為指令對應的數據指針。
對於SPI Flash芯片,為支持FatFs格式化功能,需要用到獲取扇區數量(GET_SECTOR_COUNT)指令和獲取擦除塊數量(GET_BLOCK_SIZE)。另外,SD卡扇區大小為512字節,SPI Flash芯片一般設置扇區大小為4096字節,所以需要用到獲取扇區大小(GET_SECTOR_SIZE)指令。
時間戳獲取
代碼清單 257 時間戳獲取
1 __weak DWORD get_fattime(void)
2 {
3 /* 返回當前時間戳 */
4 return ((DWORD)(2015 - 1980) << 25) /* Year 2015 */
5 | ((DWORD)1 << 21) /* Month 1 */
6 | ((DWORD)1 << 16) /* Mday 1 */
7 | ((DWORD)0 << 11) /* Hour 0 */
8 | ((DWORD)0 << 5) /* Min 0 */
9 | ((DWORD)0 >> 1); /* Sec 0 */
10 }
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) 。
25.3.5 FatFs功能配置
ffconf.h文件是FatFs功能配置文件,我們可以對文件內容進行修改,使得FatFs更符合我們的要求。ffconf.h對每個配置選項都做了詳細的使用情況說明。下面只列出修改的配置,其他配置采用默認即可。
代碼清單 258 FatFs功能配置選項
1 #define _USE_MKFS 1
2 #define _CODE_PAGE 936
3 #define _USE_LFN 2
4 #define _VOLUMES 2
5 #define _MIN_SS 512
6 #define _MAX_SS 4096
1) _USE_MKFS:格式化功能選擇,為使用FatFs格式化功能,需要把它設置為1。
2) _CODE_PAGE:語言功能選擇,並要求把相關語言文件添加到工程宏。為支持簡體中文文件名需要使用"936",正如在圖 256的操作,我們已經把cc936.c文件添加到工程中。
3) _USE_LFN:長文件名支持,默認不支持長文件名,這里配置為2,支持長文件名,並指定使用棧空間為緩沖區。
4) _VOLUMES:指定物理設備數量,這里設置為2,包括預留SD卡和SPI Flash芯片。
5) _MIN_SS 、_MAX_SS:指定扇區大小的最小值和最大值。SD卡扇區大小一般都為512字節,SPI Flash芯片扇區大小一般設置為4096字節,所以需要把_MAX_SS改為4096。
25.3.6 FatFs功能測試
移植操作到此,就已經把FatFs全部添加到我們的工程了,這時我們編譯功能,順利編譯通過,沒有錯誤。接下來,我們就可以使用編寫圖 254中用戶應用程序了。
主要的測試包括格式化測試、文件寫入測試和文件讀取測試三個部分,主要程序都在main.c文件中實現。
變量定義
代碼清單 259 變量定義
1 FATFS fs; /* FatFs文件系統對象 */
2 FIL fnew; /* 文件對象 */
3 FRESULT res_flash; /* 文件操作結果 */
4 UINT fnum; /* 文件成功讀寫數量 */
5 BYTE buffer[1024]= {0}; /* 讀緩沖區 */
6 BYTE textFileBuffer[] = /* 寫緩沖區*/
7 "歡迎使用野火STM32 F429開發板今天是個好日子,新建文件系統測試文件\r\n";
FATFS是在ff.h文件定義的一個結構體類型,針對的對象是物理設備,包含了物理設備的物理編號、扇區大小等等信息,一般我們都需要為每個物理設備定義一個FATFS變量。
FIL也是在ff.h文件定義的一個結構體類型,針對的對象是文件系統內具體的文件,包含了文件很多基本屬性,比如文件大小、路徑、當前讀寫地址等等。如果需要在同一時間打開多個文件進行讀寫,才需要定義多個FIL變量,不然一般定義一個FIL變量即可。
FRESULT是也在ff.h文件定義的一個枚舉類型,作為FatFs函數的返回值類型,主要管理FatFs運行中出現的錯誤。總共有19種錯誤類型,包括物理設備讀寫錯誤、找不到文件、沒有掛載工作空間等等錯誤。這在實際編程中非常重要,當有錯誤出現是我們要停止文件讀寫,通過返回值我們可以快速定位到錯誤發生的可能地點。如果運行沒有錯誤才返回FR_OK。
fnum是個32位無符號整形變量,用來記錄實際讀取或者寫入數據的數組。
buffer和textFileBuffer分別對應讀取和寫入數據緩存區,都是8位無符號整形數組。
主函數
代碼清單 2510 主函數
1 int main(void)
2 {
3 /* 初始化LED */
4 LED_GPIO_Config();
5 LED_BLUE;
6
7 /* 初始化調試串口,一般為串口1 */
8 Debug_USART_Config();
9 printf("****** 這是一個SPI FLASH 文件系統實驗 ******\r\n");
10
11 //在外部SPI Flash掛載文件系統,文件系統掛載時會對SPI設備初始化
12 res_flash = f_mount(&fs,"1:",1);
13
14 /*----------------------- 格式化測試 ---------------------------*/
15 /* 如果沒有文件系統就格式化創建創建文件系統 */
16 if (res_flash == FR_NO_FILESYSTEM) {
17 printf("》FLASH還沒有文件系統,即將進行格式化...\r\n");
18 /* 格式化 */
19 res_flash=f_mkfs("1:",0,0);
20
21 if (res_flash == FR_OK) {
22 printf("》FLASH已成功格式化文件系統。\r\n");
23 /* 格式化后,先取消掛載 */
24 res_flash = f_mount(NULL,"1:",1);
25 /* 重新掛載 */
26 res_flash = f_mount(&fs,"1:",1);
27 } else {
28 LED_RED;
29 printf("《《格式化失敗。》》\r\n");
30 while (1);
31 }
32 } else if (res_flash!=FR_OK) {
33 printf("!!外部Flash掛載文件系統失敗。(%d)\r\n",res_flash);
34 printf("!!可能原因:SPI Flash初始化不成功。\r\n");
35 while (1);
36 } else {
37 printf("》文件系統掛載成功,可以進行讀寫測試\r\n");
38 }
39
40 /*---------------------- 文件系統測試:寫測試 ----------------------*/
41 /* 打開文件,如果文件不存在則創建它 */
42 printf("\r\n****** 即將進行文件寫入測試... ******\r\n");
43 res_flash = f_open(&fnew, "1:FatFs讀寫測試文件.txt",
44 FA_CREATE_ALWAYS | FA_WRITE );
45 if ( res_flash == FR_OK ) {
46 printf("》打開/創建FatFs讀寫測試文件.txt文件成功,向文件寫入數據。\r\n");
47 /* 將指定存儲區內容寫入到文件內 */
48 res_flash=f_write(&fnew,WriteBuffer,sizeof(WriteBuffer),&fnum);
49 if (res_flash==FR_OK) {
50 printf("》文件寫入成功,寫入字節數據:%d\n",fnum);
51 printf("》向文件寫入的數據為:\r\n%s\r\n",WriteBuffer);
52 } else {
53 printf("!!文件寫入失敗:(%d)\n",res_flash);
54 }
55 /* 不再讀寫,關閉文件 */
56 f_close(&fnew);
57 } else {
58 LED_RED;
59 printf("!!打開/創建文件失敗。\r\n");
60 }
61
62 /*------------------- 文件系統測試:讀測試 --------------------------*/
63 printf("****** 即將進行文件讀取測試... ******\r\n");
64 res_flash = f_open(&fnew, "1:FatFs讀寫測試文件.txt",
65 FA_OPEN_EXISTING | FA_READ);
66 if (res_flash == FR_OK) {
67 LED_GREEN;
68 printf("》打開文件成功。\r\n");
69 res_flash = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum);
70 if (res_flash==FR_OK) {
71 printf("》文件讀取成功,讀到字節數據:%d\r\n",fnum);
72 printf("》讀取得的文件數據為:\r\n%s \r\n", ReadBuffer);
73 } else {
74 printf("!!文件讀取失敗:(%d)\n",res_flash);
75 }
76 } else {
77 LED_RED;
78 printf("!!打開文件失敗。\r\n");
79 }
80 /* 不再讀寫,關閉文件 */
81 f_close(&fnew);
82
83 /* 不再使用文件系統,取消掛載文件系統 */
84 f_mount(NULL,"1:",1);
85
86 /* 操作完成,停機 */
87 while (1) {
88 }
89 }
首先,初始化RGB彩燈和調試串口,用來指示程序進程。
FatFs的第一步工作就是使用f_mount函數掛載工作區。f_mount函數有三個形參,第一個參數是指向FATFS變量指針,如果賦值為NULL可以取消物理設備掛載。第二個參數為邏輯設備編號,使用設備根路徑表示,與物理設備編號掛鈎,在代碼清單 251中我們定義SPI Flash芯片物理編號為1,所以這里使用"1:"。第三個參數可選0或1,1表示立即掛載,0表示不立即掛載,延遲掛載。 f_mount函數會返回一個FRESULT類型值,指示運行情況。
如果f_mount函數返回值為FR_NO_FILESYSTEM,說明沒有FAT文件系統,比如新出廠的SPI Flash芯片就沒有FAT文件系統。我們就必須對物理設備進行格式化處理。使用f_mkfs函數可以實現格式化操作。f_mkfs函數有三個形參,第一個參數為邏輯設備編號;第二參數可選0或者1,0表示設備為一般硬盤,1表示設備為軟盤。第三個參數指定扇區大小,如果為0,表示通過代碼清單 256中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函數取消掛載。
25.3.7 下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板"USB TO UART"接口跟電腦,在電腦端打開串口調試助手,把編譯好的程序下載到開發板。程序開始運行后,RGB彩燈為藍色,在串口調試助手可看到格式化測試、寫文件檢測和讀文件檢測三個過程;最后如果所有讀寫操作都正常,RGB彩燈會指示為綠色,如果在運行中FatFs出現錯誤RGB彩燈指示為紅色。
雖然我們通過RGB彩燈指示和串口調試助手信息打印方法來說明FatFs移植成功,並順利通過測試,但心底總是很踏實,所謂眼見為實,雖然我們創建了"FatFs讀寫測試文件.txt"這個文件,卻完全看不到實體。這個確實是個問題,因為我們這里使用SPI Flash芯片作為物理設備,並不像SD卡那么方便直接用讀卡器就可以在電腦端打開驗證。另外一個問題,就目前來說,在SPI Flash芯片上掛載FatFs好像沒有實際意義,無法發揮文件系統功能。
實際上,這里歸根到底就是我們目前沒辦法在電腦端查看SPI Flash芯片內FatFs的內容,沒辦法非常方便拷貝、刪除文件。我們當然不會做無用功,STM32控制器還有一個硬件資源可以解決上面的問題,就是USB!我們可以通過編程把整個開發板變成一個U盤,而U盤存儲空間就是SPI Flash芯片的空間。這樣非常方便實現文件讀寫。至於USB內容將在USB相關章節講解。
25.4 FatFs功能使用實驗
上個實驗我們實現了FatFs的格式化、讀文件和寫文件功能,這個已經滿足很多部分的運用需要。有時,我們需要更多的文件操作功能,FatFs還是提供了不少的功能的,比如設備存儲空間信息獲取、讀寫文件指針定位、創建目錄、文件移動和重命名、文件或目錄信息獲取等等功能。我們接下來這個實驗內容就是展示FatFs眾多功能,提供一個很好了范例,以后有用到相關內容,參考使用非常方便。
25.4.1 硬件設計
本實驗主要使用FatFs軟件功能,不需要其他硬件模塊,使用與FatFs移植實驗相同硬件配置即可。
25.4.2 軟件設計
上個實驗我們已經移植好了FatFs,這個例程主要是應用,所以簡單起見,直接拷貝上個實驗的工程文件,保持FatFs底層驅動程序,我們只改main.c文件內容,實現應用程序。
FatFs多項功能測試
代碼清單 2511 FatFs多項功能測試
1 static FRESULT miscellaneous(void)
2 {
3 DIR dir;
4 FATFS *pfs;
5 DWORD fre_clust, fre_sect, tot_sect;
6
7 printf("\n*************** 設備信息獲取 ***************\r\n");
8 /* 獲取設備信息和空簇大小 */
9 res_flash = f_getfree("1:", &fre_clust, &pfs);
10
11 /* 計算得到總的扇區個數和空扇區個數 */
12 tot_sect = (pfs->n_fatent - 2) * pfs->csize;
13 fre_sect = fre_clust * pfs->csize;
14
15 /* 打印信息(4096 字節/扇區) */
16 printf("》設備總空間:%10lu KB。\n》可用空間: %10lu KB。\n",
17 tot_sect *4, fre_sect *4);
18
19 printf("\n******** 文件定位和格式化寫入功能測試 ********\r\n");
20 res_flash = f_open(&fnew, "1:FatFs讀寫測試文件.txt",
21 FA_OPEN_EXISTING|FA_WRITE|FA_READ );
22 if ( res_flash == FR_OK ) {
23 /* 文件定位 */
24 res_flash = f_lseek(&fnew,f_size(&fnew)-1);
25 if (res_flash == FR_OK) {
26 /* 格式化寫入,參數格式類似printf函數 */
27 f_printf(&fnew,"\n在原來文件新添加一行內容\n");
28 f_printf(&fnew,"》設備總空間:%10lu KB。\n》可用空間: %10lu KB。\n",
29 tot_sect *4, fre_sect *4);
30 /* 文件定位到文件起始位置 */
31 res_flash = f_lseek(&fnew,0);
32 /* 讀取文件所有內容到緩存區 */
33 res_flash = f_read(&fnew,readbuffer,f_size(&fnew),&fnum);
34 if (res_flash == FR_OK) {
35 printf("》文件內容:\n%s\n",readbuffer);
36 }
37 }
38 f_close(&fnew);
39
40 printf("\n********** 目錄創建和重命名功能測試 **********\r\n");
41 /* 嘗試打開目錄 */
42 res_flash=f_opendir(&dir,"1:TestDir");
43 if (res_flash!=FR_OK) {
44 /* 打開目錄失敗,就創建目錄 */
45 res_flash=f_mkdir("1:TestDir");
46 } else {
47 /* 如果目錄已經存在,關閉它 */
48 res_flash=f_closedir(&dir);
49 /* 刪除文件 */
50 f_unlink("1:TestDir/testdir.txt");
51 }
52 if (res_flash==FR_OK) {
53 /* 重命名並移動文件 */
54 res_flash=f_rename("1:FatFs讀寫測試文件.txt",
55 "1:TestDir/testdir.txt");
56 }
57 } else {
58 printf("!! 打開文件失敗:%d\n",res_flash);
59 printf("!! 或許需要再次運行"FatFs移植與讀寫測試"工程\n");
60 }
61 return res_flash;
62 }
首先是設備存儲信息獲取,目的是獲取設備總容量和剩余可用空間。f_getfree函數是設備空閑簇信息獲取函數,有三個形參,第一個參數為邏輯設備編號;第二個參數為返回空閑簇數量;第三個參數為返回指向文件系統對象的指針。通過計算可得到設備總的扇區個數以及空閑扇區個數,對於SPI Flash芯片我們設置每個扇區為4096字節大小。這樣很容易就算出設備存儲信息。
接下來是文件讀寫指針定位和格式化輸入功能測試。文件定位在一些場合非常有用,比如我們需要記錄多項數據,但每項數據長度不確定,但有個最長長度,使用我們就可以使用文件定位lseek函數功能把數據存放在規定好的地址空間上。當我們需要讀取文件內容時就使用文件定位函數定位到對應地址讀取。
使用文件讀寫操作之前都必須使用f_open函數打開文件,開始文件是讀寫指針是在文件起始位置的,馬上寫入數據的話會覆蓋原來文件內容的。這里,我們使用f_lseek函數定位到文件末尾位置,再寫入內容。f_lseek函數有兩個形參,第一個參數為文件對象指針,第二個參數為需要定位的字節數,這個字節數是相對文件起始位置的,比如設置為0,則將文件讀寫指針定位到文件起始位置了。
f_printf函數是格式化寫入函數,需要把ffconf.h文件中的_USE_STRFUNC配置為1才支持。f_printf函數用法類似C庫函數printf函數,只是它將數據直接寫入到文件中。
最后是目錄創建和文件移動和重命名功能。使用f_opendir函數可以打開路徑(這里不區分目錄和路徑概念,下同),如果路徑不存在則返回錯誤,使用f_closedir函數關閉已經打開的路徑。新版的FatFs支持相對路徑功能,使路徑操作更加靈活。f_opendir函數有兩個形參,第一個參數為指向路徑對象的指針,第二個參數為路徑。f_closedir函數只需要指向路徑對象的指針一個形參。
f_mkdir函數用於創建路徑,如果指定的路徑不存在就創建它,創建的路徑存在形式就是文件夾。f_mkdir函數只要一個形參,就是指定路徑。
f_rename函數是帶有移動功能的重命名函數,它有兩個形參,第一個參數為源文件名稱,第二個參數為目標名稱。目標名稱可附帶路徑,如果路徑與源文件路徑不同見移動文件到目標路徑下。
文件信息獲取
代碼清單 2512 文件信息獲取
1 static FRESULT file_check(void)
2 {
3 FILINFO fno;
4
5 /* 獲取文件信息 */
6 res_flash=f_stat("1:TestDir/testdir.txt",&fno);
7 if (res_flash==FR_OK) {
8 printf(""testdir.txt"文件信息:\n");
9 printf("》文件大小: %ld(字節)\n", fno.fsize);
10 printf("》時間戳: %u/%02u/%02u, %02u:%02u\n",
11 (fno.fdate >> 9) + 1980, fno.fdate >> 5 & 15, fno.fdate & 31,
12 fno.ftime >> 11, fno.ftime >> 5 & 63);
13 printf("》屬性: %c%c%c%c%c\n\n",
14 (fno.fattrib & AM_DIR) ? 'D' : '-', // 是一個目錄
15 (fno.fattrib & AM_RDO) ? 'R' : '-', // 只讀文件
16 (fno.fattrib & AM_HID) ? 'H' : '-', // 隱藏文件
17 (fno.fattrib & AM_SYS) ? 'S' : '-', // 系統文件
18 (fno.fattrib & AM_ARC) ? 'A' : '-'); // 檔案文件
19 }
20 return res_flash;
21 }
f_stat函數用於獲取文件的屬性,有兩個形參,第一個參數為文件路徑,第二個參數為返回指向文件信息結構體變量的指針。文件信息結構體變量包含文件的大小、最后修改時間和日期、文件屬性、短文件名以及長文件名等信息。
路徑掃描
代碼清單 2513 路徑掃描
1 static FRESULT scan_files (char* path)
2 {
3 FRESULT res; //部分在遞歸過程被修改的變量,不用全局變量
4 FILINFO fno;
5 DIR dir;
6 int i;
7 char *fn; // 文件名
8
9 #if _USE_LFN
10 /* 長文件名支持 */
11 /* 簡體中文需要2個字節保存一個"字"*/
12 static char lfn[_MAX_LFN*2 + 1];
13 fno.lfname = lfn;
14 fno.lfsize = sizeof(lfn);
15 #endif
16 //打開目錄
17 res = f_opendir(&dir, path);
18 if (res == FR_OK) {
19 i = strlen(path);
20 for (;;) {
21 //讀取目錄下的內容,再讀會自動讀下一個文件
22 res = f_readdir(&dir, &fno);
23 //為空時表示所有項目讀取完畢,跳出
24 if (res != FR_OK || fno.fname[0] == 0) break;
25 #if _USE_LFN
26 fn = *fno.lfname ? fno.lfname : fno.fname;
27 #else
28 fn = fno.fname;
29 #endif
30 //點表示當前目錄,跳過
31 if (*fn == '.') continue;
32 //目錄,遞歸讀取
33 if (fno.fattrib & AM_DIR) {
34 //合成完整目錄名
35 sprintf(&path[i], "/%s", fn);
36 //遞歸遍歷
37 res = scan_files(path);
38 path[i] = 0;
39 //打開失敗,跳出循環
40 if (res != FR_OK)
41 break;
42 } else {
43 printf("%s/%s\r\n", path, fn); //輸出文件名
44 /* 可以在這里提取特定格式的文件路徑 */
45 }//else
46 } //for
47 }
48 return res;
49 }
scan_files函數用來掃描指定路徑下的文件。比如我們設計一個mp3播放器,我們需要提取mp3格式文件,諸如*.txt、*.c文件我們統統不可要的,這時我們就必須掃描路徑下所有文件並把*.mp3或*.MP3格式文件提取出來。這里我們提取特定格式文件,而是把所有文件名稱都通過串口打印出來。
我們在ffconf.h文件中定義了長文件名稱支持(_USE_LFN=2),一般有用到簡體中文文件名稱的都要長文件名支持。短文件名稱是8.3格式,即名稱是8個字節,后綴名是3個字節,對於使用英文名稱還可以,使用中文名稱就很容易長度不夠了。使能了長文件名支持后,使用之前需要指定文件名的存儲區還有存儲區的大小。
接下來就是使用f_opendir函數打開指定的路徑。如果路徑存在就使用f_readdir函數讀取路徑下內容,f_readdir函數可以讀取路徑下的文件或者文件夾,並保存信息到文件信息對象變量內。f_readdir函數有兩個形參,第一個參數為指向路徑對象變量的指針,第二個參數為指向文件信息對象的指針。f_readdir函數另外一個特性是自動讀取下一個文件對象,即循序運行該函數可以讀取該路徑下的所有文件。所以,在程序中,我們使用for循環讓f_readdir函數讀取所有文件,並在讀取所有文件之后退出循環。
在f_readdir函數成功讀取到一個對象時,我們還不清楚它是一個文件還是一個文件夾,此時我們就可以使用文件信息對象變量的文件屬性來判斷了,如果判斷得出是個文件那我們就直接通過串口打印出來就好了。如果是個文件夾,我們就要進入該文件夾掃描,這時就重新調用掃描函數scan_files就可以了,形成一個遞歸調用結構,只是我們這次用的參數與最開始時候是不同的,現在是使用子文件夾名稱。
主函數
代碼清單 2514 主函數
1 int main(void)
2 {
3 /* 初始化調試串口,一般為串口1 */
4 Debug_USART_Config();
5 printf("******** 這是一個SPI FLASH 文件系統實驗 *******\r\n");
6
7 //在外部SPI Flash掛載文件系統,文件系統掛載時會對SPI設備初始化
8 res_flash = f_mount(&fs,"1:",1);
9 if (res_flash!=FR_OK) {
10 printf("!!外部Flash掛載文件系統失敗。(%d)\r\n",res_flash);
11 printf("!!可能原因:SPI Flash初始化不成功。\r\n");
12 while (1);
13 } else {
14 printf("》文件系統掛載成功,可以進行測試\r\n");
15 }
16
17 /* FatFs多項功能測試 */
18 res_flash = miscellaneous();
19
20
21 printf("\n*************** 文件信息獲取測試 **************\r\n");
22 res_flash = file_check();
23
24
25 printf("***************** 文件掃描測試 ****************\r\n");
26 strcpy(fpath,"1:");
27 scan_files(fpath);
28
29
30 /* 不再使用文件系統,取消掛載文件系統 */
31 f_mount(NULL,"1:",1);
32
33 /* 操作完成,停機 */
34 while (1) {
35 }
36 }
串口在程序調試中經常使用,可以把變量值直觀打印到串口調試助手,這個信息非常重要,同樣在使用之前需要調用Debug_USART_Config函數完成調試串口初始化。
使用FatFs進行文件操作之前都使用f_mount函數掛載物理設備,這里我們使用SPI Flash芯片上的FAT文件系統。
接下來我們直接調用miscellaneous函數進行FatFs設備信息獲取、文件定位和格式化寫入功能以及目錄創建和重命名功能測試。調用file_check函數進行文件信息獲取測試。
scan_files函數用來掃描路徑下的所有文件,fpath是我們定義的一個包含100個元素的字符型數組,並將其賦值為SPI Flash芯片物理編號對於的根目錄。這樣允許scan_files函數見打印SPI Flash芯片內FatFs所有文件到串口調試助手。注意,這里的定義fpaht數組是必不可少的,因為scan_files函數本身是個遞歸函數,要求實際參數有較大空間的緩存區。
25.4.3 下載驗證
保證開發板相關硬件連接正確,用USB線連接開發板"USB TO UART"接口跟電腦,在電腦端打開串口調試助手,把編譯好的程序下載到開發板。程序開始運行,在串口調試助手可看到每個階段測試相關信息情況。
25.5 每課一問
1、使用FatFs功能編程,在"1:每課一問/測試文件.txt"文本文件的第20字節開始寫入一下內容:今天是個好日子,我學會了FatFs。要求如果文件不存在就新建文件,如果文件存在就修改文件。最后在串口調試助手打印調試結果。