1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机
做工业嵌入式这行十几年,最头疼的从来不是算法有多复杂,而是数据存不住、存不快、存不久。EEPROM 写一次要等 5 毫秒,Flash 擦除要按扇区来,掉电那一瞬间数据就没了,电池还得额外配一颗。很多项目做到最后,客户一句“这个参数要每秒记录一次,断电不能丢”,就能把整个存储方案推翻重来。
MR25H40CDF 这颗芯片第一次上手的时候,我确实有点眼前一亮。4Mbit 容量,SPI 接口,最关键的是它属于 MRAM(磁性随机存储器),写入不需要擦除、没有写延迟、字节级随机写、理论擦写次数近乎无限。这几个特性放在工业数据记录场景里,几乎是降维打击。而 PIC18F4620 是 Microchip 经典到不能再经典的 8 位单片机,64KB Flash、3968 字节 RAM、内置 MSSP 模块支持 SPI 主模式,工作温度覆盖工业级,价格便宜、供货稳定,很多老项目维护和中小批量产品里还在大量使用。
把这两颗芯片凑到一起,核心目标就一个:在低成本 8 位平台上,实现高可靠、高频次、掉电安全的数据存储与读取。这套组合适合谁?适合做工业仪表、数据记录仪、设备参数存储、黑匣子类产品的嵌入式工程师,也适合刚接触 SPI 外设和外部存储器、想找一个完整可复现案例练手的朋友。整套方案不依赖操作系统,裸机就能跑,代码量可控,调试手段也直观。
1.2 为什么是 MRAM 而不是 EEPROM 或 Flash
选型这件事,我一般会拿一张表把候选方案摆开对比,不然很容易被“便宜”两个字带偏。下面是我在实际项目中整理的对比,参数基于常见器件手册的典型值:
| 特性 | MR25H40CDF (MRAM) | 典型 SPI EEPROM | 典型 SPI NOR Flash |
|---|---|---|---|
| 写入前擦除 | 不需要 | 不需要 | 必须按扇区擦除 |
| 字节写入时间 | 无内部写延迟 | 约 5ms/页 | 页编程约 0.7ms,擦除约 45ms |
| 擦写寿命 | 近乎无限 | 约 100 万次 | 约 10 万次 |
| 掉电数据保持 | 数据即时固化 | 需等待写周期 | 需等待写周期 |
| 随机写效率 | 极高 | 低 | 极低 |
| 单位容量成本 | 偏高 | 低 | 最低 |
从表里能看出来,MRAM 的短板只有一个字:贵。但工业场景里,一次现场故障的返修成本,往往能买几百颗 MRAM。尤其是那些“每秒写一次、连续写几年”的应用,EEPROM 的寿命根本扛不住,Flash 的擦除开销又会把 CPU 拖死。MR25H40CDF 的写入是即时完成的,SPI 时钟发完,数据就已经在存储单元里了,不需要轮询状态寄存器等写完成,这一点在掉电保护逻辑里非常关键。
注意:MRAM 虽然写入快,但它的 SPI 时序和普通 EEPROM 并不完全一样,尤其是状态寄存器和写使能的处理,不能直接照搬 25 系列 EEPROM 的驱动代码。
1.3 PIC18F4620 的 SPI 资源够不够用
PIC18F4620 内置的 MSSP 模块可以配置成 SPI 主模式,支持时钟极性、相位、时钟频率的灵活设置。它的 SPI 时钟由系统时钟分频得到,公式是:
Fosc = Fcy × 4 SPI 时钟 = Fosc / (4 × (SSPADD + 1)) (主模式下的分频关系)假设晶振用 10MHz,系统时钟 Fosc 就是 10MHz,指令周期 Fcy 是 2.5MHz。如果 SSPADD 设为 0,SPI 时钟最高可以到 2.5MHz 左右。MR25H40CDF 支持的最高 SPI 时钟是 40MHz,所以 PIC18F4620 这边完全不是瓶颈,反而要担心的是 PCB 走线和信号完整性。实际项目里我一般把 SPI 时钟设在 1MHz 到 2.5MHz 之间,兼顾速度和稳定性,长排线或者工装环境里还会再降一档。
引脚分配上,PIC18F4620 的 MSSP 默认使用 RC3(SCK)、RC4(SDI)、RC5(SDO),片选 CS 用普通 IO 口控制,比如 RC2 或者 RB0。这里有个细节:硬件 SPI 只负责时钟和数据线,片选必须由软件控制,因为 MRAM 的片选需要在每个命令前后精确拉低拉高,硬件片选反而不好控制时序。
2. 硬件连接与底层时序拆解
2.1 硬件连线与去耦处理
先把线接对,后面才有得聊。MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很清晰:
- VCC:3.3V 供电
- GND:地
- SCK:SPI 时钟,接 PIC18F4620 的 RC3
- SI:数据输入,接 RC5(单片机输出)
- SO:数据输出,接 RC4(单片机输入)
- CS:片选,接 RC2
- WP:写保护,不用时接 VCC
- HOLD:保持,不用时接 VCC
这里要重点说去耦。MRAM 在写入瞬间的电流波动比 EEPROM 大,我在板子上一般会在 VCC 和 GND 之间放一颗 0.1uF 陶瓷电容,紧贴芯片引脚,再并一颗 1uF 的钽电容做低频储能。别小看这一颗电容,我遇到过好几次“读正常、写偶尔出错”的问题,最后查出来都是去耦不到位,电源纹波在写操作时把数据打乱了。
提示:WP 和 HOLD 引脚千万不要悬空。悬空时引脚电平不确定,可能导致写保护误触发或者通信被意外保持,表现为“有时候能写有时候不能写”,非常难查。
2.2 SPI 模式选择与命令集解析
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我一般用模式 0,因为 PIC18F4620 的 MSSP 配置成模式 0 最直观:时钟空闲为低,数据在上升沿采样。配置寄存器时,SSPCON1 的 CKP 位设为 0,SSPSTAT 的 CKE 位设为 0,就对应模式 0。
它的命令集和 25 系列 EEPROM 很像,但有几个关键区别:
| 命令 | 编码 | 作用 | 注意事项 |
|---|---|---|---|
| WREN | 0x06 | 写使能 | 每次写操作前必须发 |
| WRDI | 0x04 | 写禁止 | 写完后建议发 |
| RDSR | 0x05 | 读状态寄存器 | 检查 WEL 和 BP 位 |
| WRSR | 0x01 | 写状态寄存器 | 配置块保护 |
| READ | 0x03 | 读数据 | 24 位地址 |
| WRITE | 0x02 | 写数据 | 24 位地址,无写延迟 |
地址是 24 位的,因为 4Mbit 等于 512KB,需要 19 位地址,但芯片统一用 3 个字节传输地址,高位补零。这一点在写驱动时要注意,别只发两个字节地址,否则数据会写到错误的偏移上。
2.3 写使能与状态检查的必要性
很多人写 EEPROM 驱动写习惯了,觉得 WREN 可有可无,直接发 WRITE 命令就完事。MRAM 不行,每次写操作前必须发 WREN,否则写操作会被静默忽略,而且不会报错,读回来还是旧数据。这个坑我踩过,当时调了半天以为时序有问题,最后发现是漏了 WREN。
正确的写流程是这样的:
- 拉低 CS
- 发送 WREN(0x06)
- 拉高 CS
- 拉低 CS
- 发送 WRITE(0x02)
- 发送 24 位地址
- 发送数据字节
- 拉高 CS
注意第 3 步和第 4 步之间必须拉高 CS,让 WREN 命令生效。如果 WREN 和 WRITE 在同一个 CS 低电平周期里连续发,芯片不会识别写使能,写操作照样失败。这个细节在数据手册里写得很清楚,但很容易被忽略。
状态寄存器读取也有用。RDSR 返回的字节里,bit0 是 WEL(写使能锁存),bit1 是 BP0,bit2 是 BP1,这些块保护位决定了哪些区域可写。如果发现写不进去,先读状态寄存器看看 WEL 是不是 1,BP 位是不是把目标区域保护了。
3. 驱动代码实现与关键函数
3.1 SPI 初始化与字节收发函数
先上底层。PIC18F4620 用 C18 或者 XC8 编译器都可以,我这里用 XC8 的写法,寄存器名是通用的。
#include <xc.h> #define CS_PIN PORTCbits.RC2 #define CS_TRIS TRISCbits.TRISC2 void SPI_Init(void) { CS_TRIS = 0; // CS 配置为输出 CS_PIN = 1; // 初始拉高,不选中 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC4 = 1; // SDI 输入 TRISCbits.TRISC5 = 0; // SDO 输出 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SPI 主模式 Fosc/4 SSPSTAT = 0x00; // CKE=0, 模式 0 SSPADD = 0; // 分频系数最小,时钟最快 } unsigned char SPI_Transfer(unsigned char data) { SSPBUF = data; while(!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; // 清标志 return SSPBUF; }这段代码里,SSPADD = 0让 SPI 时钟跑到 Fosc/4,10MHz 晶振下就是 2.5MHz。如果通信不稳定,把 SSPADD 加大,比如设成 3,时钟就降到 625kHz。我一般先用低速调通,再逐步提速。
SPI_Transfer是阻塞式的,发一个字节等一个字节。在 8 位平台上,这种写法最简单可靠,不需要中断和 DMA。MRAM 的读写速度远高于 SPI 时钟,所以瓶颈在 SPI 本身,阻塞式完全够用。
3.2 MRAM 写使能与写数据函数
写函数是核心,逻辑必须严谨:
void MRAM_WriteEnable(void) { CS_PIN = 0; SPI_Transfer(0x06); // WREN CS_PIN = 1; } void MRAM_WriteByte(unsigned long addr, unsigned char data) { MRAM_WriteEnable(); CS_PIN = 0; SPI_Transfer(0x02); // WRITE SPI_Transfer((addr >> 16) & 0xFF); // 地址高字节 SPI_Transfer((addr >> 8) & 0xFF); // 地址中字节 SPI_Transfer(addr & 0xFF); // 地址低字节 SPI_Transfer(data); CS_PIN = 1; // MRAM 无写延迟,无需等待 }这里地址用unsigned long,因为 512KB 需要 19 位,unsigned int在 8 位平台上只有 16 位,不够用。移位操作要显式转成 8 位,避免符号扩展问题。
写完之后不需要轮询状态,这是 MRAM 相比 EEPROM 最大的优势。EEPROM 写完要等 5ms,期间 CPU 要么空转要么切任务,MRAM 直接返回,下一句代码就能继续执行。
3.3 读数据与页写优化
读函数更简单:
unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS_PIN = 0; SPI_Transfer(0x03); // READ SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); data = SPI_Transfer(0x00); // 发空字节读数据 CS_PIN = 1; return data; }虽然 MRAM 支持字节写,但连续写多个字节时,可以在一个 CS 低电平周期里连续发地址和数据,芯片内部地址会自动递增。这样比每字节都发一次 WREN 和地址要快得多。我实测过,连续写 256 字节,用页写方式比逐字节写快大约 3 倍。
void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_WriteEnable(); CS_PIN = 0; SPI_Transfer(0x02); SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for(i = 0; i < len; i++) { SPI_Transfer(buf[i]); } CS_PIN = 1; }注意:连续写的时候,地址会从当前地址一直递增到 0x7FFFF 然后回绕到 0x00000。如果写入长度超过剩余空间,数据会覆盖到开头,这个行为在数据手册里有说明,写之前一定要做边界检查。
4. 数据可靠性与掉电保护实战
4.1 掉电检测与写入时机
工业场景里,掉电是常态。MRAM 虽然写入即时,但如果掉电发生在 SPI 传输过程中,数据可能只写了一半。所以掉电保护的核心不是“等写完成”,而是“在掉电前把关键数据写完”。
我的做法是用一个 GPIO 检测电源电压,配合比较器或者 ADC,当电压降到阈值以下时触发中断,在中断里把关键参数写入 MRAM。因为 MRAM 没有写延迟,只要 SPI 时钟还在,几个字节的写入在微秒级就能完成,比 EEPROM 靠谱得多。
具体阈值怎么定?假设系统 3.3V 供电,MRAM 最低工作电压 2.7V,那么掉电检测阈值设在 3.0V 左右比较合适。从 3.0V 掉到 2.7V 的时间,取决于电源端的储能电容。用公式估算:
t = C × ΔV / I假设储能电容 100uF,负载电流 20mA,ΔV 是 0.3V,那么 t = 100e-6 × 0.3 / 0.02 = 1.5ms。1.5ms 足够 SPI 以 1MHz 时钟写入几十个字节。这个计算在选电容和设阈值时非常有用,别凭感觉拍脑袋。
4.2 数据校验与冗余存储
MRAM 本身可靠性很高,但 SPI 通信在工业电磁环境里可能受干扰。我在关键数据上会加一层校验,常用的是 CRC16 或者简单的异或校验。存储结构一般是这样:
| 偏移 | 内容 | 长度 |
|---|---|---|
| 0x00000 | 数据头(魔数) | 4 字节 |
| 0x00004 | 参数版本号 | 2 字节 |
| 0x00006 | 参数数据 | N 字节 |
| 0x00006+N | CRC16 校验 | 2 字节 |
读取时先检查魔数,再算 CRC,两者都对才认为数据有效。如果校验失败,就回退到备份区。备份区放在另一个地址段,写入时先写主区再写备份区,读取时主区坏了读备份区。这套逻辑在 EEPROM 上因为写入慢很难实现,MRAM 上几乎没有额外开销。
4.3 常见问题排查速查表
调这套组合的过程中,我整理了一份问题速查表,基本都是实际踩过的坑:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读出来全是 0xFF | CS 没拉低或 SPI 模式不对 | 示波器看 CS 和 SCK 波形 |
| 写进去读出来是旧数据 | 漏发 WREN 或 WREN 和 WRITE 在同一 CS 周期 | 检查写流程,分开 CS |
| 偶尔写入错误 | 电源去耦不足或 SPI 时钟太快 | 加电容,降低 SPI 时钟 |
| 地址错乱 | 地址只发了 2 字节 | 确认发满 24 位地址 |
| 状态寄存器 WEL 始终为 0 | WP 引脚被拉低或悬空 | WP 接 VCC |
| 通信完全无响应 | HOLD 引脚悬空或接线错误 | HOLD 接 VCC,检查连线 |
提示:调试 SPI 设备,示波器或者逻辑分析仪是必备的。光看代码很难发现问题,波形一抓,CS、SCK、SI 的时序对不对一目了然。我习惯先用低速时钟抓一次完整读写波形,确认命令、地址、数据都对得上,再提速。
5. 性能实测与场景延展
5.1 实测写入速度与寿命表现
我在 10MHz 晶振、SPI 时钟 2.5MHz 的条件下做过实测。单字节写入(含 WREN 和地址)大约耗时 8 微秒,连续页写 256 字节大约耗时 850 微秒,平均每字节 3.3 微秒。作为对比,同条件下 EEPROM 单字节写入要 5 毫秒以上,差距是三个数量级。
寿命方面,MRAM 的擦写次数标称是 10^14 次以上,实际上根本写不到这个量级。我做过一个连续写入测试,每秒写 1000 次,连续跑了 72 小时,大约 2.6 亿次写入,读写数据全部正常。这个测试虽然不能代表长期可靠性,但至少说明在工业数据记录的频次下,寿命完全不是问题。
5.2 这套方案还能怎么扩展
MR25H40CDF 加 PIC18F4620 的组合,本质上是一个“高可靠数据存储节点”。在这个基础上可以延展很多方向:
- 数据记录仪:加一颗 RTC 和传感器,定时把数据写入 MRAM,通过 UART 或者 CAN 导出。
- 设备参数管理:把校准参数、配置信息存在 MRAM,支持在线修改和掉电保存。
- 黑匣子功能:循环写入运行日志,掉电后通过接口读取最后一段记录。
- 工装测试:在产线上快速写入测试参数和序列号,MRAM 的写入速度能显著提升工装节拍。
如果项目对容量要求更高,可以换更大容量的 MRAM 型号,SPI 驱动逻辑基本不用改,只需要调整地址位宽。如果主控升级到 32 位平台,SPI 时钟可以拉到 10MHz 以上,写入速度还能再上一个台阶。
我个人在实际操作中的体会是,MRAM 这类器件的价值不在于“更快”,而在于“更省心”。不用等写完成、不用算擦除次数、不用担心掉电丢数据,这些省下来的精力可以放在产品功能本身。PIC18F4620 虽然老,但胜在稳定、资料全、供货稳,配 MRAM 做工业存储节点,是一套很务实的组合。最后再分享一个小技巧:调试阶段可以在 MRAM 里划一块区域专门存调试计数器和错误码,每次复位后读出来看看上次运行到哪、出了什么错,比单纯用串口打印日志更可靠,因为串口在掉电时往往来不及输出。