1. 为什么工业现场还在用并行SRAM,而MRAM已经悄悄替换了它
做工业控制和嵌入式数据采集的人,大概率都遇到过这个场景:设备跑在现场,每隔几毫秒要记录一次关键状态,比如电机转速、阀门开度、累计运行时长。这些数据不能丢,掉电之后必须还在。传统方案无非几种——EEPROM写入太慢,擦写寿命也就百万次级别,高频写入很快就报废;SRAM速度快但掉电即失,必须配电池或者超级电容,电池又要考虑温度范围和更换周期;NOR Flash写入前得擦除整个扇区,写入延迟动辄几毫秒到几十毫秒,而且擦写次数只有十万次量级。
MRAM(磁阻随机存储器)的出现正好卡在这个缝隙里。它的写入原理是靠磁隧道结的磁化方向变化来存储数据,不需要电荷保持,所以掉电不丢;写入速度接近SRAM,没有擦除动作,字节级随机写入;擦写寿命理论上无限次,至少也是10的14次方以上。MR25H40CDF就是Everspin出的一颗4Mbit SPI接口MRAM,工业级温度范围,SOIC-8封装,引脚和常见的SPI Flash、EEPROM高度兼容。
我这次选它搭配PIC18F47Q10做数据记录模块,核心诉求就三条:第一,写入频率高,每秒几百次不能卡;第二,掉电不能丢数据,不能靠电池;第三,接口要简单,SPI四根线搞定,PCB不用大改。PIC18F47Q10是Microchip新出的8位机,带SPI外设,Q10系列还集成了很多独立于内核的外设,比如DMA、CLC、CIP,用来做数据搬运和接口时序控制非常省心。
这篇文章我会把整个链路拆开讲:MR25H40CDF的SPI时序到底有什么坑,PIC18F47Q10的SPI模块怎么配置才能跑出稳定时序,数据写入和读取的完整代码逻辑,以及我在实测中遇到的几个典型问题。如果你手头正好有类似需求,或者单纯想搞清楚MRAM和普通SPI Flash在操作上到底差在哪,这篇应该能帮你省下不少调试时间。
2. MR25H40CDF的SPI协议细节:和普通Flash完全不是一回事
2.1 命令集精简到只有几条,但每条都有讲究
MR25H40CDF的SPI命令集比NOR Flash简单得多。它没有扇区擦除、块擦除这些概念,因为MRAM不需要擦除。核心命令就这几条:
| 命令名称 | 命令码 | 作用 | 注意事项 |
|---|---|---|---|
| WREN | 0x06 | 写使能 | 每次写操作前必须发 |
| WRDI | 0x04 | 写禁止 | 写完后自动复位,一般不用手动发 |
| RDSR | 0x05 | 读状态寄存器 | 检查WEL位和写保护状态 |
| WRSR | 0x01 | 写状态寄存器 | 配置块保护 |
| READ | 0x03 | 读数据 | 支持任意地址连续读 |
| WRITE | 0x02 | 写数据 | 支持任意地址连续写,无页边界限制 |
这里最关键的一点:MR25H40CDF的WRITE命令没有页边界限制。普通SPI Flash比如W25Q64,一次写操作不能跨页,256字节页边界到了必须重新发命令。MRAM没有这个限制,你可以从地址0x0000一直写到0x7FFFF,中间不用停。这个特性在连续记录场景下非常舒服,不用在代码里做页对齐判断。
但注意,WREN命令必须在每次WRITE之前单独发送,而且WREN之后如果SPI片选拉高,写使能状态会保持,但一旦执行完WRITE,WEL位自动清零。所以你不能发一次WREN然后连续写多个不连续地址,每次WRITE之前都要重新发WREN。
2.2 SPI模式选择:Mode 0还是Mode 3
MR25H40CDF支持SPI Mode 0(CPOL=0,CPHA=0)和Mode 3(CPOL=1,CPHA=1)。这两个模式的区别在于时钟空闲电平和数据采样边沿。
Mode 0:时钟空闲低电平,数据在上升沿采样,下降沿变化。 Mode 3:时钟空闲高电平,数据在上升沿采样,下降沿变化。
实际用下来,Mode 0和Mode 3在功能上都能正常工作,但如果你SPI总线上还挂了其他设备,最好统一模式。我这次总线上只有MRAM一颗从设备,选了Mode 0,因为PIC18F47Q10的SPI默认配置就是Mode 0,省得改。
有个细节要注意:MRAM的SPI时钟最高可以跑到40MHz,但PIC18F47Q10在64MHz系统时钟下,SPI分频后最高也就16MHz(Fosc/4)。这个速度对MRAM来说完全够用,16MHz下连续读的吞吐率大概2MB/s,写的话因为要发WREN和等待,实际有效写入速率大概几百KB/s,对于数据记录场景绰绰有余。
2.3 状态寄存器的WEL位和写保护逻辑
RDSR命令读回来的状态寄存器,bit1是WEL(Write Enable Latch),bit0是WIP(Write In Progress)。MRAM的写入速度极快,WIP位基本上发完WRITE命令之后几个时钟周期就清零了,不像Flash要等几毫秒。
但WEL位的检查不能省。我遇到过一种情况:SPI总线受到干扰,WREN命令的位被翻转了,结果WRITE命令发出去但数据没写进去。后来在每次WRITE之前加了一步RDSR检查WEL位,确认是1再发WRITE,问题就没再出现过。
状态寄存器还有BP0、BP1两位做块保护,可以保护部分地址区域不被写入。如果你的应用里有 bootloader 区域或者校准参数区域不希望被误写,可以用WRSR命令把对应块保护起来。但注意,块保护一旦设置,要解除必须发WRSR,而WRSR本身又需要WEL=1,所以解除保护的流程是:WREN -> WRSR(写0到BP位)-> 完成。
3. PIC18F47Q10的SPI外设配置:别被寄存器名字骗了
3.1 SPI1模块的时钟源和分频计算
PIC18F47Q10有两个SPI模块:SPI1和SPI2。SPI1挂在PPS(外设引脚选择)上,可以映射到任意数字引脚,灵活性很高。我这次用SPI1,映射到RC3(SCK)、RC4(SDI)、RC5(SDO),片选CS用RA5普通GPIO控制。
SPI时钟的计算公式:
Fspi = Fosc / (4 * (SPI1CON1bits.CKP + 1) * (2^SPI1CON1bits.SSP1ADD))
实际配置时,我系统时钟跑64MHz,SPI1CON1bits.SSP1ADD设为0,CKP=0(Mode 0),得到Fspi = 64MHz / 4 = 16MHz。
这里有个坑:PIC18F47Q10的SPI模块在高速下对引脚驱动能力有要求。我一开始用默认的引脚配置,16MHz下波形上升沿有点塌,读回来的数据偶尔出错。后来把SCK和SDO引脚的Slew Rate控制打开,并且把驱动电流设到最大档,波形才干净。
3.2 PPS映射的步骤和容易漏掉的寄存器
PPS映射是PIC18F47Q10比老PIC18F系列方便的地方,但步骤不能少:
// 解锁PPS PPSLOCK = 0x55; PPSLOCK = 0xAA; PPSLOCKbits.PPSLOCKED = 0; // 映射SCK到RC3 RC3PPS = 0x0F; // 0x0F是SPI1 SCK的输出代码 // 映射SDO到RC5 RC5PPS = 0x10; // 0x10是SPI1 SDO的输出代码 // 映射SDI到RC4 SPI1SDIPPS = 0x13; // 0x13是RC4的输入代码 // 锁定PPS PPSLOCK = 0x55; PPSLOCK = 0xAA; PPSLOCKbits.PPSLOCKED = 1;注意SPI1SDIPPS是输入映射,写法和其他输出映射不一样,它是把某个引脚映射到SPI1的SDI输入。这个寄存器的名字容易搞混,我一开始写成RC4PPS,结果SDI一直读不到数据。
3.3 片选信号的处理:硬件片选还是软件片选
PIC18F47Q10的SPI模块支持硬件片选(SS1),但我不建议用。原因有两个:第一,硬件片选在SPI使能之后如果SS1引脚为高,模块会自动进入从模式,容易误触发;第二,MRAM的WREN和WRITE之间需要保持片选低电平,硬件片选的时序控制不如软件灵活。
我的做法是用RA5做普通GPIO,手动控制:
#define MRAM_CS_LAT LATA5 #define MRAM_CS_TRIS TRISA5 // 初始化 MRAM_CS_TRIS = 0; // 输出 MRAM_CS_LAT = 1; // 默认拉高 // 片选拉低 MRAM_CS_LAT = 0; // ... SPI操作 ... // 片选拉高 MRAM_CS_LAT = 1;片选拉低和第一个SCK边沿之间要留至少20ns的建立时间。16MHz下每个时钟周期62.5ns,所以直接在片选拉低后插入一个NOP就够了。但如果你SPI跑得更快,比如32MHz,就要考虑加一点延时或者用CLC模块做硬件延时。
4. 数据写入与读取的完整实现:从单字节到批量记录
4.1 单字节写入的完整时序
写一个字节到MRAM的完整流程:
- 片选拉低
- 发送WREN(0x06)
- 片选拉高
- 片选拉低
- 发送WRITE(0x02)
- 发送24位地址(MR25H40CDF是4Mbit,需要19位地址,但按24位发送,高5位忽略)
- 发送数据字节
- 片选拉高
注意第3步和第4步之间片选必须拉高,这是WREN命令生效的条件。我试过WREN之后不拉高片选直接发WRITE,数据写不进去。
代码实现:
void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x06); // WREN MRAM_CS_LAT = 1; MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); SPI1_ExchangeByte(data); MRAM_CS_LAT = 1; }SPI1_ExchangeByte函数就是往SPI1BUF写数据,然后等SPI1IF标志置位,读SPI1BUF返回接收到的字节。
4.2 批量写入时如何避免频繁WREN
如果你要连续写多个字节,比如一次记录16字节的数据包,有两种做法:
方案A:每个字节都发WREN + WRITE。这样最保险,但效率低,每次写都要额外的WREN开销。
方案B:发一次WREN,然后发WRITE命令,连续发送多个数据字节,最后拉高片选。MRAM支持在WRITE命令之后连续写入,地址自动递增,没有页边界限制。
我实测方案B完全可行,而且效率高很多。写16字节的数据包,方案A需要16次WREN+WRITE,方案B只需要1次WREN+1次WRITE+16个数据字节。在16MHz SPI下,方案A大概耗时200us,方案B只要30us左右。
但方案B有一个风险:如果SPI传输过程中受到干扰,片选意外拉高,后面的数据就丢了。所以在高干扰环境下,我建议还是用方案A,或者至少加CRC校验。
4.3 读取操作的地址自动递增特性
MRAM的READ命令(0x03)支持连续读取,地址自动递增,可以一直读到0x7FFFF然后回绕到0x0000。这个特性用来做数据回放非常方便。
void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LAT = 0; SPI1_ExchangeByte(0x03); // READ SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI1_ExchangeByte(0x00); } MRAM_CS_LAT = 1; }注意读取的时候发送的是0x00,因为SPI是全双工,你发什么字节出去不重要,重要的是从设备返回的数据。
4.4 数据记录模块的环形缓冲区设计
我的应用场景是每秒记录100次设备状态,每次16字节。MRAM总容量512KB,我划了前256KB做环形缓冲区,后256KB做参数存储和日志。
环形缓冲区的写指针存在MRAM的固定地址,每次写入前先读指针,写完数据后更新指针。这里有个关键点:指针更新必须和数据写入在同一个WRITE命令序列里完成,否则掉电时可能出现数据写了但指针没更新,或者指针更新了但数据没写。
我的做法是把指针和数据打包成一个结构体,一次写入:
typedef struct { uint32_t write_ptr; uint8_t data[16]; uint16_t crc; } Record_t;每次记录时,先读回当前write_ptr,构造Record_t,计算CRC,然后从write_ptr对应的地址开始连续写入sizeof(Record_t)个字节。因为MRAM没有页边界限制,这一整块可以一次WRITE命令写完,保证原子性。
5. 实测中遇到的三个典型问题与排查过程
5.1 问题一:连续写入时偶发数据错位
现象:批量写入1000次,偶尔有几次读回来的数据整体偏移一个字节。
排查过程:先用逻辑分析仪抓SPI波形,发现错位的那几次,WRITE命令之后的第一个数据字节和地址的最后一个字节之间多了一个时钟周期。仔细看波形,是片选拉低之后,SCK的第一个边沿来得太早,MRAM还没准备好。
根因:PIC18F47Q10的SPI模块在片选拉低后,如果SPI1BUF里还有之前的数据,第一个SCK边沿可能会提前。解决办法是在片选拉低之后、写SPI1BUF之前,先清空SPI1BUF,或者插入一个短延时。
我最后的做法是在MRAM_CS_LAT = 0之后加了一个__delay_us(1),问题消失。1us在16MHz下是16个时钟周期,对整体性能影响可以忽略。
5.2 问题二:掉电测试时最后几条记录丢失
现象:正常运行时数据都对,但突然断电再上电,最后几条记录读出来是0xFF。
根因分析:MRAM本身写入是非易失的,写入完成就保存了。问题出在写指针的更新上。我原来的代码是先写数据,再更新指针,两个操作分开。掉电发生在写数据和更新指针之间时,数据写进去了但指针没更新,下次上电后新数据会覆盖掉刚写的那条。
解决办法:把数据和指针打包成一个结构体,一次WRITE写完。这样要么全写进去,要么全没写,不会出现中间状态。改完之后掉电测试跑了200次,没有再丢数据。
5.3 问题三:SPI时钟跑高之后读数据出错
现象:SPI时钟从8MHz提到16MHz后,读回来的数据偶尔有bit翻转。
排查:用示波器看SCK和SDI波形,发现16MHz下SDI的上升沿有点慢,在SCK采样边沿处还没达到VIH。
根因:PIC18F47Q10的SDI引脚输入没有施密特触发器,而且默认驱动能力较弱。MRAM的SDO输出驱动能力也有限,两者之间的PCB走线如果比较长,分布电容会导致边沿变缓。
解决办法:第一,缩短SPI走线,MRAM尽量靠近MCU;第二,在SDI线上加一个4.7kΩ上拉电阻到3.3V,加速上升沿;第三,把SPI时钟降到12MHz,留出余量。三招一起上,问题彻底解决。
注意:SPI时钟不是越高越好,尤其是工业现场有干扰的情况下,留20%的余量比跑满速更可靠。
6. 工业场景下的可靠性加固与经验总结
6.1 电源监控与掉电保护
MRAM虽然写入快,但也不是瞬间完成。16MHz SPI下写一个32字节的结构体大概需要20us。如果电源掉电时间小于20us,写入可能不完整。
我的做法是用PIC18F47Q10的HLVD(高/低电压检测)模块,设置检测阈值为3.0V。当电源电压降到3.0V时,HLVD触发中断,在中断里立即停止所有写入操作,把关键状态保存到MRAM的保留区域。从3.0V降到MRAM最低工作电压2.7V,大概有几百微秒的窗口,足够完成一次紧急写入。
6.2 CRC校验的选型和计算开销
数据记录场景下,CRC是必须的。我选的是CRC-16/CCITT,多项式0x1021,初始值0xFFFF。PIC18F47Q10没有硬件CRC模块,用软件查表法计算,16字节数据大概耗时8us,可以接受。
如果你对速度要求更高,可以用PIC18F47Q10的CLC模块配合DMA做硬件CRC,但配置比较复杂,我这次没上。
6.3 MRAM的寿命和磨损均衡
MRAM的擦写寿命是10的14次方以上,按每秒写100次算,一年也就3.15×10的9次方次,远远没到寿命上限。所以MRAM不需要磨损均衡,环形缓冲区直接循环覆盖就行。
但有一点要注意:MRAM的写入次数虽然无限,但每次写入的功耗比读取高。如果设备是电池供电,要控制写入频率,或者用批量写入减少WREN命令的次数。
6.4 和EEPROM、FRAM的对比选型建议
| 特性 | MR25H40CDF (MRAM) | 典型EEPROM | 典型FRAM |
|---|---|---|---|
| 接口 | SPI | I2C/SPI | I2C/SPI |
| 写入速度 | 极快,无擦除 | 慢,5ms/字节 | 快,无擦除 |
| 擦写寿命 | 10^14+ | 10^6 | 10^12+ |
| 掉电保持 | 是 | 是 | 是 |
| 容量 | 4Mbit | 通常≤2Mbit | 通常≤4Mbit |
| 成本 | 中高 | 低 | 中 |
| 工业温度 | -40~85°C | -40~85°C | -40~85°C |
选型逻辑很简单:写入频率高、寿命要求高、容量需求大,选MRAM;成本敏感、写入频率低,选EEPROM;介于两者之间,FRAM是折中方案。
6.5 我在这个项目里踩过的几个小坑
第一个坑:MRAM的SOIC-8封装和W25Q64的SOIC-8封装引脚定义不一样。W25Q64的pin1是CS,pin2是DO,pin3是WP,pin4是GND;MR25H40CDF的pin1是CS,pin2是SO,pin3是WP,pin4是GND。看起来一样,但pin5、pin6、pin7、pin8的定义有差异。我一开始直接拿W25Q64的PCB改,结果pin7和pin8接反了,MRAM一直不工作。后来查数据手册才发现,MR25H40CDF的pin7是NC,pin8是VCC,而W25Q64的pin7是HOLD,pin8是VCC。虽然pin8都是VCC,但pin7的处理不一样,MRAM的pin7必须悬空,不能接地。
第二个坑:PIC18F47Q10的SPI1模块在配置为Master模式时,如果SS1引脚被配置为输入且为低电平,模块会自动切换到Slave模式。我一开始把SS1引脚也映射到了RA5,结果片选拉低的时候SPI模块直接罢工。后来把SS1功能关掉,RA5纯做GPIO,问题解决。
第三个坑:MRAM的WREN命令之后,如果片选拉高再拉低,WEL位会保持。但如果中间插入了其他SPI设备的操作,WEL位可能会被意外清除。我的SPI总线上只有MRAM,所以没这个问题,但如果你的总线上有多个从设备,每次WRITE之前都要重新发WREN,不能假设WEL位还在。
6.6 给后续开发者的几条实用建议
如果你准备用MR25H40CDF做数据记录,我的建议是:第一,SPI时钟先跑8MHz,调通之后再往上提,不要一上来就16MHz;第二,片选控制用GPIO,不要用硬件SS;第三,数据和指针打包写入,保证原子性;第四,加CRC校验,工业现场没有CRC的数据不可信;第五,电源监控一定要做,HLVD模块用起来,成本几乎为零但能救命。
这个方案我已经在三个现场设备上跑了半年多,每天写入量大概800万次,目前没有出现过数据丢失或MRAM失效的情况。MRAM的价格虽然比EEPROM贵,但省掉了电池、超级电容和复杂的磨损均衡算法,整体BOM成本和维护成本反而更低。如果你也在纠结数据存储方案,不妨试试MRAM这条路。