ARTICLE DETAIL

资讯详情

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

MRAM在工业数据记录仪中的应用:SPI接口与掉电保护设计

MRAM在工业数据记录仪中的应用:SPI接口与掉电保护设计 先说个我自己的经历。前阵子给一套工业环境数据记录仪做改版客户需求特别直白每秒钟往存储里写一条记录每天大概86400条断电不能丢数据至少存一个月还要能随时回读。我之前当然也用过外部EEPROM、SPI NOR Flash这类方案但单从这一个需求上两条路都不太顺EEPROM写寿命低大流量写入很快就到寿命极限NOR Flash要扇区擦除、磨损均衡程序复杂度上去了掉电保护还是得提心吊胆。最后我选定了Microchip的PIC18F86K90做控制核心外部存储器用Everspin的MR25H40CDF——一颗4Mbit的SPI接口MRAM。这篇就借着这个项目把这个存储方案从头到尾拆开讲清楚为什么选MRAM、硬件怎么接、驱动怎么写、应用层怎么设计以及调试过程中踩过的一些坑。适合正在给仪表、采集器、工控设备做数据存储或者对MRAM这个存储品类有兴趣的嵌入式开发朋友参考。一家之言有不同方案欢迎评论区讨论。下面直接进正题。1. 方案怎么定下来的从“该用什么存储”想到MRAM1.1 需求从头捋一遍很多人一看到“存储数据”四个字第一反应就是“找个Flash写进去不就行了”。但实际落地之前需求必须抠细。我这个项目的情况是每秒产生一条记录记录内容是时间戳、温度、多个通道电压值、状态标志等单条大小16字节记录要循环覆盖保存最少保留30天也就是大约259万条记录上电后能直接从上次的记录点继续写不丢索引现场可能随时断电不允许因为断电把整段日志搞坏需要支持外部设备RS485/Modbus读取历史记录板子上还要带一个段式LCD实时显示运行状态。这几条列出来存储需求就非常明确要求写寿命极高一天写86400次要求字节级随机写索引和参数要频繁更新要求掉电后数据稳定要求容量在256KB以上。别的先不说光是“每天86400次写”这一条就已经把很多常规存储介质堵在门外了。1.2 为什么EEPROM和NOR Flash都撑不住先看EEPROM。传统I2C/SPI EEPROM的标称擦写寿命一般是10万到100万次工程上按最保守的10万次算每天写86400次一天多一点就到上限了。哪怕按100万次算也就撑十来天。而且EEPROM的页写时间和字节写时间都在毫秒级别写一个字节要等3到5毫秒每秒写记录虽然不要求超高速度但频繁擦写带来的寿命焦虑是硬伤。再看NOR Flash。SPI NOR Flash虽然容量大、便宜但它的架构决定写入之前必须按扇区擦除擦除一次要几十到几百毫秒。做日志记录时要么搞“满一个扇区再擦下一个”的流水线要么就得引入FTL层做磨损均衡代码量直接上一个台阶。更重要的是如果掉电正好发生在扇区擦除或者页编程过程中轻则丢当前页数据重则把文件系统目录搞坏。MRAM刚好是两个方案的“折中点”它不像EEPROM那样怕写寿命比EEPROM高出好几个数量级也不像NOR Flash那样要擦除写入直接覆盖物理地址不存在“先擦后写”的流程。在这个项目里我需要的不是“大容量”而是“写得起”“不怕掉电”“实现简单”MRAM简直是为这类需求量身定做的。1.3 MRAM凭什么把这件事做简单MRAM的存储单元核心是一个磁隧道结MTJ两个铁磁层中间夹一层很薄的氧化物势垒。两个铁磁层的磁化方向平行时电阻低反平行时电阻高正好对应0和1。写入的时候通过内部电路改变自由层的磁化方向撤掉电源后磁化方向不会变所以数据天然是非易失的。整个过程不需要像EEPROM那样往浮栅里注入电荷也不需要像Flash那样先擦除再写入。我用的MR25H40CDF就是Everspin的SPI接口MRAM容量4Mbit换算过来是512KB对日志型应用来说很够用。它的关键参数可以快速过一下参数MR25H40CDF 的典型值容量4Mbit / 512KB接口SPI片选CS# SCK SI SO最高SPI时钟40MHz实际系统不一定跑这么高写寿命官方标称不低于10的12次方次部分资料给到10的13次方写操作方式直接覆盖写不需要擦除可字节级寻址状态查询状态寄存器WIP位轮询判断忙/闲工作温度工业级满足-40℃到105℃常规需求封装DFN8尺寸很小适合紧凑PCB用一句话总结我的感受MRAM把“日志存储”这件事从“伺候Flash”变成了“随便写”。你用SRAM的思维去读写一个地址空间掉电了它也不会忘这就能省掉一大堆维护逻辑。2. 硬件连接与PCB设计2.1 器件引脚与前因后果MR25H40CDF虽然功能强但引脚其实特别简单DFN8封装引脚不多CS#片选低有效SCKSPI时钟SI串行数据输入接MCU的SDO/MOSISO串行数据输出接MCU的SDI/MISOWP#写保护低电平禁止写HOLD#暂停通信低电平有效VDD、GND电源。很多人第一次接SPI MRAM会把SI和SO接反。注意MRAM这边的SI是“进”接主控的输出SO是“出”接主控的输入。差别只在数据方向但接反了读出来全是0xFF或者0x00排查起来特别恼火。PIC18F86K90属于Microchip K90系列自带段式LCD驱动板上还能用其低功耗特性。这个芯片有硬件MSSP模块可以配置成SPI主模式。我实际用的是MSSP1模块SCK1、SDO1、SDI1是固定引脚CS片选我做成普通GPIO控制。用GPIO做CS比用硬件SS的好处是软件完全可控不至于因为硬件自动片选的时序把通信搞乱。WP#和HOLD#两个脚一定要处理好。WP#低电平时写入是被禁止的如果悬空受外部干扰拉低写操作就会莫名其妙失效所以我在硬件上把它通过10k电阻上拉到3.3V同时用一根GPIO控制需要写保护时拉低正常运行时拉高。HOLD#也是一样悬空或者被拉低会让芯片“暂停”在某个状态导致后面的命令全部接收不到这个也上拉到高电平。2.2 接线表和电平问题下面是我这个板子上实际的接线关系MRAM引脚连接到说明CS#PIC18F86K90普通GPIO我用的RC2软件控制片选SCK引脚SCK1RC3SPI时钟SI引脚SDO1RC5MCU数据输出SO引脚SDI1RC4MCU数据输入WP#GPIO拉高/可控写保护控制HOLD#上拉到3.3V禁用暂停功能VDD3.3V电源GNDGND地电平方面PIC18F86K90和MR25H40CDF在工作电压上不是必须兼容同一个电压但我是整个板统一3.3V供电不存在5V/3.3V混电平的问题。如果有人在别的项目里用5V的MCU带MRAM一定别直接连老老实实加电平转换或者串阻分压MRAM不是5V容忍器件硬接很可能烧芯片。CS引脚我在电路上加了10k上拉这样MCU上电复位期间SPI引脚是高的MRAM不会因为复位期间的引脚抖动被误选中。MCU端也不需要额外电流驱动普通推挽GPIO足够。2.3 布局布线和防干扰工业场景不比实验室桌面电机、继电器、变频器都是潜在干扰源。MRAM本身是磁存储但它内部的磁隧道结是纳米级的对外部强磁场并不完全免疫。我在这块PCB上的处理办法是SPI四条线CS#、SCK、SI、SO尽量短集中在板子的同一区域不要跨越大电流走线。走线尽量在表层避免穿过多层过孔。虽然8MHz的SPI不算高频但信号完整性问题少一点是一点。MRAM不要放在继电器、变压器、大功率电感附近。继电器吸合瞬间会产生磁场变化虽然短时间拉弧不会直接把MRAM的数据擦掉但是这种干扰如果耦合到SPI线上会导致通信数据错误表现就是偶发性记录错乱。布局时我让电源部分和存储部分分开中间用地平面隔开。电源去耦不能省。VDD旁边我放了一颗0.1µF陶瓷电容一颗10µF铝电解或者钽电容放在板级电源入口。DFN8封装底部有散热焊盘虽然MRAM功耗不大但我还是把底部焊盘接了GND一是散热二是机械强度更好。样板阶段手工焊接DFN8有点痛苦脚间距小容易连锡建议直接让贴片厂用钢网刷锡膏回流焊一次过稳定得多。3. 软件驱动把SPI和MRAM“跑顺”3.1 SPI初始化和底层收发硬件接线确认之后第一步就是把PIC18F86K90的MSSP1配置成SPI主模式。我这个项目系统时钟跑到32MHzSPI分频后实际时钟8MHz远低于MR25H40CDF的40MHz上限时序余量很足。配置要点SPI模式选择Mode 0也就是CPOL0、CPHA0空闲时SCK为低在第一个边沿采样数据。MR25H40CDF对Mode 0和Mode 3都兼容但代码里统一用Mode 0避免不同工程师维护时理解偏差。#include stdint.h #define MRAM_CS_PIN LATBbits.LATB2 #define MRAM_CS_TRIS TRISBbits.TRISB2 #define MRAM_CS_LOW() (MRAM_CS_PIN 0) #define MRAM_CS_HIGH() (MRAM_CS_PIN 1) void mram_spi_init(void) { // 配置CS引脚为输出默认高电平 MRAM_CS_TRIS 0; MRAM_CS_HIGH(); // 配置SDI1为输入SDO1/SCK1为输出 TRISCbits.TRISC3 0; // SCK1 output TRISCbits.TRISC4 1; // SDI1 input TRISCbits.TRISC5 0; // SDO1 output // MSSP1 配置为主模式时钟Fosc/4Mode0 SSP1STAT 0b00000000; // SMP0, CKE0 SSP1CON1 0b00100010; // CKP0, SSPEN1, SSPM0010 (Master Fosc/4) SSP1CON3 0b00000000; }这段代码只是示意具体寄存器要以MPLAB编译时芯片头文件里的定义为准K90系列和早期PIC18的寄存器名可能略有区别。我重点想说的是初始化阶段把CS先拉高别让MRAM在MCU刚复位、时钟还没稳定时就进入正在接收命令的状态。底层字节收发我用一个函数封装uint8_t mram_spi_rw(uint8_t byte) { SSP1BUF byte; while (!PIR1bits.SSP1IF); PIR1bits.SSP1IF 0; return SSP1BUF; }这个函数的逻辑很简单把要发送的字节写进发送缓冲等MSSP中断标志置位说明一个字节收发完成然后把收到的字节读回来。MR25H40CDF读数据时主控就是不断往SPI发送任意字节通常发0x00同时从SO上把数据读回来这个函数刚好够用。3.2 MRAM命令体系与读写实现MR25H40CDF的命令集属于标准的SPI MRAM风格最常用的几条命令是命令指令码操作说明WREN0x06写使能真正的写命令开始前必须先发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器实际用得少READ0x03读数据连续读WRITE0x02写数据可连续写单条命令最多256字节RDID0x9F读器件ID用于上电自检先说读。读流程是CS拉低 → 发送0x03 → 发送3字节目标地址高位在前→ 连续读N个字节 → CS拉高。地址虽然是24位的但MR25H40CDF只有512KB实际有效地址是19位我用代码的时候把地址的高5位固定为0免得因为数据手册里“保留位”在不同批次上有不同表现造成困惑。void mram_read_bytes(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); mram_spi_rw(0x03); // READ 命令 mram_spi_rw((addr 16) 0xFF); // 地址高字节 mram_spi_rw((addr 8) 0xFF); // 地址中字节 mram_spi_rw(addr 0xFF); // 地址低字节 for (uint16_t i 0; i len; i) { buf[i] mram_spi_rw(0x00); // 读数据 } MRAM_CS_HIGH(); }写流程稍微多一点。每次写之前必须先发WREN命令而且发完WREN要把CS拉高一次芯片才会真正把“写使能”状态锁存。如果WREN之后不拉高CS紧接着就发WRITE命令很多情况下写操作不会生效。这是和SPI NOR Flash很一致的习惯我就见过有人调试半天发现读回全是旧值最后就是WREN后面少了那一次CS拉高。void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint16_t len) { // 写使能 MRAM_CS_LOW(); mram_spi_rw(0x06); // WREN MRAM_CS_HIGH(); // 写命令 MRAM_CS_LOW(); mram_spi_rw(0x02); // WRITE mram_spi_rw((addr 16) 0xFF); mram_spi_rw((addr 8) 0xFF); mram_spi_rw(addr 0xFF); for (uint16_t i 0; i len; i) { mram_spi_rw(buf[i]); } MRAM_CS_HIGH(); // 等待写周期完成 uint8_t status; do { MRAM_CS_LOW(); mram_spi_rw(0x05); // RDSR status mram_spi_rw(0x00); MRAM_CS_HIGH(); } while (status 0x01); // WIP位为1表示还在忙 }这里要说一下状态寄存器。MR25H40CDF的状态寄存器最低位是WIPWrite In Progress写操作进行时WIP为1完成后自动清0。虽然MRAM的写周期极短几十微秒级别但在工程上绝不能省掉这个轮询。尤其是连续写多页时如果不检查WIP就立刻发下一条写命令芯片可能来不及处理偶发性丢数据。连续写超过256字节时驱动层要自动拆分。因为MR25H40CDF的单条WRITE命令最多携带256字节超出部分要么拆成多条命令要么注意地址边界。我在驱动里做了一个简单判断只要len 256就先把前256字节写掉再写剩余部分这样调用方不用关心分页细节。3.3 驱动封装与中断处理底层驱动我分成三层避免业务代码直接操作SPI引脚第1层mram_spi_rw实现单个字节收发第2层mram_read_bytes/mram_write_bytes实现按地址和长度读写第3层record_logger等应用函数只在第2层的基础上做记录、索引等逻辑。这样做的直接好处是调错快。比如新板子刚焊回来怀疑焊接问题时我可以直接在串口调试里调用第1层函数发RDID命令看能不能收到预期的ID字节。如果第1层都不通根本不用去看上层的环形日志逻辑。中断处理值得单独提醒。MRAM的SPI通信是一整段CS低电平期间完成的如果某个高优先级中断正好在这个期间来而中断服务程序里又用了同一个MSSP模块或者GPIO电平被临时改变CS时序就可能被打断。我在写记录的那几行关键代码里进入SPI读写前会临时关闭高优先级中断写完再恢复。注意关中断的窗口一定要短几微秒级别可以接受拖太长就影响其他实时任务了。还有一个细节MRAM芯片上电之后默认是允许读的但写操作需要WREN。所以驱动初始化时我固定做一次WRDI也就是写禁止然后等到实际写数据时再做WREN。这个动作可以把“意外写”的概率再压一压工业产品的遗忘成本高宁可多写两行代码求个心安。4. 应用层设计环形日志、掉电防护与数据回读4.1 存储空间规划MR25H40CDF总共512KB看起来不大但用在一个记录仪里规划合理的话非常够用。我把它分成三块区域地址范围大小用途参数区0x00000 ~ 0x00FFF4KB设备配置、校准系数、序列号索引区0x01000 ~ 0x017FF2KB日志写位置、启动次数、标志字日志区0x01800 ~ 0x7FFFF约506KB循环日志主体参数区的数据是“写得少但必须可靠”的类型比如设备地址、RS485波特率、传感器校准偏移。这些我用简单键值对方式存储每次修改后立即回读校验防止写入过程中掉电导致半写状态。索引区是整个可靠性设计的关键。日志区是一个环形缓冲任何一个时刻都需要知道“下一个记录应该写到哪个地址”。这个指针如果丢了整个日志就等于一盘散沙。我把索引区做成双备份0x01000存主索引0x01100存备份索引每次更新时先写备份再写主索引。读取时如果主索引的CRC正确则使用否则回退到备份索引。4.2 环形日志和索引怎么写才不丢日志区的记录结构我定义为16字节定长这样一个环形缓冲区的计算可以简单用地址偏移搞定不需要解析变长记录。typedef struct { uint32_t timestamp; // Unix时间戳 int16_t temperature; // 温度单位0.1℃ uint16_t voltage[4]; // 4个通道电压 uint8_t flags; // 状态标志 uint16_t crc16; // CRC16校验 } LogRecord;每个记录写完之后把当前写地址往前推进一个LogRecord长度如果到达日志区末尾就绕回起点。这里有一个关键工程点绕回时不能直接覆盖日志区最前面的记录因为在环形缓冲区里写地址追到读地址之前意味着旧数据还在被上位机读取。我在代码里做了一次边界检查如果写地址即将追上读地址就先删除最老的一批记录也就是把读地址往前推。为避免掉电让“索引已更新但记录还没写完”或者反过来我采用一个脏标志位机制写一条记录时先在索引区把该位置的脏标志置1再写日志记录数据等日志写完后把脏标志清0。下次上电启动时检测到脏标志为1的位置知道这条记录可能没写完直接跳过从下一条开始读。这么做看起来有点“小题大做”但在实际工业环节里掉电永远是你无法预测的。MRAM能保证数据不丢但保证不了“SPI传了一半的字节是完整的”。加了脏标志CRC读端就有办法甄别哪些记录是真正写完的。4.3 掉电检测与完整性校验PIC18F86K90本身有BOR欠压复位功能我把BOR触发电压设置到3.0V左右当供电跌到这个值以下MCU先不执行普通任务而是快速进入保存流程。但这只能作为最后防线真正稳妥的做法是设计一路独立的电源监测电路用比较器检测3.3V是否低于阈值一旦触发MCU的INT引脚进入中断在毫秒级时间内把当前记录索引、最近一次采样值写入MRAM参数区。因为MRAM写入不需要擦除、速度快这段保存流程在中断里的执行时间很短几微秒到几十微秒就够。相比之下同样场景如果用的是EEPROM写一个字节要几毫秒掉电瞬间很可能还没来得及写完电源就已经跌出工作范围了。每条日志记录里的CRC16至少要算上时间戳和所有数据字段。读取时先算CRC不一致就跳过该记录当作“未完成写入”处理。这里我建议别用太弱的校验CRC16够用但如果你追求更稳可以上CRC32MRAM容量足够日志区每记录多占4字节一个月的日志总量完全扛得住。4.4 数据读取与上位机对接历史数据的读取路径是和记录路径分开的。RS485/Modbus协议里我增加了一个功能码上位机下发“读取第N条到第M条记录”的命令MCU从MRAM日志区读出记录按帧协议打包后发送。这里有一个性能优化点。MRAM是字节寻址的但我的记录是16字节定长如果上位机要读1000条记录那就得连续读16000字节。我的底层驱动支持一次性跨地址连续读只要给定首地址和长度循环调用mram_spi_rw就行。实测8MHz SPI下读16KB大约需要不到20毫秒加上协议打包和串口发送完全能满足实际需求。LCD显示我用了PIC18F86K90片上自带的段式LCD驱动显示的其中一个页面就是当前日志的写位置和剩余空间。这样在现场维护的时候操作员不用接上位机直接看屏幕就能知道存储有没有异常。这算是K90系列相对其他单片机的一个实用价值吧。5. 实测表现与常见问题排查5.1 写入和读取的实测数据项目做完以后我专门用逻辑分析仪抓过几个关键操作的时序。以8MHz SPI时钟为例操作实测耗时说明读256字节约0.35ms包括命令地址数据以及少量GPIO开销写1字节约20µs包括WREN、WRITE命令和WIP轮询写256字节约0.4ms包括WREN、地址、数据、WIP轮询上电初始化索引恢复小于1ms主要是读取索引区并做CRC校验这个速度对每秒1条的记录需求绰绰有余。即使把记录频率提高到每秒100条MRAM的写入能力也远远够用。我们曾经专门做过一次压力测试让设备连续运行10天每秒写1条跑下来总写入量约864万条记录再回读全部数据逐条CRC校验全部通过。写寿命方面再做一次简单估算。MRAM官方寿命按10的12次方次算每秒写1条一年约3153万次单一物理地址连续写一年也才用掉寿命的几万分之一。就算日志循环把压力平均到整个512KB区域寿命压力也几乎可以忽略。这也是我最终放心大胆用环形日志直接覆盖写的重要原因。5.2 常见问题速查表调试期间我先后遇到几类问题整理成一个表格方便以后直接对照现象检查点解决办法上电读回全是0xFF电平、接线方向、SPI模式先确认WP#和HOLD#都是高电平再用RDID命令看ID是否能读到写入后读回还是旧值WREN时序、CS时序WREN之后必须把CS拉高一次再发WRITE命令偶发某条记录CRC失败数据线干扰、CS毛刺、中断打断加长CS上电稳定时间SPI期间屏蔽高优先级中断连续写超过256字节时出错跨页地址处理驱动层把写操作按256字节拆分掉电后索引恢复错乱双备份未做或校验薄弱索引区做双份更新顺序为先备份再主索引读出来的ID和预期不符接线、芯片焊接先查SI/SO有没有接反再查DFN8有没有连锡5.3 调试心得新板子焊接后我习惯先把MRAM电源和地测一遍确认没有短路再上电。上电后第一个调试动作是发RDID而不是急着读数据。RDID如果正常说明SPI物理链路、寄存器配置、芯片供电都基本没问题。如果RDID都读不到后面的所有读写都不用看了先把硬件问题解决。逻辑分析仪在这种调试里是必需品。把CS#、SCK、SI、SO四根线挂上触发条件设为CS下降沿抓一次写命令过程。我遇到过最诡异的一个问题是从波形看命令和地址都对但数据就是写不进去。后来发现WREN命令发送完成后CS拉高的时间太短MCU代码里连续两条语句之间没有加延时CS高电平只维持了一个时钟周期芯片没有把写使能状态锁存成功。解决办法很简单在WREN和WRITE之间加一条空操作延时或者干脆重新组织代码让CS保持高电平至少几百纳秒。还有一个经验是别过度依赖调试器的在线下载功能。工业现场经常有强干扰下载器的接地和现场设备的地不一定等电位有时候调试器一连接SPI就莫名奇妙出错。我后面测试都改成脱机运行通过串口打印关键变量反而问题少很多。最后再说几句这个项目做下来我最大的体会不是MRAM的写入速度有多快也不是寿命有多长而是它把整个存储系统的逻辑复杂度降下来了。以前用NOR Flash做日志要管理扇区、管理磨损均衡、管理掉电恢复代码量大不说还很难测试到所有边界情况。用了MRAM以后我脑子里想的就只是“往这个地址写一段数据掉电也不怕”上层设计一下子就清爽了。如果以后再让我做类似的数据记录产品我会继续沿用“MCU SPI MRAM”这个组合。当然MRAM的价格比同容量的NOR Flash贵这个在选型时必须算进BOM成本里但当产品对写入频率、掉电可靠性、研发周期都有要求时价格差换来的稳定性和开发效率是值得的。真要在低成本大容量场景里和NOR Flash抢地盘MRAM确实不是最佳选择可在“写得多、不能丢、要简单”这个特定赛道里MRAM的体验是碾压级的。
返回列表