
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 复用主频跑到 64MHzPLL 后有 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; // CKE1, SMP0对应模式0 SSPCON1 0x20; // SSPEN1, CKP0, SSPM0000 (Fosc/4) TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入模式 0 的意思是CPOL0空闲低电平、CPHA0第一个边沿采样MR25H40CDF 两种模式都支持我一般用模式 0因为跟大多数 SPI 器件一致调试时逻辑分析仪抓波形好认。时钟频率这块要注意PIC18F85K90 在 64MHz 系统时钟下Fosc/4 就是 16MHzMRAM 能跑到 40MHz所以主控这边反而是瓶颈。如果你想要更快可以把 SSPM 设成 0001Fosc/16或者用 Fosc/4 已经够用。16MHz 的 SPI 时钟写 512KB 全片大概需要 512K × 8 / 16M ≈ 0.26 秒这个速度做数据记录绰绰有余。2.3 硬件连接与 PCB 布局注意事项接线很简单标准四线 SPI 加片选SCK → RC3SDI → RC4SDO → RC5CS → 任意 GPIO比如 RA5VCC → 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 → 发 WREN0x06→ 拉高 CS → 拉低 CS → 发 WRITE0x02→ 发 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 字节总共 512KB16MHz 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 是 CPOL0、CPHA0模式 3 是 CPOL1、CPHA1。如果你用模式 1 或模式 2数据会错位。PIC18 的 SSPSTAT 寄存器里CKE 位对应 CPHACKP 位对应 CPOL模式 0CKE1, CKP0模式 3CKE0, CKP1我一般用模式 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μs256 字节页写约 135μs512KB 全片写约 0.28 秒单字节读约 1.8μs512KB 全片读约 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典型 5nsCS 拉高前最后一个时钟沿到 CS 拉高也有延时tCHSH典型 5ns。PIC18 的 GPIO 操作有指令周期延迟一般自然满足但如果你用硬件 CSMSSP 的 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 效率最高。最后分享一个小技巧调试时先用低速 SPI1MHz确认通信正常再逐步提速。高速下如果出错很难判断是时序问题还是信号完整性问题。低速能通说明逻辑对提速出错就是硬件问题查走线和匹配电阻。这个顺序能省很多时间。