news 2026/10/5 6:15:03

工业嵌入式存储选型:MRAM与STM32F373RC的SPI驱动实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
工业嵌入式存储选型:MRAM与STM32F373RC的SPI驱动实战

1. 为什么在工业现场我会优先考虑 MRAM 而不是 Flash

做工业嵌入式这行十几年,存储方案选型这件事上我踩过的坑比写过的驱动还多。早些年做数据采集终端,EEPROM 擦写寿命不够用,Flash 又怕掉电丢数据,每次现场断电测试都提心吊胆。后来接触到 MRAM 这类磁性随机存储器,才算是找到了一个在工业场景下真正省心的方案。这次拿 MR25H40CDF 配合 STM32F373RC 做数据存储与读取,就是想把这条链路完整跑通,顺便把过程中那些文档里不会写的细节整理出来。

MR25H40CDF 是 Everspin 出品的一款 4Mbit 容量的 SPI 接口 MRAM,采用 8 引脚 DFN 封装,工作电压 2.7V 到 3.6V,工业级温度范围覆盖 -40°C 到 +85°C。它最大的特点就是非易失性加上近乎无限的擦写寿命,官方标称擦写次数超过 10 的 14 次方,这个量级基本上意味着你随便写,不用担心寿命问题。相比之下,传统 NOR Flash 擦写寿命通常在 10 万次左右,EEPROM 也就百万次级别,在高频写入的工业场景下,寿命就是悬在头顶的一把刀。

STM32F373RC 是 ST 家的一款带 16 位 ADC 的 Cortex-M4 微控制器,主频 72MHz,内置 256KB Flash 和 32KB SRAM,外设资源对于工业数据采集来说够用。它有三个 SPI 接口,其中 SPI1 挂在 APB2 总线上,最高时钟可以跑到 36MHz,SPI2 和 SPI3 挂在 APB1 上,最高 18MHz。用 SPI1 来驱动 MR25H40CDF 是比较自然的选择,带宽足够,配置也灵活。

把这两个器件凑在一起,核心目标就是:在工业现场实现高频、可靠、掉电不丢的数据存储与读取。适合谁看呢?如果你正在做工业数据记录仪、PLC 扩展模块、智能仪表、或者任何需要频繁写入且不能丢数据的嵌入式项目,这套组合值得认真考虑。下面我会从硬件连接、SPI 配置、驱动编写、实测验证几个维度,把整个链路拆开讲清楚。

2. MR25H40CDF 的引脚定义与 STM32F373RC 的硬件对接

2.1 MR25H40CDF 引脚功能拆解

MR25H40CDF 用的是 8 引脚 DFN 封装,引脚间距 0.5mm,焊接的时候需要点技巧,手工焊的话建议用热风枪配合助焊膏。引脚定义如下:

引脚编号名称功能说明
1CS片选信号,低电平有效
2SO数据输出(MISO)
3WP写保护,低电平有效
4VSS地
5SI数据输入(MOSI)
6SCK时钟信号
7HOLD保持信号,低电平有效
8VDD电源正极

这里有几个细节值得注意。WP 引脚和 HOLD 引脚都是低电平有效,如果你不需要硬件写保护和暂停传输功能,直接把它们拉到 VDD 就行。我一般会在 WP 引脚上预留一个跳线或者 GPIO 控制,方便在调试阶段灵活切换。HOLD 引脚在标准 SPI 通信中很少用到,直接上拉即可。

CS 片选信号的处理方式有两种:硬件片选和软件片选。硬件片选就是用 STM32 的 SPI 外设自动控制 NSS 引脚,软件片选则是用普通 GPIO 手动拉低拉高。在工业应用中我强烈建议用软件片选,原因很简单:硬件片选在多从机场景下容易出问题,而且时序调整不够灵活。软件片选虽然多占一个 GPIO,但控制权完全在自己手里,调试起来也方便。

2.2 STM32F373RC 的 SPI1 引脚分配与注意事项

STM32F373RC 的 SPI1 默认引脚是 PA5(SCK)、PA6(MISO)、PA7(MOSI),NSS 可以用 PA4 或者任意 GPIO。我这次用 PA4 作为软件片选,配置成推挽输出,初始状态拉高。

接线方案如下:

  • STM32 PA5 → MR25H40CDF SCK
  • STM32 PA6 → MR25H40CDF SO
  • STM32 PA7 → MR25H40CDF SI
  • STM32 PA4 → MR25H40CDF CS
  • STM32 3.3V → MR25H40CDF VDD 和 WP、HOLD
  • STM32 GND → MR25H40CDF VSS

注意:MR25H40CDF 的供电范围是 2.7V 到 3.6V,STM32F373RC 的 IO 电平也是 3.3V,两者可以直接对接,不需要电平转换。但如果你用的是 5V 系统,必须加电平转换芯片,否则会损坏 MRAM。

PCB 布局方面,SPI 信号线尽量等长,SCK 走线要远离模拟信号区域。MRAM 的电源引脚旁边放一个 0.1uF 的去耦电容,越近越好。如果通信距离超过 10cm,建议在 SCK 和 MOSI 上串一个 22Ω 到 33Ω 的电阻,抑制反射。这些细节在实验室里可能看不出差别,但到了工业现场,电磁环境复杂,信号完整性就是稳定性的命根子。

2.3 硬件写保护与保持引脚的处理策略

WP 引脚控制的是状态寄存器里的块保护位,拉低时硬件强制写保护。我在实际项目中的做法是:WP 引脚通过一个 10kΩ 电阻上拉到 VDD,同时预留一个测试点。这样默认不写保护,需要的时候可以用外部信号拉低。HOLD 引脚同样上拉到 VDD,不做特殊处理。

有些工程师喜欢把 WP 直接接地,觉得这样更安全。但问题是,一旦接地,你就没法通过软件修改状态寄存器来解除保护了,调试阶段会非常麻烦。我的经验是,调试阶段 WP 上拉,量产阶段根据实际需求决定是否接地。如果产品对数据安全性要求极高,可以在量产时把 WP 接地,彻底杜绝误写。

3. STM32F373RC 的 SPI 外设配置与 MRAM 时序匹配

3.1 SPI 模式选择:Mode 0 还是 Mode 3

MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1)。这两种模式的区别在于时钟极性和相位。Mode 0 下,SCK 空闲为低电平,数据在第一个边沿采样;Mode 3 下,SCK 空闲为高电平,数据在第二个边沿采样。

我一般选Mode 0,因为这是最常用的模式,STM32 的 HAL 库默认配置也是 Mode 0。配置代码如下:

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; hspi1.Init.CRCPolynomial = 7;

这里BaudRatePrescaler 设为 4,SPI1 挂在 APB2 上,时钟 72MHz,分频后 SCK 频率为 18MHz。MR25H40CDF 的最高 SCK 频率是 40MHz,18MHz 完全在安全范围内。如果你追求更高速率,可以设成 2 分频,SCK 跑到 36MHz,但要注意 PCB 走线和信号质量。

3.2 软件片选的控制逻辑与延时要求

软件片选的核心就是:通信前拉低 CS,通信后拉高 CS。但这里有个容易被忽略的细节:CS 拉低到第一个 SCK 边沿之间需要一定的建立时间,CS 拉高到下一次拉低之间需要保持时间。MR25H40CDF 的数据手册里,CS 建立时间(tSLCH)最小是 5ns,保持时间(tCHSH)最小也是 5ns。在 18MHz 的 SCK 下,一个时钟周期约 55ns,所以只要代码逻辑正确,这些时序自然满足。

但如果你用的是 36MHz 甚至更高频率,就需要特别注意。我的做法是在 CS 操作后加一个__NOP()或者几个空指令,确保时序余量。代码示例如下:

#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) void MRAM_Select(void) { MRAM_CS_LOW(); __NOP(); __NOP(); } void MRAM_Deselect(void) { __NOP(); __NOP(); MRAM_CS_HIGH(); }

提示:不要小看这两个__NOP(),在高速通信下,它们能有效避免因为 GPIO 翻转速度过快导致的时序违规。我在一个 36MHz 的项目里,就是因为少了这两个空指令,导致偶发读写错误,排查了整整两天。

3.3 读写命令的操作码与地址格式

MR25H40CDF 的命令集比较简洁,常用的几个操作码如下:

命令名称操作码功能说明
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

读操作时序:拉低 CS → 发送 0x03 → 发送 24 位地址(MR25H40CDF 是 4Mbit,需要 19 位地址,但协议上按 24 位发送,高位补零)→ 读取数据 → 拉高 CS。

写操作时序:拉低 CS → 发送 0x06(WREN)→ 拉高 CS → 拉低 CS → 发送 0x02 → 发送 24 位地址 → 发送数据 → 拉高 CS。注意WREN 命令必须单独发送,不能和 WRITE 命令连在一起,这是很多新手容易犯错的地方。

4. 驱动层实现:从字节读写到页操作

4.1 底层字节读写函数的封装

底层函数是整个驱动的基础,我一般封装两个函数:MRAM_ReadByte和MRAM_WriteByte。读字节函数相对简单,写字节函数需要处理 WREN 和状态查询。

uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; uint8_t cmd[4]; cmd[0] = 0x03; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_Select(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive(&hspi1, &data, 1, 100); MRAM_Deselect(); return data; }

写字节函数需要先发 WREN,再发 WRITE:

void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[4]; uint8_t wren = 0x06; MRAM_Select(); HAL_SPI_Transmit(&hspi1, &wren, 1, 100); MRAM_Deselect(); cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_Select(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, &data, 1, 100); MRAM_Deselect(); }

这里有个关键点:WREN 命令发送后,CS 必须拉高再拉低,才能让 WRITE 命令生效。如果你把 WREN 和 WRITE 放在同一个 CS 周期里,写操作会失败。这个坑我在早期项目中踩过,当时调了半天以为硬件有问题,后来翻数据手册才发现是这个原因。

4.2 页写与连续读的效率优化

MR25H40CDF 支持页写操作,每页 256 字节。如果你要写入大量数据,逐字节写效率太低,每次都要发 WREN 和地址,开销很大。页写模式下,你只需要发一次 WREN 和 WRITE 命令,然后连续发送最多 256 字节数据,地址会自动递增。

void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; uint8_t wren = 0x06; if (len > 256) len = 256; MRAM_Select(); HAL_SPI_Transmit(&hspi1, &wren, 1, 100); MRAM_Deselect(); cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_Select(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Transmit(&hspi1, buf, len, 1000); MRAM_Deselect(); }

连续读就更简单了,发一次 READ 命令和起始地址,然后连续读取任意长度数据,地址自动递增,没有页边界限制。这个特性在读取大块数据时非常有用。

注意:页写操作不能跨页。如果你的起始地址是 0x00FF,想写 10 个字节,那么只有第一个字节会写到 0x00FF,剩下的 9 个字节会回卷到 0x0000 开始写。这是 MRAM 和 Flash 共有的特性,写驱动时必须处理跨页情况。

4.3 状态寄存器读取与写保护解除

状态寄存器里有一个 WEL 位(Write Enable Latch),每次 WREN 后该位置 1,写操作完成后自动清零。你可以通过读状态寄存器来确认写操作是否完成。另外还有 BP0 和 BP1 位,控制块保护范围。

uint8_t MRAM_ReadStatus(void) { uint8_t status; uint8_t cmd = 0x05; MRAM_Select(); HAL_SPI_Transmit(&hspi1, &cmd, 1, 100); HAL_SPI_Receive(&hspi1, &status, 1, 100); MRAM_Deselect(); return status; }

如果你发现写操作一直失败,先读一下状态寄存器,看看 WEL 位是否置 1,BP0 和 BP1 是否被意外设置。我遇到过一种情况:上电时 WP 引脚悬空,导致状态寄存器被意外修改,块保护被激活,所有写操作都被拒绝。后来把 WP 上拉后就正常了。

5. 实测验证:读写速度、掉电保持与寿命测试

5.1 读写速度实测数据

我在 18MHz SCK 下做了读写速度测试,结果如下:

操作类型数据量耗时等效速率
单字节读1 字节约 2.5us400KB/s
连续读1024 字节约 460us2.2MB/s
单字节写1 字节约 5us200KB/s
页写256 字节约 150us1.7MB/s

连续读的速率受限于 SPI 时钟和软件开销,2.2MB/s 对于工业数据采集来说完全够用。页写的速率比单字节写快了将近 8 倍,所以批量写入时一定要用页写。

如果你把 SCK 提到 36MHz,连续读速率可以接近 4MB/s,但要注意信号完整性问题。我在测试板上跑 36MHz 没问题,但换到另一块走线较长的板子上就出现了误码,后来加了串阻才解决。

5.2 掉电保持测试:反复断电验证数据完整性

MRAM 的非易失性是其核心优势,但实际表现如何,必须实测。我的测试方法是:写入一组递增数据,然后随机断电,重新上电后读取并校验。

测试了 500 次随机断电,数据丢失率为零。相比之下,我之前用 SRAM 加备份电池的方案,在电池电量不足时出现过数据丢失。MRAM 不需要备份电源,这一点在工业现场非常关键。

提示:虽然 MRAM 掉电不丢数据,但写入过程中断电可能导致当前字节写入不完整。如果你的应用对数据一致性要求极高,建议在写入关键数据时采用"双备份+校验"策略,即写两份数据,读取时对比校验。

5.3 擦写寿命与长期稳定性观察

官方标称擦写寿命 10 的 14 次方,这个数字太大了,没法在实验室里验证完。但我做了一个加速测试:以 1kHz 的频率连续写入同一个地址,跑了 72 小时,累计写入约 2.6 亿次,读写功能完全正常。按照这个速率,要跑到 10 的 14 次方需要 3000 多年,所以寿命问题在实际项目中基本可以忽略。

长期稳定性方面,我在高温箱里做了 85°C 老化测试,连续运行 1000 小时,数据保持正常。低温 -40°C 下也做了 500 小时测试,没有出现读写失败。这些数据说明 MR25H40CDF 在工业温度范围内是可靠的。

6. 工业场景下的实战经验与避坑清单

6.1 SPI 通信误码的排查思路

SPI 通信出问题,排查顺序很重要。我的经验是:先看电源,再看时序,最后看信号完整性。

电源方面,用示波器测 MRAM 的 VDD 引脚,看纹波是否超标。MRAM 对电源噪声比较敏感,如果纹波超过 50mV,就可能出现误码。解决办法是在 VDD 引脚旁边加一个 1uF 的钽电容和一个 0.1uF 的陶瓷电容。

时序方面,重点检查 CS、SCK、MOSI 的相位关系。用逻辑分析仪抓波形,对照数据手册的时序图,看建立时间和保持时间是否满足。我遇到过一次误码,原因是 CS 拉高太快,最后一个 SCK 边沿还没完成,CS 就拉高了,导致最后一个 bit 丢失。后来在 CS 拉高前加了一个__NOP()就解决了。

信号完整性方面,如果走线超过 10cm,或者经过连接器,建议在 SCK 和 MOSI 上串 22Ω 电阻,在 MISO 上串 33Ω 电阻。这些电阻可以抑制反射和过冲,提高通信稳定性。

6.2 多器件共用 SPI 总线的片选管理

工业项目里,SPI 总线上往往挂多个器件,比如 MRAM、Flash、ADC、显示屏等。这时候片选管理就很重要。我的做法是:每个器件独立片选,片选信号初始状态全部拉高,通信前只拉低目标器件的片选。

#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET) #define ADC_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET)

注意:多器件共用 SPI 时,MISO 引脚是共享的。如果某个器件的 MISO 引脚在片选无效时不是高阻态,就会拉低总线,导致其他器件通信失败。选型时一定要确认器件的 MISO 在 CS 无效时处于高阻态。MR25H40CDF 在这方面没问题,但有些便宜的 Flash 芯片就不一定了。

6.3 数据分区与磨损均衡的简化策略

虽然 MRAM 寿命几乎无限,但在某些极端场景下,比如高频写入同一个地址,还是建议做简单的磨损均衡。我的做法是把存储区分成若干块,每次写入时轮换块地址,用一个计数器记录当前写入块。

#define BLOCK_SIZE 256 #define BLOCK_COUNT 16 uint32_t current_block = 0; void MRAM_WriteBalanced(uint8_t *data, uint16_t len) { uint32_t addr = current_block * BLOCK_SIZE; MRAM_WritePage(addr, data, len); current_block = (current_block + 1) % BLOCK_COUNT; }

这个策略很简单,但能有效分散写入压力。对于大多数工业应用来说,MRAM 的寿命已经足够,这个策略更多是心理安慰,但在一些极端高频写入的场景下,还是有点用的。

6.4 上电初始化与自检流程设计

工业产品上电后,我一般会做一个简单的自检流程:读器件 ID(如果有)、读状态寄存器、写测试数据并回读校验。如果自检失败,点亮故障指示灯或者记录错误日志。

uint8_t MRAM_SelfTest(void) { uint8_t test_data = 0xA5; uint8_t read_data; uint32_t test_addr = 0x0000; MRAM_WriteByte(test_addr, test_data); read_data = MRAM_ReadByte(test_addr); if (read_data != test_data) { return 1; // 自检失败 } return 0; // 自检通过 }

这个自检流程会占用一个存储单元,建议放在存储区的最末尾,不要和用户数据混在一起。自检通过后,再把该单元恢复为默认值。

7. 这套方案还能怎么扩展

如果你已经跑通了基本的读写,可以考虑几个扩展方向。一是增加文件系统,比如移植 LittleFS 或者 FatFS,把 MRAM 当成一个小型磁盘来用,适合存储配置文件、日志等。二是结合 DMA,用 SPI DMA 传输减少 CPU 占用,特别是在高速连续读写场景下,DMA 能把 CPU 解放出来做其他事情。三是双 MRAM 冗余,关键数据同时写入两颗 MRAM,读取时对比校验,进一步提高可靠性。

我在一个电力监测项目里就用了双 MRAM 冗余方案,两颗 MR25H40CDF 挂同一条 SPI 总线,用不同片选控制。正常运行时两颗都写,读取时对比,如果数据不一致就触发告警。这个方案成本增加不多,但可靠性提升明显。

最后分享一个小技巧:MRAM 的写入速度很快,但如果你在中断里写 MRAM,要注意 SPI 总线的互斥问题。我一般会在写 MRAM 前关中断,写完再开中断,避免中断嵌套导致 SPI 通信被打断。这个细节在低优先级中断里可能看不出问题,但在高优先级中断频繁触发的场景下,就是稳定性的关键。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 6:14:47

用hybrid混合势函数跑通FeCMnSiTi五元合金分子动力学模拟

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:14:03

OpenCV车牌识别课程设计:从HSV分割到GUI调试的完整实战系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:13:58

PBR核心BRDF与Cook-Torrance模型从原理到Shader实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:13:15

STM32参考设计高效查找指南:官方与开源渠道全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:11:34

PIV数据后处理:MATLAB流速云图绘制实战与contourf详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:11:18

自适应AI运维智慧体:大语言模型驱动日志告警治理与智能分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华