# 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硬體規格簡介

- 記憶體最小寫入單位為一個 **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**

- 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
假設初始記憶體狀態為:

這時若從0x02位置開始寫入1 Page的資料,資料並不會從0x02位置一路放置到0x101位置,而是會寫至256倍數位置後從Page起點wrap around(如下圖)

:::
- 輸入指令後須接續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。

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動作

從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擦除順序由低位置至高位置的循環。
**初始狀態**

**檔案刪除**

**新增檔案並寫入資料**

(執行garbage collect)

:::
### 與舊版本之間的差異
- **Wear leveling改進**

舊版本Wear leveling策略為四個sectors隨機交替使用,sector內堆放Disc information以及全部file information。

此版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方式改進**


上圖為舊版本實作方式,採用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。

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的速度上升。