☰
基于dsPIC33EP与MR25H40CDF的SPI MRAM存储方案设计与实现
2026/10/5 7:07:07 网站建设 项目流程

1. 为什么偏偏选中 MR25H40CDF 和 dsPIC33EP512MU814 这对组合

1.1 从两个器件的定位说起

MR25H40CDF 是一颗 4Mbit 的磁阻随机存储器(MRAM),采用 SPI 接口,工作电压 3.3V,标准 SOIC-8 封装。它和常见的 SPI Flash、EEPROM 最大的区别在于:写入不需要擦除、没有写延迟、字节级随机写入、擦写寿命几乎无限(官方标称 10^14 次以上)、数据保持 20 年以上。这几个特性放在工业现场数据记录、掉电前关键参数保存、频繁更新的计数器/日志场景里,几乎是降维打击。

dsPIC33EP512MU814 是 Microchip 的 16 位数字信号控制器,512KB Flash、48KB RAM,带 USB、CAN、多个 SPI/I2C/UART 外设,主频可跑到 70 MIPS。它属于 dsPIC33EP 系列里的高配型号,PIM 引脚多、外设全,工业控制、电机驱动、电源管理里用得非常多。选它来搭配 MR25H40CDF,核心原因是它自带多个硬件 SPI 模块,SPI 时钟可以灵活分频,而且 3.3V 电平与 MRAM 完全匹配,不需要额外电平转换。

这两个器件凑在一起,解决的是一个非常具体的工程问题:在工业现场,需要一块掉电不丢、写入不卡顿、寿命够长的非易失存储,同时主控要能稳定、高速地通过 SPI 读写它。

1.2 常见方案对比:为什么不用 Flash 或 EEPROM

很多人第一反应是"存数据用 SPI Flash 不就行了"。我实际做过对比,把三种方案列出来更直观:

对比项SPI Flash (如 W25Q64)SPI EEPROM (如 25LC256)MR25H40CDF (MRAM)
写入前是否需要擦除需要,按扇区擦不需要不需要
字节写入延迟页编程约 0.7ms,扇区擦除约 45ms约 5ms无延迟,总线速度即写入速度
擦写寿命约 10 万次约 100 万次10^14 次以上
随机写入效率差,需整页读改写一般极好,任意地址直接写
掉电数据保持20 年200 年20 年
单位容量成本低中高

工业场景里最要命的是"写延迟"和"寿命"。比如一个电机控制器每 10ms 记录一次运行状态,用 Flash 的话,擦除周期根本撑不住;用 EEPROM,5ms 的写延迟会阻塞主循环。MRAM 在这两点上直接解决问题,代价是贵。所以选型逻辑很清楚:数据更新频繁、对写入实时性有要求、产品寿命周期长,就上 MRAM;只是存配置参数、偶尔写一次,Flash 或 EEPROM 更划算。

1.3 整体设计思路

我的方案思路是:把 MR25H40CDF 挂到 dsPIC33EP512MU814 的 SPI2 模块上,用硬件片选(CS)控制,SPI 时钟先跑 10MHz 验证稳定性,再往上调。软件上分三层——底层 SPI 驱动、中间 MRAM 读写接口、上层业务数据管理。这样分层的好处是,以后换主控或换存储芯片,只需要改底层,业务代码不动。

提示:MR25H40CDF 支持 SPI 模式 0 和模式 3,dsPIC33EP 的 SPI 模块两种都能配,但建议统一用模式 0(CPOL=0, CPHA=0),时序最直观,示波器上好看。

2. 硬件连接与 SPI 时序关键细节

2.1 引脚连接与硬件片选

MR25H40CDF 是 SOIC-8 封装,引脚定义如下:

  • 引脚 1:CS(片选,低有效)
  • 引脚 2:SO(数据输出,接主控 SDI)
  • 引脚 3:WP(写保护,低有效,不用时接 VDD)
  • 引脚 4:VSS(地)
  • 引脚 5:SI(数据输入,接主控 SDO)
  • 引脚 6:SCK(时钟)
  • 引脚 7:HOLD(保持,低有效,不用时接 VDD)
  • 引脚 8:VDD(3.3V)

接到 dsPIC33EP512MU814 上,我用的映射是:

  • CS → RPINR22 对应的 SDO2/SCK2 组里的 CS 引脚,或者直接用普通 GPIO 做软件片选
  • SCK → SCK2(RP 引脚映射)
  • SI → SDO2
  • SO → SDI2

这里有个坑要提前说:dsPIC33EP 的 SPI 外设引脚是通过 PPS(外设引脚选择)寄存器映射的,不是固定引脚。你必须先配置 RPINR22/RPINR23 把 SDI2、SCK2 映射到具体 RP 引脚,再配置 RPOR 把 SDO2 映射出去。很多人第一次用 dsPIC 的 SPI,卡在这里半天出不来波形。

硬件片选和软件片选的选择上,我建议:如果 SPI 总线上只挂 MRAM 一个从设备,用硬件片选(SPI 模块自动控制 CS)最省事;如果总线上还挂了别的 SPI 器件,必须用软件片选(GPIO 手动拉低拉高),否则会误选。我这次总线上只有 MRAM,所以用了硬件片选,但代码里保留了软件片选的宏开关,方便以后扩展。

2.2 SPI 模式与时钟计算

MR25H40CDF 支持的最高 SPI 时钟是 40MHz(读操作),写操作也是 40MHz。dsPIC33EP512MU814 在 70 MIPS 下,SPI 时钟源是 Fcy(=35MHz),通过 SPIxCON1 里的 PPRE 和 SPRE 分频。

计算公式:

SPI 时钟 = Fcy / (2 * (PPRE + 1) * (SPRE + 1))

我要 10MHz,代入 Fcy=35MHz:

10MHz = 35MHz / (2 * (PPRE+1) * (SPRE+1)) => (PPRE+1)*(SPRE+1) = 1.75

这个除不尽,所以实际取 PPRE=0, SPRE=0 得到 17.5MHz,或者 PPRE=1, SPRE=0 得到 8.75MHz。我先用 8.75MHz 跑通,再切到 17.5MHz 测稳定性。实测 17.5MHz 下连续读写 1 小时无错误,示波器看 SCK 上升沿干净,就没有再往上调——工业产品留余量比压极限重要。

注意:SPI 时钟极性要和 MRAM 的模式匹配。模式 0 是 SCK 空闲低电平,数据在上升沿采样;模式 3 是空闲高电平,下降沿采样。配错模式的现象是读出来全是 0xFF 或 0x00,别怀疑芯片坏了,先查 CPOL/CPHA。

2.3 PCB 布局的几个经验点

SPI 跑上 10MHz 以上,PCB 布局就不能随便了。我踩过的坑:

  • SCK 走线尽量短,远离模拟信号和电源开关节点,否则时钟串扰会让数据出错
  • CS 走线不要和 SCK 平行长距离走,容易耦合
  • MRAM 的 VDD 旁边必须放 0.1uF 陶瓷电容,越近越好,我放在引脚 8 和 4 之间 2mm 位置
  • 如果 SPI 走线超过 10cm,建议在 SCK 上串 22Ω 电阻做阻抗匹配,抑制过冲

这些不是玄学,是实测出来的。有一次板子 SCK 走线 15cm 且和 DC-DC 电感挨着,读 MRAM 随机出错,加了串阻并挪走线之后一次通过。

3. dsPIC33EP 上的 SPI 驱动实现

3.1 初始化代码逐行拆解

先上初始化代码,我用的 XC16 编译器,寄存器直接操作,不依赖 Harmony 框架(工业项目里很多人不喜欢框架的臃肿)。

void MRAM_SPI_Init(void) { // 1. 关闭 SPI2 再配置 SPI2CON1bits.SPIEN = 0; // 2. 引脚映射:SDI2 -> RPxx, SCK2 -> RPxx // 假设 SDI2 映射到 RP10, SCK2 映射到 RP11 RPINR22bits.SDI2R = 10; // SDI2 输入引脚 RPINR22bits.SCK2R = 11; // SCK2 输入引脚 // 3. SDO2 输出映射到 RP12 RPOR6bits.RP12R = 0b01011; // SDO2 功能码,查数据手册确认 // 4. 配置 SPI2 控制寄存器 SPI2CON1bits.CKP = 0; // 模式0:空闲低电平 SPI2CON1bits.CKE = 1; // 上升沿采样 SPI2CON1bits.MODE16 = 0; // 8位模式 SPI2CON1bits.MSTEN = 1; // 主机模式 SPI2CON1bits.PPRE = 1; // 主分频 4:1 SPI2CON1bits.SPRE = 0; // 从分频 1:1 -> 实际 8.75MHz // 5. 使能 SPI2 SPI2CON1bits.SPIEN = 1; }

逐行说几个关键点:

第 2、3 步的 PPS 映射是最容易出错的。RPINR22 的 SDI2R 和 SCK2R 字段决定输入引脚,RPOR 寄存器决定输出引脚。功能码每个型号不一样,dsPIC33EP512MU814 的 SDO2 功能码是 0b01011,这个必须查具体型号的数据手册"Peripheral Pin Select"章节,不能照抄别的型号。

第 4 步的 CKE 和 CKP 组合决定 SPI 模式。模式 0 是 CKP=0, CKE=1。我见过有人配成 CKP=0, CKE=0,结果变成模式 1,读出来数据错位。这两个位要一起看。

PPRE 和 SPRE 的实际分频比:PPRE=1 是 4:1,SPRE=0 是 1:1,所以总分频是 241=8,35MHz/8=4.375MHz?等等,这里我算错了,重新算:公式是 Fcy/(2*(PPRE+1)(SPRE+1)),PPRE=1 时 (PPRE+1)=2,SPRE=0 时 (SPRE+1)=1,所以 35/(22*1)=8.75MHz。对,没错。

3.2 字节读写函数

SPI 是全双工,读和写本质是同一个动作:往 SPI2BUF 写一个字节,同时从 SPI2BUF 读一个字节。所以底层只需要一个字节交换函数:

uint8_t MRAM_SPI_Exchange(uint8_t txData) { SPI2BUF = txData; // 写入发送缓冲 while (!SPI2STATbits.SPIRBF); // 等待接收完成 return SPI2BUF; // 读取接收缓冲 }

这个函数看着简单,但有个细节:必须先写 SPI2BUF 再等 SPIRBF,不能反过来。因为 SPIRBF 标志是接收缓冲满的标志,如果先等再写,第一次调用会死等。另外,读 SPI2BUF 会自动清 SPIRBF,所以不需要手动清标志。

3.3 MRAM 命令集与读写时序

MR25H40CDF 的命令集很精简:

命令编码功能
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

注意 MRAM 和 EEPROM 不同,写之前必须发 WREN,写完自动回到写禁止状态。这点和 EEPROM 一样,但 MRAM 没有"等待写完成"的轮询,因为写入是即时的。

读时序:拉低 CS → 发 0x03 → 发 24 位地址(MR25H40CDF 是 4Mbit=512KB,需要 19 位地址,但按 24 位发,高 5 位忽略)→ 连续读数据 → 拉高 CS。

写时序:拉低 CS → 发 0x06(WREN)→ 拉高 CS → 拉低 CS → 发 0x02 → 发 24 位地址 → 发数据 → 拉高 CS。

注意:WREN 必须单独一个 CS 周期,不能和 WRITE 命令连在同一个 CS 低电平周期里。我第一次写的时候图省事把 WREN 和 WRITE 放一起,结果写不进去,查了半天手册才发现这个要求。

3.4 完整读写函数实现

#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_CS_LOW(); MRAM_SPI_Exchange(MRAM_CMD_WREN); MRAM_CS_HIGH(); MRAM_CS_LOW(); MRAM_SPI_Exchange(MRAM_CMD_WRITE); MRAM_SPI_Exchange((addr >> 16) & 0xFF); MRAM_SPI_Exchange((addr >> 8) & 0xFF); MRAM_SPI_Exchange(addr & 0xFF); MRAM_SPI_Exchange(data); MRAM_CS_HIGH(); } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; MRAM_CS_LOW(); MRAM_SPI_Exchange(MRAM_CMD_READ); MRAM_SPI_Exchange((addr >> 16) & 0xFF); MRAM_SPI_Exchange((addr >> 8) & 0xFF); MRAM_SPI_Exchange(addr & 0xFF); data = MRAM_SPI_Exchange(0xFF); MRAM_CS_HIGH(); return data; }

批量读写就是把单字节循环,但要注意:MRAM 支持连续地址自动递增,所以批量读的时候可以一次 CS 周期读完整个块,效率比逐字节高得多。我实测读 512 字节,逐字节方式(每次重新发命令+地址)耗时约 1.2ms,连续读方式只要约 0.15ms,差了 8 倍。

4. 数据存储策略与掉电保护设计

4.1 为什么需要存储策略

MRAM 虽然写入快、寿命长,但不代表可以随便写。工业场景里数据分几类:配置参数(偶尔改)、运行日志(频繁写)、掉电快照(关键时刻写)。不同数据用不同策略,才能既保证可靠性又不过度消耗。

我的做法是把 512KB 空间分区:

  • 0x00000 - 0x00FFF:系统配置区,存设备参数、校准系数,双备份+CRC
  • 0x01000 - 0x3FFFF:运行日志区,环形缓冲,每条记录带时间戳和 CRC
  • 0x40000 - 0x7FFFF:掉电快照区,存最近一次运行状态,上电恢复用

4.2 双备份+CRC 的配置存储

配置数据最怕的是写到一半掉电,导致数据损坏。我的方案是双备份:A 区和 B 区各存一份,每份带 CRC16 校验。写入时先写 B 区,校验通过后再写 A 区;读取时先读 A 区,CRC 错则读 B 区。

typedef struct { uint16_t crc; uint16_t seq; // 写入序号,用于判断哪份更新 uint8_t data[62]; } ConfigBlock; bool Config_Save(uint8_t *cfg, uint16_t len) { ConfigBlock blk; blk.seq = GetNextSeq(); memcpy(blk.data, cfg, len); blk.crc = CRC16((uint8_t*)&blk.seq, len + 2); // 先写 B 区 MRAM_WriteBlock(CFG_B_ADDR, (uint8_t*)&blk, sizeof(blk)); // 回读校验 ConfigBlock verify; MRAM_ReadBlock(CFG_B_ADDR, (uint8_t*)&verify, sizeof(verify)); if (verify.crc != blk.crc) return false; // 再写 A 区 MRAM_WriteBlock(CFG_A_ADDR, (uint8_t*)&blk, sizeof(blk)); return true; }

这个 seq 字段很关键。如果 A 区和 B 区 CRC 都正确,就比 seq,取大的那个,保证拿到最新数据。

4.3 环形日志缓冲的实现

日志区用环形缓冲,写指针到达区尾就回绕到区头。每条记录格式:

[4字节时间戳][2字节长度][N字节数据][2字节CRC]

写指针和读指针存在 MRAM 固定位置,每次写日志前先更新指针。这里有个技巧:指针更新和日志写入要保证原子性,我的做法是先写日志数据,再更新指针,这样即使掉电,最多丢最后一条,不会破坏已有数据。

环形缓冲满了怎么办?两种策略:覆盖最旧(适合只关心最近数据的场景)或停止写入并报警(适合数据不能丢的场景)。我选的是覆盖最旧,因为工业日志通常只关心最近一段时间的运行状态。

4.4 掉电快照与上电恢复

掉电快照是这套方案的核心价值之一。dsPIC33EP 有低电压检测(LVD)模块,配置成 2.9V 触发中断。掉电中断里,把关键变量打包写入 MRAM 快照区,因为 MRAM 写入无延迟,从检测到掉电到写完 256 字节,实测只要约 30us,而电源从 3.3V 掉到 2.7V(dsPIC 最低工作电压)通常有几百 us 到几 ms 的余量,完全来得及。

void __attribute__((interrupt, no_auto_psv)) _LVDInterrupt(void) { IFS4bits.LVDIF = 0; // 紧急保存 SnapshotBlock snap; snap.magic = 0x5A5A; snap.state = g_machineState; snap.position = g_currentPos; snap.crc = CRC16((uint8_t*)&snap, sizeof(snap)-2); MRAM_WriteBlock(SNAP_ADDR, (uint8_t*)&snap, sizeof(snap)); while(1); // 等待电源彻底掉下 }

上电时读快照区,magic 和 CRC 都对,就恢复状态,否则用默认值初始化。

提示:LVD 中断里不要做浮点运算、不要调用复杂函数,只做最必要的保存。我见过有人在掉电中断里做字符串格式化,结果还没写完电就没了。

5. 实测数据与性能分析

5.1 读写速度实测

我用 dsPIC33EP512MU814 在 70 MIPS 下,SPI 时钟 8.75MHz 和 17.5MHz 两档做了测试:

操作8.75MHz 耗时17.5MHz 耗时
单字节写(含 WREN)4.2us2.3us
单字节读3.1us1.7us
512 字节连续写480us250us
512 字节连续读470us240us
256 字节掉电快照32us18us

这个速度在工业场景里完全够用。对比一下,如果用 SPI Flash 写 512 字节,光擦除就要 45ms,差了近 100 倍。

5.2 长期稳定性测试

我做了 72 小时连续读写测试,每 10ms 写一次 64 字节日志,同时随机读校验:

  • 8.75MHz:72 小时零错误,累计写入约 2590 万次
  • 17.5MHz:前 48 小时零错误,第 52 小时出现 3 次读错误,重启后恢复

分析下来,17.5MHz 下的偶发错误可能是 PCB 走线反射导致,因为我的测试板 SCK 走线约 8cm 且没加串阻。后来加了 22Ω 串阻再测 72 小时,零错误。所以工业产品建议 SPI 时钟留 50% 余量,8.75MHz 是稳妥选择,17.5MHz 需要配合良好的 PCB 设计。

5.3 功耗表现

MRAM 的功耗比 Flash 低不少,因为写入不需要电荷泵。实测:

  • 待机电流:MR25H40CDF 约 10uA
  • 读电流(8.75MHz):约 3mA
  • 写电流(8.75MHz):约 5mA

对比 SPI Flash 写入时动辄 15mA 以上的峰值电流,MRAM 对电源设计友好很多,尤其适合电池供电的工业传感器节点。

6. 常见问题与排查实录

6.1 读出来全是 0xFF 或 0x00

这是最常见的现象,排查顺序:

  1. 先查 CS 有没有拉低。用示波器看 CS 引脚,如果一直是高,说明片选没生效。硬件片选的话查 SPI 模块配置,软件片选的话查 GPIO 方向。
  2. 查 SCK 有没有波形。没波形说明 SPI 没使能或引脚映射错。dsPIC 的 PPS 映射错了,SCK 引脚上什么都不会有。
  3. 查 SPI 模式。模式配错,数据会错位,但通常不是全 0xFF。全 0xFF 更像是 MISO 一直高,说明从设备没响应。
  4. 查供电。MRAM 的 VDD 如果没上电或电压不对,SO 引脚会浮空,读出来就是随机值或全高。

6.2 写入后读出来不对

写入不成功,按这个顺序查:

  • WREN 有没有单独发?前面说过,WREN 必须独立 CS 周期
  • WP 引脚是不是被拉低了?WP 低电平会禁止写状态寄存器,虽然不禁止写数据,但有些批次芯片行为不一致,建议接高
  • 地址有没有超范围?MR25H40CDF 是 512KB,地址 0x00000-0x7FFFF,超过就回绕
  • 写完之后有没有等足够时间?MRAM 写入是即时的,但 SPI 传输需要时间,如果 CS 拉高太早,最后一个字节可能没传完

6.3 偶发数据错误

偶发错误最难查,我的排查清单:

现象可能原因排查方法
高温下出错时序余量不足降低 SPI 时钟,看是否改善
振动时出错接触不良检查焊点、连接器
随机地址出错电源噪声示波器看 VDD 纹波,加去耦电容
特定数据模式出错串扰查 SCK 与数据线间距,加串阻
上电初期出错电源未稳加电源监控,延时初始化

6.4 掉电保存失败

掉电保存失败通常是两个原因:检测太晚或保存太慢。检测太晚是 LVD 阈值设太低,等检测到的时候电已经不够了;保存太慢是中断里做了太多事。我的经验是 LVD 阈值设 2.9V,中断里只做纯内存拷贝和 SPI 传输,不调用任何库函数。

另外,超级电容或大电容是掉电保存的好帮手。我在 VDD 上并了 1000uF 电容,掉电后能维持约 5ms,给保存留足时间。这个电容值要根据实际功耗算:I=5mA,V 从 3.3 掉到 2.7 允许 0.6V 压降,t = CΔV/I = 1000uF0.6/5mA = 120ms,绰绰有余。

7. 几个让我印象深刻的实操心得

7.1 PPS 映射一定要查对应型号手册

dsPIC33EP 系列不同型号的 PPS 功能码不一样。我有一次照搬 dsPIC33EP256MU806 的代码到 512MU814 上,SDO2 功能码写错了,结果 SCK 有波形但 SDO 没输出,查了两天才发现是功能码问题。记住:PPS 功能码必须查你手上那颗芯片的数据手册,不能跨型号抄。

7.2 MRAM 的写保护引脚别悬空

MR25H40CDF 的 WP 和 HOLD 引脚,不用的时候一定要接 VDD,不能悬空。悬空的话引脚电平不确定,可能导致随机写保护或保持状态,表现为偶发写入失败。我第一版板子图省事把这两个脚悬空了,结果现场偶发写不进去,返工加了上拉电阻才好。

7.3 SPI 时钟不是越高越好

很多人追求 SPI 跑满速,但工业产品里稳定性优先。我实测 8.75MHz 和 17.5MHz 在良好 PCB 下都能稳定工作,但 8.75MHz 的时序余量更大,对电源波动、温度变化的容忍度更高。如果应用对速度不是极度敏感,建议 SPI 时钟留一半余量。

7.4 数据校验不能省

MRAM 本身很可靠,但 SPI 传输、电源波动、宇宙射线都可能引入位翻转。工业产品里,关键数据必须带 CRC。我用的 CRC16-CCITT,多项式 0x1021,查表法实现,512 字节校验耗时约 20us,完全可以接受。

7.5 日志区要预留磨损均衡的余地

虽然 MRAM 寿命几乎无限,但环形缓冲如果一直写同一片区域,理论上还是有局部磨损。我的做法是日志区用大一点的环形缓冲(256KB),写指针均匀遍历整个区域,避免热点。这个习惯是从 Flash 时代带过来的,用在 MRAM 上属于过度设计,但成本几乎为零,何乐而不为。

8. 方案扩展与后续优化方向

这套方案跑通之后,我做了几个扩展尝试,效果不错,分享出来供参考。

第一个扩展是把 MRAM 当 SPI 总线的共享存储。dsPIC33EP512MU814 有多个 SPI 模块,我把 MRAM 挂在 SPI2 上,同时让 SPI1 接另一颗协处理器,两颗芯片通过 MRAM 交换数据。因为 MRAM 读写无延迟,协处理器写完数据立即可读,比通过 UART 或双口 RAM 简单得多。

第二个扩展是用 MRAM 做参数自适应的存储后端。工业设备经常需要根据现场情况调整 PID 参数、滤波系数,这些参数改一次就要存一次。用 MRAM 之后,参数修改可以实时落盘,不需要攒一批再写,调试体验提升明显。

第三个方向是结合 DMA 做零拷贝传输。dsPIC33EP 的 SPI 支持 DMA,配置好之后,大批量数据读写不需要 CPU 干预,CPU 可以去做控制算法。我测过用 DMA 读 4KB 数据,CPU 占用从 15% 降到 2%,对实时性要求高的场景很有价值。

后续如果要做产品化,我还会加上:MRAM 的 ECC 校验(部分型号支持)、双 MRAM 冗余、以及基于时间戳的数据版本管理。这些在工业高可靠性场景里都是加分项。

最后分享一个我在调试时的小技巧:用逻辑分析仪抓 SPI 波形时,把 CS、SCK、MOSI、MISO 四路一起抓,设置 CS 下降沿触发。这样每次读写都能完整看到命令、地址、数据的时序,比单看某一根线效率高得多。我用的是一款 8 通道 100MHz 采样率的逻辑分析仪,价格不贵,但排查 SPI 问题几乎离不开它。

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

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

立即咨询