1. 为什么 MRAM 在工业嵌入式场景里越来越受关注
搞过工业设备数据记录的朋友应该都有体会,选存储介质这件事,看着简单,实际上坑特别多。EEPROM 擦写寿命十万次,频繁记录的场合撑不了多久;NOR Flash 写入前必须擦除整个扇区,掉电时机不对就丢数据;FRAM 倒是好用,但容量小、价格高,大一点的型号不好买。这几年 MRAM 慢慢进入大家的视野,MR25H40CDF 就是其中一颗很有代表性的芯片。
MR25H40CDF 是 Everspin 出的 4Mbit 串行 MRAM,SPI 接口,512K 字节容量,3.3V 供电,工业级温度范围覆盖 -40 到 105 摄氏度。它最大的特点就是:写入不需要擦除、没有写入延迟、擦写寿命理论上无限、掉电数据不丢。这几点加在一起,对于工业数据记录、参数存储、日志缓存这类场景来说,几乎是理想方案。
我这次用的主控是 STM32F745ZG,Cortex-M7 内核,216MHz 主频,片上带 Quad-SPI 和多个普通 SPI 外设。选它是因为这个项目本身还要跑一些实时控制逻辑,F7 的算力和外设资源都够用,而且 STM32 的 HAL 库生态成熟,SPI 驱动写起来快。整套方案的目标很明确:用 MR25H40CDF 做非易失数据存储,STM32F745ZG 通过 SPI 总线读写,实现工业现场的参数保存和运行日志记录。
这篇文章我会把整个链路拆开讲:从 MRAM 的选型逻辑、SPI 硬件设计、CubeMX 配置、HAL 库驱动编写,到实际调试中遇到的问题和排查方法。如果你手头正好有类似的存储需求,或者单纯想了解一下 MRAM 怎么用,这篇内容应该能帮你少走一些弯路。
2. 方案整体设计与核心选型思路
2.1 为什么选 MRAM 而不是 EEPROM 或 Flash
先把这个事情说清楚,因为选型选错了后面全是白费功夫。工业场景下的非易失存储,常见选项就那么几个:EEPROM、NOR Flash、FRAM、MRAM。我列个表对比一下,这样更直观。
| 特性 | EEPROM | NOR Flash | FRAM | MRAM (MR25H40CDF) |
|---|---|---|---|---|
| 擦写寿命 | 约100万次 | 约10万次 | 10^14次 | 无限次 |
| 写入前擦除 | 不需要 | 需要 | 不需要 | 不需要 |
| 写入速度 | 慢(ms级) | 慢(ms级) | 快(ns级) | 快(ns级) |
| 掉电保持 | 10年+ | 10年+ | 10年+ | 20年+ |
| 容量范围 | 小 | 中 | 小 | 中 |
| 接口 | I2C/SPI | SPI | I2C/SPI | SPI |
| 单位成本 | 低 | 低 | 高 | 中高 |
从表里能看出来,MRAM 在寿命和写入速度上优势明显。有人可能会问,FRAM 不是也很快吗?对,但 FRAM 的容量通常偏小,而且价格更贵,供货也不如 MRAM 稳定。MR25H40CDF 的 512KB 容量对于大多数参数存储和日志记录场景已经够用了。
还有一个很关键的点:MRAM 的写入行为和普通 RAM 几乎一样,你直接往地址里写数据就行,不需要关心扇区边界、不需要先擦后写。这意味着驱动层可以做得非常简单,不用维护复杂的擦除管理逻辑。对于工业设备这种要求长期稳定运行、维护成本高的场景,简单就意味着可靠。
2.2 STM32F745ZG 的 SPI 外设分配
STM32F745ZG 有多个 SPI 接口,SPI1 到 SPI6,其中 SPI1/4/5/6 支持最高 50MHz 时钟,SPI2/3 最高 25MHz。MR25H40CDF 的 SPI 最高支持 40MHz,所以理论上用 SPI1 可以跑到比较高的速率。
但实际选哪个 SPI 口,不能只看速度。我当时的考虑是:
- SPI1 通常被板上的其他高速外设占用(比如某些传感器或显示屏),需要确认引脚复用情况
- SPI2/3 速度够用,而且引脚布局更方便走线
- 如果后续要接多个 SPI 从设备,需要提前规划片选引脚
最后我选了 SPI2,时钟配置到 20MHz 左右。为什么不上更高?因为工业现场的 PCB 走线往往比较长,信号完整性是个现实问题,20MHz 在稳定性和速度之间是个比较好的平衡点。后面调试部分我会详细说这个取舍。
2.3 硬件连接与片选设计
MR25H40CDF 的引脚不多,核心就是 SPI 四线加片选:
- SCLK:SPI 时钟
- SI:数据输入(MOSI)
- SO:数据输出(MISO)
- CS:片选,低有效
- VDD:3.3V
- VSS:地
片选这块有个细节值得说。STM32 的 SPI 外设支持硬件片选(NSS)和软件片选两种方式。硬件片选由 SPI 外设自动控制,时序更精确,但灵活性差,多个从设备时管理麻烦。软件片选就是用普通 GPIO 手动拉低拉高,时序由代码控制,灵活但需要自己保证时序正确。
我选的是软件片选。原因很简单:工业板子上往往不止一个 SPI 从设备,用软件片选可以自由管理多个设备的片选信号,而且调试的时候更容易用逻辑分析仪抓波形。代价就是代码里要严格保证片选拉低和拉高的时机,这个后面驱动部分会讲。
硬件上还有两个容易忽略的点:一是 CS 引脚最好加上拉电阻,防止上电瞬间浮空导致误触发;二是 SCLK 和 MOSI 走线如果比较长,建议串接小电阻(22到33欧姆)做阻抗匹配,减少过冲和振铃。
3. MR25H40CDF 的 SPI 通信协议细节
3.1 命令集与操作码
MR25H40CDF 的 SPI 命令集比较简洁,常用的就几条:
| 命令 | 操作码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
注意 WRITE 操作之前必须先发 WREN 命令,这是很多 SPI 存储芯片的通用规则。MRAM 虽然写入不需要擦除,但写保护机制还是有的,WREN 就是用来解除写保护的。
状态寄存器的 bit0 是 WEL(Write Enable Latch),发完 WREN 之后这位会置1,写完数据后自动清零。调试的时候如果发现写不进去,第一件事就是读状态寄存器看 WEL 位是不是1。
3.2 读写时序分析
读操作的时序是这样的:CS 拉低,发送 0x03,然后发送 3 字节地址(MR25H40CDF 是 19 位地址,但用 3 字节传输,高位补0),接着时钟继续输出,数据就从 SO 引脚出来了。可以连续读,地址自动递增,直到 CS 拉高。
写操作的时序类似:CS 拉低,先发 WREN(0x06),CS 拉高;然后 CS 再拉低,发 WRITE(0x02),发 3 字节地址,然后发数据。数据可以连续写,地址自动递增。写完最后一个字节后 CS 拉高,芯片内部开始写入周期。
这里有个关键点:MRAM 的写入是几乎无延迟的,不像 Flash 需要等几毫秒的写入周期。但状态寄存器的 WEL 位在 CS 拉高后需要一点时间才能反映最新状态,所以连续写操作之间建议稍微加一点延时,或者每次写之前都重新发 WREN。
3.3 SPI 模式选择
MR25H40CDF 支持 SPI Mode 0 和 Mode 3。Mode 0 是 CPOL=0、CPHA=0,时钟空闲低电平,第一个边沿采样;Mode 3 是 CPOL=1、CPHA=1,时钟空闲高电平,第二个边沿采样。
我用的是 Mode 0,这也是最常用的配置。在 CubeMX 里配置 SPI 的时候,CPOL 和 CPHA 都设成 Low 就行。如果你发现读出来的数据全是 0xFF 或者乱码,先检查一下模式设置对不对,这是最常见的坑之一。
4. STM32F745ZG 的 CubeMX 配置与驱动实现
4.1 CubeMX 中的 SPI 参数配置
打开 CubeMX,选好 STM32F745ZG 之后,找到 SPI2 进行配置。关键参数如下:
- Mode:Full-Duplex Master
- Data Size:8 Bits
- Clock Polarity:Low
- Clock Phase:1 Edge(也就是第一个边沿)
- NSS:Software(软件片选)
- Baud Rate Prescaler:根据主频计算,目标是 20MHz 左右
- First Bit:MSB First
波特率这块说一下计算过程。STM32F745ZG 的 APB1 总线时钟我配置的是 54MHz,SPI2 挂在 APB1 上。分频系数可选 2、4、8、16、32、64、128、256。54MHz 除以 4 等于 13.5MHz,除以 2 等于 27MHz。27MHz 虽然没超过 MR25H40CDF 的 40MHz 上限,但考虑到走线损耗,我选了 4 分频,实际时钟 13.5MHz。实测这个速率下读写都很稳定,对于数据记录场景完全够用。
另外别忘了配置一个 GPIO 输出引脚作为 CS 片选,初始电平设为高。
4.2 HAL 库驱动代码实现
CubeMX 生成代码之后,SPI 的初始化已经自动完成了。我们需要自己写的是 MRAM 的读写函数。下面是我实际用的代码,基于 HAL 库。
先定义片选操作宏:
#define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET)写使能函数:
void MRAM_WriteEnable(void) { uint8_t cmd = 0x06; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); MRAM_CS_HIGH(); }读状态寄存器:
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; }写数据函数:
void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { uint8_t cmd[4]; cmd[0] = 0x02; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; MRAM_WriteEnable(); MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Transmit(&hspi2, data, len, 1000); MRAM_CS_HIGH(); }读数据函数:
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; MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi2, cmd, 4, 100); HAL_SPI_Receive(&hspi2, buf, len, 1000); MRAM_CS_HIGH(); }这段代码看起来简单,但有几个地方容易出问题。第一,HAL_SPI_Transmit 和 HAL_SPI_Receive 的超时参数要设够,尤其是写操作,如果超时太短可能返回 HAL_TIMEOUT。第二,CS 拉高之后最好加一个微秒级的延时再执行下一次操作,给芯片内部状态机一点恢复时间。第三,地址是 19 位的,但用 3 字节传输,高位补0就行,MR25H40CDF 会自动忽略超出容量的地址位。
4.3 读写验证与测试方法
驱动写完之后,第一步肯定是做基本的读写测试。我的做法是:往地址 0x00000 写一串已知数据,比如 0xAA、0x55、0x01、0x02 这样,然后读回来对比。如果一致,再测试跨页写入、连续读写、边界地址等场景。
这里有个小技巧:测试数据不要全用 0x00 或 0xFF,因为这两种数据在 SPI 通信异常时最容易出现(MISO 一直低或一直高),用交替的 pattern 更容易发现问题。
我一般会写一个简单的测试函数,循环写入递增的数据,然后读回来校验,跑个几千次,看看有没有偶发错误。这个测试能覆盖大部分硬件连接和时序问题。
5. 实操过程中的问题排查与经验总结
5.1 常见问题速查表
在实际调试过程中,我遇到和收集到的问题大概有这些:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读出来全是0xFF | MISO未连接或CS未拉低 | 检查硬件连接,用逻辑分析仪抓CS和MISO |
| 读出来全是0x00 | MISO被拉低或SPI模式错误 | 检查SPI模式设置,确认CPOL/CPHA |
| 写入后读回数据不对 | WREN未发送或WEL位未置1 | 读状态寄存器确认WEL位 |
| 偶发数据错误 | 时钟太快或走线干扰 | 降低SPI时钟,检查PCB走线 |
| 完全无响应 | 供电异常或芯片损坏 | 测量VDD电压,检查焊接 |
| 写入成功但掉电丢失 | 写保护引脚状态错误 | 检查WP引脚电平 |
5.2 逻辑分析仪抓波形的重要性
调试 SPI 设备,逻辑分析仪几乎是必备工具。我用的是一款入门级的 8 通道逻辑分析仪,配合开源软件就能解码 SPI 协议。抓波形的时候重点看几个东西:
CS 拉低到第一个时钟边沿的建立时间够不够,一般要求至少几纳秒,实际看波形只要不是同时变化就行。时钟极性对不对,Mode 0 下空闲应该是低电平。数据在时钟的哪个边沿变化、哪个边沿采样,这个和 CPHA 设置直接相关。最后一个字节发完之后 CS 拉高的时机,太早可能导致最后一个字节没发完,太晚可能影响下一次操作。
有一次我遇到读数据偶尔错一位的情况,抓波形发现是 CS 拉高的时机比最后一个时钟边沿早了大概 10ns,导致最后一个 bit 没被正确采样。后来在 CS 拉高之前加了一个 __NOP() 就好了。这种问题不看波形根本找不到。
5.3 工业现场的可靠性加固
实验室里跑通不代表现场能用。工业环境的电磁干扰、温度变化、电源波动都比实验室恶劣得多。我在实际项目中做了这几件事来加固:
电源端加 0.1uF 和 10uF 的退耦电容,尽量靠近 MRAM 的 VDD 引脚。SPI 信号线上串接 33 欧姆电阻,减少反射。CS 信号加上拉电阻,防止浮空。PCB 布局上,MRAM 尽量靠近主控,走线短而直,避免和功率线路平行走。
软件层面,每次写入之后都读回来校验,如果校验失败就重试,重试三次还失败就记录错误标志。这个机制在实际运行中确实抓到过几次偶发的写入异常,虽然概率很低,但工业设备要求长期无人值守运行,这种保护是必要的。
另外,MRAM 虽然写入寿命无限,但读操作理论上也有次数限制(虽然极高),所以不要在高频循环里反复读同一地址,该用缓存的地方还是用缓存。
6. 数据存储结构设计与实际应用场景
6.1 存储区域划分
512KB 的空间看着不大,但规划好了能存不少东西。我的划分方式是:
前 4KB 作为配置参数区,存储设备ID、校准参数、运行模式等。中间 252KB 作为日志区,循环写入运行日志。最后 256KB 作为数据记录区,存储采集到的历史数据。
配置参数区每次修改都直接写入,因为 MRAM 不需要擦除,改哪个字节就写哪个字节,非常方便。日志区用环形缓冲的方式管理,写满之后从头覆盖。数据记录区按固定大小的记录块存储,每条记录带时间戳和校验和。
6.2 掉电保护策略
MRAM 的一大优势就是掉电数据不丢,但前提是数据已经写进去了。如果掉电发生在写入过程中,可能会造成部分数据不完整。我的做法是:每条记录写入之前先写一个标志字节表示"正在写入",写完之后再改成"写入完成"。上电初始化的时候扫描记录区,遇到"正在写入"状态的记录就标记为无效。
这个机制配合 MRAM 的快速写入特性,实际上掉电丢失数据的窗口非常小,基本可以忽略不计。相比 Flash 需要几毫秒的写入周期,MRAM 的写入是纳秒级的,掉电时正在写入的概率极低。
6.3 与文件系统的配合使用
如果数据量比较大,需要更复杂的管理,可以考虑在 MRAM 上跑一个轻量级文件系统,比如 LittleFS 或者 SPIFFS。这两个都是为嵌入式设计的,支持掉电保护、磨损均衡(虽然 MRAM 不需要)、目录结构。
不过对于大多数工业参数存储场景,直接按地址读写反而更简单可靠。文件系统会引入额外的复杂度和开销,除非你确实需要文件管理功能,否则没必要上。我个人的经验是:数据量小于 100KB、结构固定的话,直接地址映射最省事;数据量大、结构灵活的话,再考虑文件系统。
7. 性能实测与优化建议
7.1 读写速度实测数据
我在 13.5MHz SPI 时钟下做了实际测试,结果如下:
| 操作 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节写 | 1字节 | 约2us | - |
| 连续写 | 1KB | 约620us | 约1.65MB/s |
| 连续读 | 1KB | 约610us | 约1.68MB/s |
| 单字节读 | 1字节 | 约2us | - |
这个速度对于工业数据记录来说完全够用。如果你需要更高的吞吐量,可以把 SPI 时钟提到 27MHz 甚至更高,但要注意信号完整性问题。我试过 27MHz,在短走线的板子上没问题,但走线超过 10cm 之后误码率明显上升。
7.2 DMA 传输的优化
如果 CPU 需要同时处理其他任务,可以用 DMA 来搬运 SPI 数据。STM32F745ZG 的 SPI2 支持 DMA 请求,配置好 DMA 通道之后,读写操作可以后台完成,CPU 只需要处理完成中断。
不过对于小数据量的读写(比如几十字节),DMA 的配置开销可能比直接传输还大。我的建议是:数据量超过 256 字节再考虑 DMA,小数据量直接阻塞传输更简单。
7.3 实际项目中的经验参数
最后分享几个我在实际项目中总结的参数,可以直接参考:
- SPI 时钟:13.5MHz(稳定优先)或 27MHz(速度优先,需验证信号质量)
- CS 建立时间:拉低后至少延时 100ns 再发时钟
- CS 保持时间:最后一个时钟后至少延时 100ns 再拉高
- 写操作重试次数:3次
- 读校验:每次写入后立即读回校验
- 退耦电容:0.1uF + 10uF 并联,靠近芯片引脚
这套参数在我做的几个工业项目里都跑得很稳,现场运行一年多没有出现过存储相关的故障。当然具体项目还要根据实际情况调整,尤其是 SPI 时钟,一定要用逻辑分析仪或者示波器确认波形质量之后再定。