ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MRAM替代EEPROM:PIC18F86J50+MR25H40CDF工业级存储方案

MRAM替代EEPROM:PIC18F86J50+MR25H40CDF工业级存储方案 上一台工业控制器的EEPROM又不够用了日志只能存三天而且反复写几百次以后寿命肉眼可见地往下掉。我最后把方案换成“MR25H40CDF PIC18F86J50”的组合一颗4Mbit的MRAM当存储一颗8位PIC做主控专门解决工业现场“频繁写、掉电不丢、开机能读回来”的问题。这篇文章是我这次改造的完整记录从选型逻辑、硬件接线、PIC18F86J50的SPI驱动到调试验证时踩过的几个坑一次讲清楚。工业仪表、PLC、运动控制、电力终端这类场景的工程师可以直接参考这套做法。1. 为什么MRAM会出现在工业存储场景1.1 三种非易失存储方案的横向对比做嵌入式的人都知道非易失存储无非就那几类EEPROM、NOR Flash、MRAM。以前我习惯性用EEPROM因为便宜、驱动简单、字节随便写。但一涉及到“每秒钟要写几十条记录”“一年要跑几百万次写入”“掉电瞬间要保存关键状态”这些需求EEPROM就明显顶不住了。EEPROM写一个字节要5到10毫秒这在工业控制器里是很致命的时间开销。如果主流程在高速运转突然要写一条日志一下卡住几个毫秒某些对时间敏感的控制逻辑就会出问题。而且EEPROM的寿命一般标10万到100万次擦写看着不少但高频日志可能在几个月内就用完。NOR Flash容量大、价格低但是擦除粒度大、擦除时间毫秒级甚至上百毫秒还必须做磨损均衡和坏块管理。它的写寿命虽然在10万次左右但对于频繁覆盖的数据仍然需要软件层花大量精力去维护。更重要的是Flash写一个page往往需要先擦掉整个扇区掉电管理一做不好数据完整性就悬了。MRAM是磁阻随机存储器它同时具备“内存的速度”和“Flash的非易失性”。MR25H40CDF这颗4Mbit MRAM容量是512KB写入不需要擦除没有延迟寿命上几乎可以无限次写。唯一的缺点是单价偏高但把返修成本、产品可靠性、售后时间成本算进去工业应用里真的划算。1.2 MRAM为什么“写不坏”MRAM的存储单元核心是磁性隧道结上下两层铁磁材料中间夹着一层极薄的绝缘层。上面一层磁化方向固定下面一层可以改变方向。两层平行时电阻低代表“0”两层反平行时电阻高代表“1”。读数据就是测电阻写数据就是翻转磁化方向整个过程中不存在电子的注入和释放也就不存在像Flash那样的电荷陷阱累积。这点和一个磁性白板很像。你把磁贴翻个方向贴纸不会损耗翻一万次、一亿次磁贴还是那个磁贴。Flash和EEPROM是靠浮栅里存电荷来维持状态每次擦写都会对氧化层造成一些损伤损伤积累到一定程度就漏电单元就废了。另外MRAM的读操作是非破坏性的读一遍不会改变磁化状态这比FRAM更强一点。FRAM虽然也是非易失RAM但它的读操作是破坏性的读完之后要自动写回读操作本身也会参与疲劳过程。所以在“高频写高频读”的工业应用里MRAM的结构性优势非常明显不需要复杂的寿命管理机制。1.3 MR25H40CDF这颗料有什么特点MR25H40CDF是Everspin的4Mbit串行MRAM容量为512KB接口是标准SPI四线命令集和常见的SPI NOR Flash几乎一样所以驱动移植成本很低。它支持最高40MHz的SPI时钟在3.3V供电下字节地址用24位表示操作地址范围是0x000000到0x07FFFF。它的写入页大小是256字节和很多SPI Flash的概念一样但区别在于MRAM“每一页都像内存一样可以直接写”不需要先擦除旧数据。它内部还自动处理读-修改-写操作单独写一个字节时芯片会保证周边字节不受影响。这一点对参数存储特别友好不需要像Flash那样先读整页到RAM再改一个字节再整页写回去。芯片供电是2.65V到3.6V和PIC18F86J50的3.3V系统完全兼容。工作温度范围是-40到85摄氏度适合工业环境。它还支持休眠命令休眠状态下功耗很低对电池供电的仪表设备很实用。芯片的状态寄存器可以用RDSR命令读取主要用来判断写使能锁存的状态具体位定义要核对批次数据手册驱动里一般不依赖它做busy轮询因为MRAM写入本身是瞬间完成的。2. 硬件连接与关键设计2.1 PIC18F86J50的SPI资源PIC18F86J50是Microchip的8位增强型单片机最高工作频率48MHz带MSSP模块可以配置成SPI主模式。它本身资源不算特别丰富但做数据采集、协议转换、设备控制这类任务绰绰有余。MRAM只占少量IO两者搭配起来非常合适。在80引脚封装的PIC18F86J50上MSSP模块的SPI引脚通常对应RC3、RC4、RC5分别作为SCK、SDI、SDO使用。具体端口分配要按实际封装和硬件设计手册确认但思路一样SCK输出时钟SDO输出数据SDI输入数据。片选信号我用一个普通GPIO直接控制不依赖外设的自动片选这样时序完全可控MRAM的CS上升沿和下降沿都能精确控制。如果你用的是别的PIC18型号只要MSSP引脚位置不同改一下TRIS方向和端口定义就行后面的驱动代码结构不需要变。2.2 MR25H40CDF与PIC18F86J50的引脚连接下面是我实际用的接法直接给大家参考MR25H40CDF引脚功能连接到说明CS#片选PIC18F86J50的RA5加10k上拉电阻SCK时钟PIC18F86J50的RC3可加33Ω串联电阻DI数据输入PIC18F86J50的RC5SDO单片机输出到MRAM输入DO数据输出PIC18F86J50的RC4SDI单片机输入WP#写保护3.3V直接拉高禁用写保护HOLD#暂停通信3.3V直接拉高不用暂停功能VCC电源3.3V就近放去耦电容GND地系统地低阻抗连接CS#加上拉电阻非常重要。单片机在上电复位阶段IO口会处于高阻状态如果CS#浮空MRAM可能误识别一次片选动作。上拉到3.3V以后不会出现这种问题。WP#和HOLD#这两个引脚一定不要悬空悬空时芯片内部没有可靠的上拉外来干扰可能让芯片进入写保护或暂停状态我在调试中就吃过这个亏后面细说。2.3 电源和PCB布局的几个细节MR25H40CDF在工作时电流不大但工业现场电源波动大供电质量不能马虎。我在VCC引脚附近放了一个1uF陶瓷电容和一个0.1uF陶瓷电容两者尽量靠近芯片引脚给高速切换的电流提供低阻抗回路。PIC18F86J50的供电同样做了去耦数字地和模拟地单点连接。SPI信号线在PCB上尽量短并用“时钟线单独走、数据线不跨层”的方式布线。如果SPI通信频率比较高信号线超过10厘米建议在SCK、DI上加33欧姆串联电阻减少过冲和振铃。实测下来短距离板内连线用12MHz SPI时钟没什么问题但如果通过接插件连接外部模块我建议把时钟降到4MHz或1MHz优先保证通信稳定。3. PIC18F86J50端SPI驱动实现3.1 MSSP模块初始化配置用Microchip XC8编译器先把MSSP配成SPI主模式。MSSP支持几种SPI模式这里用标准模式0也就是CPOL0、CPHA0。在PIC的寄存器里模式0需要把CKP设为0、CKE设为1。我的初始化代码是这样的#include xc.h #define MRAM_CS_LAT LATAbits.LATA5 #define MRAM_CS_TRIS TRISAbits.TRISA5 void SPI_Init(void) { // CS引脚配置 MRAM_CS_TRIS 0; MRAM_CS_LAT 1; // SPI引脚方向SCK输出、SDI输入、SDO输出 TRISCbits.TRISC3 0; TRISCbits.TRISC4 1; TRISCbits.TRISC5 0; // SSPEN1, CKP0, SSPM0000 主模式时钟FOSC/4 SSPCON1 0b00100000; // CKE1, SMP0 SPI Mode 0 SSPSTAT 0b01000000; }SSPM设置为0000SPI时钟就是系统时钟的四分频。PIC18F86J50如果跑16MHz那么SPI时钟就是4MHz对MR25H40CDF来说非常轻松。如果跑48MHz那SPI时钟就是12MHz依然在MRAM的能力范围内但信号完整性要注意。想要更稳妥的生产环境可以把SSPM改成0011也就是FOSC/64先把功能跑对再逐步提速。3.2 底层字节收发函数SPI收发是一个交换过程发一个字节的同时会收到一个字节。MSSP模块发送时把数据写入SSPBUF然后等标志位BF置1再读SSPBUF拿到返回数据。这个函数是整个驱动的地基uint8_t SPI_Transfer(uint8_t byte) { SSPBUF byte; while (!SSPSTATbits.BF); return SSPBUF; }看起来简单但要注意两个细节。第一写SSPBUF之前要确保上一次传输已经结束否则会触发写冲突。在实际驱动里可以通过检查WCOL位来判断是否有过冲突调试时尤其有用。第二读取SSPBUF的动作本身会清除BF标志位所以不要在读完之前再次写入SSPBUF。CS的操作我单独封装这样命令层代码读起来更清晰static void MRAM_CS_Low(void) { MRAM_CS_LAT 0; } static void MRAM_CS_High(void) { MRAM_CS_LAT 1; }3.3 命令层写使能、读状态、禁用写MR25H40CDF写入数据之前必须先发WREN命令把内部的写使能锁存位置1。这是SPI MRAM和EEPROM不一样的地方。EEPROM通常没有这个机制MRAM为了防误写沿用了Flash的命令体系。WREN命令有个关键时序要求CS拉低后发送一个字节0x06然后必须把CS拉高这个上升沿才真正把写使能锁存住。我的命令定义和基础操作如下#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 void MRAM_WriteEnable(void) { MRAM_CS_Low(); SPI_Transfer(MRAM_CMD_WREN); MRAM_CS_High(); } void MRAM_WriteDisable(void) { MRAM_CS_Low(); SPI_Transfer(MRAM_CMD_WRDI); MRAM_CS_High(); } uint8_t MRAM_ReadStatus(void) { uint8_t stat; MRAM_CS_Low(); SPI_Transfer(MRAM_CMD_RDSR); stat SPI_Transfer(0xFF); MRAM_CS_High(); return stat; }调试时读状态寄存器可以确认WEL位是不是真的置1。如果发了WREN之后WEL位没变化大概率是CS时序没写好或者WREN操作数不对。MRAM没有Flash那种busy轮询概念写入完成后立刻读到的就是新数据所以RDSR主要用来做系统自检和调试不参与每次写入的等待流程。4. MR25H40CDF数据读写封装4.1 连续读和连续写函数MR25H40CDF的READ命令格式是0x03后面跟3字节地址然后是连续的读数据。WRITE命令格式是0x02后面跟3字节地址然后是连续写数据。地址都是高字节先传和SPI接口的传统习惯一致。连续读函数void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_Low(); SPI_Transfer(MRAM_CMD_READ); SPI_Transfer((uint8_t)(addr 16)); SPI_Transfer((uint8_t)(addr 8)); SPI_Transfer((uint8_t)(addr)); while (len--) { *buf SPI_Transfer(0xFF); } MRAM_CS_High(); }连续写函数void MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_Low(); SPI_Transfer(MRAM_CMD_WRITE); SPI_Transfer((uint8_t)(addr 16)); SPI_Transfer((uint8_t)(addr 8)); SPI_Transfer((uint8_t)(addr)); while (len--) { SPI_Transfer(*buf); } MRAM_CS_High(); MRAM_WriteDisable(); }写入必须把MRAM_WriteEnable放在CS拉低之前这是标准SPI命令流程。写完以后我习惯执行一次MRAM_WriteDisable把写使能锁存清掉防止后续干扰造成意外写入。代价极小收益却很实在。4.2 跨页写入处理MR25H40CDF的页大小是256字节地址每256字节对页取模。如果写入长度跨过了页边界芯片会把地址回绕到页开头覆盖掉本页起始位置的数据。这种情况在驱动层面必须处理否则连续写入一个结构体时就可能踩雷。我做了一个带分页处理的写入函数void MRAM_WriteBytesPaged(uint32_t addr, uint8_t *buf, uint32_t len) { while (len) { uint32_t page_left 256 - (addr 0xFF); uint32_t chunk page_left len ? page_left : len; MRAM_WriteBytes(addr, buf, chunk); addr chunk; buf chunk; len - chunk; } }这个函数的思路很直接先算当前地址还剩多少到页尾每次最多写这个长度然后地址递增直到全部写完。结构体长度只要不超过256字节一次PD没有分页问题但做设备固件时不能赌结构体长度以后加字段很容易超页封装到位更省心。4.3 用CRC16保证数据完整性工业现场的数据不能简单相信“写进去就读出来”。SPI信号受到干扰、地址配错、掉电瞬间写一半都会让读回的数据和原数据不一致。我的习惯是给每个存储块加CRC16读出来以后先校验再使用。CRC16的实现可以很简单用多项式0x1021就行uint16_t CRC16_CCITT(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; uint8_t i; while (len--) { crc ^ ((uint16_t)(*data) 8); for (i 0; i 8; i) { if (crc 0x8000) { crc (crc 1) ^ 0x1021; } else { crc crc 1; } } } return crc; }配合结构体存储比如设备参数typedef struct { uint32_t magic; uint16_t version; uint16_t temperature_setpoint; uint16_t pid_kp; uint16_t pid_ki; uint16_t pid_kd; uint8_t work_mode; uint8_t reserved[3]; uint16_t crc; } DeviceParam;保存时先填magic和业务字段再算CRC最后把整块写进MRAM。读取时先读整块再检查magic和CRC。CRC放在最后一个字段计算时长度要减去CRC字段本身的字节数。void DeviceParam_Save(DeviceParam *param, uint32_t base_addr) { param-magic 0x4D524D31; param-crc CRC16_CCITT((uint8_t *)param, sizeof(DeviceParam) - 2); MRAM_WriteBytesPaged(base_addr, (uint8_t *)param, sizeof(DeviceParam)); } uint8_t DeviceParam_Load(DeviceParam *param, uint32_t base_addr) { MRAM_ReadBytes(base_addr, (uint8_t *)param, sizeof(DeviceParam)); if (param-magic ! 0x4D524D31) return 0; if (CRC16_CCITT((uint8_t *)param, sizeof(DeviceParam) - 2) ! param-crc) return 0; return 1; }设备上电以后固件调DeviceParam_Load校验如果返回0就说明参数区损坏可以触发恢复出厂值或进入维护模式。4.4 日志环形缓冲区的设计思路如果要存几十万条历史日志全写在固定地址然后递增地址显然不可行地址总会跑完。工业设备常用环形缓冲固定一块区域写满了再从头部覆盖。MRAM的优势就在这里它不需要擦除直接覆盖旧数据环形缓冲实现极其简单。我建议先用一个LogHeader结构体记录当前写指针位置而不是每次开机扫描全部记录typedef struct { uint16_t head; uint16_t generation; uint16_t crc; } LogHeader;每次写入一条新记录后更新LogHeader。抄写头时可以做双备份也就是把LogHeader写在两个不同地址主头校验失败就用备份头。这样即使掉电发生在更新header的瞬间系统也能通过备份恢复出有效的写位置。MRAM不像Flash那样有块对齐要求header可以放在任意地址这一点非常方便。5. 调试验证中踩过的坑5.1 读回全0xFFSPI模式配错第一次调通硬件后我用READ命令读MRAM结果所有数据都是0xFF。写进去的数据再读回来还是0xFF很像芯片根本没响应。用示波器抓SPI时序后发现SCK波形正常、MOSI命令也正常但MISO一直是高电平。问题出在MSSP的SPI模式配置上。我把寄存器设置成了SPI Mode 1而不是Mode 0。MR25H40CDF支持模式0和模式3当时我没有把CKE位设置成模式0对应的值导致芯片在错误的采样沿解析命令。处理方式很简单把CKE置1后MISO立刻出现正常数据。这里要提醒大家不同单片机的模式命名并不统一PIC的SSPSTAT寄存器里CKE位和标准SPI CPOL/CPHA不是一一对应一定要抓波形确认不要只看“配置成模式0”这几个字。5.2 写不进去忘了写使能换上MRAM之后读没有问题但写操作怎么执行都不生效读回的数据还是原来的值。查了很多寄存器后才发现是漏掉了MRAM_WriteEnable。MR25H40CDF继承了SPI Flash的写保护机制。芯片内部写使能锁存默认是0必须发0x06命令把锁存位置1紧接着CS拉高生效之后WRITE命令才会被接受。我最初把WRITE命令发完直接拉低CS整个过程看起来没什么问题实际芯片把所有写命令都忽略了。解决办法就是严格按照“WREN - CS高 - WRITE - CS高”的顺序执行并且写完以后读回校验。这条流程看起来琐碎但少了任何一步都会踩坑。5.3 跨页写入覆盖了不该覆盖的数据刚开始我用MRAM保存设备参数结构体大概100多字节直接调用MRAM_WriteBytes写。后来结构体加字段长度超过了256字节一部分数据突然出现和预期完全不符的情况。排查后发现问题出在跨页回绕。地址走到页尾后继续写会自动回绕到同一页的起始位置覆盖原有内容而不是自动进入下一页。数据结构的头部因此被损坏。后来我把所有上层调用统一改成MRAM_WriteBytesPaged问题彻底消失。5.4 WP#和HOLD#悬空导致的随机故障原型板上WP#和HOLD#引脚一开始没有连线结果系统运行时偶尔会出现MRAM没有响应读回的数据突然全0xFF或者写入失败重启后又恢复正常。这种随机故障最让人头疼查了大半天最后发现HOLD#在高阻状态下受到干扰芯片进入了Hold状态SPI通信被暂停。把WP#和HOLD#全部直接接到3.3V后故障再也没出现。所以画原理图时这两个引脚一定要处理要么接单片机的IO做控制要么直接接高电平。不要以为不用的引脚就能悬空芯片手册上写着“内部无上拉”的引脚尤其要注意。5.5 掉电瞬间写入不完整的处理办法工业现场经常毫无预兆地停电。MRAM本身没有擦除延迟掉电写入成功率比Flash高很多但也不是完全无损。掉电瞬间电源电压一路下滑如果MCU检测到电压异常后再去执行SPI写入可能写到一半电压已经低于MRAM的最低工作电压数据就损坏了。我用PIC18F86J50的低电压检测模块把阈值设为3.0V中断触发后第一时间把关键数据打包写入MRAM。由于MRAM写入是即时完成的几十个字节的数据在电源跌落到2.65V以下之前往往就能写完。同时我在设计上还加了“双槽写入”机制同一份关键数据写两个地址每次启动时比较两份数据的CRC取有效的那份。这样即便某一次真的写到一半掉电另一份备份也能兜底。5.6 性能实测数据配置好SPI时钟以后我顺手测了几组基础数据。PIC18F86J50跑16MHzSPI时钟4MHz连续读取1KB数据大约耗时2.1ms连续写入1KB数据大约也是2.1ms因为MRAM写入不需要额外的擦除等待。如果把SPI时钟提到12MHz1KB读写在0.7ms左右对工业控制器来说完全够用。SPI时钟单字节写耗时写入1KB耗时1MHz约8µs约8.2ms4MHz约2µs约2.1ms12MHz约0.7µs约0.7ms这个速度比EEPROM动不动几毫秒写一个字节强太多了。高频记录日志时主循环几乎感受不到写入带来的卡顿。我个人在这套方案里最满意的点实际跑完这套方案以后我最强烈的感受是MRAM把“存储”这件事的复杂度大幅降低了。不需要磨损均衡、不需要页擦除管理、不需要等待busy、不需要小心翼翼的写策略驱动层只要处理好跨页和校验就够。对于做产品的人来说少一个管理模块就少一类bug长期维护成本非常可观。另外一个小建议量产时在开机初始化流程里加一段自检读一次MRAM的状态寄存器再读一块已知的数据确认SPI链路正常后再进入主逻辑。这个检查成本很低但能提前发现硬件接触不良、供电异常等问题让现场维护人员在设备还没跑起来的时候就能看到错误指示。这套“MR25H40CDF PIC18F86J50”的组合我现在已经把它作为工业控制器存储的标准配置在用。如果你的项目也在为EEPROM容量发愁、为Flash寿命头疼、为掉电保存数据不放心照着这个思路做一版评估应该很快就能判断MRAM到底适不适合你的设备。
返回列表