☰
工业数据记录选型:SPI MRAM如何替代Flash并实现掉电保存
2026/10/4 6:19:39 网站建设 项目流程

1. 选型背景:为什么工业侧的数据记录选了SPI MRAM而不是Flash

前阵子做一台工业设备的运行数据记录模块,要求掉电保存、高频写入、环境温度经常逼近70℃,还得在几毫秒内把当前状态写完。第一版图省事用了串行Flash,跑了一个月就发现问题:日志越写越慢,擦除时间不稳定,断电测试还偶尔出现整扇区花掉的情况。后来把存储芯片换成Everspin的MR25H40CDF,挂在STM32F723IE的SPI1上,这套方案才真正稳定下来。这篇文章就是记录我自己的选型、接线、驱动和调试过程,希望能给同样做工业控制、嵌入式数据采集的人一个可复用的参考。

1.1 工业设备对存储介质的三个硬性要求

工业环境下的数据记录和消费电子产品不太一样。消费级产品里掉个文件、丢一份配置,重启恢复出厂设置就能接受;工业现场不行,伺服参数、相机标定、产线配方、故障时刻的状态量丢了,轻则停机重调,重则要返工排查事故原因。我在这个项目里给自己列了三条硬指标。

第一是温度范围要宽。户外机柜夏天能到60多度,产线设备旁边就是发热源,电池供电的SRAM在这个温度下泄漏电流会明显变大,普通Flash在高温区的数据保持时间也会缩短。第二是写入频次要高。日志型应用往往每个控制周期都写数据,按10Hz频率算,一天就是86万次写入,传统Flash标称10万次擦写寿命在这种场景下撑不过一周。第三是掉电时刻不能丢数据。设备可能在写一半的时候突然断电,如果存储介质本身写入延迟高,就很容易留下半截脏数据。

这三个约束叠加,普通NOR Flash和EEPROM很难同时满足。NOR Flash擦除慢、按扇区操作,小记录反复写还要额外做磨损均衡;EEPROM虽然按字节写,但容量小、写耐受力也一般。MRAM恰好把这三个短板都补上了:非易失、随机字节写入、极高的写寿命。这也是我最终看向MR25H40CDF的原因。

1.2 MR25H40CDF与Flash、EEPROM、FRAM的本质差异

MR25H40CDF是一颗4Mbit的SPI接口MRAM,本质上是磁阻存储器,不是靠电荷存储,所以没有擦除概念。这意味着它和NOR Flash的底层操作逻辑完全不同:Flash写一个字节前得先把所在扇区擦成0xFF,MRAM不需要,直接往目标地址写新值就行,读改写照常进行。说得直白一点,它用起来更像一颗非易失的SRAM,只是接口是SPI。

和EEPROM比,MRAM的写寿命和写速度都高出几个数量级。串行EEPROM很多型号标称100万次擦写,写一个字节还需要等待几毫秒内部计时;MR25H40CDF在写命令发出后,数据几乎是即时进入存储层的,不需要专门轮询“写忙”标志。和FRAM比,FRAM也有高写寿命和随机写能力,但大容量串行FRAM的可选型号比较少,MRAM在4Mbit这个容量上的选择更丰富,且宽温型号覆盖更全。最后还要提一句带电池的SRAM,那玩意儿虽然写寿命无限、速度快,但电池在工业现场是维护隐患,能不用我尽量不用。

下表是我在选型时列的一个简化对比,方便后面同事参考:

介质类型按字节随机写写寿命写入延迟是否需要擦除掉电保持
NOR Flash否10万级毫秒级是是
EEPROM是百万级毫秒级否是
FRAM是极高纳秒级否是
带电池SRAM是无限纳秒级否需要电池
MR25H40CDF是极高SPI时钟级否是

对很多嵌入式同事来说,MRAM一开始最需要扭转的认知就是“这不是Flash”。只要把“擦除”两个字从脑海里删掉,很多设计都会顺起来。

1.3 容量、寿命与写入耗时的提前估算

选型不能只看芯片指标,还得算账。MR25H40CDF容量是4Mbit,换算下来就是512KByte。我在设计时按一条记录64字节算,最多可以放8192条日志。如果内部做双缓冲,一半空间做数据区一半做索引,也能放四千多条。对一个按分钟记录运行状态的小型设备来说,这个容量够用;如果嫌不够,可以选更大容量的MRAM系列,但需要把地址长度和页尺寸相关的底层驱动一起调整。

写入耗时也要提前估算。假设SPI时钟跑13.5MHz,写一条64字节记录需要先发4字节命令头(命令字加3字节地址),再加上64字节数据,总共68字节。每个字节8个bit,568个bit,换算下来大概42微秒。再加上GPIO翻转片选和命令间隔的开销,实测在45微秒左右。这个速度意味着设备在断电瞬间只要还有几十微秒的供电余量,就能把一小条记录安全落地。

寿命方面更不用纠结。MRAM的写耐受力极高,工程上可以认为它比MCU本身寿命还长,完全不需要像Flash那样设计磨损均衡层。当然,具体数字还是建议以Everspin官方数据手册为准,不同批次、不同温度范围可能有细微出入,但不影响结论:在工业日志记录场景里,MRAM的寿命余量非常充足。

2. MR25H40CDF的底层行为:引脚、状态寄存器和命令流

2.1 引脚功能与最小硬件连接

MR25H40CDF是标准SPI从器件封装,常见引脚包括CS片选、SCK时钟、SI数据输入、SO数据输出、WP写保护、HOLD保持、VCC和GND。第一次画原理图时,很多人会把这颗芯片当成普通Flash来画,结果把WP和HOLD引脚悬空了。这在实验室环境有时候能用,但在工业现场很容易出现偶发写保护或者时钟被HOLD干扰的问题。

我的建议是最小系统里把WP引脚直接接VCC,让块保护功能不生效;HOLD引脚也接VCC,让SPI通信永远不被暂停。如果设计上确实想用WP做硬保护,那就必须用GPIO控制它,而不是随手上拉到VCC了事。CS片选一定要用MCU的GPIO控制,不能直接接地,否则没法区分每次命令的起始和结束。SCK、SI、SO对应STM32F723IE的SPI引脚,接法按照“SI接MOSI、SO接MISO”来就行,别搞反。

供电上,VCC旁边要放一个100nF去耦电容,并尽量靠近芯片电源引脚。如果板子上MCU和MRAM共用一路电源,还要留意MCU大电流瞬态对MRAM供电的影响,必要时加磁珠或者RC隔离。这些细节在原理图阶段不注意,到EMC测试阶段会很痛苦。

2.2 状态寄存器与写使能锁存

MR25H40CDF虽然是MRAM,但它的SPI指令集基本沿用了串行Flash那套,所以状态寄存器仍然存在,只是含义有区别。最需要关注的是WEL位,也就是写使能锁存位。每次发送写命令后,芯片并不一定会执行写入,必须先把WEL位置1。

置位方式是通过WREN指令,也就是0x06。发送WREN时片选拉低,把0x06发过去,然后片选拉高,WEL位就变为1。写完数据后,WEL位会自动清0。这意味着如果你打算分多次写,每次写之前都得重新发WREN。我见过不少同事从Flash经验迁移过来,直接把WREN省了,结果数据怎么都写不进去,最后读状态寄存器才发现WEL一直是0。

状态寄存器里还有块保护位BP0、BP1和WPEN位。如果这些位被配置成了非零值,即使WEL为1,受保护区域的写入也会被拒绝。我实际遇到过芯片在调试过程中被误写,BP位变成01,结果从0x20000开始怎么都写不动。排查方法很简单,发0x05读状态寄存器,看低两位是不是00,不是就发WRSR把它清掉。

2.3 读、写和快速读的命令格式

读数据用0x03命令,格式是:片选拉低、发送0x03、发送3字节地址、然后持续读MISO上的数据。读多少字节完全由你保持片选的时间决定,想读1字节就拉高片选,想读一整块就持续读下去。MRAM的读是非破坏性的,读完不会改变存储内容,不用像某些存储器那样担心读操作会翻转数据。

写数据用0x02命令,但前提是先发0x06把WEL置1。写命令格式和读几乎一样:0x02加3字节地址加要写入的数据,片选拉高的瞬间写入生效。因为MRAM不需要擦除,也不存在Flash那种“页编程”概念,你可以在一次片选低电平期间连续写任意长度的字节,地址会自动递增。需要注意的是,片选在整个命令期间必须保持稳定,中间任何抖动都可能导致命令被芯片丢弃。

如果SPI时钟跑得比较高,手册上可能还提供快速读命令0x0B,它和普通读的区别是在地址后面多了一个dummy字节,方便总线上有足够时间翻转方向。我这次SPI时钟只跑了13.5MHz左右,所以直接用0x03就够了,不需要Fast Read。

3. STM32F723IE硬件侧落地:引脚分配与SPI外设配置

3.1 F723上的SPI引脚选择和PCB布局要点

STM32F723IE是Cortex-M7内核、主频最高216MHz,外设资源非常丰富,SPI、QSPI、CAN、以太网都带。这类芯片在工业主板上通常不只是做存储读写,可能还兼着通信、采集和控制,所以给MRAM分配引脚时要提前留好布局。

我这次用的是SPI1,引脚分配为SCK、MISO、MOSI分别接到PA5、PA6、PA7,CS用PA4作为普通GPIO输出。选择PA4是因为它靠近SPI1的固定引脚,走线短,而且它可以不用硬件NSS功能,纯粹当成软件片选口用。这里有个提醒:STM32的SPI外设虽然有硬件NSS,但软件控制片选更灵活,尤其是做多字节连续传输时不会因为NSS状态变化导致通信半途中断。

PCB走线方面,三条SPI信号线尽量等长、短走,避免跨越分割地平面。如果板上有电机驱动或者开关电源,最好在SPI信号线上加22欧姆到33欧姆的串联电阻,抑制振铃。工业设备还要考虑ESD防护,SPI引脚如果可能被引出到外部接插件,建议加TVS管。但MRAM一般就在板上,不引出的话可以不加。

3.2 SPI模式、时钟分频与CS控制方式

MR25H40CDF的SPI协议支持模式0和模式3,也就是CPOL和CPHA的组合都可以用。我统一用的模式0:CPOL为0,CPHA为0,空闲时钟为低,数据在第一个边沿采样。这和我板子上其他SPI从器件一致,驱动代码也最通用。如果项目里其他器件强制要求模式3,MRAM一般也能跑,但我在工程上不混用,避免排查问题时多一点干扰因素。

初始化SPI时,设置主模式、8位数据帧、MSB先行。时钟分频我选了APB2时钟的8分频。F723的APB2外设时钟是108MHz,8分频后SPI时钟约13.5MHz。这个频率不高,但换来了很强的抗干扰余量,而且日志写入45微秒的时间完全够用。如果后续要提高吞吐,把分频调到4就是27MHz,再往上就要仔细量波形了。

CS控制只需要记住一个原则:一次完整的命令必须从CS拉低开始、到CS拉高结束。初始化阶段把PA4输出设置为高电平,平时保持CS为高。每次通信前拉低,通信完成后马上拉高。不要用SPI外设的硬件NSS自动控制,因为它和软件时序配合起来容易出奇怪的竞争问题。

3.3 用DMA减少数据记录对CPU的占用

工业主板上MCU不可能只顾着写日志,它还要跑控制算法、处理通信、响应中断。写一段几十字节的数据虽然只有几十微秒,但如果每秒要写几十条,累积起来对CPU的占用就不可忽略了。解决办法是让SPI配合DMA传输。

最简单的做法是把命令头和数据拼成一个连续缓冲区,然后用HAL_SPI_Transmit_DMA发给SPI。命令数据全部发送完毕后,SPI的DMA传输完成中断里再把CS拉高,结束本次写命令。注意CS必须等DMA完全发完才能拉高,否则会截断最后一个字节。我见过有人把CS拉高的代码放在发起DMA之后,结果每次都少写一个字节。

用DMA的代价是需要管理缓冲区生命周期。工业代码里最好用静态数组或者带信号量的任务模型,避免DMA还在写的时候缓冲区被其他地方修改。如果项目相对简单,日志频率不高,完全不用DMA,直接在中断里阻塞发送几十微秒也能接受。这个选择取决于你对CPU实时性的要求,不必为了“用DMA”而用DMA。

4. 驱动与数据模块实现:从单字节读写到固定槽位日志

4.1 最小编码:WREN、读、写、校验四件套

下面这段是我实际在F723上跑的简化驱动,基于STM32Cube HAL库。先定义MRAM的SPI句柄、片选引脚和命令字,然后写最小读写函数。代码里的宏定义和引脚变量需要根据你的工程替换。

#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET) static void mram_write_enable(void) { uint8_t cmd = MRAM_CMD_WREN; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); MRAM_CS_HIGH(); } static uint8_t mram_read_status(void) { uint8_t cmd = MRAM_CMD_RDSR; uint8_t txdummy = 0x00; uint8_t status = 0x00; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); HAL_SPI_TransmitReceive(&hspi1, &txdummy, &status, 1, 10); MRAM_CS_HIGH(); return status; } void mram_write_bytes(uint32_t addr, const uint8_t *data, uint32_t len) { uint8_t header[4]; header[0] = MRAM_CMD_WRITE; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; mram_write_enable(); MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, header, 4, 10); HAL_SPI_Transmit(&hspi1, (uint8_t *)data, len, 10); MRAM_CS_HIGH(); } void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; header[0] = MRAM_CMD_READ; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, header, 4, 10); HAL_SPI_Receive(&hspi1, buf, len, 10); MRAM_CS_HIGH(); }

读状态函数要注意,SPI是双向总线,发完0x05之后要再发一个字节的假数据才能把SO上的状态读回来。写函数里的mram_write_enable是必须的,漏了这句就是写得进去。写完拉高CS之后,如果还想验证,就调用mram_read_bytes把数据读回,逐字节比较。工业代码里我建议每一步关键写入后都做读回校验,MRAM写坏的概率极低,但总线上有干扰时读回对比能最快暴露问题。

4.2 固定长度记录的槽位组织和回环寻址

日志系统最常用的是固定长度记录加环形地址。分配一块数据区,把空间分成N个槽位,每个槽位大小相等,比如64字节。每条新日志写入时,根据当前记录序号计算出目标地址,写完后更新一个“最新记录位置”的索引。这种设计最大好处是寻址简单,重启之后也能很快定位到当前写入点。

计算公式就是base_addr加槽位序号乘槽位大小。比如数据区基地址是0x00000,槽位大小64,第42条记录就写到0x00000 + 42 * 64 = 0x00A80。环形处理也很简单,记录序号不断增加,但地址只在0到N-1之间循环。索引区存储当前序号,读取时先找到最新一条,再向前追溯。

由于MRAM不需要擦除,槽位覆盖写非常干净。旧的64字节数据会被新数据直接替换,不会出现Flash那种“必须先擦后写、中途断电导致旧数据和新数据混在一起”的恶心情况。这也是我后期为什么彻底放弃Flash方案的原因之一:环形日志在Flash上最难处理的磨损均衡和扇区擦除,在MRAM上几乎不存在。

4.3 掉电记录场景下的双缓冲与版本保护

虽然MRAM写入速度快,但工业设备掉电可能发生在任何一条指令之间。为了做到“断电瞬间也能恢复上一条有效记录”,我设计了双缓冲加提交标记的方式。原理很简单:每条日志预先准备两个槽位,先把完整数据写入A槽,再写B槽,两个槽都写完以后,最后写一个提交标记到固定控制区。读取时先看提交标记,再决定使用哪个槽位的数据。

实际代码里不需要做得很复杂。控制区可以放两个字节的magic数,比如0xA5和0x5A,配合写入序号一起用。每次启动时先扫描控制区,如果magic有效,说明上一次掉电时整条记录已经完整提交;如果magic无效,说明数据可能写了一半,就回退到上一个有效记录。由于MRAM写入速度极快,数据区和提交标记之间的时间窗口非常短,这套机制在实测中表现很好。

需要特别说明的是,MRAM解决的是“存储介质写入中断后的数据完整性”问题,不等于解决了“掉电瞬间系统能不能来得及触发保存”的问题。想做到断电瞬间自动保存,还需要MCU供电端加掉电检测和足够的保持电容,让系统在电压跌落时还能运行几毫秒。这个属于电源设计范畴,但和存储配合好了,整套掉电记录方案才完整。

5. 实测结果、性能数字和调试避坑

5.1 实测读写速率和单条记录写入耗时

我在这块F723板子上实际跑出来的数据是这样的。SPI时钟13.5MHz,写一条64字节日志,包括发送0x02命令、3字节地址、64字节数据和片选翻转时间,整体耗时在45到50微秒之间。实际测量时我在写函数前后翻转一个GPIO,用示波器看高电平宽度,误差很小。读取64字节日志大概在42微秒左右,读取速度对完整日志上传也够用。

整片512KByte空间全量读取一次,按13.5MHz算,理想时间约310毫秒,实际读完整片差不多340毫秒。这个数字对开机自检来说可以接受。如果将来要做整片备份,可以把SPI时钟提高到27MHz,全量读压缩到170毫秒左右,但对PCB走线和波形质量要求更高。

写入耐力的工程验证我做了个压力测试:连续对同一地址写入100万次,每写一次读回一次校验,全程没有检测到一次校验失败。这个实验不能完全证明MRAM寿命,但至少说明在这个板子和这个时序参数下,通信链路是稳定可靠的。我后来又把测试地址固定到同一字节,跑了一整夜,第二天早上起来再读,数据依然正确。

5.2 踩过的坑:WP/HOLD浮空、SPI模式错误、伪读回

讲几个实际踩过的坑,每一个都花了我不少时间排查。

第一个坑是HOLD引脚悬空。第一版原理图把HOLD引脚漏接了,板子拿回来基本功能都能跑,但偶发写失败,而且不是固定地址失败,是东一榔头西一棒子。用逻辑分析仪抓波形,命令和电压看起来都正常,后来翻数据手册才想到HOLD引脚没处理。悬空的HOLD在磁场干扰较大的工业环境下可能被拉低,芯片就进入暂停状态,后续指令全部不认。解决办法是把HOLD和WP都直接接VCC,再没出现过这类问题。

第二个坑是忘发WREN。从Flash迁移过来时,我一度以为SPI存储器的写命令都是直接发0x02就行,结果写操作全部无效,读回来的数据全是0xFF。当时我甚至怀疑芯片是坏的,换了一颗还是一样。后来读状态寄存器发现WEL为0才反应过来。这个坑其实非常好避免,只要在每次写函数的第一行调用mram_write_enable,然后调试时先读一下状态寄存器确认WEL为1,就能快速排除问题。

第三个坑是SPI模式错配。某次我为了调试另一个SPI器件,把SPI1的CPOL和CPHA顺手改成了模式3,结果MRAM读写紊乱。原因不是MRAM完全不支持模式3,而是我当时板子上其他器件只能跑模式0,两者混在一起,让排查变得非常混乱。这种事不在技术难度,在于工程管理。一个工程里多个SPI从器件尽量统一SPI模式,不要给调试埋雷。

第四个坑是读回“伪成功”。有几次写完数据,读回校验也通过了,但掉电后重新上电数据又不对。后来发现是板子供电不稳,写操作时VCC跌到芯片最低工作电压以下,MRAM可能进入了欠压状态。这个问题和存储芯片本身关系不大,但我当时第一反应是换芯片,走了弯路。排查掉电保存类问题,第一步永远是检查电源波形。

5.3 调试顺序:逻辑分析仪、状态检查、与Flash习惯的差异

如果你也准备把MRAM用进自己的嵌入式项目,我建议调试顺序按这个来:先发RDSR读状态寄存器,确认芯片通信基本正常;再对某个地址写一个字节,读回来对比;然后写连续多字节,验证地址递增;最后才上日志系统。每一步都确认无误再进行下一步,基本能避开90%的坑。

逻辑分析仪是排查SPI问题的最好工具,别省。直接把CS、SCK、MOSI、MISO四根线夹到板子的测试点上,抓一次写操作,看CS低电平时序是否符合预期,命令字和地址是否发全,最后有没有在CS拉高前把数据发完。波形正常而读回不对时,优先怀疑硬件连接和数据位方向。

最后还是要强调一下和Flash的习惯差异。有些同事拿到MRAM会不自觉地去找“扇区擦除”“页编程”“磨损均衡”的代码片段移植过来,这些都是多余的。MRAM的思维模型就是“SPI接口的SRAM”,地址随便写,字节随便改,不用擦除也不用等忙。把这些习惯扭过来之后,MRAM用起来会顺手很多,我也因此在后续好几个项目里,凡是涉及工业数据记录,第一选择基本就是它。

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

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

立即咨询