ARTICLE DETAIL

资讯详情

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

MRAM工业嵌入式实战:MR25H40CDF与STM32F373RC SPI驱动与掉电保护

MRAM工业嵌入式实战:MR25H40CDF与STM32F373RC SPI驱动与掉电保护 1. 为什么MRAM在工业嵌入式场景里越来越受关注搞过工业数据采集或者电力终端的朋友应该都有体会选存储方案这件事往往比选主控还让人头疼。EEPROM擦写寿命十万次频繁记录日志的话一两年就写废了NOR Flash虽然容量大但写入前得先擦除整个扇区掉电保护做不好就丢数据FRAM倒是快但容量小、价格高大一点的缓冲需求就顶不住。这几年MRAM磁性随机存储器慢慢进入大家的视野尤其是Everspin的MR25H40CDF这颗4Mbit的SPI MRAM在工业圈子里讨论度明显上来了。我最早接触MR25H40CDF是在一个配电终端的项目上主控用的是STM32F373RC。当时的需求很明确每200ms采集一次三相电压电流带时间戳存到非易失存储里断电不能丢而且要能连续存至少一周的数据。算下来一天大概43万条记录每条16字节一天就是将近7MB的写入量。这个量级用EEPROM完全不现实用NOR Flash做磨损均衡又太复杂最后选了MR25H40CDF实测下来确实省心。这篇文章就把我在这颗MRAM和STM32F373RC搭配使用过程中积累的经验完整梳理一遍。从芯片选型逻辑、SPI接口配置、读写时序、掉电保护设计到实际调试中踩过的坑都会讲到。不管你是刚开始接触MRAM还是已经在用但遇到了一些奇怪的问题应该都能找到有用的东西。文章偏实操代码基于STM32 HAL库硬件平台是STM32F373RC但思路对其它STM32系列同样适用。2. 核心器件选型与整体方案设计2.1 MR25H40CDF到底是一颗什么样的存储器MR25H40CDF是Everspin出品的一款4Mbit512K×8bitSPI接口MRAM。它最大的特点是非易失、写入不需要擦除、写入速度接近SRAM、擦写寿命几乎无限。官方数据手册给出的指标是擦写次数超过10的14次方数据保持时间超过20年写入周期时间从片选拉低到数据写入完成典型值只要几十纳秒级别SPI时钟最高支持40MHz。和常见的存储介质对比一下就很清楚了特性EEPROMNOR FlashFRAMMRAM (MR25H40CDF)写入前是否需要擦除否是否否擦写寿命10^610^510^1410^14写入速度慢ms级慢ms级快快ns级容量小大小中等接口I2C/SPISPII2C/SPISPI掉电数据保持是是是是单价低低高中高从表里能看出来MRAM的定位很明确它填补了FRAM容量不够和NOR Flash写入慢、寿命短之间的空白。对于需要频繁写入、数据量中等、又要求非易失的场景MRAM几乎是目前最优解。MR25H40CDF的SPI接口支持模式0和模式3也就是CPOL0/CPHA0或者CPOL1/CPHA1。命令集很简洁主要就是读0x03、写0x02、读状态寄存器0x05、写状态寄存器0x01这几条。没有复杂的扇区擦除命令也不需要等待写完成轮询这是它比Flash用起来舒服的地方。2.2 为什么选STM32F373RC来搭配STM32F373RC是STM32F3系列里比较特殊的一颗带16位ADC、3个12位ADC、2个DAC还有CAN接口主频72MHzCortex-M4内核带FPU。选它做这个项目的主控主要是看中了它的模拟外设和CAN通信能力适合工业现场的数据采集和总线传输。从存储扩展的角度看STM32F373RC有3个SPI接口其中SPI1挂在APB2总线上最高时钟可以到36MHz72MHz的二分频SPI2和SPI3挂在APB1上最高18MHz。MR25H40CDF支持40MHz所以用SPI1的话时钟完全够用甚至还有余量。我用的是SPI1配置为主模式、8位数据帧、MSB先行、软件片选实际跑在18MHz下非常稳定。这里有个细节值得说一下STM32F373RC的SPI1在APB2上如果系统时钟72MHzSPI1的时钟源就是72MHz分频系数最小可以设2得到36MHz。但MR25H40CDF在3.3V供电下40MHz是上限36MHz虽然没超但考虑到PCB走线和信号完整性我实际用的是4分频即18MHz。这个速度对于MRAM来说已经绰绰有余了因为MRAM的瓶颈不在SPI时钟而在命令开销。2.3 整体硬件连接方案硬件连接其实很简单MR25H40CDF是8脚SOIC封装引脚定义如下引脚1CS片选低有效引脚2SO数据输出MISO引脚3WP写保护低有效引脚4VSS地引脚5SI数据输入MOSI引脚6SCK时钟引脚7HOLD保持低有效引脚8VDD电源2.7V~3.6V和STM32F373RC的连接方式MR25H40CDFSTM32F373RC说明CSPA4软件片选也可用SPI1_NSSSOPA6SPI1_MISOSIPA7SPI1_MOSISCKPA5SPI1_SCKWP接VDD不用写保护时拉高HOLD接VDD不用保持功能时拉高VDD3.3V电源VSSGND地WP和HOLD这两个引脚如果不用一定要拉到VDD不能悬空。我见过有人悬空HOLD引脚导致通信时好时坏查了半天以为是时序问题结果就是HOLD被干扰拉低了。另外CS用软件控制还是硬件NSS我建议用软件控制因为MRAM的操作需要精确控制CS的拉低和拉高时机硬件NSS在多主机或者有其它SPI设备时容易出问题。电源部分MR25H40CDF的工作电流在写入时大概十几毫安读取时几毫安待机时微安级。VDD旁边放一个0.1uF的陶瓷电容就够了如果走线比较长再并一个1uF的。PCB布局上MRAM尽量靠近MCUSPI走线等长、远离高频信号线这些常规操作就不多说了。3. SPI接口配置与底层驱动实现3.1 STM32CubeMX里的SPI参数怎么设用CubeMX配置SPI1的时候几个关键参数这样设ModeFull-Duplex MasterHardware NSS SignalDisable用软件片选Data Size8 BitsFirst BitMSB FirstPrescaler4得到18MHzClock PolarityLowClock Phase1 EdgeCRC CalculationDisabledNSS Pulse ModeDisabledTI ModeDisabledCPOL和CPHA的组合就是SPI模式0这是MR25H40CDF支持的两种模式之一。如果你习惯用模式3把CPOL设成High、CPHA设成2 Edge也可以两种模式MRAM都认。但整个系统里最好统一别一会儿模式0一会儿模式3容易乱。GPIO方面PA4设成GPIO_Output初始电平HighCS不选中输出速度设成High或者Very High。PA5/PA6/PA7设成Alternate Function Push-Pull速度也设High。这里有个小经验SPI时钟线SCK的输出速度建议设成High数据线可以设成Medium这样能减少过冲和振铃。如果PCB走线比较短都设High也没问题。3.2 底层读写函数的实现MR25H40CDF的操作逻辑很直接拉低CS发送命令字节发送地址3字节然后读或写数据最后拉高CS。读操作和写操作的命令码分别是0x03和0x02。先定义几个宏和全局变量#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRSR 0x01 #define MRAM_CMD_RDSR 0x05 extern SPI_HandleTypeDef hspi1;写使能函数MRAM每次写操作之前需要发送WREN命令0x06void MRAM_WriteEnable(void) { uint8_t cmd 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); }写数据函数void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] MRAM_CMD_WRITE; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }读数据函数void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] MRAM_CMD_READ; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }读状态寄存器函数主要用来检查WEL位和BP位uint8_t MRAM_ReadStatus(void) { uint8_t cmd MRAM_CMD_RDSR; uint8_t status 0; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; }这几个函数就是最核心的底层驱动了。看起来很简单对吧但实际用的时候有几个地方特别容易出问题我在下一节详细说。3.3 时序上的关键细节MR25H40CDF的SPI时序有几个参数必须注意。CS建立时间tSLCH最小5nsCS保持时间tCHSH最小5ns时钟高电平时间tCH和低电平时间tCL各最小10ns。18MHz下时钟周期是55.6ns高电平低电平各27.8ns都满足要求。但有个地方容易忽略CS拉低到第一个时钟沿之间的时间。MRAM要求CS拉低后至少5ns才能给时钟STM32的HAL_SPI_Transmit函数在调用时CS已经提前拉低了中间有函数调用的开销这个时间远远大于5ns所以没问题。真正需要注意的是连续操作之间的CS高电平时间MRAM要求CS拉高后至少5ns才能再次拉低。如果你在中断里频繁调用读写函数两个操作之间CS高电平时间可能不够。我的做法是在MRAM_CS_HIGH()之后加一个__NOP()或者几个空指令确保时序满足。还有一个坑HAL_SPI_Transmit和HAL_SPI_Receive在传输完成后会等待TXE和RXNE标志这个等待时间在18MHz下对于4字节命令来说大概几微秒。如果你在写操作后面紧接着读状态寄存器中间不需要额外延时因为CS拉高再拉低的时间已经够MRAM完成内部写入了。MRAM的写入是真正的非易失写入不需要像Flash那样等待写完成CS拉高后数据就已经保存了。4. 数据存储方案与掉电保护设计4.1 存储结构怎么规划4Mbit的MRAM地址空间是0x00000到0x7FFFF总共512KB。我把它分成几个区域来用地址范围大小用途0x00000 - 0x00FFF4KB系统配置参数区0x01000 - 0x01FFF4KB设备信息与校准数据0x02000 - 0x7FFFF504KB循环数据记录区配置参数区存的是设备ID、通信参数、采集周期这些写入频率很低但要求绝对可靠。我用了双备份加CRC校验的方式每个参数存两份读的时候对比不一致就用备份恢复。数据记录区采用循环队列的方式。每条记录固定16字节包含时间戳4字节、三相电压各2字节、三相电流各2字节、状态字2字节。504KB除以16字节能存32256条记录。按200ms一条算能存大概1.8小时。等等这个容量不够一周啊。这里就要说到实际项目中的一个取舍了。如果真要存一周要么降低采集频率要么增加外部存储。我最后的方案是MRAM只存最近2小时的高频数据同时通过CAN总线把数据实时传到上位机上位机存历史数据。MRAM的作用是保证最近2小时的数据在断电时不丢这个定位就很合理了。4.2 循环队列的实现循环队列的核心是两个指针写指针和读指针。写指针指向下一个要写入的位置读指针指向最老的一条记录。当写指针追上读指针时读指针向前移动丢弃最老的记录。typedef struct { uint32_t write_ptr; uint32_t read_ptr; uint32_t count; uint32_t magic; } RingBufferHeader; #define RING_START_ADDR 0x02000 #define RING_END_ADDR 0x7FFFF #define RECORD_SIZE 16 #define RING_CAPACITY ((RING_END_ADDR - RING_START_ADDR 1) / RECORD_SIZE)写指针和读指针本身也需要掉电保护。我的做法是把它们存在配置参数区每次写入新记录后更新写指针。但这样每次写记录都要写两次MRAM一次数据一次指针效率减半。优化方案是每写16条记录更新一次指针这样最多丢15条记录对于工业采集来说可以接受。void RingBuffer_Write(uint8_t *record) { uint32_t addr RING_START_ADDR ring_hdr.write_ptr * RECORD_SIZE; MRAM_Write(addr, record, RECORD_SIZE); ring_hdr.write_ptr; if (ring_hdr.write_ptr RING_CAPACITY) { ring_hdr.write_ptr 0; } ring_hdr.count; if (ring_hdr.count RING_CAPACITY) { ring_hdr.count RING_CAPACITY; ring_hdr.read_ptr (ring_hdr.write_ptr 1) % RING_CAPACITY; } if ((ring_hdr.count 0x0F) 0) { SaveRingHeader(); } }4.3 掉电检测与数据保护工业现场掉电是常态掉电保护做不好前面所有设计都白搭。STM32F373RC有PVD可编程电压检测器可以设置一个电压阈值当VDD降到阈值以下时触发中断。我在PVD中断里做紧急保存把当前未保存的缓冲数据写入MRAM更新指针。PVD的配置在HAL库里有现成的函数void PVD_Init(void) { PWR_PVDTypeDef pvd_config; pvd_config.PVDLevel PWR_PVDLEVEL_2V9; pvd_config.Mode PWR_PVD_MODE_IT_RISING_FALLING; HAL_PWR_ConfigPVD(pvd_config); HAL_PWR_EnablePVD(); HAL_NVIC_SetPriority(PVD_IRQn, 0, 0); HAL_NVIC_EnableIRQ(PVD_IRQn); } void PVD_IRQHandler(void) { HAL_PWR_PVD_IRQHandler(); } void HAL_PWR_PVDCallback(void) { if (__HAL_PWR_GET_FLAG(PWR_FLAG_PVDO)) { EmergencySave(); } }PVD阈值设的是2.9V3.3V系统掉到2.9V时电源上电容的储能还能维持几毫秒足够把几十字节的数据写进MRAM。MRAM的写入速度是纳秒级SPI传输18MHz下16字节大概7微秒加上命令开销也就10微秒左右时间非常充裕。这里有个经验PVD中断里不要做复杂操作只做最紧急的数据保存。我见过有人在PVD中断里做文件系统同步结果还没写完电就没了。正确的做法是平时就把数据组织好中断里只做一次MRAM写入。5. 实际调试中遇到的问题与排查方法5.1 读出来的数据全是0xFF这是最常见的问题原因通常有三个CS没拉低、SPI模式不对、MRAM没供电。排查顺序建议这样先量MRAM的VDD引脚确认3.3V正常。然后量CS引脚在读写操作时应该能看到拉低的脉冲。如果CS一直是高检查GPIO配置和代码里的CS控制。如果CS有脉冲但数据不对用逻辑分析仪抓SPI波形看时钟极性、相位是否和MRAM匹配。MR25H40CDF在模式0下时钟空闲低电平数据在上升沿采样。如果抓到的波形是空闲高电平那就是CPOL设错了。还有一个隐蔽的问题WP引脚悬空。WP是写保护引脚低电平有效。如果悬空可能被干扰拉低导致写操作被禁止但读操作正常。表现就是读出来是旧数据或者0xFF写进去没反应。所以WP和HOLD一定要拉高。5.2 写入后立即读取数据不对这个问题我遇到过原因是CS拉高的时间不够。MRAM在CS拉高后才真正完成内部写入如果CS拉高后立刻又拉低进行读操作MRAM可能还没完成写入。解决办法是在写操作和读操作之间加一个短延时或者读状态寄存器确认写入完成。MR25H40CDF的状态寄存器bit0是WEL写使能锁存bit1是BP0bit2是BP1。写入完成后WEL会自动清零。可以轮询WEL位来判断写入是否完成void MRAM_WaitWriteComplete(void) { uint32_t timeout 10000; while ((MRAM_ReadStatus() 0x01) timeout--) { __NOP(); } }不过实测下来18MHz下CS拉高后等1微秒就足够了不需要这么复杂的轮询。我一般是在MRAM_CS_HIGH()后面加一个delay_us(1)简单有效。5.3 长时间运行后数据出错这个问题比较隐蔽通常是电源纹波或者SPI信号完整性问题。MRAM对电源噪声比较敏感如果VDD上有较大的纹波可能导致写入数据错误。建议在MRAM的VDD和GND之间并一个0.1uF和一个1uF的电容位置尽量靠近芯片引脚。SPI信号完整性方面如果SCK频率较高比如36MHz走线又比较长可能出现振铃。用示波器看SCK波形如果有明显的过冲可以在SCK线上串一个22欧姆到33欧姆的电阻。MOSI和MISO也可以串但一般SCK的问题最大。还有一个可能MRAM的地址空间是512KB如果地址计算溢出会回绕到0地址。比如写地址0x7FFFF之后再加1就变成0x80000但MRAM只认低19位实际访问的是0x00000。如果你的循环队列指针没有正确处理边界就会覆盖配置区。这个bug在测试时不容易发现因为只有写满整个MRAM才会触发。5.4 常见问题速查表现象可能原因排查方法解决措施读出全0xFFCS未拉低量CS引脚波形检查GPIO配置读出全0xFFSPI模式错误逻辑分析仪抓波形改为模式0或模式3读出全0xFFWP/HOLD悬空量WP/HOLD电压拉到VDD写入无效未发送WREN抓SPI波形写前加WriteEnable写入后读不对CS高电平时间不足示波器看CS加1us延时偶发数据错误电源纹波大示波器看VDD加去耦电容高速下通信失败信号振铃示波器看SCK串33欧姆电阻地址回绕指针边界错误检查地址计算加边界判断6. 性能实测与优化建议6.1 实际读写速度测试我在18MHz SPI时钟下做了实测。单条记录16字节的写入从拉低CS到拉高CS用示波器测量大概12微秒。其中命令地址4字节传输时间约2.2微秒16字节数据约8.9微秒CS建立和保持时间约1微秒。读取同样16字节也是12微秒左右。如果按200ms采集周期算每次写入12微秒占空比只有0.006%对CPU的占用几乎可以忽略。即使把采集周期缩短到10ms写入时间占比也只有0.12%完全不影响主循环。连续写入时如果不等CS高电平时间直接背靠背操作实测也能工作但我不建议这么做因为已经接近时序极限了。稳妥起见每次操作之间留1微秒的间隔。6.2 优化建议第一个建议是批量写入。如果你有多条记录要写不要一条一条调用MRAM_Write而是攒够一批比如16条一次性写入。MRAM支持页写入一次CS周期内可以连续写多个字节地址会自动递增。这样命令开销从每条记录一次变成每批一次效率提升明显。第二个建议是合理设置SPI时钟。18MHz是个比较稳妥的选择36MHz也能跑但需要更好的PCB布局。如果你的板子走线短、电源干净可以试试36MHz实测也能稳定工作。但工业环境建议留余量18MHz足够了。第三个建议是减少不必要的状态寄存器读取。每次写操作后读状态寄存器会额外增加一次SPI传输如果写入频率高这个开销不小。我的做法是只在初始化时读一次状态寄存器确认WP和BP位之后就不再读了靠CS高电平时间来保证写入完成。6.3 与其它方案的对比体会之前用W25Q64 NOR Flash做过类似的项目对比下来MRAM的优势太明显了。NOR Flash写入前要擦除4KB扇区擦除时间几十毫秒期间如果掉电整个扇区数据都可能丢失。而且擦除次数只有10万次按每天1000次写入算不到一年就写废了。MRAM完全没有这些问题写入就是写入不需要擦除寿命几乎无限。和FRAM比MR25H40CDF的容量优势很大。FRAM常见的是256Kbit或者1Mbit4Mbit的FRAM价格就上去了。MRAM在容量和价格之间找到了一个不错的平衡点。当然MRAM也不是没有缺点。它的容量还是偏小512KB对于需要存大量历史数据的场景不够用。另外MRAM的写入电流比FRAM大对电源的要求稍高。但在工业嵌入式这个领域可靠性和寿命是第一位的MRAM的综合表现确实是最好的。7. 一些实操心得和避坑建议调试MRAM的这段时间有几个心得值得分享。第一个是逻辑分析仪真的必备SPI通信出问题时抓波形是最快的定位方法。看CS、SCK、MOSI、MISO四根线一眼就能看出是模式不对、片选不对还是数据不对。没有逻辑分析仪的话用示波器双通道看CS和SCK也能判断个大概。第二个是初始化时一定要做自检。上电后先往MRAM的某个测试地址写一个已知模式比如0x5A5A再读回来对比。如果不对说明硬件有问题直接报错停机不要带着隐患运行。这个自检花不了几毫秒但能避免很多现场故障。第三个是地址管理要小心。512KB的地址空间19位地址线0x00000到0x7FFFF。在代码里用uint32_t存地址没问题但传给MRAM时要确保只取低19位。我习惯在地址计算后加一个 0x7FFFF的掩码防止溢出。第四个是PVD阈值的选择。2.9V是一个比较通用的值但如果你的系统电源波动比较大可以适当降低到2.7V给紧急保存留更多时间。但也不能太低太低的话电容储能不够还没写完就断电了。具体值要根据你的电源电容和MRAM写入时间来算。最后说一个关于MRAM数据保持的小知识。MRAM的数据保持是基于磁性材料的不需要供电就能保持20年以上。但强磁场环境可能会影响数据如果你的设备用在电机旁边或者有强磁场的场合要注意屏蔽或者远离磁场源。一般工业环境没问题但特别强的磁场下要评估一下。整体来说MR25H40CDF加STM32F373RC这个组合在工业数据采集和存储场景下非常靠谱。SPI接口简单驱动好写掉电保护容易做寿命和可靠性都让人放心。如果你正在选型类似的方案可以放心用这个组合踩坑的地方我都帮你踩过了。
返回列表