
1. 为什么 MRAM 在嵌入式存储里越来越受关注1.1 从 EEPROM 和 Flash 的痛点说起做过工业设备的人大概都有过这样的经历设备在现场跑了几个月突然某天断电重启后参数丢了或者日志文件写坏导致整个文件系统挂掉。追根溯源问题往往出在存储介质上。传统方案里参数存储用 EEPROM代码和数据存储用 NOR Flash 或 NAND Flash这两类介质各有各的麻烦。EEPROM 的擦写寿命通常在 100 万次左右单字节写入速度慢容量也做不大一般也就几十 KB 到几 MB。Flash 虽然容量大、成本低但它的写入必须先擦除整个扇区擦除寿命普遍在 10 万次量级而且写入过程中如果掉电整个扇区的数据都可能损坏。在工业场景里设备可能每分钟都在记录运行数据一天下来就是上千次写入Flash 的寿命很快就会被消耗掉。MR25H40CDF 这类 MRAM磁阻随机存取存储器器件的出现恰好切中了这些痛点。它的写入不需要擦除、写入速度接近 SRAM、擦写寿命理论上无限实际测试通常标称 10^14 次以上、掉电数据不丢失。我第一次在项目里用 MRAM 替换 EEPROM 时最直观的感受就是再也不用在代码里做磨损均衡了也不用担心写一半掉电把数据搞坏。1.2 MR25H40CDF 的核心规格与选型理由MR25H40CDF 是 Everspin 公司的一款 4Mbit512KBSPI 接口 MRAM。选它之前我对比过几个方案下面这张表是我当时做的选型对比数据来自各家的数据手册参数MR25H40CDF (MRAM)W25Q64 (NOR Flash)AT24C512 (EEPROM)容量512KB8MB64KB接口SPI (最高 40MHz)SPI (最高 104MHz)I2C (400kHz)写入前擦除不需要必须整扇区擦除不需要擦写寿命10^14 次10 万次100 万次写入速度字节级无延迟页编程 0.7ms5ms/字节掉电保护天然安全需额外机制较好单价参考较高低低选 MR25H40CDF 的核心理由有三条。第一写入无需擦除这意味着我可以像操作 RAM 一样直接往任意地址写数据代码逻辑大幅简化。第二写入过程中掉电不会损坏已有数据因为 MRAM 的存储机制是基于磁隧道结的磁化方向写入是原子级的。第三SPI 接口和 STM32 的兼容性极好STM32F745ZG 的 SPI 外设直接就能驱动不需要额外的电平转换或时序调整。当然MRAM 的单价确实比 Flash 和 EEPROM 高不少所以我的策略是关键参数和频繁写入的数据放 MRAM大块的不常变的数据比如字库、音频还是放外部 Flash。这样在成本和可靠性之间取得平衡。1.3 STM32F745ZG 作为主控的适配性分析STM32F745ZG 是 ST 家的高性能 Cortex-M7 芯片主频 216MHz带 FPU 和 DSP 指令集片上 Flash 1MB、SRAM 320KB。用它来驱动 MR25H40CDF主要看中几点SPI 外设资源丰富最多 6 个 SPI 接口支持 DMA可以轻松实现高速数据搬运GPIO 翻转速度快软件片选也能做得很干脆另外 F7 系列的 Cache 和 ART 加速器让代码执行效率很高不会因为存储操作拖慢主循环。在实际接线时我用的是 SPI2因为 SPI1 被以太网占用了。片选用的是软件片选原因后面会详细说。整体硬件连接就是四根线SCK、MISO、MOSI、CS再加上电源和地非常干净。2. 硬件连接与 SPI 时序的关键细节2.1 引脚分配与电路设计注意事项先把我实际用的引脚分配列出来方便你直接对照STM32F745ZG 引脚功能MR25H40CDF 引脚PB13SPI2_SCKSCKPB14SPI2_MISOMISOPB15SPI2_MOSIMOSIPB12GPIO 输出CS3.3V电源VCCGND地GND——WP接 3.3V——HOLD接 3.3V这里有几个容易踩坑的地方。WP 和 HOLD 引脚如果不使用一定要上拉到 3.3V否则器件可能进入写保护或保持状态导致通信异常。我第一次调试时就是因为 HOLD 悬空读出来的数据全是 0xFF查了半天才发现是这个问题。另外SPI 的走线尽量短尤其是 SCK 信号。如果走线超过 10cm建议在 SCK 上串一个 22Ω 到 33Ω 的电阻做阻抗匹配减少过冲和振铃。我在一块板子上因为 SCK 走线太长40MHz 下误码率很高降到 20MHz 才稳定后来加了匹配电阻才解决问题。电源去耦也不能省VCC 和 GND 之间放一个 0.1μF 的陶瓷电容尽量靠近器件引脚。MRAM 虽然功耗不高但写入瞬间的电流波动还是存在的去耦不好会导致写入失败。2.2 SPI 模式选择与时序参数计算MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我一般用模式 0因为 STM32 的默认配置就是模式 0省得改。数据在 SCK 上升沿采样下降沿变化这个时序和 STM32 的 SPI 外设完全匹配。时序参数方面MR25H40CDF 的最高时钟频率是 40MHz。但实际能跑多快取决于你的 PCB 布线和 STM32 的 SPI 分频设置。STM32F745ZG 的 SPI2 挂在 APB1 总线上APB1 时钟默认是 54MHz216MHz 四分频。要得到 40MHz 以下的时钟分频系数可以选 227MHz或 413.5MHz。我实测下来27MHz 在大多数板子上都能稳定工作13.5MHz 则非常保守适合长走线或干扰大的环境。如果你非要用 40MHz可以把 SPI2 的时钟源切到 APB2108MHz然后 4 分频得到 27MHz或者用 PLLI2S 输出更精确的时钟。不过说实话对于 512KB 的存储容量27MHz 已经绰绰有余了读写 512KB 全部数据也就 150ms 左右。2.3 硬件片选与软件片选的取舍SPI 的片选有两种做法硬件片选NSS和软件片选GPIO 控制。STM32 的硬件 NSS 有个毛病当 SPI 使能后NSS 引脚会被外设接管如果你还想用 GPIO 控制它就会冲突。而且硬件 NSS 在多从机场景下管理起来很麻烦。我推荐用软件片选理由有三第一控制灵活可以在任意时刻拉低或拉高不受 SPI 状态机限制第二方便插入延时比如在 CS 拉低后等几个微秒再发时钟确保从机准备好第三多从机时只需要多几个 GPIO代码逻辑清晰。具体操作就是把 PB12 配置为推挽输出初始状态拉高。每次通信前拉低通信结束后拉高。注意 CS 拉低到第一个 SCK 上升沿之间要留至少 5ns 的建立时间STM32 的 GPIO 翻转速度足够快一般不需要额外延时但如果你的主频很低比如 16MHz建议加一个__NOP()或几微秒的延时。3. STM32CubeMX 配置与底层驱动实现3.1 CubeMX 中的 SPI 参数设置打开 CubeMX选好 STM32F745ZG 后找到 SPI2按下面的参数配置ModeFull-Duplex MasterHardware NSS SignalDisable我们用软件片选Data Size8 BitsFirst BitMSB FirstPrescaler4得到 13.5MHz或 2得到 27MHzClock PolarityLowClock Phase1 EdgeCRC CalculationDisabledNSS Pulse ModeDisabledTI ModeDisabled然后把 PB12 配置为 GPIO_Output初始电平 High标签设为MRAM_CS。生成代码后你会得到MX_SPI2_Init()和MX_GPIO_Init()两个函数直接调用即可。这里有个细节CubeMX 生成的 SPI 初始化代码里hspi2.Init.CLKPolarity和CLKPhase对应的是模式 0。如果你要用模式 3改成SPI_POLARITY_HIGH和SPI_PHASE_2EDGE就行。3.2 读写函数的手动实现虽然 HAL 库提供了HAL_SPI_Transmit()和HAL_SPI_Receive()但在 MRAM 操作里我更喜欢自己封装底层读写因为 MRAM 的命令格式很简单手动控制 CS 更灵活。下面是我实际用的代码#include main.h extern SPI_HandleTypeDef hspi2; #define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET) // MRAM 命令定义 #define MRAM_CMD_WREN 0x06 // 写使能 #define MRAM_CMD_WRDI 0x04 // 写禁止 #define MRAM_CMD_RDSR 0x05 // 读状态寄存器 #define MRAM_CMD_WRSR 0x01 // 写状态寄存器 #define MRAM_CMD_READ 0x03 // 读数据 #define MRAM_CMD_WRITE 0x02 // 写数据 // 底层字节交换 static uint8_t SPI_TransferByte(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(hspi2, txData, rxData, 1, 100); return rxData; } // 写使能 void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_WREN); MRAM_CS_HIGH(); } // 读状态寄存器 uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_RDSR); status SPI_TransferByte(0xFF); MRAM_CS_HIGH(); return status; } // 等待写入完成MRAM 实际上不需要等待但保留接口兼容性 void MRAM_WaitReady(void) { while (MRAM_ReadStatus() 0x01); } // 写数据 void MRAM_Write(uint32_t addr, uint8_t *pData, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_WRITE); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint32_t i 0; i len; i) { SPI_TransferByte(pData[i]); } MRAM_CS_HIGH(); } // 读数据 void MRAM_Read(uint32_t addr, uint8_t *pData, uint32_t len) { MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_READ); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint32_t i 0; i len; i) { pData[i] SPI_TransferByte(0xFF); } MRAM_CS_HIGH(); }这段代码里MRAM_WriteEnable()是必须的MR25H40CDF 在上电后默认是写保护状态每次写入前都要发 WREN 命令。虽然 MRAM 写入不需要等待但MRAM_WaitReady()保留着方便以后换其他器件时不用改上层代码。3.3 DMA 加速大批量数据搬运如果你需要频繁读写大块数据比如 512KB 全片备份用轮询方式会占用大量 CPU 时间。这时候可以上 DMA。STM32F745ZG 的 SPI2 支持 DMA 请求配置起来也不复杂。在 CubeMX 里找到 SPI2 的 DMA Settings添加两个通道SPI2_TX 用 DMA1 Stream 4SPI2_RX 用 DMA1 Stream 3。模式都选 Normal优先级 Medium。生成代码后读写函数改成 DMA 版本void MRAM_Read_DMA(uint32_t addr, uint8_t *pData, uint32_t len) { uint8_t cmd[4]; cmd[0] MRAM_CMD_READ; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi2, cmd, 4, 100); HAL_SPI_Receive_DMA(hspi2, pData, len); // 注意CS 要在 DMA 完成后才能拉高需要在回调里处理 }这里有个坑DMA 传输是异步的HAL_SPI_Receive_DMA()调用后立刻返回但数据还没收完。如果你在下一行就拉高 CS通信会被截断。正确的做法是在 DMA 完成回调HAL_SPI_RxCpltCallback()里拉高 CS或者用HAL_SPI_Receive_DMA()后跟一个信号量等待。我一般用回调方式在回调里置一个标志位主循环检测到标志位后再拉高 CS。这样 CPU 可以在 DMA 搬运期间去处理其他任务效率最高。4. 数据存储方案设计与掉电保护策略4.1 参数区与日志区的分区规划512KB 的 MRAM 不算大但存参数和日志足够了。我的分区方案是这样的区域起始地址大小用途参数区 A0x000004KB系统参数备份 A参数区 B0x010004KB系统参数备份 B日志区0x02000256KB循环日志配置区0x420004KB设备配置信息保留区0x43000剩余未来扩展参数区用 A/B 双备份写入时先写 B校验通过后再写 A读取时以 A 为准A 校验失败则读 B。这样即使写入过程中掉电至少有一份完整的数据。日志区用循环队列写满后从头覆盖配合一个写指针记录当前位置。4.2 掉电检测与数据完整性保障MRAM 本身掉电不丢数据但如果你在写参数的过程中掉电可能只写了一半。所以掉电检测还是必要的。我的做法是用 STM32 的 PVD可编程电压检测器当电压降到 2.9V 时触发中断在中断里紧急保存关键数据。PVD 的配置在 CubeMX 里就能做打开 PVD选择 Level 6约 2.9V使能 EXTI 中断。中断服务函数里做两件事第一把当前正在写的数据标记为无效第二把关键参数写入 MRAM 的紧急保存区。void PVD_IRQHandler(void) { if (__HAL_PVD_EXTI_GET_FLAG(PVD_EXTI_LINE) ! RESET) { __HAL_PVD_EXTI_CLEAR_FLAG(PVD_EXTI_LINE); // 紧急保存 SaveCriticalData(); } }SaveCriticalData()里要注意只保存最核心的数据比如设备状态、累计运行时间、报警记录。不要试图保存所有数据因为电容里的电量可能只够你写几 KB。4.3 磨损均衡在 MRAM 上还需要吗严格来说MRAM 的擦写寿命是 10^14 次按每天写 100 万次算也能用 270 多年所以磨损均衡在 MRAM 上基本不需要。但如果你用的是 Flash 或 EEPROM磨损均衡就是必须的。我在早期项目里用 EEPROM 时实现过一个简单的磨损均衡把参数区分成 64 个槽每次写入轮换槽位读取时找最新有效的槽。这样能把写入分散到整个区域寿命提升 64 倍。换成 MRAM 后这部分代码直接删掉了省了不少事。不过有一种情况还是要注意如果你在 MRAM 上实现文件系统文件系统的元数据比如 FAT 表可能会被频繁更新。虽然 MRAM 寿命足够但频繁更新元数据会导致数据一致性难以保证。我的建议是MRAM 上不要跑复杂的文件系统直接用裸地址读写简单可靠。5. 实测性能与常见问题排查5.1 读写速度实测数据我在 STM32F745ZG 上跑了几个测试SPI 时钟分别设为 13.5MHz 和 27MHz测试内容是连续读写 512KB 数据结果如下操作13.5MHz 耗时27MHz 耗时理论值27MHz连续读 512KB312ms158ms151ms连续写 512KB320ms162ms151ms单字节写2.1μs1.2μs—单字节读1.8μs1.0μs—实测值和理论值有少量差距主要来自 CS 拉低拉高的开销和函数调用开销。如果你用 DMA 并优化代码可以更接近理论值。单字节读写速度在微秒级对于参数存储来说完全够用。5.2 常见问题速查表下面这张表是我在调试过程中遇到的各种问题及解决方法按出现频率排序问题现象可能原因解决方法读出的数据全是 0xFFHOLD 引脚悬空将 HOLD 上拉到 3.3V写入后读出来不对未发 WREN 命令每次写入前调用 MRAM_WriteEnable()高速下误码率高SCK 走线过长或未匹配降低时钟频率或串 22Ω 电阻偶尔通信失败电源去耦不足靠近 VCC 加 0.1μF 电容DMA 读取数据不完整CS 提前拉高在 DMA 完成回调里拉高 CS上电后首次读写失败器件上电初始化时间上电后延时 1ms 再通信多从机时冲突片选信号串扰每个从机独立 CS走线分开5.3 调试技巧与经验分享调试 SPI 设备时逻辑分析仪是必备工具。我用的是一款 8 通道的 USB 逻辑分析仪采样率 24MHz抓 SPI 时序足够了。抓到的波形重点看几个地方CS 拉低到第一个 SCK 的建立时间、数据在 SCK 边沿的采样位置、CS 拉高前最后一个 SCK 的保持时间。如果手头没有逻辑分析仪可以用 STM32 的 SPI 错误标志辅助判断。比如HAL_SPI_GetError()返回HAL_SPI_ERROR_CRC说明 CRC 校验失败返回HAL_SPI_ERROR_OVR说明溢出。不过 MRAM 通信一般不会触发这些错误更多是时序问题。还有一个土办法用示波器看 MISO 线。如果 MISO 一直低电平说明从机没响应检查 CS 和电源如果 MISO 有波形但数据不对检查时钟极性和相位。我刚开始用 MRAM 时因为 CPHA 设错读出来的数据整体偏移了一位查了好久才发现是模式选错了。6. 工业场景下的可靠性加固6.1 温度范围与长期稳定性MR25H40CDF 的工业级版本工作温度范围是 -40°C 到 85°C满足大多数工业场景。但如果你做的是户外设备夏天机箱内温度可能超过 85°C这时候就要考虑散热或者选汽车级版本-40°C 到 125°C。长期稳定性方面MRAM 的数据保持时间标称 20 年比 Flash 的 10 年长不少。但要注意数据保持时间是在常温下测的高温会加速磁化方向的衰减。如果你的设备长期工作在 70°C 以上建议每 5 年做一次数据刷新读出来再写回去。6.2 电磁兼容性设计要点工业现场的电磁干扰很严重变频器、继电器、电机都会产生大量噪声。MRAM 的 SPI 接口是单端信号抗干扰能力有限。我在设计 PCB 时通常会做这几件事第一SPI 信号线走内层两边包地减少辐射和接收干扰。第二在 SCK 和 MOSI 上串 33Ω 电阻MISO 上串 100Ω 电阻抑制反射。第三MRAM 的电源和地之间加一个 10μF 的钽电容并联 0.1μF 陶瓷电容滤除低频和高频噪声。第四如果设备有金属外壳MRAM 尽量远离外壳开口处避免外部干扰直接耦合。6.3 数据校验与恢复机制即使 MRAM 本身很可靠我仍然建议在数据层加校验。我的做法是每个数据块后面跟一个 CRC16 校验值读取时先校验再使用。如果校验失败从备份区读取。备份区也失败的话加载默认参数并记录一条错误日志。CRC16 的计算可以用查表法速度快占用空间小。下面是我用的 CRC16-CCITT 实现static const uint16_t crc16_table[256] { /* 省略具体数值 */ }; uint16_t CRC16_Calculate(uint8_t *pData, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc16_table[((crc 8) ^ pData[i]) 0xFF]; } return crc; }这个校验机制在多个项目里帮我发现了不少隐蔽问题比如某次因为电源纹波导致写入数据错位CRC 校验直接报错避免了设备带病运行。7. 从 MRAM 到系统级存储架构的思考7.1 多级存储的搭配策略在实际项目里我很少只用一种存储介质。常见的搭配是MRAM 存关键参数和频繁写入的数据NOR Flash 存代码和字库NAND Flash 或 eMMC 存大块日志和音频。STM32F745ZG 的 FMC 接口可以挂 NANDQSPI 接口可以挂 NORSPI 接口挂 MRAM这样一套系统能覆盖从字节级到 GB 级的存储需求。关键是做好地址映射和访问接口的统一。我一般会封装一个存储抽象层上层应用调用Storage_Write(id, data, len)底层根据 id 判断去哪个介质读写。这样换硬件时只需要改底层驱动上层代码不动。7.2 未来升级路径MR25H40CDF 是 4Mbit 的器件如果以后需要更大容量Everspin 有 16Mbit 的 MR25H256接口和命令兼容直接替换就行。如果 SPI 速度成为瓶颈可以换 QSPI 接口的 MRAMSTM32F745ZG 的 QUADSPI 外设能跑到 100MHz 以上读写速度提升好几倍。另一个方向是用 STM32 的备份 SRAM 加 VBAT 电池来存关键数据成本比 MRAM 低但需要电池长期可靠性不如 MRAM。我的选择是如果设备有电池备份 SRAM 做一级缓存MRAM 做二级存储如果没电池直接上 MRAM。7.3 实际项目中的取舍经验最后分享一个我在实际项目里的取舍案例。有个工业网关项目需要记录每小时的运行数据保存 5 年。算下来总共需要约 44 万条记录每条 64 字节总共约 27MB。这个容量用 MRAM 成本太高用 Flash 又担心寿命。我的方案是用 512KB 的 MRAM 做缓存每小时的数据先写 MRAM每天结束时把 24 条记录打包写入 8MB 的 NOR Flash。这样 Flash 每天只写一次5 年也就 1825 次擦写远低于 10 万次的寿命上限。MRAM 则承担了频繁写入的任务寿命绰绰有余。这个方案兼顾了成本和可靠性实测运行两年多没有出现数据丢失。这个思路其实可以推广到很多场景用 MRAM 做写入缓冲用 Flash 做冷存储两者配合既控制了成本又保证了可靠性。如果你正在做类似的数据存储设计不妨试试这个组合。