做工业设备最怕的故障不是硬件烧了,而是数据写丢了。我最早在一台电机驱动器上做参数保存,用的是片内Flash,量产没多久就遇到“参数偶尔变成0xFF”的客诉。排查到最后,问题基本锁定在Flash擦写寿命和掉电瞬间误写入上。后来我把存储方案迁到 Everspin 的 MR25H40CDF,配合 STM32F411RE,通过 SPI 接口在嵌入式系统里做数据存储和读取,困扰很久的数据丢失问题基本绝迹。这篇文章就是把我的选型逻辑、硬件接线、驱动代码和实测验证过程完整梳理一遍,适合正在做工业控制器、数据记录器、参数存储方案的嵌入式工程师参考。
1. 为什么是 MRAM:非易失、无限寿命、掉电不丢的工业存储逻辑
1.1 Flash 的三个死穴让我最终换掉了方案
在真正接触 MRAM 之前,我一直在用 SPI Flash 存“需要掉电保存、但又会频繁更新”的数据。比如设备运行次数、累计运行时间、用户配置参数。这类数据有两个特点:单次写入的字节量不大,但写入频率可能很高,而且绝对不允许随机丢失。
Flash 在这类场景里有三个绕不过去的问题。
第一个是擦写寿命。常规 SPI NOR Flash 的擦写寿命通常在 10 万次到 100 万次之间。如果设备每 10 秒记录一次运行状态,一天就是 8640 次,一块号称 10 万次寿命的 Flash,十几天的量就能吃掉一半。虽然实际工程上会把所有参数放到一个数据区做“磨损均衡”,但写入频率一高,均衡算法本身也会变成负担。
第二个是“先擦除再写入”的时序。Flash 写入前必须擦除扇区,而擦除一个扇区的时间动辄几十毫秒,这期间一旦掉电,整个扇区可能处于未定义状态。我在掉电保存的逻辑里加过很多保护,比如掉电检测、等待擦除完成、状态标志位,但总归是提心吊胆。
第三个是掉电瞬间的误写入。系统电压下跌时,如果 MCU 因为欠压复位而把 Flash 的 CS、SCK 信号“拉成半吊子”,SPI 总线上就可能出现一次非法写序列,Flash 里被写进几个坏字节。这种问题很难复现,但偶发性很强,客诉压力不小。
后来我用了一颗 MR25H40CDF 做对比测试,才发现这些问题大多可以绕开。它是 MRAM,全称是 Magnetoresistive Random Access Memory,本质上是一种非易失存储器,但读写的特性和 RAM 几乎一样。写入不需要擦除,单个字节直接覆盖;写入时间就是普通 SPI 传输的时间,没有内部编程等待;写寿命官方标称几乎是无限次。这几个特性组合起来,简直像是为“频繁写关键数据”这类工业场景量身定做的。
1.2 MR25H40CDF 关键参数与实际适用边界
MR25H40CDF 是 Everspin 的串行 SPI 接口 MRAM,容量 4Mbit,也就是 512KB。工作电压典型 3.3V,SPI 接口,封装是很常见的 8 脚小外形封装,引脚间距和常规 SPI Flash 基本一致。工业级温度范围按型号后缀确定,通常能在 -40℃ 到 +85℃ 甚至更宽的温度范围里稳定工作,具体以数据手册为准。
512KB 这个容量,放在今天的嵌入式系统里并不算大。跑嵌入式 Linux 系统和文件打包肯定不够,但用来做参数存储、运行日志、黑匣子记录、计量数据累计,绰绰有余。我在项目里把它当成一颗“掉电不丢的 SRAM”用,而不是当成 Flash 用,这个定位很重要。
成本方面,MRAM 比 Flash 贵,这是它最大的短板。所以在项目选型时要算清楚:哪些数据值得用 MRAM,哪些数据用 Flash 就够了。比如嵌入式系统里几百兆的录音文件、固件升级包、字库图片,这些属于大容量冷数据,放 Flash 或 SD 卡完全没问题;但设备每天要改几百次的关键参数,还是放到 MRAM 里踏实。
另外提一句,有些工程师会把 MRAM 和 FRAM 放在一起比较。两者都是非易失,且都支持近乎无限的写次数。FRAM 也有写入快的优点,但在高频重复写同一地址时可能存在“印刻效应”,MRAM 没有这种问题。工业电磁干扰比较大的环境里,MRAM 的数据保持特性通常也更有优势。当然具体指标还是要看厂商数据手册,我这里只说工程体感。
2. MR25H40CDF 的引脚、指令和数据手册里没写透的细节
2.1 接线之前先看懂 SPI 接口与多功能引脚
MR25H40CDF 虽然是 8 脚封装,但真正需要关注的引脚功能并不少。除了 VCC 和 VSS,核心信号是 CS、SCK、SI、SO,这是标准 SPI 的四根线。另外一个容易忽略的点是它还有 WP 和 HOLD 两个多功能引脚。
WP 是写保护输入。WP 拉低时,芯片的状态寄存器和部分写操作可能被锁死,具体保护范围同样要看数据手册。我在调试阶段会把 WP 固定拉高,让芯片处于允许写入状态;如果产品最终要做成“运行期间只能读,升级时才能写”,再把 WP 接到一个可控 IO 上。
HOLD 引脚是用于暂停串行通信的。HOLD 有效时,芯片会忽略 SCK 上的信号,保持当前引脚状态。对大多数应用来说,这个功能用不到,但不接不行。HOLD 悬空会有被噪声拉低的风险,一旦在通信过程中进入 Hold 状态,后面的数据就全乱套了。稳妥做法是把 HOLD 直接上拉到 VCC。
还有一个小细节:CS 引脚建议外部加一个 10kΩ 左右的上拉电阻到 3.3V。MR25H40CDF 和其他 SPI 器件一样,是靠 CS 的下降沿启动命令、上升沿结束命令。上电瞬间 MCU 的 GPIO 状态是不确定的,如果 CS 悬空,可能因为噪声产生一次伪下降沿,芯片误以为收到了指令。加上拉电阻后,CS 在上电阶段保持高电平,能避免这类“幽灵写入”。
2.2 指令集与状态寄存器:为什么每次写操作前都要发 0x06
MR25H40CDF 的 SPI 指令集非常接近传统 SPI Flash,有一条读状态寄存器指令 0x05,一条写状态寄存器指令 0x01,读数据指令 0x03,写数据指令 0x02,另外还有睡眠 0xB9、唤醒 0xAB 这些辅助指令。常用的其实就是读、写、读状态寄存器、写使能这四条。
这里最值得展开说的是写使能指令 WREN,也就是 0x06。手册要求,芯片每次要改写状态寄存器之前,必须先发送 WREN,让状态寄存器里的写使能锁存位 WEL 置位。我自己的工程习惯是:不管是写数据还是写状态寄存器,统一走“CS 拉低 → 发 0x06 → CS 拉高 → 再发真正的写命令”这套流程。就算某些操作手册没有强制要求先 WREN,多这一条指令也不会出错,而且能防止换用其他同类芯片时踩坑。
0x06 的时序有个容易出错的地方:WREN 结束后 CS 必须拉高,WEL 位才会真正锁存。如果你发完 0x06 后没有释放 CS,芯片可能不会把这次使能当成一次完整指令。我一开始就是在这里栽过跟头,读状态寄存器 WEL 一直是 0,后来查数据手册才发现,CS 的上升沿才是指令确认的边界。
状态寄存器里有 WIP 位和 WEL 位。MRAM 写入是即时生效的,没有 Flash 那种“擦除/编程中”的长时间忙状态,所以轮询 WIP 意义不大。我实际的驱动里不等待写完成,而是写完立刻读回校验。这个差异和 Flash 是完全不同的思路,后面测试部分会细说。
2.3 关于页面大小、地址范围和上电时序
MR25H40CDF 容量 512KB,地址范围是 0x00000 到 0x7FFFF。SPI 指令里的地址是 24 位,高字节高位补 0 即可。要注意不要因为“4Mbit”就下意识认为最大地址是 0xFFFFF,实际字节容量是 512KB,超过 0x7FFFF 的地址都在访问不存在的空间。
单次写指令能连续写入的字节数,数据手册里有限制,通常和页大小相关。我习惯按页边界拆分写操作,跨越页边界时拆成两次写指令。虽然 MRAM 表面上看是随机写入,但 SPI 接口本身依然有突发长度的限制,不相信“无限写”就可以突破协议边界。
上电时序也值得提。MRAM 上电后,VCC 必须稳定在手册规定的范围内,芯片才能可靠响应指令。这里有个实用技巧:MCU 初始化时,先把 CS、WP、HOLD 三个引脚全部拉到确定电平,再执行第一次读状态寄存器。第一次读不为了取数据,就是为了确认 SPI 通信链路是通的,同时把芯片从不确定的上电状态拉回正常指令流。读不到预期值时,不要急着换芯片,先查这几根信号线的默认电平。
3. STM32F411RE 侧搭建:SPI 初始化、GPIO 复用和读写函数封装
3.1 选 SPI1 还是 SPI2:从时钟域和项目结构说起
STM32F411RE 是 Cortex-M4F 内核,主频可以跑到 100MHz,带 FPU。这颗芯片的 Flash 有 512KB,SRAM 有 128KB,外设资源在中等容量 MCU 里相当够用。它的 SPI1 挂在 APB2 总线上,时钟上限 100MHz;SPI2 挂在 APB1 总线上,时钟上限 50MHz。
MR25H40CDF 的 SPI 时钟上限我没有一直顶着跑,最初用 12.5MHz,后面验证余量充足才提高到 25MHz。如果未来想逼近芯片的极限速度,建议选 SPI1,因为它的时钟源更高,分频选择更从容。另一个考虑是 SPI1 通常在 PA5、PA6、PA7 引脚上,和调试下载口、串口默认引脚的冲突相对少一些,布局更灵活。
片选脚我不用硬件 NSS,而是用普通 GPIO 软件控制。硬件 NSS 在外设初始化、DMA 配置等环节可能被自动拉低,一旦控制不好会出现意外片选脉冲。软件控制 CS 虽然多几条代码,但每一笔读写都是确定性操作,工业代码里确定性比省事重要得多。
3.2 HAL 库初始化与关键参数说明
如果用 STM32CubeMX 生成工程,SPI1 的配置很简单。模式选 Master,数据宽度 8 位,时钟极性 CPOL=0,时钟相位 CPHA=0,也就是 SPI Mode 0,NSS 选 Software,MSB First。发送和接收都开启。
关键参数是波特率预分频。STM32F411RE 的 SPI1 在 APB2 上,APB2 若配置为 100MHz,预分频 8 就是 12.5MHz。示例代码里我用的是 SPI_BAUDRATEPRESCALER_8,稳妥优先。
static 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_CLOCK_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_CLOCK_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi1); }GPIO 初始化里,PA5 是 SCK,PA6 是 MISO,PA7 是 MOSI,全部复用为 AF5。CS 用 PA4,配置成推挽输出,初始化时直接拉高。
static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_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; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); }初始化顺序有个细节:在 SPI 外设初始化之前,先要把 CS 引脚拉高。因为 SPI 外设初始化过程中,复用功能还没完全生效,如果此时 CS 是低电平,MRAM 可能把 GPIO 抖动当成片选信号。先把 CS 置高,再打开 SPI 时钟,最后配置复用功能,这样上电过程最干净。
3.3 三段式驱动:写使能—写入—读回校验
驱动代码我用的是 HAL 库,核心就三个函数:写使能、读数据、写数据。
#define MR_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define MR_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) static void mr_write_enable(void) { uint8_t cmd = 0x06; MR_CS_LOW(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); MR_CS_HIGH(); } void mr_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t hdr[4] = {0x03, (uint8_t)((addr >> 16) & 0xFF), (uint8_t)((addr >> 8) & 0xFF), (uint8_t)(addr & 0xFF)}; MR_CS_LOW(); HAL_SPI_Transmit(&hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); MR_CS_HIGH(); } void mr_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t hdr[4] = {0x02, (uint8_t)((addr >> 16) & 0xFF), (uint8_t)((addr >> 8) & 0xFF), (uint8_t)(addr & 0xFF)}; mr_write_enable(); MR_CS_LOW(); HAL_SPI_Transmit(&hspi1, hdr, 4, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, len, HAL_MAX_DELAY); MR_CS_HIGH(); }写数据的逻辑就是“先写使能,再发写指令”。命令头 0x02 后面跟着 24 位地址,然后是数据。MRAM 没有擦除步骤,也不需要等待内部编程完成,数据从 SI 引脚进芯片的同时就已经写进存储单元了。所以写完立刻读回,理论上不会有 Flash 那种“写入完成但校验失败”的延迟问题。
读数据更直接:CS 拉低,发 0x03 和地址,然后开始收数据,收完拉高 CS。如果读的数据量很大,要注意单次读突发长度限制,超出长度后要么在驱动层拆分,要么在应用层保证不超过页大小。
需要注意的是,HAL_SPI_Transmit 的 len 参数是 uint16_t,如果单次写入超过 65535 字节,就要在调用侧拆分。不过 MR25H40CDF 总共才 512KB,正常项目里单次读写很少超过几 KB,这个限制基本不会踩到。
4. 实际读写回环和掉电保存测试:能直接抄作业的验证流程
4.1 回环测试步骤与判定标准
驱动写完不能直接上产线,先做一轮严格回环测试。我当时的测试流程分四步,每一步都有明确判定标准。
第一步是固定数据写入读回。向 0x00000、0x3FFFF、0x7FFFF 这几个关键地址写入全 0xAA、全 0x55、递增序列,然后重新上电读回,逐字节比较。这个测试主要确认地址线和数据线没有接错。
第二步是随机地址写读回环。用随机数生成器产生地址和数据,连续执行写和读,记录错误次数。我跑了 10 万组,0 错误。
第三步是连续覆盖写入。在固定一块区域内反复写同样长度的数据,比如 4KB 缓冲区,用不同 pattern 交替覆盖 100 万次。Flash 在这种情况下早就磨损到报废了,MRAM 完全无压力,读回数据始终一致。
第四步是页边界穿越测试。在 0x1FF 和 0x200 之间、0x3FF 和 0x400 之间等页面边界写入跨边界数据,确认实现层没有把跨页数据写错。这个测试最容易暴露驱动只处理单页、没做边界拆分的隐患。
这轮测试我建议在开发板上先跑通,再移到实际产品主板上跑一遍。因为开发板走线和主板的电磁环境不一样,MRAM 这种器件虽然抗干扰不错,但只有实物验证过才安心。
4.2 掉电与重启后的数据完整性验证
回环测试通过后,最关键的掉电测试来了。我的做法是在代码里写一个“掉电标记测试例程”:先写入一串带 CRC 的标记数据,然后不等任何确认立即断电;重新上电后读回,校验 CRC。重复 500 次,结果全部通过。
这里有个和 Flash 方案完全不同的体验:以前用 Flash 做掉电保护,还要考虑“写入是否完成”“擦除是否完成”“误写了半扇区怎么办”。MRAM 没有这些顾虑,只要 CS 时序完整,数据就是立刻写进去的。掉电瞬间真正要防的是 MCU 自身复位产生的 GPIO 乱序,而不是 MRAM 的写入等待时间。
掉电测试的电路也有讲究。不要用手动开关反复拔插电源,那种操作重复性差,还容易把接口弄坏。我建议用一个可控电子开关或者继电器,由另一个 MCU 控制断电时机,这样才能模拟出“数据正在写入时突然掉电”的恶劣场景。
测试判定的核心指标有两个:一个是掉电后数据 100% 保留,另一个是多次异常掉电后芯片仍然能够正常写入。只要这两条过了,工业现场的突然断电场景基本就有底了。
4.3 温度环境和长期运行观察
工业设备不像消费电子,环境温度经常超出常规。我当时把测试板放进高低温箱,在 -40℃ 和 +85℃ 各保温 4 小时,期间执行读写回环,然后在两种温度下分别做掉电测试,数据全部一致。
长期运行观察则是在常温下连续跑 72 小时写读日志,每 10 秒写入一条带时间戳的记录,读回后检查连续性。MR25H40CDF 的写入速度足够快,12.5MHz 的 SPI 频率下,写一条 32 字节的记录只需要几十微秒,完全不影响主循环的实时性。
温度测试里发现的一个小问题是,低温下 SPI 线速过高时信号边沿会变差。我把 SPI 时钟从 25MHz 降到 12.5MHz 之后,读回数据波形明显更稳。如果你的产品要过低温认证,不妨一开始就保持 12.5MHz,不要为了追求性能把时序余量留太紧。
5. 工业现场常见的坑和排查方法
5.1 CS 毛刺和 WP 悬空引发的不定时写入
MRAM 写数据太“容易”了,这是它最大的优点,也是它和 Flash 在使用习惯上最大的差异。Flash 写入前有擦除过程,误触发写指令的后果相对滞后;MRAM 指令对上的瞬间数据就变了。所以周围噪声导致 CS 出现毛刺时,MRAM 比 Flash 更容易出现“意外写入”。
我处理这个问题有三层措施。第一层,CS、WP、HOLD 三根线全部上拉,让芯片在上电和静态状态下处于“不选中、不写保护、不暂停”的稳态。第二层,在 MCU 代码里,只有进入写函数时才允许 CS 拉低,其他时间 CS 引脚始终输出高电平。第三层,PCB 布线时 SCK 和 CS 不要走平行长线,避免时钟边沿耦合到 CS 上。
WP 引脚也要特别确认。如果 WP 悬空,芯片可能随机处于写保护状态,导致明明发了 WREN,写操作却不生效。更麻烦的是,有些环境噪声会反复切换 WP 电平,造成系统时而能写、时而不能写。所以 WP 要么直接接高,要么接 IO 明确控制。每一个“莫名写不进去”的 bug,排查时第一件事就是量 WP 电平。
5.2 沿用 Flash 驱动导致的“地址和时序”错乱
很多同事第一次接 MR25H40CDF 时,直接复制了 Flash 的驱动代码,然后遇到两个典型问题。
第一个是写前执行“擦除扇区”指令。SPI MRAM 根本不存在擦除指令,Flash 的 0x20 扇区擦除、0x52 块擦除,MRAM 都不支持。这些指令发出去不会报错,但后续读写数据全乱。解决办法是把驱动完全按 MRAM 手册重写,不要试图在 Flash 驱动上打补丁。
第二个是地址范围习惯性按“扇区/块”组织。Flash 靠擦除均衡来分配地址,MRAM 不需要擦除,地址随便用。512KB 空间可以理解成一个大数组,任意字节任意时刻都能写。沿用 Flash 的“先擦后写”“整块覆盖”逻辑,不仅多余,还可能掩盖真正的 bug。
我在项目里把 MRAM 驱动单独封装成一个模块,对外提供 mr_read、mr_write、mr_erase 三个接口。mr_erase 在 MRAM 里其实是假接口,直接返回成功。这样上层应用代码不用大改,底层实现却彻底替换了。
5.3 数据一致性和软件冗余设计
MRAM 虽然硬件存储可靠,但 SPI 通信本身仍可能受到干扰。上位机读写过程中如果发生中断抢占,CS 释放时机被延迟,可能造成一次完整指令被拆成两截,数据就写坏了。我在写函数内部会关闭可屏蔽中断,写完成后再恢复,确保 CS 释放时指令流是完整的。
另一个数据一致性策略是给每条记录加上“长度 + CRC + 序号”。读取时先校验 CRC,再判断序号是否比当前值大。如果读到一条 CRC 错误且序号奇怪的记录,就认为它是一次不完整写入,直接跳过或用备份数据。这套逻辑和 MRAM 本身无关,但在工业现场能显著提升系统的鲁棒性。
我还做了一个电子工程里常见的“双备份”设计。关键配置参数同时存放在 MRAM 的 A 区和 B 区,写入时先写 A 区再写 B 区,读回时优先读 A 区,A 区校验失败才读 B 区。这样即使发生一次极端情况下的半写,系统也能从备份区恢复。
6. 如果还要继续扩展:分区管理、双备份、DMA 记录
6.1 把 512KB 空间拆成配置区、日志区、暂存区
MR25H40CDF 的 512KB 空间不要拿来当一个大数组随便用,建议按照功能拆成清晰的逻辑分区。我常用的布局是这样的:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 配置区 A | 0x00000 | 64KB | 用户参数、校准数据 |
| 配置区 B | 0x10000 | 64KB | 配置区 A 的备份 |
| 运行日志区 | 0x20000 | 384KB | 循环记录运行状态、事件 |
| 暂存区 | 0x80000 上限以下 4KB | 4KB | 临时计算、协议缓存 |
配置区故意预留备份区,是因为参数区的数据一致性要求最高。日志区和暂存区则可以放心反复擦写,MRAM 没有磨损问题,不需要为磨损均衡多花心思。
日志区采用追加式写入,每条记录固定长度,记录头包含魔数、序号、时间戳和 CRC。读日志时从当前写指针向前扫描,遇到魔数不对就停止。这套方式比“固定位置反复覆盖”更可靠,因为它天然保留了最近一次完整写入的现场。
6.2 双备份与 CRC 校验的落地实现
双备份的真正意义不在于 MRAM 会坏,而在于系统可能在任何不恰当的时间复位。软件设计上,写入配置区的过程可以拆成四个动作:计算 CRC → 写 A 区 → 读 A 区校验 → 写 B 区。读回时先校验 A 区,失败再校验 B 区,两边都失败才恢复默认值。
CRC 算法可以选择 CRC16 或 CRC32。STM32F411RE 是 Cortex-M4F 核,软算 CRC 并不慢,但如果你希望节省 CPU,也可以直接用 MCU 自带的硬件 CRC 外设。我在项目里用软件查表法实现 CRC16,每次校验几百字节数据耗时几十微秒,完全可以接受。
更进一步的可靠性做法是给每条日志记录附加全局递增序号。读取时比较序号大小来判断哪条记录更新,避免因为时间戳回退而误判。这个设计在双备份、掉电恢复、数据合并三种场景下都是统一逻辑。
6.3 DMA 循环记录与中断安全
如果你想让 STM32F411RE 边采集边记录,同时不想让 SPI 读写拖慢主流程,可以考虑用 DMA 配合 MRAM。具体做法是:用定时器触发 ADC 采样,采样结果通过 DMA 搬运到内存缓冲,再把缓冲数据通过 SPI DMA 写入 MRAM 的日志区。
DMA 模式下 CS 控制要特别小心。SPI DMA 传输结束有完成中断,但 CS 拉高的时机必须在最后一个字节真正移出 SPI 移位寄存器之后。我习惯在 DMA 传输完成中断里先读取 SPI 状态寄存器的 BSY 位,确认移位寄存器空闲后再拉高 CS。跳过去这一步,可能偶发“最后一个字节没写完 CS 就释放”的隐患。
中断安全的另一个要点是写指针的原子更新。日志写指针如果被主循环和中断同时修改,可能出现指针错位。我把它放在关中断临界区里更新,或者用一个无符号 32 位变量,利用单次写操作的原子性来降低风险。
扩展功能做到这个阶段,MR25H40CDF + STM32F411RE 的组合就不再只是简单的读写操作了,它已经成了整个系统的数据中枢。调完 DMA 记录后还有个小技巧:写日志时把长度信息放在记录尾部,读日志时先扫魔数定位起始位置,再根据尾部长度字段确认记录的完整性。这个顺序能避免读到半条日志时误判整条数据,实际使用中相当顺手。