做工业电子的人,对“存储”这两个字的感情很复杂。既要断电不丢数据,又要扛得住宽温、振动和电源波动,最好写数据的时候不用算擦除寿命、不用等编程时间,更不要偶尔丢一两个字节。以前我遇到这类需求,第一反应就是上 SPI EEPROM,便宜、驱动也熟,直到连续两次在户外设备上遭遇“最后一笔数据写坏”的故障,才下决心把方案换成 Everspin 的 MR25H40CDF MRAM,搭配 Microchip 的 PIC18F66K40 来做控制核心。这篇文章把从选型、硬件接线、驱动编写,到量产前踩过的一些坑和验证办法都整理出来,给正在评估 MRAM 的同行一个可以少走弯路的参考。
1. 为什么工业现场我最终选了 MR25H40CDF,而不是便宜得多的 EEPROM/Flash
1.1 先回忆一次真实掉电故障
之前做一台田间用的采集终端,供电来自太阳能加蓄电池,主控用 PIC,外挂了一个 256Kbit 的 SPI EEPROM,用来保存每小时一组的水位、流量和状态字。设备本身逻辑不复杂,但客户反馈偶发数据回读不对,有些记录写了一半就断了。接上示波器后发现,问题大多出现在供电跌落瞬间,主控虽然用“掉电检测中断”抢着去保存现场数据,但 EEPROM 的写周期固定要等几毫秒,中断里把 CS 拉低了、发完命令和数据,还没等页写完成,电压就掉到阈值以下了。传统 EEPROM 的擦写本质是电荷泵加浮栅电子注入,一旦电压不够,写入就会失败,而且你没法判断它到底写进去多少。
MRAM 的写入机制完全不是这么回事。MR25H40CDF 这一类磁阻存储芯片,不是靠电荷存储,而是靠磁隧道结中自由层的磁化方向来记录 0 和 1。写入就是用电流产生磁场把自由层翻转,断电之后磁化方向保持不变,所以数据天然就是非易失的。最核心的一点是,它没有“写周期”这个概念,写入延迟和普通 SRAM 几乎一样,写入过程中不存在电荷泵,也不会因为电压跌落导致半个字写坏。
1.2 MRAM 看起来和 SRAM 一样,但断电后不丢
实际用下来,你会发现最简单的理解方式就是把它当成“一颗掉电不丢内容的 SRAM”。对你写驱动的人来说,读它像读 Flash,写它像写 SRAM,不需要先擦后写,也不需要管理块均衡。在那些需要频繁记录、随时可能掉电、又不想引入复杂文件系统的场景,这个特性能把软件逻辑砍掉一大截。
我当时对比过几颗常见器件,表格如下:
| 特性 | 普通 SPI EEPROM | SPI NOR Flash | MR25H40CDF MRAM |
|---|---|---|---|
| 单字节随机写 | 支持,但有 3~5ms 编程时间 | 需要先擦除块,通常按页写 | 支持,无额外等待时间 |
| 写寿命 | 约 10 万~100 万次 | 约 1 万~10 万次/块 | 无限次(官方叫 essentially unlimited) |
| 掉电写失败风险 | 高,写周期对电压敏感 | 高,擦/写过程较长 | 极低,写入即时完成 |
| 是否需要擦除 | 不需要 | 必须块擦除 | 不需要 |
| 数据保持 | 典型 10 年以上 | 典型 10~20 年 | 典型 20 年以上 |
| 工作温度 | 常见 -40~85°C | 常见 -40~85°C | 支持 -40~125°C |
| 随机读速度 | 受 SPI 时钟限制 | 受 SPI 时钟限制 | 受 SPI 时钟限制,无额外延迟 |
如果你是做数据采集或仪表,一年按 52 万条记录算,传统 EEPROM 一百万的寿命不到两年就用完;Flash 虽然有磨损均衡,但管理算法和掉电保护逻辑又要占不少代码量。MRAM 在这些场景里省掉的不仅是硬件成本核算,更是一堆维护性的设计补偿。
1.3 最适合 MRAM 的嵌入式应用
从目标应用看,MR25H40CDF 很适合这几类需求:每分钟甚至每秒钟都需要记录一条数据的工业采集终端;需要保存传感器校准系数、加工参数,并且不允许回读出现坏块的管理系统;需要做故障录波或事件顺序记录的电力设备;还有车上、农机上那种电源环境很脏、温度波动大的控制器。它不适合的是超大数据量的多媒体存储,毕竟价格还降不到替代 TF 卡的程度,遇到几十 MB 以上的非易失存储需求,还是老老实实上 Flash 或专门方案。
2. MR25H40CDF 引脚、封装与 PIC18F66K40 接线
2.1 先把芯片规格吃透
MR25H40CDF 是 Everspin 的 4Mbit MRAM,内部组织是 512K 字节,也就是 512KB,容量安排是 512K x 8bit。SPI 接口支持模式 0 和模式 3,时钟速度按官方手册最高可以跑到 40MHz 左右,实际批量设计建议先按 20MHz 验证,再根据板材和信号完整性决定是否往上拉。工作电压为 2.7V 到 3.6V,不是 5V 器件,这点后面接线时要特别注意。封装是标准 8 脚 SOIC 或者 DFN,引脚间距和常规 SPI Flash 一致,我手头样片是 SOIC-8,手工焊接问题不大。
片子的命令集和普通 SPI Flash 高度相似,有写使能、读状态寄存器、读数据、写数据这几条基本指令。对老手来说最大的不同就是,写数据过程不需要等状态寄存器翻转,发完最后一个字节,数据就已经在存储阵列里生效了。
2.2 确定单片机引脚分配
PIC18F66K40 这代 8 位机带增强型 SPI 模块,跑在 64MHz 系统时钟下很轻松,3.3V 供电时也能跑到足够高的外设频率。我这里用的是 SPI1 模块,默认引脚对是 SCK 在 RC3、SDO 在 RC5、SDI 在 RC4,片选用普通 GPIO。需要注意,PIC18F66K40 的 SPI 引脚可以通过 PPS 映射到其他引脚,如果你板子上这几个脚被占了,完全可以重映射,但最好把片选之外的 CS 也安排到一路干净的 GPIO 上。
接线逻辑如下:
| MR25H40CDF 引脚 | 功能 | 接到 PIC18F66K40 | 备注 |
|---|---|---|---|
| 1 | CS# | RA0 | 片选,低有效 |
| 2 | SDO | RC4/SDI | 芯片输出,接单片机的 SPI 数据入 |
| 3 | WP# | 3.3V | 不用保护时直接上拉 |
| 4 | VSS | GND | 电源地 |
| 5 | SI | RC5/SDO | 芯片输入,接单片机的 SPI 数据出 |
| 6 | SCK | RC3/SCK | SPI 时钟 |
| 7 | HOLD# | 3.3V | 必须接高,不许悬空 |
| 8 | VDD | 3.3V | 电源正 |
这个表里最容易出差错的是 WP# 和 HOLD#。HOLD# 一旦悬空,芯片偶尔会进入保持状态,不再响应命令,现象看起来像 SPI 死掉或者数据全 FF。WP# 在大多数工程里不需要启用硬件写保护,因为状态寄存器里的保护位已经够用,直接拉高最省心。
2.3 电平匹配是个大坑
MR25H40CDF 只有 3.3V 工作电压。PIC18F66K40 本身支持 1.8V 到 5.5V 供电,很多人习惯性把整板跑在 5V,结果 SPI 主输出高电平 5V 直接怼到芯片的 SI 引脚,轻则读回来异常,重则长期使用后损伤芯片。我在这批板卡上把主控和 MRAM 都放在 3.3V 供电域,PIC18F66K40 在 3.3V 下工作没问题,只是系统时钟最高频率会限制在 40MHz 左右,对存储读写场景完全够用。如果你整个系统必须跑在 5V,那么 SPI 信号线上至少要做电平转换,或者用电阻分压把 MOSI、SCK、CS 都压到 3.3V,而 MISO 方向因为是从设备输出 3.3V 到 5V 单片机,通常还能忍,但最好也确认一下高电平阈值。
2.4 PCB 上这些小细节会影响稳定性
工业环境不比实验室,电源纹波、电机启停、无线发射都可能干扰存储芯片。我在布局上做了三件事:VDD 引脚旁放了 100nF 陶瓷电容,并且尽量贴近引脚;芯片正下方没有走高频时钟线;CS、SCK、SI 三根线在空间上离感性负载比较远。另外,CS 线不建议在多个 SPI 从设备间共用一组很长的排线,因为 MRAM 的 CS 一旦受到振铃干扰,可能被误触发为一个无效操作。对于长线连接,可以在 CS 线上加一个小阻值串联电阻,比如 22 欧姆,能很有效地抑制反射。
3. 存储引擎部分:命令集、写使能机制和驱动分层
3.1 几条必须背下来的命令
MR25H40CDF 的命令和多数 SPI 存储一致,片选拉低后从 SI 输入命令字节,每个字节都是高位在前。常用命令表如下:
| 命令名 | 命令字节 | 操作说明 |
|---|---|---|
| WREN | 0x06 | 写使能,必须在每次写操作前发送 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器,可配置保护位 |
| READ | 0x03 | 读数据,紧跟 3 字节地址 |
| FAST_READ | 0x0B | 读数据,紧跟 3 字节地址并带 1 个 dummy 字节 |
| WRITE | 0x02 | 写数据,紧跟 3 字节地址和数据 |
地址是 3 字节,也就是 24 位格式,但容量只有 512KByte,实际有效地址是 19 位,范围 0x000000~0x7FFFF。发地址时高位字节只使用低 3 位,其余位写成 0,别犯把地址高位写成 FF 的低级错误。
3.2 写使能锁存器:最容易漏掉的一环
MRAM 的状态寄存器里有一位 WEN 写使能状态位。芯片上电后默认不允许写,必须先发一条 WREN,状态寄存器里的 WEN 才会置 1。执行完一次 WRITE 命令后,WEN 会自动清零。也就是说,每次写操作前都必须重新发 WREN,除非你从状态寄存器读到 WEN 仍然是 1。这是很多人第一次移植驱动时最容易翻车的地方:按照读操作那样直接发 WRITE,结果读回数据还是旧值,又没报任何错误。
驱动设计上我把 WREN 封装成一个独立函数,在写操作的开始处调用,并且在写操作完成后不依赖任何状态寄存器的等待,因为确实没有可等的。这也是 MRAM 的特色:你不需要写一个“等待忙状态”的轮询函数,命令结束即写入完成。
3.3 状态寄存器的位含义
状态寄存器可以随时用 RDSR 读出来,低两位是 BP0、BP1,用来配置保护区域,默认全 0 表示整片可写。第 4 位附近一般能看到 WEN、WP# 和 HOLD# 的实时状态。调试时可以用 RDSR 确认是不是写使能没有打开。实际工程里,如果不想让误操作覆盖重要校准数据,可以在初始化时设置保护位,把高地址段锁住。但大多数场景下,我把保护位留成默认,在软件层面用地址校验和 CRC 来保证数据完整性。
3.4 驱动分层,方便以后换芯片
我建议驱动代码分三层写。最底层是 SPI 字节收发函数,中间层是 MRAM 命令函数,最上层是用户友好的多字节读写函数。这样以后你把 PIC18F66K40 换成别的单片机,或者把 MRAM 换成其他 SPI 设备,只需要改最底层和片选宏,上层业务代码不用动。我实际工程里就是按照这个思路组织的,下面第 4 章给的就是这套代码的核心骨架。
4. 完整的 PIC18F66K40 驱动代码与读写调用示例
4.1 开发环境和基础初始化
我用的是 MPLAB X 和 XC8 编译器。单片机主频配置为 32MHz,SPI 时钟分频后大概在 4MHz 左右,这个速度对 40MHz 上限的 MRAM 来说非常稳妥,也足够大部分工业场景使用。MRAM 片选接 RA0,配置为数字输出;SPI1 模块使用 RC3/RC4/RC5 默认引脚对。
先看初始化和底层收发:
#include <xc.h> #include <stdint.h> // 片选控制 #define MRAM_CS_LOW() LATAbits.LATA0 = 0 #define MRAM_CS_HIGH() LATAbits.LATA0 = 1 #define MRAM_PAGE_SIZE 512U #define MRAM_SIZE 0x80000UL // 512KByte void MRAM_Init(void) { // 配置片选引脚 TRISAbits.TRISA0 = 0; ANSELAbits.ANSA0 = 0; LATAbits.LATA0 = 1; // 配置 SPI1 主模式,SPI Mode 0 SPI1CON0 = 0x00; SPI1CON1 = 0x00; SPI1CON0bits.MST = 1; // 主机模式 SPI1CON0bits.CKP = 0; // SCK 空闲为低 SPI1CON1bits.CKE = 1; // 对应 SPI Mode 0,具体位名以你的寄存器手册为准 SPI1CON0bits.LSBF = 0; // MSB 先发 SPI1CON0bits.SPIEN = 1; // 启动 SPI 模块 } uint8_t MrSPI_Transfer(uint8_t byte) { SPI1TXB = byte; // FIFO 方式发送 while (!SPI1STATUSbits.SPI1BF) { // 等待接收缓冲有新数据 ; } return SPI1RXB; }这里需要说明一下,PIC18F66K40 的 SPI 模块寄存器命名在不同批次资料里可能有差异,有些新芯片把 CKE、CKP 归到 SPI1CON1,有些归到 SPI1CON0。上面代码是常见系列应用的写法,你如果用的型号引脚定义不一致,按手头头文件里的位定义调整即可。核心点是模式 0:空闲时 SCK 低电平,数据在时钟上升沿有效。
4.2 命令层函数
接下来是几条关键命令:
void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MrSPI_Transfer(0x06); // WREN MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t st; MRAM_CS_LOW(); MrSPI_Transfer(0x05); // RDSR st = MrSPI_Transfer(0xFF); MRAM_CS_HIGH(); return st; } void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS_LOW(); MrSPI_Transfer(0x03); // READ MrSPI_Transfer((uint8_t)((addr >> 16) & 0xFF)); MrSPI_Transfer((uint8_t)((addr >> 8) & 0xFF)); MrSPI_Transfer((uint8_t)(addr & 0xFF)); for (i = 0; i < len; i++) { buf[i] = MrSPI_Transfer(0xFF); // 读数据 } MRAM_CS_HIGH(); }读操作不受页边界限制,可以从任意地址连续读任意长度,内部地址会递增,跨过 512KB 上限后会回绕到 0,工程上注意别读超范围就行。真正麻烦的是写操作。
4.3 跨页写函数:这个必须认真处理
MR25H40CDF 的 WRITE 命令单次最多写 512 字节,而且不能跨越 512 字节的页边界。如果你的起始地址不在页首,第一段只能写到当前页末尾,然后必须重新发起一次 WRITE,接着写下一页。我见过很多移植失败的人,就是忽略了这一点,只处理了“长度小于 512”的情况,结果地址跨越页边界后数据跑到错误位置。
我的写法如下:
void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; uint16_t pageLeft; uint16_t n; // 简单防越界截断 if ((addr + len) > MRAM_SIZE) { len = MRAM_SIZE - addr; } while (len > 0) { // 计算当前页还有多少空间 pageLeft = MRAM_PAGE_SIZE - (uint16_t)(addr & (MRAM_PAGE_SIZE - 1)); n = (len > pageLeft) ? pageLeft : (uint16_t)len; // 每次写事务前先写使能 MRAM_WriteEnable(); MRAM_CS_LOW(); MrSPI_Transfer(0x02); // WRITE MrSPI_Transfer((uint8_t)((addr >> 16) & 0xFF)); MrSPI_Transfer((uint8_t)((addr >> 8) & 0xFF)); MrSPI_Transfer((uint8_t)(addr & 0xFF)); for (i = 0; i < n; i++) { MrSPI_Transfer(buf[i]); } MRAM_CS_HIGH(); // 拉高 CS,本次写入完成 buf += n; addr += n; len -= n; } }这个函数虽然叫 WriteBytes,实际可以写任意长度。第一次调用完整写 300 字节、起始地址是 0x1FF 时,第一次只会写 1 个字节到页尾,第二次再从下一页偏移 0 继续写 299 字节。这样既不会跨页,也避免了大量冗余的临时缓存管理。
4.4 一个实际的数据存储示例
工业设备存数据很少只存一个裸数据,我一般会把记录封装成结构体,再加上魔数、序列号、CRC。这样回读时能判断是不是完整记录。以一条 64 字节的日志为例:
typedef struct { uint32_t seq; uint32_t timestamp; uint16_t adcValue; uint16_t temperature; uint16_t crc; uint8_t reserved[48]; } LogEntry; void SaveLogEntry(uint32_t addr, const LogEntry *entry) { MRAM_WriteBytes(addr, (const uint8_t *)entry, sizeof(LogEntry)); } uint8_t ReadAndVerifyLog(uint32_t addr, LogEntry *entry) { uint16_t calcCrc; MRAM_ReadBytes(addr, (uint8_t *)entry, sizeof(LogEntry)); calcCrc = CalcCRC16((uint8_t *)entry, sizeof(LogEntry) - sizeof(uint16_t)); if (calcCrc != entry->crc) { return 0; // 数据无效 } return 1; }由于 MRAM 是字节可写、不需要擦除,你可以把日志按固定地址顺序写入,读出来时检查魔数和 CRC 就可以区分“从来没写过”和“写过但数据损坏”两种情况。传统 EEPROM 方案里我们还得考虑掉电时写了一半的问题,现在这部分逻辑可以直接删掉。
4.5 初始化时先确认通信是否正常
上电后建议做一次基本握手,最简单的办法是读状态寄存器,然后对某个固定地址先写 0x55、0xAA 再读回来。第一次调试时如果全返回 0xFF,先别急着怀疑芯片,多半就是接线或者 SPI 模式不对;如果全返回 0x00,先检查 CS 是否一直处于拉低状态,或者芯片处于 HOLD。写一个短的自检函数放在主循环开头,能省掉不少背时的纠结。
5. 我把这些坑踩过一遍后,写驱动时加上的防御式处理
5.1 写使能没成功,但代码不报错
MRAM 和 EEPROM 一个很大的区别是,EEPROM 在写命令结束后状态寄存器会显示忙状态,如果写使能没成功,可能还会返回错误;MR25H40CDF 的数据线是复用双向的,很多情况下发 WRITE 命令时如果 WREN 没置 1,整条命令会直接忽略,不做任何提示,读回来还是老数据。我第一次移植时就被这个坑了十分钟,一直在查 SPI 时钟和引脚,后来用逻辑分析仪看到我的 WREN 和 WRITE 之间片选动作不对,才明白问题出在自己把 WREN 和 WRITE 放在同一次 CS 拉低事务里了。
要明确两个动作是独立事务:先 CS 拉低,发 0x06,CS 拉高,写使能置位;之后 CS 再次拉低,发 WRITE 命令。不要把 WREN 和 WRITE 首尾相接放在同一个 CS 低电平窗口内。
5.2 HOLD# 悬空导致偶发“失联”
有一块测试板,刚焊好时完全正常,几天后连续工作两三个小时就开始偶发读回全 FF,重新上电又好了。排查到最后发现是 HOLD# 引脚没有连接,悬空状态下受电源纹波干扰,芯片时不时把自己 Hold 住。后来把 HOLD# 和 WP# 都通过 10k 电阻接到 3.3V,跑了一周再没出现过这个现象。这件事给我的教训是:MRAM 这类有特殊控制引脚的芯片,所有控制脚不许悬空,哪怕你觉得用不到。
5.3 5V 电平直接怼进去
前面说过 MR25H40CDF 工作电压最高 3.6V。我同事有一块试验板直接从旧产品上借来了五伏供电的 PIC18F66K40,整板其他电路全是 5V 逻辑,接上 MRAM 后写入十次有五六次读回来不对。测试最后发现不是 SPI 时序问题,而是 SI 和 SCK 的高电平超过了芯片绝对最大额定值。工业上很多单片机虽然标称 5V 工作,但实际 I/O 高电平就在 4.8V 左右,对 3.3V 器件长期积压会有损伤。如果你没法改主板供电,至少要在这几根线上加电平转换,别贪省事。
5.4 SPI 模式不要想当然
MR25H40CDF 支持模式 0 和模式 3,但你的 SPI 主设备要固定选择其中一种,不能改来改去。有次我把 PIC 的 SPI 配置改了分频,顺手把 CPOL 也设置了,结果一会儿读得对一会儿读不对,还因为数据采样沿偏差导致某些字节差一个位。排查方法就是拿示波器看 SCK 空闲电平是不是正确的低电平(模式 0),以及读回的数据在 SCK 上升沿是否已经稳定。SPI 早期调试建议把时钟分频拉到最保守的 1MHz,先跑通再提速。
5.5 诊断链路:背景全 FF 时按这个顺序查
如果初始化后读数据全是 0xFF,大概率是通信没建立,不是数据真的没写进去。我推荐按下面顺序排查:
- 用万用表量 VDD 和 VSS,确认是 3.3V 左右。
- 量 HOLD# 和 WP#,确认已经拉高。
- 用示波器看 CS,拉低时应该是干净的低电平,没有长振铃。
- 发一条 RDSR,看返回字节能不能出来。如果返回 0xFF,大概率是线路或 SPI 配置问题。
- 写一个只写 0x55、0xAA 的测试函数,连续写 10 次再读,看是不是稳定一致。
如果读出来全 0x00,一般怀疑 CS 被拉死在地或者 SCK 有异常的毛刺。用这个顺序排查,百分之九十的情况能在半小时内定位。
6. 板级验证、实测数据和你现在就可以照抄的检查清单
6.1 掉电反复写测试
量产前我做了一个小实验:用一只继电器给板卡反复快速通断电,每轮断电前主控检测掉电信号,然后往 MRAM 里追加写入一条 32 字节记录,加电后回读所有记录并做 CRC 校验。连续跑了 2000 轮断电,记录条数一条不差,没有出现半条写入、也没有出现读到 FF 的情况。这个测试如果放在之前那套 EEPROM 方案上,估计早就翻车了。
6.2 高频写入压力测试
我又把写入频率拉高到每秒 100 次,也就是一天约 864 万次写入,连跑 24 小时,总计接近 2 亿次写操作。读取结果仍然全部正确。这样的寿命压在 EEPROM 或 Flash 上早就到极限了,MRAM 这边连位翻转都没观察到。当然,2 亿次测试不能证明芯片无限寿命的理论,但至少说明我选的这颗芯片在这种写入压力下没有性能衰退。
6.3 温度和电源波动测试
工业环境肯定要看宽温和电源纹波。我把整板放进温箱,从 -40°C 到 85°C 循环,每个温度点驻留 2 小时,期间每 10 秒写一条数据并回读校验。结果在 -40°C 低温下写入正常、读回正常,85°C 下同样正常。电源纹波测试用电子负载叠加 100mV 的 100kHz 纹波,MRAM 部分没有出现数据翻转。这说明在合理的 PCB 布局和退耦条件下,MRAM 对电源的敏感度甚至低于很多 Flash。
6.4 我也不建议完全不做保护
虽然 MRAM 很皮实,但不代表你可以完全裸奔。我建议驱动层仍然加上地址边界检查、CRC16 校验和魔数检查,原因很简单:总线上的干扰、代码跑飞、看门狗复位的异常行为,都可能往存储器里写进不完整的数据。MRAM 保证的是“你写进去什么,读出来就是什么”,但不保证你的软件一定写进了对的东西。软件防护和存储介质可靠性是两件事,别混为一谈。
6.5 几个我个人的做法,共勉
如果你准备在下一个项目里用 MR25H40CDF 搭配 PIC18F66K40,我建议第一版固件里就带上状态寄存器读取函数和页写分块逻辑,别等出了问题再加。首次打板先焊一片专用测试板,把 SPI 信号飞线接出来,方便挂逻辑分析仪。通讯速率先低后高,稳定了再去追求吞吐量。最后,一定要把 HOLD# 和 WP# 的 10k 上拉电阻画到原理图里,这是最容易写进评审清单、又最容易在画 PCB 时被漏掉的细节。