# Emulated EEPROM using on-chip flash memory based on STM32
**References : [ST AN4894](https://www.st.com/resource/en/application_note/an4894-how-to-use-eeprom-emulation-on-stm32-mcus-stmicroelectronics.pdf),[STM32内部Flash使用磨损均衡算法(Erase Leveling)](https://blog.csdn.net/jiasike/article/details/100537362),[STM32学习笔记一一FLASH 模拟 EEPROM](https://blog.csdn.net/wwt18811707971/article/details/83662769)**
<font color=red><sup>*</sup>*本篇筆記是基於STM32G030系列的MCU架構討論*</font>
## Introduction
由於 STM32 chip 本身並無 EEPROM 元件,所以如果想在<font color=red>不外接任何元件及增加開銷成本的前提下</font>實時紀錄 updatable application data,並且不會因系統發生電源故障或斷電造成資料損失,就需要藉由 emulation EEPROM 的技術實現。
本篇應用筆記將圍繞著此主題深入探討及實作,分為以下幾個部分:
* EEPROM 與 flash memory 的物理特性
* ST 官方所提供的 firmware package 實現 emulation EEPROM
* 網路上的 open source 所提供的輕量級 emulation EEPROM
透過以上兩種方式都能夠實現 emulation EEPROM base on flash memory 的技術,並且各有優缺點。希望藉由本篇筆記能夠讓自己/大家更了解這項技術,未來引用到自己的專案中!
### EEPROM 和 Flash 的差別 ?
EEPROM(Electrically Erasable Programmable Read Only Memories)用於可更新的非揮發性存儲,或在複雜系統發生電源故障時保留少量資料;至於flash memory相信大家都不陌生,這裡不多贅述。
:::info
#### 兩者的物理特性比較
1. 寫入方式
EEPROM :
可以逐字節(byte-level)寫入,並且在每次寫入前不需要先擦除,可直接覆蓋先前資料;這意味著它允許更精確的操作,適合頻繁小資料的更新。
Flash Memory:
必須以區塊(block-level)方式進行擦除和寫入。通常一個區塊包含數千個字節,這意味著如果需要改變單一位元,整個區塊需要擦除後重新寫入。
2. 耐久性:
EEPROM:
擁有較高的寫入/擦除循環耐久性,通常可以承受約 100,000 至 1,000,000 次的寫入操作。
Flash:
相對而言,Flash 的寫入/擦除循環耐久性較低,約 10,000 至 100,000 次。
3. 存取速度:
EEPROM:
通常寫入速度較慢,但可以單獨擦除和重寫特定字節,適合少量資料的持久存儲。
Flash:
擁有較快的擦除速度,特別適合大容量的資料存儲,例如固態硬碟(SSD)。
4. 容量:
EEPROM:
通常容量較小,適合用來存儲較少的數據,如設定參數或校準數據。
Flash:
容量較大,適合儲存大量資料,例如程式碼、圖片等。
:::
為了降低成本,只要使用特定的軟體演算法,就可以用 on-chip flash memory 取代 external EEPROM。
### AN4894 - How to use EEPROM emulation on STM32 MCUs
此文件介紹了官方的軟體解決方案(X-CUBE-EEPROM),透過使用STM32 on-chip flash memory 來模擬 EEPROM 機制,以取代獨立的EEPROM。除此之外,文中也包含了很多的延伸知識,也會一併整理提出。
#### EEPROM emulation driver following feauture
文件一開始就統整了firmware package所有的特點,其中對此專案最重要的不外乎是 : <span style="background-color: yellow; color: red;">輕量化的實現、提供已包裝好的API供使用、最少只需兩個 pages、Wear-Leveling 演算法</span>。

#### Main differences between external and emulated EEPROM
接著,文件中提到外接EEPROM及模擬EEPROM的差異,我們專注一項資訊即可

可以看到在 write time 的部分,模擬所需的一個word編程時間遠小於外接EEPROM,這是因為flash的操作速度遠勝於EEPROM。
:::info
:book: AN4894 : page 22 提到
<span style="background-color: yellow; color: red;">The typical write time is close to the minimal write time as there is no data transfer in most cases.</span> The maximum value refers to a write operation that generates a data transfer.
:::
#### Implementing EEPROM emulation
在開始討論實作前,先來聊聊整個架構的核心概念

如上圖,可以看到 flash 其中一段記憶體位置被分成兩組 page set,一組 set 中又包含兩頁 pages。
當我們寫入新的 data,第一組的 pages 會先負責儲存這些 data,等到第一組的兩個 pages 都存滿之後,第二組的 pages 就需要從第一組 pages 中接收有效(最新)資料,接著第一組 pages 中存放的資料就可以全部刪除,並且第二組 pages 繼續做儲存 data 的動作,以此類推形成一個2 set 交互使用的效果。
#### Status element format
了解了基本運作原理後,不難發現要做到這樣的效果,就必須知道每個pages當前的狀態,於是我們犧牲一些空間,在每頁 page 前4個 element(8 bytes * 4 = 32 bytes)存放狀態:
* ERASED: the page is empty (initial state).
* RECEIVE: the page used during data transfer to receive data from other full pages.
* ACTIVE: the page is used to store new data.
* VALID: the page is full. This state does not change until all valid data is completely transferred to the receiving page.
* ERASING: valid data in this page has been transferred. The page is ready to be erased.
並且每個狀態的四個 element 裡面長這樣
```c
/*
* header1 | header2 | header3 | header4
* 0xAAAA AAAA AAAA AAAA => ERASING
* 0xAAAA AAAA AAAA FFFF => VALID
* 0xAAAA AAAA FFFF FFFF => ACTIVE
* 0xAAAA FFFF FFFF FFFF => RECEIVE
* 0xFFFF FFFF FFFF FFFF => ERASED
*/
```
:::info
:point_right:**為什麼存放狀態的 elements 要這樣設計?**
不管是未使用過或被擦除過的 flash memory 空間,<mark>存放的值都是```0xFF```,也就是預設值</mark>,而上面提到的initial state(ERASED)正是全為```0xFF```的組合,
並且整個運作流程為:```ERASED->RECEIVE->ACTIVE->VAILD->ERASING```,即 element 依序寫入```0xAAAA```。
:::
#### Data element format
討論完存放 page 狀態的 elements 後,我們可以接著討論存放 data 的 elements

如上圖所示,可以看到除了存放 data 本身,還需要存放虛擬位址和 CRC 驗證碼才能構成完整的 format,由此可知原廠的實作是非常嚴謹的。
:::info
The driver requires the virtual address values to be between ```0x0001```
(```0x0000``` corresponds to an EEPROM element invalidated by the driver) and ```0xFFFE```, cannot exceed ```0xFFFE``` (```0xFFFF``` corresponds to an erased flash memory line).
:::
以上我們對 emulation EEPROM 技術有了基本的理解,接著來一探究竟官方提供的 firmware package API ...
## STM32 offical firmware package
從此章開始,我們不再討論過多理論,著重在API的功能實現。
software package中包含幾個檔案:
* ```eeprom_emul.c``` : contains the EEPROM emulation firmware functions that can be called from the user program.
* ```flash_interface.c``` : contains the functions needed to handle the STM32 flash specific features.
* ```eeprom_emul.h``` : definitions about page and element.
* ```eeprom_emul_conf.h``` : configuration of eeprom emulation.
* ```eeprom_emul_types.h``` : contains all the functions prototypes for the EEPROM emulation.
* ```flash_interface.h``` : contains all the functions prototypes for the EEPROM emulation.
下面我會介紹使用者較需注意的巨集以及函式做說明
#### 1. ```eeprom_emul_conf.h```
這個標頭檔中存放使用者可以自定義的巨集:
* ```START_PAGE_ADDRESS``` : Start address of the 1st page in flash, for EEPROM emulation.
* ```CYCLES_NUMBER``` : The number of kcycles, for the equivalent EEPROM endurance.
* ```GUARD_PAGES_NUMBER``` : Number of guard pages used to reduce pressure on flash memory. This number has to be even. <font color=red>No guard page is necessary if the number of emulated variables leaves significant room in the ACTIVE page.</font>
* ```NB_OF_VARIABLES``` : Number of nonvolatile elements.
#### 2. ```eeprom_emul.c```
- [ ] ```EE_init```
```c
/**
* @brief Restore the pages to a known good state in case of power loss. **在斷電時將PAGE恢復到已知的良好狀態
* If a page is in RECEIVE state, resume transfer. **receive狀態為正在接收vaild變數(尚未完成)
* Then if some pages are ERASING state, erase these pages. **把狀態ERASING的page擦除
* @param EraseType: Type of erase to apply on page requiring to be erased.
* This parameter can be one of the following values:
* @arg @ref EE_FORCED_ERASE pages to erase are erased unconditionnally
* @arg @ref EE_CONDITIONAL_ERASE pages to erase are erased only if not fully erased
* @retval EE_Status
* - EE_OK in case of success
* - EE error code in case of error
*/
```
初始化可分為8個部分:
1. 對 CRC peripheral 做初始化設定。
2. 讀取 emulation EEPROM 的每個 pages 的每個 element,以刪除那些可以通過 NMI 檢測到的損壞 element。
3. 處理沒有任何一頁page處於 active 的情況(可能是在transfer page 時 reset 導致),重設 last valid page 的後一頁 page 為 receive page;如果同時沒有 active, receive, valid page 則需要格式化。
4. 處理在 transfer page 時 reset 導致的轉換不完全,遍歷每頁 page 尋找有無 receive page,有的話代表轉換被中斷了,重啟頁面轉換。
5. 驗證當前的 page 中有一個 active page。
6. 初始化當前的 active page 的 element 數以及下一個可寫入 data 的位址。
7. Step 5僅記錄當前 active page 的 element 數,這裡將記錄在 emulation EEPROM 中所有的 element。
8. 確保所有 erased page 確實被擦除。
9. 進行一次虛擬寫操作來避免數據不穩定,並處理重置期間可能導致的問題。
- [ ] ```EE_Format```
```c
/**
* @brief Erases all flash pages of eeprom emulation, and set first page
* header as ACTIVE. //就是格式化的意思,一頁一頁erase並檢查
* @note This function can be called the very first time eeprom emulation is
* used, to prepare flash pages for eeprom emulation with empty eeprom
variables. It can also be called at any time, to flush all eeprom
* variables. //如果使用到的EE區塊原本有資料覆蓋,可呼叫此API格式化!
* @param EraseType: Type of erase to apply on page requiring to be erased.
* This parameter can be one of the following values:
* @arg @ref EE_FORCED_ERASE pages to erase are erased unconditionnally
* @arg @ref EE_CONDITIONAL_ERASE pages to erase are erased only if not fully erased
* @retval EE_Status
* - EE_OK: on success
* - EE error code: if an error occurs
*/
```
如果我們深入查看此API內的運作,不難發現負責page erase動作的函式為```FI_PageErase()```
其中結構體```FLASH_EraseInitTypeDef```用來存放擦除操作的相關資訊 (擦除類別、擦除起始頁、擦除頁數),再透過HAL層API```FLASH_PageErase()```對 flash control register 操作。
```c=588
void FLASH_PageErase(uint32_t Banks, uint32_t Page)
{
uint32_t tmp;
/* Check the parameters */
assert_param(IS_FLASH_BANK(Banks));
assert_param(IS_FLASH_PAGE(Page));
/* Get configuration register, then clear page number */
tmp = (FLASH->CR & ~FLASH_CR_PNB);
/* Prevent unused argument(s) compilation warning */
UNUSED(Banks);
/* Set page number, Page Erase bit & Start bit */
FLASH->CR = (tmp | (FLASH_CR_STRT | (Page << FLASH_CR_PNB_Pos) | FLASH_CR_PER));
/** FLASH_CR_PNB:頁碼字段,用來指定要擦除的頁面。
* FLASH_CR_PER:頁面擦除使能位,用來表示這次操作是頁面擦除。
* FLASH_CR_STRT:啟動位元,寫入1後將開始擦除操作。
**/
}
```
:::info
前綴詞 FI = Flash Interface,代表實際操作flash層面的函式。
:::
- [ ] ```EE_WriteVariableXbits```
```c
/**
* @brief Writes/updates variable data in EEPROM.
* Trig internal Pages transfer if half of the pages are full.
* //如果page滿了呼叫transfer page
* @warning This function is not reentrant
* @param VirtAddress Variable virtual address on 16 bits (can't be 0x0000 or 0xFFFF)
* // 使用者自訂virtual address
* @param Data 16bits data to be written
* @retval EE_Status
* - EE_OK: on success
* - EE_CLEANUP_REQUIRED: success and user has to trig flash pages cleanup
* - EE error code: if an error occurs
*/
```
這個API有幾個有趣的地方值得一談:
```c=1226
/* Write the variable virtual address and value in the EEPROM, if not full */
status = VerifyPagesFullWriteVariable(VirtAddress, Data);
if (status == EE_PAGE_FULL)
{
/* In case the EEPROM pages are full, perform Pages transfer */
return PagesTransfer(VirtAddress, Data, EE_TRANSFER_NORMAL);
}
/* Return last operation status */
return status;
}
```
首先,```VerifyPagesFullWriteVariable()```包辦了非常的多事,包括:
1. 檢查 page 是否已滿
2. CRC驗證碼
3. 將 virtual address + CRC + data 包成format寫入
再來,真正把 data 寫入的函式為```stm32g0xx_hal_flash.c```中的
```c=656
static void FLASH_Program_DoubleWord(uint32_t Address, uint64_t Data)
{
/* Set PG bit */
SET_BIT(FLASH->CR, FLASH_CR_PG); //control register, PG = Flash memory programming enable.
/* Program first word */
*(uint32_t *)Address = (uint32_t)Data;
/* Barrier to ensure programming is performed in 2 steps, in right order
(independently of compiler optimization behavior) */
__ISB();
/* Program second word */
*(uint32_t *)(Address + 4U) = (uint32_t)(Data >> 32U);
}
```
:::warning
需要注意的是,如果 Flash 頁面編程模式(如 STM32 中的編程操作)是基於 64 位元的操作,那麼每次編程操作可能會期望一次寫入 8 bytes 的資料。如果只寫入 4 bytes,則需要檢查 Flash 設備的具體規範,<font color=red>確保這樣的操作不會產生意外的結果,比如無法正確寫入資料或破壞頁面的其餘部分。</font>
:::
- [ ] ```EE_ReadVariableXbits```
```c
/**
* @brief Returns the last stored variable data, if found, which correspond to
* the passed virtual address //遍歷整頁page,回傳最後更新值
* @param VirtAddress Variable virtual address on 16 bits (can't be 0x0000 or 0xFFFF)
* @param pData Variable containing the 16bits read variable value
* @retval EE_Status
* - EE_OK: if variable was found
* - EE error code: if an error occurs
*/
```
可以發現,不論是```EE_ReadVariableXbits```或```EE_WriteVariableXbits```都會使用到CRC驗證機制,可見官方的防錯機制完善。
```c=1158
/* Compare the read address with the virtual address */
if (EE_VIRTUALADDRESS_VALUE(addressvalue) == VirtAddress)
{
/* Calculate crc of variable data and virtual address */
crc = CalculateCrc(EE_DATA_VALUE(addressvalue), EE_VIRTUALADDRESS_VALUE(addressvalue));
/* if crc verification pass, data is correct and is returned.
if crc verification fails, data is corrupted and has to be skip */
if (crc == EE_CRC_VALUE(addressvalue))
{
/* Get content of variable value */
*pData = EE_DATA_VALUE(addressvalue);
return EE_OK;
}
}
```
- [ ] ```CleanUp```
```c
/**
* @brief Erase group of pages which are erasing state, in polling mode.
* Could be either first half or second half of total pages number.
* // 擦除一個set的page,且必須要是處於erasing狀態。意即當一個set的page都寫
滿,將valid data轉到另一個set的first page後,即可呼叫此API擦除
pages of set。
* @note This function should be called when EE_WriteVariableXXbits has
* returned EE_CLEANUP_REQUIRED status (and only in that case)
* @retval EE_Status
* - EE_OK: in case of success
* - EE error code: if an error occurs
*/
```
與```EE_format```相同,負責 page erase 動作的函式為```FI_PageErase()```。
- [ ] ```CleanUp_IT```
```c
/**
* @brief Erase group of pages which are erasing state, in IT mode.
* Could be either first half or second half of total pages number.
* @note This function should be called when EE_WriteVariableXXbits has
* returned EE_CLEANUP_REQUIRED status (and only in that case)
* @retval EE_Status
* - EE_OK: in case of success
* - EE error code: if an error occurs
*/
```
操作跟```CleanUp```幾乎一樣,唯一的差別是多設置了 control register interrupt bit。
在```HAL_FLASHEx_Erase_IT()```中
```c=259
/* Enable End of Operation and Error interrupts */
FLASH->CR |= FLASH_CR_EOPIE | FLASH_CR_ERRIE;
/** FLASH_CR_EOPIE: 中斷始能位,當 Flash 操作(如擦除、寫入)成功完成時,會觸發這個中斷。
* FLASH_CR_ERRIE: 中斷始能位,當 Flash 操作過程中出現錯誤(如寫入保護、非法地址操作等),會觸發這個中斷。
```
這樣的設置可以讓 CPU 不必輪詢操作狀態,而是通過中斷處理完成或錯誤事件,從而提高系統的效率和可靠性。
- [ ] ```EE_DeleteCorruptedFlashAddress```
```c
/**
* @brief Delete corrupted Flash address, can be called under NMI.
* @param Address Address of the FLASH Memory to delete
* @retval EE_Status
* - EE_OK: on success
* - EE error code: if an error occurs
*/
```
針對已損壞的element,透過 NMI 刪除,這邊的刪除其實是指[<font color=red><sup>**</sup>對該element再次寫入0標記。</font>](#note-1)
:::info
:point_right:**NMI(Non-Maskable Interrupt,不可屏蔽中斷):**
是一種特殊的高優先級的硬體中斷,它無法被程式或操作系統屏蔽或忽略。NMI 通常用於處理緊急的、關鍵性的事件,這些事件需要立即響應,無論當前系統正在處理什麼其他任務。通常用在
* 看門狗計時器
* 內存錯誤檢測
:::
## EEPROM Emulation lite version
在了解完官方的 firmware package API 實作後,可以發現作法雖然完善且嚴謹,但也帶來一些 trade off。在專案中最明顯感受到的就是 package 的 code size 非常大 (約10KB),這是由於官方提供了諸多的功能及繁瑣的機制,包含:
* CRC 驗證寫入資料可靠性。
* page 的初始化過程。
* page 轉換的過程。
* 詳細(層層)的 API 包裝。
* (對於只需紀錄少數data的情況)virtual address。
那麼,我們能夠在不破壞官方方法完整性的前提下簡化實作 EEPROM emulation 嗎? 答案是可以的! 本章節我將參考 open source 來實作一個輕量級的 EEPROM emulation 技術。
### Principle
#### feature
* 無繁瑣的CRC碼機制,而是透過標頭及結尾驗證資料完整性。
* 每頁開頭(32 bytes)不再有狀態項,也不再把 pages 分為兩個 set,而是一次寫完所有 pages 再進行擦除。
* 每個變數不再有對應的 virtual address,而是把全部變數存在同一次寫入(適合需紀錄少量變數的情況)。
#### spec
本章節以VIOLET美容儀專案所使用的MCU做探討及實作
* MCU : STM32G030C8T6
* Software : STM32CubeHAL
* Flash size : 64 KB
* Flash page size : 2 KB
* Total Flash page used : 2 page (page 30, 31)

#### Data format
接著,規範欲寫入的 Data Format,之後每次寫入都須遵照此格式
```c
/* 0x5A(header) and 0xA5(ending) are the label about
* current vaild data.
* X is the data space, which mean you can set this
* part up to your requirment.
*/
|0x5AXX|0xXXXX|0xXXXX|0xXXA5|
```
<a id="note-2"></a>
:::info
data space並不一定只限制存放一個變數,且因為沒有virtual address對應不同變數,等同於共有6個bytes的空間給所需存取的所有變數使用。
:::
#### How it works
1. 對於<font color=red>初次使用的空間</font>做格式化,確保擦除他先前遺留下來的 data。<font color=red>該動作只需在第一次做,之後都不需再做。</font>
2. 每次寫入 data 時,會依序尋找```0xFF```值(未寫入數據)的 index,找到後寫入 data [<font color=red><sup>**</sup>並將前一個有效數據設置為```0x00```。</font>](#note-1),若未找到```0xFF```表示所有 page 已寫滿,擦除並從頭開始寫入。
3. 每次讀出 data 時,會依序尋找```0x5A```值(有效數據)的 index,然後再依使用者的 data space 存放方式讀出各別變數。
4. 每次寫入 data 後都會順便驗證剛寫進去的值是否相同,如果連續三次出現寫入不符,則判斷flash memory使用壽命結束,停用該 pages 所有操作。
:::info
:book: <a id="note-1">**flash 只能寫入一次值?**</a>
相信很多初次接觸 flash 的人都會跟我有一樣的誤解 - flash 只能寫入一次值,之後就必須擦除才能重新寫入新值。這個理解沒錯,但也不全然正確! 事實上,維基百科上提到
> ["This generally sets all bits in the block to 1. Starting with a freshly erased block, any location within that block can be programmed. However, once a bit has been set to 0, only by erasing the entire block can it be changed back to 1."
> "For example, a nibble value may be erased to 1111, then written as 1110. Successive writes to that nibble can change it to 1010, then 0010, and finally 0000. Essentially, erasure sets all bits to 1, and programming can only clear bits to 0."](https://en.wikipedia.org/wiki/Flash_memory)
所以應該要說得更嚴謹一點 - flash 只能把 bit ```1``` 設成 ```0```,如果想要把```0```變回```1```就需要 erase 整個 page。
:::
### Implement
輕量版的 EEPROM emulation 包含以下幾個檔案
* ```Flash_WL.h``` : configuration of eeprom emulation and data format.
* ```Flash_WL.c``` : contains the EEPROM emulation API that can be called from the user program.
下面我會介紹使用者較需注意的巨集以及函式做說明
#### 1. ```Flash_WL.h```
- [ ] ```EE_XXX_Mask```, ```EE_XXX_Pos```
```c
#define EE_BD_Pos 0xFF000000 /* position of the data */
#define EE_EMS_Pos 0xFF00000000
#define EE_BD_Mask 24U /* mask of the data */
#define EE_EMS_Mask 32U
```

這兩個巨集是儲存的 data 在 data format 中的相對位置跟遮罩,目的是為了從 format 中取值方便,使用者應依自己需求定義。
- [ ] ```flash_pack_u```
```c
typedef union
{
uint64_t data;
uint8_t data_quarter[8];
// for violet case, we need to take the data of index 3, 4
}flash_pack_u;
```
union 型態中的變數共享記憶體位置,這樣不但省記憶體空間,也可以對 data format 以 byte 為單位做操作。
- [ ] ```flasher_t```
```c
typedef struct
{
uint32_t start_addr; //should be fixed
uint32_t cur_addr;
uint32_t new_addr; //address of not used element
uint16_t page_size; //total page size
flash_pack_u buff; //to store data format
}flasher_t;
```
#### 2. ```Flash_WL.c```
- [ ] ```erase_flash()```
```c
void erase_flash(void)
{
HAL_FLASH_Unlock();
for (int idx = PAGE_START_INDEX; idx <= PAGE_END_INDEX; idx++)
{
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | FLASH_FLAG_PGSERR | FLASH_FLAG_PGAERR | FLASH_SR_OPERR);
//Eed of Operation | Write Protection Error | Programming Sequence Error | Programming Alignment Error
FLASH_PageErase(USELESS_BANK1, idx); // the first param is useless (for 1 bank case)
FLASH_WaitForLastOperation(FLASH_TIMEOUT_VALUE); // if flash operation execute more than 1 sec, return error
/* If operation is completed or interrupted, disable the Page Erase Bit */
CLEAR_BIT(FLASH->CR, FLASH_CR_PER);
}
HAL_FLASH_Lock();
}
```
:::info
:point_right: **Flash status register**
EOP : Flash memory 操作(program / erase)成功時被設置。
WRPERR : 當要寫入的位置屬於寫入保護區域時被設置。
PGSERR : 編程序列不正確時被設置,如 page 先未擦除就想再次寫入。
PGAERR : 當寫入的資料不能與 double word(64-bit) 對齊被設置。
OPERR : Set Flash memory 操作(program / erase)未成功時被設置。
:::
- [ ] ```find_new_entry()```
```c
void find_new_entry(void)
{
while(flasher.cur_addr < (flasher.start_addr + flasher.page_size))
{
flasher.buff.data = read_Dbword_from_flash(flasher.cur_addr);
if(flasher.buff.data_quarter[0] == 0xff)
{
flasher.new_addr = flasher.cur_addr;
return;
}
flasher.cur_addr += EE_ELEMENT_SIZE; // format = 8bytes
}
if(flasher.cur_addr >= (flasher.start_addr + flasher.page_size))
{
erase_flash();
flasher.cur_addr = flasher.start_addr;
flasher.new_addr = flasher.start_addr;
}
}
```
藉由指針```flasher.cur_addr```依序尋找未使用過的element,找到的話不回傳值(```flasher.cur_addr```已被更新成指向未使用的element);找不到的話代表 pages 滿了,執行```erase_flash()```。
- [ ] ```find_used_entry()```
```c
uint32_t find_used_entry(void)
{
while(flasher.cur_addr < (flasher.start_addr + flasher.page_size))
{
flasher.buff.data = read_Dbword_from_flash(flasher.cur_addr);
if (flasher.buff.data_quarter[0] == 0x5A && flasher.buff.data_quarter[7] == 0xA5)
return flasher.cur_addr;
flasher.cur_addr += EE_ELEMENT_SIZE;
}
return 0;
}
```
藉由指針```flasher.cur_addr```依序尋找存放有效值的element,找到的話回傳```flasher.cur_addr```;沒找到 return ```0```。
:::info
可以注意到```find_used_entry()```跟```find_new_entry()```都沒有把```flasher.cur_addr```指回```flasher.start_addr```從頭開始尋找,這是因為```flasher.cur_addr```永遠都會被更新到當前存放有效值的 element。這樣一來可以省去遍歷整個 pages 的時間。
:::
- [ ] ```write_Dbword_to_flash()```
```c
/**
* @brief write the format and data into element of page, and set previous element to all 0 (0x00 00 00 00 00 00 00 00).
* @param writer contain the format | 0x5A | (optional) | (optional) | data1 | data2 | (optional) | (optional) | 0xA5 | total 8 bytes
* @retval None
*/
void write_Dbword_to_flash(void)
{
/* check if data was updated */
if ( (BD_Mode ^ buff.data_quarter[3]) || (EMS_Level ^ buff.data_quarter[4]))
{
buff.data_quarter[3] = BD_Mode;
buff.data_quarter[4] = EMS_Level;
find_new_entry();
HAL_FLASH_Unlock();
if (flasher.new_addr-8 >= flasher.start_addr)
{
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, flasher.new_addr-EE_ELEMENT_SIZE, 0x00);
}
HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, flasher.new_addr, buff.data);
/*hand check if the write process is actually successful (flash w/r endurance)*/
if (read_Dbword_from_flash(flasher.new_addr-EE_ELEMENT_SIZE) != 0x00 || read_Dbword_from_flash(flasher.new_addr) != buff.data)
{
DB_printf("\n\rerror occur\n\r");
WriteErrorCount++;
//if (WriteErrorCount >= 3) flash_damaged_message();
}
else WriteErrorCount = 0;
HAL_FLASH_Lock();
}
}
```
先檢查當前欲存儲的變數有無更新,如果有更新才寫入。每次寫入後都會再讀值一次確保存進去的 data 無損壞,如果連續三次發現 data 損壞視為 flash memory 達使用壽命,禁用相關操作。
- [ ] ```read_Dbword_from_flash()```
```c
uint64_t read_Dbword_from_flash(uint32_t target_addr)
{
flasher.buff.data = *(uint64_t* )target_addr;
return flasher.buff.data;
}
```
### Results and comparison
此章節比較兩種不同的作法,對於flash使用壽命的增加,優缺點以及最後的code size大小
| | 無引入 EEPROM emulation | ST官方 EEPROM emulation | 開源 EEPROM emulation |
|----| --------- | -------- | -------- |
|使用壽命| 約可寫入1000次 | 約可寫入504000次 | 約可寫入512000次 |
|擴充程式大小| 無須擴充(約52 Kbytes)|約 64 Kbytes|約 53 Kbytes
|優點|不用增加code size|做法嚴謹,適合需儲存較多個變數的情況|code size僅 1 KBytes,使用壽命延長最多|
|缺點|只可寫入1000次變數,浪費flash page|code size太大|機制較少,只適合儲存1~2個變數的情況|
## Maintenance and optimization
本章節以開源 EEPROM emulation lite 做後續討論,套用一些演算法or機制對程式碼做進一步時間/空間優化。
### 如何判斷頁損壞?
在前一章,可以發現當連續出現三次同一個 element 寫入跟讀取的值不符,會呼叫```flash_damaged_message()```,這個 function 的作用就是將```EE_flag```設置起來並且寫入已損壞的 start page 的上一個未損壞的 element(假設start address = ```0x0800F000```,```EE_flag```就會被寫入記憶體位置```0x0800EFF8```),但這樣的作法最終導致額外用到一個page的情況。
#### <font color=red>那麼,有辦法從 format 裡面動手腳嗎?</font>
一開始我的理解是沒有辦法,因為如果 page 已經損壞,那麼如何驗證```EE_flag```被寫入後能夠跟我們預期的一樣?
:::info
:book: **Flash 存儲特性**
1. 寫入操作:Flash 記憶體的寫入操作只能將 1 改為 0。因此,在 Flash 記憶體中寫入新的數據,只能覆蓋先前的 1 值(高電壓)為 0 值(低電壓)(將電荷寫入特定的 Flash 單元)。這個操作比較可靠,即使 page 有部分損壞。
2. 擦除操作:將 bit 0 復位成 1 通常需要擦除整個 page,這個操作會將所有 bit 復位為 1(即 0xFF)。在 page 有損壞的情況下,擦除操作可能無法成功,因為該操作需要釋放電荷,而損壞的 Flash 單元可能無法完全清除電荷,導致無法達到預期的 1 狀態。
:::
在得知上述特性後,萌生了一個想法 - 如果將 format 的結尾```0xA5```當作一個標記,當發生 page 損壞,就把```0xA5```寫入```0x00```,<font color=red>就可以避免多一頁page的使用,也不必擔心寫入的值跟我們預期不同。</font>
```c
| 0x5A | (optional) | (optional) | data1 | data2 | (optional) | (optional) | 0xA5 | --> page work
| 0x5A | (optional) | (optional) | data1 | data2 | (optional) | (optional) | 0x00 | --> page broken
```
### 如何查找被使用的element?
```find_used_entry()```用來尋找被使用的 element (同一時間只會有一個),但其缺點是每次都要從頭一個一個搜尋下來,如果今天被使用的 element 剛好在page的底部,就會花費相對久的時間 (時間複雜度$O(n)$)。
[觀察page的結構](#note-2)可以發現,整個 list 其實是類排序的結構 ! 可以用 binary search 增加尋找效率(時間複雜度$O(\log n)$)。
```c
uint32_t find_used_entry(void)
{
/* for the case which MCU first start up*/
flasher.buff.data = read_Dbword_from_flash(flasher.start_addr);
if (flasher.buff.data_quarter[0] == 0xFF) return 0;
/* binary search */
uint32_t left_addr = flasher.start_addr;
uint32_t right_addr = flasher.start_addr + flasher.page_size;
uint32_t mid_addr = 0;
while(left_addr <= right_addr)
{
mid_addr = left_addr + (right_addr - left_addr) / 2;
mid_addr -= (mid_addr % EE_ELEMENT_SIZE);
flasher.buff.data = read_Dbword_from_flash(mid_addr);
if (flasher.buff.data_quarter[0] == 0x00) left_addr = mid_addr + EE_ELEMENT_SIZE;
else if (flasher.buff.data_quarter[0] == 0xFF) right_addr = mid_addr - EE_ELEMENT_SIZE;
else break;
}
if (flasher.buff.data_quarter[0] == 0x5A && flasher.buff.data_quarter[7] == 0xA5) // if data is valid
{
flasher.cur_addr = mid_addr + EE_ELEMENT_SIZE;
return mid_addr;
}
else if (flasher.buff.data != 0x00 && flasher.buff.data_quarter[7] == 0x00) // if page broken flag has been set
{
EE_flag = 1;
return 1;
}
return 0; // if no found
}
```