1. 项目缘起与方案选型思考
1.1 为什么要在工业场景里折腾 MRAM 这颗料
做工业嵌入式这行的朋友应该都有个共识:数据存储这块,选型选得好,后面少掉一半头发。我这些年经手的项目里,EEPROM 写次数不够、Flash 掉电丢数据、FRAM 容量太小价格太贵,这些坑基本踩了个遍。直到前两年接触到MR25H40CDF这颗磁阻随机存储器(MRAM),才算找到一个在工业环境下真正能打的方案。
MR25H40CDF 是 Everspin 家的 4Mbit 串行 MRAM,走 SPI 接口,SOIC-8 封装,工作温度覆盖 -40°C 到 +85°C 的工业级范围。它最吸引我的地方在于三点:写入寿命近乎无限(官方标称 10^14 次以上)、写入无需等待(没有 Flash 那种擦除周期)、掉电数据不丢(本质是磁性存储,非电荷存储)。这三点恰好对应工业现场最头疼的三个问题:频繁记录、实时响应、突然断电。
我这次用的主控是STM32F745ZG,Cortex-M7 内核,216MHz 主频,1MB Flash,320KB SRAM,带多个 SPI 外设。选它是因为项目里同时要跑数据采集、协议解析和存储管理,M7 的算力和丰富外设刚好够用,而且 STM32 的 HAL 库生态成熟,SPI 驱动写起来不费劲。MR25H40CDF 挂到 STM32F745ZG 的 SPI 总线上,就构成了一个典型的工业数据存储节点。
1.2 存储方案横向对比:为什么不是 EEPROM 也不是 Flash
在动手之前,我把常见方案拉出来对比了一遍,这个决策过程值得记录,因为很多新手一上来就问“用哪个芯片”,其实应该先问“我的数据写入模式是什么”。
| 存储类型 | 写入寿命 | 写入速度 | 掉电保持 | 典型容量 | 工业适用性 |
|---|---|---|---|---|---|
| EEPROM | 100万次 | 慢(ms级) | 好 | KB级 | 低频配置存储 |
| NOR Flash | 10万次 | 慢(需擦除) | 好 | MB级 | 固件、日志 |
| FRAM | 10^12次 | 快 | 好 | KB~MB | 高频记录 |
| MRAM | 10^14次 | 快(ns级) | 好 | MB级 | 高频+工业 |
从表里能看出来,MRAM 在写入寿命和速度上是碾压性的。我实测过,往 MR25H40CDF 里连续写 4KB 数据,SPI 跑 20MHz,耗时不到 2ms,而且中间不需要任何擦除等待。换成 Flash,同样的数据量得先擦一个扇区(几十毫秒),再写进去,整个流程下来 50ms 都打不住。对于需要每秒记录几十次传感器数据的场景,这个差距是致命的。
注意:MRAM 虽然写入快,但读取速度受 SPI 时钟限制,别指望它能当 SRAM 用。它的定位是“非易失性高速记录介质”,不是内存扩展。
1.3 硬件连接的整体思路
MR25H40CDF 和 STM32F745ZG 的连接非常干净,SPI 四线制加一根片选,总共五根线。我选的是 SPI2,因为 SPI1 留给了一块 TFT 屏,SPI3 被 SD 卡占了。SPI2 挂在 APB1 总线上,时钟源 54MHz,分频后跑 13.5MHz 或 27MHz 都行,MR25H40CDF 最高支持 40MHz,余量充足。
具体引脚分配是这样的:SCK 用 PB13,MISO 用 PB14,MOSI 用 PB15,片选 CS 用 PB12。这里有个细节,片选我用了硬件 NSS 的软件管理模式,也就是把 PB12 配成普通 GPIO 输出,手动拉低拉高。为什么不直接用硬件 NSS?因为硬件 NSS 在多从机场景下容易出幺蛾子,而且 STM32 的硬件 NSS 在某些模式下会自己抖动,调试起来很烦。软件片选虽然多写两行代码,但控制权完全在自己手里,稳。
电源方面,MR25H40CDF 是 3.3V 供电,和 STM32 同电平,不需要电平转换。去耦电容我放了 100nF 和 10uF 各一个,紧贴芯片电源脚。工业现场电磁环境复杂,这个去耦不能省,我见过因为省电容导致 SPI 通信偶发错误的案例。
2. MR25H40CDF 核心细节与 SPI 通信要点
2.1 芯片内部结构与地址映射
MR25H40CDF 的 4Mbit 容量换算成字节是 512KB,地址范围 0x00000 到 0x7FFFF,需要 19 位地址。它的 SPI 指令集和标准 Flash 类似,但有几个关键区别。读数据用 0x03(READ),写数据用 0x02(WRITE),这两个指令后面跟 3 字节地址,然后就是连续的数据流。注意,MRAM 没有擦除指令,这是它和 Flash 最大的不同,写之前不需要发 0x06(WREN)写使能,也不需要等 0x05(RDSR)状态寄存器。
我刚开始用的时候习惯性地按 Flash 的流程走,先发写使能再写数据,结果发现数据也能写进去,但多了一步无意义的操作。后来查手册才确认,MR25H40CDF 的写操作是直接生效的,不需要预置任何状态。这个特性简化了驱动逻辑,也减少了通信开销。
芯片内部还有一个状态寄存器,地址 0x00,可以通过 0x05 指令读取。不过对于基本的读写操作,这个寄存器可以不管。我只有在调试阶段会读一下,确认芯片没进入某种保护状态。
2.2 SPI 模式与时钟极性的选择
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我选的是模式 0,因为 STM32 的 HAL 库默认配置就是模式 0,省得改。模式 0 的意思是:时钟空闲时低电平,数据在时钟上升沿采样。这个和大多数 SPI Flash 一致,兼容性好。
时钟频率方面,我一开始跑的是 27MHz,后来发现工业现场线缆较长时偶发误码,降到 13.5MHz 后稳定了。这里有个经验:SPI 时钟不是越高越好,要看 PCB 走线和现场干扰。如果 MR25H40CDF 离 STM32 很近(比如同一块板子上),27MHz 甚至 40MHz 都没问题;如果通过排线连接,建议降到 10MHz 以下,或者加缓冲芯片。
提示:STM32F745ZG 的 SPI2 时钟来自 APB1,最高 54MHz。分频系数选 2 得 27MHz,选 4 得 13.5MHz。用 HAL_SPI_Init 配置时,把 BaudRatePrescaler 设成 SPI_BAUDRATEPRESCALER_4 就是 13.5MHz。
2.3 页写与边界处理
MR25H40CDF 没有页的概念,整个 512KB 是线性地址空间,可以跨页连续写。这一点比 Flash 友好太多。Flash 通常有 256 字节的页限制,跨页写会回卷,导致数据错位。MRAM 完全没这个问题,你从地址 0x7FF0 开始写 32 字节,它会老老实实写到 0x800F,不会回卷到 0x0000。
不过要注意,SPI 传输本身是流式的,地址递增由芯片内部自动完成。你发完 3 字节地址后,只要 CS 保持低电平,后续每个时钟周期写入的数据都会按顺序存到下一个地址。如果中途拉高 CS,本次写操作就结束了,下次再写需要重新发地址。
我在驱动里封装了一个MRAM_Write函数,参数是地址、数据指针、长度。内部逻辑就是拉低 CS、发 0x02、发 3 字节地址、循环发数据、拉高 CS。读操作类似,指令换成 0x03。这个函数我用了两年多,没出过问题。
3. STM32F745ZG 端的驱动实现与实操步骤
3.1 CubeMX 配置与 SPI 初始化
我用 STM32CubeMX 做初始化配置,这是 ST 生态里最省事的工具。打开 CubeMX,选 STM32F745ZGT6,然后配置 SPI2:
- Mode 选 Full-Duplex Master
- Hardware NSS Signal 选 Disable(我们用软件片选)
- Data Size 选 8 Bits
- First Bit 选 MSB First
- Clock Polarity 选 Low
- Clock Phase 选 1 Edge
- Baud Rate Prescaler 选 4(得 13.5MHz)
- CRC Calculation 选 Disabled
然后配置 PB12 为 GPIO Output,初始电平 High(片选不选中)。生成代码后,HAL 会自动初始化 SPI2 和 GPIO。
这里有个坑:CubeMX 生成的 GPIO 初始化代码里,PB12 的默认输出电平可能是 Low。如果你不手动改成 High,上电瞬间片选就是选中的,MRAM 可能会误触发。我一般在MX_GPIO_Init里把HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET)放在初始化最后。
3.2 底层读写函数实现
驱动代码我分成三层:底层 SPI 收发、MRAM 指令封装、上层数据管理。底层用 HAL 库的HAL_SPI_Transmit和HAL_SPI_Receive,但这两个函数在连续读写时效率不高,因为每次调用都有函数开销。我后来改成了HAL_SPI_TransmitReceive配合 DMA,速度提升明显。
先看基础版,不用 DMA 的:
#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET) 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_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Transmit(&hspi2, buf, len, 1000); MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] = 0x03; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Receive(&hspi2, buf, len, 1000); MRAM_CS_HIGH(); }这段代码逻辑很直白,但有个性能问题:HAL_SPI_Transmit和HAL_SPI_Receive是阻塞式的,每次调用都要等传输完成。如果 len 是 512 字节,在 13.5MHz 下大概需要 300us,CPU 全程干等。对于实时性要求高的场景,这不可接受。
3.3 DMA 优化与速度实测
改成 DMA 后,CPU 可以在 SPI 传输期间去处理其他任务。STM32F745ZG 的 SPI2 支持 DMA,我用 DMA1 Stream 3(SPI2_RX)和 Stream 4(SPI2_TX)。配置好 DMA 后,读写函数变成这样:
void MRAM_Write_DMA(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_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Transmit_DMA(&hspi2, buf, len); // 等待DMA完成,实际项目中用回调或信号量 while (hspi2.State != HAL_SPI_STATE_READY); MRAM_CS_HIGH(); }实测数据:写 512 字节,阻塞模式耗时约 320us,DMA 模式耗时约 310us(传输时间本身没变,但 CPU 占用从 100% 降到接近 0)。读 512 字节,阻塞模式约 310us,DMA 模式约 300us。速度提升不明显是因为 SPI 时钟是瓶颈,但 CPU 解放出来了,可以同时跑 PID 控制或通信协议。
实操心得:DMA 模式下,CS 的拉高时机很关键。必须在 DMA 传输完成中断里拉高 CS,不能在调用
HAL_SPI_Transmit_DMA后立刻拉高,否则数据还没发完 CS 就失效了,MRAM 会截断写入。我踩过这个坑,数据只写进去前几个字节,后面全丢。
3.4 数据完整性校验方案
工业现场干扰大,SPI 通信偶尔会翻位。我在每个数据块后面加了 CRC16 校验,存的时候算好 CRC 一起写进去,读的时候重新算一遍对比。CRC16 用查表法实现,速度快,占用 Flash 小。
数据块结构我设计成:4 字节头(0xAA55AA55)+ 2 字节长度 + N 字节数据 + 2 字节 CRC。这样即使某次写入被干扰,读出来也能发现。如果 CRC 不对,就重读一次,连续三次失败就上报错误。
这个机制帮我抓过好几次现场问题,有一次是电源纹波太大导致 SPI 时钟抖动,CRC 错误率飙升,换了 LDO 就好了。如果没有 CRC,这种偶发错误根本查不出来,数据错了都不知道。
4. 工业场景下的实战问题与排查记录
4.1 上电初始化失败的几种可能
MR25H40CDF 上电后不需要特殊初始化序列,但我在实际项目里遇到过几次“读出来全是 0xFF”的情况。排查下来,原因主要有三类:
第一类是片选引脚状态不对。STM32 复位后 GPIO 默认是浮空输入,如果 PB12 没及时配成输出高,MRAM 的 CS 可能被外部干扰拉低,导致芯片进入某种中间状态。解决办法是在main函数最开头就把 PB12 配好,早于 SPI 初始化。
第二类是SPI 时钟极性配错。有一次我复制了另一个项目的代码,那个项目用的是模式 3,结果 MRAM 读出来全是乱码。改成模式 0 后正常。这个问题的隐蔽性在于,模式配错时 SPI 通信本身不报错,HAL 库照样返回 HAL_OK,但数据就是不对。
第三类是电源上升时间太慢。MR25H40CDF 要求电源在 1ms 内上升到 3.0V 以上,如果电源缓启动太慢,芯片内部状态机可能没复位干净。我在电源脚并了一个 10uF 电容后问题消失。
4.2 SPI 通信误码的排查思路
误码是 SPI 调试中最常见的问题,表现是读出来的数据偶尔和写入的不一致。我的排查顺序是这样的:
先看时钟频率。把 SPI 时钟降到 1MHz,如果误码消失,说明是时序问题。这时候检查 PCB 走线,SCK 和 MOSI 是否等长,有没有跨分割地平面。我遇到过 SCK 走线太长(超过 10cm)导致上升沿变缓,MRAM 采样出错。
再看片选时序。用逻辑分析仪抓 CS、SCK、MOSI 三根线,看 CS 拉低到第一个 SCK 上升沿之间是否有足够时间。MR25H40CDF 要求 CS 建立时间最小 5ns,一般都能满足,但如果 GPIO 驱动能力弱,可能会延迟。
最后看电源噪声。用示波器看 MRAM 电源脚的纹波,如果峰峰值超过 100mV,就要加去耦或换 LDO。工业现场电机启停时电源波动很大,我一般会在 MRAM 电源前加一个磁珠加电容的π型滤波。
4.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读出全 0xFF | CS 未拉低或 SPI 模式错 | 逻辑分析仪抓波形 | 检查 GPIO 配置和 SPI 模式 |
| 读出全 0x00 | MISO 未接或芯片未供电 | 万用表测电压 | 检查硬件连接 |
| 偶发误码 | 时钟太快或干扰 | 降频测试 | 降 SPI 时钟、加去耦 |
| 写入不生效 | CS 拉高太早 | 抓 CS 和 SCK 时序 | DMA 完成后再拉高 CS |
| 数据错位 | 地址计算错误 | 打印地址值 | 检查地址移位运算 |
| 芯片发热 | 电源接反或过压 | 测电源电压 | 检查供电,换芯片 |
4.4 工业环境下的可靠性加固
工业现场和实验室完全是两个世界。我在实验室跑得好好的板子,到了现场可能一天重启三次。针对 MRAM 存储,我做了几项加固:
电源监控:用 STM32 的内部 PVD(可编程电压检测器)监控 3.3V 电源,一旦低于 2.9V 就触发中断,在中断里紧急保存关键数据到 MRAM。MRAM 写入快,2ms 内能存完 1KB 数据,足够在电源彻底掉下去之前完成。
写保护:MR25H40CDF 有一个 WP 引脚,我把它接到 STM32 的另一个 GPIO,正常运行时拉高,只有在需要写入时才拉低。这样可以防止程序跑飞时误写数据。
数据冗余:关键配置数据我存三份,地址分别是 0x00000、0x10000、0x20000。读取时三份对比,取多数值。如果某一份 CRC 错误,就用另外两份修复。这个策略牺牲了 3 倍空间,但换来了极高的可靠性。
温度监控:STM32F745ZG 内部有温度传感器,我每 10 秒读一次,如果温度超过 85°C,就降低 SPI 时钟频率并减少写入频次。虽然 MRAM 本身耐温到 85°C,但长期高温下 PCB 和焊点会老化,降频能延长寿命。
5. 性能优化与进阶玩法
5.1 批量写入的缓冲策略
MRAM 写入虽然快,但频繁调用 SPI 传输也有开销。我设计了一个 4KB 的环形缓冲区,上层应用往缓冲区里写数据,底层每 100ms 或缓冲区满时一次性刷到 MRAM。这样把多次小写入合并成一次大写入,减少了 CS 切换和指令开销。
实测下来,每秒记录 100 次、每次 32 字节的场景,直接写 MRAM 的 CPU 占用约 15%,用缓冲策略后降到 3% 以下。而且 MRAM 的写入寿命是 10^14 次,就算每秒写 1000 次,也能用 3000 年以上,完全不用担心寿命问题。
5.2 掉电检测与紧急保存
掉电检测我用的是 STM32 的 PVD 加一个外部比较器。PVD 阈值设 2.9V,外部比较器设 2.7V。正常运行时 PVD 中断先触发,我在中断里把缓冲区数据刷到 MRAM;如果电源继续掉到 2.7V,比较器触发,直接进紧急保存流程,只存最关键的 256 字节。
这个双阈值设计给了我大约 5ms 的缓冲时间。实测从 3.3V 掉到 2.7V,用 1000uF 电容能撑 8ms 左右,足够完成保存。电容容量要根据实际功耗算,我的板子运行电流约 120mA,1000uF 电容从 3.3V 放到 2.7V 释放的电荷是 Q=C×ΔU=1000uF×0.6V=0.6mC,除以电流 120mA 得 5ms,和实测吻合。
5.3 与文件系统的结合
如果项目需要存大量结构化数据,可以在 MRAM 上跑一个轻量级文件系统,比如 LittleFS 或 SPIFFS。这两个都是为嵌入式设计的,支持掉电恢复和磨损均衡。不过 MRAM 本身寿命无限,磨损均衡其实用不上,但掉电恢复很有价值。
我试过在 MRAM 上跑 LittleFS,512KB 空间格式化成文件系统后可用约 480KB,读写小文件速度很快,因为 MRAM 没有擦除延迟。不过 LittleFS 的元数据操作比较频繁,如果直接跑在 MRAM 上,SPI 通信次数会很多。我的做法是加一层缓存,把元数据缓存在 STM32 的 SRAM 里,定期同步。
5.4 多芯片级联扩展容量
512KB 如果不够用,可以挂多片 MR25H40CDF。SPI 总线是共享的,每片用独立的 CS 引脚。STM32F745ZG 有足够的 GPIO 来片选多片芯片。我最多挂过 4 片,总容量 2MB,片选分别用 PB12、PB11、PB10、PB9。
多片管理的难点在于地址映射。我封装了一个MRAM_SelectChip函数,根据地址高两位选择芯片,低 19 位作为片内地址。上层应用只需要调MRAM_Write(addr, buf, len),不用关心具体写到哪片。这个抽象层让扩展容量变得很简单,加芯片只需要改配置表。
注意:多片共享 SPI 总线时,未选中芯片的 MISO 必须处于高阻态。MR25H40CDF 在 CS 高时 MISO 确实是高阻,所以可以直接并联。但如果总线上混了其他类型的 SPI 从机,要确认它们的 MISO 行为,必要时加缓冲器。
6. 一些踩坑后的个人体会
这个项目从选型到量产,前后折腾了大半年,中间踩的坑比预想的多。最大的体会是:MRAM 虽好,但不能无脑用。它的优势在于高频写入和掉电保持,如果你的应用只是存个配置参数,一年写不了几次,那用 EEPROM 更便宜。选型要匹配场景,不是越贵越好。
另一个体会是SPI 时序一定要用仪器验证。我早期调试时全靠打印日志,结果一个偶发误码查了两周。后来买了台二手逻辑分析仪,几百块钱,抓一次波形就定位到是 CS 拉高太早。这个工具投入绝对值得,比瞎猜效率高十倍。
最后说个细节:MR25H40CDF 的 SOIC-8 封装在手工焊接时容易虚焊,尤其是地线脚。我建议用热风枪加焊膏,或者直接买转接板。虚焊的表现是时好时坏,特别难查。我有一批板子就是地线虚焊,常温下正常,一到低温就通信失败,折腾了好久才找到原因。
这个方案目前已经在三个工业现场稳定运行超过一年,最长的那个节点记录了超过 2 亿次传感器数据,MRAM 一颗没换过。如果你也在做类似的数据存储需求,这套组合值得试试。