尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MRAM与PIC18F87J10组合:工业级不掉电数据存储方案详解

MRAM与PIC18F87J10组合:工业级不掉电数据存储方案详解 记得有一回做工业控制器日志问题差点把我坑惨了。设备里用 EEPROM 存运行参数和告警记录现场工人频繁断电不到半年就开始偶发丢数据查了半天才发现是 EEPROM 擦写寿命被磨穿了。那次之后我学乖了凡是需要高频写入、掉电保存、长期可靠记录数据的场合我基本都优先考虑 MRAM。今天聊的这套组合就是 MR25H40CDF 和 PIC18F87J10一颗 SPI 接口的 4 Mbit MRAM加上 Microchip 经典的 8 位 MCU拿来在工业和嵌入式场景里做数据存储与读取属于非常典型的“又稳又省心”方案。这个项目的核心价值其实就一句话用接近 RAM 的写入速度获得掉电不丢的非易失存储同时摆脱 Flash 的页擦除限制和寿命焦虑。MR25H40CDF 写入无需等待理论上可以无限次擦写这正好命中工业设备里高频存日志、断电瞬间保存关键参数、循环覆盖缓冲区的痛点。如果你是做嵌入式开发的尤其是搞工控、数据记录仪、仪器仪表、电力设备这类产品的这篇文章里从硬件选型、接线、协议命令到代码实现和调试经验都能直接参考。1. 项目概述为什么要用 MRAM 而不是 Flash 或 EEPROM工业嵌入式里最常见的数据存储方案是 EEPROM 和 SPI Flash但这两种器件在真实工况下都有让人头疼的地方。EEPROM 容量通常只有几十 KB 到几 Mb写一页还要等好几毫秒寿命虽然比 Flash 好一些但高频写入照样扛不住SPI Flash 容量是够大但必须先擦除再写最小擦除单位是一个扇区写个小日志都可能把整个扇区翻一遍写坏了数据区还容易影响固件区。MRAM 不一样它的存储单元是磁隧道结写“0”和“1”靠的是磁化方向翻转而不是电荷注入所以没有擦除过程也没有写次数上限。MR25H40CDF 是 Everspin 家的串行 MRAM容量 4 Mbit换算下来是 512 KByte恰好是 SPI Flash 那种 8 脚小封装引脚上跟 EEPROM 基本兼容。PIC18F87J10 则是 Microchip 的一颗 8 位 MCU80 MHz 主频20 MIPS 处理能力自带两个 MSSP 模块可以同时跑一路 SPI 接 MRAM、另一路 SPI 接传感器或显示内存和外设在 8 位机里算是相当充裕。这样做出来的数据存储系统有几个非常实际的好处。第一是写入延迟极低MRAM 的写操作在最后一个时钟沿到来时就已经完成不需要像 Flash 那样等待“页编程时间”所以很适合在中断里直接写日志。第二是没有寿命概念产品生命周期内随便写不用设计磨损均衡算法代码逻辑可以简化很多。第三是可以在掉电瞬间快速保存状态因为写入不需要额外等待MCU 检测到掉电后往 MRAM 里写几字节关键数据完全来得及。适合这个方案的人主要分两类。一类是正在做产品选型评估的嵌入式工程师想知道 MRAM 到底比 Flash 好在哪、驱动难不难写另一类是准备从 8 位机切换到工业级存储方案的开发者需要一套能直接抄的代码和接线办法。下面就把这套方案从芯片到代码完整过一遍。2. 硬件搭档MR25H40CDF 与 PIC18F87J10 的关键特性2.1 MR25H40CDF一颗“随便写”的 4 Mbit MRAMMR25H40CDF 的存储结构是 512K x 8也就是按字节寻址4 Mbit 容量刚好可以用一个 3 字节地址帧覆盖最高字节在当前容量下通常为 0。工作电压 2.7V 到 3.6V所以 3.3V 系统直接连工业级型号支持 -40°C 到 85°C部分版本甚至能到 105°C放在柜子里、电机旁边、户外箱体里都行。SPI 时钟主频最高到几十 MHz实测在 PIC18F87J10 上配置为 FOSC/64 甚至 FOSC/16 都能稳定通信。选这颗料最大的理由是它解决了工业现场的数据可靠性问题。传统 EEPROM 的 MTTF 和写次数是有限的而 MRAM 的写耐久度基本可以视为无限数据保持时间超过 20 年抗辐射和抗磁场干扰能力也比普通存储强。官方资料里提到 MRAM 没有“写干扰”问题多个字节连续写也不会影响相邻单元这一点对日志型应用特别重要。芯片是标准 8 脚 SOIC 封装引脚包括 CS#片选、SI数据输入、SCK时钟、WP#写保护、SO数据输出、HOLD#暂停传输、VSS、VCC。注意 SI 和 SO 是分开的这跟 SPI Flash 的四线制一模一样接线时不能把主机的 SDO 和 SDI 接反否则读回来的全是 0xFF。2.2 PIC18F87J10MSSP 外设与引脚分配PIC18F87J10 是 Microchip 的 80 脚也有小封装版本8 位 MCU内部 Flash 程序空间 128 KB数据 RAM 将近 4 KB最高跑 80 MHz。该系列最大的优势是外设丰富带两个 MSSP 模块每个都能配置成 SPI、I2C 或者三线 SPI 从机模式。数据存储项目里一路 MSSP 挂 MRAM另一路可以挂 SD 卡、传感器或者触摸屏互不干扰。我需要提醒的是不同封装型号的 SPI 引脚映射并不完全一样用之前一定要翻一下具体型号的数据手册把 MSSP1 的 SCK1、SDO1、SDI1 对应到实际引脚上。直接抄网上的原理图引脚号很容易翻车因为 80 脚和 64 脚封装的复用位置有区别。我这里给出的代码都用 MSSP1 的标准功能名接线表也是按功能名对应这样换不同封装时只需要改 Pin Mux。PIC18F87J10 的 3.3V 供电正好覆盖 MR25H40CDF 的工作电压范围不用加转换芯片电平天然匹配。MCU 的晶振可以用内部振荡器或外部晶振但如果用 MRAM 存储工业日志还是推荐外部晶振保证 SPI 时钟频率稳定尤其是当环境温度变化比较大的时候内部振荡器会有频率漂移高速 SPI 容易出错。2.3 接线方式与硬件设计注意点MRAM 和 PIC 的接线非常直接总共只需要 5 根信号线其中 4 根是 SPI 总线1 根是片选。片选可以不接在硬件 SPI 的 SS 引脚上用普通 GPIO 拉低拉高就行这样更灵活可以随时把 MRAM 挂到不同的 SPI 通道上。参考接线如下PIC18F87J10 引脚MR25H40CDF 引脚说明SDO1主机输出SI主机发送数据到 MRAMSDI1主机输入SOMRAM 发送数据到主机SCK1SCKSPI 时钟输出普通 GPIO如 RB0CS#片选拉低使能 MRAMVCC3.3VVCCMRAM 供电GNDVSS共地不用接WP#接 VCC解锁写保护不用接HOLD#接 VCC禁止暂停功能硬件上有几个坑我必须提前讲清楚。WP# 引脚如果悬空有些批次内部没有上拉可能在噪声干扰下误触发写保护所以直接焊 10K 电阻到 VCC 最保险。HOLD# 也是一样拉低就暂停 SPI 通信悬空可能导致偶发通信卡死同样接 VCC。MR25H40CDF 的电源引脚旁边要放一个 0.1uF 陶瓷电容如果 PCB 上走得比较远再加一个 4.7uF 电解电容滤波否则高速写数据时电源纹波大偶尔会出现写入错误。3. SPI 通信协议与 MRAM 命令细节3.1 SPI 模式选择SPI Mode 0 还是 Mode 3MR25H40CDF 官方支持 SPI Mode 0CPOL0CPHA0和 SPI Mode 3CPOL1CPHA1。我用得最多的是 Mode 0也就是时钟空闲时为低电平数据在上升沿采样下降沿切换数据。这个模式在 PIC18F87J10 的 MSSP 上配置起来最顺手也最容易跟逻辑分析仪对波形。PIC18F87J10 的 MSSP 寄存器里MSTEN 位负责主从模式选择CKP 位控制时钟极性CKE 位控制采样沿。配置成 Mode 0 时 CKP0、CKE0。很多新手在 MSSP 上栽跟头就是 CKE 搞反了导致读回来的数据整体移了一位现象就是写入正常但读出来每个字节都翻倍或者变成 0xAA 这种规律数据。排查办法很简单发一个 RDSR 命令读状态寄存器如果命令帧时序错位读回来的肯定不是预期值。SPI 通信基本规则是高位在前、低位在后地址和多字节数据都是先发高字节再发低字节。MRAM 的 CS# 在整个命令帧传输期间必须保持低电平发完最后一个时钟后再拉高拉高的瞬间 MRAM 才开始解析命令。这里最关键的是不要在命令中间把 CS# 拉高那样命令会被丢弃但也不会损坏数据所以就算拉高时机不对最多是操作无效。3.2 核心命令速查表MR25H40CDF 的命令集不复杂多数情况下只需要六条命令就能跑起来。我把每条命令的帧结构和用途整理成一张表方便开发时对照。命令操作码帧格式说明WREN0x060x06写使能任何写操作前必须先发WRDI0x040x04写禁止防止误写READ0x030x03 3 字节地址 数据输出连续读WRITE0x020x02 3 字节地址 数据输入连续写RDSR0x050x05 状态寄存器输出读状态寄存器WRSR0x010x01 状态寄存器输入写状态寄存器WREN 是 MRAM 的看门钥匙每次执行 WRITE 或 WRSR 之前必须先拉低 CS#、发一个 0x06、再拉高 CS#把内部写使能锁存器打开。发送 WRITE 命令时如果再发一次 0x06新的 WREN 不会覆盖未完成的写操作但为了代码清晰我习惯在每次写操作前面单独调用一次写使能函数。READ 命令不需要写使能任何时候都可以读而且对 MRAM 没有任何寿命损耗这一点跟 Flash 的“读干扰”问题完全不同读操作可以无限执行。MR25H40CDF 的地址容量是 512 KB3 字节地址里的最高字节正常情况下发送 0x00如果未来换成更大容量的 MRAM 型号这段代码可以直接复用地址扩展不需要改驱动框架这是用 3 字节地址帧的额外好处。3.3 状态寄存器与写保护机制状态寄存器总共 8 位但对我们日常使用来说主要关心三个位WEL写使能锁存状态、BP1、BP0块保护位。WEL 位在 WREN 命令之后会自动置 1写操作完成后自动清 0所以我想确认写使能有没有成功最简单的方法就是读状态寄存器看 WEL 是不是 1。BP1 和 BP0 用于规定地址范围写保护默认出厂状态通常是 0也就是整个存储区都允许写。如果在调试时发现能读不能写首先要检查发送 WREN 之后有没有立刻发 WRITE其次就是检查 WRSR 是不是把 BP 位设成了非零值。WP# 引脚接 VCC 之后软件上不让设保护但软件里的 BP 位依然有效所以最好不要在应用代码里随便写 WRSR除非你有完整的产品固件升级策略。有一个细节值得注意MRAM 的状态寄存器写入后也是立即生效不需要等待状态寄存器忙标志。如果你写的代码里有“写完 WRSR 后轮询 BUSY”这一步那在 MRAM 上会永远轮询不过去因为根本没有 BUSY 位。这也是 MCU 从 Flash 驱动迁移过来最容易出 bug 的地方代码里残留的等待逻辑统统要去掉。4. 实操过程与核心代码实现4.1 硬件初始化GPIO 和 MSSP 模块配置我用 XC8 编译器开发目标芯片选 PIC18F87J10。初始化分为两步先把片选脚配置为普通输出并拉高再把 MSSP1 配置成 SPI 主模式。重点说 MSSP1 的几个寄存器位SPI1CON1bits.MSTEN 写 1 进入主模式SPI1CON1bits.CKP 写 0 让时钟空闲为低SPI1CON1bits.CKE 写 0 匹配 SPI Mode 0。下面这段是我实际用过的初始化代码可以直接当模板。#define MRAM_CS_TRIS TRISBbits.TRISB0 #define MRAM_CS_LAT LATBbits.LATB0 void gpio_init(void) { MRAM_CS_TRIS 0; // 片选脚设输出 MRAM_CS_LAT 1; // 默认不选中 MRAM } void spi1_init(void) { // 先把 MSSP1 禁掉配置完再开 SPI1CON1bits.SSPEN 0; // 主模式CPOL0CPHA0对应 SPI Mode 0 SPI1CON1bits.MSTEN 1; SPI1CON1bits.CKP 0; SPI1CON1bits.CKE 0; SPI1CON0bits.SMP 1; // 时钟分频系数初期调试用 FOSC/64 SPI1CON1bits.SSPM 0b0010; // 使能 MSSP SPI1CON1bits.SSPEN 1; }这里我特意把时钟分频设成 FOSC/64也就是时分频系数 64。因为调试初期用杜邦线连接 MRAM 和 MCU线缆寄生电容大跑高频时钟容易读到毛刺数据。等做成 PCB 板、走线短了再把分频系数调到 FOSC/16 甚至 FOSC/4MRAM 完全能跟上但 PCB 质量不够好时速度反而不是瓶颈。SPI 发送和接收用同一个寄存器 SPI1BUF写一个字节的同时会收到一个字节。底层基础函数长这样uint8_t spi1_xfer(uint8_t byte) { SPI1BUF byte; while (!SPI1STATbits.BF); // 等待接收缓冲满 return SPI1BUF; }4.2 读状态寄存器与写使能操作基础传输函数搞定之后接下来写 MRAM 的高层驱动。第一步建议先把状态寄存器读出来如果这条通了说明 SPI 总线、电源、接线基本正确。读状态寄存器的代码很容易理解CS# 拉低、发 0x05、读一个字节、CS# 拉高整个过程只需要 8 个时钟周期。uint8_t mram_read_status(void) { uint8_t status; MRAM_CS_LAT 0; spi1_xfer(0x05); // RDSR 命令 status spi1_xfer(0xFF); // 读回状态寄存器 MRAM_CS_LAT 1; return status; }写使能函数更简单就是拉低 CS#、发一个 0x06、拉高 CS#。注意每条命令都必须有完整的 CS# 低电平期间不能把多条命令拼在一起连发尤其是 WREN 和 WRITE 之间中间必须有一次 CS# 拉高再拉低的间隔。void mram_write_enable(void) { MRAM_CS_LAT 0; spi1_xfer(0x06); // WREN MRAM_CS_LAT 1; }如果读状态寄存器发现 WEL 位一直是 0先检查代码里是不是把 WREN 漏了再看 CS# 的拉高拉低有没有在函数外被别的操作干扰。CS# 引脚在初始化之后要始终保持高电平只有传输命令时才能拉低如果程序里把它当成普通引脚频繁翻转MRAM 根本来不及识别命令帧。4.3 连续写数据与连续读数据的实现真正的数据写入分三步先写使能再拉低片选然后依次发送写命令、三字节地址、要写的数据。连续写模式下地址会在 MRAM 内部自动递增所以我可以在一次命令里写入任意长度的字节流不需要每写一个字节都重新发地址。下面是完整的写函数。void mram_write(uint32_t addr, const uint8_t *data, uint16_t len) { mram_write_enable(); MRAM_CS_LAT 0; spi1_xfer(0x02); // WRITE spi1_xfer((addr 16) 0xFF); // 地址高字节 spi1_xfer((addr 8) 0xFF); // 地址中字节 spi1_xfer(addr 0xFF); // 地址低字节 while (len--) spi1_xfer(*data); MRAM_CS_LAT 1; }读数据不需要写使能直接拉低片选、发读命令和地址然后连续读字节。这里有一个小技巧读数据时主机必须持续产生时钟所以我在循环里调用 spi1_xfer(0xFF)发送一个无效字节的代价就是同时获得一个接收字节。如下void mram_read(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_CS_LAT 0; spi1_xfer(0x03); // READ spi1_xfer((addr 16) 0xFF); spi1_xfer((addr 8) 0xFF); spi1_xfer(addr 0xFF); while (len--) *data spi1_xfer(0xFF); MRAM_CS_LAT 1; }写一个简单的自测函数把一组测试数据写到地址 0再读回来逐个对比。这是移植驱动时最常用也最有效的检查方式uint8_t write_buf[8] {0x5A, 0xA5, 0x00, 0xFF, 0x55, 0xAA, 0x01, 0x02}; uint8_t read_buf[8]; mram_write(0x000000, write_buf, sizeof(write_buf)); mram_read(0x000000, read_buf, sizeof(read_buf));如果比较结果完全一致说明基础读写链路没问题。如果不一致先把读状态寄存器挂上去看返回值如果返回值是 0xFF大概率是 SI/SO 接反或 SCK 没信号如果返回值是 0x00大概率是 SPI Mode 配置不对。4.4 工业日志场景中的循环写入设计单纯读读写写还不够体现这套方案的价值我实际做产品时最常用的是一个循环日志缓冲设计。在 MRAM 里划分一个固定区域头部存写指针后面是环形日志数据。每次记录日志时先读头部拿到当前写指针把日志写入对应地址然后把写指针递增如果超过缓冲末尾就回绕到起始位置。这个设计在 Flash 上相当麻烦因为回绕时可能要擦除整个扇区处理不好还会丢失头记录但在 MR25H40CDF 上非常简单因为任意地址都能直接改写环形缓冲的回绕就是一次普通写入。代码如下#define LOG_BUF_SIZE 0x10000 // 64 KB 循环区 #define LOG_BUF_START 0x00010000 // 从 256 KB 地址开始 #define LOG_PTR_ADDR 0x00008000 // 头部指针保存地址 void log_append(uint8_t *record, uint16_t len) { uint32_t ptr; mram_read(LOG_PTR_ADDR, (uint8_t *)ptr, sizeof(ptr)); if (ptr len LOG_BUF_START LOG_BUF_SIZE) { ptr LOG_BUF_START; // 回绕 } mram_write(ptr, record, len); ptr len; mram_write(LOG_PTR_ADDR, (uint8_t *)ptr, sizeof(ptr)); }代码里先把写指针存在固定地址 0x8000每次写日志之前读一次。写指针地址和日志区地址错开避免头部被日志覆盖。把日志块长度控制在 128 字节以内既方便对齐也能保证单次连续写的时间极短掉电瞬间保存关键记录完全来得及。5. 常见问题与排查技巧实录5.1 看波形、测电压、查时序三板斧排查真机调试时我最常用的工具是逻辑分析仪和示波器。如果 MRAM 读回来的数据不对先把 SPI 三根线SCK、SI、SO都挂上逻辑分析仪抓一次 RDSR 命令的波形数一下时钟边沿和字节时序。不要一上来就怀疑芯片有问题大多数情况是代码或接线问题而且问题通常在你知道它是问题之前就已经存在了。电源电压也要重点检查。MRAM 在 2.7V 以下可能工作异常我用万用表量芯片 VCC 引脚时发现只有 2.4V原因是 PCB 走线太细加上稳压器压降MRAM 偶尔写错地址。后来在芯片旁边加了一颗 47uF 电容问题就消失了。工业现场布线复杂电压跌落比实验室里严重得多电源设计一定要留余量。还有一种特别坑的情况是 WP# 和 HOLD# 悬空。样机阶段飞线连接时这两根线不接可能也能跑但手指一碰板子或者现场电机启动产生干扰WP# 电平抖动一下写保护就被触发现象是数据读得出来但写不进去。现在我焊样机时都会把 WP# 和 HOLD# 直接短路到 VCC哪怕浪费 10 秒钟也算买个保險。5.2 常见问题速查表现象可能原因排查方法读出来全是 0xFFSI/SO 接反或 SDI 没配置成输入检查接线用万用表量电平读出来全是 0x00SCK 没有输出或 SPI Mode 不对示波器看 SCK 波形改 CKP/CKE写入后读回旧数据没发 WREN或 CS# 时序不对用逻辑分析仪看整段 CS# 低电平偶发一个字节错位时钟频率太高或杜邦线太长降低分频系数换成短导线上电后 CPU 卡死MRAM 的 HOLD# 悬空引入噪声HOLD# 接 VCC使能内部上拉写多字节后地址错乱引脚电平不满足数据手册要求检查电源电压和上拉电阻5.3 几个我从项目里总结出来的实操习惯第一每次上电初始化后先读一次状态寄存器把它打印到串口。我习惯把这个值作为“通信握手”的信号状态寄存器能稳定读出来才继续后面的数据操作。如果状态寄存器读出来每次都不一样说明 SPI 电平不稳定别急着写数据先解决电气问题。第二正式产品的数据写入一定要做校验。MRAM 虽然可靠但工业现场有强电磁干扰长距离飞线时 SPI 信号可能被干扰。我常用的办法是每 16 字节追加两个 CRC16 字节读取时校验发现不对就重读一次。MRAM 写入速度极快花在小开销上的时间几乎可以忽略。第三写函数里不要加多余的 Delay。有些不熟悉 MRAM 的工程师会把 Flash 的习惯带过来写完一个字节后加几毫秒延时既拖慢速度又没有任何作用。MR25H40CDF 写数据是即时完成的只要代码没有执行到下一个操作数据就已经稳定在存储单元里了。如果要在掉电瞬间保存状态这个“零延时”特性才是真正的救命稻草。6. 代码移植与扩展思路这套驱动在 PIC18F87J10 上跑通之后换到其他带 SPI 的单片机上也很容易只需要改底层的 spi1_xfer 函数和片选宏。比如换到 STM32就把 spi1_xfer 换成 HAL_SPI_TransmitReceive地址帧和命令帧的逻辑完全不用动。MRAM 的 SPI 命令是行业标准的Everspin 的驱动兼容性很好这是选 SPI 接口 MRAM 的一大好处。容量扩展方面MR25H40CDF 是 512 KB如果产品日志量大可以直接换同一命令兼容的更大容量 MRAM比如 8 Mbit 或 16 Mbit 的型号地址帧从 3 字节的高位开始扩展即可。驱动里的 mram_write 和 mram_read 已经用了 uint32_t 地址所以容量翻倍不需要改函数原型只在项目里改总容量宏即可。另外一个扩展方向是双 MRAM 冗余存储。对可靠性要求极高的电力设备我做过一份数据同时写两块 MRAM 的方案一块为主、一块为备份读取时优先读主块主块数据异常再读备份。因为 MRAM 写入不需要额外等待双写消耗的总时间依然非常短比 Flash 双备份的方案简单很多。最后再分享一个个人体会我在实际项目里最满意这套方案的地方不是跑分也不是黑科技而是“不用等”。工业设备现场调试本来就够乱了以前写 EEPROM 要盯着延时函数数毫秒写 Flash 要想怎么分配扇区、怎么做磨损均衡。现在换了 MR25H40CDF 之后驱动只有几个简单函数日志想写就写掉电也不怕这种“省心感”是参数表上看不出来的。如果你也在为掉电保存、高频日志、数据可靠性发愁建议直接按这套思路起一版原型接线和代码都不复杂试过一次就知道值不值。
返回列表