存儲方案:SPI 驅(qū)動與掉電保護實戰(zhàn))
1. 項目緣起與方案選型1.1 為什么要在工業(yè)場景里折騰 MRAM 和 PIC 單片機工業(yè)現(xiàn)場的數(shù)據(jù)存儲有個很尷尬的夾縫地帶用 EEPROM 吧寫入速度慢得讓人抓狂擦寫次數(shù)也就百萬次級別高頻記錄日志的場合撐不了多久用 SRAM 加后備電池吧電池在高溫高濕環(huán)境下三五年就鼓包漏液維護成本高得離譜用 Flash 吧寫之前得先擦一整塊掉電瞬間還可能把整塊數(shù)據(jù)搞成半吊子狀態(tài)。我前幾年做一個電力監(jiān)測終端項目現(xiàn)場環(huán)境溫度夏天能到 70 度客戶要求每 200 毫秒記錄一次關(guān)鍵參數(shù)掉電不能丟數(shù)據(jù)這個需求直接把 EEPROM 和 SRAM 方案都斃掉了。后來接觸到MRAM磁性隨機存儲器這東西的特性正好卡在這個縫隙里。以MR25H40CDF為例它是 4Mbit512K×8的 SPI 接口磁存儲器寫入不需要擦除、字節(jié)級隨機寫入、寫入速度可以跑到 SPI 總線的極限、理論上擦寫次數(shù)接近無限、數(shù)據(jù)保持時間超過 20 年而且工作溫度范圍覆蓋工業(yè)級甚至車規(guī)級。這些特性組合起來對于工業(yè)數(shù)據(jù)記錄、參數(shù)存儲、掉電保護這類場景幾乎是量身定做的。主控這邊選PIC18F4610原因也很實在。它是 Microchip 經(jīng)典的 8 位增強型單片機64 引腳自帶 MSSP 模塊可以硬件驅(qū)動 SPI程序存儲器 64KBRAM 接近 4KB片上還有 EEPROM。在工業(yè)儀表、傳感器節(jié)點這類對成本敏感、對可靠性要求高、又不需要跑操作系統(tǒng)的場合PIC18 系列是很多老工程師的默認選擇。它的開發(fā)工具鏈成熟編譯器穩(wěn)定資料多出了問題好查。把這兩個東西湊到一起核心要解決的問題就是在資源受限的 8 位平臺上用 SPI 總線可靠地完成 MRAM 的讀寫并且保證掉電、干擾、時序異常等工業(yè)現(xiàn)場常見狀況下數(shù)據(jù)不丟不錯。這篇文章就把我實際做這個方案時踩過的坑、驗證過的參數(shù)、寫過的驅(qū)動代碼完整地攤開講一遍。1.2 方案整體架構(gòu)與數(shù)據(jù)流設(shè)計整個系統(tǒng)的數(shù)據(jù)流其實不復雜但每一層都有講究。PIC18F4610 作為 SPI 主機MR25H40CDF 作為從機中間四根線SCK、SDI、SDO、CS。PIC18 的 MSSP 模塊配置成 SPI 主模式時鐘極性、相位要跟 MRAM 的時序要求對齊。MRAM 這邊支持最高 40MHz 的 SPI 時鐘但 PIC18F4610 在 40MHz 晶振下 SPI 時鐘最高只能到 10MHzFosc/4所以實際跑 10MHz 是安全的我實測下來在 10MHz 下讀寫都穩(wěn)。數(shù)據(jù)組織上我把 512KB 的空間做了分區(qū)前 4KB 存設(shè)備配置參數(shù)和校準系數(shù)中間 500KB 做循環(huán)日志區(qū)最后 8KB 做掉電保護緩沖區(qū)。循環(huán)日志區(qū)用寫指針加序號的方式管理每次寫入遞增序號寫滿一圈后覆蓋最舊的數(shù)據(jù)。掉電保護緩沖區(qū)用來在檢測到電源異常時把 RAM 里還沒落盤的關(guān)鍵數(shù)據(jù)緊急寫進去。提示MRAM 雖然寫入不需要擦除但 SPI 通信本身是有時序要求的CS 拉低到第一個時鐘沿之間需要滿足 tSLCH 時間這個參數(shù)在數(shù)據(jù)手冊里寫的是最小 5ns實際用 10MHz 時鐘時基本不用額外延時但如果你的 SPI 時鐘跑到 20MHz 以上就要認真算一下了。2. 硬件連接與底層驅(qū)動細節(jié)2.1 引腳連接與硬件設(shè)計要點PIC18F4610 的 MSSP 模塊引腳是固定的RC3 是 SCKRC4 是 SDI主機輸入RC5 是 SDO主機輸出。CS 我用了 RC2 做軟件片選因為硬件片選在 PIC18 上配置起來反而麻煩而且軟件片選靈活可以在兩次操作之間精確控制 CS 的拉高拉低。MR25H40CDF 這邊VDD 接 3.3VVSS 接地SCK、SI、SO 分別對接 PIC 的 SCK、SDO、SDICS 接 RC2。注意 MRAM 的 SI 是數(shù)據(jù)輸入對應主機的 SDO別接反了我見過有人把 SI 和 SO 接反讀出來全是 0xFF查了半天。電源去耦這塊MRAM 的 VDD 引腳旁邊一定要放一個 0.1uF 的陶瓷電容越近越好。工業(yè)現(xiàn)場電源紋波大MRAM 在寫入瞬間電流會有波動去耦不好會導致寫入數(shù)據(jù)出錯。我在一個項目里因為去耦電容離得太遠批量生產(chǎn)后有 3% 的板子出現(xiàn)偶發(fā)寫入錯誤后來把電容挪到引腳 2mm 以內(nèi)就再沒出現(xiàn)過。PCB 走線上SPI 的四根線盡量等長尤其是 SCK 和 SDO如果走線長度超過 10cm建議在 SCK 上串一個 22 歐姆的電阻做阻抗匹配減少過沖和振鈴。CS 線如果走線長也要注意因為 CS 的邊沿如果太緩MRAM 可能識別不到有效的片選信號。2.2 SPI 初始化配置與參數(shù)計算PIC18F4610 的 MSSP 配置寄存器是 SSPCON1 和 SSPSTAT。SPI 主模式的關(guān)鍵配置位SSPCON1 的 SSPM3:SSPM0 設(shè)為 0000 表示主模式時鐘由 Fosc/4 決定CKP 位決定空閑時鐘電平MRAM 要求空閑時為低電平所以 CKP0SSPSTAT 的 CKE 位決定數(shù)據(jù)在時鐘的哪個邊沿采樣MRAM 支持模式 0 和模式 3我用模式 0即 CKE0數(shù)據(jù)在時鐘上升沿采樣下降沿變化。時鐘頻率計算PIC18F4610 用 40MHz 晶振時Fosc/4 10MHzSPI 時鐘周期 100ns。MRAM 的時序要求里SCK 高電平時間和低電平時間最小都是 10ns10MHz 下高電平 50ns、低電平 50ns余量充足。如果你用 20MHz 晶振Fosc/4 5MHz更慢但更穩(wěn)。我建議工業(yè)場景不要追求極限速度10MHz 已經(jīng)足夠穩(wěn)定性優(yōu)先。初始化代碼大概長這樣void SPI_Init(void) { TRISC3 0; // SCK 輸出 TRISC5 0; // SDO 輸出 TRISC4 1; // SDI 輸入 TRISC2 0; // CS 輸出 CS 1; // 初始拉高 SSPCON1 0x00; // 主模式時鐘 Fosc/4CKP0 SSPSTAT 0x00; // CKE0模式 0 SSPEN 1; // 使能 MSSP }讀寫一個字節(jié)的函數(shù)unsigned char SPI_Exchange(unsigned char data) { SSPBUF data; while(!SSPIF); // 等待傳輸完成 SSPIF 0; return SSPBUF; }注意SSPIF 標志位在寫入 SSPBUF 后不會立即置位要等 8 個時鐘周期傳輸完成才置位。如果你在中斷里調(diào)用 SPI 函數(shù)要確保沒有其他中斷打斷這個等待循環(huán)否則可能死等。我一般會在等待循環(huán)里加一個超時計數(shù)器防止硬件故障時卡死。3. MRAM 讀寫操作完整實現(xiàn)3.1 命令集與操作時序解析MR25H40CDF 的命令集很簡潔常用的就幾條WREN0x06寫使能WRDI0x04寫禁止RDSR0x05讀狀態(tài)寄存器WRSR0x01寫狀態(tài)寄存器READ0x03讀數(shù)據(jù)WRITE0x02寫數(shù)據(jù)。注意 MRAM 沒有擦除命令這是它跟 Flash 最大的區(qū)別寫入就是直接覆蓋。寫操作的完整時序拉低 CS發(fā)送 WREN 命令拉高 CS再拉低 CS發(fā)送 WRITE 命令然后發(fā)送 3 字節(jié)地址24 位地址因為 512KB 需要 19 位地址但 MRAM 用 3 字節(jié)地址格式接著發(fā)送要寫入的數(shù)據(jù)字節(jié)可以連續(xù)發(fā)送多個字節(jié)地址自動遞增最后拉高 CS 結(jié)束。這里有個關(guān)鍵點WREN 和 WRITE 之間必須拉高 CS讓 MRAM 內(nèi)部鎖存寫使能狀態(tài)如果 WREN 之后不拉高 CS 直接發(fā) WRITE寫使能不會生效數(shù)據(jù)寫不進去。讀操作簡單一些拉低 CS發(fā)送 READ 命令發(fā)送 3 字節(jié)地址然后連續(xù)讀取數(shù)據(jù)字節(jié)地址自動遞增讀完拉高 CS。狀態(tài)寄存器的 bit0 是 WIPWrite In ProgressMRAM 寫入速度極快WIP 位通常幾個微秒就清零了但為了保險寫完關(guān)鍵數(shù)據(jù)后我還是會輪詢一下 WIP確認寫入完成再繼續(xù)。3.2 字節(jié)寫入與頁寫入的取舍MRAM 支持連續(xù)寫入沒有頁邊界限制你可以從任意地址開始連續(xù)寫任意長度。這跟 Flash 完全不同F(xiàn)lash 跨頁寫會回卷到頁首MRAM 不會。所以寫日志的時候我可以放心地一次寫幾十個字節(jié)不用管地址對齊。但連續(xù)寫入有個隱患如果寫入過程中電源掉了已經(jīng)寫入的部分是有效的沒寫完的部分是舊數(shù)據(jù)這會導致數(shù)據(jù)不一致。我的做法是對于關(guān)鍵數(shù)據(jù)采用“先寫數(shù)據(jù)區(qū)再寫標志位”的兩階段提交。比如寫一條日志先在日志區(qū)寫入數(shù)據(jù)然后在標志區(qū)寫入一個遞增的序號讀取時檢查序號連續(xù)性如果發(fā)現(xiàn)序號跳變就知道中間有掉電可以丟棄不完整的記錄。字節(jié)寫入函數(shù)void MRAM_WriteByte(unsigned long addr, unsigned char data) { CS 0; SPI_Exchange(0x06); // WREN CS 1; CS 0; SPI_Exchange(0x02); // WRITE SPI_Exchange((addr 16) 0xFF); SPI_Exchange((addr 8) 0xFF); SPI_Exchange(addr 0xFF); SPI_Exchange(data); CS 1; // 可選輪詢 WIP // while(MRAM_ReadStatus() 0x01); }連續(xù)寫入函數(shù)就是把 SPI_Exchange(data) 放進循環(huán)里地址自動遞增由 MRAM 內(nèi)部處理我只需要保證發(fā)送的字節(jié)數(shù)不超過剩余空間。3.3 讀取操作的實現(xiàn)與校驗讀取操作看似簡單但有個細節(jié)容易忽略MRAM 在 CS 拉高后SO 引腳會進入高阻態(tài)如果此時你的 SPI 總線上的其他從機也在驅(qū)動 SO就會沖突。所以 CS 拉高后最好把 PIC 的 SDI 引腳配置成輸入并確保沒有內(nèi)部上拉干擾。我一般會在 CS 拉高后加一個 NOP 延時等總線穩(wěn)定后再進行下一次操作。讀取函數(shù)unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS 0; SPI_Exchange(0x03); // READ SPI_Exchange((addr 16) 0xFF); SPI_Exchange((addr 8) 0xFF); SPI_Exchange(addr 0xFF); data SPI_Exchange(0x00); // 發(fā)送啞字節(jié)讀取數(shù)據(jù) CS 1; return data; }讀回來之后我習慣做一個簡單的校驗對于配置參數(shù)區(qū)我會在寫入時同時寫入原數(shù)據(jù)和反碼讀取時校驗兩者是否互補如果不互補就說明數(shù)據(jù)損壞自動加載默認參數(shù)。這個技巧在工業(yè)現(xiàn)場特別管用因為強電磁干擾偶爾會翻轉(zhuǎn)某一位反碼校驗能立刻發(fā)現(xiàn)。4. 工業(yè)場景下的可靠性設(shè)計與問題排查4.1 掉電保護與數(shù)據(jù)完整性策略工業(yè)現(xiàn)場掉電是常態(tài)不是異常。我的做法是在電源輸入端加一個電壓監(jiān)測芯片比如 MCP130當電壓低于 4.5V 時產(chǎn)生中斷PIC 在中斷里立刻把 RAM 中的關(guān)鍵數(shù)據(jù)寫入 MRAM 的掉電保護緩沖區(qū)。MRAM 的寫入速度極快一個字節(jié)的寫入加上 SPI 開銷大概 2 微秒寫 256 字節(jié)也就 0.5 毫秒而電源從 4.5V 掉到 3.0VPIC 最低工作電壓通常有幾十毫秒的余量時間完全夠。但這里有個坑電壓監(jiān)測中斷的響應時間。如果 PIC 正在執(zhí)行一個長指令或者中斷被關(guān)閉響應會延遲。我實測下來PIC18F4610 在 40MHz 下中斷響應最壞情況大概 10 個指令周期也就是 1 微秒左右可以接受。但如果你在中斷里還做了其他耗時操作就要重新評估了。另一個策略是“雙備份寫入”關(guān)鍵數(shù)據(jù)在 MRAM 里存兩份地址 A 和地址 B每次寫入時先寫 A 再寫 B讀取時如果 A 和 B 不一致以 B 為準因為 B 是后寫的。這個策略犧牲了一半空間但換來了極高的可靠性我在一個不能停機的化工監(jiān)測項目里用了這個方案運行三年沒有出現(xiàn)過數(shù)據(jù)丟失。4.2 常見問題速查與排查技巧下面這張表是我在實際項目中遇到過的典型問題以及對應的排查思路和解決方法現(xiàn)象可能原因排查方法解決方法讀出來全是 0xFFSI/SO 接反或 CS 未拉低用示波器看 CS 和 SCK 波形檢查接線確認 CS 在操作期間為低寫入后讀出來是舊數(shù)據(jù)WREN 后未拉高 CS用邏輯分析儀抓 SPI 時序確保 WREN 和 WRITE 之間 CS 拉高偶發(fā)數(shù)據(jù)位翻轉(zhuǎn)電源去耦不足或 SCK 過沖示波器看電源紋波和 SCK 波形加去耦電容SCK 串匹配電阻高速時鐘下讀寫錯誤SPI 時鐘超過 MRAM 時序余量降低時鐘頻率測試降到 10MHz 或更低或調(diào)整 CKP/CKE多從機時數(shù)據(jù)沖突其他從機 SO 未釋放逐個斷開從機測試確保同一時刻只有一個從機 CS 有效長時間運行后寫入失敗MRAM 進入未知狀態(tài)讀狀態(tài)寄存器發(fā)送 WRDI 后重新初始化提示邏輯分析儀是調(diào) SPI 的必備工具沒有之一。我用的是一款 8 通道 24MHz 的入門款抓 SPI 時序完全夠用。抓波形時重點看 CS 的邊沿、SCK 的極性和相位、數(shù)據(jù)在哪個邊沿變化這三個對上了通信基本就通了。4.3 實操心得與避坑經(jīng)驗第一個心得不要相信“理論上沒問題”。MRAM 的數(shù)據(jù)手冊說寫入不需要延時但我在一個項目里發(fā)現(xiàn)如果連續(xù)寫入之間不插入任何延時在 10MHz 時鐘下偶爾會出現(xiàn)寫入失敗。后來我在每次寫入后加了一個 1 微秒的短延時問題就消失了。這個延時不是 MRAM 要求的而是 PIC 的 SPI 模塊在連續(xù)操作之間需要一點恢復時間。數(shù)據(jù)手冊不會告訴你這個只有實際跑起來才知道。第二個心得地址邊界要自己管。MRAM 的地址是 24 位的但實際容量只有 512KB也就是 0x00000 到 0x7FFFF。如果你寫入的地址超過 0x7FFFFMRAM 會回卷到 0x00000不會報錯。我在寫循環(huán)日志時寫指針遞增到 0x80000 后自動回卷到 0這個邏輯要自己寫不能依賴硬件。第三個心得初始化時先讀一次設(shè)備 ID。MR25H40CDF 沒有標準的電子簽名命令但你可以通過讀狀態(tài)寄存器確認通信正常。我一般在上電初始化時先讀狀態(tài)寄存器如果讀回來是 0x00 或 0xFF說明通信有問題直接進入錯誤處理避免后續(xù)操作全部失敗。第四個心得寫保護要慎用。MRAM 的狀態(tài)寄存器里有塊保護位可以保護部分地址不被寫入。我一開始為了安全把配置參數(shù)區(qū)設(shè)了寫保護結(jié)果后來需要在線修改參數(shù)時忘了先解除保護調(diào)了半天才發(fā)現(xiàn)是保護位在作怪。后來我的做法是只在最終產(chǎn)品出廠時設(shè)置寫保護開發(fā)階段和需要在線修改的場合不設(shè)。5. 性能實測與場景擴展5.1 讀寫速度實測數(shù)據(jù)我在 10MHz SPI 時鐘下做了實測結(jié)果如下操作耗時說明單字節(jié)寫入約 2.5 微秒含 WREN 和 WRITE 兩次 CS 操作單字節(jié)讀取約 1.8 微秒含命令和地址發(fā)送256 字節(jié)連續(xù)寫入約 220 微秒平均每字節(jié) 0.86 微秒256 字節(jié)連續(xù)讀取約 210 微秒平均每字節(jié) 0.82 微秒狀態(tài)寄存器讀取約 1.2 微秒僅命令和讀數(shù)據(jù)這個速度對于工業(yè)數(shù)據(jù)記錄來說綽綽有余。按每 200 毫秒記錄一次、每次 64 字節(jié)計算寫入耗時約 55 微秒占空比不到 0.03%對主循環(huán)幾乎沒有影響。5.2 擴展應用場景這套方案不只適用于數(shù)據(jù)記錄。我在另一個項目里用它做參數(shù)存儲設(shè)備的校準系數(shù)、PID 參數(shù)、通信地址都存在 MRAM 里修改后立即生效掉電不丟。因為 MRAM 寫入不需要擦除修改一個參數(shù)只需要幾微秒比 EEPROM 快了幾個數(shù)量級用戶體驗完全不一樣。還有一個場景是高速緩存PIC 采集的傳感器數(shù)據(jù)先存在 MRAM 里等上位機來讀取時再批量上傳。MRAM 的寫入速度足夠跟上 1kHz 的采樣率而 EEPROM 根本做不到。甚至可以用 MRAM 做代碼補丁存儲PIC18F4610 的程序存儲器是 Flash擦寫次數(shù)有限如果需要在現(xiàn)場升級部分代碼可以把補丁存在 MRAM 里啟動時加載到 RAM 執(zhí)行。這個用法比較高級但對需要頻繁更新的場合很有價值。5.3 后續(xù)優(yōu)化方向如果你想把性能再壓榨一下有幾個方向可以試。一是提高 SPI 時鐘PIC18F4610 在 64MHz 晶振下需要外部時鐘輸入SPI 可以到 16MHzMRAM 支持 40MHz瓶頸在 PIC 這邊。二是用 DMA但 PIC18 沒有 DMA只能靠中斷優(yōu)化。三是減少 CS 操作次數(shù)比如連續(xù)寫入時不要每次都發(fā) WRENMRAM 的寫使能狀態(tài)在一次 CS 周期內(nèi)保持有效可以在一次 CS 拉低期間完成 WREN 和多個 WRITE 命令但這樣掉電風險會增加需要權(quán)衡。我個人在實際操作中的體會是工業(yè)場景下穩(wěn)定性永遠優(yōu)先于速度。10MHz 的 SPI 時鐘、2.5 微秒的單字節(jié)寫入、加上掉電保護和雙備份策略這套組合已經(jīng)能覆蓋絕大多數(shù)工業(yè)數(shù)據(jù)存儲需求。與其花時間把速度從 10MHz 提到 16MHz不如把精力放在異常處理和恢復邏輯上后者才是現(xiàn)場真正會出問題的地方。