前言
- 硬件:
- 單片機:stm32f072CB,sram大小16k。(其他單片機只要sram>8k即可通用)
- SPIFlash:W25Q128FV,16Mbyte,單次擦除最小4k。
- 程序使用Keil編譯器,C99標准。
- 程序已經全部完成並測試通過,目前沒出現明顯問題。
- 程序使用的FatFs庫版本:R0.13b。下文所有內容僅保證在此版本可行。
添加文件
- 獲取FatFs庫(官網)
- 將source文件夾全部復制到目標工程中
- 添加所有.c文件到工程中,添加相關路徑
移植修改
需要修改的文件:
- integer.h:修改各種整型的宏定義(注:C99--long long對應64位整型)
- ffconf.h:修改各種設置:(全部宏定義意義見官網)
- FF_USE_STRFUNC設為1:開啟字符串功能
- FF_USE_MKFS設為1:開啟格式化功能
- FF_CODE_PAGE設為936:簡體中文
- FF_MIN_SS、FF_MAX_SS設為4096:扇區大小4k
- FF_FS_TINY設為1:文件對象(FIL)不再包括數據緩沖區,而是使用FatFs中的公用緩沖區,適用於RAM偏小的情況。
- FF_FS_NORTC設為1:禁用RTC(時間戳)功能,因為stm32不具備獲取時間的功能
- diskio.c:修改各磁盤IO層操作函數
- 修改磁盤設備定義:#define DEV_SPI 0
- 修改各函數中case DEV_RAM的操作:stat = STA_NOINIT; 或res = RES_PARERR;
- 修改各函數中case DEV_SPI的操作:指向spi_disk.c中的各執行函數
- spi_disk.c:自定義文件,是diskio.c中各函數指向的執行函數
- 定義靜態全局變量_s_SPI_Init_OK,用於指示當前磁盤初始化狀態
- SPI_disk_status函數:獲取驅動器狀態。_s_SPI_Init_OK為0時返回STA_NOINIT。
- SPI_disk_initialize函數:驅動器初始化。執行SPIFlash初始化函數,執行完畢后將_s_SPI_Init_OK置1。
- SPI_disk_read函數:讀磁盤驅動器。進行異常處理后,將所有數據讀到指定的指針內。
-
1 DRESULT SPI_disk_read(BYTE *buff, DWORD sector, UINT count) { 2 if(sector > SEC_MAX || sector + count - 1 > SEC_MAX) return RES_PARERR; 3 if(CS_STATUS() == Bit_RESET) return RES_NOTRDY; 4 5 if(SSTF016B_RD(sector*SEC_SIZE, SEC_SIZE*count, buff) == ERR) { 6 return RES_ERROR; 7 } 8 return RES_OK; 9 }
-
- SPI_disk_write函數:寫磁盤驅動器。進行異常處理后,將所有數據寫入指定的扇區內。
- 注意事項:在寫完之后,有一個延時20ms的動作。這是因為:FatFs寫頁表和目錄表時,如果寫完SPIFlash不加延時,就會寫入不成功。這導致的后果是:對頁表的修改無法生效。也就是說,對已有文件的數據修改(不改變文件大小)會生效,但新增文件、刪除文件不會生效。具體原理暫時不明。
-
1 DRESULT SPI_disk_write(const BYTE *buff, DWORD sector, UINT count) { 2 if(sector > SEC_MAX || sector + count - 1 > SEC_MAX) return RES_PARERR; 3 if(CS_STATUS() == Bit_RESET) return RES_NOTRDY; 4 5 if(SSTF016B_Erase(sector, sector + count - 1) == ERR) { 6 return RES_ERROR; 7 } 8 if(SSTF016B_WR(sector*SEC_SIZE, buff, SEC_SIZE*count) == ERR){ 9 return RES_ERROR; 10 } 11 sys_Delay_Ms(20); 12 return RES_OK; 13 }
- SPI_disk_ioctl函數:執行ioctl命令。因為沒有開啟強制擦除的功能,強制擦除指令不執行任何操作。
- 注意事項:GET_BLOCK_SIZE指令要獲取的是塊大小(一次擦除的扇區數量),是以扇區為單位的(不是以字節為單位),必須是扇區的2的冪倍(1,2,4,8,...)。這里單扇區設為4k,實際上SPIFlash單次擦除允許的最小也是4k,所以塊大小設為1。
-
1 DRESULT SPI_disk_ioctl(BYTE cmd, void *buff) { 2 DWORD *pdword = NULL; 3 WORD *pword = NULL; 4 5 switch(cmd) { 6 case CTRL_SYNC://確保寫入操作已完成 7 return RES_OK; 8 9 case GET_SECTOR_COUNT://獲取扇區數量 10 pdword = (DWORD *)buff; 11 *pdword = SEC_MAX + 1; 12 return RES_OK; 13 14 case GET_SECTOR_SIZE://獲取單個扇區大小 15 pword = (WORD *)buff; 16 *pword = SEC_SIZE; 17 return RES_OK; 18 19 case GET_BLOCK_SIZE://獲取擦除塊大小(以扇區為單位) 20 pdword = (DWORD *)buff; 21 *pdword = 1; 22 return RES_OK; 23 24 case CTRL_TRIM://強制擦除 25 return RES_PARERR; 26 } 27 return RES_PARERR; 28 }
- user_fatfs_app.c:自定義文件,定義fatfs的應用函數。
- FatFs的基礎知識
- FAT16的結構(參見FAT16文件系統之總結構分析(一)):
- rsv:系統保留區(0扇區的DBR,可能存在的分區表,以及其他保留扇區),位於第0--x扇區。
- fat:文件頁表區(有時會有FAT2作為FAT的備份),位於x+1--y扇區。
- dir:文件目錄表區,位於y+1--z扇區。
- data:數據區,位於z+1--末扇區。
- FatFs的格式化(f_mkfs):
- f_mkfs的第二個參數:FM_FAT、FM_FAT32、FM_EXFAT、FM_ANY,這四個選項是向下兼容的。也就是說,當你選擇FM_FAT時,只可能格式化為FAT12或者FAT16(由扇區數量和簇大小決定);當你選擇FM_FAT32時,如果扇區數量和簇大小足以格式化為FAT32,最終就會格式化為FAT32,否則就會向下格式化為FAT16或者FAT12。
- f_mkfs的第二個參數:FM_SFD,是格式化為超級軟盤格式:如果不選擇此選項,rsv區會占用64個扇區(DBR、分區表等);如果選擇了此選項,rsv區會占用1個扇區(僅含DBR)。這里如果不設置FM_SFD,減去rsv、fat、dir區后的data區簇數量低於MAX_FAT12(0xFF5),系統格式化為FAT12;所以要設置FM_SFD,以格式化為FAT16。
- f_mkfs的第三個參數:可限定簇大小,必須是扇區的2的冪倍(1,2,4,8,...)【注意,BLOCK_SIZE是以扇區為單位,簇大小是以字節為單位。比如塊和簇同樣設為一個扇區大小,塊設為了1,簇設為了4096】。如果設為0,程序就會根據卷數量來分配簇大小。這里設為4096,否則程序根據卷數會將簇設為2扇區,簇數量只能格式化為FAT12。
- f_mkfs函數只會寫入FAT16的rsv、fat、dir區,對data區不進行任何操作。
- f_mkfs函數需要至少一個扇區大小的工作緩沖區。這個緩沖區可以在執行格式化指令的前一句定義,而不是定義為全局數組,如此可以省下4k的ram。局部變量是存儲在棧區中的,棧區一般不會很大;而且棧區同樣要占用ram,如果把棧區定義得很大,同樣是對ram的浪費。所以,還是只能把這個緩沖區定義為全部變量。
- FatFs的f_open和f_close函數:
- f_open的第三參數:FA_OPEN_ALWAYS--存在則打開、不存在則創建;FA_OPEN_APPEND--同上,但指針指向文件尾;FA_WRITE--要寫入必須帶此參數;FA_READ--要讀取必須帶此參數。
- 判斷f_open操作是打開還是新建的方法:f_size文件對象,返回0則為新建,否則為打開。
- 執行寫入操作后,必須執行f_close或f_sync函數,這是執行刷新文件頁表和目錄表的操作。如果不執行此操作而斷電,下次上電后文件系統會出錯。
- FatFs判斷文件系統是否存在的方法:f_getfree,返回FR_OK就沒有問題。
- FAT16的結構(參見FAT16文件系統之總結構分析(一)):
- 注意事項:
- 在單個扇區設為4k時,創建文件系統需要4k的ram(FATFS對象),每一個文件對象需要4k的ram(FIL對象)。當硬件sram不大時,應僅在需要時創建文件對象並使用、使用完畢馬上f_close丟棄文件對象;或者啟用FF_FS_TINY選項,文件對象不含數據緩沖區。
- 每次執行讀/寫操作后,文件指針都會指向之前操作的結尾處。可通過f_lseek函數移動文件指針。用f_lseek移動有三個常用的方法:移動至從頭開始的第x位--直接傳入參數x;移動至末尾前的x位:傳入參數f_size(fp)-x;前移/后移x位:傳入參數f_tell(fp)±x。
-
1 #include "user_fatfs_app.h" 2 3 extern uint8_t g_User_Data[sizeof(t_g_Statistical_Data)]; 4 extern t_g_Statistical_Data *pg_User_Data; 5 extern uint8_t g_History_Data[sizeof(t_g_History_Data)]; 6 extern t_g_History_Data *pg_History_Data; 7 8 FATFS g_Fatfs; 9 FATFS *fs = &g_Fatfs; 10 BYTE work_buffer[FF_MAX_SS]; 11 12 /****************************************************************************** 13 ** 函數名稱: fatfs_Init 14 ** 功能描述: fatfs初始化 15 ** 入口參數: 無 16 ** 返 回 值: 無 17 ** 18 ** 作 者: 19 ** 日 期: 20 **----------------------------------------------------------------------------- 21 ******************************************************************************/ 22 void fatfs_Init(void) { 23 DWORD num; 24 25 f_mount(&g_Fatfs, "", 0); 26 FRESULT result = f_getfree("", &num, &fs); 27 if(result != FR_OK) { 28 result = f_mkfs("", FM_FAT+FM_SFD, FF_MAX_SS, work_buffer, FF_MAX_SS); 29 } 30 31 memset(g_User_Data, 0, sizeof(g_User_Data)); 32 memset(g_History_Data, 0, sizeof(g_History_Data)); 33 fatfs_Get_Statistical_Data(); 34 } 35 36 /****************************************************************************** 37 ** 函數名稱: fatfs_Get_Statistical_Data 38 ** 功能描述: 獲取統計數據 39 ** 入口參數: 無 40 ** 返 回 值: 獲取結果:-1--打開文件失敗;0--讀取成功;>0--讀取錯誤 41 ** 42 ** 作 者: 43 ** 日 期: 44 **----------------------------------------------------------------------------- 45 ******************************************************************************/ 46 int8_t fatfs_Get_Statistical_Data(void) { 47 FIL statistical_data; 48 UINT len = 0; 49 int8_t result = 0; 50 51 result = f_open(&statistical_data, "0:statdat.bin", FA_OPEN_ALWAYS | FA_WRITE | FA_READ);//指針指向文件頭 52 if(result == FR_OK) { 53 uint16_t size = f_size(&statistical_data); 54 if(size < sizeof(t_g_Statistical_Data)) { //初次創建 55 pg_User_Data->year = 2018; 56 pg_User_Data->month = 7; 57 pg_User_Data->day = 1; 58 pg_User_Data->hour = 12; 59 pg_User_Data->min = 0; 60 pg_User_Data->sec = 0; 61 f_write(&statistical_data, g_User_Data, sizeof(t_g_Statistical_Data), &len); 62 f_lseek(&statistical_data, 0); 63 } 64 f_read(&statistical_data, g_User_Data, sizeof(t_g_Statistical_Data), &len); 65 } else { 66 result = -1; 67 } 68 result = f_close(&statistical_data); 69 // free(&statistical_data); 70 return result; 71 } 72 73 74 /****************************************************************************** 75 ** 函數名稱: fatfs_Update_Statistical_Data 76 ** 功能描述: 更新統計數據 77 ** 入口參數: 無 78 ** 返 回 值: 更新結果:-1--打開文件失敗;0--寫入成功;>0--寫入錯誤 79 ** 80 ** 作 者: 81 ** 日 期: 82 **----------------------------------------------------------------------------- 83 ******************************************************************************/ 84 int8_t fatfs_Update_Statistical_Data(void) { 85 FIL statistical_data; 86 UINT len = 0; 87 int8_t result = 0; 88 89 result = f_open(&statistical_data, "0:statdat.bin", FA_OPEN_EXISTING | FA_WRITE);//指針指向文件頭 90 if(result == FR_OK) { 91 result = f_write(&statistical_data, g_User_Data, sizeof(t_g_Statistical_Data), &len); 92 } else { 93 result = -1; 94 } 95 f_close(&statistical_data); 96 // free(&statistical_data); 97 98 return result; 99 } 100 101 102 /****************************************************************************** 103 ** 函數名稱: fatfs_Add_Historical_Data 104 ** 功能描述: 添加歷史記錄 105 ** 入口參數: 無 106 ** 返 回 值: 添加結果:-2--打開文件錯誤;-1--寫入錯誤;>0--更新成功 107 ** 108 ** 作 者: 109 ** 日 期: 110 **----------------------------------------------------------------------------- 111 ******************************************************************************/ 112 int8_t fatfs_Add_Historical_Data(void) { 113 FIL historical_data; 114 char node_type[2][7] = {"Zigbee", "LoRa"}; 115 char test_result[2][8] = {"Fail", "Success"}; 116 int8_t result = 0; 117 118 result = f_open(&historical_data, "0:histdat.csv", FA_OPEN_APPEND | FA_WRITE);//指針指向文件尾 119 if(result == FR_OK) { 120 result = f_printf(&historical_data, \ 121 "%u,%s,%s,#%08x%08x%08x,%04u/%02u/%02u,%02u:%02u:%02u\r\n", \ 122 pg_History_Data->index, node_type[pg_History_Data->type], \ 123 test_result[pg_History_Data->result], pg_History_Data->id[0], \ 124 pg_History_Data->id[1], pg_History_Data->id[2], pg_History_Data->year,\ 125 pg_History_Data->month, pg_History_Data->day, pg_History_Data->hour, \ 126 pg_History_Data->min, pg_History_Data->sec); 127 } else { 128 result = -2; 129 } 130 f_close(&historical_data); 131 // free(&historical_data); 132 133 return result; 134 }
- FatFs的基礎知識
待優化的問題:
- 文件頁表區、目錄表區對應扇區的擦寫次數必然遠大於數據區,等到文件頁表區、目錄表區擦寫次數超限后,數據區仍能繼續擦寫很多次。考慮加入平衡擦寫功能。
調試修改
詳見我的另一篇博客《stm32--FatFs調試過程(SPIFlash)》。
后續優化
- 將一個文件設為隱藏,以免使用USB功能時被修改:
- FF_USE_CHMOD設為1:開啟元數據控制功能(允許更改文件/目錄的屬性、時間戳)
- 通過f_chmod("0:statdat.bin", AM_HID, AM_HID); 將此文件設為隱藏
-
第二個參數:要設置的屬性,對應第三個參數中的屬性。
-
第三個參數:要變更的屬性。如果是第二個參數中存在的屬性,就設置這個屬性;如果是第二個參數中不存在的屬性,就清除這個屬性。
- 添加文件時間戳功能,創建/修改文件時記錄操作時間:
- FF_FS_NORTC設為0:開啟文件時間戳功能
- 在diskio.c中添加函數get_fattime,用於獲取當前RTC時間
- 返回值是32位無符號數,31-25位為當前年份與1980的差值;24-21位為月;20-16為日;15-11為時;10-5為分;4-0為秒除以2。
-
1 DWORD get_fattime (void) { 2 extern t_g_Statistical_Data *pg_User_Data; 3 DWORD fat_time = 0; 4 5 fat_time += (pg_User_Data->year - 1980) << 25; 6 fat_time += pg_User_Data->month << 21; 7 fat_time += pg_User_Data->day << 16; 8 fat_time += pg_User_Data->hour << 11; 9 fat_time += pg_User_Data->min << 5; 10 fat_time += pg_User_Data->sec / 2; 11 12 return fat_time; 13 }
- 在diskio.h中添加get_fattime函數聲明