☰
MRAM与PIC18F86J15 SPI驱动实现及掉电保护数据记录方案
2026/10/5 3:19:16 网站建设 项目流程

1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18F86J15

工业现场的数据记录仪、电力监控终端、医疗设备参数存储模块,这类场景有一个共同的痛点:数据写入频繁、掉电随时可能发生、现场环境温度跨度大、设备要求十年以上不换电池。传统方案里,EEPROM 擦写寿命十万次左右,铁电存储器容量小且贵,带后备电池的 SRAM 又要考虑电池老化和更换问题。我最初接手这个项目时,客户的要求很直接——每秒记录一次关键参数,设备寿命内不能丢数据,掉电瞬间正在写的那条记录也不能损坏。

选型阶段我对比了几条路线。SPI NOR Flash 便宜、容量大,但写入前要擦除扇区,写入延迟高,而且擦写寿命通常只有十万次量级,按每秒一次算,单扇区很快就写穿了。EEPROM 字节级写入方便,但同样受限于擦写次数,而且写入周期在毫秒级,频繁写会拖慢主循环。FRAM 读写速度快、寿命近乎无限,但容量普遍偏小,价格也偏高。最后落到MRAM上,具体型号就是MR25H40CDF,4Mbit 容量,SPI 接口,最关键的是它没有擦写寿命的概念——写入次数理论上无限,写入延迟在几十纳秒量级,而且掉电后数据保持二十年以上。

主控这边选PIC18F86J15,理由也很实际。这颗芯片是 Microchip 的 8 位增强型单片机,自带硬件 SPI 模块、64KB Flash、3936 字节 RAM,工作电压 2.0V 到 3.6V,工业级温度范围 -40 到 +85 摄氏度。它的 SPI 模块支持主从模式,波特率可以通过寄存器分频配置,配合 MRAM 的 40MHz 最高时钟完全够用。更重要的是,PIC18 系列在工业现场存量巨大,开发工具链成熟,客户那边的维护工程师也熟悉,后续产线烧录和现场升级都不需要重新培训。

这个组合的核心价值在于:用 MRAM 解决"写不死、掉电不丢"的问题,用 PIC18F86J15 解决"控制简单、成本可控、工业可靠"的问题。两者通过 SPI 总线连接,硬件上只需要四根线(SCK、SDI、SDO、CS),软件上就是标准的 SPI 读写时序。适合谁参考?做工业数据记录、设备参数存储、掉电保护型应用的嵌入式工程师,尤其是那些被 EEPROM 擦写寿命和掉电数据完整性折磨过的朋友。

2. 硬件连接与 SPI 时序细节拆解

2.1 引脚连接与硬件设计要点

MR25H40CDF 是 8 引脚封装,实际用到的信号线不多,但每一根都有讲究。先看连接关系:

MR25H40CDF 引脚功能PIC18F86J15 对应引脚说明
CS片选RC2(可配置为普通 IO)低电平有效,必须由主控控制
SCK时钟RC3/SCK硬件 SPI 时钟输出
SI数据输入RC5/SDO主出从入
SO数据输出RC4/SDI主入从出
VDD电源3.3V2.7V 到 3.6V 范围
VSS地GND就近接地
WP写保护接 VDD 或 IO 控制低电平禁止写入
HOLD保持接 VDD低电平暂停通信

这里有几个容易踩坑的地方。WP 引脚很多人直接悬空,结果发现写不进去数据,因为悬空电平不确定,芯片可能处于写保护状态。我的做法是接一个 10K 上拉到 VDD,如果应用需要动态写保护,就用一个 IO 口控制,正常写入时拉高,关键配置写完后拉低锁定。HOLD 引脚同理,不用的时候必须拉高,否则 SPI 通信会在中途被暂停,表现为读出来的数据错位或者全 FF。

PCB 布局上,MRAM 尽量靠近主控,SPI 走线控制在 10 厘米以内。SCK 是时钟线,最容易引入干扰,走线要避免和电机驱动、继电器控制线平行。我一般会在 SCK 和 CS 上各串一个 22 欧姆电阻,靠近主控端放置,抑制过冲和振铃。电源引脚旁边放一个 0.1uF 陶瓷电容加一个 1uF 钽电容,MRAM 在写入瞬间电流会有小幅波动,去耦不到位会导致写入失败。

2.2 SPI 模式与波特率配置

MR25H40CDF 支持 SPI 模式 0 和模式 3,也就是 CPOL=0/CPHA=0 或者 CPOL=1/CPHA=1。我选模式 0,因为 PIC18F86J15 的硬件 SPI 在模式 0 下最稳定,时序余量最大。具体来说,空闲时 SCK 为低电平,数据在 SCK 上升沿采样,下降沿输出。

波特率方面,MRAM 最高支持 40MHz,但 PIC18F86J15 在 3.3V 供电、系统时钟 40MHz 的情况下,SPI 时钟通过 SSPADD 寄存器分频。计算公式是:

Fosc / (4 * (SSPADD + 1))

假设系统时钟 Fosc 为 40MHz,想要 10MHz 的 SPI 时钟,SSPADD 应该设为 0,因为 40MHz / 4 = 10MHz。如果系统时钟是 16MHz,想要 4MHz,SSPADD 设为 0 得到 4MHz,设为 1 得到 2MHz。实际项目中我一般跑 5MHz 到 10MHz,太快了在长走线或者干扰环境下容易出错,太慢了影响数据记录吞吐。

配置代码大致如下,用 C 语言基于 Microchip XC8 编译器:

// SPI 初始化,模式 0,时钟 Fosc/4 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 (主模式 Fosc/4) SSPSTAT = 0x00; // SMP=0, CKE=0,模式 0 SSPADD = 0; // 分频值 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC4 = 1; // SDI 输入 TRISCbits.TRISC2 = 0; // CS 输出 CS = 1; // 初始拉高

这段代码里SSPSTAT的CKE位决定数据在时钟的哪个边沿输出,模式 0 下 CKE=0,SMP=0 表示在数据末尾采样。很多人配置完发现读出来全是 0xFF,八成是模式搞错了,或者 CS 没有正确拉低。

2.3 读写命令集与地址组织

MR25H40CDF 的指令集很精简,常用的就几条:

指令名称指令码功能说明
WREN0x06写使能,写入前必须发送
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

地址是 19 位,因为 4Mbit 等于 512KB,需要 19 根地址线。发送地址时要分三个字节:第一个字节的高 3 位是无效的,低 5 位是地址的 A18 到 A14,第二个字节是 A13 到 A6,第三个字节是 A5 到 A0。这个细节很容易搞错,我第一次写驱动的时候就是地址移位没对齐,读出来的数据整体偏移了一个扇区。

读操作的完整时序是:CS 拉低,发送 0x03,发送三个字节地址,然后连续读取数据,每读一个字节 SCK 产生八个脉冲,读完拉高 CS。写操作多一步:先发 WREN(0x06),拉高 CS,再拉低 CS,发 0x02,发地址,发数据,最后拉高 CS。注意 WREN 和 WRITE 之间必须有一次 CS 的上升沿,否则写使能不生效。

3. 驱动实现与数据存储策略

3.1 底层读写函数实现

先写最基础的字节收发函数,这是所有操作的地基:

unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; while (!SSPIF); // 等待传输完成 SSPIF = 0; return SSPBUF; }

这个函数看着简单,但有个隐患:如果 SPI 模块因为故障没有置位 SSPIF,这里会死循环。工业现场我一般加一个超时计数,超过一定次数就返回错误码,让上层决定是否复位 SPI 模块。

读数据函数:

void MRAM_Read(unsigned long addr, unsigned char *buf, unsigned int len) { CS = 0; SPI_ExchangeByte(0x03); SPI_ExchangeByte((addr >> 16) & 0x07); // 只取低 3 位有效 SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (unsigned int i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0x00); } CS = 1; }

写数据函数:

void MRAM_Write(unsigned long addr, unsigned char *buf, unsigned int len) { CS = 0; SPI_ExchangeByte(0x06); // WREN CS = 1; CS = 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0x07); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (unsigned int i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS = 1; // 等待写入完成,MRAM 写入很快,但保险起见查状态 while (MRAM_IsBusy()); }

MRAM_IsBusy读状态寄存器的 WIP 位,MRAM 的写入周期极短,通常几十纳秒,实际测试中几乎查不到忙状态,但保留这个检查是良好习惯。

3.2 掉电保护型数据记录结构设计

数据记录不能裸写,要有结构。我设计了一个环形缓冲区加双备份头的方案。存储空间从 0x00000 开始,前 4KB 放元数据,后面是数据区。

元数据区结构:

偏移地址长度内容
0x00004 字节魔数 0x4D52414D("MRAM")
0x00044 字节写指针,指向下一个写入位置
0x00084 字节记录总数
0x000C4 字节元数据 CRC32
0x00104 字节备份魔数
0x00144 字节备份写指针
0x00184 字节备份记录总数
0x001C4 字节备份 CRC32

每次写入一条记录,流程是:先写数据区,更新写指针和记录总数,计算 CRC,写入主元数据,再写入备份元数据。掉电如果发生在写主元数据之后、备份之前,上电时主备不一致,以 CRC 正确的那个为准。如果两个都坏了,说明掉电发生在更早的阶段,数据区那条记录可能不完整,通过记录本身的 CRC 校验丢弃即可。

每条记录的结构:

typedef struct { unsigned long timestamp; // 时间戳,秒 unsigned char type; // 记录类型 unsigned char data[27]; // 数据载荷 unsigned short crc; // 记录 CRC16 } LogRecord; // 共 34 字节

34 字节对齐到 32 字节边界,每条记录占 64 字节,方便地址计算。512KB 减去 4KB 元数据,大约能存 8000 条记录,环形覆盖。

3.3 写入性能实测与优化

实测数据:SPI 时钟 10MHz 下,写一条 34 字节记录,包括 WREN、地址、数据、状态查询,总耗时约 45 微秒。读一条记录约 38 微秒。每秒写一次,CPU 占用率不到 0.01%,完全无压力。

如果追求更高吞吐,可以去掉每次写前的 WREN,改成上电时发一次 WREN,之后连续写。但这样有风险,如果中间发生干扰导致状态机跑飞,后续写入会全部失败。我的做法是保留每次 WREN,45 微秒的代价换可靠性,值得。

还有一个优化点:MRAM 支持页写吗?严格说 MR25H40CDF 没有页的概念,它是真正的随机访问,任意地址连续写都行,不需要考虑页边界。这是它比 Flash 和 EEPROM 省心的地方,驱动里不用处理跨页拆分。

4. 常见问题排查与实战避坑指南

4.1 读出来全是 0xFF 或 0x00

这是最高频的问题。排查顺序如下:

  1. CS 有没有拉低。用示波器看 CS 引脚,读操作期间必须是低电平。如果 CS 一直是高,MRAM 根本不响应。
  2. SPI 模式对不对。模式 0 和模式 3 搞反了,数据采样边沿错位,读出来就是全 FF 或全 00。用逻辑分析仪抓 SCK 和 SI,看数据在哪个边沿变化。
  3. WP 引脚电平。WP 为低时写入被禁止,但读操作不受影响。如果读都失败,先查 CS 和模式。
  4. 地址移位错误。19 位地址分三个字节发送,如果第一个字节没有屏蔽高 5 位,地址会错到别的区域,读出来可能是空白区。

我遇到过最隐蔽的一次,是 PCB 上 CS 走线和 SCK 走线挨得太近,SCK 的跳变耦合到 CS 上,导致 CS 在通信中途被干扰出一个窄脉冲,MRAM 状态机复位,后续数据全错。解决办法是 CS 走线包地,或者在 CS 上并一个 100pF 电容到地,滤掉窄脉冲。

4.2 写入后读出来数据不对

写入成功但读出来不一致,常见原因:

  • WREN 和 WRITE 之间 CS 没有拉高。MRAM 要求 WREN 之后必须有一个 CS 上升沿来锁存写使能,如果 CS 一直保持低,WRITE 命令会被忽略。
  • 写入后立即读取,没有等待。虽然 MRAM 写入极快,但如果 SPI 时钟太快,写入周期还没结束就读,可能读到旧数据。加一个状态查询或者几微秒延时。
  • 电源电压偏低。MRAM 写入时电流比读大,如果电源去耦不足,写入瞬间电压跌落,导致写入失败。用示波器看 VDD 引脚,写入时不能有超过 100mV 的跌落。

4.3 掉电后数据丢失或元数据损坏

掉电保护方案再完善,也怕一种情况:掉电发生在写元数据的过程中,主元数据写了一半,CRC 对不上,备份元数据还是旧的,上电后以备份为准,但备份的写指针落后一条记录。这条记录的数据区已经写了,但元数据没更新,相当于丢了一条。

我的处理方式是:数据区写入后,先更新备份元数据,再更新主元数据。这样掉电最坏情况是主元数据旧、备份新,上电时以备份为准,数据不丢。如果掉电发生在更新备份的过程中,备份 CRC 坏,主元数据还是旧的,同样以主为准,数据也不丢。只有主备都坏的情况才会丢,而这种情况概率极低,因为两次写入之间有足够的时间间隔。

另外,元数据区我用了两个独立的 4KB 块,物理地址隔开,避免单点故障。MRAM 本身没有坏块概念,但 PCB 上的干扰可能只影响某个地址段,物理隔离能降低同时损坏的概率。

4.4 常见问题速查表

现象可能原因排查方法解决措施
读全 FFCS 未拉低 / 模式错误示波器看 CS 和 SCK检查 CS 控制,确认 SPI 模式 0
读全 00MISO 未接 / 芯片未供电万用表测 VDD 和 MISO检查供电和连线
写入无效WP 为低 / WREN 未生效测 WP 电平,抓 WREN 时序WP 上拉,WREN 后加 CS 上升沿
数据错位地址移位错误对比读写地址屏蔽地址高 5 位
偶发错误电源干扰 / 走线耦合示波器看电源纹波加去耦电容,CS 包地
掉电丢数据元数据更新顺序错误模拟掉电测试先备份后主,加 CRC 校验

5. 工业场景下的可靠性加固与扩展思路

5.1 温度与长期稳定性考量

MR25H40CDF 的工业级版本支持 -40 到 +85 摄氏度,数据保持 20 年。但在实际现场,温度循环会导致 PCB 应力变化,焊点疲劳。我的经验是,MRAM 芯片底部不要铺大面积铜皮,避免热膨胀系数不匹配导致焊点开裂。如果设备工作在振动环境,芯片四周点红胶固定。

长期稳定性方面,MRAM 的数据保持不需要刷新,也不依赖电荷,所以没有电荷泄漏问题。但要注意,强磁场环境可能影响 MRAM 的磁隧道结状态。虽然 MR25H40CDF 有磁屏蔽设计,但在电机、变压器附近安装时,还是尽量远离强磁场源,或者加一个坡莫合金屏蔽罩。

5.2 与上位机的数据交互设计

设备记录的数据最终要传到上位机。PIC18F86J15 有两个 UART 模块,我用 UART1 做调试输出,UART2 做数据上传。上传协议自定义,帧格式:

字节内容
0帧头 0xAA
1命令码
2-3数据长度
4-N数据载荷
N+1CRC16 低字节
N+2CRC16 高字节

上位机发命令读取指定范围的记录,下位机从 MRAM 读出后打包上传。如果记录量大,分页传输,每页 512 字节,上位机确认后再传下一页。这个协议简单可靠,现场调试时用串口助手就能手动发命令验证。

5.3 固件升级与参数配置的存储分离

MRAM 除了存记录,还可以存设备参数和固件升级标志。我把存储空间划分成三个区:参数区(0x00000-0x00FFF)、记录区(0x01000-0x7FFFF)、升级区(0x80000-0x7FFFF)。参数区存设备 ID、校准系数、通信地址等,升级区存新固件的标志和校验值。

固件升级流程:上位机把新固件通过 UART 传到升级区,写一个升级标志到参数区,然后复位。Bootloader 启动时检查升级标志,如果有效,就把升级区的固件搬运到程序 Flash,清除标志,跳转到新程序。这个方案利用了 MRAM 掉电不丢的特性,升级过程中断电也不会变砖,重新上电继续升级。

5.4 实操心得与避坑清单

最后分享几条我在这个项目里踩过的坑和总结的经验:

  • SPI 时钟先慢后快。调试阶段用 1MHz,确认读写正常后再逐步提高到 10MHz。直接上高频,出了问题很难判断是时序还是硬件。
  • CS 控制用宏定义。#define CS LATCbits.LATC2,方便统一修改,也避免直接操作寄存器时手误。
  • 每次写入前读一次状态寄存器。虽然 MRAM 几乎不忙,但这个习惯能提前发现 SPI 通信异常。
  • 元数据双备份加 CRC。这是掉电保护的底线,不要省。
  • 预留调试接口。PCB 上留出 SCK、SDI、SDO、CS 的测试点,现场排查时直接夹逻辑分析仪。
  • 记录结构体加填充字节。对齐到 4 字节边界,读写效率更高,也方便后续扩展字段。

这个方案目前已经在几个工业现场跑了两年多,没有出现过数据丢失。MRAM 的价格虽然比 EEPROM 高,但省掉了电池、省掉了擦写寿命计算、省掉了掉电保护电路,综合成本反而更低。如果你也在做类似的数据记录项目,这套组合值得认真考虑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询