ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MRAM与PIC32MZ工业存储方案:从选型到掉电保护实战

MRAM与PIC32MZ工业存储方案:从选型到掉电保护实战 1. 项目缘起与方案选型为什么是 MRAM 加 PIC32MZ1.1 一个真实的需求场景去年接了一个工业数据采集终端的活儿设备装在配电柜里环境温度夏天能到 70 度冬天户外柜能到零下 30 度。需求说起来不复杂每 100ms 采集一次三相电流电压本地要保存最近 72 小时的原始波形数据掉电不能丢而且设备寿命要求 10 年以上。一开始我选的是常见的 SPI NOR Flash 加铁电存储器的组合结果现场跑了三个月就出问题了——NOR Flash 在频繁写入的场景下扇区擦写次数很快就逼近上限而铁电虽然写入寿命够但容量太小存不下波形数据。后来重新选型把目光放到了 MRAM 上。MR25H40CDF 这颗 4Mbit 的磁性随机存储器SPI 接口读写寿命理论上无限写入速度是纳秒级不需要擦除操作掉电数据保持 20 年以上。主控这边选了 PIC32MZ2048EFH144200MHz 主频2MB Flash512KB RAM自带 6 个 SPI 模块硬件 CRC 校验工业级温度范围 -40 到 85 度正好匹配需求。这个组合我前后调了大概两个月踩了不少坑也总结了一些经验。下面把整个方案的设计思路、硬件连接、软件驱动、数据管理策略和实际调试中遇到的问题都梳理一遍给做类似工业嵌入式存储方案的朋友做个参考。1.2 为什么不用 EEPROM、NOR Flash 或 FRAM选型这件事核心是看三个维度写入寿命、写入速度、数据保持能力。我把常见方案列了个表对比存储类型写入寿命次写入速度擦除需求数据保持容量范围工业适用性EEPROM100万毫秒级需要10年KB级一般NOR Flash10万毫秒级需要按扇区10年MB级频繁写入场景差FRAM10^14纳秒级不需要10年KB-MB级容量受限MRAM10^15纳秒级不需要20年MB级优秀MR25H40CDF 的 4Mbit 容量换算过来是 512KB对于存波形数据来说如果做压缩和环形缓冲72 小时的数据是够用的。关键是它不需要擦除写一个字节就是写一个字节没有扇区概念这对频繁小数据写入的场景太友好了。PIC32MZ2048EFH144 这边我选它的原因有几个SPI 模块支持 8/16/32 位传输模式有 4 级 FIFO支持 DMA 触发这些特性对高速连续读写 MRAM 很关键。另外它的 SPI 时钟可以跑到 50MHzMR25H40CDF 最高支持 40MHz匹配得上。1.3 整体架构设计整个数据存储系统分三层最底层是 MR25H40CDF 芯片通过 SPI 总线和主控连接中间层是驱动层负责 SPI 时序控制、命令解析、地址管理最上层是应用层实现环形缓冲区、数据压缩、掉电保护逻辑。数据流是这样的ADC 采样中断触发采集到的数据先放到 RAM 缓冲区攒够一页比如 256 字节后由低优先级任务写入 MRAM。写入时采用双备份加 CRC 校验的方式每个数据块存两份读的时候对比校验确保数据可靠性。地址空间划分上前 64KB 存系统配置和元数据后面 448KB 做环形数据区。这个架构的好处是写入操作和采集操作解耦不会因为存储慢而丢采样点。MRAM 的写入速度足够快256 字节的页写入在 40MHz SPI 下大概 60 微秒左右对 100ms 的采集周期来说完全不是瓶颈。2. 硬件连接与 SPI 时序要点2.1 引脚连接与硬件设计注意事项MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准CS、SO、SI、SCK、VDD、VSS、WP、HOLD。和 PIC32MZ 的连接方式如下CS 接 PIC32MZ 的 SPI 片选引脚我用的是 SPI2CS 接 RG9SO 接 SDI2RG7SI 接 SDO2RG8SCK 接 SCK2RG6WP 和 HOLD 都上拉到 VDD因为我们不需要硬件写保护和暂停功能这里有个细节要注意MR25H40CDF 的 WP 引脚是低电平有效如果悬空可能会被干扰导致误写保护。我一开始偷懒没接结果调试时发现偶尔写不进去查了半天才发现是 WP 引脚受干扰了。后来直接 10K 上拉到 3.3V问题解决。PCB 布线方面SPI 信号线尽量等长SCK 走线要远离模拟信号。我在第一版板子上把 SCK 和 ADC 的模拟输入走得很近结果采样噪声明显增大。第二版把 SCK 包地处理并且和模拟区域拉开 5mm 以上距离噪声问题就没了。另外 CS 信号线如果比较长建议加一个 22 欧姆的串联电阻做阻抗匹配抑制过冲。2.2 SPI 模式选择与时钟配置MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我选的是模式 0因为 PIC32MZ 的 SPI 模块在模式 0 下配置最简单而且大部分例程都是这个模式。时钟配置这块需要算一下。PIC32MZ 的 SPI 时钟来源于外设总线时钟我用的系统时钟是 200MHz外设总线时钟是 100MHz。SPI 波特率计算公式是SPI 时钟 外设总线时钟 / (2 * (SPIxBRG 1))要得到 40MHz 的 SPI 时钟SPIxBRG 应该设为 0这样 SPI 时钟 100MHz / 2 50MHz。但 MR25H40CDF 最高只支持 40MHz所以我把 SPIxBRG 设为 1得到 100MHz / 4 25MHz。实测 25MHz 下读写都很稳定误码率为零。如果想跑更快可以试试 SPIxBRG0 的 50MHz但要注意信号完整性问题走线长的话可能会出错。提示SPI 时钟极性配置错误是新手最常见的坑。如果发现读出来的数据全是 0xFF 或者 0x00先检查 CPOL 和 CPHA 设置是否正确。2.3 上电初始化和写保护处理MR25H40CDF 上电后需要等待一段时间才能操作数据手册上写的是 tPUWPower-Up Wait典型值 1ms。我一般在上电初始化时加 5ms 延时确保芯片内部电路稳定。初始化流程是这样的配置 SPI 模块模式 025MHz8 位传输手动片选拉低 CS发送 WREN0x06命令使能写操作发送 WRSR0x01命令写入状态寄存器清除块保护位拉高 CS完成初始化这里有个容易忽略的点MR25H40CDF 的状态寄存器里有 BP0、BP1 两个块保护位默认可能是保护状态。如果不先清除这些位写操作会被静默忽略读出来全是 0xFF。我建议初始化时直接读一次状态寄存器确认 BP 位为 0如果不是就写一次清除。3. 驱动层实现从底层读写到数据管理3.1 底层 SPI 读写函数封装驱动层我分成了三个层次寄存器级操作、字节级读写、页级读写。寄存器级操作直接操作 PIC32MZ 的 SPI 寄存器字节级读写封装了 CS 控制和单字节传输页级读写实现了连续多字节传输。先看字节级读写的核心代码// SPI 单字节传输 uint8_t SPI_TransferByte(uint8_t data) { while (SPI2STATbits.SPITBF); // 等待发送缓冲空 SPI2BUF data; // 写入发送缓冲 while (!SPI2STATbits.SPIRBF); // 等待接收完成 return SPI2BUF; // 返回接收数据 } // 读 MRAM 数据 void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI_TransferByte(0x03); // READ 命令 SPI_TransferByte((addr 16) 0xFF); // 地址高字节 SPI_TransferByte((addr 8) 0xFF); // 地址中字节 SPI_TransferByte(addr 0xFF); // 地址低字节 for (uint32_t i 0; i len; i) { buf[i] SPI_TransferByte(0x00); } MRAM_CS_HIGH(); }写操作稍微复杂一点因为每次写之前都要发 WREN 命令// 写 MRAM 数据 void MRAM_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI_TransferByte(0x06); // WREN 命令 MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI_TransferByte(0x02); // WRITE 命令 SPI_TransferByte((addr 16) 0xFF); SPI_TransferByte((addr 8) 0xFF); SPI_TransferByte(addr 0xFF); for (uint32_t i 0; i len; i) { SPI_TransferByte(buf[i]); } MRAM_CS_HIGH(); // 等待写入完成 while (MRAM_IsBusy()); }这里有个性能优化的点WREN 命令和 WRITE 命令之间需要拉高 CS 再拉低这是 SPI 协议要求的。如果连续写多个页每页都要重复这个过程。我实测下来25MHz SPI 下写 256 字节大概需要 120 微秒其中 WREN 和 CS 切换占了大概 20 微秒。如果对写入速度要求极高可以考虑用 DMA 传输把 CPU 解放出来。3.2 状态寄存器与忙等待处理MR25H40CDF 的写入操作是异步的发出 WRITE 命令后芯片内部需要时间完成实际写入。虽然 MRAM 的写入速度是纳秒级但芯片内部的状态机切换需要时间数据手册上写的是 tWR 典型值 10 毫秒。等等这里我要纠正一下MR25H40CDF 的 tWR 其实非常短因为 MRAM 不需要擦除实际写入完成时间在微秒级。但为了保险我还是实现了忙等待逻辑// 检查芯片是否忙 uint8_t MRAM_IsBusy(void) { uint8_t status; MRAM_CS_LOW(); SPI_TransferByte(0x05); // RDSR 命令 status SPI_TransferByte(0x00); MRAM_CS_HIGH(); return (status 0x01); // 返回 WIP 位 }实测下来忙等待循环通常执行 0 到 1 次就退出了说明 MRAM 的写入确实很快。但为了代码的健壮性我还是加了一个超时计数器防止芯片异常时死循环uint32_t timeout 100000; while (MRAM_IsBusy() timeout--); if (timeout 0) { // 记录错误日志 Log_Error(MRAM write timeout); }3.3 环形缓冲区设计与地址管理数据存储我采用的是环形缓冲区结构把 448KB 的数据区当成一个循环队列。每个数据块固定 256 字节包含 4 字节的块头序号、时间戳低 16 位、CRC16和 252 字节的有效数据。地址管理用一个写指针和一个读指针写指针指向下一个要写入的块读指针指向最老的未读块。当写指针追上读指针时说明缓冲区满了这时候有两种策略覆盖最老数据或者停止写入。工业场景下我选择覆盖最老数据因为最新的数据通常更重要。块头的设计有个讲究序号用 32 位循环计数方便上位机判断是否有数据丢失。时间戳用 16 位单位是 100ms可以表示 6553.6 秒大概 1.8 小时配合序号可以推算出绝对时间。CRC16 校验整个块的数据读的时候先校验再使用。typedef struct { uint32_t seq; // 块序号 uint16_t timestamp; // 时间戳单位 100ms uint16_t crc; // CRC16 校验 uint8_t data[252]; // 有效数据 } DataBlock_t;写入的时候先计算 CRC然后调用 MRAM_Write 写入整个块。读取的时候先读块头校验 CRC如果校验失败就尝试读备份块。每个块我都在 MRAM 里存了两份地址相差 256KB这样即使某个区域出现坏块数据也不会丢。4. 数据可靠性与掉电保护策略4.1 双备份加 CRC 校验的实现工业现场电磁干扰大单份数据存储风险太高。我的做法是每个数据块存两份主份和备份分别位于 MRAM 地址空间的两端。写入时先写主份再写备份读取时先读主份CRC 校验通过就用主份不通过就读备份。CRC 校验我用的是 CRC-16/CCITT 算法多项式 0x1021初始值 0xFFFF。这个算法在 PIC32MZ 上有硬件加速模块但我为了代码可移植性用的是软件查表法。查表法速度快256 字节的块校验大概 20 微秒左右完全可以接受。// CRC16 查表法 static const uint16_t crc16_table[256] { ... }; uint16_t CRC16_Calculate(const uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc (crc 8) ^ crc16_table[((crc 8) ^ data[i]) 0xFF]; } return crc; }实测下来双备份加 CRC 的方案在强干扰环境下表现很好。我做过一个测试用对讲机在设备旁边发射单份存储的方案大概每 1000 次写入会出现 1 到 2 次数据错误双备份方案一次错误都没有。4.2 掉电检测与数据保存掉电保护是工业设备的基本要求。PIC32MZ 有一个内置的掉电检测模块BOR可以配置成在电源电压低于阈值时触发中断。我在中断里做了一件事把 RAM 缓冲区里还没写入 MRAM 的数据紧急写入。这里有个时间窗口的问题。电源从正常电压掉到芯片无法工作大概有 10 到 20 毫秒的窗口期取决于电源的滤波电容大小。我的板子上用了 470uF 的电解电容加 100nF 的陶瓷电容实测窗口期大概 15 毫秒。在这 15 毫秒里我需要完成检测到掉电、进入中断、把缓冲区数据写入 MRAM、记录掉电事件。15 毫秒够不够算一下缓冲区最多 1KB 数据25MHz SPI 下写入 1KB 大概 400 微秒加上 CRC 计算和双备份写入总共不超过 1 毫秒。所以时间绰绰有余。// 掉电中断处理 void __ISR(_BOR_VECTOR, IPL7AUTO) BOR_Handler(void) { // 立即保存缓冲区数据 SaveBufferToMRAM(); // 记录掉电事件 Log_PowerFailEvent(); // 清除中断标志 IFS1bits.BORIF 0; }注意掉电中断的优先级要设到最高IPL7否则可能被其他中断打断导致数据保存不完整。4.3 数据保持与寿命评估MR25H40CDF 的数据保持能力是 20 年写入寿命是 10^15 次。这两个指标对工业应用来说都远远够用。我算过一笔账假设设备每秒写入 10 次每次写 256 字节一年写入 3.15 亿次10 年 31.5 亿次远低于 10^15 的上限。数据保持方面20 年足够覆盖设备的设计寿命。不过有个细节要注意MRAM 的数据保持能力是在一定温度范围内的。数据手册上写的是 20 年 85 度如果温度更高保持时间会缩短。我的设备最高工作温度 70 度所以 20 年的指标是保守的。另外虽然 MRAM 写入寿命很长但 SPI 接口的电气寿命是有限的。CS 引脚每次拉低拉高都是一次电气操作虽然不像 Flash 那样有明确的次数限制但长期高频操作还是要注意。我的做法是尽量合并写入操作攒够一页再写减少 CS 切换次数。5. 调试实录与常见问题排查5.1 读出来全是 0xFF 或 0x00 怎么办这是调试 SPI 存储芯片最常见的问题我遇到过好几次原因各不相同。整理了一个排查表现象可能原因排查方法解决方法读出全 0xFFCS 未正确拉低示波器看 CS 波形检查 CS 引脚配置和代码读出全 0xFFSPI 模式错误对比 CPOL/CPHA 设置改为模式 0 或模式 3读出全 0x00MISO 未连接万用表测通断重新焊接或飞线读出全 0x00芯片未供电测 VDD 电压检查电源连接数据错位时钟太快降低 SPI 时钟从 25MHz 降到 10MHz 测试偶尔出错信号干扰示波器看信号质量加串联电阻或缩短走线我第一次调试时遇到的是 SPI 模式问题。PIC32MZ 的 SPI 模块默认是模式 0但 MR25H40CDF 我误以为它需要模式 3结果读出来全是 0xFF。后来查数据手册确认支持模式 0改过来就正常了。5.2 写入不生效的几种情况写入不生效比读取出错更隐蔽因为读操作可能正常但写进去的数据读出来还是旧值。我遇到过三种情况第一种是 WREN 命令没发或者发错了。MR25H40CDF 要求每次写操作前都要发 WREN如果漏了这一步WRITE 命令会被忽略。我的做法是在 MRAM_Write 函数里强制发 WREN不依赖上层调用。第二种是状态寄存器的块保护位没清除。这个前面提过BP0 和 BP1 位默认可能是保护状态。初始化时一定要读一次状态寄存器确认。第三种是写入地址越界。MR25H40CDF 的地址空间是 0x00000 到 0x7FFFF如果传入的地址超过这个范围写入会被忽略。我在 MRAM_Write 函数里加了地址检查if (addr len MRAM_SIZE) { Log_Error(MRAM address out of range); return; }5.3 SPI 通信不稳定怎么调SPI 通信不稳定通常表现为偶尔读到错误数据或者写入成功率不是 100%。我总结了几条经验先降速。把 SPI 时钟从 25MHz 降到 10MHz如果问题消失说明是信号完整性问题。这时候再逐步提高时钟找到稳定的最高频率。检查电源去耦。MR25H40CDF 的 VDD 引脚旁边一定要放 100nF 的陶瓷电容越近越好。我一开始放在离芯片 10mm 的地方后来挪到 2mm 以内通信稳定性明显提升。检查地线。SPI 的 GND 要和主控的 GND 良好连接最好用星型接地。如果地线上有压差SPI 电平判断会出错。用示波器看波形。重点看 SCK 的上升沿和下降沿是否陡峭有没有过冲或振铃。如果有加串联电阻或者调整驱动能力。5.4 实际项目中的踩坑记录说几个我在这个项目里实际踩过的坑都是文档里不会写的。第一个坑PIC32MZ 的 SPI 模块在配置为手动片选模式时CS 引脚需要配置为普通 GPIO 输出而不是 SPI 功能引脚。我一开始把 CS 配置成了 SPI 功能结果 CS 信号不受代码控制一直处于低电平导致通信混乱。后来改成 GPIO 输出问题解决。第二个坑MRAM 的写入速度虽然快但连续写入时如果不等忙状态清除就发下一个命令会导致数据错乱。我一开始为了追求速度在 WRITE 命令后没有等待就发下一个 WREN结果写进去的数据偶尔会错位。后来老老实实加忙等待问题消失。第三个坑掉电中断里调用了 printf 调试输出导致保存时间过长数据没保存完就断电了。后来把调试输出全部去掉只保留必要的保存操作问题解决。这个坑让我明白中断里绝对不能做耗时操作。第四个坑PCB 布局时把 MRAM 放在了开关电源旁边电源的开关噪声耦合到了 SPI 信号上导致通信误码率很高。后来把 MRAM 挪到远离电源的位置并且在电源入口加了 LC 滤波问题解决。6. 性能实测与优化建议6.1 读写速度实测数据我在 25MHz SPI 时钟下做了一组读写速度测试结果如下操作类型数据量耗时等效速度单字节读1 字节2.1 微秒0.48 MB/s页读256 字节95 微秒2.69 MB/s单字节写1 字节8.5 微秒0.12 MB/s页写256 字节120 微秒2.13 MB/s连续页写4KB1.8 毫秒2.22 MB/s单字节操作效率低是因为每次都要发命令和地址开销大。页操作效率高很多因为命令和地址只发一次。所以实际使用中一定要攒够一页再写不要一个字节一个字节地写。连续页写 4KB 的速度是 2.22 MB/s比单页写略高因为减少了 CS 切换开销。如果开启 DMA速度还能再提升 20% 左右。6.2 如何进一步提升读写效率如果对速度有更高要求有几个优化方向第一用 DMA 传输。PIC32MZ 的 SPI 模块支持 DMA 触发可以把数据搬运的工作交给 DMACPU 只负责配置和等待。我实测 DMA 模式下4KB 连续写入速度可以到 2.8 MB/s。第二提高 SPI 时钟。25MHz 是保守值实际可以试试 40MHz。但要注意信号完整性可能需要调整 PCB 走线和端接电阻。第三减少 CS 切换。如果连续写多个页可以在一次 CS 拉低期间完成所有页的写入。MR25H40CDF 支持连续写入地址会自动递增。第四用 32 位传输模式。PIC32MZ 的 SPI 支持 32 位传输一次传 4 个字节可以减少传输次数。但 MR25H40CDF 是 8 位接口需要确认它是否支持 32 位对齐传输。我查了数据手册它支持任意字节长度的连续传输所以 32 位模式是可行的。6.3 功耗表现与低功耗设计这个项目对功耗没有特别要求但我还是测了一下。MR25H40CDF 的待机电流是 10 微安左右读写电流大概 5 毫安。PIC32MZ 在 200MHz 下运行电流大概 80 毫安。整体功耗在 300 毫瓦左右对于市电供电的工业设备来说完全不是问题。如果要做低功耗设计可以考虑这几个方向降低 SPI 时钟频率、减少写入次数、在空闲时把 MRAM 置于待机模式。MR25H40CDF 有一个 SLEEP 命令0xB9进入后电流降到 2 微安唤醒只需要发一个命令。7. 方案扩展与个人体会7.1 这个方案还能怎么用MR25H40CDF 加 PIC32MZ 的组合不只能做数据存储还可以用在很多场景。比如做配置参数存储把设备的校准参数、网络配置、用户设置存在 MRAM 里掉电不丢而且写入次数无限用户可以随便改。再比如做日志记录把设备的运行日志、错误记录存在 MRAM 里方便现场排查问题。我还把这个方案用在了另一个项目上一个便携式医疗设备需要记录患者的生理参数。MRAM 的低功耗和快速写入特性非常适合这种场景设备可以在检测到异常时立即保存数据不会因为存储延迟而丢失关键信息。如果数据量更大可以考虑用多颗 MR25H40CDF 并联通过不同的 CS 引脚选择芯片容量可以扩展到几 MB。PIC32MZ 有 6 个 SPI 模块最多可以挂 6 组 MRAM总容量 3MB。7.2 给后来者的几点建议如果你也在做类似的工业存储方案我有几个建议。选型阶段一定要算清楚写入寿命和容量需求。不要只看价格工业场景下可靠性比价格重要得多。MRAM 比 Flash 贵不少但省下来的维护成本和数据丢失风险远远超过那点差价。硬件设计阶段一定要留测试点。SPI 的 SCK、CS、SI、SO 都要留测试点方便用示波器抓波形。我第一版板子没留测试点调试时只能飞线很麻烦。软件设计阶段一定要做错误处理和日志记录。SPI 通信出错是常态关键是要能检测到错误并恢复。我的做法是每次读写都校验出错就重试重试三次还失败就记录错误日志并切换备份块。调试阶段一定要有耐心。SPI 通信问题往往不是单一原因可能是硬件、软件、时序多个因素叠加。我的经验是先用示波器确认硬件信号正常再排查软件配置最后调时序参数。最后说一个个人体会MRAM 这种新型存储介质用起来和传统 Flash 差别很大。最大的差别是不需要擦除写之前不用考虑扇区对齐这简化了很多软件逻辑。但也要注意MRAM 的写入次数虽然无限但 SPI 接口的电气寿命是有限的所以还是要尽量减少不必要的写入操作。我在实际项目中会在 RAM 里做一层缓存攒够一页再写这样既提高了效率又减少了 SPI 操作次数。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进