做嵌入式这十来年,跟各种存储芯片打交道的机会太多了,NOR Flash、EEPROM、SRAM都用过。最近一个工业现场的项目让我重新认识了MRAM这套方案,组合用了MR25H40CDF和STM32F446RE,把困扰很久的“频繁写数据还怕掉电丢”的问题一次性解决了。如果你做工业设备、数据采集或者任何需要持久化存储又对可靠性敏感的场景,这篇文章值得看完。我尽量把硬件接法、软件驱动、调试坑都讲透,让有基础的朋友能直接复用。
先说结论:MRAM这种存储芯片,介于Flash和SRAM之间,它把两者的优点都占了——非易失、不用擦除、读写速度快、寿命几乎无限。搭配 STM32F446RE 这种带多个SPI的M4内核芯片,实现起来难度比想象中低很多,但里面的细节和坑也不少。
1. MR25H40CDF 到底是什么,为什么工业场景它更扛打
1.1 传统存储方案的痛点
很多工业产品落地时最头疼的一件事就是数据保存。用Flash,写入前必须整块擦除,而且写入寿命一般在十万次量级。对一个每秒记录一条数据的设备,一天就是八万多次写,十万次寿命根本撑不过两天。用EEPROM,寿命更窄,容量普遍也不大。用SRAM,速度快了,但一掉电全没了,工业现场经常有掉电和重启,一旦丢数据,轻则参数归零,重则直接出安全事故。
我遇到过最典型的一个场景是电机驱动器的参数更新:上位机通过通信接口下发一堆系数,设备需要马上存起来。如果存Flash,擦除扇区那几百毫秒里正好碰上主循环的其他实时任务,系统容易卡顿;如果只写RAM,下次上电全没了。这个矛盾过去一直靠“电池+SRAM”的方案硬扛,电池还得定期换,维护成本高。
1.2 MRAM的关键特性
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,换算下来容量是512KB。它的原理是利用磁隧道结来存储数据,跟电荷存储的Flash、EEPROM完全不一样。因为是磁性状态,所以写入不需要擦除,速度能压到跟SRAM一个级别,同时掉电后数据不丢。
几个关键的参数我实际体验下来:
- 读写CLOCK最高能到40MHz,对SPI从机来说非常快了
- 写入寿命达到10^14次量级,这意味着几乎所有嵌入式应用都可以忽略寿命问题
- 数据保持时间在十年以上,工业现场五年八年的生命周期完全够用
- 写操作不需要先擦除,单条WRITE命令能写1到64字节,地址自动递增
- 供电范围2.7V到3.6V,和 STM32F446RE 的3.3V系统直接无缝连接
1.3 和其他存储介质的公开对比
我把常用介质整理过一张对比表,顺手放在这里:
| 特性 | NOR Flash | EEPROM | SRAM + 电池 | MRAM |
|---|---|---|---|---|
| 写入前擦除 | 需要 | 需要 | 不需要 | 不需要 |
| 写寿命 | 1万~10万次 | 10万~100万次 | 无限制 | 10^14次 |
| 读速度 | 中 | 慢 | 快 | 快 |
| 写速度 | 慢 | 慢 | 快 | 快 |
| 掉电保持 | 支持 | 支持 | 依赖电池 | 支持 |
| 位/字节操作 | 按扇区 | 按字节 | 按字节 | 按字节 |
表格一摆就清楚了。在“又要频繁写、又要掉电保、又要速度快”这个前提下,MRAM几乎是唯一不需要做太多妥协的选择。MR25H40CDF 这个型号在Everspin产品线里属于低功耗、SPI接口、容量中等的定位,非常适合 STM32F446RE 这种中等规模的嵌入式控制器来挂载。
2. 硬件连接与工程搭建
2.1 为什么选用 STM32F446RE 的 SPI2
STM32F446RE 是Cortex-M4F内核,主频180MHz,片上带512KB Flash和128KB RAM。对很多工业项目来说这个配置相当能打,关键是它提供了3个独立的SPI接口。MR25H40CDF最大支持40MHz的SPI时钟,我直接用它的SPI2来挂MRAM。
选择SPI2而不是SPI1或SPI3,有几个原因:
- SPI1挂在APB2总线上,主频可以跑到90MHz,拿来跑MRAM有点浪费,留给更快的外设更合理
- SPI2挂在APB1上,最高45MHz,配置SPI时钟分频后正好能落在10MHz~40MHz这个区间
- 在Nucleo或自制底板上,SPI2默认引脚PB12~PB15布局清晰,和MRAM的DFN小封装布线不冲突
如果你用CubeMX初始化,SPI2我一般配成:
- Mode:Full-Duplex Master
- 数据大小:8bit
- 时钟极性 CPOL = Low,相位 CPHA = 1 Edge(也就是 SPI Mode 0)
- NSS:Disable,由软件GPIO控制
- 波特率分频:先配 8分频,后面根据实际需要再调
2.2 最小电路图构思与Key细节
MR25H40CDF 是8脚DFN封装,个头非常小,PCB设计时要注意引脚间距。核心引脚一共就7个有用的:CS、SCK、SI、SO、WP、HOLD、VCC。除了电源,WP和HOLD绝对不能悬空。WP是写保护,HOLD是暂停通信,工业环境下的电磁干扰可能让这两个脚状态漂移,一旦悬空,时序莫名其妙就乱了。
正确的接法是:
- WP:直接接VCC,解除写保护
- HOLD:直接接VCC,禁用Hold功能
- CS:接STM32的任意GPIO(我用PB12)
- SCK:接PB13
- SI即MOSI:接PB15
- SO即MISO:接PB14
- VCC就近放一颗0.1uF的去耦电容,再加一颗10uF的钽电容或者MLCC做低频滤波
PCB布线方面,我踩过一次比较深的坑。MRAM的SPI时钟在10MHz以上的时候,SCK走线如果绕得太远或者和旁边的大电流线靠太近,MISO上的回读信号会惨不忍睹。实际布局时我建议SCK和SI、SO三根线等长平行走,中间不要过孔,算是参考一下,不一定绝对必须,但能让眼图更干净。
2.3 DFN封装的焊接与板级测试
DFN封装手焊挺烦的,底部有散热焊盘,引脚又在两边,新手很容易连锡。我自己的习惯是优先用热风枪,温度350℃左右,快速吹焊,然后拖锡清理引脚。焊完之后一定要用万用表量一遍:
- VCC对地不能短路
- 每个引脚和相邻引脚不能连锡
- CS、SCK、SI、SO到MCU对应引脚的连通性要确认
上电后不跑程序前,先量一下MRAM的VCC是不是3.3V,再量WP和HOLD是否也是高电平。这一步几十秒,能省后面一小时的排查时间。
3. 软件驱动设计与读写实现
3.1 SPI初始化与片选管理
用STM32CubeMX生成工程时,我把SPI2的NSS设置为Disable,然后手动把PB12配置为普通推挽输出,初始电平拉高。这里有个很关键的理念:MRAM的通信过程完全由CS低电平来驱动,任何一条指令都必须CS先拉低,再送命令字节,结束后CS拉高,否则MRAM无法确认指令边界。
HAL库下的初始化片段是这个样子:
void MRAM_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = MRAM_CS_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(MRAM_CS_GPIO_Port, &GPIO_InitStruct); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }芯片选型上,SPI2已经在CubeMX里配置好,模式是Master、8bit、Mode 0。我额外在应用层把片选宏封装了一下:
#define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET) #define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET)3.2 指令集与状态寄存器操作
MR25H40CDF的指令不多,核心就几类:
- 0x06:写使能 WREN
- 0x04:写禁止 WRDI
- 0x05:读状态寄存器 RDSR
- 0x01:写状态寄存器 WRSR
- 0x03:读数据 READ
- 0x02:写数据 WRITE
这里有反直觉的地方:MRAM不是Flash,但它的状态寄存器里依然有WEL(写使能锁存)位。每次写数据命令之前,必须先发0x06,把WEL置1,然后再发写命令。如果跳过WREN直接写,芯片会直接忽略后续数据,让我一度以为板子坏了。
状态寄存器读出来之后,最低位就是WEL。读状态寄存器的小函数很实用:
uint8_t MRAM_ReadStatus(void) { uint8_t cmd = 0x05; 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; }写使能操作要注意一个细节:发送完WREN之后,最好回读一次状态寄存器,确认WEL位为1。如果读到0,说明硬件或者时序有问题,继续往下写也是白写。
3.3 读数据与写数据函数封装
读写函数是整个驱动的基础。我封装成下面这样,注释写得比较详细,方便直接抄:
uint8_t MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; if (addr + len > 0x80000UL) { return 0; // 地址溢出判断 } cmd[0] = 0x03; 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(); return 1; } uint8_t MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; uint32_t chunk; if (addr + len > 0x80000UL) { return 0; } while (len) { chunk = len > 64 ? 64 : len; // 单次写指令最多64字节 MRAM_WriteEnable(); cmd[0] = 0x02; 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_Transmit(&hspi2, buf, chunk, 1000); MRAM_CS_HIGH(); addr += chunk; buf += chunk; len -= chunk; } return 1; }写入做得比较保守,一条指令只处理64字节,超过64字节就拆成多条指令。MRAM没有擦除步骤,所以拆指令不会带来性能灾难。实测SPI时钟20MHz的时候,512字节的连续写入耗时在微秒到毫秒级,比Flash快了一个数量级。
3.4 为什么每次写前必须拉高CS
有一类隐蔽的问题特别值得提醒:在HAL库的SPI发送完成后,直接拉高片选有时候会有残留字节问题。因为在MRAM看来,CS拉高那一个边沿,既是命令的结束标志,也是最后一位数据的采样边界。如果SPI外设还在传输FIFO里最后一位,CS已经拉高了,最后一个字节就会落到前一个地址上。
我的处理办法是每次传输结束后,加一个短暂的等待,让SPI总线彻底空闲后再拉高CS:
while (HAL_SPI_GetState(&hspi2) != HAL_SPI_STATE_READY); MRAM_CS_HIGH();如果你用CubeMX生成的代码,DMA模式下尤其要注意这个。DMA完成中断和总线空闲并不是瞬间同步的,多这一句等待,鲁棒性会好很多。
4. 一个真实项目里的应用解剖
4.1 数据记录与参数存储的双应用
我这个项目是一台工业设备的运行参数记录器,STM32F446RE 作为主控,传感器数据每100ms刷新一次,需要把最近一条运行状态、累计运行时长、故障码历史保存下来。如果不掉电保持,设备重启后统计信息全丢,客户那边直接不能接受。
参数存储我放在MRAM的高地址区,逻辑上划分了两块:
- 0x00000~0x7B000:滚动数据存储区
- 0x7B000~0x7FFFF:参数备份区
滚动存储区按“环形队列+数据头”的方式组织。每次上电都从固定起始位置扫描,找最新一条记录的位置。因为写MRAM不需要擦除,环形队列的写操作就是在地址上原地填充新帧,省了Flash那种“先搬数据、再擦块、再写”的麻烦。程序上简洁非常多。
4.2 掉电保护与数据一致性设计
工业现场经常遇到突然断电,这时候MRAM虽然不怕掉电,但软件层面如果数据写到一半断电,还是可能出现半条记录。我的做法是给每条记录增加一个16位的“魔法头”和16位校验值:
- 每条记录固定结构:帧头 + 数据长度 + 数据 + 校验和
- 帧头用固定字节 0x5A 0xA5,用于判断记录是否有效
- 校验和用简单的累加,计算数据部分的字节和,取低16位
写入时,先写数据主体,再写校验字,最后写帧头。读取时,先看到帧头才认为记录有效。如果帧头擦了还是半条,就直接跳过,保证旧的完整记录还能被识别。这样配合MRAM的掉电非易失特性,数据可靠性直接拉满。
除了MRAM本身,强烈建议MCU的电源入口加一个掉电检测电路,用STM32F446RE的内部比较器或者外部复位IC,检测到VCC跌到阈值以下时,触发外部中断,在主循环里尽快完成一条记录的最后刷新。MRAM的写入速度足够快,掉电的“黄金窗口”内写完一条记录绰绰有余。
4.3 和系统里的其他外设共存
STM32F446RE在项目里不只挂了一颗MRAM,SPI总线上还挂了一颗ADC芯片和一颗显示驱动。SPI从设备片选互斥就是大问题。MRAM的CS必须保持在低电平才能完成一次操作,中途如果其他从设备把总线拉走,MRAM的时序就被破坏了。
我的做法是增加一个全局互斥标志位,在裸机环境下通过关中断或临界区保护,保证多个外设的SPI操作不重叠:
__disable_irq(); MRAM_WriteBytes(...); __enable_irq();如果用的是FreeRTOS,那就可以用一个互斥信号量,把每个外设驱动包一层锁。注意一点,MRAM的命令序列是原子性的,不能让CS低电平期间发生任务切换。所以锁的粒度是整个MRAM函数,而不是单个SPI传输。
5. 常见问题与调试实录
5.1 读回来全是0xFF
这个问题我一开始也遇到过,而且排查了很久。MRAM在空片状态或者通信异常时,SPI读出的可能全是0xFF,看起来像“没焊好、芯片坏了、地址不对”都有可能。系统的排查路径是:
- 先检查WP引脚和HOLD引脚是否都接到了高电平,这两个脚悬空尤其容易造成写操作被忽略
- 用示波器抓CS、SCK、SI、SO四根线,确认CS低电平时间范围内SCK确实有脉冲
- 降低SPI时钟到1MHz,排除高频干扰
- 直接读状态寄存器,看看能不能返回一个非0xFF的值
如果状态寄存器都读不回来,大概率是硬件连接或者焊接问题。此时把MRAM换成一颗新料,或者用万用表手动点测引脚,都能定位问题。
5.2 SPI 时钟配置不当导致的数据错位
MRAM的SCK最高能到40MHz,但实际能不能跑到这个频率,取决于整个系统的PCB布线和MCU的SPI外设。我实测过SPI2挂在APB1总线,45MHz总线频率分频后,10MHz相当稳定,20MHz偶尔出错,最后稳定在16MHz。倒不是MRAM不行,而是我的底板走线还有干扰。
数据错位的典型表现是:读出来的内容有规律偏移,比如第一个字节是0x00,后面全是乱码。这种时候优先怀疑时钟相位和极性问题。MR25H40CDF同时支持Mode 0和Mode 3,但STM32侧如果配成了Mode 1或Mode 2,采集时机完全对不上,表现出来就是乱七八糟。
建议直接用SPI Mode 0,也就是CPOL=0、CPHA=1。如果换了模块批次还有问题,就切到Mode 3对比验证。
5.3 写入数据偶发出错
偶发出错比全错更烦人。我排查的经验是先分清楚是单字节错还是整块错:
- 整块错,优先怀疑CS时序或WREN没生效
- 单字节错,优先怀疑电源纹波、SCK信号质量或者地址拼接错误
地址拼接错误特别隐蔽。MRAM的地址是24位,我早期在写驱动时,地址拆分用了addr >> 16、addr >> 8、addr三段,如果addr是无符号16位变量,高位段直接截断,地址一进入0x010000以上就完全错位了。后来把地址变量全部改成uint32_t,问题立刻消失。
5.4 MRAM读写实测速度参考
同一个调好的SPI2总线上测量,SPI时钟16MHz,条件如下:
| 操作 | 传输长度 | 平均耗时 |
|---|---|---|
| 写使能 | 1字节 | 约 1us |
| 写64字节 | 68字节 | 约 35us |
| 读64字节 | 68字节 | 约 30us |
| 写512字节 | 4次WRITE拆包 | 约 160us |
| 读512字节 | 一次READ | 约 120us |
对于大多数嵌入式记录场景,这个速度完全够用。而且MRAM不存在“擦除时间”这个隐藏成本,真实延时比标称值还稳。
5.5 数据校验的强化手段
如果项目对可靠性要求更高,可以在MRAM驱动之上再套一层CRC校验。最简单的是用CRC16,STM32F446RE内置CRC硬件外设,算起来几乎是零成本。
做法是每次写完数据,紧接着在数据末尾区域写一条CRC记录。读取时先读数据再读CRC,计算比对,不一致就尝试读取一个镜像副本。MRAM容量512KB,存两份完全没问题。这套方案我沿用了好几个项目,至今没出过数据损坏问题。
我个人在实际操作里的体会是:MRAM不是什么高不可攀的新技术,它就是把“快”和“不丢”这两个看起来很矛盾的需求同时满足了。真要在现场踏踏实实用,重点还是软硬件的细节——片选时序、写使能、掉电策略、数据校验,每一项都别偷懒。这个组合后续还可以继续扩展,比如把MRAM的一部分地址域直接映射成虚拟U盘,或者配合文件系统用来做日志数据库,玩法挺多。希望这篇关于 MR25H40CDF 和 STM32F446RE 的实战记录,能帮你少走几步弯路。