☰
工业数据采集掉电保存方案:MRAM芯片与PIC32微控制器实战
2026/10/4 2:43:42 网站建设 项目流程

MR25H40CDF 和 PIC32MX795F512L,一个来自 Everspin,一个来自 Microchip,把它们放在一起做非易失存储,是我在一个工业数据采集项目里验证过的组合。当时要做的是工业现场环境监控记录仪,需要把温度、振动、压力这些过程数据高频写入,同时保证设备突然断电时不丢最后一段现场信息。一开始的方案用 SPI EEPROM,测下来寿命和写入速度都顶不住,后来换成 MR25H40CDF 这颗 4Mbit 的 SPI MRAM,主控用 PIC32MX795F512L,整套系统才算真正稳定下来。

这篇文章不准备念数据手册,而是讲清楚几个实操层面的问题:为什么工业存储要选 MRAM而不是 EEPROM/Flash,MR25H40CDF 和 PIC32MX795F512L 之间怎么接线、怎么初始化 SPI、怎么写驱动,以及掉电保存和双备份这类数据完整性设计是怎么落地的。适合谁看?做仪器仪表、工控板卡、车载记录设备的嵌入式工程师,还有想在 MCU 项目里引入 MRAM 但还没动手的人,应该都能从这里拿到可直接照抄的方案。

1. 选型核算:为什么工业现场要 MRAM 而不是 EEPROM/Flash

1.1 一次掉电事件暴露的问题

最开始用的是一颗 256Kbit 的 SPI EEPROM,型号不说了,实际测试中遇到的第一个大问题是写入速度。工艺数据每 500ms 要保存一次,一次写一条 100 字节左右的记录,按页写还好,但 EEPROM 内部编程时间实在磨人,写一页要等 5ms 左右,某些批次甚至能到 7ms。如果赶上数据缓冲满,要把整块 4KB 写进 EEPROM,一次要几百毫秒,主循环直接被拖死。

第二个问题是寿命。EEPROM 写入次数上限一般是 100 万次,看起来挺多,但算账就发现根本不够用。500ms 写一次,一天就是 172800 次,100 万次寿命只够跑 5.8 天。就算改成 10s 写一次,也不过 58 天。工业设备要求连续运行几年,这种方案从寿命上就是死路一条。

后来有人建议换 SPI NOR Flash,容量大、价格也便宜,但 NOR Flash 有个更头疼的点:写入前必须先擦除,而且擦除以扇区为单位,一个 4KB 扇区擦除一次要几十毫秒。频繁小数据写入要做擦写均衡、要维护坏块表、要处理掉电擦除中断等一堆问题,对一个小型工业记录仪来说,这套复杂度有点过度。

MRAM 的思路完全不一样。它用磁存储单元,不需要擦除、不需要页编程,每个字节都能独立直接写,写入次数标称 10^14 次级别,可以把“擦写均衡”“搬运页”“计划擦除”这些 Flash 里的麻烦事全部扔掉。

1.2 三种非易失存储的对比

把三种方案放在一张表里对比,选型理由会清楚很多。

特性传统 SPI EEPROMSPI NOR FlashMR25H40CDF (SPI MRAM)
容量256Kbit 常见64Mbit 常见4Mbit
写入单位页/字节页,且需先擦除扇区字节级直接写
页面编程/擦除耗时页写约 3~5ms擦除 4KB 约 40~100ms无需擦除
典型写入次数10^5 ~ 10^6 次10^4 ~ 10^5 次10^14 次
复杂驱动逻辑简单,但要管理等页复杂,要替换逻辑+坏块管理简单,按地址直读直写
掉电保持几十年典型 20 年典型 20 年以上

从上表能看出来,MRAM 在读写模式上最接近 SRAM,驱动最简单的同时寿命又最长。代价是单位容量价格确实比 Flash 贵,但在工业设备里,可靠性优先于成本,尤其数据记录功能是整个设备的“黑匣子”,这部分不能省。

1.3 从需求反推容量和方案

项目需求是保存三类数据:设备参数、运行日志、报警记录。算下来,单条日志 100 字节、每天大约 10 万条事件,保留 7 天的量大概需要 70MB,这个量级 MRAM 装不下,所以设计上做了分层:日志量大的部分用大容量 SD 卡,MRAM 只保存最关键的那一部分——设备配置参数、最近的报警状态、断电瞬间的最后一段过程数据。

MR25H40CDF 的 4Mbit 等于 512KB,用来放设备参数(16KB)、报警历史(128KB)和掉电紧急保存区(16KB)完全够用,剩余空间还能做双备份。这也给后来做数据完整性留下了充足余量。选型时的一个关键判断是:不是所有数据都需要 MRAM,把最有价值的高频小数据、断电关键数据放进 MRAM,方案性价比最高。

2. MR25H40CDF 硬件接入:引脚、供电与信号完整性

2.1 芯片最小电气连接

MR25H40CDF 是 Everspin 的 SPI 接口 MRAM,最小系统相当简单,本质就是一个四线 SPI 从设备。芯片上除了 SPI 的 SCK、SI、SO、CS,还有 HOLD 和 WP 两个控制脚。

这些引脚的实际连接经验如下:

  • CS(片选):接主控任意 GPIO。工业环境干扰多,CS 建议加上拉 10kΩ 到 VCC,避免悬空时进入不定状态。
  • SCK(时钟):接 PIC32 的 SPI SCK 输出脚。
  • SI(串行输入):接主控的 SDO/SDOx 输出脚。
  • SO(串行输出):接主控的 SDI/SDIx 输入脚。
  • HOLD:这是暂停通讯的控制脚,低电平时芯片忽略 SCK 和 CS 上的状态。整个系统里没有需要用它的场景,直接 10kΩ 上拉到 VCC 最省心。
  • WP:写保护输入,低电平时禁止写状态寄存器。一般不需要动态控制,同样 10kΩ 上拉。

DFN 封装没有封装体大引脚,手工焊接时尤其要注意 WP 和 HOLD 这两个脚,很多板子画完图没检查,结果这两个脚悬空,设备运行一段时间后偶发写入失败,排查起来非常折磨人。

2.2 PIC32MX795F512L 侧引脚与模块分配

PIC32MX795F512L 是 100 引脚的 MIPS32 M4K 内核 MCU,主频 80MHz,512KB Flash、128KB RAM,片上带上 USB、CAN、以太网,外设资源很丰富。这颗片子上的 SPI 模块有多个,PIC32MX 的串口引脚可以通过 PPS(外设引脚选择)映射到不同引脚上,布线自由度很高。

我实际在项目里把 MR25H40CDF 挂在 SPI1 上,SD 卡挂在 SPI2 上,两块存储互不干扰。PPS 映射具体怎么填寄存器,不需要背表,直接用 Microchip 的 MCC 工具生成即可。要提醒的是:如果 SPI 没有走到固定功能脚,而是走 RPn 引脚,需要确认对应 RPn 引脚的 TRIS 方向配置正确,SDI 必须置输入、SDO/SCK 置输出。很多初学 PIC32 的人在这里翻车,方向配反,调了半天 SPI 始终读不到数据。

MRAM 的 CS 我用了一个普通 GPIO,不使用 SPI 模块自带的 SS 功能,这样方便软件控制片选时序。工业设备里 CS 时序需要比较灵活的控制,比如掉电保存时可以先拉高 CS 中断当前操作,再重新发起关键写入,用普通 GPIO 最直接。

2.3 布局走线与上电时序处理

SPI 走线在 PCB 层面要做几件小事,都属于成本很低但能省去后期大量调试麻烦的做法。

第一,SCK、SI、SO 三条信号线在源端各串 33Ω 电阻。这个电阻作用是抑制振铃。工业环境长走线加上连接器,SPI 信号很容易过冲,串一个小电阻能把波形拉干净。实际示波器上看,不串电阻时 3.3V 信号过冲能到 4V 以上,串了电阻后基本贴着 3.3V 走。

第二,去耦电容靠近 VCC 引脚放一个 0.1μF,再加上一颗 10μF 钽电容。MRAM 写入电流不大,但这个组合对供电稳定性有帮助。上电时序上,需要等 3.3V 稳定后再操作 MRAM,PIC32 复位后延时几十毫秒再初始化 SPI,基本就避开了电源爬坡阶段。

第三,MRAM 与 PIC32 之间如果通过连接器跨板连接,尽量把 SPI 信号和地线绑在一起走。地回路太长是工业现场通讯异常的隐形杀手,这个问题不解决,后面软件写得再好也是白搭。

3. PIC32 SPI 驱动与读写实现

3.1 SPI 模块初始化的配置要点

PIC32MX795F512L 的 SPI 初始化并不复杂,关键是把模式、位宽、波特率设对。MR25H40CDF 的数据手册明确支持 SPI Mode 0 和 Mode 3,我在项目里选了 Mode 3,因为同一块板上另一颗 SPI Flash 也用 Mode 3,共用一套底层初始化逻辑,驱动代码少一半。

波特率配置要用到 PIC32 SPI 的公式:SCK 频率等于外设时钟除以两倍的波特率寄存器值加一。项目里外设时钟是 40MHz,我把 SPI1BRG 设成 4,算下来 SCK 频率是 4MHz。这个速度对 MR25H40CDF 来说非常保守,芯片标称支持到几十 MHz,4MHz 只是为了避免长线干扰。

初始化代码示意如下,核心寄存器操作保留,实际使用中可以放到 MCC 生成的外设初始化文件里:

void SPI1_Init(void) { // 关闭 SPI 模块进行配置 SPI1CON = 0; // 波特率:F_SCK = F_PB / (2 * (SPI1BRG + 1)) // F_PB = 40MHz, SPI1BRG = 4 -> F_SCK = 4MHz SPI1BRG = 4; // 主模式、8 位数据、Mode 3 SPI1CONbits.MSTEN = 1; SPI1CONbits.MODE16 = 0; // CKP = 1, CKE 按 Mode 3 配置 SPI1CONbits.CKP = 1; SPI1CONbits.CKE = 0; // 使能 SPI 模块 SPI1CONbits.ON = 1; }

注意一点,PIC32MX 的 SPI 是同步收发结构,主模式发送一个字节的同时会接收一个字节。写驱动时不能像某些 MCU 那样只关心发送完成,还要把接收缓冲读掉,否则下一次传输会触发接收溢出标志,导致后续数据错乱。

3.2 SPI 字节交换的底层函数

PIC32MX 的 SPI 收发通过 SPI1BUF 这一个寄存器完成,写它表示发送,读它表示取走刚收到的字节。底层交换函数写成这样:

static uint8_t spi_xfer(uint8_t byte) { SPI1BUF = byte; while (!(SPI1STAT & 0x01)) // SPIRBF 置位说明收到一个字节 { ; } return SPI1BUF; }

这段代码是所有 MRAM 命令的基础。发送读命令时,后续要连续发假字节来产生时钟,才能把数据从 SO 脚移位出来。不少人第一次接触 PIC32 SPI 会把“发一个字节等一个回包”的逻辑弄混,其实只要记住spi_xfer()发什么不重要、回什么才重要,就顺了。

3.3 MRAM 命令封装与读写流程

MR25H40CDF 的 SPI 指令集很直观,主要用到六条:写使能(0x06)、写失能(0x04)、写数据(0x02)、读数据(0x03)、读状态寄存器(0x05)、写状态寄存器(0x01)。

写入操作有个强制性前提:每次写数据前必须先发写使能,否则写命令直接被忽略。标准流程是这样:

  • 拉低 CS;
  • 发 0x06(WREN);
  • 拉高 CS;
  • 再次拉低 CS;
  • 发 0x02(WRITE);
  • 发 3 字节地址,高位在前;
  • 连续发待写入数据;
  • 拉高 CS。

这里有个容易踩的坑:写完数据后芯片内部 WIP 位会短暂置位,虽然 MRAM 不像 EEPROM 有几十毫秒编程时间,但连续发起下一条写命令前,还是建议查一下状态寄存器,确认上次写操作完成。代码实现如下:

#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_RDSR 0x05 static void mram_cs_low(void) { LATBbits.LATB10 = 0; } static void mram_cs_high(void) { LATBbits.LATB10 = 1; } static void mram_write_enable(void) { mram_cs_low(); spi_xfer(MRAM_CMD_WREN); mram_cs_high(); } static void mram_wait_busy(void) { mram_cs_low(); spi_xfer(MRAM_CMD_RDSR); while (spi_xfer(0x00) & 0x01) // bit0 为 WIP { ; } mram_cs_high(); } void mram_write_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { mram_write_enable(); mram_cs_low(); spi_xfer(MRAM_CMD_WRITE); spi_xfer((addr >> 16) & 0xFF); spi_xfer((addr >> 8) & 0xFF); spi_xfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { spi_xfer(buf[i]); } mram_cs_high(); mram_wait_busy(); }

读取数据的流程省掉了写使能,直接拉低 CS、发 0x03、发 3 字节地址,然后连续spi_xfer(0x00)把数据读出来:

void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { mram_cs_low(); spi_xfer(MRAM_CMD_READ); spi_xfer((addr >> 16) & 0xFF); spi_xfer((addr >> 8) & 0xFF); spi_xfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = spi_xfer(0x00); } mram_cs_high(); }

这段驱动直接拿到实际项目里就能用。MRAM 没有页边界限制,连续写几百字节不需要像 Flash 那样关心是否跨页,地址可以一直递增,这是设计上最省心的地方。

3.4 关于 DMA 和高速写入的延伸

PIC32MX795F512L 的 SPI 支持 DMA,如果一次要批量写几百字节,可以配置 DMA 把内存数据直接搬运到 SPI1BUF,CPU 只在传输完成后处理一个中断。工业记录仪掉电保存时数据量不大,普通中断写就够了,但如果是高频采集系统,DMA 模式能显著降低 CPU 占用。

我自己做批量写入时给 DMA 留了接口,分两档:关键的小块数据(几十字节)走查询方式,逻辑简单可靠;批量日志走 DMA,通过定时触发搬运。这里一个小小的建议,MRAM 虽然写入快,但 MCU 和 MRAM 之间的时钟毕竟是串行链路,批量传输要估算好时间,别在中断服务里做太多事。

4. 掉电保存与数据完整性设计

4.1 存储区规划与启动校验

512KB 的 MRAM 如果不做规划,很容易写成一锅粥。我在项目里把存储区划分为下面几个部分:

地址范围大小用途
0x00000 ~ 0x03FFF16KB设备参数区,双备份
0x04000 ~ 0x07FFF16KB设备参数镜像区
0x08000 ~ 0x1FFFF96KB报警历史循环记录
0x20000 ~ 0x2FFFF64KB掉电紧急保存区
0x30000 ~ 0x3FFFF64KB运行日志临时缓冲
0x7FFE0 ~ 0x7FFFF32B存储区状态标志

启动后先读状态标志区,这个区域里存放一个预先定义的魔数和存储格式版本号。魔数匹配了才认为自己读到的配置有效,否则说明可能是首次上电,或者上次运行中发生了严重异常,直接回退默认参数。这个设计和嵌入式领域经典的启动 loader 校验思路完全一致,成本低、收益大。

4.2 掉电检测与紧急保存流程

设备掉电时,电源会从正常值跌落到 0。问题是 MRAM 写入也需要时间,虽然很快,但也不能在主控已经掉到欠压点才开始保存。我的做法是加一个掉电检测电路:把输入电源经过电阻分压后送进 PIC32 的 INT 引脚,再配合一个大一点的储能电容。

当输入电压低于某个阈值时,INT 引脚触发中断。此时 PIC32 依靠供电电容上储存的电量还能正常跑几毫秒到十几毫秒,而这段窗口足够完成一次紧急保存。比如在 4MHz SPI 下,往 MRAM 写一条 64 字节的掉电记录,算上命令头大约 68 字节,传输时间大约 136μs,加一点软件开销也就几百微秒,完全来得及。

掉电中断服务函数里要做的动作很明确:关掉其他中断,把关键全局状态结构体拷进一个静态区,然后调用 MRAM 写入函数,把这个结构体写到紧急保存区。为了尽可能可靠,写完后再读回校验,一致了才拉高一个掉电完成标志:

volatile uint32_t g_globalState[16]; void __ISR(_EXTERNAL_0_VECTOR, IPL2SOFT) Int0_Handler(void) { // 清除中断标志 IFS0CLR = _IFS0_INT0IF_MASK; // 临时状态拷贝到本地变量 uint8_t tmp[64]; memcpy(tmp, (uint8_t *)g_globalState, sizeof(tmp)); // 写入紧急保存区,并回读校验 mram_write_bytes(SAVE_AREA_ADDR, tmp, sizeof(tmp)); mram_read_bytes(SAVE_AREA_ADDR, (uint8_t *)g_globalState, sizeof(tmp)); // 设置保存完成标志 saveDoneFlag = 1; }

这里有个非常关键的经验:中断处理中不能再依赖系统滴答、延时函数或者任何需要调度机制的 API,所有操作必须是顺序执行、不阻塞的。所以紧急保存用的就是最简单直接的状态机式调用,不用 RTOS,不用队列。

4.3 双备份与 CRC 校验

掉电那一下不可控因素很多,就算 MRAM 本身是非易失的,也保不齐写了一半就去电了。为了把这种“半个数据”的风险也兜住,重要的参数区要做双份备份。

参数写入采用 A/B 区交替策略:第一次写 A 区,第二次写 B 区,第三次再写 A 区。每个区头部都放一个 CRC 校验和、数据长度和递增序列号。上电读取时,优先读 A 区,校验通过就采用;A 区校验失败就试 B 区;两个区都失败才回退默认参数。

CRC 的计算用软件实现,数据量不大,用 CRC16 就够了。这个双备份策略带来的额外写入量不大,因为参数区的写入频率本身很低,通常只有在设备调试和参数修改时才会触发。

报警历史区则用循环覆盖的方式,写成环形缓冲,满了就从头覆盖最旧的一条。报警记录不要求每一条都永久保存,但要求最后写入的若干条能完整读取。环形缓冲区的头尾指针也放到 MRAM 里,每次写记录时先更新指针再写数据,能保证即使掉电,下次启动也能根据指针定位到最后一条可用记录。

5. 实测数据与问题排查

5.1 读写性能实测记录

在 PIC32MX795F512L 跑 4MHz SPI 时,实测数据如下:

操作数据量耗时
读 64 字节64B约 140μs
写 64 字节(含回读校验)64B约 300μs
写 100 字节(掉电保存场景)100B约 220μs
启动时读取全部参数区16KB约 30ms

这个耗时完全在项目预算内。相比 EEPROM 每次页写至少 5ms 的等待,MRAM 写 64 字节只要一两百微秒,手工把 SPI 时钟提到 10MHz 后还能再快一倍多。不过 4MHz 够用,我在量产固件中保持了保守配置。

5.2 SPI 数据错位和进程卡死的排查过程

调试中遇到过两次比较典型的故障。第一次是 MRAM 回读大量数据时,前几个字节全是 0xFF。检查波形发现 CS 拉低后,第一字节时钟就来了,但此时 SI 线上可能还没稳定,芯片把第一个字节的移位时钟吃了半个。解决方法是 CS 拉低后加几个微秒延时,再发起首个命令字节。这在数据手册里叫 CS 建立时间,不能省。

第二次是连续写较长数据时,SPI 总线上的设备偶发卡死。示波器看波形,SCK 末端有一串振铃超过逻辑阈值,导致多时钟信号被打进芯片。后来把 SPI 时钟从 8MHz 降到 4MHz,并在 SCK 源端串了 33Ω 电阻,问题就不再出现。这里想说的是,SPI 速度不是越高越好,稳定才是工业设备的第一诉求。

5.3 HOLD 与 WP 悬空的坑

这个坑最具隐蔽性。有一版试制板,因为 DFN 封装体积小,HOLD 引脚没引出上拉,结果设备运行一段时间后,MRAM 偶发“卡住”不动,RDSR 读状态也读不到。最终定位到是 HOLD 引脚受到邻近信号线干扰,瞬间拉低触发了 hold 功能,芯片把 SCK 和 CS 都忽略了。

修复很简单,把 HOLD 和 WP 都加上 10kΩ 上拉电阻,后续再没复现。这算是 MRAM 应用里最经典的“芯片本身没问题、封装引脚坑人”的案例,画 PCB 时多留意一眼就能避免。

5.4 排查技巧速查

现象可能原因排查方法
首字节读出 0xFFCS 建立时间不足CS 拉低后延时几个 μs
数据偶发错位SPI 信号振铃/线太长降速,串 33Ω 电阻
写使能后写命令无效WREN 与 WRITE 之间 CS 没拉高严格按指令时序执行
芯片“死掉”无响应HOLD 悬空受干扰HOLD/WP 上拉到 VCC
上电偶发读校验失败3.3V 未稳定就操作上电延时数十 ms 再初始化
掉电保存数据不完整掉电检测阈值太晚加电容储能,阈值提前触发

这些排查经验在实际项目中帮了大忙,也说明了一个道理:MRAM 本身很可靠,但外围时序和封装细节一样会影响它的表现。

我在实际项目里还留了一手,把 MRAM 的读写驱动单独抽象成一个模块,接口只有读一段、写一段、读状态三个函数。这样后续如果要迁移到其他 MCU 平台,只需要替换底层spi_xfer和引脚操作,上层的数据机构、双备份逻辑、掉电保存策略全部可以复用。工业存储这块,一旦把数据完整性机制跑通,后续就是稳定的复制和经验积累,这也是 MRAM 相比普通存储方案给我最大的感触。

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

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

立即咨询