☰
MRAM替代EEPROM:基于STM32 SPI接口的工业频繁写存储方案
2026/10/4 1:38:01 网站建设 项目流程

最近在搞一台老设备的控制系统,里面的工业EEPROM快被写穿了。设备每班都要记录几百条产量和报警信息,加上调试时反复改参数,原来那片EEPROM的寿命撑不过一年。后来我把存储方案换成了Everspin的MR25H40CDF,配合STM32F215RE主控,通过SPI口直接读写。MR25H40CDF是4Mbit的串口MRAM,容量不大,但胜在写入随便写、掉电不丢。STM32F215RE是一颗Cortex-M3核,跑120MHz,外设资源丰富,拿来驱动它再合适不过。这篇文章我把选型思路、硬件接线、驱动代码和调试过程完整过一遍,给还在EEPROM、Flash和MRAM之间犹豫的朋友做个参考。

1. 为什么用MRAM替代EEPROM和Flash

1.1 MR25H40CDF的核心特性

MR25H40CDF这颗片子表面上看就是个普通SPI存储芯片,8个引脚,512KB容量,按字节寻址,接上SCK、MOSI、MISO、CS就能通信。但它和人的本质区别在于存储介质:EEPROM和Flash靠电荷保存数据,而MRAM靠磁隧道结的磁阻状态保存数据。所以它没有擦除周期,写字节之前不需要把整个扇区擦一遍,指定地址直接写就行。

一颗典型的MR25H40CDF支持40MHz的SPI时钟,数据传输率在同容量非易失芯片里算很拔尖的。写入寿命这块,MRAM标称 endurance 可以达到10^14次以上,工业上基本可以当成“无限写”。断电数据保持时间一般也能达到20年。这些数值对于需要频繁记录参数的设备来说非常诱人。

我拿它和手头常用的存储芯片列了个对比:

项目MR25H40CDF外置EEPROM 24C256SPI Flash W25Q64
容量4Mbit256Kbit64Mbit
写入单位字节,无需擦除字节,无需擦除先擦扇区再写页
写寿命>10^14次约100万次一般10万次
SPI时钟最高40MHz最高1MHz左右最高80MHz(读)
掉电数据保持20年以上100年以上20年以上
写入前操作只需写使能只需写使能必须执行块擦除

这表一眼就能看出问题:EEPROM寿命虽然能接受,但速度太慢,容量偏小;Flash容量大,但频繁写日志要处理擦除和磨损均衡,逻辑复杂,一旦掉电时正好在擦除,数据就悬了。MR25H40CDF更像一个“掉电不丢的SRAM”,所以放在工业记录场景里非常顺手。

1.2 STM32F215RE在这套方案里的定位

STM32F215RE之所以合适,首先是因为它有一堆可用的SPI外设。以我实际用的SPI1为例,它挂在APB2上,APB2最高60MHz,SPI1通过预分频可以跑到30MHz甚至更高,基本能接近MR25H40CDF的最大时钟。再把PA4作为软件CS脚,PA5、PA6、PA7分别接SCK、MISO、MOSI,一套硬件就成型了。

其次是STM32F215RE自带DMA,批量读写MRAM时不需要CPU一个字节一个字节去喂SPI,DMA跑起来之后CPU还能去干别的算法任务。而STM32F2系列的电源管理里还有PVD可编程电压检测器,可以在掉电瞬间检测到电压跌落,进中断把关键数据紧急写入MRAM。这套组合天然适合做掉电保护。

第三个原因是芯片本身的市场存量很大,工业控制产品用了这么多年,芯片资料、库函数、硬件设计参考都非常成熟,哪怕中途换人接手项目,也容易找到熟悉STM32F2系列的开发者。STM32F215RE虽然有512KB Flash和128KB RAM,但我在这个方案中主要拿它当控制器,MRAM负责外部非易失数据存储,两级存储角色非常清晰。

1.3 工业频繁写场景为什么最后选了MRAM

我这次的设备需要保存的信息有三类:生产计数、故障报警履历、掉电前的运行状态。生产计数每次加工零件都要加一,锻压设备一分钟可能完成几十个零件,一天的写入次数就上万。报警履历更麻烦,现场一次故障可能短时间内连续上报几十条,传统EEPROM在这种压力下寿命很快见底。

Flash的情况也没好到哪里去。虽然单字节写入前要先擦除整个扇区,如果设备突然断电,数据可能停留在半擦除状态。而且为了保证Flash寿命,通常还得做磨损均衡、空闲块管理,工作量和代码复杂度直接上一个台阶。

MRAM就不存在这些问题。它不需要擦除,写入地址可以任意指定,写寿命又高,SPI时钟上来之后一次写入只有零点几微秒的时间,掉电窗口被压得非常小。所以我认为,对于“频繁写、掉电不丢、数据量不超过512KB”的工业场景,MR25H40CDF几乎是贴着需求来的。

2. 硬件电路设计:从引脚到PCB

2.1 引脚连接与最小系统

MR25H40CDF的引脚逻辑和传统SPI Flash很像,但有几个细节特别容易踩坑。核心引脚是CS#、SCK、SI、SO。CS#就是片选,低电平有效;SCK是时钟;SI接主控MOSI,SO接主控MISO。我用的STM32F215RE这边是PA4做CS#,PA5做SCK,PA7做MOSI,PA6做MISO,连接关系如下:

MR25H40CDF引脚功能连接到STM32F215RE
CS#片选PA4(GPIO输出)
SCK时钟PA5(SPI1_SCK)
SI数据输入PA7(SPI1_MOSI)
SO数据输出PA6(SPI1_MISO)
WP#写保护接GPIO或VCC
HOLD#暂停通信接VCC
VCC电源3.3V
GND地GND

WP#这个引脚平时很多人直接拉高,这样软件写保护没打开,读写都自由。如果想在系统层面防误写,可以单独用一个GPIO控制,正常拉高,需要锁存时拉低。HOLD#必须处理,不能悬空。HOLD#拉低时SPI总线被冻结,SO输出高阻,看起来就像芯片“死掉”了。我在样板调试时遇到过这个情况,后面会细说。

2.2 电源去耦与HOLD#、WP#的详细处理

MRAM本质上是个数字存储芯片,对电源噪声不算特别苛刻,但工业现场的电源环境千奇百怪,电机的启停、变频器的斩波都可能把干扰引到3.3V上。我在VCC和GND之间放了1个10uF钽电容和1个100nF陶瓷电容,两者尽量贴近芯片引脚。SCK和CS这样的敏感信号线上没有串电阻,但走线都控制在10mm内,避免形成天线。

HOLD#和WP#如果有条件,不要只接一个死的跳线,而是在芯片附近用10k电阻上拉到VCC。这样做的好处是:主控复位瞬间IO电平不确定时,MRAM不会因为HOLD#被意外拉低而进入暂停状态。对于一些使用电池供电的便携式设备,还可以把WP#接到一个高边开关上,电池电量低时硬件自动锁定,防止写入不完整的数据。

PCB布局上,我建议把MR25H40CDF放在MCU同一面、靠近SPI引脚的位置,中间不要穿过连接器。如果是两层板,尽量保证SPI走线下方有完整的地平面,避免形成大的回流环路。实际测试中,走线超过30mm后SCK边沿会出现明显过冲,SPI时钟降到10MHz才好一些。所以布局紧凑对高速通信有益。

2.3 与SPI Flash的兼容性坑

MR25H40CDF的封装和引脚定义与不少SPI Flash接近,有人会想着直接把它替换到原来Flash的PCB上。但这里要特别小心,因为两者的指令集和状态寄存器定义并不完全一致,而且WP#和HOLD#的位置在不同厂家的封装里可能有变化。

我在一块测试板上试过直接换片,原板子按W25Q64编写驱动,换上MR25H40后发送读ID指令,返回值完全对不上,读写操作也没有响应。原因很简单,MR25H40的读ID指令和数据格式与Winbond不同,而且有些引脚的功能在两种芯片上是颠倒的。所以不要指望直接替代,硬件上核对每一个引脚,软件上按Everspin的数据手册重写驱动。

另外还要注意MRAM的容量与地址范围。MR25H40CDF是512KB,地址范围0x00000到0x7FFFF,超过上界的地址操作不会得到反馈,读出来的数据是所有地址线为高时的无效数据。驱动里对传入地址做严格限制,尤其在使用不同批次芯片时更要小心。

3. 底层驱动实现:把MRAM当内存用

3.1 SPI外设初始化

我使用STM32 HAL库,把SPI1配成主机模式,8位数据,MSB先行,时钟极性低、第一边沿采样,也就是SPI Mode 0。MR25H40CDF也支持Mode 3,但Mode 0最省事,和大多数ARM单片机的SPI默认配置一致。

SPI_HandleTypeDef hspi1; void MRAM_SPI_Init(void) { __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_Init = {0}; GPIO_Init.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_Init.Mode = GPIO_MODE_AF_PP; GPIO_Init.Pull = GPIO_PULLUP; GPIO_Init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_Init.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, &GPIO_Init); // PA4作为片选脚,必须是推挽输出 GPIO_Init.Pin = GPIO_PIN_4; GPIO_Init.Mode = GPIO_MODE_OUTPUT_PP; GPIO_Init.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_Init); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }

预分频我选SPI_BAUDRATEPRESCALER_2,STM32F215RE的SPI1在APB2 60MHz下可以得到30MHz的SCK。如果MCU工作在120MHz之外的其他频率,记得核算实际SCK不要超过40MHz,留一点裕量更稳妥。

3.2 指令集与状态寄存器

MR25H40CDF的指令集不复杂,常用的就那么几条:

指令操作码说明
WREN0x06写使能,任何写操作前必须发送
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读取数据,支持连续读
WRITE0x02写入数据,支持连续写
RDID0x9F读ID,用于验证芯片

状态寄存器里最常用的是bit0 WEL和bit1 WIP。WEL是写使能锁存,发送WREN后置1,芯片上电或发送WRDI后清零。WIP是写进行中标志,对于MRAM来说,写入几乎是瞬时完成的,但严谨起见,可以在批量写后轮询一下WIP。

写操作前强制发一次WREN,不然芯片会把WRITE指令忽略掉。这一点和EEPROM很像,很多第一次用MRAM的人只发送WRITE指令而忘记WREN,结果数据写不进去,回读全是旧值。

3.3 单字节与多字节读写代码

单字节读、单字节写是最底层的原语。我的实现里把CS拉低后发送指令和地址,最后一个字节数据接收或发送完,再把CS拉高。CS拉高是MRAM执行内部写操作的时刻,所以整个SPI数据帧必须保持CS低,中间不能断开。

void MRAM_CS_Low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } void MRAM_CS_High(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } uint8_t MRAM_ReadStatus(void) { uint8_t cmd = 0x05; uint8_t status = 0; MRAM_CS_Low(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); HAL_SPI_Receive(&hspi1, &status, 1, 100); MRAM_CS_High(); return status; } void MRAM_WriteEnable(void) { uint8_t cmd = 0x06; MRAM_CS_Low(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); MRAM_CS_High(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t cmd[4] = {0x03, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF}; uint8_t data = 0; MRAM_CS_Low(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, &data, 1, 100); MRAM_CS_High(); return data; } void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[4] = {0x02, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF}; MRAM_WriteEnable(); MRAM_CS_Low(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, &data, 1, 100); MRAM_CS_High(); }

多字节读写不需要额外改地址。MRAM支持连续地址自动递增,一次CS低电平保持期间可以发任意长度的数据。这点和Flash非常不同,Flash一页通常256字节,跨页要自己拆包,而MRAM不存在页的概念。

void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_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(&hspi1, buf, len, 100); MRAM_CS_High(); } void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4] = {0x02, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF}; MRAM_WriteEnable(); MRAM_CS_Low(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, buf, len, 100); MRAM_CS_High(); }

多字节操作时,务必把长度控制在地址不会越过0x7FFFF。一旦地址到了末尾还继续发数据,内部地址会回绕到0,可能把你不想覆盖的地方覆盖掉。

3.4 用DMA做批量数据传输

工业设备记录数据时,经常要一次性把几十甚至上百字节的工况数据写入MRAM。如果都用阻塞方式传SPI,CPU会被占住很久。STM32F215RE的SPI1支持DMA,配合MRAM连续读写,可以做到很高效。

用DMA之前先把对应DMA流配置好,我习惯让SPI1_TX走DMA2_Stream5,SPI1_RX走DMA2_Stream2。读写命令的4字节地址头和DMA缓冲区分离,避免地址头变化影响数据。关键点是CS信号必须由DMA传输完成回调拉高,不能在启动DMA后立刻拉高。

void MRAM_WriteBuffer_DMA(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4] = {0x02, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF}; MRAM_WriteEnable(); MRAM_CS_Low(); HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Transmit_DMA(&hspi1, buf, len); } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { MRAM_CS_High(); } }

这里有个细节:DMA回调里拉高CS,要确保MRAM的连续写操作已经把最后一个数据锁存。SPI时钟在最后一个数据位之后还有一小段保持时间,DMA发送完成中断被响应时,SCK已经转完最后一拍,所以CS拉高的时机是安全的。实测用30MHz时钟,DMA搬运1KB数据耗时不到340微秒,比CPU逐字节发送快了不少。

4. 掉电保存与工业可靠性设计

4.1 MRAM为什么不怕掉电

Flash和EEPROM写入时要先建立电荷,这个过程需要时间,也存在中间状态。一旦中途掉电,写入的比特可能处于“半写”状态,读出来非0非1。MRAM的原理决定了它没有这个问题,磁隧道结在写入过程中是按位翻转,写入操作实际上就是把当前位设置为所需状态,不存在擦除等待,所以掉电对MRAM本身不会造成数据损坏。

更妙的是,MRAM在读操作过程中也不会破坏数据,读和写通路是分开的。这意味着你在掉电瞬间读到什么状态,之后上电还是那个状态。这点在工业设备上特别重要,很多设备上电后要判断“上次是正常关机还是断电停机”,有了MRAM,这个标志位可以放心地靠掉电保存。

不过硬件上仍然要注意CS引脚在上电过程中的电平。MCU的IO口在复位期间是高阻,如果CS浮空,外界干扰可能把CS拉低,让MRAM误进入SPI通信状态。我用的解决方法是CS引脚外接10k上拉电阻到VCC,保证主控还没初始化时CS是确定的无效电平。

4.2 用PVD触发紧急数据保存

MRAM本身不怕掉电,真正需要处理的是“掉电那一刻,MCU有没有把关键数据成功写进去”。最常用的方案是STM32F2系列的PVD电压检测器,设定一个阈值,当VDD跌到阈值以下时触发中断,在中断里紧急保存现场数据。

配置PVD其实很简单。STM32F215RE的PVD阈值可调,我选的是2.9V左右,供电3.3V,正常工作时不会触发,电压跌到2.9V说明外部电源已经断了。PVD中断优先级设到最高,因为这是最后的机会,等电压跌到2.4V以下时,芯片内部逻辑可能已经无法保证SPI通信。

void PVD_IRQHandler(void) { if (PWR_GetFlagStatus(PWR_FLAG_PVDO) != RESET) { // 紧急保存运行状态 MRAM_WriteBuffer(SAVE_ADDR, (uint8_t*)&DevStatus, sizeof(DevStatus)); // 清标志 PWR_ClearFlag(PWR_FLAG_PVDO); } }

由于MRAM写入速度快,这个中断里能做的操作很多。实际项目中我在一个12ms掉电测量窗口内连续写完了4K字节的运行参数,如果换成SPI Flash,根本不可能完成这么大体积的擦写。配合外部一个470uF电解电容,整套方案在掉电后甚至能维持20ms以上,绰绰有余。

4.3 数据完整性校验与双区策略

MRAM解决了“介质不会坏”的问题,但SPI链路在工业现场仍然可能被电磁干扰打乱。比如一次雷击浪涌或变频器干扰,SCK上多出一个毛刺,传输的数据可能就错了。所以不能因为用了MRAM就放弃校验。

我的做法很简单:每条记录在头部放4字节CRC32,写入时对整块数据算CRC,读取时重新算一遍。CRC不对就判定该条记录无效。如果需要更高的可靠性,可以在MRAM里划两个区,交替写入,写A区时如果掉电,下次上电发现A区校验失败,就自动回退到B区。这个思路和Flash的双备份是一样的,但MRAM不需要先擦除,所以实现起来更简单,速度也更快。

还要注意一点:MRAM虽然支持任意字节写,但并不是说连续写入一个长度超过64字节的数据也能保证原子性。MRAM内部没有页缓冲,数据是一字节一字节落进去的,如果中间掉电,前面写进去的字节已经生效,后面没写进去的保持旧值。这种“部分更新”在逻辑上是存在的,所以关键数据最好用校验和与双区配合,而不是单纯依赖介质本身的可靠性。

4.4 硬件写保护与现场抗干扰

MR25H40CDF的WP#引脚可以配合状态寄存器的保护位使用。如果在初始化时通过WRSR设置了保护,再拉低WP#,整个芯片或者某段地址区域就变成只读。这在产品出厂后防误写很有用,但调试阶段建议不要开,不然每次烧写都要先解锁,麻烦。

现场抗干扰方面,我遇到过SPI总线被变频器干扰导致读到的状态寄存器异常。后来在SCK和CS上各串了一个22欧姆电阻,靠近芯片端再对地接一个小电容,效果改善非常明显。不过加电容会影响上升沿,SCK频率高时容易劣化时序,需要权衡,一般100MHz以下的控制信号使用10到100pF的电容问题不大。

另一个容易被忽略的是HOLD#。HOLD#在工业环境下如果被毛刺拉低,芯片会暂停SPI通信,主控发完数据后收不到响应,程序卡在等待,看起来像芯片死锁了。把HOLD#用10k电阻上拉到VCC后,这个现象再没出现过。所以我在所有涉及MRAM的图纸上都会特别标注HOLD#必须上拉。

5. 调试实录:常见问题与排查方法

5.1 读回全0xFF或0x00

这是SPI设备最常见的故障。如果读到全0xFF,先怀疑芯片根本没被选中,或者SO线没接通。用万用表量CS脚在通信时是否有低电平脉冲,量SO与PA6之间是否导通。如果读到全0x00,重点检查WP#是否被拉低,同时检查是否误发了WRDI指令。

再有一个隐蔽原因就是HOLD#悬空。HOLD#被干扰拉低后,SO输出高阻,读数据时MISO线上多次采样到同一个电平,表现出固定读值。我把HOLD#上拉后,问题彻底消失。

5.2 写入正常但掉电后数据丢失

如果在线调试时能正常写入、正常读出,但断电几分钟后数据没了,那基本可以排除MRAM本身的问题。第一检查点是我前面说的CS上拉电阻,MCU复位期间CS电平不定,有可能对整个存储区执行了奇怪的写入指令。第二检查点看电源是否在MRAM最低工作电压以下时主控还在跑,这会导致SPI写入失败,但MCU无感知。

我在一块样板上遇到过一次,原因是3.3V电源掉电时跌落到2V附近,MRAM不再保证写操作,但MCU还在PVD中断里执行写函数。后来我把掉电检测阈值调高,并且在一进入掉电流程后就关闭SPI的时钟输出,不让无效的SCK继续翻转,数据就稳定了。

5.3 偶发读写错误与SPI时序问题

偶发错误排查起来比固定错误头疼。我先用逻辑分析仪抓了整个SPI帧,发现CS下降沿之前,SCK线上有几次毛刺。这些毛刺来自主控GPIO初始化时序,在硬件复位时PA5上没有默认上拉,导致SPI时钟在配置前浮动。解决方法是把SCK配置为带上拉的复用功能,并且在初始化MRAM驱动之前不要操作SPI外设的时钟。

如果还有偶发错误,可以把SPI预分频调大,比如从30MHz降到15MHz,看问题是否复现。MRAM支持40MHz,但实际设备里布线、插座、经过连接器都会降性能。工业产品不必追求极限速度,稳定更重要。实测30MHz在短走线上很稳,一旦穿过一个2.54mm排针连接器,信号质量明显下降,降到15MHz后所有异常消失。

5.4 用RDID验证连通性

调试初期我建议先写一个读ID函数,芯片能正确返回ID才能保证后续所有读写操作有效。发送0x9F操作码,CS拉低,连续读3到4个字节。Everspin的SPI MRAM返回的数据格式和传统SPI Flash不同,具体字节内容以数据手册为准,不要照搬Winbond的ID表。

我自己的排查习惯是:先读ID,再读状态寄存器,然后写一个已知数据的地址,紧接着读回比对。这三步过了,再去跑连续读写压力测试。压力测试可以每秒钟写1000次同一个地址,然后累计读回校验,跑几个小时不出现一次错误,这套驱动才敢交给产线。

最后再说一个经验:MRAM和传统存储芯片的驱动代码不要随便套用。网上很多现成的SPI Flash驱动,拿过来改个厂家ID就以为能用,结果指令集、状态寄存器和写保护逻辑全都要重新核对。宁可花20分钟把MR25H40CDF的数据手册认真读一遍,也不要等设备批量交付后因为存储故障被客户喊回现场。

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

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

立即咨询