1. 为什么 MRAM 在嵌入式存储里越来越受关注
搞嵌入式的人大多有过这种纠结:用 EEPROM 吧,写入速度慢得让人抓狂,擦写寿命也就百万次量级;用 NOR Flash 吧,写之前还得先擦除整个扇区,掉电瞬间没写完就丢数据;用 FRAM 吧,容量小、价格高,而且很多型号已经停产或者交期长得离谱。这几年我在做工业数据采集和边缘设备的时候,越来越频繁地遇到一个需求——高频写入、掉电不能丢、还要能扛工业级温度。传统的方案要么性能不够,要么寿命不够,要么就是供应链不靠谱。
MR25H40CDF 这颗芯片就是在这个背景下进入我视野的。它是 Everspin 出的 4Mbit(512KB)串行 MRAM,SPI 接口,SOIC-8 封装,工业级温度范围 -40°C 到 +85°C,部分批次能到 +105°C。MRAM 的核心优势在于:写入不需要擦除、写入速度是纳秒级、擦写寿命几乎无限(10^14 次以上)、掉电数据不丢。这几点加在一起,基本上把 EEPROM 和 NOR Flash 的痛点全解决了。
搭配的 MCU 是 STM32F732IE,Cortex-M7 内核,216MHz 主频,带 FPU 和 DSP 指令集,1MB Flash、320KB SRAM,外设资源丰富,SPI 接口有好几个。选它是因为在工业场景里,这个型号的算力和外设够用,而且 STM32 的 HAL 库生态成熟,开发周期短。两者配合,可以搭出一套高可靠、高频写入、掉电安全的数据存储方案。
这篇文章适合谁看?如果你正在做工业控制器、数据记录仪、边缘计算节点、电力监测设备,或者任何需要频繁保存关键参数、运行日志、故障快照的嵌入式项目,那这套组合值得你花时间研究。我会从硬件连接、SPI 配置、驱动编写、读写优化、掉电保护、常见问题排查几个维度,把整个方案拆开讲清楚。即使你之前没用过 MRAM,只要熟悉 STM32 和 SPI 基本操作,跟着走一遍就能上手。
2. 方案整体设计与选型思路拆解
2.1 为什么是 MRAM 而不是 EEPROM 或 NOR Flash
先把这个核心问题说清楚。很多项目里,存储选型往往是最容易被忽视的一环,直到量产之后才发现写入寿命不够、掉电丢数据、写入延迟影响实时性。我见过不少案例,用 EEPROM 存运行日志,结果几个月就把扇区写坏了;用 NOR Flash 存参数,掉电时正在擦除,整个扇区数据全丢。
MRAM 的物理机制和它们完全不同。它用磁性隧道结(MTJ)来存储数据,写入靠的是电流改变磁化方向,读取靠的是测量电阻变化。这个过程不涉及电荷的注入和隧穿,所以没有擦除操作、没有写入磨损、写入能量极低。具体对比看下面这张表:
| 特性 | EEPROM | NOR Flash | FRAM | MRAM (MR25H40CDF) |
|---|---|---|---|---|
| 写入前是否需要擦除 | 不需要 | 需要 | 不需要 | 不需要 |
| 擦写寿命 | 10^6 次 | 10^5 次 | 10^12 次 | 10^14 次以上 |
| 写入速度 | 毫秒级 | 毫秒级 | 微秒级 | 纳秒级 |
| 掉电数据保持 | 10 年 | 10 年 | 10 年 | 20 年以上 |
| 写入功耗 | 中等 | 高 | 低 | 极低 |
| 容量范围 | 小 | 中 | 小 | 中 |
| 接口 | I2C/SPI | SPI | I2C/SPI | SPI |
从表里能看出来,MRAM 在写入寿命和速度上是碾压级的优势。对于需要每秒写几次甚至几十次数据的场景,EEPROM 和 NOR Flash 根本扛不住,而 MRAM 可以随便写。MR25H40CDF 的 512KB 容量,对于存参数、日志、故障快照来说也够用了。
2.2 STM32F732IE 的 SPI 外设能力评估
STM32F732IE 有多个 SPI 接口,其中 SPI1 挂在 APB2 总线上,最高时钟可以到 108MHz(取决于 APB2 分频),SPI2 和 SPI3 挂在 APB1 上,最高 54MHz。MR25H40CDF 的 SPI 最高时钟是 40MHz,所以用 SPI1 的话需要分频到 40MHz 以下。
我一般选 SPI1,因为它的时钟源更干净,而且 DMA 通道资源也够。具体配置上,用硬件 NSS 还是软件片选?我建议用软件片选,原因有两个:一是硬件 NSS 在多从机场景下容易出问题,二是软件片选可以灵活控制时序,方便在片选拉低之后加一点延时,确保 MRAM 能正确识别命令。这个延时在高速 SPI 下尤其重要,后面讲时序的时候会细说。
另外,STM32F732 的 SPI 支持 8 位和 16 位数据帧,MRAM 的命令和地址是 8 位的,数据可以按 8 位或 16 位读写。我一般用 8 位模式,简单直接,DMA 配置也方便。
2.3 整体架构与数据流设计
整个方案的架构可以分成三层:硬件层、驱动层、应用层。
硬件层就是 STM32 和 MRAM 的物理连接,包括 SPI 四根线(SCK、MISO、MOSI、CS)加上电源和地。MRAM 的 VDD 是 3.3V,和 STM32 的 IO 电平一致,不需要电平转换。注意 MRAM 的 WP(写保护)引脚,如果不用可以接地或者接 GPIO 控制,我一般接一个 GPIO,方便在调试的时候临时锁住写入。
驱动层负责 SPI 的初始化、MRAM 的命令封装、读写函数实现。MRAM 的命令集很简单,主要就是 WREN(写使能)、WRITE(写数据)、READ(读数据)、RDSR(读状态寄存器)。没有擦除命令,也没有扇区概念,整个 512KB 空间是线性可寻址的。
应用层就是具体怎么用这块存储,比如参数保存、日志记录、故障快照。这里的关键是设计好数据结构和写入策略,避免频繁写同一地址导致局部过热(虽然 MRAM 寿命几乎无限,但局部反复写同一地址在极端情况下还是会影响可靠性)。
3. 硬件连接与 SPI 时序关键细节
3.1 引脚连接与 PCB 布局要点
MR25H40CDF 是 SOIC-8 封装,引脚定义如下:
| 引脚 | 名称 | 功能 |
|---|---|---|
| 1 | CS | 片选,低有效 |
| 2 | SO | 数据输出(MISO) |
| 3 | WP | 写保护,低有效 |
| 4 | VSS | 地 |
| 5 | SI | 数据输入(MOSI) |
| 6 | SCK | 时钟 |
| 7 | HOLD | 保持,低有效 |
| 8 | VDD | 电源 3.3V |
连接上,CS 接 STM32 的任意 GPIO,SO 接 MISO,SI 接 MOSI,SCK 接 SCK。WP 和 HOLD 如果不用,WP 接 VDD(禁用写保护),HOLD 接 VDD(禁用保持功能)。我一般会把 WP 接一个 GPIO,方便调试。
PCB 布局有几个坑要注意。第一,SPI 走线尽量等长,尤其是 SCK 和 MOSI,在 40MHz 下走线长度差太多会导致时序偏移。第二,MRAM 的电源引脚旁边必须放 0.1uF 去耦电容,越近越好,我一般还会并一个 1uF 的。第三,CS 走线不要和 SCK 平行太长,容易耦合噪声导致误触发。第四,如果板子上有多个 SPI 从机,每个从机的 CS 要独立走线,不要共用。
3.2 SPI 模式与时钟配置
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我一般用模式 0,因为 STM32 的 SPI 默认就是模式 0,配置简单。时钟频率方面,MRAM 最高支持 40MHz,但实际用的时候我建议先降到 10MHz 调试,确认读写正常后再往上提。
STM32F732 的 SPI1 挂在 APB2 上,如果 APB2 时钟是 216MHz,分频系数选 8 的话就是 27MHz,选 4 的话就是 54MHz(超了)。所以 27MHz 是一个比较稳妥的选择。如果你用 SPI2 或 SPI3,APB1 是 108MHz,分频 4 就是 27MHz,分频 2 就是 54MHz(超了)。所以不管用哪个 SPI,27MHz 都是一个安全且够用的频率。
配置代码大概长这样:
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_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 7; HAL_SPI_Init(&hspi1);注意FirstBit要设成 MSB,MRAM 是高位先出的。NSS设成软件模式,片选自己控制。
3.3 片选时序与命令间隔
这是最容易出问题的地方。MRAM 的片选时序要求是:CS 拉低之后,至少要等 5ns 才能开始发 SCK。在 27MHz 下,一个时钟周期是 37ns,所以理论上 CS 拉低后直接发时钟也没问题。但实际调试中,我发现有些板子因为走线电容和驱动能力的问题,CS 拉低后需要等更久。我的做法是在 CS 拉低之后加一个__NOP()或者几个空指令,确保时序稳定。
另一个关键是命令之间的间隔。MRAM 在收到一个命令并执行完之后,需要 CS 拉高再拉低才能接收下一个命令。如果你在 CS 保持低的情况下连续发多个命令,MRAM 会把后面的字节当成数据或者忽略掉。所以每次读写操作都要完整地拉低 CS、发命令、发地址、读写数据、拉高 CS。
还有一个细节:写操作之后需要等待写入完成。MRAM 的写入是纳秒级的,但状态寄存器里的 WIP(Write In Progress)位还是需要检查一下。虽然实际上 MRAM 的写入几乎瞬间完成,但为了保险,我一般会在写完之后读一次状态寄存器,确认 WIP 为 0 再进行下一步。
4. 驱动层实现与核心读写函数
4.1 MRAM 命令集与状态寄存器解析
MR25H40CDF 的命令集很精简,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
状态寄存器只有一位有用,就是 WIP(bit 0)。WIP=1 表示写入正在进行,WIP=0 表示写入完成。其他位都是保留的,读出来是 0。
写操作的流程是:发 WREN 命令 → 发 WRITE 命令 + 24 位地址 + 数据 → 等待 WIP 变 0。读操作的流程是:发 READ 命令 + 24 位地址 → 读数据。
注意地址是 24 位的,因为 512KB 需要 19 位地址,但 MRAM 用 24 位地址格式,高 5 位忽略。发地址的时候要按 MSB 先发的顺序,分三个字节发。
4.2 底层 SPI 读写封装
先封装两个基础函数,一个发一个字节,一个收一个字节。STM32 HAL 库的HAL_SPI_Transmit和HAL_SPI_Receive可以用,但在高速下频繁调用会有开销。我一般直接用寄存器操作,速度更快:
static inline uint8_t spi_transfer(uint8_t data) { while (!(SPI1->SR & SPI_SR_TXE)); *(__IO uint8_t *)&SPI1->DR = data; while (!(SPI1->SR & SPI_SR_RXNE)); return *(__IO uint8_t *)&SPI1->DR; }这个函数同时完成发送和接收,SPI 的全双工特性在这里用得很顺手。注意要先等 TXE(发送缓冲区空)再写 DR,然后等 RXNE(接收缓冲区非空)再读 DR。
片选控制用宏定义:
#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)4.3 写使能与写操作实现
写操作之前必须先发 WREN,这是 MRAM 的硬性要求。WREN 命令发完之后,CS 必须拉高,然后再拉低发 WRITE 命令。这个顺序不能乱。
void mram_write_enable(void) { MRAM_CS_LOW(); spi_transfer(0x06); // WREN MRAM_CS_HIGH(); } void mram_write(uint32_t addr, uint8_t *data, uint32_t len) { mram_write_enable(); MRAM_CS_LOW(); spi_transfer(0x02); // WRITE spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { spi_transfer(data[i]); } MRAM_CS_HIGH(); // 等待写入完成 while (mram_read_status() & 0x01); }这里有个细节:mram_read_status()里面也要控制 CS,所以不能在 CS 拉低的时候调用。写完之后先拉高 CS,再读状态。
4.4 读操作与连续读取优化
读操作比写操作简单,不需要 WREN,直接发 READ 命令加地址就行:
void mram_read(uint32_t addr, uint8_t *data, uint32_t len) { MRAM_CS_LOW(); spi_transfer(0x03); // READ spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { data[i] = spi_transfer(0x00); } MRAM_CS_HIGH(); }连续读取的时候,MRAM 的地址会自动递增,所以你可以一次读一大片数据,不需要每次都发地址。这个特性在读取日志或者参数块的时候很有用。
如果要追求更高的读取速度,可以用 DMA。STM32F732 的 SPI1 有对应的 DMA 通道,配置好之后可以一次传输大量数据,CPU 只需要等 DMA 完成中断。不过对于大多数应用来说,512KB 的数据量用轮询方式读也就几十毫秒,DMA 的收益没那么明显。我一般只在需要高速连续采集的场景下才用 DMA。
5. 应用层数据管理与掉电保护策略
5.1 参数存储的结构设计
参数存储最怕的就是写了一半掉电,导致数据损坏。MRAM 虽然写入速度快,但如果你在写一个多字节参数的时候掉电,还是可能写到一半。解决办法是双备份 + 校验。
具体做法是:把参数分成两个区,A 区和 B 区,每个区都存一份完整的参数,加上 CRC 校验。写入的时候先写 A 区,校验通过后再写 B 区。读取的时候先读 A 区,CRC 不对再读 B 区。如果两个区都坏了,那就用默认参数。
typedef struct { uint32_t magic; // 0x5A5A5A5A uint32_t version; uint8_t data[256]; uint32_t crc; } param_block_t; #define PARAM_A_ADDR 0x00000 #define PARAM_B_ADDR 0x01000写入的时候,先写 A 区,然后读回来校验 CRC,通过后再写 B 区。这样即使写 A 区的时候掉电,B 区还是好的。
5.2 日志记录与循环缓冲区实现
日志记录的特点是写入频繁、数据量大、不需要随机访问。我一般用循环缓冲区的方式,把整个 MRAM 空间分成若干个日志块,每个块有块头和块尾标记,写满一个块就跳到下一个块,写到最后一个块就回到第一个块覆盖最旧的数据。
块头里存一个递增的序列号,这样读取的时候可以按序列号排序,知道哪些是最新的。块尾存一个 CRC,校验数据完整性。
typedef struct { uint32_t seq; // 序列号 uint32_t timestamp; // 时间戳 uint8_t payload[60]; uint32_t crc; } log_entry_t;每个 log_entry_t 是 72 字节,512KB 可以存 7000 多条日志。对于大多数工业场景来说,这个容量够用了。
写入的时候,先读当前块的序列号,加一,然后写入新数据。如果当前块写满了,就跳到下一个块。这里要注意块切换的原子性,我一般会在块头里加一个状态标志,表示这个块是正在写还是已写完。掉电恢复的时候,扫描所有块,找到最后一个状态为“已写完”的块,从它的下一个块开始继续写。
5.3 掉电检测与数据保护
掉电保护的核心是在电源掉到 MCU 工作电压以下之前,把关键数据写完。STM32F732 的工作电压是 1.7V 到 3.6V,MRAM 的工作电压是 2.7V 到 3.6V。如果电源从 3.3V 开始掉,MCU 还能工作一段时间,但 MRAM 可能先失效。
我的做法是在电源输入端加一个大电容(比如 1000uF),掉电后电容能维持几十毫秒的供电。同时用 STM32 的 PVD(可编程电压检测器)监测电源电压,当电压降到 2.9V 的时候触发中断,在中断里把关键数据写完。
PVD 的配置大概是这样:
PWR_PVDTypeDef pvd_config; pvd_config.PVDLevel = PWR_PVDLEVEL_7; // 2.9V pvd_config.Mode = PWR_PVD_MODE_IT_RISING_FALLING; HAL_PWR_ConfigPVD(&pvd_config); HAL_PWR_EnablePVD(); HAL_NVIC_SetPriority(PVD_IRQn, 0, 0); HAL_NVIC_EnableIRQ(PVD_IRQn);在 PVD 中断里,不要做复杂操作,只把最关键的几个变量写进 MRAM。写完之后可以进入低功耗模式或者直接等电源耗尽。
注意:PVD 中断里不要调用 HAL_Delay 或者任何阻塞函数,也不要打印日志。中断里只做最必要的写入操作,越简单越好。
6. 常见问题排查与实操避坑指南
6.1 读写失败与数据错乱排查
调试 MRAM 的时候,最常见的问题就是读出来的数据全是 0xFF 或者全是 0x00。这种情况一般是 SPI 配置不对或者片选没控制好。排查步骤我一般按这个顺序来:
- 用示波器看 SCK、MOSI、CS 的波形。确认 CS 拉低之后 SCK 才开始动,CS 拉高之后 SCK 停止。如果 CS 和 SCK 的时序不对,MRAM 根本不会响应。
- 确认 SPI 模式。MRAM 支持模式 0 和模式 3,如果你配成了模式 1 或模式 2,数据会错位。用逻辑分析仪抓一下波形,看看数据是在 SCK 的上升沿还是下降沿采样。
- 检查地址对齐。MRAM 是字节寻址的,没有对齐要求,但如果你用 16 位数据帧,地址要按 2 字节对齐。
- 确认 WREN 是否成功。写操作之前必须发 WREN,如果 WREN 没发成功,写操作会被忽略。可以读状态寄存器确认 WEL(Write Enable Latch)位是否为 1。
下面这张表整理了我遇到过的问题和解决方法:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 读出来全是 0xFF | MISO 没接好或 SPI 模式错 | 检查接线,确认 SPI 模式 0 或 3 |
| 读出来全是 0x00 | CS 一直拉低或 SCK 没动 | 检查 CS 控制,确认 SCK 有波形 |
| 写入后读出来不对 | WREN 没发或写时序不对 | 确认 WREN 命令,检查 CS 时序 |
| 偶尔数据错乱 | 电源噪声或走线干扰 | 加去耦电容,缩短 SPI 走线 |
| 高速下读写失败 | 时钟太快或时序不满足 | 降低 SPI 时钟,加 CS 延时 |
6.2 SPI 时钟与模式配置陷阱
SPI 模式这个问题我踩过好几次坑。STM32 的 HAL 库里,CLKPolarity和CLKPhase的组合决定了 SPI 模式:
- 模式 0:CPOL=0,CPHA=0(
SPI_POLARITY_LOW+SPI_PHASE_1EDGE) - 模式 1:CPOL=0,CPHA=1(
SPI_POLARITY_LOW+SPI_PHASE_2EDGE) - 模式 2:CPOL=1,CPHA=0(
SPI_POLARITY_HIGH+SPI_PHASE_1EDGE) - 模式 3:CPOL=1,CPHA=1(
SPI_POLARITY_HIGH+SPI_PHASE_2EDGE)
MRAM 支持模式 0 和模式 3,我一般用模式 0。如果你不确定,可以用逻辑分析仪抓波形,看数据在 SCK 的哪个边沿变化。模式 0 是上升沿采样,下降沿变化;模式 3 是下降沿采样,上升沿变化。
还有一个坑是时钟频率。MRAM 标称最高 40MHz,但实际用的时候,如果你的 PCB 走线不好,40MHz 下可能不稳定。我建议先用 10MHz 调试,确认读写正常后再逐步提高。如果提高到某个频率后开始出错,那就是走线或者时序的问题,不要再往上提了。
6.3 写入寿命与数据保持的实测经验
MRAM 的写入寿命标称是 10^14 次,这个数字大到几乎可以忽略。但实际使用中,我还是建议避免反复写同一个地址。虽然 MRAM 不像 Flash 那样有擦除磨损,但局部反复写同一地址在极端情况下还是会影响可靠性。我的做法是在日志记录场景下用循环缓冲区,让写入地址均匀分布。
数据保持方面,MRAM 标称 20 年以上,但这是在常温下。如果工作温度高,数据保持时间会缩短。工业场景下,如果环境温度经常在 85°C 以上,建议定期刷新数据,比如每年把关键参数读出来重写一遍。
还有一个实测经验:MRAM 的写入电流很小,在低功耗场景下很有优势。我测过 MR25H40CDF 的写入电流,大概在 5mA 左右,比 NOR Flash 的 15mA 到 20mA 低不少。如果你的设备是电池供电的,这个差异很关键。
6.4 与 STM32 HAL 库的兼容性注意事项
STM32 的 HAL 库用起来方便,但在 SPI 高速传输的时候,HAL 库的函数开销比较大。HAL_SPI_Transmit和HAL_SPI_Receive每次调用都要检查一堆标志位,在 27MHz 下可能成为瓶颈。我一般会绕过 HAL 库,直接用寄存器操作。
另外,HAL 库的HAL_SPI_TransmitReceive函数在同时收发的时候很好用,但它的超时机制有时候会误触发。如果你在中断里调用它,超时时间设得太短会导致传输失败。我一般把超时设成 1000ms,或者直接用轮询方式自己写。
还有一点:HAL 库的 SPI 初始化函数会重置 SPI 配置,如果你在运行过程中修改了 SPI 的某些参数(比如时钟分频),下次调用HAL_SPI_Init的时候会被覆盖。所以如果要动态调整 SPI 时钟,要么自己操作寄存器,要么在初始化之后不要再调用HAL_SPI_Init。
7. 性能实测与方案扩展思路
7.1 读写速度实测数据
我在 STM32F732IE 上跑了一组实测数据,SPI 时钟 27MHz,模式 0,轮询方式读写:
| 操作 | 数据量 | 耗时 | 等效速度 |
|---|---|---|---|
| 单字节写 | 1 字节 | 约 2.5us | 400KB/s |
| 连续写 | 1KB | 约 320us | 3.2MB/s |
| 单字节读 | 1 字节 | 约 1.8us | 550KB/s |
| 连续读 | 1KB | 约 310us | 3.3MB/s |
| 连续读 | 64KB | 约 19ms | 3.4MB/s |
从数据能看出来,连续读写的速度受限于 SPI 时钟,27MHz 下理论极限是 3.375MB/s,实测 3.2MB/s 到 3.4MB/s 已经接近极限了。单字节操作的开销主要在 CS 控制和命令发送上,所以如果你要写很多小数据,建议攒成一块再写,效率会高很多。
7.2 多设备共享 SPI 总线的处理
如果你的板子上有多个 SPI 从机,比如 MRAM 加一个 Flash 加一个传感器,共享 SPI 总线的时候要注意几点:
第一,每个从机有独立的 CS,不要共用。第二,SPI 模式要统一,如果某个从机只支持模式 0,那其他从机也得用模式 0。第三,时钟频率要取最低的那个,比如 MRAM 支持 40MHz,但 Flash 只支持 20MHz,那整个总线就得跑 20MHz。第四,切换从机的时候要先拉高当前 CS,再拉低目标 CS,中间加一点延时,避免总线冲突。
如果从机的 SPI 模式不统一,那就得用软件模拟 SPI,或者用多个 SPI 接口分别连接。STM32F732 有 3 个 SPI,一般够用了。
7.3 从参数存储扩展到固件日志与故障快照
MRAM 的 512KB 空间,除了存参数,还可以用来存固件日志和故障快照。固件日志就是记录系统运行过程中的关键事件,比如启动、关机、模式切换、错误码。故障快照就是在系统崩溃或者异常的时候,把关键寄存器和变量保存下来,方便事后分析。
故障快照的实现方式是:在系统初始化的时候,把 MRAM 的一块区域预留出来,专门存快照。当发生 HardFault 或者看门狗复位的时候,在异常处理函数里把关键数据写进 MRAM。注意异常处理函数里不能做复杂操作,只把最关键的几个变量写进去就行。
void HardFault_Handler(void) { uint32_t snapshot[16]; snapshot[0] = __get_PSP(); snapshot[1] = __get_MSP(); // ... 保存其他关键寄存器 mram_write(FAULT_SNAPSHOT_ADDR, (uint8_t *)snapshot, sizeof(snapshot)); NVIC_SystemReset(); }这个快照在下次启动的时候读出来,可以帮你快速定位问题。我靠这个机制定位过好几次偶发的硬件异常,比单纯看日志有用得多。
7.4 低功耗场景下的 MRAM 使用技巧
MRAM 的写入功耗很低,但在低功耗场景下,还是有一些技巧可以进一步省电。第一,不写的时候把 CS 拉高,MRAM 在 CS 高的时候进入待机模式,电流只有几微安。第二,降低 SPI 时钟,时钟越低功耗越低,如果对速度要求不高,可以用 1MHz 甚至更低。第三,批量写入,把多次小数据攒成一次大数据写,减少 CS 切换和命令开销。
我测过一组数据:在 3.3V 供电下,MRAM 待机电流约 2uA,写入电流约 5mA,读取电流约 3mA。如果你的设备大部分时间在休眠,只有偶尔写数据,那 MRAM 的平均功耗可以做到很低。
8. 写在最后的一些实操体会
这套方案我在两个工业项目里实际用过,一个是电力监测终端,一个是边缘数据采集器。电力监测终端需要每秒记录一次电压电流数据,用 EEPROM 的话几个月就写坏了,换成 MRAM 之后跑了两年多,一点问题没有。边缘数据采集器需要掉电保存故障快照,MRAM 的纳秒级写入速度让快照保存变得非常可靠,再也不用担心掉电丢数据。
踩过的坑主要集中在这几个方面:SPI 模式配错导致读出来全是 0xFF,CS 时序不对导致写入偶尔失败,PVD 中断里做了太多操作导致数据没写完就掉电了。这些问题在调试的时候都花了不少时间,但解决之后方案就很稳定了。
如果你正在选型,我的建议是:先确认你的写入频率和数据量。如果每秒写入超过 10 次,或者总写入量超过 100 万次,那就直接上 MRAM,不要犹豫。如果写入频率很低,一年也就写几百次,那 EEPROM 也够用,没必要多花钱。MRAM 的价格比 EEPROM 贵不少,但省下来的调试时间和售后成本,绝对值回差价。
最后分享一个小技巧:在 MRAM 里留一块区域专门存“出厂参数”,这块区域在产线烧录的时候写一次,之后永远不写。这样即使应用参数被写坏了,出厂参数还在,可以一键恢复。这个做法在售后维护的时候特别有用,能省掉很多返修的麻烦。