☰
STM32与MRAM工业存储方案:告别Flash磨损与掉电丢数据
2026/10/4 1:17:35 网站建设 项目流程

做嵌入式系统的人,八成都在“数据存储”这件事上交过学费。我说的不是SD卡存照片,而是设备里那些小体积、高频次、还不能丢的数据:校准参数、事件记录、工况日志。最近一个工业网关项目里,我用一颗富士通MR25H40CDF(4Mbit SPI MRAM)配合STM32F107VC,把“存储和读取”这件事做成了一套可靠且几乎不用维护的方案。这篇文章我会从头梳理:为什么不用Flash和EEPROM、硬件怎么接、驱动怎么写、现场跑出来了哪些坑。不管是做电力终端、工业采集器还是运动控制,只要你有“频繁写参数还不许掉电丢”的需求,这套思路可以直接参考。

1. 为什么工业项目里我选了MR25H40CDF这颗MRAM

1.1 传统NOR Flash和EEPROM的痛点

先说说我原来踩过的坑。用NOR Flash存参数,最难受的是擦除机制。NOR Flash写入之前必须先擦除,而且擦除是按扇区进行的,一个扇区常常是4KB甚至64KB。如果你的应用只是修改4个字节的校准值,也得把整个扇区读进RAM、改好、擦除整个扇区、再写回去。这一套操作下来毫秒级都算快的,关键是Flash的擦写寿命通常只有十万次左右,工业设备一年不到就被磨穿了。

用EEPROM,比如24C256这种,按字节写确实方便,不用擦除,但容量普遍偏小,256Kbit也就32KB,存个配置表还行,想存点历史波形或者事件日志就捉襟见肘。而且EEPROM的写寿命一般是百万次,虽然比NOR强一个数量级,但和工业现场的写入频率比起来,仍然是很现实的瓶颈。

更麻烦的是掉电问题。NOR Flash在擦除或写入过程中突然掉电,很容易出现扇区状态不一致,轻则数据错误,重则整块失效。为了防这个,我还得额外设计双备份、启动时校验、损坏扇区隔离之类的机制,代码量蹭蹭往上涨。

1.2 MR25H40CDF核心参数与优势

MR25H40CDF是富士通的4Mbit SPI MRAM,换算过来512KB。它最吸引我的四个点:

第一,不需要擦除,直接覆盖写。这彻底改变了存储代码的复杂度——你不用再维护一个“擦除队列”,想改哪里就发一条写命令,跟SRAM一样随意。

第二,写入寿命是10的12次方,也就是一万亿次。这是Flash的十万倍,几乎可以视为“无限写”。就算每秒钟写一次同一个地址,也要三万年才能把它写坏,工业场景根本不用考虑磨损均衡。

第三,掉电不丢数据。MRAM的存储单元本质是磁隧道结,靠磁化方向记录数据,掉电后磁性状态不会变。它既不是靠浮栅电荷保存的Flash,也不是靠电容保存的DRAM,所以不存在“需要刷新”或者“电荷泄漏”的问题。你可以在任意时刻拔电,数据稳稳留在那里。

第四,读写速度快。SPI接口的最高时钟频率能到40MHz级别,而STM32F107的SPI外设上限是18MHz,所以MRAM的速度完全不会拖后腿。写一个字节连命令带地址,也就是几十个时钟周期的事,比Flash那套“读-改-擦-写”流程快了不止一个数量级。

如果要用一句话向别人解释MRAM的本质:它是一颗非易失的SRAM,或者一颗不需要擦除的EEPROM。这种特性在工业存储场景里实在太省心了。

1.3 为什么配合STM32F107VC

选MCU这part,核心原因是F107VC这块片子特别适合做工业通信加数据存储的中枢。它是100引脚的Cortex-M3,主频72MHz,内置512KB Flash和64KB RAM,外设方面直接带了以太网MAC、两个CAN、USB OTG,做电力集中器、工业网关、轨交辅助控制器都很常见。

存储这块,F107VC的SPI1挂在APB2总线上,外设时钟最高72MHz,SPI1最高可以跑到18Mbit/s。虽然比不上MRAM标称的40MHz,但只要把预分频设成4,通信速率已经够用。再加上F107VC的代码生态极其成熟,HAL库、标准库、寄存器版资料满天飞,就算中途换人也容易接手。

另一个现实考量是成本。F107VC在工业级Cortex-M3里不算贵,MRAM单价确实比Flash贵不少,但胜在省事。用一个MCU加一颗MRAM,电路简单、驱动简单、后期维护也简单,综合算下来比“MCU+高性能NOR Flash+磨损均衡算法+掉电保护电路”更划算,至少少了一大块固件复杂度。

2. 硬件连接与SPI接口设计

2.1 MR25H40CDF引脚与STM32接线

MR25H40CDF是标准8脚DFN封装,引脚不多,但DFN封装的脚间距小,设计PCB的时候要把引脚图看清楚。它的引脚包括:CS#、SCK、SI(MOSI)、SO(MISO)、WP#、HOLD#、VCC、VSS,其中WP#和HOLD#这两个脚是容易忽略的点。

我的接线方案是:

MR25H40CDF引脚接STM32F107VC说明
CS#PA4(任意GPIO)软件片选,拉低有效
SCKPA5 / SPI1_SCKSPI1时钟
SIPA7 / SPI1_MOSI主机输出到MRAM数据输入
SOPA6 / SPI1_MISOMRAM数据输出到主机输入
WP#PA3(GPIO控制)平时拉高,支持运行时保护配置区
HOLD#VCC必须拉高,不能悬空
VCC3.3V并联100nF+10uF去耦电容
VSSGND接地

SPI1的PA5、PA6、PA7这三个脚是固定的复用功能,CS我特意选了普通GPIO而不是SPI的NSS硬件引脚,原因很简单:MRAM的命令时序里,像写使能WREN和数据写入WRITE不能在同一个片选周期完成,必须分成两次拉低CS。软件控制CS才能真正自由地控制时序,硬件NSS在这种场景下反而添乱。

2.2 SPI模式与时钟速率的选择逻辑

MR25H40CDF支持SPI Mode 0和Mode 3,也就是CPOL/CPHA的四种组合里选两种。我习惯用Mode 0,也就是CPOL=0、CPHA=0:空闲时钟为低电平,数据在第一个时钟沿采样。原因没有多高深,纯粹是大部分SPI NOR和EEPROM默认也是Mode 0,后续如果要换芯片,驱动改动最小。

速率方面,STM32F1系列的SPI外设最高支持18MHz。72MHz系统时钟下,SPI1预分频选4分频,得到18MHz,刚好压线。实际使用中,如果PCB走线超过5厘米,或者现场干扰比较大,我建议降到9MHz,也就是预分频选8,保证通信余量。

这里有个细节:MRAM这种SPI设备没有内部时钟恢复机制,SCK必须干净稳定,特别是在工业现场的电机变频器附近,SCK走线过长容易被干扰。如果你的板子空间允许,SCK和MISO、MOSI这三条线尽量短、尽量平行,别在高频信号线旁边绕圈。

2.3 电源去耦、HOLD引脚和DFN焊接

电源部分,MRAM的工作电压是3.3V,电流消耗很小,待机电流微安级,运行时也就几个毫安,所以不需要大电流稳压器。但去耦电容不能省,VCC引脚旁边我习惯放一个100nF陶瓷电容加一个10uF电解电容,前者滤高频噪声,后者应对电压瞬变。

HOLD引脚必须强调:千万不能悬空。HOLD#低电平时,MRAM会暂停SPI通信,表现为MISO保持当前电平、SCK和MOSI的输入被忽略。如果这个引脚悬空,在强电磁干扰环境下被噪声拉低,整个存储器的通信就会莫名其妙“卡住”,而且你怎么查都查不到原因。正确的做法是直接接VCC,或者串联一个10k电阻到VCC,确保它稳定在高电平。

WP#硬写保护引脚,我在开发调试阶段直接接的VCC。后面想保护配置区不被误写的时候,改成GPIO控制,置高允许写,置低禁止写状态寄存器。注意WP#的具体保护范围要对着数据手册看,不同厂商的MRAM定义略有差异,比如有些只保护状态寄存器,有些连数据区一起保护。

DFN-8焊接是个体力活。热风枪加助焊剂是最稳的组合,先给焊盘上锡,再放芯片,热风枪350度左右吹到锡珠融化,镊子轻推一下芯片能自动复位,基本就成了。焊完之后务必用万用表确认相邻引脚没有短路,特别是VCC和VSS之间,DFN封装的底下那个大焊盘如果没做热隔离,很容易和旁边的引脚黏在一起。

还有个硬件层面的经验:MCU复位的时候,GPIO会变成浮空输入,CS#引脚如果恰好被干扰拉低,MRAM可能接收到一串伪命令。解决办法是在MCU的初始化代码里,第一步就把CS#配置成推挽输出并置高电平,然后再去初始化SPI外设和其他外设,这样能从软件上消除复位期间的片选毛刺。

3. 驱动代码设计与读写实现

3.1 指令集与状态寄存器

MR25H40CDF的指令集和标准SPI EEPROM很接近,核心就那么几条:

指令字节码功能
WREN0x06写使能锁存
WRDI0x04写禁止锁存
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

状态寄存器里最关键的两个位是bit0的WIP(写进行中)和bit1的WEL(写使能锁存)。WIP为1表示芯片正在执行内部写入,读数据没问题,但再发写命令会被忽略;WEL为1表示写使能已经打开,允许执行WRITE或WRSR。每次上电后WEL默认是0,所以想写入数据,必须先发一条WREN指令把WEL置1。

这里有个特别容易踩的坑:WREN本身也要占一个完整的片选周期。你必须是CS#拉低、发送0x06、CS#拉高,这样一个周期结束之后,WEL才会真正置1。很多人偷懒,CS#拉低之后连续发WREN和WRITE,结果发现数据写不进去,就是这个原因。

3.2 HAL库底层驱动封装

我用的是STM32F1的HAL库,先把SPI1初始化和单字节收发封装好。SPI1的GPIO配置和SPI参数如下:

static void MX_SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // PA4 片选,先置高 GPIO_InitStruct.Pin = GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // PA5 SCK, PA6 MISO, PA7 MOSI GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); 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_4; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); } static uint8_t MR25H40_SpiByte(uint8_t byte) { uint8_t rxByte; while (HAL_SPI_TransmitReceive(&hspi1, &byte, &rxByte, 1, 100) != HAL_OK); return rxByte; }

这里有个小事要提醒:HAL库的HAL_SPI_TransmitReceive在18MHz下单字节传输是有时间开销的,如果你的日志写入量大,可以换成DMA或者直接寄存器操作,但这套代码队调试和中小数据量存储完全够用。另外不要在中断里直接调用这个函数,否则嵌套中断容易把SPI状态机搞乱。

3.3 读数据与写数据的完整实现

读数据比较简单,发送READ指令、24位地址,然后连续读取字节,直到CS拉高结束本次访问。

void MR25H40_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); MR25H40_SpiByte(MR25H40_READ); MR25H40_SpiByte((addr >> 16) & 0xFF); MR25H40_SpiByte((addr >> 8) & 0xFF); MR25H40_SpiByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = MR25H40_SpiByte(0x00); } HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }

写数据的流程比读数据多了两步:先发WREN使能写锁存,再发WRITE指令完成写入。

void MR25H40_WriteData(uint32_t addr, const uint8_t *buf, uint32_t len) { // 第一步:WREN,完整片选周期 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); MR25H40_SpiByte(MR25H40_WREN); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 第二步:WRITE HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); MR25H40_SpiByte(MR25H40_WRITE); MR25H40_SpiByte((addr >> 16) & 0xFF); MR25H40_SpiByte((addr >> 8) & 0xFF); MR25H40_SpiByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { MR25H40_SpiByte(buf[i]); } HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 第三步:等待WIP清零 MR25H40_WaitBusy(); }

等待写完成也很简单,发RDSR命令后连续读状态寄存器,直到bit0变成0:

static void MR25H40_WaitBusy(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); MR25H40_SpiByte(MR25H40_RDSR); uint8_t sr; do { sr = MR25H40_SpiByte(0x00); } while (sr & 0x01); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }

MRAM的写入速度极快,内部写操作时间在微秒级,所以这个等待循环通常一遍就过了。但保留它是正确的习惯——万一你换了一颗其他厂家的MRAM,或者温度极端情况下内部时序变慢,有这套等待才能保证兼容性。

3.4 512KB空间的数据组织方式

MRAM最大的好处之一就是不需要做磨损均衡,所以存储区的规划可以完全按照业务逻辑来,不需要像Flash那样考虑扇区、块、页和垃圾回收。我在这套工程里的划分方式是这样:

  • 0x00000~0x0FFFF:配置参数区A,存设备编号、通信地址、校准系数,带CRC32校验。
  • 0x10000~0x1FFFF:配置参数区B,和区A内容一样,每次改动同时写两份,读取时优先选校验通过的那份。这叫双镜像备份,专门防写入过程意外掉电。
  • 0x20000~0x3FFFF:事件日志区,每条日志64字节,头两个字节是序号,循环写入。
  • 0x40000~0x7FFFF:预留区,存波形采样数据、临时变量等大块数据。

为什么配置参数需要双镜像?虽然MRAM不像Flash那样会因为掉电出现半扇区损坏,但一次多字节写入如果中途掉电,是有可能只完成前面几个字节的。双镜像加CRC校验就能识别出哪份镜像完整、哪份损坏,启动的时候自动选择健康镜像,下次写入时再修复损坏的那份。

日志区用“序号头”的方式也特别省事。MRAM允许任意地址直接覆盖写,我只需维护一个日志写指针,每次追加一条表头有序号的记录,重启后从第一条开始扫描,就能知道最后写到哪。如果是Flash,这种追加式日志还得考虑擦除对齐,MRAM完全不用。

3.5 多任务访问的注意点

在实际项目中,F107VC经常同时跑着通信协议栈和存储任务,SPI1总线不能同时被两个任务操作。我的做法是给MRAM驱动加一个全局互斥量,写操作和读操作都先拿锁。如果系统没有RTOS,就用临界区,进临界区间关中断,出临界区恢复中断状态。

尤其要注意的是,写数据过程中不能被打断,因为一个WRITE命令的片选周期一旦中断,比如被高优先级中断抢占导致CS时序拉乱,数据就会写到未知地址。即使MRAM便宜、寿命长,也不能容忍这种总线错误。所以带状态保护的临界区是必须的。

4. 工业现场的可靠性经验与问题排查

4.1 掉电瞬间的安全机制

在工业现场,掉电是最常见、也最致命的事件。MRAM的好处是掉电不丢数据,但“写入过程中的掉电”依然需要应用层做防护。比如你一次写10条日志,写到第5条的时候电源断了,那第1到第5条是有效的,后5条可能还没进存储阵列。

规避方案有两层。第一层是硬件层,用STM32F107的PVD掉电检测功能。配置好PVD中断后,当电源电压掉到阈值以下,MCU会立刻进中断。在这个中断里,我可以把最多几十字节的紧急参数写入MRAM,然后让系统安全复位。第二层是软件层,任何一片数据都带长度和CRC字段,读取的时候先校验再使用,校验失败就用备份或者标记为坏记录。

MRAM本身有很强的抗掉电能力,它的写入是原子性的,不会出现Flash那种“半个扇区擦成随机数”的情况,但多字节记录依然遵循“要么全写,要么只写了一部分”的实际,应用层的校验是最后一道防线。

4.2 SPI通信异常时的三板斧

工业环境里SPI通信最容易出现的现象就是误码。MRAM不像CAN那样自带CRC,SPI协议本身没有应答机制。所以当传输距离偏长、干扰源多时,我会做三件事。

第一,所有读操作加CRC校验。读回来的数据如果CRC不对,就重新读一次。MRAM读操作不会破坏数据,重读没有副作用。

第二,写操作之后加回读验证。写完关键配置之后,立刻把同一地址读出来比对。比对不过就重新写,连续三次失败就上报故障。这套“回读校验”在设备量产测试里非常有用,能抓出一批虚焊或者PCBA工艺问题。

第三,SCK线贴上地线保护。如果布线空间紧张,至少让SCK和GND之间走一条地线,把SCK夹在中间,能明显减少容性耦合干扰。

4.3 实际踩过的五个坑

第一个坑是最后一个字节丢失。刚开始写驱动时,我用循环调用HAL_SPI_Transmit把数据全部发出去,发完之后立刻拉高CS。结果每条记录最后一个字节经常写进去是0xFF。原因是SPI发送寄存器不是即时到位,最后一字节还没从移位寄存器送出去,CS就被拉高了。解决办法是使用上面那套TransmitReceive收发函数,它会等待TXE标志,确保每字节真正发送完成。

第二个坑是WREN没有形成完整片选周期。这个前面已经说过,CS#拉低、发0x06、CS#拉高,三步缺一不可。我调试时遇到过WEL位始终是0,查了半天发现是CS#操作顺序写反了,先拉了高才发的指令。

第三个坑是地址回绕。MR25H40CDF的地址是24位的,但有效地址只有0x000000到0x07FFFF。如果你从0x07FFFE开始写三个字节,第三个字节会回绕到0x000000,覆盖你的配置文件。这个现象和SPI NOR一样,不是芯片坏了,而是地址线位数不够。解决办法是应用层限制写入范围,凡是跨边界写入都做拆分。

第四个坑是WP#碰地。有次我把WP#接到了板子上的一个测试点,测试点默认是GND,结果所有写状态寄存器的操作都没反应,普通数据写入倒是正常,一度以为是芯片型号有问题。后来测了下电平才知道是WP#被拉低了。所以调试时,WP#直接接VCC最省心。

第五个坑是JTAG引脚复用。F107的PB3、PB4默认是JTAG相关引脚,如果你把CS#或者HOLD#接到PB3、PB4上,并且调试器还在用JTAG模式,上电后MCU会把这几个脚拉成调试模式,MRAM通信直接废掉。我这块板子CS#差点画到PB3上,还好画板前检查了数据手册。如果非要用这两个脚,记得在代码里先禁用JTAG、只保留SWD。

4.4 高低温与长期运行实测

这个方案做完之后,我在实验室跑了两个测试。第一个是日志写入压力测试:每100毫秒写一条64字节日志,连续跑一周,总共写了大约六百万次。结束后读取全部日志,逐条比对CRC,全部通过。这个数据要是换成NOR Flash,大概率已经磨掉好几轮的磨损均衡区块了。

第二个是高低温测试:用高低温箱把设备从-40度升到85度,循环72小时,过程中持续写读MRAM。读出来的数据和写入值完全一致。MRAM的数据保持能力在工业级温度范围内很稳,这也是它敢标称20年数据保持期的底气。

5. 一些想留给你参考的补充建议

做存储方案没有万能的解,MRAM也不是所有场景都合适。如果你的设备一年就写几百次配置,EEPROM绰绰有余,何必多花钱。但只要有高频次、多数据量、掉电不能丢这三个条件同时出现,MRAM几乎是唯一不用做磨损均衡就能放心跑的方案。尤其是在“读改写”场景里,Flash要先扇区备份再擦除再写回,MRAM一步到位,省掉的不只是代码,还有故障概率。

如果后续项目需要更大容量,富士通和Everspin都有8Mbit、16Mbit甚至更高容量的SPI MRAM,驱动层只需要改容量定义和地址相关参数,整体架构不用动。这套驱动移植性也很好,因为底层只依赖SPI读写和三个GPIO控制,换到STM32G4、GD32、NXP的LPC系列都是很快的事。

我个人在实际使用中最满意的其实是“调试体验”。以前用Flash做日志存储,总得提心吊胆防备擦写磨损和掉电损坏,动不动就要做坏块管理。换了MR25H40CDF之后,存储相关的故障基本绝迹,久违地感受到了“存数据就像写内存一样简单”的轻松感。工业嵌入式里能这样放心的存储方案,真不多。

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

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

立即咨询