1. 为什么 MRAM 在工业嵌入式场景里越来越受关注
1.1 从 EEPROM 和 Flash 的痛点说起
做过工业设备的人大概都有过这样的经历:设备在现场跑了几个月,突然某天断电重启后参数丢了,或者日志文件写坏导致文件系统挂载失败。追根溯源,问题往往出在存储介质上。传统方案里,参数存储用 EEPROM,代码和数据存储用 NOR Flash 或 NAND Flash,这套组合用了十几年,但它的短板在工业环境下被放得很大。
EEPROM 的擦写寿命通常在 100 万次量级,单字节写入速度慢,页写入还有 5ms 左右的等待时间。Flash 更麻烦,写之前必须先擦除整个扇区,擦除寿命一般只有 10 万次,而且擦除操作耗时长,掉电时如果正好在擦除过程中,整个扇区数据可能全部丢失。工业设备经常需要高频记录运行状态、故障日志、累计运行时间,这些场景下 Flash 的寿命和掉电风险就成了硬伤。
MR25H40CDF 这类 MRAM 芯片的出现,恰好切中了这些痛点。MRAM 的全称是磁性随机存储器,它的存储单元用磁性隧道结来保存数据,不需要电荷保持,所以具备几个非常关键的特性:写入不需要擦除、写入速度接近 SRAM、擦写寿命理论上无限(实际标称 10^14 次以上)、掉电数据不丢失。这几点组合起来,对于工业数据记录场景几乎是理想解。
1.2 MR25H40CDF 的关键规格解读
MR25H40CDF 是 Everspin 出品的一款 4Mbit(512KB)SPI 接口 MRAM。选型时我重点看了这几个参数:
| 参数项 | 规格 | 实际意义 |
|---|---|---|
| 容量 | 4Mbit / 512KB | 适合参数区、日志区、配置区,不适合大文件 |
| 接口 | SPI,最高 40MHz | 和 STM32 的 SPI 外设直接对接 |
| 供电 | 2.7V ~ 3.6V | 和 STM32F746 的 3.3V 系统兼容 |
| 写入寿命 | 10^14 次 | 基本可以认为无限写 |
| 数据保持 | 20 年以上 | 工业设备生命周期内无需担心 |
| 工作温度 | -40°C ~ +85°C(工业级) | 满足工业现场要求 |
| 封装 | DFN-8 或 SOIC-8 | 小体积,适合紧凑布局 |
这里有个细节值得注意:MR25H40CDF 的 SPI 模式支持 Mode 0 和 Mode 3,最高时钟 40MHz。很多人看到 40MHz 就想着直接拉满,但实际布线如果走线较长或者有干扰,建议先降到 20MHz 调试,稳定后再往上提。我在一块四层板上跑 40MHz 没问题,但在另一块两层板上 30MHz 就开始偶发读错误,后来加了串联电阻才稳住。
1.3 STM32F746VG 作为主控的适配性
STM32F746VG 是 ST 的 Cortex-M7 系列,主频 216MHz,带 FPU 和 DSP 指令,外设资源丰富。选它来搭配 MR25H40CDF,主要看中几点:一是 SPI 外设数量多(最多 6 个 SPI/I2S),可以灵活分配;二是带 Quad-SPI 接口,虽然 MR25H40CDF 是标准 SPI,但 F746 的 SPI 外设支持 FIFO 和 DMA,读写效率高;三是 F746 的供电和 IO 电平是 3.3V,和 MRAM 直接对接不需要电平转换。
F746VG 的 SPI 外设有个特点:支持 8 位到 16 位数据帧,带 32 字节 FIFO。这意味着在连续读写时,可以配合 DMA 减少 CPU 干预。我在实际项目里用 SPI2 接 MRAM,配置 DMA 通道后,512KB 全片读取大概 200ms 左右(20MHz 时钟),如果不用 DMA 纯轮询,CPU 基本被占满。
2. 硬件连接与 SPI 时序要点
2.1 引脚连接方案
MR25H40CDF 的引脚定义很标准:CS(片选)、SCK(时钟)、SI(数据输入)、SO(数据输出)、VDD、VSS,加上 WP 和 HOLD 两个控制脚。实际连接时,WP 和 HOLD 如果不用可以直接上拉到 VDD,但我的习惯是接到 GPIO 上,方便后续调试时控制。
和 STM32F746VG 的连接我一般这样分配:
- CS 接 SPI 的 NSS 引脚(硬件片选)或任意 GPIO(软件片选)
- SCK 接 SPI_SCK
- SI 接 SPI_MOSI
- SO 接 SPI_MISO
- WP 接 GPIO(可选)
- HOLD 接 GPIO(可选)
这里有个选择:硬件片选还是软件片选。硬件片选由 SPI 外设自动控制,时序精准,但 F746 的硬件 NSS 在某些配置下会有额外延迟。软件片选用 GPIO 手动拉低拉高,灵活但需要自己保证时序。我在工业项目里倾向用软件片选,因为可以在 CS 拉低前后加延时,避免时序临界问题,而且多片 MRAM 挂同一条 SPI 总线时切换更方便。
2.2 SPI 模式与时序参数
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。我一般用 Mode 0,因为大部分 STM32 的 SPI 例程默认就是 Mode 0,配置起来省事。
时序上要注意几个关键点:
- CS 拉低到第一个 SCK 边沿之间要有至少 5ns 的建立时间
- 最后一个 SCK 边沿到 CS 拉高之间要有至少 5ns 的保持时间
- SCK 空闲电平在 Mode 0 下是低电平
这些参数在 40MHz 时比较紧张,如果 GPIO 翻转速度不够或者走线有电容,容易出问题。我的做法是在 CS 操作前后各加一个 __NOP() 或者几微秒的延时,实测下来稳定性提升明显。
注意:MR25H40CDF 的 CS 拉高后,芯片需要一定时间完成内部操作(虽然 MRAM 写入很快,但状态机切换需要时间)。连续操作时,两次 CS 拉高之间建议间隔至少 100ns。
2.3 PCB 布局的实操经验
SPI 总线在高速下对布局敏感。我踩过的坑是:MRAM 离 STM32 太远(超过 5cm),SCK 走线没有包地,结果 40MHz 下读数据偶发位翻转。后来重新布局,把 MRAM 放在 STM32 背面,SCK 和 MOSI 走线尽量短且等长,MISO 加了一个 22Ω 串联电阻,问题解决。
另外,MRAM 的电源去耦不能省。VDD 引脚旁边放一个 100nF 加一个 1uF 的电容,越近越好。工业环境下电源纹波大,去耦不好会导致写入失败。
3. STM32 端驱动实现与核心代码
3.1 SPI 初始化配置
用 STM32CubeMX 配置 SPI2,参数如下:
- Mode: Full-Duplex Master
- Data Size: 8 Bits
- Clock Polarity: Low
- Clock Phase: 1 Edge
- NSS: Software
- Prescaler: 根据时钟树计算,目标 20MHz
- First Bit: MSB
如果 APB1 时钟是 54MHz,Prescaler 设为 4 得到 13.5MHz,设为 2 得到 27MHz。我一般先用 4 分频调试,稳定后改 2 分频。
初始化代码用 HAL 库:
hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; hspi2.Init.Direction = SPI_DIRECTION_2LINES; hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi2.Init.TIMode = SPI_TIMODE_DISABLE; hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi2);3.2 MRAM 读写基本操作
MR25H40CDF 的指令集和普通 SPI Flash 类似,但有几个关键区别:没有擦除指令,写入前不需要发 Write Enable(部分型号需要,MR25H40CDF 的写入直接生效)。核心指令如下:
| 指令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能(部分操作需要) |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
读操作流程:CS 拉低 -> 发 0x03 -> 发 24 位地址 -> 读数据 -> CS 拉高。
写操作流程:CS 拉低 -> 发 0x02 -> 发 24 位地址 -> 写数据 -> CS 拉高。
注意 MR25H40CDF 的地址是 24 位的,虽然 512KB 只需要 19 位,但芯片要求发满 24 位。
读函数实现:
void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; 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(); }写函数实现:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; 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, len, 1000); MRAM_CS_HIGH(); }3.3 DMA 加速大批量读写
512KB 全片读写如果纯轮询,CPU 占用率很高。用 DMA 可以把 CPU 解放出来。配置 SPI2_TX 和 SPI2_RX 的 DMA 通道,然后改写读写函数:
void MRAM_Read_DMA(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; 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_DMA(&hspi2, buf, len); // 等待DMA完成 while (hspi2.State != HAL_SPI_STATE_READY); MRAM_CS_HIGH(); }实测 20MHz 时钟下,DMA 方式读 512KB 约 210ms,轮询方式约 230ms,差距不算大,但 DMA 方式 CPU 可以同时处理其他任务,在 RTOS 环境下优势明显。
实操心得:DMA 传输结束后,SPI 的 BSY 标志可能还没清零,直接拉高 CS 会截断最后一个字节。我的做法是在 DMA 完成回调里等 BSY 清零再拉高 CS,或者加一个 1us 延时。
4. 数据存储策略与掉电保护设计
4.1 分区规划思路
512KB 的 MRAM 不算大,但用于参数和日志足够。我一般这样分区:
- 0x00000 ~ 0x00FFF:系统参数区(4KB),存设备配置、校准数据
- 0x01000 ~ 0x01FFF:备份参数区(4KB),参数区的镜像
- 0x02000 ~ 0x3FFFF:日志区(约 248KB),循环记录运行日志
- 0x40000 ~ 0x7FFFF:预留区(256KB),后续扩展
参数区用双备份加 CRC 校验,写入时先写备份区再写主区,读取时如果主区 CRC 错误就切到备份区。这个策略在 EEPROM 时代就很成熟,搬到 MRAM 上同样有效,而且因为 MRAM 写入快,双写带来的时间开销几乎可以忽略。
4.2 掉电检测与紧急保存
MRAM 本身掉电不丢数据,但 STM32 在掉电瞬间可能正在写数据,如果写到一半断电,数据就不完整了。解决办法是加掉电检测电路:用电阻分压监测电源电压,接到 STM32 的 ADC 或比较器,当电压低于阈值时触发中断,在中断里把关键数据紧急写入 MRAM。
MRAM 的写入速度在这里体现价值:写 256 字节只需要几十微秒,而 Flash 写同样数据需要几毫秒,根本来不及。我实测过,3.3V 电源掉到 2.7V 大约有 2ms 的窗口期,足够 MRAM 写完几KB数据。
具体实现:用 STM32 的 PVD(可编程电压检测器)监测电压,配置在 2.9V 触发中断,中断服务程序里把 RAM 中的关键数据结构体写入 MRAM 的紧急保存区。
4.3 日志循环写入的实现
日志区用循环缓冲方式管理。维护一个写指针,每次写日志时从指针位置开始写,写完后指针后移,到区尾后回到区头。同时维护一个序号,每条日志带序号,读取时按序号排序。
MRAM 不需要擦除,所以循环写入非常简单,直接覆盖旧数据即可。但要注意:如果日志区写满一圈,旧日志会被覆盖,所以需要根据日志产生频率估算区大小。假设每条日志 64 字节,248KB 可以存约 4000 条,如果每天产生 100 条,可以存 40 天。
注意:循环写入时,写指针本身也需要保存。我一般把写指针存在参数区,每次写日志前更新参数区的指针值。这样掉电后重新上电,能从正确位置继续写。
5. 常见问题排查与调试技巧
5.1 读回数据全为 0xFF 或 0x00
这是最常见的现象。排查顺序:
- 先确认 CS 是否正常拉低拉高,用示波器看 CS 波形
- 确认 SCK 是否有输出,频率是否正确
- 确认 MISO 是否有数据返回,如果一直高电平,可能是 MRAM 没供电或者 CS 没接对
- 检查 SPI 模式是否匹配,Mode 0 和 Mode 3 搞反会导致数据错位
我遇到过一次,CS 接在了 SPI 的硬件 NSS 引脚上,但 CubeMX 里配置成了软件 NSS,结果 CS 一直不动作。后来改成普通 GPIO 就正常了。
5.2 写入后读回数据不一致
可能原因:
- 写入时序不满足,CS 拉高太早,最后一个字节没写进去
- 电源纹波大,写入过程中电压跌落
- SPI 时钟太快,MRAM 来不及响应
解决办法:降低 SPI 时钟到 10MHz 测试,如果正常再逐步提高;在 CS 操作前后加延时;检查电源去耦。
5.3 高频写入时偶发错误
工业场景下需要高频记录数据时,如果发现偶发写入错误,重点检查:
- 是否有其他中断打断了 SPI 传输过程
- DMA 和 CPU 是否同时访问了 SPI 数据寄存器
- MRAM 的 WP 引脚是否被误触发
我的经验是,在 RTOS 环境下,SPI 传输期间要加互斥锁,防止其他任务抢占。另外,WP 引脚如果不用,直接上拉到 VDD,不要悬空。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读全 0xFF | CS 未动作 | 示波器看 CS | 检查片选配置 |
| 读全 0x00 | MISO 未接或 MRAM 未供电 | 万用表测电压 | 检查供电和连线 |
| 数据错位 | SPI 模式不匹配 | 对比时序图 | 改 CPOL/CPHA |
| 偶发错误 | 时钟太快或干扰 | 降频测试 | 降速、加屏蔽 |
| 写入无效 | WP 引脚触发 | 测 WP 电平 | WP 上拉 |
| DMA 丢字节 | CS 拉高过早 | 看 BSY 标志 | 等 BSY 清零 |
6. 性能实测与方案对比
6.1 读写速度实测数据
我在 STM32F746VG 平台上做了几组测试,SPI 时钟分别设为 10MHz、20MHz、40MHz,测试 512KB 全片读写:
| 时钟 | 写入耗时 | 读取耗时 | 误码率 |
|---|---|---|---|
| 10MHz | 420ms | 410ms | 0 |
| 20MHz | 215ms | 208ms | 0 |
| 40MHz | 110ms | 105ms | 偶发(长走线) |
40MHz 下在短走线(<3cm)时误码率为 0,长走线时偶发。所以最终产品我定在 20MHz,兼顾速度和稳定性。
6.2 与 EEPROM、Flash 方案的对比
| 对比项 | MR25H40CDF | EEPROM (24C512) | NOR Flash (W25Q64) |
|---|---|---|---|
| 容量 | 512KB | 64KB | 8MB |
| 接口 | SPI 40MHz | I2C 400kHz | SPI 80MHz |
| 写入前擦除 | 不需要 | 不需要 | 需要 |
| 擦写寿命 | 10^14 | 10^6 | 10^5 |
| 写入速度 | 快 | 慢 | 慢(含擦除) |
| 掉电风险 | 低 | 中 | 高 |
| 单价 | 高 | 低 | 低 |
MRAM 的单价确实比 EEPROM 和 Flash 高不少,但在需要高频写入、掉电保护、长寿命的工业场景下,综合维护成本和可靠性,MRAM 的性价比反而更高。我有个项目原来用 EEPROM 存参数,现场跑了两年后陆续出现参数丢失,换成 MRAM 后三年零故障。
6.3 适用场景建议
MRAM 不是万能的,选型时要看场景:
- 适合:参数存储、高频日志、掉电紧急保存、工业宽温环境
- 不适合:大容量数据存储(如音频、图像)、成本极度敏感的量产消费类产品
如果数据量超过 512KB,可以考虑 MRAM 加 Flash 的组合:MRAM 存关键参数和日志,Flash 存大块数据。
7. 写在最后的一些实操体会
这个方案我在两个工业项目上落地过,一个是电力监测终端,一个是数控机床控制器。电力监测终端需要每秒记录一次电压电流数据,原来用 Flash 方案,擦除寿命算下来两年就到头了,换 MRAM 后寿命问题彻底解决。数控机床控制器则是看中掉电保护,加工过程中断电不能丢参数,MRAM 的快速写入让紧急保存变得很从容。
踩过的坑主要集中在前期的时序调试上。MR25H40CDF 虽然标称 40MHz,但实际能达到多少和你的 PCB 布局、GPIO 速度、电源质量都有关系。我的建议是先用低时钟把功能跑通,再逐步提速,每一步都用示波器确认波形。另外,HAL 库的 SPI 函数在高速下有一定开销,如果追求极致性能,可以直接操作寄存器。
还有一点:MRAM 的 WP 和 HOLD 引脚不要悬空,即使不用也要上拉或下拉到确定电平。我有一次忘了处理 HOLD,结果芯片偶尔进入保持状态,读数据全是错的,查了半天才找到原因。
最后分享一个小技巧:在 MRAM 里划一小块区域做“自检区”,上电时写入一个已知模式再读回,确认芯片工作正常后再加载参数。这个自检只需要几十微秒,但能提前发现硬件故障,避免设备带病运行。