☰
PIC18F46K80与MR25H40CDF:SPI MRAM工业掉电保存方案
2026/10/4 6:58:36 网站建设 项目流程

MR25H40CDF这颗SPI MRAM,我是从一次客户现场掉电丢数据的事故里真正认识它的。当时用的还是外部串行EEPROM加PIC18F46K80的组合,设备在电柜里被频繁断电,日志区经常莫名其妙写穿或者丢记录,售后换回来一堆板子,分析到最后就是存储方案的问题。后来我把关键数据全部搬到MR25H40CDF上,再用PIC18F46K80的MSSP外设读写,跑了快两年,再没出现掉电丢数据的情况。

这篇文章就把这套“PIC18F46K80 + MR25H40CDF”组合从硬件连接到固件实现完整拆一遍。适合正在做现场仪表、工业控制器、数据记录仪或者需要高频掉电保存的嵌入式工程师参考,内容属于可以直接抄作业的那种。

1. 先搞清楚这个组合到底解决什么问题

1.1 工业设备最头疼的三类存储问题

做工业嵌入式的人应该都有同感,存储选型是个看起来很基础、但特别容易翻车的环节。早期方案常用EEPROM,优点是字节可写、掉电保存,但写寿命普遍是十万到百万次级别,这对“每次运行都记录一条状态”的现场设备来说并不宽裕。更麻烦的是,EEPROM容量普遍偏小,动不动就几Kbit,记录点一多就得频繁搬地址,代码逻辑绕来绕去。

后来很多人换Flash,容量倒是够大,但Flash有先擦后写的硬性要求,页擦除时间动不动几毫秒甚至几十毫秒。掉电瞬间你想快速保存现场数据,来不及等它擦除就是雪上加霜。而且Flash如果没做磨损均衡,反复写同一块区域,寿命一样难堪。

还有人用FRAM,随机访问和无限次写入确实香,但性价比不高,大容量选型少,很多人用下来发现供货渠道也折腾。真正适合工业现场大日志、掉电快速保存、长期反复写入的场景,MRAM其实是更合适的答案。MRAM是非易失存储,写入速度接近SRAM,又不依赖电荷保存,理论上写耐久度远超EEPROM和Flash,而且写入前不用擦除,这一条就解决了掉电保存里的很多麻烦事。

1.2 MR25H40CDF为什么适合和PIC18F46K80搭

MR25H40CDF是Everspin的4Mbit串行MRAM,换算过来是512KB字节空间,SPI接口,单芯片解决容量、速度和耐久性三个问题。对8位MCU的项目来说,512KB已经是非常宽裕的空间,可以做比较大的循环日志,也可以直接划分成参数区和掉电保护区。

PIC18F46K80这个片子本身也适合工业场景。它带CAN模块、带硬件MSSP(SPI/I2C)、Flash和RAM空间在8位机里都属于偏大的。更关键的是它在3.3V下能稳定工作,能跟MR25H40CDF共享同一个电源轨,不用额外折腾电平转换。MSSP外设做SPI主机是纯硬件移位,配好寄存器后读写MRAM的效率非常高,不像IO模拟SPI那样占用中断和CPU时间。

这套组合最舒服的一点是:PIC18F46K80作为主控负责采集、运算、通信,MR25H40CDF作为外部非易失存储专门扛数据。MRAM写一个字节不需要擦除整个页,也没有“写后等待忙”的状态轮询,掉电瞬间能尽可能多地把关键数据塞进去,这点在实际产品里价值极高。

2. 硬件连接与电路设计(照着焊就行)

2.1 引脚定义与最小接线

MR25H40CDF是8脚DFN封装,引脚不多,连线非常直接。我用的板子上PIC18F46K80也是QFN封装,SPI引脚刚好分布在RC3、RC4、RC5上。注意不同封装和引脚的复用不一定完全一致,动手前一定打开对应型号的数据手册查引脚功能表。

MR25H40CDF引脚功能接到PIC18F46K80
/CS片选普通GPIO,我用的是RB0
SCKSPI时钟RC3(MSSP时钟)
SIMOSIRC5(MSSP数据出)
SOMISORC4(MSSP数据入)
/WP写保护通过10k电阻上拉到VDD
/HOLD暂停通信通过10k电阻上拉到VDD
VDD电源3.3V
GND地电源地

这里要特别说一下,/WP和/HOLD千万别悬空。我之前见过有人图省事直接不接,结果设备在振动环境下偶尔出现写入失效,查了半天就是/HOLD脚受到干扰被拉低,MRAM直接忽略SCK上的时序,数据自然写不进去。对、所以要加一个10k上拉电阻,逻辑上强制拉高。

2.2 三个容易被忽略的电路细节

第一个是去耦电容。MR25H40CDF在写入翻转内部磁隧穿结时会有瞬态电流,电源上必须放一个100nF陶瓷电容,尽量靠近VDD引脚。我习惯再并一个4.7uF钽电容或X7R电容,覆盖低频能量需求。千万不要把去耦电容放得老远,那样瞬态电流会在走线电感上产生压降,严重时会导致写入数据错误。

第二个是SPI信号线处理。MCU到MRAM之间的距离如果超过5cm,建议在SCK、SI线上各串一个22欧姆到47欧姆的电阻,能明显减小振铃。CS线可以稍微缓一点,但也要保持干净。现场设备里如果有继电器、接触器,SPI线缆尽量跟功率线缆分开走,实在分不开就得考虑屏蔽处理。

第三个是电源掉电检测。MRAM本身掉电不丢数据,但MCU的保存程序需要提前感知到“电源要没了”,否则在电压已经掉到MCU无法工作的时候才去写MRAM,什么存储介质都救不了。PIC18F46K80内部带模拟比较器,可以直接用比较器监测主电源分压;或者用一颗电源监控芯片,输出接到PIC的外部中断引脚。掉电检测触发后,MCU应该在电压跌到复位阈值之前完成最关键数据的写入。

2.3 电平和共地问题

MR25H40CDF工作电压是3.3V,PIC18F46K80可以跑3.3V,这个组合最省事的就是两边同电压,不用加电平转换芯片。如果你的系统里有5V的外设,注意别把5V直接灌到MRAM的引脚上,MRAM不是5V容忍类型。整个系统要保证单点共地,SPI信号和电源地之间不能存在大的地电位差,否则高速SPI在长线上非常容易错位。

3. SPI协议和寄存器:别把MRAM当普通Flash用

3.1 指令集速览

MR25H40CDF的SPI协议和普通SPI NOR Flash长得有点像,但有几个关键区别。它也支持WREN、读状态寄存器这些指令,可最核心的一点是:写入前真的不用擦除。Flash必须先把目标扇区擦成全F,MRAM没有这个限制,直接对任意地址覆盖写就行。

我用到的几条指令放在下面:

指令操作码说明
READ0x03连续读,从指定地址开始
FSTRD0x0B快速读,带dummy字节
WRITE0x02连续写,从指定地址开始
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
RDID0x9F读ID
SLEEP0xB9进入睡眠(低功耗)
WAKE0xAB唤醒

地址是19位,因为512KB需要覆盖0x00000到0x7FFFF。发送地址时按照三字节地址帧来,除非你确定只访问低地址段,否则不要自己精简成两字节,容易埋雷。

3.2 时序关键点

MR25H40CDF支持SPI Mode 0和Mode 3,我习惯用Mode 0(CPOL=0,CPHA=0)。SCK空闲为低,数据在SCK上升沿被锁存。对PIC18F46K80的MSSP来说,配置成Master模式后注意把CKP和CKE设置成匹配Mode 0的组合。实际调试时如果发现读出来全是乱码或者立刻丢第一个字节,先用逻辑分析仪抓一下SCK和SI的相位,别盲目改时钟极性。

所有写命令和数据都是在CS拉高的那个时刻生效。CS低电平期间,MRAM认为主机还在传输过程;CS一旦拉高,当前传输结束并锁存数据。所以代码里CS的拉高动作非常关键,别在一次读出过程中随便抖CS,否则读地址可能被截断。

快速读FSTRD比普通READ多一个dummy字节,适合高速连续读。如果SPI时钟不高,普通READ完全够用。我做日志读取时用了普通READ,简单可靠,硬件也不需要额外花时间适应dummy周期。

3.3 状态寄存器和写保护

MR25H40CDF的状态寄存器里包含区块保护位,上电默认是0x00,也就是全片可写。但要注意,如果代码里不小心执行了WRSR,并且把保护位置1了,后续所有WRITE指令都会被MRAM忽略,读出来的数据却一切正常。这种问题特别隐蔽,因为运行时根本不会报错。

建议每次系统初始化的时候,先用RDSR读一下状态寄存器,确认值是0x00。如果发现不是0x00,主动通过WRSR恢复,或者检查外部/WP引脚是否被拉低。因为/WP引脚本身也有硬件写保护功能,它对应状态寄存器里的WPEN位,两者叠加才生效,这也是为什么我上面强调/WP一定要上拉。

3.4 睡眠唤醒的处理

如果整机有低功耗需求,可以把MR25H40CDF放进睡眠模式。SLEEP指令是0xB9,执行后电流非常低,但芯片会忽略普通的读写指令,需要先发WAKE(0xAB)唤醒。这里有个坑:如果程序在睡眠状态下强行发READ,读出来的全是0xFF,非常容易被误判成“芯片坏了”。我一般只在待机模式需要压到微安级别时才用睡眠,系统唤醒流程里固定先发WAKE再发RDSR确认,避免状态错乱。

4. PIC18F46K80侧的固件实现:从底层读写到日志方案

4.1 MSSP初始化

PIC18F46K80的MSSP外设做SPI主机非常方便,用MPLAB X + XC8编译器。我建议直接用MPLAB Code Configurator生成初始化代码,然后手动改分频系数。下面是我实际用过的配置参数:

  • 工作模式:Master模式
  • SPI Mode 0(CKP=0,CKE对应Mode 0)
  • 8位数据宽度
  • SCK时钟不高于40MHz,实际跑20MHz留余量
  • 片选引脚作为普通GPIO手动拉低控制

SPI时钟分频必须结合PIC18F46K80的工作频率来算。我系统主频是64MHz(16MHz晶振+4倍PLL),MSSP分频后SCK大概在16MHz或20MHz左右,完全满足MR25H40CDF的需求,也降低了信号完整性问题。不要上来就把SCK拉到极限,稳定比好看重要。

底层字节收发可以封装成一个函数:

uint8_t mram_xfer(uint8_t out_byte) { SSPBUF = out_byte; // 写数据,启动传输 while(!PIR1bits.SSPIF); // 等待移位完成 PIR1bits.SSPIF = 0; // 清中断标志 return SSPBUF; // 返回收到的字节 }

这个函数是所有后续读写的基础。注意PIC的MSSP里,SPI移位完成标志需要软件清零,别漏。

4.2 底层读写函数

MR25H40CDF的WRITE指令格式是:操作码,三个地址字节,然后是一个或多个数据字节。写单个字节的时候,需要先拉低CS,发WREN(0x06),再把CS拉高完成写使能锁存,然后再拉低CS发WRITE指令。这个步骤不能省,和Flash的写使能逻辑类似。

void mram_write_byte(uint32_t addr, uint8_t data) { CS_LO(); // 拉低片选 mram_xfer(0x06); // WREN CS_HI(); // 写使能锁存 CS_LO(); // 开启一次写事务 mram_xfer(0x02); // WRITE mram_xfer((addr >> 16) & 0xFF); mram_xfer((addr >> 8) & 0xFF); mram_xfer(addr & 0xFF); mram_xfer(data); CS_HI(); // 事务结束,数据锁存 }

读函数更加简单,不需要WREN,直接拉低CS、发READ指令和地址,然后连续收字节:

uint8_t mram_read_byte(uint32_t addr) { uint8_t data; CS_LO(); mram_xfer(0x03); // READ mram_xfer((addr >> 16) & 0xFF); mram_xfer((addr >> 8) & 0xFF); mram_xfer(addr & 0xFF); data = mram_xfer(0x00); // 发空字节,接收数据 CS_HI(); return data; }

MRAM写入后不需要轮询BUSY位,这是它和Flash体验上的最大区别。我从CS拉高后加了一个2us左右的延时,纯粹是为了让MCU自身的GPIO输出稳定,不是MRAM要求。如果你需要连续写入一大块数据,可以在一个WRITE事务里连续发送数据,不需要每个字节都做一次WREN和CS翻转。

4.3 掉电快速保存的完整设计

实际项目里,掉电保存不是简简单单写一个字节就完事。我用的方法是“数据先落位、校验后落标志”,MRAM不需要擦除,所以完全能支撑这种策略。

假设要保存一组生产参数和生产计数器,结构体定义成这样:

typedef struct { uint32_t magic; // 固定标志,例如0xA5A5_1234 uint16_t batch_no; uint32_t total_count; uint16_t voltage_mv; uint16_t temperature_c; uint16_t crc16; // 对前面字段的CRC校验 } save_frame_t;

保存流程是:先把除了magic和crc16之外的字段写入MRAM地址区,再计算CRC,写入crc16字段,最后写入magic字段。读取时先看magic对不对,不对就说明数据不完整;magic对再看CRC,CRC不对说明数据被破坏或者写入中途掉电。这套流程比“一次把整个结构体写进去”稳得多,因为MRAM虽然写得快,但掉电瞬间如果CS电平抖动或者时钟中断,后写入的字节可能覆盖了一部分旧数据,只有带标志和校验才能识别出来。

掉电检测中断触发后,PIC18F46K80先把当前采集到的最终数据放到临时变量,然后调用上面的保存函数。整个过程大概几百微秒。如果掉电检测太晚,导致MCU电压已经低于工作电压,哪怕MRAM再快也白搭。所以硬件上掉电检测电路要提前动作,建议在电源跌落到正常值90%左右就触发中断。

4.4 循环日志区的实现

现场设备记录历史数据时,我更推荐循环日志方式。把512KB空间分成日志区和关键参数区。日志区从0x00000开始,每条记录16字节,一直写到0x7F000附近,写满后从头覆盖。关键参数区固定放在0x7F800之后,保存出厂标定和运行参数。

每条日志记录可以这样定义:

typedef struct { uint32_t timestamp; uint16_t event_id; uint16_t value1; uint32_t value2; uint16_t crc; } log_entry_t; // 正好16字节

写入时按当前写指针连续写,写完后把写指针加上16字节。由于MRAM不需要考虑擦除寿命,这种最简单的“覆盖式日志”就够了,不需要像Flash那样搞复杂的磨损均衡。读取时从头扫描,先看每条记录的CRC,CRC合法就算有效记录。如果碰到连续多条CRC错误,大概率是掉电写了一半的记录,直接跳过即可。

这里有个细节:日志写指针本身也需要掉电保存。我把它放在关键参数区的一个独立地址,每次更新日志后顺手更新写指针。为了防止写指针本身损坏,可以同时存两份,读写时取校验通过的那份。

5. 现场调试与问题排查实录(真实踩坑)

5.1 首字节全0xFF

有一次新板子上电后,PIC18F46K80读MRAM的ID,读出来的永远是0xFF。用逻辑分析仪看波形,SCK有脉冲,CS方向也对,但SO上就是没有数据回出来。

后来发现是CS拉低之后我立刻发了第一个字节,没有给MRAM留出从片选有效到接收命令的建立时间。MRAM在CS下降沿之后需要一点点时间进入工作状态,这个时间在数据手册上的tCS参数有标注。解决办法特别简单,CS拉低后加一个1us的延时再发第一个字节。如果是高速模式下40MHz时钟,1us已经算很充裕了,对整体吞吐影响不大。

5.2 写进去但读出来是旧数据

这个症状比读0xFF更坑。程序明明把数据写进去了,再读出来还是原来那个数。一开始我以为MRAM坏了,后来排查发现是/WP引脚没上拉,芯片在某种状态下把写保护使能了。由于状态寄存器里的保护位本身是0x00,按道理全片可写,外部/WP引脚悬空导致它被干扰拉低,再加上WPEN位默认状态配合,MRAM就把所有WRITE指令都忽略了。

从那以后我的所有MRAM设计都把/WP和/HOLD通过10k电阻上拉到VDD,并且在初始化时读一次状态寄存器做断言。如果状态寄存器不为0x00,还要主动发WRSR恢复,防止之前有代码误设。

5.3 偶发的数据错位

还有一次是设备在继电器吸合的瞬间,日志里偶尔出现一两条错数据。查到最后不是MRAM的问题,而是SPI时钟线受到继电器线圈放电的干扰,SCK上出现了毛刺,导致MRAM多采了一个时钟周期,数据位全部错位。

处理办法有三步:第一,SPI时钟从20MHz降到8MHz;第二,SCK和SI串33欧电阻;第三,把MRAM和SPI走线尽量远离继电器和功率驱动部分。降速之后问题彻底消失。对于现场存在强干扰的设备,SCK速度真的没有必要追求极限,8MHz已经能在一秒钟内写几百KB数据,大多数日志场景完全够用。

5.4 常见问题速查表

现象可能原因处理办法
读出来全0xFFCS建立时间不足CS拉低后延时1us再发指令
读出来全0xFF芯片处于睡眠模式发WAKE(0xAB)唤醒
写入无效,读回旧数据/WP悬空被拉低10k电阻上拉/WP
写入无效,读回旧数据状态寄存器保护位被置1初始化RDSR检查并恢复
数据偶发错位SCK被干扰降SPI时钟、串电阻、布线远离干扰源
第一个字节不对SPI模式不匹配抓波形确认CKP/CKE组合
地址超过0x7FFFF512KB边界越界地址掩码后发三字节
CS频繁抖动程序在中断里碰GPIO片选状态只在主流程里修改

6. 最后交代几句大实话

这套方案我已经用在两个产品上了,一个现场采集器,一个小型控制器。如果让我给建议,第一个建议是别把MRAM当普通RAM用,它再快也是SPI外设,频繁单字节写会消耗大量CPU时间,能攒批就批量写。第二个建议是掉电检测电路一定要做,MRAM只保证“数据存进去不会丢”,不保证“你来得及存”,这两个概念差了十万八千里。第三个建议是先把SPI波形用逻辑分析仪抓干净再调业务代码,MRAM本身逻辑不复杂,大多数问题都出在主控配置和硬件干扰上,排查顺序别搞反。

MR25H40CDF和PIC18F46K80这套组合,我现在给新项目做选型时依然会优先考虑。写代码时最爽的瞬间就是掉电保存函数里不用写擦除等待、不用看状态寄存器忙位,直接写完拉高CS就完事。工业现场这些东西,稳定和简单往往比花哨更重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询