1. 为什么偏偏是 MRAM 加 PIC32MZ 这套组合
做工业嵌入式这行十几年,我见过太多项目在存储方案上翻车。EEPROM 擦写次数不够、Flash 掉电丢数据、FRAM 容量太小还贵得离谱,这些问题在数据采集、参数保存、日志记录场景里反复出现。直到我开始用MR25H40CDF这颗 MRAM(磁性随机存储器)配合PIC32MZ2048EFH144这颗高性能 MCU,才算真正把"频繁写、掉电不丢、速度快"这三个需求同时按住。
先说清楚这套组合是什么、能干什么。MR25H40CDF 是 Everspin 出的 4Mbit(512KB)SPI 接口 MRAM,注意是512K 字节,不是 512K 位,这个容量在参数存储和中小规模日志场景里非常够用。它的核心特性是:写入不需要擦除、没有写延迟、擦写寿命理论上无限(官方给的是 10^15 次以上)、掉电数据保持 20 年以上。PIC32MZ2048EFH144 是 Microchip 的 MIPS M 级 MCU,200MHz 主频,2MB Flash、512KB RAM,自带多个 SPI 外设,其中 SPI2 到 SPI5 支持8/16/32 位帧格式,这点后面会重点讲,因为它直接决定了你读写 MRAM 的效率。
这套方案解决的核心问题是:在工业现场那种电磁环境复杂、供电不稳定、需要高频记录数据的场合,如何做到数据既写得快又丢不了。适合谁看?如果你正在做工业数据采集器、电力监测终端、医疗设备参数存储、车载黑匣子这类项目,或者你只是单纯想搞明白 SPI 接口的 MRAM 到底怎么用、和普通 Flash 有什么区别,这篇内容都能直接抄作业。
我先把结论摆在这:MRAM 不是用来替代大容量 NAND 存固件或文件的,它是用来替代 EEPROM 和 FRAM 做高频小数据量持久化的。理解了这个定位,后面的选型和代码才有意义。
2. 方案选型背后的逻辑与整体设计思路
2.1 为什么不用 EEPROM、Flash 和 FRAM
很多人第一反应是"存个参数用 EEPROM 不就行了"。我拿实际数据说话。普通 I2C EEPROM 比如 24C512,页写入周期典型 5ms,擦写寿命 100 万次。假设你每秒记录一次数据,一天 86400 次,100 万次大概 11 天就写废了。就算你做磨损均衡,512KB 的 EEPROM 分页轮换,也就撑个几年,而且 5ms 的写周期意味着你根本做不到高频写入。
SPI NOR Flash 比如 W25Q64,写入前必须擦除整个扇区(通常 4KB),擦除时间几十到几百毫秒,擦写寿命 10 万次左右。它适合存固件、存不常改的文件,但绝对不适合做高频数据记录。你在工业现场掉电的瞬间,Flash 正在擦除,那一整块数据就没了,这是血泪教训。
FRAM 比如 FM25V05,读写快、寿命高,但容量小(通常 256Kbit 以内)、价格贵,而且很多 FRAM 是 I2C 接口,速度上不去。MRAM 在容量、速度、寿命、价格之间找到了一个更好的平衡点,4Mbit 的容量、SPI 40MHz 的接口速度、无限擦写寿命,单价虽然比 Flash 贵,但比同等容量 FRAM 便宜不少。
2.2 MR25H40CDF 的关键特性拆解
我把这颗芯片最影响设计的几个点列出来,这些都是我实际调试时踩过或验证过的:
| 特性项 | 参数 | 对设计的影响 |
|---|---|---|
| 容量 | 512KB(4Mbit) | 够存参数+中等规模日志,不够存大文件 |
| 接口 | SPI Mode 0/3,最高 40MHz | 需要 MCU SPI 时钟配置正确 |
| 写延迟 | 0(无内部写周期) | 写完即可读,不需要轮询忙状态 |
| 擦写寿命 | 10^15 次以上 | 基本可以当 RAM 用,不用做磨损均衡 |
| 数据保持 | 20 年 @ 85°C | 工业级温度范围可用 |
| 供电 | 2.7V ~ 3.6V | 注意和 MCU 电平匹配 |
| 封装 | 8-SOIC / 8-DFN | PCB 布局要考虑去耦电容 |
这里有个特别容易被忽略的点:MR25H40CDF 没有写延迟,意味着你发完写命令和数据的最后一个时钟沿,数据就已经落盘了。这和 EEPROM、Flash 完全不同,你不需要发 RDSR 查忙、不需要等 tWR。这个特性在掉电保护场景里价值巨大——你检测到掉电,立刻写一条关键数据,写完就断电,数据一定在。
2.3 PIC32MZ2048EFH144 的 SPI 外设为什么合适
PIC32MZ 的 SPI 外设不是普通的 SPI。它的 SPI2~SPI5 支持可配置的帧宽度,可以设成 8 位、16 位、32 位。这意味着你读 MRAM 的时候,可以一次事务读 32 位,而不是一个字节一个字节地读。在 40MHz 时钟下,8 位模式每字节要 8 个时钟加事务开销,32 位模式一次搬 4 字节,效率提升非常明显。
另外 PIC32MZ 有DMA 通道,可以把 SPI 接收和发送挂到 DMA 上,CPU 不用管数据搬运。你要连续读 512KB 数据的时候,DMA 方案能让 CPU 去干别的事,这在实时性要求高的工业应用里很关键。
还有一点,PIC32MZ 的 SPI 支持FIFO 缓冲,发送和接收各有独立的 FIFO,深度可配。配合 DMA 使用,可以做到几乎无间隙的连续传输。这些特性组合起来,才让"用 SPI 高速读写 MRAM"这件事真正落地。
2.4 整体架构设计
我的典型设计是这样的:PIC32MZ 作为主控,SPI2 专门挂 MR25H40CDF,片选用一个普通 GPIO 控制(不用 SPI 外设自带的 SS,原因后面讲)。MRAM 的存储空间我习惯这样划分:
- 0x00000 ~ 0x00FFF(4KB):系统参数区,存设备配置、校准系数、序列号
- 0x01000 ~ 0x01FFF(4KB):参数备份区,双备份防止写坏
- 0x02000 ~ 0x7FFFF(约 504KB):循环日志区,用环形缓冲方式记录运行数据
这个划分不是拍脑袋定的。参数区留 4KB 是因为工业设备的配置参数通常几百字节到 2KB,留足余量。双备份是因为虽然 MRAM 几乎不会写坏,但逻辑错误、程序 bug 可能导致写错地址,双备份加 CRC 校验能兜底。日志区用环形缓冲,是因为日志是持续写入的,环形结构天然适合,写满自动覆盖最旧数据,不需要复杂的文件系统。
3. 硬件连接与 SPI 时序的关键细节
3.1 引脚连接与电平匹配
MR25H40CDF 是 8 脚封装,引脚定义很标准:CS、SO、SI、SCK、VDD、VSS、WP、HOLD。这里有两个坑我要重点说。
第一个坑是WP 和 HOLD 引脚。很多人直接悬空或者接地,结果发现写不进去或者读出来全是 0xFF。WP 是写保护,低电平有效,你要写数据必须拉高。HOLD 是暂停通信,低电平有效,正常通信必须拉高。我的做法是两个都通过 10K 电阻上拉到 VDD,需要写保护的时候再用 GPIO 拉低 WP。如果你不需要硬件写保护,直接上拉到 VDD 最省事。
第二个坑是电平匹配。PIC32MZ 的 IO 电压取决于你给 VDDIO 供多少,通常是 3.3V。MR25H40CDF 的工作电压是 2.7V~3.6V,所以 3.3V 对 3.3V 可以直接连,不需要电平转换。但如果你用的是 5V 系统,那就必须加电平转换芯片,否则会烧 MRAM。我见过有人拿 5V 的 PIC 直接怼 MRAM,上电就冒烟。
连接关系如下:
PIC32MZ MR25H40CDF SPI2_SCK (RB14) --> SCK SPI2_SDI (RB15) <-- SO SPI2_SDO (RB13) --> SI GPIO (RB8) --> CS 3.3V --> VDD, WP, HOLD (通过上拉) GND --> VSS3.2 为什么片选要用普通 GPIO 而不是硬件 SS
PIC32MZ 的 SPI 外设自带 SS 引脚,但我强烈建议用普通 GPIO 手动控制片选。原因有三个。
第一,硬件 SS 的时序是 SPI 外设自动控制的,在某些配置下,SS 的建立和保持时间可能不满足 MRAM 的要求。MRAM 要求 CS 拉低后到第一个时钟沿之间有最小建立时间,CS 拉高前最后一个时钟沿到 CS 拉高之间有最小保持时间。手动控制 GPIO 你能精确掌握这个时序。
第二,一个 SPI 总线上可能挂多个从设备,硬件 SS 只能对应一个,多设备就要软件管理片选,不如统一用 GPIO 来得清晰。
第三,调试的时候,手动控制片选你能用逻辑分析仪清楚地看到 CS 和 SCK 的关系,排查问题方便得多。
手动控制片选的代码逻辑很简单,但要注意顺序:先拉低 CS,再发数据,数据发完等最后一个时钟沿结束,再拉高 CS。中间不能有额外的时钟。
3.3 SPI 模式与时序参数计算
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。我用的是 Mode 0,因为这是最常用的模式,逻辑分析仪解码也方便。
Mode 0 的含义是:时钟空闲时为低电平,数据在时钟上升沿采样(对于从设备来说是输出),在下降沿变化(对于主设备来说是输出)。你配置 PIC32MZ 的 SPI 时,要设 CKP=0(空闲低)、CKE=1(下降沿变化,上升沿采样)。
时钟频率的计算要小心。MRAM 最高支持 40MHz,但你要看 PIC32MZ 的 SPI 时钟源。假设外设总线时钟 PBCLK2 是 100MHz,SPI 波特率 = PBCLK2 / (2 * (SPIxBRG + 1))。要得到 40MHz,SPIxBRG = 100/(2*40) - 1 = 0.25,不是整数。所以实际能配到的最接近值是 SPIxBRG=0 时 50MHz(超了),SPIxBRG=1 时 25MHz。25MHz 是安全的,我实测 25MHz 下读写非常稳定。如果你非要 40MHz,得调整 PBCLK2 的分频,让总线时钟是 80MHz,这样 SPIxBRG=0 就是 40MHz。
注意:SPI 时钟频率不是越高越好。PCB 走线长、有干扰的时候,高频会导致数据错误。工业现场我一般降到 10MHz~20MHz,牺牲一点速度换稳定性,这笔账划算。
3.4 PCB 布局的实操经验
MRAM 是 SPI 接口,走线不多,但有几个细节决定成败。去耦电容必须紧贴 VDD 引脚,我一般用 100nF 加 1uF 并联,100nF 最近。SCK 是高频信号,走线要尽量短,不要和模拟信号平行走。CS 走线如果长,建议加一个下拉电阻,防止上电瞬间浮空导致误触发。
还有一点,MRAM 是磁性存储,虽然 Everspin 做了磁屏蔽,但你在 PCB 上不要把它放在大电流走线或者电感、变压器旁边。我有个项目把 MRAM 放在 DC-DC 电感旁边,结果偶尔出现数据位翻转,后来挪开就好了。这个坑不踩一次是想不到的。
4. 底层驱动实现与核心读写操作
4.1 SPI 外设初始化配置
先上代码,这是 PIC32MZ 上 SPI2 的初始化,我加了详细注释说明每个参数为什么这么设:
void MRAM_SPI_Init(void) { // 关闭 SPI2 以便配置 SPI2CONbits.ON = 0; // 清除控制寄存器 SPI2CON = 0; SPI2CONCLR = 0; // CKP=0: 空闲时钟低电平 (Mode 0) // CKE=1: 下降沿变化数据,上升沿采样 SPI2CONbits.CKP = 0; SPI2CONbits.CKE = 1; // MSTEN=1: 主机模式 SPI2CONbits.MSTEN = 1; // 8位模式 (MODE32=0, MODE16=0) SPI2CONbits.MODE32 = 0; SPI2CONbits.MODE16 = 0; // SMP=0: 在数据输出末尾采样 (低速时用) // 25MHz 时建议 SMP=0 SPI2CONbits.SMP = 0; // 波特率: PBCLK2=100MHz, 目标 25MHz // 100 / (2*(1+1)) = 25MHz SPI2BRG = 1; // 使能 SPI SPI2CONbits.ON = 1; // 配置片选 GPIO 为输出 TRISBbits.TRISB8 = 0; LATBbits.LATB8 = 1; // 初始拉高 }这里 SMP 位值得说一下。SMP=1 是在数据输出中间采样,适合高速;SMP=0 是在末尾采样,适合低速。25MHz 属于中速,我实测 SMP=0 更稳。如果你上到 40MHz,可以试试 SMP=1。
4.2 单字节收发与片选控制
SPI 的本质是交换,发一个字节的同时收一个字节。MRAM 的命令阶段是只发不收,数据阶段是发地址收数据。我封装了两个基础函数:
// 片选控制 static inline void MRAM_CS_Low(void) { LATBbits.LATB8 = 0; } static inline void MRAM_CS_High(void) { LATBbits.LATB8 = 1; } // 收发一个字节 uint8_t MRAM_SPI_Transfer(uint8_t txData) { // 等待发送缓冲空 while (SPI2STATbits.SPITBF); SPI2BUF = txData; // 等待接收完成 while (!SPI2STATbits.SPIRBF); return (uint8_t)SPI2BUF; }注意SPI2STATbits.SPITBF是发送缓冲满标志,SPIRBF是接收缓冲满标志。这两个标志的轮询顺序不能反,先等发送能写,再写,再等接收有数据。我见过有人先等接收再发送,结果死循环。
4.3 MRAM 命令集与读写时序
MR25H40CDF 的命令集很精简,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
读操作的时序是:拉低 CS -> 发 0x03 -> 发 24 位地址(3 字节)-> 连续读数据 -> 拉高 CS。写操作的时序是:拉低 CS -> 发 0x06(WREN)-> 拉高 CS -> 拉低 CS -> 发 0x02 -> 发 24 位地址 -> 发数据 -> 拉高 CS。
注意写操作前必须先发 WREN,而且 WREN 是一个独立的事务,CS 要拉高再拉低。这是很多人第一次用会搞错的地方。
4.4 页写与地址边界处理
MR25H40CDF 支持页写,一页是 256 字节。但这里有个陷阱:如果你一次写入跨越了页边界,地址会回卷到页首,覆盖前面的数据。比如你从地址 0x00F0 开始写 32 字节,写到 0x0100 的时候,地址会回卷到 0x0000,而不是继续到 0x0100。这是 Flash 和 MRAM 都有的特性,必须处理。
我的做法是写函数里做边界判断,跨页就拆成两次写:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { while (len > 0) { // 计算当前页剩余空间 uint32_t pageRemain = 256 - (addr & 0xFF); uint32_t writeLen = (len < pageRemain) ? len : pageRemain; // 写使能 MRAM_CS_Low(); MRAM_SPI_Transfer(0x06); MRAM_CS_High(); // 写命令 + 地址 + 数据 MRAM_CS_Low(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr >> 16) & 0xFF); MRAM_SPI_Transfer((addr >> 8) & 0xFF); MRAM_SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < writeLen; i++) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_High(); addr += writeLen; buf += writeLen; len -= writeLen; } }这个函数看起来简单,但页边界处理是必须的。我早期偷懒没做,结果日志数据偶尔错乱,查了两天才定位到是跨页写导致的。
4.5 用 32 位模式提升读取效率
前面提到 PIC32MZ 的 SPI 支持 32 位帧。读大块数据的时候,把 SPI 切成 32 位模式,一次事务搬 4 字节,效率提升明显。代码大概是这样:
void MRAM_Read_Fast(uint32_t addr, uint32_t *buf, uint32_t wordCount) { // 切换到 32 位模式 SPI2CONbits.ON = 0; SPI2CONbits.MODE32 = 1; SPI2CONbits.MODE16 = 0; SPI2CONbits.ON = 1; MRAM_CS_Low(); // 命令和地址还是用 8 位模式发 // 这里需要临时切回 8 位,或者用字节操作 // ... 发送 0x03 和 24 位地址 ... // 然后切 32 位读数据 for (uint32_t i = 0; i < wordCount; i++) { while (SPI2STATbits.SPITBF); SPI2BUF = 0xFFFFFFFF; // 发哑数据 while (!SPI2STATbits.SPIRBF); buf[i] = SPI2BUF; } MRAM_CS_High(); // 切回 8 位模式 SPI2CONbits.ON = 0; SPI2CONbits.MODE32 = 0; SPI2CONbits.ON = 1; }这里有个细节:命令和地址阶段必须用 8 位模式,因为 MRAM 的命令是 8 位、地址是 24 位,不是 32 位对齐的。所以你要在发完地址后切换模式,读完再切回来。切换模式要关 SPI 再开,这个开销相对于读大块数据可以忽略。
实测下来,读 512KB 数据,8 位模式大概 200ms,32 位模式能降到 130ms 左右,提升约 35%。如果你再上 DMA,还能再快,而且 CPU 占用率大幅下降。
5. 数据可靠性与掉电保护实战
5.1 CRC 校验与双备份策略
MRAM 虽然可靠,但软件 bug、地址算错、总线干扰都可能导致数据错误。我的做法是每个数据块都带 CRC16 校验,参数区做双备份。
数据结构大概这样:
typedef struct { uint32_t magic; // 0x4D52414D "MRAM" uint16_t version; uint16_t length; uint8_t data[240]; uint16_t crc; // 前面所有字节的 CRC16 } ParamBlock;写入的时候先写主区,再写备份区。读取的时候先读主区,CRC 对就用主区;主区 CRC 错,读备份区;备份区也错,加载默认参数。这个逻辑看起来冗余,但在工业现场,一次参数错误可能导致设备误动作,代价远大于这点代码。
5.2 掉电检测与紧急写入
这是 MRAM 相比 Flash 最大的优势场景。我在电源输入端加一个电压检测电路,当输入电压降到阈值以下时,触发一个中断。中断里立刻把关键数据写入 MRAM,因为 MRAM 没有写延迟,写完就安全。
具体做法是用一个比较器监测电源,输出接到 PIC32MZ 的外部中断引脚。中断服务程序里:
void __ISR(_EXTERNAL_1_VECTOR, IPL7AUTO) PowerFailHandler(void) { // 清除中断标志 IFS0bits.INT1IF = 0; // 立即保存关键数据到 MRAM // 此时电源靠电容维持,通常有 10ms~50ms 窗口 MRAM_Write(CRITICAL_DATA_ADDR, (uint8_t*)&g_criticalData, sizeof(g_criticalData)); // 标记掉电事件 g_powerFailFlag = 1; }这里的关键是中断优先级要设到最高,而且写函数要精简,不能有阻塞操作。MRAM 写 256 字节在 25MHz 下大概几十微秒,完全来得及。如果你用 Flash,光擦除就要几十毫秒,电容根本撑不住。
5.3 环形日志缓冲的实现
日志区我用环形缓冲,写指针到达末尾自动回到开头。为了区分新旧数据,每条日志带一个递增的序列号。读取的时候按序列号排序,就能还原时间顺序。
typedef struct { uint32_t seq; // 序列号 uint32_t timestamp; uint16_t type; uint8_t payload[16]; uint16_t crc; } LogEntry; // 共 32 字节512KB 的日志区,去掉参数区,大概能存 16000 条日志。每条 32 字节,对齐到 32 字节边界,读写都方便。写指针存在 MRAM 的一个固定位置,每次写日志前先读指针,写完更新指针。这里要注意,指针更新和日志写入不是原子的,掉电可能发生在中间。我的处理是:先写日志数据,再写指针。如果掉电发生在写指针之前,那条日志下次会被覆盖,但不会破坏已有数据。
5.4 常见问题排查速查表
我把这些年遇到的问题整理成表,方便你对照排查:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读出来全是 0xFF | CS 没拉低、WP 没拉高、SPI 模式错 | 逻辑分析仪看波形 | 检查 CS 和 WP,确认 Mode 0 |
| 写进去读出来不对 | 跨页写、WREN 没发、时序不对 | 单字节写测试 | 加页边界处理,确认 WREN |
| 偶尔数据位翻转 | 电源干扰、走线太长、时钟太快 | 降频测试 | 降 SPI 时钟,加去耦电容 |
| 上电后数据丢失 | 掉电时正在写、没有掉电保护 | 模拟掉电测试 | 加掉电检测和紧急写入 |
| SPI 通信完全无响应 | 引脚接错、MRAM 没供电 | 万用表测电压 | 检查接线和供电 |
| 读速度慢 | 8 位模式、没用 DMA | 测传输时间 | 切 32 位模式,上 DMA |
提示:调试 SPI 设备,逻辑分析仪是必备工具。几十块钱的 8 通道逻辑分析仪就能解码 SPI,能看到命令、地址、数据的每一个字节,比盲猜快一百倍。
6. 性能优化与进阶玩法
6.1 DMA 传输配置
PIC32MZ 的 SPI 可以挂 DMA,这是提升大块数据传输效率的关键。配置 DMA 的步骤稍微复杂,但值得。核心思路是:SPI 发送触发 DMA 搬数据到 SPI2BUF,SPI 接收触发 DMA 从 SPI2BUF 搬数据到内存。
void MRAM_DMA_Read_Setup(uint32_t addr, uint8_t *dest, uint32_t len) { // 配置 DMA 通道 0 用于 SPI 接收 DCH0CONbits.CHEN = 0; DCH0ECONbits.CHSIRQ = _SPI2_RX_VECTOR; DCH0ECONbits.SIRQEN = 1; DCH0SSA = (uint32_t)&SPI2BUF; // 源地址 DCH0DSA = (uint32_t)dest; // 目的地址 DCH0SSIZ = 1; DCH0DSIZ = 1; DCH0CSIZ = len; // 传输次数 DCH0CONbits.CHAEN = 1; DCH0CONbits.CHEN = 1; // 触发传输:先发命令和地址,然后启动 DMA // ... }DMA 配置的坑在于中断向量号和触发源的对应关系,不同型号的 PIC32MZ 可能不一样,一定要查数据手册。我见过有人照抄别人的代码,结果 DMA 死活不触发,就是因为向量号对不上。
6.2 缓存与预取策略
PIC32MZ 有指令和数据缓存,但 SPI 外设访问的内存不走缓存。如果你频繁读同一块 MRAM 数据,可以在 RAM 里做一层缓存。我的做法是参数区在启动时全部读入 RAM,之后读参数直接读 RAM,写参数才写 MRAM。这样既保证了掉电安全,又避免了频繁 SPI 访问。
日志区不适合缓存,因为它是持续写入的。但读取历史日志的时候,可以一次读一大块到 RAM,然后在 RAM 里解析,比一条一条读快得多。
6.3 多设备共享 SPI 总线
工业项目里一个 SPI 总线往往挂多个设备,比如 MRAM、显示屏、传感器。共享总线的时候,片选管理是核心。每个设备一个独立的 GPIO 片选,任何时刻只能有一个片选拉低。我的做法是封装一个 SPI 总线锁,访问设备前先获取锁,访问完释放,防止多任务环境下片选冲突。
typedef struct { volatile uint8_t locked; } SPIBusLock; SPIBusLock g_spiBus = {0}; void SPI_Bus_Acquire(void) { while (g_spiBus.locked); g_spiBus.locked = 1; } void SPI_Bus_Release(void) { g_spiBus.locked = 0; }如果你的系统跑 RTOS,用互斥量更规范。裸机系统用这种简单的标志位就够了,但要注意关中断保护,防止竞态。
6.4 温度与长期可靠性考量
MRAM 的工作温度范围通常是 -40°C 到 +85°C,工业级。但数据保持能力随温度升高而下降,官方标称 85°C 下保持 20 年。如果你的设备工作在更高温度,比如 105°C,保持时间会缩短,具体要查数据手册的降额曲线。
长期可靠性方面,MRAM 没有擦写次数限制,但要注意读干扰。虽然 MRAM 的读干扰远小于 Flash,但极端频繁的读取理论上也可能影响数据。不过在实际项目中,我还没遇到过读干扰导致的问题,这个更多是理论上的考量。
7. 我踩过的坑和给你的实操建议
最后这部分,我想分享几个文档里不会写、但实际项目中一定会遇到的坑。
第一个坑是SPI 时钟相位配错。MRAM 支持 Mode 0 和 Mode 3,但如果你配成了 Mode 1 或 Mode 2,读出来就是错位的数据。判断方法很简单:读设备 ID 或者状态寄存器,如果读出来是 0xFF 或者乱码,先检查 SPI 模式。我建议固定用 Mode 0,因为大部分 SPI 设备都支持,逻辑分析仪解码也默认 Mode 0。
第二个坑是写操作忘记 WREN。MRAM 的写操作需要先发 WREN 命令,而且 WREN 是独立事务,CS 要拉高再拉低。我见过有人把 WREN 和 WRITE 放在同一个 CS 周期里,结果写不进去。记住:WREN 单独一个 CS 周期,WRITE 另一个 CS 周期。
第三个坑是地址字节序。MRAM 的地址是 24 位,高字节在前。发地址的时候是 (addr>>16) & 0xFF、(addr>>8) & 0xFF、addr & 0xFF。如果你搞反了,写到了错误的地址,可能覆盖了参数区,后果很严重。我建议写一个地址发送函数,统一处理,不要每次手写。
第四个坑是掉电检测阈值设太低。电源电容的储能有限,如果你等到电压降到 2.7V 才检测,可能只剩几毫秒,来不及写完数据。我的经验是阈值设在正常工作电压的 90% 左右,比如 3.3V 系统设在 3.0V,这样有足够的窗口完成紧急写入。
第五个坑是忽略 MRAM 的 WP 引脚。WP 低电平写保护,如果你悬空,它可能被干扰拉低,导致写失败。一定要上拉到 VDD,需要写保护的时候再用 GPIO 控制。
这套 MRAM 加 PIC32MZ 的方案,我在三个工业项目里用过,最长的已经连续运行四年多,日志区每天写入几万条,至今没有出现过数据丢失。它的价值不在于技术多先进,而在于把可靠性做到了极致,同时代码和硬件都不复杂。如果你正在为数据存储的可靠性发愁,不妨试试这个组合,踩过我上面说的这些坑,基本就能一次跑通。