工厂设备突然断电,最先遭殃的往往不是机械部件,而是里头的存储数据。跑了几年的现场,什么“上电参数全丢”“日志文件变成一段乱码”“擦写太频繁芯片报废”这类问题见得太多了。后来接触了 MRAM 这种非易失存储,再配合 NXP 的 MK20DN128VFM5 单片机做了一整套存储方案,才算是把这类痛点彻底压下去。这篇文章就专门拆一下这个组合:为什么选 MR25H40CDF,怎么接线、怎么写驱动,掉电保存怎么做才可靠,以及我在实际调试和量产中踩过的坑。适合正在做传感器采集、工业仪表、PLC 参数掉电保存、设备运行日志记录的嵌入式工程师参考,玩的是嵌入式数据存储的硬核路子,但阅读门槛不高,代码和思路都能直接搬。
1. 为什么是 MRAM 与 Kinetis 这个组合
1.1 传统存储方案在工业场景的三个“老大难”
工业设备里存数据,过去最常用的无非是 EEPROM、NOR Flash,或者 SRAM 加后备电池。这三种方案各有各的脾气,但放到现场环境里,问题就集中爆发了。
先说 EEPROM 和 Flash。它们的共同问题是写入前必须擦除,而擦除动作本身就意味着老化。Flash 的擦写寿命通常在 10 万次到 100 万次之间,听起来不少,可一旦程序有 bug 或者逻辑疏忽,频繁写日志、循环覆盖参数,寿命衰减比想象中快得多。更麻烦的是擦除粒度:NOR Flash 擦一个扇区就是 4KB、64KB 甚至更大,你想改 4 个字节,得先把一整个扇区读出来、改掉、再全擦全写,这中间一旦掉电,那一个扇区的数据基本就报废了。我在现场修过一块 PLC 扩展卡,故障现象就是“每次断电重启参数随机丢”,拆开一看,程序每次上电都往固定地址写 32 字节的配置,Flash 那个扇区的块已经被磨穿了。
再看 SRAM 加电池的经典方案。掉电瞬间靠电池维持 SRAM 内容,逻辑简单,但电池本身是消耗品,在高温设备间里三五年就漏液或者亏电,维护成本极高。而且 SRAM 的内容不是磁盘文件,没有“格式”概念,哪怕掉电一瞬间的毛刺干扰,都可能把内存里的数据打得支离破碎。工业现场的强电、变频器、继电器,哪个不是电磁干扰大户,这种方案在机房还行,放到设备层就是给自己埋雷。
1.2 两颗芯片各自的定位
MR25H40CDF 是 Everspin 的 SPI 接口 MRAM 芯片,容量 4Mbit,也就是 512KB。MRAM 这货的技术底子是磁隧道结,本质上靠磁矩方向存数据。它同时拥有 SRAM 的读写速度和 Flash 的非易失性,写入不需要擦除,读和写的时间是一致的,而且理论写寿命是无限的。工业级温度范围、低功耗、SPI 接口直接挂 MCU,省事儿。
MK20DN128VFM5 来自 NXP Kinetis 系列,Cortex-M4 内核,主频跑到百兆级,128KB Flash,16KB SRAM,QFN32 小封装,板子面积能做得非常小。这芯片上我尤其看重两个点:一是硬件 SPI 模块的配置灵活,时钟和帧格式都可以按 MRAM 的要求调;二是 Kinetis 自带的低压检测模块(LVD),可以用来做掉电预警,这在后面做掉电保存时是核心硬件保障。换句话说,MR25H40CDF 负责“可靠地存”,MK20DN128VFM5 负责“及时地管”,这个组合在数据完整性和实时性上是互补的。
1.3 MRAM 与 FRAM、Flash 的对比
选型的时候,很多人会拿 MRAM 和 FRAM 比,毕竟 FRAM 也是不擦除、快写、耐用的非易失存储。我列一张实际工程视角的对比表:
| 参数 | MR25H40CDF(MRAM) | MB85RS256(FRAM) | 普通 SPI NOR Flash |
|---|---|---|---|
| 容量 | 4Mbit(512KB) | 256Kbit(32KB) | 1Mbit 起 |
| 写前擦除 | 不需要 | 不需要 | 按扇区擦除,毫秒级 |
| 写入寿命 | 理论无限 | 10 的 10 次方次 | 10 万到 100 万次 |
| 写单字节耗时 | 微秒级(SPI 时钟决定) | 微秒级 | 写加擦除,毫秒级 |
| 数据保持 | 10 年以上 | 10 年以上 | 10 年以上 |
| 接口 | SPI | SPI | SPI |
| 掉电后数据 | 不丢失 | 不丢失 | 不丢失(但擦除中掉电会坏块) |
FRAM 的问题是容量天花板低,大厂现货常见到 256Kbit 或 1Mbit 就到顶了,日志抓录、波形存储这种需求根本装不下。NOR Flash 容量大,但就是前面说的擦写寿命和掉电损坏问题。MRAM 恰好站在两者中间:容量足够装几万条日志,写起来像 SRAM 一样痛快,掉电又不丢。多花的那几块钱,买的是现场少跑一趟的售后服务。
2. 硬件设计与接线要领
2.1 MR25H40CDF 引脚与接口
MR25H40CDF 是标准的 8 脚 SPI NOR Flash 兼容引脚排列,所以 PCB 上甚至可以直接兼容普通 Flash 的焊盘。引脚功能如下:
| 引脚 | 功能 | 说明 |
|---|---|---|
| CS | 片选 | 低电平有效,整个读写命令帧必须保持低 |
| SCK | 串行时钟 | 输入时钟,最高可达几十 MHz 甚至 108MHz |
| SI | 数据输入 | MOSI,MCU 发送到 MRAM |
| SO | 数据输出 | MISO,MRAM 返回数据 |
| WP | 写保护 | 低电平禁止状态寄存器写操作 |
| HOLD | 保持 | 低电平时暂停通信但不复位内部逻辑 |
| VDD | 电源 | 3.3V,需就近去耦 |
| GND | 地 | 公共地 |
接线没有悬念:四根信号线直接连到 MK20 的一个 SPI 外设上,我选的是 SPI0,因为它的引脚可以做引脚复用,和调试串口、下载口不冲突。CS 我没有用 Kinetis DSPI 硬件自动片选,而是拿一个普通 GPIO 手动控制,原因后面写驱动的时候会说。HOLD 和 WP 这两个脚是关键,很多人第一次画板子会把它们空着,结果就是上电后芯片偶尔“罢工”——其实是 HOLD 脚受干扰被拉低了,SPI 通信被暂停。正确做法是 WP 接上拉到 VDD,HOLD 接上拉到 VDD,板上再各加一个小电容到地,100nF 就够,滤掉手持设备、继电器动作产生的毛刺。
2.2 K20 端电路注意事项
MK20DN128VFM5 这颗芯片是 3.3V 供电的,没问题,但电路设计上我有三处想提醒:
第一,电源去耦不能省。MRAM 写入瞬间的瞬态电流会拉动电源电压,如果去耦电容离芯片太远,走线电感一叠加,VDD 尖峰可能突破极限。我在 MRAM 的 VDD 脚上放了一颗 0.1μF 陶瓷电容,紧贴引脚;同一网络再放一颗 10μF 的钽电容或者 MLCC,管住中低频纹波。MK20 侧的电源脚同理会有一组去耦,别共用一根细走线接到三端稳压器。
第二,CS 信号上电期间的电压问题。MCU 的 GPIO 在复位期间是高阻态,板子上电瞬间 CS 如果悬空,MRAM 可能因为噪声被误触发写入。我在这里加了一颗 10kΩ 上拉电阻到 VDD,让 CS 默认保持高电平,等 MCU 初始化完再接管。同样,SI、SCK、HOLD 最好也有确定电平,最省事的就是各自加一个上拉或者下拉,具体极性按复位状态来。
第三,PCB 布线控制在合理范围。SPI 时钟在几十 MHz 以下,不必像高速 DDR 那样阻抗匹配,但也不能把导线拉到十几厘米。我的经验是 MRAM 和 MCU 保持 3~5 厘米以内,SCK 和 SI 串 22Ω 到 33Ω 的贴片电阻,用来抑制过冲。地线包好,尽量避免信号线跨越噪声源,比如继电器驱动线、开关管驱动线。
2.3 掉电检测硬件回路
要可靠掉电保存,光靠 MCU 自己反应不够,必须有一个硬件掉电检测机制告诉我“电源马上要没了”。Kinetis K20 的 PMC 模块里就有低压检测功能,可以设置一个阈值,当 VDD 降到阈值以下时触发复位或者中断。我把 LVD 中断用起来,设置为下降沿触发,阈值选在 2.9V 附近(VDD 正常工作 3.3V)。
有了 LVD 中断,软件就能在电源真正跌穿芯片工作电压之前抢出一段时间来。但这段“抢救窗口”能撑多久,完全取决于后端电容的容量。估算公式很简单:
电源输出电流 I 大约等于系统总耗流。假设掉电瞬间系统耗流 50mA,允许电压从 3.3V 跌到 2.7V(再低 MCU 就没法安全操作了)压差 0.6V,要撑 20ms,那么电容 C = I × t / ΔV = 0.05 × 0.02 / 0.6 ≈ 1667μF。
所以我在电源输入端并了一颗 2200μF 的电解电容,再配合 LVD 中断,实测能给我大概 25ms 到 30ms 的保存窗口。这个时间用来写完 64 字节的关键参数绰绰有余。
3. 软件实现:从驱动到掉电保存
3.1 SPI 驱动与读写原语
MR25H40CDF 的 SPI 帧格式和普通 SPI Flash 高度类似,支持模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1),我用的是模式 0。写命令序列是这样的:
- CS 拉低。
- 发送 0x06(WREN),把写使能锁存位置 1。
- CS 拉高,结束命令。
- 再次 CS 拉低。
- 发送 0x02(WRITE),随后发送 3 字节的 24 位地址,高字节在前。
- 连续发送数据字节,写入多少个都可以,边发送边写入。
- 全部数据发送完,CS 拉高,本次写入完成。
读命令更简单:
- CS 拉低。
- 发送 0x03(READ),随后发送 3 字节地址。
- 持续输出时钟,从 SO 引脚读回数据。
- 读完,CS 拉高。
这里有一个和其他 SPI Flash 非常不一样的点:MRAM 写入完成后,不需要任何等待时间,不需要查状态寄存器里的“忙”标志,CS 拉高那一刻数据就已经落进磁隧道结里了。这一点让驱动代码和整个工程逻辑都简单了不少。
我手写了一个最简的裸机驱动,骨架如下:
#define MRAM_CS_LOW() GPIO_WriteLow(MRAM_CS_PORT, MRAM_CS_PIN) #define MRAM_CS_HIGH() GPIO_WriteHigh(MRAM_CS_PORT, MRAM_CS_PIN) static void mram_send_byte(uint8_t byte) { // 等待 SPI 发送寄存器为空 while ((SPI0->SR & SPI_SR_TFFF_MASK) == 0); SPI0->PUSHR = byte; } static uint8_t mram_recv_byte(void) { // 等待接收寄存器满 while ((SPI0->SR & SPI_SR_RFDF_MASK) == 0); return SPI0->POPR; } static void mram_write_enable(void) { MRAM_CS_LOW(); mram_send_byte(0x06); MRAM_CS_HIGH(); } void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { mram_write_enable(); // 每一帧写之前都必须 WREN MRAM_CS_LOW(); mram_send_byte(0x02); // WRITE mram_send_byte((addr >> 16) & 0xFF); // 地址高字节 mram_send_byte((addr >> 8) & 0xFF); // 地址中字节 mram_send_byte(addr & 0xFF); // 地址低字节 for (uint32_t i = 0; i < len; i++) { mram_send_byte(buf[i]); } MRAM_CS_HIGH(); // 拉高即完成写入 } void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); mram_send_byte(0x03); // READ mram_send_byte((addr >> 16) & 0xFF); mram_send_byte((addr >> 8) & 0xFF); mram_send_byte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = mram_recv_byte(); // 发 0x00 占位,读出数据 } MRAM_CS_HIGH(); }注意代码里的细节:每次写帧开始前,都必须先发一次0x06写使能命令,而且 CS 必须拉高再拉低完成一个独立命令周期。这个“先 WREN 再 WRITE”的节奏如果省了,写入会直接被芯片忽略。我见过好多新手栽在这上面,SCK 打了一堆波形,数据一问三不知,最后发现漏了 WREN。
3.2 掉电检测与数据保存流程
有了硬件 LVD,软件流程就顺理成章。我把保存任务分成三层:
第一层,LVD 中断处理函数。中断一旦触发,马上停止不重要的外设操作,把当前要保存的关键参数从 RAM 里聚拢出来,调用 MRAM 写函数写进去。中断里不要做耗时的文件系统操作、不要跑协议栈,那会浪费宝贵的毫秒级窗口。我的做法是:中断里只做一件事——把“紧急保存请求”标志置位,同时唤醒一个最高优先级的任务/裸机主循环,让主循环里最精简的保存逻辑执行。如果担心中断里直接写 MRAM 够快,我是实测过写入时间才敢用“中断里直接写”的方案的:50 字节关键数据,SPI 40MHz 下大概 20μs 内完成,这个时间在 LVD 窗口内完全没问题。所以项目上我直接中断里写完,然后再复位。
第二层,正常运行的定时保存。工业设备运行期间参数可能随时变化,比如温度曲线表、累计运行时长,这些数据不能只在掉电时写一次,万一掉电检测瞬间 MCU 已经没法跑中断了,数据就丢的是最后一段。我的方案是“关键参数变化即写”,变化频率控制在每秒最多 10 次以内,MRAM 无限写寿命扛得住。
第三层,上电恢复。上电复位后,MCU 先从 MRAM 里把参数读出来,做校验,校验通过就拷贝到全局变量;校验失败就用默认参数,同时记录一个“上次参数丢失”的事件,方便现场排查。
判断 MRAM 里有没有有效数据,不能指望出厂状态。MRAM 未写入区域不一定是什么值,所以我在开头设计了固定的魔数头。一个有效参数区头格式如下:
typedef struct { uint32_t magic; // 固定为 0xA5A55A5A uint16_t seq; // 序列号,每次写入递增 uint16_t crc; // 对 data 部分做 CRC16 uint8_t data[64]; // 真正的参数 } param_block_t;上电读取流程是:读 magic,对不上就当没有有效数据;对上了,把 data 算一遍 CRC 和存储的 crc 比,一致才采用,不一致视为损坏。这个方案能防住绝大多数位翻转和半途写入导致的乱数据。
3.3 数据一致性与可靠性方案
单块参数区有个缺点:如果掉电正好发生在“写了一半,还没写入 crc 字段”的时刻,这个块就永久损坏了。工业上不许这种事情发生,所以我加了双区镜像:
| 区域 | 地址范围 | 用途 |
|---|---|---|
| 区 A | 0x00000 ~ 0x000FF | 主参数区 |
| 区 B | 0x00100 ~ 0x001FF | 镜像参数区 |
每次写入时,先写区 A,再写区 B,两个区的 seq 都加 1。上电恢复时,读取两个区,比较 magic、crc 和 seq,选“有效且 seq 较大”的那一份作为最终参数。如果一块损坏,另一块仍然可以兜底。这种“双写 + 序列号”的模式花不了多少代码,实用性却极高,强烈建议任何做掉电保存的兄弟都采用。
CRC16 的算法我也顺便附上,之前一直用查表法,省 CPU 时间:
static uint16_t crc16_ccitt(const uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { crc ^= (uint16_t)data[i] << 8; for (int bit = 0; bit < 8; bit++) { if (crc & 0x8000) crc = (crc << 1) ^ 0x1021; else crc <<= 1; } } return crc; }配合这套逻辑,即使掉电时机刁钻到“区 A 写了一半”,镜像区 B 的数据也完整,上电后系统能自动恢复到最近一次完整状态。
4. 项目实测与关键数据
4.1 读写性能实测
用 MK20DN128VFM5 跑起来之后,我做了几个简单但直观的实测。SPI 时钟配置为 40MHz,测试方法是用 GPIO 翻转配合逻辑分析仪抓时间。
- 写 4 字节:从发起写使能到 CS 拉高,约 4μs。
- 写 256 字节连续数据:约 16μs。
- 读 4 字节:约 3μs。
- 读 256 字节连续数据:约 12μs。
作为对比,以前用 NOR Flash 写一个 64 字节参数块,光擦除一个小扇区就得 20ms 到 100ms,再加上写数据时间,一次固化参数的代价是 MRAM 的几千倍。这个性能差距在日常参数保存上感知或许不强,但在实时性要求高的数据记录场景里,比如连续捕捉电机电流波形、振动传感器高频采样值,MRAM 才真正体现出价值。
我还特意试过用 DMA 方式把数据灌进 MRAM,吞吐率还能再拉高一截,不过那属于锦上添花。对大多数存储参数和日志的应用,阻塞式 SPI 驱动已经完全够用,代码还容易维护。
4.2 掉电验证过程
实验室里做掉电测试,不能只是按一下电源开关看结果,那样太粗糙。我的做法是用一个电子负载或者继电器控制板,让系统分别按 100ms、50ms、20ms、10ms、5ms 的间隔反复通断电源,每个间隔至少跑 200 次,同时每次断电前 1ms 左右随机改写参数区数据,模拟最恶劣情况。
实测结果很稳定:在 2200μF 电容和 LVD 配置的支撑下,20ms 以上的断电间隔全部恢复正确;5ms 级别的瞬时断电偶有恢复不到最新数据,但也不会出现数据损坏,恢复的是上一次完整写入的状态。这个表现完全满足工业设备对参数归档的需求。以前用 Flash 方案做同样测试,大概第 30 次断电就会出现“写入一半被断”的损坏记录,而 MRAM 侧的表现几乎是零缺陷——因为它压根没有擦除过程,不需要原子性保证的擦写窗口。
4.3 耐久性与数据保持
耐久性方面,MRAM 的写入机制是磁矩翻转而不是电荷注入,理论上没有磨损概念。我没法真的跑到无限次,但做了一个快速循环测试:以 10μs 间隔连续写 1000 万次,再周期读回,数据始终一致。放到实际产品中,如果每秒写一次,一年也就 3150 万次,依然在 MRAM 舒适区,完全免去了 Flash 那种“磨损均衡算法”的负担。
数据保持时间官方标称不少于 10 年,并且工业级温度范围覆盖到 105℃,这在电机控制柜和户外设备里非常够用。环境温度这一块我也做了高低温箱测试:-40℃ 和 85℃ 下各存放 72 小时,数据完好,读写时序没有明显恶化。
5. 常见问题与排查技巧实录
5.1 SPI 写不进数据
这是遇到最多的问题,四成原因是漏了写使能命令。注意 MRAM 和 Flash 不太一样,它不只写命令前需要 WREN,而是每一帧写命令都需要独立的 WREN 命令周期。另一个高频原因是 SPI 模式不匹配,芯片默认支持模式 0 和模式 3,但有的代码把 CPOL/CPHA 配成了模式 1,SCK 极性和相位一错,芯片收到的指令全部废掉。排查技巧是先发一个最小的读命令,抓 SO 上的波形,看会不会跟着地址回数据,如果回的全是 0xFF,多半是模式或者接线问题。
5.2 数据偶发丢失
在稳定性测试里偶发数据不对,优先怀疑电路噪声导致 CS、HOLD 或 SCK 被干扰。CS 在上电和高速翻转时如果有毛刺,可能让 MRAM 误认为片选被选中,进而进入一个假命令周期。解决方法就是我之前说的:CS 加 10kΩ 上拉,HOLD 和 WP 必须固定到确定电平,最好再串一个 1kΩ 到 10kΩ 的小电阻到 VDD,降低外部干扰影响。如果现场环境特别恶劣,还可以在 SCK、SI、SO 三根线上各串 100Ω 左右的电阻,牺牲一点点速度换抗干扰能力,把 SPI 时钟从 40MHz 降到 20MHz 也更稳。
5.3 K20 的 SPI 配置问题
用 Kinetis 的 DSPI 模块时,很多人会被寄存器搞晕。几个常见坑:
- 引脚复用没配好。Kinetis 的引脚默认是 GPIO 功能,必须设置
PORTx_PCRn的 MUX 位为 SPI 对应的 ALT 值,否则引脚永远是 GPIO,SPI 电平根本不输出。 - FIFO 没处理对。DSPI 的发送和接收各有一个 FIFO,连续写入数据时如果超过 FIFO 深度又不处理,发送会卡死。要么检查 FIFO 状态位,要么直接禁用 FIFO,用我前面那段阻塞式代码就绕开了这个问题。
- 手动 CS 和帧格式的配合。DSPI 的 PCS 信号自带自动拉低拉高的时序,如果同时用 GPIO 手动拉 CS,容易出现两个片选打架。我的建议是从头到尾全用 GPIO 手动控制 CS,把 DSPI 的 PCS 功能禁用或避免使用,逻辑上更直观,也更容易排查。
我把常见问题整理成一张速查表,方便现场对照:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 读回全是 0xFF | SPI 模式不匹配 / 接线错误 | 检查 CPOL、CPHA;核对 SI/SO 是否接反 |
| 写后读回仍为旧值 | 漏 WREN 命令 | 每一帧 WRITE 前补发 0x06 的完整命令周期 |
| 偶发写入不成功 | HOLD 引脚受干扰 | HOLD 接上拉 + 100nF 电容到地 |
| 上电瞬间数据被破坏 | CS 悬空误触发 | CS 加 10kΩ 上拉,MCU 复位期间保持高电平 |
| 掉电后再上电参数变回旧版 | 双区镜像未采用 seq 机制 | 增加 seq 字段,每次写递增,上电取 seq 大的有效区 |
| SPI 长时间传输出错 | 数据线过长或时钟太高 | 降时钟到 20MHz,信号线串 22Ω~100Ω 电阻 |
5.4 一个小技巧:用读回校验代替经验判断
很多工程师写完数据不读回,等到现场出问题才后悔。我的习惯是每次写完关键数据,立即从同一个地址读回来和源数据比较,不等则重试两次。虽然 MRAM 出错概率极低,但这条“写完即读”的规矩能挡住很多外围故障,比如 CS 拉高时序不对、SPI 时钟不匹配这类问题,当场就能暴露,不用等掉电才现形。
另一条经验是给 MRAM 驱动写一个自检函数,上电时对整个地址空间做一次写-读-回写-再读的模式测试,一旦检测到某个地址异常,就能立刻通过串口上报,避免带病运行。虽然 MRAM 寿命无限,但焊接缺陷、PCB 短路这类物理问题依然会发生,自检函数就是最后一道防线。
最后说一点个人体会:做工业嵌入式存储,最大的敌人不是芯片不够高级,而是设计者对“掉电这回事”没有敬畏。用了 MR25H40CDF 这种非易失、无限写、免擦除的芯片,软件上该做的双区镜像、CRC 校验、上电恢复逻辑一样都不能少。芯片解决的是物理层面的可靠性,软件解决的是逻辑层面的完整性,把 MRAM 的无损写入特性和 Kinetis 的灵活外设配合起来,这套方案才真正在工业现场立得住。