
1. 为什么 MRAM 在嵌入式存储里越来越受关注搞嵌入式的人都有一个共同的痛选存储介质的时候永远在做取舍。EEPROM 写入慢、寿命有限NOR Flash 擦写块大、写之前还得先擦FRAM 速度快、寿命高但容量小、价格贵SRAM 快是快一断电全没了。所以当 MRAM磁性随机存储器这类新型非易失性存储进入视野的时候我第一反应是——这东西到底能不能在实际项目里替代掉那些让人头疼的方案。MR25H40CDF 就是一颗 4Mbit 的 SPI MRAMEverspin 家的产品。它的核心卖点很直接非易失、写入不需要擦除、写入速度接近 SRAM、几乎无限次的擦写寿命。这几个特性放在工业现场数据记录、参数存储、掉电保护这些场景里简直是量身定做。而 STM32L031C6 是 ST 家的超低功耗 Cortex-M0 MCU20 个引脚32KB Flash8KB RAM主打低功耗和小体积。把这两颗芯片搭在一起就是一个典型的“低功耗采集端 高可靠非易失存储”的组合。这篇文章面向的是正在做工业数据记录、嵌入式参数存储、或者想从 EEPROM/Flash 切换到 MRAM 的工程师。不管你是刚接触 SPI 存储的新手还是已经在用 W25Q 系列 Flash 的老手我都会把 MR25H40CDF 和 STM32L031C6 的配合使用拆开揉碎讲清楚——从硬件连接到 SPI 配置从读写时序到实际踩过的坑尽量让你看完就能直接上手。2. 方案整体设计与选型考量2.1 为什么选 MRAM 而不是 EEPROM 或 Flash先说选型逻辑。在一个工业数据记录器里存储介质的选择直接决定了系统架构。我用过 24C02 这类 I2C EEPROM也用过 W25Q64 这类 SPI NOR Flash它们各有各的问题。EEPROM 的问题在于写入速度。以 24C02 为例页写入周期典型值 5ms也就是说你写 8 个字节要等 5ms。如果每秒要记录 100 次传感器数据每次 16 字节那 EEPROM 根本扛不住。而且 EEPROM 的擦写寿命通常标称 100 万次在频繁写入的场景下几年就到头了。NOR Flash 的问题在于写入前必须擦除。W25Q64 的扇区是 4KB你改一个字节也得把整个扇区读出来、改掉、擦除、再写回去。这个“读-改-写”流程不仅慢而且在掉电时极其危险——擦除到一半断电整个扇区的数据就没了。虽然可以用磨损均衡和日志结构来缓解但软件复杂度直线上升。MR25H40CDF 的 MRAM 技术从根本上绕开了这些问题。它的写入不需要擦除直接覆盖写写入周期只有几十纳秒级别SPI 接口下受限于总线速度。擦写寿命官方标称 10^14 次以上基本上你写到设备报废都写不完。而且 MRAM 的写入是真正的非易失写入断电不会丢数据也不存在擦除过程中断电导致数据损坏的问题。注意MRAM 的“写入不需要擦除”是指存储单元层面。通过 SPI 接口写入时你仍然需要发送写命令和地址但不需要像 Flash 那样先发擦除命令。这是本质区别。2.2 STM32L031C6 作为主控的适配性分析STM32L031C6 是一颗很“克制”的 MCU。Cortex-M0 内核最高 32MHz 主频32KB Flash8KB RAM1 个 SPI 接口封装有 TSSOP20 和 UFQFPN20 可选。它的优势在于低功耗——运行模式下约 100μA/MHz停止模式下不到 1μA。对于电池供电的工业传感器节点来说这个功耗水平很关键。用它来驱动 MR25H40CDF资源上是够的。SPI 接口最高支持 16MHz受限于 GPIO 翻转速度和时钟分频MR25H40CDF 支持最高 40MHz 的 SPI 时钟所以 MCU 这边是瓶颈。不过对于数据记录应用来说16MHz 的 SPI 已经绰绰有余——每字节传输时间约 0.5μs写 256 字节不到 150μs。RAM 只有 8KB这意味着你不能开大缓冲区。实际使用中我通常开一个 64 字节或 128 字节的页缓冲区攒够一页再写减少 SPI 事务开销。32KB Flash 对于裸机程序来说也够用但如果你要跑 RTOS 或者文件系统就得精打细算了。2.3 硬件连接方案与注意事项MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准VCC、GND、SCK、SI、SO、CS、WP、HOLD。和 STM32L031C6 的连接方式如下MR25H40CDF 引脚STM32L031C6 引脚说明VCC3.3V供电2.7V~3.6VGNDGND共地SCKPA5 (SPI1_SCK)时钟SIPA7 (SPI1_MOSI)主出从入SOPA6 (SPI1_MISO)主入从出CSPA4 (GPIO)片选软件控制WP3.3V写保护不用时拉高HOLD3.3V保持不用时拉高这里有几个实际布线时容易忽略的点。第一CS 片选我建议用 GPIO 软件控制而不是用 SPI 外设的硬件 NSS。原因后面讲时序的时候会详细说。第二WP 和 HOLD 引脚一定要拉高悬空的话芯片可能进入不确定状态。第三SCK 和 SI/SO 走线尽量等长如果 PCB 上 SPI 走线超过 10cm建议在 SCK 上串一个 22Ω~33Ω 的电阻做阻抗匹配减少过冲和振铃。提示MR25H40CDF 的 VCC 建议加一个 100nF 陶瓷电容就近去耦再并一个 1μF 钽电容做低频滤波。MRAM 在写入瞬间电流会有小幅波动去耦不好可能导致写入失败。3. SPI 通信协议与 MR25H40CDF 指令集详解3.1 SPI 模式选择与时序要求MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。这两种模式的区别在于时钟空闲电平和数据采样边沿。模式 0 下SCK 空闲为低数据在上升沿采样模式 3 下SCK 空闲为高数据在上升沿采样。我一般用模式 0因为 STM32 的 SPI 外设在模式 0 下配置最直观。在 CubeMX 里设置 CPOLLowCPHA1Edge 就是模式 0。实际调试时如果你发现读出来的数据全是 0xFF 或者 0x00第一件事就是拿示波器看 SCK 和 MOSI 的相位关系确认模式对不对。时序参数方面MR25H40CDF 在 3.3V 供电下最高支持 40MHz。但 STM32L031C6 的 SPI1 挂在 APB2 总线上最高时钟是 32MHz分频后最高只能到 16MHz32/2。所以实际 SPI 时钟我通常设成 8MHz 或 16MHz。8MHz 更稳16MHz 在长走线或者劣质杜邦线上可能出错。3.2 核心指令集与操作码MR25H40CDF 的指令集和标准 SPI Flash 很像但去掉了擦除相关的指令。核心指令如下指令名称操作码功能说明WREN0x06写使能写入前必须发送WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据RDID0x9F读设备 ID写操作的标准流程是拉低 CS → 发送 WREN0x06→ 拉高 CS → 拉低 CS → 发送 WRITE0x02→ 发送 3 字节地址 → 发送数据 → 拉高 CS。注意 WREN 和 WRITE 之间必须有一次 CS 的上升沿这是很多新手容易漏掉的步骤。读操作简单一些拉低 CS → 发送 READ0x03→ 发送 3 字节地址 → 读取数据 → 拉高 CS。READ 指令支持连续读取地址会自动递增直到你拉高 CS。3.3 状态寄存器与写保护机制状态寄存器只有 1 个字节但里面的位很重要位名称说明7SRWD状态寄存器写保护6-3保留读为 02BP1块保护位 11BP0块保护位 00WEL写使能锁存WEL 位在发送 WREN 后自动置 1写入完成后自动清 0。如果你发完 WREN 再读状态寄存器看到 WEL1说明写使能成功。如果 WEL 一直是 0检查 CS 时序和 SPI 模式。BP0 和 BP1 用于块保护可以保护部分或全部存储区域不被写入。在工业应用中我通常把参数区和数据记录区分开参数区用块保护锁住防止程序跑飞时误写。具体保护范围参考数据手册不同组合保护的区域不同。注意WRSR 指令写入状态寄存器时也需要先发 WREN。而且如果 SRWD 位被置 1 且 WP 引脚为低状态寄存器就变成只读无法再修改。所以 WP 引脚在正常使用时一定要拉高。4. STM32L031C6 端 SPI 驱动实现与代码拆解4.1 CubeMX 配置要点用 CubeMX 配置 SPI1 的时候几个关键参数这样设Mode: Full-Duplex MasterData Size: 8 BitsFirst Bit: MSB FirstClock Polarity: LowClock Phase: 1 EdgeNSS: SoftwarePrescaler: 432MHz/4 8MHzBaud Rate: 8 MBits/sGPIO 方面PA5/PA6/PA7 自动配置为 SPI 复用功能PA4 配置为 GPIO Output初始电平高CS 空闲为高。注意 PA4 的输出速度建议设为 High这样 CS 拉低和拉高的边沿更陡减少时序问题。4.2 底层读写函数实现先定义 CS 控制宏#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_WriteEnable(void) { uint8_t cmd 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); MRAM_CS_HIGH(); }读状态寄存器uint8_t MRAM_ReadStatus(void) { uint8_t cmd 0x05; uint8_t status 0; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_HIGH(); return status; }写数据函数void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t cmd[4]; cmd[0] 0x02; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Transmit(hspi1, data, len, 1000); MRAM_CS_HIGH(); }读数据函数void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0xFF; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; MRAM_CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 1000); MRAM_CS_HIGH(); }这几个函数看起来简单但有几个细节值得说。第一HAL_SPI_Transmit 和 HAL_SPI_Receive 的超时参数我给了 100ms 和 1000ms实际在 8MHz 下传输 256 字节只需要 256μs给足超时是为了防止意外阻塞。第二写操作中 WREN 和 WRITE 之间必须有 CS 拉高再拉低的过程上面的代码通过 MRAM_WriteEnable 函数内部的 CS 操作实现了这一点。4.3 页写入与缓冲区管理策略MR25H40CDF 没有页写入限制你可以一次写任意长度地址自动递增。但实际使用中我建议按 64 字节或 128 字节对齐写入。原因有两个一是 STM32L031C6 的 RAM 有限大缓冲区不现实二是分页写入便于实现日志结构和磨损均衡虽然 MRAM 不需要磨损均衡但日志结构便于管理数据版本。我的做法是定义一个 128 字节的环形缓冲区采集到的数据先存入缓冲区攒够 128 字节或者达到定时周期就触发一次 MRAM 写入。写入地址用一个全局变量维护每次写完递增 128到末尾回绕。#define BUF_SIZE 128 static uint8_t mram_buf[BUF_SIZE]; static uint16_t buf_idx 0; static uint32_t write_addr 0; void MRAM_BufferWrite(uint8_t *data, uint16_t len) { for (uint16_t i 0; i len; i) { mram_buf[buf_idx] data[i]; if (buf_idx BUF_SIZE) { MRAM_Write(write_addr, mram_buf, BUF_SIZE); write_addr BUF_SIZE; if (write_addr MRAM_SIZE) write_addr 0; buf_idx 0; } } }这个策略在工业数据记录中很实用。128 字节的缓冲区只占 128 字节 RAM对 8KB RAM 的 MCU 来说毫无压力。写入频率取决于数据产生速率如果每秒产生 10 字节数据大约 13 秒写一次 MRAM平均写入带宽极低功耗也很小。5. 实际项目中的性能测试与数据对比5.1 写入速度实测我在 8MHz SPI 时钟下做了几组测试用 STM32L031C6 的 TIM2 做微秒级计时操作数据量耗时备注单字节写入1 字节约 5μs含 WREN 和 CS 开销128 字节写入128 字节约 140μs含 WREN 和 CS 开销256 字节写入256 字节约 270μs含 WREN 和 CS 开销单字节读取1 字节约 4μs含 CS 开销256 字节读取256 字节约 260μs含 CS 开销对比我之前用过的 24C02 EEPROM写 128 字节需要约 5ms页写入周期MR25H40CDF 快了将近 40 倍。对比 W25Q64 Flash虽然 Flash 的页写入时间约 0.7ms但加上擦除时间扇区擦除约 45ms实际写入 128 字节的等效耗时超过 45msMRAM 快了 300 倍以上。5.2 功耗表现STM32L031C6 在 32MHz 运行模式下电流约 3.5mASPI 传输时由于 GPIO 翻转电流会略高约 4mA。MR25H40CDF 在写入时电流约 15mA读取时约 5mA待机时不到 10μA。实际数据记录场景下假设每 10 秒写入 128 字节写入耗时 140μs那么 MRAM 的平均电流贡献是 15mA × 140μs / 10s 0.21μA几乎可以忽略。MCU 大部分时间可以进入 STOP 模式整体平均功耗可以做到 10μA 以下用一颗 CR2032 纽扣电池能跑好几年。5.3 数据保持与可靠性MRAM 的数据保持时间官方标称 20 年工作温度范围 -40°C 到 85°C工业级。我做过一个简单的温度循环测试-40°C 和 85°C 各保持 1 小时循环 10 次写入的数据没有出现位翻转。当然这个测试样本量小不能替代正式认证但至少说明在常规工业环境下是可靠的。抗辐射方面MRAM 天生比 Flash 和 SRAM 强因为它是磁性存储不受单粒子翻转影响。如果你的应用场景有辐射顾虑比如某些工业检测设备MRAM 是个加分项。6. 常见问题排查与避坑经验6.1 读出来全是 0xFF 或 0x00这是最常见的现象。排查顺序如下先确认 CS 是否正常工作。用示波器看 CS 在传输期间是否拉低传输结束后是否拉高。如果 CS 一直低芯片可能一直处于选中状态后续指令会错乱。确认 SPI 模式。MR25H40CDF 支持模式 0 和模式 3如果你配成模式 1 或模式 2数据采样边沿就错了。拿示波器同时看 SCK 和 MOSI确认数据在 SCK 的上升沿稳定。确认供电。MRAM 在 2.7V 以下可能工作不正常用万用表量一下 VCC 引脚电压。检查 WP 和 HOLD 引脚。这两个引脚如果悬空芯片可能进入写保护或保持状态导致读写异常。6.2 写入后读出来数据不对如果写入返回成功但读出来不对大概率是地址对齐或者写入流程有问题。检查以下几点WREN 和 WRITE 之间是否有 CS 上升沿。没有这个上升沿WEL 位不会锁存写入会被忽略。地址是否超出范围。MR25H40CDF 是 4Mbit地址范围 0x000000 到 0x07FFFF。超出范围的地址会回绕导致数据写到错误位置。写入过程中是否有中断打断。SPI 传输期间如果被高优先级中断打断CS 可能被意外拉高导致写入不完整。建议在 SPI 传输前后关中断或者用 DMA 传输。6.3 SPI 通信不稳定偶尔出错如果通信大部分正常但偶尔出错通常是硬件问题SPI 走线太长或没有阻抗匹配。在 SCK 上串 22Ω 电阻缩短走线。电源去耦不足。在 MRAM 的 VCC 引脚就近加 100nF 电容。地线回路不好。确保 MCU 和 MRAM 共地地线尽量宽。SPI 时钟太快。降到 4MHz 试试如果稳定了再逐步提高。6.4 常见问题速查表现象可能原因解决方法读全 0xFFCS 未拉低、SPI 模式错、供电不足检查 CS 时序、确认模式 0/3、量 VCC读全 0x00MISO 未连接、芯片未选中检查 SO 引脚焊接、CS 信号写入无效缺少 WREN、CS 时序错确认 WREN 后 CS 有上升沿数据偶尔出错走线干扰、时钟太快加匹配电阻、降低 SPI 时钟状态寄存器读不到指令错误、SPI 相位错确认 RDSR 操作码 0x05、检查 CPHA写入后立即读出错未等待写入完成MRAM 写入很快但建议加 1μs 延时提示MRAM 的写入虽然不需要擦除但写入周期仍然需要时间。在极高频率的连续写入场景下建议每次写入后读状态寄存器确认 WEL 已清 0再进行下一次写入。7. 工业场景下的应用扩展与优化思路7.1 掉电保护与数据完整性工业现场最怕的就是掉电导致数据丢失。MRAM 的非易失特性天然适合掉电保护但软件层面还需要配合。我的做法是在 MRAM 中维护一个“写入指针”和“校验和”每次写入数据后更新指针和校验和。上电时先读指针和校验和验证数据完整性如果校验失败就从上一个有效位置继续。具体实现是在 MRAM 的固定地址比如 0x000000存放一个 16 字节的头结构typedef struct { uint32_t magic; // 固定值 0x4D52414D (MRAM) uint32_t write_ptr; // 当前写入位置 uint32_t data_count; // 已写入数据条数 uint32_t crc; // 头结构 CRC32 } MRAM_Header_t;每次写入数据前先更新头结构再写数据。上电时先读头结构验证 magic 和 crc然后从 write_ptr 继续。这个方案简单可靠我在多个项目中用过没有出现过掉电丢数据的情况。7.2 多区域管理与参数存储MR25H40CDF 的 512KB 空间可以划分成多个区域参数区、数据记录区、日志区、备份区。参数区存放设备配置数据记录区存放采集数据日志区存放系统事件备份区存放关键数据的副本。参数区我建议用块保护锁住防止程序跑飞时误写。数据记录区用环形缓冲区管理写满后覆盖最旧的数据。日志区用追加写入每条日志带时间戳和 CRC。备份区定期从参数区同步。这种分区管理在工业设备中很常见好处是数据隔离一个区域出问题不会影响其他区域。实现上只需要在头结构里记录各区域的起始地址和大小读写时做地址映射即可。7.3 低功耗优化技巧STM32L031C6 的低功耗模式配合 MRAM 的非易失特性可以做出极低功耗的数据记录器。我的优化策略是MCU 大部分时间处于 STOP 模式RTC 定时唤醒比如每 10 秒唤醒一次。唤醒后采集传感器数据存入 RAM 缓冲区。缓冲区满或者达到写入周期时唤醒 SPI 和 MRAM执行写入。写入完成后立即关闭 SPI 时钟MCU 回到 STOP 模式。SPI 外设在不用时一定要关掉时钟__HAL_RCC_SPI1_CLK_DISABLE()否则会有漏电流。MRAM 本身在待机时电流不到 10μA对整体功耗影响很小。实测下来这个方案的平均功耗约 8μA用 CR2032 电池220mAh可以连续工作约 3 年。如果降低唤醒频率到每分钟一次功耗可以降到 3μA 以下电池寿命超过 8 年。7.4 与文件系统的配合如果你需要在 MRAM 上跑文件系统比如 LittleFS 或 SPIFFS也是可以的。MR25H40CDF 的 512KB 空间对于小型文件系统来说够用。但要注意文件系统通常假设底层存储有擦除操作而 MRAM 没有。你需要修改文件系统的底层驱动把擦除操作实现为空操作或者直接覆盖写。LittleFS 的块设备接口中erase 函数可以留空prog 函数直接调用 MRAM_Writeread 函数调用 MRAM_Read。这样 LittleFS 就能在 MRAM 上正常工作。不过 LittleFS 的磨损均衡机制对 MRAM 来说是多余的反而增加了写入放大。如果只是存几个配置文件直接裸写 MRAM 更简单高效。8. 调试工具与实操建议调试 SPI 存储设备光靠 printf 是不够的。我常用的工具组合是逻辑分析仪抓 SCK、MOSI、MISO、CS 四根线直接看时序。Saleae 或者国产的 24MHz 逻辑分析仪都够用。抓到的数据可以导出成 SPI 解码格式直接看指令和数据。示波器看信号质量特别是 SCK 的过冲和振铃。如果 SCK 上升沿有明显的振铃说明阻抗不匹配需要加串阻。ST-Link STM32CubeIDE在线调试打断点看变量配合逻辑分析仪交叉验证。实操中我建议先写一个简单的测试程序写一个字节到地址 0x000000然后读回来比较是否一致。如果这一步通过再测试连续写入和读取。如果第一步就不通过先查硬件连接和 SPI 配置不要急着改代码。还有一个容易忽略的点MR25H40CDF 的 CS 拉低到 SCK 第一个边沿之间需要至少 5ns 的建立时间。STM32 的 GPIO 翻转速度很快通常没问题但如果你在 CS 拉低后立即启动 SPI 传输某些情况下可能违反这个时序。保险的做法是在 CS 拉低后加一个 __NOP() 或者几微秒的延时。我在实际项目中还遇到过一个问题SPI 传输过程中如果发生 DMA 传输错误CS 可能一直保持低电平导致 MRAM 一直处于选中状态。解决方法是给 CS 加一个超时机制或者在 DMA 传输完成回调中强制拉高 CS。这个坑在调试阶段不容易发现但在长时间运行后可能出现值得注意。最后分享一个实用技巧如果你手头没有逻辑分析仪可以用 STM32 的 SPI 回环模式先验证 MCU 端的 SPI 配置是否正确。把 MOSI 和 MISO 短接发送什么就应该收到什么。如果回环测试通过说明 MCU 端没问题问题在 MRAM 端或者硬件连接上。这个方法简单有效能帮你快速定位问题范围。