去年在调一台工业环境监测设备的存储子系统时,我被一个老问题折磨了好几天:设备需要连续记录运行状态和报警事件,掉电后数据不能丢,现场又不允许我隔几个月去拆机换电池。以前项目里用的 24C256 EEPROM 容量太小,按每分钟一条记录算,两周就满了;换成 W25Q64 之后容量是够了,但写日志要频繁擦除,日志满了以后的写入延迟经常把主逻辑卡住。思来想去,最后把存储颗粒换成了 Everspin 的 MR25H40CDF,主控用 STM32F415ZG。这套组合解决了我当时所有痛点,这篇文章就把从硬件搭建、驱动设计、可靠性验证到实际踩坑的完整过程整理出来,给正在做工业数据存储或掉电记录的朋友参考。无论你是刚接触嵌入式存储,还是准备把 Flash 换成 MRAM,这篇内容都值得花几分钟过一遍。
1. 工业存储选型:为什么最后选了 MR25H40CDF 而不是 EEPROM/NOR Flash
1.1 一个掉电记录需求把我逼上了 MRAM
那台环境监测设备的工作场景并不复杂:主板上有一批传感器,主控 STM32F415ZG 每秒钟做一次采样,每分钟把采样均值、报警状态、设备工况写入非易失存储。最麻烦的需求有三个:
第一,数据必须掉电不丢,而且不能在写入中途被异常断电弄坏。设备的安装位置经常在野外配电箱里,电源来自太阳能电池和铅酸电池,电压波动和突然断电是常态。
第二,写入频率不算低。按一分钟一条记录算,一天接近 1500 条,一年就是 54 万条左右。如果再加上报警事件、参数变更记录,两年下来超过一百万次写入很常见。
第三,现场不能频繁维护。验收方要求设备至少连续工作五年,存储不能因为“写满”或“擦写寿命耗尽”而罢工。
我当时第一反应是继续用 I2C EEPROM。24C256 这类器件的好处是熟悉、便宜、写入简单,字节级覆盖,不需要擦除。但劣势也明显:容量小,页写要等 5ms 左右,而且存储单元寿命普遍标称 100 万次写循环。一百万次听起来不少,可如果设备每分钟写一两次,两三年的时间就接近极限了。
然后我考虑过 SPI NOR Flash,比如 W25Q64。容量确实大,驱动也成熟,但 Flash 的物理特性天然带一个尴尬:写入前必须先擦除,而擦除操作按扇区来,一次擦除要几十甚至上百毫秒。对连续日志来说,只改一个扇区里的几个字节,也要整扇区擦除再重写。一旦在擦除过程中断电,轻则丢数据,重则整个扇区逻辑坏掉。想在野外环境里稳定跑五年,我心里没底。
后来接触到 MRAM,才觉得这是更贴合工业“掉电记录”场景的存储介质。当时手头有几颗 Everspin 的 MR25H40CDF,简单测了一轮,性能和数据安全表现都超出预期。
1.2 MRAM 的底层原理和 MR25H40CDF 的基本参数
MRAM 的全称是磁阻随机存取存储器,核心存储单元不是电容电荷,也不是浮栅电荷,而是磁隧道结(MTJ)。写入数据时改变磁性层极化方向,靠磁电阻效应区分 0 和 1。这意味着它不像 Flash 那样需要电荷泵来“推”电子,也不存在擦除和写入的物理延迟差异,写操作本身就是直接覆盖。
MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM,折算下来是 512KB。这个容量在 MRAM 里属于中规中矩,但对工业参数存储和日志记录来说非常合适。它使用标准的 SPI 接口,最高时钟可以跑到 40MHz,操作指令和普通 SPI NOR Flash 很像,有大写使能、读状态寄存器、读取数据、写入数据这些基本命令,唯独没有擦除相关指令。
我选它的核心原因可以归纳成三点:
- 不用擦除,直接覆盖写,任何地址都可以按字节写。
- 写入耐久性极高,厂家宣称没有磨损限制,工业场景基本不用考虑寿命。
- 数据保持时间长,至少 20 年不掉数据,环境温度范围覆盖工业级。
实际测试下来,MR25H40CDF 的时序比 Flash 干净太多:写一个字节和写一页在操作复杂度上几乎一样,只要发送 WRITE 指令、地址和数据,CS 拉高以后数据就落进去了,不需要再等“页编程时间”或者“擦除时间”。
1.3 用表格看清三种存储颗粒的差别
很多做嵌入式的人一开始都会在 EEPROM、NOR Flash 和 MRAM 之间纠结。我列一个常用对比表,方便你直接对照选型:
| 对比项 | 24C256 EEPROM | W25Q64 NOR Flash | MR25H40CDF MRAM |
|---|---|---|---|
| 容量 | 256Kbit / 32KB | 64Mbit / 8MB | 4Mbit / 512KB |
| 写入前是否需要擦除 | 不需要,可字节覆盖 | 必须按扇区擦除 | 不需要,可字节覆盖 |
| 典型写操作延迟 | 页写等待约 5ms | 页写约 1ms,扇区擦除约 80ms | SPI 时钟内直接写入,无额外等待 |
| 写入耐久性 | 约 100 万次/字节 | 约 10 万次/扇区 | 无限耐久 |
| 掉电保存时间 | 约 10 年 | 约 20 年 | 20 年以上 |
| 接口 | I2C | SPI | SPI |
| 写中断的风险 | 页写中断电丢当前页 | 擦除中断电可能损坏扇区 | 字节写即时生效,风险极低 |
从表里能明显看到,MRAM 几乎站在了 EEPROM 和 Flash 的中间偏优位置:它有 EEPROM 的字节覆盖便利,又有 Flash 的 SPI 接口和容量,同时把擦除、长等待、寿命限制这几个痛点全去掉了。价格确实比 EEPROM 和 Flash 贵,但在工业设备里整机可能卖几千块,存储颗粒贵出一两块钱根本不是主要矛盾。
2. 和 STM32F415ZG 的硬件连接:版图与引脚处理比想象中讲究
2.1 SPI 总线选型和引脚分配
STM32F415ZG 自带 3 个 SPI 外设,我选择了 SPI1,配到 PA5 做 SCK、PA6 做 MISO、PA7 做 MOSI。片选 CS 没有用硬件 NSS,而是单独拿 PA4 做普通 GPIO 手动控制。
为什么选 SPI1 而不是 SPI2 或 SPI3?原因是 SPI1 挂在 APB2 总线上,APB2 时钟最高 84MHz,SPI1 经过分频后可以跑到 42MHz;SPI2 和 SPI3 挂在 APB1 上,APB1 时钟最高 42MHz,SPI2/SPI3 最高只能到 21MHz。MR25H40CDF 的极限是 40MHz,理论上 42MHz 超了一点,所以我用 SPI1 四倍分频得到 21MHz 运行,既有充足余量,又不会因为信号完整性问题在产线上大批量翻车。
具体引脚连接是这样的:
| MR25H40CDF 引脚 | 信号方向 | 连接到 STM32F415ZG |
|---|---|---|
| CS | 输入 | PA4 GPIO 推挽输出 |
| SCK | 输入 | PA5 SPI1_SCK |
| SI / MOSI | 输入 | PA7 SPI1_MOSI |
| SO / MISO | 输出 | PA6 SPI1_MISO |
| WP | 输入 | VCC 3.3V |
| HOLD | 输入 | VCC 3.3V |
| VCC | 电源 | VCC 3.3V |
| GND | 地 | GND |
需要留意的是,MRAM 是 SPI 从机,它没有地址线,也不像并行存储器有那么多控制引脚。整颗芯片只有 8 个脚,硬件连接非常简洁,真正需要花心思的其实是 WP 和 HOLD 这两个功能引脚。
2.2 电源、去耦以及 WP/HOLD 引脚的固定电平
工业现场电源环境通常不干净,MRAM 虽然是存储器,对电源也做不到完全无感。我在 PCB 上给 VCC 靠近芯片的位置放了一个 0.1uF 陶瓷电容,另外又在附近放了一颗 10uF 的钽电容做低频储能。这两颗电容的作用是吸收 SPI 写入时的小幅电流波动,防止电压跌落导致写数据出错。
真正容易被忽略的是 WP 和 HOLD 两个引脚。
WP 是写保护引脚,低电平时会保护状态寄存器,造成写使能指令无法置位。HOLD 是暂停输入引脚,低电平时芯片会暂停 SPI 通信,此时时钟和数据线上的电平变化都会被忽略。很多参考设计在这两个脚上只留了默认空置,但空置就等于把可靠性交给运气。HOLD 一旦被噪声拉低,主控看到的现象就是 SPI 发送正常,芯片完全没反应,读出来全是旧数据或者 0xFF,这种故障在排查时非常隐蔽。
我的做法是把 WP 和 HOLD 都直接接到 VCC 3.3V,让它们固定在高电平,既确保可以正常写,又彻底排除信号悬空带来的风险。如果你希望保留硬件写保护功能,WP 脚可以串一个 10k 电阻接 VCC,并预留到地焊盘,需要时再拉低。
上电时序也有一个细节:MRAM 的 VCC 还没稳定时,CS 必须保持高电平,否则芯片可能误判为一次无效访问。STM32F415ZG 的 GPIO 在复位期间默认是输入浮空,不会主动驱动 CS,所以只要 MRAM 的 VCC 和 MCU 的 VCC 来自同一电源平面,这个要求基本自动满足。若用独立电源给 MRAM 供电,就要额外确认 CS 上拉到 VCC 或 MCU 上电后再初始化总线。
2.3 片选信号为什么必须用 GPIO 手控
SPI 通信里,CS 是整个事务的起始和结束标志。STM32 的硬件 NSS 虽然也能用,但应用场景更多是配合“多主机模式”或者自动片选逻辑,在工业存储这种单主多从场合,我坚持用 GPIO 手动控制 CS。
原因有三:第一,手动 CS 控制指令边界非常直观,低电平代表开始,高电平代表结束,时序完全由自己的代码决定,不容易被 HAL 库的自动 NSS 行为干扰。第二,如果以后总线上还要挂 SD 卡、SPI Flash 或者其他传感器,每个从器件一个 GPIO 片选,互不干扰。第三,MRAM 的读写是几个连续字节组成的事务,中途不能让 CS 发生抖动,GPIO 输出经过设置后不会被外设时钟变化影响,稳定性更好。
实际布线时,CS 信号没有串联电阻,直接从 STM32 的 PA4 拉到 MRAM 的 CS。SCK、MOSI、MISO 三根线在 MCU 端各串联了一个 22 欧姆电阻,起到阻抗匹配和抑制振铃的作用。21MHz 的 SPI 时钟对走线长度不算苛刻,但我的原则是能短则短,尽量保证四根信号线等长,并且远离继电器、电动机驱动等大电流回路。
3. 驱动设计与读写流程:从指令集到可复用代码
3.1 MR25H40CDF 的指令集和 SPI 时序
MR25H40CDF 的指令集和 SPI NOR Flash 非常接近,用到的基本指令只有几条:
| 指令名称 | Opcode | 用途 |
|---|---|---|
| WREN | 0x06 | 写使能,写入数据前必须执行 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| READ | 0x03 | 从指定地址连续读取数据 |
| WRITE | 0x02 | 从指定地址连续写入数据 |
SPI 工作模式我配置为 Mode 0,即 CPOL=0、CPHA=0,SCK 空闲时为低电平,数据在 SCK 第一个上升沿采样。这颗芯片也支持 Mode 3,但 Mode 0 是绝大多数 MCU SPI 外设的默认习惯,和 STM32F415ZG 配合时最省心。
必须记住一点:MRAM 没有 Flash 的扇区擦除、块擦除这类指令,也不需要“先擦后写”。如果从旧项目里复制了 Flash 驱动,看到 erase 函数可以直接删掉,保留 WREN、RDSR、READ、WRITE 这部分逻辑就够了。
3.2 SPI 初始化代码与时钟树的选择
STM32F415ZG 的工程我习惯用 STM32CubeMX 做前期配置,然后手工补充 MRAM 驱动。SPI1 初始化时,关键参数是主模式、8 位数据长度、CPOL 为低、CPHA 为第一边沿、串行软件 NSS、四倍分频。
SPI1 初始化代码类似这样:
SPI_HandleTypeDef hspi1; 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; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; HAL_SPI_Init(&hspi1); }APB2 的时钟如果配成 84MHz,四倍分频后 SPI1_SCK 就是 21MHz。这里的四倍分频不是随便选的:MR25H40CDF 最高支持 40MHz,SPI1 的 42MHz 极限已经超出了器件指标,项目量产阶段我不会顶着极限工作。21MHz 足够满足日志记录的速度需求,总线上走线带来的干扰也更小。
对应的 PA5、PA6、PA7 要配置为 SPI1 的复用功能,PA4 配置为推挽输出。用寄存器写的话大致如下:
void MR25H40_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); }3.3 写使能、读状态、写数据和读数据函数
驱动层我用宏定义了片选控制和指令集,把 MRAM 当成标准 SPI 从机操作:
#define MR25H40_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MR25H40_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define MR25H40_CMD_WREN 0x06 #define MR25H40_CMD_WRDI 0x04 #define MR25H40_CMD_RDSR 0x05 #define MR25H40_CMD_READ 0x03 #define MR25H40_CMD_WRITE 0x02 #define MR25H40_STATUS_WEL 0x02写使能函数每次写入前调用:
void MR25H40_WriteEnable(void) { uint8_t cmd = MR25H40_CMD_WREN; MR25H40_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); MR25H40_CS_HIGH(); }读状态寄存器的函数:
uint8_t MR25H40_ReadStatus(void) { uint8_t cmd = MR25H40_CMD_RDSR; uint8_t status = 0; MR25H40_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, &status, 1, HAL_MAX_DELAY); MR25H40_CS_HIGH(); return status; }写数据函数是驱动核心。需要注意地址范围检查、写使能检查,然后在一个 CS 连续低电平周期内发完 WRITE 指令、三个字节地址和数据:
int MR25H40_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t header[4]; if ((addr + len) > MR25H40_SIZE) { return -1; } MR25H40_WriteEnable(); if ((MR25H40_ReadStatus() & MR25H40_STATUS_WEL) == 0) { return -2; } header[0] = MR25H40_CMD_WRITE; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; MR25H40_CS_LOW(); HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, len, HAL_MAX_DELAY); MR25H40_CS_HIGH(); return 0; }读数据函数类似,把 READ 指令发出去后直接接收数据即可,读操作不需要写使能:
int MR25H40_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; if ((addr + len) > MR25H40_SIZE) { return -1; } header[0] = MR25H40_CMD_READ; header[1] = (addr >> 16) & 0xFF; header[2] = (addr >> 8) & 0xFF; header[3] = addr & 0xFF; MR25H40_CS_LOW(); HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); MR25H40_CS_HIGH(); return 0; }如果你希望读数据时 MOSI 线上不发无用数据,可以改用HAL_SPI_TransmitReceive并传入一个全 0xFF 的填充缓冲区。实际效果区别不大,全双工模式下 HAL_SPI_Receive 会自动让 MOSI 保持高电平,设备端不会误解。
3.4 地址边界、返回值和读写验证
MR25H40CDF 是 4Mbit 容量,对应地址范围是 0x00000 到 0x7FFFF,共 512KB。驱动里用 24 位地址,高字节填 0。在写数据函数里进行边界检查非常关键,因为 MRAM 不像 Flash 有扇区概念,地址越界后地址高位会被芯片忽略或回卷,出错时非常难排查。
我在项目里定义了统一的存储映射表,把 512KB 分成几个区域:
- 0x00000 ~ 0x01FFF:主参数区,8KB
- 0x02000 ~ 0x03FFF:参数备份区,8KB
- 0x04000 ~ 0x7FFFF:运行日志区,约 496KB
这样应用层永远只和“区域起始地址 + 区内偏移”打交道,驱动层拿到的是换算后的绝对地址。读写完成后,我会立刻把写入内容读回来做一次逐字节对比,作为基本校验。虽然这会多花一点时间,却能尽早发现 SPI 接线、时钟极性、片选时序的问题。
4. 数据可靠性与工厂级验证:不光能写,还得扛得住现场
4.1 存储区规划和带 CRC 的帧格式
工业存储最怕的不是“芯片坏了”,而是“数据写了一半,上电后分不清哪条记录是完整的”。MRAM 虽然单次写入很快,但一条事件记录往往包含多个字段,如果掉电恰好发生在整条记录写了一半的时刻,没有校验机制的数据区就是一团乱麻。
我的做法是给每条日志定义固定帧格式:
- 2 字节帧头,固定为 0xA55A
- 2 字节序列号,单调递增
- 4 字节时间戳
- 8 字节业务数据
- 2 字节 CRC16 校验
- 1 字节帧完整标志,可取值 0xAA 表示有效,0x00 表示无效
写入顺序也很有讲究:先把数据区、序列号、时间戳和 CRC 写进去,但最后一个字节的完整标志先不写有效值。等前面所有字节都写完,再一次普通 WRITE 写这个完整标志字节。读日志时如果帧头正确、CRC 正确且完整标志为 0xAA,就认为这是一条完整记录;如果完整标志还是 0x00,说明这条记录当时写入被中断,直接跳过。
这个方案把“掉电损坏”的影响控制在单条记录内,不会污染整个日志区。MRAM 的字节写能力让最后一步“补一个标志字节”变得毫无压力,不需要像 Flash 那样为了改一个标志位重新擦写整扇区。
CRC 校验我用的是查表法 CRC16-CCITT,生成多项式 0x1021。CRC 覆盖除 CRC 本身和完整标志外的所有字节。STM32F415ZG 自带硬件 CRC32,但实测日志帧更适合 16 位长度,所以软件算了,一条日志 19 个字节,主机 168MHz 算一次 CRC 耗时可以忽略。
4.2 实测读写性能与掉电断电测试
性能方面,21MHz SPI 的理论吞吐率大约是 2.625MB/s,加上 HAL 库调用开销和地址头部,实际大块读写测下来在 2MB/s 左右。这个速度对每秒几条日志来说绰绰有余。
和 Flash 对比一下就很直观:W25Q64 写一个 4KB 扇区,如果扇区需要先擦除,擦除就是几十毫秒,再写数据又是几毫秒;如果要频繁写小日志,每次都得处理“擦除-写入-校验”的延迟。MR25H40CDF 则是直接覆盖,单次 64 字节的日志写入从起始到结束,实测大概 100 微秒级别,几乎不影响主流程。
掉电测试我是这样做的:用一个继电器控制整机电源,每秒钟随机断开电源,上电后让系统从上次记录的序列号继续写。循环跑了 500 次断电,再逐个扫描日志区,检查有没有出现 CRC 错误、帧头错乱或序列号跳变。最终结果非常干净,凡是完整标志为 0xAA 的记录都能通过 CRC 校验,说明 MRAM 的字节写即时生效能力在真实断电场景下是可靠的。
有一点要提醒:MRAM 单字节写不会损坏相邻字节,但如果你用 DMA 或中断写数据,断电时可能只发送了一半字节,SPI 从机收到不完整位流,芯片内部不会产生实际写入。所以我最终的写入函数都采用阻塞式HAL_SPI_Transmit,保证 CS 拉高前所有字节已经在硬件移位寄存器里完成发送。
4.3 我实际踩过的三个坑和完整排查过程
第一个坑是 HOLD 引脚悬空。最初版本 PCB 上 HOLD 没接,只跑了内部测试,一切正常。到了现场有几台设备偶尔出现读数据卡死,复位后恢复。排查了很久才发现,是配电箱电机启停产生的电磁噪声把 HOLD 拉低,MRAM 进入了暂停模式。把 HOLD 直接接到 VCC 后,问题再没出现。这个坑的隐蔽之处在于它不像数据错误那样直接暴露,而更像系统偶发死机。
第二个坑是“写不进去”。有一版驱动移植自 SPI Flash 代码,逻辑里保留了“写前先擦除”的步骤。MRAM 不支持擦除,发送的 sector erase opcode 被芯片忽略,表面上没报错,但后续读回数据和自己写入的内容不一致。排查时我先用一台示波器抓了 SPI 波形,发现发送序列里多了一个 0x20 擦除指令。删除擦除逻辑,直接覆盖写,问题立刻消失。
第三个坑是对 SPI 时钟域没有概念。早期为了追求速度,我把 SPI1 预分频设为 2,跑在 42MHz。MR25H40CDF 的极限是 40MHz,个别芯片在常温下能工作,但温度升高或总线上有干扰时,读回的数据偶尔出现位翻转。后来改成四倍分频 21MHz,实测几天不再出现随机错误。对于工业应用,速度只要够用,留出至少 30% 的时序余量是非常必要的。
5. 从“能读能写”到工业落地:存储层设计的一些经验
5.1 512KB 并不大,日志要设计成环形缓冲
MR25H40CDF 的 512KB 在 MRAM 里已经不算小,但相对于海量日志来说仍然有限。按一条日志 19 字节计算,理论可以放 2.7 万条记录,再加上一些管理和对齐,实际能放 2 万条左右。如果每分钟写一条,两万多分钟大约半个月就写满了。
所以日志区必须设计成环形缓冲,而不是越界就停止。我在日志区头部维护了一个 16 字节的管理块,包含起始写入地址、写指针、有效记录数和总容量。每次写入前先检查写指针,如果到达末尾就回绕到日志区开头,覆盖最旧的数据。
MRAM 覆盖写旧数据的开销和写新数据完全一样,不需要像 Flash 那样回收空闲区块,这让环形缓冲实现极其简单。但要注意,环形日志的“读顺序”和“物理地址顺序”不是一回事。读取时我要根据管理块里的写指针反推记录时间顺序,避免用户看到乱序日志。
5.2 参数区和日志区的 A/B 镜像策略
设备参数比日志更金贵,因为日志丢了还能再记,参数丢了整个设备可能就无法正常工作。参数区我用了双镜像设计,主参数区和备份参数区各存一份完全相同的参数结构。
更新参数时按三步走:先把新参数写入备份区,校验通过后把主参数区也更新,最后把当前使用的镜像标志写到参数区管理头。上电启动时,程序先校验镜像标志,再校验主参数区 CRC。如果主参数区 CRC 错误,自动切到备用参数区;如果两个区都异常,才恢复出厂参数。
有人可能会问,MRAM 本身已经特别可靠,为什么还要做双镜像?因为可靠性不是某一个器件的单点属性,而是整个系统的综合属性。MRAM 能保证写入即时生效,但应用层的软件缺陷、地址越界写、强电磁干扰造成的逻辑错位仍然存在。双镜像是最便宜的保险。
5.3 沿着这条路还能怎么扩展
这套 MR25H40CDF 与 STM32F415ZG 的组合,如果以后要扩展,有几个方向是顺畅的:
第一,把驱动改成 DMA 模式。现在的阻塞式读写已经满足需求,但如果日志吞吐要求更高,SPI1 加 DMA 可以把 CPU 占用率降到几乎为零。MRAM 没有擦除等待,DMA 写完后只需要检查 WEL 和完整标志,流程比 Flash 简单太多。
第二,给存储层加一个轻量的磨损均衡逻辑。虽然 MRAM 宣称无限耐久,但工业设备设计讲究“不管器件多耐用,软件结构都要兜底”。地址轮换能让写入分散到整个 512KB 空间,即使某几个物理单元因制造缺陷或机械应力出现问题,也不会让同一组参数反复受损。
第三,遇到更大容量的需求时,Everspin 的 MR25H 系列还有更高密度型号,引脚和指令风格基本一致,驱动层的寄存器操作不需要大改。这意味着项目硬件升级时,软件成本可以压得很低。
最后再分享一个小经验:给 MRAM 驱动写自动测试脚本时,不要只做固定地址的读写循环,最好随机生成地址、随机生成长度和随机数据,然后用慢速模式读回校验。MRAM 的“快”很容易让人忽略时序细节,但工业存储真正值钱的,恰恰是那些极端情况下也能稳定不出错的表现。MR25H40CDF 在我项目里半年跑下来没有一次数据问题,希望这篇记录也能帮你少走几步弯路。