
1. 项目缘起与方案选型思考1.1 为什么要在工业场景里折腾 MRAM 这颗料做工业嵌入式这行的朋友应该都有体会选存储芯片这件事往往比选主控还让人头疼。EEPROM 写入慢、寿命有限擦写次数撑死也就百万次级别NOR Flash 虽然容量大但写入前必须擦除而且擦除块大、写入延迟高掉电瞬间还容易丢数据FRAM 倒是快但容量小、价格贵而且很多型号已经停产或者交期感人。我前两年做一个工业数据采集终端的项目需求很明确设备要在 -40℃ 到 85℃ 的宽温范围内稳定工作每秒要记录一次传感器数据还要保证突然断电时最近的数据不能丢同时设备寿命要求十年以上。这个需求一摆出来传统方案基本都被否掉了。后来接触到 Everspin 的 MR25H40CDF 这颗 MRAM磁性随机存储器才算找到了比较合适的答案。MRAM 的原理和传统存储不太一样它用磁性隧道结MTJ来存储数据而不是靠电荷。这意味着几个天然优势写入速度接近 SRAM没有擦除概念字节级随机写入写入寿命几乎无限官方标称 10^14 次以上而且掉电后数据能保持二十年以上。MR25H40CDF 是 4Mbit512KB容量SPI 接口40MHz 时钟宽温版本支持 -40℃ 到 85℃工业级应用非常合适。主控这边我选了 PIC18LF4515原因后面细说先把这个组合的整体思路讲清楚。1.2 PIC18LF4515 作为主控的取舍逻辑PIC18LF4515 是 Microchip 的一款 8 位单片机48KB Flash、3968 字节 RAM、1KB EEPROM自带 SPI 模块工作电压 2.0V 到 5.5V低功耗版本LF 后缀在 3V 下运行电流很小。有人可能会问都什么年代了还用 8 位机但工业场景里稳定、成熟、供货周期长、开发工具链完善这几点比性能重要得多。PIC18 系列在工业领域用了十几年抗干扰能力强外设配置直接没有那些花里胡哨的抽象层寄存器操作一目了然。而且 PIC18LF4515 的 SPI 模块支持主从模式时钟极性、相位可配置正好匹配 MR25H40CDF 的 SPI 时序要求。这个组合的核心价值在于用一颗简单可靠的主控通过标准 SPI 接口驱动一颗非易失性、高速、高耐久性的 MRAM实现工业现场的数据记录与读取。整个方案没有文件系统没有复杂的驱动栈就是裸机 SPI 读写代码量小、可预测性强、出问题好排查。适合做工业仪表、数据记录仪、PLC 扩展模块、医疗设备参数存储这类对可靠性要求高、但对成本又比较敏感的场景。1.3 整体架构与数据流设计整个系统的数据流很清晰传感器数据通过 ADC 或者 UART 进入 PIC18LF4515主控做必要的处理后通过 SPI 总线写入 MR25H40CDF 的指定地址。读取时反过来主控发起 SPI 读命令MR25H40CDF 把数据移位输出。MR25H40CDF 内部是 512KB 空间我把它划分成几个区域前 4KB 做系统配置区存设备参数、校准系数、版本号中间 500KB 做数据记录区用环形缓冲区的方式循环写入最后 8KB 做日志区记录异常事件和掉电时间戳。这种分区方式的好处是配置区和日志区不会被数据记录覆盖而且环形缓冲区天然适合持续记录场景不需要频繁擦除。SPI 总线上只挂 MR25H40CDF 一个从设备片选CS用普通 GPIO 控制这样最稳妥。有些朋友喜欢用硬件片选但在多设备或者需要精确控制时序的场景下软件片选更灵活。MR25H40CDF 支持 SPI 模式 0 和模式 3我选模式 0CPOL0CPHA0因为 PIC18 的 SPI 模块在模式 0 下配置最简单而且 40MHz 时钟下时序余量也够。下面这张表是我整理的关键参数对照方便大家选型时参考。参数项MR25H40CDF典型 EEPROM典型 NOR Flash容量512KB32KB-512KB1MB-16MB接口SPII2C/SPISPI写入方式字节级随机写字节级随机写扇区擦除后写写入寿命10^14 次10^6 次10^5 次写入延迟无延迟5ms 页写1-3ms 扇区擦除掉电保持20 年40 年20 年宽温范围-40~85℃-40~85℃-40~85℃2. 硬件连接与 SPI 底层配置细节2.1 引脚连接与 PCB 布局注意事项MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准VCC、GND、CS、SO、SI、SCK、WP、HOLD。WP 和 HOLD 这两个引脚在简单应用里可以直接拉高但工业场景我建议还是接到 GPIO 上方便做写保护和总线暂停。PIC18LF4515 这边我用 RC3 做 SCK、RC4 做 SDI主入从出、RC5 做 SDO主出从入RC2 做 CS另外 RA0 接 WPRA1 接 HOLD。这里有个细节要注意PIC18 的 SPI 引脚是复用功能配置前必须先把对应引脚设为输入或输出方向TRISC 寄存器要正确设置否则 SPI 模块不工作。PCB 布局这块SPI 总线虽然速度不算特别高但 40MHz 下走线还是不能太随意。我的经验是SCK 和 MOSI 尽量等长走线不要超过 10cmCS 线单独走不要和时钟线平行太长MRAM 的 VCC 引脚旁边必须放 0.1uF 陶瓷电容再并一个 1uF 钽电容因为 MRAM 写入瞬间电流会有波动。地平面要完整SPI 信号线下面不要有分割。还有一点MR25H40CDF 的 SO 引脚是推挽输出不需要外部上拉但如果你总线上挂了多个从设备SO 线上最好加一个 10K 上拉电阻防止总线浮空时误触发。2.2 PIC18LF4515 SPI 模块初始化代码PIC18LF4515 的 SPI 配置涉及几个寄存器SSPCON1、SSPSTAT、SSPADD、TRISC。我一般用 10MHz 时钟Fosc40MHz 时SSPADD0 就是 Fosc/4这个速度对 MR25H40CDF 来说完全在规格内而且留足了时序余量。下面是我实际项目里用的初始化代码直接可以抄。void SPI_Init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC2 0; // CS 输出 CS 1; // 片选拉高空闲状态 SSPCON1 0x20; // SSPEN1, CKP0, SSPM0000 (主模式 Fosc/4) SSPSTAT 0x40; // CKE0, SMP0 (模式 0) SSPADD 0; // 时钟 Fosc/4 10MHz PIR1bits.SSPIF 0; // 清中断标志 }这段代码里SSPCON1 的 SSPM 位设成 0000 表示主模式、时钟 Fosc/4。SSPSTAT 的 CKE 和 SMP 位决定 SPI 模式CKE0、SMP0 对应模式 0。这里有个坑我踩过PIC18 的 SPI 发送和接收是同时进行的你写 SSPBUF 的时候接收移位寄存器也在工作所以读数据的时候必须等 SSPIF 置位然后读 SSPBUF否则会读到旧数据。另外SSPADD 在主机模式下是时钟分频值不是从机地址别搞混了。2.3 MR25H40CDF 命令集与操作时序MR25H40CDF 的命令集很简洁常用的就几条WREN0x06写使能、WRDI0x04写禁止、READ0x03读数据、WRITE0x02写数据、RDSR0x05读状态寄存器、WRSR0x01写状态寄存器。每次写操作前必须先发 WREN这是硬性要求否则写命令会被忽略。读操作不需要 WREN直接发 READ 命令加 24 位地址就行。地址是 24 位的因为 512KB 需要 19 位地址但 MR25H40CDF 用 24 位地址格式高 5 位忽略。时序上CS 拉低后第一个字节是命令接着是地址然后才是数据。写操作时CS 必须在最后一个数据字节移入后拉高MRAM 内部会自动完成写入不需要等待写周期。这一点和 EEPROM 完全不同EEPROM 写完要等 5msMRAM 是即时完成代码里不需要加延时。读操作时CS 拉低后发命令和地址然后就可以连续读数据地址会自动递增直到 CS 拉高。这个自动递增特性很好用读大块数据时不用反复发地址。3. 数据存储与读取的完整实现3.1 底层读写函数封装底层函数我封装了三个SPI_Transfer 负责单字节收发MRAM_Write 负责写任意长度数据MRAM_Read 负责读任意长度数据。SPI_Transfer 是基础每次写一个字节到 SSPBUF等 SSPIF 置位然后读 SSPBUF 返回。这里注意PIC18 的 SPI 是全双工的发送和接收同时发生所以读的时候也要写一个 dummy 字节通常是 0xFF来产生时钟。uint8_t SPI_Transfer(uint8_t data) { SSPBUF data; while(!PIR1bits.SSPIF); PIR1bits.SSPIF 0; return SSPBUF; } void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { CS 0; SPI_Transfer(0x06); // WREN CS 1; CS 0; SPI_Transfer(0x02); // WRITE SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); for(uint16_t i 0; i len; i) { SPI_Transfer(buf[i]); } CS 1; } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { CS 0; SPI_Transfer(0x03); // READ SPI_Transfer((addr 16) 0xFF); SPI_Transfer((addr 8) 0xFF); SPI_Transfer(addr 0xFF); for(uint16_t i 0; i len; i) { buf[i] SPI_Transfer(0xFF); } CS 1; }这段代码里WREN 和 WRITE 之间必须把 CS 拉高再拉低因为 WREN 是一个独立命令CS 拉高才算完成。有些朋友图省事WREN 之后不拉高 CS 直接发 WRITE这样 MRAM 不认。另外写操作时 CS 拉高的时机很关键必须在最后一个字节移入后立刻拉高不能有额外时钟否则 MRAM 可能把多余时钟当成数据。3.2 环形缓冲区设计与地址管理数据记录区我用环形缓冲区起始地址 0x1000结束地址 0x7EFFF总共约 500KB。缓冲区头部存两个指针写指针和读指针各占 4 字节存在配置区里。每次写入一条记录写指针加记录长度如果超过结束地址就回绕到起始地址。读的时候从读指针开始读到写指针为止。这种设计的好处是不需要擦除不需要移动数据写入永远是追加操作寿命消耗均匀。记录格式我定义成固定 32 字节4 字节时间戳、2 字节传感器 ID、2 字节数据长度、20 字节数据、4 字节 CRC32。CRC 校验很重要工业现场干扰大SPI 通信偶尔会出错没有校验的话读出来的数据可能是错的。CRC32 我用查表法实现速度够快PIC18 跑 10MHz 时钟算 32 字节 CRC 大概 200us完全可以接受。写指针更新的时候先写数据再更新指针这样即使掉电最多丢最后一条记录不会破坏整个缓冲区结构。3.3 掉电保护与数据完整性策略掉电保护是这个方案的核心价值之一。MRAM 本身掉电不丢数据但主控在掉电瞬间可能正在写数据如果写到一半断电这条记录就不完整。我的做法是在配置区里维护一个“写入状态”标志写数据前先置位写完后清除。上电初始化时检查这个标志如果发现上次写入未完成就把写指针回退到上一条记录的末尾丢弃不完整的记录。这个逻辑很简单但非常有效。另外MR25H40CDF 的 WP 引脚我接到 GPIO 上正常运行时 WP 拉高允许写入系统进入低功耗或者准备断电时WP 拉低硬件写保护。这样即使主控跑飞了也不会误写 MRAM。还有一点MRAM 的写入是即时完成的没有内部写周期所以不需要像 EEPROM 那样等待但这也意味着写入过程中如果电源波动可能写入错误数据。所以电源设计上VCC 要加足够的滤波电容保证掉电时电压下降斜率平缓给主控足够时间完成保护动作。4. 实际调试中踩过的坑与排查经验4.1 SPI 通信失败的常见原因调试 SPI 最怕的就是通信不上读出来全是 0xFF 或者 0x00。我遇到过几次总结下来无非几个原因一是 CS 时序不对CS 拉低太早或太晚导致命令字节没被正确识别二是 SPI 模式配置错误MR25H40CDF 支持模式 0 和模式 3如果主控配成模式 1 或 2数据采样边沿就错了三是时钟太快虽然 MR25H40CDF 标称 40MHz但如果你走线长、负载大实际信号质量可能撑不住降到 10MHz 往往就稳了四是电源问题MRAM 对电源纹波比较敏感VCC 不稳会导致内部状态机异常。排查的时候我一般先用示波器看 SCK、MOSI、CS 三根线。CS 拉低后第一个 SCK 上升沿应该对应 MOSI 上的命令字节最高位。如果 CS 和 SCK 的相位关系不对那就是配置问题。如果波形都对但读不到数据就检查 SO 线看从机有没有输出。SO 线如果一直高阻可能是 MRAM 没上电或者 CS 没拉低。还有一个容易忽略的点PIC18 的 SPI 模块在配置为从模式时SS 引脚必须拉高否则会进入从模式冲突状态。虽然我们用的是主模式但 SS 引脚如果悬空偶尔会误触发所以最好把它设为输出并拉高。4.2 数据写入后读出来不一致的排查有一次客户反馈写进去的数据读出来偶尔会变。我一开始怀疑是 SPI 时序问题后来用逻辑分析仪抓了半天发现是写指针更新逻辑有 bug。当时我的代码是先更新写指针再写数据结果如果写数据过程中断电写指针已经指向新位置但数据没写进去下次上电读的时候就读到空白区域。改成先写数据再更新指针后问题解决。这个教训让我明白任何非原子操作顺序都很重要。还有一种情况是 CRC 校验失败但数据看起来是对的。这通常是 CRC 计算范围搞错了比如把时间戳算进去了但读的时候没算或者字节序搞反了。我的建议是CRC 计算和校验用同一个函数参数完全一致不要一个用大端一个用小端。另外MRAM 的地址是 24 位的但 PIC18 的指针是 16 位的操作大地址时要注意拆分别把高位丢了。4.3 宽温环境下的稳定性验证工业级应用必须做宽温测试。我把设备放到高低温箱里从 -40℃ 升到 85℃每个温度点保持 2 小时期间持续读写 MRAM记录误码率。实测下来MR25H40CDF 在 -40℃ 和 85℃ 下都能正常工作但有一点要注意低温下 SPI 时钟不能跑太高-40℃ 时我把时钟降到 5MHz误码率就降到零了。高温下反而没问题40MHz 也能跑。这个现象和半导体特性有关低温下载流子迁移率变化时序余量会变小。另外宽温测试时我发现一个现象温度变化过程中MRAM 的读取电流会有波动如果电源滤波不够VCC 会有几十毫伏的跌落。后来我在 MRAM 的 VCC 引脚旁边加了 10uF 陶瓷电容问题解决。所以如果你也要做宽温产品电源设计一定要留余量别抠那几毛钱电容钱。4.4 常见问题速查表现象可能原因排查方法解决方案读出来全是 0xFFCS 未拉低或 MRAM 未上电测 CS 电平和 VCC检查片选和电源读出来全是 0x00SO 线未连接或 SPI 模式错误测 SO 波形检查连线确认模式 0写入后读不一致写指针更新顺序错误检查代码逻辑先写数据再更新指针偶尔 CRC 失败SPI 时钟太快或干扰降低时钟测试降速或加滤波电容低温下通信异常时序余量不足高低温箱测试低温下降时钟写保护误触发WP 引脚悬空测 WP 电平WP 接 GPIO 并拉高5. 方案扩展与个人实操体会5.1 从裸机到 RTOS 的平滑过渡这套底层驱动封装好后移植到 RTOS 上并不难。我后来把项目升级到 FreeRTOS把 MRAM 读写封装成独立任务通过队列接收写请求SPI 总线加互斥锁保护。这样多个任务可以并发请求存储不会互相干扰。关键点是 SPI 操作要原子化一次完整的命令序列CS 拉低到拉高不能被其他任务打断否则时序会乱。用互斥锁包住整个 MRAM_Write 或 MRAM_Read 函数就行粒度不用太细。如果你用的是嵌入式 Linux那就更简单了SPI 设备在 /dev 下用 spidev 接口读写ioctl 配置模式和速度。不过 Linux 下 SPI 传输有额外开销单次读写延迟比裸机高适合数据量不大、实时性要求不极端的场景。MR25H40CDF 在 Linux 下我实测过用 spidev 以 10MHz 读写吞吐量大概 800KB/s对于数据记录来说足够了。5.2 多设备共享 SPI 总线的注意事项有时候一个系统里不止一颗 SPI 设备比如 MRAM 加一个 SPI Flash 或者 SPI 显示屏。这时候片选管理就很重要。每个设备独立 CS同一时刻只能拉低一个。PIC18 的 SPI 模块只有一个所以必须用软件片选。切换设备时先拉高当前 CS再拉低目标 CS中间加几个 NOP 延时确保总线稳定。另外不同设备的 SPI 模式可能不同切换设备时也要重新配置 SSPSTAT别嫌麻烦这是必须的。还有一点如果总线上有多个从设备SO 线是共享的但每个从设备的 SO 都是推挽输出如果两个设备同时被选中SO 就会冲突。所以软件上要严格保证 CS 互斥硬件上可以在每个 SO 线上串一个 100 欧姆电阻万一冲突了也能限流不至于烧芯片。5.3 我个人的几条实操心得第一MRAM 虽然写入寿命几乎无限但也不是让你往死里写。环形缓冲区的设计本身就是为了均匀磨损但如果你频繁写同一个地址比如配置区那还是要注意。配置区我一般只在参数变更时写平时只读这样寿命完全不是问题。第二SPI 通信的可靠性七分靠硬件三分靠软件。硬件上走线短、滤波足、地平面完整软件上再加 CRC 校验和重试机制基本就稳了。我一般会在 MRAM_Read 里加三次重试如果 CRC 连续三次失败就上报错误让上层处理。第三调试阶段一定要留测试点。SCK、MOSI、MISO、CS 这四根线PCB 上都要留测试焊盘不然出了问题只能飞线很难看也不可靠。我吃过这个亏后来每次画板子都老老实实留测试点。第四MR25H40CDF 的 HOLD 引脚如果不用就拉高别悬空。悬空的话偶尔的干扰脉冲会让 MRAM 进入保持状态SPI 通信就断了。这个坑我踩过查了两天才发现是 HOLD 引脚的问题。这套 MR25H40CDF 加 PIC18LF4515 的方案我前后做了三个项目从数据记录仪到工业控制器稳定性都没出过问题。代码量不大底层驱动加应用逻辑总共不到 2000 行维护起来也轻松。如果你也在找一种简单可靠的工业存储方案不妨试试这个组合。