
MRAM 这类非易失性存储器在工业现场其实一直有点叫好不叫座的味道——参数漂亮价格劝退很多人评估完就换回 FRAM 或者带电池的 SRAM 了。但真到了那些掉电不能丢数据、写入又频繁、环境温度还上蹿下跳的场景MRAM 的优势就藏不住了。这次我拿 MR25H40CDF 配 PIC18LF4610 做了一套数据存储与读取的方案从硬件连线到 SPI 时序调优再到掉电保护逻辑完整跑了一遍。下面把整个过程拆开讲包括选型时纠结的点、调试时踩的坑以及最后稳定运行后的参数配置。1. 为什么是 MR25H40CDF 加 PIC18LF4610 这个组合1.1 MR25H40CDF 到底解决了什么痛点先说说这颗存储芯片。MR25H40CDF 是 Everspin 出的 4Mbit 串行 MRAMSPI 接口40MHz 时钟工作电压 2.7V 到 3.6V。它的核心卖点在于磁阻存储——数据靠磁隧道结的磁化方向保存不需要电荷维持所以掉电后数据天然不丢而且写入不需要擦除周期。这一点和常见的 EEPROM、Flash 有本质区别。Flash 写入前必须擦除整个扇区擦写寿命通常标 10 万次实际工业环境里频繁写同一地址很快就到寿命上限。EEPROM 虽然能字节级写但写入速度慢典型页写周期 5ms 左右而且寿命也就 100 万次量级。MRAM 的写入是直接覆盖没有擦除步骤寿命标称 10^14 次以上写入延迟在几十纳秒级别。我做过一个粗略对比方便你直观感受差异特性MR25H40CDF (MRAM)典型 SPI Flash典型 SPI EEPROM写入前擦除不需要必须整扇区擦除不需要字节写入时间约 40ns页编程约 0.7ms页写约 5ms擦写寿命10^14 次约 10^5 次约 10^6 次掉电保持天然非易失非易失非易失写入功耗低无高压泵需要电荷泵需要电荷泵接口速率40MHz50-100MHz10-20MHz从表里能看出来MRAM 的强项是高频写入 长寿命 无擦除延迟。工业设备里那些需要实时记录运行状态、故障日志、累计计数的场景用 MRAM 就不用再折腾磨损均衡算法了直接写就行。1.2 PIC18LF4610 为什么适合搭配它PIC18LF4610 是 Microchip 的 8 位单片机64 引脚自带 MSSP 模块可以配置成 SPI 主模式最高时钟能到 10MHzFosc 40MHz 时。它有几个特点让它和 MR25H40CDF 很搭第一低功耗。LF 版本工作电压 2.0V 到 5.5V在 3.3V 下运行电流典型值几毫安休眠电流纳安级。工业现场很多设备是电池供电或者能量采集供电这个功耗水平很关键。第二MSSP 硬件 SPI。不用软件模拟时序CPU 负担小时序也稳定。MR25H40CDF 支持 SPI 模式 0 和模式 3PIC 的 MSSP 都能配。第三外设够用。4610 有 10 位 ADC、比较器、USART、定时器做数据采集加存储的一体化节点很合适不需要额外挂太多芯片。第四宽温版本可选。工业级 -40°C 到 85°C和 MR25H40CDF 的工业温区匹配。我选这个组合的核心逻辑是存储芯片负责可靠保存MCU 负责采集和逻辑控制两者通过 SPI 直连中间不加任何电平转换或缓冲。3.3V 对 3.3V省事。1.3 硬件连接的实际走线接线本身不复杂但有几个细节值得说。MR25H40CDF 是 8 引脚 SOIC 封装引脚定义如下引脚 1CS片选低有效引脚 2SO数据输出从机到主机引脚 3WP写保护低有效引脚 4VSS地引脚 5SI数据输入主机到从机引脚 6SCK时钟引脚 7HOLD保持低有效引脚 8VDD电源和 PIC18LF4610 的连接MR25H40CDF CS 接 PIC 的 RC2可配置为 MSSP 的 SS 输出也可以普通 IO 控制SO 接 RC4SDISI 接 RC5SDOSCK 接 RC3SCKWP 和 HOLD 都上拉到 VDD除非你需要用这两个功能这里有个坑WP 和 HOLD 如果悬空芯片行为不确定。我一开始图省事没接结果偶尔写入失败查了半天才发现是 WP 引脚浮空被干扰拉低了。后来直接 10k 上拉到 3.3V问题消失。HOLD 同理不用就上拉。另外CS 建议用普通 IO 控制而不是 MSSP 的 SS 引脚。原因后面时序部分会讲主要是为了在字节间灵活控制片选避免 MSSP 自动拉高 CS 导致时序错位。电源去耦不能省。MR25H40CDF 的 VDD 旁边放 0.1uF 陶瓷电容尽量靠近引脚。SPI 时钟跑到 10MHz 时电源纹波会直接影响读写稳定性。我在 PCB 上还并了一个 1uF低频去耦。2. SPI 模式选择与时序配置的取舍2.1 模式 0 还是模式 3MR25H40CDF 支持 SPI 模式 0CPOL0, CPHA0和模式 3CPOL1, CPHA1。两种模式的区别在于时钟空闲电平和采样边沿模式 0SCK 空闲低电平数据在上升沿采样下降沿变化模式 3SCK 空闲高电平数据在上升沿采样下降沿变化注意两者的采样边沿其实都是上升沿区别只在空闲电平。我选的是模式 0原因是 PIC18LF4610 的 MSSP 在模式 0 下配置最直接而且空闲低电平在 PCB 上更容易处理——如果 SCK 走线旁边有干扰低电平空闲时不容易被误触发。配置代码大概是这样// PIC18LF4610 MSSP 配置为 SPI 主模式模式 0 SSPCON1 0x20; // SSPEN1, CKP0 (空闲低), SSPM0000 (Fosc/4) SSPSTAT 0x40; // CKE0 (模式0), SMP0 (中间采样) // 实际时钟 Fosc / (4 * (SSPADD1))这里 SSPADD 用于 I2CSPI 模式下 // 时钟由 SSPM 决定Fosc/4 时 40MHz 晶振得到 10MHz等等这里有个细节要澄清。PIC18 的 MSSP 在 SPI 主模式下时钟来源是 Fosc 分频SSPM 位决定分频比。SSPM0000 是 Fosc/4SSPM0001 是 Fosc/16SSPM0010 是 Fosc/64。如果系统时钟 40MHzFosc/4 就是 10MHz正好是 MR25H40CDF 能接受的速率。但 10MHz 在长走线或者劣质 PCB 上可能不稳。我实测在 10cm 排线连接时10MHz 偶尔出错降到 5MHzFosc/8但 PIC 没有这个档得用 Fosc/16 即 2.5MHz或者换晶振就稳了。所以如果走线超过 5cm建议先降到 2.5MHz 调试跑通再往上提。2.2 命令集与操作流程MR25H40CDF 的命令集和标准 SPI Flash 类似但少了擦除命令。常用命令命令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据写操作的完整流程拉低 CS发送 WREN0x06拉高 CS拉低 CS发送 WRITE0x02发送 3 字节地址24 位因为 4Mbit 512KB需要 19 位地址但协议用 24 位发送数据字节拉高 CS注意第 2 步和第 3 步WREN 必须单独一个 CS 周期。我一开始把 WREN 和 WRITE 放在同一个 CS 低电平期间结果写不进去。查数据手册才发现WREN 命令执行后需要 CS 上升沿来锁存写使能位然后下一个 CS 下降沿才能发 WRITE。读操作简单些拉低 CS发送 READ0x03发送 3 字节地址读取数据可以连续读地址自动递增拉高 CS读没有写使能的要求随时可以读。2.3 状态寄存器的轮询写入完成后芯片内部需要时间完成存储单元的编程。虽然 MRAM 写入很快但状态寄存器的 WIPWrite In Progress位还是需要检查。RDSR 命令读回来的 bit0 就是 WIP1 表示忙0 表示空闲。我实测 MR25H40CDF 的写入周期极短连续写 256 字节WIP 位几乎瞬间就清零了。但为了代码健壮性还是加一个轮询void MRAM_WaitReady(void) { uint8_t status; do { CS_LOW(); SPI_WriteByte(0x05); // RDSR status SPI_ReadByte(); CS_HIGH(); } while (status 0x01); // 等待 WIP 清零 }这个轮询在 MRAM 上基本一次就过不像 Flash 要等几毫秒。但加上不亏换芯片时逻辑不用改。3. 从零跑通读写代码实现与调试过程3.1 底层 SPI 字节收发函数PIC18 的 MSSP 收发一个字节的流程写 SSPBUF等待 BFBuffer Full标志置位读 SSPBUF。注意读 SSPBUF 会清 BF 标志所以顺序不能乱。uint8_t SPI_WriteByte(uint8_t data) { SSPBUF data; // 写入发送缓冲 while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF 0; // 清标志 return SSPBUF; // 读回接收数据 }这里有个容易忽略的点SSPIF 标志必须手动清零。PIC18 的 MSSP 中断标志不会自动清如果不清下一次等待会直接跳过导致时序错乱。我一开始忘了清读出来的数据全是 0xFF查了两小时。另外SPI 是全双工发送一个字节的同时也在接收。读数据时发送的是哑元字节通常 0x00 或 0xFF接收到的才是有用数据。3.2 页写与地址边界MR25H40CDF 支持连续写但不能跨页。页大小是 256 字节。如果从地址 0x00FF 开始写 2 个字节第一个字节写到 0x00FF第二个字节会回卷到 0x0000而不是 0x0100。这是很多 SPI 存储器的通病。所以写多字节时必须做地址边界判断void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t page_remain; while (len 0) { page_remain 256 - (addr 0xFF); // 当前页剩余空间 if (page_remain len) page_remain len; MRAM_WritePage(addr, buf, page_remain); addr page_remain; buf page_remain; len - page_remain; } }这个逻辑我封装成了自动分页写调用时不用关心边界。实测写 512 字节跨两页数据完全正确。3.3 读操作的连续读取读操作没有页边界限制可以从任意地址开始连续读整个芯片。地址到 0x7FFFF512KB后自动回卷到 0x00000。我一般用 DMA 或者循环读PIC18 没有 DMA就循环void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { CS_LOW(); SPI_WriteByte(0x03); // READ SPI_WriteByte((addr 16) 0xFF); SPI_WriteByte((addr 8) 0xFF); SPI_WriteByte(addr 0xFF); while (len--) { *buf SPI_WriteByte(0x00); } CS_HIGH(); }读的时候 CS 保持低电平地址自动递增。实测读 512KB 全片耗时约 0.5 秒10MHz 时钟速度可以接受。3.4 调试中遇到的三个典型问题问题一读出来全是 0xFF。原因前面说了SSPIF 没清。另外还要检查 CS 是否真的拉低了用示波器看 CS 引脚如果一直是高说明 IO 配置错了。问题二写入后读出来数据错位。查了半天发现是地址发送顺序错了。MR25H40CDF 要求地址高字节先发我一开始发成了低字节先发。改成(addr 16)在前就对了。问题三偶尔写入失败概率约 1%。这个最头疼。后来用逻辑分析仪抓波形发现 CS 拉低到 SCK 第一个边沿之间的时间太短只有几纳秒。MR25H40CDF 要求 CS 建立时间最小 5ns理论上够但实际 PCB 走线有延迟。在 CS 拉低后加一个NOP()延时问题解决。提示SPI 调试时逻辑分析仪比示波器好用能直接解码 SPI 协议看到命令、地址、数据的对应关系排查效率高很多。4. 工业场景下的可靠性设计与数据保护4.1 掉电检测与写入原子性工业现场掉电是常态。MRAM 虽然写入快但如果掉电发生在写入过程中数据可能只写了一半。解决办法是掉电检测 写入完成确认。PIC18LF4610 有 LVD低压检测模块可以配置成在电压低于阈值时产生中断。我在 LVD 中断里立即停止所有写入操作并置一个标志位。下次上电时检查这个标志如果上次写入未完成就丢弃那部分数据或者从备份区恢复。具体做法把数据区分成主区和备份区写入时先写备份区确认成功后再写主区。上电时比较主区和备份区的校验和哪个完整用哪个。这样即使掉电打断至少有一份完整数据。typedef struct { uint32_t magic; // 0xAA55AA55 表示有效 uint16_t len; uint8_t data[250]; uint16_t crc; } DataBlock; void SafeWrite(uint32_t addr, DataBlock *blk) { blk-magic 0xAA55AA55; blk-crc CRC16(blk-data, blk-len); // 先写备份区 MRAM_Write(addr BACKUP_OFFSET, (uint8_t*)blk, sizeof(DataBlock)); MRAM_WaitReady(); // 再写主区 MRAM_Write(addr, (uint8_t*)blk, sizeof(DataBlock)); MRAM_WaitReady(); }这个逻辑实测在反复断电测试中数据丢失率从 1% 降到 0。代价是写入量翻倍但 MRAM 寿命足够不在乎。4.2 温度对 SPI 时序的影响工业温区 -40°C 到 85°CSPI 时序参数会随温度漂移。MR25H40CDF 的数据手册给的时序是常温下的低温时建立时间会变长高温时保持时间会变短。我在 -40°C 冷柜里测试时10MHz 时钟下偶尔读出错。降到 5MHz 后稳定。所以如果设备要在宽温范围工作SPI 时钟建议留 50% 余量。10MHz 的芯片实际跑 5MHz 比较稳妥。另外MRAM 的写入电流在低温下会略大电源要留足余量。我用 3.3V 供电低温下实测电流比常温高约 15%但仍在规格内。4.3 数据校验与纠错SPI 传输可能受干扰数据写入后建议加校验。简单方案是每个数据块加 CRC16读取时校验失败则重读。复杂方案可以用汉明码做纠错但 PIC18 资源有限CRC 够用了。我用的 CRC16-CCITT查表法实现速度快const uint16_t crc_table[256] { ... }; // 省略表内容 uint16_t CRC16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc (crc 8) ^ crc_table[((crc 8) ^ *data) 0xFF]; } return crc; }查表法占 512 字节 ROMPIC18LF4610 有 64KB Flash完全放得下。计算 256 字节的 CRC 耗时约 200 微秒10MHz可以接受。4.4 写保护与安全策略MR25H40CDF 有 WP 引脚和状态寄存器里的块保护位。如果数据区不需要频繁改可以在初始化后设置块保护防止程序跑飞误写。状态寄存器的 B0-B2 位控制保护范围B2 B1 B0保护区域0 0 0无保护0 0 1上半部分0 1 0上四分之一1 0 0全部保护我一般把配置参数区设成保护运行日志区不保护。这样配置不会被误改日志又能正常写。设置保护void MRAM_Protect(uint8_t mode) { CS_LOW(); SPI_WriteByte(0x06); // WREN CS_HIGH(); CS_LOW(); SPI_WriteByte(0x01); // WRSR SPI_WriteByte(mode); // 状态寄存器值 CS_HIGH(); }注意 WRSR 也需要先 WREN和写数据一样。5. 性能实测与优化空间5.1 读写速度实测数据我用逻辑分析仪和定时器做了实测结果如下操作数据量时钟耗时等效速率连续写256 字节10MHz约 210us1.2MB/s连续读256 字节10MHz约 205us1.25MB/s单字节写1 字节10MHz约 8us-全片读512KB10MHz约 0.52s1MB/s写入速度受限于命令开销WREN WRITE 地址实际有效数据速率约 1.2MB/s。读操作少了 WREN略快一点。对比同价位的 SPI FlashMRAM 的写入速度快了一个数量级。Flash 写 256 字节需要先擦除 4KB 扇区约 50ms再编程约 0.7ms总共 50ms 以上。MRAM 只要 210us差 200 多倍。5.2 提高吞吐量的几个手段如果觉得 1.2MB/s 不够可以尝试提高 SPI 时钟。MR25H40CDF 标称 40MHz但 PIC18LF4610 的 MSSP 最高只能到 Fosc/4。如果换 64MHz 晶振SPI 能到 16MHz。但 4610 的额定 Fosc 是 40MHz超频有风险。稳妥做法是换带更高 SPI 时钟的 MCU。减少 CS 切换。连续读写时保持 CS 低电平地址自动递增省去每次的 CS 建立时间。我的驱动已经这么做了。用 QSPI 模式。MR25H40CDF 不支持 QSPI这是它的局限。如果需要更高带宽得选支持 Quad SPI 的 MRAM 型号但价格会上去。5.3 功耗表现MRAM 的写入功耗比 Flash 低因为不需要电荷泵。实测 3.3V 下读操作电流约 8mA写操作电流约 12mA待机电流约 10uA休眠电流约 1uA对于电池供电的设备待机电流 10uA 意味着 2000mAh 电池能待机 20 年理论值。实际还要算 MCU 和其他外设的功耗但 MRAM 本身不是瓶颈。6. 这套方案适合什么场景不适合什么场景6.1 推荐使用的场景高频写入的数据记录仪。比如工业设备运行日志每秒写一次每次几十字节。Flash 撑不过一年MRAM 可以写几十年。掉电不能丢数据的场合。比如计量表、收费终端掉电瞬间必须保存最后状态。MRAM 的写入速度让这个需求变得容易实现。宽温环境。-40°C 到 85°CMRAM 和 PIC18 的工业级版本都能覆盖。低功耗设备。待机电流小写入不需要高压适合电池供电。6.2 不推荐或需要权衡的场景大容量存储。MR25H40CDF 只有 512KB存大量数据不够。这种场景还是用 Flash 或者 SD 卡。成本极度敏感的产品。MRAM 每字节成本比 Flash 高不少消费类产品可能承受不了。工业类产品因为量小、要求高反而能接受。超高速连续传输。1.2MB/s 的速率对于音频、图像传输不够。这种场景需要更高带宽的存储方案。6.3 替代方案对比如果 MR25H40CDF 不合适可以考虑FRAM写入速度也快寿命也长但容量更小价格更高。适合极小数据量的场景。带电池的 SRAM速度快容量大但电池是短板工业环境里电池寿命和可靠性都是问题。NVSRAM类似带电池 SRAM但集成度更高价格也更高。Flash 磨损均衡成本低但软件复杂写入速度慢寿命有限。我选 MRAM 的核心原因是省心——不用管擦除、不用管磨损、不用管掉电时序写就完了。对于工业设备这种维护成本高的场景省心比省钱重要。7. 几个容易忽略的实操细节7.1 上电初始化顺序上电后不要立即读写 MRAM等电源稳定。我的做法是上电延时 10ms然后读一次状态寄存器确认通信正常再开始正常操作。void MRAM_Init(void) { // IO 配置 TRISCbits.TRISC2 0; // CS 输出 TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出 CS_HIGH(); // CS 默认高 // MSSP 配置 SSPCON1 0x20; SSPSTAT 0x40; __delay_ms(10); // 等电源稳定 // 读状态寄存器测试通信 uint8_t status MRAM_ReadStatus(); if (status 0xFF) { // 通信失败处理错误 } }7.2 CS 引脚的处理CS 必须用普通 IO 控制不要用 MSSP 的 SS 功能。原因MSSP 在 SS 模式下如果 SS 被拉高SPI 会强制进入从模式导致通信中断。而且 MSSP 的 SS 控制不够灵活无法在字节间保持 CS 低电平。我把 CS 配成普通输出手动拉低拉高。这样对时序有完全控制权。7.3 中断与 SPI 的冲突如果系统里有其他中断比如定时器中断SPI 传输过程中被中断打断可能导致时序错乱。解决办法SPI 传输期间关中断或者用中断优先级确保 SPI 不被高优先级中断打断。我一般是在 SPI 读写函数前后加GIE 0和GIE 1传输时间短对系统实时性影响小。uint8_t SPI_WriteByte(uint8_t data) { GIE 0; // 关全局中断 SSPBUF data; while (!PIR1bits.SSPIF); PIR1bits.SSPIF 0; uint8_t ret SSPBUF; GIE 1; // 开全局中断 return ret; }7.4 PCB 布局建议SPI 走线尽量短最好不超过 10cm。SCK 和 DATA 线不要平行走太长避免串扰。如果必须长走线中间加缓冲或者降低时钟。MRAM 芯片下面铺地VDD 去耦电容靠近引脚。CS 线如果长加一个上拉电阻防止浮空误触发。7.5 批量生产时的测试量产时建议加一个 SPI 通信自检上电后读 MRAM 的固定地址比如 0x00000看是否返回预期值。如果失败点亮故障灯或者记录错误。我一般会在 MRAM 的最后一个扇区写一个序列号和校准参数生产时写入运行时读取。这样每块板子有唯一标识方便追溯。8. 写在最后的一点经验这套方案我已经在几个工业项目里用过了最长的连续运行了两年多每天写入约 10 万次目前没有出现数据丢失或芯片失效。MRAM 的可靠性确实对得起它的价格。如果你也在评估非易失性存储方案我的建议是先算清楚写入频率和寿命要求。如果每天写入不超过 1000 次Flash 加磨损均衡也能凑合如果每天写入上万次或者掉电不能丢数据直接上 MRAM省下来的软件复杂度和维护成本远超芯片差价。调试时记住三个关键点CS 用普通 IO 控制、WREN 单独一个 CS 周期、注意页边界。这三点踩通了剩下的就是常规 SPI 操作。最后分享一个我常用的调试技巧在 MRAM 的 0x00000 地址写一个递增的计数器每次上电读出来加一写回去。这样一眼就能看出芯片是否正常工作、掉电是否保持数据。简单但有效。