去年我维护一套工业网关的时候,碰到了一个非常现实的场景:现场设备通过 Modbus 和 OPC UA 上报数据,上位机要求网关保存最近一小时的关键运行日志,断电重启后不能丢。当时板子上是 STM32F469II,主控资源非常充足,但数据存储这块让我纠结了很久。后来我选了 Everspin 的 MR25H40CDF,一颗 4Mbit 的 SPI 接口 MRAM,配合 STM32F469II 的硬件 SPI,把整个日志存储模块跑得又稳又简单。这篇内容就是围绕这个组合展开的,我会把从硬件接线、驱动设计、代码实现到调试踩坑的完整过程梳理一遍,适合正在做嵌入式存储选型、工业数据记录或者想了解 MRAM 实际用法的朋友参考。
1. 项目概述与方案选型
1.1 工业场景下的存储需求到底是什么
工业设备对存储的要求和消费电子不太一样。消费类产品里,SD 卡和 eMMC 已经足够用了,但工业现场往往面临几个绕不开的问题:
- 掉电不可预测:很多设备是直接切断电源的,根本来不及做文件系统卸载或者数据落盘操作。数据写到一半掉电,普通 Flash 就有可能出现页损坏。
- 写入次数密集:网关可能每隔几秒就记录一条状态数据,一天下来写入次数轻松到几十万次级别。NOR Flash 的擦写寿命通常在 10 万次左右,这种强度下很容易被写穿。
- 温度范围和振动环境恶劣:柜内温度可能从零下到七八十摄氏度,还会有持续的机械振动干扰。
这些问题指向一个共同的技术方向:需要一种非易失性、写入速度快、写入寿命长的存储介质。MRAM(磁阻随机存储器)就是在这个背景下进入我视野的。
1.2 为什么是 MR25H40CDF
MR25H40CDF 是 Everspin 推出的 4Mbit SPI 接口 MRAM,容量折算下来是 512KB。这个容量对工业日志记录来说不大不小,存几千条结构化记录完全够用。真正打动我的不是容量,而是它的物理特性:
- 无限写入耐久:MRAM 靠磁化状态存储数据,不存在电荷泄漏和擦除损耗问题,官方给出的耐久度超过 10 的 12 次方次写入。这意味着你完全不用像维护 Flash 那样做磨损均衡。
- 写入不需要先擦除:普通 SPI Flash 写入前必须先按扇区擦除,而 MRAM 可以像 SRAM 一样直接覆盖写入,用户不需要维护“擦除-写入”两阶段流程。
- 掉电数据保持能力强:数据保持时间典型值是 20 年以上,温度范围 -40℃ 到 +85℃,工业级场景完全覆盖。
- 接口兼容性极好:MR25H40CDF 的指令集基本兼容标准 SPI NOR Flash 的读 03h、写 02h、读状态寄存器 05h 等操作,软件驱动写起来非常顺手。
如果用一句话概括:MRAM 就是把 SRAM 的读写方式和非易失性结合在了一起,代价是单颗价格比同容量 NOR Flash 贵一些,但省下的开发时间和后期维护成本远比这点差价值钱。
1.3 为什么是 STM32F469II
STIM32F469II 属于 STM32F4 系列的高端型号,内核是 Cortex-M4F,主频最高 180MHz,带浮点单元和 DSP 指令。选它主要看中三点:
- 外设资源丰富:有 6 个 SPI、4 个 USART、2 个 SDMMC,还有 LCD 控制器。做工业网关时,SPI 可以接 MRAM,UART 可以接 Modbus 从站设备,以太网接口处理 OPC UA 报文,外设之间不打架。
- SRAM 容量达到 384KB:可以开很大的 DMA 缓冲,实现“采集一批、批量写 MRAM”的架构,避免频繁小数据写入降低效率。
- 生态成熟:HAL 库、标准外设库、裸机编程的路子都很清晰,团队里新人也容易上手。
这里顺带说一个选型细节:STM32F469II 的尾缀 II 表示封装是 176 脚 BGA,封装大、引脚密,画板时要注意预留好测试点和扇出走线。如果只是做小批量设备,建议优先选 LQFP 封装的型号,焊接和调试都省事。
2. 硬件设计与接线细节
2.1 引脚信号与电路
MR25H40CDF 采用 SOIC-8 封装,引脚定义非常标准:
| 引脚序号 | 名称 | 功能说明 |
|---|---|---|
| 1 | CS# | 片选,低有效 |
| 2 | SCK | SPI 时钟 |
| 3 | SI | 主机数据输入(写数据) |
| 4 | WP# | 写保护,低有效 |
| 5 | SO | 主机数据输出(读数据) |
| 6 | HOLD# | 暂停通讯,低有效 |
| 7 | GND | 地 |
| 8 | VDD | 电源,典型 3.3V |
我在 STM32F469II 侧的连接是这样的:
- SPI1 的 SCK 接 PA5
- SPI1 的 MISO 接 PA6
- SPI1 的 MOSI 接 PA7
- SPI1 的 NSS 不用硬件片选,直接用 PA4 做普通 GPIO 软件控制 CS#
为什么不用硬件 NSS?主要原因是硬件 NSS 在某些库版本里行为比较“个性”,一旦配置不当会出现多主机冲突误触发;软件控制 CS 虽然多写两行代码,但时序完全可控,排查问题也方便。
WP# 和 HOLD# 必须拉高。这两脚如果悬空,很容易在强电磁干扰下被意外拉低,导致芯片进入写保护或者保持状态,表现就是数据写不进去或者读出来全是 0xFF。我在原理图上直接给 WP# 和 HOLD# 各接一个 10kΩ 上拉电阻到 VDD,稳。
2.2 电源与去耦电路
MR25H40CDF 的工作电压是 2.7V 到 3.6V,直接用 3.3V 供电没问题。但要注意,SPI 写入瞬间电流会有一个小的跳变,如果电源纹波太大会造成写入错误。我的做法是:
- 在 VDD 和 GND 之间放一个100nF 陶瓷电容,尽量靠近芯片引脚放置。
- 同时并联一个4.7uF 钽电容,吸收低频波动。
- 芯片的 GND 引脚直接打到主地平面,不要单独走细长线。
这类细节在低速场景下无所谓,但 MRAM 在高时钟下配合工业现场的电源噪声,去耦做不好就是间歇性故障,排查起来非常头疼。
2.3 STM32F469II 侧 SPI 配置
SPI1 挂在 APB2 总线上,STM32F469 的 APB2 最高可达 90MHz。为了让 SPI 时钟尽量接近 MRAM 的最高工作频率,我把 SPI1 的时钟配成 45MHz,预分频设置为 2 分频,最终 SCK 就是 22.5MHz。MR25H40CDF 在 3.3V 供电时最高支持 40MHz,22.5MHz 远低于极限,信号质量有保障。
不过这里有个边界条件:如果你的板子布线很长或者环境电磁干扰严重,适当降低 SPI 频率比一味追求高速更明智。我在样品板上实测,22.5MHz 没问题,但碰到一根 20cm 飞线连接测试板时,掉到 5.6MHz 才稳定。主线板做好阻抗控制后回到 22.5MHz 就没有再出过问题。
3. 驱动代码与读写流程
3.1 SPI 初始化
我最终用寄存器方式实现 SPI 收发,因为工业项目里对实时性要求高,寄存器级操作更直接,HAL 库的接口为了通用性做了太多状态检查,在一些低配场景下反而显得拖沓。初始化代码如下:
void MRAM_SPI_Init(void) { // 1. 使能时钟 RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // 2. 配置 PA5(SCK)、PA7(MOSI) 为复用推挽输出,PA6(MISO) 为复用输入 GPIOA->MODER &= ~(GPIO_MODER_MODER5_Msk | GPIO_MODER_MODER6_Msk | GPIO_MODER_MODER7_Msk); GPIOA->MODER |= (GPIO_MODER_MODER5_AF | GPIO_MODER_MODER6_AF | GPIO_MODER_MODER7_AF); GPIOA->AFR[0] |= (5 << GPIO_AFRL_AFSEL5_Pos) | (5 << GPIO_AFRL_AFSEL6_Pos) | (5 << GPIO_AFRL_AFSEL7_Pos); GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEED5_Msk | GPIO_OSPEEDER_OSPEED6_Msk | GPIO_OSPEEDER_OSPEED7_Msk); // 3. 配置 PA4 为输出,做 CS GPIOA->MODER &= ~GPIO_MODER_MODER4_Msk; GPIOA->MODER |= GPIO_MODER_MODER4_0; GPIOA->ODR |= GPIO_ODR_OD4; // 默认高电平 // 4. SPI1 主模式,CPOL=0, CPHA=0,8位数据,最高速度 SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_BR_2 | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_SPE; }几个参数的解释:
SPI_CR1_MSTR:主机模式。SPI_CR1_BR_2:波特率预分频为 4 分频,45MHz / 4 = 11.25MHz。如果是 22.5MHz,应该用BR_1(2 分频)。这里看具体板子的稳定度来定。SPI_CR1_SSM | SPI_CR1_SSI:软件管理 NSS,禁止硬件片选自动控制。
模式 0(CPOL=0, CPHA=0)的含义是:空闲时 SCK 为低电平,数据在第一个时钟沿采样。MR25H40CDF 支持 SPI 模式 0 和模式 3,都能正常工作,但推荐模式 0,因为大多数 STM32 示例代码默认就是这个配置,踩坑概率最小。
3.2 命令集与状态机
MR25H40CDF 的命令集继承了 SPI NOR Flash 常用的几个指令,但要注意一个关键差异:MRAM 没有擦除指令,任何地址都可以直接写覆盖。
| 指令名称 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,每次写操作前必须执行 |
| WRDI | 0x04 | 写禁用 |
| RDSR | 0x05 | 读状态寄存器 |
| WRITE | 0x02 | 写数据,地址 3 字节 |
| READ | 0x03 | 读数据,地址 3 字节 |
| SLP | 0xB9 | 进入睡眠模式 |
| WUP | 0xAB | 唤醒 |
状态寄存器值得说一句。它只有几个有效位:bit0 是 WIP(写进行中),bit1 是 WEL(写使能锁存),bit2-bit4 是块保护位,bit7 是 WPEN。正常使用中,保持块保护位全为 0,这样才能全地址写入。如果不小心通过 WRSR 指令设置了块保护,低地址区就写不进去了,而且下次上电依然保持,排查时会怀疑人生。
3.3 按页写入与跨页处理
MR25H40CDF 的页大小也是 256 字节。所谓“页”,其实是芯片内部一次连续写入操作的单位,跨页写入不一定失败,但严谨起见,建议写成“按页切块”的逻辑,保证每次事务都落在页边界内。
void MRAM_WriteEnable(void) { MRAM_CS_Low(); MRAM_SPI_SendByte(0x06); MRAM_CS_High(); } void MRAM_WriteBytes(uint32_t addr, const uint8_t *data, uint32_t len) { while (len > 0) { uint32_t pageOffset = addr % 256; uint32_t chunk = 256 - pageOffset; if (chunk > len) chunk = len; MRAM_WriteEnable(); MRAM_CS_Low(); MRAM_SPI_SendByte(0x02); MRAM_SPI_SendByte((addr >> 16) & 0xFF); MRAM_SPI_SendByte((addr >> 8) & 0xFF); MRAM_SPI_SendByte(addr & 0xFF); for (uint32_t i = 0; i < chunk; i++) { MRAM_SPI_SendByte(data[i]); } MRAM_CS_High(); // 等待写入完成 while (MRAM_ReadStatus() & 0x01); addr += chunk; data += chunk; len -= chunk; } }重点说一下为什么每次写之前都要发WREN。MRAM 和 Flash 一样,每次 CS 从高变低并接收到写命令后,WEL 位会自动清零,所以每一个 WRITE 事务都必须先置位 WEL。漏掉这步,芯片会直接忽略写入操作。
3.4 读取数据
读取比写入简单得多,不需要 WREN,直接发读命令和地址就能连续读:
void MRAM_ReadBytes(uint32_t addr, uint8_t *data, uint32_t len) { MRAM_CS_Low(); MRAM_SPI_SendByte(0x03); MRAM_SPI_SendByte((addr >> 16) & 0xFF); MRAM_SPI_SendByte((addr >> 8) & 0xFF); MRAM_SPI_SendByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { data[i] = MRAM_SPI_RecvByte(); } MRAM_CS_High(); }读取速度可以跑满 SPI 带宽,而且没有等待周期,不像有些闪存需要插入 dummy byte。这也是 MRAM 在实时数据记录场景下特别好用的原因。
4. 数据可靠性与一致性设计
4.1 MRAM 的原子性写入优势
工业环境里最怕的就是写到一半掉电。普通 Flash 的页编程过程中掉电,这个页可能处于“半写”状态,之后这块区域可能变成坏块。MRAM 的写入机制本质上是改变磁性层的磁化方向,掉电不会破坏已写入部分,也不存在坏块问题。需要注意的是,“原子性”只针对单个字节,如果你写入一条多字节记录时掉电,理论上会出现“部分字节已经更新,部分还是旧值”的情况。这就需要在应用层做记录完整性校验。
4.2 CRC 校验与记录格式
我在日志模块里定义了这么一种记录格式:
typedef struct { uint16_t magic; // 固定魔数,比如 0x5A5A,用于定位记录起始 uint16_t length; // 实际数据长度 uint32_t timestamp; // 时间戳 uint16_t crc16; // 对整个记录体的 CRC16 校验值 uint8_t data[]; // 实际数据 } LogRecord;读取的时候,先检查 magic 是否匹配,再读 length,取数据后重新算 CRC 和记录里的 crc16 对比。如果 CRC 不对,说明这条记录写入不完整或者被破坏,直接跳过,继续找下一条,不影响整个日志系统的可用性。
这里补充一个我自己的小习惯:CRC 用 CRC16-CCITT(多项式 0x1021),软实现很简洁,查表法在 STM32F469 上跑毫无压力。虽然有硬件 CRC 外设,但那个计算单位更适合大块数据,对几十字节的结构体反而麻烦。
4.3 掉电与复位保护
系统掉电检测是另一个关键点。我用 STM32F469II 的 PVD(可编程电压检测器)监控 3.3V 电源。当电压跌到 3.0V 以下时触发 PVD 中断,中断服务程序里只做一件事:把当前还没写进 MRAM 的关键数据强制写入。因为 MRAM 写入速度极快,一个 64 字节记录在 11.25MHz SPI 下的耗时是微秒级别,即使在掉电瞬间也来得及。
这个机制可以保证最近一条数据不丢,但要注意 PVD 中断里不能调用 HAL_Delay 或者复杂函数,只做最必要的写操作。我就是因为一开始在中断里加了太多日志打印,导致电压都跌到 2.5V 了还没写完,后来才改成精简流程的。
5. 调试踩坑与排查方法
5.1 读写全 0xFF 或者全 0x00
这是最常见的起步问题。一上电读回来全是 0xFF,基本可以判定 SPI 通信完全没有建立。排查顺序:
- 用示波器抓 CS 拉低后 SCK 是不是有连续 8 个上升沿。如果没有,先查 SPI 初始化是否成功。
- 查 MISO 引脚有没有波形。如果发送了命令但 MISO 一直是高,说明芯片没有回应,可能 WP# 或 HOLD# 悬空把芯片锁住了。
- 查供电。MRAM 工作电压下限是 2.7V,如果板子供电不足,芯片可能处于上电复位后的不确定状态。
5.2 写入后读取数据对不上,重启又变回来了
这个现象的典型原因是WREN 没生效。注意:MRAM 每次上电后 WEL 位默认是 0,必须发 WREN 才能写。如果你的代码里只发了一次 WREN,后面连续写多页,第二页开始就会失败。解决方法是每个 WRITE 事务前都重新发 WREN,代码段里我已经这么写了,不要贪图省事把它提到循环外面。
还有一种情况是状态寄存器里的块保护位被设置成了非 0。可以用 RDSR 读一下 0x05,如果 bit2-bit4 有值,就发 WRSR (0x01) 把状态寄存器清成 0x00。注意 MRAM 的 WRSR 操作只有一位,后面跟的字节直接覆盖整个状态寄存器内容。
5.3 数据偶发错位或者 CRC 校验失败
如果写入后立刻读出来是对的,但系统运行一段时间偶尔发现某条记录 CRC 错了,大概率是 SPI 时序受干扰。我遇到过两种情况:
- CS 信号毛刺:CS 是高电平有效释放,释放不干净时芯片可能多接收几个时钟。对策是启用 GPIO 内部上拉,并且在 CS 低有效期间不要执行其他中断,保证 SPI 事务不被拆分。
- SCK 振铃:在高速率下长走线的反射可能造成额外的时钟沿。对策是在 SCK 上串一个 22Ω 电阻,或者降低 SPI 预分频系数。
还有一个容易忽略的点:DMA 和 CPU 并发访问 SPI 外设时,如果 DMA 配置错了字节序,读出来的数据段会整体翻转。这问题看起来很像是芯片坏了,实际只是DMA_CCR->PSIZE或者MSIZE设置不一致。
5.4 快速验证流程推荐
当你拿到新板子,别急着写一大堆业务逻辑。先跑一个最简验证程序:
- 全片写 0xA5,然后读出来比对。
- 随机地址写 100 组随机数据,全部读回并逐一比对。
- 连续 10 万次写同一个地址,验证数据一致性,顺便测一下写耐久度。
- 在写入过程中反复开关电源,确认掉电后的数据状态符合预期。
这几步跑完,基本可以排除硬件和驱动层的问题,再往上层业务走就轻松了。
6. 场景拓展:把 MRAM 真正用进工业项目
6.1 与 Modbus / OPC UA 的联动
热搜词里提到了 MODBUS 和 OPC UA 读取设备状态,这恰好是我做的那个网关的完整链路。现场 PLC、传感器、数控机床通过 Modbus RTU 或者 OPC UA 把运行数据送到网关,网关的 STM32F469II 解析完数据后,一部分实时上报,一部分关键数据存入 MRAM。
这里有个架构上的考虑:MRAM 属于“慢设备”内存映射的外设,不应该直接供业务线程频繁调用。更好的做法是:
- 定义一个环形缓冲的结构体数组,放在 STM32 的 SRAM 里。
- MODBUS 采集任务往缓冲里追加记录。
- 每隔一段时间或者缓冲满了一半,统一把整段数据写入 MRAM。
这样既充分用到了 MRAM 的写入速度,又让业务代码解耦。
6.2 环形日志记录器设计
工业设备记录的日志应当是环形覆盖的,这样即使长时间运行也不会把容量耗尽。
起始地址: 记录区头部,存放当前写指针 数据区: 从偏移 4 开始依次存放 LogRecord 容量: 512KB 中划出 16KB 存配置文件,其余全部做日志写指针以 4 字节对齐递增,写到末端后回到起始位置覆盖最旧记录。每次上电先读头部指针,如果指针值超出数据区范围,就按损坏处理,重置到起始地址。这个设计很轻量,不需要文件系统,也不会产生垃圾碎片。
我当时的实际配置:日志记录固定 48 字节,512KB 数据区可以存大概 1 万条记录。设备每 5 秒记录一次,大约 14 小时循环一轮。对于现场故障排查来说,最近半天的数据基本都是够用的。
6.3 后续扩展想法
如果你不想局限于裸机驱动,想把 MRAM 做得更通用,可以考虑这几个方向:
- 移植一个微型文件系统,比如 LittleFS 或者 SPIFFS。MRAM 的写入寿命让它不需要 Flash 那样的损耗均衡,文件系统可以简化很多。
- 在 MRAM 中保存网络参数和校准参数,替代传统的 EEPROM。MRAM 容量大,可以存多套配置带版本号,方便远程升级回滚。
- 把 MRAM 和 RTC 结合,每次上电后从 MRAM 读最近一次关机时间,对比 RTC 当前时间,推断掉电持续时长,这在故障分析里非常有用。
- 做多副本存储:关键配置存三份,启动时投票决定用哪份。MRAM 容量 512KB,复制几份几乎没有压力。
从我实际使用几个月的体验看,MR25H40CDF 和 STM32F469II 的组合在工业存储场景下非常省心。MRAM 最打动我的地方就是不用考虑“擦除”和“磨损”这两个 Flash 时代的心头大患,驱动代码写起来干净利落。如果你正在为选型犹豫,我建议先拿样品板跑一下这个验证流程,实际感受一下直接写覆盖带来的开发体验提升,大概率你也会把 MRAM 放进方案里。