1. 项目概述:为什么工业现场需要一个"掉电不丢"的存储方案
最近在调试一个工业数据采集模块,控制器选了 STM32F030RC,采集端需要暂存一批校准参数、掉电时保存现场状态、记录诊断日志。看了一圈存储方案,最后上了 Everspin 的 MR25H40CDF。这是颗 4Mbit 的串行 MRAM,走 SPI 接口,和 STM32F030RC 的 SPI1 对接。整套方案跑下来,实测效果很稳,这里把选型思路、硬件连接、驱动实现和踩坑过程都整理出来。
先说结论:如果你正在做工控板、仪表、PLC 外围模块或者任何需要频繁写数据、掉电不能丢、又担心 Flash 寿命不够用的项目,MRAM 值得认真考虑。它最大的特点就是按字节读写、几乎无限次擦写、数据掉电保持不丢失。这三点刚好补上了 EEPROM 容量小、Flash 擦写慢且块结构限制多、FRAM 容量偏低这几块短板。
需要说明的是,MR25H40CDF 本质是磁阻存储器,用磁隧道结存储数据,读写机制和电容存储的 DRAM、浮栅存储的 Flash 完全不同。它的写操作不需要先擦除,没有"写坏"的寿命焦虑,写一个字节和写一整页的耗时基本一致。加上工业级温度范围(这个后缀,封装是 8 引脚 PDFN,温度等级覆盖 -40℃ 到 +85℃),非常适合放在环境不太友好的设备里。
这篇分享适合三类人看:准备在嵌入式项目里引入 MRAM 的开发者、正在纠结存储方案选型的硬件工程师、以及手上刚好有 STM32F0 系列但没用过串行 MRAM 的同学。看完你应该能自己画电路、写驱动、跑通读写,并且知道现场调试中最容易翻车的几个细节。
2. 核心方案拆解:MRAM、EEPROM、Flash、FRAM 到底怎么选
2.1 不同存储介质的本质差异
存储选型这件事,本质上是在四个维度上做权衡:容量、写入寿命、写入粒度和掉电保持。传统的 SPI Flash(比如 W25Q64)容量大、成本低,但写入前必须按 sector 擦除,最小擦除块动辄 4KB,写一个字节也得先读出整个扇区、改掉其中一位、再整块写回。对需要频繁小数据更新的场景,这种机制既慢又难受。
EEPROM(比如 AT24C02)可以按字节写,寿命标称通常 100 万次,看起来够用。但实际做产品时你会发现,如果一个字段每次上电都更新,100 万次也就是一年多点的量级,而且 EEPROM 写一个字节的时间在毫秒级,写多字节要等内部时序,实时性差。更不用说大容量 EEPROM 型号少、价格贵,想存几十 KB 数据基本没得选。
FRAM 也不错,铁电存储,按字节写、寿命高,但容量普遍偏小,最大的串行 FRAM 也就 8Mbit 左右,价格比 MRAM 还贵,而且供应商选择少。MRAM 的出现正好填补了这个空间:容量做到 4Mbit(512KB),接口走标准 SPI,寿命理论上 100 亿次写入,写时序和读时序一样快,不需要擦写调度,驱动写起来跟读 Flash 的 0x03 指令几乎一样简单。
2.2 针对本项目为什么锁定了 MR25H40CDF
STM32F030RC 是 Cortex-M0 内核,48MHz 主频,通信接口有一个 SPI、一个 I2C,默认没有 FSMC 并口总线。这意味着我没办法像接 NOR Flash 那样用并行总线高速读写,只能走串行接口。MR25H40CDF 就是 SPI 从设备,标准 4 线制(CS、SCLK、MOSI、MISO),跟 MCU 的 SPI1 直接相连,一点额外逻辑都不用加。
容量上 4Mbit 对当前项目刚好够用:我分配了 64KB 给参数区(含三份冗余备份)、128KB 给事件日志、剩余空间做 ADC 校准数据暂存和 bootloader 升级缓存。如果只是存几个参数,512KB 确实偏大,但考虑到日志记录和现场固件升级的需求,这个容量反而成了大缓冲区,省掉了外挂 Flash 的物料成本。
还有一点是功耗。MR25H40CDF 的待机电流在微安级别,工作电流大概 15mA 左右,比并行 NOR Flash 低得多。我这块板子是 24V 供电的工业模块,M0 芯片本身功耗就低,整板功耗控制很轻松。当然,如果你需要更低功耗版本,Everspin 有深睡眠模式,指令也简单,后面代码里我会带上。
3. 硬件连接与电路设计细节
3.1 MCU 管脚分配与 SPI 接线
STM32F030RC 的 SPI1 可以映射到多个管脚组合,我用的是默认映射:PA5 做 SCK、PA6 做 MISO、PA7 做 MOSI,CS 用软件控制的普通 GPIO,选 PA4。为什么不用硬件 NSS?因为项目里这个 SPI 总线上还可能挂其他从设备(比如外部 ADC 或者传感器),硬件 NSS 在多从机场景下管理起来别扭,还不如软件控制来得干脆。
具体连接关系如下:
| MR25H40CDF 引脚 | 功能 | 连接到 STM32F030RC |
|---|---|---|
| 1 CS# | 片选,低有效 | PA4(GPIO 推挽输出) |
| 2 SCLK | 时钟 | PA5(SPI1_SCK) |
| 3 MOSI | 主机输出/从机输入 | PA7(SPI1_MOSI) |
| 4 MISO | 主机输入/从机输出 | PA6(SPI1_MISO) |
| 5 WP# | 写保护,低有效 | 直接拉高到 VCC |
| 6 VSS | 地 | GND |
| 7 VCC | 电源 3.3V | 3.3V |
| 8 HOLD# | 暂停通信,低有效 | 直接拉高到 VCC |
WP# 和 HOLD# 这两个引脚的功能需要特别注意。WP# 拉低时会禁止写状态寄存器和写存储区,很多人调试时遇到写操作没反应,排查到最后发现是 WP# 悬空或者被拉低了。HOLD# 拉低时芯片暂停 SPI 通信,相当于时钟无效,总线上其他设备可以占用,这两个引脚在单从机场景下直接接上拉电阻到 VCC 最省心。
3.2 电源、去耦与上拉电阻
MR25H40CDF 支持 2.7V 到 3.6V 供电,我直接接在 3.3V 主电源上。VCC 引脚旁边放 0.1uF 和 1uF 两个陶瓷电容并联去耦,位置尽量靠近芯片。VCC 和 VSS 之间不要省这个电容,MRAM 内部读写时电流变化比较快,特别是高频翻转时,退耦不好容易在电源轨上出现毛刺。
SPI 的 MISO 线是芯片输出,它在读操作时由从机驱动,不需要额外上拉。但 MOSI、SCLK、CS 这几个输入端,如果 MCU 复位期间管脚处于浮空状态,建议加 10K 上拉电阻,防止芯片在 MCU 未初始化时误动作。尤其注意 CS 的上拉,如果 CS 悬空被干扰拉低,芯片会进入等待指令状态,虽然不会误写数据,但会增加莫名的调试困难。我这个板子空间紧张,只在 CS 上加了上拉,实测没有问题,但正规布局建议四条线都处理一下。
3.3 工业现场的电气鲁棒性补充
工业环境和实验室不一样,强电干扰、电机启停、长线缆传输都很常见。SPI 线如果引出到板外(比如通过排线连接显示面板),建议在 MCU 侧串联 33Ω 电阻,配合寄生电容可以起到简单的低通滤波作用。信号线上再加 TVS 管,注意 TVS 的结电容不能太大,否则 SPI 速率上不去。
另一个容易忽略的点是地线。MR25H40CDF 是纯数字器件,抗干扰能力本身不错,但不要让它和电机驱动电路共享回流路径。数字地和功率地单点相连,存储芯片尽量靠近 MCU 摆放,走线短而粗。实测下来,把 SPI 走线走在底层打过孔、周围铺地铜,EMC 表现会比直接顶层走线好很多。这些细节看似不起眼,但做产品认证时能省大量整改时间。
4. 驱动代码实现与调试实录
4.1 SPI 初始化配置
STM32F030RC 的 SPI1 配置不算复杂,但有几个点要仔细。MR25H40CDF 支持 SPI Mode 0 和 Mode 3,我选了 Mode 0(CPOL=0,CPHA=0),这是最通用的配置。主频方面,芯片手册标称最大 40MHz,实际上 STM32F0 的 SP I尽可以跑到 48MHz(PCLK/2),我这边为了兼容长走线和工业干扰,只用了 9MHz,留足裕量。
void SPI1_Init_MRAM(void) { GPIO_InitTypeDef gpioInit; SPI_InitTypeDef spiInit; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); gpioInit.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; // SCK, MOSI gpioInit.GPIO_Mode = GPIO_Mode_AF; gpioInit.GPIO_OType = GPIO_OType_PP; gpioInit.GPIO_PuPd = GPIO_PuPd_NOPULL; gpioInit.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpioInit); gpioInit.GPIO_Pin = GPIO_Pin_6; // MISO gpioInit.GPIO_Mode = GPIO_Mode_AF; gpioInit.GPIO_OType = GPIO_OType_PP; // 注意:MISO 设为浮空输入更标准 gpioInit.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &gpioInit); gpioInit.GPIO_Pin = GPIO_Pin_4; // CS 软件控制 gpioInit.GPIO_Mode = GPIO_Mode_OUT; gpioInit.GPIO_OType = GPIO_OType_PP; gpioInit.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &gpioInit); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS 默认高 spiInit.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spiInit.SPI_Mode = SPI_Mode_Master; spiInit.SPI_DataSize = SPI_DataSize_8b; spiInit.SPI_CPOL = SPI_CPOL_Low; spiInit.SPI_CPHA = SPI_CPHA_1Edge; spiInit.SPI_NSS = SPI_NSS_Soft; spiInit.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 72MHz/8 = 9MHz spiInit.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spiInit); SPI_Cmd(SPI1, ENABLE); }这里有个小坑:SPI1 挂的是 APB2 时钟,F030RC 的 APB2 最大 48MHz,如果按 SDK 里的默认时钟树,PCLK 可能是 48MHz,9MHz 就是 48/8=6MHz 或者 48/6=8MHz,用 8 分频得到 6MHz 也完全够用。调试时不必死磕最高速率,稳定第一。
4.2 基础读写函数
MRAM 的 SPI 指令集和普通 SPI NOR Flash 高度相似,读数据是 0x03,写数据是 0x02,不过是按字节写,没有页缓冲这种概念。这意味着你不需要拆页、不需要对齐,想写哪就写哪。
uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t rx; MRAM_CS_LOW(); SPI_SendByte(0x03); // READ SPI_SendByte((addr >> 16) & 0xFF); SPI_SendByte((addr >> 8) & 0xFF); SPI_SendByte(addr & 0xFF); rx = SPI_ReceiveByte(); // 读任意字节 MRAM_CS_HIGH(); return rx; } void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LOW(); SPI_SendByte(0x06); // WREN 写使能,必须先发 MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI_SendByte(0x02); // WRITE SPI_SendByte((addr >> 16) & 0xFF); SPI_SendByte((addr >> 8) & 0xFF); SPI_SendByte(addr & 0xFF); SPI_SendByte(data); // 直接写入,无需页擦除 MRAM_CS_HIGH(); }连续读取可以拉低 CS 后一直发 0x03、地址,然后连续读字节,地址自动递增,直到你拉高 CS。这对我做日志读取和固件上传非常方便。实测直接读 512KB 数据,9MHz 时速率约 1.1MB/s,比用 I/O 模拟 SPI 快太多了。
连续写一个缓冲区时,也不必像 Flash 那样每写一页重新发一次指令,只要 CS 保持低电平,写完一个字节后地址自动加一,可以继续写下一个字节。官方手册说可以持续写直到地址到顶,我一般写个 64 字节就拉高 CS 重新发指令,既保持简洁又不至于在异常情况下把整个芯片写穿。
4.3 状态寄存器与写保护
大多数情况下,MRAM 不需要管写保护的事,因为写操作本身就是即时完成的,没有"忙"状态,这也是 MRAM 区别于 Flash 的关键。但我还是建议软件上读一下状态寄存器,主要是确认 WP# 引脚和状态寄存器里的 WPEN 位没把写保护打开。读状态寄存器指令是 0x05,注意它只返回一个字节,其中最高位就是 WPEN。
uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI_SendByte(0x05); status = SPI_ReceiveByte(); MRAM_CS_HIGH(); return status; }如果状态寄存器返回 0x80,说明 WPEN 和 WP# 引脚都处于写保护状态,任何写操作都会被拒绝。这个位出厂默认是 0,但我碰到过客户板子上的 WP# 引脚连到 MCU 某个 GPIO,程序初始化时把这个 GPIO 拉低了,结果写数据一直失败。排查思路很简单:读状态寄存器,如果值是 0x80,查硬件。
4.4 性能实测与使用技巧
我特地在板子上跑了一组简单的性能测试,记录在案:
| 场景 | 数据量 | 实测耗时 | 备注 |
|---|---|---|---|
| 连续写 4KB | 4096 字节 | 约 4.5ms | 9MHz SPI,无页对齐开销 |
| 连续读 4KB | 4096 字节 | 约 3.7ms | 读取略快于写入 |
| 擦除 1 sector | 无 | 0ms | 根本不需要擦除 |
| 掉电保存 1 字节 | 1 字节 | 1ms | 配合掉电检测,10μs 级可完成 |
对比一下普通 SPI Flash 写 4KB 数据的时间,假设一个扇区擦除要 60ms,再算上写页时间,至少也要 100ms。MRAM 的优势不仅是快,更重要的是调试体验好——你不需要做磨损均衡、不需要维护坏块表、不需要等待内部擦写完成。写代码时完全可以把 MRAM 当作一块超级大的 RAM 来用。
实际项目中我把日志系统设计成了环形缓冲:一块 128KB 的区域,头指针和尾指针都存在 MRAM 里,每次事件写入一个 80 字节的结构体,写完更新指针。之前用 Flash 时,日志写满一块区域就要做擦除,擦除期间事件会丢失几十毫秒。换成 MRAM 后,日志系统几乎没有峰值延迟,每次掉电重启指针完全恢复,数据一条不丢。
5. 调试中遇到过的疑难杂症与排查心得
5.1 读出来全是 0xFF 或 0x00,不死不活
这类问题九成出在 SPI 配置或硬件连接上。先说那个比较隐蔽的坑:STM32F0 的 SPI 在 Master 模式下,MISO 引脚要配置为浮空输入,不能配置成复用推挽。我最初把 MISO 配成了 GPIO_Mode_AF 复用,而且默认是开漏,结果读取全是 0。原因很简单,把输出驱动和输入缓冲混用了。
排查步骤建议按这个顺序来:先示波器看 SCLK 有没有时钟,再看 CS 波形是否正常拉低,然后看 MOSI 上的指令字节对不对,最后再看 MISO 是否有返回。四步下来基本能定位是接线、还是时钟、还是配置问题。如果手头有逻辑分析器,直接抓 SPI 时序,一眼就能看出芯片有没有回数据。
5.2 偶发写入丢失,找半天是掉电时序问题
有段时间现场反馈设备偶尔上电后参数被重置,我在实验室复现不了,后来模拟电源跌落才定位到问题。当时 MCU 检测到掉电,在 VCC 跌落到阈值以下后,还想往 MRAM 里写关键的现场状态。VCC 已经降到 2.5V 左右,MRAM 本身还能工作,但 SPI 线路噪声增大,写入的数据出现了错误。
解决办法是加了一颗超级电容或者大电解电容,给存储供电回路提供几百毫秒的维持时间,同时把掉电检测阈值调高,让 MCU 在 VCC 还有 3.0V 时就进入紧急保存流程。这里有个经验:不要把"保存现场"留到电源已经崩溃的那一刻才启动,要提前一点。我用的是 STM32F030RC 内置的 PVD(可编程电压检测器),设置 2.9V 阈值,中断里立刻把 16 字节的关键数据写入 MRAM 的固定地址,再把 WP# 拉低防止后续误写。实测电容撑住 200ms 足够完成操作。
5.3 连续写之后某个字节突然错乱,检查 CS 时序
还有一次,客户反馈日志里偶尔出现一个字节的乱码。追了半天发现是 CS 拉高的时机不对。标准 SPI 从设备,CS 高电平表示命令结束,芯片在这个上升沿锁存数据。在快速连续操作时,如果 CS 刚拉高之后太短时间又拉低,芯片内部可能会把上一次的指令残留识别成新指令,造成地址错位。
解决办法是软件上保证 CS 两次操作之间至少留一个时钟周期的间隔,实际操作中 CS 拉高之后延时 1μs 再拉低。同时检查 MCU 的 SPI 状态寄存器,确保发送完成标志置位后再拉高 CS,避免后面一个字节的数据还没发完就切断了通信。
5.4 与烧录器共用一个 SPI 导致冲突
最后说一个产品化时很实际的坑。我的板子上把 SPI1 同时接到了 MRAM 和调试用的外部 Flash(或者烧录器),通过 CS 区分设备。MRAM 的 HOLD# 引脚默认拉高,如果烧录器拉低 HOLD#,MRAM 就进入暂停状态,不会响应任何片选。这本身没问题,但有些调试器会在这个总线上枚举设备,把 MRAM 误识别成 Flash,然后尝试往里面擦写。一旦擦写指令误发出去,MRAM 会按字节写模式执行,可能会破坏某个地址的数据。
解决方案是分区隔离:调试接口走独立 SPI,或者直接把 MRAM 的 WP# 在出厂固件阶段拉死。如果必须共用总线,记得在 Bootloader 里对 MRAM 的操作加上一个 0xA5 之类的魔数校验,防止误操作。
6. 用在真实项目中的总体评价与经验沉淀
这块板子已经在实验室跑了三周,反复上下电超过两千次,日志写入超过五十万条,没有出现一条数据丢失或者校验失败的情况。MR25H40CDF 加上 STM32F030RC 这套组合,让我体会最深的不是指标有多高,而是整个开发过程的"省心"。
原来用 SPI Flash,每次写数据前要想擦除策略,日志区要留冗余,掉电要防写一半的坏块。换到 MRAM 之后,这些负担全部卸掉了。存储芯片回到了它本该有的样子:就是一个通用的字节寻址存储器件,你给它地址和数据,它记住;你给它地址,它原样吐出来。即使写了一半突然断电,再上电读那个地址,大概率也还是旧的完整数据,因为 MRAM 的写入是即时翻转,不存在 Flash 那样的"擦除中间态"。
从成本角度看,单独看 MR25H40CDF 的单价确实比同容量的 SPI Flash 贵,但把 Flash 需要的坏块管理、磨损均衡软件开销、擦写出错重试机制、掉电保护电路、增加校验冗余的 Flash 容量这些统统算进去,系统级成本反而没有想象中那么夸张。如果你的产品对数据可靠性要求高、对写入频率不敏感、又不想引入文件系统来做 Flash 管理,MRAM 是一种非常干净的答案。