
1. 项目缘起与方案选型为什么是 MRAM 加 STM32F446ZE1.1 一个真实的需求场景去年接手一个工业数据采集终端的项目设备安装在配电柜里环境温度夏天能到 70 度冬天户外柜内能到零下 30 度。设备每 200 毫秒采集一次电流、电压、温度数据需要把关键参数和故障记录保存下来断电不能丢而且要求写入次数足够多——因为有些状态量是秒级刷新的一天下来写入次数轻松上万次。一开始我选的是常见的 SPI NOR Flash比如 W25Q 系列便宜、资料多、STM32 的 HAL 库直接支持。但跑了一段时间发现两个问题第一写入速度慢页编程一次 256 字节要几毫秒频繁写入时 CPU 被阻塞得厉害第二擦写寿命有限虽然标称 10 万次但实际在高温环境下会打折扣而且擦除操作必须按扇区来写之前还得先擦逻辑复杂。后来换成了 FRAM铁电存储器读写快、寿命几乎无限但容量小、价格高而且高温下数据保持能力会下降。再后来接触到 MRAM磁性随机存储器才算是找到了平衡点。这次用的就是 Everspin 的 MR25H40CDF4Mbit 容量SPI 接口工业级温度范围读写寿命号称无限次数据保持 20 年以上。主控选的是 STM32F446ZECortex-M4 内核180MHz 主频带 FPUSPI 接口丰富工业场景里很常见。两者搭配一个负责高速可靠存储一个负责实时控制和数据处理算是嵌入式存储方案里比较务实的一个组合。1.2 MR25H40CDF 到底特别在哪MR25H40CDF 是一颗 4Mbit512K 字节的 MRAMSPI 接口最高时钟 40MHz。和传统存储介质比它的特点很鲜明写入不需要擦除这是和 Flash 最大的区别。Flash 写之前必须先擦除整个扇区MRAM 可以直接覆盖写像 RAM 一样。这意味着写入逻辑简单很多不需要管理擦除块也不需要做磨损均衡。写入速度快没有擦除等待单字节写入和页写入的时间基本就是 SPI 传输时间。40MHz 时钟下写 256 字节大概几十微秒比 Flash 快一个数量级。寿命几乎无限官方标称读写次数无限制实际测试中连续写入几天几夜也没有出现坏块。对于需要频繁记录状态的应用这一点非常关键。数据保持时间长磁性存储原理断电后数据保持 20 年以上而且不需要像 Flash 那样担心电荷泄漏。功耗低写入电流比 Flash 小待机电流也低适合电池供电或低功耗场景。当然也有缺点容量比同价位的 Flash 小价格贵一些而且国内货源不如 Flash 那么充足。但对于工业场景里需要频繁写入、高可靠性的小数据量存储MRAM 是很合适的选择。1.3 STM32F446ZE 的 SPI 资源分配STM32F446ZE 有 6 个 SPI 接口其中 SPI1、SPI4 支持最高 45MHzSPI2、SPI3 支持 22.5MHz。MR25H40CDF 最高支持 40MHz所以理论上可以用 SPI1 跑到 45MHz但实际布线要考虑信号完整性我一般会降到 20MHz 到 30MHz 之间留点余量。在这个项目里我把 MR25H40CDF 挂在 SPI1 上引脚分配如下功能引脚说明SCKPA5SPI1 时钟MISOPA6主入从出MOSIPA7主出从入CSPA4软件片选GPIO 控制VCC3.3V电源GNDGND地片选我用的是软件片选而不是硬件 NSS。原因后面会详细说简单讲就是软件片选更灵活可以精确控制时序避免多从机场景下的冲突。2. 硬件设计与 SPI 通信基础2.1 MRAM 的引脚连接与去耦MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准1 脚 CS片选低有效2 脚 SO数据输出3 脚 WP写保护低有效不用的话接高4 脚 GND地5 脚 SI数据输入6 脚 SCK时钟7 脚 HOLD保持低有效不用的话接高8 脚 VCC电源实际布线时VCC 和 GND 之间要加一个 0.1uF 的陶瓷电容尽量靠近芯片引脚。如果走线较长再并一个 1uF 的电容。SPI 信号线尽量等长SCK 走线避免直角如果板上还有其他高速信号MRAM 的走线要远离。注意WP 和 HOLD 引脚不要悬空悬空可能导致意外写保护或通信异常。我一般直接接 3.3V如果不需要硬件写保护功能的话。2.2 SPI 模式选择Mode 0 还是 Mode 3MR25H40CDF 支持 SPI Mode 0 和 Mode 3。Mode 0 是 CPOL0CPHA0时钟空闲低电平第一个边沿采样Mode 3 是 CPOL1CPHA1时钟空闲高电平第二个边沿采样。我一般用 Mode 0因为 STM32 的 HAL 库默认配置就是 Mode 0而且大多数逻辑分析仪和示波器在 Mode 0 下解码更直观。实际测试中两种模式在 20MHz 以下都能稳定工作但 Mode 0 的时序余量更大一些。配置 SPI 时关键参数如下hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial 10;STM32F446ZE 的 SPI1 挂在 APB2 总线上时钟 90MHz。分频系数 8 的话SPI 时钟是 11.25MHz。如果想跑到 22.5MHz分频系数改成 4。我实测 22.5MHz 下读写稳定但为了兼容不同批次的 MRAM 芯片最终产品里用的是 11.25MHz。2.3 软件片选 vs 硬件片选STM32 的 SPI 支持硬件 NSS 和软件 NSS。硬件 NSS 在单主单从场景下没问题但多从机时容易出问题因为 NSS 引脚的电平变化和 SPI 使能时序可能不匹配。软件片选就是用一个普通 GPIO 控制 CS 引脚在每次传输前拉低传输后拉高。这样做的好处是时序完全可控可以在 CS 拉低后加一点延时再发时钟确保 MRAM 准备好。多从机时每个从机独立片选不会互相干扰。调试方便逻辑分析仪上能清楚看到每次传输的边界。代码里这样操作#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_Select(void) { MRAM_CS_LOW(); // 可选加1us延时确保MRAM检测到片选 } void MRAM_Deselect(void) { MRAM_CS_HIGH(); // 可选加1us延时确保MRAM完成内部操作 }实操心得CS 拉低后不要立刻发时钟尤其是长线缆或高时钟频率下加 1 到 2 微秒的延时能显著提高稳定性。这个延时可以用__NOP()循环实现也可以用定时器。3. MRAM 驱动实现从指令集到读写函数3.1 MR25H40CDF 的指令集MR25H40CDF 的指令集和标准 SPI Flash 类似但简化了很多因为不需要擦除。常用指令如下指令名称指令码说明WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据RDID0x9F读设备ID状态寄存器只有一位有用就是 WEL写使能锁存bit1。每次写操作前必须先发 WREN 指令否则写不进去。写完成后 WEL 自动清零。读设备 ID 返回 7 个字节前几个是厂商 ID 和产品 ID可以用来检测芯片是否正常连接。MR25H40CDF 的厂商 ID 是 0xEverspin 的代码具体值可以查数据手册。3.2 底层字节读写函数先实现最基础的字节收发函数基于 HAL 库uint8_t MRAM_SPI_Transfer(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(hspi1, txData, rxData, 1, HAL_MAX_DELAY); return rxData; }这个函数发一个字节同时收一个字节。SPI 是全双工的发和收同时进行。然后实现写使能void MRAM_WriteEnable(void) { MRAM_Select(); MRAM_SPI_Transfer(0x06); MRAM_Deselect(); }读状态寄存器uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_Select(); MRAM_SPI_Transfer(0x05); status MRAM_SPI_Transfer(0xFF); MRAM_Deselect(); return status; }3.3 页写与连续读的实现MR25H40CDF 支持页写每页 256 字节。写操作可以跨页但跨页时地址会回绕到页首所以最好按页对齐写。写数据函数void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_WriteEnable(); MRAM_Select(); MRAM_SPI_Transfer(0x02); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint16_t i 0; i len; i) { MRAM_SPI_Transfer(data[i]); } MRAM_Deselect(); // 等待写完成实际MRAM写入很快但保险起见延时 HAL_Delay(1); }读数据函数void MRAM_Read(uint32_t addr, uint8_t *buffer, uint16_t len) { MRAM_Select(); MRAM_SPI_Transfer(0x03); MRAM_SPI_Transfer((addr 16) 0xFF); MRAM_SPI_Transfer((addr 8) 0xFF); MRAM_SPI_Transfer(addr 0xFF); for (uint16_t i 0; i len; i) { buffer[i] MRAM_SPI_Transfer(0xFF); } MRAM_Deselect(); }注意地址是 24 位的因为 4Mbit 等于 512K 字节需要 19 位地址但 MRAM 用 24 位地址格式高 5 位忽略。发送地址时要从高到低依次发送。3.4 写保护与状态检查每次写之前最好检查一下状态寄存器的 WEL 位确保写使能成功。虽然大多数情况下没问题但在电磁干扰强的工业环境里偶尔会出现 WREN 指令没被正确识别的情况。uint8_t MRAM_WriteWithCheck(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_WriteEnable(); if ((MRAM_ReadStatus() 0x02) 0) { return 1; // 写使能失败 } MRAM_Select(); MRAM_SPI_Transfer(0x02); // ... 发送地址和数据 MRAM_Deselect(); return 0; }这个检查会增加一点开销但对于可靠性要求高的场景是值得的。4. 数据存储策略与磨损管理4.1 为什么 MRAM 不需要磨损均衡Flash 需要磨损均衡是因为擦写寿命有限每个扇区擦写次数不能超过 10 万次。MRAM 没有这个限制理论上可以无限次写入所以不需要复杂的磨损均衡算法。但这不意味着可以随便写。实际使用中我还是会做一些优化批量写入不要每采集一个数据就写一次攒够一定数量再写减少 SPI 传输次数。缓存机制在 RAM 里维护一个缓存区定期同步到 MRAM。关键数据优先只把真正需要断电保存的数据写到 MRAM其他数据放 RAM。4.2 数据结构设计在这个项目里我把存储区分成几个区域区域起始地址大小用途配置区0x000004KB设备参数、校准系数日志区0x01000256KB运行日志、故障记录数据区0x41000252KB采集数据、统计值配置区用结构体存储每次修改后整体写入。日志区用环形缓冲区新日志覆盖旧日志。数据区按时间戳索引支持快速查询。结构体定义示例typedef struct { uint32_t magic; // 0x55AA55AA用于检测是否初始化 float voltage_gain; // 电压校准系数 float current_gain; // 电流校准系数 uint16_t sample_rate; // 采样率 uint8_t device_id[16]; // 设备ID uint32_t crc; // 校验值 } DeviceConfig;写入时先算 CRC读出来时校验防止数据损坏。4.3 环形日志缓冲区的实现日志区用环形缓冲区写指针到达末尾后回到开头。每条日志包含时间戳、事件类型和数据。typedef struct { uint32_t timestamp; uint8_t event_type; uint8_t data[11]; } LogEntry; // 16字节对齐写日志时先读当前写指针写入数据然后更新写指针。写指针本身也保存在 MRAM 里每次更新。void Log_Write(uint8_t type, uint8_t *data) { uint32_t write_ptr GetWritePointer(); LogEntry entry; entry.timestamp HAL_GetTick(); entry.event_type type; memcpy(entry.data, data, 11); MRAM_Write(LOG_BASE write_ptr * sizeof(LogEntry), (uint8_t*)entry, sizeof(LogEntry)); write_ptr (write_ptr 1) % LOG_MAX_ENTRIES; SetWritePointer(write_ptr); }实操心得写指针更新和日志写入最好放在一个原子操作里或者用双备份机制。我遇到过写日志过程中断电导致写指针和实际数据不一致的情况。后来改成写指针存两份读取时比较不一致就用备份恢复。5. 性能测试与优化实录5.1 读写速度实测用逻辑分析仪抓 SPI 波形实测数据如下操作时钟频率耗时说明单字节写11.25MHz约 3us含 WREN 和地址256字节页写11.25MHz约 240us含 WREN 和地址单字节读11.25MHz约 2us含地址256字节读11.25MHz约 230us含地址读设备ID11.25MHz约 8us7字节返回对比 W25Q64 Flash页写 256 字节需要约 3ms含擦除MRAM 快了 10 倍以上。读速度两者差不多因为都受 SPI 时钟限制。5.2 优化技巧DMA 传输当需要读写大量数据时用 DMA 可以解放 CPU。STM32F446ZE 的 SPI1 支持 DMA 请求。配置 DMAhdma_spi1_tx.Instance DMA2_Stream3; hdma_spi1_tx.Init.Channel DMA_CHANNEL_3; hdma_spi1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_spi1_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_spi1_tx.Init.Mode DMA_NORMAL; hdma_spi1_tx.Init.Priority DMA_PRIORITY_HIGH;用 DMA 后CPU 可以在数据传输期间处理其他任务整体效率提升明显。但要注意DMA 传输完成前不能拉高 CS否则数据会截断。5.3 电源管理与低功耗工业设备有时需要低功耗模式。MR25H40CDF 待机电流约 10uA写入电流约 5mA读电流约 3mA。STM32F446ZE 在 STOP 模式下电流约 100uA。低功耗策略不访问 MRAM 时把 SPI 外设关闭CS 保持高电平。数据采集间隔较长时STM32 进入 STOP 模式用 RTC 定时唤醒。唤醒后先初始化 SPI再访问 MRAM。实测下来整体平均电流可以降到 1mA 以下对于电池供电的设备很关键。6. 常见问题与排查技巧6.1 读写失败排查表现象可能原因排查方法解决方法读设备ID返回全0或全FFSPI接线错误检查MISO/MOSI是否接反交换MISO和MOSI写入后读出来不对写使能失败读状态寄存器WEL位重新发WREN检查CS时序偶尔读写错误时钟太快降低SPI时钟测试降频或加延时高温下数据丢失电源不稳测VCC纹波加去耦电容检查电源多从机冲突片选逻辑错误逻辑分析仪看CS信号确保同一时间只有一个CS低6.2 一个典型的时序问题有一次调试发现写入 256 字节后前几个字节正确后面的数据错乱。用逻辑分析仪抓波形发现 CS 在数据传输过程中有毛刺。原因是软件片选用的是 GPIO但在中断里也操作了同一个 GPIO 端口导致 CS 被意外拉高。解决方法是在操作 CS 时关中断或者用独立的 GPIO 端口。void MRAM_Select(void) { __disable_irq(); MRAM_CS_LOW(); __enable_irq(); }注意关中断时间要尽量短只包住 CS 操作不要包住整个数据传输。6.3 数据保持与校验虽然 MRAM 数据保持时间很长但在工业环境里电磁干扰、电源波动都可能影响数据。我一般会做以下校验每个数据块加 CRC16 或 CRC32 校验。关键配置存两份读取时比较。定期巡检发现校验失败就用备份恢复。CRC 计算可以用 STM32 的硬件 CRC 外设速度快不占 CPU。7. 项目落地经验与扩展思路7.1 从原型到产品的注意事项原型阶段用杜邦线连接跑通了就行。但到产品阶段要考虑的事情多很多PCB 布线SPI 信号线要短最好在同一层避免过孔。SCK 和 MOSI 不要平行走太长减少串扰。电源滤波MRAM 的 VCC 引脚旁边必须放 0.1uF 电容如果板上开关电源噪声大再加一个磁珠。连接器如果 MRAM 通过连接器连接要选可靠的连接器工业环境振动大接触不良是常见故障。防护SPI 信号线可以加 TVS 管防止静电和浪涌。7.2 扩展方向文件系统与数据管理如果数据量再大一些可以考虑在 MRAM 上跑一个轻量级文件系统比如 LittleFS 或 SPIFFS。这样可以用文件的方式管理数据不用自己写环形缓冲区。但文件系统有开销会占用一些存储空间和 CPU 时间。对于小数据量、高频写入的场景直接操作地址更高效。另一个扩展方向是加一颗 NOR Flash 做冷数据存储MRAM 做热数据缓存。热数据频繁更新冷数据偶尔写入两者结合成本和性能都能兼顾。7.3 我在这个项目里踩过的坑第一个坑是 CS 时序。一开始没加延时CS 拉低后立刻发时钟在 22.5MHz 下偶尔出错。后来加了 1us 延时问题消失。第二个坑是写指针更新。日志区用环形缓冲区写指针存在 MRAM 里。有一次断电后重启发现写指针指向的位置和实际数据不匹配导致日志错乱。后来改成写指针双备份问题解决。第三个坑是 DMA 传输。用 DMA 写 MRAM 时DMA 传输完成中断里拉高 CS但有时候中断响应慢CS 拉高晚了导致多写了一个字节。后来改成在 DMA 传输完成回调里拉高 CS并且加了一个标志位确保不会重复操作。这些坑在数据手册里不会写只有实际做过才会遇到。希望这些经验能帮到正在做类似项目的朋友。