大家好,我是痞子衡,是正經(jīng)搞技術(shù)的痞子。今天給大家?guī)?lái)的是痞子衡的個(gè)人小項(xiàng)目 - kFlashFile。
痞子衡最近在參與一個(gè)基于 i.MXRT1170 的項(xiàng)目,項(xiàng)目有個(gè)需求,需要在 Flash 里實(shí)時(shí)保存一些關(guān)鍵數(shù)據(jù)(初步設(shè) 512 bytes),掉電能恢復(fù)。這些數(shù)據(jù)在訪問(wèn)方式上要友好,最好是很簡(jiǎn)單的 API 接口,上層無(wú)需操心這些關(guān)鍵數(shù)據(jù)在 Flash 里是如何存儲(chǔ)以及具體存儲(chǔ)在什么位置,只需在意關(guān)鍵數(shù)據(jù)保存和讀取的操作即可(就像在 RAM 里動(dòng)態(tài)存取那樣)。
根據(jù)上述需求,痞子衡做了一個(gè)參考設(shè)計(jì),命名為 kFlashFile,當(dāng)前是 v1.0 版本。痞子衡寫了比較詳細(xì)的設(shè)計(jì)文檔,特地分享給大家,如果大家有更好的建議和想法,歡迎在文章下面留言。
項(xiàng)目地址:https://github.com/JayHeng/kFlashFile
kFlashFile
一、簡(jiǎn)介
kFlashFile 是一個(gè)基于 NOR Flash 的輕量級(jí)文件數(shù)據(jù)存儲(chǔ)方案,用于需要斷電數(shù)據(jù)保存的項(xiàng)目。
kFlashFile 主要為 i.MXRT 系列設(shè)計(jì),但其分層框架設(shè)計(jì)使其也可輕松移植到其他 MCU 平臺(tái)。
kFlashFile 從設(shè)計(jì)上分為三層:
- 最底層是 Driver 層:即 Low-level 驅(qū)動(dòng),這層是 MCU 相關(guān)的,對(duì)于 i.MXRT 來(lái)說(shuō),就是 FlexSPI 模塊的驅(qū)動(dòng)。
- 中間是 Adapter 層:主要用于適配底層 Driver,不同 MCU 其 Driver 接口函數(shù)可能不同,因此會(huì)在這一層做到接口統(tǒng)一。
- 最頂層是 API 層:純軟件邏輯設(shè)計(jì)來(lái)實(shí)現(xiàn)文件數(shù)據(jù)存儲(chǔ),提供了四個(gè)非常簡(jiǎn)易的 API。
?
二、設(shè)計(jì)
2.1 API 定義
kFlashFile 是一個(gè)文件數(shù)據(jù)存儲(chǔ)的設(shè)計(jì),file_read()、file_save()是兩個(gè)必備的 API,此外也提供業(yè)界通用 API 接口 file_init()、file_deinit()。
kflash_file_init(): 用于初次分配 Flash 空間來(lái)存儲(chǔ)文件數(shù)據(jù),并且指定文件長(zhǎng)度。如果當(dāng)前指定的 Flash 空間里存在有效文件數(shù)據(jù),那么繼續(xù)復(fù)用。kflash_file_read(): 用于獲取當(dāng)前有效存儲(chǔ)的文件數(shù)據(jù),文件數(shù)據(jù)可以部分讀取。
kflash_file_save(): 用于實(shí)時(shí)寫入最新的文件數(shù)據(jù),文件數(shù)據(jù)可以部分更新。
kflash_file_deinit(): 用于清除當(dāng)前分配的 Flash 空間里的文件數(shù)據(jù),以便下次重新分配。
status_t?kflash_file_init(kflash_file_t?*flashFile,?uint32_t?memStart,?uint32_t?memSize,?uint32_t?fileSize);
status_t?kflash_file_read(kflash_file_t?*flashFile,?uint32_t?offset,?uint8_t?*data,?uint32_t?size);
status_t?kflash_file_save(kflash_file_t?*flashFile,?uint32_t?offset,?uint8_t?*data,?uint32_t?size);
status_t?kflash_file_deinit(kflash_file_t?*flashFile);
2.2 空間分配
kFlashFile 將分配的 Flash 空間分成兩個(gè)部分,前面是文件數(shù)據(jù)區(qū)(Data Sectors),后面是文件頭區(qū)(Header Sectors)。
文件數(shù)據(jù)區(qū):從區(qū)內(nèi)起始地址開(kāi)始按序存放一份份文件數(shù)據(jù),只要文件數(shù)據(jù)出現(xiàn)無(wú)法覆蓋的更新(即 Flash 無(wú)法改寫的特性),便會(huì)在下一個(gè)新地址重新存儲(chǔ)。如果數(shù)據(jù)區(qū)滿了,便擦除區(qū)內(nèi)起始地址處的歷史文件數(shù)據(jù),繼續(xù)循環(huán)存儲(chǔ)。
文件頭區(qū):區(qū)內(nèi) Sector 起始地址放一個(gè) Magic 值(4 字節(jié)),用于標(biāo)識(shí)文件頭。然后開(kāi)始按序記錄一份份文件數(shù)據(jù)在文件數(shù)據(jù)區(qū)里的位置信息(默認(rèn)用 2byte 去記錄一份文件數(shù)據(jù)的位置)。如果當(dāng)前 Header Sector 存儲(chǔ)滿了,便換到下一個(gè) Header Sector 繼續(xù)記錄。
?
2.3 API 主參數(shù)
kFlashFile 設(shè)計(jì)上使用 kflash_file_t 型作為 API 主參數(shù),這個(gè)參數(shù)原型定義如下:
typedef?struct?{
????uint32_t?managedStart;
????uint32_t?managedSize;
????uint32_t?activedStart;
????uint32_t?activedSize;
????uint32_t?recordedIdx;
????uint32_t?recordedPos;
????uint8_t?buffer[KFLASH_MAX_FILE_SIZE];
}?kflash_file_t;
managedStart:表示文件存儲(chǔ)區(qū)映射首地址,即 kflash_file_init() 調(diào)用時(shí)的 memStart 值加上 Flash 在內(nèi)存里映射首地址,managedStart 需要以 Flash Sector 大小對(duì)齊。
managedSize:表示文件存儲(chǔ)區(qū)總大小,即 kflash_file_init() 調(diào)用時(shí)的 memSize 值,需要是 Flash Sector 大小的整數(shù)倍。
activedStart:表示當(dāng)前有效文件數(shù)據(jù)存儲(chǔ)的映射首地址,需要以 Flash Page 大小對(duì)齊。
activedSize:表示當(dāng)前有效文件數(shù)據(jù)長(zhǎng)度,需要是 Flash Page 大小的整數(shù)倍。
recordedIdx:表示當(dāng)前有效文件頭所在的 Header Sector 索引。
recordedPos:表示 Header Sector 中用于存儲(chǔ)當(dāng)前有效文件數(shù)據(jù)位置信息的區(qū)域偏移。
buffer[]:當(dāng)前有效的文件數(shù)據(jù)暫存區(qū)。
三、實(shí)現(xiàn)
3.1 Driver 層
在 i.MXRT 系列上,kFlashFile 的 Driver 層即 FlexSPI NOR 驅(qū)動(dòng),這個(gè)驅(qū)動(dòng)既可以采用 MCU SDK 版本,也可以采用 BootROM 版本。
此處推薦 BootROM 版本的 FlexSPI NOR 驅(qū)動(dòng),因?yàn)檫@個(gè)驅(qū)動(dòng)歷經(jīng)多個(gè) MCU ROM 的洗禮,已經(jīng)相當(dāng)成熟穩(wěn)定。這里簡(jiǎn)單講下其中 Flash 操作的函數(shù):
- flexspi_nor_flash_erase(uint32_t instance, flexspi_nor_config_t *config, uint32_t start, uint32_t length):這個(gè)函數(shù)實(shí)現(xiàn) Flash 擦除,雖然形參里是任意設(shè)定的 start, address,但實(shí)際擦除還是以 Sector 對(duì)齊的,函數(shù)內(nèi)部會(huì)對(duì) start 和 address 做自動(dòng)對(duì)齊。flexspi_nor_flash_page_program(uint32_t instance, flexspi_nor_config_t *config, uint32_t dstAddr, const uint32_t *src):這個(gè)函數(shù)實(shí)現(xiàn) Flash 編程,一次固定寫一整個(gè) Page 大小的數(shù)據(jù),即使 dstAddr 不是以 Page 對(duì)齊,實(shí)際寫入的 Page 數(shù)據(jù)也不會(huì)跨物理 Page(會(huì)自動(dòng)跳回同一個(gè)物理 Page 首地址,這是 Flash 自身特性)。
因?yàn)?flexspi_nor_flash_page_program() 每次都要固定編程整個(gè) Page 數(shù)據(jù),不夠靈活,因此我新寫了一個(gè) flexspi_nor_flash_program() 函數(shù),這個(gè)函數(shù)支持編程用戶自定義長(zhǎng)度的數(shù)據(jù),并且支持跨物理 Page 去寫:
- flexspi_nor_flash_program(uint32_t instance, flexspi_nor_config_t *config, uint32_t dstAddr, const uint32_t *src, uint32_t length):
需要特別注意,對(duì)于 SDR 模式的 Flash,最小編程長(zhǎng)度可以是 1Byte;而 DDR 模式的 Flash,最小編程長(zhǎng)度應(yīng)是 2Bytes(如果這 2Bytes 地址上有一個(gè) Byte 內(nèi)容是 0xFF,該 Byte 依舊可以被再次編程)。
此外 flexspi_nor_flash_program() 函數(shù)有一個(gè)限制,即傳入的 src 源數(shù)據(jù)首地址必須 4 字節(jié)對(duì)齊,哪怕你只想寫入 2 個(gè)字節(jié),這是 FlexSPI 模塊底層對(duì)驅(qū)動(dòng)的要求。
3.2 Adapter 層
kFlashFile 的 Adapter 層是對(duì) Driver 層做了一層封裝,用于屏蔽硬件相關(guān)特性。該層與 MCU 以及板載 Flash 型號(hào)息息相關(guān)。下面的宏定義適用 i.MXRT1170 芯片以及連接在 FlexSPI1 上的 Octal Flash(MX25UM51345):
//?表示?Flash?連接的是?FlexSPI1
#define?KFLASH_INSTANCE??????????(1)
//?BootROM?FlexSPI?驅(qū)動(dòng)對(duì)?Octal?Flash?支持的簡(jiǎn)易配置值
#define?KFLASH_CONFIG_OPTION?????(0xc0403007)
//?FlexSPI1?在系統(tǒng)內(nèi)存中的映射首地址
#define?KFLASH_BASE_ADDRESS??????(0x30000000)
//?默認(rèn)的?Flash?Sector/Page?大?。ㄈ绻?Flash?里有?SFDP,則此處定義無(wú)效)
#define?KFLASH_SECTOR_SIZE???????(0x1000)
#define?KFLASH_PAGE_SIZE?????????(256)
//?FlexSPI?編程接口對(duì)傳入的?src?源數(shù)據(jù)首地址必須?4?字節(jié)對(duì)齊
#define?KFLASH_PROGRAM_ALIGNMENT?(4)
//?Flash?SDR?模式為?1,DDR?模式為?2
#define?KFLASH_PROGRAM_UNIT??????(2)
kFlashFile 的 Adapter 層接口函數(shù)如下,參數(shù)是硬件無(wú)關(guān)的,因此上層可以輕松基于這些接口函數(shù)做純軟件邏輯設(shè)計(jì)。
status_t?kflash_drv_init(void);
uint32_t?kflash_drv_get_info(kflash_mem_info_t?flashInfo);
status_t?kflash_drv_erase_region(uint32_t?start,?uint32_t?length);
status_t?kflash_drv_program_region(uint32_t?dstAddr,?const?uint32_t?*src,?uint32_t?length);
3.3 API 層
kFlashFile 的 API 功能設(shè)計(jì)思路前面介紹過(guò)了,這里介紹具體代碼實(shí)現(xiàn),先來(lái)看幾個(gè)關(guān)鍵的宏定義:
//?設(shè)置?Header?Sector?的個(gè)數(shù),至少是?2?個(gè)
#define?KFLASH_HDR_SECTORS?????(2)
//?設(shè)置?Header?Sector?中用于存儲(chǔ)當(dāng)前有效文件數(shù)據(jù)位置信息的區(qū)域存儲(chǔ)類型
//?uint16_t?最多可記錄?65536?個(gè)位置,最大可支持的?Data?區(qū)域大小為?65536?*?文件數(shù)據(jù)長(zhǎng)度
#define?KFLASH_HDR_POS_TYPE????uint16_t???/*?uint16_t?or?uint32_t?*/
//?設(shè)置總分配的?Flash?長(zhǎng)度(Data+Header?Sector?的個(gè)數(shù)),至少是?4?個(gè)
#define?KFLASH_MIN_SECTORS?????(KFLASH_HDR_SECTORS?+?2)
//?設(shè)置最大支持的文件數(shù)據(jù)長(zhǎng)度,需是?Flash?Page?的整數(shù)倍
#define?KFLASH_MAX_FILE_SIZE???(KFLASH_PAGE_SIZE?*?2)
3.3.1 init()
kflash_file_init() 函數(shù)處理流程如下:
?
如果是首次指定 Flash 空間,那么直接將全部空間擦除干凈,并在第一個(gè) Header Sector 中寫入初始文件頭(Magic + 文件數(shù)據(jù)位置值 0),即最新有效文件數(shù)據(jù)在 Flash 空間文件數(shù)據(jù)區(qū)的首地址。
這里有一個(gè)特殊的設(shè)計(jì),文件數(shù)據(jù)區(qū)其實(shí)并不是直接存儲(chǔ)用戶寫入的文件數(shù)據(jù),而是將用戶文件數(shù)據(jù)全部按位取反之后再存儲(chǔ)進(jìn) Flash。這里假定用戶數(shù)據(jù)初始應(yīng)該是全 0,然后更改主要是將 0 值改為其他值,取反之后,正好對(duì)應(yīng) Flash 里的 bit1 編程為 bit0(Flash 擦除后是全 0xFF),這樣可以充分利用 Flash 覆蓋操作以減少擦除次數(shù)。
函數(shù)中比較關(guān)鍵的步驟是找尋當(dāng)前 Flash 空間中是否存在有效文件數(shù)據(jù),方法是遍歷 Header Sector,發(fā)現(xiàn)存在 Magic 便繼續(xù)尋找最新文件數(shù)據(jù)位置信息存放的區(qū)域(默認(rèn) 2 字節(jié)),按照前面的設(shè)計(jì),只需要按序讀取區(qū)域內(nèi)容,直到遇到 0xFFFF 為止。
3.3.2 read()
kflash_file_read() 函數(shù)最簡(jiǎn)單了,直接從緩存區(qū) buffer 里獲取數(shù)據(jù)即可,因?yàn)槊看胃挛募?shù)據(jù)操作完成之后都會(huì)將最新文件數(shù)據(jù)放在 buffer 里。
3.3.3 save()
kflash_file_save() 函數(shù)是最核心的函數(shù)了,這里邏輯比較復(fù)雜,涉及文件數(shù)據(jù)區(qū)全部滿了之后的動(dòng)作,以及文件頭區(qū)某個(gè) Sector 滿了的動(dòng)作。其處理流程如下:
當(dāng)有一個(gè)新文件數(shù)據(jù)要求保存時(shí),首先會(huì)判斷這個(gè)文件能不能在 Flash 中直接覆蓋存儲(chǔ),如果能,那就直接覆蓋存儲(chǔ),文件頭完全不需要更新,這種情況比較簡(jiǎn)單。
如果新文件數(shù)據(jù)無(wú)法直接覆蓋存儲(chǔ),那么首先判斷文件數(shù)據(jù)區(qū)是否滿了,如果上一個(gè)文件數(shù)據(jù)已經(jīng)存在了文件數(shù)據(jù)區(qū)的最后位置,此時(shí)需要擦除數(shù)據(jù)區(qū)第一個(gè) Sector 從頭開(kāi)始存儲(chǔ)。如果沒(méi)有到最后位置,那就按序往下存儲(chǔ)。
新文件數(shù)據(jù)已經(jīng)保存到數(shù)據(jù)區(qū)之后,此時(shí)需要處理文件頭,記錄這個(gè)新文件數(shù)據(jù)的位置。如果文件頭區(qū)已經(jīng)記錄到當(dāng)前 Sector 的最后位置,需要切換到下一個(gè) Sector 開(kāi)始存儲(chǔ),切換存儲(chǔ)完新位置后,將之前 Sector 擦除。如果沒(méi)有,那就按序在當(dāng)前 Sector 繼續(xù)記錄。
?
3.3.4 deinit()
kflash_file_deinit() 函數(shù)也比較簡(jiǎn)單,就是將文件頭區(qū)域 Header Sectors 全部擦除即可,文件數(shù)據(jù)區(qū)內(nèi)容可以不用管,下次重新分配 Flash 時(shí)會(huì)做擦除。