1. 项目缘起与方案选型思考
1.1 为什么要在工业场景里折腾 MRAM 这颗"新料"
做工业嵌入式这行的朋友应该都有体会,选存储芯片这件事,往往比选主控还让人头疼。EEPROM 擦写寿命撑不住高频采集,NOR Flash 写入前要擦块、掉电还容易丢数据,FRAM 容量小价格高,带电池的 SRAM 又要考虑电池寿命和环保问题。我手上这个项目是做工业现场的数据采集终端,要求每秒记录一次传感器数据,断电后最近的数据不能丢,还要能扛住车间里的强电磁干扰和宽温环境。翻来覆去对比之后,最终把目光落在了MR25H40CDF这颗MRAM(磁性随机存储器)上。
MRAM 这东西有意思的地方在于,它用磁性隧道结存储数据,兼具 SRAM 的高速读写和 Flash 的非易失特性,写入不需要擦除、没有写延迟、理论擦写次数近乎无限。MR25H40CDF 是 4Mbit(512KB)容量、SPI 接口的串行 MRAM,工作电压 2.7V~3.6V,工业级温度范围 -40℃~85℃,SPI 时钟最高能跑到 40MHz。这些参数放在工业采集场景里,基本就是"对症下药"。
主控这边我选的是PIC18F46K20,8 位机里比较经典的一颗,64KB Flash、3936 字节 RAM、1KB EEPROM,自带 MSSP 模块可以配置成 SPI 主机,工作电压 1.8V~3.6V,工业级温度范围也够。选它不是因为性能多强,而是因为项目对算力要求不高,PIC18 系列的外设稳定、资料全、供货稳,配合 MRAM 做数据记录绰绰有余。这套组合的核心任务就一句话:通过 SPI 总线,让 PIC18F46K20 稳定地往 MR25H40CDF 里写数据、读数据,并且保证掉电不丢、上电可恢复。
1.2 方案对比:为什么不是 EEPROM、不是 Flash
在动手之前,我把几种常见方案摆在一起做了个对比,这也是我建议每个做存储选型的人都该走一遍的流程。
| 存储类型 | 写入前是否需擦除 | 擦写寿命 | 写入速度 | 掉电安全性 | 成本 |
|---|---|---|---|---|---|
| EEPROM | 否 | 约 100 万次 | 慢(ms 级) | 好 | 低 |
| NOR Flash | 是(按扇区) | 约 10 万次 | 慢(ms 级) | 擦写中掉电有风险 | 低 |
| FRAM | 否 | 近乎无限 | 快 | 好 | 高 |
| MRAM | 否 | 近乎无限 | 快(ns 级) | 好 | 中 |
| SRAM+电池 | 否 | 无限 | 最快 | 依赖电池 | 中 |
从表里能看出来,MRAM 在"免擦除、长寿命、掉电安全"这三点上几乎全占,成本又比 FRAM 友好。对于每秒写一次、一写就是几年的工业采集场景,EEPROM 的百万次寿命算下来大概几个月就写爆了,Flash 的擦除延迟和掉电风险更是硬伤。所以选 MR25H40CDF 不是赶时髦,是算过账的。
1.3 整体架构一句话说清
整个系统的数据流很直白:传感器数据进 PIC18F46K20,主控通过 MSSP 模块的 SPI 主机模式,把数据按页写进 MR25H40CDF;需要读取时,主控发读命令,MRAM 把数据回传。中间涉及片选控制、SPI 时序配置、地址管理、掉电保护几个关键环节。下面我会把这些环节一个个拆开讲,包括我踩过的坑和实测有效的参数。
2. 硬件连接与 SPI 底层配置要点
2.1 引脚连接:别小看这几根线
MR25H40CDF 是 8 脚 SOIC 封装,SPI 接口,引脚定义很标准:CS(片选)、SCK(时钟)、SI(数据输入)、SO(数据输出)、VDD、VSS,另外还有 WP(写保护)和 HOLD(保持)两个控制脚。和 PIC18F46K20 连接时,我用的对应关系是这样的:
- MR25H40CDF 的 CS 接 PIC18F46K20 的 RC2(可软件控制,也可用硬件 SPI 的 SS)
- SCK 接 RC3(SCK)
- SI 接 RC5(SDO,主出从入)
- SO 接 RC4(SDI,主入从出)
- WP 和 HOLD 都上拉到 VDD(不用的功能别悬空)
这里有个新手常犯的错:把 WP 和 HOLD 悬空。悬空状态下引脚电平不确定,可能被干扰拉低,导致写保护误触发或者通信被 Hold 打断。我的做法是各接一个 10K 上拉电阻到 VDD,稳当。
注意:MR25H40CDF 的 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 最省心,因为 PIC18F46K20 的 MSSP 模块配置成 Mode 0 时,SCK 空闲为低,数据在上升沿采样,和 MRAM 的时序完全对得上。配置寄存器 SSPCON1 的关键位:
// PIC18F46K20 MSSP 配置为 SPI 主机,Mode 0,时钟 = Fosc/4 SSPCON1 = 0b00100000; // SSPEN=1, CKP=0(空闲低), SSPM=0000(Fosc/4) SSPSTAT = 0b10000000; // SMP=1(在数据末尾采样), CKE=0(空闲到活动沿发送)这里 SMP 和 CKE 两位决定了采样时机,配错了会出现读回来的数据整体移位或者全 0xFF。我一开始就是 CKE 配反了,读出来全是 0xFF,查了半天才发现是时序问题。
2.3 时钟频率:40MHz 是上限,但别一上来就拉满
MR25H40CDF 标称最高 40MHz,但 PIC18F46K20 在 64MHz 系统时钟下,SPI 分频后最高也就 16MHz(Fosc/4)。我实际用的是 16MHz,跑下来很稳。如果你用的是更快的 MCU,想跑到 40MHz,那 PCB 布线就得讲究了:SCK 走线尽量短、远离干扰源、必要时串一个 22Ω 电阻做阻抗匹配。工业现场我建议留足余量,别贴着上限跑,16MHz 对每秒一次的数据记录来说完全够用,稳定性优先。
2.4 片选控制:硬件片选还是软件片选
PIC18F46K20 的 MSSP 模块支持硬件 SS 控制,但我更推荐软件片选,也就是用普通 IO 口手动拉低拉高 CS。原因有两个:一是硬件 SS 在某些配置下会和 SPI 从机模式冲突,二是软件片选时序完全可控,方便在命令之间插入必要的延时。我的做法是把 RC2 配成输出,每次通信前拉低,通信完拉高:
#define MRAM_CS_LAT() (LATC2 = 0) // 片选拉低,选中 MRAM #define MRAM_CS_HIGH() (LATC2 = 1) // 片选拉高,释放 MRAM提示:CS 拉低到第一个 SCK 上升沿之间,MRAM 要求至少有几个 ns 的建立时间。16MHz 下这个时间天然满足,但如果你的 SPI 跑得很快,记得在拉低 CS 后加一两个 NOP 再发数据。
3. MRAM 读写命令与实操流程
3.1 先搞懂 MR25H40CDF 的命令集
MR25H40CDF 的命令集不复杂,常用的就几条:
| 命令名称 | 命令码 | 作用 |
|---|---|---|
| WREN | 0x06 | 写使能,写之前必须先发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
关键点在于:每次写操作之前必须先发 WREN(0x06),否则写命令会被忽略。这是 MRAM 和 EEPROM 类似的地方,很多人第一次用会忘,写完发现数据没进去,其实就是漏了写使能。
3.2 写数据的完整时序
写一个字节到指定地址,完整流程是这样的:
- 拉低 CS
- 发送 WREN(0x06)
- 拉高 CS(WREN 需要独立的一个片选周期)
- 再次拉低 CS
- 发送 WRITE(0x02)
- 发送 24 位地址(MR25H40CDF 是 4Mbit,需要 19 位地址,但按 3 字节发送,高位补 0)
- 发送数据字节
- 拉高 CS
这里有个细节:WREN 必须单独一个片选周期,不能和 WRITE 命令连在同一个 CS 低电平里。我一开始图省事把 WREN 和 WRITE 连着发,结果写不进去,后来翻数据手册才发现这个要求。
void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LAT(); SPI_Exchange(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LAT(); SPI_Exchange(0x02); // WRITE SPI_Exchange((addr >> 16) & 0xFF); SPI_Exchange((addr >> 8) & 0xFF); SPI_Exchange(addr & 0xFF); SPI_Exchange(data); MRAM_CS_HIGH(); }3.3 读数据的完整时序
读就简单多了,不需要 WREN:
- 拉低 CS
- 发送 READ(0x03)
- 发送 24 位地址
- 连续读取数据(地址会自动递增)
- 拉高 CS
MRAM 支持连续读,地址会自动递增,所以读一整页数据只需要发一次命令,后面一直读就行,效率很高。
void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LAT(); SPI_Exchange(0x03); // READ SPI_Exchange((addr >> 16) & 0xFF); SPI_Exchange((addr >> 8) & 0xFF); SPI_Exchange(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI_Exchange(0x00); } MRAM_CS_HIGH(); }3.4 状态寄存器:判断写是否完成
MRAM 的写速度极快,基本不需要像 Flash 那样轮询等待。但为了保险,写完关键数据后可以读一下状态寄存器(RDSR,0x05),确认 WEL(写使能锁存)位已经清零。不过实测下来,MRAM 的写周期在 ns 级,MCU 发完命令拉高 CS 的时候早就写完了,所以这个轮询在大多数场景下可以省略。我保留这个步骤主要是为了调试阶段确认时序没问题。
3.5 地址管理:512KB 怎么规划
512KB 看着不小,但要做循环记录就得规划好。我的做法是把存储区分成两块:前 4KB 存配置参数和元数据(比如写指针、记录条数、校验值),后面 508KB 做环形缓冲区存采集数据。每条记录 16 字节,算下来能存约 3.2 万条,按每秒一条算能存 9 个小时左右,够工业现场一个班次用了。写指针存在固定地址,每次上电先读出来,接着上次的位置继续写,实现掉电续传。
4. 掉电保护与数据完整性设计
4.1 为什么 MRAM 也要考虑掉电保护
有人会问,MRAM 不是非易失的吗,掉电就掉电,数据又不会丢,还要什么保护?这话对了一半。MRAM 确实掉电不丢数据,但掉电瞬间正在进行的写操作可能只写了一半,比如地址已经发出去、数据发了一半,这时候断电,那条记录就是残缺的。所以掉电保护要保护的不是"数据丢失",而是"数据一致性"。
4.2 双缓冲加校验的实操方案
我的做法是每条记录后面跟一个 CRC16 校验值,读取的时候校验不过就丢弃这条。同时用双指针机制:写指针和提交指针分开,数据写完并校验通过后,才更新提交指针。这样即使掉电发生在写数据的过程中,提交指针还指向上一条完整记录,不会读到半截数据。
typedef struct { uint32_t timestamp; uint16_t sensor1; uint16_t sensor2; uint8_t reserved[6]; uint16_t crc; } Record_t; // 16 字节CRC 我用的是标准 CRC16-CCITT,多项式 0x1021,初始值 0xFFFF。这个算法在 8 位机上跑也不慢,一条 16 字节记录算下来几十微秒,完全不影响每秒一次的采集节奏。
4.3 上电恢复流程
上电后第一件事不是急着写数据,而是先恢复状态:
- 读配置区,拿到写指针和提交指针
- 从提交指针位置读最后一条记录,校验 CRC
- 校验通过,说明上次掉电前数据完整,从写指针继续
- 校验不通过,回退到上一条,修正指针
这个流程我实测过很多次,包括故意在写数据中途断电,上电后都能正确恢复到上一条完整记录,没有出现过数据错乱。
注意:配置区本身也要做备份,我是在配置区存两份互为镜像的元数据,读取时两份对比,不一致就用备份恢复。工业现场电磁干扰强,单份元数据被翻位的概率虽然低,但一旦发生就是灾难性的。
5. 常见问题排查与避坑经验
5.1 读出来全是 0xFF 或 0x00
这是最典型的 SPI 通信失败症状。排查顺序我总结成一张表:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 全 0xFF | MISO 没接好或从机没响应 | 查 SO 引脚连接、CS 是否拉低 |
| 全 0x00 | 时钟没出来或模式不对 | 示波器看 SCK、检查 CPOL/CPHA |
| 数据移位 | 采样边沿配错 | 调 SMP/CKE 位 |
| 偶发错误 | 干扰或时序余量不足 | 降 SPI 频率、加去耦电容 |
我遇到过一次全 0xFF,查了半天是 CS 引脚虚焊,重新补焊就好了。所以硬件问题永远先查,别一上来就怀疑代码。
5.2 写不进去数据
九成是漏了 WREN,或者 WREN 和 WRITE 挤在同一个片选周期里。剩下的一成是 WP 引脚被拉低了,写保护生效。我的习惯是在初始化时先读一次状态寄存器,确认 WP 状态正常,再开始写。
5.3 长时间运行后数据错乱
这个问题比较隐蔽,通常是电源纹波或者地弹导致的。工业现场我建议:MRAM 的 VDD 脚旁边放一个 0.1uF 陶瓷电容加一个 10uF 钽电容,PCB 上 MRAM 尽量靠近 MCU,SPI 走线等长、包地。我有个项目就是因为 MRAM 离 MCU 太远,走线长了,跑了两天才出现偶发错误,后来缩短走线加电容就稳了。
5.4 调试小技巧
调试 SPI 通信,我最推荐的工具是逻辑分析仪,几十块钱的那种就够用。把 CS、SCK、SI、SO 四根线一夹,命令码、地址、数据一目了然,比用串口打印高效得多。另外,初始化阶段可以先做一个"写一个字节再读回来"的自检,确认通信链路通了再跑业务逻辑,能省掉大量排查时间。
6. 实测性能与扩展思路
6.1 实测数据
在 16MHz SPI 时钟下,我实测单字节写入(含 WREN 和地址)约 5 微秒,连续读 512 字节约 260 微秒。每秒写一条 16 字节记录,占用时间不到 100 微秒,对主循环毫无压力。连续跑了一周,写了约 60 万条记录,没有出现一次数据错误,MRAM 的稳定性确实对得起它的定位。
6.2 还能怎么扩展
这套方案往上扩展空间不小。比如把记录结构改成变长,支持不同传感器类型;比如加一个简单的文件系统层,按时间检索;再比如用 PIC18F46K20 的 EEPROM 存配置、MRAM 存数据,分工更清晰。如果数据量再大,可以换更大容量的 MRAM 或者多片级联,SPI 总线上挂多个片选就行。
我个人在实际操作中的体会是,MRAM 这类器件的价值不在于参数多亮眼,而在于它把"高频写入"和"掉电不丢"这两件本来矛盾的事给统一了。以前做数据记录,要么牺牲写入频率保寿命,要么加电池保数据,现在一颗 MRAM 就解决了。选型的时候别只盯着价格,算一算全生命周期的维护成本和数据风险,很多时候贵一点的那颗反而是省钱的。最后分享一个小技巧:MRAM 的写使能 WREN 命令,可以在初始化时先发一次,确认状态寄存器 WEL 位置位,这样能提前发现 SPI 链路的问题,比等到真正写数据时才发现要主动得多。