
做工业控制器的人大概都遇到过这种尴尬设备每天要保存几百次运行状态用了两三年EEPROM先罢工了Flash的某个扇区也写穿了现场只能停机换板。这个项目的核心就是解决这个问题——用一颗4Mbit的SPI MRAMMR25H40CDF搭配PIC32MX795F512L在设备里做可靠的参数存储和运行日志记录。MR25H40CDF没有擦写寿命焦虑写入不需要页擦除PIC32MX795F512L又是一个外设齐全、带CAN和以太网的MIPS内核MCU两者配合很合适。文章从硬件连接到驱动移植、掉电保护策略都会过一遍适合正在选型、自己写SPI驱动或者被存储可靠性问题折磨的嵌入式工程师参考。1. 项目背景为什么工业存储要选MRAM而不是Flash1.1 频繁写入的场景Flash先顶不住了先说一个我实际遇到的案例。有一台设备需要每分钟记录一次电流、温度、累计运行时间折算下来一天要写1440条一年就是50多万次写操作。最开始用的是常规SPI NOR Flash容量倒是够但问题出在寿命上。一般NOR Flash的擦写次数在10万次左右就算按扇区做磨损均衡高频写入的日志区也撑不过两三年。一旦坏块出现在关键参数区域设备表现就是偶发重启后数据错乱这种故障最难定位客户只会觉得“你们的控制器不稳定”。EEPROM更不用说了I2C接口的写速度本身就慢页写要5ms左右频繁写入时占用的CPU时间很可观而且寿命通常也只有100万次。对于“每次状态变化都记录”这种需求EEPROM和Flash其实都是在硬扛。那工业现场到底需要什么需要一种写入速度接近SRAM、断电数据不丢、写寿命基本无限的非易失存储。MRAM正好落在这些要求的交集上。1.2 MRAM的工作原理磁隧道结MRAM全称是磁阻随机存取存储器Magnetoresistive Random Access Memory存储单元的核心是一个磁隧道结MTJ结构。MTJ由两层铁磁层夹着一层极薄的绝缘隧穿层构成其中一层磁化方向固定另一层可以翻转。当两层磁化方向平行时隧穿电阻低反平行时隧穿电阻高。读操作就是检测这个电阻状态写操作则是通过电流改变自由层的磁化方向。把这个原理对比Flash就很容易理解差异了。Flash写入靠的是电荷在浮栅里的注入和释放电荷会慢慢泄漏、氧化层会磨损所以有数据保持年限和擦写次数限制。MRAM写的是磁化方向本质是物理状态翻转没有电荷存储过程所以理论上写寿命极高数据保持也不依赖电荷。回到存储器的日常使用体验MRAM最直观的三个优势是字节级读写、写入前不需要擦除、写操作基本不需要等待。这三点让驱动代码比Flash简单很多后面写代码时能直接感受到。1.3 MR25H40CDF与PIC32MX795F512L的选型逻辑MR25H40CDF是Everspin的SPI接口MRAM容量4Mbit也就是512KB。我需要在这个容量下存放系统参数、配方表和最近一段时间的运行日志同时给OTA固件暂存区留点空间512KB刚好够用。这颗芯片的关键参数如下参数数值容量4Mbit512KB接口SPI最高40MHz工作电压2.7V ~ 3.6V写寿命理论无限典型10^14次数据保持10年以上工作温度-40℃ ~ 85℃工业级封装8引脚DFN选择PIC32MX795F512L的原因也很直接。这颗芯片是MIPS32 M4K内核主频80MHz512KB Flash和128KB RAM外设里有4个SPI模块、8个DMA通道、双CAN控制器、USB和10/100M以太网MAC。用在工业控制器上SPI挂MRAM做本地存储CAN或者以太网做上位机通信硬件上是一套很完整的方案。更重要的是PIC32MX795F512L的SPI模块支持8位、16位、32位数据宽度配合DMA可以做长数据块传输而不占用CPU。这在后续要记录大量日志数据的时候很有用。PIC32MX系列还有引脚重映射功能PPS片选和SPI引脚的位置可以灵活调整画PCB的时候走线能舒服不少。2. 硬件连接与PCB设计4根线加1根片选就够2.1 引脚对应与电源处理SPI接口器件在硬件上很省事MR25H40CDF总共就8个引脚跟MCU之间只需要SCK、SI、SO、CS四根信号线另外两个控制引脚WP和HOLD需要处理。我实际使用的对应关系如下MRAM引脚功能连接目标SCKSPI时钟PIC32MX795F512L的SCK2引脚SI串行输入MCU的SDO2引脚SO串行输出MCU的SDI2引脚CS片选任意GPIO低有效WP写保护GPIO控制默认高HOLD传输暂停直接接高电平3.3VVDD电源3.3V并接0.1uF去耦电容VSS地系统地这里有两个必须注意的引脚。HOLD是传输暂停引脚低电平时会让SPI通信停下来如果悬空噪声可能直接把HOLD拉低结果就是读到一半数据卡住。所以HOLD必须通过10k电阻上拉到3.3V或者直接短接3.3V。WP引脚我选择了用GPIO控制而不是固定接高。正常工作时置高允许写入当系统处于一些特殊阶段比如启动自检未完成、刚上电、程序异常分支时拉低给状态寄存器加一层写保护。这样多一个保险成本几乎为零。电源处理上VDD要选低噪声的3.3V电源在芯片旁边放0.1uF陶瓷电容位置尽量靠近VDD引脚。如果PCB空间允许再加一个10uF的钽电容做低频储能因为掉电保存瞬间电流会有个小尖峰。我建议在MCU侧的SPI时钟线上串一个22Ω到33Ω的电阻靠近MCU端。这个电阻不是为了阻抗匹配主要是抑制过冲和振铃。在20MHz SPI时钟下长走线如果反射严重波形边缘会很毛糙即使时序没错也可能导致偶发的采样错误。这点在后面调试环节会体现得很明显。2.2 PCB布局布线注意事项MR25H40CDF是DFN封装底部有散热焊盘。打样回来后第一件事就是检查焊接质量DFN封装虚焊是这类存储芯片的头号杀手。焊盘设计要按照数据手册给的推荐尺寸散热焊盘是否要接地看手册说明我用的型号要求焊接到地这样既能散热也能减少地弹噪声。布局上MRAM要尽量靠近MCUSPI信号线越短越好。对20MHz的SPI来说3cm以内的布线长度基本不需要考虑阻抗控制但超过5cm就建议把时钟线和数据线等长处理避免采样点偏移。SPI的四根线不要跟继电器、电机驱动这类强干扰源平行走线中间最好隔一层地。如果板子上同时有开关电源注意MRAM的3.3V尽量不要从DCDC的开关节点就近取电源平面的去耦要充足。我遇到过一板子现象是MRAM写数据偶尔出错查到最后是DCDC布局离MRAM太近纹波直接耦合到了VDD上。多层板的话SPI信号线走在底层顶层铺完整地平面效果最好。两层板就退而求其次确保信号线下方有连续的地参考不要让走线跨过地平面的断裂带。3. SPI驱动开发寄存器级完整代码3.1 SPI模式与波特率设置MR25H40CDF支持SPI Mode 0CPOL0CPHA0和SPI Mode 3CPOL1CPHA1用Mode 0就够了。要注意的是Microchip的SPI模块对时钟极性和相位的定义跟通用术语不完全一致配置的时候需要查一下对应关系。在PIC32MX795F512L上SPI Mode 0对应的寄存器配置是CKP0、CKE1。不要直接照搬意法半导体的SPI配置习惯容易踩坑。波特率方面PIC32MX的SPI时钟计算公式是FSCK FPB / (2 × (SPIxBRG 1))其中FPB是外设总线时钟。我系统时钟跑80MHz外设总线时钟也是80MHz取SPI2BRG1计算得到FSCK 80MHz / (2 × (1 1)) 20MHzMR25H40CDF最高支持40MHz20MHz留了一半余量信号完整性压力小很多。如果以后要追求更高吞吐把SPI2BRG改成0就能到40MHz但建议那时候回头检查一下PCB走线质量。初始化代码直接操作寄存器不用库函数方便理解底层行为void SPI2_Init(void) { SPI2CON 0; // 先关闭SPI2 SPI2BRG 1; // FPB80MHzSCK20MHz SPI2STATCLR 0x40; // 清除SPIROV溢出标志 SPI2CONbits.MSTEN 1; // 主模式 SPI2CONbits.CKP 0; // 时钟极性空闲低 SPI2CONbits.CKE 1; // 与CKP0组合对应SPI Mode 0 SPI2CONbits.MODE16 0; // 8位数据宽度 SPI2CONbits.MODE32 0; SPI2CONbits.ON 1; // 使能SPI2 }SPI2STATCLR 0x40这行很多人会漏掉。PIC32MX的SPI接收溢出后SPIROV位会一直置位不手动清除的话后续接收会被阻塞。初始化阶段做一次清零后面就没这个隐患。3.2 MR25H40CDF命令集与读写时序MR25H40CDF的命令集基本兼容标准SPI Flash但细节上有个关键差异写数据前必须发写使能WREN。每次写操作完成后写使能锁存器WEL会自动清零。这意味着每次写数据、写状态寄存器都要重新发WREN不能只在初始化时发一次。主要命令如下命令操作码说明WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据MR25H40CDF的地址是24位也就是3字节虽然512KB容量实际只需要19位地址但命令格式仍然要完整发送3字节。顺序是先发命令码再发地址高字节、中字节、低字节然后才是数据。时序要点CS在命令第一个字节发送前就要拉低并且在整个命令和数据传输期间保持低最后所有字节发完后拉高。CS拉高动作是命令的结束标志。如果中途CS提前拉高芯片会认为当前命令被中止已经写入的数据不会生效。跟Flash最大的不同是MRAM写数据不需要等待内部编程完成。NOR Flash写完一页要轮询状态寄存器的WIP位MRAM的写操作在纳秒级完成状态寄存器的WIP位几乎不会让CPU等待。连续写任意数量字节都可以只要CS保持低地址会自动递增到当前写入的最后一个地址之后。3.3 C语言驱动实现基础读写函数底层字节交换函数是SPI驱动的核心PIC32MX上收发是同时完成的往SPI2BUF写一个字节同时从SPI2BUF读取对方返回的一个字节。uint8_t SPI2_Xfer(uint8_t data) { while (SPI2STATbits.SPITBF); // 等待发送缓冲空 SPI2BUF data; while (!SPI2STATbits.SPIRBF); // 等待接收完成 return SPI2BUF; // 返回收到的字节 }这里有个新手常犯的错误发送完数据后要等的是接收完成标志不是发送完成标志。单片机SPI在全双工模式下发送完成和接收完成是绑定在一起的你发出去一个字节必然同时收回来一个字节。所以这个函数的等待逻辑就是发完一个字节等接收缓冲收到数据再读走。写使能函数void MRAM_WriteEnable(void) { MRAM_CS_L(); // CS拉低 SPI2_Xfer(MRAM_CMD_WREN); // 发写使能命令 MRAM_CS_H(); // CS拉高命令生效 }读状态寄存器uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_L(); SPI2_Xfer(MRAM_CMD_RDSR); status SPI2_Xfer(0xFF); // 发送任意值时钟驱动SO输出 MRAM_CS_H(); return status; }状态寄存器的bit0一般是WIP写正在进行bit1是WEL写使能锁存。写完使能后可以读状态确认WEL为1写完数据后可以确认WIP为0。虽然MRAM写入很快但用状态寄存器做双保险成本很低。单字节写入void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); // 必须先写使能 MRAM_CS_L(); SPI2_Xfer(MRAM_CMD_WRITE); SPI2_Xfer((addr 16) 0xFF); // 地址高字节 SPI2_Xfer((addr 8) 0xFF); // 地址中字节 SPI2_Xfer(addr 0xFF); // 地址低字节 SPI2_Xfer(data); // 数据 MRAM_CS_H(); }地址拆分这里要注意MRAM地址是24位即使你只用低19位3个字节都要发。有些人在地址计算上图省事只发两字节结果高字节缺失导致地址错乱写入位置完全不对。连续写入void MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; MRAM_WriteEnable(); MRAM_CS_L(); SPI2_Xfer(MRAM_CMD_WRITE); SPI2_Xfer((addr 16) 0xFF); SPI2_Xfer((addr 8) 0xFF); SPI2_Xfer(addr 0xFF); for (i 0; i len; i) { SPI2_Xfer(buf[i]); } MRAM_CS_H(); }注意MRAM的连续写没有页边界限制Flash常常有256字节页缓冲跨页时要重新发起页编程命令。MRAM不需要这也让驱动简单不少。连续读取void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS_L(); SPI2_Xfer(MRAM_CMD_READ); SPI2_Xfer((addr 16) 0xFF); SPI2_Xfer((addr 8) 0xFF); SPI2_Xfer(addr 0xFF); for (i 0; i len; i) { buf[i] SPI2_Xfer(0x00); // 发0x00产生时钟接收数据 } MRAM_CS_H(); }读取时每读一个字节也要发送一个字节来产生SPI时钟。这个发送值无所谓习惯用0x00。如果接收到的数据总是错位一个字节检查这里的时钟产生逻辑是否多发了或少发了一个字节。3.4 C语言驱动实现连续读写与DMA优化上面IO模式驱动在20MHz下连续读100字节大概需要40多微秒。对这类的数据记录场景来说已经很快了。但是如果记录的数据量比较大比如要连续读几KB日志用于上抛每次都让CPU一个字节一个字节搬占用就明显了。PIC32MX795F512L有DMA模块可以配置SPI2接收事件触发DMA传输把数据直接从SPI2BUF搬到内存缓冲区整个过程不占CPU。DMA配置思路是把UART或者定时器触发作为启动源配置DMA通道的源地址为SPI2BUF的接收寄存器地址目标地址为内存缓冲区传输计数为要读取的字节数。PIC32MX的DMA支持在每次外设事件发生时自动搬运数据。加16字节的FIFO缓冲后逻辑分析仪抓波形能看到SPI始终以满速运行中间没有CPU介入产生的停顿。需要提醒的是DMA方式适合大块连续数据读取小数据块比如几十字节DMA初始化的开销反而比IO模式还大。我的习惯是小于256字节用IO模式更大数据量才上DMA。4. 工业可靠性设计掉电、校验与写保护4.1 掉电保存策略工业设备最怕的就是运行过程中突然断电。如果正在写Flash碰到页擦除中间掉电可能整个扇区数据损坏。MRAM没有擦除操作单字节写入是物理上瞬时完成的这给掉电保存设计省了很多麻烦。我用的策略是用PIC32MX795F512L的BOR欠压复位检测配合稍大一点的储能电容在电压掉到MCU复位阈值之前利用BOR中断把关键运行参数写入MRAM。具体实现上把BOR阈值设为2.7V左右正常3.3V供电时从3.3V掉到2.7V之间的时间由储能电容决定。如果负载电流是50mA用一个470uF的电容根据电容放电公式t C × (V1 - V2) / I 470uF × 0.6V / 50mA ≈ 5.6ms这5.6ms够做什么2.7V是MCU还能稳定运行的电压关键参数假如有64字节WRITE命令加地址加数据共68字节20MHz下传输时间只有27.2微秒。就算再加上BOR中断响应时间、GPIO切换时间1ms以内肯定写完了。所以根本不需要复杂的“缓冲页FAT文件系统”设计直接在BOR中断里拼一个写入函数就行。当然BOR中断里写数据要遵循一个原则只写真正需要保存的关键数据不要在中断里做长时间循环或者扫描式写日志。日志类数据可以等系统完全掉电前用最后一点能量写最近一批但优先级低于关键参数。4.2 CRC校验与双备份存储布局存储芯片本身可靠不代表整个系统的数据链路可靠。SPI时钟受干扰、软件逻辑错误、固件升级异常都可能导致写入错误数据。所以我在MRAM的存储规划上做了一层应用层的保障CRC校验加双备份区。存储布局大致这样区域地址范围用途参数区A0x000000 ~ 0x00FFFF关键参数主备份参数区B0x010000 ~ 0x01FFFF关键参数镜像备份日志区0x020000 ~ 0x07FFFF运行日志环形覆盖参数区A和B中都存放一个结构体头部是魔数比如0x5A5A1234、数据版本、数据长度、数据本体、CRC16校验值。读参数时先读A区校验CRC如果通过就用A区如果A区校验失败读B区两个区都失败才恢复默认参数。typedef struct { uint32_t magic; uint16_t version; uint16_t length; uint8_t data[64]; uint16_t crc; } AppParamBlock;写入前把两个区都更新一次顺序是先B后A这样就算更新过程中掉电旧区也保持完整。这个策略在工业通信设备里很常见实现简单效果很扎实。CRC16的实现如下多项式用0x1021标准CCITTuint16_t crc16_ccitt(const uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; uint32_t i; int bit; for (i 0; i len; i) { crc ^ (uint16_t)data[i] 8; for (bit 0; bit 8; bit) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }CRC校验不仅用于参数区日志区每一条日志记录也带一个CRC。读日志时如果某条记录CRC不对直接跳过不因此中断整个日志回放流程。这样日志区可以在环形覆盖时容忍一两条异常记录不会影响整个文件系统的可用性。4.3 写保护与误写防护MRAM写寿命无限不代表你可以肆无忌惮地乱写。乱写的后果不是寿命耗尽而是数据被覆盖成垃圾值。工业现场的电磁干扰很厉害SPI信号线上一个毛刺可能在CS拉低期间被当作有效命令导致意外写入。保护措施分硬件和软件两层。硬件上WP引脚通过GPIO控制正常运行时置高程序在非写窗口比如刚上电、看门狗复位后、主程序死循环异常主动拉低禁止写入。软件上写操作前做一个解锁序列先写入一个固定的解锁魔数到某个影子寄存器再执行真正的WRITE命令。MCU端的C代码把所有写操作收敛到两三个函数里防止业务代码到处乱调。另外MRAM的HOLD功能在工业环境里反而可以变成保护手段。如果SPI总线上挂了多个从设备而某个从设备的干扰导致总线忙可以让MRAM进入HOLD状态暂停通信。但在我的设计里只用它做固定拉高因为单机场景没有总线仲裁需求多一个控制逻辑就多一个出错点。还要注意MRAM本身没有坏块概念不需要像Flash那样做坏块管理或磨损均衡。这省了不少代码存储寿命测试也简单很多。5. 调试实录与高频问题排查5.1 调试工具与SPI波形观察这套系统调试的时候我用的工具是MPLAB X IDE加ICD4调试器配合一个逻辑分析仪。逻辑分析仪采样率至少要50MHz以上因为20MHz的SPI信号按照奈奎斯特定理最低采样率也要40MHz实际用80MHz或100MHz采样才能看清波形细节。调SPI波形抓几个点就够了CS拉低到SCK第一个上升沿的时间模式0下SCK空闲电平和边沿采样点数据线上的字节顺序。逻辑分析仪里把CS、SCK、SI、SO四路信号都接上触发条件设置为CS下降沿触发然后单步执行写命令就能看到完整的命令时序。如果波形显示CS和SCK之间有异常的毛刺优先检查MCU侧串阻是否加了走线是否太长。示波器能看到过冲的话把串阻从22Ω加大到47Ω试试。5.2 高频问题与解决方案速查下面是实际调试中遇到的几个典型问题有些问题花了我不少时间去定位整理成表格方便排查现象根本原因解决办法读回数据全是0xFFSPI模式不对或者芯片没进入工作状态确认CKP0、CKE1检查VDD到3.3V检查HOLD引脚是否拉高读回数据全是0x00SI/SO接反或CS没有正常工作核对原理图SI接MCU的SDOSO接MCU的SDI写入后再读数据还是原来的值写使能没有执行或者WP引脚拉低确认WREN已经发出WP引脚默认拉高连续读多字节数据错位CS拉高时机不对或者时钟多了一个检查READ命令的地址字节是否完整CS一定要在数据读完后拉高偶发写错干扰环境下出问题电源噪声或信号串扰VDD加去耦电容SPI线加串阻走线远离干扰源DFN封装焊好后性能不稳底部焊盘虚焊补焊检查X-Ray或AOI确保散热焊盘可靠连接这里重点说一下连续读写错位的问题。SPI是全双工你在发第N个字节的时钟时收到的是第N-1个字节的响应。所以READ操作中发完3个地址字节后要再发一个空字节才能收到第一个数据字节。如果读数据的代码里少发了这个空字节或者多发了一个读回来的数据就会整体错位。代码里我已经强调了用循环发送产生时钟顺序不能错。5.3 实测性能数据在20MHz SPI时钟下实测读写性能如下单字节读约2μs含CS切换和命令头单字节写约5μs含WREN和写命令连续读100字节约47μs连续写100字节约62μs对比一下传统方案I2C EEPROM在400kHz下写100字节按页写也要2.5ms左右SPI NOR Flash写100字节如果目标地址恰好跨页需要先擦除一个扇区时间至少是几十毫秒。MRAM这个速度完全是另一个数量级。长时间稳定性方面我做了连续写入100万次循环测试。每次写一个递增计数到MRAM的固定地址然后回读比较。100万次下来全部校验通过没有一次错误。这个量级如果放在NOR Flash上早就接近寿命上限了。掉电测试做了200次随机掉电每次掉电后检查最近写入的50条日志CRC全部有效。温度测试因为条件限制只做了70℃高温箱24小时老化。高温下SPI时序余量会变小20MHz时钟下波形下降沿变缓但数据读回完全正常。如果是批量产品建议在-40℃和85℃两个极限温度各做一轮完整读写测试重点看状态寄存器轮询是否超时。6. 一点实操体会做完这个项目我最大的感触是SPI MRAM驱动本身并不复杂难的是把它嵌进一套可靠的工业系统里。MR25H40CDF给了你几乎不用担心的写入寿命和速度但掉电处理、CRC校验、双备份设计这些应用层功夫还是得自己做。这也是为什么我不建议直接裸机for循环乱读乱写分层设计一套读写接口后续维护和扩展都会轻松很多。另外一个实际建议是DFN封装的MRAM打样回来先做一次AOI检查再上电。第一次做样板时有一片芯片虚焊现象是偶尔读对偶尔读错我一度怀疑是SPI时序问题排查了整整一下午。后来换了新片子一切正常回来看焊盘才发现是底部焊盘没吃透锡。从那以后这种小封装芯片上电前一定先过目测和万用表检查。最后说一句如果你的产品也需要频繁记录运行状态、校准参数或者做掉电保存试试MRAM加主流MCU的组合写驱动的时候你会明显感觉到省心很多。