☰
弃用Flash和EEPROM:MRAM在工业数据存储中的工程实践
2026/10/4 5:07:12 网站建设 项目流程

1. 为什么在工业存储场景里,我弃用了Flash和EEPROM

1.1 被擦写寿命和掉电窗口坑了太多次

嵌入式设备做数据存储,最常见的选择是EEPROM和NOR Flash。EEPROM容量小,常见的是128KB以内的I2C/SPI器件,写一个字节要几毫秒;NOR Flash容量大、价格也便宜,但按页编程、按扇区擦除的特性在"频繁记录"场景下非常难受。我之前做过一套现场设备,每5秒往EEPROM里写一条运行参数,算下来一天写入一万多次。普通EEPROM耐写次数在百万级,看着够用,但实际现场环境的高温加上偶发的掉电写入,一年半左右就开始出现偶发丢数据。

换NOR Flash之后反而更头疼。Flash写入前要先擦除整个扇区,而且掉电那一瞬间如果正好在写,数据很容易损坏。你要么做一个FAT-like的日志系统来处理磨损均衡,要么在后备电源上花成本,把掉电时序写得足够干净。工业设备里为了这一个存储功能,引入一套文件系统级别的复杂度,性价比太低了。

1.2 MRAM把这两类问题直接消除了

MRAM(磁阻随机存取存储器)的原理跟Flash完全不一样:数据不是用电荷存储在浮栅里,而是靠磁隧道结中磁化方向的变化来记录0和1。磁化状态本身不依赖电源,所以断电后数据不丢;改变磁化方向的过程也不需要"擦除-写入"两步,因此写入就像SRAM一样直接发生。

MR25H40CDF就是Everspin一颗很典型的SPI接口MRAM。容量4Mbit,也就是512KB,组织成512K x 8。对我这类应用来说,它的三个硬指标几乎是为工业数据存储量身定做的:

  • 写入耐久性标称10^14次,对嵌入式日志记录来说基本等于无限寿命;
  • 写入操作没有页编程等待时间,单字节可直接覆写,无需先擦除;
  • SPI接口,工作电压2.7~3.6V,工业级温度范围,和STM32L0系列配合得非常好。

实际的体感和Flash完全不同。Flash写一页,等待时间一般要0.5ms到几ms;MRAM写一个字节,命令行发完就是写进去了,状态寄存器里的WIP位基本读不到有效电平。这个特性在掉电保存场景里价值极大,后面我会详细说。

MR25H40CDF的关键操作命令和普通SPI Flash很像,熟悉的人可以直接上手:

命令操作码说明
WREN0x06写使能锁存,写操作前必须执行
WRITE0x0224位地址 + 数据字节
READ0x0324位地址 + 连续读数据
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器(配置WP相关保护位)
SLEEP0xB9进入睡眠模式,降低待机功耗
WAKE0xAB唤醒芯片

这颗芯片最高支持40MHz的SPI时钟,不过STM32L021K4的SPI外设最高只有16MHz,所以时钟速度完全不是瓶颈。

MCU方面我选STM32L021K4,主要是看中了三点:Cortex-M0+内核,功耗控制做得好;16KB Flash和8KB SRAM虽然不大,但驱动一颗MRAM绰绰有余;32脚的封装很小,适合做独立的存储模块或者低功耗采集板。如果你手头是其他STM32型号,后面的驱动代码改一下引脚定义就能直接用。

2. MR25H40CDF与STM32L021K4的硬件连接细节

2.1 引脚分配:SPI加三个GPIO就够了

先看MR25H40CDF的引脚。它除了电源VDD和地VSS,剩下的引脚是CS#、CLK、DI、DO,外加WP#和HOLD#。WP#是硬件写保护使能,HOLD#是SPI总线暂停功能。这两个脚在实际项目里建议直接拉高到VDD,避免因为噪声干扰导致芯片意外进入保护或者暂停状态。

STM32L021K4的SPI1默认可以映射到PA5(SCK)、PA6(MISO)、PA7(MOSI),芯片片选我用普通GPIO控制,不用SPI硬件NSS。我的连接方式如下:

MR25H40CDF引脚STM32L021K4引脚说明
CS#PA4GPIO输出,软件控制片选
CLKPA5SPI1_SCK
DIPA7SPI1_MOSI,MCU输出到MRAM
DOPA6SPI1_MISO,MRAM输出到MCU
WP#VDD拉高,硬件写保护关闭
HOLD#VDD拉高,暂停功能关闭
VDD3.3V接0.1uF去耦电容
VSSGND接地

CS#用软件GPIO而不是SPI硬件NSS,是我在多个项目里的习惯。硬件NSS在多主机环境或配置不当的时候会出现莫名其妙的片选错乱,尤其像STM32L0这种资源不多的芯片,软件CS反而最可靠。

2.2 电源、去耦和电平匹配的实际考量

MR25H40CDF工作电压是2.7V到3.6V,STM32L021K4用3.3V供电,两者直接相连,不需要电平转换。

去耦电容一定要靠近VDD引脚放。MRAM在连续读写的瞬间电流变化比较快,如果去耦电容离得太远,电源纹波可能触发芯片内部保护。我习惯在VDD和VSS之间放一颗0.1uF的陶瓷电容,同时在板卡总电源入口放一颗10uF钽电容或电解电容。如果你MCU的VDDA没有单独管理,也给它放一个0.1uF。

还有一个容易忽略的点:HOLD#和WP#这两根脚一定不要悬空。有些工程师觉得不用的引脚不用接,但SPI器件在HOLD#悬空状态下,现场强干扰很容易让时钟线边的毛刺触发暂停,整片MRAM就像卡死一样不响应。拉高之后这类问题基本消失。

如果你想把WP#引脚用起来做硬件写保护,那它不能直接接3.3V,最好由MCU的一个GPIO控制,平时输出高,需要保护时拉低。不过按我的经验,软件层用状态寄存器的SRWD位已经能实现绝大部分保护需求,WP#直接拉高简化设计更省事。

3. 软件驱动:SPI初始化、命令时序与封装接口

3.1 SPI初始化:模式0最省心

MR25H40CDF支持SPI模式0和模式3,也就是CPOL=0/CPHA=0或者CPOL=1/CPHA=1。ST官方LL库初始化SPI主机非常简洁,代码如下:

void MRAM_SPI_Init(void) { LL_APB2_EnableClock(LL_APB2_GRP1_PERIPH_SPI1); LL_GPIO_InitTypeDef GPIO_Init = {0}; // PA5 SCK, PA7 MOSI, PA6 MISO GPIO_Init.Mode = LL_GPIO_MODE_ALTERNATE; GPIO_Init.Speed = LL_GPIO_SPEED_FREQ_HIGH; GPIO_Init.OutputType = LL_GPIO_OUTPUT_PUSHPULL; GPIO_Init.Pull = LL_GPIO_PULL_NO; GPIO_Init.Alternate = LL_GPIO_AF_0; GPIO_Init.Pin = LL_GPIO_PIN_5 | LL_GPIO_PIN_6 | LL_GPIO_PIN_7; LL_GPIO_Init(GPIOA, &GPIO_Init); // CS PA4 as GPIO output GPIO_Init.Mode = LL_GPIO_MODE_OUTPUT; GPIO_Init.OutputType = LL_GPIO_OUTPUT_PUSHPULL; GPIO_Init.Pull = LL_GPIO_PULL_NO; GPIO_Init.Pin = LL_GPIO_PIN_4; LL_GPIO_Init(GPIOA, &GPIO_Init); LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); LL_SPI_InitTypeDef SPI_Init = {0}; SPI_Init.TransferDirection = LL_SPI_FULL_DUPLEX; SPI_Init.Mode = LL_SPI_MODE_MASTER; SPI_Init.DataWidth = LL_SPI_DATAWIDTH_8BIT; SPI_Init.ClockPolarity = LL_SPI_POLARITY_LOW; // CPOL=0 SPI_Init.ClockPhase = LL_SPI_PHASE_1EDGE; // CPHA=0 SPI_Init.NSS = LL_SPI_NSS_SOFT; SPI_Init.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV4; // 32MHz/4=8MHz SPI_Init.BitOrder = LL_SPI_MSB_FIRST; LL_SPI_Init(SPI1, &SPI_Init); LL_SPI_Enable(SPI1); }

时钟分频这里我用的是DIV4,也就是8MHz。MRAM支持40MHz,STM32L0的SPI外设极限16MHz,8MHz距离上限还很远,留足了电平余量。高速不代表高可靠,长走线、连接器转接的情况下我宁可降到2MHz再跑。

3.2 读写命令时序:写使能是关键

MRAM的字节写和SPI Flash逻辑类似。第一步永远是发WREN(0x06)把状态寄存器里的WEL位置1,然后CS拉高,再拉低CS发送写命令、地址和数据。以下是我一直在用的核心函数:

void MRAM_CS_Low(void) { LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); } void MRAM_CS_High(void) { LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); } uint8_t MRAM_SPI_RW(uint8_t byte) { while (LL_SPI_IsActiveFlag_TXE(SPI1) == 0); LL_SPI_TransmitData8(SPI1, byte); while (LL_SPI_IsActiveFlag_RXNE(SPI1) == 0); return LL_SPI_ReceiveData8(SPI1); } void MRAM_WriteEnable(void) { MRAM_CS_Low(); MRAM_SPI_RW(0x06); // WREN MRAM_CS_High(); } void MRAM_WriteBytes(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t i; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SPI_RW(0x02); // WRITE MRAM_SPI_RW((addr >> 16) & 0xFF); MRAM_SPI_RW((addr >> 8) & 0xFF); MRAM_SPI_RW(addr & 0xFF); for (i = 0; i < len; i++) { MRAM_SPI_RW(data[i]); } MRAM_CS_High(); }

写命令发送完后不需要轮询等待。这也是MRAM和Flash在编程模型上最大的区别:Flash写完一页后要查状态寄存器的忙标志,MRAM直接再来一次写操作就好。我一般会在每次写事务之间加上极短的延时,不是为了等芯片,而是给MCU一个缓冲,防止连续操作时自己的SPI缓冲溢出。

读操作就更简单了,不需要WREN,直接发命令、地址,然后连续收数据:

void MRAM_ReadBytes(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t i; MRAM_CS_Low(); MRAM_SPI_RW(0x03); // READ MRAM_SPI_RW((addr >> 16) & 0xFF); MRAM_SPI_RW((addr >> 8) & 0xFF); MRAM_SPI_RW(addr & 0xFF); for (i = 0; i < len; i++) { data[i] = MRAM_SPI_RW(0x00); } MRAM_CS_High(); }

注意MR25H40CDF的地址是24位。512KB空间需要19根地址线,所以24位地址的高位实际用不到,但发送时序上必须一字节不少,否则芯片的地址译码会错位,读出来的数据完全不是你要的位置。这个坑后面细讲。

3.3 驱动层封装:让上层像操作数组一样用MRAM

硬件驱动做完,更重要的是给上层应用一个不操心的接口。工业设备里最典型的两种数据是:设备参数区(校准值、序列号、配置项)和运行记录区(事件日志、实时指标)。我习惯把它们映射到MRAM的不同地址段:

#define MRAM_ADDR_PARAM 0x00000 // 参数区 0-255字节 #define MRAM_ADDR_LOG 0x00100 // 日志区 256字节起 #define MRAM_SIZE 0x80000 // 512KB

参数区启动时一次性读入内存,只有在校准或者配置写入时才回写。日志区做成追加写,每条记录固定16字节,写指针单独存在MRAM末尾的一个状态槽里。因为有MRAM的随机写入特性,追加一条日志只需两次写操作:一次写记录,一次更新指针,完全没有Flash日志系统的擦写均衡复杂度。

如果只是报一个读写的API,上层应用还是容易犯错,最好把参数区和日志区各自封成一个结构体接口。参数区用Param_Load()和Param_Save(),日志区用Log_Append()和Log_Read(index),内部再检查地址边界,越界的就直接报错。这样写业务代码的人根本不需要关心底下是MRAM还是其他什么存储介质。

4. 工业级可靠性:掉电保存、写保护与睡眠模式

4.1 用PVD掉电检测争取最后的写入窗口

工业设备一个最典型的场景是外部突然断电,你还想在那最后的几毫秒里把当前状态记录下来。以前用NOR Flash,掉电中断里根本不敢做大操作,因为页编程要等芯片擦写完成,动不动几毫秒,电源早就撑不住了。换成MRAM之后,这个问题被天然简化,因为写一个字节就是几个SPI时钟周期的事。

STM32L021K4内部有可编程电压检测器(PVD),可以监测VDD电压,当VDD跌落到设定阈值时触发中断。我通常把阈值设在2.9V,在PVD中断里把需要保存的运行状态写入MRAM:

void PVD_IRQHandler(void) { if (LL_PVD_IsActiveFlag_IT()) { Log_Append(&current_status); // 如果需要可以继续写入其他关键数据 LL_PVD_ClearFlag_IT(); } }

这里我实测过,一次16字节的状态写操作,从进入中断到写完,大约只需要几十微秒。对比NOR Flash页编程动辄几毫秒的等待,这个时间窗口几乎不构成压力。

但要注意一点,PVD中断本身和MRAM写入之间尽量不要做太多事情。中断里只做数据打包和写入,其他冗余判断全部去掉,给MCU省下周期。如果你需要保存的数据量大,也可以先把数据整理好,在PVD中断里一次性连续写完,MRAM的连续写协议能一直维持,直到你把CS拉高。

4.2 WP引脚与状态寄存器SRWD的双层写保护

工业设备最怕两件事:程序跑飞乱写存储,或者现场强干扰把存储内容改坏。MRAM和Flash一样,默认状态下是可以随时写入的,所以在实际产品里我建议把写保护真正用起来。

MR25H40CDF的写保护有两个层面。第一层是芯片的WP#引脚,第二层是状态寄存器里的SRWD位。两者配合的逻辑是:当SRWD=1且WP#引脚为低时,状态寄存器的保护位和整个存储阵列都不能被写。如果SRWD=0,WP#引脚只影响状态寄存器的写入,不影响存储数据区。

我的一个成熟方案是:正常运行时把存储阵列放在可写状态,但只有在PVD中断或者明确的保存指令发出时才执行写操作;程序启动的初始化阶段,先写一个状态标志,然后立即把WP#拉低并置位SRWD开启保护。关键参数区在运行阶段完全不可写,要修改数据必须先通过一个专门的维护模式解锁,再写入、重新加锁。

这句逻辑用代码表示就是:

void MRAM_EnableWriteProtect(void) { MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SPI_RW(0x01); // WRSR MRAM_SPI_RW(0x80); // SRWD=1 MRAM_CS_High(); // 此时将WP#引脚拉低,硬件锁生效 LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_3); }

这里有个更实际的原因让我推荐这种软硬结合方式:如果某个菜单参数被现场误改,或者调试口被接上后无意中向存储地址写入错误数据,有这层保护至少能在下一次重启时通过校验发现异常。只靠软件校验不如芯片层面直接禁止写来得干净。

4.3 SLEEP命令:低功耗设计的另一个省电手段

MR25H40CDF在待机时也有一定的静态电流,对电池供电的采集终端来说,这是白白浪费的功耗。芯片提供了SLEEP命令,发送0xB9后芯片进入睡眠模式,待机电流能降到微安级别,和Flash深度掉电状态是一个意思。

唤醒的方式是发送WAKE命令(0xAB),或者直接执行RDSR读状态寄存器也能唤醒。但注意唤醒后芯片需要一段恢复时间,手册上标注大约是30微秒左右,在这段时间里不要发起读写。我的驱动里加了一个简单的状态机:

void MRAM_Sleep(void) { MRAM_CS_Low(); MRAM_SPI_RW(0xB9); MRAM_CS_High(); } void MRAM_Wakeup(void) { MRAM_CS_Low(); MRAM_SPI_RW(0xAB); MRAM_CS_High(); delay_us(50); // 留足tREC恢复时间 }

低功耗设备的主循环里,每次采集完成后让MRAM进入睡眠,下一次采集前先唤醒再做读写。这个50微秒的延时看似小,但必须加,我最初直接唤醒后立刻读数据就会读到FF,排查了半天才发现是恢复时间不足。

5. 实测数据与踩坑记录:循环写入、SPI模式、DFN焊接

5.1 连续写入循环测试:百万次无压力

我在开发阶段做了一套连续写入校验程序:从地址0开始,每个地址写一个伪随机字节,循环写满整片512KB后再读回来比对,然后又写另一组数据,如此反复。实测下来连续跑了一百万次循环,读取结果始终一致。这在意料之中,但真正让我安心的是整个测试过程里连一次CRC错误都没有。

对比之前EEPROM做同样测试,写到一定次数后偶发错误就开始出现。MRAM的这个写入延迟接近零的特性还有一个额外的好处:写操作不需要额外的充电泵升压,芯片内部功耗更稳定,工业温度下的一致性也更好。

5.2 SPI模式与地址位宽:最容易踩的两个坑

SPI模式这个坑,说起来非常低级,但还真容易犯。MRAM支持模式0和模式3,但有些国产兼容芯片只支持模式0。如果代码里按模式3初始化,通信有时候能通,数据却全是乱的。我调试时先用逻辑分析仪抓了CLK和DI的波形,发现数据位基本都是对的,就是采样沿偏了半个周期。后面统一按模式0初始化,同时也配好MCU侧的CPOL和CPHA两个参数,问题才彻底消失。

地址位宽的坑要更隐蔽。MR25H40CDF是512KB,虽然地址在物理上只需要19位,但SPI命令协议固定使用24位地址。有些参考代码图省事,只发两字节地址,这样实际只能访问低64KB区域。如果你的代码在地址0x10000之后写数据,选用的芯片又恰好是大容量的MR25H40,那你根本写不进去,读出来的也是错的。原因是MRAM内部地址计数器从低字节开始滚动,高位缺失会直接造成地址回绕。

我在驱动里加了一个宏编译检查,地址超过MRAM_SIZE - 1就直接断言。另外,每次传地址时强制用uint32_t做移位运算,避免默认整数宽度被截断。

5.3 DFN封装焊接与Layout:手工打样最容易翻车的地方

MR25H40CDF常见封装是DFN-8,引脚都在底部,没有那种外露的翼形引脚。手工焊接的时候如果焊膏量控制不好,相邻引脚很容易连锡。我踩过一次,VDD和VSS之间短接,上电瞬间MRAM直接发烫。

如果你也用热风枪焊接,建议先给焊盘刷一层薄薄的助焊剂,放上芯片后用贴片镊子轻压,热风枪温度设到300度左右,风速调低,等锡膏熔化后再凝固。焊完必须用万用表蜂鸣档测量VDD和GND之间是否短路,确认没问题再上电。

Layout上还有两个细节:MR25H40CDF的CLK引脚走线尽量短,不要穿过高阻抗区域;CS#到MCU的连线要远离继电器、电机驱动这类强干扰源。工业现场比实验室环境恶劣得多,我第一版板子走线稍微绕了一下,在带电机干扰的测试里偶尔出现读数据错乱,后来把SPI四条线整体缩短并加了地线包边,问题才消失。

另外吐槽一下,DFN封装的MR25H40CDF底部散热焊盘一般是连到GND的。如果你用回流焊,散热焊盘上一定要开钢网,否则芯片贴不牢;手工焊的话散热焊盘可以不接地,但别让它悬空,最好也连到地。

整个项目用下来,MR25H40CDF加STM32L021K4这套组合,在工业数据存储场景里的表现是让我放心的。尤其掉电保存那一下,MRAM的即时写入特性救了不少关键数据。如果你只是做消费类产品,Flash够用就没必要换;但要是设备要长期跑在恶劣环境里、需要高频次保存状态,MRAM确实值得你为它预留一颗物料的位置。

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

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

立即咨询