☰
MRAM替代EEPROM:PIC18F67K40工业设备掉电保存方案
2026/10/4 8:46:45 网站建设 项目流程

这个月给一台工业设备改固件,客户反复抱怨断电后配方参数偶尔全丢。原方案用串行 EEPROM 存参数,日志数据更没地方放,我直接把存储芯片换成了 MR25H40CDF,主控端用 PIC18F67K40,从底层 SPI 驱动到环形日志区整套重新写了一遍。这篇文章就是这次改造的完整记录,适合正在做嵌入式数据存储、设备参数掉电保存,或者想把日志方案从“能存”做到“存得住”的朋友参考。

MR25H40CDF 是 Everspin 的 4Mbit 串行 MRAM,PIC18F67K40 是 Microchip 的 8 位 MCU,两者在工业设备里都是很常见的组合。MRAM 最大的特点是没有擦写寿命限制,写入速度快,在数据需要频繁更新的场合比 EEPROM 和 NOR Flash 都合适。后面我会把硬件接线、SPI 驱动、数据格式设计、常见排查手段一步步拆开讲,代码直接可以抄过去改。

1. 为什么是 MR25H40CDF 加 PIC18F67K40

1.1 工业数据存储的三个硬需求

工业设备的存储需求跟消费电子不太一样,我先说三个最典型的场景:

第一是参数频繁更新。比如设备上的配方参数、校准系数,工人调机的时候可能几秒钟就写一次,一天下来写几千次很常见。普通 EEPROM 的擦写寿命一般是 100 万次,看着挺多,但按一天 3000 次算,一年多就逼近极限。NOR Flash 更麻烦,寿命只有几万次到十几万次,必须先擦后写,直接拿来做参数存储根本不现实。

第二是掉电数据不能丢。产线设备经常直接拉闸,如果数据正写到一半突然断电,EEPROM 和 Flash 都可能出现状态异常。MRAM 是磁阻存储,写入过程不需要等待电荷保存,掉电瞬间数据就已经稳定了,天然适合这种场景。

第三是读写的片外数据和主控之间的通信不能太复杂。PIC18F67K40 这种 8 位 MCU 算力有限,跑不了复杂的 Flash 文件系统,最好用简单的 SPI 读写就能搞定。MR25H40CDF 的接口就是标准 SPI,指令集简单,对 8 位机非常友好。

1.2 选型对比:MRAM 与 EEPROM、NOR Flash

我整理了一个对比表,方便大家理解 MRAM 在中间的位置:

项目串行 EEPROMNOR FlashMR25H40CDF(MRAM)
存储原理电荷保存电荷保存磁阻状态
写寿命约 100 万次1 万到 10 万次无实际磨损限制
写前擦除不需要需要不需要
写入等待页写 3~5ms擦除几百 ms 到秒级基本无等待
单条写长度按页 8~256 字节按扇区有限长度,跨页需拆帧
数据保持受温度影响受温度影响高温下仍能保持多年
接口SPI/I2CSPI/并口SPI

在 PIC18F67K40 这类的 8 位平台上,如果既要容量又要耐磨性,MRAM 几乎是唯一不用动架构的方案。选 4Mbit 是因为 512KB 空间能同时放下参数区和日志区,后续扩展也够用。很多人一听到 MRAM 就觉得贵,实际上在小批量工业设备里,一块 MR25H40CDF 带来的可靠性提升,分摊到售后成本里是很划算的。

选 PIC18F67K40 也没有特别复杂的原因:它自带 MSSP 外设可以跑 SPI,IO 电平 3.3V 和 MRAM 直接对接,而且这颗料在工业项目里供货稳定。更重要的是,团队对这个系列已经很熟,固件认证流程不用重做,这是项目稳定性里容易被忽略的一环。

2. 硬件连接:先把这个 8 脚芯片接明白

2.1 最小系统连接图

MR25H40CDF 是标准的 8 引脚 SPI 从设备,引脚功能如下:

  1. CS:片选,低有效,必须由 MCU 控制
  2. SCK:SPI 时钟
  3. SI:MOSI,MCU 输出到 MRAM
  4. SO:MISO,MRAM 输出到 MCU
  5. WP:写保护输入,低电平保护
  6. HOLD:通讯暂停输入,低电平暂停
  7. VCC:3.3V 供电
  8. VSS:地

和 PIC18F67K40 连接时,我把 CS 接到 RC2,SCK 接到 RC3,SI 接到 RC5,SO 接到 RC4。不同开发板的引脚分配不一样,重点不是具体用哪个引脚,而是必须确认这些引脚在 PIC 内部要正确配置成 SPI 功能的复用口。

特别注意 WP 和 HOLD 两个引脚。WP 如果被拉低,状态寄存器会被保护,WRITE 指令不生效。HOLD 如果被拉低,SPI 通信会暂停,数据线上的电平变化都会被忽略。这两个脚绝对不能悬空,实际电路里 VCC 和它们之间各接一个 10k 电阻拉到高电平。

2.2 电源、去耦与电平匹配

工业环境里的电源纹波比消费类产品大得多,MRAM 的 VCC 脚旁边必须放 100nF 陶瓷电容,尽量靠近芯片引脚,有条件再并一个 10uF 钽电容,吸收前端电源的毛刺。

电压匹配是整个项目里最容易踩的坑。MR25H40CDF 是 3.3V 供电,如果 PIC18F67K40 也跑 3.3V,那万事大吉,直连就行。如果板子上其他电路是 5V 逻辑,就不能直接把 MRAM 连到 5V 的引脚上。我的建议是加电平转换芯片,或者用电阻分压加二极管的方式处理 SI、SCK、CS,SO 方向因为 MRAM 是 3.3V 输出,5V MCU 往往能直接识别,但还是尽量加转换才算稳妥。

PCB 布局还有一个容易被忽略的点:SCK 和 SI 是高频信号,要尽量短,别来回绕,否则波形边沿变差,高速 SPI 时容易读到错误数据。CS 线一般变化频率低,可以稍微走远一点,但也不要和电源线平行长距离走线。

2.3 为什么要单独留一个掉电检测引脚

我在这次设计中额外用 PIC18F67K40 的 LVD 模块做断电检测,或者外接一个简单的电阻分压网络监测 VCC。目的很简单:断电瞬间把正在攒的日志记录写进 MRAM。

虽然 MRAM 本身写入很快,不需要像 Flash 那样预留擦除时间,但 MCU 检测到掉电到彻底没电之间还是需要几百微秒。PIC18F67K40 内置了可编程低压检测,可以在 VCC 掉到设定阈值前触发中断,在中断里把关键数据通过 SPI 写进去。如果系统没有这个引脚,至少要在主循环里周期检查 VCC 电压。

3. 软件驱动:PIC18F67K40 上把数据写进去再读出来

3.1 MSSP 外设初始化的思路

PIC18F67K40 的 MSSP 模块可以配置成 SPI 主机模式。初始化代码在 XC8 编译器下大致是下面这个逻辑:

void SPI_Init(void) { // 1. 引脚方向:SCK/SDO/CS 设为输出,SDI 设为输入 // 2. 关闭 MSSP 模块 // 3. 配置主模式、时钟极性空闲为低、采样点选择中间 // 4. 设置 SPI 时钟分频,建议先使用 Fosc/4 或更慢 // 5. 重新使能 MSSP }

我不在这里贴某个开发板的寄存器级代码,因为不同批次 PIC18F67K40 的寄存器命名和 MSSP 库函数略有差异,直接搬容易出错。关键是搞清楚这几步在做什么,然后对着数据手册把位填上。

SPI 模式要特别注意时钟极性和相位。MR25H40CDF 支持 SPI 模式 0 和模式 3,我习惯用模式 0,也就是空闲时 SCK 为低、第一个边沿采样。如果初始化错了,读出来的数据会莫名奇妙地全 0xFF 或者错位,而且不容易一眼看出来。

3.2 MR25H40 的基本指令与读写时序

MR25H40 的指令集和普通 SPI NOR Flash 很像,这里只列本次用到的几条:

指令命令字节功能
WREN0x06写使能,每次写操作前必须发
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据,可连续读
WRITE0x02写数据
SLEEP0xB9进入深度休眠
WAKE0xAB退出深度休眠

写数据时最标准的流程是:CS 拉低,发 WREN,CS 拉高,然后再次 CS 拉低,发 WRITE 命令,再发 24 位地址,最后发数据字节,写完 CS 拉高。很多人第一次写 MRAM 都会漏掉第一步的 WREN,结果数据怎么都写不进去,这是最常见的低级错误。

读数据要简单得多,不需要 WREN,直接 CS 拉低,发 READ 命令,发地址,然后连续读即可。READ 命令支持按任意长度连续读,读超过地址范围会回卷到起点。

3.3 写使能状态与写保护的处理

MR25H40 内部有一个状态寄存器,其中 WEL 位表示写使能锁存状态。发完 WREN 之后,WEL 会置 1,然后执行一次 WRITE 或者 WRDI 之后,WEL 自动清零。这意味着每次写操作之前都必须重新发一次 WREN。

我写驱动时加了一个简单函数来检查 WEL:

uint8_t MRAM_ReadSR(void) { uint8_t sr; CS_LOW(); SPI_ExchangeByte(0x05); // RDSR sr = SPI_ExchangeByte(0x00); CS_HIGH(); return sr; }

如果状态寄存器读回来 WEL 一直是 0,首先检查 WP 引脚是不是被拉低了,再检查 WRSR 有没有把块保护设置打开。工业现场干扰严重,MOSI 线上的毛刺可能影响写入数据内容,所以每次写完数据后,我都习惯读回来比对,而不是盲信写成功。

3.4 一个可以直接用的读写驱动代码

下面的代码是我在项目里的实际逻辑,去掉了工程依赖,保留核心流程:

#define CS_LOW() LATCbits.LATC2 = 0 #define CS_HIGH() LATCbits.LATC2 = 1 void MRAM_WriteEnable(void) { CS_LOW(); SPI_ExchangeByte(0x06); // WREN CS_HIGH(); } void MRAM_WriteByte(uint32_t addr, uint8_t dat) { MRAM_WriteEnable(); CS_LOW(); SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); SPI_ExchangeByte(dat); CS_HIGH(); MRAM_WaitWIP(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t d; CS_LOW(); SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); d = SPI_ExchangeByte(0x00); CS_HIGH(); return d; } void MRAM_WaitWIP(void) { uint8_t sr; do { CS_LOW(); SPI_ExchangeByte(0x05); sr = SPI_ExchangeByte(0x00); CS_HIGH(); } while (sr & 0x01); // bit0 为写进行中标志 }

这里有一个硬件细节要提:MR25H40 的 WRITE 指令支持连续写入,但单条写指令能连续写的字节数不是无限大,超过 8 字节边界后地址会回卷。所以如果你想在一个连续区域写入超过 8 字节的数据,不能图省事发一条长 WRITE,而是要按 8 字节边界拆成多帧。READ 没有这个限制,你可以一口气读任意长度。

3.5 操作 MRAM 的缓冲区设计建议

实际项目里很少用上面这种单字节读写的 API 去存业务数据,效率太低。我习惯在驱动层之上封装一套带缓冲区的小函数:

void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len); void MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint16_t len);

WriteBuffer 内部自动按每帧最多 8 字节拆分,然后依次调 MRAM_WriteEnable 和 MRAM_WriteByte。加这一层之后,上层做参数存储的时候就不用操心跨页回卷问题了。

如果 MCU 的 RAM 够用,还可以在掉电检测中断里用一个大数组把待写日志缓存起来,然后连续写进 MRAM。PIC18F67K40 的 RAM 有限,不能太奢侈,但几个扇区的日志缓冲是完全够用的。

4. 数据组织:如何设计一个掉电不乱的存储结构

4.1 分区规划:参数区和日志区各管各的

芯片选好了,驱动也通了,只能算“能读写”。真正决定产品稳定性的,是上层存储格式怎么设计。我这次把 512KB 空间分成两块:低地址区存参数,高地址区存日志。

参数区我用双镜像,也就是同一份参数存两份。启动时先读 A 区,校验失败就自动切到 B 区,如果 A、B 都校验失败,就用出厂默认参数。双镜像增加了成本,但 MRAM 容量大,多占一份空间换来的是固件更新断电时的容错能力。

日志区我采用环形覆盖方式,最新记录写到最后时回卷到开头,老记录被覆盖。这个设计对工艺设备的运行追溯很实用。参数区和日志区都通过一个分区表来管理:

区域地址范围用途
参数镜像 A0x00000 - 0x0FFFF当前参数主区
参数镜像 B0x10000 - 0x1FFFF当前参数备份区
日志区起始块0x20000 - 0x200FF日志索引区
日志数据区0x20100 - 0x7FFFF环形日志正文

如果不去规划,MRAM 虽然容量大,但日志区和参数区互相干扰,排查旧数据时定位不到具体位置,所以分区这一步值得认真做。

4.2 日志帧格式:CRC16 比什么都重要

日志区每一条记录,我定义成下面的结构体:

typedef struct __attribute__((packed)) { uint16_t magic; // 固定 0xA55A,用于识别帧起始 uint16_t seq; // 递增序号 uint16_t len; // payload 长度 uint16_t crc; // payload 的 CRC16 uint8_t payload[128]; // 业务数据 } LogRecord;

magic 字段的作用是快速定位帧边界。工业设备工况复杂,日志区可能出现半截记录,特别是掉电瞬间。启动时从头扫描,遇到 magic 不匹配就继续往后找,直到发现完整有效帧。

seq 字段用来判断写入到一半的残留帧。如果读到一条 seq 比上一条还小,说明这是断电前的脏数据,直接跳过并把它标记为可用空间。这听上去复杂,其实就是写一层很简单的状态判断,比依赖 MRAM 的“不掉电”特性更靠得住。

CRC16 我选的是常见的 CCITT 多项式 0x1021。计算范围只针对 payload,不含头部,这样改头部字段时不用重新计算整包 CRC。写入时先算好 CRC,再整体写进 MRAM;读取后重新计算比对,不一致就认为记录无效。

4.3 参数区的更新与回滚机制

参数区我用双备份加版本号的办法,具体流程是这样的:

  1. 写参数前,先读当前版本号。
  2. 新参数写进 A 区,版本号加 1。
  3. 再把同样参数写进 B 区,版本号相同。
  4. 启动时比较 A 区和 B 区的版本号,选择版本高的作为有效参数。

如果写入 A 区后掉电,B 区还是旧版本,下一轮启动时版本号高的那区可能不完整,所以还需要 CRC 校验配合:只有版本高且 CRC 正确的区才被采用。这样即使掉电发生在任意一帧写一半的时刻,也不会出现整个参数丢失的情况。

4.4 一次完整的写入与读取调用流程

我把上层调用设计得很简单:

int Param_Save(struct DeviceParam *p); int Param_Load(struct DeviceParam *p);

Param_Save 内部做 BCD 编码、加版本号、加 CRC、按双镜像写入 MRAM。Param_Load 启动时调用,自动完成版本选择、CRC 校验、回滚默认参数。

这里有一个经验:不要在设备的每个线程里都直接操作 MRAM。每次参数写入虽然很快,但并发访问会乱掉。我单独设立一个存储任务,其他模块通过队列把写请求发给它。PIC18F67K40 这种 8 位 MCU 没有操作系统,用消息队列也足够。

5. 现场排查:实测中最容易踩的 5 个坑

5.1 读回全是 0xFF 或 0x00

读回全 FF,绝大多数原因是 SPI 引脚初始化不对,或者 CS 没有正确拉低。我曾遇到过一次,困扰半天,最后发现是 PIC18F67K40 的 MSSP 复用引脚没有切换成 SPI 功能,默认是数字 IO,CS 拉低了 MRAM 也没反应。

还有一个隐蔽原因:WP 或 HOLD 悬空。HOLD 一旦被噪声拉低,MRAM 会忽略 SPI 时钟,读出来就是未定义值。把这两个引脚用 10k 电阻上拉到 VCC,问题马上消失。

5.2 写不进去,状态寄存器 WEL 一直为 0

这个坑太经典了,每次培训都会被问到。WRITE 指令前必须先发 WREN,而且 CS 必须按“低-高-低”切换,保证 WREN 指令完整结束。部分代码把 WREN 的 CS 拉高动作漏掉了,MSSP 会认为两条指令是连续的,状态寄存器里的 WEL 根本不会置位。

再检查一遍 WP 引脚。如果 WP 被拉低,MRAM 的写保护区域是受保护的,即使 WREN 成功也不能写。我见过调试板上 WP 接到了地,找半天找不到原因。

5.3 SPI 速率上不去,波形边沿太丑

MRAM 本身支持很高 SPI 时钟,但实际速率受限于 PCB 走线和 MCU 驱动能力。我最初把 SPI 时钟配到 Fosc/2,结果读回来的数据偶尔错位,用示波器一看,SCK 波形上升沿有严重过冲。把分频降到 Fosc/4,问题消失。

如果必须跑高速,建议从芯片出去到 MRAM 之间串 33Ω 电阻,抑制信号反射。SCK 线尽量避开大电流电源线,单片机的地和 MRAM 地之间不要有细长走线。

5.4 掉电瞬间最后一条记录损坏

MRAM 理论上不怕掉电,但整个链路上还有 MCU 和 SPI 总线。掉电瞬间 VCC 跌落,MCU 可能在半电压状态下乱发 SPI 数据,导致 MRAM 写入一个错误帧。

我的解决方法是软件和硬件配合:硬件上用 LVD 中断提前捕获掉电,软件上在日志头部加 magic 和 CRC,启动时遇到无效记录就停止回卷,不再继续往后写。这样即使最后一条脏了,也只是丢掉一条,不影响前面所有数据。

5.5 上电自检时发现整个芯片数据全变样

遇到一次很诡异的问题:设备运行一段时间后参数区大量数据错乱。后来定位到是 GPIO 初始化顺序的问题。PIC18F67K40 上电后,如果 MRAM 的 CS 引脚在初始化前处于低电平,而 SCK、SI 上同时出现过毛刺,MRAM 会误判为一次写指令,把内部数据改成随机值。

解决方式很简单:MCU 上电后第一时间把所有 MRAM 相关引脚配置为确定的数字输出,CS 默认拉高,再初始化 SPI 模块。所有 SPI 外设,Flash、EEPROM、MRAM 都一样,CS 在初始化前必须保持高电平,这条规矩我吃了不少教训才记住。

6. 我在实际项目里的一些体会

这套方案跑下来之后,我的体会还是那句老话:存储芯片选型解决大部分问题,但存储格式设计才能解决剩下的问题。MR25H40CDF 把“写不进、寿命短、等待长”这些硬伤都抹平了,PIC18F67K40 的资源也刚好够用,真正要小心的反而是分层、校验、掉电时序这些看起来不起眼的地方。

最后再分享一个小技巧:量产自检时,我在 MRAM 的高地址区固定存了一串测试数据,上电后读出来和预设模式比对。这个步骤成本几乎为零,但能提前暴露虚焊、贴片偏移、IO 连接不良。我在产线上靠这个办法抓到了三块坏板子,比麻烦客户回来返修强太多了。

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

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

立即咨询