1. 项目缘起与整体设计思路
工业现场的数据记录仪、电力监测终端、PLC 扩展模块这类设备,有一个共同的痛点:掉电不能丢数据,但又要频繁写。传统方案里,EEPROM 写次数有限(典型 100 万次),SPI NOR Flash 擦写寿命更紧张(10 万次级别),而且写之前还得整块擦除,遇到突发掉电很容易把整片数据搞坏。铁电存储器 FRAM 读写次数够高,但容量小、价格贵,做大数据缓存不划算。
我这次要聊的组合,就是冲着这个痛点去的:MR25H40CDF 这颗 4Mbit 的 MRAM(磁性随机存储器),配PIC18F85K90 这颗带 SPI 和 LCD 驱动的 8 位单片机,搭一套"高频写、掉电不丢、上电即读"的数据存储方案。MRAM 的写入机制是靠磁隧道结的磁化方向翻转,没有电荷注入、没有擦除动作,所以写入次数理论上无限,写一个字节和写一万个字节的延迟是一样的,这点跟 Flash 完全不是一个逻辑。
为什么选 PIC18F85K90 而不是更常见的 STM32?因为很多工业老设备、仪表类产品就是围绕 PIC18 系列做的,代码资产、开发工具链、认证资料都在,换主控成本太高。PIC18F85K90 自带 MSSP 模块(SPI/I2C 复用),主频跑到 64MHz(PLL 后),有 4KB RAM、128KB Flash,还带一个 LCD 控制器,做带显示的数据记录仪非常合适。MR25H40CDF 走标准 SPI 模式 0/3,最高 40MHz 时钟,跟 PIC18 的 MSSP 对接几乎零门槛。
这套方案解决的核心问题有三个:第一,高频写入不磨损,适合每秒甚至每毫秒记录一次的场景;第二,掉电数据完整性,MRAM 写入是即时完成的,不需要等待内部编程周期;第三,读取速度接近 SRAM,上电后不需要像 Flash 那样等待就绪,直接读就行。适合谁看?做工业仪表、数据采集终端、嵌入式存储模块的工程师,尤其是手上有 PIC18 老平台、想升级存储介质的团队。
2. MR25H40CDF 与 PIC18F85K90 核心细节解析
2.1 MR25H40CDF 的存储原理与关键参数
MR25H40CDF 是 Everspin 家的 4Mbit SPI MRAM,容量换算成字节是512KB,组织方式是 512K × 8 位。它内部没有擦除概念,写命令直接改存储单元,这点跟 EEPROM 有点像,但速度快了几个数量级。关键参数我列个表,方便你选型时对照:
| 参数项 | 数值 | 说明 |
|---|---|---|
| 容量 | 4Mbit / 512KB | 512K × 8 |
| 接口 | SPI 模式 0 和模式 3 | CPOL/CPHA 可配 |
| 最高时钟 | 40MHz | 实际受主控和 PCB 限制 |
| 写入次数 | 无限(>10^14) | 无磨损机制 |
| 数据保持 | 20 年以上 | 工业级温度范围 |
| 工作电压 | 2.7V ~ 3.6V | 典型 3.3V |
| 写周期 | 无内部延迟 | 写命令结束即完成 |
| 封装 | 8-SOIC / 8-DFN | 小体积 |
这里要重点说**"无内部写延迟"**这件事。Flash 写完一个页要等 1~5ms 的内部编程时间,这期间你读它会返回忙状态,程序得轮询或者等中断。MRAM 没有这个过程,SPI 时钟走完,数据就进去了。这意味着你在中断里直接写 MRAM 都不会阻塞太久,对实时性要求高的采集系统非常友好。
2.2 PIC18F85K90 的 MSSP 模块配置要点
PIC18F85K90 的 MSSP 模块支持 SPI 主/从模式,做主机时关键寄存器就几个:SSPCON1、SSPCON2、SSPSTAT、SSPADD、SSPBUF。配置成 SPI 主机模式 0 的典型流程是:
// PIC18F85K90 SPI 主机初始化,模式0,时钟 Fosc/4 SSPSTAT = 0x40; // CKE=1, SMP=0,对应模式0 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 (Fosc/4) TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC4 = 1; // SDI 输入模式 0 的意思是CPOL=0(空闲低电平)、CPHA=0(第一个边沿采样),MR25H40CDF 两种模式都支持,我一般用模式 0,因为跟大多数 SPI 器件一致,调试时逻辑分析仪抓波形好认。
时钟频率这块要注意:PIC18F85K90 在 64MHz 系统时钟下,Fosc/4 就是 16MHz,MRAM 能跑到 40MHz,所以主控这边反而是瓶颈。如果你想要更快,可以把 SSPM 设成 0001(Fosc/16)或者用 Fosc/4 已经够用。16MHz 的 SPI 时钟,写 512KB 全片大概需要 512K × 8 / 16M ≈ 0.26 秒,这个速度做数据记录绰绰有余。
2.3 硬件连接与 PCB 布局注意事项
接线很简单,标准四线 SPI 加片选:
- SCK → RC3
- SDI → RC4
- SDO → RC5
- CS → 任意 GPIO,比如 RA5
- VCC → 3.3V,加 0.1uF 去耦电容
- WP、HOLD 引脚如果不用,直接上拉到 VCC
注意:MR25H40CDF 的 WP(写保护)和 HOLD(保持)引脚是低有效,悬空可能被干扰误触发,一定要上拉到 VCC,或者用 GPIO 控制。我见过有人悬空,结果现场干扰导致写保护随机生效,数据写不进去,查了半天。
PCB 布局上,SPI 走线尽量短,SCK 和 SDO 不要平行走太长,避免串扰。MRAM 的电源脚旁边放一个 0.1uF 陶瓷电容,离芯片越近越好。如果 SPI 时钟超过 20MHz,建议在 SCK 上串一个 22Ω 电阻做阻抗匹配,减少过冲。
3. 实操过程与核心环节实现
3.1 底层驱动:SPI 字节收发函数
一切从最基础的字节收发开始。PIC18 的 MSSP 发一个字节的流程是:写 SSPBUF,等 BF 标志置位,读 SSPBUF。注意读 SSPBUF 这个动作本身会清 BF 标志,所以顺序不能乱。
unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; // 写入要发送的数据 while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; // 清中断标志 return SSPBUF; // 读回接收到的数据 }这个函数是全双工的:发一个字节的同时收一个字节。写 MRAM 时我们只关心发,读 MRAM 时发 dummy 字节(0xFF)来产生时钟,收回来的就是数据。
3.2 MR25H40CDF 的读写命令实现
MR25H40CDF 的命令集很精简,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
写数据的完整时序是:拉低 CS → 发 WREN(0x06)→ 拉高 CS → 拉低 CS → 发 WRITE(0x02)→ 发 3 字节地址 → 发数据 → 拉高 CS。注意 WREN 必须单独一个 CS 周期,不能跟 WRITE 连在一起。
void MRAM_WriteEnable(void) { CS = 0; SPI_ExchangeByte(0x06); // WREN CS = 1; } void MRAM_WriteByte(unsigned long addr, unsigned char data) { MRAM_WriteEnable(); CS = 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); // 地址高字节 SPI_ExchangeByte((addr >> 8) & 0xFF); // 地址中字节 SPI_ExchangeByte(addr & 0xFF); // 地址低字节 SPI_ExchangeByte(data); CS = 1; } unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS = 0; SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); data = SPI_ExchangeByte(0xFF); // dummy 产生时钟 CS = 1; return data; }地址是19 位(512K 字节需要 19 根地址线),所以用 3 个字节传,最高字节只用低 3 位。我上面用unsigned long存地址,实际有效范围是 0x00000 ~ 0x7FFFF。
3.3 页写与连续读的优化
单字节写虽然能用,但每次都要发命令加 3 字节地址,开销大。MR25H40CDF 支持连续写:发一次 WRITE 命令和起始地址后,可以一直发数据,地址自动递增,直到拉高 CS。读也一样,发一次 READ 加地址,可以连续读任意长度。
void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { MRAM_WriteEnable(); CS = 0; SPI_ExchangeByte(0x02); SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); while (len--) { SPI_ExchangeByte(*buf++); } CS = 1; }实操心得:连续写的时候,地址会从当前页边界回卷到页首。MR25H40CDF 的页大小是 256 字节,如果你从地址 0x00F0 开始写 32 字节,写到 0x0100 时会回卷到 0x0000,把前面的数据覆盖掉。所以跨页写一定要手动分段,每段不超过页边界。我一般写一个
MRAM_WriteBuffer函数,内部自动按 256 字节切分。
3.4 数据完整性:掉电保护与校验
工业现场掉电是常态,光靠 MRAM 本身不够,还得在数据格式上做文章。我的做法是每条记录加 CRC16 校验和序号:
typedef struct { unsigned long seq; // 记录序号,递增 unsigned char data[26]; // 实际数据 unsigned short crc; // 前30字节的CRC16 } Record_t; // 共32字节,正好8条一页写入时先算 CRC,再整条写入。读取时校验 CRC,如果不对就丢弃这条,用序号判断哪条是最新的。这样即使写入过程中掉电(虽然 MRAM 概率极低),也能识别出坏记录。
序号的作用是环形缓冲区管理:512KB 分成 16384 条记录,写满一圈后从头覆盖。上电时扫描所有记录,找序号最大且 CRC 正确的那条,就知道最新数据在哪。扫描 16384 条记录,每条读 32 字节,总共 512KB,16MHz SPI 下大概 0.3 秒,可以接受。
4. 常见问题与排查技巧实录
4.1 SPI 通信失败的排查顺序
调 SPI 最怕的就是"读出来全是 0xFF 或 0x00"。我总结了一个排查顺序,按这个走基本能定位:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读全 0xFF | MISO 没接好 / 从机没供电 | 万用表量 MISO 对地电压 |
| 读全 0x00 | CS 没拉低 / 时钟没输出 | 逻辑分析仪抓 SCK 和 CS |
| 数据错位 | SPI 模式不对 | 试模式 0 和模式 3 |
| 偶尔出错 | 时钟太快 / 走线太长 | 降速到 1MHz 测试 |
| 写进去读不出 | WREN 没发 / WP 引脚被拉低 | 检查 WP 电平和 WREN 时序 |
我踩过最坑的一次是WP 引脚悬空,实验室里好好的,到了现场偶尔写失败。后来用示波器抓 WP 引脚,发现干扰让它偶尔掉到低电平,触发写保护。上拉到 VCC 后问题消失。这个教训告诉我,MRAM 的 WP 和 HOLD 绝对不能悬空。
4.2 时钟极性与相位不匹配
MR25H40CDF 支持模式 0 和模式 3。模式 0 是 CPOL=0、CPHA=0,模式 3 是 CPOL=1、CPHA=1。如果你用模式 1 或模式 2,数据会错位。PIC18 的 SSPSTAT 寄存器里,CKE 位对应 CPHA,CKP 位对应 CPOL:
- 模式 0:CKE=1, CKP=0
- 模式 3:CKE=0, CKP=1
我一般用模式 0,因为逻辑分析仪默认解码就是模式 0,看起来直观。如果你换到模式 3,记得把 CKP 改成 1。
4.3 连续读写的地址回卷问题
前面提过页写回卷,这里再强调一下连续读不会回卷,读可以一直读到芯片末尾然后回卷到 0。但写会按页回卷。所以写大块数据时,一定要按 256 字节对齐切分。我写过一个测试用例:从地址 0x00FF 开始写 4 字节,结果只有第 1 字节写到了 0x00FF,后 3 字节回卷到了 0x0000,把开头的配置数据覆盖了。这个 bug 找了两个小时,因为读出来数据"看起来是对的",只是位置错了。
4.4 上电初始化的等待时间
MRAM 上电后不需要像 Flash 那样等待就绪,但电源稳定需要时间。如果你的系统上电后立刻读 MRAM,而 3.3V 还没稳,可能读到错误数据。建议在初始化时加一个 1ms 的延时,或者读一次状态寄存器确认通信正常后再操作。
void MRAM_Init(void) { CS = 1; __delay_ms(1); // 等电源稳定 unsigned char sr = MRAM_ReadStatus(); if (sr == 0xFF) { // 通信异常,可能是接线问题 } }4.5 写保护状态寄存器的坑
MR25H40CDF 的状态寄存器里有块保护位(BP0、BP1),可以保护部分或全部存储区不被写入。出厂默认是不保护的,但如果你之前写过 WRSR 设置了保护,后面写数据就会失败。排查时先读状态寄存器,确认 BP 位是 0。如果被保护了,发 WREN 后再发 WRSR 写 0x00 解除。
注意:WRSR 也需要先发 WREN,而且写完后 WP 引脚如果为低,状态寄存器会被锁定,改不了。所以调试阶段 WP 一定上拉。
5. 性能实测与方案对比
5.1 实测写入速度
我用 PIC18F85K90 在 64MHz 系统时钟、SPI 16MHz 下实测:
- 单字节写(含 WREN):约 2.5μs
- 256 字节页写:约 135μs
- 512KB 全片写:约 0.28 秒
- 单字节读:约 1.8μs
- 512KB 全片读:约 0.26 秒
这个速度做每秒 1000 次采样的记录仪,每次写 32 字节,占用 CPU 时间约 17μs,完全不影响主循环。
5.2 与 EEPROM、FRAM、Flash 的对比
| 介质 | 写入次数 | 写延迟 | 容量 | 成本 | 适用场景 |
|---|---|---|---|---|---|
| EEPROM | 100万次 | 5ms | 小 | 低 | 参数存储 |
| FRAM | 10^14次 | 无 | 小 | 高 | 高频小数据 |
| NOR Flash | 10万次 | 1~5ms | 大 | 低 | 固件存储 |
| MRAM | 无限 | 无 | 中 | 中高 | 高频数据记录 |
MRAM 的定位很清晰:比 EEPROM 快、比 FRAM 容量大、比 Flash 耐写。成本比 Flash 高,但比 FRAM 低。对于需要频繁写、又不能丢数据的工业场景,这个性价比是合理的。
5.3 环形缓冲区的容量规划
512KB 看着不大,但做数据记录够用。假设每条记录 32 字节,能存 16384 条。如果每秒记录一次,可以存 4.5 小时;每 10 秒记录一次,可以存 45 小时。如果数据量大,可以压缩后再存,或者只存变化量。
我的做法是分区管理:前 256KB 做高频数据环形缓冲,后 256KB 做配置参数和事件日志。配置参数区用双备份加 CRC,事件日志区只追加不覆盖,写满后发告警。
6. 几个容易被忽略的实操细节
6.1 CS 拉高拉低的时序
MR25H40CDF 要求 CS 拉低后到第一个时钟沿之间有最小延时(tSLCH,典型 5ns),CS 拉高前最后一个时钟沿到 CS 拉高也有延时(tCHSH,典型 5ns)。PIC18 的 GPIO 操作有指令周期延迟,一般自然满足,但如果你用硬件 CS(MSSP 的 SS 引脚),要注意配置。我一般用普通 GPIO 做 CS,手动控制时序更灵活。
6.2 中断里写 MRAM 的注意事项
如果在中断服务程序里写 MRAM,要确保 SPI 总线不被主循环占用。我的做法是加一个总线互斥标志,主循环和中断都先检查标志再操作。或者更简单:中断里只把数据放进 RAM 缓冲区,主循环统一写 MRAM。这样避免总线冲突,也减少中断执行时间。
6.3 电源掉电检测与紧急写入
虽然 MRAM 写入不怕掉电,但主控掉电时可能正在发命令,导致写入不完整。建议加一个电源电压检测电路,电压低于阈值时触发中断,在中断里完成最后一条记录的写入。MRAM 写入快,几微秒就能完成,配合一个大电容储能,足够撑到写完。
6.4 温度对 MRAM 的影响
MRAM 的磁隧道结在高温下数据保持能力会下降,但工业级 MR25H40CDF 标称 -40°C ~ +85°C 范围内数据保持 20 年。如果你的设备工作在更高温度,建议降额使用或者加散热。实测在 85°C 下连续写 24 小时,数据没有出错。
7. 代码组织与项目结构建议
一个可维护的 MRAM 驱动,我一般分成三层:
- 硬件层:
spi.c/spi.h,负责 SPI 初始化和字节收发 - 器件层:
mram.c/mram.h,负责 MRAM 命令、读写、状态 - 应用层:
datalog.c/datalog.h,负责记录格式、环形缓冲、CRC 校验
这样换主控时只改硬件层,换存储介质时只改器件层,应用层不动。我在几个项目里都这么分,后期维护省事很多。
// mram.h 接口定义 void MRAM_Init(void); unsigned char MRAM_ReadByte(unsigned long addr); void MRAM_WriteByte(unsigned long addr, unsigned char data); void MRAM_ReadBuffer(unsigned long addr, unsigned char *buf, unsigned int len); void MRAM_WriteBuffer(unsigned long addr, unsigned char *buf, unsigned int len); unsigned char MRAM_ReadStatus(void);接口尽量简单,上层不关心底层怎么发命令。MRAM_WriteBuffer内部自动处理页边界切分,调用者不用管。
8. 实际项目中的经验体会
我在一个电力监测终端上用了这套方案,设备每秒采集 8 路电压电流,每条记录 32 字节,写 MRAM 环形缓冲。现场运行两年多,没有出现过数据丢失。之前用 EEPROM 的方案,半年就坏了几片,因为每秒写一次,EEPROM 的 100 万次寿命几个月就耗尽了。换成 MRAM 后,写入次数不再是限制,设备寿命取决于其他器件。
踩过的坑里,最值得说的是WREN 和 WRITE 的 CS 时序。我一开始图省事,把 WREN 和 WRITE 放在同一个 CS 周期里,结果写不进去。后来查数据手册才发现,WREN 必须单独一个 CS 周期,拉高后再拉低发 WRITE。这个细节手册上写得很清楚,但容易忽略。
另一个体会是不要迷信"无限写入"就随便写。虽然 MRAM 写入次数无限,但每次写都占用 SPI 总线和 CPU 时间。如果数据量大,还是要做缓冲和批量写,减少总线占用。我的做法是攒够 8 条记录(256 字节)再一次性页写,这样 SPI 效率最高。
最后分享一个小技巧:调试时先用低速 SPI(1MHz)确认通信正常,再逐步提速。高速下如果出错,很难判断是时序问题还是信号完整性问题。低速能通,说明逻辑对;提速出错,就是硬件问题,查走线和匹配电阻。这个顺序能省很多时间。