數(shù)據(jù)存儲(chǔ)方案實(shí)踐)
做工業(yè)嵌入式設(shè)備的這幾年一提到“存儲(chǔ)和讀取數(shù)據(jù)”很多人第一反應(yīng)就是 SPI Flash 或者 I2C EEPROM??梢坏﹫?chǎng)景變成高頻寫日志、斷電要保數(shù)據(jù)、產(chǎn)品生命周期內(nèi)要反復(fù)擦寫這兩類器件很容易成為整個(gè)系統(tǒng)里最先掉鏈子的部分。我最近在一個(gè)工業(yè)數(shù)據(jù)采集器項(xiàng)目里把這些底層存儲(chǔ)整個(gè)換成了 Everspin 的 MR25H40CDF 磁阻存儲(chǔ)器MRAM主控用的是瑞薩 R7FA2L1AB2DFP搭建了一套適合工業(yè)環(huán)境的數(shù)據(jù)讀寫方案。這篇就完整記錄這顆 MRAM 和這顆 MCU 的配合過(guò)程為什么選了 MRAM、硬件怎么接、FSP 里怎么配置 RSPI、驅(qū)動(dòng)代碼如何組織、以及實(shí)測(cè)中遇到的那些典型問(wèn)題和排查方法。如果你也在做嵌入式數(shù)據(jù)記錄、參數(shù)存儲(chǔ)或者需要頻繁寫入的緩沖類應(yīng)用希望這些內(nèi)容能幫你少走幾步彎路。1. 為什么工業(yè)數(shù)據(jù)存儲(chǔ)偏偏選了 MRAM1.1 一個(gè)簡(jiǎn)單算賬把 EEPROM 的壽命劣勢(shì)攤開以前很多工業(yè)采集板都用串行 EEPROM比如 25LC256、AT24CXXX 之類。EEPROM 不是不能用而是它的擦寫次數(shù)天花板太低低到什么程度呢常規(guī) SPI EEPROM 的擦寫壽命一般是 10 萬(wàn)到 100 萬(wàn)次寫入一個(gè)字節(jié)還要先擦后寫耗時(shí)大概 3 到 5 毫秒。我當(dāng)時(shí)那個(gè)項(xiàng)目現(xiàn)場(chǎng)設(shè)備每 2 秒要記錄一條 16 字節(jié)的工況數(shù)據(jù)。一天下來(lái)就是 43200 次寫入一年就是 1576 萬(wàn)次左右。拿 100 萬(wàn)次壽命的 EEPROM 來(lái)算兩個(gè)月多一點(diǎn)就報(bào)廢了。哪怕把壽命說(shuō)到 1000 萬(wàn)次一年左右也要提心吊膽?,F(xiàn)場(chǎng)反饋回來(lái)的故障也確實(shí)吻合寫失敗、數(shù)據(jù)偶爾丟失、設(shè)備重啟后日志斷檔。這還沒算 EEPROM 的另一個(gè)問(wèn)題寫一頁(yè)和寫一個(gè)字節(jié)的時(shí)間幾乎一樣但頁(yè)編程前必須把整個(gè)頁(yè)擦掉。日志類應(yīng)用往往是隨機(jī)追加、字節(jié)級(jí)修改成本非常高。所以那版設(shè)計(jì)我基本沒猶豫直接上了 MRAM。MR25H40CDF 的寫耐久性典型值是 10 的 14 次方次換算成剛才那個(gè)寫入頻率設(shè)備要跑幾十萬(wàn)年才可能摸到壽命邊界。對(duì)工業(yè)設(shè)備來(lái)說(shuō)基本等于永久。而且它寫數(shù)據(jù)不需要擦除任意字節(jié)直接覆蓋這就把“先擦后寫”的流程徹底省掉了。1.2 MRAM 的技術(shù)底子與 Flash/EEPROM 的差異很多人聽到“非易失性存儲(chǔ)器”腦子里只有 Flash 和 EEPROM。MRAM 的原理和它們完全不同。EEPROM 和 Flash 靠的是浮柵里的電荷電荷會(huì)被慢慢泄漏編程時(shí)還要用電荷泵產(chǎn)生高壓把電子注入進(jìn)去擦除時(shí)再靠隧穿效應(yīng)把電子拉出來(lái)。這個(gè)過(guò)程天生慢而且每一次擦寫都在氧化層上留下一點(diǎn)損傷所以壽命有限。MRAM 用的是磁隧道結(jié)MTJ。存儲(chǔ)單元里有兩層磁性材料中間夾著一層極薄的絕緣層自由層的磁化方向和固定層的磁化方向平行或反平行時(shí)隧道電阻不同對(duì)應(yīng) 0 和 1。寫入的時(shí)候施加電流產(chǎn)生的磁場(chǎng)或自旋極化電流直接把自由層的磁化方向翻轉(zhuǎn)。沒有電荷注入沒有擦除操作翻轉(zhuǎn)完成數(shù)據(jù)就穩(wěn)定了。理解了這個(gè)原理你就能明白為什么 MRAM 在工業(yè)場(chǎng)景里這么香寫入前不需要擦除所以隨機(jī)寫、字節(jié)寫都很快不挑地址。寫壽命極高不用做磨損均衡。非易失性斷電后數(shù)據(jù)不丟而且不存在電荷泄漏的長(zhǎng)期可靠性隱患。讀速度接近 SRAMSPI 接口跑到 40MHz 沒什么壓力。數(shù)據(jù)保持能力通常在 20 年以上。當(dāng)然MRAM 也不是沒有缺點(diǎn)最直接的就是單位容量?jī)r(jià)格比 Flash 高而且大容量型號(hào)選擇少。但在工業(yè)數(shù)據(jù)記錄這個(gè)場(chǎng)景里容量需求往往只有幾十到幾百 KBMRAM 的價(jià)格劣勢(shì)并不明顯可靠性優(yōu)勢(shì)卻是實(shí)打?qū)嵉摹?.3 MR25H40CDF 規(guī)格里最該關(guān)心的幾個(gè)數(shù)字MR25H40CDF 是 Everspin MR25H40 系列里的 SPI 接口 MRAM。我選它最看重的幾個(gè)數(shù)字列一下參數(shù)數(shù)值容量4 Mbit也就是 512 KB組織結(jié)構(gòu)512 K × 8 bit接口SPI支持模式 0 / 模式 3最高時(shí)鐘40 MHz供電電壓3.0 ~ 3.6 V典型 3.3 V寫耐久性典型 10 的 14 次方次寫周期數(shù)據(jù)保持 20 年地址字段3 字節(jié)有效地址 0x00000 ~ 0x7FFFF這里有一個(gè)容易忽略的地方4 Mbit 看起來(lái)不大但 512 KB 劃分成日志區(qū)、參數(shù)區(qū)、緩沖區(qū)已經(jīng)非常夠用。拿每條記錄 64 字節(jié)來(lái)算512 KB 能存 8192 條配合環(huán)形覆蓋設(shè)計(jì)等于一個(gè)掉電不丟失的小型磁盤。命令集也比較簡(jiǎn)單核心就 6 條WREN0x06、WRDI0x04、RDSR0x05、WRSR0x01、WRITE0x02、READ0x03。后面驅(qū)動(dòng)代碼主要就是圍繞這幾條命令展開。2. 硬件接線與 SPI 總線搭建把兩顆芯片接起來(lái)并不復(fù)雜2.1 R7FA2L1AB2DFP 的 RSPI 外設(shè)與引腳分配R7FA2L1AB2DFP 屬于瑞薩 RA2L1 系列Arm Cortex-M23 內(nèi)核主頻 48 MHz有 256 KB Flash 和 32 KB RAM片上帶了 RSPI 串行外設(shè)接口。對(duì) MRAM 這種標(biāo)準(zhǔn) SPI 器件來(lái)說(shuō)不需要什么特殊外設(shè)RSPI 即可。在 FSP 里RSPI 可以配置成主模式引腳一般使用 SCLK、MOSI、MISO 三根線。片選 CS 我建議不要用 RSPI 的硬件 SS而是單獨(dú)拿一個(gè)普通 GPIO 來(lái)控制。原因后面寫驅(qū)動(dòng)時(shí)會(huì)詳細(xì)說(shuō)核心點(diǎn)是 MRAM 的寫使能和寫執(zhí)行需要精確控制 CS 的下降沿和上升沿用 GPIO 軟控時(shí)序最直接可讀性也最好。我的接線示意如下MR25H40CDF 引腳功能接 R7FA2L1AB2DFPCS片選P301GPIO 輸出默認(rèn)拉高SCLKSPI 時(shí)鐘RSPI0 SCLKSI數(shù)據(jù)輸入RSPI0 MOSISO數(shù)據(jù)輸出RSPI0 MISOWP寫保護(hù)接 3.3 V禁用寫保護(hù)HOLD傳輸暫停接 3.3 V禁用暫停功能VCC電源3.3 V并聯(lián) 100 nF 10 uFVSS地GND這里要特別提醒一句接線時(shí)先分清 SO 和 SI別接反了。MRAM 的 SO 是輸出接 MCU 的 MISOSI 是輸入接 MCU 的 MOSI。有些開發(fā)板絲印容易讓人看反我第一版調(diào)試就吃過(guò)這個(gè)虧全部數(shù)據(jù)讀回都是 0xFF排查半天才發(fā)現(xiàn)是 MOSI 和 MISO 交叉了。2.2 CS、WP、HOLD 這些控制腳的接法CS 腳作為片選每次讀寫事務(wù)都要拉低事務(wù)結(jié)束后拉高。這個(gè)腳不能懸空如果 MCU 復(fù)位期間 GPIO 是高阻狀態(tài)外部最好加一個(gè) 10 kΩ 上拉電阻到 VCC保證 MRAM 在系統(tǒng)上電瞬間不被誤觸發(fā)。WP 腳叫寫保護(hù)低電平有效。接低電平會(huì)禁止寫狀態(tài)寄存器和數(shù)據(jù)區(qū)具體行為取決于器件配置但最穩(wěn)妥的做法就是直接接高電平徹底關(guān)掉寫保護(hù)功能。不要讓它懸空工業(yè)現(xiàn)場(chǎng)電磁干擾多懸空引腳很容易被耦合出錯(cuò)誤電平導(dǎo)致寫操作莫名失敗。HOLD 腳是傳輸暫停低電平有效。HOLD 拉低時(shí)SPI 時(shí)鐘和 CS 狀態(tài)會(huì)被器件忽略總線上的數(shù)據(jù)保持外部主機(jī)暫時(shí)沒辦法繼續(xù)通信。這個(gè)腳同樣不能懸空接高電平即可。VCC 旁邊要放 100 nF 的陶瓷電容盡量貼近引腳再加一顆 10 uF 鉭電容做低頻去耦。工業(yè)現(xiàn)場(chǎng)如果電源本身波動(dòng)大建議在電源入口加 TVS 管和磁珠防止繼電器或其他大功率設(shè)備啟停時(shí)把尖峰灌進(jìn)來(lái)。2.3 電路上常見的三個(gè)坑第一個(gè)坑是電平匹配。RA2L1 的 GPIO 和 MRAM 都是 3.3 V 系統(tǒng)直接連接沒有問(wèn)題。但如果你之前用慣了 5 V 的 AVR 或老式 51千萬(wàn)別順手把 MRAM 接到 5 V 上MR25H40CDF 的絕對(duì)最大額定值不允許。5 V 系統(tǒng)必須加電平轉(zhuǎn)換。第二個(gè)坑是 SPI 信號(hào)走線太長(zhǎng)。工業(yè)電路板上如果有電機(jī)驅(qū)動(dòng)、繼電器、開關(guān)電源這些干擾源會(huì)通過(guò)長(zhǎng)走線耦合進(jìn) SPI 總線。調(diào)通之后可以把時(shí)鐘跑到 40 MHz但前期調(diào)試建議降到 1 MHz 到 10 MHz 之間先把功能跑對(duì)再慢慢提速度。40 MHz 下串 33 Ω 電阻可以抑制振鈴具體阻值示波器看波形再定。第三個(gè)坑是 MISO 引腳誤開了內(nèi)部上拉。RA2L1 的 GPIO 可以配置內(nèi)部上下拉如果在 FSP 里把 MISO 配成帶上拉的輸入雖然讀取邏輯上問(wèn)題不大但上拉會(huì)抬高信號(hào)的低電平上升沿高速 SPI 下容易造成數(shù)據(jù)采樣錯(cuò)誤。MISO 建議關(guān)閉上下拉。3. FSP 里配完 RSPI剩下就是命令序列和驅(qū)動(dòng)封裝3.1 用 e2 studio FSP 生成 RSPI 工程瑞薩 RA 系列用 e2 studio 開發(fā)FSP 是它的軟件包配置框架。新建工程時(shí)選擇 R7FA2L1AB2DFP 這個(gè)具體型號(hào)然后在 Stacks 頁(yè)面添加 SPI 驅(qū)動(dòng)也就是 r_spi 模塊。配置項(xiàng)里幾個(gè)關(guān)鍵參數(shù)Operation ModeMaster主機(jī)模式。Frame FormatSPI。Data Bit Width8 bit。SPI Mode模式 0 或者模式 3MRAM 兩種都支持。Bitrate先填 1 MHz跑通后調(diào)到 10 MHz 或 20 MHz 都可以。Bit OrderMSB First。片選不用 RSPI 的硬件 SS用 GPIO 自己控制。生成代碼后工程里會(huì)出現(xiàn) g_spi0_ctrl 和 g_spi0_cfg。主函數(shù)里調(diào)用 R_SPI_Open 初始化一次即可。有一點(diǎn)必須強(qiáng)調(diào)FSP 的 R_SPI_WriteRead 默認(rèn)是異步行為代碼不能裸調(diào)完就立刻拉 CS否則數(shù)據(jù)可能還沒完全從移位寄存器里移出去CS 就拉高了整個(gè)命令會(huì)丟失。我習(xí)慣在配置里開傳輸完成中斷注冊(cè)回調(diào)函數(shù)在回調(diào)里置一個(gè)事件標(biāo)志主流程等待這個(gè)標(biāo)志后再拉高 CS。如果你喜歡輪詢也可以用 R_SPI_EventGet 不斷查詢事件狀態(tài)但回調(diào)加標(biāo)志位的方式更清晰多任務(wù)下也更容易擴(kuò)展。3.2 命令序列從 WREN 到最后的 CS 上升沿MR25H40CDF 的讀寫雖然簡(jiǎn)單但命令順序很講究尤其 CS 邊沿的時(shí)序。先看寫使能 WREN這是所有寫操作的前提CS L 發(fā)送 0x06WREN CS H上升沿寫使能鎖存到狀態(tài)寄存器這一步如果 CS 拉高前數(shù)據(jù)還沒發(fā)完寫使能就不會(huì)生效。所以代碼里發(fā)送完 0x06 之后必須等 SPI 傳輸完成事件再拉高 CS。然后是寫數(shù)據(jù) WRITECS L 發(fā)送 0x02WRITE 發(fā)送 3 字節(jié)地址高位在前 發(fā)送數(shù)據(jù)字節(jié) CS H上升沿執(zhí)行寫入MRAM 的好處是 CS 拉高后寫入立即完成不需要像 EEPROM 那樣等待內(nèi)部編程時(shí)間。寫完可以馬上發(fā)下一條命令也可以馬上讀回驗(yàn)證。但注意每次寫命令之前都必須重新做一次 WREN因?yàn)?WEL 位在一次寫操作完成后會(huì)自動(dòng)清掉。很多第一次用 MRAM 的工程師都會(huì)在這里翻車寫使能只做了一次然后連續(xù)寫多個(gè)地址第二個(gè)地址開始全部寫不進(jìn)去。讀數(shù)據(jù) READ 更簡(jiǎn)單CS L 發(fā)送 0x03READ 發(fā)送 3 字節(jié)地址 持續(xù)發(fā)送時(shí)鐘MISO 上讀出數(shù)據(jù) CS HREAD 命令下地址可以連續(xù)遞增保持 CS 為低就可以一直讀下去直到地址越過(guò) 0x7FFFF 后自動(dòng)回繞到 0x00000。寫數(shù)據(jù)也一樣地址會(huì)自動(dòng)遞增所以批量寫入時(shí)不需要每字節(jié)都帶地址。3.3 驅(qū)動(dòng)模塊設(shè)計(jì)與完整讀寫代碼驅(qū)動(dòng)邏輯我拆成三個(gè)層次底層 SPI 收發(fā)、MRAM 命令封裝、上層讀寫接口。底層只關(guān)心把數(shù)據(jù)發(fā)出去或者收發(fā)同時(shí)進(jìn)行上層接口負(fù)責(zé)地址合法性判斷和分包。先給一個(gè)頭文件定義命令字和地址范圍#ifndef MRAM_H_ #define MRAM_H_ #include stdint.h #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_MAX_ADDR 0x7FFFFu int mram_init(void); int mram_write_buf(uint32_t addr, const uint8_t *data, uint32_t len); int mram_read_buf(uint32_t addr, uint8_t *data, uint32_t len); #endif再看底層實(shí)現(xiàn)。以 FSP 生成的 g_spi0_ctrl 為例我用回調(diào)方式等待傳輸完成#include hal_data.h #include mram.h #include string.h static volatile bool g_mram_spi_done false; void mram_spi_callback(spi_callback_args_t * p_args) { if (SPI_EVENT_TRANSFER_COMPLETE p_args-event) { g_mram_spi_done true; } } static void mram_cs_low(void) { R_IOPORT_PinWrite(g_ioport_ctrl, PIN_MRAM_CS, BSP_IO_LEVEL_LOW); } static void mram_cs_high(void) { R_IOPORT_PinWrite(g_ioport_ctrl, PIN_MRAM_CS, BSP_IO_LEVEL_HIGH); } static void mram_wait_tx_done(void) { while (!g_mram_spi_done) { } g_mram_spi_done false; } static int mram_tx_bytes(const uint8_t *tx, uint16_t len) { fsp_err_t err R_SPI_Write(g_spi0_ctrl, (uint8_t *)tx, len, false); if (FSP_SUCCESS ! err) { return -1; } mram_wait_tx_done(); return 0; } static int mram_txrx_bytes(const uint8_t *tx, uint8_t *rx, uint16_t len) { fsp_err_t err R_SPI_WriteRead(g_spi0_ctrl, (uint8_t *)tx, rx, len, false); if (FSP_SUCCESS ! err) { return -1; } mram_wait_tx_done(); return 0; }這里提醒一下回調(diào)事件具體名稱取決于 FSP 版本有些版本是 SPI_EVENT_OPERATION_COMPLETE我示例里用的是 SPI_EVENT_TRANSFER_COMPLETE編譯報(bào)錯(cuò)的話改一下宏名就行。如果工程里開了 DTC傳輸完成后的處理邏輯會(huì)略有不同我建議這個(gè)模塊保持最簡(jiǎn)單的中斷模式不開 DTC方便排查。寫使能和讀狀態(tài)static int mram_write_enable(void) { uint8_t cmd MRAM_CMD_WREN; mram_cs_low(); if (mram_tx_bytes(cmd, 1) ! 0) { mram_cs_high(); return -1; } mram_cs_high(); return 0; } static uint8_t mram_read_status(void) { uint8_t tx[2] { MRAM_CMD_RDSR, 0x00 }; uint8_t rx[2] { 0x00, 0x00 }; mram_cs_low(); if (mram_txrx_bytes(tx, rx, 2) ! 0) { mram_cs_high(); return 0xFF; } mram_cs_high(); return rx[1]; }狀態(tài)寄存器最低位就是 WEL讀取后判斷一下能幫我們快速定位“寫不進(jìn)去”的問(wèn)題。寫數(shù)據(jù)接口。我每次最多寫 256 字節(jié)超過(guò)就分包處理。原因有兩個(gè)一是避免在棧上開太大的數(shù)組二是每次寫事務(wù)的時(shí)間可控不會(huì)長(zhǎng)時(shí)間占用 SPI 總線int mram_write_buf(uint32_t addr, const uint8_t *data, uint32_t len) { static uint8_t tx[256 4]; if (addr MRAM_MAX_ADDR || (addr len - 1) MRAM_MAX_ADDR) { return -1; } uint32_t pos 0; while (pos len) { uint16_t chunk (len - pos 256) ? 256 : (uint16_t)(len - pos); uint32_t cur addr pos; tx[0] MRAM_CMD_WRITE; tx[1] (cur 16) 0xFF; tx[2] (cur 8) 0xFF; tx[3] cur 0xFF; memcpy(tx[4], data[pos], chunk); if (mram_write_enable() ! 0) { return -1; } mram_cs_low(); if (mram_tx_bytes(tx, 4 (uint16_t)chunk) ! 0) { mram_cs_high(); return -1; } mram_cs_high(); pos chunk; } return 0; }讀數(shù)據(jù)接口用全雙工 R_SPI_WriteRead 實(shí)現(xiàn)。發(fā)送命令和地址后后續(xù)時(shí)鐘由 MCU 生成同時(shí) MISO 逐位移出數(shù)據(jù)收到的前 4 個(gè)字節(jié)是命令和地址階段的無(wú)效數(shù)據(jù)從 rx[4] 開始才是真正的數(shù)據(jù)int mram_read_buf(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t tx[256 4] { 0 }; uint8_t rx[256 4] { 0 }; uint8_t hdr[4]; if (addr MRAM_MAX_ADDR || (addr len - 1) MRAM_MAX_ADDR) { return -1; } hdr[0] MRAM_CMD_READ; uint32_t pos 0; while (pos len) { uint16_t chunk (len - pos 256) ? 256 : (uint16_t)(len - pos); uint32_t cur addr pos; hdr[1] (cur 16) 0xFF; hdr[2] (cur 8) 0xFF; hdr[3] cur 0xFF; memset(tx, 0x00, 4 chunk); memcpy(tx, hdr, 4); mram_cs_low(); if (mram_txrx_bytes(tx, rx, (uint16_t)(4 chunk)) ! 0) { mram_cs_high(); return -1; } mram_cs_high(); memcpy(data[pos], rx[4], chunk); pos chunk; } return 0; }這個(gè)驅(qū)動(dòng)讀寫?yīng)毩⒎职看?CS 重新建立事務(wù)所以跨 0x7FFFF 邊界的問(wèn)題也分塊堵住了上層不用太擔(dān)心地址回繞。唯一要注意的是靜態(tài)局部變量 tx 讓驅(qū)動(dòng)不可重入如果項(xiàng)目里有多個(gè)任務(wù)同時(shí)訪問(wèn) MRAM記得在外面加互斥保護(hù)。4. 實(shí)測(cè)與排錯(cuò)全 0xFF、寫不進(jìn)去、以及滾寫可靠性4.1 讀回全 0xFFSPI 模式會(huì)和信號(hào)完整性背鍋第一次上板我調(diào)用 mram_read_buf 讀 MRAM 廠商出廠數(shù)據(jù)區(qū)返回的全是 0xFF。這個(gè)現(xiàn)象在 SPI 存儲(chǔ)器件調(diào)試?yán)锾?jīng)典了基本可以按下面的順序排查。先測(cè)電源和地。MRAM 供電要是 3.3 VVCC 和 VSS 之間不能有虛焊。然后用示波器看 CS 拉低后SCLK 上是否有時(shí)鐘輸出。如果 CS 一直高M(jìn)RAM 根本沒被選中數(shù)據(jù)線自然一直高電平讀回來(lái)全是 0xFF。第二步查 SPI 模式。MR25H40CDF 只支持模式 0 和模式 3如果 FSP 里配成了模式 1 或模式 2器件永遠(yuǎn)不會(huì)給出正確數(shù)據(jù)。模式 0 和模式 3 的區(qū)別在于空閑時(shí)鐘電平和采樣沿示波器上一抓就能看出來(lái)。我當(dāng)時(shí)把 RSPI 從模式 0 換成模式 3數(shù)據(jù)立刻正常了。第三步查 MISO。用示波器看 MISO 引腳在 CS 拉低、發(fā)送 READ 命令后MISO 上應(yīng)該有脈沖信號(hào)。如果始終是一條高電平要么芯片虛焊要么 SO 引腳沒接到 MCU 的 MISO。再不行就把 SPI 速度降到 1 MHz排除信號(hào)振鈴。還有一個(gè)容易被忽略的點(diǎn)就是讀狀態(tài)寄存器。上電后正常 RDSR 應(yīng)該返回 0x00。如果 RDSR 返回 0xFF幾乎可以斷定命令根本沒發(fā)進(jìn)去問(wèn)題出在 CS 時(shí)序或 SPI 配置如果 RDSR 返回 0x02 之類說(shuō)明 WEL 處于置位狀態(tài)可能是上次寫操作后沒清這本身不算故障。4.2 寫不進(jìn)去先查 WEL 位再查 CS 邊沿全 0xFF 的問(wèn)題解決后緊接著遇到寫不進(jìn)去調(diào)用 write_buf 再讀回來(lái)數(shù)據(jù)跟寫之前完全一樣。這個(gè)時(shí)候第一件事就是查 WEL。我在 mram_write_enable 后加了一個(gè)斷言函數(shù)讀 RDSR判斷 bit0 是不是 1。如果置位失敗說(shuō)明 WREN 命令沒被正確執(zhí)行。常見原因有兩個(gè)第一個(gè)是傳輸沒完成就拉高了 CS。代碼里用 R_SPI_Write 發(fā)送 0x06 后如果沒有等待事件標(biāo)志而立刻 cs_high在 SPII 高速率下0x06 可能只說(shuō)了一半CS 就上去了。解決方式就是前面驅(qū)動(dòng)里寫的發(fā)送后必須等 g_mram_spi_done 標(biāo)志置位再拉高 CS。第二個(gè)是 WP 腳被拉低了。WP 拉低會(huì)讓器件進(jìn)入寫保護(hù)狀態(tài)WREN 命令本身雖然能發(fā)但 WEL 可能不會(huì)被正確置位或者即使置位了后續(xù) WRITE 也會(huì)被拒絕。所以先量一下 WP 引腳電壓確認(rèn)是高電平。如果 WEL 已經(jīng)是 1但 WRITE 還是寫不進(jìn)去就要查地址對(duì)不對(duì)。MR25H40CDF 只有 19 位有效地址最高地址是 0x7FFFF如果地址算錯(cuò)寫到了非法區(qū)域器件不會(huì)報(bào)錯(cuò)但數(shù)據(jù)也進(jìn)不去。我總是先用固定地址比如 0x00000 來(lái)測(cè)試寫一組 0x5A 5A 5A 5A讀回比對(duì)最大限度減少變量。最后還要注意一個(gè)時(shí)序細(xì)節(jié)寫命令的數(shù)據(jù)字節(jié)必須在 CS 上升沿之前全部發(fā)完。對(duì)于 MR25H40CDF寫入動(dòng)作就是 CS 上升沿那個(gè)瞬間提交的如果你在發(fā)送數(shù)據(jù)的中途就拉高 CS這次寫入會(huì)失敗或者只寫入半個(gè)字節(jié)。用示波器抓 CS 和 SCLK 的關(guān)系確認(rèn) CS 拉高前最后一個(gè)數(shù)據(jù)位已經(jīng)穩(wěn)定這比盯著代碼猜靠譜得多。4.3 百萬(wàn)次滾寫與掉電回讀測(cè)試驅(qū)動(dòng)調(diào)通之后真正的考驗(yàn)才開始。我寫了一個(gè)壓力測(cè)試固件對(duì)固定地址反復(fù)寫入遞增計(jì)數(shù)每次寫完立即讀回校驗(yàn)并使用軟件 CRC32 做數(shù)據(jù)完整性判斷。日志通過(guò)串口輸出。測(cè)試結(jié)果很直觀260 萬(wàn)次滾寫全部通過(guò)沒有一次讀回錯(cuò)誤。這個(gè)數(shù)字對(duì) EEPROM 來(lái)說(shuō)已經(jīng)是不可想象的任務(wù)對(duì) MRAM 來(lái)說(shuō)只是熱身。我也用示波器同時(shí)觀察了 SPI 波形寫操作完成后 CS 上升沿到下一次命令發(fā)起之間沒有額外延時(shí)說(shuō)明 MRAM 確實(shí)不需要內(nèi)部寫等待。掉電測(cè)試更有意思。我在主循環(huán)里周期性寫一條 64 字節(jié)記錄然后人為隨機(jī)斷電再重新上電掃描 MRAM 里的記錄區(qū)檢查每一條記錄的 magic、長(zhǎng)度和 CRC。因?yàn)?MRAM 的寫入是原子提交每次結(jié)果要么是完整的舊記錄要么是完整的新記錄沒有出現(xiàn)半條記錄混雜的情況。這正是工業(yè)掉電場(chǎng)景最需要的特性。高低溫方面我用熱風(fēng)槍把板子加熱到 105℃ 左右持續(xù)運(yùn)行再放回常溫驗(yàn)證數(shù)據(jù)讀寫均正常。有條件的話建議做一輪完整的 -40℃ 到 85℃ 溫度循環(huán)工業(yè)級(jí)存儲(chǔ)器件在這種條件下反而比消費(fèi)級(jí) Flash 穩(wěn)定得多。5. 工程化思考掉電保護(hù)、容量規(guī)劃與跨平臺(tái)遷移5.1 掉電瞬間怎么寫不丟MRAM 本身是非易失的但“非易失”不代表“掉電時(shí)正在寫的操作也一定能完成”。真正的工程問(wèn)題是當(dāng)電源開始跌落而 MCU 恰好正在往 MRAM 寫數(shù)據(jù)這個(gè)事務(wù)到底能不能完整提交。我建議從兩頭發(fā)力。第一硬件上在系統(tǒng)電源端加掉電檢測(cè)。RA2L1 自帶低電壓檢測(cè)模塊也可以外接 TPS3839 之類的電壓監(jiān)控芯片當(dāng) VCC 低于 3.0 V 時(shí)觸發(fā)中斷或復(fù)位。第二固件里把關(guān)鍵數(shù)據(jù)維護(hù)成“鏡像”。比如環(huán)形日志區(qū)先寫入記錄正文和 CRC全部成功后再更新日志頭里的寫指針。掉電發(fā)生在新記錄寫了一半時(shí)上電后頭部指針還指向上一條記錄新記錄雖然空間占用了但不會(huì)被當(dāng)作有效記錄也不會(huì)破壞舊數(shù)據(jù)。MRAM 寫 512 字節(jié)在 20 MHz SPI 下大概是 200 微秒左右加上命令和地址開銷也不到 300 微秒。如果 MCU 的掉電檢測(cè)能在電壓跌落到器件最低工作電壓之前留出 1 毫秒那么寫一兩條關(guān)鍵記錄完全來(lái)得及。真正要避免的是在電源已經(jīng)不穩(wěn)的時(shí)候還去做耗時(shí)的讀改寫流程所以掉電中斷里只做最精簡(jiǎn)的寫操作。5.2 磨損、容量與緩沖區(qū)規(guī)劃MRAM 雖然壽命極高但它也有自己的“寫周期”概念。一次完整的 CS 上升沿提交算一個(gè)寫周期無(wú)論寫 1 字節(jié)還是寫 256 字節(jié)都是消耗一次。所以真正高效的用法是數(shù)據(jù)攢夠一包再寫而不是每來(lái)一個(gè)字節(jié)就發(fā)起一次事務(wù)。這樣做還能減少 SPI 總線占用和調(diào)度開銷。容量規(guī)劃方面512 KB 需要想清楚分區(qū)。我習(xí)慣把 MRAM 分成三個(gè)區(qū)域區(qū)域大小用途參數(shù)區(qū)4 KB設(shè)備配置、校準(zhǔn)參數(shù)低頻寫日志索引區(qū)4 KB寫指針、讀指針掉電恢復(fù)依據(jù)循環(huán)日志區(qū)剩余空間環(huán)形數(shù)據(jù)記錄高頻覆蓋寫參數(shù)區(qū)低頻寫入用雙緩沖加 CRC 防止更新過(guò)程中掉電。日志索引區(qū)單獨(dú)劃出來(lái)是因?yàn)槊看螌懭胗涗浐蠖家轮羔樔绻羔樅陀涗浕煸谝黄馂?zāi)難恢復(fù)會(huì)非常麻煩。主日志區(qū)做環(huán)形緩沖滿了以后覆蓋最舊記錄這樣 MRAM 的寫壽命被分散在整個(gè)區(qū)域而不是集中在一個(gè)地址上。如果設(shè)備需要保存較大的波形數(shù)據(jù)或批量事件512 KB 可能不夠那就需要考慮把 MRAM 換成功耗更高的 SPI NOR Flash或者加 SD 卡。但請(qǐng)注意這種擴(kuò)容通常會(huì)犧牲隨機(jī)寫入壽命和寫速度本質(zhì)上是用復(fù)雜度換容量不是所有場(chǎng)景都劃算。5.3 換個(gè) MCU 怎么辦MR25H40CDF 是標(biāo)準(zhǔn) SPI 命令集和主控品牌沒有綁定關(guān)系。RA2L1 用 FSPSTM32 用 HALNXP 用 SDK底層函數(shù)各不一樣但上層邏輯完全可以原樣搬走。我在驅(qū)動(dòng)設(shè)計(jì)時(shí)就把硬件有關(guān)的操作收斂成了幾個(gè)內(nèi)部函數(shù)mram_cs_low、mram_cs_high、mram_tx_bytes、mram_txrx_bytes、mram_wait_tx_done。換平臺(tái)時(shí)只需要重寫這五個(gè)函數(shù)的實(shí)現(xiàn)mram_write_buf 和 mram_read_buf 不需要?jiǎng)?。如果換成帶硬件 CS 的 MCU我還是堅(jiān)持用 GPIO 軟控 CS因?yàn)?MRAM 命令序列對(duì) CS 邊沿的要求比較明確軟控不受外設(shè)自動(dòng)控制邏輯干擾出問(wèn)題也好定位。另外提醒一點(diǎn)不同 MCU 的 SPI 時(shí)鐘極性命名可能不同。ST 的 HAL 里 SPI_MODE0 是 CPOL0、CPHA0SPI_MODE3 是 CPOL1、CPHA1。瑞薩 FSP 里同樣用模式 0/模式 3 命名但有些老工程師習(xí)慣叫“空閑時(shí)鐘電平高還是低”這時(shí)候直接看寄存器定義最安全。這次把存儲(chǔ)介質(zhì)從 EEPROM 換成 MRAM改動(dòng)的代碼量并不大但帶來(lái)的是整個(gè)系統(tǒng)可靠性的明顯提升。如果非要說(shuō)留下什么經(jīng)驗(yàn)我會(huì)記下三條第一MRAM 不是 Flash別再用擦除的老思路第二CS 邊沿的時(shí)序比命令字本身更關(guān)鍵第三測(cè)試存儲(chǔ)器件一定要在真實(shí)寫頻和掉電條件下跑而不是寫入一次讀一次就算驗(yàn)證完成。后面如果再遇到類似的工業(yè)數(shù)據(jù)記錄需求我還是會(huì)優(yōu)先看這類 MRAM 方案。