1. 为什么 MRAM 在工业嵌入式场景里越来越受关注
搞过工业数据采集或者电力终端的朋友应该都有体会,选存储芯片这件事,看着简单,实际上坑特别多。EEPROM 擦写次数撑不住高频写入,Flash 写入前要擦除整个扇区、掉电还容易丢数据,FRAM 容量小价格贵,铁电那套东西供货又不稳定。这几年 MRAM 慢慢进入大家的视野,尤其是 Everspin 的 MR25H40CDF 这颗 4Mbit 的 SPI MRAM,在工业现场设备、计量仪表、PLC 模块里用得越来越多。
我最近一个项目正好用 PIC18LF45K50 搭配 MR25H40CDF 做了一套数据记录方案,需求说起来不复杂:每 100ms 采一次传感器数据,带时间戳存起来,断电不能丢,设备要能连续跑五年以上,现场环境温度 -40 到 85 度。听起来是不是挺常规的?但真选起存储来,EEPROM 的 100 万次擦写寿命按 100ms 一次算,不到 28 小时就写爆一个字节地址了,就算做磨损均衡也扛不住。Flash 更别提,扇区擦除的延迟和掉电风险在工业现场就是定时炸弹。
MR25H40CDF 这颗芯片的核心价值就在这:真正的非易失性 MRAM,写入不需要擦除,字节级随机写入,擦写次数 10^14 次以上,写入延迟纳秒级,掉电数据保持 20 年以上。这几个参数放在一起,基本就是为工业高频记录场景量身定做的。而 PIC18LF45K50 作为 Microchip 的 8 位增强型 MCU,自带 MSSP 模块支持 SPI 主模式,3.3V 供电和 MRAM 天然匹配,低功耗特性也适合电池备份的终端设备。
这篇文章我打算把整个方案从选型逻辑、硬件连接、SPI 时序配置、驱动代码、到实际调试中踩的坑,完整地讲一遍。不管你是刚接触 SPI 存储的新手,还是想从 EEPROM 迁移到 MRAM 的老手,应该都能从里面找到能直接抄作业的东西。下面所有代码和参数都是我在实际项目里跑通的,不是纸上谈兵。
2. 方案整体设计与关键选型考量
2.1 为什么是 MRAM 而不是 EEPROM 或 Flash
先把三种存储介质在工业记录场景下的表现摆出来对比,这样选型逻辑一目了然。
| 特性 | EEPROM (如 24LC512) | NOR Flash (如 W25Q64) | MRAM (MR25H40CDF) |
|---|---|---|---|
| 擦写寿命 | 100 万次 | 10 万次 | 10^14 次以上 |
| 写入方式 | 字节级 | 扇区擦除后写入 | 字节级 |
| 写入延迟 | 5ms 典型 | 擦除 50ms+ 写入 0.7ms | 纳秒级,无等待 |
| 掉电风险 | 写入中掉电丢当前字节 | 擦除中掉电丢整扇区 | 写入即完成,无窗口 |
| 容量 | 通常 512Kbit 以内 | 几 Mbit 到几百 Mbit | 4Mbit (本型号) |
| 接口 | I2C/SPI | SPI | SPI |
| 单价 | 低 | 低 | 较高 |
从表里能看出来,MRAM 唯一的劣势就是价格。但工业设备算的是全生命周期成本:EEPROM 方案要加磨损均衡算法、要加掉电检测和电容储能保证写完、要预留更大的存储空间做冗余,软件复杂度和 BOM 成本一叠加,MRAM 的溢价就没那么刺眼了。而且现场设备一旦因为存储失效返修,一次上门成本就够买几百颗 MRAM 了。
2.2 PIC18LF45K50 的 SPI 资源匹配分析
PIC18LF45K50 这颗 MCU 在低功耗 8 位机里算是很能打的。它内部集成了 MSSP(Master Synchronous Serial Port)模块,可以配置成 SPI 主模式,最高时钟频率是 Fosc/4。假设我们用 16MHz 外部晶振,系统时钟 16MHz,SPI 时钟最高能到 4MHz。MR25H40CDF 的 SPI 接口最高支持 40MHz,所以 MCU 这边完全不是瓶颈。
这里有个细节要注意:PIC18LF45K50 的 MSSP 在 SPI 主模式下,时钟极性(CKP)和时钟边沿(CKE)的组合决定了数据在哪个边沿采样。MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。我一般选 Mode 0,因为逻辑分析仪抓波形的时候看着最直观,SCK 空闲低电平,数据在上升沿采样。
另外 PIC18LF45K50 的引脚耐压要注意,它是低电压版本,VDD 范围 1.8V 到 3.6V,典型 3.3V。MR25H40CDF 的 VDD 也是 2.7V 到 3.6V,两者可以直接对接,不需要电平转换。如果你用的是 5V 的 PIC18F45K50(注意型号里没有 L),那就必须加电平转换芯片或者电阻分压,否则会打坏 MRAM。
2.3 硬件连接与 PCB 布局要点
接线本身不复杂,SPI 四线制加一个片选:
- PIC18LF45K50 的 RC3/SCK 接 MR25H40CDF 的 SCK
- RC5/SDO 接 MRAM 的 SI(MOSI)
- RC4/SDI 接 MRAM 的 SO(MISO)
- 任意 GPIO,比如 RA5,接 MRAM 的 CS#(片选,低有效)
- VDD 和 VSS 分别接 3.3V 和地,VDD 旁边放 0.1uF 去耦电容,越近越好
PCB 布局这块我有几个血泪教训。第一,SPI 时钟线 SCK 一定要远离模拟信号线,尤其是如果你板子上有 ADC 采样的话,4MHz 的时钟串扰过去能把 12 位 ADC 的低 4 位搅得乱七八糟。第二,CS# 走线不要和 SCK 平行长距离走,否则片选信号上会耦合出时钟毛刺,导致 MRAM 误触发。第三,MRAM 的 VDD 去耦电容我一般放两颗,一颗 0.1uF 陶瓷电容紧贴引脚,一颗 1uF 的在附近,对付工业现场的电源波动很有效。
注意:MR25H40CDF 的 CS# 必须在每次 SPI 事务开始时拉低,事务结束后拉高。如果你把 CS# 一直拉低,MRAM 内部状态机会一直以为在传输中,后续命令全部错乱。这个坑我在第一次调试时踩过,查了半天以为是时序问题,其实是片选没管好。
3. MR25H40CDF 核心操作与时序细节
3.1 命令集与地址空间划分
MR25H40CDF 是 4Mbit 容量,也就是 512K 字节,地址范围 0x00000 到 0x7FFFF,需要 19 位地址。它的命令集很精简,常用的就几条:
| 命令名称 | 命令码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写操作前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
状态寄存器里主要关注 WEL(写使能锁存)和 BP0/BP1(块保护位)。上电默认 WEL 是 0,所以任何写操作之前必须先发 WREN 命令把 WEL 置 1。写完一个字节或者一页之后,WEL 会自动清零,下次写还得重新发 WREN。这个机制是防止误写的,但如果你写连续数据忘了每次发 WREN,就会出现"第一段写进去了,后面全没写进去"的诡异现象。
地址空间我一般这样划分:前 256 字节做设备配置区,存序列号、校准参数、设备 ID;中间 256K 字节做循环记录区,用环形缓冲区的方式存传感器数据;最后留 256 字节做掉电保护区和日志区。这样划分的好处是配置区和记录区物理隔离,记录区写满了循环覆盖也不会碰到配置。
3.2 SPI 时序配置的实操细节
PIC18LF45K50 的 MSSP 配置成 SPI 主模式,关键寄存器是 SSPCON1 和 SSPSTAT。我实际用的配置值如下:
// SPI 主模式初始化,Fosc/16,Mode 0 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0010 (Fosc/16) SSPSTAT = 0x40; // CKE=0, SMP=0 (数据在时钟中间采样) TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC4 = 1; // SDI 输入这里 SSPM 选 0010 是 Fosc/16,16MHz 系统时钟下 SPI 时钟是 1MHz。为什么不选 Fosc/4 跑 4MHz?因为工业现场线缆可能比较长,1MHz 的边沿速率更温和,EMI 表现更好,而且 MRAM 本身写入没有延迟,1MHz 完全够用。实测 1MHz 下写 512 字节只要 4ms 左右,对于 100ms 的采集周期绰绰有余。
SSPSTAT 的 SMP 位我设成 0,意思是数据在时钟周期的中间采样。如果设成 1 是末尾采样,高速下容易采到不稳定的数据。CKE=0 配合 CKP=0 就是标准的 Mode 0。
3.3 字节读写与页写机制
MR25H40CDF 支持页写,一页是 256 字节。页写的意思是你可以连续发最多 256 个字节,地址在页内自动递增,跨页会回卷到页首。这个"回卷"特性是个双刃剑:如果你一次写超过 256 字节且没手动处理跨页,数据会覆盖到页首,把前面的数据冲掉。
我的做法是封装一个MRAM_Write函数,内部自动处理跨页:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { while (len > 0) { uint16_t page_remain = 256 - (addr & 0xFF); uint16_t chunk = (len < page_remain) ? len : page_remain; CS_LOW(); SPI_Transfer(0x06); // WREN CS_HIGH(); CS_LOW(); SPI_Transfer(0x02); // WRITE SPI_Transfer((addr >> 16) & 0x07); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (uint16_t i = 0; i < chunk; i++) { SPI_Transfer(buf[i]); } CS_HIGH(); addr += chunk; buf += chunk; len -= chunk; } }注意每次跨页都要重新发 WREN 和 WRITE 命令,因为 CS# 拉高后 WEL 会清零。这个逻辑我调试的时候改了三版才稳定,第一版忘了重新发 WREN,第二版地址高位算错了,第三版才跑通。
读操作就简单多了,发 READ 命令加 24 位地址(实际只用 19 位,高 5 位忽略),然后连续读就行,地址自动递增,没有页限制。
4. 完整驱动实现与数据记录方案
4.1 底层 SPI 收发函数
PIC18LF45K50 的 MSSP 收发是同一个寄存器 SSPBUF,写进去启动发送,读出来是接收数据。标准写法:
uint8_t SPI_Transfer(uint8_t data) { SSPBUF = data; while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; // 清标志 return SSPBUF; }这个函数看着简单,但有个隐藏问题:如果 SPI 时钟配置太快,而 MRAM 那边还没准备好,读回来的可能是旧数据。不过 MR25H40CDF 的 SPI 接口是纯组合逻辑,没有等待状态,1MHz 下完全不需要加延时。我在 4MHz 下测过也没问题,但为了工业环境的余量,还是保持 1MHz。
4.2 环形缓冲区记录逻辑
数据记录我用环形缓冲区,头指针和尾指针都存在 MRAM 的固定地址里,每次写入后更新头指针。这样掉电后重新上电,从头指针继续写,不会丢数据也不会覆盖未读数据。
#define RECORD_BASE 0x00100 #define RECORD_SIZE 0x40000 // 256K 字节 #define RECORD_LEN 16 // 每条记录 16 字节 typedef struct { uint32_t timestamp; int16_t sensor1; int16_t sensor2; uint16_t crc; uint8_t reserved[6]; } Record_t; void Record_Write(Record_t *rec) { uint32_t head = Get_Head_Ptr(); uint32_t addr = RECORD_BASE + (head * RECORD_LEN); if (addr + RECORD_LEN > RECORD_BASE + RECORD_SIZE) { addr = RECORD_BASE; // 回卷 head = 0; } MRAM_Write(addr, (uint8_t*)rec, RECORD_LEN); head++; Set_Head_Ptr(head); }每条记录 16 字节,包含 4 字节时间戳、2 个 2 字节传感器值、2 字节 CRC 和 6 字节保留。CRC 用 CRC-16/CCITT,校验整条记录,防止 MRAM 在极端环境下出现位翻转。虽然 MRAM 的可靠性很高,但工业设备该有的校验一个都不能少。
4.3 掉电保护与数据完整性
MRAM 最大的优势就是写入即完成,没有擦除窗口,所以掉电保护逻辑比 Flash 简单得多。但有一个细节要注意:头指针的更新必须在数据写入完成之后。如果先更新头指针再写数据,掉电时头指针指向了一条不完整的记录,下次上电读出来就是脏数据。
我的做法是:先写数据区,再写头指针,头指针写入前加一个"写入中"标志,写完清除。上电时检查这个标志,如果发现上次写入中断,就把头指针回退一位,重新写那条记录。
void Record_Write_Safe(Record_t *rec) { Set_Write_Flag(1); // 标记写入中 Record_Write(rec); // 写数据 Set_Head_Ptr(Get_Head_Ptr() + 1); // 更新头指针 Set_Write_Flag(0); // 清除标志 }这个标志本身也存在 MRAM 里,因为 MRAM 写入是原子的(纳秒级完成),不存在写一半掉电的情况,所以标志位本身是可靠的。
5. 调试实录与常见问题排查
5.1 读回全是 0xFF 或 0x00
这是最常见的现象。如果读回来全是 0xFF,说明 MISO 线一直是高电平,MRAM 根本没响应。排查顺序:先量 CS# 有没有正常拉低拉高,再量 SCK 有没有波形,最后量 MISO 在 CS# 有效期间有没有变化。我遇到过一次是 CS# 接错了引脚,配置成了模拟输入,数字输出驱动不了,拉不低。
如果读回来全是 0x00,可能是 SPI 模式配错了,数据在错误的边沿被采样。把 CKP 和 CKE 的组合换一下试试,Mode 0 和 Mode 3 都试一遍。
5.2 写入后读出来数据错位
数据错位通常是地址发送顺序搞错了。MR25H40CDF 是 24 位地址,高位在前。我见过有人先发低 8 位再发高 8 位,结果地址完全乱套。正确的顺序是:命令码 -> 地址高 8 位 -> 地址中 8 位 -> 地址低 8 位 -> 数据。
还有一种可能是页写回卷导致的。如果你一次写 300 字节,前 256 字节写在第 0 页,后 44 字节会回卷到第 0 页的开头,把前面的数据覆盖了。用我上面那个自动分页的函数就不会有这个问题。
5.3 长时间运行后数据丢失
这个在工业现场比较隐蔽。我遇到过一次,设备跑了三天后记录区数据开始出现零星错误。查下来是电源纹波太大,MRAM 在写入瞬间 VDD 跌到 2.5V 以下,虽然没掉电但写入不可靠了。解决办法是在 MRAM 的 VDD 引脚旁边加一颗 10uF 的钽电容,配合 0.1uF 陶瓷电容,把写入瞬间的电流需求兜住。
另外建议在软件里加一个简单的写入后回读校验,写完立刻读回来比对,不一致就重写。MRAM 写入快,重写成本几乎为零。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读回全 0xFF | CS# 未拉低 / MISO 断线 | 示波器量 CS# 和 MISO | 检查片选引脚配置和焊接 |
| 读回全 0x00 | SPI 模式错误 | 换 Mode 0/3 测试 | 调整 CKP/CKE 组合 |
| 数据错位 | 地址顺序错误 | 逻辑分析仪抓地址段 | 按高-中-低顺序发送 |
| 写入无效 | 忘记发 WREN | 抓命令码 | 每次写前发 0x06 |
| 跨页数据覆盖 | 页写回卷 | 检查写入长度 | 用自动分页函数 |
| 长期运行出错 | 电源纹波 | 示波器量 VDD | 加去耦电容,加回读校验 |
6. 几个容易被忽略的实操心得
第一个心得关于上电初始化。MR25H40CDF 上电后需要一段时间稳定,虽然数据手册说 100us 就够了,但我在工业现场遇到过电源缓慢上升的情况,MRAM 还没准备好 MCU 就开始发命令了。后来我在初始化里加了一个 10ms 的延时,再读一次设备 ID 确认通信正常,才开始正常读写。这个延时对启动时间影响可以忽略,但能避免很多偶发问题。
第二个心得关于状态寄存器的块保护位。MR25H40CDF 的 BP0 和 BP1 可以保护部分地址空间不被写入。如果你不小心把保护位设上了,写操作会静默失败——不报错,但数据没写进去。我建议在初始化时显式地把状态寄存器写成 0x00,确保全地址空间可写,然后再开始业务逻辑。
第三个心得关于SPI 时钟的相位余量。虽然 1MHz 下时序很宽松,但如果你板子走线比较长或者有干扰,可以在 SCK 和地之间并一颗 22pF 的小电容,把边沿稍微缓一下,能显著改善信号完整性。这个技巧在 EMC 测试整改时特别有用。
第四个心得关于数据记录的时间戳。PIC18LF45K50 没有 RTC,我用的是外部 32.768kHz 晶振加 Timer1 做秒计数,再配合一个 32 位软件计数器做毫秒级时间戳。时间戳存在 MRAM 里,掉电后重新上电从上次的值继续累加。这里要注意时间戳的字节序,我统一用大端存储,方便用逻辑分析仪直接看。
最后说一个关于容量规划的经验。4Mbit 看着不大,但按每条记录 16 字节、100ms 一条算,512K 字节能存 32768 条,也就是 54 分钟的数据。如果设备需要存更长时间,要么降低采样频率,要么用压缩算法。我一般会在记录区做两级存储:高频数据存 MRAM 环形缓冲区,满了之后批量转存到外部大容量存储,MRAM 只做掉电保护的最后一道防线。这样既发挥了 MRAM 写入快、掉电安全的优势,又规避了容量限制。
这套方案我在三个不同的工业项目里用过,最长的已经连续跑了两年多,记录区循环擦写了上百万次,没出过一次数据丢失。MRAM 的价格确实比 EEPROM 高,但在工业场景下,省下来的软件复杂度和现场维护成本,早就把差价赚回来了。如果你正在做类似的数据记录设备,不妨试试这个组合,调试通了之后你会发现,存储这块终于不用再提心吊胆了。