说实话,做工业设备这几年来,数据存储这块一直是最让我头疼的环节。以前项目里用的多是 SPI Flash 或者 EEPROM,小容量场景还能忍,但一旦涉及“掉电瞬间保存现场状态”“频繁记录运行日志”“参数需要可靠更新”这类需求,Flash 的页擦除、写等待和寿命衰减问题就会反复跳出来恶心人。后来在一个基于 STM32L476RG 的工业控制板上,我把存储方案换成了 Everspin 的 MR25H40CDF,也就是一颗 4Mbit 的 SPI 接口 MRAM,这才算是把“存储和读取数据”这件事彻底理顺了。如果你也在嵌入式项目里被非易失存储的可靠性坑过,这篇内容应该能给你不少参考。
MR25H40CDF 这东西,本质上是一颗磁阻随机存储器,容量 512KB,走标准 SPI 接口,跟 STM32L476RG 这种自带多个 SPI 外设的 MCU 配合起来非常顺。它最大的特点是没有擦除概念、写数据不用等待、掉电不丢数据,而且寿命远比 Flash 长。我在这篇文章里会完整讲清楚为什么选它、怎么接线、底层驱动怎么写、掉电保护怎么做,以及量产过程中实际踩过的坑,全程结合 STM32L476RG 的 HAL 库代码说,力求你照着就能复现。
1. 为什么工业数据存储我选了 MRAM 而不是 Flash/EEPROM
1.1 三种非易失存储的可靠性对比
先摆一个常见误区:很多人觉得“非易失存储”都差不多,EEPROM 能存、Flash 也能存,选哪个主要看容量和价格。但在工业现场,这个想法会让你吃大亏。Flash 和 EEPROM 本质上都是电荷存储器件,靠浮栅里的电荷数量来表示 0 和 1。电荷会泄漏,写久了绝缘层会退化,所以它们天生就有两个问题:写入寿命有限、数据保持时间受温度影响大。更麻烦的是,Flash 写入前必须擦除,而擦除是按块(sector)来的,动辄几十毫秒甚至上百毫秒。
MRAM 就不一样。它的存储单元是一个磁隧道结,数据靠磁化方向来保持,而不是靠电荷。写入数据时,实际上是在改变磁化方向,这个过程非常快,而且理论上不会磨损。我拿手头常用的几个器件做了个对比,用起来感受非常直观:
| 项目 | W25Q64(NOR Flash) | AT24C256(EEPROM) | MR25H40CDF(MRAM) |
|---|---|---|---|
| 容量 | 8MB | 256KB | 512KB |
| 写入前是否需擦除 | 需要,按扇区擦除 | 需要,按字节擦除 | 不需要,直接写 |
| 单字节写入时间 | 不具备,按页/扇区写 | 约5ms | SPI时钟周期级,微秒内完成 |
| 擦写寿命 | 约10万次 | 约100万次 | 官方给出1E16次量级 |
| 掉电写失败风险 | 高(块擦除期间掉电会毁数据) | 中 | 极低 |
| 典型数据保持 | 10~20年,高温下打折 | 20年左右 | 20年以上(无电荷泄漏机制) |
从这个表能看出来,MR25H40CDF 在“写”这件事上几乎等同于 SRAM 的体验,但数据又是非易失的。对于工业控制器来说,这一个差异就足以改变整个存储架构的设计方式。
1.2 工业现场真正吃掉的“性能”是什么
很多人选存储只看容量和读写速度,但工业现场真正的需求往往是“关键时刻别掉链子”。我举几个实际项目里遇到的场景。
第一个是掉电保存。设备运行中突然断电,需要把当前工步、累计产量、报警代码、最后一批参数全部存进去。用 Flash 的话,你得提前规划一个空的扇区,写之前还要看它是否需要擦除。掉电检测触发后往往只有几毫秒的维持时间,Flash 一个扇区擦除就要几十毫秒,根本来不及。EEPROM 虽然能单字节写,但一个字节 5ms,存 64 字节状态数据要 300ms 以上,也不现实。MRAM 呢,SPI 时钟一拉,写一个字节就是几十个时钟周期的事,掉电瞬间存几十上百字节毫无压力。
第二个是频繁日志记录。我们有一台设备要每 100ms 记录一次温度值,一天下来就是 86 万次写入。用 W25Q64 这种 Flash,写入寿命 10 万次,不出两天就达到寿命上限。就算用磨损均衡算法,把写入打散到整个 Flash,也只是把几天变成几十天。MRAM 的 1E16 次写寿命意味着每天 86 万次写入,连续写几万年都没问题,完全不需要考虑磨损均衡。
这些场景拼在一起,结论就非常明确了:在小容量、高频率、高可靠性的存储需求面前,MRAM 不是“可选项”,而是“正确选项”。MR25H40CDF 的 512KB 容量也正好够用——存参数、存日志、存掉电状态,全都塞得下,不需要像 Flash 那样为了磨损均衡而大材小用。
2. MR25H40CDF 的引脚、指令与硬件接线
2.1 SOIC-8 引脚定义与典型接线
MR25H40CDF 是标准的 SOIC-8 封装,引脚排列跟常见的 SPI EEPROM/Flash 很像,很多情况下甚至可以 pin-to-pin 兼容替换到现有板子。它的 8 个引脚分别是:
| 引脚号 | 名称 | 功能 |
|---|---|---|
| 1 | CS# | 片选,低电平有效 |
| 2 | SO | 串行数据输出(MISO) |
| 3 | WP# | 硬件写保护,低电平禁止写 |
| 4 | VSS | 地 |
| 5 | SI | 串行数据输入(MOSI) |
| 6 | SCK | 串行时钟 |
| 7 | HOLD# | 暂停传输,低电平有效 |
| 8 | VDD | 电源,3.3V |
接 STM32L476RG 的时候,我的建议是片选 CS# 用一个普通 GPIO,不要用 SPI 的硬件 NSS。原因很简单:硬件 NSS 在极端时序下可能产生误触发,而且如果你的板上还有其它 SPI 器件,用一个软件 CS 可以一个 SPI 总线挂多个设备,灵活得多。我这里用的是 SPI1,PA5 做 SCK,PA6 做 MISO,PA7 做 MOSI,CS 放在 PA4。
WP# 和 HOLD# 两个引脚,如果不用硬件写保护和暂停功能,必须上拉到 VDD,不能悬空。我之前有一版板子把 HOLD# 悬空了,结果在高温测试时偶发读写失败,查了半天才发现是 HOLD# 引脚受干扰误触发,把 SPI 传输暂停了。这种问题在开发阶段非常隐蔽,因为不是每次都复现。
VDD 引脚旁边建议放两个去耦电容,一个 0.1uF 高频去耦,一个 1uF 低频储能,尽量靠近引脚。MRAM 在高速写操作时电流会有一个小的跳变,如果去耦做得不好,可能会影响同板 ADC 采样精度。地线要跟 MCU 共地,不要走太长回路,尤其是工业环境里电磁干扰多一点,布局越干净越省心。
2.2 指令集与 SPI 模式
MR25H40CDF 的 SPI 指令集非常简洁,主用的就六条:WREN(写使能)、WRDI(写禁止)、RDSR(读状态寄存器)、WRSR(写状态寄存器)、READ(读数据)、WRITE(写数据)。
操作的内容很简单:
| 指令 | 指令码 | 功能 |
|---|---|---|
| WREN | 0x06 | 将状态寄存器 WEL 位置 1,允许后续写操作 |
| WRDI | 0x04 | 将 WEL 位清零,禁止写 |
| RDSR | 0x05 | 读取状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 按地址读取,可连续读 |
| WRITE | 0x02 | 按地址写入,可连续写 |
跟 Flash 不同,MRAM 没有“页编程”操作,它是一次一条 READ/WRITE 指令可以连续读写任意长度字节,地址自动递增,直到 CS# 拉高才结束。这种特性让驱动代码变得非常简单——不需要查页边界,不需要算页内偏移,因为 MRAM 本身就没有页的概念。
SPI 模式我使用的是 Mode 0,也就是 CPOL=0、CPHA=0,这是绝大多数 SPI 存储器的标准工作模式。MRAM 的数据手册上一般也会给出 Mode 0 和 Mode 3 两种支持,但我实测下来 Mode 0 最稳,所以统一用 Mode 0。如果你在别的项目里把 MRAM 挂到了一个只支持 Mode 3 的控制器上,建议先查手册确认支持,再调时序验证,不要凭感觉直接跑。
2.3 关于 3 字节地址的一个重点提醒
MR25H40CDF 是 4Mbit 容量,也就是 512KB 空间,需要 19 位地址才能覆盖完整。SPI 指令帧里地址位宽是按 3 字节发送的,发送顺序是 A16~A0 的高字节在前、低字节在后。这个 3 字节地址是很多新人第一天就踩的坑——用惯了 2 字节地址的 64KB EEPROM,看到 MRAM 还以为是“指令 + 2 字节地址 + 数据”,结果地址怎么发都不对。
所以写驱动的时候,地址字段一定要当成 24 位来处理:
uint8_t addr_h = (uint32_t)addr >> 16 & 0xFF; uint8_t addr_m = (uint32_t)addr >> 8 & 0xFF; uint8_t addr_l = (uint32_t)addr >> 0 & 0xFF;这样即使你只在低地址区读写,也强制把三个字节发完整。还有一个细节:如果发生越界访问,MRAM 内部地址会在容量范围内回绕,不会产生硬件异常,但这可能导致“你以为写到了 0x80000,其实是写到了 0x00000”这种隐蔽错误。上层代码必须自己做越界检查,我在第 4 节会专门讲地址分配的策略。
3. STM32L476RG 侧 SPI 配置与底层驱动
3.1 CubeMX 配置要点
STM32L476RG 这颗芯片主频最高 80MHz,自带多个 SPI 外设。MR25H40CDF 的最高 SPI 时钟是 40MHz,所以即便把 SPI 分频配到最高,也够用了。我实际开发时为了稳定性,把 SPI1 时钟配在 10MHz,主要考虑到工业现场走线不会太短,10MHz 是兼顾速度和抗干扰的折中。
CubeMX 里的核心配置:
- MCU 主频:外部晶振或 HSI 倍频到 80MHz
- SPI1:全双工主机,CPOL=0,CPHA=0,8 位数据帧,MSB First
- 波特率分频:分频后 SPI 时钟约 10MHz
- CS# PA4:推挽输出,初始置高
- 其它外设按项目需要配置
这里有一个容易忽略的点:SPI1 挂在 APB2 总线上,而 STM32L476RG 的 APB2 最高时钟是 80MHz,所以 SPI1 的时钟来源可以很高。但 SPI2/SPI3 挂在 APB1 上,APB1 上限通常是 40MHz 或更低,如果你后续要扩展多个 MRAM,注意查一下对应链路的最大频率,别被 APB 时钟限制住。
3.2 按字节随机写的基础驱动
MRAM 的写操作必须先去置位写使能锁存位 WEL。这个机制跟 EEPROM 一样,是为了防止误写。少了 WREN 这一步,WRITE 指令会被直接忽略,而且状态寄存器返回的 WEL 位是 0,光看数据没写进去往往还没发现。写成代码是这么一套流程:
static void mram_wren(void) { uint8_t cmd = 0x06; HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); } static void mram_write_enable_check(void) { uint8_t cmd = 0x05; uint8_t status = 0; HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, &status, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); // 若 WEL 位为 0,说明写使能没成功 } void mram_write_byte(uint32_t addr, uint8_t val) { uint8_t buf[4]; buf[0] = 0x02; // WRITE buf[1] = (uint8_t)((addr >> 16) & 0xFF); buf[2] = (uint8_t)((addr >> 8) & 0xFF); buf[3] = (uint8_t)(addr & 0xFF); mram_wren(); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, buf, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, &val, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }每次写完一个字节,WEL 位会自动清零,所以下一次写之前要重新发 WREN。如果你要连续大批量写,不需要每字节重复发 WREN——一条 WRITE 指令内可以连续发任意字节数据,地址会自动递增。驱动可以改为先发 WREN,再拉低 CS,发完指令头和所有数据后统一拉高 CS,这样效率最高。
3.3 连续读写驱动与字节序处理
读操作跟写类似,但没有 WREN 前置。READ 指令发出后,MCU 持续给 SCK,MRAM 就在 SO 上吐数据。用 HAL 库写一个连续读函数如下:
void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] = 0x03; cmd[1] = (uint8_t)((addr >> 16) & 0xFF); cmd[2] = (uint8_t)((addr >> 8) & 0xFF); cmd[3] = (uint8_t)(addr & 0xFF); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }连续读最需要注意的是 buf 长度和地址边界。如果 len 超过剩余容量,MRAM 会把地址回绕到 0x000000 继续读,这在环形日志场景下可能正好是想要的行为,但普通参数读取时就是 bug 了。所以我在驱动上层会做一个封装,传入指针、长度、目标区间的上下界,先校验再操作。
字节序方面,MRAM 按字节编址,跟 STM32 的内存字节序天然一致。如果你定义了一个结构体,想直接 memcpy 后整块写入 MRAM,要注意结构体内部的 padding。工业数据存储里,我强烈建议不要直接写结构体,而是定义一个明确的、逐字节排列的存储帧格式,配合 CRC 校验,这样设备升级固件后数据结构变了也不会错乱。这一点在第 4 节展开。
4. 掉电保存与存储架构设计
4.1 数据帧格式设计
MRAM 的可靠性再高,也不能保证 SPI 线上的信号永远不受到干扰,更不能保证 MCU 程序不会因为 bug 把错误数据写进去。所以我在 MRAM 里存的所有结构化数据,都套了一个固定帧格式。项目里用的记录结构大概是这样的:
typedef struct __attribute__((packed)) { uint32_t magic; // 固定魔数,如 0x4D52414D uint16_t type; // 记录类型 uint16_t seq; // 序列号,用于新老判断 uint32_t timestamp; // 时间戳 uint8_t payload[64]; // 业务数据 uint16_t crc16; // 对前面所有字节的 CRC 校验 } StorageRecord;magic 用来快速识别一个位置是不是有效记录。seq 序列号解决“掉电时写了一半”的问题——即使 MRAM 写入极快,也存在 MCU 在写完 magic 还没写完 crc 时就断电的可能性,有了 seq,读取端就能判断哪一份记录是完整的。crc16 是最后一道防线,用来捕获接口毛刺或极端干扰导致的比特翻转。
工业项目里,我还会把同一份记录写到两个不同的地址区,形成双备份。读的时候优先取 seq 较新且 CRC 通过的那一份。虽然 MRAM 需要双倍容量,但换来的安全性完全值得。512KB 容量下,就算每条记录 80 字节,双份保存也能存几千条以上。
4.2 环形缓冲与地址分配
我习惯把 MR25H40CDF 的 512KB 空间规划成几块,而不是随手乱写。项目里的大致布局是这样的:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x000000 ~ 0x0000FF | 设备标识区 | 产品序列号、硬件版本、生产日期 |
| 0x000100 ~ 0x0001FF | 参数区 A | 运行参数镜像,带 CRC |
| 0x000200 ~ 0x0002FF | 参数区 B | 参数镜像备份 |
| 0x000300 ~ 0x3FFFFF | 环形日志区 | 每 256 字节一条日志,写指针循环 |
| 0x400000 ~ 0x7FFFFF | 掉电快照区 | 掉电瞬间保存运行状态 |
环形日志区的写指针需要存在一个固定地址里。每次写日志时,先把写指针读出来,写新日志,再把指针更新到下一个位置。如果指针到达日志区末尾,就回绕到日志区起始位置,继续覆盖最老的日志。这种设计依赖 MRAM 的无限写寿命,换成 Flash 早就把写指针那个扇区磨穿了。
掉电快照区不要做的太大。我的经验是,掉电时能写入的数据量取决于主控板上电容能维持供电的时间。STM32L476RG 的功耗很低,再搭配一个几百微法的储能电容,掉电后 MCU 继续跑几毫秒没问题。在这几毫秒内,用 MRAM 写入 64~128 字节的状态快照绰绰有余。
4.3 掉电检测的完整链路
掉电保存的关键是“尽早知道电要没了”。STM32L476RG 内置了可编程电压检测器 PVD,可以设定一个阈值,当 VDD 跌到阈值以下时触发中断。我在 3.3V 系统里一般把 PVD 阈值设在 2.9V 左右,主电源掉电瞬间 PVD 先于 MCU 最低工作电压到来,此时还能安全运行几毫秒。
检测到掉电后的处理流程我建议按这个顺序:
- 进入掉电中断后,先禁掉其它无关中断,把 SPI 中断优先级提到最高。
- 把关键状态数据整理到固定结构的 buffer 里。
- 通过掉电快照区的入口函数一次性写入 MRAM。
- 写完立即拉高 CS,读取状态寄存器确认 WEL 已清零。
- 最后才进入休眠或停机状态。
这里有个容易忽略的地方:如果在掉电中断里调用 HAL_SPI_Transmit,并且锁了超时机制,万一 SPI 总线异常会卡住整个掉电流程。所以我写的掉电保存函数里,HAL_SPI_Transmit 的我们用的是 HAL_MAX_DELAY,并且把 SPI 外设的通信中断关掉,只靠状态寄存器轮询判断完成。这样能保证掉电流程不会因为任何异常而卡死。
另外,MRAM 写入单字节数据的时间基本等同于按 SPI 时钟传输 5 个字节的时间,不存在 Flash 那种“硬件正在编程,外部掉电导致编程中断”的窗口。这也是为什么掉电保存用 MRAM 会让人格外安心——它没有那个“半写坏”的状态。
5. 性能实测与对比
5.1 实测读写时序
我把 MR25H40CDF 接在逻辑分析仪上,实测了不同场景的时序数据。SPI 时钟 10MHz,一次单字节写操作包括 WREN 指令(8 clk)、WRITE 指令头(32 clk,含 3 字节地址)、数据字节(8 clk),总共 48 个时钟周期,折合 4.8 微秒。再加上 CS 拉低拉高的时间,一次完整单字节写不到 6 微秒。
这个数据放在工业场景是什么概念呢?如果掉电时你要存 80 字节状态数据,连续写模式下只需要 80 个时钟周期传输数据,加上指令头 32 个时钟周期,总共 112 个时钟周期,在 10MHz 时钟下就是 11.2 微秒。就算 SPI 时钟降到 2MHz,也就是 56 微秒。而掉电维持时间通常是毫秒级,这差距大到几乎可以忽略。
更重要的对比是 W25Q64 的擦除时间。W25Q64 擦除一个扇区(4KB)大约需要 50ms 量级,写入一页 256 字节也要 0.7ms 左右。这意味着如果要更新一条分布在两个扇区的数据,必须先擦除、再写入、再轮询等待,整个流程可能有上百毫秒的不确定窗口。在掉电场景下,这就是灾难。
5.2 与 W25Q64 Flash 的实测对比
我在同一块板子上,把日志存储驱动分别跑在 W25Q64 和 MR25H40CDF 上,对比了几个指标:
| 测试项目 | W25Q64 | MR25H40CDF |
|---|---|---|
| 写 4KB 数据(不含擦除) | 约 15ms(页编程) | 约 3.3ms @10MHz |
| 擦除一个扇区再写 | 约 50ms + 15ms | 不需要擦除 |
| 掉电现场保存 80 字节 | 不可靠,需预先擦好扇区 | 约 11us,稳定 |
| 连续写 10 万次后 | 接近寿命极限,开始出现坏块 | 无感觉 |
| 软件复杂度 | 需要擦写均衡、坏块管理 | 无擦写管理 |
直观感受是,用 Flash 的时候,写一条日志要考虑“当前扇区擦干净了没有”“写到一半跨了页怎么办”“这个扇区是不是已经快挂了”。换成 MRAM 之后,这些通通不用想了,写就是了。我甚至把原来的 Flash 磨损均衡层直接删掉,代码量少了一大截,bug 面也小了。
当然,MRAM 不是没有缺点。单价高是最大的短板,同样容量下比 Flash 贵不少。所以我的选型原则是:日志、参数、掉电快照这类小容量关键数据放 MRAM,音视频、大文件这类海量数据还是老老实实放 Flash 或 SD卡。把合适的数据放在合适的存储里,整体成本才可控。
6. 量产现场遇到的坑与注意事项
6.1 温度等级与型号后缀核对
MR25H40CDF 这个具体型号,CDF 后缀对应的是 SOIC-8 150mil 封装。但不同完整型号的温度等级可能不一样,有的批次是商业级(0~70°C),有的是工业级(-40~85°C),还有一些是车规级。如果你做的是工业设备,一定要在 BOM 清单里明确温度等级要求,不要只写一个模糊的“MR25H40”。我身边就有同事因为只写核心型号,结果采购到了商业级版本,做高低温测试时读写异常,排查了两天才发现问题出在芯片等级上。
采购渠道也要注意。MRAM 因为用量相对小众,仿造芯片没那么多,但翻新件是有的。正规代理商渠道最稳妥,到货后建议抽检:先读全片的地址是否回绕正常,再在关键位置写满 0x55/0xAA 测试,最后对数据做回读校验。反正 MRAM 写入很快,产线测试烧录时间不会成为瓶颈。
6.2 焊接与板级可靠性
SOIC-8 的焊接本身没什么难度,但 MRAM 内部有磁性存储单元,焊接工艺里有两个注意点。一个是回流焊温度曲线,峰值温度不要超芯片规格书上限,常见的无铅曲线峰值 260°C 持续 10 秒内问题不大,但不要在峰值区反复回流。另一个是清洗,如果用超声清洗,注意功率不要开太大,长时间强超声对内部结构不友好,能用免洗助焊剂尽量用免洗的。
手工焊接的时候,给 WP# 和 HOLD# 两个引脚焊上拉电阻时特别注意别连锡。SOIC-8 引脚间距 1.27mm,不算密,但如果不涂助焊剂硬焊,还是容易把相邻引脚桥接。焊完拿万用表量一下 VDD 对 VSS 有没有短路,再量 WP# 和 HOLD# 是不是都被拉到了高电平。
6.3 产线测试与长期可靠性验证
MRAM 的无限写寿命不等于“你随便写就能不出错”。我在产线测试里加了几个检测项,建议你也参考:
- 全址遍历回读测试:写入递增型数据,读出来比对。
- 极值数据测试:写 0x00 和 0xFF 交替模式,检测地址线和数据线短路。
- 掉电反复测试:在随机时刻反复断上电 100 次,验证掉电快照区的数据一致性。
- 高温老化后的数据保持测试:在 85°C 下老化若干小时后,重新读数据,确认没有漂移。
另外,我强烈建议在产品固件里保留一个“存储自检”的隐藏命令。现场设备出问题时,远程让设备执行一次全区间读回校验,能以最快速度区分是 MCU 软件问题还是 MRAM 硬件问题,能省掉大量现场排查时间。
还有一个容易被忽略的点是,MRAM 属于非易失存储,但芯片出厂时内部数据可能是随机的,不要指望它默认全是 0xFF。第一次上电时要先对整个参数区和日志区进行初始化,写入初始标记,之后再正常读写。否则可能出现“读到了看似合理但其实是无效数据”的假象。
最后说点实在的
我把方案从 Flash 切到 MR25H40CDF + STM32L476RG 之后,最明显的变化不是性能数据好看,而是存储相关的 bug 几乎消失了。以前调掉电保存要熬夜抓时序,现在掉电保存就是“把 buffer 丢给写函数”这么简单;以前担心 Flash 写寿命不够要设计各种均衡策略,现在直接写,代码砍了一大截。我个人实际体会是,如果项目里对可靠性要求高、写入频繁、数据量又不是特别大,用一颗 SPI MRAM 代替 Flash/EEPROM,是一个非常划算的选型决策。至于怎么把地址规划好、驱动怎么写、掉电链路怎么搭,上面这些经验你拿过去直接落地就行。如果后面做了更深入的多机通信存储方案,或者搞了远程固件升级里的双备份逻辑,我再单独写一篇出来聊聊。