MRAM 这类存储介质在嵌入式圈子里讨论度一直不算高,很多人做数据存储第一反应还是 EEPROM 或者 SPI Flash。但如果你做过工业现场的设备,就会知道频繁写 Flash 有多难受——擦写寿命、写入延迟、掉电丢数据,每一个都是坑。我最近在一个电机控制项目里用MR25H40CDF配合dsPIC33FJ256GP710A做参数存储和运行日志记录,整套方案跑下来稳定性超出预期,所以把选型思路、SPI 驱动实现、读写策略这些内容整理一下。
这篇内容适合正在做工业控制、数据采集、嵌入式存储方案选型的工程师,也适合刚接触 dsPIC 系列或者第一次用 MRAM 的朋友。我会从为什么选 MRAM 而不是 EEPROM/Flash 讲起,然后一步步拆解硬件连接、SPI 初始化、读写函数实现、掉电保护策略,最后分享几个实际调试中踩过的坑。代码基于 dsPIC33FJ256GP710A 的 SPI 模块,用 C30 编译器,整体思路对 STM32 或其他平台同样有参考价值。
1. 为什么在这个项目里选了 MRAM 而不是 EEPROM 或 Flash
1.1 工业场景对存储的真实需求
先说说项目背景。这是一个工业电机控制器,需要存储几类数据:电机参数(PID 系数、额定电流、极对数等)、运行统计(累计运行时间、启停次数、故障记录)、以及实时采集的运行日志(每 100ms 记录一次电流、温度、转速)。参数数据量不大,几百字节;但运行日志是持续写入的,一天下来写入次数轻松上万次。
一开始我考虑的是外置 SPI Flash,比如常见的 W25Q 系列。但算了一笔账就发现问题了:Flash 的擦写寿命典型值是 10 万次,按每天 1 万次写入算,十天就把一个扇区写废了。虽然可以用磨损均衡算法分散写入,但实现复杂度上去了,而且 Flash 写入前必须先擦除整个扇区,写入延迟动辄几毫秒到几十毫秒,对于需要快速记录故障现场数据的场景来说太慢了。
EEPROM 的寿命好一些,典型 100 万次擦写,字节级写入不需要擦除。但 EEPROM 的写入速度也不快,而且容量普遍偏小,做日志存储很快就满了。更关键的是,EEPROM 写入过程中如果掉电,数据可能处于不确定状态。
MRAM(磁阻随机存取存储器)恰好解决了这几个痛点。MR25H40CDF 是 Everspin 出的 4Mbit SPI MRAM,它的核心优势在于:写入寿命几乎无限(官方标称 10^14 次以上)、写入速度接近 SRAM(不需要擦除,字节级随机写入)、掉电数据不丢失。这三点对于工业数据存储来说简直是量身定做。
1.2 MR25H40CDF 的关键参数解读
选型的时候我把 MR25H40CDF 的 datasheet 翻了好几遍,几个关键参数值得单独拎出来说:
| 参数 | 数值 | 实际意义 |
|---|---|---|
| 容量 | 512KB (4Mbit) | 存参数和日志绰绰有余 |
| 接口 | SPI (Mode 0/3) | 和 dsPIC 的 SPI 模块直接对接 |
| 最高时钟 | 40MHz | 读取速度远超 EEPROM |
| 写入寿命 | 10^14 次 | 基本等于无限 |
| 数据保持 | 20 年 | 工业设备生命周期内无忧 |
| 工作电压 | 2.7V ~ 3.6V | 3.3V 系统直接供电 |
| 写入时间 | 无延迟(字节级) | 不需要等待写完成 |
这里要特别说一下"写入时间无延迟"这件事。MRAM 的写入是真正的随机访问,你发一个写命令,数据就写进去了,不需要像 Flash 那样等待内部编程完成。这意味着你可以在 SPI 总线上连续写入,中间不需要插入延时或者轮询状态寄存器。对于高频日志记录场景,这个特性带来的效率提升非常明显。
另外 MR25H40CDF 支持 SPI Mode 0 和 Mode 3,这一点在对接 dsPIC 的时候要注意,后面讲 SPI 初始化的时候会详细说。
1.3 dsPIC33FJ256GP710A 的 SPI 资源匹配
dsPIC33FJ256GP710A 是 Microchip 的一款 16 位数字信号控制器,主频最高 40MIPS,自带两个 SPI 模块(SPI1 和 SPI2)。我选它是因为这个项目需要做电机 FOC 控制,对运算能力有要求,同时它的外设资源足够丰富。
SPI 模块这边,dsPIC33FJ 的 SPI 支持 8 位和 16 位数据传输,主从模式可配,时钟极性(CKP)和时钟边沿(CKE)可以独立设置,正好能匹配 MR25H40CDF 的 Mode 0 或 Mode 3 要求。最高 SPI 时钟可以做到 10MHz 以上(取决于主频分频),虽然跑不到 MR25H40CDF 的 40MHz 上限,但对于存储读写来说完全够用。
有一点需要注意:dsPIC33FJ256GP710A 的 SPI 模块在 8 位模式下,数据寄存器是 16 位的,实际只用低 8 位。发送和接收是同时进行的,读数据的时候需要先发一个 dummy 字节把数据"顶"出来。这个特性和 STM32 的 SPI 类似,但寄存器操作方式不同,后面代码部分会具体展开。
2. 硬件连接与 SPI 时序匹配的细节
2.1 引脚连接方案
MR25H40CDF 是 8 引脚封装,引脚定义很清晰:VCC、GND、SCK、SI(数据输入)、SO(数据输出)、CS(片选)、WP(写保护)、HOLD(保持)。实际使用中 WP 和 HOLD 如果不用可以上拉到 VCC,但我的建议是至少把 WP 引出来接到 GPIO,这样可以在固件层面做写保护,防止误写。
和 dsPIC33FJ256GP710A 的连接我用了 SPI2 模块,具体引脚分配如下:
- SCK 接 dsPIC 的 SCK2(具体引脚号根据封装查 datasheet)
- SI 接 dsPIC 的 SDO2
- SO 接 dsPIC 的 SDI2
- CS 用一个普通 GPIO 控制(我用的 RF5)
- WP 接另一个 GPIO(RF4),做软件写保护
- HOLD 直接上拉到 VCC
这里有个细节:CS 我特意没有用 SPI 模块的硬件片选(SS),而是用普通 GPIO 手动控制。原因后面会讲,简单说就是硬件片选在多设备共享总线时容易出问题,手动控制更灵活。
2.2 SPI 模式选择:Mode 0 还是 Mode 3
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。这两种模式的区别在于时钟空闲电平和数据采样边沿。
Mode 0 下,SCK 空闲为低电平,数据在 SCK 上升沿采样;Mode 3 下,SCK 空闲为高电平,数据在 SCK 上升沿采样。两种模式都能正常工作,但我在实际调试中发现,Mode 0 在长线传输时抗干扰能力稍好一些,因为空闲低电平在总线浮空时不容易被误判为时钟信号。所以最终选了 Mode 0。
在 dsPIC33FJ 上配置 Mode 0,需要设置 SPI2CON1 寄存器的 CKP 位为 0(空闲低电平),CKE 位为 1(数据在时钟边沿变化,在下一个边沿采样)。这里要特别注意 dsPIC 的 CKE 定义和有些平台是反的,我第一次配的时候就是这里搞错了,导致读出来的数据全是 0xFF。
2.3 上拉电阻与信号完整性
SPI 总线在高速运行时,信号完整性是个容易被忽视的问题。我在 PCB 上做了以下处理:
- SCK、SI、SO 三条线各串了一个 22Ω 的电阻,靠近 dsPIC 端放置,用来抑制过冲和振铃
- CS 线上加了一个 10kΩ 上拉电阻,确保在 dsPIC 复位期间 CS 保持高电平,防止 MRAM 被误触发
- MRAM 的 VCC 引脚旁边放了 0.1μF 和 1μF 的退耦电容,尽量靠近芯片
实测下来,在 10MHz SPI 时钟下,信号质量很好,示波器上看上升沿干净,没有明显的振铃。如果 PCB 走线比较长(超过 10cm),建议把时钟降到 5MHz 以下,或者加缓冲器。
提示:MRAM 的 WP 引脚是低电平有效,也就是说拉低 WP 会禁止写入。如果你不需要硬件写保护,直接上拉到 VCC 即可。但如果你的应用有误写风险,强烈建议用 GPIO 控制,在固件里做双重保护。
3. dsPIC33FJ 的 SPI 初始化与底层驱动实现
3.1 SPI2 模块的寄存器配置
dsPIC33FJ256GP710A 的 SPI 配置主要涉及三个寄存器:SPI2CON1、SPI2CON2、SPI2STAT。我先把配置代码贴出来,然后逐行解释。
void SPI2_Init(void) { // 关闭 SPI2 模块,配置前先禁用 SPI2STATbits.SPIEN = 0; // SPI2CON1 配置 // bit15: SPIEN = 0 (先关闭) // bit14: 未使用 // bit13: DISSDO = 0 (使能 SDO 输出) // bit12: MODE16 = 0 (8 位模式) // bit11: SMP = 0 (在数据输出中间采样) // bit10: CKE = 1 (数据在时钟变化沿传输) // bit9: SSEN = 0 (不使用硬件片选) // bit8: CKP = 0 (空闲低电平,Mode 0) // bit7-6: MSTEN = 1 (主模式) // bit5-2: SPRE = 0b0001 (主模式预分频 4:1) // bit1-0: PPRE = 0b00 (主模式后分频 1:1) SPI2CON1 = 0x0120; // 主模式,8 位,Mode 0,预分频 4:1 // SPI2CON2 配置 // bit15-13: FRMEN = 0 (关闭帧模式) // bit12: SPIFSD = 0 // bit11: FRMDLY = 0 // bit10-9: SPIBEN = 0 (关闭增强缓冲) // bit8: 未使用 // bit7-0: 未使用 SPI2CON2 = 0x0000; // 清除接收溢出标志 SPI2STATbits.SPIROV = 0; // 使能 SPI2 模块 SPI2STATbits.SPIEN = 1; }这里重点解释几个关键位:
SPRE 预分频设置:dsPIC33FJ256GP710A 在 40MIPS 时,系统时钟(Fcy)是 40MHz。SPI 时钟 = Fcy / (2 * (SPRE + 1))。我设 SPRE = 1,所以 SPI 时钟 = 40MHz / (2 * 2) = 10MHz。这个速度在 MR25H40CDF 的 40MHz 上限之内,同时信号质量也有保障。
SMP 采样位:SMP = 0 表示在数据输出时间的中间采样,SMP = 1 表示在末尾采样。对于 10MHz 时钟,中间采样更稳妥,留出的建立保持时间更充裕。
CKE 位:这是最容易搞错的地方。dsPIC 的 CKE = 1 表示数据在时钟变化沿(从空闲态到有效态)传输,在下一个边沿采样。配合 CKP = 0,就构成了 SPI Mode 0。
3.2 字节收发函数的实现
dsPIC 的 SPI 收发是同步的,写一个字节到 SPI2BUF 的同时,接收寄存器里会收到一个字节。所以读和写用的是同一个函数:
uint8_t SPI2_Transfer(uint8_t data) { // 等待发送缓冲空 while (SPI2STATbits.SPITBF); // 写入数据,启动传输 SPI2BUF = data; // 等待接收完成 while (!SPI2STATbits.SPIRBF); // 返回接收到的数据 return (uint8_t)(SPI2BUF & 0xFF); }这个函数看起来简单,但有两个坑要注意:
第一,SPI2STATbits.SPITBF是发送缓冲满标志,不是发送完成标志。等待它清零只能保证发送缓冲有空位,不能保证数据已经移出去了。对于连续传输,这样写没问题;但如果传输完要立刻拉高 CS,需要额外等待一个时钟周期或者检查 SPIRBF。
第二,SPI2BUF是 16 位寄存器,8 位模式下高 8 位是无效数据,读的时候要屏蔽掉。我见过有人直接读整个 16 位然后强转 uint8_t,在某些编译器优化下会出问题,显式& 0xFF更保险。
3.3 MRAM 读写命令的封装
MR25H40CDF 的命令集很简洁,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
基于这些命令,我封装了几个基础函数:
#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CS_LOW() (LATFbits.LATF5 = 0) #define MRAM_CS_HIGH() (LATFbits.LATF5 = 1) void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI2_Transfer(MRAM_CMD_WREN); MRAM_CS_HIGH(); } void MRAM_WriteDisable(void) { MRAM_CS_LOW(); SPI2_Transfer(MRAM_CMD_WRDI); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI2_Transfer(MRAM_CMD_RDSR); status = SPI2_Transfer(0xFF); MRAM_CS_HIGH(); return status; }写数据函数需要先发 WREN 命令,然后发 WRITE 命令加 24 位地址,再连续发送数据:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_WriteEnable(); MRAM_CS_LOW(); SPI2_Transfer(MRAM_CMD_WRITE); SPI2_Transfer((uint8_t)(addr >> 16)); SPI2_Transfer((uint8_t)(addr >> 8)); SPI2_Transfer((uint8_t)(addr)); for (i = 0; i < len; i++) { SPI2_Transfer(buf[i]); } MRAM_CS_HIGH(); }读数据函数类似,只是不需要 WREN:
void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_CS_LOW(); SPI2_Transfer(MRAM_CMD_READ); SPI2_Transfer((uint8_t)(addr >> 16)); SPI2_Transfer((uint8_t)(addr >> 8)); SPI2_Transfer((uint8_t)(addr)); for (i = 0; i < len; i++) { buf[i] = SPI2_Transfer(0xFF); } MRAM_CS_HIGH(); }注意:MRAM 的地址是 24 位的,因为 512KB 容量需要 19 位地址,但 MR25H40CDF 用 24 位地址格式(3 个字节)。高 5 位是无关位,填 0 即可。我一开始只发了 2 个字节地址,结果读写全乱套了,查了半天 datasheet 才发现这个问题。
4. 数据存储策略:参数区与日志区的分区管理
4.1 存储空间规划
512KB 的空间如果不做规划,很容易写乱。我把整个 MRAM 分成三个区域:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 参数区 | 0x000000 | 4KB | 电机参数、配置数据 |
| 统计区 | 0x001000 | 4KB | 运行时间、启停次数、故障计数 |
| 日志区 | 0x002000 | 504KB | 循环写入的运行日志 |
参数区和统计区采用"双备份 + 校验"的方式存储,每个数据块存两份,读取时对比校验。如果一份损坏,自动切换到另一份。日志区采用环形缓冲区结构,写满后从头覆盖。
4.2 参数区的双备份与校验机制
参数数据虽然不大,但一旦丢失或损坏,设备可能无法正常启动。所以我用了双备份加 CRC 校验的方案。每个参数块的结构如下:
typedef struct { uint16_t magic; // 魔数 0x5A5A,标识有效数据 uint16_t version; // 版本号 uint16_t length; // 数据长度 uint16_t crc; // CRC16 校验值 uint8_t data[504]; // 实际参数数据 } ParamBlock_t;存储的时候,先在地址 0x000000 写一份,再在 0x000200 写一份。读取的时候先读第一份,检查 magic 和 CRC;如果失败,读第二份;如果两份都失败,加载默认参数并记录故障。
CRC16 我用的是 Modbus 多项式(0xA001),计算速度快,检错能力足够。这里有个经验:CRC 计算一定要覆盖 magic、version、length 和 data 字段,不要只算 data,否则头部损坏了 CRC 还是对的,就失去了校验意义。
4.3 日志区的环形缓冲实现
日志区是写入最频繁的区域,设计目标是:写入高效、掉电不丢最近数据、读取方便。我用了环形缓冲结构,每个日志记录固定 32 字节:
typedef struct { uint32_t timestamp; // 时间戳(ms) int16_t current; // 电流 int16_t temperature; // 温度 int16_t speed; // 转速 uint16_t fault_code; // 故障码 uint8_t reserved[20]; // 预留 } LogEntry_t;环形缓冲需要维护一个写指针,每次写入后指针递增。写指针本身也需要存储,我把它存在统计区里,每次写入日志后更新。为了防止写指针本身掉电丢失,我用了"写指针 + 计数器"的方式:每次更新写指针时,同时更新一个递增的计数器,读取时取计数器最大的那个作为有效值。
日志区的写入不需要每次都发 WREN 命令吗?实际上 MRAM 每次写入前都需要 WREN,这是 SPI 协议的要求。但 MRAM 的 WREN 和写入之间没有延迟要求,所以连续写入时可以在每次 WRITE 命令前都发一次 WREN,开销很小。
4.4 掉电保护的实际处理
工业现场掉电是常态,所以掉电保护必须做扎实。我的方案是在电源输入端加了一个大电容(1000μF),配合一个电压检测电路。当检测到输入电压低于阈值时,触发 dsPIC 的外部中断,在中断里把当前的关键数据(写指针、统计计数)紧急写入 MRAM。
因为 MRAM 写入不需要等待,这个紧急写入过程非常快,几十微秒就能完成。相比之下,如果用 EEPROM,同样的数据量可能需要几毫秒,大电容要加大很多才能撑住。
这里有个细节:电压检测的阈值要留足余量。我设的是 4.5V(系统是 5V 供电,LDO 降到 3.3V),从 4.5V 掉到 LDO 失效大概有 10ms 的窗口,足够完成紧急写入。阈值设太高会频繁误触发,设太低来不及写,需要根据实际电源特性调试。
5. 调试过程中踩过的坑与排查思路
5.1 读出来全是 0xFF 的问题
第一次跑通 SPI 后,读 MRAM 的状态寄存器,返回的全是 0xFF。0xFF 通常意味着 MISO 线一直是高电平,要么是 MRAM 没响应,要么是 SPI 模式不对。
排查步骤是这样的:先用示波器看 CS、SCK、MOSI 三条线。CS 拉低正常,SCK 有 10MHz 时钟,MOSI 上有数据波形,说明 dsPIC 这边发送没问题。然后看 MISO,发现一直是高电平,没有任何变化。
这就指向两个可能:MRAM 没被正确选中,或者 SPI 模式不匹配导致 MRAM 没识别到命令。我先检查了 CS 线,确认拉低到了 0V,排除片选问题。然后回头查 SPI 模式配置,发现 CKE 位设反了。dsPIC 的 CKE=1 对应的是 Mode 0,我一开始设成了 CKE=0,实际配成了 Mode 1,MRAM 不认。改成 CKE=1 后,状态寄存器读出来是 0x00,正常了。
这个坑的教训是:不同平台对 SPI 模式的寄存器定义可能不同,一定要对着 datasheet 的时序图确认,不要凭经验想当然。
5.2 连续写入时的数据错位
解决了读的问题后,开始测试连续写入。写 256 字节的数据,读回来发现从第 128 字节开始数据错位了,后面的数据整体偏移了一个字节。
这个问题比较隐蔽。我一开始怀疑是缓冲区溢出,检查了代码没发现问题。后来用逻辑分析仪抓 SPI 波形,发现第 128 字节处 CS 有一个短暂的毛刺。原来是我的 CS 控制宏在循环中被意外调用了,导致 CS 短暂拉高又拉低,MRAM 把这次毛刺当成了命令结束,后续数据就被当成了新命令。
修复方法是在 MRAM_Write 函数里,CS 拉低后直到所有数据发完才拉高,中间不允许任何其他操作打断。另外,我把 CS 控制改成了直接操作寄存器(LATFbits.LATF5),而不是调用函数,减少意外调用的可能。
5.3 写保护引脚的误触发
MRAM 的 WP 引脚是低电平有效,我把它接到了 dsPIC 的 RF4,初始化为输出高电平。但在调试过程中发现,有时候写入不成功,读状态寄存器发现 WEL 位(写使能锁存)是 0。
查了半天,发现是 RF4 在 dsPIC 复位期间处于高阻态,WP 引脚被外部电路拉低了,导致 MRAM 进入写保护状态。虽然复位后 RF4 被配置为高电平输出,但 MRAM 可能已经锁存了写保护状态。
解决办法是在 WP 引脚上加一个 10kΩ 上拉电阻,确保复位期间 WP 保持高电平。同时在固件初始化时,先拉高 WP,再发 WRDI 命令清除可能存在的写保护状态。
5.4 SPI 时钟过高的稳定性问题
一开始我把 SPI 时钟设到了 20MHz(SPRE=0),想看看 MRAM 能不能跑满。结果发现大部分时候正常,但偶尔会出现读写错误,概率大概千分之一。
用示波器看 20MHz 下的 SCK 信号,发现上升沿有明显的过冲,幅度大概到 4V 左右(3.3V 系统)。虽然 MR25H40CDF 的 IO 耐压是 3.6V,但过冲长期来看会影响可靠性。而且 20MHz 下,dsPIC 的 SPI 建立保持时间余量很小,稍微有点干扰就出错。
最终我把时钟降到了 10MHz,过冲消失了,连续跑 72 小时压力测试零错误。对于存储应用来说,10MHz 的吞吐率(约 1.25MB/s)完全够用,没必要追求极限速度。
6. 性能实测与方案对比
6.1 读写速度实测数据
在 10MHz SPI 时钟下,我实测了不同操作的耗时:
| 操作 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节写 | 1B | 约 3.2μs | - |
| 连续写 | 256B | 约 220μs | 1.16MB/s |
| 连续读 | 256B | 约 210μs | 1.22MB/s |
| 参数区完整读取 | 4KB | 约 3.4ms | 1.20MB/s |
| 日志写入 | 32B | 约 30μs | 1.07MB/s |
这个速度对于工业数据存储来说绰绰有余。作为对比,我之前用过的某款 SPI EEPROM 在 5MHz 下连续写 256 字节需要约 5ms(因为要等待内部写周期),速度差了 20 多倍。
6.2 与 EEPROM、Flash 方案的横向对比
| 对比项 | MR25H40CDF | SPI EEPROM | SPI Flash |
|---|---|---|---|
| 写入寿命 | 10^14 次 | 10^6 次 | 10^5 次 |
| 写入延迟 | 无 | 5ms/页 | 3ms/扇区 |
| 擦除需求 | 不需要 | 不需要 | 必须擦除 |
| 字节级写入 | 支持 | 支持 | 不支持 |
| 容量 | 512KB | 通常 ≤128KB | 1MB~16MB |
| 单价 | 较高 | 中等 | 低 |
| 掉电保持 | 20 年 | 40 年 | 20 年 |
MRAM 唯一的劣势就是价格。MR25H40CDF 的单价大概是同容量 SPI Flash 的 10 倍左右。但如果算上磨损均衡算法的开发成本、EEPROM 的更换维护成本,以及数据丢失带来的风险,在工业场景下这个溢价是值得的。
6.3 长期运行稳定性验证
为了验证可靠性,我做了一个 72 小时连续写入测试:每 100ms 写入一条 32 字节的日志,总共写入约 260 万次。测试结束后读取全部日志,校验数据完整性,零错误。
同时做了 1000 次掉电测试:在随机时刻切断电源,重新上电后检查参数区和统计区的数据完整性。1000 次测试中,参数区零损坏,统计区有 3 次出现了写指针不一致,但因为采用了双备份机制,都成功恢复了。
这个结果让我对 MRAM 在工业场景下的可靠性有了信心。当然,实际部署时还是要做好电源设计和掉电保护,不能完全依赖存储介质本身的特性。
7. 几个容易被忽视的实操经验
7.1 上电初始化的顺序很重要
MRAM 上电后需要一定的时间才能进入正常工作状态,datasheet 里写的是 400μs(从 VCC 稳定到可以接受命令)。但实际测试发现,如果 VCC 上升较慢(比如 LDO 软启动),这个时间可能要更长。
我的做法是在固件初始化时,先延时 1ms,然后读一次状态寄存器。如果读到的值不是预期值,再延时 1ms 重试,最多重试 5 次。这样既保证了可靠性,又不会浪费太多启动时间。
另外,上电后建议先发一次 WRDI 命令,清除可能存在的写使能状态。虽然理论上上电后 WEL 应该是 0,但实际调试中遇到过异常情况,多发一次 WRDI 不费事,能避免一些奇怪的问题。
7.2 地址对齐与页边界
MRAM 没有页的概念,理论上可以任意地址写入任意长度。但实际使用中,我建议还是做地址对齐,比如 4 字节对齐。原因有两个:一是 dsPIC 是 16 位架构,非对齐访问在某些情况下会降低效率;二是对齐后数据结构更清晰,调试时看内存 dump 更直观。
另外,虽然 MRAM 支持跨页写入,但我的习惯是单次写入不超过 256 字节。这不是 MRAM 的限制,而是为了代码的可维护性——单次写入太长,出问题时不好定位。
7.3 状态寄存器的定期检查
MR25H40CDF 的状态寄存器里有几个有用的位:WEL(写使能锁存)、BP0/BP1(块保护)、SRWD(状态寄存器写保护)。我建议在每次写入操作前读一次状态寄存器,确认 WEL 为 1,写入完成后再读一次确认 WEL 归零。
这样做会增加一点开销(每次读状态大概 2μs),但能及时发现写入异常。我在实际项目中就遇到过因为 WP 引脚干扰导致 WEL 没置位,写入静默失败的情况。如果没检查状态寄存器,这种问题很难发现。
7.4 日志数据的压缩存储
如果日志数据量很大,可以考虑做简单的压缩。我的做法是对变化缓慢的数据(比如温度)做差分存储,只存变化量。这样每条日志可以从 32 字节压缩到 16 字节左右,存储密度翻倍。
不过压缩会增加代码复杂度,而且解压需要额外的 CPU 时间。是否值得做,取决于你的日志量和 CPU 负载。如果 CPU 负载已经很高,建议直接存原始数据,MRAM 的容量和速度都撑得住。
7.5 固件升级时的数据迁移
产品固件升级时,参数区的数据结构可能会变化。我的做法是在参数区头部加一个 version 字段,升级后读取时检查 version,如果不匹配就执行数据迁移函数,把旧格式的数据转换成新格式。
这个迁移函数要写得非常小心,因为一旦出错,用户的参数就丢了。我的建议是迁移前先把原始数据完整读出来备份到 RAM,迁移成功后再写入,写入前先擦除旧数据(MRAM 不需要擦除,直接覆盖即可)。如果迁移过程中掉电,下次上电时 version 还是不匹配,会重新执行迁移,所以迁移函数必须是幂等的。
8. 这套方案适合什么场景,不适合什么场景
MRAM 方案最适合的场景是:写入频繁、数据量不大、对可靠性要求高、掉电不能丢数据。典型的应用包括工业控制器参数存储、医疗设备运行日志、汽车电子故障记录、电力监控数据采集等。
不太适合的场景是:需要大容量存储(比如音视频数据)、对成本极度敏感(比如消费类电子产品)、写入频率很低(比如一年写几次的配置数据,用 EEPROM 就够了)。
我在另一个项目里做过对比:一个智能家居的传感器节点,每天只写几次配置数据,用 2KB 的 EEPROM 成本不到一块钱,完全没必要上 MRAM。所以选型还是要看具体需求,不要为了用新技术而用新技术。
最后分享一个我在调试中总结的小技巧:如果你不确定 MRAM 是否正常工作,可以先写一个已知的模式(比如 0xAA、0x55 交替),然后读回来对比。这个测试能快速判断 SPI 通信是否正常、地址是否对齐、数据线是否接反。比读状态寄存器更直观,因为状态寄存器的值有时候不好判断对错。
另外,MR25H40CDF 的 datasheet 里有一个细节容易被忽略:HOLD 引脚在 SPI 通信过程中拉低会暂停传输,但不会复位内部状态机。如果你不用 HOLD 功能,一定要上拉到 VCC,不要悬空。我见过有人把 HOLD 悬空,结果在电磁干扰下偶尔触发,导致通信异常,查了很久才找到原因。