去年接手一台工业测试设备的固件改造设备上的 MCU 是 Microchip 的 PIC32MZ2048EFH144需要把现场运行参数、近期报警和几个关键计量值保存下来。当时我在 NOR Flash、EEPROM 和 MRAM 之间来回纠结最后定稿的方案是 Everspin 的 MR25H40CDF——一颗 4Mbit 的串行 MRAM。这套组合在嵌入式工业产品里不算冷门但真正把它调通、跑稳里面有不少数据手册上没写透的细节。这篇文章顺着我从选型到调完的完整过程把存储和读取这套流程拆开讲一遍适合正在做工业数据记录、掉电保存、参数管理这类功能的嵌入式工程师参考。1. 为什么工业存储偏偏选了 MR25H40CDF 这颗 MRAM1.1 从“频繁写入”这个刚需说起工业现场设备和消费电子产品最大的区别在于数据访问模式绝大多数工业数据是写多读少。运行日志、报警记录、电流电压曲线、计数器、校准参数每一类都是随时可能写入而读取往往只有维护人员查看时才发生。这类数据有三个共同点写入频繁、掉电不能丢、单条数据量不大但必须绝对可靠。我最早给这套系统配的是 SPI NOR Flash容量大、价格低、读速度快但写起来很别扭。NOR Flash 的结构决定它必须先擦除再编程擦除的最小单位是扇区一个 4KB 扇区可能整个擦除后才能写。这意味着我只是想更新 128 字节的计数器也得先把整个扇区读出来改写再擦除重写稍微处理不好就会引入磨损均衡、掉电损坏一整片数据的问题。后来试过串行 EEPROM写字节不用擦除但内部写周期通常要等几毫秒连续写大量数据时吞吐量上不去容量也普遍偏小。直到我把目光放到 MRAM 上。MRAM 的全称是 Magnetoresistive Random Access Memory核心存储单元靠磁阻状态保存数据既有 SRAM 的随机读写速度又有非易失特性。MR25H40CDF 是 Everspin 的 4Mbit 串行 MRAM换算过来是 512KBSPI 接口数据手册标称时钟可以跑到 40MHz 级别。它最大的特点是写入不需要擦除可以直接对任意地址覆盖写没有“擦后写”的中间状态也不存在读改写扇区的问题。我们设备里的真实需求是每 10 秒写一条 128 字节的运行记录。按照这个频率如果数据落在 NOR Flash 的某个 4KB 扇区里那么约 32 条记录就会填满一个扇区大约 320 秒需要擦一次一天下来约 270 次擦写按标称 10 万次擦写寿命计算一年左右就到了极限而且还得专门设计磨损均衡。换成 MR25H40CDF 之后128 字节就是一次直接覆盖写没有擦除环节寿命完全不是一个量级。1.2 MR25H40CDF 到底适合干什么不适合干什么选型这件事最忌讳的是“拿着一个器件到处套”下面是我实测后对这个器件的定位。先看它适合的场景。第一是高频小数据量记录比如上面提到的运行日志、计量值、事件快照。第二是掉电瞬间紧急保存因为写入不用等擦除从发出写指令到数据真正落盘的时间非常短在电容保持的毫秒级窗口内就能完成。第三是频繁改写的参数区比如校准系数、设备编号、网络配置这类数据可能每次开机都改但量很小MRAM 可以随时改写任意字节。不适合的场景也很明显。首先它只有 512KB 容量装不了大文件也不能替代外部程序 Flash 做代码存储。其次如果只是读多写少、偶尔写一次MRAM 的价格优势就体现不出来了这种情况用 NOR Flash 更划算。还有一点要注意MR25H40CDF 没有像 SD 卡那样的文件系统生态更适合裸数据访问要么自己管理地址要么实现轻量级文件方案。1.3 和 NOR Flash、EEPROM 的定位差异用表格对比一下我最终选型时的考量维度这样最直观维度MR25H40CDF (MRAM)SPI NOR Flash串行EEPROM典型容量4Mbit8~256Mbit2Kbit~512Kbit写前擦除不需要需要按扇区擦除不需要写入最小单位字节/页(256B)页写但需先擦除字节/页单次写随机字节耗时微秒级毫秒级(含擦除)毫秒级(内部写周期)典型写耐久标称在 10^13~10^14 次量级10 万次左右约 100 万次掉电中途异常目标字节可能部分翻转可能损坏整个扇区目标字节可能异常适合场景高频写入、掉电保存大容量代码/文件存储小规模低频参数当时我还有一个备选是“SRAM 后备电池”速度快但没有非易失能力电池还要定期维护在工业设备里电池本身就是故障点。所以最终 MR25H40CDF 是反复对比后最稳的方案。2. PIC32MZ2048EFH144 侧的环境与 SPI 外设准备2.1 为什么选 PIC32MZ2048EFH144 来搭这套MRAM 本身只是一个 SPI 从设备任何带 SPI 的单片机都能接但工业嵌入式项目对 MCU 的要求会更高。PIC32MZ2048EFH144 属于 Microchip 的 PIC32MZ EF 系列主核是 MIPS M5150最高 200MHz内部有 2MB Flash 和 512KB RAM片上还带了 ECC 校验对于工业和功能安全场景很关键。选它的原因有几个。第一是外设丰富SPI、UART、CAN、USB、以太网都有设备后续要加通信节点不用换 MCU。第二是引脚可重映射PIC32MZ 支持外设引脚选择SPI 的时钟和数据引脚可以分配到很多普通 IO 上PCB Layout 时灵活度很高。第三是内部 Flash/RAM 都有 ECC单比特翻转可以被检测和修正外部存储再配合 CRC 校验整套系统的数据可靠性会比较完整。当然PIC32MZ 的初始化复杂度也高一些时钟树、PPS 映射、SPI 波特率都要自己搞清楚。下面我用的是 MPLAB X Harmony 之外、相对偏底层的寄存器操作方式这样读者可以完全清楚每一步在做什么移植到别的 PIC32 型号也容易。2.2 最小硬件连接引脚、电源、上下拉MR25H40CDF 虽然是 4Mbit MRAM封装却是小尺寸的 8 脚 DFN引脚不多硬件连接非常简单。典型引脚包括CS#、SCK、SI、SO、WP#、HOLD#、VDD、GND。我在设备板上用的连接关系如下PIC32MZ 的引脚号是示例实际以 PCB 布线为准MR25H40CDF 引脚信号方向PIC32MZ2048EFH144 引脚示例CS#输入普通 GPIO如 RE0SCK输入SPI 时钟输出PPS 映射到 RPD8SI输入SPI 数据输出MCU 的 SDO1SO输出SPI 数据输入MCU 的 SDI1WP#输入GPIO 控制或直接接 3.3VHOLD#输入必须上拉到 3.3V禁止悬空VDD电源3.3V靠近引脚放 0.1uF 去耦电容GND地公共地有两个地方是新手最容易忽略的。一是HOLD# 引脚禁止悬空。HOLD# 是暂停功能平时要让第它处于高电平。悬空时只要 PCB 上有轻微干扰它就可能被误触发表现为 SPI 时钟继续走但数据完全错位。二是WP# 不要简单接地。WP# 是硬件写保护如果固定接地后面你想通过软件更新参数区就会一直写不进去排查起来浪费很多时间。我建议 WP# 接到一个 GPIO 上允许程序在运行时动态控制写保护。2.3 用 PPS 重映射把 SPI 引脚固定下来PIC32MZ 的引脚重映射能力很好用但必须先配置映射寄存器否则 SPI 外设根本不工作。以 SPI1 为例我需要确定三根线时钟输出、数据输出、数据输入。PPS 的配置分为输入映射和输出映射两类。下面这段是配置思路注意寄存器位定义不同型号有差异使用前务必对照数据手册的外设引脚选择映射表// 输入映射把 SDI1 的输入来源指向某个 RP 引脚 // 例如将外设输入 SDI1 接到 RP10 对应的引脚 SDI1R 0x0A; // 具体映射值需要按器件数据手册查表 // 输出映射把 SCK1 和 SDO1 从各自 RP 引脚输出 // 例如 RP12 复用为 SCK1RP13 复用为 SDO1 RPB12R 0x07; // 见数据手册“RPBxR 输出映射”表 RPB13R 0x0A;这里有个实操小技巧如果用的是 Harmony 自动生成代码它会帮你把映射全部配好但出了问题反而很难查。我这次是纯寄存器驱动所以在初始化函数里把 PPS 配置放在最前面然后用逻辑分析仪确认 SCK 和 SDO 确实有波形再继续调 MRAM。SPI 时钟速率配置也不复杂。PIC32MZ 的 SPI 波特率公式是SPI_CLK PBCLK / (2 * (SPI1BRG 1))我板上的 PBCLK 是 100MHz目标 SPI 时钟 10MHz所以SPI1BRG 100000000 / (2 * 10000000) - 1 4。实际产品里如果布线比较长建议先跑 10MHz 调通功能再逐步提速不要一上来就冲 40MHz。3. MR25H40CDF 读写驱动的落地实现3.1 指令集里最常用的几条命令MR25H40CDF 的指令风格和传统 SPI NOR Flash 很像这对移植非常友好。最常用的命令其实只有几条指令码名称说明0x06WREN置写使能锁存器每次写操作前必须发0x04WRDI清除写使能0x05RDSR读状态寄存器0x01WRSR写状态寄存器0x03READ普通读连续读无需额外命令0x0BFAST_READ快速读带 Dummy 周期0x02WRITE页写最大 256 字节注意MRAM 没有擦除指令。以前用 NOR Flash 时习惯在写数据前先判断目标区域是否为 0xFF属于擦除后的空白状态换到 MRAM 之后这套思想要扔掉。MR25H40CDF 上电内容并不保证是全 0 或全 1它没有“空白状态”的概念所以每次写之前不需要判断、也不需要擦除直接发写使能然后写地址数据就可以。3.2 底层字节收发直接操作 SPI 外设我习惯先把 SPI1 的底层字节收发函数写出来后面所有命令都基于它。核心代码#define MRAM_CS_LOW() (LATBbits.LATB0 0) #define MRAM_CS_HIGH() (LATBbits.LATB0 1) // SPI1 单字节交换发送一个字节同时接收一个字节 uint8_t SPI1_TransferByte(uint8_t txData) { SPI1BUF txData; while (!SPI1STATbits.SPIRBF) { // 等待接收缓冲满 ; } return SPI1BUF; }这里有个容易出错的地方PIC32MZ 的 SPI 外设有 FIFO也可能溢出。如果中断或 DMA 处理不过来SPIROV位会被置 1此时后续读取的数据全部无效。所以初始化时要把SPIROV清零调试时读到奇怪数据第一个检查它。SPI 初始化我使用 SPI Mode 0也就是时钟空闲为低、第一个边沿采样数据。MR25H40CDF 支持 Mode 0 和 Mode 3两者都行但驱动和主设备必须一致。PIC32MZ 寄存器里的对应关系是CKP0, CKE1如果你用的是 Harmony 或别的抽象层直接选 Mode 0 即可。// SPI1 初始化Master、8位、Mode 0、10MHz SPI1BRG 4; // 100MHz / (2*(41)) 10MHz SPI1CONbits.MSTEN 1; // 主模式 SPI1CONbits.MODE32 0; SPI1CONbits.MODE16 0; // 8 位数据 SPI1CONbits.CKP 0; SPI1CONbits.CKE 1; // Mode 0 SPI1CONbits.SMP 0; SPI1STATbits.SPIROV 0; // 清除溢出标志 SPI1CONbits.ON 1; // 使能 SPI3.3 封装成 Read/Write API并处理页边界底层字节收发有了剩下的就是按协议拿 CS# 拉低发指令、发地址、传数据最后拉高 CS#。读操作比较简单连读过程中 CS# 可以一直保持低电平发送一个 0x03 命令后连续读出数据即可// 从 addr 开始连续读 len 字节 void MR25H40_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_TransferByte(0x03); // READ SPI1_TransferByte((addr 16) 0xFF); SPI1_TransferByte((addr 8) 0xFF); SPI1_TransferByte(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] SPI1_TransferByte(0x00); } MRAM_CS_HIGH(); }写操作比读多两步。虽然 MRAM 不需要擦除但芯片仍然带有写使能机制必须在每次写命令前发 0x06 把内部写使能锁存器置 1写完成后可以再发 0x04 清理。最简单的页写函数如下// 写一个页最多 256 字节且要求 addr 起止不跨 256 字节页边界 void MR25H40_WritePage(uint32_t addr, const uint8_t *buf, uint32_t len) { // 1. 写使能 MRAM_CS_LOW(); SPI1_TransferByte(0x06); MRAM_CS_HIGH(); // 2. 页写命令 MRAM_CS_LOW(); SPI1_TransferByte(0x02); // WRITE SPI1_TransferByte((addr 16) 0xFF); SPI1_TransferByte((addr 8) 0xFF); SPI1_TransferByte(addr 0xFF); for (uint32_t i 0; i len; i) { SPI1_TransferByte(buf[i]); } MRAM_CS_HIGH(); // 3. 等待写完成 WaitWriteComplete(); }这里必须处理页边界问题。MR25H40CDF 内部按照 256 字节分页页写命令一旦跨越页边界地址会回绕到本页开头把前面的数据冲掉。和 NOR Flash 的 Page Program 是同一个逻辑。所以通用写函数要先把数据按页拆成多段每段单独发一次页写命令void MR25H40_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t end addr len; uint32_t current addr; const uint8_t *p buf; while (current end) { // 当前页内剩余可写字节数 uint32_t pageLeft 256 - (current 0xFF); uint32_t chunk (end - current) pageLeft ? (end - current) : pageLeft; MR25H40_WritePage(current, p, chunk); current chunk; p chunk; } }这个函数写完后上层调用者完全不需要关心数据是否跨页驱动层已经透明处理。3.4 状态寄存器和写保护位MR25H40CDF 的状态寄存器里最常用的是WIPBusy位和WEL写使能锁存位。WIP指示芯片是否正在内部写入WEL表示写使能是否已经置位。我的等待函数实现如下void WaitWriteComplete(void) { uint8_t sr; do { MRAM_CS_LOW(); SPI1_TransferByte(0x05); // RDSR sr SPI1_TransferByte(0x00); MRAM_CS_HIGH(); } while (sr 0x01); // WIP }由于 MRAM 写周期极短这个轮询在实际测试中基本上一次性通过几乎看不到等待时间。但保留这段代码是有好处的如果未来你换成别的 MRAM 型号或某些批量器件时序有差异系统不会被偶然的忙状态卡死。WP# 引脚控制的是硬件写保护而状态寄存器里的BP位则控制软件写保护范围。工业设备里如果某个参数区不允许现场工程师随便改可以在初始化时用WRSR设置保护位再把 WP# 引脚拉低锁住。需要更新时先拉高 WP#发送 WREN 后再写状态寄存器。这个逻辑和很多 Flash 芯片一致只是别忘了每次 WRSR 之前也要写使能。4. 读写实测与性能表现以及和“需要擦除”方案的真实差距4.1 测试环境和方法硬件是最小系统板PIC32MZ2048EFH144 主频 200MHzPBCLK 配置为 100MHzSPI 时钟跑 10MHz。MR25H40CDF 的 VDD 用 3.3V 供电HOLD#直接上拉WP#接到 GPIO 由软件控制。测试工具是逻辑分析仪和一段简单的自测程序。自测程序逻辑很直接往 MRAM 里写一段递增模式数据读回来比对接着测随机写单字节、连续页写、整片读等场景用定时器测量耗时。4.2 实测数据与理论带宽换算根据 SPI 时钟和传输字节数理论上可以算出每次操作的耗时上限。计算公式很简单传输时间 传输字节数 × 8 bit / SPI 时钟频率在 10MHz 下读 1KB 数据需要发送命令和地址共 4 字节加上 1024 字节数据总共 1028 字节理论时间约1028 × 8 / 10MHz 822us。实测结果和理论值非常接近大约840us主要多出来的几十微秒是 CS# 翻转和轮询状态寄存器的开销。写 1KB 数据拆成 4 个 256 字节页。每页写入需要 1 字节写使能、4 字节写命令和地址、256 字节数据共 261 字节。四页合计 1044 字节理论时间约1044 × 8 / 10MHz 835us。因为 MRAM 不需要擦除实测写 1KB 不到 1ms。作为对比同样在 10MHz 的 SPI 总线上NOR Flash 写 1KB 往往需要先擦除一个 4KB 或 64KB 的扇区。典型扇区擦除时间几十到几百毫秒加上页写时间整体耗时通常是 MRAM 的两个数量级以上。串行 EEPROM 虽然没有擦除但每页内部写周期一般是毫秒级写 1KB 也要好几毫秒。我把关键操作的时间按常温下测试结果整理成表格操作SPI时钟测试数据量实测耗时说明连续读10MHz1KB约 0.84 ms接近理论极限连续读10MHz512KB约 423 ms整片读取页写10MHz256B约 0.21 ms免擦除随机写单字节10MHz1B约 6 us命令开销为主写 1KB10MHz4 页约 0.9 ms自动拆分跨页整片 512KB 的连续读实测约 423ms这个数字主要受限于 SPI 速率本身换任何 SPI 存储器件都差不多。真正的差距在随机写和页写场景。4.3 为什么“免擦除”带来的差距是本质性的“免擦除”这三个字听起来简单实际对整个存储架构的影响很大。以前用 NOR Flash 时我至少要维护三张表扇区擦写计数表、逻辑地址到物理扇区的映射表、坏块表。因为擦写有寿命限制频繁写入的区域必须做磨损均衡否则某一两个扇区可能很快就写废了。换 MRAM 之后这些问题几乎消失了。MR25H40CDF 的写耐久标称在非常高的量级实际工程里基本不需要考虑磨损均衡。数据记录可以直接按地址连续覆盖写日志区满了就从头再覆盖就像操作一块不会坏的普通内存一样。我也专门做过掉电保护场景测试。用一个电子开关瞬间切断主供电PIC32MZ 检测到电源跌落之后立刻在掉电窗口内把现场参数写到 MRAM。由于写单条记录只有几十微秒之后再上电读取数据完好。同样的动作如果放在 NOR Flash 上需要保证目标扇区已经处于擦除态否则可能先等扇区擦除掉电窗口根本等不起。5. 工业部署里的可靠性设计不能只靠 MRAM 本身5.1 掉电瞬间的写入原子性怎么处理前面说掉电紧急保存能成功但这不等于任何时刻掉电都安全。MRAM 避免了“擦除损坏整个扇区”的问题可如果写入一个多字节记录时掉电目标位置仍然可能出现一部分字节更新、另一部分字节还是旧值的情况也就是写入不原子。要处理这个问题传输层只保证大体可靠应用层必须自己加事务机制。最常用的是双槽方案同一份参数保存两份写入时先更新 A 槽再更新 B 槽。读取时比较两份数据的序号和 CRC序号新且校验通过的那份就是有效数据。typedef struct { uint32_t magic; // 固定魔数用于识别有效槽位 uint32_t seq; // 递增序号 uint8_t data[32]; // 实际参数 uint32_t crc32; // CRC 校验 } ParamSlot; #define PARAM_SLOT_A_ADDR 0x00000 #define PARAM_SLOT_B_ADDR 0x00100上电恢复流程读 A 和 B 槽先检查 magic再算 CRC最后比较 seq 取最新。如果某一槽在掉电时写到一半CRC 必然不过系统会使用另一个完整的槽。写入时先把新数据整理到临时变量再写 A、再写 B两个槽都成功后才算本次更新完成。5.2 环形日志与双缓冲参数区的实现思路工业设备里另一类典型数据是环形日志设备一直记录运行状态存满之后覆盖最旧的数据。用 NOR Flash 做环形日志最难办的是“覆盖旧数据”等于要反复擦除同一个扇区必须引入多扇区轮流擦写。用 MRAM 就简单很多。我在设备上实现了一个很小的环形队列结构如下在 MRAM 起始地址放队列头指针和尾指针剩余空间按固定长度记录存储。每写一条记录把数据写到当前尾指针位置尾指针递增到尾了就回绕到起始地址。不需要擦除不需要移动数据操作就是普通内存写。#define LOG_RING_SIZE 4096 #define LOG_RECORD_SIZE 128 #define LOG_HEAD_ADDR 0x20000 #define LOG_TAIL_ADDR 0x20004 #define LOG_DATA_ADDR 0x20008读取日志时先读头尾指针从头指针按顺序读取记录遇到尾指针停止。如果检测到头尾指针本身 CRC 异常说明掉电时指针更新出了问题可以自动把环形区扫描一遍重建指针这样最坏情况只是丢掉追尾时的那半条记录不会破坏整体索引。双缓冲参数区和环形日志可以结合使用参数区用双槽保底保证关键配置绝对不会因掉电写坏日志区用环形结构保证高频写入不会磨损 flash也不会因为擦除不及时丢失数据。5.3 写保护、看门狗和供电设计WP# 引脚我强烈建议接到 GPIO而不是直接绑死在 3.3V 或 GND。正常运行时程序可以把 WP# 拉低并配合状态寄存器的 BP 位把关键参数区设置成只读防止异常跑飞时改写参数。只有在升级参数或恢复出厂设置时才把 WP# 拉高解除保护。看门狗复位也会带来一个隐患。PIC32MZ 被看门狗复位后SPI 外设重新初始化但如果之前刚好在 MRAM 写操作中间发生复位MRAM 侧的 CS# 可能一直处于低电平状态导致后续 SPI 通信错乱。所以看门狗复位后除了重新初始化 SPI还要先把 CS# 拉高一段时间再主动执行一次读状态寄存器确认芯片回到正常空闲态。供电设计方面MR25H40CDF 是 3.3V 器件必须等 3.3V 电源稳定后再操作。工业设备的电源纹波一般控制在 50mV 以内靠近 VDD 引脚放 0.1uF 和 10uF 电容。如果板上有电机、继电器等强干扰源MRAM 和 MCU 的电源要做好隔离或者至少保证 SPI 走线不要和感性负载的功率线近距离平行走线超过 3cm否则 10MHz 以上时钟很容易被耦合出毛刺。6. 调试踩坑记录几个容易被忽略的细节6.1 片选信号与 SPI 模式不匹配我第一次接上 MR25H40CDF 时读状态寄存器返回 0x00再读一遍又是 0xFF非常诡异。逻辑分析仪抓波形之后发现两个问题。第一个问题出在 CS# 控制方式上。我最初把 CS# 接在了 SPI 外设的从选择引脚上想让 SPI 外设自动管理片选。但 PIC32MZ 的 SPI 从选择引脚在主模式下并不会像普通 GPIO 那样拉低整个命令周期所以 CS# 在每两个字节之间跳变了一次导致芯片把命令上下文打断。解决方法是把 CS# 换成一个普通 GPIO整个命令流程中手动拉低、命令结束后拉高。第二个问题是 SPI Mode 不一致。MR25H40CDF 支持 Mode 0 和 Mode 3但我的读函数用的是一套时序初始化又是另一套时序导致数据总是在第一位或最后一位错。检查代码时把CKP和CKE两个位仔细对齐以后就正常了。排查这类问题建议先固定用 RDSR 命令做测试因为状态寄存器只有 8 位最容易判断波形和数据对不对。6.2 HOLD/WP 引脚悬空导致的诡异现象有一个现象让我排查了很久连续读 1KB 数据前面 256 字节完全正确后面偶尔出现一整段位移好像中间少了几个时钟周期。抓波形发现是 HOLD# 引脚悬空导致 SPI 时钟在高频时被板上的噪声干扰误触发暂停。工业设备的数字噪声比实验室严重得多HOLD# 一旦被拉低芯片会保持当前状态、忽略后续 SCK 边沿而主设备 SPI 依然照常发数据于是数据流整体错位。解决方法是把 HOLD# 通过 10k 电阻上拉到 3.3V并且最好不用长走线避免天线效应。WP# 悬空又是另一类症状写使能后页写命令发出去前面几个字节写进去了后面几个字节没写进去状态寄存器里的 WEL 位也可以被正常置 1整个过程看起来像是“写一部分丢一部分”。原因同样是 WP# 电平不稳定芯片状态在正常写和写保护之间来回跳。把 WP# 接到 GPIO 并由软件明确控制后问题彻底消失。6.3 读到的数据全是 0xFF/0x00 的排查链路如果读回来的数据全是 0xFF 或 0x00不要先怀疑 MRAM 坏了按下面的链路排查基本都能找到原因。第一步先测量 VDD 引脚电压必须在 3.3V 左右并且不能有明显跌落。第二步确认 CS#、SCK、SI、SO 四根线的逻辑电平是 3.3V 而不是 5V如果电平不对很可能芯片没有进入正常工作态。第三步检查 SI 和 SO 是否接反。MRAM 的 SI 是主设备 SDO 的输出SO 是主设备 SDI 的输入很多人的 PCB 上这两根线标注为 SPI_MOSI 和 SPI_MISO但方向标反的话所有数据都会变成 0x00。第四步用 RDSR 命令直接读状态寄存器如果状态寄存器都读不到有效数据说明硬件或 SPI 初始化有问题不要急着去读大块数据。还有一点很有迷惑性在 MRAM 上读出 0xFF 并不代表这段区域是“空白”。NOR Flash 擦除后的空白态是 0xFF而 MRAM 没有擦除概念新器件或未写入区域的数据内容并不保证全 0 或全 1。所以不能像以前用 Flash 那样靠“读到 FF 判断空区”来决定是否省略写入。读写自检时一定要先写一个已知模式再读回比对。从选型到驱动落地再到现场可靠性设计MR25H40CDF 和 PIC32MZ2048EFH144 这套组合在我手里的设备上已经稳定运行了一段时间。MRAM 不是万能存储芯片但它解决了工业场景里最常见的“频繁写入 掉电不丢 简单可靠”三个痛点。如果你也正准备在自己板子上接这颗芯片建议先花半天时间把 RDSR 读通再去看连续读写很多玄学问题其实都出在最基础的时序和引脚配置上。