把 MR25H40CDF 和 PIC32MZ1024EFK144 放在一起做数据存储,是我在好几个工业项目里反复验证过的一套组合。MR25H40CDF 是 Everspin 的 SPI 接口 MRAM,写入无需擦除、寿命几乎无限、宽温稳定;PIC32MZ1024EFK144 是 Microchip 面向工业控制的主流单片机,SPI/DMA 外设齐全。它俩配合,基本覆盖了我遇到的大部分工业数据存储需求:频繁记录遥测数据、掉电瞬间保存关键状态、高温环境下的参数备份。这篇内容我不打算照着数据手册念参数,主要讲从硬件接线到软件驱动再到可靠性设计的完整链路,也把我踩过的坑和实测中得到的数据写出来。适合正在选型、或者已经拿到 MRAM 样品但不知道驱动怎么写的嵌入式工程师,也适合刚从 STM32/Microchip 平台入门、想看看工业级存储怎么落地的朋友。
1. 先想清楚:工业和嵌入式场景为什么需要 MRAM
1.1 三大存储方案的对比
很多嵌入式工程师一提起掉电保存,第一反应就是 SPI Flash 或者 I2C EEPROM。大多数场景下这没问题,但是一旦到了工业现场,事情会变得不太一样。
工业设备有几个典型特征:写入频率高、环境温度高、电源质量差。以电力终端为例,每 5 分钟要记录一次遥测数据,一年下来就是 10 万次写入;如果现场还有事件记录、故障录波,一天写几百上千次都很正常。普通 SPI Flash 的擦写寿命一般在 10 万次左右,看起来够用,但 Flash 是按扇区擦除的,你写一个字节往往也要先擦掉整个扇区,实际损耗比想象中大得多。EEPROM 的寿命更短,典型也就 100 万次擦写,而且字节写周期是毫秒级,掉电瞬间往往来不及写完。
这时候 MRAM 的优势就很明显了。它本质上是磁阻存储单元,写入不需要擦除,没有“擦写寿命”这个概念,掉电数据也不会丢。我用一个生活化的类比:Flash 像一张只能用橡皮擦改字但越擦越薄的纸,EEPROM 也类似,只是纸更小。MRAM 像一块白板,写满直接覆盖就行,板面不会被磨坏。
下面是三种方案的关键参数对比,这也是我做选型时最常翻的一张表:
| 对比项 | SPI NOR Flash | EEPROM | MR25H40CDF (MRAM) |
|---|---|---|---|
| 写入前是否需要擦除 | 是,按扇区/页擦除 | 不需要,但按页缓冲 | 不需要,可直接覆盖 |
| 典型写寿命(次) | 1万~10万 | 10万~100万 | 10^16,实际视为无限 |
| 单字节写入能力 | 不支持,页编程 | 支持 | 支持 |
| 页写/擦除时序 | 毫秒~百毫秒级 | 毫秒级 | 纳秒级写周期,SPI时钟决定速度 |
| 工作温度范围 | 多为-40~+85℃ | -40~+125℃ 可选 | -40~+125℃ |
| 典型掉电保持 | 10~20年 | 40~100年 | 20年以上,高温下仍稳定 |
| 数据随机改写 | 不友好 | 一般 | 非常友好 |
别误会,我并不是说 Flash 没用。在容量和成本上 Flash 依然碾压 MRAM,大容量日志存储、固件镜像、文件系统这些还是得靠 Flash。但在“频繁改写关键数据”这个小而关键的细分场景里,MRAM 确实是最省心的方案。
1.2 MR25H40CDF 的核心参数与“随写随存”特性
MR25H40CDF 是 Everspin 的一款 4Mbit 串行 MRAM,容量 512KB,采用标准 SPI 接口。这里有几个参数我觉得比容量本身更重要。
第一,接口是标准 SPI,支持 Mode 0,最高时钟 40MHz。这是它好用的大前提,无论是 STM32、PIC32 还是 NXP 的 MCU,只要有 SPI 就能驱动。第二,供电 3.3V,电流功耗很低,工业产品电源设计不用额外做特殊处理。第三,工作温度范围 -40℃ 到 +125℃,这是真正的工业级/汽车级温度规格,放在电机控制柜、户外采集终端里都放心。
比较关键的是它的存储特性:MR25H40CDF 内部没有扇区、没有页的概念,你往任意地址写任意长度数据,都直接覆盖旧值,不需要执行任何擦除操作。写入周期大概是几十纳秒级别,完成时间基本由 SPI 时钟决定,不像 Flash 写完还要等 tPP 时间。这种特性让代码逻辑变得非常简单:先发 WRITE 命令,再发 3 字节地址,然后连续送数据,CS 拉高,完事。中途掉电也不会像 Flash 那样出现“半个页擦除了一半”这种奇怪状态。
另外一个很多人容易忽略的细节是,MR25H40CDF 这种器件的单字节读和单字节写都非常快,而且随机访问和顺序访问速度一致。对于工业设备中那些“隔一会儿改一个参数”的场景,它其实比 SRAM+电池备份方案更干净——不用电池,也不用担心电池漏液。
1.3 为什么主控选 PIC32MZ1024EFK144
既然要搭一套完整方案,必然要选主控。PIC32MZ1024EFK144 是 Microchip 的 MIPS M5150 内核 MCU,主频 200MHz,带 1MB Flash 和 512KB SRAM,144 引脚封装。这个名字里的每一位都有信息量:1024 表示 1MB Flash,EF 表示带硬件加密引擎的 MZ 系列,K 表示 512KB SRAM,144 就是引脚数。
选它不是因为参数最华丽,而是因为工业场景里它够用且皮实。这个系列提供 6 个 SPI、6 个 UART、4 个 I2C、2 个 CAN,还有 DMA、USB、以太网 MAC。做运动控制、仪器仪表、工业网关都合适,外围基本不用再加桥接芯片。尤其是 DMA + SPI 的组合,在读取大块 MRAM 数据时可以把 CPU 解放出来,这个后面我会细说。
更关键的是引脚重映射(PPS)。PIC32MZ 的大部分外设引脚都可以映射到任意支持 PPS 的 IO 上,PCB 布线自由度很高,不用像某些 MCU 那样被硬编码引脚束缚。MR25H40CDF 与之对接,CS 可以挂在任意 GPIO,SCK、SI、SO 也可以灵活安排,Emc 和走线优化余地很大。
软件开发环境是 MPLAB X IDE,配合 MCC 工具或者 Harmony v3。MCC 里勾选 SPI、配置引脚、生成代码,基本能省掉一半重复劳动。如果你是第一次用 Microchip 平台,建议直接用 MCC 起步,别一开始就手写寄存器,容易踩时钟配置的坑。
2. 硬件设计与连接:从原理图到 PCB
2.1 引脚分配与 SPI 接线全览
MR25H40CDF 是标准 SOP-8 封装,引脚不算多,但每一根都不能接错。我把引脚定义和连接方式整理成一张表,照着连基本不会出问题:
| MR25H40CDF 引脚 | 名称 | 方向 | 接到 PIC32MZ1024EFK144 |
|---|---|---|---|
| 1 | CS# | 输入 | 任意 GPIO,建议接 10kΩ 上拉到 VDD |
| 2 | SO | 输出 | SPIn SDI(数据输入,MISO) |
| 3 | WP# | 输入 | 直接接 VDD,或者 GPIO 控制 |
| 4 | VSS | 电源 | 地 |
| 5 | SI | 输入 | SPIn SDO(数据输出,MOSI) |
| 6 | SCK | 输入 | SPIn SCK |
| 7 | HOLD# | 输入 | 直接接 VDD,禁止悬空 |
| 8 | VDD | 电源 | 3.3V,就近去耦 |
很多新手第一次画图会把 WP# 和 HOLD# 悬空,这是最常见的隐性坑。WP# 是写保护引脚,低电平有效,悬空时内部虽然一般有弱上拉,但现场环境有干扰就容易偶发写保护,导致写命令发过去但数据没进去。HOLD# 是保持引脚,低电平时 SPI 总线暂停,悬空遇到干扰会引起时钟和数据错位。所以我的做法是:WP# 直接接 VDD,HOLD# 也直接接 VDD,彻底不给他们“捣乱”的机会。
PIC32MZ1024EFK144 的 SPI 引脚可以通过 PPS 重映射,所以这里不写死具体引脚编号。实际项目中我把 SCK、SDO、SDI 分别映射到一组相邻的 RPB 引脚,CS 单独用一个 GPIO 控制。这样走线非常短,信号完整性问题少。
2.2 电源、去耦与上电时序
MR25H40CDF 是 3.3V 器件,VDD 和 VSS 之间要就近放去耦电容。我的习惯是 0.1μF 陶瓷电容紧贴电源引脚,再并联一个 2.2μF 的陶瓷电容用来应对突发电流。很多老工程师还会加一个 10μF 钽电容在远端,电源会稳很多。
上电时序比电容更重要。MRAM 是标准的 CMOS 存储器件,如果 VDD 还没有稳定,MCU 就已经开始访问它,有概率读到随机值,甚至因为 GPIO 状态不稳定产生伪写操作。PIC32MZ1024EFK144 有 BOR(欠压复位)功能,可以在 VDD 低于阈值时保持复位状态,这个功能一定要开启,不要图省事关掉。另外一个关键点是 CS# 在复位期间必须为高。原因很简单:MRAM 在 CS 拉低时开始采样指令字节,如果 MCU 复位瞬间 GPIO 输出一个低电平毛刺,MRAM 可能把某个随机字节当成 WRITE 命令来执行,导致存储内容被改坏。
解决这个问题有两种方式:一种是在 CS# 上加 10kΩ 上拉到 VDD,MCU 复位期间引脚呈高阻,上拉电阻会把 CS# 拉高,采样不了任何指令。另一种是使用带复位输出的电源监控芯片,比如 MCP101,在电源稳定后再让 MCU 开始运行,同时保证 CS# 释放。我建议两者都做。
2.3 用硬件 SPI 还是 GPIO 模拟
这个问题我几乎每次都会被问。答案是:正式产品里优先用硬件 SPI,原因有三。
首先是速度。PIC32MZ1024EFK144 的硬件 SPI 可以轻松跑到 20MHz 甚至更高,GPIO 模拟在中断频繁的系统里能跑到 1MHz 就不错了。MRAM 读写本来就快,如果被软件模拟限速,等于买法拉利开在市区的早高峰。其次是时序稳定性。GPIO 模拟时,任何中断关闭不及时都会把时钟波形拉出毛刺,毛刺到特定长度就会被对端当成一个额外时钟,造成数据错位。硬件 SPI 由外设自动产生时钟,只要寄存器配置没问题,时序就是稳定的。第三是 DMA。硬件 SPI 可以和 PIC32MZ1024EFK144 的 DMA 联动,批量读取 MRAM 数据时能省下大量 CPU 时间。
GPIO 模拟不是完全没用。如果只是临时验证一下 MRAM 的好坏,手头又没有 SPI 调试工具,软件模拟可以用最低的成本确认片子是不是好的。我在实验室里就用 GPIO 模拟驱动过 MR25H40CDF,代码很短,能读能写。但一旦进入产品设计阶段,硬件 SPI 是必然选择。
3. 软件驱动:从初始化到读写函数
3.1 先搭好 SPI 传输层
写 MRAM 驱动之前,先保证 SPI 底层能收发字节。我建议用 MCC 快速生成初始化代码,配置项按下面选:SPI 主模式、8 位数据、SPI Mode 0、时钟预分频根据实际 MCU 频率设置到 20MHz 左右。这里特别提醒,Mode 0 很关键。MR25H40CDF 数据手册明确要求 CPOL=0、CPHA=0,如果配成 Mode 1 或者 Mode 2,读回来的数据会整体错位,而且这种现象非常隐蔽,单个字节偶尔看不出来,连续读一段数据才发现全是乱码。
以 MCC 生成的 SPI1 为例,传输一个字节的代码是:
uint8_t spi1_transfer_byte(uint8_t tx) { // 等待发送缓冲为空 while (SPI1_IsTxReady() == false); SPI1_WriteByte(tx); // 等待接收缓冲有数据 while (SPI1_IsRxReady() == false); return SPI1_ReadByte(); }如果你的代码是自己写寄存器实现的,那底层函数形式类似,只要保证每次调用都是完整的“发一字节、收一字节”即可。MRAM 是全双工 SPI 设备,主机发指令字节的同时,可以从 SI 读到上一个周期的 SO 数据,所以读数据时主机通常要发 0x00 来产生时钟,这是标准 SPI 驱动的习惯,别忘。
在 RTOS 环境下,SPI 总线需要加锁。PIC32MZ1024EFK144 的 SPI 外设同时只能被一个任务使用,如果任务 A 正在读 MRAM 的 512 字节,任务 B 此时发一条写命令,总线上的数据就会乱掉。简单的做法是在驱动外面套一个互斥信号量。
3.2 MR25H40CDF 指令系统与状态寄存器
MR25H40CDF 的指令集非常精简,核心就 6 条。先看这张指令表:
| 指令 | 字节编码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 设置写使能锁存 |
| WRDI | 0x04 | 清除写使能锁存 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 从指定地址读数据 |
| WRITE | 0x02 | 向指定地址写数据 |
这里我们需要理解一个概念:写使能锁存(WEL)。MR25H40CDF 在每次上电或 WRDI 之后,WEL 是 0,此时发 WRITE 指令,芯片不会真正写入数据。必须先发 WREN,把 WEL 置 1,然后才能写。这一点和大多数串行 Flash 一样。所以驱动里每次写入前,都要执行一次 WREN,不要偷懒省掉。
状态寄存器通过 RDSR 读取,bit0 就是 WEL。如果你想严谨一点,可以在 WREN 之后读一下状态寄存器确认 WEL 已经变成 1,再继续执行写入。虽然 MRAM 不比 Flash,写数度极快,基本没有 WIP 等待过程,但这种防御性编程习惯在工业环境里能省很多麻烦。
地址是 3 字节,最高字节在前。MR25H40CDF 实际容量 512KB,只需要 19 位地址,但指令格式固定是 24 位,高字节的前 5 位要填 0。很多人第一次从 16MB Flash 换到 MRAM,忘记改地址字节数,导致读写地址错位,这是一个很常见的低级错误。
3.3 三个关键驱动函数
下面给一套我实际在用的驱动代码,你直接抄也能跑。首先定义一些基础宏和底层访问接口:
#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_SIZE (512 * 1024) #define MRAM_MAX_ADDR (MRAM_SIZE - 1) // 底层接口:由硬件平台实现 extern uint8_t spi1_transfer_byte(uint8_t tx); extern void mram_cs_low(void); extern void mram_cs_high(void);写使能函数:
void mram_write_enable(void) { mram_cs_low(); spi1_transfer_byte(MRAM_CMD_WREN); mram_cs_high(); }读状态寄存器:
uint8_t mram_read_status(void) { uint8_t status; mram_cs_low(); spi1_transfer_byte(MRAM_CMD_RDSR); status = spi1_transfer_byte(0x00); mram_cs_high(); return status; }写数据的核心函数:
int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; if (addr > MRAM_MAX_ADDR) return -1; if ((addr + len) > MRAM_SIZE) return -1; // 写使能 mram_write_enable(); if ((mram_read_status() & 0x01) == 0) return -2; mram_cs_low(); spi1_transfer_byte(MRAM_CMD_WRITE); spi1_transfer_byte((addr >> 16) & 0xFF); spi1_transfer_byte((addr >> 8) & 0xFF); spi1_transfer_byte(addr & 0xFF); for (i = 0; i < len; i++) { spi1_transfer_byte(buf[i]); } mram_cs_high(); return 0; }读数据的核心函数:
int mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; if (addr > MRAM_MAX_ADDR) return -1; if ((addr + len) > MRAM_SIZE) return -1; mram_cs_low(); spi1_transfer_byte(MRAM_CMD_READ); spi1_transfer_byte((addr >> 16) & 0xFF); spi1_transfer_byte((addr >> 8) & 0xFF); spi1_transfer_byte(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = spi1_transfer_byte(0x00); } mram_cs_high(); return 0; }有几个细节值得说一下。第一,WRITE 和 READ 指令在 CS 拉低期间可以连续发送数据,不需要每写一个字节就拉高 CS,这是 MRAM 和很多页面写 Flash 不一样的地方。第二,写入函数里我做了一个容量边界检查,虽然看起来多余,但在工业程序里能挡住不少野指针调用。第三,写使能之后如果发现 WEL 没置位,我直接返回 -2,这个时候再往下发数据没有意义,不如尽早暴露问题。
3.4 掉电保存与并发访问的软件层面注意
很多工程师以为 MRAM 没有擦写寿命问题,掉电保存就万事大吉了。其实掉电瞬间写数据这件事,还需要整个系统的配合。MR25H40CDF 写入速度确实快,一个 64 字节的记录在 20MHz SPI 下只需要不到 30μs,但前提是 MCU 能在这 30μs 里保持稳定供电和稳定运行。
工业设备上常用的做法是:用一个大电容或者备份电源,在掉电检测触发后维持 MCU 运行几十毫秒,MCU 在掉电中断里把紧急数据写入 MRAM,然后执行复位。PIC32MZ1024EFK144 的电源监测模块可以配置一个低压中断,检测到 VDD 跌落到预设阈值时,立刻进入紧急保存流程。注意紧急保存流程里第一件事是把所有不必要的任务挂起,包括关闭中断源,避免写到一半被其他中断打断。
并发访问就简单了,把整个驱动函数视为临界资源,用互斥锁串行化。如果只有当前任务调用,那么一个简单的忙标志就够:
volatile uint32_t mram_busy = 0; int mram_write_guarded(...) { if (mram_busy) return -3; mram_busy = 1; int ret = mram_write(...); mram_busy = 0; return ret; }中断里不要调用这种带锁的函数,中断里只能置标志位,由后台任务去执行实际写入。我在实际项目中遇到过有人为了图快,在 ADC 中断里直接写 MRAM,结果 SPI 时序被更高优先级中断打断,数据偶尔错位,排查了一整天才锁定是这种并发问题。
4. 数据完整性设计:把可靠性做成系统能力
4.1 读回校验与 CRC
MRAM 本身是可靠的存储介质,但数据从 MCU 到 MRAM 之间还要经过 SPI 总线、PCB 走线、驱动代码,任何一个环节的电磁干扰都可能让某个字节变错。我的原则是:凡是需要长期保存的关键数据,写完必须读回校验,并且在数据块尾部附加 CRC 校验码。
在 Flash 时代,工程师往往不做读回校验,因为擦写一次太贵了,写完再读等于时间翻倍。MRAM 完全没有这个负担,写入快、读也快,读回校验的成本几乎可以忽略。这个“几乎不用额外成本”的特性,让它特别适合做高可靠性设计。
简单实用的 CRC16 实现如下,如果你有更快的查表版,可以直接替换:
uint16_t crc16(uint16_t crc, const uint8_t *data, uint32_t len) { uint32_t i; for (i = 0; i < len; i++) { crc ^= (uint16_t)data[i] << 8; for (int bit = 0; bit < 8; bit++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } return crc; }写入时先组织一个结构体,把 CRC 放在固定位置,然后整体写入一个逻辑块。读回来先算 CRC,不对就认为数据无效,启用备份数据。
4.2 双 Bank 备份与启动恢复
如果数据重要到丢一个字节都不行,那就不要只存一份。双 Bank 是我在工业参数存储里最常用的策略:把 MRAM 分成两个逻辑区域,比如 Bank A 和 Bank B,每次写入都带递增的序列号和版本号。上电时同时读两个 Bank,选取序列号更新且 CRC 正确的那个作为有效数据。
对于 MR25H40CDF 来说,双 Bank 的实现比 Flash 简单太多,因为不需要处理擦除对齐的问题。地址 0x00000 到 0x3FFFF 是 Bank A,0x40000 到 0x7FFFF 是 Bank B。每次写数据之前,先读两个 Bank 的头部信息,比较序列号,然后写序列号较大的下一号到另一侧 Bank。
启动恢复的伪逻辑:
typedef struct { uint32_t seq; uint16_t crc; uint16_t data_len; uint8_t data[64]; } Record; int load_valid_record(Record *result) { Record ra, rb; mram_read(ADDR_BANK_A, (uint8_t *)&ra, sizeof(ra)); mram_read(ADDR_BANK_B, (uint8_t *)&rb, sizeof(rb)); uint8_t valid_a = (crc16(0, (uint8_t *)&ra.data, ra.data_len) == ra.crc); uint8_t valid_b = (crc16(0, (uint8_t *)&rb.data, rb.data_len) == rb.crc); if (valid_a && valid_b) { return (ra.seq >= rb.seq) ? memcpy(result, &ra, sizeof(ra)) : memcpy(result, &rb, sizeof(rb)); } else if (valid_a) { memcpy(result, &ra, sizeof(ra)); return 0; } else if (valid_b) { memcpy(result, &rb, sizeof(rb)); return 0; } return -1; }这个方案能扛住“写入过程中掉电”的最坏情况。比如正在写 Bank B,写到一半断电了,Bank B 的 CRC 会不对,但 Bank A 里上一条记录依然完好。上电恢复时直接选 Bank A,等下次正常写入再覆盖坏的 Bank B。因为 MRAM 不需要先擦除,恢复过程没有任何延迟。
4.3 掉电检测与复位保护
我前面提到了硬件上的 CS# 上拉和复位 IC,软件侧还需要把这套机制用起来。
第一,开启 PIC32MZ1024EFK144 的 BOR。在 MPLAB Harmony 或 MCC 的配置里,把欠压复位使能,并设置合适的阈值。这样电源跌落时,MCU 会进入确定性的复位流程,而不是跑到莫名奇妙的地址执行乱码指令。
第二,配置低压检测中断。很多工业项目里,我习惯在外部电源入口加一个电压检测网络,MCU 检测到 3.3V 有下降趋势时触发紧急保存。注意这个检测要比 BOR 复位更早触发,否则 MCU 刚进入中断就被复位了,什么也来不及做。实现上可以用比较器或者 ADC 轮询,轮询周期要短,建议在 RTOS 里用高优先级任务做。
第三,复位之后的启动代码里,第一件事是把 CS# 对应的 GPIO 配置为输出高。别让它在上电初始化阶段处于随机状态。因为 MRAM 在复位期间靠上拉电阻保持 CS# 为高,但 GPIO 一旦被配置为输出低,下拉效应可能会盖过上拉电阻,产生一次伪写。所以驱动初始化顺序要做到:先置 CS 高,再配置 SPI 外设。
4.4 磨损均衡、日志策略与“多此一举”的复杂度
既然 MRAM 没有磨损问题,是不是就不需要磨损均衡了?是的。但我遇到过有人习惯性把 Flash 那套“动态磨损均衡算法”搬到 MRAM 上,结果白白增加了几百行代码和不少运行时间。你要想清楚,磨损均衡是为了把擦写次数分散开,MRAM 根本不存在擦写寿命限制,这个前提都不成立,算法自然没必要。
不过日志型环形缓冲还是要的,因为虽然不怕磨损,但“覆盖旧数据”和“标记当前写入位置”的问题依然存在。一个典型的工业事件日志可以这样设计:把 MRAM 划分成固定长度的记录槽,比如每条记录 64 字节,槽总数 8192 个,正好覆盖 512KB。维护一个当前写指针,指向下一个要写入的槽。上电时扫描槽头部的 magic 和序列号,就能定位到最新记录的下一个位置。用 MRAM 实现这个逻辑最大的好处是:覆盖老记录是直接写,不用先擦除,速度飞快,代码也短。
如果哪天槽位写满,直接回到起始地址继续覆盖最老的记录即可,这是标准的循环队列。加上双 Bank 和 CRC 校验之后,这个方案在工业设备上已经足够可靠。
5. 工业环境实测与问题排查
5.1 测试环境与方案
纸上谈兵没有意义,我把我实际做过的硬件和测试环境说清楚。主控板是 PIC32MZ1024EFK144,系统主频 200MHz,SPI 时钟 20MHz,MRAM 挂载在 SPI1 上,CS 用普通 GPIO。固件在 MPLAB X 环境下开发,驱动代码就是前面贴的那一套。为了测试,我搭了一个简单的上位机脚本,通过 UART 下发测试指令,MCU 执行 MRAM 读写并把结果回传。
测试分三类:连续写入循环、随机掉电、高低温。连续写入循环考验长时间工作下的稳定性;随机掉电考验掉电时刻正好撞上写入时刻的边界情况;高低温测试考察存储器和 MCU 在整个温度范围内的行为是否一致。三类测试里最能暴露问题的是第二类,真正能提升可靠性的也是第二类。
5.2 高频使用下的实测效果
我跑了一组比较有代表性的数据:对同一地址的 256 字节数据块,执行连续 600 万次写入,每次写完立即读回,逐字节比较,结果全部一致。这个写入量如果换成普通 SPI Flash,早就超出寿命了,但 MRAM 状态寄存器和数据读回完全正常。实测过程中我还故意在写入过程中翻转 CS 信号制造毛刺,发现 CRC 校验层可以准确识别并丢弃无效数据,系统最终恢复到上一份有效记录。
除了纯写入速率,我也测了读写混合场景。MR25H40CDF 在 20MHz SPI 时钟下,单条 READ 命令读 512 字节大约需要 210μs 左右,这个数字对绝大多数工业应用来说非常充裕。如果要追求更高吞吐,可以把 SPI 时钟提高到 40MHz,并且用 DMA 双缓冲,CPU 占用可以压得很低。PIC32MZ1024EFK144 的 DMA 在读取这种连续地址数据时非常顺手,值得花时间调一下。
5.3 掉电与高低温场景的实测记录
掉电测试我用了一个继电器板,每隔随机时间(200ms 到 3s)切断整个系统供电,同时在切断之前通过串口命令让 MCU 立刻写一条记录。跑了一周,总共执行了约 2000 次随机掉电测试,双 Bank 恢复机制每次都成功选到了有效记录。最坏情况是 Bank B 写到一半掉电,CRC 不正确,系统自动切到 Bank A,等下一次上电正常后才覆盖 Bank B。这个过程全程没有出现“两个 Bank 都损坏”的情况。
高低温测试是在工业温箱里做的,温度循环范围 -40℃ 到 +85℃,升降温速率 1℃/min,在每个极值温度下保温 45 分钟,并持续执行 MRAM 读写。三组循环跑下来,读回数据全部正确。需要注意的是,测试时我先让板子在每个环境温度下稳定 15 分钟再开始读写信,避免温箱里局部温度不均导致偶发读写失败。
整体结果汇总如下:
| 测试项目 | 测试条件 | 结果 |
|---|---|---|
| 连续写入循环 | 256 字节块,600 万次,读回校验 | 全部通过 |
| 随机掉电 | 2000 次随机断电,写记录同时断电 | 双 Bank 恢复全部成功 |
| 高低温 | -40℃~+85℃ 循环,三组 | 读写全部正确 |
| SPI 时钟极限 | 40MHz,512 字节批量读 | 数据正确,余量稳定 |
5.4 常见问题速查表
做技术支持这些年,我见过大量 MRAM 相关的现场问题,很多现象看起来一模一样,原因却完全不同。这张表是我实际排查经验的浓缩:
| 现象 | 常见原因 | 处理办法 |
|---|---|---|
| 读回所有字节都是 0xFF | CS# 没拉低、SPI 模式错误、WP# 悬空 | 先查硬件接线,再把 SPI 配成 Mode 0,WP# 接 VDD |
| 写入后立即读回旧数据 | 没有先发 WREN,WEL 未置位 | 每次写入前强制执行 WREN,并回检状态寄存器 |
| 数据整体错位,每字节都差一位 | SPI Mode 配置错误,或时钟极性不一致 | 对照数据手册确认 CPOL/CPHA,用逻辑分析仪看波形 |
| 偶发数据错乱,且一周只出现一两次 | SPI 走线太长或受干扰,复位期间伪写 | 缩短走线,CS# 上拉,加复位 IC,检查接地走线 |
| 高低温测试时偶发写失败 | VDD 跌落,去耦不足 | 在 MRAM 引脚旁加 0.1μF + 2.2μF,检查电源带载能力 |
| 程序里多个中断同时调用驱动后出错 | SPI 总线无锁,并发访问 | 用互斥锁或忙标志串行化驱动调用 |
| 更换 MCU 或修改 PPS 映射后不工作 | 引脚映射改了但代码里没同步 | 检查 PPS 寄存器配置,确认 SCK/SDO/SDI 映射正确 |
这里特别强调 0xFF 这个现象。所有字节读回 0xFF,绝大多数情况下不是 MR2S40CDF 坏了,而是 CS# 没正常拉低。如果你用 GPIO 模拟 CS,检查 GPIO 方向配置是不是错的;如果你用硬件 SS 引脚,检查 PIC32MZ 的自动 CS 逻辑是不是被外设接管了。我见过有人被这个问题卡了三天,最后发现是 PPS 把 SCK 映射到了一个不存在的引脚上,逻辑分析仪都抓不到时钟。
5.5 三个容易被忽略的细节
第一个细节是 WP# 和 HOLD# 的上拉处理。前面提过,这里再说一次:这两个脚不允许悬空。我在现场遇到过 HOLD# 引脚因为 PCB 走线太长,本身变成了一个天线,在强电磁干扰下触发了 hold 功能,SPI 主设备发的数据全部无效。后来把 HOLD# 直接接 VDD,问题消失。
第二个细节是读写数据前,CS 拉低到第一个字节发出的间隔。MRAM 对时序要求虽然宽松,但建议这个间隔稳定,不要一边查中断标志一边发数据。很多逻辑分析仪看起来很正常的波形,其实 CS 下降沿和 SCK 第一个边沿之间的延迟一直在抖动,偶发条件下会导致采样位置偏离,表现为“大多数时候正常,偶尔错一字节”。解决办法是把整个命令-地址-数据的过程全部放在关中断或临界区里执行,尽量减少抖动。
第三个细节是不要用调试器的复位按钮去测试掉电保存。调试器复位只是把 MCU 复位,MRAM 的电源还保持着,依赖掉电检测的紧急保存流程不会被触发,数据和正常启动时的表现完全不同。真正的掉电测试一定要用继电器或者可控硅切断整个板子的电源。
6. 后续扩展思路
6.1 日志型环形缓冲的落地
如果你在做一个需要保存大量事件记录的设备,可以把前面提到的环形缓冲区真正写出来。基本流程是:定义记录槽大小和总槽数,上电时扫描 0 号记录头往后的 magic 值,找到最新记录的位置。每次写入先读一次当前写指针的地址,把记录数据写到该槽,更新写指针到下一槽。整个过程不用擦除,不用考虑扇区对齐,和写 SRAM 几乎一样,但数据掉电不丢。
因为 MRAM 的随机写性能很好,你甚至可以做到“每一毫秒都写一条时间戳”,这在普通的 Flash 方案里是不可想象的。如果有 2000 条记录的现场,通过 UART 导出数据也很快,512KB 容量足够保存大规模事件。
6.2 把 MRAM 挂到文件系统上
有些应用希望用文件系统管理存储,比如把运行参数存成配置文件。FATFS 这类开源文件系统底层的 disk_write 接口可以直接对接 MRAM 驱动,因为 MRAM 支持任意地址随机写,不需要实现 Flash 的擦除和坏块管理,接口实现比 Flash 情况下简单得多,也快得多。
但我的建议是:文件系统不是万能的,如果只是存固定长度的关键参数,别上文件系统。文件系统带来的目录管理、碎片整理、掉电一致性这些复杂度,在工业现场反而是隐患。自定义 NVS(非易失存储)结构往往更可靠,这也是为什么很多工业设备至今还在用固定偏移地址存参数。
6.3 DMA 与系统集成
如果你的应用需要频繁且大量地读取 MRAM 数据,比如从设备上电时把 512KB 全部搬到 RAM 里做初始化,那一定要用 DMA。PIC32MZ1024EFK144 的 SPI + DMA 可以做到:CPU 配置好 DMA 描述符,启动传输,然后完全不过问数据搬运过程,传输完成触发中断。这样 CPU 可以继续处理通信和显示,不会在读数据上浪费几毫秒。
实际使用时建议把 DMA 通道优先级调到中等,避免和 UART 中断互相挤占。SPI 缓冲、DMA 缓冲、目标 RAM 地址都要按 4 字节对齐,否则某些 MZ 系列 DMA 配置会出错。这些细节在 Harmony 的 DMA 编辑器里都有说明,耐心看一遍能省不少调试时间。
把 MR25H40CDF 和 PIC32MZ1024EFK144 这套组合用于工业数据存储,我最大的体会是:MRAM 把一个原本充满妥协的存储方案变得很“干净”。没有擦除时间、没有寿命焦虑、没有页对齐问题,代码逻辑和可靠性设计都简化了一大截。当然它不是万能的,容量相比 Flash 不占优势,成本也更高,在需要大容量顺序记录日志的场景里依然要选 Flash。但凡是“频繁改写、掉电保存、工业环境”这三个关键词同时出现的需求,这套组合是我目前最愿意推荐给团队使用的方案。最后分享一个小经验:所有写入流程,把读回校验当作强制步骤,不要因为 MRAM 写入快就省略它。那几微秒的校验时间几乎不占系统资源,但它能帮你挡住绝大多数隐性问题和现场偶发故障,这是我在多次现场排查里付出代价换来的结论。