1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 和 PIC24
工业现场的数据记录有个很尴尬的处境:用 EEPROM 吧,写入速度慢得让人着急,擦写寿命也就百万次级别,高频采集场景下没几个月就写废了;用 SRAM 加后备电池吧,电池在高温高湿环境下撑不了几年,换电池还得停机,维护成本高得离谱;用 Flash 吧,写之前得先擦一整块,掉电时机不对还会丢数据,而且擦写次数同样有限。这几个方案我都实际踩过坑,尤其是电池方案,在北方冬天低温环境下容量衰减特别明显,客户投诉过好几次。
后来接触到MRAM(磁性随机存储器),情况就不一样了。以Everspin 的 MR25H40CDF为例,它本质上是用磁隧道结存储数据,写入不需要擦除动作,字节级随机写入,没有写延迟,擦写寿命理论上无限次。这几个特性放在工业数据记录场景里,几乎是量身定做的。而主控这边选Microchip 的 PIC24EP512GU814,是因为它在工业嵌入式领域属于老牌选手,512KB Flash、48KB RAM、70 MIPS 的处理能力,外设资源丰富,SPI 模块成熟稳定,工作温度范围覆盖 -40°C 到 +125°C,跟 MRAM 搭配起来非常合适。
这套组合解决的核心问题是:在恶劣工业环境下,实现高频、高可靠、长寿命的数据存储与读取。适合谁看?如果你正在做工业数据采集器、电力监测终端、车载黑匣子、医疗设备日志记录这类项目,或者你单纯想搞清楚 MRAM 到底怎么用、SPI 通信在 PIC24 上怎么配,这篇内容应该能帮你省下不少查手册和调试的时间。
1.2 方案对比:为什么不是 EEPROM、Flash 或 FRAM
选存储介质这件事,不能只看参数表,得结合实际工况。我把几个常见方案拉出来对比一下,你就明白为什么最终选了 MRAM。
| 特性 | EEPROM | NOR Flash | FRAM | MRAM (MR25H40CDF) |
|---|---|---|---|---|
| 写入速度 | 慢(ms级) | 慢(需擦除) | 快(ns级) | 快(ns级) |
| 擦写寿命 | 100万次 | 10万次 | 10^14次 | 无限次 |
| 写入方式 | 字节 | 页/块 | 字节 | 字节 |
| 掉电保护 | 差 | 差 | 好 | 好 |
| 工作温度 | -40~85°C | -40~85°C | -40~85°C | -40~125°C |
| 接口 | I2C/SPI | SPI | I2C/SPI | SPI |
| 成本 | 低 | 低 | 中 | 中高 |
从表里能看出来,MRAM 在写入寿命和温度范围上优势明显。FRAM 虽然也不错,但容量普遍偏小,而且供货不如 MRAM 稳定。MR25H40CDF 是 4Mbit(512KB)容量,SPI 接口,40MHz 时钟,对于大多数工业数据记录场景来说够用了。
注意:MRAM 虽然写入寿命无限,但读取操作理论上也有次数限制,只是这个限制高到可以忽略不计。实际使用中不用担心读坏。
1.3 硬件连接的整体思路
PIC24EP512GU814 有多个 SPI 模块,我选的是 SPI2,因为 SPI1 留给了板上的其他外设。MR25H40CDF 的引脚定义很标准:CS、SCK、SI、SO,加上 VDD 和 VSS。硬件连接上没什么特别的,但有几个细节值得注意。
CS 片选信号我用了硬件片选,也就是接到 PIC24 的 SPI2 片选引脚上。为什么不用软件片选?因为 MRAM 的时序要求比较严格,硬件片选能保证片选信号和时钟信号之间的时序关系更精确,尤其是在 40MHz 高速通信时,软件片选的抖动可能导致数据出错。当然,如果你的引脚资源紧张,软件片选也能用,但速度得降下来。
电源方面,MR25H40CDF 工作电压是 2.7V 到 3.6V,PIC24 是 3.3V 供电,可以直接对接,不需要电平转换。但要注意在电源引脚旁边放一个 0.1uF 的去耦电容,越近越好,这个电容对 MRAM 的写入可靠性影响很大,我实测过,去掉这个电容后误码率明显上升。
2. MR25H40CDF 的核心细节与操作要点
2.1 存储结构与地址映射
MR25H40CDF 的 512KB 存储空间是按字节寻址的,地址范围从 0x00000 到 0x7FFFF。它内部没有页的概念,你可以直接往任意地址写任意长度的数据,不需要考虑对齐问题。这一点比 Flash 方便太多了,Flash 写之前还得算页边界,一不小心就跨页了。
指令集方面,常用的就几条:WREN(写使能)、WRITE(写数据)、READ(读数据)、RDSR(读状态寄存器)、WRSR(写状态寄存器)。每条指令都是先发指令码,再发地址,然后才是数据。地址是 24 位的,因为 512KB 需要 19 位地址,但 MRAM 统一用 24 位地址格式,高 5 位忽略。
这里有个容易踩的坑:WREN 指令必须在每次写操作之前发送,写完一次数据后写使能会自动清除。如果你连续写多个地址,每次都得重新发 WREN。我一开始不知道这个,以为发一次 WREN 就能一直写,结果只有第一笔数据写进去了,后面的全丢了,排查了半天才发现是这个问题。
2.2 SPI 模式与时序要求
MR25H40CDF 支持 SPI 模式 0 和模式 3。模式 0 是 CPOL=0、CPHA=0,模式 3 是 CPOL=1、CPHA=1。我选的是模式 0,因为 PIC24 的 SPI 模块在模式 0 下配置最直观,而且大多数例程也是用模式 0。
时序上,MRAM 的 SCK 最高频率是 40MHz,但实际能跑多快取决于你的 PCB 布线和 PIC24 的 SPI 时钟分频。PIC24EP512GU814 的系统时钟我设的是 140MHz,SPI 时钟分频后可以到 35MHz,已经接近 MRAM 的极限了。为了留余量,我实际用的是 20MHz,稳定性很好。
提示:如果你发现高速通信时数据出错,先降时钟频率试试。很多时候不是芯片问题,是 PCB 走线太长或者没有做阻抗匹配。
2.3 状态寄存器与写保护
MR25H40CDF 的状态寄存器里有两个关键位:WEL(写使能锁存)和 BP0/BP1(块保护)。WEL 位在发送 WREN 后自动置 1,写操作完成后自动清零。BP0 和 BP1 用来设置写保护区域,可以保护部分或全部存储空间不被写入。
在工业应用中,我建议把关键配置参数放在受保护的区域,比如把前 64KB 设为写保护,用来存校准系数和设备序列号,防止程序跑飞时误写。设置方法就是发 WRSR 指令,把对应的 BP 位写 1。解除保护同样发 WRSR,把 BP 位清零。这个功能在实际项目中救过我一次,有个客户的设备因为电源波动导致程序异常,但校准数据完好无损,就是因为开了写保护。
3. PIC24EP512GU814 的 SPI 配置与驱动实现
3.1 SPI 模块初始化步骤
PIC24 的 SPI 初始化说简单也简单,说麻烦也麻烦,因为寄存器比较多,一不小心就配错了。我按实际顺序捋一遍。
第一步,配置引脚方向。SCK2 和 SDO2 设为输出,SDI2 设为输入,CS 引脚设为输出并默认拉高。这里注意,PIC24 的引脚复用功能需要通过 PPS(外设引脚选择)来映射,不是直接设 TRIS 就完事了。具体来说,要把 SPI2 的 SCK 和 SDO 输出映射到具体引脚上,SDI 输入也要映射。
第二步,设置 SPI2CON1 寄存器。关键位包括:MSTEN=1(主模式)、CKP=0(空闲低电平)、CKE=0(数据在时钟上升沿采样)、SMP=0(在数据输出末尾采样)、SPRE=0(主模式预分频 1:1)、PPRE=0(主模式后分频 1:1)。这几个位决定了 SPI 的工作模式,配错了通信直接不通。
第三步,设置 SPI2CON2 寄存器。主要是 FRMEN=0(禁用帧模式)、SPIFSD=0、SPIBEN=0(禁用增强缓冲)。这些高级功能一般用不上,保持默认就行。
第四步,设置 SPI2BRG 寄存器。这个寄存器决定波特率,计算公式是:SPI 时钟 = FCY / (2 * (SPI2BRG + 1))。FCY 是系统时钟的一半,如果系统时钟是 140MHz,FCY 就是 70MHz。要得到 20MHz 的 SPI 时钟,SPI2BRG = 70 / (2 * 20) - 1 = 0.75,取整为 0,实际时钟就是 35MHz。如果要更精确的 20MHz,可以调整系统时钟或者用后分频。
第五步,使能 SPI2。把 SPI2CON1 的 SPIEN 位置 1。
// SPI2 初始化示例 void SPI2_Init(void) { // 引脚配置 TRISDbits.TRISD1 = 0; // SCK2 输出 TRISDbits.TRISD2 = 0; // SDO2 输出 TRISDbits.TRISD3 = 1; // SDI2 输入 TRISDbits.TRISD0 = 0; // CS 输出 LATDbits.LATD0 = 1; // CS 默认高 // PPS 映射 RPINR22bits.SDI2R = 0b0110011; // SDI2 映射到 RD3 RPOR12bits.RP25R = 0b01011; // SCK2 映射到 RD1 RPOR13bits.RP26R = 0b01100; // SDO2 映射到 RD2 // SPI2 配置 SPI2CON1 = 0x0120; // 主模式,模式0,预分频1:1 SPI2CON2 = 0x0000; // 禁用帧模式 SPI2BRG = 0; // 35MHz SPI2STATbits.SPIEN = 1; // 使能 SPI2 }3.2 字节读写函数实现
SPI 通信的本质就是交换字节:你发一个字节出去,同时收一个字节回来。PIC24 的 SPI 模块有一个 4 级深的 FIFO,但为了简单可靠,我一般用阻塞方式,发一个等一个。
// SPI2 交换一个字节 uint8_t SPI2_ExchangeByte(uint8_t data) { SPI2BUF = data; while (!SPI2STATbits.SPIRBF); return SPI2BUF; }这个函数是基础,MRAM 的所有操作都建立在它之上。注意 SPI2STATbits.SPIRBF 是接收缓冲满标志,等它置 1 说明数据收到了。这里不要用延时循环,用标志位判断最可靠。
3.3 MRAM 读写驱动封装
有了字节交换函数,就可以封装 MRAM 的读写操作了。先定义指令码:
#define MRAM_WREN 0x06 #define MRAM_WRITE 0x02 #define MRAM_READ 0x03 #define MRAM_RDSR 0x05 #define MRAM_WRSR 0x01写数据函数:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { CS_LOW(); SPI2_ExchangeByte(MRAM_WREN); // 写使能 CS_HIGH(); CS_LOW(); SPI2_ExchangeByte(MRAM_WRITE); SPI2_ExchangeByte((addr >> 16) & 0xFF); SPI2_ExchangeByte((addr >> 8) & 0xFF); SPI2_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { SPI2_ExchangeByte(buf[i]); } CS_HIGH(); }读数据函数:
void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { CS_LOW(); SPI2_ExchangeByte(MRAM_READ); SPI2_ExchangeByte((addr >> 16) & 0xFF); SPI2_ExchangeByte((addr >> 8) & 0xFF); SPI2_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI2_ExchangeByte(0xFF); } CS_HIGH(); }这两个函数是核心,实际项目中我还会加上超时检测和错误重试。比如读操作如果连续几次读到的数据都是 0xFF,可能是通信断了,得报错。
注意:CS 的拉低和拉高之间要有足够的延时,尤其是在高速时钟下。我一般会在 CS 操作后加几个 NOP,确保信号稳定。
4. 完整实操流程与关键环节
4.1 硬件搭建与上电检查
硬件连接完成后,别急着写代码,先做静态检查。用万用表量一下 VDD 和 VSS 之间有没有短路,CS、SCK、SI、SO 四根线有没有接反。我见过太多因为接反线导致通信不通的案例,排查半天最后发现是硬件问题。
上电后,先测电压。MRAM 的 VDD 应该是 3.3V,波动不超过 ±5%。然后用示波器看 SCK 引脚,如果没有波形,说明 SPI 没配置对或者引脚映射错了。这一步能快速定位是硬件还是软件问题。
4.2 读写测试与验证
硬件没问题后,先写一个最简单的测试:往地址 0x00000 写 0x55,然后读回来,看是不是 0x55。如果读回来是 0xFF 或者 0x00,说明通信有问题。
我习惯用递增模式测试:往连续地址写 0x00 到 0xFF,然后读回来对比。这样能发现地址对齐、边界处理等问题。测试通过后,再测大块数据读写,比如一次写 4KB,验证 FIFO 和缓冲区的处理逻辑。
// 测试代码示例 uint8_t test_buf[256]; uint8_t read_buf[256]; for (int i = 0; i < 256; i++) { test_buf[i] = i; } MRAM_Write(0x00000, test_buf, 256); MRAM_Read(0x00000, read_buf, 256); for (int i = 0; i < 256; i++) { if (read_buf[i] != test_buf[i]) { // 测试失败,记录错误位置 printf("Error at %d: expected %02X, got %02X\n", i, test_buf[i], read_buf[i]); } }4.3 高频写入场景的优化
工业数据记录往往是高频的,比如每 1ms 写一次传感器数据。如果每次都单独调用 MRAM_Write,SPI 的 CS 拉低拉高开销会很大。优化方法是攒一批数据再写,比如每 100ms 写一次,每次写 100 个采样点。
另外,MRAM 的写入没有延迟,写完立即就能读,不需要像 Flash 那样等擦除完成。这个特性在实时系统中很有用,写完关键数据后可以立即回读校验,确保数据正确。
4.4 掉电保护与数据完整性
虽然 MRAM 本身掉电不丢数据,但系统掉电时如果正在写数据,可能写到一半就断了。为了保证数据完整性,我用了双缓冲加校验的机制:数据区分为 A、B 两个块,轮流写入,每块末尾加 CRC 校验。上电时先读 A 块,CRC 通过就用 A,否则读 B 块。这样即使掉电时正在写 A 块,B 块的数据还是完整的。
CRC 我用的是 CRC-16-CCITT,计算速度快,检错能力强。具体实现就不贴代码了,网上有很多现成的库。
5. 常见问题与排查技巧实录
5.1 通信失败问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读回全 0xFF | CS 未拉低 | 示波器看 CS 波形 | 检查 CS 引脚配置和代码 |
| 读回全 0x00 | MISO 未连接 | 万用表测通断 | 重新焊接或更换排线 |
| 数据错位 | SPI 模式不匹配 | 对比时钟极性和相位 | 改为模式 0 或模式 3 |
| 高速时出错 | 时钟太快 | 降低 SPI2BRG 值 | 降到 10MHz 测试 |
| 写入无效 | 未发 WREN | 逻辑分析仪抓指令 | 每次写前加 WREN |
| 偶发错误 | 电源噪声 | 示波器看电源纹波 | 加去耦电容,改善布线 |
5.2 我踩过的三个坑
第一个坑是WREN 指令的时序。我一开始把 WREN 和 WRITE 放在同一个 CS 低电平周期里,结果写不进去。后来看手册才发现,WREN 必须单独一个 CS 周期,发完 WREN 后 CS 要拉高,然后再拉低发 WRITE。这个细节手册上写得不是很明显,容易忽略。
第二个坑是PPS 映射。PIC24 的 SPI 引脚不是固定引脚,需要通过 PPS 映射。我一开始没配 PPS,直接设 TRIS,结果引脚没输出。后来查了数据手册的 PPS 章节才搞明白。PPS 的寄存器名和映射值每个型号都不一样,得对着手册查。
第三个坑是电源去耦。MRAM 在写入瞬间电流会有波动,如果去耦电容离得太远,写入会出错。我一开始把电容放在电源入口处,离 MRAM 有 3cm 远,结果误码率很高。后来把电容移到 MRAM 电源引脚旁边,问题就解决了。
5.3 调试工具与技巧
调试 SPI 通信,逻辑分析仪是神器。我用的是 Saleae 的 8 通道逻辑分析仪,能同时抓 CS、SCK、SI、SO 四根线,直接解码 SPI 协议,看到底发了什么、收了什么。没有逻辑分析仪的话,示波器也行,但只能看波形,解码得靠人眼。
另外,PIC24 的 SPI 模块有错误标志位,比如 SPI2STATbits.SPIROV 是接收溢出标志。如果这个位置 1,说明数据没及时读走,被覆盖了。调试时可以在每次读写后检查这个位,帮助定位问题。
提示:如果条件允许,先在低速下把功能调通,再逐步提高时钟频率。这样能把时序问题和逻辑问题分开排查,效率高很多。
6. 实际项目中的经验体会
这套 MRAM 加 PIC24 的方案,我在三个工业项目中实际用过,最长的已经连续运行了两年多,每天写入几十万次,至今没有出现数据丢失或存储失效的情况。相比之前用 EEPROM 的方案,维护成本几乎降到了零。
有一点值得注意:MRAM 虽然写入寿命无限,但并不意味着可以无限制地高频写入。每次写入都有功耗,如果系统是电池供电,得算一下功耗预算。MR25H40CDF 的写入电流大概是 10mA 左右,写入时间几十纳秒,平均功耗其实很低,但高频写入时累积起来也不能忽视。
另外,MRAM 的容量有限,512KB 对于大数据量的应用可能不够。如果数据量更大,可以考虑用多个 MRAM 芯片级联,或者用 MRAM 做缓存、大容量 Flash 做存储的分层方案。具体怎么选,得看你的数据量、写入频率和成本预算。
最后分享一个小技巧:MRAM 的读取速度也很快,可以把它当成高速缓存用。比如系统启动时,把常用的配置参数从 MRAM 读到 RAM 里,运行过程中直接访问 RAM,需要保存时再写回 MRAM。这样既能保证数据不丢,又能提高访问速度。