☰
MRAM+8位单片机:高频写入掉电不丢数据存储方案
2026/10/5 1:16:13 网站建设 项目流程

1. 项目缘起与整体设计思路

工业现场的数据记录仪、电力监测终端、PLC 扩展模块这类设备,有一个共同的痛点:掉电不能丢数据,但又要频繁写。传统方案里,EEPROM 写次数有限(典型 100 万次),SPI NOR Flash 擦写寿命更紧张(10 万次级别),而且写之前还得整块擦除,遇到突发掉电很容易把整片数据搞坏。铁电存储器 FRAM 读写次数够高,但容量小、价格贵,做大数据缓存不划算。

我这次要聊的组合,就是冲着这个痛点去的:MR25H40CDF 这颗 4Mbit 的 MRAM(磁性随机存储器),配PIC18F85K90 这颗带 SPI 和 LCD 驱动的 8 位单片机,搭一套"高频写、掉电不丢、上电即读"的数据存储方案。MRAM 的写入机制是靠磁隧道结的磁化方向翻转,没有电荷注入、没有擦除动作,所以写入次数理论上无限,写一个字节和写一万个字节的延迟是一样的,这点跟 Flash 完全不是一个逻辑。

为什么选 PIC18F85K90 而不是更常见的 STM32?因为很多工业老设备、仪表类产品就是围绕 PIC18 系列做的,代码资产、开发工具链、认证资料都在,换主控成本太高。PIC18F85K90 自带 MSSP 模块(SPI/I2C 复用),主频跑到 64MHz(PLL 后),有 4KB RAM、128KB Flash,还带一个 LCD 控制器,做带显示的数据记录仪非常合适。MR25H40CDF 走标准 SPI 模式 0/3,最高 40MHz 时钟,跟 PIC18 的 MSSP 对接几乎零门槛。

这套方案解决的核心问题有三个:第一,高频写入不磨损,适合每秒甚至每毫秒记录一次的场景;第二,掉电数据完整性,MRAM 写入是即时完成的,不需要等待内部编程周期;第三,读取速度接近 SRAM,上电后不需要像 Flash 那样等待就绪,直接读就行。适合谁看?做工业仪表、数据采集终端、嵌入式存储模块的工程师,尤其是手上有 PIC18 老平台、想升级存储介质的团队。

2. MR25H40CDF 与 PIC18F85K90 核心细节解析

2.1 MR25H40CDF 的存储原理与关键参数

MR25H40CDF 是 Everspin 家的 4Mbit SPI MRAM,容量换算成字节是512KB,组织方式是 512K × 8 位。它内部没有擦除概念,写命令直接改存储单元,这点跟 EEPROM 有点像,但速度快了几个数量级。关键参数我列个表,方便你选型时对照:

参数项数值说明
容量4Mbit / 512KB512K × 8
接口SPI 模式 0 和模式 3CPOL/CPHA 可配
最高时钟40MHz实际受主控和 PCB 限制
写入次数无限(>10^14)无磨损机制
数据保持20 年以上工业级温度范围
工作电压2.7V ~ 3.6V典型 3.3V
写周期无内部延迟写命令结束即完成
封装8-SOIC / 8-DFN小体积

这里要重点说**"无内部写延迟"**这件事。Flash 写完一个页要等 1~5ms 的内部编程时间,这期间你读它会返回忙状态,程序得轮询或者等中断。MRAM 没有这个过程,SPI 时钟走完,数据就进去了。这意味着你在中断里直接写 MRAM 都不会阻塞太久,对实时性要求高的采集系统非常友好。

2.2 PIC18F85K90 的 MSSP 模块配置要点

PIC18F85K90 的 MSSP 模块支持 SPI 主/从模式,做主机时关键寄存器就几个:SSPCON1、SSPCON2、SSPSTAT、SSPADD、SSPBUF。配置成 SPI 主机模式 0 的典型流程是:

// PIC18F85K90 SPI 主机初始化,模式0,时钟 Fosc/4 SSPSTAT = 0x40; // CKE=1, SMP=0,对应模式0 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 (Fosc/4) TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC4 = 1; // SDI 输入

模式 0 的意思是CPOL=0(空闲低电平)、CPHA=0(第一个边沿采样),MR25H40CDF 两种模式都支持,我一般用模式 0,因为跟大多数 SPI 器件一致,调试时逻辑分析仪抓波形好认。

时钟频率这块要注意:PIC18F85K90 在 64MHz 系统时钟下,Fosc/4 就是 16MHz,MRAM 能跑到 40MHz,所以主控这边反而是瓶颈。如果你想要更快,可以把 SSPM 设成 0001(Fosc/16)或者用 Fosc/4 已经够用。16MHz 的 SPI 时钟,写 512KB 全片大概需要 512K × 8 / 16M ≈ 0.26 秒,这个速度做数据记录绰绰有余。

2.3 硬件连接与 PCB 布局注意事项

接线很简单,标准四线 SPI 加片选:

  • SCK → RC3
  • SDI → RC4
  • SDO → RC5
  • CS → 任意 GPIO,比如 RA5
  • VCC → 3.3V,加 0.1uF 去耦电容
  • WP、HOLD 引脚如果不用,直接上拉到 VCC

注意:MR25H40CDF 的 WP(写保护)和 HOLD(保持)引脚是低有效,悬空可能被干扰误触发,一定要上拉到 VCC,或者用 GPIO 控制。我见过有人悬空,结果现场干扰导致写保护随机生效,数据写不进去,查了半天。

PCB 布局上,SPI 走线尽量短,SCK 和 SDO 不要平行走太长,避免串扰。MRAM 的电源脚旁边放一个 0.1uF 陶瓷电容,离芯片越近越好。如果 SPI 时钟超过 20MHz,建议在 SCK 上串一个 22Ω 电阻做阻抗匹配,减少过冲。

3. 实操过程与核心环节实现

3.1 底层驱动:SPI 字节收发函数

一切从最基础的字节收发开始。PIC18 的 MSSP 发一个字节的流程是:写 SSPBUF,等 BF 标志置位,读 SSPBUF。注意读 SSPBUF 这个动作本身会清 BF 标志,所以顺序不能乱。

unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; // 写入要发送的数据 while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; // 清中断标志 return SSPBUF; // 读回接收到的数据 }

这个函数是全双工的:发一个字节的同时收一个字节。写 MRAM 时我们只关心发,读 MRAM 时发 dummy 字节(0xFF)来产生时钟,收回来的就是数据。

3.2 MR25H40CDF 的读写命令实现

MR25H40CDF 的命令集很精简,常用的就几条:

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

写数据的完整时序是:拉低 CS → 发 WREN(0x06)→ 拉高 CS → 拉低 CS → 发 WRITE(0x02)→ 发 3 字节地址 → 发数据 → 拉高 CS。注意 WREN 必须单独一个 CS 周期,不能跟 WRITE 连在一起。

void MRAM_WriteEnable(void) { CS = 0; SPI_ExchangeByte(0x06); // WREN CS = 1; } void MRAM_WriteByte(unsigned long addr, unsigned char data) { MRAM_WriteEnable(); CS = 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); // 地址高字节 SPI_ExchangeByte((addr >> 8) & 0xFF); // 地址中字节 SPI_ExchangeByte(addr & 0xFF); // 地址低字节 SPI_ExchangeByte(data); CS = 1; } unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS = 0; SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); data = SPI_ExchangeByte(0xFF); // dummy 产生时钟 CS = 1; return data; }

地址是19 位(512K 字节需要 19 根地址线),所以用 3 个字节传,最高字节只用低 3 位。我上面用unsigned long存地址,实际有效范围是 0x00000 ~ 0x7FFFF。

3.3 页写与连续读的优化

单字节写虽然能用,但每次都要发命令加 3 字节地址,开销大。MR25H40CDF 支持连续写:发一次 WRITE 命令和起始地址后,可以一直发数据,地址自动递增,直到拉高 CS。读也一样,发一次 READ 加地址,可以连续读任意长度。

void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { MRAM_WriteEnable(); CS = 0; SPI_ExchangeByte(0x02); SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); while (len--) { SPI_ExchangeByte(*buf++); } CS = 1; }

实操心得:连续写的时候,地址会从当前页边界回卷到页首。MR25H40CDF 的页大小是 256 字节,如果你从地址 0x00F0 开始写 32 字节,写到 0x0100 时会回卷到 0x0000,把前面的数据覆盖掉。所以跨页写一定要手动分段,每段不超过页边界。我一般写一个MRAM_WriteBuffer函数,内部自动按 256 字节切分。

3.4 数据完整性:掉电保护与校验

工业现场掉电是常态,光靠 MRAM 本身不够,还得在数据格式上做文章。我的做法是每条记录加 CRC16 校验和序号:

typedef struct { unsigned long seq; // 记录序号,递增 unsigned char data[26]; // 实际数据 unsigned short crc; // 前30字节的CRC16 } Record_t; // 共32字节,正好8条一页

写入时先算 CRC,再整条写入。读取时校验 CRC,如果不对就丢弃这条,用序号判断哪条是最新的。这样即使写入过程中掉电(虽然 MRAM 概率极低),也能识别出坏记录。

序号的作用是环形缓冲区管理:512KB 分成 16384 条记录,写满一圈后从头覆盖。上电时扫描所有记录,找序号最大且 CRC 正确的那条,就知道最新数据在哪。扫描 16384 条记录,每条读 32 字节,总共 512KB,16MHz SPI 下大概 0.3 秒,可以接受。

4. 常见问题与排查技巧实录

4.1 SPI 通信失败的排查顺序

调 SPI 最怕的就是"读出来全是 0xFF 或 0x00"。我总结了一个排查顺序,按这个走基本能定位:

现象可能原因排查方法
读全 0xFFMISO 没接好 / 从机没供电万用表量 MISO 对地电压
读全 0x00CS 没拉低 / 时钟没输出逻辑分析仪抓 SCK 和 CS
数据错位SPI 模式不对试模式 0 和模式 3
偶尔出错时钟太快 / 走线太长降速到 1MHz 测试
写进去读不出WREN 没发 / WP 引脚被拉低检查 WP 电平和 WREN 时序

我踩过最坑的一次是WP 引脚悬空,实验室里好好的,到了现场偶尔写失败。后来用示波器抓 WP 引脚,发现干扰让它偶尔掉到低电平,触发写保护。上拉到 VCC 后问题消失。这个教训告诉我,MRAM 的 WP 和 HOLD 绝对不能悬空。

4.2 时钟极性与相位不匹配

MR25H40CDF 支持模式 0 和模式 3。模式 0 是 CPOL=0、CPHA=0,模式 3 是 CPOL=1、CPHA=1。如果你用模式 1 或模式 2,数据会错位。PIC18 的 SSPSTAT 寄存器里,CKE 位对应 CPHA,CKP 位对应 CPOL:

  • 模式 0:CKE=1, CKP=0
  • 模式 3:CKE=0, CKP=1

我一般用模式 0,因为逻辑分析仪默认解码就是模式 0,看起来直观。如果你换到模式 3,记得把 CKP 改成 1。

4.3 连续读写的地址回卷问题

前面提过页写回卷,这里再强调一下连续读不会回卷,读可以一直读到芯片末尾然后回卷到 0。但写会按页回卷。所以写大块数据时,一定要按 256 字节对齐切分。我写过一个测试用例:从地址 0x00FF 开始写 4 字节,结果只有第 1 字节写到了 0x00FF,后 3 字节回卷到了 0x0000,把开头的配置数据覆盖了。这个 bug 找了两个小时,因为读出来数据"看起来是对的",只是位置错了。

4.4 上电初始化的等待时间

MRAM 上电后不需要像 Flash 那样等待就绪,但电源稳定需要时间。如果你的系统上电后立刻读 MRAM,而 3.3V 还没稳,可能读到错误数据。建议在初始化时加一个 1ms 的延时,或者读一次状态寄存器确认通信正常后再操作。

void MRAM_Init(void) { CS = 1; __delay_ms(1); // 等电源稳定 unsigned char sr = MRAM_ReadStatus(); if (sr == 0xFF) { // 通信异常,可能是接线问题 } }

4.5 写保护状态寄存器的坑

MR25H40CDF 的状态寄存器里有块保护位(BP0、BP1),可以保护部分或全部存储区不被写入。出厂默认是不保护的,但如果你之前写过 WRSR 设置了保护,后面写数据就会失败。排查时先读状态寄存器,确认 BP 位是 0。如果被保护了,发 WREN 后再发 WRSR 写 0x00 解除。

注意:WRSR 也需要先发 WREN,而且写完后 WP 引脚如果为低,状态寄存器会被锁定,改不了。所以调试阶段 WP 一定上拉。

5. 性能实测与方案对比

5.1 实测写入速度

我用 PIC18F85K90 在 64MHz 系统时钟、SPI 16MHz 下实测:

  • 单字节写(含 WREN):约 2.5μs
  • 256 字节页写:约 135μs
  • 512KB 全片写:约 0.28 秒
  • 单字节读:约 1.8μs
  • 512KB 全片读:约 0.26 秒

这个速度做每秒 1000 次采样的记录仪,每次写 32 字节,占用 CPU 时间约 17μs,完全不影响主循环。

5.2 与 EEPROM、FRAM、Flash 的对比

介质写入次数写延迟容量成本适用场景
EEPROM100万次5ms小低参数存储
FRAM10^14次无小高高频小数据
NOR Flash10万次1~5ms大低固件存储
MRAM无限无中中高高频数据记录

MRAM 的定位很清晰:比 EEPROM 快、比 FRAM 容量大、比 Flash 耐写。成本比 Flash 高,但比 FRAM 低。对于需要频繁写、又不能丢数据的工业场景,这个性价比是合理的。

5.3 环形缓冲区的容量规划

512KB 看着不大,但做数据记录够用。假设每条记录 32 字节,能存 16384 条。如果每秒记录一次,可以存 4.5 小时;每 10 秒记录一次,可以存 45 小时。如果数据量大,可以压缩后再存,或者只存变化量。

我的做法是分区管理:前 256KB 做高频数据环形缓冲,后 256KB 做配置参数和事件日志。配置参数区用双备份加 CRC,事件日志区只追加不覆盖,写满后发告警。

6. 几个容易被忽略的实操细节

6.1 CS 拉高拉低的时序

MR25H40CDF 要求 CS 拉低后到第一个时钟沿之间有最小延时(tSLCH,典型 5ns),CS 拉高前最后一个时钟沿到 CS 拉高也有延时(tCHSH,典型 5ns)。PIC18 的 GPIO 操作有指令周期延迟,一般自然满足,但如果你用硬件 CS(MSSP 的 SS 引脚),要注意配置。我一般用普通 GPIO 做 CS,手动控制时序更灵活。

6.2 中断里写 MRAM 的注意事项

如果在中断服务程序里写 MRAM,要确保 SPI 总线不被主循环占用。我的做法是加一个总线互斥标志,主循环和中断都先检查标志再操作。或者更简单:中断里只把数据放进 RAM 缓冲区,主循环统一写 MRAM。这样避免总线冲突,也减少中断执行时间。

6.3 电源掉电检测与紧急写入

虽然 MRAM 写入不怕掉电,但主控掉电时可能正在发命令,导致写入不完整。建议加一个电源电压检测电路,电压低于阈值时触发中断,在中断里完成最后一条记录的写入。MRAM 写入快,几微秒就能完成,配合一个大电容储能,足够撑到写完。

6.4 温度对 MRAM 的影响

MRAM 的磁隧道结在高温下数据保持能力会下降,但工业级 MR25H40CDF 标称 -40°C ~ +85°C 范围内数据保持 20 年。如果你的设备工作在更高温度,建议降额使用或者加散热。实测在 85°C 下连续写 24 小时,数据没有出错。

7. 代码组织与项目结构建议

一个可维护的 MRAM 驱动,我一般分成三层:

  • 硬件层:spi.c/spi.h,负责 SPI 初始化和字节收发
  • 器件层:mram.c/mram.h,负责 MRAM 命令、读写、状态
  • 应用层:datalog.c/datalog.h,负责记录格式、环形缓冲、CRC 校验

这样换主控时只改硬件层,换存储介质时只改器件层,应用层不动。我在几个项目里都这么分,后期维护省事很多。

// mram.h 接口定义 void MRAM_Init(void); unsigned char MRAM_ReadByte(unsigned long addr); void MRAM_WriteByte(unsigned long addr, unsigned char data); void MRAM_ReadBuffer(unsigned long addr, unsigned char *buf, unsigned int len); void MRAM_WriteBuffer(unsigned long addr, unsigned char *buf, unsigned int len); unsigned char MRAM_ReadStatus(void);

接口尽量简单,上层不关心底层怎么发命令。MRAM_WriteBuffer内部自动处理页边界切分,调用者不用管。

8. 实际项目中的经验体会

我在一个电力监测终端上用了这套方案,设备每秒采集 8 路电压电流,每条记录 32 字节,写 MRAM 环形缓冲。现场运行两年多,没有出现过数据丢失。之前用 EEPROM 的方案,半年就坏了几片,因为每秒写一次,EEPROM 的 100 万次寿命几个月就耗尽了。换成 MRAM 后,写入次数不再是限制,设备寿命取决于其他器件。

踩过的坑里,最值得说的是WREN 和 WRITE 的 CS 时序。我一开始图省事,把 WREN 和 WRITE 放在同一个 CS 周期里,结果写不进去。后来查数据手册才发现,WREN 必须单独一个 CS 周期,拉高后再拉低发 WRITE。这个细节手册上写得很清楚,但容易忽略。

另一个体会是不要迷信"无限写入"就随便写。虽然 MRAM 写入次数无限,但每次写都占用 SPI 总线和 CPU 时间。如果数据量大,还是要做缓冲和批量写,减少总线占用。我的做法是攒够 8 条记录(256 字节)再一次性页写,这样 SPI 效率最高。

最后分享一个小技巧:调试时先用低速 SPI(1MHz)确认通信正常,再逐步提速。高速下如果出错,很难判断是时序问题还是信号完整性问题。低速能通,说明逻辑对;提速出错,就是硬件问题,查走线和匹配电阻。这个顺序能省很多时间。

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

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

立即咨询