ARTICLE DETAIL

资讯详情

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

STM32F107VC+MR25H40CDF:SPI MRAM掉电数据存储方案

STM32F107VC+MR25H40CDF:SPI MRAM掉电数据存储方案 做了这么多年工业控制器我越来越觉得很多设备的可靠性不是死在算法上而是死在“数据放哪儿”这种不起眼的细节上。以前用EEPROM存运行参数一天写几百次用不了两年就把寿命熬光了换成NOR Flash又得处理块擦除、坏块和掉电丢数据这些破事。直到把 Everspin 的 MR25H40CDF 串行 MRAM 挂到 STM32F107VC 上这个问题才算彻底解决。这颗4Mbit的SPI接口MRAM非易失、写入前不用擦除、几乎无限次读写特别适合工业设备里频繁刷新、掉电不能丢的现场数据。而STM32F107VC是意法半导体互联型Cortex-M3 MCU带以太网、CAN、USB和多个SPI非常适合做工业网关、协议转换器和数据采集终端。这篇文章就从选型思路、硬件接线、软件驱动到工业现场排障完整还原这套组合的落地过程适合正在做嵌入式存储、数据记录或者想给设备加掉电保存功能的工程师参考。1. 为什么这个组合在工业现场这么能打1.1 MR25H40CDF到底是个什么东西MR25H40CDF是Everspin MR25H40系列里的串行MRAM容量4Mbit换算过来是512KB。它走SPI接口外形是DFN8小封装引脚不多硬件上很省事。MRAM的全称是磁阻随机存取存储器核心存储单元用的是磁隧道结数据靠磁性状态保存所以断电之后不会丢这点跟Flash、EEPROM一样但它写入的速度和随意性却跟普通的SRAM差不多。我第一次用MR25H40CDF时最直观的感受是写数据根本不需要等待。之前用EEPROM写一个页要好几个毫秒掉电瞬间想多塞几条日志都来不及。MRAM写完指令和数据CS一拉高数据立刻就是新值没有擦除、没有页写缓冲、没有编程时间。你甚至可以对同一个地址连续写100亿次依旧能正常读出来。这种特性对于运行中需要频繁更新参数的工业设备来说价值极高。1.2 和EEPROM、NOR Flash比工业项目为什么选MRAM很多新接触MRAM的人会问它比EEPROM贵那么多Flash不是也能存数据吗为什么非选它我用一个对比表说清楚。对比维度MRAMMR25H40CDFEEPROMNOR Flash写入前是否需要擦除不需要字节/页直接写必须先擦除块写耐久性约10^12次级别可当RAM用10万~100万次1万~10万次/扇区写入速度SPI时钟级无需等待页写约2~5ms页写加擦除可能近百ms最小写入单位任意地址任意字节单字节/页页/扇区掉电数据保持保持写入即生效保持保持典型场景频繁写参数、日志、掉电保存少量低频配置固件、大块数据从表里能看出来Flash的强项是大容量固件存储EEPROM的强项是超小容量简单配置MRAM则正好补上了中间那块容量不大不小、写入频繁、掉电不能丢失、最好代码还不要写得太复杂。工业设备里的累计运行时间、当前工况参数、最近故障记录、校准值、计量数据几乎都是这种“几K到几百K、一天写几百次”的数据。电路设计上MRAM还有一个隐性好处它没有块擦除写坏了整个Flash扇区再去搬数据的问题根本不存在。软件逻辑里不需要写磨损均衡不需要维护坏块表也不需要为“写到一半掉电导致整页报废”做太多兜底。代价自然是价格比EEPROM高一些但在动辄几十万上百万设备产值和售后成本面前这几块钱的差价根本不值得纠结。1.3 STM32F107VC在此场景的角色STM32F107VC是F107互联型系列里的一款100引脚MCUCortex-M3内核跑到72MHz内置256KB Flash和64KB SRAM。这个芯片最有意思的地方是互联外设非常全以太网MAC、双CAN、USB OTG FS全在里面。也就是说它天生就适合当一个“工业现场数据汇聚和转发”的角色。我用它做主控是因为一个典型的工业网关需要同时干好几件事一路CAN总线收设备数据一路以太网把数据上传到后台还有一路串口给本地调试。这些接口把MCU的通信资源占掉大半之后SPI2仍然空着正好可以挂MR25H40CDF做一个掉电不丢的运行数据仓库。LQFP100封装的引脚数量也够不会为了接一颗存储芯片去挤引脚。如果项目不需要以太网和CAN换成F103系列也行软件SPI部分基本可以原样搬甚至代码量更少。2. 硬件设计要点从接线到板级处理2.1 最小连接与引脚分配MR25H40CDF的SPI接口非常精简CS#、SCK、SI、SO四条信号线加上电源和地剩下WP#和HOLD#两个控制脚。我用STM32F107VC的SPI2连接引脚分配如下。MR25H40CDF引脚功能连接目标CS#片选低有效PB12作为GPIO推挽输出控制SCKSPI时钟PB13SPI2_SCK复用功能SI主机发送到芯片的数据PB15SPI2_MOSI复用功能SO芯片返回主机的数据PB14SPI2_MISO复用功能WP#写保护低有效10kΩ上拉到3.3VHOLD#暂停通信低有效10kΩ上拉到3.3VVCC电源3.3V就近放100nF退耦电容GND地公共地如果你不想用SPI2改用SPI1也是一样的思路对应PA4、PA5、PA6、PA7。我选SPI2主要是为了PCB走线方便让存储芯片离MCU另一边更近避开以太网变压器的干扰区域。NSS引脚我没有让SPI硬件自动控制而是直接用PB12做软件片选。工业项目中软件片选的灵活性更高可以避免硬件NSS在主初始化、DMA传输、总线复位时产生意外脉冲。2.2 这几个信号脚千万别乱接WP#和HOLD#是MR25H40CDF上最容易被忽略的两个脚。WP#拉低会把状态寄存器里的块保护功能打开如果你在代码里又启用了保护位写操作就会被芯片直接拒绝现象就是“写入函数跑完了读回来还是旧数据”。HOLD#拉低会让芯片暂停SPI通信此时SCK和SI上的变化全部无效如果你的HOLD#引脚浮空在某些干扰下很容易被拉出低电平整个存储设备就会间歇性失灵。我习惯的做法是这两个脚各接一个10kΩ电阻上拉到3.3V。即使MCU在复位期间所有GPIO都变成高阻态这两个脚也能保持确定的高电平避免上电瞬间误触发写入保护或者HOLD状态。还有一点要多说一句MRAM芯片在CS#为高的时候SCK和SI上的信号本来不应该影响芯片状态但工业现场干扰复杂让WP#和HOLD#保持确定电平能少很多稀奇古怪的问题。SPI电平方面STM32F107VC的IO是3.3V逻辑MR25H40CDF也是3.3V供电所以不用做电平转换。如果项目里主控是5V单片机那就要加电平转换或者至少用MOSI串联分压电阻的方式做降幅MRAM的SO输出3.3V去接5V MCU的输入通常没问题但SI和SCK从5V打到3.3V器件上就必须处理否则 IO 长期过压时间长了会损坏。2.3 PCB布局与电源处理工业产品的PCB布局我从来不指望靠一根飞线就把SPI跑得又快又顺。SCK、MOSI、MISO这三条线要尽量短不要横穿大功率继电器、开关电源或电机驱动区域。CS#虽然是慢信号但它是决定传输时机的关键信号最好也一样短。如果不得不过孔保证SCK和MISO过孔数量尽量一致避免时钟和数据到达时间差得太多。电源退耦这一点很多人只放一个100nF就结束了。实际MRAM读写瞬间电流虽然不大但当SPI时钟在几兆赫兹切换时电源纹波会直接影响信号质量。我在VCC引脚旁边放了一颗100nF陶瓷电容再在稍远处放10μF的钽电容实测数据线上抖动明显减少。如果设备要经常断电还可以在系统电源输入端加大容量电解电容配合MCU的掉电检测做最后一笔关键数据保存后面软件部分我会细说。3. 软件实现SPI驱动与数据读写落地3.1 SPI模式、时钟极性这些参数怎么定MR25H40CDF支持SPI Mode 0和Mode 3也就是CPOL0、CPHA0或者CPOL1、CPHA1。绝大多数项目用Mode 0就够了这也是SPI设备里默认最常用的模式。Mode 0的特点是SCK空闲为低电平数据在第一个时钟边沿被采样发送方在边沿前把数据准备好。很多新手卡在“乱码”“首字节读出来不对”这类问题上十有八九是CPOL和CPHA配成了Mode 1或Mode 2。另一个重要参数是SPI时钟频率。STM32F107VC里SPI2挂在APB1总线上系统时钟72MHz时APB1是36MHz。我的代码里预分频取8SPI时钟就是4.5MHz。这个速度对MRAM来说非常轻松对普通飞线调试也更宽容。如果你想让数据传输更快可以逐级提高但工业产品我建议最多跑到9MHz左右追求极限速度带来的边际收益远不如牺牲一点抗干扰能力划算。配置代码用标准外设库写出来是这样的核心就是设置主模式、8位数据、MSB先行、软件NSS和Mode 0。void MRAM_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); /* PB12做CS片选信号 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_12); /* PB13 SCK、PB15 MOSI 复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); /* PB14 MISO 输入可以浮空也可以加上拉 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }3.2 读状态、写使能、读写数据的完整时序MR25H40CDF的SPI协议跟普通SPI Flash很像但细节上有一点差别我先说最关键的命令。写数据之前必须先发送写使能命令0x06。这是SPI MRAM的规矩目的是防止总线上误操作改掉存储内容。WREN命令时序很简单CS拉低发送0x06CS拉高。注意CS必须有一个完整的高电平间隔让芯片把写使能锁存住然后才能发起真正的写命令。真正写数据的命令是0x02后面跟24位目标地址再跟要写入的数据字节。读取用0x03后面跟24位地址然后连续读。由于MRAM不是Flash它没有页的概念只要地址递增你可以一条写命令往芯片里连续灌上几百个字节不用像Flash那样担心跨页。地址空间是0x000000到0x07FFFF正好对应4Mbit容量。状态寄存器读取命令是0x05。状态寄存器里有个WEL位写使能后它会置位写完数据后自动清零。调试的时候读一下状态寄存器能快速判断芯片有没有正确接收WREN命令。我一般在写重要数据前会主动读一次状态寄存器确保WEL为1再往下走。3.3 代码实现初始化、写入、读取接下来给一套可以直接抄作业的查询式驱动。查询式代码虽然不如DMA和中断高效但胜在逻辑简单、时序可控工业现场排查问题也方便。#define MRAM_CS_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define MRAM_CS_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_RDSR 0x05 static uint8_t MRAM_SendByte(uint8_t data) { uint8_t rx; while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI2, data); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) RESET); rx SPI_I2S_ReceiveData(SPI2); return rx; } static void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SendByte(MRAM_CMD_WREN); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SendByte(MRAM_CMD_RDSR); status MRAM_SendByte(0x00); MRAM_CS_HIGH(); return status; } void MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SendByte(MRAM_CMD_WRITE); MRAM_SendByte((uint8_t)(addr 16)); MRAM_SendByte((uint8_t)(addr 8)); MRAM_SendByte((uint8_t)(addr 0xFF)); for (i 0; i len; i) { MRAM_SendByte(buf[i]); } MRAM_CS_HIGH(); } void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS_LOW(); MRAM_SendByte(MRAM_CMD_READ); MRAM_SendByte((uint8_t)(addr 16)); MRAM_SendByte((uint8_t)(addr 8)); MRAM_SendByte((uint8_t)(addr 0xFF)); for (i 0; i len; i) { buf[i] MRAM_SendByte(0x00); } MRAM_CS_HIGH(); }这里的MRAM_SendByte是收发一体的因为SPI是全双工主机发数据的同时必须接收数据。读MRAM时主机要不断发送0x00来产生SCK时钟否则MISO上不会吐数据。很多新手把读函数写成了“先发命令然后单纯等RXNE”结果程序卡死或者读回来全是0xFF原因就在这里。如果项目用了HAL库逻辑也完全一样只是把SPI_I2S_SendData换成HAL_SPI_TransmitReceive。我记得HAL在使用完成后RXNE里可能残留尾字节如果下次传输前不清空有时会造成多一个字节错位。稳当的做法是每次传输完毕后手动读一次DR寄存器把残留数据清掉。3.4 把MRAM当“掉电不丢的大RAM”用的几个设计套路把这颗512KB的MRAM用好关键是给它做一个清晰的分区规划。我习惯沿用以前做Flash时的思路但去掉了擦除逻辑后简单得多。第一个区放设备参数比如设备编号、通信地址、标定系数。这些数据平时很少写只有配置变化时才更新。第二个区放累计量比如运行时间、总产量、功耗累计值这类数据要频繁写MRAM随便写不用考虑寿命。第三个区放故障日志做成环形缓冲区从0x40000开始每次写一条固定长度的记录写到末尾就回绕到起始位置继续写。回绕前不需要擦除直接覆盖这就是MRAM最大的优势。实际使用中我会把整个MRAM按256字节为一个对齐块来管理。因为底层的SPI命令虽然能任意长度写但系统里一旦涉及CRC校验和分区状态标记按块管理的方式更不容易乱。每次写一包数据我都在数据末尾追加两个字节的CRC16校验和。读出来的时候先验CRC不通过就报错并从备份区恢复。虽然MRAM寿命很长但工业现场的强电磁干扰、电源跌落、插拔干扰都可能让一个字节出错CRC校验永远是成本最低的保命手段。4. 工业场景可靠性设计与异常排查4.1 掉电、干扰、总线冲突下的处理MRAM是掉电不丢这没错但“掉电不丢”的前提是写入操作在电压降到芯片工作范围之前完成。真到了关键时刻你反而要特别注意掉电瞬间的写入流程。STM32F107VC自带PVD可编程电压检测器可以设置在供电电压掉到某个阈值时触发中断。把阈值设成比主电源正常略低一点的电压例如3.0V甚至2.9V掉电中断触发后立刻停止所有非关键任务SPI总线上如果有其他设备先把它们的片选全部拉高释放总线然后马上把当前最重要的状态数据写入MRAM。因为MRAM写几百字节只需要几十微秒到几百微秒只要电源电容能支撑这几个毫秒这最后一笔数据就能保下来。如果项目对数据可靠性要求极高我还会在硬件上加强在系统的3.3V电源输出端加一个大的储能电容容量根据掉电保存需要的总时间和负载电流估算。假设掉电瞬间需要维持3ms系统总电流为50mA那至少要保证电容储能能够供给约3ms。粗略算下来如果允许电压从3.3V降到2.8V电容值在1000μF左右比较稳但实际还要考虑MCU本身复位电压、LDO压差等因素这个电容宁可大一点也不要临界。总线冲突主要是SPI总线上挂了多个设备时会遇到。MR25H40CDF和另一个SPI Flash共用同一条SPI总线时每次操作之前必须严格选中目标设备操作完后立刻把片选全部拉高。MISO是共享的如果没有选中芯片而它的SO口还处于驱动状态会引起总线竞争。工业级芯片虽然三态做得不错但我还是习惯把片选操作封装成一个互斥函数避免两个任务同时访问SPI导致设备选择错乱。4.2 数据完整性校验、备份、磨损的重新认识经常有人把MRAM宣传成“可以无限写”于是就不做任何保护。我见过一个项目把MRAM当成普通RAM直接用掉电时正在写日志上电后发现日志最后几条变得不对。问题不是MRAM寿命到了而是写入过程中电源波动导致SPI传输被硬生生打断芯片可能只收了一半字节或者状态寄存器里的半写状态干扰了数据完整性。所以即便是MRAM我仍然坚持两个原则第一所有关键数据必须带校验第二重要数据一定要有双份镜像。我的做法是将参数区拆成A区和B区A区写入完成后把一份“该分区有效”的标志也写入固定位置B区镜像相同数据。启动时先读A区CRC通过就用A区不通过就读B区。如果A、B区标志都不对说明系统发生过严重的非正常写入此时进入出厂默认配置并且记录一条初始化诊断信息。这套逻辑在Flash上需要小心翼翼处理擦写磨损在MRAM上就简单多了因为你可以随时重写A区和B区不必担心寿命。再强调一次MRAM不需要做磨损均衡因为它的写耐久性已经远远超过了设备寿命需求。你不需要维护“坏块表”也不需要“写之前检查剩余空间”这些在过去Flash项目里让人头疼的东西用MRAM都可以直接删掉。日志区的环形覆盖写也不怕但它仍然需要你定义好“有效记录”的边界避免旧数据和新数据混在一起。4.3 常见问题速查表这里把我的实测排障经验整理成一个速查表遇到类似问题可以直接照着查。现象最常见原因排查与解决读回来全是0xFFPB14 MISO没配对芯片SO没过来万用表量PB14和MRAM的SO是否导通确认MISO引脚复用配置正确写入后读回仍是旧数据写命令前没发WREN或WP#被拉低检查是否调用MRAM_WriteEnable测量WP#电压是否为高电平首字节总是错的SPI的CPOL/CPHA配置成了Mode 1或2确认配置为CPOL0、CPHA0或用逻辑分析仪对比SCK和SI的相位数据整体错位或乱码MSB/LSB配置反了、发送地址字节顺序不对配置SPI_FirstBit_MSB确认地址发送顺序是高字节在前读函数卡死读数据时没有发送dummy字节产生时钟把收发函数改成MRAM_SendByte(0x00)而不是只读能写一次之后失效HOLD#引脚浮空被干扰芯片进入HOLD状态HOLD#上拉10kΩ测量常态电压是否为高掉电后最后几条记录损坏掉电瞬间没完成写时序或CRC校验缺失加PVD掉电中断加大储能电容数据加CRC镜像备份偶发写错地址的数据CS控制时序不正确或软件片选与其他SPI设备冲突用逻辑分析仪看CS低电平窗口确保操作完成后CS拉高足够时间4.4 实测经验与调优心得最后说说我实际调试这套系统时总结的几条经验。第一刚开始调SPI时别图快先把时钟预分频放到16甚至32等读写完全正常后再逐步提高频率。这样做能快速排除“信号质量”和“参数配置”两类问题避免一上来就陷入波形里。第二调试阶段在电路板上预留MRAM的CS、SCK、MISO三个测试点用逻辑分析仪抓一次完整写时序比在代码里加无数打印管用得多。第三如果你用的是标准外设库写完SPI_Cmd(SPI2, ENABLE)以后第一次读状态寄存器之前最好先读一次SPI_I2S_ReceiveData(SPI2)把DR寄存器清干净。这个问题看起来小但遇到“第一次调用数据多出一位”的怪现象时能省你半天时间。第四工业现场对MRAM的访问频率不需要刻意限制但软件里要保证对同一地址的写操作不会多任务并发。我用一个简单的互斥标志或者关中断几微秒来完成“WREN写数据”整个过程就能有效避免两个任务交叉操作同一个SPI。最近我在一个设备里把所有运行参数和故障记录都放了进去SPI跑4.5MHz连续运行几个月一次数据都没丢。这个方案真正解决了我在开头说的痛点你不用再为EEPROM寿命发愁也不用去给Flash做擦除均衡。MRAM不是万能的保险箱它不会替你扛住电源设计错误和软件逻辑漏洞但它让你从“每次写入都担心磨损”的焦虑里解脱出来。剩下的可靠性靠的是可靠的电源、CRC校验和合理的软件架构。这套驱动逻辑我后来换到另一颗芯片上也只用了一小时就移植完如果你准备在自己的STM32板子上试建议直接复制代码改引脚先跑一遍读写自检再开始做正式应用。
返回列表