1. 程式人生 > >痞子衡嵌入式:kFlashFile v1.0 - 一個基於Flash的掉電資料存取方案

痞子衡嵌入式:kFlashFile v1.0 - 一個基於Flash的掉電資料存取方案

大家好,我是痞子衡,是正經搞技術的痞子。今天給大家帶來的是痞子衡的個人小專案 - kFlashFile。 痞子衡最近在參與一個基於 i.MXRT1170 的專案,專案有個需求,需要在 Flash 裡實時儲存一些關鍵資料(初步設 512 bytes),掉電能恢復。這些資料在訪問方式上要友好,最好是很簡單的 API 介面,上層無需操心關鍵這些資料在 Flash 裡是如何儲存以及具體儲存在什麼位置,只需在意關鍵資料儲存和讀取的操作即可(就像在 RAM 裡動態存取那樣)。 根據上述需求,痞子衡做了一個參考設計,命名為 kFlashFile,當前是 v1.0 版本。痞子衡寫了比較詳細的設計文件,特地分享給大家,如果大家有更好的建議和想法,歡迎在文章下面留言。 > 專案地址:https://github.com/JayHeng/kFlashFile # kFlashFile ## 一、簡介 kFlashFile 是一個基於 NOR Flash 的輕量級檔案資料儲存方案,用於需要斷電資料儲存的專案。 kFlashFile 主要為 i.MXRT 系列設計,但其分層框架設計使其也可輕鬆移植到其他 MCU 平臺。 kFlashFile 從設計上分為三層: > * 最底層是Driver層:即Low-level驅動,這層是MCU相關的,對於i.MXRT來說,就是FlexSPI模組的驅動。 > * 中間是Adapter層:主要用於適配底層Driver,不同MCU其Driver介面函式可能不同,因此會在這一層做到介面統一。 > * 最頂層是API層:純軟體邏輯設計來實現檔案資料儲存,提供了四個非常簡易的API。 ![](http://henjay724.com/image/github/kFlashFile_Framework.PNG) ## 二、設計 ### 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空間裡的檔案資料,以便下次重新分配。 ```C 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 繼續記錄。 ![](http://henjay724.com/image/github/kFlashFile_Design0.PNG) ### 2.3 API主引數 kFlashFile 設計上使用 kflash_file_t 型作為 API 主引數,這個引數原型定義如下: ```C 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): ```C // 表示 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 層介面函式如下,引數是硬體無關的,因此上層可以輕鬆基於這些介面函式做純軟體邏輯設計。 ```C 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 功能設計思路前面介紹過了,這裡介紹具體程式碼實現,先來看幾個關鍵的巨集定義: ```C // 設定 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() 函式處理流程如下: ![](http://henjay724.com/image/github/kFlashFile_Flow_init.PNG) 如果是首次指定 Flash 空間,那麼直接將全部空間擦除乾淨,並在第一個 Header Sector 中寫入初始檔案頭(Magic + 檔案資料位置值 0),即最新有效檔案資料在 Flash 空間檔案資料區的首地址。 ![](http://henjay724.com/image/github/kFlashFile_Design_Init.PNG) 這裡有一個特殊的設計,檔案資料區其實並不是直接儲存使用者寫入的檔案資料,而是將使用者檔案資料全部按位取反之後再儲存進 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 滿了的動作。其處理流程如下: ![](http://henjay724.com/image/github/kFlashFile_Flow_save.PNG) 當有一個新檔案資料要求儲存時,首先會判斷這個檔案能不能在 Flash 中直接覆蓋儲存,如果能,那就直接覆蓋儲存,檔案頭完全不需要更新,這種情況比較簡單。 ![](http://henjay724.com/image/github/kFlashFile_Design_Save0.PNG) 如果新檔案資料無法直接覆蓋儲存,那麼首先判斷檔案資料區是否滿了,如果上一個檔案資料已經存在了檔案資料區的最後位置,此時需要擦除資料區第一個 Sector 從頭開始儲存。如果沒有到最後位置,那就按序往下儲存。 ![](http://henjay724.com/image/github/kFlashFile_Design_Save1.PNG) 新檔案資料已經儲存到資料區之後,此時需要處理檔案頭,記錄這個新檔案資料的位置。如果檔案頭區已經記錄到當前 Sector 的最後位置,需要切換到下一個 Sector 開始儲存,切換儲存完新位置後,將之前 Sector 擦除。如果沒有,那就按序在當前 Sector 繼續記錄。 ![](http://henjay724.com/image/github/kFlashFile_Design_Save2.PNG) #### 3.3.4 deinit() kflash_file_deinit() 函式也比較簡單,就是將檔案頭區域 Header Sectors 全部擦除即可,檔案資料區內容可以不用管,下次重新分配 Flash 時會做擦除。 ![](http://henjay724.com/image/github/kFlashFile_Design_Deinit.PNG) ### 歡迎訂閱 文章會同時釋出到我的 [部落格園主頁](https://www.cnblogs.com/henjay724/)、[CSDN主頁](https://blog.csdn.net/Henjay724)、[微信公眾號](http://weixin.sogou.com/weixin?type=1&query=痞子衡嵌入式) 平臺上。 微信搜尋"__痞子衡嵌入式__"或者掃描下面二維碼,就可以在手機上第一時間看了哦。 ![](http://henjay724.com/image/github/pzhMcu_qrcode_258x2