ARTICLE DETAIL

资讯详情

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

STM32驱动MRAM替换Flash:工业级非易失存储完整方案

STM32驱动MRAM替换Flash:工业级非易失存储完整方案 做工业设备最烦的一件事就是存数据。我这里说的数据不是那种上电初始化读一次、几年不动一次的配置参数而是像故障记录、运行日志、工艺曲线这类高频追加写入的东西。以前我用SPI Flash做过一版记录模块写到后面发现寿命和擦除时间全成了瓶颈Flash写数据之前必须先擦除一擦就是几十到上百毫秒掉电瞬间那点宝贵时间根本不够把现场数据抢救下来而且Flash擦写寿命十万次上下工业设备一天写几百条记录一年就能拿掉一多半寿命。后来我把存储芯片换成了MRAM型号是富士通的MR25H40CDF主控用STM32F407ZG整套驱动从零写了一遍这才算把存储这块彻底治利索了。这篇文章把这次从选型、接线、写驱动到调坑的完整过程记下来。整个方案对工业场景很合适代码量不大HAL库环境下直接能跑。不管你是想把旧项目从EEPROM或Flash迁到MRAM还是新项目想选一种省心的大容量非易失存储这套思路都值得看一眼。1. 为什么是这个组合MRAM到底比Flash和EEPROM强在哪1.1 MR25H40CDF是个什么器件MR25H40CDF是富士通半导体出的一颗SPI接口的MRAM芯片容量4Mbit也就是512KB。MRAM全称Magnetoresistive Random Access Memory磁阻随机存取存储器核心存储单元是磁性隧道结靠磁化方向记录数据而不是靠Flash和EEPROM那种电荷捕获。这个物理原理带来一个直观的好处写数据不需要先擦除也不需要等待电荷慢慢累积写入本身是纳秒级别的物理翻转掉电之后磁性方向依然保留数据不丢。打个不太严谨但好懂的比方Flash像一块写满字要先用橡皮擦干净才能再写字的草稿纸而MRAM相当于一面翻页牌板拨一下牌子就换了一行字不用等也不用擦。工业级MRAM的温度范围也很宽常规型号能扛-40℃到85℃以上恰好覆盖绝大多数工控设备的工作环境。我用的是3.3V版本和STM32F407ZG的IO电平直接匹配不需要额外做电平转换。1.2 三类非易失存储的参数对比很多朋友一看到MRAM价格比Flash贵转头就去选大容量Flash。真算一笔账就知道贵有贵的原因。我把几种常见方案的关键参数放在一起看参数MR25H40CDFMRAM典型SPI NOR Flash典型I2C EEPROM容量4Mbit1Mbit~64Mbit一般≤2Mbit接口SPISPII2C写前擦除不需要需要不需要写入速度极快指令后近乎立即完成页编程几百微秒到几毫秒擦除更慢单字节写入典型5ms擦写寿命10的12次方次10万次量级100万次量级数据保持典型10年85℃典型20年但受磨损影响典型100年写入功耗较低交大擦除时电流尖峰明显较低我算过一笔账假设一台设备每秒钟往存储里写一条记录Flash的10万次寿命大约两天多就见底了但MRAM的10的12次方次寿命哪怕每天写100万次也能跑两千多年。工业设备设计寿命通常要求十到二十年只有MRAM这种量级的寿命才不用天天担心颗粒写穿了。1.3 工业场景下真正卡脖子的三个约束选型不能只看容量工业存储真正的难点集中在三个地方。第一是掉电保存。设备断电的瞬间系统要抢在电压跌落到工作范围以下之前把最后几个关键参数写进非易失存储。Flash在掉电前往往还卡在擦除流程里一个扇区擦一半就被断电打断数据直接损坏。MRAM没有擦除概念写数据就是几个SPI时钟的事掉电前抢救数据从容得多。第二是高频写入的寿命。日志、计数器、工艺曲线这类数据写入频率极高而且毫无规律。EEPROM写次数虽然比Flash高一个数量级但密度做不大4Mbit级别的I2C EEPROM又贵又少见Flash容量倒是大但磨损均衡要自己维护。MRAM直接把寿命问题抹平了不用做磨损均衡这种复杂的软件策略。第三是数据可靠性。MRAM对磁场和温度不敏感没有刷新机制也不会漏电比DRAM加后备电池的方案省心比电荷存储更抗辐射在工业现场的强电干扰环境里表现稳定。STM32F407ZG这边就更不用多说了Cortex-M4F内核168MHz主频1MB Flash192KB RAM三个SPI外设齐全还有硬件CRC单元做数据校验不需要软件逐位算。这个组合在工业控制板里属于非常成熟的配置。2. 硬件接线与CubeMX配置先把手册和板子对齐2.1 MR25H40CDF引脚定义与STM32F407ZG接线表MR25H40CDF的SPI接口引脚和常见SPI存储器件完全一致片选CS、时钟SCK、数据输入SI、数据输出SO外加两个控制引脚WP和HOLD剩下就是电源和地。我选STM32F407ZG的SPI1来做对接具体接法如下MR25H40CDF引脚引脚功能接到STM32F407ZG备注VCC电源3.3V必须就近放去耦电容GND电源地GND就近接地走线尽量短CS片选PA4用普通GPIO软件控制低有效SCKSPI时钟PA5SPI1_SCKSISPI主机输出/从机输入PA7SPI1_MOSISOSPI主机输入/从机输出PA6SPI1_MISOWP写保护3.3V拉高禁用硬件写保护HOLD保持输入3.3V拉高禁用保持功能CS用普通GPIO做软件控制这是工业里最常见的做法。硬件NSS虽然在STM32上也支持但多设备共用SPI总线时软件CS的灵活性高得多也避免NSS自动管理带来的时序意外。2.2 电路设计里那几个容易被忽略的细节第一个重点是HOLD引脚。这颗引脚低电平时会让存储芯片进入保持状态SCK和SI上的信号一概不认。如果HOLD悬空或者受到干扰被拉低你会在调试时看到MISO上死活没有有效数据但示波器量波形又看不出什么大问题。正确做法是直接接到3.3V没有第二种接法。第二个重点是WP引脚。WP低电平会把状态寄存器的某些位锁死导致写使能指令无效。如果焊接时这个引脚不小心接地了你后面写数据会一直失败。拉高就可以绕开所有软件写保护的坑工业环境里对可靠性要求高省去软件写保护的复杂度不是坏事。第三个重点是电源去耦。MRAM写入瞬间虽然功耗不大但任何非易失存储都需要干净的供电。我在VCC引脚附近放了100nF陶瓷电容加10μF钽电容的组合这是非常稳妥的做法。另外CS引脚建议加一个10kΩ上拉电阻防止主控还没初始化时GPIO输出高阻态导致CS电平漂移芯片被误选。第四点是PCB布线。SPI时钟在10MHz量级时对走线长度不算苛刻但工业设备经常有电机、继电器这种强干扰源SPI信号线尽量短远离大电流回路有条件的话做一点包地处理。不要用几十厘米的杜邦线飞线调试飞线带来的反射和串扰会让你误判成软件问题。2.3 CubeMX下的SPI1配置要点打开STM32CubeMX选F407ZG这张板子把SPI1设成Full-Duplex Master模式。PA5、PA6、PA7会自动复用为SPI1_SCK、SPI1_MISO、SPI1_MOSIPA4保持GPIO Output。关键配置项说几个数据帧格式8bitMSB First这是SPI存储器件的基本要求。时钟极性CPOLLow时钟相位CPHA1 Edge这个组合就是SPI Mode 0MR25H40CDF手册标称支持Mode 0和Mode 3用Mode 0是存储类器件最通用的默认选择。NSS管理方式Software不用硬件NSS。时钟分频要算一下。STM32F407ZG的APB2总线时钟是84MHzSPI1挂在APB2上。选分频系数8SPI时钟就是10.5MHz选分频系数4就是21MHz。MR25H40CDF的手册标称最高SPI时钟可以到几十MHz水平但工业板上走线长度、干扰、代码效率都得留余地我用10.5MHz跑得很稳。如果想在后面做高速批量写入可以提高到21MHz但一定要先用示波器确认波形质量。3. 读写驱动的完整实现HAL库代码逐段拆解3.1 驱动文件骨架与基础收发函数我这边工程是用STM32CubeMX生成之后再加了spi_mram.h和spi_mram.c两个文件。所有操作都基于HAL库底层SPI收发函数只有一个非常省事。// spi_mram.h #ifndef __SPI_MRAM_H #define __SPI_MRAM_H #include stm32f4xx_hal.h #define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET) #define MRAM_PAGE_SIZE 256 #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 void MRAM_Init(SPI_HandleTypeDef *hspi); uint8_t MRAM_ReadStatus(void); void MRAM_WriteEnable(void); void MRAM_ReadData(uint32_t addr, uint8_t *buf, uint32_t len); void MRAM_WriteData(uint32_t addr, const uint8_t *buf, uint32_t len); #endifMRAM_CS_LOW和MRAM_CS_HIGH在CubeMX生成的main.h里会自动定义引脚对应的_GPIO_Port和_Pin宏所以这里直接引用就行。// spi_mram.c #include spi_mram.h static SPI_HandleTypeDef *mram_spi; void MRAM_Init(SPI_HandleTypeDef *hspi) { mram_spi hspi; MRAM_CS_HIGH(); } static uint8_t MRAM_TransmitReceive(uint8_t tx) { uint8_t rx 0x00; HAL_SPI_TransmitReceive(mram_spi, tx, rx, 1, 100); return rx; }MRAM_TransmitReceive是唯一真正碰SPI外设的函数。发送一个字节的同时接收一个字节这正好是SPI全双工的特性。100ms超时对SPI存储操作来说绰绰有余如果连100ms都等不来说明硬件链路出问题了。3.2 读状态寄存器与写使能一切写入的前提SPI存储器件有个标准套路写数据之前必须先发写使能指令否则芯片会拒绝写入。这个规则看起来多余其实是防止系统跑飞时乱写数据破坏存储内容。uint8_t MRAM_ReadStatus(void) { uint8_t sr 0x00; MRAM_CS_LOW(); MRAM_TransmitReceive(MRAM_CMD_RDSR); sr MRAM_TransmitReceive(0x00); MRAM_CS_HIGH(); return sr; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_TransmitReceive(MRAM_CMD_WREN); MRAM_CS_HIGH(); while ((MRAM_ReadStatus() 0x02) 0) { // 等待WEL位置位 } }状态寄存器最低两个bit很关键bit0是WIPWrite In Progress写进行中bit1是WELWrite Enable Latch写使能锁存。这两个bit的位置和SPI Flash、SPI EEPROM完全一致所以这套驱动迁移到其他SPI存储器上也通用。MRAM_WriteEnable里的等待循环是我习惯性加的安全检查。正常情况MRAM在几个微秒内就会把WEL置位但如果WP引脚被拉低或者芯片有问题这个循环就永远不会退出。实际调试时这里加个超时计数多少都能接受建议用systick做几百毫秒超时来防止死等。写使能结束后状态寄存器的值应该是0x02。做自检的时候可以读一下如果读回来不是0x02那就得回头查接线和SPI配置了。3.3 读数据一条READ指令搞定MRAM读数据没有页边界限制可以从任意地址连续读到任意长度直接用0x03指令加24位地址后面跟多少字节就收多少字节。业界管这叫连续读非常适合做日志导出、参数批量加载。void MRAM_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); MRAM_TransmitReceive(MRAM_CMD_READ); MRAM_TransmitReceive((addr 16) 0xFF); MRAM_TransmitReceive((addr 8) 0xFF); MRAM_TransmitReceive(addr 0xFF); while (len--) { *buf MRAM_TransmitReceive(0x00); } MRAM_CS_HIGH(); }地址是24位格式高字节先发。MR25H40CDF实际容量512KB地址范围是0x00000到0x7FFFF高位字节永远不超过0x07。地址范围算错了也不怕芯片地址计数器会在超过容量后回卷但数据写到哪里去就乱了所以应用层务必做好越界检查。3.4 写数据拆页逻辑必须自己做MR25H40CDF的页大小是256字节这是兼容SPI存储器的典型值。写数据时一次最多写256字节如果跨越页边界地址计数器会回卷到本页开头把数据覆盖到错误位置。因此驱动里必须做拆页处理一次写一个页内连续块。void MRAM_WriteData(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { uint32_t page_remain MRAM_PAGE_SIZE - (addr % MRAM_PAGE_SIZE); uint32_t chunk (len page_remain) ? len : page_remain; MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_TransmitReceive(MRAM_CMD_WRITE); MRAM_TransmitReceive((addr 16) 0xFF); MRAM_TransmitReceive((addr 8) 0xFF); MRAM_TransmitReceive(addr 0xFF); for (uint32_t i 0; i chunk; i) { MRAM_TransmitReceive(buf[i]); } MRAM_CS_HIGH(); while ((MRAM_ReadStatus() 0x01) ! 0) { // 等待WIP清零 } addr chunk; buf chunk; len - chunk; } }这段拆页逻辑是整套驱动里最值得看的地方。每一轮循环先算当前位置到页尾还剩多少字节chunk取剩余要写字节数和页内剩余空间的较小值然后把这一块连续写完。写完一轮等WIP清零再把地址指针往后挪。MRAM不需要等页编程那种毫秒级的延迟因为物理写入是瞬时的WIP清零极快。但把这个轮询保留是正确习惯也方便向其他SPI存储迁移。3.5 主程序调用示例参数掉落电恢复场景光有驱动还不够我用一个实际场景把这些函数串起来。假设设备需要保存三个运行参数当前温度、运行模式、累计运行时间要求断电重启后能恢复。这属于典型的掉电保存需求。typedef struct { uint32_t magic; // 固定签名用于识别数据有效性 uint16_t version; // 数据结构版本 int16_t temperature; uint8_t mode; uint32_t run_seconds; uint32_t crc32; // 数据校验 } AppParams; #define PARAM_MAGIC 0x4D52414D // MRAM #define PARAM_ADDR 0x00000 #define PARAM_VERSION 1 uint8_t app_save_params(AppParams *params) { uint8_t *raw (uint8_t *)params; params-magic PARAM_MAGIC; params-version PARAM_VERSION; params-crc32 HAL_CRC_Calculate(hcrc, (uint32_t *)raw, (sizeof(AppParams) - 4) / 4); MRAM_WriteData(PARAM_ADDR, raw, sizeof(AppParams)); return 1; } uint8_t app_load_params(AppParams *params) { uint8_t *raw (uint8_t *)params; uint32_t calc_crc; MRAM_ReadData(PARAM_ADDR, raw, sizeof(AppParams)); if (params-magic ! PARAM_MAGIC) return 0; if (params-version ! PARAM_VERSION) return 0; calc_crc HAL_CRC_Calculate(hcrc, (uint32_t *)raw, (sizeof(AppParams) - 4) / 4); if (calc_crc ! params-crc32) return 0; return 1; }这里用F407的硬件CRC单元做校验省CPU资源。注意CRC计算范围是结构体除去crc32字段的其余部分否则自己校验自己没意义。保存函数里先填magic和version再算CRC最后整块写入加载函数按相反顺序读回来判断。这个模式就是工业存储里最基本的结构体加校验方案能挡住绝大部分数据错乱问题。4. 实测踩坑实录SPI时序和写保护是最容易翻车的地方4.1 坑一读回全0xFF连状态寄存器都不对第一次上板调我写了个读状态寄存器的测试函数结果读回来的值是0xFF。正常情况哪怕芯片没初始化好读出来的也不该是全1。全1通常意味着MOSI或MISO这一侧根本没通或者SCK没送进去。我先用示波器量了SCK和CS波形正常。再量MISO发现CS拉低之后MISO一直保持高电平没有任何数据活动。当时排除了硬件断线开始怀疑是SPI模式没配对。检查CubeMX配置才发现时钟极性CPOL被默认设成了High也就是说我其实跑在SPI Mode 2而不是Mode 0。有些芯片对SPI模式不敏感无非是数据采到错开的沿但MRAM这类严格的SPI器件就吃这一套采沿错了全链路都是垃圾数据。把CPOL改回Low、CPHA设成1 Edge之后读状态寄存器正常返回0x00。这里要提醒一下MRAM本身支持Mode 0和Mode 3两种模式如果你用Mode 3也能工作但前提是配置和驱动里的时序假设一致。排查SPI问题永远先看这两根配置再看波形。4.2 坑二写了使能WEL位却始终不置位写使能等待WEL的循环卡死也是我见过很多新手翻车的地方。这个坑的典型特征是读状态寄存器一直返回0x00WEL永远不置位写数据自然全部无效。排查链路的正确顺序是这样先查WP引脚的电位。WP接地会导致写保护生效芯片拒绝把WEL置位。用万用表直接量引脚电压如果WP没接高或者被下拉问题就出在这。再查CS时序。写使能指令要求CS拉低发完0x06命令字节后CS必须拉高让芯片锁存写使能状态。如果代码里CS拉低后忘了拉高或者中间加了个长延时芯片会认为这是一个未完的事务WEL不会置位。我在代码里写的模式是先CS_LOW发命令立即CS_HIGH中间不插入任何其他操作就是为了一次性满足这个要求。最后查状态寄存器里有没有写保护位被软件置位。如果之前跑过WRSR指令把WPEN或相关保护位设成了1即便WP引脚接高电平状态寄存器的锁存也会把写入关掉。这种情况下需要重新发WRSR把保护位清掉或者干脆复位芯片。4.3 坑三跨页写入数据错位回卷有个同事拿我的驱动移植到自己的项目跑去掉了拆页那段逻辑只留了一次性发的循环结果写到地址0x100附近数据就跑到0x000开头去了。他看了半天没想明白来问我是不是芯片坏了。这不是芯片坏是所有页式SPI存储的通用行为。页内地址计数器到页尾之后在同一个WRITE事务里继续发数据地址不会自动进位到下一页而是回卷到本页起始地址。0x000和0x100刚好是相邻页写0x100地址的数据在越过256字节之后全部落到了0x000开头的位置把已经存在的参数覆盖掉了。解决办法就是我在3.4节里的拆页写法一个WRITE事务最多只写到页尾到页边界就收工下一轮重新发WRITE指令从下一页地址开始写。这条经验对后续迁移到任何SPI Flash、SPI EEPROM都通用页大小可能不同但拆页逻辑一脉相承。4.4 坑四HOLD引脚悬空导致MISO完全锁死这个坑属于硬件接线范畴但我在调试时印象很深。有块试验板为了省事HOLD引脚没接悬在空中。结果芯片上电后MISO上什么都读不到CS、SCK、MOSI波形量着都对芯片却像睡着了一样。原因在于HOLD引脚是准模拟输入悬空时内部逻辑电平不确定可能被噪声拉低。一旦HOLD拉低芯片就会锁存当前地址和状态忽略后续SPI事务。把HOLD直接焊接在3.3V上之后再也没出现过这个现象。所以再次提醒HOLD和WP两个引脚不用就必须拉高不是可选项。5. 从Demo到产线掉电存储、校验和文件系统的落地思路5.1 用STM32F407ZG的PVD做掉电紧急写入裸驱动的读写跑通只是第一步真正上产线要考虑掉电场景。STM32F407ZG内置了PVD也就是可编程电压检测器可以设定一个电压阈值主电源电压跌到阈值以下时触发中断。PVD中断里可以做紧急数据保存这是工业设备断电保存数据的标准姿势。PVD阈值我习惯设在3.0V左右。当系统检测到电压跌落到阈值以下进入PVD中断回调此时判断是否有需要保存的关键数据如果有就调用MRAM_WriteData。MRAM的好处在这时候体现得淋漓尽致没有擦除等待整个写入过程就是几十个SPI时钟周期几个参数在十几微秒内就能落盘。换成Flash的话PVD中断里先要处理擦除状态、再等页编程电压很可能已经掉到芯片无法工作的程度了。需要说明的是PVD中断里跑SPI操作要用轮询或DMA尽量别用需要等待RTOS调度的接口。另外要预留掉电瞬时的储能余量比如在电源输入端挂一个几十微法的电解电容确保PVD触发到存储写完这段时间内供电不掉。5.2 存储布局双备份区与版本化数据只靠一个结构体加CRC在不同版本固件之间升级时容易踩雷。比如固件从V1.0升到V1.1结构体加了字段旧版本的数据读取时CRC对不上加载失败设备只能恢复出厂。这种问题我在实际项目里遇到过后来改成双备份加版本控制的方案才省心。具体做法是把存储区划分成两个区A区和B区每个区都保存一份完整参数结构体。写入时一个区一个区轮流写读取时先读A区如果CRC不对就读B区两个区都不对就恢复默认值。同时结构体里带version字段读取时发现version比当前固件低就做一次数据迁移把旧版本字段转成新版本格式再写回。对MRAM来说双备份不会造成寿命压力所以可以放心大胆地做冗余。我见过有人在Flash上为了省寿命不敢做双备份结果断电写坏了一个区导致整个参数区崩溃完全是得不偿失。5.3 把驱动接到文件系统上如果数据量变大需要管理多个日志文件裸地址驱动就不够用了。MRAM上可以跑littlefs或FatFS这类文件系统但有几个点要注意。littlefs本来是给Flash设计的有磨损均衡和掉电保护能力跑在MRAM上毫无问题因为MRAM写次数无限littlefs在MRAM上的磨损均衡机制等于无用功但也不会造成额外麻烦。FatFS则是传统PC文件系统跑在MRAM上也没问题最大优势是文件可以直接在PC上读取方便日志导出做分析。挂在文件系统上的MRAM驱动底层就是我在第3章写的MRAM_ReadData和MRAM_WriteData这两个函数文件系统库会自动把它们封装成diskio层的读写接口。迁移时留意页大小配置MR25H40CDF的页大小是256字节这个值会在文件系统底层影响缓冲区的分配别填错了。如果项目里原来用的是SPI EEPROM驱动迁移到MRAM时几乎可以无缝替代因为指令集和状态寄存器布局是兼容的。唯一的硬差异就是页大小原来EEPROM可能是32字节一页现在MRAM是256字节拆页逻辑按新的页大小走就行。5.4 再聊两句扩展方向MR25H40CDF这篇驱动方案里还有几个我可以继续深挖的方向。一个是把SPI收发从轮询改成DMA加中断这样批量读写日志时CPU可以不阻塞对需要同时处理多个任务的系统很有价值。另一个是多设备共享SPI总线MRAM、传感器、显示器挂同一条SPI总线注意CS信号隔离防止MISO竞争冲突。我目前的项目里MR25H40CDF这块板子已经在数据记录模块上跑了三个月每天上万次写入没有出过一次数据错误。如果换了以前的Flash方案同样的写入频率早就该换颗粒了。整套驱动代码量不大核心就是拆页、写使能、状态轮询这三个点逻辑清晰之后移植到别的SPI存储器件上也很快。最后给个实操层面的小建议调试这类SPI存储模块一定要备一个逻辑分析仪淘宝几十块钱那种带8通道的够用。CS、SCK、MOSI、MISO四根线抓一块一帧一帧对波形比拿示波器一屏一屏刷效率高太多。我就是靠这个把上面那些坑一个个找出来的省下来的调试时间远超那几十块的成本。
返回列表