做工业控制器这些年,存储这块真没少花力气。稳的容量小,容量大的怕掉电,快的不够持久,持久的又慢得离谱。直到在一个电能质量监测项目里被 SPI NOR Flash 的擦除写日志折磨了几轮之后,我换上了 MR25H40CDF 这颗 SPI MRAM,用 TM4C129ENCZAD 做主控,整套存储逻辑一下简单得让人不习惯:不用擦除、按字节直接写、掉电不丢、寿命几乎不用考虑。这篇文章就把这套“工业嵌入式存储”方案从选型逻辑、硬件接法、驱动实现到现场排障完整拆一遍。适合做 PLC、数据采集终端、电力保护设备、医疗仪器底层存储的朋友参考,尤其是被 Flash 寿命和掉电可靠性折磨过的人,看完应该能少走不少弯路。
1. 为什么是这两颗芯片:工业存储的痛点与选型逻辑
1.1 我在现场踩过的存储坑
之前做一台电能质量监测终端,要求实时记录电压暂降事件,每 100ms 采一波数据,出事件后要把前后 128 个周波存下来。现场条件很恶劣,随时可能断电,客户要求“断电前最后一批事件必须完整可查”。
最开始我用的是常规 SPI NOR Flash,32Mbit 那种,掉电存储确实没问题,但写到后面问题全出来了。
首先是擦除。NOR Flash 写之前必须擦,一个 4KB 扇区擦除时间按手册算要 40ms 到 150ms,页编程还要 1ms 左右。你想,掉电检测触发到系统真正断电往往只有几毫秒到几十毫秒,等 Flash 擦完,电早就没了。客户那边还统计过,高频事件日志写多了之后,Flash 的坏块开始冒出来,擦写寿命到了 10 万次就要做坏块管理和磨损均衡。
后来我换过 I2C EEPROM,比如 AT24C256。寿命倒是比 Flash 高一个数量级,但容量小、页面小,写入一个字节要 5ms 左右,而且 I2C 总线上的电平转换做不好,在强电磁干扰现场容易把数据写飞。
也考虑过电池供电的 SRAM,断电瞬间靠电池维持。但电池本身就是个维护项,几年换一次,客户嫌麻烦,工业现场没人愿意拆机换电池。
最后盯上了 MRAM。当时选型条件很直接:SPI 接口、容量不低于 4Mbit、工业温度范围、写入速度快、掉电不丢、寿命足够长。MR25H40CDF 基本就是照着这些条件长的。
1.2 MR25H40CDF:一颗不需要擦除的存储器
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,容量 512KB,支持标准 SPI 接口,时钟可以跑到 40MHz 级别,工作电压 2.7V 到 3.6V,工业级温度范围是 -40°C 到 +85°C。
它和 Flash 最本质的区别,在于存储单元不是电荷,而是磁隧道结(MTJ)。
磁隧道结结构上很像三明治:两层铁磁材料中间夹一层极薄的绝缘势垒层。其中一层磁化方向固定,叫固定层;另一层可以翻转,叫自由层。当自由层和固定层磁化方向平行时,隧道结呈现低电阻;反平行时呈现高电阻。读数据的时候,就是往这个结上施加一个小电压,测流过结的电流大小,从而判断是 0 还是 1。
写数据的时候,用电流产生磁场去翻转自由层的磁化方向,这个过程不需要高电压、不需要擦除、不需要电荷累积,物理上几十纳秒内就能完成翻转。所以 MRAM 的写操作是真正意义上的“直接覆盖写”,想写哪个地址就写哪个地址,写入前完全不需要擦除。
这带来的结果就是:
写寿命极高。MR25H40 的手册标注写入耐久性在 10 的 14 次方次以上,读操作理论上无限。这个数字意味着你即使每秒写同一地址一百次,连续写三千年也写不完。所以它根本不需要坏块管理,不需要磨损均衡。
写速度快。内部单元写操作是纳秒级,实际写入速度完全由 SPI 接口的时钟频率决定。10MHz 下写一个字节 0.8us,写 256 字节也不过 0.2ms。掉电前抢救数据,时间窗口充裕得多。
数据保持稳定。工业温度下数据保持 10 年以上,不依赖电荷存储,不存在“电荷泄漏导致数据慢慢翻转”的问题。
还有一个很多人没注意的优势:MR25H40 的封装引脚和常见 SPI NOR Flash 是兼容的,像 W25Q 系列那种标准 8 脚封装,CS、DO、WP、GND、DI、CLK、HOLD、VCC 的排布基本一致。老项目想升级存储介质,PCB 改版量很小,甚至有时候飞线就能验证。
1.3 TM4C129ENCZAD:适合干这个活的主控
TM4C129ENCZAD 是 TI 的 Cortex-M4F 主控,主频 120MHz,带浮点单元,内置 256KB SRAM 和 1MB Flash。这芯片在工业设备里很常见,因为外设相当齐全:四个 SSI 模块(也就是 SPI)、12 位 ADC、CAN、USB、QEI 编码器接口,最关键是内置了以太网 MAC 和 PHY,一颗芯片就能把采集、控制、存储、通信全包了。
拿我的项目来说,TM4C129 既要通过 ADC 采集电压电流,又要用 Modbus TCP 上送数据,还要把这些原始波形写到 MRAM 里。四个 SSI 模块里有三个都忙得过来:一个给 MRAM,一个给 ADC 前端,还有一个给其它 SPI 外设。MRAM 挂在独立 SPI 总线上,读写时序不会被别的器件抢占,这在高实时性场景里非常重要。
而且 TM4C129 的 SSI 模块支持最高几十 MHz 的波特率(实际受系统时钟和外设限制),内部还有 8 级 FIFO 和 uDMA 控制器,可以做到大块读数据不占 CPU。做事件记录、波形暂存这种数据搬运任务,非常合适。
工业级版本的工作温度也是 -40°C 到 +85°C,和 MR25H40 匹配,整机不用额外考虑不同芯片温度等级不一致的问题。
1.4 这套方案到底适用哪些场景
MR25H40CDF 加 TM4C129ENCZAD 的组合,最适合下面几类场景:
- 高频事件日志。比如故障录波、事件顺序记录,要求毫秒级持续写入,MRAM 写寿命和速度都撑得住。
- 关键参数定值保存。电力保护装置的定值、工业仪表的校准系数、伺服驱动器的运行参数,这些数据会被频繁修改,如果用 Flash,每次都担心寿命,用 MRAM 随便写。
- 掉电前的黑匣子数据。MRAM 写入快,掉电检测触发后还有充足时间把关键上下文推进去。
- 原位替换 SRAM 加电池方案。很多老设备是靠电池维持实时数据,用 MRAM 可以把电池模块直接去掉,维护成本立刻降下来。
不适合的场景也有:超大容量文件存储、音视频缓冲、需要低成本海量数据的应用,MRAM 单位比特成本还是比 Flash 高不少,没必要硬上。4Mbit 这种容量,定位就是“关键数据的高可靠存储”,不是当硬盘用的。
2. 硬件连接与电路设计:先把基础打好
2.1 引脚分配与 SPI 接线
MR25H40 的 8 个引脚功能很标准,和常见的 SPI NOR Flash 基本一致:
| MR25H40 引脚 | 功能 | 连接到 TM4C129 |
|---|---|---|
| 1 CS# | 片选,低有效 | GPIO 输出(建议手动控制) |
| 2 DO / MISO | 数据输出 | SSI2 RX(PB6) |
| 3 WP# | 写保护,低有效 | GPIO 输出或上拉 |
| 4 GND | 地 | 地平面 |
| 5 DI / MOSI | 数据输入 | SSI2 TX(PB7) |
| 6 CLK | 时钟 | SSI2 CLK(PB4) |
| 7 HOLD# | 暂停传输,低有效 | GPIO 输出或上拉 |
| 8 VCC | 电源 3.3V | 3.3V 电源 |
CS# 我强烈建议用普通 GPIO 控制,而不是用 SSI 模块自带的硬件 FSS。虽然 TM4C129 的 FSS 可以做片选,但工业场景下我们经常要在一个 CS 低电平周期内精确控制命令帧的起止,用 GPIO 可以随时拉高拉低,配合中断优先级做掉电保护也更灵活。而且如果以后想扩展多个 SPI 器件,GPIO 做片选扩展起来非常轻松。
MISO 和 MOSI 要注意方向。MR25H40 的 DO 接 TM4C129 的 RX(数据进来),DI 接 TX(数据出去)。很多第一次做的人把 TX 和 RX 画反了,结果数据全错位,读出来永远是 0xFF 或者 0x00。
2.2 电源、去耦和保护脚的接法
MR25H40 是 3.3V 供电,直接接 TM4C129 的 3.3V 电源域就行。芯片旁边放一个 0.1uF 陶瓷电容加一个 1uF 陶瓷电容,尽量靠近 VCC 引脚。为什么要两个容值?因为不同频率的噪声需要不同容量的电容去滤,0.1uF 滤高频、1uF 滤中低频,工业现场电源线上的毛刺非常多,这点成本不能省。
WP# 和 HOLD# 是最容易翻车的地方。
WP# 是写保护脚,低电平有效。如果这个脚悬空,芯片内部状态可能不稳定,尤其在上电瞬间,容易误触发写保护,导致后面所有写命令都不生效。HOLD# 是暂停脚,低电平有效。如果悬空,在强电磁干扰下可能被拉低,这时 SPI 时钟还在走,但数据会停住,表现为“读出来一半对一半错”或者“总线卡死”。
这两个脚的正确接法,要么直接 10kΩ 电阻上拉到 VCC,要么接 MCU 的 GPIO 并默认输出高。我实际项目里是把 WP# 接到 GPIO 的,这样可以在掉电检测触发时主动拉低 WP#,从硬件层面禁止任何意外的写操作。HOLD# 则老老实实上拉,毕竟平时用不到暂停功能,没必要多占用一个 GPIO。
注意 MCU 复位期间 GPIO 是浮空的,所以哪怕接了 GPIO,也要在外部加上拉电阻保证默认状态正确。不要单纯依赖 MCU 的引脚内部上拉,内部上拉阻值太大,抗干扰能力不够。
2.3 PCB 布局布线经验
MR25H40 工作在 10MHz 以上时,PCB 走线就不能太随意了。
时钟线 CLK 和 MOSI 是干扰源,也是被干扰对象。布线时尽量短,不要超过 5cm,不要和继电器驱动线、PWM 功率线、变压器附近走线平行。MISO 是敏感信号,建议串联一个 22Ω 到 33Ω 的电阻,能有效抑制振铃和反射。CS# 线的时序要求最严格,长度尽量和 CLK 线保持一致,避免因为走线长短差导致片选时序错位。
地线一定要完整。MRAM 放在 MCU 附近,芯片底下的地平面不要被割开。工业设备里常有电机、开关电源、接触器这些强干扰源,地平面不完整的话,高频 SPI 信号容易把噪声耦合回电源,轻则误码,重则复位。
如果板子上除了 MRAM 还有 LCD、传感器等 SPI 设备,最好把 MRAM 放在离 MCU 最近的位置,并且单独走一条短的 SPI 总线。别把 MRAM 和高速传感器挂在同一根总线上复用,调试阶段你会怀念这种安排的。
2.4 SPI 模式的选择:Mode 0 还是 Mode 3
MR25H40 官方支持 SPI Mode 0 和 Mode 3。TM4C129 的 SSI 也支持这两种,配置参数分别对应 SSI_FRF_MOTO_MODE_0 和 SSI_FRF_MOTO_MODE_3。
Mode 0 和 Mode 3 的区别在于时钟极性(CPOL)和采样相位(CPHA)。Mode 0 是时钟空闲为低、上升沿采样;Mode 3 是时钟空闲为高、下降沿采样。MR25H40 两种模式都能工作,但你的板卡上如果还有别的 SPI 外设,建议统一选一种,通常选 Mode 0,因为大多数 SPI Flash、SD 卡、传感器默认都支持 Mode 0。
SPI 模式配错的典型现象是:读 ID 返回全 0xFF,或者读出来的数据全是 0x00。因为时钟极性和采样沿不对,从机发出的数据和主机采样时刻错开半个周期,数据就全错位了。遇到这种问题,第一步不是怀疑芯片坏了,而是用示波器抓 CLK 引脚的静态电平,空闲时是低电平还是高电平,就能立刻判断出当前是 Mode 0 还是 Mode 3。
3. 驱动层实现:从零把数据存进去读出来
3.1 SSI 模块初始化
我用 TivaWare 驱动库做初始化,以 SSI2 为例,引脚接法是 PB4 做 CLK、PB6 做 RX(MISO)、PB7 做 TX(MOSI),CS 用 PA5 的 GPIO。
#include <stdint.h> #include <stdbool.h> #include "inc/hw_memmap.h" #include "inc/hw_types.h" #include "driverlib/sysctl.h" #include "driverlib/gpio.h" #include "driverlib/ssi.h" #include "driverlib/pin_map.h" #define MR_SSI_BASE SSI2_BASE #define MR_CS_PORT GPIO_PORTA_BASE #define MR_CS_PIN GPIO_PIN_5 #define MR_WP_PORT GPIO_PORTA_BASE #define MR_WP_PIN GPIO_PIN_6 #define MR_HOLD_PORT GPIO_PORTA_BASE #define MR_HOLD_PIN GPIO_PIN_7 static void mr_cs_low(void) { GPIOPinWrite(MR_CS_PORT, MR_CS_PIN, 0); } static void mr_cs_high(void) { GPIOPinWrite(MR_CS_PORT, MR_CS_PIN, MR_CS_PIN); } void MR25H40_Init(void) { SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI2); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOB); SysCtlPeripheralEnable(MR_CS_PORT); while(!SysCtlPeripheralReady(SYSCTL_PERIPH_SSI2)) {} while(!SysCtlPeripheralReady(SYSCTL_PERIPH_GPIOB)) {} while(!SysCtlPeripheralReady(MR_CS_PORT)) {} GPIOPinConfigure(GPIO_PB4_SSI2CLK); GPIOPinConfigure(GPIO_PB6_SSI2XDAT0); GPIOPinConfigure(GPIO_PB7_SSI2XDAT1); GPIOPinTypeSSI(GPIO_PORTB_BASE, GPIO_PIN_4 | GPIO_PIN_6 | GPIO_PIN_7); GPIOPinTypeGPIOOutput(MR_CS_PORT, MR_CS_PIN | MR_WP_PIN | MR_HOLD_PIN); GPIOPinWrite(MR_CS_PORT, MR_WP_PIN | MR_HOLD_PIN, MR_WP_PIN | MR_HOLD_PIN); GPIOPinWrite(MR_CS_PORT, MR_CS_PIN, 0); SSIConfigSetExpClk(MR_SSI_BASE, SysCtlClockGet(), 10000000, SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 8, 0); SSIEnable(MR_SSI_BASE); }时钟我保守设了 10MHz。MR25H40 本身支持更高频率,但工业现场线缆长度、PCB 布线、干扰水平都不可控,10MHz 下时序余量很大,不折腾自己。如果做的是高速数据采集,验证过 PCB 走线质量后再提到 20MHz 也不迟。
注意 GPIOPinTypeSSI 设置引脚为 SSI 功能前,一定要先使能 GPIOB 外设时钟。漏掉这一步是 Tiva 项目最常见的问题,现象就是引脚不干活,初始化函数卡在 while 里出不来。GPIOA 的时钟也要使能,因为 CS、WP、HOLD 都是挂在 GPIOA 上的。
3.2 基础命令:写使能、读状态、读 ID
MR25H40 的命令体系非常简单,就这么几个核心命令:
- 0x06 WREN:写使能,置位状态寄存器中的 WEL 位
- 0x04 WRDI:写禁止,清零 WEL 位
- 0x05 RDSR:读状态寄存器
- 0x02 WRITE:写数据
- 0x03 READ:读数据
- 0x9F RDID:读器件 ID
写数据之前必须先发 WREN,这和 Flash 一样。MRAM 上电默认是写禁止状态,WEL 位为 0,如果不发 WREN,写命令会被忽略。每次写操作完成之后,WEL 会被自动清除,所以每次写数据命令前都要重新发 WREN,不能偷懒。
先封装一个底层传输函数。SPI 是全双工的,每发一个字节的同时会收到一个字节,哪怕读数据时主机也必须持续发时钟。我习惯把发送和接收放在一个函数里处理:
static void mr_spi_transfer(const uint8_t *tx, uint8_t *rx, uint32_t len) { uint32_t tmp; for (uint32_t i = 0; i < len; i++) { SSIDataPut(MR_SSI_BASE, tx ? tx[i] : 0xFF); while (SSIBusy(MR_SSI_BASE)) { } SSIDataGet(MR_SSI_BASE, &tmp); if (rx) rx[i] = (uint8_t)tmp; } }读状态寄存器的实现:
uint8_t MR25H40_ReadSR(void) { uint8_t tx[2] = {0x05, 0x00}; uint8_t rx[2]; mr_cs_low(); mr_spi_transfer(tx, rx, 2); mr_cs_high(); return rx[1]; }状态寄存器只有两个有效位:bit0 是 WIP(写进行中),bit1 是 WEL(写使能锁存)。对 MRAM 来说,WIP 基本永远是 0,因为写操作在 SPI 命令帧结束时就完成了。但如果你是从 Flash 代码移植过来的,保留一个等待 WIP 清零的循环也无妨,反正它不会死等。
读 ID 的代码:
bool MR25H40_ReadID(uint8_t *id) { uint8_t tx[4] = {0x9F, 0x00, 0x00, 0x00}; uint8_t rx[4]; bool ok = false; mr_cs_low(); mr_spi_transfer(tx, rx, 4); mr_cs_high(); id[0] = rx[1]; id[1] = rx[2]; id[2] = rx[3]; if (id[0] == 0x00 && id[1] == 0x51 && id[2] == 0x2A) ok = true; return ok; }读 ID 返回三个字节,按手册标注常见是 0x00 0x51 0x2A。这个命令强烈建议上电自检时调用一次。如果读 ID 不对,后面所有存储操作都没有意义,先解决通信问题再继续。
3.3 写数据和读数据的完整实现
写数据命令格式是:0x02 + 24 位地址 + 数据字节。MR25H40 的地址是 19 位有效,高 5 位忽略。地址在写入过程中会自动递增,地址到达 0x7FFFF 后会回卷到 0x00000。所以如果写入长度会跨过地址顶部,最好分两次写,或者在调用前做边界检查。
bool MR25H40_Write(uint32_t addr, const uint8_t *data, uint32_t len) { uint8_t wren = 0x06; uint8_t sr; if (addr + len > 0x80000) return false; mr_cs_low(); mr_spi_transfer(&wren, NULL, 1); mr_cs_high(); sr = MR25H40_ReadSR(); if ((sr & 0x02) == 0) return false; uint8_t tx[4] = {0x02, (uint8_t)(addr >> 16), (uint8_t)(addr >> 8), (uint8_t)addr}; mr_cs_low(); mr_spi_transfer(tx, NULL, 4); mr_spi_transfer(data, NULL, len); mr_cs_high(); return true; }注意地址三字节的发送顺序是高位在前,地址 0x12345 要发成 0x01、0x23、0x45。如果发反了,数据会写到完全不同的位置,而且这种错误很难排查,因为它不会报错,只是写到你看不到的地方。
读数据命令格式是:0x03 + 24 位地址,然后持续输出数据。主机每发一个 dummy 字节(比如 0xFF),MISO 上就会返回一个数据字节。
bool MR25H40_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t tx[4]; uint8_t dummy = 0xFF; if (addr + len > 0x80000) return false; tx[0] = 0x03; tx[1] = (uint8_t)(addr >> 16); tx[2] = (uint8_t)(addr >> 8); tx[3] = (uint8_t)addr; mr_cs_low(); mr_spi_transfer(tx, NULL, 4); for (uint32_t i = 0; i < len; i++) { mr_spi_transfer(&dummy, &buf[i], 1); } mr_cs_high(); return true; }连续读很方便,CS# 保持低电平,地址自动递增,想读多少字节就发多少个 dummy。MRAM 没有页边界的限制,读完整个 512KB 都可以一气呵成。这一点比很多 Flash 的 256 字节页模式舒服太多了。
3.4 用 FIFO 和 uDMA 提高传输效率
轮询方式在数据量小的时候完全够用。但如果要从 MRAM 里一次性读回几 KB 的波形数据,轮询会把 CPU 时间全耗在等 FIFO 上,这时候就该让 uDMA 上场了。
TM4C129 的 SSI 模块带发送 FIFO 和接收 FIFO,当 FIFO 水位达到阈值时,可以触发 uDMA 传输。你要做的,是把 MRAM 的数据搬运任务交给 uDMA 控制器,CPU 可以去处理协议栈或者中断。
大块读数据的思路是这样的:
- 先把读命令和地址通过轮询或者 DMA 发出去
- 然后配置 uDMA 的 RX 通道,目的地址指向 RAM 缓冲区
- 持续向 SSI TX 写入 dummy 字节(可以用 uDMA TX 通道自动发送固定值)
- 接收 FIFO 到达阈值后,uDMA 自动把数据搬到内存
- 传输完成后触发完成中断
uDMA 的通道号要查对应数据手册,TivaWare 头文件里也有定义。配置时要特别注意源地址和目的地址的递增模式,RX 通道的源地址是 SSI 数据寄存器(固定地址),目的地址是 RAM(递增),一旦递增方向配反了,收到的数据全是错位的。
我用 uDMA 在 10MHz 下连续读 4KB 数据,CPU 占用几乎为零。对于要边采集边存储边通信的工业设备来说,这一点很关键。
3.5 驱动代码骨架与验证要点
完整的驱动骨架就是:初始化 -> 读 ID 自检 -> 写数 -> 读回比对 -> 正常使用。
上电自检函数我一般这样写:
uint8_t mr_id[3]; uint8_t test_buf[256]; uint8_t read_buf[256]; MR25H40_Init(); if (!MR25H40_ReadID(mr_id)) { // 打印错误,存储系统不可用 return; } for (int i = 0; i < 256; i++) test_buf[i] = (uint8_t)(i * 3 + 1); MR25H40_Write(0x1000, test_buf, 256); MR25H40_Read(0x1000, read_buf, 256); if (memcmp(test_buf, read_buf, 256) != 0) { // 自检失败 } else { // 存储系统正常 }为什么自检用 0x1000 而不是地址 0?因为很多工业设备希望把 0 地址留作启动信息区,自检程序不应该去破坏真正的业务数据。自检地址选一个不敏感的区域,写完后最好再擦掉或者覆盖成 0xFF,避免给后面的正式代码留下脏数据。
4. 工业场景下的可靠性设计
4.1 数据校验与存储区规划
MRAM 本身很可靠,但工业环境里的干扰、电源跌落、总线毛刺都可能让数据在传输过程中变错。所以数据校验不能省。
我用的方案是 CRC16 或者 CRC32,看数据量大小。单条事件记录 128 字节以内用 CRC16,整块波形数据用 CRC32。TM4C129 自带硬件 CRC 模块,算 4KB 数据几乎瞬时完成,不需要软件查表。
每条记录的结构我习惯这样设计:
| 偏移 | 内容 |
|---|---|
| 0 | 记录魔数,比如 0xA55A |
| 2 | 数据长度 |
| 4 | 数据 |
| 4 + len | CRC16/CRC32 校验 |
读回数据时先验证魔数,再看长度,最后算 CRC。任何一个环节不对,这条记录直接丢弃或者走备份区读取。不要试图去修数据,MRAM 不会像 Flash 那样产生半写状态,出错的记录基本可以确定是传输过程中被外部干扰破坏了,重读一次往往就正常了。
存储区规划方面,512KB 听起来不大,但规划好了完全够用。我把地址空间分成三块:
- 配置区:0x00000 到 0x01FFF,8KB,双备份。存放设备参数、校准系数、网络配置。
- 日志区:0x02000 到 0x3FFFF,224KB,环形日志。存放历史报警、事件顺序记录。
- 波形区:0x40000 到 0x7FFFF,256KB,暂存故障前后的波形数据。
环形日志区的写位置指针我存在 MRAM 末尾一个固定地址里。每写一条日志,先更新日志区内容,再更新指针。指针数据量小,写入时间极短,掉电丢失的概率可以忽略。因为 MRAM 没有磨损问题,环形日志区不用做磨损均衡,直接覆盖写就行,这也让代码简单了很多。
4.2 掉电保护:不靠电池也能守住关键数据
工业现场最怕的就是掉电瞬间那一顿操作。以前用 Flash,掉电时写日志要擦除、要等时间,往往还没写完电就没了。MRAM 的写入速度快,掉电窗口内能完成的操作量完全不一样。
以 10MHz SPI 时钟为例,写 256 字节加上命令和地址也就 0.2ms 多一点。如果 MCU 电源端加一个 100uF 电容,从检测到电压跌落开始,通常还有几个毫秒的时间窗口。这个时间里足够把关键上下文、状态字、最近一段事件数据全部推进 MRAM。
我采用的做法是:
- TM4C129 通过 ADC 监控 3.3V 电源,当电压低于 3.0V 时触发掉电中断
- 掉电中断里关掉所有不必要的中断,只保留 SPI 和这个掉电检测
- 快速把关键 RAM 数据按固定格式写到 MRAM 的紧急保存区
- 写完拉低 WP#,从硬件上锁住 MRAM,防止后续电源波动造成意外写入
这里有一个细节:MRAM 写操作即使被中断,也不会让旧数据变成“半新半旧”的状态。因为磁隧道结的翻转是物理阈值行为,没有达到翻转条件就不会发生变化。这一点相比电荷存储的 Flash 是天壤之别。Flash 写一半掉电,那个单元可能处于一个模糊状态,读出来还可能不稳定;MRAM 要么旧值,要么新值,不会出现第三种情况。
外部再搭配一个复位监控芯片比如 TPS3809 更稳,电压掉到阈值直接给 MCU 复位,同时把 WP# 拉死。不过如果 TM4C129 内部 BOR 用得好,外部芯片也不是必须。
4.3 长时间可靠性测试记录
我板子定型之后做过一组针对性测试,结果可以作为参考。
第一项,位翻转测试。往 MRAM 全地址空间写入伪随机数,然后连续读回比对。没有 MRAM 是不允许位翻转的,有的话先怀疑接线和电源,而不是芯片。
第二项,耐久写入测试。对同一块 4KB 区域循环写入 100 万次,每写完一轮读回校验。MR25H40 规格上写着 10 的一次方次方级耐久度,100 万次对它是小意思,但这个过程能验证你的驱动时序是否稳定。我当时用 10MHz 时钟,每轮写加读完大概 6ms,100 万次跑了大概 100 分钟,全程无错。
第三项,高低温测试。整机放在 -40°C 和 +85°C 环境箱里,各跑 8 小时,期间持续往 MRAM 写日志,结束后断电静置 24 小时,再上电读回,数据完好。
第四项,静电和群脉冲测试。接触放电 8kV、EFT 4kV 情况下,反复读写 MRAM,偶发一次读错误,加上 CRC 校验后能自动重读纠正。这说明校验机制不是摆设。
4.4 与常见存储方案的对比和迁移建议
| 参数 | MR25H40CDF | 常见 SPI NOR Flash | 常见 I2C EEPROM |
|---|---|---|---|
| 容量 | 4Mbit | 1Mbit ~ 128Mbit | 1Kbit ~ 1Mbit |
| 写方式 | 无需擦除,直接写 | 需先擦除扇区 | 无需擦除,按页写 |
| 单字节写时间 | SPI 时钟周期级 | 页编程 0.5ms ~ 3ms | 约 5ms |
| 擦除时间 | 无 | 扇区擦 40ms ~ 150ms | 无 |
| 写寿命 | 10 的 14 次方 | 10 的 5 次方 | 10 的 6 次方 |
| 数据保持 | 10 年以上 | 20 年左右 | 几十年级 |
| 坏块管理 | 不需要 | 需要 | 不需要 |
| 成本 | 偏高 | 低 | 极低 |
从 Flash 迁移到 MR25H40 时,有几个坑值得提前知道。
最大的坑就是“擦除”。Flash 代码里到处是擦除函数,迁移到 MRAM 后这些函数可以直接删掉。MRAM 写之前不需要擦除,你如果保留擦除命令调用,擦除命令本身不会生效,但会让代码逻辑变复杂,还容易让人误以为操作成功了。
第二是全地址写。Flash 有页的概念,很多驱动按页编程,一次最多写 256 字节。MRAM 完全没这个限制,想写多少写多少,但地址顶部回卷的规则要处理好。
第三是读状态寄存器等待。Flash 写完后要轮询 WIP 位等待编程完成,MRAM 写完就完成了,没有等待状态。从 Flash 迁过来的代码里如果有 while(WIP) 死等,在 MRAM 上会因为 WIP 永远是 0 直接跳过,不会出问题,但你要理解它为什么跳过了。
5. 现场问题排查与经验速查
5.1 读回来的数据全是 0xFF 或 0x00
这是最常见的现象,十个问题里有八个是这个。读出来全 0xFF,十有八九是 SPI 模式不对或者 MISO 线没接好。先把读 ID 命令调通,只要读 ID 能返回 0x00 0x51 0x2A,就说明通信链路是通的,再去排查读写命令。
读出来全 0x00,重点检查 MOSI 线和 CS 线。MOSI 如果虚焊,主机发出的命令字节根本没送到芯片,芯片也不会返回数据,MISO 就一直保持低电平。CS 线如果一直是低电平,芯片可能在启动时就把状态搞乱了,复位 CS 试试。
我在现场最喜欢用的排查顺序是:量 VCC 和 GND -> 示波器抓 CLK 有没有波形 -> 抓 CS 有没有正确拉低 -> 读 ID。不要上来就怀疑芯片坏了,MR25H40 没那么容易坏,大部分问题出在焊接和配置上。
5.2 WEL 总是不为 1,写命令无效
写数据前发 WREN,然后读状态寄存器,发现 WEL 位始终是 0,这就意味着芯片没有进入写使能状态。
排查顺序和原因:
- WP# 被拉低了。看原理图和 GPIO 初始化,WP# 必须为高才能写。很多板子上电瞬间 GPIO 默认输出 0,如果不加外部上拉,WP# 一直是低,写使能永远不成功。
- WREN 命令之后 CS 没有拉高。WREN 和 WRITE 是两个独立的命令,中间必须 CS 拉高分隔。如果 CS 一直低,芯片会认为 WREN 是 WRITE 命令的一部分,WEL 不会被锁存。
- SPI 帧格式配置错了。TM4C129 的 SSI 除了摩托罗拉帧格式,还支持德州仪器同步串行帧格式和 MICROWIRE 帧格式。如果配置成后者,命令字节根本对不上,读状态寄存器可能读到全 0。检查 SSIConfigSetExpClk 里的协议参数是不是 SSI_FRF_MOTO_MODE_0。
还有一个容易被忽略的:每次写命令结束后 WEL 自动清零。如果你在循环里连续写多个地址,不要在循环外只发一次 WREN。必须在每次写命令前都发一次 WREN,否则从第二次开始写命令全部被忽略。
5.3 高速通信下的数据错误
时钟频率提上去之后,偶尔出现读回来的数据某一位错误,重启后可能又正常了。这种随机错误多半是信号完整性问题。
SPI 速率越高,信号边沿越陡,如果走线太长或者没有端接,反射和振铃会导致采样点不稳定。解决办法:降速。10MHz 在工业现场已经够用,512KB 容量也不可能让你传大文件。如果能降到 5MHz,时序余量巨大,纠错成本最低。
另外检查一下 MISO 线上有没有串联电阻。如果没有,加一个 22Ω 到 33Ω 的电阻和一个小电容(比如 15pF)组成滤波,能有效吸收振铃。
电源也要看。如果 3.3V 纹波超过 50mV,SPI 信号电平就有风险。给 MRAM 供电脚再加一个 0.01uF 高频电容,对比测试往往有惊喜。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读 ID 全 0xFF | SPI 模式错误、MISO 虚焊 | 用示波器抓 CLK 空闲电平,确认 Mode 0 或 Mode 3 |
| 读 ID 全 0x00 | MOSI 不通、CS 异常 | 量 MOSI 通路,检查 CS 极性 |
| 写数据读回不变 | 未发 WREN、WP# 拉低 | 每次写前发 WREN,检查 WP# 上拉 |
| WEL 始终为 0 | WREN 后 CS 未拉高、WP# 低 | 确认命令帧之间 CS 拉高 |
| 数据错位 | 地址三字节顺序反了 | 检查 24 位地址高位在前 |
| 偶发读错误 | 信号完整性问题 | 降速、MISO 加串联电阻、加大去耦电容 |
| HOLD# 悬空导致总线卡死 | 干扰拉低 HOLD# | 10kΩ 上拉 HOLD# 到 VCC |
| 复位后数据没问题但写不进 | GPIO 初始化把 WP# 拉低 | 初始化时先输出高再启用写 |
做工业场景的存储,我最后想说的其实是心态问题。不要指望任何一颗芯片能“永远不错”,也不要为了追求极限性能把时序余量压到零。MR25H40CDF 的优势在于把存储这件破事从“系统瓶颈”变成了“无感外设”,你不需要伺候它,它也不会天天给你找事。TM4C129ENCZAD 也是一样,外设全、资料多、调试工具成熟,两者结合,真正到了现场会让你省心很多。
最后再分享一个我个人的习惯:新板子回来第一版固件,我永远先跑一个“写全地址 + 读全地址 + 比对”的马拉松测试,跑个几百遍再往上面挂业务逻辑。存储这种基础模块,验证得越早,后面整个项目的节奏就越稳。