ARTICLE DETAIL

建站实战干货

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

工业级MRAM数据记录方案:PIC18F47K40与MR25H40CDF实战

2026/10/4 13:48:46 拓冰建站 浏览量
工业级MRAM数据记录方案:PIC18F47K40与MR25H40CDF实战 1. 项目缘起与方案选型1.1 为什么要在工业场景里折腾 MRAM 和 PIC工业现场的数据记录有个很尴尬的处境用 EEPROM 吧写入速度慢得让人抓狂擦写寿命也就百万次级别高频记录的场景下没几个月就写废了用 SRAM 加后备电池吧电池在高温高湿环境下两三年就鼓包漏液维护成本高得离谱用 NOR Flash 吧写之前得擦除整个扇区掉电瞬间还容易丢数据关键参数丢了整台设备就得返厂。我前几年做一个电力监测终端现场环境温度夏天能到 70 度电池方案直接放弃Flash 方案又因为掉电丢参数被客户投诉过好几次。后来接触到MRAM这类磁性随机存储器才算找到了一个相对理想的平衡点。它的原理是用磁性隧道结的磁化方向来存储数据不需要电荷保持所以掉电不丢、写入不需要擦除、速度快、寿命近乎无限。这次项目用的MR25H40CDF就是一颗 4Mbit 的 SPI MRAM而主控选了PIC18F47K40一颗 8 位单片机带硬件 MSSP 模块可以跑 SPI工业温度范围也够宽。两者搭配正好覆盖工业嵌入式里“频繁记录、掉电不丢、长期免维护”这类需求。这篇文章我会把整个从选型、硬件连接、SPI 时序配置、读写驱动、掉电保护到现场踩坑的完整过程拆开讲。适合正在做工业数据记录、参数存储、黑匣子类功能的嵌入式工程师也适合刚接触 MRAM 想找个落地案例的朋友。代码基于 MCC 生成的 HAL 风格但思路对裸机寄存器操作同样适用。1.2 MR25H40CDF 到底强在哪和常见方案怎么比先把这颗芯片的关键参数摆出来方便你判断是否适合自己的场景特性MR25H40CDF典型 EEPROM典型 NOR Flash带电池 SRAM容量4Mbit (512KB)通常 1Mbit 以内4Mbit~64Mbit视 SRAM 而定接口SPII2C/SPISPI并口/SPI写入前是否需擦除不需要不需要需要整扇区擦除不需要字节写入时间无内部写周期5ms 级扇区擦除几十 ms纳秒级擦写寿命近乎无限约 100 万次约 10 万次无限掉电保持是无需电池是是否需电池工作温度工业级视型号视型号电池受限从表里能看出来MRAM 最大的价值就是**“SRAM 的速度 Flash 的非易失 无限寿命”**。它没有内部写周期意味着你写完一个字节下一个 SPI 事务可以立刻开始不用像 EEPROM 那样轮询等待“写完成”。这一点在需要连续记录传感器数据的场景里非常关键比如每 1ms 记录一次电流采样值EEPROM 根本扛不住MRAM 却可以轻松应对。注意MRAM 虽然写入不需要擦除但它是按字节可写的没有扇区概念所以软件上不需要做磨损均衡。这一点和 Flash 的思维习惯完全不同别把 Flash 那套搬过来。1.3 PIC18F47K40 作为主控的匹配度分析选 PIC18F47K40 不是随便挑的。它有几个点正好契合这个项目第一内置 MSSP 模块支持 SPI 主模式最高时钟可以到系统时钟的四分之一跑 64MHz 系统时钟时 SPI 能到 16MHz而 MR25H40CDF 的 SPI 最高支持 40MHz完全喂得饱第二它有 128KB Flash 和 3.5KB RAM对于记录类应用够用第三工业级温度范围 -40 到 125 度和 MRAM 匹配第四封装小巧QFN40 适合紧凑的工业板卡。这里有个细节值得说PIC18F47K40 的 MSSP 在 SPI 主模式下片选信号 CS 是需要软件手动控制的硬件不会自动拉低拉高。所以驱动里必须自己管理 CS 引脚每次事务前拉低、事务后拉高。这一点和某些带硬件片选的 MCU 不同写代码时要注意。2. 硬件连接与 SPI 时序配置2.1 引脚连接与 PCB 布局要点MR25H40CDF 是 8 脚封装SPI 接口引脚定义很标准CS、SO、SI、SCK、VCC、GND另外还有 WP 和 HOLD 两个控制脚。实际连接时CS 接 PIC 的任意 GPIO我选的是 RC0方便软件控制SCK 接 MSSP 的 SCK 引脚SI 接 MSSP 的 SDOSO 接 MSSP 的 SDIWP 和 HOLD 直接上拉到 VCC除非你需要硬件写保护功能PCB 布局上SPI 走线尽量短SCK 和 SI/SO 之间不要平行走长线否则高速时容易串扰。我在第一版板子上把 SCK 走线拉到了 8cm结果 16MHz 下读数据偶尔出错后来缩短到 3cm 以内就稳了。另外 CS 走线要远离 SCK因为 CS 是异步信号容易被时钟耦合出毛刺导致误触发。提示MRAM 的 WP 引脚如果悬空内部有弱上拉但工业环境建议外部加 10K 上拉到 VCC避免干扰导致误写保护。2.2 SPI 模式选择与时钟计算MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL0/CPHA0 或 CPOL1/CPHA1。我选的是模式 0因为 PIC 的 MSSP 默认配置就是模式 0省事。模式 0 的含义是时钟空闲为低电平数据在时钟上升沿采样。这个要和从机手册对齐选错了读出来全是 0xFF 或 0x00。时钟频率的计算PIC18F47K40 系统时钟我配置为 64MHzMSSP 的 SPI 时钟由 SSPADD 寄存器分频。公式是Fosc 系统时钟 / (4 * (SSPADD 1))要得到 16MHz 的 SPI 时钟SSPADD 应该设为 0因为 64MHz / 4 16MHz。如果系统时钟是 16MHz那 SSPADD0 时 SPI 只有 4MHz想要更高就得提高系统时钟。实测 16MHz 下 MRAM 读写完全稳定再高没试过因为 PIC 的 SPI 上限就在这。系统时钟SSPADDSPI 时钟是否可用64MHz016MHz稳定64MHz18MHz稳定16MHz04MHz稳定但慢64MHz34MHz稳定2.3 MSSP 初始化代码与关键寄存器初始化 MSSP 的代码不长但每个寄存器都有讲究void SPI1_Initialize(void) { // 关闭 MSSP 再配置 SSP1CON1 0x00; // 时钟极性空闲低电平 SSP1STATbits.CKE 1; // 采样在时钟中间 SSP1STATbits.SMP 0; // SPI 主模式时钟 Fosc/4 SSP1CON1bits.SSPM 0b0000; // 时钟极性低 SSP1CON1bits.CKP 0; // 使能 MSSP SSP1CON1bits.SSPEN 1; // 片选引脚配置为输出 TRISCbits.TRISC0 0; LATCbits.LATC0 1; }这里SSP1STATbits.CKE 1表示在时钟从活跃到空闲时传输数据配合CKP0就是模式 0。SMP0表示在数据输出时间的中间采样这样采样点最稳。SSPM0b0000是 Fosc/4 分频。这些位如果配错SPI 波形就不对用逻辑分析仪一抓就能看出来。注意PIC18F47K40 有两个 MSSP 模块SPI1 和 SPI2别搞混了。我用的是 SPI1对应 RC0/RC3/RC4/RC5 这组引脚。3. 读写驱动实现与核心操作3.1 MRAM 指令集与读写时序MR25H40CDF 的指令集很简洁常用的就几条指令操作码功能WREN0x06写使能WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据读操作的时序是拉低 CS发送 0x03发送 24 位地址因为 512KB 需要 19 位地址但芯片用 24 位地址格式然后连续读出数据最后拉高 CS。写操作类似但写之前必须先发 WREN 指令否则写不进去。这一点和 EEPROM 一样是防误写的机制。地址是 24 位的高 5 位保留实际有效的是低 19 位。发送时按高字节、中字节、低字节的顺序。比如地址 0x000123就发 0x00、0x01、0x23。3.2 单字节读写函数实现先看写使能和写禁止void MRAM_WriteEnable(void) { CS_LOW(); SPI1_ExchangeByte(0x06); CS_HIGH(); } void MRAM_WriteDisable(void) { CS_LOW(); SPI1_ExchangeByte(0x04); CS_HIGH(); }SPI1_ExchangeByte是阻塞式收发一个字节MCC 生成的库里有也可以自己写uint8_t SPI1_ExchangeByte(uint8_t data) { SSP1BUF data; while(!SSP1STATbits.BF); return SSP1BUF; }写字节函数void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); SPI1_ExchangeByte(data); CS_HIGH(); }读字节函数uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; CS_LOW(); SPI1_ExchangeByte(0x03); SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); data SPI1_ExchangeByte(0xFF); CS_HIGH(); return data; }读的时候发 0xFF 是为了产生时钟从机在这期间把数据推出来。这个 0xFF 是“哑数据”内容无所谓只要时钟在跑就行。3.3 页写与连续读的优化单字节写虽然简单但每次都要发 WREN 和完整地址开销大。MRAM 支持连续写也就是发一次地址后连续写多个字节地址自动递增。这个特性在批量记录时非常有用void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); CS_LOW(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((addr 16) 0xFF); SPI1_ExchangeByte((addr 8) 0xFF); SPI1_ExchangeByte(addr 0xFF); for(uint16_t i 0; i len; i) { SPI1_ExchangeByte(buf[i]); } CS_HIGH(); }连续读同理发完地址后连续读 len 个字节即可。实测连续写 256 字节比单字节写 256 次快了一个数量级因为省掉了 256 次 WREN 和地址发送。提示MRAM 没有页边界限制可以跨页连续写地址到 0x7FFFF 后回绕到 0x00000。这一点比 Flash 友好太多Flash 跨页写会回绕到页首覆盖数据。4. 掉电保护与数据完整性设计4.1 为什么 MRAM 还需要掉电保护有人会问MRAM 掉电不丢数据那还需要掉电保护吗答案是需要的但保护的对象不是 MRAM 本身而是写入过程的原子性。假设你正在写一条 16 字节的记录写到第 8 字节时系统掉电了MRAM 里就留下了一条半截记录。下次上电读出来这条记录是无效的如果不做校验程序可能解析出错误数据。所以掉电保护的核心是保证每条记录要么完整写入要么完全没写。常见做法是加一个“有效标志”字节写数据前先清标志写完数据再置标志。上电时检查标志只有标志有效才认为记录完整。4.2 双缓冲与序列号方案更稳妥的做法是双缓冲加序列号。把存储区分成两个区域 A 和 B交替写入。每条记录带一个递增的序列号上电时比较 A 和 B 的序列号取大的那个作为最新数据。这样即使写入过程中掉电另一个区域的数据还是完整的。具体实现typedef struct { uint32_t seq; uint8_t valid; uint8_t data[16]; uint16_t crc; } Record_t;写入时先算 CRC写到非当前区域写完更新序列号再切换当前区域指针。上电时读两个区域校验 CRC 和 valid选序列号大的。方案优点缺点适用场景单区域加标志简单掉电瞬间可能丢当前记录记录频率低双缓冲加序列号掉电不丢可恢复占用双倍空间关键数据日志式追加保留历史需要垃圾回收高频记录4.3 CRC 校验的选型与计算CRC 我选的是 CRC-16/CCITT多项式 0x1021初值 0xFFFF。这个在嵌入式里很常见查表法和逐位法都行。逐位法代码小适合 Flash 紧张的场合uint16_t CRC16_Calc(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for(uint16_t i 0; i len; i) { crc ^ (uint16_t)buf[i] 8; for(uint8_t j 0; j 8; j) { if(crc 0x8000) crc (crc 1) ^ 0x1021; else crc 1; } } return crc; }实测 16 字节数据算一次 CRC 大概几十微秒对记录频率没影响。如果记录频率很高可以换成查表法速度快 5 到 10 倍代价是占用 512 字节 Flash 存表。5. 实操踩坑与问题排查5.1 读出来全是 0xFF 或 0x00 怎么查这是最常见的现象原因通常有三个第一CS 没拉低或者拉低时机不对从机根本没被选中第二SPI 模式配错CPOL/CPHA 和从机不匹配第三SCK 没输出MSSP 没使能或者引脚配置错了。排查顺序先用逻辑分析仪抓 CS、SCK、SI、SO 四根线看 CS 有没有拉低SCK 有没有波形SI 上有没有指令和地址。如果 CS 正常但 SCK 没波形检查 SSPEN 位和 TRIS 配置。如果 SCK 有波形但 SO 一直是高检查 SPI 模式试着把 CPOL 和 CPHA 都翻转一下。我遇到过一次读出来全是 0xFF查了半天发现是 CS 引脚被配置成了模拟功能数字输出被禁用了。PIC18F47K40 的引脚有 ANSEL 寄存器默认有些引脚是模拟模式必须手动设为数字。这个坑很隐蔽因为编译不报错波形上 CS 看起来也动了但电平不对。5.2 写入不生效的几种可能写入不生效读回来还是旧数据常见原因第一忘了发 WRENMRAM 默认是写保护的第二WP 引脚被拉低了硬件写保护生效第三地址算错了写到了别的位置第四CS 拉高的时机太早最后一个字节还没移完就结束了事务。WREN 这个最容易忘因为读操作不需要 WREN写操作需要代码里如果读函数抄过来改的很容易漏掉。我的做法是在写函数里强制先调 WREN不管调用者有没有调过多调一次 WREN 没有副作用。注意WREN 之后如果 CS 拉高写使能会自动清除。所以 WREN 和实际写操作之间不能插入其他 SPI 事务否则 WREN 失效。5.3 高速 SPI 下的数据错误16MHz 下如果出现偶发数据错误先检查 PCB 走线和端接。SPI 在高速下对走线阻抗敏感如果走线长且没有端接电阻信号反射会导致采样错误。可以在 SCK 和 SI 上串 22 到 33 欧姆的电阻抑制反射。另外 CS 拉低到第一个 SCK 沿之间要留足够的时间MRAM 要求至少 5ns实际留 100ns 以上比较稳。如果降速到 8MHz 就正常那基本可以确定是信号完整性问题不是代码问题。这时候要么优化 PCB要么降速使用。工业场景下稳定性优先降速到 8MHz 完全够用没必要为了跑满 16MHz 牺牲可靠性。现象可能原因排查方法解决读全 0xFFCS 未选中/模式错逻辑分析仪抓波形检查 CS 和 CPOL/CPHA读全 0x00SO 未连接/从机未供电万用表测电压检查供电和连线写入无效缺 WREN/WP 拉低读状态寄存器补 WRENWP 上拉偶发错误信号完整性降速测试串阻/缩短走线地址错乱地址字节序错对照手册高字节在前5.4 上电初始化的顺序问题上电时 MRAM 和 MCU 的供电时序如果不一致可能出现 MCU 先启动、MRAM 还没就绪的情况。虽然 MRAM 上电很快但保险起见初始化 SPI 后先读一次状态寄存器确认能通信再开始读写数据。如果读不到延时 10ms 再试最多重试 3 次。另外PIC 的配置位里如果有看门狗初始化时间太长可能被复位。我的做法是在初始化 MRAM 前先喂一次狗初始化完成后再正常喂。这个细节在低功耗应用里尤其重要因为低功耗模式下唤醒后外设就绪时间更长。6. 性能实测与场景延展6.1 实测读写速度数据我用 16MHz SPI 实测了一组数据操作耗时说明单字节写约 3.5us含 WREN 和地址单字节读约 2.8us含地址256 字节连续写约 140us平均 0.55us/字节256 字节连续读约 135us平均 0.53us/字节CRC16 计算 16 字节约 40us逐位法连续写的速度比单字节写快了 6 倍多所以批量记录一定要用连续写。按 256 字节 140us 算理论上每秒能写 1.8MB当然实际受限于 MCU 处理和其他任务但应付工业记录绰绰有余。6.2 工业数据记录器的完整思路基于这套方案可以做一个完整的工业数据记录器定时器每 10ms 触发一次采样采样值存入 RAM 缓冲区缓冲区满 256 字节后连续写入 MRAM。每条记录带时间戳和 CRC双缓冲交替写。上电时扫描两个缓冲区找到最新有效记录恢复现场。这样即使频繁掉电数据也不会丢而且 MRAM 寿命无限不用担心写坏。这个思路可以扩展到很多场景电表参数存储、PLC 配方保存、医疗设备日志、车载黑匣子。核心都是“频繁写、掉电不丢、长期免维护”正好是 MRAM 的强项。6.3 从 MRAM 到其他非易失方案的迁移考虑如果项目对成本敏感MRAM 单价比 EEPROM 高不少可以考虑混合方案关键参数用 MRAM历史日志用 Flash。或者用 FRAM原理类似但接口和容量不同。迁移时主要改驱动层上层的数据结构和 CRC 逻辑可以复用。我个人的经验是先把驱动抽象成统一的nv_read和nv_write接口底层换芯片时只改驱动上层不动这样迁移成本最低。最后分享一个我在实际项目里养成的习惯每次写 MRAM 后立刻读回来比对一次。虽然多花一倍时间但能第一时间发现写入失败避免错误数据被后续逻辑当成有效值。这个习惯帮我抓到过好几次硬件接触不良的问题比等到现场出故障再排查省事得多。