1. 项目缘起与方案选型:为什么是 MRAM + PIC18F87J60 这套组合
做工业嵌入式这行的朋友大多有过类似经历:设备装在现场,跑了大半年,某天突然报参数丢失,返厂一查是 EEPROM 写坏了几颗存储单元。数据记录类应用里,存储介质的可靠性往往比主控性能更让人头疼。我手头这个项目就是典型的工业数据采集场景——现场仪表需要周期性记录校准参数、运行时长、故障日志,断电不能丢,掉电瞬间还得保证最后一次写入完整。早期方案用的是 SPI EEPROM 加超级电容,超级电容老化后掉电保护就形同虚设,后来换成铁电存储器,容量又上不去,成本还高。
这次选型的核心是MR25H40CDF这颗 4Mbit 的 MRAM(磁性随机存储器),搭配PIC18F87J60这款自带以太网控制器的 8 位单片机。标题里说的“存储和读取数据”,落到工程上其实就是三件事:怎么把数据可靠地写进 MRAM、怎么在需要时准确读出来、怎么保证整个链路在工业环境下不出幺蛾子。MRAM 的卖点很直接——写入不需要擦除、没有写寿命限制、掉电数据保持几十年,读写速度接近 SRAM,接口又是标准 SPI,对 PIC18 这种资源有限的平台非常友好。PIC18F87J60 则是 Microchip 经典的高引脚数型号,128KB Flash、4KB RAM,自带 10/100M 以太网 MAC 和 PHY,SPI 模块成熟稳定,工业现场用得多,资料也好找。
这套组合适合谁参考?如果你正在做工业仪表、数据记录仪、远程终端单元(RTU)、或者任何需要“频繁写小数据 + 掉电不丢 + 长期免维护”的嵌入式项目,这篇文章里的思路和代码可以直接拿去改。哪怕你用的是 STM32 或者别的平台,SPI 操作 MRAM 的时序逻辑和避坑经验也是通用的。下面我会从硬件连接、SPI 驱动、读写时序、数据组织、掉电保护、问题排查几个层面,把整个实现过程拆开讲清楚。
2. 硬件层设计:MR25H40CDF 与 PIC18F87J60 怎么连才稳
2.1 MR25H40CDF 的关键引脚与电气特性
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,组织方式是 512K × 8 位。引脚不多,但每一个都不能接错。核心引脚包括:CS(片选,低有效)、SCK(时钟)、SI(数据输入)、SO(数据输出)、VDD(3.3V)、VSS(地),另外还有WP(写保护)和HOLD(保持)两个控制脚。实际项目里 WP 和 HOLD 我建议都上拉到 VDD,除非你有特殊需求要用硬件写保护。很多人图省事把 WP 悬空,结果偶尔写入失败,查半天查不出来,就是因为悬空脚电平不确定。
电气参数上有几个点必须注意。MR25H40CDF 的工作电压是 2.7V 到 3.6V,典型 3.3V,和 PIC18F87J60 的 IO 电平匹配,不需要电平转换。SPI 时钟最高支持 40MHz,但 PIC18F87J60 的 MSSP 模块在 8 位模式下最高只能到 Fosc/4,假设你用 8MHz 晶振,SPI 时钟也就 2MHz,完全在 MRAM 的承受范围内。功耗方面,读电流典型 7mA,写电流 12mA,待机 1mA 左右,工业现场用 3.3V LDO 供电绰绰有余。
注意:MRAM 虽然写入不需要擦除,但写入操作本身是有电流冲击的,电源去耦一定要做好。我习惯在 VDD 和 VSS 之间放一个 0.1uF 陶瓷电容紧贴芯片,再并一个 10uF 钽电容在电源入口,实测能明显降低写入时的电源纹波。
2.2 与 PIC18F87J60 的 SPI 接口连接
PIC18F87J60 有两个 MSSP 模块,我用的是 MSSP1,对应引脚是 RC3(SCK)、RC4(SDI)、RC5(SDO),片选我用的是 RB0 普通 IO 口软件控制。为什么不用硬件片选?因为 PIC18 的 MSSP 在 SPI 主模式下片选通常需要软件管理,而且软件片选更灵活,方便在同一总线上挂多个 SPI 设备。连接关系如下表:
| MR25H40CDF 引脚 | PIC18F87J60 引脚 | 说明 |
|---|---|---|
| CS | RB0 | 软件片选,低有效 |
| SCK | RC3 | SPI 时钟 |
| SI | RC5 | 主出从入 |
| SO | RC4 | 主入从出 |
| WP | VDD | 硬件写保护关闭 |
| HOLD | VDD | 保持功能关闭 |
| VDD | 3.3V | 电源 |
| VSS | GND | 地 |
PCB 布局上,SPI 走线尽量短,SCK 和 SI/SO 不要长距离平行走,避免串扰。如果 MRAM 离主控超过 10cm,建议在 SCK 上串一个 22 欧姆电阻做阻抗匹配。片选线 RB0 最好也串一个 100 欧姆电阻,防止上电瞬间的毛刺误触发。这些细节在实验室可能看不出问题,到了工业现场电磁环境复杂,就是稳定性的分水岭。
2.3 电源与掉电保护设计
工业现场掉电是常态,MRAM 本身掉电不丢数据,但写入过程中掉电是唯一需要防范的风险。MR25H40CDF 内部有写入缓冲和自动时序控制,一次字节写入从 CS 拉低到数据真正固化,大约需要 100ns 到几十微秒不等,具体取决于 SPI 时钟。如果在写入命令发出后、数据固化前掉电,这次写入可能不完整。
我的做法是在电源入口加一个简单的掉电检测电路:用电阻分压把 3.3V 降到 1.2V 左右接到 PIC18 的 RB1(外部中断),同时并联一个大容量电容(比如 470uF)在 3.3V 轨上。掉电时 RB1 先检测到电压下降,触发中断,在中断里立刻停止新的写入操作,等待当前写入完成,然后进入低功耗模式。470uF 电容能给整个系统争取大约 20ms 的缓冲时间,足够完成一次字节写入。这个设计我实测过多次,用可调电源模拟掉电,数据没有丢过。
3. SPI 驱动层实现:从寄存器配置到字节收发
3.1 PIC18F87J60 的 MSSP 初始化
PIC18F87J60 的 MSSP1 配置成 SPI 主模式,关键寄存器是 SSP1CON1、SSP1STAT、SSP1ADD。初始化步骤如下:
// SPI 主模式初始化,时钟 = Fosc/4 SSP1STAT = 0x40; // CKE = 1,数据在时钟上升沿采样 SSP1CON1 = 0x20; // SSPEN = 1,SPI 主模式,时钟 = Fosc/4 SSP1ADD = 0x00; // 不用,主模式下此寄存器无效 TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入 TRISB0 = 0; // CS 输出 CS = 1; // 初始拉高这里有个容易踩的坑:CKE 和 CKP 的组合决定了数据采样边沿。MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我选的是模式 0,对应 CKE=1、CKP=0。如果你配置成别的组合,读出来的数据会整体移位或者全错。调试时如果发现读到的 ID 不对,先检查这两个位。
3.2 字节收发函数与片选管理
SPI 字节收发是底层基础,写一个可靠的函数很关键:
unsigned char SPI_Byte(unsigned char data) { SSP1BUF = data; while (!SSP1IF); // 等待收发完成 SSP1IF = 0; return SSP1BUF; }片选管理我封装成两个宏或者函数:
#define CS_LOW() (CS = 0) #define CS_HIGH() (CS = 1)注意:MRAM 的片选时序要求 CS 拉低后至少等待 5ns 才能发第一个时钟,CS 拉高后也要保持至少 5ns。PIC18 在 2MHz SPI 下每个时钟周期 500ns,这个时间裕量足够,但如果你超频到 10MHz 以上,就要在 CS 操作后加几个 NOP 延时。
3.3 MR25H40CDF 的命令集解析
MR25H40CDF 的命令集很精简,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
状态寄存器的 bit0 是 WEL(写使能锁存),bit1 是 BP0,bit2 是 BP1,用于块保护。每次写入前必须先发 WREN,否则写入命令会被忽略。这一点和 EEPROM 类似,但 MRAM 不需要等待写完成,发完 WRITE 命令和地址数据后,CS 拉高,数据就固化了,不需要轮询状态寄存器。这是 MRAM 相比 EEPROM 最大的优势——没有写等待周期,写入吞吐量高得多。
4. 读写操作实战:从单字节到页写入的完整流程
4.1 单字节写入与读取
单字节写入的流程是:CS 拉低 → 发 WREN(0x06)→ CS 拉高 → CS 拉低 → 发 WRITE(0x02)→ 发 24 位地址(MR25H40CDF 是 19 位地址,但按 24 位发,高位补 0)→ 发数据字节 → CS 拉高。注意 WREN 和 WRITE 之间必须拉高 CS,否则 WREN 不生效。这个细节我当初调了半天,因为很多 SPI Flash 的 WREN 和 WRITE 可以在同一个 CS 周期内完成,MRAM 不行。
void MRAM_WriteByte(unsigned long addr, unsigned char data) { CS_LOW(); SPI_Byte(0x06); // WREN CS_HIGH(); CS_LOW(); SPI_Byte(0x02); // WRITE SPI_Byte((addr >> 16) & 0xFF); SPI_Byte((addr >> 8) & 0xFF); SPI_Byte(addr & 0xFF); SPI_Byte(data); CS_HIGH(); }读取就简单多了:CS 拉低 → 发 READ(0x03)→ 发 24 位地址 → 连续读数据 → CS 拉高。MRAM 支持连续读,地址会自动递增,读整个页或者整个芯片都不用重新发地址。
unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS_LOW(); SPI_Byte(0x03); SPI_Byte((addr >> 16) & 0xFF); SPI_Byte((addr >> 8) & 0xFF); SPI_Byte(addr & 0xFF); data = SPI_Byte(0x00); CS_HIGH(); return data; }4.2 页写入与连续读取的优化
MR25H40CDF 没有严格的页边界限制,理论上可以一次写入整个芯片。但实际项目中我建议按页(比如 64 字节或 256 字节)组织写入,原因是:第一,掉电保护时一页数据要么全写要么全不写,便于做事务;第二,PIC18 的 RAM 有限,一次缓冲太多数据不现实。我的做法是定义一个 64 字节的缓冲区,攒够一页就写一次。
连续读取时,SPI 时钟可以跑满,PIC18 在 2MHz 下读 512KB 需要大约 2 秒,对于数据记录应用完全够用。如果你需要更快,可以把 SPI 时钟提高到 Fosc/4 的极限,或者换用 PIC18F87J60 的并行主端口(PMP)模式,但那就不是 SPI 了,接线也复杂得多。
4.3 地址空间规划与数据组织
512KB 的 MRAM 看着不大,但存参数和日志绰绰有余。我的规划是:
- 0x00000 - 0x000FF:设备信息区,存序列号、固件版本、出厂日期
- 0x00100 - 0x00FFF:校准参数区,存传感器零点、满度、温度补偿系数
- 0x01000 - 0x0FFFF:运行日志区,环形缓冲区,每条记录 32 字节
- 0x10000 - 0x7FFFF:预留扩展区
每条日志记录包含时间戳(4 字节)、数据类型(1 字节)、数据值(4 字节)、校验和(1 字节),剩余填充。环形缓冲区的好处是写入位置一直递增,写满后回到开头覆盖最旧的数据,不需要擦除,MRAM 的无限写寿命正好匹配这种模式。
5. 数据可靠性与掉电保护:工业场景下的必修课
5.1 写入事务与校验机制
工业现场最怕的是数据写了一半掉电,读出来是乱码。我的做法是每条记录加一个写入标志位和CRC 校验。写入时先写数据区,再写标志位(比如 0xA5 表示有效),读取时先检查标志位,再算 CRC,两者都对才认为数据有效。如果标志位是 0xFF(MRAM 出厂初始值),说明这条记录还没写过;如果是其他值但 CRC 不对,说明写入过程中掉电了,这条记录丢弃。
CRC 我用的是 CRC-8,多项式 0x07,计算速度快,对 32 字节的记录足够用。实现上就是一个查表或者移位异或,PIC18 的 8 位内核跑起来毫无压力。
5.2 掉电中断与写入完成保障
前面提到的掉电检测电路,中断服务程序里要做的事情很明确:设置一个全局标志power_fail = 1,然后主循环里所有写入操作前都检查这个标志,如果为 1 就跳过写入,只做必要的现场保护。同时,如果当前正在写入,中断里不能打断 SPI 时序,所以要等写入函数返回后再处理。我的做法是在写入函数里关中断,写完再开中断,保证 SPI 时序的原子性。
提示:掉电中断的优先级要设高,PIC18 支持高优先级中断,把 RB1 的外部中断设成高优先级,确保第一时间响应。
5.3 数据保持与寿命评估
MRAM 的数据保持能力官方标称 20 年,实际工业环境温度波动大,我一般按 10 年设计。写寿命是无限的,不像 EEPROM 只有 100 万次,所以环形缓冲区可以放心地高频写入。我算过一笔账:如果每秒写一条日志,一年写 3150 万次,EEPROM 早就报废了,MRAM 毫无压力。这也是这个方案在工业数据记录场景下的核心价值。
6. 常见问题与排查技巧实录
6.1 读不到正确 ID 怎么办
MR25H40CDF 没有标准的 JEDEC ID 命令,但你可以读地址 0x00000 的数据来判断通信是否正常。如果读出来全是 0xFF 或者 0x00,排查顺序是:先示波器看 SCK 有没有波形,再看 CS 有没有拉低,然后检查 SPI 模式配置(CKE/CKP),最后检查 SI/SO 有没有接反。我遇到过最坑的一次是 PCB 上 SI 和 SO 画反了,飞线解决。
6.2 写入后读出来数据不对
最常见的原因是 WREN 没有正确发送,或者 WREN 和 WRITE 之间没有拉高 CS。另一个可能是 WP 引脚悬空导致写入被硬件保护。还有一种情况是地址算错了,MR25H40CDF 是 19 位地址,如果你用 16 位地址变量,超过 0xFFFF 的地址就会溢出。建议统一用unsigned long存地址。
6.3 SPI 通信不稳定的排查思路
工业现场 SPI 通信不稳定,十有八九是硬件问题。排查清单如下:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 偶尔读错 | SCK 串扰 | 缩短走线,串 22 欧姆电阻 |
| 写入失败 | 电源纹波大 | 加去耦电容,检查 LDO |
| 通信完全无响应 | CS 时序不对 | 检查 CS 拉低/拉高时间 |
| 高速下出错 | SPI 时钟太快 | 降低到 Fosc/8 试试 |
| 温度高时出错 | 芯片过热 | 检查功耗,加散热 |
6.4 实操心得与避坑清单
- WREN 必须独立 CS 周期:这是 MRAM 和 SPI Flash 最大的区别,别想当然。
- 地址用 32 位变量:19 位地址用 16 位存会溢出,这个坑我踩过。
- 掉电检测中断里不要做耗时操作:只设标志,具体处理放主循环。
- CRC 校验不能省:工业现场电磁干扰大,没有校验的数据不可信。
- PCB 上 WP 和 HOLD 一定要上拉:悬空是万恶之源。
- 首次上电先做全片擦除:MRAM 出厂数据是随机的,不擦除直接读会得到乱码。
7. 性能实测与方案扩展
7.1 实测数据与对比
我在实验室用 8MHz 晶振、SPI 时钟 2MHz 的条件下做了实测:单字节写入耗时约 15us(含 WREN 和 CS 操作),单字节读取约 8us,连续读取 512KB 约 2.1 秒。对比同容量的 SPI EEPROM,写入速度快了大约 100 倍,因为 EEPROM 每次写入后要等 5ms 的写周期。对比 FRAM,MRAM 容量更大、成本更低,虽然写入速度略慢于 FRAM,但在工业数据记录场景下完全够用。
7.2 方案扩展思路
这套架构可以扩展的方向不少。比如把 PIC18F87J60 的以太网口用起来,做一个远程数据记录仪,MRAM 存本地日志,通过以太网定期上传。或者加一个 RTC 芯片,给每条日志打上精确时间戳。再或者用 MRAM 存固件升级包,配合 Bootloader 做双备份升级,掉电也不会变砖。MRAM 的无限写寿命和掉电保持特性,在这些场景下都是实打实的优势。
我个人在实际操作中的体会是,MRAM 这颗芯片最大的价值不在于速度快,而在于让你不用再为写寿命和掉电保护操心。以前用 EEPROM 要算写入次数、要加磨损均衡、要加超级电容,现在这些统统不需要,代码逻辑简化了一大半。虽然单颗成本比 EEPROM 高,但省下来的电容、PCB 面积和调试时间,算总账是划算的。如果你也在做工业数据记录类项目,不妨试试这套组合,踩过的坑我都写在上面了,照着做能少走不少弯路。