去年做一款用于数字电源的主控板我选了 STM32F334R8 这颗带 HRTIM 的 Cortex-M4整体方案跑得很顺但一个不起眼的环节把整个项目拖了两周运行参数和故障日志的存储。一开始用的是 SPI NOR Flash结果调试时频繁擦写直接把这个位置的存储写穿了数据开始随机损坏。后来我把方案换成了 Everspin 的 MRAM 芯片 MR25H40CDF问题才算彻底解决。这篇文章就把我在工业环境里用 MR25H40CDF 搭配 STM32F334R8 做数据存储和读取的完整经验写出来包括硬件接线、底层驱动、应用层的数据组织以及几个真实踩过的坑给正在做类似嵌入式项目的人一个可以直接抄作业的参考。1. 先说说为什么是 MRAMFlash 掉坑之后的选择1.1 MR25H40CDF 到底是个什么器件MR25H40CDF 是 Everspin 生产的一颗 4Mbit 串行 MRAM容量折合 512KB接口是标准的 SPI和普通 SPI Flash、SPI EEPROM 在引脚和协议上非常接近。但它和 Flash 的存储原理完全不一样MRAM 的存储单元用的是磁隧道结MTJ数据是靠着自由层和参考层的磁化方向来记录 0 和 1写入的过程是物理上的磁矩翻转不是往浮置栅极里注入电荷。这个区别意味着什么呢Flash 和 EEPROM 写数据之前需要先擦除而且擦写本身会给存储单元带来物理磨损写个十万次、一百万次之后就可能扛不住了。而 MRAM 写数据不用擦除磁性翻转本身几乎不会带来损耗所以写次数可以达到百亿次级别对于绝大多数工业日志、参数保存这类场景基本等同于无限寿命。这还不是全部。MRAM 的读写速度也非常接近 SRAM写入完成后不需要等待内部编程完成上电瞬间数据就在那里掉电也不会丢。我当时选这颗料就是看中它在高频率写入 掉电保存这种组合下属于当前最适合的方案。1.2 和 EEPROM、FRAM、SRAM电池相比强在哪、弱在哪很多人会问这种场合我是不是也可以用 EEPROM或者铁电存储器 FRAM再不然直接用 SRAM 加电池。我把实际对比列出来方案写寿命写入速度掉电保存容量密度实际体会SPI EEPROM约百万次每字节等待数毫秒支持一般到 2Mbit 左右写一条日志要等批量写更难受SPI NOR Flash十万到百万次先擦后写页写入支持容量大频繁改写寿命不够磨损均衡麻烦FRAM近乎无限快无需等待支持常见偏小方案本身不错但可选料和渠道相对少SRAM 电池/超级电容无限快需带电池长期维护麻烦偏小适合掉电瞬间保鲜不适合长期存储MR25H40CDF MRAM百亿级快无需等待支持10年以上数据保持4Mbit 起跳可以直接当 RAM 存不用考虑擦写策略当然 MRAM 也不是没有短板。最直接的就是价格比同容量的 SPI Flash 贵不少而且常见的 SPI MRAM 容量没有 Flash 那么大做海量文件系统不是它的主力场景。但在工业控制、数字电源、伺服驱动这种单条数据量不大、又需要反复改写、掉电必须留住的场合它非常合适。一句话总结MRAM 是把非易失性装进了 RAM 一样的壳里。2. 硬件接线想让 MR25H40CDF 稳定工作先把这几个脚伺候好2.1 SPI1 引脚与片选规划STM32F334R8 的资源不算少SPI 外设也有两三个但我实际接 MR25H40CDF 用的是 SPI1因为 SPI1 挂在不同时钟树上最高频率比 SPI2/SPI3 要舒服一些在 F334 上配合 72MHz 主频分频之后跑 18MHz 毫无压力。整体接线我整理成一张表信号功能STM32F334R8 引脚MR25H40CDF 引脚SCKPA5SCKMOSIPA7SIMISOPA6SOCSPA4任意 GPIO 控制CS#写保护直接接 3.3VWP#通信暂停直接接 3.3VHOLD#电源3.3VVDD地GNDVSSCS 片选我建议不要用硬件 NSS直接拿一个 GPIO 控制。原因很简单软件控制 CS 的时序更加灵活尤其在后面做 DMA 读写时你可以想拉低就拉低、想拉高就拉高不会被外设自动控制牵着走。我用的是 PA4你如果这个引脚被占用了换其他任意 GPIO 都行只是代码里记得同步修改。2.2 容易翻车的 WP#、HOLD# 和电源滤波这两个不能悬空的引脚是我第一版板子翻车的根源。WP# 是写保护输入低电平时芯片会被锁死只能读不能写HOLD# 是通信暂停输入拉低时芯片会忽略 SPI 总线上的一切操作。如果这两个脚悬空或者 PCB 上布线不小心引入了干扰就会出现一种很诡异的现象芯片大部分时间工作是正常的但偶尔一条数据写不进去或者一次读操作卡在半路。我自己第一次打样时WP# 没接上拉靠外部环境凑巧维持在一个浮空电平上结果整板在线调试时前 32KB 写入正常后面区域的地址怎么都写不进。查了一个晚上才用示波器发现这个脚的电平在 1.2V 上下飘芯片内部逻辑一会认为写保护开了一会又关了。所以这两只脚务必通过 10k 电阻上拉到 VDD干脆利落不要留任何悬念。电源方面MR25H40CDF 工作电压 2.7V~3.6V可以直接挂在 3.3V 电源域。VDD 旁边放一个 0.1uF 陶瓷电容作为高频去耦如果 PCB 空间允许再加一个 10uF 电容兜底。工业板子上MRAM 对电源纹波不算特别敏感但旁边如果有功率 MOSFET 在开关我还是会把 MRAM 的供电单独走一小段星形布线不和功率驱动的地线串在一起。2.3 PCB 摆放的工程经验MR25H40CDF 一般是标准 8 脚 SOIC 封装焊接和布线都不麻烦。我的经验是把它放在 MCU 附近尽量让 SPI 四根线短而直。SCK 是时钟频率到 18MHz 之后虽然不太高但走线太长仍然可能收到邻近信号干扰。如果板子上有电感、变压器、IGBT 驱动回路SPI 走线不要和这些强干扰源平行长距离伴行。另外工业环境常常要过 ESD 测试MRAM 这类芯片本身对静电不那么皮实接口插座或者连接器如果离它很近建议在信号线上加 ESD 防护器件或者至少在 SI、SO、SCK 上串 22Ω~33Ω 的小电阻既能限流也能起到一定的阻尼作用。3. 底层驱动指令集、状态寄存器与“不需要等待”的写时序3.1 指令集和 SPI 模式MR25H40CDF 的指令集与 SPI EEPROM 非常相似常用的就那么几条指令操作码说明WREN0x06写使能写入操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据后面跟 3 字节地址WRITE0x02写数据后面跟 3 字节地址RDID0x9F读 ID部分型号支持SPI 模式方面这颗芯片支持模式 0CPOL0CPHA0和模式 3CPOL1CPHA1我实际使用并推荐的是模式 0。在 STM32CubeMX 里配置 SPI 时时钟极性选 Low时钟相位选 1 Edge数据帧 8 位MSB 先行。这里有一个小经验如果你的 SPI 已经初始化在模式 3只要 MRAM 这边也按模式 3 来理解其实通信也能正常怕就怕两边的相位理解不一致导致第二个字节开始全部串位。3.2 写使能与状态寄存器MRAM 有个比较容易理解错的地方它的写保护和状态寄存器逻辑和 EEPROM 类似写操作之前要先发一条 WREN 命令把 WEL写使能锁存位置 1然后才能发 WRITE 指令。成功写完一组数据之后WEL 位又自动清零。读状态寄存器的最低一位就能看到当前 WEL 是否置位。区别在于MRAM 没有忙等待的概念。Flash 和 EEPROM 写一个字节后内部要花几毫秒去编程你必须读状态寄存器的 BUSY 位或者直接延时否则下一笔写容易丢。而 MR25H40CDF 的写操作在 SPI 时钟关断那一刻就已经完成了写完立刻可以继续下一条指令。刚开始用的时候我习惯性地写了轮询等待后来发现纯属多此一举。还要注意 WP# 的保护逻辑。如果通过 WRSR 开启了块保护配合 WP# 引脚低电平指定区域内数据会被锁死。在不需要这种保护的应用里最简单的方法就是不写 WRSR保持默认状态同时把 WP# 引脚硬件上拉到高避免意外。3.3 最小可用驱动代码下面这套代码是我在实际项目里跑通的风格接近寄存器操作如果你用 HAL 库对照着换 API 即可。#define MRAM_CS_LOW() GPIOA-BSRR GPIO_PIN_4 #define MRAM_CS_HIGH() GPIOA-BRR GPIO_PIN_4 static void spi_xfer(uint8_t *tx, uint8_t *rx, uint32_t len) { // 按实际使用的 SPI 发送接收函数填充 for (uint32_t i 0; i len; i) { while (!(SPI1-SR SPI_SR_TXE)); SPI1-DR tx[i]; while (!(SPI1-SR SPI_SR_RXNE)); if (rx) rx[i] SPI1-DR; else SPI1-DR; } } void mram_write_enable(void) { uint8_t cmd 0x06; MRAM_CS_LOW(); spi_xfer(cmd, NULL, 1); MRAM_CS_HIGH(); } uint8_t mram_read_status(void) { uint8_t cmd 0x05, status 0x00; MRAM_CS_LOW(); spi_xfer(cmd, NULL, 1); spi_xfer(status, status, 1); MRAM_CS_HIGH(); return status; } void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4] {0x03, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF}; MRAM_CS_LOW(); spi_xfer(cmd, NULL, 4); spi_xfer(buf, buf, len); MRAM_CS_HIGH(); } void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4] {0x02, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF}; mram_write_enable(); MRAM_CS_LOW(); spi_xfer(cmd, NULL, 4); spi_xfer((uint8_t *)buf, NULL, len); MRAM_CS_HIGH(); }这套代码看似简单但必须注意一个问题CS 的低脉宽和指令时序要连续。尤其是 WREN 之后CS 必须先拉高让芯片完成写使能锁存然后再拉低发 WRITE 指令。如果 WREN 和 WRITE 之间 CS 没有释放芯片会把后面传来的 0x02 当作 WREN 指令的一部分写操作自然无法触发。4. STM32F334R8 上的两种驱动形态查询模式与 DMA4.1 查询模式简单直接适合低频存储如果项目里 MRAM 的读写频率不高比如每隔 500ms 存一组参数、上电时读一次配置查询模式就够了。上面的示例代码就是查询模式每个字节通过 SPI 外设同步收发CPU 占用的时间很短。18MHz 下512KB 全部读一遍也就几十毫秒实际上工业应用很少一下子读那么多数据。我用查询模式跑过一个故障记录功能系统检测到过流或过压后在中断里标记事件主循环里把故障码、时间戳、关键模拟量打包成一条 16 字节的记录写进 MRAM。整个过程不到 1ms对控制环路的影响可以忽略。如果你只是存标定参数、PID 系数、产品序列号这类低频数据查询模式完全够用不要动不动就上 DMA。4.2 DMA 模式把 CPU 从拷贝中解放出来但有一种场景让我果断切到 DMA数字电源在启动、满载、关机时需要连续记录几十组运行状态比如输出电压、输出电流、管子温度、频率值。这些数据要在一个控制周期内尽可能多抓几笔查询模式虽然也能做但 CPU 被 SPI 阻塞的时间积累起来会影响 HRTIM 中断里的实时响应。DMA 模式的核心思路是初始化一次 SPI 的 DMA 请求把要发的缓冲区地址告诉 DMA 控制器然后 CPU 该干嘛干嘛去传完一帧后通过中断通知你。写 MRAM 可以简化成void mram_write_dma(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4] {0x02, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF}; volatile uint8_t dummy 0x00; mram_write_enable(); MRAM_CS_LOW(); spi_xfer_dma(cmd, NULL, 4); // 发送命令头等待完成 spi_xfer_dma((uint8_t *)buf, NULL, len); // 发送数据 MRAM_CS_HIGH(); }读的时候同样用 DMA 把数据搬到内存然后在 DMA 完成中断里置一个标志位主任务检查到标志位后开始解析数据。这里有一个工程细节如果 MRAM 的读写操作和系统的低功耗模式叠加在一起DMA 传输中突然进入休眠会直接打断传输所以进入低功耗前一定要判断 DMA 传输是否结束。我在低功耗任务里加了一个互斥标志任何 MRAM 操作期间不允许进入 STOP 模式。4.3 CubeMX 配置要点如果你是先用 STM32CubeMX 生成工程有几个地方容易配错SPI1 的时钟来源是 APB2在 F334 上要确认 APB2 时钟是否已经拉满到 72MHz否则 SPI 最高速率会打折。波特率分频选 4得到 18MHz。MRAM 手册支持更高的时钟但我在工业板上没有追求极限18MHz 足够快且余量大。NSS 设置成 Disable也就是不使用硬件片选用 GPIO PA4 手动控制。DMA 配置里SPI1_RX 和 SPI1_TX 分别选择 DMA 通道优先级按需设置。在 rang 数据写入时TX DMA 的优先级可以比 RX 高因为 RX 主要在读取时才用到。我遇到过一种现象DMA 模式下每次写入最后一个字节好像丢了。排查后发现问题不在 MRAM而在 SPI 的 CRC 功能被误开了DMA 传输完成后多了 CRC 校验字节导致 CS 拉高时最后一个有效字节被吞掉。在 CubeMX 里把 SPI 的 CRC 计算关闭即可。5. 数据格式与应用层从裸读写到掉电不丢的日志/参数中心5.1 数据一致性为什么不能直接往地址里写很多第一次用 MRAM 的工程师上来就按数组方式直接往固定地址里写觉得反正写入无限次、不怕掉电那就很稳了。但实际系统掉电不是整齐地断电而是电压从 3.3V 一路下降到 0V 的过程。MCU 在电压跌落期间可能是正在执行写 MRAM 指令也可能已经发出了一半的命令CS 就失控。这种情况下MRAM 里某个地址可能写入了一条残缺记录或者地址附近的旧数据被破坏。这就是我建议在应用层做记录头设计的原因。工业项目里你不光要能读出来数据还要能判断读到的数据是不是完好的。5.2 双槽位 CRC 序列号的方案我经常使用的方案是划分两个存储槽位交替写入。每个槽位的数据结构如下typedef struct { uint32_t magic; // 固定魔数比如 0x20240325 uint32_t seq; // 序列号每次写入递增 uint16_t crc; // CRC16-CCITT对 data 区域计算 uint16_t len; // 实际数据长度 uint8_t data[64]; // 业务数据 } MRAM_RECORD;写流程是先读两个槽位的头部确定谁的序列号更大且 CRC 合法下一次写入时写到较旧的那一个槽位携带新的 seq 和新的 CRC。这样任意时刻断电最多只有当前正在写的槽位不完整另一个槽位总是完整可用的。上电读取流程则是读出两个槽位过滤掉 magic 不对或者 CRC 校验失败的记录取 seq 较大的那份作为当前有效数据如果两份都合法说明上次写入是完整的可以顺手把旧槽位也修复一致。这个方案在逻辑上不复杂代码量也不大但能显著提升工业现场的可靠性。日志类数据也是同样的道理不必整条日志都做双槽用环形缓冲区配合段头段尾校验就能避免把一半殘缺日志当成真实数据。5.3 示例数字电源运行参数记录回到我那个数字电源项目MR25H40CDF 承担了两个职责。第一是保存配置包括 HRTIM 的开关频率、PWM 死区时间、过流阈值的 ADC 标定值、PID 参数等总共 80 字节。这些参数通过串口上位机可以改写一旦写坏电源上电后的行为可能完全异常所以我用双槽位 CRC 来放效果很好。第二是事件日志如过压、过流、板温过高、风扇故障等。每一条日志定义成typedef struct { uint32_t timestamp_ms; // 上电后的系统运行时间 uint16_t fault_code; // 故障类型编码 uint16_t vout_adc; // 故障时刻输出电压 ADC 值 uint16_t iout_adc; // 故障时刻输出电流 ADC 值 uint8_t reserved[6]; // 预留扩展 uint16_t crc; // 本行记录校验 } LOG_ITEM; // 共 16 字节MRAM 容量 512KB如果分配 128KB 作为日志区用环形方式写入每条 16 字节可以存 8192 条。考虑到电源板的日常运行十天半个月的故障历史都有据可查。STM32F334R8 的运算能力处理这些结构体绰绰有余唯一要克制的是不要在主中断里直接解析大量日志把这些批量解析放到主循环中保持中断服务函数的短小。实测下来一次完整日志写入从打包到 SPI 发完约 30 微秒级别对 HRTIM 主控制周期几乎无感。6. 实测数据与翻车现场我在 F334 上踩过的坑6.1 性能和功耗实测我将 MR25H40CDF 接到 STM32F334R8 的 SPI1实际时钟 18MHz实测几个数据连续写 2KB 数据MRAM 无需先擦除逻辑分析仪实测耗时约 1.1ms。换算下来每秒可以写入接近 2MB 数据。作为对比相同容量的 SPI EEPROM 写 2KB每个字节都要等内部写周期最乐观也要几百毫秒。读 2KB 数据约 1ms读操作非常线性。写入之后立刻回读数据完全正确没有出现 EEPROM 那种写完还需要稳定时间的情况。待机状态的电流在手册标注范围内工业板实测常温下 VDD 电流为微安级。这个数据对电池供电或者需要低功耗待机的设备仍然友好。另外必须说一句MRAM 的数据保持时间足够宽裕但不要把它的存储单元理解成永久的。它本质上还是磁性材料的物理状态极端温度、强磁场都可能有影响。工业场景中避免把 MRAM 靠近大功率电感或永磁体这个注意事项手册未必会写在显眼位置但工程上确实需要考虑。6.2 三个真实问题第一WP# 悬空导致的鬼事件写。这个我在硬件部分提过它的表现很害人程序跑起来大部分时间正常但偶尔某个地址写入后回读是全 0xFF。我足足花了两天排查最终用示波器发现 WP# 电平漂浮。解决方式就是接上拉电阻到 VDD从那以后再没出现过。第二SPI 模式不匹配导致的神秘零字节。有次换了一颗不同批次芯片复制代码后读数据全是 0排查发现这颗批次只认模式 0而我之前用的是模式 3。所以我的建议是工程里把 SPI 初始化写死成模式 0同时如果用 CubeMX 生成工程复查一次 CPOL 和 CPHA别指望所有芯片在所有模式下都通用。第三CS 时序太急。我开始做 DMA 后为了缩短单次操作时间把命令帧发完立刻拉高 CS没有保证片选脉冲的最小宽度。个别情况下MRAM 会丢弃最后一条指令。排查方法是用逻辑分析仪抓取 CS 低电平的持续时间确认达到手册要求。后来我在写操作里加了一个极小的延时或者把命令长度做够问题不再出现。6.3 给新手的几条提醒如果你也是第一次在嵌入式项目里用 MR25H40CDF我额外送你几条建议拿到新 PCB 后先不要急着跑业务代码。写一个很简单的回环测试向整片 MRAM 写入递增数再读回来比对。这一步能一次性验证 SPI 接线、供电、WP#/HOLD# 的状态比自己翻电路图猜问题高效得多。上拉电阻不要用可选的 0Ω 电阻替代。很多开发板喜欢留 0Ω方便调试但在工业板批量生产时一旦贴错或者省料后果和悬空一样。如果你的 MCU 引脚紧张MRAM 可以和 SPI Flash 共用一条 SPI 总线各自独立片选。实测下来完全没问题但要注意两条片选之间的任务互斥防止一个通信序列还没结束另一个芯片就被片选抢走。出厂前建议把芯片状态寄存器读出来打印在产测工装上确认 WEL 和块保护位状态。这一条能帮你拦截不少明明代码没问题但芯片写不了的潜在故障。