有一类存储需求,在工业现场特别扎心:数据要频繁写入、掉电不能丢、还要扛得住温度波动。我最近在设备上就用 Everspin 的 MR25H40CDF MRAM 芯片,配合 Microchip 的 PIC32MX675F256L 单片机组了一套外部存储方案,专门用来保存运行参数和过程数据。MR25H40CDF 是一颗 4Mbit 容量的 SPI 接口 MRAM,PIC32MX675F256L 是带丰富通信外设的 32 位 MIPS 内核 MCU,两者搭在一起,工业嵌入式场景里存储和读取数据能做得非常省心。这篇就把方案从选型、硬件接线、SPI 驱动,到掉电保护设计和现场排查完整拆开讲,希望能给正在给工业项目选外部存储的朋友一点参考。
1. 方案选型:为什么是 MR25H40CDF + PIC32MX675F256L
1.1 MRAM 在工业存储里的特殊地位
做工业嵌入式的人应该都有体会,最难受的存储需求不是容量不够,而是“写得太频繁”。传统方案里,串行 NOR Flash 写一个字节之前要先擦除整个扇区,而且擦写寿命普遍在 10 万次到 100 万次之间。设备每秒钟要记录一次运行状态、累计运行时间、温度曲线,用不了几个月就把 Flash 写穿了。为了延长寿命,你得写磨损均衡、坏块管理、掉电保护,代码一坨一坨地堆,最后还不一定可靠。
EEPROM 虽然能按字节写,但容量小得可怜,超过 64KB 的 EEPROM 又贵又难买,掉电保护做起来同样麻烦。所以工业现场一直需要一种“像 RAM 一样随便写,像 Flash 一样掉电不丢”的存储介质,MRAM 就是冲这个需求来的。
MRAM 的存储单元本质是一个磁性隧道结,用一个很小的磁场方向来表示 0 和 1。它的特点有两个:一是改变磁化方向不需要先擦除,所以写入是直接覆盖的,不存在“先擦后写”的周期;二是磁化方向在掉电之后不会变,数据自然就保住了。打个比方,Flash 像一块白板,想写新内容得先拿板擦把旧字擦掉,擦多了板面就花了;MRAM 像一堆永久的磁力开关,拨过去就完事,拨多少次都不会坏。
正是因为这个原理,MRAM 在工业设备里特别适合干三件事:频繁更新的计量数据、需要掉电保存的断点状态、还有不允许丢失的关键参数。我以前用串行 Flash 做频繁记录,光磨损均衡和掉电保护就花了两周,换成 MRAM 之后,写一条记录就是“发个写指令、数据进去、完事”,省下来的时间全拿去调别的功能了。
1.2 MR25H40CDF 的技术画像
MR25H40CDF 是 Everspin 的 MR25H40 系列,容量 4Mbit,也就是 512KB,8 位组织。接口是标准的 SPI,支持模式 0 和模式 3,时钟可以跑到 40MHz 左右,具体速度极限要看 PCB 走线和 MCU 的 SPI 外设能力。供电范围是 2.0V 到 3.6V,典型的 3.3V 系统直接就能用,我用的 CDF 尾缀是工业温度级别的版本,具体温度范围以你采购型号的规格书为准。
这颗芯片的指令集和 SPI NOR Flash 很像,写使能、读状态、读数据、写数据、读 ID 这些基本指令都能对应上,但它没有 Flash 的页擦除和扇区擦除指令。要说最大的区别,就是写之前完全不需要擦除,任意地址可以直接覆盖写,而且连续写可以一直跨越整片芯片,只有到了地址末尾 0x7FFFF 才会回绕到 0x00000。
价格上 MRAM 确实比 Flash 贵不少,这是事实。但工业产品算账不能只算 BOM 成本,要算上可靠性、代码维护、现场售后这些隐性成本。一颗 MRAM 能把存储相关的软件复杂度降一个量级,产品在客户现场少出一次问题,省下的差旅和停机成本往往就能覆盖好几片 MRAM 的差价。这个账在工业项目里是算得过来的。
1.3 PIC32MX675F256L 能提供什么
PIC32MX675F256L 是 Microchip 的 32 位 MCU,内核是 MIPS M4K,主频 80MHz,带 256KB Flash 程序空间和 64KB SRAM,这个配置放在工业控制场景里足够踏实。它外设给得很全,多个 SPI、UART、CAN、USB,还有 DMA 和比较丰富的中断系统,既能和上位机通信,又能同时挂传感器,还能用 SPI 和 MRAM 打交道。
我选它做这套存储方案的主控,主要看重几点:第一,它本身有独立的 SPI 外设,不用靠软件模拟时序,配合 DMA 可以做到大块数据搬运不占 CPU;第二,中断系统响应快,掉电保存这种对时效敏感的场景,能在收到掉电信号后迅速进入紧急保存流程;第三,如果整个项目已经基于 Microchip 生态,就没必要为了一个存储方案引入另一套开发工具链,统一用 MPLAB X 和编译器省去很多麻烦。
后缀里的 L 代表 100 引脚 TQFP 封装,引脚资源丰富。实际项目里我就用了一片 100 脚封装,一部分引脚走 SPI 和 MRAM 通信,另一部分走 RS485、IO 控制和状态灯,整个系统一片 MCU 全包了,没有额外加协处理器。
2. 硬件连接:原理图级别的接线要点
2.1 引脚级接线方案
MR25H40CDF 是标准 SPI 从设备,和 PIC32 的连接非常直接。SCK 接 SPI 时钟,SI 接主设备的数据输出,SO 接主设备的数据输入,CS 片选引出来接一个普通 GPIO。注意数据手册里的叫法,MRAM 的写数据脚叫 SI 或 MOSI,读数据脚叫 SO 或 MISO,和 MCU 侧的叫法反过来,画原理图的时候很容易绕晕。
我把接线列出来,照着连就可以。
| MR25H40CDF 引脚 | 接到 PIC32MX675F256L | 说明 |
|---|---|---|
| CS | GPIO,例如 RF13 | 片选,软件控制,不能直接接地 |
| SCK | SCK1 输出引脚 | SPI 时钟 |
| SI / MOSI | SDO1 输出引脚 | 主发从收 |
| SO / MISO | SDI1 输入引脚 | 主收从发 |
| VDD | 3.3V | 电源 |
| VSS | GND | 地 |
| HOLD | 通过 10kΩ 上拉到 3.3V | 悬空会有隐患 |
| WP | 通过 10kΩ 上拉到 3.3V | 写保护,正常运行时置高 |
CS 必须用 GPIO 控制,这个我在现场踩过坑。如果偷懒直接接地,MCU 复位的瞬间 SPI 引脚还是高阻态,线上一个毛刺就可能让 MRAM 误以为被选中,导致莫名其妙的误写。加个 10kΩ 上拉把 CS 默认拉高,只有在真正要通信的时候才拉低,这是最稳妥的做法。
SPI 信号线上我还串联了 33Ω 的电阻,位置尽量靠近 MCU 的引脚。这个电阻不是摆设,它能抑制信号过冲,在工业设备里尤其是电机驱动器旁边,信号线上经常有振铃,串个电阻能明显改善波形完整性。如果板子空间紧张,至少给 SCK 串一个。
2.2 电源、去耦与 HOLD/WP 的隐含风险
MRAM 对电源质量不是特别娇气,但工业现场的供电环境复杂,不能在电源上省钱。VDD 引脚旁边放了 0.1μF 的陶瓷电容,要求贴芯片电源脚放,越近越好,这是基本功。如果空间允许,再在稍远一点的地方加一颗 4.7μF 的钽电容,用来吸收低频波动。别小看这几个电容,掉电瞬间要靠它们撑着让 MRAM 完成最后几次写操作。
HOLD 和 WP 这两个引脚是最容易被忽略的。MR25H40CDF 的 HOLD 引脚如果悬空,噪声耦合进去可能导致芯片暂停通信,表现出来就是“偶尔读写超时”;WP 引脚悬空则可能导致写保护状态不确定。正确做法是两个引脚都通过电阻上拉到 VDD,让它们默认处于无效状态。我习惯用 10kΩ 上拉,既能固定电平,又不会产生额外的功耗问题。
这里还要提醒一个 PIC32 特有的坑:PIC32MX675F256L 的 VCAP 引脚必须接正确容量的电容到 VSS,具体数值看型号数据手册。接错或者漏接,MCU 可能根本无法启动,这时候排查外围电路全是白费功夫。很多人在新画的板子上被这个引脚坑过,包括我。
2.3 PIC32 引脚复用(PPS)的配置方法
PIC32 的外设引脚不像某些 MCU 那样固定绑定,SPI1 的 SCK、SDO、SDI 引脚需要通过外设引脚选择(PPS)功能映射到实际的物理引脚。也就是说,原理图连接只完成了一半,如果软件里没配置 PPS,SPI 外设根本没信号输出到引脚上。
典型的 PPS 配置代码是这样:
// 示例:把 SPI1 的信号映射到具体物理引脚 // 具体映射值和寄存器位定义,以你用的 PIC32 型号数据手册 PPS 章节为准 SDI1R = 0b0011; // SDI1 输入映射到某个输入引脚 RPD1R = 0b0011; // SDO1 输出映射到 RD1 RPD2R = 0b0011; // SCK1 输出映射到 RD2这段代码必须在 SPI 外设初始化之前执行,因为 PPS 配置的是引脚和外设之间的连接关系,相当于先把路修好,车才能开。如果 SPI 调试时示波器测不到时钟,第一个怀疑对象就是 PPS 没配对。这个坑几乎每个第一次上手 PIC32 的人都会遇到。
另外注意,PPS 的输出映射寄存器名字一般是 RPxR,输入映射寄存器名字是 SDIxR 或者对应外设输入编号,不同封装、不同型号的可用映射范围不一样。开发的时候最靠谱的方式是直接打开 Microchip 的 Pin Manager 图形界面,它会自动帮你看能不能映射,避免你自己翻手册算到眼花。
3. 驱动实现:从 SPI 初始化到读写数据
3.1 SPI 工作模式与初始化参数
MR25H40CDF 支持 SPI 模式 0 和模式 3,区别在于时钟极性和采样边沿。模式 0 是空闲时钟为低、在上升沿采样,模式 3 是空闲时钟为高、在下降沿采样。PIC32 的 SPI 外设里,模式 0 对应 CKP=0、CKE=0,模式 3 对应 CKP=1、CKE=1。我用的是模式 0,大多数 SPI 从设备默认都吃这个模式,兼容性最好。
初始化代码可以参考这个:
void SPI1_Init_MRAM(void) { // 先关闭 SPI 再配置 SPI1STATbits.SPIEN = 0; // 主模式、8 位数据、模式 0 SPI1CONbits.MSTEN = 1; SPI1CONbits.CKP = 0; SPI1CONbits.CKE = 0; SPI1CONbits.MODE16 = 0; SPI1CONbits.ENHBUF = 0; // 波特率设置:BRG = (PBCLK / (2 * 目标SPI时钟)) - 1 // 假设 PBCLK = 40MHz,目标 SPI 时钟约 5MHz SPI1BRG = 3; // 使能 SPI SPI1STATbits.SPIEN = 1; MRAM_CS_LAT = 1; // 片选默认拉高 }SPI 时钟频率不要一上来就追求最高。MR25H40CDF 支持 40MHz,但 MCU 到芯片之间的走线、连接器、排线都会影响信号质量。我验证板初期都是用 5MHz 甚至 1MHz 跑,功能全部正常之后再慢慢调高。工业设备上如果 MRAM 离 MCU 比较远,走线超过 5 厘米,我建议保守一点,10MHz 以下比较稳。
3.2 读写指令与核心代码
MR25H40CDF 的指令集和 SPI NOR Flash 很像,常用的几条指令列出来:
| 指令 | 操作码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读器件 ID |
写数据有一个基本规矩:必须先发 WREN 写使能命令,否则芯片会忽略写操作。WREN 的时序是 CS 拉低、发送 0x06、CS 拉高,CS 必须有一个完整的低-高过程,不能把 WREN 和后面的写指令连在一起。写完 WREN 之后可以读状态寄存器看写使能锁存位是否置位。
状态寄存器里有一位是 WP 保护状态相关,具体位号不同批次略有区别,使用时以规格书为准。我一般只看 WEL 位来确认写使能是否成功,其他位暂时用不到。
SPI 字节传输函数:
uint8_t SPI1_Transfer(uint8_t data) { SPI1BUF = data; while (!SPI1STATbits.SPIRBF); return SPI1BUF; }写缓冲区的函数:
void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { // 写使能 MRAM_CS_LAT = 0; SPI1_Transfer(0x06); MRAM_CS_LAT = 1; // 发写指令和 24 位地址 MRAM_CS_LAT = 0; SPI1_Transfer(0x02); // WRITE SPI1_Transfer((addr >> 16) & 0xFF); // A23-A16 SPI1_Transfer((addr >> 8) & 0xFF); // A15-A8 SPI1_Transfer(addr & 0xFF); // A7-A0 for (uint32_t i = 0; i < len; i++) { SPI1_Transfer(buf[i]); } MRAM_CS_LAT = 1; }读缓冲区的函数:
void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LAT = 0; SPI1_Transfer(0x03); // READ SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = SPI1_Transfer(0); } MRAM_CS_LAT = 1; }注意,MRAM 写完之后不需要像 Flash 那样等待编程时间,芯片内部的数据存储是立即完成的。但“芯片完成了”不代表“系统一定写对了”,传输过程中如果 CS 上有一个毛刺,或者 SPI 线上有干扰,最后写进去的数据可能跟想写的不一致。所以我在所有关键数据的写入流程里,写完都必须回读核对,或者至少读取状态寄存器确认状态正常。
3.3 数据校验与可靠性设计
存储驱动能读写只是第一步,工业产品的存储系统设计必须考虑数据完整性。我的做法是三件套:写后回读、CRC 校验、双槽备份。
写后回读是最直接的手段。写完一个关键参数块之后,马上用 READ 指令把相同地址的数据读回来,逐字节比较。如果一致,才能算写入成功。这个操作虽然多花一点时间,但对 MRAM 来说读操作很快,成本完全可以接受。
CRC 校验用来解决更隐蔽的问题,比如系统意外掉电导致数据写到一半。MRAM 本身存储的数据不会丢,但 MCU 写入过程可能被掉电打断,最后留下一个不完整的帧。这种情况下回读数据本身也是完整的,但内容却不对。解决办法是每条记录都带一个头部,包含帧长度、数据校验值、序号。读取的时候先算 CRC,对不上就认为这条数据无效。
双槽备份是工业参数存储的经典方案。把参数区划分为 A 槽和 B 槽,每次写参数时轮流写其中一个,比如这次写 A、下次写 B。每个槽里放一个版本号和 CRC。上电读取时,先读 A,校验失败就读 B;如果两个都成功,取版本号大的那个。这样即使上一次写入被掉电打断,最多坏掉一个槽,另一个槽的数据始终是完整的,系统永远能恢复到最后一次正确的参数。
4. 工业现场存储策略:掉电保存与日志记录
4.1 掉电检测与关键状态保存
工业设备最怕的不是正常写入,而是掉电瞬间的那一次保存。伺服电机的当前位置、设备正在执行的工艺步骤、一批产品的累计数量,这些数据丢失了轻则重新校准,重则整批产品报废。MRAM 的优势在于它写入速度快、不需要擦除,能利用掉电后的最后一点点电能完成保存。
我的方案是在系统电源端加一个掉电监测电路,用 MCP101 这类电压监控芯片检测主电源。正常工作时它输出电压正常信号;当电源开始跌落时,它会提前几十毫秒产生一个低电平中断信号给 PIC32。同时,电源轨上放一个比较大的电解电容或者超级电容,支撑 MCU 和 MRAM 在掉电后继续工作一段时间。
PIC32 收到掉电中断后,立即进入紧急保存流程:关闭不必要的中断和外围设备,把关键运行状态整理成一条精简记录,写入 MRAM 的专用掉电保护区。整个流程要控制在掉电维持时间内完成。512 字节的数据,通过 5MHz 的 SPI 写入 MRAM,理论传输时间不到 1 毫秒,加上软件开销也就几毫秒级别,完全来得及。
这里有一个细节值得注意:掉电中断处理程序里不要再做复杂操作,比如别去读其他传感器、别做计算转换,只做一件事,把内存里的状态快照原样搬到 MRAM。处理逻辑越短,掉电窗口内完成的可能性越高。我就吃过亏,开始时在中断里做了太多事,结果电容都快放干了程序还没保存完。
4.2 存储空间的区域规划
512KB 的 MRAM 在工业场景里不算小,但如果没有合理规划,用起来很快就会乱。我习惯在项目初期就把地址空间分好区,每个区固定用途,方便管理和升级。下面是我在这套方案里用的规划:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00000 - 0x00FFF | 设备信息区 | 设备唯一 ID、硬件版本、生产日期 |
| 0x01000 - 0x01FFF | 参数双槽区 | A/B 槽交替保存运行参数 |
| 0x02000 - 0x0FFFF | 运行日志区 | 环形缓冲区,记录操作和报警 |
| 0x10000 - 0x7FFFF | 事件记录区 | 预留大块空间给批量数据 |
设备信息区写入频率极低,属于“一次写入,多次读取”,放在最前面。参数区用双槽,保证关键参数的完整性。运行日志区做成环形缓冲,这是 MRAM 最舒服的应用场景,因为它没有擦除块大小限制,新纪录可以直接覆盖最旧的数据,实现起来比 Flash 简单太多。后面的 0x10000 开始的大块空间是预留的,以后如果要记录 IO 波形、温度曲线这类批量数据,不用重新划分地址。
环形日志的实现里面有个小技巧:日志的写位置指针本身也要存到 MRAM 里,如果这个指针在掉电时写坏了,整个日志就不知道从哪继续写了。所以我不用单一指针,而是给每条日志记录编一个递增序号,启动时扫描日志区,找到序号最大的那条作为最新记录,再往后写。虽然扫描要一点时间,但可靠性高得多。
4.3 延长系统寿命的软件习惯
MRAM 的写入寿命理论上是无限的,但“无限”不代表可以乱写。高频无意义的写入会占用 CPU 时间,增加功耗,还会让那些需要和 MRAM 交互的临时变量频繁变化,增加软件出错概率。所以即使是 MRAM,我也坚持两个软件习惯。
第一,批量合并写入。现场采集的数据先放到 RAM 缓冲区里,攒够一定数量或者到固定时间周期再一次性写入 MRAM。这样既减少了 SPI 通信的次数,又能让写入的数据以完整帧的形式落盘,不容易出现半截数据。比如记录温度曲线,我都是 32 个采样点攒成一条记录,一次写入,而不是每个点写一次。
第二,写入前暂停相关任务。MRAM 写入过程中,如果另一个任务正好在修改同一份源数据,最终写入的数据可能是一个不一致的混合状态。我的做法是在写参数前用一个互斥标志,暂停所有可能修改该参数的任务,等写完之后再恢复。这个在 RTOS 环境里尤其重要,裸机程序也要注意中断里改数据的问题。
5. 常见故障与排查实录
5.1 故障排查速查表
做存储方案,出问题时最怕没头绪。我把实际调试中常见的现象和排查方向整理成一张表,照着查能省不少时间。
| 故障现象 | 可能原因 | 排查思路 |
|---|---|---|
| 读回全 0xFF | CS 没选中、HOLD 引脚拉低、接线错误 | 示波器看 CS 是否正常拉低,量 HOLD 电压 |
| 读回全 0x00 | SPI 模式不对、MOSI/MISO 接反 | 检查 SPI 的 CKP/CKE,核对 SI/SO 连接 |
| 写后回读不一致 | 没发 WREN、WP 保护、电源噪声 | 确认写使能流程,检查 WP 引脚电平 |
| SPI 时钟无波形 | PPS 未配置、SPI 未使能 | 检查 PPS 映射,确认 SPI1CON 配置 |
| 偶尔坏一两个字节 | 电源纹波大、信号线过长、CS 毛刺 | 加强去耦,降 SPI 时钟,CS 加上拉 |
| 上电瞬间数据被改 | CS 上电高阻、噪声误选中 | CS 加 10kΩ 上拉到 VDD |
其中 CS 问题是最高频的坑,十个问题里有三个都跟 CS 相关。MRAM 的片选是低有效,CS 悬空时任何一个噪声尖峰都可能让它进入接收状态,进而执行一些莫名其妙的指令。所以我的每块板子默认都有 CS 上拉电阻,这是设计规范,不是可选项。
5.2 现场遇到的两个典型问题
第一个问题是 SPI 信号全通但 MRAM 就是不听话。用示波器抓 CS、时钟、数据都正常,写入却总是失败,回读出来全是 0xFF。排查了一下午,最后发现是 HOLD 脚虚焊,引脚电压只有 1.2V,芯片的 HOLD 功能处于部分触发状态,SPI 指令发了一半就被挂起。把 HOLD 脚重新焊接并确认上拉到 3.3V 后,一切恢复正常。这个经历让我后来每次画板都要求工艺人员重点检查 HOLD 和 WP 两个引脚的焊接质量。
第二个问题是 SPI 时钟在示波器上看不到。当时是新画的板子,第一个版本的程序直接跑 MRAM 读写,结果读 ID 读不出来。用示波器点 SCK 引脚,屏幕上一片平静,完全没有波形。查到最后才发现是 PPS 映射没有配置,SPI1 的信号根本没连接到物理引脚上。配置好 PPS 之后,同样的程序立即可用。这件事提醒我,PIC32 平台调试 SPI 的第一步永远是确认引脚映射,而不是检查 SPI 本身。
还有一个顺带的问题值得提:电源掉电瞬间正好赶上写数据。现场反映设备偶尔出现参数错乱,后来跟踪发现是用户直接断总闸,掉电瞬间正好在写参数区。软件加了掉电监测和双槽备份之后,这个故障再也没出现过。硬件再可靠,软件也必须站在“掉电随时可能发生”的前提来设计。
5.3 验证自查清单
板子打样回来,我有一套固定的验证流程,每次都能快速判断存储系统是否健康。
上电第一件事,发读器件 ID 指令 0x9F,确认和 MRAM 能够正常握手。如果 ID 能读回来,说明 SPI 基本链路通了,电源和接线都正常。读不回来再去查硬件。
第二步,读写全片测试。先对某一小段地址写一个特定模式,比如 0x55、0xAA 交替,读回来对比。这一步能验证 SPI 模式和时序是否正确。然后再做随机地址测试,确保不是只有某一段地址能工作。
第三步,做掉电测试。在系统运行中直接断电,反复几十次,每次上电后检查参数区和日志区是否完整。这是检验整个掉电保护设计最有价值的测试,只有通过了才敢把设备拉到现场。
第四步,有条件的话做高低温测试。MRAM 芯片本身工业级没问题,但 PCB 上的电容、电阻在高低温下特性会漂移,尤其是掉电维持电容。我见过低温下电容容量下降导致掉电保存时间不够的案例,所以环境测试绝对不能省。
最后再分享一个习惯:无论 MRAM 手册上写得多完美,所有关键数据的可靠性最终还是靠“写后回读 + CRC + 双槽备份”这套软件方案兜底。MRAM 解决的是物理存储介质的问题,让写入不再受擦除和寿命限制,但工程上的可靠性永远需要软件在另一个维度做校验。特别是掉电保存的场景,硬件再稳,也要保证软件能在几十毫秒的窗口内按部就班地把该做的事做完。这个思路不仅适用于 MR25H40CDF 和 PIC32 的组合,换成任何 NVM 介质,这套方法论都不会过时。