ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MRAM替代Flash做工业存储:PIC18F86K22驱动MR25H40CDF完整方案

MRAM替代Flash做工业存储:PIC18F86K22驱动MR25H40CDF完整方案 真正在工业现场踩过坑的人大概都经历过这种场景设备运行得好好的突然一次非正常断电再上电工艺参数回到出厂值现场几百公斤的半成品直接报废或者一台驱动器每 100ms 要存一次运行状态用普通 Flash 做日志几个月后写入寿命耗尽只能返厂换芯片。这些问题本质上是同一个——嵌入式系统里需要“频繁、小量、可靠、断电不丢”的数据一直没找到合适的存储介质。MR25H40CDF 是我最近一个工业控制器项目里最终选定的存储 IC它是 Everspin 的 4Mbit SPI 接口 MRAM靠磁性隧道结存数据不需要电池、不需要擦除、读写次数接近无限。驱动它的主控是 PIC18F86K22Microchip 在工业市场打磨了很多年的 8 位 MCU。这篇文章把这套链路完整讲一遍为什么这么选、电路怎么连、驱动怎么写、现场怎么调以及最容易出问题的地方在哪里给正在做存储选型、或者打算用 MRAM 换掉 Flash/EEPROM 的朋友一份可以直接上手的参考。1. 为什么工业存储最终选了 MR25H40CDF 这颗 SPI MRAM1.1 工业存储场景对硬件的四个硬要求先从需求说起。工业控制器、测试仪器、边缘采集终端里要存的数据和消费电子不太一样。消费电子存照片视频量大但写入不频繁工业设备存的是工艺参数、校准系数、运行状态、事件日志单条数据只有几十到几百字节但写入频率可能非常高而且掉电不能丢。归纳下来是四个硬要求。第一非易失断电后数据必须还在这是底线。第二写入要快很多时候数据是在掉电瞬间、或某个实时事件发生的几毫秒窗口内写入的慢一点都不行。第三寿命要长PLC 里的产量计数、伺服里的运行时间统计几乎每 100ms 就写一次普通 Flash 十万次的寿命几个月就会耗尽。第四容量适中、接口简单能存下参数表和历史日志同时驱动代码不能太复杂毕竟 MCU 的 Flash 空间还要留给业务逻辑。这四个要求放在一起传统方案就各有各的短板了。EEPROM 太慢且容量小NOR Flash 要擦除而且寿命有限带电池的 SRAM 则要人伺候电池。这也是我最终走向 MRAM 的原因——MRAM 在四个维度上都没有明显短板。1.2 主流非易失存储方案对比EEPROM、NOR Flash、电池SRAM、MRAM直接上一张对比表这是我在选型时反复核对过的数据。存储介质典型容量写入速度擦除/寿命断电保持关键短板I2C EEPROM如 24C256几十 KB单字节约 5ms100 万次好太慢、容量小SPI NOR Flash如 W25Q64几 MB页编程 1~3ms需先擦扇区10 万次好要擦除、寿命有限、算法复杂带电池 SRAM任意总线速度无限依赖电池电池维护、低温失效、漏液风险SPI MRAMMR25H40CDF512KB总线速度无擦除1e14 次好单位容量价格偏高这里面我最想吐槽的是带电池 SRAM。算下来确实寿命无限、速度快但电池三年一换偏远风电场的运维换一次电池路费人工比电池本身贵得多更怕的是低温环境电池容量大跌设备一断电数据就悄悄丢了。MRAM 相当于把“电池的钱”一次性买断换来几十年的安心。至于 Flash做参数存储其实有点“杀鸡用牛刀”。写一个参数得先擦除整个扇区代码要处理磨损均衡、掉电中间态、写保护切换一套流程下来调试成本非常高。而 MRAM 是真正的随机读写不擦除、不磨损、按字节访问驱动代码和普通 SRAM 一样简单。1.3 为什么是 MR25H40CDF 和 PIC18F86K22 这对组合选存储芯片时我先圈定了 Everspin 的 SPI 接口 MRAM 系列。SPI 接口比并口省引脚比 I2C 快得多在 8 位 MCU 上非常合适。MR25H40CDF 的规格我对照过数据手册4Mbit 即 512KB 容量工作电压 2.7V~3.6V工业级温度范围SCK 最高 40MHz支持 SPI Mode 0 和 Mode 3。最让我安心的是它没有“页边界”限制可以从任意地址开始连续写写完不用等擦除状态寄存器、写保护、读 ID 这些功能和普通 SPI EEPROM 类似上手成本很低。主控选 PIC18F86K22 也经过了实际考量。K22 系列在工业市场足够皮实宽电压、工业温度范围外设很全两个 MSSP 模块、CAN、多个 USART、12 位 ADC还有低压检测和欠压复位。对存储应用来说3.3V 供电时跑 40MHz 主频、SPI 出 4MHz 时钟轻轻松松。更重要的是它带硬件 SPI不占 CPU 太多时间——我用一个 MSSP 挂 MRAM另一个还能挂外部传感器互不干扰。选型定下来之后我当时的判断是MRAM 负责“可靠快速存储”PIC18F86K22 负责“稳定控制和丰富外设”两个都是工业级器件整个存储链路的可靠性就从器件层面先有了保障。2. MR25H40CDF 与 PIC18F86K22 的硬件连接与电平匹配2.1 引脚连接表从 MRAM 到 MCU 的每一个信号硬件连接并不复杂但细节藏在每一根线里。MR25H40CDF 的信号我分成两组一组是 SPI 数据线一组是控制线。SPI 数据线就是 CS#、SCK、SI、SO控制线是 WP# 和 HOLD#。MRAM 信号接到 PIC18F86K22方向说明CS#RD0MCU - MRAM手动 GPIO 控制片选不用硬件自动片选SCKRC3MCU - MRAMSPI 时钟实测配 4MHzSIRC5MCU - MRAM主出从入连接 MSSP1 的 SDO1SORC4MRAM - MCU主入从出连接 MSSP1 的 SDI1WP#直接接 VDD 3.3V-禁用写保护避免状态寄存器误锁HOLD#直接接 VDD 3.3V-禁用暂停功能防止总线被挂起VDD3.3V-100nF 10uF 退耦电容尽量靠近芯片VSSGND-低阻抗接地CS# 用手动 GPIO 控制是我在这类 SPI 存储芯片上的一个习惯。MR25H40CDF 的命令帧要求在 CS# 低电平期间发送完整的命令、地址和数据CS# 拉高时命令才真正生效。用普通 GPIO 拉低、拉高时序上最直观也方便调试时用逻辑分析仪抓。WP# 和 HOLD# 两个引脚我直接固定接 VDD。WP# 拉高表示禁用写保护HOLD# 拉高表示不用暂停功能。如果接成 GPIO 控制就要在软件里时刻注意电平状态一旦 HOLD# 被意外拉低SCK 上的一切操作都会被冻结SO 引脚还会维持上一个状态现象非常诡异排查起来很费劲。工业电路板上少一个变量就少一份故障源。2.2 SPI Mode 0 时序与 PIC 的 CKE/CKP 映射MR25H40CDF 支持 SPI Mode 0 和 Mode 3我把系统统一跑在 Mode 0也就是 CPOL0、CPHA0。选 Mode 0 没有特殊原因只是它最常用之前板子上其他 SPI 器件也大多用 Mode 0统一起来软件好维护。但 PIC 的 SPI 模块有个容易搞混的地方它的寄存器位不直接叫 CPOL 和 CPHA而是叫 CKP 和 CKE并且 CKE 和 CPHA 是反向映射的。我见过不少人在这一步被坑初始化配置写反了读回来的数据全是 0xFF。对应关系是这样的CKP 等于 CPOLCKE 要取反后才是 CPHA。所以 SPI Mode 0 实际配置是 CKP0、CKE1另外把 SMP 设为 1让数据在输出末端采样时序余量更充足。我建议不要死记寄存器数字而是在代码注释里明确写清楚“这里对应 SPI Mode 0CPOL0/CPHA0”避免几个月后自己回来改代码时还要重新翻数据手册。2.3 电平匹配3.3V MRAM 与 5V MCU 之间的稳妥做法硬件上最容易忽略的问题是电平匹配。MR25H40CDF 是纯 3.3V 器件工作电压上限 3.6VPIC18F86K22 虽然支持 2.0V~5.5V但如果 MCU 用了 5V 供电它的 SPI 输出高电平会到 5V直接灌进 MRAM 输入引脚长期下来可能损坏芯片。我的做法是一刀切整套系统用 3.3V 供电。PIC18F86K22 在 3.3V 下跑 40MHz 主频毫无压力SPI 出 4MHz 时钟对 MRAM 来说余量很大中间不用加任何电平转换器件。这样不仅省了电路面积还避开了电平转换引入的延迟和毛刺。如果你遇到的情况是主控必须 5V 供电那就别想办法省电平转换了。稳妥起见在 SPI 四根线上加一片 74LVC245 单向缓冲或者在每个信号线上用两三个电阻分压但分压会降低信号边沿速度SPI 频率高了容易出错。我的建议是优先统一 3.3V这比任何补救措施都干净。3. 用 PIC18F86K22 驱动 MR25H40CDF 的读写代码实现3.1 SPI1 模块初始化PIC 的 SPI 分频与寄存器配置代码从初始化开始。我在工程里用的是 MPLAB X IDE XC8 编译器目标芯片 PIC18F86K22。SPI1 模块的引脚是固定的RC3 输出 SCK、RC5 输出 SDO、RC4 输入 SDI这和其他一些需要 PPS 外设引脚选择的新型 PIC 不同K22 系列反而是个优势——少配置一步心里踏实。初始化代码分五步配引脚方向、关模块、配时序模式、选主模式分频、开模块。#include xc.h #define MR_CS_LOW() LATDbits.LATD0 0 #define MR_CS_HIGH() LATDbits.LATD0 1 void SPI1_Init(void) { // 1. SPI1 引脚方向 TRISCbits.TRISC3 0; // SCK1 输出 TRISCbits.TRISC4 1; // SDI1 输入 TRISCbits.TRISC5 0; // SDO1 输出 // 2. 先禁用模块再改配置 SSP1CON1bits.SSPEN 0; // 3. SPI Mode 0CPOL0, CPHA0 // PIC 的 CKE 是 CPHA 的反相所以 CKE1 SSP1STATbits.SMP 1; // 输出末端采样 SSP1STATbits.CKE 1; SSP1CON1bits.CKP 0; // SCK 空闲为低 // 4. 主机模式时钟分频 Fosc/4 // 注意SPI 主模式不用 SPBRG分频靠 SSPM 位 SSP1CON1bits.SSPM 0b0000; // 5. 使能模块 SSP1CON1bits.SSPEN 1; }这里有个容易踩的坑我特意在注释里写清楚PIC 的 SPI 主模式时钟不是靠 SPBRG/波特率寄存器算的而是靠 SSPM 的分频位。SPBRG 是 I2C 模式的波特率寄存器在 SPI 模式下它根本不参与。一开始我在这个上面浪费过时间以为像串口一样写 SPBRG 就能调速结果一点反应都没有。3.2 底层命令封装WREN、RDSR、READ、WRITEMR25H40CDF 的命令集和普通 SPI NOR Flash 很像但有个本质区别写数据前不需要擦除只需要发一个 WREN 写使能命令。底层封装我拆成三个小函数一位交换、写使能、状态寄存器读取然后再组合成读和写。字节交换是 SPI 全双工的基本操作往 SSP1BUF 写入一个字节同时从 SO 线收一个字节收完后 BF 位置 1。uint8_t SPI1_Exchange(uint8_t byte) { SSP1BUF byte; while (!SSP1STATbits.BF) ; return SSP1BUF; }写使能函数很简单但每次写操作前都必须调用这是 MRAM 跟普通 SRAM 最不一样的地方。如果漏了 WREN写命令会被芯片直接忽略读回来的数据纹丝不动。void MR_WriteEnable(void) { MR_CS_LOW(); SPI1_Exchange(0x06); // WREN MR_CS_HIGH(); } uint8_t MR_ReadStatus(void) { uint8_t status; MR_CS_LOW(); SPI1_Exchange(0x05); // RDSR status SPI1_Exchange(0x00); MR_CS_HIGH(); return status; }读数据函数是 READ 命令0x03 后面跟 24 位地址然后连续读任意长度字节。MRAM 的地址自动递增到 512KB 末尾会回卷所以读操作根本不需要处理页边界。void MR_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MR_CS_LOW(); SPI1_Exchange(0x03); // READ SPI1_Exchange((addr 16) 0xFF); // 高 8 位地址 SPI1_Exchange((addr 8) 0xFF); // 中 8 位地址 SPI1_Exchange(addr 0xFF); // 低 8 位地址 for (i 0; i len; i) { buf[i] SPI1_Exchange(0x00); } MR_CS_HIGH(); }写数据函数是 WRITE 命令0x02 后跟 24 位地址再跟数据。和 Flash 最大的不同是这里没有页缓冲、没有扇区擦除、没有编程时间数据在 CS# 拉高时就已经写进存储阵列。写任意地址、任意长度都不需要做额外操作。void MR_WriteBytes(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t i; MR_WriteEnable(); MR_CS_LOW(); SPI1_Exchange(0x02); // WRITE SPI1_Exchange((addr 16) 0xFF); SPI1_Exchange((addr 8) 0xFF); SPI1_Exchange(addr 0xFF); for (i 0; i len; i) { SPI1_Exchange(buf[i]); } MR_CS_HIGH(); }此外还有一个 RDID 命令0x9F上电后可以用来读回制造商标识和器件标识确认芯片在位、型号无误。这个我在后面的产线自检部分还会提到。注意 CS# 的时序CS# 拉低到第一个 SCK 上升沿之间要有一点建立时间最后一个 SCK 沿之后到 CS# 拉高也要留出一点保持时间。我的系统里 SPI 时钟只有 4MHz一个周期 250ns这些时序余量天然满足不需要额外加延时。但如果你把 SPI 跑到接近 40MHz 的上限就必须逐项核对数据手册里的 tSLCH、tSHSL 参数。3.3 应用层读写配置结构体与地址规划底层封装完应用层就是把 MRAM 当一块普通内存来用。我常用的做法是定义一个配置结构体把分散的参数收拢在一起整体读写。#pragma pack(push, 1) typedef struct { uint8_t device_id; uint16_t work_mode; int16_t calib_offset; uint16_t crc16; } AppConfig_t; #pragma pack(pop) AppConfig_t g_cfg; #define CFG_ADDR 0x00000 void LoadConfig(void) { MR_ReadBytes(CFG_ADDR, (uint8_t *)g_cfg, sizeof(g_cfg)); } void SaveConfig(void) { g_cfg.crc16 CRC16_Calc((uint8_t *)g_cfg, sizeof(g_cfg) - sizeof(g_cfg.crc16)); MR_WriteBytes(CFG_ADDR, (uint8_t *)g_cfg, sizeof(g_cfg)); }使用#pragma pack是我从踩坑里学来的。XC8 默认会对结构体成员做对齐填充不同编译器、不同优化等级下填充规则可能有差异如果只是自己读写自己问题不大一旦牵扯到固件升级、上位机解析填充字节不一致就会闹鬼。所以配置、日志这类跨模块共用的数据结构我会统一禁掉填充。地址规划上我习惯把 512KB 分成三段0x00000 开始的 16KB 做配置区0x04000 开始的 128KB 做运行日志剩下的空间做事件记录和大块数据缓存。这样地址清晰调试或做产线自检时每个区域的作用一眼就能看懂。4. 工业现场可靠读写双备份、CRC 与掉电保存设计4.1 双备份配置区与 CRC16 校验MRAM 虽然是工业级器件但外部的电源跌落、强电磁干扰仍然可能让一次写入在物理上只完成一半。这时候单靠 MRAM 自身的可靠性是不够的软件上要有兜底机制。我的方案是“双备份 CRC16”简单而且实用。配置区实际占两个槽位A 区在 0x00000B 区在 0x02000每个槽位存一份完整的配置结构体结构体尾部带 CRC16 校验值。每次保存时交替写入 A、B读取时先读 A 区CRC 校验不过就读 B 区两个都不行就恢复出厂默认值同时点亮故障指示灯。这样即使某次写入被干扰打断系统永远有一份可用的配置。CRC16 用 CRC-CCITT 多项式 0x1021位运算版本逻辑简练方便移植到其他平台。uint16_t CRC16_Update(uint16_t crc, uint8_t byte) { uint8_t i; crc ^ (uint16_t)byte 8; for (i 0; i 8; i) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } return crc; } uint16_t CRC16_Calc(const uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; while (len--) { crc CRC16_Update(crc, *buf); } return crc; }4.2 掉电瞬间的数据拯救低压检测中断与 MRAM 快写在工业设备里最难搞的数据往往是掉电瞬间要保存的那一份当前工件的加工位置、伺服轴的绝对坐标、一次事件发生的准确时间。以前用 Flash掉电窗口只有几毫秒Flash 擦除一个扇区就要几十毫秒根本来不及只能靠大电容硬撑非常狼狈。换成 MRAM 之后这个问题算是基本解决了。MRAM 的写入是实时的每个字节跟着 SCK 时钟边沿进芯片CS# 拉高就完成全程不需要等待擦除。在 4MHz SPI 下写 32 字节大约 100 微秒出头。电源的保持电容只需要撑住这不到 200 微秒的时间常规的钽电容完全够用。代码层面的做法是利用 PIC18F86K22 的低压检测模块。把低压检测阈值设置在略高于 MCU 最低工作电压的位置比如 3.3V 系统里选 2.8V 档位电压掉到阈值时触发 LVD 中断在中断里关闭其他中断用最快速度把关键变量写入 MRAM然后进入低功耗等待直到系统完全断电。我实测过很多次主电源断开后MCU 从 LVD 中断触发到数据写完整个动作加起来大约 150 微秒即使电源电压继续快速下跌也足够完成。这个体验和以前用 Flash 时完全不同心里踏实得多。4.3 日志记录设计顺序写环形缓冲区与序列号运行日志是另一个让存储方案拉开差距的场景。以前用 Flash 写循环日志要处理扇区擦除、磨损均衡、日志头更新电机每转一圈可能就写一条十万次寿命根本撑不了几个月。MRAM 没有磨损问题写日志可以退回到最朴素的设计固定一块区域做环形缓冲区每条日志头带一个递增的序列号顺序往后写写满末尾后从头覆盖。因为 MRAM 的每个 bit 都可以任意改写覆盖老日志不需要先擦出整个扇区直接写新内容就行。形象点说Flash 写日志像用铅笔在作业本上写写错了要撕掉整页重写MRAM 写日志像用白板笔新内容直接覆盖旧内容省了“撕页”这一步。日志读回的时候按序列号判断新旧顺序。读扫描时遇到序列号明显跳变的位置就是最新的日志起点。这个算法实现很简单但以前在 Flash 上做同样的逻辑代码量至少要翻一倍。4.4 产线自检用 RDID 确认芯片在位还有一个容易被忽略的实用细节MR25H40CDF 支持 RDID 命令上电自检时可以读回制造标识。产线上如果贴错料、虚焊、或者买到翻新片SPI 读写可能偶尔正常偶尔异常很难定位。自检时发 0x9F 读回固定字节和数据手册比对能直接把异常挡在出厂前。我习惯在开机初始化流程里加一步读 ID 失败超过三次直接进入故障状态通过串口上报“MRAM 自检失败”。这一步成本极低但给现场维护省了很多事。5. 调试实录MRAM 读写常见故障与排查速查表5.1 五个高频故障的现象与根因分析第一类故障写使能不生效状态寄存器 WEL 始终为 0。这个现象往往是软件问题。我遇到过最隐蔽的一次是 SPI 引脚方向没配对SDO 引脚被设成了输入导致 WREN 命令根本没送到芯片。排查时用逻辑分析仪抓 CS# 低电平期间 SI 线上的数据如果看不到 0x06 这个命令字节问题基本就在 MCU 侧。另外WREN 命令本身要保证 CS# 完整拉高再拉低芯片才会真正锁存写使能状态命令之间 CS# 高电平时间太短也可能导致 WEL 置位失败。第二类故障读回来的数据全是 0xFF。十有八九是 SPI 模式不对。MR25H40CDF 只支持 Mode 0 和 Mode 3如果配置成了 Mode 1 或 Mode 2芯片根本不识别。在 PIC 上要反复确认 CKP、CKE 的组合。另一个常见原因是 HOLD# 引脚悬空它一旦被噪声干扰拉低SO 线就维持最后状态读出来的数据照样是 0xFF。检查一下 HOLD# 是否可靠接 VDD。第三类故障写数据成功但断电后数据丢失。这个最隐蔽我一开始也怀疑过芯片。后来发现是掉电瞬间电压快速下降MCU 在低噪声状态下执行了写操作但写了一半电压就撑不住了。这不是 MRAM 的问题而是整个系统的掉电策略没做好。解决方法是前面说的 LVD 中断 双备份区让数据保存动作在受控的电压窗口内完成而不是等系统已经在复位边沿上挣扎。第四类故障读写偶发一位错。通常不是芯片坏了而是信号完整性问题。SCK 走线太长、电源纹波大、或者 SPI 时钟跑得太高都会让 SO 线上的数据在采样点处不稳定。排查时先把 SPI 分频往下降了再看比如从 10MHz 降到 4MHz故障消失就说明是时序余量不足。第五类故障5V 供电的 MCU 在 MRAM 上频繁读写失败甚至芯片异常发烫。这是电平超限导致的长期损伤软件怎么调都救不回来。处理方案只有一个把系统改成 3.3V 供电或者加电平转换。5.2 问题排查速查表现象可能原因排查与处理方案写入后数据不变忘发 WREN、WEL 未置位写前调用 MR_WriteEnable读状态寄存器确认 WEL1读回全 0xFFSPI Mode 配置错、HOLD# 悬空核对 CKP/CKE 组合HOLD# 接 VDD断电后数据丢失掉电瞬间写操作不完整LVD 中断快写 双备份区 CRC偶发一位错信号完整性差、SCK 过快降低 SPI 分频加退耦电容缩短走线芯片异常发烫5V 电平超限损伤器件换 3.3V 供电或加电平转换RDID 读不出正确字节虚焊、贴错料、SPI 不通用逻辑分析仪抓命令用万用表量 CS# 到 GND5.3 调试工具、波形判断与实际经验调试这套存储链路我常用的工具是逻辑分析仪和 MPLAB X 的调试器。逻辑分析仪采样率设成 SCK 的四倍以上同时抓 CS#、SCK、SI、SO 四根线一眼能看出命令帧是否完整、数据是否对齐。比如发 READ 命令后CS# 低电平期间应该依次看到 0x03、地址三字节、然后 SO 上流出数据。如果地址字节对不上多半是 24 位地址的顺序发错了。在 MPLAB X 的调试模式里可以直接看 SSP1STATbits.BF 和 SSP1CON1bits.SSPEN 的状态。如果发完字节后 BF 长时间不置位说明总线上没有设备应答时钟重点查硬件连接。还有一个经验是写代码时故意在每一笔读写之间加一个极短的延时保证 CS# 高电平时间足够避免连续快速访问时时序太紧凑。我的系统里 4MHz SPI 本来余量就大加上后更稳。调试中我还发现MR25H40CDF 的封装很小贴片后虚焊并不少见。遇到读全 0xFF 的板子先别急着怀疑程序——用万用表量 CS# 到 MCU 引脚是否有连接VDD 到 3.3V 是否正常往往能省下一整个下午的查错时间。这套方案我已经在三个项目里跑过一台现场控制器、一台测试仪器、一套产线数据采集终端。用 MR25H40CDF 配合 PIC18F86K22最大的感受是心理负担小很多——不用算擦写均衡不用怕写一半丢数据不用维护电池。最开始我还有点不习惯 MRAM 的价格但摊到整个产品的生命周期里省下的维护成本和代码复杂度早就把差价赚回来了。最后再分享一个让设计更从容的小技巧这组代码对更大容量的 MRAM 也基本通用。如果以后工程需要从 4Mbit 升级到 8Mbit比如换 MR25H80CDF地址宽度本身兼容驱动层几乎不用改动。选型时把容量往上留一档后续加功能会轻松不少。
返回列表