☰
STM32F722ZE 驱动 MR25H40CDF MRAM 工业存储方案实战
2026/10/5 21:16:01 网站建设 项目流程

1. 项目缘起与方案选型

1.1 为什么要在工业场景里折腾 MRAM

做工业嵌入式这行十几年,最头疼的往往不是算法跑不动,而是数据存不住。传统方案里,EEPROM 擦写寿命十万次封顶,SPI NOR Flash 写入前得先擦除整个扇区,掉电瞬间还可能把整块数据搞成半吊子状态。铁电存储器 FRAM 倒是快,但容量小、价格高,而且很多型号已经停产。这几年 MRAM 慢慢进入视野,MR25H40CDF 就是其中比较有代表性的一颗。

MR25H40CDF 是 Everspin 家的 4Mbit 磁阻随机存储器,SPI 接口,40MHz 时钟,最关键的是它没有擦除概念——写就是写,不需要先擦后写,字节级随机访问,写入延迟和读取延迟基本在一个量级。官方标称擦写次数 10^14 次以上,数据保持 20 年,工业级温度范围 -40°C 到 +85°C。这几个参数放在一起,对于需要频繁记录运行日志、保存标定参数、掉电前紧急存盘的工业设备来说,吸引力是实打实的。

我这次的项目背景是一台工业环境监控终端,主控用 STM32F722ZE,需要周期性记录传感器数据、保存设备配置、并在异常掉电时把关键状态写进非易失存储。之前用 SPI Flash 方案,每次写之前要判断扇区状态、搬移数据、擦除、再写回,代码复杂不说,掉电保护还得额外加超级电容。换成 MR25H40CDF 之后,整个存储层的逻辑简化了一大截。

1.2 STM32F722ZE 的 SPI 外设能力评估

STM32F722ZE 是 ST 家 F7 系列里性价比比较高的一颗,Cortex-M7 内核,216MHz 主频,带 512KB Flash 和 256KB SRAM,外设资源丰富。它有多达 6 个 SPI 接口,其中 SPI1 挂在 APB2 上,最高时钟可以到 108MHz,SPI2/3 挂在 APB1 上,最高 54MHz。对于 MR25H40CDF 的 40MHz 上限来说,SPI1 完全够用,甚至还有余量。

选 SPI1 还有几个考虑:一是它的引脚在 F722ZE 的常见封装上位置比较顺手,PA5/PA6/PA7 分别是 SCK/MISO/MOSI,片选可以用 PA4 或者任意 GPIO;二是 SPI1 支持硬件 NSS,但实际用下来我更倾向于软件片选,后面会细说原因;三是 DMA 通道资源上,SPI1 的 RX 是 DMA2 Stream0/2,TX 是 DMA2 Stream3/5,和项目里其他外设不冲突。

这里插一句,很多人一上来就纠结用硬件 NSS 还是软件 NSS。我的经验是:单主单从、片选引脚固定、不需要动态切换从机的场景,软件片选更省心。硬件 NSS 在 STM32 上有个坑,就是当 NSS 配置为硬件模式时,如果引脚被拉高,SPI 会自动进入从模式,稍不注意就出怪问题。软件片选就是普通 GPIO 输出,想什么时候拉低就什么时候拉低,时序完全可控。

1.3 整体存储架构设计

这个项目的存储需求分三类:第一类是设备配置参数,比如传感器校准系数、通信地址、报警阈值,写入频率低但要求绝对可靠;第二类是运行日志,每分钟记录一条,包含时间戳、温度、湿度、电压等,写入频繁但可以容忍少量丢失;第三类是掉电快照,检测到电源异常时立刻把当前关键状态写进去,要求写入延迟极低。

针对这三类需求,我在 MR25H40CDF 的 512KB 空间里做了分区:

区域起始地址大小用途写入策略
配置区0x0000016KB设备参数、校准数据双备份轮换写
日志区0x04000480KB运行日志环形缓冲顺序写、覆盖最旧
快照区0x7C00016KB掉电状态快照双备份轮换写

配置区和快照区用双备份,是因为这两个区域的数据最关键,万一写入过程中掉电,至少还有一份旧数据可用。日志区用环形缓冲,写满就从头覆盖,不需要擦除,这也是 MRAM 相比 Flash 的一大优势——Flash 做环形缓冲还得处理扇区擦除,MRAM 直接覆盖写就行。

2. 硬件连接与底层驱动要点

2.1 引脚连接与 PCB 布局注意事项

MR25H40CDF 是 8 引脚 DFN 封装,引脚定义很标准:CS、SCK、MOSI、MISO、VDD、VSS、WP、HOLD。实际连接时,CS 接 STM32 的 PA4,SCK 接 PA5,MISO 接 PA6,MOSI 接 PA7,VDD 接 3.3V,VSS 接地。WP 和 HOLD 这两个引脚,如果不用硬件写保护和暂停功能,直接上拉到 VDD 就行。

PCB 布局这块有几个坑我踩过。第一,SPI 走线尽量等长,尤其是 SCK 和 MOSI,40MHz 下走线差太多会导致采样窗口偏移。第二,CS 走线不要和 SCK 平行太长,容易耦合噪声,片选误触发。第三,电源去耦电容必须靠近芯片 VDD 引脚,我一般放一个 100nF 加一个 1uF,100nF 最近。第四,如果板子上有多个 SPI 从机,每个从机的 CS 单独走线,不要共用。

还有一点,MR25H40CDF 的 DFN 封装底部有个散热焊盘,这个焊盘要接地,不仅为了散热,也为了信号完整性。我第一版板子忘了接地,结果高温测试时偶发读写错误,后来补上就好了。

2.2 SPI 初始化配置详解

用 STM32CubeMX 生成初始化代码很方便,但有些参数得手动调。SPI1 的配置如下:

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;

这里重点说几个参数。CLKPolarity 和 CLKPhase要匹配 MR25H40CDF 的时序要求,它支持 SPI Mode 0 和 Mode 3,我用的是 Mode 0,即 CPOL=0、CPHA=0,空闲时 SCK 低电平,第一个边沿采样。BaudRatePrescaler设 4,SPI1 挂 APB2 上 108MHz,分频后 27MHz,在 40MHz 上限内,实测很稳。如果想跑更快,可以设 2 分频到 54MHz,但那就超规格了,我不建议。

NSS 选 SPI_NSS_SOFT,片选用 GPIO 手动控制。初始化 GPIO 的时候,PA4 配成推挽输出,初始拉高。每次读写前拉低,读写完拉高。这里有个细节:拉低和第一个 SCK 边沿之间要留一点时间,MR25H40CDF 的 CS 建立时间最小 5ns,STM32 的 GPIO 翻转速度足够,但如果你在中断里操作,最好加一个 __NOP() 或者几个空指令,确保时序。

2.3 MRAM 指令集与读写时序

MR25H40CDF 的指令集比 Flash 简单得多,常用的就几条:

指令编码功能
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

注意,MRAM 没有擦除指令,也没有页编程概念。写操作就是直接发 WRITE 指令,跟地址,然后连续写数据。地址是 24 位,因为 512KB 需要 19 位地址,但芯片按 24 位地址传输,高位补零。

读时序:拉低 CS,发 0x03,发 24 位地址,然后连续读数据,每个 SCK 周期出一个字节。写时序:拉低 CS,发 0x06(WREN),拉高 CS;再拉低 CS,发 0x02,发 24 位地址,发数据,拉高 CS。写操作前必须发 WREN,这是很多人容易忘的一步,忘了就写不进去,而且读状态寄存器也看不出明显错误。

状态寄存器的 bit0 是 WEL(写使能锁存),发完 WREN 后读一下确认 WEL=1,再发 WRITE。写完之后 WEL 自动清零。bit1 是 BP0、bit2 是 BP1,用于块保护,默认都是 0,不保护。如果你不小心写了 WRSR 把块保护开了,那就只能写特定区域,这个坑我见过有人踩。

3. 驱动层实现与核心代码

3.1 底层读写函数封装

先封装几个基础函数,后面所有操作都基于它们:

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); } static uint8_t MRAM_SPI_Transfer(uint8_t data) { uint8_t rx; HAL_SPI_TransmitReceive(&hspi1, &data, &rx, 1, 100); return rx; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x06); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x05); status = MRAM_SPI_Transfer(0xFF); MRAM_CS_HIGH(); return status; } void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr >> 16) & 0xFF); MRAM_SPI_Transfer((addr >> 8) & 0xFF); MRAM_SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((addr >> 16) & 0xFF); MRAM_SPI_Transfer((addr >> 8) & 0xFF); MRAM_SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = MRAM_SPI_Transfer(0xFF); } MRAM_CS_HIGH(); }

这段代码看着简单,但有几个地方值得说。HAL_SPI_TransmitReceive 的超时设 100ms,正常 27MHz 下传一个字节不到 0.3us,100ms 是兜底,防止总线卡死。每次传输前不重新初始化 SPI,因为初始化一次就够了,频繁初始化反而容易出问题。CS 拉高之后不要立刻进行下一次操作,MR25H40CDF 的 CS 高电平保持时间最小 10ns,STM32 的 GPIO 翻转足够慢,一般不用额外延时,但如果你在 200MHz 以上主频跑,加一个 __NOP() 更保险。

3.2 双备份配置区读写策略

配置区用双备份,逻辑是这样的:两个备份块各 8KB,每个块头部有一个 4 字节的序列号和 2 字节的 CRC。写入时,先读两个块的序列号,找到较旧的那个,把新数据写进去,序列号加一,CRC 重算。读取时,读两个块,校验 CRC,选序列号大的那个。

typedef struct { uint32_t seq; uint16_t crc; uint8_t data[8186]; } ConfigBlock; #define CONFIG_BLOCK_A_ADDR 0x00000 #define CONFIG_BLOCK_B_ADDR 0x02000 int Config_Read(uint8_t *out) { ConfigBlock blkA, blkB; MRAM_Read(CONFIG_BLOCK_A_ADDR, (uint8_t*)&blkA, sizeof(blkA)); MRAM_Read(CONFIG_BLOCK_B_ADDR, (uint8_t*)&blkB, sizeof(blkB)); int validA = (CRC16((uint8_t*)&blkA.data, sizeof(blkA.data)) == blkA.crc); int validB = (CRC16((uint8_t*)&blkB.data, sizeof(blkB.data)) == blkB.crc); if (validA && validB) { if (blkA.seq >= blkB.seq) memcpy(out, blkA.data, sizeof(blkA.data)); else memcpy(out, blkB.data, sizeof(blkB.data)); return 0; } else if (validA) { memcpy(out, blkA.data, sizeof(blkA.data)); return 0; } else if (validB) { memcpy(out, blkB.data, sizeof(blkB.data)); return 0; } return -1; }

写入的时候,先读两个块的序列号,假设 A 的序列号是 10,B 是 11,那就写 A,序列号设 12。这样即使写 A 的过程中掉电,B 还是完整的,下次上电读 B 就行。CRC 用 CRC16-CCITT,多项式 0x1021,初始值 0xFFFF,这个在嵌入式里很常用,计算速度快。

这里有个经验:序列号不要用 0 和 0xFFFFFFFF 作为有效值,因为 MRAM 出厂时全是 0,如果你第一次上电读到两个块序列号都是 0,CRC 也恰好是 0(概率极低但存在),就会误判为有效数据。我的做法是初始化时先写一个魔数 0x5A5A5A5A 到两个块的序列号位置,确保首次上电时 CRC 校验失败,走默认配置。

3.3 日志区环形缓冲实现

日志区 480KB,每条日志 32 字节,能存 15360 条。环形缓冲的关键是维护一个写指针,写到末尾就回绕到开头。因为 MRAM 不需要擦除,回绕时直接覆盖写就行。

#define LOG_START_ADDR 0x04000 #define LOG_END_ADDR 0x7C000 #define LOG_ENTRY_SIZE 32 #define LOG_MAX_ENTRIES ((LOG_END_ADDR - LOG_START_ADDR) / LOG_ENTRY_SIZE) typedef struct { uint32_t timestamp; int16_t temp; uint16_t humidity; uint16_t voltage; uint8_t reserved[22]; } LogEntry; static uint32_t log_write_idx = 0; void Log_Write(LogEntry *entry) { uint32_t addr = LOG_START_ADDR + log_write_idx * LOG_ENTRY_SIZE; MRAM_Write(addr, (uint8_t*)entry, LOG_ENTRY_SIZE); log_write_idx++; if (log_write_idx >= LOG_MAX_ENTRIES) { log_write_idx = 0; } }

写指针本身也需要保存,不然掉电后不知道写到哪了。我把写指针存在配置区的一个固定偏移里,每次写日志后更新。但这样每次写日志都要写两次 MRAM,一次日志一次指针,效率减半。优化方案是每写 16 条日志更新一次指针,掉电最多丢 16 条,对于环境监控场景完全可以接受。

读日志的时候,从写指针往前读,读到有效数据为止。判断有效性的方法是看时间戳是否在合理范围内,比如大于某个基准时间且小于当前时间。因为 MRAM 掉电不丢数据,回绕覆盖后旧数据的时间戳会变成新数据的时间戳,所以按时间戳过滤是可行的。

3.4 掉电快照的紧急写入

掉电快照要求写入延迟极低。MR25H40CDF 的单字节写入时间在 27MHz SPI 下大约是 0.3us 传输加芯片内部写入时间。官方数据手册里写周期时间最大 35ns(字节写入),实际上 SPI 传输才是瓶颈。写 32 字节快照,传输时间约 10us,加上指令开销,总共不到 20us。这个速度足够在电源跌落的前几毫秒内完成。

掉电检测用 STM32 的 PVD(可编程电压检测器),配置在 2.9V 触发。当 VDD 降到 2.9V 时,PVD 中断触发,在中断里立刻调用快照写入函数。注意,PVD 中断里不要做复杂操作,只写快照,写完就等复位。快照区也用双备份,逻辑和配置区一样。

void PVD_IRQHandler(void) { if (__HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)) { __HAL_PWR_CLEAR_FLAG(PWR_FLAG_PVDO); Snapshot_Save(); } }

这里有个坑:PVD 中断优先级要设最高,不然可能被其他中断打断,导致快照写不完。我设的是抢占优先级 0,子优先级 0。另外,快照写入函数里不要用 HAL_Delay 或任何阻塞操作,全部用寄存器级操作或者确保 HAL 函数不会阻塞。

4. 实测数据与性能分析

4.1 读写速度实测

我用逻辑分析仪抓了 SPI 波形,配合代码里的时间戳,测了几组数据:

操作数据量耗时等效速率
连续读1KB312us3.28MB/s
连续写1KB340us3.01MB/s
单字节写1B1.2us-
32字节快照写32B11.5us-

读速率 3.28MB/s 对应 SPI 时钟 27MHz,理论值 27/8=3.375MB/s,实际有指令和地址开销,312us 读 1KB 是合理的。写速率略低,因为每次写之前要发 WREN,多了一次 CS 翻转和指令传输。

单字节写 1.2us 这个数据很关键,意味着掉电快照写 32 字节只要 11.5us,加上 PVD 响应时间和中断延迟,总共不超过 50us。电源从 3.3V 跌到 2.9V 的时间取决于电容容量,一般 100uF 电容在 100mA 负载下能撑几毫秒,完全够用。

4.2 擦写寿命与数据保持

MR25H40CDF 标称擦写次数 10^14 次,这个数字什么概念?如果每秒写一次,一年 31536000 次,写 10^14 次需要 317 万年。实际项目中日志每分钟写一次,一天 1440 次,一年 525600 次,十年才 525 万次,连标称值的零头都不到。所以寿命这块完全不用担心。

数据保持 20 年,工业级温度范围内。我做了高温老化测试,85°C 下连续读写 100 万次,数据校验全部通过。低温 -40°C 下读写也正常,没有出现数据错误。不过要注意,MRAM 对强磁场敏感,如果设备附近有大型电机或磁铁,建议加磁屏蔽或者保持距离。我测试时把一块钕铁硼磁铁靠近芯片 2cm,读写开始出错,拿开就恢复,数据没丢。

4.3 与 SPI Flash 方案的对比

对比项MR25H40CDFW25Q64 (SPI Flash)
擦除需求无写前必须擦除
写入粒度字节页 (256B)
擦写寿命10^1410^5
写入速度3MB/s0.5MB/s (含擦除)
掉电风险低高 (擦除中掉电)
价格高低
容量4Mbit64Mbit

从表里能看出来,MRAM 的优势在写入体验和可靠性,劣势在容量和价格。对于需要频繁写入、数据量不大的工业场景,MRAM 是更优解。对于大容量存储、写入不频繁的场景,Flash 更划算。我这个项目日志量不大,512KB 够用,所以选了 MRAM。

5. 常见问题与排查实录

5.1 读写失败问题速查

现象可能原因排查方法解决方案
读全 0xFFCS 没拉低示波器看 CS 引脚检查 GPIO 初始化
读全 0x00MISO 没接好万用表测通断重新焊接
写不进去没发 WREN读状态寄存器 bit0写前加 WREN
偶发错误SPI 时钟太快降低分频改 8 分频
高温出错电源去耦不足测 VDD 纹波加 100nF 电容
数据错位CPHA/CPOL 不对试 Mode 0 和 Mode 3改 SPI 配置

这个表是我实际调试中总结的,覆盖了 90% 以上的问题。重点说几个:读全 0xFF 最常见的原因是 CS 没拉低,因为 MISO 上拉电阻会把线拉高,如果 CS 没拉低,芯片不驱动 MISO,读出来就是 0xFF。写不进去九成是忘了 WREN,这个我一开始也犯过,后来在 MRAM_Write 函数里强制加 WREN,就再没出过。

5.2 SPI 时序调试经验

调试 SPI 时序,逻辑分析仪是必备工具。我用的是一款 8 通道 100MHz 采样率的,抓 27MHz SPI 波形够用。抓波形时重点看几个点:CS 拉低到第一个 SCK 边沿的时间、SCK 的占空比、MISO 的采样点是否在数据稳定区。

有一次遇到读数据偶尔错一位,抓波形发现 SCK 上升沿和 MISO 数据跳变几乎同时,采样窗口太窄。原因是 PCB 走线太长,MISO 有延迟。解决办法是把 SPI 时钟降到 13.5MHz(8 分频),问题消失。后来改板时缩短了走线,27MHz 也能稳定跑。

还有一次,写数据时好时坏,抓波形发现 CS 在数据传输中途被拉高了。查代码发现是中断里也调用了 MRAM 读写函数,打断了主循环的写操作。MRAM 读写函数不是可重入的,必须加互斥保护。我的做法是在 MRAM_Write 和 MRAM_Read 里关中断,操作完再开。虽然会稍微增加延迟,但保证了数据完整性。

5.3 掉电保护实测与优化

掉电保护这块我做了专门的测试。用可调电源模拟掉电,从 3.3V 快速降到 0V,下降时间设 1ms,看快照能不能写进去。第一次测试失败,快照没写完整。查原因发现 PVD 中断里调用了 HAL_SPI_TransmitReceive,这个函数内部有超时判断,会读系统 tick,而掉电时系统 tick 可能已经不准了。

优化方案:PVD 中断里的 SPI 传输改用寄存器级操作,不依赖 HAL 库和系统 tick。直接操作 SPI1 的 DR 寄存器,轮询 TXE 和 RXNE 标志位。这样即使系统时钟乱了,SPI 外设只要有时钟就能工作。改完之后再测,1ms 下降时间内快照写入成功率 100%。

另外,快照区双备份的写入顺序要注意:先写备份 B,再写备份 A。因为读取时选序列号大的,如果写 B 的过程中掉电,A 还是旧的但完整,下次上电读 A。如果先写 A 再写 B,写 A 时掉电,A 可能半新半旧,B 是旧的,读 B 也没问题。但为了逻辑简单,我固定先写 B 再写 A,序列号 B 比 A 大 1。

6. 项目扩展与个人体会

这套 MRAM 存储方案在这个环境监控终端上跑了大半年,现场部署了二十多台,没有出现过数据丢失或读写故障。后来我把这套驱动封装成了一个独立的中间件,换到其他 STM32 型号上也能用,只需要改 SPI 初始化和 CS 引脚定义。

扩展方向有几个:一是把日志区改成文件系统,比如 LittleFS,这样可以直接用 PC 读取日志文件,不用自己写解析工具;二是加一个 MRAM 到 SD 卡的定期转存,把历史数据归档;三是利用 MRAM 的快速写入特性,做高频数据采集的缓存,比如振动监测,每秒采几千个点,先写 MRAM,再慢慢处理。

我个人在实际操作中的体会是,MRAM 这东西用起来确实省心,但价格摆在那,选型时要算清楚账。如果你的项目写入频率不高,一年写不了几万次,那 SPI Flash 完全够用,没必要多花钱。但如果你的设备需要频繁记录、掉电保护要求高、或者维护成本很高(比如装在高塔上、井下),那 MRAM 多出来的成本很快就能通过减少现场故障赚回来。

最后分享一个小技巧:MR25H40CDF 的 WP 引脚如果不用,一定要上拉到 VDD,不要悬空。悬空时引脚电平不确定,可能随机进入写保护状态,表现就是写不进去但读正常,这个坑很隐蔽,查起来费时间。我现在的做法是在 PCB 上直接放一个 10K 上拉电阻,不管用不用都焊上,省得后面调试时抓瞎。

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

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

立即咨询