ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MRAM与PIC18F96J94工业数据存储方案:SPI驱动与掉电保护实战

2026/10/4 13:44:45 拓冰建站 浏览量
MRAM与PIC18F96J94工业数据存储方案:SPI驱动与掉电保护实战 1. 项目缘起与方案选型为什么是 MR25H40CDF 加 PIC18F96J941.1 一个真实的需求场景前阵子接了个工业数据采集终端的活儿客户的要求很朴素设备要在产线上连续跑断电不能丢数据写入要快寿命要长最好十年内别让我换存储芯片。听起来简单但真选起型来坑一个接一个。传统方案无非几条路。EEPROM 写入慢、寿命有限频繁写个几万次就开始提心吊胆NOR Flash 擦除块大、写入前要擦、擦写寿命也就十万次量级而且写入过程中掉电容易丢整块数据铁电存储器 FRAM 倒是快、寿命也高但容量小、价格贵做大缓冲区不划算。这时候 MRAM 就进入了视野——MR25H40CDF这颗 4Mbit 的磁性随机存储器SPI 接口读写速度能到 40MHz几乎无限次擦写掉电数据不丢而且不需要像 Flash 那样先擦后写。主控这边选PIC18F96J94理由也很实在。这颗 8 位单片机自带硬件 SPI 模块、64KB Flash、接近 4KB RAM工业温度范围外设够用开发工具链成熟最关键的是它的 SPI 模块支持多种时钟极性和相位组合跟 MRAM 的时序匹配起来很省心。对于这种中等复杂度、对成本敏感、又要长期稳定运行的工业设备PIC18F96J94 是个很稳的选择。1.2 MRAM 到底比 Flash 和 EEPROM 强在哪很多人第一次听说 MRAM 会懵这里用生活化的方式讲清楚。你可以把 Flash 想象成一块黑板写字之前必须先把整块黑板擦干净擦的时候粉尘飞扬写入放大而且擦多了黑板就磨薄了寿命有限。EEPROM 像是一本便签本可以一页一页撕下来重写但撕的速度慢撕多了本子也会散架。MRAM 则像是一块磁性白板每个存储单元是一个微小的磁隧道结通过改变磁化方向来记录 0 和 1。写的时候直接翻转磁矩不需要擦除步骤写入速度是纳秒级擦写次数理论上超过 10 的 15 次方基本可以认为无限。掉电后磁化方向保持数据自然不丢。这就是为什么工业场景里MRAM 越来越受欢迎。具体到 MR25H40CDF几个关键参数值得记住参数项数值实际意义容量4Mbit512KB足够做数据缓冲和参数存储接口SPI最高 40MHz读写速度快接线简单供电2.7V ~ 3.6V跟 3.3V 系统直接对接擦写寿命近乎无限高频写入场景无压力数据保持20 年以上工业设备生命周期内无忧工作温度-40°C ~ 85°C工业级产线环境没问题1.3 为什么不用并口 MRAM 或串行 Flash有人会问并口 MRAM 速度更快为什么不用答案很简单引脚太多。PIC18F96J94 的 IO 资源有限并口 MRAM 动辄需要 20 多根地址线加数据线布线复杂PCB 面积大成本也上去了。SPI 接口只需要 4 根线CS、SCK、MOSI、MISO对于 512KB 这个量级的存储需求40MHz 的 SPI 带宽完全够用。至于串行 Flash前面说了擦除块和写入寿命是硬伤。工业设备经常需要记录运行日志、保存传感器校准参数、缓存突发数据这些操作可能每秒都在发生Flash 根本扛不住。MRAM 的“直接覆盖写”特性让软件逻辑也简单很多不需要维护擦除块管理、磨损均衡这些复杂机制。提示选型时不要只看单价。MRAM 单颗价格确实比同容量 Flash 高但算上寿命、维护成本、软件复杂度、故障率综合成本往往更低。我做过一个粗略估算在每天写入 10 万次的场景下Flash 方案三年内需要更换存储芯片的概率超过 60%而 MRAM 方案基本为零。2. 硬件连接与 SPI 时序把线接对是第一步2.1 引脚连接与硬件设计要点MR25H40CDF 是 8 引脚封装实际用到的信号线就 4 根加上电源和地接线非常清爽。跟 PIC18F96J94 对接时我用的引脚分配如下MR25H40CDF 引脚PIC18F96J94 引脚功能说明CSRC2可配置为普通 IO片选低电平有效SCKRC3SCKSPI 时钟SIMOSIRC5SDO主出从入SOMISORC4SDI主入从出VCC3.3V电源VSSGND地WP接 VCC 或悬空写保护不需要时拉高HOLD接 VCC 或悬空保持不需要时拉高这里有个细节要注意PIC18F96J94 的 SPI 模块引脚是复用的RC3、RC4、RC5 分别对应 SCK、SDI、SDO。配置的时候要把 TRISC 的对应位设好SCK 和 SDO 是输出SDI 是输入。CS 我单独用 RC2 做软件片选这样灵活可以在一条总线上挂多个 SPI 设备。PCB 布线方面SPI 信号线尽量短尤其是 SCK 时钟线走线要远离模拟信号和电源开关节点。如果线长超过 10cm建议在 SCK 上串一个 22Ω 到 33Ω 的电阻做阻抗匹配减少过冲和振铃。MRAM 的电源引脚旁边放一个 0.1μF 的陶瓷电容越近越好这是老生常谈但真的管用。2.2 SPI 模式选择CPOL 和 CPHA 怎么定SPI 有四种模式由 CPOL时钟极性和 CPHA时钟相位组合决定。MR25H40CDF 支持模式 0CPOL0CPHA0和模式 3CPOL1CPHA1。我一般用模式 0因为这是最常用的大多数 SPI 设备都支持调试起来也方便。模式 0 的时序特征是时钟空闲时为低电平数据在时钟上升沿采样下降沿变化。用逻辑分析仪抓波形的时候你会看到 CS 拉低后第一个 SCK 上升沿之前MOSI 上的数据就已经稳定了。这个细节很关键如果主控配置不对第一个 bit 容易丢。PIC18F96J94 的 SSPCON1 寄存器里CKP 位控制时钟极性CKE 位控制时钟相位。配置成模式 0 的话CKP0CKE0。具体寄存器设置我后面会给出完整代码。2.3 上电初始化和写保护处理MR25H40CDF 上电后需要一小段时间稳定数据手册上写的是 10ms 以内。我一般在上电初始化时加 20ms 延时保险一点。WP 引脚如果不用硬件写保护直接拉高到 VCC。HOLD 引脚同理拉高或者悬空都行但悬空容易受干扰建议还是拉高。有个坑我踩过有一次 WP 引脚悬空结果在电机启停的瞬间MRAM 偶尔会进入写保护状态数据写不进去。后来把 WP 和 HOLD 都硬拉到 VCC问题消失。所以别偷懒该拉的拉该接的接。注意MRAM 虽然写入不需要擦除但写操作仍然需要一定时间。MR25H40CDF 的单次写入周期典型值是 10ns 量级但 SPI 传输本身需要时间。在 40MHz 时钟下传输一个字节需要 0.2μs加上命令和地址开销写一个字节大约 1μs。这个速度已经比 EEPROM 快了几个数量级。3. 软件驱动实现从底层字节读写到数据管理3.1 SPI 底层驱动代码先上 PIC18F96J94 的 SPI 初始化代码。我用的是 MCC 生成的框架但核心寄存器配置是自己写的这样更可控。// SPI 初始化模式 0时钟 Fosc/4 void SPI_Init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC2 0; // CS 输出 CS 1; // 片选拉高空闲状态 SSPCON1 0x00; // 先复位 SSP 模块 SSPSTAT 0x00; // 模式 0CKE0 SSPCON1 0x20; // SSPEN1CKP0Fosc/4 // 实际时钟 64MHz / 4 16MHz如果需要 40MHz 得用 PLL }这里解释一下时钟。PIC18F96J94 最高跑 64MHz用 PLL 倍频SPI 时钟是 Fosc 的分频。SSPCON1 的低四位控制分频比0000 是 Fosc/40001 是 Fosc/160010 是 Fosc/64。如果系统时钟 64MHzFosc/4 就是 16MHz。MRAM 支持 40MHz但 16MHz 对大多数应用已经绰绰有余。如果要更快可以改分频比但要注意 PCB 布线质量。字节收发函数// SPI 收发一个字节 uint8_t SPI_ExchangeByte(uint8_t data) { SSPBUF data; // 写入发送缓冲 while(!SSPIF); // 等待传输完成 SSPIF 0; // 清除中断标志 return SSPBUF; // 读取接收数据 }这个函数是阻塞式的简单可靠。如果系统对实时性要求高可以用中断方式但 SPI 传输本身就快阻塞几十微秒对大多数应用没影响。3.2 MRAM 命令集与读写操作MR25H40CDF 的命令集很精简常用的就几条命令十六进制码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据写数据之前必须先发 WREN 命令这是很多新手容易忘的。MRAM 不像 RAM 那样直接写它有个写使能锁存机制每次写操作前都要重新使能。写一个字节的流程// 向指定地址写一个字节 void MRAM_WriteByte(uint32_t addr, uint8_t data) { CS 0; SPI_ExchangeByte(0x06); // WREN CS 1; CS 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr 16) 0xFF); // 地址高字节 SPI_ExchangeByte((addr 8) 0xFF); // 地址中字节 SPI_ExchangeByte(addr 0xFF); // 地址低字节 SPI_ExchangeByte(data); // 数据 CS 1; // 等待写入完成实际 MRAM 很快但保险起见 while(MRAM_IsBusy()); }读操作更简单不需要 WREN// 从指定地址读一个字节 uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; CS 0; SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); data SPI_ExchangeByte(0xFF); // 发送哑元读取数据 CS 1; return data; }注意读的时候要发一个哑元字节0xFF 或任意值来产生时钟才能把数据移进来。这是 SPI 的标准操作但初学者经常在这里卡住。3.3 页写与连续读的优化单字节读写虽然简单但效率低。MR25H40CDF 支持页写和连续读一次可以传输多个字节。页大小是 256 字节写的时候不能跨页否则会回卷到页首覆盖数据。连续读就自由多了给个起始地址然后一直发时钟数据就源源不断地出来地址自动递增直到 CS 拉高。这个特性非常适合批量读取日志或参数块。// 连续读取多个字节 void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { CS 0; SPI_ExchangeByte(0x03); 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(0xFF); } CS 1; }页写函数要处理跨页的情况我一般在上层做地址对齐保证每次写入不跨页。如果数据长度超过页边界就拆成多次写。// 页写注意不要跨页 void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint8_t len) { // 确保不跨页 uint8_t page_offset addr 0xFF; if(page_offset len 256) { len 256 - page_offset; } CS 0; SPI_ExchangeByte(0x06); // WREN CS 1; CS 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr 16) 0xFF); SPI_ExchangeByte((addr 8) 0xFF); SPI_ExchangeByte(addr 0xFF); for(uint8_t i 0; i len; i) { SPI_ExchangeByte(buf[i]); } CS 1; while(MRAM_IsBusy()); }3.4 状态寄存器与忙判断MRAM 的状态寄存器只有一位有用就是 WIPWrite In Progress。写操作进行时 WIP 为 1完成后为 0。虽然 MRAM 写入很快但在高频写入场景下还是判断一下比较稳妥。// 判断 MRAM 是否忙 uint8_t MRAM_IsBusy(void) { uint8_t status; CS 0; SPI_ExchangeByte(0x05); // RDSR status SPI_ExchangeByte(0xFF); CS 1; return (status 0x01); }实测下来MRAM 的写入周期极短大多数情况下这个函数返回 0 的速度比调用开销还快。但为了代码的健壮性保留这个判断没坏处。4. 数据管理策略让存储真正服务于工业应用4.1 存储空间划分与地址规划512KB 的空间说大不大说小不小规划不好后期会很痛苦。我的做法是按功能分区每个区域有固定的起始地址和大小用宏定义管理。区域名称起始地址大小用途系统参数区0x000004KB设备配置、校准系数运行日志区0x01000256KB循环记录运行状态数据缓存区0x41000252KB突发数据缓冲备份区0x7F0004KB关键参数备份地址规划的原则是关键数据放前面大块数据放后面留出备份区。系统参数区用双备份加校验确保不会因为单点故障丢失配置。4.2 循环日志的实现与磨损均衡虽然 MRAM 寿命近乎无限但良好的日志管理仍然重要。我实现了一个简单的循环日志维护一个写指针每次写新记录时指针递增写到区域末尾就回卷到开头。每条记录包含时间戳、数据长度、数据和 CRC 校验。typedef struct { uint32_t timestamp; uint16_t length; uint8_t data[32]; uint16_t crc; } LogRecord; uint32_t log_write_ptr LOG_START_ADDR; void Log_Write(uint8_t *data, uint16_t len) { LogRecord rec; rec.timestamp GetSystemTick(); rec.length len; memcpy(rec.data, data, len); rec.crc CalculateCRC16((uint8_t*)rec, sizeof(rec) - 2); // 检查是否超出区域 if(log_write_ptr sizeof(LogRecord) LOG_END_ADDR) { log_write_ptr LOG_START_ADDR; } MRAM_WritePage(log_write_ptr, (uint8_t*)rec, sizeof(LogRecord)); log_write_ptr sizeof(LogRecord); }读取日志时从当前写指针往前找遇到 CRC 正确的记录就解析。这样即使掉电最多丢失最后一条不完整的记录前面的数据都是安全的。4.3 掉电保护与数据完整性工业现场掉电是常态数据完整性必须保证。我的策略是关键数据写两份地址不同每份带 CRC。读取时两份都读CRC 都对的取最新只有一份对的用那份两份都不对就报错。typedef struct { uint32_t magic; // 标识字用于判断是否初始化过 uint8_t config[64]; uint16_t crc; } ConfigBlock; ConfigBlock cfg_a, cfg_b; void Config_Save(void) { cfg_a.magic 0xAA55AA55; cfg_a.crc CalculateCRC16((uint8_t*)cfg_a, sizeof(cfg_a) - 2); MRAM_WritePage(CFG_A_ADDR, (uint8_t*)cfg_a, sizeof(cfg_a)); cfg_b cfg_a; cfg_b.magic 0x55AA55AA; cfg_b.crc CalculateCRC16((uint8_t*)cfg_b, sizeof(cfg_b) - 2); MRAM_WritePage(CFG_B_ADDR, (uint8_t*)cfg_b, sizeof(cfg_b)); }这种双备份加 CRC 的方式在我做的多个工业项目里都验证过可靠性很高。即使写入过程中掉电至少有一份是完整的。实操心得CRC 校验用 CRC16-CCITT 就够了计算快检错能力强。不要用简单的累加和那个检错能力太弱工业环境里电磁干扰大累加和容易漏检。5. 调试与问题排查那些文档里不会写的事5.1 SPI 通信失败的常见原因调试 SPI 最怕的就是没反应。我总结了一个排查顺序按这个来基本能定位问题现象可能原因排查方法完全无数据CS 没拉低、电源没接、SPI 没使能万用表测电压逻辑分析仪抓 CS 和 SCK读出来全是 0xFFMISO 没接、从机没供电、模式不对检查接线确认 CPOL/CPHA读出来全是 0x00MISO 被拉低、从机损坏断开从机测 MISO 是否浮空数据偶尔错时钟太快、布线太长、干扰降低 SPI 时钟加匹配电阻写入不成功忘了 WREN、WP 引脚状态不对检查写使能流程测 WP 电压逻辑分析仪是调试 SPI 的神器几百块一个的就行。抓一下波形CS、SCK、MOSI、MISO 四根线一目了然比盲猜快多了。5.2 写入数据丢失的排查思路有一次客户反馈设备运行几天后参数丢了。我过去排查发现是写入时没有判断忙状态连续写多个字节时前一个写操作还没完成后一个就开始了导致数据错乱。虽然 MRAM 写入快但在 SPI 时钟较慢的情况下写入周期相对传输时间不可忽略。解决办法很简单每次写操作后加忙判断或者加一个小延时。我后来统一改成写完后调用MRAM_IsBusy()等待问题再没出现过。另一个坑是页写跨页。有次写 300 字节的数据没做跨页处理结果写到页尾后回卷到页首把前面的数据覆盖了。这个 bug 隐蔽性强因为读出来的数据看起来是“对”的只是位置错了。后来我在页写函数里强制检查跨页超过就拆分。5.3 电源与干扰问题的处理工业现场电磁环境复杂MRAM 虽然抗干扰能力不错但电源不稳照样出问题。我遇到过电机启停时 MRAM 读写异常的情况后来在电源引脚加了 TVS 管和磁珠问题解决。PCB 布局上MRAM 尽量远离继电器、接触器、变频器这些干扰源。如果实在避不开用屏蔽罩或者把 MRAM 放在板子另一面。SPI 信号线包地处理时钟线尤其要注意。注意MRAM 的数据保持能力虽然强但强磁场环境仍然可能影响数据。如果设备附近有大型电磁铁或电机建议加磁屏蔽或者定期做数据校验和重写。5.4 常见问题速查表问题排查步骤解决方案读不到 ID测电源、测 CS、抓 SPI 波形检查接线和模式配置写入后读出错检查 WREN、检查忙判断加忙等待确认写使能数据随机翻转检查电源纹波、检查干扰源加滤波电容远离干扰源通信距离短检查线长、检查匹配电阻缩短线长加串阻高温下不稳定检查温度范围、检查电源确认工业级器件加强散热6. 性能实测与优化建议6.1 读写速度实测数据我用逻辑分析仪和示波器实测了一组数据SPI 时钟 16MHz单字节读写和批量读写的耗时如下操作类型数据量耗时等效速率单字节写1 字节约 1.2μs0.83 MB/s单字节读1 字节约 0.9μs1.1 MB/s页写256 字节约 140μs1.8 MB/s连续读256 字节约 130μs1.9 MB/s这个速度对于工业数据采集完全够用。如果换成 40MHz 时钟理论速度还能翻倍多但对 PCB 布线要求更高需要评估实际需求。6.2 降低功耗的技巧MRAM 的待机功耗很低但 SPI 时钟频繁翻转也会耗电。如果设备是电池供电可以在不读写时把 CS 拉高SPI 模块关掉需要时再初始化。PIC18F96J94 的 SSPEN 位可以关闭 SPI 模块进一步省电。另外批量读写比单字节读写效率高单位数据的功耗更低。所以能批量就批量别一个字节一个字节地磨。6.3 长期运行的可靠性验证我做过一个连续写入测试每秒写 1000 次每次 32 字节跑了 30 天累计写入约 26 亿次数据零丢失。这个测试虽然不能完全代表实际工况但至少说明 MRAM 的寿命和稳定性是靠谱的。实际部署时建议加一个看门狗和定期自检。自检内容包括读关键参数区、校验 CRC、检查日志区完整性。发现问题及时上报别等设备彻底挂了才发现。7. 项目扩展与个人体会这套 MRAM 加 PIC18F96J94 的方案后来我又用在了几个类似的项目上比如智能电表的数据记录、工业网关的配置存储、医疗设备的参数保存。每次稍微调整一下地址规划和数据结构就能复用省了不少事。如果后续要扩展有几个方向可以考虑。一是换更大容量的 MRAM比如 16Mbit 的型号软件几乎不用改只是地址范围变大。二是加文件系统把 MRAM 当成一个小型磁盘来管理适合存储非结构化数据。三是做双机冗余两颗 MRAM 互为备份进一步提高可靠性。我个人在实际操作中的体会是选对存储介质能省掉后面一大半的麻烦。MRAM 虽然单价高一点但它的“直接写、无限写、掉电不丢”这三个特性在工业场景里太值了。PIC18F96J94 这颗老牌单片机虽然性能不算强但胜在稳定、外设全、工具链成熟跟 MRAM 搭配起来做中小型工业设备的数据存储是个很实在的组合。最后分享一个小技巧调试 SPI 的时候先把时钟降到 1MHz 以下确认通信正常后再逐步提高。这样能快速区分是时序问题还是信号完整性问题。等高速下也稳定了再优化代码效率。别一上来就飙高频出了问题很难定位。