MRAM 这类器件在工业项目里属于典型的"参数不起眼、用起来真香"方案。MR25H40CDF 是一颗 4Mbit 的 SPI 接口 MRAM,配合 STM32F413RH 这颗带 320KB SRAM 的 Cortex-M4F 芯片,可以构建一套非常可靠的参数存储与日志记录通道。本篇文章我会从选型动机、硬件接线、SPI 时序、驱动代码到掉电保护和实测数据,尽量把关键细节一次讲透,覆盖掉那些 datasheet 里没写清楚的坑。
1. 为什么工业设备里我会把 Flash 和 EEPROM 换成 MRAM
1.1 Flash 在工业现场最让我头疼的三件事
做嵌入式这几年,我在设备维护、产线调试、故障追溯上踩过的坑,有一大半跟数据存储有关。第一类是 Flash 的擦写寿命问题,工业现场的变频器、伺服驱动器、仪器仪表这类设备,一天下来参数存储频次可以高达几万次,普通 SPI NOR Flash 的擦写寿命也就十万次级别,看起来够用,但日志密集型场景加上频繁上下电,寿命消耗实际非常快。第二类是掉电丢数据,很多系统对 3.3V 跌落时间的容忍度很低,代码刚写完 Flash 的 page program,电源就断了,现场排查起来极其痛苦。第三类是 EEPROM 容量太小、写得太慢,常见的 24Cxx 系列也就 256 字节到 64KB,扩展外置 EEPROM 还要占 I2C 总线和引脚,读写速度在高速数据采集场景下完全不够用。
1.2 MR25H40CDF 到底解决了什么问题
MR25H40CDF 是一颗 4Mbit(512KB)的 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 引脚 | 说明 |
|---|---|---|
| PB3(SPI1_SCK) | SCK | SPI 时钟 |
| PB4(SPI1_MISO) | SO | 从机输出 |
| PB5(SPI1_MOSI) | SI | 从机输入 |
| PA4(GPIO 推挽输出) | CS | 软件片选控制 |
| 3.3V 通过 10kΩ 上拉 | WP | 禁止写保护有效 |
| 3.3V 通过 10kΩ 上拉 | HOLD | 禁止保持功能触发 |
| 3.3V | VDD | 电源 |
| GND | GND | 地 |
WP 和 HOLD 这两个脚在大部分应用里都要接死到高电平。WP 拉高才能保证写保护功能关闭,HOLD 拉高才能保证 SPI 通信不会被意外暂停。如果这两个脚悬空,现场强干扰环境下很容易出现总线挂死、命令异常的情况,这个后面我在实测部分会专门讲。
2.2 SPI 命令集与状态寄存器的工作逻辑
MR25H40CDF 的命令集非常精简,核心命令就是下面这几个:
| 命令 | opcode | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读取状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
和普通 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 的主频最高可以跑到 100MHz,APB2 外设时钟也基本能到 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 对应 CPOL=0、CPHA=0,数据在时钟上升沿采样;SPI Mode 3 对应 CPOL=1、CPHA=1,数据在时钟上升沿采样。两者在空闲状态的电平不同,但采样沿一致。
实际工程中我习惯使用 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 的核心原因。