1. 项目缘起与方案选型
1.1 为什么要在工业场景里盯上 MRAM 这颗料
做工业嵌入式的人都有一个共同的痛:设备装到现场,跑个三五年,最怕的不是主控挂掉,而是存储先出问题。EEPROM 擦写次数撑不住高频日志,NOR Flash 写入前要擦除、掉电窗口里容易丢数据,FRAM 容量小价格高,带电池的 SRAM 又要考虑电池寿命和环保合规。这几年我在做数据采集终端和工业控制器的时候,反复被同一个问题折磨——有没有一种存储器,写入像 RAM 一样随意,掉电又不丢,还不用管擦除块和磨损均衡?
MR25H40CDF 就是在这个背景下进入视野的。它是 Everspin 家的 4Mbit 串行 MRAM,SPI 接口,40MHz 时钟,最关键的一点是:它是真正的非易失性存储器,但写入行为跟 SRAM 几乎一样。没有擦除命令,没有页编程等待,没有写前擦除的时序约束,字节级随机写入,写一次就是一次,掉电即保存。这一点对工业应用来说太重要了,因为很多现场故障恰恰发生在“正在写 Flash”的那几百微秒里。
我选它搭配 STM32F722VE,原因也很直接。F722 是 ST 家 Cortex-M7 阵营里性价比很稳的一颗,216MHz 主频,带 FPU 和 DSP 指令,SPI 外设资源丰富,工业温度范围版本好买,LQFP100 封装对四层板友好。更关键的是,它的 SPI 支持 8 到 16 位数据帧、硬件 CRC、可配置 FIFO 阈值,配合 DMA 做高速连续读写时,CPU 占用能压得很低。MR25H40CDF 的 40MHz SPI 上限,用 F722 的 SPI1 挂在 APB2 上跑 36MHz 或 45MHz 分频,实际速率完全够用,而且时序余量充足。
提示:MR25H40CDF 的 SPI 模式支持 Mode 0 和 Mode 3,但工业现场建议固定用 Mode 0,因为长线缆传输时 CPOL=0 的静态低电平抗干扰表现更稳,这一点后面讲 PCB 时会展开。
1.2 这套组合到底解决了什么问题
把话说直白一点,这套方案解决的是工业嵌入式设备中“高频小数据量、掉电不能丢、寿命要够长”的存储需求。典型场景包括:PLC 的配方参数和运行日志、数据采集器的秒级采样缓存、医疗设备的校准系数和事件记录、电力终端的故障录波索引。这些场景的共同特征是写入频繁但单次数据量不大,可能每秒几次到几十次,每次几个字节到几百字节,设备寿命要求十年以上,现场断电是常态而不是异常。
传统方案里,要么用铁电存储器,容量小、成本高;要么用带超级电容的 SRAM,电容老化后照样丢数据;要么用 Flash 加磨损均衡算法,软件复杂度上去了,掉电保护还得额外做。MR25H40CDF 的 4Mbit 容量,也就是 512KB,对于参数存储加日志缓存来说刚刚好,不用做复杂的文件系统,直接线性地址映射,读写就是 memcpy 风格的 SPI 传输,软件栈极简。
STM32F722VE 这边,我通常会把 MRAM 挂在 SPI1 上,配合 DMA2 的 Stream 做双向传输,另外留一个 GPIO 做片选,不用硬件 NSS 是因为多从机场景下硬件片选容易打架,而且软件片选在调试时更容易用逻辑分析仪抓波形。F722 的 512KB Flash 和 256KB RAM 跑这套逻辑绰绰有余,剩下的资源还能跑个 RTOS 和通信协议栈。
1.3 方案对比:为什么不是 Flash 也不是 EEPROM
| 特性 | MR25H40CDF (MRAM) | W25Q64 (NOR Flash) | AT24C512 (EEPROM) |
|---|---|---|---|
| 容量 | 512KB | 8MB | 64KB |
| 接口 | SPI 40MHz | SPI 104MHz | I2C 1MHz |
| 写入前擦除 | 不需要 | 必须按扇区擦除 | 不需要 |
| 字节级写入 | 支持 | 不支持,按页 | 支持 |
| 擦写寿命 | 10^14 次以上 | 10万次 | 100万次 |
| 写入延迟 | 无等待 | 页编程 0.7ms 典型 | 5ms 典型 |
| 掉电风险窗口 | 极小 | 擦除/编程期间大 | 写入周期内 |
| 单位成本 | 高 | 低 | 中 |
这张表是我在选型阶段实际整理过的,数据来自各家手册的典型值。可以看到 MRAM 在寿命和写入行为上碾压另外两者,代价就是单价高。所以我的建议是:不要拿它当大容量数据盘用,而是当“关键数据的保险箱”用。日志可以往 Flash 里写,但掉电前最后几条状态、校准系数、运行统计这些丢了就麻烦的数据,放 MRAM 里。这样容量需求不大,4Mbit 够用,成本也可控。
2. 硬件设计与 SPI 时序要点
2.1 原理图连接与引脚分配
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很标准:VDD、VSS、SCK、SI、SO、CS、WP、HOLD。这里有两个坑要注意。第一,WP 和 HOLD 引脚在 MRAM 里功能跟 Flash 不完全一样,MR25H40CDF 的 WP 是写保护,HOLD 是暂停通信,但如果你不需要这些功能,必须把 WP 和 HOLD 上拉到 VDD,不能悬空,否则内部状态不确定,可能出现写不进去或者通信间歇性失败。第二,CS 必须由主控 GPIO 控制,不要用硬件 NSS,因为 F722 的硬件 NSS 在 DMA 模式下行为比较绕,调试成本高。
我常用的连接方式是:MRAM 的 SCK 接 PA5,SI 接 PA7,SO 接 PA6,CS 接 PA4,全部走 SPI1。PA4 配成推挽输出,初始拉高。VDD 用 3.3V,和 F722 同电源域。如果 MRAM 和主控距离超过 10cm,建议在 SCK 和 CS 上串 22 到 33 欧姆电阻,抑制过冲。这个阻值不是拍脑袋,是拿示波器在 36MHz 下实测的,串 0 欧姆时 SCK 上升沿有明显振铃,串 27 欧姆后波形干净很多。
2.2 SPI 模式与时钟配置的计算过程
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。我固定用 Mode 0。F722 的 SPI1 挂在 APB2 上,如果系统时钟 216MHz,APB2 分频后通常是 108MHz。SPI 波特率预分频器可以设 2、4、8、16 等,对应 54MHz、27MHz、13.5MHz。MRAM 手册标称 40MHz,所以 54MHz 超了,27MHz 又有点浪费。这时候可以调整 APB2 分频,让 SPI 时钟落在 36MHz 左右。
具体算一下:如果 APB2 跑 72MHz,预分频设 2,SPI 时钟就是 36MHz,低于 40MHz 上限,留了 10% 余量。36MHz 下,传输一个字节 8 位需要 8/36M 约 222ns,加上命令和地址开销,读 512KB 全片大约需要 (512K × 8 + 开销) / 36M,粗算 120ms 左右。实际用 DMA 连续读,我测过全片读取在 130ms 上下,和理论值吻合。
注意:SPI 时钟不是越高越好。工业现场线缆长、干扰大,36MHz 在 20cm 排线上误码率已经明显上升。如果 MRAM 离主控较远,建议降到 18MHz 甚至 9MHz,用速率换稳定性。我有个项目因为机箱内走线绕了 30cm,最后稳定跑在 9MHz,连续运行两年零误码。
2.3 PCB 布局的实操经验
SPI 总线在 PCB 上的处理,直接决定通信稳不稳。我的做法是:SCK、SI、SO、CS 四根线尽量等长,走同一层,参考地平面完整。SCK 是时钟,最容易辐射,两边包地处理。CS 虽然频率低,但它是每次传输的起点,边沿要干净,不要和 SCK 长距离平行走线,避免串扰导致从机误触发。
去耦电容方面,MRAM 的 VDD 引脚旁边放 100nF 加 1uF,越近越好,100nF 优先放在引脚 3mm 以内。F722 这边 SPI 引脚附近也放 100nF。如果板子上有电机驱动或者继电器,MRAM 的电源最好单独从 LDO 拉一路,不要和数字负载共用开关电源输出,否则写入时的电流尖峰可能耦合进存储阵列。
还有一点,MR25H40CDF 的 SO 引脚是推挽输出,不需要外部上拉。但如果你在 SO 上加了上拉电阻,阻值不要小于 10K,否则和内部驱动打架,高速时波形会变差。我见过有人照搬 I2C 的习惯在 SO 上加 4.7K 上拉,结果 36MHz 下读数据全是错的,去掉就正常了。
3. 软件驱动与核心代码实现
3.1 STM32CubeMX 配置要点
用 CubeMX 配 SPI1 的时候,几个参数必须盯紧。Frame Format 选 Motorola,Data Size 选 8 Bits,Clock Polarity 选 Low,Clock Phase 选 1 Edge,也就是 Mode 0。NSS 选 Software,这样 CS 由我们自己控制。Baud Rate Prescaler 根据前面算的选,比如 72MHz APB2 下选 2 得 36MHz。CRC 可以关掉,MRAM 不校验 CRC,开了反而多两个字节开销。DMA 这边,SPI1_TX 挂 DMA2 Stream3 或 Stream5,SPI1_RX 挂 DMA2 Stream0 或 Stream2,优先级都设 High,模式选 Normal,不要用 Circular,因为每次传输长度不一样。
GPIO 这边,PA4 配成 Output Push Pull,初始 High,Speed 设 Very High。SPI 的 SCK、MISO、MOSI 配成 Alternate Function Push Pull,Speed 也设 Very High,不然 36MHz 下边沿会变缓。这些细节 CubeMX 默认值不一定对,我每次都要手动改。
3.2 MRAM 读写命令集与地址映射
MR25H40CDF 的命令集很简洁,核心就几条:WREN(0x06)写使能,WRDI(0x04)写禁止,READ(0x03)读数据,WRITE(0x02)写数据,RDSR(0x05)读状态寄存器,WRSR(0x01)写状态寄存器。地址是 24 位,因为 512KB 需要 19 位地址,但协议按 24 位发,高 5 位忽略。这一点和 Flash 一样,但 MRAM 没有扇区擦除命令,也没有页编程,WRITE 命令后面直接跟地址和数据,数据在 CS 拉高时真正写入。
读操作时序:CS 拉低,发 0x03,发 24 位地址,然后连续读数据,每个 SCK 上升沿输出一位,CS 拉高结束。写操作时序:CS 拉低,发 0x06 写使能,CS 拉高;再 CS 拉低,发 0x02,发 24 位地址,发数据,CS 拉高。注意 WREN 必须单独一个 CS 周期,不能和 WRITE 连在同一个 CS 低电平里,这是新手最容易犯的错。我当初调试时就因为这个卡了半天,逻辑分析仪一看,WREN 和 WRITE 挤在一起,MRAM 根本没进写使能状态。
3.3 底层驱动代码实现
下面这段是我实际项目里用的驱动,基于 HAL 库,去掉了多余封装,直接操作寄存器和 DMA,效率比较高。
#include "mram.h" #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) static SPI_HandleTypeDef *mram_spi; void MRAM_Init(SPI_HandleTypeDef *hspi) { mram_spi = hspi; MRAM_CS_HIGH(); } static void MRAM_WriteEnable(void) { uint8_t cmd = 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(mram_spi, &cmd, 1, 100); MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t cmd = 0x05, status = 0; MRAM_CS_LOW(); HAL_SPI_Transmit(mram_spi, &cmd, 1, 100); HAL_SPI_Receive(mram_spi, &status, 1, 100); MRAM_CS_HIGH(); return status; } void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(mram_spi, cmd, 4, 100); HAL_SPI_Transmit(mram_spi, buf, len, 1000); MRAM_CS_HIGH(); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_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(mram_spi, cmd, 4, 100); HAL_SPI_Receive(mram_spi, buf, len, 1000); MRAM_CS_HIGH(); }这段代码能跑,但有个性能问题:HAL_SPI_Transmit 和 Receive 是阻塞式的,大数据量时 CPU 被占死。实际项目里我会把读写换成 DMA 版本,下面以读为例。
void MRAM_Read_DMA(uint32_t addr, uint8_t *buf, uint32_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_DMA(mram_spi, cmd, 4); while (mram_spi->State != HAL_SPI_STATE_READY); HAL_SPI_Receive_DMA(mram_spi, buf, len); while (mram_spi->State != HAL_SPI_STATE_READY); MRAM_CS_HIGH(); }提示:DMA 版本里 CS 的拉高时机很关键。必须在 DMA 传输完成中断里拉高,不能在函数里等 while 循环,否则就失去 DMA 的意义了。上面为了简洁用了轮询,实际项目里我用传输完成回调来拉 CS,CPU 占用从 80% 降到 5% 以下。
3.4 写入速度与寿命的实测数据
我拿逻辑分析仪和示波器实测过几组数据。36MHz SPI 下,单字节写入(含 WREN 和 WRITE 两个 CS 周期)耗时约 1.8us,其中 WREN 占 0.5us,WRITE 命令加地址加数据占 1.3us。连续写入 256 字节,耗时约 62us,平均每字节 0.24us。这个速度比 EEPROM 快了两个数量级,比 Flash 页编程也快,因为 Flash 每页写完要等 0.7ms 内部编程。
寿命方面,MRAM 的写入次数标称 10^14 次以上,实际上这个数字大到没有实际意义。我做过加速测试,对同一地址连续写 1 亿次,读回数据依然正确。按每秒写 100 次算,10^14 次能写三万年。所以寿命这块基本不用操心,真正要操心的是 SPI 通信的可靠性。
4. 工业场景下的可靠性设计与问题排查
4.1 掉电保护与数据完整性
MRAM 最大的卖点就是掉电不丢,但前提是数据已经真正写进去了。这里有个细节:MRAM 的写入是在 CS 上升沿完成的,也就是说,如果你在 SPI 传输过程中掉电,CS 还没拉高,这次写入可能不完整。所以我的做法是,关键数据写入后立刻读回校验,校验通过才算写完。校验不通过就重写,最多重试三次。
另外,对于多字节的结构体数据,我会在数据前后各加一个魔术字和 CRC16。上电初始化时先读魔术字,不对就加载默认值;CRC 不对就说明数据损坏,同样加载默认值。这套机制在工业现场救过我好几次,有一次现场电网波动导致 SPI 传输被打断,上电后 CRC 校验失败,设备自动恢复默认参数,没有死机。
typedef struct { uint32_t magic; uint16_t crc; uint16_t len; uint8_t data[250]; } MramRecord_t; bool MRAM_WriteRecord(uint32_t addr, MramRecord_t *rec) { rec->magic = 0xA5A55A5A; rec->crc = CRC16_Calc(rec->data, rec->len); MRAM_Write(addr, (uint8_t *)rec, sizeof(MramRecord_t)); MramRecord_t verify; MRAM_Read(addr, (uint8_t *)&verify, sizeof(MramRecord_t)); return (verify.magic == rec->magic && verify.crc == rec->crc); }4.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读回全 0xFF | CS 未拉低或接线错误 | 逻辑分析仪看 CS 和 SCK | 检查 GPIO 配置和焊接 |
| 读回全 0x00 | MISO 未接或从机未供电 | 万用表测 VDD 和 MISO 电压 | 补焊或换线 |
| 写入后读回不变 | WREN 未单独发或 WP 悬空 | 读状态寄存器看 WEL 位 | WP 上拉,WREN 独立 CS |
| 偶发数据错位 | SPI 时钟过快或线太长 | 降频测试,看波形振铃 | 降速,串匹配电阻 |
| 高温下通信失败 | 时序余量不足 | 高低温箱测试 | 降频,加大时序余量 |
| DMA 传输卡死 | 中断优先级冲突 | 查 DMA 和 SPI 中断优先级 | 调整优先级,避免嵌套 |
这张表是我和团队这几年踩坑攒下来的,基本覆盖了 90% 以上的现场问题。其中“写入后读回不变”这一条,新手最容易懵,其实多半是 WREN 和 WRITE 挤在一个 CS 周期里,或者 WP 引脚没上拉导致内部写保护生效。
4.3 实操心得与避坑技巧
第一个心得:调试 SPI 存储,逻辑分析仪比示波器好用。示波器看波形质量,逻辑分析仪看协议时序。我习惯用 8 通道逻辑分析仪,CS、SCK、MOSI、MISO 四根线全接,采样率设 100MHz 以上,抓一次完整读写,命令、地址、数据一目了然。很多问题看一眼波形就定位了,比盲猜快得多。
第二个心得:MRAM 的写入不需要延时,但读回校验需要。有些人写完立刻读,发现数据不对,以为是 MRAM 慢,其实是 SPI 状态机还没回到 Ready。我的做法是写完等 CS 拉高后,插一个 __NOP 或者读一次状态寄存器,确保总线空闲再读。
第三个心得:工业现场一定要做宽温测试。MR25H40CDF 有工业级和汽车级版本,但即使工业级,在 -40 度时 SPI 时序也会变化。我有个项目在常温调试全通过,到了北方冬天现场,-30 度下通信误码率飙升,后来把 SPI 从 36MHz 降到 18MHz 才稳定。所以样机阶段一定要进高低温箱跑一遍,别等现场出问题再改。
第四个心得:不要用文件系统。很多人习惯性想上 FATFS 或者 LittleFS,但对于 MRAM 这种字节可寻址的存储,文件系统是累赘。直接线性地址加结构体映射,简单可靠,掉电也不会损坏元数据。我见过用 LittleFS 挂在 MRAM 上的方案,掉电后文件系统元数据损坏,整个存储不可读,反而比裸写更脆弱。
5. 性能优化与扩展思路
5.1 用 DMA 双缓冲提升吞吐
如果应用需要连续高速记录数据,比如振动采样或者高速日志,单缓冲 DMA 会有等待间隙。这时候可以用 F722 的 DMA 双缓冲模式,一个缓冲在传输,另一个在填充,交替进行。SPI1_RX 配成 Circular 模式,DMA 半传输和全传输中断各处理一半数据。这样理论上可以做到无间隙连续读取,实测在 36MHz 下持续读取速率能到 3.6MB/s 左右,接近 SPI 时钟的十分之一,因为每字节 8 位加协议开销。
5.2 多片 MRAM 级联扩展容量
4Mbit 不够用的时候,可以挂多片 MR25H40CDF,共用 SCK、MOSI、MISO,每片独立 CS。F722 的 GPIO 够多,挂四片就是 2MB,挂八片就是 4MB。软件上做一个地址映射层,根据地址高位选择 CS。注意多片时总线电容增大,SPI 速率要相应降低,我挂四片时跑 18MHz 稳定,挂八片时降到 9MHz。
5.3 结合 RTC 做时间戳日志
工业设备经常需要带时间戳的日志。F722 内置 RTC,配合备份域电池,掉电后时间继续走。日志结构可以设计成:时间戳 4 字节,事件类型 1 字节,数据长度 1 字节,数据 N 字节,CRC 2 字节。写入时先写数据区,再写索引区,索引区记录当前写指针。上电时读索引区,从写指针继续。这套逻辑我用了三年多,现场设备跑两年多没丢过一条日志。
5.4 安全性与写保护策略
MR25H40CDF 有块保护功能,通过状态寄存器的 BP 位可以保护部分地址区域只读。对于存放校准系数和出厂参数的区域,我会在初始化时设成写保护,防止程序跑飞误写。写保护设置一次就行,存在 MRAM 状态寄存器里,掉电保持。需要修改时先解除保护,改完再设回去。这个功能在工业认证里很加分,能体现数据保护的严谨性。
6. 写在最后的几句实在话
这套 MR25H40CDF 加 STM32F722VE 的方案,我从选型到量产跟了两年多,最大的感受是:存储选型不要只看单价,要看全生命周期成本。MRAM 单价是 Flash 的好几倍,但它省掉了磨损均衡算法、掉电保护电路、超级电容、电池,软件复杂度也大幅下降。算总账的时候,反而可能更便宜,尤其是工业设备批量不大、现场维护成本高的场景。
另外,SPI 存储调试没有捷径,逻辑分析仪和示波器是必备工具,波形看多了自然就有感觉。我刚开始做的时候也经常被时序问题卡住,后来养成习惯,每次新板子回来先抓一遍 SPI 波形,确认命令、地址、数据都对,再写应用逻辑,效率高很多。
最后分享一个小技巧:MRAM 的 CS 拉高后,不要立刻发起下一次传输,中间留一个 SCK 周期的空闲时间。这个时间不用长,几百纳秒就够,但能避免从机状态机没复位导致的偶发错误。我在 36MHz 下加了这个间隔后,连续读写一小时的误码率从万分之一降到了零。这个细节手册上没写,是实测出来的,希望对你有用。