ARTICLE DETAIL

资讯详情

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

用MRAM替代Flash:工业控制器防掉电丢数据的完整存储方案

用MRAM替代Flash:工业控制器防掉电丢数据的完整存储方案 做工业控制器的头两年我一直被“掉电丢数据”这件事折腾得够呛。设备在现场今天要存标定参数明天要存工艺配方操作工赶时间直接拉闸是常态。后来一套测试仪器项目里我换了一套存储方案用 Everspin 的MR25H40CDF4Mbit SPI MRAM做存储介质Microchip 的PIC18F85J50做主控把数据存储与读取的完整链路重新设计了一遍。从原理图到量产跑了快两年中间踩过不少坑今天把这套方案完整拆开聊聊——选型逻辑、硬件连接、初始化代码、读写时序以及工业现场才遇得到的可靠性问题给正在做嵌入式存储的同行一个参照。1. 工业存储选型复盘为什么不用Flash/EEPROM/FRAM偏偏选了MR25H40CDF1.1 工业现场的第一个问题不是容量而是“能写多少次”做工业设备的人容易犯一个直觉错误先比容量再比价格最后才看可靠性。但要命的恰恰是可靠性。普通消费级产品里配置参数掉一次、丢一次用户重启就好了最多骂两句。工业现场完全不同设备可能连续跑几个月不关机每个班次都在写产量、写报警记录、写温区设定值操作工交接班还喜欢直接关总闸。这种情况下存储介质面对的不是“偶尔写一次”而是高频写入加随机掉电的组合拳。传统 SPI NOR Flash 的问题很明显——页擦除 写放大。你只想改 2 个字节的温度设定值Flash 却要把整个扇区搬进缓存擦掉再整块写回。这个过程中任何一次断电轻则这个扇区数据错乱重则文件系统损坏。EEPROM 稍微好一点支持字节写但寿命也就百万次级日志类应用很容易在设备生命周期内写穿。所以这个项目从一开始就把“写入寿命”放在第一位。既然要高频写、随时写、写很多年不坏那常规 Flash 和 EEPROM 基本直接出局。1.2 MR25H40CDF 参数拆解和常见替代方案对比MR25H40CDF 是 Everspin 的 SPI MRAM4Mbit 也就是 512KB 容量3.3V 供电标准 SPI 接口。这类器件最吸引我的点不是速度也不是容量而是它根本不存在“擦除周期”。MRAM 的存储单元是基于磁隧道结MTJ的写数据就是翻转磁化方向不像 Flash 那样要先擦后写也不存在电子隧穿导致的绝缘层磨损。所以它的写入是“物理级无限次”这直接解决了我最头疼的寿命问题。我整理过一个对比表供选型时参考对比项SPI NOR FlashEEPROMFRAMMRAMMR25H40CDF典型容量1MB~64MB2KB~256KB32KB~512KB512KB写入方式扇区擦除 页编程字节/页写字节写字节写写前是否需要擦除必须内部自动擦不需要不需要写入寿命约10万次约100万次约1e10次无实际损耗限制字节级随机写不支持支持支持支持读破坏问题无无有读后需内部恢复无数据保持10年以上几十到上百年高温下保持期明显缩短官方标称可达20年以上是否需要后备电池否否否否成本低极低高更高FRAM 其实和 MRAM 很像也是非易失、字节写、速度也不错。但我当时调研发现FRAM 的写入寿命虽然比 Flash 高了好几个数量级仍不是真正的无限次更关键的是 FRAM 存在读破坏效应读操作本身会消耗存储单元的极化状态需要内部自动恢复这在高温环境下会让数据保持能力打折。MRAM 没有这类问题读写都不改变磁隧道结的稳定性更适合“不停读、不停写、还要存得久”的工业场景。提示MRAM 的价格确实比 Flash 贵但它贵在“省心”。一套设备如果因为存储介质寿命到了返厂换板那成本可远不止一颗芯片的差价。1.3 什么场景下适合用 MRAM什么场景别用经过这个项目我形成了自己的判断标准。适合用 MRAM 的场景很明确数据量不大512KB 以内够用、写入频繁、要求掉电保存、设备生命周期长。典型如工业仪表的标定参数、传感器校准表、设备运行日志、报警记录、断电前的现场状态快照。不适合用 MRAM 的场景也很多如果只是存一个几 KB 的出厂配置、一年写不了几次EEPROM 完全够用没必要上 MRAM如果需要存几十 MB 的波形数据、图片、固件升级包那还得靠大容量 Flash 或 SD 卡MRAM 的容量和成本都不占优。MRAM 在这个项目里扮演的是“核心数据保险箱”而不是“仓库”。2. PIC18F85J50 的 SPI 接口对接硬件连接与初始化代码2.1 最小硬件连接与引脚分配PIC18F85J50 是 Microchip 的 8 位 MCU80 脚封装自带 MSSP 外设可以做 SPI 主机。我的板子上用的就是它的 MSSP1 模块接 MR25H40CDF 正好。先看一下接线关系这是最容易被忽略却最基础的部分MR25H40CDF 引脚引脚功能连接目标说明CS#片选MCU 普通 GPIO比如 RE0低电平有效操作期间必须保持低SCKSPI 时钟MSSP1 的 SCK 脚由 MCU 提供时钟SI串行输入MSSP1 的 SDO1 脚主控发数据到 MRAMSO串行输出MSSP1 的 SDI1 脚MRAM 返回数据给主控WP#写保护10k 电阻上拉到 VCC低电平会禁止写操作不用时务必拉高HOLD#挂起10k 电阻上拉到 VCC低电平暂停通信不用时务必拉高VCC电源3.3V就近 0.1uF 去耦电容GND地GND尽量单点接地这里我想重点强调 WP# 和 HOLD#。很多人画原理图时图省事把这两个脚悬空。MR25H40CDF 的 HOLD# 悬空在工业环境的电磁干扰下可能被拉低一旦进入挂起状态MRAM 对 SCK 上的时钟视而不见表现为“芯片死掉了”。WP# 悬空同样危险可能会意外进入写保护状态导致写操作无效。这两个脚必须在硬件层上拉到 VCC别指望软件处理。另外CS# 不要追求“复用”到某些专用功能脚就用普通 GPIO 控制最好能独立操作。后面调试片选毛刺时这个灵活性帮了大忙。2.2 SPI 初始化代码模式 0 的寄存器配置MR25H40CDF 支持 SPI Mode 0 和 Mode 3。我习惯用 Mode 0也就是CPOL0、CPHA0时钟空闲时为低电平数据在第一个边沿采样。PIC18F85J50 的 MSSP 模块里SPI 模式的对应关系跟教科书写法不完全一样这里最容易翻车后面会详细说。初始化代码我当时是这样写的#define MRAM_CS_LOW() LATEbits.LATE0 0 #define MRAM_CS_HIGH() LATEbits.LATE0 1 void mram_spi_init(void) { // CS 引脚配置为普通输出默认高电平 TRISEbits.TRISE0 0; LATEbits.LATE0 1; // MSSP1 配置为 SPI 主机模式Fosc/64 // Mode 0 对应CKP0, CKE1, SMP0 SSP1STAT 0x40; // SMP0, CKE1 SSP1CON1 0x32; // SSPEN1, CKP0, SSPM0b0010Fosc/64 } // 写一个字节同时也是产生 SCK 时钟 void mram_spi_write_byte(uint8_t dat) { PIR1bits.SSP1IF 0; SSP1BUF dat; while (!PIR1bits.SSP1IF); // 等待发送完成 } // 读一个字节发一个空字节产生时钟 uint8_t mram_spi_read_byte(void) { PIR1bits.SSP1IF 0; SSP1BUF 0x00; while (!PIR1bits.SSP1IF); return SSP1BUF; }当时我 SPI 时钟配的是 Fosc/64算下来大概几 MHzMR25H40CDF 支持到几十 MHz余量非常足。工业 PCB 走线往往比较长还有排线、接插件保守的时钟频率反而省了很多信号完整性的麻烦。注意不同型号的 PIC 头文件里MSSP 寄存器名称可能叫 SSP1CON1 或 SSPCON1中断标志位也可能是 SSPIF 或 SSP1IF编译时以 XC8 头文件里的实际宏名为准。我上面的代码是在 MPLAB X XC8 环境下验证过的。2.3 硬件布局与抗干扰细节这个项目让我意识到MRAM 本身很皮实但外围电路粗糙照样会把它坑死。首先是去耦电容。VCC 脚必须就近放一个 0.1uF 陶瓷电容有条件再加一个 4.7uF 的钽电容。MRAM 在写入瞬间会有电流尖峰电源不稳时可能出现写错位。工业电源通常噪声大别省这几个电容。其次是电平一致性。MR25H40CDF 是 3.3V 器件PIC18F85J50 也是 3.3V 供电这个没问题。如果主控是 5V 的SPI 信号线必须加电平转换或分压否则长时间运行会损坏 MRAM 输入端。最后是布局。SCK 和 SI 走线不要平行太长避免串扰CS# 线上串一个 33 欧姆电阻能抑制振铃WP# 和 HOLD# 的上拉电阻必须接到 MRAM 的 VCC而不是系统的其他 3.3V 电源域。这些细节看着小但每一处后面都可能变成线上反馈的“偶发故障”。3. MRAM 的指令时序与状态寄存器操作读写链路的完整实现3.1 关键指令速查表MR25H40CDF 的指令集和普通 SPI NOR Flash 有点像但很多地方是 MRAM 特有的不能照搬 Flash 的习惯。我整理了一份常用指令指令字节码功能说明WREN0x06写使能每次写操作前必须发送WRDI0x04写禁止关闭写使能状态RDSR0x05读状态寄存器返回当前状态WRSR0x01写状态寄存器配置块保护位等READ0x03普通读地址后直接出数据FSTRD0x0B快速读地址后需要 1 字节 dummyWRITE0x02写数据地址后跟数据流SLEEP0xB9进入睡眠低功耗模式唤醒0xAB退出睡眠唤醒后需等待恢复时间先说一个容易踩的坑WREN 是独立事务。发送 0x06 时必须把 CS# 拉低、发完 WREN、再把 CS# 拉高。而后面的 WRITE 指令是另一次 CS# 低电平事务。不能偷懒把 WREN 和 WRITE 放在同一次 CS# 拉低时间内发完MRAM 不会认的。3.2 状态寄存器与写保护逻辑这一步不能省MR25H40CDF 有一个状态寄存器控制着写保护、块保护等行为。这个寄存器很容易被忽视但一旦配置错你会看到“写入时一切正常读出来却是旧数据”的诡异现象。我当时做的是最保守配置把 WP# 引脚硬件拉高软件里不主动写 WRSR保持默认的写保护状态。这样每次写数据前只发 WREN 让 MRAM 进入写使能状态写完自动退出写使能后面的误写操作即使时序错了也写不进去相当于一个硬件级的安全锁。如果要做完整的写保护管理逻辑大概是这样的读取状态寄存器RDSR 指令检查 WEL 位是否为 1确认上一事务是否处于写使能状态如果需要修改状态寄存器先发 WREN再发 WRSRWRSR 也是独立事务CS# 拉低、发指令、发数据、CS# 拉高。提示不要在一开始就把 WRSR 玩得很花。默认状态够用就先用默认状态等基本读写链路全部跑通再考虑加块保护和原子操作。3.3 完整的读写函数实现下面是我实际用的读写函数。地址是三字节的4Mbit 对应 512KB地址范围是0x00000到0x7FFFF。// 写数据到 MRAM void mram_write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; // 第一步WRENCS 必须完整拉低再拉高 MRAM_CS_LOW(); mram_spi_write_byte(0x06); // WREN MRAM_CS_HIGH(); // 第二步WRITE 事务 MRAM_CS_LOW(); mram_spi_write_byte(0x02); // WRITE mram_spi_write_byte((addr 16) 0xFF); mram_spi_write_byte((addr 8) 0xFF); mram_spi_write_byte(addr 0xFF); for (i 0; i len; i) { mram_spi_write_byte(buf[i]); } MRAM_CS_HIGH(); } // 从 MRAM 读数据 void mram_read(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_CS_LOW(); mram_spi_write_byte(0x03); // READ mram_spi_write_byte((addr 16) 0xFF); mram_spi_write_byte((addr 8) 0xFF); mram_spi_write_byte(addr 0xFF); for (i 0; i len; i) { buf[i] mram_spi_read_byte(); } MRAM_CS_HIGH(); } // 快速读用于大块数据搬运 void mram_fast_read(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_CS_LOW(); mram_spi_write_byte(0x0B); // FSTRD mram_spi_write_byte((addr 16) 0xFF); mram_spi_write_byte((addr 8) 0xFF); mram_spi_write_byte(addr 0xFF); mram_spi_write_byte(0x00); // 1 字节 dummy cycle必须发 for (i 0; i len; i) { buf[i] mram_spi_read_byte(); } MRAM_CS_HIGH(); }这里有个细节读操作不需要 WREN任何时候都可以直接发 READ 或 FSTRD。而写操作前如果没有发 WREN写指令会被 MRAM 直接丢弃这种情况 MCU 是感觉不到异常的只能在读回来时发现数据没变。另外FSTRD 快速读在地址后必须有一个dummy字节用来让 MRAM 内部定位地址。漏掉这个 dummy读回来的第一个字节就会错位整段数据全乱。这是我在调试中踩过的一个很典型的坑。3.4 关于地址范围与连续读写的边界MRAM 有一个好处是没有页边界限制。Flash 写数据时跨页会在页边界被截断或卷绕MRAM 不会你可以从任意地址连续写任意长度写到地址末尾会自然卷回但正常逻辑里没人会这么干。不过不建议因此就毫无规划地乱写。实际项目里我还是按“记录”为单位组织数据每条记录固定长度或带长度字段尽量避免跨越大块边界。这样即使以后要升级固件、增加记录类型老数据的兼容性也更好。还有一个值得提的点MR25H40CDF 的数据保持期和温度有关工业级批次的手册里通常会给出不同温度下的保持时间。我的经验是不要把整片 MRAM 都塞满给关键数据留出冗余写入区既方便 A/B 备份也减少频繁写同一块区域的极端情况。4. 工业现场可靠性设计校验、掉电保护与多备份机制4.1 每条记录必须带校验和状态标记MRAM 本身很可靠但可靠存储 ≠ 可靠系统。主控可能跑飞、电压可能掉一半、SPI 时序可能被中断打断这些都不是 MRAM 能兜底的。所以我在应用层给每条记录设计了固定格式偏移字段大小说明0magic1 字节固定 0x5A用于快速识别有效记录1type1 字节记录类型/功能号2len2 字节数据长度4seq2 字节记录序号用于区分新旧6dataN 字节实际数据6Ncrc2 字节对以上所有字段的 CRC16 校验8Nstate1 字节0xAA 表示写入中0x55 表示写入完成写入流程是先写除了 state 以外的所有字段此时 state 保持 0xAA等数据全部写完、CRC 也写好了最后单独写 state 为 0x55作为“提交标志”。读取时先看 state 和 magic再校验 CRC。这个设计的好处是如果写入过程中掉电记录里一定是 state0xAA 或 CRC 不对程序就能判断这条记录无效转而去读备份区。如果只是 CRC 校验万一读到写了一半的数据CRC 不匹配会知道数据坏了但配合 state 可以更快定位“是不是写到一半断电”这种状态。4.2 A/B 区双备份与启动判定逻辑双备份也叫 A/B 切换、交替写。原理很简单在 MRAM 里划分两个固定区域比如 A 区从0x00000开始B 区从0x00100开始每次写入交替选择 A 或 B。写过数据的区域带递增的 seq 序号。每次上电启动时读 A、B 两区的 magic、seq、CRC、state如果 A 区有效且 B 区无效用 A如果 B 区有效且 A 区无效用 B如果两区都有效比较 seq取序号大的那个如果两区都无效使用出厂默认值并重建记录。实际运行中如果某次写入只写到一半就掉电该区 state 会停在 0xAA下次启动直接跳过它使用上一次完整写入的另一个区。下一轮正常写入时再把坏区重新覆盖掉。这个逻辑不复杂但能覆盖绝大多数“写一半断电”的现场事故。我见过很多项目连单份记录都不加 CRC全靠 MRAM 的“非易失性”硬扛那是把可靠存储和可靠系统混为一谈了。4.3 掉电联锁与电压监测工业设备最恶劣的时刻不是正常运行而是异常断电那几百毫秒。电压掉到 MCU 最低工作电压以下之前MCU 可能还在跑指令如果这时恰好触发了 MRAM 写操作数据会处于什么状态全看运气。我的做法是加了一道“软件掉电联锁”在 MCU 的电源引脚部署一个大容量储能电容几十毫法级别让掉电后 3.3V 能维持几个毫秒用 MCU 内置的 BOR欠压复位或一个外置电压监测芯片检测电压低于设定阈值比如 3.0V一旦触发掉电标志主程序在剩余时间内只做一件事把 CS# 拉高禁止任何 SPI 写事务然后停止执行。这里有个反直觉的经验掉电时不要试图“赶紧把最后几个字节写进 MRAM”。MRAM 写入虽然没有擦除过程但电源不稳时即便是几个时钟周期也可能产生错误数据。与其赌这一刻的时序不如保证掉电前已经把数据写完整了掉电后什么都不做等下次上电用备份区恢复。4.4 可靠性验证我们实际做了什么测试方案定下来后我在板子上做了几轮针对性测试连续读写测试对同一个区域反复写入不同的随机数据并读回比对跑了 10000 次以上MRAM 没有出现过一次数据错位随机掉电测试程序正在写不同长度记录的时候人为随机关断电源累计约 500 次上电后通过 A/B 区状态判断全部能恢复到断电前的完整记录高温老化把板子放进 85℃ 温箱连续读写 72 小时并反复断电上电数据始终完好逻辑分析仪监控在调试口挂逻辑分析仪观察 CS#、SCK、SI、SO 的时序确认没有毛刺和异常片选。结果基本符合预期。MRAM 自身表现稳定倒是第一版硬件在掉电测试中暴露了 CS 毛刺问题这个放到下一章专门讲。5. 实际调试中的四个坑从片选时序到写保护异常5.1 坑一WP# 和 HOLD# 悬空导致芯片“假死”第一版样板画原理图时我为了少两个电阻把 WP# 和 HOLD# 直接悬空。结果产线调试时发现MRAM 大约每工作十几分钟就出现一次“无响应”写操作发下去读回来全是旧数据有时候连读都读不出来。排查了一圈最后用示波器点 HOLD# 脚才发现这个脚在电源纹波和环境噪声下自己跌到了低电平。HOLD# 一旦拉低MRAM 会忽略整个 SPI 输入相当于被“挂起”了必须重新拉高才能恢复。教训很直接WP# 和 HOLD# 必须由外部电阻上拉到 VCC不要悬空也不要只靠 MCU 内部上拉。10k 电阻甚至 4.7k 电阻都行关键是让这两个脚在常态下稳定在高电平。5.2 坑二SPI Mode 0 的寄存器配置写反这个坑特别隐蔽因为代码编译没错逻辑分析仪看波形也有模有样但读回来的数据就是不对。问题出在 PIC18F85J50 的 MSSP 模块对 SPI 模式的映射上。我一开始凭经验把SSP1STAT配成了CKE0结果实际对应的是 SPI Mode 1而不是 Mode 0。Mode 1 的采样沿正好差了一个相位MRAM 在第一个边沿还没把数据准备好主控就去采样读回来的数据自然全部错位。后来对照手册仔细看MSSP 模块里SPI Mode 0CPOL0、CPHA0对应 CKP0、CKE1、SMP0。把SSP1STAT改成0x40后同一个时序下数据立刻全部正确。提示不同 MCU 的 SPI 外设对 Mode 0 的寄存器配置各不相同。不要想当然一定要核对数据手册里“SPI Mode vs 寄存器位”的对应表最好再配合逻辑分析仪实测一遍。5.3 坑三FSTRD 快速读漏掉了 dummy 字节普通读 READ0x03用得很顺读写都正常。后来为了搬运大块数据我改用 FSTRD0x0B结果读回来的内容从第二个字节开始全部错位。我当时一度怀疑是 MRAM 坏了换了一片还是一样。最后翻到数据手册“Fast Read”时序图才发现 FSTRD 在发送 3 字节地址之后必须额外发送一个 dummy 字节MRAM 才能把内部地址锁存好之后才输出数据。补上一行mram_spi_write_byte(0x00)之后快速读完全正常。这个坑对我最大的触动是凡是手册上标了 dummy cycle 的指令一个字节都不能省这不是我可以自由发挥的地方。5.4 坑四CS 片选毛刺与掉电瞬间误写最后这个坑来自硬件测试。随机掉电测试做了几十轮之后有一次上电发现 A 区和 B 区的状态都变成了“写入中”两个区都废了。用逻辑分析仪深挖发现问题不在 MRAM而在 CS# 线上。CS# 走线较长在开关电源瞬间会产生振铃把 CS# 拉出了一串毛刺。MRAM 一旦检测到 CS# 下降沿会以为新一轮 SPI 事务开始了此时 SCK 上如果刚好有残余时钟就可能误触发写入。解决手段有三层CS# 线加上拉电阻10k到 VCC同时串联 33 欧姆电阻抑制振铃在 CS# 和地之间加一个 100pF 小电容滤掉高频毛刺软件层面所有 SPI 操作开始前加几条空指令延时让 CS# 电平稳定后再发数据。加上这些措施之后又跑了两轮随机掉电测试再没出现双区同时损坏的情况。这个经历让我深刻体会到一个道理MRAM 是可靠的但你给它的电气环境必须可靠。工业现场的干扰远比实验室复杂画板时多花半小时处理 CS#、WP#、HOLD# 的上拉和滤波后面能省下几周的售后排查时间。这套方案后来在设备里跑了差不多一年半没有再收到用户关于数据丢失的反馈。回过头来看选型时在存储器件上多花的几块钱远比后面上门改程序划算。嵌入式存储这块没有银弹把写入寿命、掉电行为、校验备份和电气环境这些边界都想清楚才敢放心把设备交到现场去。
返回列表