
1. 项目缘起与方案选型为什么是 MRAM 加 PIC18F86K90工业现场的数据记录仪、智能电表、PLC 扩展模块、医疗泵控制器这类设备有一个共同特征它们需要频繁记录关键状态掉电不能丢而且现场环境往往伴随高温、振动和电磁干扰。传统方案里EEPROM 擦写寿命只有百万次量级写入速度慢到毫秒级铁电存储器 FRAM 速度快、寿命高但容量普遍偏小、价格偏高带后备电池的 SRAM 容量大、速度快可电池本身就是个隐患低温失效、高温漏液、长期不换电池导致数据丢失维护成本极高。MR25H40CDF 这类 MRAM 器件正好卡在中间位置。它的核心存储单元是磁性隧道结靠电子自旋方向存储数据没有电荷泄漏问题所以掉电后数据能保持二十年以上不需要任何后备电源。写入不需要先擦除字节级随机写入没有写延迟等待擦写次数在 10^14 量级基本可以理解为“随便写”。SPI 接口40MHz 时钟4Mbit 容量工业级温度范围SOP8 封装这些参数放在一起对于需要“高频写入 掉电保持 免维护”的工业场景来说是一个非常务实的选择。主控这边选 PIC18F86K90理由也很直接。它是 8 位机里外设比较全的一颗自带 MSSP 模块可以配置成 SPI 主模式硬件 SPI 比软件模拟稳定得多尤其是在 40MHz 这种较高时钟下软件翻转 IO 根本跟不上。它还有 4KB 的片上 SRAM 和 128KB Flash足够跑一个轻量的数据记录状态机不需要上 RTOS。工作电压 1.8V 到 5.5V和 MR25H40CDF 的 2.7V 到 3.6V 供电区间有交集3.3V 系统里两者可以直接对接省掉电平转换芯片。我个人的判断是这套组合最适合的场景是写入频率高每秒几次到几十次、单次写入数据量不大几十字节到几百字节、要求掉电后数据完整、设备部署后基本不拆开维护。如果你做的是消费类产品成本敏感那 EEPROM 或者 Flash 更合适但如果是工业设备返修一次的人工成本就超过芯片差价了MRAM 的溢价是值得的。提示MR25H40CDF 是 4Mbit 容量换算成字节是 512KB。注意区分 bit 和 Byte很多新手看手册时会把 4Mbit 当成 4MB实际可用空间只有 512KB规划存储结构时要按 512KB 来算。2. 硬件连接与 SPI 时序关键细节2.1 引脚连接与片选处理MR25H40CDF 是标准 SPI 从器件引脚定义很清晰SCK、SIMOSI、SOMISO、CS#、VDD、VSS加上两个写保护相关的引脚。PIC18F86K90 这边用 MSSP 模块的 SPI 主模式映射到 RC3SCK、RC5SDO、RC4SDI片选用一个普通 GPIO 控制。片选这块我要多说一句。很多教程图省事直接把 CS# 接地让器件一直选中。单器件系统里这样确实能跑但有两个隐患一是上电瞬间 SCK 线上如果有毛刺器件可能误触发二是后续如果要加第二个 SPI 从器件就得改板子。所以我的习惯是始终用 GPIO 控制片选初始化时先拉高通信前拉低通信完拉高养成习惯。// PIC18F86K90 片选引脚定义假设用 RB0 #define MRAM_CS_LAT LATBbits.LATB0 #define MRAM_CS_TRIS TRISBbits.TRISB0 void MRAM_CS_Init(void) { MRAM_CS_TRIS 0; // 输出 MRAM_CS_LAT 1; // 默认拉高不选中 }写保护引脚 WC# 建议接 GPIO 或者直接上拉。如果接 GPIO可以在固件里做一层保护只有在明确要写入时才拉低 WC#其他时间保持高电平防止程序跑飞时误写数据。这个细节在工业设备里很重要我见过因为没做写保护程序异常后把整个存储区写乱的案例。2.2 SPI 模式选择与时钟计算MR25H40CDF 支持 SPI 模式 0 和模式 3也就是 CPOL0/CPHA0 或者 CPOL1/CPHA1。PIC18F86K90 的 MSSP 模块配置成 SPI 主模式时通过 SSPCON1 寄存器的 CKP 位和 SSPSTAT 的 CKE 位来设置。我一般用模式 0因为大多数逻辑分析仪和调试工具默认就是模式 0抓波形方便。时钟频率的计算要结合 PIC18F86K90 的系统时钟。假设系统时钟 Fosc 是 64MHzMSSP 的 SPI 时钟来源是 Fosc/4也就是 16MHz然后通过 SSPADD 或者 SSPCON1 的分频位再分频。MR25H40CDF 最高支持 40MHz所以 16MHz 完全在范围内不需要额外分频。void SPI_Init(void) { // 配置 MSSP 为 SPI 主模式时钟 Fosc/4 SSPCON1 0x00; SSPSTAT 0x00; // 模式 0CKP0, CKE0 SSPCON1bits.SSPM 0b0000; // SPI Master, Fosc/4 SSPCON1bits.CKP 0; // 空闲低电平 SSPSTATbits.CKE 0; // 数据在时钟上升沿采样 TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入 SSPCON1bits.SSPEN 1; // 使能 MSSP }这里有个容易踩的坑PIC18F86K90 的 MSSP 在 SPI 主模式下写 SSPBUF 之后要等 BF 位SSPSTATbits.BF或者 SSPIF 中断标志才能读接收到的数据。如果不等标志位直接读读到的可能是上一次的残留值。我在早期调试时因为这个原因读回来的状态寄存器值总是差一位排查了半天才发现是时序没等够。2.3 上电初始化与器件识别MRAM 上电后不需要像 Flash 那样等待就绪但稳妥起见初始化流程里还是加一段延时然后读器件 ID 确认通信正常。MR25H40CDF 的器件 ID 读取命令是 0x9F返回 3 个字节第一个是制造商 ID后面是产品 ID。uint8_t MRAM_ReadID(uint8_t *id_buf) { MRAM_CS_LAT 0; SPI_Transfer(0x9F); // 读 ID 命令 id_buf[0] SPI_Transfer(0xFF); id_buf[1] SPI_Transfer(0xFF); id_buf[2] SPI_Transfer(0xFF); MRAM_CS_LAT 1; return 0; }实测下来制造商 ID 应该是 0x0E产品 ID 的高字节是 0x22 左右。如果读回来全是 0xFF 或者 0x00基本可以判断是接线问题或者片选没拉低。这个 ID 检查步骤建议写进产品自检流程设备出厂前跑一遍能挡掉不少焊接不良的板子。3. 存储结构设计与读写实现3.1 地址空间规划512KB 的空间看着不大但如果没有规划写到后面会乱成一锅粥。我的做法是把它分成几个固定区域头部信息区、配置参数区、循环记录区、故障快照区。区域起始地址大小用途头部信息区0x000004KB设备序列号、固件版本、存储格式版本配置参数区0x010004KB校准系数、报警阈值、通信参数循环记录区0x02000480KB按时间顺序循环写入的运行数据故障快照区0x7A00024KB故障发生前后的关键变量快照头部信息区放存储格式版本号这个设计是我踩过坑之后加的。早期产品升级固件时改了数据结构结果旧数据读出来全是乱的现场设备又没法返厂格式化。后来加了版本号固件启动时先读版本不匹配就走兼容解析或者提示需要迁移问题就解决了。循环记录区的设计要点是“环形缓冲 序号标记”。每条记录固定长度比如 64 字节包含一个递增的序号、时间戳、数据载荷和 CRC 校验。写满之后回到区域起始位置覆盖最旧的记录。读取时通过序号判断哪些是最新数据不需要额外的指针管理。3.2 单条记录的写入流程MRAM 的写入命令是 0x02后面跟 24 位地址然后连续写入数据。地址是 3 字节高位在前。写完之后 CS# 拉高数据就真正写入了不需要等待写周期这是 MRAM 相比 EEPROM 最大的优势。void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len) { MRAM_CS_LAT 0; SPI_Transfer(0x02); // 写命令 SPI_Transfer((addr 16) 0xFF); // 地址高字节 SPI_Transfer((addr 8) 0xFF); // 地址中字节 SPI_Transfer(addr 0xFF); // 地址低字节 for (uint16_t i 0; i len; i) { SPI_Transfer(data[i]); } MRAM_CS_LAT 1; }这里有个细节MRAM 支持跨页写入不像 EEPROM 那样有页边界限制。EEPROM 写跨页时会回卷到页首导致数据写错位置MRAM 没有这个问题连续写多少字节都行只要不超出芯片容量。这个特性简化了驱动代码不需要做页对齐处理。读取命令是 0x03格式和写入一样只是数据方向反过来。void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LAT 0; SPI_Transfer(0x03); 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); } MRAM_CS_LAT 1; }3.3 数据完整性保障工业设备最怕的是“写了一半掉电”记录区里出现半条脏数据。我的做法是每条记录末尾加两个字节的 CRC16读取时先校验 CRC不通过就跳过这条记录。同时记录头里放一个“有效标志”写入时先写数据区最后写标志字节这样即使掉电标志字节没写成功这条记录就被视为无效不会污染数据链。CRC16 的计算用查表法在 PIC18F86K90 上跑一次 64 字节的 CRC 大概几十微秒对整体性能影响可以忽略。查表放在 Flash 里占 512 字节空间换来的是计算速度提升好几倍。uint16_t CRC16_Calc(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc (crc 8) ^ crc16_table[(crc ^ data[i]) 0xFF]; } return crc; }注意CRC 校验的多项式要固定不要中途换。我见过一个项目两个工程师分别写了写入端和读取端的 CRC 代码用的多项式不一样结果数据死活读不出来查了两天才发现是这个问题。建议把 CRC 算法封装成一个公共函数两边都调用同一个。4. 性能实测与优化经验4.1 写入速度实测在 16MHz SPI 时钟下我实测连续写入 64 字节记录从拉低 CS# 到拉高 CS#示波器抓下来大约 36 微秒。其中命令和地址占 4 字节数据 64 字节总共 68 字节68 × 8 / 16MHz 34 微秒加上片选和函数调用开销36 微秒是合理的。对比一下 EEPROM同样 64 字节EEPROM 需要先发写命令然后等待 5ms 左右的写周期期间芯片不响应任何命令。也就是说 EEPROM 写一次要 5msMRAM 只要 36 微秒差了将近 140 倍。如果设备每秒记录 10 次EEPROM 方案里 50ms 都花在等待写完成上CPU 什么都干不了MRAM 方案里这 360 微秒几乎无感。4.2 读取速度与随机访问读取速度更快因为没有写周期等待。16MHz 时钟下读 64 字节大约 34 微秒。MRAM 的随机访问延迟是固定的不像 Flash 有读取缓存命中率的问题读任何地址都是同样的速度。这个特性对于需要频繁查询历史数据的应用很友好。我做过一个测试在 480KB 循环记录区里随机读 1000 次每次读 64 字节总耗时大约 34 毫秒平均每次 34 微秒和连续读取没有明显差异。这说明 MRAM 的随机访问性能确实稳定。4.3 功耗表现MR25H40CDF 的写入电流典型值在 10mA 左右待机电流只有几十微安。对于电池供电或者能量采集供电的设备这个功耗水平是可以接受的。写入 36 微秒消耗的能量大约是 10mA × 3.3V × 36μs 1.2μJ非常小。如果设备用超级电容供电掉电后需要把关键数据紧急写入 MRAM这个能量预算完全够用。我算过一笔账一个 0.1F 的超级电容从 3.3V 放到 2.7V释放的能量是 0.5 × 0.1 × (3.3² - 2.7²) 0.18J足够写几万次 64 字节记录。所以掉电保护方案里MRAM 是比 EEPROM 更省心的选择。5. 常见问题排查与避坑指南5.1 通信失败排查流程调试 SPI 器件最怕的是“什么都读不到”。我整理了一个排查顺序按这个走基本能定位问题现象可能原因排查方法读回全 0xFFMISO 没接好或片选没拉低示波器看 CS# 和 MISO 波形读回全 0x00器件没供电或时钟没输出万用表量 VDD示波器看 SCK读回数据错位SPI 模式不对切换模式 0 和模式 3 对比偶发读写失败片选时序太紧在 CS# 拉低后加 1μs 延时高温下不稳定时钟太快或电源纹波大降时钟到 8MHz 测试我遇到最多的是第一种和第四种。第一种通常是焊接问题SOP8 封装引脚间距小手工焊接容易虚焊。第四种是片选和时钟的建立时间不够MRAM 要求 CS# 拉低到第一个时钟沿至少有 5ns 的建立时间一般 GPIO 操作都能满足但如果用中断里直接操作寄存器可能会太快加个 NOP 或者微秒延时就能解决。5.2 数据异常与恢复如果发现读出来的数据 CRC 校验不通过先不要急着格式化。我的处理流程是先统计整个记录区里有效记录的数量和分布判断是局部损坏还是大面积损坏。局部损坏通常是掉电时机不巧那条记录作废就行大面积损坏可能是电源问题或者程序跑飞需要检查硬件和固件。有个技巧在头部信息区里存一个“写入次数计数器”每次设备启动时加一。如果发现计数器异常增长说明设备在频繁复位可能是电源不稳或者看门狗误触发。这个计数器我一般放在 MRAM 里因为它写入无损耗放 EEPROM 里反而担心写坏。5.3 与 PIC18F86K90 配合的注意事项PIC18F86K90 的 MSSP 模块在 SPI 主模式下如果连续发送数据中间不能有太长的间隔否则从器件可能认为通信结束。我在写批量数据时每发一个字节都等 BF 标志确保 SSPBUF 空了再发下一个这样虽然慢一点但稳定。如果追求速度可以用中断或者 DMA但 PIC18F86K90 没有 DMA只能用中断方式代码复杂度会上升。另外PIC18F86K90 的 IO 口驱动能力有限如果 SPI 走线较长超过 10cm建议在 SCK 和 MOSI 上串 22Ω 到 33Ω 的电阻减少反射和过冲。我在一个工控板上没加这个电阻SPI 时钟跑到 16MHz 时波形振铃严重读数据偶发错误加了 33Ω 电阻后波形干净了问题消失。6. 固件架构与状态机设计6.1 分层结构固件我分成三层硬件抽象层、MRAM 驱动层、应用逻辑层。硬件抽象层封装 SPI 收发和片选操作驱动层实现读写擦和 CRC 校验应用层负责记录管理、掉电检测和数据恢复。这样分层的好处是如果以后换主控或者换存储芯片只需要改硬件抽象层和驱动层应用层代码基本不动。// 硬件抽象层接口 void HAL_SPI_Init(void); uint8_t HAL_SPI_Transfer(uint8_t data); void HAL_CS_Set(uint8_t level); // 驱动层接口 void MRAM_Init(void); void MRAM_Write(uint32_t addr, uint8_t *data, uint16_t len); void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len); uint16_t MRAM_GetRecordCount(void); // 应用层接口 void Record_Write(uint8_t *payload, uint16_t len); uint8_t Record_ReadLatest(uint8_t *buf, uint16_t len); void Record_Format(void);6.2 掉电检测与紧急写入PIC18F86K90 有内置的欠压复位模块可以配置成在电源电压降到阈值时触发中断。我在中断里做紧急写入把当前未保存的关键变量打包写到故障快照区然后进入死循环等待电源彻底掉下去。这个过程要在电源电容放电到芯片最低工作电压之前完成所以代码要尽量精简不能有浮点运算和复杂逻辑。实测下来3.3V 系统里用 100μF 电容从检测到掉电到电压降到 2.7V大约有 2ms 的时间窗口。写 64 字节记录只要 36μs加上打包和 CRC 计算总共不超过 200μs时间非常充裕。6.3 上电恢复流程上电后第一件事是读头部信息区的版本号和设备序列号确认存储格式匹配。然后扫描循环记录区找到最新的有效记录序号从那里继续写。扫描过程不需要读全部数据只需要读每条记录的头部几个字节判断有效标志和序号这样 480KB 区域扫描一遍也就几毫秒。如果发现记录区全空或者全无效说明是首次使用或者存储被擦除了这时候走格式化流程重建头部信息和记录区结构。格式化不是真的擦除所有数据只是重置头部信息区的计数器和指针旧数据留着不管后续写入会自然覆盖。7. 实际项目中的经验沉淀7.1 关于 MRAM 的选型建议MR25H40CDF 是 4Mbit 容量如果项目需要更大容量同系列还有 8Mbit 和 16Mbit 的型号引脚兼容驱动代码只需要改地址位数。如果对成本敏感可以选容量小一点的型号比如 1Mbit 的 MR25H10价格会低一些。选型时要注意温度等级。工业级是 -40°C 到 85°C汽车级是 -40°C 到 125°C。如果设备部署在户外或者靠近热源建议选汽车级虽然贵一点但可靠性有保障。我有个项目用在锅炉房附近环境温度经常到 70°C 以上用工业级芯片跑了两年没出问题但余量确实不大。7.2 关于 SPI 布线的经验SPI 是高速信号布线要注意几点SCK 和 MOSI 尽量等长减少时序偏差走线远离模拟信号和电源开关节点避免耦合噪声如果板上有多片 SPI 器件片选线要分开走不要共用。我见过一个板子把两个 SPI 器件的片选接在一起结果两个器件同时被选中数据冲突读回来全是乱的。7.3 关于固件升级的考虑如果设备支持固件升级存储格式的兼容性要提前考虑。我的做法是在头部信息区预留一个“格式版本”字段每次升级固件时检查这个字段如果版本不匹配走数据迁移流程。迁移流程可以很简单读旧格式数据转换成新格式写到新区域然后更新版本号。这个过程可以在设备启动时自动完成不需要人工干预。提示数据迁移过程中如果掉电可能导致新旧数据都不完整。稳妥的做法是迁移前先在 MRAM 里写一个“迁移中”标志迁移完成后清除。如果启动时发现这个标志还在说明上次迁移没完成需要重新迁移或者回滚。7.4 关于测试覆盖嵌入式存储的测试不能只测正常读写要覆盖边界条件写满整个区域、跨页写入、掉电中断、高温低温、电压波动。我一般会写一个测试固件自动跑这些场景记录每次的结果。测试固件里加一个“错误注入”功能随机在写入过程中复位模拟掉电然后检查数据完整性。这个测试跑一晚上基本能把大部分隐患暴露出来。8. 写在最后的一些实操体会这套 MRAM 加 PIC18F86K90 的方案我在三个项目里实际用过累计出货几千台现场返修率很低。最让我满意的是它的“无感写入”——应用层调用写记录函数底层 36 微秒就完成了CPU 几乎不用等待程序逻辑可以写得很顺。相比之下之前用 EEPROM 的方案每次写都要等 5ms程序里到处是状态机和延时代码复杂度高很多。踩过的坑里最深刻的一次是片选时序问题。当时用了一个新批次的 MRAM发现偶发读写失败概率大概千分之一。查了很久最后用示波器抓波形发现 CS# 拉低到 SCK 第一个上升沿之间只有 2ns而手册要求最小 5ns。原因是那批 MRAM 的输入建立时间比之前批次略慢加上 PCB 走线延迟刚好卡在边界上。在片选拉低后加了一个 NOP 延时问题就解决了。这件事让我养成了一个习惯所有 SPI 器件的片选操作后面都加一点延时宁可慢一点也要稳。另一个体会是MRAM 虽然写入寿命几乎无限但也不是可以随便乱写。如果程序有 bug在循环里疯狂写同一个地址虽然芯片不会坏但数据会被覆盖得乱七八糟。所以我在驱动层加了一个简单的写入频率限制同一个地址在 1ms 内只允许写一次超过就丢弃并记录错误。这个保护机制在实际运行中触发过几次帮我发现了应用层的逻辑错误。最后分享一个小技巧MRAM 的头部信息区可以存一份“出厂默认配置”的副本设备恢复出厂设置时直接从副本拷贝到配置区不需要固件里硬编码默认值。这样如果默认配置需要调整只需要在出厂前更新副本不用重新编译固件。这个做法在需要频繁调整参数的场景里特别省事。