做工业嵌入式设备的人都有一个共识:数据存储比计算更磨人。选型表里塞满了各种Flash和EEPROM,可真到现场,断电丢数据、写坏一个块、日志翻不出来,哪个问题都比CPU多跑几条指令麻烦。我这两年一直在用MR25H40CDF配合PIC32MZ1024EFE144做工业数据存储与读取,这套组合在参数保存和设备日志这两个方向上是那种用上就回不去的方案。MR25H40CDF是Everspin的4Mbit SPI MRAM,PIC32MZ1024EFE144是Microchip的200MHz MIPS内核MCU,两个器件都是为工业温度和长时间运行准备的。这篇文章把选型理由、硬件接法、驱动代码、数据管理格式和踩坑记录完整过一遍,适合正在做嵌入式存储方案,或者被掉电保存问题折磨过的工程师。
1. 选型思路:MR25H40CDF和PIC32MZ1024EFE144为什么搭
1.1 MR25H40CDF的核心参数
MR25H40CDF这颗芯片,本质是一颗4Mbit的串行MRAM,也就是磁阻随机存取存储器。容量算下来是512KB,对工业参数和日志来说不算大,但很够用。它走SPI接口,3.3V供电,8引脚DFN封装,链路和一颗普通串行Flash几乎一样,不需要额外改动板卡结构。
它和Flash最大的区别在存储原理。Flash靠浮栅电荷表示数据,写入之前必须整块擦除,而且每个单元大概只有十万到一百万次擦写寿命。MRAM靠磁隧道结的电阻状态存储数据,通电时是RAM的读写特性,掉电后磁性状态保持,数据不丢。所以MRAM同时具备RAM的速度、Flash的非易失性,并且没有擦除限制。Everspin官方给的说法是无限次读写,数据保持超过20年,温度范围覆盖工业级。
SPI接口速率方面,MR25H40CDF最高支持40MHz左右,具体视供电电压和温度等级而定。实际项目里跑到10MHz已经非常稳,这个速率对参数读写和日志周期写入绰绰有余。
1.2 PIC32MZ1024EFE144的角色
PIC32MZ1024EFE144是Microchip的32位MCU,MIPS microAptiv内核,主频可以到200MHz。片上有1MB Flash存固件,512KB SRAM跑业务数据,带DMA、硬件CRC、加密引擎、USB、CAN,还有6个SPI模块,其中SPI1到SPI4都是可以自由映射引脚的标准外设。
选这颗MCU,主要因为工业设备里不止存储一件事。通常还要跑显示界面、通信协议栈、现场总线和故障诊断逻辑,这些用8位机做费劲,用带MMU的MPU做又大材小用。PIC32MZ正好卡在中间,资源充裕,外设齐全,SPI挂MRAM只是它工作的一部分。关键是这个系列带E后缀的型号覆盖扩展温度范围,电源、时钟、复位电路都有完善的监控机制,适合在工业现场长期跑。
1.3 存储方案的横向对比
很多工程师第一反应是:512KB随便用一颗串行Flash不就行了,为什么要单独挂MRAM?这里有个对比表,可以直接说明问题。
| 存储器件 | 写入方式 | 单字节改写 | 寿命 | 掉电保持 | 实际工程痛点 |
|---|---|---|---|---|---|
| NOR Flash | 先擦除再写 | 否 | 10万次左右 | 10~20年 | 频繁改参数会写坏块 |
| EEPROM (I2C) | 内部擦写 | 可 | 100万次 | 20年+ | 写一字节几毫秒,且容量普遍小 |
| SRAM+电池 | 直接写 | 可 | 无限 | 靠电池 | 掉电换电池麻烦,有安全隐患 |
| MR25H40CDF MRAM | 直接写 | 可 | 无限 | 20年+ | 成本略高,但省心 |
实际工业场景里,工艺参数、标定系数、生产计数值这些数据,特点是单次写入数据量小、改写频率高、一点都不能丢。用Flash要整页擦写,用EEPROM寿命和数据量都受限,用电池SRAM又怕电池失效。MRAM把这些都解决了。
2. 硬件最小系统:引脚怎么接才不会翻车
2.1 最小连接图
MR25H40CDF的引脚不多,和PIC32MZ之间的连接就是标准的SPI四线加两根控制线:
| MR25H40CDF | 方向 | 接PIC32MZ1024EFE144 |
|---|---|---|
| CS# | 输入 | 任意GPIO,推荐空闲GPIO |
| SCK | 输入 | SPI1_SCK,通过PPS映射 |
| SI (MOSI) | 输入 | SPI1_SDO |
| SO (MISO) | 输出 | SPI1_SDI |
| WP# | 输入 | 直接上拉到VCC |
| HOLD# | 输入 | 直接上拉到VCC |
| VCC | 电源 | 3.3V |
| GND | 地 | GND |
PIC32MZ的SPI引脚是重映射的,也就是PPS外设引脚选择。比如把SCK1映射到某个复用引脚,SDO1映射到另一个,这些在MPLAB Harmony的Pin Settings图形界面里配置就行,生成的代码会处理好锁存和映射逻辑。CS用哪个GPIO都行,选一个离SPI引脚近的,方便布线。
2.2 HOLD和WP两个不能省的细节
HOLD#引脚低电平时,MRAM会暂停外部通信,SCK和SI上的信号被忽略,但SO保持高阻或当前状态。这个功能设计初衷是让多个器件共享SPI总线时,可以随时暂停某一个。实际应用中如果HOLD#悬空,或者走线太长被干扰拉低,MCU这边就表现为SPI读取超时、数据卡死。所以HOLD#必须通过电阻上拉到VCC,阻值10k或者4.7k都行。
WP#是写保护引脚。MRAM内部状态寄存器有BP0、BP1和WPEN位,如果这些保护位被设置,WP#低电平时对应区域就禁止写入。多数应用只需要全片读写,不需要保护区域,所以WP#直接上拉到VCC,保证写操作永远不被硬件封锁。
如果板子工作在强干扰环境,比如变频器旁边、电机驱动附近,建议在CS#、SCK、SI、SO四根线上各加一个33pF到100pF的滤波电容到地,位置尽量靠近MRAM引脚。SPI速率不高时这个电容不会影响信号,但可以滤掉不少空间辐射噪声。
2.3 布局和电源处理
MR25H40CDF工作电流大约十几到二十几毫安,比Flash略高,因为MRAM内部是磁隧道结阵列,读写时动态功耗大一些。电源上需要在VCC引脚旁边放一颗100nF陶瓷电容,再放一颗4.7uF钽电容或者10uF陶瓷电容用于稳压,位置尽量靠近芯片,走线短粗。
PIC32MZ的VDDCORE等电源引脚也要接好对应的去耦电容。两个器件共用一个3.3V电源域时,注意MRAM读写瞬间的电流变化不要拉低MCU的AVDD或者VDD,否则会引起MCU复位或者SPI数据错乱。最简单的做法是3.3V主电源出来之后,先走一小段加宽铜皮,再分支给两边的去耦电容。
信号线建议走线长度不超过5cm,如果板子空间限制必须走长,就把SPI速率降下来,不要抱着10MHz不放。工控板上飞线调试的时候尤其注意,杜邦线一拉几十厘米,高速必翻车。
3. 软件驱动:SPI初始化与MRAM读写实现
3.1 命令集和状态寄存器
MR25H40CDF的命令集非常简单,比串行Flash简单太多,因为不需要擦除命令。
| 命令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读ID信息 |
状态寄存器里最重要的位是WEL,也就是写使能锁存位。MRAM上电后WEL默认是0,此时执行的WRITE命令会被忽略。必须先发WREN命令把WEL置1,然后才能写。写完一次之后WEL会清零,所以下一次写之前还要再发WREN。这个机制和Microchip的串行EEPROM很像,本质上就是防止误写。
状态寄存器还有一些BP0、BP1和WPEN位,用来实现块保护。对普通应用来说,不要动这些位,保持默认全零即可,代表整个存储区都允许写入。
3.2 SPI模块配置要点
PIC32MZ1024EFE144的SPI配置,重点在模式、时钟频率和采样点。MR25H40CDF支持SPI Mode 0和Mode 3,实际用Mode 0就行。
SPI Mode 0的含义是:空闲时SCK为低电平,数据在SCK上升沿被采样,下降沿发生变化。对应到PIC32MZ的SPI寄存器,就是CKP=0、CKE=0。如果CKP或者CKE配置不对,读回来的数据基本就是全FF或者全00,这是第一个最容易踩的坑。
时钟频率的计算公式是:
SPIxBRG = PBCLK3 / (2 * 目标SPI时钟) - 1比如PBCLK3是100MHz,目标5MHz,那SPIxBRG就等于100000000 / (2 * 5000000) - 1 = 9。BRG存9,实际SPI时钟就是5MHz。
采样点由SMP位控制。SMP=0时在数据周期中间采样,适合低速和一般场景;SMP=1时在数据周期末尾采样,适合高速长走线。低速调试阶段先用SMP=0,跑稳定后再考虑提速。
3.3 核心驱动代码
如果用MPLAB Harmony v3生成工程,SPI1的初始化会由代码生成器自动完成。需要自己写的是MRAM驱动层。下面代码是我在Harmony生成的SPI1基础包上精简的版本,底层收发函数直接用SPI1BUF,逻辑清晰也容易移植到其他SPI模块。
/* ==================== MR25H40CDF 命令定义 ==================== */ #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_WRITE 0x02 #define MRAM_CMD_RDID 0x9F /* 状态寄存器位定义 */ #define MRAM_STS_WEL (1u << 1) #define MRAM_STS_BP0 (1u << 3) #define MRAM_STS_BP1 (1u << 4) #define MRAM_STS_WPEN (1u << 7) /* CS引脚控制,宏替换为你的实际GPIO定义 */ #define MRAM_CS_LOW() (LATFCLR = CS_PIN) #define MRAM_CS_HIGH() (LATFSET = CS_PIN)批量读写函数是核心,READ和WRITE命令后面都跟三个字节地址。MR25H40CDF虽然只有4Mbit,也就是19根地址线有效,但命令格式里依然按24位地址处理,高5位填0就行。
读操作最简单,CS拉低,发READ命令,发三个字节地址,然后连续读回数据。CS拉高结束操作。
void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MRAM_CS_LOW(); SPI1_ByteIO(MRAM_CMD_READ); SPI1_ByteIO((uint8_t)(addr >> 16)); SPI1_ByteIO((uint8_t)(addr >> 8)); SPI1_ByteIO((uint8_t)addr); for (i = 0; i < len; i++) { buf[i] = SPI1_ByteIO(0x00); } MRAM_CS_HIGH(); }写操作比读多一步,必须先发WREN让WEL置1,然后才能发WRITE命令和数据。
void MRAM_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_ByteIO(MRAM_CMD_WRITE); SPI1_ByteIO((uint8_t)(addr >> 16)); SPI1_ByteIO((uint8_t)(addr >> 8)); SPI1_ByteIO((uint8_t)addr); for (i = 0; i < len; i++) { SPI1_ByteIO(buf[i]); } MRAM_CS_HIGH(); }底层收发字节的函数:
uint8_t SPI1_ByteIO(uint8_t tx) { while (SPI1STATbits.SPITBF) { } SPI1BUF = tx; while (!SPI1STATbits.SPIRBF) { } return SPI1BUF; }写使能可以做一个带验证的版本,发完WREN之后读状态寄存器检查WEL是否真的置位,防止硬件异常时闷头写。
int MRAM_WriteEnable(void) { uint8_t st = 0; uint8_t i; MRAM_CS_LOW(); SPI1_ByteIO(MRAM_CMD_WREN); MRAM_CS_HIGH(); for (i = 0; i < 10; i++) { st = MRAM_ReadStatus(); if (st & MRAM_STS_WEL) { return 0; } } return -1; }注意MRAM和Flash一个显著区别:写完不需要等待内部编程完成。Flash写一整页之后要轮询状态寄存器的WIP位,MRAM没有这个说法,字节写入在时钟周期内就完成物理切换,所以连续写512字节就是512个字节时钟,速度非常稳定。
地址和数据格式上,MRAM是纯字节存储,不区分页、扇区、块。ADDRESS在单个CS周期内自动递增,可以连续读到最后一个地址再回绕到0。这一点让驱动极其简单,读写函数不用关心页对齐、块对齐这些Flash才有的概念。
3.4 DMA搬数的进阶用法
日志记录和固件升级场景里,一次性搬几百KB数据很常见。如果主循环里逐字节调用SPI1_ByteIO,CPU会被占用很久。PIC32MZ自带的DMA可以接管SPI收发的搬运工作。
DMA配合SPI的做法很简单:发送时把要写的数据缓冲区地址配给DMA源地址,SPI1BUF作为目的地址,长度设成数据长度加4,DMA会在每次SPI发送完成后自动搬入下一个字节。接收时反过来,DMA从SPI1BUF搬到SRAM缓冲区。
启用DMA前有几个细节要先确认。第一,SPI要用增强缓冲模式,也就是ENHBUF位打开,这样DMA和SPI状态标志的握手才顺畅。第二,DMA中断里要检查SPI的接收溢出标志,否则高速时可能丢字节。第三,DMA传输长度建议按4字节对齐,PIC32MZ的DMA对非对齐长度的处理效率会差一截。
如果只是参数区读写,前CPU轮询方式完全够用,我一般只在日志批量导出时启用DMA。
4. 工业场景的数据组织与可靠性设计
4.1 参数块设计:魔数、版本、CRC和冗余
硬件驱动通了之后,真正的工业设计才开始。直接裸读裸写MRAM只能用作风控函数跑飞前最后一道处理:读写驱动只是工具,数据管理才是能不能在现场长期跑的关键。
设备参数区我常用的格式是一个固定长度的结构体,每次写入整体覆盖:
#define PARAM_MAGIC 0x5A5A1234 #define PARAM_VERSION 3 #define PARAM_COPIES 3 typedef struct { uint32_t magic; uint16_t version; uint16_t len; uint32_t crc32; uint32_t seq; uint8_t payload[240]; } ParamBlock;整个结构体固定256字节。magic用来快速判断这块区域有没有被写过,version给参数格式做版本管理,len和crc32负责数据校验,seq是一个单调递增的序号,用于多副本之间做仲裁。
保存参数的流程是:把payload打包好,依次写到三个副本区域。读取时读三个副本,先检查magic和CRC,通过校验的副本再比较seq,取最新的一份使用。如果某个副本校验失败,用最新有效副本重新写回,完成自修复。
三个副本承担的意义是:单次掉电写入过程中,MRAM物理层面不会丢数据,但软件层面可能在写入一半时被复位打断。三份冗余加仲裁,任何一份损坏都能恢复。
CRC的选择上,工业参数区我建议用CRC32,不要用CRC8或者简单校验和。逆这种操作不复杂,网上有标准的查表法实现,跑出来的校验强度完全够。
4.2 日志区的环形缓冲区
故障记录和设备日志的特点是写入频繁、数据量随时间累积、每一条都很重要。用线性地址写日志,写满之后就必须覆盖旧数据,所以天然需要一个环形结构。
MRAM的环形日志比Flash简单一个数量级。因为Flash有擦除块限制,环形日志要按扇区对齐,写满一个扇区擦一个扇区,代码里全是块管理和磨损均衡。MRAM没有擦除操作,头尾指针就是一个普通计数器,写到末尾回绕到开头即可。
日志条目可以设计成变长结构,用类型、长度字段区分:
typedef struct { uint32_t timestamp; uint16_t type; uint16_t len; uint32_t crc; uint8_t data[]; } LogEntry;日志区头部放两个指针:下一个写入位置writeIndex,以及当前有效记录数recordCount。每次写日志时,先读writeIndex,写入LogEntry,再更新writeIndex。如果写入位置加上本记录长度超过了日志区末尾,就先回绕到开头再写。
掉电瞬间正在写一条日志怎么办?日志写入采用先写数据后更新头指针的顺序。头指针的更新是最后一步,如果头指针没来得及更新,上电读到的writeIndex还是旧值,最新一条不完整的记录会被忽略,最多丢一条日志,不会污染整个环形区。这种设计在工控日志里非常实用。
4.3 掉电一致性:提交标志和双区轮换
MRAM本身非易失,掉电不会丢数据,但掉电瞬间如果正在执行写操作,数据可能只写了一半。比如参数块有256字节,电源在写到第100个字节时断了,那MRAM里存储的就是半新半旧的数据。
三份冗余加CRC能解决数据损坏问题,但更好的方案是做提交标志。核心思想是:先把完整的数据写到目标区域,数据全部写完并校验通过之后,再单独写一个特定的提交字节到固定地址。这个提交字节就是事务完成的标志。
上电加载时,先看提交标志。如果标志有效,说明上次写入完整结束,正常加载数据。如果提交标志无效,说明上次写入在某个中间步骤被打断了,此时直接忽略新数据,走回退逻辑。
双区轮换是另一种更彻底的做法。参数区划分成A和B两个区域,当前有效区域记录在固定地址。写入时先写非有效区,数据全部写完之后,把有效标志切换到新区。这样任何时刻都有一份完整的有效数据在上电后可用。MRAM容量512KB,参数区双份保存完全不是问题。
5. 实测性能与调试经验
5.1 读写速度实测
MR25H40CDF在5MHz SPI时钟下,连续读512字节,算上命令和地址开销,大约耗时0.83ms。连续写512字节,在写使能和命令开销之后,大约也是0.85ms左右,因为MRAM没有等待时间。
在10MHz下,连续写16KB数据的实测时间是13.4ms左右,包含所有命令开销。换算下来有效吞吐大约1.2MB/s。很多人看到这个数字觉得不算惊艳,但注意这是同步SPI的纯数据速率,而且没有等待周期。
对比一串Flash:写16KB数据需要先擦除4个4KB扇区,一个扇区擦除大约50ms,四个就是200ms,加上页编程时间,总共220ms左右。MRAM写16KB只要13ms,凭空快了一个数量级。而且Flash这一通擦写之后,如果只是改了其中几百字节的参数,整块被重写一遍,磨损和耗时都不划算。
5.2 调试顺序建议
新板子拿到手,不要急着跑业务日志,按照下面顺序做基本验证。
第一步,读ID。MR25H40CDF支持RDID命令,会返回一串ID信息。能读到稳定、一致的ID,说明SPI时序、引脚映射、电源基本都对。
第二步,全FF验证。新MRAM上电内容通常不是固定值,先读整个1MB区域看看是否有一个稳定模式。如果读回来全是FF或者00,多半是SPI模式、采样点或者引脚映射问题。
第三步,单字节回环。在一个地址写一个pattern,比如0xAA55,然后读回验证。连续试几个地址,确认写使能、地址递增都没有问题。
第四步,边界测试。重点测起始地址0x00000、末尾地址0x7FFFF,以及跨中间地址的连续读写。MRAM地址回绕逻辑如果驱动有bug,在边界最容易暴露。
第五步,连续长时间写入稳定性测试。用10MHz SPI反复写满整个512KB再读回,循环几百次,观察有没有偶发的CRC错误或卡死。这一步能排查出信号完整性、去耦电容、地平面这几类问题。
5.3 避坑清单
总结几个实际调试中遇到的问题,都是常规文档里不会系统写的内容。
- 不要使用默认SPI模式。很多MCU上电默认SPI配置不是Mode 0,直接挂MRAM读回来全FF。
- 状态寄存器的WEL位位置,不同批次手册可能有差异,一定以你手头规格书为准。驱动里宏定义后写个小函数读几次,确认返回值。
- WP#和HOLD#不要直接连MCU的IO。MCU上电瞬间IO状态不确定,如果输出低电平,MRAM要么被写保护,要么被暂停通信。直接上拉最省心。
- 连续写操作之间,CS必须拉高再拉低。MRAM内部虽然不需要写等待,但CS的上升沿是命令结束的标识,所有写使能和写数据序列都要有完整的CS脉冲。
- 长距离飞线调试时,HOLD#是最容易受干扰的。开机后如果SPI偶尔卡死,先抓HOLD#引脚波形。
6. 常见问题排查速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 读回数据全FF | SPI模式不匹配 | 核对CKP/CKE,使用Mode 0 |
| 读回数据全00 | SO引脚映射错或采样点不对 | 检查PPS映射,尝试SMP=0/1 |
| 写入后读回旧值 | 没有执行WREN | 检查WEL状态,确认写使能时序 |
| 写入后读回随机值 | 电源抖动或干扰 | 检查3.3V纹波,加强去耦 |
| 高速传输偶发错字节 | 走线太长、采样点靠前 | 降速或调整SMP位为末尾采样 |
| SPI卡死无响应 | HOLD#被拉低 | 用示波器抓HOLD#波形 |
| 掉电后参数丢失 | 写入中间掉电,无校验机制 | 引入提交标志和CRC,使用双区轮换 |
最后一个建议:MRAM在嵌入式系统里的定位不是替代Flash,而是替代那些需要频繁改写、断电不丢、快速响应的存储场景。参数保存、校准数据、生产计数、故障记录这四个方向完全够用。固件升级这类大块静态数据,仍然建议放在板载Flash里,没必要把512KB MRAM浪费在固件镜像上。
个人体会是,MRAM把存储从玄学变成了普通函数调用。以前用Flash,写一个字节要琢磨磨损和擦除,现在不用管;以前用EEPROM,写一字节要算内部擦写时间,现在也不用管。如果你正准备在PIC32MZ或者类似MCU上挂MRAM,第一版先别急着上高速,5MHz跑通读写和状态寄存器,再逐步提速。引脚上该上拉就上拉,别省那两个电阻。数据格式上,提交标志和CRC这个习惯建议从一开始就养成,后面现场问题会少很多。