ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F207ZG与MR25H40CDF:SPI MRAM掉电保存方案

2026/10/4 15:17:16 拓冰建站 浏览量
STM32F207ZG与MR25H40CDF:SPI MRAM掉电保存方案 做嵌入式这些年存储选型一直是“看着简单、踩坑一堆”的环节。手里这套组合——MR25H40CDF 这颗 4Mbit 串行 MRAM直接挂在 STM32F207ZG 的 SPI 上——其实是我给工业采集板做参数保存和运行日志时反复验证过的一套方案。在嵌入式圈子里大家习惯把存储分成两层一层是跑代码的 NOR Flash另一层是存数据的 EEPROM。可一旦遇到“既要频繁写入、又要在掉电瞬间保证数据完整”的需求这两个老伙计就开始别扭了。MR25H40CDF 恰好补上了这个空档它不需要擦除、没有写入次数限制写入速度和读出速度基本对称。STM32F207ZG 也不用多说Cortex-M3、120MHz、外设丰富1MB 内部 Flash 跑程序再外挂一颗 MRAM 管数据和日志算是工控板上很稳的搭配。接下来我会从选型思路讲到硬件接线再给出可以直接抄作业的驱动代码最后把我在调试中遇到的几个典型问题一起列出来。这篇文章适合正在做存储选型、或者已经拿到 SPI 类 MRAM 但总调不通的朋友读完你应该能少走不少弯路。1. 选型背景工业数据存储需要的到底是什么1.1 MR25H40CDF 到底是什么MR25H40CDF 是 Everspin 推出的 4Mbit 串行 MRAM按字节算就是 512KB。它的接口是标准 SPI支持 Mode 0 和 Mode 3外部只需要 6 根信号线就能跑起来封装是常见的 8 脚 DFN体积很小。和传统 Flash 最大的区别是它把数据存在磁隧道结里掉电后数据不会丢写入时又不需要内部电荷泵、不需要先擦除再写所以单字节写入和整页写入的成本差不多。很多朋友一听到 MRAM 就觉得贵但要看用在什么地方。如果你只是想存几个校准参数一个月写一次那普通 EEPROM 确实够用。但工业设备一旦开机就在采样传感器数据每分钟往存储里写还要随时应对突发断电EEPROM 的写入次数和等待时间就会成为瓶颈。MR25H40CDF 在 3.3V 供电下能跑 40MHz SPI 时钟数据手册里标称的耐久性远超 Flash 类器件这对频繁写入的日志型应用非常有吸引力。1.2 对比 EEPROM 和 NOR FlashMRAM 赢在哪里我整理了一张对比表拿最常见的三类器件放在一起看类别典型容量写入方式擦除操作寿命特征随机写成本EEPROM8Kbit ~ 512Kbit按字节写无需显式擦除100 万次左右有写周期等待SPI NOR Flash1Mbit ~ 128Mbit按页写必须先擦除扇区1 万到 10 万次写放大严重SPI MRAM4Mbit 常见按字节/连续写完全不需要实际无限耐久近似 SRAM 写入NOR Flash 在工业代码存储上依然是绝对主力因为它容量大、读速度快、支持 XIP。但把它拿来做数据记录就会很尴尬你只想改某一个字节却要先把整个扇区读到 SRAM擦除再写回去中间任何一次掉电都可能让旧数据和新数据同时丢失。EEPROM 没有擦除问题但容量普遍偏小而且写一个字节通常要等几毫秒。MR25H40CDF 把这两边的痛点同时解决了容量够做日志写入不需要擦除写完后不需要等内部编程完成地址可以像 SRAM 一样随机访问。代价是单位容量价格比 NOR 高但在工业小容量数据存储这个细分场景里这个代价通常换来了更高的可靠性。1.3 为什么和 STM32F207ZG 特别搭STM32F207ZG 本身不是冷门片子1MB Flash 加 128KB SRAM跑协议栈、跑嵌入式 RTOS 都够用。问题在于板子越大要保存的现场数据就越多。网络诊断记录、温湿度历史栈、工艺参数版本、故障断电信息这些数据如果都存在内部 Flash一方面会压缩代码空间另一方面频繁擦写内部 Flash 对整机寿命也不是好事。MR25H40CDF 和 STM32F207ZG 的配合很自然MCU 原生 SPI 接口直接接3.3V 电平不需要额外转换512KB 容量在“放得下”和“不会浪费”之间比较平衡。STM32F207ZG 的 SPI1 最高时钟可以跑到 30MHz 左右MR25H40CDF 标称 40MHz两边都留了余量即便板子布线一般跑 20MHz 也很轻松。再加上 STM32F207ZG 有可编程电压检测器 PVD可以在掉电瞬间触发中断配合 MRAM 的快速写入能力把最后的运行状态记录下来这套设计在工业现场非常实用。2. 硬件连接与电路设计2.1 MR25H40CDF 引脚速览MR25H40CDF 是标准 8 脚 SPI 存储引脚排列和很多 SPI EEPROM 兼容初看在硬件上不会太陌生。引脚定义如下引脚号名称方向说明1CS#输入片选低有效2SO输出MISO数据输出3WP#输入写保护低有效4VSS电源地5SI输入MOSI数据输入6SCK输入串行时钟7HOLD#输入暂停通信低有效8VCC电源3.3V 供电注意 WP# 和 HOLD# 这两个引脚很多人布线时不重视。WP# 如果悬空或受干扰拉低芯片会进入写保护状态导致写指令全部被忽略。HOLD# 如果悬空在电磁干扰强的现场可能瞬间被拉低通信就会暂停主机端表现为数据偶发错误。这两个引脚不用操作时都应该通过电阻上拉到 VCC。2.2 与 STM32F207ZG 的实际接线我的板子用了 STM32F207ZG 的 SPI1GPIO 映射如下PA5 - SPI1_SCK复用功能 AF5PA6 - SPI1_MISO复用功能 AF5PA7 - SPI1_MOSI复用功能 AF5PA4 - 软件控制的 CS普通 GPIO 推挽输出CS 我从来不建议用硬件 SPI 的 NSS 引脚管原因很简单如果 SPI 总线上只挂一颗 MRAM硬件 NSS 或许能凑合但一旦你后面扩容想在总线上再加一颗温度传感器或者另一片存储软件控制 CS 会灵活得多。而且 STM32 的 NSS 自动模式有时会产生额外电平变化容易把 MRAM 的状态机弄乱。PA4 接 MRAM 的 CS#平时拉高每次通信时拉低。如果你的系统里 SPI1 已经被其他外设占用换成 SPI2 或 SPI3 也完全没有问题只要把 GPIO 复用号和引脚对应表查清楚就行。STM32F207ZG 的特点就是外设多SPI 资源一般不会紧到没得用。2.3 电源、上拉和 PCB 走线MR25H40CDF 对电源不算挑剔但还是要在 VCC 和 GND 之间放一个 100nF 陶瓷电容位置越靠近芯片引脚越好。如果是比较复杂的工业主板VCC 上再加一颗 2.2uF 到 10uF 的容值会更稳反正成本不高预留位置总没错。WP# 和 HOLD# 各加一颗 10kΩ 上拉电阻到 VCC。CS、SCK、MOSI 三根线是主机推向从机的走线尽量短避免在 PCB 上绕圈。MISO 是从机推回主机的同样要减少耦合长度。如果板子靠近变频器、电机驱动这类强干扰源建议在 SPI 时钟线上串联一颗 22Ω33Ω 的小电阻能有效减缓信号边沿降低辐射噪声。实测下来这套处理能减少很多“低速率时正常、高速率时偶发错位”的诡异问题。3. 软件驱动从 SPI 基础到可直接跑的读写函数3.1 初始化 STM32F207ZG 的 SPI我用 STM32 HAL 库做工程初始化 SPI1 的代码大概是下面这样。首先要使能 SPI1 和 GPIOA 的时钟然后把 PA5、PA6、PA7 配置成复用输出PA4 配置成普通 GPIO。SPI_HandleTypeDef hspi1; void MX_SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /* PA4 做 CS 片选普通推挽输出 */ GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* PA5 SCK, PA6 MISO, PA7 MOSI 复用为 SPI1 */ GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); 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_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }这里 CLKPolarity 为 Low、CLKPhase 为 1Edge对应 SPI Mode 0这也是 MR25H40CDF 最常用的模式。APB2 时钟如果是 60MHz预分频器设成 4 就是 15MHz设成 2 就是 30MHz。先用 15MHz 调通代码再提频这是我建议的节奏。3.2 MR25H40CDF 指令集速查这颗芯片的基本指令和很多 SPI Flash、SPI EEPROM 相似但写操作不需要先擦除。我用到的指令如下命令命令字功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读数据和写数据的地址都是 24 位。发送顺序是命令字、地址高 8 位、地址中 8 位、地址低 8 位然后是数据。MR25H40CDF 内部按 256 字节为页组织连续读写跨页时不会自动跨到下一页而是会回卷到当前页的开头。这一点后面写函数时必须处理。3.3 写函数实现记住先发 WREN写数据之前必须先发 WREN 写使能命令这也是很多人第一次调的时候最容易漏掉的一步。WREN 是一个独立命令拉低 CS发 0x06拉高 CS芯片才会把内部写使能锁存置位。不能把 WREN 和随后的 WRITE 拼在同一个 CS 低电平窗口里那样状态机不会按你预期工作。#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) static uint8_t mram_xfer(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 100); return rx; } void mram_write_enable(void) { MRAM_CS_LOW(); mram_xfer(0x06); /* WREN */ MRAM_CS_HIGH(); } void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { mram_write_enable(); MRAM_CS_LOW(); mram_xfer(0x02); /* WRITE */ mram_xfer((addr 16) 0xFF); mram_xfer((addr 8) 0xFF); mram_xfer(addr 0xFF); for (uint32_t i 0; i len; i) { mram_xfer(buf[i]); } MRAM_CS_HIGH(); }这个函数对一次写入不超过页内剩余长度的场景已经够用。写入完成后不需要等擦除超时因为根本不存在擦除动作。MRAM 的写入是在 SPI 时钟驱动下直接完成的CS 拉高后数据就已经在存储单元里了。如果你想更保守可以在写完以后立刻回读校验一遍。3.4 读函数和状态寄存器读取读数据比写数据简单不需要 WREN直接拉低 CS发 READ 命令和地址然后连续接收数据。void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_xfer(0x03); /* READ */ mram_xfer((addr 16) 0xFF); mram_xfer((addr 8) 0xFF); mram_xfer(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] mram_xfer(0x00); } MRAM_CS_HIGH(); }状态寄存器可以通过 RDSR 读取。如果怀疑芯片进入了写保护状态可以先用这个命令看一眼uint8_t mram_read_status(void) { uint8_t status 0; MRAM_CS_LOW(); mram_xfer(0x05); /* RDSR */ status mram_xfer(0x00); MRAM_CS_HIGH(); return status; }状态寄存器里的写使能锁存位、块保护位、写保护使能位具体布局以规格书为准。日常使用中我很少频繁查状态主要是在调试阶段用来判断“写不进去到底是因为 WP 引脚还是因为状态寄存器里有块保护位”。3.5 封装成存储 API裸的读写函数只能保证接口通了但真正要存业务数据最好在驱动上面再包一层。拿最常见的“掉电保存设备参数”举例我会定义一个结构体结构体里带上魔数、版本号和 CRC 校验然后把它整体写入 MRAM。typedef struct { uint32_t magic; /* 固定魔数 */ uint16_t version; /* 参数版本 */ uint16_t crc; /* 结构体 CRC16 */ float setpoint; /* 工艺设定值 */ uint16_t alarm_threshold; uint16_t fault_count; uint32_t last_run_time; } sys_param_t; #define PARAM_ADDR 0x00000000u #define PARAM_MAGIC 0xA5A5A5A5u void mram_save_param(sys_param_t *param) { param-magic PARAM_MAGIC; param-crc crc16((uint8_t *)param, sizeof(sys_param_t) - 2); mram_write_bytes(PARAM_ADDR, (uint8_t *)param, sizeof(sys_param_t)); } int mram_load_param(sys_param_t *param) { mram_read_bytes(PARAM_ADDR, (uint8_t *)param, sizeof(sys_param_t)); if (param-magic ! PARAM_MAGIC) { return -1; } if (crc16((uint8_t *)param, sizeof(sys_param_t) - 2) ! param-crc) { return -2; } return 0; }CRC 放在结构体倒数第二个字段计算时把它自己排除掉这样做非常省事不需要计算整个缓冲区再减去 CRC。加载参数时先判断魔数再判断 CRC能挡掉绝大多数“写入写了一半”或者“地址错位”的情况。3.6 跨页写入与缓冲策略前面说过 MR25H40CDF 按 256 字节分页连续写如果跨页地址会自动回卷这会让数据写到错误的位置。所以封装一层安全的跨页写函数很有必要。思路很简单每次写当前页剩余长度写完以后把地址、缓冲区指针和剩余长度往前挪直到全部写完。static uint32_t page_remain(uint32_t addr) { return 256u - (addr 0xFFu); } void mram_write_bytes_safe(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { uint32_t chunk len; uint32_t remain page_remain(addr); if (chunk remain) { chunk remain; } mram_write_bytes(addr, buf, chunk); addr chunk; buf chunk; len - chunk; } }实际项目中我还会再加一个优化如果单包数据小于 256 字节且起始地址偏离页边界不远尽量把地址布局设计成从页边界开始这样很多写入只需要一次 CS 周期就能完成。MRAM 虽然不需要擦除但频繁拉高拉低 CS 也是有开销的聚合写不仅省时还能减少总线上的干扰窗口。4. 工业场景下的掉电保存与日志记录4.1 掉电保存的关键流程工业设备最怕的不是正常关机而是生产过程中突然断电。这时候 CPU 可能没时间做完整收尾但最后几个关键参数必须留下来否则重启以后整个工艺状态就丢了。STM32F207ZG 自带 PVD可以设定一个阈值电压当 VDD 跌到阈值以下时触发中断。在这个中断里时间非常宝贵不能再做复杂的 Flash 擦除操作。用 MR25H40CDF 就舒服得多。PVD 中断触发后直接调用 mram_write_bytes_safe把当前几个运行状态结构体写进去。由于写 MRAM 不需要等待毫秒级擦除整个写入时间几乎取决于 SPI 时钟和字节数。假设跑 15MHz写 64 字节结构体大约只需要微秒级时间加上少量协议开销完全赶在系统电压彻底掉下去之前完成。当然掉电中断里不能只靠软件。MRAM 的供电最好由保持电容维持至少保证从 PVD 触发的几毫秒里 VCC 不塌。我一般会在 3.3V 输入到 MRAM 附近放一颗 100uF 钽电容实测可以撑住几百微秒到毫秒级写入窗口具体容量要根据掉电斜率估算。4.2 循环日志区的实现思路如果只是存参数512KB 可能用不完。但做设备日志记录时这 512KB 就是一个循环缓冲区。我习惯把 MRAM 分成两块开头一小块放日志头后面大片区域放日志数据块。日志头里记录当前写指针、总写入次数和魔数。每次新日志写入流程是在数据区当前写指针位置写入一条带序号和 CRC 的日志记录。更新日志头里的写指针和序号。如果写指针到达数据区末尾则回卷到数据区起始。因为 MRAM 没有擦除限制覆盖旧日志不会降低寿命这一点比 NOR Flash 省太多事。按一条日志 64 字节算512KB 能存 8192 条。如果一分钟记录一次可以覆盖约 5 天半的历史数据如果一小时记录一次能覆盖大半年。对大多数设备排查现场问题来说这个容量很实用。4.3 数据校验与双备份工业日志最怕读到“半截数据”。明明只写了一部分掉电就来了重启后这条记录 CRC 错误。处理办法很简单每条记录自带 sequence 号和 CRC启动时扫描数据区跳过 CRC 错误的部分从最后一条有效记录继续写。MRAM 连续地址读写很快启动扫描 512KB 也就是几十毫秒的事完全可接受。对于特别重要的参数区我会用双备份策略。地址 0x00000 放参数 A 副本0x00400 放参数 B 副本每次保存先写 A、再写 B。读取时先看 A 的 CRC如果 A 坏了再看 B两边都坏了才认定参数丢失。这个方案在 Flash 上会因为擦除次数让人心疼但在 MRAM 上没有任何心理负担随便写。5. 调试实录我踩过的坑和排查方法5.1 回读全 0xFF先查 CS 和 SPI 模式我第一次调 MR25H40CDF 时焊好板子写一个数组回读发现全是 0xFF。第一反应是芯片坏了后面冷静下来用逻辑分析仪抓波形才发现CS 信号在单个字节传输之间被 STM32 的硬件 NSS 自动拉高了整个 READ 命令被拆成了好几段MRAM 根本没收到完整的命令。这个问题的教训就是CS 一定要由软件 GPIO 控制不要依赖硬件 NSS。另外 SPI 模式也要逐项核查。MRAM 支持 Mode 0 和 Mode 3但 MCU 配置成 Mode 1 或 Mode 2 后MISO 采样点会落在数据变化沿上读回的数据就是乱码。先用低速 1MHz、把 SPI Mode 配成 0回读状态寄存器如果能读到非 0xFF 的值说明物理链路基本通了。5.2 写不进去可能是 WP 和状态寄存器有一块测试板写使能明明发了WRITE 命令也完整写后回读却还是旧数据。排查下来发现是 WP# 引脚没有上拉默认电平被干扰拉低了芯片整片进入了写保护状态。把 WP# 用 10kΩ 上拉到 3.3V问题立刻消失。还有一种情况是状态寄存器里的块保护位被无意中置位。可以用 RDSR 读取状态寄存器如果对应块保护位为 1就先执行 WREN再执行 WRSR 命令把状态寄存器清零。注意如果 WPEN 位为 1 且 WP# 引脚为低WRSR 也会失效所以硬件上必须保证 WP# 在高电平。5.3 高时钟频率下偶发抖动20MHz 以下一切正常提高到 30MHz 以后偶尔会出现某条日志 CRC 错误。这类问题很少是芯片本身的问题更多是信号完整性和电源噪声。我的板子环境有变频器SPI 走线又离电源线太近最后做了三件事解决把 SPI 时钟降到 15MHz、SCK 线上串联 33Ω 电阻、在 MRAM 附近补了一颗 100nF 电容。实测已经跑了一个月没有复现。另外 HOLD# 引脚对噪声很敏感。如果它所在网络离 PWM 控制信号太近高速切换噪声可能把 MRAM 的通信暂停表现就是整包数据里的某几个字节突然不对。HOLD# 上拉电阻最好放到靠近 MRAM 引脚的位置。5.4 常见问题速查表现象最可能原因建议排查方向回读全 0xFFSPI 模式不匹配、CS 时序被硬件 NSS 干扰、MISO 没接对用逻辑分析仪抓 CS、SCK、MISO写入后数据不变忘记发 WREN、WP# 被拉低、状态寄存器块保护位置位检查 WREN 时序、WP# 电平和 RDSR 状态连续写数据串页没有处理 256 字节页边界使用跨页安全写函数偶发 CRC 错误电源纹波、SPI 走线噪声、HOLD# 干扰降频、加滤波电容、HOLD# 上拉、串阻上电后参数为默认值魔数或 CRC 没有写入成功或写入时掉电被中断双备份 启动时扫描最近有效记录6. 个人经验和扩展建议6.1 使用 RTOS 和 DMA 时的注意事项如果板子上跑 FreeRTOSSPI 总线是共享资源MRAM 的操作必须保证原子性。因为 CS 低电平窗口本身就是一个完整事务如果任务 A 拉低 CS 后刚发了命令字任务 B 抢占了 CPU把 SPI 外设配置改了甚至也往 SPI 上发了数据那 MRAM 收到的内容就乱套了。最简单的做法是给 SPI 总线加一把互斥锁整个 mram_write_bytes_safe 调用都持锁不放行其他任务。用 DMA 做大数据写入时CS 最好也放在 DMA 完成后统一拉高。HAL 库里 HAL_SPI_Transmit_DMA 是异步的你如果刚启动 DMA 就把 CS 拉高数据根本发不完。要么用 HAL_SPI_TransmitReceive 阻塞要么在 DMA 完成回调里再拉高 CS二选一别折中。6.2 什么情况下别选 MRAMMRAM 不是万能药。如果你的产品要存几十 MB 的音频、图片或者长时间波形文件512KB 容量完全不够这时候应该用 SPI NAND 或者 eMMC。如果你只是存几个静态参数、一个月都未必写一次EEPROM 的成本和供应链优势仍然存在。MRAM 最适合的场景是容量在 KiB 到几百 KiB 之间、写入频率高、要求掉电不丢、不允许擦除等待。我实际体会是MR25H40CDF 和 STM32F207ZG 这一组合真正解决的是“小数据、高频率、强可靠”这三个痛点。别把它当成大容量 Flash 的替代品把它看成一片不会磨坏的 SRAM用的时候思路一下就通了。最后再分享一个小习惯第一次上电调试时别急着把 SPI 时钟调到极限。先用 1MHz 把最基础的写读回函数调通然后配合逻辑分析仪确认 CS、SCK、MOSI、MISO 四根线的时序再逐步提频。这个流程帮我排除过不少“看似芯片问题、实际是初始化配置问题”的坑。存储芯片本身通常很皮实软件和布线才是大多数人栽跟头的地方。