☰
MRAM与PIC18F86K90实战:高可靠SPI存储方案设计与实现
2026/10/5 6:12:17 网站建设 项目流程

1. 为什么 MR25H40CDF 加 PIC18F86K90 是个值得聊的组合

搞嵌入式存储的兄弟大多有过这种纠结:用 EEPROM 吧,写入速度慢得让人抓狂,擦写次数也就百万次级别;用 Flash 吧,写之前还得先擦一个扇区,掉电保护做不好数据直接丢;用 SRAM 加后备电池吧,体积大、成本高,电池还有寿命问题。我前几年做一个工业数据采集终端的时候就卡在这个点上,设备要每秒钟记录一次传感器数据,断电不能丢,还要能扛住现场的高低温循环,选来选去都不太满意。

后来接触到 MRAM 这个品类,具体型号就是MR25H40CDF,才算把这个问题理顺了。MRAM 全称是磁性随机存储器,它的存储单元用的是磁隧道结,靠磁化方向来记录 0 和 1,而不是电荷。这个物理机制决定了它几个很硬核的特性:写入不需要擦除、写入速度接近 SRAM、擦写寿命理论上无限、断电数据不丢。MR25H40CDF 是 4Mbit 容量的 SPI 接口 MRAM,也就是 512KB,对于大多数嵌入式参数存储、日志记录、掉电保护场景来说,这个容量相当够用。

主控这边我选的是PIC18F86K90,Microchip 自家的一款 8 位单片机,80 引脚,128KB Flash,4KB RAM,自带多个 MSSP 模块可以配成 SPI 主机。选它有个很实际的原因:它和 MR25H40CDF 同属 Microchip 生态,数据手册、参考代码、勘误表都在一个体系里,调试的时候少踩很多跨厂商的坑。而且 PIC18F86K90 的 SPI 模块支持硬件片选和多种时钟极性配置,跟 MRAM 的时序匹配起来很省心。

这套组合能干什么?简单说就是:在工业和嵌入式场景下,做高可靠、高频次、掉电安全的数据存储与读取。适合谁看?如果你正在做工业控制器、电力监测终端、医疗设备、车载记录仪这类对数据完整性要求高的项目,或者你单纯想从 EEPROM/Flash 迁移到更可靠的存储方案,这篇内容应该能帮你少走弯路。下面我会从方案选型、硬件连接、SPI 时序配置、读写实操、常见问题几个维度,把整套东西拆开讲清楚。

2. 方案选型背后的逻辑与整体设计思路

2.1 为什么不用 EEPROM 和 Flash,偏要上 MRAM

先把这个"为什么"说透,不然选型就是拍脑袋。EEPROM 的写入机制是 Fowler-Nordheim 隧穿,写一个字节大概需要 5ms 左右,擦写寿命典型值 100 万次。如果你每秒写一次,一天就是 86400 次,100 万次大概 11 天就写废了。当然实际项目不会这么极端,但高频写入场景下 EEPROM 的寿命焦虑是真实存在的。

Flash 的问题在于写入粒度。NOR Flash 可以按字节读,但写之前必须擦除整个扇区,典型扇区 4KB。这意味着你要改一个字节,得先把 4KB 读到 RAM,改完再擦再写回去。这个过程耗时长,而且擦除期间如果掉电,整个扇区的数据就没了。很多项目为了规避这个问题,搞双备份扇区加磨损均衡算法,代码复杂度直接上去了。

MRAM 的写入是磁化翻转,不需要擦除,写入时间在几十纳秒量级,擦写次数官方标称 10^14 次以上,实际基本可以当无限次用。MR25H40CDF 的 SPI 接口最高支持 40MHz 时钟,读写都是按字节操作,没有扇区概念。这就意味着你可以像操作 SRAM 一样操作它,代码逻辑简单太多。

我整理了一个对比表,方便你直观感受差异:

特性EEPROMNOR FlashMR25H40CDF (MRAM)
写入前是否需擦除否是(按扇区)否
典型写入时间5ms/字节擦除 100ms+ 写入 10us几十 ns
擦写寿命100 万次10 万次10^14 次以上
掉电数据保持是是是
接口I2C/SPISPISPI
容量(本型号)--4Mbit / 512KB
写入功耗较高高(擦除时)低

从表里能看出来,MRAM 在写入速度、寿命、操作简便性上是全面占优的,代价就是单位容量成本比 Flash 高。所以选型的判断标准很简单:如果你的项目写入频繁、对掉电安全敏感、代码想写简单点,MRAM 值这个钱;如果只是存个配置参数、一年写不了几次,那 EEPROM 就够了。

2.2 PIC18F86K90 的 SPI 资源怎么分配

PIC18F86K90 有两个 MSSP 模块,MSSP1 和 MSSP2,都可以配置成 SPI 模式。我一般把 MSSP1 留给 MRAM,MSSP2 留给其他外设比如显示屏或者第二颗存储芯片。这样分配的好处是中断和状态寄存器互不干扰,调试的时候逻辑清晰。

PIC18F86K90 的 SPI 主机模式支持四种时钟极性/相位组合,也就是常说的 Mode 0 到 Mode 3。MR25H40CDF 支持 Mode 0 和 Mode 3,我习惯用 Mode 0,即 CPOL=0、CPHA=0,时钟空闲为低电平,数据在上升沿采样。这个模式最直观,示波器上看波形也容易对齐。

引脚分配上,我用的方案是:

  • RC3 作为 SCK(时钟)
  • RC5 作为 SDO(主机输出,接 MRAM 的 SI)
  • RC4 作为 SDI(主机输入,接 MRAM 的 SO)
  • RA5 作为 CS(片选,普通 GPIO 控制)

这里有个细节值得说:PIC18F86K90 的 MSSP 模块在 SPI 主机模式下,SS 引脚(RB5 或 RA5,取决于配置)如果配置成输入,会被当作多主机模式的冲突检测引脚。如果你只用单主机,一定要把 SS 配置成普通 GPIO 或者输出模式,否则可能出现莫名其妙的传输中止。我一开始就踩过这个坑,CS 用软件控制但 SS 引脚没处理好,SPI 传输时不时就卡住,查了半天才发现是 SS 引脚在作怪。

2.3 整体数据流设计

整套系统的数据流是这样的:PIC18F86K90 采集传感器数据,经过处理后通过 SPI 总线写入 MR25H40CDF 的指定地址;需要读取时,主机发起读命令,MRAM 把数据通过 MISO 送回。MRAM 内部没有地址自增的限制,你可以连续读写整个 512KB 空间,也可以随机访问任意地址。

我在项目里把 512KB 空间做了分区规划:

  • 0x00000 - 0x00FFF:系统配置区,存设备参数、校准系数
  • 0x01000 - 0x7FFFF:数据日志区,环形写入
  • 0x80000 - 0xFFFFF:预留区,存固件升级标志和版本信息

这种分区方式的好处是配置区和日志区物理隔离,日志写满环形覆盖的时候不会碰到配置数据。环形写入的写指针存在 MRAM 的固定位置,每次写入前先读指针、写数据、更新指针,三步操作。因为 MRAM 写入不需要擦除且速度快,这三步可以在几微秒内完成,掉电窗口极小。

3. 硬件连接与 SPI 时序配置的实操细节

3.1 硬件连线与去耦要点

MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很标准:CS、SO、SI、SCK、VDD、VSS、WP、HOLD。WP 是写保护,HOLD 是暂停通信。在正常使用中,WP 和 HOLD 都要拉高,否则芯片可能进入保护状态或者暂停响应。

连线本身不复杂,但有几个地方容易出问题。第一是去耦电容,MRAM 在写入瞬间电流会有波动,我一般在 VDD 和 VSS 之间放一个 0.1uF 的陶瓷电容,位置尽量靠近芯片引脚,走线越短越好。第二是 SPI 走线,如果 MRAM 和主控距离超过 10cm,建议在 SCK 线上串一个 22 到 33 欧姆的电阻,抑制反射。第三是 CS 线,软件片选的情况下 CS 翻转频繁,这条线不要和其他高速信号平行走太长,避免串扰。

我实际用的电路连接是这样的:

// PIC18F86K90 引脚配置 // RC3 -> SCK -> MR25H40CDF SCK // RC5 -> SDO -> MR25H40CDF SI // RC4 -> SDI <- MR25H40CDF SO // RA5 -> CS -> MR25H40CDF CS // MR25H40CDF WP -> VDD (通过 10K 上拉) // MR25H40CDF HOLD -> VDD (通过 10K 上拉)

注意:WP 和 HOLD 不要直接接 VDD 而不加上拉电阻,虽然理论上可以,但加上拉电阻能在上电时序不稳定时提供更好的保护。我见过有人直接短接,结果上电瞬间 MRAM 进入写保护,排查了很久。

3.2 SPI 初始化代码与参数计算

PIC18F86K90 的 SPI 时钟频率由 SSPxCON1 寄存器的 SSPxM 位和系统时钟共同决定。公式是:

Fosc / (4 * (SSPxM + 1))

假设系统时钟 Fosc 是 64MHz(PIC18F86K90 用 16MHz 晶振加 4 倍频 PLL),我想要 10MHz 的 SPI 时钟,那么:

10MHz = 64MHz / (4 * (SSPxM + 1)) SSPxM + 1 = 64 / 40 = 1.6 SSPxM = 0.6,取整为 0,实际时钟 = 64 / 4 = 16MHz

16MHz 对 MR25H40CDF 来说完全在 40MHz 上限之内,所以直接用 SSPxM=0 就行。如果你系统时钟更低,比如 16MHz,那 SSPxM=0 时 SPI 时钟是 4MHz,也够用。

初始化代码大概长这样:

void SPI1_Init(void) { // 配置引脚方向 TRISC3 = 0; // SCK 输出 TRISC5 = 0; // SDO 输出 TRISC4 = 1; // SDI 输入 TRISA5 = 0; // CS 输出 // 关闭模拟功能(如果有) ANSELC = 0x00; ANSELA = 0x00; // CS 初始拉高 LATA5 = 1; // MSSP1 配置 SSP1STAT = 0x40; // 数据在上升沿采样,时钟空闲低电平 SSP1CON1 = 0x20; // SPI 主机模式,时钟 = Fosc/4 SSP1CON3 = 0x00; // 关闭所有高级功能 // 清空缓冲区 uint8_t dummy = SSP1BUF; } uint8_t SPI1_Transfer(uint8_t data) { SSP1BUF = data; while(!PIR1bits.SSP1IF); // 等待传输完成 PIR1bits.SSP1IF = 0; return SSP1BUF; }

这里有个经验:SPI 传输函数里等待标志位的时候,不要用超时机制去打断,除非你有特殊需求。因为 SPI 传输本身很快,正常情况下几个时钟周期就完成了,如果卡住说明硬件有问题,超时返回反而会掩盖故障。我早期加过超时,结果一次 CS 线虚焊,程序一直返回错误码但没报硬件故障,查了两天才定位到。

3.3 MR25H40CDF 的命令集与操作时序

MR25H40CDF 的命令集很简洁,常用的就几条:

命令操作码说明
WREN0x06写使能,写操作前必须发
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

写操作的完整时序是:拉低 CS -> 发 WREN -> 拉高 CS -> 拉低 CS -> 发 WRITE -> 发 3 字节地址 -> 发数据 -> 拉高 CS。注意 WREN 和 WRITE 之间必须拉高 CS 再拉低,这是芯片要求的时序,不能连着发。

读操作简单一些:拉低 CS -> 发 READ -> 发 3 字节地址 -> 读数据 -> 拉高 CS。读操作不需要 WREN。

地址是 24 位的,因为 512KB 需要 19 位地址,用 3 字节覆盖绰绰有余。高位地址字节在前,这是 SPI 存储器的通用约定。

4. 读写实操:从单字节到环形日志的完整实现

4.1 单字节读写与页写优化

先看最基础的字节读写函数:

void MRAM_WriteByte(uint32_t addr, uint8_t data) { LATA5 = 0; // CS 拉低 SPI1_Transfer(0x06); // WREN LATA5 = 1; // CS 拉高 LATA5 = 0; // CS 拉低 SPI1_Transfer(0x02); // WRITE SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); SPI1_Transfer(data); LATA5 = 1; // CS 拉高 } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; LATA5 = 0; SPI1_Transfer(0x03); // READ SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); data = SPI1_Transfer(0x00); // 发送 dummy 字节读取数据 LATA5 = 1; return data; }

单字节操作简单但效率低,每次都要发命令和地址。MRAM 支持连续读写,你可以在发完起始地址后连续传输多个字节,地址会自动递增。所以批量写入可以这样优化:

void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { LATA5 = 0; SPI1_Transfer(0x06); // WREN LATA5 = 1; LATA5 = 0; SPI1_Transfer(0x02); // WRITE SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); for(uint16_t i = 0; i < len; i++) { SPI1_Transfer(buf[i]); } LATA5 = 1; }

这里有个关键点:MRAM 没有页边界限制。Flash 通常一页 256 字节,跨页写会回绕,需要特殊处理。MRAM 是真正的线性地址空间,你从任意地址开始连续写多少字节都行,地址自动递增到芯片末尾才回绕。这让代码简单了很多,不用算页边界。

4.2 环形日志区的实现

工业场景下最常见的需求是循环记录数据,写满覆盖最旧的数据。我在 MRAM 里划了一块 480KB 的区域做环形日志,每条记录 32 字节,能存 15360 条。写指针和读指针存在配置区,每次操作前先读出来。

环形写入的核心逻辑:

#define LOG_START_ADDR 0x01000 #define LOG_END_ADDR 0x7FFFF #define LOG_RECORD_SIZE 32 #define LOG_MAX_RECORDS ((LOG_END_ADDR - LOG_START_ADDR + 1) / LOG_RECORD_SIZE) void Log_WriteRecord(uint8_t *record) { uint32_t write_ptr = MRAM_ReadDWord(CFG_WRITE_PTR_ADDR); uint32_t addr; // 计算实际地址 addr = LOG_START_ADDR + (write_ptr % LOG_MAX_RECORDS) * LOG_RECORD_SIZE; // 写入记录 MRAM_WriteBuffer(addr, record, LOG_RECORD_SIZE); // 更新写指针 write_ptr++; MRAM_WriteDWord(CFG_WRITE_PTR_ADDR, write_ptr); }

这里有个隐患:写指针更新和记录写入是两步操作,如果在这两步之间掉电,指针没更新,下次上电会覆盖同一条记录。对于大多数场景这可以接受,但如果你的数据绝对不能丢,可以用双指针加校验的方式。我在电力监测项目里用的是"记录头带序号"的方案:每条记录的前 4 字节存一个递增序号,上电时扫描最后几条记录,找到最大序号来确定真实写指针。这样即使指针更新失败,也能通过记录内容恢复。

4.3 掉电保护与数据完整性校验

MRAM 本身掉电不丢数据,但主机写入过程中掉电可能导致记录写了一半。为了保证完整性,我在每条记录末尾加了 2 字节 CRC16 校验。读取时先校验 CRC,不通过就认为这条记录无效,跳过。

CRC16 的计算用查表法,速度快:

static const uint16_t crc16_table[256] = { ... }; // 标准 CRC16-CCITT 表 uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for(uint16_t i = 0; i < len; i++) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF]; } return crc; }

写入时先算好 CRC 放在记录末尾,读取时重新算一遍对比。这个开销很小,PIC18F86K90 在 64MHz 下算 32 字节的 CRC 大概几十微秒,完全可以接受。

实操心得:CRC 校验虽然简单,但能挡住绝大多数掉电半写、总线干扰导致的数据错误。我建议所有存到 MRAM 的关键数据都带校验,尤其是配置参数。曾经有个项目因为配置区一个字节被干扰翻转,设备启动后行为异常,查了一整天才定位到。加了 CRC 之后这类问题直接在上电自检时就能发现并恢复默认值。

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

5.1 SPI 通信失败的分层排查法

SPI 调不通是最常见的问题,我总结了一个从物理层到协议层的排查顺序:

排查层级检查项常见问题
物理层供电、地线、去耦VDD 电压不足、地线虚焊
连线层CS/SCK/SI/SO 通断线序接反、虚焊
信号层示波器看波形时钟无输出、CS 不翻转
配置层SPI 模式、时钟极性Mode 不匹配、SS 引脚冲突
协议层命令时序、WREN忘记发 WREN、CS 时序错误

我遇到最多的是配置层问题。有一次用 PIC18F86K90 调 MRAM,波形都正常但读出来全是 0xFF。查了半天发现 SSP1STAT 的 CKE 位设错了,采样边沿不对,数据全错位。后来养成习惯,调 SPI 第一步先用示波器确认 SCK 和 CS 的波形,第二步用逻辑分析仪抓一帧完整数据,对比数据手册的时序图,基本能定位到问题。

5.2 写入不生效的几个原因

MRAM 写入不生效,按概率排序大概是这几个原因:

第一,忘记发 WREN。MR25H40CDF 每次写操作前都必须发 WREN 命令,而且 WREN 和 WRITE 之间要拉高 CS。我见过有人把 WREN 和 WRITE 连着发,结果写不进去。

第二,WP 引脚没拉高。WP 是硬件写保护,低电平有效。如果你把它接地了,所有写操作都会被拒绝。检查一下电路,WP 应该通过上拉电阻接 VDD。

第三,状态寄存器被写保护。MR25H40CDF 的状态寄存器里有块保护位,如果之前被设置过,某些地址区域会被保护。读一下状态寄存器确认。

第四,CS 时序不对。CS 必须在整个命令序列期间保持低电平,中间不能有毛刺。如果 CS 被中断打断拉高了,命令就废了。

5.3 高速 SPI 下的信号完整性问题

当 SPI 时钟超过 20MHz 时,信号完整性问题开始显现。我遇到过 SCK 上升沿过冲导致 MRAM 误采样的情况,表现是偶尔读出错数据,概率大概千分之一。用示波器看 SCK 波形,上升沿有明显的振铃。

解决办法是在 SCK 线上串一个 33 欧姆的电阻,同时缩短走线长度。如果 PCB 已经做好了改不了,可以降低 SPI 时钟到 10MHz 以下,问题基本消失。对于大多数工业应用,10MHz 的 SPI 时钟已经足够,512KB 全读一遍也就 0.4 秒左右。

注意:不要盲目追求高 SPI 时钟。MRAM 标称支持 40MHz,但那是理想条件下的实验室数据。实际 PCB 上受走线、负载、干扰影响,能稳定跑 20MHz 就不错了。我一般默认配 10MHz,稳定压倒一切。

5.4 常见问题速查表

现象可能原因解决方法
读出来全是 0xFFSPI 模式错误、MISO 未连接检查 CPOL/CPHA、确认 SO 线
读出来全是 0x00CS 未拉低、芯片未供电检查 CS 电平和 VDD
写入后读回不一致忘记 WREN、WP 拉低补发 WREN、拉高 WP
偶尔数据错误信号完整性、时钟过快串电阻、降时钟
通信完全无响应引脚配置错误、SS 冲突检查 TRIS、SS 设为 GPIO
掉电后数据丢失写入未完成、无校验加 CRC、优化写入时序

6. 从这套方案延伸出去的几个实用思路

6.1 用 MRAM 做频繁写入的参数存储

很多工业设备需要实时保存运行参数,比如电机的累计运行时间、阀门的开关次数。这些参数变化频繁,用 EEPROM 存寿命堪忧,用 MRAM 就完全没压力。我的做法是在 RAM 里维护一份参数副本,变化时更新 RAM,然后每隔一段时间或者检测到掉电信号时批量写入 MRAM。因为 MRAM 写入快,即使掉电瞬间也能完成几条记录的写入。

掉电检测可以用 PIC18F86K90 的 LVD(低压检测)模块,配置成中断模式。当 VDD 降到阈值以下时触发中断,在中断里把关键数据写进 MRAM。从检测到掉电到电源完全失效,通常有几十毫秒的窗口,足够写几百字节。我实测过,用 1000uF 的储能电容,掉电后能维持 50ms 左右,写 32 字节记录绰绰有余。

6.2 多芯片级联扩展容量

512KB 如果不够用,MR25H40CDF 支持多芯片级联。每个芯片独立 CS,共用 SCK、SI、SO。主机通过拉低不同的 CS 来选择芯片。这样理论上可以扩展到任意容量,代价是占用更多 GPIO 做片选。

级联时要注意 SO 线的处理。多个芯片的 SO 如果直接并联,未选中的芯片 SO 处于高阻态,不会冲突。但有些芯片高阻态漏电流较大,可以在每个 SO 上串一个小电阻隔离。我级联过 4 片,总共 2MB,用 4 个 GPIO 做片选,读写逻辑封装成统一接口,上层代码不用关心具体是哪片。

6.3 数据加密与安全存储

工业设备的数据有时候需要防篡改。MRAM 本身不带加密功能,但可以在写入前用软件加密。PIC18F86K90 的运算能力有限,跑 AES 有点吃力,但跑轻量级的 XOR 混淆或者 TEA 加密还是可以的。我的做法是配置参数用 TEA 加密后存储,读取时解密。这样即使有人把 MRAM 拆下来用编程器读,拿到的也是密文。

TEA 加密在 PIC18F86K90 上跑一次大概 1ms 左右,对于配置参数这种低频操作完全可以接受。密钥存在 PIC 的 Flash 里,配合代码保护功能,安全性够用。

6.4 上电自检与数据恢复策略

设备上电时,我建议做一次存储自检。流程是:读配置区,校验 CRC;如果失败,尝试从备份区恢复;如果备份也失败,加载默认配置并标记异常。日志区则扫描最后几条记录,确定有效写指针。

这套自检逻辑我封装成了一个函数,上电时调用一次,返回存储健康状态。如果状态异常,可以通过通信接口上报,方便现场维护人员判断是否需要更换芯片。实际运行下来,MRAM 出问题的概率极低,但自检机制给了系统一个兜底,心里踏实。

我个人在实际操作中的体会是,MRAM 这类器件的价值不在于它有多快多能写,而在于它把嵌入式存储的复杂度降下来了。你不用再纠结擦除、磨损均衡、掉电窗口这些事,代码可以写得很直白。PIC18F86K90 加 MR25H40CDF 这套组合,硬件上不复杂,软件上也不绕,真正花时间的是把时序调稳、把异常处理做全。最后再分享一个小技巧:调 SPI 的时候,手边备一个逻辑分析仪,比示波器好用得多,一帧数据抓下来,命令、地址、数据一目了然,定位问题的效率能提高好几倍。

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

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

立即咨询