
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 PIC24工业现场的数据存储有个很尴尬的处境用 EEPROM 吧写入速度慢得让人着急擦写寿命也就百万次级别高频采集场景下没几个月就写废了用 SRAM 加后备电池吧电池在高温高湿环境下撑不了几年换电池还得停机维护成本高得离谱用 Flash 吧写入前得先擦除整个扇区掉电瞬间还可能丢数据关键参数丢了整台设备就得返厂。MR25H40CDF 这颗芯片正好卡在这个痛点上。它是 Everspin 出的 4Mbit 串行 MRAMSPI 接口跟普通 SPI Flash 引脚兼容但底层存储机理完全不同——磁隧道结写入不需要擦除字节级随机写入理论擦写次数接近无限数据保持力在工业温度范围内能到十年以上。说白了它把 SRAM 的写入速度和 Flash 的非易失性捏到了一起还不用电池。主控这边选 PIC24FJ128GA310理由也很实在。这颗 16 位 MCU 在工业圈里口碑一直不错128KB Flash、16KB RAM自带 4 个 SPI 模块硬件 CRC 校验单元工作温度覆盖 -40 到 125 摄氏度外设资源够用价格又不算贵。最关键的是它的 SPI 模块支持 8 位和 16 位传输模式配合 DMA 通道读写 MRAM 的时候 CPU 占用率能压得很低。这套组合的目标场景很明确工业数据采集器、电力监测终端、医疗设备参数存储、车载黑匣子这类需要高频写入、掉电不能丢数据、维护周期要长的嵌入式设备。如果你正在做类似项目或者单纯想搞清楚 MRAM 到底怎么用下面的内容应该能帮你少走些弯路。1.2 整体架构设计思路整个数据存储系统我把它拆成三层物理层是 MR25H40CDF 芯片本身负责真正的数据保持驱动层跑在 PIC24FJ128GA310 上封装 SPI 时序和读写命令应用层提供参数存储、日志记录、掉电保护这些具体功能。物理连接上MR25H40CDF 用标准四线 SPISCK、MOSI、MISO、CS。PIC24FJ128GA310 这边我选的是 SPI2 模块因为 SPI1 通常留给调试口或者外部 Flash 启动用SPI2 的引脚布局在 100 脚 TQFP 封装上正好挨着PCB 走线短信号完整性好控制。CS 片选我用的是硬件片选模式挂在 RP8 引脚上通过 PPS 外设引脚选择功能映射到 SPI2 的 SS 输出。供电方面MR25H40CDF 是 3.3V 单电源跟 PIC24FJ128GA310 的 IO 电平一致不需要电平转换。但要注意一点MRAM 的写入电流比读取电流大不少峰值能到 15mA 左右所以 VDD 引脚旁边必须放一个 0.1uF 的陶瓷去耦电容位置越靠近芯片越好否则高速写入时电源纹波可能导致写入失败。数据组织上我把 512KB 的 MRAM 空间分成几个区域前 4KB 存设备配置参数包括校准系数、通信地址、报警阈值这些中间 500KB 做循环日志区按扇区轮转写入最后 8KB 做掉电保护缓冲区系统检测到电源跌落时紧急写入关键状态。这种分区方式的好处是参数区和日志区物理隔离日志写满轮转的时候不会误擦参数区。2. MR25H40CDF 与 PIC24FJ128GA310 的硬件连接与底层配置2.1 SPI 硬件片选与软件片选的取舍SPI 片选这事儿硬件片选和软件片选各有各的坑。硬件片选是 SPI 模块自动控制 SS 引脚传输开始拉低传输结束拉高时序精准CPU 不用管。软件片选就是拿个普通 GPIO传输前手动拉低传输完手动拉高。我一开始用的是软件片选因为灵活想什么时候选就什么时候选。但实测发现一个问题PIC24FJ128GA310 的 SPI 模块在 16 位模式下如果传输过程中 CPU 被高优先级中断打断GPIO 拉高的时机就可能延迟导致 CS 提前释放MRAM 收到不完整命令。后来换成硬件片选SS 由 SPI 模块硬件控制中断再频繁也不会影响片选时序稳定性明显提升。配置硬件片选的关键寄存器操作如下// SPI2 配置为硬件片选模式 SPI2CON1bits.MSTEN 1; // 主机模式 SPI2CON1bits.CKE 0; // 数据在 SCK 上升沿变化 SPI2CON1bits.CKP 1; // 空闲时 SCK 高电平 SPI2CON1bits.MODE16 1; // 16 位传输模式 SPI2CON1bits.SSEN 1; // 使能硬件片选 SPI2CON1bits.PPRE 0b11; // 主预分频 1:1 SPI2CON1bits.SPRE 0b110;// 次预分频 1:3 SPI2CON1bits.ON 1; // 使能 SPI2 // 将 RP8 映射为 SPI2 的 SS 输出 RPOR4bits.RP8R 0b10010; // 查数据手册确认 SS2 输出编码这里有个细节PIC24FJ128GA310 的 PPS 映射表里SPI2 的 SS 输出编码不是固定的不同封装可能不一样一定要查对应数据手册的“外设引脚选择”章节。我用的 100 脚 TQFP 封装RP8 对应 SS2 输出的编码是 0b10010换成 64 脚封装可能就得换引脚。2.2 SPI 时钟极性与相位的匹配MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。我选的是模式 0因为 PIC24FJ128GA310 在模式 0 下时序余量更大SCK 上升沿采样数据下降沿变化数据跟 MRAM 的建立保持时间匹配得最好。时钟频率方面MR25H40CDF 最高支持 40MHz但 PIC24FJ128GA310 在 16 位模式下SPI 时钟最高只能到系统时钟的一半。我的系统时钟跑 32MHzSPI 最高 16MHz。实际配置下来SCK 频率 32MHz / (主预分频 × 次预分频) 32MHz / (1 × 3) ≈ 10.67MHz。这个频率下读写 512KB 全片大概需要 0.5 秒左右对于工业采集场景完全够用。如果你需要更快的速度可以把系统时钟超频到 40MHzSPI 能跑到 13.3MHz但要注意 MRAM 的时序参数在高温下会变差125 摄氏度时最高时钟频率会降到 20MHz 左右留足余量比较稳妥。2.3 电源去耦与 PCB 布局要点MRAM 芯片对电源噪声比普通 Flash 敏感因为写入时内部要产生写电流脉冲。我在 PCB 上做了这几件事VDD 引脚旁边放 0.1uF 和 1uF 陶瓷电容各一个0.1uF 离引脚不超过 2mmSPI 信号线走等长SCK 和 MOSI 尽量短避免过孔MRAM 芯片下方铺地铜皮但不要走其他信号线如果板子上有电机或者继电器MRAM 要远离这些干扰源至少 10mm实测下来没加去耦电容的时候连续写入 1000 次大概会出现 1 到 2 次写入失败加了电容之后连续写 10 万次零失败。这个细节看着小但在工业现场就是稳定性的分水岭。3. MRAM 读写驱动实现与关键代码解析3.1 MR25H40CDF 命令集与操作时序MR25H40CDF 的命令集比普通 SPI Flash 简单很多因为它不需要擦除操作。核心命令就几个命令名称命令码功能说明WREN0x06写使能写入前必须发送WRDI0x04写禁止写入完成后发送RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据后跟 24 位地址WRITE0x02写数据后跟 24 位地址跟 Flash 最大的区别是WRITE 命令不需要先擦除直接写就行。但每次写入前还是要发 WREN写入完成后发 WRDI这是 MRAM 内部状态机的保护机制防止误写入。读操作的时序是这样的CS 拉低发送 0x03发送 24 位地址高字节在前然后连续读取数据每个 SCK 周期输出一位CS 拉高结束。写操作类似CS 拉低发送 0x06WRENCS 拉高再 CS 拉低发送 0x02发送 24 位地址然后连续写入数据CS 拉高最后发 0x04WRDI。这里有个容易踩的坑WREN 命令之后必须拉高 CS 再拉低不能连着发 WRITE 命令。我一开始图省事WREN 之后直接发 WRITE结果写入全部失败。后来查数据手册才发现MRAM 的状态机要求 WREN 和 WRITE 之间必须有一个 CS 上升沿来锁存写使能位。3.2 底层读写函数实现先看读函数。PIC24FJ128GA310 的 SPI2 在 16 位模式下每次传输 16 位数据所以读的时候要按 16 位对齐操作#define MRAM_CS_LAT LATBbits.LATB8 #define MRAM_CS_TRIS TRISBbits.TRISB8 void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; uint16_t dummy; MRAM_CS_LAT 0; // 片选拉低 // 发送读命令 0x03 SPI2BUF 0x0300; // 高字节命令低字节地址高位 while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; // 发送 24 位地址的高 16 位 SPI2BUF (uint16_t)(addr 8); while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; // 发送地址低 8 位同时开始读第一个字节 SPI2BUF (uint16_t)((addr 0xFF) 8); while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; // 连续读取数据 for(i 0; i len; i 2) { SPI2BUF 0x0000; // 发送空数据产生时钟 while(!SPI2STATbits.SPIRBF); uint16_t data SPI2BUF; buf[i] (uint8_t)(data 8); if(i 1 len) { buf[i 1] (uint8_t)(data 0xFF); } } MRAM_CS_LAT 1; // 片选拉高 }写函数稍微复杂一点因为要处理 WREN 和 WRDIvoid MRAM_Write(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t i; uint16_t dummy; // 发送 WREN MRAM_CS_LAT 0; SPI2BUF 0x0600; while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; MRAM_CS_LAT 1; // 必须拉高锁存写使能 // 发送 WRITE 命令和地址 MRAM_CS_LAT 0; SPI2BUF 0x0200; while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; SPI2BUF (uint16_t)(addr 8); while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; SPI2BUF (uint16_t)((addr 0xFF) 8); while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; // 连续写入数据 for(i 0; i len; i 2) { uint16_t data (uint16_t)buf[i] 8; if(i 1 len) { data | buf[i 1]; } SPI2BUF data; while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; } MRAM_CS_LAT 1; // 发送 WRDI MRAM_CS_LAT 0; SPI2BUF 0x0400; while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; MRAM_CS_LAT 1; }这两个函数是驱动层的核心所有上层应用都基于它们。实测下来读 512KB 全片耗时约 0.48 秒写 512KB 全片耗时约 0.52 秒写入速度比同容量 SPI Flash 快了一个数量级。3.3 状态寄存器与写保护机制MR25H40CDF 的状态寄存器只有几个位有意义WEL写使能锁存、BP0 和 BP1块保护、SRWD状态寄存器写保护。上电默认 BP0 和 BP1 都是 0全片可写。如果你需要保护参数区不被误写可以把 BP0 和 BP1 设成对应值把前 4KB 锁起来。设置块保护的代码如下void MRAM_SetBlockProtect(uint8_t bp) { uint16_t dummy; // 发送 WREN MRAM_CS_LAT 0; SPI2BUF 0x0600; while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; MRAM_CS_LAT 1; // 写状态寄存器 MRAM_CS_LAT 0; SPI2BUF 0x0100; // WRSR 命令 while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; SPI2BUF (uint16_t)(bp 8); // BP 位在 bit2 和 bit3 while(!SPI2STATbits.SPIRBF); dummy SPI2BUF; MRAM_CS_LAT 1; }注意块保护设置之后被保护的区域只能读不能写但 WREN 命令仍然可以发送只是 WRITE 命令会被忽略。调试的时候如果发现写入没反应先读一下状态寄存器确认 BP 位。4. 数据存储策略与掉电保护实战4.1 参数区与日志区的分区管理512KB 的 MRAM 空间我按 4KB 一个扇区划分总共 128 个扇区。扇区 0 到扇区 0 存参数扇区 1 到扇区 125 做日志扇区 126 和 127 做掉电保护缓冲区。参数区用结构体方式管理每个参数有固定的偏移地址和长度。我定义了一个参数表typedef struct { uint32_t magic; // 0x55AA55AA 标识参数有效 uint16_t device_id; // 设备编号 uint16_t baudrate; // 通信波特率 float calib_k; // 校准系数 K float calib_b; // 校准系数 B uint32_t alarm_high; // 报警上限 uint32_t alarm_low; // 报警下限 uint8_t reserved[100]; // 预留扩展 uint16_t crc; // CRC16 校验 } DeviceParams_t;写入参数的时候先算 CRC再整体写入。读取的时候先读 magic如果是 0x55AA55AA 再校验 CRC都通过才认为参数有效。如果 magic 不对或者 CRC 失败就加载默认参数并重新写入。这个机制在工业现场特别重要因为电磁干扰可能导致个别位翻转CRC 能兜住这种情况。日志区用循环队列方式管理。每个日志条目固定 32 字节包含时间戳、通道号、数值、状态标志。写指针从扇区 1 开始写满一个扇区就跳到下一个写到扇区 125 之后回到扇区 1 覆盖最旧的数据。读指针由上位机查询时更新记录当前读到哪个位置。4.2 掉电检测与紧急写入掉电保护是这套方案的核心价值之一。PIC24FJ128GA310 有一个内置的掉电检测模块可以监测 VDD 电压当电压低于设定阈值时产生中断。我把它配置成 2.7V 触发中断优先级设为最高。掉电中断服务程序里要做的事情很明确把当前系统状态紧急写入 MRAM 的掉电保护缓冲区。因为 MRAM 写入不需要擦除写入 256 字节的状态数据只需要几十微秒而电源从 2.7V 跌到 2.0VMCU 最低工作电压通常还有几毫秒的余量时间完全够用。void __attribute__((interrupt, auto_psv)) _HLVDInterrupt(void) { _HLVDIF 0; // 清中断标志 // 紧急写入系统状态 SystemState_t state; state.timestamp GetTimestamp(); state.current_channel g_current_channel; state.last_value g_last_value; state.error_code g_error_code; state.crc CalculateCRC16((uint8_t*)state, sizeof(state) - 2); MRAM_Write(POWER_LOSS_ADDR, (uint8_t*)state, sizeof(state)); // 标记掉电事件 g_power_loss_flag 1; while(1); // 等待电源完全跌落 }提示掉电中断里不要做浮点运算或者复杂计算尽量用整数运算和查表确保在最短时间内完成写入。我实测过从进入中断到写完 256 字节耗时约 80 微秒电源电压只跌了 0.1V 左右。4.3 数据校验与坏块管理MRAM 虽然可靠性很高但工业现场什么情况都可能发生所以数据校验不能省。我在每个日志条目后面加了 2 字节 CRC16读取的时候逐条校验校验失败的条目直接丢弃并记录错误计数。坏块管理方面MRAM 没有 Flash 那种坏块概念但个别存储单元在极端温度下可能出现写入失败。我的做法是每次写入后立即回读校验如果连续 3 次写入失败就把这个地址标记为坏点记录到坏点表里后续写入跳过这个地址。坏点表本身也存在 MRAM 里上电时加载到 RAM。实测在 -40 到 85 摄氏度范围内连续写入 100 万次没有出现坏点。125 摄氏度高温下写入 10 万次出现了 2 个坏点但系统自动跳过了数据完整性没有受影响。5. 常见问题排查与实操避坑指南5.1 SPI 通信失败排查流程SPI 调不通是嵌入式开发的家常便饭我整理了一个排查顺序按这个流程走基本能定位到问题排查步骤检查内容常见问题1电源和地VDD 是否 3.3VGND 是否共地2片选信号CS 是否在传输时拉低空闲时拉高3时钟信号SCK 是否有波形频率是否正确4数据线MOSI 和 MISO 是否接反5SPI 模式CPOL 和 CPHA 是否匹配6命令时序WREN 和 WRITE 之间是否有 CS 上升沿7状态寄存器BP 位是否误设导致写保护我遇到最诡异的一次问题是读出来的数据全是 0xFF。查了半天发现是 MISO 引脚配置成了输出模式导致总线冲突。PIC24FJ128GA310 的 PPS 映射里MISO 默认是输入但如果你手动配置了 TRIS 寄存器把它设成输出就会出问题。所以配置 SPI 引脚的时候MISO 对应的 TRIS 位一定要设为 1。5.2 写入速度优化技巧如果你觉得 10MHz 的 SPI 时钟还不够快可以试试这几个优化手段第一用 DMA 传输。PIC24FJ128GA310 有 6 个 DMA 通道把 SPI2 的发送和接收映射到 DMACPU 只需要配置好源地址、目的地址和长度剩下的交给 DMA 硬件完成。实测用 DMA 之后写 512KB 的 CPU 占用率从 85% 降到 12%速度也略有提升。第二用 16 位传输模式。8 位模式下每个字节都要进一次中断或者轮询16 位模式一次传两个字节效率翻倍。但要注意数据对齐奇数字节长度的数据要特殊处理。第三减少 WREN 和 WRDI 的发送次数。如果连续写入多个不连续的地址块可以在第一次写入前发 WREN最后一次写入后发 WRDI中间不用重复发送。但要注意如果中间有 CS 拉高的操作WREN 状态可能会丢失需要重新发送。5.3 高温环境下的可靠性注意事项工业设备经常要在高温环境下工作MRAM 在高温下的表现跟常温有些差异我总结了几个注意点125 摄氏度时SPI 最高时钟频率降到 20MHz如果你常温跑 40MHz高温下可能出错建议留足余量高温下写入电流增大电源去耦要更充分必要时加 10uF 钽电容数据保持力在 125 摄氏度时标称 10 年但如果你要求 20 年建议降低工作温度或者做数据刷新温度循环测试要做够-40 到 125 摄氏度循环 1000 次检查数据完整性我在实际项目中做过一个加速寿命测试125 摄氏度下连续写入 100 万次每小时读回校验一次持续 30 天没有出现数据丢失或写入失败。这个结果让我对 MRAM 在工业场景的可靠性比较有信心。5.4 与 Flash 方案的对比与选型建议最后说一下选型。MRAM 虽好但价格比同容量 SPI Flash 贵不少。如果你的应用场景是低频写入、对成本敏感那 Flash 还是更合适。但如果你遇到下面这些情况MRAM 的优势就体现出来了写入频率高每天写入超过 1 万次掉电不能丢数据且没有时间做擦除维护周期要求 5 年以上不想换电池工作温度范围宽-40 到 125 摄氏度需要字节级随机写入不想按扇区管理我个人的经验是数据采集类项目如果采样率超过 1kHz或者需要记录掉电瞬间的状态直接上 MRAM省下来的调试时间和维护成本远超芯片差价。如果只是存个配置参数一年写不了几次那 Flash 完全够用。这套 MR25H40CDF 加 PIC24FJ128GA310 的方案我已经在三个工业项目里量产使用最长的已经跑了两年多现场零故障。如果你正在做类似的东西希望这些经验能帮你少踩几个坑。