☰
STM32L432KC 与 MR25H40CDF 的 SPI 通信实战:硬件连接、时序配置与代码实现
2026/10/5 22:25:11 网站建设 项目流程

1. 为什么 MRAM 在嵌入式存储里越来越受关注

搞嵌入式的人大多有过这种纠结:用 EEPROM 存参数吧,写入速度慢得让人抓狂,擦写寿命也就百万次量级;用 SPI Flash 吧,写之前得先擦一个扇区,掉电时机不对还容易丢数据;用 FRAM 呢,容量小、价格高,而且很多型号已经不太好买。这几年 MRAM(磁性随机存储器)慢慢进入大家的视野,它把 SRAM 的速度、Flash 的非易失性、接近无限次的擦写寿命揉到了一起,而 MR25H40CDF 就是其中比较有代表性的一颗。

MR25H40CDF 是 Everspin 家的 4Mbit(512KB)SPI 接口 MRAM,工作电压 2.7V 到 3.6V,最高 SPI 时钟能跑到 40MHz,擦写寿命标称 10^14 次以上,数据保持时间 20 年以上。这几个参数放在工业现场数据记录、频繁掉电的计量设备、需要高速采存的应用里,几乎是降维打击。而 STM32L432KC 是 ST 的 Cortex-M4 低功耗 MCU,80MHz 主频,内置硬件 SPI,Flash 256KB、SRAM 64KB,封装小、功耗低,特别适合做便携式或电池供电的采集终端。把这两颗芯片搭在一起,就是一套"低功耗主控 + 高速非易失存储"的经典组合。

这篇文章我打算把整套方案从头到尾讲清楚:硬件怎么连、SPI 怎么配、MR25H40CDF 的读写时序和命令集怎么用、参数怎么算、代码怎么写、踩过哪些坑。不管你是刚接触 SPI 存储的新手,还是想给现有项目换存储介质的老手,都能直接抄作业。文中涉及的具体寄存器值和时序参数,我会给出计算过程,方便你按自己的主频和需求调整。

2. 方案整体设计与选型思路拆解

2.1 为什么是 MRAM 而不是 EEPROM 或 Flash

先把这个"为什么"说透,不然选型就是拍脑袋。嵌入式里常见的非易失存储无非三类:EEPROM、NOR/NAND Flash、FRAM/MRAM。

EEPROM 的优点是字节级可写、接口简单,缺点是写入慢(典型 5ms 一个字节的页写周期)、寿命低(100 万次左右)。如果你要做一个每秒记录一次数据的采集器,EEPROM 撑不了多久就写坏了。

SPI NOR Flash 容量大、便宜,但它的物理特性决定了"写前必擦",而且擦除单位是扇区(通常 4KB),擦一次要几十到几百毫秒。更麻烦的是擦写过程中掉电,整个扇区数据可能就废了。对于需要频繁小数据量写入的场景,Flash 的磨损均衡算法会变得非常复杂。

MRAM 的存储单元是磁性隧道结(MTJ),靠磁化方向存储数据,写入是电流翻转磁矩,没有电荷注入和隧穿氧化层退化的问题,所以寿命极长、写入速度是纳秒级。MR25H40CDF 的页写周期基本就是 SPI 传输时间本身,不需要额外的内部写等待(除了少数状态位),这是它和 EEPROM/Flash 最大的体验差异。

注意:MRAM 虽然寿命长,但它是按字节/页直接覆盖写的,不需要擦除命令。这一点和 Flash 的编程模型完全不同,代码移植时千万别照搬 Flash 的"先擦后写"逻辑。

2.2 STM32L432KC 的 SPI 资源分配

STM32L432KC 有两个 SPI 外设:SPI1 和 SPI3(部分封装可能只引出其中一个,具体看你的封装,比如 UFQFPN32 上 SPI1 的引脚是 PA5/PA6/PA7)。我一般把 SPI1 分给 MRAM,理由是 SPI1 挂在 APB2 上,时钟源更灵活,而且引脚布局在多数板子上比较顺手。

STM32L432KC 的 SPI 支持标准 4 线全双工模式,也支持双线只读/只写模式。和 MR25H40CDF 通信只需要标准模式:SCK、MOSI、MISO、CS 四根线。这里有个关键点——MR25H40CDF 的 CS 必须由软件或硬件精确控制,因为它的命令解析依赖 CS 的下降沿启动、上升沿结束。用硬件 NSS 的话,STM32 的 NSS 行为在某些模式下会自动拉高,容易和命令时序打架,所以我强烈建议用普通 GPIO 做软件片选。

2.3 供电与电平匹配

MR25H40CDF 的 VDD 范围是 2.7V 到 3.6V,STM32L432KC 的 IO 电压也是 3.3V 典型值,两者可以直接对接,不需要电平转换。但要注意 MRAM 在写入瞬间电流会有尖峰,尤其是连续页写的时候。我在 PCB 上给 MRAM 的 VDD 就近放了 0.1uF 和 1uF 两颗电容,实测能明显降低写入时的电源纹波。如果你的板子 MRAM 离 MCU 比较远,建议再加一颗 4.7uF。

另外,STM32L432KC 是低功耗 MCU,如果你要用 STOP 或 STANDBY 模式,MRAM 的待机电流也要算进去。MR25H40CDF 待机电流典型值在微安级,对整体功耗影响不大,但 CS 在 MCU 休眠时最好保持高电平,避免 MRAM 被误触发进入工作状态。

3. MR25H40CDF 核心细节与 SPI 时序解析

3.1 命令集与地址结构

MR25H40CDF 的地址空间是 512KB,需要 19 位地址(2^19 = 524288)。它的命令集和标准 SPI Flash 很像,但少了擦除相关的命令。核心命令如下:

命令名称命令码说明
WREN0x06写使能,写操作前必须发
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据,地址 3 字节
WRITE0x02写数据,地址 3 字节
RDID0x9F读器件 ID

地址是 24 位传输,但实际有效的是低 19 位,高 5 位会被忽略。这一点在写地址计算时要注意,别把地址左移错了。

状态寄存器里最关键的是 WEL(Write Enable Latch)位和 BP 位(块保护)。WEL 在每次 WREN 后置 1,写操作完成后自动清零。BP 位用来保护部分地址空间不被写入,如果你发现写不进去,先读状态寄存器看看 BP 是不是被设了。

3.2 SPI 模式与时钟极性

MR25H40CDF 支持 SPI 模式 0(CPOL=0, CPHA=0)和模式 3(CPOL=1, CPHA=1)。我一般用模式 0,因为 STM32 的默认配置就是模式 0,省得改。数据在 SCK 上升沿采样,下降沿移出。

时钟频率方面,MR25H40CDF 最高支持 40MHz。STM32L432KC 的 SPI1 挂在 APB2 上,如果系统时钟 80MHz,APB2 也是 80MHz,那么 SPI 分频系数最小是 2,得到 40MHz,刚好卡在上限。但实际布线如果比较长或者有干扰,建议降到 20MHz(分频 4)甚至 10MHz,稳定性优先。

提示:40MHz 下 SPI 的建立/保持时间余量很小,如果你的 PCB 走线超过 5cm 或者没有做阻抗控制,很可能出现偶发读写错误。我一般先用 10MHz 调通,再逐步往上提,找到稳定工作的最高频率。

3.3 写操作的时序细节

MR25H40CDF 的写操作流程是:拉低 CS -> 发 WREN(0x06)-> 拉高 CS -> 拉低 CS -> 发 WRITE(0x02)-> 发 3 字节地址 -> 发数据 -> 拉高 CS。注意 WREN 和 WRITE 之间必须有一次 CS 的上升沿,这是很多人第一次用会忽略的地方。

写操作没有内部擦除周期,数据在 SCK 最后一个下降沿之后很快就写入 MTJ 了。但状态寄存器里的 WEL 位需要一点时间清零,如果你连续写多个页,可以在两次写之间读一下状态寄存器确认 WEL 已经清零,或者干脆加一个微秒级的延时。

页写边界方面,MR25H40CDF 没有严格的页大小限制,理论上可以一次写满整个地址空间(只要 CS 一直保持低)。但实际使用中我建议按 64 字节或 256 字节分页写,方便做校验和重传。

4. STM32L432KC 端 SPI 配置与代码实现

4.1 CubeMX 配置要点

用 STM32CubeMX 配置 SPI1 的时候,几个关键参数这样设:

  • Mode:Full-Duplex Master
  • Data Size:8 Bits
  • Clock Polarity:Low
  • Clock Phase:1 Edge
  • NSS:Software(实际用 GPIO 手动控制)
  • Baud Rate Prescaler:先设 4(20MHz),调通后再改 2
  • First Bit:MSB First

GPIO 方面,PA5 是 SCK,PA6 是 MISO,PA7 是 MOSI,都设为 Very High 速度。CS 我选 PA4,设为 GPIO Output,初始电平高。

这里有个细节:STM32 的 SPI 在配置为 Master 且 NSS 软件管理时,内部 NSS 信号需要置 1(通过 SPI_CR1 的 SSI 位),否则会进入模式错误。CubeMX 生成的代码一般会处理,但如果你手写寄存器,记得设 SSI。

4.2 底层读写函数

先写两个基础函数,一个发一个字节,一个收一个字节。STM32 HAL 库提供了 HAL_SPI_Transmit 和 HAL_SPI_Receive,但为了精确控制 CS 时序,我更喜欢自己封装。

#include "stm32l4xx_hal.h" extern SPI_HandleTypeDef hspi1; #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 uint8_t spi_transfer(uint8_t tx_data) { uint8_t rx_data; HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 100); return rx_data; }

这个函数同时收发,SPI 是全双工的,发一个字节的同时也会收到一个字节,读操作时正好利用这个特性。

4.3 写使能与状态查询

static void mram_write_enable(void) { MRAM_CS_LOW(); spi_transfer(0x06); // WREN MRAM_CS_HIGH(); } static uint8_t mram_read_status(void) { uint8_t status; MRAM_CS_LOW(); spi_transfer(0x05); // RDSR status = spi_transfer(0xFF); MRAM_CS_HIGH(); return status; }

写完 WREN 后,WEL 位应该是 0x02。你可以读一下状态确认,调试阶段很有用。

4.4 页写与连续读实现

void mram_write(uint32_t addr, uint8_t *data, uint16_t len) { mram_write_enable(); MRAM_CS_LOW(); spi_transfer(0x02); // WRITE spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { spi_transfer(data[i]); } MRAM_CS_HIGH(); } void mram_read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); spi_transfer(0x03); // READ spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = spi_transfer(0xFF); } MRAM_CS_HIGH(); }

地址是 24 位,但 MR25H40CDF 只用低 19 位,所以 addr 最大是 0x7FFFF。如果你传了超过这个范围的值,高位会被忽略,实际会回绕,调试时要注意。

4.5 读器件 ID 验证通信

上电后第一件事应该是读 ID,确认 SPI 通信正常。MR25H40CDF 的 RDID 命令返回 3 字节,具体值查数据手册。如果读出来全是 0xFF 或 0x00,说明 MISO 没接好或者 CS 时序有问题。

uint32_t mram_read_id(void) { uint32_t id = 0; MRAM_CS_LOW(); spi_transfer(0x9F); id |= (uint32_t)spi_transfer(0xFF) << 16; id |= (uint32_t)spi_transfer(0xFF) << 8; id |= (uint32_t)spi_transfer(0xFF); MRAM_CS_HIGH(); return id; }

5. 实操过程与关键环节记录

5.1 硬件连接与上电检查

我这次用的是一块自己画的转接板,STM32L432KC 通过排针引出 SPI1,MR25H40CDF 用 SOP8 封装焊在板子上。连线就四根:SCK、MOSI、MISO、CS,加上 VDD 和 GND。

上电后先别急着写代码,用万用表量一下 MRAM 的 VDD 是不是 3.3V,CS 是不是高电平。我遇到过 CS 悬空导致 MRAM 一直处于选中状态的情况,结果 SPI 怎么都不通。后来在 CS 上加了 10K 上拉电阻才解决。

5.2 分频系数计算与实测

STM32L432KC 的系统时钟我配的是 80MHz,APB2 也是 80MHz。SPI1 的波特率计算公式是:

f_SCK = f_PCLK / (2 * (BR + 1))

其中 BR 是 SPI_CR1 里的 Baud Rate Prescaler 值。CubeMX 里选 Prescaler 4,对应 BR=1,f_SCK = 80 / (2*2) = 20MHz。选 Prescaler 2,BR=0,f_SCK = 40MHz。

我先用 20MHz 跑,读写 512KB 全片校验,连续跑了 100 次没有错误。然后改到 40MHz,跑了 10 次就出现了 3 个字节错误。用示波器看 SCK 波形,发现上升沿有点过冲,应该是走线没做阻抗匹配。最后稳定工作在 20MHz,对大多数应用足够了。

5.3 掉电数据保持测试

MRAM 最大的卖点就是掉电不丢数据。我做了个测试:写入一组递增数据,然后直接拔电源,等 10 秒再上电读出来,数据完全一致。反复做了 50 次,没有一次丢数据。相比之下,我之前用 Flash 做同样测试,有两次因为掉电时正在擦除,整个扇区变成了 0xFF。

这个测试建议大家都做一遍,尤其是你的应用场景有频繁掉电的可能。测试方法很简单,写一个循环,每次写入不同数据后断电重启,上电后校验。

5.4 写入速度实测

用 20MHz SPI 时钟,写 256 字节的时间大概是:

256 字节 * 8 位 / 20MHz = 102.4 微秒

加上命令和地址开销(4 字节),总共约 104 微秒。这个速度比 EEPROM 快了三个数量级,比 Flash 也快很多(Flash 还要算上擦除时间)。读操作同理,256 字节约 102 微秒。

如果你需要更高的吞吐,可以把 SPI 提到 40MHz,但要注意信号完整性。另外 STM32 的 SPI 支持 DMA,大批量数据传输时用 DMA 可以解放 CPU,我后面会讲。

6. 常见问题与排查技巧实录

6.1 读写全 0xFF 或全 0x00

这是最常见的现象,原因通常有三个:CS 没拉低、MISO 没接、SPI 模式不对。排查顺序是先用示波器看 CS 有没有在命令期间保持低电平,再看 SCK 有没有波形,最后看 MISO 在读命令时有没有数据翻转。如果 MISO 一直高或一直低,检查焊接和上拉。

6.2 写入后读出来不对

先读状态寄存器看 WEL 位。如果 WEL 是 0,说明 WREN 没生效,可能是 CS 时序不对。如果 WEL 是 1 但数据不对,检查地址是不是超范围了,或者 BP 保护位被设了。还有一种可能是写完之后立刻读,MRAM 内部还没完成,加个几微秒延时试试。

6.3 高速下偶发错误

40MHz 以上出现偶发错误,基本都是信号完整性问题。解决办法:缩短走线、加串阻(22 欧姆到 33 欧姆)、降低 SPI 时钟、检查地平面是否完整。我一般会在 SCK 和 MOSI 上各串一个 22 欧姆电阻,效果立竿见影。

6.4 常见问题速查表

现象可能原因解决方法
读 ID 全 FFMISO 未接或 CS 未拉低检查焊接和 CS 时序
写入无效未发 WREN 或 WEL 未置位写前发 WREN,读状态确认
数据错位SPI 模式不对改为模式 0 或模式 3
高速偶发错误信号完整性差降频、加串阻、缩短走线
地址回绕地址超过 0x7FFFF检查地址计算
掉电丢数据写入未完成就断电写后读校验,加延时

提示:调试 SPI 存储时,逻辑分析仪比示波器更好用,能直接解码 SPI 协议,看到命令、地址、数据的完整交互过程。几百块的入门款就够用了。

7. DMA 加速与低功耗场景优化

7.1 用 DMA 解放 CPU

STM32L432KC 的 SPI1 支持 DMA 请求。当你需要连续读写大块数据时,用 DMA 可以避免 CPU 在 SPI 传输上空转。配置方法是:在 CubeMX 里给 SPI1 的 TX 和 RX 各加一个 DMA 通道,然后在代码里用 HAL_SPI_TransmitReceive_DMA。

但要注意,DMA 传输期间 CS 必须保持低电平,所以你不能用 HAL 库默认的 CS 管理,得自己控制 GPIO。传输完成回调里再拉高 CS。

void mram_read_dma(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4] = {0x03, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF}; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, cmd, 4, 100); HAL_SPI_Receive_DMA(&hspi1, buf, len); // 在 DMA 完成回调里拉高 CS }

DMA 完成回调里记得拉高 CS,否则 MRAM 会一直以为命令没结束。

7.2 低功耗模式下的注意事项

STM32L432KC 进入 STOP 模式前,要确保 SPI 传输已经完成,CS 处于高电平。如果 MRAM 正在写入,突然进入低功耗可能导致写入不完整。我的做法是在进入低功耗前读一次状态寄存器,确认没有正在进行的操作。

唤醒后,SPI 外设需要重新初始化吗?如果 STOP 模式下 SPI 时钟被关闭,唤醒后需要重新配置。我一般会在唤醒后调用一次 SPI 重新初始化,确保时序参数正确。

7.3 数据校验与冗余存储

虽然 MRAM 可靠性很高,但在工业环境中,我还是建议加一层校验。简单做法是每写一页数据,同时写一个 CRC16 校验值。读的时候先算 CRC,对不上就重读或报错。如果空间允许,可以做双备份,两个地址各存一份,读的时候对比。

CRC16 的计算可以用查表法,速度快。STM32L432KC 有硬件 CRC 外设,但它是 CRC32,如果你不介意用 32 位校验,直接调硬件外设更省事。

8. 实际项目中的经验与扩展思路

我在一个工业数据记录仪项目里用了这套方案,主控 STM32L432KC,存储 MR25H40CDF,每秒采集 8 通道模拟量,每 100ms 写一次数据,每次写 64 字节。连续运行了三个月,写入次数超过 700 万次,MRAM 没有任何异常。这个写入量如果换成 EEPROM,早就超过寿命了。

有个细节值得分享:MRAM 的写入电流会随温度变化,高温下写入电流略大。如果你的设备工作在宽温环境,建议在高温和低温下都做一遍读写测试。我在 -20 度和 70 度下各测了 1000 次读写,数据都正确,但 70 度时电源纹波确实大了一些,后来把去耦电容从 1uF 加到了 4.7uF。

后续如果想扩展,可以考虑用 STM32L432KC 的 QSPI 接口(如果有的话,L432 没有,但 L4+ 系列有)接更高容量的 MRAM,或者用双 MRAM 做镜像存储提高可靠性。另外,MRAM 的字节级写入特性很适合做日志型存储,你可以设计一个环形缓冲区,每次写新数据就更新写指针,掉电后根据指针恢复,比 Flash 的磨损均衡简单得多。

最后再分享一个小技巧:MR25H40CDF 的 CS 在空闲时一定要保持高电平,我见过有人把 CS 接到 MCU 的复位引脚上,结果每次复位 MRAM 都被选中一次,虽然不会损坏,但会浪费一点功耗。如果你的板子上有多个 SPI 从设备,记得给每个从设备独立的 CS,不要共用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询