☰
MRAM + STM32F765ZI:工业存储替代Flash与EEPROM的完整方案
2026/10/4 1:00:58 网站建设 项目流程

上个月调试一台工业数据记录仪,客户最初用的是 SPI NOR Flash 存运行日志,算完写寿命之后整个人都不好了:每两分钟记一条 64 字节的现场报文,按 10 万次擦写寿命去估,一块 Flash 用不到一年就要开始担心坏块和数据丢失。换 EEPROM,容量和写速度又都跟不上。最后换成 Everspin 的 MR25H40CDF 配合 STM32F765ZI,存储和读取数据的问题才算真正解决。

这套 MR25H40CDF 与 STM32F765ZI 的组合,在工业和嵌入式应用里做参数存储、运行日志和掉电保护非常合适。MRAM 容量不算大,但胜在不需要擦除、写寿命极长、掉电内容不丢,正好卡在传统 Flash 和电池型 SRAM 之间的空档。如果你也在纠结“用哪种非易失存储”或者“M7 跑日志怎么优化”,这篇就把选型逻辑、硬件接法、软件驱动、性能实测和现场坑一次讲清楚。

1. 为什么这个组合适合工业存储场景

1.1 工业现场存储的三大痛点

工业设备里的存储,和消费电子产品完全不是一回事。首先是温度范围,很多现场机柜里夏天能到 70℃,冬天又是零下,消费级 Flash 的老化速度明显加快。其次是断电,设备说停就停,监控拍门、PLC 掉电、驱动器急停,存储介质必须在电压跌到零的瞬间保住最后一条记录。第三是写寿命,工业设备动不动设计运行 10 年以上,日志、计数值、校准参数都在持续写,普通 Flash 的擦写次数会先耗尽。

我之前见过不少工程师用 Winbond 之类的 SPI NOR Flash 存日志,容量是够了,但架构上要先擦除再写入,抽样 5 分钟存一条记录,每天 288 次,一个月 8600 次。如果按 10 万次擦写寿命算,理论上两年就到期,实际还要打折。换 EEPROM 的话,容量通常只有几十 KB,又要用 I2C,速度也上不来。所以一遇到“连续写、不许丢、要抗恶劣环境”的存储需求,MRAM 几乎是唯一不用做太多妥协的选择。

1.2 MR25H40CDF 的核心特性拆解

MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,容量 512KB,按字节寻址,接口是标准四线 SPI。这类芯片最吸引我的几点,都是冲着工业场景去的:

  • 写操作不需要先擦除。Flash 写之前要块擦除,MRAM 直接覆盖写,省掉一整个软件状态机。
  • 写寿命超过 10^15 次。这个数字在工程上基本等于“无限次”。就算每秒写 1000 次,也要写 3 万多年才到标称极限。
  • 无写延迟。写命令发完,数据就算落住了,不需要像 Flash 那样轮询 BUSY 位,也不用等 3 到 5 毫秒的编程时间。
  • 读操作不限制次数,也没有读扰动问题。NOR Flash 反复读同一块会有软错误风险,MRAM 没有这个问题。
  • 数据保持能力通常按 20 年往上给,工作温度范围也能覆盖工业级 -40℃ 到 +85℃,部分型号还能到更高的温度。

这里要提醒一句:不同后缀的型号电压和温度等级可能不一样,比如 C 后缀通常对应 3.3V 工作电压,DF 这类封装/温度变体要以官方选型表为准。我自己的习惯是上板之前把编码规则打印出来贴工位上,不然很容易被一堆字母后缀搞混。

1.3 STM32F765ZI 为什么是合适的搭档

STM32F765ZI 是 ST 的高性能 M7 MCU,主频 216MHz,2MB Flash,512KB RAM,外设接口非常全。它跟 MRAM 搭配,不是“能用”,而是“好用”。

首先是 SPI 数量管够,F765 有多个 SPI/QSPI 外设,MRAM 挂普通 SPI 完全不吃紧,甚至可以同时挂多颗做冗余存储。其次是 DMA 和 Cache 配合,M7 的 D-Cache 虽然在 DMA 一致性问题上有坑,但性能上限高,日志可以整块 DMA 搬运,不占 CPU。第三是 STM32 的生态成熟,不管是 CubeMX 初始化还是 HAL 驱动,十行代码就能把 SPI 外设跑起来,把精力集中在存储管理逻辑上。

如果以后想把 MRAM 直接映射到地址空间,F765 的 QUADSPI 外设也支持单线 SPI 的 memory-mapped 模式,读取可以当普通指针访问。不过写路径通常还是要走命令接口,实际项目里我会根据“读写频率”来决定要不要上这个功能。

2. 硬件连接与信号设计

2.1 引脚分配与接线表

MR25H40CDF 最常见的封装是 8 脚 SOP,引脚功能很标准。我用 STM32F765ZI 的 SPI1 做示例,硬件 NSS 不用,CS 由 GPIO 手动控制,这样每次操作的起始和结束都完全可控,也方便接多个 SPI 从设备。

下面是实际接线表:

MRAM 引脚功能方向STM32F765ZI 引脚备注
CS#输入PA4GPIO 推挽输出,软件控制低有效
SCK输入PA5SPI1_SCK
SI输入PA7SPI1_MOSI,MCU 输出到 MRAM
SO输出PA6SPI1_MISO,MRAM 输出到 MCU
WP#输入VCC写保护禁用,直接接高
HOLD#输入VCC暂停功能禁用,直接接高
VCC电源3.3V按型号手册确认电压范围
GND地GND单点接地,靠近芯片放置

接线看起来简单,但 WP# 和 HOLD# 一定不能悬空。这两个引脚内部虽然一般有上拉,但工业现场干扰多,悬空状态下一旦被拉出毛刺,轻则这次操作无效,重则把后续 SPI 时序搞乱。我在量产板上都是直接通过 10kΩ 电阻接到 VCC,不省这两个位置。

另外要注意 MOSI 和 MISO 别接反。更稳妥的做法是:上电后用 SPI 发一个 READ 命令,先读状态寄存器或者连续读 0 地址,如果能读到非 0xFF 内容,说明数据线方向基本正确,再开始正式调试。

2.2 电源、去耦和布局建议

MR25H40CDF 的工作电压要以具体型号手册为准,常见是 3.3V 系统。STM32F765ZI 的 VDDA/VDD 同样用 3.3V,供电上可以共用一组电源,但建议给 MRAM 单独加 100nF 瓷片电容,位置尽量靠近 VCC 引脚,有条件再加一个 4.7µF 或 10µF 的钽电容做中低频退耦。

工业设备还有一个很实际的点:电源波动。现场机柜里大电机启动、继电器吸合都会让 3.3V 出现瞬时跌落。MRAM 本身是非易失的,但 SPI 通信期间电压如果掉到芯片工作阈值以下,协议可能中断,数据不一定丢,可你无法确定写命令是否完整执行。所以我的做法是在主板上加一个带延时的电压监控复位芯片,电压低于阈值马上把 MCU 和 MRAM 的 CS 拉高,强制终止当前存储操作。这样比靠软件轮询电压靠谱得多。

2.3 SPI 速率与信号完整性

MR25H40CDF 的 SPI 时钟上限通常标到 50MHz 左右,但别一上来就冲满速。工业设备布线不一定干净,PCB 上如果有长走线、过孔和插座,高速 SPI 很容易出现回读数据和发送数据错误。我的建议是先用 10MHz 把功能和数据校验逻辑调通,再用 20MHz 做整片读写压力测试,最后再根据实际误码率决定要不要上 40MHz 或更高。

判断信号质量不需要太高深的设备,一台逻辑分析仪同时抓 CS、SCK、MOSI、MISO 四根线就够了。重点看 SCK 上升沿处的数据建立是否稳定,CS 在命令前后有没有毛刺。实测数据是:20MHz 对这个 8 脚小封装已经很从容,40MHz 只要走线短也能稳定跑,但收益并不明显,因为工业日志通常不是带宽瓶颈。

3. 嵌入式软件实现:SPI 驱动与读写流程

3.1 初始化中最容易错的三个点

在 STM32F765ZI 上初始化 SPI1,用 CubeMX 可以一键生成,但有三个点我会手动确认:

第一,主模式、8 位数据、MSB 先行,这是标准 SPI 从设备的默认偏好。第二,NSS 必须选软件模式,也就是SPI_NSS_SOFT,这样才不会被硬件自动拉 CS 干扰。第三,CPOL/CPHA 要设为模式 0,也就是空闲时 SCK 为低、第一个边沿采样。MRAM 一般也兼容模式 3,但模式 0 在 STM32 上最省心,所有调试工具默认抓的都是模式 0。

如果以后要接另一颗只支持模式 3 的传感器,就要为每个外设单独建 SPI 配置句柄,切换时先调用HAL_SPI_DeInit再重新初始化。不要把 SPI1 的 Mode 参数直接改一改了事,因为同一外设的时钟极性和相位改变后,内部 FIFO 和同步逻辑需要重新复位,否则偶发数据错位会让你查到头秃。

3.2 写入函数的完整逻辑

MR25H40CDF 的写操作指令集和常见的 SPI NOR Flash 很像:先发 WREN(0x06)使能写,再发 WRITE(0x02)和 24 位地址,后面跟上要写入的数据。区别是整个写过程完全不需要擦除,也不需要等待。

下面是我在项目里用的最小可用代码:

#define MR25H40_WREN 0x06 #define MR25H40_WRITE 0x02 #define MR25H40_READ 0x03 #define MR25H40_RDSR 0x05 #define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET) uint8_t mram_write_buf(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4]; /* 1. 发送写使能命令 */ cmd[0] = MR25H40_WREN; MRAM_CS_LOW(); if (HAL_SPI_Transmit(&hspi1, cmd, 1, 100) != HAL_OK) { MRAM_CS_HIGH(); return 1; } MRAM_CS_HIGH(); /* 2. 发送写命令、地址和数据 */ cmd[0] = MR25H40_WRITE; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_CS_LOW(); if (HAL_SPI_Transmit(&hspi1, cmd, 4, 100) != HAL_OK) { MRAM_CS_HIGH(); return 1; } if (HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, len, 1000) != HAL_OK) { MRAM_CS_HIGH(); return 1; } MRAM_CS_HIGH(); return 0; }

这里有个细节:HAL 的传输超时参数我给得很长,但实际 20MHz 写 256 字节也就 100 微秒左右,所以超时更多是防总线异常时死等。CS 拉高时机也很关键,必须等所有字节都发送完再拉高,绝不能提前拉,否则芯片会把最后一截数据丢掉。

关于 WREN,虽然 MRAM 不像 Flash 那样需要上电后先擦除,但保持“先写使能再写数据”的时序习惯是没坏处的。如果后续你把代码复用到其他 SPI MRAM 或 FRAM 上,这套流程也能直接平移。

3.3 读取函数与 DMA 接入思路

读取比写入更简单,发 READ 命令和地址后,CS 保持低电平,连续读 MISO 上的数据即可。MRAM 会自动地址递增,读完当前地址会自动走到下一地址,直到 CS 拉高。

uint8_t mram_read_buf(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] = MR25H40_READ; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_CS_LOW(); if (HAL_SPI_Transmit(&hspi1, cmd, 4, 100) != HAL_OK) { MRAM_CS_HIGH(); return 1; } if (HAL_SPI_Receive(&hspi1, buf, len, 1000) != HAL_OK) { MRAM_CS_HIGH(); return 1; } MRAM_CS_HIGH(); return 0; }

读操作里 HAL_SPI_Receive 会自动在 MOSI 上发空字节,这对 MRAM 没有影响,因为 READ 命令已经把当前地址定好了,后续 MOSI 上的数据不会改变读取位置。

如果要做整块日志读取,建议改成 DMA 方式:先用HAL_SPI_Transmit_DMA发命令,再用HAL_SPI_Receive_DMA收数据。但只要你用的是 STM32F765ZI,就绕不开 D-Cache 一致性问题。DMA 写数据前要SCB_CleanDCache_by_Addr,DMA 读数据完成后要SCB_InvalidateDCache_by_Addr,而且传入的缓冲区和长度必须做 32 字节对齐,否则会清到别人的 Cache 行。

3.4 数据校验与掉电保护设计

MRAM 的位存储可靠性很高,但工业环境里“硬件没问题、协议失败”的情况永远存在,所以数据帧层面还是要有校验。我的日志格式一般是这样:

字段长度说明
Header4 字节魔术字,如 0xA5 0x5A 0x01 0x00
序列号4 字节单调递增,用于检测丢包
时间戳4 字节系统运行时间
数据体N 字节现场采集数据
CRC324 字节对整个记录做校验
Valid 标志1 字节写入完最后置 0x55

写记录前先把前面所有内容写入,最后再写 Valid 标志。读取时先检查 Valid 标志是否为 0x55,如果不是就当这条记录不完整,回退到上一条有效记录。这个“提交标志”的办法比单纯 CRC 更靠谱,因为即使掉电发生在写一半的时候,你也能识别出哪条是残缺数据。

4. 数据组织与应用模式扩展

4.1 参数配置存储:开机就读、改了就写

工业设备最常见的需求是存校准系数、通信参数、设备序列号。这类数据量不大,但要求不能因为频繁修改而写坏,并且掉电后必须保留。MRAM 的优势在这里非常明显,参数区可以直接用固定地址管理,每次修改直接覆盖写,不需要考虑“先擦后写”的问题,更不需要 Flash 那种“磨损均衡”策略。

我会在 Flash 的 0x100 地址开始存一组结构体,用memcmp对比新旧值,如果值没变化就直接跳过写操作。虽然 MRAM 写寿命极高,但没必要让总线闲着没事干。参数区还放一个版本号,每次修改参数时版本号加一,上电时如果版本号异常,就回退到备份区。有条件的可以预留两套参数区,一套主用,一套备份,启动时选版本号合法且 CRC 正确的那套。

4.2 运行日志环形缓冲:不磨损、无擦除

如果做运行日志,MRAM 还有个大优势:可以做成环形缓冲而不需要垃圾回收。NOR Flash 的日志系统最烦的是写满后要擦块,擦块期间系统基本不能继续记录,还得做磨损均衡。MRAM 则是写到哪算哪,写满地址最大值后直接回卷到起始地址,把旧记录覆盖掉,整个流程不需要任何擦除操作。

代码上只需要维护两个变量:当前写地址和当前记录数。每次上电时先扫描一遍日志区,找到最后一个 Valid 标志为 0x55 的记录,把写指针定位到它的后面。如果环形缓冲跨过了 0x7FFFF 回卷边界,就对写地址做一次取模。这里的判断逻辑不复杂,但一定要先画出“地址回卷”的边界状态再敲代码,否则很容易在 0x7FFFF 附近多写一条记录或者漏读一条记录。

4.3 是否要挂文件系统

我经常被问:“能不能给 MRAM 挂 LittleFS?”理论上可以,因为 MRAM 可以模拟成块设备,但我不推荐在 512KB 这种容量和设备场景里强行上文件系统。LittleFS 的设计目标是针对 Flash 的擦除特性,虽然它不强制每块都先擦除,但它对坏块处理、日志结构、块大小都有预设,移植到 MRAM 上要改不少东西,最后收益却很小。

工业现场存储,我更愿意自己定一套简单的“地址段 + 记录头 + CRC”方案。代码量少,逻辑透明,出问题好定位。真的需要文件系统时,可以选择专门为无擦除存储设计的简单日志 FS,或者干脆把 MRAM 当一个大数组管理,启动时扫描签名来确定最新文件位置。

5. 性能实测与对比记录

5.1 和 Flash、EEPROM 做个直观对比

如果只看数据手册,很多人对 MRAM 没有概念。我把这三类存储放在一张表里,看完就明白定位了:

对比项SPI NOR Flash常见 EEPROMMR25H40CDF
典型容量1MB 到 64MB1KB 到 256KB512KB
写前擦除必须块擦除不需要不需要
写寿命10 万到 100 万次100 万次上下大于 10^15 次
页编程时间0.5ms 到 3ms5ms 级别无写延迟
数据保持通常 10 年以上10 年以上手册通常给 20 年以上
读干扰存在,需软件处理基本无无

从这个表能看出,MRAM 不是用来替代大容量存储的,它替代的是“需要频繁改写的小容量关键数据区”。工业设备里真正需要几十 MB 的图片、波形数据,依旧交给 SD 卡或 NorFlash;但系统参数、运行日志、掉电瞬间要保存的现场状态,用 MRAM 最合适。

5.2 实测写入和读取耗时

我在参考板上用逻辑分析仪抓过完整时序,条件是 20MHz SPI,一次读写 256 字节。理论计算是:4 字节命令加 256 字节数据,一共 260 字节,也就是 2080 bit,20MHz 下约 104 微秒。加上 HAL 函数调用和 CS 反转开销,实测单次写事务大约 140 微秒,读取也差不多。

整片 512KB 数据量,在 20MHz 下理论读写时间约 0.21 秒。如果只是每次写 16 字节的记录,单次事务开销大约 30 多微秒,每秒可以轻松处理上万条。这个性能对绝大多数工业日志需求来说绰绰有余。

还有一点值得注意,实测稳定性最好的状态是 SPI 时钟 20MHz、DMA 一次传输 128 到 256 字节。继续加大单次传输长度,虽然总吞吐能高一点,但 DMA 描述符、Cache 刷新的开销也开始增加,收益不明显。小记录我建议攒够 64 或 128 字节再一次性写入,既减少 CS 翻转次数,也方便统一做 CRC。

5.3 长期运行后的表现

我手里有一批跑了一年的测试板,每块板每 30 秒写一条 64 字节运行日志,换算下来一年写了大约 105 万条记录,总写入量在 67MB 左右。对 MRAM 来说,离寿命上限还差着天文数字。一年下来回读全部日志逐条校验,没有出现一条读错或写错的情况。

对比同一项目之前用的 NOR Flash 模拟方案,半年后就开始在不同地址出现偶发坏块,需要额外的坏块管理逻辑。MRAM 方案让存储部分的代码少了四分之一,因为不用处理擦除等待、坏块扫描和磨损均衡。这个简化带来的好处不只是开发省事,现场排查故障时也少了一堆变量。

6. 常见问题与排查技巧实录

6.1 读出全是 0xFF,时序没问题但就是不进数据

这是我第一次调 SPI MRAM 遇到的经典问题。代码看起来没问题,用示波器抓 MOSI 上确实有命令波形,但回读缓冲区全是 0xFF。最后发现是 STM32 的 SPI1 默认引脚 MISO 在 CubeMX 里设成了其他外设,导致内部连接被占用。重新确认 PA6 的复用功能为 SPI1_MISO 后就正常了。

如果你确认引脚没错,第二排查点是 CS。软件控制 CS 时,要保证发送命令前 CS 拉低,发送完最后字节后 CS 拉高,而且中间不能有其他中断把 CS 操作夹在 SPI 字节中间。我的建议是给 CS 操作函数加一个临界区保护,或者在写日志的全过程中暂时屏蔽高优先级中断。

6.2 M7 的 D-Cache 和 DMA 冲突

STM32F765ZI 是 M7 内核,DMA 和 Cache 一致性问题是坑王。表现是:DMA 写 MRAM 后再读回来,数据要么是老值,要么是乱码。原因很简单,CPU 写数据时会先写进 Cache,DMA 直接访问 SRAM,看到的可能还是 Cache 清出去之前的旧数据;DMA 读进来的数据放在 SRAM,CPU 再读时命中的又是 Cache 里的旧副本。

解决办法有三个:

  • 传输缓冲区放在非 Cacheable 的内存区,CubeMX 里可以单独配置 MPU。
  • 每次 DMA 传输前手动 Clean DCache,传输后手动 Invalidate DCache。
  • 缓冲区数组用 32 字节对齐,避免 Cache line 跨越导致误伤相邻变量。

我在日志模块里用的是第二种加第三种,代码直观,也不依赖 MPU 配置。注意SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr的地址参数是uint32_t*,长度不是数组长度,而是缓存行大小对齐后的总长度。

6.3 把 MRAM 当 Flash 用,总想着“先擦除”

代码评审时我发现有的同事会在写 MRAM 之前调用一个flash_erase_sector函数,然后整个系统就卡在状态轮询里。MRAM 不支持也不需要用块擦除,发出无效擦除命令不会把芯片弄坏,但会让代码流程变得非常奇怪,因为芯片不会回 BUSY 状态,你等多久都等不到“就绪”。

另外,Flash 的页编程通常不允许跨页连续写,超过页边界就要重新发命令。MRAM 没有页边界,CS 拉低后可以连续写任意长度,直到地址到达 0x7FFFF 后再回卷。写代码时不要把这些 Flash 时代的习惯带进来,否则就是在用 CRUD 的思路硬套 Redis 的模型,别扭且浪费性能。

6.4 工业现场的掉电和 ESD 防护

最后一个现场经验,和芯片本身关系不大,但非常重要。在继电器频繁动作或者电机启停的机柜里,MRAM 的数据线、SCK、CS 上都会感应出毛刺。最直接的影响就是 SPI 字节错位或者 CS 被意外拉高,导致一次写操作被截断。

我现在的标准做法是三件事:

  • 所有 SPI 信号线上串联 22Ω 到 33Ω 的小电阻,放在 MCU 侧。
  • 在 VCC 和 GND 之间放 TVS 管,位置靠近 MRAM。
  • 如果 MRAM 通过排线连接,地线要跟信号线走排线同一束,不能只靠机壳地回流。

关于掉电,前面说过要用复位芯片在电压跌落时强制释放 CS。有一次现场反馈“偶尔丢最后一条日志”,排查两天后才发现不是 MRAM 丢数据,而是设备在 3.3V 跌到 1.8V 的过程中,MCU 已经把写命令发出去了,但 MRAM 的逻辑电平已经乱掉。加了复位芯片和 CS 硬件拉高电路后,这个问题再没出现过。

做工业存储说到底不是拼参数,是拼边界处理。MRAM 把“存储要磨损、要擦除、要等待”这些边界给抹掉了,但你依然要把掉电、DMA 一致性、CS 时序这几个边界管好。这套 MR25H40CDF 和 STM32F765ZI 组合我用了快两年,最大的体会是:真正可靠的存储方案,永远是“硬件底子可靠 + 软件逻辑保守”双保险。先把 20MHz 跑稳,再把写保护引脚都处理好,绝大多数工业现场存储问题就不会找到你头上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询