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出品的一款4Mbit(512K×8bit)SPI接口MRAM。它最大的特点是非易失、写入不需要擦除、写入速度接近SRAM、擦写寿命几乎无限。官方数据手册给出的指标是:擦写次数超过10的14次方,数据保持时间超过20年,写入周期时间(从片选拉低到数据写入完成)典型值只要几十纳秒级别,SPI时钟最高支持40MHz。
和常见的存储介质对比一下就很清楚了:
| 特性 | EEPROM | NOR Flash | FRAM | MRAM (MR25H40CDF) |
|---|---|---|---|---|
| 写入前是否需要擦除 | 否 | 是 | 否 | 否 |
| 擦写寿命 | 10^6 | 10^5 | 10^14 | 10^14 |
| 写入速度 | 慢(ms级) | 慢(ms级) | 快 | 快(ns级) |
| 容量 | 小 | 大 | 小 | 中等 |
| 接口 | I2C/SPI | SPI | I2C/SPI | SPI |
| 掉电数据保持 | 是 | 是 | 是 | 是 |
| 单价 | 低 | 低 | 高 | 中高 |
从表里能看出来,MRAM的定位很明确:它填补了FRAM容量不够和NOR Flash写入慢、寿命短之间的空白。对于需要频繁写入、数据量中等、又要求非易失的场景,MRAM几乎是目前最优解。
MR25H40CDF的SPI接口支持模式0和模式3,也就是CPOL=0/CPHA=0或者CPOL=1/CPHA=1。命令集很简洁,主要就是读(0x03)、写(0x02)、读状态寄存器(0x05)、写状态寄存器(0x01)这几条。没有复杂的扇区擦除命令,也不需要等待写完成轮询,这是它比Flash用起来舒服的地方。
2.2 为什么选STM32F373RC来搭配
STM32F373RC是STM32F3系列里比较特殊的一颗,带16位ADC、3个12位ADC、2个DAC,还有CAN接口,主频72MHz,Cortex-M4内核带FPU。选它做这个项目的主控,主要是看中了它的模拟外设和CAN通信能力,适合工业现场的数据采集和总线传输。
从存储扩展的角度看,STM32F373RC有3个SPI接口,其中SPI1挂在APB2总线上,最高时钟可以到36MHz(72MHz的二分频),SPI2和SPI3挂在APB1上最高18MHz。MR25H40CDF支持40MHz,所以用SPI1的话时钟完全够用,甚至还有余量。我用的是SPI1,配置为主模式、8位数据帧、MSB先行、软件片选,实际跑在18MHz下非常稳定。
这里有个细节值得说一下:STM32F373RC的SPI1在APB2上,如果系统时钟72MHz,SPI1的时钟源就是72MHz,分频系数最小可以设2,得到36MHz。但MR25H40CDF在3.3V供电下40MHz是上限,36MHz虽然没超,但考虑到PCB走线和信号完整性,我实际用的是4分频即18MHz。这个速度对于MRAM来说已经绰绰有余了,因为MRAM的瓶颈不在SPI时钟,而在命令开销。
2.3 整体硬件连接方案
硬件连接其实很简单,MR25H40CDF是8脚SOIC封装,引脚定义如下:
- 引脚1:CS(片选,低有效)
- 引脚2:SO(数据输出,MISO)
- 引脚3:WP(写保护,低有效)
- 引脚4:VSS(地)
- 引脚5:SI(数据输入,MOSI)
- 引脚6:SCK(时钟)
- 引脚7:HOLD(保持,低有效)
- 引脚8:VDD(电源,2.7V~3.6V)
和STM32F373RC的连接方式:
| MR25H40CDF | STM32F373RC | 说明 |
|---|---|---|
| CS | PA4 | 软件片选,也可用SPI1_NSS |
| SO | PA6 | SPI1_MISO |
| SI | PA7 | SPI1_MOSI |
| SCK | PA5 | SPI1_SCK |
| WP | 接VDD | 不用写保护时拉高 |
| HOLD | 接VDD | 不用保持功能时拉高 |
| VDD | 3.3V | 电源 |
| VSS | GND | 地 |
WP和HOLD这两个引脚如果不用,一定要拉到VDD,不能悬空。我见过有人悬空HOLD引脚导致通信时好时坏,查了半天以为是时序问题,结果就是HOLD被干扰拉低了。另外CS用软件控制还是硬件NSS,我建议用软件控制,因为MRAM的操作需要精确控制CS的拉低和拉高时机,硬件NSS在多主机或者有其它SPI设备时容易出问题。
电源部分,MR25H40CDF的工作电流在写入时大概十几毫安,读取时几毫安,待机时微安级。VDD旁边放一个0.1uF的陶瓷电容就够了,如果走线比较长,再并一个1uF的。PCB布局上,MRAM尽量靠近MCU,SPI走线等长、远离高频信号线,这些常规操作就不多说了。
3. SPI接口配置与底层驱动实现
3.1 STM32CubeMX里的SPI参数怎么设
用CubeMX配置SPI1的时候,几个关键参数这样设:
- Mode:Full-Duplex Master
- Hardware NSS Signal:Disable(用软件片选)
- Data Size:8 Bits
- First Bit:MSB First
- Prescaler:4(得到18MHz)
- Clock Polarity:Low
- Clock Phase:1 Edge
- CRC Calculation:Disabled
- NSS Pulse Mode:Disabled
- TI Mode:Disabled
CPOL和CPHA的组合就是SPI模式0,这是MR25H40CDF支持的两种模式之一。如果你习惯用模式3,把CPOL设成High、CPHA设成2 Edge也可以,两种模式MRAM都认。但整个系统里最好统一,别一会儿模式0一会儿模式3,容易乱。
GPIO方面,PA4设成GPIO_Output,初始电平High(CS不选中),输出速度设成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命令(0x06):
void 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)最小5ns,CS保持时间(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 - 0x00FFF | 4KB | 系统配置参数区 |
| 0x01000 - 0x01FFF | 4KB | 设备信息与校准数据 |
| 0x02000 - 0x7FFFF | 504KB | 循环数据记录区 |
配置参数区存的是设备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.9V,3.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是BP0,bit2是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 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读出全0xFF | CS未拉低 | 量CS引脚波形 | 检查GPIO配置 |
| 读出全0xFF | SPI模式错误 | 逻辑分析仪抓波形 | 改为模式0或模式3 |
| 读出全0xFF | WP/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或者1Mbit,4Mbit的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接口简单,驱动好写,掉电保护容易做,寿命和可靠性都让人放心。如果你正在选型类似的方案,可以放心用这个组合,踩坑的地方我都帮你踩过了。