☰
MRAM工业嵌入式存储方案:MR25H40CDF与PIC18LF46K22实战
2026/10/5 1:37:56 网站建设 项目流程

1. 为什么MRAM在工业嵌入式场景里越来越受关注

搞过工业数据采集或者电力终端的朋友应该都有体会,选存储芯片这件事,很多时候比选主控还让人头疼。EEPROM写入慢、擦写寿命有限,Flash虽然容量大但写入前必须擦除整个扇区,而且擦写次数撑死十万次,放在高频记录数据的场景里,几个月就能把某个扇区写废。铁电存储器FRAM倒是不错,读写快、寿命长,但容量普遍偏小,价格也不便宜。这几年MRAM(磁性随机存储器)逐渐进入大家的视野,它兼具SRAM的读写速度和Flash的非易失特性,擦写寿命理论上无限,而且抗辐射、耐高低温,非常适合工业现场那种电磁环境复杂、温度跨度大的场合。

我这次要聊的组合是MR25H40CDF这颗4Mbit的SPI MRAM芯片,搭配PIC18LF46K22这颗经典的8位MCU,做一套完整的数据存储与读取方案。MR25H40CDF是Everspin家的产品,4Mbit容量换算过来是512KB,按字节寻址,SPI接口,最高时钟可以跑到40MHz。PIC18LF46K22是Microchip的8位机,自带MSSP模块支持SPI主模式,64KB Flash、3896字节RAM,低功耗版本工作电压1.8V到3.6V,工业级温度范围-40到+85度。这两颗芯片搭配在一起,成本可控,可靠性高,代码量也不大,很适合做工业仪表、数据记录仪、PLC扩展模块这类产品。

这篇文章我会从方案选型、硬件设计、SPI驱动编写、读写时序、数据完整性保护、常见问题排查几个维度,把整套方案讲透。不管你是刚接触嵌入式存储的新手,还是想从EEPROM迁移到MRAM的老手,应该都能从里面找到可以直接用的东西。代码部分我会给出基于XC8编译器的完整驱动,硬件部分会讲清楚去耦电容怎么放、片选怎么处理、PCB走线要注意什么。

2. 方案整体设计与选型思路拆解

2.1 为什么选MRAM而不是EEPROM或Flash

先把这个事情说清楚,不然后面很多设计决策没法理解。工业场景下存储数据有几个硬性需求:写入要快、寿命要长、掉电不能丢数据、温度范围要宽。我们逐条对比一下三种方案。

特性EEPROMNOR FlashMRAM
写入速度5-10ms/字节需要先擦除扇区,擦除时间百毫秒级无延迟,SPI时钟直接写入
擦写寿命100万次10万次无限次
写入功耗较高擦除时功耗高极低
数据保持100年20年20年以上
工作温度-40~85-40~85-40~125
接口I2C/SPISPISPI
容量小(KB级)大(MB级)中(KB~MB级)

从表里能看出来,MRAM最大的优势就是写入不需要等待,也不需要擦除。你可以像写RAM一样直接往里面写数据,写完就生效,掉电就保存。这对于需要频繁记录数据的场景来说,简直是救命稻草。比如一个电力监测终端,每秒钟要记录一次电压电流值,一天就是86400条记录,用EEPROM的话,就算每次只写4个字节,一年下来也接近擦写寿命上限了。用MRAM就完全不用担心这个问题。

MR25H40CDF的具体参数我列一下:容量4Mbit(512K x 8),SPI模式0和模式3都支持,最高时钟40MHz,工作电压2.7V到3.6V,封装有8脚SOIC和8脚DFN,工业级温度-40到+85度,还有扩展级-40到+105度可选。页写入没有页边界限制,可以连续写整颗芯片。这点比Flash强太多了,Flash跨页写会回卷,一不小心就覆盖了前面的数据。

2.2 PIC18LF46K22的MSSP模块配置要点

PIC18LF46K22的SPI功能由MSSP模块提供,这个模块可以配置成SPI主模式或从模式。我们这里用主模式,时钟由MCU产生。配置的时候有几个关键寄存器需要设置。

SSPCON1寄存器控制SPI的使能、时钟极性、时钟相位。SSPCON2控制片选、时钟沿等高级功能。SSPSTAT反映当前状态。SSPADD在SPI主模式下用来设置时钟分频。PIC18LF46K22的SPI时钟来源于Fosc/4,然后通过SSPADD进行分频。假设系统时钟是16MHz,Fosc/4就是4MHz,如果SSPADD设为0,SPI时钟就是4MHz。如果要跑更高速度,可以调整系统时钟或者SSPADD值。

这里有个细节要注意:PIC18LF46K22的SPI模块在发送数据的时候,实际上是同时接收的。也就是说,你往SSPBUF写入一个字节,硬件会把这个字节移出去,同时把MISO线上的数据移进来。所以读和写是同一个操作,只是你关不关心接收到的数据而已。这个特性在写驱动的时候要利用好,可以省掉很多不必要的等待。

2.3 硬件连接方案与去耦设计

硬件连接其实很简单,四根线:SCK、MOSI、MISO、CS。MR25H40CDF的引脚定义是这样的:1脚是CS,2脚是SO(MISO),3脚是WP(写保护),4脚是VSS,5脚是SI(MOSI),6脚是SCK,7脚是HOLD,8脚是VCC。

WP和HOLD这两个脚如果不用的话,直接拉到VCC就行。WP是硬件写保护,拉低的时候芯片拒绝写入。HOLD是暂停通信,拉低的时候SPI总线暂停,SCK和SI被忽略。一般应用里这两个脚都拉高,让芯片处于正常工作状态。

去耦电容这块我要多说两句。MRAM芯片在写入的瞬间电流会有波动,虽然平均功耗很低,但瞬态电流如果供电跟不上,可能导致写入失败或者数据错误。我的做法是在VCC和GND之间放一个0.1uF的陶瓷电容,紧贴芯片引脚,再并一个1uF的电容做低频滤波。这两个电容的走线要尽量短,最好直接打在焊盘上。PCB布局的时候,MRAM芯片尽量靠近MCU,SPI走线不要超过10厘米,如果实在要拉长,中间加个缓冲器或者降低SPI时钟频率。

CS片选信号我建议用MCU的普通GPIO来控制,不要用MSSP模块的硬件CS。原因很简单,硬件CS的时序是固定的,在某些情况下可能不满足MRAM芯片的建立时间和保持时间要求。用GPIO手动控制,可以在发送数据前提前拉低CS,发送完再拉高,时序完全可控。PIC18LF46K22的任何一个IO口都可以做CS,我一般选RC2或者RA5这种带中断功能的引脚,方便以后扩展。

3. SPI驱动编写与底层读写实现

3.1 SPI初始化代码与时钟计算

先上代码,这是基于XC8编译器的SPI初始化函数。假设系统时钟配置为16MHz,SPI时钟目标为4MHz。

#include <xc.h> // 定义CS引脚 #define MRAM_CS_LAT LATC2 #define MRAM_CS_TRIS TRISC2 void SPI_Init(void) { // 配置CS引脚为输出,默认拉高 MRAM_CS_TRIS = 0; MRAM_CS_LAT = 1; // 配置SCK和MOSI为输出,MISO为输入 TRISC5 = 0; // SCK TRISC4 = 0; // MOSI (SDO) TRISC3 = 1; // MISO (SDI) // 关闭模拟功能 ANSELC = 0x00; // SSPCON1配置:SPI主模式,时钟极性低,时钟相位中间采样 // SSPEN=1使能,CKP=0空闲低,SSPM=0000主模式Fosc/4 SSPCON1 = 0x00; SSPCON1bits.SSPM = 0b0000; // SPI主模式,时钟=Fosc/4 SSPCON1bits.CKP = 0; // 空闲时SCK为低 SSPCON1bits.SSPEN = 1; // 使能SPI // SSPSTAT配置:数据在时钟上升沿发送,下降沿采样 SSPSTATbits.CKE = 0; // 数据在SCK上升沿发送 SSPSTATbits.SMP = 0; // 中间采样 // 时钟分频,SSPADD=0时SPI时钟=Fosc/4=4MHz SSPADD = 0; }

这里解释一下时钟极性和相位。MR25H40CDF支持SPI模式0(CPOL=0,CPHA=0)和模式3(CPOL=1,CPHA=1)。我上面配置的是模式0,SCK空闲为低,数据在上升沿发送,下降沿采样。这个模式最常用,和大多数SPI设备兼容。如果你要和其他设备共用SPI总线,记得确认所有设备的模式一致,不然通信会出错。

SSPADD的计算公式是:SPI时钟 = Fosc / (4 * (SSPADD + 1))。当SSPADD=0时,SPI时钟就是Fosc/4。如果系统时钟是16MHz,SPI时钟就是4MHz。MR25H40CDF最高支持40MHz,所以4MHz完全在安全范围内。如果你需要更快的速度,可以把系统时钟提到64MHz,SPI时钟就能到16MHz。不过速度越快,对PCB走线和信号完整性的要求就越高,这个后面会讲。

3.2 单字节读写函数与状态机

SPI的读写函数是驱动的基础,所有上层操作都靠它。PIC18的SPI模块发送和接收是同时进行的,所以一个函数就能完成读写。

unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; // 写入数据,启动发送 while(!SSPSTATbits.BF); // 等待发送完成,BF位为1表示接收缓冲满 return SSPBUF; // 返回接收到的数据 }

这个函数看起来简单,但里面有个坑。SSPSTATbits.BF位在发送完成后会置1,表示SSPBUF里有接收到的数据。如果你在调用这个函数之前没有清空SSPBUF,可能会读到旧数据。所以每次调用前最好确保BF位是0。另外,在高速SPI下,while循环等待的时间很短,如果编译器优化级别太高,可能会把循环优化掉,建议加个volatile或者用NOP填充。

MRAM的读写操作都是基于命令的。每个操作先发一个命令字节,然后发地址,然后读写数据。MR25H40CDF的命令集如下:

命令编码功能
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

写操作之前必须先发WREN命令,这是MRAM的写保护机制。如果你忘了发WREN,写操作会被忽略,数据不会写入。这个坑我踩过,当时调了半天以为硬件有问题,后来发现是漏了WREN。

3.3 页写入与连续读取的实现细节

MR25H40CDF的写入没有页边界限制,你可以从任意地址开始连续写,地址会自动递增,写到芯片末尾会自动回卷到0。这个特性比Flash好用太多,Flash跨页写会回卷到页首,一不小心就把前面的数据覆盖了。

写数据的函数实现:

void MRAM_WriteData(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_LAT = 0; // 拉低CS,开始通信 SPI_ExchangeByte(0x06); // 发送WREN命令 MRAM_CS_LAT = 1; // 拉高CS,WREN命令需要独立帧 MRAM_CS_LAT = 0; // 再次拉低CS,开始写操作 SPI_ExchangeByte(0x02); // 发送WRITE命令 SPI_ExchangeByte((addr >> 16) & 0xFF); // 发送地址高8位 SPI_ExchangeByte((addr >> 8) & 0xFF); // 发送地址中8位 SPI_ExchangeByte(addr & 0xFF); // 发送地址低8位 for(i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); // 连续写入数据 } MRAM_CS_LAT = 1; // 拉高CS,写入完成 }

注意WREN命令需要独立的CS帧,也就是说发完WREN之后要拉高CS再拉低,才能发WRITE命令。这个时序在数据手册里有明确要求,如果WREN和WRITE在同一个CS帧里,写操作不会执行。我见过有人把WREN和WRITE放在一起,结果怎么都写不进去,查了半天才发现是这个问题。

读数据的函数更简单,不需要WREN:

void MRAM_ReadData(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_LAT = 0; // 拉低CS SPI_ExchangeByte(0x03); // 发送READ命令 SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for(i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0xFF); // 发送哑元数据,读取返回数据 } MRAM_CS_LAT = 1; // 拉高CS }

读操作的时候,MOSI线上发什么数据无所谓,芯片会忽略,但必须发东西来产生时钟。我一般发0xFF,这样示波器上看波形比较整齐。

4. 数据完整性保护与掉电处理策略

4.1 状态寄存器与写保护机制

MR25H40CDF内部有一个状态寄存器,里面有几个关键位:WEL(写使能锁存)、BP0和BP1(块保护)、SRWD(状态寄存器写保护)。WEL位在每次WREN命令后置1,写操作完成后自动清零。你可以通过读状态寄存器来确认写操作是否真的执行了。

unsigned char MRAM_ReadStatus(void) { unsigned char status; MRAM_CS_LAT = 0; SPI_ExchangeByte(0x05); // RDSR命令 status = SPI_ExchangeByte(0xFF); // 读取状态寄存器 MRAM_CS_LAT = 1; return status; }

写完数据后,我习惯读一下状态寄存器,确认WEL位已经清零,说明写操作被芯片接受了。如果WEL还是1,说明写操作没有执行,可能是CS时序有问题或者芯片没响应。

BP0和BP1用来设置块保护,可以把芯片的一部分区域设为只读。这个功能在工业场景里很有用,比如你把校准参数存在某个区域,设成只读,防止程序跑飞的时候误写。SRWD位和WP引脚配合,可以实现硬件写保护。WP拉低且SRWD置1的时候,状态寄存器本身也被保护,无法修改。

4.2 数据校验与冗余存储方案

MRAM虽然可靠性很高,但在工业现场,电磁干扰、电源波动、宇宙射线(别笑,高空设备真的要考虑这个)都可能导致数据位翻转。我一般会做两层保护:CRC校验和冗余存储。

CRC校验很简单,每写一条记录,后面跟两个字节的CRC16。读的时候重新计算CRC,和存储的值对比,不一致就说明数据出错了。CRC16的实现网上很多,我一般用查表法,速度快,占用空间也不大。

unsigned int CRC16_Calculate(unsigned char *data, unsigned int len) { unsigned int crc = 0xFFFF; unsigned int i, j; for(i = 0; i < len; i++) { crc ^= data[i]; for(j = 0; j < 8; j++) { if(crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; }

冗余存储就是同一条数据存两份,放在芯片的不同区域。读的时候两份都读出来,对比一下,如果一致就用,不一致就说明有一份出错了,可以用另一份恢复。这个方案会浪费一半的存储空间,但对于关键数据来说,可靠性比容量更重要。512KB的芯片,拿256KB做冗余存储,还能存不少数据。

4.3 掉电检测与写入原子性保证

掉电是工业现场最常见的问题。如果正在写数据的时候突然掉电,可能会写到一半,导致数据不完整。MRAM的写入速度很快,一个字节的写入时间在微秒级,但如果你一次写几百个字节,中间掉电的概率还是有的。

我的做法是在数据区前面加一个写入标志。写数据之前,先把标志置为0xAA,表示正在写入。写完之后,把标志改为0x55,表示写入完成。读数据的时候先检查标志,如果是0x55,说明数据完整;如果是0xAA,说明上次写入被中断了,数据可能不完整,需要丢弃或者从备份恢复。

#define WRITE_FLAG_ADDR 0x000000 #define DATA_START_ADDR 0x000010 void SafeWriteData(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned char flag = 0xAA; // 先写标志,表示正在写入 MRAM_WriteData(WRITE_FLAG_ADDR, &flag, 1); // 写实际数据 MRAM_WriteData(addr, buf, len); // 写完成标志 flag = 0x55; MRAM_WriteData(WRITE_FLAG_ADDR, &flag, 1); } unsigned char SafeReadData(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned char flag; // 读标志 MRAM_ReadData(WRITE_FLAG_ADDR, &flag, 1); if(flag != 0x55) { return 0; // 数据不完整 } // 读实际数据 MRAM_ReadData(addr, buf, len); return 1; // 数据完整 }

这个方案简单有效,代价是多了一次标志写入。MRAM写入速度快,这点开销可以忽略不计。如果你对可靠性要求更高,还可以加一个备份区,每次写入前先把旧数据备份到备份区,写入失败可以从备份区恢复。

5. 实操调试与常见问题排查

5.1 硬件调试步骤与示波器抓波形

硬件焊好之后,不要急着写代码,先用示波器确认SPI波形正常。步骤如下:

第一步,确认供电。用万用表量MRAM的VCC引脚,应该是3.3V。如果电压偏低或者波动,检查去耦电容有没有焊好。

第二步,确认CS信号。MCU上电后,CS应该是高电平。如果你量到CS是低电平,说明GPIO配置有问题,检查TRIS和LAT寄存器。

第三步,抓SCK和MOSI波形。发一个读命令,用示波器双通道抓SCK和MOSI。SCK应该有8个脉冲,MOSI上应该能看到命令字节0x03和地址字节。如果SCK没有波形,检查SPI使能位和时钟配置。如果MOSI波形不对,检查数据是不是写进了SSPBUF。

第四步,抓MISO波形。读操作的时候,MISO上应该有数据返回。如果MISO一直是高电平或者低电平,说明芯片没有响应,检查芯片有没有焊反、CS有没有拉低、供电是否正常。

我遇到过一个问题,SPI波形看起来都正常,但读出来的数据全是0xFF。查了半天发现是MISO引脚配置成了输出,导致MCU和芯片抢总线。PIC18的SPI引脚是复用的,配置SPI模式的时候要确保MISO对应的TRIS位是1(输入)。这个坑很隐蔽,因为波形上看起来MISO有信号,但实际上是MCU自己在驱动。

5.2 常见问题速查表

现象可能原因排查方法
读出来全是0xFFMISO配置错误、CS未拉低、芯片未供电检查TRIS寄存器、量CS电压、量VCC
写不进去漏发WREN、WREN和WRITE在同一CS帧、WP引脚拉低检查写时序、确认WP接VCC
数据偶尔出错SPI时钟太快、走线太长、去耦不足降低SPI时钟、缩短走线、加去耦电容
芯片发热供电电压过高、引脚短路量VCC是否超过3.6V、检查焊接
通信不稳定地线干扰、CS信号振铃加地线屏蔽、CS串电阻

5.3 提升可靠性的几个实操技巧

第一个技巧,SPI时钟线上串一个22欧姆到100欧姆的电阻。这个电阻可以抑制信号反射和振铃,特别是在走线比较长的时候。我一般用33欧姆,效果不错。

第二个技巧,CS信号在空闲的时候一定要拉高。有些工程师为了省事,CS一直拉低,靠命令来区分操作。这样做在低速下可能没问题,但在高速下容易误触发。MRAM芯片要求CS在每条命令之间拉高,这是硬性要求。

第三个技巧,写操作之后加一个小延时再读状态寄存器。虽然MRAM写入很快,但芯片内部的状态机需要几个时钟周期来更新WEL位。如果你写完立刻读状态,可能读到旧的WEL值。我一般加10微秒的延时,保险一点。

第四个技巧,定期做一次全片擦除测试。虽然MRAM不需要擦除,但你可以通过写全0再写全1来测试每个存储单元是否正常。这个测试在产线老化环节很有用,可以筛出不良品。

6. 性能优化与扩展应用场景

6.1 SPI时钟提速与DMA传输可行性

前面用的SPI时钟是4MHz,对于大多数工业数据记录场景已经够用了。但如果你需要高速采集,比如振动监测或者音频记录,4MHz可能不够。PIC18LF46K22的系统时钟最高可以跑到64MHz,SPI时钟可以到16MHz。把系统时钟提上去之后,SPI的配置也要相应调整。

void SPI_Init_Fast(void) { // 假设系统时钟已配置为64MHz MRAM_CS_TRIS = 0; MRAM_CS_LAT = 1; TRISC5 = 0; // SCK TRISC4 = 0; // MOSI TRISC3 = 1; // MISO SSPCON1 = 0x00; SSPCON1bits.SSPM = 0b0000; // 主模式,Fosc/4 SSPCON1bits.CKP = 0; SSPCON1bits.SSPEN = 1; SSPSTATbits.CKE = 0; SSPSTATbits.SMP = 0; SSPADD = 0; // SPI时钟 = 64MHz/4 = 16MHz }

16MHz的SPI时钟下,传输512KB数据需要的时间是:512K字节 x 8位 / 16MHz = 0.26秒。这个速度已经相当快了。如果你需要更快,可以考虑用PIC18的DMA模块,不过PIC18LF46K22没有DMA,只能靠中断或者轮询。如果速度要求特别高,建议换PIC32或者ARM Cortex-M的芯片。

6.2 多芯片级联与存储容量扩展

512KB的容量如果不够用,可以级联多颗MR25H40CDF。SPI总线是共享的,SCK、MOSI、MISO三根线并联,每颗芯片用独立的CS控制。MCU通过拉低不同的CS来选择不同的芯片。

#define MRAM1_CS_LAT LATC2 #define MRAM2_CS_LAT LATC1 #define MRAM3_CS_LAT LATC0 void MRAM_Select(unsigned char chip) { // 先全部拉高 MRAM1_CS_LAT = 1; MRAM2_CS_LAT = 1; MRAM3_CS_LAT = 1; // 拉低选中的芯片 switch(chip) { case 1: MRAM1_CS_LAT = 0; break; case 2: MRAM2_CS_LAT = 0; break; case 3: MRAM3_CS_LAT = 0; break; default: break; } }

级联的时候要注意,所有芯片的SPI模式必须一致,时钟极性、相位要匹配。另外,MISO线是多个芯片的输出,如果某个芯片没有被选中,它的MISO应该是高阻态,不会影响总线。MR25H40CDF的MISO在CS拉高的时候确实是高阻态,所以可以直接并联。

6.3 工业数据记录仪完整方案示例

最后给一个完整的应用示例,一个工业数据记录仪。功能是每秒钟采集一次温度、湿度、电压三个参数,存储到MRAM里,支持通过串口读取历史数据。

数据结构定义:

typedef struct { unsigned long timestamp; // 时间戳,4字节 unsigned short temp; // 温度,2字节,放大10倍 unsigned short humidity; // 湿度,2字节,放大10倍 unsigned short voltage; // 电压,2字节,放大100倍 unsigned short crc; // CRC16校验,2字节 } Record_t; // 总共12字节

存储地址规划:

地址范围用途
0x000000 - 0x00000F系统参数区(写入标志、记录数量等)
0x000010 - 0x00001F备份参数区
0x000020 - 0x07FFFF数据记录区

每条记录12字节,数据区大小约512KB,可以存大约43690条记录。按每秒一条算,可以存12小时的数据。如果不够,可以降低采样频率或者用压缩算法。

写入流程:

void Record_Write(Record_t *rec) { unsigned long addr; unsigned short count; // 读取当前记录数量 MRAM_ReadData(0x000004, (unsigned char*)&count, 2); // 计算写入地址 addr = 0x000020 + (unsigned long)count * sizeof(Record_t); // 计算CRC rec->crc = CRC16_Calculate((unsigned char*)rec, 10); // 写入记录 MRAM_WriteData(addr, (unsigned char*)rec, sizeof(Record_t)); // 更新记录数量 count++; MRAM_WriteData(0x000004, (unsigned char*)&count, 2); }

读取流程类似,先读记录数量,然后从数据区逐条读取,校验CRC,通过串口发送出去。

这个方案我已经在实际项目里用过,跑了两年多,没有出现过数据丢失。MRAM的可靠性确实比EEPROM和Flash强很多,特别是在频繁写入的场景下,优势非常明显。唯一要注意的是成本,MR25H40CDF的价格比同容量的EEPROM贵一些,但考虑到寿命和可靠性,这个差价是值得的。

如果你也在做工业数据存储相关的项目,不妨试试这个方案。代码可以直接拿去用,硬件设计也不复杂,关键是理解SPI时序和MRAM的写保护机制。踩过几次坑之后,你会发现这套东西其实很稳。

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

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

立即咨询