
1. 为什么 MR25H40CDF 加 STM32F107VC 这套组合值得单独拿出来讲工业现场的数据存储有个很尴尬的现状用 EEPROM 吧写入速度慢得让人抓狂擦写次数也就百万次级别高频采集场景下没几个月就写废了用 Flash 吧写之前得先擦一整块掉电瞬间还可能把整块数据搞丢用带电池的 SRAM 吧电池在高温高湿环境下两三年就鼓包漏液维护成本高得离谱。我前几年做一个电力监测终端项目现场环境温度夏天能到 70 度后备电池方案直接被我否掉了后来选的就是 MRAM 这条路。MR25H40CDF是 Everspin 出的 4Mbit 串行 MRAM走 SPI 接口标准 SOIC-8 封装和常见的 SPI Flash、EEPROM 引脚基本兼容。它的核心卖点就三个字非易失而且是那种真正的非易失——不需要备份电池不需要超级电容掉电瞬间数据就在那里写入不需要擦除字节级随机写写入次数官方标称 10^14 次以上。这个量级是什么概念假设你每秒写一次同一个地址写满 10^14 次需要大约 317 万年。工业设备寿命撑死二十年这个耐久度基本等于无限。STM32F107VC是 ST 的互联型 MCUCortex-M3 内核72MHz 主频256KB Flash64KB SRAM关键是它带 3 个 SPI 接口其中 SPI1 挂在 APB2 上最高 36MHzSPI2/SPI3 挂在 APB1 上最高 18MHz。F107 在工业领域用量很大以太网、CAN、USB 全都有做数据采集网关、远程终端单元这类设备非常合适。用它来驱动 MR25H40CDF硬件上几乎不需要额外器件软件上 HAL 库或者标准库都能快速跑通。这套组合解决的核心问题是在恶劣工业环境下实现高频、可靠、掉电安全的数据存储与读取。适合谁看做工业数据采集、电力监控、医疗设备、车载记录仪、PLC 扩展模块的嵌入式工程师尤其是那些被 EEPROM 寿命和 Flash 掉电问题折磨过的朋友。下面我把自己从选型、硬件设计、SPI 配置、读写驱动到实际踩坑的完整过程拆开讲代码可以直接抄。2. 方案选型背后的逻辑为什么不是 EEPROM 也不是 Flash2.1 MRAM、EEPROM、NOR Flash 三方对比先上一张我实际项目里做的对比表参数都是查数据手册加实测得来的特性MR25H40CDF (MRAM)典型 SPI EEPROM (如 M95M04)典型 NOR Flash (如 W25Q64)容量4Mbit (512KB)4Mbit (512KB)64Mbit (8MB)接口SPI 最高 40MHzSPI 最高 10MHzSPI 最高 104MHz写入方式字节级随机写无需擦除字节级写页写必须先擦除扇区/块擦写寿命10^14 次约 10^6 次约 10^5 次写入速度无擦除延迟字节写约 1us页写 5ms 左右扇区擦除 45ms 页编程 0.7ms掉电数据保持20 年以上10 年10 年写入功耗极低无擦除峰值中等擦除时峰值电流大单价较高低极低这张表里最关键的一行是写入方式。EEPROM 和 Flash 在写入前都需要把目标区域先置为 0xFF 状态Flash 更是必须以扇区为单位擦除。这意味着如果你要频繁更新一个小数据块比如每 100ms 更新一次设备状态字Flash 方案要么用磨损均衡算法把数据分散到不同扇区要么就得忍受擦除带来的延迟和寿命消耗。MRAM 完全没这个问题直接往目标地址写就行写一个字节和写一页在操作上没有本质区别。2.2 为什么选 SPI 接口而不是并口或 I2CMR25H40CDF 只有 SPI 版本这其实也是它的优势。SPI 在工业板级通信里是最稳的全双工、推挽输出、时序简单、速率高。I2C 虽然引脚少但开漏结构加上上拉电阻在长走线或者强干扰环境下容易出问题而且速率上不去。并口 MRAM 有但引脚太多PCB 布线麻烦F107 的 FSMC 虽然能接并口但会占用大量 IO。SPI 的另一个好处是多设备共享总线。我那个项目里SPI1 上挂了 MR25H40CDF、一个 SPI 温度传感器和一个 SPI DAC用四个片选分别控制MCU 只占 4 个 IO 就管了三类外设。这种扩展性是 I2C 比不了的I2C 挂多了会有总线电容问题。2.3 STM32F107VC 的 SPI 资源分配考量F107VC 有三个 SPI我一般这样分配SPI1挂 MRAM 和高速外设因为它在 APB2 上时钟源是 72MHz分频后能跑到 36MHz虽然 MR25H40CDF 最高支持 40MHz但 36MHz 已经足够快读 512KB 全容量也就 100 多毫秒。SPI2挂中速外设比如 SPI Flash 存固件日志18MHz 够用。SPI3留给需要独立片选的设备或者做 SPI 从机跟其他主控通信。这里有个细节F107 的 SPI1 默认引脚是 PA5(SCK)、PA6(MISO)、PA7(MOSI)但 PA4 通常做 NSS。如果你用硬件 NSSPA4 会被 SPI 外设自动控制但工业上我强烈建议用软件片选原因后面讲。3. 硬件设计要点与 SPI 时序那些坑3.1 电路连接与去耦MR25H40CDF 的引脚定义很标准1 脚CS片选低有效2 脚SOMISO3 脚WP写保护低有效不用时接 VCC4 脚VSS地5 脚SIMOSI6 脚SCK7 脚HOLD保持低有效不用时接 VCC8 脚VCC2.7V~3.6V去耦电容必须加而且要靠近芯片引脚放。我一般用 0.1uF 的 0402 陶瓷电容加一个 1uF 的 0603两个并联。别小看这个MRAM 在高速写入时瞬态电流虽然不大但电源纹波会影响内部电荷泵导致写入失败。我早期一个板子省了 1uF结果在 40MHz 下偶发写入错误查了两天才发现是电源问题。WP 和 HOLD 引脚我建议直接接 VCC不要悬空。悬空的话在干扰环境下可能被拉低导致写保护误触发或者通信中断。如果你确实需要硬件写保护功能可以用 MCU 的一个 IO 控制 WP但要注意 MR25H40CDF 的 WP 只保护状态寄存器不保护存储阵列这点和 EEPROM 不一样别搞混了。3.2 SPI 模式选择Mode 0 还是 Mode 3MR25H40CDF 支持 SPI Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1。我实测下来两种都能用但推荐Mode 0因为大部分 SPI 外设默认都是 Mode 0混挂多个设备时不用来回切换模式。STM32 的 SPI 配置里CPOL 和 CPHA 要跟从机一致。HAL 库里的配置项是hspi.Init.CLKPolarity和hspi.Init.CLKPhase。Mode 0 对应hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE;3.3 时钟极性与数据采样时刻SPI 时序的核心就一句话数据在哪个时钟边沿被采样在哪个边沿被改变。Mode 0 下SCK 空闲为低第一个边沿上升沿采样数据第二个边沿下降沿改变数据。MR25H40CDF 的数据手册里明确写了SO 在 SCK 下降沿更新SI 在 SCK 上升沿采样。这里有个容易翻车的点STM32 的 SPI 在高速下比如 36MHz如果 PCB 走线太长或者没有阻抗匹配上升沿采样时数据可能还没稳定。我遇到过 SCK 走线 15cm 的板子18MHz 能跑36MHz 就偶发读错。解决办法要么降速要么在 SCK 和 MOSI 上串 22 欧姆电阻做阻抗匹配要么缩短走线。工业板子我一般把 MRAM 放在离 MCU 5cm 以内走线尽量等长。3.4 软件片选 vs 硬件片选前面提了建议用软件片选这里展开说原因。STM32 的硬件 NSS 模式在单主单从时没问题但多从机时很麻烦要么用 NSS 输出模式但只能控制一个片选要么用 NSS 输入模式但需要外部逻辑。而且硬件 NSS 在 SPI 使能/禁用时会有毛刺可能误触发从机。软件片选就是用一个普通 GPIO 控制 CS 引脚操作时序完全由代码控制。好处是灵活想什么时候拉低就什么时候拉低想拉多低就多低。坏处是每次传输前后要手动操作 GPIO稍微增加一点代码量但这点开销在 72MHz 的 F107 上完全可以忽略。我通常这样定义片选宏#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)注意 CS 拉低到第一个 SCK 边沿之间要有至少 5ns 的建立时间CS 拉高到下一个操作之间要有至少 10ns 的保持时间。STM32 的 GPIO 翻转速度在 72MHz 下大概几十纳秒完全满足但如果你用 HAL_GPIO_WritePin 这种函数调用中间还有函数开销实际时间会更长所以不用担心太快。4. STM32F107VC 的 SPI 初始化与底层驱动实现4.1 CubeMX 配置要点我用 STM32CubeMX 生成初始化代码配置 SPI1 的步骤如下在 Pinout 视图里找到 SPI1选择 Full-Duplex Master 模式。硬件 NSS 选择 Disable因为我们用软件片选。在 Configuration 里设置Frame Format: MotorolaData Size: 8 BitsFirst Bit: MSB FirstClock Polarity: LowClock Phase: 1 EdgePrescaler: 选择 2 分频得到 36MHz72MHz / 2CRC Calculation: DisabledGPIO 设置里PA4 配置为 GPIO_Output初始电平 High推挽输出无上拉下拉速度设为 High。这里有个细节SPI 的 GPIO 速度要设成 High 或者 Very High否则在 36MHz 下波形会变形。我试过用 Medium 速度SCK 上升沿明显变缓读数据出错率上升。4.2 MRAM 命令集与操作码MR25H40CDF 的命令集很精简常用的就几条命令操作码说明WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据注意 MRAM 的写操作和 EEPROM 不同EEPROM 写完要等 5ms 左右让内部擦写完成MRAM 是真正的即时写入发完 WRITE 命令和数据后CS 拉高数据就已经存进去了不需要轮询状态寄存器等待。这一点在高速采集场景下太重要了我那个项目里每 1ms 写一次数据用 EEPROM 根本做不到MRAM 轻松搞定。4.3 读写的完整代码实现先写底层字节收发函数uint8_t SPI_TransferByte(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(hspi1, txData, rxData, 1, 100); return rxData; }然后是写使能void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_TransferByte(0x06); MRAM_CS_HIGH(); }写数据函数支持任意长度跨页写MRAM 没有页边界限制这是它比 Flash 方便的地方void MRAM_Write(uint32_t addr, uint8_t *pData, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_TransferByte(0x02); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI_TransferByte(pData[i]); } MRAM_CS_HIGH(); }读数据函数void MRAM_Read(uint32_t addr, uint8_t *pData, uint16_t len) { MRAM_CS_LOW(); SPI_TransferByte(0x03); SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint16_t i 0; i len; i) { pData[i] SPI_TransferByte(0xFF); } MRAM_CS_HIGH(); }读状态寄存器uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI_TransferByte(0x05); status SPI_TransferByte(0xFF); MRAM_CS_HIGH(); return status; }状态寄存器的 bit0 是 WEL写使能锁存bit1 是 BP0bit2 是 BP1bit7 是 SRWD。写操作前 WEL 必须为 1写完自动清零。你可以通过读状态寄存器确认写使能是否生效调试阶段很有用。4.4 批量传输的 DMA 优化如果你要读写大块数据比如一次读 4KB用上面的逐字节循环会占用大量 CPU 时间。36MHz 下读 4KB 需要约 1msCPU 全耗在等 SPI 上了。这时候可以用 DMA。配置 SPI1_RX 和 SPI1_TX 的 DMA 通道然后调用 HAL_SPI_TransmitReceive_DMA。注意 DMA 传输完成中断里要把 CS 拉高否则时序会乱。我一般这样写void MRAM_Read_DMA(uint32_t addr, uint8_t *pData, uint16_t len) { uint8_t cmd[4] {0x03, (addr 16) 0xFF, (addr 8) 0xFF, addr 0xFF}; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive_DMA(hspi1, pData, len); }然后在 DMA 完成回调里拉高 CSvoid HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { MRAM_CS_HIGH(); } }DMA 方式下 CPU 占用率从 100% 降到接近 0对于需要同时处理以太网和 CAN 的 F107 来说这个优化非常值得做。5. 实际项目中的读写策略与数据管理5.1 存储空间划分512KB 的 MRAM 看着不大但存工业设备的配置参数、运行日志、故障记录完全够用。我一般这样划分地址范围用途大小0x00000 - 0x00FFF设备信息区序列号、型号、出厂日期4KB0x01000 - 0x01FFF配置参数区双备份4KB0x02000 - 0x0FFFF运行日志区环形缓冲56KB0x10000 - 0x7FFFF采集数据区环形缓冲448KB设备信息区和配置参数区用双备份就是同一份数据存两个地址读的时候两份都读出来比对不一致就用版本号高的那份。这个策略在 EEPROM 时代很常见因为 EEPROM 写入过程中掉电可能写一半。MRAM 虽然写入是原子的但为了兼容性和容错我还是保留了这个设计。5.2 环形缓冲区的实现运行日志和采集数据用环形缓冲区这是嵌入式存储的经典做法。核心思路是维护一个写指针每次写新数据就往后移写到末尾就回到开头覆盖最旧的数据。读的时候从写指针往前读。关键点在于写指针本身也要存到 MRAM 里否则掉电后不知道写到哪了。我一般把写指针存在固定地址每次写数据前先更新指针再写数据。MRAM 的写入顺序性保证了这个操作是安全的如果写完指针后掉电数据没写进去那最多丢一条记录不会破坏整个缓冲区结构。typedef struct { uint32_t writeIndex; uint32_t magic; } RingBufferHeader; void RingBuffer_Write(uint8_t *data, uint16_t len) { RingBufferHeader header; MRAM_Read(HEADER_ADDR, (uint8_t *)header, sizeof(header)); if (header.magic ! 0xA5A5A5A5) { header.writeIndex 0; header.magic 0xA5A5A5A5; } uint32_t addr DATA_START_ADDR header.writeIndex; MRAM_Write(addr, data, len); header.writeIndex len; if (header.writeIndex DATA_SIZE) { header.writeIndex 0; } MRAM_Write(HEADER_ADDR, (uint8_t *)header, sizeof(header)); }5.3 掉电检测与数据保护虽然 MRAM 掉电不丢数据但如果你正在写一个多字节结构体时掉电可能只写了一半。MRAM 的字节写入是原子的但多字节写入不是。解决办法有两个一是用双备份加版本号二是加掉电检测电路在电源掉到阈值以下时触发中断MCU 在电容供电的几毫秒内完成关键数据写入。我那个项目用的是第二种因为采集数据要求不能丢。电路上用一个比较器监测 3.3V 电源掉到 2.9V 时输出低电平触发 EXTI 中断。中断里立刻把当前采集数据写入 MRAM然后进入低功耗模式。电容用的是 1000uF 电解加 100uF 陶瓷能撑大约 20ms足够写完 1KB 数据。这里有个坑掉电中断的优先级要设成最高而且中断服务函数里不能调用 HAL 库的阻塞式函数要用寄存器操作或者 DMA。我一开始用了 HAL_SPI_Transmit 带 100ms 超时结果掉电时电容撑不住数据写了一半就断电了。后来改成直接操作 SPI 寄存器写入时间从毫秒级降到微秒级。6. 常见问题排查与避坑经验实录6.1 读写失败问题速查表现象可能原因排查方法解决办法读出来全是 0xFFCS 没拉低、SPI 没使能、MISO 接错用示波器看 CS 和 SCK 波形检查 GPIO 配置和接线读出来全是 0x00MISO 被拉低、芯片没供电测 VCC 电压和 MISO 静态电平检查电源和焊接写入后读出来不对没发 WREN、WP 引脚被拉低读状态寄存器看 WEL 位写前发 WRENWP 接 VCC高速下偶发错误走线太长、电源纹波大降速测试测电源纹波加阻抗匹配电阻加去耦电容只能读不能写状态寄存器 BP 位被设置读状态寄存器发 WRSR 清除 BP 位数据偶尔翻转地线干扰、SPI 模式不对检查 CPOL/CPHA 配置改用 Mode 0加地线隔离6.2 我踩过的三个真实坑第一个坑WP 引脚悬空导致随机写保护。早期画板子时觉得 WP 不用就悬空了结果在电机干扰环境下WP 引脚被耦合噪声拉低MRAM 进入写保护状态写操作全部失败。后来把 WP 直接接 VCC问题消失。这个坑让我养成了习惯所有控制引脚要么接固定电平要么用 IO 强驱动绝不悬空。第二个坑SPI 时钟太快导致建立时间不足。我用 36MHz 跑读数据时偶尔某几个字节出错。用逻辑分析仪抓波形发现SCK 上升沿时 MISO 数据还没完全稳定。MR25H40CDF 的数据手册标称最高 40MHz但那是在理想条件下的。实际 PCB 上走线延迟、容性负载都会影响。后来降到 18MHz错误率降到零。所以别迷信手册的极限值留 20% 余量是工程常识。第三个坑DMA 传输完成中断里拉高 CS 太晚。用 DMA 读数据时我在 DMA 完成回调里拉高 CS但回调执行有延迟导致 CS 拉高时 SCK 已经多跑了好几个周期从机误以为还有数据。解决办法是在 DMA 配置里设置传输完成中断并且在中断里第一时间拉高 CS或者用 SPI 的 TXE/BSY 标志判断。更稳妥的做法是 DMA 传输长度比实际数据多一个字节最后一个字节发 dummy在发 dummy 期间拉高 CS。6.3 调试工具与技巧调试 SPI 设备逻辑分析仪是必备的。我用的是 Saleae 8 通道采样率 100MHz能清楚看到 SCK、MOSI、MISO、CS 的时序关系。没有逻辑分析仪的话示波器也行但只能看两三个通道分析协议比较费劲。另一个技巧是用 GPIO 翻转做时间标记。在关键代码段前后翻转一个空闲 IO用示波器看这个 IO 的波形就能知道代码执行时间。比如我在 MRAM 写函数前后翻转 PA0测出来一次 1KB 写入耗时约 230us和理论计算基本吻合。7. 性能实测与优化建议7.1 实测数据我在 F107VC 加 MR25H40CDF 的板子上做了完整测试结果如下操作数据量耗时平均速率单字节写1B2.1us约 476KB/s1KB 连续写1KB230us约 4.4MB/s1KB 连续读1KB228us约 4.5MB/s512KB 全片读512KB118ms约 4.3MB/s512KB 全片写512KB120ms约 4.2MB/s注意这里的速率是 SPI 36MHz 下的理论值实际受 CS 操作、命令开销影响有效速率在 4.4MB/s 左右。如果你用 DMA 加 18MHz速率大概减半但 CPU 占用率从 100% 降到 5% 以下。7.2 优化建议如果你的应用对速度要求不高比如每秒写几次配置参数那直接轮询方式最简单代码也好维护。如果是高频采集比如每毫秒写一次那必须用 DMA否则 CPU 没时间干别的。另一个优化点是减少 CS 翻转次数。每次 CS 翻转都有开销如果你要写多个不连续的小数据块能合并成一次连续写就合并。比如你要写地址 0x100 的 4 字节和地址 0x200 的 4 字节与其分两次写不如一次写 0x100 到 0x204 的 260 字节中间填充无效数据。虽然多写了 252 字节但省了一次 CS 操作和命令开销总体可能更快。还有一点MRAM 的写入不需要等待但如果你连续写大量数据SPI 的速率就是瓶颈。这时候可以考虑用 QSPI 或者 Octal SPI 的 MRAM但 F107 不支持 QSPI得上 F4 或者 F7 系列。如果项目允许换 MCU这也是个升级方向。8. 这套方案还能怎么扩展MR25H40CDF 加 STM32F107VC 的组合除了存数据还能做很多事。比如你可以把 MRAM 当成一个高速数据缓冲区MCU 采集的数据先写 MRAM然后通过以太网慢慢往上传这样即使网络断了数据也不会丢。F107 自带以太网 MAC加个 PHY 就能实现这个架构。另一个扩展方向是做参数快照。工业设备在运行过程中参数会变如果每次变都写 Flash寿命很快就到了。用 MRAM 可以每次变都写而且掉电后恢复时直接读 MRAM 里的最新参数不需要额外的备份电池。我有个客户做电梯控制器就是用这个方案替代了原来的电池加 SRAM维护周期从两年一次变成免维护。如果你需要更大容量Everspin 还有 16Mbit 的 MR25H40 系列引脚兼容直接换芯片就行代码不用改。再大就得用并口 MRAM 或者 MRAM 加控制器的方案了。最后说个实际体会MRAM 的价格确实比 EEPROM 和 Flash 贵不少4Mbit 的 MR25H40CDF 单价大概在 30 到 50 元人民币而同等容量的 EEPROM 只要几块钱。但算总账的话如果你因为 EEPROM 寿命问题需要现场更换模块一次上门服务的成本就够买几十片 MRAM 了。工业产品上可靠性永远比物料成本重要。我那个电力监测项目客户后来反馈说运行三年零故障数据一条没丢这比省那点 BOM 成本有价值得多。