1. 项目缘起与方案选型思考
1.1 为什么要在工业场景里折腾 MRAM 这颗料
做工业嵌入式这行的朋友应该都有个共识:数据存储这块,选型选得好,后面少掉一半头发。我这些年做过的板子里,EEPROM、NOR Flash、NAND Flash、FRAM 都用过一圈,各有各的脾气。这次项目里用到的MR25H40CDF是一颗 4Mbit 的MRAM(磁性随机存储器),说实话第一次接触这个品类的时候我也犯嘀咕,但用下来之后,它在工业场景里的几个特性确实让人省心。
先把这颗料的基本盘交代清楚。MR25H40CDF 是 4Mbit 容量,也就是 512KB,接口是标准的SPI,支持最高 40MHz 的时钟频率,工作电压 2.7V 到 3.6V,工业级温度范围 -40℃ 到 +85℃,还有扩展版本能到 +105℃。封装是 8 引脚的 DFN 或者 SOIC,引脚定义和普通的 SPI Flash 几乎一模一样,这意味着硬件改板的时候基本可以做到 pin-to-pin 替换,这一点对已经量产的项目来说太重要了。
那为什么不用更便宜的 SPI NOR Flash 呢?这就得说到 MRAM 的底层原理了。MRAM 用的是磁性隧道结(MTJ)来存储数据,靠的是电子自旋方向而不是电荷来表征 0 和 1。这个原理带来的直接好处是:写入不需要先擦除,可以按字节随机写入,写入次数几乎无限(官方标称 10^14 次以上),数据保持时间超过 20 年,而且没有 Flash 那种擦写寿命焦虑和写前擦除的等待时间。
我举个实际场景你就明白了。工业设备里经常要记录一些运行参数,比如电机累计运行时间、故障日志、校准系数。这些数据的特点是:写入频繁、每次写入量很小、掉电不能丢。用 EEPROM 吧,寿命一般 100 万次,频繁写几年就到头了;用 NOR Flash 吧,得整块擦除再写,还得做磨损均衡,软件复杂度上去了;用 FRAM 吧,容量小、价格高。MRAM 在这个场景里就非常合适,写入快、寿命长、不用擦除、掉电数据不丢,软件层面几乎不用做特殊处理。
1.2 STM32L162ZE 在这套方案里扮演什么角色
主控选的是STM32L162ZE,这是 ST 家 L1 系列的超低功耗 MCU,Cortex-M3 内核,最高 32MHz 主频,512KB Flash、80KB SRAM,带 LCD 控制器、AES 硬件加密、多路 SPI/I2C/USART。选它主要看中三点:一是低功耗特性适合电池供电或者能量采集的工业节点;二是外设资源够用,SPI 接口多,方便挂 MRAM 和其他传感器;三是工业级温度范围和长期供货相对稳定。
STM32L162ZE 的 SPI 外设支持标准 SPI 模式,最高时钟可以到 16MHz(受限于 APB 时钟分频),虽然跑不满 MR25H40CDF 的 40MHz 上限,但对于大多数工业数据记录场景来说完全够用。这里要说明一下,SPI 时钟不是越高越好,线长了、干扰大了,时钟太高反而容易出错,后面实操部分我会详细讲怎么定这个频率。
两者搭配起来,硬件上就是 STM32 的 SPI 引脚(SCK、MISO、MOSI)加上一根片选(CS)连到 MRAM,再加上电源和地,基本就齐活了。软件上需要实现 SPI 底层读写、MRAM 的命令解析、地址管理、数据校验这几块。整体架构不复杂,但细节上坑不少,下面我按实际做项目的顺序,把每个环节拆开讲。
1.3 整体方案的设计取舍
在动手之前,有几个设计决策需要先定下来,这些决策会直接影响后面的代码结构和调试难度。
第一个决策是用硬件 SPI 还是软件模拟 SPI。硬件 SPI 速度快、CPU 占用低,但引脚固定,PCB 布线受限;软件 SPI 灵活,任何 GPIO 都能用,但速度慢、占 CPU。这个项目里我选的是硬件 SPI,因为 STM32L162ZE 的 SPI1 引脚正好和 PCB 布局不冲突,而且数据记录对速度有一定要求,硬件 SPI 更合适。如果你遇到引脚被占用的情况,软件 SPI 也不是不能用,只是要把时钟降到 1MHz 以下,保证时序稳定。
第二个决策是片选信号怎么管。SPI 协议里片选有两种做法:硬件片选(由 SPI 外设自动控制)和软件片选(用普通 GPIO 手动拉低拉高)。STM32 的硬件 NSS 在实际使用中有些限制,比如多从机场景下不太灵活,所以我一般推荐用软件片选,也就是随便找一个 GPIO,在每次传输前拉低、传输后拉高。这样控制权完全在自己手里,调试的时候也方便用逻辑分析仪抓波形。
第三个决策是数据怎么组织。MRAM 是字节寻址的,512KB 空间可以随便用。但为了管理方便,我一般会做一个简单的分区表,比如前 4KB 放设备信息和配置参数,中间 256KB 放运行日志,后面放校准数据和备份区。每个区域有自己的起始地址和长度,代码里用宏定义或者结构体来管理,避免到处写魔法数字。
第四个决策是要不要做数据校验。工业环境里电磁干扰大,SPI 通信偶尔会出错,所以校验是必须的。简单一点可以用 CRC8 或者 CRC16,复杂一点可以用双备份加版本号。这个项目里我用的是每条记录后面跟一个 CRC16,读取的时候校验,不通过就重读或者标记为坏数据。
2. MR25H40CDF 的核心细节与 SPI 通信要点
2.1 这颗 MRAM 的命令集和操作时序
MR25H40CDF 的 SPI 命令集和普通 SPI Flash 很像,但有几个关键区别需要特别注意。先看它的核心命令:
| 命令名称 | 命令码 | 功能说明 | 地址字节数 |
|---|---|---|---|
| WREN | 0x06 | 写使能 | 0 |
| WRDI | 0x04 | 写禁止 | 0 |
| RDSR | 0x05 | 读状态寄存器 | 0 |
| WRSR | 0x01 | 写状态寄存器 | 0 |
| READ | 0x03 | 读数据 | 3 |
| WRITE | 0x02 | 写数据 | 3 |
| SLEEP | 0xB9 | 进入睡眠 | 0 |
| WAKE | 0xAB | 唤醒 | 0 |
和 Flash 最大的区别在于:WRITE 命令不需要先擦除。Flash 写之前必须发 WREN、然后发擦除命令、等擦除完成、再发 WREN、再写数据,一套流程下来几十毫秒就没了。MRAM 直接发 WREN、然后发 WRITE 加地址加数据就行,写入时间就是 SPI 传输时间,几微秒搞定。
时序上,MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1)。我一般用 Mode 0,因为 STM32 的 SPI 默认配置就是 Mode 0,省得改。数据在 SCK 上升沿采样,下降沿变化,这个和大多数 SPI 从机一致。
有一个细节容易被忽略:MRAM 的写操作之后需要等待内部写周期完成。虽然 MRAM 写入很快,但并不是瞬间完成的,官方手册里写的是典型 35ns 的写周期时间。实际使用中,如果你连续写很多字节,中间不需要额外等待,因为 SPI 传输本身的时间就远大于写周期。但如果你写完立刻要读同一地址,最好还是查一下状态寄存器的 WIP 位(Write In Progress),确保写完了再读。
2.2 状态寄存器的解读与使用
状态寄存器是判断 MRAM 当前状态的关键,RDSR 命令读回来的一个字节里,各个位的含义如下:
- bit 0(WIP):写进行中标志,1 表示正在写,0 表示空闲。这个位在连续写操作时很有用。
- bit 1(WEL):写使能锁存,执行 WREN 后置 1,写操作完成后自动清 0。
- bit 2-5:保留位,读出来是 0。
- bit 6(BP0):块保护位 0。
- bit 7(BP1):块保护位 1。
块保护位可以用来保护部分地址空间不被写入,这在工业场景里很有用。比如你把校准参数放在某个区域,设置块保护之后,即使程序跑飞了也不会误写。不过这个功能我一般不用,因为调试的时候容易把自己坑了,忘记解除保护导致写不进去,排查半天。
实际代码里,我一般会封装两个函数:一个读状态寄存器,一个等待写完成。等待写完成的逻辑就是循环读状态寄存器,直到 WIP 位为 0,加一个超时计数防止死循环。
uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI_Transfer(0x05); status = SPI_Transfer(0xFF); MRAM_CS_HIGH(); return status; } void MRAM_WaitWriteComplete(void) { uint32_t timeout = 100000; while ((MRAM_ReadStatus() & 0x01) && timeout--) { // 等待写完成 } }2.3 地址空间的组织与访问方式
MR25H40CDF 的 512KB 地址空间是线性映射的,地址从 0x00000 到 0x7FFFF。读写命令都是 3 字节地址,高位在前。这里要注意一个坑:地址是 24 位的,但实际只用了 19 位,高 5 位是保留的,发的时候填 0 就行。如果你不小心把高位填了 1,MRAM 可能会忽略或者行为不确定,所以地址变量最好用 uint32_t,然后和 0x7FFFF 做与运算,确保不会越界。
数据组织上,我一般会做一个简单的分区表,用结构体或者宏定义来管理:
#define MRAM_BASE_ADDR 0x00000 #define MRAM_CONFIG_ADDR 0x00000 #define MRAM_CONFIG_SIZE 0x01000 // 4KB #define MRAM_LOG_ADDR 0x01000 #define MRAM_LOG_SIZE 0x40000 // 256KB #define MRAM_CALIB_ADDR 0x41000 #define MRAM_CALIB_SIZE 0x02000 // 8KB #define MRAM_BACKUP_ADDR 0x43000 #define MRAM_BACKUP_SIZE 0x3D000 // 剩余空间这样组织的好处是,每个区域职责明确,代码里用宏定义引用,改地址的时候只改一处。另外,日志区域我一般会做成环形缓冲区,写满之后从头覆盖,配合一个写指针和读指针,实现简单的日志管理。
2.4 SPI 时钟频率与信号完整性的权衡
前面提到 STM32L162ZE 的 SPI 最高能到 16MHz,MR25H40CDF 支持到 40MHz,理论上可以跑满 16MHz。但实际项目里,我一般会把时钟定在8MHz 或者 4MHz,原因有几个:
第一,工业现场的电磁环境复杂,SPI 线走长了之后,高频信号容易振铃和串扰,导致数据出错。8MHz 下信号质量明显好于 16MHz,误码率低很多。
第二,STM32L162ZE 是低功耗 MCU,主频只有 32MHz,SPI 分频之后 16MHz 需要 APB 时钟 32MHz 二分频,这时候 CPU 访问 Flash 和 SRAM 可能会有等待周期,影响整体性能。8MHz 下系统更从容。
第三,MRAM 的写入时间虽然短,但如果你用 DMA 传输,时钟太高可能导致 DMA 和 SPI 的配合出问题,调试起来很麻烦。
具体怎么定这个频率,我的经验是:先用示波器或者逻辑分析仪看波形,如果上升沿和下降沿干净、没有明显过冲和振铃,就可以往上提;如果波形已经变形了,就降频。另外,SPI 的 PCB 走线尽量短、等长,SCK 线两边包地,能显著改善信号质量。
3. 基于 STM32L162ZE 的实操过程与代码实现
3.1 硬件连接与 CubeMX 配置
先看硬件连接。STM32L162ZE 的 SPI1 引脚是 PA5(SCK)、PA6(MISO)、PA7(MOSI),片选我用的是 PA4,配置成普通 GPIO 输出。MRAM 的 VCC 接 3.3V,GND 接地,WP 和 HOLD 引脚如果不用就上拉到 VCC。这里注意,MR25H40CDF 的 WP 和 HOLD 是低有效,不用的时候一定要拉高,否则写保护或者通信会被打断。
CubeMX 里的配置步骤如下:
- 使能 SPI1,模式选 Full-Duplex Master,硬件 NSS 选 Disable,因为我们要用软件片选。
- 参数配置:Clock Polarity 选 Low,Clock Phase 选 1 Edge,也就是 Mode 0。Prescaler 先选 8,这样 SPI 时钟是 4MHz,调试稳定之后再改。
- Data Size 选 8 Bits,First Bit 选 MSB First,CRC 关掉。
- PA4 配置成 GPIO Output,初始电平 High,也就是片选不选中。
- 使能 SPI1 的全局中断(如果用中断模式)或者 DMA(如果用 DMA 模式),这个项目里我先用阻塞模式,简单可靠。
生成代码之后,HAL 库会初始化好 SPI 和 GPIO,我们只需要写 MRAM 的读写函数就行。
3.2 底层 SPI 读写函数的封装
HAL 库提供了HAL_SPI_Transmit和HAL_SPI_Receive,但直接调用有点繁琐,我一般封装一个字节交换函数:
uint8_t SPI_Transfer(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, 100); return rxData; }然后基于这个函数实现 MRAM 的读、写、读状态等操作:
void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_Transfer(0x06); MRAM_CS_HIGH(); } void MRAM_Write(uint32_t addr, uint8_t *data, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_Transfer(0x02); SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { SPI_Transfer(data[i]); } MRAM_CS_HIGH(); MRAM_WaitWriteComplete(); } void MRAM_Read(uint32_t addr, uint8_t *data, uint32_t len) { MRAM_CS_LOW(); SPI_Transfer(0x03); SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { data[i] = SPI_Transfer(0xFF); } MRAM_CS_HIGH(); }这里有几个细节要注意。第一,MRAM_CS_LOW()和MRAM_CS_HIGH()是宏定义,直接操作 GPIO:
#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)第二,写操作之后调用了MRAM_WaitWriteComplete(),确保数据真正写入。虽然 MRAM 写入很快,但保险起见还是等一下。
第三,读操作的时候发送 0xFF 作为 dummy 字节,因为 SPI 是全双工的,你要读数据就必须发东西出去,发什么无所谓,0xFF 是惯例。
3.3 数据记录与读取的完整流程
有了底层读写函数,接下来实现业务逻辑。这个项目里我要记录的是设备的运行参数,包括累计运行时间、启停次数、故障代码、校准系数。数据结构定义如下:
typedef struct { uint32_t total_runtime; // 累计运行时间,秒 uint32_t start_count; // 启动次数 uint16_t fault_code; // 最近故障代码 uint16_t reserved; // 对齐 float calib_k; // 校准系数 K float calib_b; // 校准系数 B uint16_t crc; // CRC16 校验 } DeviceData_t;写入流程是这样的:先更新内存里的结构体,计算 CRC16,然后把整个结构体写到 MRAM 的配置区。读取流程反过来:从 MRAM 读出结构体,校验 CRC,如果通过就使用,不通过就用默认值并标记异常。
void SaveDeviceData(DeviceData_t *data) { >void WriteLog(uint8_t *log, uint8_t len) { uint32_t ptr; MRAM_Read(MRAM_LOG_PTR_ADDR, (uint8_t*)&ptr, 4); if (ptr < MRAM_LOG_ADDR || ptr >= MRAM_LOG_ADDR + MRAM_LOG_SIZE - len) { ptr = MRAM_LOG_ADDR; } MRAM_Write(ptr, log, len); ptr += len; MRAM_Write(MRAM_LOG_PTR_ADDR, (uint8_t*)&ptr, 4); }这里有个细节:写指针本身也要存在 MRAM 里,而且每次写日志都要更新指针,所以指针的写入频率和日志一样高。MRAM 的寿命足够长,不用担心写坏,但如果你用 Flash 做这个事,指针区域很快就会被写爆,必须做磨损均衡。
3.4 掉电保护与数据完整性设计
工业设备最怕的就是掉电丢数据。MRAM 本身掉电不丢数据,但如果你在写的过程中掉电,可能会写到一半,导致数据不完整。解决办法有两个:
第一个是双备份加版本号。配置区开两块,每块前面加一个版本号,写的时候先写备份区,再写主区,读的时候比较两个版本号,取新的那个。如果主区 CRC 不通过,就用备份区恢复。
第二个是掉电检测中断。STM32L162ZE 有 PVD(可编程电压检测器),可以设置一个阈值,比如 2.9V,当电压降到阈值以下时触发中断,在中断里紧急保存关键数据。MRAM 写入快,几微秒就能写完,所以只要检测及时,完全来得及。
我一般两个方案一起用:PVD 中断里保存关键数据,双备份保证即使没来得及保存也能恢复。PVD 的配置在 CubeMX 里使能,中断优先级设高一点,确保能及时响应。
void PVD_IRQHandler(void) { if (__HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)) { // 电压低于阈值,紧急保存 SaveDeviceData(&g_device_data); __HAL_PWR_CLEAR_FLAG(PWR_FLAG_PVDO); } }4. 常见问题排查与实操避坑经验
4.1 SPI 通信失败的排查思路
SPI 调不通是最常见的问题,我总结了一套排查流程,按顺序来基本能定位到问题。
第一步,检查硬件连接。用万用表量一下 SCK、MISO、MOSI、CS 四根线是否导通,有没有虚焊。MRAM 的 VCC 是不是 3.3V,GND 是不是共地。这一步看似简单,但我遇到过好几次是杜邦线接触不良导致的,换了线就好了。
第二步,用逻辑分析仪抓波形。这是最有效的手段。抓 SCK、MOSI、CS 三根线,看 CS 拉低之后 SCK 有没有输出,MOSI 上的数据是不是你发的命令码。如果 CS 没拉低,检查 GPIO 配置;如果 SCK 没输出,检查 SPI 使能和时钟配置;如果数据不对,检查 SPI 模式(CPOL/CPHA)和位序。
第三步,读设备 ID 验证通信。MR25H40CDF 有一个 RDID 命令(0x9F),可以读出厂商 ID 和设备 ID。厂商 ID 是 0xE0,设备 ID 是 0x22。如果读出来不对,说明通信有问题;如果读出来是对的,说明底层通了,问题在后面的逻辑。
第四步,降低 SPI 时钟。如果高速下通信不稳定,把 Prescaler 调大,降到 1MHz 甚至 500KHz 试试。如果降频之后正常了,说明是信号完整性问题,需要检查 PCB 走线和端接电阻。
4.2 写入不成功或数据读回错误的处理
写入不成功一般有几个原因。第一个是忘记发 WREN。MRAM 和 Flash 一样,写之前必须发写使能命令,否则写操作会被忽略。我刚开始用的时候也犯过这个错,调了半天才发现少了一句MRAM_WriteEnable()。
第二个是块保护位被设置。状态寄存器的 BP0 和 BP1 如果被置位,对应区域会被保护,写不进去。解决办法是发 WRSR 命令清除保护位,或者干脆在初始化的时候检查一下状态寄存器,确保保护位是 0。
第三个是写完之后没有等待。虽然 MRAM 写入很快,但如果你写完立刻读,可能读到的还是旧数据。加一个MRAM_WaitWriteComplete()就能解决。
第四个是地址越界。前面说过地址是 19 位的,如果你传了一个超过 0x7FFFF 的地址,行为不确定。代码里加一个断言或者与运算,确保地址在合法范围内。
数据读回错误的话,先检查 CRC 校验是否通过。如果 CRC 不通过,可能是写入过程中掉电了,或者 SPI 通信受干扰了。解决办法是重读几次,如果还是不对,就用备份数据恢复。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读设备 ID 返回 0x00 或 0xFF | SPI 未通信 | 逻辑分析仪抓波形 | 检查 CS、SCK、MOSI 连接和配置 |
| 写入后读回数据不变 | 未发 WREN | 检查代码流程 | 写操作前加 MRAM_WriteEnable() |
| 写入后读回数据部分错误 | SPI 时钟太高 | 降低 Prescaler | 降到 4MHz 或 1MHz 测试 |
| 状态寄存器 WIP 一直为 1 | 写周期未完成或芯片异常 | 读状态寄存器 | 等待或复位芯片 |
| 掉电后数据丢失 | 写入未完成 | 检查 PVD 配置 | 使能 PVD 中断,双备份 |
| 日志区域数据混乱 | 写指针越界 | 检查指针逻辑 | 加边界判断,环形缓冲 |
4.4 几个我踩过的坑和独家经验
第一个坑是CS 信号的时序。SPI 协议要求 CS 拉低之后才能发时钟,CS 拉高之前时钟必须停止。我遇到过因为 GPIO 操作太快,CS 还没稳定 SCK 就开始了,导致第一个位丢失。解决办法是在 CS 拉低之后加一个微秒级的延时,或者用硬件 NSS(虽然不灵活但时序有保证)。
第二个坑是MRAM 的睡眠模式。MR25H40CDF 支持 SLEEP 命令进入低功耗模式,电流降到几微安。但进入睡眠之后,普通的读写命令不响应,必须先发 WAKE 命令唤醒。我有一次调试的时候不小心发了 SLEEP,然后怎么读都是 0xFF,查了半天手册才发现要唤醒。所以如果你用低功耗模式,一定要在读写之前加唤醒逻辑。
第三个坑是电源去耦。MRAM 虽然功耗低,但写入瞬间的电流波动还是有的。如果电源去耦没做好,写入的时候电压跌落可能导致写入失败。我一般会在 MRAM 的 VCC 引脚旁边放一个 100nF 和一个 1uF 的电容,尽量靠近引脚。
第四个经验是批量写入用 DMA。如果你要写大量数据,比如一次写几 KB 的日志,用阻塞模式会占 CPU 很久。改成 DMA 模式之后,CPU 可以去做别的事,传输完成中断里再处理后续逻辑。STM32L162ZE 的 SPI1 支持 DMA,配置起来也不复杂,值得花时间搞一下。
第五个经验是定期做读写测试。工业设备出厂前,我一般会做一个老化测试,连续读写 MRAM 几万次,验证稳定性。测试的时候覆盖所有区域,包括边界地址,确保没有越界或者漏写的情况。这个测试能提前发现很多潜在问题,比现场出故障再排查划算得多。
4.5 性能优化与低功耗设计建议
如果你的项目对功耗敏感,比如电池供电的传感器节点,MRAM 的低功耗特性可以好好利用。几个优化点:
第一,不读写的时候让 MRAM 进入睡眠。SLEEP 命令之后电流从几毫安降到几微安,对电池寿命提升明显。但要注意唤醒时间,WAKE 之后需要等一小段时间才能正常读写,具体看手册。
第二,降低 SPI 时钟。SPI 时钟越低,功耗越低。如果数据量不大,1MHz 甚至 500KHz 完全够用,没必要跑 8MHz。
第三,批量操作。把多次小数据写入合并成一次大数据写入,减少 CS 拉低拉高的次数,也减少唤醒次数。
第四,用 DMA 减少 CPU 唤醒时间。CPU 在 DMA 传输期间可以进入低功耗模式,传输完成中断再唤醒,整体功耗更低。
这些优化我在几个电池供电的项目里都用过,效果不错。一个 2000mAh 的电池,优化前只能跑三个月,优化后能跑一年以上。
5. 方案扩展与不同场景的适配思路
5.1 从单机记录到多节点组网的扩展
这套 MRAM 加 STM32 的方案,单机用没问题,但如果要做多节点组网,比如工厂里的设备监控网络,就需要考虑数据汇总和远程读取。我的做法是在 STM32 上跑一个简单的 Modbus RTU 从机协议,上位机通过 RS485 轮询每个节点,读取 MRAM 里的数据。MRAM 在这里充当本地缓存,即使网络断了,数据也不会丢,网络恢复后上位机可以补读。
Modbus 寄存器和 MRAM 地址的映射关系可以做成一张表,比如保持寄存器 0x0000 对应累计运行时间,0x0002 对应启动次数,等等。上位机读寄存器的时候,STM32 从 MRAM 里读数据返回。这样上位机不需要知道 MRAM 的具体地址,只需要按寄存器地址访问就行,耦合度低,后期维护方便。
5.2 在数据采集器里的应用
数据采集器是另一个很适合 MRAM 的场景。采集器一般会以很高的频率采集传感器数据,然后打包上传。如果网络不稳定,数据需要本地缓存。用 Flash 做缓存的话,擦写寿命和写入速度都是瓶颈;用 MRAM 就没这个问题,采集一次写一次,完全跟得上。
我做过一个振动采集器,采样率 10KHz,每次采集 1024 个点,打包成 2KB 的数据块写入 MRAM。MRAM 的写入速度是 SPI 时钟决定的,8MHz 下写 2KB 大概 2ms,完全能满足 10KHz 的采集节奏。如果用 Flash,光擦除就要几十毫秒,根本来不及。
5.3 与其它非易失存储方案的对比
最后做一个横向对比,方便你在选型的时候参考:
| 特性 | MRAM (MR25H40CDF) | EEPROM | NOR Flash | FRAM |
|---|---|---|---|---|
| 写入速度 | 极快(纳秒级) | 慢(毫秒级) | 慢(毫秒级,需擦除) | 快(纳秒级) |
| 擦写寿命 | 10^14 次 | 10^6 次 | 10^5 次 | 10^14 次 |
| 写入方式 | 字节随机写 | 字节随机写 | 块擦除后写 | 字节随机写 |
| 容量 | 4Mbit | 通常 1Mbit 以下 | 1Mbit 到 1Gbit | 通常 1Mbit 以下 |
| 功耗 | 低 | 低 | 中 | 极低 |
| 成本 | 中高 | 低 | 低 | 高 |
| 工业适用性 | 优秀 | 一般 | 一般 | 优秀 |
从表里可以看出,MRAM 在写入速度、寿命、写入方式上都有明显优势,缺点是容量和成本。如果你的场景是频繁写入小数据、对可靠性要求高、容量需求不大,MRAM 是非常合适的选择。如果容量需求大、写入不频繁,NOR Flash 更划算。如果对功耗极其敏感、容量需求小,FRAM 可以考虑,但成本更高。
我在实际项目里的体会是,MRAM 最大的价值不是某一个参数特别突出,而是它在多个维度上都没有明显短板。你不用为了寿命牺牲速度,也不用为了速度牺牲可靠性,软件层面也简单很多。对于工业嵌入式项目来说,这种省心是很有价值的,因为现场维护的成本远高于器件本身的差价。