做嵌入式开发的人应该都遇到过这种场景:产品已经量产,现场反馈偶尔丢参数,或者某台设备在反复升级、频繁写日志之后,EEPROM 先扛不住了。换一个更大容量的器件吧,引脚重新改,驱动重写,验证周期拖得很长。我之前在工控设备上遇到同样的痛点,最后用 MR25H40CDF 这颗 SPI MRAM 配合 R7FA2E2A72DNK 瑞萨 MCU,把存储部分彻底改舒服了。MRAM 的读写行为和 SRAM 几乎一样,没有擦除、没有写寿命焦虑,掉电数据不丢,这在嵌入式工业场景里属于降维打击。这篇文章把整个方案从选型、硬件接线、FSP 配置到读写代码、故障排查完整记录下来,适合正在做数据记录、参数存储、故障日志类项目的嵌入式工程师直接参考。
1. 方案选型:为什么是 MR25H40CDF + R7FA2E2A72DNK
1.1 MRAM 和传统非易失存储的本质区别
做存储方案的时候,大家第一反应通常是 SPI NOR Flash 或者 I2C EEPROM。这两种器件在消费电子产品里非常成熟,但一旦放到工业现场,就会暴露出两个问题:写寿命有限,写之前要擦除。
以普通 SPI NOR Flash 为例,典型扇区擦除次数是 10 万次,写一页数据之前要先做整块擦除,这个动作在实时性要求高的系统里很尴尬。EEPROM 虽然不用擦除,但容量通常不大,而且 I2C 总线速度一般,频繁写容易把器件写穿。
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,按字节编址,相当于一个 512KB 的 Byte 寻址非易失存储区。它最大的特点是没有擦除操作、没有写入次数限制,写入速度是真正意义上的“总线有多快,写就有多快”。实际标称读写循环次数是 10 的 14 次方量级,对工业设备来说基本等于无限寿命。
| 对比维度 | MR25H40CDF (MRAM) | SPI NOR Flash | EEPROM |
|---|---|---|---|
| 写入方式 | 直接改写 | 先擦除后写 | 直接写 |
| 写寿命 | 10^14 级别 | 10^5 级别 | 10^6 级别 |
| 写速度 | 总线时钟快,无擦除等待 | 受擦除时间限制 | 受总线协议限制 |
| 掉电保持 | 常温约 20 年 | 10~20 年 | 5~10 年 |
| 容量 | 4Mbit | 常见 1~64Mbit | 常见 128~512Kbit |
也就是说,把 MRAM 当作 MCU 外部的一颗“掉电不丢的 SRAM”来用,完全不需要考虑磨损均衡,不需要管扇区对齐,也不需要为擦除时间做任何妥协。这在写日志、实时保存运行参数、频繁更新配置这类场景里,优势非常明显。
1.2 RA2E2 这颗 MCU 能做什么
R7FA2E2A72DNK 属于瑞萨 RA2E2 系列,基于 Arm Cortex-M23 内核,主频 48MHz,片上 Flash 和 SRAM 配置做中小型工控传感器、数据采集板足够用。RA2 系列的外设接口统一由 FSP(Flexible Software Package)配置,SPI、UART、I2C 这些常用外设通过 e2 studio 图形界面拖一拖就能生成驱动,不用抱着寄存器和参考手册啃半天。
Cortex-M23 内核虽然不带很多浮点和复杂运算资源,但跑工业控制逻辑、管理一个 SPI MRAM、做状态机和数据记录都绰绰有余。它还能工作在 -40℃ 到 +85℃ 的工业级温度范围,适合现场设备。
选这个组合的另一个原因,是 RA2E2 的 SPI 外设本身可以工作到几 MHz 甚至更高,配合 MR25H40CDF 的 40MHz 理论时钟上限,实际项目里跑 10MHz 左右非常轻松。数据记录任务不再成为系统实时性的瓶颈。
1.3 这个组合适合做什么项目
经验上最贴合的场景有以下几类:
- 设备运行日志:不停记录事件、报警、故障码,写频繁但单次数据量小。
- 停机保参数:变频器、驱动器、工业仪表关机前需要把当前状态写入,上电再读回来。
- 在线升级辅助参数:在固件升级过程中保存临时配置和回滚标志。
- 检定记录:计量设备需要保存校准数据和测量历史,不能丢也不能磨损。
如果你正在做上述类型的嵌入式产品,MR25H40CDF 加 RA2E2 这套组合,硬件改动小,软件驱动简单,长期维护成本低,很值得认真考虑。
2. 硬件连接:别把简单的事情搞复杂
2.1 MR25H40CDF 管脚与接线
MR25H40CDF 的封装是 8 脚 DFN 模块,体积很小,管脚功能非常标准。下面是管脚定义和实际接法:
| 管脚名 | 功能 | 接法说明 |
|---|---|---|
| VDD | 主电源 | 接 3.3V,靠近管脚放 0.1uF 去耦电容 |
| VSS | 地 | 接 GND |
| CS# | 片选 | 接 MCU 任意空闲 GPIO,低电平有效 |
| SCK | 串行时钟 | 接 MCU SPI SCK |
| SI | 串行输入(MOSI) | 接 MCU MOSI,命令和数据从 MCU 发往 MRAM |
| SO | 串行输出(MISO) | 接 MCU MISO,数据从 MRAM 读回 MCU |
| WP# | 写保护 | 接 VDD 上拉,禁止上电时意外写保护 |
| HOLD# | 通信暂停 | 接 VDD 上拉,不使用该功能时保持高电平 |
这里第 3 题是片选,很多人偷懒直接把 CS# 固定接地,这是不行的。要等待状态寄存器、执行写使能、区分不同命令,CS# 必须在命令和命令之间拉高一下。我习惯用一个普通 GPIO 控制 CS#,不占用 SPI 主设备的硬件片选结构,操作更灵活。
WP# 和 HOLD# 这两个管脚一定不能悬空。HOLD# 悬空,遇到外部干扰或者上电时序瞬间,可能进入 hold 状态,SPI 通信直接呆住。WP# 悬空也可能出现写不了数据或者误写保护的情况。稳妥做法是各接一个 10kΩ 电阻到 3.3V。
2.2 用 RA2E2 的 SPI 通道连接
以 RA2E2 的 SPI0 为例,接线就是标准的四线 SPI:
- SCK 连接 MRAM 的 SCK
- MOSI 连接 MRAM 的 SI
- MISO 连接 MRAM 的 SO
- 一个 GPIO 连接 MRAM 的 CS#
MCU 侧 SPI 引脚和 GPIO 都通过 FSP 配置。实际的管脚编号和开发板有关,建议打开 e2 studio 生成工程后用针脚映射图确认,不要想当然拿同一个号码。
电源方面,RA2E2 和 MR25H40CDF 如果共用同一个 3.3V 电源轨,注意去耦。MRAM 在写操作时的瞬时电流变化不大,但工业现场电源纹波往往比较大,建议给 VDD 加 1μF 和 0.1μF 双电容组合,并且在 PCB 上尽可能靠近管脚走线。
如果 MCU 和 MRAM 分属不同电源轨,必须做上电时序检查。MCU 先稳定运行,再给 MRAM 上电,或者反过来,不能出现 MRAM 已经正常供电但 MCU 还没配置好 GPIO、把 CS# 拉低的时序窗口。最简单的方法是让两个器件共用同一颗 LDO 输出,然后控制 MCU 复位时间,等电源稳定再操作。
2.3 信号完整性和工业现场的抗干扰
SPI 时钟在 10MHz 以下,一般不需要复杂的信号完整性设计,但工业现场有电机、继电器、变频器等强干扰源,还是要做一些基础防护:
- SPI 数据线和时钟线尽量短,不走长排线。
- 在 MCU 侧给 SCK、SI 串接 33Ω 电阻,抑制反射。
- CS# 线建议加一个 1kΩ 串联电阻,避免现场浪涌直接打进 GPIO。
- PCB 的 GND 要完整,不要在 MRAM 下方大面积镂空。
这些都不是必须项,但加了的板子在 EMC 测试时能少很多麻烦。尤其是 DFN 封装背面有散热焊盘,很多工程师漏接或者错接,轻则温度特性变差,重则芯片工作不稳定。设计封装时务必确认中间焊盘是否连接 GND,然后好好焊接到地平面。
3. FSP 工程配置与软件驱动实现
3.1 用 e2 studio 创建 RA2E2 SPI 驱动
瑞萨的 FSP 配置界面已经相当成熟。新建工程时选择 RA2E2 芯片型号,然后在 Stacks 配置里添加 SPI Master 组件,设置为 Polling 或 Interrupt 模式都可以。
实际操作时我推荐这样配置:
| 配置项 | 推荐值 | 理由 |
|---|---|---|
| SPI 模式 | Master | MCU 作为主设备控制 MRAM |
| 时钟极性 CPOL | 0 | MRAM 支持 Mode 0 和 Mode 3,这里的组合都能用 |
| 时钟相位 CPHA | 0 | 与 CPOL 保持一致,Mode 0 最常用 |
| 比特率 | 10MHz 起步 | 稳妥起见,调通后再升到 20MHz 以上 |
| 位宽 | 8 Bit | 命令和地址都是字节操作 |
| 片选控制 | 手动 GPIO | 灵活控制 CS#,便于多命令组合 |
FSP 自动生成 SPI 驱动后,回调函数和初始化代码都有了。因为 MRAM 的每次访问都是先拉低 CS、发命令、再拉高 CS,所以最好把 SPI 的硬件自动片选功能关掉,或者不用硬件的 CS 通道,只利用 SCK/MOSI/MISO,由普通 GPIO 负责 CS#。
3.2 MR25H40CDF 的核心命令集
MR25H40CDF 的 SPI 命令集和普通 SPI Flash 很像,但没有擦除命令。要真正用好它,掌握以下命令就足够了:
| 命令名 | 命令码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,任何写操作前必须先发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器,可配置块保护 |
| READ | 0x03 | 读数据,3 字节地址 |
| WRITE | 0x02 | 写数据,3 字节地址 |
状态寄存器最低位是 WIP(Write In Progress),写入操作完成后,需要轮询这个位,直到它变 0,才能进行下一次操作。这和 Flash 的忙等待机制相似,区别是 MRAM 的忙等待时间极短,通常几百纳秒,轮询一次就通过了。
还有一个关键点是 WEL 位(Write Enable Latch),在状态寄存器 bit1。WREN 命令之后 WEL 置 1,一旦完成一次写操作或 WRSR 后,WEL 自动清 0,所以每一次写命令前都要重新执行 WREN。很多新手上手 MRAM 时直接发 WRITE 命令,发现写不进去,就是漏了这一步。
3.3 手动片选下的 SPI 收发封装
在 RA2E2 上使用 FSP 生成的 SPI 驱动,最方便的调用接口是R_SPI_WriteRead。这个函数是全双工通信,发送一个字节的同时会接收一个字节。对于 MRAM 这种“主设备发命令、从设备回数据”的器件,必须要理解这一点。
下面先封装一个简单的 SPI 字节交换函数:
#include "hal_data.h" static void spi_exchange(uint8_t *tx, uint8_t *rx, uint32_t len) { uint8_t dummy = 0x00; spi_event_t event; for (uint32_t i = 0; i < len; i++) { /* 发送 tx[i],同时读回 rx[i] */ R_SPI_WriteRead(&g_spi0_ctrl, &tx[i], &rx[i], 1, SPI_BIT_WIDTH_8_BITS); do { R_SPI_EventGet(&g_spi0_ctrl, &event); } while (event != SPI_EVENT_TRANSFER_COMPLETE); } }这里用一个字节一个字节的循环来保证逻辑清晰。实际追求吞吐率时,可以把整包数据一次传给R_SPI_WriteRead,但我个人觉得一个字节一个字节交换更容易理解片选时序,也不容易踩 buffer 管理的坑。硬件 SPI 速度足够快后,这种封装对整体性能影响不大。
接着写片选控制函数:
static void mram_cs_low(void) { R_GPIO_PinWrite(&g_cs_pin_ctrl, BSP_IO_PORT_04_PIN_01, BSP_IO_LEVEL_LOW); } static void mram_cs_high(void) { R_GPIO_PinWrite(&g_cs_pin_ctrl, BSP_IO_PORT_04_PIN_01, BSP_IO_LEVEL_HIGH); }注意实际函数里g_cs_pin_ctrl以及BSP_IO_PORT_04_PIN_01要换成 FSP 生成的 pin 句柄,这里只是示意。
3.4 读操作完整实现
读 MRAM 的过程很简单:拉低 CS,发送 READ 命令(0x03),再发 3 字节地址(大端模式,高位在前),然后从 SO 连续读回 N 个字节,最后拉高 CS。
由于是全双工 SPI,在读取数据阶段,MCU 必须不断发送空字节,比如 0x00,才能把 SO 上的数据用时钟采进来。完整代码如下:
#define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_ADDR_MASK 0x7FFFF static fsp_err_t mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t tx[64 + 4]; uint8_t rx[64 + 4]; if (len > 64) { return FSP_ERR_ASSERTION; } addr &= MRAM_ADDR_MASK; tx[0] = MRAM_CMD_READ; tx[1] = (uint8_t)(addr >> 16); tx[2] = (uint8_t)(addr >> 8); tx[3] = (uint8_t)(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { tx[4 + i] = 0x00; } mram_cs_low(); spi_exchange(tx, rx, 4 + len); mram_cs_high(); memcpy(buf, &rx[4], len); return FSP_SUCCESS; }这段代码在 len 的边界上没做太多优化,但可读性非常高。将读回数据rx[4]开始拷贝到用户缓冲区,正是跳过了命令和地址阶段的无效数据。
3.5 写操作完整实现
写操作需要三步:先发 WREN 写使能,再拉低 CS 发 WRITE 命令和地址以及数据,最后轮询 WIP 位确认完成。如果省略最后一步,紧接着发下一次命令,虽然 MRAM 写本身极快,但严格状态下还是可能出现状态未就绪的问题。
static void mram_wait_wip(void) { uint8_t tx[2]; uint8_t rx[2]; do { tx[0] = MRAM_CMD_RDSR; tx[1] = 0x00; mram_cs_low(); spi_exchange(tx, rx, 2); mram_cs_high(); } while ((rx[1] & 0x01) != 0x00); } static fsp_err_t mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t tx[64 + 4]; uint8_t rx[64 + 4]; if (len > 64) { return FSP_ERR_ASSERTION; } addr &= MRAM_ADDR_MASK; /* 步骤1: 写使能 */ tx[0] = MRAM_CMD_WREN; mram_cs_low(); spi_exchange(tx, rx, 1); mram_cs_high(); /* 步骤2: 写命令 + 3字节地址 + 数据 */ tx[0] = MRAM_CMD_WRITE; tx[1] = (uint8_t)(addr >> 16); tx[2] = (uint8_t)(addr >> 8); tx[3] = (uint8_t)(addr & 0xFF); memcpy(&tx[4], buf, len); mram_cs_low(); spi_exchange(tx, rx, 4 + len); mram_cs_high(); /* 步骤3: 等待写完成 */ mram_wait_wip(); return FSP_SUCCESS; }这里len最大 64 是示例,实际 MR25H40CDF 支持更长的连续写。在首版驱动里,建议限制单次读写长度,等调通后再放开,这样排查问题定位更简单。
需要多说一句,MR25H40CDF 不像 NOR Flash 有“页”概念,虽然是按命令发送多字节,但它可以逐字节写入,不需要整页缓冲,也没有“跨页写入限制”。这让代码逻辑比 Flash 驱动简单太多。不过依然要尊重“每次写操作都先 WREN”的规则,芯片不会跳过这个检查。
3.6 初始化与自检代码
项目启动时,建议做一次简单的 MRAM 自检,避免现场板子出现存储芯片虚焊或者损坏时才暴露问题。自检思路:写一个固定测试模式到某个地址,读回来比对。
void mram_init_self_test(void) { uint8_t pattern[4] = {0xA5, 0x5A, 0xC3, 0x3C}; uint8_t readback[4] = {0}; mram_write(0x00000, pattern, sizeof(pattern)); mram_read(0x00000, readback, sizeof(readback)); if (memcmp(pattern, readback, sizeof(pattern)) != 0) { /* 这里留一个失败标志,让上层应用报错 */ } }这个自检地址可以选在存储区末尾或者独立位置,不要覆盖正式数据区。实际量产时,还可以把测试地址设置为烧录后的保留区域,自检失败直接点亮故障 LED 或上报上位机。
4. 工业场景下的可靠数据存储设计
4.1 掉电存储的关键:写使能不要乱来
工业设备最担心的是突然断电。使用 MRAM 后,掉电存储的可靠性比 Flash 高很多,但也不是完全不设防。
首先要明确一点:MRAM 写入窗口极短,只要 MCU 检测到掉电中断,在电压跌落到芯片无法工作之前,能完成几个字节甚至几十个字节的写入。RA2E2 有低电压检测和中断功能,可以设置一个电压阈值,当电源开始跌落时立刻触发中断,在中断服务函数里把关键运行状态写入 MRAM。
写操作本身有 WREN 保护,每次写都要先使能,这避免了上电瞬间误写。另一方面,系统上电时要确保 MRAM 的 CS# 是高电平,否则 MCU 在初始化期间如果误发字节,可能把 MRAM 当成垃圾命令执行。我的做法是:在 MCU 启动代码最早阶段就把片选 GPIO 配置为输出高,再做 SPI 初始化。
4.2 数据校验:CRC32 或者双备份
MR25H40CDF 的误码率极低,但工业现场的数据完整性不能只靠芯片保证。关键数据我习惯做两层保护:
- 每一帧数据末尾附带 CRC32 校验值,读取时重新计算,发现不匹配就认为该记录失效。
- 重要配置参数存两份,一份主区,一份备份区,同时记录一个序列号或者版本号。写入时先写备份区,再写主区;上电读取时比较两组数据,以序列号新的为准。
这种做法可以防止以下情况:记录写入一半时系统断电,或者极端干扰导致单帧数据错误。MRAM 没有擦除周期,双备份的代价非常小,不像 Flash 那样还要考虑两个扇区的磨损均衡。
4.3 数据缓冲区与写入策略
MRAM 虽然写速快,但在工业环境里也不要傻乎乎每次变量改变都立刻写一个字节。太频繁的 SPI 写入会让 CPU 时间碎片化,也增加了通信出错概率。
我的经验是设计一个小型写缓冲池:
- 运行时把需要保存的参数缓存在 RAM 中。
- 设定一个保存周期,比如每 100ms 批量写入一次。
- 关键变量变更时,通过标志位触发立即保存。
- 掉电中断时,把缓冲区数据整体写入 MRAM。
这样既保持了实时性,又把 SPI 通信次数降到最低。MR25H40CDF 写操作没有最小写入单位,所以每次只改一个字节也是合法且高效的,批量写更多是为了减少总线的占用。
4.4 数据保存格式建议
给 MR25H40CDF 做一个简单的逻辑地址映射,非常有必要。不要直接用裸地址散装保存各种变量,以后加一个字段会很痛苦。
我通常会在 MRAM 起始位置放一个 Header 结构体:
typedef struct { uint32_t magic; /* 魔数,比如 0x4D52414D */ uint32_t version; uint32_t data_len; uint32_t crc32; } mram_header_t;每次写入真实数据,先更新这个 Header,再在紧随其后的地址写入数据。读取时先检查 magic 和 version,如果不匹配就认为数据区未初始化或者格式不兼容,走默认配置流程。这样产品的存储布局可以随固件版本升级而演进,不用每次改都要考虑老设备兼容问题。
5. 常见问题与排查实录
5.1 读写一切正常,但板子掉电重启后数据丢失
这个问题看起来是 MRAM 没保存,实际上极有可能是写了但没写成功,或者写到了错误地址。先确认状态寄存器的 WIP 轮询逻辑是否正确,再确认 WREN 是否在每次写命令前都执行。
MRAM 写数据时如果 WP# 被拉低,写操作会被禁止。检查硬件原理图,WP# 必须接上拉电阻到高电平。HOLD# 同样要上拉,如果 HOLD# 浮空,SPI 通信可能在某个电平状态下停止时钟的有效采样,表现为偶尔读对、偶尔读错。
5.2 读回的数据是上电初始值或全 FF
全 FF 一般代表没有正确选中芯片,或者 SPI 的 MISO 数据根本没采回来。先看 CS# 是否在命令期间保持低电平,SCK 是否真的在翻转,再从示波器或逻辑分析仪看 MISO 有没有波形。
全 00 可能是读到空白初始状态,MRAM 出厂时通常不是全 FF,这个和 Flash 不太一样,不用太纠结,直接读原厂默认状态寄存器或写一个特征值验证即可。
5.3 SPI 通信偶发错误,多发生在温度变化后
这个现象我遇到过两次,最终都是因为 DFN 封装焊接问题。MR25H40CDF 的 DFN 封装底部散热焊盘如果没有可靠接地,或者焊接时温度不够,芯片外壳和内部 die 之间可能存在虚接,温度变化后接触电阻漂移,直接表现为通信不稳定。
排查思路:用热风枪给芯片局部加温,如果故障频率明显变化,大概率是焊接或引脚接触问题。另外,SPI 信号线尽量靠近 GND,不要跨越板上的强电区域。
5.4 同一批板子部分工作正常,部分上电卡死
这种情况优先排查 CS# 上电时序。如果 GPIO 在初始化前处于高阻态,CS# 被悬空,MRAM 可能误判 SPI 信号而进入异常状态。解决方法是给 CS# 接一个上拉电阻到 VDD,或者在 MCU 初始化最早期就把该引脚下拉/上拉到确定电平。RA2E2 的 GPIO 默认状态通常在 FSP 初始化后才稳定,因此硬件上拉电阻更可靠。
5.5 调试工具:逻辑分析仪怎么用
调试 SPI 到 MRAM 时,我习惯用逻辑分析仪同时抓 CS#、SCK、SI、SO 四根线。重点看:
- CS# 拉低后,SCK 和 SI 上的命令字节是否和代码一致。
- 命令之间 CS# 是否确实拉高过。
- 读操作时 MISO 上的数据和预期是否一致。
有一次排查了很久,结果发现代码里地址字节顺序写反了,把(addr >> 16)和(addr & 0xFF)对调,逻辑分析仪一眼就看到了地址错位。所以强烈建议先把驱动跑起来用逻辑分析仪拖一遍波形,再继续上层业务开发。
6. 个人经验与后续扩展
这套方案在实际项目里已经跑了一年多,最直观的体感是“存储不再是一个让人焦虑的模块”。以前用 Flash 时,写日志前要想尽办法减少擦写次数,还要维护磨损均衡算法;换成 MRAM 后,这些全部删掉了,代码量少了一大截。RA2E2 的 FSP 环境也确实省事,SPI 驱动生成后直接把上面的封装函数丢进去就能用,几乎没有和寄存器较劲的阶段。
再分享一个实用小技巧:如果项目偶尔需要升级,可以用 MRAM 里一个字节做启动标志。固件跳转前,先把这个字节写成某个升级状态,重启后 Bootloader 读取这个标志决定是否进入升级模式。升级成功后再写回正常运行状态。用普通的 EEPROM 或 Flash 做这件事,担心写入次数和掉电中间状态,但 MRAM 完全不用担心,标志位的读写就像玩一样,稳定且干净。
如果后面想把方案做得更完整,可以考虑在 MRAM 上实现一个简单的环形日志区,通过读地址指针和写地址指针实现无磨损的循环记录。因为 MRAM 没有擦除限制,日志区天然支持无限覆盖,这在数据采集和故障记录产品里非常实用。有兴趣的同行可以把本文的读写驱动进一步封装成存储抽象层,上层直接调用record_save和record_load就够了。