ARTICLE DETAIL

资讯详情

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

MRAM替代EEPROM:PIC18与MR25H40CDF构建工业非易失存储方案

MRAM替代EEPROM:PIC18与MR25H40CDF构建工业非易失存储方案 做工业产品最怕的不是运算能力不够而是现场突然掉电之后那点关键数据到底还在不在。前阵子我把一套数据记录方案从 EEPROM 换成了 MRAM具体配置就是 MR25H40CDF 加 PIC18F85J50两颗芯片走 SPI 接口在 3.3V 下完成非易失存储。这套组合在工业表计、控制器、数据采集器里相当典型能解决很多设备频繁写入、掉电丢失、存储芯片提前报废的麻烦。这篇直接把我从选型、硬件连接、固件实现到调试排坑的完整过程讲透适合正在做嵌入式存储方案、或者准备在项目里引入 MRAM 的人参考。1. 为什么在工业现场要换掉 EEPROM 和 Flash1.1 EEPROM 与 Flash 的三个“卡脖子”点传统做法里非易失存储无非是 SPI EEPROM 或者 SPI NOR Flash。芯片便宜、资料多工程师也熟。但真正放到工业现场长时间跑问题就出来了。第一是写入寿命。EEPROM 一般标 10 万到 100 万次擦写NOR Flash 更保守常见 1 万到 10 万次。如果设备每隔几秒就记录一次状态一天下来就是上万次写入寿命是按年算的复杂工况下可能撑不到产品设计周期结束。第二是写入速度。EEPROM 单字节写要等好几毫秒Flash 写一页之前还得先擦除整个扇区动不动就是几十毫秒甚至更久。在频繁存储、掉电中断、多任务并存的场景里这个时间窗口非常难受。第三是写入机制带来的复杂性。Flash 必须先擦后写、按扇区对齐日志型的覆盖写尤其痛苦你得在固件里维护擦写均衡和块管理代码量不小还容易出边界 bug。这三件事叠加在一起就逼着人去找一种“写起来像 SRAM、掉电不丢数据、寿命又长”的存储介质。MRAM 就是冲着这个需求来的。1.2 MR25H40CDF 能解决什么问题MR25H40CDF 是 Everspin 的 4Mbit SPI 接口 MRAM内部结构是 512K x 8工作电压 2.7V 到 3.6V工业级温度范围一般做到 -40°C 到 105°C。它的核心特点是写入不需要擦除单字节直接覆盖写入时间在百纳秒级擦写次数达到 10 的 14 次方级别基本不用考虑磨损问题数据保持能力可以到 20 年以上。我实际用下来最强烈的感受是它把软件复杂度降下来了。以前用 Flash 做环形日志要处理扇区擦除、写指针回卷、块平衡现在用 MRAM就是随便挑一个地址直接写写坏了一个字节就再写下一个字节不需要任何“搬运”逻辑。它没有页的概念连续写可以跨整片地址空间只要不越过 512K 边界就不存在 Flash 那种“当前页写满了要把下一页擦掉”的尴尬。在掉电数据保持这件事上MRAM 和电池供电的 SRAM 方案相比也更省心。不用电池、不用充电电路、不用上电初始化数据天生就是非易失的。这也是为什么很多计量、电力、轨交项目愿意在关键路径上用 MRAM而不是靠外部电池维持 SRAM。1.3 为什么配 PIC18F85J50PIC18F85J50 属于 Microchip 的低电压 J 系列工作电压 2.0V 到 3.6V这正好和 MR25H40CDF 的 2.7V 到 3.6V 匹配两边都可以稳定跑在 3.3V不需要额外做电平转换。它内部有 MSSP 模块原生支持 SPI 主模式最高可以跑到 48MHz 的内核频率对这个存储任务来说性能绰绰有余。选它还有一个很实际的原因在 8 位机上做这类表格类、记录类应用非常合适。记录仪、表计需要的运算并不复杂主要是处理中断、维护缓冲区、和上位机通信。PIC18 的外设库成熟XC8 编译器顺手引脚也够用。MRAM 的片选信号用任意 GPIO 就能控制不需要占用专门的硬件 SS 引脚。整体方案成本可控采购渠道也稳定非常适合量产型工业产品。2. 硬件连接与电路设计要点2.1 MR25H40CDF 引脚识别MR25H40CDF 常见的封装是 SOP-8也有 DFN-8 版本引脚定义基本一致只是封装尺寸不同。以 SOP-8 为例关键引脚是CS# 片选、SCK 时钟、SI 数据输入、SO 数据输出、VCC 电源、VSS 地还有一个 HOLD# 引脚。HOLD# 拉低时芯片会暂停 SPI 通信正常使用必须把它接高不能悬空。第一次用的时候建议仔细对一下数据手册的引脚图因为不同批次、不同温度等级的后缀可能对应不同封装网上搜到的引脚图不一定和你手上的芯片完全一致。我自己的习惯是把 SOP-8 的脚位图打印出来和手册对照一遍再画原理图省得焊接完才发现 SO 和 SI 接反。MR25H40CDF 引脚方向说明接到 PIC18F85J50CS#输入片选低有效任意 GPIO如 RC0SCK输入SPI 时钟MSSP1 SCKSI输入SPI 数据输入MOSIMSSP1 SDOSO输出SPI 数据输出MISOMSSP1 SDIHOLD#输入暂停通信低有效通过 10kΩ 电阻接 VCCVCC电源2.7V~3.6V3.3V并联 0.1μF 和 4.7μF 电容VSS电源地地GND2.2 电平匹配和基本连线PIC18F85J50 和 MR25H40CDF 都在 3.3V 供电这是最舒服的情况GPIO 直接互连不需要电平转换芯片。要注意的是有些工程师习惯把 PIC 和其他 5V 系列单片机混用如果 MCU 那边是 5V 供电这里就不能直连了必须加电平转换。MRAM 的 VCC 如果超过 3.6V芯片可能直接损坏这一点要在原理图评审时专门盯一下。SCK、SI、SO 三条线之间不需要特别处理正常走线即可。CS# 用 GPIO 控制上电初始化时先把 CS# 置高避免 MRAM 在主控还在配置 SPI 模块前被误选中。HOLD# 一定要拉高我见过有人把 HOLD# 悬空结果系统偶发卡死在 SPI 通信上查了很久才发现是这个引脚电平漂移导致芯片进入暂停状态。电源去耦要靠近 VCC 引脚放0.1μF 高频去耦电容是必须的如果板子布局允许再并一个 4.7μF 的钽电容或者陶瓷电容应对瞬间电流变化。MRAM 工作时电流不算小大致在几十毫安量级电源纹波如果过大高速 SPI 传输时可能出现偶发错误。2.3 PCB 布局的几条小经验如果 SPI 时钟跑在 10MHz 以上PCB 走线就不要绕太远SCK 和 SI 之间也不要靠得太近减少串扰。工业现场往往有电机、继电器这类干扰源我一般在靠近 MCU 一侧的 SPI 线上串联 33Ω 到 50Ω 的电阻用来抑制振铃。电阻值不要取太大超过 100Ω 会把波形边沿拖得很慢影响高速模式下的时序。另外一个容易忽视的点是接地。MRAM 的地引脚要直接回到主控的同一块地平面不要绕到电源模块那边再回来避免地电位差产生噪声。如果板子上有继电器或者感性负载最好在靠近负载的地方做好续流吸收否则打继电器瞬间的电压尖峰可能会串到 SPI 线上。3. SPI 指令集与状态寄存器细节3.1 SPI 模式选择与指令帧格式MR25H40CDF 支持 SPI Mode 0 和 Mode 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。我习惯用 Mode 0因为 PIC18 的 MSSP 配置起来更直接而且绝大多数 SPI 器件默认都是 Mode 0。读写指令的帧结构是片选拉低后先发送 1 字节操作码再发送 3 字节地址最后才是数据。地址虽然用了 3 字节但这个芯片实际上只有 512K 字节也就是 19 位有效地址最高 5 位必须保持为 0。这个细节很容易踩坑如果只按 16 位地址处理访问范围就只剩 64K数据写到后面会覆盖前面的内容整片都被搞乱。常用指令包括READ 读数据、WRITE 写数据、WREN 写使能、WRDI 写禁用、RDSR 读状态寄存器、WRSR 写状态寄存器、SLEEP 休眠、WAKEUP 唤醒。读写数据的操作码分别是 0x03 和 0x02和很多 SPI NOR Flash 一致因此兼容性很好。3.2 写使能和块保护机制MRAM 虽然写数据不需要擦除但为了兼容标准 SPI 协议仍然保留了一个写使能机制。每次往存储区写数据之前必须先发送 WREN 指令把状态寄存器里的 WEL 位置 1然后才能执行 WRITE 指令。写完一次之后WEL 位会自动清零所以连续两次写操作之间每次都得重新发 WREN。状态寄存器里还有 BP0 和 BP1 两个块保护位用来锁定存储区的上端区域防止程序跑飞时误写。默认情况下块保护是关闭的理论上可以直接使用。但为了保险我的初始化流程里会先执行一次 WRSR把状态寄存器清零确保所有保护位处于确定状态然后再开始读写操作。注意WRSR 本身也需要先发 WREN否则写状态寄存器不会生效。这是很多人第一次调 MRAM 卡住的原因写完 WEL 之后忘记状态寄存器也要走同样的使能流程。3.3 关键时序参数与休眠模式MRAM 手册里会给出 CS# 高电平最短保持时间、CS# 下降沿到 SCK 上升沿的建立时间等参数单位都是纳秒级。实际调的时候在 CS# 拉高之后加几个 NOP 或者 1 微秒延时就能确保满足这些要求。PIC18F85J50 主频 48MHz 时一条指令就几十纳秒写几个空操作成本很低没必要去抠极限时序。如果设备长时间不访问 MRAM可以发送 SLEEP 指令让它进入低功耗模式再做读操作之前必须发 WAKEUP 唤醒。这个机制在电池供电设备上很有价值待机功耗能降不少。但要记住MRAM 上电后默认是退出睡眠的不需要专门初始化唤醒只有你自己主动发了 SLEEP才需要后续唤醒。所以代码里别一上来就发 WAKEUP那会白白增加一次没必要的操作。4. 固件实现PIC18F85J50 读写 MRAM 的代码级拆解4.1 SPI 初始化配置PIC18F85J50 的 MSSP1 模块配置不算复杂关键是分清几个控制位。SPI Mode 0 对应 CKP0、CKE0SMP 位设为 1让模块在传输末尾采样输入数据这样读回来的数据更稳。下面是 XC8 环境下的初始化代码// SPI1 初始化主模式Mode 0Fosc/16 void spi1_init(void) { SSP1CON1bits.SSPEN 0; // 先关闭 MSSP SSP1STATbits.SMP 1; // 输入采样于传输末端 SSP1STATbits.CKE 0; // Mode 0 数据边沿 SSP1CON1bits.CKP 0; // SCK 空闲为低 SSP1CON1bits.SSPM 0b0101; // SPI MasterFosc/16 SSP1CON1bits.SSPEN 1; // 使能 MSSP } uint8_t spi1_xfer(uint8_t out_byte) { SSP1BUF out_byte; // 写入发送缓冲 while (!SSP1STATbits.BF); // 等待接收完成 return SSP1BUF; // 返回收到的字节 }如果系统时钟是 48MHzFosc/16 就是 3MHz这个速度对 MRAM 来说已经绰绰有余。跑更高速度时可以改成 Fosc/4但要自己量一下波形是否干净。我的习惯是先把速度降下来调通功能再根据实际情况提升时钟避免一上来就把问题复杂化。4.2 写使能与单字节读写初始化完成后第一步封装写使能函数。每次写数据之前都要调用它#define MRAM_CS_LAT LATCbits.LATC0 // CS 接到 RC0 #define MRAM_CS_TRIS TRISCbits.TRISC0 void mram_cs_low(void) { MRAM_CS_LAT 0; } void mram_cs_high(void) { MRAM_CS_LAT 1; } void mram_write_enable(void) { mram_cs_low(); spi1_xfer(0x06); // WREN mram_cs_high(); } void mram_write_status(uint8_t value) { mram_write_enable(); mram_cs_low(); spi1_xfer(0x01); // WRSR spi1_xfer(value); mram_cs_high(); } uint8_t mram_read_status(void) { uint8_t status; mram_cs_low(); spi1_xfer(0x05); // RDSR status spi1_xfer(0x00); // 让时钟跑起来读回数据 mram_cs_high(); return status; }这里有个细节SPI 是全双工协议读数据的时候主机必须同时发送一个字节才能产生时钟给从机输出数据。我习惯于读操作里发送 0x00纯粹是为了产生时钟边沿读回来的字节才是真正要取的数据。完成这些底层函数后单字节读写就比较直接了uint8_t mram_read_byte(uint32_t addr) { uint8_t val; mram_cs_low(); spi1_xfer(0x03); // READ spi1_xfer((addr 16) 0xFF); spi1_xfer((addr 8) 0xFF); spi1_xfer(addr 0xFF); val spi1_xfer(0x00); mram_cs_high(); return val; } void mram_write_byte(uint32_t addr, uint8_t val) { mram_write_enable(); mram_cs_low(); spi1_xfer(0x02); // WRITE spi1_xfer((addr 16) 0xFF); spi1_xfer((addr 8) 0xFF); spi1_xfer(addr 0xFF); spi1_xfer(val); mram_cs_high(); }故意写成独立的读写函数是为了让测试代码能往下发写一个地址立刻读回来和预期值比对判断底层通信是否正常。这个自测逻辑在换板子、换芯片批次的时候非常有用。4.3 多字节连续读写实际业务很少只写一个字节更多是把一段配置或者一条日志记录整体写进去。MRAM 支持突发模式发完操作码和起始地址后数据可以连续发送或读取地址会自动递增。多字节写函数如下void mram_write_burst(uint32_t addr, const uint8_t *data, uint16_t len) { uint16_t i; if (addr len 0x80000) // 512K 字节边界检查 len 0x80000 - addr; mram_write_enable(); mram_cs_low(); spi1_xfer(0x02); spi1_xfer((addr 16) 0xFF); spi1_xfer((addr 8) 0xFF); spi1_xfer(addr 0xFF); for (i 0; i len; i) spi1_xfer(data[i]); mram_cs_high(); } void mram_read_burst(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; mram_cs_low(); spi1_xfer(0x03); spi1_xfer((addr 16) 0xFF); spi1_xfer((addr 8) 0xFF); spi1_xfer(addr 0xFF); for (i 0; i len; i) buf[i] spi1_xfer(0x00); mram_cs_high(); }和 Flash 不一样MRAM 不需要考虑“页边界”的问题只要保证不越过整片存储空间哪怕从 0x7FFF0 一直写到 0x7FFFF 也没关系。这个特性让连续日志记录变得非常轻松固件逻辑能省掉一大块“擦除和搬运”的代码。4.4 应用层封装配置参数区与环形日志底层读写稳定之后就要考虑实际业务数据的组织方式。我常用的是一个“参数区 日志区”的划分方案。参数区放在 MRAM 起始地址 0x00000存放设备的配置项。读取时先读整块数据校验魔数和 CRC如果校验失败就认为配置无效恢复默认参数。这样做的好处是即使掉电瞬间赶上一次写了一半系统上电也能自己发现数据异常不会带病运行。typedef struct { uint8_t magic[4]; // 固定为 C,F,G,1 uint16_t sample_rate; uint16_t alarm_threshold; uint8_t reserved[8]; uint16_t crc; // 对前面字段计算 CRC16 } AppConfig;日志区放在地址 0x00100 之后每条记录固定长度写指针存两个备份槽位。写入时先把写指针读到 RAM然后把日志写到写指针指向的位置写完再把写指针递增并写回备份槽位。两个备份槽交替使用避免写指针本身因为掉电损坏typedef struct { uint32_t seq; // 序号 uint32_t timestamp; // 时间戳 int32_t value; // 采样值 } LogEntry; #define LOG_START_ADDR 0x00000100 #define LOG_ENTRY_SIZE sizeof(LogEntry) #define LOG_PTR_ADDR_A 0x0007FFF0 #define LOG_PTR_ADDR_B 0x0007FFFC uint32_t log_read_write_ptr(void) { uint32_t a, b; mram_read_burst(LOG_PTR_ADDR_A, (uint8_t*)a, 4); mram_read_burst(LOG_PTR_ADDR_B, (uint8_t*)b, 4); if (a b) return a; return 0; // 简单处理不一致就从头开始 } void log_write_ptr(uint32_t ptr) { mram_write_burst(LOG_PTR_ADDR_A, (uint8_t*)ptr, 4); mram_write_burst(LOG_PTR_ADDR_B, (uint8_t*)ptr, 4); }这个方案比 Flash 的日志实现简单太多。以前用 Flash 时代码要区分“当前扇区在哪”“下一个扇区要不要先擦除”“脏数据怎么搬”现在 MRAM 把这些全部省掉了。固件里只需要维护一个写指针剩下的事情交给存储芯片本身。5. 调试实录与常见问题排查5.1 读出来全是 0xFF 或全 0x00这是最经典的“第一次上电”问题。读回全 0xFF先查电源和 HOLD#。MRAM 没供电时读出来往往都是 0xFFHOLD# 悬空或者被拉低时通信被暂停读出来的数据也会异常。其次查 SPI 模式如果 MCU 侧配成了 Mode 1 或者 Mode 2时钟边沿不匹配数据完全对不上。读回全 0x00 通常是 MISO 线路问题。SO 引脚没有真正连到 MCU 的 SDI或者 SDI 引脚方向配成了输出都会导致读到固定的 0。排查时用万用表量一下 CS# 电平是否切换再看 SCK 有没有时钟输出一层层缩小范围。5.2 写入之后读出来还是旧值写不进去的头号原因是漏发 WREN。MRAM 复制了 SPI Flash 的写保护习惯WEL 位没置 1WRITE 指令会被忽略。看到这种现象先用 RDSR 读一下状态寄存器如果 WEL 一直是 0说明 WREN 没有正确执行要么是命令时序不对要么是 CS# 在整个 WREN 过程中没有保持低电平。第二个常见原因是块保护位。不少工程师会在初始化时擦写状态寄存器不小心把 BP0/BP1 写成非零把存储区上半部分保护起来。写入地址如果落在保护区域内写操作会被拒绝。处理办法就是初始化时写一次 WRSR把状态寄存器清零同时读回来验证一遍。5.3 高速 SPI 下的偶发数据错误如果低速下一切正常把 SPI 时钟调高之后开始偶发出错大概率是信号完整性导致。我实际遇到过跑 10MHz 时正常、跑 20MHz 时数据偶尔错位的情况。排查时用一个逻辑分析仪抓 SCK、SI、SO、CS# 四根线重点看数据线上的振铃和边沿毛刺。解决方向有三个降低 SPI 时钟、缩短走线、在数据线上串联 33Ω 电阻。还可以把 SMP 位配置为 1让采样点往后移给信号多留一点稳定时间。如果这些手段都不够就要考虑 PCB 布局问题了看看 SCK 是否和 SI 并行走线太长。5.4 掉电瞬间数据损坏的处理MRAM 本身速度很快写入窗口比 EEPROM 小很多但任何存储方案都怕“写入进行到一半断电”。我的经验是两条腿走路。第一在应用层给每条记录加 CRC 或者简单的累加和校验读取时先校验再使用。第二对关键信息采用双槽位交替写入比如写指针写在 A 地址下一次更新写到 B 地址上电时比较两个槽位取合法的那个。这样即使某一次写入正好被断电打断也不会丢全部数据。另外不要在上电后立刻对 MRAM 做大面积擦写先确认电源和主时钟稳定再操作。工业产品如果有外部复位监控芯片最好等复位释放后再初始化存储区。6. 这套方案量产之后的一些体会如果让我重新选我还是会选 MR25H40CDF 和 PIC18F85J50 这个组合但有几个地方会做得更早。第一原理图阶段就把 HOLD# 的接法标准化不要每次画板都重新想。第二固件里预留一个存储自检函数产线测试时对整片 MRAM 做一遍随机读写和 CRC 校验能提前筛掉坏片。第三SPI 时钟一开始就按最终目标配置不要为了调通而长期跑低速免得后来改时钟又引入新问题。最后分享一个小技巧在量产固件里把 MRAM 的型号和版本信息也存进去比如把 “MR25H40CDF” 这个字符串写在一个固定地址后续如果需要远程确认芯片批次直接读出来就知道。这颗芯片写寿命长也不需要担心这个信息会被频繁更新磨损工业产品维护起来会省不少心。
返回列表