
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM做嵌入式这行十几年最头疼的往往不是代码写不出来而是数据存不住。工业现场那些设备动不动就要在零下四十度到零上一百多度的环境里跑还要扛住震动、电磁干扰、频繁掉电。以前用 EEPROM 存参数写入速度慢得让人着急擦写次数也就一百万次左右高频采集的场景下没几个月就写废了。后来换 SPI Flash容量是大了但写入前得先擦除整个扇区掉电瞬间还容易丢数据得额外加掉电检测和电容储能硬件成本上去了软件逻辑也复杂。MR25H40CDF 这颗芯片进入视线是因为它用的是 MRAM磁阻随机存储器技术。这东西的原理和传统存储不太一样它靠磁隧道结的电阻变化来存数据不需要电荷保持所以写入速度能到纳秒级擦写次数理论上无限而且掉电数据不丢。我第一次拿到样品的时候心里还犯嘀咕这么小一颗 DFN 封装的芯片真能扛住工业现场那些折腾后来实测下来它在反复掉电、宽温循环的测试里表现确实稳这才放心把它用到项目里。这颗芯片的容量是 4Mbit也就是 512KB组织成 512K×8 位。接口是标准的 SPI支持最高 40MHz 时钟模式 0 和模式 3 都能跑。对于 STM32F373RC 这颗 Cortex-M4 内核、带浮点运算单元的 MCU 来说用硬件 SPI 驱动它几乎不占什么 CPU 资源。我选这个组合核心诉求就一个在工业数据采集节点上实现高频、可靠、掉电安全的数据存储同时尽量简化硬件设计和软件逻辑。1.2 STM32F373RC 的资源匹配与 SPI 外设选择STM32F373RC 在 STM32 家族里算是个比较特殊的存在它带 16 位 ADC、12 位 DAC、比较器、运放模拟外设丰富得有点不像话特别适合做传感器信号采集。它的存储资源是 256KB Flash 和 32KB SRAM跑个实时操作系统或者裸机调度都够用。SPI 接口有三个SPI1 挂在 APB2 总线上最高时钟能到 36MHzSPI2 和 SPI3 挂在 APB1 上最高 18MHz。我一般把 MRAM 挂在 SPI1 上因为 APB2 的时钟频率更高能充分发挥 MR25H40CDF 的 40MHz 上限。这里有个细节值得说STM32F373RC 的 SPI1 支持硬件 NSS 管理但实际用的时候我建议还是用软件片选。原因很简单硬件 NSS 在多主或者需要精确控制片选时序的场景下容易出问题而且一旦配置成硬件模式引脚就固定了布线不灵活。软件片选就是普通 GPIO 推挽输出初始化的时候拉高通信前拉低通信完拉高时序完全可控。我在 PCB 上会把片选信号线尽量短远离时钟线减少串扰。另一个选型考量是供电。MR25H40CDF 的工作电压是 2.7V 到 3.6VSTM32F373RC 也是 3.3V 供电两者可以直接对接不需要电平转换。但要注意MRAM 在写入瞬间的电流会比待机时大不少典型写入电流大概 6mA 左右峰值可能到 10mA。如果系统里还有其他大电流外设电源设计得留够余量去耦电容要靠近芯片引脚放0.1μF 和 1μF 并联效果比较好。1.3 硬件连接与 PCB 布局要点接线本身不复杂SPI 四根线加电源地SCK、MOSI、MISO、CS再加 VCC 和 GND。但工业环境里这几根线的走法有讲究。SCK 是时钟线频率高的时候辐射比较厉害我一般会把它包地处理两边走地线或者至少保证它和相邻信号线的间距大于三倍线宽。MISO 是输入线STM32 端配置成浮空或者上拉输入都行我习惯配上拉防止悬空时读到随机数据。CS 片选线要特别注意它虽然不是高速信号但它的边沿决定了 SPI 帧的起始和结束。如果 CS 线上有振铃可能导致 MRAM 误判命令。我通常会在 CS 线上串一个 22Ω 到 33Ω 的电阻靠近 STM32 端放用来抑制反射。电源引脚旁边必须放去耦电容0.1μF 的陶瓷电容紧贴引脚1μF 的钽电容或者陶瓷电容放在附近给写入瞬间的电流需求提供缓冲。还有一点MR25H40CDF 的 DFN 封装底部有个散热焊盘这个焊盘要接到 GND 平面不仅为了散热也为了提供低阻抗的回流路径。焊接的时候注意别让焊锡膏堵住引脚DFN 引脚间距小手工焊接容易连锡用热风枪加锡膏的方式比较稳。2. MR25H40CDF 的底层原理与操作逻辑2.1 MRAM 存储机制与 SPI 命令集MRAM 的核心是磁隧道结每个存储单元由一个固定磁层、一个自由磁层和一个薄绝缘层组成。自由磁层的磁化方向可以通过电流产生的磁场来改变方向不同单元的电阻就不同高阻代表 1低阻代表 0。读取的时候给单元加一个小电压测电流就能判断电阻状态写入的时候给对应的字线和位线通电流产生磁场翻转自由层。这个过程不涉及电荷的注入和移出所以没有擦写次数限制也不存在电荷泄漏导致的掉电丢失。MR25H40CDF 的 SPI 命令集和标准 SPI Flash 很像但简化了不少。它没有扇区擦除命令因为 MRAM 写入就是直接覆盖不需要先擦后写。主要命令包括写使能WREN0x06、写禁止WRDI0x04、读状态寄存器RDSR0x05、写状态寄存器WRSR0x01、读数据READ0x03、写数据WRITE0x02、读标识RDID0x9F。状态寄存器的 bit0 是 WEL写使能锁存bit1 是 BP0bit2 是 BP1用来设置块保护。上电默认是写保护状态所以每次写入前必须先发 WREN 命令。这里有个坑我踩过MR25H40CDF 的写使能是自动复位的。你发了 WREN 之后只要完成一次写操作WEL 位就自动清零。如果连续写多个字节必须在每次写操作前重新发 WREN。我一开始以为发一次 WREN 就能连续写一整页结果只有第一个字节写进去了后面的全丢。后来看数据手册才发现这个细节改成每次写之前都发 WREN 才正常。2.2 时序参数与速度计算MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我一般用模式 0因为 STM32 的 SPI 默认配置就是模式 0省得改。时钟频率最高 40MHz但实际能跑多快取决于 PCB 布线和 STM32 的 SPI 配置。STM32F373RC 的 SPI1 在 APB2 上如果系统时钟 72MHzAPB2 不分频SPI1 的时钟就是 72MHz分频系数最小是 2所以 SPI 时钟最高 36MHz。这个频率下MRAM 完全能跟上实测读写都正常。算一下传输时间36MHz 时钟一个时钟周期约 27.8ns。SPI 是全双工每个时钟传一位。读一个字节需要 8 个时钟加上命令字节和地址字节读一个字节的总时钟数是 8命令 24地址 8数据 40 个时钟约 1.11μs。写一个字节类似但多了 WREN 命令的 8 个时钟总共 48 个时钟约 1.33μs。如果连续读地址会自动递增不需要重复发命令和地址效率更高。连续读 512KB 全部数据理论时间大约是 512×1024×8 / 36M ≈ 116ms实际因为 CS 拉高拉低和命令开销大概在 130ms 左右。注意SPI 时钟频率不是越高越好。如果 PCB 走线长、干扰大高频下 MISO 采样可能出错。我一般先在低速比如 9MHz下调通再逐步提高频率用逻辑分析仪抓波形确认时序余量。2.3 状态寄存器与块保护配置状态寄存器的 BP0 和 BP1 位用来设置块保护可以保护部分或全部存储区不被写入。上电默认 BP00BP10但 WEL0所以实际是写保护的。如果要写数据先发 WREN 把 WEL 置 1然后发 WRSR 配置 BP 位。BP 位的组合如下BP1BP0保护范围00无保护01保护高 1/4 区域10保护高 1/2 区域11保护全部区域工业应用里我通常会把引导区或者校准参数放在高地址段然后设置 BP10BP01保护高 1/4 区域防止程序跑飞时误写。配置完状态寄存器后WEL 会自动清零后续写入需要重新发 WREN。这个逻辑在初始化代码里要写清楚不然调试的时候会发现数据写不进去查半天才发现是块保护没关。3. STM32F373RC 端的驱动实现3.1 硬件 SPI 初始化与 GPIO 配置用 STM32CubeMX 配置 SPI1 最省事。时钟源选外部晶振系统时钟配到 72MHzAPB2 不分频。SPI1 模式选全双工主模式数据宽度 8 位CPOL 低CPHA 1 边沿NSS 软件管理预分频系数先设 89MHz调通后再改 236MHz。GPIO 配置PA5 和 PA7 复用推挽输出PA6 上拉输入PA4 普通推挽输出做片选初始电平高。初始化代码大概长这样void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; SPI_HandleTypeDef hspi1; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5 SCK, PA7 MOSI GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; 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); // PA4 CS GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); 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; HAL_SPI_Init(hspi1); }这段代码里片选引脚 PA4 配置成普通推挽输出初始拉高。SPI 的 NSS 设成软件模式这样 HAL 库不会去动 NSS 引脚我们自己控制。预分频系数 8 对应 9MHz先低速调通后面再改。3.2 读写函数封装与页写优化MR25H40CDF 的读写函数我封装成几个基本操作读状态、写使能、读数据、写数据、读 ID。读数据的时候先拉低 CS发 0x03然后发 24 位地址接着连续读数据地址自动递增。写数据类似但每次写之前要发 WREN而且写操作有页边界限制。MR25H40CDF 的页大小是 256 字节如果一次写跨页地址会回卷到页首覆盖之前的数据。所以写数据的时候要按页对齐或者分多次写。我一般写一个带页处理的函数void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t page_remain; while(len 0) { page_remain 256 - (addr % 256); if(page_remain len) page_remain len; MRAM_WritePage(addr, buf, page_remain); addr page_remain; buf page_remain; len - page_remain; } }MRAM_WritePage 里面先发 WREN拉低 CS发 0x02发地址然后连续写 page_remain 个字节最后拉高 CS。注意每次写之前都要发 WREN因为 WEL 在写完成后自动清零。这个逻辑我调试的时候用逻辑分析仪抓过波形确认 WREN 命令确实在每次写之前都发了数据也正确写入。读函数就简单多了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; CS_LOW(); HAL_SPI_Transmit(hspi1, cmd, 4, 100); HAL_SPI_Receive(hspi1, buf, len, 1000); CS_HIGH(); }这里用 HAL_SPI_Transmit 发命令和地址然后用 HAL_SPI_Receive 读数据。注意 HAL_SPI_Receive 会自己产生时钟所以不需要额外发 dummy 字节。如果用的是标准库或者寄存器操作读的时候要发 dummy 数据来产生时钟。3.3 掉电保护与数据完整性校验工业现场掉电是常态MRAM 虽然写入快但如果在写入过程中掉电数据可能只写了一半。为了保证数据完整性我通常会在数据区加一个 CRC 校验或者双备份加标志位。具体做法是把参数分成两个区A 区和 B 区每个区前面加一个 4 字节的序列号和 2 字节的 CRC。写入的时候先写 A 区写完更新序列号再写 B 区。读取的时候比较两个区的序列号取新的那个如果 CRC 校验失败就取另一个。MRAM 的写入速度很快一个 256 字节的页写大概 1.33μs×256≈340μs加上命令开销不到 400μs。这个时间窗口内掉电的概率很低但工业设备要求万无一失所以双备份加 CRC 是值得的。我实测过在电源上并联一个 100μF 的电容掉电后 MRAM 还能完成当前页的写入配合双备份机制数据丢失的概率几乎为零。提示MRAM 的写入次数无限所以不用担心双备份会缩短寿命。但要注意频繁写入时电源电流会增大去耦电容要选低 ESR 的不然电压跌落可能导致写入失败。4. 实操过程与关键环节记录4.1 从零搭建测试环境我拿到 MR25H40CDF 样品后先焊了一块转接板把 DFN 封装转到 DIP 上方便插在面包板上测试。转接板上的去耦电容不能省0.1μF 和 1μF 各一个紧贴芯片引脚。然后连到 STM32F373RC 的开发板上SPI1 的 PA4 到 PA7 对应接好电源 3.3V 从开发板取。上电后第一件事是读 ID。发 0x9F 命令应该返回 0x29 0x25 0x40 0x13 之类的标识。如果读回来全是 0xFF 或者 0x00先查接线再查片选和时钟。我遇到过读回来全是 0xFF 的情况查了半天发现是 MISO 线没接好虚焊了。重新焊好就正常了。读 ID 成功后先写一个字节到地址 0x000000然后读回来对比。写之前记得发 WREN写完等一会儿再读。如果读回来不对用逻辑分析仪抓 SPI 波形看命令、地址、数据是否匹配。逻辑分析仪是调试 SPI 的利器几百块的那种就够用能解码 SPI 协议直接看到发了什么命令、收到什么数据。4.2 批量读写与速度测试单字节读写调通后开始批量测试。我写了一个测试程序从地址 0 开始写 512KB 的递增数据然后读回来校验。第一次跑的时候发现写入速度比预期慢测了一下写 512KB 花了大概 2.5 秒算下来平均每个字节 4.8μs比理论值 1.33μs 慢了不少。查原因发现是每次写一个字节都调用了 HAL_SPI_Transmit函数调用开销和 CS 拉高拉低的时间占了大头。后来改成页写模式一次写 256 字节速度提升明显。写 512KB 花了大概 0.8 秒平均每个字节 1.5μs接近理论值。读速度更快连续读 512KB 只花了 0.15 秒。这个速度对于工业数据采集来说完全够用1kHz 采样率下每次采完存一个 16 位数据写入时间不到 3μsCPU 占用率极低。测试的时候我还发现一个现象MRAM 在写入后立即读取数据是正确的但如果在写入后马上读状态寄存器WEL 位可能还是 1过几个时钟周期才清零。所以写完之后不要立刻依赖 WEL 的状态来判断写入完成直接读数据校验更可靠。4.3 宽温与掉电循环测试工业级芯片的宽温测试是必须的。我把板子放到高低温试验箱里从零下 40 度到零上 85 度循环每个温度点保持 30 分钟然后读写数据校验。MR25H40CDF 的数据手册标称工作温度是零下 40 到零上 85实测在这个范围内读写都正常没有出现数据错误。零下 40 度的时候SPI 时钟频率要降一点我降到 18MHz 跑36MHz 在低温下时序余量不够偶尔会出错。掉电循环测试更狠我用继电器控制电源每秒通断一次连续跑 24 小时。板子上加了 100μF 电容掉电后 MRAM 能完成当前写操作。24 小时后读数据CRC 校验全部通过没有丢失。这个测试让我对 MRAM 的可靠性有了信心后来在项目里批量用了上千颗现场反馈没有出现过数据丢失的问题。5. 常见问题与排查技巧实录5.1 SPI 通信失败排查流程SPI 通信失败是最常见的问题排查思路可以按以下顺序来现象可能原因排查方法读 ID 返回全 0xFFMISO 未连接或片选未拉低检查接线用万用表测通断读 ID 返回全 0x00MOSI 未连接或时钟未输出检查 MOSI 接线示波器看 SCK写入后读回数据错误WREN 未发或块保护未关逻辑分析仪抓 WREN 命令和状态寄存器高速下数据偶发错误时序余量不足或干扰降低 SPI 时钟检查 PCB 布线写入跨页数据覆盖页边界未处理按 256 字节页对齐写入我遇到最多的是 WREN 未发导致写入失败。MR25H40CDF 的 WEL 位在每次写操作后自动清零如果代码里只在初始化时发一次 WREN后续写入全部无效。这个坑我踩过两次后来在写函数里强制每次写之前都发 WREN问题就解决了。5.2 数据丢失与误写防护数据丢失通常有两个原因一是掉电时写入未完成二是程序跑飞误写。对于掉电问题加电容和双备份机制能解决。对于误写块保护功能很有用。把关键参数放在高地址段设置 BP 位保护起来即使程序跑飞执行了写命令也写不进去。还有一个细节MRAM 的写入电压如果低于 2.7V写入可能失败或者数据不可靠。所以电源监控很重要STM32F373RC 内部有掉电检测PVD可以设置阈值 2.9V低于这个值就禁止写入操作等电源恢复再写。这个逻辑在工业设备里是标配能有效防止低压误写。5.3 与 Flash 方案的对比与替换经验以前用 SPI Flash 的项目换成 MRAM 后软件改动不大主要是去掉擦除命令把写函数改成直接写。但有几个地方要注意Flash 的写使能是发一次 WREN 可以连续写一页MRAM 是每次写之前都要发 WRENFlash 有忙等待MRAM 没有写完立即可以读Flash 的页大小可能是 256 字节MRAM 也是 256 字节但 MRAM 跨页会回卷Flash 跨页会覆盖行为类似都要处理页边界。速度上MRAM 的写入速度比 Flash 快一个数量级Flash 写一个字节要几毫秒MRAM 只要几微秒。读取速度两者差不多都受 SPI 时钟限制。功耗方面MRAM 写入电流比 Flash 小但待机电流略大不过对于工业设备来说这点差异可以忽略。6. 项目扩展与个人体会这套方案后来被我扩展到了几个不同的项目里。一个是振动传感器节点1kHz 采样率每次采 3 轴加速度存到 MRAM 里攒够一批再通过无线发出去。MRAM 的快速写入让 CPU 不用等采完直接写写完继续采实时性很好。另一个是电能质量监测装置记录电压电流波形掉电时 MRAM 能保住最后几秒的数据方便事后分析。我个人在实际操作中的体会是MRAM 这东西贵是贵了点但省心。不用操心擦写寿命不用加掉电检测电路不用写复杂的磨损均衡算法软件逻辑简单了很多。对于工业设备来说可靠性比成本更重要一次现场故障的维修成本可能就超过芯片差价了。当然如果只是存个配置参数写入频率很低那用 EEPROM 或者 Flash 也够没必要上 MRAM。最后分享一个小技巧MR25H40CDF 的 DFN 封装手工焊接时先在 PCB 焊盘上涂一层薄锡用热风枪 280 度吹芯片放上去对准轻轻按压锡融化后自然对齐。焊完用放大镜检查引脚有没有连锡万用表测一下电源和地有没有短路。这个封装虽然小但焊好了很可靠我焊了几十块板子没有出现过虚焊。