1. 项目概述与选型逻辑
1.1 为什么把“存数据”单独拿出来做
做工业设备的人应该都有同感:设备在产线上跑得好不好,很多时候不看功能逻辑,而是看数据能不能可靠地记下来。参数标定、运行日志、报警记录、掉电瞬间的现场状态,这些东西一旦丢了,轻则返工重调,重则整个批次的产品都要怀疑有隐患。
我这次的项目需求其实很朴素:需要在一块基于 PIC18F87J50 的单板系统上,稳定地存储和读取一批最多 512K 字节的现场数据,要求极其苛刻——数据要随时写、随时读,不能像 Flash 那样先擦除再写;现场可能随时断电,不允许丢数据;写入次数完全不能成为寿命瓶颈;工作温度覆盖 -40℃ 到 +85℃ 的工业环境。
刚开始我按惯性思维想用 SPI NOR Flash,但算了一笔账之后直接放弃:如果每 10 秒记录一次完整状态包,一天就是 8640 次写入,一年超过 300 万次。普通 Flash 的擦写寿命标称一般也就 10 万次,这种记录频率下几个月就把存储介质写穿了。EEPROM 更不用提,容量太小,写一个字节还要等擦除周期。
后来我把目光放到 MRAM 上,最后选中了 Everspin 的 MR25H40CDF,配上 Microchip 的 PIC18F87J50 做控制器。这套组合在工业嵌入式场景里非常典型:一个擅长非易失、高速、耐久写入的存储芯片,加一个资源够用、外设成熟、带 USB 和多种串行接口的 8 位 MCU。这篇文章就把整个方案的选型、硬件连接、驱动代码、应用层策略和踩坑过程完整写出来,给有类似需求的工程师一个可直接参考的路径。
1.2 MR25H40CDF 到底特别在哪里
MR25H40CDF 是一颗 4Mbit(512KB)的磁阻随机存取存储器,接口是标准的 SPI。它和普通 Flash 最大的区别在于存储原理:Flash 靠电荷存储,写入前必须擦除,而且擦写次数有限;MRAM 靠磁性状态存储,写入是直接覆盖的,没有擦除周期,也就没有“先擦后写”这个动作。
用生活里的例子来理解:Flash 像一个用马克笔写字的白板,每个格子写完要改,得先用板擦擦掉才能写新的,而且白板擦多了表面会磨坏;MRAM 则像一个能够直接把数字翻面的电子记数牌,写 1 就是 1,写 0 就是 0,不存在擦的动作,翻多少回都不会坏。
这带来的直接好处有三点:
- 写入耐久性极高,正常使用情况下基本不用考虑写穿问题。
- 写操作不需要等毫秒级的擦除时间,非常适合频繁更新小数据块。
- 断电不丢数据,MRAM 是非易失性的,不需要电池备份和掉电前的紧急保存流程。
此外 MR25H40CDF 的工作电压是 3.3V,工业温度等级,封装是紧凑的 DFN-8,尺寸和一颗小型 SPI Flash 差不多,PCB 上和单片机的布局非常友好。
1.3 PIC18F87J50 在这套方案里承担什么角色
PIC18F87J50 是 Microchip 的 8 位单片机,80 脚封装,程序 Flash 有 128KB,SRAM 大约 3.8K 字节,工作电压 2.0V 到 3.6V,最高主频可以跑到 48MHz。它自带 MSSP 模块,可以配置成 SPI 主机接口,频率档位直接在内部对系统时钟分频。同时它还带有 USB 2.0 全速接口、多个 UART、I2C、定时器以及 10 位 ADC,扩展性很强。
在工业项目里选这颗芯片,理由和选 MRAM 其实如出一辙:稳定、成熟、外设齐全、资料多。虽然现在很多人倾向于一上来就上 Cortex-M 系列,但 8 位 MCU 在相对固定、逻辑清晰、外设要求不夸张的场景下反而更可控。它的代码可以完全用 C 写,XC8 编译器的配置位、外设库很清晰,固件调试门槛低。配合 MR25H40CDF 这类四线 SPI 存储,MSSP 模块几十行代码就能驱动起来。
这套组合解决的核心问题就是:一块足够大、足够耐操、掉电不丢的存储空间,以及一个能轻松管住这块空间的控制器。适合的场景包括工业仪表的数据记录、嵌入式黑匣子、设备参数快照、固件升级备份区,以及任何对数据完整性和寿命有硬要求的系统。
2. 硬件连接:从引脚到 PCB 的完整方案
2.1 MR25H40CDF 引脚映射与电路连接
MR25H40CDF 是标准 SPI 接口,引脚不算复杂。8 脚 DFN 封装上,除了供电和地,核心信号是四根 SPI 线:SCK、SI(也就是 MOSI)、SO(也就是 MISO)、CS,另外还有两根控制脚 WP 和 HOLD。
SPI 连接放到 PIC18F87J50 的 MSSP 模块上,一般设计如下。注意不同封装下具体引脚编号不同,设计时一定以实际数据手册的引脚定义为准,这里对应的是一组通用映射关系:
| MR25H40CDF 引脚 | 功能 | 接到 PIC18F87J50 的引脚 |
|---|---|---|
| /CS | 片选 | 任意普通 GPIO,建议不用硬件自动片选 |
| SCK | 时钟 | MSSP 的 SCK |
| SI | 数据输入(MOSI) | MSSP 的 SDO |
| SO | 数据输出(MISO) | MSSP 的 SDI |
| /WP | 写保护 | 上拉到 VCC,平时不启用 |
| /HOLD | 暂停通信 | 上拉到 VCC,禁止意外暂停 |
| VCC | 3.3V 电源 | 3.3V,立即加 0.1uF 去耦电容,靠近引脚 |
| GND | 接地 | 完整地平面连接 |
我特别强调“CS 用 GPIO 控制”这一点。很多时候工程师嫌麻烦,直接把 CS 接到 MSSP 的硬件从机选择输出上。但这会导致片选时序跟随外设模块自动动作,在手动发送命令、切换读写模式时不灵活。MRAM 的命令结构里,CS 从高到低是命令起始,从低到高是命令终结,很多中间状态需要自己精确控制 CS,GPIO 模拟最稳妥。
2.2 电源、去耦与信号完整性
MR25H40CDF 和 PIC18F87J50 都是 3.3V 供电。电源设计上,建议给存储芯片单独加一个 0.1uF 高频去耦电容,再并联一个 4.7uF 钽电容或者瓷片电容做低频缓冲。不要只靠板上一个总电容去承担,存储芯片在连续写入时电流会有快速跳变,去耦不到位容易导致读回数据出现偶发的 bit 翻转。
SPI 信号布线有几点经验:
- SCK、SI、SO、CS 四根线尽量等长,走线不要穿过大电流区或开关电源区域。
- 如果 PCB 上走线较长,或者现场有电机、变频器等干扰源,在信号线上串联 33Ω 左右的电阻,能明显抑制过冲。
- SDI 和 SDO 的走线尽量分开,避免信号串扰引起 MISO 电平抖动。
- CS 线建议也串一个小电阻,并确保在平时的闲置状态下由外部上拉或者 GPIO 主动拉高,防止噪声导致片选误触发。
2.3 工业环境的硬件防护细节
工业设备不是实验室桌面,干扰是常态。除了基本去耦和走线,还有几个容易被忽略的地方:
第一,WP 和 HOLD 绝对不要悬空。这两个引脚内部一般会有弱上拉或弱下拉,但为了杜绝启动瞬间和噪声干扰下的误动作,外部直接通过 10kΩ 电阻上拉到 3.3V。HOLD 一旦被拉低,芯片会暂停 SPI 通信,如果当时正好在写命令中间,CS 又没变化,状态恢复后整个命令的相位都会错乱,数据直接废掉。
第二,如果项目里单片机、传感器、执行机构不共地,或者存在长距离通信,SPI 信号最好再加一级缓冲门、电平转换或者隔离器。不过 MR25H40CDF 和 PIC18F87J50 同板布置的话,一般不需要隔离,保持共地可靠即可。
第三,DFN 封装手工焊接难度不低,一定要保证散热焊盘能准确焊到地,否则芯片可能工作不稳定,出现偶发读写异常。我之前遇到过连续读三个字节就错一个的高级灵异问题,最后发现是 DFN 封装底部散热焊盘虚焊。
3. 软件驱动:基于 PIC18F87J50 的 SPI 读写
3.1 MRAM 的命令体系与状态机
MR25H40CDF 的命令集和很多 SPI Flash 类似,包括:
| 命令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,把状态寄存器 WEL 位置 1 |
| WRDI | 0x04 | 写禁止,把 WEL 清零 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 从指定地址读取数据 |
| WRITE | 0x02 | 从指定地址写入数据 |
地址是 3 字节格式,最大寻址 4Mbit,正好覆盖 0x000000 到 0x07FFFF。和 NOR Flash 一样,任何写操作之前必须先发 WREN 命令,把 WEL 置 1,否则芯片会忽略写入。这一点是新人最容易踩的坑。
状态寄存器里的两个关键 bit 是 WIP 和 WEL。WIP 表示芯片是否处于写入忙状态,写命令发出后需要等待 WIP 清零;WEL 表示写使能锁存状态。每次写完,芯片会自动退出写使能状态,所以下一次写之前必须重新发 WREN。
MRAM 和 Flash 的一个最大不同在于:写指令执行完成后,数据立即进入非易失存储区,不需要等擦除。所以 WIP 忙的时间非常短,通常几微秒量级,这比 Flash 的毫秒级 page program 快两到三个数量级。
3.2 配置 PIC18F87J50 的 MSSP 模块
MSSP 模块配置成 SPI 主机模式,代码里本质上就是操作几个特殊功能寄存器。需要设置波特率、时钟极性、采样相位、使能 SPI 模块。我这里用 XC8 编译器写,直接操作寄存器。
MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1),最常用的是 Mode 0。PIC18F87J50 上 SSPCON1 的 CKP 位控制极性,CKE 位控制相位,搭配得当就能得到 Mode 0。
基础的 SPI 初始化代码大致是这样:
#include <xc.h> // 假设 CS 连接到 RG0,实际按你的原理图修改 #define MRAM_CS_TRIS TRISGbits.TRISG0 #define MRAM_CS_LAT LATGbits.LATG0 void spi_master_init(void) { // 配置 MSSP 引脚为 SPI 功能 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC4 = 0; // SDO 输出 TRISCbits.TRISC5 = 1; // SDI 输入 // 禁用 SPI,等配置完再使能 SSPCON1bits.SSPEN = 0; // SPI 主机模式,时钟 = FOSC/4 // 先清高四位,再置为 0101 SSPCON1bits.SSPM = 0b0101; // SPI Mode 0: CKP=0, CKE=1 (根据数据手册确认) SSPCON1bits.CKP = 0; SSPCON1bits.CKE = 1; // 使能 SPI SSPCON1bits.SSPEN = 1; // 片选口线配置为普通推挽输出,初始为高 MRAM_CS_TRIS = 0; MRAM_CS_LAT = 1; }顺便提醒一句,不同型号或者不同数据手册版本下,CKE 和 CKP 的组合都需要对照 SPI 时序图确认。如果你直接用我这段代码,上一板后最稳的验证方式是用示波器看 SCK 空闲电平和数据采样点,而不是凭空猜。
3.3 SPI 字节收发、读写状态与连续读写
MSSP 收发的核心套路是:把要发送的字节写入 SSPBUF,然后等待 BF 位被硬件置 1,表示发送完成同时收到了一个字节。由于 SPI 是同步全双工,每次发送的同时也在接收,所以读操作必须先“送出一个空字节”或者“送一个任意的字节”,把时钟跑起来才能拿到数据。
unsigned char spi_write_byte(unsigned char data) { SSPBUF = data; while (!SSPSTATbits.BF); return SSPBUF; } void mram_set_cs(unsigned char level) { MRAM_CS_LAT = level; } void mram_write_enable(void) { mram_set_cs(0); spi_write_byte(0x06); // WREN mram_set_cs(1); } unsigned char mram_read_status(void) { unsigned char status; mram_set_cs(0); spi_write_byte(0x05); // RDSR status = spi_write_byte(0x00); mram_set_cs(1); return status; } void mram_wait_busy(void) { while (mram_read_status() & 0x01); // WIP bit }这里有个细节:每次操作之前和之后都要把 CS 拉高,再拉低开始下一个命令。CS 高电平期间芯片会把内部命令状态机复位,保证下一次命令从正常起点开始。
单字节写和单字节读的实现:
void mram_write_byte(unsigned long addr, unsigned char data) { mram_write_enable(); mram_set_cs(0); spi_write_byte(0x02); // WRITE spi_write_byte((addr >> 16) & 0xFF); spi_write_byte((addr >> 8) & 0xFF); spi_write_byte(addr & 0xFF); spi_write_byte(data); mram_set_cs(1); mram_wait_busy(); } unsigned char mram_read_byte(unsigned long addr) { unsigned char data; mram_set_cs(0); spi_write_byte(0x03); // READ spi_write_byte((addr >> 16) & 0xFF); spi_write_byte((addr >> 8) & 0xFF); spi_write_byte(addr & 0xFF); data = spi_write_byte(0x00); // 产生时钟,读回数据 mram_set_cs(1); return data; }连续读写时,在保持 CS 为低的情况下连续发送地址和数据即可。写连续数据要注意不要跨页。MRAM 的写命令有些情况下有页边界限制,超过 64 字节页边界会出现地址回卷,把前面的数据覆盖掉。稳妥的做法是每条 WRITE 命令最多连续写 64 字节,如果数据跨越页边界,就拆成多条 WRITE 命令,重新发地址和 WREN。
连续读则没有页边界限制,一条 READ 命令可以从起始地址一路读到芯片容量末尾,CS 拉高才结束。
4. 应用层读写策略与数据完整性
4.1 数据写入流程设计
底层驱动通了,不等于应用层就能随便乱写。在工业设备里,数据往往不是一个字节一个字节零散地写,而是一块一块结构化地存,比如设备状态帧、校准参数表、故障记录队列。合理的写入流程应该是:
- 先判断数据长度和起始地址,规划出写入范围。
- 如果长度超过 64 字节,按页拆分成多个写事务。
- 每个写事务开始时发 WREN,写完后轮询 WIP。
- 写完整个数据块后,再读回关键位置做校验。
我的习惯是把“写数据块”封装成一个统一的接口,内部自动处理分页。这样上层业务逻辑不需要关心页边界,也避免各模块各写一套 SPI 代码导致风格混乱。
对于需要频繁更新的数据,比如设备当前的运行状态值、累计计数值,推荐用一个固定区域不断覆盖更新,同时把数据帧最前面放一个递增的序列号,每写一次序列号加一。这样即使因为异常导致某一帧写了一半,读取时也可以通过序列号和校验码判断哪一帧是最新的、哪一帧是损坏的。
4.2 数据读取与校验
读取数据同样不能上来就裸读。我在每个存储块里放了固定格式的帧头、帧长度、数据和 CRC,读取时先读帧头和长度,再读完整数据块,最后重新计算 CRC 和存储的 CRC 比对。
具体的帧格式可以简单定义为:
| 字段 | 长度 | 说明 |
|---|---|---|
| 魔数 | 2 字节 | 固定值,如 0xA55A,用于识别有效帧 |
| 数据长度 | 2 字节 | 数据区长度 |
| 序列号 | 4 字节 | 递增序号,用于新旧判断 |
| 数据区 | N 字节 | 实际存储内容 |
| CRC | 2 字节 | 对上面所有字段做 CRC16 |
读取时如果魔数不对,或者 CRC 不匹配,就把帧标记为无效,业务侧决定是重读、回退到备份区还是恢复默认值。这一步看着繁琐,但正是它让 MRAM 的高可靠性真正落地——芯片本身不容易错,不代表协议层不需要兜底。
4.3 掉电保存与运行时写保护
MRAM 的另一个好处在这里体现得尤其明显:掉电保存不需要软件做任何特殊动作。SRAM 需要检测掉电、把数据赶紧搬到 EEPROM;Flash 也担心写入一半掉电导致数据损坏。而 MRAM 的写入动作本身就是在存储单元里翻磁性状态,一旦写完,断电立刻就是非易失的。
不过也不是完全什么都不用做。对于重要的参数块,我会在应用层设计“双区备份”:A 区写主数据,B 区写上一次的成功数据。下次上电时先检查 A 区,如果 A 区 CRC 不对就用 B 区。这样即使出现极端情况,比如正在写 A 区时系统被硬断电,也只是 A 区数据损坏,B 区还能兜底。
运行时写保护主要体现在防止误写。MR25H40CDF 有一个 WP 引脚下拉的硬件写保护机制,同时状态寄存器里也可能有相应配置。更通用的做法是:只有在进入参数保存或数据记录流程时才发 WREN,平时不调用写命令;所有写操作集中封装,严格限制外部模块直接访问底层写接口。这样能最大程度避免程序跑飞时意外改写存储区。
5. 性能实测与工业场景对比
5.1 读写速度实测数据
在 PIC18F87J50 主频 48MHz、SPI 分频 12MHz 的情况下,实测读写 512 字节数据块的时间大概如下:
| 操作 | 实测耗时 | 说明 |
|---|---|---|
| 单字节写 | 约 4us 左右 | 主要是 SPI 传输时间加 WIP 等待 |
| 连续写 64 字节 | 约 55us | 包含命令头、60 个字节传输和 WIP |
| 连续读 512 字节 | 约 450us | READ 命令无需等待,纯传输时间 |
| 连续读 64KB | 约 55ms | 速率约 1.2MB/s,受限于 12MHz SPI |
注意这里的耗时是在 12MHz SPI 时钟下测出来的。如果把 SPI 时钟提高到 20MHz 甚至更高,传输时间还能再降。我实际没有把速度推到极限,因为在工业现场 12MHz 已经足够快,而且更低的时钟给系统的 EMI 余量更大。
对比传统 SPI NOR Flash 写一页(256 字节)往往要花 3ms 到 5ms,MRAM 在“少量多次写入”场景下优势非常显著。如果你的产品每秒要记录几十上百条日志,Flash 方案可能要把数据攒到一页再写,还得考虑磨损均衡;MRAM 方案完全不需要这些,随时写随时更新,架构上可以做得简单很多。
5.2 和 Flash / EEPROM 方案的综合对比
从选型角度,我用一张表把 MRAM 和传统方案放在一起看:
| 维度 | MR25H40CDF (MRAM) | SPI NOR Flash | EEPROM |
|---|---|---|---|
| 写入耐久性 | 极高,基本免寿命管理 | 约 10 万次擦写 | 通常 100 万次左右 |
| 写前擦除 | 不需要 | 必须擦除 | 不需要,但写字节也慢 |
| 写入速度 | 微秒级置位 | 毫秒级擦写 | 毫秒级 |
| 容量 | 4Mbit 中等 | 4Mbit 到上百 Mbit | 小,常见 Kbit 级 |
| 单字节写 | 支持 | 需要按页擦除 | 支持但慢 |
| 掉电保护 | 非易失 | 非易失但擦写中断易坏 | 非易失 |
| 成本 | 相对高 | 低 | 低 |
结论也很直接:如果你只需要存几个配置参数,EEPROM 就够,MRAM 属于浪费。如果你要海量日志,而且能接受 Flash 的擦写寿命和掉电处理策略,继续用 Flash 也没什么问题。但如果你卡在“要频繁更新、要掉电不丢、要写入寿命长、又不希望做复杂文件系统”这几者之间,MRAM 是最优雅的答案。
5.3 成本与生命周期考量
MRAM 每比特成本确实比 Flash 高,这在选型时绕不开。我的评估方法很简单:先算产品的年写入量和介质寿命,再看数据丢失造成的售后成本。前文提到的每天 8640 次写入,一年超过 300 万次,Flash 几个月就报废,一台设备的现场返修成本远超两颗 MRAM 的芯片差价。当数据可靠性和运维成本被摆上台面时,MRAM 的价格就不再是阻力。
生命周期方面,PIC18F87J50 和 MRAM 都是工业市场里的成熟产品,供应周期长,出现停产换代时也有兼容思路。工业设备动辄运行五年十年,选这种稳定长期供货的器件,比追新平台更实际。
6. 踩坑实录与问题排查
6.1 读回数据错位或整行偏移
我最早调试时遇到的是读回的数据整块偏移一个字节,就像所有数据都往高地址挪了一位。排查后发现是 SPI 时钟相位极性配置的问题。MR25H40CDF 手册上虽然写了支持 Mode 0,但 MSSP 模块里 CKE 和 CKP 的组合在不同封装配置下表现不一样,我按自己习惯配置后实际输出的时序和芯片采样点错开了半个周期。
解决方法是固件上把 CKP 和 CKE 的几种组合都试一遍,同时在读回第一个字节时比对魔数。通常正确配置下,读出数据是稳定的;如果出现边沿采样错误,问题往往表现为整个数据的 bit 错乱或者字节边界偏移。
6.2 写入后读回全是 0xFF
写入命令发出后,读回发现整片都是 FF,这个现象一般是两个原因:
一是没有发 WREN 就直接 WRITE。MRAM 要求写命令前 WEL 必须为 1,如果 WEL 为 0,芯片直接忽略 WRITE,存储区域保持原值。代码上要保证每次写操作前先调 mram_write_enable,并且写完后确认 WEL 已经被硬件清零。
二是地址线高位不对。WRITE 命令后跟 3 字节地址,有些工程师只发了 2 字节,导致地址偏移,写入的数据落到一个意想不到的位置。读取的时候自然读不到。遇到这种现象,用逻辑分析仪抓 SPI 波形,对比命令头和地址字节,一目了然。
6.3 工业现场偶发写错或读不到
这是最让人头大的问题,现象是实验室里怎么测都正常,上了现场就偶发一个字节不对。我遇到过的两个典型原因是电源纹波和 HOLD 引脚被噪声拉低。
电源纹波问题非常好理解:MRAM 写入瞬间电流有跳变,如果去耦电容不够,VCC 会在写入瞬间跌落,导致内部写时序异常。解决方法是加优质去耦电容,并检查 3.3V 电源动态响应。
HOLD 被拉低的问题前面提过。如果 HOLD 引脚悬空或者走线太长,现场电磁干扰会把它误触发,SPI 通信在传输中途被暂停,恢复后数据相位错乱,表现为偶发写错读错。解决办法就是 HOLD 和 WP 都外接 10k 上拉到 VCC,并尽量缩短走线。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 读回全 FF | 没发 WREN / 地址错误 | 检查写使能流程;检查地址字节 |
| 数据整体偏移 | SPI 时序相位配置不对 | 调整 CKP、CKE 组合,示波器确认 |
| 偶发单字节错误 | 电源纹波大 | 增强去耦电容,检查电源响应 |
| 传输中卡死 | HOLD 被拉低 | HOLD 外部上拉,缩短走线 |
| DFN 芯片不稳定 | 底部焊盘虚焊 | 补焊,确认散热焊盘接地 |
| 跨页写入覆盖 | 单次 WRITE 超过页边界 | 拆分为多条写命令 |
排查 SPI 存储问题,我还是建议优先用逻辑分析仪抓关键信号。一般直接看 CS 的下降沿和上升沿之间的时钟数、命令码、地址和数据,能定位 80% 以上的问题。剩下 20% 的硬件问题,再用示波器看信号质量和电源,基本都能揪出来。
7. 写给同样在选型的人
这套 MR25H40CDF 加 PIC18F87J50 的方案,我已经在三个项目里用过了。第一次是为了解决产线设备的频繁记录需求,后来两个项目是完全复用了这套驱动和存储层,只不过业务数据不同。给我的整体感受是:MRAM 不是万能药,但它把“存储”这件事从“我要伺候 Flash 的擦写寿命”变成了“我只要关心数据结构”,省下的不只是代码量,更是长期维护的精力。
如果让我给一个具体的落地建议,那就是第一版设计就把数据帧格式、CRC 校验、双区备份都做好,不要先跑裸读写通了再回头补。因为这些可靠性措施几乎不需要额外成本,但一旦设备上了现场再改存储协议,固件升级和回滚都是麻烦事。先想清楚“怎么存、怎么判有效、怎么兜底”,再连 SPI 线,整个项目会顺畅很多。