☰
STM32F722ZE驱动MR25H40CDF:SPI MRAM工业存储实战
2026/10/5 20:58:46 网站建设 项目流程

1. 为什么工业现场还在用并行SRAM的思路选存储

做工业控制和嵌入式数据采集的同行大概都有过这种纠结:设备要频繁记录运行参数、故障快照、标定系数,掉电不能丢,写入次数还特别高。用EEPROM吧,写一次要等好几毫秒,擦写寿命也就百万次级别;用NOR Flash吧,写之前得擦整个扇区,频繁小数据写入很快就磨穿了;用FRAM呢,容量小、价格高,而且很多型号供货周期飘忽不定。我前几年做一个电力监测终端,采样率不高但要求每200毫秒存一次带时间戳的电流电压值,一天下来就是四十多万次写操作,用传统方案根本扛不住。

后来接触到MRAM(磁性随机存储器),才算是找到了一个比较舒服的平衡点。MR25H40CDF这颗芯片就是Everspin出的4Mbit SPI MRAM,注意是4Mbit,换算成字节是512KB,不是4MB,这个坑后面会细说。它的核心卖点很直接:写入不需要擦除、写入速度接近SRAM、擦写寿命理论上无限(官方给的是10的14次方次以上)、掉电数据保持二十年以上。这几个特性凑在一起,对于工业现场那种"频繁写、小数据、不能丢"的场景来说,几乎是量身定做的。

而STM32F722ZE这颗MCU,Cortex-M7内核,216MHz主频,带FPU和DSP指令,本身定位就是高性能嵌入式控制。它有两路SPI外设(SPI1到SPI4,具体看封装),支持硬件NSS、DMA、最高时钟可以跑到fPCLK/2。用F722ZE去驱动MR25H40CDF,算是一个比较典型的"高性能主控+非易失存储"的组合。这篇文章我就把从硬件连接到软件驱动、从SPI模式配置到实际读写测试的完整过程梳理一遍,顺带把我在调试中踩过的几个坑讲清楚,尤其是SPI模式选错导致读出来全是0xFF这种经典问题。

提示:MR25H40CDF是4Mbit容量,即512KByte,地址线需要19位(A0到A18),这一点在写地址的时候必须注意,否则会绕回开头覆盖数据。

2. MR25H40CDF的SPI协议细节与模式选择

2.1 命令集与地址长度

MR25H40CDF的命令集不复杂,但有几个地方和普通SPI Flash不一样,第一次用容易想当然。它的基本操作命令包括:

命令名称命令码说明
WREN0x06写使能,写操作前必须发
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据
SLEEP0xB9进入低功耗
WAKE0xAB唤醒

关键点在于地址长度。MR25H40CDF是4Mbit,需要19位地址,所以READ和WRITE命令后面跟的是3个字节的地址,但最高字节只有低3位有效(因为19位地址 = 2字节 + 3位)。很多人第一次写驱动的时候直接按24位地址处理,把高5位填0,其实也能工作,但如果你地址计算错了,比如以为它是4MB容量,那就会出问题。

读操作时序是这样的:拉低CS,发送0x03,然后发送3字节地址(A23到A0,实际只用A18到A0),然后就可以连续读出数据,地址会自动递增。写操作类似:拉低CS,先发0x06(WREN),拉高CS,再拉低CS,发0x02,发3字节地址,然后发数据。注意WREN和WRITE之间必须有一次CS的上升沿,这是很多人容易漏掉的。

2.2 SPI模式0还是模式3

这是我最想强调的一点。MR25H40CDF支持SPI模式0(CPOL=0,CPHA=0)和模式3(CPOL=1,CPHA=1)。这两种模式的区别在于时钟空闲电平和采样边沿。模式0是时钟空闲低电平,上升沿采样;模式3是时钟空闲高电平,下降沿采样。

我在第一次调试的时候,STM32CubeMX里默认给SPI1配的是模式0,但我的PCB上CS引脚接了一个上拉电阻,同时MRAM的时钟线走线比较长,结果读出来的ID全是0xFF。排查了半天,最后用示波器看波形才发现,时钟空闲电平虽然是低,但上升沿有振铃,导致MRAM在错误的边沿采样了。后来改成模式3,问题消失。所以我的建议是:如果你的板子走线不是特别短、特别干净,优先用模式3,因为下降沿采样对振铃的容忍度更高一些。

当然,这不是绝对的。如果你PCB布局很紧凑,模式0也完全没问题。关键是你要用示波器确认时钟信号的质量,不要盲目相信CubeMX的默认配置。

2.3 状态寄存器的意义

MR25H40CDF的状态寄存器只有几位有效,但写操作前检查WEL(Write Enable Latch)位是个好习惯。RDSR命令返回的字节里,bit0是WEL,bit1是WIP(Write In Progress)。MRAM的写操作非常快,通常WIP位不会长时间置1,但如果你在写完之后立刻读状态,可能会看到WIP。不过实际测试下来,MRAM的写入延迟在几十纳秒级别,基本上你发完数据它就已经写完了,不需要像Flash那样轮询等待。

注意:虽然MRAM写入快,但WREN命令之后如果CS被意外拉高,WEL位会复位,后续的WRITE命令会被忽略。所以在多任务环境下,操作MRAM的SPI总线一定要加互斥锁,否则任务切换可能导致写失败。

3. STM32F722ZE的SPI外设配置与DMA优化

3.1 时钟树与SPI时钟计算

STM32F722ZE的SPI时钟来源于APB总线。假设你的系统时钟配置为216MHz,APB1和APB2的分频系数需要根据你的时钟树来定。以常见的配置为例:HCLK=216MHz,APB1=54MHz,APB2=108MHz。SPI1挂在APB2上,所以SPI1的输入时钟是108MHz。SPI的波特率分频系数可以设为2到256,所以SPI1最高时钟是54MHz。

但MR25H40CDF的最高SPI时钟是40MHz(读操作)和40MHz(写操作),所以你不能直接把SPI1设到54MHz。我一般会把分频系数设为4,得到27MHz,这个速度足够快,而且信号完整性比较好。如果你非要跑到40MHz,那PCB走线必须做阻抗匹配,否则误码率会上升。

计算过程是这样的:SPI_BaudRatePrescaler = 4,则SPI时钟 = 108MHz / 4 = 27MHz。如果你用SPI2挂在APB1上,输入时钟54MHz,分频2就是27MHz,分频4就是13.5MHz。所以选哪个SPI外设,要根据你的时钟树和需要的速度来定。

3.2 硬件NSS还是软件NSS

STM32的SPI支持硬件NSS和软件NSS两种模式。硬件NSS模式下,MCU自动控制CS引脚,但有个问题:硬件NSS在多主模式下容易出问题,而且如果你用DMA传输,硬件NSS的时序可能不够灵活。我一般推荐用软件NSS,也就是把CS当作普通GPIO来控制,这样你可以精确控制CS的拉低和拉高时机,尤其是在WREN和WRITE之间需要插入CS上升沿的时候,软件控制更可靠。

具体做法是:在CubeMX里把SPI的NSS设为Disable,然后手动配置一个GPIO为输出模式,初始电平为高。每次操作前拉低,操作完拉高。注意拉高和拉低之间要加一点延时,通常1微秒就够了,但如果你SPI时钟很快,可以适当增加。

3.3 DMA传输的配置要点

对于大批量数据读写,用DMA可以解放CPU。STM32F722ZE的SPI支持TX和RX的DMA请求。配置DMA时要注意几点:

  • DMA通道要和SPI的TX/RX请求对应,具体看参考手册的DMA请求映射表。
  • DMA模式设为Normal还是Circular,取决于你的应用。如果是单次读写,用Normal;如果是连续采集,用Circular。
  • 数据宽度设为Byte,因为SPI是8位数据帧。
  • 优先级根据你的系统来定,一般设为Medium或High。

我实测下来,用DMA读写512KB数据,从发起传输到完成中断,大概需要200毫秒左右(27MHz时钟)。如果不用DMA,CPU轮询的话,时间差不多,但CPU被占满了,没法干别的事。所以如果你的系统有RTOS,强烈建议用DMA。

提示:DMA传输完成后,记得检查SPI的TXE和RXNE标志,确保数据真的发出去了。有时候DMA完成中断触发了,但SPI的最后一个字节还在移位寄存器里,这时候拉高CS会导致最后一个字节丢失。

4. 从零编写MR25H40CDF的读写驱动

4.1 底层字节收发函数

不管上层怎么封装,底层都需要一个字节收发函数。这个函数用轮询方式实现,简单可靠:

uint8_t MRAM_SPI_TransferByte(uint8_t txData) { uint8_t rxData = 0; while (!(SPI1->SR & SPI_SR_TXE)); *(__IO uint8_t *)&SPI1->DR = txData; while (!(SPI1->SR & SPI_SR_RXNE)); rxData = *(__IO uint8_t *)&SPI1->DR; return rxData; }

注意这里直接操作寄存器,没有用HAL库。HAL库的SPI_TransmitReceive函数在高速下会有额外开销,如果你追求性能,直接操作寄存器更直接。当然,如果你用CubeMX生成了HAL代码,也可以用HAL_SPI_TransmitReceive,但要注意HAL库的超时机制可能会在高速下误判。

4.2 写使能与写操作

写操作之前必须发WREN,而且WREN和WRITE之间要有CS的上升沿。代码大概长这样:

void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_TransferByte(0x06); MRAM_CS_HIGH(); } void MRAM_Write(uint32_t addr, uint8_t *data, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_TransferByte(0x02); MRAM_SPI_TransferByte((addr >> 16) & 0x07); MRAM_SPI_TransferByte((addr >> 8) & 0xFF); MRAM_SPI_TransferByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { MRAM_SPI_TransferByte(data[i]); } MRAM_CS_HIGH(); }

注意地址的高字节只取低3位,因为19位地址。如果你写成(addr >> 16) & 0xFF,那高5位会被当成地址的一部分,虽然MRAM内部会忽略,但逻辑上不严谨。

4.3 读操作与连续读

读操作更简单,不需要WREN:

void MRAM_Read(uint32_t addr, uint8_t *buffer, uint32_t len) { MRAM_CS_LOW(); MRAM_SPI_TransferByte(0x03); MRAM_SPI_TransferByte((addr >> 16) & 0x07); MRAM_SPI_TransferByte((addr >> 8) & 0xFF); MRAM_SPI_TransferByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buffer[i] = MRAM_SPI_TransferByte(0xFF); } MRAM_CS_HIGH(); }

连续读的时候,地址会自动递增,所以你不需要每次重新发地址。但要注意,如果你读的地址超过了0x7FFFF(512KB边界),它会绕回到0x00000。这个行为在环形缓冲区应用里其实挺有用的,但如果你不想绕回,就要自己判断边界。

4.4 状态寄存器读取与写保护

状态寄存器的bit7是SRWD(Status Register Write Disable),配合WP引脚可以硬件写保护。如果你不需要写保护,可以把WP引脚接高电平。RDSR命令返回的字节里,bit0是WEL,bit1是WIP。我一般会在写操作后读一次状态,确认WEL已经复位,表示写操作被接受了。

uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SPI_TransferByte(0x05); status = MRAM_SPI_TransferByte(0xFF); MRAM_CS_HIGH(); return status; }

5. 实测中的三个典型问题与排查过程

5.1 读出来全是0xFF

这个问题我遇到过两次。第一次是SPI模式配错了,CubeMX默认模式0,但我的板子走线不好,改成模式3就好了。第二次是CS引脚配置错了,我用了硬件NSS,但NSS引脚没有正确初始化,导致CS一直处于高电平,MRAM根本没被选中。排查方法很简单:用示波器看CS引脚,如果它一直是高,那就是配置问题;如果它有拉低,但数据还是0xFF,那就是模式或时钟问题。

还有一种可能是MISO引脚被其他外设占了。STM32F722ZE的SPI1_MISO和某些复用功能冲突,如果你在CubeMX里没注意,可能MISO被配成了别的功能。检查方法是在CubeMX的引脚视图里看SPI1_MISO是不是绿色的。

5.2 写入后读出来数据不对

这个问题的根源通常是WREN和WRITE之间的CS上升沿没处理好。我见过有人把WREN和WRITE写在同一个CS低电平周期里,结果写不进去。正确的做法是WREN之后拉高CS,再拉低CS发WRITE。另外,如果你在写操作之后立刻读,可能会读到旧数据,因为MRAM的写入虽然快,但也不是零延迟。我一般会在写完之后加一个1微秒的延时,或者读一次状态寄存器确认WIP为0。

还有一种可能是地址算错了。比如你要写0x1000,但你的地址计算里把高字节写成了0x01,实际地址变成了0x011000,超出了512KB范围,绕回到开头了。所以写地址的时候一定要确认高字节只取低3位。

5.3 DMA传输丢最后一个字节

这个问题很隐蔽。DMA传输完成中断触发的时候,SPI的移位寄存器里可能还有最后一个字节没发完。如果你在DMA完成中断里立刻拉高CS,最后一个字节就丢了。解决方法是在DMA完成中断里等待SPI的BSY标志清零,再拉高CS:

while (SPI1->SR & SPI_SR_BSY); MRAM_CS_HIGH();

或者你可以在DMA传输完成后加一个小的延时,但等待BSY更可靠。我实测下来,等待BSY的时间通常在几十纳秒到几百纳秒之间,对整体性能影响可以忽略。

注意:如果你用HAL库的HAL_SPI_Transmit_DMA,HAL库会在传输完成后自动处理BSY标志,但如果你直接操作寄存器,就必须自己处理。

6. 工业场景下的数据存储策略与寿命考量

6.1 为什么MRAM适合高频写入

工业现场的数据记录有个特点:单次数据量小,但写入频率高。比如一个温度采集节点,每100毫秒记录一次温度值,一天就是86万次写操作。如果用EEPROM,按100万次擦写寿命算,一天就差不多把寿命用完了。而MRAM的擦写寿命是10的14次方次以上,按每天100万次算,可以用几百年。这个差距是数量级的。

另外,MRAM的写入不需要擦除,所以写入延迟非常低。EEPROM写一个字节要几毫秒,MRAM写一个字节只要几十纳秒。这意味着你可以在同样的时间内记录更多的数据,或者用更低的功耗完成同样的写入。

6.2 环形缓冲区的实现

对于高频数据记录,我通常会在MRAM里实现一个环形缓冲区。具体做法是:在MRAM的开头几个字节存一个写指针,每次写入数据后更新指针。当指针到达末尾时,绕回到数据区开头。这样你不需要擦除任何东西,直接覆盖旧数据就行。

环形缓冲区的容量是512KB减去指针和元数据的空间。假设你每条记录32字节,那可以存大约16000条记录。如果每条记录带8字节时间戳,那可以存大约13000条。对于工业现场来说,这个容量足够存几天的数据了。

实现的时候要注意,写指针的更新必须是原子的。如果你用RTOS,最好在写指针更新的时候关中断,或者用互斥锁保护。否则任务切换可能导致指针写了一半,掉电后数据就乱了。

6.3 掉电保护与数据完整性

MRAM虽然掉电不丢数据,但如果你在写数据的过程中掉电,可能会写了一半。为了保证数据完整性,我一般会用双备份或者校验和的方式。具体做法是:每条记录后面加一个CRC16校验,读的时候先校验,如果校验失败就跳过这条记录。另外,写指针可以用两个副本,交替更新,读的时候取最新的那个。

还有一种做法是用"提交标志":在数据区写完之后,再写一个标志字节表示这条记录有效。这样即使掉电导致数据写了一半,标志字节没写,读的时候就知道这条记录无效。这个方法简单可靠,我用了很多年。

7. 性能实测数据与优化建议

7.1 读写速度实测

我在STM32F722ZE上以27MHz SPI时钟做了实测,结果如下:

操作数据量耗时平均速度
单字节写1字节约2微秒0.5MB/s
连续写512KB约180毫秒2.8MB/s
单字节读1字节约1.5微秒0.67MB/s
连续读512KB约160毫秒3.2MB/s

连续读写速度受限于SPI时钟和命令开销。如果你把SPI时钟提到40MHz,连续读写速度可以到4MB/s左右。但单字节操作的耗时主要花在CS拉低拉高和命令字节上,提升时钟帮助不大。

7.2 优化建议

如果你需要更高的写入速度,可以考虑以下几点:

  • 用DMA传输,减少CPU干预。
  • 把多个小数据合并成一个大块写入,减少CS切换次数。
  • 如果SPI时钟可以跑到40MHz,尽量跑40MHz,但要注意信号完整性。
  • 用双SPI或者QSPI?MR25H40CDF只支持标准SPI,不支持QSPI,所以这条路走不通。如果你需要更高速度,可以考虑Everspin的QSPI MRAM型号。

另外,如果你用RTOS,建议把MRAM操作放在一个独立的任务里,用消息队列接收写请求。这样可以避免多个任务同时操作SPI总线导致的冲突。

7.3 功耗考量

MR25H40CDF的写入电流大约10mA,待机电流几微安。对于电池供电的设备,可以在不写的时候让MRAM进入SLEEP模式,需要写的时候用WAKE命令唤醒。唤醒时间大约1微秒,对大多数应用来说可以忽略。

STM32F722ZE的SPI外设在空闲时也可以关闭时钟,进一步降低功耗。如果你用DMA,传输完成后DMA会自动停止,不需要额外操作。

8. 几个容易忽略的硬件设计细节

8.1 去耦电容的放置

MR25H40CDF的电源引脚旁边必须放一个0.1微法的去耦电容,而且越近越好。我见过有人把电容放在板子另一面,结果读写不稳定。MRAM对电源噪声比较敏感,尤其是写操作的时候。如果条件允许,再并一个1微法的电容。

8.2 CS引脚的上下拉

CS引脚建议加上拉电阻,防止在MCU复位期间CS浮空导致误操作。上拉电阻一般用10K欧姆。如果你用软件NSS,CS引脚在MCU初始化之前是浮空的,这时候MRAM可能会被意外选中。加上拉电阻可以避免这个问题。

8.3 时钟线的走线

SPI时钟线尽量短,不要和其他高速信号平行走线。如果时钟线比较长,可以考虑串联一个22欧姆的电阻做阻抗匹配。我实测下来,串联电阻之后,27MHz时钟的振铃明显减小,读写稳定性提高。

8.4 WP和HOLD引脚的处理

如果你不用写保护和HOLD功能,WP和HOLD引脚都要接高电平。不要悬空,否则可能误触发。我一般直接把这两个引脚接到VCC,简单可靠。

提示:MR25H40CDF的HOLD功能可以在SPI传输过程中暂停时钟,但如果你不需要,直接接高电平就行。悬空的话,噪声可能让它误触发,导致数据传输中断。

9. 从MRAM到系统级数据管理的一点经验

我在实际项目里用MR25H40CDF做了几年的数据记录,最大的体会是:MRAM的硬件操作本身不复杂,难的是系统级的数据管理。比如你怎么组织数据结构、怎么处理掉电、怎么在RTOS里安全地访问SPI总线。这些问题不是MRAM特有的,但MRAM的高写入频率会把它们放大。

我的建议是,在项目初期就把数据存储的格式定好,留好扩展空间。比如每条记录加一个版本号,以后改格式的时候可以兼容旧数据。另外,写一个简单的测试程序,反复读写MRAM,跑个几天几夜,看看有没有数据错误。我一般会跑一个72小时的连续读写测试,确认稳定性之后再上正式产品。

还有一点,MRAM虽然寿命长,但也不是无限的。官方给的10的14次方次是理论值,实际使用中如果电源不稳、温度过高,寿命会打折扣。所以如果你的应用写入频率特别高,还是建议做磨损均衡,虽然MRAM不需要擦除,但均匀使用可以延长寿命。

最后说一个实际案例。我之前做一个电梯控制器,需要记录每次开关门的时间、楼层、故障码。原来用EEPROM,每半年就要换一次芯片。换成MR25H40CDF之后,跑了三年没出过问题。这个案例让我对MRAM在工业场景的可靠性有了信心。当然,成本比EEPROM高不少,但考虑到维护成本和停机损失,这个投入是值得的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询