
先描述一個我前幾天剛處理的現(xiàn)場。一塊STM32F103C8T6的板子用來做一個小型儀表需要用掉電保存的方式存校準(zhǔn)參數(shù)和累計(jì)數(shù)據(jù)。圖省事直接選了ST官方的X-CUBE-EEPROM模擬庫按例程初始化、讀寫、連續(xù)開關(guān)機(jī)測試一切都很順利。結(jié)果換到產(chǎn)線上一批新板子大約有百分之三的板子一上電就卡死看門狗反復(fù)復(fù)位。接上調(diào)試器一看EE_Init()的返回值清清楚楚寫著EE_NO_PAGE_FOUND。這個錯誤對第一次用這個庫的開發(fā)者來說真的很容易踩到。官方的API文檔里只寫了一句“找不到有效頁”至于什么情況下找不到、為什么找不到、怎么定位和修復(fù)基本要靠自己從代碼里翻。這篇文章我就圍繞這個錯誤拆開講清楚X-CUBE-EEPROM在Flash上模擬EEPROM的機(jī)制、EE_NO_PAGE_FOUND產(chǎn)生的具體環(huán)節(jié)以及我排查這個問題時的完整路徑和修復(fù)方案。準(zhǔn)備做掉電存儲、正在用這個庫、或者已經(jīng)遇到類似報錯的朋友可以對照自己的工程一步步查下去。1. 先搞清楚這個報錯到底從哪來1.1 一個典型的故障現(xiàn)場先說現(xiàn)場。上電后程序卡死EE_Init()返回EE_NO_PAGE_FOUND我第一反應(yīng)是Flash擦除不干凈于是用STM32CubeProgrammer把整片F(xiàn)lash做了Mass Erase重新燒錄固件板子恢復(fù)正常。但過了兩天又有一批板子出現(xiàn)同樣問題這就不是“偶然沒擦干凈”能解釋的了。拿故障板逐一檢查發(fā)現(xiàn)這批板的EEPROM模擬區(qū)在出廠測試時被測試程序?qū)懭肓藬?shù)據(jù)后來燒錄正式固件時只擦除了程序區(qū)沒有擦EEPROM模擬區(qū)。新固件的模擬區(qū)起始地址和頁大小配置跟測試程序不一樣庫里找不到它認(rèn)識的“頁頭”自然報EE_NO_PAGE_FOUND。這類問題的根源不是代碼邏輯而是Flash上的歷史數(shù)據(jù)殘留和配置不匹配。還有一類非常隱蔽的故障程序里加入了低功耗模式在EE_Write寫Flash過程中遇到掉電頁頭寫到一半頁狀態(tài)處于中間態(tài)。下次上電時庫嘗試恢復(fù)發(fā)現(xiàn)頁頭校驗(yàn)不通過把它當(dāng)作壞頁處理如果所有頁都進(jìn)入這種狀態(tài)同樣會報這個錯誤。1.2 X-CUBE-EEPROM是干什么的X-CUBE-EEPROM是ST官方發(fā)布的軟件擴(kuò)展包解決的是“MCU內(nèi)部沒有EEPROM又不想外掛一顆”的場景。它借助STM32內(nèi)部的Flash用軟件方式模擬出一塊可頻繁寫入的小容量存儲區(qū)提供類似EEPROM的EE_Write和EE_Read接口支持按虛擬地址讀寫數(shù)據(jù)。不少人一聽到Emulator就聯(lián)想到Android模擬器那一套其實(shí)完全兩碼事。這里的Emulator是在MCU內(nèi)部的、利用Flash模擬非易失存儲的軟件層。X-CUBE-EEPROM本質(zhì)上是ST對“Flash模擬EEPROM”方案的封裝屏蔽了底層頁管理、磨損均衡和掉電恢復(fù)細(xì)節(jié)。它適合的MCU帶內(nèi)部Flash、存儲參數(shù)在幾千字節(jié)以內(nèi)、寫入頻率不極端的場景比如儀表校準(zhǔn)參數(shù)、設(shè)備序列號、累計(jì)運(yùn)行時間、用戶配置項(xiàng)等。它的典型接口就幾個EE_Init()負(fù)責(zé)初始化存儲區(qū)并找到當(dāng)前有效頁EE_Write()寫入一個虛擬地址對應(yīng)的數(shù)據(jù)EE_Read()讀出來。用起來很簡單但正因?yàn)楹唵魏芏嗳撕雎粤怂澈蟮捻摴芾磉壿嬕坏┏鲥e就無從下手。1.3 錯誤碼表里它排在哪在庫的錯誤碼枚舉中EE_NO_PAGE_FOUND是其中一個狀態(tài)。不同版本枚舉定義略有差異但通常包括以下幾個含義明確的錯誤碼錯誤碼含義EE_OK操作成功EE_BUSYFlash正在忙上次操作未完成EE_RANGE虛擬地址越界EE_NO_PAGE沒有空閑頁可用EE_NO_VALID_PAGE存在頁但沒有有效頁EE_NO_VALID_ADDRESS沒有找到對應(yīng)虛擬地址的有效數(shù)據(jù)EE_NO_PAGE_FOUND在初始化或頁轉(zhuǎn)移時沒有找到任何可用頁EE_NO_PAGE_FOUND在錯誤碼體系里屬于“災(zāi)難級”錯誤因?yàn)樗ǔ3霈F(xiàn)在系統(tǒng)啟動階段。一旦EE_Init()返回這個錯后面所有讀寫都不可用程序基本只能停機(jī)或進(jìn)錯誤處理分支。2. 追根溯源它為何會找不到“頁”2.1 模擬EEPROM的基本原理要理解這個錯誤得先明白為什么需要“頁”。Flash和EEPROM最大的區(qū)別是擦除粒度EEPROM能按字節(jié)擦寫而內(nèi)部Flash只能按扇區(qū)或頁整片擦除擦除后每一位回到1。因此直接在Flash上按“某個地址存某個值”的方式做掉電保存更新一次數(shù)據(jù)就要擦除整個扇區(qū)效率低且壽命差。X-CUBE-EEPROM采用了一種類似日志文件的策略把Flash劃分成多個頁寫入時只在當(dāng)前活動頁的末尾追加一條新記錄而不是修改舊記錄。每次寫入包含“虛擬地址數(shù)據(jù)”的記錄舊記錄暫時保留等頁寫滿了再做一次垃圾回收把最新數(shù)據(jù)挑出來復(fù)制到新頁然后擦除舊頁。這樣把頻繁小寫入分?jǐn)偟秸麄€頁空間避免反復(fù)擦除同一扇區(qū)也提升了寫入次數(shù)。這就是“模擬”二字的核心它不再像真實(shí)EEPROM那樣按字節(jié)尋址而是用“頁記錄”的方式實(shí)現(xiàn)非易失存儲。庫把數(shù)據(jù)結(jié)構(gòu)封裝好對上層提供虛擬地址讀寫接口底層則是一套頁管理狀態(tài)機(jī)。2.2 頁的生命周期與狀態(tài)機(jī)每個頁的開頭都有一小塊頁頭數(shù)據(jù)里面記錄了頁的狀態(tài)、頁標(biāo)識、以及這個頁里管理的虛擬地址信息。頁頭狀態(tài)決定了這個頁在庫的工作流中處于什么角色通常包括這幾類已擦除狀態(tài)整頁都是0xFF可以被當(dāng)作新頁使用。接收/寫入狀態(tài)正在往這個頁追加記錄但尚未真正激活。有效狀態(tài)當(dāng)前正在被使用的頁可以被讀寫。待擦除狀態(tài)已經(jīng)完成數(shù)據(jù)轉(zhuǎn)移等待被擦除。EE_Init()啟動后做的事情就是遍歷所有頁讀取頁頭識別出哪些頁是合法的有效頁并把其中一個選定為當(dāng)前活動頁。如果頁頭內(nèi)容完整且正確庫就能正常繼續(xù)如果所有頁頭都無法被識別庫就會認(rèn)為這里沒有可用頁拋出EE_NO_PAGE_FOUND。這非常像在雜亂的倉庫里找一份特定編號的貨單如果所有貨單都被撕得只剩碎片倉庫管理員只能告訴你“找不到”。Flash里的數(shù)據(jù)殘留、被其他程序覆蓋過的區(qū)域、或者掉電導(dǎo)致頁頭寫了一半都會造成這種“找不到”。2.3 兩個最常觸發(fā)EE_NO_PAGE_FOUND的環(huán)節(jié)排查這個問題時要區(qū)分它是在哪個調(diào)用點(diǎn)觸發(fā)的因?yàn)樘幚矸绞酵耆煌?。第一個環(huán)節(jié)是EE_Init()初始化時。這個場景下庫需要找一個可用的有效頁作為工作頁如果所有頁的頁頭都不符合預(yù)期立即返回錯誤。最常見的誘因是Flash模擬區(qū)域根本沒有被正確初始化過或者區(qū)域里是別的數(shù)據(jù)。比如程序里配置的模擬區(qū)起始地址被其他代碼段占用編譯出來的固件實(shí)際把程序?qū)戇M(jìn)了模擬區(qū)Flash上自然找不到有效頁頭。第二個環(huán)節(jié)是垃圾回收或頁轉(zhuǎn)移Page Transfer過程中。活動頁寫滿之后庫需要在剩余頁里找一個空閑頁來搬移數(shù)據(jù)如果所有剩余頁都處于非空閑或損壞狀態(tài)庫會在這個點(diǎn)返回EE_NO_PAGE_FOUND。這通常是因?yàn)轫摂?shù)量配置得過少或者某次掉電寫入破壞了多個頁的狀態(tài)導(dǎo)致轉(zhuǎn)移無法繼續(xù)。我遇到的產(chǎn)線故障屬于第一類但排障過程中必須把兩個環(huán)節(jié)都考慮到不然修好這個場景下一個場景又會踩雷。3. 一步步排查從配置到落盤3.1 先核對庫的配置別急著懷疑代碼邏輯遇到EE_NO_PAGE_FOUND先別把重心放在找代碼bug上最優(yōu)先的檢查項(xiàng)是庫的配置文件。X-CUBE-EEPROM的配置通常在eeprom_conf.h或eeprom_cfg.h里核心是三個參數(shù)模擬區(qū)起始地址、頁大小、頁數(shù)量。我碰到的第一次誤判就是這個環(huán)節(jié)一開始以為是Flash損壞反復(fù)擦除燒錄后來用CubeProgrammer查看Flash內(nèi)存才發(fā)現(xiàn)模擬區(qū)起始地址配錯了。比如STM32F103C8T6的Flash從0x08000000開始固件本身占用了前32KB如果配置里把模擬區(qū)起始地址寫在0x08000000庫會把程序代碼當(dāng)成頁數(shù)據(jù)去解析頁頭解析當(dāng)然全部失敗。正確做法是先把起始地址放在固件結(jié)束之后留出足夠余量的位置同時確認(rèn)頁大小等于MCU實(shí)際的Flash扇區(qū)大小。不同型號的扇區(qū)大小不一樣有的是1KB有的是2KB甚至4KB務(wù)必查手冊確認(rèn)不能照抄別人的例程。配置項(xiàng)檢查要點(diǎn)模擬區(qū)起始地址必須在固件占用區(qū)域之后且與某個扇區(qū)邊界對齊頁大小必須等于MCU實(shí)際扇區(qū)大小否則地址錯位頁數(shù)量至少2頁推薦4頁以上太少會導(dǎo)致頻繁垃圾回收3.2 檢查Flash上的實(shí)際狀態(tài)配置沒問題之后第二步是看Flash上到底有什么。用STM32CubeProgrammer連接板子在Memory窗口直接跳到模擬區(qū)起始地址一頁一頁地看內(nèi)容。如果是全0xFF說明是干凈的已擦除狀態(tài)。這種情況下EE_Init()理論上應(yīng)該能自動初始化第一頁不應(yīng)報錯。如果此時仍然報錯就要檢查是不是庫版本里把“首次初始化”邏輯放在某個條件后面或者配置方式有誤。如果看到的是隨機(jī)數(shù)據(jù)、全0x00、或者明顯的程序代碼說明這個區(qū)域之前被寫入過別的內(nèi)容。這時候分兩種情況如果是開發(fā)階段直接擦除這幾個扇區(qū)再試如果是產(chǎn)線就要檢查燒錄流程是否漏了擦除步驟。我踩過的一個坑是用J-Flash燒錄時只下載了應(yīng)用程序固件沒有包含EEPROM模擬區(qū)的擦除動作導(dǎo)致舊測試數(shù)據(jù)一直殘留在Flash里正式程序一跑就報錯。檢查Flash內(nèi)容時有個小技巧如果頁頭的前幾個字節(jié)能看出來像是狀態(tài)標(biāo)記比如常見的一些狀態(tài)值可以手動比對是不是庫能識別的格式如果完全是一堆亂碼基本可以判斷是區(qū)域被別的數(shù)據(jù)占了。3.3 代碼調(diào)用時序與動態(tài)擴(kuò)容排查配置和Flash內(nèi)容都正常仍然報錯就得從代碼調(diào)用時序出發(fā)。我見過一個比較典型的問題有人為了“提速”在EE_Init()完成前就提前調(diào)用了EE_Write()庫內(nèi)部的頁表還沒建立寫操作發(fā)生異常之后初始化就卡在錯誤狀態(tài)。正確的調(diào)用順序一定是系統(tǒng)上電先調(diào)用EE_Init()確認(rèn)返回EE_OK后再進(jìn)行任何EE讀寫操作。這個順序被打破時很多詭異問題都會冒出來。另一個是動態(tài)擴(kuò)容場景。有些項(xiàng)目在運(yùn)行中動態(tài)調(diào)整存儲策略比如從每字節(jié)一條記錄改成批量寫入如果調(diào)用EE_Write時頁空間不足且沒有空閑頁可用就可能觸發(fā)垃圾回收邏輯而垃圾回收需要空閑頁沒有空閑頁就報EE_NO_PAGE_FOUND。換句話說不是初始化出問題而是運(yùn)行到中途存儲區(qū)滿了。這種情況要重點(diǎn)看頁數(shù)量是否足夠。以我常用的4頁配置為例假設(shè)每頁1KB每4字節(jié)一條記錄一頁能存256條記錄。如果應(yīng)用每秒寫一條一頁不到半天就寫滿必然頻繁觸發(fā)垃圾回收如果回收時機(jī)被其他中斷拖住就容易累積異常狀態(tài)。生產(chǎn)環(huán)境建議根據(jù)寫入頻率和寫入量估算頁數(shù)量是否夠用不要機(jī)械照搬例程。3.4 修復(fù)后的完整驗(yàn)證流程定位到原因、修改配置或擦除Flash后一定要跑一套完整的驗(yàn)證流程不能只驗(yàn)證“現(xiàn)在能開機(jī)”。我的驗(yàn)證步驟是這樣的第一步全新燒錄固件上電后確認(rèn)EE_Init()返回EE_OK隨后寫入一組已知數(shù)據(jù)斷電再上電讀取校驗(yàn)確認(rèn)掉電保存生效。第二步連續(xù)寫入幾百次每次改寫不同虛擬地址期間人為斷電幾次確認(rèn)數(shù)據(jù)不丟并且不進(jìn)入錯誤分支。第三步用調(diào)試器把Flash模擬區(qū)域填充成隨機(jī)數(shù)據(jù)模擬歷史殘留再上電確認(rèn)程序能識別異常狀態(tài)并且不會卡死。第三步很多人會忽略但恰恰是模擬“產(chǎn)線不良板”最有效的手段。程序應(yīng)當(dāng)對EE_Init()失敗有明確的后續(xù)策略比如進(jìn)入恢復(fù)模式、恢復(fù)默認(rèn)參數(shù)并重新初始化存儲區(qū)而不是直接死循環(huán)。這不僅是修bug更是在做產(chǎn)品級的健壯性。4. 踩坑記錄與問題速查4.1 我在這個錯誤上踩過的坑這里集中寫幾個我實(shí)際踩過、并且花費(fèi)了不少時間才弄明白的細(xì)節(jié)希望能幫你少走彎路。第一個坑是“調(diào)試器復(fù)位后報錯直接上電不報錯”。這個現(xiàn)象特別迷惑。原因是調(diào)試器在連接過程中可能把模擬區(qū)內(nèi)存改寫或破壞了也可能是復(fù)位瞬間調(diào)試器訪問了Flash。當(dāng)時我花了半天懷疑EEPROM驅(qū)動有問題最后發(fā)現(xiàn)是調(diào)試器配置里把Flash下載算法設(shè)置成了全片擦除每次按復(fù)位鍵都觸發(fā)了擦除。所以遇到只在調(diào)試模式下復(fù)現(xiàn)的問題先檢查調(diào)試器對Flash的訪問設(shè)置。第二個坑是“換了一個庫版本后突然報錯”。X-CUBE-EEPROM不同版本的頁頭格式可能不兼容。老版本寫的頁頭新版本解析時可能不識別導(dǎo)致找不到有效頁。這個問題在開發(fā)中途升級庫時特別容易踩。如果你升級過庫又遇到了EE_NO_PAGE_FOUND別急著查業(yè)務(wù)邏輯先確認(rèn)是否需要做一次數(shù)據(jù)遷移或者徹底擦除重建。第三個坑是“看門狗復(fù)位導(dǎo)致寫入中斷”??撮T狗超時復(fù)位時如果恰好正在執(zhí)行EE_WriteFlash寫入被中斷頁頭可能處于半寫狀態(tài)。雖然庫理論上支持掉電恢復(fù)但極端情況下仍可能損壞頁狀態(tài)。解決辦法是寫入Flash的代碼段里臨時關(guān)閉看門狗或者把寫Flash的任務(wù)優(yōu)先級調(diào)高防止被頻繁打斷寫完成后再恢復(fù)看門狗。4.2 EE_NO_PAGE_FOUND問題速查表最后整理一張速查表方便你對照排查。實(shí)際項(xiàng)目里大部分場景都能落到下面幾類原因中?,F(xiàn)象可能原因解決方法全新板首次上電報錯模擬區(qū)起始地址配置錯誤被程序代碼覆蓋修正起始地址確保在固件末尾之后且扇區(qū)對齊同批次部分板子報錯產(chǎn)線燒錄流程未擦除舊數(shù)據(jù)或舊固件占用模擬區(qū)燒錄流程中增加整片擦除或指定區(qū)域擦除程序運(yùn)行一段時間后報錯頁數(shù)量不足垃圾回收時無空閑頁增加頁數(shù)量或降低寫入頻率、合并寫入調(diào)試器連接時報錯直接上電正常調(diào)試器下載算法全片擦除或調(diào)試過程破壞了Flash修改調(diào)試器Flash下載算法只下載程序區(qū)域升級庫版本后報錯新舊版本頁頭格式不兼容備份數(shù)據(jù)后徹底擦除重建或做數(shù)據(jù)遷移看門狗復(fù)位后偶發(fā)報錯Flash寫入被復(fù)位中斷頁狀態(tài)損壞寫Flash區(qū)域臨時關(guān)閉看門狗或提高寫任務(wù)優(yōu)先級排查EE_NO_PAGE_FOUND總體思路就是三層先查配置對不對再查Flash里實(shí)際有什么最后查調(diào)用時序和運(yùn)行壓力。絕大多數(shù)情況下問題出在前兩層——配置錯位或者歷史數(shù)據(jù)殘留。把這三步走完基本都能定位到根因。我做這類調(diào)試時還有個習(xí)慣在EE_Init()返回非EE_OK時把具體的錯誤碼和模擬區(qū)首地址通過調(diào)試串口打出來這樣即使板子在沒有調(diào)試器的環(huán)境下運(yùn)行也能通過日志快速判斷問題類型。加上這個輸出之后產(chǎn)線定位故障的效率會高很多建議你也這么干。