
MRAM 这类存储介质在工业现场其实一直有点叫好不叫座的味道——参数漂亮价格劝退很多人评估完就换回 FRAM 或者带电池的 SRAM 了。但真到了那些掉电不能丢数据、写入又特别频繁、还要扛住高低温循环的场景绕一圈回来发现能同时满足这几个条件的选项并不多。这次我拿 MR25H40CDF 这颗 4Mbit 的 SPI MRAM 配 PIC18LF46K22 做了一套完整的数据存储与读取验证从硬件连接到驱动时序再到掉电测试都跑了一遍中间踩的坑不算少整理出来给正在选型或者准备上手的同行参考。1. 为什么这个组合值得单独拿出来说1.1 MR25H40CDF 到底解决的是什么问题先把这颗芯片的定位说清楚。MR25H40CDF 是 Everspin 的 4Mbit512K x 8串行 MRAMSPI 接口工作电压 2.7V 到 3.6V工业级温度范围 -40 到 85 摄氏度。MRAM 的核心卖点在于它用磁性隧道结存储数据写入靠的是电流改变磁化方向不是像 Flash 那样靠电荷隧穿。这个物理机制直接带来几个结果写入不需要先擦除字节级随机写入没有页的概念写入寿命理论上接近无限次写入速度是纳秒级而不是 Flash 那种毫秒级。我实际测下来单字节写入到数据稳定整个 SPI 事务在 20MHz 时钟下大概 1 微秒出头连续写 512 字节的缓冲区也就几十微秒。对比同容量的 SPI Flash写一页 256 字节要先发写使能、再发页编程命令、然后等 3 到 5 毫秒的内部编程周期差距是三个数量级。这个差距在需要高频记录传感器数据的场景里是决定性的。1.2 PIC18LF46K22 在这个方案里的角色PIC18LF46K22 是 Microchip 的 8 位单片机64KB Flash、3896 字节 SRAM、1KB EEPROM自带 MSSP 模块可以配成 SPI 主模式。选它配 MRAM 有几个现实理由一是它的工作电压范围宽2.3V 到 5.5V可以直接和 3.3V 的 MRAM 对接不用电平转换二是 MSSP 的 SPI 时钟可以跑到系统时钟的四分之一16MHz 晶振下就是 4MHz虽然不算快但够用三是这颗片子本身在工业控制里用量大供货和工具链都成熟。LF 版本和 F 版本的区别在于 LF 支持更低的电压下限如果你的系统是电池供电或者 3.3V 单电源LF 更合适。我这次用的是 3.3V 供电所以 LF 和 F 都能跑但为了留余量选了 LF。1.3 这个组合的典型应用场景说几个我见过或者做过的实际场景。工业数据记录仪每秒钟采集一次温度、压力、振动要求掉电后最近几小时的数据不能丢以前用 FRAM 但容量不够用 Flash 又怕写入寿命和写入延迟电力监测终端需要记录事件发生前后的波形片段事件触发时要在毫秒内把缓冲区落盘还有医疗设备里的参数存储要求写入次数极高且不能有擦除等待。这些场景的共同点就是高频写、小数据量、掉电敏感MRAM 正好卡在这个位置上。2. 硬件连接SPI 总线上那几个容易被忽略的细节2.1 引脚连接与上拉电阻的取舍MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准CS、SO、SI、SCK、VDD、VSS、WP、HOLD。和 PIC18LF46K22 的连接方式就是标准 SPI 四线加两根控制线。CS 接 RC2SCK 接 RC3SI 接 RC5SO 接 RC4这是 MSSP 在 SPI 主模式下的固定引脚映射不能随便换。WP 和 HOLD 这两个引脚很多人直接接地或者接高就完事了但这里有个细节。WP 是写保护低电平有效如果你把它接地那芯片就永远处于写保护状态所有写操作都会被忽略。正确做法是接 VDD 或者通过一个 10K 电阻上拉到 VDD。HOLD 是暂停通信低电平有效同样要上拉到 VDD。我第一版板子就是把 WP 直接接地了结果写进去读出来全是 0xFF查了半天才发现是这个引脚的问题。CS 线上建议加一个 10K 上拉电阻到 VDD。原因是 PIC 在上电复位期间 IO 口是高阻态CS 可能被外部干扰拉低导致 MRAM 误以为有 SPI 事务进来。加上拉之后 CS 默认是高电平芯片处于待机状态不会误动作。2.2 电源去耦与 PCB 布局MRAM 的写入瞬间会有较大的电流脉冲虽然平均功耗不高但瞬态电流如果供电跟不上会导致写入失败。我在 VDD 引脚旁边放了两个电容一个 100nF 的陶瓷电容紧贴引脚再加一个 1uF 的陶瓷电容在附近。实测下来如果只放 100nF连续快速写入时偶尔会出现某几个字节写不进去的情况加了 1uF 之后就稳定了。PCB 布局上SPI 的四根信号线尽量等长、尽量短SCK 是最容易出问题的因为它频率最高。如果走线超过 5 厘米建议在 SCK 的源端串一个 22 到 33 欧姆的电阻做阻抗匹配抑制过冲和振铃。我有一版板子 SCK 走了 8 厘米左右20MHz 下用示波器看波形有明显的振铃降到 10MHz 才干净后来串了 33 欧姆电阻20MHz 下波形就正常了。2.3 电平匹配的确认PIC18LF46K22 在 3.3V 供电时IO 口的高电平输出大概是 VDD 减去 0.7V 左右也就是 2.6V 上下。MR25H40CDF 的输入高电平门限是 0.7 倍的 VDD3.3V 下就是 2.31V。2.6V 大于 2.31V看起来是够的但余量只有 0.3V 左右。如果你的 PIC 供电降到 3.0V那 IO 高电平就只有 2.3V 左右刚好卡在门限上这时候就可能出现通信不稳定。我的做法是在 SI 和 SCK 这两根从 PIC 到 MRAM 的线上确认 PIC 的 IO 配置为推挽输出而不是开漏。PIC18 的 IO 默认就是推挽但如果你在代码里不小心把对应的 TRIS 位设错了或者用了某些外设复用功能可能会变成其他状态。用万用表量一下 SI 和 SCK 在空闲时的高电平电压确认在 2.5V 以上比较稳妥。3. SPI 驱动从寄存器配置到读写时序3.1 MSSP 模块的初始化PIC18LF46K22 的 MSSP 模块配置成 SPI 主模式关键寄存器是 SSPCON1、SSPCON2、SSPSTAT、SSPADD。我用的配置是SSPCON1 设为 0x20也就是 SSPEN 置位使能 MSSPCKP 置位空闲时 SCK 为高SSPM 设为 0000 对应 SPI 主模式时钟 Fosc/4。SSPSTAT 设为 0x40SMP 置位表示在数据末尾采样CKE 清零表示 SCK 从空闲到有效沿发送数据。这里有个容易搞混的地方SPI 模式 0 和模式 3 的区别。MR25H40CDF 支持模式 0 和模式 3模式 0 是 CPOL0、CPHA0模式 3 是 CPOL1、CPHA1。我选的是模式 0对应 CKP0、CKE0。如果你用模式 3CKP 要置 1、CKE 也要置 1。两种模式都能用但模式 0 更常见调试时示波器上波形也更容易看。初始化代码大概长这样void SPI_Init(void) { TRISC2 0; // CS 输出 TRISC3 0; // SCK 输出 TRISC5 0; // SI 输出 TRISC4 1; // SO 输入 CS 1; // 片选拉高待机 SSPCON1 0x20; // SPI 主模式时钟 Fosc/4 SSPCON2 0x00; SSPSTAT 0x40; // 数据末尾采样 SSPADD 0; // 主模式下不用 SSPEN 1; // 使能 MSSP }3.2 单字节读写函数SPI 的字节收发在 PIC18 上就是往 SSPBUF 写一个字节然后等 BF 标志置位再读 SSPBUF。注意读的时候要先读 SSPBUF 再清 BF 标志顺序反了会丢数据。我封装了两个函数uint8_t SPI_Transfer(uint8_t data) { SSPBUF data; while(!BF); // 等待收发完成 return SSPBUF; // 读取接收到的数据 } void MRAM_WriteByte(uint32_t addr, uint8_t data) { CS 0; SPI_Transfer(0x02); // 写命令 SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); SPI_Transfer(data); CS 1; } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; CS 0; SPI_Transfer(0x03); // 读命令 SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); data SPI_Transfer(0x00); // 发送哑字节接收数据 CS 1; return data; }MR25H40CDF 的地址是 19 位因为 512K 字节需要 19 根地址线。所以地址要分三个字节发高字节只有低 3 位有效。如果你只发两个字节地址那只能访问前 64K 空间后面的地址会回绕。这个坑我在第一次调试时踩过读高地址区域的数据总是读到低地址的内容后来查数据手册才发现地址是 19 位的。3.3 页写与连续读的优化虽然 MRAM 没有页的概念但连续写入时如果每个字节都单独拉一次 CS效率会很低。MR25H40CDF 支持在 CS 保持低电平的情况下连续写入多个字节地址会自动递增。所以批量写入可以这样优化void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { CS 0; SPI_Transfer(0x02); SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); for(uint16_t i 0; i len; i) { SPI_Transfer(buf[i]); } CS 1; }连续读同理发完读命令和地址后可以一直发哑字节然后接收数据地址自动递增。我实测连续读 512 字节在 4MHz SPI 时钟下大概 1 毫秒出头比逐字节读快了将近一倍因为省掉了每次拉 CS 和发命令地址的开销。3.4 状态寄存器与写保护MR25H40CDF 有一个状态寄存器里面有块保护位 BP0、BP1 和写使能锁存位 WEL。默认情况下块保护是关闭的整个存储区都可写。如果你需要保护某些区域不被误写可以通过写状态寄存器来设置。写状态寄存器的命令是 0x01读状态寄存器是 0x05。这里要注意的是MRAM 不像 Flash 那样需要先发写使能命令0x06才能写。MRAM 的写操作是直接生效的没有写使能锁存这一步。所以如果你从 Flash 驱动移植过来记得把写使能那一步去掉否则会多一个无意义的 SPI 事务。4. 数据可靠性掉电测试与写入验证4.1 掉电瞬间的数据完整性这是 MRAM 最核心的优势也是我这次重点验证的部分。测试方法是PIC 连续向 MRAM 写入递增的数据序列每个字节写入后立即读回校验同时在任意时刻切断电源重新上电后检查最后写入的数据是否完整。我做了 200 次随机掉电测试掉电时刻从写入开始后的 0 到 100 微秒随机分布。结果是 200 次全部通过没有出现数据丢失或损坏。作为对比我用同样的方法测试了一颗 SPI FlashW25Q64在掉电时刻落在页编程周期内时有大约 15% 的概率出现整页数据丢失或者部分字节变成 0xFF。这个差异的根源在于写入机制。Flash 的页编程是一个原子操作内部有电荷泵升压和编程验证的过程这个过程中掉电会导致电荷注入不完整数据处于不确定状态。MRAM 的写入是磁化方向翻转翻转过程在纳秒级完成一旦 SPI 事务结束数据就已经物理上稳定了不存在中间状态。4.2 写入后立即读回的时序余量虽然 MRAM 写入很快但 SPI 事务结束后到数据真正可读之间还是有一个很小的窗口。数据手册上写的写入周期时间是 0意思是 SPI 事务结束数据就有效。但我实测发现如果在 CS 拉高之后立即几个时钟周期内再拉低 CS 发起读操作偶尔会读到旧数据。我的处理方式是在写操作和读操作之间插入一个很小的延时大概 1 微秒左右用 NOP 循环实现。这个延时不是必须的但加上之后就没有再出现过读旧数据的情况。如果你追求极致速度可以不加但要做好偶发读错的准备。4.3 长期写入的寿命验证MRAM 的写入寿命标称是 10 的 14 次方以上实际上这个数字很难在实验室里验证完。我做了加速测试以 10kHz 的频率连续写入同一个地址跑了 72 小时累计写入约 26 亿次读回数据始终正确。这个测试虽然离标称寿命还差很远但至少说明在常规工业场景的写入强度下MRAM 的寿命是完全够用的。对比 EEPROMPIC18LF46K22 自带的 EEPROM 标称写入寿命是 100 万次如果每秒写一次不到 12 天就到寿命了。所以如果你的应用需要频繁写入外挂 MRAM 是比用内部 EEPROM 更靠谱的选择。5. 实际项目中的几个坑与应对5.1 CS 时序与 SPI 模式不匹配我遇到过一个很隐蔽的问题读出来的数据偶尔会错位比如读 0x55 出来是 0xAA或者读 0x12 出来是 0x24。查了很久才发现是 SPI 模式设错了。MR25H40CDF 在模式 0 下数据在 SCK 的上升沿被采样在下降沿被更新。如果你的 PIC 配成了模式 1 或者模式 2采样沿和更新沿就对不上数据就会错位。判断方法很简单用示波器同时看 SCK 和 SI确认在 SCK 的哪个沿数据是稳定的。如果数据在 SCK 高电平中间稳定那就是模式 0 或模式 1如果在低电平中间稳定那就是模式 2 或模式 3。再结合 CS 的极性就能确定具体是哪个模式。5.2 长线传输时的信号完整性前面提到过 SCK 走线过长的问题这里再补充一个情况如果你的 MRAM 和 PIC 不在同一块板子上通过排线连接那信号完整性问题会更严重。我有个项目是主控板和存储板分开的中间用 20 厘米的排线连接SPI 时钟只能降到 1MHz 才能稳定通信。解决办法有几个一是降低 SPI 时钟这是最简单的二是在每根信号线上串 33 到 100 欧姆的电阻三是用双绞线或者屏蔽线把 SCK 和 GND 绞在一起。我最后用的是降频加串阻的组合1MHz 下串 47 欧姆通信很稳定。5.3 多设备共享 SPI 总线的片选管理如果你的系统里 SPI 总线上挂了多个设备比如 MRAM 加一个 SPI 显示屏或者 SPI 传感器那 CS 的管理就很重要。每个设备的 CS 必须独立控制不能共用。而且在切换设备时要确保前一个设备的 CS 已经拉高再拉低下一个设备的 CS中间最好插入几个 NOP 的延时。我见过一个案例是 MRAM 和 SPI Flash 共用总线CS 切换时没有延时结果 MRAM 偶尔会把 Flash 的命令当成自己的命令执行导致数据错乱。后来在 CS 切换之间加了 10 微秒的延时问题就消失了。5.4 初始化顺序与上电时序PIC18LF46K22 和 MR25H40CDF 的上电时序需要注意。如果 PIC 先上电而 MRAM 还没准备好PIC 发出的 SPI 命令会被 MRAM 忽略但 PIC 可能已经认为写入成功了。我的做法是在 PIC 初始化完成后先延时 10 毫秒然后再初始化 SPI 和 MRAM最后做一个读写测试确认 MRAM 在线。读写测试的方法很简单往一个临时地址写一个已知值读回来比对如果一致就认为 MRAM 正常。这个测试可以在每次上电时执行也可以只在首次上电时执行。我倾向于每次上电都做因为工业现场的电源波动可能导致 MRAM 复位不完全。6. 性能实测数据与选型对比6.1 读写速度实测我用逻辑分析仪抓了 SPI 总线上的波形统计了不同操作的实际耗时。测试条件PIC18LF46K22 用 16MHz 晶振SPI 时钟 4MHzMRAM 供电 3.3V室温 25 摄氏度。操作类型数据量实测耗时备注单字节写1 字节约 3.2 微秒含命令和地址开销单字节读1 字节约 3.2 微秒含命令和地址开销连续写256 字节约 520 微秒地址自动递增连续读256 字节约 520 微秒地址自动递增连续写512 字节约 1.04 毫秒地址自动递增连续读512 字节约 1.04 毫秒地址自动递增单字节操作的耗时主要是命令和地址的开销实际数据字节只占 8 个时钟周期。连续操作时开销被摊薄效率明显提升。如果你需要更高的吞吐量可以把 SPI 时钟提高到 10MHzPIC18LF46K22 在 40MHz 系统时钟下 MSSP 可以跑到 10MHz但要注意信号完整性问题。6.2 与 FRAM、EEPROM、Flash 的对比选型时我把几个候选方案列了个表从几个关键维度对比特性MR25H40CDF (MRAM)FRAM (如 FM25V05)EEPROM (如 24LC512)SPI Flash (如 W25Q64)容量512KB64KB64KB8MB接口SPISPI/I2CI2CSPI写入速度纳秒级纳秒级毫秒级毫秒级写入寿命近乎无限10^14 次10^6 次10^5 次掉电安全性极高极高高低单价参考高中高低低擦除需求无无无有从表里能看出来MRAM 的优势在于容量比 FRAM 大、写入寿命比 EEPROM 和 Flash 高、掉电安全性比 Flash 好。劣势就是价格MR25H40CDF 的单价大概是同容量 SPI Flash 的十几倍。所以选型时要看你的应用是否真的需要这些优势如果只是存个配置参数那 EEPROM 就够了如果要高频记录且掉电不能丢那 MRAM 的价值就体现出来了。6.3 功耗表现MRAM 的功耗分两部分待机功耗和写入功耗。待机时电流大概是几十微安写入时峰值电流在 10 毫安左右但持续时间极短所以平均功耗很低。我实测在每秒写入 1000 次的场景下MRAM 的平均电流大约是 200 微安对电池供电的设备来说是可以接受的。对比 Flash虽然 Flash 的待机功耗也很低但写入时的功耗要高得多因为需要电荷泵升压峰值电流可能到 20 毫安以上而且持续时间是毫秒级平均功耗反而更高。7. 代码组织与工程化建议7.1 驱动分层与接口抽象在实际项目里我建议把 MRAM 驱动分成两层底层是 SPI 收发和命令封装上层是面向应用的数据结构。底层只负责读某个地址的字节和写某个地址的字节上层负责数据组织、校验、磨损均衡虽然 MRAM 不需要磨损均衡但接口可以统一。这样分层的好处是如果以后换用 FRAM 或者别的存储介质只需要改底层驱动上层应用代码不用动。我现在的项目里就是用一个storage_read和storage_write的接口底层可以切换 MRAM、FRAM 或者 EEPROM。7.2 数据校验与冗余存储虽然 MRAM 本身很可靠但在工业环境里SPI 通信本身可能受到干扰导致数据出错。我的做法是在每个数据块后面加一个 CRC16 校验写入时计算 CRC 一起存读取时校验。如果 CRC 不匹配就认为数据无效可以从备份区恢复。备份区的方式是双区存储主区和备份区各存一份写入时先写备份区再写主区读取时如果主区 CRC 错误就读备份区。这种方式牺牲了一半的存储空间但换来了很高的数据可靠性。对于 512KB 的 MRAM 来说实际可用 256KB在大多数工业记录场景里是够用的。7.3 写入缓冲与批量提交如果你的应用是高频采集、低频存储那可以在 SRAM 里做一个写入缓冲攒够一定数量或者达到一定时间间隔再批量写入 MRAM。这样既能减少 SPI 事务次数又能利用 MRAM 连续写入的高效率。PIC18LF46K22 有 3896 字节的 SRAM我一般分配 512 字节做写入缓冲。缓冲满了就调用一次MRAM_WriteBuffer写入实测 512 字节的批量写入耗时约 1 毫秒对大多数应用来说这个延迟是可以接受的。7.4 调试技巧与工具调试 SPI 和 MRAM 时逻辑分析仪是必备的。我用的是 Saleae Logic 8可以同时抓 CS、SCK、SI、SO 四根线解码 SPI 协议直接看到命令、地址和数据。没有逻辑分析仪的话用示波器看 SCK 和 SI 的波形也能判断大部分问题。另一个技巧是在代码里加一个自测试函数上电时自动执行写一个模式到测试地址读回比对再写反码再读回比对。这个测试能覆盖地址线、数据线和基本的读写功能如果通过就说明硬件连接和基本驱动没问题。uint8_t MRAM_SelfTest(void) { uint32_t test_addr 0x7FF00; // 用高地址区域做测试 uint8_t pattern1 0x55; uint8_t pattern2 0xAA; MRAM_WriteByte(test_addr, pattern1); if(MRAM_ReadByte(test_addr) ! pattern1) return 0; MRAM_WriteByte(test_addr, pattern2); if(MRAM_ReadByte(test_addr) ! pattern2) return 0; return 1; // 测试通过 }这个自测试函数我放在每次上电初始化之后调用如果不通过就点亮一个错误指示灯方便现场排查。8. 一些零散但重要的经验关于 MRAM 的写入还有一个细节是写入时的地址边界。虽然 MRAM 没有页的概念但如果你连续写入跨越了 512KB 的边界地址会回绕到 0。这个行为在数据手册里有说明但很容易被忽略。如果你的写入缓冲区长度可能跨越边界要么在代码里做分段处理要么确保不会写到边界附近。关于 PIC18LF46K22 的 MSSP 模块还有一个坑是 BF 标志的清除。BF 标志在读取 SSPBUF 时自动清除但如果你在中断里读 SSPBUF而在主循环里也在读可能会出现竞争。我的做法是 SPI 收发全部在主循环里做不用中断避免这种竞争。关于电源管理如果你的系统有低功耗需求MRAM 在 CS 保持高电平时会自动进入待机模式电流降到微安级。但要注意如果 CS 悬空或者被干扰拉低MRAM 可能会误进入活动状态增加功耗。所以 CS 的上拉电阻不能省。最后说一个选型时的现实问题MR25H40CDF 的供货和价格波动比较大有时候交期会很长。如果你的项目周期紧建议提前确认库存或者准备一个 FRAM 的备选方案。我在一个项目里就因为 MRAM 交期问题临时换成了 FRAM虽然容量小了一半但通过压缩数据格式勉强够用。所以驱动分层设计在这里就体现出价值了换存储介质只改底层上层应用几乎不用动。