☰
工业掉电场景下MRAM数据存储方案:PIC18F46K20与MR25H40CDF SPI驱动实战
2026/10/5 6:08:42 网站建设 项目流程

1. 项目缘起与方案选型思考

工业现场的数据记录仪、PLC 扩展模块、电力监测终端这类设备,有一个绕不开的痛点:频繁掉电场景下的关键数据保存。我手上这个项目就是给一台工业称重仪表做数据存储升级,要求每秒记录一次称重数据,断电后数据不能丢,而且设备可能随时被拉闸,没有任何"安全关机"的窗口期。

原来的方案用的是 EEPROM(类似 24C 系列),问题很明显:写入速度慢,字节写入周期动辄 5ms,一秒一次勉强够用,但如果要提速到 10Hz 记录,EEPROM 根本扛不住。后来考虑过 FRAM,速度快、寿命长,但容量小、价格高,而且供货周期不稳定。再后来评估 SPI Flash(W25Q 系列),容量大、便宜,但写入前要擦除扇区,擦除时间长,掉电时正在擦除的扇区数据全丢,而且 Flash 有写寿命限制,频繁写同一地址很快就坏。

最终选定的方案是MR25H40CDF 这颗 MRAM(磁性随机存储器),主控用PIC18F46K20,通过SPI 接口通信。MRAM 的核心优势在于:写入不需要擦除、字节级写入、写入速度接近 SRAM、几乎无限次擦写寿命(官方标称 10^14 次以上)、掉电后数据保持 20 年以上。这几个特性几乎完美匹配工业数据记录的需求。

PIC18F46K20 是 Microchip 的一款经典 8 位单片机,64KB Flash、3936 字节 RAM、64KB 外部存储空间寻址能力,自带 MSSP 模块支持 SPI 主模式,主频最高 64MHz(用 PLL 倍频后),在工业控制领域用量极大,资料齐全,开发工具成熟。用它来驱动 MR25H40CDF,属于"老将配新兵"的组合,稳定性和开发效率都能兼顾。

这篇文章我会把整个方案从选型逻辑、硬件连接、SPI 时序配置、读写驱动实现、掉电保护策略到实际调试踩坑,完整地拆一遍。适合正在做工业嵌入式数据存储、或者想了解 MRAM 实际用法的朋友参考。即使你用的是 STM32 或者其他平台,SPI 操作 MRAM 的核心逻辑是通用的,移植起来不难。

2. MR25H40CDF 与 PIC18F46K20 核心特性拆解

2.1 MR25H40CDF 到底强在哪

MR25H40CDF 是 Everspin 公司的一款 4Mbit(512KB)SPI MRAM。注意单位,是 4Mbit,换算成字节是 512KB,不是 4MB,这个容量在数据记录场景里够用,但如果你要存大量历史曲线,得做好分页管理和滚动覆盖策略。

它的关键参数我整理成表格,方便对照:

参数项数值说明
容量4Mbit / 512KB按字节寻址
接口SPI(Mode 0 和 Mode 3 都支持)最高 40MHz 时钟
写入方式字节级、页级均可无需擦除
写入寿命10^14 次以上几乎不用考虑磨损
数据保持20 年掉电不丢
工作电压2.7V ~ 3.6V典型 3.3V
写入周期无内部写等待写命令发出即完成
封装8-SOIC / 8-DFN引脚兼容部分 SPI Flash

这里要重点说"无需擦除"和"无内部写等待"这两点。SPI Flash 写入前必须先发擦除命令,擦除一个 4KB 扇区典型要 50~100ms,这期间芯片忙,你得轮询状态寄存器等它完成。如果这时候掉电,整个扇区数据可能处于不确定状态。MRAM 完全没有这个问题,写命令发完,数据就进去了,不需要等待,也不需要擦除。对于掉电频繁的工业场景,这是决定性的优势。

2.2 PIC18F46K20 的 MSSP 模块

PIC18F46K20 内部集成了 MSSP(Master Synchronous Serial Port)模块,可以配置成 SPI 或 I2C。做 SPI 主设备时,它支持四种 SPI 模式(由 CKP 和 CKE 位组合决定),时钟极性、相位都可配。最高 SPI 时钟是 Fosc/4,如果系统跑 64MHz,SPI 时钟最高能到 16MHz,驱动 MR25H40CDF 绰绰有余。

MSSP 相关寄存器主要有这几个:

  • SSPCON1:控制位,包括 CKP(时钟极性)、SSPM(模式选择,SPI 主模式设为 0b0000 或 0b0001)
  • SSPSTAT:状态位,CKE 位决定时钟边沿
  • SSPBUF:数据缓冲,写入即发送,读取即接收
  • SSPADD:在 SPI 主模式下用于设置时钟分频
  • PIR1 的 SSPIF 位:中断标志,发送/接收完成时置位

配置成 SPI 主模式的核心代码大概是这样:

// PIC18F46K20 SPI 主模式初始化 // 假设系统时钟 64MHz,SPI 时钟 = Fosc/16 = 4MHz SSPSTAT = 0x40; // CKE=1,数据在时钟上升沿采样(对应 SPI Mode 0 的变体,需结合 CKP) SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 -> SPI Master, clock = Fosc/4 // 如果要降速,用 SSPADD 设置分频 // 注意:PIC18 的 SPI 主模式时钟由 SSPADD 决定,公式为 Fosc/(4*(SSPADD+1)) SSPADD = 3; // 64MHz / (4*(3+1)) = 4MHz

这里有个容易搞混的点:PIC18 的 SPI 主模式时钟计算和 STM32 不一样。STM32 是直接分频,PIC18 是通过 SSPADD 寄存器算的,公式是Fosc / (4 × (SSPADD + 1))。我一开始按 STM32 的思路去配,结果时钟差了 4 倍,读出来的数据全是乱的,后来查手册才发现这个公式。

2.3 为什么不用硬件片选

PIC18F46K20 的 MSSP 模块在 SPI 主模式下,片选信号(CS)需要软件控制,它没有专门的硬件 CS 引脚。这一点和 STM32 的 SPI 外设不同,STM32 可以配置 NSS 硬件管理,也可以软件管理。PIC18 这边,你只能用一个普通 GPIO 来拉 CS。

软件片选的好处是灵活,总线上可以挂多个 SPI 从设备,每个设备用不同 GPIO 控制 CS。坏处是每次读写都要手动拉低、拉高,代码里不能忘。我踩过的坑是:有一次在中断里读 MRAM,忘了在函数开头拉低 CS,结果数据读出来全是 0xFF,查了半天才发现是 CS 没动。

提示:软件片选时,CS 拉低到第一个时钟沿之间要留至少 5ns 的建立时间,MR25H40CDF 手册里叫 tSLCH。PIC18 跑 4MHz SPI 时,指令周期是 62.5ns,天然满足,但如果 SPI 跑到 16MHz,就要注意在 CS 拉低后加一个 NOP 或者短延时。

3. 硬件连接与 PCB 布局要点

3.1 引脚连接对照

MR25H40CDF 是 8 引脚封装,引脚定义和标准 SPI Flash 基本一致,这给硬件替换带来了便利。具体连接如下:

MR25H40CDF 引脚名称连接到 PIC18F46K20说明
1CSRC2(GPIO)软件片选,低有效
2SORC4(SDI)主入从出
3WP接 VDD 或 GPIO写保护,低有效
4VSSGND地
5SIRC5(SDO)主出从入
6SCKRC3(SCK)时钟
7HOLD接 VDD 或 GPIO暂停通信,低有效
8VDD3.3V电源

WP 和 HOLD 这两个引脚,如果不用硬件保护功能,直接上拉到 VDD 就行。我建议 WP 用一个 GPIO 控制,这样可以在固件里做写保护,防止程序跑飞时误写关键区域。HOLD 一般不用,直接拉高。

3.2 PCB 布局的几条经验

SPI 总线跑 4MHz 时,PCB 布局要求不算苛刻,但如果想跑到 16MHz 以上,就得注意了。我总结了几条实际踩坑经验:

第一,SCK 走线尽量短,且不要和 SI/SO 长距离并行。并行走线会产生串扰,SCK 的跳变会耦合到数据线上,导致误码。如果必须并行,中间加地线隔离。

第二,CS 走线不要和 SCK 靠太近。CS 是控制信号,如果被 SCK 干扰,可能出现误触发。我在一块板子上把 CS 和 SCK 走了并排 5cm,结果偶尔读出错数据,后来把 CS 绕开,问题消失。

第三,电源去耦电容必须加。MR25H40CDF 的 VDD 引脚旁边放一个 0.1uF 陶瓷电容,越近越好。MRAM 写入瞬间电流会有波动,去耦不好会导致写入失败。我实测过,去掉这个电容后,连续写入 1000 次里会出现 1~2 次错误。

第四,地平面要完整。双层板的话,底层尽量铺完整地平面,不要被其他信号割裂。SPI 的返回电流需要地平面提供低阻抗路径。

注意:MR25H40CDF 的 WP 和 HOLD 引脚内部有弱上拉,但不要依赖它。工业环境干扰大,建议外部加 10K 上拉电阻到 VDD,确保引脚状态确定。

4. SPI 读写驱动实现与关键命令解析

4.1 MR25H40CDF 的命令集

MR25H40CDF 的命令集比 SPI Flash 简单很多,因为它不需要擦除。核心命令就几个:

命令十六进制码功能
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

注意,MRAM 的写操作必须先发 WREN 命令,否则写不进去。这一点和 SPI Flash 一样。但不同的是,MRAM 的 WREN 之后不需要等待,直接发 WRITE 命令就行。SPI Flash 的 WREN 之后要等状态寄存器的 WEL 位置位,MRAM 没这个要求。

状态寄存器的 bit0 是 WEL(写使能锁存),bit1 是 BP0,bit2 是 BP1,用于块保护。读状态寄存器可以确认写使能是否生效,调试时很有用。

4.2 底层字节收发函数

PIC18 的 SPI 收发是同一个寄存器 SSPBUF,写进去就发送,读出来就是接收。底层函数这样写:

// SPI 收发一个字节 unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; // 写入数据,启动发送 while (!PIR1bits.SSPIF); // 等待发送/接收完成 PIR1bits.SSPIF = 0; // 清除标志 return SSPBUF; // 返回接收到的数据 }

这个函数是阻塞式的,简单可靠。如果系统对实时性要求高,可以用中断方式,但 SPI 事务通常很短,阻塞几微秒影响不大。

4.3 读数据函数实现

读 MRAM 的流程是:拉低 CS -> 发 READ 命令(0x03)-> 发 3 字节地址(24 位地址,因为 512KB 需要 19 位地址,用 3 字节对齐)-> 连续读数据 -> 拉高 CS。

// 从指定地址读取指定长度数据 void MRAM_Read(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; CS_LOW(); // 拉低片选 SPI_ExchangeByte(0x03); // READ 命令 SPI_ExchangeByte((addr >> 16) & 0xFF); // 地址高字节 SPI_ExchangeByte((addr >> 8) & 0xFF); // 地址中字节 SPI_ExchangeByte(addr & 0xFF); // 地址低字节 for (i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0x00); // 发送哑字节,读取数据 } CS_HIGH(); // 拉高片选 }

这里地址是 24 位的,虽然 MR25H40CDF 只有 512KB(19 位地址),但命令格式要求发 3 字节地址,高位补 0 即可。如果你换成更大容量的 MRAM(比如 16Mbit 的 MR25H16),地址还是 3 字节,代码不用改。

4.4 写数据函数实现

写数据的流程多一步 WREN:拉低 CS -> 发 WREN(0x06)-> 拉高 CS -> 拉低 CS -> 发 WRITE(0x02)-> 发地址 -> 发数据 -> 拉高 CS。

// 向指定地址写入指定长度数据 void MRAM_Write(unsigned long addr, const unsigned char *buf, unsigned int len) { unsigned int i; // 第一步:写使能 CS_LOW(); SPI_ExchangeByte(0x06); // WREN CS_HIGH(); // 第二步:写数据 CS_LOW(); SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS_HIGH(); }

注意 WREN 和 WRITE 之间必须拉高 CS 再拉低,这是 SPI 事务的分界。有些朋友图省事,WREN 之后不拉高 CS 直接发 WRITE,这样是不行的,MRAM 会把 WREN 和 WRITE 当成一个事务,命令解析出错。

4.5 页写入与跨页处理

MR25H40CDF 支持页写入,一页是 256 字节。如果你写入的数据超过 256 字节,且跨越了页边界,地址会自动回绕到页首,覆盖前面的数据。这是个坑,我第一次用的时候没注意,写了 300 字节,结果后 44 字节覆盖了前 44 字节,数据全乱了。

解决办法是在写入函数里做跨页判断:

void MRAM_WriteSafe(unsigned long addr, const unsigned char *buf, unsigned int len) { unsigned int pageRemain; while (len > 0) { pageRemain = 256 - (addr % 256); // 当前页剩余空间 if (pageRemain > len) pageRemain = len; MRAM_Write(addr, buf, pageRemain); addr += pageRemain; buf += pageRemain; len -= pageRemain; } }

这个函数会自动把长数据拆成不跨页的块,逐块写入。虽然多几次 CS 拉低拉高,但数据安全。

5. 掉电保护与数据完整性策略

5.1 掉电检测电路

工业现场掉电是随机的,你不可能预测。所以需要一个掉电检测电路,在电源电压降到阈值以下时,给单片机一个中断,让它在电容储能耗尽前把关键数据写进 MRAM。

典型做法是用一个电压比较器(比如 TL431 或者专用监控芯片),设定阈值 3.0V。当 VDD 降到 3.0V 时,比较器输出翻转,触发 PIC18 的外部中断(INT0 或 INT1)。中断服务程序里立刻把 RAM 中的关键数据写入 MRAM。

PIC18F46K20 的中断响应时间很快,从触发到进入 ISR 大概几个微秒。MRAM 写入 256 字节,按 4MHz SPI 算,每字节 8 个时钟周期,加上命令和地址开销,大概 600 微秒。电源端加一个 1000uF 电容,3.3V 降到 2.7V 的时间足够完成写入。

5.2 双备份与校验

即使有掉电检测,也不能保证 100% 可靠。我的做法是双备份 + CRC 校验。关键数据存两份,地址 A 和地址 B,每份后面跟一个 CRC16 校验值。读取时先读 A,校验通过就用 A;不通过再读 B,校验通过就用 B;两个都不通过,用默认值并报警。

typedef struct { unsigned char data[64]; unsigned short crc; } Record_t; #define ADDR_A 0x00000 #define ADDR_B 0x00100 void SaveRecord(Record_t *rec) { rec->crc = CRC16(rec->data, 64); MRAM_WriteSafe(ADDR_A, (unsigned char*)rec, sizeof(Record_t)); MRAM_WriteSafe(ADDR_B, (unsigned char*)rec, sizeof(Record_t)); } int LoadRecord(Record_t *rec) { MRAM_Read(ADDR_A, (unsigned char*)rec, sizeof(Record_t)); if (CRC16(rec->data, 64) == rec->crc) return 0; // A 有效 MRAM_Read(ADDR_B, (unsigned char*)rec, sizeof(Record_t)); if (CRC16(rec->data, 64) == rec->crc) return 0; // B 有效 return -1; // 都无效 }

这个策略的代价是存储空间翻倍,但 512KB 容量下,存 64 字节记录可以存 4000 条,翻倍后还有 2000 条,够用。

5.3 写入顺序与原子性

双备份写入时,顺序很重要。如果先写 A 再写 B,写 A 的过程中掉电,A 可能损坏,但 B 还是旧的完整数据,读取时 B 能通过校验。如果先写 B 再写 A,同理。关键是两份数据不能同时处于写入状态。

我的做法是:先写 A,延时 1ms,再写 B。这个延时确保 A 的写入完全结束。MRAM 写入本身很快,1ms 足够。如果掉电发生在写 A 期间,A 损坏,B 完好;如果发生在写 A 之后、写 B 之前,A 是新数据,B 是旧数据,读取时 A 校验通过,用 A;如果发生在写 B 期间,A 是新数据,B 损坏,读取时 A 通过,用 A。所有情况都能保证至少有一份完整数据。

6. 实际调试中的常见问题与排查

6.1 读出来全是 0xFF 或 0x00

这是最常见的问题,原因通常有三个:

第一,CS 没拉低。检查代码里 CS_LOW() 是否执行。我遇到过宏定义写错,CS_LOW() 实际是拉高,结果读出来全是 0xFF。

第二,SPI 模式不对。MR25H40CDF 支持 Mode 0 和 Mode 3。Mode 0 是 CPOL=0, CPHA=0,Mode 3 是 CPOL=1, CPHA=1。PIC18 的 CKP 和 CKE 组合要对应。如果模式错了,数据会移位,读出来全是乱码。我建议先用 Mode 0,CKP=0, CKE=1(PIC18 的 CKE 定义和标准 CPHA 相反,注意看手册)。

第三,时钟太快。MR25H40CDF 最高 40MHz,但如果你 PCB 布局不好,或者杜邦线连接,4MHz 以上就可能出错。调试时先把 SPI 降到 1MHz,确认能读写后再提速。

6.2 写入后读出来不对

写入数据后立刻读,发现数据不对,可能的原因:

  • WREN 没发或者没生效。读状态寄存器确认 WEL 位是否为 1。如果 WREN 后 WEL 还是 0,说明 WREN 命令没被正确接收,检查 CS 时序。
  • 跨页写入。前面说过,超过 256 字节且跨页会回绕。用 MRAM_WriteSafe 函数避免。
  • 写入过程中被中断打断。如果 SPI 事务被高优先级中断打断,CS 可能被意外拉高,导致写入不完整。解决办法是在 SPI 事务期间关中断,或者用互斥锁保护。

6.3 掉电后数据丢失

如果掉电检测电路正常,但数据还是丢了,检查:

  • 掉电中断优先级。确保掉电中断是最高优先级,不会被其他中断阻塞。
  • 写入时间是否足够。用示波器看 VDD 下降曲线,确认从掉电中断触发到电压降到 MRAM 最低工作电压(2.7V)之间的时间,是否大于写入所需时间。如果不够,加大电容或者减少每次写入的数据量。
  • MRAM 的 WP 引脚状态。如果 WP 被拉低,写入会被禁止。检查 WP 引脚是否意外被拉低。

6.4 常见问题速查表

现象可能原因排查方法
读出全 0xFFCS 未拉低、SPI 模式错、时钟太快查 CS 宏、降速、换模式
读出全 0x00MISO 未连接、从设备未供电查硬件连接、测 VDD
写入无效WREN 未发、WP 拉低、跨页读状态寄存器、查 WP、用安全写函数
数据偶尔出错电源干扰、CS 串扰、去耦不足加去耦电容、改 PCB 走线
掉电丢数据中断优先级低、电容不够调中断、加大电容、减写入量

7. 性能实测与优化建议

7.1 实测数据

我在 4MHz SPI 时钟下做了实测,结果如下:

操作数据量耗时备注
读256 字节约 520us含命令和地址开销
写256 字节约 540us含 WREN 和 WRITE
读64 字节约 140us适合高频记录
写64 字节约 150us每秒 100 次无压力

如果把 SPI 提到 16MHz,耗时会降到 1/4 左右。但 16MHz 对 PCB 布局要求高,杜邦线连接肯定不行,必须 PCB 走线。

7.2 优化建议

第一,批量写入。如果每秒记录一次,不要每次只写 64 字节,可以攒 10 次写一次 640 字节。虽然单次写入时间长了,但总开销(CS 拉低拉高、WREN)减少了。实测批量写入 640 字节比 10 次写 64 字节节省约 30% 时间。

第二,用 DMA。PIC18F46K20 没有 DMA,但如果你换成 PIC24 或者 dsPIC 系列,可以用 DMA 驱动 SPI,CPU 占用率大幅降低。不过对于这个项目,4MHz SPI 下 CPU 占用率本来就不高,DMA 不是必须的。

第三,地址对齐。MRAM 支持任意地址读写,但如果你按页对齐(256 字节边界)写入,可以避免跨页拆分,代码更简单,速度也略快。

第四,减少 WREN 次数。如果连续写多个不连续地址,可以在第一次 WREN 后连续发多个 WRITE 命令,只要 CS 不拉高,WEL 位保持有效。但要注意,每次 WRITE 之间要拉高 CS 再拉低,否则命令解析会出错。这个技巧在写多个分散数据块时有用。

8. 方案扩展与移植思路

这套方案的核心是 SPI 读写 MRAM,换平台主要改 SPI 底层驱动。如果你用 STM32,把 SPI_ExchangeByte 换成 HAL_SPI_TransmitReceive 就行,上层读写逻辑不用动。如果你用 ESP32,SPI 主机驱动更复杂一些,但原理一样。

MR25H40CDF 的容量是 512KB,如果不够用,可以换 MR25H16(2MB)或者 MR25H40(512KB 的工业级版本)。命令集完全兼容,代码不用改,只需要注意地址范围。

如果项目对成本敏感,可以考虑用 FRAM 替代 MRAM,比如 FM25V05(512Kbit),价格便宜一些,但容量小、速度慢一点。或者用带电池备份的 SRAM,成本更低,但电池需要维护,工业现场不太喜欢。

最后分享一个我在实际项目中的小技巧:在 MRAM 的末尾区域存一份配置参数和校准数据,这些数据不常变,但很重要。用 WP 引脚保护这个区域,防止程序跑飞时误写。配置区用单独的 CRC 校验,启动时读取,校验失败就用默认值并记录故障标志。这样即使 MRAM 部分区域损坏,设备还能以默认参数运行,不会完全瘫痪。

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

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

立即咨询