☰
MRAM替代Flash:PIC32上的高频写入存储方案全解析
2026/10/4 8:40:21 网站建设 项目流程

在嵌入式项目里做非易失性数据存储,很多人第一反应就是用 SPI Flash 或者 I2C EEPROM。但我最近完成的这个项目,情况比较特殊:设备装在工业产线旁边,电机启停频繁,电网波动家常便饭,主控偶尔会直接掉电,而且运行期间需要把工艺参数、故障码和能耗数据实时写进存储。光算下来每分钟几十次写入,常规 Flash 的擦写上限撑不了几年。最后我选了 Everspin 的 MR25H40CDF(4Mbit SPI 接口 MRAM)配合 Microchip 的 PIC32MX695F512L 来做这套存储和读取方案。整个方案跑下来,效果比预想的稳。这篇把我从选型、硬件接线、底层驱动到上层数据布局的完整过程拆出来,把这颗 MRAM 怎么接到 PIC32 上、代码怎么写、有哪些坑,一次说透。适合正在选型嵌入式存储方案,或者想用 MRAM 替代 Flash/EEPROM 的朋友。

1. 方案设计的核心思路:为什么 MRAM 更适合工业高频写入场景

1.1 工业现场对存储介质的要求并不温柔

先理解工业现场的真实环境。设备旁边有变频器、伺服电机、接触器,一启动就是几十安培的电流冲击,地线上经常带着干扰。主控板随时可能因为电源波动复位,甚至直接断电。在这种场合,存储介质要面对三个硬指标。

第一是掉电安全。数据写了一半断电是最常见的故障场景,Flash 正在做块擦除时掉电,有可能把整个扇区搞坏;EEPROM 虽然字节可写,但写入时也有一个内部编程时间窗口,断电可能导致数据不完整。第二是写入频率。产线设备需要记录工艺参数、计数器、故障时间戳,部分数据可能每秒甚至每毫秒都在变。普通 NOR Flash 的擦写寿命通常只有 10 万次,如果每秒写一次,一年不到就磨损了;即便有磨损均衡算法支撑,也扛不住长时间高频写入。第三是温度范围。控制柜里夏天温度能到七八十度,存储芯片必须支持工业级温度,在高温下保持数据可靠性。

用个生活化类比:Flash 就像一支圆珠笔,同一个位置写多了会磨损,写错了还得用涂改液先清掉再写;MRAM 则像一块白板配了可重复利用的磁粒,写上去马上显字,撕掉重写也不会坏,还不怕停电。这种特性恰好命中上面三个痛点。

1.2 MR25H40CDF 的选型逻辑:非易失、速度快、寿命长

MR25H40CDF 是 Everspin 的 SPI 接口 MRAM(磁阻随机存取存储器),容量 4Mbit,组织方式为 512K x 8bit。MRAM 和传统存储器的本质区别在于存储原理:它用磁隧道结(MTJ)的磁阻状态来记录数据,而不是用电荷。这就带来了几个非常关键的特性。

典型参数比较如下:

参数MR25H40CDF (MRAM)SPI NOR FlashSPI EEPROM
写入前是否需要擦除不需要需要块擦除不需要
写入延迟无编程延迟页编写通常数 ms5~10ms 编程等待
写入耐久性10^13~10^14 次10^5 次左右10^6 次左右
字节随机写支持通常按页写支持
数据保存时间>20 年10~20 年100 年级别
工作电压2.7~3.6V常见 3.3V常见 3.3V/5V

这个芯片支持标准 SPI 模式 0 和模式 3,时钟最高可以到 50MHz 级别(具体看温度等级),读写对称,没有"擦除优先"这种限制。512KB 的容量对存储参数、日志、采集数据来说也足够充裕,不需要外扩 SD 卡或者串行 Flash 那么复杂的文件管理。

选它还有一个原因:封装是标准的 8 脚 SOIC/TSSOP,硬件上替换非常方便。如果项目早期用 SPI EEPROM 测试过,换 MRAM 几乎不用改板子,只需要把驱动调整一下。

1.3 PIC32MX695F512L 在整个方案里扮演什么角色

PIC32MX695F512L 是 Microchip 的 32 位 MCU,MIPS32 M4K 核心,最高 80MHz,带 512KB 程序 Flash 和 128KB 数据 RAM。选择它不是因为一定要"大马拉小车",而是这个项目本身还需要跑通信协议、人机交互逻辑和算法控制,8 位 MCU 的内存和外设不够用。

在存储方案里,它的角色是 SPI 主机。PIC32MX695 上有多个 SPI 外设,可以在不影响其他通信接口的前提下,专门划出一路 SPI 给 MRAM。它自带的 128KB RAM 可以用来做缓存,把需要批量写入的数据攒到一定长度再一次性写入 MRAM,减少频繁触发 SPI 传输。

另外一个考虑是 Microchip 的生态。PIC32 有完整的寄存器级外设库和 Harmony 框架,驱动开发无论是裸机写还是跑 RTOS 都很方便。对于工业产品,元器件选型稳定、供货周期长、技术支持资料全,比一味追新更重要。

2. 硬件接线与电路设计:把芯片正确连到总线上

2.1 SPI 四线连接和引脚规划

MR25H40CDF 是标准 SPI 从设备,最少只需要四根线:SCK、MOSI、MISO、CS。再加上电源和地,一共 6 根线,剩下还有 WP# 和 HOLD# 两个控制引脚。

我用的连接关系如下:

MR25H40CDF 引脚功能接 PIC32MX695F512L备注
CS#片选RB4GPIO 输出,平时置高
SCKSPI 时钟SPI1 SCK模式 0 / 模式 3
SI主出从入SPI1 SDOMCU 输出到芯片
SO主入从出SPI1 SDI芯片输出到 MCU
WP#写保护直接接 3.3V不用时拉高
HOLD#暂停传输直接接 3.3V不用时拉高
VCC电源3.3V需就近去耦
VSS地GND—

这里特别提醒一句:具体引脚号要以所选封装的引脚复用表为准,不同封装和引脚配置会有差异,我上面这张表是我自己板子上的接法,不是所有 PIC32MX695F512L 都完全一样。

片选 CS 千万要用 GPIO 单独控制,不要直接接地。SPI 总线上可能还有别的从设备,而且每次操作时要先拉低 CS、完成整个读或写序列后再拉高,GPIO 控制最灵活。

2.2 供电、去耦和 WP#/HOLD# 的处理

MR25H40CDF 工作电压范围是 2.7V 到 3.6V,主控 PIC32 也是 3.3V 系统,直接共用一个 3.3V 电源轨就行。在芯片 VCC 引脚旁边放一个 100nF 陶瓷电容,再在板级放一个 10uF 钽电容或陶瓷电容做低频储能。这是常规做法,但很多项目出问题恰恰是因为去耦电容离芯片太远,导致 SPI 时钟高速翻转时电源抖动。

WP# 和 HOLD# 这两个引脚如果不处理,是非常容易踩的坑。WP# 是硬件写保护输入,低电平时芯片的块保护状态会生效;HOLD# 低电平时芯片暂停 SPI 通信并忽略时钟线上的变化。这两脚在 SOIC-8 封装里本身有内部上拉,但为了保险,我直接外部接了 10kΩ 上拉到 VCC。如果它们在调试时悬空,芯片可能随机进入写保护或暂停状态,表现就是数据写入失败或者 SPI 时序被打断。

对于工业现场的布线,SPI 走线一定要短,尽量控制在 5cm 以内,避免形成天线。如果距离较长,可以在 MOSI、SCK 线路上串联 22Ω 到 33Ω 的电阻,抑制信号振铃。我实测在 10MHz 时钟下,串 33Ω 后波形明显更干净,读取数据也稳定很多。

2.3 数据手册里最值得关注的几个时序点

很多人拿到芯片直接看示例代码,很少翻数据手册的时序参数。对于 MR25H40CDF,有几个参数在调试时必须心里有数。

首先是 SPI 模式。这颗芯片支持模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。在 PIC32 的 SPI 外设里,模式 0 对应 CKP=0、CKE=1。如果配置反了,读取回来全是 0xFF,因为主设备在错误的时钟边沿采样,从设备送出的数据根本没被捕捉到。

其次是 CS 的建立保持时间。片选拉低之后不能马上送时钟,要留一点时间让芯片准备;同样,写完最后一个字节、CS 拉高的瞬间,芯片才真正锁存写入数据。所以代码里 CS 拉高之后最好加一个小的延时,至少几十纳秒,保证芯片内部时序满足要求。

最后是写入无需等待。这跟 Flash 完全不同,MRAM 写完拉高 CS 就生效了,不需要轮询忙标志,也不需要等待 ms 级编程时间。代码写起来简单,但这也是双刃剑:一旦在写入过程中掉电,可能有部分指令序列没有真正锁存,需要靠上层数据校验机制来兜底。

3. 驱动实现:从空 SPI 到能用的存储接口

3.1 SPI 外设初始化:4 步让波特率和模式对齐

PIC32MX695F512L 的 SPI 外设配置主要涉及 SPI1CON、SPI1BRG 和 SPI1STAT 这几个寄存器。初始化分四步。

第一步,配置相关引脚的复用功能。PIC32 的外设引脚,要确保 SDI 被配置为数字输入,SCK 和 SDO 被配置为数字输出。同时 CS 引脚配置为 GPIO 输出并初始化为高电平。

第二步,清零 SPI1CON、配置为主模式。把 MSTEN 位置 1,选择 8 位数据,关闭 SPI 或先关再开。

第三步,设置波特率。PIC32 的 SPI 波特率计算公式是:Fsck = Fpb / (2 * (SPI1BRG + 1))。如果外设总线时钟 Fpb 是 80MHz,想让 SPI 时钟跑到 1MHz,SPI1BRG 就取 39。1MHz 是调试阶段的稳妥速度,远低于芯片上限,方便接逻辑分析仪排查问题。

第四步,设置 CKP 和 CKE。对于模式 0,CKP=0,CKE=1。这两位的组合很多人分不清,建议直接记住:CKE 位控制时钟边沿,在模式 0 下必须等于 1。

下面是我当时的初始化代码:

#define MRAM_CS_TRIS TRISBbits.TRISB4 #define MRAM_CS_LAT LATBbits.LATB4 void mram_gpio_init(void) { MRAM_CS_TRIS = 0; // RB4 输出 MRAM_CS_LAT = 1; // 片选默认高电平 } void spi1_init_for_mram(void) { SPI1CON = 0; SPI1BRG = 39; // 80MHz / (2 * (39 + 1)) = 1MHz SPI1STATbits.SPIROV = 0; // 清除溢出标志 SPI1CONbits.MSTEN = 1; // 主机模式 SPI1CONbits.CKP = 0; // 空闲时钟低电平 SPI1CONbits.CKE = 1; // 模式 0 的时钟边沿选择 SPI1CONbits.ON = 1; // 打开 SPI }

3.2 底层读写函数:WREN、READ、WRITE 的完整实现

MR25H40CDF 的 SPI 指令集和普通串行 Flash 类似,但少了擦除操作。它有几个核心指令:写使能 WREN(0x06)、写禁用 WRDI(0x04)、读状态寄存器 RDSR(0x05)、写状态寄存器 WRSR(0x01)、读数据 READ(0x03)、写数据 WRITE(0x02)。

写操作最关键的规则是:每次写操作前必须先发 WREN 指令,把内部写使能锁存器 WEL 置位。如果没有 WREN,芯片会直接忽略随后发来的 WRITE 指令。这是为了防误写,和 EEPROM 的写使能机制类似。

读数据则是纯 SPI 操作,发指令、发地址、再读数据。地址总共 19 位,对应 512KB 空间,在实际操作中都发成 3 字节,高字节补 0。

一个单字节读取的实现:

uint8_t mram_read_byte(uint32_t addr) { uint8_t val; MRAM_CS_LAT = 0; spi1_exchg(0x03); // READ spi1_exchg((addr >> 16) & 0xFF); spi1_exchg((addr >> 8) & 0xFF); spi1_exchg(addr & 0xFF); val = spi1_exchg(0x00); // 发送空数据,同时接收芯片回复 MRAM_CS_LAT = 1; return val; }

单字节写入实现:

int mram_write_byte(uint32_t addr, uint8_t val) { if (addr >= MRAM_SIZE) return -1; MRAM_CS_LAT = 0; spi1_exchg(0x06); // WREN 写使能 MRAM_CS_LAT = 1; MRAM_CS_LAT = 0; spi1_exchg(0x02); // WRITE spi1_exchg((addr >> 16) & 0xFF); spi1_exchg((addr >> 8) & 0xFF); spi1_exchg(addr & 0xFF); spi1_exchg(val); // 写入一个字节 MRAM_CS_LAT = 1; return 0; }

其中 spi1_exchg 是一个最基本的 SPI 收发函数:

uint8_t spi1_exchg(uint8_t byte) { while (SPI1STATbits.SPITBF); // 等待发送缓冲空 SPI1BUF = byte; while (!SPI1STATbits.SPIRBF); // 等待接收完成 return SPI1BUF; }

注意,多字节连续读写时,只要 CS 保持低电平,芯片地址会自动递增,可以连续发数据,不需要重新发指令。这个特性在批量读写日志时非常有用,可以减少 SPI 协议开销。

3.3 状态寄存器与写保护:防止误写不可少

虽然 MRAM 不怕写磨损,但正因为写起来太容易,误写风险更高。系统初始化时,我会先读一下状态寄存器,确认芯片状态。

状态寄存器的低字节和通用 SPI 存储芯片类似,有写使能锁存 WEL、块保护 BP0/BP1、写保护使能 WPEN 等位。如果块保护位被意外设置,芯片的一部分或全部地址会被硬件保护,写入时不会生效。

初始化时建议做一次主动清零保护位的操作:

void mram_reset_protection(void) { uint8_t status; MRAM_CS_LAT = 0; spi1_exchg(0x05); // RDSR status = spi1_exchg(0x00); MRAM_CS_LAT = 1; if (status & 0x0C) { // 存在块保护位 MRAM_CS_LAT = 0; spi1_exchg(0x06); // WREN MRAM_CS_LAT = 1; MRAM_CS_LAT = 0; spi1_exchg(0x01); // WRSR spi1_exchg(status & 0x03); // 只保留 WEL/WPEN 等非保护位 MRAM_CS_LAT = 1; } }

这段代码不能完全照搬,因为状态寄存器的位定义在数据手册里有明确说明,不同批次可能有差异。但思路是共通的:上电先确认保护状态,把不需要的块保护清除,保证后续读写操作畅通。

3.4 接入上层:封装一层简单的存储抽象

如果业务代码里到处直接调 mram_write_byte,到后面一定是一团乱麻。比较稳妥的做法是封装一层存储接口,对外提供几个函数:

int storage_init(void); int storage_write_param(uint16_t id, const uint8_t *buf, uint16_t len); int storage_read_param(uint16_t id, uint8_t *buf, uint16_t len); int storage_write_log(const uint8_t *record, uint16_t len);

这样底层是 MRAM 也好、Flash 也好,上层逻辑只跟一组 API 打交道。以后换存储介质,只需要替换驱动文件,业务代码不用动。

在封装层内部,还需要处理一个并发问题。如果有 FreeRTOS 或调度器,多个任务同时调用存储接口,必须加互斥锁,否则一次读写操作被任务切换打断,CS 时序就乱了。裸机环境下,如果主循环和中断都会访问存储,也要临时关闭中断来保证原子性。

4. 数据布局与可靠性设计:关键不是存什么,而是怎么存

4.1 512KB 地址空间怎么划分

MR25H40CDF 有 512KB 空间,如果把它当成一个大数组随便读写,那和用 Flash 没有区别,系统的可靠性仍然没保障。我更建议把地址空间按功能区隔开,每一块有明确的用途。

我当时划分的布局如下:

地址范围大小用途
0x00000 ~ 0x07FFF32KB参数区 A 副本
0x08000 ~ 0x0FFFF32KB参数区 B 副本
0x10000 ~ 0x17FFF32KB运行日志环形队列
0x18000 ~ 0x5FFFF约 288KB工艺数据/历史数据存储
0x7F000 ~ 0x7FFFF4KB状态标志与启动信息

参数区为什么要分成 A/B 两份?这是嵌入式里最常见的双备份思想。系统启动时先读 A 区,如果校验失败就回退到 B 区;正常写入时先写 A,再写 B,两边内容保持一致。某一边写入时掉电,另边还能兜底,系统不会因为参数区损坏直接变砖。

日志区用环形队列,固定大小 32KB,可以记录最近数千条故障或操作记录。环形队列的细节下一节展开。

4.2 校验、回退与掉电保护策略

MRAM 写入没有擦除等待,掉电时的主要风险是:CPU 把数据写到一半,比如已经写了 10 个字节,还没来得及写 CRC 和完成标志,电源就断了。这时存储区里残留的是半截新数据,如果不做处理,下次启动读到这半截数据就可能误以为是一段完整记录。

解决思路很简单:在每一条记录的末尾附加 CRC 和一个状态字,状态字是整个记录的最后写入内容。写记录时按顺序写数据、写 CRC、最后写状态字。判断记录是否有效时,先看状态字是否等于约定的魔数(比如 0xA5A5),再校验 CRC。因为状态字总是最后写,掉电时它大概率还是旧值或无效值,系统就能识别出这条记录是损坏的,选择丢弃或回退到上一个有效记录。

参数区也一样,A/B 双备份的每个区都存一个魔数头 + CRC。启动时按以下流程判断:

  • 先读 A 区头部,检查魔数和 CRC。若通过,则采用 A。
  • 若 A 区无效,读 B 区,检查魔数和 CRC。若通过,则采用 B 并修复 A。
  • 若 A、B 都无效,恢复出厂默认参数,并置启动错误标志。

对于日志环形队列,每条记录有头部长度字段和自增序号。读日志时从序号最大的有效记录往回读,可以直接过滤掉断电导致的半写记录。因为 MRAM 写入快,这个机制实现起来比 Flash 简单太多,Flash 还要考虑块擦除边界问题。

4.3 MRAM 的寿命到底能撑多久,手把手算一遍

很多人以为只有 Flash 才需要考虑寿命,MRAM 就完全不用管了。这其实是个误解。MRAM 也有写入耐久上限,只是这个上限极其高。MR25H40CDF 的写入耐久一般在 10^13 到 10^14 次量级,针对的是每一个单独存储单元的写入次数。

假设设备每秒钟往同一地址写一次数据,一年大约 3.15×10^7 次。取最保守的 10^13 次寿命,那么单个地址能坚持 317000 年以上。这已经完全超过设备本身的使用寿命,所以确实可以忽略磨损。

对比一下普通 SPI NOR Flash:如果按每 1 秒写一个 4KB 扇区,假设 Flash 扇区擦写寿命 10 万次,一天就是 86400 秒,不到两天就达到寿命上限,即便有磨损均衡把负载摊到多个扇区,也就撑几十天到几个月。这就是工业高频写入场景必须用 MRAM 或者铁电存储器 FRAM 的根本原因。

另外 MRAM 不需要做磨损均衡,这在代码层面也简化了很多。Flash 的磨损均衡要维护逻辑地址到物理地址的映射,要定期搬运数据,要写管理表,非常容易出 bug。MRAM 直接映射地址,读什么就是什么,逻辑瞬间清爽。

5. 调试实录:现场踩过的坑和排查方法

5.1 读出来全是 0xFF:SPI 模式没对上

这是我最先遇到的问题。驱动写完,上电初始化,读状态寄存器,返回全是 0xFF。最初以为是芯片没焊好,用示波器量了 MISO,发现根本没有有效数据输出。

后来用逻辑分析仪抓 SPI 波形,发现时钟极性完全反了。我的初始化代码里 CKP 和 CKE 配置错误,主设备在错误的边沿采样。MR25H40CDF 典型工作在模式 0,对应 CKP=0、CKE=1。修正之后,读状态寄存器立即返回正常值。

这个坑的共性是:几乎所有 SPI 芯片读回全 0xFF,第一反应都应该是 SPI 模式不一致。可以先用低速时钟、手动发一个 RDSR 指令,用逻辑分析仪抓波形,确认时钟空闲电平、数据采样边沿是否和芯片数据手册里的时序图匹配。

5.2 写入后立刻读回错误数据:WREN 没生效或者 CS 时序太紧张

我碰到过一次很诡异的现象:同一个地址连续写两次,第二次才能写对。第一次写进去,读出来还是 0xFF。

排查后确认有两个叠加原因。一个是 WP# 引脚悬空,导致芯片在某些时候进入硬件写保护状态,WREN 发了也没用。把 WP# 外部上拉之后,写保护的问题消失。另一个原因是 WREN 指令和 WRITE 指令之间的 CS 释放间隔太短,芯片内部写使能锁存器还没有可靠置位。解决办法是确保 WREN 之后 CS 拉高至少 1us,再拉低发起正式写操作。

在代码层面,所有写操作统一封装,永远先发 WREN,再发 WRITE,不放任何捷径。每次写完后,也可以读一下状态寄存器的 WEL 位确认,如果 WEL 为 0 而写入预期发生,大概率是保护位或者时序问题。

5.3 MISO 一直没信号:引脚复用和输入配置没做好

另一个常见问题是 MOSI 线上有正常波形,但 MISO 线一直没有信号,读到的数据始终是 0 或者随机值。

原因通常在 PIC32 的引脚配置。有些引脚默认是模拟输入,或者被其他外设模块占用。PIC32MX 系列的引脚复用规则比较固定,但并非所有引脚都能作为任意外设引脚使用。我在一个版本里把 SDI 接到了 B 口,但忘了把对应引脚的模拟输入功能关掉,结果 SDI 永远读到的是不正确的电平。

在初始化代码里,需要对 SPISCK、SPISDO、SPISDI 引脚做明确的数字 I/O 配置,尤其是 SDI 引脚,必须设为数字输入,同时检查数据手册里是否要求禁用模拟功能或者重映射。配置完引脚之后,最好先用万用表或示波器确认引脚电平正常,再跑驱动代码。

5.4 常见问题速查表

现象可能原因排查方法
读回全 0xFFSPI 模式不对检查 CKP/CKE,用模式 0 或模式 3 抓波形
写入后读回还是旧值WREN 未生效 / WP# 悬空 / CS 间隔太短先发 WREN,检查 WEL 位,WP# 外部上拉
MISO 无输出引脚复用或模拟功能未禁用核对引脚配置,确认 SDI 为数字输入
偶尔写入失败电源波动或 CS 时序紧张优化去耦电容,CS 拉高后加延时
高温下读写异常供电不稳定或走线过长增强滤波,缩短 SPI 走线,降低 SPI 时钟

还有一个我在量产测试时才发现的细节:SPI 时钟频率不要一味追求高。芯片虽然支持几十兆赫兹,但在工业现场,连接器、线缆、干扰都会限制实际可靠频率。最终产品我固定在 8MHz 运行,经过一整晚高温老化测试,读写全部通过。对于可靠性要求高的产品,留一点时序裕量是值得的。

6. 最后的心得与扩展建议

用 MRAM 替代 Flash 做高频存储,整体体验是让人放心的。 MCU 侧驱动写起来并不复杂,复杂的是上层数据布局和掉电保护机制。我个人的经验是,存储方案在硬件选型阶段就要想清楚数据模型,不要等代码写完了再往里塞 CRC 和双备份。

如果你准备在自己项目里复现这套方案,我有几个具体建议。第一,硬件上把 CS、SCK、MOSI、MISO 都引出测试点,方便出问题时挂逻辑分析仪。第二,上电初始化时把 SPI 时钟先设到 1MHz,等驱动调通以后再逐步提高,不要在第一天就挑战最高时钟。第三,MRAM 虽然寿命长,数据保护机制不能省,CRC 和状态字比什么都重要。

后续扩展方面,可以用同样的硬件在 MRAM 上挂一个小型文件系统,比如 littlefs 或者自研的分区表管理,方便记录变长数据和检索日志。也可以利用 MRAM 的字节可写特性做数据累加器,把过账数据和统计值持续更新到固定地址,这在很多仪表类产品里都非常实用。我现在就在把日志区改成带索引的结构,让上位机可以通过 RS485 直接读取历史记录。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询