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