1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 这颗料
做嵌入式这行十几年,存储方案的选择一直是个绕不开的坎。早些年做工业数据采集器,EEPROM 擦写寿命只有 100 万次,铁电存储器容量又上不去,NOR Flash 写之前还得先擦除整个扇区,掉电瞬间数据就丢了。后来接触到 Everspin 的 MR25H40CDF 这颗磁阻存储器,才算真正把"高频写入 + 掉电不丢 + 长寿命"这三个需求同时兜住。
MR25H40CDF 是一颗 4Mbit(512KB)的 SPI 接口 MRAM,采用 8 引脚 DFN 封装,工作电压 2.7V 到 3.6V,工业级温度范围覆盖 -40℃ 到 +85℃。它最核心的价值在于:写入不需要擦除、没有写延迟、擦写寿命理论上无限(官方标称 10^14 次以上),而且掉电后数据能保持 20 年以上。这几点放在工业现场那种"随时可能断电、数据必须保住"的环境里,比什么都重要。
我这次选的主控是 PIC32MZ2048EFH144,Microchip 家的 32 位 MIPS 核心 MCU,200MHz 主频,2MB Flash,512KB RAM,自带 6 个 SPI 模块。选它一方面是因为手头项目对运算能力有要求,另一方面是它的 SPI 外设配置灵活,支持 8/16/32 位传输模式,跟 MR25H40CDF 配合起来很顺手。
1.2 方案对比:MRAM 到底比 EEPROM 和 Flash 强在哪
很多人问我,既然都是 SPI 接口的存储芯片,为什么不用便宜的 25 系列 SPI Flash 或者 24 系列 EEPROM?我整理了一张对比表,把实际项目里最关心的几个维度拉出来看:
| 对比维度 | MR25H40CDF (MRAM) | 25系列 SPI Flash | 24系列 EEPROM |
|---|---|---|---|
| 写入前是否需要擦除 | 不需要 | 需要按扇区擦除 | 不需要 |
| 字节写入时间 | 无延迟,直接写 | 页编程约 0.7ms | 约 5ms |
| 擦写寿命 | 10^14 次以上 | 约 10 万次 | 约 100 万次 |
| 掉电数据保持 | 20 年以上 | 20 年 | 40 年 |
| 写入功耗 | 极低 | 较高(擦除时) | 中等 |
| 单位容量成本 | 高 | 低 | 中等 |
| 接口速率 | 最高 40MHz | 最高 104MHz | 最高 10MHz |
从表里能看出来,MRAM 的短板就是贵,同容量下价格可能是 SPI Flash 的十几倍甚至几十倍。但工业场景里,如果每秒要记录几十次传感器数据,而且设备可能随时被拉闸,那 Flash 的擦除延迟和寿命限制就是致命伤。我之前有个项目用 SPI Flash 做数据记录,现场跑了半年就出现扇区坏块,返修成本远超芯片差价。所以这个方案的核心逻辑就是:用成本换可靠性,用简单换稳定。
1.3 硬件连接的整体思路
PIC32MZ2048EFH144 和 MR25H40CDF 之间就是标准的 SPI 四线制连接:SCK、SDI、SDO、CS,再加上电源和地。我选用的是 SPI2 模块,映射到 RF4(SCK)、RF5(SDI)、RF3(SDO)、RB2(CS)。这里有个细节要注意:MR25H40CDF 的 CS 引脚必须由主控的 GPIO 或者 SPI 外设的硬件片选来控制,不能悬空,否则芯片内部状态机会乱跳。
电源部分我在 MR25H40CDF 的 VDD 引脚旁边放了 0.1uF 和 1uF 两颗电容,一颗滤高频,一颗滤低频。工业现场电磁环境复杂,电源纹波大了会导致 SPI 通信误码,这个钱不能省。另外 WP 引脚(写保护)我直接拉到 VDD,因为项目里不需要硬件写保护功能,软件层面控制就够了。
2. SPI 通信底层细节与寄存器操作
2.1 MR25H40CDF 的指令集拆解
MR25H40CDF 的指令集不复杂,但有几个地方容易踩坑。它支持标准 SPI 模式 0 和模式 3,我选的是模式 0(CPOL=0,CPHA=0),也就是时钟空闲低电平,数据在上升沿采样。这个跟 PIC32MZ 的 SPI 配置要对应上,不然读回来的数据全是 0xFF 或者 0x00。
核心指令我列一下:
- WREN (0x06):写使能,任何写操作之前必须先发这条指令
- WRDI (0x04):写禁止,写完可以发也可以不发,芯片上电默认是禁止写
- READ (0x03):读数据,后面跟 3 字节地址,然后连续输出数据
- WRITE (0x02):写数据,后面跟 3 字节地址,然后连续写入数据
- RDSR (0x05):读状态寄存器
- WRSR (0x01):写状态寄存器
这里有个关键点:MR25H40CDF 的写操作没有页边界限制。SPI Flash 写的时候不能跨页,一页 256 字节,跨页就得重新发指令。MRAM 没这个毛病,你可以从任意地址开始连续写,写到芯片末尾都没问题。这个特性在记录不定长数据的时候特别爽,不用去算页对齐。
2.2 PIC32MZ 的 SPI 模块配置要点
PIC32MZ 的 SPI 配置比 STM32 要繁琐一些,因为它的寄存器分得更细。我一般用 Harmony 框架来初始化,但底层寄存器也得心里有数。关键寄存器是 SPI2CON、SPI2BRG、SPI2STAT。
SPI2CON 里要设置:
- MODE32/16/8:我选 8 位模式,因为 MRAM 指令和地址都是按字节走的
- SMP:采样相位,模式 0 下设为 0(在数据输出中间采样)
- CKE:时钟边沿,模式 0 下设为 0(空闲低,上升沿变化)
- MSTEN:主模式使能
- SPIEN:SPI 使能
SPI2BRG 决定时钟频率。PIC32MZ 的 SPI 时钟源是 PBCLK2,我这边配置的是 100MHz。MR25H40CDF 最高支持 40MHz,所以 BRG 值不能小于 100/40 - 1 = 1.5,取整就是 2,实际时钟 100/(2+1) = 33.3MHz。这个速率在工业现场跑起来很稳,再高的话 PCB 走线不好就容易出错。
// SPI2 初始化代码片段(寄存器级) SPI2CON = 0; SPI2CONbits.MSTEN = 1; // 主模式 SPI2CONbits.CKE = 0; // 空闲低电平 SPI2CONbits.SMP = 0; // 中间采样 SPI2CONbits.MODE8 = 1; // 8位模式 SPI2BRG = 2; // 33.3MHz SPI2CONbits.ON = 1; // 使能SPI2.3 片选信号的处理技巧
SPI 通信里片选信号的处理最容易被忽视,但恰恰是出问题最多的地方。PIC32MZ 的 SPI 模块支持硬件片选,但我习惯用普通 GPIO 来控制 CS,因为这样时序更可控。
具体操作是:每次传输前把 CS 拉低,传输完再拉高。MR25H40CDF 要求 CS 下降沿到第一个时钟沿之间至少有 5ns 的建立时间,CS 上升沿之前最后一个时钟沿之后至少有 5ns 的保持时间。33MHz 下时钟周期是 30ns,所以只要代码里拉低 CS 之后不要立刻写 SPI 缓冲寄存器,中间隔几条指令就足够了。
注意:如果你用硬件片选,PIC32MZ 的 SPI 模块在传输结束后会自动拉高 CS,但有时候最后一个字节还没完全移出去就拉高了,导致数据截断。我实测下来,软件片选更稳妥。
3. 完整读写实操流程
3.1 单字节写入的完整时序
写一个字节到 MR25H40CDF 的指定地址,流程分三步:发 WREN、发 WRITE 指令加地址加数据、等待写入完成。虽然 MRAM 没有写延迟,但状态寄存器的 WEL 位需要时间更新,所以稳妥起见还是查一下状态。
void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LOW(); SPI2_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI2_ExchangeByte(0x02); // WRITE SPI2_ExchangeByte((addr >> 16) & 0xFF); SPI2_ExchangeByte((addr >> 8) & 0xFF); SPI2_ExchangeByte(addr & 0xFF); SPI2_ExchangeByte(data); MRAM_CS_HIGH(); // 等待写入完成(实际MRAM几乎无延迟,但保险起见) while(MRAM_ReadStatus() & 0x01); }这里 SPI2_ExchangeByte 函数就是往 SPI2BUF 写数据,然后等 SPI2RXBUF 有数据回来再读走。PIC32MZ 的 SPI 是全双工的,发一个字节的同时会收一个字节,写操作时收到的字节直接丢弃就行。
3.2 连续读取的优化写法
读操作比写操作简单,不需要 WREN。但连续读的时候有个效率问题:如果每次读一个字节都重新发指令和地址,那大部分时间都花在指令开销上了。MR25H40CDF 支持连续读模式,发一次 READ 指令和起始地址之后,可以一直读下去,地址会自动递增。
void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI2_ExchangeByte(0x03); // READ SPI2_ExchangeByte((addr >> 16) & 0xFF); SPI2_ExchangeByte((addr >> 8) & 0xFF); SPI2_ExchangeByte(addr & 0xFF); for(uint32_t i = 0; i < len; i++) { buf[i] = SPI2_ExchangeByte(0x00); } MRAM_CS_HIGH(); }这个写法在 33MHz 时钟下,读 512 字节大概只需要 130 微秒左右。我实测过,用逻辑分析仪抓波形,SCK 连续不断,数据一个接一个出来,效率很高。
3.3 数据记录场景的地址管理策略
工业数据记录通常是个环形缓冲区的结构:数据一直往后写,写到末尾就绕回开头覆盖最老的数据。MR25H40CDF 有 512KB 空间,我把它分成 512 个 1KB 的块,每块存一条记录。记录头里放时间戳和校验和,后面跟实际数据。
地址管理我用两个指针:写指针和读指针。写指针每次写完一条记录就往后移,读指针由上位机查询时更新。两个指针都存在 MRAM 的固定地址里,掉电也不会丢。这里有个细节:指针更新和记录写入最好放在同一个写操作序列里,避免掉电时指针和记录不一致。我的做法是先把记录写到新地址,再更新指针,这样即使掉电,最坏情况就是丢最后一条记录,不会破坏整个缓冲区结构。
4. 常见问题排查与避坑经验
4.1 读回来全是 0xFF 或者 0x00
这是最典型的问题,八成是 SPI 模式不对。MR25H40CDF 支持模式 0 和模式 3,如果你主控配的是模式 1 或模式 2,数据肯定错。检查方法很简单:用示波器看 SCK 空闲时是高还是低,再看数据在哪个边沿变化。模式 0 是空闲低、上升沿采样,模式 3 是空闲高、下降沿采样。
另一个可能是 CS 信号没接对。有些开发板把 CS 接到了 SPI 模块的硬件片选引脚上,但代码里又用 GPIO 去控制,结果两个信号打架。我建议统一用 GPIO 控制,把硬件片选功能关掉。
4.2 写入后读出来数据不对
先确认 WREN 指令有没有发。MR25H40CDF 上电默认是写禁止状态,不发 WREN 直接发 WRITE 指令,数据写不进去。另外 WREN 之后如果发了其他指令(比如读状态),WEL 位会自动清零,所以 WREN 和 WRITE 之间不能插入其他操作。
还有一种情况是地址算错了。MR25H40CDF 是 4Mbit,地址范围是 0x000000 到 0x07FFFF。如果你传了个 0x080000 进去,地址会回绕到 0x000000,数据就写到开头去了。这个在调试的时候很容易迷惑,明明写的是末尾,读出来却是开头的数据。
4.3 高速通信下的误码问题
33MHz 以上时钟频率时,如果 PCB 走线太长或者没有做阻抗匹配,SCK 和数据的边沿会出现振铃,导致采样错误。我一般会在 SCK 和数据线上串 22 欧姆到 33 欧姆的电阻,靠近主控端放。另外 SPI 走线尽量等长,不要跨分割地平面。
如果误码率还是高,可以先把时钟降到 10MHz 试试。如果降速后正常,那就是信号完整性问题,不是芯片或代码的问题。工业现场我一般不会跑超过 25MHz,留足余量比追求极限速度重要。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读回全 0xFF | SPI 模式错误 | 示波器看 SCK 空闲电平 | 改为模式 0 或模式 3 |
| 读回全 0x00 | MISO 没接或 CS 没拉低 | 万用表测 MISO 通断 | 检查焊接和 CS 控制 |
| 写入无效 | 没发 WREN | 读状态寄存器 WEL 位 | 写前先发 0x06 |
| 地址错乱 | 地址超出范围 | 核对地址计算 | 限制地址在 0x00000-0x7FFFF |
| 高速误码 | 信号完整性差 | 降速测试 | 串匹配电阻,降低时钟 |
| 掉电丢数据 | 写入未完成就断电 | 检查写时序 | 写后读回校验 |
5. 工业场景下的可靠性加固
5.1 数据校验与恢复机制
MRAM 虽然可靠,但工业现场该做的校验一样不能少。我在每条记录后面加 2 字节 CRC16 校验,读的时候先算 CRC 再比对。如果校验失败,就把这条记录标记为无效,跳过继续读下一条。这样即使某次写入过程中掉电导致数据不完整,也不会影响其他记录。
另外我在 MRAM 开头留了 1KB 做元数据区,存记录总数、写指针、读指针、以及一个"魔数"(比如 0x5A5A)。上电初始化时先读魔数,如果不对就说明 MRAM 是全新的或者数据格式变了,这时候就重新格式化整个存储区。这个机制在固件升级后特别有用,避免新旧数据格式混在一起。
5.2 电源监控与掉电保护
PIC32MZ2048EFH144 自带比较器,我把它配置成电源电压监控:当 VDD 降到 2.9V 以下时触发中断,在中断里立刻停止所有写操作,把关键指针存到 MRAM 里。因为 MRAM 写入不需要等待,这个中断处理可以在几微秒内完成,远快于电源电容的放电时间。
硬件上我在电源输入端放了一颗 470uF 的电解电容,保证掉电后 MCU 还能撑 10ms 以上。这个时间足够完成紧急数据保存。实测下来,从检测到掉电到写完关键数据,总共不到 1ms,余量很足。
5.3 温度漂移与长期稳定性
MR25H40CDF 的工业级温度范围是 -40℃ 到 +85℃,但实际在极端温度下,SPI 时序参数会有漂移。我在 -40℃ 低温箱里测试过,33MHz 时钟下通信正常,但 40MHz 时偶尔出错。所以如果产品要过宽温测试,建议把时钟降到 25MHz 以下。
长期稳定性方面,MRAM 的数据保持能力跟温度有关,温度越高保持时间越短。85℃ 下官方标称 10 年,25℃ 下是 20 年以上。如果设备长期工作在高温环境,建议每 5 年做一次数据刷新:读出来重新写一遍,相当于给数据"充电"。
6. 性能实测与优化建议
6.1 读写速度实测数据
我用逻辑分析仪抓了实际波形,统计了不同时钟频率下的读写速度:
| 时钟频率 | 单字节写入耗时 | 512字节连续读耗时 | 512字节连续写耗时 |
|---|---|---|---|
| 10MHz | 约 3.2us | 约 420us | 约 450us |
| 20MHz | 约 1.8us | 约 220us | 约 240us |
| 33MHz | 约 1.2us | 约 135us | 约 150us |
从数据看,33MHz 下写 512 字节只要 150 微秒,这个速度做高频数据记录完全够用。就算每秒记录 1000 次,每次 512 字节,总写入时间也才 150 毫秒,CPU 占用率很低。
6.2 DMA 传输的可行性分析
PIC32MZ 的 SPI 模块支持 DMA 触发,理论上可以用 DMA 来搬数据,进一步降低 CPU 占用。但我实际试下来,对于 512 字节以下的数据量,DMA 的配置开销比直接轮询还大。DMA 适合那种一次传输几 KB 以上的场景,比如把整个 MRAM 内容 dump 出来做备份。日常记录用轮询就够了,代码简单,出问题也好排查。
6.3 磨损均衡的必要性讨论
有人问 MRAM 要不要做磨损均衡。我的答案是:不需要。MRAM 的擦写寿命是 10^14 次,就算你每秒写同一个地址 1000 次,也要 3000 多年才能用完。所以地址可以随便写,不用像 Flash 那样搞复杂的均衡算法。这也是 MRAM 用起来最省心的地方——你不需要为存储寿命操任何心。
7. 项目落地后的几点体会
这个方案我在两个工业项目里实际用了,一个是环境监测终端,一个是电机振动记录仪。环境监测终端每秒记录一次温湿度和气压,连续跑了 8 个月,数据一条没丢。电机振动记录仪采样率更高,每 10ms 记录一次三轴加速度,MRAM 的写入速度完全跟得上。
踩过的坑主要集中在前期的 SPI 配置上。第一次调试时读回来全是 0xFF,查了半天发现是 SPI 模式配成了模式 1。还有一次是 CS 信号被硬件片选和 GPIO 同时驱动,导致通信时好时坏。这些问题在逻辑分析仪面前都藏不住,所以调试 SPI 设备,逻辑分析仪是必备工具,比万用表和示波器都好使。
最后分享一个小技巧:MRAM 的 WP 引脚如果不使用,一定要拉到 VDD 或者 GND,不能悬空。悬空时引脚电平不确定,可能导致芯片内部状态机误动作,出现随机写入失败。这个细节在数据手册里写得很清楚,但很容易被忽略。