1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18
1.1 一个真实场景引出的存储需求
前阵子帮一个做工业数据采集的朋友处理一块老设备的主控板,板子上跑着一颗 PIC18F85K90,外挂了一颗 EEPROM 用来存配置参数和掉电前的关键数据。问题出在现场:设备每次断电重启后,偶尔会丢几条记录,尤其是掉电瞬间正在写的那一笔,直接变成乱码。排查下来是 EEPROM 写入速度太慢,加上掉电时电源跌落时间不够,写操作没完成就被打断了。
这个场景其实在工业和嵌入式领域非常典型。很多设备需要频繁记录运行状态、累计计数、故障日志,而且要求掉电不丢、上电即读、写入次数足够多。传统方案无非几种:EEPROM 写入慢、寿命有限;SPI Flash 写入前要擦除、有块管理开销、擦写寿命也就十万次量级;铁电存储器 FRAM 速度快、寿命高,但容量小、价格偏高。而 MRAM(磁性随机存储器)这几年逐渐成熟,像 Everspin 的 MR25H40CDF 就是一颗 4Mbit 的 SPI MRAM,兼具 SRAM 级的写入速度、近乎无限的擦写寿命和掉电非易失特性,正好补上了这个缺口。
所以这个项目的核心目标很明确:用 PIC18F85K90 通过 SPI 总线驱动 MR25H40CDF,实现工业现场数据的可靠存储与读取。关键词里出现的 MR25H40CDF、PIC18F85K90、嵌入式、SPI、MRAM,基本就是这条技术链的全部要素。适合谁看?做工业控制、数据采集、仪表设备的嵌入式工程师,尤其是被掉电丢数据折磨过的人,这篇内容应该能帮你少走弯路。
1.2 为什么选 MR25H40CDF 而不是 EEPROM 或 Flash
先把三者的关键差异摆出来,选型逻辑就清楚了。
| 特性 | EEPROM(如 25LC 系列) | SPI Flash(如 W25Q 系列) | MRAM(MR25H40CDF) |
|---|---|---|---|
| 写入速度 | 毫秒级(页写 5ms 左右) | 毫秒级,且需先擦除 | 纳秒级,无擦除 |
| 擦写寿命 | 约 100 万次 | 约 10 万次 | 近乎无限(10^14 量级) |
| 掉电非易失 | 是 | 是 | 是 |
| 写入前擦除 | 不需要 | 必须 | 不需要 |
| 接口 | SPI | SPI | SPI |
| 容量 | 小 | 大 | 中等(4Mbit) |
| 单价 | 低 | 低 | 偏高 |
从表里能看出,MR25H40CDF 最大的价值在于“写入不用等、不用擦、不怕写坏”。工业现场那种每秒都在更新计数值、随时可能断电的场景,用 EEPROM 你得做复杂的掉电保护电路和写缓存,用 Flash 你得做磨损均衡和块管理,而 MRAM 直接像写 RAM 一样写进去就完事,掉电数据还在。这就是我最终选它的核心理由。
1.3 PIC18F85K90 的角色与 SPI 资源分配
PIC18F85K90 是 Microchip 的一款 8 位增强型单片机,自带 MSSP 模块,可以配置成 SPI 主模式。它在这个项目里干三件事:一是作为 SPI 主机发起读写时序;二是管理数据帧格式和地址映射;三是处理上电初始化与掉电前的数据落盘。
选它而不是更高级的 ARM,主要是成本和生态考虑。工业设备很多还是 8 位或 16 位主控,PIC18 系列资料全、工具链成熟、抗干扰能力也不错。它的 MSSP 模块支持 SPI 模式 0 和模式 1,时钟极性 CPOL 和相位 CPHA 可配,正好匹配 MR25H40CDF 的 SPI 模式 0(CPOL=0,CPHA=0)。这一点很关键,后面配置寄存器时会详细说。
提示:MR25H40CDF 只支持 SPI 模式 0 和模式 3,PIC18F85K90 的 MSSP 配置时务必确认 CPOL/CPHA 组合,配错了读出来全是 0xFF 或 0x00。
2. 硬件连接与 SPI 时序细节
2.1 引脚连接与片选处理
MR25H40CDF 是 8 引脚封装,关键引脚包括 VDD、VSS、SCK、SI、SO、CS#、WP#、HOLD#。和 PIC18F85K90 连接时,我用的映射如下:
- SCK 接 PIC 的 SCK 引脚(RC3,MSSP 时钟)
- SI 接 PIC 的 SDO(RC5)
- SO 接 PIC 的 SDI(RC4)
- CS# 接一个普通 GPIO(我用的是 RA5),软件控制片选
- WP# 和 HOLD# 直接上拉到 VDD,禁用写保护和保持功能
这里有个细节值得说:CS# 我用的是软件片选而不是硬件片选。原因有两个,一是 PIC18F85K90 的 MSSP 在 SPI 主模式下,硬件片选(SS)主要用于从模式,主模式下用软件控制更灵活;二是 MRAM 的片选时序要求 CS# 拉低后到第一个时钟沿之间要有一定建立时间,软件控制能精确插入延时。实测下来,软件片选在 10MHz 时钟下依然稳定。
注意:WP# 和 HOLD# 千万不要悬空,悬空会导致写入被意外保护或通信被挂起,直接上拉到 VDD 是最稳妥的做法。
2.2 SPI 模式与时序参数核算
MR25H40CDF 的数据手册明确写了支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我选模式 0,因为 PIC18F85K90 的 MSSP 在模式 0 下配置最简单,SCK 空闲为低,数据在上升沿采样、下降沿变化。
时序上要关注几个参数:SCK 周期最小 20ns(对应最高 50MHz),CS# 建立时间最小 5ns,CS# 保持时间最小 5ns,数据输出有效时间等。PIC18F85K90 在 64MHz 系统时钟下,SPI 时钟可以做到 16MHz(Fosc/4),完全在 MRAM 的能力范围内。我实际跑的是 10MHz,留足余量给 PCB 走线和干扰。
计算一下:SPI 时钟 10MHz,一个字节 8 位,传输时间 0.8μs。MR25H40CDF 的读操作是发一个命令字节加三个地址字节,然后连续读数据,所以读一个字节的总开销约 4 字节命令开销加 1 字节数据,约 4μs。写操作类似,命令加地址加数据,约 4μs 写完一个字节。这个速度比 EEPROM 的毫秒级快了三个数量级。
2.3 PCB 布局与信号完整性要点
SPI 虽然简单,但在 10MHz 以上还是要讲究布局。我的经验是:
- SCK 走线尽量短,避免和 SI/SO 长距离平行走线,减少串扰
- CS# 走线单独走,不要和时钟捆在一起
- MRAM 的电源引脚旁边放 0.1μF 陶瓷电容,越近越好
- 如果板子上还有其他 SPI 从设备,每个设备的 CS# 独立控制,不要共用
有一次我偷懒把 MRAM 和一颗 Flash 共用 CS#,结果两个设备同时被选中,数据总线打架,读出来全是错。后来改成独立片选,问题立刻消失。这个坑希望大家别踩。
3. 软件驱动实现:从初始化到读写
3.1 MSSP 模块初始化配置
PIC18F85K90 的 MSSP 初始化分几步:设置 TRIS 方向、配置 SSPCON1、设置时钟速率、使能模块。下面是我实际用的初始化代码片段。
// PIC18F85K90 MSSP SPI 主模式初始化 // 系统时钟 64MHz,SPI 时钟 Fosc/4 = 16MHz,实际用 Fosc/16 = 4MHz 起步 TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入 TRISA5 = 0; // CS# 输出 CS_MRAM = 1; // 初始拉高,不选中 SSPSTAT = 0x40; // 数据在上升沿采样,下降沿变化(模式0) SSPCON1 = 0x20; // SSPEN=1,SPI 主模式,时钟 Fosc/4 // 如需更慢时钟,改 SSPCON1 低两位:0x21=Fosc/16, 0x22=Fosc/64这里 SSPSTAT 的 SMP 位和 CKE 位决定采样时刻。模式 0 要求 SMP=0(上升沿采样)、CKE=0(下降沿变化),所以 SSPSTAT 写 0x40 是设置 SMP=0、CKE=0,同时保留其他位默认值。SSPCON1 的 SSPM3:0 设为 0b0000 是 Fosc/4,0b0001 是 Fosc/16,我一般先用慢速调通再提速。
3.2 字节收发函数与片选控制
SPI 收发在 PIC18 上就是写 SSPBUF 然后等 BF 标志。我封装了两个函数,一个发一个收,实际读的时候是发一个哑字节同时收。
unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; return SSPBUF; } void MRAM_CS_Low(void) { CS_MRAM = 0; // 插入短延时,满足 CS# 建立时间 __delay_us(1); } void MRAM_CS_High(void) { __delay_us(1); CS_MRAM = 1; }片选拉低后我加了 1μs 延时,虽然手册只要求 5ns,但实际 PCB 和引脚电容会让边沿变缓,多等一点更稳。这个延时在 10MHz 下对吞吐影响可以忽略。
3.3 MR25H40CDF 命令集与地址映射
MR25H40CDF 的命令集很简洁,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
地址是 24 位,因为 4Mbit 等于 512KB,需要 19 位地址,但 MRAM 用 24 位地址格式,高 5 位忽略。读操作时序是:CS# 拉低,发 0x03,发 24 位地址(3 字节),然后连续读数据,CS# 拉高结束。写操作类似,但写之前要先发 WREN。
这里有个和 EEPROM 不同的地方:MRAM 写操作不需要等待写完成,发完数据拉高 CS# 就结束了,没有轮询状态寄存器那一套。这是它速度快的重要原因。
3.4 读写的完整代码实现
下面是我实际用的读写函数,读和写都封装好了。
void MRAM_WriteByte(unsigned long addr, unsigned char data) { MRAM_CS_Low(); SPI_ExchangeByte(0x06); // WREN MRAM_CS_High(); MRAM_CS_Low(); SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); SPI_ExchangeByte(data); MRAM_CS_High(); } unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; MRAM_CS_Low(); SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); data = SPI_ExchangeByte(0x00); // 哑字节换数据 MRAM_CS_High(); return data; }写的时候先单独发 WREN 再拉高 CS#,然后再拉低发 WRITE 命令,这是 MRAM 的要求。有些朋友会想省掉中间那次拉高,直接把 WREN 和 WRITE 连在一起发,实测不行,MRAM 不认。这个细节手册里写得清楚,但容易忽略。
4. 数据可靠性与掉电保护策略
4.1 为什么 MRAM 还需要掉电保护
有人会问,MRAM 写入是纳秒级、掉电非易失,那还需要掉电保护吗?答案是:MRAM 本身不需要,但系统需要。因为掉电瞬间,单片机可能正在执行写操作,如果电源跌落太快,SPI 时序没走完,MRAM 收到的可能是半个命令或半个数据,导致写入不完整。所以掉电保护的重点不是 MRAM,而是保证写操作的原子性。
我的做法是在电源端加一个大电容(比如 1000μF),配合一个电压检测电路。当检测到输入电压低于阈值时,立刻触发中断,在电容供电的几毫秒内完成当前写操作并停止新写入。因为 MRAM 写一个字节只要几微秒,几毫秒足够写完几十个字节。
4.2 数据帧格式与校验设计
工业数据不能只存原始值,我设计了一个简单的帧格式,每个记录 16 字节:
- 2 字节帧头(0xAA55)
- 4 字节时间戳或序号
- 8 字节数据区
- 2 字节 CRC16 校验
写入时先写数据区,再写帧头,最后写校验。读取时先读帧头,如果帧头不对就跳过;帧头对了再读数据和校验,校验通过才认为有效。这样即使掉电导致某条记录写了一半,上电后也能识别出来并跳过,不会把乱码当数据用。
CRC16 我用的是 Modbus 多项式 0xA001,计算速度快,8 位机上跑几十微秒就够。这个校验在工业现场非常必要,我遇到过好几次因为电源波动导致的数据位翻转,有 CRC 就能发现。
4.3 写入次数与寿命管理
虽然 MRAM 寿命近乎无限,但也没必要往死里写。我的策略是分区域循环写:把 512KB 分成若干块,每块存固定条数的记录,写满一块后跳到下一块,全部写满后回到第一块覆盖最旧的数据。这样既实现了日志滚动,又避免了单一地址被反复写。
具体实现时,我在 MRAM 的第一个扇区存一个写指针,记录当前写到哪个块、哪个偏移。每次上电先读指针,然后从指针位置继续写。指针本身也是 MRAM,更新它同样不怕掉电。
提示:写指针更新频率高的话,可以每写 10 条记录更新一次指针,减少指针写次数。虽然 MRAM 不怕写,但减少操作总是好的。
5. 常见问题与排查实录
5.1 读出来全是 0xFF 或 0x00
这是最常见的现象,原因通常有三个:一是 SPI 模式配错,CPOL/CPHA 不对;二是片选没拉低或拉低时机不对;三是 SI/SO 接反。排查顺序是先量 SCK 有没有波形,再量 CS# 有没有拉低,最后用示波器看 SI 上有没有命令字节。我遇到过一次是 SO 和 SI 焊反了,读出来全是 0xFF,换了就好了。
5.2 写入后读出来不对
如果写进去和读出来不一致,先确认 WREN 有没有发成功。可以读状态寄存器(RDSR),看 WEL 位是不是 1。如果 WEL 是 0,说明 WREN 没生效,检查 CS# 时序。另一个可能是地址算错了,24 位地址的高位没处理好,导致写到别的地址去了。
5.3 高速时钟下通信不稳定
10MHz 以上如果出现偶发错误,多半是信号完整性问题。先降速到 1MHz 试试,如果降速后稳定,就是布局或走线问题。可以在 SCK 上串一个 22Ω 电阻,减缓边沿,减少反射。另外检查电源去耦电容有没有放够。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读出全 0xFF | SI/SO 接反、模式错 | 检查接线和 CPOL/CPHA |
| 读出全 0x00 | CS# 没拉低、MRAM 没供电 | 量 CS# 和 VDD |
| 写入无效 | WREN 没发、CS# 时序错 | 读状态寄存器 WEL 位 |
| 偶发错误 | 时钟太快、干扰 | 降速、加去耦、串电阻 |
| 掉电丢数据 | 电源跌落太快 | 加大电容、加电压检测 |
5.5 几个实操心得
第一,调试 SPI 一定要有逻辑分析仪或示波器,光看代码看不出时序问题。我用的是一台便宜的逻辑分析仪,抓 SPI 时序足够。
第二,MRAM 的 WP# 和 HOLD# 一定要处理,我见过有人悬空导致写入被保护,查了半天。
第三,上电初始化时先读一次 MRAM 的 ID 或固定地址,确认通信正常再开始业务逻辑,这样能早发现问题。
第四,如果系统里有多个 SPI 设备,每个设备的 CS# 独立控制,初始化时全部拉高,避免总线冲突。
6. 性能实测与扩展思路
6.1 实测数据
我在 10MHz SPI 时钟下做了连续读写测试,结果如下:
| 操作 | 耗时 |
|---|---|
| 写一个字节 | 约 4.2μs |
| 读一个字节 | 约 4.0μs |
| 写 16 字节记录 | 约 68μs |
| 读 16 字节记录 | 约 64μs |
| 连续写 1000 条记录 | 约 68ms |
这个速度对于工业数据采集完全够用,每秒可以写上万条记录。对比 EEPROM 的毫秒级写入,提升非常明显。
6.2 后续可以怎么扩展
如果数据量更大,可以换更大容量的 MRAM,比如 MR25H40 是 4Mbit,还有 16Mbit 的型号,接口兼容,改一下地址位数就行。如果主控升级到 16 位或 32 位,SPI 时钟可以拉到 50MHz,速度还能再快几倍。
软件上可以加一个简单的文件系统,把 MRAM 当成一个小型磁盘,支持按文件名读写。不过对于大多数工业场景,直接按地址读写加帧格式就够了,没必要上文件系统增加复杂度。
我在实际使用中的体会是,MRAM 这种器件最大的价值不是速度快,而是让嵌入式工程师从“怕掉电、怕写坏”的焦虑中解放出来。以前用 EEPROM 要算寿命、做缓存、加掉电检测,现在这些都可以简化。当然成本确实高一些,但对于数据可靠性要求高的场合,这个投入是值得的。