
1. 项目缘起与整体设计思路工业现场的数据记录仪、电力监控终端、PLC 扩展模块这类设备对存数据这件事的要求其实非常朴素写入要快、掉电不能丢、擦写寿命要够长、工作温度范围要宽。但就是这几个朴素要求把 EEPROM、NOR Flash、甚至带后备电池的 SRAM 都逼到了墙角。EEPROM 写入慢且寿命只有百万次量级NOR Flash 有擦除块限制、写入前得先擦、掉电窗口期容易丢数据带电池的 SRAM 又要考虑电池更换和环保问题。我这两年做的一个工业采集板项目最后选定的方案就是MR25H40CDF 这颗 4Mbit 的 MRAM 芯片搭配 PIC18F86J11 这款 8 位单片机用 SPI 总线做数据存储和读取。这套组合在工业嵌入式圈子里不算新鲜但真正把它用稳、用透的资料并不多所以我把整个设计、调试、踩坑的过程整理出来。先说说这两个主角是什么。MR25H40CDF是 Everspin 家的串行 MRAM磁性随机存储器容量 512K×8bit也就是 4MbitSPI 接口支持最高 40MHz 时钟。MRAM 这个东西最大的特点就是它像 RAM 一样可以按字节随机读写不需要擦除写入速度是纳秒级而且掉电后数据能保持 20 年以上擦写寿命号称无限实际上 10^14 次以上。PIC18F86J11是 Microchip 的 8 位 MCU80 引脚128KB Flash、4KB RAM自带 MSSP 模块可以配置成 SPI 主机工作温度 -40℃ 到 85℃ 工业级。两者通过 SPI 连接MCU 做主MRAM 做从就能实现一个类 EEPROM 但性能碾压 EEPROM的非易失存储方案。这套方案解决的核心问题是在工业环境下需要高频次、小数据量、掉电即存的场景。比如每 10ms 记录一次电机电流值、每秒钟保存一次设备运行状态、故障瞬间要立刻把现场数据落盘。这些场景用 Flash 会死在擦除寿命和写入延迟上用 EEPROM 会死在写入速度上用 FRAM 虽然也行但容量和价格不占优。MRAM 恰好卡在这个甜点位上。适合谁来参考这篇内容如果你正在做工业数据采集、电力仪表、医疗设备、汽车电子里的数据记录模块或者你只是单纯想搞明白 SPI 接口的 MRAM 到底怎么用、和 Flash 有什么区别、PIC 单片机怎么配 SPI 主机那这篇都能给你可直接抄作业的东西。我会从硬件连接、SPI 时序配置、读写指令、页写与字节写、状态寄存器、掉电保护策略一路讲到实际调试中遇到的怪问题。2. 核心器件解析与选型背后的逻辑2.1 MR25H40CDF 到底强在哪和 EEPROM、Flash 的硬碰硬对比很多人第一次听说 MRAM 会以为是某种新型 Flash其实原理完全不同。MRAM 用磁性隧道结MTJ存储数据通过电子自旋方向表示 0 和 1写入靠电流产生的磁场翻转磁矩。这个物理机制决定了它的几个关键特性写入不需要高压电荷泵所以写入能量极低写入是直接覆盖不需要先擦除读写都是随机访问没有块的概念。我把三者在工业场景下的关键参数拉个表你一眼就能看出差距特性MR25H40CDF (MRAM)典型 EEPROM (如 25LC256)典型 NOR Flash (如 W25Q64)容量4Mbit (512KB)256Kbit (32KB)64Mbit (8MB)接口SPI 最高 40MHzSPI 最高 10MHzSPI 最高 104MHz写入方式按字节直接写按字节直接写必须先擦除扇区字节写入时间无内部写周期纳秒级5ms 典型擦除 45ms 编程 0.7ms擦写寿命10^14 次10^6 次10^5 次数据保持20 年200 年20 年掉电风险写入即完成无窗口有内部写周期窗口擦除中掉电会丢整块工作温度-40~85℃-40~85℃-40~85℃单价参考较高低极低这张表里最关键的一行是字节写入时间。EEPROM 写一个字节要等 5ms 内部写周期这 5ms 里如果你掉电这个字节就废了。Flash 更惨擦除一个 4KB 扇区要 45ms这期间掉电整块数据都可能损坏。而 MRAM 写入是即时的SPI 时钟发完数据数据就已经在存储单元里了没有内部写周期没有掉电窗口。这一点对工业设备来说价值巨大——你可以在检测到掉电中断的瞬间抢在电源彻底跌落到 MCU 工作电压以下之前把关键数据写进 MRAM成功率极高。2.2 为什么选 PIC18F86J11 做主控PIC18F86J11 在 2024 年看确实不算新片子但工业项目选型从来不是选最新的而是选最稳、供货最久、资料最全的。这颗片子的几个点很契合这个项目第一它自带MSSPMaster Synchronous Serial Port模块可以硬件配置成 SPI 主机模式支持 Mode 0/1/2/3 四种时序最高时钟是 Fosc/4。我用 8MHz 晶振系统时钟 32MHzPLL 后SPI 可以跑到 8MHz对 MRAM 来说完全够用甚至还有余量。第二它有128KB Flash 和 4KB RAM跑一个数据记录的状态机绰绰有余还能留出空间做数据校验和协议解析。第三80 引脚意味着 IO 资源丰富SPI 之外还能接 LCD、按键、多路 ADC、RS485 收发器适合做完整的工业终端。第四工业级温度范围和宽电压 2.0~3.6V和 MR25H40CDF 的 2.7~3.6V 供电能直接对接不需要电平转换。提示PIC18F86J11 的 MSSP 模块在 SPI 主机模式下SS片选引脚需要手动用普通 IO 控制硬件不会自动拉片选。这一点和 STM32 的硬件 NSS 不同配置时别踩坑。2.3 SPI 总线选型的考量为什么不用 I2C 或并口MR25H40CDF 只有 SPI 版本没有 I2C 版本所以接口其实没得选。但即便有得选SPI 也是这个场景的最优解。I2C 最高 400kHz 或 1MHzFast Mode Plus而 SPI 可以到 40MHz差了两个数量级。工业数据记录要求快速落盘SPI 的带宽优势是决定性的。并口虽然更快但引脚太多PCB 布线复杂80 引脚的 MCU 也扛不住并口 MRAM 的引脚开销。SPI 的另一个好处是全双工读数据的同时可以发下一条指令配合 DMA虽然 PIC18 没有 DMA但可以用中断优化能进一步提升吞吐。而且 SPI 协议简单四根线SCK、MOSI、MISO、CSPCB 上走线清爽抗干扰设计也好做。3. 硬件连接与 SPI 时序配置实操3.1 硬件连线与 PCB 布局要点MR25H40CDF 是 8 引脚 SOIC 封装引脚定义很标准1 脚 CS片选低有效、2 脚 SOMISO、3 脚 WP写保护低有效、4 脚 VSS、5 脚 SIMOSI、6 脚 SCK、7 脚 HOLD低有效、8 脚 VDD。和 PIC18F86J11 的连接如下MR25H40CDF CS → PIC RC2普通 IO软件控制片选MR25H40CDF SO → PIC RC4MSSP 的 SDIMR25H40CDF SI → PIC RC5MSSP 的 SDOMR25H40CDF SCK → PIC RC3MSSP 的 SCKMR25H40CDF WP → 接 VDD不用写保护时拉高MR25H40CDF HOLD → 接 VDD不用暂停功能时拉高VDD 接 3.3VVSS 接地PCB 布局上有几个经验点。SPI 的 SCK 是最容易出问题的线因为它频率高、边沿陡容易辐射干扰。我的做法是 SCK 走线尽量短两侧包地如果板子空间允许在 SCK 上串一个 22Ω 到 33Ω 的电阻做阻抗匹配和边沿缓化。MISO 和 MOSI 可以并行走但不要和 SCK 长距离平行避免串扰。CS 线虽然频率低但它是控制信号走线也要短且要远离电源开关节点。去耦电容方面MR25H40CDF 的 VDD 引脚旁边必须放一个0.1μF 的陶瓷电容越近越好最好在 2mm 以内。如果板子上有多颗 SPI 从设备每颗都要独立去耦不要共用。我见过一个案例MRAM 偶发读写错误查了半天最后发现是去耦电容离芯片 8mm 远高频时电源纹波导致内部逻辑误动作挪近就好了。注意MR25H40CDF 的 WP 和 HOLD 引脚是低有效如果悬空内部有弱上拉但不可靠工业环境下必须明确拉到 VDD否则可能被干扰触发写保护或暂停导致读写失败。3.2 PIC18F86J11 的 MSSP 配置寄存器级操作PIC18F86J11 配置 SPI 主机核心是操作这几个寄存器SSPCON1、SSPCON2、SSPSTAT、SSPADD、TRISC、TRISB。我直接给出可用的初始化代码用 XC8 编译器void SPI_Init(void) { // 配置 IO 方向 TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC2 0; // CS 输出 // 关闭 MSSP 再配置 SSPCON1 0x00; SSPSTAT 0x00; // SPI 主机模式时钟 Fosc/4 // SSPM3:SSPM0 0000 - SPI Master, clock Fosc/4 SSPCON1bits.SSPM 0b0000; // 时钟极性空闲低电平 (CKP0) SSPCON1bits.CKP 0; // 数据采样中间采样 (SMP0) SSPSTATbits.SMP 0; // 数据变化空闲到有效沿 (CKE0) // 组合起来就是 SPI Mode 0,0 SSPSTATbits.CKE 0; // 使能 MSSP SSPCON1bits.SSPEN 1; // CS 默认拉高 CS_MRAM 1; }这里解释几个关键选择。SPI Mode 0,0CPOL0CPHA0是 MR25H40CDF 支持的模式之一也是最常用的。CKP0 表示空闲时 SCK 为低CKE0 表示数据在 SCK 从低到高的上升沿被采样。MR25H40CDF 的数据手册明确支持 Mode 0 和 Mode 3我选 Mode 0 是因为大部分 SPI 从设备都兼容它调试时用逻辑分析仪看波形也最直观。时钟频率 Fosc/4如果系统时钟是 32MHzSPI 就是 8MHz。MR25H40CDF 最高支持 40MHz8MHz 远在安全范围内。为什么不跑更快因为 PIC18 的 MSSP 在 Fosc/4 下最稳再快就要用 Fosc/16 之外的配置而且 8MHz 对数据记录场景已经足够——写 512 字节只要 512×8/8MHz 512μs完全满足工业实时性。读写一个字节的函数uint8_t SPI_ExchangeByte(uint8_t data) { SSPBUF data; // 写入发送缓冲 while(!SSPSTATbits.BF); // 等待接收完成 return SSPBUF; // 读取接收数据 }这个函数是全双工的发一个字节的同时收一个字节。注意SSPSTATbits.BF是缓冲满标志发送完成后硬件自动置位读走 SSPBUF 后自动清零。3.3 MR25H40CDF 的指令集与读写时序MR25H40CDF 的指令集很简洁常用的就几条指令名称指令码功能WREN0x06写使能写操作前必须发WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读数据的完整时序是拉低 CS → 发 0x03 → 发 24 位地址3 字节高位在前→ 连续读 N 字节 → 拉高 CS。写数据的时序是拉低 CS → 发 0x06WREN→ 拉高 CS → 拉低 CS → 发 0x02 → 发 24 位地址 → 连续写 N 字节 → 拉高 CS。注意 WREN 是一个独立的事务必须先发 WREN 并拉高 CS再开始写事务。这是很多新手容易漏的一步不发 WREN 直接写MRAM 会忽略写操作数据写不进去而且读回来还是旧值非常迷惑。读函数实现void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { CS_MRAM 0; SPI_ExchangeByte(0x03); // READ 指令 SPI_ExchangeByte((addr 16) 0xFF); // 地址高字节 SPI_ExchangeByte((addr 8) 0xFF); // 地址中字节 SPI_ExchangeByte(addr 0xFF); // 地址低字节 for(uint16_t i 0; i len; i) { buf[i] SPI_ExchangeByte(0x00); // 发 dummy 收数据 } CS_MRAM 1; }写函数实现void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { // 第一步写使能 CS_MRAM 0; SPI_ExchangeByte(0x06); // WREN CS_MRAM 1; // 第二步写数据 CS_MRAM 0; SPI_ExchangeByte(0x02); // WRITE 指令 SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { SPI_ExchangeByte(buf[i]); } CS_MRAM 1; }提示MR25H40CDF 的地址是 24 位的因为 512KB 需要 19 位地址但芯片用 3 字节地址格式高 5 位保留。写地址时高字节的高 5 位要填 0否则可能访问到未定义区域。4. 数据存储策略与掉电保护实战4.1 工业数据记录的存储结构设计MRAM 容量 512KB怎么组织数据直接决定了系统的可靠性和可维护性。我的做法是把整个空间分成几个区域0x00000 ~ 0x000FF系统配置区存设备参数、校准系数、通信地址等256 字节足够。0x00100 ~ 0x00FFF循环记录区存实时采集数据按固定长度记录循环覆盖。0x01000 ~ 0x01FFF故障快照区检测到异常时把现场数据写到这里不覆盖。0x02000 ~ 0x7FFFF扩展数据区留给后续功能。循环记录区的设计是关键。每条记录固定 32 字节包含时间戳4 字节、通道数据16 字节、状态字4 字节、CRC 校验2 字节、保留6 字节。512KB 减去前面的区域大约能存 16000 条记录。用一个写指针记录当前写位置写满后回到起点覆盖最旧的数据。这里有个技巧写指针本身也要存到 MRAM 里而且要用双备份加校验的方式。我是在配置区里存两个写指针副本每次更新时先写副本 A再写副本 B读取时如果 A 和 B 不一致取较大的那个因为较大的更新。这样即使写指针更新过程中掉电也能恢复出正确的值。4.2 掉电检测与紧急写入的实现工业设备最怕的就是掉电丢数据。MR25H40CDF 的即时写入特性让掉电瞬间保存成为可能但前提是 MCU 要能及时检测到掉电并抢在电源跌落前完成写入。硬件上我在电源输入端加了一个分压电阻网络接到 PIC 的 ADC 引脚同时用一个比较器产生掉电中断。电源正常时是 24V 转 3.3V掉电时 3.3V 会有一个跌落过程从 3.3V 跌到 MCU 最低工作电压 2.0V 大约有 5~10ms 的窗口取决于输出电容大小。这 5~10ms 就是我们的黄金时间。软件上掉电中断服务程序要做的事void __interrupt() ISR(void) { if(PIR1bits.ADIF PIE1bits.ADIE) { // 掉电中断 PIE1bits.ADIE 0; // 关中断防止重入 // 立即保存关键数据 SaveCriticalData(); // 进入低功耗或等待断电 while(1); } }SaveCriticalData()函数要把当前采集的最新数据、运行状态、写指针全部写入 MRAM。因为 MRAM 写入是纳秒级写 32 字节记录加上写指针更新总共不到 100μs5ms 窗口绰绰有余。注意掉电中断里不要做浮点运算、不要调用 printf、不要做复杂逻辑判断。所有需要的数据要提前算好放在 RAM 里中断里只做搬运。我见过有人在掉电中断里做 CRC 计算结果时间不够数据写了一半就断电了。4.3 数据校验与坏块管理MRAM 虽然可靠性极高但工业环境下的电磁干扰、电源波动仍可能导致 SPI 通信误码。所以每条记录都要加 CRC16 校验。我用的是 CRC-16/CCITT 算法查表法实现速度快const uint16_t crc_table[16] { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF }; uint16_t CRC16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; while(len--) { crc (crc 4) ^ crc_table[((crc 12) ^ (*data 4)) 0x0F]; crc (crc 4) ^ crc_table[((crc 12) ^ (*data 0x0F)) 0x0F]; data; } return crc; }读取时先算 CRC和存储的 CRC 比对不一致就标记这条记录无效跳过。如果连续多条记录 CRC 错误说明这个区域可能有问题可以触发区域迁移——把有效数据搬到备用区域更新区域映射表。MRAM 没有坏块概念但长期强干扰下个别单元可能翻转这种迁移策略能保证系统长期稳定运行。5. 常见问题排查与调试经验实录5.1 读写失败问题速查表调试 SPI MRAM 时遇到的问题八成集中在下面这几类。我整理成速查表你遇到问题时按顺序排查现象可能原因排查方法解决措施读回全是 0xFFMISO 没接好或 CS 没拉低示波器看 MISO 和 CS 波形检查连线确认 CS 时序读回全是 0x00芯片没供电或 SPI 模式错万用表量 VDD逻辑分析仪看时序检查供电确认 Mode 0/3写入后读回旧值没发 WREN 或 WP 被拉低检查写函数是否发 0x06补 WRENWP 拉高偶发数据错误去耦不良或 SCK 干扰加大去耦缩短 SCK 走线加 0.1μF 电容串匹配电阻高速时出错SPI 时钟太快降速到 1MHz 测试降低时钟或优化布线地址错乱地址字节顺序错确认高位在前修正地址发送顺序5.2 几个我实际踩过的坑第一个坑CS 拉高拉低的时机。我一开始写读函数时在发完最后一个字节后立刻拉高 CS结果偶尔读到错误数据。后来用逻辑分析仪抓波形发现最后一个 SCK 上升沿到 CS 拉高之间需要至少一个时钟周期的保持时间否则 MRAM 内部还没把数据锁存到输出寄存器。解决方法是发完最后一个字节后加一个NOP()或者微秒级延时再拉高 CS。这个延时不用长1μs 足够但必须有。第二个坑WREN 和 WRITE 之间的 CS 必须拉高。我最初图省事把 WREN 和 WRITE 放在同一个 CS 低电平周期里结果写不进去。查手册才发现WREN 是一个独立命令必须在 CS 上升沿时被锁存然后才能开始新的写事务。这个细节手册里写得不显眼但很关键。第三个坑PIC 的 MSSP 缓冲溢出。PIC18 的 SSPBUF 只有一个字节的缓冲如果发送速度太快前一个字节还没被读走后一个字节就写进去会导致 SSPOV 溢出错误。我的 SPI_ExchangeByte 函数里用while(!SSPSTATbits.BF)等待但如果中断里也调用了 SPI 函数就可能冲突。解决办法是 SPI 操作加互斥锁或者确保 SPI 操作只在主循环里做中断里不碰 SPI。第四个坑电源上电顺序。MR25H40CDF 和 PIC18F86J11 如果共用 3.3V上电时如果 MRAM 先上电、MCU 后上电MRAM 的 CS 引脚在 MCU 还没初始化时是浮空的可能被干扰触发误写。我的做法是在 CS 线上加一个 10kΩ 上拉电阻到 VDD确保上电期间 CS 保持高电平MRAM 处于未选中状态。5.3 用逻辑分析仪调试 SPI 的正确姿势调试 SPI 最有效的工具是逻辑分析仪几十块钱的 8 通道 USB 逻辑分析仪就够用。接线时至少接四根CS、SCK、MOSI、MISO。采样率要至少是 SPI 时钟的 10 倍8MHz SPI 就用 100MHz 采样率。看波形时重点看几个点CS 拉低到第一个 SCK 边沿之间是否有足够建立时间至少 5nsSCK 的占空比是否接近 50%MISO 的数据在 SCK 采样沿是否稳定CS 拉高前最后一个 SCK 沿到 CS 上升沿的保持时间。如果这些时序都正常但数据还是错那就要怀疑电源和去耦了。我习惯在调试时先发一条 READ 指令读固定地址比如 0x000000看返回的数据是否稳定。如果稳定再测试写入。写入测试用写-读-比对三步法写一个已知模式比如 0xAA、0x55 交替读回来比对不一致就报错。这个方法能快速定位是写问题还是读问题。6. 性能优化与扩展思路6.1 提升 SPI 吞吐的几种手段8MHz SPI 写 512 字节要 512μs对于大多数工业场景够用但如果你的采集频率很高比如每 1ms 记录一次那就需要优化。几个可行的手段第一提高 SPI 时钟。PIC18F86J11 在 32MHz 系统时钟下MSSP 最高可以到 Fosc/4 8MHz。如果换成 64MHz 系统时钟PIC18F86J11 最高支持 64MHzSPI 可以到 16MHz。但要注意 PCB 布线质量要跟上SCK 走线要更短匹配电阻要调整。第二减少 CS 切换次数。连续写多个字节时CS 只在开始时拉低、结束时拉高中间不切换。我的 MRAM_Write 函数已经这么做了一次写 32 字节记录只切换两次 CSWREN 一次WRITE 一次。第三用中断或状态机优化等待。while(!SSPSTATbits.BF)是忙等浪费 CPU 时间。如果系统还有其他任务可以把 SPI 传输做成状态机发一个字节后去处理其他事收到中断再发下一个。不过 PIC18 没有 DMA这个优化收益有限除非你的系统任务很重。6.2 从单芯片到多芯片扩展如果 512KB 不够用可以挂多颗 MR25H40CDF每颗独立 CS共用 SCK、MOSI、MISO。PIC18F86J11 的 IO 足够控制 4~8 颗片选。软件上维护一个设备地址到 CS 引脚的映射表读写时先选对应 CS再发指令。多芯片的地址空间可以统一编址比如芯片 0 对应 0x00000~0x7FFFF芯片 1 对应 0x80000~0xFFFFF上层应用不用关心底层是哪颗芯片。这种设计在数据记录仪里很实用容量可以按需扩展。6.3 这套方案还能用在哪些场景除了工业数据记录这套 MRAM PIC 的组合还能用在几个地方。医疗设备里存病人参数和校准数据要求高可靠和长寿命MRAM 很合适。汽车电子里的里程记录、故障码存储需要宽温和抗振动MRAM 没有机械部件比带电池的 SRAM 可靠。智能电表里的电量数据要求掉电不丢且写入频繁MRAM 的无限擦写正好匹配。甚至在一些高频交易或测试测量设备里需要缓存大量瞬时数据MRAM 的快速写入也能派上用场。我有个朋友做电力故障录波用 MRAM 做前端缓存检测到故障后把缓存数据转存到 SD 卡效果很好。最后分享一个小技巧MR25H40CDF 的状态寄存器里有一个 SRWD 位状态寄存器写保护配合 WP 引脚可以实现硬件写保护。量产时可以把配置区设为写保护防止现场误操作改坏校准数据。调试阶段先不启用等参数确定后再打开这个流程能省不少事。