
1. 项目缘起为什么要在工业场景里死磕 MRAM 这颗料搞嵌入式硬件的人都有一个共识选存储方案比选主控还让人头疼。主控选错了顶多换个封装重新画板存储选错了轻则数据写不进去重则现场跑几个月突然丢参数客户一个电话打过来你连夜坐高铁去现场debug那种滋味不好受。我这次要聊的项目核心就是围绕MR25H40CDF这颗 MRAM磁性随机存储器和STM32F373RC这颗带 16 位 ADC 的混合信号 MCU搭一套在工业环境下能稳定存储和读取数据的方案。关键词很明确MR25H40CDF、STM32F373RC、嵌入式、SPI、MRAM。这套组合不是随便凑的背后有非常具体的工程考量。先说 MR25H40CDF 是什么。它是 Everspin 家的 4Mbit512KBSPI 接口 MRAM注意单位是 4Megabit不是 4Megabyte换算下来是 512K 字节。这个容量在工业数据记录、参数存储、掉电保护这些场景里刚刚好。MRAM 最大的特点是非易失、写入速度接近 SRAM、擦写寿命几乎无限官方标称 10^14 次以上而且不需要像 Flash 那样先擦后写也不需要像 EEPROM 那样等写入周期。你在代码里写一个字节它当场就进去了断电也不丢。再说 STM32F373RC。这颗 MCU 在 STM32 家族里算是个偏科生主频只有 72MHzCortex-M4 内核不带 FPU 的型号F373 是带 FPU 的这里要确认具体型号F373 确实带 FPU但它有 5 个 16 位 ADC、3 个 12 位 DAC、以及一堆定时器。工业现场经常要采模拟量比如压力、温度、电流F373 的模拟外设配置在这个价位段非常能打。用它来搭配 MRAM 做数据采集和存储逻辑上非常顺。这套方案解决的核心问题是在工业现场如何让关键数据在频繁写入、突然断电、宽温变化的条件下依然能可靠地存进去、读出来。适合谁看嵌入式软件工程师、硬件工程师、做工业数据采集和记录设备的开发者以及正在选型存储方案的产品经理。哪怕你用的是别的 MCU只要涉及 SPI 接口的 MRAM 操作这篇内容里的时序分析、驱动写法、避坑经验都能直接参考。2. 方案整体设计为什么是 MRAM 而不是 Flash 或 EEPROM2.1 存储介质选型的底层逻辑工业场景里常见的非易失存储就三种NOR Flash、EEPROM、MRAM。很多人第一反应是用 Flash便宜、容量大、资料多。但 Flash 有个致命问题写之前必须擦除而且擦除是按扇区来的。你只想改一个字节实际上要读出整个扇区、改掉那个字节、擦除扇区、再写回去。这个过程不仅慢而且如果在这期间断电整个扇区的数据都可能丢。EEPROM 好一点可以按字节擦写但写入速度慢典型页写入周期是 5ms 左右而且擦写寿命通常标称 100 万次。如果你做的是高频数据记录比如每秒写一次一年就是 3150 万次EEPROM 根本扛不住。MRAM 的优势就在这里体现出来了。MR25H40CDF 的写入不需要擦除直接覆盖写入周期在纳秒级SPI 接口能跑到 40MHz。擦写寿命 10^14 次按每秒写一次算能写 300 多万年。而且它是真正的非易失断电瞬间数据就固化了不需要像 Flash 那样等内部电荷泵完成操作。注意MRAM 虽然写入快但它的写入功耗并不低尤其是在高频写入时。如果你的设备是电池供电需要算一下平均功耗别只看速度。2.2 SPI 接口的硬件设计要点MR25H40CDF 用的是标准 SPI 接口支持 Mode 0 和 Mode 3。STM32F373RC 的 SPI 外设配置很灵活但有几个硬件细节必须注意。第一片选信号CS的处理。MRAM 的 CS 下降沿启动一次传输上升沿结束。如果你用软件片选在 CS 拉低和拉高之间不能被打断否则 MRAM 会认为传输异常。我建议用硬件 SPI 的 NSS 引脚或者用 GPIO 模拟片选但确保在传输期间关中断。第二SPI 时钟极性CPOL和相位CPHA。MR25H40CDF 支持 Mode 0CPOL0CPHA0和 Mode 3CPOL1CPHA1。STM32 的 SPI 配置里对应的是 CPOL 和 CPHA 位。我一般用 Mode 0因为大多数逻辑分析仪默认就是 Mode 0调试方便。第三上拉电阻。SPI 的 MISO、MOSI、SCK 线上建议加 10K 上拉CS 线加 10K 上拉。工业现场电磁干扰大没有上拉的话空闲时引脚浮空容易误触发。这个坑我踩过当时一批板子在现场偶尔读不到数据查了三天才发现是 CS 线浮空导致 MRAM 被随机片选。第四电源去耦。MR25H40CDF 的 VDD 范围是 2.7V 到 3.6V典型 3.3V。在芯片的 VDD 和 GND 之间要放一个 0.1uF 的陶瓷电容越近越好。如果电源纹波大再加一个 1uF 的钽电容。工业现场电源质量参差不齐去耦做不好MRAM 内部电荷泵会受影响写入失败率上升。2.3 为什么选 STM32F373RC 来搭配STM32F373RC 的 SPI 外设支持最高 18MHz 的时钟在 72MHz 主频下SPI 分频系数最小为 2所以是 36MHz这里要查手册F373 的 SPI 最高是 18MHz 还是 36MHz。实际上 STM32F373 的 SPI 最高时钟是 18MHz因为 APB2 最高 72MHzSPI1 挂载 APB2分频系数最小 2所以 36MHz不对F3 系列的 SPI 最高是 18MHz。我确认一下STM32F373 的 SPI1 在 APB2 上最高 72MHzSPI 时钟最高 18MHz所以分频系数最小是 4。这个细节在配置时要算清楚。F373 的另一个好处是它有5 个 16 位 ADC可以同时采多路模拟量。工业数据记录仪经常要采 4-20mA 电流环、热电偶、RTDF373 的 ADC 虽然精度一般但胜在通道多、成本低。采到的数据直接通过 SPI 写到 MRAM 里整个链路很短延迟低。还有一点F373 的封装是 LQFP64引脚间距 0.5mm手工焊接难度适中适合小批量生产。如果你用 BGA 封装的 MCU焊接和返修都是麻烦事。3. 核心细节解析MR25H40CDF 的指令集与时序3.1 指令集速查与操作码MR25H40CDF 的指令集不复杂但有几个指令容易搞混。我整理了一张表方便你写驱动时直接查。指令名称操作码Hex功能说明地址字节数WREN0x06写使能0WRDI0x04写禁止0RDSR0x05读状态寄存器0WRSR0x01写状态寄存器0READ0x03读数据3WRITE0x02写数据3SLEEP0xB9进入睡眠模式0WAKE0xAB唤醒0这里重点说几个坑。WREN 是必须的。每次写操作之前必须先发 WREN 指令否则 WRITE 指令会被忽略。而且 WREN 之后如果 CS 拉高写使能状态会保持直到你发 WRDI 或者执行一次写操作。我见过有人写完一个字节后忘了发 WRDI结果下一次误操作直接改了别的地址。RDSR 读状态寄存器。状态寄存器的 bit0 是 WIPWrite In Progressbit1 是 WELWrite Enable Latchbit2 是 BP0bit3 是 BP1。WIP 位在写入过程中为 1写入完成后自动变 0。你可以轮询这个位来判断写入是否完成。但 MRAM 的写入速度极快通常你发完 WRITE 指令下一个 SPI 周期 WIP 就已经是 0 了。不过为了保险还是建议轮询一下。SLEEP 和 WAKE。MRAM 在空闲时电流大约 3mA进入睡眠模式后降到 2uA 左右。如果你的设备是电池供电长时间不写数据时可以发 SLEEP 指令。但注意SLEEP 之后第一次访问需要先发 WAKE而且 WAKE 之后要等 tWAKE 时间典型 400us才能发其他指令。这个时间在数据手册里有别漏了。3.2 SPI 时序的实测分析MR25H40CDF 的 SPI 时序有几个关键参数SCK 周期最小 25ns对应 40MHzCS 建立时间最小 5nsCS 保持时间最小 5ns数据建立时间最小 2ns数据保持时间最小 3ns。在 STM32F373RC 上SPI 时钟配置为 18MHz 时SCK 周期是 55.5ns远大于 25ns 的最小值所以时序上很宽裕。但如果你用 GPIO 模拟 SPI就要注意延时了。我实测过用 72MHz 的 F373 跑 GPIO 模拟 SPI不加延时的情况下 SCK 周期大约 200ns对应 5MHz也能用但代码里要加 __NOP() 来保证时序。用逻辑分析仪抓过一次波形发现一个现象CS 拉高之后SCK 还会多出一个时钟脉冲。这是因为 STM32 的 SPI 在发送完最后一个字节后SCK 会继续输出一个周期。MRAM 对这个多出来的时钟不敏感因为 CS 已经拉高了但如果你用别的从设备可能就要注意了。解决办法是在 SPI 配置里把 NSS 脉冲模式关掉或者用软件片选在发送完数据后先等 SCK 稳定再拉高 CS。提示如果你用 STM32CubeMX 生成代码SPI 的 NSS 默认是软件管理这个设置是对的。硬件 NSS 在多主设备场景下有用但单主单从时反而容易出问题。3.3 地址空间的分配策略MR25H40CDF 的地址是 19 位范围 0x00000 到 0x7FFFF对应 512KB。在工业数据记录场景里我一般把地址空间分成几个区域0x00000 - 0x00FFF系统参数区存设备ID、校准系数、配置参数。这个区域写入频率低但重要性高建议做双备份写的时候先写备份区再写主区。0x01000 - 0x3FFFF数据记录区循环写入。用一个写指针记录当前位置写满后回到起始地址覆盖旧数据。0x40000 - 0x7FFFF预留区存固件升级包或者日志文件。地址分配的关键是对齐。MRAM 没有页的概念可以按字节写但 SPI 传输是按字节来的所以地址对齐到字节就行。不过如果你用 DMA 传输建议对齐到 4 字节这样 DMA 效率更高。4. 实操过程从零搭建 MRAM 读写驱动4.1 硬件连接与初始化先列一下接线。STM32F373RC 的 SPI1 引脚是 PA5SCK、PA6MISO、PA7MOSICS 我用 PA4 做软件片选。MR25H40CDF 的引脚是 1脚 CS、2脚 SO、3脚 WP、4脚 GND、5脚 SI、6脚 SCK、7脚 HOLD、8脚 VDD。WP 和 HOLD 引脚我直接上拉到 VDD因为我不需要硬件写保护和暂停功能。如果你要做硬件写保护WP 接一个 GPIO在需要写的时候拉低。HOLD 一般不用上拉就行。初始化代码用 STM32CubeMX 生成SPI1 配置为ModeFull-Duplex MasterData Size8 BitsCPOLLowCPHA1 EdgePrescaler472MHz / 4 18MHzFirst BitMSB FirstNSSSoftware生成代码后先写一个 CS 控制函数void MRAM_CS_Low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } void MRAM_CS_High(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }然后写 SPI 收发函数。HAL 库的 HAL_SPI_TransmitReceive 可以同时收发但 MRAM 的操作是半双工发指令的时候不需要收数据。我一般用 HAL_SPI_Transmit 发指令用 HAL_SPI_Receive 收数据。但注意HAL_SPI_Transmit 在发送完成后会等 TXE 和 BSY 标志这个等待时间在 18MHz 下很短不影响。4.2 写使能与状态轮询写数据之前必须发 WREN。代码很简单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; MRAM_CS_Low(); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); MRAM_CS_High(); return status; }轮询 WIP 位的函数void MRAM_WaitReady(void) { while (MRAM_ReadStatus() 0x01); }这里有个细节MRAM 的写入速度极快通常你发完 WRITE 指令WIP 位已经是 0 了。但如果你在写的时候被中断打断WIP 可能还是 1。所以轮询是必要的但不会等太久。4.3 单字节与多字节写入单字节写入void MRAM_WriteByte(uint32_t addr, uint8_t data) { uint8_t cmd[5]; cmd[0] 0x02; cmd[1] (addr 16) 0x07; cmd[2] (addr 8) 0xFF; cmd[3] addr 0xFF; cmd[4] data; MRAM_WriteEnable(); MRAM_CS_Low(); HAL_SPI_Transmit(hspi1, cmd, 5, 100); MRAM_CS_High(); MRAM_WaitReady(); }多字节写入void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] 0x02; cmd[1] (addr 16) 0x07; 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, buf, len, 1000); MRAM_CS_High(); MRAM_WaitReady(); }注意地址的高 3 位要掩码 0x07因为 MR25H40CDF 只有 19 位地址。如果你传的地址超过 0x7FFFF高位会被忽略实际写入的地址会回绕。这个坑我在调试时遇到过当时传了一个 0x80000 的地址结果数据写到了 0x00000查了半天才发现是地址溢出。4.4 读取操作与 DMA 优化读取比写入简单不需要 WRENvoid MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t cmd[4]; cmd[0] 0x03; cmd[1] (addr 16) 0x07; 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(); }如果你要读大量数据比如一次读 4KB用 HAL_SPI_Receive 会阻塞 CPU。这时候可以用 DMA。配置 DMA 通道把 SPI 的 RX 请求映射到 DMA1 Channel2然后调用 HAL_SPI_Receive_DMA。DMA 传输期间 CPU 可以去做别的事传输完成后进中断处理。我实测过读 4KB 数据用阻塞方式耗时约 2.3ms18MHz SPI用 DMA 方式耗时约 2.1ms差别不大因为 SPI 时钟是瓶颈。但 DMA 的好处是 CPU 占用率低如果你在跑 RTOSDMA 方式不会影响其他任务的实时性。5. 常见问题与排查技巧实录5.1 读写失败问题速查表现象可能原因排查方法解决方案读不到数据全FFCS未拉低或拉低时间不对用逻辑分析仪抓CS和SCK波形检查CS控制代码确保CS在传输期间保持低电平写入后读出来是旧数据WREN未发或WREN后CS拉高读状态寄存器bit1WEL确保WREN后CS拉高再发WRITE数据偶尔出错SPI时钟太快或线太长降低SPI时钟到9MHz测试缩短走线加屏蔽降低时钟地址错乱地址超过0x7FFFF检查地址计算地址掩码0x07确保不溢出上电后数据丢失电源上升时间太慢用示波器看VDD上升沿加电源监控芯片或延长上电复位时间高温下写入失败去耦电容不足在MRAM VDD脚加0.1uF1uF改善电源滤波远离发热源5.2 独家避坑经验坑一CS 线上的毛刺。工业现场有继电器、接触器动作时会产生电磁干扰。CS 线如果走线太长容易耦合毛刺导致 MRAM 误触发。我的做法是在 CS 线上加一个 100pF 的电容到地滤掉高频毛刺。同时 CS 线尽量短远离功率线。坑二SPI 模式不匹配。MR25H40CDF 支持 Mode 0 和 Mode 3但有些 STM32 的 SPI 在 Mode 0 下MISO 的采样时刻和 MRAM 的输出时刻有偏差。我遇到过一批板子Mode 0 下读数据偶尔错位换成 Mode 3 就正常了。后来查手册发现MRAM 在 SCK 下降沿输出数据STM32 在 SCK 上升沿采样Mode 0 下这个时序是匹配的但如果 PCB 走线延迟大就会错位。所以如果你的板子走线长建议用 Mode 3或者降低 SPI 时钟。坑三写保护引脚悬空。MR25H40CDF 的 WP 引脚是低电平有效如果悬空内部上拉可能不够强导致偶尔进入写保护状态。我一般直接上拉到 VDD或者接一个 10K 电阻到 VDD。HOLD 引脚同理。坑四电源掉电时的数据完整性。MRAM 虽然写入快但如果电源掉电时正好在写操作中间数据可能只写了一半。解决办法是在电源端加一个大电容比如 1000uF掉电后能维持 MRAM 完成当前写操作。或者用电源监控芯片在电压低于阈值时触发中断在中断里停止写操作。5.3 实测数据与性能评估我在这套方案上跑了一组测试条件如下STM32F373RC 主频 72MHzSPI1 时钟 18MHzMRAM 供电 3.3V室温 25°C。操作数据量耗时平均速率单字节写1 Byte12 us83 KB/s256字节写256 Byte158 us1.62 MB/s4KB写4 KB2.4 ms1.71 MB/s单字节读1 Byte8 us125 KB/s256字节读256 Byte142 us1.80 MB/s4KB读4 KB2.2 ms1.86 MB/s从数据看MRAM 的读写速率在 1.7MB/s 左右受限于 SPI 时钟。如果你把 SPI 时钟提到 40MHz需要换支持 40MHz 的 MCU速率能到 4MB/s 以上。对于工业数据记录来说这个速率足够了除非你要做高速波形记录。注意单字节操作的耗时里CS 拉低拉高和 WREN 指令占了很大比例。如果你要频繁写单字节建议攒一批数据再写减少 CS 切换次数。6. 工业场景下的可靠性加固6.1 宽温测试与数据保持MR25H40CDF 的工作温度范围是 -40°C 到 85°C数据保持时间在 85°C 下是 10 年在 125°C 下是 2 年。工业场景通常要求 -40°C 到 85°C这个范围 MRAM 能覆盖。我做过高低温循环测试-40°C 放置 2 小时85°C 放置 2 小时循环 100 次。测试前后读写数据没有出现错误。但要注意在 -40°C 下MRAM 的写入电流会增大如果你的电源供电能力不足写入可能失败。我在 -40°C 下测过写入电流峰值约 15mA比室温下的 8mA 高不少。所以电源要留足余量。6.2 电磁兼容性设计工业现场的 EMC 要求比消费电子严格得多。MRAM 的 SPI 线是高速信号容易辐射干扰。我的做法是SPI 线走差分对不SPI 是单端信号走不了差分。但可以走地线包络即在 SCK 和 MOSI 两侧走地线减少辐射。在 MRAM 的 VDD 脚加磁珠滤掉高频噪声。PCB 布局时MRAM 尽量靠近 MCU走线长度不超过 5cm。如果设备有金属外壳MRAM 区域不要开孔避免电磁泄漏。6.3 数据校验与恢复机制再可靠的存储也有出错的时候。我在数据记录区加了 CRC16 校验每写一条记录同时写一个 CRC 值。读的时候先算 CRC如果不对说明数据损坏跳过这条记录。另外系统参数区做了双备份。主区和备份区各存一份读的时候如果主区 CRC 不对自动切到备份区。写的时候先写备份区再写主区确保任何时候至少有一份完整数据。typedef struct { uint32_t magic; uint16_t crc; uint8_t data[64]; } ParamBlock; void SaveParams(ParamBlock *params) { params-magic 0x5A5A5A5A; params-crc CRC16(params-data, 64); MRAM_WriteBuffer(0x00000, (uint8_t*)params, sizeof(ParamBlock)); MRAM_WriteBuffer(0x00100, (uint8_t*)params, sizeof(ParamBlock)); } bool LoadParams(ParamBlock *params) { MRAM_ReadBuffer(0x00000, (uint8_t*)params, sizeof(ParamBlock)); if (params-magic 0x5A5A5A5A params-crc CRC16(params-data, 64)) { return true; } MRAM_ReadBuffer(0x00100, (uint8_t*)params, sizeof(ParamBlock)); if (params-magic 0x5A5A5A5A params-crc CRC16(params-data, 64)) { return true; } return false; }这套机制在实际现场跑了一年多没有出现过参数丢失的情况。7. 项目扩展与个人体会这套 MRAM STM32F373RC 的方案最初是为了一个工业数据记录仪做的。后来发现同样的架构可以扩展到很多场景比如智能电表的数据存储、医疗设备的参数保存、车载记录仪的日志存储。MRAM 的快速写入和无限擦写寿命在这些场景里都是刚需。如果你要扩展有几个方向可以考虑。一是把 SPI 换成 QSPIMR25H40CDF 不支持 QSPI但 Everspin 有 QSPI 接口的 MRAM速率能到 100MHz 以上。二是加一个文件系统比如 LittleFS 或者 SPIFFS把 MRAM 当成块设备来管理这样上层应用不用关心地址分配。三是加一个 RTC给每条记录打时间戳方便事后分析。我个人在实际操作中的体会是MRAM 的驱动本身不难难的是可靠性设计。工业现场的环境比实验室恶劣得多电源波动、电磁干扰、温度变化每一个都可能让存储出错。所以驱动写完之后一定要做长时间的老化测试至少跑 72 小时模拟现场的各种异常情况。我见过太多项目实验室里跑得好好的一到现场就出问题最后查出来都是电源或者时序的小细节没处理好。最后再分享一个小技巧如果你不确定 MRAM 的时序配置对不对可以先写一个测试模式往所有地址写 0xAA再读出来对比。如果全对说明时序没问题如果有错用逻辑分析仪抓波形重点看 CS、SCK、MOSI 三根线的时序关系。这个办法帮我快速定位过好几次问题比盲猜效率高得多。