MRAM掉电存储方案:MR25H40CDF与PIC18F45K40实战
发布时间:2026/10/5 9:53:13 锦皓数字建站

1. 项目缘起与方案选型思路工业现场的数据记录仪、PLC 扩展模块、智能电表这些设备有一个共同的痛点频繁掉电。市电波动、设备急停、人为拉闸随时可能把正在写入的关键参数打断。传统方案用 EEPROM 或者带后备电池的 SRAM 来扛这个问题但 EEPROM 写入寿命只有百万次量级擦写还慢电池方案又要考虑电池寿命、低温失效、运输合规一堆麻烦事。我这两年做的一个工业采集终端项目就卡在这个环节上最后选定了MR25H40CDF 这颗 MRAM磁性随机存储器搭配PIC18F45K40单片机来做数据存储与读取实测下来稳定性非常满意。先说清楚这套组合到底解决什么问题。MR25H40CDF 是一颗 4Mbit512KB容量的 SPI 接口 MRAM它的核心特性是写入无延迟、近乎无限次擦写、掉电数据不丢。跟 Flash 比它不需要先擦后写字节级随机写入写一个字节和写一整页耗时几乎一样跟 EEPROM 比它没有擦写寿命焦虑写入次数在 10^14 量级基本可以认为写不坏。PIC18F45K40 是 Microchip 的 8 位单片机自带 MSSP 模块可以配置成硬件 SPI主频 64MHz内置 EUSART、ADC、比较器等外设工业温度范围版本很常见价格也友好。两者通过 SPI 总线连接MCU 做主机MRAM 做从机构成一个极简但极其可靠的非易失存储子系统。为什么不用片内 EEPROMPIC18F45K40 本身有 1KB 的片内 EEPROM小数据量确实够用。但项目里要存的是带时间戳的采样记录一天几千条还要支持历史数据回读1KB 根本不够而且片内 EEPROM 写入会阻塞 CPU擦写寿命也就十万次级别长期高频写入迟早出问题。外挂 MRAM 之后存储容量、写入速度、寿命三个维度同时解决代价只是多一根 SPI 总线和几个 IO 口。为什么不用 SD 卡或者 SPI FlashSD 卡文件系统复杂工业现场震动、湿度、掉电都会导致文件系统损坏维护成本高。SPI Flash比如 W25Q 系列容量大、便宜但写入前必须擦除整个扇区掉电时正在擦除的扇区数据全丢而且擦写寿命只有十万次做高频数据记录很快就写废了。MRAM 的字节级写入和掉电原子性恰好补上了这个短板。所以这套方案的核心定位就是中等容量、高频写入、掉电绝对不能丢数据的工业嵌入式存储场景。适合谁来参考这篇内容如果你正在做工业数据采集、设备参数存储、黑匣子记录、或者任何需要“写不坏、掉电不丢”的嵌入式项目这套组合值得认真考虑。下面我会从硬件连接、SPI 配置、读写驱动、掉电保护策略到实际踩坑完整拆一遍。2. MR25H40CDF 与 PIC18F45K40 核心细节解析2.1 MR25H40CDF 的关键特性与引脚定义MR25H40CDF 是 Everspin 的 4Mbit MRAM采用 8 引脚 SOIC 封装工作电压 2.7V 到 3.6VSPI 模式支持 0 和 3最高时钟频率 40MHz。它的存储阵列是 512K × 8 位也就是 524288 个字节地址线需要 19 位所以读写命令里地址字段是 3 个字节24 位高 5 位忽略。这一点跟常见的 24 系列 EEPROM 用 2 字节地址不同写驱动的时候地址拼接千万别搞错否则会写到错误的页去。引脚方面8 个脚分别是CS片选低有效、SO从机输出即 MISO、SI从机输入即 MOSI、SCK时钟、VDD、VSS还有两个 NC 脚。注意 MR25H40CDF 没有 HOLD 和 WP 引脚这意味着它不需要像 Flash 那样通过 WP 引脚做硬件写保护写保护完全靠软件指令或者状态寄存器。这个设计简化了硬件连线但也意味着软件上要自己保证不会误写。状态寄存器里有一个WELWrite Enable Latch位每次写操作之前必须先发 WREN0x06指令把 WEL 置 1写完成后 WEL 自动清零。如果 WREN 没发或者发失败后续的 WRITE 指令会被直接忽略数据写不进去而且不会报错。这是新手最容易踩的坑读回来发现数据没变查半天以为是 SPI 时序问题其实是 WREN 没生效。我一般会在 WRITE 之后立刻读状态寄存器确认 WEL 已经清零作为写入完成的间接标志。2.2 PIC18F45K40 的 MSSP 模块与 SPI 主机配置PIC18F45K40 的 MSSP 模块可以工作在 SPI 或 I2C 模式做 SPI 主机时关键寄存器是 SSP1CON1、SSP1STAT、SSP1ADD。配置成 SPI 主机的基本步骤是先设置 SSP1STAT 的 CKE 位和 SSP1CON1 的 CKP 位来确定 SPI 模式然后设置 SSP1CON1 的 SSPM 位为 0b0000 或 0b0010 选择时钟频率最后把 SSPEN 置 1 使能模块。MR25H40CDF 支持模式 0 和模式 3我一般用模式 0即 CKP0、CKE0空闲时时钟低电平数据在上升沿采样。时钟频率的选择要算一下。PIC18F45K40 的 SPI 时钟来自系统时钟 FoscSSPM0b0000 时是 Fosc/4SSPM0b0010 时是 Fosc/64。假设系统时钟 64MHzFosc/4 就是 16MHzMR25H40CDF 最高支持 40MHz16MHz 完全在范围内。但实际布线如果比较长或者走线经过噪声环境建议降到 Fosc/16 或 Fosc/64先保证通信稳定再谈速度。我在一块四层板上用 16MHz 跑得很稳但在一块两层的实验板上 16MHz 偶尔会读到 0xFF降到 4MHz 就正常了所以速度不是越高越好要看板子。片选信号我强烈建议用硬件片选也就是用 MCU 的一个普通 GPIO 手动拉低拉高而不是依赖 MSSP 模块的 SS 引脚自动控制。原因很简单MR25H40CDF 的指令序列是“拉低 CS → 发指令 → 发地址 → 发/收数据 → 拉高 CS”整个过程必须在一个 CS 低电平周期内完成。如果用自动 SS中间一旦被中断打断CS 可能被意外释放MRAM 会认为指令结束后续数据就全乱了。手动控制 GPIO 虽然多写两行代码但时序完全可控这是工业项目里更稳妥的做法。2.3 SPI 通信协议与指令集要点MR25H40CDF 的指令集很精简常用的就几条WREN0x06写使能、WRDI0x04写禁止、RDSR0x05读状态寄存器、WRSR0x01写状态寄存器、READ0x03读数据、WRITE0x02写数据。没有扇区擦除指令因为 MRAM 不需要擦除这是它跟 Flash 最大的区别。写数据时直接发 WRITE 指令加 3 字节地址然后连续发数据字节地址自动递增可以一直写到 512KB 末尾。读数据用 READ 指令同样 3 字节地址然后连续读SO 引脚上会依次输出数据。这里有个细节MRAM 的读是非破坏性的读操作不会改变存储内容也不需要刷新所以可以放心地频繁读。另外MRAM 的写入是原子性的一个字节的写入要么完成要么没完成不会出现写了一半的中间状态这对掉电保护极其重要。时序参数方面CS 拉低到第一个 SCK 上升沿需要至少 5ns 的建立时间SCK 周期最小 25ns对应 40MHzCS 拉高到下一次拉低需要至少 10ns。这些参数在 16MHz 下都有充足余量不用特别担心。真正需要注意的是CS 拉高之后要等一段时间再发下一条指令因为 MRAM 内部需要时间完成写入。虽然 MRAM 写入很快但状态寄存器里的 WIPWrite In Progress位还是建议查一下或者至少延时几个微秒确保上一次写入彻底完成。3. 硬件连接与实操过程详解3.1 硬件连线与去耦电容布置先给一张接线对照表这是最基础的接错了后面全白搭。PIC18F45K40 引脚方向MR25H40CDF 引脚说明RC3SCK输出SCK脚 6SPI 时钟RC5SDO输出SI脚 5主机输出从机输入RC4SDI输入SO脚 2主机输入从机输出RB0GPIO输出CS脚 1手动片选低有效3.3V电源VDD脚 8供电GND地VSS脚 4共地注意 PIC18F45K40 的 SDO 是 RC5SDI 是 RC4别跟其他型号搞混。MR25H40CDF 的 SI 接 MCU 的 SDOSO 接 MCU 的 SDI这是标准 SPI 交叉接法。CS 我用了 RB0你也可以用任何空闲 GPIO只要在代码里对应改掉就行。去耦电容这块必须强调。MR25H40CDF 的 VDD 和 VSS 之间要放一个0.1μF 陶瓷电容尽量靠近芯片引脚走线越短越好。如果电源纹波比较大再并一个 1μF 或 10μF 的钽电容。我遇到过一块板子因为去耦电容离得太远SPI 读数据偶尔出错换了位置立刻就好。工业现场电源噪声大这个电容不是可选项是必选项。另外SPI 的四根信号线如果走线超过 10cm建议在 SCK 和 CS 上串 22Ω 到 33Ω 的电阻做阻抗匹配减少反射。3.2 SPI 初始化代码与参数计算下面这段是 PIC18F45K40 的 SPI 主机初始化代码用 XC8 编译器直接可以抄。#include xc.h #define CS_PIN LATBbits.LATB0 #define CS_TRIS TRISBbits.TRISB0 void SPI_Init(void) { // 配置 CS 为输出默认拉高 CS_TRIS 0; CS_PIN 1; // 配置 SCK、SDO 为输出SDI 为输入 TRISCbits.TRISC3 0; // SCK TRISCbits.TRISC5 0; // SDO TRISCbits.TRISC4 1; // SDI // MSSP1 配置为 SPI 主机模式 0 SSP1STAT 0x00; // CKE0SMP0模式 0 SSP1CON1 0x00; // 先清零 SSP1CON1bits.CKP 0; // 空闲时钟低 SSP1CON1bits.SSPM 0b0000; // Fosc/4 SSP1CON1bits.SSPEN 1; // 使能 MSSP // 清空接收缓冲 volatile unsigned char dummy SSP1BUF; (void)dummy; }时钟频率的计算假设系统时钟配置为 64MHzSSPM0b0000 对应 Fosc/4即 16MHz。SPI 时钟周期 62.5nsMR25H40CDF 最小 SCK 周期 25ns满足要求。如果系统时钟是 16MHzFosc/4 就是 4MHz更保守也完全够用。我一般先用 4MHz 调通再逐步往上提确认稳定后再定最终频率。SPI 收发一个字节的函数也很简单核心是写 SSP1BUF 然后等 BF 位SSP1STATbits.BF置 1再读 SSP1BUF。unsigned char SPI_ExchangeByte(unsigned char data) { SSP1BUF data; while (!SSP1STATbits.BF); // 等待收发完成 return SSP1BUF; }这个函数是阻塞式的在工业项目里如果 SPI 速度够快阻塞几微秒完全可以接受。如果不想阻塞可以用中断方式但代码复杂度会上升对于 MRAM 这种高速器件阻塞式反而更简单可靠。3.3 MRAM 读写驱动实现写数据之前必须先发 WREN这是铁律。下面这个函数封装了 WREN 加 WRITE 的完整流程。void MRAM_WriteEnable(void) { CS_PIN 0; SPI_ExchangeByte(0x06); // WREN CS_PIN 1; } void MRAM_Write(uint32_t addr, unsigned char *buf, uint16_t len) { MRAM_WriteEnable(); CS_PIN 0; SPI_ExchangeByte(0x02); // WRITE 指令 SPI_ExchangeByte((addr 16) 0xFF); // 地址高字节 SPI_ExchangeByte((addr 8) 0xFF); // 地址中字节 SPI_ExchangeByte(addr 0xFF); // 地址低字节 for (uint16_t i 0; i len; i) { SPI_ExchangeByte(buf[i]); } CS_PIN 1; // 等待写入完成MRAM 很快延时 1us 足够 __delay_us(1); }读数据更简单不需要 WREN。void MRAM_Read(uint32_t addr, unsigned char *buf, uint16_t len) { CS_PIN 0; SPI_ExchangeByte(0x03); // READ 指令 SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI_ExchangeByte(0x00); // 发 dummy 收数据 } CS_PIN 1; }地址拼接这里再强调一次MR25H40CDF 是 19 位地址但指令格式要求发 3 个字节高 5 位是无关位一般填 0。所以(addr 16) 0xFF取的是地址的第 16 到 23 位对于 512KB 空间最高位到第 18 位第 19 到 23 位始终是 0。如果你不小心把地址算错比如用了 2 字节地址格式那读出来的数据会完全错位而且很难查。3.4 掉电保护与数据完整性策略MRAM 本身掉电不丢数据但正在写入的那一瞬间掉电怎么办MRAM 的写入是原子性的一个字节的写入要么完成要么没完成不会出现半写状态。但如果你一次写多个字节比如写一条 32 字节的记录写到第 16 个字节时掉电那前 16 个字节写进去了后 16 个没写这条记录就不完整了。所以数据完整性要靠软件层面的双缓冲或日志结构来保证。我的做法是把存储区分成两个区域一个叫数据区一个叫索引区。数据区按固定大小的记录块顺序写入每条记录带一个 CRC 校验和。索引区存当前写入位置和记录计数。每次写新记录时先写数据区CRC 算好一起写进去然后再更新索引区。如果掉电发生在写数据区时索引区还是旧值上电后从索引区指向的位置重新写就行旧记录被覆盖不会影响已确认的数据。如果掉电发生在更新索引区时因为索引区只有一个字节或几个字节MRAM 的原子性保证它要么是旧值要么是新值不会中间态。还有一种更简单的做法用双份存储加标志位。关键参数存两份每份带一个有效标志。写的时候先写第二份再写第一份读的时候优先读第一份如果第一份 CRC 错就读第二份。这样即使写第一份时掉电第二份还是好的。这个方法代码简单适合参数不多的情况。注意MRAM 虽然写入快但也不要在中断里频繁写。SPI 通信本身需要时间如果在高优先级中断里写 MRAM可能阻塞其他中断导致系统实时性下降。建议把写操作放到主循环或者低优先级任务里用标志位触发。4. 常见问题与排查技巧实录4.1 SPI 通信失败排查速查表现象可能原因排查方法解决方法读回全是 0xFFCS 没拉低或接线错用示波器看 CS 和 SCK检查 CS 引脚定义和接线读回全是 0x00MISO 没接或 MRAM 没供电万用表测 VDD 电压检查供电和 MISO 连线数据偶尔错位SPI 时钟太快或走线太长降低时钟频率测试降速或加阻抗匹配电阻写入后读回没变WREN 没生效读状态寄存器 WEL 位确保 WREN 在 WRITE 之前发送地址错乱地址字节数不对检查地址拼接代码确认用 3 字节地址格式上电后数据丢失去耦电容不足或电源跌落示波器看 VDD 纹波加 0.1μF 和 10μF 电容这张表是我实际调试中总结出来的基本上覆盖了 90% 以上的问题。其中“写入后读回没变”是最常见的新手往往以为是 SPI 时序问题其实十有八九是 WREN 没发或者发完 WREN 之后 CS 拉高太早导致 WREN 没被 MRAM 识别。WREN 是一个独立指令发完之后必须拉高 CS 才能生效然后再拉低 CS 发 WRITE。如果你把 WREN 和 WRITE 放在同一个 CS 低电平周期里MRAM 是不认的。4.2 实操心得与避坑经验第一个坑CS 拉高之后不要立刻发下一条指令。MRAM 在 CS 上升沿之后需要一点时间完成内部操作虽然 datasheet 上写的是纳秒级但实际板子上如果电源不稳这个时间会变长。我的做法是在 CS 拉高之后加一个__delay_us(1)1 微秒对系统性能几乎没影响但能避免很多偶发问题。第二个坑不要用 SPI 自动片选。前面提过这里再强调一次。PIC 的 MSSP 模块有 SS 引脚可以自动控制片选但那个片选在字节之间会跳动不适合 MRAM 这种需要连续 CS 低电平的器件。手动 GPIO 控制虽然多写代码但时序完全可控。第三个坑写入数据量大的时候要分块。虽然 MRAM 支持连续写直到地址末尾但如果你一次写几 KB中间如果被中断打断CS 可能被意外拉高导致写入中断。我的做法是每 256 字节分一块每块写完拉高 CS再重新发 WREN 和 WRITE。这样即使中间被打断最多丢一块不会影响整个数据区。第四个坑上电初始化要加延时。MRAM 上电后需要一段时间稳定datasheet 上写的是 1ms 左右。我一般在上电后延时 10ms 再初始化 SPI 和访问 MRAM确保电源稳定。这个延时在工业设备上完全不是问题但能避免上电瞬间的误操作。4.3 性能实测与优化建议我在这套组合上做过实测SPI 时钟 16MHz写 256 字节耗时约 130μs读 256 字节耗时约 128μs。换算下来写入速度约 1.9MB/s读取速度约 2MB/s。这个速度对于工业数据记录完全够用一天记录 10 万条 32 字节的记录总数据量 3.2MB写入时间加起来不到 2 秒分散到全天更是可以忽略不计。如果要进一步优化可以把 SPI 时钟提到 20MHz 甚至更高但要看板子布线质量。另外读操作可以用 DMA 或者中断方式减少 CPU 占用。不过对于大多数工业应用阻塞式读写已经足够没必要过度优化。功耗方面MR25H40CDF 的待机电流典型值 1mA 左右写入电流约 10mA读取电流约 5mA。对于电池供电的设备可以在不访问时把 CS 拉高MRAM 会自动进入低功耗待机模式。PIC18F45K40 本身也有多种低功耗模式配合起来可以做到整体功耗很低。5. 方案扩展与个人体会这套 MRAM 加 PIC 的组合除了做数据记录还可以扩展成参数存储区、黑匣子、固件配置区。比如把设备的校准参数、网络配置、运行日志都放在 MRAM 里掉电不丢写入无寿命焦虑维护起来非常省心。如果容量不够MR25H40CDF 是 512KB还有更大容量的 MRAM 型号可以选接口和指令基本兼容换芯片只需要改地址位宽。我在实际使用中发现MRAM 最大的价值不是速度快而是让软件设计变简单了。用 Flash 的时候你要考虑擦除块大小、写前擦除、磨损均衡、掉电保护代码复杂且容易出 bug。用 MRAM 之后写就是写读就是读不需要擦除不需要均衡掉电原子性天然保证软件层面只需要处理多字节记录的完整性问题。这种简化带来的开发效率提升和后期维护成本降低远比芯片本身的差价有价值。最后分享一个小技巧如果你不确定 MRAM 是否正常工作可以先写一个字节到地址 0x00000然后读回来对比。如果读回来一致再写一个不同的值到地址 0x7FFFF最后一个字节再读回来。这两个地址覆盖了地址空间的最高位和最低位能快速验证地址线是否全部正常。这个方法比写一大段数据再对比要快得多调试初期非常实用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。