
1. 项目概述1.1 为什么我在工业项目里选中了 MRAM先说结论这年头做嵌入式数据存储方案看着多——Flash、EEPROM、SRAM、铁电真到工业现场一较真各有各的难处。我这个项目需要的是“频繁写、掉电不丢、不怕数据错乱”的存储介质最终选了 Everspin 的 MR25H40CDF挂在 STM32F437ZG 的 SPI 总线上。板子已经跑了大半年经过了几十次意外断电和高温环境考验数据一次都没丢过。MR25H40CDF 是 Everspin 的 4Mb 串行 MRAMSPI 接口容量 512KB。它和普通 Flash 或者 EEPROM 最大的区别是写入不需要擦除、没有写寿命上限、写入速度接近 SRAM。对你没看错没有写寿命上限。MRAM 的存储单元是磁隧道结靠磁化方向存数据而不是靠电荷所以没有 Flash 那种“先擦后写、擦除次数有限”的物理限制。这个特性在工业数据记录、掉电保存、频繁日志写入的场景里是刚需。STM32F437ZG 则是意法半导体的高性能 Cortex-M4 系列主频 168MHz带 512KB Flash 和 192KB RAM。这块芯片本身没什么稀奇的但它的 SPI 外设最高可以跑到 45Mbps配合 DMA 做数据搬运非常顺用来驱动 MRAM 是绰绰有余。1.2 这套组合能解决什么问题工业嵌入式里最头疼的场景就几类设备状态频繁写入、运行日志循环记录、关键参数掉电保存、以及系统升级时的备份恢复。以前用 SPI Flash 做这些事情最怕的就是频繁写同一个地址把扇区写穿了用 EEPROM 又蛋疼在容量太小4Kbit、64Kbit 根本放不下几张日志表用电池供电的 SRAM 掉电保护还要额外维护电池和充电电路。MRAM 把这几个痛点一次性解决了。4Mb 容量做循环日志能存几千条无限写入寿命不用做磨损均衡算法掉电数据不丢不需要后备电池写入速度又快SPI 时钟拉到 40MHz 写一个字节的理论时间是 0.2 微秒比异步 SRAM 稍慢一点但比 Flash 快几个数量级。而且它和 SRAM 一样是随机访问的读某个字节和读一整页的速度是一样的不像 SPI Flash 必须先读页再搬数据。这篇博文我打算完整拆解 MR25H40CDF 和 STM32F437ZG 的软硬件联调全过程包括硬件接线、SPI 时序配置、驱动代码、DMA 传输、以及我在项目中遇到的各种坑。适合正在选型存储方案、或者被 Flash 写寿命困扰的嵌入式开发者参考。2. 存储方案选型与总体设计思路2.1 为什么 Flash 在工控场景里不够用在说 MRAM 之前我想先花一点笔墨聊聊为什么传统的存储方案在工业环境里不够顶。这不是为了凑字数而是选型逻辑本身就得从对比中得来。嵌入式系统里常用的非易失性存储无非三种NOR Flash、EEPROM、以及 SRAM 加电池的组合。NOR Flash 的问题在于写放大和擦除寿命。哪怕你只想改一个字节也得先把整个扇区通常 4KB 或 64KB读出来在内存里改掉然后整块擦除再写回。这个过程不仅有额外的软件复杂度还会放大写入次数。一个扇区若反复写几千次到几万次就可能出现坏块。如果你的设备每分钟记录一次日志Flash 的寿命按月算就扛不住了。再加上 Flash 写入时需要 5V 左右的编程电压虽说是片内电荷泵完成但高温下保持特性衰减也是个隐患。EEPROM 好在可以字节级擦写寿命也比 Flash 长一些大概 100 万次。但 EEPROM 的容量普遍偏小常用的 24C64 只有 64Kbit工业上有些应用要记录几百个参数、带时间戳的日志、或者波形数据64Kbit 根本不够。而且 EEPROM 的写入速度很慢I2C 模式下写一页也要几毫秒频繁写入时会直接拖慢主程序的实时性。带电池的 SRAM 方案更麻烦。SRAM 读写快、可无限次写入但它是易失性的必须靠电池在掉电后维持供电。电池本身要定期更换高温下有漏液风险还要处理电源切换电路。产线上一旦电池没电设备里的配方和运行数据就全丢了这锅谁也背不起。MRAM 正好卡在所有方案之间容量够4Mb 起步、可以无限次写、非易失、写入速度接近 SRAM、随机访问不需要显式擦除。所以说MRAM 并不是什么黑科技它就是把 SRAM 的速度和 IEEE 的非易失性组合到了一起代价就是单价稍微贵一点点。2.2 MR25H40CDF 的硬件结构特性MR25H40CDF 是 Everspin 面向工业和汽车级的 MRAM 产品线工作温度范围 -40°C 到 125°C。封装是 SOP-8管脚定义跟常见的 SPI Flash 兼容度很高基本就是那 8 个脚片选、时钟、数据输入、数据输出、写保护、HOLD、电源和地。这个封装布局是故意的方便那些原本用 SPI Flash 或 EEPROM 的硬件设计无痛升级到 MRAM。芯片的存储深度是 512KB按字节寻址需要 19 位地址。SPI 指令里地址部分是 3 字节前 3 位保留后 19 位是有效地址。写指令、读指令、读状态寄存器、写状态寄存器这四类操作基本就是全部。它的状态寄存器是 8 位的其中低 7 位保留最高位是 WP 引脚状态指示位。听上去很简单但越是简单的东西越要用好后面我会讲它细节里的门道。MRAM 一个非常有用的特性是“写的时候不需要先擦除”。Flash 写之前必须把目标扇区清成全 1而 MRAM 直接就是覆写。这意味着写数据没有“读-改-擦-写”的中间环节整个软件栈都能简化很多。另外它还没有写操作超时的问题——Flash 擦写一个扇区可能要几十毫秒到几百毫秒这期间 SPI 控制器得一直等而 MRAM 的写周期就是普通的寄存器写入SPI 时钟走完就完成了。还有一个常常被忽略的点MRAM 对电源纹波不太敏感。因为存储机理是磁畴翻转不是电荷泵编程所以电源跌落造成的数据写坏概率比 Flash 低得多。工业现场如果供电质量差、频繁上下电这个特性非常关键。2.3 与 STM32F437ZG 的匹配点分析STM32F437ZG 有 6 个 SPI 接口我选的是 SPI1。选 SPI1 的原因有几个首先是它可以和 DMA1 的通道 2 和通道 3 配合两个通道分别对应发送和接收做数据块搬运很省心其次是 SPI1 的引脚 PA5、PA6、PA7 在 100 脚封装的 LQFP 上没有复用冲突把片选放在 PA4 或者 PB12 都可以布局也方便。STM32F437ZG 的 SPI 外设最高支持到 45Mbps主时钟 168MHz 下分频系数最小可以到 4也就是 42Mbps。MR25H40CDF 的最高 SPI 时钟是 40MHz所以我把分频设置成 4实际跑 42Mbps。这里要较个真虽然手册说 42Mbps 超出了 MRAM 的额定 40MHz但实际测试下来写入读取都稳定因为 SPI 上没有任何长走线也没有负载电容问题。不过如果你是照着参考设计抄想保守一点就用 8 分频21Mbps也已经够快了。还有一个匹配点是 STM32F437ZG 内置了硬件 CRC 模块。MRAM 不像 Flash 或者 EEPROM 那样自带 ECC所以对数据的完整性校验要自己做。我用的方案是写入时硬件 CRC 算一遍把校验值存在帧尾读取时再算一遍做比对。STM32 的 CRC 外设支持 CRC-32配置起来只要 30 行代码比纯软件算快多了。这样既利用了 MCU 的资源又保证了 MRAM 里数据的安全。STM32F437ZG 还有个大彩蛋它有 192KB 的 SRAM其中 64KB 是紧耦合内存 CCM不能直接挂 DMA。SPI 的 DMA 缓冲区只能放在常规 SRAM也就是片内 SRAM1/SRAM2/SRAM3 那部分。这个细节我在第一次写 DMA 搬运代码时踩了个坑缓冲区放了 CCM 区域DMA 就是不动。后面在做工程时一定要留意 GCC 链接脚本的 MEMORY 区间声明把 DMA 缓冲段放在非 CCM 区域。3. 硬件接线与初始化细节3.1 MR25H40CDF 的 SOP-8 引脚功能与接线先看管脚定义。从 SOP-8 的标准丝印左上角开始逆时针编号1 脚是 CS片选低有效、2 脚是 SCK串行时钟、3 脚是 SI串行数据输入对应主机的 MOSI、4 脚是 VSS地、5 脚是 HOLD保持低有效、6 脚是 VDD电源、7 脚是 SO串行数据输出对应主机的 MISO、8 脚是 WP写保护低有效。有几个接线细节我必须单独拎出来强调因为不少人直接照抄 SPI Flash 的接法就翻车了。首先是 HOLD 引脚。SPI Flash 上 HOLD 引脚通常悬空也不影响正常读写但 MR25H40CDF 的 HOLD 引脚如果悬空在强电磁干扰环境下会导致时钟信号被锁存数据直接错乱。我在这颗料上吃过一次亏最初 HOLD 脚悬空设备在电机启动瞬间偶发读回全 0xFF排查了两天才定位到 HOLD 无上拉导致噪声误触发。正确做法是把 HOLD 引脚通过 10kΩ 电阻上拉到 VDD。WP 引脚同样要处理它低电平时状态寄存器不可写。如果不小心判定了写保护后面写数据时就会静默失败——指令收到了数据也传了但芯片就是不存。我建议把 WP 也上拉到 VDD用软件的方式去控制写保护而不是硬件强拉低。除非你的应用有非常严格的多级写保护需求才考虑用 GPIO 控制 WP。电源方面MR25H40CDF 的工作电压范围是 3.0V 到 3.6V典型 3.3V最大静态电流在 36mA 左右。这个电流比同样容量的 SPI Flash 要大因为 MRAM 的内部结构更像 SRAM静态保持电流稍高一些。电源引脚旁边放一个 0.1μF 去耦电容 一个 4.7μF 钽电容这个组合在评估板上就是这么配的我实测电压纹波能控制在 30mV 以内足够稳定。片选 CS 引脚建议加上拉电阻可以避免主机复位或 SPI 外设尚未配置时 CS 处于低电平、误触发片选操作。虽说 MRAM 不像 Flash 那样有“写状态寄存器被误触发导致保护”的隐患但严谨一点没坏处。3.2 STM32F437ZG 侧 SPI1 引脚分配我用的 STM32F437ZG 是 LQFP-144 封装SPI1 默认引脚是 PA5SCK、PA6MISO、PA7MOSICS 我选了 PB6 来做软件控制。为什么不直接用 SPI 外设的硬件 NSS 呢因为 STM32 的硬件 NSS 在 SPI master 模式下有好几种工作方式要么禁用 NSS 输出要么开启 NSS 脉冲模式但在片选独立控制的应用里大多数时候大家还是选择 GPIO 手动拉 CS灵活性最高切换不同的 SPI 设备也方便。PA5、PA6、PA7 都设置成复用推挽输出AF 选择 AF5SPI1 的复用功能编号。打开 GPIO 时钟和 SPI1 时钟后把 SCK、MOSI 设置成 50MHz 速度等级MISO 设置成输入模式带上拉——实际上上拉可以不加因为 MRAM 的 SO 是三态输出只有 CS 拉低时才驱动总线其他时候释放总线。如果总线上只挂一颗 MRAMMISO 不上拉也能工作。但如果 SPI 总线上还挂了其他设备建议加 10kΩ 上拉防止浮空输入导致读入噪声。PB6 作为片选脚设置为推挽输出、默认高电平初始状态下必须保证 CS 处于高电平防止 MRAM 误以为主机正在和它通信。这里有一个容易忽略的问题如果 MRAM 在上电瞬间 CS 是低电平它的 SPI 状态机可能进入不确定状态。所以 GPIO 配置为输出的代码必须在系统时钟使能后第一时间执行而且在 SPI 外设使能之前就要拉高 CS。3.3 SPI 模式与极性配置要点MR25H40CDF 支持 SPI Mode 0CPOL0CPHA0和 SPI Mode 3CPOL1CPHA1这是 Everspin 手册里明确写的。大多数 SPI 设备用 Mode 0 或者 Mode 3MRAM 两种都兼容但你要注意它和普通 SPI Flash 的差别普通 SPI Flash 的读指令在 Mode 0 和 Mode 3 下都有定义而 MRAM 的某些状态寄存器读取命令在 Mode 3 时序下可能不是设计者预期的行为。稳妥起见我用的是 Mode 0也是 SPI 最默认的时序空闲时 SCK 为低电平数据在第一个时钟沿采样。在 STM32 的 SPI 配置里这对应的就是 CPOL0、CPHA0。如果你用 HAL 库SPI_InitTypeDef 里的 SPI_Mode、SPI_Direction、SPI_DataSize 这几项设置好之后把 SPI_CPOL 设成 SPI_POLARITY_LOW、SPI_CPHA 设成 SPI_PHASE_1EDGE。如果用寄存器操作BSY 标志的处理也有讲究后面第四节我给出完整的寄存器初始化代码。时序查看的一个实用技巧MR25H40CDF 手册里有一个时序图CS 拉低后SCK 要经过最小的 tCSS 时间才能开始跳变大约 10ns而 CS 拉高之前SCK 信号也要持续到时钟结束。这个要求在低速 SPI 下完全不用担心但如果你把 SPI 时钟提到 42MHzSCK 周期就大约 23.8ns此时必须保证程序在拉低 CS 和启动 SPI 数据传输之间不要有任何多余代码。最保险的方案是写一个 32 位寄存器函数把 GPIO_BSRR 拉低 CS 的操作和 SPI_DR 写数据的操作间隔缩短到几条指令或者干脆在拉低 CS 后立即执行 SPI 发送不做其他任何操作。另外还有一个信号完整性的点如果 SPI 时钟线走线超过 5cm建议在 SCK 线上串联一个 22Ω 的电阻用来抑制过冲。过冲不仅会造成数据采样错误长期运行还可能导致 MCU 的 GPIO 驱动器退化。我在双层板上做过对比测试串了电阻后SCK 沿的过冲从 800mV 降到 200mV 以内效果立竿见影。4. 驱动代码实现与读写函数详解4.1 寄存器级 SPI 初始化用寄存器写 SPI 驱动并不是为了炫技而是因为在 DMA 和中断交错的生产代码里寄存器级的可控性确实比 HAL 更好。这里给出我验证过的完整初始化函数。基于标准外设库还是直接操作寄存器其实都可以我这次直接操作寄存器因为代码量少逻辑也好理解你移植到任何 STM32F4 系列上都能直接用。void BSP_SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // PA5 - SCK, PA7 - MOSI, 复用推挽 GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA6 - MISO, 输入带上拉 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PB6 - CS, 推挽输出, 初始高电平 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // SPI1 配置: Master, Mode 0, 8bit, MSB First, 4分频 - 42MHz SPI1-CR1 0; SPI1-CR1 | SPI_CR1_MSTR | SPI_CR1_BR_0 | SPI_CR1_SSI | SPI_CR1_SSM; // BR_0 对应 4分频, 即 168MHz / 4 42MHz SPI1-CR1 | SPI_CR1_SPE; }有几个配置位需要解释。SPI_CR1_MSTR 将 SPI 设置为主模式SPI_CR1_SSM 和 SPI_CR1_SSI 表示软件管理 NSS并且强制拉高内部 NSS 信号。这个操作很重要因为 STM32 的 SPI 主模式如果不手动管理 NSS会自动把 NSS 拉低导致多主机冲突错误 MODF。我不使用硬件 NSS所以软件 NSS 置高的方式是必须的。4 分频对应 BR_0 置位、BR_1 到 BR_3 清零。STM32F437ZG 的主时钟是 168MHz4 分频是 42MHz芯片手册上 MR25H40CDF 最高支持 40MHz。如果你要保守一点就用 8 分频也就是 BR_0 清零、BR_1 置位、BR_2 清零、BR_3 清零21MHz在长线布板的场景下更稳。转速差距其实感受不明显因为 MRAM 随机访问本身就不是瓶颈。SPI1-CR1 清零这行是为了把波特率、极性、相位这些配置全部恢复成默认。在系统上电时这可能是多余的但在热复位场景下比如从 Bootloader 跳转到 AppSPI 可能已经处于半配置状态。先清零再配置保证状态机干净。4.2 读操作和 Flash 读指令几乎一致MR25H40CDF 的读操作指令是 0x03后面跟 3 字节地址高字节在前。其中地址的最高 5 位无效因为容量只有 512KB19 位地址就够用。发送完地址后芯片从指定地址开始连续输出数据CS 拉高结束读操作。我在项目里实际用的是整块读取也就是一次性把 MRAM 的容量读走一半或者全部再在内存里解析。为什么不按需读取单一字节因为这样 DMA 的效率最高SPI 连续模式下也不会有频繁的 CS 翻转开销。代码实现如下。void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { while (SPI1-SR SPI_SR_BSY); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); // 发送读命令 0x03 while (SPI1-SR SPI_SR_TXE 0); SPI1-DR 0x03; // 发送地址高字节, 19位地址所以最高3位为0 while (SPI1-SR SPI_SR_TXE 0); SPI1-DR (uint8_t)(addr 16); while (SPI1-SR SPI_SR_TXE 0); SPI1-DR (uint8_t)(addr 8); while (SPI1-SR SPI_SR_TXE 0); SPI1-DR (uint8_t)(addr); // 持续读数据: 每个字节需要先发送一个0x00以产生时钟 for (uint32_t i 0; i len; i) { while (SPI1-SR SPI_SR_TXE 0); SPI1-DR 0x00; while (SPI1-SR SPI_SR_RXNE 0); buf[i] (uint8_t)SPI1-DR; } // 等待发送完成 while (SPI1-SR SPI_SR_BSY); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); }这里要注意循环里先等 TXE 再写 DR、再等 RXNE 再读 DR。SPI 全双工模式下的收发是同时进行的你每写一个字节同时就会收到一个字节。读 MRAM 时发到的 0x00 只是用来产生 SCK 时钟真正有意义的是 RXNE 里收到的数据。如果你用 HAL 库的 HAL_SPI_TransmitReceive这一步就是一个函数搞定但寄存器级实现能让你对时序有更直观的把握。关于等待 BSY 标志这是很多人写 SPI 时容易遗漏的。BSY 置位表示 SPI 还在进行数据传输或 SCK 没有完全停止如果你在 BSY 为 1 时直接拉高 CS某些外设会把这个截断的时钟识别为错误信号。MRAM 还好但严谨的总线时序总是要求数据位传输完成后、时钟稳定后再释放片选。实测下来等待 BSY 再拉高 CS 可以完全避免偶发的字节错位问题。4.3 写操作无擦除直接覆写MR25H40CDF 的写指令是 0x02后面跟 3 字节地址和数据没有页编程的概念不需要先擦除。单次写操作的长度没有上限只要 CS 保持低电平你可以一次连续写完全部 512KB。这带来了极大的灵活性写任意长度数据都行不像 Flash 必须按页、按扇区操作。我的写入函数设计成分段写这主要是为了配合上层的数据包格式。每一包数据前面有 8 字节头部帧头、时间戳、长度后面是负载数据和 CRC整个包一次性写入 MRAM。代码结构如下。void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { while (SPI1-SR SPI_SR_BSY); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); while (SPI1-SR SPI_SR_TXE 0); SPI1-DR 0x02; while (SPI1-SR SPI_SR_TXE 0); SPI1-DR (uint8_t)(addr 16); while (SPI1-SR SPI_SR_TXE 0); SPI1-DR (uint8_t)(addr 8); while (SPI1-SR SPI_SR_TXE 0); SPI1-DR (uint8_t)(addr); for (uint32_t i 0; i len; i) { while (SPI1-SR SPI_SR_TXE 0); SPI1-DR buf[i]; while (SPI1-SR SPI_SR_RXNE 0); (void)SPI1-DR; // 丢弃收到的无用数据 } while (SPI1-SR SPI_SR_BSY); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); }读回寄存器丢弃这段是很多人不写的但如果你不读RXNE 标志一直被置位下一次传输时若 RXNE 未被清除SPI 会认为是过载错误。所以哪怕不在乎读回来的数据也要把 DR 寄存器读一次。写 MRAM 时最需要想清楚的是“要不要做写保护”。MRAM 的 WP 引脚和状态寄存器的 WPEN 位组合起来可以实现硬件写保护——WP 为低且 WPEN 为高时状态寄存器被锁住状态寄存器的位 7WPEN自己也不可能被写。这个机制用来防固件跑飞后乱写数据很有用但它需要硬件上把 WP 引到 GPIO。如果只是把 WP 上拉到 VDD那么软件层就没有写保护了全靠程序自觉。4.4 状态寄存器读取与写使能判断MR25H40CDF 没有像 SPI Flash 那样的写使能指令0x06因为它本身就不需要“写使能”这个状态。你发送写命令后直接写数据就行芯片永远处于可写状态。但它的状态寄存器里有一个 WIP写进行中位吗答案是没有。MRAM 的写入是即时完成的SPI 传输结束就意味着写入完成没有任何忙等待机制。不过状态寄存器并非完全没用它的最高位是 WP 引脚状态指示只读。读状态寄存器的指令是 0x05。如果你用软件控制写保护可以从这里读取当前 WP 引脚状态做逻辑判断判断硬件写保护是否生效。这在工业现场排查“为什么写不进去”的时候非常方便。uint8_t MRAM_ReadStatus(void) { while (SPI1-SR SPI_SR_BSY); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); while (SPI1-SR SPI_SR_TXE 0); SPI1-DR 0x05; while (SPI1-SR SPI_SR_TXE 0); SPI1-DR 0x00; while (SPI1-SR SPI_SR_RXNE 0); uint8_t status (uint8_t)SPI1-DR; // 还需要再读一次, 因为我们第一次接收到的其实是发送0x05时的应答 while (SPI1-SR SPI_SR_RXNE 0); status (uint8_t)SPI1-DR; while (SPI1-SR SPI_SR_BSY); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); return status; }有一个细节读状态寄存器时第一个 RXNE 收到的字节其实是对 0x05 命令的“无意义应答”因为 SPI 是全双工主机发 0x05 时芯片并不知道主机要干嘛可能在第一拍返回的是任意值或 0x00。于是我在发送 0x05 后又发送了一个 0x00 来产生第二个字节的时钟第二次接收到的才是真正的状态寄存器内容。如果你用逻辑分析仪观察会发现很多 SPI 设备的“读状态”都有这个特性。有的外设会在命令阶段就返回状态位比如 W25Q 系列第一个字节就是状态值但 MRAM 不是。所以要格外注意。4.5 DMA 方式的大块数据读写如果每次都靠 CPU 循环搬运数据SPI 的速度优势会被浪费掉。所以在传输超过 64 字节的数据块时我用了 DMA。STM32F437ZG 的 DMA1 通道 2 是 SPI1_RX通道 3 是 SPI1_TX。DMA 配置成内存到外设、外设到内存数据宽度 8 位不用 FIFOSPI 外设自带 2 字节的 RX FIFO 和 TX FIFODMA 直通即可。写入时的 DMA 流程先把 DMA1_Stream3 配置为内存到外设方向 MEM2MEM 或 PERIPH_TO_MEMORY设置外设地址为 SPI1-DR内存地址为发送缓冲。然后拉低 CS手动发送 0x02 和地址再启动 DMA 发送全部数据等 DMA 传输完成中断最后拉高 CS。有个非常关键的细节DMA 搬运的是数据部分命令和地址还是要 CPU 先发。如果不先发命令和地址DMA 一启动就直接从缓冲的第一个字节开始了芯片会把数据当成指令解析数据直接写飞。我在最初实现的时候就是没注意这个点DMA 写出去的头 4 个字节被 MRAM 误解为命令和地址剩下的数据全部错位查了好久才意识到。读取时的 DMA 流程类似先发 0x03 和地址然后把 DMA 配置成外设到内存。因为读 MRAM 时主机需要持续发送 0x00 来产生 SCK所以 DMA 发送缓冲可以指向一个全 0 的静态数组接收缓冲指向目标读取数组。DMA 同时启动发送和接收等接收完成中断后数据就全部到位了。用 DMA 还有一个附带好处CPU 在 MRAM 搬运数据的几百微秒里可以去处理别的任务比如刷新看门狗、读取传感器。拿 512 字节的数据块来说42MHz 时钟下 DMA 传输大约需要 512 * 8 / 42MHz ≈ 97.5 微秒而 CPU 循环方式大约要 300 微秒左右差距还是很明显的。5. 工业现场的可靠性设计与数据保护5.1 掉电保护电路与检测机制数据存储最怕掉电瞬间的写入中断。MRAM 虽然没有擦写寿命问题但如果在写操作进行到一半时电源突然断开这次写就等于没写或者写了半截。更糟糕的是如果掉电发生在地址发送期间芯片可能收到一个错误的地址和数据在干扰期间写入垃圾数据。我的解决方案分两级一级是硬件掉电检测另一级是软件写序列保护。硬件上STM32F437ZG 的 PDR_ON 引脚可以配置成电源电压检测模式当 VDD 跌到阈值以下时MCU 自动进入复位或产生掉电中断。我是在 PVD 中断里设一个标志主循环检测到这个标志后立即停止对 MRAM 的新写入并且把当前写地址、长度、CRC 这些关键信息刷到 MRAM 的预留区。另一个硬件技巧是给 MRAM 的 VDD 引脚加一个 10mF 左右的电容注意这里是毫法拉级不是微法。如果主电源瞬间跌落这个电容可以维持 MRAM 供电几个毫秒让正在传输的 SPI 数据写完最后几个字节。MRAM 的写入功耗其实不高一个 10mF 钽电容够维持几十个字节的写入。一定不要选电解电容工业环境低温性能太差。软件上的保护更简单每个完整的数据包写入时先写数据区再写头部记录区。头部里面有一个 16 位的“完成标志”字段初始值是 0xFFFF写完所有数据后把完成标志改成 0xA55A。读取端判断如果头部完成标志是 0xA55A就认为这个包完整有效如果还是 0xFFFF说明上次写入中途断电这个包视为无效直接跳到上一份有效包。这是非常经典的双缓冲日志管理思路。5.2 CRC 校验与读回验证MRAM 没有硬件 ECC所以应用层的校验必须自己做。我选了 CRC-32IEEE 802.3 标准STMF4 系列自带硬件 CRC 外设算法不用 CPU 算。写入时计算待写数据的 CRC追加到包尾读取时对整包数据重新计算 CRC和包尾存储的 CRC 比对。如果一致数据直接可用如果不一致读回的数据不可信要从上一个有效包恢复。这里要提醒一个工程上的问题STM32 的硬件 CRC 外设的工作原理是把数据一位一位地送入移位寄存器它的初始值默认是 0xFFFFFFFF。如果你写了多个数据块想把它们组合成一个大包做校验就必须手动重置 CRC 外设再开始计算否则前一个数据块的 CRC 状态会残留。我一个同事在这个问题上折腾了一个下午最后发现是 CRC 外设没有复位导致校验永远不对。另一个建议是“写后读回验证”。虽然 MRAM 的写入可靠性很高但为了确保总线时序、引脚焊接没有问题在首次烧写或者诊断模式下我会做一个全片写读比对。跑完后如果对比失败直接定位到硬件焊接不良或时序问题而不是等实际运行时才暴露。这个测试大概耗时 2 秒放到产线自检项里非常合适。5.3 磨损均衡还需要吗说了 MRAM 没有写寿命限制那磨损均衡算法还需要吗我的答案是在 MRAM 本身这一层不需要但在你上层的日志文件系统层面可能还是需要一点工程考虑。原因是这样的MRAM 的单比特理论上可以无限次翻转这没有错。但实际上你写数据时SPI 总线上传的是整个字节甚至整个字。如果你频繁地更新一个 32 位的计数变量每次写 4 个字节都是完整覆写——MRAM 不会有任何损伤因为它本来就是按位翻转。所以硬件层面真的不需要磨损均衡。但是如果你的软件有人在某个地址上写了 10 亿次比如一个运行计数器每秒写一次写了 30 年这也不会损坏 MRAM。MRAM 的磁隧道结的翻转寿命是 10^12 到 10^14 次量级远远超出工业设备生命周期的写入次数。所以你可以把算法从驱动层拿掉让上层逻辑更简单。但如果你愿意保留一个简单的循环缓冲区指针它可以让整个存储区的写入分布均匀防止某个特定地址在高温下因为长期保持某种磁化状态而出现轻微偏移——虽然这个效应在数据手册的保持特性里也覆盖了。5.4 与外部硬件看门狗联动工业现场跑飞是躲不掉的。程序跑飞后最怕的是持续写 MRAM把日志区写到逻辑混乱。我的做法是外部硬件看门狗STM32F437ZG 内置 IWDG 也行超时复位 MCU而 MRAM 的写入只允许在主循环的特定任务中执行中断服务程序里一律不写 MRAM——除了掉电保护中断只写紧急标志。这样即使某个中断异常反复触发也不会导致 MRAM 数据被连续覆盖。另外我在 CS 控制逻辑里加了一个“写会话”机制。任何写操作开始前先把 CS 拉低写一个 8 位的会话 ID 到 MRAM 的固定地址。写完后清除这个 ID。如果程序在写入过程中被看门狗复位下次启动时会发现会话 ID 不为空说明上次写入被异常中断这时候就做一次全片扫描把中断的记录标记为无效。这个机制比靠读回的 CRC 判断更早一步发现问题。6. 典型问题排查与坑点总结6.1 读取数据全是 0xFF 或 0x00这是最常见的故障。读回来全是 0xFF大概率是 MISO 引脚上没有数据返回要么是 MRAM 没被片选、要么是 SO 引脚没接到 MCU 的 MISO。读回全是 0x00则可能是 SPI 时钟极性或者相位反了数据在错误的边沿被采样。排查顺序是这样的先用万用表量 CS 在通信时有没有被拉低再量 SCK 有没有时钟输出然后用示波器同时抓 CS、SCK、MOSI 三个信号确认主机的命令和地址是否正确。如果 MOSI 正常但 MISO 一直是高那就是 SO 引脚虚焊或者 MRAM 供电有问题。我第二次调试这套系统时就遇到了 MISO 虚焊看着波形有时候对有时候不对后来把板子放到显微镜下才发现 SO 引脚只有半边焊盘沾锡了。还有一个经常被忽略的原因SPI1 的 PA6 如果被配置成了复用推挽而不是输入模式MISO 上读到的数据永远是 GPIO 输出的驱动值而不是 MRAM 返回的值。初始化时一定要把 MISO 对应引脚配成输入模式。别笑我见过不止一个工程师把全部 SPI 引脚一股脑配成 AF_PP。6.2 写操作静默失败读回全是旧数据写命令发出去了读回来却是旧值。这个现象一般出现在 WP 引脚被意外拉低的情况下。MR25H40CDF 的写保护机制是当 WP 拉低且状态寄存器的 WPEN 位是 1 时整个芯片的写操作被禁止。如果你初始化时把状态寄存器写过 WPEN1后面 WP 引脚一旦被干扰拉低所有写操作都会失效但芯片不会给你任何错误回报。排查方法读状态寄存器看最高位是不是 0。如果是 0说明 WP 引脚确实被拉低了。硬件上给 WP 加上拉到 VDD 的电阻以后这个问题基本不会再出现。如果 WP 是靠 GPIO 控制的检查一下 GPIO 初始化时序确保它是先被拉高再被复用的。另外还有一个伪故障写地址越界。MR25H40CDF 只有 19 位有效地址如果你不小心传了一个大于 0x7FFFF 的地址高位的地址 bit 会被芯片忽略或者混入命令导致写到了错误的地方。读回的时候你从正确地址读自然读不到刚写的数据。这种情况看起来就像“静默失败”实际是你的地址错了。6.3 DMA 传输时偶发错位DMA 大批量传输的时候偶尔会出现读回的数据整体偏移了几个字节或者最后一包数据缺失。这类问题十有八九出在 CS 的控制时机上。DMA 传输完成的标志是传输完成中断但此时 SPI 外设的移位寄存器可能还有一个字节没有完全移出。如果中断服务程序里立即拉高 CS等于把最后一个字节的时钟切断了。正确做法DMA 传输完成中断里先关 DMA再等待 SPI1-SR 的 BSY 位清零然后再拉高 CS。我在代码里加了一个 200ns 左右的延时几条 NOP 指令也是一个折中方案但最可靠的还是等 BSY。还有一点DMA 配置时外设地址是 SPI1-DR但 STM32F4 的片上外设地址在 AHB 总线上DMA 访问外设时不需要开启 FIFO 模式。如果你开了 DMA 的 FIFO可能会引入数据丢帧问题。我在实际开发中把 DMA_FIFOMode 设成 DISABLE数据宽度设成 Byte问题就再没出现过。6.4 高温环境下偶发写入失败MRAM 的工作温度范围是 -40°C 到 125°C工业应用里芯片本身不是瓶颈。但如果你的板子里面还有其他发热源——比如线性稳压器——MRAM 附近的温度可能超过 125°C那就可能触发芯片的过温行为。表现为偶发的写入校验失败。解决方向有三个一是把 MRAM 布局远离大功率器件二是在 PCB 上给 MRAM 区域做敷铜散热三是在软件层做一个“温度降频”策略当 MCU 内部温度传感器读数过高时把 SPI 时钟从 42MHz 降到 21MHz降低动态功耗也降低附近温升。这个策略听上去很土但在户外设备上实测非常有效。其实很多时候高温下的写入失败不是 MRAM 本身的问题而是电源纹波变大导致的 SPI 逻辑电平边沿抖动。在电源入口加一个 LC 滤波给 MRAM 的 VDD 加一个 100nF 10μF 的组合电容就能吸收掉大部分开关噪声引起的电压跌落。6.5 逻辑分析仪判断时序的实用技巧调试 SPI 设备时逻辑分析仪是我必用的工具。但使用时有几个细节可以让排查效率翻倍。采样率必须设置成 SPI 时钟的 4 倍以上。如果 SPI 时钟是 42MHz逻辑分析仪的采样率至少要 168MS/s否则边沿会丢失波形看起来全是毛刺。很多入门级逻辑分析仪最高只有 24MS/s这个时候就不要硬调 42MHz老老实实把 SPI 分频到 8 分频、21MHz 再抓波形。抓取时把 CS、SCK、MOSI、MISO 四路信号同时抓下来。重点观察三个位置CS 拉低后到第一个 SCK 上升沿的间隔、每一个字节传输之间 CS 是否被拉高、最后一个 SCK 下降沿之后到 CS 拉高的间隔。这三个间隔如果有一个不符合手册时序图就能快速判断是主机问题还是外设问题。解码器的设置也要注意协议选择 SPICPOL 设置成 0CPHA 设置成 0位顺序 MSB First片选选择“CS 低有效”。设置错一项解码出来的数据顺序就会完全乱掉误导你的排查方向。7. 经验总结与扩展建议这套 MR25H40CDF STM32F437ZG 的组合在我手上已经迭代了两版硬件跑过振动台、高低温循环、反复上下电测试没有一次因为存储介质导致的数据丢失。MRAM 看似只是换了个存储芯片实际上把整个系统的数据可靠性设计思路都简化了不需要考虑 Flash 擦除寿命不需要做磨损均衡调度不需要等擦除时间代码逻辑和实时性都受益。如果你手头的项目也在频繁记录日志、保存运行参数或者被 Flash 的写寿命卡得很痛苦我建议可以花几天时间把 SPI Flash 的驱动平滑迁移到 MRAM 上。硬件改动基本为零管脚和命令体系高度相似软件改动主要是删掉“先擦除再写”的步骤、把页写入改成连续写入。迁移成本极低收益却非常直接。下一步我准备在这个基础上做一套环形日志区的管理工具利用 MRAM 的字节随机访问特性不再以页为粒度做擦除而是直接在字节级别追加写入日志格式会更紧凑。还可以把 STM32 的硬件加密引擎和 MRAM 结合给工业数据加一层 AES 加密。毕竟 MRAM 是非易失的万一设备被拆解关键数据裸奔总归不妥。这个方向的应用如果跑通了再写一篇单独的文章分享。