拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F215RE 与 MR25H40CDF MRAM 的 SPI 驱动实战

STM32F215RE 与 MR25H40CDF MRAM 的 SPI 驱动实战

1. 为什么偏偏选 MR25H40CDF 这颗 MRAM

1.1 从一次掉电丢数据的现场说起

前两年做一个工业数据采集终端,主控用的是 STM32F215RE,外挂一颗常见的 SPI NOR Flash 存配置和运行日志。设备装在配电柜里,现场偶尔会瞬断,结果每次断电重启后,最近几分钟的计量数据就没了——因为 NOR Flash 写之前要先擦除一个扇区,擦除过程中掉电,整个扇区就废了。后来换成带超级电容的方案,成本上去了,板子也变大了,客户还不满意。

再后来接触到 MR25H40CDF 这颗磁阻随机存储器(MRAM),才算把这个问题从根上解决。它最大的特点就是写入不需要擦除、字节级可写、掉电不丢、写入次数近乎无限。这几点对工业现场那种“随时可能断电、又要频繁记录”的场景来说,几乎是量身定做的。

所以这篇东西,我就围绕“STM32F215RE + MR25H40CDF 通过 SPI 存取数据”这条主线,把选型逻辑、硬件连接、SPI 时序、驱动代码、踩坑经验完整讲一遍。适合正在做工业采集、嵌入式数据记录、需要频繁小数据量写入的同行参考,也适合刚接触 SPI 外设、想找一个靠谱非易失存储方案的朋友。

1.2 MR25H40CDF 到底是个什么东西

先把这颗芯片的底细说清楚。MR25H40CDF 是 Everspin 家的 4Mbit(512K × 8)串行 MRAM,接口是标准 SPI,工作电压 2.7V ~ 3.6V,最高 SPI 时钟可以跑到 40MHz。它的存储单元是磁性隧道结(MTJ),靠磁化方向存数据,不是靠电荷。

这个物理原理决定了它和 Flash、EEPROM 的本质区别:

  • 不需要擦除:Flash 写之前必须擦,MRAM 直接覆盖写,像 SRAM 一样。
  • 字节级写入:可以只写一个字节,不用整页操作。
  • 写入寿命几乎无限:官方标称 10^14 次以上,实际用起来基本不用考虑寿命。
  • 写入速度快:没有擦除等待,单字节写入时间在纳秒到微秒级。
  • 掉电即存:写进去就稳定了,不需要等待内部编程完成。

对比一下常见的几种非易失存储,差距一目了然:

特性MR25H40CDF (MRAM)SPI NOR FlashSPI EEPROM
写入前擦除不需要必须整扇区擦不需要
最小写入单位1 字节1 页(通常 256B)1 字节
写入寿命10^14 次约 10^5 次约 10^6 次
写入速度极快,无擦除等待慢,擦除几十 ms较慢,约 5ms/字节
容量4Mbit常见 8~128Mbit常见 1K~512Kbit
成本偏高低中

容量上 MR25H40CDF 只有 512KB,比不了大容量 Flash,但工业场景里存配置、存关键日志、存掉电前的现场快照,这个容量完全够用。它的定位不是替代大容量存储,而是替代“频繁写、怕掉电、怕擦坏”的那一小块关键区域。

1.3 为什么主控选 STM32F215RE

STM32F215RE 是 ST 家的 Cortex-M3,主频 120MHz,带 512KB Flash、128KB SRAM,外设资源很全,SPI 有 3 个(SPI1/2/3),都支持硬件 NSS 和 DMA。选它配 MR25H40CDF,主要看中几点:

第一,SPI 速率够。F215 的 SPI 在 120MHz 主频下,分频后轻松跑到 30MHz 以上,能发挥 MRAM 的高速优势。第二,DMA 支持完善,做大数据块搬运时 CPU 占用低。第三,工业温度范围版本(-40~85℃ 或 105℃)好买,和 MRAM 的工业级温度匹配。第四,生态成熟,HAL 库、标准库资料多,调试方便。

提示:MR25H40CDF 有工业级和汽车级版本,选型时注意看后缀,工业现场建议选 -40~85℃ 以上的型号,别图便宜用商业级。

2. 硬件连接与 SPI 模式的关键细节

2.1 引脚连接与片选处理

MR25H40CDF 是 8 脚封装,SPI 接口引脚定义很标准:

  • VDD / VSS:电源和地,就近放 0.1uF 去耦电容。
  • SCK:SPI 时钟,接 STM32 的 SPIx_SCK。
  • SI(MOSI):主机输出,接 SPIx_MOSI。
  • SO(MISO):主机输入,接 SPIx_MISO。
  • CS#:片选,低有效。
  • WP# / HOLD#:写保护和保持引脚,不用的话上拉到 VDD。

片选这块我要多说一句。STM32 的 SPI 支持硬件 NSS 和软件片选两种方式。工业项目里我强烈建议用软件片选,也就是随便找一个 GPIO 手动拉低拉高。原因很简单:硬件 NSS 在多主或者时序要求严格的场景下容易出幺蛾子,而且一旦配置不当,NSS 会在字节间隙自动抖动,MRAM 可能把中间的抖动误判成命令边界。软件片选虽然多写两行代码,但时序完全可控,调试时也方便用示波器抓。

// 软件片选宏定义,假设 CS 接在 PA4 #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)

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

MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。这两种模式的区别在于时钟空闲电平和采样边沿:

  • Mode 0:时钟空闲低电平,第一个边沿(上升沿)采样。
  • Mode 3:时钟空闲高电平,第二个边沿(上升沿)采样。

实际用哪个?我一般选Mode 0,因为它是绝大多数 SPI 器件的默认模式,调试时不容易和其他外设冲突。配置的时候在 STM32CubeMX 里把 CPOL 设成 Low、CPHA 设成 1 Edge 就行。

这里有个容易踩的坑:STM32 的 CPHA 配置和很多资料里的描述是反的。HAL 库里CLKPolarity对应 CPOL,CLKPhase对应 CPHA,但SPI_PHASE_1EDGE表示第一个边沿采样,对应 CPHA=0。很多人照着“CPHA=1”去配SPI_PHASE_2EDGE,结果读出来全是 0xFF 或者 0x00,查半天查不出来。记住:Mode 0 = CPOL Low + 1 Edge。

2.3 时钟速率与信号完整性

MRAM 支持 40MHz,但实际能跑多快取决于你的 PCB 走线和 STM32 的分频。STM32F215 的 SPI1 挂在 APB2 上,假设 APB2 是 120MHz,分频系数选 4 就是 30MHz,选 2 就是 60MHz(超了 MRAM 上限,不行)。所以 30MHz 是个稳妥的高速选择。

但我要提醒一句:别一上来就冲 30MHz。先用低速(比如 1MHz 或 2MHz)把驱动调通,确认读写正确,再逐步往上提。走线长、没有阻抗控制、旁边有开关电源的板子,30MHz 下 MISO 上很容易出现振铃和过冲,读出来的数据高位会随机翻转。我遇到过一块板子,2MHz 下一切正常,提到 20MHz 就偶尔读错,最后在 SCK 和 MISO 上各串了一个 22Ω 电阻才稳住。

注意:SPI 走线尽量等长、远离高频干扰源,MISO 是输入线,最容易受干扰,必要时加串联电阻或小电容滤波。

3. 驱动实现:从命令码到读写函数

3.1 MR25H40CDF 的命令集

这颗芯片的命令集很简洁,常用的就几条:

命令编码作用
WREN0x06写使能,写之前必须发
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

注意,虽然 MRAM 不需要擦除,但写操作前仍然要发 WREN(0x06),这是芯片的写保护机制,不是 Flash 那种擦除逻辑。发完 WREN 后状态寄存器的 WEL 位会置 1,写完自动清零。如果你忘了发 WREN,写命令会被忽略,数据写不进去,而且不会报错,很容易误以为驱动有问题。

3.2 底层字节收发函数

STM32 HAL 库提供了HAL_SPI_Transmit和HAL_SPI_Receive,但做 MRAM 驱动时,最方便的是自己封装一个“收发一个字节”的函数,因为 SPI 是全双工的,发命令的同时就在收数据。

uint8_t MRAM_SPI_Transfer(uint8_t txData) { uint8_t rxData = 0; HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, 100); return rxData; }

这个函数是整个驱动的地基。发命令、发地址、发数据、读数据,全靠它。注意超时时间别设太短,虽然 MRAM 快,但调试阶段给个 100ms 比较稳。

3.3 写使能与状态查询

写之前先发 WREN,然后可以读状态寄存器确认 WEL 位。虽然实际项目里为了速度经常省略确认,但调试阶段建议加上,能快速定位问题。

void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x06); // WREN MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); MRAM_SPI_Transfer(0x05); // RDSR status = MRAM_SPI_Transfer(0xFF); MRAM_CS_HIGH(); return status; }

状态寄存器 bit1 是 WEL,写完 WREN 后读一下,如果是 0x02 就说明写使能成功了。

3.4 单字节与多字节写入

MRAM 的写命令格式是:命令码 0x02 + 16 位地址(高字节在前)+ 数据。地址是 19 位(512KB = 2^19),但用 3 个字节发地址时,最高几位是无关位,直接填 0 就行。

void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); // WRITE MRAM_SPI_Transfer((addr >> 16) & 0xFF); MRAM_SPI_Transfer((addr >> 8) & 0xFF); MRAM_SPI_Transfer(addr & 0xFF); MRAM_SPI_Transfer(data); MRAM_CS_HIGH(); }

多字节写入就是把数据循环发出去,地址自动递增,不需要每字节都发命令。这是 MRAM 比 EEPROM 爽的地方——EEPROM 跨页写要处理页边界,MRAM 完全不用管,一口气写到末尾都行。

void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr >> 16) & 0xFF); MRAM_SPI_Transfer((addr >> 8) & 0xFF); MRAM_SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { MRAM_SPI_Transfer(buf[i]); } MRAM_CS_HIGH(); }

3.5 读取实现

读命令 0x03,格式和写一样,发完地址后连续读,地址自动递增。

void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); MRAM_SPI_Transfer(0x03); // READ MRAM_SPI_Transfer((addr >> 16) & 0xFF); MRAM_SPI_Transfer((addr >> 8) & 0xFF); MRAM_SPI_Transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = MRAM_SPI_Transfer(0xFF); } MRAM_CS_HIGH(); }

读的时候发 0xFF 是惯例,因为 MOSI 上的数据无所谓,MISO 上才是芯片返回的数据。

4. 工业场景下的数据组织与可靠性设计

4.1 存储分区规划

512KB 看着不大,但规划好了很够用。我一般这么分:

  • 0x00000 ~ 0x00FFF(4KB):设备参数区,存序列号、校准系数、通信配置。
  • 0x01000 ~ 0x01FFF(4KB):参数备份区,双份存储,读的时候校验。
  • 0x02000 ~ 0x7FFFF(约 504KB):循环日志区,按记录结构循环覆盖。

参数区做双备份是工业项目的常规操作。主份读出来 CRC 不对,就切到备份区,同时把主份修复。MRAM 写入快,双写几乎不增加时间开销。

4.2 掉电保护与写入原子性

MRAM 虽然掉电不丢,但多字节写入过程中掉电,仍然可能出现“写了一半”的情况。比如你要更新一条 32 字节的记录,写到第 16 字节时断电,这条记录就残缺了。

解决办法是加一个“提交标志”或者用双缓冲。我的做法是每条记录末尾加 2 字节 CRC,再加 1 字节有效标志。写入顺序是:先写数据体,再写 CRC,最后写有效标志。读的时候先看有效标志,再看 CRC,都对了才认这条记录。这样即使写一半掉电,有效标志没写上去,这条记录会被判为无效,不会污染数据。

typedef struct { uint32_t timestamp; float value[4]; uint16_t crc; uint8_t valid; } LogRecord_t;

提示:有效标志一定要最后写,而且单独一次写操作完成,别和数据混在一个 buffer 里一次性发出去,否则掉电时标志可能先于数据落盘。

4.3 CRC 校验的选型

工业数据校验我一般用 CRC-16/CCITT 或者 CRC-16/MODBUS。MRAM 本身出错率极低,但 SPI 传输过程可能受干扰,加上 CRC 能兜住传输错误。CRC 表可以预先算好放 Flash,也可以运行时算,STM32F215 的算力跑 CRC-16 毫无压力。

uint16_t CRC16_Modbus(uint8_t *buf, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { crc ^= buf[i]; for (int j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; }

5. 实操调试与常见问题排查

5.1 上电后读出来全是 0xFF 或 0x00

这是最常见的现象,八成是 SPI 没通。排查顺序:

  1. 先用示波器看 SCK 有没有波形。没有波形,检查 SPI 初始化和时钟使能。
  2. 看 CS 有没有拉低。CS 一直是高,说明片选代码没执行或者 GPIO 配错。
  3. 看 MOSI 上有没有命令码。没有,检查HAL_SPI_TransmitReceive的返回值。
  4. 看 MISO 有没有反应。MISO 一直不动,检查 MRAM 供电和焊接。

我遇到过一块板子,MRAM 的 VDD 焊盘虚焊,上电后芯片根本没工作,MISO 一直是高阻,读出来全 0xFF。补焊后立刻正常。

5.2 写入后读出来不对

先确认 WREN 有没有发。忘了发 WREN 是新手最常犯的错,写命令被静默忽略。其次检查地址有没有越界,512KB 的地址范围是 0x00000 ~ 0x7FFFF,超过这个范围行为未定义。再检查 SPI 模式,Mode 0 和 Mode 3 搞反了,读出来的数据会整体移位。

5.3 高速下偶发读错

前面提过,这是信号完整性问题。降速测试,如果低速正常高速出错,基本就是走线或者干扰问题。处理办法:SCK 和 MISO 串 22Ω~33Ω 电阻,缩短走线,远离电源电感,必要时在 MISO 对地加 10pF 小电容。

5.4 常见问题速查表

现象可能原因排查方法
读出全 0xFF芯片未供电/虚焊/CS 未拉低查供电、补焊、示波器看 CS
读出全 0x00MISO 短路到地/模式错误查 MISO 走线、确认 SPI 模式
写入无效未发 WREN读状态寄存器确认 WEL
数据错位SPI 模式或 CPHA 配错改 Mode 0 重试
高速偶发错误信号完整性降速、串阻、缩短走线
地址越界地址超过 0x7FFFF检查地址计算

5.5 几个实测有效的经验

第一,调试阶段一定用逻辑分析仪抓一次完整时序。命令码、地址、数据、CS 边沿,一眼就能看出问题,比盲猜快十倍。第二,先写一个已知模式(比如 0x55、0xAA 交替)再读回对比,比写随机数据更容易发现位翻转。第三,MRAM 不需要延时等待,写完立刻就能读,如果你在代码里加了 Flash 那种while(忙等待),纯属浪费时间。第四,批量写的时候尽量一次 CS 拉低写完,减少 CS 翻转次数,既快又稳。

6. 性能实测与方案对比

6.1 实测写入速度

在 STM32F215RE + 30MHz SPI 下,我实测连续写 1KB 数据(含 WREN 和命令开销)大约耗时 280 微秒,平均每字节不到 0.3 微秒。同样条件下,SPI NOR Flash 写 1KB 需要先擦一个 4KB 扇区(约 40ms),再编程(约 1ms),总共 41ms 左右。差距是 140 倍以上。对于需要高频记录的场景,这个差距是决定性的。

读取速度方面,30MHz 下连续读 1KB 约 270 微秒,和写入相当,因为 SPI 是全双工的,读写速率都受时钟限制。

6.2 和 EEPROM 方案的对比

有人会问,用 I2C EEPROM 不是更便宜吗?确实便宜,但 EEPROM 单字节写入要 5ms 左右,写 1KB 要 5 秒,而且寿命只有 100 万次。工业设备如果每秒记录一次,EEPROM 几个月就写坏一个扇区了。MRAM 的 10^14 次寿命,按每秒写一次算,可以写几百万年。在“频繁写”这个维度上,MRAM 是碾压性的。

6.3 成本与容量的取舍

MR25H40CDF 单价确实比同容量 Flash 贵不少,这是它的唯一短板。所以我的建议是:关键数据用 MRAM,大容量历史数据用 Flash 或 SD 卡。比如设备参数、掉电快照、最近一段时间的核心日志放 MRAM,几个月的历史曲线放大容量存储。这样既保证了关键数据的可靠性,又控制了整体成本。

7. 代码组织与工程化建议

7.1 驱动分层

我习惯把 MRAM 驱动分成两层:底层mram_hal.c只负责 SPI 字节收发和 CS 控制,上层mram.c提供MRAM_Read、MRAM_Write、MRAM_ReadRecord、MRAM_WriteRecord这些业务接口。这样换主控或者换 SPI 外设时,只改底层,上层不动。

7.2 参数默认值与恢复

设备第一次上电时 MRAM 是空的(或者全是随机值),需要有默认参数恢复机制。我的做法是在参数区头部放一个魔数(比如 0x5A5AA5A5),读出来不等于魔数就加载默认参数并写入。这样既处理了首次上电,也处理了参数区被意外破坏的情况。

7.3 循环日志的读写指针

循环日志区用两个指针:写指针和读指针。写指针每次写完一条记录后递增,到区尾回绕到区头。读指针由上位机读取时更新。为了防止读写指针本身丢失,我把它们也存两份,带 CRC。指针更新频率不高,双写开销可以忽略。

typedef struct { uint32_t write_ptr; uint32_t read_ptr; uint16_t crc; } LogCtrl_t;

8. 一些容易被忽略的细节

WP# 和 HOLD# 引脚别悬空。虽然内部有上拉,但工业环境干扰大,悬空可能导致误触发写保护或者保持状态。我一般直接上拉到 VDD,不用这两个功能。

电源去耦要到位。MRAM 虽然功耗不高,但写入瞬间电流会有跳变,VDD 上并一个 0.1uF 加一个 1uF,离芯片越近越好。

SPI 的 GPIO 速度等级要配对。STM32 的 GPIO 有 Low/Medium/Fast/High 四档,跑 30MHz SPI 时 SCK 和 MOSI 要设成 High,MISO 设成 High 或者 Medium 都行。设成 Low 的话波形上升沿会变缓,高速下直接读错。

初始化顺序要注意。先初始化 GPIO 和 SPI,再把 CS 拉高,最后才操作 MRAM。如果 CS 初始化前是浮空低电平,上电瞬间可能被 MRAM 误认为片选有效,收到一堆乱码命令。

提示:MRAM 上电后不需要任何初始化命令就能读写,这点比 Flash 省事,Flash 有时候要发释放掉电或者读 ID 的命令。

9. 这套方案适合什么场景

工业数据采集终端、电力监测设备、车载记录仪、医疗设备参数存储、任何需要“频繁写小数据 + 怕掉电 + 长寿命”的嵌入式项目,这套 STM32F215RE + MR25H40CDF 的组合都很合适。它的核心价值不在于容量,而在于把“关键数据的可靠写入”这件事做到极致简单——不用擦除、不用等待、不用管寿命、掉电即存。

我自己在实际项目里用这套方案跑了两年多,现场断电几十次,没有丢过一条关键记录。踩过的坑主要集中在 SPI 模式和信号完整性上,把这两块处理好,剩下的就是常规的代码组织问题。如果你正在为掉电丢数据发愁,不妨试试 MRAM 这条路,前期多花的那点成本,比起现场返修和客户投诉,真的不算什么。

返回列表