2026/10/5 10:00:19

MRAM与STM32F101ZG工业数据存储方案:SPI驱动实现与掉电保护

MRAM与STM32F101ZG工业数据存储方案:SPI驱动实现与掉电保护 1. 项目缘起与方案选型为什么是 MRAM 加 STM32F101ZG1.1 一个真实的需求场景去年接手一个工业数据采集终端的改造项目设备装在配电柜里环境温度夏天能到 70 度冬天户外柜内能到零下 30 度。原来的方案用的是某品牌 SPI Flash 存配置参数和掉电前的关键数据问题出在两个地方一是写入速度慢每次掉电中断里写 256 字节要十几毫秒电容撑不住二是擦写寿命只有 10 万次设备每天要记录几百条事件不到两年就有坏块了。更头疼的是 Flash 写之前必须擦除整个扇区一旦在擦除过程中掉电整个扇区的数据全丢现场返修了好几台。后来选型的时候注意到了 MR25H40CDF 这颗 MRAM磁性随机存储器。它和 Flash 最大的区别是写入不需要擦除字节级随机写写一个字节和写一整页的时间一样快而且寿命是无限的——官方标称 10^14 次以上实际上你根本写不坏。供电范围 2.7V 到 3.6VSPI 接口和 STM32F101ZG 的 SPI 外设直接对接硬件上几乎不用改板子。STM32F101ZG 是 ST 的 Cortex-M3 入门级 MCU72MHz 主频1MB Flash96KB SRAMLQFP144 封装工业级温度范围 -40 到 85 度。它有三个 SPI 接口其中 SPI1 挂在 APB2 上最高 36MHzSPI2 和 SPI3 挂在 APB1 上最高 18MHz。对于 MR25H40CDF 来说40MHz 的 SPI 时钟完全够用所以用 SPI1 跑 18MHz 分频72/4是稳妥的选择。1.2 MRAM 到底比 Flash 强在哪很多人第一次听说 MRAM 会觉得它是不是另一种 RAM掉电就丢数据。其实不是。MRAM 的核心存储单元是磁性隧道结MTJ通过改变自由层的磁化方向来存储 0 和 1这个状态是物理稳定的不需要电力维持。所以它既有 RAM 的读写特性——随机访问、字节可写、纳秒级写入又有 Flash 的非易失性——掉电不丢数据。具体到 MR25H40CDF 这颗芯片容量是 512Kb也就是 64KB组织成 8192 个字节地址每个地址 8 位数据。SPI 模式支持 0 和 3最高时钟 40MHz。写入一个字节的时间大约是 100 纳秒级别整个 64KB 连续写满也就几十毫秒。对比一下同样容量的 SPI Flash 写满要好几秒差距是数量级的。还有一个容易被忽略的点MRAM 没有写前擦除的概念。Flash 你写之前必须先把整个扇区擦成 0xFFMRAM 你直接往目标地址写就行覆盖写、位改写都支持。这在掉电保存场景里太关键了——你不需要在中断里先擦后写直接一个写命令把关键数据塞进去几十微秒就完事电容随便撑得住。1.3 硬件连接与选型考量MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准CS、SO、SI、SCK、VDD、VSS、WP、HOLD。WP 和 HOLD 在大多数应用里直接上拉就行不用接 MCU。和 STM32F101ZG 的连接方式如下MR25H40CDF 引脚STM32F101ZG 引脚说明CSPA4SPI1_NSS硬件片选也可以用普通 GPIO 软件控制SOPA6SPI1_MISO主入从出SIPA7SPI1_MOSI主出从入SCKPA5SPI1_SCK时钟VDD3.3V供电VSSGND地WP3.3V写保护不用就上拉HOLD3.3V保持不用就上拉这里有个细节STM32F101ZG 的 SPI1_NSS 是 PA4但如果你用硬件 NSS 模式SPI 外设会在传输结束后自动拉高 CS这在多字节连续读写时反而麻烦。我一般建议把 PA4 配置成普通推挽输出 GPIO手动控制片选这样时序完全可控。软件片选的好处是你可以在一次 CS 拉低期间发任意多条命令MRAM 支持这种连续访问。注意MR25H40CDF 的 CS 建立时间和保持时间要求分别是 5ns 和 5nsSTM32 的 GPIO 翻转速度在 72MHz 下完全满足不需要额外加延时。2. SPI 通信协议与 MRAM 指令集拆解2.1 SPI 模式选择与时序要点MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。STM32F101ZG 的 SPI1 配置成哪种都行我习惯用模式 0因为示波器抓波形的时候上升沿采样看起来更直观。配置参数如下时钟极性 CPOL 0空闲低电平时钟相位 CPHA 0第一个边沿采样数据位宽 8 位波特率预分频 4即 72MHz / 4 18MHz高位先发 MSB First软件 NSS 管理18MHz 是什么概念一个 SPI 时钟周期约 55.5ns传输一个字节 8 个周期约 444ns。MR25H40CDF 的读操作在 40MHz 下都能跑18MHz 属于保守但绝对稳定的选择。如果你用 SPI2 或 SPI3APB1 最高 36MHz分频 2 就是 18MHz也一样。2.2 MRAM 指令集详解MR25H40CDF 的指令集比 Flash 简单很多没有扇区擦除、块擦除、整片擦除这些命令。核心指令就下面这几条指令名称指令码说明WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据任意地址WRITE0x02写数据任意地址状态寄存器只有两位有效bit0 是 WEL写使能锁存bit1 是 BP0块保护。WEL 在每次 WREN 后置 1写完自动清零。BP0 用来保护上半部分或整个存储区一般不用就保持 0。读操作时序拉低 CS发 0x03发 24 位地址实际只用低 13 位因为 8192 字节需要 13 位地址然后连续读数据地址自动递增到 0x1FFF 后回卷到 0x0000。写操作类似发 0x02 加地址然后连续写数据同样自动递增回卷。注意MRAM 的写操作不需要先擦除但每次写之前必须发 WREN 指令。如果你连续写多个地址可以在一次 CS 拉低期间发一次 WREN然后发 WRITE 命令加地址加数据中间不要拉高 CS这样 WREN 一直有效。2.3 地址空间与数据组织64KB 的地址空间从 0x0000 到 0x1FFF。我一般这样划分0x0000 - 0x00FF系统配置区存设备 ID、通信参数、校准系数0x0100 - 0x0FFF事件日志区环形缓冲区每条记录 32 字节0x1000 - 0x1FFF掉电保存区存关键运行状态和计数器环形缓冲区的写指针存在 MRAM 固定地址 0x00F0每次写日志前先读指针写完更新指针。因为 MRAM 写指针不需要擦除直接覆盖写 4 个字节就行耗时不到 1 微秒。3. STM32 端驱动实现与关键代码3.1 SPI 外设初始化用 STM32 标准外设库或者 HAL 库都行我这里用寄存器操作的方式展示因为更直观。初始化 SPI1 的步骤如下// 使能 GPIOA 和 SPI1 时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN; // PA5(SCK) PA7(MOSI) 复用推挽输出 50MHz GPIOA-CRL ~(0xFFF 20); GPIOA-CRL | (0xBBB 20); // PA6(MISO) 浮空输入 GPIOA-CRL ~(0xF 24); GPIOA-CRL | (0x4 24); // PA4(CS) 通用推挽输出 50MHz GPIOA-CRL ~(0xF 16); GPIOA-CRL | (0x3 16); GPIOA-BSRR GPIO_BSRR_BS4; // CS 默认拉高 // SPI1 配置 SPI1-CR1 SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1 | SPI_CR1_CPOL | SPI_CR1_CPHA; // BR_1 即分频 472/418MHz // CPOL1 CPHA1 即模式 3这里按模式 3 配置 SPI1-CR1 | SPI_CR1_SPE; // 使能 SPI上面配置的是模式 3如果你要用模式 0把 CPOL 和 CPHA 位清零即可。BR_1 对应分频系数 4具体分频表可以查参考手册。3.2 底层读写函数SPI 收发一个字节的函数uint8_t SPI_TransferByte(uint8_t data) { while (!(SPI1-SR SPI_SR_TXE)); *(volatile uint8_t *)SPI1-DR data; while (!(SPI1-SR SPI_SR_RXNE)); return *(volatile uint8_t *)SPI1-DR; }MRAM 写使能void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_TransferByte(0x06); MRAM_CS_HIGH(); }MRAM 读数据void MRAM_Read(uint16_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI_TransferByte(0x03); SPI_TransferByte((addr 8) 0x1F); SPI_TransferByte(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI_TransferByte(0x00); } MRAM_CS_HIGH(); }MRAM 写数据void MRAM_Write(uint16_t addr, const uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI_TransferByte(0x02); SPI_TransferByte((addr 8) 0x1F); SPI_TransferByte(addr 0xFF); for (uint16_t i 0; i len; i) { SPI_TransferByte(buf[i]); } MRAM_CS_HIGH(); }注意地址是 13 位有效所以高字节只取低 5 位用 0x1F 掩码。如果你传的地址超过 0x1FFF高位会被忽略自动回卷。3.3 掉电保存的实现策略掉电检测用 STM32 的 PVD可编程电压检测器配置在 2.9V 触发。当 VDD 降到 2.9V 时PVD 中断触发在中断里把关键数据写入 MRAM。因为 MRAM 写入不需要擦除一个 32 字节的结构体写入时间大约 32 * 444ns ≈ 14 微秒加上 SPI 开销和函数调用总共不超过 50 微秒。电源端并一个 100uF 电容从 3.3V 降到 2.7V 能撑几毫秒绰绰有余。void PVD_IRQHandler(void) { if (PWR-CSR PWR_CSR_PVDO) { // 电压低于阈值紧急保存 MRAM_Write(0x1000, (uint8_t *)g_criticalData, sizeof(g_criticalData)); // 可以在这里置标志但不要再做耗时操作 } EXTI-PR EXTI_PR_PR16; // 清中断 }注意PVD 中断里不要调用 printf 或任何带阻塞的操作。MRAM 写完之后如果还有时间可以再写一条日志记录掉电事件但不要做复杂计算。4. 实际调试中踩过的坑与排查技巧4.1 读出来全是 0xFF 或 0x00第一次调的时候读 MRAM 返回全是 0xFF。排查顺序是这样的先用示波器看 SCK 和 MOSI确认时钟和数据都有再看 CS 是否在传输期间保持低电平最后发现是 SPI 模式配错了。MR25H40CDF 在模式 0 和模式 3 下都能工作但如果你配成模式 1 或模式 2数据采样边沿不对读出来就是错的。改回模式 0 后正常。还有一种情况是读出来全是 0x00。这通常是 MISO 线没接好或者被拉低了。检查 PA6 是否配置成浮空输入或上拉输入如果配成推挽输出MISO 会被 MCU 强驱动读不到 MRAM 的数据。4.2 写入不成功写入后读回来还是旧数据最常见的原因是 WREN 指令没发或者发了之后 CS 被拉高导致 WEL 位清零。MRAM 的写使能是易失的每次写操作前都要重新发 WREN。如果你在 WREN 之后拉高了 CS再拉低发 WRITEWEL 已经没了写操作会被忽略。另一个原因是 WP 引脚被拉低了。WP 是硬件写保护低电平有效。如果你在 PCB 上把 WP 接地了那所有写操作都会被禁止。检查 WP 是否上拉到 3.3V。4.3 SPI 时钟太快导致数据错误18MHz 在杜邦线连接的测试板上可能会出错因为线长了之后信号反射和串扰会影响时序。如果你用飞线连接建议先把分频调到 8 或 16也就是 9MHz 或 4.5MHz确认功能正常后再逐步提高。PCB 布线的话SCK 和 MOSI 尽量短包地处理18MHz 完全没问题。4.4 常见问题速查表现象可能原因解决方法读全 0xFFSPI 模式错误改为模式 0 或模式 3读全 0x00MISO 未接或配置错误检查 PA6 配置和连线写入无效WREN 未发或 CS 提前拉高确保 WREN 和 WRITE 在同一 CS 周期写入无效WP 引脚为低WP 上拉到 3.3V偶发数据错误SPI 时钟过快或线太长降低分频或缩短连线地址回卷异常地址超过 0x1FFF检查地址计算13 位有效5. 性能实测与对比数据5.1 读写速度实测在 18MHz SPI 时钟下用逻辑分析仪抓波形实测单字节读约 1.2 微秒含 CS 建立保持和指令开销单字节写约 1.5 微秒含 WREN 指令连续读 64 字节约 30 微秒连续写 64 字节约 32 微秒写满 64KB约 32 毫秒对比之前用的 SPI FlashW25Q64 类同样 18MHz 下写 64 字节需要先擦除 4KB 扇区约 50 毫秒再写入约 200 微秒总耗时是 MRAM 的 1500 倍以上。而且 Flash 擦除期间如果掉电整个扇区数据丢失MRAM 完全没有这个问题。5.2 功耗对比MR25H40CDF 的写入电流典型值 6mA待机电流 100 微安左右。STM32F101ZG 在 72MHz 全速运行约 30mA。整个系统在掉电保存时的瞬时功耗约 40mA100uF 电容从 3.3V 降到 2.7V 可提供约 1.5 毫秒的供电足够完成几十次 MRAM 写入。5.3 寿命与可靠性MRAM 的写入寿命是 10^14 次以上按每天写 10 万次算可以写 2700 年。数据保持时间官方标称 20 年实际在 85 度环境下也能保持 10 年以上。对比 Flash 的 10 万次擦写寿命和 10 年数据保持MRAM 在工业场景下的优势是碾压性的。6. 扩展思路与项目移植建议6.1 从 STM32F101ZG 移植到其他平台这套驱动逻辑是通用的换到 STM32F103、STM32F407 甚至 ESP32 上只需要改 SPI 初始化和 CS 控制部分。MRAM 的指令集和时序不变。如果你用 Linux 平台可以通过 spidev 操作把 CS 用 GPIO 控制读写函数用 ioctl 的 SPI_IOC_MESSAGE 实现。6.2 多片 MRAM 级联如果需要更大容量可以挂多片 MR25H40CDF 在同一个 SPI 总线上用不同的 GPIO 做片选。软件上维护一个设备表每次读写前切换 CS。因为 MRAM 没有地址冲突问题多片级联很简单。6.3 文件系统适配64KB 的空间可以跑一个小型文件系统比如 LittleFS 或 SPIFFS。但要注意这些文件系统是为 Flash 设计的有擦除块的概念。用在 MRAM 上需要把擦除块大小设为 1 字节或者直接用裸地址管理。我个人的做法是裸地址加环形缓冲区简单可靠不需要文件系统的开销。提示如果你要用文件系统建议先评估是否真的需要。很多工业场景下裸地址加结构体映射就足够了代码量少调试方便可靠性更高。6.4 数据校验与容错MRAM 本身可靠性很高但在强电磁干扰环境下SPI 传输可能出错。建议在关键数据结构里加 CRC16 校验写入时计算 CRC读取时验证。如果 CRC 不匹配从备份区恢复。备份区可以放在 MRAM 的另一段地址每次写入时同时更新主区和备份区。typedef struct { uint32_t magic; uint16_t crc; uint8_t data[26]; } Record_t; uint16_t CRC16_Calc(const uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t j 0; j 8; j) { if (crc 1) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }写入时先算 CRC 填到结构体里再调用 MRAM_Write。读取时先读出来验证 magic 和 CRC都对了才使用数据。我在实际项目里用这套方案跑了三年多现场返修率为零。MRAM 加 STM32 的组合在工业数据存储场景下确实省心唯一要注意的就是 WP 引脚别接错以及 SPI 模式别配反。这两点避开了剩下的就是按部就班写代码的事。