拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MRAM替代SPI Flash:STM32工业日志存储掉电保护实践

去年在给一套工业设备做运行状态记录模块时我把原来的 SPI Flash 方案换成了 MR25H40CDF 这颗 MRAM搭配意法半导体的 STM32L151ZD 来读写。起因是现场连续退回几台设备打开日志一查坏块、掉电瞬间写入的乱码、擦写次数快到上限各种问题搅在一起。排查下来根子都在 Flash 的块结构上而 MRAM 这种存储介质天然没有这些毛病。这篇就围绕“用 MR25H40CDF 通过 SPI 总线存储数据、由 STM32L151ZD 读取”这件事把选型思路、硬件连接、驱动实现和掉电保护完整讲一遍。如果你正在做设备状态记录、参数掉电保存、日志存储这类工作并且被擦写寿命和扇区管理折磨过这篇值得看完。1. 从 Flash 换到 MRAM工业现场日志丢失问题的根源与选型1.1 设备状态记录场景下 Flash 的两个硬伤先说说我遇到的实际问题。那台设备部署在车间里通过 MODBUS、OPC UA 这类协议从 PLC、传感器、数控机床读取运行状态数据本地缓存一份等网络空闲再补传。原来用的是 SPI NOR Flash比如 W25Q40 这种容量档位存几条日志、几个参数足够但寿命和掉电表现让我头疼。第一个硬伤是擦写寿命。Flash 的浮栅晶体管在反复擦写后会逐渐失效10 万次是普遍标称值实际受温度和电压影响很多芯片到 5 万次就开始出现坏块。我的写入频率是每 5 秒一条状态记录一天下来约 1.7 万次写入按一个扇区 10 万次寿命算不到一个星期就摸到上限。虽然可以做磨损均衡把负载摊到整片存储空间但工业设备要连续跑好几年均衡算法本身复杂一旦掉电时映射表没更新恢复出来的数据根本没法看。第二个硬伤是掉电完整性。Flash 编程和擦除都是毫秒级操作在擦除过程中断电芯片可能留下一个半擦除的扇区重新上电读出来 0xFF 和随机数据混在一起日志链直接断掉。这个现象不是某个品牌独有而是 Flash 架构决定的。对高频写入、随时可能断电的工业现场来说这种风险几乎不可接受。1.2 写入次数的账磨损均衡解决不了根本问题我算过一笔账。按每天 1.7 万次写入、可用擦写循环按 10 万次再打个对折算一个扇区一个多月就报废。用全局磨损均衡把负载摊到全部 512KB 上一年也要消耗大几万次循环对 10 万次标称寿命来说依然很险。这些数字当时让我相当焦虑因为产品要承诺三年的生命周期按这个频率算下去Flash 方案在寿命上就过不了关。有人可能会说FRAM 写寿命也长可以替代。我对比过几款 FRAM容量普遍偏小几百 Kbit 到几 Mbit 之间价格也不低MRAM 的优势是容量能做到 4Mbit 甚至更大接口就是标准 SPI替换 NOR Flash 在硬件上和软件上都比 FRAM 平滑。综合考虑容量、接口兼容性、写入速度和寿命MR25H40CDF 成了我的目标芯片。项目SPI NOR FlashMR25H40CDF (MRAM)写入方式先擦后写按扇区/页任意地址直接覆盖写擦除寿命通常 10 万次理论无限次写入时间页写毫秒级整块擦除更久微秒级无擦除过程掉电写入风险写入中掉电可能破坏扇区写入窗口极短风险大幅降低软件复杂度需坏块管理和磨损均衡可当掉电不丢的 RAM 使用1.3 MRAM 的物理特性让存储逻辑大幅简化MR25H40CDF 的存储单元靠磁隧道结的磁化方向保存数据断电后磁性不会消失这是非易失性的来源而写入时直接在单元里翻转磁化方向不需要擦除也不存在“编程前必须先清块”的操作。用大白话说它就是一个掉电不丢数据的 SRAM。这对嵌入式软件是很大的减负不用维护坏块表不用做扇区均衡指定地址直接写就行代码量和出 bug 的概率都降下来。选型确认后我把这个芯片直接焊到了原来 Flash 的位置引脚几乎兼容软件上把存储驱动替换成 MRAM 驱动日志模块很快就跑通了。下面从芯片本身说起带你看清 MR25H40CDF 到底有哪些容易忽略的细节。2. 理解 MR25H40CDFSPI 接口、指令集与那些容易看漏的寄存器2.1 引脚定义和 SPI NOR Flash 兼容但别忽略 WP 和 HOLDMR25H40CDF 是标准的 8 脚封装引脚功能对齐了传统的 SPI NOR FlashCS、SCK、SI、SO、WP、HOLD、VDD、GND。我的板子以前用的 W25Q40引脚几乎一样硬件改动很小这对老产品改造非常友好。但有两个引脚设计时必须管好WP写保护和 HOLD。有的工程师图省事直接悬空MRAM 手册里明确要求这两个引脚不能悬空否则在强电磁干扰环境里可能误入写保护或者 HOLD 状态导致写入无效或者时钟被暂停。我在 PCB 设计时给两个引脚各接了一个 10kΩ 上拉到 3.3V实测放在变频器旁边做传导抗扰测试没有再出现过写入失败。这个细节看似不起眼但到了工业现场悬空引脚的隐患会被干扰放大。SI 和 SO 是独立的两根线不像三线制器件需要双向切换SPI 通信模型很清晰。芯片支持 SPI Mode 0 和 Mode 3手册推荐 Mode 0也就是 CPOL0、CPHA0。后面 STM32 初始化时我就按 Mode 0 配置省去不少沟通成本。引脚功能连接建议CS片选接 MCU 普通 GPIO软件控制SCKSPI 时钟接 MCU SPI SCKSI数据输入接 MCU MOSISO数据输出接 MCU MISOWP写保护接 10kΩ 上拉到 3.3VHOLD保持接 10kΩ 上拉到 3.3VVDD电源3.3V就近接 0.1μF 去耦电容GND地可靠接地2.2 指令集和状态寄存器没有块擦除但 WRSR 也别急着写MR25H40CDF 的指令集和普通 SPI NOR 非常像有 WREN、WRDI、RDSR、WRSR、READ、WRITE 这些移植驱动时大部分逻辑可以复用。但它有一个典型 NOR Flash 没有的特点没有“块擦除”指令因为每个字节都能独立覆盖写想改第 N 个字节直接发 WRITE 指令带上地址和数据就完了。状态寄存器里值得关注的是 bit3WPEN和 bit7SRWD这两个位配合 WP 引脚决定写保护等级跟 Flash 的设置方式几乎一样。真正要仔细看的是 WRSR 之后芯片不会立刻接受写入需要等待内部状态就绪。你可以在 WRSR 后面轮询 RDSR 的 bit0WIP 位等它清 0 再继续。MRAM 写数据本身不需要“忙等待”但写状态寄存器这个过程还是要等一下我一开始按惯性以为写完 WRSR 立刻就能写数据结果有几条日志没进去。还有一个容易忽略的点READ 和 WRITE 指令都支持地址自动递增突发长度理论上可以覆盖整个 4Mbit 空间。因为 MRAM 没有页的概念不需要像 NOR Flash 那样处理跨页边界读取和写入的代码可以写得非常简洁。我在设计驱动的时候干脆把“页大小”这个参数彻底删除少了一层容易错的逻辑。3. STM32L151ZD 硬件连接与 SPI 外设初始化细节3.1 引脚分配与电平匹配STM32L151ZD 是 LQFP144 封装的超低功耗 MCUSPI 外设不止一个。我用的 SPI1对应引脚分配如下表。片选用普通 GPIO 而不是硬件 NSS因为 NSS 在主模式下经常被外设自动拉低拉高配合低速器件时容易出现时序抖动软件控制反而干净。MR25H40CDF 引脚STM32L151ZD 引脚说明CSPB6软件片选默认拉高SCKPB3SPI1_SCKSIPB5SPI1_MOSISOPB4SPI1_MISOWP3.3V10kΩ 上拉HOLD3.3V10kΩ 上拉STM32L151ZD 的 IO 部分支持 5V 容忍但 MRAM 是纯 3.3V 器件所以全部按 3.3V 电平驱动。如果想省事直接接 5V 逻辑芯片会有烧毁风险这一点我在硬件评审表里特别标注过。另外去耦电容要就近放在芯片电源引脚旁边量的是芯片引脚上的电压而不是电源模块输出端的电压很多“偶发写不进”的问题其实都是电源纹波造成的。3.2 SPI 初始化代码与时钟配置初始化代码我用的是 STM32 标准外设库风格HAL 库也是同理。void mram_spi_init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // CS: PB6 输出推挽默认高 GPIO_StructInit(gpio); gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); GPIO_SetBits(GPIOB, GPIO_Pin_6); // SCK/MOSI/MISO: SPI1 复用推挽 gpio.GPIO_Pin GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); // SPI1: Mode 0, 8bit, MSB first SPI_StructInit(spi); spi.SPI_Direction SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_1Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }时钟方面MR25H40CDF 最高 SPI 时钟可以跑到 40MHz 以上但 STM32L151ZD 的 APB2 在 32MHz 时SPI 预分频最小只能到 2所以实际跑 16MHz。这个速度对读日志、写状态记录来说绰绰有余我也没有刻意去追求极限频率。16MHz 下信号沿比较快PCB 布线和去耦如果没做好波形容易振铃后面我会专门说。3.3 高速读写时的 PCB 与电源去耦注意MRAM 毕竟是高速器件16MHz 时钟下 SO/SI 信号沿已经很快了如果拿杜邦线飞线拉得很长波形会振铃读回来的数据偶尔会错。我的做法是把 SP1 信号线尽量短并且让 SCK 和 SI 走线长度接近减小时钟和数据之间的相对延迟。电源引脚旁边放了 0.1μF 陶瓷电容位置尽量靠近芯片CS 到地加了一个 4.7nF 的小电容做缓启动。HOLD 引脚的 10kΩ 上拉电阻也要离芯片近我一开始放在板子边缘测试时发现干扰环境下偶尔出现时钟暂停问题把电阻挪到芯片附近后现象消失。这些细节在实验室未必暴露到了工业现场设备旁边可能有大电机、变频器干扰源一开存储时序问题就现原形。4. 读写驱动实现从状态轮询到数据校验的完整代码4.1 基本读操作与地址位宽读数据就是拉低片选发 READ0x03指令跟普通 NOR Flash 完全一致。注意地址是 24 位而 4Mbit 换算成字节是 512KB所以地址高 8 位必须为 0否则会读到错误位置。我第一次写驱动时把地址打包成 16 位日志存储区后半部分读出来的数据全对不上排查半天才发现是高字节没处理。int mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; if (addr len MR25H40CDF_SIZE) return -1; cmd[0] MRAM_CMD_READ; // 0x03 cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; mram_cs_low(); SPI_WriteRead(SPI1, cmd, buf, 4 len); mram_cs_high(); return 0; }片选拉低后再发命令整个过程保持片选为低读完后拉高。读操作没有写保护和忙等待的限制只要地址合法随时可以读。这里有一个小经验如果读出来的是全 0xFF 或者规律性的数据优先查地址移位和 SPI 模式而不是怀疑芯片坏了。4.2 写操作WREN 是必须的片选时序更要守住写数据前必须先发 WREN0x06使能写操作这是 SPI 存储器的通用逻辑芯片上电后默认写保护必须先解锁。写完 WREN 之后要立刻发 WRITE0x02指令再跟地址和数据中间不要让片选拉高否则 WREN 状态会丢失。int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4]; if (addr len MR25H40CDF_SIZE) return -1; // WREN cmd[0] MRAM_CMD_WREN; // 0x06 mram_cs_low(); SPI_SendByte(SPI1, cmd[0]); mram_cs_high(); // WRITE cmd[0] MRAM_CMD_WRITE; // 0x02 cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; mram_cs_low(); SPI_SendBuffer(SPI1, cmd, 4); SPI_SendBuffer(SPI1, buf, len); mram_cs_high(); return 0; }这里有一个我实测后印象深刻的点MRAM 的 WRITE 指令期间片选必须始终保持为低不能中途拉高再拉低否则芯片认为当前传输结束后面数据会被丢弃而且这种丢弃不会报错。在 NOR Flash 上很多人习惯一字节一字节发到 MRAM 上这样操作很容易出现“数据没写进去但程序以为写成功了”的隐蔽问题。另外写入数据本身不需要等 WIP 清 0因为磁化翻转发生在纳秒级这是 MRAM 相比 Flash 的巨大优势。如果你还是不放心可以连续多个 WRITE 之间插一次 RDSR检查 WIP 位求个心理安慰。4.3 环形日志封装与接口我的应用不是简单地“读一个地址、写一个地址”而是做成了环形日志缓冲区。头部存最新记录索引和魔数数据区按记录块排列。每次写入新记录先读头部索引计算新块写地址写入数据块最后更新头部索引。这个结构中关键点只在一小块头部区域上正好发挥 MRAM 任意地址直接覆盖写的优势。typedef struct { uint32_t magic; // 0x4D52414D uint32_t seq; // 递增序号 uint16_t len; // 数据长度 uint16_t crc; // CRC16 uint8_t payload[256]; } log_record_t; int log_write_record(log_record_t *rec) { uint32_t head_idx, addr; uint8_t head[8]; mram_read(LOG_HEAD_ADDR, head, 8); head_idx *(uint32_t *)head[4]; addr LOG_DATA_BASE head_idx % LOG_COUNT * sizeof(log_record_t); if (mram_write(addr, (uint8_t *)rec, sizeof(log_record_t)) ! 0) return -1; head_idx; mram_write(LOG_HEAD_ADDR, (uint8_t *)head_idx, sizeof(head_idx)); return 0; }环形日志的容错设计是先写数据块再更新头部索引。如果写入数据块时掉电头部索引还是旧值下一条记录会覆盖旧数据但不会破坏整个日志链如果更新头部时掉电只丢一条索引数据块完好无损。这种“先数据后索引”的顺序配合 MRAM 的无擦写特性写起来非常顺手换成 Flash 我还要考虑新旧块交替和擦除失败的各种边界情况。5. 掉电保护设计与实测设备状态记录场景下的验证结果5.1 掉电瞬间的写入风险分析与欠压处理用 MRAM 之后掉电风险主要不在存储介质而在 MCU 的供电。因为 MRAM 写入太快只要 MCU 在掉电瞬间还能维持几百微秒的供电SPI 总线上完成一条 WRITE 指令是完全来得及的。我的做法是在 3.3V 电源轨上加一个大电容让 MCU 检测到掉电后可以继续运行一段时间把关键状态写进 MRAM再从容停机。代码层面用了外部中断做欠压检测在中断里完成最后的现场保存#define PWR_LOSS_EXTI_PIN GPIO_Pin_15 void EXTI15_10_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line15) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line15); // 保存设备状态 dev_state_t state load_dev_state(); mram_write(DEV_STATE_ADDR, (uint8_t *)state, sizeof(state)); // 关闭不必要外设 SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; } }欠压检测的中断优先级要设到最高不能被其他业务逻辑阻塞。还有一个小技巧掉电写入期间把 SPI 时钟降到最低稳定档位比如 2Mbps可以提高极端情况下的成功率。别小看这一步我用示波器抓过掉电时序电压跌破 3.0V 之后 SPI 信号会开始变形降速后写入窗口明显扩展。5.2 数据完整性校验方案工业现场电磁干扰强光靠存储芯片自身可靠还不够协议层要有校验兜底。我用了两个手段一是每个记录块加 CRC16二是关键索引块连续写两份互为备份。读取时先对数据块做 CRC 校验校验失败就尝试备份块。MRAM 没有擦除操作备份块的更新逻辑非常简单不需要处理 Flash 那种“新旧块交替”的写保护策略。CRC 计算可以查表也可以按位算我按位算了几百条记录性能完全够用。代码示意uint16_t log_crc16(const uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc ^ data[i]; for (int b 0; b 8; b) { if (crc 1) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }实际使用中CRC 校验失败的概率极低三个月里只出现过一次那一次还是因为我用杜邦线飞线调试时线头松动导致的接触不良。装上正式 PCB 后日志校验全程通过证明硬件设计对可靠性的影响远大于芯片本身的差异。5.3 现场实测数据与结论再回到最初的应用场景。设备每 5 秒通过 MODBUS/OPC UA 从 PLC、传感器、数控机床采集一次运行状态数据写入 MRAM 保存同时需要及时读取这些数据用于判断设备健康状况。连续运行三个月写入约 150 万条记录读写全部正常没有出现坏块、乱码或者掉电后数据丢失的情况。对比之前 Flash 方案每两周就需要重新格式化日志区的遭遇这个结果让我基本确定在频繁写入工业干扰掉电随机的场景里MR25H40CDF STM32L151ZD 是可靠的。更重要的是开发节奏变了原来花在磨损均衡和掉电恢复上的时间全部省下来我才能在一个迭代周期里把日志读取、上位机对接这些功能打磨得更细。5.4 避坑清单把这次项目里踩过的坑和注意事项整理一下方便你直接对照检查不要用写 Flash 的思维写 MRAM不用擦除、不用跨页处理但 WREN 和片选时序必须严格遵守。HOLD 和 WP 引脚不要悬空用 10kΩ 上拉到 3.3V电阻靠近芯片放置。地址是 24 位写入地址变量时别只传低 16 位。WRITE 命令期间片选必须全程保持低电平中途拉高会导致剩余数据静默丢弃。掉电保护的重点在 MCU 供电维持而不是存储芯片本身电源轨电容和欠压中断是关键。工业现场建议加 CRC 校验和冗余存储MRAM 再可靠也扛不住外部线路的物理接触问题。实际上如果你手里有同款芯片可以把 SP1 时钟先降到 8MHz 跑通基本读写再慢慢提频到 16MHz 甚至更高。这样即使 PCB 布线不太理想也能先把功能验证完后续再优化速度排错会容易很多。最后再分享一个实际体会如果你的产品既要跑常温环境又要扛住工业现场的强干扰建议第一版就把电源、复位、去耦按工业级标准设计而不是等出了问题再补。MR25H40CDF 本身很稳定但任何存储芯片都怕电源毛刺和地弹这算是整个项目里我最有感触的一点。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门