據(jù)存儲(chǔ)實(shí)戰(zhàn):從Flash磨損均衡到掉電保護(hù)策略)
1. 從一次“數(shù)據(jù)丟失”事故說起幾年前我參與過一個(gè)智能水表的項(xiàng)目。現(xiàn)場(chǎng)反饋有部分水表在運(yùn)行幾個(gè)月后累計(jì)用水量會(huì)莫名其妙地歸零或者跳變成一個(gè)異常值。我們排查了硬件、電源、甚至通信模塊折騰了好幾周最后才發(fā)現(xiàn)問題出在最不起眼的地方——數(shù)據(jù)存儲(chǔ)。單片機(jī)內(nèi)部的Flash在頻繁擦寫特定扇區(qū)后出現(xiàn)了位翻轉(zhuǎn)導(dǎo)致存儲(chǔ)的計(jì)量數(shù)據(jù)損壞。這次事故讓我深刻意識(shí)到在資源受限的嵌入式世界里數(shù)據(jù)存儲(chǔ)與管理絕非簡(jiǎn)單的“讀”和“寫”它是一套關(guān)乎系統(tǒng)可靠性、壽命乃至產(chǎn)品口碑的完整策略。今天我們就拋開那些空洞的理論直接切入嵌入式開發(fā)者每天都要面對(duì)的現(xiàn)實(shí)如何在有限的ROM、RAM和Flash空間里安全、高效、長(zhǎng)壽地保管好那些關(guān)鍵數(shù)據(jù)。無(wú)論是設(shè)備唯一標(biāo)識(shí)、校準(zhǔn)參數(shù)、運(yùn)行日志還是像水表讀數(shù)那樣需要掉電保存的歷史記錄處理不好輕則功能異常重則釀成事故。接下來我將結(jié)合多年踩坑經(jīng)驗(yàn)為你拆解從存儲(chǔ)介質(zhì)選型、到數(shù)據(jù)組織、再到可靠寫入與磨損均衡的一整套實(shí)戰(zhàn)策略。2. 存儲(chǔ)介質(zhì)面面觀不只是選型更是妥協(xié)的藝術(shù)提到嵌入式數(shù)據(jù)存儲(chǔ)很多人第一反應(yīng)是Flash但這只是選項(xiàng)之一。不同的介質(zhì)決定了完全不同的管理策略。我們需要像挑選搭檔一樣了解它們的脾氣和底線。2.1 片內(nèi)Flash最親密的“室友”幾乎所有MCU都自帶片內(nèi)Flash用于存放程序代碼剩余部分常被用來存儲(chǔ)參數(shù)。它的最大優(yōu)點(diǎn)是“零”成本已包含在芯片內(nèi)和極高的讀取速度。但它的缺點(diǎn)同樣鮮明擦寫壽命有限通常只有1萬(wàn)到10萬(wàn)次EEPROM區(qū)域可能更高。頻繁寫入同一個(gè)扇區(qū)會(huì)迅速耗盡其壽命。擦除單位大必須以扇區(qū)Sector或頁(yè)P(yáng)age為單位進(jìn)行擦除通常為幾百字節(jié)到幾KB然后才能寫入。這意味著修改幾個(gè)字節(jié)的數(shù)據(jù)也可能需要備份并擦寫整個(gè)扇區(qū)。操作復(fù)雜需要特定的解鎖序列、擦除命令和編程命令期間必須關(guān)閉總中斷否則可能導(dǎo)致操作失敗或芯片鎖死。注意對(duì)片內(nèi)Flash進(jìn)行寫操作時(shí)務(wù)必確保程序不是從正在被擦寫的區(qū)域運(yùn)行即不能對(duì)當(dāng)前代碼所在扇區(qū)進(jìn)行操作否則會(huì)導(dǎo)致程序跑飛。通常需要將寫Flash的相關(guān)函數(shù)拷貝到RAM中執(zhí)行。實(shí)戰(zhàn)心得片內(nèi)Flash最適合存儲(chǔ)那些幾乎不變的數(shù)據(jù)如設(shè)備序列號(hào)、生產(chǎn)校準(zhǔn)參數(shù)、硬件版本號(hào)。如果必須存儲(chǔ)變化的數(shù)據(jù)務(wù)必精心設(shè)計(jì)扇區(qū)輪換策略避免對(duì)單一地址的集中寫入。2.2 EEPROM專為數(shù)據(jù)而生的“老將”EEPROM是電可擦除只讀存儲(chǔ)器的簡(jiǎn)稱很多MCU會(huì)集成一小塊獨(dú)立的EEPROM區(qū)域或者通過I2C、SPI接口外掛一顆EEPROM芯片如AT24C系列。它的特點(diǎn)是字節(jié)可擦寫可以單獨(dú)修改某一個(gè)字節(jié)無(wú)需擦除整個(gè)扇區(qū)靈活性高。壽命更長(zhǎng)通常可達(dá)到100萬(wàn)次甚至更高的擦寫次數(shù)。接口簡(jiǎn)單I2C等接口操作簡(jiǎn)單驅(qū)動(dòng)程序成熟。但它的容量通常較小幾KB到幾十KB且寫入速度較慢毫秒級(jí)。EEPROM是存儲(chǔ)頻繁修改但數(shù)據(jù)量不大的小參數(shù)如運(yùn)行模式、計(jì)數(shù)器、用戶設(shè)置的理想選擇。2.3 外置SPI Flash容量與成本的“平衡者”當(dāng)需要存儲(chǔ)大量數(shù)據(jù)如字庫(kù)、圖片、音頻、歷史日志時(shí)外置的SPI接口Nor Flash如W25Q系列成為主流選擇。它價(jià)格低廉容量從幾Mb到數(shù)Gb不等。其特性與片內(nèi)Flash類似也是按扇區(qū)擦除按頁(yè)編程壽命在10萬(wàn)次左右。由于通過SPI總線訪問速度比片內(nèi)Flash慢且需要額外的驅(qū)動(dòng)代碼。選型關(guān)鍵點(diǎn)除了容量要特別關(guān)注Flash的“頁(yè)編程”和“扇區(qū)擦除”大小。例如W25Q128JV的頁(yè)編程大小為256字節(jié)扇區(qū)擦除為4KB。這意味著即使你只想寫1個(gè)字節(jié)理論上也需要先讀取該扇區(qū)4KB的數(shù)據(jù)到緩存修改目標(biāo)字節(jié)擦除整個(gè)扇區(qū)再將緩存數(shù)據(jù)寫回。當(dāng)然好的驅(qū)動(dòng)和管理策略可以優(yōu)化這個(gè)過程。2.4 FRAM/NVRAM性能與壽命的“貴族”FRAM鐵電存儲(chǔ)器和基于電池供電的SRAMNVRAM屬于“非主流”但性能強(qiáng)悍的選擇。它們像RAM一樣可以字節(jié)尋址、高速隨機(jī)讀寫同時(shí)又具備非易失性。FRAM的擦寫壽命極高可達(dá)1萬(wàn)億次幾乎無(wú)需考慮磨損問題。但缺點(diǎn)是價(jià)格昂貴容量較小目前多用于對(duì)數(shù)據(jù)寫入速度和可靠性有極端要求的場(chǎng)合如高精度數(shù)據(jù)采集的實(shí)時(shí)存儲(chǔ)。如何選擇沒有最好的只有最合適的。一個(gè)常見的組合策略是片內(nèi)Flash存固件和核心不變參數(shù) 外置SPI Flash存大容量日志和資源文件 片內(nèi)/外置EEPROM存頻繁修改的小數(shù)據(jù)。成本敏感的產(chǎn)品則可能全部依賴片內(nèi)Flash通過精巧的軟件策略來彌補(bǔ)硬件的局限。3. 數(shù)據(jù)組織與結(jié)構(gòu)設(shè)計(jì)為數(shù)據(jù)安一個(gè)“家”選好了存儲(chǔ)介質(zhì)接下來就要設(shè)計(jì)數(shù)據(jù)的“家”——即存儲(chǔ)結(jié)構(gòu)?;靵y的存儲(chǔ)布局是后期維護(hù)和升級(jí)的噩夢(mèng)。3.1 定義清晰的數(shù)據(jù)分區(qū)表首先你需要一份明確的“分區(qū)表”就像給房子的每個(gè)房間貼上標(biāo)簽。這份表格應(yīng)該記錄在案可以是代碼中的宏定義也可以是獨(dú)立的設(shè)計(jì)文檔并包含以下信息分區(qū)名稱起始地址大小存儲(chǔ)介質(zhì)數(shù)據(jù)類型更新頻率備注BOOTLOADER0x0800000016KB片內(nèi)Flash程序代碼幾乎不變引導(dǎo)程序區(qū)APP_FIRMWARE0x08004000480KB片內(nèi)Flash程序代碼固件升級(jí)時(shí)應(yīng)用程序區(qū)PARAM_ZONE_A0x080E00004KB片內(nèi)Flash結(jié)構(gòu)體參數(shù)偶爾參數(shù)存儲(chǔ)區(qū)APARAM_ZONE_B0x080E10004KB片內(nèi)Flash結(jié)構(gòu)體參數(shù)偶爾參數(shù)存儲(chǔ)區(qū)B備份LOG_SECTOR_00x000000 (SPI)4KBW25Q128日志結(jié)構(gòu)體頻繁日志循環(huán)存儲(chǔ)區(qū)0DEVICE_SN0x00 (EEPROM)16字節(jié)AT24C02字符串不變?cè)O(shè)備序列號(hào)設(shè)計(jì)要點(diǎn)預(yù)留空間為固件升級(jí)、參數(shù)擴(kuò)展預(yù)留足夠的空間。例如APP分區(qū)后預(yù)留一部分空閑Flash。對(duì)齊分區(qū)起始地址和大小最好與存儲(chǔ)介質(zhì)的擦除單位對(duì)齊避免一個(gè)數(shù)據(jù)塊橫跨兩個(gè)物理扇區(qū)增加管理復(fù)雜度。版本兼容在參數(shù)分區(qū)的開頭可以預(yù)留一個(gè)“版本號(hào)”字段。當(dāng)參數(shù)結(jié)構(gòu)體因需求變更而修改時(shí)通過版本號(hào)來區(qū)分和兼容舊數(shù)據(jù)。3.2 結(jié)構(gòu)化數(shù)據(jù)與序列化嵌入式C語(yǔ)言中我們常用struct來組織一組相關(guān)的參數(shù)。這是最自然、訪問最高效的方式。typedef struct { uint32_t head_magic; // 魔數(shù)用于識(shí)別數(shù)據(jù)有效性如0xAA55AA55 uint16_t param_ver; // 參數(shù)結(jié)構(gòu)體版本號(hào) uint32_t device_id; float calibration_factor; uint8_t work_mode; uint32_t total_work_time; uint32_t crc32; // 循環(huán)冗余校驗(yàn)值覆蓋前面所有字段 } system_params_t;關(guān)鍵技巧添加“魔數(shù)”在結(jié)構(gòu)體開頭定義一個(gè)固定的魔術(shù)數(shù)字Magic Number。每次讀取數(shù)據(jù)后先檢查魔數(shù)是否正確。這能快速判斷該存儲(chǔ)區(qū)域是否被初始化過或已損壞。引入CRC校驗(yàn)在結(jié)構(gòu)體末尾計(jì)算并存儲(chǔ)一個(gè)CRC32校驗(yàn)值。寫入前計(jì)算讀取后驗(yàn)證。這是檢測(cè)數(shù)據(jù)在存儲(chǔ)過程中是否發(fā)生位翻轉(zhuǎn)的最有效手段之一遠(yuǎn)比簡(jiǎn)單的求和校驗(yàn)可靠。處理字節(jié)序如果數(shù)據(jù)可能在不同架構(gòu)的處理器間傳遞如通過網(wǎng)絡(luò)或者你需要直接以二進(jìn)制形式解析存儲(chǔ)文件就要考慮字節(jié)序大端/小端問題。可以在結(jié)構(gòu)體內(nèi)統(tǒng)一使用固定字節(jié)序的數(shù)據(jù)類型或在存儲(chǔ)前進(jìn)行轉(zhuǎn)換。序列化與反序列化對(duì)于更復(fù)雜或需要跨平臺(tái)的數(shù)據(jù)可以定義簡(jiǎn)單的TLV類型-長(zhǎng)度-值格式或使用CBOR、MessagePack等輕量級(jí)序列化庫(kù)。但對(duì)于大多數(shù)嵌入式應(yīng)用精心設(shè)計(jì)的struct加上CRC校驗(yàn)已經(jīng)足夠健壯。4. 可靠寫入與掉電保護(hù)與“意外”賽跑嵌入式設(shè)備常面臨突然斷電的風(fēng)險(xiǎn)。如果在寫入數(shù)據(jù)的過程中斷電很可能導(dǎo)致存儲(chǔ)的數(shù)據(jù)半新半舊甚至整個(gè)扇區(qū)損壞。我們必須設(shè)計(jì)能抵御這種風(fēng)險(xiǎn)的機(jī)制。4.1 “雙區(qū)備份”與“原子提交”策略這是應(yīng)對(duì)參數(shù)存儲(chǔ)最經(jīng)典、最有效的策略。我們至少需要兩個(gè)大小相同的物理扇區(qū)如上文分區(qū)表中的PARAM_ZONE_A和PARAM_ZONE_B。操作流程如下初始化系統(tǒng)啟動(dòng)時(shí)依次讀取A區(qū)和B區(qū)的數(shù)據(jù)校驗(yàn)其魔數(shù)和CRC。選擇校驗(yàn)通過且版本號(hào)最新的數(shù)據(jù)作為有效數(shù)據(jù)加載到內(nèi)存中。如果兩區(qū)都無(wú)效則加載默認(rèn)參數(shù)。更新參數(shù)當(dāng)需要保存參數(shù)時(shí)總是寫入到非當(dāng)前活動(dòng)區(qū)。例如當(dāng)前使用A區(qū)數(shù)據(jù)則將新參數(shù)寫入B區(qū)。原子性切換在成功寫入并校驗(yàn)B區(qū)數(shù)據(jù)后執(zhí)行一個(gè)原子操作來更新“活動(dòng)區(qū)指針”。這個(gè)指針本身必須存儲(chǔ)在另一個(gè)非??煽康牡胤奖热缌硪粋€(gè)獨(dú)立的、壽命極長(zhǎng)的EEPROM字節(jié)或者Flash中一個(gè)專門的小區(qū)域。這個(gè)操作比如將一個(gè)標(biāo)志從0xA5改為0x5A必須極快且本身是完整的即使此時(shí)斷電也只會(huì)導(dǎo)致指針停留在舊值系統(tǒng)下次啟動(dòng)仍會(huì)加載舊的有效參數(shù)而不會(huì)加載一個(gè)可能寫壞的新參數(shù)。擦除舊區(qū)成功切換指針后可以在系統(tǒng)空閑時(shí)安全地擦除舊的A區(qū)以備下次使用。這個(gè)策略的核心思想是永遠(yuǎn)有一個(gè)完整的備份并且通過一個(gè)極小的、原子的“提交”操作來完成新舊數(shù)據(jù)的切換將斷電風(fēng)險(xiǎn)窗口降到最低。4.2 日志式存儲(chǔ)與循環(huán)緩沖區(qū)對(duì)于日志、事件記錄這類只增不改、且數(shù)量可能很大的數(shù)據(jù)采用日志式存儲(chǔ)Append-Only Log配合循環(huán)緩沖區(qū)是最佳實(shí)踐。具體實(shí)現(xiàn)在SPI Flash上劃分一個(gè)大的連續(xù)區(qū)域作為日志區(qū)。每條日志記錄包含序列號(hào)、時(shí)間戳、事件類型、數(shù)據(jù)體和CRC。寫入時(shí)總是追加到下一條可用地址。只需記錄一個(gè)“寫指針”即可。當(dāng)寫指針到達(dá)日志區(qū)末尾時(shí)繞回到起始地址繼續(xù)寫覆蓋最老的記錄循環(huán)緩沖區(qū)。這意味著存儲(chǔ)空間被循環(huán)利用。需要讀取日志時(shí)可以從“讀指針”通常是最老的未過期記錄開始順序讀取。掉電保護(hù)關(guān)鍵寫指針本身是關(guān)鍵元數(shù)據(jù)必須像保護(hù)參數(shù)一樣保護(hù)它。可以采用類似“雙區(qū)備份”的策略或者將寫指針與最后幾條日志記錄一起采用“預(yù)寫式日志”WAL的方式先在一個(gè)固定位置寫入“我準(zhǔn)備更新指針為X”然后寫入日志數(shù)據(jù)最后再更新指針為X。這樣即使中途斷電也能通過掃描日志來恢復(fù)出正確的指針位置。4.3 寫緩存與批量提交頻繁的單字節(jié)或單次寫入會(huì)顯著降低Flash壽命尤其是EEPROM和SPI Flash。一個(gè)優(yōu)化策略是在RAM中開辟一塊寫緩存。例如有一個(gè)需要每秒記錄一次的溫度值。與其每秒都操作一次Flash不如在RAM中緩存最近10分鐘的數(shù)據(jù)600個(gè)值。每10分鐘或當(dāng)緩存快滿時(shí)再一次性將這批數(shù)據(jù)以緊湊的格式寫入Flash的日志區(qū)。這不僅能減少擦寫次數(shù)還能將多次小寫入合并為一次大寫入提高存儲(chǔ)空間的利用率并降低因頻繁寫入導(dǎo)致的功耗峰值。5. 磨損均衡與壞塊管理讓存儲(chǔ)介質(zhì)“延年益壽”對(duì)于Flash類介質(zhì)磨損均衡是延長(zhǎng)其使用壽命的核心技術(shù)。目標(biāo)是讓所有的物理存儲(chǔ)單元被均勻地擦寫避免某些“熱點(diǎn)”區(qū)域過早失效。5.1 軟件磨損均衡策略在沒有硬件FTL閃存轉(zhuǎn)換層的SPI Flash上我們需要在軟件層實(shí)現(xiàn)。動(dòng)態(tài)地址映射系統(tǒng)維護(hù)一個(gè)邏輯扇區(qū)號(hào)到物理扇區(qū)號(hào)的映射表。當(dāng)需要寫入一個(gè)邏輯扇區(qū)時(shí)算法總是選擇一個(gè)擦寫次數(shù)最少的物理扇區(qū)來存放新數(shù)據(jù)并更新映射表。這個(gè)映射表本身需要持久化存儲(chǔ)且必須非常可靠可考慮存多份。日志結(jié)構(gòu)文件系統(tǒng)LFS思想不直接覆蓋舊數(shù)據(jù)而是將任何更新都作為新的日志條目追加寫入。然后定期或后臺(tái)進(jìn)行“垃圾回收”將有效的數(shù)據(jù)整理到新的塊中并擦除無(wú)效數(shù)據(jù)占用的舊塊。像LittleFS、SPIFFS等嵌入式文件系統(tǒng)都內(nèi)置了這種策略。簡(jiǎn)單的扇區(qū)輪換對(duì)于參數(shù)存儲(chǔ)的雙區(qū)備份本身就是一種最簡(jiǎn)單的兩區(qū)輪換磨損均衡??梢詳U(kuò)展為多區(qū)如4個(gè)扇區(qū)循環(huán)每次寫入下一個(gè)扇區(qū)。實(shí)戰(zhàn)踩坑我曾實(shí)現(xiàn)過一個(gè)簡(jiǎn)單的四區(qū)輪換存儲(chǔ)方案。每個(gè)扇區(qū)開頭存一個(gè)遞增的序列號(hào)。啟動(dòng)時(shí)讀取序列號(hào)最大的有效扇區(qū)。問題出在一次異常斷電后兩個(gè)扇區(qū)的序列號(hào)竟然相同導(dǎo)致系統(tǒng)無(wú)法判斷哪份數(shù)據(jù)最新。后來在序列號(hào)之外又增加了精確到毫秒的時(shí)間戳作為二級(jí)判斷依據(jù)并加強(qiáng)了寫入過程的原子性才徹底解決。5.2 壞塊檢測(cè)與隔離Flash在使用過程中可能會(huì)產(chǎn)生無(wú)法擦除或?qū)懭氲膲膲K。尤其是NAND Flash在嵌入式領(lǐng)域不如NOR Flash常用壞塊是出廠就存在或使用中產(chǎn)生的正?,F(xiàn)象。管理策略初始化壞塊表首次使用或格式化時(shí)全片掃描嘗試擦除和寫入每個(gè)塊將失敗的塊標(biāo)記為壞塊記錄到一張壞塊表中。運(yùn)行時(shí)處理在動(dòng)態(tài)地址映射或文件系統(tǒng)分配空間時(shí)主動(dòng)跳過壞塊表中的塊。預(yù)留空間在規(guī)劃Flash容量時(shí)預(yù)留一部分如2-5%作為壞塊替換的冗余空間。當(dāng)某個(gè)塊變壞時(shí)用預(yù)留的好塊將其替換并更新映射關(guān)系。對(duì)于大多數(shù)使用NOR Flash如W25Q系列的應(yīng)用壞塊率極低通??梢圆蛔鰧?shí)時(shí)壞塊管理但在產(chǎn)品出廠測(cè)試和固件升級(jí)程序中加入全片讀寫校驗(yàn)的環(huán)節(jié)是很有必要的。6. 文件系統(tǒng) vs 裸機(jī)管理何時(shí)需要“重量級(jí)”方案當(dāng)你的數(shù)據(jù)不再是簡(jiǎn)單的幾個(gè)參數(shù)而是大量的文件如圖片、配置文件、語(yǔ)音包時(shí)引入一個(gè)輕量級(jí)文件系統(tǒng)FS會(huì)比裸機(jī)管理方便得多。6.1 輕量級(jí)文件系統(tǒng)選型FATFS兼容性好支持長(zhǎng)文件名在PC上可直接讀寫SD卡。但代碼量相對(duì)較大磨損均衡和掉電保護(hù)較弱更適合SD/TF卡這類本身有控制器的介質(zhì)。LittleFS由ARM mbed團(tuán)隊(duì)開發(fā)專為嵌入式Flash設(shè)計(jì)。核心特點(diǎn)是強(qiáng)大的掉電安全性和動(dòng)態(tài)磨損均衡。它采用日志結(jié)構(gòu)和COW寫時(shí)復(fù)制技術(shù)在突然斷電時(shí)能極大程度保證文件系統(tǒng)一致性。代碼量適中是目前在SPI Flash上非常推薦的選擇。SPIFFS專為SPI NOR Flash設(shè)計(jì)非常輕量API簡(jiǎn)單。但它不支持目錄所有文件都在根目錄下且磨損均衡算法相對(duì)簡(jiǎn)單。在中等容量、文件數(shù)不多的場(chǎng)景下是個(gè)不錯(cuò)的選擇。6.2 文件系統(tǒng)下的數(shù)據(jù)管理策略即使使用了文件系統(tǒng)上層的數(shù)據(jù)管理策略依然重要。關(guān)鍵參數(shù)文件化將重要的系統(tǒng)參數(shù)如system.cfg仍以整體文件的形式存儲(chǔ)。更新時(shí)采用“寫新文件-重命名替換”的原子操作。例如先寫入system.cfg.tmp寫入成功并校驗(yàn)后調(diào)用rename(system.cfg.tmp, system.cfg)。好的文件系統(tǒng)如LittleFS的rename操作是原子的能防止掉電導(dǎo)致配置損壞。日志文件滾動(dòng)對(duì)于運(yùn)行日志可以按日期或大小生成文件如log_20231027.txt。當(dāng)文件超過一定大小如64KB后就關(guān)閉當(dāng)前文件創(chuàng)建新的日志文件??梢远ㄆ趧h除最老的日志文件防止存儲(chǔ)空間被占滿。文件系統(tǒng)健康檢查在系統(tǒng)啟動(dòng)時(shí)可以嘗試掛載文件系統(tǒng)。如果掛載失敗可能由于上次異常斷電則執(zhí)行文件系統(tǒng)自帶的修復(fù)工具如LittleFS的littlefs_format或littlefs_mount時(shí)的修復(fù)選項(xiàng)嘗試恢復(fù)。同時(shí)可以記錄文件系統(tǒng)的掛載錯(cuò)誤次數(shù)超過閾值后預(yù)警或恢復(fù)出廠設(shè)置。7. 實(shí)戰(zhàn)案例一個(gè)物聯(lián)網(wǎng)終端的數(shù)據(jù)存儲(chǔ)架構(gòu)讓我們用一個(gè)具體的物聯(lián)網(wǎng)傳感終端例子把上面的策略串起來。這個(gè)終端需要每5分鐘采集一次溫濕度并存儲(chǔ)保留7天有10個(gè)可配置的系統(tǒng)參數(shù)需要記錄運(yùn)行事件日志并通過OTA升級(jí)固件。存儲(chǔ)介質(zhì)MCU片內(nèi)Flash 512KB外置4MB SPI Flash (W25Q32)。架構(gòu)設(shè)計(jì)片內(nèi)Flash分區(qū)0x08000000 - 0x08007FFF: Bootloader (32KB)。0x08008000 - 0x0803FFFF: 應(yīng)用程序主區(qū) (224KB)。0x08040000 - 0x0807FFFF: 應(yīng)用程序備份區(qū) (256KB用于OTA接收新固件)。0x080E0000 - 0x080E0FFF: 參數(shù)區(qū)A (4KB)。0x080E1000 - 0x080E1FFF: 參數(shù)區(qū)B (4KB)。參數(shù)采用雙區(qū)備份CRC校驗(yàn)策略。0x080FF000 - 0x080FFFFF: OTA標(biāo)志與狀態(tài)區(qū) (4KB)。存儲(chǔ)當(dāng)前運(yùn)行分區(qū)、新固件校驗(yàn)和等信息。SPI Flash管理前256KB移植LittleFS文件系統(tǒng)。在LittleFS中創(chuàng)建以下文件/目錄/cfg/sys.param存儲(chǔ)系統(tǒng)參數(shù)實(shí)際上參數(shù)主要仍在片內(nèi)Flash這里可存?zhèn)浞莼驍U(kuò)展參數(shù)。/log/event_20231027.log按日期滾動(dòng)的文本日志文件。/data/sensor_202310.bin按月存儲(chǔ)的壓縮后的傳感器二進(jìn)制數(shù)據(jù)文件。每5分鐘的數(shù)據(jù)打包成一個(gè)記錄追加寫入當(dāng)月文件。/ota/packet.binOTA升級(jí)時(shí)臨時(shí)存放的固件包。LittleFS負(fù)責(zé)底層壞塊管理和磨損均衡。數(shù)據(jù)流傳感器數(shù)據(jù)采集后在RAM中緩存。每積累1小時(shí)12條記錄進(jìn)行一次壓縮并追加寫入到SPI Flash的當(dāng)月數(shù)據(jù)文件中。事件日志如“設(shè)備重啟”、“連接服務(wù)器成功”實(shí)時(shí)以文本格式寫入當(dāng)日的日志文件。系統(tǒng)參數(shù)修改時(shí)按雙區(qū)備份策略寫入片內(nèi)Flash。OTA升級(jí)時(shí)新固件下載到/ota/packet.bin校驗(yàn)通過后復(fù)制到片內(nèi)Flash的應(yīng)用程序備份區(qū)更新OTA標(biāo)志區(qū)然后重啟由Bootloader完成切換。這個(gè)架構(gòu)綜合運(yùn)用了多種介質(zhì)、分區(qū)策略、文件系統(tǒng)和原子操作在有限的資源下實(shí)現(xiàn)了可靠、高效且易于維護(hù)的數(shù)據(jù)存儲(chǔ)。它可能不是最精簡(jiǎn)的但在產(chǎn)品化過程中穩(wěn)定性和可維護(hù)性帶來的收益遠(yuǎn)大于那一點(diǎn)點(diǎn)額外的代碼和存儲(chǔ)空間。存儲(chǔ)無(wú)小事在嵌入式開發(fā)中多花一點(diǎn)心思設(shè)計(jì)存儲(chǔ)策略能為產(chǎn)品的長(zhǎng)期穩(wěn)定運(yùn)行打下最堅(jiān)實(shí)的基礎(chǔ)。