
MRAM 这类器件在工业项目里属于典型的参数不起眼、用起来真香方案。MR25H40CDF 是一颗 4Mbit 的 SPI 接口 MRAM配合 STM32F413RH 这颗带 320KB SRAM 的 Cortex-M4F 芯片可以构建一套非常可靠的参数存储与日志记录通道。本篇文章我会从选型动机、硬件接线、SPI 时序、驱动代码到掉电保护和实测数据尽量把关键细节一次讲透覆盖掉那些 datasheet 里没写清楚的坑。1. 为什么工业设备里我会把 Flash 和 EEPROM 换成 MRAM1.1 Flash 在工业现场最让我头疼的三件事做嵌入式这几年我在设备维护、产线调试、故障追溯上踩过的坑有一大半跟数据存储有关。第一类是 Flash 的擦写寿命问题工业现场的变频器、伺服驱动器、仪器仪表这类设备一天下来参数存储频次可以高达几万次普通 SPI NOR Flash 的擦写寿命也就十万次级别看起来够用但日志密集型场景加上频繁上下电寿命消耗实际非常快。第二类是掉电丢数据很多系统对 3.3V 跌落时间的容忍度很低代码刚写完 Flash 的 page program电源就断了现场排查起来极其痛苦。第三类是 EEPROM 容量太小、写得太慢常见的 24Cxx 系列也就 256 字节到 64KB扩展外置 EEPROM 还要占 I2C 总线和引脚读写速度在高速数据采集场景下完全不够用。1.2 MR25H40CDF 到底解决了什么问题MR25H40CDF 是一颗 4Mbit512KB的 SPI 接口 MRAM由 Everspin 出品工作电压 2.7~3.6V支持 SPI Mode 0 和 Mode 3最高时钟 40MHz。MRAM 的核心存储单元是磁隧道结MTJ通过磁化方向而不是电荷来保存数据所以它天然具备了两个让嵌入式工程师眼前一亮的特点无限次擦写以及掉电后数据不丢失。没有擦除周期概念也不需要页擦除、扇区擦除这种操作写操作一进去就生效真正做到了类 SRAM 的使用体验。从数据手册指标看MR25H40CDF 的写入时间远低于传统 NOR Flash 的 page program 时间后者的页编程通常要几十微秒到几毫秒而 MRAM 的写周期基本等同于一次普通 SPI 写命令的时序开销。数据保持能力官方标称为 20 年以上相比 SRAM电池后备方案彻底摆脱了电池维护问题。温度范围也覆盖了工业级常见的 -40℃ 到 105℃对于户外机柜、高温车间这类场景非常合适。1.3 一句话总结选型思路如果系统里需要频繁写入参数、记录事件日志、防止掉电丢数据而且容量需求在几 KB 到 512KB 之间MR25H40CDF 就是一个非常自然的中间选择。它比 EEPROM 容量大、速度快比 NOR Flash 写入简单、不需要擦写均衡和坏块管理比 SRAM后备电池维护成本低。配合 STM32F413RH 的高速 SPI 接口可以做到上电即读、随时可写、断电无忧的效果。2. MR25H40CDF 的数据手册细节接线前必须搞懂的引脚与时序2.1 引脚定义与硬件接线方案MR25H40CDF 的封装是 SOP-8引脚不多典型引脚包括 SI串行输入、SO串行输出、SCK时钟、CS片选、WP写保护、HOLD保持。和普通 SPI Flash 的引脚定义几乎一致区别主要在于 WP 和 HOLD 这两个脚在 MRAM 上的逻辑作用更加明确。我常用的接线方案如下表所示STM32F413RH 引脚MR25H40CDF 引脚说明PB3SPI1_SCKSCKSPI 时钟PB4SPI1_MISOSO从机输出PB5SPI1_MOSISI从机输入PA4GPIO 推挽输出CS软件片选控制3.3V 通过 10kΩ 上拉WP禁止写保护有效3.3V 通过 10kΩ 上拉HOLD禁止保持功能触发3.3VVDD电源GNDGND地WP 和 HOLD 这两个脚在大部分应用里都要接死到高电平。WP 拉高才能保证写保护功能关闭HOLD 拉高才能保证 SPI 通信不会被意外暂停。如果这两个脚悬空现场强干扰环境下很容易出现总线挂死、命令异常的情况这个后面我在实测部分会专门讲。2.2 SPI 命令集与状态寄存器的工作逻辑MR25H40CDF 的命令集非常精简核心命令就是下面这几个命令opcode说明WREN0x06写使能WRDI0x04写禁止RDSR0x05读取状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据和普通 SPI NOR Flash 最大的区别是它没有 0xD8扇区擦除、0xC7芯片擦除这类命令。需要覆盖写时直接发送 WRITE 命令即可不需要先擦除。状态寄存器的低两位中包括我们最关心的 BUSY 位但 MRAM 的 BUSY 窗口通常极短甚至可以不用轮询不过为了时序严谨驱动里我还是保留了忙检查逻辑。地址是 24 位格式芯片总容量 512KB实际有效地址位是 A18~A0高 5 位地址线可以视为忽略位或必须为 0。发送 READ 或 WRITE 命令时先发 opcode再发 3 字节地址紧接着传数据。读和写的基本时间序列如下CS 拉低 → 发送 0x03 → 发送 24bit 地址 → 连续读取数据 → CS 拉高 CS 拉低 → 发送 0x02 → 发送 24bit 地址 → 连续写入数据 → CS 拉高2.3 模/数混合环境中的去耦与布局建议工业板卡上极少只有一颗 MCU 和一颗 MRAM大概率还有继电器、电机驱动、开关电源模块。MRAM 虽然抗干扰能力不错但 SPI 总线属于高速数字信号电源噪声过大依然会导致读写错误。我的经验是在 MR25H40CDF 的 VDD 引脚旁边放置一个 0.1μF 陶瓷电容并且在稍远位置并联一个 10μF 钽电容或陶瓷电容用于低频去耦MRAM 尽量靠近 MCU缩短 SCK 和 MOSI/MISO 走线长度如果 PCB 空间允许SPI 信号线做等长处理时钟线尽量不穿过大电流走线区域。HOLD 和 WP 的上拉电阻靠近 MRAM 引脚端放置而不是靠近 MCU 端这样能最大程度避免引脚拾取噪声。3. STM32F413RH 上 SPI 外设的工程化配置3.1 选 SPI1 还是 SPI2以及分频系数怎么算STM32F413RH 内部有多个 SPI 外设SPI1 挂在 APB2 总线上SPI2/SPI3 挂在 APB1 总线上。我选择 SPI1是因为 APB2 的时钟频率更高可以给 SPI 留出更高分频上限。STM32F413 的主频最高可以跑到 100MHzAPB2 外设时钟也基本能到 100MHz。MR25H40CDF 最高支持 40MHz 时钟但 STM32F4 系列的 SPI 预分频器只有 2、4、8、16、32、64、128、256 这几个档位100MHz 除以 2 等于 50MHz已经超出 MRAM 的额定值除以 4 等于 25MHz是稳定且留有余量的选择。25MHz 的时钟频率意味着每秒钟最多可以搬移约 3MB 数据对于 512KB 容量的器件来说读写完整片不到 200 毫秒实际场景足够用。这里顺带提一句不要为了追求 40MHz 极限去把 SPI 配在 50MHz超规格运行带来的误码隐患在工业现场会被无限放大。3.2 SPI1 的初始化代码与 GPIO 复用配置下面这段代码基于 HAL 库实际工程中可以直接整理成mx_spi1_init()函数。GPIO 复用模式需要打开 SPI1 的 SCK/MOSI/MISO片选 CS 用普通推挽输出即可。static void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 7; HAL_SPI_Init(hspi1); } static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }从 STM32F413 的数据手册看PB3、PB4、PB5 的复用功能编号是 AF5对应 SPI1_SCK、SPI1_MISO、SPI1_MOSI。复用函数设置错误是最容易踩的坑我见过不少人把 pin 分配到了 AF 其他编号然后 SPI 怎么调都不通。3.3 SPI 模式选择Mode 0 与 Mode 3 都能用吗MR25H40CDF 数据手册明确支持 Mode 0 和 Mode 3。SPI Mode 0 对应 CPOL0、CPHA0数据在时钟上升沿采样SPI Mode 3 对应 CPOL1、CPHA1数据在时钟上升沿采样。两者在空闲状态的电平不同但采样沿一致。实际工程中我习惯使用 Mode 0理由很简单大多数 SPI 外设和逻辑分析仪默认分析 Mode 0 或 Mode 0/3排查波形时少一层换算。HAL 库初始化代码里CLKPolarity SPI_POLARITY_LOW、CLKPhase SPI_PHASE_1EDGE就是 Mode 0 的配置。如果换用其他 MRAM 型号比如 MR25H40MDF 这类后缀不同的型号仍然建议先确认数据手册的时序图不要默认沿用上一颗芯片的 SPI 模式配置。4. 驱动代码实现从轮询到 DMA 的完整落地4.1 基础读写函数与片选控制整个 MRAM 驱动可以拆成三层片选控制、命令发送、数据读写。片选由 GPIO 手动控制这是绝大多数 SPI 从机设备的通用做法软件控制的时间点最灵活。#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02这里有一个很关键的细节每次操作完成之后CS 必须拉高。SPI 从机通过 CS 的上升沿来判断命令结束和完成内部状态更新。如果 CS 一直拉低下一次操作发送的 opcode 会被当成上一段数据流的延续命令解析必定错乱。4.2 写使能与状态轮询MRAM 的写操作之前需要发送 WREN 命令将状态寄存器里的写使能锁存位置位。从数据手册时序角度看WREN 完成后紧接着就可以发 WRITE 命令但稳妥起见我在代码里加入了读取状态寄存器进行确认的步骤用于调试阶段快速定位问题。static void mram_write_enable(void) { uint8_t cmd MRAM_CMD_WREN; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); MRAM_CS_HIGH(); } static uint8_t mram_read_status(void) { uint8_t cmd MRAM_CMD_RDSR; uint8_t sr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, sr, 1, HAL_MAX_DELAY); MRAM_CS_HIGH(); return sr; } static void mram_wait_ready(void) { while ((mram_read_status() 0x01) ! 0) { /* busy 等待MRAM 通常极快此循环一般只会进入一次 */ } }状态寄存器的 bit0 是 BUSY 位。对 MRAM 来说这个位可能短到观察不到高电平但保留轮询逻辑可以在市场批次差异、温度变化时提供保护。对于性能敏感的场景可以在轮询条件里加超时计数避免由于 SPI 线路异常导致 while 死循环。4.3 读取数据和写入数据的核心逻辑读操作的流程是片选拉低、发送 READ 命令、发送 3 字节地址、连续接收数据、片选拉高。写操作则是写使能、片选拉低、发送 WRITE 命令、发送地址、连续发送数据、片选拉高。void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t hdr[4]; hdr[0] MRAM_CMD_READ; hdr[1] (addr 16) 0xFF; hdr[2] (addr 8) 0xFF; hdr[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); MRAM_CS_HIGH(); } void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t hdr[4]; hdr[0] MRAM_CMD_WRITE; hdr[1] (addr 16) 0xFF; hdr[2] (addr 8) 0xFF; hdr[3] addr 0xFF; mram_write_enable(); mram_wait_ready(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(hspi1, (uint8_t *)buf, len, HAL_MAX_DELAY); MRAM_CS_HIGH(); }4.4 DMA 模式的改造与注意事项对于大批量日志写入轮询模式的效率瓶颈在于 CPU 每个字节都要参与。DMA 模式可以把 SPI 搬运任务的 CPU 占用率降下来。HAL 库下推荐使用HAL_SPI_Transmit_DMA和HAL_SPI_Receive_DMA但要注意全双工 SPI 在连续收发时需要同时启动收发通道否则接收端时钟缺失。这里给一个简单可靠的 DMA 读写方案void mram_write_dma(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t hdr[4]; hdr[0] MRAM_CMD_WRITE; hdr[1] (addr 16) 0xFF; hdr[2] (addr 8) 0xFF; hdr[3] addr 0xFF; mram_write_enable(); mram_wait_ready(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Transmit_DMA(hspi1, (uint8_t *)buf, len); /* 等待 DMA 传输完成中断或 HAL_SPI_GetState 回到 READY */ while (HAL_SPI_GetState(hspi1) ! HAL_SPI_STATE_READY) {} MRAM_CS_HIGH(); }DMA 方式下最容易忽略的问题是 CS 拉高的时机。HAL_SPI_Transmit_DMA 是非阻塞的如果不等 DMA 传输结束就把 CS 拉高从机会提前认为数据流结束导致最后一个字节甚至最后一段数据被截断。我一般会在传输完成中断回调里置一个标志位主流程轮询这个标志位或者在函数末尾等待HAL_SPI_GetState恢复 READY 状态。5. 掉电保存与数据完整性的设计思路5.1 用 STM32F413RH 的 PVD 检测电源跌落工业设备掉电不是瞬间从 3.3V 跳到 0V而是一个缓慢下降的过程。如果能在电源跌落到 MCU 最低工作电压之前把关键数据写进 MRAM就能避免现场数据丢失。STM32F413RH 内部集成了可编程电压检测器PVD可配置多个电压阈值。当 VDD 降到阈值以下时会产生 PVD 中断。实际项目中我通常把阈值配置在 2.9V 左右配合电源输入端的大容量电容可以在掉电中断触发后获得几十毫秒甚至上百毫秒的裕量这足够写完一条 4KB 的日志记录。PVD 初始化代码如下static void MX_PVD_Init(void) { PWR_PVDTypeDef sConfigPVD {0}; sConfigPVD.PVDLevel PWR_PVDLEVEL_7; /* 约 2.9V具体值参考芯片手册 */ sConfigPVD.Mode PWR_PVD_MODE_IT_RISING_FALLING; HAL_PWR_ConfigPVD(sConfigPVD); HAL_PWR_EnablePVD(); HAL_NVIC_SetPriority(PVD_IRQn, 15, 0); HAL_NVIC_EnableIRQ(PVD_IRQn); }5.2 写入流程中的防撕裂与先备份策略即使有 MRAM 的瞬时写入能力也不能认为掉电高枕无忧。一次写入如果横跨多个字节而掉电发生在写入过程中间可能会留下半条有效记录。MRAM 是字节级非易失写入掉电瞬间已经写入的字节会保留未写入的字节保持不变所以需要设计合理的记录格式来保证数据完整性。我常用的做法是双区交替写入 序号校验。每条记录头部放一个 magic number 和递增序号写入时交替写入 A 区和 B 区启动时读取两个区比较序号取序号连续且校验正确的一份。这样做的好处是即使某次写入只完成了一半另一份完整记录还在系统可以自动回退到最近一次有效状态。typedef struct __attribute__((packed)) { uint32_t magic; uint32_t seq; uint16_t crc16; uint8_t payload[256]; } log_record_t;CRC16 校验是必备的。MRAM 本身误码率很低但 SPI 总线噪声、外部干扰仍可能破坏数据加一层 CRC 能在读取失效时快速识别并进入恢复流程。5.3 上电恢复与坏区处理上电后第一步不是直接使用主存储区数据而是读取双区记录头比较 magic 和 seq。如果 A 区有效且比 B 区新就采用 A 区如果 B 区有效且比 A 区新则采用 B 区如果两份都无效则进入出厂默认配置。MRAM 没有坏块概念所以不需要像 NAND Flash 那样做坏块管理。但写入地址仍然存在边界问题读写跨 512KB 地址回绕时如果命令设置不当会从 0x00000 重新开始这一点在长数据连续读写时尤其要注意。6. 实测结果与现场踩过的坑6.1 25MHz 时钟下的实际吞吐表现我在自制测试板上用 STM32F413RH 100MHz、SPI1 25MHz、DMA 模式实测连续写入 4KB 缓存耗时大约 1.4ms连续读取 4KB 大约 1.2ms其中大部分开销在 SPI 时钟搬运。如果使用轮询模式读 4KB 大概要 2.2ms主要差在 HAL 层每次收发函数的调用开销。坦白说对于 512KB 的容量和 25MHz 的通信速率这个表现对大多数工业应用是绰绰有余的。真正该关注的是写循环效率比如每秒写入 100 条 60 字节的记录MRAM 可以轻松应付而同样的频率对 NOR Flash 会显著加速寿命衰减。6.2 坑HOLD 脚悬空导致的总线假死第一次打样时我把 HOLD 脚留了 NC结果系统运行一段时间后SPI 通信会偶发停止主控读回来的数据全部是 0xFF。逻辑分析仪抓波形发现SCK 和 MOSI 都有正常信号但 SO 一直是高电平。后来查资料才意识到HOLD 脚悬空时周围辐射噪声可能让该引脚瞬时落入低电平MRAM 进入 hold 状态忽略后续 SPI 输入直到 CS 重新拉低或者 HOLD 恢复高电平。解决办法就是一开始就接 10kΩ 上拉到 3.3V同时 HOLD 引脚走线远离继电器和大电流回路。此后再没有出现过类似假死问题。6.3 坑WP 脚没拉高导致写使能不生效另一个朋友调试时反馈写命令发下去后读回来的数据一直是 0xAA 的默认值WREN 也发了状态寄存器的写使能位就是不为 1。排查到最后发现 WP 脚接到了 MCU 的一个 GPIO而 GPIO 默认输出低电平导致硬件写保护一直使能。MR25H40CDF 的写保护优先级高于软件写使能这种情况下 WREN 无论如何都不会生效。这个问题在测试阶段很容易被忽略因为 MRAM 型号众多部分型号对 WP 的策略并不完全一致。我的建议是不需要动态切换保护时直接把 WP 接高电平别省这个电阻。6.4 坑自以为支持任意长度连续读写MR25H40CDF 确实支持突发模式连续读写但地址计数器走到 0x7FFFF 之后会回绕到 0x00000。如果上层协议没有留意长度计算写一条跨地址边界的日志会出现低地址段被覆盖的惨状。我的处理是所有跨区写入都按单次不超过 4KB 的数据块拆分并且强制从对齐地址开始写从根上规避回绕问题。6.5 给新项目的上手建议如果第一次在项目里用 MRAM建议先做四件事用逻辑分析仪抓一次 WREN 后状态寄存器的变化用固定 pattern如 0x55、0xAA写满前 16KB 再读回比对断电瞬间重复写擦测试 100 次确认 PVD 触发逻辑可靠将 SPI 时钟从 25MHz 降到 8MHz 跑一次长稳定性测试验证干扰环境下的边角时序。另外MR25H40CDF 的 READ/WRITE 命令时序简单在现有项目中甚至可以直接替换部分 NOR Flash 芯片只需把擦除命令去掉、删除写入前的擦除流程即可硬件改动非常小。这也是我后来在多个项目里快速铺开 MRAM 的核心原因。