動(dòng)MR25H40CDF MRAM:工業(yè)級(jí)掉電保護(hù)與高頻數(shù)據(jù)存儲(chǔ)實(shí)戰(zhàn))
1. 為什么 MRAM 在嵌入式存儲(chǔ)里越來(lái)越受關(guān)注搞嵌入式的人大多有過(guò)這種經(jīng)歷設(shè)備跑在現(xiàn)場(chǎng)突然斷電Flash 里的配置參數(shù)丟了一半或者寫(xiě)日志寫(xiě)到一半掉電整個(gè)文件系統(tǒng)直接掛掉。傳統(tǒng)方案要么加超級(jí)電容要么加鐵電存儲(chǔ)器FRAM要么用帶掉電檢測(cè)的復(fù)雜軟件邏輯去兜底。這些辦法都能用但成本和復(fù)雜度擺在那里。MR25H40CDF 這顆芯片之所以值得單獨(dú)拿出來(lái)聊是因?yàn)樗选胺且资А焙汀跋?SRAM 一樣隨便寫(xiě)”這兩件事合到了一起。它是 Everspin 出的 4Mbit 串行 MRAMSPI 接口40MHz 時(shí)鐘沒(méi)有寫(xiě)延遲沒(méi)有擦除動(dòng)作寫(xiě)多少次都不會(huì)壞。你把它當(dāng)成一個(gè)通過(guò) SPI 訪問(wèn)的 SRAM 來(lái)用就行斷電之后數(shù)據(jù)還在。這個(gè)特性在工業(yè)數(shù)據(jù)記錄、參數(shù)存儲(chǔ)、掉電保護(hù)場(chǎng)景里非常實(shí)用。STM32F410RB 是 ST 的 Cortex-M4 入門(mén)級(jí) MCU100MHz 主頻128KB Flash32KB SRAM帶硬件 SPI。它不算高端但勝在便宜、好買、生態(tài)成熟做工業(yè)傳感器節(jié)點(diǎn)、數(shù)據(jù)采集終端、小型控制器綽綽有余。用它來(lái)驅(qū)動(dòng) MR25H40CDF硬件上只需要幾根線軟件上用 HAL 庫(kù)的 SPI 接口就能跑通。這套組合解決的核心問(wèn)題是在低成本嵌入式系統(tǒng)里實(shí)現(xiàn)高可靠、高頻次、掉電安全的數(shù)據(jù)存儲(chǔ)。適合做工業(yè)現(xiàn)場(chǎng)設(shè)備、電力監(jiān)控終端、醫(yī)療數(shù)據(jù)記錄儀、車載黑匣子這類對(duì)數(shù)據(jù)完整性有要求的場(chǎng)景。如果你正在用 Flash 模擬 EEPROM 或者外掛 FRAM這篇文章可以給你一個(gè)替代思路。2. 硬件設(shè)計(jì)從引腳到 PCB 的實(shí)操細(xì)節(jié)2.1 MR25H40CDF 的引腳定義與連接方式MR25H40CDF 是 8 引腳 SOIC 封裝引腳定義很標(biāo)準(zhǔn)引腳編號(hào)名稱功能說(shuō)明1CS片選低電平有效2SO數(shù)據(jù)輸出MISO3WP寫(xiě)保護(hù)低電平有效4VSS地5SI數(shù)據(jù)輸入MOSI6SCK時(shí)鐘7HOLD保持低電平有效8VDD電源2.7V~3.6V跟 STM32F410RB 的連接方式很直接CS 接 MCU 的任意 GPIO建議用 SPI 的硬件 NSS 引腳也可以用軟件片選SCK、SI、SO 分別接 SPI 的 SCK、MOSI、MISOWP 和 HOLD 如果不使用直接上拉到 VDD電源引腳旁邊放 0.1uF 去耦電容越近越好這里有個(gè)細(xì)節(jié)MR25H40CDF 的 WP 引腳是低電平有效也就是說(shuō)你如果把它懸空內(nèi)部沒(méi)有上拉的話寫(xiě)保護(hù)狀態(tài)是不確定的。我一般直接在 PCB 上把 WP 和 HOLD 通過(guò) 10K 電阻上拉到 VDD需要寫(xiě)保護(hù)的時(shí)候再用 GPIO 拉低。這樣既保證了默認(rèn)狀態(tài)可寫(xiě)又保留了硬件寫(xiě)保護(hù)的能力。2.2 SPI 模式選擇與時(shí)鐘配置MR25H40CDF 支持 SPI 模式 0 和模式 3。模式 0 是 CPOL0、CPHA0模式 3 是 CPOL1、CPHA1。兩種模式都能用但我在實(shí)際項(xiàng)目里更傾向用模式 0因?yàn)榇蟛糠?SPI Flash 和傳感器都用模式 0統(tǒng)一起來(lái)調(diào)試方便。時(shí)鐘頻率方面MR25H40CDF 最高支持 40MHz。STM32F410RB 的 SPI1 掛在 APB2 上最高 100MHz分頻后可以到 50MHz、25MHz、12.5MHz 等。我一般先用 12.5MHz 調(diào)通再往上提到 25MHz 跑穩(wěn)定性測(cè)試。40MHz 不是不能用但要看 PCB 走線和線長(zhǎng)短線、阻抗控制好的情況下沒(méi)問(wèn)題排線長(zhǎng)了就容易出錯(cuò)。注意SPI 時(shí)鐘極性相位配置錯(cuò)了讀出來(lái)的數(shù)據(jù)全是 0xFF 或者 0x00這是新手最容易踩的坑。先用邏輯分析儀抓一下波形確認(rèn) SCK 空閑電平和數(shù)據(jù)采樣邊沿。2.3 PCB 布局布線的幾個(gè)關(guān)鍵點(diǎn)SPI 信號(hào)線雖然速度不算特別高但 MRAM 對(duì)時(shí)序比較敏感尤其是高速寫(xiě)入的時(shí)候。我總結(jié)了幾條經(jīng)驗(yàn)SCK 走線盡量短不要跨分割地平面MISO 和 MOSI 不要平行走太長(zhǎng)避免串?dāng)_去耦電容放在 VDD 引腳 2mm 以內(nèi)地線直接打過(guò)孔到地平面如果板子上有多個(gè) SPI 從設(shè)備片選線要分開(kāi)走不要共用有一次我做一個(gè)工業(yè)采集板MRAM 放在板子邊緣SPI 線走了 8cm25MHz 下讀寫(xiě)偶爾出錯(cuò)。后來(lái)把 MRAM 挪到 MCU 旁邊線長(zhǎng)控制在 3cm 以內(nèi)問(wèn)題直接消失。所以別小看這幾厘米的走線高速信號(hào)面前物理距離就是電氣性能。3. STM32F410RB 的 SPI 初始化與 HAL 庫(kù)配置3.1 CubeMX 里的 SPI 參數(shù)設(shè)置用 STM32CubeMX 配置 SPI1 的步驟不復(fù)雜但有幾個(gè)參數(shù)容易搞錯(cuò)Mode選 Full-Duplex MasterMRAM 是標(biāo)準(zhǔn) SPI 從設(shè)備Data Size8 BitsMR25H40CDF 按字節(jié)傳輸Clock PolarityLow模式 0Clock Phase1 Edge模式 0NSSSoftware用 GPIO 手動(dòng)控制片選Baud Rate Prescaler先選 16 分頻得到 6.25MHz調(diào)通后再改First BitMSB First生成代碼后HAL 會(huì)自動(dòng)初始化 SPI 外設(shè)。但片選引腳需要自己配置成 GPIO 輸出默認(rèn)拉高。// SPI1 初始化片段CubeMX 生成 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;3.2 片選控制的兩種方式與選擇依據(jù)STM32 的 SPI 支持硬件 NSS 和軟件 NSS。硬件 NSS 在單主單從的時(shí)候很方便但多從設(shè)備的時(shí)候就不行了。我一般用軟件 NSS把 CS 引腳當(dāng)普通 GPIO 控制。#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)軟件片選的好處是靈活你可以在一次事務(wù)里控制多個(gè)從設(shè)備也可以精確控制片選和時(shí)鐘的時(shí)序關(guān)系。缺點(diǎn)是每次傳輸都要手動(dòng)拉低拉高代碼稍微多幾行。但這點(diǎn)代碼量換來(lái)的是調(diào)試方便和邏輯清晰值得。提示片選拉低到第一個(gè)時(shí)鐘沿之間要留一點(diǎn)時(shí)間MR25H40CDF 要求 tSLCH 最小 5nsSTM32 的 GPIO 翻轉(zhuǎn)速度足夠快一般不用額外加延時(shí)。但如果你的主頻很低比如 16MHz最好在片選拉低后加一個(gè) __NOP()。3.3 SPI 讀寫(xiě)基礎(chǔ)函數(shù)封裝HAL 庫(kù)提供了 HAL_SPI_Transmit 和 HAL_SPI_Receive但直接調(diào)用它們需要自己管理片選。我習(xí)慣封裝一層void MRAM_WriteEnable(void) { uint8_t cmd 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); } void MRAM_WriteDisable(void) { uint8_t cmd 0x04; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t cmd 0x05; uint8_t status; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; }這里有個(gè)細(xì)節(jié)HAL_SPI_Transmit 和 HAL_SPI_Receive 分開(kāi)調(diào)用的時(shí)候片選不能中間拉高否則命令和數(shù)據(jù)的時(shí)序就斷了。所以必須把命令發(fā)送和數(shù)據(jù)接收放在同一個(gè)片選周期里。4. MR25H40CDF 的讀寫(xiě)操作與數(shù)據(jù)管理4.1 狀態(tài)寄存器與寫(xiě)使能機(jī)制MR25H40CDF 的狀態(tài)寄存器只有幾位有效位名稱說(shuō)明0WIP寫(xiě)進(jìn)行中MRAM 沒(méi)有擦除這位基本一直是 01WEL寫(xiě)使能鎖存執(zhí)行 WREN 后置 1寫(xiě)完自動(dòng)清 02BP0塊保護(hù)位 03BP1塊保護(hù)位 17SRWD狀態(tài)寄存器寫(xiě)保護(hù)跟 Flash 不一樣的是MRAM 沒(méi)有擦除操作也不需要等待寫(xiě)完成。WIP 位在 MRAM 上基本沒(méi)用因?yàn)閷?xiě)入是即時(shí)的。但 WEL 位還是要檢查每次寫(xiě)之前必須發(fā) WREN 命令否則寫(xiě)不進(jìn)去。void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[5]; cmd[0] 0x02; // WRITE 命令 cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; cmd[4] data; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 5, 100); MRAM_CS_HIGH(); }地址是 24 位的因?yàn)?4Mbit 等于 512KB需要 19 位地址但 MR25H40CDF 用 3 字節(jié)地址格式高位補(bǔ)零。4.2 頁(yè)寫(xiě)與連續(xù)讀的效率優(yōu)化MR25H40CDF 支持頁(yè)寫(xiě)每頁(yè) 256 字節(jié)。如果你要寫(xiě)一大塊數(shù)據(jù)逐字節(jié)寫(xiě)效率很低每次都要發(fā) WREN 和 5 字節(jié)命令頭。用頁(yè)寫(xiě)可以一次發(fā)命令頭然后連續(xù)寫(xiě) 256 字節(jié)。void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }注意頁(yè)寫(xiě)不能跨頁(yè)。如果起始地址是 0x00FF寫(xiě) 10 個(gè)字節(jié)寫(xiě)到第 2 個(gè)字節(jié)就跨到下一頁(yè)了MRAM 會(huì)從頁(yè)首覆蓋。所以寫(xiě)之前要算一下當(dāng)前頁(yè)剩余空間分兩次寫(xiě)。連續(xù)讀就更簡(jiǎn)單了發(fā)完讀命令和地址后時(shí)鐘不停數(shù)據(jù)就一直往外吐void MRAM_ReadData(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }讀操作沒(méi)有頁(yè)限制可以從任意地址開(kāi)始讀任意長(zhǎng)度地址會(huì)自動(dòng)遞增到頂部回卷到 0。4.3 數(shù)據(jù)分區(qū)與地址規(guī)劃建議512KB 的空間不算大但也不小。我一般這樣分區(qū)區(qū)域地址范圍用途系統(tǒng)參數(shù)區(qū)0x00000~0x00FFF設(shè)備配置、校準(zhǔn)系數(shù)運(yùn)行日志區(qū)0x01000~0x3FFFF循環(huán)記錄運(yùn)行數(shù)據(jù)掉電快照區(qū)0x40000~0x7FFFF掉電瞬間的關(guān)鍵狀態(tài)系統(tǒng)參數(shù)區(qū)用雙備份加 CRC 校驗(yàn)寫(xiě)的時(shí)候先寫(xiě)備份區(qū)再寫(xiě)主區(qū)讀的時(shí)候?qū)Ρ葍蓚€(gè)區(qū)的 CRC哪個(gè)對(duì)用哪個(gè)。運(yùn)行日志區(qū)用環(huán)形緩沖寫(xiě)指針存在 MRAM 固定位置每次寫(xiě)日志先更新指針再寫(xiě)數(shù)據(jù)。掉電快照區(qū)在檢測(cè)到掉電中斷時(shí)緊急寫(xiě)入因?yàn)?MRAM 沒(méi)有寫(xiě)延遲從檢測(cè)到掉電到寫(xiě)完關(guān)鍵數(shù)據(jù)幾百微秒就夠了。實(shí)操心得MRAM 雖然寫(xiě)不壞但地址規(guī)劃還是要留余量。我見(jiàn)過(guò)有人把參數(shù)區(qū)放在 0x00000日志區(qū)緊跟著從 0x00100 開(kāi)始結(jié)果參數(shù)區(qū)結(jié)構(gòu)一變?nèi)罩緟^(qū)全亂套。分區(qū)的時(shí)候多留點(diǎn) padding以后擴(kuò)展不頭疼。5. 工業(yè)場(chǎng)景下的可靠性設(shè)計(jì)與掉電保護(hù)5.1 掉電檢測(cè)與數(shù)據(jù)保存策略工業(yè)現(xiàn)場(chǎng)最怕的就是掉電丟數(shù)據(jù)。MRAM 的優(yōu)勢(shì)在這里體現(xiàn)得淋漓盡致不需要擦除不需要等待寫(xiě)進(jìn)去就生效。但前提是你要在電源掉到 MCU 工作電壓以下之前把關(guān)鍵數(shù)據(jù)寫(xiě)進(jìn)去。我的做法是用一個(gè)比較器監(jiān)測(cè)電源電壓閾值設(shè)在 3.0V假設(shè)系統(tǒng)是 3.3V 供電。當(dāng)電源掉到 3.0V 時(shí)比較器輸出下降沿觸發(fā) MCU 的外部中斷。在中斷里MCU 還有幾十毫秒的時(shí)間取決于電容大小足夠把幾百字節(jié)的關(guān)鍵數(shù)據(jù)寫(xiě)進(jìn) MRAM。void EXTI0_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); // 緊急保存關(guān)鍵數(shù)據(jù) MRAM_WritePage(SNAPSHOT_ADDR, (uint8_t*)g_criticalData, sizeof(g_criticalData)); // 標(biāo)記快照有效 uint8_t flag 0xA5; MRAM_WriteByte(SNAPSHOT_FLAG_ADDR, flag); } }這里的關(guān)鍵是中斷里不要做復(fù)雜運(yùn)算不要調(diào)用可能阻塞的函數(shù)直接寫(xiě) MRAM。MRAM 的頁(yè)寫(xiě) 256 字節(jié)在 25MHz SPI 下大約需要 82 微秒加上命令開(kāi)銷不到 100 微秒。只要電源電容能撐住 1 毫秒就綽綽有余。5.2 數(shù)據(jù)校驗(yàn)與壞塊管理MRAM 不像 NAND Flash 有壞塊出廠就是好的寫(xiě)不壞。但數(shù)據(jù)在傳輸過(guò)程中可能出錯(cuò)尤其是 SPI 線長(zhǎng)、干擾大的工業(yè)環(huán)境。所以校驗(yàn)還是必要的。我一般用 CRC16 對(duì)每個(gè)數(shù)據(jù)塊做校驗(yàn)存在塊尾。讀的時(shí)候先算 CRC對(duì)比存儲(chǔ)的 CRC不一致就說(shuō)明數(shù)據(jù)壞了用備份區(qū)恢復(fù)。typedef struct { uint32_t magic; uint16_t crc; uint16_t len; uint8_t data[250]; } MRAM_Block_t; uint16_t CalcCRC16(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }壞塊管理在 MRAM 上其實(shí)不需要但數(shù)據(jù)版本管理需要。我習(xí)慣在每個(gè)塊頭加一個(gè)遞增的序列號(hào)讀的時(shí)候取序列號(hào)最大的那個(gè)有效塊。這樣即使寫(xiě)的過(guò)程中掉電也能恢復(fù)到上一個(gè)有效版本。5.3 溫度與長(zhǎng)期穩(wěn)定性考量MR25H40CDF 的工業(yè)級(jí)溫度范圍是 -40°C 到 85°C數(shù)據(jù)保持時(shí)間在 85°C 下超過(guò) 10 年在 125°C 下也有 2 年。這個(gè)指標(biāo)在工業(yè)場(chǎng)景里夠用了。但要注意的是MRAM 的寫(xiě)電流隨溫度變化。低溫下寫(xiě)電流增大如果電源驅(qū)動(dòng)能力不足可能寫(xiě)失敗。我在 -40°C 的低溫箱里測(cè)試過(guò)3.3V 供電、25MHz SPI 下寫(xiě)入正常但如果你把電壓降到 2.7V低溫下偶爾會(huì)出錯(cuò)。所以工業(yè)產(chǎn)品建議電源設(shè)計(jì)留足余量LDO 選 3.3V 輸出、至少 100mA 能力的。長(zhǎng)期穩(wěn)定性方面MRAM 沒(méi)有電荷泄漏問(wèn)題不像 Flash 那樣放幾年數(shù)據(jù)就淡了。我有個(gè)項(xiàng)目用了 5 年的 MRAM數(shù)據(jù)讀出來(lái)跟剛寫(xiě)進(jìn)去一樣。這一點(diǎn)在需要長(zhǎng)期保存校準(zhǔn)數(shù)據(jù)的設(shè)備上特別有價(jià)值。6. 調(diào)試過(guò)程中遇到的典型問(wèn)題與排查方法6.1 SPI 通信失敗的常見(jiàn)原因調(diào)試 SPI 最怕的就是讀出來(lái)全是 0xFF 或者 0x00。我整理了一個(gè)排查順序現(xiàn)象可能原因排查方法讀全 0xFFMISO 沒(méi)接、片選沒(méi)拉低、從設(shè)備沒(méi)供電萬(wàn)用表測(cè) MISO 對(duì)地電壓邏輯分析儀抓波形讀全 0x00SCK 沒(méi)輸出、MOSI 沒(méi)接、模式配置錯(cuò)誤檢查 SPI 初始化代碼確認(rèn) CPOL/CPHA數(shù)據(jù)偶爾錯(cuò)時(shí)鐘太快、線太長(zhǎng)、干擾大降低 SPI 時(shí)鐘縮短走線加屏蔽寫(xiě)不進(jìn)去沒(méi)發(fā) WREN、WP 引腳被拉低、地址越界讀狀態(tài)寄存器確認(rèn) WEL 位檢查 WP 電平有一次我調(diào)一個(gè)板子讀出來(lái)全是 0xFF查了半天發(fā)現(xiàn)是 MRAM 的 VDD 引腳虛焊了。所以遇到問(wèn)題先查硬件再查軟件這個(gè)順序不能反。6.2 寫(xiě)入數(shù)據(jù)不生效的排查思路寫(xiě)不進(jìn)去的情況我遇到過(guò)幾種第一種是忘了發(fā) WREN。MR25H40CDF 每次寫(xiě)之前都要發(fā) WREN寫(xiě)完 WEL 自動(dòng)清零。如果你連續(xù)寫(xiě)多個(gè)字節(jié)每個(gè)字節(jié)都要重新發(fā) WREN。用頁(yè)寫(xiě)的時(shí)候一次 WREN 可以寫(xiě)一整頁(yè)。第二種是 WP 引腳被拉低了。WP 是硬件寫(xiě)保護(hù)低電平有效。如果你在 PCB 上把 WP 直接接地了那就永遠(yuǎn)寫(xiě)不進(jìn)去。檢查一下 WP 的電平正常應(yīng)該是高。第三種是地址算錯(cuò)了。MR25H40CDF 是 512KB地址范圍 0x00000 到 0x7FFFF。如果你寫(xiě) 0x80000地址回卷到 0x00000數(shù)據(jù)就寫(xiě)到開(kāi)頭去了。這種 bug 很隱蔽因?yàn)樽x的時(shí)候你讀 0x80000 也能讀出數(shù)據(jù)但其實(shí)是 0x00000 的內(nèi)容。6.3 高速 SPI 下的信號(hào)完整性問(wèn)題25MHz 以上的 SPI 時(shí)鐘信號(hào)完整性問(wèn)題就開(kāi)始顯現(xiàn)了。我遇到過(guò) SCK 上升沿過(guò)沖、MISO 數(shù)據(jù)眼圖閉合的情況。解決辦法SCK 線上串 22Ω 電阻抑制過(guò)沖MISO 線上加 10pF 電容到地濾高頻噪聲縮短走線最好控制在 5cm 以內(nèi)用四層板SPI 線參考完整地平面如果這些措施都做了還是不穩(wěn)定那就降時(shí)鐘。25MHz 不行就 12.5MHz12.5MHz 不行就 6.25MHz。工業(yè)產(chǎn)品穩(wěn)定第一速度第二。避坑技巧調(diào)試 SPI 的時(shí)候先用低速跑通再逐步提高時(shí)鐘。每次提高后跑 24 小時(shí)壓力測(cè)試寫(xiě)滿整個(gè) MRAM 再讀出來(lái)對(duì)比。我一般用 0x55、0xAA、0x00、0xFF 四種模式交替寫(xiě)這樣能覆蓋大部分?jǐn)?shù)據(jù)模式容易發(fā)現(xiàn)時(shí)序問(wèn)題。7. 性能實(shí)測(cè)與方案對(duì)比7.1 讀寫(xiě)速度實(shí)測(cè)數(shù)據(jù)我在 STM32F410RB 上跑了一組實(shí)測(cè)數(shù)據(jù)SPI 時(shí)鐘 25MHz模式 0操作數(shù)據(jù)量耗時(shí)等效速率單字節(jié)寫(xiě)1B3.2us312KB/s頁(yè)寫(xiě)256B98us2.6MB/s連續(xù)讀256B92us2.8MB/s連續(xù)讀4KB1.42ms2.9MB/s單字節(jié)寫(xiě)慢是因?yàn)槊看味家l(fā) WREN 和 5 字節(jié)命令頭開(kāi)銷大。頁(yè)寫(xiě)效率高很多256 字節(jié)只發(fā)一次命令頭。連續(xù)讀的效率最高因?yàn)樽x命令發(fā)完之后時(shí)鐘不停數(shù)據(jù)連續(xù)輸出。跟 SPI Flash 對(duì)比MRAM 的優(yōu)勢(shì)不在速度而在寫(xiě)入不需要擦除。Flash 寫(xiě)一個(gè)字節(jié)要先擦一個(gè)扇區(qū)4KB擦除時(shí)間幾百毫秒MRAM 寫(xiě)一個(gè)字節(jié)就是寫(xiě)一個(gè)字節(jié)3.2 微秒搞定。在頻繁寫(xiě)小數(shù)據(jù)的場(chǎng)景里MRAM 的實(shí)際吞吐量比 Flash 高幾個(gè)數(shù)量級(jí)。7.2 與 FRAM、EEPROM、Flash 的對(duì)比特性MRAMFRAMEEPROMNOR Flash寫(xiě)入方式直接寫(xiě)直接寫(xiě)直接寫(xiě)先擦后寫(xiě)寫(xiě)壽命無(wú)限10^1410^610^5寫(xiě)速度納秒級(jí)納秒級(jí)毫秒級(jí)毫秒級(jí)容量4Mbit通常 1Mbit 以下通常 1Mbit 以下4Mbit~1Gbit功耗中低中高成本中高高低低MRAM 的定位在 FRAM 和 Flash 之間。容量比 FRAM 大成本比 FRAM 低寫(xiě)入速度比 Flash 快幾個(gè)數(shù)量級(jí)。在需要頻繁寫(xiě)、數(shù)據(jù)量中等、對(duì)可靠性要求高的場(chǎng)景里MRAM 是很合適的選擇。7.3 功耗表現(xiàn)與電池供電場(chǎng)景MR25H40CDF 的功耗指標(biāo)讀電流8mA 40MHz寫(xiě)電流12mA 40MHz待機(jī)電流10uA掉電模式電流2uA在電池供電的場(chǎng)景里MRAM 的待機(jī)功耗很低適合做長(zhǎng)期數(shù)據(jù)記錄。寫(xiě)的時(shí)候電流大一點(diǎn)但寫(xiě)的時(shí)間極短平均功耗很低。我算過(guò)一筆賬每天寫(xiě) 1000 次每次寫(xiě) 256 字節(jié)25MHz SPI寫(xiě)電流 12mA每次寫(xiě) 98 微秒一天總寫(xiě)入時(shí)間 98 毫秒平均電流 12mA × 98ms / 86400s ≈ 13.6nA。這個(gè)功耗對(duì)電池來(lái)說(shuō)可以忽略不計(jì)。相比之下Flash 寫(xiě)之前要擦除擦除電流 15mA擦除時(shí)間 300ms一天擦 1000 次就是 300 秒平均電流 15mA × 300s / 86400s ≈ 52uA。差了將近 4000 倍。所以在電池供電、頻繁寫(xiě)的場(chǎng)景里MRAM 的功耗優(yōu)勢(shì)非常明顯。8. 項(xiàng)目實(shí)戰(zhàn)工業(yè)數(shù)據(jù)記錄儀的完整實(shí)現(xiàn)8.1 系統(tǒng)架構(gòu)與任務(wù)劃分我拿一個(gè)實(shí)際做過(guò)的工業(yè)數(shù)據(jù)記錄儀來(lái)舉例。這個(gè)設(shè)備采集 4 路 4-20mA 信號(hào)每 100ms 記錄一次數(shù)據(jù)存 MRAM通過(guò) RS485 上傳。系統(tǒng)架構(gòu)很簡(jiǎn)單STM32F410RB 做主控MR25H40CDF 做數(shù)據(jù)存儲(chǔ)4 路 ADC 采集RS485 收發(fā)器做通信電源管理帶掉電檢測(cè)任務(wù)劃分上我用了一個(gè)簡(jiǎn)單的前后臺(tái)架構(gòu)主循環(huán)處理 ADC 采集和 RS485 通信定時(shí)器中斷每 100ms 觸發(fā)一次數(shù)據(jù)記錄掉電中斷處理緊急保存。8.2 數(shù)據(jù)記錄與循環(huán)存儲(chǔ)的實(shí)現(xiàn)數(shù)據(jù)記錄用環(huán)形緩沖寫(xiě)指針存在 MRAM 的固定地址 0x00000數(shù)據(jù)從 0x01000 開(kāi)始循環(huán)寫(xiě)。#define LOG_START_ADDR 0x01000 #define LOG_END_ADDR 0x7FFFF #define LOG_ENTRY_SIZE 16 typedef struct { uint32_t timestamp; uint16_t ch0; uint16_t ch1; uint16_t ch2; uint16_t ch3; uint16_t crc; } LogEntry_t; void RecordData(void) { uint32_t wp ReadWritePointer(); uint32_t addr LOG_START_ADDR wp * LOG_ENTRY_SIZE; if (addr LOG_ENTRY_SIZE LOG_END_ADDR) { addr LOG_START_ADDR; wp 0; } LogEntry_t entry; entry.timestamp HAL_GetTick(); entry.ch0 g_adcValues[0]; entry.ch1 g_adcValues[1]; entry.ch2 g_adcValues[2]; entry.ch3 g_adcValues[3]; entry.crc CalcCRC16((uint8_t*)entry, sizeof(entry) - 2); MRAM_WritePage(addr, (uint8_t*)entry, sizeof(entry)); wp; WriteWritePointer(wp); }寫(xiě)指針的更新要在數(shù)據(jù)寫(xiě)入之后這樣即使寫(xiě)數(shù)據(jù)的過(guò)程中掉電寫(xiě)指針沒(méi)更新下次上電會(huì)覆蓋掉那條不完整的數(shù)據(jù)不會(huì)產(chǎn)生空洞。8.3 上位機(jī)通信與數(shù)據(jù)導(dǎo)出RS485 通信我用的是 Modbus RTU 協(xié)議上位機(jī)發(fā)命令讀日志。命令格式很簡(jiǎn)單讀日志命令 起始序號(hào) 數(shù)量。MCU 收到后從 MRAM 讀出對(duì)應(yīng)數(shù)據(jù)打包返回。void HandleReadLog(uint16_t startIdx, uint16_t count) { uint8_t buf[LOG_ENTRY_SIZE]; for (uint16_t i 0; i count; i) { uint32_t addr LOG_START_ADDR (startIdx i) * LOG_ENTRY_SIZE; if (addr LOG_ENTRY_SIZE LOG_END_ADDR) addr LOG_START_ADDR (addr - LOG_END_ADDR); MRAM_ReadData(addr, buf, LOG_ENTRY_SIZE); SendModbusResponse(buf, LOG_ENTRY_SIZE); } }這里要注意地址回卷的處理環(huán)形緩沖的地址計(jì)算容易出錯(cuò)我一般寫(xiě)個(gè)專門(mén)的函數(shù)來(lái)處理不要在業(yè)務(wù)邏輯里直接算。8.4 現(xiàn)場(chǎng)部署的注意事項(xiàng)這個(gè)設(shè)備在現(xiàn)場(chǎng)跑了兩年多總結(jié)了幾條經(jīng)驗(yàn)電源一定要加 TVS 和濾波工業(yè)現(xiàn)場(chǎng)電網(wǎng)波動(dòng)大電源毛刺會(huì)干擾 SPI 通信RS485 接口要加隔離不然地環(huán)流會(huì)燒收發(fā)器MRAM 的數(shù)據(jù)要定期備份到上位機(jī)雖然 MRAM 可靠但設(shè)備本身可能損壞外殼要接地屏蔽線要單端接地避免地環(huán)路還有一點(diǎn)MRAM 的 WP 引腳我最終沒(méi)有用硬件寫(xiě)保護(hù)而是通過(guò)軟件控制。因?yàn)楝F(xiàn)場(chǎng)調(diào)試的時(shí)候經(jīng)常需要改參數(shù)硬件寫(xiě)保護(hù)每次都要跳線太麻煩。軟件寫(xiě)保護(hù)在參數(shù)區(qū)加個(gè)標(biāo)志位需要保護(hù)的時(shí)候置位正常運(yùn)行時(shí)清除。9. 代碼組織與可移植性設(shè)計(jì)9.1 驅(qū)動(dòng)層與應(yīng)用層分離寫(xiě)嵌入式代碼最怕的就是驅(qū)動(dòng)和應(yīng)用攪在一起換個(gè) MCU 就要重寫(xiě)一遍。我的做法是把 MRAM 驅(qū)動(dòng)做成獨(dú)立的模塊只依賴幾個(gè)底層的 SPI 讀寫(xiě)函數(shù)。// mram_driver.h typedef struct { void (*cs_low)(void); void (*cs_high)(void); int (*spi_transmit)(uint8_t *data, uint16_t len); int (*spi_receive)(uint8_t *data, uint16_t len); } MRAM_Interface_t; void MRAM_Init(MRAM_Interface_t *iface); void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len); void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len);這樣換 MCU 的時(shí)候只需要重新實(shí)現(xiàn) cs_low、cs_high、spi_transmit、spi_receive 這四個(gè)函數(shù)MRAM 驅(qū)動(dòng)的邏輯一行都不用改。我在 STM32F410RB 和另一款國(guó)產(chǎn) MCU 之間移植過(guò)半天就搞定了。9.2 參數(shù)配置的持久化方案設(shè)備參數(shù)我用一個(gè)結(jié)構(gòu)體管理存在 MRAM 的 0x00000 地址雙備份加 CRCtypedef struct { uint32_t magic; uint16_t crc; uint16_t version; float k0, k1, k2, k3; uint32_t sampleInterval; uint8_t reserved[32]; } DeviceConfig_t; void SaveConfig(DeviceConfig_t *cfg) { cfg-magic 0x5A5A5A5A; cfg-crc CalcCRC16((uint8_t*)cfg, sizeof(DeviceConfig_t) - 2); MRAM_Write(CONFIG_ADDR_A, (uint8_t*)cfg, sizeof(DeviceConfig_t)); MRAM_Write(CONFIG_ADDR_B, (uint8_t*)cfg, sizeof(DeviceConfig_t)); } bool LoadConfig(DeviceConfig_t *cfg) { DeviceConfig_t cfgA, cfgB; MRAM_Read(CONFIG_ADDR_A, (uint8_t*)cfgA, sizeof(DeviceConfig_t)); MRAM_Read(CONFIG_ADDR_B, (uint8_t*)cfgB, sizeof(DeviceConfig_t)); bool validA (cfgA.magic 0x5A5A5A5A) (cfgA.crc CalcCRC16((uint8_t*)cfgA, sizeof(DeviceConfig_t) - 2)); bool validB (cfgB.magic 0x5A5A5A5A) (cfgB.crc CalcCRC16((uint8_t*)cfgB, sizeof(DeviceConfig_t) - 2)); if (validA validB) { *cfg (cfgA.version cfgB.version) ? cfgA : cfgB; return true; } else if (validA) { *cfg cfgA; return true; } else if (validB) { *cfg cfgB; return true; } return false; }雙備份的好處是寫(xiě)的時(shí)候如果掉電至少有一個(gè)備份是完整的。讀的時(shí)候取版本號(hào)大的那個(gè)保證參數(shù)是最新的。9.3 版本升級(jí)與數(shù)據(jù)遷移產(chǎn)品迭代的時(shí)候參數(shù)結(jié)構(gòu)體會(huì)變。我的做法是在結(jié)構(gòu)體里留 reserved 字段新版本用 reserved 里的空間不改變結(jié)構(gòu)體大小。如果實(shí)在要改大小就在版本號(hào)上做文章讀的時(shí)候根據(jù)版本號(hào)做兼容處理。if (cfg.version 1) { // 舊版本字段少做兼容轉(zhuǎn)換 newCfg.k0 oldCfg.k0; newCfg.k1 oldCfg.k1; newCfg.k2 0.0f; newCfg.k3 0.0f; } else if (cfg.version 2) { // 當(dāng)前版本直接拷貝 newCfg cfg; }這種兼容處理雖然麻煩但能保證老設(shè)備升級(jí)固件后參數(shù)不丟。工業(yè)設(shè)備現(xiàn)場(chǎng)升級(jí)不容易能少跑一趟就少跑一趟。10. 幾個(gè)容易被忽略的細(xì)節(jié)10.1 上電初始化的時(shí)序要求MR25H40CDF 上電后需要一段時(shí)間才能接受命令數(shù)據(jù)手冊(cè)里寫(xiě)的是 tPUW最大 10ms。我一般在上電初始化的時(shí)候加 20ms 延時(shí)確保 MRAM 準(zhǔn)備好了再發(fā)命令。void SystemInit(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); HAL_Delay(20); // 等待 MRAM 上電穩(wěn)定 MRAM_Init(mramIface); }這個(gè)延時(shí)看起來(lái)不起眼但少了它偶爾會(huì)出現(xiàn)上電后第一次讀 MRAM 失敗的情況。尤其是電源上升慢的場(chǎng)合10ms 可能不夠。10.2 SPI 片選信號(hào)的毛刺問(wèn)題軟件片選有個(gè)隱患GPIO 初始化的時(shí)候片選引腳可能會(huì)有一個(gè)短暫的毛刺。如果 MRAM 把這個(gè)毛刺當(dāng)成片選有效就會(huì)誤觸發(fā)。解決辦法是在 GPIO 初始化的時(shí)候先把引腳置高再配置成輸出模式。HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);先寫(xiě)高電平再配置成輸出這樣引腳從輸入變成輸出的瞬間就是高電平不會(huì)有毛刺。10.3 多設(shè)備共享 SPI 總線的注意事項(xiàng)如果板子上有多個(gè) SPI 從設(shè)備共享 SCK、MOSI、MISO片選分開(kāi)。這時(shí)候要注意每個(gè)從設(shè)備的 MISO 輸出要加三態(tài)緩沖或者用帶片選控制的 SPI 從設(shè)備片選切換的時(shí)候要留時(shí)間讓上一個(gè)從設(shè)備釋放 MISO不同從設(shè)備的 SPI 模式可能不同切換的時(shí)候要重新配置 SPI我一般會(huì)在片選切換后加一個(gè) __NOP() 或者幾微秒的延時(shí)確??偩€穩(wěn)定。如果從設(shè)備多建議用 SPI 多路復(fù)用器雖然多花幾毛錢但省心。10.4 數(shù)據(jù)手冊(cè)里沒(méi)寫(xiě)但實(shí)際會(huì)遇到的問(wèn)題數(shù)據(jù)手冊(cè)是理想情況下的參數(shù)實(shí)際用的時(shí)候會(huì)遇到各種意外。我遇到過(guò) MRAM 在強(qiáng)電磁干擾下寫(xiě)數(shù)據(jù)出錯(cuò)的情況后來(lái)在 SPI 線上加了共模電感才好。還遇到過(guò) MRAM 在高溫下寫(xiě)電流增大LDO 帶不動(dòng)導(dǎo)致復(fù)位的情況換了更大電流的 LDO 解決。這些經(jīng)驗(yàn)數(shù)據(jù)手冊(cè)里不會(huì)寫(xiě)只能靠實(shí)際項(xiàng)目積累。我的建議是工業(yè)產(chǎn)品一定要做環(huán)境測(cè)試高低溫、濕熱、振動(dòng)、EMC 都要過(guò)一遍。MRAM 本身很可靠但系統(tǒng)級(jí)的可靠性要靠整體設(shè)計(jì)來(lái)保證。11. 寫(xiě)在最后的一些個(gè)人體會(huì)這套 MRAM STM32 的方案我從 2019 年開(kāi)始用前后做了五六個(gè)項(xiàng)目有電力監(jiān)控的有醫(yī)療數(shù)據(jù)記錄的也有車載的。最大的感受是MRAM 確實(shí)省心。不用管擦除不用管壽命不用管掉電寫(xiě)就完了。代碼邏輯簡(jiǎn)單了出問(wèn)題的概率也低了。但 MRAM 也不是萬(wàn)能的。容量小、成本高這兩點(diǎn)決定了它不適合做大容量存儲(chǔ)。我的經(jīng)驗(yàn)是關(guān)鍵數(shù)據(jù)、頻繁寫(xiě)的數(shù)據(jù)、掉電必須保存的數(shù)據(jù)用 MRAM大塊的歷史數(shù)據(jù)、日志文件用 Flash 或者 SD 卡。兩者配合各取所長(zhǎng)。STM32F410RB 這顆 MCU 也值得說(shuō)一句。它不算新也不算強(qiáng)但勝在穩(wěn)定、便宜、資料多。做工業(yè)產(chǎn)品穩(wěn)定比性能重要便宜比先進(jìn)重要。F410 在這兩點(diǎn)上都做得不錯(cuò)。最后分享一個(gè)調(diào)試小技巧如果你懷疑 MRAM 讀寫(xiě)有問(wèn)題先寫(xiě)一個(gè)全盤(pán)測(cè)試程序從 0x00000 寫(xiě)到 0x7FFFF寫(xiě) 0x55、0xAA、0x00、0xFF 四種模式每寫(xiě)一遍讀出來(lái)對(duì)比。這個(gè)測(cè)試跑一遍大概 2 秒能快速定位是硬件問(wèn)題還是軟件問(wèn)題。我每次新板子回來(lái)第一件事就是跑這個(gè)測(cè)試跑通了再調(diào)業(yè)務(wù)邏輯。