
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 PIC32工业现场的数据存储有个很尴尬的处境用 EEPROM 吧写入速度慢得让人着急擦写次数也就百万次级别高频记录的场景下没几年就写废了用 SRAM 加后备电池吧电池在高温高湿的车间里撑不了太久换电池还得停机用 NOR Flash 吧写之前得先擦除整个扇区掉电瞬间如果正好在擦写数据就可能丢得莫名其妙。我前几年做一个电力监测终端现场环境温度夏天能到 70 度用 FRAM 方案成本压不下来用 Flash 方案又总在掉电时刻出问题折腾了好几轮才把目光转向 MRAM。MR25H40CDF 这颗芯片是 Everspin 家的 4Mbit 串行 MRAM走 SPI 接口最高 40MHz 时钟。它最吸引我的地方在于写入不需要擦除、字节级随机写、写入次数近乎无限、掉电数据不丢而且没有 Flash 那种写前擦除的等待周期。说白了它把 SRAM 的写入体验和 Flash 的非易失性揉到了一起。对于工业设备里那种“每隔几毫秒记录一次状态、掉电时还要把最后一批数据完整落盘”的需求这颗芯片几乎是量身定做的。主控这边选的是 PIC32MZ1024EFF144Microchip 家的 32 位 MIPS 核心 MCU1MB Flash、512KB RAM带多个 SPI 外设主频能跑到 200MHz。选它有几个现实理由一是它的 SPI 模块支持 8/16/32 位数据宽度配合 DMA 做大批量 MRAM 读写时 CPU 占用很低二是工业级温度范围-40 到 85 度甚至 105 度都有对应型号三是 Microchip 的 Harmony 框架虽然上手有点门槛但底层驱动写得比较扎实SPI 时序配置灵活。这套组合我在两个项目里实际用过一个是用电信息采集一个是运动控制器的参数存储稳定性都经得起考验。1.2 这套方案到底解决了什么问题把 MR25H40CDF 和 PIC32MZ1024EFF144 搭在一起核心解决的是三类问题。第一类是高频小数据量的持久化记录。比如工业设备每 10ms 采集一次温度、压力、电流需要把这些数据带时间戳存下来设备断电后重新上电还能接着读。传统方案要么用铁电存储器容量小、贵要么用 Flash擦写寿命和掉电风险MRAM 在这里的优势是写入无延迟、无擦除、寿命几乎无限。第二类是掉电瞬间的关键数据保护。工业现场断电往往不是优雅关机而是直接拉闸。这时候系统需要在检测到掉电后的几毫秒内把当前的关键状态、计数器、故障码写进非易失存储。MRAM 的写入是真正的即时完成不像 Flash 需要几十毫秒的页编程时间也不像 EEPROM 需要几毫秒的写周期。第三类是频繁参数修改的场景。比如运动控制器的 PID 参数、设备的校准系数工程师在现场调试时会反复修改。如果用 EEPROM改个几百次就开始担心寿命用 MRAM 就完全不用管这个随便改。注意MRAM 虽然写入寿命长但也不是完全没有限制。MR25H40CDF 的数据保持年限在 20 年以上但前提是工作温度不要长期超过 85 度。如果应用场景是发动机舱附近或者户外暴晒的机柜得留足温度余量。1.3 方案对比为什么不是别的组合在确定这个方案之前我对比过几种常见的搭配列个表更直观。方案组合写入速度擦写寿命掉电风险成本适用场景PIC32 EEPROM慢ms级100万次中低低频参数存储PIC32 NOR Flash中需擦除10万次高低固件存储、日志PIC32 FRAM快万亿次低高高频记录PIC32 MRAM快近乎无限低中高工业高频记录PIC32 SRAM电池最快无限依赖电池中短时数据保持从表里能看出来MRAM 在写入速度和寿命上接近 FRAM但成本比 FRAM 低不少容量也更容易做大。跟 SRAM 加电池比它不需要维护电池长期可靠性更好。所以在工业嵌入式这个场景里MRAM 是一个平衡得比较好的选择。2. 硬件设计与 SPI 接口要点2.1 MR25H40CDF 的引脚与电气特性MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准CS、SO、SI、SCK 四个 SPI 信号加上 VDD、VSS、WP写保护、HOLD保持。实际用的时候WP 和 HOLD 如果不需要硬件保护功能直接上拉到 VDD 就行。我一般会把 WP 接到 MCU 的一个 GPIO 上这样可以在固件里做一层软件写保护防止误写。电气参数方面供电范围 2.7V 到 3.6V跟 PIC32MZ 的 3.3V IO 电平完全匹配不需要电平转换。这点很省事直接连就行。工作电流在 40MHz 下大约 15mA 左右待机电流只有几十微安对于电池供电或者低功耗场景也友好。SPI 模式支持 Mode 0 和 Mode 3我一般用 Mode 0CPOL0CPHA0因为 PIC32 的 SPI 模块默认配置就是 Mode 0省得改。最高时钟 40MHz但实际布线的时候如果走线比较长或者有干扰建议先降到 20MHz 调试稳定后再往上提。2.2 PIC32MZ1024EFF144 的 SPI 外设配置PIC32MZ 的 SPI 模块功能比较全有几个关键寄存器需要配置。SPIxCON 寄存器里要设置主模式、时钟极性、时钟相位、数据宽度SPIxBRG 寄存器设置波特率分频SPIxSTAT 寄存器用来使能和查看状态。波特率的计算方式是SPI 时钟 外设总线时钟 / (2 * (BRG 1))。假设外设总线时钟是 100MHz想要 20MHz 的 SPI 时钟那么 BRG 100 / (2 * 20) - 1 1.5取整为 1实际时钟就是 100 / (2 * 2) 25MHz。这里要注意PIC32 的 SPI 时钟不能超过 MRAM 的 40MHz 上限也不能超过 MCU 自身 SPI 模块的极限。数据宽度我一般设成 8 位因为 MRAM 的指令、地址、数据都是按字节组织的。虽然 PIC32 支持 32 位宽度但在 MRAM 这种字节寻址的设备上8 位模式最直观不容易出错。2.3 PCB 布线的几个坑SPI 虽然是个低速总线但在 40MHz 下也不能太随意。我踩过的坑主要有这几个。第一个是片选信号的走线。CS 信号一定要从 MCU 直接拉到 MRAM中间不要经过任何缓冲或者长距离绕线。CS 的边沿如果太缓MRAM 可能会误判指令起始。我一般会在 CS 上串一个 22 欧姆的电阻抑制振铃。第二个是SCK 和数据的等长。虽然 SPI 不是差分信号但 SCK 和 SI/SO 的走线长度差最好控制在 5mm 以内减少时序偏差。如果板子上 MRAM 离 MCU 比较远比如超过 10cm那就得考虑降低时钟频率或者加缓冲器。第三个是电源去耦。MRAM 的 VDD 引脚旁边一定要放一个 0.1uF 的陶瓷电容越近越好。我见过一个案例因为去耦电容离得太远MRAM 在写入时偶尔出现数据错误查了半天才发现是电源纹波的问题。提示如果板子上有多个 SPI 从设备MRAM 的 CS 一定要独立控制不要跟其他设备共用片选。MRAM 对片选信号的响应比较敏感共用片选容易导致指令错乱。3. 软件驱动与读写实现3.1 SPI 底层驱动的初始化流程在 PIC32MZ 上写 SPI 驱动我习惯用 Harmony 框架的 PLIB 层直接操作寄存器效率高也容易控制。初始化流程大概是这样// 1. 配置引脚为 SPI 功能 PLIB_PORTS_PinModeSet(PORTS_ID_0, PORT_CHANNEL_B, PORTS_BIT_POS_14, PORTS_PIN_MODE_DIGITAL); // SCK、SI、SO 的引脚配置类似根据实际板子调整 // 2. 关闭 SPI 模块再配置 PLIB_SPI_Disable(SPI_ID_2); // 3. 配置为主模式、Mode 0、8位数据 PLIB_SPI_MasterEnable(SPI_ID_2); PLIB_SPI_ClockPolaritySelect(SPI_ID_2, SPI_CLOCK_POLARITY_IDLE_LOW); PLIB_SPI_ClockPhaseSelect(SPI_ID_2, SPI_CLOCK_PHASE_MIDDLE); PLIB_SPI_DataSizeSelect(SPI_ID_2, SPI_DATA_SIZE_8BIT); // 4. 设置波特率目标 20MHz PLIB_SPI_BaudRateSet(SPI_ID_2, 100000000, 20000000); // 5. 使能 SPI PLIB_SPI_Enable(SPI_ID_2);这段代码里PLIB_SPI_BaudRateSet的第二个参数是外设总线时钟频率第三个是目标波特率。函数内部会自动计算 BRG 值。实际跑下来20MHz 的 SPI 时钟在 10cm 的排线上很稳再高就得看板子质量了。3.2 MRAM 的指令集与读写时序MR25H40CDF 的指令集不复杂常用的就几条WREN写使能0x06、WRDI写禁止0x04、RDSR读状态寄存器0x05、READ读数据0x03、WRITE写数据0x02。地址是 19 位因为 4Mbit 等于 512KB需要 19 根地址线。发送地址的时候要发 3 个字节高 3 位是无效的实际有效的是低 19 位。读操作的时序是拉低 CS发送 0x03发送 3 字节地址然后连续读取数据数据在 SCK 的下降沿输出。写操作的时序是拉低 CS发送 0x06WREN拉高 CS再拉低 CS发送 0x02发送 3 字节地址然后连续写入数据。注意 WREN 之后必须拉高 CS 再拉低否则写使能不生效。这个细节我当初调的时候卡了半天手册上写得不太显眼。// 写使能 void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_Exchange8bit(0x06); MRAM_CS_HIGH(); } // 写数据 void MRAM_Write(uint32_t addr, uint8_t *data, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_Exchange8bit(0x02); SPI_Exchange8bit((addr 16) 0x07); // 高3位无效只取低3位 SPI_Exchange8bit((addr 8) 0xFF); SPI_Exchange8bit(addr 0xFF); for (uint32_t i 0; i len; i) { SPI_Exchange8bit(data[i]); } MRAM_CS_HIGH(); // 等待写入完成MRAM 写入很快但保险起见查一下状态 while (MRAM_IsBusy()); }读函数类似只是把 0x02 换成 0x03然后把SPI_Exchange8bit的返回值存到缓冲区里。3.3 用 DMA 提升大批量读写效率如果只是存几个参数用上面的轮询方式就够了。但如果要连续记录大量数据比如每秒几千个采样点轮询方式会占用大量 CPU 时间。这时候可以用 PIC32MZ 的 DMA 配合 SPI让数据自动搬运。配置 DMA 的步骤稍微复杂一点先设置 DMA 通道的源地址和目的地址源地址指向内存缓冲区目的地址指向 SPI 的发送寄存器然后设置传输长度最后使能 DMA 和 SPI 的发送请求。接收方向类似源地址是 SPI 接收寄存器目的地址是内存缓冲区。我实测过用 DMA 做 512 字节的连续写入CPU 占用从轮询方式的 30% 降到不到 5%效果很明显。不过 DMA 配置的坑也不少比如传输完成中断的标志位要清干净否则会反复进中断还有 DMA 的地址要对齐不对齐的话传输效率会下降。注意MRAM 的写入虽然不需要擦除但连续写入时如果 CS 一直保持低电平地址会自动递增这跟 Flash 的页编程类似。但如果跨页边界地址会回绕到页首而不是继续递增到下一页。所以大批量写入时最好按页对齐每页 256 字节写满一页就拉高 CS 重新发指令。4. 实际项目中的问题排查与经验4.1 常见问题速查表现象可能原因排查方法解决方法读出的数据全是 0xFFCS 没拉低、SPI 没使能、MRAM 供电异常用示波器看 CS 和 SCK 波形检查片选引脚配置、测量 VDD写入后读出来不对WREN 没生效、地址算错、时序太快读状态寄存器看 WEL 位确保 WREN 后 CS 有拉高再拉低偶尔数据错误电源纹波、走线干扰、时钟太快降低 SPI 时钟到 10MHz 测试加去耦电容、缩短走线、串电阻写入超时MRAM 忙、状态寄存器一直置位读状态寄存器检查供电电压是否在 2.7V 以上DMA 传输不完成DMA 通道配置错误、中断未清查 DMA 状态寄存器重新配置通道、清中断标志4.2 一个真实的掉电数据保护案例我之前做的一个用电信息采集终端要求掉电后能把最后 100 条记录完整保存。最初的方案是用 Flash结果在现场测试时发现每次拉闸后总有几条记录丢失。后来分析发现Flash 的页编程需要 1 到 3 毫秒而掉电检测到电源完全跌落只有不到 2 毫秒时间不够。换成 MRAM 之后写入时间缩短到微秒级掉电检测中断里直接调用 MRAM 写入函数100 条记录在 1 毫秒内全部写完再也没有丢过数据。这个案例让我深刻体会到在掉电保护场景里存储介质的写入延迟是决定性因素。具体实现上我在掉电检测中断里做了这几件事首先关闭其他外设的中断防止干扰然后把要保存的数据打包成一个结构体接着调用 MRAM 写入函数最后读回校验一遍。整个流程控制在 500 微秒以内。为了保险我还在正常运行时每隔一段时间就把关键数据同步到 MRAM这样即使掉电中断没来得及执行数据也不会丢太多。4.3 调试工具与技巧调试 SPI 和 MRAM最有用的工具是逻辑分析仪。我用的是一款 8 通道、100MHz 采样率的价格不贵但足够用。抓波形的时候重点看 CS、SCK、SI、SO 四根线。正常的读操作CS 拉低后第一个字节应该是 0x03然后是 3 字节地址接着是数据。如果第一个字节不对说明 SPI 配置有问题如果地址不对说明地址计算有误如果数据不对但指令和地址都对那可能是时序或者电源问题。另一个技巧是用 MRAM 的状态寄存器做自检。状态寄存器的最低位是 WEL写使能锁存发完 WREN 后读状态寄存器如果 WEL 是 1说明写使能成功如果是 0说明 WREN 没生效。这个自检步骤我一般放在初始化里确保 MRAM 通信正常后再进行后续操作。还有个经验是MRAM 的写入虽然快但也不是瞬间完成。手册上写的写入周期是 0但实际上从 CS 拉高到数据真正写入还是有一个很小的延迟。如果写完立刻读偶尔会读到旧数据。保险的做法是写完等 100 纳秒再读或者直接读状态寄存器确认不忙。5. 性能优化与扩展思路5.1 提升读写吞吐量的几个手段如果应用对吞吐量要求比较高可以从这几个方面优化。第一是提高 SPI 时钟。MR25H40CDF 支持 40MHzPIC32MZ 的 SPI 也能跑到这个频率。但前提是 PCB 布线质量要过关走线要短、要有地平面。我一般会先跑 20MHz稳定后再逐步提到 40MHz每提一次都做长时间的压力测试。第二是用 DMA 做批量传输。前面提到过DMA 能把 CPU 从数据搬运中解放出来。对于连续记录的场景可以配置 DMA 在后台自动把数据从内存搬到 MRAMCPU 只负责触发和检查状态。第三是减少 CS 的切换次数。每次 CS 拉低拉高都有开销如果连续写多个字节尽量在一次 CS 有效期内完成。MRAM 支持地址自动递增所以连续写同一页的数据不需要反复发指令。第四是合理规划数据布局。把频繁写入的数据放在一起把只读的数据放在另一区域这样可以减少不必要的写使能操作。我一般会把 MRAM 分成几个区参数区、日志区、掉电保护区每个区独立管理。5.2 数据完整性的保障机制工业场景对数据完整性要求高光靠 MRAM 本身的可靠性还不够还得在软件层面加保护。我常用的做法是双备份加校验。每个关键数据存两份地址相隔一段距离每份数据后面跟一个 CRC16 校验值。读取的时候两份都读出来校验都通过且数据一致才采用如果一份坏了用另一份恢复如果两份都坏了那就报错。这个机制虽然多占了一倍空间但对于关键参数来说值得。另一个做法是写入前先擦除逻辑标记。虽然 MRAM 不需要物理擦除但可以在软件层面做一个“有效标记”。写入新数据前先把标记位置为无效写完数据后再把标记置为有效。这样即使写入过程中掉电读取时看到无效标记就知道数据不完整可以用旧数据或者默认值。还有个细节是避免在中断里做大量 MRAM 写入。虽然 MRAM 写入快但在中断里长时间操作外设会影响系统实时性。我的做法是在中断里只把数据存到一个 RAM 缓冲区然后置一个标志位在主循环里再真正写入 MRAM。只有掉电中断是例外那种情况下必须立刻写。5.3 这套方案的扩展方向这套 MRAM 加 PIC32 的组合除了基本的存储读取还能扩展出不少玩法。比如做数据日志系统。利用 MRAM 的大容量和无限写入寿命可以实现一个循环日志记录设备的运行状态、故障事件、操作记录。日志按时间顺序写入写满后覆盖最旧的记录。因为 MRAM 不需要擦除覆盖写就是直接写逻辑很简单。再比如做参数的双区备份和在线升级。把参数分成 A、B 两个区正常运行时用 A 区升级时写到 B 区写完校验通过后切换指针指向 B 区。如果升级过程中掉电A 区还是完整的系统可以回滚。这个机制在工业设备远程升级时很有用。还可以结合 RTOS 做存储管理。如果系统跑了 FreeRTOS 之类的实时操作系统可以把 MRAM 的读写封装成独立的任务通过队列接收其他任务的存储请求。这样存储操作不会阻塞关键任务系统的实时性更有保障。我个人在实际操作中的体会是MRAM 这颗芯片最大的价值不在于它有多快或者多大而在于它把“非易失”和“像 RAM 一样写”这两件事同时做到了。在工业嵌入式这个领域很多设计难题的根源就是存储介质的物理特性限制而 MRAM 恰好解开了这个限制。当然它的成本比 Flash 高容量比 SRAM 小选型的时候还是要根据具体需求权衡。但如果你的场景是高频写入、掉电保护、长期可靠那这套方案值得认真考虑。