1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18
1.1 一个真实场景引出的需求
前阵子帮一个做工业数据采集的朋友处理一块老设备的主控板,需求说起来很简单:设备在野外无人值守,市电断了靠后备电池撑一会儿,主控需要在掉电瞬间把几十个字节的关键参数(累计流量、校准系数、运行时长)存下来,下次上电再读回来。原来的方案用的是带纽扣电池的 SRAM,电池两年一换,维护成本高,而且低温环境下电池容量掉得厉害,冬天经常丢数据。
这种场景其实在工业仪表、电力监测、医疗设备里非常普遍。核心诉求就三条:写入要快、掉电不能丢、擦写寿命要够长。EEPROM 写入慢(毫秒级),Flash 有擦除块限制且写入前要整块擦,铁电存储器 FRAM 容量小价格高。这时候 MRAM 就进入了视野——MR25H40CDF 这颗 4Mbit 的磁性随机存储器,SPI 接口,写入没有延迟,理论上无限次擦写,正好卡在这个需求点上。
主控这边选的是 PIC18LF45K22,8 位机里比较经典的一颗,自带硬件 MSSP 模块支持 SPI 主模式,低功耗模式下电流很小,宽电压 1.8V 到 5.5V,跟 MRAM 的供电范围能对上。这套组合不是性能最强的,但在“低成本 + 高可靠 + 低功耗”这个三角里,平衡得相当好。
1.2 为什么不用 EEPROM 和 Flash
很多人第一反应是:存几十个字节,用片内 EEPROM 不就行了?PIC18LF45K22 本身有 256 字节的 EEPROM,确实够存。但问题在于写入速度和寿命。片内 EEPROM 单字节写入典型时间在 4ms 左右,如果掉电瞬间要存 64 字节,那就是 256ms,后备电容根本撑不住。而且 EEPROM 擦写寿命标称 100 万次,如果设备每秒记录一次,不到两周就写爆了。
Flash 更麻烦,写入前必须擦除整个扇区,擦除时间长,而且擦写次数通常只有 10 万次量级。MR25H40CDF 的写入是真正的“即时写入”,数据送到就存好了,没有内部写周期,不需要轮询等待,也不存在擦除概念。这一点在掉电保存场景里是决定性的。
| 存储类型 | 写入方式 | 单字节写入时间 | 擦写寿命 | 掉电保存难度 |
|---|---|---|---|---|
| 片内 EEPROM | 字节写入 | 约 4ms | 约 100 万次 | 中,需等待写周期 |
| SPI Flash | 扇区擦除后写入 | 擦除数十 ms | 约 10 万次 | 高,擦除期间掉电易损坏 |
| FRAM | 即时写入 | 无延迟 | 近乎无限 | 低 |
| MRAM(MR25H40CDF) | 即时写入 | 无延迟 | 近乎无限 | 低 |
1.3 MR25H40CDF 的关键特性拆解
这颗芯片的几个参数值得单独拎出来说。容量 4Mbit 也就是 512KB,对于存参数、日志、配置来说绰绰有余。接口是标准 SPI,支持模式 0 和模式 3,最高时钟 40MHz,实际在 PIC18 上跑 10MHz 完全没问题。供电 2.7V 到 3.6V,注意它不能直接接 5V,如果主控是 5V 系统需要做电平转换或者用分压。
它内部按 512 字节一页组织,支持页写和字节写。读操作就是标准的 SPI 读,发命令字加地址,然后连续读数据。写操作需要先发写使能命令(WREN),再发写命令加地址加数据,最后拉高片选。整个流程没有等待,写完片选一拉高数据就落盘了。
注意:MR25H40CDF 的写保护引脚 WP 和保持引脚 HOLD 在实际布线时不要悬空,建议 WP 上拉到 VCC,HOLD 也上拉,否则容易受干扰误触发写保护或暂停通信。
2. 硬件连接与 SPI 时序要点
2.1 引脚连接与电平匹配
PIC18LF45K22 的 MSSP 模块在 RC3/SCK、RC4/SDI、RC5/SDO 这几个引脚上,片选可以随便选一个普通 IO,我用的是 RA5。连接的时候注意几个细节:MRAM 的 VCC 加 0.1uF 去耦电容,尽量靠近芯片引脚;SPI 的三根线如果走线超过 10cm,建议串 22 到 33 欧姆的电阻做阻抗匹配,减少过冲。
电平方面,PIC18LF45K22 如果供 3.3V,那和 MRAM 直接连没问题。如果主控供 5V,MRAM 的输入引脚虽然标称耐压到 VCC+0.3V,但长期超压会缩短寿命,稳妥做法是用 74LVC1T45 这类电平转换芯片,或者用电阻分压(只适合低速场景)。我实测在 5V 主控下用 10k 加 20k 分压,跑 1MHz 时钟没问题,但再高就不稳了。
2.2 SPI 模式选择与时序参数
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。PIC18 的 SSPCON1 寄存器里 CKP 位控制时钟极性,CKE 位控制采样边沿。用模式 0 的话,CKP=0,CKE=0,数据在时钟上升沿采样,空闲时时钟为低。
时序上要注意几个时间参数。片选拉低到第一个时钟沿的建立时间 tSLCH 最小 5ns,这个基本不用管。关键是片选拉高到下一次片选拉低之间的时间 tSHSL,最小 20ns,在 10MHz 时钟下就是 0.2 个时钟周期,正常操作间隔远大于这个值。写命令发出后,片选必须拉高才能触发内部写入,如果片选一直保持低,写入不会生效。
// PIC18LF45K22 SPI 初始化,模式 0,时钟 Fosc/16 void SPI_Init(void) { TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SPI 主模式 Fosc/16 SSPSTAT = 0x00; // CKE=0, SMP=0 TRISAbits.TRISA5 = 0; // 片选引脚 PORTAbits.RA5 = 1; // 片选默认拉高 }2.3 片选控制与总线竞争
SPI 是总线型接口,多个从设备共享 SCK、SDI、SDO,靠片选区分。MRAM 的片选必须严格控制在每次操作前后拉低拉高,不能在两次操作之间一直保持低电平。我见过有人为了省事把片选一直拉低,结果读数据正常但写数据死活不成功,查了半天才发现是片选没拉高导致写命令没触发。
另外如果总线上还挂了其他 SPI 设备,比如 Flash 或者显示屏,要确保同一时刻只有一个片选有效。上电初始化的时候,先把所有片选拉高,再逐个初始化,避免某个设备在初始化过程中被误选中。
3. 驱动实现:从字节读写到页操作
3.1 底层字节收发函数
SPI 的底层就是一个字节换一个字节。PIC18 的 MSSP 模块发送时把数据写入 SSPBUF,然后等待 BF 位(SSPSTATbits.BF)置位表示接收完成,读走 SSPBUF 就行。注意发送和接收是同时进行的,即使你只想发不想收,也要读一次 SSPBUF 来清标志。
// SPI 字节交换 unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; while(!SSPSTATbits.BF); // 等待收发完成 return SSPBUF; }这个函数看着简单,但有个坑:如果时钟配置太快而 MRAM 还没准备好,读回来的数据可能是错的。稳妥做法是在片选拉低后加一个短延时,或者把时钟降到 5MHz 以下先调通再提速。
3.2 MRAM 命令集与地址格式
MR25H40CDF 的命令字很简洁。读数据是 0x03,写数据是 0x02,写使能是 0x06,读状态寄存器是 0x05,写状态寄存器是 0x01。地址是 24 位,因为 512KB 需要 19 位地址,但芯片按 24 位地址格式传输,高 5 位忽略。
读操作流程:片选拉低,发 0x03,发 24 位地址(高字节在前),然后连续读数据,读完拉高片选。写操作流程:片选拉低,发 0x06 写使能,拉高片选;再拉低片选,发 0x02,发 24 位地址,发数据,拉高片选触发写入。
// 读 MRAM 数据 void MRAM_Read(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; CS_LOW(); SPI_ExchangeByte(0x03); SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for(i = 0; i < len; i++) buf[i] = SPI_ExchangeByte(0x00); CS_HIGH(); } // 写 MRAM 数据 void MRAM_Write(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; CS_LOW(); SPI_ExchangeByte(0x06); // 写使能 CS_HIGH(); CS_LOW(); SPI_ExchangeByte(0x02); SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for(i = 0; i < len; i++) SPI_ExchangeByte(buf[i]); CS_HIGH(); // 拉高触发写入 }3.3 页写与跨页处理
MR25H40CDF 内部按 512 字节一页组织,页写的时候如果数据超过页边界,地址会自动回卷到页首,而不是跳到下一页。这个行为和 EEPROM 类似,是个容易踩的坑。比如从地址 0x1FE 开始写 4 个字节,实际会写到 0x1FE、0x1FF、0x000、0x001,而不是 0x1FE 到 0x201。
处理办法很简单:写之前先算一下当前地址到页尾还剩多少字节,如果剩余空间不够,就分两次写。我一般封装一个带跨页处理的写函数,内部自动拆分。
// 带跨页处理的写函数 void MRAM_WriteSafe(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int pageRemain; while(len > 0) { pageRemain = 512 - (addr % 512); if(pageRemain > len) pageRemain = len; MRAM_Write(addr, buf, pageRemain); addr += pageRemain; buf += pageRemain; len -= pageRemain; } }实操心得:虽然 MRAM 写入没有延迟,但连续写大量数据时建议每写 512 字节拉高一次片选,让芯片内部有个“喘息”的机会。我实测连续写 4KB 不分页,偶尔会出现最后几个字节读回来不对,分页写之后就没再复现过。
4. 掉电保存与数据完整性设计
4.1 掉电检测与写入时机
掉电保存的核心是“检测到掉电后,在电容放电完毕前把数据写完”。PIC18LF45K22 的 LVD(低电压检测)模块可以配置成在电压跌落到阈值时产生中断。我一般把 LVD 阈值设在 2.8V 左右(MRAM 最低 2.7V),检测到中断后立刻进入保存流程。
保存流程要尽量精简:关掉不必要的外设,直接调 MRAM 写函数,写完再进休眠。整个过程要在几百微秒内完成,所以后备电容的容量要算够。假设系统电流 10mA,电容从 3.3V 放到 2.7V,要撑 1ms,根据 I = C * dV/dt,C = I * dt / dV = 10mA * 1ms / 0.6V = 16.7uF。实际选 100uF 留足余量。
4.2 双备份与校验机制
工业场景里数据丢了就是事故,所以我在 MRAM 里做了双备份。地址 0x0000 到 0x00FF 存主数据,0x0100 到 0x01FF 存备份数据,每次写入先写主区再写备区,读取时两个区都读出来比对,不一致就以主区为准并触发告警。
每个数据块前面加 4 个字节的头部:2 字节魔数(0x5A5A)、1 字节长度、1 字节 CRC8 校验。读取时先验魔数和 CRC,通过才认为数据有效。这样即使写入过程中掉电导致数据不完整,也能识别出来。
// 带校验的数据块结构 typedef struct { unsigned int magic; // 0x5A5A unsigned char len; unsigned char crc; unsigned char data[60]; } DataBlock; // CRC8 计算 unsigned char CRC8(unsigned char *data, unsigned int len) { unsigned char crc = 0x00; unsigned int i; unsigned int j; for(i = 0; i < len; i++) { crc ^= data[i]; for(j = 0; j < 8; j++) { if(crc & 0x80) crc = (crc << 1) ^ 0x07; else crc <<= 1; } } return crc; }4.3 写入顺序与原子性保证
双备份的写入顺序有讲究。如果先写主区再写备区,写主区过程中掉电,主区可能损坏,但备区还是旧的完整数据,读取时发现主区 CRC 错,自动切到备区。如果先写备区再写主区,写备区时掉电,备区损坏,主区还是旧的完整数据,同样能恢复。
两种顺序都能保证至少有一份完整数据,但要注意读取时的判断逻辑:先读主区,CRC 通过就用主区;主区不通过再读备区,备区通过就用备区并把备区数据回写到主区;两个都不通过就报错,用默认值初始化。
注意:回写操作本身也可能掉电,所以回写之前先把备区数据复制到 RAM 里,确认复制完整再执行回写。不要在读取备区的过程中直接回写主区,那样如果回写时掉电,两份数据都可能损坏。
5. 常见问题排查与实测经验
5.1 读写失败问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读回全 0xFF | 片选未拉低或接线错误 | 示波器看片选和 SCK 波形 | 检查片选引脚配置和接线 |
| 读回全 0x00 | MISO 未接或上拉缺失 | 万用表测 MISO 对地阻值 | 检查 MISO 连线,必要时加上拉 |
| 写入后读回不变 | 写使能未发或片选未拉高 | 逻辑分析仪抓写时序 | 确认 WREN 命令和片选拉高 |
| 偶发数据错误 | 时钟太快或电源纹波大 | 降时钟到 1MHz 测试 | 加去耦电容,降低 SPI 时钟 |
| 跨页写入错位 | 未处理页边界回卷 | 检查写入地址和长度 | 使用带跨页处理的写函数 |
| 掉电后数据丢失 | 后备电容不足或 LVD 阈值不对 | 测掉电时 VCC 下降波形 | 加大电容,调整 LVD 阈值 |
5.2 实测中的几个坑
第一个坑是 SPI 时钟相位搞反。我一开始用模式 0 调不通,读回来全是 0xFF,后来换成模式 3 就正常了。后来查手册发现 MR25H40CDF 对模式 0 和模式 3 都支持,但我的 PIC 初始化代码里 CKE 位设错了,实际输出的是模式 1 的时序。所以调不通的时候先确认示波器看到的时序和手册要求是否一致,不要盲目改代码。
第二个坑是写保护引脚。有一批板子 WP 引脚悬空,结果在电机干扰下偶尔触发写保护,数据写不进去。后来把 WP 直接上拉到 VCC,问题消失。HOLD 引脚同理,悬空时容易受干扰导致通信暂停。
第三个坑是电源去耦。MRAM 在写入瞬间电流会有个小尖峰,如果去耦电容离得远,VCC 会瞬间跌落导致写入失败。我的做法是在 MRAM 的 VCC 和 GND 之间直接焊一个 0.1uF 的 0603 电容,距离不超过 2mm,另外再并一个 10uF 的钽电容做储能。
5.3 性能实测数据
在 PIC18LF45K22 跑 10MHz SPI 时钟的条件下,我实测了读写速度。单字节读大约 2.5us,单字节写大约 3us(含写使能命令)。连续读 512 字节耗时约 420us,连续写 512 字节耗时约 480us。这个速度对于掉电保存几十到几百字节的场景完全够用,即使加上双备份和 CRC 校验,总时间也在 1ms 以内。
功耗方面,MRAM 读电流典型 3mA,写电流 8mA,待机电流 10uA。在电池供电的野外设备里,大部分时间处于待机,平均功耗可以控制在 100uA 以下,一颗 2000mAh 的锂电池能撑两年以上。
6. 工程化封装与移植建议
6.1 驱动分层设计
在实际项目里,我习惯把驱动分成三层:最底层是 SPI 字节收发,中间层是 MRAM 命令封装(读、写、写使能),最上层是应用接口(存参数、读参数、存日志)。这样换主控或者换存储芯片时,只需要改最底层和中间层,应用层不动。
应用层接口设计成“键值对”形式,比如Param_Save(KEY_CALIB, &calibData, sizeof(calibData)),内部根据键算出 MRAM 地址,加上头部和 CRC 再写入。读取时Param_Load(KEY_CALIB, &calibData, sizeof(calibData)),内部校验通过才返回成功。这样应用代码完全不关心底层是 MRAM 还是 EEPROM。
6.2 移植到其他平台的注意事项
如果从 PIC18 移植到 STM32 或者 ESP32,SPI 硬件操作会变,但 MRAM 的命令时序不变。移植时重点检查三点:SPI 模式是否匹配(模式 0 或 3)、片选控制是否严格、时钟频率是否在芯片支持范围内。STM32 的 HAL 库用HAL_SPI_TransmitReceive就能完成字节交换,ESP32 用spi_transaction接口,逻辑是一样的。
另外注意地址宽度。MR25H40CDF 是 24 位地址,如果换成容量更大的 MRAM 比如 16Mbit 的,地址还是 24 位,但如果是 64Mbit 的,地址可能变成 32 位,命令字也会不同。移植前先确认目标芯片的手册。
6.3 批量生产中的工装测试
小批量手工调试没问题,但批量生产时得有个工装来快速验证 MRAM 是否焊接正常。我做过一个简单的测试工装:用另一颗 PIC 做主控,通过弹簧针接触板子上的 SPI 测试点,上电后先读 MRAM 的 Device ID(MR25H40CDF 支持 0x9F 命令读 ID),读到的 ID 正确再执行一次全片读写测试,写 0x55 读回比对,写 0xAA 再读回比对,两步都通过就亮绿灯。
这个工装单板测试时间在 200ms 以内,产线上用起来很顺手。ID 读不到的基本是虚焊或者芯片方向贴反,读写比对失败的多半是去耦电容没焊好或者 SPI 走线有短路。
实操心得:工装测试的时候不要只测一颗芯片,最好把板子上所有 SPI 设备都遍历一遍。我遇到过 MRAM 正常但 Flash 虚焊的情况,如果只测 MRAM 就漏检了。遍历测试虽然多花几十毫秒,但能挡住更多不良品。
7. 写在最后的一点个人体会
这套 MRAM 加 PIC18 的方案我前后用了三个项目,从工业流量计到电力监测终端,最长的已经连续运行两年多,没有出现过数据丢失。MRAM 的价格比 EEPROM 贵不少,但省掉了电池和复杂的掉电保护电路,整体成本反而更低,而且免维护这一点在野外场景里太重要了。
如果你也在做类似的数据存储需求,我的建议是先把掉电时序用示波器测清楚,确认电容能撑到数据写完,再考虑双备份和校验。很多人一上来就堆冗余,结果基础时序没调通,冗余逻辑反而增加了出错概率。先把最简单的单区读写跑稳,再逐步加保护机制,这样每一步都可控。
另外 MRAM 虽然标称无限次擦写,但也不建议在循环里无节制地写。我一般会在 RAM 里做一层缓存,攒够一定量或者检测到掉电才真正写入 MRAM,这样既减少总线占用,也降低意外干扰导致误写的风险。这个策略在低功耗场景里尤其有用,因为每次写 MRAM 都会拉高功耗,攒批写入能让系统大部分时间待在休眠模式。