在工业现场折腾存储方案,最让人头疼的一件事就是掉电丢数据。以前用 SPI EEPROM,写一页要等十几毫秒,等不起;换 Nor Flash,又要先擦除再写入,磨损次数还让人焦虑。后来我在一个变电所监测装置里试了 MR25H40CDF 这颗 SPI MRAM,配合 MK22FN512VLH12 这颗 Kinetis K22F 单片机,整套数据记录方案终于不用再跟时序、寿命和掉电掉链子较劲了。这篇博文就把我在这套硬件上从原理图到驱动代码、再到现场排查的经验完整写出来,适合正在做嵌入式仪表、工业控制器、数据记录器,或者被“非易失存储”折腾过的朋友参考。
1. 为什么工业存储场景需要一对“偏门”组合
先说结论:MR25H40CDF 不是一颗网红芯片,但它把 SRAM 的无延迟写入和 Flash 的非易失性合在了一起。而 MK22FN512VLH12 作为 NXP 的 Kinetis 系列中偏工业向的 MCU,提供了足够多的 SPI 和 DMA 资源,让这颗 MRAM 能真正被玩出花来。这两颗器件的组合,不是为了跑分,而是为了在工业现场把“存储”这件事的复杂度降下来。
1.1 MR25H40CDF 到底是什么
MR25H40CDF 是 Everspin 推出的 4Mbit 串行 SPI MRAM,容量换算过来是 512K 字节,工作电压 3.3V,SPI 接口,支持标准 SPI Mode 0 和 Mode 3,最高时钟可以跑到 40MHz 左右。MRAM 的存储单元是磁隧道结,通过磁场方向来记忆数据,而不是靠电荷,所以它天然具备几个让嵌入式工程师两眼放光的特性:掉电数据不丢失、写入不需要擦除、理论上无限次写入、没有 EEPROM 那种“页写+等待”的延迟。
我最早注意到这芯片,是因为现场的一台环境监测设备每次断电重启后,最后几分钟的采样数据总是丢。原来的代码用的 EEPROM,定时写太频繁会磨寿命,写一次又必须等,所以只能用“定时批量写”糊弄过去。换成 MR25H40 之后,我直接把每次采样结果实时写进去,完全没有等待时间,数据完整率一下就上去了。
还要注意,MR25H40CDF 的字尾 CDF 代表封装和温度等级,我记得是工业温度范围,具体要查手册,但工程上用在 -40 到 +85℃ 甚至更宽的场合是没问题的。芯片还有 WP 和 HOLD 引脚,做板子时候别浮空,最好用电阻拉高,否则会在调试时出现莫名其妙的数据错乱。
1.2 MK22FN512VLH12 选中它不只是因为主频
MK22FN512VLH12 是 Kinetis K22F 家族的中端型号,Cortex-M4F 内核,主频 120MHz,512KB Flash、128KB SRAM。这块芯片的 Flash 容量和 SRAM 都够用,但真正吸引我的是它的 DSPI 模块和 DMA 支持。K22 的 SPI 可以配置 FIFO,还能通过 DMA 把数据从内存搬运到 SPI 发送寄存器,这在大批量写入 MRAM 时非常有用。
还有一个工程上的原因:K22F 的供电范围宽,1.71V 到 3.6V,I/O 耐压和工业级时序都符合我们现场设备的电源环境。与 MR25H40CDF 共用 3.3V 电源,接口电平直接匹配,不用加电平转换,省了不少事。
有人问为什么不用 STM32?不是不行,而是我手头这个项目其实是沿用老产品平台,原来就是 NXP 系,迁移成本最低。K22F 的调试工具链成熟,SDK 和库函数也做得比较规整,我可以在半小时内完成 SPI 初始化并跑通读写。如果你们是从零选型,只要 MCU 有 SPI、有 DMA、有足够 GPIO,基本都能复刻这套方案。
2. 硬件连接与电路设计
2.1 最小系统与供电
MR25H40CDF 工作在 3.3V,MK22FN512VLH12 也用 3.3V 供电。我从开关电源出来先做一级 DC-DC 降到 3.3V,然后加一个低压差 LDO 给数字部分供电,实测纹波能控制在 30mV 以内。MRAM 对电源噪声比普通 Flash 敏感一些,因为内部磁存储单元的读写过程对电压稳定性有要求,所以至少要有 100nF 和 10uF 的去耦电容紧挨着电源引脚。
K22F 这边要注意 VCAP 引脚,需要按手册要求外接电容,这个电容位置必须靠近 MCU,否则上电不工作或者运行不稳定。很多朋友第一次画 Kinetis 板子失败,多半是 VCAP 没处理好。MRAM 的 VCC 引脚我建议并联一个 1uF 陶瓷电容再加一个 0.1uF 高频电容,PCB 上摆放在芯片电源脚附近 3mm 以内。
2.2 SPI 引脚分配与片选策略
我选用的 K22F 型号是 LQFP64 封装,SPI 引脚分配上,我走了 SPI1 模块。SCK 用 PB11,MOSI 用 PB16,MISO 用 PB17,片选 CS 不接硬件外设,而是直接用一个普通 GPIO 控制。原因是 MRAM 的片选需要快速拉高拉低,用 GPIO 会更灵活,而且可以避免 DSPI 自动片选信号在连续传输之间出现多余的脉冲。
这里有个细节:MR25H40 的片选是低电平有效,每次命令必须以 CS 拉低开始,以 CS 拉高结束。有些工程师直接把片选接到 MCU 的硬件 SS 引脚,结果发现调用 SPI 库时硬件会自动控制 CS,在发送命令字节和地址字节之间产生不规则的片选翻转,导致 MRAM 直接忽略命令。所以我强烈建议用普通 GPIO 手控 CS,不要偷懒。
另外,WP 和 HOLD 引脚千万别悬空。WP 低电平时禁止写状态寄存器,HOLD 低电平时暂停串行通信,这两个引脚如果 PCB 走线受到干扰,就会偶发性锁死 MRAM。我一般把 WP 直接接 VCC,HOLD 也接 VCC,板子上拉 10k 电阻。
2.3 信号完整性和去耦实战
工业现场常见的干扰是电机启停、继电器吸合这类瞬态脉冲,如果 PCB 布局不合理,SPI 线路上会出现振铃。SCK 上沿太陡,会导致 MRAM 误采样数据。我在实际布线时的规则是:SPI 四根线尽量靠近并保持等长,走线宽度不小于 0.3mm,并且在 SCK 和 MOSI 上串联 33Ω 或 22Ω 的小电阻,用来抑制反射。
还有人问我需不需要屏蔽罩。我做的变电所监测装置其实没有用屏蔽罩,只要把模拟地和数字地在电源输入端单点连接,SPI 线远离继电器驱动线路,实测在 2kV 群脉冲干扰下数据读写依然稳定。如果你做更严苛的电机驱动板,建议 MRAM 的正下方铺完整地平面,减少地回路面积。
3. 底层驱动:SPI 配置与 MRAM 命令实现
3.1 K22 的 DSPI 初始化
写驱动之前,先要把 DSPI 模块配置成主模式。K22 的 DSPI 比我用过的普通 SPI 复杂一点,有 MCR 主配置寄存器、CTAR 时钟配置寄存器、SR 状态寄存器。初始化时我主要确认以下几点:主模式、CPOL=0/CPHA=0、帧大小 8 位、波特率。
波特率计算方式在 Kinetis 的参考手册里有公式,我直接用 SDK 里的 DSPI_HAL_Configure 函数,省得自己推寄存器。配置成 20MHz 比较稳妥。MR25H40 虽然能上到 40MHz,但我的 PCB 走线不算特别短,20MHz 留了一倍裕量,实测也没感觉到性能瓶颈。
代码大致是这个味道:
void spi_init(void) { // 使能 SPI1 时钟,配置引脚复用 SIM->SCGC6 |= SIM_SCGC6_SPI1_MASK; // 设置引脚为 DSPI 功能,并带上较强的驱动力 PORTE->PCR[0] = PORT_PCR_MUX(2) | PORT_PCR_DSE_MASK; // ... 省略 MOSI、MISO 配置 // 复位后配置为主模式 SPI1->MCR = SPI_MCR_MSTR_MASK | SPI_MCR_PCSIS(0x1) | SPI_MCR_DIS_RXF | SPI_MCR_DIS_TXF; SPI1->CTAR[0] = SPI_CTAR_FMSZ(7) | SPI_CTAR_CPOL(0) | SPI_CTAR_CPHA(0) | SPI_CTAR_BR(0); // 实际工程建议用 SDK 函数,可读性更好 SPI1->SR = SPI_SR_EOQF_MASK; }如果不用寄存器,用 NXP 官方 SDK 的DSPI_Init跟我上面配置的效果是一样的。关键是确保 CTAR 的时钟极性和相位与 MRAM 匹配,MR25H40 数据手册上写明支持模式 0 和模式 3,我选模式 0,这样和大多数 SPI 外设的习惯一致。
3.2 MR25H40 命令集与读写函数
MR25H40 的 SPI 命令非常简单,核心就几个:WREN(0x06)、WRDI(0x04)、READ(0x03)、WRITE(0x02)、RDSR(0x05)、WRSR(0x01)。它没有 Nor Flash 那种复杂的 Sector Erase 命令,因为写入前根本不需要擦除。这一点让驱动代码长度直接砍掉三分之一。
读单字节或者读整块,逻辑完全一样。比如读数据:
void mram_read_buffer(uint32_t addr, uint8_t *buf, uint32_t len) { mram_cs_low(); spi_write_byte(MRAM_CMD_READ); spi_write_byte((addr >> 16) & 0xFF); spi_write_byte((addr >> 8) & 0xFF); spi_write_byte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = spi_read_byte(); } mram_cs_high(); }这里注意,MR25H40 的地址是 19 位,需要发送 3 个字节地址,高位在前。我碰到过有人只发 2 字节地址,误以为 512Kbit 对应 64K 字节,结果地址一超过 64K 就错乱。实际上 512K 字节需要 19 根地址线,所以必须 3 字节地址。如果只用到低 64K,可以发 3 字节但高字节写 0,别省略。
写数据的时序稍微多一步,要先发 WREN 写使能。MRAM 虽然不需要擦除,但为了安全仍然设计了写使能锁存器,必须“先开锁再写”。整体流程是:
void mram_write_buffer(uint32_t addr, const uint8_t *buf, uint32_t len) { mram_write_enable(); mram_cs_low(); spi_write_byte(MRAM_CMD_WRITE); spi_write_byte((addr >> 16) & 0xFF); spi_write_byte((addr >> 8) & 0xFF); spi_write_byte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { spi_write_byte(buf[i]); } mram_cs_high(); mram_write_disable(); }WREN 后如果连续写很多条,可以省去中间几次 WREN,但稳妥起见我每次写前都发一次 WREN。多一个命令字节的开销几乎可以忽略,换来的是确定性更高。WRDI 也不是必须的,但写完关闭写使能可以防止干扰脉冲意外误写,工业环境里这算是一种安全习惯。
3.3 缓冲区读写与地址边界处理
MRAM 是按字节寻址的,没有页限制,但我的程序里仍然在 128 字节上做了封装,主要是为了配合系统里的 Modbus 寄存器映射。每个数据帧包含固定长度的上报数据,封装成 buffer,直接写入 MRAM 的特定偏移位置。
地址空间规划很重要。我通常把 512K 字节分成几个区:前面 64K 存参数配置,中间 384K 存历史记录,最后 64K 存运行日志和自检信息。这样不同用途的数据互不干扰,也方便以后做数据导出。封装一个“区块读”和“区块写”函数,传入区块编号和缓冲区,避免直接操作裸地址导致越界踩到别的数据区。
边界问题在跨 512K 地址末端时要特别注意,MRAM 地址是 19 位,如果写超了会回绕。所以我在封装函数里加了范围检查,len 超过区块剩余空间就拒绝执行并返回错误码。嵌入式里最怕的就是这种隐蔽的越界,加一行判断能省无数现场排查时间。
4. 让数据真正可靠的几个工程细节
4.1 CRC 校验与版本标记
存储芯片再可靠,也不代表 SPI 链路不可能受到干扰。工业现场强电磁环境会造成 MISO 线上毛刺,读回来的数据偶尔出现错位。我的做法是每个数据帧尾部加 CRC16,写入时计算好,读取时重新计算,不一致就丢弃该帧并尝试重新读取。
同时,每个数据区块开头放一个结构头,包含 magic number、版本号、数据长度、写入时间戳。上电恢复时先检查 magic number,如果不对就说明数据无效或地址错乱。这比单纯靠芯片非易失特性更稳妥,也是嵌入式数据库“元数据”思想的简化版。
CRC 多项式我用的是 Modbus 常用的 CRC16-CCITT,代码占用很小,注意查表法比逐位运算快得多,在 120MHz 主频下几乎没有感觉。读取 MRAM 时,如果 CRC 错误次数连续超过三次,我会把该区块标记为“读取异常”,下次写入时覆盖重写。
4.2 掉电保护与事务日志
MR25H40 的写操作是即时生效的,不需要像 Flash 那样担心“写一半掉电导致块损坏”,因为它没有擦写状态机。但如果你正在写一个多字节数据帧,恰好掉电,MRAM 可能会保留“部分新数据、部分旧数据”的混合状态。所以还要靠事务日志机制来保证场景一致性。
我的做法是先把数据写入临时区,写完以后在日志区写一条“提交记录”,内容包括目标地址、数据长度、CRC。上电启动时先扫描日志区,如果发现一条未完成的提交记录,就执行回滚或重放。这样即使掉电发生在数据写入过程中的任意时刻,恢复后都能判断出当前数据是否可信。
这个逻辑在传统 EEPROM 项目里也能见到,但 MRAM 让写日志本身变得非常快,不用等待擦除,所以事务日志的开销非常小。我用 64K 日志区做了环形队列,每条日志 16 字节,可以记录几千次事务,足够现场常年运行。
4.3 磨损寿命:MRAM 的优势到底有多大
传统 EEPROM 典型擦写寿命是 100 万次,Nor Flash 通常是 10 万次,而且按扇区擦除很麻烦。MR25H40CDF 的写耐久规格是无限次,至少数据手册上写的是 unlimited。实际工程中我按每秒写 10 次来计算,一年约 3 亿次,这对 EEPROM 来说早挂了,但 MRAM 完全没有压力。
正是因为寿命无限,我才敢把运行日志、事件记录、实时采样都直接写到 MRAM,让代码简单粗暴。这不是浪费硬件,工业设备生命周期十年以上,真的应该把“按天压缩写入次数”这类优化从存储驱动的需求中剔除。嵌入式工程师的时间更值钱,少做性能折中就是赚到。
当然,无限次写入不代表无限次 SPI 访问,所以代码里还是尽量用 DMA 和突发写入减少 CPU 占用,把时间留给控制逻辑。MRAM 的字节级随机写能力,让它特别适合记录高频瞬态事件,比如故障前后几十毫秒的波形,这在电机保护装置里非常实用。
5. 实测数据与性能表现
5.1 读写吞吐率测试
我把 SPI 配成 20MHz,用 K22 的定时器测了 1KB 连续读和写的时间。读 1KB 约耗时 0.42ms,写 1KB 约耗时 0.43ms,换算出来大约 2.4MB/s,这个数字和理论值很接近。如果 SPI 跑到 40MHz,理论上可以到 4.8MB/s 左右,但 20MHz 下已经比普通 SPI EEPROM 快一个数量级。
写入没有额外等待时间,这一点和 Flash 差距最大。Nor Flash 即使页写也要几百微秒到几毫秒的编程时间,而 MRAM 写命令刚发完最后一个数据字节,下一次 CS 拉高就完成写入。我们测试循环写单字节一万次,总耗时不到 10ms,而 EEPROM 至少要 200ms 以上。
5.2 示波器和逻辑分析仪上的信号表现
我在开发阶段用逻辑分析仪抓过 READ 命令时序,确认 CS 在发送地址期间保持低电平、MISO 在命令和地址阶段保持三态或输出无效,进入读数据阶段才开始输出有效字节。这个检查和手册时序图完全吻合。
还要留意 CS 拉高后,MISO 是否很快进入高阻。MRAM 在 CS 拉高后释放总线,如果 MISO 拉电阻太大,信号下降沿可能变缓,影响下一个命令。我用的逻辑分析仪采样率 100M,看 20MHz 时钟下的信号边沿基本干净,没有明显回勾,说明 33Ω 串阻和地平面布局起了作用。
5.3 与常用存储的对比表格
| 存储类型 | 写入是否需要擦除 | 典型写入等待 | 耐久性 | 随机写效率 | 适合场景 |
|---|---|---|---|---|---|
| SPI EEPROM | 按页写,部分需擦除 | 5-10ms | 100万次 | 低 | 低频参数保存 |
| Nor Flash | 需要先擦除 | 页写 0.1-1ms | 10万次 | 低 | 固件、批量数据 |
| SPI MRAM | 不需要 | 无 | 无限 | 高 | 高频日志、实时数据 |
这张表是我做选型时整理出来的。EEPROM 胜在便宜,Flash 胜在容量大,MRAM 胜在读得快、写得快、不用管磨损。MR25H40CDF 的优势就是填补传统存储方案的空白地带,不是要完全取代谁。
6. 新坑和老坑:常见问题排查与避坑清单
6.1 上电读不全数据
我遇到过连续读 1024 字节,读到后半段数据全 0xFF,但读小段数据没问题的诡异现象。排查半天发现是 DMA 配置的 buffer 大小不匹配,SPI DMA 传输到 1024 字节时,接收缓冲 FIFO 溢出,导致后续数据丢失。解决方法是把 DMA 中断或 SPI FIFO 水印设置对,或者改用轮询读取长数据。
还有一个常见原因:CS 由 GPIO 控制时,没有在读取前做足够延时切换电平。GPIO 拉低到 SPI 发送第一个字节之间,至少留一个时钟周期的低电平时间。有些库函数先写命令再拉低 CS,命令字节就会发丢。
6.2 写入正确读出错误
写入正确读出错误,第一反应查 SPI 模式。MR25H40 支持 Mode 0 和 Mode 3,如果主设备配置成了 Mode 1 或 Mode 2,读数据时 MISO 的采样时刻会偏一半周期,结果就是 MSB 移位。
第二要查的是 HOLD 引脚。如果 HOLD 浮空,现场干扰可能让它瞬时拉低,导致读操作被暂停而返回无效数据。我一开始就是把这个引脚空着,低温试验时偶尔读错,后来拉高到 VCC,问题再没出现。
6.3 片选控制与 DMA 冲突
DMA 搬运数据写 MRAM 时,如果 DMA 结束回调里把 CS 拉高,但 SPI 发送移位寄存器里还有一个尾巴没发送完,就会截断最后一个字节。这件事特别隐蔽,因为逻辑分析仪上只看到最后一个字节没被正确写进 MRAM。
我的解决方案是 DMA 传输完成后再等待 SPI 空闲标志,再拉高 CS。K22 的 DSPI 有EOQF或 FIFO 空标志,等它置位再操作 CS。现在代码里所有写操作都遵守这个顺序,没有丢字节的困扰。
6.4 排查流程速查表
| 现象 | 可能原因 | 检查顺序 |
|---|---|---|
| 读写全 FF | 片选未拉低,或 SPI 引脚复用不对 | 查 GPIO 配置、量 CS 电平 |
| 读回数据移位 | SPI 模式不匹配 | 查 CTAR 的 CPOL/CPHA |
| 偶发读写失败 | 电源噪声,WP/HOLD 浮空 | 查供电纹波,拉高 WP/HOLD |
| 长数据丢失 | DMA 溢出或 FIFO 水印错误 | 查 DMA 配置和 SPI 状态标志 |
| 写入成功读回旧数 | 写使能未发出或 CS 截断 | 查 WREN 时序,CS 等待空闲 |
这套排查表是我在多个项目里沉淀出来的。每一条都踩过,写下来就是希望后面的兄弟能少折腾一天。
另外想说一个经验:MR25H40CDF 和 MK22FN512VLH12 的组合看起来冷门,但它们的寄存器手册都写得清清楚楚,驱动并不难。真正花力气的地方在现场的电磁干扰、掉电保护和 DMA 调度上。如果你现在正被 EEPROM 的页写延迟折磨,或者被 Flash 的擦除逻辑搞得头大,真心建议试试 MRAM,也许你也会像我一样,用完就再也回不去那些老方案了。最后再分享一个小技巧:在 MRAM 驱动里留一个mram_selftest()函数,上电时随机读写几个地址并做 CRC 校验,能提前暴露大部分硬件问题,比等现场故障再排查划算得多。