數(shù)據(jù)記錄新方案:MRAM替換并行SRAM與PIC18F47Q10實戰(zhàn))
1. 為什么工業(yè)現(xiàn)場還在用并行SRAM而MRAM已經(jīng)悄悄替換了它做工業(yè)控制和嵌入式數(shù)據(jù)采集的人大概率都遇到過這個場景設(shè)備跑在現(xiàn)場每隔幾毫秒要記錄一次關(guān)鍵狀態(tài)比如電機轉(zhuǎn)速、閥門開度、累計運行時長。這些數(shù)據(jù)不能丟掉電之后必須還在。傳統(tǒng)方案無非幾種——EEPROM寫入太慢擦寫壽命也就百萬次級別高頻寫入很快就報廢SRAM速度快但掉電即失必須配電池或者超級電容電池又要考慮溫度范圍和更換周期NOR Flash寫入前得擦除整個扇區(qū)寫入延遲動輒幾毫秒到幾十毫秒而且擦寫次數(shù)只有十萬次量級。MRAM磁阻隨機存儲器的出現(xiàn)正好卡在這個縫隙里。它的寫入原理是靠磁隧道結(jié)的磁化方向變化來存儲數(shù)據(jù)不需要電荷保持所以掉電不丟寫入速度接近SRAM沒有擦除動作字節(jié)級隨機寫入擦寫壽命理論上無限次至少也是10的14次方以上。MR25H40CDF就是Everspin出的一顆4Mbit SPI接口MRAM工業(yè)級溫度范圍SOIC-8封裝引腳和常見的SPI Flash、EEPROM高度兼容。我這次選它搭配PIC18F47Q10做數(shù)據(jù)記錄模塊核心訴求就三條第一寫入頻率高每秒幾百次不能卡第二掉電不能丟數(shù)據(jù)不能靠電池第三接口要簡單SPI四根線搞定PCB不用大改。PIC18F47Q10是Microchip新出的8位機帶SPI外設(shè)Q10系列還集成了很多獨立于內(nèi)核的外設(shè)比如DMA、CLC、CIP用來做數(shù)據(jù)搬運和接口時序控制非常省心。這篇文章我會把整個鏈路拆開講MR25H40CDF的SPI時序到底有什么坑PIC18F47Q10的SPI模塊怎么配置才能跑出穩(wěn)定時序數(shù)據(jù)寫入和讀取的完整代碼邏輯以及我在實測中遇到的幾個典型問題。如果你手頭正好有類似需求或者單純想搞清楚MRAM和普通SPI Flash在操作上到底差在哪這篇應該能幫你省下不少調(diào)試時間。2. MR25H40CDF的SPI協(xié)議細節(jié)和普通Flash完全不是一回事2.1 命令集精簡到只有幾條但每條都有講究MR25H40CDF的SPI命令集比NOR Flash簡單得多。它沒有扇區(qū)擦除、塊擦除這些概念因為MRAM不需要擦除。核心命令就這幾條命令名稱命令碼作用注意事項WREN0x06寫使能每次寫操作前必須發(fā)WRDI0x04寫禁止寫完后自動復位一般不用手動發(fā)RDSR0x05讀狀態(tài)寄存器檢查WEL位和寫保護狀態(tài)WRSR0x01寫狀態(tài)寄存器配置塊保護READ0x03讀數(shù)據(jù)支持任意地址連續(xù)讀WRITE0x02寫數(shù)據(jù)支持任意地址連續(xù)寫無頁邊界限制這里最關(guān)鍵的一點MR25H40CDF的WRITE命令沒有頁邊界限制。普通SPI Flash比如W25Q64一次寫操作不能跨頁256字節(jié)頁邊界到了必須重新發(fā)命令。MRAM沒有這個限制你可以從地址0x0000一直寫到0x7FFFF中間不用停。這個特性在連續(xù)記錄場景下非常舒服不用在代碼里做頁對齊判斷。但注意WREN命令必須在每次WRITE之前單獨發(fā)送而且WREN之后如果SPI片選拉高寫使能狀態(tài)會保持但一旦執(zhí)行完WRITEWEL位自動清零。所以你不能發(fā)一次WREN然后連續(xù)寫多個不連續(xù)地址每次WRITE之前都要重新發(fā)WREN。2.2 SPI模式選擇Mode 0還是Mode 3MR25H40CDF支持SPI Mode 0CPOL0CPHA0和Mode 3CPOL1CPHA1。這兩個模式的區(qū)別在于時鐘空閑電平和數(shù)據(jù)采樣邊沿。Mode 0時鐘空閑低電平數(shù)據(jù)在上升沿采樣下降沿變化。 Mode 3時鐘空閑高電平數(shù)據(jù)在上升沿采樣下降沿變化。實際用下來Mode 0和Mode 3在功能上都能正常工作但如果你SPI總線上還掛了其他設(shè)備最好統(tǒng)一模式。我這次總線上只有MRAM一顆從設(shè)備選了Mode 0因為PIC18F47Q10的SPI默認配置就是Mode 0省得改。有個細節(jié)要注意MRAM的SPI時鐘最高可以跑到40MHz但PIC18F47Q10在64MHz系統(tǒng)時鐘下SPI分頻后最高也就16MHzFosc/4。這個速度對MRAM來說完全夠用16MHz下連續(xù)讀的吞吐率大概2MB/s寫的話因為要發(fā)WREN和等待實際有效寫入速率大概幾百KB/s對于數(shù)據(jù)記錄場景綽綽有余。2.3 狀態(tài)寄存器的WEL位和寫保護邏輯RDSR命令讀回來的狀態(tài)寄存器bit1是WELWrite Enable Latchbit0是WIPWrite In Progress。MRAM的寫入速度極快WIP位基本上發(fā)完WRITE命令之后幾個時鐘周期就清零了不像Flash要等幾毫秒。但WEL位的檢查不能省。我遇到過一種情況SPI總線受到干擾WREN命令的位被翻轉(zhuǎn)了結(jié)果WRITE命令發(fā)出去但數(shù)據(jù)沒寫進去。后來在每次WRITE之前加了一步RDSR檢查WEL位確認是1再發(fā)WRITE問題就沒再出現(xiàn)過。狀態(tài)寄存器還有BP0、BP1兩位做塊保護可以保護部分地址區(qū)域不被寫入。如果你的應用里有 bootloader 區(qū)域或者校準參數(shù)區(qū)域不希望被誤寫可以用WRSR命令把對應塊保護起來。但注意塊保護一旦設(shè)置要解除必須發(fā)WRSR而WRSR本身又需要WEL1所以解除保護的流程是WREN - WRSR寫0到BP位- 完成。3. PIC18F47Q10的SPI外設(shè)配置別被寄存器名字騙了3.1 SPI1模塊的時鐘源和分頻計算PIC18F47Q10有兩個SPI模塊SPI1和SPI2。SPI1掛在PPS外設(shè)引腳選擇上可以映射到任意數(shù)字引腳靈活性很高。我這次用SPI1映射到RC3SCK、RC4SDI、RC5SDO片選CS用RA5普通GPIO控制。SPI時鐘的計算公式Fspi Fosc / (4 * (SPI1CON1bits.CKP 1) * (2^SPI1CON1bits.SSP1ADD))實際配置時我系統(tǒng)時鐘跑64MHzSPI1CON1bits.SSP1ADD設(shè)為0CKP0Mode 0得到Fspi 64MHz / 4 16MHz。這里有個坑PIC18F47Q10的SPI模塊在高速下對引腳驅(qū)動能力有要求。我一開始用默認的引腳配置16MHz下波形上升沿有點塌讀回來的數(shù)據(jù)偶爾出錯。后來把SCK和SDO引腳的Slew Rate控制打開并且把驅(qū)動電流設(shè)到最大檔波形才干凈。3.2 PPS映射的步驟和容易漏掉的寄存器PPS映射是PIC18F47Q10比老PIC18F系列方便的地方但步驟不能少// 解鎖PPS PPSLOCK 0x55; PPSLOCK 0xAA; PPSLOCKbits.PPSLOCKED 0; // 映射SCK到RC3 RC3PPS 0x0F; // 0x0F是SPI1 SCK的輸出代碼 // 映射SDO到RC5 RC5PPS 0x10; // 0x10是SPI1 SDO的輸出代碼 // 映射SDI到RC4 SPI1SDIPPS 0x13; // 0x13是RC4的輸入代碼 // 鎖定PPS PPSLOCK 0x55; PPSLOCK 0xAA; PPSLOCKbits.PPSLOCKED 1;注意SPI1SDIPPS是輸入映射寫法和其他輸出映射不一樣它是把某個引腳映射到SPI1的SDI輸入。這個寄存器的名字容易搞混我一開始寫成RC4PPS結(jié)果SDI一直讀不到數(shù)據(jù)。3.3 片選信號的處理硬件片選還是軟件片選PIC18F47Q10的SPI模塊支持硬件片選SS1但我不建議用。原因有兩個第一硬件片選在SPI使能之后如果SS1引腳為高模塊會自動進入從模式容易誤觸發(fā)第二MRAM的WREN和WRITE之間需要保持片選低電平硬件片選的時序控制不如軟件靈活。我的做法是用RA5做普通GPIO手動控制#define MRAM_CS_LAT LATA5 #define MRAM_CS_TRIS TRISA5 // 初始化 MRAM_CS_TRIS 0; // 輸出 MRAM_CS_LAT 1; // 默認拉高 // 片選拉低 MRAM_CS_LAT 0; // ... SPI操作 ... // 片選拉高 MRAM_CS_LAT 1;片選拉低和第一個SCK邊沿之間要留至少20ns的建立時間。16MHz下每個時鐘周期62.5ns所以直接在片選拉低后插入一個NOP就夠了。但如果你SPI跑得更快比如32MHz就要考慮加一點延時或者用CLC模塊做硬件延時。4. 數(shù)據(jù)寫入與讀取的完整實現(xiàn)從單字節(jié)到批量記錄4.1 單字節(jié)寫入的完整時序?qū)懸粋€字節(jié)到MRAM的完整流程片選拉低發(fā)送WREN0x06片選拉高片選拉低發(fā)送WRITE0x02發(fā)送24位地址MR25H40CDF是4Mbit需要19位地址但按24位發(fā)送高5位忽略發(fā)送數(shù)據(jù)字節(jié)片選拉高注意第3步和第4步之間片選必須拉高這是WREN命令生效的條件。我試過WREN之后不拉高片選直接發(fā)WRITE數(shù)據(jù)寫不進去。代碼實現(xiàn)void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LAT 0; SPI1_ExchangeByte(0x06); // WREN MRAM_CS_LAT 1; MRAM_CS_LAT 0; SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); SPI1_ExchangeByte(data); MRAM_CS_LAT 1; }SPI1_ExchangeByte函數(shù)就是往SPI1BUF寫數(shù)據(jù)然后等SPI1IF標志置位讀SPI1BUF返回接收到的字節(jié)。4.2 批量寫入時如何避免頻繁WREN如果你要連續(xù)寫多個字節(jié)比如一次記錄16字節(jié)的數(shù)據(jù)包有兩種做法方案A每個字節(jié)都發(fā)WREN WRITE。這樣最保險但效率低每次寫都要額外的WREN開銷。方案B發(fā)一次WREN然后發(fā)WRITE命令連續(xù)發(fā)送多個數(shù)據(jù)字節(jié)最后拉高片選。MRAM支持在WRITE命令之后連續(xù)寫入地址自動遞增沒有頁邊界限制。我實測方案B完全可行而且效率高很多。寫16字節(jié)的數(shù)據(jù)包方案A需要16次WRENWRITE方案B只需要1次WREN1次WRITE16個數(shù)據(jù)字節(jié)。在16MHz SPI下方案A大概耗時200us方案B只要30us左右。但方案B有一個風險如果SPI傳輸過程中受到干擾片選意外拉高后面的數(shù)據(jù)就丟了。所以在高干擾環(huán)境下我建議還是用方案A或者至少加CRC校驗。4.3 讀取操作的地址自動遞增特性MRAM的READ命令0x03支持連續(xù)讀取地址自動遞增可以一直讀到0x7FFFF然后回繞到0x0000。這個特性用來做數(shù)據(jù)回放非常方便。void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LAT 0; SPI1_ExchangeByte(0x03); // READ SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI1_ExchangeByte(0x00); } MRAM_CS_LAT 1; }注意讀取的時候發(fā)送的是0x00因為SPI是全雙工你發(fā)什么字節(jié)出去不重要重要的是從設(shè)備返回的數(shù)據(jù)。4.4 數(shù)據(jù)記錄模塊的環(huán)形緩沖區(qū)設(shè)計我的應用場景是每秒記錄100次設(shè)備狀態(tài)每次16字節(jié)。MRAM總?cè)萘?12KB我劃了前256KB做環(huán)形緩沖區(qū)后256KB做參數(shù)存儲和日志。環(huán)形緩沖區(qū)的寫指針存在MRAM的固定地址每次寫入前先讀指針寫完數(shù)據(jù)后更新指針。這里有個關(guān)鍵點指針更新必須和數(shù)據(jù)寫入在同一個WRITE命令序列里完成否則掉電時可能出現(xiàn)數(shù)據(jù)寫了但指針沒更新或者指針更新了但數(shù)據(jù)沒寫。我的做法是把指針和數(shù)據(jù)打包成一個結(jié)構(gòu)體一次寫入typedef struct { uint32_t write_ptr; uint8_t data[16]; uint16_t crc; } Record_t;每次記錄時先讀回當前write_ptr構(gòu)造Record_t計算CRC然后從write_ptr對應的地址開始連續(xù)寫入sizeof(Record_t)個字節(jié)。因為MRAM沒有頁邊界限制這一整塊可以一次WRITE命令寫完保證原子性。5. 實測中遇到的三個典型問題與排查過程5.1 問題一連續(xù)寫入時偶發(fā)數(shù)據(jù)錯位現(xiàn)象批量寫入1000次偶爾有幾次讀回來的數(shù)據(jù)整體偏移一個字節(jié)。排查過程先用邏輯分析儀抓SPI波形發(fā)現(xiàn)錯位的那幾次WRITE命令之后的第一個數(shù)據(jù)字節(jié)和地址的最后一個字節(jié)之間多了一個時鐘周期。仔細看波形是片選拉低之后SCK的第一個邊沿來得太早MRAM還沒準備好。根因PIC18F47Q10的SPI模塊在片選拉低后如果SPI1BUF里還有之前的數(shù)據(jù)第一個SCK邊沿可能會提前。解決辦法是在片選拉低之后、寫SPI1BUF之前先清空SPI1BUF或者插入一個短延時。我最后的做法是在MRAM_CS_LAT 0之后加了一個__delay_us(1)問題消失。1us在16MHz下是16個時鐘周期對整體性能影響可以忽略。5.2 問題二掉電測試時最后幾條記錄丟失現(xiàn)象正常運行時數(shù)據(jù)都對但突然斷電再上電最后幾條記錄讀出來是0xFF。根因分析MRAM本身寫入是非易失的寫入完成就保存了。問題出在寫指針的更新上。我原來的代碼是先寫數(shù)據(jù)再更新指針兩個操作分開。掉電發(fā)生在寫數(shù)據(jù)和更新指針之間時數(shù)據(jù)寫進去了但指針沒更新下次上電后新數(shù)據(jù)會覆蓋掉剛寫的那條。解決辦法把數(shù)據(jù)和指針打包成一個結(jié)構(gòu)體一次WRITE寫完。這樣要么全寫進去要么全沒寫不會出現(xiàn)中間狀態(tài)。改完之后掉電測試跑了200次沒有再丟數(shù)據(jù)。5.3 問題三SPI時鐘跑高之后讀數(shù)據(jù)出錯現(xiàn)象SPI時鐘從8MHz提到16MHz后讀回來的數(shù)據(jù)偶爾有bit翻轉(zhuǎn)。排查用示波器看SCK和SDI波形發(fā)現(xiàn)16MHz下SDI的上升沿有點慢在SCK采樣邊沿處還沒達到VIH。根因PIC18F47Q10的SDI引腳輸入沒有施密特觸發(fā)器而且默認驅(qū)動能力較弱。MRAM的SDO輸出驅(qū)動能力也有限兩者之間的PCB走線如果比較長分布電容會導致邊沿變緩。解決辦法第一縮短SPI走線MRAM盡量靠近MCU第二在SDI線上加一個4.7kΩ上拉電阻到3.3V加速上升沿第三把SPI時鐘降到12MHz留出余量。三招一起上問題徹底解決。注意SPI時鐘不是越高越好尤其是工業(yè)現(xiàn)場有干擾的情況下留20%的余量比跑滿速更可靠。6. 工業(yè)場景下的可靠性加固與經(jīng)驗總結(jié)6.1 電源監(jiān)控與掉電保護MRAM雖然寫入快但也不是瞬間完成。16MHz SPI下寫一個32字節(jié)的結(jié)構(gòu)體大概需要20us。如果電源掉電時間小于20us寫入可能不完整。我的做法是用PIC18F47Q10的HLVD高/低電壓檢測模塊設(shè)置檢測閾值為3.0V。當電源電壓降到3.0V時HLVD觸發(fā)中斷在中斷里立即停止所有寫入操作把關(guān)鍵狀態(tài)保存到MRAM的保留區(qū)域。從3.0V降到MRAM最低工作電壓2.7V大概有幾百微秒的窗口足夠完成一次緊急寫入。6.2 CRC校驗的選型和計算開銷數(shù)據(jù)記錄場景下CRC是必須的。我選的是CRC-16/CCITT多項式0x1021初始值0xFFFF。PIC18F47Q10沒有硬件CRC模塊用軟件查表法計算16字節(jié)數(shù)據(jù)大概耗時8us可以接受。如果你對速度要求更高可以用PIC18F47Q10的CLC模塊配合DMA做硬件CRC但配置比較復雜我這次沒上。6.3 MRAM的壽命和磨損均衡MRAM的擦寫壽命是10的14次方以上按每秒寫100次算一年也就3.15×10的9次方次遠遠沒到壽命上限。所以MRAM不需要磨損均衡環(huán)形緩沖區(qū)直接循環(huán)覆蓋就行。但有一點要注意MRAM的寫入次數(shù)雖然無限但每次寫入的功耗比讀取高。如果設(shè)備是電池供電要控制寫入頻率或者用批量寫入減少WREN命令的次數(shù)。6.4 和EEPROM、FRAM的對比選型建議特性MR25H40CDF (MRAM)典型EEPROM典型FRAM接口SPII2C/SPII2C/SPI寫入速度極快無擦除慢5ms/字節(jié)快無擦除擦寫壽命10^1410^610^12掉電保持是是是容量4Mbit通?!?Mbit通?!?Mbit成本中高低中工業(yè)溫度-40~85°C-40~85°C-40~85°C選型邏輯很簡單寫入頻率高、壽命要求高、容量需求大選MRAM成本敏感、寫入頻率低選EEPROM介于兩者之間FRAM是折中方案。6.5 我在這個項目里踩過的幾個小坑第一個坑MRAM的SOIC-8封裝和W25Q64的SOIC-8封裝引腳定義不一樣。W25Q64的pin1是CSpin2是DOpin3是WPpin4是GNDMR25H40CDF的pin1是CSpin2是SOpin3是WPpin4是GND??雌饋硪粯拥玴in5、pin6、pin7、pin8的定義有差異。我一開始直接拿W25Q64的PCB改結(jié)果pin7和pin8接反了MRAM一直不工作。后來查數(shù)據(jù)手冊才發(fā)現(xiàn)MR25H40CDF的pin7是NCpin8是VCC而W25Q64的pin7是HOLDpin8是VCC。雖然pin8都是VCC但pin7的處理不一樣MRAM的pin7必須懸空不能接地。第二個坑PIC18F47Q10的SPI1模塊在配置為Master模式時如果SS1引腳被配置為輸入且為低電平模塊會自動切換到Slave模式。我一開始把SS1引腳也映射到了RA5結(jié)果片選拉低的時候SPI模塊直接罷工。后來把SS1功能關(guān)掉RA5純做GPIO問題解決。第三個坑MRAM的WREN命令之后如果片選拉高再拉低WEL位會保持。但如果中間插入了其他SPI設(shè)備的操作WEL位可能會被意外清除。我的SPI總線上只有MRAM所以沒這個問題但如果你的總線上有多個從設(shè)備每次WRITE之前都要重新發(fā)WREN不能假設(shè)WEL位還在。6.6 給后續(xù)開發(fā)者的幾條實用建議如果你準備用MR25H40CDF做數(shù)據(jù)記錄我的建議是第一SPI時鐘先跑8MHz調(diào)通之后再往上提不要一上來就16MHz第二片選控制用GPIO不要用硬件SS第三數(shù)據(jù)和指針打包寫入保證原子性第四加CRC校驗工業(yè)現(xiàn)場沒有CRC的數(shù)據(jù)不可信第五電源監(jiān)控一定要做HLVD模塊用起來成本幾乎為零但能救命。這個方案我已經(jīng)在三個現(xiàn)場設(shè)備上跑了半年多每天寫入量大概800萬次目前沒有出現(xiàn)過數(shù)據(jù)丟失或MRAM失效的情況。MRAM的價格雖然比EEPROM貴但省掉了電池、超級電容和復雜的磨損均衡算法整體BOM成本和維護成本反而更低。如果你也在糾結(jié)數(shù)據(jù)存儲方案不妨試試MRAM這條路。