工业级SPI MRAM与PIC24FJ128GA310的嵌入式存储实战
1. 为什么工业现场还在用 SPI 接 MRAM而不是直接上 Flash如果你拆过工业控制器、电力监测终端或者车载数据记录仪大概率会在板子上看到一颗 8 脚的小芯片旁边连着主控的 SPI 引脚。这颗芯片很多时候不是普通的 NOR Flash而是MRAM——磁性随机存储器。我这次要聊的组合就是MR25H40CDF这颗 512Kb 的 SPI MRAM搭配PIC24FJ128GA310这款 16 位单片机做工业与嵌入式场景下的数据存储和读取。先说清楚这个组合解决什么问题。工业现场的数据记录有个很尴尬的特点写入频繁、单次数据量小、掉电随时可能发生、环境温度跨度大而且设备往往要求十年以上的生命周期。用 EEPROM 吧写入速度慢、擦写寿命有限用 NOR Flash 吧写之前要擦除整个扇区掉电时正在擦除的扇区可能直接损坏用 FRAM 吧容量小、价格高。MRAM 恰好卡在中间像 RAM 一样按字节随机写入不需要擦除掉电数据不丢读写寿命几乎无限。MR25H40CDF 是 Everspin 的 512Kb64KBSPI MRAM40MHz SPI 接口2.7V 到 3.6V 供电工业级温度范围 -40 到 85 摄氏度。PIC24FJ128GA310 是 Microchip 的 16 位 MCU128KB Flash、8KB RAM带多个 SPI 模块本身定位就是工业控制。两者搭配一个负责永不丢失的快速暂存区一个负责逻辑控制和协议处理是很典型的工业数据记录架构。这篇文章适合谁看如果你正在做工业数据采集、设备参数存储、掉电保护记录、黑匣子类的功能或者你手上有 PIC24 平台但不确定怎么把 MRAM 用稳那这篇内容可以直接抄作业。我会从硬件连接、SPI 时序配置、读写驱动、掉电保护策略、常见踩坑几个角度完整讲一遍中间会穿插我实际调试时遇到的问题和解决办法。提示MRAM 和 Flash 最大的区别是写之前不用擦。这个特性决定了驱动层的设计思路完全不同后面会重点展开。2. MR25H40CDF 的引脚、时序与 PIC24FJ128GA310 的硬件对接2.1 先搞清楚 MR25H40CDF 的引脚定义和工作模式MR25H40CDF 是 8 脚 SOIC 封装引脚定义很标准引脚名称功能说明1CS片选低电平有效2SO数据输出MISO3WP写保护低电平有效4VSS地5SI数据输入MOSI6SCK时钟7HOLD保持低电平有效8VDD电源 2.7V~3.6V这里有两个容易被忽略的引脚WP 和 HOLD。WP 拉低时芯片进入硬件写保护所有写操作被拒绝HOLD 拉低时当前 SPI 传输暂停时钟可以继续跑但数据不采样。很多人在原理图阶段直接把这两个脚接地结果发现写不进去数据排查半天才发现是 WP 被拉低了。我的建议是WP 和 HOLD 都不要直接接地而是通过 10K 上拉到 VDD。如果确实需要硬件写保护功能WP 可以接到 MCU 的一个 GPIO 上正常工作时输出高电平需要保护时拉低。HOLD 如果不使用上拉到 VDD 即可不要悬空。2.2 PIC24FJ128GA310 的 SPI 模块配置要点PIC24FJ128GA310 有多个 SPI 模块我用的是 SPI1。配置 SPI 的核心寄存器是 SPI1CON1、SPI1CON2、SPI1STAT。工业场景下我推荐的配置是主模式MSTEN 1时钟极性CKP 0空闲低电平时钟边沿CKE 1数据在时钟上升沿采样字长MODE16 08 位模式预分频主预分频 4:1次预分频 2:1MR25H40CDF 支持 SPI Mode 0 和 Mode 3。Mode 0 是 CKP0、CKE1也就是空闲低、上升沿采样。这个模式和大多数 SPI Flash 一致配置起来最省事。关于时钟频率MR25H40CDF 最高支持 40MHz但 PIC24FJ128GA310 在 32MHz 系统时钟下SPI 最高能跑到 16MHzFCY/2。实际工业场景我不建议跑满8MHz 左右是比较稳的选择。原因有两个一是长走线时高速 SPI 的信号完整性会变差二是 MRAM 的写入时序在高温下需要留裕量。计算一下如果 FCY 16 MIPS32MHz 晶振PLL 后主预分频 4:1、次预分频 2:1SPI 时钟 16MHz / (4 × 2) 2MHz。这个速度对 MRAM 来说偏慢但胜在稳定。如果要提速可以把主预分频改成 2:1得到 4MHz再改成 1:1得到 8MHz。// PIC24FJ128GA310 SPI1 初始化示例 void SPI1_Init(void) { SPI1STATbits.SPIEN 0; // 先关闭 SPI SPI1CON1bits.MSTEN 1; // 主模式 SPI1CON1bits.CKP 0; // 空闲低电平 SPI1CON1bits.CKE 1; // 上升沿采样 SPI1CON1bits.SMP 0; // 中间采样 SPI1CON1bits.MODE16 0; // 8 位模式 SPI1CON1bits.PPRE 0b10; // 主预分频 4:1 SPI1CON1bits.SPRE 0b110; // 次预分频 2:1 SPI1STATbits.SPIROV 0; // 清除溢出标志 SPI1STATbits.SPIEN 1; // 使能 SPI }2.3 硬件走线和去耦的实战经验SPI 虽然只有四根线但在工业板上走不好照样出问题。我踩过的坑包括SCK 和 MISO 平行走线太长导致串扰、CS 没有单独走线而是和别的信号共用、去耦电容离芯片太远。正确的做法是MRAM 的 VDD 引脚旁边放一个 0.1uF 的陶瓷电容距离不超过 2mm。如果板子上有多个 SPI 从设备每个从设备的 CS 都要独立走线不要用译码器共用。SCK 走线尽量短如果超过 5cm建议在源端串一个 22 到 33 欧姆的电阻做阻抗匹配。还有一点MR25H40CDF 的 WP 和 HOLD 上拉电阻不要省。我见过一个板子为了省两个电阻把 WP 直接接 VDD、HOLD 悬空结果在湿度大的环境下 HOLD 引脚受干扰SPI 通信间歇性失败。后来补上 10K 上拉就稳定了。3. 驱动层怎么写从单字节读写到页写入的完整实现3.1 MR25H40CDF 的指令集和地址空间MR25H40CDF 的指令集很精简常用的就几条指令操作码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据地址空间是 64KB地址从 0x0000 到 0xFFFF用 16 位地址寻址。注意 MR25H40CDF 的 READ 和 WRITE 指令后面跟的是 16 位地址高位在前。状态寄存器的 bit0 是 WEL写使能锁存bit1 是 BP0bit2 是 BP1bit7 是 SRWD。写操作之前必须先发 WREN 指令把 WEL 置 1写完自动清零。这个和 EEPROM、Flash 的逻辑一样。3.2 单字节读写函数最基础也最容易写错的部分先看读一个字节// 读取 MR25H40CDF 指定地址的一个字节 uint8_t MRAM_ReadByte(uint16_t addr) { uint8_t data; MRAM_CS_LOW(); // 片选拉低 SPI1_ExchangeByte(0x03); // 发送 READ 指令 SPI1_ExchangeByte(addr 8); // 地址高字节 SPI1_ExchangeByte(addr 0xFF); // 地址低字节 data SPI1_ExchangeByte(0xFF); // 读数据 MRAM_CS_HIGH(); // 片选拉高 return data; }写一个字节稍微复杂因为要先发 WREN// 写入 MR25H40CDF 指定地址的一个字节 void MRAM_WriteByte(uint16_t addr, uint8_t data) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte(addr 8); SPI1_ExchangeByte(addr 0xFF); SPI1_ExchangeByte(data); MRAM_CS_HIGH(); // 等待写入完成 while(MRAM_ReadStatus() 0x01); }这里有个关键点WREN 和 WRITE 之间必须拉高 CS。很多人习惯把 WREN 和 WRITE 放在同一个 CS 低电平周期里这对 MRAM 是不行的。MR25H40CDF 要求 WREN 指令在独立的 CS 周期内完成然后再开一个新的 CS 周期发 WRITE。3.3 页写入和连续读取提升吞吐量的关键单字节写虽然简单但每次都要发 WREN、发地址、等写入完成效率很低。MR25H40CDF 支持页写入一页是 256 字节。连续写的时候地址低 8 位会自动递增超过 255 会回绕到页首所以跨页写入必须手动分页。// 页写入注意不能跨页 void MRAM_WritePage(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t page_end (addr 0xFF00) 0x100; if(addr len page_end) { len page_end - addr; // 截断到页边界 } MRAM_CS_LOW(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte(addr 8); SPI1_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { SPI1_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); while(MRAM_ReadStatus() 0x01); }连续读取就简单多了发完 READ 指令和起始地址后可以一直读地址自动递增直到 CS 拉高。// 连续读取 void MRAM_ReadBuffer(uint16_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI1_ExchangeByte(0x03); SPI1_ExchangeByte(addr 8); SPI1_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { buf[i] SPI1_ExchangeByte(0xFF); } MRAM_CS_HIGH(); }3.4 SPI 字节交换函数的实现细节上面反复用到的SPI1_ExchangeByte是底层函数实现要注意等待标志位uint8_t SPI1_ExchangeByte(uint8_t txData) { SPI1BUF txData; while(!SPI1STATbits.SPIRBF); // 等待接收完成 return SPI1BUF; }这里有个坑必须先写 SPI1BUF 再等 SPIRBF。如果反过来先等标志再写第一次调用会死等。另外读 SPI1BUF 会自动清 SPIRBF 标志所以不需要手动清。注意PIC24 的 SPI 在高速时钟下如果两个字节之间间隔太短可能出现 SPIROV 溢出。工业场景建议在连续读写时检查 SPIROV如果置位要重新初始化 SPI。4. 掉电保护与数据可靠性MRAM 真正的价值所在4.1 为什么 MRAM 的掉电保护比 Flash 简单得多Flash 的掉电保护是个老大难问题。写 Flash 要先擦除扇区擦除过程中如果掉电整个扇区数据可能变成 0xFF 或者不确定状态。所以用 Flash 做数据记录通常要设计双备份、日志结构、磨损均衡代码复杂度很高。MRAM 没有这个问题。MR25H40CDF 的写入是直接覆盖不需要擦除写入过程中掉电要么是旧数据要么是新数据不会出现中间态。这个特性让掉电保护逻辑大大简化。我实际项目里的做法是把 64KB 空间分成若干条记录每条记录带一个序号和 CRC 校验。写入时先写数据区再写序号和 CRC。读取时扫描所有记录找序号最大且 CRC 正确的作为最新数据。因为 MRAM 写入快单字节写入时间在微秒级掉电窗口极小。4.2 用状态寄存器做写保护的分级策略MR25H40CDF 的状态寄存器有 BP0 和 BP1 两位可以设置不同级别的写保护BP1BP0保护范围00无保护01保护高 1/4 地址空间10保护高 1/2 地址空间11保护整个地址空间工业场景下我通常把参数区放在低地址记录区放在高地址。正常运行时全片可写当设备进入参数锁定模式时把 BP10、BP01保护高 1/4 空间防止记录区被误写。这个策略比软件标志位可靠因为它是硬件级别的保护。4.3 写入完成判断轮询状态寄存器还是固定延时MRAM 的写入速度很快但并不是发完就完。WRITE 指令的最后一个字节移入后芯片内部还需要时间完成实际存储。这个时间典型值是几十纳秒到几百纳秒但数据手册给的最大值是 5ms实际上远小于这个值。我见过两种做法一种是发完 WRITE 后直接延时 1ms 再操作另一种是轮询状态寄存器的 WIP 位。推荐轮询方式因为延时是拍脑袋的轮询是确定性的。uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI1_ExchangeByte(0x05); status SPI1_ExchangeByte(0xFF); MRAM_CS_HIGH(); return status; }状态寄存器的 bit0 是 WIPWrite In Progress写入完成时该位为 0。轮询这个位比固定延时靠谱得多。4.4 数据校验CRC16 在 PIC24 上的轻量实现工业数据不能只靠写进去就行必须有校验。我在 PIC24FJ128GA310 上用的是一个查表法的 CRC16-CCITT表放在 Flash 里运行时查表计算。64KB 数据全量校验一次大概几十毫秒对于周期性记录场景完全够用。// CRC16-CCITT 查表法 uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while(len--) { crc (crc 8) ^ crc16_table[((crc 8) ^ *data) 0xFF]; } return crc; }每条记录的结构我设计成4 字节序号 N 字节数据 2 字节 CRC。读取时先校验 CRC再比较序号确保拿到的是完整且最新的数据。5. 调试过程中真实踩过的坑和排查思路5.1 读出来全是 0xFF 或者 0x00这是最常见的现象。第一次调 MRAM读出来全是 0xFF第一反应是芯片坏了。其实大概率是三个原因第一CS 没有正确拉低。用逻辑分析仪抓一下 CS 引脚确认读操作期间 CS 确实是低电平。我遇到过 CS 配置成开漏输出但没加上拉导致 CS 一直是高。第二SPI 模式不对。MR25H40CDF 支持 Mode 0 和 Mode 3如果 MCU 配成 Mode 1 或 Mode 2数据会错位。用示波器看 SCK 空闲电平Mode 0 空闲低Mode 3 空闲高。第三WP 引脚被拉低。这个最隐蔽因为读操作不受 WP 影响但写操作会被拒绝。如果你发现读正常但写不进去先量一下 WP 引脚电压。5.2 写入后读出来数据不对但偶尔又对这种间歇性错误通常是时序问题。我遇到过一次SPI 时钟跑到 16MHz短走线时正常但板子装进金属外壳后分布电容变化导致 SCK 边沿变缓数据采样出错。后来把时钟降到 8MHz问题消失。另一个可能是写入后没有等待完成就发起下一次操作。MRAM 虽然快但连续写不同地址时如果不等 WIP 清零就发下一条 WRITE芯片可能忽略第二条指令。我的做法是每次写操作后都轮询 WIP虽然牺牲一点速度但稳定性大幅提升。5.3 高温环境下数据出错工业设备要做高低温测试。我在 85 摄氏度环境下测试时发现偶尔有记录丢失。排查后发现两个问题一是 SPI 时钟在高温下裕量不足二是去耦电容用的是 Y5V 材质高温下容量衰减严重。解决办法SPI 时钟从 8MHz 降到 4MHz去耦电容换成 X7R 材质。改完之后高低温循环测试 100 次没有再出现数据错误。提示工业级 MRAM 虽然标称 -40 到 85 摄氏度但外围电路的温漂同样要考虑。电容、电阻的材质在极端温度下表现差异很大。5.4 用逻辑分析仪抓 SPI 波形的正确姿势调试 SPI 问题逻辑分析仪比示波器好用。抓波形时注意几点采样率至少是 SCK 频率的 10 倍触发条件设成 CS 下降沿解码协议选 SPI模式选对。我习惯同时抓 CS、SCK、MOSI、MISO 四根线这样一眼就能看出是命令发错了还是数据错了。有一次我抓到的波形显示WREN 指令发出后 CS 拉高但下一个 CS 周期开始时 SCK 上有一个毛刺。这个毛刺导致 MRAM 误判指令。后来在 SCK 线上串了一个 33 欧姆电阻毛刺消失。6. 把 MRAM 用出价值的几个进阶思路6.1 用 MRAM 做参数存储和日志记录的分区设计64KB 空间说大不大说小不小。我的分区方案是低 16KB 做参数区存设备配置、校准系数、序列号中间 32KB 做循环日志区每条记录 64 字节可以存 512 条高 16KB 做备份区存关键参数的副本。参数区用固定地址访问读写简单。日志区用环形缓冲写满后覆盖最旧记录。备份区在参数更新时同步写入主参数区损坏时可以恢复。6.2 和 Flash 配合使用的混合存储架构MRAM 容量小、价格高不适合存大量数据。我的做法是MRAM 做热数据缓存Flash 做冷数据归档。实时采集的数据先写 MRAM保证掉电不丢当 MRAM 里的记录积累到一定数量再批量搬运到 Flash。这样既利用了 MRAM 的快速写入和掉电安全又利用了 Flash 的大容量和低成本。搬运过程要注意先写 Flash校验成功后再清 MRAM 对应区域避免搬运过程中掉电导致数据丢失。6.3 低功耗场景下的 MRAM 使用技巧MRAM 的待机电流很低但 SPI 接口在空闲时如果 CS 一直拉低芯片会持续耗电。低功耗设计时每次操作完成后把 CS 拉高让 MRAM 进入待机模式。另外如果 MCU 支持可以把 SPI 引脚配置成普通 GPIO 输出低电平进一步降低漏电流。我做过一个电池供电的记录仪MRAM 平时待机电流不到 10 微安每分钟唤醒一次写一条记录整体功耗可以做到几年不用换电池。6.4 从 MR25H40CDF 迁移到更大容量 MRAM 的注意事项如果 64KB 不够用Everspin 有更大容量的 SPI MRAM比如 1Mb、4Mb 的型号。迁移时要注意几点地址位数可能从 16 位变成 24 位指令集基本兼容但地址字节数不同页大小可能变化写入分页逻辑要调整封装可能不同PCB 要改版。我的建议是驱动层做好抽象把地址宽度、页大小做成宏定义迁移时只改配置不改逻辑。7. 写在最后的一些个人体会这套 MR25H40CDF 加 PIC24FJ128GA310 的组合我在三个工业项目里用过最长的已经连续运行四年多没有出现过数据丢失。MRAM 这个器件最大的价值不是速度快而是它让嵌入式工程师不用再为掉电时数据会不会丢这件事焦虑。用 Flash 做记录你要设计日志、磨损均衡、掉电检测、备份恢复代码写几千行用 MRAM核心逻辑可能几百行就搞定了。当然 MRAM 也不是万能的。容量小、单价高这两点决定了它只能用在关键数据的存储上。我的经验是凡是丢了会出大事的数据放 MRAM凡是丢了可以重采的数据放 Flash 或者外部存储。这个原则帮我在多个项目里快速做了存储架构决策。最后分享一个小技巧MRAM 刚上电时建议先读一遍状态寄存器确认 BP0、BP1 都是 0WEL 是 0。如果状态不对先写状态寄存器复位再做其他操作。这个习惯能避免很多莫名其妙写不进去的问题。