
1. 为什么 MRAM 在工业嵌入式场景里越来越受关注搞过工业数据采集或者电力终端的朋友应该都有体会选存储芯片这件事很多时候比选主控还让人头疼。EEPROM 擦写寿命不够、Flash 写入前要擦除、掉电保护还得额外加超级电容或者铁电方案越堆越复杂。我最早接触 MR25H40CDF 这颗料是因为一个配电终端的项目要求每秒记录一次电参量断电不能丢数据而且现场环境温度从零下四十度到八十五度普通方案根本扛不住。MR25H40CDF 是 Everspin 推出的一款 4Mbit 串行 MRAM磁性随机存储器SPI 接口支持 40MHz 时钟工作电压 2.7V 到 3.6V工业级温度范围。它最大的特点就是写入不需要擦除、写入速度接近 SRAM、擦写寿命几乎无限官方标称 10^14 次以上而且掉电后数据能保持二十年以上。这几个特性放在一起基本上就是为工业现场数据记录量身定做的。STM32F100ZE 则是 ST 家的 Cortex-M3 入门级 MCU主频 24MHz带 512KB Flash、32KB SRAM外设资源对于这种数据记录场景绰绰有余。它自带三个 SPI 接口其中 SPI1 挂在 APB2 上最高 12MHzF100 系列 APB2 最高 24MHzSPI 分频最低 2 分频SPI2 挂在 APB1 上最高 6MHz。虽然跑不满 MR25H40CDF 的 40MHz但对于每秒一次的数据记录来说12MHz 的 SPI 时钟已经绰绰有余了。这套组合我前后在三个项目里用过踩过不少坑也总结了一些比较实用的经验。下面我会从方案选型、硬件设计、驱动实现、数据管理策略、问题排查几个维度把整个链路拆开讲清楚。不管你是刚接触 SPI 存储的新手还是想从 Flash 方案迁移到 MRAM 的老手应该都能找到有用的东西。2. 方案选型为什么是 MRAM 而不是 Flash 或 EEPROM2.1 三种非易失存储的硬核对比在动手写代码之前先把选型逻辑理清楚。很多人拿到需求第一反应是上 SPI Flash比如 W25Q64 这类便宜量大。但工业场景和消费电子不一样几个关键指标一摆出来选择就很清楚了。对比维度SPI Flash (W25Q64)EEPROM (AT25)MRAM (MR25H40CDF)写入前擦除必须按扇区不需要不需要字节写入速度慢ms 级中等5ms/页快无等待擦写寿命10 万次100 万次10^14 次以上掉电保护需额外电路较好天然可靠写入功耗高擦除时中等低单位成本低中高接口速率高可达 80MHz低通常 10MHz高40MHz从表里能看出来MRAM 唯一的劣势就是贵。但工业项目里一颗料贵几块钱换来的是省掉超级电容、省掉擦除管理逻辑、省掉磨损均衡算法整体 BOM 成本和开发成本反而是降低的。2.2 MR25H40CDF 的关键参数解读选型的时候有几个参数容易被忽略我逐个说一下。容量 4Mbit 等于 512KB这个容量对于数据记录来说不算大但配合合理的存储结构设计存几十万条记录没问题。我一般会把空间划分成元数据区、索引区和数据区后面会详细讲。SPI 模式支持 Mode 0 和 Mode 3这个在初始化的时候必须和 MCU 端配置一致。STM32 的 HAL 库默认是 Mode 0CPOL0CPHA0MR25H40CDF 两种都支持所以直接用默认配置就行。40MHz 最高时钟但 STM32F100ZE 的 SPI1 最高只能到 12MHz所以实际跑 12MHz。这里要注意虽然芯片支持 40MHz但 PCB 走线质量不好的话12MHz 都可能出问题后面硬件部分会讲。写入无延迟这是 MRAM 和 Flash 最大的区别。Flash 写完一个字节要等内部编程完成通常需要几毫秒期间不能响应其他命令。MRAM 写完之后下一个时钟周期就能读不需要轮询状态寄存器。这个特性直接简化了驱动逻辑。2.3 STM32F100ZE 的 SPI 资源分配STM32F100ZE 有三个 SPI实际用的时候要规划好。我的习惯是 SPI1 专门给 MRAM 用因为它在 APB2 上时钟频率高而且 DMA 通道独立不会和其他外设抢资源。SPI2 和 SPI3 留给其他外设比如显示屏或者无线模块。这里有个细节STM32F100 的 SPI1 挂载在 APB2 总线上APB2 最高 24MHzSPI 时钟分频系数最小是 2所以 SPI1 最高 12MHz。SPI2 和 SPI3 挂在 APB1 上APB1 最高也是 24MHz但 SPI2/3 的分频系数最小也是 2所以最高也是 12MHz。不过实际配置的时候我一般会把 SPI1 设成 6MHz 或者 12MHz看 PCB 走线情况。注意STM32F100 系列属于 Value LineSPI 的 FIFO 深度和 F4 系列不一样没有 16 字节 FIFO收发都是一个字节一个字节来。用 DMA 的时候要注意这一点别按 F4 的思路写。3. 硬件设计SPI 接口和 PCB 布局的实战要点3.1 引脚连接与上拉电阻配置MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准CS、SO、SI、SCK、VCC、GND另外还有两个 NC 引脚。和 STM32F100ZE 连接的时候我一般这样分配CS 接 PA4SPI1_NSS也可以用普通 GPIO 软件控制SCK 接 PA5SPI1_SCKMISO 接 PA6SPI1_MISOMOSI 接 PA7SPI1_MOSI这里有个选择硬件片选还是软件片选。STM32 的硬件 NSS 在 SPI 使能的时候会自动管理但多从机场景下容易出问题。我建议用软件片选也就是把 PA4 配成普通推挽输出手动拉低拉高。这样控制更灵活也不会因为 SPI 外设状态异常导致片选信号乱跳。CS 引脚必须加一个 10K 的上拉电阻到 VCC这是很多新手会漏掉的。MRAM 在上电过程中如果 CS 悬空或者被干扰拉低可能会误触发写操作。虽然概率不高但工业现场电磁环境复杂加上更稳妥。3.2 PCB 走线的几个硬性规则SPI 虽然速率不算特别高但 12MHz 的时钟边沿已经比较陡了走线不好照样出问题。我总结了几条必须遵守的规则第一SCK 走线尽量短不要有过孔。时钟信号对阻抗变化最敏感过孔会引入寄生电容和电感导致边沿变缓或者振铃。如果实在要换层旁边放一个接地过孔。第二MISO 和 MOSI 不要平行走太长。这两根线容易串扰尤其是 MISO 是输入线被 MOSI 干扰之后数据可能出错。如果必须平行走中间加一根地线隔离。第三MRAM 的电源引脚旁边必须放 0.1uF 去耦电容越近越好。我一般还会并一个 1uF 的应对写入瞬间的电流波动。虽然 MRAM 写入功耗比 Flash 低但瞬间电流还是有的。第四CS 走线不要和 SCK 平行。CS 是控制信号如果被 SCK 耦合可能在时钟边沿产生毛刺导致误操作。3.3 电源设计和掉电检测MR25H40CDF 的工作电压是 2.7V 到 3.6V和 STM32F100ZE 的 3.3V 系统可以直接对接不需要电平转换。但工业现场电源波动大我建议在 MRAM 的 VCC 引脚前面加一个 LC 滤波L 用磁珠C 用 10uF 和 0.1uF 并联。另外虽然 MRAM 掉电不丢数据但写入过程中掉电仍然可能导致数据不完整。所以如果系统有掉电检测需求可以在电源端加一个电压监测芯片当电压低于 2.9V 的时候触发中断MCU 在中断里完成当前写入操作。MRAM 的写入速度极快几个微秒就能完成所以只要检测及时基本不会丢数据。4. 驱动实现从 SPI 底层到 MRAM 读写封装4.1 STM32CubeMX 配置要点用 CubeMX 配置 SPI1 的时候几个关键参数这样设ModeFull-Duplex MasterData Size8 BitsClock PolarityLowClock Phase1 EdgeNSSSoftwareBaud Rate Prescaler4得到 6MHz或者 2得到 12MHzFirst BitMSB First这里重点说一下Clock Polarity 和 Clock Phase。MR25H40CDF 支持 Mode 0 和 Mode 3Mode 0 是 CPOL0、CPHA0也就是时钟空闲低电平第一个边沿采样。CubeMX 里对应 CPOLLow、CPHA1Edge。这个配置和大多数 SPI Flash 一样直接用就行。Baud Rate Prescaler 的选择要看 PCB 情况。我一般先用 4 分频6MHz调通然后再尝试 2 分频12MHz。如果 12MHz 下读写不稳定就退回 6MHz。工业项目里稳定性优先6MHz 对于每秒一次的数据记录完全够用。4.2 MRAM 命令集和读写时序MR25H40CDF 的命令集很简洁常用的就几条命令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据和 Flash 不一样的是MRAM 的 WRITE 命令不需要先擦除直接写就行。而且写完之后不需要轮询状态寄存器等待完成下一个操作可以直接开始。读时序是这样的拉低 CS发送 0x03然后发送 24 位地址MR25H40CDF 是 4Mbit需要 19 位地址但按 24 位发送高 5 位忽略然后连续读取数据每读一个字节地址自动递增。写时序类似命令换成 0x02但写之前必须先发 WREN。4.3 底层驱动代码实现先看 SPI 底层收发函数我用的是 HAL 库static uint8_t SPI1_ReadWriteByte(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(hspi1, txData, rxData, 1, 100); return rxData; }然后是 MRAM 的读函数void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_ReadWriteByte(0x03); SPI1_ReadWriteByte((addr 16) 0xFF); SPI1_ReadWriteByte((addr 8) 0xFF); SPI1_ReadWriteByte(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] SPI1_ReadWriteByte(0xFF); } MRAM_CS_HIGH(); }写函数void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_ReadWriteByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ReadWriteByte(0x02); // WRITE SPI1_ReadWriteByte((addr 16) 0xFF); SPI1_ReadWriteByte((addr 8) 0xFF); SPI1_ReadWriteByte(addr 0xFF); for (uint32_t i 0; i len; i) { SPI1_ReadWriteByte(buf[i]); } MRAM_CS_HIGH(); }这里有个细节WREN 和 WRITE 之间必须拉高 CS否则 WREN 不生效。这是 SPI 存储器的通用规则但很多人第一次写的时候会漏掉。4.4 用 DMA 提升大批量读写效率如果只是每秒写几十个字节上面的阻塞式读写完全够用。但如果要批量导出数据比如一次读几 KB用 DMA 会快很多。STM32F100ZE 的 SPI1 有独立的 TX 和 RX DMA 通道配置起来也不复杂。用 DMA 的时候要注意CS 信号必须在 DMA 传输完成之后才能拉高。我一般用 DMA 传输完成中断来拉高 CS或者在发送完地址之后启动 DMA然后在 DMA 完成回调里操作 CS。void MRAM_Read_DMA(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_ReadWriteByte(0x03); SPI1_ReadWriteByte((addr 16) 0xFF); SPI1_ReadWriteByte((addr 8) 0xFF); SPI1_ReadWriteByte(addr 0xFF); HAL_SPI_Receive_DMA(hspi1, buf, len); // CS 在 DMA 完成回调里拉高 }注意STM32F100 的 DMA 通道和 SPI 的对应关系是固定的SPI1_RX 是 DMA1_Channel2SPI1_TX 是 DMA1_Channel3。CubeMX 会自动配置但手动写的时候别搞错。5. 数据管理策略怎么把 512KB 用出花来5.1 存储空间的分区设计512KB 听起来不大但合理规划之后能存不少东西。我一般把 MRAM 分成四个区分区起始地址大小用途配置区0x000004KB设备参数、校准系数索引区0x010004KB记录索引、写指针数据区0x02000504KB实际记录数据备份区0x7F0004KB关键数据备份配置区存一些不常变但很重要的参数比如设备 ID、量程、校准系数。索引区存当前写指针、记录总数、循环覆盖标志。数据区按固定长度记录存储比如每条记录 32 字节那 504KB 能存 16128 条。备份区存一份配置区和索引区的副本防止意外改写。5.2 循环存储和磨损均衡虽然 MRAM 寿命几乎无限但为了数据管理的方便我还是会用循环存储的方式。写指针到了数据区末尾就回到开头覆盖最老的记录。这样不需要复杂的磨损均衡算法因为 MRAM 每个字节的写入寿命都是一样的不存在某个扇区先坏的问题。不过有一个点要注意索引区的写指针更新频率很高如果每次写数据都更新索引索引区的写入次数会远高于数据区。虽然 MRAM 扛得住但为了减少不必要的写入我一般会每写 16 条记录才更新一次索引中间用 RAM 缓存。掉电的时候最多丢 15 条记录的索引但数据本身还在上电后可以扫描恢复。5.3 数据完整性校验工业数据最怕的就是存进去是错的读出来还是错的。我一般会在每条记录末尾加两个字节的 CRC16 校验。写入的时候算好 CRC 一起写进去读取的时候重新算一遍对比。如果 CRC 不匹配说明这条记录损坏跳过或者标记。CRC16 的计算用查表法最快提前生成一张 256 项的表格放在 Flash 里每次计算查表就行。STM32F100ZE 的 Flash 有 512KB放一张 CRC 表绰绰有余。static const uint16_t crc16_table[256] { ... }; uint16_t CRC16_Calc(uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc16_table[((crc 8) ^ data[i]) 0xFF]; } return crc; }5.4 掉电保护的实际处理MRAM 本身掉电不丢数据但写入过程中掉电可能导致记录不完整。我的做法是每条记录写入之前先在索引区标记“写入中”写完之后再标记“写入完成”。上电初始化的时候扫描索引区如果发现“写入中”的记录说明上次写入被中断了直接丢弃这条记录。这个机制用两个字节就能实现代价很小但能保证数据的完整性。实际测试下来反复断电上电几百次没有出现过数据错乱的情况。6. 常见问题与排查技巧实录6.1 读写数据全 FF 或者全 00这是最常见的问题一般有三个原因。第一CS 信号没拉低或者拉低时间不对用示波器看 CS、SCK、MOSI 三根线确认 CS 拉低之后 SCK 才开始跳。第二SPI 模式配置错误MRAM 支持 Mode 0 和 Mode 3如果配成 Mode 1 或 Mode 2数据肯定错。第三MISO 和 MOSI 接反了这个低级错误我见过不止一次检查原理图和 PCB 走线。6.2 写入成功但读出来是旧数据这种情况通常是 WREN 没有生效。检查 WREN 和 WRITE 之间 CS 是否拉高过。另外如果 WRITE 的数据长度超过了页边界MRAM 不会像 Flash 那样回卷而是继续往下一个地址写所以不会出现回卷导致的数据覆盖。但如果地址计算错了写到了别的区域读的时候自然读不到。6.3 高速 SPI 下数据偶发错误12MHz 下如果偶发出错先降速到 6MHz 试试。如果 6MHz 稳定说明是信号完整性问题。检查 SCK 走线是否过长、是否有过孔、MISO 是否被干扰。可以在 SCK 和 MISO 上串一个 22 欧姆的电阻减缓边沿通常能解决问题。6.4 常见问题速查表现象可能原因排查方法读全 FFCS 未拉低、SPI 模式错示波器看时序读全 00MISO 断开、从机未供电万用表测电压写入无效WREN 未生效检查 CS 时序偶发错误信号完整性差降速或加串阻数据错位地址计算错误打印地址调试上电丢数据掉电时序问题加电压监测6.5 几个实用的调试技巧第一用 GPIO 翻转法测时序。在 CS 拉低和拉高的地方翻转一个空闲 GPIO用示波器同时看这个 GPIO 和 SCK能直观看到 CS 和时钟的关系。第二先写固定模式再读。比如写 0xAA、0x55 交替读出来对比能快速判断是数据线问题还是地址线问题。第三用 STM32 的 SPI 错误标志。HAL 库的 HAL_SPI_GetError 能返回 OVR、MODF 等错误初始化的时候打印出来能发现配置问题。第四MRAM 的 ID 读取。虽然 MR25H40CDF 没有标准的 JEDEC ID 命令但可以通过读固定地址的已知数据来判断通信是否正常。我一般会在出厂前往地址 0x00000 写一个魔术字上电的时候读出来对比。7. 性能实测与优化建议7.1 实际读写速度测试在 12MHz SPI 时钟下我实测的连续写入速度大约是 1.1MB/s读取速度差不多。单字节写入包含 WREN 和地址开销大约需要 5 微秒。对于每秒记录一次、每次 32 字节的场景写入耗时不到 200 微秒CPU 占用率极低。如果用 DMA 批量读取 4KB 数据耗时大约 3.7 毫秒。这个速度对于数据导出完全够用。7.2 降低功耗的几个手段工业现场很多设备是电池供电或者用超级电容功耗很关键。MRAM 的待机电流只有几微安写入电流也只有几毫安比 Flash 低不少。进一步降功耗可以这样做不操作的时候把 CS 拉高SPI 外设关掉降低 SPI 时钟到 3MHz对速度影响不大但功耗明显下降用 DMA 传输让 CPU 多睡觉。7.3 从 Flash 迁移到 MRAM 的注意事项如果你之前用的是 SPI Flash迁移到 MRAM 的时候有几个地方要改。第一去掉所有擦除相关的代码MRAM 不需要擦除。第二去掉状态轮询MRAM 写完就能读。第三地址计算要改Flash 通常按扇区管理MRAM 可以按字节随机访问。第四写入前不需要判断是否为 0xFF直接覆盖写就行。我在实际迁移的时候驱动层改动不大主要是上层的数据管理逻辑简化了很多代码量反而减少了。8. 写在最后的一些个人体会这套 MRAM 加 STM32F100ZE 的方案我在电力监测和工业数据采集场景里用了三年多累计出货几千台现场反馈很稳定。最冷的地方在东北室外最热的地方在南方工厂车间温度范围覆盖了 MRAM 的工业级规格没有出现过数据丢失的情况。如果让我给正在选型的朋友一个建议那就是先算清楚数据写入频率和寿命要求再决定用不用 MRAM。如果只是偶尔写一次配置参数EEPROM 就够了如果要频繁记录数据而且不能丢MRAM 多花的钱绝对值得。另外STM32F100ZE 这颗 MCU 虽然老但胜在便宜稳定配合 MRAM 做数据记录整体方案的成本和可靠性平衡得很好。最后分享一个小技巧MRAM 的 CS 引脚上拉电阻我试过 4.7K 和 10K10K 在大多数场景下够用但如果现场干扰特别大换成 4.7K 更稳。这个细节看起来不起眼但在电磁环境复杂的工厂里能省掉很多莫名其妙的故障排查时间。