STM32F746ZG 与 MR25H40CDF MRAM 工业掉电保护存储方案

发布时间:2026/10/5 20:47:16
STM32F746ZG 与 MR25H40CDF MRAM 工业掉电保护存储方案 1. 为什么工业现场还在用一颗 4Mbit 的 MRAM第一次拿到 MR25H40CDF 这颗料的时候我心里是有点犯嘀咕的4Mbit 的容量放在今天动辄几百 MB 的存储环境里实在不起眼。但真正把它焊到板子上、跑完一轮掉电测试之后我才明白为什么做工业采集和嵌入式控制的人会专门挑它——它解决的不是存得多的问题而是存得稳、写得快、掉电不丢的问题。MR25H40CDF 是 Everspin 的 SPI 接口磁阻随机存储器MRAM容量 512KB4Mbit支持最高 40MHz 的 SPI 时钟。它和常见的 SPI NOR Flash、EEPROM 最大的区别在于写入不需要擦除、没有擦写寿命焦虑、写入延迟是纳秒级而不是毫秒级。这三点在工业场景里几乎是决定性的。我举个真实场景。做过电力监测终端的朋友应该熟悉这种需求设备每 100ms 采样一次三相电压电流需要把最近若干条记录缓存下来一旦主电源掉电靠后备电容撑住最后几十毫秒把关键数据写进非易失存储。如果用 EEPROM一次页写要 5ms 左右电容根本撑不住如果用 NOR Flash还得先擦扇区更慢。而 MR25H40CDF 的字节写入几乎是即时的SPI 时钟推完数据就落盘这才是它真正的价值所在。STM32F746ZG 这边则是另一类典型需求。这颗 Cortex-M7 主频 216MHz带 1MB Flash、320KB SRAM还有一堆外设做工业 HMI、运动控制、数据网关都很合适。它的 SPI 外设成熟稳定配合 HAL 库或者寄存器操作都能跑得很顺。把这两颗料凑到一起本质上是在搭一个高速主控 高可靠非易失存储的组合用来应对工业现场那些数据不能丢、响应不能慢的场合。这篇内容我打算按实际项目推进的顺序来讲先搞清楚 MR25H40CDF 到底该怎么用、和 Flash 的差异在哪再落到 STM32F746ZG 上的硬件连接和 SPI 配置然后是读写驱动的实现细节最后重点聊掉电保护、时序余量和几个我踩过的坑。适合正在做工业数据采集、嵌入式存储选型或者单纯想搞明白 MRAM 怎么用的朋友。2. MR25H40CDF 的脾气和 SPI Flash 到底差在哪2.1 存储机理决定了它的行为方式MR25H40CDF 用的是磁性隧道结MTJ存储单元数据靠磁化方向保存而不是靠电荷。这个物理层面的差异直接导致了几个使用上的不同写入即生效不需要先擦除再写直接覆盖写就行。Flash 那种写之前必须擦整个扇区的规矩在这里不存在。寿命几乎不用管官方标称读写次数是 10^14 量级实际工程中按 10^12 保守估计也够用几十年而 NOR Flash 通常只有 10 万次擦写。写入速度快单字节写入在 SPI 时钟推完后基本就完成了没有内部编程等待周期。Flash 写完要轮询状态寄存器等它忙完。掉电即保存数据写入完成的那一刻就已经是非易失状态不需要额外的保存动作。这几点听起来很美好但也不是没有代价。MRAM 的容量密度低、单位成本高所以它适合做关键数据的小容量高速缓存而不是拿来存固件或者大块日志。我一般会这样分工MR25H40CDF 存掉电必须保住的关键参数和最近记录大块历史数据还是走 Flash 或者 SD 卡。2.2 指令集和操作模式MR25H40CDF 的 SPI 指令集和标准 SPI Flash 很像但精简了不少。常用的几条指令编码作用WREN0x06写使能写之前必须先发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据注意这里没有擦除指令也没有页编程的概念——WRITE 指令后面跟 24 位地址然后连续推数据地址会自动递增跨页也不会出问题这点比 Flash 省心Flash 跨页写会回卷到页首是个经典坑。状态寄存器里主要关注 WEL写使能锁存和 BP0/BP1块保护位。上电后 WEL 是 0任何写操作前都要先发 WREN 把 WEL 置 1写完自动清零。这个机制和 Flash 一致但因为没有擦除流程简单很多。2.3 速度与时序余量MR25H40CDF 支持的最高 SPI 时钟是 40MHz。在 STM32F746ZG 上SPI1 挂在 APB2 上最高可以跑到 108MHz分频后取 27MHz 或者 13.5MHz 都很稳。我实测下来27MHz 下连续读写 512KB 全片读大概 150ms 左右写大概 160ms这个速度在工业缓存场景里完全够用。但这里有个容易被忽略的点MRAM 的写入速度虽然快但对 SPI 时序的建立/保持时间要求并不宽松。如果你把时钟拉得太高、PCB 走线又长容易出现写入数据错位。我在一块走线比较随意的板子上试过 40MHz读没问题写偶尔出错降到 27MHz 就完全稳定了。所以我的建议是除非 PCB 做得很讲究否则 20~27MHz 是性价比最高的档位。提示MRAM 写入出错往往不会立刻报错而是数据悄悄写错。所以调试阶段一定要做写-读-比对的闭环验证不能只看写操作有没有返回。3. 硬件连接STM32F746ZG 与 MR25H40CDF 怎么接3.1 引脚分配与片选策略MR25H40CDF 是标准 4 线 SPI 器件SCK、MOSI、MISO、CS加上 VDD、VSS一共 8 个引脚DFN 封装。接到 STM32F746ZG 上我一般用 SPI1因为它的时钟能力最强引脚也好布线MR25H40CDFSTM32F746ZG说明SCKPA5 (SPI1_SCK)时钟MOSIPA7 (SPI1_MOSI)主出从入MISOPA6 (SPI1_MISO)主入从出CSPA4 (GPIO)软件片选VDD3.3V供电VSSGND地关于片选这里有个值得展开说的选择用硬件 NSS 还是软件 GPIO 片选。STM32 的 SPI 硬件 NSS 在多主或者需要严格时序的场合有用但在单主单从的场景下我强烈建议用软件 GPIO 控制片选。原因有两个一是硬件 NSS 的行为受 SSM/SSI 位控制配置起来绕二是软件片选可以精确控制 CS 拉低到第一个时钟沿之间的间隔方便调试时序问题。3.2 去耦和布线上的实际讲究MRAM 对电源噪声比普通 Flash 敏感一些因为它的读写是靠电流驱动磁隧道结的。我在 VDD 引脚旁边放了 100nF 加 1uF 的组合去耦位置尽量贴近引脚。如果板子上有电机或者继电器这类大电流负载最好再串一个磁珠隔离。布线方面SCK 是最需要照顾的信号。它是唯一持续翻转的时钟线边沿陡、频率高容易串扰到 MISO 上。我的做法是SCK 和 MISO 之间尽量拉开距离中间走一根地线做隔离如果实在拉不开就把 SCK 的走线尽量短并且避免和 MISO 长距离平行。还有一点CS 线不要和 SCK 平行走太长。CS 的边沿如果被 SCK 耦合出毛刺可能被 MRAM 误判成一次新的命令起始导致数据错乱。这个坑我在早期项目里踩过现象是偶尔读出来的数据整体偏移一个字节查了很久才发现是 CS 串扰。3.3 上电时序与复位MR25H40CDF 没有专门的复位引脚靠上电复位。数据手册里要求 VDD 上升时间不能太慢一般建议在几十毫秒以内。如果电源上升很慢比如用了大电容缓启动芯片可能进入不确定状态。稳妥的做法是在初始化时先发一次 WRDI写禁止再读一次状态寄存器确认通信正常然后再开始正常操作。STM32F746ZG 这边SPI 初始化之前要先把 CS 拉高确保 MRAM 处于空闲状态。我见过有人初始化顺序搞反先配 SPI 再配 GPIO结果 SPI 配置过程中 CS 是浮空的MRAM 收到一堆乱时钟虽然不至于损坏但状态可能异常。4. 在 STM32F746ZG 上把 SPI 驱动跑通4.1 CubeMX 配置的关键几项用 STM32CubeMX 配 SPI1几个参数要盯紧ModeFull-Duplex MasterData Size8 BitsClock Polarity (CPOL)LowClock Phase (CPHA)1 EdgeNSSSoftware因为我们用 GPIO 片选Baud Rate Prescaler根据 APB2 时钟算目标是 20~27MHzFirst BitMSB FirstCPOL/CPHA 这个组合对应 SPI Mode 0是 MR25H40CDF 支持的模式。数据手册里明确写了它支持 Mode 0 和 Mode 3我一般用 Mode 0因为大多数 SPI Flash 也是 Mode 0代码可以复用。波特率这块算一下STM32F746ZG 的 APB2 默认 108MHzSPI1 的时钟源就是它。分频系数选 4 得到 27MHz选 8 得到 13.5MHz。我通常先用 8 分频调通再往上提到 4 分频。4.2 底层读写函数的写法HAL 库提供了HAL_SPI_Transmit和HAL_SPI_Receive但直接用来操作 MRAM 有点别扭因为每次操作都要手动控制 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_WriteEnable(void) { uint8_t cmd 0x06; 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; }这里有个细节HAL_SPI_Transmit和HAL_SPI_Receive之间 CS 不能拉高否则命令就断了。所以整个发命令 读数据的过程必须在一次 CS 拉低期间完成。上面这个写法是对的但如果你用 DMA 或者中断方式就要注意别让 CS 在传输中途被别的任务拉高。4.3 连续读写的实现读操作比较直接发 0x03跟 24 位地址然后连续读。写操作要先发 WREN再发 0x02 和地址然后推数据。void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_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(); } void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_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(); }注意地址是 24 位的因为 512KB 需要 19 位地址用 3 字节表示刚好。这里HAL_SPI_Transmit的 timeout 参数我给了 1000ms实际传输远小于这个值给宽一点是防止在调试时因为断点导致超时误报。4.4 用 DMA 提升大块传输效率如果要一次性读写几 KB 的数据用阻塞式传输会占着 CPU 空转。STM32F746ZG 的 SPI1 支持 DMA可以配两个 DMA 流一个 TX、一个 RX。配置的时候注意 DMA 的优先级和 SPI 的 TX/RX 请求要对应上。用 DMA 的时候CS 的控制要特别小心。我的做法是在启动 DMA 传输前拉低 CS在 DMA 传输完成中断里拉高 CS。如果中途有更高优先级的任务打断要确保它不会去碰同一个 SPI 外设。简单起见我给 MRAM 的访问加了一个互斥标志同一时刻只允许一个任务操作。5. 掉电保护MRAM 真正发挥价值的地方5.1 为什么掉电保护是这类项目的核心工业设备最怕的不是运行中出错而是掉电瞬间数据处于半写状态。Flash 和 EEPROM 在写入过程中掉电可能导致整个扇区数据损坏甚至把相邻数据也带坏。MRAM 因为写入是原子性的单个存储单元的翻转是瞬时的掉电时最多是这一字节没写完不会波及别的数据。但原子性不等于你什么都不用做。如果你的应用逻辑是先写数据 A再写校验 B掉电发生在两者之间数据 A 是新的、校验 B 是旧的读出来就矛盾了。所以还是需要一套软件层面的保护机制。5.2 双区备份加序号标记的方案我在项目里常用的做法是把关键数据存两份每份带一个递增的序号和 CRCtypedef struct { uint32_t seq; // 序号每次写入递增 uint32_t crc; // 数据 CRC32 uint8_t data[60]; // 实际数据 } Record_t; // 共 68 字节对齐到 72 字节存储布局是区 A 在地址 0x00000区 B 在地址 0x01000。写入时总是写序号较大的那个区的另一区写完更新序号。读取时比较两区的序号和 CRC取序号大且 CRC 正确的那个。这个方案的好处是任何时刻掉电至少有一个区的数据是完整且 CRC 正确的。因为写入是先写数据区再写序号如果掉电发生在写数据过程中那个区的 CRC 会失败读取时自动回退到另一个区。5.3 后备电容与写入窗口的计算光有软件机制还不够得保证掉电后有时间把数据写完。这就涉及后备电容的容量计算。假设系统正常工作电流 200mA掉电后需要维持 50ms 完成一次 72 字节的写入。MRAM 写入 72 字节在 27MHz 下大约需要 25us加上检测掉电、切换电源的延迟留 5ms 余量足够。那么电容需要提供的能量Q I × t 0.2A × 0.05s 0.01C如果电容从 5V 放到 3.0VLDO 最低工作电压可用电压差 2VC Q / ΔV 0.01 / 2 5000uF这个容量不小实际选型时我会用两颗 2200uF 并联再加上掉电检测电路尽早触发。如果能把工作电流降下来比如掉电后立刻关掉屏幕背光、停掉电机驱动电容可以小很多。注意掉电检测的阈值要设在 LDO 输出还能稳定工作的电压之上留出足够的余量。我一般设在 4.5V 触发这样从 4.5V 掉到 3.0V 有 1.5V 的窗口可用。5.4 写入顺序的讲究前面提到先写数据再写序号这个顺序不能反。如果先写序号再写数据掉电后序号是新的但数据是旧的CRC 校验会失败反而丢掉了本来完好的旧数据。正确的顺序永远是先写内容再写提交标记序号或标志位。这个思路和数据库的 WAL预写日志是一个道理核心就是让提交动作成为原子性的最后一步。6. 调试过程中踩过的几个坑6.1 读出来全是 0xFF 或 0x00第一次调通的时候读出来全是 0xFF我以为是芯片没焊好。后来发现是 MISO 配置问题STM32 的 SPI 在 Master 模式下如果 MISO 引脚没有正确配置成复用功能读回来就是浮空状态表现为全 1 或者随机值。排查顺序建议这样先用示波器看 SCK 有没有波形再看 MOSI 上有没有正确的命令字节最后看 MISO 在 CS 拉低期间有没有被从机驱动。如果 MISO 一直是高阻那就是从机没响应检查供电和 CS。6.2 写入后读出来数据错位这个就是我前面提到的 CS 串扰问题。现象是写入 100 字节读出来前几个字节对后面整体偏移。用逻辑分析仪抓波形发现 CS 在传输过程中有毛刺。解决办法是把 CS 走线改短、远离 SCK并且在 CS 上加了一个 100pF 的小电容滤毛刺。6.3 高速下偶发写入失败27MHz 以上开始出现偶发写入错误读没问题。这个通常是建立/保持时间不够。STM32 的 SPI 可以配置时钟相位和极性也可以调整输出速度。我把 GPIO 的输出速度从 Very High 降到 High边沿变缓了一点反而稳定了。另外在 SCK 上串了一个 22 欧姆的电阻抑制过冲也有帮助。6.4 多任务环境下的访问冲突项目里如果有 RTOS多个任务都可能访问 MRAM必须加互斥。我一开始用了一个全局标志结果发现标志检查和 CS 拉低之间可能被任务切换打断导致两个任务同时操作 SPI。后来改成用 RTOS 的互斥量Mutex把整个CS 拉低到 CS 拉高的过程保护起来问题就没了。7. 几个提升可靠性的工程习惯7.1 上电自检不能省每次上电我会做一次完整的自检读状态寄存器确认通信正常然后往一个测试地址写一个模式比如 0x5A5A读回来比对再写反码 0xA5A5 比对。两次都通过才认为 MRAM 可用。这个自检花不了几毫秒但能提前发现焊接不良或者芯片损坏。7.2 关键数据加 CRC 而不是简单校验和校验和Checksum实现简单但对多位翻转的检出能力弱。CRC32 虽然多花一点 CPU但在 STM32F746ZG 上可以用硬件 CRC 外设几乎不占时间。我所有存进 MRAM 的关键结构体都带 CRC32读取时先校验再用。7.3 地址空间规划要留余量512KB 看着不大但规划不好也会紧张。我的习惯是前 64KB 放系统参数和配置中间 256KB 放环形记录缓冲区最后留 192KB 给未来的扩展和坏块预留虽然 MRAM 基本不会坏块但留余量是好习惯。地址规划写进头文件用宏定义避免代码里到处是魔法数字。7.4 写入频率和数据寿命的权衡虽然 MRAM 寿命极长但也没必要每毫秒都写。我会根据业务需求设定写入周期比如状态数据 1 秒写一次事件记录触发式写入。这样既减少 SPI 占用也降低掉电时正在写入的概率。8. 关于这套组合的适用边界MR25H40CDF 加 STM32F746ZG 这套组合我用了几个项目下来感受是它特别适合数据量不大但可靠性要求极高、写入频繁、掉电不能丢的场景。比如工业仪表的参数存储、运动控制器的位置缓存、电力终端的故障录波索引。但它不适合拿来存固件或者大块日志。512KB 的容量存个配置和几千条记录就到头了。如果项目需要存几 MB 的历史数据还是得配一颗 SPI NOR Flash 或者 eMMC让 MRAM 专注做它擅长的事。另外成本上要有心理准备MRAM 的单位容量价格比 Flash 高不少。选型的时候要算清楚到底有多少数据是掉电必须保住的。如果只有几百字节的关键参数用一颗小容量 MRAM 就够了没必要上大容量。最后分享一个我在实际项目里养成的习惯每次改完存储相关的代码都要做一次随机掉电测试。用继电器或者 MOS 管控制电源随机时刻切断反复几百次然后检查数据完整性。这个测试能暴露很多平时看不出来的时序问题比任何静态代码审查都管用。我有个项目就是靠这个测试发现了一个写入窗口刚好卡在掉电检测延迟之后的边界问题改完之后再没出过数据丢失的投诉。