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

资讯详情

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

MRAM替代EEPROM:PIC18F67K40工业设备掉电保存方案

MRAM替代EEPROM:PIC18F67K40工业设备掉电保存方案 这个月给一台工业设备改固件客户反复抱怨断电后配方参数偶尔全丢。原方案用串行 EEPROM 存参数日志数据更没地方放我直接把存储芯片换成了 MR25H40CDF主控端用 PIC18F67K40从底层 SPI 驱动到环形日志区整套重新写了一遍。这篇文章就是这次改造的完整记录适合正在做嵌入式数据存储、设备参数掉电保存或者想把日志方案从“能存”做到“存得住”的朋友参考。MR25H40CDF 是 Everspin 的 4Mbit 串行 MRAMPIC18F67K40 是 Microchip 的 8 位 MCU两者在工业设备里都是很常见的组合。MRAM 最大的特点是没有擦写寿命限制写入速度快在数据需要频繁更新的场合比 EEPROM 和 NOR Flash 都合适。后面我会把硬件接线、SPI 驱动、数据格式设计、常见排查手段一步步拆开讲代码直接可以抄过去改。1. 为什么是 MR25H40CDF 加 PIC18F67K401.1 工业数据存储的三个硬需求工业设备的存储需求跟消费电子不太一样我先说三个最典型的场景第一是参数频繁更新。比如设备上的配方参数、校准系数工人调机的时候可能几秒钟就写一次一天下来写几千次很常见。普通 EEPROM 的擦写寿命一般是 100 万次看着挺多但按一天 3000 次算一年多就逼近极限。NOR Flash 更麻烦寿命只有几万次到十几万次必须先擦后写直接拿来做参数存储根本不现实。第二是掉电数据不能丢。产线设备经常直接拉闸如果数据正写到一半突然断电EEPROM 和 Flash 都可能出现状态异常。MRAM 是磁阻存储写入过程不需要等待电荷保存掉电瞬间数据就已经稳定了天然适合这种场景。第三是读写的片外数据和主控之间的通信不能太复杂。PIC18F67K40 这种 8 位 MCU 算力有限跑不了复杂的 Flash 文件系统最好用简单的 SPI 读写就能搞定。MR25H40CDF 的接口就是标准 SPI指令集简单对 8 位机非常友好。1.2 选型对比MRAM 与 EEPROM、NOR Flash我整理了一个对比表方便大家理解 MRAM 在中间的位置项目串行 EEPROMNOR FlashMR25H40CDFMRAM存储原理电荷保存电荷保存磁阻状态写寿命约 100 万次1 万到 10 万次无实际磨损限制写前擦除不需要需要不需要写入等待页写 3~5ms擦除几百 ms 到秒级基本无等待单条写长度按页 8~256 字节按扇区有限长度跨页需拆帧数据保持受温度影响受温度影响高温下仍能保持多年接口SPI/I2CSPI/并口SPI在 PIC18F67K40 这类的 8 位平台上如果既要容量又要耐磨性MRAM 几乎是唯一不用动架构的方案。选 4Mbit 是因为 512KB 空间能同时放下参数区和日志区后续扩展也够用。很多人一听到 MRAM 就觉得贵实际上在小批量工业设备里一块 MR25H40CDF 带来的可靠性提升分摊到售后成本里是很划算的。选 PIC18F67K40 也没有特别复杂的原因它自带 MSSP 外设可以跑 SPIIO 电平 3.3V 和 MRAM 直接对接而且这颗料在工业项目里供货稳定。更重要的是团队对这个系列已经很熟固件认证流程不用重做这是项目稳定性里容易被忽略的一环。2. 硬件连接先把这个 8 脚芯片接明白2.1 最小系统连接图MR25H40CDF 是标准的 8 引脚 SPI 从设备引脚功能如下CS片选低有效必须由 MCU 控制SCKSPI 时钟SIMOSIMCU 输出到 MRAMSOMISOMRAM 输出到 MCUWP写保护输入低电平保护HOLD通讯暂停输入低电平暂停VCC3.3V 供电VSS地和 PIC18F67K40 连接时我把 CS 接到 RC2SCK 接到 RC3SI 接到 RC5SO 接到 RC4。不同开发板的引脚分配不一样重点不是具体用哪个引脚而是必须确认这些引脚在 PIC 内部要正确配置成 SPI 功能的复用口。特别注意 WP 和 HOLD 两个引脚。WP 如果被拉低状态寄存器会被保护WRITE 指令不生效。HOLD 如果被拉低SPI 通信会暂停数据线上的电平变化都会被忽略。这两个脚绝对不能悬空实际电路里 VCC 和它们之间各接一个 10k 电阻拉到高电平。2.2 电源、去耦与电平匹配工业环境里的电源纹波比消费类产品大得多MRAM 的 VCC 脚旁边必须放 100nF 陶瓷电容尽量靠近芯片引脚有条件再并一个 10uF 钽电容吸收前端电源的毛刺。电压匹配是整个项目里最容易踩的坑。MR25H40CDF 是 3.3V 供电如果 PIC18F67K40 也跑 3.3V那万事大吉直连就行。如果板子上其他电路是 5V 逻辑就不能直接把 MRAM 连到 5V 的引脚上。我的建议是加电平转换芯片或者用电阻分压加二极管的方式处理 SI、SCK、CSSO 方向因为 MRAM 是 3.3V 输出5V MCU 往往能直接识别但还是尽量加转换才算稳妥。PCB 布局还有一个容易被忽略的点SCK 和 SI 是高频信号要尽量短别来回绕否则波形边沿变差高速 SPI 时容易读到错误数据。CS 线一般变化频率低可以稍微走远一点但也不要和电源线平行长距离走线。2.3 为什么要单独留一个掉电检测引脚我在这次设计中额外用 PIC18F67K40 的 LVD 模块做断电检测或者外接一个简单的电阻分压网络监测 VCC。目的很简单断电瞬间把正在攒的日志记录写进 MRAM。虽然 MRAM 本身写入很快不需要像 Flash 那样预留擦除时间但 MCU 检测到掉电到彻底没电之间还是需要几百微秒。PIC18F67K40 内置了可编程低压检测可以在 VCC 掉到设定阈值前触发中断在中断里把关键数据通过 SPI 写进去。如果系统没有这个引脚至少要在主循环里周期检查 VCC 电压。3. 软件驱动PIC18F67K40 上把数据写进去再读出来3.1 MSSP 外设初始化的思路PIC18F67K40 的 MSSP 模块可以配置成 SPI 主机模式。初始化代码在 XC8 编译器下大致是下面这个逻辑void SPI_Init(void) { // 1. 引脚方向SCK/SDO/CS 设为输出SDI 设为输入 // 2. 关闭 MSSP 模块 // 3. 配置主模式、时钟极性空闲为低、采样点选择中间 // 4. 设置 SPI 时钟分频建议先使用 Fosc/4 或更慢 // 5. 重新使能 MSSP }我不在这里贴某个开发板的寄存器级代码因为不同批次 PIC18F67K40 的寄存器命名和 MSSP 库函数略有差异直接搬容易出错。关键是搞清楚这几步在做什么然后对着数据手册把位填上。SPI 模式要特别注意时钟极性和相位。MR25H40CDF 支持 SPI 模式 0 和模式 3我习惯用模式 0也就是空闲时 SCK 为低、第一个边沿采样。如果初始化错了读出来的数据会莫名奇妙地全 0xFF 或者错位而且不容易一眼看出来。3.2 MR25H40 的基本指令与读写时序MR25H40 的指令集和普通 SPI NOR Flash 很像这里只列本次用到的几条指令命令字节功能WREN0x06写使能每次写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据可连续读WRITE0x02写数据SLEEP0xB9进入深度休眠WAKE0xAB退出深度休眠写数据时最标准的流程是CS 拉低发 WRENCS 拉高然后再次 CS 拉低发 WRITE 命令再发 24 位地址最后发数据字节写完 CS 拉高。很多人第一次写 MRAM 都会漏掉第一步的 WREN结果数据怎么都写不进去这是最常见的低级错误。读数据要简单得多不需要 WREN直接 CS 拉低发 READ 命令发地址然后连续读即可。READ 命令支持按任意长度连续读读超过地址范围会回卷到起点。3.3 写使能状态与写保护的处理MR25H40 内部有一个状态寄存器其中 WEL 位表示写使能锁存状态。发完 WREN 之后WEL 会置 1然后执行一次 WRITE 或者 WRDI 之后WEL 自动清零。这意味着每次写操作之前都必须重新发一次 WREN。我写驱动时加了一个简单函数来检查 WELuint8_t MRAM_ReadSR(void) { uint8_t sr; CS_LOW(); SPI_ExchangeByte(0x05); // RDSR sr SPI_ExchangeByte(0x00); CS_HIGH(); return sr; }如果状态寄存器读回来 WEL 一直是 0首先检查 WP 引脚是不是被拉低了再检查 WRSR 有没有把块保护设置打开。工业现场干扰严重MOSI 线上的毛刺可能影响写入数据内容所以每次写完数据后我都习惯读回来比对而不是盲信写成功。3.4 一个可以直接用的读写驱动代码下面的代码是我在项目里的实际逻辑去掉了工程依赖保留核心流程#define CS_LOW() LATCbits.LATC2 0 #define CS_HIGH() LATCbits.LATC2 1 void MRAM_WriteEnable(void) { CS_LOW(); SPI_ExchangeByte(0x06); // WREN CS_HIGH(); } void MRAM_WriteByte(uint32_t addr, uint8_t dat) { MRAM_WriteEnable(); CS_LOW(); SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); SPI_ExchangeByte(dat); CS_HIGH(); MRAM_WaitWIP(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t d; CS_LOW(); SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); d SPI_ExchangeByte(0x00); CS_HIGH(); return d; } void MRAM_WaitWIP(void) { uint8_t sr; do { CS_LOW(); SPI_ExchangeByte(0x05); sr SPI_ExchangeByte(0x00); CS_HIGH(); } while (sr 0x01); // bit0 为写进行中标志 }这里有一个硬件细节要提MR25H40 的 WRITE 指令支持连续写入但单条写指令能连续写的字节数不是无限大超过 8 字节边界后地址会回卷。所以如果你想在一个连续区域写入超过 8 字节的数据不能图省事发一条长 WRITE而是要按 8 字节边界拆成多帧。READ 没有这个限制你可以一口气读任意长度。3.5 操作 MRAM 的缓冲区设计建议实际项目里很少用上面这种单字节读写的 API 去存业务数据效率太低。我习惯在驱动层之上封装一套带缓冲区的小函数void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len); void MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint16_t len);WriteBuffer 内部自动按每帧最多 8 字节拆分然后依次调 MRAM_WriteEnable 和 MRAM_WriteByte。加这一层之后上层做参数存储的时候就不用操心跨页回卷问题了。如果 MCU 的 RAM 够用还可以在掉电检测中断里用一个大数组把待写日志缓存起来然后连续写进 MRAM。PIC18F67K40 的 RAM 有限不能太奢侈但几个扇区的日志缓冲是完全够用的。4. 数据组织如何设计一个掉电不乱的存储结构4.1 分区规划参数区和日志区各管各的芯片选好了驱动也通了只能算“能读写”。真正决定产品稳定性的是上层存储格式怎么设计。我这次把 512KB 空间分成两块低地址区存参数高地址区存日志。参数区我用双镜像也就是同一份参数存两份。启动时先读 A 区校验失败就自动切到 B 区如果 A、B 都校验失败就用出厂默认参数。双镜像增加了成本但 MRAM 容量大多占一份空间换来的是固件更新断电时的容错能力。日志区我采用环形覆盖方式最新记录写到最后时回卷到开头老记录被覆盖。这个设计对工艺设备的运行追溯很实用。参数区和日志区都通过一个分区表来管理区域地址范围用途参数镜像 A0x00000 - 0x0FFFF当前参数主区参数镜像 B0x10000 - 0x1FFFF当前参数备份区日志区起始块0x20000 - 0x200FF日志索引区日志数据区0x20100 - 0x7FFFF环形日志正文如果不去规划MRAM 虽然容量大但日志区和参数区互相干扰排查旧数据时定位不到具体位置所以分区这一步值得认真做。4.2 日志帧格式CRC16 比什么都重要日志区每一条记录我定义成下面的结构体typedef struct __attribute__((packed)) { uint16_t magic; // 固定 0xA55A用于识别帧起始 uint16_t seq; // 递增序号 uint16_t len; // payload 长度 uint16_t crc; // payload 的 CRC16 uint8_t payload[128]; // 业务数据 } LogRecord;magic 字段的作用是快速定位帧边界。工业设备工况复杂日志区可能出现半截记录特别是掉电瞬间。启动时从头扫描遇到 magic 不匹配就继续往后找直到发现完整有效帧。seq 字段用来判断写入到一半的残留帧。如果读到一条 seq 比上一条还小说明这是断电前的脏数据直接跳过并把它标记为可用空间。这听上去复杂其实就是写一层很简单的状态判断比依赖 MRAM 的“不掉电”特性更靠得住。CRC16 我选的是常见的 CCITT 多项式 0x1021。计算范围只针对 payload不含头部这样改头部字段时不用重新计算整包 CRC。写入时先算好 CRC再整体写进 MRAM读取后重新计算比对不一致就认为记录无效。4.3 参数区的更新与回滚机制参数区我用双备份加版本号的办法具体流程是这样的写参数前先读当前版本号。新参数写进 A 区版本号加 1。再把同样参数写进 B 区版本号相同。启动时比较 A 区和 B 区的版本号选择版本高的作为有效参数。如果写入 A 区后掉电B 区还是旧版本下一轮启动时版本号高的那区可能不完整所以还需要 CRC 校验配合只有版本高且 CRC 正确的区才被采用。这样即使掉电发生在任意一帧写一半的时刻也不会出现整个参数丢失的情况。4.4 一次完整的写入与读取调用流程我把上层调用设计得很简单int Param_Save(struct DeviceParam *p); int Param_Load(struct DeviceParam *p);Param_Save 内部做 BCD 编码、加版本号、加 CRC、按双镜像写入 MRAM。Param_Load 启动时调用自动完成版本选择、CRC 校验、回滚默认参数。这里有一个经验不要在设备的每个线程里都直接操作 MRAM。每次参数写入虽然很快但并发访问会乱掉。我单独设立一个存储任务其他模块通过队列把写请求发给它。PIC18F67K40 这种 8 位 MCU 没有操作系统用消息队列也足够。5. 现场排查实测中最容易踩的 5 个坑5.1 读回全是 0xFF 或 0x00读回全 FF绝大多数原因是 SPI 引脚初始化不对或者 CS 没有正确拉低。我曾遇到过一次困扰半天最后发现是 PIC18F67K40 的 MSSP 复用引脚没有切换成 SPI 功能默认是数字 IOCS 拉低了 MRAM 也没反应。还有一个隐蔽原因WP 或 HOLD 悬空。HOLD 一旦被噪声拉低MRAM 会忽略 SPI 时钟读出来就是未定义值。把这两个引脚用 10k 电阻上拉到 VCC问题马上消失。5.2 写不进去状态寄存器 WEL 一直为 0这个坑太经典了每次培训都会被问到。WRITE 指令前必须先发 WREN而且 CS 必须按“低-高-低”切换保证 WREN 指令完整结束。部分代码把 WREN 的 CS 拉高动作漏掉了MSSP 会认为两条指令是连续的状态寄存器里的 WEL 根本不会置位。再检查一遍 WP 引脚。如果 WP 被拉低MRAM 的写保护区域是受保护的即使 WREN 成功也不能写。我见过调试板上 WP 接到了地找半天找不到原因。5.3 SPI 速率上不去波形边沿太丑MRAM 本身支持很高 SPI 时钟但实际速率受限于 PCB 走线和 MCU 驱动能力。我最初把 SPI 时钟配到 Fosc/2结果读回来的数据偶尔错位用示波器一看SCK 波形上升沿有严重过冲。把分频降到 Fosc/4问题消失。如果必须跑高速建议从芯片出去到 MRAM 之间串 33Ω 电阻抑制信号反射。SCK 线尽量避开大电流电源线单片机的地和 MRAM 地之间不要有细长走线。5.4 掉电瞬间最后一条记录损坏MRAM 理论上不怕掉电但整个链路上还有 MCU 和 SPI 总线。掉电瞬间 VCC 跌落MCU 可能在半电压状态下乱发 SPI 数据导致 MRAM 写入一个错误帧。我的解决方法是软件和硬件配合硬件上用 LVD 中断提前捕获掉电软件上在日志头部加 magic 和 CRC启动时遇到无效记录就停止回卷不再继续往后写。这样即使最后一条脏了也只是丢掉一条不影响前面所有数据。5.5 上电自检时发现整个芯片数据全变样遇到一次很诡异的问题设备运行一段时间后参数区大量数据错乱。后来定位到是 GPIO 初始化顺序的问题。PIC18F67K40 上电后如果 MRAM 的 CS 引脚在初始化前处于低电平而 SCK、SI 上同时出现过毛刺MRAM 会误判为一次写指令把内部数据改成随机值。解决方式很简单MCU 上电后第一时间把所有 MRAM 相关引脚配置为确定的数字输出CS 默认拉高再初始化 SPI 模块。所有 SPI 外设Flash、EEPROM、MRAM 都一样CS 在初始化前必须保持高电平这条规矩我吃了不少教训才记住。6. 我在实际项目里的一些体会这套方案跑下来之后我的体会还是那句老话存储芯片选型解决大部分问题但存储格式设计才能解决剩下的问题。MR25H40CDF 把“写不进、寿命短、等待长”这些硬伤都抹平了PIC18F67K40 的资源也刚好够用真正要小心的反而是分层、校验、掉电时序这些看起来不起眼的地方。最后再分享一个小技巧量产自检时我在 MRAM 的高地址区固定存了一串测试数据上电后读出来和预设模式比对。这个步骤成本几乎为零但能提前暴露虚焊、贴片偏移、IO 连接不良。我在产线上靠这个办法抓到了三块坏板子比麻烦客户回来返修强太多了。
返回列表