ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MRAM替代Flash和EEPROM:工业设备非易失存储的可靠方案

MRAM替代Flash和EEPROM:工业设备非易失存储的可靠方案 做工业项目的人多多少少都被“数据保存”坑过。去年我们给一条产线做在线检测设备要把工业相机抓到的异常图编号、检测结果、设备维护日志写进非易失存储器。最开始用的是SPI NOR Flash前一个月没什么问题三个月后老化测试开始频繁出现坏块找厂家确认才发现连续写入场景根本不适合这种存储芯片。后来换了MR25H40CDF这颗4Mb SPI MRAM搭配STM32F446RE做主控才把这一摊子事彻底理顺。这篇文章就把这个方案完整拆开讲。你如果正在做工业控制、嵌入式边缘设备、视觉检测数据记录或者只是想找一颗不怕掉电、不用擦除、寿命长的非易失存储芯片可以直接参考。我会从选型逻辑、硬件接线、驱动代码讲到工业现场真正考验人的断电完整性和日志设计最后再聊聊我在调试中踩过的坑。1. 为什么我把MRAM放进工业设备而不是继续用Flash和EEPROM1.1 工业现场的存储需求和消费电子完全不同很多做嵌入式的新人有个习惯要存配置就拨一颗EEPROM要存大点日志就上SPI NOR Flash。消费电子里这套够用因为产品坏了可以返厂数据丢了用户最多骂两句。但工业现场不一样设备装在产线里一天二十四小时跑最怕三件事突然断电、反复写、高温高湿。突然断电意味着你正在写扇区的时候MCU可能瞬间失电。写Flash最怕这个因为Flash要先擦除再写入擦除过程中掉电扇区状态就变成中间态轻则这一块数据损坏重则整片固件区域不可用。EEPROM虽然能字节写但内部还是电荷泵加浮栅结构电压不稳时写入可靠性同样受影响而且EEPROM写一个字节要等几毫秒频繁写还要考虑寿命衰减。再一个就是写入频率。工业设备存日志按每三秒一条计算一天就是两万八千多条。普通SPI Flash的扇区擦除寿命通常在十万次量级听着不少但如果你用日志分区按页轮转一年就会摸到生命周期边缘。真等坏块出现时往往已经是被动状态现场换板子成本很高。MR25H40CDF这类MRAM完全避开了这两条它用磁阻结构存储数据写入过程不是“擦除再写”而是直接翻转磁状态所以写入不需要擦除命令也没有写等待时间更不会在掉电瞬间留下一个半擦除的扇区状态。对工业现场来说这一条就够值回票价。1.2 我实测下来MRAM、Flash、EEPROM的真实差距为了让你有个直观感受我列一个对比表都是我们在项目中实测过的典型数据。维度SPI NOR FlashSPI EEPROMMR25H40CDF MRAM写入方式先擦除后写字节写内部擦写一体直接覆盖写擦除动作必须按扇区/块擦除无显式擦除但写时间较长无擦除概念写一个字节的时间页写约1ms起步单字节约3~5ms一个SPI命令时间约1~2us擦写寿命约10万次量级约100万次量级10的14次方量级磨损均衡必须做通常不做但寿命有限不用做断电中间态有擦除中断容易坏块有但相对温和无写入即时完成读操作随机读很快随机读很快随机读很快我特别想提醒你注意“断电中间态”这一行。工业设备不是实验室里那种“先关机再断电”的温顺环境产线上经常是空开直接跳掉尤其在电磁阀、大电机启停的瞬间电源毛刺和掉电混在一起。Flash在这种场景里的表现很不稳定MRAM则安静得多数据到位就到位了不会给固件留下一个需要启动自检去修复的烂摊子。1.3 换MRAM之后固件架构都被动变简单了原来的Flash方案里我得维护一套磨损均衡逻辑记录哪个扇区正在用、哪个扇区擦干净了还要在启动时扫描一遍。换到MR25H40CDF之后这部分代码全部删掉了。因为不需要擦除写地址就是直接覆盖任意地址可以反复改固件里少了一个大模块反而更稳。这个“架构变简单”的点可能是很多人在选型时忽略的。少一段代码就是少一类bug。尤其在工业设备里你不可能指望现场工程师去处理存储系统的“碎片整理”能用最简单可靠的方式把数据落盘才是最务实的方案。2. MR25H40CDF的引脚、命令和访问模型其实和SPI Flash很像2.1 八个引脚分工非常明确MR25H40CDF是标准的8引脚封装外形和普通SPI NOR Flash几乎一样引脚名也差不多。核心引脚就是CS、SCK、SI、SO外加WP和HOLD再加上VCC和GND。引脚方向作用CS输入片选低电平选中芯片SCK输入SPI时钟SI输入主机写给芯片的数据SO输出芯片返回给主机的数据WP输入写保护低电平保护寄存器HOLD输入暂停通信低电平让芯片忽略时钟VCC电源3.3V供电GND电源地最容易出问题的是WP和HOLD这两个引脚。很多人把它们当作“可不管”的引脚直接悬空结果芯片莫名其妙写不进去或者通信中断。正确做法是两个引脚都通过10k电阻上拉到3.3V。WP拉高就是不启用写保护HOLD拉高就是保持正常通信。还有一点需要注意HOLD和WP在MRAM内部的逻辑通常也是低电平有效的。也就是说只要HOLD被拉低芯片就不再响应SPI时钟所有数据保持在当前状态。这在硬件上是一个静默暂停固件层面完全感知不到排查起来非常隐蔽。2.2 指令集不复杂和SPI Flash共用一套习惯MR25H40CDF的指令集非常接近传统SPI Flash所以如果你以前写过NOR Flash驱动上手几乎零成本。最常用的命令就这几条0x06写使能、0x04写失能、0x03读数据、0x02写数据、0x05读状态寄存器。写入一个字节的标准流程是先发0x06写使能再把CS拉低发送0x02写命令、24位地址、8位数据最后把CS拉高。CS拉高那一瞬间数据才真正提交到存储阵列。读数据更简单不需要写使能。把CS拉低发送0x03读命令和24位地址然后持续读取字节地址会自动递增。只要你保持CS为低就可以一次读完整片512KB不需要像Flash那样担心跨页问题。这里有个细节MRAM虽然有24位地址但MR25H40CDF实际容量是4Mb也就是512KB所以地址范围是0x000000到0x07FFFF。高字节地址实际上用不到但指令帧里还是要给足3字节否则芯片会解析错位。2.3 和Flash最不一样的地方没有了“页”和“擦除”概念SPI Flash写入时你必须先擦除整个扇区然后页写。比如一个4KB扇区你要改里面一个字节也得把4KB内容先读出来擦除再整体写回去。这个逻辑既是性能瓶颈也是断电损坏的根源。MRAM完全没这个约束它允许对任意字节直接覆盖。写一个字节和写一千字节对芯片来说没有区别。所以你在设计数据存储方式时可以像“操作一个超大SRAM”一样去操作它随便改任意地址不用考虑对齐、不用考虑扇区备份、不用考虑磨损均衡。习惯了这个思维方式之后你会发现很多以前被存储约束扭曲的设计都能回归自然。比如配置结构体可以直接原地更新日志数据可以顺序追加不需要在Flash上做环形缓冲区的“伪擦除”优化。这些都大大降低了固件的复杂度和出bug概率。3. STM32F446RE与MRAM互连的硬件设计细节全在这里3.1 为什么要选SPI1而不是SPI2时钟树已经给出了答案STM32F446RE这颗MCU有4个SPI外设SPI1和SPI4挂在APB2总线上SPI2和SPI3挂在APB1总线上。APB2最高能到84MHzAPB1最高到42MHz所以如果追求最高SPI时钟肯定是选SPI1或SPI4。MR25H40CDF的SPI时钟上限在40MHz附近而F446在APB2时钟84MHz下SPI1预分频2就能得到42MHz预分频4得到21MHz。工程上我一般不会一上来就压到最高频率尤其是连接线和工业现场干扰还未知时先以21MHz跑通全系统再根据信号质量逐步提频。21MHz下一帧写命令加数据不到一微秒对大多数工业数据记录场景绰绰有余。引脚分配我建议这样接STM32F446RE引脚对应MRAM引脚说明PA5SCKSPI1时钟PA6SOSPI1 MISOPA7SISPI1 MOSIPA4CSGPIO输出软件控制片选3.3VVCC同一电源域GNDGND共地这里用PA4做CS而不是直接用SPI1的硬件NSS。硬件NSS在接收模式切换时容易自动拉低拉高和MRAM这种需要CS全程保持的时序容易冲突。软件控制CS最可控也方便将来扩展其他SPI从设备。3.2 原理图上的几个老生常谈但必须做到位供电部分MR25H40CDF的VCC和GND之间要放一个0.1uF陶瓷电容优先靠近VCC引脚。我还会在附近加一个2.2uF的钽电容或大容量MLCC用来应付SPI高速翻转时的小幅电源毛刺。STM32F446RE的电源引脚本身已有电容但MRAM这一路不能省。WP和HOLD的上拉电阻我建议靠近芯片引脚放置而不是靠近主控。这两个信号不是高速信号但抗干扰要求高上拉电阻离芯片越近引脚被噪声拉低的概率越小。如果PCB空间允许在CS、SCK、SI、SO四根线上串22欧姆的小电阻位置靠近主控端。这能抑制振铃尤其在SPI时钟频率比较高时效果明显。不要串太大否则边沿变缓反而增加误采样风险。3.3 PCB布线和工业环境的关系比你想的更密切我见过不少在测试台上跑得好好的板子一上产线就间歇性数据错误。最大的嫌疑就是PCB布线和电源布局。SPI信号速度不高但SCK沿仍然很陡如果SCK走线旁边贴着MOSI串扰会直接把数据采样点搞乱。最稳的做法是让SCK和SI/SO之间尽量拉开距离中间有地线隔离更好。所有SPI信号线下面保持完整的参考地平面不要跨越电源分割槽。电源走线加粗至少保证MRAM供电回路压降小于50mV。另外如果设备里有变频器、伺服驱动器这类强干扰源SPI线不要和它们的功率线近距离平行走。信号线远离电感、变压器、继电器触点。真到了现场再处理EMC问题改板的成本比提前注意这些高得多。4. 在STM32F446RE上写一个干净利落的MRAM驱动4.1 SPI外设初始化直接用HAL库也能很清爽我一般直接用STM32CubeMX生成工程把SPI1配置成主机模式数据宽度8位CPOL和CPHA都选0软NSS预分频先选4分频。MR25H40CDF的时序就是SPI Mode 0这和老款SPI Flash一致别选错。static void MX_SPI1_Init(void) { 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; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 10; HAL_SPI_Init(hspi1); }PA4作为CS要配置成推挽输出、上拉、初始电平为高。MRAM的片选是高电平无效所以初始状态必须是高防止上电瞬间芯片被误选中。4.2 最小的读写函数一次操作把命令和数据发完很多人的驱动写得慢是因为把地址、数据、命令分开多次调用HAL_SPI_Transmit。每调用一次HAL内部都有额外开销而且CS的时序被拉松散。最优解是把命令、地址、数据拼成一个发送缓冲区一次发完。下面是写入单字节的示例void MR25H40_CS_Low(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); } void MR25H40_CS_High(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } void MR25H40_WriteEnable(void) { uint8_t cmd 0x06; MR25H40_CS_Low(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); MR25H40_CS_High(); } void MR25H40_WriteByte(uint32_t addr, uint8_t data) { uint8_t frame[5]; MR25H40_WriteEnable(); frame[0] 0x02; frame[1] (uint8_t)(addr 16); frame[2] (uint8_t)(addr 8); frame[3] (uint8_t)(addr); frame[4] data; MR25H40_CS_Low(); HAL_SPI_Transmit(hspi1, frame, 5, HAL_MAX_DELAY); MR25H40_CS_High(); }读数据的函数思路一样只是需要在发送完读命令后持续接收void MR25H40_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (uint8_t)(addr 16); cmd[2] (uint8_t)(addr 8); cmd[3] (uint8_t)(addr); MR25H40_CS_Low(); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); MR25H40_CS_High(); }如果你要读取大量数据比如一次读64字节也可以用HAL_SPI_TransmitReceive发送一个长度的占位数据同时接收。不过STM32的HAL实现里Transmit和Receive分别调用其实也够用因为CS在整个区间内都保持低电平。4.3 多任务系统里SPI总线和CS必须一起保护如果你的固件用了FreeRTOS或其他RTOSMRAM可能同时被参数管理任务、日志任务、网络配置任务访问。这时候必须给整个SPI访问过程加互斥锁并且锁的范围要覆盖“拉低CS到拉高CS”的完整事务不能只锁HAL_SPI_Transmit那一行。我建议封装一个带锁的访问接口void MR25H40_ReadBufferSafe(uint32_t addr, uint8_t *buf, uint32_t len) { osMutexAcquire(mramMutex, osWaitForever); MR25H40_ReadBuffer(addr, buf, len); osMutexRelease(mramMutex); }这里必须注意CS控制是GPIO操作不在HAL的SPI锁保护范围内。如果两个任务同时调用SPI一个任务拉低了CS另一个任务抢到SPI传输就会把MRAM的时序彻底打乱。只有把所有从片选到传输完成的过程放在同一个互斥锁里才能保证一次访问是一个不可分割的原子操作。5. 工业数据“怎么放”比“怎么存”更重要5.1 不要一上来就套文件系统先定义记录格式很多工程师拿到大容量非易失存储第一反应就是移植FatFS。但MRAM只有512KB如果只是存参数、存日志文件系统的目录结构、簇表、磨损均衡虽然MRAM不需要磨损均衡、文件分配表都会占掉不少空间而且断电时文件系统元数据损坏的风险并不小。我建议用更轻量的记录格式。比如定义这样一个结构体#define LOG_MAGIC 0xA55A #define LOG_RECORD_LEN 64 typedef struct { uint16_t magic; uint16_t seq; uint32_t timestamp; uint8_t data[56]; uint16_t crc16; } LogRecord;写日志时按固定地址顺序追加。每写完一条记录就把写位置指针更新到下一个空白地址。开机时扫描一遍读magic校验crc16找到第一条损坏的记录位置把它作为新的写入起点。这样即使现场断电也不会破坏已有日志最多只是最后一条不完整。MRAM不需要擦除所以这种“顺序追加然后覆盖”的环形日志模式特别自然。如果用Flash你还得额外设计一块区域记录擦除状态用MRAM则可以直接干。5.2 原子提交机制用双缓冲最简单可靠如果需要保存一组关键参数比如视觉检测阈值、运动控制坐标、通信波特率最简单的原子更新方式就是双缓冲。把参数分成两份分别放在A区和B区每条参数前面都带版本号和CRC。更新参数时先读当前生效的版本号。如果A区版本是1B区版本是2那就更新A区并把A区版本改成3。下次读取时比较两个区的版本号谁大就代表谁是新数据。如果突然断电旧的完整数据仍然保留在另一个区启动时选择版本号较大且CRC完整的那一份即可。伪代码非常简单#define PARAM_VERSION_A offset #define PARAM_VERSION_B offset uint8_t read_param(uint32_t addr, uint8_t *out) { ParamHeader hdrA, hdrB; read_region(REGION_A, hdrA, paramA); read_region(REGION_B, hdrB, paramB); if (hdrA.version hdrB.version hdrA.crc_ok) return paramA; if (hdrB.version hdrA.version hdrB.crc_ok) return paramB; return PARAM_ERROR; }这个方案的优势极其明显没有复杂的文件系统事务不需要日志重放代码量很少而且容错能力强。MRAM的字节覆盖能力让双缓冲实现起来比Flash简单得多不需要额外分配多个擦除块来交替使用。5.3 什么时候才考虑FatFS或LittleFS如果你的整个系统有配套的SD卡或者更大量存储而且需要把MRAM作为一个“小分区”暴露给用户查看那也可以考虑FatFS。但FatFS在512KB的MRAM上并不舒服簇大小、文件目录项都浪费空间。LittleFS适合Flash因为它把擦除块当作基础MRAM没有擦除块用LittleFS反而会做一些无意义的擦除模拟。我的实际建议是MRAM这种器件最适合的是简单地址管理加自定义索引。512KB不算小但也不算大做成“固定地址的配置区 环形日志区 OTA暂存区”的结构最清晰也最好维护。6. 实际调试中遇到的坑以及整个排查链路6.1 写入没反应先查WP、HOLD和CS别急着怀疑芯片有一次我们把MR25H40CDF焊到测试板上跑读取一切正常但只要写数据就失败。读状态寄存器写使能位始终不置位。排查了很久发现问题出在WP引脚上。因为原理图里WP只接了一个10k下拉电阻我用“下拉”防干扰结果WP为低MRAM的寄存器被写保护WREN命令永远无效。正确做法是WP默认上拉到高电平只有在固件明确要保护寄存器时才主动拉低。HOLD也要上拉而且要确认它没有被什么莫名其妙的初始化代码误配成输出低电平。我把这类问题的排查顺序总结成一张表照着查能少绕很多路现象第一步检查第二步检查第三步检查写完读回来还是旧值WP引脚电平是否偏低是否调用了写使能地址是否正确读时MISO全为1CS是否一直为高HOLD是否被拉低SPI时钟极性是否正确每次重启数据随机坏双缓冲校验机制电源掉电时序CRC计算是否一致偶发数据错位SPI速度是否过高布线是否过长SI/SO附近干扰源6.2 同一个SPI总线上挂了多个设备这坑更隐蔽MRAM经常和外部Flash、传感器挂在同一个SPI总线上。如果CS控制没有做好一个设备的传输结束但CS没拉高就会“霸占”总线另一个设备发命令时时钟和数据完全对不上。更隐蔽的情况是某设备的中断回调里偷偷拉低了CS导致主循环里的MRAM操作被切断。我的建议是给每个SPI从设备单独定义一个CS控制函数并在片选切换间隙增加一个极短的延时确保前一个设备彻底释放总线。比如static inline void SPI_Select(int dev_id) { SPI_DeassertAll(); SPI_Assert(dev_id); __NOP(); __NOP(); __NOP(); }三个NOP足够让信号稳定又不会明显降低性能。别小看这个细节我见过好几次“同一块板子换一颗MCU就不稳定”的诡异问题最后都出在片选切换毛刺上。6.3 掉电测试时发现的数据损坏不要全怪MRAMMRAM虽然不怕写中断但如果MCU和MRAM不在同一个电源域或者复位时序不一致主控还没完全进入复位状态就把MRAM的引脚拉成了非法电平同样可能造成数据异常。在做掉电测试时我用示波器抓过一组很典型的波形VCC掉到2.5V时STM32的GPIO还在勉强维持输出高电平但时钟已经乱了SCK上出现毛刺CS被抖动干扰。这时候MRAM即使硬件上安全但主机侧送过来的不是完整SPI帧存储的数据内容当然是不完整的。解决办法有两个第一掉电检测比较器产生中断在电压跌到STM32最低工作电压之前把CS拉高、SPI失能让MRAM脱离总线第二在所有关键数据格式里加上CRC启动时把校验不过的记录标记成无效不要当作有效数据去用。我的做法是两者都做。硬件上安排一个PVD或外部比较器软件上所有数据记录都带CRC16。这样即使掉电时刻特别刁钻最坏情况也只是丢失一条日志而不会把旧数据覆盖成无效数据。6.4 一个实用的小技巧在固件里加入存储自检命令工业设备维护时现场工程师不一定懂SPI时序但一定需要一个简单的诊断入口。我在固件里放了一条串口命令可以执行以下动作读MRAM的ID或状态寄存器、写一个固定测试地址再读回、统计芯片坏块虽然MRAM理论上没有坏块但自检能确认接口连通性。这个自检函数在产线调试时能省大量时间。设备出厂前跑一遍确认MRAM焊接和接口无误现场出问题时远程发一条命令也能快速定位是主控问题、线束问题还是芯片问题。uint8_t MR25H40_SelfTest(void) { uint8_t test_data[8] {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88}; uint8_t read_data[8]; uint32_t test_addr 0x07FFF0; MR25H40_WriteBuffer(test_addr, test_data, 8); MR25H40_ReadBuffer(test_addr, read_data, 8); return (memcmp(test_data, read_data, 8) 0) ? 1 : 0; }我习惯把自检测试地址放在存储区末尾不会干扰正常参数和日志。这个函数保留在正式固件里并不会增加多少开销但会给售后排查提供极大方便。另外还有一个经验MR25H40CDF这类MRAM的SPI通信不需要特殊的复位时序上电后可以直接读写。这和某些Flash芯片上电后需要等待内部初始化完全不同。我见过有人沿用Flash的“上电延时100ms再操作”习惯其实没必要反而拖慢了设备启动速度。MRAM上电稳定后立即操作就行但前提是VCC达到正常电压所以至少要等主控本身完成电源稳定。真正适合MRAM的场景是那些“数据必须最新、断电必须无惧、写入必须频繁”的地方。用对器件很多以前需要靠复杂软件协议来补的坑直接从硬件层面就消失了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进