☰
MRAM与PIC18F86J16的SPI数据存储方案:硬件连接与读写实现
2026/10/4 5:27:49 网站建设 项目流程

最近在一个工业控制器项目里,把 4Mbit 的 MRAM 芯片 MR25H40CDF 和 Microchip 的 PIC18F86J16 单片机搭在了一块,用来保存运行参数、故障码和周期性日志。这块 MRAM 最吸引人的地方是写入速度快、擦写寿命几乎不用考虑,而且不像 EEPROM 或 SPI NOR Flash 那样必须先擦除再写入,现场跑数据记录时非常省心。这篇文章把整套组合的电路连接、SPI 读写流程、代码框架和几个调试时踩过的坑整理出来,给正在做嵌入式或工业数据存储方案的朋友一份可以直接参考的实操记录。

1. 从需求看选型:为什么是 MR25H40CDF 与 PIC18F86J16

1.1 工业存储绕不开的三个硬指标

工业设备里的数据存储,和消费电子场景差别很大。参数表可能一天只写几次,但故障记录和运行日志往往是高频写入,甚至在设备运行的每分每秒都在写。如果用的是普通 SPI NOR Flash,很快会撞上擦写寿命的上限,比如常规 NOR Flash 号称十万次擦写,看着不少,但按每分钟记录一条日志来算,用不了几个月就接近极限,整片存储就成了耗材。

很多工程师习惯用铁电存储器 FRAM 来做高频写入,但 FRAM 容量普遍偏小,几 K 到几百 Kbit 常见,要存几十万条日志时得频繁换页或者压缩,逻辑越写越重。MRAM 正好卡在中间:容量能到 Mbit 级,写入延时是纳秒级,掉电以后数据不丢,而且擦写寿命在 10 的 15 次方以上,基本可以当成 SRAM 用,同时又具备非易失特性。MR25H40CDF 这个 4Mbit 容量的芯片,对工业控制器来说是一个很舒服的甜点尺寸,能装下电机参数、配方表、累积运行时间和最近 N 条历史告警。

掉电安全性也是关键指标。Flash 在写入过程中突然断电,有可能留下一个半写状态,轻则当前扇区数据损坏,重则需要整片擦除重来。MRAM 的写入机制是磁性翻转,写入是直接的、原子化的,没有先擦后写的过程,数据完整性明显好很多。配合 PIC18F86J16 这类 8 位单片机做工业控制,整套方案在成本和可靠性之间取得了很好的平衡。

1.2 PIC18F86J16 在方案里扮演什么角色

PIC18F86J16 是 Microchip 的 8 位 MCU,属于 PIC18F J 系列,内部集成多个串行接口、定时器和丰富的中断源,并且通过外设引脚选择功能,可以让不少通信外设灵活映射到不同引脚。这个特性在画 PCB 时特别有用,比如 SPI 的 SCK、SDO、SDI 三者可以分配到那些走线顺、不打架的引脚上,而不用像老式单片机那样被固定引脚绑死。

选择 PIC18F86J16 还有一个原因:它在工业温度范围内稳定,外设资源足够,而且功耗控制做得不错。工业项目里不是所有需求都要上 ARM Cortex-M,很多逻辑其实一个 8 位单片机就能扛住,比如电机启停控制、温度采集、Modbus 从站协议这些场景,8 位机反而更皮实,上电时序、复位逻辑、端口的默认电平都更直观,调试起来没有那么多“隐藏机制”。

用 PIC18F86J16 做存储管理的角色,我认为比用高主频的 32 位机更合适:SPI 跑 5MHz 到 20MHz 已经足够覆盖绝大多数工业数据记录的吞吐量,不需要追求极致的总线速度,稳定性和可维护性才是现场第一位。把复杂逻辑放在上位机或边缘网关,单片机只负责可靠地把数据落盘,系统整体更简单。

1.3 这套组合适合哪些具体场景

先给个场景画像,方便判断是不是戳中你的需求点。

第一类是设备参数自动保存。比如伺服驱动器、变频器、温控仪表,每次修改参数后希望立即掉电保存,MRAM 写入快且不需要整页操作,直接写入指定地址,异常掉电也不担心参数丢一半。

第二类是高频数据记录。一些在线监测设备每秒钟要记录电压、电流、温度等快照,一天下来数据量可能达到几十万条。传统 EEPROM 和电池供电的 SRAM 都不太合适,MRAM 则能轻松应对。MR25H40CDF 有 512KB 空间,如果每条日志 16 字节,可以存三万多条记录,配合双区交替使用,实际容量还能再翻倍。

第三类是引导加载与启动校验。MRAM 里可以放一份启动标志区,记录上次运行状态、看门狗复位次数、固件升级标记,引导程序只需要读几个字节就能决定下一步该进运行态还是恢复态,这套机制在工业设备里非常常用。

2. 把 MR25H40CDF 接到 PIC18F86J16 的硬件细节

2.1 引脚一览与基本读法

MR25H40CDF 是标准的 SPI 接口存储芯片,8 个引脚,功能很直白。先看一张表格,把每个引脚的作用和对单片机的连接写清楚。

引脚名称方向连接到 MCU说明
1CS#输入普通 GPIO 或 PPS 输出片选,低电平有效
2SCK输入SPI 时钟由主设备提供
3SI输入主设备 SDO数据输入到 MRAM
4SO输出主设备 SDI数据从 MRAM 输出
5WP#输入GPIO 或接 3.3V写保护,低电平有效
6HOLD#输入GPIO 或接 3.3V暂停通信,固定拉高
7VSS电源GND地
8VDD电源3.3V供电

这里有个容易忽略的点:WP# 和 HOLD# 如果不用,也必须通过上拉电阻接到 VDD,不能悬空。我见过不少调试案例,最开始 SPI 读写不稳定,排查到最后发现是 HOLD# 没接上拉,线上噪声稍微大一点就意外触发 Hold 功能,SPI 波形直接被截断,数据读出来全是乱的。

2.2 标准 SPI 接线和 PPS 重映射

PIC18F86J16 这类带外设引脚选择功能的单片机,接 SPI 是最灵活的。以某个具体项目为例,我习惯把 MSSP1 的 SCK、SDO、SDI 映射到 R 端口上,让信号线尽量短且远离大电流走线。PPS 配置分两步:先解锁 PPS 锁定寄存器,再对对应寄存器赋值,最后重新加锁。

下面是一段典型的 PPS 初始化代码,注意不同封装和引脚号需要按照数据手册的 PPS 映射表调整。

void PPS_Config_SPI1(void) { // 解锁 PPS 寄存器 PPSLOCK = 0x55; PPSLOCK = 0xAA; PPSLOCK = 0x01; // 输入方向:SDI1 固定连接到某输入引脚,假设是 R18 RPINR20bits.SDI1R = 0x12; // 0x12 对应 R18,具体查数据手册 // 输出方向:SDO1、SCK1 分配到 R19、R20 RPOR4bits.RP19R = 0x09; // 0x09 表示 SDO1 RPOR5bits.RP20R = 0x08; // 0x08 表示 SCK1 // 重新锁定 PPSLOCK = 0x55; PPSLOCK = 0xAA; PPSLOCK = 0x00; }

如果阅读困难,可以换个角度理解:PPS 的本质是一张“内部交叉矩阵”,把外设信号和引脚解耦。好处是硬件设计自由度很大,你可以把 SCK 放到远离干扰源的引脚,甚至有样品板引脚画错后,不重新打板也能通过 PPS 软件调整救回来。代价是每次下载代码后如果 PPS 配置没执行,外设信号不会自动出现在引脚上,这点必须记住。

2.3 供电、去耦和板级布局要点

MR25H40CDF 的工作电压是 3.3V 范围,和 PIC18F86J16 的 I/O 电平完全兼容,不需要额外电平转换。但供电要特别注意,不要直接从单片机的某个 IO 口拉电给存储芯片,电源要单独走一小段星形布线,VDD 引脚旁边放一个 100nF 的陶瓷电容,最好再并联一个 1uF 到 10uF 的电容,用来吸收传输线瞬间的大电流需求。

还有一个在工业环境中非常要命的细节:供电毛刺。设备里如果有继电器、接触器,吸合和断开时会产生微秒级的电流抖动,如果 PCB 布局不给力,MRAM 的 VDD 会被拉到最低工作电压以下,芯片可能进入异常状态,最典型的故障是后续 SPI 写操作无响应。解决方式是在 3.3V 进入存储区域前加一个 RC 或磁珠滤波,且 CPU 和 MRAM 的电源走线不要形成大环。

信号线布线也最好等长、短直,SPI 时钟线不要和其他高频信号平行走太长距离。工业现场电磁环境差,即使芯片本身硬件抗干扰能力不弱,明摆着的噪声问题也不该留给运气去解决。

3. 读写实现:从初始化到数据落盘

3.1 SPI 主模式初始化

在 PIC18F86J16 上,MSSP1 模块配置成 SPI 主模式。下面代码假设系统时钟是 32MHz,选择 SPI 时钟分频为 1/8,即 4MHz 的 SCLK。这个速度对 MR25H40CDF 完全够用,也给信号留了充足裕量。

void SPI1_Init(void) { // 配置引脚方向:SCK、SDO 输出,SDI 输入,CS# 作为 GPIO 输出 TRISBbits.TRISB2 = 0; // CS# TRISBbits.TRISB3 = 0; // SCK TRISBbits.TRISB4 = 0; // SDO TRISBbits.TRISB5 = 1; // SDI LATBbits.LATB2 = 1; // CS# 默认高电平 // 禁用 MSSP1,然后配置相关寄存器 SSP1CON1 = 0x00; SSP1STAT = 0x00; // SCK 空闲为低、数据在上升沿发送 SSP1STATbits.CKE = 1; SSP1STATbits.SMP = 0; // 主模式,时钟分频 Fosc/8 SSP1CON1bits.SSPEN = 0; SSP1CON1bits.CKP = 0; SSP1CON1bits.SSPM = 0b0001; SSP1CON1bits.SSPEN = 1; }

注意 CSP/SCK 极性不要搞反。MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 1,大多数情况下 CPOL=0、CPHA=0,也就是时钟空闲低、芯片在 SCK 第一个上升沿采样 SI 数据。如果你的代码移植自其他芯片厂家的 SPI Nor Flash 驱动,极性往往也是 Mode 0,不用担心。

3.2 写使能与状态寄存器轮询

MRAM 的 SPI 命令体系和 SPI NOR Flash 很像,读操作可以直接执行,写操作必须先发 WREN 写使能命令,再发写命令。MR25H40CDF 不需要擦除,这是和 Flash 最大的区别。

常用命令我整理了一张表,开发时照着查很方便。

命令字节码作用
WREN0x06写使能
WRDI0x04写禁用
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03从指定地址连续读
FAST_READ0x0B加一个 dummy 字节的快速读
WRITE0x02从指定地址开始写入,一次最多写 256 字节

状态寄存器里最重要的位是 Bit0(WIP,Write In Progress)和 Bit1(WEL,Write Enable Latch)。每次写完数据后,要轮询 WIP 直到变为 0,这条冷不丁忘掉,下一笔写入就可能在下一次上电时丢数据。写使能后虽然没有像 Flash 那样严格的页擦除,但所有人必须明白,任何写命令都会覆盖目标地址原有内容,没有解锁保护,出错就是真出错。

下面这段代码是典型的单字节写流程,把它作为基础函数封装起来,后续所有日志写入都复用它。

uint8_t MRAM_ReadStatus(void) { uint8_t status; CS_LOW(); SPI1_ExchangeByte(0x05); // RDSR status = SPI1_ExchangeByte(0x00); CS_HIGH(); return status; } void MRAM_WriteEnable(void) { CS_LOW(); SPI1_ExchangeByte(0x06); // WREN CS_HIGH(); } void MRAM_WriteByte(uint32_t addr, uint8_t data) { // 写使能 MRAM_WriteEnable(); // 发起写命令 CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); SPI1_ExchangeByte(data); CS_HIGH(); // 等待写完成 while (MRAM_ReadStatus() & 0x01) { // 这里可以插入软狗喂狗操作 } }

每次写命令之前都要执行 WREN,这是一个容易漏的细节。有些工程师会尝试只在系统初始化时开一次写使能,后面连续写。MRAM 在每次写周期完成后会自动清除写使能位,所以这个偷懒想法不成立,老老实实每次写前都发 0x06,代码虽然看起来重复,但正是在这种细节上省事,后面反而惹来莫名其妙的丢失数据问题。

3.3 页写与连续读代码

MR25H40CDF 支持连续写,一次最多 256 字节。很多人的第一反应是“那我就整块写”,但遇到跨 256 字节边界时,SPI 设备通常会有自己的处理方式,最稳妥的做法是强制按 256 字节页拆分,避免挑战芯片边界行为。

下面的函数演示了如何把一段缓冲区写到任意地址,内部自动处理跨页拆分。

void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t chunk; while (len > 0) { // 本页剩余可写字节数 chunk = 256 - (addr % 256); if (chunk > len) { chunk = len; } MRAM_WriteEnable(); CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < chunk; i++) { SPI1_ExchangeByte(buf[i]); } CS_HIGH(); while (MRAM_ReadStatus() & 0x01) { // 等待 } addr += chunk; buf += chunk; len -= chunk; } }

读取就简单得多,不需要写使能,也不存在页边界问题,只要地址不越过整片存储末尾,就可以连续读。下面代码用于读取任意长度数据。

void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { CS_LOW(); SPI1_ExchangeByte(0x03); // READ SPI1_ExchangeByte((addr >> 16) & 0xFF); SPI1_ExchangeByte((addr >> 8) & 0xFF); SPI1_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI1_ExchangeByte(0x00); } CS_HIGH(); }

在实际项目里,我建议加一个 MRAM 自检函数:读回厂家 ID。虽然每个芯片的读 ID 命令和返回值需要以数据手册为准,但利用它做上电自检非常实用,能迅速确认片选、时钟、数据线连接是否全部正确。一套系统在现场运行,有时候是接触不良导致 SPI 返回 0xFF,如果启动过程直接报“存储芯片未识别”,现场维护人员就能快速定位,而不是瞎猜。

3.4 写保护与分区管理

MR25H40CDF 支持状态寄存器写保护,可以保护顶部或底部四分之一、二分之一甚至整片存储区,具体配置位在状态寄存器的 BP1、BP0 和 WPEN。对工业设备来说,我强烈建议把固件参数区设置为写保护,运行日志区保持开放。这样即使程序跑飞,误写操作最多影响日志区,不至于把校准参数和关键配置全部冲掉。

分区逻辑可以这样规划。

分区起始地址大小内容
参数区0x0000016KB校准参数、通讯地址、版本号
日志区0x04000448KB循环运行日志
系统信息区0x7F8002KB上电次数、故障统计等

实际地址要根据自己的数据结构重新计算,但分区表一旦定下来,就不要随意改动,否则旧固件升级到新固件后可能读到错乱数据,很容易被现场误判为硬件故障。

写保护配置要谨慎,因为一旦保护锁上,再想改动参数区首先要解除保护,而解除保护本身也需要执行特定的 WRSR 序列,配合 WP# 引脚电平。建议把解除保护的代码单独抽出来,只在维护模式或固件升级时调用,并加上双重确认,比如连续两帧特殊密钥。

4. 工业化落地:可靠性、一致性与掉电保护

4.1 MRAM 在掉电中的原子写特性

MRAM 的每一位是一个磁性隧道结,数据写入不靠电荷积累,而是靠磁场方向翻转。这意味着整个写周期里,不会有“电荷写了一半”的中间状态,掉电时刚写入的那一位要么是旧值,要么是新值,不会出现第三种状态。相比 NOR Flash 必须先写入一个全 0 的页缓冲再编程,掉电出现半页的可能性天然小很多。

这不是说系统完全不需要防掉电逻辑。如果一个日志包含时间戳、温度值、运行状态三个字段,每个字段占 4 字节,程序在一次操作里分三次写,那么在第二次写完后掉电,日志结构就是新的时间戳加旧温度值。所以业务层面的原子性仍然要代码保证,要么先写一个“准备好”标志,所有字段写完后翻转“有效”标志,要么一次性把记录写到固定长度槽位,再更新提交标志。

工业项目里我最常用的是“双缓冲提交”方案:日志区有两块相同大小的槽位,写着当前槽位编号和 CRC 校验值。上电时先读两个槽的提交标志,取最新且 CRC 通过的那一份。MRAM 保证单次写入原子性,加上代码层面的提交标志,就能做到很强的电源中断免疫。

4.2 双区交替与日志轮转

数据记录需求变化多样,有些要按天分文件,有些只保留最近 N 条。针对 MRAM 高寿命特性,我把日志区设计成环形缓冲区,并且分成 A、B 两区,每次写入序号递增,写满 A 区就写 B 区,B 区写满时从 A 区顶部覆盖最老的数据。

因为 MRAM 不存在写放大或擦除周期,环形覆盖对存储芯片本身是零负担的。这时真正要关心的只剩数据一致性:每条日志的头部存储一个 4 字节序号和一个 4 字节 CRC,读取端可以通过序号判断哪条更新,通过 CRC 判断记录是否完整。发生突然掉电时,最多有一条半成品记录作废,之前的数据全部无损,这就是双区环形方案的价值。

没有电池、没有超级电容,也能做到掉电后重启快速恢复,这对工业现场非常友好。原先用电池供电 SRAM 保存关键数据的方案,维护成本高,电池几年后漏液甚至会腐蚀电路板,换成 MRAM 后这部分烦恼直接消失。

4.3 看门狗与 SPI 总线复位策略

PIC18F86J16 这类单片机通常都有看门狗定时器。存储操作如果卡在某个无限循环里,例如 SPI 忙等待时从设备一直不拉低状态位,整机看门狗复位后重新初始化即可。但要注意,SPI 从设备在 MCU 异常复位时可能还保持半通信状态,CS# 电平可能停在低电平,导致后续所有 SPI 操作都被当成一次超长传输。

我习惯在系统复位后,先把 CS# 对应的 GPIO 拉高,并做一次完整的 SPI 模块禁用再重新启用,确保总线回到空闲状态。这个动作看起来多余,但能省下很多现场偶发性故障排查时间。复位原因也要判断,通过 PWRSAV、RCON 寄存器区分是上电复位、看门狗复位还是外部复位,然后决定日志是重新打开还是先补一条“异常重启记录”。

在写日志过程中穿插喂狗是另一个实用习惯。如果是长数据块跨多个 256 字节页,比如一次写入 4KB,那么必须在每写完一页的等待循环里喂一次狗,否则看门狗可能在等待 MRAM 完成时触发复位。MRAM 写入再快,也会在通信毛刺导致超时时出现信号异常,代码里留一个超时计数,借用硬狗兜底,才够皮实。

5. 排障与心得:我实际踩过的一些坑

5.1 一读取就是 0xFF 或 0x00

这是最经典也是最容易排查的现象。如果 MRAM 读出来的数据全是 0xFF,大概率是 CS# 或 SPI 时钟根本没到芯片,或者供电没起来,芯片完全没参与通信。先把示波器探头挂在 CS# 和 SCK 引脚上,观察读操作时是否有完整的片选低电平和时钟脉冲。

还有一种情况是 HOLD# 引脚悬空,芯片被意外拉入 Hold 状态,SO 引脚一直输出高阻,读出来全 0xFF。把 HOLD# 和 WP# 都加上拉到 3.3V,通常症状立刻消失。如果读出来是 0x00,则可能 SDI 和 SDO 接反,或者 SPI 模式里数据采样相位错误,收到的一直不是芯片发出的数据。

我最推荐的排查顺序是:先查供电和 GPIO 电平,再查 CS# 时序,然后用逻辑分析仪看整条 SPI 波形,最后用读 ID 命令验证芯片状态。逻辑分析仪在这个场景比示波器更好用,因为能直接解开协议层的帧,看清地址、数据、命令是否都正确。

5.2 PPS 映射失效问题

PPS 配置如果没生效,最常见的现象是程序下载后一运行,SPI 引脚完全没有波形。原因多半是 PPS 解锁序列没写对,或者对 RP 寄存器赋值时写错了映射编号,把这根线连到了另一路外设的输出上,外部引脚测试时发现电平有变化,但根本不是 SPI 数据。

另一个坑是程序里有两个外设同时抢占同一个 PPS 输入端口,比如把 SPI1 的 SDI 和某个 UART 的 RX 都映射到同一个引脚。配置不会报错,实际工作时信号会串扰得一塌糊涂。修改 PPS 配置前必须仔细查数据手册里的输入输出映射表,用它做出的“接线”不能违反物理规则,硬件外部怎么连,软件内部就得怎么对应。

还有一次现场故障是上电后 MRAM 偶尔读写失败,最后发现是看门狗复位后主程序没有重新执行 PPS 配置,所有通信外设全处于未映射状态。解决方式是确保 PPS 初始化放在系统启动的早期阶段,并且在每次复位源判断后都重新执行一遍。

5.3 高速率与长线信号不稳定

理论上 MR25H40CDF 的 SPI 时钟可以做到几十兆赫兹,但在实际工业板卡上,你很难保证 MRAM 芯片和 MCU 之间只有短短几厘米走线。一旦超过 10 厘米,又没有做阻抗控制,线上反射就很容易让数据采样错误,出现偶发的读写错误,而且错误时好时坏,难以复现。

解决方式最直接的就是把 SPI 时钟降下来,从 20MHz 降到 4MHz 或 1MHz。对于在 PIC18F86J16 上运行的工业逻辑,数据吞吐量本来就不高,1MHz 到 4MHz 完全够用。不要为了看起来很快就把时钟拉到极限,存储可靠性比跑分重要得多。

如果板子必须用长线连接外部存储模块,比如通过排线把 MRAM 放在独立的存储板上,强烈建议在 SCK 和 SI 上各串一个 33Ω 到 100Ω 的电阻,SO 线上同理但可以留小一点。这个电阻能抑制反射,也会牺牲一点上升沿速度,但对于低速 SPI 影响可以忽略不计。

5.4 给后来者的建议

先验证芯片,再写业务代码。拿到一颗新的 MR25H40CDF,不要直接写日志系统,先做一次“读 ID、擦写全片、回读比对”的裸测。MRAM 不需要擦除,可以直接往全地址写 0xAA 和 0x55 的交替模式,再回读比对,这一步能把硬件连接、SPI 初始化、读写函数这些底层问题全部暴露干净,后续调试效率会大幅提升。

再把数据结构设计固化下来。日志格式、参数表布局、分区编号这些在第一次写代码时就要用头文件定义清楚,不要散落在各个函数里。换颗芯片、改个容量、调整分区,都只改一个配置文件是最理想的状态。

最后讲一个很实用的小习惯:在每次对 MRAM 进行批量写操作之前,先在系统 RAM 里组好完整的数据帧,加好 CRC,再一次性写入。不要边读传感器边写存储,尽可能缩短 MRAM 写入期间暴露在异常环境里的窗口。我自己就是在连续掉电测试里发现过几次偶发数据错位,后来强制改成“组帧、校验、单次提交”后,问题再没有出现过。这套组合如果能配合好中断管理,把写入放到低优先级任务去做,在大多数工业设备上都能稳定跑很多年。

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

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

立即咨询