去年给客户做一台现场数据记录仪表核心需求很简单把温度、运行状态、累计电量这类参数定期存下来断电之后不能丢。最开始用一颗 SPI EEPROM结果客户现场电源质量比较差变频器一启动就是电压跌落几次之后参数变成 0xFFFF记录直接乱掉。后来把存储换成 Everspin 的 MR25H40CDF主控用 Microchip 的 PIC18LF45K50问题才算彻底解决。这篇文章把整个项目的选型思路、硬件连接、驱动实现、掉电保护设计以及我在现场踩过的坑完整写一遍给同样在工业数据存储、参数掉电记忆、运行日志记录上折腾的人做个参考。1. 为什么工业现场我最终选了 MRAMEEPROM 和 Flash 都坑过我1.1 现场数据丢失事件先说最初那台设备。控制板是一颗常见的 8 位单片机外挂 8KB 的 SPI EEPROM用来保存累计运行时间和最近几十条告警记录。逻辑很简单每次产生新数据就写一次掉电前再补一次。静态测试怎么都正常但到了客户现场只要变频器重载启动或者接触器吸合偶尔就会出现“参数全丢”或者“读回来是一堆 0xFF”的现象。排查到最后问题其实不在软件逻辑而在介质特性。EEPROM 写一个字节要 3~5ms现场电压跌落几十毫秒内就可能出现供电坍缩掉电检测程序还没等到数据写完芯片已经进入欠压状态。更麻烦的是EEPROM 的写寿命通常只有 10 万到 100 万次频繁覆盖写加上电压不稳坏块概率会明显上升。这种场景下再优化代码也只能降低概率不能从根上解决。1.2 三种非易失存储器的真实差异很多工程师第一反应是“换一颗容量更大的 EEPROM”但那只解决容量问题不解决写入速度和寿命问题。工业频繁写、掉电随机的场景我更倾向于直接对比三类常用器件SPI EEPROM、SPI NOR Flash、SPI MRAM也有人会考虑 FRAM。项目MR25H40CDFMRAM普通 SPI EEPROMSPI NOR FlashFRAM写入寿命官方按 10^14 次量级标识工程上可以按无限次算10 万~100 万次1 万~10 万次10^12 次左右也很高写一个字节的延时几乎无写延时写入在 SPI 事务内完成3~5ms页编程加扇区擦除毫秒级起步几乎无写延时是否需要擦除不需要直接覆盖写不需要必须先擦除整个扇区再写不需要掉电敏感度磁存储电荷丢失问题不存在电荷存储掉电半写容易坏电荷存储掉电可能丢整页相对不敏感典型容量几 Mbit 到几十 Mbit通常 1Mbit 以内大容量为主容量选择较少价格最贵最便宜便宜中等偏上表格里最打动我那一条其实是“不需要擦除”。NOR Flash 每次改写都要先擦整块意味着代码里要维护扇区状态、写磨损均衡EEPROM 虽然没有擦除但字节写速度实在太慢。MRAM 把这两者的短板都避开了覆盖写、快、寿命长。1.3 MR25H40CDF 的值在哪里MR25H40CDF 是 Everspin 的一颗 4Mbit SPI MRAM换算下来是 512KB做参数区加运行记录区完全够用。它内部存储单元是磁隧道结写数据靠电流改变磁化方向而不是像 Flash 那样靠电荷保存状态所以天然不怕长时间断电也不容易出现“半写”状态。实际工程里我最看重三个点。第一写操作不需要擦除任意地址都能按字节覆盖代码里不需要维护“哪个扇区干净”这种状态。第二写寿命极高运行参数每次变化都写一次也没压力不用像对 Flash 那样精心设计磨损均衡。第三工作温度范围和抗干扰特性比普通 EEPROM 更适合工业控制柜环境。价格确实比 EEPROM 贵不少但在现场跑一趟换板子的人工成本、停机损失一般都比芯片差价款高得多。这个账算清楚之后选型就很自然了。2. 硬件搭台PIC18LF45K50 与 MR25H40CDF 怎么接才靠谱2.1 PIC18LF45K50 选型理由主控选 PIC18LF45K50最直接的原因是这个项目需要 USB 接口做现场数据导出同时希望整块板子工作在 3.3V 单电源下。PIC18LF45K50 是 Microchip 的 8 位 USB MCU工作电压可以到 1.8~3.6V内置 32KB Flash、1.5KB 左右的 SRAM、EEPROM、双 MSSP 模块和丰富的中断资源。MR25H40CDF 的供电范围也是 3.3V 系统合适所以单片机和存储之间不需要电平转换SPI 直连即可。MSSP 模块既能当 SPI 用也能当 I2C 用一个模块接 MRAM另一个模块可以留给外部传感器或者调试口硬件上很灵活。另外PIC18LF45K50 内置 BOR 和 LVD 掉电检测这对做掉电数据保存非常重要。后面章节我会专门讲怎么用这些资源配合 MRAM 做“最后 100 微秒”的完整写入。2.2 SPI 接线与外围电路MR25H40CDF 是标准 SPI 从机接口信号只有片选、时钟、数据输入、数据输出四条再就是写保护和保持引脚。我的接法如下表实际硬件按原理图微调即可。MR25H40CDF 引脚方向推荐接法CS输入接 PIC 任意 GPIO本项目用 RC0同时加 10k 上拉到 3.3VSCK输入接 PIC MSSP1 的 SCK1SI输入接 PIC MSSP1 的 SDO1SO输出接 PIC MSSP1 的 SDI1WP输入必须接 3.3V不能悬空HOLD输入必须接 3.3V不能悬空VDD电源3.3V靠近芯片放 0.1uF 陶瓷电容VSS地接 GND单点回流WP 和 HOLD 这两个引脚最容易被人忽略。WP 一旦被拉低MRAM 的写操作会被禁止程序表现就是“写命令发了数据读回来没变”。HOLD 一旦被拉低SPI 时钟和数据会被冻结直接导致总线通信错乱。工业现场电磁环境复杂这两个引脚绝对不要悬空最稳妥的办法就是直接连接到 3.3V如果要用软件控写保护那也必须通过 GPIO 输出确定的高电平。SPI 走线尽量短片选和时钟线距离 MRAM 控制 5cm 以内。我在 SCK、SI、CS 线上各串了一个 33Ω 电阻用来抑制振铃和过冲这在现场长连接线时非常管用。2.3 电源和复位对存储可靠性的隐形影响很多人以为 MRAM 既然不怕掉电电源就可以随便做。实际不是这样。SPI 通信时 MCU 和存储芯片同时翻转VDD 上的噪声如果超过一定幅度读回来的数据就可能错 bit。工业仪表里常见做法是宽压输入再加一级 LDO 稳压到 3.3V不要直接从高压 DC-DC 的输出取电给存储系统。我给 MRAM 的 VDD 脚放了 0.1uF给 MCU 的每个电源脚也放了 0.1uF整个 3.3V 域再加了一颗 10uF 钽电容做低频储能。这样做的另一个原因是配合掉电检测给最后的数据提交提供能量缓冲。复位方面也要留意。MCU 异常复位时如果 CS 恰好处于低电平MRAM 会把后续总线状态当成一个新的命令序列有可能误写。所以 CS 信号除了接 MCU GPIO一定要外加 10k 上拉电阻保证 MCU 复位期间 CS 稳定在高电平从机不被意外选中。3. 驱动代码SPI 初始化、状态机和多字节读写实现3.1 MSSP 配置成 SPI 主模式下面这套代码基于 XC8 编译器和 PIC18LF45K50 的 MSSP1 模块。MR25H40CDF 支持 SPI Mode 0 和 Mode 3我习惯用 Mode 0也就是 CPOL0、CPHA0。PIC 的 MSSP 寄存器描述里的 CKE、CKP 组合要按数据手册核对别照搬其他单片机示例。#include stdint.h #define MR_CS_LAT LATCbits.LATC0 #define MR_CS_TRIS TRISCbits.TRISC0 #define MR_WP_LAT LATCbits.LATC1 #define MR_WP_TRIS TRISCbits.TRISC1 #define MR_HOLD_LAT LATCbits.LATC2 #define MR_HOLD_TRIS TRISCbits.TRISC2 #define MR_CMD_READ 0x03 #define MR_CMD_WRITE 0x02 #define MR_CMD_RDSR 0x05 #define MR_CMD_WRSR 0x01 #define MR_CMD_WREN 0x06 #define MR_CMD_WRDI 0x04 #define MRAM_SIZE 0x80000UL // 512KB void MRAM_SPI_Init(void) { // MSSP1 配置为 SPI 主模式时钟 Fosc/4 SSP1STAT 0xC0; // SMP1, CKE0 - 对应 SPI Mode 0 SSP1CON1 0x20; // SSPEN1, CKP0, SSPM0000 - Master, Fosc/4 MR_CS_TRIS 0; MR_WP_TRIS 0; MR_HOLD_TRIS 0; MR_CS_LAT 1; MR_WP_LAT 1; MR_HOLD_LAT 1; }时钟选择上Fosc/4 在我这个项目里是 12MHzMR25H40CDF 完全有能力跑这个速度。如果现场走线长、干扰大我建议直接把分频降下来后面第 5 章会讲实测对比。3.2 底层收发与状态寄存器操作SPI 是全双工总线MSSP 模块每发送一个字节的同时也会收到一个字节。所以“读 MRAM 数据”这个动作本质上是主机持续发送 0x00把从机 SO 上的数据一个个换回来。unsigned char MRAM_spi_io(unsigned char tx) { SSP1BUF tx; while (!SSP1STATbits.BF) { // 等待发送完成并接收完成 } return SSP1BUF; }片选操作和状态寄存器读取如下void MRAM_CS_LOW(void) { MR_CS_LAT 0; } void MRAM_CS_HIGH(void) { MR_CS_LAT 1; } unsigned char MRAM_ReadStatus(void) { unsigned char sr; MRAM_CS_LOW(); MRAM_spi_io(MR_CMD_RDSR); sr MRAM_spi_io(0x00); MRAM_CS_HIGH(); return sr; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_spi_io(MR_CMD_WREN); MRAM_CS_HIGH(); } void MRAM_WaitReady(void) { // MRAM 几乎没有内部写延时WIP 位通常很快为 0 // 但保留轮询操作方便后续迁移到其他 SPI 存储 while (MRAM_ReadStatus() 0x01) { // 等待写操作完成 } }读状态寄存器的操作里我会额外加一个简短超时计数防止总线异常导致死循环。工业现场单片机跑飞是常见事任何 while 循环都建议有溢出退出的保护。3.3 多字节读写函数与缓冲区策略MR25H40CDF 的容量是 512KB地址需要 24bit也就是三字节。READ 和 WRITE 命令都遵循同一个格式命令字节 3 字节地址 数据。void MRAM_Read(uint32_t addr, unsigned char *buf, uint32_t len) { if ((addr len) MRAM_SIZE) { return; } MRAM_CS_LOW(); MRAM_spi_io(MR_CMD_READ); MRAM_spi_io((addr 16) 0xFF); MRAM_spi_io((addr 8) 0xFF); MRAM_spi_io(addr 0xFF); while (len--) { *buf MRAM_spi_io(0x00); } MRAM_CS_HIGH(); } void MRAM_Write(uint32_t addr, const unsigned char *buf, uint32_t len) { if ((addr len) MRAM_SIZE) { return; } MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_spi_io(MR_CMD_WRITE); MRAM_spi_io((addr 16) 0xFF); MRAM_spi_io((addr 8) 0xFF); MRAM_spi_io(addr 0xFF); while (len--) { MRAM_spi_io(*buf); } MRAM_CS_HIGH(); MRAM_WaitReady(); }写入之前必须先发 WREN 命令否则 MRAM 会忽略后面的 WRITE 操作。这是 SPI 存储类器件的通用规矩很多人第一次调的时候会在这个地方卡住数据看起来发出了但读回来全是旧值。因为 MRAM 没有擦除限制代码里不需要处理页边界、扇区擦除这些逻辑。我直接把存储规划成两块低地址 0x00000 附近放参数区高地址 0x40000 附近放运行记录区两边的写入都是简单的覆盖式。4. 掉电保存与数据完整性工业场景的保命设计4.1 掉电监测与供电电容的配合片子本身再快也得给 MCU 一个“知道自己要死了”的机会。PIC18LF45K50 内置的 LVD 低电压检测模块可以在 VDD 跌到设定阈值时触发中断我把它配置在 3.0V 左右。一旦 LVD 中断触发程序立即停止非必要的任务只做一件事把手头这一帧数据的 CRC 算好写到 MRAM 的提交标志位然后进入低功耗等待。这个过程有多长按 128 字节数据帧计算SPI 时钟 12MHz发送 1 字节命令 3 字节地址 128 字节数据总共大约 (13128) x 8 / 12MHz也就是不到 100 微秒。供电电容这边我在 3.3V 域放了 100uF 的储能电容。按掉电时整板工作电流 10mA 估算电容从 3.3V 跌到 2.7V 能维持的时间大约是 100uF x 0.6V / 10mA 6ms。这足够覆盖掉最后那 100 微秒的写事务还能给 BOR、LVD 复位留出余量。实际验证时用电子负载模拟从 3.3V 快速跌到 0V观察 MRAM 读回的数据帧依然是完整的。4.2 双区交替写入与 CRC 校验MRAM 天然不容易丢数据但工程上不能只靠器件特性。我习惯在 MRAM 里做“双槽”方案同一份业务数据准备两个存储槽位交替写入每次启动时选择最新且合法的一帧。帧结构大致如下偏移长度内容02帧头 magic例如 0xAA5522帧序号 seq递增42数据长度 len6N业务数据6N2CRC16 校验8N1提交标志例如 0xC3写入顺序上有个关键点先写数据体和 CRC最后写提交标志。提交标志放在单独偏移最好单独做一次 CS 拉低、拉高的写事务。这样哪怕中间掉电提交标志还是旧值启动时就会判定这一帧无效继续用另一个槽的旧数据。MRAM 不需要擦除所以覆盖写旧帧非常直接不像 Flash 那样还得先擦块、再写页码代码量省了一截。启动读取时我把两个槽都读出来依次检查 magic、CRC、seq。合法的槽中seq 较大者作为当前有效数据。运行过程中每隔一段时间再写一次两个槽轮流使用即使发生物理损坏也还能退化到单槽工作。4.3 工业现场抗干扰的几道防线SPI 本身不是抗干扰最强的总线长度一长就容易出问题。工业控制柜里变频器、接触器在旁边我的处理思路是分层防御物理层SPI 四根线尽量短并远离动力线必要时加磁珠SCK、SI、CS 串 33Ω 电阻HOLD 和 WP 强制拉高。协议层每次读写都带地址边界检查读状态寄存器有超时保护通信异常就重新初始化 MSSP 模块不让 SPI 状态机卡死在半空。数据层帧头、CRC、seq 三重校验任何一关不过就丢弃这一帧。因为 MRAM 写寿命长即使偶发错误真的写脏了一个帧下次启动也能自动回退到另一个槽。复位层MCU 开启看门狗跑飞后强制复位重新初始化时 CS 确定拉高MRAM 不可能残留一个被中断的写序列。实际现场跑了两个月没有再出现一例参数丢失。这中间当然有软件配合但介质选对了问题确实会简单很多。5. 实测数据与踩坑记录含时序抓波5.1 不同时钟频率下的表现与最佳工作点我在实验室用同一块板子测了 1MHz、4MHz、12MHz、20MHz 四档 SPI 时钟数据线长度控制在 3cm 左右结果如下。SPI 时钟实验结果说明1MHz稳定兼容性最好适合长线4MHz稳定现场抗干扰和速度兼顾12MHzFosc/4稳定我项目里的工作点20MHz勉强稳定短走线、电源干净时可以电源纹波大时偶尔读错最终我把量产版本锁定在 4MHz。原因很简单现场走线长度不可控4MHz 的性能已经完全够用没必要为了节省几十微秒去赌电源质量。MRAM 的数据手册标称可以跑更高但“手册标称值”和“现场长期稳定值”之间我永远选择后者。5.2 容易翻车的几处细节整个调试过程中最典型的几个坑我专门记一下。第一SI 和 SO 接反。表现是写入后立刻读数据对不上或者读回来全是 0xFF。排查这种问题最快的方法是写一个固定 pattern比如在 0x00000 写 0xA5、0x5A、0x55、0xAA再原地址读回比较。如果第一个字节就对不上先怀疑接线而不是怀疑驱动代码。第二WP、HOLD 悬空。WP 悬空时周围电磁干扰可能把它拉低导致写入被拒绝HOLD 悬空更加隐蔽数据传到一半被暂停读回来的内容错位。解决方法是硬件直接接 3.3V程序里也顺手把对应 GPIO 拉高。第三WRITE 命令之前漏了 WREN。SPI MRAM 上电后写使能锁存默认是关闭状态必须先发 0x06 再发 WRITE。漏掉的典型现象是写入函数没有报错数据却一直是旧值。第四地址超界。512KB 空间最大地址是 0x7FFFF连续读或写一旦越过末尾部分 SPI 存储芯片会回卷到起始地址继续操作。我在读写函数开头做了 addr len 的边界检查宁可函数直接返回也不要出现悄悄覆盖 0x00000 参数区的情况。第五状态寄存器的块保护位。MRAM 默认状态寄存器是 0x00写保护关闭。但如果你或者同事曾经调用过 WRSR 设置了 BP0、BP1MRAM 就会把部分地址区域变成只读此时写入看起来成功读回来却是旧数据。遇到“写不进去”的问题第一步先读状态寄存器确认保护位状态。5.3 给后续移植/扩展的一些建议这套驱动虽然写在 PIC18LF45K50 上但命令序列完全遵循通用 SPI 存储协议移植到 STM32、ESP32 或者 Linux 的 spidev 都很直接。换平台时重点检查三处SPI 模式选 Mode 0 还是 Mode 3MRAM 都支持地址宽度是不是 24bit取决于你用的具体容量CS 引脚在初始化时默认拉高避免一上电就误选中从机。如果后续要扩容到更大容量的 MRAM读写函数基本不用动只需要改MRAM_SIZE宏和地址掩码。如果是换到只有 16bit 地址的小容量 MRAM记得把地址发送从三字节改成两字节这个最容易漏。我现在的习惯是样机阶段就把 SPI 测试点留出来所有读写都先用逻辑分析仪抓一遍波形确认 CS、SCK、最后一位数据的时序都干净了再进产线。逻辑分析仪比示波器好用因为可以快速验证整帧命令和数据的字节顺序。
阅读完成 · 觉得有帮助?