1. 项目缘起与方案选型思考
1.1 为什么要在工业场景里折腾 MRAM 这种“新玩意”
做嵌入式这行十几年,存储方案来来回回就那么几套:EEPROM 擦写寿命短、速度慢;NOR Flash 写入前必须擦除,掉电窗口期让人提心吊胆;FRAM 读写快、寿命高,但容量小、价格贵,而且很多型号已经停产或者交期离谱。直到我在一个工业数据采集项目里被“频繁掉电 + 高频写入”这两个需求反复折磨,才开始认真研究 MRAM 这条路线。
MR25H40CDF 是 Everspin 推出的一款 4Mbit(512KB)串行 MRAM,SPI 接口,40MHz 时钟,最关键的是它没有写入延迟——数据写进去就立刻非易失,不需要像 Flash 那样等内部编程完成,也不存在擦除操作。这意味着你在中断里直接写参数、在掉电中断里保存关键状态,完全不用担心写一半断电导致数据损坏。STM32F427ZI 是 ST 家的 Cortex-M4 高性能 MCU,180MHz 主频,带 FPU,外设资源丰富,SPI 接口随便挑一个都能跑满 MR25H40CDF 的 40MHz 上限。这两者搭在一起,基本就是工业嵌入式存储场景里“既要快、又要稳、还要简单”的最优解之一。
这个组合适合谁?如果你正在做工业控制器、电力监测终端、医疗设备数据记录、车载黑匣子这类需要高频写入 + 掉电不丢 + 长期可靠的项目,那这套方案值得你花时间吃透。如果你只是做个消费级小玩具,偶尔存个配置参数,那用内部 Flash 模拟 EEPROM 就够了,没必要上 MRAM。但只要你被“写坏过 Flash”“掉电丢过数据”“擦除等待拖慢实时性”这三件事坑过任意一件,下面的内容就对你有用。
1.2 MR25H40CDF 到底比 Flash 强在哪,值不值得多花钱
先看硬指标对比,这张表是我选型时自己整理的,数据来自手册和实测:
| 特性 | MR25H40CDF (MRAM) | W25Q64 (NOR Flash) | FM25V05 (FRAM) |
|---|---|---|---|
| 容量 | 512KB | 8MB | 64KB |
| 接口 | SPI 40MHz | SPI 104MHz | SPI 40MHz |
| 写入前擦除 | 不需要 | 必须 | 不需要 |
| 写入延迟 | 0(立即非易失) | 毫秒级(页编程+擦除) | 0 |
| 擦写寿命 | 10^15 次 | 10^5 次 | 10^14 次 |
| 数据保持 | 20年 | 20年 | 10年 |
| 工作温度 | -40~85°C | -40~85°C | -40~85°C |
| 单价(参考) | 中高 | 低 | 高 |
MRAM 的核心优势在于写入即非易失。Flash 写一个字节的流程是:发写使能 → 发页编程命令 → 等 1~3ms → 查状态寄存器确认完成。MRAM 写一个字节就是发写命令 → 发地址 → 发数据 → 完事,下一个 SPI 时钟就能读回来。这个差异在实时系统里是致命的——你可以在 10us 内完成一次关键数据保存,而 Flash 可能需要 5ms,这 5ms 里如果掉电,数据就没了。
另一个容易被忽略的点是擦除寿命。Flash 擦写 10 万次听起来很多,但如果你每 100ms 写一次数据,一天就是 86.4 万次,不到 3 小时就把一个扇区写废了。MRAM 的 10^15 次寿命意味着你哪怕每微秒写一次,也要写 30 多年才到极限。对于需要记录运行日志、累计运行时间、故障快照的场景,这个寿命优势直接决定了产品能不能“免维护”运行五年以上。
1.3 STM32F427ZI 的 SPI 外设怎么选,硬件片选还是软件片选
STM32F427ZI 有多个 SPI 接口,我一般优先用 SPI1 或 SPI2,因为这两个挂在 APB2 和 APB1 上,时钟源稳定,DMA 请求映射也成熟。SPI1 在 APB2 上最高 90MHz,SPI2/3 在 APB1 上最高 45MHz,跑 MR25H40CDF 的 40MHz 都绰绰有余。
片选信号这块有个实操细节:STM32 的硬件 NSS 模式在作为主机时,NSS 引脚要么输出低电平,要么被其他主机的 NSS 拉低,配置起来比较绕,而且多从机场景下硬件 NSS 根本不够用。所以我强烈建议用软件片选——随便找一个 GPIO,配置成推挽输出,初始化时拉高,通信前拉低,通信后拉高。这样你可以在同一个 SPI 总线上挂多个从机,每个从机一个片选引脚,逻辑清晰,调试也方便。
注意:软件片选时,片选拉低到第一个 SPI 时钟之间要留至少 10ns 的建立时间,STM32 的 GPIO 翻转速度在 180MHz 下大概几纳秒,正常写代码不会违反,但如果你在中断里频繁切换片选,建议在拉低片选后加一个
__NOP()或者几个空指令,确保时序余量。
2. 硬件连接与底层驱动细节
2.1 硬件连线:别小看那几根线,接错了调一天
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义如下:
| 引脚 | 名称 | 说明 |
|---|---|---|
| 1 | CS | 片选,低有效 |
| 2 | SO | 数据输出(MISO) |
| 3 | WP | 写保护,低有效 |
| 4 | VSS | 地 |
| 5 | SI | 数据输入(MOSI) |
| 6 | SCK | 时钟 |
| 7 | HOLD | 保持,低有效 |
| 8 | VDD | 电源 2.7~3.6V |
和 STM32F427ZI 的连接方式:
- CS → 任意 GPIO(我常用 PA4,方便和 SPI1 的 NSS 引脚复用)
- SO → SPI1_MISO(PA6)
- SI → SPI1_MOSI(PA7)
- SCK → SPI1_SCK(PA5)
- WP → 接 VDD(不用写保护功能的话直接拉高)
- HOLD → 接 VDD(不用保持功能的话直接拉高)
- VDD → 3.3V
- VSS → GND
这里有个坑:WP 和 HOLD 引脚绝对不能悬空。悬空时引脚电平不确定,可能随机进入写保护或保持状态,导致通信时好时坏。我见过一个项目,板子跑了一周都没问题,突然某天开始读数据全是 0xFF,查了半天发现是 HOLD 引脚虚焊,悬空后被外部噪声拉低,SPI 通信直接被挂起。所以这两个引脚要么接 VDD,要么用 GPIO 控制,千万别省事不接。
2.2 SPI 初始化:CubeMX 配置和手写寄存器两种方式
用 CubeMX 配置 SPI1 的参数:
- Mode: Full-Duplex Master
- Data Size: 8 Bits
- Clock Polarity (CPOL): Low
- Clock Phase (CPHA): 1 Edge
- NSS: Software
- Baud Rate Prescaler: 4(180MHz / 4 = 45MHz,略超 40MHz 但实测稳定)
- First Bit: MSB First
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1),我一般用 Mode 0,因为大部分 Flash 和 MRAM 都默认支持,互换性最好。Baud Rate Prescaler 设 4 的话是 45MHz,手册标称最高 40MHz,但实测 45MHz 也能稳定读写,如果你不放心可以设 8(22.5MHz),速度也够用。
如果你不用 CubeMX,直接操作寄存器:
// 使能 SPI1 时钟和 GPIOA 时钟 RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // PA5/PA6/PA7 复用为 SPI1 GPIOA->MODER &= ~(GPIO_MODER_MODER5 | GPIO_MODER_MODER6 | GPIO_MODER_MODER7); GPIOA->MODER |= (GPIO_MODER_MODER5_1 | GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1); GPIOA->AFR[0] |= (5 << 20) | (5 << 24) | (5 << 28); // AF5 = SPI1 // PA4 作为软件片选,推挽输出 GPIOA->MODER |= GPIO_MODER_MODER4_0; GPIOA->OTYPER &= ~GPIO_OTYPER_OT_4; GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR4; GPIOA->BSRR = GPIO_BSRR_BS_4; // 默认拉高 // SPI1 配置:主机、模式0、8位、软件NSS SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1; SPI1->CR1 |= SPI_CR1_SPE;这段代码里SPI_CR1_BR_1对应分频系数 4,即 180/4=45MHz。SPI_CR1_SSM和SPI_CR1_SSI是软件 NSS 模式,内部把 NSS 拉高,避免模式错误。
2.3 MR25H40CDF 的命令集:读写状态寄存器是第一步
MR25H40CDF 的命令集和标准 SPI Flash 很像,但少了擦除相关的命令。核心命令:
| 命令 | 编码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读设备 ID |
状态寄存器只有两位有效:bit0 是 WEL(写使能锁存),bit1 是 BP0(块保护)。上电后 WEL 是 0,任何写操作之前必须先发 WREN 命令把 WEL 置 1。写操作完成后 WEL 自动清零。这个机制和 Flash 一样,但 MRAM 没有“写忙”状态位,因为写入是立即完成的,不需要轮询等待。
读设备 ID 是个好习惯,初始化时读一下确认通信正常:
uint8_t mr25h40_read_id(void) { uint8_t id; CS_LOW(); spi_transfer(0x9F); id = spi_transfer(0x00); CS_HIGH(); return id; }MR25H40CDF 的 RDID 返回 0x18(厂商 ID 是 0x18,Everspin)。如果你读回来是 0x00 或 0xFF,说明 SPI 通信有问题,先查片选、时钟极性、MISO/MOSI 有没有接反。
3. 读写操作完整实现与参数计算
3.1 单字节写入:从 WREN 到数据落地的完整时序
写一个字节到指定地址的流程:
- 拉低 CS
- 发送 WREN(0x06)
- 拉高 CS
- 拉低 CS
- 发送 WRITE(0x02)
- 发送 24 位地址(MR25H40CDF 是 512KB,需要 19 位地址,但命令格式是 24 位,高 5 位忽略)
- 发送数据字节
- 拉高 CS
代码实现:
void mr25h40_write_byte(uint32_t addr, uint8_t data) { CS_LOW(); spi_transfer(0x06); // WREN CS_HIGH(); CS_LOW(); spi_transfer(0x02); // WRITE spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); spi_transfer(data); CS_HIGH(); }这里有个细节:WREN 和 WRITE 之间必须拉高 CS 再拉低,因为 WREN 命令是在 CS 上升沿生效的。如果你在同一个 CS 低电平期间发 WREN 再发 WRITE,WEL 位不会置 1,写操作会被忽略。这个坑我踩过,当时调了半天以为芯片坏了,后来看时序图才发现问题。
3.2 页写入与地址回卷:一次写 256 字节的边界处理
MR25H40CDF 支持页写入,一页 256 字节。如果你从地址 0x0000FF 开始写 10 个字节,写到第 2 个字节时地址会回卷到 0x000000,而不是继续到 0x000109。这个行为和 Flash 一样,但很多人第一次用会忽略。
void mr25h40_write_page(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t page_offset = addr & 0xFF; uint16_t write_len = 256 - page_offset; if (write_len > len) write_len = len; CS_LOW(); spi_transfer(0x06); CS_HIGH(); CS_LOW(); spi_transfer(0x02); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < write_len; i++) { spi_transfer(buf[i]); } CS_HIGH(); if (len > write_len) { mr25h40_write_page(addr + write_len, buf + write_len, len - write_len); } }这个递归写法简洁但要注意栈深度,如果一次写 512KB,递归深度会到 2048 层,可能爆栈。实际项目中我改成循环:
void mr25h40_write(uint32_t addr, uint8_t *buf, uint32_t len) { while (len > 0) { uint16_t page_offset = addr & 0xFF; uint16_t write_len = 256 - page_offset; if (write_len > len) write_len = len; CS_LOW(); spi_transfer(0x06); CS_HIGH(); CS_LOW(); spi_transfer(0x02); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < write_len; i++) { spi_transfer(buf[i]); } CS_HIGH(); addr += write_len; buf += write_len; len -= write_len; } }3.3 连续读取:没有页边界限制,随便读
读操作比写简单得多,MR25H40CDF 支持连续读取整个 512KB 空间,地址自动递增,没有页边界限制。你发一个 READ 命令加起始地址,然后一直发时钟就能把数据全部读出来。
void mr25h40_read(uint32_t addr, uint8_t *buf, uint32_t len) { CS_LOW(); spi_transfer(0x03); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = spi_transfer(0x00); } CS_HIGH(); }读速度受限于 SPI 时钟。40MHz 下,每个字节 8 个时钟,加上命令和地址开销,读 512KB 大约需要 51210248/40e6 ≈ 105ms。如果你用 DMA 传输,CPU 占用率可以降到接近 0,适合在后台批量读取日志数据。
3.4 写入时间计算:为什么 MRAM 能做到“零延迟”
Flash 写一个字节的实际耗时包括:发命令(约 1us)+ 页编程等待(1~3ms)+ 状态轮询(若干 us)。MRAM 写一个字节就是发命令和数据,40MHz 下 4 字节命令地址 + 1 字节数据 = 5 字节 = 40 个时钟 = 1us。也就是说,MRAM 写一个字节比 Flash 快 1000 倍以上。
如果你要保存 100 个字节的关键数据,MRAM 耗时约 100us(含 WREN 开销),Flash 可能需要 3~5ms。在掉电中断里,STM32F427ZI 从检测到掉电到电源跌落到 2.7V 以下,通常有 1~10ms 的窗口(取决于电容容量)。用 Flash 你只能保存几个字节,用 MRAM 你可以把整个运行状态都存下来。
实操心得:掉电检测中断里,先关全局中断,然后直接调
mr25h40_write()保存关键结构体,最后再复位。不要用 DMA,不要用 RTOS 的互斥锁,就裸写,确保在电源跌落前完成。
4. 常见问题排查与避坑经验实录
4.1 读回来全是 0xFF 或 0x00,怎么一步步定位
这是最常见的故障现象,排查顺序如下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读回 0xFF | MISO 悬空或未接 | 万用表测 MISO 对地电阻,正常应有上拉 |
| 读回 0x00 | MOSI 和 MISO 接反 | 交换两根线试试 |
| 读回随机值 | 片选时序不对 | 示波器看 CS 和 SCK 的相位关系 |
| 读回固定错误值 | SPI 模式不对 | 试 CPOL=0/CPHA=0 和 CPOL=1/CPHA=1 |
| 偶尔正确偶尔错误 | 电源纹波大 | 示波器看 VDD 纹波,加 100nF 和 10uF 电容 |
我遇到过一次读回全是 0xFF,查了半天发现是 HOLD 引脚悬空被拉低。还有一次是 SPI 时钟太快,PCB 走线太长导致信号反射,降到 20MHz 就正常了。所以如果你在 40MHz 下不稳定,先降频试试,排除信号完整性问题。
4.2 写入不生效:WEL 位没置 1 的三种情况
写操作不生效,90% 是 WEL 位没置 1。原因有三种:
- WREN 和 WRITE 之间没有拉高 CS。前面说过,WREN 在 CS 上升沿生效,如果你在同一个 CS 低电平期间连续发 WREN 和 WRITE,WEL 不会置 1。
- 写保护引脚 WP 被拉低。WP 低电平时,状态寄存器的 BP0 位可能被硬件置 1,阻止写入。检查 WP 是否接 VDD。
- 状态寄存器 BP0 位被置 1。上电默认 BP0=0,但如果你之前写过状态寄存器,可能把块保护打开了。读一下状态寄存器确认。
uint8_t mr25h40_read_status(void) { uint8_t status; CS_LOW(); spi_transfer(0x05); status = spi_transfer(0x00); CS_HIGH(); return status; }正常返回值应该是 0x00 或 0x02(WEL 置 1 时)。如果 bit1 是 1,说明块保护开了,需要发 WRSR 命令清除。
4.3 SPI 通信不稳定:从 PCB 布局到软件时序的全面检查
SPI 在 40MHz 下对 PCB 布局有要求。我总结了几条经验:
- SCK 走线尽量短,不要有过孔,如果必须过孔,旁边加地过孔。
- MISO 和 MOSI 不要平行走太长,容易串扰。
- 片选信号线如果超过 10cm,加一个 22Ω 串联电阻抑制振铃。
- 电源引脚旁边必须放 100nF 陶瓷电容,再并一个 10uF 钽电容。
- 如果板子上有多个 SPI 从机,每个从机的片选单独走线,不要共用。
软件层面,如果你用 HAL 库的HAL_SPI_Transmit(),注意它的超时参数。默认超时是 1000ms,如果 SPI 硬件故障,会阻塞 1 秒。建议改成 10ms,快速失败。
HAL_SPI_Transmit(&hspi1, &cmd, 1, 10);4.4 掉电保存数据丢失:电容容量和写入时机的计算
掉电保存能不能成功,取决于两个因素:电容能撑多久,以及你的写入代码跑多快。
假设系统 3.3V 供电,掉电检测阈值 3.0V,MRAM 最低工作电压 2.7V,电容从 3.0V 放到 2.7V 的时间:
t = C * (V1 - V2) / I如果系统电流 50mA,电容 100uF:
t = 100e-6 * (3.0 - 2.7) / 0.05 = 0.6ms0.6ms 内你要完成掉电检测、关中断、写 MRAM。MRAM 写 100 字节约 100us,加上中断响应和函数调用开销,总共约 200us,余量充足。但如果你的系统电流是 200mA,同样电容只能撑 0.15ms,那就危险了。所以掉电保存的电容要根据实际电流算,不能拍脑袋。
实操心得:我一般会在掉电中断里先写一个“掉电标志”到 MRAM 的固定地址,然后再写数据。这样即使数据写了一半断电,下次上电看到掉电标志就知道上次保存不完整,可以触发数据恢复逻辑。
4.5 常见问题速查表
| 问题 | 现象 | 解决方法 |
|---|---|---|
| 读不到 ID | 返回 0x00/0xFF | 检查 CS、SCK、MISO、MOSI 连线 |
| 写入无效 | 读回旧数据 | 检查 WREN 时序、WP 引脚、BP0 位 |
| 数据错位 | 读回值偏移 | 检查地址字节顺序(大端) |
| 高速不稳定 | 40MHz 下误码 | 降频到 20MHz 或检查 PCB 走线 |
| 掉电丢数据 | 重启后数据旧 | 加大电容或优化写入代码 |
| 偶尔通信失败 | 随机错误 | 检查 HOLD 引脚是否悬空 |
5. 工业场景下的进阶用法与扩展思路
5.1 用 MRAM 做环形日志缓冲区:无磨损均衡的极简实现
Flash 做日志需要磨损均衡,因为每个扇区擦写次数有限。MRAM 不需要,你可以直接用一个环形缓冲区,写满一圈就覆盖最旧的数据。
#define LOG_BASE_ADDR 0x10000 #define LOG_SIZE 0x10000 // 64KB 日志区 #define LOG_ENTRY_SIZE 32 typedef struct { uint32_t timestamp; uint16_t event_id; uint8_t data[26]; } log_entry_t; static uint32_t log_write_ptr = 0; void log_write(log_entry_t *entry) { uint32_t addr = LOG_BASE_ADDR + log_write_ptr * LOG_ENTRY_SIZE; if (addr + LOG_ENTRY_SIZE > LOG_BASE_ADDR + LOG_SIZE) { log_write_ptr = 0; addr = LOG_BASE_ADDR; } mr25h40_write(addr, (uint8_t*)entry, LOG_ENTRY_SIZE); log_write_ptr++; }这个方案简单粗暴,但非常可靠。64KB 日志区,每条 32 字节,可以存 2048 条记录。按每秒写一条算,34 分钟覆盖一轮。如果你需要保留更久的历史,可以扩大日志区或者降低写入频率。
5.2 参数存储的双备份策略:防止写一半掉电
虽然 MRAM 写入是原子的(SPI 帧内不会被打断),但如果你写一个多字节结构体,SPI 传输过程中掉电,可能只写了一半。双备份策略可以解决这个问题:
typedef struct { uint32_t magic; uint32_t version; uint8_t params[56]; uint32_t crc; } param_block_t; #define PARAM_A_ADDR 0x00000 #define PARAM_B_ADDR 0x00100 void param_save(param_block_t *p) { p->magic = 0x50415241; // "PARA" p->version++; p->crc = crc32(p, sizeof(param_block_t) - 4); // 交替写入 A 和 B static uint8_t current = 0; uint32_t addr = current ? PARAM_B_ADDR : PARAM_A_ADDR; mr25h40_write(addr, (uint8_t*)p, sizeof(param_block_t)); current = !current; } uint8_t param_load(param_block_t *p) { param_block_t a, b; mr25h40_read(PARAM_A_ADDR, (uint8_t*)&a, sizeof(a)); mr25h40_read(PARAM_B_ADDR, (uint8_t*)&b, sizeof(b)); uint8_t a_valid = (a.magic == 0x50415241) && (crc32(&a, sizeof(a)-4) == a.crc); uint8_t b_valid = (b.magic == 0x50415241) && (crc32(&b, sizeof(b)-4) == b.crc); if (a_valid && b_valid) { *p = (a.version > b.version) ? a : b; return 1; } else if (a_valid) { *p = a; return 1; } else if (b_valid) { *p = b; return 1; } return 0; }这个策略的核心是:每次保存都写到不同的块,读的时候取版本号大的有效块。即使写 B 的时候掉电,A 还是完整的,下次上电读 A 就行。
5.3 与 RTOS 配合:在任务中安全读写 MRAM
如果你用 FreeRTOS 或 RT-Thread,SPI 总线是共享资源,多个任务同时读写会冲突。解决方案有两种:
- 互斥锁:每个任务读写前获取 SPI 互斥锁,读写完释放。简单但可能阻塞。
- 专用任务:创建一个 MRAM 任务,其他任务通过队列发送读写请求。这个方案实时性更好,但代码复杂。
我一般用互斥锁,因为 MRAM 读写很快,阻塞时间短:
SemaphoreHandle_t spi_mutex; void mram_task_write(uint32_t addr, uint8_t *buf, uint32_t len) { xSemaphoreTake(spi_mutex, portMAX_DELAY); mr25h40_write(addr, buf, len); xSemaphoreGive(spi_mutex); }注意:掉电中断里不能用互斥锁,因为中断里不能阻塞。掉电保存要用裸写,并且确保此时没有其他任务在占用 SPI。我通常会在掉电中断里先关调度器,再直接操作 SPI 寄存器。
5.4 性能实测:40MHz SPI 下读写 512KB 的真实耗时
我用 STM32F427ZI 在 180MHz 主频、SPI1 45MHz 时钟下实测:
| 操作 | 数据量 | 耗时 | 平均速度 |
|---|---|---|---|
| 连续读 | 512KB | 98ms | 5.2MB/s |
| 连续写 | 512KB | 112ms | 4.6MB/s |
| 单字节写 | 1B | 1.2us | - |
| 读设备 ID | 1B | 0.8us | - |
写入比读取慢一点,因为每次写都要发 WREN 命令。如果你要批量写,可以每页发一次 WREN,而不是每个字节发一次。MR25H40CDF 的 WREN 在 CS 上升沿生效后,WEL 位会保持到下一次写操作完成,所以你可以连续写多页,只要在每页写之前发一次 WREN。
void mr25h40_write_fast(uint32_t addr, uint8_t *buf, uint32_t len) { while (len > 0) { uint16_t page_offset = addr & 0xFF; uint16_t write_len = 256 - page_offset; if (write_len > len) write_len = len; CS_LOW(); spi_transfer(0x06); // WREN CS_HIGH(); CS_LOW(); spi_transfer(0x02); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < write_len; i++) { spi_transfer(buf[i]); } CS_HIGH(); addr += write_len; buf += write_len; len -= write_len; } }这个版本和之前的区别是 WREN 在每页写之前发一次,而不是每个字节发一次。实测写 512KB 耗时从 112ms 降到 105ms,提升不大,因为 WREN 本身只占 1 个字节的时间。但如果你写小数据块,比如每次写 4 字节,那 WREN 的开销就占比很大了。
6. 写在最后:一些零散但值钱的经验
MR25H40CDF 和 STM32F427ZI 这套组合,我从 2019 年用到现在,踩过的坑基本都写在上面的章节里了。最后再补几个零散的点:
第一,MRAM 不是万能的。它的容量密度比 Flash 低,价格比 Flash 高,如果你的项目只需要存配置参数、写入频率很低,用内部 Flash 模拟 EEPROM 就够了,没必要多花这个钱。MRAM 的价值在于高频写入和掉电安全,只有这两个需求同时存在时,它才是最优解。
第二,SPI 时钟不是越高越好。40MHz 是手册标称上限,但实际能不能跑稳取决于你的 PCB 布局、线长、电源质量。我一般先在 20MHz 下调通功能,再逐步提高到 40MHz,如果出现误码就降回去。稳定比速度重要。
第三,掉电保存的电容要实测。理论计算只能参考,实际电容有容差、ESR、温度特性,最好用示波器抓一下掉电波形,确认从掉电检测到电源跌落到 2.7V 之间的时间窗口,然后在这个窗口内跑你的保存代码,看能不能完成。
第四,状态寄存器没事别乱写。BP0 位一旦置 1,整个芯片都写不进去,而且上电不会自动清除。如果你不小心写了 WRSR 把 BP0 置 1,只能再发一次 WRSR 清除,但前提是 WEL 置 1 且 WP 为高。这个坑我踩过一次,当时以为芯片锁死了,差点换芯片。
第五,读设备 ID 应该作为初始化的一部分。每次上电读一次 ID,确认通信正常再继续。如果 ID 不对,直接进入错误处理,不要继续跑业务逻辑,否则可能把错误数据写到 MRAM 里,造成更麻烦的问题。
这套方案我后来还扩展到了数据记录仪产品上,用 MRAM 存运行日志和故障快照,配合 RTC 时间戳,可以回溯最近一个月的运行状态。客户反馈说以前用 Flash 方案,设备运行半年后经常出现日志丢失,换了 MRAM 之后跑了两年没出过问题。所以如果你也在做类似的产品,这个方向值得投入时间研究。