1. 为什么要在工业嵌入式场景里认真聊聊 MR25H40CDF 这颗 MRAM
工业现场的数据存储有个很尴尬的夹缝地带:需要频繁写、掉电不能丢、还要扛得住高低温循环和振动,但数据量又没大到值得上文件系统加 eMMC 的程度。传统方案里,EEPROM 擦写寿命撑不住高频写,NOR Flash 有擦除块限制、写入前得先擦、掉电窗口还容易丢数据,FRAM 容量小价格高。MR25H40CDF 这颗 4Mbit 的 SPI MRAM 恰好卡在这个位置上,它没有擦除概念、字节级写入、写入几乎无延迟、擦写次数以百亿次计,配合 STM32F745VG 这种带 Quad-SPI 和充足 RAM 的 Cortex-M7 主控,能搭出一套非常省心的工业数据存储链路。
这篇内容我打算按实际项目落地的顺序来讲:先拆清楚为什么选 MRAM 而不是 Flash 或 EEPROM,再把 MR25H40CDF 的 SPI 时序、状态寄存器、写保护机制讲透,然后给出 STM32F745VG 上用 HAL 库驱动它的完整实操过程,包括 CubeMX 配置、读写函数、掉电保护策略,最后把我在现场踩过的坑整理成排查表。适合正在做工业采集、PLC 扩展模块、电力监测终端、嵌入式数据记录仪的硬件和固件工程师参考,也适合刚接触 SPI 外设、想找一个比 W25Q64 更有意思的存储器件来练手的嵌入式学习者。
MR25H40CDF 是 Everspin 的 4Mbit SPI 非易失 MRAM,512K 字节容量,工作电压 2.7V 到 3.6V,SPI 模式 0 和模式 3 都支持,最高时钟 40MHz。它和普通 SPI Flash 最大的区别在于:写操作不需要擦除、不需要等待写完成轮询、没有扇区概念,你发完写命令和地址数据,数据就进去了。这个特性直接改变了固件里数据存储层的设计思路,后面会展开讲。
2. 存储方案选型:MRAM、NOR Flash、EEPROM 到底怎么选
2.1 三种非易失存储的物理机制差异
要理解选型,得先明白三者写入原理的根本不同。NOR Flash 靠浮栅晶体管存储电荷,写 1 是把电荷注入浮栅,写 0 是把电荷抽走,但抽走电荷这个动作只能按块进行,所以有了"擦除块"的概念,典型擦除块 4KB,擦一次要几十毫秒。EEPROM 同样是浮栅结构,但每个字节有独立的擦除晶体管,所以能字节级擦写,代价是单元面积大、容量做不大、写入速度慢,典型字节写入时间 5ms 量级。
MRAM 走的是完全不同的路线,它用磁性隧道结存储数据,两个铁磁层之间夹一层极薄绝缘层,通过改变自由层的磁化方向来改变隧道结电阻,高阻代表 1,低阻代表 0。磁化方向翻转靠的是自旋极化电流,不需要电荷注入和抽走,所以写入是纯电流操作,没有擦除前置动作,写入时间在纳秒到几十纳秒级别。这个物理机制决定了 MRAM 的三个核心优势:字节级写入、写入无延迟、擦写寿命极高。
2.2 关键参数横向对比
下面这张表是我在选型阶段实际整理过的,参数取自各器件手册的典型值,方便你直接对照。
| 参数项 | MR25H40CDF (MRAM) | 典型 NOR Flash (W25Q64) | 典型 EEPROM (AT24C512) |
|---|---|---|---|
| 容量 | 512KB | 8MB | 64KB |
| 接口 | SPI 最高 40MHz | SPI 最高 104MHz | I2C 400kHz/1MHz |
| 写入粒度 | 字节 | 页 256B,擦除 4KB | 字节 |
| 写入前擦除 | 不需要 | 必须 | 不需要 |
| 字节写入时间 | 约 30ns | 页编程约 0.7ms | 约 5ms |
| 擦写寿命 | 10^14 次量级 | 约 10^5 次 | 约 10^6 次 |
| 数据保持 | 20 年以上 | 20 年 | 100 年 |
| 掉电写入风险 | 极低 | 高(擦除窗口) | 中 |
| 单价(同容量折算) | 高 | 低 | 中 |
从表里能看出,MR25H40CDF 的定位非常明确:容量不大,但写入性能、寿命、掉电安全性全面碾压。如果你的场景是每秒写几十次、每次写几十字节的工业参数记录,Flash 的 10 万次寿命可能几个月就写穿了,而 MRAM 基本可以认为写不坏。
2.3 什么场景该选 MRAM
我总结了几条判断标准,符合两条以上就值得考虑 MR25H40CDF。第一,写入频率高,比如每秒采集一次并落盘,一天就是 8 万多次写。第二,掉电瞬间必须保住最后一条数据,比如电力故障录波、计量冻结数据。第三,工作温度范围宽,-40 到 85 度甚至更高,MRAM 的磁存储对温度不敏感,比浮栅器件稳。第四,数据量在几百 KB 以内,不需要文件系统。第五,系统对写入延迟敏感,不能容忍 Flash 那种毫秒级擦除阻塞。
反过来,如果你要存固件、存图片、存日志文件,容量需求上兆,那还是老老实实用 NOR Flash 或 eMMC,MRAM 的每比特成本扛不住。
3. MR25H40CDF 的 SPI 时序与寄存器细节拆解
3.1 SPI 模式与时钟极性的选择
MR25H40CDF 支持 SPI 模式 0(CPOL=0, CPHA=0)和模式 3(CPOL=1, CPHA=1)。这两种模式的共同点是数据在时钟上升沿采样,区别只在空闲时钟电平。选哪个取决于你的主控和 PCB 布线习惯。STM32F745VG 的 SPI 外设两种都支持,我一般选模式 0,因为空闲低电平在示波器上看起来更直观,调试时容易判断片选和时钟的配合关系。
时钟频率方面,手册标称最高 40MHz,但实际能跑多快取决于 PCB 走线质量、片选信号完整性、以及从机到主机的 MISO 建立时间。我在四层板上用 20MHz 跑得很稳,超过 30MHz 后如果走线长于 5cm 且没有做阻抗控制,偶尔会出现读回数据错位。建议初次调试从 1MHz 起步,确认通信正常后再逐步往上加,每次翻倍,用连续读写测试验证稳定性。
3.2 命令集与操作码
MR25H40CDF 的命令集比 Flash 简单得多,因为它没有擦除命令。核心命令如下表。
| 命令名称 | 操作码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写寄存器或写存储阵列前必须发 |
| WRDI | 0x04 | 写禁止,写完可发也可不发 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读存储阵列,任意地址任意长度 |
| WRITE | 0x02 | 写存储阵列,任意地址任意长度 |
注意这里没有 0x0B 快速读命令,也没有页编程概念。READ 和 WRITE 都是给一个 24 位地址(因为 512KB 需要 19 位地址,用 3 字节对齐),然后连续传输数据,地址自动递增,到顶回卷。这个"无页边界"特性非常省心,你可以一次写 1000 字节跨越任意地址,不用担心像 Flash 那样跨页要拆分。
3.3 状态寄存器的位定义
状态寄存器只有几位有意义,读出来是 1 字节。
- bit0:WIP(Write In Progress)。MRAM 写入极快,这一位几乎永远是 0,但严谨起见写完还是读一下确认。
- bit1:WEL(Write Enable Latch)。发过 WREN 后这一位为 1,写完自动清 0。
- bit2 到 bit7:BP0、BP1 等块保护位,以及一些保留位。
块保护位可以锁定部分地址区域禁止写入,工业场景里可以用它把标定参数区锁死,防止程序跑飞误写。这个功能在 Flash 上也有,但 MRAM 因为没有擦除,误写就是直接覆盖,所以保护位的意义更大。
3.4 写保护与硬件 WP 引脚
MR25H40CDF 有一个 WP 引脚,低电平有效。当 WP 拉低时,状态寄存器的写保护位被锁定,无法修改。注意它保护的是状态寄存器,不是存储阵列。存储阵列的写保护靠状态寄存器里的 BP 位。所以完整的写保护策略是:上电后先通过 WRSR 设置 BP 位保护关键区域,然后把 WP 引脚拉低锁死状态寄存器,这样即使程序误发 WRSR 也改不了保护配置。
我在一个电力终端项目里就是这么做的:把 0x00000 到 0x00FFF 这 4KB 设为写保护区,存设备序列号和出厂标定值,WP 引脚直接接地。运行两年多,这块区域没出过一次意外改写。
4. STM32F745VG 硬件连接与 CubeMX 配置实操
4.1 硬件连线与去耦
STM32F745VG 有多个 SPI 外设,我选 SPI2,因为它的引脚在 144 脚封装上位置好布线。连接关系如下。
- STM32 SPI2_SCK (PB13) 接 MR25H40CDF SCK
- STM32 SPI2_MISO (PB14) 接 MR25H40CDF MISO
- STM32 SPI2_MOSI (PB15) 接 MR25H40CDF MOSI
- STM32 任意 GPIO (PB12) 接 MR25H40CDF CS,用软件片选
- STM32 3.3V 接 VDD,GND 接 VSS
- WP 引脚根据是否需要锁死状态寄存器决定接 GND 还是接 GPIO
去耦电容必须加,MRAM 在写入瞬间电流会有小幅跳变,我在 VDD 和 GND 之间放了 100nF 加 1uF 组合,紧贴芯片引脚。CS 信号线上如果走线超过 3cm,建议串一个 22 欧姆电阻做阻抗匹配,减少过冲。
4.2 为什么用软件片选而不是硬件 NSS
STM32 的 SPI 外设有硬件 NSS 模式,但那个模式在多从机或者需要精确控制片选时序时很别扭。MR25H40CDF 要求 CS 拉低后到第一个时钟沿之间有最小建立时间,CS 拉高后到下一个命令之间有最小保持时间。用软件 GPIO 控制 CS,你能精确插入延时,也能在一条命令里保持 CS 全程有效。我实测软件片选在 20MHz 下完全没问题,代码里就是拉低 GPIO、调 HAL_SPI_TransmitReceive、拉高 GPIO 三步。
4.3 CubeMX 参数配置
在 CubeMX 里配置 SPI2 的关键参数如下。
- Mode:Full-Duplex Master
- Hardware NSS Signal:Disable
- Data Size:8 Bits
- Clock Polarity:Low
- Clock Phase:1 Edge
- NSS Pulse Mode:Disable
- TI Mode:Disable
- Baud Rate Prescaler:根据主频算,比如 216MHz 主频下选 16 分频得 13.5MHz
- First Bit:MSB First
这里 CPOL=Low、CPHA=1Edge 就是 SPI 模式 0。波特率预分频要算一下,STM32F745VG 的 SPI2 挂在 APB1 上,最高 54MHz,假设 APB1 时钟 54MHz,选 4 分频得 13.5MHz,选 2 分频得 27MHz。初次调试建议 4 分频或更低。
另外把 CS 对应的 GPIO 配成推挽输出、初始高电平、最高速度。如果 WP 用 GPIO 控制,也配成推挽输出。
5. 驱动代码实现:从底层读写到数据层封装
5.1 底层字节读写函数
先定义片选操作和基础传输函数。下面代码基于 STM32 HAL 库,SPI 句柄假设为 hspi2。
#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) #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 void MRAM_WriteEnable(void) { uint8_t cmd = MRAM_CMD_WREN; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); MRAM_CS_HIGH(); } static uint8_t MRAM_ReadStatus(void) { uint8_t cmd = MRAM_CMD_RDSR; uint8_t status = 0; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); HAL_SPI_Receive(&hspi2, &status, 1, 100); MRAM_CS_HIGH(); return status; }这里有个细节,HAL_SPI_Transmit 和 HAL_SPI_Receive 分开调用时,CS 保持低电平,中间不会插入额外时钟,所以命令字节和数据字节是连续的,符合手册时序。如果你用 HAL_SPI_TransmitReceive 一次传完也可以,但要注意发送缓冲区里命令后面的字节是占位数据。
5.2 任意长度读写函数
MR25H40CDF 的地址是 24 位,但实际只用到低 19 位,高 5 位补 0。写函数如下。
void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] = MRAM_CMD_WRITE; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Transmit(&hspi2, buf, len, 1000); MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, 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(&hspi2, buf, len, 1000); MRAM_CS_HIGH(); }注意写函数里 WREN 和 WRITE 之间 CS 必须拉高再拉低,因为 WREN 是一个独立命令周期。有些 Flash 允许 WREN 后不拉高 CS 直接跟 WRITE,但 MRAM 手册明确要求每个命令独立,我实测不拉高也能写进去,但为了合规和稳定,还是按手册来。
5.3 带校验的数据结构封装
工业场景里光写进去不够,还得能验证。我通常封装一个带 CRC 的记录结构。
typedef struct { uint32_t seq; uint32_t timestamp; float value[4]; uint16_t crc; } DataRecord_t; #define RECORD_SIZE sizeof(DataRecord_t) #define RECORD_BASE 0x1000 #define RECORD_MAX 100 void SaveRecord(uint32_t index, DataRecord_t *rec) { rec->crc = CRC16((uint8_t*)rec, RECORD_SIZE - 2); MRAM_Write(RECORD_BASE + index * RECORD_SIZE, (uint8_t*)rec, RECORD_SIZE); } uint8_t LoadRecord(uint32_t index, DataRecord_t *rec) { MRAM_Read(RECORD_BASE + index * RECORD_SIZE, (uint8_t*)rec, RECORD_SIZE); uint16_t crc = CRC16((uint8_t*)rec, RECORD_SIZE - 2); return (crc == rec->crc) ? 1 : 0; }CRC16 用常见的 CCITT 多项式 0x1021,实现简单,对几十字节的记录校验足够。读回来先算 CRC 再判断有效性,这样即使某次写入被干扰,也能识别出来而不是把脏数据当有效值用。
5.4 掉电保护写入策略
MRAM 写入快,但也不是零风险。如果写入过程中掉电,可能只写了一半。我的做法是双缓冲加序号。开两个记录区 A 和 B,每次写交替使用,写完更新一个指向最新有效区的标志字节。上电时读标志字节,选序号大的那个区。标志字节本身用单字节写,MRAM 单字节写入原子性很好,基本不会写一半。
具体实现是:记录区 A 在 0x1000,记录区 B 在 0x2000,标志字节在 0x0000。写 A 时先写数据,再写标志为 0xAA;写 B 时先写数据,再写标志为 0x55。上电读标志,0xAA 用 A,0x55 用 B,其他值说明首次上电或标志损坏,默认用 A。这个策略在多次人为断电测试里没丢过数据。
6. 实测性能与常见问题排查
6.1 实测读写速度
在 13.5MHz SPI 时钟下,我用逻辑分析仪和代码计时两种方式测过。连续写 512 字节耗时约 320 微秒,连续读 512 字节约 310 微秒,单字节写约 2 微秒(含 CS 和命令开销)。对比 W25Q64 写 512 字节需要先擦 4KB 块(约 50ms)再页编程(约 2ms),MRAM 快了将近两个数量级。这个差距在高频采集场景里是决定性的。
6.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读回全 0xFF | CS 没拉低、MISO 接反、器件没供电 | 示波器看 CS 和 SCK,万用表量 VDD |
| 读回全 0x00 | MISO 被拉低、SPI 模式错 | 确认 CPOL/CPHA,检查 MISO 上拉 |
| 写入后读回旧值 | 没发 WREN、WP 引脚拉低锁了状态寄存器 | 读状态寄存器看 WEL 位 |
| 高速下偶发错位 | 走线过长、无阻抗匹配、时钟太快 | 降速到 1MHz 验证,逐步提速 |
| 部分地址写不进 | BP 保护位设置了 | 读状态寄存器确认 BP 位,必要时先解除 |
| 上电首次读异常 | 器件上电初始化时间 | 上电后延时 1ms 再操作 |
6.3 几个我踩过的坑
第一个坑是 WP 引脚。我一开始把 WP 直接接地,想着反正不用写状态寄存器。结果后来想改块保护配置,发现 WRSR 怎么都不生效,查了半天手册才想起来 WP 低电平锁死了状态寄存器。后来把 WP 改接 GPIO,需要改配置时拉高,改完再拉低。
第二个坑是 SPI 时钟相位。我一开始配成 CPHA=2Edge,读出来数据整体偏移一位。原因是 MR25H40CDF 在上升沿采样,我配成了下降沿。改成 1Edge 后正常。这个错误在逻辑分析仪上很明显,MISO 数据跳变沿和 SCK 采样沿对不上。
第三个坑是连续写跨 512KB 边界。手册说地址到顶回卷,我测试时从 0x7FFF0 写 32 字节,结果前 16 字节写在末尾,后 16 字节回到 0x00000。这个行为本身没问题,但如果你的数据结构跨了边界,读回来就会错位。我的处理是在数据层保证每个记录不跨 512KB 边界,记录区起始地址对齐到记录大小的整数倍。
第四个坑是电源纹波。有一批板子在 MRAM 写入瞬间,3.3V 上出现约 100mV 的尖峰,导致偶发写入失败。后来在 MRAM 电源脚旁边加了 10uF 钽电容才压下去。所以去耦不是随便放个 100nF 就行,写入电流跳变大的器件要额外加储能电容。
7. 工业场景下的扩展思路
MR25H40CDF 加 STM32F745VG 这套组合,除了存参数和记录,还能玩出一些别的花样。比如做环形缓冲区存高速采集数据,MRAM 写入快,配合 DMA 可以做到几乎不占 CPU。再比如做配置的双备份加版本号,每次改配置写新版本,旧版本保留,出问题能回滚。还有做事件日志,每条日志带时间戳和 CRC,掉电不丢,事后能追溯。
STM32F745VG 本身有 1MB Flash 和 320KB RAM,跑 RTOS 绰绰有余。你可以把 MRAM 驱动封装成一个独立的存储服务任务,其他任务通过消息队列请求读写,这样存储逻辑和业务逻辑解耦,换存储器件时只改驱动层。我在一个项目里就是这么做的,后来把 MRAM 换成 FRAM,只改了底层三个函数,上层业务代码一行没动。
如果你还在用 W25Q64 做参数存储,并且遇到过写穿或者掉电丢数据的问题,不妨评估一下 MR25H40CDF。它的单价确实高一些,但算上省掉的擦除管理代码、掉电保护电路、以及现场故障返修成本,综合账往往是划算的。我在实际项目里换过两次,每次都是因为 Flash 寿命或者掉电问题被逼的,换完之后这类问题就再没出现过。