級SPI MRAM與PIC24FJ128GA310的嵌入式存儲實戰(zhàn))
1. 為什么工業(yè)現(xiàn)場還在用 SPI 接 MRAM而不是直接上 Flash如果你拆過工業(yè)控制器、電力監(jiān)測終端或者車載數(shù)據(jù)記錄儀大概率會在板子上看到一顆 8 腳的小芯片旁邊連著主控的 SPI 引腳。這顆芯片很多時候不是普通的 NOR Flash而是MRAM——磁性隨機(jī)存儲器。我這次要聊的組合就是MR25H40CDF這顆 512Kb 的 SPI MRAM搭配PIC24FJ128GA310這款 16 位單片機(jī)做工業(yè)與嵌入式場景下的數(shù)據(jù)存儲和讀取。先說清楚這個組合解決什么問題。工業(yè)現(xiàn)場的數(shù)據(jù)記錄有個很尷尬的特點寫入頻繁、單次數(shù)據(jù)量小、掉電隨時可能發(fā)生、環(huán)境溫度跨度大而且設(shè)備往往要求十年以上的生命周期。用 EEPROM 吧寫入速度慢、擦寫壽命有限用 NOR Flash 吧寫之前要擦除整個扇區(qū)掉電時正在擦除的扇區(qū)可能直接損壞用 FRAM 吧容量小、價格高。MRAM 恰好卡在中間像 RAM 一樣按字節(jié)隨機(jī)寫入不需要擦除掉電數(shù)據(jù)不丟讀寫壽命幾乎無限。MR25H40CDF 是 Everspin 的 512Kb64KBSPI MRAM40MHz SPI 接口2.7V 到 3.6V 供電工業(yè)級溫度范圍 -40 到 85 攝氏度。PIC24FJ128GA310 是 Microchip 的 16 位 MCU128KB Flash、8KB RAM帶多個 SPI 模塊本身定位就是工業(yè)控制。兩者搭配一個負(fù)責(zé)永不丟失的快速暫存區(qū)一個負(fù)責(zé)邏輯控制和協(xié)議處理是很典型的工業(yè)數(shù)據(jù)記錄架構(gòu)。這篇文章適合誰看如果你正在做工業(yè)數(shù)據(jù)采集、設(shè)備參數(shù)存儲、掉電保護(hù)記錄、黑匣子類的功能或者你手上有 PIC24 平臺但不確定怎么把 MRAM 用穩(wěn)那這篇內(nèi)容可以直接抄作業(yè)。我會從硬件連接、SPI 時序配置、讀寫驅(qū)動、掉電保護(hù)策略、常見踩坑幾個角度完整講一遍中間會穿插我實際調(diào)試時遇到的問題和解決辦法。提示MRAM 和 Flash 最大的區(qū)別是寫之前不用擦。這個特性決定了驅(qū)動層的設(shè)計思路完全不同后面會重點展開。2. MR25H40CDF 的引腳、時序與 PIC24FJ128GA310 的硬件對接2.1 先搞清楚 MR25H40CDF 的引腳定義和工作模式MR25H40CDF 是 8 腳 SOIC 封裝引腳定義很標(biāo)準(zhǔn)引腳名稱功能說明1CS片選低電平有效2SO數(shù)據(jù)輸出MISO3WP寫保護(hù)低電平有效4VSS地5SI數(shù)據(jù)輸入MOSI6SCK時鐘7HOLD保持低電平有效8VDD電源 2.7V~3.6V這里有兩個容易被忽略的引腳WP 和 HOLD。WP 拉低時芯片進(jìn)入硬件寫保護(hù)所有寫操作被拒絕HOLD 拉低時當(dāng)前 SPI 傳輸暫停時鐘可以繼續(xù)跑但數(shù)據(jù)不采樣。很多人在原理圖階段直接把這兩個腳接地結(jié)果發(fā)現(xiàn)寫不進(jìn)去數(shù)據(jù)排查半天才發(fā)現(xiàn)是 WP 被拉低了。我的建議是WP 和 HOLD 都不要直接接地而是通過 10K 上拉到 VDD。如果確實需要硬件寫保護(hù)功能WP 可以接到 MCU 的一個 GPIO 上正常工作時輸出高電平需要保護(hù)時拉低。HOLD 如果不使用上拉到 VDD 即可不要懸空。2.2 PIC24FJ128GA310 的 SPI 模塊配置要點PIC24FJ128GA310 有多個 SPI 模塊我用的是 SPI1。配置 SPI 的核心寄存器是 SPI1CON1、SPI1CON2、SPI1STAT。工業(yè)場景下我推薦的配置是主模式MSTEN 1時鐘極性CKP 0空閑低電平時鐘邊沿CKE 1數(shù)據(jù)在時鐘上升沿采樣字長MODE16 08 位模式預(yù)分頻主預(yù)分頻 4:1次預(yù)分頻 2:1MR25H40CDF 支持 SPI Mode 0 和 Mode 3。Mode 0 是 CKP0、CKE1也就是空閑低、上升沿采樣。這個模式和大多數(shù) SPI Flash 一致配置起來最省事。關(guān)于時鐘頻率MR25H40CDF 最高支持 40MHz但 PIC24FJ128GA310 在 32MHz 系統(tǒng)時鐘下SPI 最高能跑到 16MHzFCY/2。實際工業(yè)場景我不建議跑滿8MHz 左右是比較穩(wěn)的選擇。原因有兩個一是長走線時高速 SPI 的信號完整性會變差二是 MRAM 的寫入時序在高溫下需要留裕量。計算一下如果 FCY 16 MIPS32MHz 晶振PLL 后主預(yù)分頻 4:1、次預(yù)分頻 2:1SPI 時鐘 16MHz / (4 × 2) 2MHz。這個速度對 MRAM 來說偏慢但勝在穩(wěn)定。如果要提速可以把主預(yù)分頻改成 2:1得到 4MHz再改成 1:1得到 8MHz。// PIC24FJ128GA310 SPI1 初始化示例 void SPI1_Init(void) { SPI1STATbits.SPIEN 0; // 先關(guān)閉 SPI SPI1CON1bits.MSTEN 1; // 主模式 SPI1CON1bits.CKP 0; // 空閑低電平 SPI1CON1bits.CKE 1; // 上升沿采樣 SPI1CON1bits.SMP 0; // 中間采樣 SPI1CON1bits.MODE16 0; // 8 位模式 SPI1CON1bits.PPRE 0b10; // 主預(yù)分頻 4:1 SPI1CON1bits.SPRE 0b110; // 次預(yù)分頻 2:1 SPI1STATbits.SPIROV 0; // 清除溢出標(biāo)志 SPI1STATbits.SPIEN 1; // 使能 SPI }2.3 硬件走線和去耦的實戰(zhàn)經(jīng)驗SPI 雖然只有四根線但在工業(yè)板上走不好照樣出問題。我踩過的坑包括SCK 和 MISO 平行走線太長導(dǎo)致串?dāng)_、CS 沒有單獨走線而是和別的信號共用、去耦電容離芯片太遠(yuǎn)。正確的做法是MRAM 的 VDD 引腳旁邊放一個 0.1uF 的陶瓷電容距離不超過 2mm。如果板子上有多個 SPI 從設(shè)備每個從設(shè)備的 CS 都要獨立走線不要用譯碼器共用。SCK 走線盡量短如果超過 5cm建議在源端串一個 22 到 33 歐姆的電阻做阻抗匹配。還有一點MR25H40CDF 的 WP 和 HOLD 上拉電阻不要省。我見過一個板子為了省兩個電阻把 WP 直接接 VDD、HOLD 懸空結(jié)果在濕度大的環(huán)境下 HOLD 引腳受干擾SPI 通信間歇性失敗。后來補(bǔ)上 10K 上拉就穩(wěn)定了。3. 驅(qū)動層怎么寫從單字節(jié)讀寫到頁寫入的完整實現(xiàn)3.1 MR25H40CDF 的指令集和地址空間MR25H40CDF 的指令集很精簡常用的就幾條指令操作碼功能WREN0x06寫使能WRDI0x04寫禁止RDSR0x05讀狀態(tài)寄存器WRSR0x01寫狀態(tài)寄存器READ0x03讀數(shù)據(jù)WRITE0x02寫數(shù)據(jù)地址空間是 64KB地址從 0x0000 到 0xFFFF用 16 位地址尋址。注意 MR25H40CDF 的 READ 和 WRITE 指令后面跟的是 16 位地址高位在前。狀態(tài)寄存器的 bit0 是 WEL寫使能鎖存bit1 是 BP0bit2 是 BP1bit7 是 SRWD。寫操作之前必須先發(fā) WREN 指令把 WEL 置 1寫完自動清零。這個和 EEPROM、Flash 的邏輯一樣。3.2 單字節(jié)讀寫函數(shù)最基礎(chǔ)也最容易寫錯的部分先看讀一個字節(jié)// 讀取 MR25H40CDF 指定地址的一個字節(jié) uint8_t MRAM_ReadByte(uint16_t addr) { uint8_t data; MRAM_CS_LOW(); // 片選拉低 SPI1_ExchangeByte(0x03); // 發(fā)送 READ 指令 SPI1_ExchangeByte(addr 8); // 地址高字節(jié) SPI1_ExchangeByte(addr 0xFF); // 地址低字節(jié) data SPI1_ExchangeByte(0xFF); // 讀數(shù)據(jù) MRAM_CS_HIGH(); // 片選拉高 return data; }寫一個字節(jié)稍微復(fù)雜因為要先發(fā) WREN// 寫入 MR25H40CDF 指定地址的一個字節(jié) void MRAM_WriteByte(uint16_t addr, uint8_t data) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte(addr 8); SPI1_ExchangeByte(addr 0xFF); SPI1_ExchangeByte(data); MRAM_CS_HIGH(); // 等待寫入完成 while(MRAM_ReadStatus() 0x01); }這里有個關(guān)鍵點WREN 和 WRITE 之間必須拉高 CS。很多人習(xí)慣把 WREN 和 WRITE 放在同一個 CS 低電平周期里這對 MRAM 是不行的。MR25H40CDF 要求 WREN 指令在獨立的 CS 周期內(nèi)完成然后再開一個新的 CS 周期發(fā) WRITE。3.3 頁寫入和連續(xù)讀取提升吞吐量的關(guān)鍵單字節(jié)寫雖然簡單但每次都要發(fā) WREN、發(fā)地址、等寫入完成效率很低。MR25H40CDF 支持頁寫入一頁是 256 字節(jié)。連續(xù)寫的時候地址低 8 位會自動遞增超過 255 會回繞到頁首所以跨頁寫入必須手動分頁。// 頁寫入注意不能跨頁 void MRAM_WritePage(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t page_end (addr 0xFF00) 0x100; if(addr len page_end) { len page_end - addr; // 截斷到頁邊界 } MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte(addr 8); SPI1_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { SPI1_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); while(MRAM_ReadStatus() 0x01); }連續(xù)讀取就簡單多了發(fā)完 READ 指令和起始地址后可以一直讀地址自動遞增直到 CS 拉高。// 連續(xù)讀取 void MRAM_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x03); SPI1_ExchangeByte(addr 8); SPI1_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { buf[i] SPI1_ExchangeByte(0xFF); } MRAM_CS_HIGH(); }3.4 SPI 字節(jié)交換函數(shù)的實現(xiàn)細(xì)節(jié)上面反復(fù)用到的SPI1_ExchangeByte是底層函數(shù)實現(xiàn)要注意等待標(biāo)志位uint8_t SPI1_ExchangeByte(uint8_t txData) { SPI1BUF txData; while(!SPI1STATbits.SPIRBF); // 等待接收完成 return SPI1BUF; }這里有個坑必須先寫 SPI1BUF 再等 SPIRBF。如果反過來先等標(biāo)志再寫第一次調(diào)用會死等。另外讀 SPI1BUF 會自動清 SPIRBF 標(biāo)志所以不需要手動清。注意PIC24 的 SPI 在高速時鐘下如果兩個字節(jié)之間間隔太短可能出現(xiàn) SPIROV 溢出。工業(yè)場景建議在連續(xù)讀寫時檢查 SPIROV如果置位要重新初始化 SPI。4. 掉電保護(hù)與數(shù)據(jù)可靠性MRAM 真正的價值所在4.1 為什么 MRAM 的掉電保護(hù)比 Flash 簡單得多Flash 的掉電保護(hù)是個老大難問題。寫 Flash 要先擦除扇區(qū)擦除過程中如果掉電整個扇區(qū)數(shù)據(jù)可能變成 0xFF 或者不確定狀態(tài)。所以用 Flash 做數(shù)據(jù)記錄通常要設(shè)計雙備份、日志結(jié)構(gòu)、磨損均衡代碼復(fù)雜度很高。MRAM 沒有這個問題。MR25H40CDF 的寫入是直接覆蓋不需要擦除寫入過程中掉電要么是舊數(shù)據(jù)要么是新數(shù)據(jù)不會出現(xiàn)中間態(tài)。這個特性讓掉電保護(hù)邏輯大大簡化。我實際項目里的做法是把 64KB 空間分成若干條記錄每條記錄帶一個序號和 CRC 校驗。寫入時先寫數(shù)據(jù)區(qū)再寫序號和 CRC。讀取時掃描所有記錄找序號最大且 CRC 正確的作為最新數(shù)據(jù)。因為 MRAM 寫入快單字節(jié)寫入時間在微秒級掉電窗口極小。4.2 用狀態(tài)寄存器做寫保護(hù)的分級策略MR25H40CDF 的狀態(tài)寄存器有 BP0 和 BP1 兩位可以設(shè)置不同級別的寫保護(hù)BP1BP0保護(hù)范圍00無保護(hù)01保護(hù)高 1/4 地址空間10保護(hù)高 1/2 地址空間11保護(hù)整個地址空間工業(yè)場景下我通常把參數(shù)區(qū)放在低地址記錄區(qū)放在高地址。正常運行時全片可寫當(dāng)設(shè)備進(jìn)入?yún)?shù)鎖定模式時把 BP10、BP01保護(hù)高 1/4 空間防止記錄區(qū)被誤寫。這個策略比軟件標(biāo)志位可靠因為它是硬件級別的保護(hù)。4.3 寫入完成判斷輪詢狀態(tài)寄存器還是固定延時MRAM 的寫入速度很快但并不是發(fā)完就完。WRITE 指令的最后一個字節(jié)移入后芯片內(nèi)部還需要時間完成實際存儲。這個時間典型值是幾十納秒到幾百納秒但數(shù)據(jù)手冊給的最大值是 5ms實際上遠(yuǎn)小于這個值。我見過兩種做法一種是發(fā)完 WRITE 后直接延時 1ms 再操作另一種是輪詢狀態(tài)寄存器的 WIP 位。推薦輪詢方式因為延時是拍腦袋的輪詢是確定性的。uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI1_ExchangeByte(0x05); status SPI1_ExchangeByte(0xFF); MRAM_CS_HIGH(); return status; }狀態(tài)寄存器的 bit0 是 WIPWrite In Progress寫入完成時該位為 0。輪詢這個位比固定延時靠譜得多。4.4 數(shù)據(jù)校驗CRC16 在 PIC24 上的輕量實現(xiàn)工業(yè)數(shù)據(jù)不能只靠寫進(jìn)去就行必須有校驗。我在 PIC24FJ128GA310 上用的是一個查表法的 CRC16-CCITT表放在 Flash 里運行時查表計算。64KB 數(shù)據(jù)全量校驗一次大概幾十毫秒對于周期性記錄場景完全夠用。// CRC16-CCITT 查表法 uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while(len--) { crc (crc 8) ^ crc16_table[((crc 8) ^ *data) 0xFF]; } return crc; }每條記錄的結(jié)構(gòu)我設(shè)計成4 字節(jié)序號 N 字節(jié)數(shù)據(jù) 2 字節(jié) CRC。讀取時先校驗 CRC再比較序號確保拿到的是完整且最新的數(shù)據(jù)。5. 調(diào)試過程中真實踩過的坑和排查思路5.1 讀出來全是 0xFF 或者 0x00這是最常見的現(xiàn)象。第一次調(diào) MRAM讀出來全是 0xFF第一反應(yīng)是芯片壞了。其實大概率是三個原因第一CS 沒有正確拉低。用邏輯分析儀抓一下 CS 引腳確認(rèn)讀操作期間 CS 確實是低電平。我遇到過 CS 配置成開漏輸出但沒加上拉導(dǎo)致 CS 一直是高。第二SPI 模式不對。MR25H40CDF 支持 Mode 0 和 Mode 3如果 MCU 配成 Mode 1 或 Mode 2數(shù)據(jù)會錯位。用示波器看 SCK 空閑電平Mode 0 空閑低Mode 3 空閑高。第三WP 引腳被拉低。這個最隱蔽因為讀操作不受 WP 影響但寫操作會被拒絕。如果你發(fā)現(xiàn)讀正常但寫不進(jìn)去先量一下 WP 引腳電壓。5.2 寫入后讀出來數(shù)據(jù)不對但偶爾又對這種間歇性錯誤通常是時序問題。我遇到過一次SPI 時鐘跑到 16MHz短走線時正常但板子裝進(jìn)金屬外殼后分布電容變化導(dǎo)致 SCK 邊沿變緩數(shù)據(jù)采樣出錯。后來把時鐘降到 8MHz問題消失。另一個可能是寫入后沒有等待完成就發(fā)起下一次操作。MRAM 雖然快但連續(xù)寫不同地址時如果不等 WIP 清零就發(fā)下一條 WRITE芯片可能忽略第二條指令。我的做法是每次寫操作后都輪詢 WIP雖然犧牲一點速度但穩(wěn)定性大幅提升。5.3 高溫環(huán)境下數(shù)據(jù)出錯工業(yè)設(shè)備要做高低溫測試。我在 85 攝氏度環(huán)境下測試時發(fā)現(xiàn)偶爾有記錄丟失。排查后發(fā)現(xiàn)兩個問題一是 SPI 時鐘在高溫下裕量不足二是去耦電容用的是 Y5V 材質(zhì)高溫下容量衰減嚴(yán)重。解決辦法SPI 時鐘從 8MHz 降到 4MHz去耦電容換成 X7R 材質(zhì)。改完之后高低溫循環(huán)測試 100 次沒有再出現(xiàn)數(shù)據(jù)錯誤。提示工業(yè)級 MRAM 雖然標(biāo)稱 -40 到 85 攝氏度但外圍電路的溫漂同樣要考慮。電容、電阻的材質(zhì)在極端溫度下表現(xiàn)差異很大。5.4 用邏輯分析儀抓 SPI 波形的正確姿勢調(diào)試 SPI 問題邏輯分析儀比示波器好用。抓波形時注意幾點采樣率至少是 SCK 頻率的 10 倍觸發(fā)條件設(shè)成 CS 下降沿解碼協(xié)議選 SPI模式選對。我習(xí)慣同時抓 CS、SCK、MOSI、MISO 四根線這樣一眼就能看出是命令發(fā)錯了還是數(shù)據(jù)錯了。有一次我抓到的波形顯示W(wǎng)REN 指令發(fā)出后 CS 拉高但下一個 CS 周期開始時 SCK 上有一個毛刺。這個毛刺導(dǎo)致 MRAM 誤判指令。后來在 SCK 線上串了一個 33 歐姆電阻毛刺消失。6. 把 MRAM 用出價值的幾個進(jìn)階思路6.1 用 MRAM 做參數(shù)存儲和日志記錄的分區(qū)設(shè)計64KB 空間說大不大說小不小。我的分區(qū)方案是低 16KB 做參數(shù)區(qū)存設(shè)備配置、校準(zhǔn)系數(shù)、序列號中間 32KB 做循環(huán)日志區(qū)每條記錄 64 字節(jié)可以存 512 條高 16KB 做備份區(qū)存關(guān)鍵參數(shù)的副本。參數(shù)區(qū)用固定地址訪問讀寫簡單。日志區(qū)用環(huán)形緩沖寫滿后覆蓋最舊記錄。備份區(qū)在參數(shù)更新時同步寫入主參數(shù)區(qū)損壞時可以恢復(fù)。6.2 和 Flash 配合使用的混合存儲架構(gòu)MRAM 容量小、價格高不適合存大量數(shù)據(jù)。我的做法是MRAM 做熱數(shù)據(jù)緩存Flash 做冷數(shù)據(jù)歸檔。實時采集的數(shù)據(jù)先寫 MRAM保證掉電不丟當(dāng) MRAM 里的記錄積累到一定數(shù)量再批量搬運到 Flash。這樣既利用了 MRAM 的快速寫入和掉電安全又利用了 Flash 的大容量和低成本。搬運過程要注意先寫 Flash校驗成功后再清 MRAM 對應(yīng)區(qū)域避免搬運過程中掉電導(dǎo)致數(shù)據(jù)丟失。6.3 低功耗場景下的 MRAM 使用技巧MRAM 的待機(jī)電流很低但 SPI 接口在空閑時如果 CS 一直拉低芯片會持續(xù)耗電。低功耗設(shè)計時每次操作完成后把 CS 拉高讓 MRAM 進(jìn)入待機(jī)模式。另外如果 MCU 支持可以把 SPI 引腳配置成普通 GPIO 輸出低電平進(jìn)一步降低漏電流。我做過一個電池供電的記錄儀MRAM 平時待機(jī)電流不到 10 微安每分鐘喚醒一次寫一條記錄整體功耗可以做到幾年不用換電池。6.4 從 MR25H40CDF 遷移到更大容量 MRAM 的注意事項如果 64KB 不夠用Everspin 有更大容量的 SPI MRAM比如 1Mb、4Mb 的型號。遷移時要注意幾點地址位數(shù)可能從 16 位變成 24 位指令集基本兼容但地址字節(jié)數(shù)不同頁大小可能變化寫入分頁邏輯要調(diào)整封裝可能不同PCB 要改版。我的建議是驅(qū)動層做好抽象把地址寬度、頁大小做成宏定義遷移時只改配置不改邏輯。7. 寫在最后的一些個人體會這套 MR25H40CDF 加 PIC24FJ128GA310 的組合我在三個工業(yè)項目里用過最長的已經(jīng)連續(xù)運行四年多沒有出現(xiàn)過數(shù)據(jù)丟失。MRAM 這個器件最大的價值不是速度快而是它讓嵌入式工程師不用再為掉電時數(shù)據(jù)會不會丟這件事焦慮。用 Flash 做記錄你要設(shè)計日志、磨損均衡、掉電檢測、備份恢復(fù)代碼寫幾千行用 MRAM核心邏輯可能幾百行就搞定了。當(dāng)然 MRAM 也不是萬能的。容量小、單價高這兩點決定了它只能用在關(guān)鍵數(shù)據(jù)的存儲上。我的經(jīng)驗是凡是丟了會出大事的數(shù)據(jù)放 MRAM凡是丟了可以重采的數(shù)據(jù)放 Flash 或者外部存儲。這個原則幫我在多個項目里快速做了存儲架構(gòu)決策。最后分享一個小技巧MRAM 剛上電時建議先讀一遍狀態(tài)寄存器確認(rèn) BP0、BP1 都是 0WEL 是 0。如果狀態(tài)不對先寫狀態(tài)寄存器復(fù)位再做其他操作。這個習(xí)慣能避免很多莫名其妙寫不進(jìn)去的問題。