
1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 8 位单片机工业现场的数据记录有个很尴尬的夹缝需求要频繁写、要断电不丢、要能扛住高低温、还要便宜。传统方案里EEPROM 擦写寿命大概 100 万次铁电存储器 FRAM 容量小且贵NOR Flash 又要先擦后写、写入慢、寿命也就 10 万次量级。一旦遇到“每秒钟记录一次设备状态、连续跑好几年”这种活Flash 和 EEPROM 都得跪。MR25H40CDF 这颗芯片就是冲着这个痛点来的。它是 Everspin 家的 4Mbit512KB串行 MRAMSPI 接口最关键的一点是写入不需要擦除、没有写延迟、理论寿命近乎无限。MRAM 靠磁隧道结存储数据写一个字节和读一个字节在时间上是一个量级不像 Flash 那样写之前得先擦一整块。工业场景里那些“掉电瞬间必须把最后一条数据落盘”的需求MRAM 天然适配。主控这边我选的是 PIC18LF45K50。这颗 8 位单片机在工业圈子里属于老黄牛级别的存在宽压供电1.8V 到 5.5V、自带硬件 SPI、有 MSSP 模块、低功耗、外设够用、开发工具链成熟。很多人一上来就想用 STM32但对于“采集几路模拟量、通过 SPI 存到 MRAM、偶尔通过串口上报”这种任务PIC18 完全够用而且 BOM 成本能压下来一大截。8 位机做数据记录仪代码量小、实时性可控、没有操作系统带来的不确定性这在工业环境里反而是优势。这个组合的核心价值在于用最低的硬件成本实现工业级的高频数据存储与可靠读取。适合做设备黑匣子、电表水表数据记录、工业传感器节点、PLC 扩展存储模块这类项目。下面我把整个设计思路、硬件连接、SPI 时序、驱动代码、踩坑经验全部拆开讲。1.2 整体架构怎么搭整个系统的数据流很清晰传感器或外部信号 → PIC18LF45K50 采集处理 → 通过 SPI 总线写入 MR25H40CDF → 需要时再通过 SPI 读回来 → 通过 UART 或其它接口上报。这里有个关键设计决策MRAM 挂在硬件 SPI 上还是软件模拟 SPI我的建议是优先用 PIC18 的 MSSP 硬件 SPI 模块。原因有三第一硬件 SPI 速率可以跑到系统时钟的四分之一PIC18LF45K50 在 16MHz 晶振下 SPI 能到 4MHz写 512KB 数据也就一秒多第二硬件 SPI 不占用 CPU 周期写入期间 CPU 可以去处理采集和协议解析第三时序由硬件保证不会因为中断打断导致 SPI 时钟抖动。软件模拟 SPI 只在引脚冲突或者需要非标准时序时才考虑。电源部分也要注意。MR25H40CDF 是 3.3V 供电的器件而 PIC18LF45K50 是宽压的可以工作在 3.3V。所以整个系统统一用 3.3V 供电最省事不需要电平转换。如果你非要用 5V 给单片机供电那 SPI 线上必须加电平转换否则 MRAM 的输入引脚会被 5V 打坏。这一点在画原理图的时候就要定死。2. 硬件连接与 SPI 时序细节2.1 引脚怎么接才不踩坑MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准CS片选、SO数据输出、SI数据输入、SCK时钟、VDD、VSS还有两个 WP 和 HOLD 引脚。这里重点说 WP 和 HOLD。WP 是写保护引脚低电平有效。如果你不需要写保护功能直接拉到 VDD。我一般建议在 PCB 上留一个测试点调试阶段可以手动拉低验证写保护逻辑量产再固定拉高。HOLD 引脚用于暂停 SPI 通信低电平有效不用的话也拉到 VDD。这两个引脚如果悬空芯片行为不确定可能出现“有时候能写有时候写不进去”的玄学问题我早期就吃过这个亏。和 PIC18LF45K50 的连接对应关系如下MR25H40CDF 引脚PIC18LF45K50 引脚说明CSRC2可配置为普通 IO片选低有效SORC4SDI主机输入SIRC5SDO主机输出SCKRC3SCK时钟WPVDD3.3V写保护关闭HOLDVDD3.3V不暂停VDD3.3V电源VSSGND地PIC18LF45K50 的 MSSP 模块在 SPI 主模式下SCK 和 SDO 是输出SDI 是输入。CS 我用普通 IO 控制因为 MSSP 的硬件 CS 在有些配置下不够灵活手动控制 CS 能精确掌握每个命令帧的边界。这一点很重要MRAM 的 SPI 命令是“CS 拉低 → 发命令 → 发地址 → 读写数据 → CS 拉高”这样一个完整帧CS 必须在整个帧期间保持低电平中间不能跳变。2.2 SPI 模式选择与速率计算MR25H40CDF 支持 SPI 模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我一般用模式 0因为这是最通用的配置和大多数 SPI 器件一致。在 PIC18 的 SSPCON1 寄存器里CKP 位设 0CKE 位设 0就是模式 0。速率方面MR25H40CDF 最高支持 40MHz 的 SCK。PIC18LF45K50 在 16MHz 晶振下SPI 时钟由 SSPADD 寄存器分频得到。公式是Fosc / (4 * (SSPADD 1))。要得到 4MHz 的 SPI 时钟SSPADD 设为 0 即可16MHz / 4 4MHz。如果你用 64MHz 的 PLL 时钟那 SSPADD 要相应调整。实测下来4MHz 已经足够快写 512KB 大约 1.3 秒读更快。没必要追求极限速率工业环境里稳定性比速度重要。注意SPI 时钟速率越高PCB 走线越要讲究。如果 MRAM 离单片机超过 5 厘米建议 SCK 线上串一个 22 欧姆的电阻抑制过冲和振铃。我见过因为走线太长导致读数据偶发错误的案例加电阻后问题消失。3. 驱动代码实现与关键命令解析3.1 MRAM 的命令集与操作流程MR25H40CDF 的命令集很精简核心就几条命令十六进制码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据写数据的完整流程是拉低 CS → 发 WREN0x06→ 拉高 CS → 拉低 CS → 发 WRITE0x02→ 发 3 字节地址24 位地址因为 512KB 需要 19 位地址用 3 字节对齐→ 发数据字节 → 拉高 CS。注意 WREN 和 WRITE 之间必须拉高 CS这是很多新手容易漏掉的步骤。MRAM 不像有些 Flash 那样 WREN 和 WRITE 可以连续在同一个 CS 帧里它要求 WREN 单独一个帧。读数据就简单了拉低 CS → 发 READ0x03→ 发 3 字节地址 → 读数据字节 → 拉高 CS。读操作不需要 WREN。3.2 PIC18 上的 SPI 底层函数先写两个基础函数SPI 收发一个字节以及 CS 控制。// SPI 收发一个字节 unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF data; // 写入发送缓冲 while (!SSP1IF); // 等待传输完成 SSP1IF 0; // 清除中断标志 return SSPBUF; // 返回接收到的数据 } // CS 控制 #define MRAM_CS_LOW() (LATC2 0) #define MRAM_CS_HIGH() (LATC2 1)这里有个细节PIC18 的 MSSP 模块在发送完成后SSP1IF 标志会置位。必须用while(!SSP1IF)等待不能直接读 SSPBUF否则读到的是旧数据。另外SSPBUF 是双缓冲的读 SSPBUF 会清除 BF 标志这个顺序不能乱。3.3 写使能与写数据函数// 写使能 void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI_ExchangeByte(0x06); // WREN MRAM_CS_HIGH(); } // 写数据到指定地址 void MRAM_WriteData(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_WriteEnable(); // 先发 WREN MRAM_CS_LOW(); SPI_ExchangeByte(0x02); // WRITE 命令 SPI_ExchangeByte((addr 16) 0xFF); // 地址高字节 SPI_ExchangeByte((addr 8) 0xFF); // 地址中字节 SPI_ExchangeByte(addr 0xFF); // 地址低字节 for (i 0; i len; i) { SPI_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); }地址是 24 位的但 MR25H40CDF 只有 512KB实际有效地址是 19 位。高 5 位会被忽略但为了兼容性还是按 3 字节发。这里用unsigned long存地址在 PIC18 上 long 是 32 位的够用。3.4 读数据函数// 从指定地址读数据 void MRAM_ReadData(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; MRAM_CS_LOW(); SPI_ExchangeByte(0x03); // READ 命令 SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for (i 0; i len; i) { buf[i] SPI_ExchangeByte(0xFF); // 发哑数据收实际数据 } MRAM_CS_HIGH(); }读的时候发 0xFF 是惯例因为 SPI 是全双工主机必须提供时钟才能收到从机的数据。0xFF 是什么值不重要从机在 READ 命令后会自动输出数据。3.5 SPI 初始化配置void SPI_Init(void) { TRISC3 0; // SCK 输出 TRISC5 0; // SDO 输出 TRISC4 1; // SDI 输入 TRISC2 0; // CS 输出 MRAM_CS_HIGH(); // CS 默认高 SSPCON1 0x20; // SSPEN1, CKP0, SPI 主模式, Fosc/4 SSPCON2 0x00; // 无特殊配置 SSPSTAT 0x00; // CKE0, SMP0 SSPADD 0x00; // 4MHz SPI 时钟16MHz 晶振 }SSPCON1 的 0x20 是二进制 00100000SSPEN1 使能 MSSPCKP0 空闲低电平SSPM0000 表示 SPI 主模式 Fosc/4。SSPSTAT 的 SMP 位设 0 表示在数据输出末尾采样CKE 设 0 表示时钟从空闲到有效沿发送数据。这些配置对应 SPI 模式 0。4. 实际项目中的问题排查与经验总结4.1 常见问题速查表现象可能原因排查方法解决方案读回数据全 0xFFCS 没拉低或时序不对示波器看 CS 和 SCK检查 CS 控制代码确保帧期间 CS 保持低写进去读出来不对WREN 和 WRITE 之间没拉高 CS逻辑分析仪抓 SPI 帧严格按“WREN 单独帧”流程操作偶发读写错误SPI 速率过高或走线太长降低 SPI 时钟测试降速到 1MHz或加串阻完全无响应电源或地没接好万用表测 VDD 和 VSS检查焊接确认 3.3V 供电写保护误触发WP 引脚悬空或拉低测 WP 引脚电平WP 拉到 VDD数据错位地址字节顺序搞反对比数据手册地址格式高字节在前MSB first4.2 几个只有踩过才知道的坑第一个坑PIC18 的 SPI 在发送和接收之间有一个字节的延迟。如果你连续调用 SPI_ExchangeByte第一次调用返回的是上一次的接收数据。我在读 MRAM 的时候第一个读到的字节总是 0xFF后来发现是因为发 READ 命令后第一个 SPI_ExchangeByte(0xFF) 返回的是发命令时收到的数据真正的数据从第二个字节开始。解决办法是在发完地址后先空读一个字节丢弃再开始收数据。或者更简单发命令和地址的时候不关心返回值从数据阶段开始正常收发。第二个坑MRAM 的写入不需要等待。Flash 写完要轮询状态寄存器等 busy 位清零MRAM 完全不需要。你发完 WRITE 命令拉高 CS数据就已经写进去了下一个命令可以立刻开始。这个特性在高速记录场景下非常爽省掉了轮询等待的时间。但要注意如果你在 WRITE 帧还没结束时就读状态寄存器可能会干扰当前操作。所以 CS 的拉高时机要精确。第三个坑地址边界处理。MR25H40CDF 是 512KB地址范围 0x00000 到 0x7FFFF。如果你写入时地址超过 0x7FFFF它会回卷到 0x00000。这个特性可以用来做环形缓冲区但如果你不想回卷就要在代码里做地址判断。我在做数据记录仪的时候就是利用这个回卷特性实现了一个简单的环形日志写满 512KB 后自动从头覆盖不需要额外的地址管理逻辑。4.3 工业环境下的可靠性加固工业现场有电机、继电器、变频器电磁干扰比实验室大得多。除了前面说的 SCK 串阻还有几个措施MRAM 的 VDD 引脚旁边放一个 0.1uF 陶瓷电容和一个 10uF 钽电容靠近芯片引脚SPI 走线尽量短最好在同一层不要跨分割地平面如果 MRAM 和单片机不在同一块板子上用屏蔽排线屏蔽层单点接地。另外PIC18LF45K50 的看门狗一定要开。工业环境里单片机可能因为干扰跑飞看门狗能在几毫秒内复位系统。复位后从 MRAM 里读回最后的状态继续记录数据不会丢。这就是 MRAM 非易失性的价值即使系统崩溃重启之前写进去的数据依然在。4.4 读取速度的实测数据我用逻辑分析仪实测了一组数据SPI 时钟 4MHz读 512 字节连续数据从 CS 拉低到 CS 拉高耗时约 1.05 毫秒。写 512 字节耗时约 1.08 毫秒差异主要在 WREN 命令的额外帧。换算下来持续写入速率大约 470KB/s对于每秒记录几十条传感器数据的场景绰绰有余。如果你把 SPI 时钟提到 10MHz速率还能翻倍但工业环境建议留余量。提示MRAM 的读操作是无限次的写操作也是。但如果你做的是高频写入比如每秒上千次建议在软件层做一个缓冲攒够一页再写减少 CS 切换次数。虽然 MRAM 不怕写坏但减少 SPI 帧数量能降低功耗和 CPU 占用。5. 数据存储结构设计与扩展思路5.1 怎么组织 512KB 的存储空间512KB 听起来不大但如果每条记录 32 字节能存 16384 条。对于工业设备黑匣子这个容量够记录好几天的运行状态。我的做法是把存储空间分成几个区域头部 4KB 存元数据记录格式版本、写入指针、设备 ID、校验信息后面 508KB 做环形数据区。元数据区每次写入时更新写入指针这样掉电后能知道数据写到哪里了。记录格式我一般用固定长度每条记录包含时间戳4 字节、通道号1 字节、数据值4 字节、状态标志1 字节、CRC 校验2 字节总共 12 字节不够的填充到 16 字节对齐。固定长度记录的好处是寻址简单第 N 条记录的地址 数据区起始地址 N * 16。不需要遍历链表读取速度快。5.2 掉电保护与数据完整性MRAM 虽然写入不需要等待但如果你在写一条记录的过程中掉电这条记录可能只写了一半。解决办法是双缓冲或者写完成标志。我用的方法是每条记录最后写一个“有效标志”字节只有这个字节写完了记录才算有效。读取的时候检查有效标志无效就跳过。因为 MRAM 写入是字节级的不存在“写一半”的物理状态只有“写了几个字节”的逻辑状态。所以有效标志放在记录末尾能保证记录的原子性。CRC 校验也是必须的。工业环境里 SPI 通信可能受干扰读回来的数据可能有一位翻转。每条记录带 CRC16读取时校验校验失败就标记为坏数据。MRAM 本身不会产生位翻转但传输过程可能出错。加上 CRC 后数据可靠性提升一个数量级。5.3 这个方案还能怎么扩展如果你需要更大的存储容量可以把多片 MR25H40CDF 挂在同一条 SPI 总线上用不同的 CS 引脚选择。PIC18LF45K50 有足够的 IO 口控制 4 到 8 片 MRAM容量扩展到 2MB 到 4MB。代码上只需要把 CS 控制封装成带片选参数的函数其它逻辑不变。如果主控需要升级PIC18LF45K50 可以换成 PIC24 或者 dsPIC33SPI 速率更高RAM 更大能做更复杂的数据处理。但 8 位机的低功耗和低成本优势就没了。选型永远是权衡没有绝对的好坏。另外MRAM 的 SPI 接口和标准 SPI Flash 是兼容的如果你哪天想换成 Flash硬件不用改只需要改驱动里的命令和等待逻辑。这也是我选 MR25H40CDF 的一个原因引脚和封装跟常见的 SPI Flash 一致PCB 设计可以复用。我在实际项目里用这套方案做了三款产品最久的一款已经在现场跑了两年多每天写入大约 5 万次至今没有出现数据丢失或芯片损坏。MRAM 的耐久性确实不是吹的。唯一要注意的是采购渠道MRAM 芯片价格比 Flash 贵不少一定要找正规代理拿货避免买到翻新片。翻新片的磁隧道结可能已经退化写入次数一多就出问题这种坑在工业项目里是致命的。