做工业设备固件的人基本都经历过这种尴尬:一套设备调试好了,客户用了一两个月,某天突然上电发现校准参数全是乱的,或者历史运行记录停在一个时间点再也写不进去。问题往往不在算法,而在存储介质。EEPROM写寿命不够,Flash擦写块又麻烦,SRAM+电池方案在高温下还容易丢数据。我这次想聊的就是一套很实用的组合:用 Everspin 的 MR25H40CDF 这颗 SPI MRAM,配合 STM32F042C6 这颗 Cortex-M0 核心的 MCU,在工业和嵌入式场景里做可靠的数据存储和读取。
这个项目最适合两类人参考:一类是在做工业控制器、仪器仪表、边缘采集设备,需要频繁记录标定数据和运行日志的嵌入式工程师;另一类是刚接触 MRAM 替换 EEPROM/Flash,想知道怎么快速把驱动写出来、怎么避坑的开发者。MR25H40CDF 容量 4Mbit,SPI 接口,不需要擦除,写入速度和读一样快,写寿命比 Flash 和 EEPROM 高好几个数量级。STM32F042C6 则是一颗带 CAN、USB、多串口的入门级工业芯片,价格低、供货稳定,拿来搭一个独立存储模块或者做小系统主控都很合适。这套组合最大的价值就是把“随时写、随便写、写不坏”这件事真正落地到产品里。
1. 项目整体设计与选型思路
1.1 为什么存储会成为工业设备里的隐形瓶颈
很多嵌入式工程师前期选型时注意力都在 MCU 主频、外设数量、算法跑不跑得动上面,存储往往只考虑“能存参数就行”,于是习惯性选一颗 24C02 或者 25AA256。前几个月没问题,等到设备真正进入连续运行状态,问题就来了。
第一个问题是写寿命。普通 EEPROM 擦写寿命标称通常在 10 万次到 100 万次之间,听起来很多,但工业设备经常要每秒钟记录一次运行状态,或者每次上电都保存一次累计计数。一天下来可能上千次写入,几个月就把寿命耗尽。数据手册上标的寿命是擦写次数,不是使用天数,这个换算关系经常被忽略。
第二个问题是写周期。EEPROM 按字节写通常要 3 到 5 毫秒等待时间,Flash 虽然块写快,但是要擦除,擦除一个扇区动辄几十毫秒甚至更久。对强实时系统来说,阻塞在存储操作上是非常难受的。
第三个问题是数据安全问题。掉电瞬间如果正好在写入,EEPROM 和 Flash 都有可能出现飞字节或者数据损坏。需要做掉电检测、双备份、校验恢复这些防御设计,代码复杂度一下子就上去了。
MRAM 正好把这三个问题都绕开了。MR25H40CDF 是磁性随机存储器,核心是磁隧道结,写入不依赖电荷积累,没有擦除这个概念。写一个字节的时间就是 SPI 传输这个字节的时间,不需要等待内部编程完成。写寿命理论上在 10 的 14 次方量级,基本可以认为随便写。掉电时只要写命令和 SPI 时序没被中断,数据就能稳定落进去,不需要先擦后写,所以断点风险大大降低。
1.2 MR25H40CDF 与 STM32F042C6 的组合逻辑
选 STM32F042C6 不是因为它性能强,而是因为它在工业场景里足够“省心”。这颗芯片是 Cortex-M0 内核,48MHz 主频,64KB Flash,8KB RAM,外设包含 4 个 USART、两路 SPI/I2S、I2C、CAN 和 USB FS Device,还有一个 12 位 ADC。对很多工业节点设备来说,单片 F042 就能把通信、采集、控制、存储全包下来。
MR25H40CDF 则是 Everspin 的 SPI 接口 MRAM,容量 4Mbit,512KB 空间,3.3V 供电,工业级温度范围。接口上完全兼容普通的 SPI NOR Flash 命令风格,但有本质区别:读命令是 0x03,写命令是 0x02,不需要擦除命令,也没有页概念。也就是说,你可以随时对任意地址发起写操作,不管这个地址之前写过什么,直接覆盖就是最终状态。
这两个器件配合起来,整个存储架构变得非常简单。我的项目里 MCU 通过一路 SPI 接 MRAM,另外一片 SPI 留作调试或者接传感器,时间和资源都不会被存储器吃掉太多。对于需要记录大量事件日志、震动波形、故障快照的设备,512KB 空间做环形队列已经非常宽裕。
1.3 系统框图与引脚规划
这个项目的硬件连接非常简单,核心就五根线:CS、SCK、MOSI、MISO,再加上电源和地。我用的 STM32F042C6 是 LQFP48 封装,SPI1 的默认引脚可以直接映射到 PA4-PA7,距离近,布线也顺。
| 功能 | MCU引脚 | 方向 | MRAM引脚 | 说明 |
|---|---|---|---|---|
| SPI_SCK | PA5 | 输出 | 6 (SCLK) | SPI时钟 |
| SPI_MOSI | PA7 | 输出 | 5 (SI/SIO0) | 主机输出、从机输入 |
| SPI_MISO | PA6 | 输入 | 2 (SO/SIO1) | 主机输入、从机输出 |
| SPI_CS | PA4 | 输出 | 1 (CS#) | 片选,软件GPIO控制 |
| 写保护 | PB0(或直接接VCC) | 输出/上拉 | 7 (WP#) | 低电平有效,需要允写时拉高 |
| 暂停控制 | PB1(或直接接VCC) | 输出/上拉 | 3 (HOLD#) | 低电平暂停,不需要时拉高 |
CS 这里我强烈建议用普通 GPIO 软件控制,不要用 SPI 硬件的 NSS。原因后面会详细说。WP 和 HOLD 如果不控制,就分别通过 10kΩ 电阻上拉到 3.3V,保证芯片一直处于允许写入且不暂停的正常状态。很多第一次用 MRAM 的人会在 HOLD 引脚上翻车,因为手册里画的是低电平生效,悬空状态下如果板面有毛刺,芯片会莫名其妙不响应。
数据流就一条线:MCU 把需要保存的数据通过 SPI 发送到 MRAM,读取时用读命令连地址带数据一起拿回来。整个存储模块对上层业务来说就是一个支持任意地址覆盖写的大字节数组,不用考虑块对齐、不用考虑擦除耗时,设计逻辑和普通 RAM 一样。
2. MR25H40CDF 这颗存储器值得注意的细节
2.1 存储原理与命令集速览
MRAM 的存储单元结构和工作原理其实不复杂:每个 bit 由一个磁隧道结构成,两个磁性层的磁化方向相同表示 0,相反表示 1。写数据时通过电流改变自由层的磁化方向,读数据时测量隧道电阻的变化。因为状态由磁化方向决定,断电后信息不丢,和 Flash 靠浮栅电荷存储是两码事。
对嵌入式开发者来说,关心原理的目的只是为了理解两个特性从哪来:为什么不用擦除,为什么写寿命几乎无限。因为改变磁化方向不会损耗材料,所以反复写入不会像浮栅那样把氧化物磨损掉。
MR25H40CDF 的 SPI 命令集和传统 SPI NOR Flash 高度相似,但指令数量更少。我在驱动里只用到下面这些命令:
| 命令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,任何写操作前必须先发 |
| WRDI | 0x04 | 写禁用 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据,命令+24位地址+连续数据 |
| WRITE | 0x02 | 写数据,命令+24位地址+连续数据 |
| RDID | 0x9F | 读ID,返回3字节设备识别码 |
| SLEEP | 0xB9 | 进入睡眠模式 |
| WAKE | 0xAB | 唤醒 |
容量 4Mbit,对应 512KB。地址是 24 位,命令格式里要发满 3 字节,低 19 位有效,最高几位固定填 0 或者忽略。读和写都可以连续操作,地址会自动递增,跨页不限,因为根本没有页边界。这个特性在做日志记录时尤其好用,不像 Flash 还要处理“写到页尾就得换块”的逻辑。
2.2 状态寄存器、写保护与HOLD引脚的坑
状态寄存器是 8 位,但实际需要关心的只有一个位:bit7 的 WPEN,其他位保留。WPEN 和 WP# 引脚共同决定写保护是否生效,逻辑关系如下:
| WPEN 值 | WP# 引脚电平 | 主存储区写操作 | 状态寄存器写操作 |
|---|---|---|---|
| 0 | 任意 | 允许 | 允许 |
| 1 | 高 | 允许 | 允许 |
| 1 | 低 | 禁止 | 禁止 |
这个逻辑和很多 SPI Flash 的状态寄存器保护不完全一样。Flash 通常有 BP0-BP3 区块保护位,需要配了区块才生效。MR25H40CDF 没有区块保护,只靠 WPEN 加 WP# 引脚做全芯片写保护。实际使用中我一般保持 WPEN=0,WP# 引脚直接拉高,系统里就没有写保护这回事,写使能后随时可以写。
HOLD# 引脚是另一个容易出问题的点。它在 CS 为低时起作用,HOLD# 被拉低后,芯片会暂停数据发送,此时 SCK 上继续来的时钟会被忽略。如果不小心把 HOLD# 悬空,或者软件复位之后引脚处于浮空状态,一旦干扰把电平拉低,SPI 通讯就会卡死。我测过几片料,现象是回读数据时第一个字节正常,后面的数据全是乱的。排查了很久才发现是 HOLD# 引脚电平不稳定导致的。所以强烈建议硬件上直接 10kΩ 上拉到 VCC。
2.3 从 EEPROM 和 Flash 迁移过来的思维差异
如果你以前写的是 24 系列 EEPROM 或者 W25Q 系列 Flash,换到 MRAM 后最需要改的不是代码,而是思维。
第一,不用再算“擦除寿命”。以前给产品设计参数存储策略时,总要做磨损均衡,把写入位置轮换到不同的地址,避免某一字节总被写。MRAM 不需要。你可以设计一个非常简单的数据结构,每次直接覆盖写固定地址,完全不需要考虑磨损。这能把代码里的坏块管理、磨损均衡、擦写轮询全部删掉。
第二,不用再等写周期。EEPROM 写完要轮询 ACK 或者等 tWR,MRAM 只要 SPI 把数据全部送出,写操作就已经完成了。所以在 F042 的 12Mbit/s SPI 下,理论上写 1KB 数据只需要不到 1 毫秒。实际用 HAL 库因为函数调用和中断有开销,大概 2 到 3 毫秒,但这已经比 EEPROM 的 5 到 20 毫秒快太多了。
第三,地址随便扔。Flash 的页对齐、扇区擦除、搬移数据这些逻辑在 MRAM 上全部不存在。你可以像操作内存数组一样,把任意字节写到任意地址。这个特性让代码可以写得很直接,调试的时候用调试器改几个字节也特别方便。
3. STM32F042C6 侧的 SPI 工程配置
3.1 CubeMX 初始化:引脚复用和软件 NSS
用 STM32CubeMX 初始化工程时,有一个非常容易踩的坑:SPI 外设的 NSS 引脚。很多人配置 SPI1 主模式时习惯性地勾选了硬件 NSS,让 PA4 作为外设的 NSS 输出。之后就会发现 CS 信号不受你控制,每次通信前还要去摆弄 SPI_CR1 寄存器的 SSI 和 SSOE 位,麻烦又容易出错。
我这里的做法是:PA4 完全不配置成 SPI1_NSS,而是配置为普通 GPIO 推挽输出,初始电平拉高。CS 的拉高拉低全部用 HAL_GPIO_WritePin 控制。这样既可以利用外设自动产生 SCK 和 MOSI,又能按自己的节奏控制片选时序。实际上 MRAM 这类 SPI 从机对 CS 的时序要求很简单,命令间隙 CS 拉高一下就行,软件控制比硬件 NSS 更灵活。
CubeMX 里 SPI1 的关键配置如下:
- 模式:Full-Duplex Master
- 数据宽度:8 bit
- 首bit:MSB First
- 预分频:4 分频,SCK 约 12MHz
- CPOL:Low
- CPHA:1 Edge,即 SPI Mode 0
- NSS:Software
MR25H40CDF 支持 Mode 0 和 Mode 3,两个模式都能工作。我选 Mode 0 是因为它兼容性最广,后面想换其他 SPI 器件不用改 SPI 配置。
初始化代码生成后,自动生成的 MX_SPI1_Init 大概长这样:
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_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; HAL_SPI_Init(&hspi1); }PA5、PA6、PA7 全部设置为复用功能推挽,速度等级设成 High。PA4 设置为普通输出,速度等级 High,初始输出高电平。
3.2 驱动代码:从 CS 控制到底层读写
驱动代码的核心就是封装 MR25H40CDF 的几类操作。我习惯分成三层:最底层是 CS 控制和 SPI 收发,中间层是对应命令的封装,最上层是给业务调的读写函数。
CS 控制抽成两个静态函数,后面所有操作都用这两个:
static void MR25H40_CS_Low(void) { HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_RESET); } static void MR25H40_CS_High(void) { HAL_GPIO_WritePin(MRAM_CS_GPIO_Port, MRAM_CS_Pin, GPIO_PIN_SET); }写使能是写操作的第一步。MRAM 和 Flash 一样,写完 WREN 之后才能接受写指令。WREN 命令本身非常简单:CS 拉低,发送一个字节 0x06,CS 拉高。
void MR25H40_WriteEnable(void) { uint8_t cmd = 0x06; MR25H40_CS_Low(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); MR25H40_CS_High(); }读状态寄存器:
uint8_t MR25H40_ReadStatus(void) { uint8_t cmd = 0x05; 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; }读写主存储区的函数是真正会被业务代码高频调用的。写入流程:先发送 WREN,再拉低 CS,发送写命令和 24 位地址,然后连续发送数据,最后拉高 CS。注意地址是高字节在前。
HAL_StatusTypeDef MR25H40_WriteBytes(uint32_t addr, uint8_t *data, uint32_t len) { uint8_t header[4]; HAL_StatusTypeDef ret; header[0] = 0x02; header[1] = (uint8_t)((addr >> 16) & 0xFF); header[2] = (uint8_t)((addr >> 8) & 0xFF); header[3] = (uint8_t)(addr & 0xFF); MR25H40_WriteEnable(); MR25H40_CS_Low(); ret = HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); if (ret == HAL_OK) { ret = HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY); } MR25H40_CS_High(); return ret; }读流程更简单,不需要写使能,CS 拉低后直接发读命令和地址,然后接收数据。
HAL_StatusTypeDef MR25H40_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t header[4]; HAL_StatusTypeDef ret; header[0] = 0x03; header[1] = (uint8_t)((addr >> 16) & 0xFF); header[2] = (uint8_t)((addr >> 8) & 0xFF); header[3] = (uint8_t)(addr & 0xFF); MR25H40_CS_Low(); ret = HAL_SPI_Transmit(&hspi1, header, 4, HAL_MAX_DELAY); if (ret == HAL_OK) { ret = HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY); } MR25H40_CS_High(); return ret; }注意这里 HAL_SPI_Receive 在全双工模式下会同时发送空数据字节,MRAM 收到后会把地址递增后的数据发回来,所以实际效果和预期一致。如果你用的是半双工模式,就需要自己切换方向,反而麻烦。
3.3 SPI 速率的取舍:为什么我锁死 12Mbit/s
F042 的 SPI1 最高支持到 18Mbit/s,MR25H40CDF 的 SPI 时钟最高可以跑到 40MHz 以上,两边单看都能跑很快。但真正限制速度的是系统时钟分配。STM32F042 的 APB1 时钟在 48MHz 主频下也是 48MHz,SPI 预分频最小是 2,所以理论最高 SCK 是 24MHz,超过外设的 18Mbit/s 上限。直接分频 2 属于超标使用,个别芯片能跑,但换一颗同型号可能就出错。
我最终用的是 4 分频,SCK 为 12MHz。这个速率在 MRAM 的支持范围之内,也在 F042 的安全工作范围之内,对 99% 的工业数据存储需求都够用了。
12Mbit/s 换算过来大约 1.5MB/s。写满整片 512KB 用时大约 350 毫秒,读也是同样时间。如果只是存参数和日志,每次写入几百字节,消耗时间完全可以忽略。如果追求极限,可以试试分频 2,但必须拿示波器确认 SCK 波形没有明显过冲,在量产环节定期抽样验证。我的项目里求稳,所以锁死 12Mbit/s,这个设定跑过温度循环没出过问题。
4. 实操:存储标定参数和日志的完整流程
4.1 三步自检:读 ID、写读比对、断电验证
拿到一块新板子,第一步不是直接跑业务逻辑,而是先写一个自检函数,确认 MRAM 和 MCU 之间的 SPI 通路正常。我总结了一个固定的三步验证法,每次换板子换料都这么测。
第一步,读设备 ID。通过 RDID 命令读取三个字节。MR25H40CDF 会返回固定的厂家 ID 和设备 ID,只要读到的三个字节不全为 0xFF,也不会全为 0x00,基本说明 SPI 时序和接线没问题。这一招能快速区分“芯片没响应”和“数据内容错了”两种情况。如果读到的 ID 是乱码,先查 CS 波形和 SCK 极性配置。
第二步,写读比对。在随机地址写入一串 256 字节的随机数,然后读回来逐字节比对。之所以强调用随机数,是因为固定 0xAA、0x55 这种测试码看不出总线竞争和时序偏移问题。0xAA 的二进制是 10101010,确实能看出信号翻转,但如果 MISO 上存在弱驱动冲突,固定方波模式不敏感。我习惯用伪随机序列,比如线性同余生成器产生的数据,这样 MISO 上时而高时而低,能更真实地模拟业务数据。
第三步,断电验证。写进数据后把板子断电再上电,重新读回来比对。这一步确认 MRAM 的掉电保持没问题,也顺便检查板子上 MRAM 供电有没有稳压电容不够导致上电异常的情况。断电循环我通常是 100 次,每次上电后读回关键标志,全部通过才算自检完。
4.2 数据帧结构设计:CRC 和版本号不能省
虽然 MRAM 本身可靠性高,但工业现场有干扰、有电压毛刺、有 SPI 总线上的程序逻辑 bug,存储的数据不能裸写。我设计了一套很简单的数据帧结构,既能做完整性校验,也为以后升级留了空间。
每帧数据我用 16 字节的头部加负载,头部格式如下:
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 2 | 帧标志 | 固定 0x5A 0xA5,识别数据有效性 |
| 2 | 1 | 版本 | 帧格式版本号,升级算法时靠它区分 |
| 3 | 1 | 类型 | 参数类型或者日志类型 |
| 4 | 4 | 长度 | 负载区长度,支持以后变长数据 |
| 8 | 4 | 时间戳 | 记录写入时刻,调试时非常有用 |
| 12 | 4 | CRC32 | 头部和负载整体校验 |
CRC 我直接用了 CRC-32 多项式 0x04C11DB7,STM32 内置了硬件 CRC 外设,但 F042 的 CRC 外设实现的是固定多项式,适合算大块数据。如果你用 HAL 库,调用 HAL_CRC_Calculate 就行。如果不想依赖硬件外设,软件 CRC32 也就几十行代码,性能完全够。
写数据时,先把头部和负载打包到 RAM 里的缓冲区,计算 CRC,然后一次性写入 MRAM。读的时候先读固定长度头部,校验帧标志和 CRC,CRC 不对就不认这帧数据,直接从备份区读上一份有效副本。
这套结构的核心价值在于,任何一次写入失败或者读回异常,业务层都能通过 CRC 迅速感知,而不是看到一个明显错乱的数据还在照常使用。
4.3 用 MRAM 特性做掉电保存和日志环形缓冲
MRAM 最大的优势在掉电保存和事件日志这两个场景里体现得淋漓尽致。
掉电保存我配合 STM32F042 的可编程电压检测器 PVD 来做。F042 的 PVD 可以设置一个电压阈值,当 VDD 跌到阈值以下时触发中断。在 PVD 中断里,立刻把当前的关键参数写入 MRAM。因为 MRAM 不需要擦除,写操作就是 SPI 数据流,几十个字节的代码足够在数十毫秒内完成。而普通 Flash 在掉电时要先擦除一个块才能写入,时间完全不够,只能补一个大电容硬扛。
日志环形缓冲更简单。我在 MRAM 里划出一块 64KB 的区域作为环形日志,里面始终保存最近 N 条事件记录。每条事件固定 64 字节,带序号和 CRC。写入时指针递增,写到区域末尾就回到开头直接覆盖。因为没有擦除延迟,没有块对齐要求,代码就是普通的数组操作。这种设计如果在 Flash 上实现,每写一条就要考虑当前块是否写满、是否需要搬移数据,复杂度完全不在一个级别。
void Log_Write(uint8_t *event, uint16_t len) { uint16_t idx; /* 从 MRAM 中读当前写指针 */ MR25H40_ReadBytes(LOG_PTR_ADDR, (uint8_t *)&idx, 2); /* 构造 64 字节事件记录帧,填入序号、时间戳、CRC */ /* 写入环形缓冲区当前位置 */ MR25H40_WriteBytes(LOG_DATA_BASE + idx * LOG_ENTRY_SIZE, log_buf, LOG_ENTRY_SIZE); /* 更新写指针 */ idx++; if (idx >= LOG_ENTRY_COUNT) { idx = 0; } MR25H40_WriteBytes(LOG_PTR_ADDR, (uint8_t *)&idx, 2); }5. 我踩过的坑:调试记录与排查速查表
5.1 全 FF 和全 00:先检查 CS 而不是数据线
我第一次焊接完这套系统,跑读 ID 测试,读回来三个字节全是 0xFF。当时第一反应是 MISO 线没接好,拿万用表量了半天,发现 MISO 通路没问题。后来把示波器探针压在 CS 引脚上才发现,CS 在启动阶段是电平不受控的。
原因很简单:F042 复位后,所有 GPIO 默认是浮空输入状态。PA4 这个 CS 引脚在初始化完成前是浮空的,如果板子上没有下拉电阻或者上拉电阻,CS 电平就处于不定状态。MRAM 内部逻辑可能把 CS 识别为有效,输出端和 MOSI 的输入产生竞争,导致 SPI 状态机乱掉。上电后第一次读 ID 就是全 FF。
解决办法有两个:一是硬件上在 CS 到地之间加一个 10kΩ 上拉电阻到 VCC,保证复位期间 CS 是稳定高电平;二是 GPIO 初始化时先把 PA4 配成输出高电平,再去初始化 SPI 外设。我在 CubeMX 里特别留意了初始化顺序,在 HAL_SPI_Init 之前先把 CS 拉高。
排查速查表:
| 现象 | 优先检查项 | 可能原因 |
|---|---|---|
| 读 ID 全 0xFF | CS 电平、SPI 时钟配置 | 芯片未选中,或 SCK 极性相位不对 |
| 读 ID 全 0x00 | MISO 通路、芯片供电 | 芯片没上电,或 MISO 被拉低 |
| 读 ID 乱码 | WP/HOLD 引脚电平 | HOLD 被拉低导致数据不完整 |
| 写后读错数据 | 写使能、CS 时序 | 没发 WREN,或 CS 拉低时间不够 |
| 偶发卡死 | HOLD 引脚、SPI 速率 | HOLD 悬空受到干扰,或 SCK 过冲 |
5.2 数据偶尔出错:一份来自示波器的证据
有段时间我调高了 SPI 预分频,SCK 跑到 24MHz,大部分时间读写正常,但偶发出现某一个字节错误。这个 bug 非常难抓,因为不是必现,偏偏在客户现场出现过一次。后来我用示波器同时抓 SCK、MOSI 和 MISO 三根线,发现 SCK 上升沿附近有很明显的振铃,尤其是在 MISO 返回最后一个 bit 的时候,振铃幅度大得几乎要误判电平。
F042 的 SPI 输出驱动能力不弱,但板子上 SCK 走线比较长,又没有串匹配电阻,信号反射叠加就造成了过冲。虽然 MRAM 的输入采样窗口能容忍一部分,但超出极限就吃进去了错误电平。
解决方式是降速,并把 SCK 走线上的串阻加上。我在 MCU 端 SCK、MOSI、MISO 各串了一个 33Ω 电阻,谐振明显压下去。之后 12MHz 下长时间烤机没有再复现。
这个经验也说明,只要数据帧有 CRC,偶发错误也能被业务层识别并重读一次。所以前面讲的数据帧设计不是可选项,是必备项。
5.3 复位后信号异常与替代料的选型备注
还有一个容易忽略的细节,就是 MCU 软复位后 SP1 外设状态的恢复。F042 系统复位后,SPI1 的控制寄存器回到默认值,PA5-PA7 回到浮空输入状态,这时候如果 MRAM 的 CS 是高电平,问题不大;但如果 CS 被某些异常代码拉低了,在复位瞬间 MRAM 可能收到几个杂散时钟,把一个无关字节写到某个地址去。为了防止这种情况,我给关键存储区开头都放了帧标志和 CRC,每次启动时先校验,再决定要不要恢复默认参数。
关于选型,现在市面上除了 Everspin 原厂,也有一些 pin 对 pin 兼容的 SPI MRAM 替代料,引脚和命令集基本一致。真正换料时不要只看规格书,一定要重新跑一遍读 ID、写读循环和温度循环。之前有一次把替代料的 WPEN 默认状态搞错了,导致第一版固件在特定型号芯片上写不进去,浪费了整整一天时间。稳定量产的设计里,存储芯片固定一个料号,不要轻易换。
5.4 高低温环境下的注意点
工业项目肯定要过 -40 到 85 摄氏度,MRAM 本身工作范围内没问题,但我建议在环测时多测两类指标:写入失败率和高低温切换后的数据保持。方法是在每个温度点写一段特征数据,切断供电,达到目标温度后重新加电读回。不要只测常温。
实际测试中我发现 MRAM 在低温下 SPI 时序余量比常温略低,SCK 虽然配置在 12MHz,但低温下信号边沿变缓,如果走线本身质量一般,可能偶发读错。这种问题很难从代码层面定位,最直接的办法是做 1000 次高低温循环写入读回测试,用统计结果评估余量。
调试阶段我会把 MRAM 驱动里的错误返回集中到一个全局计数器里,配合串口打印,批量跑测试时能快速看到错误率。这个习惯帮我在换了一版 PCB 后第一时间发现了新板子 MISO 走线过长的问题。
最后再分享一个我养成的开发习惯:第一次上板子,别急着把业务逻辑搬进去,先做一遍针对存储器的压力测试。把每一条命令、每一种数据长度、每一个温度点都跑一遍,把这颗芯片的脾气摸清楚,再开始设计正式的数据结构。MRAM 用顺了以后,最大的感觉不是它有多快,而是它把整块存储管理逻辑变得极其简单,简单到你可以把精力从“存储可靠性”这个泥潭里拔出来,专心去做业务功能。工业设备里那些标定参数、故障日志、历史波形,以前是痛点,换了这套组合之后,真的可以像写普通数组一样去写存储。