你們好,我是痞子衡,是正經搞技術的痞子。今天給你們帶來的是痞子衡的我的小項目 - kFlashFile。git
痞子衡最近在參與一個基於 i.MXRT1170 的項目,項目有個需求,須要在 Flash 裏實時保存一些關鍵數據(初步設 512 bytes),掉電能恢復。這些數據在訪問方式上要友好,最好是很簡單的 API 接口,上層無需操心關鍵這些數據在 Flash 裏是如何存儲以及具體存儲在什麼位置,只需在乎關鍵數據保存和讀取的操做便可(就像在 RAM 裏動態存取那樣)。github
根據上述需求,痞子衡作了一個參考設計,命名爲 kFlashFile,當前是 v1.0 版本。痞子衡寫了比較詳細的設計文檔,特意分享給你們,若是你們有更好的建議和想法,歡迎在文章下面留言。編程
kFlashFile 是一個基於 NOR Flash 的輕量級文件數據存儲方案,用於須要斷電數據保存的項目。緩存
kFlashFile 主要爲 i.MXRT 系列設計,但其分層框架設計使其也可輕鬆移植到其餘 MCU 平臺。微信
kFlashFile 從設計上分爲三層:框架
- 最底層是Driver層:即Low-level驅動,這層是MCU相關的,對於i.MXRT來講,就是FlexSPI模塊的驅動。
- 中間是Adapter層:主要用於適配底層Driver,不一樣MCU其Driver接口函數可能不一樣,所以會在這一層作到接口統一。
- 最頂層是API層:純軟件邏輯設計來實現文件數據存儲,提供了四個很是簡易的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);
kFlashFile 將分配的 Flash 空間分紅兩個部分,前面是文件數據區(Data Sectors),後面是文件頭區(Header Sectors)。flex
文件數據區:從區內起始地址開始按序存放一份份文件數據,只要文件數據出現沒法覆蓋的更新(即 Flash 沒法改寫的特性),便會在下一個新地址從新存儲。若是數據區滿了,便擦除區內起始地址處的歷史文件數據,繼續循環存儲。ui
文件頭區:區內 Sector 起始地址放一個 Magic 值(4字節),用於標識文件頭。而後開始按序記錄一份份文件數據在文件數據區裏的位置信息(默認用 2byte 去記錄一份文件數據的位置)。若是當前 Header Sector 存儲滿了,便換到下一個 Header Sector 繼續記錄。
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[]: 當前有效的文件數據暫存區。
在 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 模塊底層對驅動的要求。
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);
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)
kflash_file_init() 函數處理流程以下:
若是是首次指定 Flash 空間,那麼直接將所有空間擦除乾淨,並在第一個 Header Sector 中寫入初始文件頭(Magic + 文件數據位置值 0),即最新有效文件數據在 Flash 空間文件數據區的首地址。
這裏有一個特殊的設計,文件數據區其實並非直接存儲用戶寫入的文件數據,而是將用戶文件數據所有按位取反以後再存儲進 Flash。這裏假定用戶數據初始應該是全 0,而後更改主要是將 0 值改成其餘值,取反以後,正好對應 Flash 裏的 bit1 編程爲 bit0(Flash 擦除後是全 0xFF),這樣能夠充分利用 Flash 覆蓋操做以減小擦除次數。
函數中比較關鍵的步驟是找尋當前 Flash 空間中是否存在有效文件數據,方法是遍歷 Header Sector,發現存在 Magic 便繼續尋找最新文件數據位置信息存放的區域(默認 2 字節),按照前面的設計,只須要按序讀取區域內容,直到遇到 0xFFFF 爲止。
kflash_file_read() 函數最簡單了,直接從緩存區 buffer 裏獲取數據便可,由於每次更新文件數據操做完成以後都會將最新文件數據放在 buffer 裏。
kflash_file_save() 函數是最核心的函數了,這裏邏輯比較複雜,涉及文件數據區所有滿了以後的動做,以及文件頭區某個 Sector 滿了的動做。其處理流程以下:
當有一個新文件數據要求保存時,首先會判斷這個文件能不能在 Flash 中直接覆蓋存儲,若是能,那就直接覆蓋存儲,文件頭徹底不須要更新,這種狀況比較簡單。
若是新文件數據沒法直接覆蓋存儲,那麼首先判斷文件數據區是否滿了,若是上一個文件數據已經存在了文件數據區的最後位置,此時須要擦除數據區第一個 Sector 從頭開始存儲。若是沒有到最後位置,那就按序往下存儲。
新文件數據已經保存到數據區以後,此時須要處理文件頭,記錄這個新文件數據的位置。若是文件頭區已經記錄到當前 Sector 的最後位置,須要切換到下一個 Sector 開始存儲,切換存儲完新位置後,將以前 Sector 擦除。若是沒有,那就按序在當前 Sector 繼續記錄。
kflash_file_deinit() 函數也比較簡單,就是將文件頭區域 Header Sectors 所有擦除便可,文件數據區內容能夠不用管,下次從新分配 Flash 時會作擦除。
文章會同時發佈到個人 博客園主頁、CSDN主頁、微信公衆號 平臺上。
微信搜索"痞子衡嵌入式"或者掃描下面二維碼,就能夠在手機上第一時間看了哦。