以前做配电终端类项目,最头疼的不是 RTOS 怎么调度,而是用户数据到底往哪儿存。NOR Flash 掉电写容易丢数据,EEPROM 容量又太小,铁电 FRAM 在高温和写次数极限上总让人不放心。后来换到 Everspin 的 MRAM,芯片是 MR25H40CDF,配合手里现成的 STM32F407ZG,一套组合下来,参数存储、事件记录、掉电保存这块基本再没返修过。
这篇文章就把这套方案的完整思路写出来:为什么选 MRAM 而不是 Flash/EEPROM,硬件怎么跟 F407ZG 对接,SPI 驱动和读写策略怎么实现,还踩过哪些坑。项目背景是工业数据记录器,用 MR25H40CDF 存运行参数和掉电标志位,MCU 是 STM32F407ZG,开发环境基于 HAL 库。
1. 为什么工业数据存储优先考虑 MRAM
1.1 Flash 和 EEPROM 到底不够用在哪里
工业设备里最常见的存储方案是 SPI NOR Flash 和 I2C/SPI EEPROM,这两类芯片用了很多年,但在特定场景下问题非常明显。
先看写寿命。普通 NOR Flash 的擦写次数标称一般是 10 万次,看着不少,但如果设备每秒记录一次运行数据,10 万次只够用 28 小时不到。工业现场的数据记录往往是分钟级甚至秒级连续写,Flash 根本撑不住。EEPROM 的寿命通常在 100 万次左右,比 Flash 好一点,但容量做不大,几 KByte 到几 Mbit 就到头了,而且要写一个字节往往要整页擦写缓冲,逻辑上绕得很。
再看写速度和写方式。NOR Flash 写入前必须擦除,擦除粒度是按扇区,通常 4KB 起步。也就是说哪怕你只改一个字节,也得先把整个扇区读到 RAM、修改、擦除扇区、再写回去。这个过程一旦在擦除后、写入前掉电,整个扇区的数据就全没了。EEPROM 没有擦除一说,但页写缓冲小,写大块数据要一条条指令敲,速度也上不去。
工业掉电场景最苛刻的地方在于:设备可能在写入过程中突然断电。Flash 为了节省时间,很多驱动是先擦后写,掉电窗口非常大。这一点做继电保护、轨交、能源采集的朋友应该深有体会,数据丢一次可能意味着一次错误动作。
1.2 MRAM 的核心优势来自磁存储而非电荷存储
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,原理跟 Flash 完全不是一回事。它存储数据靠的是磁性隧道结(MTJ)的自由层磁化方向,而不是半导体里的浮栅电荷。磁化方向不会像电荷那样泄漏,所以它天生是非易失的,理论上数据保持能力可以到 10 年以上。
关键是写机制。没有擦除步骤,不需要块擦除、扇区擦除,任何时刻任何地址都可以直接写入。写操作本身是物理翻转磁化方向,速度极快,标称写入周期在几十纳秒级别。实际用 SPI 总线操作时,瓶颈几乎都在 MCU 的 SPI 时钟频率上,而不是芯片内部写入时间。
写寿命上,MRAM 标称可达到 10 亿次以上写周期,甚至很多型号直接写 unlimited。这对秒级连续记录简直就是量身定做的。而且写一个字节不会影响相邻字节,不破坏其他数据,这在结构体参数存储里太有用了。整片数据可以像 SRAM 一样随机访问,读改写不需要额外擦写缓冲,代码写起来简单很多。
1.3 和 FRAM 相比,MRAM 的温度和容量表现更稳
很多工程师第一反应可能是用 FRAM,比如富士通和赛普拉斯的铁电存储器。FRAM 在写速度和寿命上也很好,但有两个短板:一是容量做不大,常见的也就 256KB 到 4Mbit,再往上很难,大容量型号贵得离谱;二是温度特性,FRAM 在 85°C 以上掉电保持特性会有衰减,一些工业现场的高温环境就顶不住。
MRAM 的工业级型号工作温度范围可以做到 -40°C 到 +125°C,数据保持不受高温影响。MR25H40CDF 本身就是工业级器件,封装是 8 引脚 DFN,焊接和贴装都方便。在关键参数记录场景,它比 FRAM 抗造,比 Flash 省心。
选型的时候还有个细节:MR25H40CDF 是 3.3V 供电,跟 STM32F407ZG 的 VDDA/VDD 完全匹配,不需要电平转换。SPI 接口最高可以跑到 40MHz 左右,F407 的 SPI 外设最高 42MHz,两边其实能直接干满。
2. 硬件设计:MR25H40CDF 与 STM32F407ZG 的接线要点
2.1 硬件连接与引脚规划
STM32F407ZG 的 SPI 外设很多,SPI1/SPI2/SPI3/SPI4/SPI5/SPI6 都有,SPI1 挂在 APB2 总线上,时钟 84MHz,分频下来能比较接近 MRAM 的极限速度,所以我选了 SPI1。
引脚规划如下,F407ZG 是 LQFP144 封装,引脚资源非常富裕:
| 信号 | STM32F407ZG 引脚 | 复用功能 | MR25H40CDF 引脚 |
|---|---|---|---|
| SPI1_SCK | PB3 | AF5 | SCK |
| SPI1_MISO | PB4 | AF5 | SO |
| SPI1_MOSI | PB5 | AF5 | SI |
| SPI1_NSS | PA4(软件控制) | GPIO 输出 | CS# |
| WP# 控制 | PC4(GPIO 输出) | - | WP# |
| HOLD# 控制 | PC5(GPIO 输出) | - | HOLD# |
这里有一个非常重要的经验:片选不要用 STM32 的硬件 NSS,用普通 GPIO 手动控制。虽然 STM32F407 的 SPI 支持硬件 NSS,但 NSS 引脚的自动控制经常会带来时序上的意外行为,比如字节间 CS 释放、多机通信冲突等。MRAM 的数据传输要求 CS 在整个命令序列期间保持低电平,用 GPIO 可以精确控制字节间隙的电平状态,排查问题也直观。
WP#(Write Protect)引脚接到 PC5?不对,上面表格里写反了,应该是 WP# 接 PC4、HOLD# 接 PC5,或者反过来都行,这里需要更正一下。我用的是 WP# 接 PC4,HOLD# 接 PC5,两个引脚都通过 10K 电阻上拉到 3.3V。
为什么要控制 WP# 而不是直接接死上拉?MR25H40CDF 的 WP# 配合状态寄存器里的 WPEN 位一起用。当 WPEN=1 且 WP#=0 时,状态寄存器和整个存储区都变成只读。这个特性可以在系统初始化完成后锁死关键数据区,防止程序跑飞误写。但如果你上电以后希望自己还能改数据,WP# 必须保持高电平。PC4 接出来以后,从代码层面想什么时候锁就什么时候锁,主动权在自己手里。
HOLD# 引脚的作用是暂停 SPI 通信而不复位片选状态。正常工作时 HOLD# 必须为高电平,接地会导致时钟暂停、数据线上状态丢失。我们用 GPIO 控制它主要是为了和 WP# 做同样的防误操作处理,平时置 1,不使用时不会出问题。
2.2 电源、去耦与 PCB 布局经验
MR25H40CDF 的工作电压范围是 2.7V~3.6V,典型值 3.3V。STM32F407ZG 的 VDD 也是 3.3V,可以直接共电源。但有几个细节不能省:
芯片旁边放一个 100nF 的陶瓷电容就近去耦,这是最基本的。如果 PCB 空间允许,再加一个 4.7uF 的钽电容做低频滤波。MRAM 写入瞬间电流变化不大,不像 Flash 编程那样有大电流脉冲,所以电源要求其实比 Flash 低,但数字电路的基本纪律还是要守。
SPI 信号线最好走等长,尤其 SCK 和 MOSI、MISO 三根线。F407 的 GPIO 速度配置选 High Speed,这样边沿不够陡峭也不至于因为压摆率不够导致高速通信失败。如果板子上走线长超过 5cm,建议在 SCK 上串一个 22Ω 左右的电阻,能明显改善振铃。
DFN 封装的散热焊盘:MR25H40CDF 是 8 引脚 DFN,中心有裸露焊盘,数据手册里写的是可连接到 GND 或者悬空,实际焊接时最好把散热焊盘接到 GND,贴片稳定性和散热都会好一些。
2.3 硬件上电时序问题
工业现场最怕的是上电瞬间误写。MR25H40CDF 有一个特征:上电过程中如果 CS# 被拉低,芯片内部状态可能不确定,最坏情况下会误写入数据。
STM32F407ZG 的 GPIO 在上电瞬间默认是浮空输入态,也就是说 PA4(CS#)在 MCU 初始化之前是悬空的。如果附近走线耦合噪声,CS# 可能被拉低,MRAM 检测到片选有效后就开始采指令。这个风险虽然概率低,但后果可能是启动参数被随机数据覆盖。
我处理的办法是:硬件上在 CS# 引脚加一个 10K 下拉电阻到 GND。GPIO 浮空的时候,CS# 被电阻拉低?这不行,CS# 至少要保证高电平才能不上当。反过来想,应该加 10K 上拉到 3.3V,保证 MCU 接管之前 CS# 是高电平,MRAM 不会误响应。这个经验是从一次现场故障反推出来的,后来所有 SPI 从机片选都统一加上拉,再没犯过病。
同理 WP# 和 HOLD# 也都加上了拉,保证上电即处于允许写、允许正常通信的状态。
3. SPI 时序与驱动代码实现
3.1 MR25H40CDF 的指令集与关键时序
MR25H40CDF 的指令集和普通 SPI NOR Flash(比如 W25Q64)非常相似,上手很快。最常用的指令如下:
| 指令名 | 操作码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能,必须存在 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据,支持连续读 |
| WRITE | 0x02 | 写数据,最大 512 字节页写 |
写入数据前必须先发 WREN,把状态寄存器里的 WEL(Write Enable Latch)位置 1。写入完成后 WEL 自动清零。这个机制和 Flash 是一样的,不能省略,否则写入指令会被忽略。
和 Flash 最大的区别有三个:
第一,没有擦除指令。Flash 的 Sector Erase(0x20)、Block Erase(0xD8)、Chip Erase(0xC7)在这里全部不需要。MRAM 任何地址在任何时刻都可以直接覆盖写入。
第二,没有等待轮询。Flash 写完一页要等 tPP 时间,一般几百微秒到几毫秒,期间需要反复读状态寄存器的 BUSY 位。MRAM 写一个字节只是把数据锁存到内部,紧接着就可以进行下一次操作,连状态寄存器都不用查。代码流程简化不止一个量级。
第三,写数据时地址可以随意对齐。Flash 要求页内地址不能跨越页边界,否则数据会回卷写到页开头,这是新手最常踩的坑。MRAM 完全没有这个限制,连续写随时可以结束,不会发生回卷写。
实际用时序还要注意一点:读操作不需要 WREN,READ 命令发出后直接跟 3 字节地址,然后从 SO 上把数据移出来即可。MRAM 支持连续读,只要不拉高 CS#,地址会自动递增,非常适合批量读取日志。
3.2 基于 HAL 库的 SPI 初始化和底层驱动
MR25H40CDF 支持 SPI Mode 0(CPOL=0、CPHA=0)和 Mode 3(CPOL=1、CPHA=1),默认用 Mode 0。数据手册建议上电后用 Read ID 指令读出 JEDEC ID 来验证通信,MR25H40CDF 的指令 0x9F 返回的 ID 是 0x9F 0x52 0x25(也有资料说是 0x9F 0x52 0x25,具体以实测为准)。
先初始化 SPI1:
void MX_SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 84MHz / 4 = 21MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); } }GPIO 配置就不完整贴了,把 PB3/PB4/PB5 配成 AF5,PA4/PC4/PC5 配成推挽输出即可。PA4 初始化时直接置 1。
底层收发函数封装成三个接口:
/* 片选控制 */ static inline void mram_cs_low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } static inline void mram_cs_high(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } /* SPI 单字节收发 */ static uint8_t spi_xfer_byte(uint8_t tx) { uint8_t rx = 0; HAL_SPI_TransmitReceive(&hspi1, &tx, &rx, 1, HAL_MAX_DELAY); return rx; } /* 发送指令+地址,然后传输数据 */ static void mram_write_command(uint8_t cmd, uint32_t addr, uint8_t *buf, uint32_t len) { mram_cs_low(); spi_xfer_byte(cmd); spi_xfer_byte((uint8_t)(addr >> 16) & 0xFF); spi_xfer_byte((uint8_t)(addr >> 8) & 0xFF); spi_xfer_byte((uint8_t)(addr & 0xFF)); while (len--) { spi_xfer_byte(*buf++); } mram_cs_high(); }这里有个细节:地址是 3 字节,但 MR25H40CDF 只有 4Mbit,也就是 512KB,实际地址范围 0x00000~0x7FFFF。高字节地址超出 8 位之后芯片会忽略多余位?实际上 MRAM 的地址线是 A[18:0],也就是 19 位。发送地址时除了低 19 位,其余位必须保持为 0,否则可能产生非预期行为。所以上面代码里(addr >> 16) & 0xFF实际上只会用到低 3 位,最好再& 0x07一下,稳妥。
3.3 数据写入与读取的完整实现
先读状态寄存器,确认 WEL 位,这是调试期排查问题的关键路径。
#define MRAM_STATUS_WEL 0x02 #define MRAM_STATUS_WPEN 0x80 uint8_t mram_read_status(void) { uint8_t status = 0; mram_cs_low(); spi_xfer_byte(0x05); status = spi_xfer_byte(0x00); mram_cs_high(); return status; } void mram_write_enable(void) { mram_cs_low(); spi_xfer_byte(0x06); mram_cs_high(); } /* 写一页数据,len 不超过 512 字节 */ void mram_write_page(uint32_t addr, uint8_t *buf, uint16_t len) { mram_write_enable(); mram_cs_low(); spi_xfer_byte(0x02); spi_xfer_byte((uint8_t)((addr >> 16) & 0x07)); spi_xfer_byte((uint8_t)((addr >> 8) & 0xFF)); spi_xfer_byte((uint8_t)(addr & 0xFF)); for (uint16_t i = 0; i < len; i++) { spi_xfer_byte(buf[i]); } mram_cs_high(); } /* 读任意长度数据 */ void mram_read_data(uint32_t addr, uint8_t *buf, uint32_t len) { mram_cs_low(); spi_xfer_byte(0x03); spi_xfer_byte((uint8_t)((addr >> 16) & 0x07)); spi_xfer_byte((uint8_t)((addr >> 8) & 0xFF)); spi_xfer_byte((uint8_t)(addr & 0xFF)); for (uint32_t i = 0; i < len; i++) { buf[i] = spi_xfer_byte(0x00); } mram_cs_high(); }读 ID 的代码可以作为上电自检用:
void mram_read_id(uint8_t *id, uint8_t len) { mram_cs_low(); spi_xfer_byte(0x9F); for (uint8_t i = 0; i < len; i++) { id[i] = spi_xfer_byte(0x00); } mram_cs_high(); }在我实测的板子上,读出来的 ID 是0x9F 0x52 0x25,如果硬件连接正确或驱动时序有问题,读出来大概率是0xFF 0xFF 0xFF。这一步是排查问题的第一步。
3.4 连续写在 MRAM 上的处理方式
前面说过 MRAM 没有页边界回卷的问题,但实际开发时我仍然按页组织数据结构,纯粹是从编程逻辑角度考虑的。比如参数区定义成固定大小结构体,事件日志区按固定 32 字节一条记录。
MRAM 写 512 字节不加等待,但有一个物理层面的细节:虽然芯片写数据快,但 SPI 总线上传数据本身需要时间。21MHz 时钟下,512 字节连续传输大约需要512 * 8 / 21MHz ≈ 195us,这个时间在工业应用里完全不是问题。
更需要注意的是:写指令的 SPI 传输期间,CS# 必须全程保持低电平。一旦 CS# 中途拉高,芯片会认为当前写命令被终止,已接收的字节会不会写入是不确定的。千万不要在每传一个字节都操作一次 CS,除非你真的想让命令终止。
4. 数据完整性设计:从写保护到掉电保存
4.1 状态寄存器配置与硬件写保护配合
MR25H40CDF 的状态寄存器里有几个关键位:WEL(bit1)表示写使能锁存;WPEN(bit7)是写保护使能位。WPEN 与 WP# 引脚组合决定写保护是否生效。
简单说:
| WPEN | WP#引脚 | 效果 |
|---|---|---|
| 0 | X | 允许写 |
| 1 | 0 | 整个芯片只读 |
| 1 | 1 | 允许写 |
我在代码里做了一个函数用于锁定存储区:
void mram_lock_write(void) { uint8_t status = mram_read_status(); status |= 0x80; // 置 WPEN=1 mram_write_enable(); mram_cs_low(); spi_xfer_byte(0x01); // WRSR spi_xfer_byte(status); mram_cs_high(); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4, GPIO_PIN_RESET); // WP#拉低 }解锁则反过来:先 WP# 拉高,然后清 WPEN。这个功能在量产设备上很有意义——固件启动完成后把日志区锁死,即使程序跑飞也不会破坏数据。如果要更新参数配置,可以设计一个特定密码指令先解锁再写。当然,如果只是普通的参数存储,不锁也行,看项目需求。
4.2 记录型数据的组织方式和页面规划
4Mbit 的 MRAM 划分 512KB,对工业记录器来说分配方案要提前规划好:
| 区域 | 地址范围 | 大小 | 用途 |
|---|---|---|---|
| 参数区 | 0x00000 - 0x1FFFF | 128KB | 设备配置参数,周期更新 |
| 事件日志区 | 0x20000 - 0x6FFFF | 320KB | 循环记录事件/运行日志 |
| 掉电标志区 | 0x70000 - 0x7FFFF | 64KB | 掉电时刻关键数据、检验码 |
参数区采用“双备份 + CRC”方案:参数 A 在地址 0x00000,参数 B 在地址 0x10000。写入时先写 A,再写 B。读取时先读 A 并验证 CRC,如果校验失败则回退到 B。这样即使写入中途掉电,也至少有一个完整备份能用。
事件日志区做成环形缓冲,每条记录固定 32 字节:2 字节事件类型 + 4 字节时间戳 + 数据内容 + 2 字节 CRC16。日志区头 4 字节保存当前写指针,每次写入前都更新指针。MRAM 没有擦除寿命限制,环形缓冲不用担心磨损均衡问题,这在 Flash 方案里根本不敢想。
4.3 掉电保存场景:PVD 中断 + MRAM 的关键优势
工业设备最常见的一种掉电场景是:外部电源被切断,MCU 靠大电容继续撑几十毫秒。这个窗口内必须把重要的运行状态保存到非易失存储器里。
STM32F407ZG 内置可编程电压检测器(PVD),可以监控 VDD 电压下降到某个阈值时触发中断。我把 PVD 阈值设在 2.9V 左右,一旦触发中断,说明电源即将耗尽,立即执行保存流程:
void PVD_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line16) != RESET) { /* 掉电保存:把运行参数写入 MRAM 事件日志区 */ mram_write_page(LOG_CURRENT_ADDR, (uint8_t *)&g_run_status, sizeof(g_run_status)); mram_write_page(LOG_TAIL_ADDR, (uint8_t *)&g_log_pointer, sizeof(g_log_pointer)); EXTI_ClearITPendingBit(EXTI_Line16); } }这个中断里做的事越少越好,最好只拷贝数据 + SPI 写入。因为掉电后的维持时间非常有限,SPI 波特率此时可以临时提高,比如从 21MHz 提到 42MHz(分频 2),减少传输时间。实测在 42MHz 下写 64 字节大约需要64 * 8 / 42MHz ≈ 12us,加上命令开销也就 20us 左右,完全来得及。
这里 MRAM 的价值体现得很彻底:如果是 NOR Flash,掉电保存前要先擦除扇区(毫秒级),根本来不及;MRAM 直接覆盖写,微秒级别搞定,而且写一半掉电也不怕,磁化状态一旦翻转,结果就是确定的。
5. 实测问题与排查实录
5.1 读回全 0xFF 的首查思路
这个问题几乎是所有 SPI 存储器调试的必经之路。我第一次拿到板子,程序的 Read ID 读出来全是 0xFF,第一反应是芯片坏了,后来一步步排查才发现根本不是。
排查顺序按下面来:
先量电压:MR25H40CDF 的 VDD 是不是 3.3V,有没有虚焊。DFN 封装引脚小,引脚间桥连也常见,用万用表打一遍相邻引脚的阻值,排除短路。
再查 WP# 和 HOLD#:这两个引脚如果悬空,芯片内部虽然一般默认上拉,但逻辑电平不明确,HOLD# 一旦被拉低,通信直接停摆。我测量时发现 HOLD# 只有 1.8V,明显不对,后来换上拉电阻到 3.3V 才解决。
最后查 SPI 配置:HAL 库里 SPI 初始化是不是配成了 Mode 3,而芯片实际工作在 Mode 0,这样数据采样的边沿错位,读回来的全是 0xFF 也正常。我习惯在调试初期把波特率降到最低,比如分频 256,先把通信调通再说速度。
5.2 写不进去或只写一次就失效
写入失败最常见的原因是没发 WREN。MRAM 和 Flash 一样,写指令前必须发0x06把 WEL 置位。如果你连续调用两次mram_write_page,第二次大概率失败,因为第一次写完 WEL 自动清 0 了。很多新手直接在写入函数里发 WREN,然后马上写数据,看似没问题,但是如果 SPI 线上有干扰,WREN 没被正确接收,芯片就认为没使能。
我写驱动的时候会在mram_write_page里先调mram_write_enable(),再读状态寄存器确认 WEL 已经置位才继续发写命令:
mram_write_enable(); status = mram_read_status(); if ((status & MRAM_STATUS_WEL) == 0) { return MRAM_ERR_WREN_FAILED; }这个操作多花 2us 左右,但能提前发现问题,不至于数据丢了半天才知道。
另一种“只写一次”的情况是 WPEN 被误置位了。如果你在调试时调过mram_lock_write,之后没解锁就开始写,芯片会拒绝所有写操作。解决办法很简单:WP# 拉高 + 清 WPEN 位。
5.3 数据零星错误或 CRC 校验失败
如果读写大部分正常,但偶尔某个字节不对,优先查 SPI 时序质量。21MHz 的 SPI 对走线要求其实不高,但如果 STM32 的 GPIO 输出速度没配置成 High Speed,或者 PCB 上 SCK 线过长导致过冲,就会偶尔采到错误电平。
我遇到的另一种情况是:在 SPI 传输过程中系统被更高优先级的中断打断,导致两次调用HAL_SPI_TransmitReceive之间的间隔变长。MRAM 对字节间延迟其实无所谓,因为 CS# 全程拉低,芯片靠 SCK 边沿同步,不分帧的概念。但 STM32 的 HAL 库如果你在传输过程中切走了 CPU,只要不把 CS# 拉高,数据不会出问题。
真正要注意的是:在中断服务函数里调用 HAL_SPI_TransmitReceive 时,如果用的是阻塞模式,中断函数会被 SPI 占用,其他更紧急的中断会遭殃。所以掉电保存这种关键路径,要么用 DMA,要么确保保存函数执行期间没有更紧急的事件要处理。
实测下来,我在 PVD 中断里直接调用 HAL 阻塞写,没有开 DMA,逻辑上可行,因为掉电保存的优先级最高,其他中断全部屏蔽即可。写完后直接进死循环,不做多余操作。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读 ID 全是 0xFF | 供电异常、HOLD#悬空、SPI 模式不匹配 | 量电压、上拉、检查 CPOL/CPHA |
| 写入无效 | 没发 WREN 或 WEL 未置位 | 读状态寄存器确认 WEL |
| 写一次后失败 | WPEN 被置位或 WP#拉低 | 清状态寄存器、拉高 WP# |
| 数据偶尔错位 | SPI 时钟过快、走线过长 | 降频、加串阻、开 High Speed |
| 上电后数据被损坏 | 上电瞬间 CS# 不确定 | CS# 接上拉电阻 |
| 掉电保存的数据丢失 | 电源维持时间不够、写入命令太慢 | 提高 SPI 分频、缩短保存数据长度 |
6. 实操中的体会与小技巧
这个项目做完以后,最大的感受是:MRAM 不是 Flash 的替代品,而是完全不同的一类存储器件。只要理解了“无需擦除、覆盖即写、寿命极高”这三个特性,很多以前绕不开的架构问题瞬间就消失了。比如环形日志缓冲,以前在 Flash 上要考虑磨损均衡和掉电一致性,现在只管写指针就行,逻辑代码量至少砍掉一半。
有几个小经验分享给要上手的朋友:
先低速调通,再提速。21MHz 听起来不高,但 PCB 工艺差的时候照样出错。把预分频调到 256,先确认 1MHz 下读写无误,然后把分频系数逐级往下调。这样即使出错也知道是时序问题,而不是逻辑问题。
上电先读 ID,再操作数据。这个习惯能帮你区分硬件问题还是软件问题。如果 ID 都读不出来,别急着调业务代码。
数据区记得多做几个 magic number。我在参数区开头放了 4 字节魔数0xA5A5A5A5,每次读出来先验证魔数,再验证 CRC。这是判断芯片是否被误写、设备是否经历过异常复位的最直接手段。
MRAM 的 WP# 和 HOLD# 虽然内部有上拉,但量产板上一定要外加电阻。实验室环境没问题,现场电磁干扰一多,浮空引脚的电压波动足以导致偶发故障。
如果你手头也遇到类似的需求,比如要做掉电保存、事件记录或者参数频繁更新,MR25H40CDF 配合 STM32F407ZG 这套组合可以拿来直接用,驱动代码量不大,调试经验也验证过了。后续如果想扩展,可以往多片 MRAM 级联、SPI DMA 传输、更完善的文件系统方向迭代,这些都是基于这套底层驱动往上加的事情,不会推倒重来。