
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机工业现场的数据记录有个很尴尬的夹缝需求数据量不大但写入频繁、掉电不能丢、环境温度还经常在零下四十度到八十五度之间横跳。传统方案里EEPROM 擦写寿命十万次左右铁电存储器容量小价格高带电池的 SRAM 又要维护电池锂电池在低温下直接罢工。我前两年做一个电力监测终端现场要求每秒钟记录一次三相电流电压的瞬时值一天就是八万六千多条记录EEPROM 根本扛不住这种写入强度三个月不到就写废了。后来接触到 MRAM也就是磁性随机存储器这东西的原理是用磁隧道结的磁化方向来存数据写入没有擦除过程寿命理论上无限读写速度接近 SRAM掉电数据保持二十年以上工作温度范围也宽。MR25H40CDF 就是 Everspin 出的 4Mbit SPI 接口 MRAM512K 字节容量工业级温度范围SOIC-8 封装硬件上跟普通 SPI Flash 几乎一样接线但行为完全不同。主控这边我选了 PIC18F86J15Microchip 的 8 位机64K Flash、3936 字节 RAM自带 SPI 模块工作电压 2.0V 到 3.6V工业级温度范围价格便宜供货稳在工业仪表里用了很多年工具链成熟调试器便宜适合这种不需要跑操作系统的场景。这个组合的核心价值在于用最低的硬件成本实现高可靠的数据存储不需要文件系统不需要操作系统裸机跑 SPI 读写就能满足大部分工业记录需求。适合做工业仪表、电力监测、环境采集、设备黑匣子这类产品的嵌入式工程师参考尤其是那些被 EEPROM 寿命和掉电丢数据折磨过的朋友。1.2 方案对比为什么不是 Flash、不是 EEPROM、不是 FRAM选存储介质这件事我一般从五个维度打分写入寿命、写入速度、掉电保持、接口复杂度、单位容量成本。把几个候选拉出来对比一下就很清楚了。存储介质擦写寿命写入速度掉电保持接口工业温度成本EEPROM约 100 万次慢毫秒级好I2C/SPI部分支持低NOR Flash约 10 万次慢需擦除好SPI支持低FRAM约 10 万亿次快好I2C/SPI支持高MRAM近乎无限快纳秒级好SPI支持中带电池 SRAM无限快依赖电池并口/SPI电池受限中MRAM 在这个表里的位置很讨巧寿命和速度接近 FRAM成本比 FRAM 低不少接口跟 SPI Flash 完全兼容硬件改动量极小。唯一的坑是容量做不大4Mbit 基本是常见型号的上限再大就贵得离谱。但对于工业记录场景512K 字节足够存几十万条定长记录配合环形缓冲策略可以一直滚动写入。PIC18F86J15 这边选它的理由也很实际SPI 模块硬件支持最高时钟可以到 10MHz 以上实际跑 8MHz 没问题有足够的 GPIO 做片选和状态指示RAM 虽然只有不到 4K但做 SPI 缓冲和记录打包够用开发环境 MPLAB X 加 XC8 编译器免费PICkit 系列调试器便宜产线烧录也方便。如果换成 STM32 当然性能更强但成本和功耗上去了对于这种低速数据记录属于杀鸡用牛刀。2. MR25H40CDF 与 PIC18F86J15 的硬件连接与底层原理2.1 MR25H40CDF 的引脚定义与 SPI 时序要点MR25H40CDF 是 SOIC-8 封装引脚定义跟常见的 25 系列 SPI Flash 一模一样这也是它方便替换的原因。八个脚分别是片选 CS、时钟 SCK、数据输入 SI、数据输出 SO、写保护 WP、保持 HOLD、电源 VDD、地 VSS。注意 WP 和 HOLD 这两个脚如果不用可以直接拉高但工业环境里我建议把 WP 接到 MCU 的 GPIO 上做写保护控制防止程序跑飞误写数据区。SPI 模式方面MR25H40CDF 支持模式 0 和模式 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。PIC18F86J15 的 MSSP 模块配置成 SPI 主模式时SSPCON1 寄存器的 CKP 位决定时钟极性CKE 位决定采样沿。我一般用模式 0空闲低电平上升沿采样配置起来最直观。时钟频率方面MR25H40CDF 手册标称最高 40MHz但实际在 3.3V 供电、长走线的情况下我建议先跑 8MHz 稳定后再往上试工业现场电磁干扰大时序余量要留够。读写时序上有个关键点跟 Flash 不同MRAM 没有页编程和扇区擦除的概念写命令发出后直接跟地址和数据就能写入不需要先擦除。这一点极大简化了驱动逻辑也避免了 Flash 那种擦除期间不能读的问题。读命令是 0x03写命令是 0x02写状态寄存器是 0x01读状态寄存器是 0x05。状态寄存器的 WEL 位表示写使能锁存每次写操作前要先发 0x06 写使能命令这是跟 Flash 一样的流程不能省。2.2 PIC18F86J15 的 SPI 模块配置与片选策略PIC18F86J15 的 MSSP 模块做 SPI 主模式初始化步骤我整理成下面这样每一步都有它的道理。// SPI 初始化模式 0时钟 Fosc/16 void SPI_Init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISBbits.TRISB2 0; // CS 手动控制输出 CS 1; // 初始拉高不选中 SSPCON1 0x20; // SSPEN1, CKP0, SPI 主模式 Fosc/4 SSPCON1bits.SSPM 0x01;// 时钟 Fosc/16先低速调试 SSPSTAT 0x40; // CKE0SMP0模式 0 SSPADD 0; // 主模式不用 }这里片选 CS 我用普通 GPIO 手动控制没有用 MSSP 的 SS 功能。原因是 MSSP 的 SS 在从模式下才有意义主模式下如果配置不当会干扰而且手动控制片选可以灵活实现多从机切换也方便在两次传输之间插入延时。工业环境里我习惯在片选拉低后加几个微秒延时再发时钟给 MRAM 内部电路稳定时间虽然手册没强制要求但实测能减少偶发通信错误。SPI 读写一个字节的底层函数很简单但要注意 PIC18 的 SSPBUF 是双缓冲的写进去之后要等 BF 位或者 SSPIF 标志不能连续写。unsigned char SPI_Exchange(unsigned char data) { SSPBUF data; while(!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF 0; return SSPBUF; }这个函数是全双工的发一个字节同时收一个字节。读 MRAM 的时候就是发地址和空字节收数据。写的时候就是发命令地址和数据忽略返回值。2.3 电源与去耦的实战细节工业现场的电源质量参差不齐MRAM 虽然抗干扰能力比 Flash 强但电源纹波大了照样出问题。我在板子上给 MR25H40CDF 的 VDD 脚旁边放了一个 0.1uF 的陶瓷电容距离越近越好最好在 2 毫米以内再加一个 1uF 的钽电容做低频滤波。地线走线要短粗不要跟大电流的功率地混在一起数字地和模拟地单点连接。还有一点容易被忽略MRAM 在写入瞬间的电流会比静态时大虽然不像 Flash 编程那样有几十毫安的尖峰但电源内阻大的话还是会引起电压跌落。我遇到过一批板子用 LDO 供电LDO 输出电容只有 1uF结果连续快速写入时偶尔读回错误数据后来把输出电容加到 10uF 就稳了。这个坑在实验室用稳压电源测不出来只有上现场才暴露。3. 驱动层实现从单字节读写到环形记录缓冲3.1 基础读写函数的完整实现在 SPI 底层函数之上封装 MRAM 的读写操作。先定义命令和地址结构。MR25H40CDF 是 512K 字节地址需要 19 位用三个字节传输高字节的高 5 位是无关位实际有效的是低 19 位。#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 void MRAM_WriteEnable(void) { CS 0; SPI_Exchange(MRAM_CMD_WREN); CS 1; } unsigned char MRAM_ReadStatus(void) { unsigned char status; CS 0; SPI_Exchange(MRAM_CMD_RDSR); status SPI_Exchange(0xFF); CS 1; return status; } void MRAM_WriteByte(unsigned long addr, unsigned char data) { MRAM_WriteEnable(); CS 0; SPI_Exchange(MRAM_CMD_WRITE); SPI_Exchange((unsigned char)(addr 16)); SPI_Exchange((unsigned char)(addr 8)); SPI_Exchange((unsigned char)(addr)); SPI_Exchange(data); CS 1; // 等待写入完成MRAM 写入很快但保险起见查状态 while(MRAM_ReadStatus() 0x01); } unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS 0; SPI_Exchange(MRAM_CMD_READ); SPI_Exchange((unsigned char)(addr 16)); SPI_Exchange((unsigned char)(addr 8)); SPI_Exchange((unsigned char)(addr)); data SPI_Exchange(0xFF); CS 1; return data; }这里有个细节值得说MRAM 的写操作虽然快但状态寄存器的 WIP 位还是会短暂置位表示内部写入进行中。我实测在 8MHz SPI 下单字节写入后 WIP 位大概持续几百纳秒几乎可以忽略但为了代码健壮性还是加上轮询。如果追求极致速度可以连续写多个字节后再查一次状态因为 MRAM 支持页内连续写入地址会自动递增。3.2 页写入与连续读取的优化单字节读写效率太低每次都要发命令和地址开销大。MR25H40CDF 支持连续写入发一次写命令和起始地址后可以连续发多个数据字节地址自动递增直到片选拉高。读取也一样发一次读命令和起始地址后可以连续读。我封装了两个批量函数。void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_WriteEnable(); CS 0; SPI_Exchange(MRAM_CMD_WRITE); SPI_Exchange((unsigned char)(addr 16)); SPI_Exchange((unsigned char)(addr 8)); SPI_Exchange((unsigned char)(addr)); for(i 0; i len; i) { SPI_Exchange(buf[i]); } CS 1; while(MRAM_ReadStatus() 0x01); } void MRAM_ReadPage(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; CS 0; SPI_Exchange(MRAM_CMD_READ); SPI_Exchange((unsigned char)(addr 16)); SPI_Exchange((unsigned char)(addr 8)); SPI_Exchange((unsigned char)(addr)); for(i 0; i len; i) { buf[i] SPI_Exchange(0xFF); } CS 1; }注意 MRAM 没有页边界限制不像 Flash 那样跨页会回卷它可以一直写到芯片末尾地址。但实际使用中我还是按固定长度分块比如 64 字节一块方便跟记录结构对齐也方便计算校验。3.3 环形记录缓冲的设计与地址管理工业记录场景最常用的数据结构是环形缓冲固定区域循环写入新数据覆盖最旧数据。这样不需要动态内存管理写入地址一直递增到区域末尾回绕到开头。我在 MRAM 里划出前 16 个字节做元数据区存写指针、记录总数、魔数校验后面全部做数据区。元数据区结构定义如下偏移长度内容说明0x004魔数固定值 0x4D52414D判断是否首次使用0x044写指针下一条记录的写入地址0x084记录总数累计写入记录数用于判断覆盖0x0C4元数据 CRC前面 12 字节的校验记录结构我设计成定长 32 字节4 字节时间戳、12 字节三路模拟量、4 字节状态字、2 字节序号、2 字节保留、8 字节记录 CRC。定长记录的好处是寻址简单第 N 条记录的地址就是数据区起始地址加上 N 乘以 32。写指针更新策略很关键。如果每写一条记录就更新一次元数据区的写指针那元数据区的写入次数会跟记录数一样多虽然 MRAM 寿命无限但频繁写同一区域也没必要。我的做法是每写 16 条记录更新一次元数据掉电最多丢 16 条对于秒级记录来说就是 16 秒数据可以接受。如果要求更高可以缩短到每 4 条更新一次代价是元数据区写入更频繁。typedef struct { unsigned long magic; unsigned long write_ptr; unsigned long total_count; unsigned long crc; } MRAM_Meta; void MRAM_UpdateMeta(MRAM_Meta *meta) { unsigned char buf[16]; unsigned char i; meta-crc 0; // 简单累加和校验实际项目建议用 CRC16 for(i 0; i 12; i) { meta-crc ((unsigned char*)meta)[i]; } for(i 0; i 16; i) { buf[i] ((unsigned char*)meta)[i]; } MRAM_WritePage(0, buf, 16); }这里校验用累加和是为了代码简洁实际工业项目我建议上 CRC16 或者 CRC32累加和对字节顺序不敏感检错能力弱。CRC 表可以放 Flash 里计算 16 字节很快。4. 数据可靠性与掉电保护实战4.1 掉电检测与写入原子性工业现场掉电是常态数据记录最怕写一半掉电导致记录损坏或者元数据跟数据区不一致。MRAM 本身的写入是原子的单次写入操作要么完成要么没开始不会出现写一半的中间态这是它比 Flash 强的地方。但我们的记录是 32 字节分多次 SPI 传输如果传到一半掉电这条记录就不完整。解决办法有两个层面。硬件层面我在电源输入端加了大电容和掉电检测电路用比较器监测输入电压低于阈值时触发 MCU 外部中断中断里立刻停止记录并保存关键状态。软件层面每条记录写入前先写一个标志字节表示写入中写完后改成有效读取时检查标志无效记录直接跳过。标志字节放在记录头跟记录一起写但因为是同一次连续写入MRAM 保证这段写入的原子性。实际测试中我用可调电源模拟掉电从 5V 快速降到 0V上升沿和下降沿都试过。加了 470uF 电容后MCU 能撑大约 20 毫秒足够完成一次 32 字节写入和元数据更新。如果电容再大撑的时间更长但体积和成本上去了。这个电容值要根据实际写入耗时来算8MHz SPI 下 32 字节传输大约 32 微秒加上命令地址开销不到 50 微秒20 毫秒余量非常充足。4.2 数据校验与坏块管理MRAM 不像 Flash 有坏块概念出厂就是好的寿命内也不会写坏。但工业环境的电磁干扰可能导致 SPI 传输误码所以校验不能省。我在每条记录末尾加 8 字节 CRC读取时校验不通过就标记该记录无效。CRC 用 CRC-32 或者 CRC-16 都行CRC-16 对于 32 字节记录足够计算量也小。还有一个隐蔽的问题SPI 通信在强干扰下可能整个字节错位导致读出的数据整体偏移。这种情况 CRC 也能检出来但恢复比较麻烦。我的做法是在记录头加一个固定的同步字比如 0xAA55读取时先找同步字找不到就往后滑动一个字节再找相当于软件层面的帧同步。这个技巧在串口通信里常用SPI 里用的人少但工业现场确实有效。#define REC_SYNC 0xAA55 unsigned char MRAM_ReadRecord(unsigned long addr, Record *rec) { unsigned char buf[32]; unsigned int sync; MRAM_ReadPage(addr, buf, 32); sync (buf[0] 8) | buf[1]; if(sync ! REC_SYNC) return 0; // 同步字错误 if(!CheckCRC(buf, 32)) return 0; // CRC 错误 memcpy(rec, buf, 32); return 1; }4.3 写入速度实测与优化我用逻辑分析仪抓过 SPI 波形8MHz 时钟下单字节写入从片选拉低到拉高大约 2 微秒32 字节记录写入大约 40 微秒加上写使能和状态查询一条记录完整写入不到 100 微秒。这个速度对于秒级记录绰绰有余就算每秒记录 100 次也才占用 1% 的 CPU 时间。如果想更快可以把 SPI 时钟提到 16MHz 甚至 20MHz。PIC18F86J15 在 3.3V 下 Fosc 可以到 40MHzSPI 分频后 10MHz 没问题。但要注意 PCB 走线SPI 时钟线如果太长或者没有阻抗控制高速下会有振铃和反射导致误码。我一般建议 SPI 走线不超过 10 厘米时钟线包地串联一个 22 欧姆的电阻做阻抗匹配。实测 10MHz 下10 厘米走线加 22 欧姆串阻波形干净通信稳定。还有一个优化点批量写入时不要每条记录都查状态寄存器。MRAM 的 WIP 位在连续写入模式下只在最后一次传输结束后短暂置位可以在写完一批后再查一次。我试过连续写 16 条记录再查状态完全没问题这样省掉了 15 次状态查询的开销。5. 常见问题排查与避坑经验5.1 SPI 通信失败的排查顺序调试 SPI 通信我总结了一个从硬件到软件的排查顺序按这个顺序走基本能定位问题。步骤检查项常见问题解决方法1电源电压低于 2.0V 或高于 3.6V调整 LDO 输出加去耦电容2片选信号一直低或一直高用示波器看 CS 波形确认 GPIO 配置3时钟信号无输出或频率不对检查 SSPCON1 配置确认 TRIS 方向4数据线SI 和 SO 接反对照手册引脚定义交换接线5SPI 模式模式不匹配确认 CPOL/CPHA试模式 0 和模式 36写使能忘记发 WREN每次写前发 0x06查 WEL 位7地址越界超过 512K检查地址计算确认 19 位有效8时序余量时钟太快降频到 1MHz 先调通再提速这个表里第 6 条是我踩过最多的坑。MRAM 跟 Flash 一样写操作前必须发写使能命令而且写使能在每次写操作后自动清除下次写还要再发。我刚开始调的时候读正常写不正常查了半天才发现漏了 WREN。5.2 数据读回错误的几种典型情况读回数据跟写入不一致原因可能有很多。我遇到过的情况包括电源纹波导致写入失败、SPI 时钟太快导致采样错误、片选时序不对导致命令没被识别、地址计算错误导致写到别的区域。排查的时候我习惯先写一个固定模式比如 0x55 和 0xAA 交替然后读回来对比这样能快速判断是通信问题还是数据问题。还有一种隐蔽情况MRAM 的 WP 引脚如果被拉低写保护生效写入会被忽略但不报错。我有一批板子把 WP 直接接地了结果怎么都写不进去读出来全是 0xFF。后来把 WP 接到 GPIO 上拉高才正常。这个坑在手册里写了但容易忽略因为 Flash 的 WP 通常只在特定条件下才生效MRAM 的 WP 是直接硬件写保护。5.3 工业现场的长期运行经验这个方案我在两个现场跑了两年多累计写入超过五亿次没有出现数据丢失。总结几条经验第一元数据区一定要做双备份我后来改成 A/B 两个区交替写防止元数据区在掉电瞬间损坏导致整个记录区无法解析。第二记录区末尾留一段空白做磨损均衡没必要MRAM 不存在磨损但留一段做紧急数据暂存区有用掉电中断里可以把当前记录快速写到暂存区上电后再恢复。第三定期自检上电时读一遍元数据和最近几条记录校验通过才进入正常记录模式不通过就触发告警。还有一点关于温度MRAM 标称工业级零下四十度到八十五度我做过高低温循环测试零下四十度冷启动读写正常八十五度连续写入七十二小时无错误。但要注意 PCB 板材的 Tg 值普通 FR-4 在八十五度长期工作会老化建议用高 Tg 板材。焊点也要注意无铅焊料在低温下脆性增加冷热循环容易开裂汽车级产品建议用含铅焊料或者加固焊点。6. 性能边界与扩展思路6.1 容量与速度的极限在哪里MR25H40CDF 的 512K 字节按 32 字节一条记录算能存 16384 条。如果每秒记录一次可以存四个半小时环形覆盖后永远保留最近四个半小时的数据。如果记录间隔拉长到一分钟能存十一天。这个容量对于大多数工业记录场景够用但如果要存更长时间要么加大记录间隔要么换更大容量的存储。速度方面SPI 接口是瓶颈。8MHz 时钟下理论最大传输率 1MB/s实际因为命令开销和状态查询有效写入速度大概 200KB/s。如果换成 QSPI 或者并口 MRAM速度能上去但 PIC18F86J15 不支持 QSPI换主控成本又上去了。所以这个组合的定位就是中低速数据记录不要指望用它做高速数据采集。6.2 多芯片级联与存储扩展如果 512K 不够可以挂多片 MR25H40CDF用不同的片选引脚切换。PIC18F86J15 有足够的 GPIO 做片选SPI 总线可以并联。软件上把地址空间映射一下比如芯片 0 对应 0 到 512K芯片 1 对应 512K 到 1M读写函数里根据地址高位选择片选。这样容量翻倍成本增加不多PCB 面积增加也有限。多芯片的挑战在于片选切换时的时序要确保前一个芯片完全释放总线后再选下一个中间加几个微秒延时。另外所有芯片的 SPI 时钟和数据线并联走线要注意分支长度一致避免信号反射。我试过两片级联跑 8MHz 没问题四片以上建议降频到 4MHz。6.3 从裸机到轻量文件系统的演进裸机环形缓冲适合定长记录但如果要存变长数据比如日志文本、配置参数、固件备份就需要更灵活的管理方式。我后来在这个方案上演进出一个极简的文件系统把 MRAM 分成若干块每块有块头和块尾块头记录块类型、长度、校验块尾记录结束标志。分配块的时候顺序查找空闲块删除的时候标记为空闲。这个文件系统代码量不到 500 行占用 RAM 很少适合 8 位机。再往上如果数据量更大、并发更多可以考虑换 32 位主控加 RTOS用现成的文件系统如 LittleFS 或者 SPIFFS。但那是另一个量级的项目了对于大多数工业记录场景裸机加环形缓冲加 CRC 校验已经足够可靠和简单。简单意味着可控可控意味着出了问题能快速定位这在工业现场比性能更重要。我个人在实际操作中的体会是这套 MRAM 加 8 位机的组合最大的价值不是性能多强而是把可靠性做扎实了。工业产品不怕慢怕的是数据丢了找不回来。把掉电保护、CRC 校验、双备份元数据这几件事做到位比堆砌高性能硬件更有意义。后续如果要扩展我建议先在现有框架上加一个简单的命令解析层通过串口或者 CAN 总线接收上位机的读写指令这样不用重新烧录就能查看和导出 MRAM 里的数据现场维护会方便很多。