MRAM与PIC18F86K22的SPI存储方案:工业数据采集终端设计
发布时间:2026/10/5 9:51:38 作者:尧图编辑部 阅读量:1,286

1. 项目缘起与方案选型为什么是 MRAM 加 PIC181.1 一个真实的需求场景前阵子接手了一个工业数据采集终端的改造项目设备装在配电柜里环境温度夏天能到 70 度冬天户外柜内能到零下 30 度。原来的方案用的是 EEPROM 存配置参数和掉电前的累计数据问题出在两个地方一是写入速度太慢一次参数保存要等 5 到 10 毫秒主循环被拖得很难受二是擦写寿命只有 100 万次左右设备每 30 秒写一次运行状态算下来不到一年就开始出现坏块现场返修换板子的成本高得离谱。后来把存储介质换成了 MR25H40CDF 这颗 MRAM主控用的是 PIC18F86K22整套方案跑下来效果很稳。这篇就把整个设计思路、硬件连接、SPI 驱动、读写时序、踩过的坑完整梳理一遍给做工业嵌入式存储的朋友一个可以直接抄的参考。MR25H40CDF 是 Everspin 出的 4Mbit 串行 MRAMSPI 接口512K 乘 8 的组织方式。它最大的特点是非易失、写入无延迟、擦写寿命几乎无限官方标称 10 的 14 次方以上而且不需要像 EEPROM 那样等待内部写周期。PIC18F86K22 是 Microchip 的 8 位单片机64 脚自带 MSSP 模块可以跑硬件 SPI主频最高 64MHz对于这种中等数据量的存储读写完全够用。1.2 为什么不用 Flash 或 FRAM选型的时候我把几个候选都摆出来对比过这里直接上表省得大家再走一遍弯路。存储类型写入速度擦写寿命掉电保持写入前是否需擦除成本EEPROM慢毫秒级约 100 万次10 年以上否低NOR Flash慢需整扇区擦约 10 万次10 年以上是低FRAM快无延迟约 10 的 12 次方10 年以上否中高MRAM快无延迟10 的 14 次方以上20 年以上否中Flash 的问题在于写之前必须擦除整个扇区而且擦除时间长对于需要频繁记录运行日志的场景非常不友好。FRAM 其实也能满足需求但当时供货周期不稳定价格也比 MRAM 高出一截。MRAM 在写入速度、寿命、保持时间三个维度上都是最优解虽然单价贵一点但算上后期维护成本整体反而更划算。提示MRAM 的写入是真正的随机访问不需要擦除也不需要等待内部写完成。这一点和 EEPROM、Flash 有本质区别驱动写法上可以省掉很多状态轮询的逻辑。1.3 整体架构设计整个系统的数据流是这样的PIC18F86K22 通过硬件 SPI 与 MR25H40CDF 通信MRAM 里划分成几个区域分别是设备配置区、运行日志区、掉电保护区和固件升级标志区。配置区存设备 ID、通信参数、校准系数这些不常变的数据日志区循环写入运行状态每条记录带时间戳和 CRC掉电保护区在检测到电源异常时紧急写入当前关键变量升级标志区就一个字节用来标记是否需要进入 Bootloader。这种分区方式的好处是各区域互不干扰日志区写满了直接覆盖最旧的数据不会影响配置区。而且 MRAM 的随机写入特性让循环日志的实现变得非常简单不需要像 Flash 那样做复杂的磨损均衡。2. 硬件连接与 SPI 时序要点2.1 引脚连接与硬件设计MR25H40CDF 是 8 脚 SOIC 封装引脚定义很标准CS 片选、SO 数据输出、SI 数据输入、SCK 时钟、VDD、VSS另外还有 WP 写保护和 HOLD 保持脚。和 PIC18F86K22 的连接方式如下。MR25H40CDF 引脚PIC18F86K22 引脚说明CSRC2硬件片选也可用普通 IO 软件控制SORC4SPI 数据输入SIRC5SPI 数据输出SCKRC3SPI 时钟WP接 VDD不用的写保护直接拉高HOLD接 VDD不用的保持功能直接拉高VDD3.3V供电VSSGND地这里有个细节要注意PIC18F86K22 的 IO 电压是 5V 容忍的但 MR25H40CDF 是 3.3V 供电所以 SPI 信号线上最好加电平转换或者确认主控的 SPI 输出电平在 MRAM 的输入高电平阈值以上。我实测下来PIC 在 3.3V 供电时直接连没问题但如果 PIC 跑 5VSI 和 SCK 这两根线必须做电平转换否则长期工作会损伤 MRAM 的输入级。PCB 布局上去耦电容一定要紧贴 MRAM 的 VDD 和 VSS 引脚我用的是一颗 0.1uF 加一颗 1uF 并联。SPI 的四根线尽量等长走线不要跨分割地平面。如果线长超过 10 厘米建议在 SCK 上串一颗 22 欧姆的电阻做阻抗匹配抑制过冲。2.2 SPI 模式与时钟配置MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL 和 CPHA 的组合。模式 0 是时钟空闲低电平数据在上升沿采样模式 3 是时钟空闲高电平数据在上升沿采样。两种模式都能用我选的是模式 0因为 PIC 的 MSSP 模块配置成模式 0 最直观。时钟频率方面MR25H40CDF 最高支持 40MHz 的 SCK。PIC18F86K22 在 64MHz 主频下MSSP 的 SPI 时钟可以配置到 16MHz 或 10.67MHz 等分频值。我实际用的是 10MHz 左右原因是我的 PCB 走线比较长10MHz 下信号质量最稳而且对于 512K 的容量来说10MHz 的读写速度已经完全够用。算一下读一整片 512K 字节需要 512K 乘 8 等于 4M 个时钟周期加上命令开销10MHz 下大约 0.4 秒实际应用里根本不需要全片读取都是按地址随机访问单次读写几个字节到几百字节耗时在微秒级。配置 MSSP 的关键寄存器就几个SSPCON1 设置 SSPEN 使能、CKP 时钟极性、SSPM 模式选择SSPSTAT 设置 SMP 采样相位和 CKE 时钟边沿SSPADD 在主机模式下设置时钟分频。具体值我后面在驱动部分给出。2.3 命令集与地址组织MR25H40CDF 的命令集和标准 SPI Flash 很像但少了擦除相关的命令。核心命令有这几个。命令名称命令码说明WREN0x06写使能写之前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据地址是 24 位的因为 512K 字节需要 19 位地址用 3 个字节传输高 5 位忽略。读写的时序都是先拉低 CS发送命令码然后发送 3 字节地址接着就是数据字节的连续传输。读操作可以连续读任意长度地址会自动递增写操作也一样但要注意写之前必须先发 WREN 命令而且 CS 拉高之后写使能会自动复位。注意WREN 命令之后必须紧跟写命令中间不能插入其他操作否则写使能可能被清除。我在调试时就遇到过因为中间插了一个状态查询导致写不进去的情况。3. 底层驱动实现与关键代码3.1 SPI 初始化与基础收发函数先看 PIC 这边的 SPI 初始化。我用的是 MSSP1 模块配置成 SPI 主机模式时钟模式 0时钟频率约 10MHz。void SPI1_Init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC2 0; // CS 输出 LATCbits.LATC2 1; // CS 默认拉高 SSP1STAT 0x40; // SMP0 中间采样, CKE0 空闲低电平 SSP1CON1 0x20; // SSPEN1, SSPM0000 主机模式 Fosc/4 SSP1ADD 1; // 时钟分频实际约 10MHz }这里 SSP1ADD 的值需要根据主频计算。PIC18F86K22 的 SPI 时钟是 Fosc 除以 (4 乘 (SSP1ADD 加 1))。如果 Fosc 是 64MHzSSP1ADD 设为 1得到 64M 除以 8 等于 8MHz。设成 0 就是 16MHz。我选 1 是为了留余量。基础收发函数很简单一个字节换一个字节。uint8_t SPI1_ExchangeByte(uint8_t data) { SSP1BUF data; while(!PIR1bits.SSP1IF); // 等待传输完成 PIR1bits.SSP1IF 0; return SSP1BUF; }这个函数是阻塞式的对于低速应用完全够用。如果系统对实时性要求高可以改成中断或 DMA 方式但 MRAM 的读写本身很快阻塞几十微秒对大多数工业应用没影响。3.2 MRAM 读写函数实现读函数先拉低 CS发 READ 命令发 3 字节地址然后连续读数据。void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t i; CS_LOW(); SPI1_ExchangeByte(0x03); // READ 命令 SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); for(i 0; i len; i) { buf[i] SPI1_ExchangeByte(0xFF); } CS_HIGH(); }写函数多一步 WREN。void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; CS_LOW(); SPI1_ExchangeByte(0x06); // WREN CS_HIGH(); CS_LOW(); SPI1_ExchangeByte(0x02); // WRITE 命令 SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); for(i 0; i len; i) { SPI1_ExchangeByte(buf[i]); } CS_HIGH(); }写完 CS 拉高之后数据就已经真正写进 MRAM 了不需要任何等待。这一点和 EEPROM 完全不同EEPROM 写完还要等 5 毫秒左右才能进行下一次操作MRAM 是立即生效的。3.3 状态寄存器与写保护MR25H40CDF 的状态寄存器只有几位有效主要是 BP0 和 BP1 两位用来做块保护还有 WEL 位表示写使能锁存。读状态寄存器用 RDSR 命令。uint8_t MRAM_ReadStatus(void) { uint8_t status; CS_LOW(); SPI1_ExchangeByte(0x05); status SPI1_ExchangeByte(0xFF); CS_HIGH(); return status; }如果应用里需要保护某些区域不被误写可以通过 WRSR 命令设置 BP 位。比如把配置区所在的块设为只读只有特定条件下才解锁写入。这个功能在工业设备里很有用防止程序跑飞时把校准参数写坏。实操心得我一般会在初始化时读一次状态寄存器确认 WEL 位是 0BP 位是 0确保 MRAM 处于可写状态。如果读回来全是 0xFF说明 SPI 通信有问题优先检查 CS、SCK 和供电。4. 数据组织与掉电保护策略4.1 存储区域划分512K 字节的空间看着不大但合理规划之后能存不少东西。我的划分方案如下。区域名称起始地址大小用途配置区0x000004KB设备参数、校准系数日志区0x01000256KB循环运行日志掉电保护区0x410004KB关键变量快照升级标志区0x420004KBBootloader 标志预留区0x43000剩余后续扩展配置区用双备份加 CRC 的方式写的时候先写备份区校验通过再写主区读的时候如果主区 CRC 错就自动切到备份区。这样即使写入过程中掉电也不会丢失配置。日志区做成环形缓冲区每条记录固定 32 字节包含 4 字节时间戳、2 字节数据类型、24 字节数据和 2 字节 CRC。写指针存在掉电保护区里每次写日志前先更新指针再写数据。MRAM 的写入无延迟特性让这个流程非常流畅不需要考虑写等待。4.2 掉电检测与紧急写入掉电保护是工业设备的核心需求。PIC18F86K22 的电源监测可以用内部 LVD 模块也可以外部分压电阻接到 ADC 或者比较器。我用的是外部比较器方案电源正常时比较器输出高一旦电源跌落到 4.5V 以下比较器翻转触发中断。中断服务程序里要尽快把关键变量写进 MRAM。因为 MRAM 写入不需要等待一个 32 字节的记录写入时间在 10MHz SPI 下大约是 30 微秒加上函数调用开销总共不到 50 微秒。电源从 4.5V 跌到 PIC 最低工作电压 2.0V靠板上的电容储能通常能撑几百微秒到几毫秒时间完全够用。void __interrupt(high_priority) HighISR(void) { if(PIR2bits.CMP2IF) { PIR2bits.CMP2IF 0; // 紧急保存关键变量 MRAM_Write(POWER_FAIL_ADDR, (uint8_t*)critical_data, sizeof(critical_data)); // 置位掉电标志 power_fail_flag 1; } }注意掉电中断里不要做复杂运算也不要调用可能阻塞的函数。写入的数据最好提前组织好放在一个结构体里中断里直接整块搬过去。4.3 数据校验与恢复MRAM 本身可靠性很高但为了万无一失每条记录都带 CRC16 校验。读的时候先算 CRC对不上就跳过这条记录继续读下一条。日志区是环形的读的时候从写指针往前找找到第一条 CRC 正确的记录作为最新有效数据。配置区的恢复逻辑稍微复杂一点。主区和备份区各有一个有效标志和 CRC。上电时先读主区如果有效就用主区如果主区无效读备份区有效就用备份区并回写主区如果两个都无效加载默认配置并写入两个区。这套逻辑跑下来即使反复掉电也不会丢配置。5. 实测性能与常见问题排查5.1 读写速度实测数据我用逻辑分析仪抓了 SPI 波形同时用定时器测了函数执行时间。在 10MHz SPI 时钟下单字节读写和批量读写的耗时如下。操作类型数据长度实测耗时说明单字节写1 字节约 8 微秒含 WREN 和地址开销单字节读1 字节约 5 微秒含命令和地址批量写256 字节约 220 微秒地址开销摊薄批量读256 字节约 215 微秒连续读全片读512KB约 0.45 秒理论值附近这个速度对于工业数据采集完全够用。我实际应用里每 100 毫秒写一次 32 字节的日志占用 CPU 时间不到 30 微秒对主循环几乎没有影响。5.2 常见问题速查表调试过程中踩了不少坑整理成表格方便大家对照排查。现象可能原因排查方法解决方案读回全 0xFFCS 未拉低或 SPI 未使能示波器看 CS 和 SCK检查初始化代码和引脚方向读回全 0x00MISO 未接或上拉万用表测 SO 引脚检查焊接和连线写入后读回旧数据WREN 未发或 CS 时序错逻辑分析仪抓完整时序确保 WREN 后紧跟写命令偶发数据错误SPI 时钟太快或走线太长降低时钟测试降速或加匹配电阻状态寄存器读不对命令码错误对照数据手册确认 RDSR 是 0x05掉电后数据丢失中断响应太慢测中断延迟优化中断优先级和代码5.3 几个容易忽略的细节第一个是 CS 的时序。MR25H40CDF 要求 CS 拉低之后到第一个 SCK 上升沿之间至少有 5 纳秒的建立时间CS 拉高之前最后一个 SCK 下降沿到 CS 上升沿之间也要有 5 纳秒。在 10MHz 下这个时间很充裕但如果跑到 40MHz就必须仔细检查代码里 CS 操作和 SPI 传输之间的指令周期。第二个是 WP 和 HOLD 引脚。这两个脚如果不使用一定要拉到 VDD不能悬空。悬空时引脚电平不确定可能导致内部写保护误触发或者通信被保持。我一开始把 HOLD 悬空了结果偶尔出现读写失败查了半天才发现是这个原因。第三个是电源上电顺序。如果 PIC 和 MRAM 用不同的电源轨要确保 MRAM 先上电或者同时上电。如果 PIC 先上电而 MRAM 还没准备好PIC 发出的命令会被忽略初始化时读到的状态就是错的。我的做法是在初始化 MRAM 之前加 10 毫秒延时等电源稳定后再操作。第四个是 SPI 模式的一致性。PIC 的 MSSP 配置成模式 0 之后MRAM 也必须工作在模式 0。如果发现读出的数据位错位比如 0x03 读成 0x06那多半是采样边沿搞反了检查 SSP1STAT 的 SMP 和 CKE 位。6. 工程化建议与扩展思路6.1 驱动分层与移植性虽然这个项目用的是 PIC18 平台但驱动最好做成两层底层是 SPI 收发和 CS 控制上层是 MRAM 的命令封装。这样换到 STM32 或者别的平台时只需要改底层几个函数上层逻辑完全不用动。我在实际项目里把底层抽象成四个函数SPI_Init、SPI_Transfer、CS_Low、CS_High。上层 MRAM_Read、MRAM_Write、MRAM_ReadStatus 都基于这四个函数实现。后来把这个驱动移植到 STM32F103 上只花了不到半小时改底层上层代码一行没动。6.2 日志系统的优化如果日志写入频率很高可以考虑在 RAM 里做一个缓冲区攒够一批再一次性写入 MRAM。虽然 MRAM 单次写入很快但每次都有命令和地址开销批量写能把这部分开销摊薄。我实测过单字节写 256 次耗时约 2 毫秒而一次性写 256 字节只要 220 微秒差了将近十倍。不过批量写要注意掉电风险。如果数据还在 RAM 缓冲区里没写进 MRAM 就掉电了这部分数据就丢了。折中方案是设置一个合理的批量大小比如 64 字节同时配合掉电中断在检测到掉电时立即把缓冲区刷进 MRAM。6.3 后续可以扩展的方向这套存储方案跑通之后还可以往几个方向扩展。一是加文件系统把 MRAM 当成一个小型磁盘用 FatFS 或者 LittleFS 管理这样上层应用可以直接用文件接口读写不用关心地址分配。二是做双机冗余两片 MRAM 同时存相同数据读的时候对比两片的结果进一步提高可靠性。三是结合 RTC给每条日志加上精确的日历时间方便后期数据分析。我个人在实际操作中的体会是MRAM 这种器件最大的价值不在于速度快而在于它把“写”这个操作变得像写 RAM 一样简单。一旦习惯了这种无延迟、无擦除、无限寿命的写入方式再回去用 EEPROM 或者 Flash 就会觉得处处受限。对于工业设备这种需要长期稳定运行、频繁记录数据的场景MR25H40CDF 加 PIC18F86K22 这套组合确实是一个省心的选择。最后再分享一个小技巧调试 SPI 通信时先用低速时钟把功能跑通确认数据正确后再逐步提高时钟频率这样能把时序问题和逻辑问题分开排查效率高很多。