ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MRAM替代EEPROM:基于PIC18F86K22的SPI MRAM嵌入式存储方案

2026/10/4 4:33:33 拓冰建站 浏览量
MRAM替代EEPROM:基于PIC18F86K22的SPI MRAM嵌入式存储方案 做嵌入式最怕的不是功能写不出来而是数据存不住。前段时间接一个工业控制器的维护项目客户报故障很头疼设备跑几天配置参数偶尔丢有时候继电器动作次数统计也莫名其妙清零。拆开看主控是 Microchip 的 PIC18F86K22外挂一颗 24C256 EEPROM。问题就出在 EEPROM 上——长期高低温、频繁写入写入寿命耗尽数据开始扛不住。后来我把存储芯片换成 Everspin 的 MR25H40CDF一颗 4Mbit 的 SPI MRAM整套读写代码用 SPI 总线重新写了一遍问题一次解决。这篇文章就完整记录这套方案为什么选 MRAM、怎么接、怎么驱动、怎么在工业现场稳定干活。1. 先搞清楚MR25H40CDF 和 PIC18F86K22 各自干什么1.1 MR25H40CDF 是什么凭什么能在工业环境长期扛MR25H40CDF 属于 Everspin 的串行 SPI MRAM 家族容量 4Mbit也就是 512K 字节。它不是 Flash也不是 EEPROM而是磁阻随机存取存储器。内部存储单元用磁隧道结实现有磁性材料通过改变磁矩方向来表达 0 和 1。这个磁性状态在断电之后不会消失所以它天然是非易失的。为什么工业现场喜欢 MRAM核心三条写入寿命极长、写入速度快、不需要擦除。写入寿命EEPROM 和 NOR Flash 都有磨损上限常见规格是 10 万到 100 万次MRAM 理论上可以无限次读写实测里连续写几百万次数据纹丝不动完全不需要写均衡这种软件补偿。写入速度EEPROM 按页或按字节写要等几毫秒NOR Flash 更麻烦要先擦除整个扇区MRAM 的写入过程本质上和 SRAM 写操作接近在 SPI 总线上发完地址和数据数据就进去了没有内部“编程时间”。不需要擦除这一条在做日志记录时特别香。Flash 必须先擦后写日志系统要么做环形缓冲要么做磨损均衡MRAM 想写哪里就写哪里按字节、按连续块都行驱动逻辑立刻简化一大截。另外 MR25H40CDF 是工业级器件常见工作温度范围能覆盖 -40℃ 到 85℃在继电器控制柜、电机驱动、光伏逆变器这种恶劣环境下很稳。后缀 CDF 一般对应贴片封装和某个温度/可靠性等级具体对应关系选型时以数据手册为准不用太纠结命名规律。1.2 PIC18F86K22为什么选这颗 8 位机PIC18F86K22 是 Microchip 的增强型 8 位 PIC18 系列80 引脚资源在 8 位平台里算很能打的一类。选它不是因为性能有多强而是因为项目本来就基于这个平台换主控风险大客户也不愿意动老产线。这其实也是嵌入式工程师经常面对的现实很多时候不是选最新最贵的芯片而是选一个够用、熟手、供货稳定的方案。PIC18F86K22 自带 MSSP 模块而且不止一个SPI、I2C 都可以配。我只用了其中一个 MSSP 做 SPI 主机速率可以从低频一直调到 MHz 级对 MR25H40CDF 这种支持几十兆时钟的芯片绰绰有余。80 引脚版本 IO 多片选随便拿一个引脚就能控制不需要像某些芯片一样必须用固定引脚硬件设计很灵活。开发环境是 MPLAB X IDE 加 XC8 编译器C 代码写起来和大多数单片机差异不大。8 位机的邻居就是简单没有复杂的 Kconfig、设备树、内存映射一个 SPI 外设从初始化到收发数据几十行代码就能跑通。这样一套方案下来BOM 成本也压得比较低特别适合工业控制器这种不需要跑操作系统的场景。2. 硬件接法SPI 四线加上两个控制脚2.1 引脚对应关系和必须处理的控制脚MR25H40CDF 的引脚不多标准 SPI 存储芯片那一套CS#、SCK、SI、SO外加电源和地还有 HOLD# 和 WP# 两个控制脚。SO 是芯片输出给主机接 PIC18 的 SDISI 是芯片输入接 PIC18 的 SDO。这两个脚容易接反接反的表现是能写不能读或者读出来全 0xFF。MR25H40CDF功能接 PIC18F86K22备注CS#片选任意 GPIO低电平选中指令结束后必须拉高SCKSPI 时钟SCK1/SCK2由 MSSP 外设产生SI串行输入SDO1/SDO2主发从收SO串行输出SDI1/SDI2主收从发VDD电源3.3V注意芯片电压等级VSS地GND就近接地HOLD#暂停通信接上拉电阻到 VDD千万不能悬空WP#写保护接上拉电阻到 VDD悬空或拉低会导致写不进去HOLD# 这个脚是很多新手忽略的重灾区。它在电平拉低时会让芯片暂停 SPI 通信这时候时钟和片选信号会被忽略通信会莫名其妙中断。如果这个引脚悬空工业现场电磁干扰一上来很容易被噪声拉低造成偶发读写失败。所以我习惯直接经过 10kΩ 上拉电阻接到 VDD让芯片永远处于正常通信状态。WP# 是写保护脚低电平时会保护状态寄存器某些情况下还会影响写操作。最稳妥的处理方式同样是 10kΩ 上拉接到 VDD。我见过有人在原型板上把 WP# 直接接地然后调了一整天“写失败”最后发现原因是写保护被打开了这种错误非常典型。2.2 电源、去耦和 PCB 布局的经验MRAM 虽然不像无线模块那样对电源敏感但工业现场里电机启停、继电器吸合都会在电源线上砸出很大的毛刺。我在原理图上给 MR25H40CDF 的 VDD 引脚加了两个电容一个 0.1μF 陶瓷电容就近贴在芯片旁边滤高频一个 4.7μF 的电解或 X7R 电容稍微远一点稳定电压。如果 PCB 空间允许再串一个 10Ω 到 100Ω 的小磁珠或电阻能把纹波再压一截。SPI 数据线在控制板上最好走短、走直不要绕着功率器件转圈。尤其是 SCK 和 CS# 这两根线如果旁边有继电器线圈建议给它们加个小阻容滤波比如串 33Ω 电阻、对地并联 10pF 电容。这样会让信号边沿稍微变钝但对抗干扰非常有帮助代价只是 SPI 时钟上限下降一点工业应用里根本无所谓。还有一个细节如果 MCU 和 MRAM 距离比较远或者这个存储模块要做成可插拔的扩展板MISO 线上可以加一个 4.7kΩ 到 10kΩ 的上拉电阻。这样既能保证芯片未选中时 MISO 电平是确定的高也能让信号抗干扰能力变强。正常情况下 SO 在 CS# 释放后是高阻不加上拉可能会导致读数据时出现随机电平这种问题排查起来非常费劲。3. 底层驱动从 SPI 初始化到 MRAM 读写3.1 用 MSSP 外设初始化 SPI 主机模式下面这段代码基于 PIC18F86K22 的 MSSP1 模块开发环境是 MPLAB X 和 XC8。我尽量保留寄存器直接配置这样不同编译器之间也能看懂。系统时钟假设是 16MHzSPI 时钟配成 FOSC/4也就是 4MHz。对于 40MHz 最大时钟的 MR25H40CDF4MHz 很保守稳如老狗。#include xc.h // CS 使用 LATH0按实际原理图修改 #define MRAM_CS_LOW() do { LATHbits.LATH0 0; } while (0) #define MRAM_CS_HIGH() do { LATHbits.LATH0 1; } while (0) void SPI1_Init(void) { // 关闭 MSSP1先清寄存器 SSP1CON1 0x00; SSP1STAT 0x00; // 配置 SPI 引脚方向 TRISFbits.TRISF0 0; // SDO1主机输出 TRISFbits.TRISF1 1; // SDI1主机输入 TRISFbits.TRISF2 0; // SCK1主机输出 TRISHbits.TRISH0 0; // CS输出 // SPI Master 模式FOSC/4 // CKP0CKE0 对应常见 SPI Mode 0如果读回来乱把 CKE 改成 1 试试 SSP1CON1 0b00100000; // SSPEN1, CKP0, SSPM0000 SSP1STAT 0b01000000; // SMP1, CKE0 }这里简单说明一下寄存器的含义SSP1CON1 的低四位 SSPM3:SSPM0 选定工作模式0000 意思是最简单的 SPI 主机模式时钟为 FOSC/4SSPEN 必须置 1 才能启用 MSSP 外设。SSP1STAT 里的 CKE 决定时钟边沿和数据采样的相位关系不同 SPI 存储芯片要求不一样MR25H40CDF 通常支持模式 0 和模式 3。如果读写数据对不上把 CKE 位改一改就能解决这是最快验证方法。3.2 SPI 字节传输函数MSSP 的 SPI 收发是同一套过程往 SSP1BUF 写一个字节硬件就会在 SCK 引脚上产生 8 个时钟同时把数据从 SDO 移出去从 SDI 移进来的数据会放在 SSP1BUF 里。所以读操作其实也是“先写一个 dummy 字节再从 SSP1BUF 里取结果”。static uint8_t SPI1_Transfer(uint8_t byte) { SSP1BUF byte; while (!SSP1STATbits.BF) { // 等待接收缓冲满 } return SSP1BUF; }注意 BF 位是硬件置位的读完 SSP1BUF 后自动清除。如果 SPI 总线上的从设备没响应或者 MISO 没接对这个函数可能会一直卡在 while 循环里。所以调试初期最好在循环外面加一个超时判断比如计数超过 10000 就返回 0xFF避免整机死等。3.3 MRAM 指令集和读写时序MR25H40CDF 支持一组经典 SPI 存储指令最常用的就四个WREN、WRITE、READ、RDSR。WREN0x06写使能。几乎所有写操作前都必须先发送这条指令写完一次后写使能位自动清除。WRITE0x02写数据。后面跟 3 字节地址再跟要写入的数据。数据可以连续写地址自动加一。READ0x03读数据。后面跟 3 字节地址然后主机持续发 0x00 产生时钟芯片从 SO 脚不断吐出数据。RDSR0x05读状态寄存器。适合用来查故障但 MRAM 不像 EEPROM 那样需要长时间查写入忙状态。地址为什么是 3 字节因为 4Mbit 对应 512K 字节地址空间需要 19 位地址手册里统一用 3 字节地址帧高字节可以看成高位地址加上未用位。地址单位是字节不是位别搞混。void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI1_Transfer(0x06); MRAM_CS_HIGH(); }写单个字节的流程是先 WREN再拉低 CS#发送 0x02 写指令发送 24 位地址再发送数据字节最后拉高 CS#。void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_Transfer(0x02); // WRITE SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); SPI1_Transfer(data); // 数据 MRAM_CS_HIGH(); }读单个字节不需要写使能拉低 CS#发 0x03发 24 位地址然后发一个 dummy 字节取回数据最后拉高 CS#。uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; MRAM_CS_LOW(); SPI1_Transfer(0x03); // READ SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); data SPI1_Transfer(0x00); // dummy实际读出 MISO 数据 MRAM_CS_HIGH(); return data; }3.4 连续读写块日志记录的核心操作工业应用里很少逐个字节去读写参数更多是一次性读一大块配置或者连续写一段运行日志。MRAM 的好处就在这里WRITE 和 READ 指令发完起始地址后只要 CS# 保持低电平地址会自动递增可以连续读写任意长度直到 CS# 拉高为止。void MRAM_WriteBlock(uint32_t addr, const uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_Transfer(0x02); SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); while (len--) { SPI1_Transfer(*buf); } MRAM_CS_HIGH(); } void MRAM_ReadBlock(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI1_Transfer(0x03); SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); while (len--) { *buf SPI1_Transfer(0x00); } MRAM_CS_HIGH(); }用这两块函数做日志非常舒服。以前用 Flash 时每次写日志要先找空闲扇区、擦除、再写日志满还要返回头部换成 MRAM 之后就是从头到尾一直写写完一圈直接回到开头覆盖连搬运都不需要。这就是“不需要擦除”带来的最大好处。3.5 状态寄存器与握手检测状态寄存器里有两个重要标记一个是写使能锁存位一个是忙标记位。MRAM 因为写入瞬时完成不需要轮询忙标记但读状态寄存器仍然是有用的调试手段。uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI1_Transfer(0x05); status SPI1_Transfer(0x00); MRAM_CS_HIGH(); return status; }如果发现写操作始终不生效先读一下状态寄存器看写使能位有没有置起来。置不起来就说明 WREN 指令没发成功或者 WP# 引脚被拉低了。这个排查思路比拿示波器查时序快好几倍。4. 工程化数据布局、CRC 校验与掉电保护4.1 把 512KB 空间按功能区划分MR25H40CDF 有 512K 字节看似不多放在工业控制器里做参数和日志绰绰有余。我最常用的划分方式是开头放设备信息和引导标志中间放参数区后面一大块全留给历史日志。0x00000 ~ 0x01FFF设备信息区存放硬件版本、序列号、生产日期。0x02000 ~ 0x03FFF运行参数区 A。0x04000 ~ 0x05FFF运行参数区 B和 A 互为备份。0x06000 ~ 0x7FFFF运行历史日志和故障记录。参数区为什么要做两份因为工业现场最怕“参数写到一半断电”。MRAM 虽然没有擦写磨损问题但写一个参数块需要一帧 SPI 数据如果在传最后一字节时突然掉电这块数据可能不完整。双备份的好处是读的时候先校验 AA 坏了读 BB 也坏了再用出厂默认值并把设备标记为异常待检修。4.2 给参数块加 CRC 校验CRC 校验是嵌入式存储的标配。我常用 CRC8多项式选 0x07简单高效能覆盖大多数随机位错误。写参数块时把结构体数据都算一遍 CRC存到块尾部读的时候重新算如果和存储的 CRC 不一致就认为这块数据已损坏。uint8_t CRC8_Update(uint8_t crc, uint8_t data) { uint8_t i; crc ^ data; for (i 0; i 8; i) { if (crc 0x80) { crc (uint8_t)((crc 1) ^ 0x07); } else { crc (uint8_t)(crc 1); } } return crc; } uint8_t CRC8_Calc(const uint8_t *buf, uint16_t len) { uint8_t crc 0; while (len--) { crc CRC8_Update(crc, *buf); } return crc; }参数块写完后最后写 CRC 字节。读的时候先读整个块再读 CRC单独比较。为什么要最后写 CRC因为这样前面某个字节没写完CRC 值就对不上能检测出半写入状态。双备份加 CRC基本能把掉电、干扰、EMI 造成的数据损坏都兜住。4.3 掉电保护与关键数据快写工业设备经常碰上瞬间停电而且停电时间不保证。传统做法是 5V 供电加一个大电容检测到掉电后进入中断抢在电容没放完电之前把关键数据存到 EEPROM 里。但 EEPROM 写入要几毫秒还得把整块数据读出来改写时间很紧张。MRAM 在这种场景下优势太明显了。它的单字节写入不需要擦除也没有编程等待在 SPI 1MHz 下写一帧命令加地址加数据大概几十微秒比 EEPROM 快了不止一个数量级。我在掉电中断里直接把当前累计值、运行状态、最近一次报警原因压缩成一个结构体调用一次 MRAM_WriteBlock 写进日志区整个过程不到 1ms只要 MCU 电源电容能维持 2ms 就绝对来得及。具体做法用 PIC18 的 INT 引脚接到外部电源监控芯片或者用内部 ADC 实时检测 VDD。电压跌到阈值后MCU 切到低功耗模式但又保留 SPI 外设工作优先完成写操作。注意写完要把 CS# 拉高不要留在一个不确定的中间状态这样即使后续电压彻底掉光MRAM 也能保证最后写入的数据完整。4.4 高低温循环与可靠性实测这个项目做了 -40℃ 到 85℃ 的循环测试。方法不复杂把整块控制板放进温箱在每个温度点保持 2 小时然后反复读写 MRI 里的全部参数区 1000 次再断电复位检查数据。结果 MR25H40CDF 常温、低温、高温下读写结果完全一致数据一个字节没错。我之前用 SPI NOR Flash 做过同样的测试高温时偶尔会读出整片 0xFF因为内部电荷泵和位线偏置受温度影响比较明显。MRAM 的磁性存储原理决定了它几乎没有这种“高温电子泄漏”问题。工业现场用这一条非常省心。5. 调试记录与常见问题排查5.1 读回全 0xFF到底哪里出了问题这是 SPI 存储最常见的问题。读出来每个字节都是 0xFF大概率是 MISO 没数据到主机原因集中在下面几个SPI 相位/极性不对把 SSP1STAT 的 CKE 位从 0 改成 1或者反过来试试很多芯片只有特定模式才工作。MISO 接错PIC18 的 SDI 接到了 MRAM 的 SI。重新确认引脚示波器量 MISO 波形最直接。WP# 悬空或拉低芯片处于写保护状态读操作一般不受影响但状态寄存器读法也可能异常先检查上拉。地址超范围MR25H40CDF 只有 512KB地址高字节超过 0x07 就无效。你访问 0x80000 以上的地址读出来大概率是 0xFF。我调试时习惯把 MRAM_ReadStatus 的结果通过串口打出来正常状态寄存器应该是一个可预期的值。如果状态寄存器也全是 0xFF那基本可以断定 SPI 通信链路没通别急着怀疑片子烧了。5.2 写入成功但偶尔有几个字节和写进去的不一样如果读操作能工作但写完之后读出错误多半不是存储单元坏而是 SPI 通信过程被低频干扰“打”了一下。常见原因和对策如下CS# 信号毛刺芯片在 CS# 下降沿开始采样如果 CS# 上有噪声导致误触发整帧数据会乱。对策是 CS# 串联 33Ω 到 1kΩ 电阻。时钟边沿毛刺SCK 走线靠近继电器或变压器会导致时钟多跳一个边沿数据就错一位。对策是降低 SPI 时钟并在 SCK 上加小电容。MISO 干扰SO 线上叠加的噪声会直接被主机读进去。对策是缩短走线、增加上拉、必要时加滤波电容。连续读写越界地址自动递增到 0x7FFFF 之后会回绕到 0x00000如果写入日志时没判断长度可能会覆盖参数区。代码里务必限制每个区域的长度。还有一个经常被忽视的坑如果 MCU 和 MRAM 电源不是同一个源上电时序不一致MCU 已经发指令了MRAM 还在上电复位第一帧数据就会丢失。所以我后来在初始化里加了 50ms 延时等 MRAM 上电稳定再开始读写。5.3 实测性能数据和一点个人感受这套方案在实际环境里我用了将近两年。PLC 控制器上一天要写几十条运行记录偶尔还升级标定参数MR25H40CDF 从没有一次让我半夜爬起来改程序。相比之下之前那批用 24C256 的板子现场巡检一次就要换十几片。具体性能上在 SPI 4MHz 下连续读 512KB 大约 1 秒出头连续写也不过这些时间。如果做成 1MHz 降频抗干扰连续读写时间会翻到 4 秒左右工业现场无所谓。最后分享一个小建议如果项目预算允许遇到频繁写坏 EEPROM 的痛点别花大力气整磨损均衡算法了直接换 MRAM。MR25H40CDF 比同等容量的 EEPROM 贵一些但换回的可靠性和调试时间远比这点成本值。PIC18F86K22 加 MR25H40CDF 这套组合我现在推荐身边所有做工业数据存储的朋友先试一把。