在工业设备里做数据存储,最烦的不是“存不下来”,而是“存了不敢用”。我以前做过一台控制柜数据记录仪,要存运行参数、故障码、累计运行时间,系统随时可能掉电,数据必须一掉电就能留住。用EEPROM寿命撑不住频繁写入,用NOR Flash又有块擦除和磨损均衡的问题,后来直接换了 Everspin 的 SPI MRAM 芯片 MR25H40CDF,搭配 STM32L073RZ 这颗低功耗 MCU,一次把读写驱动、掉电保护和数据一致性全部做完了。这篇就把这个组合从选型到硬件、从驱动到调试的完整过程写出来,给做工业控制器、数据记录仪、电池设备或者任何需要高速频繁写非易失数据的嵌入式项目当参考。
1. 为什么最后选了 MR25H40CDF:工业存储选型的底层逻辑
1.1 三种常见非易失存储的特性差异
工业设备里用到非易失存储,最常见的三个选项是 EEPROM、NOR Flash 和 MRAM。很多人第一反应是“EEPROM 便宜”“NOR Flash 容量大”,但真放到项目里,每个都有让你难受的地方。
EEPROM 的问题在于寿命。常规 SPI EEPROM 写入擦除寿命也就是 100 万次左右,看着挺多,但设备每秒钟要写一次当前状态,连续跑半个月就逼近极限。我上一台设备就是这么踩坑的,两个月后数据区偶发读回 0xFF,排查到最后发现是 EEPROM 某块地址坏了。而且 EEPROM 页写入有限制,跨页写还得拆开处理。
NOR Flash 的问题是擦除。写数据之前必须先把目标块擦成 0xFF,一次擦除最小单位是一个扇区,通常 4KB 起步。假如你只想修改一个 16 字节的参数,也要先读出整个 4KB、擦除、再写回去。这不仅慢,还引入掉电风险——擦完了写一半断电,现场数据直接飞了。要规避就得做双备份、操作日志,软件复杂度一下子上去。
MRAM 的工作原理完全不同,它靠磁阻状态保存数据,而不是靠电荷。磁存储不存在电荷泄漏,所以数据保持不依赖刷新;也不存在擦除过程,写入可以直接把 0 写成 1、把 1 写成 0。它本质上是一颗“速度像 SRAM、断电保留像 Flash”的存储芯片。MR25H40CDF 就是 Everspin 串行 MRAM 家族里容量 4Mbit、SPI 接口的型号,工业温度范围,8 引脚 DFN 封装,非常对工业场景的口味。
1.2 为什么不是 FRAM
有人会问:FRAM(铁电存储)不也是按字节写、不需擦除吗?FRAM 确实有同样的优点,我也在别的项目里用过富士通那颗 MB85RS256。但 MR25H40 在某些维度上更香。FRAM 常见容量做到 256Kbit、1Mbit 就算大了,4Mbit 的 FRAM 不是主流,价格也不友好。MR25H40CDF 是 4Mbit,存日志缓冲、存曲线数据、存多份参数备份都有富余。MRAM 的读改写接口是 SRAM 兼容的,SPI 时钟可以跑到 40MHz,而不少 SPI FRAM 官方上限在 25MHz 左右,对 MCU 应用差别不大,但持续高频读改写时 MRAM 的时序余量更充足。
另外从供应链角度看,Everspin 这颗料在很多工业存储板卡上用了很多年,工业级后缀的供货稳定,不像某些消费级 FRAM 容易停产换料。所以最终我的选择是:常规参数存储用 MRAM,不需要到底层文件系统。
1.3 这颗芯片到底解决了什么问题
把需求拆开看,MR25H40CDF 主要替我解决了三件事。
第一,写入延迟几乎为零。SPI 发完写命令和数据,数据立刻进存储阵列,不用像 Flash 那样等内部 program 状态机跑完。对掉电保存场景非常关键,掉电瞬间你根本没有几百毫秒慢慢等擦写完成。
第二,写入寿命长到不用考虑。EEPROM 要算寿命余量,Flash 要做磨损均衡,MRAM 不需要。数据手册标称读出改写次数在 10 的 14 次方量级,工程上等于无限次。我可以放心地每秒刷一次运行计数器,不用设计磨损均衡算法。
第三,按字节写。想改哪个地址就写哪个地址,没有页、没有扇区、没有擦除。代码写起来直白很多,逻辑也少很多。
2. MR25H40CDF 的硬指标解读:读数据手册时最容易忽略的几处
2.1 容量、地址与指令集
MR25H40CDF 容量 4Mbit,也就是 512KB,按字节寻址。虽然地址线逻辑上只需要 19 位,但 SPI 传输地址时约定用 3 个字节,也就是 24 位地址帧。高 5 位必须填 0,如果你把地址寄存器清零习惯性写成 4 字节地址,多出来的字节会被当作第一个数据字节,整帧数据就错位了。这是这类 SPI MRAM 和某些支持 4 字节地址模式的 SPI NOR Flash 不一样的地方,代码里要盯紧。
指令集和大多数 SPI 存储兼容,核心就这几个:
- WREN(0x06):写使能
- WRDI(0x04):写禁止
- RDSR(0x05):读状态寄存器
- WRSR(0x01):写状态寄存器
- READ(0x03):读数据
- WRITE(0x02):写数据
其中 WRITE 指令可以连续写任意字节数,地址会自动递增,到最高地址后回绕。没有 Flash 那种“必须写满一页”的限制。这一点在驱动设计里非常舒服,我后面连续写几百字节故障记录时,根本不用管页边界。
2.2 速度、寿命与保持特性
MR25H40C 这个后缀的 SPI 时钟上限是 40MHz,但 STM32L073RZ 的 SPI 最高主频到不了那么高,所以瓶颈在 MCU 侧,不在存储侧。实测里我用 16MHz SPI 时钟跑读写,512KB 全量重写大概两三百毫秒,这个速度对工业现场参数保存来说已经快到没感知了。
数据保持特性手册上给的是 20 年以上,实际上磁存储的保持特性在常温下非常保守,我见过的老工业板卡上十几年前的 MRAM 数据读出来原封不动。注意一点:如果环境温度长期超过 85℃,保持时间会打折扣,选料时要确认后缀对应的工业温度等级,不要拿商业级硬顶。
另外 MR25H40CDF 没有 Flash 那种块擦除,也不存在读改写比较慢的问题。它的写周期结束后状态寄存器 WIP 位自动清零,代码里轮询一下是为了时序严谨,实际写操作快得经常第一次读状态就发现已经不忙了。
2.3 WP、HOLD 引脚的坑
很多人在 SPI MRAM 上第一次翻车都是因为 HOLD 引脚。MR25H40CDF 的 HOLD 引脚低电平有效,一旦拉低,芯片会暂停当前 SPI 通信,此时时钟边沿再多数据也不进。问题是这颗引脚在 DFN 封装里如果不处理,悬空受到干扰自动拉低,就会出现“偶尔整段数据读出来是旧值、主机也没报错”的诡异故障。正确做法是 HOLD 直接接 VCC,不通过 GPIO 控制。
WP 引脚是写保护。系统里如果希望软件能改状态寄存器,就把 WP 拉高;如果希望关键区域硬件锁定,可以用 GPIO 控制 WP,正常运行时拉高,需要解保护时再拉低。注意 WP 只保护状态寄存器里的块保护位对应的区域,不是把整片写死,具体保护范围和位配置要看手册里状态寄存器的定义。
3. STM32L073RZ 与 MR25H40CDF 的硬件对接:原理图、引脚与总线设计
3.1 引脚分配建议
STM32L073RZ 是 Cortex-M0+ 内核的超低功耗 MCU,192KB Flash、20KB RAM,自带 3 个 SPI 外设,性能够用而且待机功耗低,很适合做工业数据记录主控。我用的是 SPI1,引脚分配如下:
| 信号 | MCU 引脚 | 说明 |
|---|---|---|
| SCK | PA5 | SPI1 时钟 |
| MOSI | PA7 | 主机输出、存储输入 |
| MISO | PA6 | 存储输出、主机输入 |
| NSS/CS | PA4 | 片选,GPIO 软件控制 |
| WP | PC0 | 可选,GPIO 输出,默认拉高 |
| HOLD | 接 VCC | 硬上拉,不做 GPIO |
片选建议用 GPIO 软件控制,而不是开硬件 NSS。原因很简单:软件控制 CS 可以在任何时刻精确拉高拉低,方便做 WREN 和后续指令之间的时序控制;硬件 NSS 在某些外设配置下会在传输开始前自动触发,行为不如软件直接。如果你非要用硬件 NSS,务必把 NSS 配置成硬件模式并确认极性和时序一致,否则容易踩坑。
MISO 是存储芯片输出,不能只上拉或者悬空,正常 MCU 的 SPI 输入引脚要配成浮空输入。SCK 和 MOSI 在 MCU 侧是输出,我一般配上拉关闭,速度等级选 High,保证 SPI 时钟 16MHz 时边沿不够肉。
3.2 电源与 PCB 布线
MR25H40CDF 工作电压范围 2.7V 到 3.6V,STM32L073RZ 同样运行在 3.3V,所以直接共用电源域,不需要电平转换。供电电路在芯片 VCC 附近放一个 100nF 去耦电容,电源入口再放一个 10μF 钽电容或者陶瓷电容,把纹波压住。
这里有个工业设计常见的坑:如果系统里有继电器、接触器、电机这类感性负载,母线电源跌落瞬间会直接砸到 MRAM 供电上。MRAM 本身没有 Flash 那种内部高压泵,对电源要求不算苛刻,但 VCC 掉到欠压阈值以下再恢复,可能出现上电复位不完全的情况。稳妥做法是给存储芯片单独加一个小 RC 滤波或者在 VCC 路径串一个磁珠,同时保证 MCU 的 BOR(欠压复位)阈值高于存储芯片的最低工作电压,让 MCU 先复位,存储芯片不会处于不确定状态。
PCB 布局上,SPI 信号线要短,特别是 MISO 回传线不要绕远。十几MHz 的 SPI 信号在工业设备里不算高速,但如果线走得很长,建议 SCK 上串联一个 22Ω 电阻用于抑制振铃。CS 线上我额外加了一个 1nF 电容到地,防止上电瞬间毛刺误触发片选。实测下来这样处理后,即使旁边有接触器动作,也没有出现过传输错误。
3.3 和 Flash 或 SD 卡共享 SPI 总线
MR25H40CDF 是标准 SPI 设备,可以和 SPI NOR Flash、SD 卡共享同一路 SPI 总线,只要片选分开即可。共享总线时唯一要注意的是:不同芯片的最高 SPI 频率可能不同。如果 Flash 只支持 50MHz,MRAM 能跑 40MHz,但你的 MCU 只能跑 16MHz,那大家统一跑 16MHz 就行,反而最省事。我在一个项目里同时挂了 W25Q64 和 MR25H40CDF,代码里给两个设备分别封装了 read/write 函数,底层共用同一个 SPI 句柄,切换设备时先拉高当前 CS,再拉低目标 CS,避免片选竞争。
还有一个细节:如果 CS 拉高之后立刻拉低,中间最好加几个空指令周期延时。某些存储芯片在 CS 拉高后需要 tHSL(片选高电平最小时间)才能进入下一帧,MR25H40 手册里也有类似要求。我的驱动里统一在每次片选翻转之间加了一个 1μs 的延时,成本几乎为零,但能避免很多边界时序问题。
4. 读写驱动的完整实现:SPI 时序与关键函数拆解
4.1 SPI 外设初始化
SPI 初始化先要确认工作模式。MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1),我统一用 Mode 0,和大多数 SPI 从设备一致,也方便和其他外设复用。时钟极性极性低、第一边沿采样,MSB 先行,8 位数据宽度。这是标准配置,不用为 MRAM 做额外改动。
SPI_HandleTypeDef hspi1; void MRAM_SPI_Init(void) { 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_2; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1); }时钟分频系数取决于 APB 总线时钟。STM32L073 的 SPI 外设虽然挂在 APB 上,但分频后实际 SPI 时钟最好控制在 16MHz 以内,留出信号完整性余量。如果 PCB 走线短、环境噪声小,可以试着把分频系数降下来跑满;如果现场有强干扰,保守一点跑 8MHz 更稳。
4.2 写使能与状态轮询
写数据之前必须先发 WREN。这个逻辑和很多 SPI 存储一样,每次写序列前都要发送 0x06,把芯片内部写使能锁存置 1。之后才能执行 WRITE 或 WRSR 指令。完整流程是:CS 拉低,发 0x06,CS 拉高,这是独立的指令帧。
void MRAM_WriteEnable(void) { uint8_t cmd = 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); MRAM_CS_HIGH(); }状态寄存器轮询在写入大量数据时也要带上。虽然 MRAM 写入几乎瞬间完成,但严谨的驱动还是应该在每次写指令发出后读取状态寄存器,直到 WIP 位清零。HAL 的阻塞传输函数实际上已经包含了等待 SPI 空闲,但这不等同于等待存储芯片非忙,两者是不一样的。
void MRAM_WaitNotBusy(void) { uint8_t cmd = 0x05; uint8_t sr = 0x80; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); while (sr & 0x01) { HAL_SPI_Receive(&hspi1, &sr, 1, 100); } MRAM_CS_HIGH(); }4.3 读写函数实现
写数据函数以 4 字节命令帧开头:指令 0x02,24 位地址高字节、中字节、低字节,随后直接跟数据。连续写任意字节,不需要额外的页编程结束命令,CS 拉高就是结束。要注意地址高字节必须清零,否则会覆盖到其他地址空间。
void MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; uint32_t i; MRAM_WriteEnable(); header[0] = 0x02; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, header, 4, 1000); for (i = 0; i < len; i++) { HAL_SPI_Transmit(&hspi1, &buf[i], 1, 1000); } MRAM_CS_HIGH(); MRAM_WaitNotBusy(); }读数据函数同理,指令 0x03 加 24 位地址,然后连续接收数据。唯一要注意的是,读操作不会改变状态寄存器的 WIP 位,所以读之前不用发 WREN。
void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; header[0] = 0x03; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, header, 4, 1000); HAL_SPI_Receive(&hspi1, buf, len, 1000); MRAM_CS_HIGH(); }如果使用的是嵌入式 Linux 环境,SPI 外设换成设备树里的 spidev 节点,设置 spi-max-frequency 和 spi-cpol、spi-cpha 属性即可,芯片侧的读写时序完全一致,上述逻辑可以直接翻译成 ioctl 读写帧。
4.4 驱动层的一些小细节
第一个细节:不要依赖 HAL 的 HAL_SPI_TransmitReceive 连续收发同一个数组。MRAM 读操作需要先发地址帧再收数据,中间不要拉高 CS,我上面的实现是先单独发 4 字节,再单独收数据,保持一致。HAL 的 Transmit 和 Receive 之间 CS 不能拉高,否则芯片认为这一帧结束了,后面接收的全是无效数据。
第二个细节:MRAM 地址递增到最高地址后回绕。如果连续写长度超过剩余地址空间,后面的数据会写到 0x00000。这不是错误,是芯片行为。工业日志存储里如果一定要求不覆盖,要自己在驱动层限制长度,判断 addr + len 是否超过 0x80000(512KB)。
第三个细节:MR25H40CDF 没有坏块概念,不需要做坏块管理。这一点和 NAND Flash 完全不同。驱动层只要保证地址不越界、数据帧完整,就不会出现“某块地址写不进去”的问题。做文件系统也不是不行,但没必要,我用裸地址加一个头部结构体做参数管理,简单直接。
5. 工业现场的可靠性设计:掉电、数据一致性与低功耗配合
5.1 掉电保存:在断电瞬间抢时间写数据
工业设备里最危险的时刻就是掉电瞬间。PLC 或伺服驱动器主回路断电,控制板还在慢慢放电,留给 MCU 保存关键数据的时间窗口通常只有几毫秒到几十毫秒。这时候 Flash 的擦除机制就是灾难:扇区擦除可能就要 50ms,还没等写完,电压就掉没了。
MRAM 在掉电场景的优势是先天性的。写入不需要擦除,也不存在内部编程状态机,SPI 发完数据,数据就已经在存储阵列里了。我的掉电保护流程是这样设计的:
- 配置 STM32L073 的 PVD(可编程电压检测器),阈值设在 2.9V。当 VDD 跌到 2.9V 以下,触发 PVD 中断。
- 在 PVD 中断服务函数里,立刻保存关键运行参数。因为 3.3V 供电掉到 2.9V 时还有足够余量维持 MCU 运行几百微秒到几毫秒,这个窗口足够把几十字节参数写完。
- 写完最后一条状态后,把“掉电标志”置位,然后等待复位或进入停机。
实测中,我在 PVD 中断里用 16MHz SPI 写 64 字节参数,加上 WREN、地址帧、状态轮询,总耗时不到 100μs,对电源余量来说毫无压力。同样的数据量如果用 SPI Flash,光擦除就至少消耗 200ms,还没算写入,根本做不完。
要注意的是 PVD 中断服务函数里尽量不要调用 HAL_Delay 这类依赖 SysTick 的函数。掉电时主时钟可能不稳定,实时性优先,直接用寄存器操作发送 SPI 数据更可靠。如果项目对响应时间敏感,可以做一个简单的宏让 SPI 发送在中断里快速执行。
5.2 数据一致性:多份备份加 CRC
掉电保存不等于数据一定不丢。虽然 MRAM 写字节很可靠,但如果你一次要写多个字节的数据块,中途掉电可能导致这帧数据只写了一半。这种情况在 EEPROM 和 Flash 上靠页写入和块擦除也不能完全避免,我的做法是双备份加校验。
参数区划成两份,A 区和 B 区,每份头部存 16 位 CRC、数据长度和递增序号。写入流程是:先写 A 区,再写 B 区,每次比较两个区域的序号,序号大的认为最新。当 A 区写了一半掉电,B 区还是完整的旧数据,上电后能识别出 A 区 CRC 校验失败,直接用 B 区。
这个方案在 Flash 上会因为擦除和写放大变得异常复杂,但在 MRAM 上实现起来非常轻松。没有块擦除,就没有“擦一半掉电”的问题;没有页写限制,数据块大小随便定义。我最终用了一个 512 字节的头部结构体存设备参数,A/B 双份加上 CRC32,上电启动时校验一次,最多浪费 1KB 空间,换来的是数据绝对可靠。
5.3 低功耗模式的配合
STM32L073RZ 是超低功耗 MCU,很多项目要求休眠时整机电流做到微安级。MR25H40CDF 的工作电流和待机电流大概在毫安级,比普通 SPI Flash 的微安待机高,这一点必须在电源设计里处理,否则整机休眠电流会超标。
我的建议是给 MRAM 加一个独立的负载开关。用一个 GPIO 控制 P-MOSFET 或负载开关芯片给 MRAM 供电,系统进入低功耗模式前,先把 MRAM 的 CS 拉高、SPI 引脚拉成固定电平,再关掉 MRAM 电源。唤醒后再上电,重新初始化 SPI。MRAM 上电后数据仍然保留,不需要额外恢复过程,这一点也比 SRAM 加电池的方案省心。
除非项目对休眠电流完全无所谓,否则不要把 MRAM 直接接在常供电源上。工业设备上电后一直工作不在乎那几毫安,但电池供电的便携仪表就是另一回事了。
5.4 文件系统与日志存储
如果只是配置参数存储,裸地址管理就够了。但如果要存历史曲线、操作日志这类持续写入的数据,建议在 MRAM 上做一个简单的环形缓冲。因为不需要擦除,环形缓冲的实现比 Flash 简单太多了:写指针从 0 增长到末尾就回绕,读指针跟着走。MRAM 允许覆盖写任意地址,不存在“块擦除后写入失败”的问题。
我在一个数据记录项目里做了一个环形日志,每条记录 128 字节,每 100ms 写一条,用 512KB 空间可以存 32K 条记录,也就是 53 分钟的连续记录。循环写入完全靠地址取模,没有任何磨损均衡逻辑,跑了一个星期没有异常。如果换 Flash,这种高频度循环写入早就把寿命耗光了。
6. 实测记录与调试踩坑:几个值得提前避开的细节
6.1 HOLD 引脚悬空导致偶发通信失败
第一次焊接测试板时,MRAM 的 HOLD 引脚没接,因为我当时想“不用的引脚悬空就行”。结果板子跑起来之后,连续读写几百次之后偶尔一次读回来全是旧数据。用逻辑分析仪抓 SPI 波形,看到 CS 正常拉低、SCK 正常翻转、MOSI 数据正常,但 MISO 就是没有响应。最后查手册确认问题就是 HOLD 引脚悬空,干扰把它拉低,芯片把后续时钟当作暂停信号处理。
定位过程持续了一个多小时,其实一开始就应该按手册把 HOLD 接 VCC。这个教训值三百字,给后来者提个醒:MRAM 的 HOLD 引脚和 WP 引脚都不是普通 GPIO,HOLD 必须接死,WP 要么上拉到 VCC 要么接 GPIO,绝不能悬空。
6.2 SPI Mode 接反产生全 0xFF
另一个低级但常见的错误是 SPI Mode 配置反了。MR25H40CDF 支持 Mode 0 和 Mode 3,但如果你配置成 Mode 2,读状态寄存器会一直读到 0xFF,写数据后回读也全是 0xFF,看起来像“芯片没焊好”或者“片选没拉低”。其实问题只是时钟极性和相位不匹配而已。
遇到这种症状先别怀疑芯片,翻开手册对照 CPOL/CPHA 和实际初始化代码。我建议统一用 Mode 0,也就是 CPOL=0、CPHA=0,因为这是绝大多数 SPI 从设备的默认模式,后续复用总线也更不容易出错。
6.3 地址高字节没清零导致写入错位
早期驱动里我写的地址帧是把 uint32_t 地址直接拆成三个字节,没有屏蔽高字节。结果地址 0x10000 写入时,高字节变成 0x01,芯片把这个 24 位地址按 19 位有效位截断后映射到了低端地址,数据存到了一个奇怪的位置,回读参数时总以为是存储内容被篡改了。后来在驱动层加了一行:
header[1] = (addr >> 16) & 0xFF;把高 5 位强制清零,问题立刻消失。这种低级错误在调试时非常恶心,因为症状很随机,你得反复对照地址映射才能发现。
6.4 信号完整性:长走线和时钟边沿
第三版 PCB 为了布线方便,SPI 线绕了比较远,MRAM 离 MCU 大概有 5cm。跑 16MHz 时示波器看 SCK 波形有振铃,MISO 数据在采样点附近出现抖动,偶发读错位。后来把 SPI 时钟降到 8MHz,同时在 TCP 焊接引脚附近的 MISO 线上加了一个 10pF 电容做滤波,波形立刻干净了。
如果项目对写速度没有极致要求,16MHz 和 8MHz 的差异基本无感。工业设备里稳定比速度快重要得多,我现在的习惯是:SPI NOR Flash 能跑多快跑多快,MRAM 统一保守用 8MHz,因为它的优势本来就不在极限速度,而在字节写能力、寿命和掉电可靠性。
6.5 上电时序:不要和 MCU 同时复读
还有一个比较少人提的坑:MRAM 和 MCU 共用一个电源时,如果上电过程 VCC 斜率太慢,MRAM 可能进入不确定状态,首次读数据会读到随机值。这不是数据丢,是芯片还没准备好。解法是在驱动初始化时做一个“伪读操作”:上电后延时至少 1ms,发一次 0x03 读地址 0x00000 的数据,丢弃结果,然后再正常读写。
我甚至在正式读关键参数前加了一个简单的自检流程:写一个 magic 字节到固定地址,回读对比,再写回原值。MRAM 没有磨损顾虑,这种自检可以随便做,对排查上电时序问题特别有用。
最后的实际体会
做完整套系统之后,我的感受是:MR25H40CDF 和 STM32L073RZ 的组合,最舒服的不是某个单一指标,而是它把存储这件事彻底简化了。我可以像操作 RAM 一样操作非易失数据,不需要关心擦除、不需要关心寿命、不需要担心写一半掉电丢数据。后面再让我接手需要频繁写参数的工业项目,我大概率会直接沿用这套方案。最后再分享一个可执行的小技巧:新板子到手后先不用写复杂业务逻辑,就写一个循环,随机地址写随机长度数据再回读比对,跑上三五万次,能快速暴露硬件焊接、SPI 时区和引脚配置的所有问题。比直接调应用代码省心得多。