痞子衡最近在參與一個基於 i.MXRT1170 的項目,項目有個需求,需要在 Flash 里實時保存一些關鍵數據(初步設 512 bytes),掉電能恢復。這些數據在訪問方式上要友好,最好是很簡單的 API 介面,上層無需操心關鍵這些數據在 Flash 里是如何存儲以及具體存儲在什麼位置,只需在意關... ...
大家好,我是痞子衡,是正經搞技術的痞子。今天給大家帶來的是痞子衡的個人小項目 - kFlashFile。
痞子衡最近在參與一個基於 i.MXRT1170 的項目,項目有個需求,需要在 Flash 里實時保存一些關鍵數據(初步設 512 bytes),掉電能恢復。這些數據在訪問方式上要友好,最好是很簡單的 API 介面,上層無需操心關鍵這些數據在 Flash 里是如何存儲以及具體存儲在什麼位置,只需在意關鍵數據保存和讀取的操作即可(就像在 RAM 里動態存取那樣)。
根據上述需求,痞子衡做了一個參考設計,命名為 kFlashFile,當前是 v1.0 版本。痞子衡寫了比較詳細的設計文檔,特地分享給大家,如果大家有更好的建議和想法,歡迎在文章下麵留言。
kFlashFile
一、簡介
kFlashFile 是一個基於 NOR Flash 的輕量級文件數據存儲方案,用於需要斷電數據保存的項目。
kFlashFile 主要為 i.MXRT 系列設計,但其分層框架設計使其也可輕鬆移植到其他 MCU 平臺。
kFlashFile 從設計上分為三層:
- 最底層是Driver層:即Low-level驅動,這層是MCU相關的,對於i.MXRT來說,就是FlexSPI模塊的驅動。
- 中間是Adapter層:主要用於適配底層Driver,不同MCU其Driver介面函數可能不同,因此會在這一層做到介面統一。
- 最頂層是API層:純軟體邏輯設計來實現文件數據存儲,提供了四個非常簡易的API。
二、設計
2.1 API定義
kFlashFile 是一個文件數據存儲的設計,file_read()、file_save()是兩個必備的 API,此外也提供業界通用 API 介面file_init()、file_deinit()。
- kflash_file_init(): 用於初次分配Flash空間來存儲文件數據,並且指定文件長度。如果當前指定的Flash空間里存在有效文件數據,那麼繼續復用。
- kflash_file_read(): 用於獲取當前有效存儲的文件數據,文件數據可以部分讀取。
- kflash_file_save(): 用於實時寫入最新的文件數據,文件數據可以部分更新。
- kflash_file_deinit(): 用於清除當前分配的Flash空間里的文件數據,以便下次重新分配。
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 空間分成兩個部分,前面是文件數據區(Data Sectors),後面是文件頭區(Header Sectors)。
文件數據區:從區內起始地址開始按序存放一份份文件數據,只要文件數據出現無法覆蓋的更新(即 Flash 無法改寫的特性),便會在下一個新地址重新存儲。如果數據區滿了,便擦除區內起始地址處的歷史文件數據,繼續迴圈存儲。
文件頭區:區內 Sector 起始地址放一個 Magic 值(4位元組),用於標識文件頭。然後開始按序記錄一份份文件數據在文件數據區里的位置信息(預設用 2byte 去記錄一份文件數據的位置)。如果當前 Header Sector 存儲滿了,便換到下一個 Header Sector 繼續記錄。
2.3 API主參數
kFlashFile 設計上使用 kflash_file_t 型作為 API 主參數,這個參數原型定義如下:
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: 表示文件存儲區映射首地址,即 kflash_file_init() 調用時的 memStart 值加上 Flash 在記憶體里映射首地址,managedStart 需要以 Flash Sector 大小對齊。
- managedSize: 表示文件存儲區總大小,即 kflash_file_init() 調用時的 memSize 值,需要是 Flash Sector 大小的整數倍。
- activedStart: 表示當前有效文件數據存儲的映射首地址,需要以 Flash Page 大小對齊。
- activedSize: 表示當前有效文件數據長度,需要是 Flash Page 大小的整數倍。
- recordedIdx: 表示當前有效文件頭所在的 Header Sector 索引。
- recordedPos: 表示 Header Sector 中用於存儲當前有效文件數據位置信息的區域偏移。
- buffer[]: 當前有效的文件數據暫存區。
三、實現
3.1 Driver層
在 i.MXRT 系列上,kFlashFile 的 Driver 層即 FlexSPI NOR 驅動,這個驅動既可以採用 MCU SDK 版本,也可以採用 BootROM 版本。
此處推薦 BootROM 版本的 FlexSPI NOR 驅動,因為這個驅動歷經多個 MCU ROM 的洗禮,已經相當成熟穩定。這裡簡單講下其中 Flash 操作的函數:
- flexspi_nor_flash_erase(uint32_t instance, flexspi_nor_config_t *config, uint32_t start, uint32_t length):這個函數實現Flash擦除,雖然形參里是任意設定的start, address,但實際擦除還是以Sector對齊的,函數內部會對start和address做自動對齊。
- flexspi_nor_flash_page_program(uint32_t instance, flexspi_nor_config_t *config, uint32_t dstAddr, const uint32_t *src):這個函數實現Flash編程,一次固定寫一整個Page大小的數據,即使dstAddr不是以Page對齊,實際寫入的Page數據也不會跨物理Page(會自動跳回同一個物理Page首地址,這是Flash自身特性)。
因為 flexspi_nor_flash_page_program() 每次都要固定編程整個 Page 數據,不夠靈活,因此我新寫了一個 flexspi_nor_flash_program() 函數,這個函數支持編程用戶自定義長度的數據,並且支持跨物理 Page 去寫:
- flexspi_nor_flash_program(uint32_t instance, flexspi_nor_config_t *config, uint32_t dstAddr, const uint32_t *src, uint32_t length):
需要特別註意,對於 SDR 模式的 Flash,最小編程長度可以是 1Byte;而 DDR 模式的 Flash,最小編程長度應是 2Bytes(如果這 2Bytes 地址上有一個 Byte 內容是 0xFF,該 Byte 依舊可以被再次編程)。
此外 flexspi_nor_flash_program() 函數有一個限制,即傳入的 src 源數據首地址必須 4 位元組對齊,哪怕你只想寫入 2 個位元組,這是 FlexSPI 模塊底層對驅動的要求。
3.2 Adapter層
kFlashFile 的 Adapter 層是對 Driver 層做了一層封裝,用於屏蔽硬體相關特性。該層與 MCU 以及板載 Flash 型號息息相關。下麵的巨集定義適用 i.MXRT1170 晶元以及連接在 FlexSPI1 上的 Octal Flash(MX25UM51345):
// 表示 Flash 連接的是 FlexSPI1
#define KFLASH_INSTANCE (1)
// BootROM FlexSPI 驅動對 Octal Flash 支持的簡易配置值
#define KFLASH_CONFIG_OPTION (0xc0403007)
// FlexSPI1 在系統記憶體中的映射首地址
#define KFLASH_BASE_ADDRESS (0x30000000)
// 預設的 Flash Sector/Page 大小(如果 Flash 里有 SFDP,則此處定義無效)
#define KFLASH_SECTOR_SIZE (0x1000)
#define KFLASH_PAGE_SIZE (256)
// FlexSPI 編程介面對傳入的 src 源數據首地址必須 4 位元組對齊
#define KFLASH_PROGRAM_ALIGNMENT (4)
// Flash SDR 模式為 1,DDR 模式為 2
#define KFLASH_PROGRAM_UNIT (2)
kFlashFile 的 Adapter 層介面函數如下,參數是硬體無關的,因此上層可以輕鬆基於這些介面函數做純軟體邏輯設計。
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 功能設計思路前面介紹過了,這裡介紹具體代碼實現,先來看幾個關鍵的巨集定義:
// 設置 Header Sector 的個數,至少是 2 個
#define KFLASH_HDR_SECTORS (2)
// 設置 Header Sector 中用於存儲當前有效文件數據位置信息的區域存儲類型
// uint16_t 最多可記錄 65536 個位置,最大可支持的 Data 區域大小為 65536 * 文件數據長度
#define KFLASH_HDR_POS_TYPE uint16_t /* uint16_t or uint32_t */
// 設置總分配的 Flash 長度(Data+Header Sector 的個數),至少是 4 個
#define KFLASH_MIN_SECTORS (KFLASH_HDR_SECTORS + 2)
// 設置最大支持的文件數據長度,需是 Flash Page 的整數倍
#define KFLASH_MAX_FILE_SIZE (KFLASH_PAGE_SIZE * 2)
3.3.1 init()
kflash_file_init() 函數處理流程如下:
如果是首次指定 Flash 空間,那麼直接將全部空間擦除乾凈,併在第一個 Header Sector 中寫入初始文件頭(Magic + 文件數據位置值 0),即最新有效文件數據在 Flash 空間文件數據區的首地址。
這裡有一個特殊的設計,文件數據區其實並不是直接存儲用戶寫入的文件數據,而是將用戶文件數據全部按位取反之後再存儲進 Flash。這裡假定用戶數據初始應該是全 0,然後更改主要是將 0 值改為其他值,取反之後,正好對應 Flash 里的 bit1 編程為 bit0(Flash 擦除後是全 0xFF),這樣可以充分利用 Flash 覆蓋操作以減少擦除次數。
函數中比較關鍵的步驟是找尋當前 Flash 空間中是否存在有效文件數據,方法是遍歷 Header Sector,發現存在 Magic 便繼續尋找最新文件數據位置信息存放的區域(預設 2 位元組),按照前面的設計,只需要按序讀取區域內容,直到遇到 0xFFFF 為止。
3.3.2 read()
kflash_file_read() 函數最簡單了,直接從緩存區 buffer 里獲取數據即可,因為每次更新文件數據操作完成之後都會將最新文件數據放在 buffer 里。
3.3.3 save()
kflash_file_save() 函數是最核心的函數了,這裡邏輯比較複雜,涉及文件數據區全部滿了之後的動作,以及文件頭區某個 Sector 滿了的動作。其處理流程如下:
當有一個新文件數據要求保存時,首先會判斷這個文件能不能在 Flash 中直接覆蓋存儲,如果能,那就直接覆蓋存儲,文件頭完全不需要更新,這種情況比較簡單。
如果新文件數據無法直接覆蓋存儲,那麼首先判斷文件數據區是否滿了,如果上一個文件數據已經存在了文件數據區的最後位置,此時需要擦除數據區第一個 Sector 從頭開始存儲。如果沒有到最後位置,那就按序往下存儲。
新文件數據已經保存到數據區之後,此時需要處理文件頭,記錄這個新文件數據的位置。如果文件頭區已經記錄到當前 Sector 的最後位置,需要切換到下一個 Sector 開始存儲,切換存儲完新位置後,將之前 Sector 擦除。如果沒有,那就按序在當前 Sector 繼續記錄。
3.3.4 deinit()
kflash_file_deinit() 函數也比較簡單,就是將文件頭區域 Header Sectors 全部擦除即可,文件數據區內容可以不用管,下次重新分配 Flash 時會做擦除。
歡迎訂閱
文章會同時發佈到我的 博客園主頁、CSDN主頁、微信公眾號 平臺上。
微信搜索"痞子衡嵌入式"或者掃描下麵二維碼,就可以在手機上第一時間看了哦。