工业存储不掉电:基于SPI MRAM与PIC单片机的数据可靠保存方案
做工业嵌入式项目最怕的不是 MCU 性能不够而是数据莫名其妙丢了。前阵子做一台配电监测终端需要不停保存事件记录和校准参数原来用 EEPROM容量不够换成 NOR Flash又嫌擦除逻辑复杂、担心坏块。后来我换成 Everspin MR25H40CDF 与 PIC18F87J11 这套组合MCU 用 SPI 直接读写 MRAM问题一下就利索了。这篇文章就把我自己的选型依据、电路连接、驱动实现、数据完整性策略和调试记录完整写下来。MR25H40CDF 是一颗 4Mbit 串行 SPI MRAM内部是 512K 字节存储空间PIC18F87J11 是 Microchip 的 8 位单片机自带 MSSP 模块做 SPI 主设备很顺手。这套方案适合仪器仪表、工业控制器、电力终端、小型物联网网关这类场景也适合想把数据存储做到“掉电也不慌”的嵌入式开发者参考。1. 为什么是 MR25H40CDF PIC18F87J111.1 工业存储需求先拆清楚很多项目选存储芯片一上来就看容量和价格却忽略了这个场景对“写寿命”和“掉电可靠性”的要求。我这次的需求其实很典型运行参数和标定数据不常改但绝对不能丢丢了设备就是错的状态。事件记录和故障日志会高频追加每次写几十到几百字节一天可能要写几千次。掉电瞬间可能需要保存关键状态比如当前计量累计值、最后一次通信帧、报警发生时刻。这类需求如果硬套 NOR Flash最大的问题是“写之前要擦除”。NOR Flash 的最小擦除单位一般是 4KB 扇区你想改一个 10 字节的参数要先把整个扇区读回来、擦掉、再回写稍不留神就会引入坏块。EEPROM 倒是能单字节写但容量普遍偏小写次数也有限。所以当我看到 MRAM 这种“像 SRAM 一样写、掉电不丢”的东西时立刻觉得这才是工业存储该走的路线。MR25H40CDF 的容量是 4Mbit也就是 512KB对日志型应用完全够用。它没有擦除操作想写哪个地址就直接写写操作没有 Flash 那种“等待内部擦写状态机”的过程数据写到存储单元的机制是磁阻状态切换不存在浮栅电荷损耗也就没有传统意义上“写坏”的概念。对我这种常年做嵌入式小项目的人来说这等于把“磨损均衡”和“坏块管理”这些事直接从需求里删掉了。1.2 MRAM、NOR Flash、EEPROM、FRAM 到底怎么选我一开始也犹豫过 FRAM毕竟 FRAM 同样耐写。但对比之后这颗 MRAM 在容量、SPI 速度和抗干扰方面更贴合我的项目。我自己做选型时习惯拉一个简单的比较表不看厂商宣传只看自己用的指标项目MRAMNOR FlashEEPROMFRAM写入前是否需要擦除不需要需要不需要不需要典型单位写次数极高基本不用考虑磨损约 10 万次约 100 万次很高字节/连续写支持字节和连续写页写为主字节写为主字节和连续写写等待极短几乎即时毫秒级擦写等待毫秒级写周期极短数据掉电保持好好好较好容量密度可以做到较大大小相对有限MR25H40CDF 在实际工程里还有一个容易被忽略的优点它不需要“先读后改”。你直接往地址里写新值旧值就被覆盖了。这让代码逻辑非常简单不用维护“影子缓冲区”也不用担心“擦除一半掉电”这种破事。1.3 为什么单片机选了 PIC18F87J11选单片机不是堆规格是够用。PIC18F87J11 最大的优势不是算力而是“老老实实”。在工业现场电源波动、脉冲群、静电这些东西随时存在8 位 MCU 的启动逻辑、复位逻辑、外设行为都非常成熟不会拿你开玩笑。PIC18F87J11 自带 MSSP 模块天生支持 SPI 主模式而且有独立的 SSP1BUF 发送/接收缓冲底层收发只要几行代码。它的 IO 电平是 3.3V正好和 MR25H40CDF 的 VDD 匹配不需要电平转换。再加上它有 BOR 和 LVD 这类电压检测外设做掉电保护很方便这是工业存储方案里非常关键的一环。如果你以后想做嵌入式架构师或者想把项目做成可复用的嵌入式开源模块建议先把这种“小存储 硬件 SPI 工业级电压监控”的组合跑熟。它虽然不起眼但在大量实际设备里都用到属于嵌入式基本功。2. 硬件连接与电路设计2.1 MR25H40CDF 的引脚和最小信号MR25H40CDF 是 SPI 从机引脚不算多连接起来很轻松。典型器件有这些关键引脚引脚名功能接法建议CS片选低有效接 MCU 的普通 GPIO独立控制SCKSPI 时钟接 MSSP 模块的 SCKSI数据输入接单片机 SDO数据从 MCU 进 MRAMSO数据输出接单片机 SDI数据从 MRAM 回 MCUWP写保护通常直接接 VDDHOLD暂停通信必须接上拉或直接接 VDD不能悬空VDD电源3.3V就近放 0.1uF 去耦电容VSS地接底层地注意 DFN 底部焊盘接地这里要重点说一句CS 不要直接接地也不要和别的 SPI 从机共用一组 CS 逻辑。MRAM 是靠 CS 的下沿开始接收命令、CS 的上沿锁存命令执行的。如果 CS 一直为低它在总线上会不断把其他设备的 SPI 数据当成自己的命令轻则数据错乱重则误写存储区。2.2 最小电路怎么接我的实际接法是单片机和 MRAM 同用一个 3.3V 电源轨VDD 旁边放一个 0.1uF 陶瓷电容再在电源根部放一个 10uF 钽电容。WP 直接接 VDDHOLD 通过 10k 电阻接 VDD。之所以不用 GPIO 控制 WP是因为应用里没有“整片锁死”的需求WP 接高让块保护功能不生效简单可靠。SPI 的四根线SCK、SI、SO、CS全部用推挽输出不加外部上拉。之前我试过在 SPI 线上加上拉电阻反而增加了信号上升沿时间高速时容易产生误采样。如果 PCB 布局不得不把 MRAM 拉得比较远可以在 MCU 输出端串 33 欧姆电阻改善振铃但电阻不要太大。注意HOLD 悬空是这颗芯片最容易踩的坑。HOLD 引脚在串行通信过程中被拉低时MRAM 会暂停当前的 SPI 操作SO 变成高阻等 HOLD 恢复后再继续。如果它悬空现场干扰一旦把它拉低整次读写就会变成半截操作数据自然就错了。所以 HOLD 必须接上拉到 VDD。2.3 PCB 布局与抗干扰经验如果 MRAM 只是放在 MCU 旁边走线短抗干扰压力不大。但工业设备常常要把存储芯片和 MCU 放得比较远或者靠近接插件这时候就要注意几点SCK 和 SO 不要平行长距离走线SCK 的串扰可能直接耦合到 SO 上导致读数据错位。存储芯片下方不要走开关电源的电流回路尤其是高频 dI/dt 比较大的开关节点。VDD 走线要宽最好直接铺铜避免负载变化时电压跌落。如果电路板环境恶劣CS、SCK、SI 上可以并联小电容到地容量选 10pF 左右别超过 22pF否则会把时钟边沿拖得没法看。MR25H40CDF 的工作温度范围和工业级 MCU 是匹配的实际用 -40℃ 到 85℃ 的场景没问题。焊接的时候注意 DFN 封装的散热焊盘焊膏不能少也不能让焊料流到引脚之间。如果手工贴片回流焊后最好用放大镜检查 SI 和 CS 引脚有没有虚焊这类问题在产线上最容易表现为“偶发读写失败”。3. PIC18F87J11 的 SPI 驱动与读写实现3.1 SPI 模式、时钟和 CS 控制MR25H40CDF 支持 SPI Mode 0 和 Mode 3我用的是 Mode 0SCK 空闲为低主设备在时钟下降沿改变数据上升沿采样。PIC18F87J11 的 MSSP1 模块里CKP 位决定 SCK 极性CKE 位决定数据在哪个边沿变化。写驱动前一定要把这两位的组合调成 MRAM 手册要求的时序不能想当然。片选我坚持用普通 GPIO 控制不用 MSSP 模块内部的自动 CS。原因很简单SPI 上可能还有别的设备CPU 需要在多设备之间灵活切换 CS而且 MRAM 的整条命令必须在一个 CS 低电平周期内完成用 GPIO 控制最直观。时钟频率方面我先把系统时钟设在 8MHzMSSP 主模式配成 FOSC/4也就是 2MHz 的 SCK。这个速度对 512KB 存储芯片来说足够快又不会因为飞线和干扰搞得太紧张。如果你把系统时钟提高SCK 可以更快但一定要先量波形。3.2 寄存器初始化和底层收发函数用 XC8 写初始化很简单下面是兼容大多数 PIC18 系列习惯写法。寄存器名在 PIC18F87J11 头文件里是 SSP1CON1、SSP1STAT、SSP1ADD、SSP1BUF如果你的工程用 MCC 生成逻辑是一样的。// 根据自己的板卡定义片选引脚 #define MRAM_CS_TRIS TRISBbits.TRISB4 #define MRAM_CS LATBbits.LATB4 void mram_gpio_init(void) { MRAM_CS_TRIS 0; // 输出 MRAM_CS 1; // 片选默认释放 } void mram_spi_init(void) { // 假设 FOSC 8MHzSPI 时钟 FOSC / (4 * (SSP1ADD 1)) 2MHz SSP1ADD 0; // 按 SPI Mode 0 配置SCK 空闲低上升沿采样 SSP1STATbits.CKE 1; SSP1CON1bits.CKP 0; // SPI 主模式使能 MSSP1 SSP1CON1bits.SSPEN 1; SSP1CON1bits.SSPM3 0; SSP1CON1bits.SSPM2 0; SSP1CON1bits.SSPM1 1; SSP1CON1bits.SSPM0 0; }底层收发其实就是一个函数往 SSP1BUF 写一个字节然后等 BF 置位再读 SSP1BUF。BF 标志会在读取 SSP1BUF 后自动清除。注意不要在中断和主循环里同时使用同一个 MSSP 模块否则返回的数据可能是“下一笔操作”的数据。uint8_t spi_xfer(uint8_t out) { SSP1BUF out; while (!SSP1STATbits.BF) { } return SSP1BUF; }3.3 MRAM 写使能、写入和读取函数MR25H40CDF 写操作前必须先发写使能命令 WREN0x06然后才能发写命令 WRITE0x02。否则写命令会被忽略。这也是很多人第一次调不通的根源。MRAM 的地址是三字节结构即使只访问很小的区域也一定要把 3 个地址字节都发出去。512KB 对应地址范围 0x000000 到 0x7FFFF高字节的高 5 位可以忽略但程序里统一发 3 字节以后换更大容量芯片不用改协议。void mr25_write_enable(void) { MRAM_CS 0; spi_xfer(0x06); // WREN MRAM_CS 1; } void mr25_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; uint8_t *p (uint8_t *)buf; mr25_write_enable(); MRAM_CS 0; spi_xfer(0x02); // WRITE spi_xfer((addr 16) 0xFF); spi_xfer((addr 8) 0xFF); spi_xfer(addr 0xFF); for (i 0; i len; i) { spi_xfer(p[i]); } MRAM_CS 1; } void mr25_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS 0; spi_xfer(0x03); // READ spi_xfer((addr 16) 0xFF); spi_xfer((addr 8) 0xFF); spi_xfer(addr 0xFF); for (i 0; i len; i) { buf[i] spi_xfer(0x00); } MRAM_CS 1; }这里有个细节MRAM 是串行从机SI 上的数据在 SCK 边沿被芯片接收SO 上的数据在同一次 SPI 交换里输出。所以发送地址时不关心 SO 返回什么真正读取数据时主机每个字节写 0x00 只是提供 SCK把 SO 上的数据采回来。注意WREN 命令和随后的 WRITE 命令之间CS 必须先拉高一次再拉低发 WRITE。如果 CS 一直为低MRAM 会把 WREN 命令和 WRITE 命令当成一次非法序列写使能不生效。3.4 连续读写和地址边界MR25H40CDF 支持连续读写一次 READ 或 WRITE 命令可以跟着很多数据字节地址会自动递增。这和 NOR Flash 的“页缓冲”概念不同它在递增过程中不需要等待内部操作。但有一个边界问题当地址递增到存储阵列末尾时很多 SPI 存储芯片会选择自动回卷到 0x000000。所以我从不依赖这种回卷行为。应用层读写尽量按块操作每块控制在 512 字节以内并且先检查 addr len 是否超过 0x80000。如果会跨边界就拆成两次操作一次读到边界结束另一次从边界起始继续。这样即使芯片的行为版本有差异程序也不会踩坑。3.5 状态寄存器和写保护如果写操作没生效第一个该怀疑的就是写使能没成功。可以用 RDSR0x05命令读状态寄存器查看 WEL 位是不是变成 1。uint8_t mr25_read_status(void) { uint8_t st; MRAM_CS 0; spi_xfer(0x05); st spi_xfer(0x00); MRAM_CS 1; return st; }如果状态寄存器读出来一直是 0说明 WREN 根本没写进去。常见原因是 SPI 模式配置错、CS 没有完全拉低或者 WREN 和 WRITE 之间 CS 没有重新拉高。MRAM 上电后写使能锁存位默认是关闭状态所以每次写入前都必须调用 mr25_write_enable()不能像 SRAM 一样直接写。块保护功能我默认不使能。只有当你需要给引导区或关键参数区加保护时才去设置状态寄存器里的 BP0、BP1 位。工业设备如果固件支持 Bootloader 升级可以考虑把 Bootloader 所在区域设成块保护防止应用飞跑时误写。否则保持默认代码越简单越不容易出错。4. 数据完整性和工业级可靠性设计4.1 写入时掉电MRAM 也救不了“整条记录”MRAM 的单元切换速度很快掉电时已经写入的字节不会丢。但是这不代表“你发了一整条多字节数据掉电时这条数据就一定是完整的”。SPI 接收和数据写入过程是逐字节进行的如果写了一半电源断了后面那半截地址可能还是旧数据或者收到了一半的字节流。所以真正可靠的做法是在应用层做好帧格式和双区备份。MRAM 解决了“存储介质不耐写、写入慢”的问题但数据事务完整性还是要靠软件来保证。4.2 给每条记录加魔数、长度、序列号和 CRC我建议设计一种非常朴素的记录格式无论存参数还是存事件都用同一个结构字段长度作用魔数2 字节标记这是一条有效记录比如 0xAA 0x55长度2 字节后面数据域的长度序列号4 字节单调递增用于判断新旧数据域N 字节实际载荷CRC162 字节覆盖前面所有字段和数据读取的时候先校验魔数不匹配直接判无效再校验 CRC不通就判损坏如果两张镜像记录都损坏才判定存储区需要重新初始化。序列号的作用是判断哪个镜像更新哪怕写入过程中掉电也可以选择“最新且校验通过”的那份。不要省略 CRC。MRAM 本身误码率非常低但 SPI 线路上的干扰、单片机 IO 配置错误、PCB 焊接不良都会导致读写路径上出现错误。CRC 是最后一道防线。4.3 A/B 双区镜像比单一地址更可靠我在设备里划分了两个槽位比如参数区放在地址 0x00000 和 0x00080每份记录 128 字节。写入流程如下构造新记录序列号在前一条基础上加 1。先写到 A 区。读回 A 区校验 CRC。如果校验通过再写到 B 区。再读回 B 区校验。启动时把 A、B 都读出来只要 CRC 通过就选序列号大的那一条。如果某一次写 A 后正好掉电A 可能已经是最新且完整B 还是旧版本没关系启动时选 A。如果 A 写到一半损坏CRC 失败那就选 B。这样在任意单点掉电时刻至少有一份完整的最新或次新记录。因为 MRAM 不需要擦除这个流程执行起来非常快不会像 Flash 方案那样为了“少擦一次”去精打细算。4.4 掉电检测和最后写入窗口PIC18F87J11 内部有 BOR 和 LVD可以在主电源跌落时产生复位或中断。我的板子在 3.3V 供电把电压检测阈值设在 MCU 稳定运行的最低点附近检测到掉电后进入一个“最后保存”流程把当前累计值、状态机、最后一次错误码这些最关键的变量一次性写入 MRAM 的紧急存储区。这个过程不要做太多事。写紧急数据前先把看门狗关掉或者确保这次写的时间远小于看门狗超时写完立即进入低功耗或复位等待。我在实际项目里从检测到掉电到写完 64 字节大约只用了几毫秒完全来得及。4.5 性能实测2MHz SPI 足够用SCK 2MHz 时一个字节传输需要 8 个时钟周期也就是 4 微秒。写一条 128 字节的事件记录加上命令头、地址和片选开销总体不到 1 毫秒。一天写几千条日志对 MRAM 来说完全不构成压力。如果以后吞吐量提高可以继续把 SCK 往上配。但要注意SPI 优势是简单不是带宽。大量连续日志还是建议按块缓冲一次写满一个 512 字节区块减少命令头开销。对工业存储来说“稳定不丢”永远比“跑满带宽”重要。5. 调试实录常见问题和排查方法5.1 先做最小系统验证别一上来写业务数据第一次上电我不会直接跑应用层。先把 MRAM 挂到 SPI 上只做三件事读状态寄存器、写一个测试字节、读回来对比。具体步骤是读状态寄存器看是否返回 0x00。发 WREN再读状态寄存器确认 WEL 位置 1。对地址 0x000000 写 0xA5、0x5A、0x00、0xFF 几个测试字节。读回同样的地址逐字节比对。如果状态寄存器读出来是 0xFF说明 SDO 线可能没接对或者 CS 根本没有选中芯片。如果读状态正常但写后读回全 0xFF优先怀疑 WREN 没成功。这一步做完再进应用层不迟。5.2 典型故障速查表下面这些问题是实际调试中最常遇到的我整理成了一张排查表现象可能原因排查和解决读状态寄存器返回 0xFFCS 没拉低SCK 悬空SO 接错示波器量 CS、SO确认引脚定义读状态寄存器返回 0x00但写不进去WREN 没执行或 WREN 与 WRITE 之间 CS 没拉高单步执行抓 WREN 后 WEL 位读回全 0x00电源掉电SI 没接对SCK 极性错检查电压量 SCK 波形核对 Mode 0某个地址区域写不进地址字节顺序写反或跨了地址边界检查 3 字节地址发送顺序偶发数据错位CRC 报错HOLD 悬空、SCK 干扰、电源纹波大HOLD 上拉缩短 SPI 走线加去耦同一条命令执行两次才成功CS 低电平时间不够命令被截断看逻辑分析仪确认 CS 低周期覆盖整条命令5.3 用逻辑分析仪看时序比猜快得多调 SPI 设备最有效的办法是接一个逻辑分析仪把 CS、SCK、SI、SO 四根线同时抓下来。重点看写命令这一段是否完整CS 先拉低然后依次出现 0x02、三字节地址、数据字节最后 CS 拉高。只要这一段出现在同一个 CS 低周期内并且边沿和模式正确协议基本没问题。我踩过最深的一个坑是底层 spi_xfer 函数在每次传输后自动拉高 CS导致整条命令被拆成很多个“CS 有效”的小段。MRAM 把每一段都当成独立命令结果写操作完全失败。后来我把 CS 控制收到命令级函数里整条 READ 或 WRITE 才真正闭合问题立刻消失。5.4 CRC 偶发失败怎么定位如果故障是“跑了很久之后偶发 CRC 不过”不要急着怀疑 MRAM 质量。先看是不是每次失败都集中在同一个物理地址如果是大概率是那个区域的数据被另外的程序覆盖了如果失败位置随机先排除 SPI 线干扰和电源纹波再检查 HOLD 引脚是不是真的拉到了 VDD。也可以在固件里加上写后读回和重试机制。即使第一遍写读不一致重写一遍往往就好了。MRAM 不是易失介质不会因为多写一遍就折寿。把重试逻辑放到底层接口里应用层只要保证“写失败后能报错或重试”长期运行的可靠性会明显提升。6. 量产化和扩展的个人经验6.1 产线测试不要只写固定模式量产时每块板子最好都跑一次完整的存储自检。我一般是这样做的产线程序先擦除不了 MRAM那就直接写测试 pattern把 512KB 空间按 4 个象限分别写 0xA5、0x5A、0xFF、0x00再逐地址读回比对。全地址遍历在 2MHz SPI 下需要一点时间但产线只需要做一次值得。测试通过后写测试区会被后续的应用数据覆盖不影响出厂状态。这个测试能同时暴露焊接不良、虚焊、引脚短路、PCB 断线等硬件问题。6.2 更大容量和多设备扩展如果以后数据量超过 512KB可以换更大容量的串行 MRAM代码里只要把地址长度逻辑和块边界检查跟着改。也可以直接做双片镜像两片 MRAM 挂同一组 SPI 总线各用一片 CS。写入时同时写两片读时优先读主片主片 CRC 失败再读从片。工业设备想要更高的容错这种冗余方案比单纯加大容量更有效。MRAM 也可以和 SPI NOR Flash 共存MRAM 存频繁修改的参数Flash 存固件镜像和升级包。两条片选线各自独立互不干扰。这种“各司其职”的组合在很多嵌入式开源项目里都有影子。6.3 最后分享一个我自己踩过的坑HOLD 引脚悬空的问题我是在稳定运行测试的后半段才发现的。当时整机看门狗没有复位但日志里偶发出现整段数据错位CRC 一次错、一次对毫无规律。用逻辑分析仪持续抓了很久才发现 HOLD 引脚在 SCK 静止时会被附近的继电器的电磁干扰拉低MRAM 因此暂停输出。后来把 HOLD 用 10k 电阻接到 VDD并且走线避开继电器驱动区问题再没出现过。这颗电阻真的不能省。写完驱动、跑通读写只是第一步真正的工业项目还要在电源、布局、掉电策略和测试流程上把每一环都卡死。MR25H40CDF 和 PIC18F87J11 这套组合给我的最大感受是“简单、耐造、可控”如果你也在做类似的工业存储方案希望这篇东西能帮你少走几段弯路。