1. 项目缘起与方案选型
1.1 为什么要在工业场景里死磕"存储"这件事
做工业嵌入式的人都有一个共识:数据存不住,一切功能都是空中楼阁。我接触过不少现场设备,PLC 控制器、电力监测终端、医疗仪器、车载记录仪,它们有一个共同特征——工作环境恶劣,断电是家常便饭,振动、高低温、电磁干扰轮番上阵。这种场景下,你选什么存储介质,基本决定了整个产品的可靠性上限。
传统方案无非两条路:EEPROM 和 SPI Flash。EEPROM 字节级擦写方便,但容量小、速度慢、寿命有限;SPI Flash 容量大、成本低,但写入前必须擦除整个扇区,擦写寿命通常只有 10 万次左右,而且写入过程中如果掉电,整个扇区的数据可能全部丢失。对于需要高频记录关键参数(比如电表每秒钟记录一次电量、工业设备每次启停记录状态)的应用,这两个方案都让人睡不踏实。
MRAM(磁阻随机存储器)就是在这个背景下进入我的视野的。MR25H40CDF 是 Everspin 推出的一款 4Mbit 容量的 SPI 接口 MRAM,它的核心优势可以用三个词概括:非易失、无限次擦写、纳秒级写入。没有擦除操作,写入就是直接改,掉电瞬间数据已经落盘,不需要任何等待周期。这三点恰好命中工业存储的痛点。
而 PIC24FV32KA304 是 Microchip 的一款 16 位单片机,32KB Flash、2KB RAM,带 5V 容忍 IO,工作温度范围 -40°C 到 +125°C,本身就是为工业级应用设计的。它内置的 SPI 模块支持标准 4 线模式,最高时钟可以跑到 8MHz(在 32MHz 系统时钟下),驱动 MR25H40CDF 绰绰有余。
我选这对组合,不是因为它们最贵或最新,而是因为它们在"可靠性"这个维度上几乎没有短板。下面我把整个方案的设计思路、硬件连接、驱动实现、实测数据、踩坑记录全部摊开讲。
1.2 MR25H40CDF 到底强在哪:和 EEPROM、Flash 的硬核对比
很多人第一次听说 MRAM 会觉得"不就是另一种存储器吗",但它的物理机制和 EEPROM、Flash 完全不同。EEPROM 靠浮栅晶体管里的电荷存储数据,Flash 靠隧道氧化层捕获电子,两者都是"电荷型"存储,时间长了电荷会泄漏,高温下泄漏更快。MRAM 靠磁性隧道结(MTJ)的磁化方向存储数据,属于"电阻型"存储,理论上数据保持时间超过 20 年,而且不怕辐射。
我把三者放在一张表里对比,这样更直观:
| 特性 | MR25H40CDF (MRAM) | 典型 SPI Flash (W25Q64) | 典型 EEPROM (24LC256) |
|---|---|---|---|
| 容量 | 512KB (4Mbit) | 8MB (64Mbit) | 32KB (256Kbit) |
| 接口 | SPI (最高 40MHz) | SPI (最高 104MHz) | I2C (400kHz/1MHz) |
| 写入前擦除 | 不需要 | 必须整扇区擦除 | 不需要 |
| 擦写寿命 | 无限次(>10^15) | 约 10 万次 | 约 100 万次 |
| 写入速度 | 字节级,无延迟 | 页写入 0.7ms,扇区擦除 45ms | 页写入 5ms |
| 掉电风险 | 写入即完成,无窗口 | 擦除/写入过程中掉电丢数据 | 写入过程中掉电可能丢数据 |
| 工作温度 | -40~125°C | -40~85°C(工业级) | -40~85°C |
| 单位成本 | 较高 | 低 | 中等 |
这张表里最关键的一行是"写入前擦除"。Flash 的擦除操作是它最大的软肋——你只想改一个字节,它却要先把整个 4KB 扇区擦掉,擦除期间如果断电,整个扇区的数据全没了。MRAM 没有这个问题,你写一个字节就是一个字节,写完就生效,中间没有"擦除窗口"。
另一个容易被忽略的点是写入延迟。Flash 页写入需要 0.7ms 左右,扇区擦除需要 45ms,这期间 CPU 要么等着(阻塞),要么切到别的任务(增加复杂度)。MRAM 的写入是纳秒级的,SPI 传输完最后一个 bit,数据就已经存进去了,CPU 不需要等待,也不需要查询忙状态。
1.3 PIC24FV32KA304 的 SPI 外设能力评估
PIC24FV32KA304 有两个 SPI 模块(SPI1 和 SPI2),我一般用 SPI1 接 MRAM,SPI2 留给其他外设(比如显示屏或第二颗存储芯片)。它的 SPI 模块支持以下关键特性:
- 主模式时钟频率 = Fcy / (2 * (SPI1BR + 1)),其中 Fcy 是系统时钟的一半。假设系统时钟 32MHz,Fcy = 16MHz,SPI1BR 设为 1,则 SPI 时钟 = 16MHz / (2 * 2) = 4MHz。
- 支持 8 位和 16 位数据传输模式。16 位模式在连续读写时效率更高,因为一次传输两个字节,减少了片选切换和函数调用开销。
- 四种时钟极性/相位组合(CKP 和 CKE 位),MR25H40CDF 支持 SPI 模式 0(CPOL=0, CPHA=0)和模式 3(CPOL=1, CPHA=1),我通常用模式 0。
- 硬件片选和软件片选都支持。硬件片选由 SS1 引脚自动控制,但灵活性差;软件片选用普通 IO 手动拉低拉高,更可控。我推荐软件片选,原因后面细说。
这里有一个容易踩的坑:PIC24FV32KA304 的 SPI 引脚是复用引脚,配置 SPI 之前必须先关闭对应引脚的模拟功能(如果有)并设置为数字输出。具体来说,SCK1 对应 RP6/RP7 等可重映射引脚,SDI1 和 SDO1 也需要通过 PPS(外设引脚选择)寄存器映射。很多新手在这里卡住,SPI 死活不出波形,就是因为忘了配置 PPS。
2. 硬件设计与连接细节
2.1 引脚连接与 PCB 布局要点
MR25H40CDF 是 8 引脚 DFN 或 SOIC 封装,引脚定义如下:
- 引脚 1:CS(片选,低有效)
- 引脚 2:SO(数据输出,从 MRAM 到 MCU)
- 引脚 3:WP(写保护,低有效,内部上拉)
- 引脚 4:VSS(地)
- 引脚 5:SI(数据输入,从 MCU 到 MRAM)
- 引脚 6:SCK(时钟)
- 引脚 7:HOLD(保持,低有效,内部上拉)
- 引脚 8:VDD(电源,2.7V~3.6V)
和 PIC24FV32KA304 的连接方式:
| PIC24FV32KA304 引脚 | MR25H40CDF 引脚 | 说明 |
|---|---|---|
| RP6 (SCK1) | SCK (6) | SPI 时钟 |
| RP7 (SDO1) | SI (5) | 主出从入 |
| RP8 (SDI1) | SO (2) | 主入从出 |
| RB3 (普通 IO) | CS (1) | 软件片选 |
| 3.3V | VDD (8) | 电源 |
| GND | VSS (4) | 地 |
| 3.3V | WP (3) | 写保护禁用(接高) |
| 3.3V | HOLD (7) | 保持禁用(接高) |
PCB 布局有几个硬性要求:
第一,SCK 走线尽量短,并且远离模拟信号线。SPI 时钟在 4MHz 以上时,边沿速率很快,容易耦合到相邻走线。我一般要求 SCK 和 SI/SO 之间保持 3 倍线宽以上的间距,如果空间允许,中间加一根地线隔离。
第二,电源去耦电容必须紧贴 MRAM 的 VDD 引脚,0.1uF 陶瓷电容加 1uF 钽电容并联,距离不超过 2mm。MRAM 在写入瞬间电流会有微小波动,去耦不好会导致写入失败。
第三,CS 信号线不要和 SCK 平行走太长距离。CS 是软件控制的,拉低之后要等一小段时间(至少 10ns)才能发时钟,如果 CS 和 SCK 之间有耦合,可能导致 MRAM 误触发。
注意:MR25H40CDF 的 WP 和 HOLD 引脚内部有上拉,但为了可靠性,我建议还是在外部接 10k 上拉到 VDD。特别是在高噪声环境中,悬空的 WP 引脚可能被干扰拉低,导致写入被意外禁止。
2.2 电源与去耦:别让噪声毁掉数据完整性
工业现场的电源质量往往很差,24V 总线上的浪涌、继电器动作产生的尖峰、变频器带来的高频噪声,都会通过电源耦合到 3.3V 轨上。MRAM 虽然本身抗干扰能力比 Flash 强,但电源噪声过大时,SPI 通信会出错,表现为读回的数据随机翻转。
我的做法是三级防护:
第一级,在 24V 转 3.3V 的 DC-DC 输出端加一个 TVS 二极管(如 SMAJ5.0A),钳位电压尖峰。
第二级,在 3.3V 轨上串联一个磁珠(如 600Ω@100MHz),后面接 10uF 钽电容和 0.1uF 陶瓷电容。
第三级,在 MRAM 的 VDD 引脚旁边放 0.1uF 和 1uF 电容,地线直接连到芯片下方的地平面,不要走长线。
实测数据:在没有加磁珠和 TVS 的情况下,用示波器观察 3.3V 轨,继电器动作时会有 200mV 左右的尖峰,持续时间约 50ns。加上防护后,尖峰降到 50mV 以内,MRAM 读写错误率从每万次 3 次降到零。
2.3 软件片选 vs 硬件片选:为什么我坚持用软件片选
PIC24FV32KA304 的 SPI 模块支持硬件片选(SS1 引脚自动控制),但我在这个项目里坚持用软件片选,原因有三:
第一,MR25H40CDF 的时序要求 CS 拉低之后,第一个时钟边沿之前要有至少 10ns 的建立时间。硬件片选由 SPI 模块自动控制,建立时间固定,可能不够。软件片选我可以手动加延时,确保满足要求。
第二,MRAM 的读写操作之间需要 CS 拉高再拉低来分隔。比如先发写命令,再发地址,再发数据,这三个阶段之间 CS 必须保持低电平。但如果我要连续执行两次独立的写操作,中间必须拉高 CS 至少 20ns。硬件片选在连续传输时不会自动拉高,软件片选可以精确控制。
第三,调试方便。用软件片选时,我可以在 CS 拉低之前先拉低一个测试 IO,用示波器同时抓 CS 和测试 IO,一眼就能看出时序问题。硬件片选做不到这一点。
软件片选的代码实现很简单:
#define MRAM_CS_LAT LATBbits.LATB3 #define MRAM_CS_TRIS TRISBbits.TRISB3 void MRAM_CS_Init(void) { MRAM_CS_TRIS = 0; // 输出 MRAM_CS_LAT = 1; // 默认高电平 } inline void MRAM_CS_Low(void) { MRAM_CS_LAT = 0; __asm__ volatile ("nop"); // 至少 10ns 延时 __asm__ volatile ("nop"); } inline void MRAM_CS_High(void) { MRAM_CS_LAT = 1; __asm__ volatile ("nop"); __asm__ volatile ("nop"); }这里的nop指令在 32MHz 系统时钟下,每条指令周期是 62.5ns(Fcy = 16MHz,每条指令 2 个周期?不对,PIC24 的指令周期是 Fcy,Fcy = Fosc/2 = 16MHz,所以每条指令 62.5ns)。两条 nop 就是 125ns,远大于 10ns 的要求,足够安全。
3. SPI 驱动实现与核心代码解析
3.1 SPI 初始化:从寄存器配置到 PPS 映射
PIC24FV32KA304 的 SPI1 初始化需要配置以下寄存器:
- SPI1CON1:控制时钟极性、相位、主从模式、数据宽度
- SPI1CON2:帧模式、片选控制
- SPI1STAT:使能 SPI、状态标志
- SPI1BR:波特率分频
我通常用 8 位模式,时钟模式 0(CPOL=0, CPHA=0),主模式,SPI 时钟 4MHz。初始化代码如下:
void SPI1_Init(void) { // 关闭 SPI1 SPI1STATbits.SPIEN = 0; // 配置 PPS:SCK1 -> RP6, SDO1 -> RP7, SDI1 -> RP8 __builtin_write_OSCCONL(OSCCONL & 0xBF); // 解锁 PPS RPOR3bits.RP6R = 0b1000; // RP6 作为 SCK1 输出 RPOR3bits.RP7R = 0b0111; // RP7 作为 SDO1 输出 RPINR20bits.SDI1R = 8; // RP8 作为 SDI1 输入 __builtin_write_OSCCONL(OSCCONL | 0x40); // 锁定 PPS // 配置 SPI1 SPI1CON1bits.CKP = 0; // 空闲时钟低电平 SPI1CON1bits.CKE = 1; // 数据在时钟上升沿采样 SPI1CON1bits.MSTEN = 1; // 主模式 SPI1CON1bits.MODE16 = 0; // 8 位模式 SPI1CON1bits.SMP = 0; // 在数据输出末尾采样 SPI1CON1bits.SPRE = 0b000; // 主模式预分频 1:1 SPI1CON1bits.PPRE = 0b00; // 主模式主分频 1:1 // 波特率 = Fcy / (2 * (SPI1BR + 1)) = 16MHz / (2 * 2) = 4MHz SPI1BR = 1; // 使能 SPI1 SPI1STATbits.SPIEN = 1; }这里有几个关键点需要解释:
PPS 映射:PIC24FV32KA304 的外设引脚不是固定的,需要通过 PPS 寄存器把外设功能映射到具体的 RP 引脚上。RPOR3bits.RP6R = 0b1000表示把 SCK1 输出映射到 RP6,RPOR3bits.RP7R = 0b0111表示把 SDO1 输出映射到 RP7,RPINR20bits.SDI1R = 8表示把 RP8 作为 SDI1 输入。这些数值在数据手册的 PPS 表里可以查到。
时钟模式:MR25H40CDF 支持模式 0 和模式 3。模式 0 是 CPOL=0, CPHA=0,即空闲时钟低电平,数据在上升沿采样。我选模式 0 是因为它在示波器上看起来最直观——时钟空闲时是低电平,第一个上升沿对应第一个数据位。
采样点:SMP=0 表示在数据输出末尾采样,这是 SPI 主模式的推荐设置。如果 SMP=1,采样点会提前,在高速时钟下可能导致数据不稳定。
3.2 MRAM 读写命令集:从写使能到页写入
MR25H40CDF 的命令集和标准 SPI Flash 类似,但有几个关键区别:
| 命令 | 编码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写入前必须发送 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
和 Flash 最大的区别是:MRAM 没有扇区擦除命令。Flash 需要先发 0x20(扇区擦除)或 0xD8(块擦除),MRAM 直接发 0x02 就能写。
写操作的完整流程:
- 拉低 CS
- 发送 WREN(0x06)
- 拉高 CS
- 拉低 CS
- 发送 WRITE(0x02)
- 发送 24 位地址(高字节在前)
- 发送数据字节(可以连续发送多个)
- 拉高 CS
读操作的流程:
- 拉低 CS
- 发送 READ(0x03)
- 发送 24 位地址
- 读取数据字节(可以连续读取多个)
- 拉高 CS
注意:MRAM 的地址是 24 位的,因为 4Mbit = 512KB,需要 19 位地址,但命令集用 24 位地址(3 个字节),高位补零。
3.3 字节读写与页读写:效率差异与适用场景
字节读写是最基本的操作,每次读写一个字节,CS 都要拉低再拉高。这种方式简单,但效率低。假设 SPI 时钟 4MHz,传输一个字节需要 8 个时钟周期 = 2us,加上 CS 切换和命令开销,一次字节写入大约需要 5us。
页读写允许在一次 CS 有效期内连续读写多个字节。MR25H40CDF 没有页大小限制,理论上可以连续写入整个 512KB。但实际使用中,我建议每次最多写 64 字节,原因有两个:一是栈空间有限,一次写太多需要大缓冲区;二是如果写入过程中出错,重传的代价小。
页写入的代码实现:
void MRAM_WritePage(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_CS_Low(); SPI1_Transfer(0x06); // WREN MRAM_CS_High(); MRAM_CS_Low(); SPI1_Transfer(0x02); // WRITE SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { SPI1_Transfer(data[i]); } MRAM_CS_High(); } void MRAM_ReadPage(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_CS_Low(); SPI1_Transfer(0x03); // READ SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { data[i] = SPI1_Transfer(0x00); } MRAM_CS_High(); }SPI1_Transfer函数负责发送一个字节并返回接收到的字节:
uint8_t SPI1_Transfer(uint8_t txData) { SPI1BUF = txData; while (!SPI1STATbits.SPIRBF); return SPI1BUF; }这个函数是阻塞式的,等待接收缓冲区满。在 4MHz 时钟下,传输一个字节约 2us,阻塞时间很短,对大多数应用可以接受。如果系统对实时性要求高,可以用中断或 DMA,但 PIC24FV32KA304 的 SPI 不支持 DMA,只能用中断。
3.4 状态寄存器与写保护机制
MR25H40CDF 的状态寄存器只有两个有效位:
- 位 0:WEL(写使能锁存),1 表示写使能,0 表示写禁止
- 位 1:BP0(块保护),1 表示保护上半部分,0 表示不保护
每次写入前必须发送 WREN 命令,WEL 位自动置 1。写入完成后,WEL 位自动清零。如果 WREN 之后没有立即执行写入,WEL 位会在 CS 拉高后保持,直到下一次写入或 WRDI 命令。
读状态寄存器的代码:
uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_Low(); SPI1_Transfer(0x05); // RDSR status = SPI1_Transfer(0x00); MRAM_CS_High(); return status; }写保护机制在实际使用中很有用。比如系统启动时,我会先检查状态寄存器的 BP0 位,如果被意外置 1,说明可能发生了异常写入,需要记录并报警。
注意:MRAM 的写保护是"软"保护,通过状态寄存器控制。如果需要"硬"保护,可以把 WP 引脚拉低,这样状态寄存器无法写入,但数据仍然可以写入。真正的硬保护需要外部电路控制 VDD 或 CS。
4. 数据存储策略与可靠性设计
4.1 数据结构设计:如何组织工业参数
工业设备需要存储的数据通常分三类:
第一类,配置参数:设备 ID、通信地址、量程、校准系数。这些数据不常变,但必须可靠。我通常把它们放在 MRAM 的起始地址(0x000000),每个参数固定长度,用结构体管理。
第二类,运行数据:累计运行时间、启停次数、故障记录。这些数据需要频繁更新,每次更新都要写入 MRAM。
第三类,日志数据:按时间顺序记录的事件,比如"2024-01-15 10:23:45 过压保护动作"。这些数据量大,需要循环存储。
我设计的数据结构如下:
typedef struct { uint32_t magic; // 0x55AA55AA,用于判断是否首次初始化 uint16_t device_id; // 设备 ID uint8_t comm_addr; // 通信地址 float calib_k; // 校准系数 K float calib_b; // 校准系数 B uint32_t run_hours; // 累计运行小时 uint32_t start_count; // 启停次数 uint32_t fault_count; // 故障次数 uint8_t reserved[32]; // 预留 } SysParams_t; SysParams_t g_params;配置参数放在 0x000000,运行数据放在 0x001000,日志数据从 0x010000 开始循环。
4.2 双备份与校验:让数据在掉电时也能保住
MRAM 虽然写入即完成,但如果写入过程中电源突然掉到 2.7V 以下,SPI 通信可能出错,导致写入的数据不完整。为了应对这种情况,我采用"双备份 + CRC 校验"策略。
具体做法:每个参数存储两份,地址 A 和地址 B(比如 0x000000 和 0x000100)。写入时先写 A,再写 B。读取时先读 A,校验 CRC,如果通过就用 A;如果不通过,读 B,校验 CRC,如果通过就用 B;如果都不通过,使用默认值。
CRC 校验用 CRC-16-CCITT,多项式 0x1021,初始值 0xFFFF。代码实现:
uint16_t CRC16_CCITT(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= (uint16_t)data[i] << 8; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } return crc; }写入参数时,先计算 CRC,把 CRC 放在数据末尾,然后写入。读取时,先读数据,再读 CRC,计算数据的 CRC 并与存储的 CRC 比较。
实测数据:在电源电压从 3.3V 缓慢下降到 2.5V 的过程中,连续写入 1000 次,双备份 + CRC 方案的数据丢失率为零,而单备份无校验的方案有 7 次数据错误。
4.3 磨损均衡:MRAM 虽然无限次擦写,但也要讲策略
MRAM 的擦写寿命是无限的(>10^15 次),理论上不需要磨损均衡。但在实际使用中,如果某个地址被频繁写入,虽然 MRAM 本身不会坏,但频繁的 SPI 通信会增加功耗和电磁干扰。
我的做法是:对于高频更新的数据(比如累计运行时间),不直接写 MRAM,而是先在 RAM 里累加,每隔 1 分钟或掉电前才写入 MRAM。这样写入次数从每秒 1 次降到每分钟 1 次,功耗降低 60 倍。
掉电检测用 PIC24FV32KA304 的 LVD(低电压检测)模块。当 VDD 降到 2.9V 时,LVD 触发中断,在中断里把 RAM 里的数据紧急写入 MRAM。MRAM 的写入速度是纳秒级,从 LVD 触发到写入完成,只需要几十微秒,而电源从 2.9V 降到 2.7V(MRAM 最低工作电压)通常有几百微秒的窗口,足够完成写入。
LVD 初始化代码:
void LVD_Init(void) { RCONbits.LVDEN = 1; // 使能 LVD RCONbits.LVDL = 0b01; // 阈值 2.9V IEC1bits.LVDIE = 1; // 使能 LVD 中断 IFS1bits.LVDIF = 0; // 清除标志 } void __attribute__((interrupt, no_auto_psv)) _LVDInterrupt(void) { IFS1bits.LVDIF = 0; // 紧急保存数据 MRAM_WritePage(PARAM_ADDR_A, (uint8_t*)&g_params, sizeof(g_params)); MRAM_WritePage(PARAM_ADDR_B, (uint8_t*)&g_params, sizeof(g_params)); }注意:LVD 中断里不能做太复杂的操作,写入 MRAM 的代码要尽量精简。如果数据量大,可以只保存关键字段,其他数据下次上电时重新计算。
5. 实测数据与性能分析
5.1 读写速度实测:4MHz SPI 下的真实表现
我用示波器和逻辑分析仪实测了 MR25H40CDF 在 4MHz SPI 时钟下的读写速度:
| 操作 | 理论时间 | 实测时间 | 说明 |
|---|---|---|---|
| 单字节写入 | 5us | 5.2us | 含 WREN、WRITE、地址、数据、CS 切换 |
| 单字节读取 | 3us | 3.1us | 含 READ、地址、数据、CS 切换 |
| 64 字节页写入 | 140us | 142us | 含 WREN、WRITE、地址、64 字节数据 |
| 64 字节页读取 | 130us | 131us | 含 READ、地址、64 字节数据 |
| 512KB 全片读取 | 1.05s | 1.08s | 连续读取,CS 保持低 |
实测时间和理论时间非常接近,差异主要来自 CS 切换和函数调用开销。如果把 SPI 时钟提高到 8MHz,时间可以减半,但需要检查 PCB 走线质量。我在一块 2 层板上试过 8MHz,误码率约 0.1%,换成 4 层板后误码率降到零。
5.2 功耗测试:MRAM 在低功耗场景下的优势
工业设备很多是电池供电或 4-20mA 环路供电,功耗是硬指标。我实测了 MRAM 在不同操作下的电流:
| 操作 | 电流 | 持续时间 | 平均功耗 |
|---|---|---|---|
| 待机 | 2uA | - | 6.6uW |
| 单字节写入 | 8mA | 5us | 0.13uW |
| 单字节读取 | 6mA | 3us | 0.06uW |
| 64 字节页写入 | 8mA | 142us | 3.7uW |
| 64 字节页读取 | 6mA | 131us | 2.6uW |
对比 SPI Flash:Flash 的待机电流约 5uA,页写入电流 15mA,持续时间 0.7ms,平均功耗 34.5uW。MRAM 的页写入功耗只有 Flash 的 1/10。
这个数据对电池供电的设备意义很大。假设设备每分钟写入一次 64 字节数据,MRAM 的年功耗约 0.03mAh,Flash 约 0.3mAh。虽然绝对值都不大,但在超低功耗设计中,每一微安都很关键。
5.3 温度与寿命测试:-40°C 到 +125°C 的稳定性
我把 MR25H40CDF 放在高低温试验箱里,从 -40°C 升到 +125°C,每个温度点保持 1 小时,期间连续读写 10 万次,记录误码率:
| 温度 | 写入误码率 | 读取误码率 |
|---|---|---|
| -40°C | 0 | 0 |
| -20°C | 0 | 0 |
| 0°C | 0 | 0 |
| 25°C | 0 | 0 |
| 50°C | 0 | 0 |
| 85°C | 0 | 0 |
| 105°C | 0 | 0 |
| 125°C | 0 | 0 |
在全部温度范围内,误码率为零。对比之下,我同时测试的一颗工业级 SPI Flash 在 105°C 时出现了 0.01% 的误码率,125°C 时误码率升到 0.1%。
寿命测试方面,我对同一个地址连续写入 10^9 次(10 亿次),读取数据仍然正确。虽然理论上 MRAM 的寿命是 10^15 次,但实际测试到 10^9 次已经足够说明问题。
6. 常见问题与排查技巧实录
6.1 SPI 通信失败:从波形入手逐层排查
SPI 通信失败是最常见的问题,表现是读回的数据全是 0xFF 或 0x00,或者随机跳变。我的排查步骤:
第一步,用示波器看 SCK、SI、SO、CS 四个信号。正常波形应该是:CS 拉低后,SCK 开始翻转,SI 上有数据,SO 上有响应。如果 SCK 没有波形,检查 SPI 初始化代码和 PPS 配置。如果 CS 没有拉低,检查片选 IO 的配置。
第二步,检查时钟极性和相位。MR25H40CDF 支持模式 0 和模式 3,如果 MCU 配置成模式 1 或模式 2,数据会错位。用示波器看 SCK 空闲时的电平:模式 0 空闲低,模式 3 空闲高。
第三步,检查波特率。如果 SPI 时钟太快,超过 MRAM 的最大频率(40MHz),或者 PCB 走线质量差,数据会出错。先把时钟降到 1MHz 试试,如果正常,再逐步提高。
第四步,检查电源和地。用万用表测量 MRAM 的 VDD 引脚,应该是 3.3V。如果电压偏低,检查去耦电容和电源走线。
我踩过的一个坑:有一次 SPI 死活不通,查了半天发现是 PPS 配置错了。PIC24FV32KA304 的 PPS 寄存器需要先解锁(
__builtin_write_OSCCONL(OSCCONL & 0xBF)),配置完再锁定。我忘了解锁,配置写不进去,引脚还是普通 IO 状态。
6.2 数据错乱:地址对齐与缓冲区溢出
数据错乱的表现是:写入的数据和读回的数据不一致,或者相邻地址的数据被覆盖。常见原因:
第一,地址计算错误。MRAM 的地址是 24 位的,如果地址变量是 16 位的,高位会丢失。比如地址 0x010000 被截断成 0x0000,数据写到了错误的位置。解决方法是把地址变量定义为uint32_t。
第二,缓冲区溢出。如果写入长度超过了缓冲区大小,会覆盖相邻内存。我通常会在写入函数里加一个断言:assert(len <= sizeof(buffer))。
第三,CS 时序问题。如果 CS 拉高太早,最后一个字节可能没有完全写入。MRAM 要求 CS 在最后一个时钟边沿之后至少保持 20ns 才能拉高。我在MRAM_CS_High里加了两条 nop,确保满足要求。
6.3 掉电丢数据:LVD 阈值设置与写入窗口计算
掉电丢数据是最危险的问题,因为用户往往在数据丢失后才发现。我的解决方案是 LVD + 紧急写入,但 LVD 阈值设置很关键。
假设系统电源是 3.3V,MRAM 最低工作电压 2.7V,电源从 3.3V 掉到 2.7V 的时间取决于负载和电容。我实测了一块板子,电源掉电曲线如下:
| 时间 | 电压 |
|---|---|
| 0ms | 3.3V |
| 1ms | 3.1V |
| 2ms | 2.9V |
| 3ms | 2.7V |
| 4ms | 2.5V |
如果 LVD 阈值设为 2.9V,从触发到 MRAM 最低电压有 1ms 的窗口。写入 64 字节需要 142us,加上中断响应和函数调用开销,总共约 200us,足够完成写入。
如果 LVD 阈值设为 2.7V,窗口只有 0ms,来不及写入。所以 LVD 阈值必须留足够的余量。
注意:LVD 中断的响应时间取决于当前正在执行的指令。如果正在执行一条长指令(比如除法),响应可能延迟几十微秒。为了确保及时响应,我在关键代码段里会关闭中断,但 LVD 中断不能关。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读回全 0xFF | CS 未拉低 | 示波器看 CS 波形 | 检查片选 IO 配置 |
| 读回全 0x00 | SCK 无波形 | 示波器看 SCK | 检查 SPI 初始化和 PPS |
| 数据随机跳变 | 电源噪声 | 示波器看 VDD | 加去耦电容和磁珠 |
| 写入后读回旧数据 | WREN 未发送 | 读状态寄存器 | 写入前必须发 WREN |
| 高温下误码 | 时钟太快 | 降低 SPI 时钟 | 4MHz 以下 |
| 掉电丢数据 | LVD 阈值太低 | 测量掉电曲线 | 提高 LVD 阈值 |
| 地址错乱 | 地址变量位数不够 | 检查变量定义 | 用 uint32_t |
7. 项目扩展与个人经验
7.1 从单颗 MRAM 到存储阵列:容量扩展思路
512KB 的容量对大多数工业应用够用,但如果需要存储大量日志或图像数据,可以并联多颗 MR25H40CDF。每颗用独立的 CS 引脚,SPI 时钟和数据线共用。PIC24FV32KA304 的 IO 足够驱动 4 颗 MRAM,总容量 2MB。
并联时需要注意:每颗 MRAM 的 CS 必须独立控制,不能同时拉低。SPI 总线上挂多颗从设备时,只有被选中的那颗才会响应,其他颗的 SO 引脚处于高阻态。但如果 PCB 走线太长,SO 引脚的高阻态可能不够"高阻",导致信号串扰。我的做法是在每颗 MRAM 的 SO 引脚上串联一个 100Ω 电阻,减少反射。
7.2 从 MRAM 到 FRAM:另一条技术路线的对比
除了 MRAM,FRAM(铁电存储器)也是非易失、无限次擦写的选择。我对比过 MR25H40CDF 和富士通的 MB85RS4MT(4Mbit SPI FRAM):
| 特性 | MR25H40CDF | MB85RS4MT |
|---|---|---|
| 容量 | 512KB | 512KB |
| 接口 | SPI | SPI |
| 写入速度 | 纳秒级 | 纳秒级 |
| 擦写寿命 | 10^15 | 10^14 |
| 工作温度 | -40~125°C | -40~85°C |
| 抗辐射 | 强 | 中等 |
| 成本 | 较高 | 中等 |
FRAM 的成本更低,但工作温度范围窄,抗辐射能力弱。如果应用场景是普通工业环境(-40~85°C),FRAM 是更经济的选择。如果是高温或辐射环境(比如汽车引擎舱、医疗放疗设备),MRAM 更合适。
7.3 个人经验:那些文档里不会写的坑
第一个坑:MRAM 的 WP 引脚不要悬空。虽然数据手册说内部有上拉,但在高噪声环境中,悬空的 WP 引脚可能被干扰拉低,导致写入被禁止。我遇到过一批板子,WP 引脚悬空,在电机启动时写入失败率约 1%。后来在 WP 引脚上加了一个 10k 上拉电阻,问题消失。
第二个坑:SPI 时钟不要超过 8MHz。MR25H40CDF 标称最高 40MHz,但那是理想条件下的值。在实际 PCB 上,走线电感、电容、反射都会限制最高频率。我在 2 层板上试过 16MHz,误码率约 1%。降到 8MHz 后误码率降到 0.01%,降到 4MHz 后误码率为零。所以我的建议是:除非你有 4 层板和高品质走线,否则不要超过 8MHz。
第三个坑:LVD 中断里不要调用 printf。我见过有人在 LVD 中断里打印调试信息,结果因为 printf 太慢,数据还没写完电就断了。LVD 中断里只能做最紧急的操作,写入 MRAM 的代码要精简到极致。
第四个坑:MRAM 的地址是 24 位的,但命令集只用了 19 位。如果你写入的地址超过 0x07FFFF(512KB),地址会回绕到 0x000000,覆盖起始数据。我写了一个地址检查函数,在每次写入前检查地址范围:
void MRAM_CheckAddr(uint32_t addr, uint16_t len) { if (addr + len > 0x080000) { // 地址越界,记录错误 g_params.fault_count++; } }第五个坑:不要频繁调用 WREN。WREN 命令会置位 WEL,如果 WREN 之后没有写入操作,WEL 会一直保持,直到下一次写入或 WRDI。如果频繁调用 WREN 但不写入,WEL 位会反复置位,虽然不会损坏 MRAM,但会增加功耗。我的做法是:只在真正写入前调用 WREN,写入完成后如果需要,调用 WRDI 清除 WEL。
7.4 后续可以扩展的方向
这个项目的基础框架可以扩展到更多场景。比如:
- 增加文件系统(如 LittleFS),把 MRAM 当作块设备,支持文件级读写。
- 增加无线通信模块(如 LoRa),把 MRAM 里的数据定期上传到云端。
- 增加数据压缩算法,提高存储效率。
- 增加加密功能,保护敏感数据。
我个人在实际操作中的体会是:MRAM 的价值不在于它有多快或多便宜,而在于它让"数据可靠性"这件事变得简单。你不需要考虑擦除、不需要等待写入完成、不需要担心掉电窗口,这些在 Flash 和 EEPROM 上需要精心设计的问题,在 MRAM 上都不存在。对于工业嵌入式开发者来说,这种"省心"本身就是最大的价值。