
1. 为什么 MRAM 在工业嵌入式场景里越来越香搞过工业数据采集或者电力终端的朋友应该都有体会选存储芯片这件事很多时候比选主控还让人头疼。EEPROM 写入慢、擦写寿命有限NOR Flash 虽然容量大但写入前必须擦除整个扇区掉电保护做起来复杂FRAM 倒是快但容量小、价格高。这几年 MRAM磁阻随机存储器慢慢进入大家的视野尤其是 Everspin 的 MR25H40CDF 这颗 4Mbit 的 SPI MRAM在工业现场设备、嵌入式数据记录仪、PLC 模块里出现的频率越来越高。我最近刚用 STM32F091RC 配合 MR25H40CDF 做了一套工业参数存储与掉电记录方案从选型、硬件设计到驱动调试踩了不少坑这里把整个过程完整梳理一遍。如果你正在做工业仪表、数据采集终端、或者任何需要频繁写入且不能丢数据的嵌入式项目这套组合值得认真考虑。STM32F091RC 是 ST 家 Cortex-M0 系列的经典型号128KB Flash、32KB SRAM、主频 48MHz带 2 个 SPI 接口性价比很高在工业控制板上非常常见。MR25H40CDF 则是 512KB4Mbit容量的 SPI MRAM40MHz 时钟支持 1.8V~3.6V 宽压供电工业级温度范围 -40 到 85℃最关键的是它没有擦写寿命限制写入不需要等待掉电数据不丢。这篇文章我会从 MRAM 的底层原理讲起然后拆解硬件连接、SPI 驱动配置、读写时序、数据管理策略最后把我调试过程中遇到的几个典型问题整理成排查表。内容偏实操代码基于 STM32 HAL 库你可以直接抄作业。2. MR25H40CDF 与 STM32F091RC 方案整体设计2.1 MRAM 到底和 Flash、EEPROM 有什么区别很多人第一次接触 MRAM 会把它当成一种更快的 Flash其实两者的存储机理完全不同。Flash 和 EEPROM 都是基于浮栅电荷存储写入时需要先加高压把电荷注入或抽离浮栅擦除时又要反向操作所以写入慢、寿命有限典型 10 万到 100 万次。MRAM 用的是磁性隧道结MTJ通过改变自由层的磁化方向来存储 0 和 1写入本质上是电流产生的磁场翻转没有电荷注入的过程所以写入速度和读取速度在一个量级而且理论上可以无限次写入。具体到 MR25H40CDF 这颗芯片几个关键参数值得记住参数MR25H40CDF典型 NOR Flash典型 EEPROM容量512KB512KB~16MB2KB~512KB接口SPISPII2C/SPI最高时钟40MHz50~104MHz1~10MHz写入前擦除不需要必须整扇区擦除不需要擦写寿命无限次约 10 万次约 100 万次写入延迟无等待毫秒级毫秒级掉电保持20 年以上20 年10~40 年工作温度-40~85℃视型号视型号从表里能看出来MRAM 的核心优势就是写入不用等、擦写不心疼。工业场景里经常需要每秒记录几十次甚至上百次传感器数据用 EEPROM 的话几个月就把寿命耗光了用 Flash 的话每次写之前还要擦扇区逻辑复杂还容易在擦除过程中掉电导致数据损坏。MRAM 直接按字节写写完就完事掉电也不丢这就是它在工业领域越来越受欢迎的根本原因。2.2 为什么选 STM32F091RC 来搭配STM32F091RC 在 STM32 家族里属于入门偏中端的定位Cortex-M0 内核48MHz 主频128KB Flash32KB SRAM封装 LQFP64。选它搭配 MR25H40CDF 主要考虑几点一是它有两个 SPI 接口SPI1 挂在 APB2 上最高 24MHzSPI2 挂在 APB1 上最高 12MHz可以一个接 MRAM 一个接其他外设互不干扰二是它支持 2.0V~3.6V 供电和 MR25H40CDF 的 1.8V~3.6V 范围有重叠3.3V 系统下直接对接没问题三是它的价格和供货在工业客户里比较稳不像某些高端型号动不动就缺货。这里有个细节要注意STM32F091RC 的 SPI1 最高时钟是 PCLK/2如果系统时钟 48MHz、APB2 不分频那 SPI1 最高就是 24MHz。MR25H40CDF 支持 40MHz所以用 SPI1 跑 24MHz 完全没问题但如果你用 SPI2 就只能跑到 12MHz读写大块数据时速度差异会很明显。我在实际项目里把 MRAM 挂在 SPI1 上SPI2 留给外部 ADC 和显示屏。2.3 整体数据流架构这套方案的数据流其实不复杂但分层要清晰。最底层是 SPI 物理层负责字节收发往上是 MR25H40CDF 的命令层包括写使能、读状态、读数据、写数据、写保护等指令再往上是存储管理层我把它分成三个区域系统参数区、运行日志区、掉电快照区。系统参数区存设备配置写入频率低运行日志区按环形缓冲区方式循环写入频率高掉电快照区在检测到掉电中断时紧急写入关键状态。这样分区的目的是让不同类型的写入互不干扰也方便做磨损均衡虽然 MRAM 不需要但逻辑上分区更清晰和地址管理。MR25H40CDF 的 512KB 地址空间是 0x00000 到 0x7FFFF我用宏定义把三个区域的起止地址固定下来后面所有读写都基于这些宏避免硬编码地址出错。3. 硬件连接与 SPI 时序关键细节3.1 引脚连接与硬件设计要点MR25H40CDF 是 8 引脚 DFN 封装引脚定义很标准VCC、GND、SCK、SI、SO、CS、WP、HOLD。和 STM32F091RC 的连接方式如下MR25H40CDF 引脚STM32F091RC 引脚说明VCC3.3V电源建议加 0.1uF 去耦GNDGND地SCKPA5 (SPI1_SCK)时钟SIPA7 (SPI1_MOSI)主出从入SOPA6 (SPI1_MISO)主入从出CSPA4 (GPIO)片选软件控制WP3.3V写保护不用时拉高HOLD3.3V保持不用时拉高这里有几个硬件上的坑要提前说。第一CS 片选我强烈建议用普通 GPIO 软件控制不要用 SPI 硬件 NSS。原因后面会详细讲简单说就是 MRAM 的指令序列对 CS 的时序要求很严格硬件 NSS 在多字节传输时容易提前拉高或者时序对不齐。第二WP 和 HOLD 引脚如果不使用一定要拉到 VCC悬空的话芯片可能进入未定义状态读出来的数据全是 0xFF 或者随机值。第三SCK 走线尽量短如果 MRAM 离主控超过 5cm建议在 SCK 上串一个 22 到 33 欧姆的电阻做阻抗匹配否则高速下波形振铃会导致读写错误。3.2 SPI 模式选择与时钟配置MR25H40CDF 支持 SPI Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1这两种模式在时钟极性和相位上是对称的。我一般用 Mode 0因为 STM32 HAL 库默认配置就是 Mode 0省得改。时钟配置上SPI1 挂在 APB2如果系统时钟 48MHz、APB2 不分频那 SPI1 的输入时钟就是 48MHz预分频设为 2 得到 24MHz 的 SCK。这里有个计算过程要说明MR25H40CDF 的数据手册规定 SCK 最高 40MHz但这是理想条件下的极限值。实际 PCB 上如果走线长、有干扰24MHz 可能都不稳。我的经验是先用 12MHz 调通确认读写正常后再逐步提高到 24MHz如果出现误码就降回 12MHz 或者加匹配电阻。STM32F091RC 的 SPI1 在 24MHz 下配合短走线小于 3cm实测很稳长走线建议降到 12MHz。配置代码大概是这样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_2; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE;注意NSS设为SPI_NSS_SOFT这样 HAL 库不会去控制硬件 NSS 引脚我们自己用 GPIO 控制 CS。3.3 CS 片选的时序陷阱这是我在调试中踩得最狠的一个坑。MR25H40CDF 的指令序列对 CS 的建立时间和保持时间有明确要求CS 拉低到第一个 SCK 上升沿之间至少要有 5ns 的建立时间最后一个 SCK 下降沿到 CS 拉高之间至少要有 5ns 的保持时间。在 24MHz 下一个时钟周期约 41ns5ns 看起来很小但如果你的 GPIO 操作和 SPI 传输之间没有加延时或者 HAL 库的函数调用开销导致时序错乱就会出现指令识别错误。我的做法是在 CS 拉低后加一个__NOP()或者几个空指令做微延时在 SPI 传输完成后再加同样的延时才拉高 CS。具体代码#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) void MRAM_CS_Select(void) { MRAM_CS_LOW(); __NOP(); __NOP(); __NOP(); __NOP(); } void MRAM_CS_Release(void) { __NOP(); __NOP(); __NOP(); __NOP(); MRAM_CS_HIGH(); }别小看这几个__NOP()在 48MHz 主频下每个 NOP 约 20ns四个就是 80ns足够满足建立和保持时间要求。如果你用更高主频的 MCUNOP 数量要相应增加。4. MRAM 驱动实现与读写操作全流程4.1 MR25H40CDF 指令集解析MR25H40CDF 的指令集和标准 SPI Flash 很像但有几个关键区别。常用指令如下指令名称指令码功能说明WREN0x06写使能写操作前必须发送WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据任意地址WRITE0x02写数据任意地址RDID0x9F读设备 ID和 Flash 最大的区别是MRAM 没有扇区擦除指令WRITE 指令直接写目标地址不需要先擦除。这是它用起来最爽的地方。但要注意WRITE 之前必须发 WREN 写使能而且 WREN 之后如果 CS 拉高写使能会自动清除所以每次写操作都要重新发 WREN。读设备 ID 是验证硬件连接是否正常的第一步。发送 0x9F 后MR25H40CDF 会返回 4 个字节制造商 ID0xEverspin 对应 0x18、存储类型、容量代码、以及一个保留字节。如果读出来全是 0x00 或 0xFF说明 SPI 通信有问题先查硬件连接和 CS 时序。4.2 底层字节收发函数HAL 库提供了HAL_SPI_Transmit和HAL_SPI_Receive但在 MRAM 驱动里我更喜欢用HAL_SPI_TransmitReceive做全双工收发因为 SPI 本质上是全双工的发一个字节的同时会收到一个字节。底层封装uint8_t MRAM_SPI_Transfer(uint8_t txData) { uint8_t rxData 0; HAL_SPI_TransmitReceive(hspi1, txData, rxData, 1, 100); return rxData; }这个函数是后面所有读写操作的基础。注意超时参数我设了 100ms实际单字节传输在 24MHz 下不到 1us100ms 是防止死等的保险值。4.3 读数据完整流程读数据的流程是拉低 CS - 发送 READ 指令0x03- 发送 3 字节地址MR25H40CDF 是 19 位地址但用 3 字节传输高 5 位忽略- 连续读取 N 字节 - 拉高 CS。代码如下void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_Select(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] MRAM_SPI_Transfer(0x00); } MRAM_CS_Release(); }这里有个效率问题每读一个字节都调用一次HAL_SPI_TransmitReceive函数调用开销很大。如果要读大块数据比如 1KB建议用HAL_SPI_Receive直接接收或者用 DMA。我实测读 1KB 数据逐字节方式耗时约 800us用 DMA 可以降到 350us 左右。对于工业日志记录这种场景逐字节够用但如果要高速采集DMA 是必须的。4.4 写数据完整流程写数据比读多一步 WREN。流程是拉低 CS - 发送 WREN0x06- 拉高 CS - 拉低 CS - 发送 WRITE0x02- 发送 3 字节地址 - 连续写入 N 字节 - 拉高 CS。注意 WREN 和 WRITE 之间必须拉高一次 CS这是 MRAM 的协议要求。void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_Select(); MRAM_SPI_Transfer(0x06); MRAM_CS_Release(); MRAM_CS_Select(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint32_t i 0; i len; i) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_Release(); }写完之后建议读状态寄存器确认写入完成。虽然 MRAM 写入没有等待周期但读一下状态位更保险尤其是在高干扰环境下。4.5 状态寄存器与写保护配置MR25H40CDF 的状态寄存器有 8 位其中比较重要的是块保护位BP0、BP1和写使能锁存位WEL。块保护可以保护部分地址区域不被写入适合存放关键参数。WEL 位在 WREN 后置 1写入完成后自动清 0。读状态寄存器uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_Select(); MRAM_SPI_Transfer(0x05); status MRAM_SPI_Transfer(0x00); MRAM_CS_Release(); return status; }如果你要保护系统参数区可以在初始化时配置块保护位把参数区所在的地址块设为只读。这样即使程序跑飞误写参数也不会丢。解除保护需要先发 WREN 再写状态寄存器流程和写数据类似。5. 数据管理策略与掉电保护实战5.1 存储区域划分与地址规划512KB 的 MRAM 空间我按下面的方式划分区域名称起始地址结束地址大小用途系统参数区0x000000x00FFF4KB设备配置、校准参数运行日志区0x010000x3FFFF252KB环形日志缓冲掉电快照区0x400000x40FFF4KB掉电瞬间状态预留扩展区0x410000x7FFFF252KB后续功能扩展系统参数区用块保护设为只读只有进入配置模式才解除保护。运行日志区用环形缓冲区写指针到达末尾后回到起始地址覆盖最旧数据。掉电快照区每次掉电时写入上电后读取并处理。5.2 环形日志缓冲区的实现环形缓冲区是工业数据记录的核心。我定义一个结构体管理读写指针typedef struct { uint32_t writeAddr; uint32_t startAddr; uint32_t endAddr; uint32_t count; } RingBuffer_t; RingBuffer_t logBuf { .writeAddr 0x01000, .startAddr 0x01000, .endAddr 0x3FFFF, .count 0 };每次写日志时先判断剩余空间是否够一条记录不够就回绕到起始地址。每条记录包含时间戳、数据类型、数据长度和校验和。校验和用简单的累加和或者 CRC16防止读出来的是半截数据。这里有个经验日志记录不要定长用变长格式更省空间。但变长格式读取时要知道每条记录的长度所以记录头里必须包含长度字段。我用的格式是2 字节时间戳高位、2 字节时间戳低位、1 字节类型、1 字节长度、N 字节数据、2 字节 CRC16。5.3 掉电检测与紧急写入掉电保护是这套方案的重点。STM32F091RC 没有内置的掉电检测比较器我用的是外部电压监测芯片当主电源降到阈值以下时产生中断MCU 在中断里把关键状态写入 MRAM 的掉电快照区。因为 MRAM 写入不需要等待从检测到掉电到写完数据整个过程可以在几百微秒内完成配合一个大电容比如 470uF维持供电足够写完几KB数据。掉电中断服务函数里要注意不要调用复杂的库函数直接操作寄存器或者用最精简的写函数。我专门写了一个MRAM_WriteFast函数去掉了所有参数检查和状态读取只做最核心的 CS 控制和字节收发确保在最短时间内完成写入。void MRAM_WriteFast(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); __NOP(); __NOP(); MRAM_SPI_Transfer(0x06); MRAM_CS_HIGH(); __NOP(); __NOP(); MRAM_CS_LOW(); __NOP(); __NOP(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint32_t i 0; i len; i) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); }实测从进入中断到写完 256 字节耗时约 180us470uF 电容在 3.3V 下能维持几十毫秒余量非常充足。5.4 数据校验与恢复机制MRAM 虽然可靠性高但工业现场电磁干扰强SPI 传输过程中仍可能出现位翻转。我在每条记录后面加 CRC16 校验读取时先校验再使用。如果校验失败标记该条记录为无效跳过继续读下一条。对于系统参数区我存了两份互为备份的参数读取时两份都校验哪份有效用哪份两份都无效则加载默认参数。CRC16 我用的是 CCITT 多项式 0x1021查表法实现速度快。校验范围是记录头加数据体不包括 CRC 本身。6. 调试过程中遇到的典型问题与排查6.1 读出来全是 0xFF 或 0x00这是最常见的问题原因通常有三个一是 CS 没有正确拉低检查 GPIO 配置和接线二是 SPI 模式不对MR25H40CDF 只支持 Mode 0 和 Mode 3如果配成 Mode 1 或 Mode 2 就读不到数据三是 WP 或 HOLD 引脚悬空芯片处于保护或保持状态。排查顺序先用示波器看 CS、SCK、MOSI 波形确认指令发出去了然后读设备 ID如果 ID 正确说明通信正常问题在地址或指令如果 ID 不对查模式和引脚。6.2 写入后读出来数据不一致这种情况多半是 CS 时序问题。MRAM 的 WRITE 指令要求 CS 在最后一个字节传输完成后才能拉高如果提前拉高最后一个字节可能没写进去。另外 WREN 和 WRITE 之间必须拉高 CS如果忘了这一步写操作会被忽略。我遇到过因为HAL_SPI_Transmit返回后立即拉高 CS但实际最后一个字节还在移位寄存器里没发完导致数据错位。解决办法是在拉高 CS 前加延时或者用HAL_SPI_GetState确认传输完成。6.3 高速下读写误码率升高24MHz 下如果出现偶发误码先降速到 12MHz 测试。如果 12MHz 正常说明是信号完整性问题。检查 SCK 走线是否过长、是否有过孔、是否靠近干扰源。可以在 SCK 上串 22 欧姆电阻或者在 MRAM 的 VCC 引脚就近加 0.1uF 和 1uF 电容。另外 STM32 的 SPI 输出驱动能力可以配置如果走线长把 GPIO 速度设为 HIGH 或 VERY_HIGH。6.4 掉电后数据丢失如果掉电快照区数据丢失检查掉电检测阈值是否设置合理。阈值太高会在电源还没完全掉下来时就触发写入过程中断电阈值太低则电容电量不够写完。我的经验是阈值设在 2.9V 左右配合 470uF 电容在 3.3V 系统下能保证至少 20ms 的供电余量。另外要确认掉电中断的优先级足够高不会被其他中断打断。6.5 常见问题速查表现象可能原因排查方法解决措施读 ID 全 0xFFCS 未拉低示波器看 CS 波形检查 GPIO 配置读 ID 全 0x00MISO 未接或模式错检查接线和 SPI 模式改为 Mode 0写入无效缺少 WREN逻辑分析仪看指令序列写前加 WREN数据错位CS 提前拉高看 CS 和 SCK 时序加延时或查传输状态高速误码信号完整性差降速测试加匹配电阻、缩短走线掉电丢数据电容不足或阈值高测掉电时 VCC 波形加大电容、调低阈值7. 性能实测与优化建议7.1 读写速度实测数据我在 24MHz SPI 时钟下做了实测结果如下操作数据量耗时等效速率单字节读1B约 2us0.5MB/s单字节写1B约 3us0.33MB/s连续读1KB约 800us1.28MB/s连续写1KB约 850us1.2MB/sDMA 连续读1KB约 350us2.9MB/sDMA 连续写1KB约 380us2.7MB/s单字节操作因为函数调用开销大速率上不去。连续读写用 DMA 后速率提升明显。如果你的项目对速度要求高建议用 DMA 加双缓冲一边接收一边处理。7.2 降低功耗的几种做法工业设备很多是电池供电或者低功耗要求MRAM 本身功耗不高但 SPI 高速传输时电流会上去。几个降功耗的做法一是降低 SPI 时钟12MHz 下功耗比 24MHz 低约 30%二是写完后让 MRAM 进入低功耗模式MR25H40CDF 支持通过特定指令进入休眠三是用 STM32 的 SPI 空闲时关闭时钟需要时再开。实测在 12MHz、间歇读写模式下整机平均电流可以控制在 5mA 以内。7.3 代码分层与可移植性最后说一下代码组织。我把 MRAM 驱动分成三层硬件抽象层SPI 收发、CS 控制、指令层WREN、READ、WRITE 等、应用层日志管理、参数管理。这样换 MCU 或者换 MRAM 型号时只需要改硬件抽象层上层代码不用动。比如从 STM32F091RC 换到 STM32F103SPI 配置变了但指令层和应用层完全复用。另外建议把 MRAM 的地址映射做成宏定义或者配置文件不要硬编码在代码里。我吃过这个亏项目后期要调整分区大小硬编码地址改得满屏都是容易漏改。用宏定义后改一个地方全局生效。这套 MR25H40CDF 加 STM32F091RC 的方案我已经在三个工业项目里落地最长的跑了两年多日志区循环写了几千万次没有出现数据丢失或芯片损坏。MRAM 的无限擦写特性在频繁记录场景下确实省心不用再算寿命、做磨损均衡。如果你也在选工业存储方案建议拿这颗芯片搭个最小系统板先试试读设备 ID、读写几个字节感受一下它的响应速度再决定要不要用在正式项目里。