1. 项目缘起与方案选型:为什么是 MRAM 加 8 位 MCU 这套组合
1.1 一个真实场景引出的存储焦虑
前两年接手过一个工业数据采集终端的改造项目,设备装在配电柜里,环境温度夏天能到 70 度,冬天户外柜能到零下 30 度。原来的方案用的是 PIC18F97J60 加一颗 SPI 接口的 EEPROM,跑了两三年陆续出现数据丢失的问题。拆回来分析,一部分是 EEPROM 的擦写寿命到了——采集频率高的时候,一天写几百次,EEPROM 标称 100 万次擦写,算下来两三年就逼近极限;另一部分是掉电瞬间正在写数据,写了一半断电,整页数据就废了。
这个痛点其实很典型:工业现场要的是"写不死、掉电不丢、断电即存"。EEPROM 和 NOR Flash 都有擦写寿命和写入延迟的问题,铁电存储器 FRAM 容量又偏小、价格偏高。后来接触到 Everspin 的 MR25H40CDF 这颗 MRAM(磁性随机存储器),才算找到了比较理想的答案。
MR25H40CDF 是一颗 4Mbit(512KB)的 SPI 接口 MRAM,工作电压 2.7V 到 3.6V,SPI 时钟最高能跑到 40MHz,最关键的是它的写入次数几乎是无限的(官方标称 10^14 次以上),而且写入没有延迟,字节写和页写都是即时完成,不需要像 Flash 那样先擦后写、等待内部编程周期。这一点对工业场景太重要了——你可以在检测到掉电中断的第一时间,把关键数据直接写进去,不用等。
而 PIC18F97J60 这颗老将,虽然现在看起来主频只有 40MHz、RAM 不到 4KB,但它内置了 10/100M 以太网控制器,工业设备联网需求它一颗芯片就搞定了,加上 SPI 外设成熟稳定,和 MR25H40CDF 搭配是"老黄牛配新鞍"的组合。这套方案我前后在三个项目里用过,下面把踩过的坑和实操细节完整梳理一遍。
1.2 为什么不用 Flash、不用 EEPROM、不用 FRAM
选型这件事,光看参数表是不够的,得把几种非易失存储放在真实工况下对比。我整理了一张表,是当时做方案评审时用的:
| 存储类型 | 擦写寿命 | 写入延迟 | 掉电数据保持 | 容量成本 | 工业适配度 |
|---|---|---|---|---|---|
| EEPROM | 约 100 万次 | 毫秒级 | 好 | 中等 | 一般,寿命是硬伤 |
| NOR Flash | 约 10 万次 | 毫秒级,需先擦 | 好 | 低 | 差,频繁写场景不适用 |
| FRAM | 10^12 次以上 | 无延迟 | 好 | 高 | 好,但容量小价格贵 |
| MRAM(MR25H40CDF) | 10^14 次以上 | 无延迟 | 好 | 中等偏高 | 优,综合最平衡 |
从表里能看出来,MRAM 的核心优势就是寿命和延迟这两项。Flash 的问题在于"先擦后写",一个扇区擦除动辄几毫秒到几十毫秒,这期间如果掉电,数据就处于不确定状态。MRAM 是直接覆盖写,写一个字节就是写一个字节,没有中间态。FRAM 其实也能做到无延迟,但同容量下价格通常是 MRAM 的两三倍,而且大容量 FRAM 供货不稳定。
提示:选 MRAM 不是因为它"高级",而是因为在这个场景下它的失效模式最少。工业设备最怕的不是性能不够,而是数据在异常情况下损坏。
1.3 整体架构长什么样
这套方案的整体数据流是这样的:PIC18F97J60 作为主控,通过 SPI 总线挂载 MR25H40CDF 作为数据存储区,同时通过内置以太网控制器把数据上传到上位机。MR25H40CDF 负责保存掉电不能丢的关键参数、采集记录、设备配置和日志。
具体分工上,我把存储空间做了分区规划:
- 配置区(前 4KB):设备参数、校准系数、网络配置,读写频率低但绝对不能丢。
- 采集数据区(中间 480KB):循环覆盖写的采集记录,按时间戳索引。
- 日志区(后 28KB):异常事件、重启原因、通信错误记录,用于现场问题追溯。
这个分区不是拍脑袋定的,后面会讲为什么配置区要留 4KB、为什么日志区要独立出来。
2. 硬件连接与 SPI 时序:把线接对只是第一步
2.1 引脚连接与硬件片选设计
MR25H40CDF 是标准的 8 引脚 SOIC 封装,引脚定义很清晰:CS(片选)、SO(数据输出)、SI(数据输入)、SCK(时钟)、VDD、VSS,另外还有 WP(写保护)和 HOLD(保持)两个控制脚。和 PIC18F97J60 连接时,我用的引脚分配是这样的:
- RC3/SCK 接 MR25H40CDF 的 SCK
- RC5/SDO 接 MR25H40CDF 的 SI
- RC4/SDI 接 MR25H40CDF 的 SO
- RA5 作为普通 GPIO 接 CS(硬件片选)
- RB0 接 WP,RB1 接 HOLD
这里有个细节值得说:PIC18F97J60 的 MSSP 模块支持硬件片选,但我不建议用。原因是硬件片选在 SPI 传输结束后会自动拉高,而 MRAM 的某些操作(比如连续读)需要 CS 在整个传输过程中保持低电平。用普通 GPIO 手动控制 CS,时序完全由软件掌握,反而更可靠。这就是热词里常说的"SPI 硬件片选与软件片选"的取舍——高速批量传输用硬件片选省 CPU,但需要精确控制帧边界的场景,软件片选更灵活。
WP 和 HOLD 两个脚我建议都接上拉电阻到 VDD,默认不使能。WP 拉低会禁止写操作,HOLD 拉低会暂停当前传输。调试阶段可以把这两个脚引出来,方便定位问题;量产时直接上拉即可。
2.2 SPI 模式选择:Mode 0 还是 Mode 3
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。这两个模式的区别在于时钟空闲电平和采样边沿。Mode 0 是时钟空闲低电平、上升沿采样;Mode 3 是时钟空闲高电平、下降沿采样。
我实测下来,Mode 0 在 PIC18F97J60 上更稳。原因是 PIC18 的 MSSP 模块在 Mode 0 下配置最简单,CKP 位清零、CKE 位清零就行。Mode 3 需要额外注意时钟极性,如果配置错了,读出来的数据会整体移位或者全 0xFF。
配置代码大概是这样:
// SPI 初始化,Mode 0,时钟 = Fosc/4 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 (Fosc/4) SSPSTAT = 0x00; // CKE=0, SMP=0 (上升沿采样) SSPADD = 0; // 主模式下此寄存器不用 TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入时钟频率方面,PIC18F97J60 在 40MHz 晶振下,Fosc/4 就是 10MHz,MRAM 完全能吃下。如果你需要更快,可以切到 Fosc/16 之外的更高分频,但要注意 PCB 走线质量。我一般留 10MHz 作为默认,稳定优先。
2.3 PCB 布局的几个硬性要求
SPI 总线在 10MHz 以上时,PCB 布局就开始影响信号完整性了。我踩过的坑是:第一版板子把 MRAM 放在离 MCU 8 厘米远的地方,走线没做阻抗控制,结果读数据偶发错误,示波器一看 SCK 上升沿有明显振铃。
后来总结了几条硬性要求:
- 走线尽量短,MRAM 紧贴 MCU 放置,最好控制在 3 厘米以内。
- SCK 和 MOSI 走线远离模拟信号和电源开关节点,避免串扰。
- CS 走线不要和 SCK 平行长距离走,否则 CS 上的毛刺可能被误认为时钟。
- 电源脚就近放 0.1uF 去耦电容,MRAM 的写入瞬间电流会有波动,去耦不到位会导致写失败。
- 如果走线实在长,在 SCK 上串一个 22 到 33 欧姆的电阻做阻抗匹配,能明显改善振铃。
注意:MRAM 虽然写入快,但对电源质量比 EEPROM 更敏感。写入瞬间的电流尖峰如果让 VDD 跌落超过 0.2V,可能触发内部保护导致写失败。去耦电容别省。
3. 驱动实现:从底层字节收发到页写页读
3.1 底层 SPI 字节收发函数
所有上层操作都建立在字节收发之上。PIC18F97J60 的 MSSP 模块收发一个字节的流程是:写 SSPBUF,等待 BF 标志置位,读 SSPBUF。这里有个容易忽略的点——接收和发送是同时进行的,即使你只想读,也得发一个哑字节(通常是 0xFF)来产生时钟。
unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; // 写入待发送数据 while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; // 清标志 return SSPBUF; // 返回接收到的数据 }这个函数看着简单,但有两个坑。第一,SSPIF 标志必须手动清零,否则下一次等待会立即通过,导致时序错乱。第二,在高速时钟下,写 SSPBUF 之后到读 SSPBUF 之间不能插入其他操作,否则可能覆盖接收缓冲。我见过有人在这里加了调试打印,结果数据全乱。
3.2 MRAM 的指令集与操作码
MR25H40CDF 的指令集和标准 SPI Flash 类似,但简化了很多。核心指令有这几条:
| 指令名称 | 操作码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写操作前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
和 Flash 最大的区别是:MRAM 没有扇区擦除指令。Flash 写之前必须先发擦除指令(0x20 或 0xD8),等擦除完成才能写;MRAM 直接 WREN 之后就能 WRITE,写进去就是最终值。这个差异直接简化了驱动逻辑,也消除了"擦除到一半掉电"的风险。
状态寄存器的 bit0 是 WEL(写使能锁存),发完 WREN 后这位会置 1,写完数据后自动清零。调试时可以读状态寄存器确认 WREN 是否生效。
3.3 写操作实现与写使能时序
写一个字节的完整流程是:拉低 CS,发 WREN,拉高 CS,再拉低 CS,发 WRITE,发 24 位地址,发数据,拉高 CS。注意 WREN 和 WRITE 之间必须有一次 CS 的上升沿,这是 MRAM 的时序要求,很多人第一次写驱动会漏掉。
void MRAM_WriteByte(unsigned long addr, unsigned char data) { // 第一步:发写使能 CS_LOW(); SPI_ExchangeByte(0x06); // WREN CS_HIGH(); // 第二步:发写指令 + 地址 + 数据 CS_LOW(); SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); SPI_ExchangeByte(data); CS_HIGH(); }地址是 24 位的,因为 512KB 需要 19 位地址,用 24 位是为了对齐和兼容。这里有个实操心得:写完一个字节后不要立即读回验证,MRAM 虽然写入无延迟,但 CS 拉高到下一次 CS 拉低之间建议留至少 100ns 的间隔,给内部逻辑一个稳定时间。我在 10MHz 下实测,不加间隔偶尔会读到旧值,加了之后从没出过问题。
3.4 页写与连续读的边界处理
MR25H40CDF 支持页写,一页 256 字节。页写的好处是减少 CS 翻转次数,提高吞吐。但页写有个边界问题:如果起始地址不是页对齐的,写到页尾会自动回卷到页首,而不是继续写到下一页。这个行为和 Flash 一样,但很多人会忽略。
比如从地址 0x00F0 开始写 32 字节,写到 0x00FF 之后会回卷到 0x0000,把前面的数据覆盖掉。所以页写之前一定要做地址对齐判断:
void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int pageRemain = 256 - (addr & 0xFF); if (len <= pageRemain) { // 不会跨页,直接写 MRAM_WriteRaw(addr, buf, len); } else { // 跨页,拆成两次 MRAM_WriteRaw(addr, buf, pageRemain); MRAM_WriteRaw(addr + pageRemain, buf + pageRemain, len - pageRemain); } }连续读就没有这个限制,READ 指令发出后地址会自动递增,可以一直读到 512KB 末尾。我一般用连续读来批量取采集数据,一次读 256 字节,效率很高。
4. 数据可靠性与掉电保护:工业场景的命门
4.1 掉电检测与紧急写入
工业设备最怕的就是掉电瞬间数据丢失。PIC18F97J60 本身没有掉电检测模块,我用的是外部电压监测芯片加中断的方式。当 VDD 跌到 4.5V(假设系统是 5V 供电,MRAM 是 3.3V 经 LDO)时,监测芯片输出低电平触发外部中断,在中断里立即把关键数据写入 MRAM。
这里的关键是中断响应时间要足够短。PIC18F97J60 的中断延迟在高优先级下大约几个微秒,加上 SPI 写一个字节的时间(10MHz 下约 0.8us),整个紧急写入可以在几十微秒内完成。而电源端的储能电容(我一般用 470uF 到 1000uF)能撑住几毫秒,时间绰绰有余。
void __interrupt(high_priority) HighISR(void) { if (INTCON3bits.INT2IF) { // 掉电中断,紧急保存 SaveCriticalData(); // 写入 MRAM INTCON3bits.INT2IF = 0; } }提示:紧急写入的数据量要提前规划好,别在中断里做复杂计算。我一般只保存计数器、状态字和最后几条记录,控制在 64 字节以内。
4.2 数据校验与坏块管理
MRAM 虽然寿命长,但不代表不会出错。工业现场的电磁干扰、电源波动都可能导致写入异常。我在每个数据块后面加了 CRC16 校验,读取时先校验再使用。
数据块结构设计成这样:
- 前 4 字节:时间戳
- 中间 N 字节:实际数据
- 后 2 字节:CRC16 校验值
读取时如果 CRC 不匹配,就把这个块标记为无效,从备份区读取。备份区我用的是双区交替写的方式:A 区写满写 B 区,B 区写满再回 A 区,这样任何时刻至少有一个区的数据是完整的。
4.3 写均衡与寿命估算
虽然 MRAM 寿命标称 10^14 次,但为了保险,我还是做了简单的写均衡。采集数据区按块循环使用,不是每次都写同一个地址。具体做法是维护一个写指针,每次写完递增,到区尾回卷到区首。
按每天写 10 万次算,10^14 次寿命可以用 2700 多年,实际上根本不用担心寿命问题。但写均衡还有个好处:避免局部地址频繁写导致的潜在工艺缺陷暴露。任何存储器都有制造一致性问题,均匀使用能降低风险。
5. 常见问题排查与实战避坑记录
5.1 读出来全是 0xFF 或 0x00
这是最常见的现象,通常有三个原因。第一,CS 没有正确拉低,检查 GPIO 配置和硬件连接。第二,SPI 模式配置错误,Mode 0 和 Mode 3 搞反了,数据会整体错位。第三,MRAM 没有上电复位完成,MR25H40CDF 上电后需要约 1ms 的初始化时间,如果 MCU 启动太快,第一次访问可能失败。我在初始化代码里加了 2ms 延时,问题解决。
5.2 写进去读出来不对
先确认 WREN 是否生效。读状态寄存器 bit0,如果是 0,说明 WREN 没发成功或者被 WP 脚拉低禁止了。检查 WP 脚电平,确认是上拉到 VDD 而不是悬空。另外,WREN 和 WRITE 之间必须有 CS 上升沿,这个时序漏掉的话写操作会被忽略。
5.3 高速时钟下偶发错误
10MHz 以上出现偶发读写错误,八成是信号完整性问题。用示波器看 SCK 和 MOSI 的波形,如果有明显过冲或振铃,在信号线上串 22 到 33 欧姆电阻。另外检查地线是否完整,SPI 的返回电流需要良好的地平面。
5.4 问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读出全 0xFF | CS 未拉低 / 模式错误 | 示波器看 CS 和 SCK | 检查 GPIO,确认 Mode 0 |
| 读出全 0x00 | MISO 未连接 / 上电未完成 | 万用表测通断 | 补焊,加 2ms 上电延时 |
| 写入无效 | WREN 未生效 / WP 拉低 | 读状态寄存器 | 检查 WP 脚,补 CS 上升沿 |
| 偶发错误 | 信号完整性 / 电源波动 | 示波器看波形 | 串匹配电阻,加去耦电容 |
| 跨页数据错乱 | 页写回卷 | 检查地址对齐 | 拆分跨页写操作 |
5.5 几个只有踩过才知道的细节
第一个,MRAM 的 CS 拉高后不要立即拉低,中间留 100ns 以上。我在 20MHz 下测试,不留间隔的话连续操作偶尔会失败。
第二个,调试阶段把 WP 和 HOLD 引出来,用跳线控制。有一次现场设备数据写不进去,查了半天发现是 WP 脚虚焊导致电平不定,引出来之后一眼就能看出来。
第三个,不要用 MRAM 存频繁变化的大数据块。虽然它寿命长,但 SPI 带宽有限,10MHz 下理论最大 1.25MB/s,实际有效吞吐大概 800KB/s。如果要存大量数据,考虑加外部 RAM 做缓冲,批量写入。
第四个,PIC18F97J60 的 MSSP 模块在同时使用以太网时要注意资源冲突。以太网控制器也占用了部分引脚和中断,SPI 操作时最好关中断,避免时序被以太网中断打断。我在 SPI 传输前后加了 GIE 的保存和恢复。
6. 性能实测与优化建议
6.1 实测吞吐数据
在 10MHz SPI 时钟下,我实测了不同操作的耗时:
| 操作类型 | 数据量 | 耗时 | 有效速率 |
|---|---|---|---|
| 单字节写 | 1 字节 | 约 8us | 125KB/s |
| 页写 | 256 字节 | 约 230us | 1.1MB/s |
| 连续读 | 256 字节 | 约 215us | 1.19MB/s |
| 连续读 | 4KB | 约 3.4ms | 1.2MB/s |
单字节写慢是因为每次都要发 WREN 加 CS 翻转,开销大。批量写一定要用页写,效率差好几倍。
6.2 优化方向
如果吞吐还不够,可以从三个方向优化。第一,提高 SPI 时钟,MRAM 支持 40MHz,PIC18F97J60 在 40MHz 晶振下 Fosc/4 是 10MHz,想更快得换更高主频的 MCU 或者用 SPI 从模式配合外部时钟。第二,减少 CS 翻转,把多个操作合并成一次连续传输。第三,用 DMA,不过 PIC18F97J60 没有 SPI DMA,这条走不通。
实际项目里,10MHz 的吞吐对工业采集已经够用了。采集频率一般也就几十赫兹到几百赫兹,每次几十字节,SPI 带宽绰绰有余。
6.3 长期运行观察
这套方案我在一个配电监测项目里连续跑了两年多,每天写入约 5 万次,累计写入超过 3000 万次。定期抽检数据完整性,没有发现丢失或损坏。MRAM 的稳定性确实比 EEPROM 高一个档次,尤其是在温度剧烈变化的环境下。
我个人在实际操作中的体会是,MRAM 加 8 位 MCU 这套组合,胜在"简单可靠"。它不追求高性能,但把工业场景最关心的数据安全问题解决得很彻底。如果你也在做类似的数据采集或参数存储项目,不妨试试这个方案,前期多花点时间在 PCB 布局和掉电保护上,后期能省掉大量现场维护的麻烦。