1. 为什么工业嵌入式项目里,存储方案的选择比主控选型还让人头疼
做过工业设备的人大概都有这种体会:主控选型其实相对简单,算力、外设、封装、温度等级一列,选型表拉出来基本就定了。真正让人反复纠结、甚至项目做到一半推翻重来的,往往是存储方案。原因也不复杂——工业现场对存储的要求和消费电子完全是两个世界。消费电子里数据丢了顶多用户骂两句,工业设备里数据丢了可能是产线停机、参数错乱、设备损坏,甚至安全事故。
我这些年接触过的工业项目里,存储需求大致分成两类,而且这两类经常同时出现在同一块板子上。一类是频繁写入的关键数据,比如设备运行状态记录、故障前后的快照、计数器、校准系数、掉电前必须保存的现场参数。这类数据的特点是写入频繁、要求写入延迟极低、掉电绝对不能丢,而且寿命要扛得住几年甚至十几年的反复擦写。另一类是大容量、低频写入的配置与固件数据,比如设备配置表、字库、日志归档、固件备份、Web 页面资源。这类数据写入次数少,但容量需求大,成本敏感。
传统方案里,第一类数据一般用 EEPROM 或者带电池的 SRAM,第二类用 SPI NOR Flash 或者 NAND Flash。但这套组合在工业场景下问题不少:EEPROM 写入速度慢、寿命有限、容量小;带电池 SRAM 要维护电池,工业现场温度一高电池就废;NOR Flash 写入前要擦除、擦除块大、写入延迟高,掉电时正在写的块可能整块损坏。于是越来越多项目开始把目光投向MRAM和高可靠性 SPI NOR Flash的组合。
这篇要聊的,就是MR25H40CDF和MKV44F64VLH16这两颗器件在工业和嵌入式应用里的存储与读取实践。前者是一颗 4Mbit 的 SPI MRAM,后者是一颗 64Mbit 的 SPI NOR Flash。一个负责高频、低延迟、掉电安全的关键数据,一个负责大容量、低成本的配置与固件数据。两颗芯片都走 SPI 接口,硬件上可以共用一组 SPI 总线,软件上分片管理,这套组合我在几个工业采集和边缘控制项目里都用过,实测下来稳定性和可维护性都不错。
如果你正在做工业数据采集、边缘控制器、电力终端、医疗设备或者任何对掉电数据完整性有要求的嵌入式项目,这套存储架构值得认真看一下。下面我会从器件特性、硬件设计、SPI 通信、软件分层、掉电保护、实测踩坑几个角度,把整套方案拆开讲清楚。
2. 把 MR25H40CDF 和 MKV44F64VLH16 放在同一块板子上之前,先搞清楚它们各自解决什么问题
2.1 MR25H40CDF:4Mbit SPI MRAM 到底强在哪
MR25H40CDF 是一颗 4Mbit(512KByte)的 SPI 接口 MRAM。MRAM 的全称是磁性随机存储器,它的存储单元用的是磁性隧道结,靠磁化方向存储数据,而不是像 Flash 那样靠电荷。这个物理机制决定了它几个非常关键的特性。
第一,写入不需要擦除。Flash 写之前必须先擦除整个扇区,擦除时间长、功耗高,而且擦除过程中掉电会导致整个扇区数据不可预测。MRAM 是直接覆盖写,写一个字节就是一个字节,没有擦除这一步。这一点在掉电保护场景里价值极大,因为写入窗口短,掉电时正在写的概率低,即使正在写,影响范围也只是一个字节或一页,不会整块丢失。
第二,写入寿命几乎可以忽略。MRAM 的写入耐久度通常在 10^14 到 10^15 次量级,而 NOR Flash 一般只有 10^5 次擦写。这意味着你可以把它当成 SRAM 一样频繁写,不用做磨损均衡,不用算寿命。工业项目里那些每秒写几次的计数器、状态机快照,用 MRAM 完全不用心疼。
第三,写入速度快、延迟低。MR25H40CDF 支持最高 40MHz 的 SPI 时钟,写入没有等待擦除的几十毫秒延迟,单次写入延迟在微秒级。对于掉电瞬间要抢时间保存数据的场景,这个差距是决定性的。
第四,宽温、抗辐射、数据保持时间长。工业级温度范围一般覆盖 -40 到 85 摄氏度,MRAM 在这个范围内数据保持能力稳定,不需要像 Flash 那样担心高温下电荷流失。数据保持时间标称 20 年以上。
当然它也有代价:容量小、单价高。4Mbit 在 Flash 面前不算大,价格却比同容量 Flash 贵不少。所以它的定位很明确——只放最关键、最频繁写、掉电不能丢的数据,不要拿它当大容量存储用。
2.2 MKV44F64VLH16:64Mbit SPI NOR Flash 的定位
MKV44F64VLH16 是一颗 64Mbit(8MByte)的 SPI NOR Flash。NOR Flash 的特点是随机读取快、支持 XIP(就地执行)、接口简单、成本低、可靠性在 Flash 家族里属于比较高的。64Mbit 这个容量在工业嵌入式里非常实用,放配置表、字库、日志、固件备份、Web 资源都够用。
它的写入流程和所有 NOR Flash 一样:写之前要先发写使能命令,然后擦除目标扇区(4KB 扇区擦除或者更大块擦除),擦除完成后才能页编程写入。擦除和编程都需要等待芯片内部完成,通过读状态寄存器判断是否忙。擦除一个 4KB 扇区典型时间在几十毫秒量级,页编程 256 字节典型时间在几百微秒到几毫秒。
它的擦写寿命典型是 10 万次每扇区,所以软件上必须做磨损均衡,尤其是日志类频繁写的区域。数据保持时间一般标称 20 年,但高温下会缩短,工业场景要注意温度降额。
2.3 两颗芯片的分工逻辑
把这两颗放在一起,核心思路是按数据特征分层:
| 数据类别 | 写入频率 | 掉电要求 | 容量需求 | 推荐器件 |
|---|---|---|---|---|
| 运行计数器、状态快照 | 高,每秒数次 | 绝对不能丢 | 小,KB 级 | MR25H40CDF |
| 校准系数、设备参数 | 中,每次校准写 | 绝对不能丢 | 小,KB 级 | MR25H40CDF |
| 故障前后快照 | 低,触发时写 | 绝对不能丢 | 中,几十 KB | MR25H40CDF |
| 配置表、字库 | 低,升级时写 | 可容忍重写 | 大,MB 级 | MKV44F64VLH16 |
| 日志归档 | 中,定期写 | 可容忍少量丢失 | 大,MB 级 | MKV44F64VLH16 |
| 固件备份、Web 资源 | 极低 | 可重新烧录 | 大,MB 级 | MKV44F64VLH16 |
这个分工不是拍脑袋定的,而是根据写入频率、掉电敏感度和容量需求三个维度交叉出来的。关键数据放 MRAM,是因为它写入快、无擦除、寿命长,掉电时能抢在电源跌落前写完。大容量数据放 Flash,是因为它便宜、容量大,写入频率低所以寿命和延迟问题不突出。
注意:不要因为 MRAM 好用就把所有数据都塞进去。4Mbit 看着不小,但如果你把日志也往里写,很快就会满,而且成本会失控。分层存储的核心是让每类数据待在最适合它的介质上。
3. 硬件设计:SPI 总线共享、片选分配和 PCB 布局的实操细节
3.1 共用一组 SPI 总线的可行性分析
MR25H40CDF 和 MKV44F64VLH16 都是标准 SPI 从设备,支持 Mode 0 和 Mode 3。理论上它们可以挂在同一组 SPI 总线上,用不同的片选信号区分。这样做的好处是节省主控的 SPI 外设资源,尤其是当主控 SPI 接口数量有限时。
但共用总线有几个前提条件必须满足。第一,两颗芯片的 SPI 模式要一致。MR25H40CDF 支持 Mode 0 和 Mode 3,MKV44F64VLH16 也支持 Mode 0 和 Mode 3,所以统一用 Mode 0 或者 Mode 3 都可以。我一般选 Mode 0,因为它在示波器上看起来最直观,时钟空闲低电平,第一个边沿采样。
第二,时钟频率要取两者都能接受的上限。MR25H40CDF 最高 40MHz,MKV44F64VLH16 在标准读命令下一般能到 50MHz 以上,快速读命令能到 80MHz 甚至更高。如果共用总线,时钟频率要按较低的那个来,也就是 40MHz。实际项目里我一般跑 20 到 30MHz,留足余量,因为工业板子的走线长度和干扰情况比实验室复杂。
第三,片选信号必须严格互斥。同一时刻只能有一个片选拉低,否则两颗芯片会同时驱动 MISO 线,造成总线冲突。这一点在软件初始化阶段就要保证,上电时所有片选都拉高,初始化完成后再按需拉低。
3.2 片选信号的硬件与软件选择
SPI 片选有两种做法:硬件片选和软件片选。硬件片选是用主控的 SPI 外设自动控制 CS 引脚,软件片选是用普通 GPIO 手动拉低拉高。
在共用总线的场景下,我强烈建议用软件片选。原因是硬件片选通常和 SPI 外设绑定,一个 SPI 外设只有一个硬件 CS,没法同时控制两个从设备。用 GPIO 做片选,虽然每次传输前后要多两条 GPIO 操作指令,但灵活性高,而且片选时序可以精确控制。
具体做法是:把 MRAM 的 CS 接到一个 GPIO,把 Flash 的 CS 接到另一个 GPIO,两个 GPIO 都配置为推挽输出,初始状态拉高。每次访问前拉低对应片选,访问完成后拉高。拉高和拉低之间要保证足够的建立时间和保持时间,一般几百纳秒就够,具体看器件手册。
提示:软件片选的一个常见坑是片选拉高后立刻发起下一次传输,导致前一个器件的 CS 还没完全拉高,后一个器件的 CS 已经拉低,出现短暂的总线重叠。解决办法是在片选切换之间插入几个空操作或者微秒级延时,确保总线空闲。
3.3 PCB 布局与信号完整性
SPI 在低速下对布局不敏感,但跑到 20MHz 以上,尤其是工业板子走线较长时,信号完整性就要认真对待了。
走线长度:SCK、MOSI、MISO、CS 尽量等长,尤其是 SCK 和 MISO。如果两颗芯片离主控距离差异大,建议把两颗芯片放在主控同一侧,走线尽量短。我见过一个项目把 Flash 放在板子另一头,走线 8 厘米,20MHz 下 MISO 波形已经明显振铃,读数据偶发错误。
串联电阻:在 SCK 和 MOSI 靠近主控端串 22 到 33 欧姆电阻,可以抑制过冲和振铃。MISO 一般不需要串,因为它是从设备驱动,但如果是长走线也可以考虑。
上拉电阻:CS 信号建议加上拉电阻,典型 10K,保证上电和复位期间片选稳定为高,避免误触发。MISO 在某些主控上需要上拉,具体看主控手册。
电源去耦:两颗芯片的 VCC 引脚旁边各放一个 0.1uF 陶瓷电容,尽量靠近引脚。MRAM 和 Flash 在写入瞬间电流会有波动,去耦不好会导致写入失败。如果板子上还有其他高速器件,建议在电源入口再加一个 10uF 钽电容。
地平面:SPI 信号下方要有完整地平面,不要跨分割。工业板子如果有模拟区域,SPI 走线要远离模拟信号,避免数字噪声耦合。
3.4 电源与掉电检测的配合
MRAM 的价值在掉电瞬间才能体现,所以硬件上必须配合掉电检测电路。常见做法是用一个电压监测芯片或者主控的 ADC 监测电源电压,当电压跌落到某个阈值时触发中断,中断服务程序里立刻把关键数据写入 MRAM。
这里有个关键参数:从掉电检测触发到电源完全跌落到 MRAM 无法工作的电压,中间有多少时间。这个时间由板子上的储能电容决定。假设板子工作电流 100mA,MRAM 最低工作电压 2.7V,电源正常 3.3V,储能电容 100uF,那么可用时间大约是 C × ΔV / I = 100uF × 0.6V / 100mA = 600 微秒。600 微秒足够写几十个字节到 MRAM,但前提是中断响应要快,写入代码要精简。
如果储能电容不够大,可以加大电容或者降低掉电检测阈值。但加大电容会增加上电冲击电流和板子体积,需要权衡。我的经验是,关键数据量控制在 256 字节以内,储能电容给到 220uF 以上,基本能保证可靠写入。
4. SPI 通信层:命令时序、读写流程和两颗芯片的差异处理
4.1 MR25H40CDF 的读写命令与时序
MR25H40CDF 的命令集比较简洁,常用的就几条:
- WREN(0x06):写使能,写入或擦除前必须发。
- WRDI(0x04):写禁止。
- RDSR(0x05):读状态寄存器。
- WRSR(0x01):写状态寄存器。
- READ(0x03):读数据,后跟 24 位地址,然后连续读出。
- WRITE(0x02):写数据,后跟 24 位地址,然后连续写入。
- FAST READ(0x0B):快速读,后跟 24 位地址和一个 dummy 字节。
写流程是:拉低 CS,发 WREN,拉高 CS,再拉低 CS,发 WRITE 命令和地址,然后连续写数据,最后拉高 CS。注意 WREN 和 WRITE 之间要拉高一次 CS,这是很多 SPI 器件的通用要求,目的是让 WREN 命令生效。
MRAM 的写入没有忙等待,写完命令拉高 CS 后数据就已经落盘,不需要轮询状态寄存器。这一点和 Flash 完全不同,也是它写入延迟低的直接体现。
读流程更简单:拉低 CS,发 READ 命令和 24 位地址,然后连续读出数据,拉高 CS。地址会自动递增,可以一次读完整个区域。
4.2 MKV44F64VLH16 的擦除、编程与忙等待
MKV44F64VLH16 的命令集和标准 NOR Flash 一致:
- WREN(0x06):写使能。
- RDSR(0x05):读状态寄存器,bit0 是 WIP(Write In Progress)。
- READ(0x03):读数据。
- FAST READ(0x0B):快速读。
- SE(0x20):4KB 扇区擦除。
- BE(0xD8):64KB 块擦除。
- CE(0xC7):整片擦除。
- PP(0x02):页编程,一页 256 字节。
写流程比 MRAM 复杂得多:拉低 CS,发 WREN,拉高 CS;拉低 CS,发 SE 命令和地址,拉高 CS;然后轮询 RDSR 直到 WIP 为 0,擦除完成;再发 WREN,再发 PP 命令和地址,写数据,拉高 CS;再轮询 RDSR 直到 WIP 为 0,编程完成。
这里最容易出错的是忘记轮询 WIP。擦除和编程都需要时间,如果在 WIP 为 1 的时候发下一条命令,芯片会忽略这条命令,导致操作失败。我见过不少新手代码里擦除后直接写,结果数据写不进去,查半天以为是硬件问题。
另一个坑是页编程不能跨页。一页 256 字节,如果写入起始地址不是页对齐,写到页边界就会回卷到页首,覆盖前面的数据。所以写之前要算好地址,跨页时分多次写。
4.3 两颗芯片共用总线时的模式切换与片选管理
共用总线时,每次访问前要明确当前操作的是哪颗芯片,拉低对应的片选。由于两颗芯片的 SPI 模式一致,不需要切换模式,但要注意片选切换之间要留总线空闲时间。
我的做法是封装两个底层函数:mram_cs_low()、mram_cs_high()、flash_cs_low()、flash_cs_high(),所有访问都通过这两个函数控制片选。在片选切换时,先拉高当前片选,插入几个 NOP 或者一个微秒延时,再拉低目标片选。这样能避免总线冲突。
还有一个细节:MISO 线的状态。当没有片选拉低时,MISO 处于高阻态,主控读到的值不确定。所以软件上不要在片选都拉高的时候去读 MISO,没有意义。每次读操作必须在片选拉低后进行。
4.4 用逻辑分析仪抓 SPI 波形的实操经验
调试 SPI 最有效的工具是逻辑分析仪。我一般用 8 通道以上的分析仪,接 SCK、MOSI、MISO、两颗芯片的 CS,再加一个掉电检测信号。抓波形时重点看几个地方:
- CS 拉低到第一个 SCK 边沿的建立时间是否足够。
- SCK 空闲电平和采样边沿是否符合 Mode 0。
- MISO 在 SCK 采样边沿是否稳定。
- 擦除和编程期间 WIP 轮询的波形是否正常。
- 掉电瞬间 CS 和 SCK 是否干净,有没有毛刺。
有一次我遇到 MRAM 偶发写入失败,逻辑分析仪抓下来发现是 CS 拉低后 SCK 第一个边沿来得太快,建立时间只有几纳秒,低于手册要求。后来在 CS 拉低后加了一个微秒延时,问题消失。这种问题用示波器很难抓到,逻辑分析仪一抓一个准。
5. 软件分层:从底层驱动到数据管理中间件的设计
5.1 底层驱动层:统一 SPI 抽象与器件差异封装
底层驱动层要做的事情是把两颗芯片的差异封装起来,对上提供统一的读写接口。我一般定义两个结构体,分别描述 MRAM 和 Flash 的设备信息,包括片选引脚、容量、页大小、扇区大小等。然后实现两组底层函数:
typedef struct { uint8_t cs_pin; uint32_t capacity; uint16_t page_size; uint32_t sector_size; } storage_dev_t; int mram_read(uint32_t addr, uint8_t *buf, uint32_t len); int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len); int flash_read(uint32_t addr, uint8_t *buf, uint32_t len); int flash_write(uint32_t addr, const uint8_t *buf, uint32_t len); int flash_erase_sector(uint32_t addr);MRAM 的写函数直接发 WRITE 命令写就行,不需要擦除。Flash 的写函数要先擦除目标扇区,再按页写入,跨页要拆分。这些差异都在底层处理掉,上层不用关心。
5.2 中间件层:键值存储与磨损均衡
直接在裸地址上读写数据,项目一大就乱。我一般会在底层之上做一个简单的键值存储中间件,把数据按 key 管理,每个 key 对应一段固定区域。MRAM 区域因为寿命无限,不需要磨损均衡,直接原地更新。Flash 区域需要磨损均衡,尤其是日志区。
磨损均衡的做法有很多种,工业项目里我一般用日志式追加写 + 定期整理的简单方案。每个扇区按顺序写,写满后跳到下一个扇区,所有扇区写满后把有效数据整理到第一个扇区,擦除其余扇区。这样每个扇区的擦写次数大致均匀,寿命能撑很久。
键值存储的元数据也要管理。我一般把 key 和地址的映射表放在 MRAM 里,因为映射表更新频繁,放 Flash 里擦写次数扛不住。映射表本身很小,几百字节就够。
5.3 掉电保护:中断响应、写入窗口与数据一致性
掉电保护是整套方案的核心价值。流程是这样的:掉电检测中断触发后,中断服务程序立刻把关键数据打包,调用 MRAM 写函数写入。由于 MRAM 写入快、无擦除,几百字节在几百微秒内能写完。
但这里有几个细节要注意。第一,中断优先级要最高,不能被其他中断打断。第二,写入代码要放在 RAM 里执行,因为掉电时 Flash 可能已经无法读取,如果中断服务程序还在 Flash 里取指,可能取不到指令。第三,写入的数据要预先组织好,中断里只做拷贝,不做复杂计算。
数据一致性方面,我一般用双备份 + 校验的方式。关键数据存两份,每份带 CRC 校验。写入时先写备份区,再写主区,读取时如果主区 CRC 错误就读备份区。这样即使写入过程中掉电,至少有一份是完整的。
5.4 上电初始化与自检流程
上电后要做几件事:初始化 SPI 外设和片选 GPIO,读取 MRAM 里的元数据和映射表,校验关键数据 CRC,检查 Flash 的 ID 和状态。如果 CRC 错误,从备份区恢复。如果 Flash ID 读不到,说明硬件有问题,进入故障状态。
自检流程我一般做成一个独立函数,返回一个状态码,主程序根据状态码决定是否继续启动。工业设备里,存储自检不通过就不应该进入正常运行,否则可能带着错误参数运行,后果更严重。
6. 实测踩坑:那些手册上不会写但项目里一定会遇到的事
6.1 MRAM 写入偶发失败的排查过程
前面提到过一次 MRAM 偶发写入失败,排查过程值得完整讲一遍。现象是设备运行几天后,偶尔出现关键数据丢失,重启后数据恢复默认值。用逻辑分析仪长时间抓波形,发现失败时 CS 拉低到 SCK 第一个边沿的建立时间只有几纳秒,而手册要求至少 5 纳秒。正常情况下有几十纳秒,为什么偶尔会变短?
查代码发现,片选拉低和 SPI 传输之间没有插入延时,编译器优化后两条指令挨得太近。在某些中断嵌套的情况下,片选拉低后被打断,中断返回后 SCK 立刻开始,导致建立时间不足。解决办法是在片选拉低后插入一个__NOP()或者一个微秒延时,保证建立时间。改完后连续跑了一个月没再出现。
这个坑的教训是:SPI 时序不能只看典型值,要留足余量,尤其是片选和时钟的配合。
6.2 Flash 擦除期间掉电导致的数据损坏
另一个项目里,Flash 日志区在擦除过程中掉电,重启后整个扇区数据变成 0xFF,之前写的日志全丢了。虽然日志可以容忍少量丢失,但整扇区丢失还是影响了故障分析。
原因是 NOR Flash 擦除是整扇区操作,擦除过程中掉电,扇区状态不确定。解决办法是日志双区交替写:两个扇区轮流用,写满一个再写另一个,擦除旧扇区前先确认新扇区数据完整。这样即使擦除掉电,另一个扇区还有完整数据。
这个方案增加了 Flash 空间开销,但工业项目里可靠性优先,多用一个扇区值得。
6.3 SPI 时钟频率与信号完整性的权衡
有个项目为了追求读取速度,把 SPI 时钟拉到 40MHz,结果 Flash 读数据偶发错误。用示波器看波形,SCK 过冲严重,MISO 振铃明显。板子走线 6 厘米,没有串阻,地平面也不完整。
后来降到 20MHz,加串阻,改地平面,问题解决。速度是降了,但读取 8MByte 数据也就多花几百毫秒,对系统整体性能影响很小。工业项目里,稳定性永远优先于极限性能。
6.4 宽温测试中的意外发现
工业级器件标称 -40 到 85 摄氏度,但实际测试中我发现,MRAM 在低温下写入电流会增大,如果电源去耦不足,写入失败率会上升。高温下 Flash 擦除时间会变长,WIP 轮询超时要放宽。
所以宽温测试不能只测常温,要在高低温箱里跑完整读写流程,尤其是掉电保护流程。我一般会在 -40、25、85 三个温度点各跑 1000 次掉电写入,统计成功率。只有三个温度点都 100% 成功,才认为方案可靠。
7. 几个容易被忽略的工程细节和我的个人建议
7.1 地址映射要留扩展空间
MRAM 只有 512KByte,Flash 有 8MByte。做地址映射时不要把所有空间都分满,留 20% 到 30% 的余量。项目后期加功能、加日志、加备份,都需要空间。我见过项目做到一半发现 MRAM 满了,只能重新规划,改动很大。
7.2 版本兼容与数据迁移
固件升级时,存储区的数据格式可能变化。我一般会在 MRAM 里放一个数据版本号,升级后如果版本号不匹配,执行数据迁移或者恢复默认值。迁移代码要简单可靠,不要做复杂转换,避免迁移过程中掉电导致数据损坏。
7.3 调试接口要保留
量产板子上也要保留 SPI 测试点,方便售后排查。我一般留 SCK、MOSI、MISO、两颗芯片的 CS 共 6 个测试点,间距 2.54mm,普通探针能戳。有了测试点,现场问题用逻辑分析仪一抓就能定位,不用拆机。
7.4 我的个人经验总结
这套 MRAM + Flash 的组合,我在工业采集、边缘控制、电力终端几类项目里都用过,整体稳定性很好。核心经验就几条:关键数据放 MRAM,大容量数据放 Flash;SPI 共用总线要软件片选、留总线空闲;掉电保护要中断快、代码在 RAM、数据双备份;宽温测试要覆盖高低温;地址映射留余量。
成本上,MRAM 确实比 Flash 贵,但换来的是掉电数据不丢、写入寿命无限、延迟极低。工业设备里,一次数据丢失造成的损失,往往远超几颗 MRAM 的成本。这笔账,做过现场的人心里都清楚。
最后再分享一个小技巧:MRAM 和 Flash 的驱动代码尽量做成可配置的,片选引脚、容量、页大小都从配置文件读。这样换主控或者换器件时,只改配置不改代码,移植成本低很多。我在几个项目之间复用这套驱动,基本半天就能跑通新板子。