最近做了一块工业控制器的主控板,核心组合是 STM32F746ZG 和一片 MR25H40CDF。前者是意法半导体基于 Cortex-M7 的高性能主控,后者是 Everspin 的 4Mbit SPI MRAM。这套组合解决的其实是嵌入式开发里最枯燥、又最容易出问题的需求:在工业和嵌入式应用中可靠地存储和读取数据。重点不在于跑分,而在于每一条写进去的数据,在断电、重启、强干扰、宽温度变化之后,还能原样读出来。这篇文章把选型思路、硬件接线、驱动代码、数据管理策略,以及调试过程中踩过的坑完整整理出来,给正在做类似存储方案的朋友做个参考。
1. 项目概述与选型思路
1.1 为什么在工业场景里,我放弃了 Flash 和 EEPROM
工业控制器里最常见的非易失存储方案是 SPI NOR Flash 和 I2C/SPI EEPROM。过去几年我做过的产品里,这两类芯片占了绝大多数,但它们各自都有绕不开的短板。
SPI NOR Flash 的特点是容量大、价格低,但写数据前必须先擦除,而且擦除是按扇区或者整块进行的。如果你的程序需要频繁修改几个字节的参数,比如伺服驱动器的 PID 系数、电能表的误差校准值、通讯设备的波特率配置,那就必须承担“读-改-擦-写”这一整套流程。这个流程里最麻烦的不是代码复杂,而是两个隐患:一个是 Flash 的擦写寿命有限,普遍在 10 万次左右,频繁写日志很快就会磨到临界值;另一个是擦除过程中万一掉电,数据大概率损坏,需要额外的备份区和掉电检测电路来兜底。
EEPROM 虽然支持按字节写、不需要擦除,但写入时间通常在 5ms 左右,而且容量普遍偏小。你要是想在本地保存几个小时的运行记录,比如温度曲线、电压波动事件、通讯报文镜像,那点容量根本不够用。
MR25H40CDF 这类 MRAM 恰好把这两类芯片的优点结合了。它本质上是带非易失特性的 RAM,写入不需要擦除,也没有写入延迟,速度接近普通 SRAM。它的写寿命按 Everspin 手册标称在 10 的 14 次方级别,基本不用考虑磨损问题。数据保持时间也足够长,工业级温度范围内能保持 20 年以上。对于需要频繁写、快速写、断电保数据、又不希望把存储管理搞得太复杂的场景,MRAM 是一个非常省心的选择。
1.2 MR25H40CDF 与 STM32F746ZG 搭配的合理之处
这套组合里,STM32F746ZG 提供的是计算能力和接口资源。它主频最高 216MHz,片内带 1MB Flash 和 320KB RAM,更重要的是它有多个 SPI 控制器,其中 SPI1/SPI4/SPI5/SPI6 都能跑到较高的时钟频率,并且支持 DMA。这意味着我用它驱动 MRAM 时,数据搬运可以不占 CPU,批量写日志的时候主控还能同时处理控制算法和通讯任务。
MR25H40CDF 这边,容量是 4Mbit,也就是 512KB 字节,对工业数据记录来说中等偏上,能存不少运行参数和事件日志。它采用标准 SPI 接口,指令集和普通 SPI Flash 相似,只是没有擦除指令。它工作在 1.8V 供电域,支持最高 40MHz 的 SPI 时钟,工业级温度范围,封装是紧凑的 DFN-8,板级占用的面积非常小。
选择这套组合还有一层考虑:STM32F746ZG 的工作电压范围是 1.7V 到 3.6V,这意味着整个系统可以设计成单 1.8V 逻辑域,MRAM 和 MCU 直连,不需要电平转换芯片,既节省 BOM 成本又减少了信号线上的转换延迟。如果你的系统里还有 3.3V 的外设,那就必须做电平转换。这一块我在后面的硬件设计章节里会详细展开。
1.3 这套方案适合谁,解决什么问题
如果你正在做的是这几类产品,这套方案可以直接抄作业:
- 伺服驱动器、变频器、PLC 这类需要频繁保存运行参数和故障录波的工业控制设备;
- 电能表、配电终端、电力监测装置,需要快速写入校准数据并且极端强调掉电不丢数据;
- 轨道交通、医疗设备、安防监控这类对数据可靠性要求极高,且可能存在辐射干扰的嵌入式系统;
- 任何基于嵌入式 Linux 或 RTOS 的现场设备,需要一个小容量、高可靠的存储空间来保存启动配置和运行日志。
对普通消费类产品来说,MRAM 的成本优势不明显,Fl起来仍然够用。但一旦进入工业现场,设备要面对的是频繁重启、电压不稳、电磁干扰、高温低温等极端环境。MRAM 的瞬时写入和无限寿命能帮你免掉一大堆掉电管理逻辑,这是它在工业场景里最大的价值。
2. 硬件设计与接线细节
2.1 引脚连接与电平匹配要点
先把我的接线表放出来。这个项目里 MR25H40CDF 挂在 STM32F746ZG 的 SPI1 上,片选没有用硬件 NSS,而是用普通 GPIO 软件控制,这样灵活性最高。
| MR25H40CDF 引脚 | 引脚编号 | 连接到 STM32F746ZG | 说明 |
|---|---|---|---|
| CS# | 1 | PB9(GPIO 输出) | 片选,低有效,软件控制 |
| SCK | 2 | PB3(SPI1_SCK) | 时钟线 |
| SI | 3 | PB5(SPI1_MOSI) | 主机输出,接到 MRAM 数据输入 |
| SO | 4 | PB4(SPI1_MISO) | 主机输入,来自 MRAM 数据输出 |
| WP# | 5 | VDD_MRAM | 写保护关断,接高电平 |
| VSS | 6 | GND | 系统地 |
| VDD | 7 | VDD_MRAM(1.8V) | 电源 |
| HOLD# | 8 | VDD_MRAM | 保持功能禁用,接高电平 |
这里最值得强调的是电平匹配。MR25H40CDF 型号里的 C 就代表 1.8V 供电版本,这和常见的 3.3V 版本 MR25H40MDF 不一样。如果你的 MCU 跑 3.3V,那么 MRAM 的 MISO 输出高电平只有 1.8V,对 3.3V 域来说可能够不到输入高电平阈值,读数据就会出错。反过来,MCU 的 MOSI、SCK、CS 输出 3.3V 高电平,直接进 1.8V 器件又有过压风险。
我的做法是让整块主控板工作在 1.8V 逻辑域。STM32F746ZG 的 VDD 设置为 1.8V,所有 GPIO 和 SPI 引脚都按 1.8V 标准驱动,这样和 MRAM 之间就完全同电平,不需要任何转换。但要注意,如果板上还有其他 3.3V 器件,比如 NOR Flash、CAN 收发器、以太网 PHY,那就要把这些外设单独供电,并且所有跨电平的信号线加 TXS0108EPW 之类的双向电平转换芯片,不能心存侥幸直接连。
2.2 供电、滤波与 PCB 布局的实操建议
MR25H40CDF 的供电电压范围按手册是 1.7V 到 2.0V,我建议把它当成 1.8V 器件来设计,供电纹波控制在 50mV 以内比较稳妥。VDD 和 VSS 之间要放两枚去耦电容,一枚 0.1uF 高频陶瓷电容,一枚 1uF 或者更大的陶瓷电容,并且尽可能靠近芯片引脚放置。
PCB 布局方面,MRAM 要靠近 STM32F746ZG 的 SPI1 引脚放置,尽量减少走线长度。SPI 时钟频率即使只跑到 20MHz 或 27MHz,信号沿质量也会影响数据传输可靠性。我的经验是:SCK、MOSI、MISO这三根信号线走线长度控制在 20mm 以内,并且保证有完整的地平面作为回流路径。如果板子空间紧张,至少要做到信号线不要跨分割区域,不要在信号线上打过孔,尤其是 SCK 线。
另外,MR25H40CDF 是 8 引脚 DFN 封装,底部有大焊盘。焊接时要注意焊盘散热导致的虚焊问题,最好用 X 光检查或者至少做一次 IO 全读写测试来验证焊接质量。我遇到过一块板子,第一次贴片回来有 3% 的 MRAM 读出来某些位固定为 0,重新加热焊接之后恢复正常,基本可以确定是虚焊导致。
2.3 /WP 与 /HOLD 引脚千万别悬空
WP# 和 HOLD# 这两个引脚必须接高电平,不能悬空。WP# 是写保护输入,低电平时 MRAM 会拒绝写操作;HOLD# 是串行挂起输入,低电平会让器件暂停数据传输。如果这两个引脚悬空,在现场电磁干扰下可能会被拉低,出现“写不进数据”或者“读出来数据错位”这种极其诡异的问题。
我见过不少开发者贪图省事,把这两个引脚直接扔在那里不管,结果产品送到现场后,在强干扰环境下频繁出现数据写入失败的故障,排查起来非常痛苦。正确的做法是:把 WP# 和 HOLD# 都通过 10kΩ 电阻上拉到 MRAM 的 VDD。如果你希望程序运行时能控制写保护,可以把 WP# 接到 MCU 的 GPIO,但默认输出高电平。对于大多数应用,直接上拉就够了。
3. 驱动代码实现与关键时序
3.1 SPI 初始化参数怎么设置
MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1),我习惯用 Mode 0。STM32F746ZG 的 SPI1 挂在 APB2 总线上,APB2 最高 108MHz,SPI1 的时钟源就是 APB2,所以预分频系数最小是 /2,也就是 54MHz。但 MR25H40CDF 的 SPI 时钟标称最高 40MHz,所以我至少要用 /4,得到 27MHz。实际项目里我直接配成 /4,运行很稳定。如果你对 PCB 走线质量没把握,或者用了电平转换芯片,降到 /8(13.5MHz)更保险。
下面是用 STM32CubeMX 生成的 HAL 初始化代码,结合这个项目做了精简:
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); }注意 NSS 一定要设为软件模式,因为我用的是 PB9 软件控制片选。如果你用硬件 NSS,要额外配置 NSS 输出使能,反而增加复杂度。
3.2 基本读操作与地址域处理
MR25H40CDF 的读指令是 0x03,发送完指令后跟 3 字节地址。芯片容量是 512KB,有效地址位只有 19 位,所以地址字节的最高字节可以直接清零。很多 SPI Flash 的驱动代码留了 3 字节地址的习惯,这里沿用即可。
读操作的核心代码长这样:
#define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; header[0] = MRAM_CMD_READ; header[1] = (uint8_t)(addr >> 16); header[2] = (uint8_t)(addr >> 8); header[3] = (uint8_t)addr; HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }这里有个细节:发指令、地址、接收数据整个过程,CS 必须保持低电平,不能中途拉高。HAL_SPI_Transmit 和 HAL_SPI_Receive 是两次独立的 SPI 事务调用,但它们之间 CS 没有变化,所以从协议上来说是连续的一帧。如果你的程序里在中间插入了其他 SPI 设备操作,就会破坏帧结构,导致地址错位。
3.3 写操作必须执行写使能指令
MR25H40CDF 和普通 SPI Flash 一样,写操作之前必须先发送 WREN(0x06)指令来置位状态寄存器里的 WEL 位。有些人第一次写 MRAM 驱动时会漏掉这一步,结果发现怎么都写不进去。写操作的核心代码如下:
void mram_write_enable(void) { uint8_t cmd = MRAM_CMD_WREN; HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); } void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t header[4]; mram_write_enable(); header[0] = MRAM_CMD_WRITE; header[1] = (uint8_t)(addr >> 16); header[2] = (uint8_t)(addr >> 8); header[3] = (uint8_t)addr; HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, len, HAL_MAX_DELAY); HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }写操作和读操作最大的不同在于:写使能必须在 CS 拉低之前完成,也就是 WREN 指令发出并结束之后,CS 先拉高,然后再发起写指令帧。千万别把 WREN 和 WRITE 两帧合并成一帧,那会违反协议,芯片不会接受。
对于连续写,MR25H40CDF 会在地址达到 512KB 边界时自动回绕。这个行为和普通 SPI NOR Flash 的页回绕类似,如果你写入的数据跨过了 0x80000 边界,数据会写到地址 0x00000。所以在驱动层必须做好保护,不要让写入长度超过剩余容量。
3.4 读改写与写入时序的注意点
MRAM 最大的优势是可以直接覆盖写,不需要先擦除。但很多实际应用场景里,我们需要修改一个字节中的某几个位,比如把某个标志位置 1,同时保留其他位不变。这时候就要用到读改写模式:
uint8_t status = mram_read_byte(0x00010); status |= 0x01; mram_write_byte(0x00010, status);这个流程要注意的是中间不能被其他任务打断,否则可能出现“读出来旧值、写入旧值、另一位被其他任务修改的值被覆盖”的问题。在 RTOS 环境下,读改写这段代码要加临界区保护,比如:
taskENTER_CRITICAL(); status = mram_read_byte(addr); status |= 0x01; mram_write_byte(addr, status); taskEXIT_CRITICAL();如果你运行的是裸机程序但有中断,也要考虑关中断或用信号量保护。这一点在工业现场多任务环境下尤其重要。
另一个时序要点是:MRAM 的数据锁存发生在 CS 拉高的上升沿。也就是说,写指令的所有字节传输完成后,CS 从低拉高那一刻数据才真正进入非易失存储单元。所以 CS 的上升沿必须干净,不能有毛刺。GPIO 驱动的 CS 一般不会有毛刺问题,但如果 CS 被硬件 NSS 自动控制,而且 SPI 总线上有其他设备,就要格外小心 NSS 信号的切换时序。
MR25H40CDF 的状态寄存器里有一个 WIP 位(bit0),表示器件是否忙。虽然 MRAM 写入几乎是瞬时的,但为了代码的通用性和后续可能替换成其他存储芯片,我保留了写完轮询 WIP 的逻辑,加 10us 超时就行,不会增加多少开销。
4. 数据管理策略与掉电保护设计
4.1 存储区域规划与地址分区
512KB 空间对于工业设备来说不算大,但合理规划后足够用。我的习惯是把存储空间分成三个区域:配置区、运行参数区、日志区。配置区保存那些很少修改但非常重要的数据,比如设备序列号、出厂校准值、通讯地址;运行参数区保存开机后可能频繁修改的数据,比如当前工作模式、PID 参数、累计运行时间;日志区则是循环写入的事件记录或故障录波。
| 起始地址 | 结束地址 | 区域大小 | 用途 |
|---|---|---|---|
| 0x00000 | 0x01FFF | 8KB | 出厂配置区,仅在产线写入 |
| 0x02000 | 0x0FFFF | 56KB | 运行参数区,支持读改写 |
| 0x10000 | 0x7FFFF | 448KB | 循环日志区,按记录追加写入 |
分区的好处是避免日志区频繁写入时意外覆盖配置数据。虽然 MRAM 不像 Flash 那样有擦除操作,覆盖写不会破坏相邻扇区,但从软件架构上划分区域仍然是必要的好习惯,能让代码更清晰,也方便后续在同一个地址空间里挂一个小型文件系统。
4.2 数据校验与双缓冲机制
光有存储介质还不够,可靠的数据管理必须考虑记录完整性。每个数据记录块我固定使用以下结构:2 字节魔数、2 字节版本、4 字节长度、N 字节有效数据、4 字节 CRC32。读取时先校验魔数和版本,再查 CRC,任何一项不合法就判定该记录无效。
CRC32 的实现在嵌入式里很常见,这里就不再贴代码,但要注意:用硬件 CRC 外设时,STM32F746ZG 的 CRC 结果寄存器是反序输出的,计算时要做多项式匹配,否则容易出现数据明明是完好却校验失败的问题。我吃过这个亏,后来直接用软件查表法,简单可靠。
双缓冲机制是针对掉电场景的。对于需要原子性更新的参数,我在参数区里设置两个槽位,例如 A 槽和 B 槽。写入时先写 A 槽,校验成功后把有效标志更新为 A;下次更新时先写 B 槽,校验成功后把有效标志切换为 B。每次开机读取时,只认有效标志指向的槽位。这样即使写入中途掉电,顶多损坏一个槽位,另一个槽位永远是之前完整的数据。
4.3 掉电保护与传统 EEPROM 的对比
做电力设备的朋友肯定熟悉 EEPROM 写入时的掉电保护逻辑:检测 VDD 跌落到阈值,拉低片选、封锁写入、等待电源耗尽。这套逻辑在 EEPROM 时代必不可少,因为 EEPROM 写入时间长,中途断电会留下半写状态。
MRAM 让这个逻辑变得多余。它的写入时间在纳秒量级,数据写入由片上电路保证原子性,不需要外部额外干预。在我的项目里,掉电检测电路只负责保存 CPU 寄存器和通知通讯协议栈,不需要再为存储芯片做额外保护。代码量减少的同时,可靠性反而提升了。
不过要注意,虽然 MRAM 写入本身是原子的,但一个多字节的数据记录写入过程仍然不是原子的。如果你掉电时正好传了半个记录,那么这半个记录是新的、另外半个是旧的。所以双缓冲和 CRC 校验依然是必需品,掉电保护解决的是“存储单元不会损坏”,数据完整性设计解决的才是“业务数据不混乱”。
4.4 磨损均衡还需要吗
MRAM 的写入寿命极长,按手册标称到 10 的 14 次方次级别,对绝大多数应用来说可以视为无限。这意味着你完全不需要像管理 SPI Flash 那样做动态磨损均衡或者坏块管理。日志区用最简单的环形缓冲区按顺序覆盖写,就是最合适的方案。
但即便如此,我仍然在日志区维护一个写指针记录,记录当前写到哪了、最后一条记录的索引是多少。这不是为了磨损,而是为了快速定位有效的历史记录,否则开机时要从头扫描整个区域才能找到最新日志。这个写指针本身也是一个参数,每次更新前走一遍双缓冲,保证它不会损坏。
5. 常见问题与排查技巧实录
5.1 读回来全 0xFF 或者全 0x00
这个问题最常见的原因有三个:片选没拉低、时钟极性和相位不对、供电异常。先用万用表量 MRAM 的 VDD 引脚,确认电压在 1.7V 到 2.0V 之间。然后用逻辑分析仪抓 CS、SCK、MOSI、MISO 四根线,确认读写波形。
我遇到过一例很隐蔽的问题:CS 引脚复用成了 SPI1_NSS 硬件模式,CubeMX 初始化时又同时开了软件 NSS,导致 GPIO 输出被外设覆盖。读操作时 CS 始终为高,自然读出来全 0xFF。解决办法是仔细检查 GPIO 复用配置,确认 CS 引脚模式为 GPIO_OUTPUT 而不是 AF。
如果读出来的数据大部分正确,但个别位固定错乱,先怀疑焊接,再怀疑电平转换。1.8V 器件输出的 MISO 高电平是 1.8V,如果你用了个反应慢的电平转换芯片,高速下波形会劣化,造成采样错误。把 SPI 时钟降到 13.5MHz 再测试,如果问题消失,基本就是信号完整性问题。
5.2 写入时返回成功,读出来还是旧值
写不进去的最典型原因是遗漏了 WREN 写使能指令。MRAM 和 Flash 一样,必须先把状态寄存器里的 WEL 位通过 WREN 指令置位,才允许写操作。另一个容易忽略的问题是 WP# 引脚被拉低,或者状态寄存器里的 WPEN 位开启。WP# 一旦为低,写操作会被硬件禁用。
排查顺序是这样的:先读状态寄存器,确认 WEL 位是否置 1;然后用万用表量 WP# 引脚电压,确认是高电平;最后检查写指令的帧结构。特别提醒一点:WREN 指令本身也需要完整的 CS 低电平帧,发送完成后 CS 要拉高,再开始写指令帧。如果把 WREN 和 WRITE 放在同一个 CS 低电平期间连续发送,芯片不会执行写使能。
如果这些都正常,就要看地址范围了。MR25H40CDF 只有 19 位有效地址,地址超过 0x7FFFF 后,访问会回绕到低地址。你往 0x80000 后面写的所有数据,实际上覆盖的是地址空间开头的区域,这会让调试者一头雾水。
5.3 与 STM32F7 D-Cache 冲突导致的数据错乱
STM32F746ZG 带 D-Cache 和 I-Cache。如果你用 DMA 从 MRAM 读数据到内存,之后 CPU 直接访问这块内存,可能读到缓存里的旧数据,这就是典型的数据一致性问题。尤其是在循环日志读取场景里,DMA 搬运完一批新数据,CPU 再解析时发现数据是旧的,就会表现出“读出来的数据好像慢了半拍”的诡异现象。
解决方案有两种。一种是在 DMA 完成后做缓存维护,HAL 库里可以用 SCB_InvalidateDCache_by_Addr 让缓存行失效:
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);另一种更彻底的做法,是把存放 MRAM 数据的缓冲区对应的 MPU 区域配置成非缓存或 write-through 模式。工业固件里我把日志缓冲区所在的 4KB RAM 区域配成 Non-Cacheable,避免在复杂的任务切换中漏掉缓存维护操作。代码简洁,也更好理解。
5.4 工业现场干扰导致 SPI 通信偶尔出错
如果你的设备要过电快速瞬变脉冲群(EFT)、静电放电(ESD)、射频辐射抗扰度这些工业级测试,SPI 这种并行总线很容易出问题。我的经验是:SPI 信号线在 PCB 上要尽量短,并且远离电源开关节点和继电器驱动线;连接器输入输出端的接口信号做好 TVS 保护,但注意 TVS 管的结电容不要太大,否则会拖垮高速信号。
软件侧也做一些冗余设计:每包数据里增加 CRC 校验,读出来校验失败就立即重读一次,连续三次失败再判定为硬件故障。对于日志记录,我采用“写后读回校验”的方式,也就是每写一条记录,立即读出来和原始数据比对,发现不一致就重写一次。MRAM 写入是瞬时的,这种读回校验的开销可以接受,但在工业现场能显著提升整体可靠性。
5.5 中断与 RTOS 环境下要避免 SPI 事务被打断
最后提醒一个很多人会忽略的问题:SPI 一个完整事务包含发出指令、发送地址、传输数据三个阶段,中间一旦被中断打断,或者被另一个任务抢占了总线,CS 时序就被破坏。MRAM 的写操作还好,最坏结果是当前帧无效、数据不写入;但读操作如果 CS 被错误拉高,数据线就会进入高阻态,读回来的数据会变成乱码。
在裸机环境下,SPI 传输函数执行期间要关中断或者用临界区保护。FreeRTOS 环境下,给 SPI 总线加一个互斥锁,并且保证整个读或写调用都持锁。DMA 方式是更好的选择,它能保证波形连续性,配合中断通知机制,不容易被任务调度打断。我的最终实现就是 SPI1 + DMA1,发送和接收都走 DMA,CPU 只负责触发传输和等待完成标志。
6. 项目扩展方向与个人经验总结
这套存储方案目前已经跑在批量产品上,稳定运行了大半年,先后过了 EFT 4kV、浪涌、温度循环等项测试。后续如果要在其他项目复用这套组合,我建议把驱动代码封装成独立模块,把设备相关的 SPI 初始化和 MRAM 驱动分离。替换成其他容量或接口的 MRAM 时,只需要改驱动层的读写函数,业务层的数据管理逻辑可以原封不动搬过去。
最后分享一个我自己的心得体会:不要抱着 Flash 的老思路来用 MRAM。一开始我做驱动时,不由自主地保留了擦除、页缓冲、块对齐这些逻辑,结果反而把简单的事情搞复杂了。后来把驱动精简成“读、写、读改写、读状态”四个基础接口,再围绕接口做分区和校验,整个系统一下子就清爽了。MRAM 的本质就是一块断电不丢数据的 RAM,你越把它当成普通内存来用,代码就越简单,可靠性反而越高。
如果你正在评估工业嵌入式场景下的存储方案,我建议认真考虑一下这个组合。MR25H40CDF 的价格虽然比同容量 NOR Flash 高一些,但省下的掉电保护硬件、磨损管理代码、故障排查时间,早就值回差价了。