最近在调一块带 CAN 总线的工业控制器,主控用了 Microchip 的 PIC18F4682,数据存储则选了 Everspin 的 MR25H40CDF(4Mbit 串行 MRAM)。这套组合不是拍脑袋定的,而是反复对比过 EEPROM、Flash、FRAM 之后选出来的。很多工程师一提到“掉电保存”就默认用 24C02 或者 SPI Flash,但在工业现场跑过几年就会明白:写寿命、写速度、掉电时序这三个坑,足够让你把 PCB 改三版。
这篇文章我把这套方案的完整设计思路、硬件连接、SPI 驱动代码、掉电保存的处理细节和踩坑记录全部整理出来。适合正在做工业数据采集、设备日志记录、参数存储的同学参考,尤其是对“频繁写入 + 突然断电”有硬性要求的场景。
1. 为什么最终选了 MRAM,而不是 EEPROM 或 Flash
先说结论:MR25H40CDF 的本质是磁性随机存储器,但它兼容 SPI NOR Flash 的引脚和命令集,MCU 端写驱动几乎和操作 Flash 一样简单。最大的区别在于——它写入前不需要擦除,也没有 P/E 寿命上限,理论上可以无限次写入,这一点直接改变了我对“掉电保存”这件事的设计思路。
1.1 EEPROM 和 Flash 在工业场景下的三个痛点
工业设备的参数存储,最常见的是 EEPROM(I2C 的 24C 系列、SPI 的 25AA 系列)。便宜、成熟、资料多,但写寿命一般只有 1 百万次,写一个字节要等 3~5ms 的 tWR。如果你做的是计量类设备,每秒钟都要累计一次流量数据,或者每收到一帧 CAN 消息就要记录一条状态,EEPROM 很快就会被打穿。
Flash 的寿命确实比 EEPROM 好一些(通常 10 万次擦写),但 Flash 是块擦除结构,写入前必须先擦整块。假设你的扇区是 4KB,每次要更新其中 16 字节的参数,你实际是在反复擦写整个 4KB 块,磨损放大很严重。更麻烦的是,Flash 掉电瞬间如果正好在执行擦除,有可能导致整块数据损坏,这在高振动、强干扰的工业现场是不可接受的。
FRAM 其实也是好选择,无限写入、速度快,但容量普遍偏小,大一点的串行 FRAM 价格也不低。MRAM 正好卡在了一个很舒服的位置:SPI 接口,4Mbit 容量,SRAM 级别的读写速度(40MHz 时钟下读操作跟玩一样),没有擦除概念,数据保持 10 年以上,工作温度范围可以到工业级甚至汽车级。
1.2 MR25H40CDF 的技术特性与选型依据
MR25H40CDF 是 Everspin 的串行接口 MRAM,容量 4Mbit,也就是 512KB,使用 8 脚 DFN 封装。它的命令集兼容经典的 SPI Flash,比如 READ(0x03)、WRITE(0x02)、WREN(0x06)、RDSR(0x05),芯片默认支持 SPI Mode 0 和 Mode 3。
关键参数我列一份,方便你对照选型:
| 参数 | 数值 | 说明 |
|---|---|---|
| 容量 | 4Mbit / 512KB | 按 512 字节一页,共 1024 页 |
| SPI 时钟 | 最高 40MHz | 普通 SPI Flash 一般是 104MHz,但 MCU 端往往跑不到 |
| 写入寿命 | 无限次 | 没有写消耗概念,这是选择它的核心理由 |
| 数据保持 | >10 年 @ 105℃ | 高温下也有保证 |
| 工作电压 | 3.0V ~ 3.6V | 注意,不能直接接 5V 系统 |
| 写入方式 | 单字节/多字节连续写入 | 和 EEPROM 一样,无块擦除 |
| 待机电流 | 典型几 mA | 相对于 Flash 略高,休眠模式下要注意 |
另外,MR25H40CDF 的写操作不会像 Flash 那样出现“写入后需要等待内部编程完成”的情况,命令发完数据立即生效。这意味着你不需要像操作 25 系列 Flash 那样轮询状态寄存器的 WIP 位,也不需要为了磨损均衡去做动态地址映射。代码层逻辑会简单很多。
还有一个值得提的细节:MRAM 是靠磁阻效应存储数据,很多人会担心磁铁干扰。实际封装后的芯片内部有屏蔽层,常规工业环境的弱磁场不会影响数据,但如果你在产品外壳上加装了强磁吸结构,建议让永磁铁离芯片远一点,这是比较稳妥的做法。
2. 硬件连接:PIC18F4682 与 MR25H40CDF 的 SPI 总线设计
PIC18F4682 是 Microchip 的增强型 CAN 8 位 MCU,40 引脚,最高 40MHz 工作频率,带 ECAN 模块、SPI 模块、多个定时器和 10 位 ADC。选它做工业控制板的主控,主要图的是 CAN 接口和 SPI 外设齐全,一颗芯片解决通讯和存储两件事。
2.1 引脚连接与电平匹配
MR25H40CDF 的工作电压是 3.0V~3.6V,典型 3.3V。如果 PIC18F4682 运行在 5V 供电下,SPI 引脚之间需要电平转换。我的建议是直接让整板跑 3.3V 逻辑,PIC18F4682 本身支持 3.3V 供电,内部的 CAN 收发器接口再单独配置。这样一来,MCU 与 MRAM 的 SPI 引脚可以直接连,不需要额外的电平转换芯片,走线干净,也少一层故障点。
引脚接线表如下:
| PIC18F4682 引脚 | MR25H40CDF 引脚 | 说明 |
|---|---|---|
| RC3(SCK) | SCK | SPI 时钟 |
| RC4(SDI) | MISO | MRAM 数据输出 |
| RC5(SDO) | MOSI | MRAM 数据输入 |
| RC2(CS) | CS# | 片选,低有效 |
| 任意 GPIO | WP# | 写保护,固定接高 |
| 任意 GPIO 或直接接地 | HOLD# | 暂停通讯,固定接高 |
| 3.3V | VDD | 电源,并接 100nF 去耦电容 |
| GND | VSS | 地 |
注意 MR25H40CDF 8 脚封装里,A0 是地址扩展引脚,在单片使用时建议接地,不接也没问题,但接地最稳。HOLD# 和 WP# 这两个脚才是真正的坑,很多人第一次画板只接了 SCK/SDI/SDO/CS,结果读写不稳定,最后发现是 HOLD# 悬空导致偶发通讯错误。这两个引脚一定要通过 10K 电阻上拉到 3.3V。
2.2 电源与去耦的细节
MRAM 在写入瞬间电流变化比 EEPROM 大,尤其是连续写操作时,电源纹波会直接影响 SPI 信号质量。我的习惯是在 VDD 引脚放 100nF 陶瓷电容靠近芯片引脚,同时在一个较小范围内放一颗 4.7uF~10uF 的钽电容。如果板上同时有 CAN 收发器,建议给 CAN 部分单独加磁珠隔离,避免通讯瞬间的大电流耦合到 SPI 上。
还有一个容易忽略的点:如果掉电检测需要保存数据,MCU 的 VDD 检测脚要能捕捉到电源跌落,因此 MRAM 的 VDD 应该和 MCU 在同一路电源上。如果 MRAM 单独用一颗 LDO 供电,掉电时 MCU 都复位了,MRAM 反而还活着,你的保存逻辑就乱了。我的做法是让 MCU 和 MRAM 共用 3.3V 主电源,掉电检测模块检测 5V 输入侧,这样能比 MCU 掉电提前大约几百毫秒触发中断。
2.3 为什么说 PIC18F4682 选得合适
PIC18F4682 这个型号相比同系列的 4680,主要增强点是 ECAN 模块,缓冲区有 2KB 的 SRAM,可以配置多个滤波器并支持 CAN 2.0B。我做的是工业设备状态采集节点:CAN 总线上定时上报传感器数据,同时本地记录运行日志。MCU 收到 CAN 数据帧后,一边转发到上位机,一边写入 MRAM。事件记录带时间戳,最多存储几千条,满了之后循环覆盖。PIC18F4682 的 RB1 引脚可以用作 CAN 中断输入,配合 MSSP 的 SPI 中断,整个读写流程不需要频繁轮询,主循环可以干别的事。
实际跑下来的数据:40MHz 系统时钟下,MSSP 的 SPI 主模式时钟最大可以配到 Fosc/4,也就是 10MHz。MR25H40CDF 最高 40MHz,折算下来完全没有发挥 MRAM 的极限能力,但 PIC18F 的数据手册说了算,10MHz 在工业场景已经够用。如果以后换更高端的 MCU,SPI 时序也不需要改,MRAM 这边不会成为瓶颈,扩展性上留了余地。
3. 软件实现:SPI 驱动与 MRAM 读写核心代码
PIC18F 的开发我推荐用 MPLAB X IDE + XC8 编译器,C 语言写起来跟标准 C 差别不大。MR25H40CDF 的命令序列和 SPI Flash 高度接近,核心驱动不需要几百行,但有几个关键点必须注意:SPI 模式配置、写使能命令、连续读写地址自增。
3.1 SPI 主模式初始化
先看初始化代码。我使用的是 MSSP 模块的 SPI 主模式,配置成 Mode 0(CPOL=0,CPHA=0),时钟为 Fosc/4。有个容易搞混的地方:在 PIC18 的 MSSP 寄存器里,CKP 对应 CPOL,CKE 对应 CPHA。我测试时把 CKE 配置为 1,CKP 配置为 0,对应 SPI Mode 0。如果你用模式 3,就两边都置 1。MR25H40CDF 两种模式都支持,但整个系统里如果有其他 SPI 设备,尽量统一。
void SPI_Init(void) { // SCK=RC3, SDO=RC5, SDI=RC4, CS=RC2 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // MOSI 输出 TRISCbits.TRISC4 = 1; // MISO 输入 TRISCbits.TRISC2 = 0; // CS 输出 SSPCON1 = 0b00100010; // SPI 主模式,时钟 = Fosc/4,CKP=0 SSPCON1bits.SSPEN = 1; SSPSTATbits.CKE = 1; // 对应 SPI Mode 0 SSPSTATbits.SMP = 1; // 在数据输出末尾采样,兼容更长距离走线 }注意 SMP 位:工业现场 SPI 线如果稍微长一点(10cm 以上),建议把 SMP 置 1,即数据在 SCK 第二个边沿被采样,给信号稳定留出时间。我自己调试时曾经遇到过 20cm 排线连接 MRAM,SMP=0 在高频下偶发读到错误字节,改成 SMP=1 后问题消失。这个经验在 PCB 走线比较长的结构件里很实用。
CS 引脚用普通 GPIO 控制,不要把 CS 绑定到硬件自动片选。因为 MRAM 的读写命令需要 CS 低电平期间逐字节发送地址和数据,手动拉高拉低最灵活,也方便后续调试时序。
3.2 基础读写函数
接下来是关键部分。MR25H40CDF 的写操作必须先发 WREN(0x06)使能命令,然后才能发 WRITE(0x02)。这一点和 EEPROM 有些像,和 Flash 更像。读操作(0x03)不需要写使能。
unsigned char SPI_Transfer(unsigned char data) { SSPBUF = data; while (!PIR1bits.SSPIF); PIR1bits.SSPIF = 0; return SSPBUF; } void MRAM_CS_Low(void) { LATCbits.LATC2 = 0; } void MRAM_CS_High(void) { LATCbits.LATC2 = 1; } void MRAM_WriteEnable(void) { MRAM_CS_Low(); SPI_Transfer(0x06); // WREN MRAM_CS_High(); } void MRAM_WriteBytes(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_WriteEnable(); MRAM_CS_Low(); SPI_Transfer(0x02); // WRITE 命令 SPI_Transfer((addr >> 16) & 0xFF); // 地址高字节 SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (i = 0; i < len; i++) { SPI_Transfer(buf[i]); } MRAM_CS_High(); } void MRAM_ReadBytes(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_Low(); SPI_Transfer(0x03); // READ 命令 SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = SPI_Transfer(0x00); } MRAM_CS_High(); }关于地址:MR25H40CDF 虽然是 4Mbit,也就是 512KB,但地址字段仍然是 24 位。高字节有一部分是用不到的,对于这个型号只需要关心低 19 位地址。写的时候把无用的高位填 0 就好。如果以后换用容量更大的 MRAM 型号(比如 MR25H256CDF,32MB),这套代码可以直接兼容,只需修改地址范围宏定义。
另一个值得注意的点是连续写(Page Write)。MR25H40CDF 支持在一个命令中连续写入多个字节,地址会自动自增。但和 Flash 的 Page Program 限制不同,MRAM 的连续写入没有“跨页断开”的限制,理论上可以一口气写完整块 512KB。不过在单片机里,一次传输缓冲区长度通常有限,我一般建议控制在 256 字节以内,方便管理和校验。
3.3 状态寄存器与写保护
MRAM 的状态寄存器不像 Flash 那样有“忙”标志,主要作用是配置写保护行为和查询写使能锁存器的状态。我一般在驱动里不强制配置状态寄存器,默认状态(写保护关闭)够用。但如果你担心系统跑飞导致误写,可以启用状态寄存器里的 WPEN 位,配合 WP# 引脚实现硬件写保护。这里提醒一下,WP# 引脚我前面建议固定接高,等于关闭硬件写保护,这适合频繁写入的日志型应用。如果是关键参数区,建议把 WP# 用 GPIO 控制,平时写保护,需要更新参数时再临时拉低。
以下是状态寄存器读取代码:
unsigned char MRAM_ReadStatus(void) { unsigned char status; MRAM_CS_Low(); SPI_Transfer(0x05); // RDSR status = SPI_Transfer(0x00); MRAM_CS_High(); return status; }实际使用中,写完数据后可以不读状态,因为 MRAM 的写入是即时生效的。但我建议在开发阶段加上一个“写后回读校验”,尤其在做掉电保存的时候,确认数据写进去了再进入休眠或复位流程。虽然 MRAM 的可靠性非常高,但任何总线通讯都可能受干扰,回读校验多花的时间不到 1ms,工业产品值得加这个成本。
4. 可靠保存与场景应用:从掉电存储到日志记录
有了基础读写驱动,下一步就是把 MRAM 真正用进工业场景。我在这套系统里做了两块功能:运行日志循环存储和掉电瞬间的紧急参数保存。这两个功能的实现思路完全不同,重点讲一下。
4.1 掉电检测与数据紧急保存
工业设备最怕的断电场景不是断电本身,而是“断电前一刻正在更新参数”。参数写到一半,电压跌到 MCU 最低工作点以下,数据直接损坏。用 MRAM 之后这个风险大幅降低,因为单字节写入不依赖电荷泵,写入的实际物理过程非常快,正常 SPI 时钟(10MHz)下写一个字节只要不到 1us,几乎是瞬时的。
但即便 MRAM 写入快,你也得保证在电压掉到不能工作之前启动写操作。我的做法是:在 5V 输入电源侧加精密电阻分压,送到 MCU 的模拟比较器或者 ADC 通道做掉电检测。当检测到电压低于阈值(比如 4.5V),立即进入中断,把 RAM 中的临时参数写进 MRAM 的固定区域。
void PowerDown_Handler(void) { unsigned char tmp[16]; // 关闭不重要中断,停止 CAN 收发 // 把当前累积的数据、计数器值拷贝到 tmp tmp[0] = hour; tmp[1] = minute; tmp[2] = currentValue & 0xFF; tmp[3] = (currentValue >> 8) & 0xFF; MRAM_WriteBytes(SAVE_FLAG_ADDR, tmp, sizeof(tmp)); // 写完后发一个字节标记数据有效,下次上电检查这个标记 }这里有一个非常实用的技巧:不要直接把有效数据放在固定地址,而是先写“数据”,再写“有效标记”。下次上电先读有效标记,如果是 0xA5,说明掉电保存完成,数据可信;如果不是,说明保存流程中断,数据不可信。这个双区策略虽然简单,但能避免“明明读到了数据,但数据是写了一半的”这种尴尬情况。我甚至会在写有效标记之后把原有效标记清零,形成完整的双槽交替写入,进一步降低同时损坏的概率。
掉电中断触发后要注意:MRAM 的写操作需要 CS 引脚多个跳变,如果 MCU 电源不稳定导致 SPI 信号失真,保存可能失败。所以掉电检测阈值不能太低,要留出至少 1ms 的裕量。PIC18F4682 内部有欠压复位模块,但那个阈值太低,只能保护 MCU 不死机,不能保证你的保存逻辑跑完。建议用 ADC 采样外部电源,阈值设在高一些的位置。
4.2 循环日志存储与磨损均衡
日志记录通常是“写满后从头覆盖”。我之前用 SPI Flash 做日志记录时,因为擦除块对齐问题,每写一条日志都要处理磨损均衡,代码复杂且容易出错。换 MRAM 之后,这个问题直接消失了。
我的日志区设计是划出 128KB 作为日志空间,每条日志固定 32 字节,头部包含时间戳和数据类型。用一个索引变量记录当前写位置,每次上电从固定位置读取索引,直接顺序写。日志区用完后,索引清零,从头部重新覆盖。因为 MRAM 无限次写入,不需要担心覆盖导致个别扇区失效。
#define LOG_BASE_ADDR 0x10000 #define LOG_ENTRY_SIZE 32 #define LOG_MAX_ENTRIES 4096 unsigned int log_index; void Log_Save(unsigned char *entry) { unsigned long addr = LOG_BASE_ADDR + ((unsigned long)log_index * LOG_ENTRY_SIZE); MRAM_WriteBytes(addr, entry, LOG_ENTRY_SIZE); log_index++; if (log_index >= LOG_MAX_ENTRIES) { log_index = 0; // 循环覆盖 } // 将 log_index 写回索引区 MRAM_WriteBytes(LOG_INDEX_ADDR, (unsigned char *)&log_index, sizeof(log_index)); }索引区和日志区分离,会多占用一点点 MRAM 空间,但好处是上电时不需要扫描整个日志区就能定位写位置,速度快。这个优化在 512KB 容量的 MRAM 上很划算。
需要注意,日志保存不能一条一写地把索引也更新了,不然每次日志操作变成两次写操作。我一般设计成:索引值缓存在 RAM 中,掉电保存时再统一写回索引区。如果系统直接断电且没有触发掉电保存,索引丢失最多也只会导致下次覆盖位置偏移,日志内容本身不会损坏,这是循环日志能接受的误差。
4.3 与 CAN 模块联动:记录总线报文
PIC18F4682 的 ECAN 模块接收报文时会产生中断,我把接收到的关键报文(比如遥控指令、参数配置帧)直接转发到 MRAM 缓冲区。这一功能在调试阶段特别好用:通过 CAN 分析仪发出各种测试报文,MRAM 完整记录每一个变数,之后用 SPI 转 USB 工具把 MRAM 内容导出,跟 CAN 分析仪的日志对一下,系统行为一目了然。
实际做的时候要注意缓冲区大小:ECAN 有 2KB 的 SRAM 可用作缓冲区,但如果你在中断里做 SPI 写 MRAM 的操作,需要控制时间。SPI 写 32 字节在 10MHz 下大约需要 30us,这在 CAN 中断里完全可以接受,不会造成报文丢失。但更稳妥的方式是中断里只把报文拷贝到 RAM 软缓冲区,主循环统一写 MRAM。两个方案我都试过,前者代码简单但阻塞时间长,后者更稳,建议选后者。
5. 常见问题与排查技巧实录
把调试 MR25H40CDF 和 PIC18F4682 过程中遇到过的典型问题列一个排查表,这些问题在论坛里被反复问过,很多都是硬件细节引起的。
| 问题现象 | 可能原因 | 排查思路与解决 |
|---|---|---|
| 第一个字节正常,后续字节全 FF | HOLD# 引脚悬空或受到干扰 | HOLD# 必须上拉到 VDD,不要在走线时绕到高噪声区域 |
| 写入后读出数据只有低位对 | SPI 模式不对 | 检查 CKP/CKE 配置,用逻辑分析仪对比时序,确认 Mode 0 或 Mode 3 |
| 偶发写入失败,回读校验不过 | 电源纹波大,或 CS 毛刺 | VDD 加 100nF + 10uF 去耦;CS 线上避免长走线并行时钟线 |
| 上电第一次读正常,连续读偶尔错 | SPI 时钟太快,信号建立时间不足 | 降低时钟到 Fosc/16,或将 SMP 位配置为 1 采样末端 |
| 掉电保存偶尔成功偶尔失败 | 掉电检测阈值太低,电压已不足以驱动 SPI 可靠通讯 | 把检测阈值调高,预留至少 2ms 掉电裕量 |
| 多片 MRAM 在同一 SPI 总线上互相干扰 | CS 控制不严格,或者命令结束没有拉高 CS | 确保每个 CS 独立 GPIO,严格在指令结束时置高;未选中芯片的 CS 保持高 |
| 高温老化后数据读出异常 | 供电电压长期偏低,或器件供电超过 3.6V | 确认电源实际输出,MRAM 不是 5V 器件,超压会损伤存储单元 |
除表格中的问题,还有一个坑值得单独说:MR25H40CDF 的 DFN-8 封装底部有大焊盘(EP),这个焊盘要接地,但很多手工样板焊接时容易把大焊盘和引脚桥接。我吃过一次亏:焊完之后 SPI 时钟波形抖动严重,排查半天,最终发现是底层焊盘虚焊导致接地不良。如果你是自己打样板手工焊,建议在焊盘处多上锡,用热风枪吹透,最好在 PCB 背面加散热过孔。
另外,开发阶段我强烈建议用逻辑分析仪抓一下 SPI 时序。MR25H40CDF 的时序几乎和 SPI Flash 一样,抓出来可以对齐 CS 拉低和时钟边沿,确认地址数据是否在正确的节拍上发送。我第一次调试时,MRAM 能读但写不进数据,看波形才发现 WREN 命令之后 CS 没有拉高,导致写使能状态被后续的 WRITE 命令覆盖。代码看起来没问题,但实际引脚时序不对,这种问题只有抓波形才能定位。
6. 扩展:固件升级备份和电源监测数据的存储
除日志和参数,MRAM 的 512KB 空间还能做不少事。我在这套板子上预留了 256KB 做固件备份区。Bootloader 从 CAN 收到新固件后,先写入 MRAM 的固件暂存区,校验通过后再由 Bootloader 把固件从 MRAM 搬运到 PIC 的程序 Flash。这个方案的好处是:即使固件传输过程中掉电,MRAM 里的旧数据和暂存的新数据都不会丢,上电后 Bootloader 可以判断是否继续烧录。如果直接用 CAN 往 PIC 内部 Flash 写,传输中断一次,板子就变砖了。
MRAM 写入速度对于固件升级也是足够的:PIC18F4682 的最高 SPI 时钟 10MHz,写一个 64KB 的固件大概需要 50ms 的量级(实际因为要读校验,会多花一点时间),完全能接受。这个应用是 MRAM 相比其他非易失存储特别大的优势,因为 Flash 的擦除块太大,无法按字节无缝存固件镜像,EEPROM 容量又不够。
另外还可以把开机次数、累计运行时间、设备故障码统计这些“慢变数据”也放 MRAM。这些数据需要频繁更新,比如每 1 秒更新一次运行时间,一年下来就是 3150 万次写入。这种量级我用 EEPROM 不敢想,但 MRAM 随便写。如果你的产品需要做预测性维护,这些数据就是最有价值的基础资产。
我个人在实际调这批板子时还有一个体会:MRAM 和 PIC18F4682 这类带 CAN 的 MCU 组合,非常适配分布式工业设备。比如一套系统里有几十个采集节点,每个节点本地存一份完整的数据,上位机通过 CAN 轮询读取。节点死了,拆下来换一块,MRAM 里的历史数据还在,对于事后故障分析很有价值。这种“存储贴近数据源头”的设计思路,是 MRAM 容量和可靠性的完美用武之地。
如果后续要把这套方案移植到其他 MCU 平台,比如 STM32 或者国产 GD32,驱动代码几乎不用改,只需要把 SPI 底层接口换成对应平台的库函数。MR25H40CDF 对 SPI 时序要求很宽松,我实测过用 STM32 的硬件 SPI 时钟 20MHz 也没问题。这也是我推荐大家选择标准串行接口 MRAM 的原因:技术红利,通用接口,换平台成本低。