拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

MR25H40CDF与STM32F469II的SPI接口MRAM驱动设计与掉电保护实现

MR25H40CDF与STM32F469II的SPI接口MRAM驱动设计与掉电保护实现

如果你搞工业控制或者嵌入式数据存储,大概率遇到过这种场景:程序里小心翼翼维护着EEPROM的磨损均衡,或者眼巴巴等着Flash页擦除的那几百毫秒,结果设备一断电,关键参数还是丢了。我从去年开始把一批产品里的数据存储芯片换成MR25H40CDF,搭配STM32F469II做主控,既解决了掉电不丢失的存储要求,又满足了工业级温度范围和高速读写的场景。这篇文章把这一套组合从选型、硬件到驱动的完整思路整理出来,希望能给正在嵌入式系统里做数据存储的朋友提供一点参考。

1. 项目背景与方案选型思考

1.1 为什么工业嵌入式场景最终选了MRAM

以前做参数存储,第一反应都是25系列SPI Flash或者I2C EEPROM,毕竟价格便宜、资料多、大家都会用。但真正放到工业设备里,这两个东西的毛病其实不少。

EEPROM的容量普遍偏小,写一次还要等5毫秒左右,如果日志记录频繁,擦写寿命很容易被耗掉。SPI Flash容量倒是大,可是它有页写、扇区擦除这些限制,写一个字节往往要先把整个扇区读出来、改完再写回去,一旦写了一半掉电,数据一致性就很难保证。更麻烦的是Flash擦除本身就有寿命上限,工业设备动不动要跑十年八年,日志和参数写得勤一点,后期坏块和磨损问题会让人非常头疼。

MRAM的原理跟前面两个完全不是一个路子。它用磁隧道结存储数据,写数据是改变磁性状态而不是存电荷,所以不需要擦除、没有页限制、写入速度极快,而且理论上可以无限次写入。断电后磁化方向保持不变,数据也就自然保住了。实际体验下来,它就是一颗像SRAM一样随便写、同时掉电不丢数据、还带SPI接口的芯片,这对嵌入式工程师来说实在是太友好了。

1.2 MR25H40CDF这颗料好在哪

MR25H40CDF是Everspin的4Mb串行MRAM。刚开始选型的时候我其实也犹豫过FRAM,毕竟铁电存储也是无磨损方案。但对比下来MR25H40CDF在几个维度上更适合我的项目。

首先看容量:4Mb换算下来是512KB,做配置参数、运行日志、故障记录都够用。如果只是存几个关键变量,512KB甚至显得有点奢侈,但正因为容量够大,我可以直接把日志做成环形缓冲,省掉很多存储管理上的麻烦。

其次是接口:它走标准SPI协议,指令集和常见的25系列Flash大部分兼容,像WREN、READ、WRITE这些指令都可以直接用。这意味着代码迁移成本很低,硬件上随便找一组SPI就能接。

加上它支持40MHz左右的SPI时钟,数据吞吐远高于EEPROM,工业级温度范围对于现场环境来说也足够了。数据手册里标称的数据保持时间很长,在常温下可以做到二十年以上,这个参数在工业设备招标和项目验收的时候是有说服力的。

1.3 STM32F469II的角色与优势

主控选STM32F469II,说实话不是因为它是市面上最强的,而是它的资源对这类存储应用来说非常顺手。

STM32F469II的CPU是Cortex-M4F,主频180MHz,带有FPU和DSP指令,跑数据校验、CRC计算、日志解析这些任务绰绰有余。它内部有2MB Flash和384KB SRAM,应用程序和运行缓冲区都宽裕,不会因为内存不足去压缩存储逻辑。

更重要的是外设配置:STM32F469II同时提供了多路SPI、DMA控制器、以及带Cache的Cortex-M4内核。SPI1挂在APB2总线上,时钟能跑到90MHz,分频后的实际SPI频率可以稳定覆盖MRAM的需求;而DMA能够把SPI收发完全交给硬件,CPU只需要在事务开始和结束的时候介入,这在大批量读写日志时非常香。

我选的这块芯片本身还带了TFT-LCD控制器和SDRAM接口,后续产品如果要加显示界面或者更大的数据缓冲,就不用换主控了,扩展性比较好。这也是当初没有选F103或者F407的原因,不想为了存储方案把未来的人机交互升级空间堵死。

2. 硬件连接与初始化细节

2.1 最小系统连接方案

MR25H40CDF接STM32F469II,我用的SPI1,因为PA4到PA7这组引脚比较好布线,而且SPI1挂在APB2上时钟更高。下面是我的接线参考:

信号STM32F469II引脚MR25H40CDF信号说明
CS#PA4(GPIO输出)CS#软件片选,低电平有效
SCKPA5(SPI1_SCK)SCKSPI时钟
MISOPA6(SPI1_MISO)DO芯片数据输出
MOSIPA7(SPI1_MOSI)SI芯片数据输入
WP#接3.3VWP#硬件写保护,拉高禁止保护
HOLD#接3.3VHOLD#拉高禁用暂停功能
VCC3.3VVDD电源
GNDGNDGND地

这里要重点提醒一句:CS片选一定用普通GPIO软件控制,不要用STM32的硬件NSS。原因很简单,硬件NSS在多字节连续事务里容易在时序上出幺蛾子,尤其是后面要跟DMA配合做大块读写,软件GPIO控制CS更可控、更稳。

2.2 硬件设计里的三个常见坑

第一个坑是HOLD#和WP#悬空。这两个引脚千万不要悬空,MR25H40CDF的HOLD#一旦被噪声拉低,芯片会暂停当前SPI通信,表现就是数据卡死或者读写的字节错位。手一抖直接悬空,调试的时候能查到人崩溃。正规做法是把两个引脚都通过10kΩ电阻上拉到3.3V。

第二个坑是去耦电容。MRAM写操作本质是改变磁矩,虽然芯片内部处理得很好,但外部电源突变依然可能影响信号完整性。我在每颗MRAM的VCC脚旁边放了0.1µF陶瓷电容,并且在电源入口处并联4.7µF钽电容,实测对SPI信号质量和稳定性有明显改善。

第三个坑是MCU和MRAM供电不是同一个源。如果STM32F469II的IO电平是3.3V而MRAM供电掉了,SPI引脚可能会通过芯片内部二极管倒灌电流,轻则逻辑混乱,重则损伤器件。所以我的设计里用了同一个3.3V电源域,确保上电掉电时序一致。

2.3 驱动初始化与状态寄存器确认

初始化主要分成三步:先初始化SPI外设,再初始化CS引脚为GPIO输出,最后读取MRAM的状态寄存器确认芯片在线。

SPI1的初始化参数如下,我使用的是HAL库:

// APB2 = 90MHz,Prescaler = 4,实际SPI时钟 = 22.5MHz hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);

注意:SPI1挂在APB2上,最大可以分频到45MHz,但MR25H40CDF的数据手册最高支持40MHz,所以不要图快直接上分频2,稳妥起见用分频4,跑22.5MHz,余量更足。

上电后读取状态寄存器的方式是发送0x05指令,然后接收一个字节:

uint8_t read_status(void) { uint8_t cmd = 0x05; // RDSR uint8_t stat = 0; SPI_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); HAL_SPI_Receive(&hspi1, &stat, 1, 10); SPI_CS_HIGH(); return stat; }

上电时若读到的数据是随机值,不要慌。MRAM与Flash不一样,Flash上电默认全0xFF,而MRAM的内容是未知的,必须由软件做格式化处理,这个细节在后面的数据可靠性设计中会详细说。

3. 核心驱动实现与读写流程剖析

3.1 指令集速览与SPI模式选择

MR25H40CDF的指令集跟25系列Flash非常接近,基础操作表如下:

指令命令码功能说明
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
FAST_READ0x0B快速读
WRITE0x02写数据
SLEEP0xB9进入睡眠模式
WAKE0xAB唤醒

MR25H40CDF支持SPI Mode 0和Mode 3,也就是CPOL low/CPHA 1Edge,或者CPOL high/CPHA 2Edge。驱动初始化我选了Mode 0,也就是CPOL=0、CPHA=0,跟大多数SPI器件的默认习惯一致,示波器抓起来也直观。

需要特别强调:MRAM写数据时无需先擦除,没有任何块擦除、扇区擦除指令。地址边界上连续写会回绕到起始地址,所以驱动里要注意不要把数据写到跨越容量末尾的位置。

3.2 读操作实现与解释

读操作其实很简单,发送READ指令,跟3字节地址,然后持续接收数据。MR25H40CDF虽然是4Mb容量,需要19位地址,但SPI协议里仍然用完整的24位地址格式,高5位直接置0。

核心代码如下:

int mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; if (addr + len > MRAM_SIZE) { return -1; } cmd[0] = 0x03; // READ cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; SPI_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, buf, len, 1000); SPI_CS_HIGH(); return 0; }

这里有个细节:读操作期间CS必须全程拉低,直到最后一个数据字节接收完成。如果CS中途拉高,芯片会立刻终止这次读操作,剩下的数据全是错的。HAL库的Transmit和Receive虽然是两次调用,但只要CS在两次调用之间保持低电平,底层的SPI引擎是连续工作的,完全没有问题。

3.3 写操作实现与解释

写操作比读操作多一个写使能步骤。标准流程是先发WREN置位WEL位,然后发WRITE指令、地址和数据。

数据手册里要求每次写操作前都要发WREN,驱动里最保险的做法就是每次写事务前发一次,别偷懒。完整代码如下:

void write_enable(void) { uint8_t cmd = 0x06; // WREN SPI_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 10); SPI_CS_HIGH(); } int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t cmd[4]; uint8_t stat; if (addr + len > MRAM_SIZE) { return -1; } write_enable(); stat = read_status(); if (!(stat & 0x02)) { // WEL 位未置位 return -2; } cmd[0] = 0x02; // WRITE cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; SPI_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, len, 1000); SPI_CS_HIGH(); // 可选:回读状态确认写入完成 stat = read_status(); if (stat & 0x01) { return -3; // WIP 还在忙,按MRAM特性一般不会发生 } return 0; }

跟Flash最大的区别是:Flash写完一页要等几十毫秒,MRAM写完发完CS拉起就算完成了,写命令本身没有页编程时间。这也是为什么MRAM适合做高频小数据写入,比如实时状态记录、批次计数这样的场景。

3.4 用DMA做大块数据搬运

如果只是读写几十个字节,CPU等SPI完全没问题。但日志读取或者固件参数备份经常一次操作几KB,如果还用CPU一个字节一个字节地喂SPI,会浪费大量CPU时间。这时候要上DMA。

HAL库直接支持SPI的DMA收发,配置起来也不复杂。以DMA接收为例,先把SPI接收DMA和发送DMA初始化好,然后调用:

uint8_t tx_cmd[4]; uint8_t rx_buf[1024]; SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, sizeof(rx_buf)); SPI_CS_LOW(); HAL_SPI_Transmit(&hspi1, tx_cmd, 4, 1000); HAL_SPI_Receive_DMA(&hspi1, rx_buf, sizeof(rx_buf)); HAL_SPI_DMAPause(&hspi1); // 等待接收结束 SPI_CS_HIGH();

注意:STM32F469II带D-Cache,DMA把数据写进内存后,CPU读到的可能是Cache里的旧数据。所以在DMA接收之前必须先调用SCB_InvalidateDCache_by_Addr把对应缓冲区的Cache行作废,否则会出现“SPI明明收对了,程序读到的却是乱码”的诡异问题。

发送方向类似,DMA发送前要确保Cache里的数据已经写回内存,也就是执行SCB_CleanDCache_by_Addr,否则DMA从内存搬运时可能搬的是旧数据。

DMA的事务结束可以用中断回调函数处理,比如在HAL_SPI_TxCpltCallback里释放信号量。我建议把CS的拉高放在DMA完成回调里,因为DMA结束之后SPI总线可能还有最后几个周期的尾巴,提前拉高CS会导致最后一个字节丢失。

4. 数据可靠性设计与工程化落地

4.1 工业环境下的数据帧结构设计

MRAM本身存储可靠性很高,但工业现场的电磁干扰、MCU跑飞、总线毛刺这些因素仍然可能造成数据异常。所以不能光把裸数据往芯片里写,一定要在应用层做数据帧封装。

我目前用的日志数据帧结构是这样的:

字段长度说明
帧头1字节固定0xA5,用于同步
版本号1字节数据帧格式版本
记录序号4字节单调递增,用于新旧判断
数据长度2字节有效负载长度
数据区N字节实际业务数据
CRC324字节对前面所有字段做校验

这个结构看起来简单,但实际用起来非常顺手。帧头用于快速定位,序号用于分辨新旧记录,CRC32用于检测数据损坏。读取数据的时候,先扫描找到帧头,然后校验CRC,如果校验失败就直接跳过这一帧,不会因为一条坏记录把整个日志解析搞挂。

生产现场经常有强电设备启停,电源毛刺躲都躲不掉,有了这个设计,即便偶尔写进去坏数据,解析端也不会崩溃,下一次正常写入覆盖掉就行。

4.2 掉电保护与原子写策略

很多人一开始用MRAM,以为芯片本身不会掉数据就万事大吉了。实际上应用层还会面临一个更现实的问题:如果系统正在写数据写到一半掉电,这块数据是不是完整的?

MRAM的物理写过程极快,和Flash那种边擦边写完全不是一回事,所以单次写操作本身不需要担心掉电损坏。但如果你的数据帧分两次写,比如先写数据体,再写帧头和序号,第二次没写完就掉电了,那读出来的就是一条“半成品”记录。

我的做法是双槽备份加原子标志。具体实现是维护两个固定槽位,交替写入最新的完整数据帧。写入顺序也很关键:先写槽位里的数据体和CRC,最后一步才更新槽位头部的有效标志。这样无论在任何一步掉电,至少有一个槽位的数据是完整可用的。

上电启动时,两个槽位都读一遍,哪个有效标志正确、序号更新,就选哪个作为当前数据。这种方案在工业设备里非常成熟,逻辑简单又稳。

4.3 实测性能数据

我把这个方案跑在真实板卡上,用逻辑分析仪抓过SPI总线,做了几组基本性能测试,整理成表格供参考:

操作数据量实测参考耗时说明
READ(DMA)1KB约0.45ms22.5MHz SPI时钟
WRITE(DMA)1KB约0.5ms含WREN和地址开销
单字节写1字节约2µs不含应用层CRC
批量日志写入每次256字节约0.13ms含状态寄存器检查

这个速度对工业数据记录来说非常富余,即使每秒写10条256字节的日志,也只占用大约百分之十几的SPI时间,主控还能干别的活。

要特别说明,22.5MHz的SPI时钟下吞吐已经够用,没必要冒险超频到40MHz。如果真的要更快,可以考虑用MRAM的FAST READ指令,但实际收益不大,因为瓶颈主要在指令开销和数据长度,不差那几个dummy周期。

5. 常见问题与排查实录

5.1 写入后读回全是0xFF

新手最容易遇到的问题就是:用WRITE指令写完,回读发现全是0xFF,跟没写一样。

排查思路先看状态寄存器。如果WEL位没置位,写操作根本没生效。原因多半是WREN指令没发成功,或者CS时序不对。WREN指令必须在CS低电平期间发送完整一个字节,然后CS拉高,芯片才会锁存写使能状态。如果CS拉高的时序太短,或者GPIO配置有问题,写使能就没有真正完成。

另外检查WP#引脚。如果WP#被拉低,MRAM会启用硬件写保护,即使发了WREN也白搭。我见过一块板子就是因为WP#标号对错,一路焊到了GND上,折腾了大半天才发现。

5.2 D-Cache导致DMA数据不对

这个坑在STM32F469II这种带Cache的芯片上特别典型。症状是SPI读回来的数据在调试器里看内存是正确的,但程序跑到下一行,变量值突然变成了一堆旧数据。其实不是变量值变了,而是CPU读的是Cache里的旧内容。

解决办法就是前面提到的,DMA接收前主动invalidate Cache:

SCB_InvalidateDCache_by_Addr((uint32_t *)rxbuf, sizeof(rxbuf));

再补充一个细节:STM32F469的Cache Line通常是32字节,invalidate操作会按整条Cache Line对齐执行。如果你的DMA缓冲区长度不是32的整数倍,干脆把缓冲区定义成32的倍数大小,比如用256字节对齐数组,省得末尾边界处出现奇怪问题。

5.3 SPI速率过高导致偶发数据错位

如果SPI分频配置错了,比如在SPI1上直接用分频2,得到45MHz时钟,已经超过MR25H40CDF的40MHz上限。芯片在边缘条件下可能偶发采样错误,症状不是每次读写都错,而是偶尔某个字节多一位少一位,排查起来极其隐蔽。

这种问题逻辑分析仪不太容易抓,因为错误频率低。我的排查方式是把SPI时钟降一档,如果连续读写几千次不再出错,基本可以锁定是时钟余量不足。工业产品要有足够的降额意识,不要卡着芯片极限跑。

5.4 HOLD#引脚干扰导致通信卡死

还遇到过一种诡异现象:设备正常运行几分钟后,MRAM读写突然就卡住了,读状态寄存器也毫无响应,必须整机断电重启才恢复。查了几天,终于发现HOLD#引脚被一根长走线带着,受到旁边电机驱动脉冲的耦合干扰,瞬时被拉低,芯片进入暂停状态。

之后把HOLD#加上了10kΩ上拉电阻,并尽量远离高频线路重新布线,问题彻底消失。这里必须强调,HOLD#和WP#不是没用到就能随便处理的功能脚,最好当成有源信号对待。

6. 项目落地后的几点补充体会

整块芯片方案的落地过程比我想象中顺利,主要归功于MR25H40CDF本身兼容SPI Flash指令集,代码层面没有伤筋动骨的改动。如果你现在正准备从EEPROM或者Flash迁过来,建议先把状态寄存器读写跑通,再过渡到正式业务数据,这样排查问题有个清晰的分界线。

另外我强烈建议在生产阶段增加一项回读校验测试,不是简单的写后读一两个字节,而是让设备烧写一段固定模式数据后完整回读比对。MRAM虽然可靠性高,但任何芯片都可能在焊接、运输或者装机过程中受损,产线测试能帮你把这些残次品提前挡在出厂之前。

根据我个人经验,用MRAM的板子,开机自检里一定要加上“数据完整性检查”,检查无效标志、CRC字段是否合法。因为MRAM上电内容可以是随机的,第一次贴片回来的新板子,不初始化就上电读,读到的数据不一定是0xFF,还可能是各种乱七八糟的数。只有靠应用层主动格式化并写入可靠标志,才能确保整个系统一直运行在可控的数据状态里。这一点是MRAM和Flash最大的使用习惯差异,谁忽略了谁吃亏。

返回列表