存儲方案:從原理到掉電保存實戰(zhàn))
開頭先聊點(diǎn)實際的我在一個工業(yè)數(shù)據(jù)采集項目里需要在高噪聲、頻繁掉電的環(huán)境下保存關(guān)鍵運(yùn)行參數(shù)最初用的是Flash結(jié)果寫壞過扇區(qū)、掉電丟數(shù)據(jù)后來換成MR25H40CDF這顆MRAM芯片搭配STM32F042K6主控整套存儲方案才算真正穩(wěn)下來。這篇博文就圍繞這個組合講清楚為什么選MRAM、怎么接線、怎么把讀寫代碼跑起來、以及工業(yè)場景里那些容易踩的坑。如果你正在評估嵌入式存儲方案或者準(zhǔn)備在STM32上接MRAM這篇內(nèi)容可以直接作為參考。1. MRAM是什么選型前的核心認(rèn)知1.1 一句話看懂MRAM工作原理MRAMMagnetoresistive Random Access Memory磁性隨機(jī)存儲器的核心存儲單元是一個磁性隧道結(jié)它由兩層鐵磁材料和中間一層極薄的絕緣勢壘組成。兩層鐵磁材料的磁化方向平行時隧道結(jié)電阻低代表數(shù)據(jù)“0”反平行時電阻高代表數(shù)據(jù)“1”。讀取數(shù)據(jù)時就是檢測這個電阻狀態(tài)寫數(shù)據(jù)時通過改變自由層的磁化方向來實現(xiàn)。這個原理帶來的直接好處是數(shù)據(jù)存儲依賴磁化方向而不是電荷所以斷電后數(shù)據(jù)不會丟而且它不涉及電子捕獲或隧穿導(dǎo)致的電荷泄漏也就沒有Flash那樣的擦寫損耗和寫前擦除操作。MRAM的寫入本質(zhì)是改變磁化狀態(tài)不是注入電荷理論上寫壽命幾乎無限MR25H40CDF標(biāo)稱超過10^14次。這一點(diǎn)在工業(yè)設(shè)備里非常重要因為設(shè)備參數(shù)、計數(shù)器、運(yùn)行日志這些數(shù)據(jù)可能會頻繁寫入如果還按Flash的“先擦后寫”思路很容易把扇區(qū)擦壞。1.2 同場競技MRAM與FRAM、Flash、BBSRAM的真實差距工業(yè)嵌入式存儲里常被放在一起比較的方案是Flash、FRAM、電池供電的BBSRAM以及MRAM。我做過一個對比表方便你直接看差異特性MRAMMR25H40CDFFRAM如FM25V10NOR Flash如W25Q64BBSRAM寫壽命10^14次10^12次10^4~10^5次/扇區(qū)接近無限寫前擦除無無必須按扇區(qū)擦除無單字節(jié)寫入支持支持不支持最小擦除單位是扇區(qū)支持寫入速度最快40MHz SPI單命令寫類似SRAM總線速度擦除慢寫入也受頁限制并行總線快數(shù)據(jù)保持10年105°C約10年10~20年靠電池需維護(hù)抗輻射/抗干擾強(qiáng)磁存儲不怕電離輻射較強(qiáng)一般一般從表格能看出來MRAM最大的優(yōu)勢就是“無限寫壽命 無需擦除 掉電不丟”。FRAM寫壽命也很高但容量偏低、成本偏高Flash雖然便宜但在頻繁寫入場景下要么加均衡算法要么就得定期搬運(yùn)扇區(qū)復(fù)雜度上去了還不一定扛得住BBSRAM速度快但要外接電池、還要擔(dān)心電池壽命和更換周期。MRAM幾乎是這幾項屬性的平衡點(diǎn)。1.3 MR25H40CDF關(guān)鍵參數(shù)解讀MR25H40CDF是Everspin現(xiàn)算恒憶/Infineon旗下的4Mbit SPI接口MRAM工作電壓2.7V到3.6V正好匹配STM32F042K6的3.3V供電。具體參數(shù)容量4Mbit即512KB按頁256字節(jié)組織共2048頁SPI時鐘最高40MHz比大部分MRAM芯片的25MHz要高寫保護(hù)支持WP引腳硬件寫保護(hù)以及狀態(tài)寄存器里的軟件寫保護(hù)位休眠模式支持Sleep命令待機(jī)電流可以降到微安級別數(shù)據(jù)保持105°C環(huán)境下大于10年工業(yè)溫度范圍-40°C到105°C封裝DFN8/DFN8L等體積小適合緊湊型設(shè)計這里有個細(xì)節(jié)要提醒MR25H40CDF是SPI接口的MRAM所以它與MCU的接線本質(zhì)上就是和一個SPI NOR Flash的接線方式類似差別在不需要“擦除”這個環(huán)節(jié)代碼上簡化了很多。2. 硬件接線與SPI模式選擇STM32F042K6怎么連MR25H40CDF2.1 引腳對接全解STM32F042K6是Cortex-M0內(nèi)核、主頻48MHz內(nèi)置1路SPI/I2SPA5、PA6、PA7正好可以復(fù)用為SPI1的SCK、MISO、MOSI配合PB0做片選。我把MR25H40CDF和STM32的引腳對應(yīng)關(guān)系整理成表MR25H40CDF引腳功能連接目標(biāo)CS#片選低有效STM32 GPIO如PB0推挽輸出SCKSPI時鐘STM32 SPI1_SCKPA5SI串行輸入STM32 SPI1_MOSIPA7SO串行輸出STM32 SPI1_MISOPA6WP#寫保護(hù)低有效可接PB1或直接上拉到VccHOLD#暫停通信低有效可接PB2或直接上拉到VccVcc電源3.3V并接0.1uF去耦電容Vss地系統(tǒng)地我實際項目的做法是WP#和HOLD#都不直接固定而是各用一個GPIO控制。雖然它們都有內(nèi)部上拉傾向但工業(yè)環(huán)境里電平不穩(wěn)與其賭芯片默認(rèn)狀態(tài)不如讓程序上電時主動拉高。只有CS#、SCK、SI、SO是真正必須掛在SPI上的信號WP和HOLD做普通IO控。2.2 SPI模式為什么必須是SPI Mode 0MR25H40CDF的數(shù)據(jù)手冊里寫明它支持SPI Mode 0CPOL0CPHA0。意思是SCK空閑時為低電平數(shù)據(jù)在SCK上升沿被采樣。STM32的SPI外設(shè)可以配置成Mode 0或Mode 3但必須明確選擇Mode 0。我把這個配置寫進(jìn)SPI初始化代碼里SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE);分頻系數(shù)用4意味著48MHz主頻下SPI時鐘是12MHz。這個速度在40MHz上限內(nèi)同時在工業(yè)PCB布線上也比較容易保證信號完整性。如果后續(xù)要提速可以改分頻到224MHz但要注意走線長度和地平面質(zhì)量。2.3 布局與電源工業(yè)現(xiàn)場的隱藏細(xì)節(jié)MRAM本身不挑電源但對紋波敏感——這和所有數(shù)字存儲芯片都一樣我卻見過不止一次因為電源紋波過大導(dǎo)致讀取數(shù)據(jù)偶爾出錯的情況。布線時注意三點(diǎn)Vcc引腳旁放0.1uF陶瓷電容盡量靠近芯片引腳如果PCB空間允許再加一個1uF鉭電容做低頻去耦CS#、SCK、SI的走線要遠(yuǎn)離電機(jī)驅(qū)動、繼電器控制線至少保持5mm以上間距實在不行就加地線隔離如果系統(tǒng)里同時有DC-DC電源模塊MRAM和MCU的3.3V最好從同一路穩(wěn)壓輸出取避免兩個電源域之間出現(xiàn)壓差工業(yè)現(xiàn)場的干擾往往是共模噪聲SPI信號線短且粗、走線在同一層、參考地完整一般都能扛住。我建議在PCB上把這幾個信號線都加上串聯(lián)電阻比如22歐姆既能降低振鈴也能在異常插拔時保護(hù)引腳。3. 讀寫操作實現(xiàn)從指令集到完整代碼3.1 指令集8條指令解決所有問題MR25H40CDF的指令集非常簡潔通過SPI發(fā)送一條8位命令即可完成對應(yīng)操作指令名操作碼說明WREN0x06設(shè)置寫使能鎖存位WEL1寫/寫狀態(tài)寄存器前必須執(zhí)行WRDI0x04清除寫使能鎖存位WEL0RDID0x9F讀取廠商/設(shè)備IDRDSR0x05讀取狀態(tài)寄存器WRSR0x01寫狀態(tài)寄存器READ0x03從指定地址開始順序讀WRITE0x02從指定地址開始寫入1~N字節(jié)SLEEP0xB9進(jìn)入深度休眠WAKE0xAB喚醒休眠中任何非WAKE指令都會被忽略其實嚴(yán)格說是9條SLEEP和WAKE是成對出現(xiàn)的。這個指令集和普通SPI Flash很像所以如果你之前寫過W25Q系列的驅(qū)動搬到MRAM上會很順——唯一區(qū)別是WRITE指令后不再需要等待“擦寫時間”寫完就是寫完。3.2 狀態(tài)寄存器與寫使能鎖存狀態(tài)寄存器一共8位其中真正用到的是bit1WEL和bit6WPEN。WEL是寫使能鎖存位。MRAM要求每一次WRITE或WRSR命令之前必須先發(fā)WREN0x06指令把WEL置1。這和SPI Flash的邏輯一致。RDSR讀出來的bit1如果是1說明寫使能已經(jīng)打開如果是0寫命令會被忽略。WPEN是軟件寫保護(hù)使能位它和硬件引腳WP#配合工作。當(dāng)WPEN1且WP#引腳為低時整個芯片被保護(hù)WRITE和WRSR都被忽略WPEN0時WP#引腳的電平不影響寫操作。我一般把WPEN保持為0用GPIO控制WP#引腳這樣既有硬件保護(hù)手段又不至于鎖死無法恢復(fù)。3.3 讀操作與讀模式分析READ命令格式是先拉低CS#發(fā)送0x03然后發(fā)送3字節(jié)地址高位在前之后每一個SCK周期從SO輸出一個字節(jié)數(shù)據(jù)。地址是24位對4Mbit來說真正有效的只有低19位0x000000~0x07FFFF高5位應(yīng)為0。我一般封裝一個讀函數(shù)uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; GPIO_ResetBits(GPIOB, GPIO_Pin_0); // CS# low SPI_SendByte(0x03); // READ command SPI_SendByte((addr 16) 0xFF); // addr[23:16] SPI_SendByte((addr 8) 0xFF); // addr[15:8] SPI_SendByte(addr 0xFF); // addr[7:0] data SPI_SendByte(0x00); // read one byte GPIO_SetBits(GPIOB, GPIO_Pin_0); // CS# high return data; }SPI_SendByte這個底層函數(shù)在發(fā)送一個字節(jié)的同時會從MISO收到一個字節(jié)所以讀數(shù)據(jù)時發(fā)送0x00作為虛擬時鐘。地址會自動遞增連續(xù)讀時不需要重新發(fā)送命令。有個細(xì)節(jié)值得注意MRAM讀操作支持整個地址空間的連續(xù)尋址地址從0x07FFFF翻轉(zhuǎn)到0x000000時不會出錯。這個特性在讀取大型日志記錄時很有用因為不需要像Flash那樣按頁處理。3.4 寫操作完整時序?qū)懖僮鞯臅r序和讀類似只是把命令換成0x02后續(xù)跟著地址和數(shù)據(jù)字節(jié)。數(shù)據(jù)可以是一個字節(jié)也可以連續(xù)多個字節(jié)CS#在整個過程中保持低電平。我封裝的寫字節(jié)函數(shù)void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); // send WREN command GPIO_ResetBits(GPIOB, GPIO_Pin_0); // CS# low SPI_SendByte(0x02); // WRITE command SPI_SendByte((addr 16) 0xFF); // addr[23:16] SPI_SendByte((addr 8) 0xFF); // addr[15:8] SPI_SendByte(addr 0xFF); // addr[7:0] SPI_SendByte(data); // data byte GPIO_SetBits(GPIOB, GPIO_Pin_0); // CS# high }關(guān)鍵點(diǎn)在于CS#必須在發(fā)完數(shù)據(jù)之后才拉高。如果CS#提前拉高這次寫操作就會被中斷寫入的數(shù)據(jù)不保證正確。另外一次WRITE命令可以寫任意長度的字節(jié)只要CS#不放下來地址會自動遞增——這個特性在寫日志緩沖區(qū)時很有用。寫操作本身是即時完成的不需要查狀態(tài)位等“忙標(biāo)志”。這是MRAM和Flash最大的體驗差異沒有擦除等待、沒有編程等待寫完就是寫完了。3.5 寫保護(hù)與休眠模式工業(yè)設(shè)備上電和掉電瞬間GPIO狀態(tài)不穩(wěn)定如果此時MRAM正好被錯誤地寫操作命中可能會寫入臟數(shù)據(jù)。為了避免這種情況硬件上我會把WP#引腳連到一個默認(rèn)拉低的GPIO系統(tǒng)初始化完成后再拉高。這樣在MCU復(fù)位、時鐘未建立、GPIO未配置的窗口期MRAM保持硬件寫保護(hù)狀態(tài)。睡眠模式在低功耗設(shè)備中很重要。發(fā)送SLEEP命令后芯片進(jìn)入深度休眠電流降到微安級任何命令都可以當(dāng)“喚醒信號”但手冊建議用WAKE指令。我實測過發(fā)送WAKE后大概需要等30到50微秒才能執(zhí)行下一條正常命令所以代碼里喚醒后要加個小延時否則第一筆讀操作可能讀到無效數(shù)據(jù)。4. 工業(yè)場景的可靠性設(shè)計與數(shù)據(jù)安全4.1 掉電瞬間如何保住最后一條數(shù)據(jù)工業(yè)設(shè)備最怕的不只是掉電而是“半掉電”——電壓在閾值附近抖動MCU處于復(fù)位邊緣程序跑到一半被掐死。此時如果正在寫MRAM會不會把數(shù)據(jù)寫壞MRAM的物理寫入過程是磁化方向翻轉(zhuǎn)由芯片內(nèi)部時序控制。只要CS#和數(shù)據(jù)線在掉電瞬間沒有異常擾動寫入操作要么完整完成要么不執(zhí)行。這和Flash的“寫一半然后整片扇區(qū)損壞”完全不同。但要在工程上真正可靠不能只靠芯片特性還需要配合掉電檢測電路。我項目里的方案是用一顆外部電壓檢測芯片比如TCM809監(jiān)控3.3V電壓當(dāng)電壓跌到2.93V以下時立即觸發(fā)STM32的EXTI中斷在中斷里把WP#拉低、關(guān)閉MRAM寫使能。這樣即便MCU主循環(huán)還在亂跑硬件寫保護(hù)也能把最后的寫入動作屏蔽掉。4.2 ARM Cortex-M0中斷與臨界區(qū)保護(hù)STM32F042K6的Cortex-M0沒有硬件優(yōu)先級分組寄存器沒有NVIC的PRIMASK那種細(xì)粒度控制想實現(xiàn)臨界區(qū)保護(hù)常用的方法是關(guān)中斷__disable_irq(); MrDrv_WaitReady(); // for MRAM, virtually no wait MRAM_WriteStatusRegister(0x00); __enable_irq();要說明的是MRAM寫操作本身就很快不像寫EEPROM要等幾個毫秒。所以“臨界區(qū)”其實非常短通常也就是幾十個時鐘周期。真正要防的不是寫操作本身被中斷而是中斷服務(wù)程序里碰了SPI外設(shè)導(dǎo)致總線狀態(tài)錯亂。因此我推薦MRAM讀寫相關(guān)的SPI收發(fā)都放在同一個任務(wù)或同一個鎖內(nèi)中斷服務(wù)程序里只置標(biāo)志位不要在中斷里直接調(diào)MRAM驅(qū)動。另外STM32F042K6的SPI1有一個FIFO發(fā)送和接收是解耦的。我在驅(qū)動里會確保每筆SPI傳輸都完整執(zhí)行完再允許上下文切換。方法很簡單所有MRAM驅(qū)動函數(shù)不帶阻塞等待、不主動讓出CPU天然就是原子化的。4.3 數(shù)據(jù)完整性CRC校驗與壞塊管理MRAM雖然寫壽命長、不會出現(xiàn)Flash那種“壞塊”但工業(yè)現(xiàn)場的電磁干擾、MCU程序跑飛、SPI時序被擾亂依然可能導(dǎo)致個別字節(jié)出錯。所以我在設(shè)計存儲協(xié)議時還是加了常見的數(shù)據(jù)完整性手段每個記錄塊頭部存4字節(jié)魔數(shù) 2字節(jié)長度記錄塊里保存32位CRC校驗值讀數(shù)據(jù)時先校驗?zāi)?shù)和CRC不對就丟棄并重新從備份區(qū)讀取我通常把數(shù)據(jù)存兩份主區(qū)和備份區(qū)交替寫入。讀的時候先讀主區(qū)CRC校驗失敗就切備份區(qū)。這個策略在MRAM上實現(xiàn)成本很低因為不需要像Flash那樣為了磨損均衡維護(hù)復(fù)雜的地址映射——MRAM寫不壞你可以用最樸素的“兩個固定地址塊”方案簡單可靠。5. 實測踩坑記錄與問題排查速查表5.1 坑一片選懸空導(dǎo)致寫錯地址項目第一次打樣MRAM偶爾會在運(yùn)行時寫入錯誤地址的數(shù)據(jù)。排查到最后發(fā)現(xiàn)CS#引腳在沒有GPIO控制時處于高阻態(tài)而MRAM的CS#內(nèi)部并沒有強(qiáng)上拉稍微一點(diǎn)噪聲就會讓它在高低電平之間抖動。此時SCK和SI上只要有數(shù)據(jù)信號芯片就可能把它當(dāng)成一次有效的命令序列。解決方式CS#直接接MCU GPIO推挽輸出并確保初始化GPIO時先把它拉高再初始化SPI外設(shè)。所有配置順序里片選必須是最早被穩(wěn)定住的那個信號。5.2 坑二狀態(tài)寄存器讀到0x00現(xiàn)象是發(fā)RDSR后讀到的值始終是0x00但芯片是好的寫操作也能成功。后來發(fā)現(xiàn)是SPI模式配錯了我的初始化代碼里CPHA設(shè)置成了2Edge即Mode 1MRAM在Mode 1下雖然能輸出部分?jǐn)?shù)據(jù)但MISO采樣點(diǎn)不對導(dǎo)致讀回全是0。改成CPHA1Edge后正常。排查這類問題有個小技巧先用邏輯分析儀抓取CS#、SCK、MOSI、MISO四根線的波形和手冊時序圖對比。肉眼就能看出采樣點(diǎn)錯位的問題。5.3 坑三電機(jī)干擾下的總線錯誤這個項目在設(shè)備旁有一臺三相電機(jī)啟動瞬間電流沖擊很大導(dǎo)致MRAM讀取偶爾返回錯誤數(shù)據(jù)。示波器看波形發(fā)現(xiàn)SPI的MISO線上疊加了大量毛刺。處理策略分三步一是把SPI時鐘從12MHz降到6MHz分頻8讓信號抗干擾能力更強(qiáng)二是MISO線上加一個10k歐姆上拉電阻到3.3V提高信號電平的驅(qū)動強(qiáng)度三是在靠近電機(jī)的位置加一個磁珠減少傳導(dǎo)干擾。改完后連續(xù)跑了72小時一次錯誤都沒有出現(xiàn)。5.4 更多問題排查速查表現(xiàn)象可能原因處理建議寫入后讀回數(shù)據(jù)全為0xFFWREN指令沒發(fā)WEL位為0確保每次WRITE前執(zhí)行WREN寫入后讀回數(shù)據(jù)全為0x00CS#被拉低后提前拉高寫中斷檢查CS#時序?qū)懭胱詈笠蛔止?jié)后再拉高數(shù)據(jù)偶發(fā)出錯電源紋波過大加強(qiáng)Vcc去耦必要時加LDO喚醒后第一筆讀數(shù)據(jù)失敗WAKE后延時不夠加50us以上延時再發(fā)READ寫操作無效且WPEN讀回1軟件寫保護(hù)鎖死WRSR清WPEN前提是WP#引腳為高多個設(shè)備共用SPI總線MRAM被誤寫片選競爭每個設(shè)備的CS#用獨(dú)立GPIO切換前先拉高所有CS#最后再分享一個我自己用慣了的小技巧新板子回來后先用RDSR讀狀態(tài)寄存器驗證SPI通信鏈路能讀到0x02說明芯片正常工作讀不到就先查硬件不要急著跑讀寫測試。這個動作我每次都會做省掉過很多無謂的排錯時間。MR25H40CDF STM32F042K6這套組合只要硬件上把CS#、WP#、HOLD#三個控制腳處理干凈軟件上記得每次寫前發(fā)WREN讀寫的穩(wěn)定性會讓你省心很多在頻繁記錄、掉電保存、抗干擾要求高的工業(yè)場景里MRAM真的是那種能給你兜底的選擇。