ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MRAM在嵌入式工业数据采集中的掉电保护与参数存储实战

MRAM在嵌入式工业数据采集中的掉电保护与参数存储实战 MRAM 这类存储介质在嵌入式圈子里一直有点叫好不叫座的味道——参数漂亮价格劝退很多人听过但没真正上手过。我最近在一个工业数据采集项目里用 MR25H40CDF 搭配 PIC18LF4553 做了一套参数存储与掉电保护方案从选型、硬件设计到 SPI 驱动调试完整走了一遍。这篇文章就把整个过程拆开讲清楚为什么选 MRAM 而不是 EEPROM 或 Flash、PIC18LF4553 的 MSSP 模块怎么配置、读写时序里有哪些容易翻车的细节、以及工业现场实测下来哪些参数必须留余量。如果你正在做嵌入式数据存储、参数保存、掉电记录这类需求或者单纯想搞清楚 MRAM 到底值不值得用这篇应该能帮你省下不少试错时间。1. 为什么这个场景值得用 MRAM 而不是 EEPROM1.1 工业数据存储的真实痛点先说说我遇到的具体需求。这个项目是一台工业现场的采集终端需要周期性记录设备运行参数——包括累计运行时长、故障代码、校准系数、最近若干条报警记录。数据量不大单条记录几十字节总量也就几 KB。但有几个硬性约束第一写入频繁某些计数器每秒都要更新第二掉电必须保住数据现场断电是常态第三设备寿命要求十年以上不能中途换存储芯片。一开始我按常规思路选了 SPI EEPROM比如 25LC 系列。用下来发现两个问题。一是写入速度慢EEPROM 的字节写入周期典型值 5ms页写入也要 3~5ms如果每秒写一次计数器光等待写完成就占用了大量 CPU 时间。二是寿命EEPROM 的擦写次数标称 100 万次听起来很多但按每秒写一次算一天就是 86400 次不到两周就耗尽了某个地址的寿命。虽然可以用磨损均衡算法分散写入但代码复杂度上去了而且工业现场最怕的就是算法本身出问题。后来考虑 SPI Flash比如 W25Q 系列。Flash 的问题是写入前必须擦除而且擦除粒度是扇区通常 4KB改一个字节要读整个扇区、改、擦、写回流程繁琐。更关键的是 Flash 的擦写寿命只有 10 万次左右比 EEPROM 还低一个数量级。对于高频写入场景Flash 基本不适合做参数存储它更适合存固件、日志这类写一次读多次的数据。1.2 MRAM 的核心优势像 RAM 一样写像 Flash 一样存MR25H40CDF 是 Everspin 出的 4Mbit512KBSPI MRAM。MRAM 的全称是磁性随机存储器它的存储单元是磁性隧道结MTJ靠磁化方向而不是电荷来存储数据。这个物理原理带来了几个对嵌入式非常友好的特性。第一写入不需要擦除也不需要等待。MRAM 的写操作就是直接改磁性状态没有 Flash 那种先擦后写的流程也没有 EEPROM 那种内部电荷泵充电的等待时间。MR25H40CDF 的写周期时间在数据手册里标的是 35ns 级别实际通过 SPI 接口受限于总线速率但至少不需要额外的写等待。第二擦写寿命极高。MRAM 的耐久性标称 10^14 次以上比 EEPROM 高 8 个数量级。这意味着你根本不用考虑磨损均衡想写哪个地址就写哪个地址每秒写一万次都无所谓。第三掉电即存。MRAM 写入完成的数据立刻就是非易失的不需要像 EEPROM 那样等内部写周期结束。这对掉电保护场景太重要了——检测到掉电信号立刻把关键数据写进去写完就安全不用等。第四SPI 接口简单。MR25H40CDF 用的是标准 SPI 模式 0 和模式 3最高时钟 40MHz和普通 SPI Flash 的接线方式几乎一样硬件改动成本低。当然MRAM 的缺点也很明显贵。同样容量的 MRAM 价格可能是 EEPROM 的十几倍甚至几十倍。所以选型逻辑很清晰——数据量小、写入频繁、掉电保护要求高、寿命要求长的场景MRAM 是正解如果是存固件、存大块日志还是老老实实用 Flash。1.3 MR25H40CDF 关键参数速查在动手之前先把这颗芯片的关键参数理清楚后面配置的时候会反复用到。参数项数值说明容量4Mbit / 512KB组织为 512K x 8接口SPI支持 Mode 0 和 Mode 3最高时钟40MHz读操作写操作同样支持 40MHz供电电压2.7V ~ 3.6V典型 3.3V写耐久性10^14 次无需磨损均衡数据保持20 年以上工业级温度范围工作温度-40°C ~ 85°C工业级封装8-SOIC / 8-DFN常见 SOIC-8指令集标准 SPI 指令READ/WRITE/WREN/RDSR 等这里有个细节要注意MR25H40CDF 的写操作不需要 WREN 指令吗实际上是需要先发 WRENWrite Enable的和 EEPROM 类似。但它的状态寄存器里没有 WIPWrite In Progress位需要轮询因为写是即时的。这一点和 EEPROM 的驱动逻辑有区别后面写代码时会专门讲。2. PIC18LF4553 的 MSSP 模块配置与硬件连接2.1 为什么选 PIC18LF4553PIC18LF4553 是 Microchip 的一款 8 位单片机44 引脚带 USB 控制器Flash 程序存储器 24KBRAM 2KBEEPROM 256 字节。它的LF前缀表示低压版本工作电压 2.0V~5.5V这点很关键——因为 MR25H40CDF 是 3.3V 器件如果主控是 5V 系统中间需要电平转换。PIC18LF4553 可以直接跑在 3.3V和 MRAM 电平匹配省掉转换电路。它内置的 MSSPMaster Synchronous Serial Port模块支持 SPI 和 I2CSPI 模式下可以配置为主机时钟极性、相位、速率都可调。对于驱动 MR25H40CDF 来说MSSP 完全够用。选这颗片子的另一个原因是它在这个项目里还要兼做 USB 通信和若干路 ADC 采集资源刚好够不用额外加芯片。如果你的项目主控是 STM32 或者其他带 SPI 的 MCU驱动逻辑是相通的只是寄存器配置不同。2.2 硬件连接与 PCB 注意事项MR25H40CDF 和 PIC18LF4553 的连接很直接四根线SCK、SIMOSI、SOMISO、CS。加上电源和地一共六根。PIC18LF4553 引脚MR25H40CDF 引脚功能RC3 / SCKSCKSPI 时钟RC5 / SDOSI主机输出从机输入RC4 / SDISO主机输入从机输出RA5任意 GPIOCS片选低有效3.3VVCC电源GNDGND地这里有几个硬件上的坑要提前说。第一CS 引脚必须用 GPIO 控制不能用 MSSP 的硬件 SS 功能。原因很简单MRAM 的指令序列要求 CS 在整条指令期间保持低电平中间不能拉高。如果用硬件 SS某些情况下 SPI 模块会在字节间自动操作 SS导致指令被打断。用普通 GPIO 手动控制 CS时序完全可控。第二SCK 走线要短最好加串阻。40MHz 的时钟在 PCB 上已经不是低速信号了如果走线长、有分支容易产生振铃和反射导致读写错误。我的做法是在 SCK 靠近 MRAM 一端串一个 22Ω 电阻实测波形干净很多。如果你的板子只能跑 10MHz 以下这个问题不突出但工业环境里电磁干扰大还是建议加上。第三电源去耦不能省。MR25H40CDF 的 VCC 引脚旁边必须放一个 0.1μF 的陶瓷电容越近越好。MRAM 在写操作瞬间会有电流波动去耦不好可能导致写入失败。我在第一版板子上因为电容放远了 5mm偶发写入错误后来挪到引脚旁边就再没出现过。第四WP写保护引脚。MR25H40CDF 有一个 WP 引脚低电平时硬件写保护。如果你不需要硬件写保护直接接 VCC。如果要保留这个功能接一个 GPIO 或者上拉电阻。我建议至少留一个上拉电阻的位置调试阶段可以手动控制量产时再决定是否启用。2.3 MSSP 初始化代码与参数计算PIC18LF4553 的 MSSP 配置涉及几个寄存器SSPCON1、SSPCON2、SSPSTAT、SSPADD。SPI 主机模式下时钟速率由 SSPADD 决定公式是Fosc / (4 * (SSPADD 1))假设系统时钟 Fosc 是 32MHz外部 8MHz 晶振经 PLL 4 倍频想要 SPI 时钟 8MHz那么SSPADD 1 32MHz / (4 * 8MHz) 1SSPADD 0也就是说 SSPADD 设为 0 时SPI 时钟是 Fosc/4 8MHz。如果要更低比如 4MHzSSPADD 1。注意 SSPADD 最小值为 0对应最高时钟 Fosc/4。下面是初始化代码用 C 语言写基于 Microchip XC8 编译器#include xc.h // 定义 CS 引脚 #define MRAM_CS LATAbits.LATA5 #define MRAM_CS_DIR TRISAbits.TRISA5 void SPI_Init(void) { // 配置 CS 为输出默认拉高 MRAM_CS_DIR 0; MRAM_CS 1; // 配置 SCK、SDO 为输出SDI 为输入 TRISCbits.TRISC3 0; // SCK TRISCbits.TRISC5 0; // SDO TRISCbits.TRISC4 1; // SDI // MSSP 配置SPI 主机模式时钟 Fosc/4 SSPCON1 0x00; // 先清零 SSPSTAT 0x00; // 清零 SSPCON1bits.SSPM 0b0000; // SPI Master, clock Fosc/4 SSPCON1bits.CKP 1; // 空闲时 SCK 高电平Mode 3 SSPSTATbits.CKE 1; // 数据在 SCK 上升沿采样Mode 3 SSPCON1bits.SSPEN 1; // 使能 MSSP // 清空接收缓冲 volatile unsigned char dummy SSPBUF; (void)dummy; }这里选的是 SPI Mode 3CPOL1CPHA1也就是空闲时 SCK 高电平数据在第二个边沿采样。MR25H40CDF 支持 Mode 0 和 Mode 3我选 Mode 3 是因为在工业环境里空闲高电平的抗干扰性略好一些而且和很多 SPI 从器件的默认状态一致。如果你用 Mode 0把 CKP 和 CKE 反过来配置即可。2.4 字节收发函数的实现细节SPI 的字节收发是驱动的基础必须写得稳。PIC18LF4553 的 MSSP 收发流程是写 SSPBUF 启动发送同时接收数据进入 SSPBUF通过 SSPIF 标志位判断完成。unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF data; // 写入发送数据 while (!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF 0; // 清除标志 return SSPBUF; // 返回接收数据 }看起来简单但有几个细节容易出问题。第一SSPIF 标志必须在写 SSPBUF 之前确保是清零的。如果上一次传输的标志没清这次会直接跳过等待读到错误数据。我在初始化里加了一句清空 SSPBUF 的操作就是为了处理上电时可能残留的状态。第二读 SSPBUF 的时机。SSPIF 置位后数据已经在 SSPBUF 里了直接读即可。但要注意读 SSPBUF 会清除 BFBuffer Full标志如果用了中断或者 DMA逻辑要相应调整。我这里用的是轮询方式简单可靠。第三高速时钟下的等待。8MHz SPI 时钟一个字节 8 位传输时间 1μs。while 循环的开销在这个量级下可以忽略。但如果你的 MCU 主频很低比如 4MHzwhile 循环本身可能就要几个微秒这时候要评估是否会影响时序。PIC18LF4553 跑 32MHz 时指令周期 125ns完全没问题。3. MR25H40CDF 的读写指令与时序拆解3.1 指令集概览MR25H40CDF 的指令集和标准 SPI EEPROM 非常接近主要指令如下指令名称指令码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据RDID0x9F读器件 ID和 EEPROM 最大的区别在于MRAM 没有 WIP 位需要轮询。EEPROM 写完一个字节后状态寄存器的 WIP 位会置 1表示内部写周期进行中主机必须等待 WIP 清零才能发下一条指令。MRAM 不需要写完指令发完数据就存好了可以直接发下一条指令。但 WREN 还是需要的。每次写操作之前必须先发 WREN 指令把内部的写使能锁存器置位。写操作完成后这个锁存器会自动复位。所以连续写多个地址时每次写之前都要发 WREN。3.2 读操作时序与代码读操作最简单。拉低 CS发 READ 指令0x03然后发 3 字节地址24 位地址因为 512KB 需要 19 位地址但协议用 3 字节接着就可以连续读数据了。地址会自动递增可以一直读到 CS 拉高为止。void MRAM_Read(uint32_t addr, unsigned char *buf, uint16_t len) { MRAM_CS 0; SPI_ExchangeByte(0x03); // READ 指令 SPI_ExchangeByte((addr 16) 0xFF); // 地址高字节 SPI_ExchangeByte((addr 8) 0xFF); // 地址中字节 SPI_ExchangeByte(addr 0xFF); // 地址低字节 for (uint16_t i 0; i len; i) { buf[i] SPI_ExchangeByte(0x00); // 发送 dummy 字节读取数据 } MRAM_CS 1; }这里有个细节地址是 24 位的但 MR25H40CDF 只有 512KB实际有效地址是 19 位。高 5 位地址会被忽略。不过为了兼容性和代码通用性还是按 24 位发。如果你以后换更大容量的 MRAM代码不用改。读操作没有页边界限制可以跨页连续读。这点比 EEPROM 方便EEPROM 读虽然也能连续但写的时候有页边界问题。3.3 写操作时序与页边界处理写操作稍微复杂一点。MR25H40CDF 的写操作支持页写入页大小是 256 字节。也就是说一次 WRITE 指令最多写 256 字节而且不能跨页。如果起始地址是 0x00FF你发 2 个字节的数据第一个字节写到 0x00FF第二个字节会回卷到 0x0000而不是写到 0x0100。这是很多新手容易踩的坑。void MRAM_Write(uint32_t addr, const unsigned char *buf, uint16_t len) { while (len 0) { // 计算当前页剩余空间 uint16_t page_remain 256 - (addr 0xFF); uint16_t write_len (len page_remain) ? len : page_remain; // 写使能 MRAM_CS 0; SPI_ExchangeByte(0x06); // WREN MRAM_CS 1; // 写数据 MRAM_CS 0; SPI_ExchangeByte(0x02); // WRITE 指令 SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for (uint16_t i 0; i write_len; i) { SPI_ExchangeByte(buf[i]); } MRAM_CS 1; // 更新地址和长度 addr write_len; buf write_len; len - write_len; } }这段代码的核心是页边界处理。每次写之前计算当前地址到页尾还有多少字节只写这么多剩下的下一轮再写。这样无论起始地址在哪都不会发生回卷。还有一个关键点CS 拉高的时机。WRITE 指令的数据是在 CS 上升沿被锁存的。也就是说你必须把数据字节全部发完然后拉高 CSMRAM 才会真正执行写入。如果 CS 拉高太早数据不完整如果 CS 一直不拉高写入不会触发。这个和 EEPROM 一样但 MRAM 不需要等 WIP所以 CS 拉高后可以立刻发下一条指令。3.4 状态寄存器与写保护配置MR25H40CDF 的状态寄存器有几位用于块保护Block Protect可以保护部分地址区域不被写入。这在工业应用里很有用比如把校准参数区保护起来防止程序跑飞时误写。状态寄存器格式如下位名称功能7SRWD状态寄存器写保护6BP1块保护位 15BP0块保护位 040保留30保留20保留1WEL写使能锁存00保留BP1 和 BP0 组合可以保护不同区域。比如 BP10BP01 时保护高 1/4 区域0x60000~0x7FFFF。具体保护范围要查数据手册不同组合对应不同区域。写状态寄存器需要先发 WREN然后发 WRSR 指令0x01再发状态字节。注意 WRSR 本身也受 WREN 控制而且如果 SRWD 位和 WP 引脚配合可以硬件锁定状态寄存器。我在项目里的做法是把最后 4KB 区域设为写保护用来存出厂校准参数。程序正常运行时不写这块区域只有进入专门的校准模式才临时解除保护。这样即使程序有 bug 乱写也不会破坏关键参数。4. 掉电保护与数据完整性设计4.1 掉电检测电路与响应时间掉电保护是这个项目的核心需求之一。工业现场断电没有预告可能是电网波动也可能是人为拉闸。要保证数据不丢必须在电源电压跌落到 MCU 和 MRAM 最低工作电压之前把关键数据写进 MRAM。我的方案是用一个电压监测芯片比如 MCP1316监测 3.3V 电源。当电压跌落到 3.0V 时监测芯片输出一个中断信号给 PIC18LF4553 的外部中断引脚。MCU 在中断服务程序里立刻把关键数据写入 MRAM。这里的时间预算是关键。从 3.0V 跌落到 2.7VMRAM 最低工作电压中间的时间取决于电源的滤波电容和负载电流。假设板子上有 100μF 的电容负载电流 50mA那么t C * ΔV / I 100μF * 0.3V / 50mA 0.6ms0.6ms 听起来很短但对于 MRAM 来说足够了。写 64 字节数据SPI 时钟 8MHz传输时间64 字节 * 8 位 / 8MHz 64μs加上指令开销和中断响应时间总共不到 100μs。所以 0.6ms 的窗口绰绰有余。这也是 MRAM 相比 EEPROM 的优势——EEPROM 写 64 字节可能需要几十毫秒根本来不及。4.2 数据结构与双备份策略虽然 MRAM 本身很可靠但为了万无一失我还是做了双备份。关键数据结构如下typedef struct { uint32_t magic; // 魔数用于判断数据有效性 uint32_t run_hours; // 累计运行时长 uint32_t fault_count; // 故障计数 uint16_t calib[8]; // 校准系数 uint16_t crc; // CRC16 校验 } CriticalData_t; // 双备份地址 #define ADDR_PRIMARY 0x00000 #define ADDR_BACKUP 0x01000写入时先写主备份再写备份。读取时先读主备份校验 magic 和 CRC如果失败再读备份。如果两个都失败说明数据从未初始化过使用默认值。这个策略的逻辑是MRAM 写入是原子的不会出现写一半的情况。但如果程序在写主备份和写备份之间掉电主备份是新的备份是旧的。读取时主备份校验通过就用主备份没问题。如果主备份的 magic 或 CRC 损坏概率极低但理论上存在就用备份。4.3 CRC 校验的实现与必要性CRC 校验在这里不是可选项是必须的。原因有两个第一SPI 通信在工业环境下可能受干扰虽然概率低但一旦发生写入的数据就是错的第二MRAM 虽然可靠但极端情况下比如电源跌落过快导致写入不完整也可能出现数据异常。我用的是 CRC16-CCITT多项式 0x1021初始值 0xFFFF。代码不复杂查表法或者逐位计算都可以。逐位计算占用代码空间小适合 PIC18 这种资源有限的 MCU。uint16_t CRC16_Calc(const unsigned char *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ (uint16_t)data[i] 8; for (uint8_t j 0; j 8; j) { if (crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }计算 CRC 的时候要注意CRC 字段本身不参与计算。也就是说先算前面所有字段的 CRC再把结果填到 crc 字段里。读取校验时先把 crc 字段保存把它置零或者跳过再算一遍比较结果。4.4 写入频率与实时性权衡虽然 MRAM 写入很快但也不是说可以无限制地写。SPI 总线是共享资源如果频繁写入会占用 CPU 时间影响其他任务。我的做法是分级处理累计运行时长每秒更新一次但只在 RAM 里累加每 10 秒写一次 MRAM。故障计数故障发生时立刻写这是关键事件不能丢。校准系数只在校准模式下写正常运行不写。报警记录用环形缓冲区每条报警写一次。这样平均下来MRAM 的写入频率很低SPI 总线占用可以忽略。掉电时只需要把 RAM 里最新的运行时长和故障计数写进去即可。这里有个经验不要把 MRAM 当 RAM 用虽然它很快但 SPI 通信本身有开销。合理做法是在 RAM 里维护一份最新数据定期或者事件触发时同步到 MRAM。掉电中断里做最后一次同步。5. 实测中遇到的坑与排查过程5.1 第一版板子的偶发写入失败第一版板子打回来后我写了个测试程序循环写入递增数据然后读回来比对。跑了几万次发现偶尔会有写入失败——读回来的数据和写入的不一致。概率很低大概几万次出现一次但工业产品不能接受这种概率。排查过程是这样的。首先怀疑 SPI 时序用逻辑分析仪抓波形。抓了很多次终于抓到一次异常CS 拉低后SCK 的第一个时钟边沿比正常情况晚了大概 200ns。这个延迟导致 MRAM 在第一个时钟边沿采样到了错误的数据。进一步查发现是 CS 和 SCK 的走线长度差异太大。CS 走线比 SCK 长了大概 15mm导致 CS 信号到达 MRAM 的时间比 SCK 晚。在 8MHz 时钟下200ns 的延迟对应的距离大约是 3cm信号在 PCB 上的传播速度约 15cm/ns但实际受介电常数影响大概 6cm/ns。15mm 的走线差异确实可能造成这个量级的延迟。解决方案重新布线让 CS 和 SCK 走线尽量等长并且在 CS 靠近 MRAM 一端加一个 100pF 的电容到地稍微延迟 CS 的上升沿保证 CS 在 SCK 之前稳定。改版后连续跑了几百万次写入没有再出现失败。5.2 电源纹波导致的写入错误第二版板子解决了时序问题但在工业现场测试时又出现了新的问题设备在电机启动瞬间会偶发写入错误。用示波器看电源发现电机启动时 3.3V 电源上有大概 200mV 的纹波持续时间几十微秒。MRAM 的写入操作对电源敏感虽然数据手册标称 2.7V~3.6V但电源波动过大时内部电荷泵可能工作异常导致写入失败。解决方案是在 MRAM 的 VCC 引脚旁边加一个 10μF 的钽电容配合原来的 0.1μF 陶瓷电容形成两级滤波。同时在软件上增加写入后的回读校验写完立刻读回来比对如果不一致就重写最多重试 3 次。这个回读校验机制后来成了标配。虽然增加了写入时间但可靠性大幅提升。实测下来加了校验之后即使在电机启动的干扰下也没有再出现数据错误。5.3 SPI 时钟速率与误码率的平衡一开始我把 SPI 时钟设到了 8MHz想着越快越好。但在长线测试用排线连接长度 20cm时误码率明显上升。降到 4MHz 后误码率降到可接受范围。最终产品里用的是 4MHz因为工业现场的电磁环境复杂留足余量比追求速度更重要。这里有个经验公式可以参考SPI 时钟频率和走线长度的乘积不宜超过某个值。对于普通 FR4 板材无屏蔽的排线建议f * L 100 MHz·cm比如 4MHz 时钟走线长度不超过 25cm8MHz 时钟不超过 12.5cm。超过这个范围就要考虑加缓冲器或者降低时钟。5.4 上电初始化的顺序问题还有一个坑是上电初始化顺序。PIC18LF4553 上电后IO 口默认是输入状态CS 引脚是浮空的。如果此时 MRAM 的 CS 引脚受到干扰被拉低而 SCK 上有杂散时钟可能会误触发写操作。解决方案是在初始化代码的最开始第一时间把 CS 引脚配置为输出并拉高。这个操作要放在其他初始化之前。我在 main 函数的第一行就做了这件事void main(void) { // 第一时间拉高 CS防止误触发 TRISAbits.TRISA5 0; LATAbits.LATA5 1; // 其他初始化... SPI_Init(); // ... }另外MRAM 的 WP 引脚在上电时最好保持低电平写保护等系统初始化完成后再拉高。这样即使有干扰也不会误写。我的板子上 WP 引脚接了一个 10k 下拉电阻MCU 初始化完成后通过 GPIO 拉高解除保护。6. 性能实测数据与选型建议6.1 读写速度实测在 4MHz SPI 时钟下我实测了 MR25H40CDF 的读写速度操作数据量耗时等效速率连续读512 字节1.04ms492 KB/s连续写256 字节单页0.54ms474 KB/s单字节写1 字节约 10μs-掉电保存 64 字节64 字节约 150μs-单字节写之所以要 10μs是因为每次写都要发 WREN 指令1 字节 WRITE 指令1 字节 地址3 字节 数据1 字节共 6 字节加上 CS 拉高拉低的开销。所以频繁写单字节效率不高尽量批量写。掉电保存 64 字节耗时 150μs包括中断响应、数据准备、SPI 传输和回读校验。这个时间在 0.6ms 的掉电窗口内绰绰有余。6.2 与 EEPROM、FRAM 的对比为了给选型提供参考我把 MRAM 和常见的 EEPROM、FRAM 做了个对比特性MR25H40CDF (MRAM)25LC256 (EEPROM)FM25V05 (FRAM)容量512KB32KB64KB接口SPI 40MHzSPI 10MHzSPI 40MHz写周期无等待5ms无等待耐久性10^1410^610^14掉电即存是否是价格参考高低中高供货一般好一般FRAM 在特性上和 MRAM 很接近也是非易失、无等待、高耐久。区别在于 FRAM 用的是铁电材料MRAM 用的是磁性材料。FRAM 的容量通常更小价格略低但供货波动较大。MRAM 的容量可以做得更大工业级温度范围更稳。选型建议如果数据量在 64KB 以内FRAM 是性价比之选如果需要更大容量或者对温度范围要求严格选 MRAM如果写入不频繁、成本敏感EEPROM 够用。6.3 什么场景不适合用 MRAM说了这么多 MRAM 的好处也要说说它不适合的场景。第一大容量存储。MRAM 目前每比特成本还是远高于 Flash存固件、存大量日志用 Flash 或者 SD 卡更划算。第二极高速度连续写入。虽然 MRAM 写周期短但 SPI 接口的带宽有限40MHz 时钟下理论最大 5MB/s实际也就 4MB/s 左右。如果需要更高速率得用并行接口或者 QSPI 的 MRAM但那又是另一个价位了。第三对成本极度敏感的量产产品。如果产品出货量大每颗芯片贵几块钱总成本就很可观了。这种情况下用 EEPROM 加磨损均衡算法或者用带备份电池的 SRAM可能更经济。6.4 长期运行可靠性观察这个项目已经在小批量试产阶段跑了大概半年现场环境有高温夏天机柜内温度到 60°C、有振动、有电磁干扰。目前反馈回来的数据是没有出现数据丢失或损坏的情况。掉电保护触发过几次现场拉闸每次都能正确保存数据。我定期会读取 MRAM 里的错误计数器每次回读校验失败会累加目前所有设备的错误计数都是 0。这说明在 4MHz SPI 时钟、加了回读校验和电源滤波的条件下MRAM 的可靠性完全满足工业要求。有一点要注意MRAM 的数据保持能力在高温下会有所下降。数据手册标称 20 年是在 85°C 下的保持时间如果长期在更高温度下运行保持时间会缩短。不过工业级应用一般不会超过 85°C所以问题不大。如果你们的场景温度更高建议查一下具体的数据保持曲线。7. 代码组织与可移植性设计7.1 分层架构硬件抽象层与业务逻辑分离写驱动的时候我习惯把代码分成两层硬件抽象层HAL和业务逻辑层。HAL 负责和 MRAM 直接打交道提供 read、write、init 这些接口业务逻辑层负责数据结构的组织、CRC 校验、双备份策略等。这样分的好处是如果以后换 MCU 或者换存储芯片只需要改 HAL业务逻辑不用动。比如从 PIC18LF4553 换到 STM32SPI 的寄存器操作完全不同但 MRAM 的指令序列是一样的HAL 里改 SPI 收发函数即可。HAL 的接口定义如下// mram_hal.h void MRAM_HAL_Init(void); void MRAM_HAL_CS_Low(void); void MRAM_HAL_CS_High(void); unsigned char MRAM_HAL_ExchangeByte(unsigned char data);业务逻辑层调用这些接口不直接操作寄存器。这样移植的时候只需要实现这几个函数。7.2 参数配置的集中管理项目里有很多参数比如 SPI 时钟、页大小、备份地址、CRC 多项式等。这些参数我统一放在一个头文件里用宏定义管理// mram_config.h #define MRAM_SPI_CLOCK_HZ 4000000 #define MRAM_PAGE_SIZE 256 #define MRAM_TOTAL_SIZE 0x80000 #define MRAM_ADDR_PRIMARY 0x00000 #define MRAM_ADDR_BACKUP 0x01000 #define MRAM_ADDR_CALIB 0x7F000 #define MRAM_CRC_POLY 0x1021 #define MRAM_WRITE_RETRY_MAX 3这样调整参数的时候不用满代码找改一个地方就行。而且这些宏在编译时展开不占用 RAM。7.3 错误处理与重试机制SPI 通信可能失败MRAM 写入也可能失败虽然概率低。我的做法是每个操作都有返回值调用者检查返回值决定是否重试。typedef enum { MRAM_OK 0, MRAM_ERR_SPI, MRAM_ERR_VERIFY, MRAM_ERR_TIMEOUT } MRAM_Status_t; MRAM_Status_t MRAM_WriteWithVerify(uint32_t addr, const unsigned char *buf, uint16_t len) { for (uint8_t retry 0; retry MRAM_WRITE_RETRY_MAX; retry) { MRAM_Write(addr, buf, len); unsigned char verify_buf[64]; uint16_t verify_len (len 64) ? 64 : len; MRAM_Read(addr, verify_buf, verify_len); if (memcmp(buf, verify_buf, verify_len) 0) { return MRAM_OK; } } return MRAM_ERR_VERIFY; }这里只校验前 64 字节是为了平衡可靠性和速度。如果数据量大可以分段校验。重试 3 次都失败说明硬件可能有问题返回错误让上层处理。7.4 掉电中断服务程序的编写要点掉电中断服务程序ISR有几个要点。第一ISR 要短。掉电窗口只有几百微秒ISR 里不能做复杂运算。我的做法是在 ISR 里只做一件事把预先准备好的数据缓冲区写入 MRAM。数据缓冲区的准备在主循环里完成ISR 直接调用写入函数。第二ISR 里不能调用可能阻塞的函数。比如 printf、malloc 这些都不能用。SPI 收发函数是轮询方式虽然会等待但等待时间确定微秒级可以用。第三ISR 要防止重入。掉电中断可能触发多次电压抖动所以要用一个标志位防止重复进入。volatile uint8_t power_fail_flag 0; void __interrupt(high_priority) HighISR(void) { if (INTCON3bits.INT2IF INTCON3bits.INT2IE) { INTCON3bits.INT2IF 0; if (!power_fail_flag) { power_fail_flag 1; // 保存关键数据 MRAM_WriteWithVerify(MRAM_ADDR_PRIMARY, (unsigned char*)g_critical_data, sizeof(g_critical_data)); MRAM_WriteWithVerify(MRAM_ADDR_BACKUP, (unsigned char*)g_critical_data, sizeof(g_critical_data)); } } }注意掉电 ISR 里我用了 WriteWithVerify虽然会增加时间但掉电保存是最后一道防线必须确保成功。实测 64 字节数据加校验在 150μs 内完成时间足够。8. 从原型到量产的工程化考量8.1 批量生产中的一致性测试原型阶段跑通不代表量产没问题。批量生产时每块板子的元器件参数有差异PCB 走线也有微小差别。为了保证一致性我设计了一个产线测试程序每块板子出厂前都要跑一遍。测试内容包括MRAM 全片读写测试写入特定模式读回比对、掉电保护功能测试模拟掉电信号检查数据是否正确保存、SPI 时钟余量测试在标称时钟的 1.5 倍下读写验证时序余量。全片读写测试耗时较长512KB 全部写一遍再读一遍按 4MHz 时钟算大概需要 2 秒左右。产线上可以接受。如果嫌慢可以只测试关键区域比如前 64KB 和最后的校准区。8.2 现场固件升级时的数据迁移产品卖出去之后可能需要升级固件。升级固件本身不影响 MRAM 里的数据但如果数据结构变了比如增加了新字段就需要做数据迁移。我的做法是在数据结构里预留版本号字段。固件启动时检查 MRAM 里的数据版本如果低于当前固件期望的版本就执行迁移函数。迁移函数负责把旧格式的数据转换成新格式然后写回。typedef struct { uint16_t data_version; // 数据结构版本 uint32_t magic; // ... 其他字段 } CriticalData_t; void Data_Migrate(CriticalData_t *data) { if (data-data_version CURRENT_DATA_VERSION) { // 根据版本号逐级迁移 if (data-data_version 1) { // 从版本 1 迁移到版本 2 // 比如新增字段赋默认值 >
返回列表