1. 项目缘起为什么要在工业场景里折腾 MRAM 和 STM32F765工业现场的数据存储有个很尴尬的现状用 EEPROM 吧写入速度慢得让人着急擦写次数也就百万次级别频繁记录日志的话没几年就报废了用 SRAM 加电池吧电池本身就是个定时炸弹高温环境下撑不了太久换电池还得停机用 NOR Flash 吧写之前得擦除整个扇区掉电保护逻辑复杂而且擦写寿命同样有限。我前两年做一个电力监测终端项目设备装在配电房里夏天柜内温度能到 70 度客户要求每秒钟记录一次三相电流电压数据掉电不能丢设备寿命至少十年。当时试过几种方案最后锁定了 MR25H40CDF 这颗 MRAM 芯片配合 STM32F765ZI 主控实测下来确实稳。MR25H40CDF 是 Everspin 出的 4Mbit 磁阻随机存储器SPI 接口关键特性是写入没有延迟、擦写次数近乎无限、掉电数据不丢。它跟普通 Flash 最大的区别在于Flash 写之前要擦擦是以扇区为单位写是以页为单位操作起来束手束脚MRAM 的写操作跟 SRAM 一样直接给地址写数据就完事了不需要擦除不需要等待写一个字节和写一整页在时间上没有本质区别。STM32F765ZI 是 ST 家的 Cortex-M7 芯片主频 216MHz带硬件 SPI 接口支持 DMA用来驱动 MR25H40CDF 绰绰有余。这个组合在工业数据记录、嵌入式黑匣子、参数存储、频繁写入的计数器场景里非常合适。这篇文章我打算把整个方案从头到尾拆一遍包括硬件怎么连、SPI 怎么配、驱动怎么写、数据怎么组织、遇到问题怎么排查。不管你是刚接触嵌入式的新手还是做过几年想换存储方案的老人应该都能从里面找到能直接用的东西。我尽量说人话把踩过的坑和想明白的道理都写出来。2. 方案整体设计与选型逻辑2.1 为什么是 MRAM 而不是 Flash 或 FRAM选存储芯片这件事核心就看三个维度写入速度、擦写寿命、掉电保持能力。我把常见几种非易失存储拉出来对比一下你就能明白为什么 MRAM 在这个场景里胜出。存储类型写入速度擦写寿命掉电保持写入前是否需擦除典型功耗EEPROM慢ms级100万次10年否低NOR Flash中页写入ms级10万次20年是扇区擦除中FRAM快ns级10^14次10年否低MRAM快ns级10^15次20年否中SRAM电池极快无限依赖电池否高待机MR25H40CDF 的写入周期是 35ns 级别SPI 接口能跑到 40MHz实际连续写入吞吐可以做到几 MB/s。对比 EEPROM 的 5ms 写入周期差距是五个数量级。擦写寿命方面MRAM 是 10^15 次按每秒写一次算能写三千万年设备寿命内根本不用担心写坏。掉电保持 20 年以上工业设备生命周期完全覆盖。FRAM 其实也是候选但 FRAM 容量普遍偏小4Mbit 的 FRAM 价格比 MRAM 贵不少而且 TI 的 FRAM 系列供货周期不太稳定。MRAM 在容量、价格、供货上更均衡。至于 SRAM 加电池电池在高温下的寿命和可靠性是硬伤工业客户明确不接受可更换电池的方案。2.2 STM32F765ZI 的角色与资源分配STM32F765ZI 在这个方案里干三件事第一通过 SPI 接口读写 MR25H40CDF第二管理数据组织逻辑包括地址分配、校验、磨损均衡虽然 MRAM 不需要但为了兼容性可以留接口第三对外提供数据访问接口比如通过串口、以太网或者 USB 把存储的数据导出去。F765ZI 的 SPI 资源很充裕有 SPI1 到 SPI6 多个实例我一般把 SPI1 分配给 MRAM因为 SPI1 在 APB2 总线上时钟频率高而且引脚布局方便。DMA 用 DMA2 的通道配合 SPI1 的收发请求。GPIO 方面除了 SCK、MISO、MOSI 三根线还需要一根片选 CS我用的是软件片选因为 MRAM 的片选时序要求不严格软件控制更灵活方便在两次传输之间插入其他操作。时钟配置上F765ZI 的 SPI1 挂在 APB2最高 108MHzSPI 分频后可以到 54MHz、27MHz 等。MR25H40CDF 的最高 SPI 时钟是 40MHz所以我一般设 27MHz 或者 13.5MHz留点余量工业环境里信号完整性比极限速度重要。2.3 硬件连接与信号完整性考量接线本身不复杂但工业环境里细节决定成败。MR25H40CDF 是 8 脚 SOIC 封装引脚定义如下1 脚 CS片选低有效2 脚 SO数据输出MISO3 脚 WP写保护低有效不用的话接高4 脚 VSS地5 脚 SI数据输入MOSI6 脚 SCK时钟7 脚 HOLD保持低有效不用的话接高8 脚 VDD电源2.7V 到 3.6V跟 STM32F765ZI 连接时CS 接一个 GPIOSO 接 MISOSI 接 MOSISCK 接 SCK。WP 和 HOLD 我建议都上拉到 VDD虽然芯片内部有弱上拉但工业环境里外部加 10k 上拉更稳妥防止干扰导致误触发写保护或保持状态。PCB 布局上SPI 走线尽量短SCK 和 MOSI 之间保持足够间距避免串扰。如果走线超过 10cm建议在 SCK 上串一个 22 欧姆电阻减缓边沿降低反射。电源引脚旁边放 100nF 加 1uF 的退耦电容越近越好。地平面要完整不要被其他信号割裂。这些在实验室里可能看不出差别但到了现场电磁干扰一上来差的就是稳定和不稳定的区别。3. MR25H40CDF 的 SPI 协议细节与操作时序3.1 SPI 模式选择与命令集MR25H40CDF 支持 SPI 模式 0 和模式 3。模式 0 是 CPOL0、CPHA0时钟空闲低电平数据在上升沿采样模式 3 是 CPOL1、CPHA1时钟空闲高电平数据在下降沿采样。我一般用模式 0因为 STM32 的 SPI 配置成模式 0 最直观而且大部分逻辑分析仪默认也是模式 0调试方便。芯片的命令集不复杂常用的就几条命令名称命令码功能说明WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据RDID0x9F读设备 ID跟 Flash 不同的是MRAM 没有扇区擦除命令也没有页编程命令写操作就是直接发 WRITE 命令加地址加数据。这是它最大的优势省掉了擦除等待和复杂的地址管理。3.2 读操作时序与代码实现读操作很简单拉低 CS发 0x03发 24 位地址MR25H40CDF 是 4Mbit需要 19 位地址但按 24 位发高位补零然后连续读数据数据在 SCK 下降沿输出模式 0 下MCU 在上升沿采样。读完拉高 CS。用 STM32 HAL 库实现的话核心代码大概这样void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }这里有个细节HAL_SPI_Transmit 和 HAL_SPI_Receive 分开调用的话CS 在两次调用之间会保持低电平因为我没有在函数内部拉高。但 HAL 库的 Transmit 和 Receive 是阻塞式的中间会有函数调用开销如果 SPI 时钟很快可能会出现时钟间隙。更稳妥的做法是用 HAL_SPI_TransmitReceive 或者直接操作寄存器保证时序连续。我实测在 27MHz 下分开调用也能正常工作但如果你追求极致稳定建议用 DMA 一次性完成。3.3 写操作时序与写使能逻辑写操作比读多一步发 WRITE 命令之前必须先发 WREN0x06否则写操作会被忽略。WREN 之后 CS 必须拉高再拉低才能开始 WRITE 命令。这个时序在数据手册里有明确要求我一开始没注意直接 WREN 之后没拉高 CS 就发 WRITE结果数据写不进去查了半天才发现问题。正确的写流程void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; uint8_t wren 0x06; // 第一步发 WREN HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, wren, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); // 第二步发 WRITE 命令加地址加数据 cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi1, buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }写操作没有等待时间发完数据拉高 CS 就完成了。这一点跟 Flash 完全不同Flash 发完数据还要等内部编程完成得轮询状态寄存器。MRAM 不需要省掉了轮询逻辑代码简洁很多。3.4 状态寄存器与写保护机制MR25H40CDF 的状态寄存器只有几位有效WIP写进行中、WEL写使能锁存、BP0 和 BP1块保护。WIP 位在 MRAM 里基本没用因为写操作是即时的不会出现 WIP 置位的情况。WEL 位在 WREN 之后置 1在 WRITE 完成或 WRDI 之后清 0。BP0 和 BP1 用来设置块保护可以保护部分地址区域不被写入。工业场景里我一般会把参数区和日志区分开参数区设置块保护防止程序跑飞误写。具体做法是发 WRSR 命令写状态寄存器设置 BP 位。但要注意设置块保护之后写状态寄存器本身也需要 WREN而且块保护生效后被保护的地址写入会被静默忽略不会报错调试的时候容易迷惑。4. STM32F765ZI 的 SPI 配置与 DMA 优化4.1 CubeMX 配置要点用 CubeMX 配置 SPI1 的时候几个关键参数ModeFull-Duplex MasterHardware NSS SignalDisable用软件片选Data Size8 BitsFirst BitMSB FirstPrescaler根据实际时钟设我一般设 8 分频APB2 108MHz 的话 SPI 时钟 13.5MHzCPOLLowCPHA1 EdgeCRC CalculationDisabledNSS Pulse ModeDisableTI ModeDisableGPIO 方面PA5 是 SCKPA6 是 MISOPA7 是 MOSI这三个自动配置为复用推挽。CS 我选的是 PA4配置为推挽输出初始高电平。注意 CS 的 GPIO 速度要设成 High 或者 Very High不然拉低拉高的边沿太慢影响时序。4.2 DMA 传输配置与双缓冲思路数据量大或者需要高速连续读写的时候DMA 是必须的。SPI1 的 TX 用 DMA2 Stream3 Channel3RX 用 DMA2 Stream0 Channel3。CubeMX 里直接勾选就行模式选 Normal不选 Circular因为每次传输长度不一样。用 DMA 的话读操作可以这样写void MRAM_Read_DMA(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit_DMA(hspi1, cmd, 4); while (hspi1.State ! HAL_SPI_STATE_READY); HAL_SPI_Receive_DMA(hspi1, buf, len); while (hspi1.State ! HAL_SPI_STATE_READY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }这里有个坑HAL_SPI_Transmit_DMA 之后如果立刻调用 HAL_SPI_Receive_DMA可能会因为 SPI 还没完全发送完而冲突。我加了一个 while 等待状态就绪虽然牺牲了一点效率但保证了可靠性。更好的做法是用 HAL_SPI_TransmitReceive_DMA一次调用完成收发但需要把命令和数据拼到一个缓冲区里内存开销大一点。4.3 时钟极性与相位实测对比我实测过模式 0 和模式 3 在 MR25H40CDF 上的表现。模式 0 下SCK 空闲低第一个边沿是上升沿数据在上升沿采样。模式 3 下SCK 空闲高第一个边沿是下降沿数据在下降沿采样。两种模式都能正常工作但模式 0 的波形更干净因为空闲低电平在 PCB 上更容易保持稳定不容易受干扰。模式 3 的空闲高电平如果走线长容易耦合噪声导致误采样。如果你用逻辑分析仪抓波形模式 0 下应该看到 CS 拉低后SCK 开始翻转第一个上升沿对应 MOSI 上的第一位数据。如果发现数据错位先检查 CPOL 和 CPHA 设置再检查 CS 拉低和第一个时钟沿之间的建立时间是否足够。MR25H40CDF 要求 CS 建立时间最小 5nsSTM32 的 GPIO 翻转速度足够快一般没问题但如果 CS 上加了 RC 滤波就要注意了。5. 数据组织与存储管理实战5.1 地址空间划分与数据结构设计4Mbit 的 MRAM 实际可用 512KB地址范围 0x00000 到 0x7FFFF。我一般这样划分0x00000 - 0x00FFF设备信息区存序列号、固件版本、生产日期0x01000 - 0x01FFF参数区存校准系数、配置参数0x02000 - 0x7FFFF数据日志区循环记录每个区域加一个简单的头部结构包含魔数、长度、CRC 校验。比如参数区的结构typedef struct { uint32_t magic; // 0x50415241 PARA uint16_t version; uint16_t length; uint8_t data[256]; uint32_t crc32; } ParamBlock_t;写参数的时候先算 CRC再整体写入。读的时候先读头部校验魔数和 CRC通过才使用数据。这样即使掉电导致写入不完整也能检测出来不会用错数据。5.2 循环日志的写入与检索策略日志区用循环缓冲的方式管理。维护一个写指针每次写一条日志指针后移到末尾就回绕到开头。每条日志加一个序号和时间戳检索的时候根据序号和时间戳定位。日志条目结构typedef struct { uint32_t seq; // 序号递增 uint32_t timestamp; // 时间戳秒 uint16_t type; // 日志类型 uint16_t len; // 数据长度 uint8_t payload[64]; // 数据 uint16_t crc16; // 校验 } LogEntry_t;写日志的时候先读当前写指针写入条目更新写指针。写指针本身也存在 MRAM 里每次更新。MRAM 写次数无限不用担心写指针频繁更新会坏。检索的时候从最新条目往前找根据序号判断哪些是新的。因为循环覆盖旧数据可能被新数据覆盖所以检索逻辑要能处理序号回绕。我一般用 32 位序号回绕周期很长实际使用中不用考虑。5.3 掉电保护与数据完整性校验MRAM 本身掉电不丢数据但写入过程中掉电可能导致条目写了一半。为了防止这种情况我采用“先写数据再写校验”的策略先把日志条目的除 CRC 外的部分写入然后再写 CRC。读的时候如果 CRC 不匹配说明这条日志不完整丢弃即可。更严格的做法是双备份每条日志写两份地址错开读的时候取 CRC 正确的那份。但这样会浪费一半空间512KB 变 256KB。工业场景里如果日志价值高建议双备份如果只是普通记录单份加 CRC 就够了。还有一个细节写日志之前先把写指针更新到一个“正在写”的临时位置写完日志后再更新正式写指针。这样即使写日志过程中掉电重启后根据正式写指针也能知道哪些数据是完整的。这个逻辑稍微复杂一点但能保证数据一致性。6. 常见问题与排查技巧实录6.1 读写失败问题速查表现象可能原因排查方法解决方案读回全 0xFFCS 没拉低、SPI 没配置、芯片没供电用万用表测 VDD、用逻辑分析仪抓 CS 和 SCK检查硬件连接和 SPI 初始化读回全 0x00MISO 被拉低、芯片损坏断开 MISO 测电压检查 MISO 走线更换芯片写入后读回不变WREN 没发、CS 时序不对抓 WREN 和 WRITE 之间的 CS 波形确保 WREN 后 CS 拉高再拉低数据偶尔出错SPI 时钟太快、走线干扰降低 SPI 时钟测试加串阻、缩短走线、加退耦电容状态寄存器读不对命令码错误、模式不对对照数据手册检查命令码确认 SPI 模式和命令码DMA 传输卡死DMA 通道冲突、中断优先级检查 DMA 配置和中断调整 DMA 通道或优先级6.2 SPI 通信不稳定的排查思路SPI 通信不稳定十有八九是硬件问题。我遇到过一次实验室里好好的到了现场偶尔读出错。用示波器抓波形发现 SCK 上升沿有振铃幅度超过 VDD导致 MRAM 误触发。解决办法是在 SCK 上串了 33 欧姆电阻振铃明显减小问题消失。还有一次是 CS 走线太长跟电机驱动线捆在一起电机一启动就读写失败。后来把 CS 走线改短远离功率线加了屏蔽地线问题解决。工业环境里SPI 走线一定要远离干扰源能短则短必要时加屏蔽。软件层面如果 SPI 时钟分频设得太高比如 54MHzMR25H40CDF 可能来不及响应。数据手册标称 40MHz但实际跟 PCB 质量有关。我一般从 13.5MHz 开始调稳定了再往上加找到稳定工作的最高频率。6.3 写入数据丢失的几种典型场景写入数据丢失最常见的原因是 WREN 没生效。WREN 之后如果 CS 拉高WEL 位会保持但如果 WREN 和 WRITE 之间插入了其他 SPI 操作WEL 可能被清除。我建议 WREN 之后立刻发 WRITE中间不要插入其他命令。另一个原因是块保护设置。如果 BP 位设置了保护被保护区域的写入会被忽略而且不会报错。调试的时候可以先读状态寄存器确认 BP 位是 0。还有一种情况是电源不稳。MRAM 写入虽然快但写入瞬间电流会增大如果电源退耦不够VDD 瞬间跌落可能导致写入失败。我在 VDD 引脚旁边加了 100nF 加 10uF 电容问题解决。6.4 实操心得与避坑建议第一条心得MRAM 虽然写入无限次但不要把它当 SRAM 用。每次写操作都要发命令和地址有开销。如果数据量小且频繁可以在 RAM 里缓存攒够一批再写。比如每秒采集 100 次数据可以每秒写一次 MRAM把 100 条数据打包写入效率更高。第二条心得MR25H40CDF 的 HOLD 引脚一定要上拉。我有个项目忘了上拉结果偶尔读写失败查了很久才发现是 HOLD 被干扰拉低SPI 传输被暂停。后来加了 10k 上拉再没出过问题。第三条心得调试 SPI 的时候逻辑分析仪比示波器好用。逻辑分析仪能直接解码 SPI 协议看到命令、地址、数据一眼就能看出哪里不对。我用的是一款国产的 8 通道逻辑分析仪几百块钱解码 SPI 很稳定推荐入手一个。第四条心得MRAM 的读写速度很快但 STM32 的 HAL 库函数调用开销不小。如果追求极致速度可以直接操作 SPI 寄存器把 CS 控制、数据收发都写成宏或者内联函数。我实测直接操作寄存器比 HAL 库快 3 到 5 倍对于高速连续采集场景很有必要。7. 性能实测与优化方向7.1 读写速度实测数据我用 STM32F765ZI 在 216MHz 主频、SPI 时钟 13.5MHz 下实测连续读 512KB 数据耗时约 380ms平均读速度约 1.35MB/s。连续写 512KB 数据耗时约 420ms平均写速度约 1.22MB/s。如果把 SPI 时钟提到 27MHz读速度能到 2.6MB/s写速度到 2.4MB/s。再往上提到 40MHz速度提升不明显因为 STM32 的 SPI 和 DMA 开销成了瓶颈。对比 EEPROM同样写 512KBEEPROM 按 5ms 写周期算需要 2560 秒也就是 40 多分钟。MRAM 只要 0.4 秒差距是几千倍。这就是为什么工业高频记录场景必须用 MRAM。7.2 功耗表现与低功耗场景适配MR25H40CDF 的读写电流典型值 15mA待机电流 100uA 左右。STM32F765ZI 在 216MHz 下运行电流约 100mA加上 MRAM 的 15mA整体功耗不低。如果设备是电池供电需要优化。低功耗场景下可以这样处理STM32 平时进入 Stop 模式MRAM 的 CS 保持高电平MRAM 进入待机。需要记录数据时STM32 唤醒SPI 初始化写 MRAM然后继续休眠。MRAM 从待机到就绪的时间很短不影响实时性。实测这种模式下整体平均电流可以降到 1mA 以下电池寿命大幅延长。7.3 后续扩展思路这个方案后续可以扩展几个方向。一是加文件系统把 MRAM 当块设备挂载 FatFS 或者 LittleFS方便管理大量小文件。二是加加密写入前对数据加密读取时解密保护敏感数据。三是加双芯片冗余两片 MRAM 同时写读的时候对比提高可靠性。四是加无线传输通过蓝牙或者 LoRa 把存储的数据发出去实现远程监控。我个人觉得对于大多数工业数据记录场景单芯片加 CRC 校验已经足够。如果客户对可靠性要求极高再考虑双芯片冗余。文件系统的话如果数据条目固定直接按地址管理更简单高效不一定非要上文件系统。这个方案我在三个项目里用过最长的已经跑了两年多每天写入几十万次至今没出过数据丢失。MRAM 加 STM32 的组合在工业嵌入式存储领域确实是个省心的选择。