工业级MRAM与PIC18F4620 SPI读写实战:选型、时序与掉电保护

发布时间:2026/10/5 1:05:13
工业级MRAM与PIC18F4620 SPI读写实战:选型、时序与掉电保护
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机工业现场的数据记录有个很尴尬的处境用 EEPROM 吧写入速度慢得让人着急擦写寿命也就百万次级别遇到高频采集的场景没几个月就把扇区写废了用 SRAM 加后备电池吧电池在高温高湿环境下撑不了几年换电池还得停机维护成本高得离谱用 Flash 吧写之前得先擦一整块掉电保护逻辑复杂稍不注意就丢数据。MR25H40CDF 这颗芯片正好卡在这个痛点上。它是 Everspin 家的 4Mbit 串行 MRAMSPI 接口跟普通 SPI Flash 引脚兼容但底层物理机制完全不同——磁隧道结存储写入不需要擦除字节级随机写写入速度能到几十纳秒级别擦写寿命标称 10^14 次以上。说白了就是“SRAM 的速度、Flash 的非易失性、EEPROM 的接口习惯”工业数据记录、参数存储、掉电瞬间保存关键状态这些活儿它都能接。PIC18F4620 则是 Microchip 经典到不能再经典的 8 位单片机64KB Flash、3968 字节 RAM、1KB EEPROM自带 MSSP 模块支持 SPI 主模式工作电压 2.0V 到 5.5V工业温度范围 -40°C 到 85°C。它不新但胜在稳定、资料多、开发工具链成熟很多工业板子上跑了十几年还在服役。拿它跟 MR25H40CDF 搭配属于“老将配新兵”成本可控可靠性有底。这个组合适合谁看做工业数据采集终端的嵌入式工程师、需要频繁记录参数又不想被擦写寿命困扰的硬件开发者、正在评估非易失存储方案的技术选型人员以及想从 STM32 转过来了解 8 位平台 SPI 操作细节的朋友。下面我把整个设计思路、硬件连接、SPI 时序配置、读写代码实现、常见坑和排查方法按实际项目落地的顺序拆开讲。1.2 方案对比MRAM、FRAM、NVSRAM、EEPROM 到底怎么选在动手画板子之前先把存储介质选型这件事说透。工业场景里常见的非易失方案就那么几种各有各的脾气。特性MR25H40CDF (MRAM)FRAM (如 FM25V05)NVSRAMEEPROM (如 25LC256)SPI NOR Flash接口SPISPISPI/并行SPISPI容量4Mbit (512KB)512Kbit (64KB)256Kbit~4Mbit256Kbit (32KB)1Mbit~128Mbit写入方式字节级随机写字节级随机写字节级随机写字节级随机写页写需擦除写入速度极快无等待快无等待快慢5ms/字节页写快擦除慢擦写寿命10^14~10^14无限电池限制~10^6~10^5掉电保持20年10年依赖电池40年20年功耗低低较高低低成本高中高高低低选 MR25H40CDF 的核心理由就三条第一写入寿命足够长高频记录场景下不用做磨损均衡第二字节级写入不需要擦除操作软件逻辑简单掉电保护容易做第三SPI 接口跟 Flash 兼容硬件改板成本低。代价就是单价贵512KB 的容量在 MRAM 里算大的但跟 Flash 比还是小。所以它适合存关键参数、频繁更新的计数器、掉电前需要保存的状态快照而不是拿来存日志文件或者固件镜像。PIC18F4620 这边选它的理由更直接MSSP 模块硬件 SPI 稳定C18 和 XC8 编译器都能用调试器 ICD3 或者 PICKit3 便宜好买工业现场对 8 位机的接受度高代码量小跑裸机足够。你要是用 STM32 当然也行但很多老设备维护项目就是 PIC 的板子换不得。2. 硬件连接与 SPI 时序配置2.1 引脚连接与硬件设计要点MR25H40CDF 是 8 引脚 SOIC 封装引脚定义跟 25 系列 SPI Flash 完全一致这给硬件设计省了不少事。具体连接如下MR25H40CDF 引脚名称PIC18F4620 引脚说明1/CSRC2 (可配置)片选低有效2SORC4/SDI主入从出3/WP接 VDD 或 GPIO写保护低有效4VSSGND地5SIRC5/SDO主出从入6SCKRC3/SCK时钟7/HOLD接 VDD 或 GPIO保持低有效8VDD3.3V电源这里有几个硬件设计上容易踩的点。第一/WP 和 /HOLD 引脚不要悬空悬空状态下容易受干扰误触发建议直接上拉到 VDD或者用 GPIO 控制以便需要时锁定芯片。第二电源去耦必须做好MRAM 虽然功耗不高但写入瞬间电流有波动VDD 和 GND 之间放一个 0.1uF 陶瓷电容紧贴芯片引脚再并一个 1uF 钽电容在板级电源入口。第三SPI 走线尽量短SCK 和 SI/SO 不要跨分割地平面如果板子上有电机或者继电器SPI 线束要远离必要时加屏蔽或者串联 22 欧姆电阻做阻抗匹配。PIC18F4620 的 MSSP 模块在 SPI 主模式下SCK 时钟由硬件产生最高频率是 Fosc/4。假设你用 10MHz 晶振4 倍频到 40MHz那 SCK 最高就是 10MHz。MR25H40CDF 的 SPI 时钟最高支持 40MHz所以 PIC 这边反而是瓶颈但 10MHz 对工业数据记录来说完全够用一次 512 字节的页读也就 400 多微秒。注意PIC18F4620 的 MSSP 在 SPI 主模式下SS 引脚RA5如果配置为输入必须拉高否则模块可能进入从模式。最简单的方法是把 RA5 配置为输出并置高或者直接接地如果确定不用从模式。2.2 SPI 模式选择与时序参数计算MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。这两个模式的区别在于空闲时钟电平和数据采样边沿。模式 0 是时钟空闲低电平上升沿采样模式 3 是空闲高电平下降沿采样。实际用哪个都行但建议跟板子上其他 SPI 设备统一避免模式切换带来的时序混乱。PIC18F4620 的 SSPCON1 寄存器里CKP 位控制时钟极性CKE 位控制时钟边沿。对应关系如下模式 0CKP 0CKE 1数据在时钟上升沿采样空闲低模式 3CKP 1CKE 0数据在时钟下降沿采样空闲高我一般选模式 0因为示波器上看波形直观上升沿采样也符合大多数人的阅读习惯。配置代码后面会给出。时序参数方面MR25H40CDF 的数据手册给了几个关键时间SCK 高电平时间最小 10nsSCK 低电平时间最小 10nsCS 建立时间CS 下降到第一个 SCK 边沿最小 5nsCS 保持时间最后一个 SCK 边沿到 CS 上升最小 5ns数据输出有效时间SCK 边沿到 SO 有效最大 8nsPIC18F4620 在 10MHz SCK 下高电平时间和低电平时间各 50ns远大于 10ns 的最小要求所以时序上完全满足。CS 建立和保持时间由软件控制在拉低 CS 之后加几个 NOP 再发时钟拉高 CS 之前也加几个 NOP就能满足 5ns 的要求。实际上 PIC 的指令周期是 100ns40MHz 下随便一条指令都超过 5ns所以不用特别担心。实操心得如果你用逻辑分析仪抓 SPI 波形发现 CS 拉低后第一个时钟边沿来得太快可以在 CS 拉低后插入一个 __delay_us(1)虽然理论上不需要但能避免某些批次芯片的建立时间裕量不足。3. 读写操作的核心实现3.1 MR25H40CDF 指令集与操作流程MR25H40CDF 的指令集跟 SPI Flash 很像但少了擦除相关的指令多了几个 MRAM 特有的。常用指令如下指令名称指令码说明WREN0x06写使能写入前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据任意地址WRITE0x02写数据任意地址RDID0x9F读设备 ID写操作的流程是拉低 CS - 发 WREN0x06- 拉高 CS - 拉低 CS - 发 WRITE0x02- 发 3 字节地址24 位因为 4Mbit 512KB需要 19 位地址但芯片用 3 字节地址格式- 发数据字节 - 拉高 CS。注意 WREN 和 WRITE 之间必须拉高 CS 再拉低这是 MRAM 跟某些 Flash 不一样的地方Flash 有时候可以在同一个 CS 周期里发 WREN 再发 WRITE但 MR25H40CDF 要求 WREN 单独一个 CS 周期。读操作简单拉低 CS - 发 READ0x03- 发 3 字节地址 - 读数据 - 拉高 CS。地址会自动递增可以连续读整个芯片。状态寄存器的 WEL 位bit 1表示写使能锁存状态写入前可以读一下确认 WREN 生效。不过实际项目中只要时序正确WREN 基本不会失败我一般省略这步节省时间。3.2 PIC18F4620 SPI 初始化代码先看初始化。PIC18F4620 的 MSSP 配置涉及 SSPCON1、SSPSTAT、TRISC 等寄存器。假设晶振 10MHz4 倍频到 40MHzSPI 时钟设为 Fosc/16 2.5MHz保守但稳定。#include xc.h // 配置位根据实际板子调整 #pragma config FOSC HSPLL_HS // HS 晶振带 PLL #pragma config WDT OFF #pragma config LVP OFF #pragma config BOREN ON #define CS_LAT LATC2 #define CS_TRIS TRISC2 void SPI_Init(void) { // 配置 CS 为输出初始高电平 CS_TRIS 0; CS_LAT 1; // 配置 SCK (RC3) 和 SDO (RC5) 为输出SDI (RC4) 为输入 TRISC3 0; TRISC5 0; TRISC4 1; // MSSP 配置SPI 主模式时钟 Fosc/16模式 0 SSPCON1 0x00; // 先清零 SSPCON1bits.SSPM 0b0010; // SPI 主模式Fosc/16 SSPCON1bits.CKP 0; // 空闲低电平 SSPSTATbits.CKE 1; // 上升沿采样 SSPSTATbits.SMP 0; // 中间采样 // 关闭 SS 引脚功能避免误入从模式 TRISA5 0; LATA5 1; SSPCON1bits.SSPEN 1; // 使能 MSSP } // SPI 收发一个字节 uint8_t SPI_ExchangeByte(uint8_t data) { SSPBUF data; while (!SSPSTATbits.BF); // 等待接收完成 return SSPBUF; }这段代码里SSPM 设为 0b0010 对应 Fosc/16。如果你想更快可以设 0b0000Fosc/4 10MHz但要注意 PCB 走线质量线长了容易出错。我一般先用 Fosc/16 调通再逐步提速。注意PIC18F4620 的 SSPBUF 是双重缓冲写进去之后硬件自动开始发送读 SSPBUF 会清除 BF 标志。如果连续快速写 SSPBUF 而不读可能会溢出。上面的 SPI_ExchangeByte 函数每次读写配对不会出问题。3.3 MRAM 读写函数实现基于上面的 SPI 底层封装 MRAM 的读写函数。#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRDI 0x04 #define MRAM_CMD_RDSR 0x05 #define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_RDID 0x9F // 写使能 void MRAM_WriteEnable(void) { CS_LAT 0; SPI_ExchangeByte(MRAM_CMD_WREN); CS_LAT 1; } // 读状态寄存器 uint8_t MRAM_ReadStatus(void) { uint8_t status; CS_LAT 0; SPI_ExchangeByte(MRAM_CMD_RDSR); status SPI_ExchangeByte(0xFF); CS_LAT 1; return status; } // 写数据addr 为 24 位地址len 为长度buf 为数据指针 void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_WriteEnable(); CS_LAT 0; SPI_ExchangeByte(MRAM_CMD_WRITE); SPI_ExchangeByte((uint8_t)(addr 16)); SPI_ExchangeByte((uint8_t)(addr 8)); SPI_ExchangeByte((uint8_t)(addr)); for (i 0; i len; i) { SPI_ExchangeByte(buf[i]); } CS_LAT 1; } // 读数据 void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; CS_LAT 0; SPI_ExchangeByte(MRAM_CMD_READ); SPI_ExchangeByte((uint8_t)(addr 16)); SPI_ExchangeByte((uint8_t)(addr 8)); SPI_ExchangeByte((uint8_t)(addr)); for (i 0; i len; i) { buf[i] SPI_ExchangeByte(0xFF); } CS_LAT 1; } // 读设备 ID用于检测芯片是否存在 uint32_t MRAM_ReadID(void) { uint32_t id 0; CS_LAT 0; SPI_ExchangeByte(MRAM_CMD_RDID); id | (uint32_t)SPI_ExchangeByte(0xFF) 16; id | (uint32_t)SPI_ExchangeByte(0xFF) 8; id | (uint32_t)SPI_ExchangeByte(0xFF); CS_LAT 1; return id; }MR25H40CDF 的设备 ID 是 0x0E 开头具体值可以查手册。上电后先读 ID确认不是 0x00 也不是 0xFF基本就能判断芯片焊接正常、SPI 通信正常。3.4 掉电保护与数据完整性设计工业场景最怕的是写到一半掉电数据变成半新半旧。MRAM 的字节级写入特性让这个问题好处理很多因为不需要擦除写入是原子的单个字节写入在几十纳秒内完成但多字节写入过程中掉电仍然可能只写了一部分。我的做法是加一个简单的双备份加校验机制。把关键参数存两份每份后面跟一个 CRC16 校验值。写入时先写备份区再写主区读取时先读主区CRC 校验通过就用主区不通过就读备份区。这样即使主区写坏了备份区还能救回来。typedef struct { uint16_t param1; uint16_t param2; uint32_t counter; // 其他参数 } ParamStruct; #define PARAM_MAIN_ADDR 0x000000 #define PARAM_BAK_ADDR 0x000100 uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i; uint8_t j; for (i 0; i len; i) { crc ^ data[i]; for (j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; } void Param_Save(ParamStruct *param) { uint8_t buf[sizeof(ParamStruct) 2]; uint16_t crc; memcpy(buf, param, sizeof(ParamStruct)); crc CRC16_Calc(buf, sizeof(ParamStruct)); buf[sizeof(ParamStruct)] (uint8_t)(crc 0xFF); buf[sizeof(ParamStruct) 1] (uint8_t)(crc 8); // 先写备份区 MRAM_Write(PARAM_BAK_ADDR, buf, sizeof(buf)); // 再写主区 MRAM_Write(PARAM_MAIN_ADDR, buf, sizeof(buf)); } uint8_t Param_Load(ParamStruct *param) { uint8_t buf[sizeof(ParamStruct) 2]; uint16_t crc, calc; // 读主区 MRAM_Read(PARAM_MAIN_ADDR, buf, sizeof(buf)); crc buf[sizeof(ParamStruct)] | (buf[sizeof(ParamStruct) 1] 8); calc CRC16_Calc(buf, sizeof(ParamStruct)); if (crc calc) { memcpy(param, buf, sizeof(ParamStruct)); return 1; } // 主区校验失败读备份区 MRAM_Read(PARAM_BAK_ADDR, buf, sizeof(buf)); crc buf[sizeof(ParamStruct)] | (buf[sizeof(ParamStruct) 1] 8); calc CRC16_Calc(buf, sizeof(ParamStruct)); if (crc calc) { memcpy(param, buf, sizeof(ParamStruct)); return 1; } return 0; // 两个区都坏了 }这个机制简单但有效代价是写入时间翻倍。MRAM 写入本来就快翻倍也就多几十微秒对大多数工业应用来说可以接受。如果你对写入时间极其敏感可以只写主区但加一个“写入完成标志”字节上电时检查标志没写完就丢弃。实操心得MRAM 的写入虽然快但 CS 拉高之后数据才真正锁存到存储单元。如果你在 CS 拉高后立刻断电数据可能还没写进去。保险起见写完关键数据后加一个 __delay_ms(1) 再允许断电或者用状态寄存器确认写入完成。MR25H40CDF 没有“写入完成”位因为写入太快了但加个短延时总是没错的。4. 常见问题与排查实录4.1 SPI 通信失败排查流程SPI 调不通是嵌入式开发里最常见的问题没有之一。我按实际排查顺序整理了一个流程基本能覆盖 90% 的情况。现象可能原因排查方法解决方法读 ID 返回 0x00MISO 没接好或芯片没供电万用表测 VDD 和 MISO 电压检查焊接补焊或换芯片读 ID 返回 0xFFMOSI 没接好或 CS 没拉低示波器看 CS 和 MOSI 波形检查 CS 配置确认拉低读 ID 返回乱码SPI 模式不对或时钟太快逻辑分析仪抓波形改模式 0降速到 Fosc/64写入后读出来不对WREN 没发或 CS 时序不对读状态寄存器 WEL 位确保 WREN 单独 CS 周期偶尔读写失败电源干扰或走线太长示波器看电源纹波加去耦电容缩短走线高温下失败时序裕量不足高低温箱测试降 SCK 频率加 CS 延时这个表里的“读 ID 返回 0xFF”特别常见。很多人以为是芯片坏了其实是 CS 没拉低或者 CS 拉低了但 MOSI 没发出去芯片收不到指令SO 保持高阻上拉电阻把它拉成 0xFF。用示波器看一眼 CS 和 SCK 波形基本就能定位。4.2 写入速度与寿命的实测数据我拿手头的板子做了一组实测PIC18F4620 跑 40MHzSPI 时钟 Fosc/16 2.5MHzMR25H40CDF 在 3.3V 供电下操作耗时说明单字节写入约 8us含 WREN 和 CS 开销256 字节页写入约 850us字节连续写地址自增单字节读取约 6us含指令和地址开销256 字节连续读约 830us地址自增读设备 ID约 5us仅 4 字节交互写入 256 字节耗时 850us平均每个字节 3.3us其中大部分是 SPI 时钟传输时间2.5MHz 下每字节 3.2usMRAM 本身的写入延迟可以忽略。这意味着如果你每秒记录一次 256 字节的数据写入占用的 CPU 时间只有 0.085%对 8 位机来说毫无压力。寿命方面MR25H40CDF 标称 10^14 次擦写。假设你每秒写同一个地址一次一年 31536000 次写满 10^14 次需要 300 多万年。所以在这个场景下寿命完全不是需要考虑的问题随便写。4.3 电源管理与低功耗设计工业现场很多设备是电池供电或者能量采集供电功耗很关键。MR25H40CDF 的功耗数据如下状态电流说明读操作约 5mASCK 2.5MHz写操作约 8mA写入瞬间峰值待机约 100uACS 高无时钟休眠约 10uA芯片内部自动进入PIC18F4620 在 40MHz 下运行电流约 15mA休眠模式可以降到 0.1uA 级别。如果你的应用是间歇性记录可以这样设计平时 PIC 休眠定时器唤醒后采集数据唤醒 MRAM 写入写完继续休眠。MRAM 从待机到可操作的时间几乎为零不需要像 Flash 那样等上电稳定这对低功耗设计很友好。注意MRAM 在 CS 拉高后会自动进入低功耗状态不需要发专门的休眠指令。但如果你把 /HOLD 引脚拉低芯片会进入保持状态功耗更低但 SPI 通信会被暂停。一般不用 /HOLD直接靠 CS 控制就行。5. 项目扩展与个人体会5.1 从单机记录到多设备组网这套 MRAM 加 PIC 的方案单机跑通之后很容易扩展到多设备组网。PIC18F4620 有一个 EUSART 模块可以接 RS485 收发器把多台设备挂到同一条总线上。每台设备用 MRAM 存本地数据主站轮询读取。RS485 的地址可以存在 MRAM 里掉电不丢换设备也不用重新配置。如果你需要更高速的组网PIC18F4620 还支持 SPI 从模式可以跟主控 MCU 通信把 MRAM 当成主控的外部存储扩展。主控通过 SPI 直接读写 MRAMPIC 只做桥接或者预处理。这种架构在分布式采集系统里很常见主控跑 Linux 或者 RTOSPIC 负责实时性要求高的采集和存储。5.2 我踩过的几个坑第一个坑是 CS 引脚配置。PIC18F4620 的 MSSP 模块在 SPI 主模式下如果 SS 引脚RA5配置为输入且为低电平模块会自动切换到从模式SPI 就不工作了。我一开始没注意RA5 悬空结果 SPI 时好时坏查了半天才发现是 SS 引脚的问题。后来把 RA5 配置为输出并置高问题消失。第二个坑是 WREN 和 WRITE 之间的 CS 时序。我一开始按照 Flash 的习惯在同一个 CS 周期里先发 WREN 再发 WRITE结果写入一直失败。后来查 MR25H40CDF 手册才发现WREN 必须单独一个 CS 周期拉高之后再拉低发 WRITE。这个细节在 Flash 上不一定有但 MRAM 要求严格。第三个坑是电源纹波。板子上有一个继电器每次吸合的时候 MRAM 写入就会出错。用示波器看 VDD发现继电器动作时电源上有 200mV 的尖峰。后来在 MRAM 的 VDD 引脚旁边加了一个 10uF 钽电容和一个 0.1uF 陶瓷电容问题解决。工业板子上电源质量比实验室差很多去耦一定要做足。5.3 后续可以怎么玩这套方案跑通之后有几个方向可以继续折腾。一是把 MRAM 当成环形缓冲区实现一个简单的日志系统记录设备运行状态出故障时导出分析。二是加一个 RTC 芯片给每条记录打时间戳方便追溯。三是把 PIC 的 EEPROM 也用起来存一些不常改的配置MRAM 专门存高频数据分工明确。如果你用的是 STM32 或者其他平台SPI 操作逻辑是一样的只是寄存器配置不同。MR25H40CDF 的指令集跟 25 系列 Flash 兼容代码移植起来很快。关键是理解 MRAM 跟 Flash 的区别不需要擦除、字节级写入、写入前发 WREN、WREN 单独 CS 周期。把这几点记住剩下的就是 SPI 基本功了。最后分享一个小技巧调试 SPI 的时候如果手头没有逻辑分析仪可以用一个简单的办法验证——把 MISO 和 MOSI 短接发什么读什么如果能读回来说明 SPI 底层收发正常问题在从设备那边。这个方法虽然土但应急的时候很好用。