做工业数据采集类产品,最怕的就是现场突然断电。早几年我做一台故障录波终端,用SPI Nor Flash记录波形,测试时连续拔插电源,间隔只有几十毫秒,结果Flash里整块整块的数据损坏,有些扇区连读出都是0xFF。后来换上了MR25H40CDF这颗4Mbit磁阻随机存取存储器(MRAM),配合PIC18F86J16单片机,才算把这个问题彻底根治。今天这篇文章,就围绕这两颗芯片,完整讲清楚在工业和嵌入式应用里怎么选型、怎么接线、怎么写驱动,以及存储可靠性上那些容易被忽视的实战细节。打算做数据记录、掉电保存、参数频繁更新的开发者,应该能从这里直接拿到一套可落地的方案。
MRAM这个名词听起来偏门,其实它已经把SRAM的速度、DRAM的密度和Flash的非易失性合到了一起。MR25H40CDF属于Everspin的第二代SPI MRAM,容量4Mbit,工作电压2.7到3.6V,SPI时钟最高能到40MHz,写入寿命大于10的14次方次,数据保存20年以上。这些数字放在工业现场,基本上是为“频繁写入+掉电不丢”这个刚需量身定做的。而PIC18F86J16作为Microchip的8位增强型单片机,有64KB程序Flash、约4KB SRAM、1KB Data EEPROM,还包括两个MSSP外设、多个定时器、比较器和BOR,做中低端工业控制板非常顺。两者通过标准SPI总线连在一起,读写的可靠性和开发复杂度,远比想象中友好。
1. 选型思路:为什么是MRAM而不是Flash和EEPROM
1.1 工业现场对非易失存储的三个硬约束
第一个硬约束是写入次数。工业设备里的参数表、累计量、事件记录,往往每个运行周期都在变。以一条产线为例,设备每天运行20小时,每秒刷新一次累计量,一天就是72000次写入。普通EEPROM的擦写寿命通常在100万次量级,换算下来不到两周就把寿命额度耗尽。即便用SPI Nor Flash,标称擦写寿命一般也只有10万次左右,在这个场景下同样撑不了多久。而MRAM的写寿命达到10的14次方,每秒写1000次连续写个几年都没有任何压力,基本不需要考虑磨损均衡。
第二个硬约束是写入速度。很多记录类应用要求在掉电瞬间把关键数据保存下来,给处理留出的时间往往只有几个毫秒。EEPROM单字节写入要几个毫秒,Flash整页编程也要毫秒级,再把擦除时间算上,掉电瞬间根本来不及。MRAM的写入本质是磁阻状态翻转,没有擦除概念,SPI时钟下每个字节仅仅需要一个时钟周期,10MHz下写512字节数据耗时不到0.5毫秒。这个速度让掉电保存从“想办法凑时间”变成了“随手就能写”。
第三个硬约束是环境干扰。工业现场有电机、变频器、接触器,电磁环境很恶劣。传统Flash靠电荷存储在浮置栅极里,强烈的电磁干扰或长期高温可能导致电荷泄漏,数据慢慢就“糊”了。MRAM利用磁性隧道结的磁电阻效应存储数据,本质上不依赖电荷,抗辐射和抗电磁干扰能力比Flash强很多。这一点对电力监控、轨道交通、医疗电子这类高可靠场景非常关键。
1.2 MRAM与主流存储介质的横向对比
我在选型时把几种常见非易失存储介质拉了个表,直接对比核心参数:
| 参数 | MRAM | SPI Nor Flash | EEPROM | FRAM |
|---|---|---|---|---|
| 擦写寿命 | >10^14次 | 约10万次 | 约100万次 | 约10^12次 |
| 写入前擦除 | 不需要 | 需要 | 不需要 | 不需要 |
| 单字节写入 | 支持 | 按页/扇区 | 支持 | 支持 |
| 典型写入速度 | <50ns | 页编程约1ms | 约3~10ms | 约100ns |
| 数据保持 | >20年 | 约10~20年 | 约10年 | 约10年 |
| 抗辐射能力 | 强 | 一般 | 一般 | 强 |
| 位密度成本 | 偏高 | 最低 | 中等 | 偏高 |
单看成本,MRAM确实不占优势,1Mbit的MRAM价格通常是同容量NOR Flash的几倍。但在整个系统里算总账,MRAM节省了Flash擦除算法、磨损均衡、坏块管理和掉电补充电路的开发成本,换来的是软件极度简单和现场故障率大幅下降。对于批量不大、可靠性要求高的工业设备来说,这个溢价完全值得。
1.3 为什么MCU选PIC18F86J16
单片机选择PIC18F86J16,主要看中三点。
第一是资源平衡。它有64KB程序Flash、约3936字节SRAM和1KB Data EEPROM,跑一个RTOS小系统或者裸机状态机都够用。接口方面有2个MSSP,硬件SPI可以直接同时挂MRAM和其他SPI器件,不需要软件模拟时序,稳定性高很多。还有5个定时器、2个EUSART、多路ADC和比较器,一块板子上的通信、采集、存储基本都能包圆。
第二是电气特性。工作电压范围2.0到3.6V,和MR25H40CDF的2.7到3.6V正好都在3.3V上,供电设计不需要额外电平转换。它的BOR欠压复位模块可以在电压跌落时产生复位或中断,和MRAM的快速写入特性配合,能形成一套很实用的掉电保护机制。
第三是封装和温度等级。PIC18F86J16常见TQFP-80封装,引脚数量充足,GPIO分配灵活,不会出现为了迁就引脚而被迫改PCB走线的问题。工业级版本支持-40到+85℃,和MR25H40CDF的工业温度等级相匹配,整板在高温高湿环境下的可靠性有依据。
2. 硬件连接与电路设计
2.1 MR25H40CDF引脚与SPI接线
MR25H40CDF的引脚不多,核心就是SPI四线和三个控制脚。8引脚封装引脚功能大致如下:
| 引脚 | 功能 | 说明 |
|---|---|---|
| 1 | /CS | 片选,低电平有效 |
| 2 | SCLK | SPI时钟 |
| 3 | SI | SPI数据输入,MCU发数据到MRAM |
| 4 | SO | SPI数据输出,MRAM发数据到MCU |
| 5 | /WP | 写保护,低电平禁止写入 |
| 6 | /HOLD | 保持,低电平暂停通信 |
| 7 | VSS | 地 |
| 8 | VCC | 电源,2.7~3.6V |
我的板子上,PIC18F86J16使用MSSP1作为SPI主机,接线关系如下:
| MR25H40CDF引脚 | 连接到哪里 |
|---|---|
| /CS | MCU的RB0,普通GPIO控制 |
| SCLK | MCU的RB1,MSSP1时钟 |
| SI | MCU的RB2,MSSP1 SDO |
| SO | MCU的RB3,MSSP1 SDI |
| /WP | 直接接VCC |
| /HOLD | 直接接VCC |
| VCC | 3.3V电源 |
| VSS | GND |
这里有个细节要注意:具体复用引脚以PIC18F86J16数据手册的引脚功能表为准,不同封装和不同外设映射会有差异。我上面写的是我板子上的分配,实际设计时务必对照手册确认RB1/RB2/RB3是不是MSSP1的SCLK/SDO/SDI。曾经有人照抄别人的接线,结果芯片型号批次不同,引脚功能不一样,SPI信号根本没到MRAM上,白白折腾了两天。
2.2 电源去耦和上下拉
MRAM是数字芯片,但电源设计不能凑合。VCC引脚旁边我放了0.1uF的陶瓷电容和10uF钽电容各一个,并且都尽量靠近芯片引脚放置。0.1uF负责滤除高频噪声,10uF应对瞬态电流波动。如果板上还有其他数字电路,建议把MRAM的电源走线从公共电源平面出来后单独加一个磁珠,防止高频噪声串进来。
/CS、SCLK、SI这几个信号如果走线过长,建议在MCU侧串33欧姆电阻,改善信号振铃。MRAM的SPI接口速度如果跑到20MHz以上,PCB走线就要注意等长和回流地,别让时钟线和数据线绕得太远。工业环境里,SPI信号线上并一个小电容到地(比如10pF左右)也能有效抑制高频干扰,但会影响边沿速率,需要和实际通信速率之间做平衡。
2.3 上板前必查的三个细节
第一个细节是/WP和/HOLD绝对不能浮空。浮空的/WP可能因为噪声被拉低,MRAM直接进入写保护状态,表现出来的现象就是“数据怎么都写不进去,读出来全是旧值”。浮空的/HOLD更危险,它会在SCLK低电平时被误触发,导致SPI通信被暂停,MCU发完命令后收不到任何回应。我的做法是把这两个引脚直接接VCC,简单粗暴,只要设计上不需要掉电保护整片存储区,就不去动它们。
第二个细节是CS的控制方式。MRAM的每个命令都是以CS上升沿作为结束点,软件里CS跳变的时机必须精确。如果用GPIO模拟CS,驱动代码里不要做任何多余的操作,比如在CS拉高和拉低之间插入无意义的延时,否则可能踩到MRAM的时序边界。直接看逻辑分析仪抓到的波形,确认CS保持低电平的时间包含了完整的命令字节和地址字节。
第三个细节是VCC电压监测。MR25H40CDF的工作电压下限是2.7V,如果板子供电在电压跌落时低于这个值,写入操作会落入不确定状态。工业设计里,建议用一个电阻分压网络或比较器把3.3V电源送到PIC18F86J16的ADC引脚,在掉电时为软件提供一个中断触发源。后面第4部分会细讲怎么配合这个电压监测做掉电保存。
3. 软件驱动:从SPI初始化到完整读写
3.1 用XC8把MSSP1配置成SPI主机
软件部分我用Microchip XC8编译器,直接操作寄存器。PIC18F86J16的MSSP模块配置SPI主机模式需要设置三个关键寄存器:SSP1CON1、SSP1STAT和SSP1ADD。下面这段是最小初始化代码:
#include <xc.h> // 根据实际引脚定义CS #define MRAM_CS LATBbits.LATB0 #define MRAM_CS_TRIS TRISBbits.TRISB0 void SPI1_Init(void) { // MSSP1引脚设为数字IO TRISBbits.TRISB1 = 0; // SCLK 输出 TRISBbits.TRISB2 = 0; // SDO 输出 TRISBbits.TRISB3 = 1; // SDI 输入 MRAM_CS_TRIS = 0; MRAM_CS = 1; // 主模式,SPI Mode 0,Fosc/16 SSP1CON1 = 0b00100010; // CKP=0: 空闲时钟为低 // SSPEN=1: 使能串行端口 // SSPM=0010: SPI Master, Fosc/16 SSP1STAT = 0b01000000; // SMP=1: 数据在采样阶段中段采样 // CKE=1: 数据在SCLK上升沿采样,对应Mode 0 SSP1ADD = 0x00; // 仅在带定时器的模式下使用,这里置0 }这段配置里经常有人搞混CKP和CKE。PIC的MSSP模块里,CKP决定SCLK空闲电平,CKE决定数据采样边沿。要匹配MRAM支持的SPI Mode 0,标准写法是CKP=0、CKE=1;如果要匹配SPI Mode 3,则要CKP=1、CKE=0。很多人从网上抄配置,抄来抄去发现读出的数据顺序错位或者全是乱码,多半就是这两个位的组合和实际选用的模式对不上。
作为验证手段,建议在初始化之后人为拉低CS、发一个0x05读状态寄存器命令,然后抓取SO线上的返回值。如果返回0x00,说明SPI链路基本通了;如果返回0xFF,大概率是SDO/SDI接反或者MOSI/MISO方向配置错误。
3.2 MRAM的指令集与读写时序
MR25H40CDF的指令集和普通SPI Flash很像,常见的指令如下:
| 指令 | 操作码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能,必须先发才能写 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| SLEEP | 0xB9 | 进入睡眠模式 |
| WAKE | 0xAB | 唤醒 |
WREN是所有写操作的前置条件。它的时序只有一个要求:拉低CS,发送0x06,然后拉高CS。这个CS上升沿会把写使能锁存置位。之后才能拉低CS发送真正的写命令。不少人第一次调MRAM,写了WREN之后不拉高CS就直接发WRITE,结果写操作被拒绝,原因就是写使能锁存没有生效。这个和Flash的体验一模一样,习惯了就好。
读数据时序:拉低CS,发送0x03,接着发送24位地址(高位在前),然后连续读取若干个字节。整个过程中CS必须一直保持低电平,读完最后一个字节后拉高CS。MRAM支持连续读,不需要管页边界,读到哪里算哪里,非常方便。
写数据时序:先执行WREN并拉高CS,再拉低CS发送0x02,发送24位地址,紧接着发送要写入的字节数据。所有字节发送完后拉高CS。关键点在于,MRAM不需要写前擦除,也不需要页编程等待,最后一个字节的CS上升沿就是写入完成的瞬间。没有“忙等待”这个步骤,驱动的复杂度比Flash低一大截。
3.3 完整读写驱动代码
下面是一套可以抄走的读写驱动,支持任意地址、任意长度读写:
unsigned char SPI1_ExchangeByte(unsigned char data) { SSP1BUF = data; while(!SSP1STATbits.BF); return SSP1BUF; } void MRAM_WriteEnable(void) { MRAM_CS = 0; SPI1_ExchangeByte(0x06); MRAM_CS = 1; } void MRAM_ReadBuffer(unsigned long addr, unsigned char *buf, unsigned int len) { MRAM_CS = 0; SPI1_ExchangeByte(0x03); SPI1_ExchangeByte((unsigned char)(addr >> 16)); SPI1_ExchangeByte((unsigned char)(addr >> 8)); SPI1_ExchangeByte((unsigned char)(addr)); while(len--) { *buf++ = SPI1_ExchangeByte(0x00); } MRAM_CS = 1; } void MRAM_WriteBuffer(unsigned long addr, unsigned char *buf, unsigned int len) { MRAM_WriteEnable(); MRAM_CS = 0; SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((unsigned char)(addr >> 16)); SPI1_ExchangeByte((unsigned char)(addr >> 8)); SPI1_ExchangeByte((unsigned char)(addr)); while(len--) { SPI1_ExchangeByte(*buf++); } MRAM_CS = 1; }读函数很好理解,罢了CS之后先发0x03和地址,然后把SO上的数据一个字节一个字节收回来。写函数要注意,MRAM地址是18位有效,但24位地址域要求高位补零。如果addr范围不超过512KB,addr的bit17以上必然为0。如果误操作把这个值写成非0,MRAM会忽略这些高位还是触发异常行为,取决于具体批次,稳妥做法是在驱动里做一次掩码。
写完数据后,如果对可靠性有执念,可以再补一个状态寄存器读取函数,确认WEL位被清0。WEL位是状态寄存器的bit0,写操作完成后自动归0。看到WEL归0,就说明这一次写循环完整结束了。不过由于MRAM没有忙状态,正常流程下这个读取不是必须的,可以在调试阶段用。
3.4 实际存储布局:不用管擦除,分区策略更自由
512KB的存储空间怎么规划?我一般分成四个区域,每个区域职责明确。
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00000~0x01FFF | 启动参数区 | 设备编号、通信地址、校准系数,双份备份 |
| 0x02000~0x0FFFF | 运行状态区 | 累计量、当前参数、运行时间 |
| 0x10000~0x6FFFF | 日志/记录区 | 事件记录、波形数据,环形覆盖 |
| 0x70000~0x7FFFF | 出厂校准区 | 出厂标定值,生产测试时一次性写入 |
以前用Flash的时候,日志区要规划好扇区边界,写满一个扇区就得擦除整个扇区,程序里到处是块管理逻辑。换用MRAM后,日志区可以做成线性环形缓冲,写指针在MRAM里保存,写到末尾就绕回开头,不需要任何擦除操作。这个体验上的差距,只有真正从Flash迁移过来的人才懂。
4. 工业应用中的数据可靠性设计
4.1 掉电保存:电容撑时间,MRAM快速把数据写进去
工业设备掉电是家常便饭,但掉电瞬间需要保存的数据量其实很少。以我做的故障录波终端为例,掉电时要保存的是一组最近状态字、几个累计量和一条事件记录,加起来200字节以内。难点不在量,而在时间窗口极短。
我的方案是硬件上用一个大电容给系统争取时间,软件上通过掉电检测中断触发保存流程。电容容量计算其实很简单。系统在掉电后仍需要工作的电流假设是50mA,允许电压从3.3V跌到2.7V,压降0.6V,想要撑住20ms,需要的电容是:
C = I × t / ΔV = 0.05 × 0.02 / 0.6 ≈ 1667uF
取一个2200uF的电解电容就够了。实际电路中,掉电检测我直接用PIC18F86J16的BOR模块或者用一个电阻分压网络把3.3V电源送到ADC通道。检测到电压跌到预设阈值,触发高优先级中断,中断里立刻调用MRAM_WriteBuffer把200字节关键数据写进去。MRAM写入200字节在10MHz SPI下大概是0.2毫秒,加上中断响应和压栈,整体不到1毫秒。也就是说,20ms的维持时间提供了接近20倍的余量,非常安全。
把关键数据保存完之后,如果有额外时间,我还会再把当前MRAM写地址和校验和保存一遍。这些数据本身就是写到MRAM里的,所以整个掉电保存过程在几毫秒内全部完成。换成Flash的话,光擦除一个扇区就要几十毫秒,电容容量得撑到几百毫秒级别,成本和PCB面积都会吓人。
4.2 数据帧加CRC,垫底可靠性
MRAM本身不依赖电荷存储,抗翻转能力比Flash强,但工业现场的强电磁干扰依然可能在SPI传输过程中破坏数据链路。我的习惯是所有写入MRAM的业务数据都带帧头和校验字节,读取的时候先验证再使用。
一个简单实用的帧结构如下:
typedef struct { unsigned char head[2]; // 固定为0xAA 0x55 unsigned char cmd; // 帧类型 unsigned char len; // 数据长度 unsigned char data[64]; // 业务数据 unsigned char crc; // CRC8 } DATA_FRAME;CRC8用多项式0x07即可,代码短,查表或位运算都行。下面是一个极简逐位实现:
unsigned char CRC8_Calc(const unsigned char *buf, unsigned char len) { unsigned char crc = 0; while(len--) { crc ^= *buf++; for(unsigned char i = 0; i < 8; i++) { if(crc & 0x80) crc = (crc << 1) ^ 0x07; else crc <<= 1; } } return crc; }写入时先算好CRC填入帧尾,读取时重新计算并与帧尾比较。如果CRC不匹配,先不要急着丢弃,可以对备用区重复读一次,排除SPI总线上的瞬时毛刺。连续两次CRC校验都失败,才判定该数据损坏。这种冗余策略在电机、变频器附近的设备上非常管用。
4.3 双备份与环形日志区
启动参数区我坚持用双份镜像。一份在0x00000,另一份在0x01000。每次更新参数时,先写备份区并加CRC,确认无误后覆盖主用区。读取时优先读主用区,CRC校验失败就自动切换到备份区。MRAM写入不需要擦除,所以双份更新的时间开销很小,完全值得做。
环形日志区也有讲究。我把日志区设计成定长记录,每条记录头部包含序号和时间戳。写指针本身存放在MRAM的固定地址,每次上电读出来继续写。掉电时可能发生写了一半的情况,解决方法是每条记录的最后一个字节固定为0x5A,读取时如果发现结尾不是0x5A,判定该条记录不完整并跳过。这样即使掉电发生在日志写入的中间,也不会影响前面的历史数据。
4.4 状态寄存器与写保护引脚的正确姿势
MR25H40CDF的状态寄存器里有一个WEL位(bit0),它是写使能锁存器。写入命令之前必须先经过WREN把WEL置1,写操作结束后WEL自动清0。调试时可以读状态寄存器确认锁存器的状态,但正常流程不需要关心它。
真正需要关心的是/WP引脚。如果/WP被拉低,即使软件里发了WREN,状态寄存器的写入也会被禁止。不过要注意,MRAM的/WP默认只保护状态寄存器,不保护数据区。如果你希望某个区域变成只读,就必须把块保护位写进状态寄存器。工业场景下,出厂校准区和引导区建议用块保护锁定,防止现场误写入。这一块的具体位定义要严格对照数据手册,不同批次可能会有差异,我用的板子上是BP0到BP3控制不同扇区范围。
还有一个容易忽略的点:MRAM进入SLEEP模式后,读数据会返回全1。如果芯片意外进入睡眠,代码里可能会误报“存储数据全是0xFF”。处理方法是初始化时先发0xAB唤醒指令。哪怕上电默认不处于睡眠态,发一次唤醒也没有副作用。
5. 调试实录与常见问题速查
5.1 排查思路:先抓CS时序,再查SPI模式
MRAM调试踩坑,九成问题出在三个地方:CS时序、SPI模式、写使能。排查顺序我建议固定下来。
第一步用逻辑分析仪抓CS、SCLK、SI、SO四根线。发一条WREN指令,看CS是否在0x06发送完成后立刻拉高。再发一条READ命令,看CS拉低期间是否完整发送了0x03加三个字节地址。CS时序不对,后续所有排查都白搭。
第二步确认SPI模式。MRAM支持模式0和模式3,两者共用相同指令集,只是时钟极性和采样边沿不同。用逻辑分析仪看SCLK空闲电平,低电平且数据在上升沿锁存就是模式0。如果SCLK空闲为高,就要切换成模式3,对应PIC上的CKP=1、CKE=0。
第三步确认写使能。如果读出来的数据全是旧值或者全是0xFF,先确认写操作指令序列里有没有WREN+CS高电平这一对组合。很多移植过来的代码在原本的Flash驱动里没写使能这步,因为有些Flash自带写使能,MRAM在这里就是缺一不可。
5.2 四个真实案例
案例一:读出固定0xFF。排查过程:先量VCC,发现只有2.5V,MRAM在欠压状态下行为异常。换到3.3V正常。这个案例提醒我,MRAM的低压边界是2.7V,供电裕量不足时不该找软件背锅。
案例二:打印日志时数据偶发错位。抓逻辑分析仪发现SCLK波形上升沿非常缓,原因是SCK线上并了100pF电容,把边沿拖慢了。SPI数据在上升沿采样时正好采到了不稳的电平。把电容去掉后问题消失。工业板上的滤波电容不是越小越好,也不是越大越好,得结合通信速率来配。
案例三:写数据后马上读,偶发出现旧数据。检查发现是MCU的SPI时钟分频配置有误,实际SPI时钟接近25MHz,超过了MR25H40CDF在高温下的实际可靠频率。把分频系数调整到10MHz后,连续跑48小时再没出过错。
案例四:掉电保存偶尔无效。查到掉电检测中断优先级设得太低,在保存流程还没跑完的时候被其他外设中断打断了。解决方法是保存流程里临时代码段进入关中断状态,保存完毕再恢复。MRAM写入本身很快,关中断几百微秒对系统影响可以忽略。
5.3 常见问题速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 写不进去,读出来是旧值 | 未发WREN | 写数据前先WREN并拉高CS |
| 写不进去,读出来全0xFF | /WP被拉低 | 把/WP接VCC |
| 读出来全0xFF | 芯片进入SLEEP | 初始化发0xAB唤醒 |
| 读写数据错位 | SPI Mode配置不对 | 检查CKP/CKE组合 |
| 偶发读错数据 | 时钟太快或信号边沿差 | 降低SPI分频,检查串阻 |
| 掉电保存数据丢失 | 电容容量不够 | 按公式核算维持时间 |
| 一个地址写不进,其他地址正常 | 块保护BP位被设置 | 检查状态寄存器 |
| 高温时读写异常 | 接近温度上限且时钟过高 | 降频使用 |
6. 写在最后的经验
如果让我重新做一遍这个项目,硬件上我会把MRAM的SCLK和SDI之间预留两个测试点,方便焊接逻辑分析仪探头;软件上我会在一开始就把CRC校验和双备份做进去,而不是等现场出了问题再补。调试MRAM的这几天最深的体会是:这种芯片省掉的不是成本,而是心智负担。不用考虑擦除、磨损、坏块,驱动代码写到能跑就基本稳定了。希望这篇文章能让更多做工业嵌入式的人少走弯路,直接享受到MRAM带来的省心体验。