FDS,全稱Flash Data Storage,用來訪問芯片內(nèi)部Flash的。當(dāng)你需要把數(shù)據(jù)存儲(chǔ)在Flash中,或者讀取Flash中的用戶數(shù)據(jù),或者更新或者刪除Flash中的數(shù)據(jù),那么FDS模塊是你最好的選擇。FDS采用文件和記錄方式來組織Flash數(shù)據(jù),也就是說,真正的數(shù)據(jù)是放在一條記錄中,而多條記錄組成一個(gè)文件。根據(jù)應(yīng)用的需要,整個(gè)系統(tǒng)可以只有一個(gè)文件,也可以包含多個(gè)文件。文件采用文件ID來標(biāo)示,文件ID為2個(gè)字節(jié)(注:不能取值為0xFFFF)。一個(gè)文件下面可以放一條記錄,也可以放多條記錄,記錄是通過記錄key來標(biāo)示的,記錄key也是2個(gè)字節(jié)長度(注:不能取值為0x0000)。這里需要注意的是,同一個(gè)文件下面的兩條或者多條記錄他們的key可以是一樣的,比如我們可以建立如下文件系統(tǒng):文件1包含2條記錄,文件2包含3條記錄,文件2包含2條key為0x0003的記錄

注:如果你可以保證一個(gè)文件下面所有記錄的key都不一樣,那么文件系統(tǒng)會(huì)變得更簡潔一些,尤其在find記錄的時(shí)候,只會(huì)返回一條記錄,可以簡化很多應(yīng)用邏輯。如前所述,這個(gè)不是強(qiáng)制要求:同一個(gè)文件下記錄key可以相同。
FDS用法
一般而言,按照如下步驟使用FDS模塊:
修改FDS的默認(rèn)配置參數(shù),比如總共分配多少Flash空間(默認(rèn)只分配了8kB Flash空間給用戶使用),請到sdk_config.h文件中修改如下默認(rèn)配置項(xiàng):

通過fds_register注冊FDS事件回調(diào)函數(shù)及通過fds_init初始化FDS模塊。FDS模塊的初始化,寫記錄,更新記錄,刪除記錄以及垃圾回收,這些API都是異步的。也就是說調(diào)用這些FDS操作的API,只是把相應(yīng)操作放入隊(duì)列然后立即返回(隊(duì)列大小由上述的FDS_OP_QUEUE_SIZE控制),真正的Flash操作結(jié)果是通過事件回調(diào)函數(shù)通知你的。注:現(xiàn)在的FDS模塊可以進(jìn)行多次初始化。示例代碼如下所示:
// Simple event handler to handle errors during initialization. static void fds_evt_handler(fds_evt_t const * p_fds_evt) { switch (p_fds_evt->id) { case FDS_EVT_INIT: if (p_fds_evt->result != FDS_SUCCESS) { // Initialization failed. } break; default: break; } } ret_code_t ret = fds_register(fds_evt_handler); if (ret != FDS_SUCCESS) { // Registering of the FDS event handler has failed. } ret_code_t ret = fds_init(); if (ret != FDS_SUCCESS) { // Handle error. }
通過fds_record_write創(chuàng)建新的記錄,即寫記錄。 注意寫記錄的時(shí)候,必須保證輸入?yún)?shù)是全局變量或者static的局部變量,推薦使用全局變量! 由于record key可以重復(fù),所以連續(xù)調(diào)用兩次相同的fds_record_write,將生成兩條同樣key的記錄。前面也提及過,fds_record_write是異步的,所以它的返回值為success只是表示操作入隊(duì)成功,真正的flash操作結(jié)果是通過前面注冊的fds_evt_handler來通知的。示例代碼如下所示:
#define FILE_ID 0x0001 /* The ID of the file to write the records into. */ #define RECORD_KEY_1 0x1111 /* A key for the first record. */ #define RECORD_KEY_2 0x2222 /* A key for the second record. */ static uint32_t const m_deadbeef = 0xDEADBEEF; static char const m_hello[] = "Hello, world!"; fds_record_t record; fds_record_desc_t record_desc; // Set up record. record.file_id = FILE_ID; record.key = RECORD_KEY_1; record.data.p_data = &m_deadbeef; record.data.length_words = 1; /* one word is four bytes. */ ret_code_t rc; rc = fds_record_write(&record_desc, &record); if (rc != FDS_SUCCESS) { /* Handle error. */ } // Set up record. record.file_id = FILE_ID; record.key = RECORD_KEY_2; record.data.p_data = &m_hello; /* The following calculation takes into account any eventual remainder of the division. */ record.data.length_words = (sizeof(m_hello) + 3) / 4; rc = fds_record_write(&record_desc, &record); if (rc != FDS_SUCCESS) { /* Handle error. */ }
通過fds_record_open來讀記錄。讀記錄之前必須先找到這條記錄,這個(gè)是通過fds_record_find來實(shí)現(xiàn)的,由于同一個(gè)文件可以包含多條key相同的記錄,所以通過多次調(diào)用同一個(gè)fds_record_find,可以找到所有相關(guān)記錄。示例代碼如下所示:
#define FILE_ID 0x1111 #define RECORD_KEY 0x2222 fds_flash_record_t flash_record; fds_record_desc_t record_desc; fds_find_token_t ftok; /* It is required to zero the token before first use. */ memset(&ftok, 0x00, sizeof(fds_find_token_t)); /* Loop until all records with the given key and file ID have been found. */ while (fds_record_find(FILE_ID, RECORD_KEY, &record_desc, &ftok) == FDS_SUCCESS) { if (fds_record_open(&record_desc, &flash_record) != FDS_SUCCESS) { /* Handle error. */ } /* Access the record through the flash_record structure. */ /* Close the record when done. */ if (fds_record_close(&record_desc) != FDS_SUCCESS) { /* Handle error. */ } }
操作記錄,比如fds_record_update,fds_record_delete等,update和delete操作,必須先找到相應(yīng)記錄,然后才能去update或者delete。fds_record_delete不是真得把記錄刪除,而是將記錄標(biāo)示為無效。而fds_record_update實(shí)際包含2步:先找到之前的記錄然后將其標(biāo)記為無效(即delete操作),然后write一條新記錄。記住:delete并不會(huì)回收Flash空間,無效記錄仍然占據(jù)著Flash空間,這些無效記錄占據(jù)著的Flash空間只有經(jīng)過垃圾回收(fds_gc)才能再次給新記錄使用。請注意fds_record_find只會(huì)去尋找有效記錄,而不會(huì)將無效記錄返回給用戶的。另外,fds_record_ update和fds_record_delete是異步的,所以它們的返回值為success只是表示操作入隊(duì)成功,真正的flash操作結(jié)果是通過前面注冊的fds_evt_handler來通知的。示例代碼如下所示:
fds_record_desc_t desc = {0};
fds_find_token_t tok = {0};
rc = fds_record_find(CONFIG_FILE, CONFIG_REC_KEY, &desc, &tok);
if (rc == FDS_SUCCESS)
{
/* A config file is in flash. Let's update it. */
fds_flash_record_t config = {0};
/* Open the record and read its contents. */
rc = fds_record_open(&desc, &config);
APP_ERROR_CHECK(rc);
/* Copy the configuration from flash into m_dummy_cfg. */
memcpy(&m_dummy_cfg, config.p_data, sizeof(configuration_t));
NRF_LOG_INFO("Config file found, updating boot count to %d.", m_dummy_cfg.boot_count);
/* Update boot count. */
m_dummy_cfg.boot_count++;
/* Close the record when done reading. */
rc = fds_record_close(&desc);
APP_ERROR_CHECK(rc);
/* Write the updated record to flash. */
rc = fds_record_update(&desc, &m_dummy_record);
if (rc == FDS_ERR_NO_SPACE_IN_FLASH) fds_gc();
else APP_ERROR_CHECK(rc);
}
ret_code_t ret = fds_record_delete(&desc);
if (ret != FDS_SUCCESS)
{
/* Error. */
}
當(dāng)Flash不夠用時(shí),即FDS寫記錄或者更新記錄操作返回錯(cuò)誤FDS_ERR_NO_SPACE_IN_FLASH,請調(diào)用垃圾回收函數(shù):fds_gc進(jìn)行垃圾回收。fds_gc是一個(gè)非常耗時(shí)的操作過程(請確保操作過程中不會(huì)掉電,否則Flash行為未知),它會(huì)一個(gè)page一個(gè)page操作,然后將該page中的有效記錄拷貝到swap page,然后擦除該page,并標(biāo)記該page為swap page,而之前的swap page則變?yōu)閐ata page,如此往復(fù),直到把所有page都回收完。只有經(jīng)過fds_gc后,之前無效記錄占據(jù)的Flash空間才會(huì)釋放,這個(gè)時(shí)候才會(huì)有多余的Flash空間給用戶去操作。
建議大家直接參考SDK里面自帶的fds例子來編寫自己的fds應(yīng)用代碼,SDK自帶的fds例子所在目錄為:SDK安裝目錄examplesperipheralflash_fds (注:從SDK14之后才有fds例子)
理解FDS
FDS作為上層模塊,它是通過調(diào)用fstorage API來實(shí)現(xiàn)自己的功能,fstorage又是通過調(diào)用NVMC外設(shè)驅(qū)動(dòng)或者softdevice Flash訪問API來達(dá)到操作Flash的目的,調(diào)用關(guān)系圖如下所示:

當(dāng)softdevice存在的時(shí)候,建議使用nrf_fstorage_sd后端;沒有softdevice的時(shí)候,請使用nrf_fstorage_nvmc后端。
根據(jù)有無bootloader,F(xiàn)DS將操作不同的Flash空間,如下:

當(dāng)你通過FDS把數(shù)據(jù)寫入Flash中,除了數(shù)據(jù)本身,F(xiàn)DS還會(huì)在這條記錄中加入額外的信息:記錄頭header,一條記錄在Flash中完整的格式如下所示:

| 字段 | 大小 | 描述 |
| Record key | 16 bits | Key that can be used to find the record. The value FDS_RECORD_KEY_DIRTY (0x0000) is reserved by the system to flag records that have been invalidated. See Restrictions on keys and IDs for further restrictions. |
| Data length | 16 bits | Length of the data that is stored in the record (in 4-byte words). |
| File ID | 16 bits | ID of the file that the record is associated with. The value FDS_FILE_ID_INVALID (0xFFFF) is used by the system to identify records that have not been written correctly. See Restrictions on keys and IDs for further restrictions. |
| CRC value | 16 bits | CRC value of the whole record (checks can be enabled by setting the FDS_CRC_ENABLED compile flag, see Configuration). |
| Record ID | 32 bits | Unique identifier of the record. 注:對用戶不可見 |
所以,在計(jì)算記錄總共占用多少Flash空間的時(shí)候,記得一定要把每條記錄的header(3個(gè)word)也加上。
FDS使用常見問題
大家在使用FDS模塊時(shí),經(jīng)常碰到的問題有如下幾種:
FDS不支持掉電保護(hù),所以在Flash操作過程中出現(xiàn)了掉電,F(xiàn)DS行為將未知
OTA的時(shí)候,新固件的FDS page數(shù)目一定要等于老固件的FDS page數(shù),否則將出現(xiàn)不可知行為
fds_record_write或者fds_record_update后,強(qiáng)烈建議回讀該記錄,以確保記錄的確write或者update成功
忘了給參數(shù)清0。Nordic提供的API輸入?yún)?shù)很多都是結(jié)構(gòu)體變量,這些變量使用之前,記得一定要通過memset先清0。如果忘了清0,就會(huì)出現(xiàn)一些匪夷所思的現(xiàn)象。
fds_record_desc_t desc; //= {0}; //錯(cuò)誤,忘了清0
fds_find_token_t tok; //= {0}; //錯(cuò)誤,忘了清0
忘了使用全局變量或者靜態(tài)局部變量。因?yàn)閣rite和update操作都是異步的,所以record.data.p_data必須指向全局變量或者靜態(tài)局部變量,以保證Flash操作過程中p_data指向的內(nèi)容不會(huì)更改。
變量起始地址必須字對齊。Flash操作是以word為單位的,所以要求write和update操作的p_data指向的變量的起始地址必須word對齊,大家可以使用偽匯編指令“__ALIGN(sizeof(uint32_t))”來保證該變量起始地址是word對齊的。
Update或者delete之前必須先find。fds_record_update或者fds_record_delete會(huì)用到參數(shù)descriptor,這個(gè)descriptor必須是通過fds_record_find返回的。
忘了使用fds_gc導(dǎo)致Flash fatal error或者其他奇奇怪怪的問題。當(dāng)write或者update報(bào)FDS_ERR_NO_SPACE_IN_FLASH錯(cuò)誤時(shí),記得一定要調(diào)用fds_gc。或者當(dāng)delete record或者update record達(dá)到一定次數(shù)后,主動(dòng)調(diào)用fds_gc。或者通過查看fds_stat得到dirty record數(shù)目達(dá)到某個(gè)值后,主動(dòng)調(diào)用fds_gc。
SDK已知問題。每個(gè)版本SDK都有或多或少的問題,這些問題都可以在Nordic devzone上查到。比如SDK12.2.0 fds_gc在某些情況下,就會(huì)有問題,請參考:https://devzone.nordicsemi.com/question/93241/what-are-sdk-12x0-known-issues/,所以,一般建議大家使用最新版SDK,最新版SDK會(huì)把之前發(fā)現(xiàn)的問題都修復(fù)掉,它的穩(wěn)定性和可靠性都是最高的。
最后再次強(qiáng)調(diào)一遍:FDS不支持掉電保護(hù),所以在FDS操作過程中,尤其是垃圾回收過程中,發(fā)生了掉電,那么Flash內(nèi)容將變得不可靠。所以強(qiáng)烈建議大家:在每一次write或者update之后,都把相應(yīng)記錄讀出來,跟原始內(nèi)容進(jìn)行比對,以確保記錄真的寫成功或者更新成功了
審核編輯 黃宇
-
SDK
+關(guān)注
關(guān)注
3文章
1101瀏覽量
51717 -
Nordic
+關(guān)注
關(guān)注
9文章
257瀏覽量
49017
發(fā)布評論請先 登錄
針對雙bank和單bank的使用方法建議
深度技術(shù)解析nRF Connect SDK裸機(jī)選項(xiàng)方案
使用nRF52840芯片的USB Host 功能參考例程
深度技術(shù)解析低功耗藍(lán)牙廠商nordic的nRF Connect SDK裸機(jī)選項(xiàng)方案
nRF Connect SDK Basic
ZYNQ UltraScalePlus RFSOC QSPI Flash固化常見問題說明
如何調(diào)試nRF5 SDK
Nordic nRF51/nRF52開發(fā)流程說明
定時(shí)模塊app_timer用法及常見問題—nRF5 SDK模塊系列二
nRF Connect SDK(NCS)/Zephyr固件升級詳解 – 重點(diǎn)講述MCUboot和藍(lán)牙空中升級
如何調(diào)試nRF5 SDK
關(guān)于功率模塊冷卻的六個(gè)常見問題
Flash訪問模塊FDS用法及常見問題—nRF5 SDK模塊系列一
評論