1. 为什么 MRAM 在工业嵌入式场景里越来越受关注
搞过工业数据采集或者电力终端的朋友应该都有体会,选存储芯片这件事,很多时候比选主控还让人头疼。EEPROM 擦写寿命不够、Flash 写入前要擦除、掉电保护还得额外加超级电容或者铁电,方案越堆越复杂。我最早接触 MR25H40CDF 这颗料,是因为一个配电终端的项目,要求每秒记录一次电参量,断电不能丢数据,而且现场环境温度从零下四十度到八十五度,普通方案根本扛不住。
MR25H40CDF 是 Everspin 推出的一款 4Mbit 串行 MRAM(磁性随机存储器),SPI 接口,支持 40MHz 时钟,工作电压 2.7V 到 3.6V,工业级温度范围。它最大的特点就是写入不需要擦除、写入速度接近 SRAM、擦写寿命几乎无限(官方标称 10^14 次以上),而且掉电后数据能保持二十年以上。这几个特性放在一起,基本上就是为工业现场数据记录量身定做的。
STM32F100ZE 则是 ST 家的 Cortex-M3 入门级 MCU,主频 24MHz,带 512KB Flash、32KB SRAM,外设资源对于这种数据记录场景绰绰有余。它自带三个 SPI 接口,其中 SPI1 挂在 APB2 上,最高 12MHz(F100 系列 APB2 最高 24MHz,SPI 分频最低 2 分频),SPI2 挂在 APB1 上最高 6MHz。虽然跑不满 MR25H40CDF 的 40MHz,但对于每秒一次的数据记录来说,12MHz 的 SPI 时钟已经绰绰有余了。
这套组合我前后在三个项目里用过,踩过不少坑,也总结了一些比较实用的经验。下面我会从方案选型、硬件设计、驱动实现、数据管理策略、问题排查几个维度,把整个链路拆开讲清楚。不管你是刚接触 SPI 存储的新手,还是想从 Flash 方案迁移到 MRAM 的老手,应该都能找到有用的东西。
2. 方案选型:为什么是 MRAM 而不是 Flash 或 EEPROM
2.1 三种非易失存储的硬核对比
在动手写代码之前,先把选型逻辑理清楚。很多人拿到需求第一反应是上 SPI Flash,比如 W25Q64 这类,便宜量大。但工业场景和消费电子不一样,几个关键指标一摆出来,选择就很清楚了。
| 对比维度 | SPI Flash (W25Q64) | EEPROM (AT25) | MRAM (MR25H40CDF) |
|---|---|---|---|
| 写入前擦除 | 必须,按扇区 | 不需要 | 不需要 |
| 字节写入速度 | 慢(ms 级) | 中等(5ms/页) | 快(无等待) |
| 擦写寿命 | 10 万次 | 100 万次 | 10^14 次以上 |
| 掉电保护 | 需额外电路 | 较好 | 天然可靠 |
| 写入功耗 | 高(擦除时) | 中等 | 低 |
| 单位成本 | 低 | 中 | 高 |
| 接口速率 | 高(可达 80MHz+) | 低(通常 10MHz) | 高(40MHz) |
从表里能看出来,MRAM 唯一的劣势就是贵。但工业项目里,一颗料贵几块钱,换来的是省掉超级电容、省掉擦除管理逻辑、省掉磨损均衡算法,整体 BOM 成本和开发成本反而是降低的。
2.2 MR25H40CDF 的关键参数解读
选型的时候有几个参数容易被忽略,我逐个说一下。
容量 4Mbit 等于 512KB,这个容量对于数据记录来说不算大,但配合合理的存储结构设计,存几十万条记录没问题。我一般会把空间划分成元数据区、索引区和数据区,后面会详细讲。
SPI 模式支持 Mode 0 和 Mode 3,这个在初始化的时候必须和 MCU 端配置一致。STM32 的 HAL 库默认是 Mode 0(CPOL=0,CPHA=0),MR25H40CDF 两种都支持,所以直接用默认配置就行。
40MHz 最高时钟,但 STM32F100ZE 的 SPI1 最高只能到 12MHz,所以实际跑 12MHz。这里要注意,虽然芯片支持 40MHz,但 PCB 走线质量不好的话,12MHz 都可能出问题,后面硬件部分会讲。
写入无延迟,这是 MRAM 和 Flash 最大的区别。Flash 写完一个字节要等内部编程完成,通常需要几毫秒,期间不能响应其他命令。MRAM 写完之后下一个时钟周期就能读,不需要轮询状态寄存器。这个特性直接简化了驱动逻辑。
2.3 STM32F100ZE 的 SPI 资源分配
STM32F100ZE 有三个 SPI,实际用的时候要规划好。我的习惯是 SPI1 专门给 MRAM 用,因为它在 APB2 上,时钟频率高,而且 DMA 通道独立,不会和其他外设抢资源。SPI2 和 SPI3 留给其他外设,比如显示屏或者无线模块。
这里有个细节:STM32F100 的 SPI1 挂载在 APB2 总线上,APB2 最高 24MHz,SPI 时钟分频系数最小是 2,所以 SPI1 最高 12MHz。SPI2 和 SPI3 挂在 APB1 上,APB1 最高也是 24MHz,但 SPI2/3 的分频系数最小也是 2,所以最高也是 12MHz。不过实际配置的时候,我一般会把 SPI1 设成 6MHz 或者 12MHz,看 PCB 走线情况。
注意:STM32F100 系列属于 Value Line,SPI 的 FIFO 深度和 F4 系列不一样,没有 16 字节 FIFO,收发都是一个字节一个字节来。用 DMA 的时候要注意这一点,别按 F4 的思路写。
3. 硬件设计:SPI 接口和 PCB 布局的实战要点
3.1 引脚连接与上拉电阻配置
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很标准:CS、SO、SI、SCK、VCC、GND,另外还有两个 NC 引脚。和 STM32F100ZE 连接的时候,我一般这样分配:
- CS 接 PA4(SPI1_NSS,也可以用普通 GPIO 软件控制)
- SCK 接 PA5(SPI1_SCK)
- MISO 接 PA6(SPI1_MISO)
- MOSI 接 PA7(SPI1_MOSI)
这里有个选择:硬件片选还是软件片选。STM32 的硬件 NSS 在 SPI 使能的时候会自动管理,但多从机场景下容易出问题。我建议用软件片选,也就是把 PA4 配成普通推挽输出,手动拉低拉高。这样控制更灵活,也不会因为 SPI 外设状态异常导致片选信号乱跳。
CS 引脚必须加一个 10K 的上拉电阻到 VCC,这是很多新手会漏掉的。MRAM 在上电过程中,如果 CS 悬空或者被干扰拉低,可能会误触发写操作。虽然概率不高,但工业现场电磁环境复杂,加上更稳妥。
3.2 PCB 走线的几个硬性规则
SPI 虽然速率不算特别高,但 12MHz 的时钟边沿已经比较陡了,走线不好照样出问题。我总结了几条必须遵守的规则:
第一,SCK 走线尽量短,不要有过孔。时钟信号对阻抗变化最敏感,过孔会引入寄生电容和电感,导致边沿变缓或者振铃。如果实在要换层,旁边放一个接地过孔。
第二,MISO 和 MOSI 不要平行走太长。这两根线容易串扰,尤其是 MISO 是输入线,被 MOSI 干扰之后数据可能出错。如果必须平行走,中间加一根地线隔离。
第三,MRAM 的电源引脚旁边必须放 0.1uF 去耦电容,越近越好。我一般还会并一个 1uF 的,应对写入瞬间的电流波动。虽然 MRAM 写入功耗比 Flash 低,但瞬间电流还是有的。
第四,CS 走线不要和 SCK 平行。CS 是控制信号,如果被 SCK 耦合,可能在时钟边沿产生毛刺,导致误操作。
3.3 电源设计和掉电检测
MR25H40CDF 的工作电压是 2.7V 到 3.6V,和 STM32F100ZE 的 3.3V 系统可以直接对接,不需要电平转换。但工业现场电源波动大,我建议在 MRAM 的 VCC 引脚前面加一个 LC 滤波,L 用磁珠,C 用 10uF 和 0.1uF 并联。
另外,虽然 MRAM 掉电不丢数据,但写入过程中掉电仍然可能导致数据不完整。所以如果系统有掉电检测需求,可以在电源端加一个电压监测芯片,当电压低于 2.9V 的时候触发中断,MCU 在中断里完成当前写入操作。MRAM 的写入速度极快,几个微秒就能完成,所以只要检测及时,基本不会丢数据。
4. 驱动实现:从 SPI 底层到 MRAM 读写封装
4.1 STM32CubeMX 配置要点
用 CubeMX 配置 SPI1 的时候,几个关键参数这样设:
- Mode:Full-Duplex Master
- Data Size:8 Bits
- Clock Polarity:Low
- Clock Phase:1 Edge
- NSS:Software
- Baud Rate Prescaler:4(得到 6MHz)或者 2(得到 12MHz)
- First Bit:MSB First
这里重点说一下Clock Polarity 和 Clock Phase。MR25H40CDF 支持 Mode 0 和 Mode 3,Mode 0 是 CPOL=0、CPHA=0,也就是时钟空闲低电平,第一个边沿采样。CubeMX 里对应 CPOL=Low、CPHA=1Edge。这个配置和大多数 SPI Flash 一样,直接用就行。
Baud Rate Prescaler 的选择要看 PCB 情况。我一般先用 4 分频(6MHz)调通,然后再尝试 2 分频(12MHz)。如果 12MHz 下读写不稳定,就退回 6MHz。工业项目里稳定性优先,6MHz 对于每秒一次的数据记录完全够用。
4.2 MRAM 命令集和读写时序
MR25H40CDF 的命令集很简洁,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
和 Flash 不一样的是,MRAM 的 WRITE 命令不需要先擦除,直接写就行。而且写完之后不需要轮询状态寄存器等待完成,下一个操作可以直接开始。
读时序是这样的:拉低 CS,发送 0x03,然后发送 24 位地址(MR25H40CDF 是 4Mbit,需要 19 位地址,但按 24 位发送,高 5 位忽略),然后连续读取数据,每读一个字节地址自动递增。写时序类似,命令换成 0x02,但写之前必须先发 WREN。
4.3 底层驱动代码实现
先看 SPI 底层收发函数,我用的是 HAL 库:
static uint8_t SPI1_ReadWriteByte(uint8_t txData) { uint8_t rxData; HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, 100); return rxData; }然后是 MRAM 的读函数:
void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_ReadWriteByte(0x03); SPI1_ReadWriteByte((addr >> 16) & 0xFF); SPI1_ReadWriteByte((addr >> 8) & 0xFF); SPI1_ReadWriteByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = SPI1_ReadWriteByte(0xFF); } MRAM_CS_HIGH(); }写函数:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_ReadWriteByte(0x06); // WREN MRAM_CS_HIGH(); MRAM_CS_LOW(); SPI1_ReadWriteByte(0x02); // WRITE SPI1_ReadWriteByte((addr >> 16) & 0xFF); SPI1_ReadWriteByte((addr >> 8) & 0xFF); SPI1_ReadWriteByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { SPI1_ReadWriteByte(buf[i]); } MRAM_CS_HIGH(); }这里有个细节:WREN 和 WRITE 之间必须拉高 CS,否则 WREN 不生效。这是 SPI 存储器的通用规则,但很多人第一次写的时候会漏掉。
4.4 用 DMA 提升大批量读写效率
如果只是每秒写几十个字节,上面的阻塞式读写完全够用。但如果要批量导出数据,比如一次读几 KB,用 DMA 会快很多。STM32F100ZE 的 SPI1 有独立的 TX 和 RX DMA 通道,配置起来也不复杂。
用 DMA 的时候要注意:CS 信号必须在 DMA 传输完成之后才能拉高。我一般用 DMA 传输完成中断来拉高 CS,或者在发送完地址之后启动 DMA,然后在 DMA 完成回调里操作 CS。
void MRAM_Read_DMA(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); SPI1_ReadWriteByte(0x03); SPI1_ReadWriteByte((addr >> 16) & 0xFF); SPI1_ReadWriteByte((addr >> 8) & 0xFF); SPI1_ReadWriteByte(addr & 0xFF); HAL_SPI_Receive_DMA(&hspi1, buf, len); // CS 在 DMA 完成回调里拉高 }注意:STM32F100 的 DMA 通道和 SPI 的对应关系是固定的,SPI1_RX 是 DMA1_Channel2,SPI1_TX 是 DMA1_Channel3。CubeMX 会自动配置,但手动写的时候别搞错。
5. 数据管理策略:怎么把 512KB 用出花来
5.1 存储空间的分区设计
512KB 听起来不大,但合理规划之后能存不少东西。我一般把 MRAM 分成四个区:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 配置区 | 0x00000 | 4KB | 设备参数、校准系数 |
| 索引区 | 0x01000 | 4KB | 记录索引、写指针 |
| 数据区 | 0x02000 | 504KB | 实际记录数据 |
| 备份区 | 0x7F000 | 4KB | 关键数据备份 |
配置区存一些不常变但很重要的参数,比如设备 ID、量程、校准系数。索引区存当前写指针、记录总数、循环覆盖标志。数据区按固定长度记录存储,比如每条记录 32 字节,那 504KB 能存 16128 条。备份区存一份配置区和索引区的副本,防止意外改写。
5.2 循环存储和磨损均衡
虽然 MRAM 寿命几乎无限,但为了数据管理的方便,我还是会用循环存储的方式。写指针到了数据区末尾就回到开头,覆盖最老的记录。这样不需要复杂的磨损均衡算法,因为 MRAM 每个字节的写入寿命都是一样的,不存在某个扇区先坏的问题。
不过有一个点要注意:索引区的写指针更新频率很高,如果每次写数据都更新索引,索引区的写入次数会远高于数据区。虽然 MRAM 扛得住,但为了减少不必要的写入,我一般会每写 16 条记录才更新一次索引,中间用 RAM 缓存。掉电的时候最多丢 15 条记录的索引,但数据本身还在,上电后可以扫描恢复。
5.3 数据完整性校验
工业数据最怕的就是存进去是错的,读出来还是错的。我一般会在每条记录末尾加两个字节的 CRC16 校验。写入的时候算好 CRC 一起写进去,读取的时候重新算一遍对比。如果 CRC 不匹配,说明这条记录损坏,跳过或者标记。
CRC16 的计算用查表法最快,提前生成一张 256 项的表格放在 Flash 里,每次计算查表就行。STM32F100ZE 的 Flash 有 512KB,放一张 CRC 表绰绰有余。
static const uint16_t crc16_table[256] = { ... }; uint16_t CRC16_Calc(uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF]; } return crc; }5.4 掉电保护的实际处理
MRAM 本身掉电不丢数据,但写入过程中掉电可能导致记录不完整。我的做法是:每条记录写入之前,先在索引区标记“写入中”,写完之后再标记“写入完成”。上电初始化的时候扫描索引区,如果发现“写入中”的记录,说明上次写入被中断了,直接丢弃这条记录。
这个机制用两个字节就能实现,代价很小,但能保证数据的完整性。实际测试下来,反复断电上电几百次,没有出现过数据错乱的情况。
6. 常见问题与排查技巧实录
6.1 读写数据全 FF 或者全 00
这是最常见的问题,一般有三个原因。第一,CS 信号没拉低或者拉低时间不对,用示波器看 CS、SCK、MOSI 三根线,确认 CS 拉低之后 SCK 才开始跳。第二,SPI 模式配置错误,MRAM 支持 Mode 0 和 Mode 3,如果配成 Mode 1 或 Mode 2,数据肯定错。第三,MISO 和 MOSI 接反了,这个低级错误我见过不止一次,检查原理图和 PCB 走线。
6.2 写入成功但读出来是旧数据
这种情况通常是 WREN 没有生效。检查 WREN 和 WRITE 之间 CS 是否拉高过。另外,如果 WRITE 的数据长度超过了页边界,MRAM 不会像 Flash 那样回卷,而是继续往下一个地址写,所以不会出现回卷导致的数据覆盖。但如果地址计算错了,写到了别的区域,读的时候自然读不到。
6.3 高速 SPI 下数据偶发错误
12MHz 下如果偶发出错,先降速到 6MHz 试试。如果 6MHz 稳定,说明是信号完整性问题。检查 SCK 走线是否过长、是否有过孔、MISO 是否被干扰。可以在 SCK 和 MISO 上串一个 22 欧姆的电阻,减缓边沿,通常能解决问题。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读全 FF | CS 未拉低、SPI 模式错 | 示波器看时序 |
| 读全 00 | MISO 断开、从机未供电 | 万用表测电压 |
| 写入无效 | WREN 未生效 | 检查 CS 时序 |
| 偶发错误 | 信号完整性差 | 降速或加串阻 |
| 数据错位 | 地址计算错误 | 打印地址调试 |
| 上电丢数据 | 掉电时序问题 | 加电压监测 |
6.5 几个实用的调试技巧
第一,用 GPIO 翻转法测时序。在 CS 拉低和拉高的地方翻转一个空闲 GPIO,用示波器同时看这个 GPIO 和 SCK,能直观看到 CS 和时钟的关系。
第二,先写固定模式再读。比如写 0xAA、0x55 交替,读出来对比,能快速判断是数据线问题还是地址线问题。
第三,用 STM32 的 SPI 错误标志。HAL 库的 HAL_SPI_GetError 能返回 OVR、MODF 等错误,初始化的时候打印出来,能发现配置问题。
第四,MRAM 的 ID 读取。虽然 MR25H40CDF 没有标准的 JEDEC ID 命令,但可以通过读固定地址的已知数据来判断通信是否正常。我一般会在出厂前往地址 0x00000 写一个魔术字,上电的时候读出来对比。
7. 性能实测与优化建议
7.1 实际读写速度测试
在 12MHz SPI 时钟下,我实测的连续写入速度大约是 1.1MB/s,读取速度差不多。单字节写入(包含 WREN 和地址开销)大约需要 5 微秒。对于每秒记录一次、每次 32 字节的场景,写入耗时不到 200 微秒,CPU 占用率极低。
如果用 DMA 批量读取 4KB 数据,耗时大约 3.7 毫秒。这个速度对于数据导出完全够用。
7.2 降低功耗的几个手段
工业现场很多设备是电池供电或者用超级电容,功耗很关键。MRAM 的待机电流只有几微安,写入电流也只有几毫安,比 Flash 低不少。进一步降功耗可以这样做:不操作的时候把 CS 拉高,SPI 外设关掉;降低 SPI 时钟到 3MHz,对速度影响不大但功耗明显下降;用 DMA 传输,让 CPU 多睡觉。
7.3 从 Flash 迁移到 MRAM 的注意事项
如果你之前用的是 SPI Flash,迁移到 MRAM 的时候有几个地方要改。第一,去掉所有擦除相关的代码,MRAM 不需要擦除。第二,去掉状态轮询,MRAM 写完就能读。第三,地址计算要改,Flash 通常按扇区管理,MRAM 可以按字节随机访问。第四,写入前不需要判断是否为 0xFF,直接覆盖写就行。
我在实际迁移的时候,驱动层改动不大,主要是上层的数据管理逻辑简化了很多,代码量反而减少了。
8. 写在最后的一些个人体会
这套 MRAM 加 STM32F100ZE 的方案,我在电力监测和工业数据采集场景里用了三年多,累计出货几千台,现场反馈很稳定。最冷的地方在东北室外,最热的地方在南方工厂车间,温度范围覆盖了 MRAM 的工业级规格,没有出现过数据丢失的情况。
如果让我给正在选型的朋友一个建议,那就是:先算清楚数据写入频率和寿命要求,再决定用不用 MRAM。如果只是偶尔写一次配置参数,EEPROM 就够了;如果要频繁记录数据而且不能丢,MRAM 多花的钱绝对值得。另外,STM32F100ZE 这颗 MCU 虽然老,但胜在便宜稳定,配合 MRAM 做数据记录,整体方案的成本和可靠性平衡得很好。
最后分享一个小技巧:MRAM 的 CS 引脚上拉电阻,我试过 4.7K 和 10K,10K 在大多数场景下够用,但如果现场干扰特别大,换成 4.7K 更稳。这个细节看起来不起眼,但在电磁环境复杂的工厂里,能省掉很多莫名其妙的故障排查时间。