
1. 为什么工业存储场景里我选了 MRAM 而不是 FlashMR25H40CDF 的定位与选型逻辑1.1 从一次掉电丢数据说起做工业设备最怕什么不是通讯断线不是控制算法跑飞而是明明写进存储器的数据一掉电再上电就变成了一堆乱码。我之前在一台三相电能采集设备上踩过这个坑校准系数放在 SPI NOR Flash 里设备频繁上下电结果某天现场报数据异常拆回来一查Flash 里某个扇区擦写次数过万坏块标志出来了整页数据读出来全是 0xFF。那之后我就开始认真审视一个嵌入式工程师常常忽略的问题——存储介质本身的寿命和可靠性。工业应用里存储需求其实很朴素参数要稳日志要勤写掉电不能丢最好还能像 SRAM 一样随便写、不心疼寿命。Flash 的问题是擦写寿命有限、写入前要先擦除、页编程时间毫秒级任何一个短板在异常断电频繁的现场都会被放大。这时候把目光放到 MRAM 上你会发现它是很“小众但刚需”的存在——比如 Everspin 的 MR25H40CDF一颗 4Mbit 的 SPI MRAM读写速度、写寿命、数据保持时间都对得起“工业级”三个字。再配合 STM32F091RC 这种性价比高、资源够用的 Cortex-M0 内核 MCU整套方案做数据存储和读取非常顺手。1.2 MR25H40CDF 硬指标与 SPI 接口特性MR25H40CDF 这颗芯片我重点关注几个参数容量 4Mbit512KB按字节寻址地址宽度 24 位高速 SPI 接口支持模式 0CPOL0CPHA0和模式 3CPOL1CPHA1数据读取没有次数限制不是破坏性读写寿命理论无限次不像 NOR Flash 那样按扇区擦除也没有写放大问题数据保持时间超过 10 年和电池无关因为它本身是非易失的。嵌入式开发的朋友看到这应该已经有感觉了这实际上就是一颗“非易失的 SRAM”。它内部存储单元的磁性状态决定了数据在掉电后依然存在运行起来又不需要像 Flash 那样先擦后写。对应的工业电表、伺服驱动器、轨交设备、电力保护装置里经常能看到 MRAM 的身影——这些场景的共同点是高频率、小数据量、强掉电风险。SPI 接口上MR25H40CDF 的操作命令和普通 SPI NOR Flash 很像读 ID、读状态寄存器、写使能这类命令风格一致甚至能共用一套软件框架。但它有个核心区别写入不需要轮询忙标志等毫秒级编程时间命令发出后写操作在芯片内部几乎是瞬时完成的。这让驱动代码逻辑大大简化也可以把“写进去再读出来校验”当成常规操作来用。1.3 它和 FRAM、NOR Flash、SRAM 的选型对比有工程师会问同样是近乎无限次写入的非易失存储MRAM 和 FRAM铁电存储器怎么选我的理解是FRAM 在低容量段更便宜很多仪表里用的 FM24V05 之类就是典型MRAM 在中高容量、高访问速度和宽温度范围上更有优势。MR25H40CDF 这类 SPI MRAM容量能到 4Mbit密度比常见 FRAM 高不少速度也更快更适合做日志缓冲、掉电保护区这类需要较大空间的数据场景。下面是几个常见存储介质在工业场景里的核心对比对比项MRAMMR25H40CDFSPI NOR FlashSPI FRAM电池备份 SRAM写入寿命理论无限次通常 10 万次擦写理论无限次无限次但依赖电池写入方式直接写入无需擦除先擦除后写、按扇区直接写入直接写入写周期时间纳秒级总线决定页编程毫秒级纳秒级纳秒级掉电保持10 年以上10-20 年10 年以上需要电池维持密度中等偏高高低-中等中等成本相对较高低中等中等从这张表能清楚看到MRAM 的定位不是替代所有 Flash而是填掉“高频写入 掉电保持”这个空白。如果你的系统里只有出厂校准参数一年写一次NOR Flash 完全够用但如果是运行日志、故障记录、计量累计值这种每次断电都可能在更新的数据MRAM 就是更省心的选择。我现在的做法是 Flash 和大容量的 SD 卡存固件和报表MRAN 专门管参数区和高频写入的日志区各干各的活。2. STM32F091RC 侧的工程准备引脚、CubeMX 配置和 SPI 时序参数怎么定2.1 引脚分配与原理图级连接STM32F091RC 是 64 引脚的 M0 芯片主频最高 48MHz片上资源足够跑这种存储应用。我用的是 SPI1 外设引脚分配如下PA5 —— SPI1_SCK接 MRAM 的 SCKPA6 —— SPI1_MISO接 MRAM 的 SOPA7 —— SPI1_MOSI接 MRAM 的 SIPA4 —— GPIO 输出接 MRAM 的 CS#PB0 —— 预留 GPIO接 MRAM 的 WP# / HOLD# 控制如果不做写保护功能按手册推荐接上拉即可。这里有个细节我要单独说一下CS# 强烈建议用普通 GPIO 控制不要用硬件 NSS。MRAM 的操作都是先拉低 CS# 发出命令和地址再传输数据最后拉高 CS# 结束事务。用 GPIO 控制 CS#时序完全由软件掌握写起来简单排查问题也直观。SPI 外设的 NSS 在多设备总线里有时候反而碍事。原理图层面Vcc 和 GND 之间放一个 100nF 陶瓷电容我另外还加了一个 10uF 的钽电容做低频滤波。不要小看电源退耦这件事MRAM 写入瞬间电流变化虽然不大但工业现场电源本身杂散很多退耦做不好长期运行稳定性会打折。WP# 和 HOLD# 这两个引脚如果不用就按照数据手册推荐方式处理——通常直接上拉到 Vcc避免芯片意外进入写保护或暂停状态。2.2 CubeMX 工程初始化要点CubeMX 里配置工程时有几个选项值得特别注意SPI1 选择 Full-Duplex Master 模式数据宽度选 8 bit这跟 MRAM 的字节寻址结构匹配帧格式选 MSB First这是 SPI NOR/MRAM 类器件一贯要求的字节序预设分频器参数先把时钟压到 4MHz 左右调通再往上提生成代码时把 HAL 库的 SPI 中断回调、DMA 等默认关闭前期先用轮询方式把逻辑跑通。M0 内核没有硬件除法器但 SPI 这种外设操作影响不大。整颗芯片跑 48MHz实际瓶颈在 SPI 总线速度而不在主频。用 CubeMX 的好处是生成的外设初始化代码很规范后续如果换到别的 STM32 型号外设配置逻辑基本一致。需要记住的是CubeMX 只是生成框架MRAM 驱动逻辑还得自己写——这个我放在下一章展开。2.3 SPI 参数为什么这么配SPI 通信参数里最容易出错的是时钟极性和相位。MRAM 支持模式 0 和模式 3工程上我推荐用模式 0CPOL0、CPHA0即时钟空闲为低电平第一个时钟沿采样数据。原因很简单绝大多数 MCU 的 SPI 外设默认配置就是模式 0减少出错概率。明确说一下MR25H40CDF 的数据手册里对这两种模式都有时序图照着模式 0 的图对一下管脚上的波形心里就有数了。波特率分频这块我在 STM32F091RC 上的实际选择是先用 /16 分频SPI1 挂在 APB2 上48MHz 时钟分频后大约 3Mbps在这个速度下哪怕杜邦线飞线调试信号完整性也没啥压力。等 PCB 板子做回来、走线正常以后再把分频调到 /4 甚至 /2把吞吐提上去。还有一点MRAM 是标准的推挽 CMOS 输出MISO 线上不需要外部上拉。如果你在别的板子上见过 SPI Flash 的 MISO 接上拉电阻那是某些旧器件的兼容性设计不是必须的。在 MR25H40CDF 上保持干净的总线拓扑就行了。3. 驱动代码落地从命令集到可用的读写函数3.1 命令集基础WREN、RDSR、READ、WRITEMR25H40CDF 的命令集不复杂实际用得到的主要就几个命令操作码作用WREN0x06写使能写入前必须执行WRDI0x04写禁止RDSR0x05读状态寄存器READ0x03读数据可连续读WRITE0x02写数据可连续写RDID0x9F读 JEDEC ID用于上电自检初学者最容易忽略的是 WREN。MRAM 的状态寄存器里有一个写使能锁存位 WEL必须执行 WREN 命令把它置 1后续的 WRITE 命令才会生效。每次写事务结束后硬件会重新把 WEL 清掉所以下一次写之前还得再发一次 WREN。这跟有些 Flash 芯片的连续写模式不一样写驱动的时候别想当然。读操作不需要 WRENREAD 命令可以直接发。上电的时候先用 0x9F 读一下器件 ID确认 SPI 链路和芯片型号都对这是我在每块新板子上都会做的第一步验证。3.2 读写代码实现细节下面这段代码是我调试通过的核心驱动基于 STM32 HAL 库写的。先定义 CS 控制宏和单字节传输函数#define MR25H40_CMD_WREN 0x06 #define MR25H40_CMD_WRDI 0x04 #define MR25H40_CMD_RDSR 0x05 #define MR25H40_CMD_WRITE 0x02 #define MR25H40_CMD_READ 0x03 #define MR25H40_CMD_RDID 0x9F #define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET) static uint8_t mram_transfer(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, HAL_MAX_DELAY); return rx; }写使能和状态寄存器读取static void mram_write_enable(void) { MRAM_CS_LOW(); mram_transfer(MR25H40_CMD_WREN); MRAM_CS_HIGH(); } static uint8_t mram_read_status(void) { uint8_t status 0; MRAM_CS_LOW(); mram_transfer(MR25H40_CMD_RDSR); status mram_transfer(0x00); MRAM_CS_HIGH(); return status; }接着是核心的按地址读、写函数。地址是 24 位传输顺序高字节在前void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { mram_write_enable(); MRAM_CS_LOW(); mram_transfer(MR25H40_CMD_WRITE); mram_transfer((addr 16) 0xFF); mram_transfer((addr 8) 0xFF); mram_transfer(addr 0xFF); for (uint32_t i 0; i len; i) { mram_transfer(buf[i]); } MRAM_CS_HIGH(); } void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_transfer(MR25H40_CMD_READ); mram_transfer((addr 16) 0xFF); mram_transfer((addr 8) 0xFF); mram_transfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] mram_transfer(0x00); } MRAM_CS_HIGH(); }写完以后HAL 库本身的 SPI 超时参数我给的是 HAL_MAX_DELAY就是死等调试阶段这样最靠谱。功能正常后再考虑要不要改成中断或 DMA 方式。这里必须提一个不算坑的坑写使能后紧接着的 WRITE 命令必须在同一个 CS# 低电平周期内完成不能说先拉高 CS# 结束 WREN过一会儿再拉低发 WRITE。WREN 的状态在 CS# 拉高后维持但总线上一旦有其他命令插入WEL 可能被意外清掉。所以我把 WREN 和 WRITE 两个步骤放在同一个函数里连续执行中间不夹任何其他操作。3.3 校验回读的设计逻辑工业环境里写进去的数据一定要读出来确认这是基本素养。我在驱动层加了个简单的校验函数int mram_write_verify(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t rd_buf[32]; uint32_t offset 0; while (offset len) { uint32_t chunk (len - offset) sizeof(rd_buf) ? sizeof(rd_buf) : (len - offset); mram_read_bytes(addr offset, rd_buf, chunk); if (memcmp(rd_buf, buf offset, chunk) ! 0) { return -1; } offset chunk; } return 0; }注意这里读缓冲只有 32 字节是为了避免在栈上分配大数组。M0 内核的 SRAM 本来就不宽裕F091RC 只有 32KB SRAM大数组容易把堆栈压爆。正确的姿势是分块回读每次读一小段比较数据量大的时候多循环几次而已。这个校验函数在量产测试里很有用。我还会配合一个全局错误计数器在写参数、日志时统一调用一旦校验失败就置位故障标志上报给上位机。MRAM 本身故障率极低但总线接触不良、虚焊这种问题单靠一次写入是发现不了的回读校验是廉价的保险。4. 实测数据说话读写性能、掉电保持和 24 小时循环写验证4.1 测试环境与测试方法驱动跑通以后我搭了一个简单的测试工程STM32F091RC 主频 48MHzSPI 时钟分别设为 3MHz 和 12MHz 两档用 GPIO 翻转来测量单次读写事务的实际耗时。测试内容包括单字节读写、256 字节页写、连续读 4KB、以及一个模拟掉电场景的验证。模拟掉电的做法很简单给板上 MRAM 写入一组已知特征码然后直接切断整块板子的电源过几秒重新上电读出来比对。我连续做了 50 次中间还包括故意在写入过程中断电的极端情况用来验证数据会不会出现半写状态。4.2 性能数据参考下面是实测下来的参考值SPI 3MHz 档操作数据量实测耗时说明写使能 单字节写1 字节约 40us含命令地址数据的总线传输单页连续写256 字节约 700us需要拆页时额外开销连续读4KB约 12ms纯读无需命令拆分写后回读校验256 字节约 1.4ms写读比较在 12MHz 档吞吐基本是线性提升256 字节连续写可以压到 200us 以内。这些数字和一个简单的事实放在一起看会很有意思MRAM 芯片本身的写动作是纳秒级的你测到的耗时几乎全部花在 SPI 总线上。换句话说想让存储变快优化方向是提高 SPI 时钟频率和减少 GPIO 翻转开销而不是担心芯片内部写入时间。作为对比SPI NOR Flash 写一个 256 字节页普遍要 3-5ms其中绝大多数时间花在内部电荷泵和单元编程上。这就是为什么高频日志、故障记录这类应用MRAM 有天然的代差优势。4.3 掉电数据完整性与循环写验证断电实验的结果很干净50 次随机断电数据都完整。唯一一次读出来异常的数据是我故意在写过程中掉电测试的那组原因是 SPI 事务才传了一半CS# 还没拉高整条命令当然没执行完。但有意思的是这次中断并没有破坏芯片里原有的旧数据——这也是 MRAM 的一个特性写入是按位直接改变存储状态的只有真正完成的那部分字节会变化未完成的地址保持原值不会出现 Flash 那种“编程中断后扇区内容不确定”的情况。我还做了一个 24 小时连续写测试每 10ms 写一次每次 128 字节一天大约写了 864 万次写入事务。读回来逐字节比对错误数保持为 0。这个测试虽然不能证明芯片寿命无限但至少说明在高频写入场景下它没有出现 Flash 那种越写越慢、越写越容易错的现象。这些测试结果我建议你们拿到自己项目里也跑一遍尤其是断电测试别省。MRAM 的可靠性参数是芯片级的但你的布线、电源、SPI 时序是板级的问题这种问题只能靠实测暴露。5. 工业现场才遇得到的坑字节序、写使能、页边界与掉电时序5.1 字节序到底谁说了算这个坑我刚接触 MRAM 时也踩过。MRAM 存储单元是字节寻址内部没有大小端的概念你发什么字节进去读出来就是什么字节。但 STM32 的 Cortex-M 内核是小端架构一个 uint32_t 变量 0x12345678 在内存里实际的字节顺序是 78 56 34 12。如果你随手把一个结构体 memcpy 进缓冲区再 mram_write_bytes写进 MRAM 的字节序和你在上位机解析时预期的字节序可能完全相反。我的建议很简单在应用层明确规定存储区的字节序按大端来。也就是无论 MCU 本身怎么排凡是往 MRAM 里写多字节数值统一按高字节在前的顺序写uint8_t buf[4]; buf[0] (value 24) 0xFF; buf[1] (value 16) 0xFF; buf[2] (value 8) 0xFF; buf[3] value 0xFF; mram_write_bytes(addr, buf, 4);这样做的理由是MRAM 里的数据经常要导出给上位机做分析上位机、脚本、Wireshark 这类工具默认看懂的都是大端字节序。你提前在 MCU 侧统一好后面所有协议解析都不用再纠结。5.2 写使能时序与“写不进去”问题前面提过 WREN 是写数据的前置条件但实际工程里有一个更隐蔽的问题某些代码框架会在每次 CS# 事务之间插入额外的 SPI 通信比如读温度传感器、读另一颗 Flash。如果两个外设共用一条 SPI 总线而 CS# 的切换代码出了问题就可能出现这种情况——你调用了 mram_write_bytesWREN 发出去了但紧接着被另一个外设的通信打断WEL 又被清掉了数据自然写不进去。排查这类问题有一个很实用的手段写失败时读一下状态寄存器看看 WEL 位到底是 0 还是 1。如果是 0说明写使能没保持住去看总线时序如果是 1说明命令和地址正确但数据没进去去看 CS# 的完整性和电源。用逻辑分析仪挂上 SCK、MOSI、CS# 三根线一眼就能看出问题。另外MRAM 的 WRITE 命令在一根 CS# 低电平周期里最多连续写 256 字节超过这个数芯片内部会自动把地址回卷到当前页首。如果你的 len 参数不小心传了个 300 字节数据就是回卷覆盖不是报错。驱动里必须做页边界拆分这是纯软件问题和数据手册无关。void mram_write_range(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { uint32_t page_left 256 - (addr 0xFF); uint32_t chunk len page_left ? len : page_left; mram_write_bytes(addr, buf, chunk); addr chunk; buf chunk; len - chunk; } }5.3 与掉电检测配合的写时序工业现场电源波动是常态。STM32F091RC 自带 PVD可编程电压检测器我习惯把 PVD 阈值设在 2.9V 左右触发中断后立刻做两件事第一置一个全局标志禁止应用层再发起任何新的 MRAM 写事务第二如果当前正有一个写事务在执行等它完成后再让系统进入掉电保护流程。因为 MRAM 写入本身很快一个 128 字节的事务在 3MHz SPI 下也就几百微秒掉电检测触发后通常有足够的电容储能支撑完成这个尾巴。PVD 中断里千万不要做复杂操作只置标志真正的收尾放在主循环里检查标志后执行。中断里跑 SPI 事务是自找麻烦万一 SPI 外设又被更高优先级中断打断CS# 时序出一丁点问题整个事务就废了。5.4 与系统里其他外设的共存问题MRAM 如果和别的 SPI 器件共用总线总线空闲时 MISO 上的电平冲突也需要留意。多从机挂在同一条 SPI 总线上所有从机的 MISO 输出在未被选中时都要处于高阻态MR25H40CDF 的 CS# 拉高后它的 MISO 会自动释放通常没问题。但有些国产兼容 MRAM 芯片的高阻特性没那么好CS# 拉高后 MISO 还会有残余电平这时候加一个 10k 下拉电阻到地可以避免总线空闲时出现不确定状态。我不建议为了省引脚把 MRAM 和别的器件压在同一根 CS# 上哪怕用 GPIO 扩展芯片去做片选也不行。MRAM 事务要求 CS# 低电平期间必须完整地传输命令地址数据中途被别的器件抢线数据就毁了。这也是为什么我觉得“软件 CS 独立 GPIO”是工业级 SPI 存储应用最稳妥的组合。6. 以此为基础能做更多参数存储、鲁棒日志与自检模板6.1 参数存储区的设计建议MRAM 不像 Flash 需要按扇区管理你可以像用普通内存一样直接按地址规划。我习惯在 512KB 空间里这样划分0x00000 - 0x03FFF系统参数区保存校准系数、设备地址、运行模式0x04000 - 0x07FFF生产信息区序列号、生产日期、硬件版本0x08000 - 0x7FFFF日志循环区存故障码、操作记录、计量累计值。参数区我每次写入前先读旧值只有内容真正变化才发起写事务减少不必要的总线占用。所有参数写完后执行一次全量回读校验把校验结果和 CRC 一起存进一个状态字里。上电的时候先读状态字CRC 不对就回退到出厂默认参数同时记录一条参数异常日志。这套逻辑在 Flash 时代我也这么干但 MRAM 时代做起来更轻松因为不用先擦除整个扇区再写回退和重写都快得多。6.2 循环日志区的鲁棒性设计日志记录类应用最担心的是写入过程中掉电留下一条半截记录。我采用的方案是“Magic 序号 数据 CRC”的记录结构每条固定 32 字节字段长度说明Magic2 字节固定值 0xA5 0x5A用于识别记录是否有效Seq4 字节单调递增序号用于判断新旧和排序Timestamp4 字节时间戳Data20 字节业务数据CRC322 字节对前面字段做校验取高 16 位写入时先在这个地址上写全 0x00再写正式记录。读取时如果 Magic 不对就判断这条记录无效往前找最后一条有效记录。因为 MRAM 写入单位是字节不会出现半页损坏的问题这种设计已经能覆盖绝大多数异常场景。日志区写满后的处理也很简单从区首继续覆盖写。MRAM 没有磨损均衡需求覆盖写不会有寿命问题这比 Flash 日志区必须做擦写均衡简单了不止一个数量级。我最初做 Flash 日志区的时候光磨损均衡算法就调试了两周换到 MRAM 后这段代码直接删掉了。6.3 一套可复用的自检模板最后给一套我在产测和上电自检里都会用的模板直接抄作业就行#define MRAM_TEST_ADDR 0x70000 #define MRAM_TEST_LEN 64 static int mram_selftest(void) { uint8_t tx_buf[MRAM_TEST_LEN]; uint8_t rx_buf[MRAM_TEST_LEN]; int ret 0; for (int i 0; i MRAM_TEST_LEN; i) { tx_buf[i] (uint8_t)(i * 7 3); } ret mram_write_verify(MRAM_TEST_ADDR, tx_buf, MRAM_TEST_LEN); if (ret ! 0) { return -1; } mram_read_bytes(MRAM_TEST_ADDR, rx_buf, MRAM_TEST_LEN); if (memcmp(tx_buf, rx_buf, MRAM_TEST_LEN) ! 0) { return -2; } return 0; }上电时调用一次返回 0 才继续跑业务逻辑。产测的时候这个函数要跑三遍分别测参数区起始地址、日志区中间地址、最高地址附近覆盖地址解码和 CS# 长走线的极限情况。别嫌这种测试啰嗦我见过不止一次因为 PCB 走线过长导致高地址区域读写不稳定的案例自检跑一遍能少返工很多板子。用 MRAM 和 STM32F091RC 组合做工业存储整体思路就是“把存储当内存用把校验当常规操作”。我在实际项目里的体会是这套方案的成本确实比普通 Flash 高一些但省下的是故障排查、返厂维修、现场升级这些看不见的钱。如果你手里有个设备需要频繁写参数、频繁记录日志、还时不时被现场断电不妨认真考虑一下 MRAM 这条路它会让你少掉很多头发。