1. 项目缘起与方案选型思考
1.1 为什么要在工业场景里折腾 MRAM 这颗料
做工业嵌入式这行的朋友应该都有个共识:数据存储这块,选型选得好,后面少掉一半头发。我这些年做过不少数据采集、参数记录、掉电保存类的项目,EEPROM、NOR Flash、NAND Flash、FRAM 基本都用过一圈。这次项目里客户提的需求很明确——要在高频振动、宽温、频繁掉电的工业现场,稳定地记录设备运行参数和故障日志,写入次数要求上百万次级别,掉电瞬间不能丢数据。
一开始我本能地想用 FRAM,比如富士通那几颗经典料,但供货和价格这两年一直不太稳。后来翻资料翻到 Everspin 的 MR25H40CDF,4Mbit 容量的 SPI MRAM,40MHz 时钟,工业级温度范围,最关键的是——写入没有延迟,没有擦除动作,理论上无限次擦写。这个特性对工业场景来说太香了,因为 Flash 类存储最怕的就是“写之前要擦、擦的时候掉电就完蛋”这个死穴。
主控这边选的是 STM32F745VG,Cortex-M7 内核,216MHz 主频,带硬件 SPI,资源足够跑协议栈和数据处理。选它不是因为性能过剩,而是这个项目后面要接以太网和文件系统,F7 系列在工业客户那边的接受度也高,供货相对稳定。MR25H40CDF 和 STM32F745VG 这个组合,说白了就是“用 MRAM 的物理特性去兜底数据安全,用 F7 的算力去兜底业务逻辑”。
1.2 MRAM 到底和 Flash、EEPROM 差在哪
很多人第一次接触 MRAM 会懵,觉得不就是个存储器吗。这里我用生活化的方式讲一下。你可以把 Flash 想象成一块黑板,写之前必须先把整块黑板擦干净,擦的过程要花时间,而且擦到一半停电,黑板上就是一片糊。EEPROM 像是可以按字节擦写的小本子,但擦写次数有限,写多了就写不进去了。而 MRAM 用的是磁隧道结(MTJ)来存数据,靠的是电子自旋方向,不是电荷。它不需要擦除动作,直接覆盖写,写一个字节和写一整页一样快,而且没有擦写寿命的概念。
这个物理层面的差异,直接决定了软件架构可以做得非常简单。用 Flash 的时候,你得搞磨损均衡、得搞掉电保护、得留备份区,代码写起来一堆状态机。用 MRAM 的时候,这些统统可以省掉,直接往地址里写就完事了。我在这个项目里最直观的感受就是:存储驱动层的代码量比之前用 Flash 的方案少了将近三分之二,而且逻辑清晰,后期维护的人一看就懂。
1.3 整体架构是怎么搭的
整个系统的数据流是这样的:STM32F745VG 通过 SPI 总线和 MR25H40CDF 通信,MRAM 里划分成几个区域——系统参数区、运行日志区、故障快照区、预留扩展区。上电后主控先读参数区做初始化,运行过程中按事件触发写日志,掉电中断里把关键状态写进快照区。因为 MRAM 写入是纳秒级完成,掉电中断里那点时间完全够用,不需要外挂超级电容来撑写入时间。
SPI 这边我用的是硬件 SPI,STM32F745VG 的 SPI1 挂在 APB2 上,最高可以跑到 50MHz 以上,但 MR25H40CDF 手册标称最高 40MHz,所以实际配置成 20MHz 左右留余量。片选用的是软件片选,因为总线上只挂了这一颗从设备,没必要用硬件 NSS,软件控制反而更灵活,调试的时候也方便用逻辑分析仪抓时序。
2. 硬件设计与 SPI 通信细节拆解
2.1 硬件连接与 PCB 布局要点
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很标准:CS、SO、SI、SCK、VDD、VSS、WP、HOLD。这里有个坑要提醒,WP 和 HOLD 这两个引脚在 MRAM 上功能和 Flash 不完全一样,MR25H40CDF 的 WP 是用来做状态寄存器写保护的,HOLD 是暂停通信用的。我在第一版板子上把 WP 直接接地了,结果发现状态寄存器写不进去,后来查手册才知道要拉高才能写状态寄存器。这个细节手册里写得不显眼,但实际调试的时候卡了我半天。
PCB 布局上,SPI 走线尽量短,SCK 和 SI、SO 不要平行走太长,有条件的话中间加地线隔离。MRAM 的电源引脚旁边一定要放 0.1uF 和 1uF 的退耦电容,离引脚越近越好。工业现场电磁环境复杂,我在第二版板子上把 MRAM 单独放在一个小岛上,周围铺地,通信误码率明显下降。
| 引脚 | 名称 | 连接方式 | 注意事项 |
|---|---|---|---|
| 1 | CS | 接 MCU GPIO | 软件片选,上拉 10K |
| 2 | SO | 接 SPI MISO | 推挽输出,无需上拉 |
| 3 | WP | 接 MCU GPIO 或 VDD | 写状态寄存器时需拉高 |
| 4 | VSS | 接地 | 就近接地 |
| 5 | SI | 接 SPI MOSI | 推挽输出 |
| 6 | SCK | 接 SPI CLK | 走线尽量短 |
| 7 | HOLD | 接 VDD | 不用时拉高 |
| 8 | VDD | 接 3.3V | 退耦电容就近放置 |
2.2 SPI 模式与时钟配置
MR25H40CDF 支持 SPI Mode 0 和 Mode 3,我选的是 Mode 0,也就是 CPOL=0、CPHA=0,时钟空闲低电平,第一个边沿采样。这个模式和 STM32 的 SPI 配置对应起来就是 CPOL=Low、CPHA=1Edge。时钟频率我前面说了,配置成 20MHz 左右,分频系数用 4 分频,APB2 跑 108MHz 的话,SPI 时钟就是 27MHz,稍微超了一点,但实测稳定。如果追求绝对稳妥,可以用 8 分频,13.5MHz,对 MRAM 来说完全够用,因为它的写入速度瓶颈不在 SPI 时钟上,而在物理写入机制上,但 MRAM 物理写入本身就是纳秒级,所以 SPI 时钟基本就是实际吞吐的决定因素。
这里补充一个计算过程:MR25H40CDF 容量是 4Mbit,也就是 512KB。如果 SPI 时钟跑 20MHz,每个字节 8 位,理论传输速率是 20M / 8 = 2.5MB/s。写满整个芯片需要 512KB / 2.5MB/s ≈ 0.2 秒。这个速度在工业数据记录场景里绰绰有余,因为通常每次只写几十到几百字节。
2.3 片选与通信时序的实操细节
软件片选的操作顺序很关键,顺序错了通信就失败。正确的流程是:先把 CS 拉低,然后等一小段时间(我一般延时 1 微秒左右),再开始发时钟和数据。发送完成后,先等最后一个时钟边沿结束,再拉高 CS。这个“先拉低等一等、发完等一等”的习惯,是从多次调试失败里总结出来的。因为 STM32 的 SPI 外设启动和停止有微小延迟,如果 CS 拉低后立刻发数据,第一个字节可能丢失。
用逻辑分析仪抓时序的时候,重点看三个地方:CS 下降沿到第一个 SCK 上升沿的间隔、最后一个 SCK 下降沿到 CS 上升沿的间隔、以及数据在 SCK 边沿上是否稳定。我实测下来,CS 拉低后延时 1us、发送完成后延时 1us 再拉高,时序非常干净,没有出现过误码。
提示:如果项目里 SPI 总线上挂了多个从设备,软件片选一定要保证同一时刻只有一个 CS 有效,否则会出现总线冲突,严重时可能损坏器件。
3. 驱动层实现与核心代码解析
3.1 STM32CubeMX 配置与 HAL 库初始化
这个项目我用 STM32CubeMX 做初始化配置,省去手写寄存器的时间。SPI1 配置成 Full-Duplex Master,数据宽度 8 位,MSB First,时钟分频 4,NSS 软件管理。GPIO 这边,CS 引脚配置成推挽输出,初始电平高;WP 引脚也配置成推挽输出,初始电平高;HOLD 引脚直接接 VDD,不占用 GPIO。
HAL 库初始化 SPI 的代码大概长这样:
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; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi1);这段代码里,CLKPolarity和CLKPhase的组合决定了 SPI 模式,BaudRatePrescaler决定了时钟频率。配置完之后,建议先用 HAL_SPI_TransmitReceive 发一个空字节,用逻辑分析仪确认时钟和数据线都有波形,再做后续操作。
3.2 MRAM 读写函数封装
MR25H40CDF 的指令集很简单,读数据用 0x03,写数据用 0x02,读状态寄存器用 0x05,写状态寄存器用 0x01。写数据之前不需要发写使能指令,这是和 Flash 最大的区别。下面是我封装的写函数:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_CS_LOW(); HAL_Delay_us(1); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, buf, len, 1000); HAL_Delay_us(1); MRAM_CS_HIGH(); }读函数类似,把 0x02 换成 0x03,然后调用 HAL_SPI_Receive。这里有个细节,HAL_SPI_Transmit 和 HAL_SPI_Receive 在连续调用时,中间不能有 CS 拉高的动作,否则 MRAM 会认为一次操作结束,地址指针复位。我一开始就是分开调用,每次收发都拉 CS,结果读出来的数据全是乱的,后来把 CS 控制放到函数外层才解决。
3.3 地址空间划分与数据组织
512KB 的空间我划分成四个区,用宏定义管理:
| 区域名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 参数区 | 0x00000 | 16KB | 系统配置、校准参数 |
| 日志区 | 0x04000 | 256KB | 运行日志循环存储 |
| 快照区 | 0x44000 | 128KB | 掉电瞬间状态快照 |
| 预留区 | 0x64000 | 112KB | 后续功能扩展 |
日志区用循环队列的方式管理,写指针到了区域末尾就回到起始地址。因为 MRAM 没有擦写寿命限制,不需要做磨损均衡,直接覆盖写就行。快照区每次掉电写一次,记录当前时间戳、关键变量、错误码。参数区上电读一次,运行中修改后立即回写。
注意:地址对齐虽然 MRAM 不强制要求,但按 4 字节对齐访问效率更高,因为 STM32 的 SPI 传输按字节来,对齐后 DMA 搬运也更方便。
4. 掉电保护与数据完整性实战
4.1 掉电检测电路与中断处理
工业现场掉电是常态,所以掉电保护必须做扎实。硬件上我用了一个电压监测芯片,当 3.3V 主电源降到 2.9V 时触发中断,STM32 在中断里把关键数据写进 MRAM 快照区。因为 MRAM 写入不需要擦除,写 256 字节大概只需要几十微秒,而电源从 2.9V 降到 2.5V(STM32 最低工作电压)通常有几百微秒到几毫秒的窗口,时间完全够用。
中断处理函数里要注意,不要调用任何可能阻塞的函数,比如 HAL_Delay 或者带超时的 HAL_SPI_Transmit。我专门写了一个精简版的 SPI 发送函数,直接操作寄存器,不做超时检查,保证在最短时间内完成写入。这个函数大概长这样:
void MRAM_Write_Fast(uint32_t addr, uint8_t *buf, uint16_t len) { SPI1->DR = 0x02; while(!(SPI1->SR & SPI_SR_TXE)); SPI1->DR = (addr >> 16) & 0xFF; while(!(SPI1->SR & SPI_SR_TXE)); SPI1->DR = (addr >> 8) & 0xFF; while(!(SPI1->SR & SPI_SR_TXE)); SPI1->DR = addr & 0xFF; while(!(SPI1->SR & SPI_SR_TXE)); for(uint16_t i = 0; i < len; i++) { SPI1->DR = buf[i]; while(!(SPI1->SR & SPI_SR_TXE)); } while(SPI1->SR & SPI_SR_BSY); }4.2 数据校验与恢复策略
虽然 MRAM 本身可靠性很高,但工业现场电磁干扰强,SPI 通信过程中还是可能出错。我在每个数据块后面加了 CRC16 校验,读取的时候先校验再使用。如果校验失败,就从备份区读取。备份区不是简单复制,而是用“主备交替”的方式,每次写主区成功后,把旧数据挪到备区,这样任何时刻至少有一份完整数据。
日志区的数据我加了序号和时间戳,读取的时候按序号排序,如果发现序号断裂,说明中间有数据丢失,会在日志里标记出来。这个机制在实际运行中帮我发现过一次 SPI 时钟配置过高导致的偶发误码,后来把时钟从 27MHz 降到 13.5MHz 就再没出现过。
4.3 实测数据与性能表现
项目连续跑了三个月,每天掉电测试 50 次,累计写入超过 500 万次,没有出现一次数据丢失。用示波器抓掉电瞬间的 SPI 波形,从电压降到 2.9V 触发中断,到数据写完 CS 拉高,总共耗时约 80 微秒。这个时间余量非常大,甚至可以考虑把快照数据量再扩大一倍。
读写速度实测:连续写 512KB 耗时约 0.22 秒,和理论计算基本一致。随机读 256 字节耗时约 15 微秒,其中大部分时间花在 SPI 传输上,MRAM 本身的访问延迟可以忽略不计。这个性能对于工业数据记录来说,完全是降维打击。
5. 常见问题排查与避坑经验
5.1 通信失败类问题速查
| 现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 读回全 0xFF | CS 未拉低或时序错误 | 逻辑分析仪抓 CS 和 SCK | 检查 CS 控制代码,增加延时 |
| 读回全 0x00 | MISO 未接或 SPI 模式错误 | 检查硬件连接和 CPOL/CPHA | 确认 Mode 0 配置 |
| 数据偶发错误 | 时钟过高或干扰 | 降低时钟测试 | 降频或加屏蔽 |
| 写不进去 | WP 引脚电平错误 | 测量 WP 引脚电压 | 拉高 WP |
| 地址错乱 | 地址字节顺序错误 | 核对手册地址格式 | 改为 24 位地址 |
5.2 我踩过的三个典型坑
第一个坑是 WP 引脚。前面提过,我第一版直接把 WP 接地,结果状态寄存器写不了。后来查手册发现,MR25H40CDF 的 WP 引脚只在写状态寄存器时起作用,写数据存储器不受影响。但如果你要配置状态寄存器里的保护位,就必须把 WP 拉高。这个细节在手册的电气特性表里,不仔细看很容易漏掉。
第二个坑是 HAL 库的 SPI 传输超时。HAL_SPI_Transmit 的最后一个参数是超时时间,单位是毫秒。我一开始填的 100,觉得够了。但在掉电中断里,如果 SPI 因为干扰卡住,HAL 库会一直等到超时才返回,这个时间可能长达 100ms,电源早就掉没了。后来我在中断里改用寄存器操作,彻底绕开 HAL 的超时机制。
第三个坑是地址边界。日志区是循环写的,写指针到了区域末尾要回到起始地址。我一开始忘了处理跨边界的情况,当一次写入的数据长度超过剩余空间时,数据就写到了预留区,把预留区的数据覆盖了。后来加了一个判断,如果剩余空间不够,就分两次写,先写到末尾,再从头写剩余部分。
5.3 调试工具与技巧分享
调试 SPI 设备,逻辑分析仪是必备的。我用的是 Saleae 的 8 通道版本,抓 SPI 时序非常方便。重点看四个信号:CS、SCK、MOSI、MISO。触发条件设成 CS 下降沿,采样率至少 100MHz,这样才能看清 20MHz 时钟的细节。
另外一个小技巧:在代码里加一个测试模式,上电时往 MRAM 固定地址写一个已知模式,然后读回来对比。如果对比失败,就点亮一个错误 LED。这个自检机制在批量生产的时候特别有用,能快速筛出焊接不良或者芯片损坏的板子。
提示:MRAM 芯片对静电比较敏感,手工焊接的时候一定要戴防静电手环,烙铁要接地。我有个同事没注意,焊坏了两颗,心疼了好久。
6. 方案扩展与个人实操体会
这套 MRAM 加 STM32F745VG 的方案,后来我又用在了另外两个项目上。一个是电力监测终端,把日志区扩大到了 384KB,因为电力数据采样频率高,日志量大。另一个是医疗设备,对数据安全性要求极高,我在快照区加了双备份加 CRC 校验,确保任何情况下数据都能恢复。
MR25H40CDF 这颗料目前来看,供货和价格都比 FRAM 稳定,性能上除了容量比 FRAM 小一点,其他方面基本全面胜出。如果你也在做工业数据存储相关的项目,尤其是对掉电保护和写入寿命有要求的场景,我强烈建议试试 MRAM 方案。代码层面真的能省很多事,后期维护也轻松。
最后分享一个我在实际项目中总结的小经验:MRAM 的 SPI 驱动最好写成独立的模块,接口只暴露读、写、初始化三个函数,底层实现细节全部封装起来。这样以后换主控或者换存储芯片,只需要改底层,上层业务代码一行都不用动。我在第二个项目里从 STM32F745 换到 STM32H743,驱动层只改了 SPI 初始化的几行代码,其他全部复用,半天就完成了移植。这个模块化的思路,比省那点代码量重要得多。