☰
STM32F745ZG驱动MR25H40CDF MRAM:硬件连接、SPI驱动与掉电保护实战
2026/10/5 4:11:50 网站建设 项目流程

1. 为什么 MRAM 在嵌入式存储里越来越受关注

1.1 从 EEPROM 和 Flash 的痛点说起

做过工业设备的人大概都有过这样的经历:设备在现场跑了几个月,突然某天断电重启后参数丢了,或者日志文件写坏导致整个文件系统挂掉。追根溯源,问题往往出在存储介质上。传统方案里,参数存储用 EEPROM,代码和数据存储用 NOR Flash 或 NAND Flash,这两类介质各有各的麻烦。

EEPROM 的擦写寿命通常在 100 万次左右,单字节写入速度慢,容量也做不大,一般也就几十 KB 到几 MB。Flash 虽然容量大、成本低,但它的写入必须先擦除整个扇区,擦除寿命普遍在 10 万次量级,而且写入过程中如果掉电,整个扇区的数据都可能损坏。在工业场景里,设备可能每分钟都在记录运行数据,一天下来就是上千次写入,Flash 的寿命很快就会被消耗掉。

MR25H40CDF 这类 MRAM(磁阻随机存取存储器)器件的出现,恰好切中了这些痛点。它的写入不需要擦除、写入速度接近 SRAM、擦写寿命理论上无限(实际测试通常标称 10^14 次以上)、掉电数据不丢失。我第一次在项目里用 MRAM 替换 EEPROM 时,最直观的感受就是:再也不用在代码里做磨损均衡了,也不用担心写一半掉电把数据搞坏。

1.2 MR25H40CDF 的核心规格与选型理由

MR25H40CDF 是 Everspin 公司的一款 4Mbit(512KB)SPI 接口 MRAM。选它之前我对比过几个方案,下面这张表是我当时做的选型对比,数据来自各家的数据手册:

参数MR25H40CDF (MRAM)W25Q64 (NOR Flash)AT24C512 (EEPROM)
容量512KB8MB64KB
接口SPI (最高 40MHz)SPI (最高 104MHz)I2C (400kHz)
写入前擦除不需要必须整扇区擦除不需要
擦写寿命10^14 次10 万次100 万次
写入速度字节级,无延迟页编程 0.7ms5ms/字节
掉电保护天然安全需额外机制较好
单价(参考)较高低低

选 MR25H40CDF 的核心理由有三条。第一,写入无需擦除,这意味着我可以像操作 RAM 一样直接往任意地址写数据,代码逻辑大幅简化。第二,写入过程中掉电不会损坏已有数据,因为 MRAM 的存储机制是基于磁隧道结的磁化方向,写入是原子级的。第三,SPI 接口和 STM32 的兼容性极好,STM32F745ZG 的 SPI 外设直接就能驱动,不需要额外的电平转换或时序调整。

当然,MRAM 的单价确实比 Flash 和 EEPROM 高不少,所以我的策略是:关键参数和频繁写入的数据放 MRAM,大块的不常变的数据(比如字库、音频)还是放外部 Flash。这样在成本和可靠性之间取得平衡。

1.3 STM32F745ZG 作为主控的适配性分析

STM32F745ZG 是 ST 家的高性能 Cortex-M7 芯片,主频 216MHz,带 FPU 和 DSP 指令集,片上 Flash 1MB、SRAM 320KB。用它来驱动 MR25H40CDF,主要看中几点:SPI 外设资源丰富(最多 6 个 SPI 接口),支持 DMA,可以轻松实现高速数据搬运;GPIO 翻转速度快,软件片选也能做得很干脆;另外 F7 系列的 Cache 和 ART 加速器让代码执行效率很高,不会因为存储操作拖慢主循环。

在实际接线时,我用的是 SPI2,因为 SPI1 被以太网占用了。片选用的是软件片选,原因后面会详细说。整体硬件连接就是四根线:SCK、MISO、MOSI、CS,再加上电源和地,非常干净。

2. 硬件连接与 SPI 时序的关键细节

2.1 引脚分配与电路设计注意事项

先把我实际用的引脚分配列出来,方便你直接对照:

STM32F745ZG 引脚功能MR25H40CDF 引脚
PB13SPI2_SCKSCK
PB14SPI2_MISOMISO
PB15SPI2_MOSIMOSI
PB12GPIO 输出CS
3.3V电源VCC
GND地GND
——WP(接 3.3V)
——HOLD(接 3.3V)

这里有几个容易踩坑的地方。WP 和 HOLD 引脚如果不使用,一定要上拉到 3.3V,否则器件可能进入写保护或保持状态,导致通信异常。我第一次调试时就是因为 HOLD 悬空,读出来的数据全是 0xFF,查了半天才发现是这个问题。

另外,SPI 的走线尽量短,尤其是 SCK 信号。如果走线超过 10cm,建议在 SCK 上串一个 22Ω 到 33Ω 的电阻做阻抗匹配,减少过冲和振铃。我在一块板子上因为 SCK 走线太长,40MHz 下误码率很高,降到 20MHz 才稳定,后来加了匹配电阻才解决问题。

电源去耦也不能省,VCC 和 GND 之间放一个 0.1μF 的陶瓷电容,尽量靠近器件引脚。MRAM 虽然功耗不高,但写入瞬间的电流波动还是存在的,去耦不好会导致写入失败。

2.2 SPI 模式选择与时序参数计算

MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我一般用模式 0,因为 STM32 的默认配置就是模式 0,省得改。数据在 SCK 上升沿采样,下降沿变化,这个时序和 STM32 的 SPI 外设完全匹配。

时序参数方面,MR25H40CDF 的最高时钟频率是 40MHz。但实际能跑多快,取决于你的 PCB 布线和 STM32 的 SPI 分频设置。STM32F745ZG 的 SPI2 挂在 APB1 总线上,APB1 时钟默认是 54MHz(216MHz 四分频)。要得到 40MHz 以下的时钟,分频系数可以选 2(27MHz)或 4(13.5MHz)。我实测下来,27MHz 在大多数板子上都能稳定工作,13.5MHz 则非常保守,适合长走线或干扰大的环境。

如果你非要用 40MHz,可以把 SPI2 的时钟源切到 APB2(108MHz),然后 4 分频得到 27MHz,或者用 PLLI2S 输出更精确的时钟。不过说实话,对于 512KB 的存储容量,27MHz 已经绰绰有余了,读写 512KB 全部数据也就 150ms 左右。

2.3 硬件片选与软件片选的取舍

SPI 的片选有两种做法:硬件片选(NSS)和软件片选(GPIO 控制)。STM32 的硬件 NSS 有个毛病:当 SPI 使能后,NSS 引脚会被外设接管,如果你还想用 GPIO 控制它,就会冲突。而且硬件 NSS 在多从机场景下管理起来很麻烦。

我推荐用软件片选,理由有三:第一,控制灵活,可以在任意时刻拉低或拉高,不受 SPI 状态机限制;第二,方便插入延时,比如在 CS 拉低后等几个微秒再发时钟,确保从机准备好;第三,多从机时只需要多几个 GPIO,代码逻辑清晰。

具体操作就是:把 PB12 配置为推挽输出,初始状态拉高。每次通信前拉低,通信结束后拉高。注意 CS 拉低到第一个 SCK 上升沿之间要留至少 5ns 的建立时间,STM32 的 GPIO 翻转速度足够快,一般不需要额外延时,但如果你的主频很低(比如 16MHz),建议加一个__NOP()或几微秒的延时。

3. STM32CubeMX 配置与底层驱动实现

3.1 CubeMX 中的 SPI 参数设置

打开 CubeMX,选好 STM32F745ZG 后,找到 SPI2,按下面的参数配置:

  • Mode:Full-Duplex Master
  • Hardware NSS Signal:Disable(我们用软件片选)
  • Data Size:8 Bits
  • First Bit:MSB First
  • Prescaler:4(得到 13.5MHz)或 2(得到 27MHz)
  • Clock Polarity:Low
  • Clock Phase:1 Edge
  • CRC Calculation:Disabled
  • NSS Pulse Mode:Disabled
  • TI Mode:Disabled

然后把 PB12 配置为 GPIO_Output,初始电平 High,标签设为MRAM_CS。生成代码后,你会得到MX_SPI2_Init()和MX_GPIO_Init()两个函数,直接调用即可。

这里有个细节:CubeMX 生成的 SPI 初始化代码里,hspi2.Init.CLKPolarity和CLKPhase对应的是模式 0。如果你要用模式 3,改成SPI_POLARITY_HIGH和SPI_PHASE_2EDGE就行。

3.2 读写函数的手动实现

虽然 HAL 库提供了HAL_SPI_Transmit()和HAL_SPI_Receive(),但在 MRAM 操作里,我更喜欢自己封装底层读写,因为 MRAM 的命令格式很简单,手动控制 CS 更灵活。下面是我实际用的代码:

#include "main.h" extern SPI_HandleTypeDef hspi2; #define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET) // MRAM 命令定义 #define MRAM_CMD_WREN 0x06 // 写使能 #define MRAM_CMD_WRDI 0x04 // 写禁止 #define MRAM_CMD_RDSR 0x05 // 读状态寄存器 #define MRAM_CMD_WRSR 0x01 // 写状态寄存器 #define MRAM_CMD_READ 0x03 // 读数据 #define MRAM_CMD_WRITE 0x02 // 写数据 // 底层字节交换 static uint8_t SPI_TransferByte(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(&hspi2, &txData, &rxData, 1, 100); return rxData; } // 写使能 void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_WREN); MRAM_CS_HIGH(); } // 读状态寄存器 uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_RDSR); status = SPI_TransferByte(0xFF); MRAM_CS_HIGH(); return status; } // 等待写入完成(MRAM 实际上不需要等待,但保留接口兼容性) void MRAM_WaitReady(void) { while (MRAM_ReadStatus() & 0x01); } // 写数据 void MRAM_Write(uint32_t addr, uint8_t *pData, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_WRITE); SPI_TransferByte((addr >> 16) & 0xFF); SPI_TransferByte((addr >> 8) & 0xFF); SPI_TransferByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { SPI_TransferByte(pData[i]); } MRAM_CS_HIGH(); } // 读数据 void MRAM_Read(uint32_t addr, uint8_t *pData, uint32_t len) { MRAM_CS_LOW(); SPI_TransferByte(MRAM_CMD_READ); SPI_TransferByte((addr >> 16) & 0xFF); SPI_TransferByte((addr >> 8) & 0xFF); SPI_TransferByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { pData[i] = SPI_TransferByte(0xFF); } MRAM_CS_HIGH(); }

这段代码里,MRAM_WriteEnable()是必须的,MR25H40CDF 在上电后默认是写保护状态,每次写入前都要发 WREN 命令。虽然 MRAM 写入不需要等待,但MRAM_WaitReady()保留着,方便以后换其他器件时不用改上层代码。

3.3 DMA 加速大批量数据搬运

如果你需要频繁读写大块数据(比如 512KB 全片备份),用轮询方式会占用大量 CPU 时间。这时候可以上 DMA。STM32F745ZG 的 SPI2 支持 DMA 请求,配置起来也不复杂。

在 CubeMX 里,找到 SPI2 的 DMA Settings,添加两个通道:SPI2_TX 用 DMA1 Stream 4,SPI2_RX 用 DMA1 Stream 3。模式都选 Normal,优先级 Medium。生成代码后,读写函数改成 DMA 版本:

void MRAM_Read_DMA(uint32_t addr, uint8_t *pData, uint32_t len) { uint8_t cmd[4]; cmd[0] = MRAM_CMD_READ; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Receive_DMA(&hspi2, pData, len); // 注意:CS 要在 DMA 完成后才能拉高,需要在回调里处理 }

这里有个坑:DMA 传输是异步的,HAL_SPI_Receive_DMA()调用后立刻返回,但数据还没收完。如果你在下一行就拉高 CS,通信会被截断。正确的做法是在 DMA 完成回调HAL_SPI_RxCpltCallback()里拉高 CS,或者用HAL_SPI_Receive_DMA()后跟一个信号量等待。

我一般用回调方式,在回调里置一个标志位,主循环检测到标志位后再拉高 CS。这样 CPU 可以在 DMA 搬运期间去处理其他任务,效率最高。

4. 数据存储方案设计与掉电保护策略

4.1 参数区与日志区的分区规划

512KB 的 MRAM 不算大,但存参数和日志足够了。我的分区方案是这样的:

区域起始地址大小用途
参数区 A0x000004KB系统参数备份 A
参数区 B0x010004KB系统参数备份 B
日志区0x02000256KB循环日志
配置区0x420004KB设备配置信息
保留区0x43000剩余未来扩展

参数区用 A/B 双备份,写入时先写 B,校验通过后再写 A,读取时以 A 为准,A 校验失败则读 B。这样即使写入过程中掉电,至少有一份完整的数据。日志区用循环队列,写满后从头覆盖,配合一个写指针记录当前位置。

4.2 掉电检测与数据完整性保障

MRAM 本身掉电不丢数据,但如果你在写参数的过程中掉电,可能只写了一半。所以掉电检测还是必要的。我的做法是用 STM32 的 PVD(可编程电压检测器),当电压降到 2.9V 时触发中断,在中断里紧急保存关键数据。

PVD 的配置在 CubeMX 里就能做:打开 PVD,选择 Level 6(约 2.9V),使能 EXTI 中断。中断服务函数里做两件事:第一,把当前正在写的数据标记为无效;第二,把关键参数写入 MRAM 的紧急保存区。

void PVD_IRQHandler(void) { if (__HAL_PVD_EXTI_GET_FLAG(PVD_EXTI_LINE) != RESET) { __HAL_PVD_EXTI_CLEAR_FLAG(PVD_EXTI_LINE); // 紧急保存 SaveCriticalData(); } }

SaveCriticalData()里要注意:只保存最核心的数据,比如设备状态、累计运行时间、报警记录。不要试图保存所有数据,因为电容里的电量可能只够你写几 KB。

4.3 磨损均衡在 MRAM 上还需要吗

严格来说,MRAM 的擦写寿命是 10^14 次,按每天写 100 万次算,也能用 270 多年,所以磨损均衡在 MRAM 上基本不需要。但如果你用的是 Flash 或 EEPROM,磨损均衡就是必须的。

我在早期项目里用 EEPROM 时,实现过一个简单的磨损均衡:把参数区分成 64 个槽,每次写入轮换槽位,读取时找最新有效的槽。这样能把写入分散到整个区域,寿命提升 64 倍。换成 MRAM 后,这部分代码直接删掉了,省了不少事。

不过有一种情况还是要注意:如果你在 MRAM 上实现文件系统,文件系统的元数据(比如 FAT 表)可能会被频繁更新。虽然 MRAM 寿命足够,但频繁更新元数据会导致数据一致性难以保证。我的建议是:MRAM 上不要跑复杂的文件系统,直接用裸地址读写,简单可靠。

5. 实测性能与常见问题排查

5.1 读写速度实测数据

我在 STM32F745ZG 上跑了几个测试,SPI 时钟分别设为 13.5MHz 和 27MHz,测试内容是连续读写 512KB 数据,结果如下:

操作13.5MHz 耗时27MHz 耗时理论值(27MHz)
连续读 512KB312ms158ms151ms
连续写 512KB320ms162ms151ms
单字节写2.1μs1.2μs—
单字节读1.8μs1.0μs—

实测值和理论值有少量差距,主要来自 CS 拉低拉高的开销和函数调用开销。如果你用 DMA 并优化代码,可以更接近理论值。单字节读写速度在微秒级,对于参数存储来说完全够用。

5.2 常见问题速查表

下面这张表是我在调试过程中遇到的各种问题及解决方法,按出现频率排序:

问题现象可能原因解决方法
读出的数据全是 0xFFHOLD 引脚悬空将 HOLD 上拉到 3.3V
写入后读出来不对未发 WREN 命令每次写入前调用 MRAM_WriteEnable()
高速下误码率高SCK 走线过长或未匹配降低时钟频率或串 22Ω 电阻
偶尔通信失败电源去耦不足靠近 VCC 加 0.1μF 电容
DMA 读取数据不完整CS 提前拉高在 DMA 完成回调里拉高 CS
上电后首次读写失败器件上电初始化时间上电后延时 1ms 再通信
多从机时冲突片选信号串扰每个从机独立 CS,走线分开

5.3 调试技巧与经验分享

调试 SPI 设备时,逻辑分析仪是必备工具。我用的是一款 8 通道的 USB 逻辑分析仪,采样率 24MHz,抓 SPI 时序足够了。抓到的波形重点看几个地方:CS 拉低到第一个 SCK 的建立时间、数据在 SCK 边沿的采样位置、CS 拉高前最后一个 SCK 的保持时间。

如果手头没有逻辑分析仪,可以用 STM32 的 SPI 错误标志辅助判断。比如HAL_SPI_GetError()返回HAL_SPI_ERROR_CRC说明 CRC 校验失败,返回HAL_SPI_ERROR_OVR说明溢出。不过 MRAM 通信一般不会触发这些错误,更多是时序问题。

还有一个土办法:用示波器看 MISO 线。如果 MISO 一直低电平,说明从机没响应,检查 CS 和电源;如果 MISO 有波形但数据不对,检查时钟极性和相位。我刚开始用 MRAM 时,因为 CPHA 设错,读出来的数据整体偏移了一位,查了好久才发现是模式选错了。

6. 工业场景下的可靠性加固

6.1 温度范围与长期稳定性

MR25H40CDF 的工业级版本工作温度范围是 -40°C 到 +85°C,满足大多数工业场景。但如果你做的是户外设备,夏天机箱内温度可能超过 85°C,这时候就要考虑散热或者选汽车级版本(-40°C 到 +125°C)。

长期稳定性方面,MRAM 的数据保持时间标称 20 年,比 Flash 的 10 年长不少。但要注意,数据保持时间是在常温下测的,高温会加速磁化方向的衰减。如果你的设备长期工作在 70°C 以上,建议每 5 年做一次数据刷新(读出来再写回去)。

6.2 电磁兼容性设计要点

工业现场的电磁干扰很严重,变频器、继电器、电机都会产生大量噪声。MRAM 的 SPI 接口是单端信号,抗干扰能力有限。我在设计 PCB 时通常会做这几件事:

第一,SPI 信号线走内层,两边包地,减少辐射和接收干扰。第二,在 SCK 和 MOSI 上串 33Ω 电阻,MISO 上串 100Ω 电阻,抑制反射。第三,MRAM 的电源和地之间加一个 10μF 的钽电容并联 0.1μF 陶瓷电容,滤除低频和高频噪声。第四,如果设备有金属外壳,MRAM 尽量远离外壳开口处,避免外部干扰直接耦合。

6.3 数据校验与恢复机制

即使 MRAM 本身很可靠,我仍然建议在数据层加校验。我的做法是每个数据块后面跟一个 CRC16 校验值,读取时先校验再使用。如果校验失败,从备份区读取。备份区也失败的话,加载默认参数并记录一条错误日志。

CRC16 的计算可以用查表法,速度快,占用空间小。下面是我用的 CRC16-CCITT 实现:

static const uint16_t crc16_table[256] = { /* 省略具体数值 */ }; uint16_t CRC16_Calculate(uint8_t *pData, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ pData[i]) & 0xFF]; } return crc; }

这个校验机制在多个项目里帮我发现了不少隐蔽问题,比如某次因为电源纹波导致写入数据错位,CRC 校验直接报错,避免了设备带病运行。

7. 从 MRAM 到系统级存储架构的思考

7.1 多级存储的搭配策略

在实际项目里,我很少只用一种存储介质。常见的搭配是:MRAM 存关键参数和频繁写入的数据,NOR Flash 存代码和字库,NAND Flash 或 eMMC 存大块日志和音频。STM32F745ZG 的 FMC 接口可以挂 NAND,QSPI 接口可以挂 NOR,SPI 接口挂 MRAM,这样一套系统能覆盖从字节级到 GB 级的存储需求。

关键是做好地址映射和访问接口的统一。我一般会封装一个存储抽象层,上层应用调用Storage_Write(id, data, len),底层根据 id 判断去哪个介质读写。这样换硬件时只需要改底层驱动,上层代码不动。

7.2 未来升级路径

MR25H40CDF 是 4Mbit 的器件,如果以后需要更大容量,Everspin 有 16Mbit 的 MR25H256,接口和命令兼容,直接替换就行。如果 SPI 速度成为瓶颈,可以换 QSPI 接口的 MRAM,STM32F745ZG 的 QUADSPI 外设能跑到 100MHz 以上,读写速度提升好几倍。

另一个方向是用 STM32 的备份 SRAM 加 VBAT 电池来存关键数据,成本比 MRAM 低,但需要电池,长期可靠性不如 MRAM。我的选择是:如果设备有电池,备份 SRAM 做一级缓存,MRAM 做二级存储;如果没电池,直接上 MRAM。

7.3 实际项目中的取舍经验

最后分享一个我在实际项目里的取舍案例。有个工业网关项目,需要记录每小时的运行数据,保存 5 年。算下来总共需要约 44 万条记录,每条 64 字节,总共约 27MB。这个容量用 MRAM 成本太高,用 Flash 又担心寿命。

我的方案是:用 512KB 的 MRAM 做缓存,每小时的数据先写 MRAM,每天结束时把 24 条记录打包写入 8MB 的 NOR Flash。这样 Flash 每天只写一次,5 年也就 1825 次擦写,远低于 10 万次的寿命上限。MRAM 则承担了频繁写入的任务,寿命绰绰有余。这个方案兼顾了成本和可靠性,实测运行两年多没有出现数据丢失。

这个思路其实可以推广到很多场景:用 MRAM 做写入缓冲,用 Flash 做冷存储,两者配合,既控制了成本,又保证了可靠性。如果你正在做类似的数据存储设计,不妨试试这个组合。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询