☰
PIC32驱动SPI MRAM实现工业掉电保存:从选型到驱动实战
2026/10/4 1:28:10 网站建设 项目流程

前阵子给一台工业设备做控制器升级,旧方案用SPI接口的EEPROM存运行参数和故障记录。参数每隔十几秒就要更新一次,照理说EEPROM的100万次写寿命应该够用,实际跑了两个多月就开始出现偶发丢数,排查下来才发现是频繁改写导致的坏块问题。最后我把存储芯片换成了 MR25H40CDF 这颗SPI接口的MRAM,主控用的是 Microchip 的 PIC32MX675F256L。这篇文章就把整套存储与读取方案从选型、硬件、驱动到实测数据完整梳理一遍,给正在做类似嵌入式数据记录项目的朋友一个可直接参考的模板。

文章会重点讲清楚三件事:为什么在这个场景下MRAM比EEPROM和Flash更合适,PIC32MX675F256L的SPI外设到底怎么配置才算稳,以及工业环境里掉电保存和故障日志应该怎么设计才不丢数据。如果你正在纠结嵌入式存储选型,或者第一次在PIC32上驱动MRAM,这篇内容应该能帮你省不少弯路。

1. 项目整体思路与选型拆解

1.1 存储介质选型:为什么是 MR25H40CDF 而不是 EEPROM/Flash

MR25H40CDF 是 Everspin 的 4Mbit SPI 接口 MRAM,换算下来是 512KB 容量。MRAM 的核心原理是磁阻效应,数据保存在磁隧道结的磁化方向上,写入过程直接改变存储单元的磁化状态。这意味着它天生支持按字节覆盖写,不需要像 Flash 那样先擦除整块再写。

这个特性放在工业设备的参数存储场景里非常关键。设备里的运行参数,比如温度阈值、补偿系数、累计运行时间,更新频率高,单次数据量小,但要求掉电之后必须还在。EEPROM 虽然也支持字节写入,但写操作要等毫秒级的时间,而且寿命通常在 10 万到 100 万次之间,高频次改写场景下很容易提前耗尽。NOR Flash 寿命也是 10 万次级别,最关键的是写之前要整块擦除,一旦在做擦除动作时掉电,数据就处于不确定状态。

MR25H40CDF 的写寿命标称在 10 的 14 次方这个量级,基本上可以认为不需要考虑磨损问题。我打个比方,Flash 和 EEPROM 像一张纸,反复折叠几次就开始破损,而 MRAM 更像一块白板,写完擦掉再写,几乎不磨损。另一个优势是写入不需要等待,写完就是写完,没有 EEPROM 那种“数据在飞”的时间窗口,掉电也不用担心写到一半卡住。

劣势也很明显,成本比同容量的 SPI Flash 贵,比 EEPROM 也贵,普通消费类产品基本不会用。但工业控制器、医疗设备、电力仪表这类对可靠性要求高的行业,多花这点钱换来的是调试和生产环节省心得多。

一个典型的对比可以看下面这张表:

维度MR25H40CDF(MRAM)SPI EEPROMSPI NOR Flash
写入寿命约10^14次约10^5~10^6次约10^5次
写入前擦除不需要不需要(但有写等待)必须整块擦除
单字节写入支持支持不支持,按页/块
擦写等待时间无3~5ms数十至数百ms
掉电写入可靠性高,写入原子完成中等低,擦写过程掉电易损坏
单价(同容量)高中低

所以选型逻辑很清楚:如果存储的是频繁改写的关键数据,且掉电之后数据必须可靠,MRAM 是省心方案。如果只存固件和静态资源,Flash 完全够用,没必要追 MRAM。

1.2 主控选择:PIC32MX675F256L 解决什么问题

PIC32MX675F256L 是 Microchip PIC32MX 系列的一款MCU,内核是 MIPS M4K,最高主频 80MHz,带 256KB Flash 和 64KB RAM。从规格上看它并不是性能最强的 MCU,但在做工业控制器这一类场景里,它有几个点非常合适。

第一,外设资源足够。PIC32MX675F256L 有多个 SPI 模块,实际项目中我用 SPI1 接 MR25H40CDF,还能留出另一路 SPI 给其它外部设备,比如 ADC 或者另一个传感器。这对于一个同时要采集多路模拟量、控制输出、跟人机界面通信的控制板来说,不会出现外设不够用的局面。

第二,外设引脚选择(PPS)机制灵活。PIC32MX 系列支持把 SPI 的时钟、数据线映射到不同引脚上。这意味着画 PCB 的时候不用非得把信号线绕到固定引脚,布局自由度大很多。对于一个需要兼顾强电和弱电区域的设备来说,这个特性非常实用,可以显著减少走线交叉。

第三,工业级温度范围。控制设备工作环境可能是 -40 到 85℃ 甚至更高,PIC32MX 系列的工业级型号覆盖了这个范围。MR25H40CDF 同样是工业级器件,两者搭配不存在工作温度上的短板。

还有一点,MPLAB X IDE 和 MCC 工具链对 PIC32 的支持已经相当成熟,寄存器操作也直接。对工程师来说,从寄存器层面写 SPI 驱动并不复杂,不像有些ARM平台还要过一遍HAL抽象层,排问题的时候反而更容易看清时序。

1.3 整体数据流设计:存储驱动在系统中的位置

在把代码拆开写之前,我先说清楚整个系统里数据是怎么流转的。主控通过传感器采集数据,经过运算后得到控制参数。一部分参数需要掉电保存,比如 PID 系数、累计运行小时、故障码。另一部分是运行日志,比如最近 N 条告警事件。

这些数据经过序列化之后,通过 SPI 写入 MR25H40CDF。上电时主控从 MRAM 里读取参数,校验通过后恢复运行状态。因为 MRAM 是随机访问的,我可以把它当作一块不丢失的 RAM 来用,驱动层的工作量集中在 SPI 命令封装和地址管理上,而不是像 Flash 那样还要设计坏块管理和擦除队列。

驱动层在设计上分三层:

  • 底层:SPI 外设初始化、单字节收发、片选控制
  • 中间层:MR25H40CDF 命令封装,包括读、写、写使能、状态寄存器操作
  • 应用层:把参数结构体、日志记录映射到 MRAM 地址空间

这样分层的好处是,如果后续换主控,只需要重写底层和中间层,应用层的数据结构不用动。如果换存储介质,只要是 SPI 接口,中间层改动也很小。

2. 硬件接线与关键设计细节

2.1 引脚分配与 SPI 接线方法

MR25H40CDF 是 8 引脚封装,常用引脚分别是 CS#、CK、SI、SO、WP#、HOLD#、VCC、VSS。对应到 PIC32MX675F256L,我选用的是 SPI1 模块。

具体接线关系如下表:

MR25H40CDFPIC32MX675F256L说明
CKSCK1(输出)SPI 时钟
SISDO1(输出)主发从收
SOSDI1(输入)从发主收
CS#任意 GPIO软件控制片选
WP#3.3V 上拉禁用写保护
HOLD#3.3V 上拉确保正常工作

CS# 不建议用 PIC32 硬件 NSS 引脚来控制。原因很简单,硬件 NSS 的自动行为在某些模式下会跟 SPI 收发产生耦合,很容易出现“还没发完数据片选就跳了”的情况。用普通 GPIO 手动拉低、拉高,时序完全可控,排查问题的时候也直观。

PIC32MX 系列支持 PPS 引脚映射,所以 SCK1、SDO1、SDI1 这三根线不一定固定在默认引脚上,需要在初始化代码里把对应引脚配置为外设功能。具体引脚号要以封装图和 PPS 映射表为准,我先不写死,后面代码里用注释说明。

2.2 电源、去耦与特殊引脚处理

MR25H40CDF 的工作电压是 3.3V 左右,不能直接接 5V。如果主控供电是 5V,一定要加电平转换,否则不仅通信不稳定,还可能损伤芯片。我当前这块板子主控和 MRAM 都是 3.3V 供电,所以直接对接。

电源引脚旁边放一个 100nF 陶瓷电容,紧贴 VCC 和 VSS,有条件的话再在稍远一点的位置放一个 10uF 钽电容或电解电容。工业环境里电源纹波通常比实验室恶劣,这个措施能明显降低高频噪声耦合到 SPI 信号的概率。

WP# 和 HOLD# 这两个引脚最容易踩坑。WP# 是写保护输入,直接接地的话,在某些状态寄存器配置下所有写命令都会被忽略,表现就是“写了但是读回来还是旧值”。HOLD# 是通信暂停脚,一旦被拉低,MRAM 会忽略 SPI 时钟信号,总线上的数据保持原状态。如果在工业现场这两个引脚悬空,受到电磁干扰就可能出现偶发读写失败。

这两颗引脚的推荐做法是:各接一个 10kΩ 电阻上拉到 3.3V。上电过程中即使 MCU 还没初始化,这两个引脚也处于确定的高电平状态,不会误触发写保护或暂停。

2.3 硬件设计容易踩的坑

我实际画板时遇到过几个问题,这里列出来提醒大家。

第一个坑是片选信号离芯片太远。CS# 信号如果走线过长,且旁边有电机驱动或者继电器触点,很容易被干扰导致芯片误识别命令。解决办法是让 SPI 四根线尽量以一组信号线的方式走线,远离功率走线,必要的时候在原理图里加串联电阻做阻尼。

第二个坑是 SPI 模式跟从机不匹配。MR25H40CDF 支持 SPI 模式 0 和模式 3,主控必须对应配置。模式 0 是空闲时钟为低电平,数据在上升沿采样;模式 3 是空闲时钟为高电平,采样沿也是上升沿。如果只配置了 SCK 极性,没有同时确认采样沿,很容易出现数据错位。

第三个坑是共用一个 SPI 总线时片选干扰。如果板上还有其他 SPI 从机,每个从机的 CS# 必须单独控制,而且在一个从机通信结束时要把片选拉到确定电平,不能浮空。浮空的片选引脚可能让多个从机同时响应总线,MISO 上就会出现数据冲突。

3. PIC32MX675F256L 驱动实现与读写流程

3.1 SPI 外设初始化细节

PIC32 的 SPI 模块初始化主要是配置 SPIxCON、SPIxBRG 和 SPIxSTAT。我用 SPI1,先配置引脚复用,再设置 SPI 参数。

示例代码如下:

void mr25_spi_init(void) { // 1. 引脚复用:把所用引脚配置为 SPI1 功能 // 以实际 PPS 映射为准,例如: // RPB14R = 0x07; // SDI1? 具体数值查阅器件手册 // 这里只演示框架 // 2. 关闭 SPI 后初始化 SPI1CON = 0; SPI1STATbits.SPIROV = 0; SPI1CONbits.MSTEN = 1; // 主模式 SPI1CONbits.MODE16 = 0; // 8 位数据 SPI1CONbits.CKP = 0; // 空闲时钟为低 SPI1CONbits.CKE = 0; // 采样沿配置 SPI1CONbits.SMP = 1; // 输入采样在数据末端 // 3. 波特率配置 SPI1BRG = 1; // 4. 启动 SPI SPI1CONbits.ON = 1; }

波特率的计算公式是:

SPI 时钟频率 = Fpb / (2 × (SPI1BRG + 1))

假设外设时钟 Fpb 配置为 40MHz,SPI1BRG 取 1,得到:

40MHz / (2 × (1 + 1)) = 10MHz

10MHz 对 MR25H40CDF 来说远低于它的最高时钟,信号完整性余量很足。工业板上走线不可能像实验室那么短,保守一点用 10MHz,后续如果要做批量读写优化,再调整 BRG 提到 20MHz 也不难。

每次收发一个字节的函数可以这样写:

uint8_t spi_write_read_byte(uint8_t byte) { SPI1BUF = byte; while (!SPI1STATbits.SPIRBF) { // 等待接收缓冲就绪 } return SPI1BUF; }

这里有个细节值得注意:SPI 是全双工协议,每次读操作都需要主控先发一个字节来产生时钟,读回来的内容在下一个字节里。所以读 MRAM 时,发送地址字节的过程中,MISO 上回来的是前一个字节对应的数据,这个时序要在心里清楚,否则容易把数据错位。

3.2 MR25H40CDF 命令集与底层封装

MR25H40CDF 的命令集跟常见 SPI 存储芯片类似,最常用的是下面这几个:

命令操作码帧格式
READ0x03命令 + 3字节地址 + 数据读出
WRITE0x02命令 + 3字节地址 + 数据写入
WREN0x06命令,无后续帧
WRDI0x04命令,无后续帧
RDSR0x05命令 + 状态寄存器读出
WRSR0x01命令 + 状态寄存器写入

注意地址是 3 字节,因为 512KB 的容量需要 19 位地址,用 24 位地址帧来覆盖整片空间。早期 1Mbit 以下容量的 MRAM 可能用 2 字节地址,换成 MR25H40CDF 之后一定要改成 3 字节,否则地址永远落在低 64KB 以内。

写使能和写禁止是这套接口里的关键。MRAM 内部有一个写使能锁存位,发送 WREN 命令之后,状态寄存器里的 WEN 位置 1,芯片才允许写入。写完一次之后 WEN 自动清零,下一次写之前必须再发 WREN。这个机制跟 EEPROM 很像,目的是防止误写。

底层封装代码:

void mr25_write_enable(void) { MR25_CS_LOW(); spi_write_read_byte(0x06); // WREN MR25_CS_HIGH(); } void mr25_write_disable(void) { MR25_CS_LOW(); spi_write_read_byte(0x04); // WRDI MR25_CS_HIGH(); } uint8_t mr25_read_status(void) { uint8_t status; MR25_CS_LOW(); spi_write_read_byte(0x05); // RDSR status = spi_write_read_byte(0x00); MR25_CS_HIGH(); return status; }

写完数据之后我建议主动调一次 WRDI。虽然 WEN 会自动清零,但主动清一次更保险,尤其后面程序里如果还要继续写的话,可以避免某些异常路径下 WEN 残留导致的意外改写。

3.3 单字节与多字节读写实现

单字节读和写是基础,多字节连续读写则主要用在日志批量存储里。

单字节读:

uint8_t mr25_read_byte(uint32_t addr) { uint8_t data; MR25_CS_LOW(); spi_write_read_byte(0x03); // READ spi_write_read_byte((addr >> 16) & 0xFF); spi_write_read_byte((addr >> 8) & 0xFF); spi_write_read_byte(addr & 0xFF); data = spi_write_read_byte(0x00); // 产生时钟,读回数据 MR25_CS_HIGH(); return data; }

单字节写:

void mr25_write_byte(uint32_t addr, uint8_t data) { mr25_write_enable(); MR25_CS_LOW(); spi_write_read_byte(0x02); // WRITE spi_write_read_byte((addr >> 16) & 0xFF); spi_write_read_byte((addr >> 8) & 0xFF); spi_write_read_byte(addr & 0xFF); spi_write_read_byte(data); MR25_CS_HIGH(); mr25_write_disable(); }

多字节连续读:

void mr25_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; MR25_CS_LOW(); spi_write_read_byte(0x03); spi_write_read_byte((addr >> 16) & 0xFF); spi_write_read_byte((addr >> 8) & 0xFF); spi_write_read_byte(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = spi_write_read_byte(0x00); } MR25_CS_HIGH(); }

连续读模式下地址会自动递增,跨越芯片容量边界之后会回到 0 地址,这个行为跟很多 SPI Flash 一致。实际使用时要保证 len 不会让地址超出边界,否则数据位置会对不上。我一般在应用层做一次地址范围检查,避免这种低级问题。

多字节连续写同理,把循环里的数据字节连续发出去就行。MRAM 的好处是连续写地址自动递增,中间没有页缓冲的概念,也不会因为跨页产生奇怪的写入失败。

3.4 调用示例与性能测试

举个例子,设备每次上电启动时要把开机次数加 1,然后把总运行时间存下来:

#define MAGIC_ADDR 0x00000 #define BOOT_COUNT_ADDR 0x00010 typedef struct { uint32_t magic; uint32_t boot_count; uint32_t total_seconds; uint32_t crc32; } sys_info_t; void save_sys_info(sys_info_t *info) { mr25_write_bytes(MAGIC_ADDR, (uint8_t *)info, sizeof(sys_info_t)); }

这里 mr25_write_bytes 是把结构体当作连续字节写入 MRAM。因为 MRAM 支持任意字节地址写入,这种做法非常直接,不需要考虑页对齐。

性能方面,按 10MHz 时钟来算,写入一个字节实际要传输 4 字节帧(命令 + 3 字节地址),大约 3.2us,加上软件循环开销,实测单字节随机写不到 5us。连续读 512 字节大约是 516 字节帧,理论传输时间约 413us,加上循环判断等开销,实测在 500us 左右。这跟 EEPROM 动辄 3~5ms 的字节写等待相比,完全是两个量级。

4. 工业场景下的可靠性设计与实测数据

4.1 掉电保存与故障记录的设计

MRAM 的写入是原子的,但应用层的“事务”不一定天然原子。比如同时要更新一组参数,A 参数已经写进去了,B 参数还没来得及写,这时候正好掉电,恢复之后就会出现 A 是新值、B 是旧值的情况。

解决这个问题我用了双备份结构,也叫“槽位法”。把参数区域分成 A、B 两个槽,每槽都保存完整数据和 CRC 校验值。写入时先写 A,再写 B。上电读取时先读 A,如果 A 的 CRC 校验通过就采用 A;如果 A 校验失败,说明 A 写到一半掉了电,此时读 B,B 应该是完整的。这样即使掉电发生在任意时刻,系统总能恢复到一个完整的数据集。

这个方案不需要额外硬件,只是把存储空间翻倍。MR25H40CDF 有 512KB,对于几组参数来说完全不是问题。伪代码结构大致如下:

typedef struct { uint32_t magic; uint32_t data_len; uint8_t data[64]; uint32_t crc; } record_slot_t; #define SLOT_A_BASE 0x10000 #define SLOT_B_BASE 0x11000

掉电保存流程就三步:拉低 GPIO 关掉外部执行器,然后按上面双槽逻辑写入数据,最后把写保护开启。整个过程因为 MRAM 写速度极快,可以在检测到掉电到系统完全断电之间轻松完成。

4.2 写入策略与寿命管理

MRAM 寿命很长,不代表可以随便写。工业设备里很多“疯狂写入”其实是因为软件 bug 导致的状态反复翻转,比如传感器瞬时值抖动让控制逻辑每隔几毫秒就更新一次参数。这种无意义的写入会放大系统功耗,也会让日志区域在短时间内被覆盖掉。

我给写入操作加了几个约束条件:参数变化小于阈值时不写;写入间隔小于 100ms 时丢弃本次写入;日志按环形队列设计,队列满之后覆盖最旧的数据。这些策略对任何存储介质都适用,但对 MRAM 来说主要是为了降低总线负载和数据管理复杂度,而不是保护寿命。

块保护方面,MR25H40CDF 的状态寄存器里有 BP0、BP1 位,可以设置块的写保护范围。如果某一段地址放的是出厂校准数据,不想被运行程序误改,可以在初始化时给对应地址加上块保护。要修改这些数据时再临时关闭保护。

4.3 实测数据与测量方法

这块板子做了一系列测试,记录如下:

  • 连续随机地址写入 10 万次,每次写后读回校验,全部通过
  • 对同一地址反复写入 100 万次,数据一致,无写失败
  • 掉电 100 次测试,每次都是在写入过程中随机断电,上电后双槽恢复机制均能读到正确数据
  • 在电机启动干扰下持续读写 8 小时,未出现位反转或通信错误

测量方法上,我在 SPI 读写函数前后翻转一个调试 GPIO,用示波器观察单次读写耗时,再用逻辑分析仪抓取完整帧,确认命令字节、地址字节和数据的电平都符合预期。

这里要特别说明,数据保持特性的验证很难在短时间内完成,因为 MRAM 的保持期限是按年计的。我们能做的是相信器件数据手册给出的指标,同时通过双槽 CRC 机制兜底。真正需要长期验证的指标,要靠设备在实际工况中积累数据。

5. 常见问题排查与避坑实录

5.1 读回 0xFF 或 0x00 的排查流程

读回固定 0xFF 是最常见的问题,通常不是 MRAM 坏了,而是 SPI 通信本身没建立起来。排查顺序我固定是这么几步。

第一步查供电和引脚电平。用万用表确认 VCC 有 3.3V,WP# 和 HOLD# 是高电平,CS# 在通信时能被拉低。第二步查SPI模式。用示波器抓 SCK,看空闲电平和数据采样沿是否符合预期,模式不对的话,MISO 上的数据会整体错位。第三步查命令帧。用逻辑分析仪抓 MOSI 上的数据,确认发送的第一个字节是 0x03,后续地址字节顺序正确。第四步查 MISO 回读。MOSI 发完地址后继续发 0x00,SCK 继续翻转,MISO 才会把数据送出来。

大部分读回 0xFF 的情况,最终都定位在引脚复用没生效或者 SPI 模式配错,芯片本身很少出问题。

5.2 写入不生效的三大原因

写入不生效比读取异常更隐蔽,因为命令看起来都发出去了,信号电平也正常,但读回来还是旧值。

第一个原因是没发 WREN。MRAM 在收到写命令之前必须要把 WEN 位置位,否则写命令被忽略。检查方法很简单,发完 WREN 之后读状态寄存器,看 WEN 位是否为 1。如果不为 1,说明 WREN 时序不对或者 CS 上沿没有正常触发锁存。

第二个原因是 WP# 或者 HOLD# 被拉低。WP# 拉低会让芯片处于写保护状态,HOLD# 拉低会让芯片忽略 SPI 时钟。这两个引脚的检查要放在所有软件调试之前,因为信号都正常但操作无效,大概率就是硬件级别的写保护被触发了。

第三个原因是地址越界。如果使用的地址超过芯片容量,有些器件会直接忽略写入,有些会做回卷。如果应用层代码不小心把某一类数据的地址算错了,写入可能落在错误区域,看起来就是“没写进去”。

5.3 调试利器:逻辑分析仪与 GPIO 打点

排查这类问题,我强烈建议在开发阶段就把逻辑分析仪用起来。SPI 时序本身不复杂,抓一次完整波形就能看清所有问题。

我常用的做法是写一个调试宏,在每次片选拉低和拉高时翻转一个 GPIO,然后把这个 GPIO 也接到逻辑分析仪上。这样不仅能看 SPI 信号,还能看出软件里片选保持时间够不够,是否存在过短的 CS 脉冲。

代码大致是:

#define DBG_PIN_TOGGLE() LATFbits.LATF0 = !LATFbits.LATF0 // 在片选操作前后调用 MR25_CS_LOW(); DBG_PIN_TOGGLE(); // ... 发送命令 DBG_PIN_TOGGLE(); MR25_CS_HIGH();

在 MPLAB X 的调试界面里同时打断点观察状态寄存器值,配合逻辑分析仪的波形,基本能定位所有 SPI 相关问题。

5.4 常见问题速查表

故障现象可能原因解决方案
读回固定 0xFFSPI 模式不匹配、CS 未拉低、引脚复用未生效检查 SCK 电平与采样沿,检查 PPS 配置
读回数据错位字节序错、地址帧少发一个字节确认地址是 3 字节,检查 SPI 位序
写不进去未发 WREN、WP# 拉低、HOLD# 拉低读状态寄存器确认 WEN 位,检查硬件引脚
偶发读写失败干扰、CS 走线过长、电源纹波大加强去耦,SPI 降频,检查布局
掉电后数据异常应用层事务未保证原子性使用双槽结构和 CRC 校验
地址超出容量后数据错乱应用层未做边界检查写驱动时增加地址范围校验

我在实际调试中还有一个习惯:第一版驱动一定用最朴素的轮询方式,把所有细节剥开,先把 CS、模式、地址帧调通,再用中断和 DMA 逐层往上加。原因很简单,SPI 设备的问题本来就不多,一旦混入 DMA 和多线程,定位起来就麻烦很多。MR25H40CDF 本身是个很省心的存储芯片,只要时序正常几乎不会掉链子,多数问题还是出在初始化配置和硬件接线上。

如果你也在做类似的数据记录项目,建议先跑通单字节循环读写,再扩展批量读写,最后把日志和双槽恢复这些业务逻辑加上去。一步步来,整个系统会稳很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询