1. 项目缘起:为什么要在工业场景里折腾 MRAM 和 STM32F765
工业现场的数据存储有个很尴尬的现状:用 EEPROM 吧,写入速度慢得让人着急,擦写次数也就百万次级别,频繁记录日志的话没几年就报废了;用 SRAM 加电池吧,电池本身就是个定时炸弹,高温环境下撑不了太久,换电池还得停机;用 NOR Flash 吧,写之前得擦除整个扇区,掉电保护逻辑复杂,而且擦写寿命同样有限。我前两年做一个电力监测终端项目,设备装在配电房里,夏天柜内温度能到 70 度,客户要求每秒钟记录一次三相电流电压数据,掉电不能丢,设备寿命至少十年。当时试过几种方案,最后锁定了 MR25H40CDF 这颗 MRAM 芯片配合 STM32F765ZI 主控,实测下来确实稳。
MR25H40CDF 是 Everspin 出的 4Mbit 磁阻随机存储器,SPI 接口,关键特性是写入没有延迟、擦写次数近乎无限、掉电数据不丢。它跟普通 Flash 最大的区别在于:Flash 写之前要擦,擦是以扇区为单位,写是以页为单位,操作起来束手束脚;MRAM 的写操作跟 SRAM 一样直接,给地址写数据就完事了,不需要擦除,不需要等待,写一个字节和写一整页在时间上没有本质区别。STM32F765ZI 是 ST 家的 Cortex-M7 芯片,主频 216MHz,带硬件 SPI 接口,支持 DMA,用来驱动 MR25H40CDF 绰绰有余。这个组合在工业数据记录、嵌入式黑匣子、参数存储、频繁写入的计数器场景里非常合适。
这篇文章我打算把整个方案从头到尾拆一遍,包括硬件怎么连、SPI 怎么配、驱动怎么写、数据怎么组织、遇到问题怎么排查。不管你是刚接触嵌入式的新手,还是做过几年想换存储方案的老人,应该都能从里面找到能直接用的东西。我尽量说人话,把踩过的坑和想明白的道理都写出来。
2. 方案整体设计与选型逻辑
2.1 为什么是 MRAM 而不是 Flash 或 FRAM
选存储芯片这件事,核心就看三个维度:写入速度、擦写寿命、掉电保持能力。我把常见几种非易失存储拉出来对比一下,你就能明白为什么 MRAM 在这个场景里胜出。
| 存储类型 | 写入速度 | 擦写寿命 | 掉电保持 | 写入前是否需擦除 | 典型功耗 |
|---|---|---|---|---|---|
| EEPROM | 慢(ms级) | 100万次 | 10年+ | 否 | 低 |
| NOR Flash | 中(页写入ms级) | 10万次 | 20年+ | 是(扇区擦除) | 中 |
| FRAM | 快(ns级) | 10^14次 | 10年+ | 否 | 低 |
| MRAM | 快(ns级) | 10^15次 | 20年+ | 否 | 中 |
| SRAM+电池 | 极快 | 无限 | 依赖电池 | 否 | 高(待机) |
MR25H40CDF 的写入周期是 35ns 级别,SPI 接口能跑到 40MHz,实际连续写入吞吐可以做到几 MB/s。对比 EEPROM 的 5ms 写入周期,差距是五个数量级。擦写寿命方面,MRAM 是 10^15 次,按每秒写一次算,能写三千万年,设备寿命内根本不用担心写坏。掉电保持 20 年以上,工业设备生命周期完全覆盖。
FRAM 其实也是候选,但 FRAM 容量普遍偏小,4Mbit 的 FRAM 价格比 MRAM 贵不少,而且 TI 的 FRAM 系列供货周期不太稳定。MRAM 在容量、价格、供货上更均衡。至于 SRAM 加电池,电池在高温下的寿命和可靠性是硬伤,工业客户明确不接受可更换电池的方案。
2.2 STM32F765ZI 的角色与资源分配
STM32F765ZI 在这个方案里干三件事:第一,通过 SPI 接口读写 MR25H40CDF;第二,管理数据组织逻辑,包括地址分配、校验、磨损均衡(虽然 MRAM 不需要,但为了兼容性可以留接口);第三,对外提供数据访问接口,比如通过串口、以太网或者 USB 把存储的数据导出去。
F765ZI 的 SPI 资源很充裕,有 SPI1 到 SPI6 多个实例,我一般把 SPI1 分配给 MRAM,因为 SPI1 在 APB2 总线上,时钟频率高,而且引脚布局方便。DMA 用 DMA2 的通道,配合 SPI1 的收发请求。GPIO 方面,除了 SCK、MISO、MOSI 三根线,还需要一根片选 CS,我用的是软件片选,因为 MRAM 的片选时序要求不严格,软件控制更灵活,方便在两次传输之间插入其他操作。
时钟配置上,F765ZI 的 SPI1 挂在 APB2,最高 108MHz,SPI 分频后可以到 54MHz、27MHz 等。MR25H40CDF 的最高 SPI 时钟是 40MHz,所以我一般设 27MHz 或者 13.5MHz,留点余量,工业环境里信号完整性比极限速度重要。
2.3 硬件连接与信号完整性考量
接线本身不复杂,但工业环境里细节决定成败。MR25H40CDF 是 8 脚 SOIC 封装,引脚定义如下:
- 1 脚 CS:片选,低有效
- 2 脚 SO:数据输出(MISO)
- 3 脚 WP:写保护,低有效,不用的话接高
- 4 脚 VSS:地
- 5 脚 SI:数据输入(MOSI)
- 6 脚 SCK:时钟
- 7 脚 HOLD:保持,低有效,不用的话接高
- 8 脚 VDD:电源,2.7V 到 3.6V
跟 STM32F765ZI 连接时,CS 接一个 GPIO,SO 接 MISO,SI 接 MOSI,SCK 接 SCK。WP 和 HOLD 我建议都上拉到 VDD,虽然芯片内部有弱上拉,但工业环境里外部加 10k 上拉更稳妥,防止干扰导致误触发写保护或保持状态。
PCB 布局上,SPI 走线尽量短,SCK 和 MOSI 之间保持足够间距,避免串扰。如果走线超过 10cm,建议在 SCK 上串一个 22 欧姆电阻,减缓边沿,降低反射。电源引脚旁边放 100nF 加 1uF 的退耦电容,越近越好。地平面要完整,不要被其他信号割裂。这些在实验室里可能看不出差别,但到了现场,电磁干扰一上来,差的就是稳定和不稳定的区别。
3. MR25H40CDF 的 SPI 协议细节与操作时序
3.1 SPI 模式选择与命令集
MR25H40CDF 支持 SPI 模式 0 和模式 3。模式 0 是 CPOL=0、CPHA=0,时钟空闲低电平,数据在上升沿采样;模式 3 是 CPOL=1、CPHA=1,时钟空闲高电平,数据在下降沿采样。我一般用模式 0,因为 STM32 的 SPI 配置成模式 0 最直观,而且大部分逻辑分析仪默认也是模式 0,调试方便。
芯片的命令集不复杂,常用的就几条:
| 命令名称 | 命令码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写操作前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读设备 ID |
跟 Flash 不同的是,MRAM 没有扇区擦除命令,也没有页编程命令,写操作就是直接发 WRITE 命令加地址加数据。这是它最大的优势,省掉了擦除等待和复杂的地址管理。
3.2 读操作时序与代码实现
读操作很简单:拉低 CS,发 0x03,发 24 位地址(MR25H40CDF 是 4Mbit,需要 19 位地址,但按 24 位发,高位补零),然后连续读数据,数据在 SCK 下降沿输出(模式 0 下),MCU 在上升沿采样。读完拉高 CS。
用 STM32 HAL 库实现的话,核心代码大概这样:
void MRAM_Read(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; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }这里有个细节:HAL_SPI_Transmit 和 HAL_SPI_Receive 分开调用的话,CS 在两次调用之间会保持低电平,因为我没有在函数内部拉高。但 HAL 库的 Transmit 和 Receive 是阻塞式的,中间会有函数调用开销,如果 SPI 时钟很快,可能会出现时钟间隙。更稳妥的做法是用 HAL_SPI_TransmitReceive 或者直接操作寄存器,保证时序连续。我实测在 27MHz 下,分开调用也能正常工作,但如果你追求极致稳定,建议用 DMA 一次性完成。
3.3 写操作时序与写使能逻辑
写操作比读多一步:发 WRITE 命令之前必须先发 WREN(0x06),否则写操作会被忽略。WREN 之后 CS 必须拉高再拉低,才能开始 WRITE 命令。这个时序在数据手册里有明确要求,我一开始没注意,直接 WREN 之后没拉高 CS 就发 WRITE,结果数据写不进去,查了半天才发现问题。
正确的写流程:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4]; uint8_t wren = 0x06; // 第一步:发 WREN HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &wren, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); // 第二步:发 WRITE 命令加地址加数据 cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }写操作没有等待时间,发完数据拉高 CS 就完成了。这一点跟 Flash 完全不同,Flash 发完数据还要等内部编程完成,得轮询状态寄存器。MRAM 不需要,省掉了轮询逻辑,代码简洁很多。
3.4 状态寄存器与写保护机制
MR25H40CDF 的状态寄存器只有几位有效:WIP(写进行中)、WEL(写使能锁存)、BP0 和 BP1(块保护)。WIP 位在 MRAM 里基本没用,因为写操作是即时的,不会出现 WIP 置位的情况。WEL 位在 WREN 之后置 1,在 WRITE 完成或 WRDI 之后清 0。BP0 和 BP1 用来设置块保护,可以保护部分地址区域不被写入。
工业场景里,我一般会把参数区和日志区分开,参数区设置块保护,防止程序跑飞误写。具体做法是发 WRSR 命令写状态寄存器,设置 BP 位。但要注意,设置块保护之后,写状态寄存器本身也需要 WREN,而且块保护生效后,被保护的地址写入会被静默忽略,不会报错,调试的时候容易迷惑。
4. STM32F765ZI 的 SPI 配置与 DMA 优化
4.1 CubeMX 配置要点
用 CubeMX 配置 SPI1 的时候,几个关键参数:
- Mode:Full-Duplex Master
- Hardware NSS Signal:Disable(用软件片选)
- Data Size:8 Bits
- First Bit:MSB First
- Prescaler:根据实际时钟设,我一般设 8 分频,APB2 108MHz 的话 SPI 时钟 13.5MHz
- CPOL:Low
- CPHA:1 Edge
- CRC Calculation:Disabled
- NSS Pulse Mode:Disable
- TI Mode:Disable
GPIO 方面,PA5 是 SCK,PA6 是 MISO,PA7 是 MOSI,这三个自动配置为复用推挽。CS 我选的是 PA4,配置为推挽输出,初始高电平。注意 CS 的 GPIO 速度要设成 High 或者 Very High,不然拉低拉高的边沿太慢,影响时序。
4.2 DMA 传输配置与双缓冲思路
数据量大或者需要高速连续读写的时候,DMA 是必须的。SPI1 的 TX 用 DMA2 Stream3 Channel3,RX 用 DMA2 Stream0 Channel3。CubeMX 里直接勾选就行,模式选 Normal,不选 Circular,因为每次传输长度不一样。
用 DMA 的话,读操作可以这样写:
void MRAM_Read_DMA(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; HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit_DMA(&hspi1, cmd, 4); while (hspi1.State != HAL_SPI_STATE_READY); HAL_SPI_Receive_DMA(&hspi1, buf, len); while (hspi1.State != HAL_SPI_STATE_READY); HAL_GPIO_WritePin(MRAM_CS_PORT, MRAM_CS_PIN, GPIO_PIN_SET); }这里有个坑:HAL_SPI_Transmit_DMA 之后如果立刻调用 HAL_SPI_Receive_DMA,可能会因为 SPI 还没完全发送完而冲突。我加了一个 while 等待状态就绪,虽然牺牲了一点效率,但保证了可靠性。更好的做法是用 HAL_SPI_TransmitReceive_DMA,一次调用完成收发,但需要把命令和数据拼到一个缓冲区里,内存开销大一点。
4.3 时钟极性与相位实测对比
我实测过模式 0 和模式 3 在 MR25H40CDF 上的表现。模式 0 下,SCK 空闲低,第一个边沿是上升沿,数据在上升沿采样。模式 3 下,SCK 空闲高,第一个边沿是下降沿,数据在下降沿采样。两种模式都能正常工作,但模式 0 的波形更干净,因为空闲低电平在 PCB 上更容易保持稳定,不容易受干扰。模式 3 的空闲高电平如果走线长,容易耦合噪声,导致误采样。
如果你用逻辑分析仪抓波形,模式 0 下应该看到 CS 拉低后,SCK 开始翻转,第一个上升沿对应 MOSI 上的第一位数据。如果发现数据错位,先检查 CPOL 和 CPHA 设置,再检查 CS 拉低和第一个时钟沿之间的建立时间是否足够。MR25H40CDF 要求 CS 建立时间最小 5ns,STM32 的 GPIO 翻转速度足够快,一般没问题,但如果 CS 上加了 RC 滤波,就要注意了。
5. 数据组织与存储管理实战
5.1 地址空间划分与数据结构设计
4Mbit 的 MRAM 实际可用 512KB,地址范围 0x00000 到 0x7FFFF。我一般这样划分:
- 0x00000 - 0x00FFF:设备信息区,存序列号、固件版本、生产日期
- 0x01000 - 0x01FFF:参数区,存校准系数、配置参数
- 0x02000 - 0x7FFFF:数据日志区,循环记录
每个区域加一个简单的头部结构,包含魔数、长度、CRC 校验。比如参数区的结构:
typedef struct { uint32_t magic; // 0x50415241 "PARA" uint16_t version; uint16_t length; uint8_t data[256]; uint32_t crc32; } ParamBlock_t;写参数的时候先算 CRC,再整体写入。读的时候先读头部,校验魔数和 CRC,通过才使用数据。这样即使掉电导致写入不完整,也能检测出来,不会用错数据。
5.2 循环日志的写入与检索策略
日志区用循环缓冲的方式管理。维护一个写指针,每次写一条日志,指针后移,到末尾就回绕到开头。每条日志加一个序号和时间戳,检索的时候根据序号和时间戳定位。
日志条目结构:
typedef struct { uint32_t seq; // 序号,递增 uint32_t timestamp; // 时间戳,秒 uint16_t type; // 日志类型 uint16_t len; // 数据长度 uint8_t payload[64]; // 数据 uint16_t crc16; // 校验 } LogEntry_t;写日志的时候,先读当前写指针,写入条目,更新写指针。写指针本身也存在 MRAM 里,每次更新。MRAM 写次数无限,不用担心写指针频繁更新会坏。
检索的时候,从最新条目往前找,根据序号判断哪些是新的。因为循环覆盖,旧数据可能被新数据覆盖,所以检索逻辑要能处理序号回绕。我一般用 32 位序号,回绕周期很长,实际使用中不用考虑。
5.3 掉电保护与数据完整性校验
MRAM 本身掉电不丢数据,但写入过程中掉电可能导致条目写了一半。为了防止这种情况,我采用“先写数据,再写校验”的策略:先把日志条目的除 CRC 外的部分写入,然后再写 CRC。读的时候如果 CRC 不匹配,说明这条日志不完整,丢弃即可。
更严格的做法是双备份:每条日志写两份,地址错开,读的时候取 CRC 正确的那份。但这样会浪费一半空间,512KB 变 256KB。工业场景里,如果日志价值高,建议双备份;如果只是普通记录,单份加 CRC 就够了。
还有一个细节:写日志之前,先把写指针更新到一个“正在写”的临时位置,写完日志后再更新正式写指针。这样即使写日志过程中掉电,重启后根据正式写指针也能知道哪些数据是完整的。这个逻辑稍微复杂一点,但能保证数据一致性。
6. 常见问题与排查技巧实录
6.1 读写失败问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读回全 0xFF | CS 没拉低、SPI 没配置、芯片没供电 | 用万用表测 VDD、用逻辑分析仪抓 CS 和 SCK | 检查硬件连接和 SPI 初始化 |
| 读回全 0x00 | MISO 被拉低、芯片损坏 | 断开 MISO 测电压 | 检查 MISO 走线,更换芯片 |
| 写入后读回不变 | WREN 没发、CS 时序不对 | 抓 WREN 和 WRITE 之间的 CS 波形 | 确保 WREN 后 CS 拉高再拉低 |
| 数据偶尔出错 | SPI 时钟太快、走线干扰 | 降低 SPI 时钟测试 | 加串阻、缩短走线、加退耦电容 |
| 状态寄存器读不对 | 命令码错误、模式不对 | 对照数据手册检查命令码 | 确认 SPI 模式和命令码 |
| DMA 传输卡死 | DMA 通道冲突、中断优先级 | 检查 DMA 配置和中断 | 调整 DMA 通道或优先级 |
6.2 SPI 通信不稳定的排查思路
SPI 通信不稳定,十有八九是硬件问题。我遇到过一次,实验室里好好的,到了现场偶尔读出错。用示波器抓波形,发现 SCK 上升沿有振铃,幅度超过 VDD,导致 MRAM 误触发。解决办法是在 SCK 上串了 33 欧姆电阻,振铃明显减小,问题消失。
还有一次是 CS 走线太长,跟电机驱动线捆在一起,电机一启动就读写失败。后来把 CS 走线改短,远离功率线,加了屏蔽地线,问题解决。工业环境里,SPI 走线一定要远离干扰源,能短则短,必要时加屏蔽。
软件层面,如果 SPI 时钟分频设得太高,比如 54MHz,MR25H40CDF 可能来不及响应。数据手册标称 40MHz,但实际跟 PCB 质量有关。我一般从 13.5MHz 开始调,稳定了再往上加,找到稳定工作的最高频率。
6.3 写入数据丢失的几种典型场景
写入数据丢失,最常见的原因是 WREN 没生效。WREN 之后如果 CS 拉高,WEL 位会保持;但如果 WREN 和 WRITE 之间插入了其他 SPI 操作,WEL 可能被清除。我建议 WREN 之后立刻发 WRITE,中间不要插入其他命令。
另一个原因是块保护设置。如果 BP 位设置了保护,被保护区域的写入会被忽略,而且不会报错。调试的时候可以先读状态寄存器,确认 BP 位是 0。
还有一种情况是电源不稳。MRAM 写入虽然快,但写入瞬间电流会增大,如果电源退耦不够,VDD 瞬间跌落,可能导致写入失败。我在 VDD 引脚旁边加了 100nF 加 10uF 电容,问题解决。
6.4 实操心得与避坑建议
第一条心得:MRAM 虽然写入无限次,但不要把它当 SRAM 用。每次写操作都要发命令和地址,有开销。如果数据量小且频繁,可以在 RAM 里缓存,攒够一批再写。比如每秒采集 100 次数据,可以每秒写一次 MRAM,把 100 条数据打包写入,效率更高。
第二条心得:MR25H40CDF 的 HOLD 引脚一定要上拉。我有个项目忘了上拉,结果偶尔读写失败,查了很久才发现是 HOLD 被干扰拉低,SPI 传输被暂停。后来加了 10k 上拉,再没出过问题。
第三条心得:调试 SPI 的时候,逻辑分析仪比示波器好用。逻辑分析仪能直接解码 SPI 协议,看到命令、地址、数据,一眼就能看出哪里不对。我用的是一款国产的 8 通道逻辑分析仪,几百块钱,解码 SPI 很稳定,推荐入手一个。
第四条心得:MRAM 的读写速度很快,但 STM32 的 HAL 库函数调用开销不小。如果追求极致速度,可以直接操作 SPI 寄存器,把 CS 控制、数据收发都写成宏或者内联函数。我实测直接操作寄存器比 HAL 库快 3 到 5 倍,对于高速连续采集场景很有必要。
7. 性能实测与优化方向
7.1 读写速度实测数据
我用 STM32F765ZI 在 216MHz 主频、SPI 时钟 13.5MHz 下实测,连续读 512KB 数据耗时约 380ms,平均读速度约 1.35MB/s。连续写 512KB 数据耗时约 420ms,平均写速度约 1.22MB/s。如果把 SPI 时钟提到 27MHz,读速度能到 2.6MB/s,写速度到 2.4MB/s。再往上提到 40MHz,速度提升不明显,因为 STM32 的 SPI 和 DMA 开销成了瓶颈。
对比 EEPROM,同样写 512KB,EEPROM 按 5ms 写周期算,需要 2560 秒,也就是 40 多分钟。MRAM 只要 0.4 秒,差距是几千倍。这就是为什么工业高频记录场景必须用 MRAM。
7.2 功耗表现与低功耗场景适配
MR25H40CDF 的读写电流典型值 15mA,待机电流 100uA 左右。STM32F765ZI 在 216MHz 下运行电流约 100mA,加上 MRAM 的 15mA,整体功耗不低。如果设备是电池供电,需要优化。
低功耗场景下,可以这样处理:STM32 平时进入 Stop 模式,MRAM 的 CS 保持高电平,MRAM 进入待机。需要记录数据时,STM32 唤醒,SPI 初始化,写 MRAM,然后继续休眠。MRAM 从待机到就绪的时间很短,不影响实时性。实测这种模式下,整体平均电流可以降到 1mA 以下,电池寿命大幅延长。
7.3 后续扩展思路
这个方案后续可以扩展几个方向。一是加文件系统,把 MRAM 当块设备,挂载 FatFS 或者 LittleFS,方便管理大量小文件。二是加加密,写入前对数据加密,读取时解密,保护敏感数据。三是加双芯片冗余,两片 MRAM 同时写,读的时候对比,提高可靠性。四是加无线传输,通过蓝牙或者 LoRa 把存储的数据发出去,实现远程监控。
我个人觉得,对于大多数工业数据记录场景,单芯片加 CRC 校验已经足够。如果客户对可靠性要求极高,再考虑双芯片冗余。文件系统的话,如果数据条目固定,直接按地址管理更简单高效,不一定非要上文件系统。
这个方案我在三个项目里用过,最长的已经跑了两年多,每天写入几十万次,至今没出过数据丢失。MRAM 加 STM32 的组合,在工业嵌入式存储领域,确实是个省心的选择。