1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM
做工业嵌入式这行的朋友应该都有体会,选存储介质这件事,往往比选主控还让人头疼。EEPROM 擦写寿命撑不住高频写入,NOR Flash 写入前得先擦块、速度上不去,SRAM 掉电就丢数据还得配电池,FRAM 容量小价格贵。这几年 MRAM(磁性随机存储器)慢慢进入大家的视野,我手上这个项目就是围绕MR25H40CDF这颗 4Mbit 的 SPI MRAM 展开的,主控用的是STM32F765ZI,Cortex-M7 内核,216MHz 主频,带一堆 SPI 外设,做工业数据采集和参数存储非常合适。
先说清楚这个项目到底要干什么:在工业现场设备里,需要频繁记录运行参数、故障日志、标定数据,要求掉电不丢、写入次数几乎无限、写入延迟低、还要能扛住宽温。MR25H40CDF 正好对上这些需求——它是 Everspin 家的 SPI 接口 MRAM,4Mbit 容量(512KB),40MHz SPI 时钟,写入不需要擦除、没有写入延迟、读写寿命号称 10^14 次以上,工业级温度范围 -40 到 85 摄氏度。STM32F765ZI 这边负责通过 SPI 总线跟它通信,完成初始化、读写、校验、掉电保护这一整套逻辑。
适合谁看这篇内容?如果你正在做工业控制器、数据采集终端、电力仪表、医疗设备这类需要高可靠存储的嵌入式项目,或者你手上有 STM32 平台想接一颗非易失存储器,这篇基本能让你少走不少弯路。我会把硬件连接、SPI 配置、读写时序、页写边界、状态寄存器轮询、掉电保护这些细节全部拆开讲,代码给到能直接抄的程度。
1.2 为什么选 SPI 接口而不是并口或 I2C
MR25H40CDF 提供 SPI 接口,这个选择背后是有讲究的。并口 MRAM 引脚多、PCB 布线复杂,在小型工业模块里不划算;I2C 速率上限通常 1MHz 左右,对于需要快速批量写入日志的场景太慢。SPI 这边,MR25H40CDF 支持最高 40MHz 时钟,STM32F765ZI 的 SPI 外设配置成主机模式,理论带宽能到 5MB/s,实际有效吞吐受协议开销影响,但读写 512KB 全片也就百毫秒级别,完全够用。
另外 SPI 的引脚少(CS、SCK、MOSI、MISO 四根线),STM32F765ZI 有多个 SPI 外设可以分配,硬件片选和软件片选都支持。我在这个项目里用的是 SPI2,硬件 NSS 没启用,改用 GPIO 软件控制片选,原因是总线上还挂了别的从设备,需要灵活切换。这个取舍后面会详细说。
1.3 整体架构设计思路
整个存储方案分三层:底层是 SPI 驱动层,负责字节级读写和时序控制;中间是 MRAM 设备层,封装页写、状态轮询、地址管理;上层是应用层,做参数存储、日志环形缓冲、掉电检测。这样分层的好处是,以后换主控或者换存储芯片,只需要改底层,上层逻辑不动。
STM32F765ZI 这边用 HAL 库打底,但关键时序部分我直接操作寄存器,因为 HAL 的 SPI 传输函数在高速下有些开销,而且状态轮询需要精确控制。MRAM 的写入流程跟 Flash 不一样,它没有擦除概念,写命令发下去数据就进去了,但要注意页写边界——MR25H40CDF 的页大小是 256 字节,跨页写会回卷到页首覆盖数据,这是最容易踩的坑。
2. 硬件连接与 SPI 配置细节
2.1 引脚连接与 PCB 注意事项
MR25H40CDF 是 8 引脚 DFN 或 SOIC 封装,引脚定义:CS(片选)、SCK(时钟)、SI(数据输入)、SO(数据输出)、VDD、VSS、WP(写保护)、HOLD(保持)。实际接线到 STM32F765ZI 如下表:
| MR25H40CDF 引脚 | STM32F765ZI 引脚 | 说明 |
|---|---|---|
| CS | PB12 | 软件片选,GPIO 推挽输出 |
| SCK | PB13 | SPI2_SCK,复用推挽 |
| SI | PB15 | SPI2_MOSI,复用推挽 |
| SO | PB14 | SPI2_MISO,浮空或上拉输入 |
| WP | 3.3V | 硬件写保护,不用时拉高 |
| HOLD | 3.3V | 保持功能,不用时拉高 |
| VDD | 3.3V | 电源,加 0.1uF 去耦 |
| VSS | GND | 地 |
PCB 布线这块有几个经验点。SCK 和 MOSI 走线尽量等长,虽然 40MHz 不算特别高,但工业环境干扰大,等长能减少时序偏差。CS 线要远离 SCK,避免耦合导致误触发。去耦电容必须紧贴芯片 VDD 引脚,我见过因为电容放远了导致写入偶发失败的案例。如果设备工作在强电磁环境,建议在 SCK 和 CS 上串 22 欧姆电阻做阻抗匹配。
注意:WP 和 HOLD 引脚不要悬空,悬空状态下电平不确定,可能导致芯片进入写保护或保持状态,表现为读写全部失败。我调试时就遇到过 HOLD 悬空导致 MISO 一直没反应的情况,查了半天才发现是引脚没处理。
2.2 STM32F765ZI 的 SPI2 初始化
STM32F765ZI 的 SPI2 挂在 APB1 总线上,时钟源来自 PCLK1。假设系统时钟配置为 216MHz,APB1 分频后 PCLK1 为 54MHz。SPI2 的波特率预分频器设置为 2 分频,得到 27MHz 时钟,MR25H40CDF 支持 40MHz,27MHz 留有余量,稳定性更好。
初始化代码用 HAL 库打底:
SPI_HandleTypeDef hspi2; void SPI2_Init(void) { hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; hspi2.Init.Direction = SPI_DIRECTION_2LINES; hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi2.Init.TIMode = SPI_TIMODE_DISABLE; hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi2.Init.CRCPolynomial = 7; HAL_SPI_Init(&hspi2); }这里 CPOL 和 CPHA 的配置很关键。MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我选模式 0,即时钟空闲低电平、第一个边沿采样。这个模式跟大多数 SPI 器件兼容,调试时用逻辑分析仪抓波形也容易看懂。
2.3 软件片选的控制逻辑
片选用 GPIO 软件控制,PB12 配置为推挽输出,初始高电平。每次通信前拉低,通信结束拉高。这里有个细节:MRAM 的 CS 拉低后到第一个时钟边沿之间需要至少 5ns 的建立时间,STM32 的 GPIO 翻转速度足够快,但为了保险,我在拉低 CS 后加了一个__NOP()空操作,实测下来波形更干净。
#define MRAM_CS_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET)软件片选相比硬件片选的好处是灵活,可以在一次事务里控制多个从设备,缺点是占用 CPU 时间。在 27MHz 时钟下,一次 256 字节页写大概几十微秒,CPU 开销可以接受。
3. MRAM 读写操作的核心实现
3.1 命令集与状态寄存器解析
MR25H40CDF 的命令集比较简洁,常用的有这几个:
| 命令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写操作前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
状态寄存器只有 bit0(WEL,写使能锁存)和 bit1(BP0、BP1 保护位)有效。WEL 位在发 WREN 后置 1,写操作完成后自动清零。BP 位用于块保护,工业场景下如果不需要保护,保持默认 0 即可。
读状态寄存器的代码:
uint8_t MRAM_ReadStatus(void) { uint8_t cmd = 0x05; uint8_t status = 0; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); HAL_SPI_Receive(&hspi2, &status, 1, 100); MRAM_CS_HIGH(); return status; }这里注意,HAL_SPI_Transmit 和 HAL_SPI_Receive 分开调用会有额外的函数开销,高速场景下建议用 HAL_SPI_TransmitReceive 一次完成,或者直接操作 DR 寄存器。
3.2 页写边界与地址回卷问题
这是 MRAM 使用中最容易翻车的地方。MR25H40CDF 内部按 256 字节分页,共 2048 页。当你发起写命令并连续写入超过页边界的数据时,地址不会自动进位到下一页,而是回卷到当前页的起始地址,覆盖之前写的数据。
举个例子:从地址 0x00F0 开始写 32 字节,写到 0x010F 时,地址会回卷到 0x0100,继续写会覆盖 0x0100 到 0x010F 的内容。这个行为跟 EEPROM 类似,但很多人第一次用 MRAM 会以为它像 SRAM 一样线性写入,结果数据莫名其妙被覆盖。
正确的做法是写之前先算好页边界,跨页时分多次写:
void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint32_t len) { while (len > 0) { uint32_t page_remain = 256 - (addr % 256); uint32_t write_len = (len < page_remain) ? len : page_remain; MRAM_WriteEnable(); MRAM_WritePage(addr, buf, write_len); while (MRAM_ReadStatus() & 0x01); // 等待 WEL 清零 addr += write_len; buf += write_len; len -= write_len; } }这个循环逻辑保证了每次写入都不跨页,虽然多几次事务开销,但数据安全。
3.3 写使能与状态轮询
每次写操作前必须发 WREN 命令,否则写不进去。发完 WREN 后读状态寄存器确认 WEL 位为 1,再发写命令。写命令发出后,WEL 位会自动清零,可以通过轮询 WEL 位判断写操作是否完成。
MRAM 的写入速度极快,通常几微秒就完成,但工业级芯片在极端温度下可能稍慢,所以轮询等待是必要的。我实测在 -40 度环境下,256 字节页写完成时间约 15 微秒,常温下约 8 微秒。
void MRAM_WriteEnable(void) { uint8_t cmd = 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); MRAM_CS_HIGH(); }提示:WREN 命令发出后如果 CS 拉高,WEL 位保持;如果 CS 保持低电平继续发其他命令,WEL 位也保持。但为了逻辑清晰,我习惯每次 WREN 后拉高 CS,再单独发起写事务。
3.4 读操作的实现与速度优化
读操作相对简单,发 READ 命令加 3 字节地址,然后连续读数据。MRAM 支持连续读,地址自动递增到全片末尾。
void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint32_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(&hspi2, cmd, 4, 100); HAL_SPI_Receive(&hspi2, buf, len, 1000); MRAM_CS_HIGH(); }速度优化方面,HAL_SPI_Receive 在 27MHz 下读 512KB 大约需要 200ms,如果嫌慢可以改用 DMA。STM32F765ZI 的 SPI2 支持 DMA 请求,配置 DMA 通道后,读操作可以后台进行,CPU 去处理其他任务。我在日志导出功能里用了 DMA,读 512KB 降到 150ms 左右,主要瓶颈在 SPI 时钟和协议开销。
4. 工业场景下的可靠性设计
4.1 掉电保护与数据完整性
工业现场掉电是常态,MRAM 本身掉电不丢数据,但写入过程中掉电可能导致数据写了一半。解决办法是在数据区加校验和或者 CRC,上电后校验,发现损坏就用备份区恢复。
我的做法是把 512KB 分成两个 256KB 的区,A 区存主数据,B 区存备份。每次写入先写 B 区,再写 A 区,上电时对比两个区的 CRC,哪个对用哪个。这样即使写入过程中掉电,至少有一个区是完整的。
typedef struct { uint32_t magic; uint32_t version; uint8_t data[248]; uint32_t crc; } ParamBlock;每个参数块 256 字节,正好一页,避免跨页写。magic 用于识别有效块,version 用于版本管理,crc 用标准 CRC32 算法。
4.2 宽温环境下的时序余量
MR25H40CDF 标称工业级 -40 到 85 度,但实际在温度极限附近,SPI 时序参数会变化。数据手册里 40MHz 是在常温下测的,高温下建议降频使用。我在 85 度环境下测试,27MHz 时钟读写正常,但 40MHz 时偶发误码。所以最终产品固件里把 SPI 时钟固定在 27MHz,牺牲一点速度换稳定性。
另外,MRAM 的写入电流在低温下会增大,电源设计要留余量。我用的是 3.3V 供电,LDO 选的是 500mA 输出能力,实际写入峰值电流约 30mA,余量充足。
4.3 电磁干扰与数据校验
工业环境电磁干扰强,SPI 总线上的数据可能被干扰。除了 PCB 布线优化,软件上我加了双重校验:每次写入后立即读回对比,确认写入成功;关键数据块加 CRC32 校验,读取时验证。
uint8_t MRAM_WriteVerify(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t readback[256]; MRAM_WriteBuffer(addr, buf, len); MRAM_ReadBuffer(addr, readback, len); return (memcmp(buf, readback, len) == 0) ? 0 : 1; }这个写后读回验证会增加一倍操作时间,但对于关键参数存储是值得的。日志类数据可以不做验证,靠 CRC 兜底。
5. 常见问题排查与实操避坑
5.1 读写失败的排查思路
调试 MRAM 时遇到读写失败,按这个顺序排查效率最高:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读全 0xFF | CS 没拉低、MISO 没接、芯片没供电 | 万用表测电压、逻辑分析仪抓 CS |
| 读全 0x00 | HOLD 引脚悬空、SPI 模式不对 | 检查 HOLD 接高、确认 CPOL/CPHA |
| 写入后读回不对 | 跨页写回卷、WREN 没发 | 检查地址边界、抓 WREN 波形 |
| 偶发误码 | 时钟太快、干扰大 | 降 SPI 时钟、加屏蔽 |
| 高温下失败 | 时序余量不足 | 降频、加去耦电容 |
我踩过最坑的一次是读全 0xFF,查了半天发现是 CS 引脚配置成了复用功能而不是 GPIO 输出,导致片选一直无效。所以初始化后先用万用表量一下 CS 静态电平,确认是高电平再往下调。
5.2 SPI 模式不匹配的典型表现
SPI 模式 0 和模式 3 的区别在于时钟空闲电平和采样边沿。如果主从模式不匹配,读出来的数据会整体移位或者完全错乱。MR25H40CDF 支持模式 0 和模式 3,STM32 这边配置成模式 0 最省事。如果你发现读出的数据每个字节都差一位,基本就是模式问题。
用逻辑分析仪抓 SCK 和 MOSI 波形,看第一个边沿是上升沿还是下降沿,对照数据手册确认。没有逻辑分析仪的话,可以发一个已知命令比如 RDSR,读回的状态寄存器值如果 WEL 位不对,也能反推模式问题。
5.3 页写回卷的隐蔽性
页写回卷这个问题隐蔽在于,如果你每次写入的数据都不跨页,永远不会触发。但一旦跨页,数据被覆盖的位置取决于起始地址,表现得很随机。我建议在驱动层就做好跨页拆分,上层应用不用关心页边界。测试时故意从地址 0x00F0 写 32 字节,读回验证 0x0100 之后的数据是否被覆盖,能快速验证驱动是否正确处理了跨页。
5.4 电源与去耦的实操经验
MRAM 写入瞬间电流会跳变,如果电源去耦不足,可能导致写入失败或者主控复位。我的做法是在 MRAM 的 VDD 引脚旁边放一个 0.1uF 陶瓷电容和一个 1uF 钽电容,前者滤高频,后者储能。PCB 上电容到引脚的走线尽量短,最好在 2mm 以内。
另外,如果设备有多个 SPI 从设备共用总线,每个从设备的 CS 要独立控制,避免同时选中多个设备导致总线冲突。我在项目里用 74HC138 做片选译码,STM32 用三个 GPIO 控制八个从设备,节省引脚。
6. 性能实测与优化建议
6.1 读写速度实测数据
在 27MHz SPI 时钟下,我实测了不同操作的耗时:
| 操作 | 数据量 | 耗时 | 有效速率 |
|---|---|---|---|
| 单字节读 | 1B | 2.1us | 0.48MB/s |
| 256字节页读 | 256B | 78us | 3.28MB/s |
| 256字节页写 | 256B | 85us | 3.01MB/s |
| 全片读 | 512KB | 165ms | 3.17MB/s |
| 全片写 | 512KB | 178ms | 2.94MB/s |
单字节操作效率低是因为命令和地址开销占了大头,批量操作才能发挥 SPI 的带宽优势。所以应用层设计时尽量批量读写,减少小数据量频繁操作。
6.2 DMA 优化的实际效果
用 DMA 替代中断方式的 HAL_SPI_Receive,全片读耗时从 165ms 降到 148ms,提升约 10%。提升不明显的原因是瓶颈在 SPI 时钟本身,DMA 只是减少了 CPU 干预。但在多任务系统里,DMA 让 CPU 可以去做别的事,整体系统效率提升更明显。
配置 DMA 时注意,SPI2_RX 用 DMA1_Stream3,SPI2_TX 用 DMA1_Stream4,优先级设中高,传输完成中断里拉高 CS。
6.3 写入寿命与磨损均衡
MRAM 写入寿命 10^14 次,理论上不需要磨损均衡。但工业设备可能连续运行十年,每天写入百万次,累计 3.65×10^9 次,远低于寿命上限。所以这个项目里我没做磨损均衡,直接线性写入。如果你的应用写入频率极高,可以考虑环形缓冲加地址轮转,进一步延长寿命。
日志存储我用的是环形缓冲,512KB 里划出 256KB 做日志区,写满后从头覆盖。每条日志 64 字节,带时间戳和 CRC,可存 4096 条。读日志时按时间戳排序,最新的在最前面。
7. 代码组织与项目结构
7.1 驱动层文件划分
项目代码分三个文件:mram_driver.c/h放底层 SPI 读写和命令封装,mram_device.c/h放页写拆分、状态轮询、校验逻辑,app_storage.c/h放参数存储和日志管理。这样分层后,换主控只需要改 driver 层,换存储芯片只需要改 device 层。
驱动层对外暴露的接口:
uint8_t MRAM_Init(void); uint8_t MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len); uint8_t MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len); uint8_t MRAM_Erase(void); // MRAM 不需要擦除,保留接口兼容7.2 参数存储的版本管理
参数区用结构体存储,带 magic 和 version 字段。固件升级后如果参数结构变了,通过 version 判断是否需要迁移。magic 用 0x4D52414D("MRAM" 的 ASCII),上电时先读 magic,不对就初始化默认参数。
#define PARAM_MAGIC 0x4D52414D #define PARAM_VERSION 0x00010002 typedef struct { uint32_t magic; uint32_t version; uint32_t write_count; uint8_t calib_data[128]; uint8_t reserved[112]; uint32_t crc; } DeviceParam;write_count 记录写入次数,用于寿命监控。虽然 MRAM 寿命极长,但记录一下心里有底。
7.3 日志环形缓冲的实现
日志区用头尾指针管理,头指针指向下一个写入位置,尾指针指向最旧数据。写满后头指针回卷,覆盖最旧数据。读日志时从尾指针开始遍历到头指针。
typedef struct { uint32_t head; uint32_t tail; uint32_t count; } LogCtrl; typedef struct { uint32_t timestamp; uint16_t event_id; uint8_t data[54]; uint32_t crc; } LogEntry;每条日志 64 字节,正好四分之一页,写入时不会跨页。LogCtrl 存在参数区,掉电后恢复。
8. 实际部署中的经验总结
8.1 上电初始化的顺序
上电后先初始化 GPIO 和 SPI 外设,再拉高 CS,延时 10ms 等 MRAM 内部上电稳定,然后读状态寄存器确认通信正常。如果读回 0xFF 或 0x00,说明硬件有问题,进入错误处理。确认通信正常后再读参数区,校验 magic 和 CRC,不对就加载默认参数。
这个顺序很重要,我见过有人先读参数再初始化 SPI,结果读出来全是随机值,查了半天是初始化顺序问题。
8.2 现场故障的远程诊断
工业设备部署到现场后,出问题不方便去现场。我在固件里加了诊断命令,通过串口或者通信接口可以读取 MRAM 的状态寄存器、写入次数、CRC 错误计数。这些信息能快速定位是存储问题还是其他问题。
诊断命令返回的数据格式:
MRAM_STATUS: WEL=0 BP=00 WRITE_COUNT: 1234567 CRC_ERRORS: 0 LAST_ERROR: 0x008.3 固件升级时的数据保留
固件升级时参数区数据要保留,我的做法是升级前把参数区读到 RAM,升级完成后写回。如果升级过程中掉电,参数区可能丢失,所以升级前先备份到备份区,升级后从备份区恢复。
这个流程在 bootloader 里实现,应用固件不用关心。bootloader 占 64KB Flash,放在 STM32F765ZI 的起始地址,应用固件从 0x08010000 开始。
8.4 长期运行的稳定性观察
项目部署后连续运行了六个月,每天写入约 50 万次,累计约 9000 万次写入,没有出现数据丢失或读写失败。MRAM 的稳定性确实比 EEPROM 和 Flash 好很多,尤其是在频繁写入场景下。
温度方面,设备工作在 -20 到 60 度环境,MRAM 读写正常。极端低温 -40 度下测试了 100 小时,也没有问题。唯一需要注意的是电源质量,如果现场电源波动大,建议加 TVS 管和滤波电路。
这个方案后续还可以扩展,比如把 MRAM 做成文件系统,支持多文件存储;或者加一个 RTC,日志带精确时间戳;再或者用双 MRAM 做冗余,进一步提高可靠性。根据具体项目需求灵活调整就行。