ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MRAM与PIC18F85K90实战:SPI非易失存储选型与掉电保护设计

2026/10/5 7:46:56 拓冰建站 浏览量
MRAM与PIC18F85K90实战:SPI非易失存储选型与掉电保护设计 1. 为什么MRAM加PIC18的组合值得单独拿出来讲做工业嵌入式的人都有一个共同的痛点设备装在现场断电是家常便饭但关键数据——比如累计运行时长、标定系数、故障快照、配方参数——一个都不能丢。传统方案无非两种要么外挂一颗EEPROM或者SPI Flash要么靠单片机内部的Data EEPROM硬扛。前者写入慢、擦写寿命有限后者容量小得可怜PIC18F85K90内部也就1KB的Data EEPROM稍微记点日志就满了。MR25H40CDF这颗芯片第一次进入我视野的时候我正被一个油田RTU项目折磨。现场每次断电重启SPI Flash里最后几条记录总是丢查了半天发现是写缓存还没落盘就掉电了。后来换成MRAM问题直接消失。MRAM磁阻随机存取存储器的写入机制和Flash完全不同它不需要擦除周期字节级写入写入完成即非易失没有内部写缓冲导致的“延迟落盘”问题。这一点在掉电频繁的工业场景里价值极大。MR25H40CDF是Everspin出的4Mbit SPI MRAM512K×8组织40MHz SPI接口工业级温度范围-40到85°CSOIC-8封装。PIC18F85K90则是Microchip的一款经典8位增强型单片机自带SPI模块64KB Flash程序空间加上前面说的1KB Data EEPROM。两者搭配一个负责高速非易失存储一个负责控制与协议处理在工业仪表、数据记录仪、电机控制器、智能传感器这些场景里非常实用。这篇文章面向的是有一定嵌入式基础、正在选型或者正在调试SPI存储的工程师。我会从选型逻辑、硬件连接、SPI时序配置、读写驱动实现、掉电保护策略、常见问题排查几个维度把整套方案拆开讲清楚。代码基于PIC18F85K90的XC8编译器环境但思路对STM32、ESP32、FPGA平台同样适用。2. 方案选型MRAM、FRAM、EEPROM、SPI Flash到底怎么选2.1 四种非易失存储介质的核心差异选存储介质这件事不能只看容量和价格得把写入速度、擦写寿命、写入粒度、掉电安全性、接口复杂度放在一起权衡。我整理了一张对比表数据来自各厂商公开手册和实际测试经验。特性MRAM (MR25H40CDF)FRAMEEPROMSPI NOR Flash写入方式字节级无需擦除字节级无需擦除字节级无需擦除按扇区擦除后写入写入速度极快无写入延迟快慢ms级慢扇区擦除ms级擦写寿命10^15次以上10^14次左右10^6次10^5次掉电安全性写入即非易失写入即非易失写入即非易失有写缓冲可能丢数据容量范围中4Mbit典型小常见256Kbit小Kbit级大Mbit到Gbit接口SPI/QSPII2C/SPII2C/SPISPI/QSPI单位成本高中高低极低从表里能看出来MRAM的核心优势在于“写入即固化”加上“近乎无限的擦写寿命”。Flash的问题在于写入前必须擦除整个扇区而且擦除操作耗时很长通常几毫秒到几十毫秒。如果你的系统在写Flash的过程中掉电整个扇区的数据可能全部损坏。MRAM没有这个问题每个字节独立写入写一个字节就是一个字节不存在“擦一半”的状态。FRAM在写入特性上和MRAM接近但容量普遍偏小而且FRAM有读取破坏性的问题每次读操作后需要重写虽然厂商做了内部补偿但在高频读取场景下功耗和可靠性不如MRAM。2.2 为什么是MR25H40CDF而不是别的MRAMEverspin的MRAM产品线里MR25H40CDF的几个参数很关键。4Mbit容量对于大多数工业数据记录场景够用512KB的空间可以存几千条结构化记录。SPI接口最高40MHz配合PIC18F85K90的SPI模块最高可跑到系统时钟的四分之一实际能跑到10MHz以上读写一条32字节的记录在微秒级完成。工作电压2.7V到3.6V和PIC18F85K90的3.3V供电直接匹配不需要电平转换。SOIC-8封装引脚定义和标准SPI Flash兼容PCB设计时可以直接替换硬件改动成本低。工业级温度范围覆盖-40到85°C满足绝大多数工业现场要求。还有一个容易被忽略的点MR25H40CDF支持SPI模式0和模式3CPOL和CPHA可配置。这意味着它可以适配几乎所有主流单片机的SPI模块不会出现“单片机只支持模式0但存储芯片只支持模式3”的尴尬。2.3 PIC18F85K90的SPI模块能力评估PIC18F85K90的MSSPMaster Synchronous Serial Port模块支持SPI和I2C两种模式。在SPI主模式下时钟极性CKP和时钟边沿CKE可配置对应四种SPI模式。时钟频率由SSPCON1寄存器的SSPM位和系统时钟共同决定公式是Fosc/(4×(SSPADD1))其中SSPADD是波特率分频值。举个例子如果系统时钟是64MHzPIC18F85K90最高可跑到64MHz要得到10MHz的SPI时钟SSPADD 64/(4×10) - 1 0.6取整后SSPADD0实际时钟是16MHz。如果要精确得到8MHzSSPADD 64/(4×8) - 1 1实际时钟8MHz。这个计算在配置SPI时必须先算清楚否则时钟太快可能导致MRAM读写失败太慢又影响吞吐。PIC18F85K90的SPI模块还有一个特点它有一个16字节的FIFO缓冲区通过SSP1BUF和SSP1ADD寄存器配合使用在连续读写时可以显著减少CPU干预。不过在实际驱动MRAM时我通常还是用查询方式逐字节收发因为MRAM的命令开销很小FIFO的优势不明显反而增加了代码复杂度。3. 硬件连接与PCB布局的实操细节3.1 引脚连接与信号完整性MR25H40CDF是标准SPI从设备引脚定义如下CS片选低有效、SCK时钟、SI数据输入、SO数据输出、VDD、VSS、WP写保护、HOLD保持。和PIC18F85K90的连接方式很直接MR25H40CDF的CS接PIC18F85K90的RB2或任意GPIO软件控制片选SCK接RC3SCK引脚SI接RC5SDO引脚SO接RC4SDI引脚WP和HOLD都上拉到VDD禁用写保护和保持功能这里有一个细节PIC18F85K90的SPI引脚是复用引脚需要先配置TRISC和TRISB方向寄存器把SCK、SDO设为输出SDI设为输入CS设为输出。同时要确保ADCON1寄存器把相关引脚设为数字模式否则模拟功能会干扰SPI通信。注意WP和HOLD引脚不要悬空。我见过一个案例HOLD引脚悬空导致MRAM偶尔进入保持状态SPI通信时好时坏查了两天才发现是这个问题。两个引脚都通过10K电阻上拉到VDD稳妥。3.2 PCB布局的注意事项SPI信号在10MHz以上时PCB布局就开始变得重要了。MRAM和单片机之间的走线尽量短最好控制在5厘米以内。SCK作为时钟信号是最容易产生振铃和串扰的走线两侧包地处理。如果板上还有其他高频信号比如PWM输出、电机驱动SPI走线要远离这些区域。电源去耦不能省。MR25H40CDF的VDD引脚旁边放一个0.1μF的陶瓷电容再并一个1μF的钽电容位置尽量靠近芯片引脚。地平面要完整不要被其他信号线割裂。CS信号的处理也有讲究。如果CS走线较长建议在单片机端串联一个22Ω到33Ω的电阻抑制反射。CS的下降沿和上升沿要干净否则MRAM可能误识别命令。3.3 电源与复位时序MR25H40CDF的上电时序要求不严格但有一个点需要注意VDD上升到2.7V之前CS必须保持高电平否则芯片可能进入未定义状态。PIC18F85K90的复位期间GPIO默认为输入CS引脚会浮空所以CS上拉电阻10K是必须的。如果系统中有独立的复位芯片确保MRAM的VDD和单片机的VDD同时上电或者MRAM先上电。如果MRAM后上电单片机在MRAM未就绪时发起SPI通信会读到无效数据。我的做法是在初始化代码里加一个延时等MRAM上电稳定后再发第一条命令。4. SPI驱动实现从寄存器配置到读写函数4.1 SPI初始化代码与参数计算先看PIC18F85K90的SPI初始化。假设系统时钟64MHz目标SPI时钟8MHzSPI模式0CPOL0CPHA0。void SPI_Init(void) { // 配置引脚方向 TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出 TRISBbits.TRISB2 0; // CS 输出 // 关闭模拟功能 ADCON1 0x0F; // CS初始高电平 LATBbits.LATB2 1; // SSPCON1: SPI主模式, 时钟 Fosc/4 * (SSPADD1) // 64MHz / (4 * (11)) 8MHz SSPCON1 0x00; // 先清零 SSPSTAT 0x00; // 模式0: CKP0, CKE0 SSPADD 1; // 分频值 SSPCON1 0x20; // SSPEN1, SPI主模式 }SSPADD的计算前面提过这里再强调一遍SSPADD1时SPI时钟 64MHz / (4 × (11)) 8MHz。如果你想要更低的时钟比如4MHzSSPADD3时钟 64/(4×4) 4MHz。注意SSPADD是8位寄存器最大值255对应最低时钟 64/(4×256) 62.5KHz。4.2 字节收发函数SPI的字节收发是底层操作MRAM的所有命令、地址、数据都通过这个函数传输。uint8_t SPI_ExchangeByte(uint8_t data) { SSP1BUF data; // 写入发送缓冲 while(!PIR1bits.SSP1IF); // 等待传输完成 PIR1bits.SSP1IF 0; // 清除中断标志 return SSP1BUF; // 读取接收数据 }这个函数看起来简单但有一个坑PIC18F85K90的SPI模块在发送完成后SSP1BUF里同时包含接收到的数据。如果你先读SSP1BUF再清标志可能会读到旧数据。正确的顺序是先等标志再清标志最后读缓冲。上面的代码顺序是对的。4.3 MRAM命令集与读写时序MR25H40CDF的命令集很精简常用的就几条命令编码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读操作的时序CS拉低 → 发送READ命令0x03→ 发送24位地址MR25H40CDF是4Mbit需要19位地址但按24位发送高5位忽略→ 连续读取数据 → CS拉高。写操作的时序CS拉低 → 发送WREN0x06→ CS拉高 → CS拉低 → 发送WRITE命令0x02→ 发送24位地址 → 发送数据 → CS拉高。注意WREN和WRITE之间必须有一次CS的上升沿这是MRAM的写保护机制。void MRAM_WriteEnable(void) { CS_LOW(); SPI_ExchangeByte(0x06); CS_HIGH(); } void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); CS_LOW(); SPI_ExchangeByte(0x02); SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { SPI_ExchangeByte(buf[i]); } CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { CS_LOW(); SPI_ExchangeByte(0x03); SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { buf[i] SPI_ExchangeByte(0x00); } CS_HIGH(); }4.4 状态寄存器与写保护MR25H40CDF的状态寄存器只有几位有效最重要的是WEL写使能锁存和BP0、BP1块保护。WEL位在WREN命令后自动置1在WRITE命令完成后自动清零。BP0和BP1可以保护部分地址区域不被写入在关键参数存储时很有用。uint8_t MRAM_ReadStatus(void) { uint8_t status; CS_LOW(); SPI_ExchangeByte(0x05); status SPI_ExchangeByte(0x00); CS_HIGH(); return status; }如果你要保护0x00000到0x0FFFF这段区域存放标定参数可以设置BP11BP00。具体保护范围参考数据手册的块保护表。设置完之后即使误发WRITE命令被保护的区域也不会被改写。5. 数据存储策略与掉电保护设计5.1 数据结构设计在MRAM里存数据不能像RAM那样随便写。我通常把512KB空间分成几个区域系统参数区、运行日志区、故障快照区、预留区。每个区域用固定的地址偏移结构体定义好字段。typedef struct { uint32_t magic; // 0xAA55AA55标识数据有效 uint16_t version; // 数据结构版本 float calib_k; // 标定系数K float calib_b; // 标定系数B uint32_t run_hours; // 累计运行小时 uint32_t power_on_count; // 上电次数 uint8_t reserved[16]; // 预留 uint16_t crc; // CRC16校验 } SystemParams_t;magic字段用来判断这块区域是否已经初始化过。第一次上电时读到的magic不是0xAA55AA55说明是空白MRAM需要写入默认值。version字段用于后续数据结构升级如果版本号不匹配可以执行迁移逻辑。5.2 双备份与CRC校验工业现场最怕的是写到一半掉电数据变成半新半旧。MRAM虽然写入即固化但如果数据结构跨多个字节写入过程中掉电仍然可能导致部分字段更新、部分字段未更新。解决办法是双备份加CRC。具体做法在MRAM里分配两块相同大小的区域A和B每次更新数据时先写A区CRC校验通过后再写B区。读取时先读A区CRC不对再读B区。如果A和B的CRC都不对说明数据损坏使用默认值。void SaveParams(SystemParams_t *params) { params-crc CRC16_Calculate((uint8_t*)params, sizeof(SystemParams_t) - 2); // 写A区 MRAM_Write(PARAM_A_ADDR, (uint8_t*)params, sizeof(SystemParams_t)); // 验证A区 SystemParams_t temp; MRAM_Read(PARAM_A_ADDR, (uint8_t*)temp, sizeof(SystemParams_t)); if(temp.crc params-crc) { // A区写入成功再写B区 MRAM_Write(PARAM_B_ADDR, (uint8_t*)params, sizeof(SystemParams_t)); } }这个策略的代价是写入时间翻倍但MRAM写入速度极快一条几十字节的记录写入时间在微秒级翻倍后仍然可以接受。可靠性提升是值得的。5.3 掉电检测与紧急写入有些数据在掉电瞬间必须保存比如电机的当前位置、阀门的开度。PIC18F85K90有内置的掉电检测模块HLVD可以设置触发电压。当电源电压下降到阈值时HLVD产生中断在中断里把关键数据写入MRAM。void __interrupt(high_priority) HighISR(void) { if(PIR2bits.HLVDIF) { PIR2bits.HLVDIF 0; // 紧急保存关键数据 MRAM_Write(EMERGENCY_ADDR, (uint8_t*)critical_data, sizeof(critical_data)); } }HLVD的阈值要设置得比单片机最低工作电压略高留出足够的写入时间。MRAM写入一个字节的时间在纳秒级加上SPI传输开销写32字节大约需要几十微秒。HLVD触发后电源上的电容要能维持单片机工作至少100微秒。这个时间取决于电容容量和系统功耗需要实际测量。提示HLVD中断里不要做复杂运算只做最紧急的写入。CRC计算可以提前算好中断里直接写。6. 常见问题排查与实战避坑指南6.1 读写失败问题速查表现象可能原因排查方法解决方案读回全0xFFCS未拉低或SPI未初始化示波器看CS和SCK波形检查CS引脚配置和SPI初始化代码读回全0x00SO引脚未连接或方向错误万用表测SO通断检查TRISC4设为输入数据偶尔错误SPI时钟太快或走线太长降低SPI时钟测试降低到4MHz或更短走线写入后读回不变WREN命令未发送或CS时序不对读状态寄存器WEL位确保WREN和WRITE之间CS有上升沿上电后数据丢失电源时序问题或CS浮空示波器看上电时序CS加10K上拉确保同时上电高温下读写异常时序裕量不足高温箱测试降低SPI时钟增加CS建立时间6.2 我踩过的三个坑第一个坑是CS引脚配置。PIC18F85K90的RB2引脚复用功能很多我一开始忘了把TRISB2设为输出结果CS一直由外部上拉保持高电平MRAM根本不响应。后来用示波器看CS波形发现一直是高电平才意识到方向寄存器没配。第二个坑是SPI模式不匹配。MR25H40CDF支持模式0和模式3我一开始配的是模式0CKP0CKE0但代码里SSPSTAT的CKE位设错了实际工作在模式1数据在时钟第二个边沿采样导致读回的数据整体偏移一位。后来对照数据手册的时序图把CKE改回0才正常。第三个坑是写保护。MR25H40CDF的WP引脚我一开始悬空了以为内部有上拉。实际上WP悬空时芯片可能随机进入写保护状态写入命令被忽略。后来把WP和HOLD都上拉到VDD问题解决。这个坑花了我一个下午因为读操作正常只有写操作失败很容易误判为MRAM坏了。6.3 性能优化技巧如果你需要高频写入比如每毫秒记录一次数据有几个优化点。第一把SPI时钟提到最高MR25H40CDF支持40MHzPIC18F85K90在64MHz系统时钟下SPI最高16MHz实际跑10MHz很稳。第二减少CS的翻转次数连续写入多个字节时CS保持低电平只在命令切换时拉高。第三用DMA或者FIFO减少CPU干预不过PIC18F85K90的SPI FIFO只有16字节效果有限。读取性能方面MRAM没有读取延迟SPI时钟多快就读多快。如果只是读少量数据直接读就行。如果要读大块数据比如导出整个日志区可以用连续读模式CS保持低电平地址自动递增不需要每次发命令。7. 实际项目中的扩展应用7.1 数据记录仪的实现我用这套方案做过一个环境数据记录仪每秒钟采集温度、湿度、压力三个参数加上时间戳打包成16字节的记录写入MRAM。512KB空间可以存32768条记录按每秒一条算可以连续记录9个多小时。如果降低采样率到每分钟一条可以记录22天。记录区的管理用环形缓冲写指针到达区域末尾后回绕到开头覆盖最旧的记录。读的时候从写指针往前读读到magic无效为止。这种方式不需要文件系统代码简单可靠性高。7.2 参数存储与在线更新工业仪表经常需要在线更新标定参数。用MRAM存储参数的好处是更新速度快不需要停机。更新流程是上位机通过串口发送新参数 → 单片机接收并校验 → 写入MRAM的B区 → 读回验证 → 更新A区 → 返回成功。整个过程在毫秒级完成不影响正常测量。如果更新过程中掉电A区还是旧参数B区可能是新参数也可能是半新半旧。下次上电时先读A区CRC通过就用A区如果A区CRC不通过读B区CRC通过就用B区并回写到A区。这个逻辑保证了参数永远不会丢失。7.3 故障快照与黑匣子设备故障时把故障前后的关键数据写入MRAM事后分析。比如电机控制器故障时记录三相电流、母线电压、转速、温度、故障码打包成一条快照。MRAM的写入速度足够快可以在故障发生后几微秒内完成记录捕捉到故障瞬间的波形数据。快照区单独划分不参与环形覆盖写满后停止记录等待人工读取。每条快照带时间戳和CRC读取时按时间排序方便分析故障序列。8. 写在最后的一些个人体会这套MRAM加PIC18F85K90的方案我从2019年用到现在前后做了七八个项目最长的已经在现场跑了四年多没有出现过数据丢失。MRAM的价格比SPI Flash贵不少但在关键数据存储场景里这个成本是值得的。一次现场数据丢失导致的停机损失可能就够买几百颗MRAM了。调试SPI存储的时候示波器是最有用的工具。CS、SCK、SI、SO四根线一起抓对照数据手册的时序图看大部分问题都能定位。不要靠猜不要靠改代码试直接看波形。最后分享一个小技巧MRAM的WP和HOLD引脚如果不使用一定要上拉。我现在的做法是在PCB上直接放两个10K电阻不依赖单片机内部上拉。这个习惯帮我省了很多调试时间。