我在一版老产品里用了蛮久的 SPI EEPROM,容量 64KB,参数加历史报警存得紧巴巴。后来换型时把主控一起升级到 PIC32MX764F128L,下位机要存的日志变量也翻了几倍,EEPROM 实在装不下了,我就把目光转向 MR25H40CDF 这枚 4Mbit MRAM。折腾完整个存储和读取数据的链路之后,我最大的感受是:工业嵌入式场景里,存储芯片选型比你想的要“挑环境”。这篇文章就围绕 MR25H40CDF 和 PIC32MX764F128L 这个组合,把我从硬件接线、SPI 驱动、掉电可靠性到实测性能的完整过程写出来,适合正在做嵌入式存储方案、或者被“频繁写参数 + 断电不能丢数据”折磨的同行参考。
1. 为什么最终是 MR25H40CDF:MRAM 与 EEPROM/NOR 的本质区别
1.1 单片 512KB、按字节写还不用擦除,这三点直接解决痛点
先说结论,MR25H40CDF 是一颗 4Mbit 的 SPI MRAM,换算过来是 512KB 用户空间,工作电压 3.3V,SOP-8 封装,接口就是标准 SPI,最高时钟规格支持到 50MHz。这颗料和传统 EEPROM 最大的差异在写机制上:EEPROM 写字节之前如果跨页要擦除,NOR Flash 更是要先擦一个大扇区才能写,而 MRAM 是基于磁隧道结的磁性存储,写操作本质是改变磁化方向,不存在“擦除-写入”两段式操作,所以任意地址、任意字节,写之前不需要先擦,写之后也不需要等待页编程时间。
这对我们设备意味着什么?设备有三类数据要存:
- 运行参数,每次上电会读,运行中偶尔写。
- 报警历史记录,一条记录可能只有 20 字节,但每天可能产生几百条。
- 掉电时需要立刻保存的现场快照。
以前用 EEPROM 的时候,日志写到边界要处理页对齐,跨页写还怕页边界的数据跟着被擦掉,得先放到内存里做“读改写”。换了 MR25H40CDF 之后,驱动层可以把整个 512KB 看成一个可随机字节写的大数组,日志模块再也不用关心页边界,代码直接砍掉一大半。
1.2 横向参数对比:为什么贵出来的差价花得值
我把当时选型时对比的几个方案整理了一下,方便你直观理解:
| 项目 | 常规 SPI EEPROM(如 25LC512) | SPI NOR Flash(如 W25Q128) | MR25H40CDF MRAM |
|---|---|---|---|
| 容量 | 64KB 级别常见 | 16MB 级别常见 | 512KB |
| 写操作 | 按字节/页,跨页需处理 | 必须先擦扇区再写 | 任意字节直写 |
| 擦除时间 | 有,几 ms | 扇区擦除几十~几百 ms | 无擦除概念 |
| 写寿命提示 | 100 万次典型 | 10 万次典型 | 可视为无磨损限制 |
| 掉电数据保持 | 20~100 年 | 10~20 年 | 20 年以上 |
| 读随机性 | 随机读方便 | 随机读可以但地址管理繁琐 | 完全随机读写 |
工业设备最怕两件事:程序跑飞后参数区损坏,以及频繁写日志把 Flash 写穿。MRAM 的耐用性在这里是“用钱买命”的典型——贵是贵一点,但日志可以直接无脑循环写,不用在 RAM 里做磨损均衡,也不怕现场工人一天断电几十次。对我们这种逻辑优先的 MCU 工程来说,把这部分复杂度从固件里拿掉,比省几块钱芯片成本划算得多。
1.3 同类 FRAM 也能读改写,为什么我没选 FRAM
选型时也看了铁电 FRAM,比如富士通和瑞萨的 SPI 铁电,同样支持字节写无擦除。没选它主要是容量和采购渠道的问题:我需要 512KB 这个容量级,FRAM 常见容量偏小,密度上去之后价格和交期都不如 MRAM 稳定。MR25H40CDF 在工业温度范围内能顶到 105°C,这对我们装在控制柜里、夏天表面温度能到 80°C 的场合比较关键。另外,MR25 系列在赛普拉斯/英飞凌的产品线里属于量产多年的成熟料,参考设计和驱动代码都齐全,踩坑概率低。
2. PIC32MX764F128L 和 MR25H40CDF 的硬件连接,具体怎么接线
2.1 引脚对应:一共 6 根线就能跑起来
MR25H40CDF 是 SOP-8,引脚不多,但每个引脚都有讲究:
| MR25H40CDF 引脚 | 功能 | 接到 PIC32MX764F128L |
|---|---|---|
| CS | 片选 | 任意 GPIO,我用的 LATC1 |
| SCK | SPI 时钟 | SCK1 |
| SI | 数据输入 | SDO1 |
| SO | 数据输出 | SDI1 |
| WP | 写保护,低有效 | 必须接 3.3V 拉高 |
| HOLD | 暂停通信,低有效 | 必须接 3.3V 拉高 |
| VCC | 3.3V 电源 | 3.3V 电源轨 |
| GND | 地 | 共地 |
PIC32MX764F128L 这颗是 MIPS 内核,外设资源比较全,我用了 SPI1 模块,没有走到哪去。SCK1/SDO1/SDI1 在 100 脚封装上有明确对应的引脚,你可以根据自己的 PCB 布局重新映射,核心逻辑不变。硬件上就三根信号线加一根片选,PCB 布线压力很小,这也是我推荐用 SPI 而不是并行接口的原因——MR25H40CDF 本身没有并行接口,硬要在 PIC32 上用 PMP 去模拟并行读写纯属自找麻烦。
2.2 SPI 模式 0 还是模式 3:这个决定影响整条通信链路
读过 MR25H40CDF 数据手册的都知道,它同时支持 SPI Mode 0 和 Mode 3,也就是 CPOL/CPHA 的四种组合里允许两种。第一次画板子时我随手用了 PIC32 的默认模式,结果读状态寄存器正常,连续读多字节时偶尔错位,查了半天才发现时钟极性和相位选错了。
我把两个模式对应关系写成表,方便你对照寄存器配置:
| SPI 模式 | CPOL(时钟空闲电平) | CPHA(采样沿) | 使用场景 |
|---|---|---|---|
| Mode 0 | 低 | 上升沿采样 | 很多 SPI 设备默认 |
| Mode 3 | 高 | 下降沿采样 | MRAM 手册推荐同样支持 |
实际操作中我选了 Mode 3。原因是 PIC32MX764F128L 在 80MHz 主频下,Mode 3 的时序余量对我来说更好量测,而且后续如果换用别的 SPI 传感器,Mode 3 兼容性也比较常见。配置代码里体现为 CKP=1、CKE=0。
这里有个经验:不要靠“默认”过活,拿到任何 SPI 存储器先读状态寄存器,能正确读出 0x00 或厂商默认值,再谈后续读写。我见过不少同事一上来直接发 READ 命令,读回全 FF,最后才发现是模式不匹配。
2.3 去耦、WP/HOLD 引脚和 PCB 上被忽略的细节
MR25H40CDF 是 3.3V 供电,数据手册要求 VCC 在允许范围内稳定。它工作电流不算大,但工业环境里电源纹波往往比实验室大得多,所以我在 VCC 和 GND 之间放了 0.1uF 陶瓷电容,并且在靠近芯片处加了一个 10uF 钽电容兜底。长线缆供电时这个组合能明显减少偶发读写错误。
WP 和 HOLD 这两个引脚内部虽然通常有上拉,但我强烈建议在 PCB 上直接拉高到 3.3V,而不是悬空或者通过 10K 电阻拉高。为什么?悬空状态下,如果 PCB 上有噪声耦合,WP 一旦被拉低,所有写命令都会被忽略,表现就是“读正常、写没反应”,极其难查;HOLD 被拉低更可怕,它会让芯片在通信过程中把数据线冻结住,主控这边表现为奇怪的时序中断。我在初版板上吃过 HOLD 悬空的亏,后来用万用表量到 HOLD 引脚在 -0.3V 附近跳,才明白是噪声惹的祸。稳定做法是 WP、HOLD 直接接 VCC,不串电阻,简单粗暴有效。
另外,PIC32MX764F128L 的 GPIO 输出高电平是 3.3V,和 MRAM 电平完全匹配,不需要加电平转换。CS 线的长度尽量控制在 5cm 以内,如果实在要走长线,可以在 CS 上对地加一个 1nF 电容滤毛刺,但要注意这会影响片选上升沿速度,别把电容加太大,否则高速通信时片选边沿会变缓。
3. 在 PIC32MX764F128L 上写驱动:从初始化到 512KB 寻址
3.1 SPI 外设初始化的寄存器写法
我用的是 MPLAB X IDE,基于 Harmony 写框架也是可以的,但如果只想快速验证 MRAM 芯片,直接用寄存器最干净,连 Harmony 的代码生成都不用跑。初始化函数如下:
#define MRAM_CS_TRIS TRISCbits.TRISC1 #define MRAM_CS_LAT LATCbits.LATC1 void SPI1_Init_MRAM(void) { // 先禁用 SPI 模块,清配置 SPI1CON = 0; SPI1STATbits.SPIROV = 0; // 清溢出标志 // 主机模式、模式3:空闲时钟高、下降沿锁存数据 SPI1CONbits.MSTEN = 1; SPI1CONbits.CKP = 1; SPI1CONbits.CKE = 0; SPI1CONbits.SMP = 0; SPI1CONbits.MODE32 = 0; SPI1CONbits.MODE16 = 0; SPI1CONbits.ENHBUF = 0; // 80MHz 外设时钟,SPI 时钟 = 80MHz / (2 * (1+1)) = 20MHz SPI1BRG = 1; // 片选脚配置为数字输出,默认拉高 MRAM_CS_TRIS = 0; MRAM_CS_LAT = 1; // 开启 SPI 模块 SPI1CONbits.ON = 1; }有几个细节说明一下:SPI1BRG 的计算公式是Fpb / (2 * (SPI1BRG + 1)),我这里外设时钟按 80MHz 算,想让 SPI 跑 20MHz,所以 80 / (2*20) - 1 = 1。如果你的时钟树配置不同,这个值要重新算。ENHBUF 增强缓冲模式在 20MHz 下没有必要开,反而会引入读数据顺序理解的复杂度,保持默认即可。
SPI 收发基础函数就这么写:
uint8_t SPI1_ExchangeByte(uint8_t data) { SPI1BUF = data; while(!SPI1STATbits.SPIRBF); return SPI1BUF; }注意这个函数在连续读多字节时,第一次读返回的是“虚拟字节”,真正你要的数据在第二次、第三次交换中才从 SO 引线上推出来,驱动里要处理好这个节奏。
3.2 驱动层实现:WREN、READ、WRITE、RDSR
MR25H40CDF 的指令集是标准 SPI Memory 结构,核心四路命令如下:
| 命令名 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,每次写命令前必须发 |
| WRITE | 0x02 | 后跟 3 字节地址 + 数据 |
| READ | 0x03 | 后跟 3 字节地址,连续读 |
| RDSR | 0x05 | 读状态寄存器 |
我把它封装成下面这些函数,代码里直接体现整个读写流程:
void Mram_WriteEnable(void) { MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x06); // WREN MRAM_CS_LAT = 1; } uint8_t Mram_ReadStatus(void) { uint8_t sr; MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x05); // RDSR sr = SPI1_ExchangeByte(0x00); MRAM_CS_LAT = 1; return sr; }写入单字节时,流程是先拉低 CS,发 WREN,拉高 CS,然后再拉低 CS 发 WRITE 命令、3 字节地址和要写的数据,最后拉高 CS。为什么 WREN 一定要单独一个 CS 周期?因为 MR25H40CDF 内部有写使能锁存,WREN 指令必须在 CS 拉高之后才会真正生效。你要是把 WREN 和 WRITE 放在同一个 CS 低电平周期里连续发,芯片不会接受这次写操作,这是新手最容易踩的坑。
void Mram_WriteByte(uint32_t addr, uint8_t data) { // 写使能 Mram_WriteEnable(); MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); SPI1_ExchangeByte(data); MRAM_CS_LAT = 1; // CS 拉高后芯片自动开始内部写入,无擦除等待 }读取则是全程 CS 拉低,发命令和地址后,连续交换空字节读回即可:
void Mram_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x03); // READ SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); // 连续读:第一个交换出的字节是无效数据 SPI1_ExchangeByte(0x00); for(uint32_t i = 0; i < len; i++) { buf[i] = SPI1_ExchangeByte(0x00); } MRAM_CS_LAT = 1; }地址是 24 位,但 MR25H40CDF 实际只有 512KB,也就是地址范围 0x000000 ~ 0x07FFFF,超过这个范围的行为未定义,所以上层模块一定要做地址边界检查。我把所有存储分配都集中在 0x0000 到 0x7FFFF 之内,高位地址直接作为保留区域不访问。
3.3 多字节连续写、跨页边界与循环日志的实现技巧
MR25H40CDF 支持在同一个 WRITE 周期里连续写入多字节,只要 CS 保持低电平,地址会内部自动递增,跨过任意边界都行,没有 Flash 那种“本页写完必须重新发命令”的限制。因此我写日志时可以直接一次写入一帧 128 字节,效率比单字节写高很多,而且不用考虑页大小。
循环日志区我把它划成 256KB,每个记录 32 字节,最多 8192 条。写指针存到另一个非易失字段里,每次上电先读指针,再按指针位置写记录,写到末尾就回卷到区域开头。这个结构在 EEPROM 时代要额外处理页对齐和磨损,在 MRAM 上就是纯数组回绕逻辑,写起来极度舒适。
有一点要注意:连续写的内部地址递增是 24 位全范围递增,不会在 512KB 边界自动截断。虽然我们不会故意越过 0x07FFFF,但程序里还是要防一手,避免日志模块 bug 导致地址跑到未定义区域。
3.4 状态寄存器里那个 WEL 位,别指望它自动帮你记忆
每次完成 WRITE 之后,芯片内部的写使能锁存会被自动清除,也就是说下一次写之前必须重新发 WREN。我在调试时还加了一段防御代码,写之前读一次 RDSR,如果 WEL 位为 0 就再发一次 WREN,如下:
if((Mram_ReadStatus() & 0x02) == 0) { Mram_WriteEnable(); }这个判断在正常状态下显得多余,但在排查问题时很有用,能确认 WREN 是否真的写进芯片了。特别是在用逻辑分析仪抓波形时,如果读写命令都对却写不进数据,十有八九是这个 WEL 位没置上。
4. 工业现场的可靠性问题:掉电、噪声、校验
4.1 我真实遇到的一次断电写丢失
设备装机后,现场反馈“上电后报警记录偶发丢失”,每次丢的都是最后两三条。我最初怀疑 MRAM 坏了,因为在概念里 MRAM 不像 Flash 会坏块。后来把掉电瞬间的 VCC 波形抓到示波器上才明白,问题出在“掉电检测点太晚”:
控制板用的是 3.3V 电源,外部 24V 掉电后,板载电容还能撑几十毫秒。程序里靠 ADC 检测 24V 电压,降到阈值后启动“紧急保存流程”,把现场数据写进 MRAM。但有一次现场电磁环境很差,24V 电压跌落后又抖动回升,ADC 阈值触发时 VCC 已经掉到 2.8V,MR25H40CDF 虽然标称 3.3V 供电,但在这个欠压边缘能不能保证内部磁翻转成功,数据手册不会给你任何承诺。
从那以后我调整了策略:不依赖“检测到掉电再去写”,而是把关键数据在正常运行中持续实时写入,掉电前需要保存的数据量压缩到最少。MRAM 无擦除、高耐久的特性在这里帮了大忙——我可以每隔几秒就把运行状态刷新写一遍,完全不心疼寿命。
4.2 复现测试:最小写入时间窗口与掉电曲线
为了验证调整后的方案靠不靠谱,我做了一个简单复现测试:
- 让 PIC32MX764F128L 以 10ms 周期不断往 MRAM 写一个 32 字节帧。
- 固态继电器随机切断 24V 输入,每次断电后重新上电,读取最后写入的帧头和 CRC。
- 统计 200 次断电结果。
实测下来,如果 VCC 在写入过程中跌落到 2.5V 以下,确实会偶发出现校验失败,但比例很低。这说明 MRAM 自身非常抗掉电,但在电源纹波极差的环境里,不能把它的工作电压范围之外的行为当作设计基准。所以我最终的方案是“持续备份 + 上电 CRC 校验”,而不是“掉电抢救式写入”。
4.3 双区备份和 CRC 回滚策略,我的具体工程做法
工业设备的数据存储要做到即使写入一半断电,也能从上一版恢复。MRAM 虽然不需要擦除,但“写入一半”依然存在,所以我自己设计了一套很轻量的双区方案:
存储区划分:
| 区域 | 地址范围 | 大小 | 用途 |
|---|---|---|---|
| A 区参数 | 0x000000 ~ 0x01FFFF | 128KB | 主参数区 |
| B 区参数 | 0x020000 ~ 0x03FFFF | 128KB | 备份参数区 |
| 日志区 | 0x040000 ~ 0x07FFFF | 256KB | 循环日志 |
每次保存参数时,先把完整参数包写入 B 区,等 B 区写完后,再写 A 区。A 区开头固定放一个 4 字节魔数 + 2 字节 CRC16。读取顺序是这样的:
- 读 A 区魔数和 CRC,正确就用 A 区。
- A 区校验失败,读 B 区,正确就用 B 区,并趁机会把 B 区回写到 A 区。
- 两个区都失败,报警并启用出厂默认值。
CRC 我用 CRC16-CCITT,一个查表函数 100 行以内搞定,跑在 PIC32 上 128KB 数据全量校验约 20ms,完全可以接受。这个双区方案比在 NAND Flash 上做 FTL 轻多了,原因是 MRAM 的上电稳定性好,不会出现坏块,双区纯粹就是为了处理“写入到一半掉电”这种边缘情况。
5. 实测数据和我保留的三条经验
5.1 读取性能实测:20MHz 时钟下的全片读测试
我在 20MHz SPI 时钟下做了全片读测试,从地址 0 连续读到 0x3FFFF,512KB 数据加命令开销,示波器测得总时间约为 220ms。计算一下:512K * 8bit / 20Mbps ≈ 204.8ms,实测多出来的十几毫秒就是每次 CS 切换、地址发送和循环指令造成的。如果按这个速度读,MCU 每次上电把 512KB 全量读进 RAM 不现实,因为 PIC32MX764F128L 的 SRAM 只有 32KB,所以读取通常按块处理。
实际操作中我验证了随机读性能:任意地址读 32 字节,从 CS 拉低到读完 32 字节再拉高,大约 16us。这个速度对数据采集、运行参数加载来说绰绰有余。
5.2 写入性能实测:连续写和随机写的真实差别
MRAM 的写性能和读性能差距很小,这一点和 Flash 完全不同。同一地址重复写 1 万次,逻辑分析仪抓到的时间几乎不变;连续写 256 字节需要约 105us + 命令字节时间,换算下来大约 19Mbps 的有效写速率。
我做了另一个更极端的测试:把同一个地址连续写 100 万次,然后读取数据,读回值和写入值完全一致。MRAM 官方标称的写耐久基本可以理解为无磨损,至少在我这个量级的寿命测试里看不到退化。所以我后来日志模块直接放弃了磨损均衡算法,这在 Flash 上等于是自寻死路,在 MRAM 上则是合情合理。
5.3 三条经验:HOLD 不能悬空、WREN 周期、电源纹波才是隐形杀手
第一条,HOLD 和 WP 必须处理到位。我已在前文说过,这俩引脚哪怕有内部上拉,也架不住工业环境地弹噪声,PCB 上直接连 3.3V 最省心。现场原理图评审时,我把 HOLD 直接接 VCC 作为硬性要求写进了 checklist。
第二条,每次写命令都要发独立的 WREN 周期。MR25H40CDF 的写使能锁存在写完后会清除,这个和很多 EEPROM 不太一样。如果你用逻辑分析仪看到 WRITE 命令发出去了,数据也出现在 MISO 上,但读回来还是旧值,先查是不是漏了 WREN。
第三条,电源纹波和电压掉电曲线是决定 MRAM 可靠性的关键。MRAM 本质上是模拟磁性存储,正常 VCC 范围内非常可靠,但跌出规格后一切皆有可能。工业现场存储方案要做到“写入内容可恢复”,一方面靠 MRAM 本身的稳定性,另一方面靠双区 + CRC 兜底,缺一不可。
这块板子后来小批量装了二十多台,最长的已经在现场跑了三个月,存储相关的售后记录为 0。后续我考虑再做一版,把日志区改用环形 DMA 加缓存批量写入,把连续写速率再往上顶一顶,这样现场数据记录密度还能再上一档次。如果你也正卡在“频繁写、怕掉电、又不想上大 Flash+文件系统”这类需求上,MR25H40CDF 这类 SPI MRAM 确实值得拿一块来试试。