
最近在调一块工业现场用的数据记录模块主控选了 STM32F373RC存储芯片用了一片 MR25H40CDF。很多朋友看到这颗料第一反应是“这不就是 SPI Flash 嘛”引脚排布、命令集确实像但它骨子里是完全不同的东西。MR25H40CDF 是 Everspin 的 4Mbit 串行 MRAM也就是磁阻 RAM非易失、按字节随机写、没有擦除周期、理论上无限次写这些特性让它特别适合做工业场景下的“掉电不丢的全局数组”。配合 STM32F373RC 这种带 FPU、带 CORDIC、带多路高精度 ADC 的 M4F 内核 MCU一套既能频繁采集、又能随时保存关键数据的嵌入式存储方案就这么搭起来了。这篇文章我把从选型、硬件接线、SPI 驱动、应用层可靠性设计到实测踩坑的完整过程都整理出来适合正在做工业控制器、仪器仪表、计量设备、电机驱动或者任何“需要边运行边保存数据”的朋友。如果你之前只用过 W25Q64 这类 Flash或者正纠结要不要用 MRAM 替代 EEPROM/FRAM这篇可以给你一个完整的参考。1. 方案选型为什么要选MRAM串在F373旁边1.1 MR25H40CDF 的本质不丢数据的RAM先把这个芯片的“脾气”说清楚。MR25H40CDF 内部是磁隧道结存储单元不是像 Flash 那样用浮栅电荷来存数据。所以它没有“先擦除再写入”的概念写操作直接改变存储状态数据保持靠磁性而不是电荷。官方标称数据保持期在 20 年以上读写在器件寿命内可以认为是无限次工业级型号工作温度覆盖 -40℃ 到 85℃。这些特性放到产品里意味着什么我用三句话总结写入前不需要擦除也没有页编程等待时间写一个字节和写一个页都只是 SPI 传输时间。没有磨损均衡问题不用像管理 Flash 那样做搬移、坏块管理、寿命统计。掉电瞬间的数据是“落盘”的写了一半掉电顶多坏一个字节不会出现整个扇区失效。如果拿它和常见的存储介质做对比选型逻辑会非常清楚对比项MR25H40CDF (MRAM)W25Q64 (SPI NOR Flash)24C256 (EEPROM)容量4Mbit / 512KB64Mbit / 8MB256Kbit / 32KB写入前擦除不需要需要按扇区擦除不需要单字节随机写支持不支持按页写支持写后等待无有页编程通常耗时 ms 级有5ms 左右写次数寿命无限次约 10 万次约 100 万次掉电保持20 年以上20 年以上40 年以上接口速率SPI 最高 40MHzSPI 最高 133MHzI2C 最高 1MHz单看容量和接口速率MRAM 比不过大容量 Flash但工业产品里大量场景并不需要超大容量需要的是“频繁、快速、可靠地把小数据存下来”。MR25H40CDF 的存在意义就是把这个场景做到极致。1.2 STM32F373RC 在这里扮演什么角色STM32F373RC 是 ST 的 M4F 内核 MCU主频 72MHz带硬件 FPU内部有 256KB Flash 和 48KB SRAM。这颗料在同系列里比较特别的地方是它的模拟外设3 个 ADC其中一个是 16 位 Sigma-Delta 型还带 PGA、比较器和 12 位 DAC另外还有 CORDIC 硬件运算单元。用在工业采集、传感器信号调理、电机控制这类场景一颗芯片能把采集、计算、控制、数据存储全包了。回到“存储和读取数据”这件事上F373 的 SPI 外设完全够用。SPI1 挂在 APB2 上可配置为最高 18MHz 或 36MHz 的时钟取决于分频配置支持 DMA 传输做连续读 512KB 数据时不会把 CPU 拖死。F373 还有可编程电压检测器 PVD掉电前能进中断保存关键状态这在第 4 章会细说。这套方案的本质是把 MRAM 当成一个挂在 SPI 总线上的“非易失全局变量池”。F373 负责现场采集和业务逻辑MR25H40CDF 负责把必须保存的东西全部兜住。1.3 什么场景不该照抄这套方案选型最忌讳无脑照搬。以下几种情况MR25H40CDF 就不合适需要几百 MB 甚至更大存储比如音频、视频、历史曲线大缓冲直接上 SPI NAND 或 eMMC。成本极其敏感、写频率很低、只需要存配置参数的产品普通 EEPROM 足够。需要按字节随机访问且对访问延迟极低可以考虑并口 MRAM 或者给 SPI MRAM 配 SRAM 缓存。想把应用程序直接放在 MRAM 里 XIP 执行串行 MRAM 不支持这活得交给并行 NOR 或主 Flash。MRAM 的核心价值是“可靠地频繁写小数据”抓住这个定位去选型方案才经得起推敲。2. 硬件接线与信号完整性设计2.1 八脚芯片的每个引脚都别闲放着MR25H40CDF 是标准 8 脚 SOP 封装引脚定义和普通 SPI Flash 几乎一样但有两个引脚必须特别对待否则调试时会被整得很难受。先看引脚定义引脚号名称方向接法说明1/CS输入片选接 MCU 的 GPIO低有效2SO输出SPI MISO接 MCU 的 MISO3/WP输入写保护不用时必须上拉到 VCC4GND电源接地5SI输入SPI MOSI接 MCU 的 MOSI6SCK输入时钟接 MCU 的 SCK7/HOLD输入暂停输入不用时必须上拉到 VCC8VCC电源2.7V~3.6V就近去耦我用的 F373 引脚映射是 SPI1PA5 接 SCKPA6 接 MISOPA7 接 MOSIPA4 做 /CS。CS 用普通 GPIO 控制最灵活读写前后精确控制时序。这里说两个容易翻车的地方/HOLD 引脚一旦被拉低芯片会暂停对外通信SO 变为高阻内部的地址计数器保持不动。如果你把它悬空现场电磁干扰一打偶发的“读写丢数据”可能查几天都查不出来。所以不用 HOLD 功能就老实接 10k 电阻上拉到 VCC。/WP 引脚拉低时如果状态寄存器的 WPEN 位被置 1整个芯片会进入写保护状态。上电默认 WPEN 是 0不会锁写但保险起见还是上拉到 VCC防止上电瞬间毛刺把芯片锁住。2.2 电平配合、去耦与高频信号的几个注意点MR25H40CDF 的工作电压是 2.7V~3.6V跟 STM32F373 的 3.3V 系统直接相连不需要电平转换。VCC 去耦我习惯用 100nF 陶瓷电容紧贴电源引脚再在板级放一个 10uF 钽电容或者 MLCC这对抑制 SPI 总线开关时引起的电源纹波很有帮助。如果产品用在电机驱动、变频器这类强干扰环境VCC 串联一个磁珠效果更好。信号完整性方面SPI 速率不高时4MHz 左右随便拉线都能跑但工业产品别赌运气。SCK、MOSI、MISO 这些信号线尽量短不要跨分割地。MCU 的 GPIO 输出速率设置成高速模式能减少边沿畸变如果 PCB 走线超过 5cm 或者用杜邦线调试可以在 MOSI、SCK 上串联 22Ω~33Ω 的电阻减小振铃。还有一个小细节MR25H40CDF 同时支持 SPI Mode 0 和 Mode 3也就是 CPOL0/CPHA0 和 CPOL1/CPHA1。我建议统一用 Mode 0。原因很简单Mode 0 下 SCK 空闲为低CS 释放时时钟处于确定状态不容易出现“末位数据被重复锁存”这类边界问题。3. SPI驱动命令集、时序与C代码实现3.1 命令系统与和Flash最大的区别MR25H40CDF 的命令集和普通 25 系列 SPI Flash 很像常用指令就 6 个命令操作码说明WREN0x06写使能拉高 CS 后自动清除WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器写前必须 WRENREAD0x03读数据可连续读WRITE0x02写数据最多 128 字节需按页边界地址是 21 位有效覆盖 512KB 空间0x00000~0x7FFFF发送时要传 3 个字节地址高 3 位会被芯片忽略。这一点很重要体现在代码层面就是“地址越界回绕”。然后是 MRAM 与 Flash 最本质的区别状态寄存器里根本没有 BUSY 位写操作本身不需要擦除也不需要等待页编程完成。很多朋友第一次用这颗芯片时会习惯性地把 W25Q 的驱动拿过来改写完一页就去轮询状态寄存器的 WIP 位结果发现永远读到 0以为芯片坏了。真相是 MRAM 在 CS 拉高的瞬间数据已经进入存储阵列不需要额外的确认流程。省掉这部分等待逻辑驱动代码反而比 Flash 更简单。3.2 基础读写代码我用标准库风格的 C 代码实现了完整驱动核心逻辑如下。先把底层传输封装好#define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET) #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 #define MRAM_PAGE_SIZE 128 #define MRAM_SIZE 0x80000 /* 512KB */ static uint8_t mram_read_byte(void) { uint8_t rx; HAL_SPI_Receive(hspi1, rx, 1, HAL_MAX_DELAY); return rx; } static void mram_write_byte(uint8_t data) { HAL_SPI_Transmit(hspi1, data, 1, HAL_MAX_DELAY); }写使能和状态寄存器操作void mram_write_enable(void) { MRAM_CS_LOW(); mram_write_byte(MRAM_CMD_WREN); MRAM_CS_HIGH(); } void mram_write_status(uint8_t status) { mram_write_enable(); MRAM_CS_LOW(); mram_write_byte(MRAM_CMD_WRSR); mram_write_byte(status); MRAM_CS_HIGH(); } uint8_t mram_read_status(void) { uint8_t status; MRAM_CS_LOW(); mram_write_byte(MRAM_CMD_RDSR); status mram_read_byte(); MRAM_CS_HIGH(); return status; }核心的页写入和连续读取int mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { if (addr len MRAM_SIZE) return -1; MRAM_CS_LOW(); mram_write_byte(MRAM_CMD_READ); mram_write_byte((addr 16) 0xFF); mram_write_byte((addr 8) 0xFF); mram_write_byte(addr 0xFF); HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); MRAM_CS_HIGH(); return 0; } int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { if (addr len MRAM_SIZE) return -1; while (len 0) { uint32_t page_offset addr (MRAM_PAGE_SIZE - 1); uint32_t chunk len; if (chunk (MRAM_PAGE_SIZE - page_offset)) { chunk MRAM_PAGE_SIZE - page_offset; } mram_write_enable(); MRAM_CS_LOW(); mram_write_byte(MRAM_CMD_WRITE); mram_write_byte((addr 16) 0xFF); mram_write_byte((addr 8) 0xFF); mram_write_byte(addr 0xFF); HAL_SPI_Transmit(hspi1, (uint8_t *)buf, chunk, HAL_MAX_DELAY); MRAM_CS_HIGH(); addr chunk; buf chunk; len - chunk; } return 0; }两点说明第一WRITE 命令一次最多写 128 字节而且不能跨页所以循环里先算当前地址到页尾的剩余空间把一次写入限制在页内。页内偏移的计算可以用addr 0x7F因为 128 字节页正好对应地址低 7 位。第二我在 WRITE 之前统一发了 WREN。虽然某些资料提到 WRITE 命令本身不强依赖 WREN但加上没有任何副作用又能保证和 WRSR 的写法一致少记一条分支规则。3.3 让数据飞一会儿SPIDMA的提速思路上面这种阻塞式收发CPU 全程占用传 512KB 数据时浪费太大。实际项目里我建议把 SPI 配成 DMA 模式。发送方向用 DMA 把内存里的数据搬到 SPI-DR接收方向用 DMA 把 SPi-DR 搬到缓冲区。以连续读为例命令和地址阶段仍然用阻塞方式发送进入连续读数据阶段后启动 DMAHAL_SPI_Receive_DMA(hspi1, dst_buf, len); /* 等待 DMA 传输完成回调 */配合 F373 的 DMA 控制器读一整片 MRAM 时 CPU 基本是空闲的可以做 CRC 计算、数据解析或者其他业务逻辑。实测下来DMA 模式能有效降低 SPI 传输对 CPU 的占用率如果你的系统有实时性要求这一步值得做。4. 应用层设计参数保护、日志与掉电恢复4.1 512KB地址空间怎么划分才不乱裸芯片就像一个没有目录的仓库应用层如果不管存储规划产品跑几个月后数据布局就会一团乱。我常用的划分方案是地址范围大小用途0x00000 ~ 0x0FFFF64KB参数区 A关键运行参数0x10000 ~ 0x1FFFF64KB参数区 B参数双备份镜像0x20000 ~ 0x2FFFF64KB运行日志区环形缓冲0x30000 ~ 0x3FFFF64KB事件记录与告警0x40000 ~ 0x7FFFF256KB大块采集数据/OTA 暂存区参数区为什么要留两份这是从工控数据可靠性角度考虑的。MRAM 本身不容易坏但应用层可能因为 bug 写乱、电磁干扰可能导致单字节翻转保留镜像区能在上电时主动比对和恢复。日志区用环形覆盖的方式不断记录时间戳和事件旧数据滚掉。采集数据区留给 ADC 采样结果、曲线数据这类量比较大的内容MRAM 没有擦除寿命限制哪怕每秒写几十个字节也没压力。4.2 参数双备份把“写坏”的后果降到零参数区我用了“双份 CRC 版本号”的方案。每个参数块在 A 区和 B 区各存一份数据结构大致是typedef struct { uint32_t magic; /* 固定标识例如 0xA5A5A5A5 */ uint32_t version; /* 参数版本 */ uint32_t length; /* 数据长度 */ uint8_t body[64]; /* 实际参数 */ uint16_t crc16; /* body 的 CRC 校验 */ } param_block_t;写入顺序固定为“先写备份区再写主区”。这样即使写主区的时候掉电备份区还是旧但完整的数据。上电恢复流程读主区校验 magic、length、crc16主区合法直接用主区主区非法读备份区备份区合法用备份区并把数据回写到主区两区都非法恢复出厂默认参数并产生一条“参数区异常”日志。为什么这个策略在 MRAM 上特别好用因为写两个区不需要擦除不需要等页编程所以切换频率可以很随意。同样的逻辑放在 Flash 上要考虑磨损均衡、要考虑写失败后的拓扑管理复杂度完全不一样。MRAM 把这些成本全部省掉了。4.3 环形日志与PVD掉电写入实战日志系统我建议做成“块头 条目流”的结构。日志区起始处放一个 64 字节的日志头里面保存写指针、覆盖计数、日志版本。写入时从写指针开始追加条目接近区尾则回绕覆盖。每个条目按以下格式组织字段长度说明时间戳4B累计开机秒或外部 RTC 时间事件码2B比如上电、掉电、故障、参数变更数据长度2B变长数据长度业务数据0~32B事件附加信息CRC81B条目校验扫描日志时从头开始逐条读遇到 CRC 错误就停止表示后面都是未初始化区域或坏数据。这样掉电时刻正好写在某个条目中间最多丢正在写的那一条之前的日志全部完好。掉电保存环节我用了 F373 的 PVD 功能。PVD 检测到 VDD 低于阈值时会触发中断在中断里把“当前运行模式、关键计数器、最近一次 ADC 采样值、堆栈关键变量”打包写入 MRAM 的固定地址。因为 MRAM 写入不需要等待SPI 在 4MHz 下写 64 字节也就 130 微秒左右电源电容撑住几毫秒完全来得及。关键代码如下void PVD_IRQHandler(void) { /* 检查 PVD 中断标志 */ if (EXTI_GetITStatus(EXTI_Line16) ! RESET) { mram_write(SAVE_ADDR, (uint8_t *)system_snapshot, sizeof(system_snapshot)); /* 关闭中断等待复位 */ NVIC_SystemReset(); } }注意PVD 中断里不要再初始化外设、不要做复杂运算只做最快的数据保存。如果现场噪声大可以给 VDD 增加一个几百 uF 的电解电容为掉电保存争取时间。掉电保存的数据在上电后要打一个专门的启动标志方便日志系统判断“上次是否为异常断电”。5. 实测数据与排查记录5.1 先说速度实测写入读取吞吐我在 F373 72MHz、SPI1 主模式下测了两组数据一组 SPI 时钟 4MHz另一组 18MHzSPI 时钟频率由 BR 分频配置。用连续读和页写128 字节/次分别测测试项目SPI4MHzSPI18MHz连续读 512KB约 1.1 秒约 0.27 秒页写 128 字节约 0.42ms约 0.11ms理论峰值速率500KB/s2.25MB/s这里的理论峰值只算了纯时钟吞吐实际工程上连续读 512KB 已经接近理论值。页写的速率达不到理论值因为每次写前要发 WREN、命令和地址这些开销在小包写入时占比很高。如果你写的是零散小数据可以把应用层做成“先攒批再一次性写 128 字节页”总线效率会明显提升。5.2 调试中踩过的六个坑第一个坑也是我印象最深的就是 /HOLD 引脚悬空。最初设计时觉得 HOLD 功能不用就没接上拉结果样机在实验室里跑 10 分钟就会出现一次写数据出错逻辑分析仪抓波形看起来又一切正常。排查了两天最后发现是电机启动瞬间的电磁干扰把 HOLD 拉低芯片暂停通信导致丢数据。解决办法就是给 HOLD 接 10k 上拉之后再也没出现过。第二个坑把 Flash 的“写后等待”逻辑带过来了。因为我之前用 W25Q 比较多写完页之后习惯性去轮询状态寄存器的 WIP 位结果发现 MRAM 的状态寄存器根本没有这个位读出来永远是 0。一开始我还以为是 SPI 时序错了反复调主频和相位后来静下心读数据手册才发现MRAM 压根不需要等。如果直接把 W25Q 驱动移植过来这块逻辑不改程序轻则多跑几行无效代码重则因为“等待超时”误判芯片故障。第三个坑SPI 模式 0 和模式 3 混用导致的边缘误锁存。MRAM 本身两个模式都支持但如果你用模式 3SCK 空闲为高拉高 CS 时必须保证 SCK 确实处于高电平。某些 GPIO 配置或者时序抖动会让 CS 释放时 SCK 正好在下降沿附近芯片可能把最后一个数据又锁存一次造成末尾字节错误。我后来统一用模式 0这个问题再没复现过。第四个坑写状态寄存器忘记加 WREN。MRAM 的 WRSR 命令和内存写入不一样它前面必须要 WREN否则状态数据根本写不进去。我一开始想设置 WPEN 位做整片保护结果写了几次状态寄存器都失败读出来还是 0x00。这个和普通 Flash 的 WRSR 行为很相似但 MRAM 没有 WIP 位如果还按 Flash 的方式去确认反而容易把自己绕进去。第五个坑SPI 时钟过高加杜邦线调试导致的首字节错位。开发阶段我图省事用杜邦线把模块和核心板连在一起SPI 频率从 4MHz 提到 12MHz 后读取的第一个字节经常出错后面的数据都是好的。这是因为长线在高速率下振铃第一个字节的建立时间不足。后面换成 PCB 短走线并加了 33Ω 串阻再把速率稳定在 9MHz 或 18MHz 的分频档问题就消失了。工业现场环境我更推荐保守一点优先保证时序余量。第六个坑地址越界回绕。说起来像个低级错误但它非常隐蔽MR25H40CDF 只有 21 位有效地址如果你不小心向 0x80000 地址写数据芯片会忽略高 3 位把数据回绕写到 0x00000 地址直接覆盖参数区 A。不做边界检查的话这个 bug 在实验室里很难触发产品一旦跑飞就闹笑话。所以我在驱动里强制加了addr len MRAM_SIZE的检查应用层调用时也统一走带校验的接口。5.3 调试工具与经验补充调试 MRAM 驱动时逻辑分析仪比示波器更好用因为 SPI 信号通道多、需要长时间抓包对比。我习惯同时抓 CS、SCK、MOSI、MISO 四根线把时序和解码打开直接看命令字节和地址对不对。如果抓到读回的数据全 FF先看 CS 是否在指令期间被意外拉高如果数据全 00基本可以确定 MOSI 或者时钟配置有问题。还可以写一个小的自检函数上电时备份首 1024 字节用一组伪随机数写入并读回比对最后恢复原数据。这样能快速区分“硬件问题”和“驱动问题”。放在产品自检流程里也很实用至少能保证每次开机都验证存储链路健康。另外说一个很多人不知道的点单片 SPI MRAM 没有标准的 JEDEC ID 指令很多国产兼容料也没有。所以别指望像 W25Q 那样用 0x9F 读 ID 来判断型号直接用读写测试最靠谱。6. 把驱动收进工程时的一点体会这一套 MR25H40CDF STM32F373RC 的存储方案我现在最常用的用法是把它当成一个“掉电不丢的全局数组”而不是“按扇区管理的存储介质”。数据结构在 SRAM 里怎么玩在 MRAM 里就怎么玩只不过写完立刻掉电它还在。工业产品里那些需要边采集边落盘、掉电必须恢复核心状态的场景这套组合特别顺手。如果给刚上手的同行留两个建议我会说一是首次调试时把所有 SPI 引脚用逻辑分析仪抓一遍边沿质量别急着跑速度先把时序余量做足二是哪怕再忙也要把地址越界检查和 CRC 校验加上。这两条帮我省过不止一个项目周期的返工时间现在也成了我做嵌入式存储模块的固定动作。