1. 为什么工业现场还在用 SPI 接 MRAM,而不是直接上 Flash
如果你拆过工业控制器、电力监测终端或者车载数据记录仪,大概率会在板子上看到一颗 8 脚的小芯片,旁边连着主控的 SPI 引脚。这颗芯片很多时候不是普通的 NOR Flash,而是MRAM——磁性随机存储器。我这次要聊的组合就是MR25H40CDF这颗 512Kb 的 SPI MRAM,搭配PIC24FJ128GA310这款 16 位单片机,做工业与嵌入式场景下的数据存储和读取。
先说清楚这个组合解决什么问题。工业现场的数据记录有个很尴尬的特点:写入频繁、单次数据量小、掉电随时可能发生、环境温度跨度大,而且设备往往要求十年以上的生命周期。用 EEPROM 吧,写入速度慢、擦写寿命有限;用 NOR Flash 吧,写之前要擦除整个扇区,掉电时正在擦除的扇区可能直接损坏;用 FRAM 吧,容量小、价格高。MRAM 恰好卡在中间:像 RAM 一样按字节随机写入,不需要擦除,掉电数据不丢,读写寿命几乎无限。
MR25H40CDF 是 Everspin 的 512Kb(64KB)SPI MRAM,40MHz SPI 接口,2.7V 到 3.6V 供电,工业级温度范围 -40 到 +85 摄氏度。PIC24FJ128GA310 是 Microchip 的 16 位 MCU,128KB Flash、8KB RAM,带多个 SPI 模块,本身定位就是工业控制。两者搭配,一个负责"永不丢失的快速暂存区",一个负责"逻辑控制和协议处理",是很典型的工业数据记录架构。
这篇文章适合谁看?如果你正在做工业数据采集、设备参数存储、掉电保护记录、黑匣子类的功能,或者你手上有 PIC24 平台但不确定怎么把 MRAM 用稳,那这篇内容可以直接抄作业。我会从硬件连接、SPI 时序配置、读写驱动、掉电保护策略、常见踩坑几个角度完整讲一遍,中间会穿插我实际调试时遇到的问题和解决办法。
提示:MRAM 和 Flash 最大的区别是"写之前不用擦"。这个特性决定了驱动层的设计思路完全不同,后面会重点展开。
2. MR25H40CDF 的引脚、时序与 PIC24FJ128GA310 的硬件对接
2.1 先搞清楚 MR25H40CDF 的引脚定义和工作模式
MR25H40CDF 是 8 脚 SOIC 封装,引脚定义很标准:
| 引脚 | 名称 | 功能说明 |
|---|---|---|
| 1 | CS | 片选,低电平有效 |
| 2 | SO | 数据输出(MISO) |
| 3 | WP | 写保护,低电平有效 |
| 4 | VSS | 地 |
| 5 | SI | 数据输入(MOSI) |
| 6 | SCK | 时钟 |
| 7 | HOLD | 保持,低电平有效 |
| 8 | VDD | 电源 2.7V~3.6V |
这里有两个容易被忽略的引脚:WP 和 HOLD。WP 拉低时芯片进入硬件写保护,所有写操作被拒绝;HOLD 拉低时当前 SPI 传输暂停,时钟可以继续跑但数据不采样。很多人在原理图阶段直接把这两个脚接地,结果发现写不进去数据,排查半天才发现是 WP 被拉低了。
我的建议是:WP 和 HOLD 都不要直接接地,而是通过 10K 上拉到 VDD。如果确实需要硬件写保护功能,WP 可以接到 MCU 的一个 GPIO 上,正常工作时输出高电平,需要保护时拉低。HOLD 如果不使用,上拉到 VDD 即可,不要悬空。
2.2 PIC24FJ128GA310 的 SPI 模块配置要点
PIC24FJ128GA310 有多个 SPI 模块,我用的是 SPI1。配置 SPI 的核心寄存器是 SPI1CON1、SPI1CON2、SPI1STAT。工业场景下我推荐的配置是:
- 主模式:MSTEN = 1
- 时钟极性:CKP = 0(空闲低电平)
- 时钟边沿:CKE = 1(数据在时钟上升沿采样)
- 字长:MODE16 = 0,8 位模式
- 预分频:主预分频 4:1,次预分频 2:1
MR25H40CDF 支持 SPI Mode 0 和 Mode 3。Mode 0 是 CKP=0、CKE=1,也就是空闲低、上升沿采样。这个模式和大多数 SPI Flash 一致,配置起来最省事。
关于时钟频率,MR25H40CDF 最高支持 40MHz,但 PIC24FJ128GA310 在 32MHz 系统时钟下,SPI 最高能跑到 16MHz(FCY/2)。实际工业场景我不建议跑满,8MHz 左右是比较稳的选择。原因有两个:一是长走线时高速 SPI 的信号完整性会变差,二是 MRAM 的写入时序在高温下需要留裕量。
计算一下:如果 FCY = 16 MIPS(32MHz 晶振,PLL 后),主预分频 4:1、次预分频 2:1,SPI 时钟 = 16MHz / (4 × 2) = 2MHz。这个速度对 MRAM 来说偏慢,但胜在稳定。如果要提速,可以把主预分频改成 2:1,得到 4MHz;再改成 1:1,得到 8MHz。
// PIC24FJ128GA310 SPI1 初始化示例 void SPI1_Init(void) { SPI1STATbits.SPIEN = 0; // 先关闭 SPI SPI1CON1bits.MSTEN = 1; // 主模式 SPI1CON1bits.CKP = 0; // 空闲低电平 SPI1CON1bits.CKE = 1; // 上升沿采样 SPI1CON1bits.SMP = 0; // 中间采样 SPI1CON1bits.MODE16 = 0; // 8 位模式 SPI1CON1bits.PPRE = 0b10; // 主预分频 4:1 SPI1CON1bits.SPRE = 0b110; // 次预分频 2:1 SPI1STATbits.SPIROV = 0; // 清除溢出标志 SPI1STATbits.SPIEN = 1; // 使能 SPI }2.3 硬件走线和去耦的实战经验
SPI 虽然只有四根线,但在工业板上走不好照样出问题。我踩过的坑包括:SCK 和 MISO 平行走线太长导致串扰、CS 没有单独走线而是和别的信号共用、去耦电容离芯片太远。
正确的做法是:MRAM 的 VDD 引脚旁边放一个 0.1uF 的陶瓷电容,距离不超过 2mm。如果板子上有多个 SPI 从设备,每个从设备的 CS 都要独立走线,不要用译码器共用。SCK 走线尽量短,如果超过 5cm,建议在源端串一个 22 到 33 欧姆的电阻做阻抗匹配。
还有一点:MR25H40CDF 的 WP 和 HOLD 上拉电阻不要省。我见过一个板子为了省两个电阻,把 WP 直接接 VDD、HOLD 悬空,结果在湿度大的环境下 HOLD 引脚受干扰,SPI 通信间歇性失败。后来补上 10K 上拉就稳定了。
3. 驱动层怎么写:从单字节读写到页写入的完整实现
3.1 MR25H40CDF 的指令集和地址空间
MR25H40CDF 的指令集很精简,常用的就几条:
| 指令 | 操作码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
地址空间是 64KB,地址从 0x0000 到 0xFFFF,用 16 位地址寻址。注意 MR25H40CDF 的 READ 和 WRITE 指令后面跟的是 16 位地址,高位在前。
状态寄存器的 bit0 是 WEL(写使能锁存),bit1 是 BP0,bit2 是 BP1,bit7 是 SRWD。写操作之前必须先发 WREN 指令把 WEL 置 1,写完自动清零。这个和 EEPROM、Flash 的逻辑一样。
3.2 单字节读写函数:最基础也最容易写错的部分
先看读一个字节:
// 读取 MR25H40CDF 指定地址的一个字节 uint8_t MRAM_ReadByte(uint16_t addr) { uint8_t data; MRAM_CS_LOW(); // 片选拉低 SPI1_ExchangeByte(0x03); // 发送 READ 指令 SPI1_ExchangeByte(addr >> 8); // 地址高字节 SPI1_ExchangeByte(addr & 0xFF); // 地址低字节 data = SPI1_ExchangeByte(0xFF); // 读数据 MRAM_CS_HIGH(); // 片选拉高 return data; }写一个字节稍微复杂,因为要先发 WREN:
// 写入 MR25H40CDF 指定地址的一个字节 void MRAM_WriteByte(uint16_t addr, uint8_t data) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte(addr >> 8); SPI1_ExchangeByte(addr & 0xFF); SPI1_ExchangeByte(data); MRAM_CS_HIGH(); // 等待写入完成 while(MRAM_ReadStatus() & 0x01); }这里有个关键点:WREN 和 WRITE 之间必须拉高 CS。很多人习惯把 WREN 和 WRITE 放在同一个 CS 低电平周期里,这对 MRAM 是不行的。MR25H40CDF 要求 WREN 指令在独立的 CS 周期内完成,然后再开一个新的 CS 周期发 WRITE。
3.3 页写入和连续读取:提升吞吐量的关键
单字节写虽然简单,但每次都要发 WREN、发地址、等写入完成,效率很低。MR25H40CDF 支持页写入,一页是 256 字节。连续写的时候,地址低 8 位会自动递增,超过 255 会回绕到页首,所以跨页写入必须手动分页。
// 页写入,注意不能跨页 void MRAM_WritePage(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t page_end = (addr & 0xFF00) + 0x100; if(addr + len > page_end) { len = page_end - addr; // 截断到页边界 } MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte(addr >> 8); SPI1_ExchangeByte(addr & 0xFF); for(uint16_t i = 0; i < len; i++) { SPI1_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); while(MRAM_ReadStatus() & 0x01); }连续读取就简单多了,发完 READ 指令和起始地址后,可以一直读,地址自动递增,直到 CS 拉高。
// 连续读取 void MRAM_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x03); SPI1_ExchangeByte(addr >> 8); SPI1_ExchangeByte(addr & 0xFF); for(uint16_t i = 0; i < len; i++) { buf[i] = SPI1_ExchangeByte(0xFF); } MRAM_CS_HIGH(); }3.4 SPI 字节交换函数的实现细节
上面反复用到的SPI1_ExchangeByte是底层函数,实现要注意等待标志位:
uint8_t SPI1_ExchangeByte(uint8_t txData) { SPI1BUF = txData; while(!SPI1STATbits.SPIRBF); // 等待接收完成 return SPI1BUF; }这里有个坑:必须先写 SPI1BUF 再等 SPIRBF。如果反过来先等标志再写,第一次调用会死等。另外,读 SPI1BUF 会自动清 SPIRBF 标志,所以不需要手动清。
注意:PIC24 的 SPI 在高速时钟下,如果两个字节之间间隔太短,可能出现 SPIROV 溢出。工业场景建议在连续读写时检查 SPIROV,如果置位要重新初始化 SPI。
4. 掉电保护与数据可靠性:MRAM 真正的价值所在
4.1 为什么 MRAM 的掉电保护比 Flash 简单得多
Flash 的掉电保护是个老大难问题。写 Flash 要先擦除扇区,擦除过程中如果掉电,整个扇区数据可能变成 0xFF 或者不确定状态。所以用 Flash 做数据记录,通常要设计双备份、日志结构、磨损均衡,代码复杂度很高。
MRAM 没有这个问题。MR25H40CDF 的写入是直接覆盖,不需要擦除,写入过程中掉电,要么是旧数据,要么是新数据,不会出现中间态。这个特性让掉电保护逻辑大大简化。
我实际项目里的做法是:把 64KB 空间分成若干条记录,每条记录带一个序号和 CRC 校验。写入时先写数据区,再写序号和 CRC。读取时扫描所有记录,找序号最大且 CRC 正确的作为最新数据。因为 MRAM 写入快(单字节写入时间在微秒级),掉电窗口极小。
4.2 用状态寄存器做写保护的分级策略
MR25H40CDF 的状态寄存器有 BP0 和 BP1 两位,可以设置不同级别的写保护:
| BP1 | BP0 | 保护范围 |
|---|---|---|
| 0 | 0 | 无保护 |
| 0 | 1 | 保护高 1/4 地址空间 |
| 1 | 0 | 保护高 1/2 地址空间 |
| 1 | 1 | 保护整个地址空间 |
工业场景下,我通常把参数区放在低地址,记录区放在高地址。正常运行时全片可写,当设备进入"参数锁定"模式时,把 BP1=0、BP0=1,保护高 1/4 空间,防止记录区被误写。这个策略比软件标志位可靠,因为它是硬件级别的保护。
4.3 写入完成判断:轮询状态寄存器还是固定延时
MRAM 的写入速度很快,但并不是"发完就完"。WRITE 指令的最后一个字节移入后,芯片内部还需要时间完成实际存储。这个时间典型值是几十纳秒到几百纳秒,但数据手册给的最大值是 5ms(实际上远小于这个值)。
我见过两种做法:一种是发完 WRITE 后直接延时 1ms 再操作;另一种是轮询状态寄存器的 WIP 位。推荐轮询方式,因为延时是拍脑袋的,轮询是确定性的。
uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI1_ExchangeByte(0x05); status = SPI1_ExchangeByte(0xFF); MRAM_CS_HIGH(); return status; }状态寄存器的 bit0 是 WIP(Write In Progress),写入完成时该位为 0。轮询这个位比固定延时靠谱得多。
4.4 数据校验:CRC16 在 PIC24 上的轻量实现
工业数据不能只靠"写进去就行",必须有校验。我在 PIC24FJ128GA310 上用的是一个查表法的 CRC16-CCITT,表放在 Flash 里,运行时查表计算。64KB 数据全量校验一次大概几十毫秒,对于周期性记录场景完全够用。
// CRC16-CCITT 查表法 uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; while(len--) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ *data++) & 0xFF]; } return crc; }每条记录的结构我设计成:4 字节序号 + N 字节数据 + 2 字节 CRC。读取时先校验 CRC,再比较序号,确保拿到的是完整且最新的数据。
5. 调试过程中真实踩过的坑和排查思路
5.1 读出来全是 0xFF 或者 0x00
这是最常见的现象。第一次调 MRAM,读出来全是 0xFF,第一反应是芯片坏了。其实大概率是三个原因:
第一,CS 没有正确拉低。用逻辑分析仪抓一下 CS 引脚,确认读操作期间 CS 确实是低电平。我遇到过 CS 配置成开漏输出但没加上拉,导致 CS 一直是高。
第二,SPI 模式不对。MR25H40CDF 支持 Mode 0 和 Mode 3,如果 MCU 配成 Mode 1 或 Mode 2,数据会错位。用示波器看 SCK 空闲电平,Mode 0 空闲低,Mode 3 空闲高。
第三,WP 引脚被拉低。这个最隐蔽,因为读操作不受 WP 影响,但写操作会被拒绝。如果你发现读正常但写不进去,先量一下 WP 引脚电压。
5.2 写入后读出来数据不对,但偶尔又对
这种间歇性错误通常是时序问题。我遇到过一次,SPI 时钟跑到 16MHz,短走线时正常,但板子装进金属外壳后,分布电容变化导致 SCK 边沿变缓,数据采样出错。后来把时钟降到 8MHz,问题消失。
另一个可能是写入后没有等待完成就发起下一次操作。MRAM 虽然快,但连续写不同地址时,如果不等 WIP 清零就发下一条 WRITE,芯片可能忽略第二条指令。我的做法是每次写操作后都轮询 WIP,虽然牺牲一点速度,但稳定性大幅提升。
5.3 高温环境下数据出错
工业设备要做高低温测试。我在 85 摄氏度环境下测试时,发现偶尔有记录丢失。排查后发现两个问题:一是 SPI 时钟在高温下裕量不足,二是去耦电容用的是 Y5V 材质,高温下容量衰减严重。
解决办法:SPI 时钟从 8MHz 降到 4MHz,去耦电容换成 X7R 材质。改完之后高低温循环测试 100 次,没有再出现数据错误。
提示:工业级 MRAM 虽然标称 -40 到 +85 摄氏度,但外围电路的温漂同样要考虑。电容、电阻的材质在极端温度下表现差异很大。
5.4 用逻辑分析仪抓 SPI 波形的正确姿势
调试 SPI 问题,逻辑分析仪比示波器好用。抓波形时注意几点:采样率至少是 SCK 频率的 10 倍;触发条件设成 CS 下降沿;解码协议选 SPI,模式选对。我习惯同时抓 CS、SCK、MOSI、MISO 四根线,这样一眼就能看出是命令发错了还是数据错了。
有一次我抓到的波形显示,WREN 指令发出后 CS 拉高,但下一个 CS 周期开始时 SCK 上有一个毛刺。这个毛刺导致 MRAM 误判指令。后来在 SCK 线上串了一个 33 欧姆电阻,毛刺消失。
6. 把 MRAM 用出价值的几个进阶思路
6.1 用 MRAM 做参数存储和日志记录的分区设计
64KB 空间说大不大,说小不小。我的分区方案是:低 16KB 做参数区,存设备配置、校准系数、序列号;中间 32KB 做循环日志区,每条记录 64 字节,可以存 512 条;高 16KB 做备份区,存关键参数的副本。
参数区用固定地址访问,读写简单。日志区用环形缓冲,写满后覆盖最旧记录。备份区在参数更新时同步写入,主参数区损坏时可以恢复。
6.2 和 Flash 配合使用的混合存储架构
MRAM 容量小、价格高,不适合存大量数据。我的做法是:MRAM 做"热数据"缓存,Flash 做"冷数据"归档。实时采集的数据先写 MRAM,保证掉电不丢;当 MRAM 里的记录积累到一定数量,再批量搬运到 Flash。
这样既利用了 MRAM 的快速写入和掉电安全,又利用了 Flash 的大容量和低成本。搬运过程要注意:先写 Flash,校验成功后再清 MRAM 对应区域,避免搬运过程中掉电导致数据丢失。
6.3 低功耗场景下的 MRAM 使用技巧
MRAM 的待机电流很低,但 SPI 接口在空闲时如果 CS 一直拉低,芯片会持续耗电。低功耗设计时,每次操作完成后把 CS 拉高,让 MRAM 进入待机模式。另外,如果 MCU 支持,可以把 SPI 引脚配置成普通 GPIO 输出低电平,进一步降低漏电流。
我做过一个电池供电的记录仪,MRAM 平时待机电流不到 10 微安,每分钟唤醒一次写一条记录,整体功耗可以做到几年不用换电池。
6.4 从 MR25H40CDF 迁移到更大容量 MRAM 的注意事项
如果 64KB 不够用,Everspin 有更大容量的 SPI MRAM,比如 1Mb、4Mb 的型号。迁移时要注意几点:地址位数可能从 16 位变成 24 位,指令集基本兼容但地址字节数不同;页大小可能变化,写入分页逻辑要调整;封装可能不同,PCB 要改版。
我的建议是驱动层做好抽象,把地址宽度、页大小做成宏定义,迁移时只改配置不改逻辑。
7. 写在最后的一些个人体会
这套 MR25H40CDF 加 PIC24FJ128GA310 的组合,我在三个工业项目里用过,最长的已经连续运行四年多,没有出现过数据丢失。MRAM 这个器件最大的价值不是速度快,而是它让嵌入式工程师不用再为"掉电时数据会不会丢"这件事焦虑。用 Flash 做记录,你要设计日志、磨损均衡、掉电检测、备份恢复,代码写几千行;用 MRAM,核心逻辑可能几百行就搞定了。
当然 MRAM 也不是万能的。容量小、单价高,这两点决定了它只能用在关键数据的存储上。我的经验是:凡是"丢了会出大事"的数据,放 MRAM;凡是"丢了可以重采"的数据,放 Flash 或者外部存储。这个原则帮我在多个项目里快速做了存储架构决策。
最后分享一个小技巧:MRAM 刚上电时,建议先读一遍状态寄存器,确认 BP0、BP1 都是 0,WEL 是 0。如果状态不对,先写状态寄存器复位,再做其他操作。这个习惯能避免很多"莫名其妙写不进去"的问题。