多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MRAM替代EEPROM与Flash的工业存储方案,基于PIC单片机SPI驱动实现

MRAM替代EEPROM与Flash的工业存储方案,基于PIC单片机SPI驱动实现 搞嵌入式这么多年凡是涉及“参数保存”“掉电存储”“运行日志”的项目我第一反应都是外挂一颗 Flash 或者 EEPROM。但最近做一套工业变送器的数据记录模块我把方案彻底换成了 MRAMEverspin 的 MR25H40CDF4Mbit 串行接口搭配 Microchip 的 PIC18F46K42 做主机。这套组合解决了一个特别熬人的问题——设备每分钟都要写一次运行状态产品设计寿命十年以上普通 EEPROM 的擦写寿命根本扛不住而 MRAM 几乎“写不死”又能像普通 SPI 存储芯片一样简单操作。接下来我会把硬件接线、MCC 配置、SPI 驱动代码、可靠写策略以及调试中踩过的坑全部整理出来希望对做工业仪表、车载设备或者需要频繁掉电保存的嵌入式项目有点参考价值。1. 为什么工业数据存储场景里我放弃了 Flash 改用 MRAM1.1 NOR Flash 的“先擦后写”和寿命上限很多工程师习惯性把 SPI NOR Flash 当成大容量存储的首选确实大容量、低成本、生态成熟但一旦遇到“频繁小数据更新”Flash 的弊端就暴露了。NOR Flash 在写入之前必须先擦除而且擦除以扇区为单位哪怕你只想改 1 个字节也得先把整个扇区搬到 SRAM、擦掉、再整体写回去。这个机制带来的问题不只是速度慢更致命的是擦除次数限制一般 SPI NOR Flash 的擦写寿命在 10 万次左右工业设备每天写几十次几年就到寿命边界了。就算你做磨损均衡日志型应用也会把寿命问题变得非常复杂。EEPROM 看起来好一些支持字节级覆写不用先擦除但容量普遍偏小寿命大多只有 100 万次。以本文这个项目为例每分钟写一次运行数据一天 1440 次一年约 52 万次两年就把普通 EEPROM 的寿命耗尽了。选型时我算了一笔账如果继续用 EEPROM要么缩小记录频率要么增加多份轮换存储都会增加固件复杂性还未必可靠。1.2 MR25H40CDF 的核心卖点MR25H40CDF 是 Everspin 的串行 MRAM4Mbit 容量也就是 512KB工作在 2.7V 到 3.6VSPI 接口支持 Mode 0 和 Mode 3。它最大的特点是存储单元基于磁阻效应而不是电荷。既然不是电荷就不存在“电荷泄漏”和“擦除老化”的问题掉电后数据靠磁性状态保存写入时直接覆盖旧值既不需要扇区擦除也没有写前读-改-写那一套。从指标上看MRAM 的写寿命通常在 10 的 14 次方以上实际工程中可以当“无限寿命”来用数据保持时间在工业温度范围内也能达到 20 年以上。更关键的体验是写入速度同样通过 SPI 发送命令和数据Flash 写完往往还要等内部擦写时间MRAM 基本是命令发完数据就持久化了对我这种要频繁记录现场数据的场景来说固件逻辑会简单很多。存储类型写入方式典型寿命是否需要擦除典型容量串行 EEPROM字节覆盖约 100 万次否几 KB 到几 MBSPI NOR Flash扇区擦除后写约 10 万次是几 MB 到几十 MBMR25H40CDF MRAM字节覆盖大于 10 的 14 次方否512KB1.3 带电池 SRAM 也不是最优解可能有人会提出来用 SRAM 加电池不是也能做到掉电保存吗确实可以典型做法是外挂一颗低功耗 SRAM再用电池和电源检测电路维持数据。但工业现场对免维护要求很高电池会漏电、会耗尽、高低温下性能还会恶化。机构提出“十年内不更换任何部件”时带电池方案第一轮就被我淘汰了。MRAM 本质上保留了 SRAM 那种直接覆写的快捷体验又完全没有掉电丢失的担忧属于那种“缺点明显但槽点极少”的存储介质用在工业参数的保存上非常合适。2. 方案定型和硬件连接2.1 为什么选用 PIC18F46K42这颗单片机是 Microchip 的 8 位 PIC 家族里外设比较完整的一员主频最高 64MHz内置 64KB 程序 Flash、4KB 数据 SRAM还有 ADCC、CLC、DMA、硬件 CRC 等模块。对于工业变送器这种项目它的 ADCC 可以带计算功能直接处理传感器信号CLC 可以做外围逻辑我这次主用它的 SPI 主机功能工作电压直接选 3.3V和 MRAM 匹配省去电平转换电路。更重要的是开发效率。MPLAB X 里集成了 MCC也就是 MPLAB Code Configurator图形化勾选 SPI 模块、引脚分配、时钟分频代码自动生成。PIC18F46K42 的数字引脚支持 PPS 重映射SCK、SDO、SDI 可以分配到任意支持的引脚上布线自由度很高。这在以前的老 PIC 上是不敢想的老平台很多外设引脚固定画板子时常常被逼得绕大圈。2.2 引脚分配和最小电路我在这个项目里用的是 SOP-8 封装的 MR25H40CDF引脚功能很明确引脚1 CS#片选输入低电平有效引脚2 SO串行数据输出接单片机的 SDI引脚3 WP#写保护输入必须接高电平或由 GPIO 控制引脚4 VSS地引脚5 SI串行数据输入接单片机的 SDO引脚6 SCK时钟输入引脚7 HOLD#暂停通信输入必须接高电平引脚8 VDD3.3V 电源我的连接规划如下PIC18F46K42 引脚方向MR25H40CDF 引脚说明RB0 (GPIO)输出CS#片选控制RB2 (SCK)输出SCKSPI 时钟RB3 (SDO)输出SI主机发送数据RB4 (SDI)输入SO主机接收数据VDD 3.3V-VDD电源GND-VSS地WP# 和 HOLD# 不能悬空。我在实际板上把这两个引脚分别通过 10K 电阻上拉到 VDD。如果悬空芯片内部虽然可能有弱上拉但工业环境里的干扰信号很容易在引脚上造成毛刺导致数据误写或者通信暂停。另外每个电源引脚旁边都放了 0.1uF 陶瓷电容并且在 MRAM 附近加了一个 10uF 的钽电容用于吸收 SPI 写入时的瞬态电流。还有一个非常容易忽略的细节CS# 也要加 10K 上拉到 VDD。原因是单片机在上电复位和编程器连接期间IO 引脚可能处于高阻态如果 CS# 悬空引脚电平就完全取决于环境噪声。一旦噪声把 CS# 拉低MRAM 会误以为主机要通信然后在 SCK 上的毛刺驱动下执行随机操作。虽然 MR25H40CDF 有写使能锁存保护但外部总线毛刺依然可能导致误读或异常功耗做工业产品必须把这种隐患在原理图上就解决掉。2.3 电源和电平匹配MR25H40CDF 是纯 3.3V 器件不能直接接 5V 输入。PIC18F46K42 支持 2.3V 到 5.5V 供电所以最简单可靠的做法是整个系统统一跑 3.3V。如果板子上不得不有 5V 单片机那就必须加电平转换芯片比如 74LVC2T45 或类似器件千万不能靠电阻分压偷懒SPI 时钟频率稍高一点分压网络的上升沿衰减就会让你怀疑人生。我这边同时用到了 3.3V 和 5V 的传感电路所以是分区域供电MCU 和 MRAM 稳定在同一路 3.3V 域内电源入口再统一做过压和反接保护。3. 用 MCC 快速初始化 SPI3.1 建立工程和添加 SPI 外设在 MPLAB X 中新建项目选择 PIC18F46K42 型号打开 MCC 插件左侧 Device Resources 里找到 SPI 模块添加后给它命名为 SPI1。配置页面需要设几个关键参数ModeHost主机模式SPI Mode0即 CPOL0、CPHA0时钟分频先选低一点的速率比如 1MHz数据位8 位帧类型标准MR25H40CDF 在数据手册里明确写了支持 SPI Mode 0 和 Mode 3两种模式都能用但必须保证主机和从机一致。我习惯选 Mode 0因为大部分 MCU 默认上电状态就是时钟空闲低电平少一个容易踩错的参数。初次调试时钟频率建议从 1MHz 起步。数据手册上 MRAM 允许跑几十 MHz但那是在理想 PCB 布线和短走线条件下。工业板卡上信号要过连接器、排线再加上 EMI 防护器件时序余量会被压缩所以我一般先在低速下把功能跑通再逐步提频最后定格在可靠值上。3.2 通过 PPS 重映射分配引脚PIC18F46K42 的 PPS 配置在 MCC 里非常直观。在 Pin Manager 界面里找到输出引脚 SCK 和 SDO把它们分别映射到 RB2 和 RB3再把输入引脚 SDI 映射到 RB4。MCC 会在生成的初始化代码里自动写入 PPS 锁定寄存器和输入选择寄存器不需要手写但你要核对生成的代码是否符合预期。这里有个经验PPS 寄存器属于“受保护”寄存器修改前需要解锁序列。MCC 生成的初始化代码会自动执行 PIN_MANAGER_Initialize()如果你手动改动过引脚映射一定要确认初始化被调用且没有被优化器跳过去。我在一个旧项目里就吃过亏手动加了引脚配置代码但忘了执行解锁结果引脚映射始终是默认值SPI 怎么调都不通最后查了半天才发现是 PPS 没锁住。3.3 核对初始化代码MCC 生成的核心代码大致长这样void SPI1_Initialize(void) { SPI1CON0 0x10; // 主机模式SPI 使能 SPI1CON1 0x00; // 模式 08 位数据 SPI1BAUD 0x01; // 分频设置 }实际寄存器位定义可能因 MCC 版本略有差异但概念一致。生成代码后我会做三件事第一确认 CS 引脚没有被 SPI 模块占用而是普通 GPIO第二确认时钟分频计算出来的实际频率符合预期第三把生成的 SPI1_Open、SPI1_Exchange8bit、SPI1_Close 接口抄到自己的驱动文件里而不是直接改动生成文件。MCC 重新生成时会覆盖用户代码所以我的习惯是单独建一个 mram_drv.c 文件把对 MRAM 的操作全部封装起来只调用 MCC 生成的底层 SPI 函数。4. 封装 MR25H40CDF 读写驱动4.1 指令速查MR25H40CDF 的指令集和普通 SPI NOR Flash 相近最常用的是这几个指令名操作码功能WREN0x06写使能置位状态寄存器 WELWRDI0x04写禁止清除 WELRDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据普通读命令WRITE0x02写数据普通写命令READ 和 WRITE 都是三字节地址寻址因为 512KB 需要 19 位地址典型发送格式是“命令字节 地址高位 地址中位 地址低位”。需要注意的是MRAM 不像 Flash 那样有页缓冲编程的概念WRITE 命令可以在片选保持低电平期间连续写任意多个字节直到地址达到 0x7FFFF 后回卷到 0x00000。写入之前必须执行 WREN否则 WRITE 和 WRSR 都会被忽略。写使能锁存器在成功执行一次写入之后会被自动清零所以这并不是“配置一次就永久有效”而是每条写入指令前都要重新发 WREN。这一点对经验不足的工程师来说特别容易漏第一次给 Flash 写数据时习惯性先擦除然后写命令直接跟着地址发结果发现数据没写进去状态寄存器里 WEL 还是 0基本就是忘了 WREN。4.2 三个基础函数下面是我实际使用的驱动骨架底层收发用的是 MCC 生成的 SPI1_Exchange8bit。先看片选和写使能#define MRAM_CS_LAT LATBbits.LATB0 #define MRAM_CS_TRIS TRISBbits.TRISB0 static void mram_cs_low(void) { MRAM_CS_LAT 0; } static void mram_cs_high(void) { MRAM_CS_LAT 1; } void mram_wren(void) { mram_cs_low(); SPI1_Exchange8bit(0x06); // WREN mram_cs_high(); }读状态寄存器的函数如下。RDSR 命令也叫“只读状态”发送命令字节后主机继续产生时钟从机就会把状态寄存器的内容一位一位送出来。因为状态寄存器只要读一个字节我就在读完之后立刻拉高 CS结束本次命令。uint8_t mram_rdsr(void) { uint8_t status; mram_cs_low(); SPI1_Exchange8bit(0x05); // RDSR status SPI1_Exchange8bit(0x00); mram_cs_high(); return status; }单字节写入的实现void mram_write_byte(uint32_t addr, uint8_t data) { mram_wren(); // 1. 写使能 while ((mram_rdsr() 0x01) 0) {} // 2. 等待 WEL 置位 mram_cs_low(); // 3. 发送 WRITE SPI1_Exchange8bit(0x02); SPI1_Exchange8bit((addr 16) 0xFF); SPI1_Exchange8bit((addr 8) 0xFF); SPI1_Exchange8bit(addr 0xFF); SPI1_Exchange8bit(data); mram_cs_high(); }在等待 WEL 置位这个循环里理论上只会执行一次因为 MRAM 不像 Flash 需要内部擦写时间写使能基本立刻生效。但如果 SPI 总线上有干扰或者刚才发过 WRDI多轮询一次并不会造成显著开销反而能提前发现异常。单字节读取的实现就更短了uint8_t mram_read_byte(uint32_t addr) { uint8_t data; mram_cs_low(); SPI1_Exchange8bit(0x03); // READ SPI1_Exchange8bit((addr 16) 0xFF); SPI1_Exchange8bit((addr 8) 0xFF); SPI1_Exchange8bit(addr 0xFF); data SPI1_Exchange8bit(0x00); // 读一个字节 mram_cs_high(); return data; }4.3 连续读写和中断保护单字节操作适合参数修改如果是批量日志写入最好还是用连续写。做法很简单WREN 之后拉低 CS发送写命令和起始地址然后连续发送数据字节全部发完再拉高 CS。这样能减少 CS 翻转次数也减少总线上的无效开销。MRAM 不需要页对齐只要不超过 0x7FFFF 的地址上界中间可以一直写下去。void mram_write_burst(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t i; mram_wren(); mram_cs_low(); SPI1_Exchange8bit(0x02); SPI1_Exchange8bit((addr 16) 0xFF); SPI1_Exchange8bit((addr 8) 0xFF); SPI1_Exchange8bit(addr 0xFF); for (i 0; i len; i) { SPI1_Exchange8bit(buf[i]); } mram_cs_high(); }这里要特别提醒中断问题整段连续写过程中CS 必须保持低电平数据传输格式不能被打断。如果在发送过程中进入中断服务程序而中断里又恰好操作了 SPI1 或者 GPIOCS 时序就可能被拉长或者产生额外时钟后果是写入的数据错位。我的处理策略是如果 MRAM 写入函数执行期间本来就会开中断就在写函数外围临时关闭中断等 CS 拉高后再恢复。PIC18F46K42 可以简单用INTCONbits.GIE 0; mram_write_burst(addr, buf, len); INTCONbits.GIE 1;这是老派但非常可靠的做法。你要是有更讲究的调度器当然可以用互斥信号量或临界区保护原理一样。4.4 快速验证驱动正确性写完驱动不要急着往正式应用里接先在 main 函数里跑一个简单的自检向几个特定地址写入一个已知测试向量然后读回来比对。我习惯用 55 AA FF 00 这种交替位模式因为它能同时暴露数据线粘连和时钟相位问题。再向某段地址连续写入递增数掉电后重新上电再读一次确认持久性正常。这一步能在十分钟内排除绝大多数接线和驱动层低级错误。5. 工业现场如何让数据真正可靠5.1 先处理掉电与复位时序MRAM 本身不丢数据但外部系统如果在上电或掉电瞬间对 MRAM 产生了不可靠访问写入可能没有正确完成。这里有两个关键手段第一CS# 引脚加上拉到 VDD并在单片机复位期间把 CS 引脚保持为输出高电平。我在初始化代码的开头就设置 CS 为输出并输出高电平再去配置 SPI。如果 CS 在复位期间是默认输入就可能出现低电平毛刺。第二应用层访问 MRAM 之前至少延时几毫秒确保 VDD 已经稳定。对于 3.3V 系统一般 10ms 足够具体以电源芯片的上电时间和复位延时为准。还要避免一个非常隐蔽的状况写入命令进行到一半时 MCU 被复位。比如看门狗超时、外接复位芯片触发、调试中断。这会让 CS 在数据未写完时提前释放写入可能被中止。MR25H40CDF 的单字节写入本身很快依然建议在正式固件里给 MRAM 操作加“事务标志”进入写入事务时置一个全局变量完成后清除。根据这个标志决定是否处理复位看门狗或者干脆在 MCU 硬件复位期间增加外部硬件延时等当前命令结束后再拉高 CS。对要求极高的项目可以用单片机的铁电库思路把关键写操作放到临界区并关闭看门狗实际操作中大部分团队都接受这种折中。5.2 双缓存和校验机制有人觉得 MRAM 寿命长、字节级覆盖就可以像操作 SRAM 一样随便用。从可靠性角度讲介质可靠不等于系统可靠。工业参数更新过程中如果程序跑飞、总线受干扰、或者数据帧本身就是错的写进去的就是坏数据。所以我在应用层做了双重保护每个参数块内部带版本号、长度和 CRC32。同一个参数在 MRAM 中保存两份分别放在不同地址区域。更新流程是先用新版本覆盖备份区校验备份区成功后再更新主区。启动读取时先读主区校验通过且版本号不落后就正常使用如果主区校验失败再用备份区恢复。这个策略很像 Flash 存储里的“双 Bank 冗余”成本只是多占一倍 MRAM 空间。对 512KB 的 MR25H40CDF 来说实际参数区通常只占几十 KB完全承担得起。写参数块时我还会把 CRC 和版本号放在参数块末尾而不是开头。原因在于典型写操作是连续写入如果在写入中途发生总线异常区块前半部分可能是新数据、后半部分是旧数据只有等全部写完再校验才能避免误把半成品当完整参数。数据块结构大致是这样typedef struct { uint16_t version; uint8_t payload[64]; uint32_t crc32; } ParamBlock;读写时都按这个结构整体处理。更新参数时先组织好整个结构再一次性 burst 写进目标地址。启动阶段只做读取和校验不会反复擦写MRAM 的容量和寿命优势就体现出来了。5.3 布线和 EMI 层面的建议工业现场往往有电机、变频器、继电器干扰源比实验室多得多。SPI 信号是数字脉冲沿很陡一旦经过长走线就可能反射和串扰。我实测下来的经验是SPI 走线越短越好最好不超过 5cm。MRAM 的地和 MCU 的地址在同一块完整地平面上不要跨区。SCK 和 SDO 可以各串一个 22 到 33 欧姆的电阻放在源端有抑制过冲的作用。如果信号必须过排线或连接器优先考虑把 SPI 速率降到 1MHz 以下同时使用屏蔽地线包裹。WP# 和 HOLD# 的上拉电阻不要省这是两处最容易受干扰的浮动引脚。现场设备出现偶发数据异常时别急着一口咬定是 MRAM 坏了先看波形、再查软件流程这是我反复强调的排查顺序。6. 调试和故障排查实录6.1 读出来全是 0xFF第一次上板SPI 读回数据全是 0xFF这是最高频的问题。优先查 SPI 模式是不是 Mode 0也就是时钟空闲是否为低电平、数据在第一个时钟边沿采样。如果 CPOL 或者 CPHA 反了从机根本收不到正确的命令和地址返回的就会是高电平默认值。我遇到过更隐蔽的情况MCC 生成的 SPI 引脚映射和实际接线不一致。PIC18F46K42 支持引脚重映射很多人画板子时觉得“反正 PPS 能分配”结果 MCC 里没把 SCK 分配到你实际走线的引脚上。这种问题查法很简单把 CS 配置成输出低电平然后手动触发 SPI 发送用示波器看对应引脚有没有时钟信号。没时钟先检查 PPS 配置和逻辑分析仪的通道接法。6.2 写进去的数据掉电后恢复旧值这个现象很迷惑现场写数据读回来是对的一断电再上电数据又变回旧版本。经历过几次之后我总结了三个常见原因第一CS# 在复位和上电期间出现了低电平毛刺MRAM 把上面残留的旧地址和数据又重新写了一遍。处理办法就是加外部上拉电阻并在初始化时第一时间把 CS 置高。第二WP# 没有正确拉高外部干扰导致 WP# 短暂拉低写保护激活WRITE 指令实际被芯片拒绝但主机依然按正常流程执行完毕并返回成功。由于读操作不受写保护影响你当时读到的可能是旧数据而你以为写入成功了必须通过 RDSR 查看 WEL 和 BP 位确认。第三状态寄存器里的 BP 位被误设置为保护区域。MR25H40CDF 的状态寄存器有块保护功能如果 BP0、BP1 被误写对应地址区间就被保护起来。很多工程师只关注 WEL忽略 BP 位结果数据写在受保护的区域里怎么改都改不动。排查方式是在系统上电后读一次 RDSR把状态寄存器的值打印到串口正常情况应该是 0x00 或者 WEL 暂时置位。6.3 个别板卡写失败降低 SPI 时钟后正常有一批板子在产线上做高低温循环测试时偶尔出现参数写入失败良率九成左右。查到最后问题出在 SPI 时钟余量不足。PCB 上 MRAM 的封装引脚到 MCU 引脚之间的距离比预期长且中间还过了两个过孔信号完整性变差。加上温度升高后芯片 IO 的翻转阈值变化时序边缘就更紧张。我不打算散布“所以工业产品 SPI 一定要低速率”的教条但有一条原则是铁律功能验证用高速定型生产留余量。最终把 SPI 频率从 8MHz 降到 2MHz再配合源端串阻整批板子随便测都没再出现写入失败。MRAM 的写操作本来就不需要像 Flash 那样等内部状态机频率降一点不影响用户感知STM32、PIC 这类现代单片机处理参数保存任务也完全绰绰有余。6.4 排错工具没有逻辑分析仪就别谈 SPI 调试。我之前用示波器也能看但抓命令字节和 CS 时序实在痛苦后来特意买了一个带 SPI 协议解析的逻辑分析仪抓一段波形直接解码出命令名和地址效率提升非常明显。调试 MRAM 驱动时我会重点抓几个信号CS 的低电平区间、SCK 的连续包、SI 上的命令字节、SO 上读操作返回的数据。如果 CS 拉低后 SCK 没有按预期产生时钟基本可以断定是 MCU 的 SPI 模块没发出数据如果命令字节和地址都对但数据仍不对再看时序参数和模式配置。7. 我个人实测的一些体会这套方案如果现在让我重新选一次我依然会坚持用 MRAM 做参数和日志存储而且会更早地验证状态寄存器和写保护引脚细节。MR25H40CDF 在项目中表现出来的最大优势不是速度而是“写就真的写了”这种确定性。做工业控制器的人最怕的就是“读回来好像对重启后却是另外一回事”MRAM 至少把介质层的悬念去掉了剩下的可靠性问题完全可以通过软件冗余和硬件规矩来解决。最后分享一个工作习惯每次板卡打样回来我会优先把 MRAM 初始化自检写成一个独立的测试函数跑一遍全地址区的写读回来校验并记录每个地址的读写耗时。这个自检虽然会增加一点生产测试时间但能在出厂前把绝大多数虚焊、引脚错位和 SPI 连线异常过滤掉。等自检稳定跑过了再把驱动接进正式业务逻辑后面的坑就会少很多。提示如果你是第一次用 MRAM建议先跳过掉电保持测试直接把读写例程跑通等确认 SPI 波形和状态寄存器都正常了再去模拟掉电、复位、高低温这些现场条件这样定位问题范围会小很多。
返回列表