☰
MRAM替代EEPROM与Flash:工业数据掉电存储的选型与SPI驱动实践
2026/10/4 6:00:54 网站建设 项目流程

做工业设备的朋友应该都遇到过这种场景:现场调试一切正常,机器一断电再上电,运行参数、故障代码、累计数据莫名其妙丢了一部分,客户直接一个电话打到售后。我在设计一套用于产线环境的数据记录板时,试过EEPROM、试过SPI Flash,最后换成了Everspin的MR25H40CDF磁阻随机存储器和Microchip PIC18F86J50这套组合,把存储和读取数据这件事做踏实了。这篇把选型逻辑、硬件电路、SPI驱动到掉电保护完整串一遍,既适合做嵌入式开发的同学参考,也适合在做架构选型时对非易失性存储拿不准的工程师。

1. 为什么我用 MRAM 做工业数据存储

1.1 这个项目要解决的痛点

先说清楚项目背景。我要做的是一块工业数据记录板,装在产线设备上,用于实时记录设备运行状态、电机电流、振动温度特征值和故障代码。这类设备有个很要命的特征:可能在任何时刻被现场人员直接关电,或者因为电网波动瞬间复位。设备重启前后,系统必须能从非易失存储里恢复最近一次运行参数,并且把断电前最后几十条日志完整保留下来。

用传统方案的时候我吃过不少亏。最开始用I2C接口的24C系列EEPROM,几百Kbit容量勉强够用,但写寿命在十万到百万次量级。数据记录板天天高频采集,一天写几千次,一年多就接近寿命上限,后面写进去的数据开始出现偶发丢失。后来换SPI NOR Flash,寿命倒是还行,但写之前必须先擦除,一个扇区擦除就是几十毫秒,掉电那一刻正好撞上擦除窗口的话,整块区域数据就直接废了。而且还要维护坏块、扇区边界、磨损均衡,一个简单的“存储和读取数据”需求被搞得异常复杂。

MR25H40CDF就是在这种背景下引入的。它是一颗4Mbit的SPI接口磁阻随机存储器,写入次数标称10^16次,写数据不需要先擦除旧内容,直接覆盖,写入速度接近总线速率,数据保持20年以上。对工业数据记录这种“频繁写、随机写、可能掉电写得一半”的场景,它几乎是量身定做。这套方案真正落地后,板子存储部分再没出过幺蛾子。

1.2 MRAM、Flash、EEPROM、FRAM 的横向对比

很多朋友一听非易失存储就说“用Flash就行了”,但在工业嵌入式场景里,还真不能一概而论。我做选型时做了个对比表,写寿命和掉电安全性是最关键的两列。

特性串行EEPROMSPI NOR FlashFRAMMRAM(MR25H40CDF)
写次数10^5~10^610^5~10^6(块擦除后)10^1210^16
写前擦除不需要必须擦除不需要不需要
写速度字节写ms级页编程ms级接近总线速度接近总线速度
掉电半程写入风险低高低极低
容量/成本小容量成本低大容量成本低中等偏贵中等偏贵
工业温度支持支持受温度影响较大-40~+85℃以上

FRAM其实也不错,写入速度快、寿命长,但高温下铁电材料的保持特性会变差,而且大容量型号不多。MRAM的存储单元是磁隧道结,数据由磁化方向决定,不存在电荷泄漏问题,高温下数据保持能力依然稳定。我在这颗MRAM上做过85℃温度循环测试,数据没有任何漂移,这对产线环境来说很重要。

有一点必须强调:MRAM写操作不需要电荷泵,没有擦除升压过程,所以写入过程中掉电不会出现位单元卡在中间状态的翻车情况。Flash那种“写到一半掉电→数据变0xFF或变随机值”的问题,在MRAM上根本不存在。

1.3 主控为什么选 PIC18F86J50

主控选择其实没有太多悬念。MR25H40CDF是3.3V供电的SPI器件,所以主控最好也是3.3V逻辑,避免电平转换。PIC18F86J50是Microchip的80引脚8位单片机,工作电压2.0~3.6V,和MRAM同电源轨,SPI电平天然匹配。

这颗MCU内置MSSP1硬件SPI模块,做主从通信都不需要软件模拟时序,中断标志也很完整。更重要的是它自带USB外设,数据记录板采集完的数据可以直接通过USB导出到PC分析,省掉一路UART转接。工业现场要做的也就是把存储、数据采集、导出这三件事跑顺,8位机完全够用,没必要上复杂处理器给自己增加审查成本。

开发环境用MPLAB X IDE加XC8编译器,生态非常成熟,调试器便宜又好买,供应链稳定。温度等级选工业级后缀,在-40~+85℃环境里长期跑没有任何压力。

2. 电路设计与硬件连接要点

2.1 引脚映射与最小连接

MR25H40CDF是标准SOIC-8封装,一共8个引脚,SPI接口时序和普通串行Flash非常像,画原理图时几乎不需要额外思考,直接连线就行。

MR25H40CDF引脚功能接MCU
1 CS#片选(低有效)接GPIO或SS1
2 SO数据输出接SDI1
3 WP#写保护接GPIO或上拉到VCC
4 GND地系统地
5 SI数据输入接SDO1
6 SCLK时钟接SCK1
7 HOLD#暂停通信接GPIO或上拉到VCC
8 VCC3.3V电源电源轨

这里有个细节我要专门强调:CS引脚我建议用普通GPIO控制,不要迷信硬件SS。用GPIO的好处是同一个SPI总线上可以再挂其他传感器或ADC,切换设备时完全由软件掌控,不会被MSSP自动拉低CS造成误操作。实测下来,GPIO控制CS的时序只要在代码里保证“先拉低,再发数据,最后拉高”,一点问题都没有。

WP#引脚我在第一版设计里直接上拉固定高电平,禁用块保护。HOLD#引脚也固定拉高。后面测试中做过对比,HOLD#悬空时板子放在变频器旁边,整批读取偶尔会错位,接上拉之后这个现象彻底消失。

2.2 电源、去耦与WP/HOLD处理

MR25H40CDF功耗本身不高,但不代表可以随便供电。3.3V电源入口我加了一个10uF钽电容,每个IC旁边再放0.1uF陶瓷电容,尽量贴着VCC引脚放。工业现场电源纹波经常超标,如果示波器看到VCC上有明显毛刺,SPI数据可能偶发错误,这时候先把电源理干净再查软件。

WP#和HOLD#看起来简单,但它们是工业现场最容易埋雷的地方。HOLD#一旦被拉低,MRAM会暂停与主控的通信,正在传输的数据直接断掉。如果HOLD#引脚悬空,振动、静电、信号串扰都可能让它瞬时拉低。我的建议是:要么10k电阻上拉固定,要么用GPIO主动控制,总之绝对不能悬空。WP#同理,悬空状态下如果被干扰拉低,写操作会被屏蔽,但读操作正常,特别容易造成“数据写不进去但读得出来”的诡异故障。

MCU复位期间GPIO会短暂变成高阻或未定义状态,这会直接影响CS和WP信号。我在CS线上加了10k上拉电阻,让MCU复位时MRAM保持解选中状态,避免收到半截命令。这块是个小细节,但省掉它,系统复位后数据错乱的概率会明显上升。

2.3 PCB布局和焊接实操

板子整体不大,我把MR25H40CDF放在MCU的MSSP引脚附近,SPI线尽量短,长度控制在5cm以内。SCK、SI、SO三根线尽量等长,并且用一根地线或者铺铜带包裹,减少互感干扰。如果SPI线上串接33pF对地电容,可以滤掉一部分高频噪声,工业现场测试下来对信号稳定性有帮助。

焊接方面,MRAM是标准器件,普通回流焊或者手工热风枪都能处理,热风枪温度调到350℃左右,吹几秒就够,不要长时间加热。PCB上VCC引脚旁边预留了测试点,方便生产阶段用示波器实测供电和SPI波形。

3. SPI 驱动与读写函数实现

3.1 SPI 模式选择与寄存器配置

MR25H40CDF支持标准SPI Mode 0和Mode 3。我统一用Mode 0,即SCK空闲为低,数据在上升沿采样,下降沿改变。Mode 0和Mode 3对MRAM来说都能正常通信,但主控寄存器配置不一样,选一个让全项目保持统一就够。

PIC18F86J50的MSSP1模块配置成SPI主模式时,关键寄存器是SSP1CON1和SSP1STAT。Mode 0对应CKP=0、CKE=1、SMP=1。这里有个常见的坑:Microchip手册上CKE和SMP的表格很容易看晕,有人按网上帖子的配置抄下来结果读回来全是0xFF。我的建议是初始代码用下面这套组合,然后用示波器抓SI和SCK波形确认,数据在上升沿稳定时就是对的。

void SPI1_Init(void) { // 主模式,SPI时钟 = FOSC/4 SSP1CON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 SSP1STAT = 0xC0; // SMP=1, CKE=1 (SPI Mode 0) // 引脚方向:SCK/SDO 输出,SDI 输入 TRISCbits.TRISC3 = 0; // SCK1 TRISCbits.TRISC5 = 0; // SDO1 TRISCbits.TRISC4 = 1; // SDI1 // CS引脚输出并拉高 MRAM_CS_TRIS = 0; MRAM_CS_LAT = 1; }

引脚具体对应PIC18F86J50的哪个PORT口,要根据你的PCB实际连接修改。代码里我用MRAM_CS_LAT这类宏替代,避免绑定太死。

3.2 基础读写命令与核心驱动代码

MR25H40CDF的命令集和串行Flash很接近,但去掉了扇区擦除、块擦除这一类操作。常用的就几条:0x06写使能、0x04写禁止、0x05读状态寄存器、0x01写状态寄存器、0x03读数据、0x02写数据。

SPI交换一个字节的函数先写好,后面所有命令都基于它:

uint8_t SPI1_ExchangeByte(uint8_t byte) { SSP1BUF = byte; while (!PIR1bits.SSP1IF); PIR1bits.SSP1IF = 0; return SSP1BUF; }

读写MRAM最基础的函数如下。写数据之前必须发WREN命令,否则写操作会被忽略,这是和串行Flash一脉相承的习惯,MRAM也保留了这个机制。

#define MRAM_CS_LOW() MRAM_CS_LAT = 0 #define MRAM_CS_HIGH() MRAM_CS_LAT = 1 // 发送 WREN 写使能 void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); MRAM_CS_HIGH(); } // 读单字节 uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t val = 0; MRAM_CS_LOW(); SPI1_ExchangeByte(0x03); // READ SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); val = SPI1_ExchangeByte(0x00); // 同时读回 MRAM_CS_HIGH(); return val; } // 写单字节 void MRAM_WriteByte(uint32_t addr, uint8_t val) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); SPI1_ExchangeByte(val); MRAM_CS_HIGH(); }

MR25H40CDF容量是4Mbit,也就是512KB,地址范围0x00000~0x7FFFF,共19位有效地址。写程序时地址用uint32_t,发送24位地址,高5位为0即可。一旦地址超过0x7FFFF,器件会从0地址回卷,这个特性和Flash一样,处理大数据块时要注意。

3.3 写使能、状态寄存器与块保护

每次写数据前发WREN是必须的,我把它封装成单独函数,写缓冲函数里统一调用。写完数据后,可以读状态寄存器确认WIP位已经清掉。虽然MRAM写入几乎是瞬间完成,但保留这个检查动作能让代码在以后移植到Flash时不用大改。

uint8_t MRAM_ReadStatus(void) { uint8_t sr; MRAM_CS_LOW(); SPI1_ExchangeByte(0x05); // RDSR sr = SPI1_ExchangeByte(0x00); MRAM_CS_HIGH(); return sr; }

状态寄存器里,bit0是WIP,bit1是WEL,bit2和bit3是BP0和BP1块保护位。大多数场景下BP位默认都是0,表示全地址可写。如果你希望系统固件区不被意外覆盖,可以在初始化时通过WRSR命令设置BP位,把高地址或者低地址区域保护起来。

这里有个很多人忽略的细节:WP#引脚不是独立的“全局写保护开关”,它只对BP位控制的区域生效,而且前提是状态寄存器里的WPEN位必须为1。如果WPEN为0,WP#引脚拉低也不会保护任何区域。换句话说,如果你想用硬件写保护,必须同时配置状态寄存器。只把WP#拉低,写操作依然照常进行,这一点和Flash厂商的习惯一致,但经常被误解。

3.4 批量读写:一次命令处理多字节

实际项目中不会单个字节地读写,都是整块搬运。MR25H40CDF的WRITE命令和READ命令都支持连续多字节,CS保持拉低期间,地址自动递增,不需要额外的页缓冲限制。这就是MRAM比Flash舒服的地方:想写多少字节就写多少字节,不用管“页”和“扇区”。

uint8_t MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { SPI1_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); return 0; } void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x03); SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI1_ExchangeByte(0x00); } MRAM_CS_HIGH(); }

在10MHz SPI时钟下,连续写512字节的耗时大约0.5ms左右,对比一下SPI NOR Flash的页编程动辄几毫秒,MRAM几乎是即时覆盖。实际数据记录板里,我把256字节作为一个日志块,每块写入时间1ms以内,完全不影响采集任务。

4. 掉电防护与数据完整性设计

4.1 为什么 MRAM 天然抗掉电

很多嵌入式开发者对MRAM不熟,我这里多解释一句原理。MRAM的存储单元是一个磁隧道结,由两层磁性材料和中间的绝缘层组成。写数据时,通过线路电流产生磁场,改变自由层的磁化方向,两层磁化同向和反向分别代表0和1。这个磁化状态在没有外界强磁场干扰时非常稳定,断电后不会消失。

这就和Flash的浮栅电荷存储原理完全不同。Flash写入需要电荷泵产生高电压,把电子注入浮栅;擦除时再把电子拉出来。如果擦除或写入过程正好断电,浮栅中的电荷量可能处于中间值,读出来数据就变得不可靠。MRAM没有这个窗口,磁化方向翻转是物理层面的状态切换,半途断电也不会停在“中间状态”。用一句白话概括:Flash像往杯子里倒水,倒一半停电,水量不确定;MRAM像翻硬币,翻过去就定了,停电也不影响。

4.2 掉电检测与最后时刻写保护

虽然MRAM本身不怕掉电,但MCU在掉电瞬间能不能把数据及时写进去,这是另一个问题。我的板子用PIC18F86J50内置的BOR功能做掉电保护。BOR阈值配置在2.7V左右,当VCC跌到2.7V以下,MCU先进入复位或者中断,我在中断里做最后处理。

这里可以简单算一笔账。假设系统3.3V掉电后,电源电容100uF,负载电流20mA,从3.3V跌到2.7V的时间大约是:

t = C × ΔV / I = 100uF × 0.6V / 20mA ≈ 3ms

3ms窗口说长不长,说短不短,够我写几十字节的紧急日志。如果掉电时想写入几百字节甚至几KB,就必须加更大的储能电容,或者降低掉电后的MCU工作负载。这个计算也解释了为什么工业设备掉电保存通常只保关键参数,不保完整日志——物理窗口就这么大,存储器件再快也没用。

实际测试中,我在掉电检测引脚触发后,先把关键参数写入MRAM固定地址,然后立刻把WP#拉低锁住写入区域,防止后续电压不稳时MCU乱写。上电后程序启动,BOR复位完成,先读MRAM参数区,再校验CRC,最后根据参数恢复运行状态。

4.3 CRC 校验与日志框架

MRAM虽然比Flash可靠,但工业现场还是不能裸奔。我在存储区划分了三块空间:参数区、日志区、备份区。参数区存设备运行参数,每个参数记录都带16位CRC校验值,CRC多项式用Modbus标准0x8005。日志区做成环形缓冲,记录满了自动覆盖最旧的数据。备份区存参数区的镜像副本,开机时主副本校验失败,就从备份恢复。

每条日志记录的结构很简单:2字节魔数、4字节时间戳、2字节数据长度、最多240字节数据、2字节CRC。这样扫描整个MRAM日志区时,先找魔数,再算CRC,CRC对上的才是有效记录。掉电瞬间写到一半的记录,CRC通常对不上,直接跳过,不影响前面完整日志的读取。

有些朋友觉得加CRC是浪费时间,在MRAM这种可靠器件上没必要。我实际踩过坑,SPI信号受外界干扰时,读出来的数据里偶尔会翻一个bit,如果没有校验,这条日志就会以错误内容入库。工业现场的电噪声不可控,校验位花不了多少CPU时间,还是加上稳妥。

4.4 实测性能与可靠性数据

手上这块样板在室温下做了几轮测试。SPI时钟10MHz,连续写512字节大约0.5ms,连续读512字节也差不多,换算吞吐接近1MB/s。对数据记录板来说完全够用,哪怕每秒采集10次、每次记256字节,只占MRAM带宽的很小一部分。

掉电可靠性测试做了上千次,方式是给板子正常供电,随机时间断电重启,每次断电前让MCU在中断里写一条日志。测试结果是所有日志要么完整,要么根本没有写入,没有出现过写了一半的坏记录。这个结果让我对MRAM的好感又上一截,之前用Flash做同类测试,几十次就能碰到一次数据损坏。

另外我还做了温度测试,把板子放在高低温箱里,-40℃和85℃各跑2小时循环,MRAM读写都正常,存储的数据读回来对比零差异。工业设备放在车间里,温度环境远没有这么极端,所以量产方案直接沿用了这套测试配置。

5. 工业现场常见问题与排查实录

5.1 读回来全是 0xFF 或 0x00

这个故障最经典,几乎每个人第一次上MRAM都会遇到。核心原因就是SPI Mode没配对。PIC的MSSP模块配置一定要确认CKP和CKE组合。用我上面的初始化代码,Mode 0应该直接能通,如果读回全FF,优先查这两个位。

另外检查一下CS时序。MRAM要求在CS拉低后,SCK开始产生有效时钟,不能先发时钟再拉低CS。用示波器抓CS和SCK的时序关系,一眼就能看出来。还有一种情况是CS拉低后,第一个命令字节发出去前,CS本身存在毛刺,导致MRAM收到乱码命令。这种情况多半是GPIO没有上拉,MCU上电过程中CS被噪声拉低,加上拉电阻就能解决。

5.2 HOLD# 和 WP# 悬空引发的诡异故障

我第一版PCB的HOLD#引脚没接上拉,就让它悬在那,结果出现了一个特别隐蔽的故障:设备运行几天后,偶尔会读到一整块重复的数据,或者日志区末尾多出几个错误字节。刚开始怀疑SPI时序,后来用示波器长时间抓HOLD引脚,发现它偶尔会被拉到低电平,持续时间几个毫秒,期间MRAM暂停通信,恢复后CS状态已经错乱。

解决办法很简单,HOLD#固定接VCC,或者用10k电阻上拉。WP#也做同样处理,如果不用硬件写保护就直接接VCC。这个教训我写进项目总结里,后面所有用SPI存储的项目都强制要求WP#和HOLD#不允许悬空。

5.3 3.3V 与 5V 电平兼容问题

如果主控是5V单片机,SCK和SI引脚输出的高电平是5V,而MR25H40CDF的VCC只有3.3V,输入引脚耐压不一定兼容5V,可能损坏芯片或者导致电流倒灌。反过来,MRAM输出的SO信号是3.3V高电平,5V单片机如果不是施密特输入,高电平阈值在2.0V以上,勉强能识别,但噪声容限会变小。

我这次选PIC18F86J50就是3.3V器件,和MRAM同电源,完美避开了电平转换问题。如果你的板子已经定了5V MCU,建议加一颗电平转换芯片,或者至少用电阻分压和施密特缓冲器处理,不要硬接。

5.4 常见问题速查表

故障现象可能原因解决办法
读回全0xFF或0x00SPI Mode配置不对、CS时序错检查CKP/CKE,示波器抓时序,CS加上拉
读第一字节乱CS未先拉低就发命令先拉CSS再操作,确保命令字节完整
写不进去没发WREN、WP#拉低、BP位保护发WREN,WP#接VCC,检查状态寄存器
偶发数据错乱HOLD#/WP#悬空、电源纹波大固定上拉,加强去耦,短期降SPI时钟
掉电后旧数据在但新数据丢失掉电检测太晚,窗口不足调BOR阈值,减掉电写入量,加储能电容

5.5 一点排障思路

每次排查存储类故障,我遵循一个固定顺序:先查硬件连接和供电,再看SPI波形,然后是寄存器配置,最后才怀疑芯片本身。MR25H40CDF这类器件在正常使用下极少损坏,绝大多数问题都出在主控配置和外围电路上。与其反复改软件,不如把示波器探头往SCK和SO上一搭,数据对不对,波形会直接告诉你。

6. 写在项目之后的一点个人体会

这套方案稳定跑了大半年,现场设备经历过多次随机断电和电压波动,再也没有出现过一次“数据莫名其妙丢失”的售后反馈。回头复盘,我最大的体会是:嵌入式项目里存储方案要敢于做减法。很多工程师一提到“记录数据”就自然想到Flash、文件系统、SD卡,结果在工业现场高频随机写、频繁掉电的场景里,给自己找了一堆坏块管理和掉电保护的麻烦。

MRAM贵一点,但贵得有理由。它把掉电安全、写寿命、写入速度这些最棘手的问题全部简化成普通的SPI读写,代码量少了一个数量级,出问题的概率也少了一个数量级。如果你手里的项目也是数据要频繁更新、断电不能丢、现场环境还比较恶劣,不妨认真考虑一下MR25H40CDF这类磁阻存储器,至少它值得出现在选型评审表的对比栏里。

最后再分享一个小技巧:MRAM的写使能命令WREN在每次写操作前都要发,我一开始为了省一次SPI传输,尝试连续两次写数据之间复用WREN状态,结果第二次写操作偶尔失败。后来老老实实每次写之前都发WREN,再没出过问题。有些看似多余的步骤,其实是芯片手册设计好的安全边界,别自作聪明去省它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询