ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MRAM替代EEPROM:PIC18F56K42+MR25H40CDF工业掉电保存方案

MRAM替代EEPROM:PIC18F56K42+MR25H40CDF工业掉电保存方案 做工业设备控制器的朋友大概率都经历过这种尴尬设备被人断了电重启后生产数据丢了又得重新校零点、对产量、补记录。我最近一台上料机控制器就遇到这个事原方案用普通EEPROM做掉电保存结果计数器频繁写入只撑了三个月个别位开始翻转日志区也时不时出现半截数据。后来我把存储芯片整体换成了 Everspin 的 MR25H40CDF4Mbit SPI MRAM主控继续用 Microchip 的 PIC18F56K42整套系统才算真正安静下来。这篇文章就把这套方案完整拆开讲为什么在工业场景里我会弃用EEPROM和Flash、MR25H40CDF这颗芯片的读写指令和状态寄存器该怎么用、PIC18F56K42的SPI与PPS怎么配置以及驱动代码、掉电保存流程、双备份策略和几个实际踩过的坑。无论你是做嵌入式硬件还是写固件这套思路基本可以照搬。1. 为什么我把板载EEPROM换成了MR25H40CDF工业存储的三个硬伤1.1 现场频繁写数据时遇到的真实故障先说我之前那套方案的死法。控制器里原来有一颗I2C接口的EEPROM容量2Kbit主要存产量累计、配方参数、报警记录。设备一天8小时运行产量计数器每分钟更新一次加上报警、断电标记、临时日志平均每天写入能在2000次上下。看起来不多但EEPROM标称写寿命10万次内部还带磨损均衡逻辑实际上三个月左右就开始出问题。最先坏的是日志区。现场反馈设备偶尔出现报警记录存下来却是乱码我远程拉日志看发现某些字节读出来是0x00或者0xFF个别位丢得毫无规律。一开始怀疑是干扰后来用编程器反复读写那块EEPROM发现部分地址写入后回读即错或者写到一半数据就保持不住了。这就是写寿命耗尽浮栅电荷困不住块也擦不动了。第二个问题是掉电窗口。EEPROM写一个字节要等3到5ms内部编程时间如果设备在写数据瞬间被拉闸刚好卡在内部擦写周期里那这个字节大概率是坏的甚至可能把相邻数据带坏。现场电工也从来不会配合你挑断电时机设备说停就停这个坑几乎无解。第三个问题是日志和累计量在Flash上很难做。如果用SPI NOR Flash写入前必须先擦除整个扇区一个日志条目只有几十字节擦一次却要带出64KB甚至更大范围频繁写日志会产生严重的磨损不均衡。最后我实在忍不了决定换存储介质。1.2 MRAM和EEPROM、Flash的本质区别MR25H40CDF这颗芯片本质上不是Flash也不是EEPROM它是磁阻随机存取存储器MRAM。存储单元是磁隧道结靠磁化方向保存数据不是靠电荷。所以它有两个天然特性写入不需要擦除数据改哪个字节就写哪个字节写入寿命非常高Everspin给的数据是单bit可写10的14次方次以上工程上可以视为无限。再加上SPI接口、3.3V供电、掉电数据不丢正好覆盖前面说的三个痛点。用一张表对比会很清楚维度常用EEPROMSPI NOR FlashMR25H40CDF写前擦除不需要必须整扇区擦除不需要典型写寿命10万~100万次10万次左右10的14次方量级写一个字节耗时3~5ms内部编程页编程几十us但擦除几百msSPI传输完成即固化无内部等待随机写支持支持但受扇区磨损影响支持无页边界限制管理复杂度低高要考虑磨损均衡很低典型容量几百字节~几十KB1MB以上4Mbit即512KB很多人问为什么不用大容量EEPROM因为EEPROM密度上去后成本很高而且SPI EEPROM写速度还是慢。MRAM本质是SRAM加非易失随机读写速度快又没有擦除周期这是我选它的核心原因。1.3 512KB空间在工业设备里能放多少东西MR25H40CDF是4Mbit也就是512KB。这个容量放在嵌入式存储里不算大但用在工业控制器里其实非常宽裕。一台设备通常需要存的东西也就三类参数区配方、校准系数、通信地址、阈值运行数据区产量累计、计数器、机器位置、运行时长日志区报警记录、事件记录、操作日志。参数区我留32KB运行数据区留16KB日志区按每条256字节算512KB扣掉前面48KB还剩464KB能存1800多条日志足够现场倒查一个月的运行记录。关键在于以前存储容量不够是因为Flash日志要频繁擦除只能小批量循环覆盖。换成MRAM之后环形日志只管追加不需要擦除动作空间利用率高得多固件逻辑也简化一大截。这也是我把整板存储方案推翻重做的底气。2. MR25H40CDF的脾气引脚、状态寄存器与指令时序2.1 引脚定义和硬件连接要点MR25H40CDF常见的封装是8脚DFN引脚分配和普通SPI串行Flash很接近/CS、SCK、SI、SO、/WP、/HOLD、VDD、VSS。我先把每个引脚的注意事项说清楚后面画原理图会轻松很多。/CS是片选低电平有效整条指令期间必须保持低电平SCK是SPI时钟SI和SO分别是输入输出数据线注意SO在写指令阶段是高阻要等读命令后才把数据发出来。/WP是硬件写保护低电平有效。如果状态寄存器里的WPEN位是1并且/WP引脚被拉低那么写状态寄存器和被块保护的区域都会被锁死。我在实际项目里把这个脚接到MCU的GPIO平时拉高需要改块保护配置时才控制它。如果你不需要搞块保护直接把/WP接VDD也行省一个IO。/HOLD是暂停引脚低电平时芯片会暂停SPI通信SCK和SI上的信号都会被忽略。这个脚最容易被忽视一旦悬空板子在高频干扰下偶尔会出现通信错位。正确做法是直接接到VDD不使用该功能。供电方面MR25H40CDF的VDD范围一般在2.7V到3.6V典型3.3V和PIC18F56K42共用一路3.3V没有问题。如果PIC这边跑5V系统SPI线上必须加电平转换不能直接怼。2.2 状态寄存器里的WEL、块保护和WPENMR25H40CDF和标准SPI Flash一样有一个8位状态寄存器。几个关键位别搞错WEL是写使能锁存位写命令之前必须先发WREN把它置1写完一条指令后芯片自动清0BP0、BP1、BP2是块保护位决定哪部分地址被保护WPEN是硬件写保护使能位只有WPEN为1时/WP引脚的低电平才能真正挡住写状态寄存器和被保护区域的写入。默认状态下电后BP位和WPEN都是0WEL也是0所以芯片本身没有保护但每次写数据前仍必须发WREN这是很多刚上手的人漏掉的步骤。如果状态寄存器配置不对比如想把某块区域保护起来结果发现WRSR写不进去多半是/WP没拉高或者WPEN没使能。调试时先用RDSR把状态寄存器读出来看一眼比瞎猜快得多。2.3 指令集READ、WRITE、WREN、RDSR、RDIDMR25H40CDF的指令集和串行Flash非常像常用的就这几条指令命令码说明WREN0x06写使能必须在WRITE/WRSR之前发WRDI0x04写禁止关闭写使能锁存RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据24位地址后连续输出FAST_READ0x0B快速读24位地址后跟1字节dummyWRITE0x02写数据24位地址后连续写入RDID0x9F读JEDEC ID返回3字节指令时序并不复杂。读操作就是CS拉低发0x03再发高字节在前的24位地址然后连续收数据CS拉高结束。写操作要先发WREN等CS拉高后重新拉低发0x02、地址、数据全部发完CS拉高即完成。这里有个和Flash最大的区别MRAM写完之后没有内部编程等待时间CS拉高的瞬间数据就已经固化。也不需要按页拆分边界你从任意地址开始连续写512KB都行因为芯片本身就是随机存取结构。我在代码里处理批量写入时完全不用关心页对齐省掉了很多恶心的边界判断。3. PIC18F56K42侧的硬件准备PPS路由、电源和最小系统3.1 为什么选PIC18F56K42做主控PIC18F56K42是Microchip K42系列里的48引脚MCUFlash在64KB级别SRAM大概4KB左右内置ADC、DAC、比较器、运放、CLC之类的CIP外设。我选它的主要原因有两点一是PPS外设引脚选择SPI1的SCK、SDO、SDI可以映射到任意数字引脚画PCB时不用为了固定引脚绕线二是BOR、比较器、FVR这些外设配合起来做掉电检测非常顺手后面掉电保存方案全依赖这套外设。K42系列支持MPLAB Code Configurator也就是MCCSPI、PPS、比较器的初始化代码可以直接生成。我实际上会先用MCC把工程搭起来再在生成的代码上补充MRAM驱动这样比自己手写寄存器省时间也减少出错概率。3.2 原理图连接和我实际用的引脚分配我的板子3.3V供电PIC18F56K42和MR25H40CDF共用同一路电源。SPI1的SCK接到MRAM的SCKSPI1的SDO接MRAM的SISPI1的SDI接MRAM的SO。片选CS我用普通GPIO控制没有用硬件SS引脚因为CS的时序要求比较灵活用GPIO拉高拉低更直观。WP引脚接了一个GPIOHOLD固定接VDD不要在原理图上留空。PIC18F56K42的复位脚通过10k电阻上拉到3.3VMCLR上再并联一个0.1uF电容抗干扰。MR25H40CDF的VDD引脚就近放一个0.1uF陶瓷电容电源入口再放一个10uF钽电容这是基本的电源滤波配置。引脚分配上我特意把SPI1放到一个避开PWM输出和模拟输入的端口。K42的PPS很自由但自由不代表随便SPI时钟线最好远离电机驱动的高频PWM引脚防止串扰。实际板上SCK走线也尽量短MRAM离MCU不超过20mm。3.3 初始化流程先配SPI再配GPIO初始化顺序有讲究GPIO要先设置好输出锁存器的默认电平再切方向避免上电瞬间引脚乱跳。CS在上电初始化时先拉高WP先拉高HOLD接VDD不管。SPI1初始化时设为主模式时钟极性选Mode 0即空闲时SCK为低、数据在第一个边沿采样SPI时钟我一开始设4MHz后面再根据实测往上调。MCC生成SPI初始化代码后会有一个底层的字节收发函数通常是SPI1_ExchangeByte之类的接口。我实际项目里就是基于这个接口封装MRAM驱动不关心内部SPI寄存器怎么操作只需要保证CS时序由自己控制。如果你喜欢手写寄存器可以把SCK引脚配置为数字输出SDI配置为数字输入SDO配置为数字输出然后设置SPI1CON0和SPI1CON1这些寄存器配置以MPLAB X生成的值为准最稳。4. MRAM读写驱动直接可用的函数封装4.1 底层字节收发和片选控制无论是读状态寄存器还是读写数据MRAM的所有通信都建立在SPI字节收发之上。我在代码里这样定义底层的交换函数以MCC生成的SPI1_ExchangeByte为例uint8_t MRAM_SPI_Transfer(uint8_t byte) { return SPI1_ExchangeByte(byte); }CS的控制封装成两个宏一处修改全局生效#define MRAM_CS_LOW() LATBbits.LATB0 0 #define MRAM_CS_HIGH() LATBbits.LATB0 1注意每次指令开始前CS拉低指令结束后CS必须拉高。CS的拉高动作在很多芯片上意味着命令边界MR25H40CDF也不例外尤其写使能WREN必须在CS高电平的上升沿才真正生效。4.2 写使能、读状态寄存器和写状态寄存器写操作前必须先WREN这是最容易漏的一步。我封装了下面三个基础函数void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x06); MRAM_CS_HIGH(); } void MRAM_WriteDisable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x04); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x05); status MRAM_SPI_Transfer(0x00); MRAM_CS_HIGH(); return status; }写状态寄存器时要注意WRSR本身也需要先WREN而且/WP必须为高。如果WP接了GPIO写状态寄存器前要确保WP拉高void MRAM_WriteStatus(uint8_t value) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x01); MRAM_SPI_Transfer(value); MRAM_CS_HIGH(); }4.3 单字节和多字节读写单字节读和写是最基本的操作。读数据用0x03地址按高字节、中字节、低字节顺序发送uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t value; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((uint8_t)(addr 16)); MRAM_SPI_Transfer((uint8_t)(addr 8)); MRAM_SPI_Transfer((uint8_t)addr); value MRAM_SPI_Transfer(0x00); MRAM_CS_HIGH(); return value; } void MRAM_WriteByte(uint32_t addr, uint8_t value) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((uint8_t)(addr 16)); MRAM_SPI_Transfer((uint8_t)(addr 8)); MRAM_SPI_Transfer((uint8_t)addr); MRAM_SPI_Transfer(value); MRAM_CS_HIGH(); }批量读写其实就是把单字节操作扩展开连续传输数据void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((uint8_t)(addr 16)); MRAM_SPI_Transfer((uint8_t)(addr 8)); MRAM_SPI_Transfer((uint8_t)addr); for (i 0; i len; i) { buf[i] MRAM_SPI_Transfer(0x00); } MRAM_CS_HIGH(); } void MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t i; MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((uint8_t)(addr 16)); MRAM_SPI_Transfer((uint8_t)(addr 8)); MRAM_SPI_Transfer((uint8_t)addr); for (i 0; i len; i) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); }这里最大的优势就是不需要处理页边界。普通SPI Flash写256字节以上要考虑跨页MRAM完全没这个限制想怎么写就怎么写出错概率小很多。4.4 用RDID做上电自检我习惯在系统上电初始化时读一次JEDEC ID如果读不到或读到0xFF就说明SPI链路或者芯片本身有问题直接进故障状态不让设备带病运行。MRAM的RDID指令是0x9Fuint8_t MRAM_ReadID(uint8_t *id0, uint8_t *id1, uint8_t *id2) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x9F); *id0 MRAM_SPI_Transfer(0x00); *id1 MRAM_SPI_Transfer(0x00); *id2 MRAM_SPI_Transfer(0x00); MRAM_CS_HIGH(); if (*id0 0xFF *id1 0xFF *id2 0xFF) { return 0; } return 1; }这个自检函数虽然简单但在工业板卡上很实用能提前暴露焊接虚焊、引脚错位、电平不匹配这些问题而不是等到设备运行半年后才报数据异常。5. 存储空间规划与工业可靠性设计5.1 512KB怎么分区双备份参数区加环形日志区芯片拿到手第一件事不是急着写代码而是把512KB地址空间规划好。我用的布局是这样区域地址范围大小用途参数区A0x000000 - 0x0007FF2KB配方、通信参数、校准系数带CRC32参数区B0x000800 - 0x000FFF2KB参数区A的备份运行状态区0x001000 - 0x0017FF2KB产量、累计运行时间、当前位置日志区0x002000 - 0x07FFFF504KB环形日志每条256字节参数区用双备份的原因很直接虽然MRAM写入可靠性远高于EEPROM和Flash但任何存储介质都存在极端情况下数据损坏的可能。双备份加CRC校验上电时读两份哪份校验通过就用哪份如果两份都通过就用更新时间较新的那份。运行状态区存的是需要频繁更新的数据比如上料机当前绝对位置、伺服使能状态、当天产量。这些数据掉电不能丢但又每次变化都要写按一天几千次的频率只有MRAM扛得住。把这个区域独立划分出来还能避免和日志区的环形指针互相干扰。日志区用纯环形结构不需要擦除。每条日志固定256字节前面存时间戳后面存数据内容。我维护一个4字节的写指针写满末尾后回卷到头部因为不需要擦除块回卷逻辑简单到只有一行判断这在Flash方案里是无法想象的。5.2 掉电保存比较器加FVR抢在断电前写关键数据工业设备最怕的不是慢悠悠关机而是突然掉电。控制器主电源跌落瞬间MCU可能还有几十毫秒的存活时间这几十毫秒足够把关键数据写进MRAM。MR25H40CDF写入没有内部编程延迟SPI 4MHz时钟下写512字节大约1ms实际哪怕只保存几百字节整个过程不到半毫秒。我用的电压跌落检测方案是PIC18F56K42内部的模拟比较器加固定电压参考FVR。外部用两个电阻分压VDD分压点进CMP1正输入负输入接FVR的1.024V参考。正常3.3V时分压点高于参考掉电瞬间分压点跌到参考以下比较器输出翻转触发中断。MCC里配置CMP1和FVR很简单我要做的只是在中断里置一个标志位主循环一旦发现标志就立刻把当前运行参数打包写入MRAMvolatile uint8_t powerFailFlag 0; void __interrupt() cmp1_isr(void) { C1INTFbits.C1IF 0; powerFailFlag 1; } void SaveCriticalData(void) { if (powerFailFlag) { powerFailFlag 0; // 把关键状态打包进saveBuf MRAM_WriteBuffer(RUNNING_STATE_BASE, saveBuf, sizeof(saveBuf)); // 关掉不必要的外设等电压死透 while (1); } }这里有个细节/WP若接了GPIO掉电保存前要确认WP是拉高的否则写保护没开还好一旦配置了块保护关键数据可能写不进去。我把WP初始化为高电平并且在整个运行过程中不做块保护彻底避免这种隐藏风险。5.3 布局布线和DFN封装的焊接教训MR25H40CDF的DFN封装底部有散热焊盘手工焊接时容易只焊住侧面引脚底部焊盘虚焊。虚焊表现为常温测试正常板子一发热或者轻微振动就通信失败。我的建议是PCB上给底部焊盘开好过孔焊接后用万用表测VDD和VSS是否导通再上电跑一轮全芯片读写测试。SPI走线在4MHz下没什么太高要求但工业现场不一样板子旁边可能有变频器、伺服驱动器这些强干扰源。我在两个地方做了处理一是SCK和SI走线上各串一个22欧姆电阻靠近MRAM引脚放置抑制反射和振铃二是MRAM的VDD引脚旁边放0.1uF陶瓷电容同时整个3.3V电源入口处放10uF钽电容保证掉电保存瞬间电压不会跌得太快。另外如果PCB空间允许建议在MRAM和MCU之间预留一个0欧姆电阻的位置。万一现场发现SPI信号质量有问题可以在这里断开信号线串入逻辑分析仪观察时序排查问题会方便很多。6. 实测结果与踩坑复盘6.1 读写速度和寿命观察板子调通后我做了一轮实测SPI时钟4MHz单字节读周期约3us左右512字节批量读大约1.2ms512字节批量写大约1.3ms。这个速度对工业控制器的数据保存需求绰绰有余。后来我把SPI时钟提到8MHz读写时间接近减半板子跑了一个月也没有出现通信错误。如果你想压极限可以再看数据手册上的最高时钟限制但工业上我建议留出一倍以上裕量。寿命方面我做了个简单压力测试写一个固定地址每秒写一次连续跑了72小时这是约26万次写入回读数据始终一致。再往前推算真要写到10的14次方次按每秒一次也得写几百万年所以工程上完全不用做磨损均衡省心。6.2 我踩过的坑HOLD悬空、时钟极性、WP电平第一个坑就是HOLD悬空。最初画板时我觉得HOLD不用就空着结果设备在电机启停瞬间偶尔出现读回数据全0xFF排查了好久才发现CS时序全都对但HOLD悬空被干扰拉低芯片直接不响应了。把HOLD改接VDD之后再没出现过。第二个坑是SPI时钟极性。刚移植程序时我用的是Mode 3也就是SCK空闲高电平结果连续读数据时总是隔几个字节错一位逻辑分析仪一看才发现MRAM和MCU的采样沿没对齐。统一配置成Mode 0之后一切正常。这个教训让我现在每换一颗SPI芯片先查数据手册里的时序图确认CPOL和CPHA再写初始化代码。第三个坑是WP电平。我想用WRSR设置块保护结果死活写不进去RDSR读回来BP位一直是0。后来看手册才明白WPEN位没置1WP引脚拉低根本不影响状态寄存器写入反而让我误以为芯片坏了。正确的顺序是先把WPEN写1再把WP拉低这样块保护才真正生效。但实际工业应用里我基本不用块保护WP固定拉高简单又不闹心。6.3 一个很实用的调试技巧上电全片读写测试新板子回来我第一件事不是跑功能逻辑而是先做全片读写测试。写一段伪随机数填满整个512KB然后回读比对任何一个字节不一致都说明焊接或布线有问题。这个测试跑一遍大概几秒钟但能挡掉相当一部分DFN虚焊、引脚短路、电源噪声问题。测试代码也很简单按页分批写分批读比对失败就把地址和期望值、实际值打印出来。如果某一个固定区域总出错优先怀疑地址线、SI/SO短路如果随机出错优先怀疑电源和时钟质量。这套流程我沿用了很久每次换存储芯片都能快速验证硬件底子。最后再说一句我个人经验MRAM在工业设备里真正的价值不只是把数据存下来而是让你完全不用操心存储寿命和擦除管理。配上PIC18F56K42的PPS和比较器外设整个存储系统可以从单纯的数据保存升级成掉电自动保护现场的机制。如果手头正好有类似项目不妨按这个思路试一次省下的调试时间远比你想象的要多。
返回列表