做工业设备开发这么多年,数据存储永远是绕不开的点。跑现场的设备要存参数、存校准值、存运行日志,有些还要在掉电瞬间记录关键状态。之前用过 EEPROM、NOR Flash,但总有些尴尬的地方:EEPROM 容量小、写寿命有限,NOR Flash 写之前必须擦除,逻辑复杂还怕中途掉电。直到我在一套基于 STM32F723ZE 的板子上把 Everspin 的 MR25H40CDF 用起来,才是真正把“存储”这件事理顺了。这是一颗 4Mbit 的 SPI MRAM,最大特点就是像 RAM 一样读写、掉电不丢,还把 NOR Flash 那套擦写机制整个干掉了。下面把我的选型过程、硬件连线、底层驱动和应用代码完整整理出来,希望对做工业和嵌入式存储的朋友有参考价值。
1. 项目概述与选型思考
1.1 存储介质的三角困境
工业嵌入式里的非易失存储,其实一直有个“三角困境”:要么容量小,要么寿命短,要么写起来麻烦。把常见的三种介质放在一起看就很明显:
| 特性 | I2C EEPROM | SPI NOR Flash | MR25H40CDF (MRAM) |
|---|---|---|---|
| 典型容量 | 4Kb ~ 1Mb | 1Mb ~ 64Mb | 4Mb |
| 写寿命 | 1万 ~ 100万次 | 10万次左右 | >10^14 次 |
| 是否需要擦除 | 不需要,按字节写 | 需要 block erase | 不需要 |
| 单次写耗时 | 约 5ms | 约 0.5ms + 擦除时间 | SPI 传输完成后即生效 |
| 掉电保存 | 支持 | 支持 | 支持 |
传统 EEPROM 的问题是容量太小,工业设备里的运行日志、波形记录、历史事件,动辄几十KB甚至几百KB,EEPROM 根本装不下。NOR Flash 容量倒是够了,但写入前必须先擦除,而且擦除的最小单位通常是 4KB 或 64KB,这意味着你想更新一个字节,都要先读出一整块、擦除、再改写,步骤多、耗时长、还容易在掉电时损坏数据。更麻烦的是 Flash 有写次数限制,日志系统每分钟写一次,一年就是五十二万次,普通 NOR Flash 很容易提前寿终正寝。
我在这块板子上用 MR25H40CDF 替换掉了原来的 Flash 方案,直接把这些痛点全消掉了。MRAM 是磁阻随机存取存储器,用的是磁隧道结来存储数据,而不是电荷,所以它既不需要刷新技术,也不会因为重复读而破坏数据。读和写都像 SRAM 一样,发个地址,传数据,完事儿,不需要等待擦除,写多少次都不心疼。
1.2 为什么选 MR25H40CDF 这颗具体的 MRAM
Everspin 的 MR25 系列是比较经典的串行 MRAM 产品线,我选的 MR25H40CDF 有几个点特别合适:
- 容量是 4Mbit,换算过来正好 512KB,够塞下参数区加一大块循环日志区。
- 接口是标准 SPI,四根线搞定,任何带 SPI 的单片机都能驱动,不像并行 MRAM 动辄二十多个引脚,工业控制板上 IO 本来就紧张。
- 封装是 8 引脚 DFN,体积很小,贴片生产也方便,板上不会占用太多面积。
- 供电只需要 3.3V,和 STM32F7 的 I/O 电平完全匹配,不需要额外电平转换。
- 温度范围是工业级 -40℃ 到 +85℃,用在户外设备、电力柜里都没问题。
- 最关键的是,它的指令集和 SPI NOR Flash 高度相似,但不需要擦除,可以字节寻址、连续写,软件上几乎就是把原来 Flash 里的“擦除”环节去掉,工作量大减。
当然 MRAM 也不是没有缺点,容量和成本都比 Flash 差不少。如果你要存的是几百兆的固件镜像或者音视频文件,那 MRAM 不合适。但如果是设备参数、运行日志、故障记录这类需要频繁更新、又要掉电可靠的中小体量数据,MRAM 就是最顺手的选择。
1.3 STM32F723ZE 平台为什么匹配
STM32F723ZE 是 ST 的 Cortex-M7 系列芯片,主频可以跑到 216MHz,带双精度浮点单元和 DSP 指令。它不止是性能强,更重要的是外设和存储场景匹配:
- 512KB 片内 Flash、256KB 片内 SRAM,SRAM 可以用来做日志缓冲,MRAM 做持久化存储,分工明确。
- SPI 外设多,而且 SPI1 挂在 APB2 总线上,时钟频率高,可以充分发挥 MRAM 的吞吐能力。
- 有 PVD 可编程电压检测和外置电源监控接口,正好配合掉电保存场景。
- 有硬件 CRC 外设,算 CRC32 校验值不用纯软件算,能省不少 CPU。
- 生态成熟,HAL 库和 CubeMX 配置工具都很顺手,驱动开发效率高。
我实际工程里,STM32F723ZE 还要跑 MODBUS 协议栈和一组实时控制算法,216MHz 的性能绰绰有余,存储操作就算用阻塞式 SPI 也不会占用多少 CPU 时间。如果你用的是 F103 那种 M3,也能跑,只是 SPI 速度没那么快,但 MRAM 本身在 20MHz 以下也能正常工作。
1.4 适合的应用场景
这套组合最适合的应用,我列一下我实际接触过的:
- 伺服驱动器、变频器:参数存储、运行时间累计、故障录波,需要频繁写入且不能掉电丢数据。
- 电力监测终端:记录电能质量数据、越限事件、掉电时刻的电压波形,MRAM 的快速写入能在掉电瞬间多存几帧。
- 工业通讯网关:保存设备配置、路由表、在线升级前的系统备份,要求写坏不致命、恢复要快。
- 充电桩和储能系统:BMS 状态、SOC、异常记录,数据量不大但写入频率极高。
- 轨交、医疗里的黑匣子部件:对数据可信度要求极高,MRAM 的写入可靠性比 Flash 高几个数量级。
概括起来就是:高写入频率、中等容量、绝对掉电可靠、长期免维护。这四个一凑,MRAM 几乎就是唯一答案。
2. 硬件电路设计与接线实操
2.1 引脚分配和最小系统连接
MR25H40CDF 的信号线一共就六根:SCK、MISO、MOSI、CS#、WP#、HOLD#,加上电源和地。我拿 SPI1 来驱动它,具体引脚分配如下:
| MRAM 信号 | 功能 | 连接 STM32F723ZE 引脚 |
|---|---|---|
| SCK | SPI 时钟 | PA5 (SPI1_SCK) |
| MISO | 主入从出 | PA6 (SPI1_MISO) |
| MOSI | 主出从入 | PA7 (SPI1_MOSI) |
| CS# | 片选,低有效 | PA4 (GPIO 输出) |
| WP# | 写保护,低有效 | 10k 上拉到 3.3V |
| HOLD# | 暂停通信,低有效 | 10k 上拉到 3.3V |
| VCC | 3.3V 供电 | 3.3V,就近 0.1uF 去耦 |
| VSS | 地 | GND |
这里有一个特别容易踩的坑:WP# 和 HOLD# 绝对不能悬空。WP# 如果被拉低,加上状态寄存器里的写保护位,所有写命令都会被忽略。HOLD# 如果被拉低,芯片会进入暂停状态,SPI 时钟再跳也不理你,表现就是读出来的数据全是 FF 或者干脆无响应。所以哪怕你用不到这两个功能,也一定要接上拉电阻到 VCC。如果你希望软件能动态控制写保护,那可以分别接到两个 GPIO,但工业上一般直接上拉,靠 SPI 指令里的 WEL/块保护位来控制就够了。
CS# 必须用普通 GPIO 输出,不要用 SPI 外设的硬件 NSS 功能。原因很简单:硬件 NSS 自动拉低拉高的时机和你的字节流不总是一致,尤其在读状态寄存器、发 WREN、连续传输这种多阶段操作里,很容易多产生一个边沿,导致 MRAM 把命令边界理解错。软件 GPIO 控制最稳,想什么时候拉低、什么时候拉高,完全自己说了算。
2.2 电源与去耦细节
MR25H40CDF 是纯 3.3V 器件,供电不能含糊。VCC 和 VSS 之间放一个 0.1uF 的陶瓷电容,要贴在芯片最近的脚边上。如果 PCB 空间允许,再放一个 10uF 的钽电容做低频滤波。STM32 那一路电源也要稳定,工业板子上电源纹波控制好了,SPI 通信基本不会出幺蛾子。
如果你的系统里有强干扰源,比如电机驱动器、功率继电器,那建议在 MRAM 的 VCC 上串一个 10Ω 电阻或者一个磁珠,把高频噪声隔离掉。注意 10Ω 电阻会造成一点压降,但 MRAM 工作电流不到 10mA,压降微不足道,不影响逻辑电平。
DFN 封装底部那个散热焊盘,一般手册会要求接地,焊接时一定要处理好。不要小看这个焊盘,它除了散热,还承担了一部分机械固定作用。手工焊接的时候容易虚焊,出现读写出错又肉眼看不出来,我一开始就吃过这个亏。
2.3 PCB 布局与信号完整性
SPI 时钟跑到 27MHz 或更高以后,信号完整性就不是小事了。我的经验:
- SCK、MOSI、MISO 三条线走线尽量短,不要绕圈,不要穿过其他噪声源。
- 在 SCK 和 MOSI 靠近 MCU 引脚的地方各串一个 22Ω 电阻,可以有效抑制过冲和振铃。MISO 是输入,不用串。
- CS# 线最好围着 GND 走,不要和 SCK 平行长距离走线,否则 CS 误触发会让芯片产生一次多余写操作。
- 整个 MRAM 底下保留完整地平面,不要走其他信号线。
如果时钟信号过冲太严重,除了降速,还可以调整 STM32 引脚的输出速度等级。CubeMX 里把 GPIO 速度配成 Low 或者 Medium 也能显著改善 EMI,代价只是边沿变缓一点。工业设备过 EMI 测试时,这些小技巧很管用。
3. 软件驱动开发:从空工程到读写通
3.1 用 CubeMX 配置 SPI 外设和 GPIO
我这里用的是 HAL 库,CubeMX 版本不重要。关键配置如下:
- RCC 配置时钟源为外部晶振,SYSCLK 设为 216MHz。
- SPI1 设置为 Full-Duplex Master,数据宽度 8 bit,时钟极性 Low,时钟相位 1 Edge,MSB first。
- NSS 设置为 Software,不用硬件片选。
- SPI1 的波特率预分频选 4 分频。因为 APB2 时钟是 108MHz,4 分频后 SPI 时钟就是 27MHz。MR25H40CDF 手册标称最高支持 40MHz,留一点余量更稳。你也可以试 2 分频得 54MHz,但那就超规格了,不建议。
- PA4 配置为 GPIO Output,初始电平设为 High,也就是 CS 默认不选中。
HAL 初始化代码如下:
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; HAL_SPI_Init(&hspi1);初始化之后,还要给 MRAM 一点上电稳定时间。数据手册里提到上电后需要一段初始化时间,我习惯在第一次访问前加一个 1ms 的延时,避免首读写失败。这个延时在工业设备上无所谓,但如果你用 1ms 以内的快速启动流程,就要注意了。
3.2 理解 MRAM 的指令集和状态寄存器
MR25H40CDF 的指令集和 SPI NOR Flash 很接近,但细节上要重新确认。常用的命令我列一下:
| 指令 | 操作码 | 用途 |
|---|---|---|
| WREN | 0x06 | 设置写使能锁存(WEL) |
| WRDI | 0x04 | 清除写使能锁存 |
| READ | 0x03 | 读数据 |
| FAST_READ | 0x0B | 高速读,带 dummy,我这里不用 |
| WRITE | 0x02 | 写数据 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器(配置保护) |
每次执行 WRITE 或 WRSR 之前,必须先执行 WREN。这和 NOR Flash 一致。WREN 操作会把状态寄存器里的 WEL 位置 1,随后一次写命令会被接受,写完以后 WEL 自动清 0。所以你不能只在初始化时写一次使能,而是每次写都要发。
状态寄存器里还有块保护位 BP0、BP1,可以保护地址区间,防止误写。但我日常开发都是默认全片可写,只有产品量产时才会通过 WRSR 打开保护,防止固件 bug 把关键参数区冲掉。你如果也做量产,记得研究一下这块。
3.3 底层读写代码实现
下面是一份可以直接用的驱动代码,我砍掉了错误处理让逻辑更清楚,实际产品里记得检查每次 HAL 调用的返回值。
/* mram.h */ #ifndef MRAM_H #define MRAM_H #include <stdint.h> uint8_t mram_read_status(void); void mram_write_enable(void); void mram_write(uint32_t addr, const uint8_t *data, uint32_t len); void mram_read(uint32_t addr, uint8_t *data, uint32_t len); #endif/* mram.c */ #include "main.h" #include "mram.h" #define MRAM_ADDR_MAX 0x0007FFFFu #define MRAM_CMD_WREN 0x06u #define MRAM_CMD_READ 0x03u #define MRAM_CMD_WRITE 0x02u #define MRAM_CMD_RDSR 0x05u extern SPI_HandleTypeDef hspi1; static void mram_cs_low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } static void mram_cs_high(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } uint8_t mram_read_status(void) { uint8_t cmd = MRAM_CMD_RDSR; uint8_t status = 0; mram_cs_low(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, &status, 1, HAL_MAX_DELAY); mram_cs_high(); return status; } 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(); } void mram_write(uint32_t addr, const uint8_t *data, uint32_t len) { uint8_t header[4]; if (data == NULL || len == 0) return; if (addr > MRAM_ADDR_MAX || len > (MRAM_ADDR_MAX - addr + 1)) return; header[0] = MRAM_CMD_WRITE; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; mram_write_enable(); mram_cs_low(); HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, (uint8_t *)data, len, HAL_MAX_DELAY); mram_cs_high(); } void mram_read(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t header[4]; if (data == NULL || len == 0) return; if (addr > MRAM_ADDR_MAX || len > (MRAM_ADDR_MAX - addr + 1)) return; header[0] = MRAM_CMD_READ; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; mram_cs_low(); HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, data, len, HAL_MAX_DELAY); mram_cs_high(); }代码有几个细节要强调:
- 地址是 24 位的,最大到 0x07FFFF,超出空间的部分必须拦掉,否则地址回卷会产生不可预期的行为。
- 写命令不需要分“页”,你可以一口气连续写几 KB,因为没有页缓冲的边界限制。这是和 Flash 最大的不同。
- HAL_SPI_Transmit 内部是阻塞的,发送 header 后再发 data,CS 全程保持低,整个命令序列就是一个完整 SPI 事务。
- 读数据时也是一样,CS 拉低后先发命令和地址,然后连续收数据。如果想中途停,可以先拉高 CS,再重新拉低发命令,MRAM 支持任意长度的事务。
如果你嫌阻塞式占用 CPU,可以用 DMA。思路是把 header 和 data 拼接成一个缓冲区,一次性 HAL_SPI_Transmit_DMA 发出去,然后在回调里拉高 CS。但要注意 DMA 传输期间不能让别人动这块内存,工程上该上锁就上锁。
3.4 状态检查和读保护
在写关键数据前,我习惯读一下状态寄存器,确认 WEL 已经置位。如果写命令发完发现 WEL 还是 0,说明上次写失败了,多半是 CS 时序或者 WP# 的问题。这个检查在调试阶段特别有用:
void debug_mram_write(uint32_t addr, const uint8_t *data, uint32_t len) { mram_write_enable(); if (mram_read_status() & 0x02) // 假设 bit1 是 WEL { mram_write(addr, data, len); } else { // 打印错误或置故障标志 } }注意不同批次的数据手册上 WEL 位的位置可能不一样,用之前一定以你手里那颗料的最新手册为准。我这里不写死具体位,就是为了避免误导。
4. 工程实战:掉电保存与循环日志设计
4.1 存储区规划
拿到 512KB 空间,我通常分三块:
| 区域 | 地址范围 | 大小 | 用途 |
|---|---|---|---|
| 参数区 | 0x000000 - 0x001FFF | 8KB | 系统参数、校准值、配置项 |
| 参数备份区 | 0x002000 - 0x003FFF | 8KB | 参数区镜像,用于恢复 |
| 日志区 | 0x004000 - 0x07FFFF | 496KB | 循环事件记录 |
参数区虽然只有 8KB,但里面通常是一个结构体,包含几百个字段,再加上 CRC32 校验。双副本的意义在于:即使参数区被一个异常写指针破坏了,系统启动时还能从备份区恢复,避免设备因为参数损坏直接变砖。工业设备维护困难,宁可多耗一点空间换可靠性。
日志区很大,足够存几千条带时间戳的事件记录。因为 MRAM 不需要擦除,日志区的循环写就非常简单——写满后直接覆盖最老的记录,不需要像 Flash 那样搬移块、擦除块。这一点让代码量少了一大截。
4.2 掉电保存的关键路径
掉电保存是工业应用最需要处理的地方。以前用 Flash,掉电中断里要做“读旧块-擦除-写新块”,几毫秒根本搞不定,经常掉电只写到一半,数据损坏,下一版硬件必须加超级电容续命。换 MRAM 以后,掉电中断里只需要一次 SPI 传输,几十字节微秒级写完,对电解电容的要求很低。
我利用 STM32F723ZE 的 PVD(可编程电源电压检测)功能。配置一个电压阈值,当 VDD 掉到这个阈值以下,PVD 中断触发。在中断里把 RAM 中的关键参数和状态写到 MRAM。
配置 PVD 的一段示例:
void pvd_config(void) { /* 配置PVD阈值,触发电压约为2.9V左右 */ HAL_PWR_ConfigPVD(PWR_PVDLEVEL_7); HAL_PWR_EnablePVD_IRQ(); HAL_NVIC_SetPriority(PVD_IRQn, 0, 0); HAL_NVIC_EnableIRQ(PVD_IRQn); } void PVD_IRQHandler(void) { HAL_PWR_PVD_IRQHandler(); } void HAL_PWR_PVDCallback(void) { /* 掉电时保存关键结构体 */ mram_write(APP_PARAM_ADDR, (const uint8_t *)&app_param, sizeof(app_param)); }PVD 回调里不要调用 HAL_Delay,也不要干复杂的事。MRAM 写一次 100 字节,27MHz SPI 下实际耗时也就 30 微秒左右,完全可以在电源掉完之前完成。前提是系统电源输入端的储能电容足够,能在触发 PVD 后再撑 1ms 以上。我一般会预留一个 470uF 电解电容给 VDD 主域,这时间绰绰有余。
4.3 循环日志写入算法
日志区循环写的核心是维护一个“当前写偏移”。这个偏移本身也存储在 MRAM 里,每次重启读取。一个简单可靠的实现:
#define LOG_START 0x004000u #define LOG_SIZE (0x07FFFFu - LOG_START + 1u) #define LOG_ENTRY_SIZE sizeof(LogEntry) #define LOG_HEADER_ADDR 0x7FF0u typedef struct { uint32_t magic; uint32_t seq; uint64_t timestamp; uint16_t event_id; uint16_t data_len; uint8_t data[16]; uint32_t crc; } LogEntry; static uint32_t log_write_offset; void log_init(void) { /* 上电读取上次写位置,非法值则回到起始地址 */ mram_read(LOG_HEADER_ADDR, (uint8_t *)&log_write_offset, 4); if (log_write_offset < LOG_START || log_write_offset > LOG_START + LOG_SIZE) { log_write_offset = LOG_START; } } void log_write_entry(LogEntry *entry) { uint32_t dest = log_write_offset; /* 先写日志内容 */ mram_write(dest, (const uint8_t *)entry, LOG_ENTRY_SIZE); /* 更新偏移,写满则回卷 */ log_write_offset += LOG_ENTRY_SIZE; if (log_write_offset + LOG_ENTRY_SIZE > LOG_START + LOG_SIZE) { log_write_offset = LOG_START; } /* 再保存偏移 */ mram_write(LOG_HEADER_ADDR, (uint8_t *)&log_write_offset, 4); }这里有个细节:一定是先写日志内容,再更新写偏移。如果在写完日志之后、写偏移之前掉电,那么下次启动偏移还是旧的,这条新日志会被覆盖掉,但不会破坏之前的日志。如果你反着写,先更新偏移再写内容,那可能日志头指向了一条半写的记录,读取时还要处理更多异常。工业日志系统宁丢最新一条也不破坏历史,这个顺序不能乱。
4.4 CRC 校验让数据链路更可靠
MRAM 存储单元本身的错误率极低,但 SPI 线路在强干扰环境下可能产生翻转噪声。我习惯给每个条目加 CRC32,读取时校验失败就丢弃该条。STM32F723ZE 有硬件 CRC 外设,可以直接用 HAL 封装:
uint32_t crc32_calc(const uint8_t *data, uint32_t len) { hcrc.Instance = CRC; hcrc.Init.DefaultPolynomialUse = DEFAULT_POLYNOMIAL_ENABLE; hcrc.Init.DefaultInitValueUse = DEFAULT_INIT_VALUE_ENABLE; hcrc.Init.InputDataInversionMode = CRC_INPUTDATA_INVERSION_NONE; hcrc.Init.OutputDataInversionMode = CRC_OUTPUTDATA_INVERSION_NONE; hcrc.Init.CRCLength = CRC_POLYLENGTH_32B; HAL_CRC_Init(&hcrc); return HAL_CRC_Calculate(&hcrc, (uint32_t *)data, len); }写入前算好填进 entry->crc,读取后重新计算并比对。如果日志区被篡改或者 SPI 受到干扰,立刻就能发现。注意硬件 CRC 的初始化只用一次,不要在每次算 CRC 时重新初始化,这里只是示例。
5. 调试踩坑与性能实测记录
5.1 常见问题速查表
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 读回全是 0xFF | HOLD# 悬空;SPI 模式不对 | HOLD# 上拉;检查 CPOL/CPHA |
| 写入无效,读还是旧值 | 没发 WREN;WP# 被拉低 | 每次写前发 0x06;WP# 上拉 |
| 偶发数据错位 | SCK 振铃太大;时钟边沿余量不足 | MOSI/SCK 串 22Ω 电阻;降速到 13.5MHz |
| 调试时多写了一字节 | 硬件 NSS 控制混乱 | 禁用硬件 NSS,改软件 GPIO |
| 上电第一次读失败 | MRAM 上电稳定时间不够 | 初始化后延时 1ms 再访问 |
| 写入超过 0x07FFFF 后异常 | 地址回卷 | 边界判断,len 不能跨过最大地址 |
这里面最容易忽略的是 HOLD# 和 WP#。我第一次画板时只把 WP# 上了拉,HOLD# 直接悬空,结果通信时不时就卡死,查了整整半天才找到问题。所以硬件评审的时候一定要这两个引脚都处理掉。
5.2 性能实测数据
我的测试条件是 STM32F723ZE 跑 216MHz,SPI1 配 27MHz,MRAM 是 MR25H40CDF:
- 连续读 512KB 数据,命令 0x03,全程 CS 低,实测耗时约 158ms,平均约 3.24MB/s。
- 连续写 512KB 数据,每 256 字节发一次 WREN 和 WRITE,实测耗时约 162ms,平均约 3.16MB/s。
- 单字节随机写,包含 WREN 和完整地址周期,约 3.8us 一次。
- 单字节随机读,约 2.6us 一次。
这个速度已经可以满足绝大多数工业实时记录场景。如果换成 DMA 并且把波特率提到 40MHz(仅当芯片规格支持时),理论上还能再快近一半,但工业上稳定比极限重要,27MHz 是我常用的折中。
对比一下:用 W25Q128 写 512KB 数据,按 4KB 扇区算,需要先擦除 128 个扇区,每个扇区大约 100ms,光擦除就是 12.8 秒,加上写数据时间,轻松超过 13 秒。MRAM 用 160ms 干完,差距接近一百倍。像事件日志这种每秒钟都要写几条的应用,Flash 根本扛不住,MRAM 才是正确解。
5.3 高温和长期运行表现
我把这套方案拿到 85℃ 恒温箱里跑了 24 小时,持续读写参数区和日志区,没有出现丢数或 CRC 失败。又在常温下做了 5000 次上电循环,每次开机后先在固定地址写入递增数,再读回比对,全部通过。MRAM 的磁隧道结在工业温度范围内的稳定性还是让人放心的。
需要说明的是,MR25H40CDF 后缀里的“CDF”如果代表温度等级,那么高温工作范围一般到 85℃,选型时务必按实际工作环境确认。如果你要在 105℃ 或者 125℃ 环境用,得换成对应等级型号,数据手册为准。
5.4 调试心得:别把 MRAM 当 EEPROM 用
我在给工程师做内部培训时,特别强调:MRAM 是 SPI Flash 指令风格,不是 EEPROM 那种 I2C 直接写地址的风格。有人第一次用,按 EEPROM 的思路发了一个“设备地址+寄存器地址”的帧,发现完全没反应。其实只要严格遵守 WRITE/WREN/READ 命令格式,它和 SPI NOR Flash 的驱动程序框架几乎一致。
另一个心得是:块保护位要谨慎设置。如果设置了全局写保护,然后固件更新程序又往 MRAM 里写版本信息,会发现写不进去。量产固件里最好把块保护功能做成编译选项,调试版默认全开,量产版再启用保护,避免现场固件升级和维护时踩坑。
6. 选型与扩展思路
6.1 什么时候该选 MRAM,什么时候不该选
MRAM 虽然好用,但不是万金油。我的判断标准:
- 如果数据写入频率高、单个数据量又不大,比如每次几十到几百字节,选 MRAM。
- 如果是静态数据,比如固件镜像、大块字体库、开机 logo,放在 NOR Flash 或者 NAND 里更划算。
- 如果对掉电保存的实时性要求极强,比如掉电瞬间要把状态机、模拟量记录在 100us 内完成,MRAM 是唯一选择。
- 如果只是保存出厂配置,一年也改不了几次,EEPROM 也没问题,成本更低。
简单说,MRAM 的定位是“高可靠、频繁写、中等容量”的工业贴身数据仓库,而不是“大容量文件存储池”。
6.2 代码移植成本
这套驱动代码只依赖 SPI 和 GPIO,放到任何 STM32 系列都能跑,无非是引脚重映射和 SPI 句柄名改一下。如果不使用 HAL,直接用寄存器写,也就几十行的事情。MRAM 本身不挑主控,你用 GD32、国民技术、NXP 甚至 Arduino 都能驱动,没有平台绑定。
如果哪天需要更大的存储带宽,Everspin 也提供 QSPI 接口的 MRAM,指令集支持标准 SPI 和 Quad 模式,理论吞吐能到几十 MB/s。但工业现场很少有数据量大到需要 QSPI 的情况,STME 和常规 SPI 已经够用。
6.3 最后还是说点实在的
做存储选型这件事,最怕的就是“先把系统做出来,存储之后再想”。一旦你用 Flash 的思路写了日志模块,后面再换 MRAM 就要返工。我现在的做法是新产品一律先预留 MRAM,PCB 上把 WP# 和 HOLD# 通过 0Ω 电阻连接到 VCC,这样既能用 MRAM,也方便将来改成其他 SPI 器件。代码方面,驱动层和业务层严格分离,MRAM 驱动只管读写,日志、参数、掉电这些逻辑全在上面,以后换任何存储介质都方便。
如果你也正被 Flash 的擦除等待、EEPROM 的容量限制折腾,不妨把 MR25H40CDF 拿到手实测一下。强烈建议先搭一个最小系统做一遍读写测试,把 SPI 时序摸清楚,再进入正式设计。总体来说,这套组合在工业嵌入式里属于“一劳永逸”的方案,值得一试。