# M4 file system 開發心得 ###### tags: `M4` ## 參考資料 - [W25Q128JV datasheet](https://datasheet.lcsc.com/szlcsc/Winbond-Elec-W25Q128JVFIQ_C111478.pdf) - [深入淺出SSD](https://www.books.com.tw/products/CN11545389) ## W25Q128JV硬體規格簡介 ![](https://i.imgur.com/5wPuQ1D.png) - 記憶體最小寫入單位為一個 **Page** (256 Bytes) - 最小擦除單位為一個 **sector** (4 KB) - 一個 **Block** 由 **16** 個 **sectors** 組成 - 整張flash由 **256** 個 **blocks** 組成。 :::warning 根據硬體時做不同分為NAND及NOR flash,在NAND FLASH下又分別有 SLC、MLC、TLC等不同技術。但不管為何種FLASH 其記憶體皆無法被複寫,**使用上皆須先擦除目標區域,再進行寫入**。 ::: :::info [NAND 與 NOR 簡易比較](https://b8807053.pixnet.net/blog/post/3611881) - NOR的讀速度比NAND稍快一些。 - NAND的寫入速度比NOR快很多。 - NAND擦除速度遠比NOR快。 - 大多數寫入操作需要先進行擦除操作。 - NAND的擦除單元更小,相應的擦除電路更少。 更多詳細比較請參考 :https://b8807053.pixnet.net/blog/post/3611881 ::: ## W25Q128JV操作簡介 :::info 此為通訊FLASH,所有操作皆藉由 **SPI** 傳達。 **以下只列出file system有用到的功能** ::: ### Read Status Register - 1(0x05) - 讀取第一 Byte的 Status register **Status Register - 1** ![](https://i.imgur.com/99Zn77s.png) - S0: BUSY 若正在執行 **Page program**、 **erase**等操作會set此旗標,完成後會自動clear - S1: WEL 執行**Page program**、 **erase**等操作前,會檢查此旗標,必須set才會執行。 - 其餘旗標未在file system中應用,欲知詳情請至規格書瞭解。 ### Write Enable(0x06) - 此為進行 **Page Program**、**Sector Erase**、**Chip Erase**前須先執行的指令。 - 執行後會set flash內special register的WEL旗標,進行上列操作前會檢查WEL旗標是否set才會執行指令。 ### Page Program(0x02) - 依次寫入至多一個Page的資料。 - **Page alignmet** :::warning 假設初始記憶體狀態為: ![](https://i.imgur.com/Mncc9Cm.png) 這時若從0x02位置開始寫入1 Page的資料,資料並不會從0x02位置一路放置到0x101位置,而是會寫至256倍數位置後從Page起點wrap around(如下圖) ![](https://i.imgur.com/MEiRcjl.png) ::: - 輸入指令後須接續3 bytes的address資訊才傳送資料。 ### Sector Erase(0x20) - 擦除一個sector (還原每個bit至1)。 - 通常花費45ms,至多花費400ms。 - 輸入指令後需接續3 bytes的address資訊。 ### Chip Erase(0xC7/0x60) - 擦除整張flash - 通常花費40s,至多花費200s ### Read Data(0x03) - 輸入指令後接續3 bytes的address資訊,之後master可任意控制要接收資料的長度,且長度上限沒有限制。 :::info 回應@cy023 大神的問題 (2023/02/26) Fast Read(0x0B) 基本上與Read Data指令相同,需注意的是Fast Read於address傳輸完畢後有1 Byte的dummy data。 ![AC Electrical Characteristic](https://i.imgur.com/44PH9EO.png) Read Data指令最高速度只到50MHz,Fast Read可達133MHz。 ::: ### Manufatcurer/Device ID(0x90) - 輸入指令後會有2 Bytes Dummy,之後便接續1 Bytes all-zeros,接著是Manufacturer ID以及Device ID(共2Bytes) - Manufacturer ID固定為 **0xEF** - Device ID固定為 **0x17** - 可用此指令檢查SPI是否斷線或是flash裝置是否正常執行。 ## Driver開發 ### is_flash_busy ```c= uint8_t is_flash_busy(void){ FLASH_CS_OPEN(); spi_swap(CMD_RD_SR1); uint8_t ret = spi_swap(0); FLASH_CS_CLOSE(); return ret & 1; } ``` 回傳status register1 的BUSY旗標,用於等待write、erase操作完成。 ### flash_write - 分析: 在此flash進行寫入操作需先致能寫入,並且在寫入過程需考慮page alignment問題 - 目標: 可以在不理解此硬體特性的情況下,直接呼叫函式 1. 解決page alignment ```c= void flash_write(uint32_t addr, uint8_t *data, uint16_t data_bytes){ uint8_t page_offset = addr & 0xff; uint16_t bytes; uint16_t idx = 0; if (!page_offset){ if (data_bytes > 255){ bytes = 256; }else{ bytes = data_bytes; } }else{ if (data_bytes > (256 - page_offset)){ bytes = 256 - page_offset; }else{ bytes = data_bytes; } } ... } ``` page alignment會引出以下兩個issues: > - 每次不能寫入超過1 page > - 不能跨page寫入 此兩個issues會帶出四個情境: > A. 寫入位置page align且寫入長度超過1 page > B. 寫入位置page align但寫入長度不大於1 page > C. 寫入位置non page align且寫入長度超過page剩餘空間 > D. 寫入位置non page align但寫入長度不大於page剩餘空間 對於case A只需要注意一次寫入不能超過1 page,因此前處理只需要告知接下來的程式碼將要燒錄1 page。 case B與case D最為簡單,只需要將實際的資料長度傳至接下來的程式碼即可。 case C先計算剩餘page空間,並將此值傳到接下來的程式碼。 2. 解決一次不能寫超過1 page的限制 ```c= void flash_write(uint32_t addr, uint8_t *data, uint16_t data_bytes){ ... while(data_bytes){ while (is_flash_busy()); FLASH_CS_OPEN(); spi_swap(CMD_WREN); FLASH_CS_CLOSE(); while (is_flash_busy()); FLASH_CS_OPEN(); uint8_t *add = (uint8_t *)&addr; spi_swap(CMD_WR); for (int i = 2; i >= 0; i--){ spi_swap(add[i]); } for (int i = 0; i < bytes; i++, idx++){ spi_swap(data[idx]); } FLASH_CS_CLOSE(); data_bytes -= bytes; addr += bytes; if (data_bytes > 255){ bytes = 256; }else{ bytes = data_bytes; } } while (is_flash_busy()); } ``` 此程式三個部分個別為: > 1. Write Enable操作 > 2. Page Program操作 > 3. 剩餘資料整理及address自動往後累加 > 因為在前處理就已先解決page alignment的問題,在此可以不必考慮位址。 ### flash_read ```c= void flash_read(uint32_t addr, uint8_t *data, uint16_t read_bytes){ uint8_t *add = (uint8_t *)&addr; FLASH_CS_OPEN(); spi_swap(CMD_RD); for (int i = 2; i >= 0; i--){ spi_swap(add[i]); } for (int i = 0; i < read_bytes; i++){ data[i] = spi_swap(0); } FLASH_CS_CLOSE(); } ``` 因為read部分操作較簡單且沒任何硬體限制,driver直接按照datasheet包裝即可。 ### flash_erase ```c= void flash_erase(void){ FLASH_CS_OPEN(); spi_swap(CMD_WREN); FLASH_CS_CLOSE(); uint8_t chk = 1; do{ chk = is_flash_busy(); } while(chk); FLASH_CS_OPEN(); spi_swap(CMD_CHIP_ER); FLASH_CS_CLOSE(); while (is_flash_busy()); } ``` ```c= void flash_sector_erase(uint32_t addr){ FLASH_CS_OPEN(); spi_swap(CMD_WREN); FLASH_CS_CLOSE(); uint8_t chk = 1; do{ chk = is_flash_busy(); } while(chk); uint8_t *add = (uint8_t *)&addr; FLASH_CS_OPEN(); spi_swap(CMD_SECTOR_ER); for (int i = 2; i >= 0; i--){ spi_swap(add[i]); } FLASH_CS_CLOSE(); while (is_flash_busy()); } ``` 按照規格指示,先write enable才進行擦除操作,並且檢查擦除完成後才離開函式。 ## File system 規劃 ### Disc information ```c= typedef struct{ const uint32_t err_chk; const uint8_t FileMax; uint8_t FileTotal; uint8_t freesector[508]; uint8_t gc_sector[508]; uint8_t filemanage[8 * 15]; } DiscStr_t; ``` - **err_chk**:不可修改值,作為檢查此裝置儲存資料是否有效的驗證碼,目前設定值為:0xaa55aa55。 - **FileMax**:不可修改值,檔案上限,目前設定值為14。 - **FilTotal**:runtime時期可改變,目前檔案總數。 - **freesector**:runtime時期可改變,每個bit代表著一個sector編號,1為空、0為佔有數據。 - **gc_sector**:runtime時期可改變,每個bit代表著一個sector編號,1代表未被trimmed、0代表已被trimmed。 - **filemanage**:所有檔案名稱所合併的一個矩陣,每八個字元代表著一個檔案名稱,檔案ID也由此決定。 ### File information ```c= typedef struct{ uint8_t FileName[8]; uint8_t FileID; uint8_t Month; uint8_t Date; uint8_t Hour; uint8_t Min; uint16_t linkedlist_len; uint16_t Year; uint16_t R_sectnum; uint16_t R_offset; uint16_t W_sectnum; uint16_t W_offset; uint32_t FileSize; } FileStr_t; ``` - **FileName**:提供8個字元作為檔案名稱 - **FileID**:由Disc info中分配得。 - **Year/Month/Date Hour:Min** :檔案最後修改時間,只有經過file_write操作後會於file_close更新。 - **linkedlist_len**:此檔案linkedlist長度變數。 - **R_sectnum、W_sectnum**:分別為當前Read sector位址及Write sector位址。 - **R_offset、W_offset**:分別為當前單一sector的Read bytes數與Write bytes數。 - **FileSize**:檔案大小,單位為Bytes。 ## File system API :::info 在使用下列API前,使用者應宣告Disc information變數,以及需求數量的file information空間。 API不會allocate變數空間。 ::: ### Disc Format ```c= void Disc_format(void); ``` chip erase後將Disc information燒錄指定位置。 ### Get Disc Information ```c= void Disc_info(DiscStr_t *DStr_p); ``` 存取指定位置的Disc information。 ### Disc Setup ```c= uint8_t Disc_set(DiscStr_t *DStr_p); ``` 使用者須準備Disc information的空間,以便函式存取。 先存取指定位置的Disc information,若檢查碼錯誤則自動進行Disc_format。 foramt過後再次檢查Disc information,若檢查碼仍然錯誤則**return 1**。 若檢查碼無誤則**return 0**。 ### File Open ```c= uint8_t FileOpen(DiscStr_t *DStr_p, FileStr_t *Fstr_p, char *f_name); ``` 函式會依照使用者提供的f_name在Disc info中進行尋找比對。 找到對應檔案:**return 0**。 未找到對應檔案而新增以f_name為名的檔案:**return 1**。 Disc information檢查碼錯誤 or 達到檔案上限 or flash sector空間用盡:**return 2**。 ### File Write ```c= uint8_t FileWrite(FileStr_t* Fstr_p, uint16_t Bytes, uint8_t* Data_p); ``` :::warning 限制: 不能一次寫入超過4096Bytes,累積寫入16個sectors後需要進行file close 詳細原因於 **** 章節進行說明 ::: 累積寫入16個sectors以上繼續呼叫此函式 or 單次寫入超過4096Bytes即**return 1**。 正常寫入**return 0**。 ### File Read ```c= uint8_t FileRead(FileStr_t* Fstr_p, uint16_t Bytes, uint8_t* Data_p); ``` :::warning 限制:單次讀取不能超過4096Bytes ::: 累積讀取超過檔案大小上限 or 單次讀取超過4096Bytes即**return 1**。 正常讀取**return 0**。 ### File Close ```c= uint8_t FileClose(DiscStr_t *DStr_p, FileStr_t *Fstr_p, uint16_t Year, uint8_t Month, uint8_t Date, uint8_t Hour, uint8_t Min); ``` 首先檢查free sector還剩下多少,若有file_write操作且空間不足時進行garbage collect動作,並執行file_sync。 若有sector空間異動則進行Disc information更新。 ### File Delete ```c= uint8_t FileDelete(DiscStr_t* DStr_p, char* f_name); ``` 先進行Disc information檢查碼比對,若比對錯誤則**return 1**。 於Disc information中搜尋f_name,若查無則**return 2**。 找到特定file後對該file占用的sector進行trim動作,結束後於Disc information中擦除此檔案名稱並**return 0**。 ## File system 設計理念 ### 名詞介紹 - **Allocate**:即安排空間。此file system分為兩種allocate情境,兩者的策略皆採用Find first fit。 > 1. file allocate:於Disc information中搜尋到第一個名稱欄位為空的位置,即安排至對應編號的file info儲存區。 > 2. sector allocate:於Disc information中搜尋到第一個標記為空的sector,標記為空後回傳此sector編號即可直接進行使用。 :::info 假設此使用狀態下進行allocate動作 ![](https://i.imgur.com/CQDSHKF.png) 從0x00開始尋找空位,並在0x01結束搜尋即為**find first** 反之,從0x04開始尋找並在0x04結束搜尋即為**find last** ::: - **Wear Leveling**:每個sector皆有擦除次數的上限,上限到達之後便無法再進行寫入動作,因此距離無法寫入之前的擦除次數便被定義為壽命(Wear)。進行壽命平衡的主要目的為:不希望某些特定區塊過度擦除,在使用上還需特別檢查哪裡有壞區。 - **Trim**:將file取得data的聯繫給斷開,並且不對data區域做擦除動作,就像是將linked list的head與後續linked nodes剪斷一樣因此叫做trim。 - **Garbage Collection**:在寫入特定sector前先進行擦除的動作即為garbage collection。 :::info 本file system的wear leveling策略為使用trim實作file delete動作,並配合garbage collection於空間不夠時再進行擦除,以及allocate空間的find first策略達成。以上幾個實作方式的互相作用下,能達成sector擦除順序由低位置至高位置的循環。 **初始狀態** ![](https://i.imgur.com/sObqNAj.png) **檔案刪除** ![](https://i.imgur.com/Mpihm8w.png) **新增檔案並寫入資料** ![](https://i.imgur.com/DKTFHef.png) (執行garbage collect) ![](https://i.imgur.com/JJ0PVPv.png) ::: ### 與舊版本之間的差異 - **Wear leveling改進** ![](https://i.imgur.com/LPsTdTX.png) 舊版本Wear leveling策略為四個sectors隨機交替使用,sector內堆放Disc information以及全部file information。 ![](https://i.imgur.com/Jpl0m3w.png) 此版file system將直接畫分特定block作為所有imformation存放區域,第0 sector作為disc information,其餘sectors為個別file information專用。 而**Block255**做為上述information儲存區,**Block254**與**Block253**分別做為information修改暫存區及file data修改暫存區。 **tradeoff**:檔案總數下降至14個,但將information sector的損耗分散開,不至於information sector損壞發生時資料無法救援。 - **Sector Allocate方式改進以及Data Access方式改進** ![](https://i.imgur.com/2DybCGG.png) ![](https://i.imgur.com/zwHixZJ.png) 上圖為舊版本實作方式,採用list紀錄空sector與使用中sector ```c= typedef struct{ const uint32_t err_chk; const uint8_t FileMax; uint8_t FileTotal; uint8_t freesector[508]; uint8_t gc_sector[508]; uint8_t filemanage[8 * 15]; } DiscStr_t; ``` 新版本改使用**freesector、gc_sector**等變數紀錄sector使用狀態,並以這些變數作為allocate sector的依據。 除此之外,新版file system還更新了data list管理方案: 如下圖,在各自的file sector中,最開始放置file information,之後便接續data個別存放於哪些sector的data list。 ![](https://i.imgur.com/aUxWzvs.png) data list中存放著node,負責記錄當前使用sector的資訊: ```c= typedef struct sector{ uint16_t sectornum; uint16_t offset; } sectorstr_t; //node ``` 除了存放用到的sector編號,還必須包含當前sector使用狀況---也就是已寫入多少長度的資料。 新增檔案時會自動建立一個node,直到file佔滿超過1個sector後才會再向file system 要求allocate一個新的sector並且開設新的node。 **tradeoff**:運行在使用端的disc information變數增大,但管理sector的速度上升。