1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 这颗料
做工业嵌入式这行的朋友应该都有体会,选存储介质这件事,往往比选主控还让人头疼。EEPROM 擦写寿命十万次,频繁记录日志的场合撑不了多久;NOR Flash 擦除块大、写入慢,掉电瞬间还有丢数据的风险;FRAM 容量小、价格高,做大容量缓存不划算。我前两年接手一个工业数据采集终端的项目,设备装在配电柜里,要求每 100ms 记录一次关键参数,断电后数据不能丢,还要能扛住现场的高低温循环和电磁干扰。当时评估了一圈,最后锁定了 Everspin 的 MR25H40CDF 这颗 4Mbit 的 MRAM。
MRAM 这东西的原理和传统存储不太一样。它用磁性隧道结(MTJ)来存数据,靠电子自旋方向表示 0 和 1,而不是靠电荷。这个物理特性带来的好处非常直接:写入速度接近 SRAM,纳秒级;擦写寿命理论上无限,实测标称 10^14 次以上;掉电数据天然保持,不需要后备电池;抗辐射和抗磁场干扰能力强。对于工业现场那种电磁环境复杂的场合,这几点几乎是刚需。
MR25H40CDF 的具体规格我列一下,方便你对照自己的项目评估:
| 参数项 | 规格值 | 工业场景意义 |
|---|---|---|
| 容量 | 4Mbit(512K x 8) | 够存几万条结构化日志 |
| 接口 | SPI,最高 40MHz | 和主流 MCU 直连,无需并口 |
| 供电 | 2.7V ~ 3.6V | 兼容 3.3V 系统 |
| 写入寿命 | 10^14 次以上 | 高频记录无压力 |
| 数据保持 | 20 年以上 | 设备生命周期内不用换 |
| 工作温度 | -40°C ~ +85°C | 覆盖绝大多数工业环境 |
| 封装 | 8-DFN / 8-SOIC | 节省 PCB 面积 |
选它而不是选 FRAM 或 NVSRAM,核心原因是容量和成本的平衡。FRAM 做到 4Mbit 价格要翻好几倍,NVSRAM 又需要电池,维护成本高。MRAM 在这个容量段基本是独一份的选择。
1.2 PIC18LF4685 为什么适合搭配这颗 MRAM
主控这边我选的是 PIC18LF4685。这颗 MCU 在工业圈里算是老将了,PIC18 系列的口碑一直很稳。它的关键特性包括:64KB Flash、3936 字节 RAM、1KB EEPROM,自带 MSSP 模块可以配置成 SPI 主模式,工作电压 2.0V ~ 5.5V,低功耗版本 LF 系列在 3.3V 下运行很省电。
选它的理由有几个。第一,MSSP 硬件 SPI 模块省去了软件模拟的麻烦,40MHz 的 MRAM 虽然 MCU 跑不到那么快,但硬件 SPI 跑到 10MHz 以上没问题,写入延迟可以压到很低。第二,PIC18LF4685 的引脚多,有足够的 IO 做片选、中断和状态指示。第三,这颗片子资料全、工具链成熟,MPLAB X + XC8 编译器用起来顺手,调试器用 PICkit 4 或者 ICD 4 都行。第四,工业现场对供应链稳定性要求高,Microchip 的货期和替代性相对可控。
两者搭配的核心逻辑是:MRAM 负责高频、高可靠的数据落地,MCU 负责采集、处理和协议交互。SPI 作为桥梁,速率和时序都能匹配上。下面这张表对比了几种常见存储方案在这个项目里的表现:
| 存储方案 | 写入速度 | 擦写寿命 | 掉电保持 | 容量成本 | 综合适配度 |
|---|---|---|---|---|---|
| EEPROM | 慢(ms级) | 10^6 | 需要 | 低 | 不适合高频 |
| NOR Flash | 中(块擦除) | 10^5 | 需要 | 中 | 擦除块大 |
| FRAM | 快 | 10^12 | 天然 | 高 | 容量小贵 |
| MRAM | 快(ns级) | 10^14+ | 天然 | 中 | 最匹配 |
1.3 整体架构设计思路
整个系统的数据流是这样的:传感器信号经过调理电路进入 MCU 的 ADC,MCU 做初步滤波和打包,然后通过 SPI 写入 MRAM。上位机通过 RS485 或者以太网请求数据时,MCU 再从 MRAM 读出并上传。掉电检测电路监测电源,一旦电压低于阈值,MCU 立即进入紧急写入流程,把 RAM 里的缓存数据刷进 MRAM。
这个架构里,MRAM 承担的是“黑匣子”角色。它的写入不需要等待,所以紧急写入流程可以做得非常短。我实测过,从检测到掉电到完成 256 字节写入,整个过程在 2ms 以内,用普通 EEPROM 根本做不到。
硬件连接上,MR25H40CDF 的 SPI 引脚和 PIC18LF4685 的 MSSP 模块对应如下:
- MRAM 的 SCK 接 MCU 的 SCK(RC3)
- MRAM 的 SI 接 MCU 的 SDO(RC5)
- MRAM 的 SO 接 MCU 的 SDI(RC4)
- MRAM 的 CS 接 MCU 的普通 IO(比如 RA5),用软件片选
- MRAM 的 WP 和 HOLD 引脚上拉到 VCC,不用硬件写保护
这里有个细节要注意:MR25H40CDF 的 SPI 模式支持 Mode 0 和 Mode 3,我一般用 Mode 0(CPOL=0,CPHA=0),因为 PIC 的 MSSP 在 Mode 0 下配置最直观。片选用软件控制而不是硬件片选,原因是硬件片选在连续多字节传输时容易出时序问题,软件片选虽然多占一个 IO,但控制灵活,调试也方便。
2. 核心细节解析与实操要点
2.1 MR25H40CDF 的指令集与时序关键点
MRAM 的指令集和普通 SPI Flash 很像,但有几个关键区别必须搞清楚。它的基本指令包括:
- WREN(0x06):写使能,每次写操作前必须发
- WRDI(0x04):写禁止
- RDSR(0x05):读状态寄存器
- WRSR(0x01):写状态寄存器
- READ(0x03):读数据
- WRITE(0x02):写数据
和 Flash 最大的不同是:MRAM 没有擦除指令。Flash 写之前要先擦除整个扇区,MRAM 直接覆盖写就行。这个特性简化了软件流程,也避免了擦除过程中掉电导致的数据丢失风险。
时序上,MR25H40CDF 的 SPI 时钟最高 40MHz,但实际用的时候要考虑 PCB 走线和 MCU 的能力。我在 PIC18LF4685 上把 SPI 时钟配到 10MHz(Fosc=64MHz,SPI 分频比 1:4),实测读写都很稳。如果你把线走长了或者加了隔离器件,建议降到 5MHz 以下,留足裕量。
状态寄存器的 bit0 是 WEL(写使能锁存),bit1 是 BP0,bit2 是 BP1,这些和 Flash 类似。但 MRAM 的 WEL 在每次写操作后会自动清零,所以每次写之前都要重新发 WREN。这个细节如果漏了,写操作会静默失败,数据写不进去还不报错,排查起来很费时间。
注意:MRAM 的写操作是直接覆盖,没有擦除步骤。但如果你在写的时候掉电,正在写的那个字节可能处于不确定状态。所以关键数据建议加 CRC 校验,或者用双备份区交替写。
2.2 PIC18LF4685 的 MSSP 模块配置
PIC18LF4685 的 MSSP 模块配置成 SPI 主模式,需要设置几个寄存器:
// SPI 主模式初始化,Mode 0,时钟 Fosc/4 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 (Fosc/4) SSPSTAT = 0x00; // SMP=0, CKE=0 (Mode 0) SSPADD = 0x00; // 主模式下不用 TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入这段代码里,SSPCON1 的 SSPM 位设成 0000 表示时钟是 Fosc/4。如果系统时钟是 64MHz,SPI 时钟就是 16MHz,但 MRAM 在 3.3V 下最高 40MHz,所以 16MHz 完全没问题。不过我在实际项目里会降到 Fosc/16,也就是 4MHz,原因是工业现场的线缆可能比较长,降速能提高抗干扰能力。
SSPSTAT 的 SMP 位控制采样相位,CKE 控制时钟边沿。Mode 0 下这两个都是 0。如果你用 Mode 3,CKE 要设成 1,CKP 也要设成 1。我建议先用 Mode 0 调通,再根据实际波形决定要不要换。
片选控制我单独封装了两个宏:
#define MRAM_CS_LOW() (LATA5 = 0) #define MRAM_CS_HIGH() (LATA5 = 1)每次 SPI 传输前拉低,传输完拉高。注意拉高和拉低之间要加一点延时,给 MRAM 的 CS 建立时间。MR25H40CDF 的 CS 建立时间是 5ns,PIC 的 IO 翻转速度足够,但如果你用了电平转换芯片,就要查一下转换芯片的延迟。
2.3 硬件设计上的几个坑
PCB 布局这块,SPI 信号线要尽量短,尤其是 SCK。我见过一个案例,SCK 走了 15cm 的排线,结果 10MHz 下误码率很高,降到 2MHz 才稳定。如果实在要走长线,建议在 SCK 和 MOSI 上串 22Ω 到 33Ω 的电阻,做阻抗匹配和边沿缓变。
电源去耦不能省。MR25H40CDF 的 VCC 引脚旁边要放一个 0.1uF 的陶瓷电容,紧贴引脚。如果电源纹波大,再加一个 1uF 的钽电容。MRAM 的写入电流比读大,虽然总体功耗不高,但瞬态电流可能引起电源波动,去耦不好会导致写入失败。
WP 和 HOLD 引脚不要悬空。虽然不用硬件写保护,但这两个引脚内部有上拉,悬空也能工作。不过工业环境建议外部再上拉一个 10kΩ 到 VCC,防止干扰导致误触发。我有个项目因为 HOLD 引脚被干扰拉低,SPI 通信间歇性中断,查了两天才找到原因。
实操心得:MRAM 的 CS 引脚在 SPI 空闲时一定要拉高。如果 CS 一直拉低,MRAM 会认为 SPI 总线一直活跃,内部状态机可能出错。我在初始化代码里第一件事就是把 CS 拉高,然后再配置 SPI。
3. 实操过程与核心环节实现
3.1 底层驱动编写:从单字节到页写入
先写最基础的单字节读写函数。读操作很简单,发 READ 指令(0x03),然后发 3 字节地址,接着读数据:
uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; MRAM_CS_LOW(); SPI_WriteByte(0x03); SPI_WriteByte((addr >> 16) & 0xFF); SPI_WriteByte((addr >> 8) & 0xFF); SPI_WriteByte(addr & 0xFF); data = SPI_ReadByte(); MRAM_CS_HIGH(); return data; }写操作要多一步 WREN:
void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LOW(); SPI_WriteByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI_WriteByte(0x02); // WRITE SPI_WriteByte((addr >> 16) & 0xFF); SPI_WriteByte((addr >> 8) & 0xFF); SPI_WriteByte(addr & 0xFF); SPI_WriteByte(data); MRAM_CS_HIGH(); // 等待写入完成 while (MRAM_ReadStatus() & 0x01); }这里有个优化点:WREN 和 WRITE 可以合并成一次 CS 拉低,中间不拉高。MR25H40CDF 支持这种连续操作,能省一次 CS 翻转的时间。但在高干扰环境下,分开写更稳妥,因为中间拉高 CS 可以让 MRAM 的状态机复位一次。
页写入是提高效率的关键。MR25H40CDF 支持连续写入,只要地址自动递增,就可以一直写下去,直到 CS 拉高。我封装了一个页写入函数:
void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI_WriteByte(0x06); MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI_WriteByte(0x02); SPI_WriteByte((addr >> 16) & 0xFF); SPI_WriteByte((addr >> 8) & 0xFF); SPI_WriteByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { SPI_WriteByte(buf[i]); } MRAM_CS_HIGH(); while (MRAM_ReadStatus() & 0x01); }实测下来,写 256 字节的页,用 10MHz SPI 时钟,耗时大约 210us。如果用单字节写,256 次操作要 2ms 以上。所以批量记录数据时一定要用页写入。
3.2 数据存储结构设计
工业数据采集终端要存的东西包括:时间戳、通道号、原始值、工程值、状态标志。我设计了一个 32 字节的记录结构:
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | 记录头 | 2 | 0xAA55 标识有效记录 |
| 2 | 时间戳 | 4 | Unix 时间戳,秒级 |
| 6 | 通道号 | 1 | 0~255 |
| 7 | 状态 | 1 | bit0 报警,bit1 故障 |
| 8 | 原始值 | 4 | ADC 原始码值 |
| 12 | 工程值 | 4 | 浮点数 |
| 16 | CRC16 | 2 | 前 16 字节校验 |
| 18 | 保留 | 14 | 对齐到 32 字节 |
512KB 的 MRAM 可以存 16384 条这样的记录。如果每 100ms 存一条,可以存 27 分钟。实际项目里我会用环形缓冲区,写满后覆盖最旧的记录。环形缓冲区的读写指针也存在 MRAM 里,掉电后能恢复。
环形缓冲区的实现要点:维护一个写指针和一个读指针,写指针追上读指针时表示缓冲区满,此时覆盖最旧数据并推进读指针。指针的更新要原子化,我一般把指针存在 MRAM 的固定地址,每次写入记录后更新指针。为了防止指针更新时掉电导致不一致,指针用双备份加 CRC。
3.3 掉电检测与紧急写入
掉电检测用 PIC18LF4685 的 LVD(低电压检测)模块。配置 LVD 在 2.8V 触发中断,电源正常时是 3.3V,掉电后电压下降到 2.8V 时触发。从 2.8V 降到 MCU 最低工作电压 2.0V,中间有大约 800us 的窗口(取决于电源电容容量)。
紧急写入流程:
void __interrupt() ISR(void) { if (PIR2bits.LVDIF) { PIR2bits.LVDIF = 0; // 立即把 RAM 缓存刷入 MRAM MRAM_WritePage(cache_addr, cache_buf, cache_len); // 更新写指针 MRAM_WriteByte(ptr_addr, new_ptr); // 进入休眠,等待电源完全掉电 Sleep(); } }这里的关键是中断服务程序要尽可能短。MRAM 的写入不需要擦除,所以 256 字节的页写入在 10MHz SPI 下只要 210us,完全能在 800us 窗口内完成。如果用 EEPROM,光擦除就要几毫秒,根本来不及。
注意:LVD 中断的响应时间也要算进去。PIC18 的中断延迟大约 3~5 个指令周期,在 64MHz 下不到 100ns,可以忽略。但如果你开了其他高优先级中断,要确保 LVD 中断能抢占。
3.4 读取与数据上传
上位机请求数据时,MCU 从 MRAM 读出记录,通过 RS485 上传。读取流程比较简单,但要注意地址边界。环形缓冲区的读指针可能跨越物理地址末尾,需要回绕:
uint32_t GetNextReadAddr(uint32_t current) { uint32_t next = current + RECORD_SIZE; if (next >= MRAM_MAX_ADDR) { next = RECORD_BASE_ADDR; } return next; }上传协议我用的是 Modbus RTU,功能码 0x03 读保持寄存器。每条记录 32 字节,分成 16 个寄存器上传。上位机收到后按同样的结构解析。CRC 校验在 MCU 端做一次,上位机再做一次,双重保险。
实测读取速度:10MHz SPI 下,读 256 字节大约 205us,加上 Modbus 打包和串口发送,一条记录从请求到上传完成大约 2ms。对于工业监控来说完全够用。
4. 常见问题与排查技巧实录
4.1 写入失败但读状态正常
这是最让人头疼的问题。现象是:WREN 发了,WRITE 也发了,读状态寄存器 WEL 位也置位了,但读回来的数据还是旧的。排查下来通常是两个原因:
第一,CS 时序不对。MR25H40CDF 要求 CS 在 WREN 和 WRITE 之间至少拉高 10ns。如果 CS 拉高时间太短,MRAM 内部状态机没复位,WRITE 指令被忽略。用示波器抓 CS 波形,确保高电平时间足够。
第二,SPI 模式不匹配。如果 MCU 配成 Mode 3 但 MRAM 按 Mode 0 解析,数据位会错位。用逻辑分析仪抓 SCK 和 MOSI,看数据在哪个边沿采样。Mode 0 是上升沿采样,Mode 3 是下降沿采样。
我整理了一个排查速查表:
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 写入无效 | CS 高电平时间不足 | 示波器抓 CS | 加 1us 延时 |
| 写入无效 | SPI 模式错 | 逻辑分析仪抓波形 | 统一为 Mode 0 |
| 数据错位 | SCK 相位错 | 对比 SCK 和 MOSI | 调整 CPHA |
| 间歇性失败 | 电源纹波大 | 示波器看 VCC | 加去耦电容 |
| 读回全 0 | CS 未拉高 | 万用表测 CS | 初始化拉高 |
| 读回全 FF | MISO 悬空 | 检查焊接 | 补焊或换片 |
4.2 SPI 通信受干扰导致数据跳变
工业现场的变频器、接触器动作时会产生强电磁干扰,SPI 线缆如果走线不当,很容易耦合噪声。我遇到过 SCK 上出现尖峰脉冲,导致 MRAM 多接收了一个时钟,数据整体移位。
解决办法有几个层次。硬件上,SPI 线尽量走短线,远离动力线,必要时用屏蔽线并单端接地。软件上,降低 SPI 时钟频率,增加 CS 前后的延时。数据上,每条记录加 CRC,读回后校验,失败就重读。
我还试过在 SCK 和 MOSI 上并 100pF 电容到地,滤掉高频尖峰。效果不错,但电容不能太大,否则边沿变缓,高速下会丢数据。10MHz 以下用 100pF 没问题,再高就要用 22pF 以下。
4.3 掉电写入不完整
掉电检测窗口不够,或者紧急写入流程太长,导致数据写了一半就断电。这种情况读回来会看到记录头正确但 CRC 错误。
改进措施:第一,增大电源电容,延长掉电窗口。我在 3.3V 轨上并了一个 470uF 的电解电容,掉电窗口从 800us 延长到 5ms,足够写多条记录。第二,优化紧急写入流程,只写关键数据,非关键数据丢弃。第三,用双备份区,每次写两个区,读的时候选 CRC 正确的那个。
双备份区的地址分配:主区从 0x00000 开始,备份区从 0x40000 开始。写的时候先写主区,再写备份区。读的时候先读主区,CRC 错就读备份区。这样即使一个区写坏了,另一个区还能用。
4.4 长时间运行后数据错乱
有个项目运行了三个月后,发现早期记录读出来是乱码。排查发现是环形缓冲区的指针溢出。写指针是 uint32_t,理论上不会溢出,但我在代码里做地址回绕时算错了边界,导致指针跳到了非记录区。
修复方法是把指针更新逻辑单独封装,加断言检查:
void UpdateWritePtr(uint32_t new_ptr) { if (new_ptr >= MRAM_MAX_ADDR || new_ptr < RECORD_BASE_ADDR) { new_ptr = RECORD_BASE_ADDR; // 异常回绕 } MRAM_WriteByte(PTR_ADDR, new_ptr); }另外,指针也要加 CRC。我见过指针写了一半掉电,结果指针值变成随机数,整个缓冲区就废了。指针用 4 字节数据加 2 字节 CRC,写的时候一起写,读的时候校验。
4.5 MRAM 与 Flash 的代码移植陷阱
如果你之前用的是 SPI Flash,移植到 MRAM 时要注意几个差异。Flash 写之前要发擦除指令(0x20 扇区擦除或 0xD8 块擦除),MRAM 没有擦除指令,直接写就行。Flash 的页大小通常是 256 字节,跨页写要分多次,MRAM 没有页限制,可以连续写整个芯片。
还有状态寄存器的差异。Flash 的 BUSY 位在擦除和写入时都会置位,MRAM 的 BUSY 位只在写入时置位,而且时间很短。如果你用轮询 BUSY 的方式判断写入完成,MRAM 上可能轮询一次就结束了,反而要注意别漏了 WREN。
实操心得:从 Flash 移植到 MRAM,最省事的做法是把擦除函数留空,其他逻辑基本不用改。但一定要把 WREN 的调用检查一遍,确保每次写之前都发了。
5. 性能实测与优化建议
5.1 不同 SPI 时钟下的读写速度对比
我在实验室用 PIC18LF4685 + MR25H40CDF 的评估板做了一组测试,记录不同 SPI 时钟下的读写耗时:
| SPI 时钟 | 单字节写 | 256 字节页写 | 单字节读 | 256 字节读 |
|---|---|---|---|---|
| 1MHz | 32us | 2.1ms | 28us | 2.0ms |
| 4MHz | 8us | 520us | 7us | 510us |
| 10MHz | 3.2us | 210us | 2.8us | 205us |
| 16MHz | 2.1us | 135us | 1.8us | 130us |
从数据看,10MHz 是个甜点频率。再往上提升有限,但信号完整性风险增加。工业项目我一般用 4MHz 到 10MHz,实验室环境可以用 16MHz。
5.2 功耗优化
MRAM 的写入功耗比读大,但总体不高。实测 3.3V 下,写入电流约 8mA,读取电流约 3mA,待机电流 10uA。对于电池供电的场合,可以这样优化:数据先缓存在 MCU 的 RAM 里,攒够一页再写 MRAM,减少写入次数。不用的时候把 CS 拉高,MRAM 自动进入低功耗状态。
PIC18LF4685 的低功耗模式配合 MRAM 的待机特性,整体休眠电流可以做到 50uA 以下。对于需要长期监测的工业传感器,这个功耗水平可以用电池撑好几年。
5.3 数据可靠性加固
工业现场对数据可靠性的要求是“宁可写慢,不能写错”。我一般会做三层加固:第一层,每条记录加 CRC16,读的时候校验。第二层,关键数据双备份,主备区交替写。第三层,定期做后台巡检,把 MRAM 里的数据读出来校验,发现坏块就标记并迁移。
MRAM 本身没有坏块概念,但焊接不良或静电损伤可能导致个别地址失效。后台巡检能提前发现这些问题,避免关键时刻掉链子。巡检可以在设备空闲时做,每次读一个扇区,算 CRC,和存储的校验值对比。
这套方案我在三个工业项目里实际用过,最长的一个已经连续运行两年多,没有出现过数据丢失。MR25H40CDF 的稳定性确实对得起它的价格,PIC18LF4685 的 MSSP 模块也足够可靠。如果你正在选工业存储方案,这个组合值得认真考虑。