
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 这颗怪胎做工业嵌入式的人都有一个共同的痛掉电丢数据。传统方案无非是 EEPROM 加磨损均衡、铁电存储器 FRAM、或者干脆挂一片 SPI Flash 加文件系统。EEPROM 写入慢、寿命有限擦写次数撑死百万级FRAM 速度快、寿命长但容量小、价格贵而且很多型号已经停产或者交期感人SPI Flash 容量大便宜但写入前必须擦除整个扇区掉电瞬间如果正在擦写整个扇区数据可能直接报废这在工业现场是致命的。MR25H40CDF 这颗芯片属于磁阻随机存储器MRAM它的定位恰好卡在中间容量 4Mbit512KBSPI 接口读写速度接近 SRAM擦写寿命理论上无限掉电数据保持至少 20 年而且不需要像 Flash 那样先擦后写。我第一次接触它是在一个电力监测终端项目上现场要求每 100ms 记录一次三相电压电流的瞬时值掉电后最近 10 分钟的数据不能丢。用 FRAM 容量不够用 Flash 怕掉电损坏最后选了 MRAM实测下来确实稳。PIC18F46K20 是 Microchip 的一款经典 8 位单片机64KB Flash、3936 字节 RAM、1KB EEPROM自带 MSSP 模块支持 SPI 和 I2C。它的 SPI 最高时钟可以跑到 10MHz系统时钟 64MHz 时用来驱动 MR25H40CDF 绰绰有余。选这对组合的核心逻辑是PIC18F46K20 在工业现场存量极大开发工具链成熟资料多而 MR25H40CDF 解决了数据可靠性的问题两者通过 SPI 连接硬件设计简单软件驱动也不复杂。1.2 硬件连接与引脚分配MR25H40CDF 是 8 引脚 SOIC 封装引脚定义如下引脚编号名称功能说明1CS片选低电平有效2SO数据输出MISO3WP写保护低电平有效4VSS地5SI数据输入MOSI6SCK时钟7HOLD保持低电平有效8VDD电源2.7V~3.6V和 PIC18F46K20 连接时我一般这样分配RC3SCK接 MR25H40CDF 的 SCKRC4SDI接 MR25H40CDF 的 SORC5SDO接 MR25H40CDF 的 SIRA5 或者任意普通 IO 接 CSWP 和 HOLD 直接上拉到 VDD除非你需要硬件写保护功能这里有个细节PIC18F46K20 的 MSSP 模块在 SPI 主模式下SCK 时钟极性CKP和时钟边沿CKE需要和 MR25H40CDF 的时序匹配。MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我一般用模式 0即空闲时 SCK 为低电平数据在 SCK 上升沿采样。配置寄存器 SSPCON1 时CKP0CKE0对于模式 0CKE 在 SSPSTAT 寄存器中设置。注意MR25H40CDF 的 WP 引脚如果悬空内部有弱上拉但工业环境建议外部加 10K 上拉到 VDD避免干扰导致误写保护。1.3 电源与去耦设计MR25H40CDF 的工作电压范围是 2.7V 到 3.6V典型值 3.3V。PIC18F46K20 可以工作在 1.8V 到 5.5V但为了和 MRAM 电平匹配整个系统统一用 3.3V 供电最省事。去耦电容方面每颗芯片的 VDD 和 VSS 之间必须放一个 0.1uF 的陶瓷电容位置尽量靠近引脚。如果 SPI 时钟跑到 10MHz建议再并联一个 1uF 的钽电容或者 MLCC应对瞬态电流。我在一个电机控制项目上踩过坑MRAM 和单片机共用 3.3V LDO但 LDO 距离 MRAM 有 5cm 远走线又细结果 SPI 读写偶尔出错。后来在 MRAM 的 VDD 引脚旁边直接加了一个 0.1uF 加一个 10uF 的电容问题消失。所以别省这几个电容工业现场电磁环境复杂电源干净比什么都重要。2. MR25H40CDF 的核心机制与操作命令2.1 MRAM 的存储原理与读写特性MRAM 的全称是 Magnetoresistive Random Access Memory它用磁性隧道结MTJ来存储数据。每个存储单元由一个固定磁层、一个自由磁层和一个隧道势垒层组成。自由磁层的磁化方向可以通过电流产生的磁场来改变当自由层和固定层磁化方向平行时电阻低代表逻辑 0反平行时电阻高代表逻辑 1。读取时通过测量隧道结的电阻来判断数据。这种机制带来的好处很直接写入不需要像 Flash 那样先充电再擦除而是直接改变磁化方向所以写入速度快而且没有擦除周期限制。MR25H40CDF 的写入时间典型值是 35ns读取时间更短SPI 接口可以跑到 40MHz但 PIC18F46K20 的 MSSP 最高 10MHz所以实际受限于单片机。不过 MRAM 也有它的脾气写入时的电流较大如果电源不稳写入可能失败。另外虽然擦写寿命理论无限但实际使用中如果频繁写入同一地址仍然建议做一定的磨损分散虽然不像 Flash 那么敏感但养成好习惯总没错。2.2 SPI 命令集详解MR25H40CDF 支持标准的 SPI 命令集常用的命令如下表命令名称命令码功能说明时钟字节数WREN0x06写使能1WRDI0x04写禁止1RDSR0x05读状态寄存器11WRSR0x01写状态寄存器11READ0x03读数据13NWRITE0x02写数据13NRDID0x9F读设备 ID13状态寄存器的 bit0 是 WEL写使能锁存bit1 是 BP0bit2 是 BP1bit7 是 SRWD。写操作之前必须先发 WREN 命令否则写操作会被忽略。这一点和 EEPROM 类似但 MRAM 不需要等待写完成因为写入是即时的发完 WRITE 命令后数据就已经写进去了不需要轮询状态寄存器。读设备 ID 的命令 RDID 返回 3 个字节MR25H40CDF 的 ID 是 0x18 0x25 0x40厂商代码 0x18 是 Everspin后面两个字节是型号。这个命令可以用来检测芯片是否焊接正常我在产线测试工装上经常用这个命令做快速检测。2.3 写保护机制与状态寄存器配置MR25H40CDF 的写保护分硬件和软件两层。硬件层面是 WP 引脚当 WP 为低电平时状态寄存器的 SRWD 位和 BP1、BP0 位共同决定保护范围。软件层面是通过 WRSR 命令写状态寄存器来设置块保护。状态寄存器的 BP1 和 BP0 组合如下BP1BP0保护范围00无保护01保护高 1/4 地址空间10保护高 1/2 地址空间11保护整个地址空间在工业应用中我通常会把关键参数区放在低地址然后设置 BP10BP01保护高 1/4 空间防止程序跑飞时误写。但要注意一旦设置了块保护写入被保护的地址时 WREN 命令会失效WEL 位不会被置 1写操作自然失败。提示如果你不需要写保护WP 引脚直接上拉到 VDD状态寄存器保持默认值 0x00 即可。但建议在初始化时读一次状态寄存器确认芯片处于预期状态。3. PIC18F46K20 的 SPI 驱动实现3.1 MSSP 模块初始化PIC18F46K20 的 MSSP 模块配置 SPI 主模式需要设置以下几个寄存器SSPCON1bit5 是 SSPEN置 1 使能 MSSPbit4 是 CKP设置时钟极性bit3-0 是 SSPM3:SSPM0设置 SPI 主模式0010 表示 SPI 主模式时钟 Fosc/640000 表示 SPI 主模式时钟 Fosc/4。SSPSTATbit7 是 SMP设置采样相位bit6 是 CKE设置时钟边沿。SSPADD在 SPI 主模式下不用设置但在 I2C 模式下用来设置波特率。TRISC设置 SCK、SDO 为输出SDI 为输入。我一般用 Fosc/4 作为 SPI 时钟如果系统时钟是 64MHzSPI 时钟就是 16MHz但 PIC18F46K20 的 MSSP 在 64MHz 下最高只能到 10MHz所以实际上 Fosc/4 会超频。稳妥起见用 Fosc/16 或者 Fosc/64。假设系统时钟 64MHzFosc/64 得到 1MHz对于 MRAM 来说完全够用因为瓶颈在单片机的处理速度不在 SPI 时钟。初始化代码示例void SPI_Init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出 TRISAbits.TRISA5 0; // CS 输出 CS 1; // 片选拉高取消选中 SSPCON1 0x20; // SSPEN1, CKP0, SPI 主模式 Fosc/64 SSPSTAT 0x00; // SMP0, CKE0, 模式 0 PIR1bits.SSPIF 0; // 清除中断标志 }3.2 字节收发函数SPI 是全双工发送一个字节的同时会接收一个字节。PIC18F46K20 的 MSSP 模块有 SSPBUF 寄存器写入数据后硬件自动开始发送同时接收数据到 SSPBUF。判断发送完成看 SSPIF 标志位。uint8_t SPI_Transfer(uint8_t data) { SSPBUF data; while(!PIR1bits.SSPIF); PIR1bits.SSPIF 0; return SSPBUF; }这个函数是阻塞式的在实时性要求高的场合可以改成中断方式但 MRAM 读写速度很快阻塞式完全够用。注意读 SSPBUF 之前一定要等 SSPIF 置位否则读到的可能是旧数据。3.3 MRAM 读写函数封装基于 SPI 传输函数封装 MRAM 的读写操作void MRAM_WriteEnable(void) { CS 0; SPI_Transfer(0x06); // WREN CS 1; } void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); CS 0; SPI_Transfer(0x02); // WRITE 命令 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; } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { CS 0; SPI_Transfer(0x03); // READ 命令 SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); for(uint16_t i 0; i len; i) { buf[i] SPI_Transfer(0x00); // 发送哑字节接收数据 } CS 1; }这里有个细节MRAM 的地址是 19 位512KB 2^19所以需要 3 个字节来传输地址但最高字节只有低 3 位有效。发送地址时高字节的 bit7 到 bit3 会被忽略所以直接发 0 也没问题。注意CS 拉低到发送命令之间不要有额外延时MR25H40CDF 要求 CS 下降沿后至少 5ns 才能开始 SCKPIC 的 GPIO 翻转速度足够快不需要额外处理。4. 数据存储策略与可靠性设计4.1 数据结构设计在工业应用中存储的数据通常分几类实时数据、历史数据、配置参数、故障记录。我一般把 MRAM 的 512KB 空间这样划分地址范围用途大小说明0x00000 - 0x00FFF系统配置区4KB设备参数、校准系数0x01000 - 0x01FFF故障记录区4KB循环存储每条记录 64 字节0x02000 - 0x7FFFF历史数据区504KB循环存储带时间戳配置区我一般存两份互为备份每份 2KB写入时先写备份区再写主区读取时先读主区如果校验失败再读备份区。校验用 CRC16 或者简单的累加和CRC16 更可靠但计算量大一点PIC18F46K20 跑 CRC16 大概几十个微秒完全可以接受。4.2 掉电保护与写入原子性MRAM 的写入是即时完成的不像 Flash 需要擦除周期所以掉电保护相对简单。但有一个问题如果你要更新一个数据结构比如先写数据再写校验和如果在写完数据后、写校验和之前掉电下次上电时校验和会对不上。解决办法是用双缓冲或者影子区。我的做法是在配置区维护一个 4 字节的序列号每次更新配置时序列号加 1然后先写数据到备份区再写序列号到备份区最后把备份区的内容复制到主区。读取时比较主区和备份区的序列号取序列号大的那个。这样即使掉电发生在任何一步至少有一个区的数据是完整的。对于历史数据区我用循环队列的方式每个记录 64 字节前 4 字节是记录头包含时间戳和 CRC后面 60 字节是数据。写指针和读指针也存在 MRAM 里每次写入后更新写指针。掉电后重新上电从写指针位置继续写读指针不变这样最多丢失最后一条正在写的记录不会影响已有数据。4.3 错误检测与恢复MRAM 虽然可靠但 SPI 通信可能受干扰出错。我在每个数据块后面加 2 字节 CRC16读取时校验。如果 CRC 失败重读一次如果还失败标记该块为坏块跳过。对于配置区如果主区和备份区都坏了那就用默认参数初始化并记录一条故障日志。另外MRAM 的状态寄存器可以读出来检查 WEL 位如果 WREN 命令发了但 WEL 没置 1说明写保护生效了或者芯片有问题。我在初始化时会做一次自检写一个测试模式到某个地址读回来比较如果一致就认为芯片正常。这个自检地址我一般选在历史数据区的末尾不影响正常使用。5. 实操调试与问题排查5.1 硬件调试要点第一次调试 SPI 设备我建议先用示波器或者逻辑分析仪抓波形。重点看几个地方CS 下降沿到第一个 SCK 上升沿的时间、SCK 的频率和占空比、MOSI 和 MISO 的数据建立保持时间。MR25H40CDF 的时序要求是 SCK 高电平和低电平时间至少 10ns数据建立时间 5ns保持时间 5ns。PIC18F46K20 在 64MHz 下Fosc/64 的 SPI 时钟周期是 1us完全满足。如果读回来的数据全是 0xFF 或者 0x00先检查 CS 是否正常拉低再检查 MISO 是否接反。我遇到过好几次是 MOSI 和 MISO 接反了因为原理图上标的是 SDI 和 SDO实际焊接时容易搞混。PIC 的 SDI 是输入接 MRAM 的 SOPIC 的 SDO 是输出接 MRAM 的 SI。5.2 常见问题速查表现象可能原因排查方法解决方案读回全 0xFFMISO 未连接或接反检查 MISO 走线重新焊接或交换 MOSI/MISO读回全 0x00CS 未拉低或 SCK 无输出示波器测 CS 和 SCK检查 GPIO 配置和 MSSP 使能写入后读回不变WREN 未发送或写保护生效读状态寄存器 WEL 位发送 WREN检查 WP 引脚数据偶尔出错电源干扰或 SPI 时钟太快降低 SPI 时钟加去耦电容加 0.1uF 和 10uF 电容降速设备 ID 读不对芯片损坏或型号不对读 RDID 命令更换芯片确认型号5.3 实操心得与避坑技巧第一个坑MRAM 的 CS 拉低后如果长时间不发时钟芯片可能会进入低功耗状态但 MR25H40CDF 没有这个问题CS 拉低后可以一直保持不会超时。但为了省电我一般在操作完成后立即拉高 CS。第二个坑PIC18F46K20 的 MSSP 模块在发送数据时如果连续写 SSPBUF 而不等 SSPIF会导致数据覆盖。所以 SPI_Transfer 函数里的 while 等待是必须的不能省。第三个坑MRAM 的写入不需要等待但如果你在写入后立即读取同一个地址读到的应该是新数据。我实测过写入后立即读取数据正确。但如果你用 DMA 或者中断方式要注意时序。第四个坑工业现场的温度范围宽MR25H40CDF 的工业级版本是 -40°C 到 85°C商业级是 0°C 到 70°C。如果你的设备要在户外使用一定要选工业级。我有一次用商业级芯片做户外项目冬天低温下数据读写不稳定后来换了工业级就好了。第五个坑MRAM 的写入电流较大如果系统里有大功率设备同时工作电源波动可能导致写入失败。我的做法是在 MRAM 的 VDD 引脚旁边放一个 100uF 的电解电容作为能量缓冲。另外写入操作尽量避开大功率设备启动的时刻比如电机启动时不要写 MRAM。6. 性能实测与优化建议6.1 读写速度实测我用 PIC18F46K20 在 64MHz 系统时钟下SPI 时钟设为 Fosc/641MHz实测连续写入 512 字节耗时约 4.2ms读取 512 字节耗时约 4.1ms。如果 SPI 时钟提高到 Fosc/164MHz写入耗时约 1.1ms读取约 1.0ms。再高到 Fosc/416MHz时PIC18F46K20 的 MSSP 模块开始不稳定偶尔丢字节所以实际可用最高时钟是 4MHz 左右。这个速度对于工业数据记录来说完全够用。假设每 100ms 记录一次每次 64 字节写入耗时 0.14ms4MHz 时钟占空比不到 0.2%对系统实时性几乎没有影响。6.2 功耗考量MR25H40CDF 的待机电流典型值 1mA写入电流 15mA读取电流 5mA。PIC18F46K20 在 64MHz 下运行电流约 15mA。如果设备是电池供电需要优化功耗。我的做法是不写数据时把 MRAM 的 CS 拉高让它进入待机模式PIC 在空闲时进入 Sleep 模式用定时器唤醒。这样平均功耗可以降到 2mA 以下。6.3 长期可靠性建议MRAM 虽然擦写寿命无限但长期高温工作可能会影响磁性材料的稳定性。Everspin 的 datasheet 里写了 20 年数据保持但这是在 85°C 下的保证值。如果你的设备工作在更高温度建议做加速老化测试。另外定期做一次全盘 CRC 校验发现坏块及时标记可以提前发现潜在问题。我在一个轨道交通项目上用了 5 年 MRAM每天写入约 10 万次至今没有出现数据丢失。所以只要电源设计和时序控制到位MRAM 的可靠性是值得信赖的。7. 扩展思路与替代方案7.1 更大容量或更小封装的选型如果 512KB 不够用Everspin 有 MR25H40 的升级型号 MR25H256256KB和 MR25H1M1MB接口兼容只是地址位数不同。如果空间受限可以考虑 DFN 封装的版本但焊接难度大需要回流焊。7.2 与 FRAM 和 NVSRAM 的对比FRAM 的优势是功耗更低写入电流只有 MRAM 的十分之一但容量小最大 2Mbit而且价格贵。NVSRAM 是 SRAM 加电池容量大速度快但电池寿命有限工业现场换电池麻烦。MRAM 在容量、速度、可靠性、成本之间取得了比较好的平衡适合中等容量、高可靠性的场景。7.3 软件层面的优化如果你用 RTOS可以把 MRAM 驱动封装成设备文件通过文件系统接口访问。但要注意文件系统本身有开销对于小数据量频繁写入的场景直接操作 MRAM 更高效。另外可以用 MRAM 做日志文件系统把写入操作顺序化提高掉电安全性。我个人在实际操作中的体会是MRAM 这颗芯片用起来比 Flash 省心但前提是硬件设计要到位电源要干净SPI 时序要匹配。软件层面写使能和状态检查不能省虽然 MRAM 写入快但该走的流程还是要走。最后再分享一个小技巧如果你不确定 MRAM 是否焊接正常先读设备 ID如果 ID 正确基本可以确定 SPI 通信没问题剩下的就是软件逻辑的事了。