ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

弃用Flash和EEPROM:MRAM在工业数据存储中的工程实践

弃用Flash和EEPROM:MRAM在工业数据存储中的工程实践 1. 为什么在工业存储场景里我弃用了Flash和EEPROM1.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.73.6V工业级温度范围和STM32L0系列配合得非常好。实际的体感和Flash完全不同。Flash写一页等待时间一般要0.5ms到几msMRAM写一个字节命令行发完就是写进去了状态寄存器里的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默认可以映射到PA5SCK、PA6MISO、PA7MOSI芯片片选我用普通GPIO控制不用SPI硬件NSS。我的连接方式如下MR25H40CDF引脚STM32L021K4引脚说明CS#PA4GPIO输出软件控制片选CLKPA5SPI1_SCKDIPA7SPI1_MOSIMCU输出到MRAMDOPA6SPI1_MISOMRAM输出到MCUWP#VDD拉高硬件写保护关闭HOLD#VDD拉高暂停功能关闭VDD3.3V接0.1uF去耦电容VSSGND接地CS#用软件GPIO而不是SPI硬件NSS是我在多个项目里的习惯。硬件NSS在多主机环境或配置不当的时候会出现莫名其妙的片选错乱尤其像STM32L0这种资源不多的芯片软件CS反而最可靠。2.2 电源、去耦和电平匹配的实际考量MR25H40CDF工作电压是2.7V到3.6VSTM32L021K4用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也就是CPOL0/CPHA0或者CPOL1/CPHA1。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; // CPOL0 SPI_Init.ClockPhase LL_SPI_PHASE_1EDGE; // CPHA0 SPI_Init.NSS LL_SPI_NSS_SOFT; SPI_Init.BaudRate LL_SPI_BAUDRATEPRESCALER_DIV4; // 32MHz/48MHz SPI_Init.BitOrder LL_SPI_MSB_FIRST; LL_SPI_Init(SPI1, SPI_Init); LL_SPI_Enable(SPI1); }时钟分频这里我用的是DIV4也就是8MHz。MRAM支持40MHzSTM32L0的SPI外设极限16MHz8MHz距离上限还很远留足了电平余量。高速不代表高可靠长走线、连接器转接的情况下我宁可降到2MHz再跑。3.2 读写命令时序写使能是关键MRAM的字节写和SPI Flash逻辑类似。第一步永远是发WREN0x06把状态寄存器里的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中断里把需要保存的运行状态写入MRAMvoid 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位。两者配合的逻辑是当SRWD1且WP#引脚为低时状态寄存器的保护位和整个存储阵列都不能被写。如果SRWD0WP#引脚只影响状态寄存器的写入不影响存储数据区。我的一个成熟方案是正常运行时把存储阵列放在可写状态但只有在PVD中断或者明确的保存指令发出时才执行写操作程序启动的初始化阶段先写一个状态标志然后立即把WP#拉低并置位SRWD开启保护。关键参数区在运行阶段完全不可写要修改数据必须先通过一个专门的维护模式解锁再写入、重新加锁。这句逻辑用代码表示就是void MRAM_EnableWriteProtect(void) { MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SPI_RW(0x01); // WRSR MRAM_SPI_RW(0x80); // SRWD1 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确实值得你为它预留一颗物料的位置。
返回列表