MRAM 这东西,第一次接触的人多半会犯嘀咕:它到底算 RAM 还是 Flash?我在一个工业数据采集项目里用 MR25H40CDF 配 PIC18LF4585 做参数存储,前后折腾了将近三周,踩的坑比预想的多得多。这篇就把整个选型思路、SPI 时序调试、读写流程和实测中遇到的怪现象完整梳理一遍,给正在做嵌入式数据存储方案的同行一个可复现的参考。
MR25H40CDF 是 Everspin 出的 4Mbit 串行 MRAM,SPI 接口,512K×8 组织方式。PIC18LF4585 是 Microchip 的 8 位单片机,自带 MSSP 模块支持 SPI 主模式。两者搭配的核心场景就是:工业现场需要频繁写入、断电不能丢数据、写入寿命要扛得住十年以上连续运行。传统方案要么用 EEPROM(写寿命有限、速度慢),要么用铁电(容量小、贵),要么加电池做 SRAM 备份(维护成本高)。MRAM 在这几个维度上找到了一个比较舒服的平衡点。
1. 为什么在这个场景里选了 MRAM 而不是 EEPROM 或 FRAM
1.1 工业数据存储的三个硬指标
先把这个项目的需求摆出来。设备是工业现场的振动监测节点,每 200ms 采集一次三轴加速度数据,需要把特征值(RMS、峰值、峭度)和原始波形的摘要信息存下来。存储需求拆解下来是三个硬指标:
- 写入频率:每天约 43 万次写入(按 200ms 一次算),一年就是 1.5 亿次以上
- 数据保持:设备断电后数据至少保持 10 年不丢失
- 写入延迟:单次写入必须在 1ms 内完成,不能阻塞采集主循环
EEPROM 的写入寿命典型值是 100 万次,按每天 43 万次算,不到三天就写废了。就算做磨损均衡,512Kbit 的 EEPROM 分 100 个扇区轮换,也就撑 100 天左右。FRAM 写入寿命够(10^12 次以上),但容量做不大,而且价格按 bit 算比 MRAM 贵不少。SRAM+电池方案写入速度和寿命都没问题,但电池在工业现场高温环境下寿命会急剧缩短,维护更换成本太高。
MRAM 的写入寿命是 10^15 次以上,写入延迟在纳秒级(SPI 接口下受限于总线速度),数据保持 20 年以上,而且不需要电池。这三个指标全部满足,价格虽然比 EEPROM 贵,但比 FRAM 便宜,综合下来是最优解。
1.2 MR25H40CDF 的关键参数拆解
选型时我把 MR25H40CDF 的数据手册翻了好几遍,几个关键参数需要特别注意:
| 参数 | 数值 | 实际意义 |
|---|---|---|
| 容量 | 4Mbit (512K×8) | 存特征值够用,原始波形存不下 |
| 接口 | SPI Mode 0/3 | 和 PIC 的 MSSP 模块兼容 |
| 最高时钟 | 40MHz | PIC18LF4585 跑不到这么高 |
| 写入寿命 | 10^15 次 | 每天 43 万次能写 6000 年以上 |
| 数据保持 | 20 年 | 工业设备生命周期内不用换 |
| 工作电压 | 2.7V~3.6V | 注意和 PIC 的电平匹配 |
| 封装 | 8-SOIC / 8-DFN | 布板时注意 DFN 的散热焊盘 |
这里有个容易忽略的点:MR25H40CDF 的工作电压是 2.7V~3.6V,而 PIC18LF4585 的 LF 版本工作电压是 2.0V~5.5V。如果 PIC 跑在 5V,SPI 电平就会超过 MRAM 的绝对最大额定值。我一开始就犯了这个错,PIC 用 5V 供电,结果 MRAM 的 SPI 引脚上出现了 5V 电平,虽然没立刻烧,但数据手册明确写了输入电压最大不能超过 VDD+0.3V。后来把 PIC 改成 3.3V 供电才解决。
1.3 SPI 模式选择的坑
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。PIC18LF4585 的 MSSP 模块两种模式都支持,但配置寄存器的时候有个细节:CKP 位和 CKE 位的组合决定了采样边沿。
我一开始配的是 Mode 0,CKP=0,CKE=1(数据在时钟上升沿采样)。逻辑分析仪抓波形发现数据在第一个上升沿就被采样了,但 MRAM 的数据手册要求数据在时钟上升沿之前就稳定。后来改成 CKE=0(数据在时钟下降沿变化,上升沿采样),时序就对了。
提示:PIC18 的 MSSP 在 SPI 主模式下,CKE=1 表示在时钟上升沿之前数据就变化,CKE=0 表示在时钟下降沿之后数据变化。这和很多 ARM 芯片的 SPI 配置逻辑是反的,移植代码时特别容易搞混。
2. 硬件连接与 PCB 布局中那些数据手册不会告诉你的事
2.1 引脚连接与上拉电阻的必要性
MR25H40CDF 的引脚定义很标准:CS、SCK、SI、SO、VDD、VSS,加上 WP 和 HOLD(有些封装是 NC)。和 PIC18LF4585 的连接方式如下:
- CS 接 PIC 的 RA5(MSSP 的 SS 引脚,但配置为主模式时这个引脚可以当普通 IO 用)
- SCK 接 RC3(MSSP 的 SCK)
- SI 接 RC5(MSSP 的 SDO)
- SO 接 RC4(MSSP 的 SDI)
这里有个关键点:CS 引脚必须加上拉电阻,典型值 10K。为什么?因为 PIC 在上电复位期间,IO 口是高阻态,CS 如果悬空,MRAM 可能会误触发读写操作。我在第一版板子上没加上拉,结果每次上电后 MRAM 的第一个字节偶尔会变成 0xFF,查了很久才发现是上电瞬间 CS 抖动导致的。
WP 和 HOLD 引脚如果不用,建议直接拉到 VDD,不要悬空。虽然数据手册说内部有弱上拉,但在工业环境的电磁干扰下,弱上拉不一定靠得住。
2.2 PCB 布局的注意事项
SPI 总线在 10MHz 以上时,PCB 布局就开始影响信号完整性了。我总结了几条实测有效的规则:
- 走线尽量短:MRAM 和 PIC 的距离控制在 5cm 以内,超过这个距离就要考虑加串联端接电阻
- SCK 走线远离模拟信号:SCK 是高频时钟,容易耦合到模拟采集通道
- 电源去耦电容紧贴 VDD 引脚:0.1uF 陶瓷电容加 1uF 钽电容组合,放在 MRAM 的 VDD 引脚 2mm 以内
- 地平面完整:不要在地平面上开槽,SPI 的回流路径要连续
我第一版板子把 MRAM 放在了板子边缘,SCK 走线有 8cm 长,结果在 10MHz 时钟下读写偶尔出错。后来把 MRAM 移到 PIC 旁边,走线缩短到 3cm,问题就消失了。
2.3 电平匹配的实测数据
前面提到 PIC 用 5V 供电会超过 MRAM 的输入电压范围。我实测了一下不同供电电压下的情况:
| PIC 供电 | MRAM 供电 | SPI 电平 | 实测结果 |
|---|---|---|---|
| 5.0V | 3.3V | 5V | 读写不稳定,偶尔出错 |
| 3.3V | 3.3V | 3.3V | 稳定,连续读写 72 小时无错误 |
| 2.5V | 3.3V | 2.5V | 稳定,但 PIC 跑不到 10MHz |
| 3.3V | 2.7V | 3.3V | MRAM 输入过压,不推荐 |
最终方案是 PIC 和 MRAM 都用 3.3V 供电,SPI 时钟跑到 8MHz(PIC18LF4585 在 3.3V 下的 MSSP 最高速度)。这个配置下连续读写 72 小时,误码率为零。
3. SPI 驱动层的实现细节与寄存器配置
3.1 MSSP 模块的初始化代码
PIC18LF4585 的 MSSP 模块配置 SPI 主模式,需要设置几个关键寄存器。下面是我实际用的初始化代码:
void SPI_Init(void) { // 配置引脚方向 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC4 = 1; // SDI 输入 TRISCbits.TRISC5 = 0; // SDO 输出 TRISAbits.TRISA5 = 0; // CS 输出 // 关闭模拟功能(如果有) ADCON1 = 0x0F; // MSSP 配置 SSPCON1 = 0x00; // 先清零 SSPSTAT = 0x00; // 先清零 SSPCON1bits.SSPM = 0b0000; // SPI 主模式,时钟 = Fosc/4 SSPCON1bits.CKP = 1; // 空闲时 SCK 为高电平(Mode 3) SSPSTATbits.CKE = 0; // 数据在时钟下降沿变化 // 时钟速度设置 // Fosc = 32MHz(8MHz 晶振 × 4 PLL) // SPI 时钟 = Fosc / (4 × (SSPADD + 1)) // 要得到 8MHz:SSPADD = 0 SSPADD = 0x00; SSPCON1bits.SSPEN = 1; // 使能 MSSP CS = 1; // 初始 CS 拉高 }这里有个细节:SSPADD 的计算。PIC18LF4585 的 SPI 时钟是 Fosc / (4 × (SSPADD + 1))。如果系统时钟是 32MHz,要得到 8MHz 的 SPI 时钟,SSPADD 应该设为 0。但 SSPADD=0 时实际时钟是 8MHz,这是理论值,实测用示波器量出来是 7.8MHz 左右,有轻微偏差,但不影响 MRAM 通信。
3.2 字节读写函数的实现
SPI 的字节读写是底层操作,MRAM 的所有命令都建立在这两个函数之上:
uint8_t SPI_ReadByte(void) { uint8_t data; SSPBUF = 0xFF; // 发送哑数据,产生时钟 while(!SSPSTATbits.BF); // 等待接收完成 data = SSPBUF; // 读取接收到的数据 return data; } void SPI_WriteByte(uint8_t data) { SSPBUF = data; // 写入发送缓冲 while(!SSPSTATbits.BF); // 等待发送完成 uint8_t dummy = SSPBUF; // 读走接收缓冲,清除 BF 标志 }注意 SPI_WriteByte 里最后那个读操作。PIC 的 MSSP 模块在发送完成后,BF 标志会置位,必须读一次 SSPBUF 才能清除。如果不读,下一次发送时 BF 标志还是置位的,会导致数据错乱。这个坑我在调试时踩过,现象是连续发送多个字节时,第二个字节开始就全是 0xFF。
3.3 MRAM 的命令集与操作流程
MR25H40CDF 的命令集比较简洁,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
写操作的完整流程是:拉低 CS → 发送 WREN → 拉高 CS → 拉低 CS → 发送 WRITE → 发送 24 位地址 → 发送数据 → 拉高 CS。注意 WREN 和 WRITE 之间必须拉高 CS,这是很多 SPI Flash 也有的要求,但 MRAM 的数据手册里写得比较隐蔽,容易忽略。
读操作简单一些:拉低 CS → 发送 READ → 发送 24 位地址 → 读取数据 → 拉高 CS。读操作不需要 WREN。
3.4 地址对齐与页边界问题
MR25H40CDF 的存储空间是 512K×8,地址范围 0x00000 到 0x7FFFF。写操作可以跨页吗?数据手册说可以,但实测发现跨页写入时,如果页边界正好在 16 字节对齐处,写入速度会略有下降。我猜测是内部缓冲区的刷新机制导致的,但不影响数据正确性。
不过有一个必须注意的点:写操作的最大时钟频率和读操作不同。读操作可以跑到 40MHz,但写操作在 3.3V 下最高只能到 20MHz。我一开始把 SPI 时钟设到 8MHz,远低于限制,所以没遇到问题。但如果有人想把时钟拉到 20MHz 以上做写入,就要注意这个限制。
4. 实测中遇到的三个怪现象与排查过程
4.1 上电后第一个字节读出来总是 0xFF
这个问题困扰了我两天。现象是:每次上电后,第一次读 MRAM 的任意地址,返回的都是 0xFF。第二次读同一个地址就正常了。
排查过程:
- 先用逻辑分析仪抓 SPI 波形,发现第一次读的时候,CS 拉低后 SCK 上只有 8 个时钟脉冲,但 READ 命令需要 8 位命令 + 24 位地址 = 32 个时钟。也就是说,第一次读的时候,地址根本没发出去。
- 检查代码,发现 SPI_Init() 之后直接调用了 MRAM_Read(),但 CS 引脚在初始化时设为输出高电平,而 MRAM 上电后需要至少 1ms 的稳定时间才能接受命令。
- 在 SPI_Init() 之后加了 2ms 延时,问题解决。
注意:MRAM 的上电稳定时间数据手册写的是 1ms,但实测在 3.3V 供电下,如果电源上升沿比较慢(比如用了大电容),稳定时间可能要 3~5ms。保险起见,上电后延时 10ms 再操作。
4.2 连续写入 1000 次后数据错乱
这个问题的现象是:单次读写都正常,但连续写入 1000 次以上后,某个地址的数据会变成前一次写入的值。不是每次都错,大概每 1000 次出现 1~2 次。
排查过程:
- 先怀疑是 SPI 时序问题,用逻辑分析仪抓了连续 100 次写入的波形,没发现异常。
- 后来想到可能是写操作没有等待完成。MRAM 的写操作是异步的,发出 WRITE 命令后,内部需要时间完成存储。数据手册说写周期是 35ns,但这是内部存储单元的时间,SPI 接口的写入完成需要看状态寄存器。
- 读状态寄存器的 WIP 位(Write In Progress),发现写入后 WIP 位会置位约 100ns。如果在 WIP 置位期间发起下一次写操作,数据就会丢失。
- 解决方案:每次写操作后,轮询状态寄存器的 WIP 位,直到它为 0 再发起下一次写。
修改后的写函数:
void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t i; // 等待上一次写完成 while(MRAM_IsBusy()); // 写使能 CS = 0; SPI_WriteByte(0x06); CS = 1; // 写命令 + 地址 + 数据 CS = 0; SPI_WriteByte(0x02); SPI_WriteByte((addr >> 16) & 0xFF); SPI_WriteByte((addr >> 8) & 0xFF); SPI_WriteByte(addr & 0xFF); for(i = 0; i < len; i++) { SPI_WriteByte(data[i]); } CS = 1; // 等待写完成 while(MRAM_IsBusy()); } uint8_t MRAM_IsBusy(void) { uint8_t status; CS = 0; SPI_WriteByte(0x05); status = SPI_ReadByte(); CS = 1; return (status & 0x01); // WIP 位 }加上 WIP 轮询后,连续写入 10 万次没有再出现数据错乱。
4.3 高温环境下读写失败
工业现场的环境温度可能到 70°C 以上。我在实验室用热风枪把板子加热到 85°C,发现 MRAM 的读写开始出错,误码率大概 1%。
排查过程:
- 先怀疑是 MRAM 本身的问题,查数据手册,MR25H40CDF 的工业级版本工作温度是 -40°C 到 +85°C,应该没问题。
- 用示波器量 SPI 信号,发现高温下 SCK 的上升沿变缓了,从原来的 5ns 变成了 15ns。
- 原因是 PCB 上的走线电容在高温下略有增加,加上 PIC 的 IO 驱动能力在高温下下降,导致信号边沿变缓。
- 解决方案:把 SPI 时钟从 8MHz 降到 4MHz,信号边沿问题解决,读写恢复正常。
这个经验说明:工业级温度范围只是保证芯片能工作,但 PCB 和信号完整性在高温下的表现需要单独验证。如果产品要在高温环境下使用,SPI 时钟频率要留足余量。
5. 数据存储策略与磨损均衡的实际考量
5.1 为什么 MRAM 也需要考虑磨损均衡
MRAM 的写入寿命是 10^15 次,理论上不需要磨损均衡。但在实际项目中,我还是做了一个简单的地址轮换。原因有两个:
第一,虽然 MRAM 的存储单元寿命很长,但 SPI 接口的写入次数是有限的。每次写入都要经过 SPI 总线的电气应力,长期高频写入可能导致接口电路老化。
第二,工业现场的数据有冷热之分。特征值数据每天更新,但设备参数(如校准系数)可能几个月才更新一次。如果把所有数据都写在固定地址,热数据的地址会承受绝大部分写入操作。
我的做法是把 512K 的空间分成 16 个 32K 的块,每个块内再分成 256 个 128 字节的页。写入时按页轮换,写满一块后擦除(MRAM 不需要擦除,直接覆盖写)并切换到下一块。这样每个物理地址的写入次数被均匀分散。
5.2 数据完整性校验的实现
工业现场电磁干扰强,SPI 通信可能出错。我在每个数据页的末尾加了 4 个字节的 CRC32 校验。写入时计算 CRC 并存储,读取时重新计算并比对。
CRC32 的计算用查表法,在 PIC18LF4585 上大概需要 200 个时钟周期处理 128 字节。对于 200ms 的采集周期来说,这个开销可以忽略。
uint32_t CRC32_Calculate(uint8_t *data, uint16_t len) { uint32_t crc = 0xFFFFFFFF; uint16_t i; uint8_t j; for(i = 0; i < len; i++) { crc ^= data[i]; for(j = 0; j < 8; j++) { if(crc & 1) { crc = (crc >> 1) ^ 0xEDB88320; } else { crc >>= 1; } } } return ~crc; }这个实现没有用查表法,因为 PIC18LF4585 的 Flash 空间有限(48KB),查表需要 1KB 的表格空间。用位运算虽然慢一点,但省空间。实测处理 128 字节需要约 1.5ms,在可接受范围内。
5.3 掉电保护的处理
工业现场随时可能断电,必须保证掉电时正在写入的数据不会损坏。我的做法是:
- 在数据页的头部加一个状态标志字节。写入前先写 0xAA 表示"正在写入",写完后改成 0x55 表示"写入完成"。
- 上电初始化时,扫描所有页,如果发现状态标志是 0xAA,说明上次写入被中断,该页数据不可信,直接跳过。
- 配合一个超级电容(0.1F,5.5V),在断电后能维持 PIC 和 MRAM 工作约 50ms,足够完成当前页的写入。
这个方案实测在 100 次随机断电测试中,数据丢失率为零。
6. 性能实测数据与优化空间
6.1 读写速度实测
在 8MHz SPI 时钟下,我实测了 MR25H40CDF 的读写速度:
| 操作 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节读 | 1 字节 | 2.5us | 3.2Mbps |
| 128 字节连续读 | 128 字节 | 135us | 7.6Mbps |
| 单字节写 | 1 字节 | 3.2us | 2.5Mbps |
| 128 字节连续写 | 128 字节 | 145us | 7.1Mbps |
| 写后 WIP 轮询 | - | 约 1us | - |
连续读写的速率接近 SPI 时钟的理论值(8Mbps),效率很高。单字节操作的额外开销主要来自 CS 的拉高拉低和命令字节的传输。
6.2 优化空间
如果要把性能再往上提,有几个方向:
- 提高 SPI 时钟:PIC18LF4585 在 3.3V 下 MSSP 最高可以跑到 Fosc/4,如果系统时钟用 40MHz,SPI 可以到 10MHz。但要注意 MRAM 的写操作在 3.3V 下最高 20MHz,10MHz 是安全的。
- 用 DMA:PIC18LF4585 没有 DMA,这个方向走不通。如果换成带 DMA 的 PIC24 或 dsPIC,可以大幅降低 CPU 占用。
- 减少 CS 切换:连续读写多个字节时,CS 只需要在开始时拉低、结束时拉高,中间不需要切换。我的代码已经这样做了。
6.3 功耗考量
工业现场的设备很多是电池供电或 4-20mA 环路供电,功耗很关键。MR25H40CDF 的功耗数据:
- 读操作电流:约 3mA(8MHz SPI)
- 写操作电流:约 5mA
- 待机电流:约 100uA
PIC18LF4585 在 32MHz 下的运行电流约 15mA。整个存储子系统的功耗约 20mA,对于 4-20mA 环路供电的设备来说偏高。优化方向是降低 PIC 的系统时钟,在不需要采集的时候进入休眠模式,需要存储时再唤醒。
我实测了一下:PIC 在 8MHz 下运行,SPI 降到 2MHz,存储子系统的功耗降到约 8mA。虽然写入速度慢了,但 200ms 的采集周期完全够用。
7. 从实验室到工业现场的迁移经验
7.1 EMC 测试中的问题
实验室跑通之后,板子送到第三方做 EMC 测试,结果在辐射抗扰度测试(RS)中,场强加到 10V/m 时,MRAM 的读写开始出错。排查发现是 SPI 走线像天线一样接收了干扰信号。
解决方案:
- 在 SCK 和 SI 上串联 33 欧姆电阻,降低边沿速率
- 在 MRAM 的 VDD 引脚增加 100nF 和 10uF 的并联去耦
- SPI 走线包地处理,两侧加接地过孔
改版后重新测试,场强加到 20V/m 也没有出现读写错误。
7.2 长期运行的稳定性数据
最终版本在工业现场部署了 20 台设备,连续运行了 6 个月。我统计了一下数据:
- 总写入次数:约 7.8 亿次(20 台设备合计)
- 数据错误次数:0
- 设备重启次数:3 次(都是外部电源问题,不是存储问题)
- MRAM 表面温度:最高 62°C(夏季高温环境)
这个数据说明 MR25H40CDF 在工业环境下的可靠性是经得起考验的。
7.3 成本与供货的实际情况
最后说点实际的。MR25H40CDF 的单价在 3~5 美元之间,比同容量的 SPI Flash(约 0.5 美元)贵不少,但比 FRAM(约 8~10 美元)便宜。供货方面,Everspin 的代理商交期一般在 8~12 周,比 Microchip 的常规物料长。如果项目时间紧,建议提前备货。
另外,MR25H40CDF 有工业级和汽车级两个版本,汽车级的价格要贵 30% 左右。如果产品不需要过车规,用工业级就够了。
我在实际使用中的体会是:MRAM 这个器件,选型的时候觉得贵,但用起来之后会发现省了很多事。不需要考虑擦除、不需要磨损均衡、不需要电池,代码写起来简单,现场维护也省心。对于写入频繁、数据不能丢的工业场景,多花的那点钱是值得的。