1. 项目缘起:工业设备需要一颗“不纠结”的存储芯片
做工业设备开发这么多年,最头疼的事之一就是数据存储。现场设备要存校准参数、运行日志、故障记录,还要求掉电不丢、频繁写入不坏、访问速度还不能拖后腿。你可能会说,这不是EEPROM和Flash的老本行吗?理论上是,但现实中一碰就碎。普通EEPROM容量小、写入慢,而且很多型号写寿命只有几十万次,日志型应用一年就能磨穿;NOR Flash倒是容量大,但页擦除、块管理、磨损均衡这一套逻辑下来,驱动代码轻松上千行,掉电瞬间还要担心状态不一致。至于外挂SRAM再加电池备份,体积、成本和安全隐患都不少。
后来在一个需要记录高频状态量的工业控制项目里,我换了一条路:用 Everspin 的 MR25H40CDF,一颗 4Mbit 的 SPI 接口 MRAM,配合 Microchip 的 PIC18F46K42 单片机来做存储和读取。MRAM 全称是 Magnetoresistive Random Access Memory,磁阻随机存取存储器,既有 SRAM 一样的高速读写,又有 Flash 一样的非易失特性,最关键一点是它的写入寿命基本可以认为是无限的,写入过程不需要先擦后写,也不用等待内部编程完成。这套方案在我手上已经稳定跑了多个批次产品,这篇就把选型思路、硬件连接、底层驱动和调试经验完整写出来,给同样在工业嵌入式场景里折腾存储的工程师做个参考。
如果你正在做物联网网关、工业控制器、仪表数据记录器,或者只是对“非易失存储还能怎么选”感兴趣,这篇文章应该能帮你省下不少弯路。内容会从“为什么选这两个芯片”一路讲到“驱动代码怎么写”“掉电数据怎么保护”,尽量让哪怕刚接触 PIC 的人也能跟着操作。
1.1 传统非易失存储方案在工业现场的痛点
先复盘一下我之前踩过的坑。某款设备需要每秒钟记录一次传感器温度和一个累计计数值,再加上一些设备状态位,数据量不大,但写入频率很高。一开始用的是一颗 64Kbit 的 SPI EEPROM,写一页数据大概需要 5ms 左右,而且 EEPROM 的写寿命按手册写的是 100 万次,听起来不少,但如果你每秒写一次,24 小时就是 86400 次,一根月不到就把寿命烧掉大半。为了规避这个问题,我只能在驱动里做磨损均衡,把存储区分成好几个扇区轮换着写,还得定期搬数据,代码复杂度上去了不说,掉电的时候如果恰好搬到一半,数据完整性就全看运气了。
后面的项目尝试过用 SPI NOR Flash,寿命确实上去了,单块至少十万次擦写,但新的麻烦又冒出来:Flash 写入之前必须擦除到全 1 状态,而擦除以扇区为单位,最小扇区通常 4KB。你只改十六个字节,也得先把整个扇区读到 RAM,修改后再整块擦除重写。这个“读-改-擦-写”流程在掉电时非常脆弱,一旦在擦除中途断电,这个扇区可能变成无效数据。更别提驱动里还得实现坏块管理、磨损均衡、掉电保护日志,一套流程下来工作量直接翻倍。对大量开发团队来说,这点存储功能反而成了最耗精力的部分。
还有一类方案是“SRAM + 电池备份”,或者叫 nvSRAM 的专用器件,它们的问题是超贵,而且体积大、需要额外的电池维护。在工业现场,电池寿命、温度范围、安规认证都很恼人。所以当我第一次看到 MRAM 的规格表时,几乎是一拍大腿:这玩意儿存取时间接近 SRAM,数据保持十年以上,读写寿命基本无限,不用擦除,不需要电池,温度范围宽,而且价格虽然比 EEPROM 高,但整体方案比“SRAM+电池”便宜太多。
1.2 MR25H40CDF 到底是什么东西
MR25H40CDF 是 Everspin 推出的一款串行 SPI 接口 MRAM,容量 4Mbit,也就是 512KB 字节。它内部存储单元的原理和传统半导体存储器完全不同,用的是磁隧道结(MTJ)结构,靠磁化方向来代表“0”和“1”,而不是靠电荷。这带来两个直觉就能感受到的优势:第一,没有电荷泄漏问题,断电后数据状态天然保持;第二,写入过程不是往氧化层里注入电子,而是翻转磁化方向,所以不存在“写坏”这回事。
接口方面,它和常见的 SPI EEPROM/Flash 几乎一模一样,有 CS、SCK、SI、SO 这些脚,指令也兼容标准 SPI 存储类操作码,比如 0x03 读、0x02 写、0x06 写使能、0x05 读状态寄存器等。这就意味着你原来为 SPI EEPROM 写的驱动框架,大部分可以复用,只是内部实现要按 MRAM 的特性做一些简化。MR25H40CDF 支持 SPI Mode 0 和 Mode 3,时钟极性相位可以按控制器习惯选。和 Flash 不同,它内部没有“页缓冲”这个概念,标准操作指令可以在一次片选拉低期间连续读写任意长度的字节,地址自动递增,不需要考虑跨页问题,这在后面写驱动的时候会体现得淋漓尽致。
具体到应用层面,我主要用它存三类数据:一是设备参数和校准系数,二是高频写入的运行日志,三是掉电前需要快速保存的状态快照。由于它的写入延迟可以忽略不计,在掉电瞬间配合单片机的掉电中断,完全来得及把关键数据刷进去。
1.3 为什么搭配 PIC18F46K42
存储芯片定了,主控也好选。这次用的是 Microchip 的 PIC18F46K42,一颗 40 引脚的中端 8 位单片机。选它的原因有三个。
第一是它有硬件 SPI 外设,而且集成度比老款 PIC 高很多。PIC18F46K42 上有 MSSP 模块,配置成 SPI 主模式后,发送接收都靠硬件移位寄存器完成,配合中断可以做到不占 CPU。对 MRAM 这种高速设备来说,硬件 SPI 比 IO 口模拟要可靠得多。第二是这颗片子带有丰富的模拟外设和多个定时器,我系统里的 ADC 采集、PWM 输出、通信口都靠它一并解决了,不需要额外再挂主控。第三是它的外设引脚可以选择(Peripheral Pin Select,PPS),SPI 的时钟和数据线不一定锁死在固定引脚上,画 PCB 的时候方便走线,这是很多老型号 PIC 不具备的灵活度。
PIC18F46K42 的工作电压范围比较宽,我直接让它在 3.3V 下运行,和 MR25H40CDF 的电平完全兼容,省去了电平转换电路。内部振荡器频率也可以配置到 64MHz,即使 SPI 分频后也足够把 MRAM 的吞吐跑起来。实际项目中还同时启用了它内部的 EEPROM,用来存一些需要掉电保存但更新频率很低的小参数,MRAM 则专注于大块、高频的数据,两者互补。
2. 硬件设计与连接,少走弯路的关键
芯片选型定了之后,硬件设计就是最容易出问题也最容易返工的环节。MRAM 的接口虽然简单,但工业现场的电源环境、外部干扰、引脚电平这几个问题处理不好,后面软件怎么调都白搭。这一节详细讲讲我这块板的接法,以及我在原理图和 PCB 阶段做的几个关键决定。
2.1 MR25H40CDF 引脚与工作原理快速梳理
先看一眼 MR25H40CDF 的引脚功能,8 脚封装,布局非常标准。除了电源和地,主要就是 SPI 四线:CS 片选、SCK 时钟、SI 数据输入(主控发给存储芯片)、SO 数据输出(存储芯片返回给主控)。剩下两个引脚经常被人忽略:WP(写保护)和 HOLD(保持)。WP 引脚拉低时,非易失状态寄存器的写操作会被禁止,但普通数据写不受影响;HOLD 引脚拉低时,芯片会暂停当前通信,SCK 上的时钟信号被忽略,这在多主机共享 SPI 总线的场景下有用。
我的建议是:如果你不需要这两个功能,千万不要让它们悬空。WP 直接接到高电平,HOLD 也直接接到高电平,或者通过 10kΩ 电阻上拉。悬空引脚在工业现场就是天线,一旦耦合进来一个毛刺,可能触发意外的保持状态,表现为主控明明在发时钟,芯片却不响应,非常难查。我在最早打样时 HOLD 悬空,一到电机启动瞬间通信就丢数据,查了两天才发现是这个问题。
MRAM 内部读写的本质是改变磁隧道结的磁化方向。读操作是纯非破坏性读出,不改变存储内容,也不需要像 EEPROM 那样先置成全 1。写操作则是直接把目标位改写为指定值,没有“擦除整个扇区”的前置动作。所以从驱动模型上看,它可以被理解为一颗“掉电不丢数据的 SRAM”。这也是为什么它特别适合频繁写入的日志场景。
2.2 与 PIC18F46K42 的连接方案
下面是我实际使用的连接方式,假设 PIC18F46K42 用 3.3V 供电,MR25H40CDF 也用同一个 3.3V 电源轨:
| MR25H40CDF 引脚 | 功能 | 接 PIC18F46K42 | 说明 |
|---|---|---|---|
| CS | 片选 | 任意 GPIO,我用的 RC5 | 必须由软件控制,拉低开始一次传输 |
| SCK | 时钟 | RC3(SCK1,由 PPS 配置) | 由主控产生时钟,MRAM 是从机 |
| SI | 主出从入 | RC4(SDO1) | 命令、地址、写入数据都从这条线进 |
| SO | 主入从出 | RC2(SDI1) | 读取数据从这条线返回 |
| WP | 写保护 | 接 VCC | 保持高电平,允许正常写操作 |
| HOLD | 保持 | 接 VCC | 保持高电平,禁用保持功能 |
| VCC | 电源 | 3.3V,并联 0.1μF 去耦电容 | 电容尽量贴近芯片电源脚 |
| GND | 地 | 系统地 | 与主控共地 |
这里要注意 PIC18F46K42 的 PPS 配置。K42 系列的外设引脚是软件可映射的,SPI 的时钟、数据引脚必须在初始化阶段写成对应的寄存器值,而不是纯硬件固定。MCC(Microchip Code Configurator)工具里可以直接拖拽选择,手写代码时则要操作 PPSLOCK、SPI1 相关的输入输出选择寄存器。务必要先解除 PPS 锁定,再配置引脚,最后重新锁定,否则配置不生效。
SCK 线上我串了一个 22Ω 的电阻,位置尽量靠近主控一侧。这不是必须的,但在工业现场,SPI 时钟线如果走线较长,阻抗不连续会引起振铃,串一个小电阻能明显改善波形边沿的过冲。CS 线我也特意拉得比较短,没有走太远的绕线。对于 SPI 速率在 8MHz 左右的应用,这样的处理已经足够稳定。
2.3 电源与去耦,工业现场的“隐形杀手”
MRAM 的电源要求不算苛刻,但工业现场经常出现大电流负载导致的电源毛刺。我的板子上,MCU、编码器、继电器都在同一块板上,继电器动作瞬间电流跳变很大。第一次调试时,我发现 MRAM 偶发数据错误,用示波器一抓,电源线上有几十毫伏的毛刺,虽然 MCU 没复位,但 MRAM 的 SPI 接口对输入电平的阈值会受到电源噪声影响,于是把数据采错了。
解决措施有三件:第一,MR25H40CDF 的 VCC 脚旁边放 0.1μF 陶瓷电容,同时再放一个 4.7μF 的钽电容进行低频滤波;第二,SPI 信号线不要和继电器驱动线、电机驱动线平行走线,实在避不开就在中间加地线隔离;第三,电源进板处加一个磁珠和较大的电解电容,把高频噪声挡在外面。做完这三步,数据错误现象基本消失。
另外一个容易忽略的点是地平面。如果板子有两层以上,给 MRAM 和 SPI 信号线提供一个完整的地平面会大幅提升可靠性;如果只有双层板,至少保证芯片底部的地过孔和主控地之间尽量短。MRAM 本身不是干扰源,而是敏感器件,地回路越干净,它越稳定。
3. 驱动开发:从底层 SPI 到实用读写函数
硬件调试通过之后,最核心的部分就是软件驱动了。和 Flash 驱动相比,MRAM 的驱动简直可以用“清爽”来形容,但越简单的东西越容易在细节上翻车。这一节我会从初始化开始,把读、写、状态检查几块代码完整走一遍,并解释每个关键步骤的意图。
3.1 初始化 SPI 与工作模式选择
PIC18F46K42 的 SPI 初始化,我强烈建议直接用 Microchip 的 MCC 生成基础代码,再去手工裁剪。MCC 能帮你避免初始化寄存器时漏位的问题,特别是 PPS 映射这部分,手写非常容易错。在 MCC 里新建项目后,选择 SPI1 外设,配置为主模式,时钟分频选择分频系数以保证波特率合适,模块极性根据 MRAM 的时序要求来选。
MR25H40CDF 支持 SPI Mode 0 和 Mode 3,区别在于时钟极性(CPOL)和采样相位(CPHA)的组合。Mode 0 是时钟空闲为低、数据在上升沿采样;Mode 3 是时钟空闲为高、数据在上升沿采样。我用的是 Mode 0,这也和大多数 SPI EEPROM 的习惯一致。关键是主控和从机的模式必须完全一致,PIC 侧的设置项是 Clock Polarity 和 Clock Edge 两个参数,MCC 里分别对应 Idle Clock 为 Low、Data Transmitted on Rising Edge 等选项。
波特率方面,MRAM 手册上给出的 SCK 上限其实挺高,但我实际配置到 8MHz。一个原因是 PIC 内部振荡器经过分频后产生的频率比较准,再高的话波形边沿变缓,长线传输容易出错;另一个原因是,工业现场电磁环境复杂,留有裕量比追求极限吞吐更重要。对大多数应用,8MHz 下读一个 512KB 的 MRAM 全片数据也用不了 0.1 秒,完全够用。
初始化完成后,可以先做一个自检:读取状态寄存器,如果读到 0x00 或 0x02 之类的固定值,再回读全片前几个字节,用来确认 SPI 基本通路正常。不要上来就写数据,容易把已有内容覆盖掉。
3.2 指令集使用,别把 MRAM 当 Flash 操作
MR25H40CDF 的指令和普通 SPI EEPROM 很接近,但有几个关键差异必须消化。第一,任何写操作(包括写状态寄存器)之前,必须先发 WREN(0x06)指令把写使能锁存器置位。WREN 指令的时序是:CS 拉低,发送一个字节 0x06,CS 拉高。注意 CS 必须真正拉高一个周期以上,写使能才会生效。如果你在一个 CS 低电平周期内连着发 WREN 和 WRITE 两段内容,芯片不会认这笔写操作,这是我见过最多的低级错误。
第二,MRAM 的数据写指令比 Flash 简单得多。发送 0x02 写指令后,紧跟三字节地址,然后是任意长度的数据字节。地址自动递增,没有页边界限制,不需要在跨页处拆分。你可以一次性写入几百字节甚至覆盖整个地址空间,只要 CS 一直保持低电平。第三,写入动作本身不需要等待。Flash 写完之后,你要轮询状态寄存器里的忙标志,直到内部编程完成才能继续。MRAM 的写入是即时完成的,操作指令的最后一个数据位发送完毕,数据就真正的落进去了,不需要再加额外的延时。
读操作同样简单:CS 拉低,发送 0x03 读指令,紧跟三字节地址,然后从 SO 线上按时钟读取数据。地址一样自动递增,可以连续读下去。如果你想从头到尾读全片,发完起始地址后直接不断给时钟就能读完,中间不需要重新拉高 CS。
3.3 完整驱动代码与关键点注释
下面是我实际在 PIC18F46K42 上使用的底层驱动代码,去掉了业务逻辑,只保留最核心的读写能力。这里假设 MCC 已经生成 SPI1 驱动,并且给出了SPI1_ExchangeByte函数。
#include "mcc_generated_files/mcc.h" #define MRAM_WREN 0x06 #define MRAM_WRDI 0x04 #define MRAM_RDSR 0x05 #define MRAM_READ 0x03 #define MRAM_WRITE 0x02 // 根据实际接线修改 #define MRAM_CS_SetHigh() LATCbits.LATC5 = 1 #define MRAM_CS_SetLow() LATCbits.LATC5 = 0 // 发送一字节并读取一字节(全双工) static uint8_t MRAM_SpiByte(uint8_t byte) { return SPI1_ExchangeByte(byte); } // 写使能,所有写操作之前必须调用 static void MRAM_WriteEnable(void) { MRAM_CS_SetLow(); MRAM_SpiByte(MRAM_WREN); MRAM_CS_SetHigh(); } // 读状态寄存器 uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_SetLow(); MRAM_SpiByte(MRAM_RDSR); status = MRAM_SpiByte(0x00); MRAM_CS_SetHigh(); return status; } // 从指定地址读取 len 字节到目标缓冲区 void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_SetLow(); MRAM_SpiByte(MRAM_READ); // 三字节地址,高字节在前,MRAM 实际只使用低 19 位 MRAM_SpiByte((uint8_t)((addr >> 16) & 0xFF)); MRAM_SpiByte((uint8_t)((addr >> 8) & 0xFF)); MRAM_SpiByte((uint8_t)(addr & 0xFF)); while (len--) { *buf++ = MRAM_SpiByte(0x00); } MRAM_CS_SetHigh(); } // 从指定地址写入 len 字节 void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_SetLow(); MRAM_SpiByte(MRAM_WRITE); MRAM_SpiByte((uint8_t)((addr >> 16) & 0xFF)); MRAM_SpiByte((uint8_t)((addr >> 8) & 0xFF)); MRAM_SpiByte((uint8_t)(addr & 0xFF)); while (len--) { MRAM_SpiByte(*buf++); } MRAM_CS_SetHigh(); }这段代码里有几个细节值得展开讲讲。
三字节地址部分,MR25H40CDF 是 4Mbit,即 512KB,有效地址范围是 0x00000 到 0x7FFFF,换算成二进制就是 19 位。发送地址时寄存器还是按 24 位格式走,高字节的 bit7~bit5 实际上会被忽略。我在驱动里保留了高字节发送,目的是让代码和标准 SPI 存储类指令保持一致,如果以后换更大容量的 MRAM,这段代码不用改。
读操作里每发一个字节都要同步收到一个字节,所以在等待数据的 clock 周期里,我发送的是 0x00 填充字节,这是 SPI 全双工机制决定的:主机不给时钟,从机就无法移出数据。如果你的驱动库函数是“先发送后接收”的分离式接口,那要注意在接收函数内部也要产生正确的 SCK 脉冲。
写操作前面的MRAM_WriteEnable()是必须的。我见过有人优化代码觉得每次写都发 WREN 太啰嗦,想要在初始化时只发一次,结果发现后续写操作时灵时不灵。其原因就是芯片内部 WEL 位会在每次成功的写操作之后自动清零,所以每一笔写事务前都要重新执行 WREN,这个步骤不能省。
3.4 为什么 MRAM 的驱动可以这么“简单”
很多从 Flash 项目转过来的同事第一次看到这份代码都会问:写数据之前为什么不需要擦除?写完之后为什么不需要等待?跨页为什么不处理?这三个问题其实指向同一个事实:MRAM 的存储机理和 Flash 完全不同。
Flash 写入的前提是把存储单元置为“1”,这个操作就是擦除。擦除只能按块进行,最小单位是扇区,几 KB 起步。所以你写一个字节,也得在后台完成一次扇区擦除,这是一个相对漫长而且高能耗的过程。MRAM 不同,它的每个存储单元可以当作一个微型磁体,写“0”和写“1”都是直接翻转磁化方向,不需要事前归位,自然也就没有“先擦后写”这个步骤。
因为没有擦除动作,MRAM 也就没有内部状态机在写完后持续运行,不需要 WIP 位,不需要轮询等待。数据从移位寄存器进入存储阵列的那一瞬间就生效。这也是为什么它在掉电保存场景里特别好用:你不需要预留“等待内部编程完成”的时间,CS 一拉高数据就安全了。
跨页问题在 Flash 驱动里也很头疼,因为 Flash 内部分页,写满一页后地址计数器会跳回当前页开头,如果你继续写下去,会覆盖本页前面的数据,所以驱动必须判断剩余字节数,拆成多笔事务。MRAM 没有内部页缓冲,地址计数器就是纯线性递增,从 0x00000 一直滚到 0x7FFFF,满地址之后回卷到 0x00000。如果你的写入长度恰好跨过顶端而你没察觉,回卷后就会覆盖低地址数据。我的经验是:应用层一定要约定好最大写入长度,并在驱动里增加越界判断,回卷行为宁可报错也不要静默覆盖。
4. 系统级应用:把 MRAM 真正用进产品里
驱动层稳定之后,真正决定项目成败的是怎么规划存储布局、怎么处理掉电保存、怎么让读写策略适配业务。MRAM 的无限寿命给了我们很大自由,但自由不等于随便用,下面讲讲我在这套系统里的实际设计方案。
4.1 地址空间规划与数据布局
4Mbit 的 MRAM 有 512KB 空间,比起单片机内部的 EEPROM 大得多,但也不能乱用。我把它从高到低划分成几个区域,每个区域只负责一类数据:
| 区域 | 地址范围 | 大小 | 存储内容 |
|---|---|---|---|
| 参数区 | 0x00000 ~ 0x0FFFF | 64KB | 设备参数、校准系数、配方数据 |
| 运行日志区 | 0x10000 ~ 0x3FFFF | 192KB | 环形日志,反复写入 |
| 状态快照区 | 0x40000 ~ 0x5FFFF | 128KB | 掉电瞬间保存的现场状态 |
| 固件升级暂存区 | 0x60000 ~ 0x7FFFF | 128KB | 用于接收新固件或新配置 |
参数区更新频率很低,可能一天就写几次,64KB 空间足够放几百组参数。每组参数我都存一份校验值,读取时先校验,校验失败就用默认值,同时记录一个错误标记,方便现场判断设备有没有存储异常。很多人只存数据不存校验,一旦 MRAM 受干扰读回错误数据,设备会拿错误参数运行,这是很危险的事。
运行日志区由我设计成一个环形缓冲,只在末尾追加写入。正常运行时每秒钟产生一条日志,每条日志固定 32 字节,这样无论写多久都不会把 MRAM 写坏。状态快照区则是在检测到掉电时写入,包括当前计数值、最近一次 ADC 采样结果、电机状态等。固件升级暂存区是我后来加的,由于 MRAM 足够大,我可以先把上位机发来的固件分包缓存进去,校验完毕后一次性搬到单片机 Flash,这样升级过程断线也不怕。
4.2 掉电保存:如何在失去电源的瞬间完成写操作
工业设备最怕掉电,特别是那些正在执行关键操作时突然断点的情况。PIC18F46K42 自带 BOR(Brown-out Reset)功能,能检测电源跌落,但这还不够,我做了更细的掉电处理流程。
系统供电经过一个二极管后给单片机供电,二极管前放一个大电容。正常供电时,二极管导通,电容两端电压等于电源电压;掉电瞬间,电源电压下降,但电容会继续给单片机供电一段时间,这个时间通常有几十毫秒。我把分压电阻接到电源检测引脚,用 ADC 或者数字输入持续监测。当检测到电压跌到阈值以下时,单片机立即进入掉电中断,把需要保存的数据一次性写入 MRAM。
MRAM 在这个场景里的优势体现得特别明显。以前用 EEPROM 时,掉电能保存的数据量受限于“一次写页大小”,通常只能写 32 字节或 64 字节,而且写完还要等 5ms。现在用 MRAM,我可以在中断服务程序里直接连续写入 512 字节的现场数据,CS 拉高后数据立即安全,不需要额外等待。实测从进入中断到写完 512 字节,在 8MHz SPI 下用时不到 1ms,剩下的大把电容能量还可以用来把设备关机前的最后状态通过通信口发出去。
有个细节要注意:掉电中断服务程序里要避免调用可能阻塞的函数,比如 UART 等待发送完成。我就是先关中断、快速写 MRAM,再开中断做善后。另外,MRAM 的 CS 控制引脚必须用普通 IO,不要用它给你不出来的外设复用功能,否则掉电中断里配置 PPS 会多花时间。
4.3 高频写入与环形日志设计
运行日志区是我最喜欢的应用,因为它最能体现 MRAM “随便写”的爽感。用一个结构体指向日志区的写指针,每次写入 32 字节后就地更新指针。因为 MRAM 掉电不丢数据,指针本身也可以存在 MRAM 里,省去了额外的索引恢复逻辑。
实现环形日志时,最关键的是写指针和读指针的边界管理。我定义了一个日志头结构体,包括魔数、写偏移、写序号和日志长度。初始化时,先扫描整个日志区,找到一个有效的日志头。正常情况下日志头和写指针连续存放,掉电时最后一条记录可能因为写入一半而不完整,但这没关系,我用魔数校验来判断最后一条记录是否有效,无效就回退到上一条。每次新写日志前,先更新日志头里的写偏移,再写入日志体,最后再回写一次日志头标识该条记录完成。这里多写一次日志头有些浪费,但换来了完美的掉电一致性。
放在 MRAM 上,环形日志的寿命无限,省去了 Flash 场景下的磨损均衡算法。以前用 Flash 做日志时,一个扇区写满就要擦除,擦除次数多了坏块管理逻辑越来越复杂;现在以 MRAM 做环形缓冲,代码就是纯粹的指针移动和写入,几乎没有边界例外。这个简化直接让固件维护成本下降了一个等级。
4.4 实测性能与功耗观察
我用逻辑分析仪抓过一轮读写波形。读操作:发 4 字节命令头(读指令 + 3 字节地址),然后连续接收 32 字节数据,整段传输加上 CS 的拉低和拉高时间,大概耗时 36 个时钟周期加若干开销,在 8MHz 时钟下约 4.5μs。写操作略微不同,因为写之前要先发 WREN,实际每笔小数据写事务会多一个字节的命令传输,但依然在微秒级完成。
相比之下,同项目原来用的 EEPROM 写单页需要 5ms,相差上千倍。这个差距在掉电保存和高频日志场景下是决定性的。
功耗方面,MRAM 正常工作时的动态电流比 EEPROM 写的时候小很多,数据手册给的待机电流很低。我的系统是常供电设备,不关心深度睡眠功耗。如果是电池供电的场景,可以关注 MRAM 的睡眠模式指令,在长时间不访问时让芯片进入低功耗状态。但用睡眠模式要注意,唤醒需要一定的恢复时间,驱动里要留足间隔再发起第一次访问,否则第一个字节可能读不出来。
5. 调试实录与常见问题排查
任何存储类器件在项目里第一次跑通,总不可能一帆风顺。这个项目我前后也踩了几个坑,有些问题查起来特别费时间。我把典型问题、排查思路和最终解决办法整理成下面的速查表,也给同样在调 MRAM 的朋友一条排查路径。
5.1 SPI 波形正常但数据全是 0xFF 或 0x00
这个现象我最初遇到时百思不得其解,示波器抓 SCK、SI、SO,波形看着都对,但读出来的数据不是全 1 就是全 0。最后发现是 SPI 模式不匹配。MR25H40CDF 支持 Mode 0 和 Mode 3,但如果你配置成 Mode 1 或 Mode 2,数据采样点会落在错误的边沿上,主控发出去的命令字节,芯片根本解释不了。这有点像两个人握手,一个人要在上升沿递东西,另一个却等下降沿才接,东西永远进不了口袋。
排查方法是先用逻辑分析仪同时抓 CS、SCK、SI、SO 四根线,重点看 SCK 空闲电平和数据变化时刻。如果 SCK 空闲为高,说明硬件上跑的可能是 Mode 3,而你在软件里配了 Mode 0,自然不对。根据实际波形调整 MCC 里的极性和相位设置,一次就能解决。
还有一个小概率可能:地址超出了容量范围。如果你发送的地址高于 0x7FFFF,芯片不会报错,但会按照内部线路译码到一个未定义或回卷的位置,读取内容没有意义。检查一下传进来的 addr 参数是不是 uint32 类型,千万别在赋值时把高字节截断了。
5.2 CS 引脚控制不到位,导致写操作静默失败
MRAM 的写事务被芯片判定为“合法”,需要 CS 从低到高完成一个完整的片选周期。如果你用 GPIO 拉低 CS 后,发送完数据立即把 CS 拉高,这个操作本身是正确的。问题往往出在中断里:如果在 CS 低电平期间的 SPI 发送过程中,被一个高优先级中断打断,中断服务程序又操作了同一个 SPI 外设,那么 MRAM 收到的字节缝隙里会混入额外时钟,整笔事务就被破坏了。
我的解决方案是:在写 MRAM 时临时关闭相关中断,或者把 SPI 发送过程做成原子操作。更稳妥的做法是,尽量让中断服务程序不要调用 MRAM 写函数,只设置一个标志,主循环里再执行写入。掉电保存那种必须在中断里写入的例外情况,也要确保中断里没有其他 SPI 操作与之竞争。
5.3 WP 和 HOLD 引脚引起的诡异故障
前面硬件部分已经提过 WP 和 HOLD 不能悬空,这里补充一个具体案例。第一次调试时我用的是样板,WP 和 HOLD 确实接了上拉电阻,但因为布局问题,HOLD 走线经过了继电器驱动线附近。继电器吸合瞬间,HOLD 线上感应出负向毛刺,把芯片打入了保持模式。表现为主控正常发时钟,SO 线却像被冻结一样,数据完全不变。
解决方法是把 HOLD 引脚直接从原来的走线里摘出来,就近接 10kΩ 电阻到 VCC,而且电阻到引脚之间的走线尽量短,不给噪声留下拾取空间。WP 也一样。如果你确实想用 HOLD 功能做多主机总线隔离,那就要对这条信号线做滤波处理,增加 RC 滤波,或者选带内部上拉的批次。
5.4 5V/3.3V 混合供电时的电平转换问题
我的方案里 PIC18F46K42 和 MR25H40CDF 都用 3.3V,不存在电平转换问题。但如果你参考的是 5V 供电的 PIC 经典型号,那么 SPI 总线上的高电平是 5V,MRAM 的工作电压上限如果按 3.6V 算,接收 5V 信号就会超压,长期工作可能损坏。反向方向,MRAM 的 SO 输出高电平只有 3.3V,对 5V 主控来说又可能达不到高电平阈值,读数据也会出错。
稳妥的转换方案有两个:一是加一颗双向电平转换芯片,比如 TXS0102 之类;二是用三态缓冲器把 5V 主控的输出降成 3.3V,再把 MRAM 的输出通过开漏加上拉接到 5V。无论哪种方案,都别忘了 HOLD、WP 这些控制引脚也要在同一个电平域下工作。最简单粗暴的办法还是像我一样,把整个板子统一到 3.3V 供电,一步到位。
5.5 写入后回读不一致,优先怀疑电源和地
如果你的驱动逻辑没问题,SPI 时序也对,但写进去的数据偶尔读出来不对,那多半不是芯片坏,而是电源和地不够干净。MRAM 写入瞬间电流比读操作略大,如果电源去耦不足,内部电压跌落可能导致写入数据出错。我建议在排查列表里加上:用手触摸芯片温度是否异常高、供电脚波形是否有纹波、地平面是否连续。
还有一种比较隐蔽的情况是地址线映射错误。MR25H40CDF 是 512KB 容量,19 位地址有效,但你在发送三字节地址时如果最高字节不小心发了非零值,比如传了一个 0x800000 之后的地址,芯片内部可能回卷覆盖到低地址的数据。这个很难从波形上看出来,因为 SCK 上该有的脉冲都有,芯片只是把高地址位忽略了。在驱动里加一个范围检查,addr 超过 0x7FFFF 就直接返回错误,能避免很多奇怪的问题。
6. 扩展方向与一点个人体会
系统稳定运行一段时间后,我开始琢磨这套存储方案还能怎么扩展。MRAM 本质上是个跑在 SPI 上的非易失 RAM,只要你把它当作“掉电不丢数据的内存”来用,想象力就能打开。比如,我可以把单片机 RAM 里的大缓冲数组定义到 MRAM 里,借助 SPI DMA 批量操作,做成一个大容量的数据暂存区;也可以在里面放一个轻量文件系统,配合 FAT 或 LittleFS 变体,让设备通过 USB 读取内部记录文件。
工业场景里,MRAM 与 PIC18F46K42 的组合还可以做得很花哨。K42 系列有多个串口和 SPI 模块,你可以同时挂一颗 MRAM 做数据记录,再挂一颗 Flash 做固件存储,分工明确。MRAM 的耐写特性也让它很适合做缓存层,把从传感器采集来的大量数据先暂存,再通过现场的弱网环境慢慢上传,断点续传的时候数据不会丢。
单独提一个让我印象深刻的点:以前做产品,存储模块往往是最后才考虑的部分,代码能跑就行。但存储策略其实严重影响设备在现场的口碑和售后成本。掉电丢参数、日志磨穿、升级断电变砖——这些故障都直接指向存储方案选型。MRAM 虽然单价高于普通 EEPROM,但它省掉的驱动开发时间、现场维护成本和故障率,折算下来完全是划算的。我现在的项目里,只要涉及频繁写、快速存、掉电保,第一反应就是查 MRAM 的选型表,而不是试图用 Flash 硬扛。
最后分享一个小技巧。如果你和我一样使用 MCC 生成初始化代码,记得在生成后把 SPI 引脚模式检查一遍。MCC 有时会根据芯片配置生成一些默认引脚方向,和实际硬件接线可能不一致。尤其是 PPS 配置,重新编译前一定要在生成的 pin_manager.c 里确认 SCK、SDO、SDI 三条线映射到了正确的物理引脚。这个检查花不了两分钟,却能帮你省掉一整天的查线时间。
MR25H40CDF 与 PIC18F46K42 这套组合,硬件设计不难,驱动实现也简洁,但它真正解决的是工业现场长期运行中的存储可靠性问题。希望这篇文章能帮你在类似项目中少走弯路,把存储这块做得既快又稳。