最近给一台现场采集终端换存储方案,让我对“掉电不丢数据”这件事有了完全不一样的看法。原来的终端用的是EEPROM,每秒写一组状态数据,结果两个多月后数据开始成片错乱,查到最后发现是同一个扇区的擦写次数早就超过了标称值。后来我把存储介质换成了Everspin的MR25H40CDF,主控用STM32F415ZG,整个存储和读取流程重新做了一遍,跑到现在稳稳当当。
MR25H40CDF是一颗4Mbit的SPI接口MRAM,STM32F415ZG则是Cortex-M4F内核、带硬件AES加速的工业级MCU。这个组合在工业嵌入式场景里做数据记录、参数保存、掉电快速存储,属于那种“用完之后就回不去”的方案。这篇文章就把整套方案讲透:从为什么不用Flash,到硬件接线、CubeMX配置、驱动代码、数据区设计,再到我在调试过程中踩过的坑,一次全列出来。适合正在做工业数据采集、嵌入式存储,或者准备把老项目里的EEPROM方案升级一轮的开发者参考。
1. 为什么我在采集终端里放弃了EEPROM/Flash,换上MRAM
1.1 从一次数据错乱说起
那台终端的故障现象很典型:连续运行两个月后,日志数据开始出现整段的00和FF交替,有些记录直接丢失,设备没有报任何错误。我一开始怀疑是SPI线路干扰,焊了几根飞线换到短距离测试,问题依旧。后来把Flash内容整个读出来,再对着时序图分析,才发现是某一块区域的擦写计数早就爆炸了。
这和传统EEPROM/Flash的存储原理有关。它们的存储单元本质上是“电荷陷阱”,写入就是给浮栅充放电,擦除则是把整个块里的电荷全部清掉。每一次擦写都会对氧化物介质造成不可逆的损伤,于是芯片厂商在手册里白纸黑字标明了写入寿命上限。普通EEPROM的标称擦写寿命大概是100万次,SPI NOR Flash更保守,常见的也就1万到10万次。在“长时间高频写入”的工业记录场景里,这个寿命根本经不起算。
1.2 EEPROM/Flash的写入痛苦:先擦除、有寿命、怕掉电
除了寿命问题,Flash还有另一个非常膈应人的点:必须先擦除再写入。
NOR Flash的物理结构决定了它只能把1写成0,想重新写回1,就得先把整个块擦除成全1。于是每次更新一小段数据,驱动层都得先读回整块,在内存里修改,然后擦除整块,再整块写回去。这个过程既吃时间又吃寿命,还引入了一个巨大的掉电风险窗口——如果擦除到一半断电,这块数据基本就废了,轻则丢一小段,重则整块变砖。
EEPROM倒是不用块擦除,可以字节级覆盖写,但它慢,单字节写周期通常要3到5毫秒,而且同样有100万次的寿命上限。我在终端里最初用的就是EEPROM,每秒写一组几十字节的状态帧,表面上看够用了,实际上100万次除以每天86400次写入,大概11天就烧完了额度。两个月后出问题,完全符合预期。
1.3 MRAM的存储原理与“磁状态”为什么能扛住一切
MRAM的出现把前面这些痛点基本都解决了。
它的存储单元是一个磁性隧道结,两层铁磁体夹着一层极薄的氧化绝缘层。底下一层是固定层,磁化方向不变;上面一层是自由层,磁化方向可以被写入电流产生的磁场翻转。读数据的时候,就是测量这个隧道结的电阻——两层磁化方向平行和反平行时,电阻值明显不同,芯片据此区分逻辑0和1。
关键在于,这个状态是磁化的方向,不是电荷的多少。磁化状态不需要依赖电源维持,断电以后它依然在那里,天然就是非易失的。写数据本质上是改变磁化方向,没有电荷陷阱的物理磨损,所以不存在传统存储器的擦写寿命瓶颈。同时写操作是瞬时的,没有内部“擦除”这个中间状态,掉电窗口比Flash小了好几个数量级。
用大白话形容就是:EEPROM像一块白板,用铅笔画字,反复擦改会留下痕迹,擦多了纸就破了;Flash像一面墙,每次粉刷前得先把旧墙皮彻底铲掉再刷;MRAM则是冰箱门上贴磁性贴纸,想换就揭下来换一张,贴一万次也不会把冰箱门贴坏。
1.4 算一笔寿命账:1秒1次记录,EEPROM只能撑11天
我把寿命账算给团队看的时候,大家都觉得不可思议。其实很简单:
- 1秒记录1次,每天86400次写入;
- EEPROM标称擦写寿命100万次;
- 1000000 ÷ 86400 ≈ 11.6天。
也就是说,如果项目要求“至少运行五年、每秒记一条”,传统EEPROM在结构上就不可能完成。如果降低频率到5分钟一条,100万次能用大约9.5年,看着够了,但这意味着你永远不能提高采样频率,一旦需求改成2秒一次,方案立刻作废。
而MRAM没有擦写次数限制,你可以把它当成一块“掉电不丢的RAM”来用。同一片地址反复原地改写都无所谓,设计存储架构的时候完全不用考虑磨损均衡和GC轮转这类事。对高速数据记录、频繁更新变量、经常掉电的工业现场来说,这不是性能提升,是方案可行性的根本差异。
2. MR25H40CDF底细:引脚、命令表、状态寄存器一次讲清
2.1 这颗4Mbit MRAM的核心参数
MR25H40CDF是Everspin MR25H40系列里的SPI接口版本,容量4Mbit,也就是512KB。工业温度等级覆盖-40℃到+85℃这个常用档位,3.3V供电,支持标准的SPI Mode 0和Mode 3,封装有SOIC-8和DFN-8等,我这边用的是SOIC-8,手工焊接和量产贴片都很方便。
它和普通SPI NOR Flash最大的不同,就是前面说的“写入无需擦除”。指令集却和NOR Flash高度兼容,这意味着你原来写的Flash驱动框架不用推翻,只需要替换底层写实现。对从Flash方案迁移过来的工程师来说,上手成本很低。
2.2 引脚功能与上拉要求
8个引脚里,除了电源和SPI四线,有两个脚特别容易忽视:WP和HOLD。
- CS:片选,低有效。
- SCK:SPI时钟。
- SI:数据输入,接MCU的MOSI。
- SO:数据输出,接MCU的MISO。
- WP:写保护输入,低电平时禁止写状态寄存器。
- HOLD:保持输入,低电平时芯片暂停通信,SO输出高阻,正在进行的指令被冻结。
WP和HOLD内部都有上拉,但在工业现场我不建议依赖芯片内部弱上拉,外面再接一个10kΩ电阻到VDD更稳妥。尤其是HOLD脚,悬空状态一旦被附近走线耦合出毛刺,芯片会突然“暂停”,表现成SPI通信偶发卡死,这种故障排查起来非常折磨人。后面我会专门讲这个坑。
2.3 SPI命令集:与SPI NOR高度兼容,但写方式完全不同
MR25H40CDF的命令集可以列一张表:
| 命令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写操作前置命令 |
| WRDI | 0x04 | 写禁用 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 普通读 |
| FSTRD | 0x0B | 快速读 |
| PP | 0x02 | 页编程,即写入数据 |
| SE | 0x20 | 扇区擦除 |
| BE | 0xD8 | 块擦除 |
| CE | 0xC7 | 整片擦除 |
| DS | 0xB9 | 深度睡眠 |
擦除命令虽然存在,但MRAM的设计目的是让你根本不用擦除。芯片里保留这些命令主要是为了兼容已有的Flash软件栈,以及方便快速把整片复位成全FF状态。平时做数据更新,只需要用PP命令直接覆盖写就行。MR25H40的页大小是512字节,PP命令一次可以写入1到512字节,超过512字节或跨页边界时地址会回卷,这一点和Flash一样,后面会对它做专门处理。
READ和PP的地址都是24位,但4Mbit容量实际只用了低19位,地址范围0x000000到0x07FFFF,超出的高位地址忽略。写驱动的时候仍然建议完整发送24位地址,这样以后如果直接换成大容量同系列芯片,驱动不用改动。
2.4 状态寄存器:WEL、BP位和写保护逻辑
状态寄存器这8个bit,写驱动的时候必须摸清楚:
- bit0:WIP,写进行中标志。MRAM写入是瞬时的,这个位基本看一眼就变0,但流程上保留等待逻辑更稳妥;
- bit1:WEL,写使能锁存位。只有先发WREN,WEL变成1,接下来的PP/WRSR命令才会被执行;
- bit2-bit4:BP0-BP2,块保护位,可以组合出不同的保护地址范围;
- bit5:WPEN,配合WP引脚实现硬件写保护;
- bit6-bit7:保留位,读出来是0。
块保护功能在实际产品里很有用。比如把参数区配置成BP保护之后,上层应用程序即使出现Bug误写,也无法破坏关键数据。但要注意,WP脚低电平锁的是状态寄存器的写入,并不直接锁主存储区的PP命令,真正要锁数据区还是要靠设置BP位。
3. STM32F415ZG的硬件连接与SPI初始化:接线和时钟里的门道
3.1 接线表与布局建议
STM32F415ZG的SPI1可以很方便地映射到PA4-PA7这组引脚。对应关系如下:
| MR25H40CDF引脚 | 功能 | 接STM32F415ZG | 备注 |
|---|---|---|---|
| CS | 片选 | PA4(GPIO输出) | 软件NSS控制 |
| SCK | 时钟 | PA5(SPI1_SCK) | AF5复用 |
| SI | 数据输入 | PA7(SPI1_MOSI) | MCU输出接芯片输入 |
| SO | 数据输出 | PA6(SPI1_MISO) | 芯片输出接MCU输入 |
| WP | 写保护 | VDD,经10kΩ上拉 | 低有效 |
| HOLD | 保持 | VDD,经10kΩ上拉 | 悬空是重大隐患 |
| VDD | 电源 | 3.3V | 并0.1μF+4.7μF电容 |
| VSS | 地 | GND | 共地 |
接线时注意SI和SO的方向,新手最容易在这里犯迷糊:SI是“Serial Input”,站在芯片角度看输入,所以要接MCU的MOSI;SO是“Serial Output”,接MCU的MISO。我见过不止一次有人把两条线焊反,然后读回来的数据全是0xFF。
布局上,CS、SCK、SI这几条线尽量短,尤其CS线不要绕大圈。SCK和SI上可以各串一个33Ω电阻,用来抑制边沿过冲,对EMI和信号完整性都有帮助。如果板子上的干扰源比较多,CS线上还可以加一个1kΩ电阻配100pF电容的RC滤波,但要注意不要把CS边沿拖得太慢,中等SPI速率下没问题,高速跑的话就保守一点。
3.2 为什么SPI时钟我选21MHz而不是跑到上限
STM32F415ZG的SPI1挂在APB2总线上,PCLK2最高84MHz,因此SPI1理论上可以跑到42MHz。MR25H40CDF手册给的普通READ最大SPI时钟是40MHz,快速读FSTRD可以到50MHz。看着42MHz贴边也能跑,但我最终把SPI1分频设成了4,也就是21MHz。
理由很朴素:规格书里的40MHz是理想环境下测出来的上限,实际板子的走线阻抗、寄生电容、电源纹波都会压低这个值。工业现场有温度变化、连接器接触电阻、附近继电器通断带来的干扰,满速跑等于把余量全部吃干净。21MHz下写满一个512字节页也只要大约0.2ms,对这个应用来说完全够快,没必要追求极限。工程上这叫降额使用,在一些可靠性要求高的行业里是明文规定。
如果你确实想跑更高的时钟,至少要保证SCK走线短而直、信号完整性经过验证,并且回读校验必须全绿。
3.3 CubeMX配置要点与软件NSS的选择
用STM32CubeMX生成工程时,SPI1的配置项这样设:
- Mode:Full-Duplex Master
- Hardware NSS Signal:Disable
- Data Size:8 bit
- First Bit:MSB First
- Baud Rate:21.0 MBits/s(也就是4分频)
- Clock Polarity:Low
- Clock Phase:1 Edge(对应CPOL=0,CPHA=0,即Mode 0)
PA4单独配置成GPIO输出,初始电平拉高。PA5/PA6/PA7会自动复用成SPI1功能,GPIO速度建议选Very High,因为引脚要跑21MHz的时钟。
软件NSS比硬件NSS更适合这种场景。硬件NSS由SPI外设自动管理,在某些库版本里会在接收模式下自动拉低片选,应用层反而不好控制时序。软件NSS就是一根普通GPIO,什么时候拉低、什么时候拉高完全由驱动决定,多设备挂同一条SPI总线时尤其好用。MRAM的CS时序要求其实很简单:命令期间保持低电平,命令结束后拉高,芯片即认为一个SPI事务结束。只要保证这一点,用软件控制就很可靠。
4. 驱动代码设计与实现:从底层SPI收发到完整读写API
4.1 底层封装与命令定义
驱动层面,我习惯分成三层:最底层是CS控制和单字节收发,中间是命令执行,最上层才是面向业务的读/写接口。先用宏把命令码定下来:
#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_WRSR 0x01 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_FSTRD 0x0B #define MRAM_CMD_PP 0x02 #define MRAM_CMD_SE 0x20 #define MRAM_CMD_BE 0xD8 #define MRAM_CMD_CE 0xC7 #define MRAM_CMD_DS 0xB9 #define MRAM_PAGE_SIZE 512u #define MRAM_CAPACITY 0x80000u /* 512KB */ #define MRAM_CS_LOW() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET) #define MRAM_CS_HIGH() HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET)底层单字节收发用HAL的HAL_SPI_TransmitReceive,它同时发送一个字节并接收一个字节:
static uint8_t mram_xfer(uint8_t out) { uint8_t in = 0; HAL_SPI_TransmitReceive(&hspi1, &out, &in, 1, 10); return in; }所有命令的CS操作都在这层统一管理,上层接口里不要再到处手动拉CS,否则一旦漏了拉高或拉低,排查起来很痛苦。
4.2 基础命令:写使能、读状态、深度睡眠
写使能必须在每次PP或WRSR命令之前执行一次,这是MRAM兼容NOR Flash的关键设计。WREN命令把状态寄存器的WEL位拉起来,芯片才允许写操作。你可能觉得多此一举,但它能防止系统上电瞬间的噪声误触发写指令,属于硬件保护机制:
void mram_write_enable(void) { MRAM_CS_LOW(); mram_xfer(MRAM_CMD_WREN); MRAM_CS_HIGH(); } void mram_write_disable(void) { MRAM_CS_LOW(); mram_xfer(MRAM_CMD_WRDI); MRAM_CS_HIGH(); }读状态寄存器是排查问题的主要手段,调试时只看返回值里WEL和WIP两个位就能判断芯片处于什么状态:
uint8_t mram_read_status(void) { uint8_t st = 0; MRAM_CS_LOW(); mram_xfer(MRAM_CMD_RDSR); st = mram_xfer(0x00); MRAM_CS_HIGH(); return st; }写状态寄存器用来设置BP块保护位。注意它前面也要先发WREN,很多从EEPROM驱动移植过来的人很容易漏这一步:
void mram_write_status(uint8_t st) { mram_write_enable(); MRAM_CS_LOW(); mram_xfer(MRAM_CMD_WRSR); mram_xfer(st); MRAM_CS_HIGH(); }深度睡眠模式适合设备长时间不访问MRAM的低功耗场景,进入睡眠后芯片功耗可以压到极低:
void mram_enter_deep_sleep(void) { MRAM_CS_LOW(); mram_xfer(MRAM_CMD_DS); MRAM_CS_HIGH(); }唤醒时序不同版本手册表述不完全一致,我吃过这个亏,所以不建议你在没仔细读手册的情况下直接照抄网上的唤醒代码。稳妥做法是把唤醒等待时间留足,统一延时至数百微秒级别,保证芯片完全退出深度睡眠状态再执行第一笔操作。
4.3 读数据:3字节地址与连续读
MRAM的读操作没有页边界限制,只要CS一直保持低,芯片就会按地址递增持续输出数据,非常适合批量读取日志和参数块。先发命令字节加3字节地址,然后开始接收数据:
void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t hdr[4]; hdr[0] = MRAM_CMD_READ; hdr[1] = (uint8_t)((addr >> 16) & 0xFF); hdr[2] = (uint8_t)((addr >> 8) & 0xFF); hdr[3] = (uint8_t)(addr & 0xFF); MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, hdr, 4, 100); HAL_SPI_Receive(&hspi1, buf, len, 1000); MRAM_CS_HIGH(); }FSTRD快速读命令只是多了几个dummy周期,适合在更高时钟频率下读取,我这里用普通READ配合21MHz时钟已经足够,没有额外使用。
4.4 写数据:页写、跨页边界处理与读回校验
写接口的核心是处理页边界。PP命令一次能写1到512字节,但地址一旦跨过512字节边界,会自动回卷到当前页的页首,把页开头的数据覆盖掉。这不是芯片设计缺陷,而是兼容Flash的约定,驱动层必须把它处理好。
我写了一个跨页安全的写接口,内部循环按页拆分写入:
int mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t hdr[4]; uint32_t chunk; while (len > 0) { uint32_t page_off = addr & (MRAM_PAGE_SIZE - 1); chunk = MRAM_PAGE_SIZE - page_off; if (chunk > len) { chunk = len; } mram_write_enable(); hdr[0] = MRAM_CMD_PP; hdr[1] = (uint8_t)((addr >> 16) & 0xFF); hdr[2] = (uint8_t)((addr >> 8) & 0xFF); hdr[3] = (uint8_t)(addr & 0xFF); MRAM_CS_LOW(); HAL_SPI_Transmit(&hspi1, hdr, 4, 100); HAL_SPI_Transmit(&hspi1, (uint8_t *)buf, chunk, 1000); MRAM_CS_HIGH(); addr += chunk; buf += chunk; len -= chunk; } return 0; }这个接口已经能处理任意地址、任意长度的写入了。但在工业环境里,我会再叠加一层读回校验。MRAM本身不会写坏,但不代表整条SPI链路不会出错——电源毛刺、总线串扰、连接器氧化,都可能让数据在传输过程中被篡改。校验函数把写入内容读回来比对,失败就返回错误,由上层决定重试:
static int mram_verify(uint32_t addr, const uint8_t *buf, uint32_t len) { uint8_t rd[64]; uint32_t off = 0; while (off < len) { uint32_t n = (len - off > sizeof(rd)) ? sizeof(rd) : (len - off); mram_read(addr + off, rd, n); if (memcmp(rd, buf + off, n) != 0) { return -1; } off += n; } return 0; }生产环境里的写入流程我建议这样组织:准备数据 -> 执行写使能 -> 页写 -> 读回校验,校验失败就整体重试,重试3次仍失败则上报错误并停止写入。这样既不会因为单次链路错误丢数据,也不会陷入无限重试的死循环。
5. 工业数据区设计:环形日志、掉电保存和加密落盘
5.1 参数存储区:结构体、Magic与CRC
工业设备的参数区(校准值、配置、序列号)更新频率不高,但绝对不允许写坏。我用一个结构体加固定头部来管理:
typedef struct { uint32_t magic; /* 固定写0xA55A5AA5 */ uint16_t version; /* 结构体版本号 */ uint16_t length; /* 有效数据长度 */ uint32_t seq; /* 更新序号 */ uint8_t data[64]; /* 实际业务参数 */ uint32_t crc32; /* 对前面所有字节的CRC32 */ } sys_param_t;每次更新前先判断magic是否正确,新数据写入相邻的槽位,两个槽位里谁的seq新就用谁。因为MRAM原地改写没有寿命负担,也可以直接用单槽反复覆盖,Broken的中断写入靠crc32识别,下次启动时读出来不合法就回退到上一次备份。相比Flash需要“备份区搬家”,MRAM的实现简单太多了。
5.2 环形日志区:写指针原地更新,省掉Flash的GC轮转
日志记录这种高速写入场景,最能体现MRAM的架构优势。我把512KB空间分成两块:
- 0x00000 - 0x0FFFF:系统参数区,64KB;
- 0x10000 - 0x7FFFF:环形日志区,448KB。
环形日志区里有几个“元数据变量”:当前写指针、起始地址、已用块数、最后写入的帧序号。原本这些指针在Flash方案里是非常麻烦的——指针本身也要频繁更新,如果放在Flash里,得用一个专门的块来做磨损均衡,每更新几次指针就换一个地址,写地址映射逻辑能写一整个模块。
用MRAM之后,写指针直接固定在参数区的一个地址上,原地覆盖就行。每次写入一条日志帧,流程就是:
- 从MRAM读出上次帧序号,加1;
- 在日志区当前写指针处写入帧数据;
- 更新MRAM中的写指针和帧序号。
日志区写满就回绕到起始地址,覆盖最老的数据。整个过程没有擦除、没有GC、没有块搬运,代码量砍掉一大半。这对数据记录类产品来说,省下的开发时间相当可观。
5.3 掉电瞬间的最后写入:PVD中断+MRAM瞬时写
工业现场掉电是不可预防的,能做的是把掉电瞬间的损失降到最低。STM32F415ZG内置了PVD可编程电压检测器,可以在CubeMX里把阈值设成2.9V左右,当电源跌到这个阈值以下时进入PVD中断。
PVD中断里只做一件最关键的事:把当前缓冲区里的最新状态帧写入MRAM。正常3.3V供电下,一帧128字节的纯写时间大约60微秒,加上中断响应和SPI开销也不会超过200微秒。只要电路上给VDD留了足够的保持电容(让电压从3.3V跌到2.9V的过程撑住几毫秒),这几百微秒的写入时间绰绰有余。
注意PVD中断里千万别做打印、日志、延时之类的事,SPI操作也要保证不被其他外设抢占。更稳妥的做法是进入中断后先把全局中断关掉,只执行必要的MRAM写入,写完直接进停机模式。
这套机制放在Flash方案里就麻烦多了:Flash写入最快也要几百微秒,擦除甚至要毫秒级,万一掉电正好撞上擦除窗口,整个块可能直接损坏。MRAM的瞬时写入特性让“最后写入”变成了确定性操作。
5.4 加密与校验:F415ZG的AES硬件加速派上用场
有些行业的存储数据涉及商业机密,不能明文落盘。STM32F415ZG相对于同系列的F407,多出来的东西正好是AES/DES硬件加密和真随机数发生器,这不就是为了加密存储准备的。
我的实现思路是:日志帧先用CRC32做完整性校验,再用AES-256-CTR模式加密,MRAM里只存密文。每帧的nonce从MRAM元数据区读出后自增,由于CTR模式下nonce坚决不能重用,而MRAM支持原地修改nonce,这个反复自增的操作对它毫无压力。对称密钥本身存放在MCU内部Flash的OTP区域,即使MRAM被物理拆下来,也无法直接读取明文数据。
AES-CTR有个好处是即使密文某一位在链路传输中被翻转,解密结果也只有对应位出错,不会像CBC那样错误扩散。配合帧尾的CRC32校验,可以定位到具体是那一帧的数据出了问题。
6. 实测与踩坑:HOLD悬空、跨页回卷、WREN漏写的现场复盘
6.1 实测写入/读取耗时
我写代码时用DWT计数器统计过几次关键操作的耗时,SPI时钟21MHz:
| 操作 | 实测耗时 |
|---|---|
| 写128字节日志帧(纯写) | 约60μs |
| 写128字节日志帧(含读回校验) | 约120μs |
| 写512字节页(含读回校验) | 约0.45ms |
| 读512字节 | 约0.21ms |
对比一下AT24C256这类常见EEPROM,写128字节要拆成4次页写,每次最长5ms,合计20ms。也就是说,MRAM写同样长度的数据,比EEPROM快大概150倍。而且这个差距会随数据量增大进一步拉开,因为MRAM页写上限是512字节,EEPROM只有32字节。
6.2 坑1:HOLD脚悬空导致偶发死锁
这个坑差点让我把SPI驱动整个推翻重写。现象是系统跑一段时间后,读MRAM偶尔返回全0xFF,重新初始化SPI后又恢复正常。因为不是每次操作都复现,一开始还以为是静电干扰。
后来用示波器挂在CS和HOLD脚上盯了很久,才发现HOLD线上有不规律的下冲毛刺。芯片一遇到HOLD拉低就暂停通信,把SPI事务冻结在半路,主机等不到数据就超时返回错误。把HOLD脚接上10kΩ上拉到VDD之后,故障再也没出现过。这个教训我后来写进团队硬件设计规范里:MRAM的WP和HOLD,一律外部上拉,禁止只靠内部弱上拉。
6.3 坑2:SPI跑满速,长线误码
最早我图省事直接把SPI1跑在42MHz,毕竟F415ZG的SPI1最高就是42MHz,MR25H40普通读也标了40MHz,感觉“差一点点没事”。结果就是读回校验偶发失败,有时候连续读一大块数据,中间某几个字节悄悄变掉。
排查过程也很有意思,数据错误没有固定规律,像是随机的。后来把时钟降到21MHz,又在SCK和SI线上串了33Ω电阻,连续读写跑了十几个小时,校验全绿。这里面的道理并不复杂:时钟频率越高,对信号边沿质量和走线阻抗越敏感,机械继电器、电源模块这些工业设备在旁边开关时,耦合噪声会直接落在SPI线上。工业应用还是那句话:降额使用,频繁出错反而更费时间。
6.4 坑3:跨页回卷覆盖,数据“神秘”错乱
日志系统上线第三天,我发现每次写满512字节,后面一大段数据里总有一小段位置的内容是旧数据。最初还以为是写指针算错了,代码都查了一遍没发现问题。最后用逻辑分析仪抓PP命令的波形,才看清楚芯片在跨页时把地址回卷到页首了。
这也是最典型的驱动Bug:当你给PP命令一个地址,如果数据长度跨越页边界,芯片不会自动切到下一页,而是回到当前页开头继续写。解决办法就是我前面写的驱动代码,按页边界把写入拆成多个事务。那次之后,我凡是看到“按块/按页写入”的存储器件,第一件事就是确认驱动有没有处理边界回卷。
6.5 坑4:漏了WREN,命令被芯片无视
有一次把驱动从一个EEPROM工程移植过来,出现了很诡异的现象:读数据一切正常,写操作有时生效有时完全没反应,芯片状态寄存器的WEL位始终是0。
这个问题归结起来就一句话:MRAM和很多NOR Flash一样,写命令之前必须先发WREN。而EEPROM没有这个概念,可以直接发写命令。从EEPROM移植过来的代码,如果没把WREN加进去,芯片会因为WEL未置位而直接忽略后续命令。排查方法很简单,读一下状态寄存器,看到WEL位为0,就知道命令根本没被接受。
6.6 坑5:RTOS下软件NSS被中断撕烂
设备上了RTOS之后,出现过一种新故障:两个任务同时操作MRAM,A任务正在发数据,B任务突然把SPI1的寄存器改掉了,CS也被拉高。SPI总线本身没有总线仲裁,软件NSS全靠前后台配合,一旦并发就会出乱子。
解决办法是给整个MRAM读写过程加一把互斥锁,二值信号量就可以。任务A拿到锁才能拉低CS,过程中任务B即便也调用了MRAM接口,也只能在锁上等待。锁的粒度要覆盖完整的“CS拉低到CS拉高”这个事务,不能只锁发送那一行,因为CS状态和SPI数据是绑定在一起的。
还有一个细节:如果用了DMA传输,DMA中断完成回调里必须释放锁,不要用HAL_SPI_Transmit的轮询超时作为锁释放条件。我见过同事因为把锁放在HAL_SPI_Transmit返回后释放,而DMA还在后台搬运数据,导致下一个任务抢到锁以后直接破坏了还在传输中的事务。
最后再分享一点个人体会。那次把采集终端换成MR25H40CDF + STM32F415ZG之后,数据记录连续跑了几个月没再出错,我也再没有为“写坏了”“擦除中断了”“扇区磨损了”这类问题熬过夜。现在做新项目,只要涉及频繁写变量、掉电保存、长时间连续记录,我都会先问一句:这里真的需要用Flash吗?很多场景MRAM不是贵一点的问题,而是能让整个存储模块简单一大截。如果你手里正好有个被存储寿命卡住的项目,值得把MRAM放进选型表里认真比较一轮。