1. 为什么在工业场景里我会优先考虑 MRAM 而不是 Flash
如果你做过工业数据采集、PLC 扩展模块或者电力监测终端,大概率遇到过同一个问题:设备跑在现场,三天两头掉电,每次掉电之后存储的数据要么丢几个字节,要么整个扇区直接读不出来。用 EEPROM 吧,写入速度慢得让人抓狂;用 NOR Flash 吧,擦除周期又有限,频繁写同一块区域,寿命很快就见底了。我前几年做一个配电柜里的电能质量监测模块,就是因为用 SPI NOR Flash 存故障录波数据,现场跑了不到一年,坏块就出来了,返修率直接拉高。
后来接触到MR25H40CDF这颗 MRAM(磁性随机存储器),才算是找到了一个比较舒服的平衡点。MRAM 的核心特点在于:它用磁性隧道结存储数据,写入不需要先擦除,字节级随机写,几乎没有擦写寿命限制(官方标称 10^14 次以上),而且掉电数据保持几十年。这些特性放在工业嵌入式场景里,简直就是为“频繁写、随时断电、长期免维护”量身定做的。
这篇文章我打算把MR25H40CDF 配合 STM32F732IE的完整存储与读取方案讲透。STM32F732IE 是 ST 家 F7 系列里带 DSP 和 FPU 的 Cortex-M7 芯片,主频 216MHz,工业现场用得很广。两者之间走SPI接口,硬件连接简单,软件上 HAL 库也能快速跑通。我会从器件选型逻辑、硬件连接细节、SPI 时序配置、读写驱动实现、掉电保护策略、实测踩坑这几个维度展开,尽量把每一步“为什么这么做”讲清楚,让你看完能直接抄作业。
提示:MRAM 不是万能的,它的容量通常比 Flash 小、单价也更高。选型前先算清楚你的数据量、写入频率和寿命要求,别盲目上。
2. MR25H40CDF 的存储特性与工业适配逻辑
2.1 磁性隧道结到底比浮栅晶体管强在哪
要理解 MRAM 为什么适合工业场景,得先搞清楚它和 Flash、EEPROM 的存储原理差异。Flash 和 EEPROM 都属于浮栅器件,写入靠的是把电子注入或抽离浮栅层,这个过程中氧化层会被反复冲击,时间长了就会漏电,也就是所谓的擦写寿命。NOR Flash 一般标称 10 万次擦写,EEPROM 好一点能到 100 万次,但都架不住工业现场那种“每秒写一次、连续写几年”的节奏。
MRAM 的存储单元是磁性隧道结(MTJ),结构上是两层铁磁材料夹一层极薄的绝缘势垒层。其中一层是固定磁化方向,另一层是自由磁化方向。写入时通过电流产生的自旋转移矩来翻转自由层磁化方向,读取时则利用两层磁化方向平行或反平行导致的电阻差异来判断 0 和 1。整个过程没有电子穿越氧化层的击穿行为,所以寿命极长,官方给的数据是 10^14 次写入,换算下来就算每秒写一次也能写三百万年。
另一个关键点是写入不需要擦除。Flash 写之前必须先擦整个扇区,擦除时间长、功耗高,而且擦除过程中如果掉电,整个扇区数据都可能损坏。MRAM 是字节级直接覆盖写,写一个字节就是一个字节,没有“先擦后写”这个中间状态,掉电安全性天然更好。
2.2 MR25H40CDF 的关键参数与选型对照
MR25H40CDF 是 Everspin 家的 4Mbit(512KB)SPI MRAM,工作电压 2.7V 到 3.6V,工业级温度范围 -40℃ 到 +85℃,SPI 接口最高支持 40MHz 时钟。下面这张表是我整理的和常见存储方案的对比,方便你选型时快速判断。
| 特性 | MR25H40CDF (MRAM) | W25Q64 (NOR Flash) | AT24C512 (EEPROM) |
|---|---|---|---|
| 容量 | 512KB | 8MB | 64KB |
| 接口 | SPI | SPI | I2C |
| 写入方式 | 字节级覆盖写 | 扇区擦除后写 | 字节级写 |
| 擦写寿命 | 10^14 次 | 10 万次 | 100 万次 |
| 写入速度 | 无擦除延迟 | 擦除慢 | 慢 |
| 掉电保持 | 20 年以上 | 20 年 | 40 年 |
| 单价 | 高 | 低 | 中 |
| 工业适配 | 极佳 | 一般 | 较好 |
从表里能看出来,MR25H40CDF 的短板就是容量和价格。512KB 对于存配置参数、故障记录、运行日志、校准系数这些完全够用,但你要是想存大量波形数据,那就得考虑更大容量的方案或者外扩。我的经验是:把 MRAM 当作“高频写入的关键数据区”,把 Flash 当作“低频写入的大容量数据区”,两者分工,成本和可靠性都能兼顾。
2.3 什么数据该放 MRAM,什么数据不该放
这个问题我在项目里被问过很多次。我的判断标准很简单:看写入频率和掉电敏感度。下面这几类数据,我强烈建议放 MRAM:
- 实时运行参数:比如电机的转速设定、PID 参数、累计运行时间,这些可能每秒都在更新。
- 故障录波索引:故障发生瞬间的标记、时间戳、通道号,掉电必须保住。
- 校准系数:传感器出厂标定的斜率、截距,写一次但绝不能丢。
- 掉电保护上下文:系统断电前需要保存的中间状态,下次上电要恢复。
反过来,像历史波形文件、固件备份、大量日志归档这些,写入频率低、数据量大,用 Flash 更划算。我一般会在系统里做一层抽象,把 MRAM 当“热存储”,Flash 当“冷存储”,上层应用不用关心底层介质。
3. STM32F732IE 与 MR25H40CDF 的硬件连接与 SPI 配置
3.1 硬件连线:别小看那几根线
MR25H40CDF 是标准 SPI 从设备,引脚包括 SCLK、SI、SO、CS#,另外还有 WP# 和 HOLD# 两个控制脚。STM32F732IE 这边我用的是 SPI1,引脚分配如下:
| MR25H40CDF | STM32F732IE | 说明 |
|---|---|---|
| CS# | PA4 | 硬件片选,也可以用软件片选 |
| SCLK | PA5 | SPI1_SCK |
| SI | PA7 | SPI1_MOSI |
| SO | PA6 | SPI1_MISO |
| WP# | 3.3V | 写保护,不用就拉高 |
| HOLD# | 3.3V | 保持,不用就拉高 |
| VCC | 3.3V | 电源 |
| GND | GND | 地 |
这里有几个细节值得说。第一,WP# 和 HOLD# 一定要处理,不能悬空。悬空的话引脚电平不确定,可能导致写入被意外保护或者通信被挂起。我一般直接拉到 3.3V,除非你确实需要硬件写保护功能。第二,CS# 建议用硬件片选,虽然软件片选也能用,但硬件片选在 SPI 外设层面自动控制,时序更稳,尤其在高频时钟下。第三,电源去耦别省,MRAM 芯片旁边放一个 0.1uF 加一个 1uF 的电容,工业现场电源纹波大,去耦不好容易出现读写错误。
3.2 SPI 模式与时钟极性的选择依据
MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1)。我一般用Mode 0,因为 STM32 HAL 库默认配置就是 Mode 0,省事。时钟频率方面,芯片最高支持 40MHz,但实际用的时候要考虑 PCB 走线和信号完整性。我一般先降到 10MHz 调通,再逐步往上提,最后稳定在 20MHz 到 30MHz 之间。
STM32F732IE 的 SPI1 挂在 APB2 总线上,时钟源是 216MHz,分频系数可以选 2 到 256。要得到 20MHz,分频系数设 8 就行(216/8=27MHz,实际会取整到可用分频)。下面是我用 CubeMX 配置 SPI1 的关键参数:
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_HARD_OUTPUT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;注意NSS设成SPI_NSS_HARD_OUTPUT之后,PA4 会自动被 SPI 外设控制,你不需要手动拉片选。但有个坑:HAL 库的HAL_SPI_Transmit在硬件片选模式下,片选会在传输开始和结束时自动翻转,如果你一次传输分多次调用,片选中间会跳变,导致命令解析出错。所以要么一次传完,要么改用软件片选。
3.3 上电初始化顺序与复位注意事项
MR25H40CDF 上电后不需要特殊复位序列,但有一个细节:上电后第一次访问前,建议先发一个 WREN(写使能)再发 WRDI(写禁止),把内部状态机复位到已知状态。这个操作在很多 MRAM 和 Flash 的数据手册里都有提到,但容易被忽略。我实测下来,不做这一步偶尔会遇到第一次写不进去的情况,做了之后就再没出现过。
初始化流程我一般这么写:
- 配置 SPI1 的 GPIO 和参数,使能 SPI。
- 延时 10ms 等 MRAM 电源稳定。
- 发送 WREN 命令(0x06)。
- 发送 WRDI 命令(0x04)。
- 读取状态寄存器(RDSR,0x05),确认 WEL 位为 0。
- 读取设备 ID(RDID,0x9F),确认返回值匹配。
设备 ID 读取是个很好的自检手段。MR25H40CDF 的 RDID 返回 1 字节制造商 ID 加 2 字节设备 ID,制造商 ID 是 0xEverspin 的编码。如果读回来全是 0xFF 或者 0x00,说明 SPI 通信有问题,先查线再查配置。
4. 读写驱动实现:从命令帧到页写缓冲
4.1 MR25H40CDF 的命令集与帧结构
MR25H40CDF 的命令集和标准 SPI Flash 很像,但少了擦除相关命令,多了些 MRAM 特有的东西。常用命令如下:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读设备 ID |
读数据的帧结构是:先发 0x03,再发 3 字节地址(24 位),然后连续读出数据,地址自动递增。写数据的帧结构是:先发 WREN,再发 0x02,再发 3 字节地址,然后连续写入数据。注意写之前必须先发 WREN,否则写操作会被忽略。这一点和 Flash 一样,但 MRAM 不需要擦除,所以流程更短。
4.2 单字节读写与页写缓冲的实现
先看单字节写。很多人以为 MRAM 可以随便连续写,其实它内部有页缓冲的概念。MR25H40CDF 的页大小是 256 字节,一次 WRITE 命令最多写 256 字节,超过页边界会回卷到页首覆盖。所以写跨页数据时,必须分多次写,每次不超过页边界。
下面是我封装的单字节写函数:
void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[4]; cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_WriteEnable(); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, &data, 1, 100); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }这里我改用了软件片选,因为硬件片选在分两次HAL_SPI_Transmit时中间会释放 CS,导致命令和数据被拆成两帧,MRAM 解析会出错。软件片选虽然多两行代码,但控制更灵活。
页写函数要注意边界处理:
void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t page_remain = 256 - (addr % 256); if (len <= page_remain) { MRAM_WriteRaw(addr, buf, len); } else { MRAM_WriteRaw(addr, buf, page_remain); MRAM_WritePage(addr + page_remain, buf + page_remain, len - page_remain); } }读函数就简单多了,READ 命令没有页限制,可以连续读整个芯片:
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; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, buf, len, 1000); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }4.3 状态寄存器轮询与写完成判断
MRAM 的写操作比 Flash 快得多,但并不是瞬间完成。写命令发出后,芯片内部需要时间完成磁性翻转,典型值是几十纳秒到几百纳秒。在 SPI 时钟 20MHz 下,一个字节传输就要 400ns,所以实际上你发完写命令,芯片早就写完了。但为了保险,我一般会在写操作后读一次状态寄存器,确认 WEL 位被清零。
状态寄存器格式如下:
| 位 | 名称 | 说明 |
|---|---|---|
| 7 | WPEN | 写保护使能 |
| 6-3 | 保留 | 读为 0 |
| 2 | BP1 | 块保护 1 |
| 1 | BP0 | 块保护 0 |
| 0 | WEL | 写使能锁存 |
WEL 位在 WREN 后置 1,写操作完成后自动清 0。如果写完读回来 WEL 还是 1,说明写操作没执行,可能是 CS 时序有问题或者 WREN 没发成功。
5. 掉电保护与数据完整性策略
5.1 为什么 MRAM 也需要掉电保护设计
有人会问:MRAM 掉电数据不丢,那还需要掉电保护吗?答案是需要,但保护的对象不同。Flash 的掉电保护是为了防止擦除过程中断电导致扇区损坏,MRAM 没有这个问题。但 MRAM 的掉电保护要解决的是多字节写入的原子性问题。
举个例子:你要保存一个结构体,里面有 10 个参数,总共 40 字节。如果写到第 20 字节时系统掉电,那前 20 字节是新数据,后 20 字节是旧数据,下次上电读出来就是一个“半新半旧”的脏数据。MRAM 虽然每个字节都写成功了,但整体逻辑上是不一致的。
5.2 双区备份加校验字的实现方案
我的做法是双区备份加 CRC 校验。把关键数据分成 A、B 两个区,每个区包含数据本体、一个序列号和一个 CRC32 校验值。写入时先写 A 区,再写 B 区,两个区的序列号递增。读取时比较两个区的序列号和 CRC,选序列号大且 CRC 正确的那个。
具体流程:
- 上电读取 A 区和 B 区的序列号和 CRC。
- 如果 A 区 CRC 正确且序列号大于等于 B 区,用 A 区数据。
- 如果 B 区 CRC 正确且序列号大于 A 区,用 B 区数据。
- 如果两个区都损坏,加载默认值并报警。
写入时:
- 计算新数据的 CRC 和序列号(旧序列号加 1)。
- 写入序列号较小的那个区(也就是被覆盖的区)。
- 写入完成后更新内存中的序列号。
这样即使写入过程中掉电,最多只有一个区损坏,另一个区仍然可用。CRC32 我一般用 STM32 硬件 CRC 外设,速度快,不占 CPU。
5.3 写入频率与寿命的实测数据
前面说 MRAM 寿命 10^14 次,但实际项目里我还是会控制写入频率。原因有两个:一是频繁写会增加功耗,二是 SPI 总线被占用会影响其他任务。我的做法是在 RAM 里维护一份数据副本,定时(比如 1 秒)或者检测到掉电信号时才写入 MRAM。
掉电检测我用的是 STM32F732IE 的 PVD(可编程电压检测器),配置在 2.9V 触发。当 VDD 降到 2.9V 时,PVD 中断触发,在中断里把关键数据紧急写入 MRAM。从 2.9V 降到 2.7V(MRAM 最低工作电压)大概有几百微秒到几毫秒的窗口,足够写几百字节。我实测写 256 字节在 20MHz SPI 下大约 150 微秒,完全来得及。
注意:PVD 中断里不要做复杂运算,只做数据搬运。CRC 计算提前在正常运行时算好,中断里直接写。
6. 实测踩坑记录与排查思路
6.1 读回全是 0xFF 的三种可能原因
第一次调 MR25H40CDF 的时候,我读设备 ID 回来全是 0xFF,折腾了大半天。后来总结出三种常见原因:
第一种是 MISO 线没接好或者接反了。SPI 的 MISO 是从设备输出、主设备输入,如果和 MOSI 搞反了,主设备收到的就是自己发出去的数据或者悬空电平。用示波器量一下 MISO 在片选有效时有没有波形,很快就能定位。
第二种是 SPI 模式不匹配。MR25H40CDF 支持 Mode 0 和 Mode 3,如果你配成 Mode 1 或 Mode 2,数据采样边沿就错了,读回来全是 0xFF 或者乱码。检查 CPOL 和 CPHA 是否和从设备一致。
第三种是片选时序问题。硬件片选模式下,HAL 库在两次HAL_SPI_Transmit之间会释放 CS,导致命令帧和数据帧被分开。MRAM 收到不完整的命令帧,自然不响应。改成软件片选,或者把命令和数据拼成一个缓冲区一次发出去。
6.2 写入成功但读出来是旧数据的排查链路
还有一种情况更隐蔽:写操作返回成功,状态寄存器 WEL 也清零了,但读出来还是旧数据。我遇到过两次,一次是地址算错了,一次是页边界回卷。
地址算错的情况是这样的:我用uint16_t存地址,但 MRAM 是 512KB,需要 19 位地址,uint16_t最大 65535,超过 64KB 的地址就溢出了。改成uint32_t之后问题消失。这个坑很典型,地址宽度一定要和芯片容量匹配。
页边界回卷是另一个坑。MR25H40CDF 的页大小是 256 字节,如果你从地址 0x00F0 开始写 32 字节,写到 0x0100 时会回卷到 0x0000,把页首的数据覆盖了。我当时的测试代码没做边界判断,写了一段跨页数据,结果页首几个字节被莫名其妙改掉。后来加了页边界拆分逻辑,问题解决。
6.3 SPI 时钟提速后的信号完整性验证
把 SPI 时钟从 10MHz 提到 30MHz 之后,我遇到了偶发的读写错误,大概每几千次操作错一次。用示波器看波形,发现 SCLK 上升沿有过冲,MISO 数据建立时间不够。解决办法有三个:
- 缩短走线:MRAM 尽量靠近 STM32,走线控制在 5cm 以内。
- 串接电阻:在 SCLK 上串一个 22 欧姆电阻,抑制过冲。
- 降低时钟:如果 PCB 已经定型,把时钟降到 20MHz,错误率降到零。
工业现场我一般不建议跑满 40MHz,20MHz 到 25MHz 是比较稳妥的区间。速度快那点收益,抵不上一次数据错误带来的返修成本。
6.4 工业温度范围内的参数漂移观察
我在高低温箱里做过 -40℃ 到 +85℃ 的循环测试,MR25H40CDF 的读写功能全程正常,但发现一个现象:低温下写入时间略微变长。数据手册标称的写周期是 35ns 典型值,但在 -40℃ 时实测大概到 50ns 左右。虽然对于 SPI 传输来说这点差异可以忽略,但如果你用 GPIO 模拟时序,就要把延时留够。
另外,高温下 MRAM 的待机电流会上升,85℃ 时比 25℃ 大概高 30%。如果系统是电池供电,这个功耗预算要算进去。
7. 把 MRAM 用好的几个工程习惯
调通 MR25H40CDF 和 STM32F732IE 的 SPI 读写只是第一步,真正让它在工业现场稳定跑几年,靠的是一套工程习惯。我自己的做法是:驱动层做自检,应用层做冗余,系统层做监控。
驱动层自检包括上电读设备 ID、定期读状态寄存器、写后校验。我一般会在系统启动时做一次全片 CRC 校验,把关键区域的数据读出来算一遍,和存储的 CRC 比对。如果不对,说明存储区有问题,触发报警。
应用层冗余就是前面说的双区备份加序列号。这个方案虽然多占一倍空间,但换来的可靠性提升非常值。512KB 的 MRAM 拿一半出来做冗余,还剩 256KB 可用,对于大多数参数存储场景足够了。
系统层监控是记录写入次数和错误次数。虽然 MRAM 寿命极长,但记录一下没坏处。我一般会在 MRAM 里划一小块区域专门存统计信息,包括总写入次数、CRC 错误次数、上次写入时间戳。这些数据在排查现场问题时非常有用。
还有一点:SPI 总线要加互斥保护。如果系统里 SPI1 上挂了多个从设备,MRAM 的读写操作必须和其他设备的操作互斥,否则片选会冲突。我一般用 RTOS 的互斥量,或者用简单的标志位加临界区保护。裸机系统里,我会在 MRAM 操作前后关中断,确保时序不被打断。
最后分享一个我在实际项目里总结的小技巧:把 MRAM 的读写封装成带重试的接口。SPI 通信在工业现场受干扰的概率不低,一次读失败不代表芯片坏了。我的接口里默认重试 3 次,每次失败后重新初始化 SPI 并延时 1ms。实测下来,很多偶发错误重试一次就过了,系统稳定性明显提升。这个重试逻辑放在驱动层,上层应用完全无感,用起来很省心。