ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从Flash到MRAM:工业设备掉电数据存储与SPI驱动完整方案

从Flash到MRAM:工业设备掉电数据存储与SPI驱动完整方案 去年我调试一台工业检测设备时被一个看似不起眼的问题折腾了挺久设备每次断电后记录运行次数的Flash扇区时不时就会出坏块连续掉电三次运维统计直接没法看。换过W25Q128也试过EEPROM最后换成Everspin的4Mbit SPI MRAM型号MR25H40CDF配合NXP的MK24FN1M0VDC12这颗Cortex-M4F处理器才把问题彻底根治。今天想把这一整套在工业与嵌入式场景里存储和读取数据的经验完整写下来包括硬件接线、SPI驱动、掉电保存策略还有我亲手踩过的几个坑给同样在做工业存储、嵌入式数据记录方案的朋友做个参考。1. 为什么我在工业项目里把Flash换成了MRAM1.1 工业存储场景的三座大山掉电、磨损、等待工业设备的数据存储和消费电子产品有本质区别。消费级设备写坏一个扇区最多少存一张照片工业设备写坏一次轻则丢失校准参数重则整个设备停机待检。我做的那台设备核心功能是记录每次上料、检测、下料的周期数还要存最近一次运行的详细状态。这些数据的特点是单个记录不大但写入频率很高而且设备随时可能被操作员直接断电。同样是存储NOR Flash在这类场景里至少有三座大山。第一座大山是擦写寿命。通用NOR Flash的擦写次数一般是10万次左右听起来很多可如果设备每秒钟写一次日志一天就是86400次10万次寿命不到两天就用完了。就算把日志分散到多个扇区做磨损均衡也只是把崩溃时间往后推并不能真正解决问题。第二座大山是擦除与编程粒度。NOR Flash写入前必须擦除擦除单位是4KB扇区或者64KB块而最小写单位是1字节或者一页256字节。这就导致一个尴尬的局面我只是想改日志文件里的两个字节却不得不先把整个扇区读出来在内存里改好再擦除整个扇区然后把整块数据写回去。这个过程专业术语叫读-改-写中间任何一步掉电扇区就可能处于半擦除半编程的中间状态。第三座大山是时间等待。写入一小页数据有些Flash要等3ms到5ms的编程时间擦除一个扇区更是要几十毫秒。MCU在这段时间内要么死等要么靠状态查询中断。如果在等待期间突然掉电写入是否成功就是个未知数。其实不只是FlashI2C EEPROM也有类似问题。24C系列虽然支持单字节写、不需要擦除但100万次的寿命和高频写入场景相比仍然偏少而且I2C在400kHz速率下写大数据块的效率本身就有限。1.2 MRAM的底层原理为什么它能同时做到“快”和“不丢”MRAM全称是磁阻随机存取存储器它存储数据的核心结构叫磁隧道结英文MTJ。MTJ由两层铁磁材料和一层极薄的绝缘隧道层构成其中一层铁磁材料的磁化方向是固定的另一层可以自由翻转。两层方向一致时隧道结电阻小代表0方向相反时电阻大代表1。写入数据的时候相当于给自由层施加一个脉冲电流让它的磁化方向翻转到目标状态。这个翻转过程是物理层面的磁化方向改变不是电荷累积也不存在绝缘层被反复充电击穿的问题。所以MRAM的擦写寿命在手册上直接写的是unlimited翻译过来就是无限制理论上想写多少次就写多少次。同时磁化方向在没有外界强磁场干扰的情况下能够长期保持掉电不丢失这也是它被称为非易失性存储器的原因。MRAM没有擦除步骤任何地址都是直接覆盖写不需要先擦后写也就不用担心写了一半掉电把扇区搞坏。我用一个生活化的例子给你感受一下NOR Flash像用铅笔在纸上写字写之前必须先用橡皮擦干净格子纸擦得多了会破MRAM像一块磁性白板上面本来就是可反复擦写的你拿起白板笔写新内容旧内容直接被覆盖不需要先擦除写多少次板面都不会坏。1.3 MR25H40CDF与常见存储方案的取舍MR25H40CDF是Everspin的4Mbit串行MRAM换算下来正好512KBSPI接口最大时钟40MHz供电2.7V到3.6V。它最大的特色就是把前面说的三大痛点一次性全解决没有磨损问题、没有擦除步骤、写入后立即可用。选型之前我做过一张对比表把当时在考虑的几种方案放在一起过了一遍存储方案接口容量写入粒度最大擦写寿命写入速度掉电可靠性MR25H40CDFSPI4Mbit单字节/连续无限制单字节约1µs40MHz极高W25Q128JVSPI128Mbit页/扇区10万次页编程约3ms掉电需处理24LC256I2C256Kbit单字节/页100万次写2字节约5ms较高FM25V20ASPI2Mbit单字节/连续无限制与MRAM相当高SRAM备份电池并行/SPI视型号单字节无限制最快依赖电池从表格能看出来MRAM和FRAM铁电存储器在写入寿命和写入速度上有明显优势。FRAM的唯一弱点是容量普遍偏小常见是8Kbit到4Mbit而且在大容量档位往往比同容量的MRAM更贵。MR25H40CDF做到4Mbit在需要存一段日志或者中等规模配置表的场景里容量刚刚好。接下来说说MRAM不合适的地方。首先是成本单颗MR25H40CDF比同容量的NOR Flash贵不少所以它适合的是对可靠性和写入频次敏感的数据区不适合拿来存几十MB的固件镜像其实是存储密度4Mbit在MRAM里算中等容量和动不动128Mbit、256Mbit的NOR Flash不在一个量级。我的做法是固件继续跑在MK24FN1M0VDC12内部的1MB Flash里MRAM只用来放运行日志、参数配置和掉电保护数据。这个分工很快就在后续开发里体现出了价值所有需要高频写入、掉电不丢的数据一律往MRAM上甩再也不用做磨损均衡不用设计坏块管理省了一大堆应用层代码。2. MR25H40CDF与MK24FN1M0VDC12的硬件搭配2.1 为什么选MK24FN1M0VDC12作为主控MK24FN1M0VDC12是NXP Kinetis K24系列的一颗工业级MCUCortex-M4F内核主频最大120MHz带硬件FPU内置1MB Flash和192KB SRAM。后缀DC12对应VFBGA封装适合对板面积要求比较紧的工业模块。选择这颗芯片的时候我看中的不只是性能更多的是它的外设组合。Kinetis K24系列自带多路DSPI模块支持最高协议时钟FIFO深度足够还支持DMA触发。DSPI相关的几个寄存器可以灵活配置时钟极性、相位和分频系数非常适合对接MR25H40CDF这类标准SPI设备。另外MK24FN1M0VDC12内置的电压检测模块LVD非常关键。工业设备掉电保护功能必须提前检测到电源跌落K24支持可编程的掉电检测阈值检测到VDD跌落到设定值以下时会触发中断我可以在这个中断里做紧急数据保存。这个特性在后文的掉电保护流程里会详细展开。当然如果你手头不是K24这颗芯片用GD32、STM32、ESP32也完全不影响MRAM读写逻辑。SPI协议是通用的MR25H40CDF对主控的要求就是能提供SPI Mode 0或者Mode 3时序IO电平3.3VCS信号可控。我选MK24FN1M0VDC12只是因为项目整体定位加上它自身资源充足可以顺手把CAN通信、以太网、人机交互全部跑在同一个主控上。2.2 接线表与硬件设计细节MR25H40CDF采用标准SPI接口引脚不多但有几个细节必须注意。先看接线表MR25H40CDF引脚功能说明连接到MK24FN1M0VDC12VDD电源正3.3V建议0.1µF去耦电容靠近引脚VSS地GNDCS#片选低有效任意GPIO输出建议带弱上拉SCKSPI时钟DSPI模块的SCK引脚例如SPI0_SCKSI主出从入MOSIDSPI模块的SOUT引脚SO主入从出MISODSPI模块的SIN引脚WP#写保护低有效不用时直接接3.3VHOLD#通信暂停低有效不用时直接接3.3V第一版电路图上我把WP#和HOLD#两个引脚漏接了认为不使用的引脚悬空问题不大。结果调试时发现MR25H40CDF偶尔写入失败读出来的数据时对时错。后来翻手册、用示波器量时序才确认是HOLD#悬空导致信号在临界电平上跳动把通信暂停了。工业设计里所有控制引脚必须有一个确定电平不能放任自流。这个教训在硬件评审清单里应该单列一条只要是MRAM/FRAM这类带使能控制引脚的存储器WP#、HOLD#必须上拉到高电平。有些设计为了省一颗电阻把WP#和HOLD#并在一起上拉也可以但千万别悬空。PCB布局上还有一个容易被忽略的点SCK和SIMOSI的走线应该尽量短且保持大体等长尤其是在SPI时钟跑到20MHz以上的时候。MR25H40CDF支持40MHz时钟但40MHz下信号边沿本身就比较陡走线过长或者经过过孔太多反射会把波形搞难看。我的经验是优先保证SCK、SI、SO、CS#四条线的长度差小于2cm如果实在做不到就把SPI时钟降到20MHz换取稳定的通信。电源去耦也要稍微讲究一点。MR25H40CDF内部在翻转磁场时需要消耗瞬时电流电源引脚旁边放一颗0.1µF的MLCC还不够的话可以再加一颗1µF或10µF的钽电容。尤其是设备同时驱动继电器、电机或者传感器时电源纹波大MRAM读写出错率会明显上升我在测试中遇到过VDD跌落到2.9V以下时状态寄存器读取不稳定。2.3 供电电压和温度范围MR25H40CDF的工作电压范围是2.7V到3.6VMK24FN1M0VDC12的IO口电平是3.3V两边直接对接没有电平转换问题。不过要注意Kinetis在VDD低于3.0V时内部Flash编程可能受限但MRAM这边只要供电高于2.7V就正常工作所以3.3V单电源供电即可。温度方面我选择的是工业级温度范围的型号可以在-40℃到105℃环境里长期工作。设备如果做户外环境试点冬天低温、夏天暴晒存储芯片的温度指标都必须逐一核对这一点MR25H40CDF的后缀含义最好以当时购买批次的数据手册为准不同后缀可能对应不同工作温度。3. 从零写驱动SPI指令集与关键代码3.1 MR25H40CDF的指令集记住“没有擦除”这件事MR25H40CDF本质上是一颗4Mbit的SPI MRAM指令集和常见的SPI NOR Flash有点像但又少了很多东西。用的最多的是这六条指令指令名称操作码功能READ0x03从指定地址连续读数据FAST_READ0x0B快速读额外加一个dummy字节WRITE0x02从指定地址连续写数据WREN0x06设置写使能锁存位WELWRDI0x04清除写使能锁存位RDSR0x05读状态寄存器WRSR0x01写状态寄存器配置写保护和NOR Flash最大的不同就是没有擦除指令也不需要页编程指令。WRITE指令支持单字节写也支持连续写如果你愿意可以从0x00000连续写到0x7FFFF不需要按扇区对齐没有分页限制地址可以随时停下完全自由。这里有一个特别容易翻车的细节MR25H40CDF虽然是4Mbit存储空间地址线实际只需要19位但它的指令格式里地址字段固定是24位也就是3字节。所以不管是发READ还是WRITE指令片选拉低之后必须依次发送操作码、地址高字节、地址中字节、地址低字节。比如地址0x00000就必须发0x00 0x00 0x00三个字节哪怕高字节全是0也不能省略。我第一次移植的时候习惯性地按照2字节地址去发结果写进去的数据全都错位因为MRAM把第三个字节当成了数据而不是地址。这个坑几乎每个第一次做SPI MRAM的人都会踩后面会用代码再强调一遍。3.2 SPI初始化Mode 0先从20MHz起步MK24FN1M0VDC12的SPI初始化我使用的是NXP MCUXpresso SDK风格方便照抄。核心配置就是选择DSPI模块、设置主模式、时钟极性和相位、波特率。#include fsl_spi.h #define MRAM_SPI_BASEADDR SPI0 #define MRAM_SPI_CLK_FREQ 120000000U spi_master_config_t mram_spi_config; void MRAM_SPI_Init(void) { SPI_MasterGetDefaultConfig(mram_spi_config); mram_spi_config.baudRate_Bps 20000000U; // 先跑20M稳定后再提40M mram_spi_config.clockPolarity kSPI_ClockPolarityActiveHigh; // CPOL0 mram_spi_config.clockPhase kSPI_ClockPhaseFirstEdge; // CPHA0 SPI_MasterInit(MRAM_SPI_BASEADDR, mram_spi_config, MRAM_SPI_CLK_FREQ); }MR25H40CDF支持SPI Mode 0和Mode 3默认按Mode 0使用即可。片选CS#不要挂在DSPI硬件PCS上建议用普通GPIO控制。一是因为后续要做的WREN和WRITE之间片选要拉高再拉低用GPIO控制更直接二是如果用硬件PCS切换不同SPI设备时容易出问题。片选和底层收发函数如下#define MRAM_CS_GPIO GPIOB #define MRAM_CS_PIN 6 static inline void MRAM_CS_Low(void) { GPIO_PinClear(MRAM_CS_GPIO, MRAM_CS_PIN); } static inline void MRAM_CS_High(void) { GPIO_PinSet(MRAM_CS_GPIO, MRAM_CS_PIN); } static void MRAM_SpiTransfer(const uint8_t *tx, uint8_t *rx, uint32_t len) { /* SDK的SPI_MasterTransfer会同时完成发送和接收 */ spi_transfer_t transfer {0}; transfer.txData (uint8_t *)tx; transfer.rxData rx; transfer.dataSize len; SPI_MasterTransferBlocking(MRAM_SPI_BASEADDR, transfer); }3.3 核心读写函数三个地址字节一个都不能少下面是一套最基础、最容易移植的读写函数。读单字节、写单字节、连续读、连续写都是标准流程。void MRAM_WriteEnable(void) { uint8_t cmd 0x06; MRAM_CS_Low(); MRAM_SpiTransfer(cmd, NULL, 1); MRAM_CS_High(); } void MRAM_WriteDisable(void) { uint8_t cmd 0x04; MRAM_CS_Low(); MRAM_SpiTransfer(cmd, NULL, 1); MRAM_CS_High(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t txBuf[4] {0x03, (uint8_t)(addr 16), (uint8_t)(addr 8), (uint8_t)addr}; uint8_t rxBuf[4] {0}; MRAM_CS_Low(); MRAM_SpiTransfer(txBuf, rxBuf, 4); MRAM_CS_High(); return rxBuf[3]; } void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t txBuf[5] {0x02, (uint8_t)(addr 16), (uint8_t)(addr 8), (uint8_t)addr, data}; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SpiTransfer(txBuf, NULL, 5); MRAM_CS_High(); MRAM_WriteDisable(); } void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[3] {0x03, (uint8_t)(addr 16), (uint8_t)(addr 8), (uint8_t)addr}; MRAM_CS_Low(); MRAM_SpiTransfer(cmd, NULL, 4); MRAM_SpiTransfer(NULL, buf, len); MRAM_CS_High(); } void MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4] {0x02, (uint8_t)(addr 16), (uint8_t)(addr 8), (uint8_t)addr}; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SpiTransfer(cmd, NULL, 4); MRAM_SpiTransfer((uint8_t *)buf, NULL, len); MRAM_CS_High(); MRAM_WriteDisable(); }这套函数有三个值得说的地方。第一MRAM_WriteByte里先调用了MRAM_WriteEnable也就是发0x06指令。WREN指令的作用是把状态寄存器里的WEL位置1表示允许写入。如果跳过这一步WRITE指令会被忽略。写完数据之后我又调了一次MRAM_WriteDisable把WEL清除防止误操作。第二MRAM_ReadBuffer里我调用了两次MRAM_SpiTransfer第一次发命令和地址第二次连续读数据。因为片选在整个过程中保持低电平中间切换一次DSPI收发并不影响操作。第三WRITE指令没有“页边界”概念所以MRAM_WriteBuffer不需要像Flash那样限制在页内末尾截断你可以一次写任意长度哪怕横跨整个512KB空间。这一点在实现日志系统时非常舒服。如果你要从MRAM里连续读取比较大数据块还可以用FAST_READ指令0x0B。它的好处是地址后面多了一个dummy字节这个dummy可以弥补地址发出后主控切换收发模式的延迟让高速时钟下读取更稳定。代码上就是把命令字节换成0x0B然后多传一个随便什么字节。void MRAM_FastRead(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[5] {0x0B, (uint8_t)(addr 16), (uint8_t)(addr 8), (uint8_t)addr, 0x00}; MRAM_CS_Low(); MRAM_SpiTransfer(cmd, NULL, 5); MRAM_SpiTransfer(NULL, buf, len); MRAM_CS_High(); }3.4 状态寄存器与写保护配置MR25H40CDF的状态寄存器结构不复杂但理解它是避免“莫名其妙写不进数据”的关键。状态寄存器各位的定义大约是bit0是WEL写使能锁存bit1到bit3是BP0到BP2块保护位bit4是WPEN写保护引脚使能剩余位保留。出厂默认状态寄存器值一般是0x00也就是写保护全部关闭全片可写。读取状态寄存器用0x05指令uint8_t MRAM_ReadStatus(void) { uint8_t txBuf[2] {0x05, 0x00}; uint8_t rxBuf[2] {0}; MRAM_CS_Low(); MRAM_SpiTransfer(txBuf, rxBuf, 2); MRAM_CS_High(); return rxBuf[1]; }如果你希望进一步防止误写可以用WRSR指令把块保护打开。比如保护存储器最前面的1/4空间void MRAM_SetBlockProtect(uint8_t bpBits) { uint8_t txBuf[2] {0x01, bpBits}; // bpBits对应状态寄存器bit1~bit3 MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SpiTransfer(txBuf, NULL, 2); MRAM_CS_High(); MRAM_WriteDisable(); }调试阶段建议先保持全片可写。产品阶段如果配置区在MRAM低地址启动后先把配置写进去再打开只保护配置区的块保护位这样运行日志区即使被误操作系统参数也不会被动掉。4. 掉电时序设计与应用层数据布局4.1 掉电紧急保存流程为什么MRAM是“最后一根稻草”工业设备掉电是最常见的异常事件操作员可能在任何时刻按下急停或者切断电源。传统的处理流程是MCU检测到掉电然后抢时间把关键状态写入非易失存储器。问题在于如果这个非易失存储器是Flash掉电中断里写入的那一刻往往是灾难——因为Flash要先擦除再编程整个流程要好几毫秒甚至几十毫秒电源电压从3.3V跌落到MCU最低工作电压1.71V的时间窗口经常不够用结果就是写入失败或者扇区损坏。MR25H40CDF天然适配这种场景。掉电检测到之后只需要把关键数据通过WRITE指令写进MRAM一次连续写64字节在20MHz时钟下大约只需要30µs左右40MHz时钟下更快。这个时间量级即使电源已经掉到2.5V只要MCU还在运行就足够完成写入。我实际用的掉电保护流程是MK24FN1M0VDC12的LVD模块配置一个掉电阈值比如3.0V当VDD下降到阈值以下时触发掉电中断中断优先级设为最高。掉电中断里先把当前运行状态、电机位置、累计计数、错误码等数据组织成一个结构体计算CRC32然后写入MRAM的专用掉电保护区。写入完成后设置一个掉电标志位表示这次关机是“干净关机”。上电启动时先读掉电保护区。如果掉电标志有效就恢复上次运行状态如果无效说明上次是异常断电或者数据损坏走默认启动流程。因为MRAM写入不需要等待编程完成时间整个掉电保存流程可以从容完成这是其他方案很难替代的价值。4.2 存储区规划双备份配置区加环形日志区存储区规划直接决定应用层代码复杂度和数据可靠性。我习惯把512KB的MRAM空间划分为三块。第一块是配置参数区从地址0x00000开始占4KB。这里存放设备编号、校准系数、通信地址、量程上下限等参数。因为参数可能在调试过程中反复修改所以我在同一区域里做了两个备份主拷贝一份镜像拷贝一份。每次写入先写主拷贝再写镜像拷贝读取时两个拷贝都读出来做CRC校验选校验通过的那份。这样即使写镜像拷贝过程中掉电主拷贝还是完好的。第二块是运行日志区从0x01000开始占128KB。日志记录采用环形覆盖方式每条日志固定64字节依次写入。MRAM无限擦写寿命让日志管理简化到了极致不需要磨损均衡不需要日志合并/压缩/翻页只需要一个递增的日志序号和一个“区块擦除”的用户逻辑索引。第三块是掉电保护区放在存储空间末尾占64KB。上面说的掉电紧急状态就写在这里固定地址每次覆盖。这样做数据布局有四个原则可以总结配置区必须有双备份加CRC校验因为配置损坏可能让整个设备无法正常工作日志区不需要磨损均衡因为MRAM寿命无限但要设计环形覆盖防止512KB空间写满后无法继续掉电保护区不要和日志区混在一起因为它写入频率最低但重要性最高所有区块首地址都尽量按4KB对齐方便以后如果要换成其它存储芯片时逻辑改动最小。4.3 数据校验CRC32比简单累加可靠得多工业数据校验我不建议用简单的8位校验和。配置参数一个字节被改写简单校验和有可能算出来仍然相同尤其某些位翻转同时影响两个字节时更不可靠。我在关键数据结构上统一用CRC32。CRC32的实现网上有很多表驱动版本这里不整段贴代码了只讲讲流程typedef struct { uint32_t magic; // 固定魔数防止读到未初始化的区域 uint32_t version; // 数据结构版本号 uint32_t crc32; // CRC32校验值 uint64_t runCount; // 累计运行次数 float lastAlgTemp; uint16_t lastErrCode; uint8_t reserved[40]; } SystemStatusBlock; #define SYSTEM_STATUS_MAGIC 0xA55A5AA5 void SystemStatusSave(SystemStatusBlock *block) { block-magic SYSTEM_STATUS_MAGIC; block-crc32 CRC32_Calc((uint8_t *)block sizeof(block-crc32), sizeof(SystemStatusBlock) - sizeof(block-crc32)); MRAM_WriteBuffer(MRAM_STATUS_SAVE_ADDR, (uint8_t *)block, sizeof(SystemStatusBlock)); } int SystemStatusLoad(SystemStatusBlock *block) { MRAM_ReadBuffer(MRAM_STATUS_SAVE_ADDR, (uint8_t *)block, sizeof(SystemStatusBlock)); if (block-magic ! SYSTEM_STATUS_MAGIC) { return -1; } uint32_t calc CRC32_Calc((uint8_t *)block sizeof(block-crc32), sizeof(SystemStatusBlock) - sizeof(block-crc32)); if (calc ! block-crc32) { return -1; } return 0; }CRC32的初始种子、多项式系数关键是要在整个工程里固定下来最好直接复用成熟的开源实现。实际运行中CRC32校验能抓出绝大多数位翻转和地址错位问题。5. 实测数据与踩坑记录5.1 连续写入和长时间运行实测硬件和驱动写好之后我跑了三轮测试第一轮是压力写入第二轮是掉电模拟第三轮是温度箱测试。压力写入测试用MK24FN1M0VDC12做循环写入每轮写512字节数据到指定地址写完立刻读回比对。40MHz SPI时钟下一次512字节的写入耗时大约110µs读回512字节耗时大约105µs。相比之下用W25Q128写一页256字节需要3到5ms差距是几十倍。我又做了单字节随机写测试随机生成100万个地址逐个写入随机值再读回全部正确。写入速度方面单字节的指令开销占比大但也在几微秒级别完全可以接受。掉电模拟测试相对复杂一点。我用一个继电器在设备正常运行时随机切断电源每次断电后重新上电启动后检查MRAM里的状态块和日志。连续做了200次掉电测试没有出现一次日志数据损坏。这个结果和Flash方案形成的对比非常明显同样200次掉电Flash方案大概率已经出现了扇区损坏。温度箱测试是从-20℃做到85℃每个温度点保持2小时期间持续读写MRAM全部通过。我选的是工业级温度范围的版本如果你的设备使用环境有更高温或更低温需求一定要核对手册的温度规格。5.2 我踩过的三个坑换个主控也跑不掉先说第一个坑也是最常见的地址字节数错误。MR25H40CDF即使容量只有4Mbit也必须发满3字节地址。我第一次移植时参考的是2字节地址的SPI存储器代码结果写数据错位读数据也错位排查了半天才发现地址格式不同。这个坑在PDF手册里其实写得清楚但人很容易惯性思维看到“4Mbit”就觉得2字节地址够用。第二个坑WREN和WRITE之间的片选时序。写入之前必须先拉低CS发WREN然后拉高CS让WREN生效再拉低CS发WRITE命令和数据。如果WREN发完之后CS一直保持低电平状态寄存器WEL位不会置位后续WRITE会被拒绝。如果你发现状态寄存器能读、写操作却无效优先检查这段时序。第三个坑HOLD#引脚悬空。这个前面接线部分已经提到了HOLD#和WP#必须老老实实接上拉电阻。HOLD#拉低会冻结数据传输WP#拉低会禁止写保护区域写入。悬空状态下的浮空电平会导致完全随机、时好时坏的故障非常难排查。除此之外还有一个软件层面的坑MRAM写入不需要等待所以很多人容易忽略写保护位的状态。出厂默认全片可写没问题但一旦你在初始化代码里调用了WRSR开启块保护之后所有写操作就必须先检查状态寄存器的BP位。我在一次升级固件时把配置代码重新写了一遍忘了恢复块保护位结果现场设备参数写了没反应运维反馈“参数改了重启后变成默认值”。所以说块保护功能是双刃剑用之前一定要把整个初始化流程理清楚。5.3 一些可以让你少走弯路的建议最后分享几个实操建议。第一SPI时钟不要一上来就拉满40MHz。先用20MHz验证功能和读写数据一致性把波形稳定了再提到40MHz。40MHz下很多问题来自PCB布局和信号完整性20MHz可能能跑40MHz就开始偶发错误。我最后产品上跑的是30MHz性价比比较高留了足够裕量。第二CS引脚务必用GPIO而不是硬件PCS。MRAM的操作序列对片选时序要求灵活WREN结束要拉高再拉低用GPIO执行起来最直观、最可控。某些MCU的硬件PCS会自动附加延时在极端情况下会影响WREN置位。第三把MRAM读写函数封装成独立驱动文件不要散落在业务代码里。后来我把这套驱动从一个项目移植到另一个项目只改了SPI基址、CS引脚和时钟配置其余代码原封不动。第四如果你的应用需要记录大量历史数据我建议把日志数据设计成“尾部写指针固定结构体”的模式读取时按指针位置反向扫描。MRAM寿命无限所以不用考虑搬移旧日志应用层逻辑简单很多也更容易维护。第五硬件评审时一定要检查MR25H40CDF的WP#、HOLD#上拉电阻是否存在电源去耦电容是否近距离放置。这两个问题最容易在原理图评审时被漏掉但它俩一出问题就是很难查的“软故障”。这套MR25H40CDF加MK24FN1M0VDC12的组合我在设备上用了将近一年后续生产的批次没有一例因存储问题退货。如果你也在做工业数据记录或者高频参数存储方案可以考虑往MRAM这个方向试一下至少在“存储和读取数据”这件事上你会少操心很多。
返回列表