1. 为什么在工业场景里我会优先考虑 MRAM 而不是 Flash
1.1 从一次数据丢失事故说起
几年前做过一个工业数据采集终端,主控用的是 MK20DX128VFM5,外部存储选了一颗常见的 SPI NOR Flash。设备在现场跑了大概三个月,陆续出现"参数丢失"的投诉——用户设置好的量程、校准系数,断电重启后偶尔会回到默认值。一开始怀疑是电源跌落导致写入中断,加了掉电检测、加了超级电容,问题依旧偶发。
后来用逻辑分析仪长时间抓 SPI 总线才定位到:Flash 的扇区擦除需要几十毫秒,在这段时间里如果主控因为看门狗复位或者电源抖动中断了写流程,整个扇区就处于"擦了一半"的中间态,数据自然就废了。这个坑让我开始认真评估 MRAM 这类"非易失、无擦除、按字节写"的存储介质,MR25H40CDF 就是在这个背景下进入选型清单的。
1.2 MR25H40CDF 到底是个什么东西
先把这颗芯片的定位说清楚。MR25H40CDF 是 Everspin 出的 4Mbit(512K × 8)串行 MRAM,接口是标准 SPI,工作电压 2.7V~3.6V,工业级温度范围 -40℃~+85℃。它的核心卖点是磁阻存储:数据靠磁性隧道结的磁化方向保存,不需要电荷,所以天生具备几个 Flash 和 EEPROM 给不了的性质。
- 写入不需要擦除:Flash 写之前必须先擦整个扇区,MRAM 直接覆盖写,字节级操作。
- 写入速度极快:没有擦除等待,单次写周期在几十纳秒量级,SPI 总线上基本就是传输时间。
- 近乎无限的耐久度:官方标称 10^14 次写入,Flash 通常只有 10^5 次,EEPROM 也就 10^6 次。
- 掉电即存:写命令发完数据就落地了,不存在"写缓存还没刷"的窗口期。
这四点里,对工业应用最致命的是第一条和第四条。工业现场最怕的就是"写一半断电",MRAM 从机制上就把这个风险消掉了。
1.3 MK20DX128VFM5 为什么适合配它
MK20DX128VFM5 是 NXP Kinetis K20 系列里的 32 位 ARM Cortex-M4 芯片,128KB Flash、16KB RAM,封装是 32 引脚 QFN,主频 50MHz。它自带多个 SPI 模块(DSPI),硬件片选、FIFO、DMA 支持都齐全。选它配 MR25H40CDF 有几个现实理由:
第一,K20 的 DSPI 支持最高 50MHz 的时钟,而 MR25H40CDF 的 SPI 时钟上限是 40MHz,两者匹配得很好,不会出现主控太快、从机跟不上的尴尬。第二,K20 的工业级型号温度范围和 MRAM 一致,整机做 -40℃~+85℃ 认证时不用为存储单独降额。第三,K20 的引脚少、外设够用,做紧凑型工业模块时布板压力小。
提示:MR25H40CDF 的 SPI 模式是 Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)都支持,但整条总线上的其他从机如果只支持其中一种,建议统一到 Mode 0,避免切换模式时出现时序毛刺。
1.4 这套组合能解决哪些实际问题
把这两颗芯片放一起,典型能落地的场景有这么几类:工业设备的参数与校准数据存储(频繁改写、掉电不能丢)、运行日志与故障记录(写入频繁,Flash 寿命扛不住)、实时数据缓冲(采集速率高,需要快速落盘)、配方与工艺参数切换(需要快速读写、反复覆盖)。
这些场景的共同特征是:写入频繁、对掉电敏感、数据量不大(512KB 足够)、要求长期可靠。如果你的项目正好命中其中两三条,那这套方案值得认真考虑;如果只是存个固件、一年写不了几次,那普通 Flash 更便宜,没必要上 MRAM。
2. 硬件连接与 SPI 时序里那些容易翻车的细节
2.1 引脚连接不是照着数据手册抄就完事
MR25H40CDF 是 8 引脚封装,关键引脚是 CS(片选)、SCK(时钟)、SI(数据入)、SO(数据出)、VDD、VSS,另外还有 WP(写保护)和 HOLD(保持)。很多人画原理图时把 WP 和 HOLD 直接悬空,结果调试时发现写不进去或者读出来全是 0xFF,排查半天。
正确的做法是:WP 和 HOLD 都要上拉到 VDD。这两个引脚是低有效,悬空时电平不确定,芯片可能进入写保护或者保持状态。上拉电阻用 10kΩ 就够,别用太小的阻值增加功耗。
和 MK20DX128VFM5 的连接对应关系是这样的:
| MR25H40CDF 引脚 | MK20DX128VFM5 引脚 | 说明 |
|---|---|---|
| CS | PTC4 / DSPI0_PCS0 | 硬件片选,也可用 GPIO 软件控制 |
| SCK | PTC5 / DSPI0_SCK | 时钟 |
| SI | PTC6 / DSPI0_SOUT | 主出从入 |
| SO | PTC7 / DSPI0_SIN | 主入从出 |
| WP | 上拉到 3.3V | 低有效写保护 |
| HOLD | 上拉到 3.3V | 低有效保持 |
2.2 硬件片选和软件片选怎么选
这是 SPI 调试里绕不开的一个话题。K20 的 DSPI 支持硬件片选(PCS),也支持把片选当普通 GPIO 用软件拉。两种方式各有适用场景。
硬件片选的好处是时序由外设自动控制,片选和时钟的建立/保持时间由硬件保证,高速通信时不容易出问题。缺点是片选引脚固定,多从机时要用到 PCS 的译码逻辑,灵活性差一些。
软件片选的好处是任意 GPIO 都能当片选,多从机时想怎么接就怎么接,调试时还能手动拉高拉低观察波形。缺点是片选和时钟的时序要靠代码保证,如果中断打断或者编译器优化,可能出现片选提前拉高、最后一个字节没发完的情况。
我的经验是:单从机、高速率用硬件片选;多从机、或者需要精细控制片选时序(比如某些从机要求片选在字节间保持)用软件片选。这套 MRAM 方案里,如果总线上只有它一颗,直接用 DSPI0_PCS0 硬件片选最省心。
2.3 SPI 模式与时钟极性的确认
MR25H40CDF 支持 Mode 0 和 Mode 3。Mode 0 是时钟空闲为低、数据在上升沿采样;Mode 3 是时钟空闲为高、数据在上升沿采样。两者采样沿相同,区别在空闲电平。
配置 K20 的 DSPI 时,对应寄存器是 CTARn 里的 CPOL 和 CPHA 位。Mode 0 就是 CPOL=0、CPHA=0。这里有个容易忽略的点:K20 的 DSPI 在修改 CTAR 寄存器前必须先停止传输,否则新配置不生效。很多人改了模式发现没变化,就是忘了先清 HALT 位或者等传输完成。
时钟频率方面,MR25H40CDF 最高 40MHz,但实际能跑多快取决于 PCB 走线质量、从机到主控的距离、以及总线上的容性负载。我一般先用 1MHz 调通功能,再逐步往上加,用示波器看 SCK 上升沿有没有过冲、SO 数据眼图是否张开。工业板子上走线长、干扰大,跑到 20MHz 就很稳了,没必要硬追 40MHz。
2.4 PCB 布局的几条硬规矩
SPI 虽然叫"低速总线",但在 20MHz 以上时,走线就是传输线了。几条我踩过坑总结的规矩:
- SCK 走线尽量短且远离其他信号,尤其是模拟信号和复位线。SCK 是持续翻转的,串扰能力很强。
- SO 和 SI 不要长距离平行走,平行走线会耦合,读数据时容易出错。如果必须平行,中间加地线隔离。
- 去耦电容紧贴 VDD 引脚,0.1μF 加 1μF 组合,别放在板子另一头。
- CS 走线不要和 SCK 平行太长,片选边沿和时钟边沿耦合会导致误触发。
注意:MRAM 对电源纹波比 Flash 敏感一些,因为写操作涉及磁性翻转,电流脉冲比较陡。VDD 上如果纹波超过 100mV,写入失败率会上升。建议在 VDD 就近加一个 10μF 钽电容兜底。
3. 驱动层实现:从寄存器操作到可复用的读写函数
3.1 先搞清楚 MR25H40CDF 的指令集
MRAM 的指令集比 Flash 简单得多,因为它不需要擦除。核心指令就几条:
| 指令 | 编码 | 作用 |
|---|---|---|
| WREN | 0x06 | 写使能,写之前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
注意这里没有扇区擦除、没有页编程,WRITE 指令直接跟地址和数据就能写。状态寄存器的 bit0 是 WEL(写使能锁存),bit1 是 BP0、bit2 是 BP1,用于块保护。
3.2 初始化 DSPI 的完整流程
在 MK20DX128VFM5 上初始化 DSPI0,按下面顺序来,每一步都有它的道理:
void mram_spi_init(void) { /* 1. 使能时钟:DSPI0 和 PORTC */ SIM_SCGC6 |= SIM_SCGC6_SPI0_MASK; SIM_SCGC5 |= SIM_SCGC5_PORTC_MASK; /* 2. 配置引脚复用:PTC4~PTC7 为 DSPI0 功能 */ PORTC_PCR4 = PORT_PCR_MUX(2) | PORT_PCR_DSE_MASK; PORTC_PCR5 = PORT_PCR_MUX(2) | PORT_PCR_DSE_MASK; PORTC_PCR6 = PORT_PCR_MUX(2) | PORT_PCR_DSE_MASK; PORTC_PCR7 = PORT_PCR_MUX(2); /* 3. 复位 DSPI:先软复位再释放 */ SPI0_MCR = SPI_MCR_MDIS_MASK; SPI0_MCR = SPI_MCR_MSTR_MASK | SPI_MCR_DIS_TXF_MASK | SPI_MCR_DIS_RXF_MASK | SPI_MCR_CLR_TXF_MASK | SPI_MCR_CLR_RXF_MASK; /* 4. 配置 CTAR0:Mode 0,8 位,时钟分频 */ /* 总线 50MHz,分频 4 得 12.5MHz,保守起步 */ SPI0_CTAR0 = SPI_CTAR_FMSZ(7) | SPI_CTAR_CPOL_MASK_OFF | SPI_CTAR_CPHA_MASK_OFF | SPI_CTAR_BR(3); /* 5. 使能 DSPI */ SPI0_MCR &= ~SPI_MCR_MDIS_MASK; }第 3 步的软复位很多人会跳过,但如果你是从 bootloader 跳转到应用、或者之前 DSPI 被别的代码用过,不复位可能残留旧配置。第 4 步的 BR 字段是波特率分频,具体值要查参考手册的 CTAR 表,别凭感觉填。
3.3 单字节收发的地基函数
所有读写都建立在一个字节交换函数上。DSPI 的收发是同一个动作:往 PUSHR 写数据,数据移出的同时 SO 上的数据移入 POPR。
static uint8_t spi_transfer_byte(uint8_t tx) { /* 等发送 FIFO 有空位 */ while (!(SPI0_SR & SPI_SR_TFFF_MASK)); SPI0_PUSHR = SPI_PUSHR_TXDATA(tx) | SPI_PUSHR_CTAS(0); /* 等接收完成 */ while (!(SPI0_SR & SPI_SR_RFDF_MASK)); return (uint8_t)(SPI0_POPR & 0xFF); }这里用轮询方式,简单可靠。如果数据量大、CPU 还要干别的,可以改成中断或 DMA,但 MRAM 读写本来就快,轮询的开销可以接受。
3.4 读操作的实现与地址对齐
读操作很简单:拉低 CS,发 0x03,发 3 字节地址(24 位地址,因为 512KB 需要 19 位,用 3 字节覆盖),然后连续读数据。
void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; CS_LOW(); spi_transfer_byte(0x03); spi_transfer_byte((addr >> 16) & 0xFF); spi_transfer_byte((addr >> 8) & 0xFF); spi_transfer_byte(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = spi_transfer_byte(0x00); } CS_HIGH(); }地址是 24 位,但实际有效地址只有 19 位(0x00000~0x7FFFF),高位发 0 就行。这里有个细节:MRAM 支持连续读,地址会自动递增,跨页不会回绕,所以一次读多长都行,不用像 Flash 那样考虑页边界。
3.5 写操作的实现与写使能
写操作比读多一步:每次写之前要发 WREN。而且 WREN 之后如果 CS 拉高,写使能就失效了,所以 WREN 和 WRITE 必须在同一次 CS 拉低期间完成。
void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; CS_LOW(); spi_transfer_byte(0x06); /* WREN */ CS_HIGH(); /* 有些实现要求 WREN 单独一次 CS */ CS_LOW(); spi_transfer_byte(0x02); /* WRITE */ spi_transfer_byte((addr >> 16) & 0xFF); spi_transfer_byte((addr >> 8) & 0xFF); spi_transfer_byte(addr & 0xFF); for (i = 0; i < len; i++) { spi_transfer_byte(buf[i]); } CS_HIGH(); }关于 WREN 要不要单独一次 CS,不同厂家的 MRAM 要求略有差异。稳妥的做法是先单独发一次 WREN 并拉高 CS,再重新拉低发 WRITE,这样兼容性最好。写完可以读状态寄存器确认 WEL 位是否清零,确认写完成。
3.6 写完成判断与状态轮询
MRAM 的写是"发完即完成",不像 Flash 需要轮询忙状态。但为了保险,写完可以读一次状态寄存器,确认 WEL 已经自动清零。如果 WEL 还是 1,说明 WREN 没生效或者写被拒绝了。
uint8_t mram_read_status(void) { uint8_t sr; CS_LOW(); spi_transfer_byte(0x05); sr = spi_transfer_byte(0x00); CS_HIGH(); return sr; }状态寄存器 bit0 是 WEL,bit1~bit2 是块保护位。如果发现写不进去,先读状态寄存器看 WEL 是否为 1,再看块保护位是不是把目标区域锁了。
4. 实测中暴露的问题与排查链路
4.1 读出来全是 0xFF 的三种可能
调试初期最常见的问题就是读出来全是 0xFF。这个现象背后可能是三种原因,要按顺序排查。
第一种:SPI 根本没通。用示波器看 SCK 有没有波形,CS 有没有拉低。如果 SCK 没波形,检查 DSPI 初始化、引脚复用、时钟使能。如果 CS 没拉低,检查片选控制代码。
第二种:模式不对。如果 SCK 有波形但数据不对,把 CPOL 和 CPHA 换一种组合试试。Mode 0 和 Mode 3 采样沿相同,但空闲电平不同,某些从机对空闲电平敏感。
第三种:WP 或 HOLD 被拉低。这两个引脚低有效,如果被误拉低,芯片不响应。用万用表量一下这两个引脚的电平,正常应该是 VDD。
我遇到过一次特别隐蔽的:WP 引脚焊盘虚焊,时通时断,导致写操作偶尔失败。这种问题示波器看不出来,得用万用表逐个引脚量通断。
4.2 写入后读回数据错位的排查
有一次写完数据读回来,发现整体偏移了一个字节。查了半天发现是 WREN 和 WRITE 之间的 CS 处理有问题:WREN 发完没拉高 CS 就直接发 WRITE,导致从机把 WRITE 指令当成了 WREN 的参数。
这个坑的教训是:严格按数据手册的时序图来,CS 的拉高拉低不能省。MRAM 的指令边界靠 CS 区分,CS 不拉高,从机不知道上一条指令结束了。
4.3 高速时钟下的数据错误
把时钟从 1MHz 提到 20MHz 后,读数据开始出现偶发错误。用示波器看 SO 信号,发现上升沿有明显的振铃,眼图闭合。这是典型的信号完整性问题。
解决办法有三个:一是降低时钟到 10MHz,错误消失;二是在 SO 线上串一个 22Ω 电阻,抑制振铃;三是缩短走线、增加地平面。实际项目里我选了串阻加降频的组合,跑到 15MHz 稳定。
提示:SPI 速率不是越高越好。工业环境下,EMC 测试时高速 SPI 往往是辐射发射的贡献大户。如果过不了 EMC,先降 SPI 时钟试试,比改板子成本低得多。
4.4 掉电测试怎么做才靠谱
验证 MRAM 掉电可靠性的方法:写一批数据,然后在写入过程中随机断电,上电后读回校验。重复几百次,看有没有数据损坏。
我用一个可编程电源做这个测试,设置成随机时间切断输出。跑了 500 次,MRAM 没有一次数据损坏,而之前用 Flash 的方案大概每几十次就有一次扇区损坏。这个对比很能说明问题。
测试时要注意:断电要断在写操作进行中,如果等写完了再断,测不出问题。可以用 GPIO 触发电源切断,和写操作同步。
5. 数据组织与上层应用的设计取舍
5.1 512KB 怎么划分才合理
512KB 看着不大,但工业应用里存参数、日志、配方绰绰有余。我一般这样划分:
| 区域 | 地址范围 | 用途 | 写入频率 |
|---|---|---|---|
| 系统参数区 | 0x00000~0x00FFF | 设备序列号、出厂参数 | 极低 |
| 用户参数区 | 0x01000~0x01FFF | 用户设置、校准系数 | 低 |
| 配方区 | 0x02000~0x0FFFF | 多套工艺配方 | 中 |
| 日志区 | 0x10000~0x6FFFF | 运行日志、故障记录 | 高 |
| 预留区 | 0x70000~0x7FFFF | 扩展用 | - |
日志区占大头,因为日志是循环写的,写入最频繁。MRAM 的耐久度扛得住,但也要做磨损均衡,避免总写同一个地址。
5.2 参数存储要不要加校验
要加。MRAM 虽然可靠,但传输过程可能出错,电源异常也可能导致写了一半。我一般给每个参数块加 CRC16 校验,读的时候校验失败就用默认值,并记录一次故障。
typedef struct { uint16_t magic; /* 0x5A5A 标识有效 */ uint16_t crc; /* 数据区 CRC16 */ uint32_t version; /* 版本号,用于升级兼容 */ uint8_t data[64]; /* 实际参数 */ } param_block_t;magic 字段用来判断这块区域是否被初始化过。新板子第一次上电,magic 不是 0x5A5A,就加载默认参数并写入。version 字段在固件升级、参数结构变化时很有用,可以做迁移。
5.3 日志的循环写入与磨损均衡
日志区用环形缓冲:写指针走到区域末尾就回到开头。但这样会导致开头区域被反复擦写(虽然 MRAM 不怕,但为了数据保留还是均衡一下)。
我的做法是分块轮转:把日志区切成 N 个块,每次写当前块,写满后跳到下一块,N 个块轮一圈再回到第一块。这样每个块的写入次数均匀,也方便按块做时间索引。
#define LOG_BLOCK_SIZE 4096 #define LOG_BLOCK_COUNT 96 static uint32_t cur_block = 0; static uint32_t cur_offset = 0; void log_write(const uint8_t *entry, uint32_t len) { if (cur_offset + len > LOG_BLOCK_SIZE) { cur_block = (cur_block + 1) % LOG_BLOCK_COUNT; cur_offset = 0; } uint32_t addr = LOG_BASE + cur_block * LOG_BLOCK_SIZE + cur_offset; mram_write(addr, entry, len); cur_offset += len; }5.4 和文件系统比,裸写到底值不值
有人会问:为什么不直接上 LittleFS 或者 SPIFFS?我的看法是,数据量小、结构固定、追求极致可靠时,裸写更可控。文件系统带来便利的同时也带来开销和不确定性:元数据可能损坏、挂载可能失败、磨损均衡策略不一定适合你的写入模式。
MRAM 的场景往往是"存几十个参数、几千条日志",这种规模用裸写加 CRC 校验,代码量小、行为可预测、出问题好排查。如果数据量大到需要目录结构、需要动态文件,那再考虑文件系统。
6. 几个提升可靠性的工程习惯
6.1 双备份参数区
关键参数存两份,地址错开。读的时候两份都读,CRC 都过就比对,不一致以主份为准并修复备份;主份坏了用备份恢复。这个策略在 Flash 上很常见,MRAM 上同样值得做,成本只是多占几 KB。
6.2 写入前后加"事务标记"
对于需要原子性的多字节更新(比如一次改 5 个参数),先写一个"事务开始"标记,写完数据再写"事务结束"标记。上电时如果发现只有开始没有结束,说明上次写被打断,回滚到旧数据。MRAM 写入快,这个窗口很短,但工业环境什么都有可能发生,加上更安心。
6.3 定期自检与坏块标记
虽然 MRAM 没有传统意义的坏块,但长期运行后个别单元可能失效。可以定期(比如每天一次)对存储区做一次读校验,发现 CRC 连续失败的地址就标记为坏块,迁移数据并避开。这个机制在无人值守的现场设备上很有价值。
6.4 温度对写入的影响
MRAM 的写入电流随温度变化,低温下写入裕度会下降。虽然 MR25H40CDF 标称 -40℃ 可用,但在 -40℃ 冷启动时,建议先让设备预热几分钟再执行关键写入,或者对写入做重试。我在北方冬天的户外设备上遇到过冷启动写入失败,重试三次就成功了,后来加了预热逻辑再没出过问题。
7. 写在最后的一点个人体会
这套 MR25H40CDF 加 MK20DX128VFM5 的组合,我在三个工业项目里用过,最长的已经连续运行四年多,没出现过数据丢失。它的价值不在于性能多强,而在于行为可预测:写就是写,写完就在,没有擦除、没有忙等待、没有掉电窗口。对于工业设备来说,可预测比高性能重要得多。
如果你正在选型,我的建议是:先算清楚写入频率和数据量,如果一年写入超过一万次、或者对掉电敏感,就认真考虑 MRAM;如果只是存个配置、几年不动,Flash 更划算。选型没有绝对的好坏,只有匹配不匹配。
最后分享一个调试小技巧:调 SPI 存储时,先写一个已知模式(比如 0xAA、0x55 交替),再读回来比对,比直接写业务数据更容易定位问题。模式数据规律性强,示波器上也好认。这个习惯帮我省过很多排查时间。