尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PIC18F45K80搭配MR25H40CDF:工业嵌入式高频写入与掉电保存的MRAM实践

PIC18F45K80搭配MR25H40CDF:工业嵌入式高频写入与掉电保存的MRAM实践 在嵌入式行业做了这么多年我养成了一个习惯拿到一个新项目先问一句数据要往哪里放。工业设备不比消费类产品掉电丢参数、记录写坏、存储寿命到期导致整机罢工每一类故障的返修代价都让人肉疼。前阵子做现场数据采集终端主控用了 PIC18F45K80存储芯片经过几轮对比定下 MR25H40CDF 这颗 4Mbit SPI MRAM。在工业和嵌入式应用里这套组合解决的核心问题就是既要像 SRAM 一样随手写又要在掉电后把数据原样留住。这篇文章从选型开始把硬件接线、驱动源码、掉电保存策略和实际踩过的坑一次说清楚。1. 存储芯片选型的权衡同样是掉电不丢MRAM 为什么值得占一个坑1.1 数据记录需求变了老存储方案开始吃力做工业设备最常遇到的存储需求无非三类保存校准参数、记录运行日志、缓存现场数据。早年间用 I2C EEPROM比如 AT24C256一颗芯片解决大多数问题。但项目一旦要求每秒钟记录一次设备状态连续记录几个月EEPROM 的短板立刻暴露写入慢、容量小、寿命有限。Atmel 时代主流 EEPROM 的标称擦写寿命在 100 万次左右听着不少可如果系统每秒写一次每天就是 86400 次一颗芯片 12 天就被写透了。就算用分页轮询把写入摊到多个地址也只是把报废日期往后拖并不能从根本上解决问题。于是很多人转向 NOR Flash比如 W25Q64。Flash 容量大、价格低但引入了另一个麻烦擦除操作。NOR Flash 写数据前必须先擦除扇区而擦除的最小单位通常是 4KB 甚至 64KB擦除一次要几十到几百毫秒。如果系统正在记录关键数据时突然掉电恰好处于擦除流程中间那一整块数据就废了。更麻烦的是Flash 的写寿命通常只有 10 万次左右比 EEPROM 还低原因是擦除本身对浮栅晶体管有物理损耗。很多工程师在项目里硬啃 Flash 的坏块管理和磨损均衡把简单问题复杂化。1.2 一张表看清四种非易失存储的实际差异我自己在选型时会把候选方案分成四类摆在一起对比EEPROM、NOR Flash、FRAM、MRAM。用下面这张表可以看得很清楚对比项EEPROMNOR FlashFRAMMRAMMR25H40CDF写前擦除不需要必须按扇区擦除不需要不需要典型写延迟字节级 5ms 左右页编程数 ms 扇区擦除数百 ms写入即时完成写入即时完成常见写寿命10^6 次10^5 次10^10~10^12 次10^14 次以上数据保持10 年以上10~20 年10 年左右高温衰减明显20 年以上单位容量成本高低高高于 Flash与 FRAM 接近使用复杂度低高需管理擦除/坏块低低关键差异在写入是否即时完成和是否要先擦除。FRAM 和 MRAM 都是真正的随机读写存储器没有写放大、没有擦除等待。FRAM 的问题是目前大容量选择少超过 1Mbit 的型号不常见而且铁电材料在高温下的数据保持能力随时间退化工业现场温度经常超过 70°C这个隐患让我不太放心。MRAM 用的是磁性隧道结数据保持靠磁矩方向而不是电荷高温特性更稳。1.3 什么时候该选 MRAM按写频率和可靠性预算来决策我的经验是如果系统里存在高频写入且掉电丢失会造成事故的数据优先考虑 MRAM。举一个实际例子某个伺服驱动器的运行状态记录需要每 100ms 写一次 32 字节一天就是 864000 次写入。用 EEPROM 的话几小时就耗尽寿命用 Flash 要不停做扇区擦除擦除间隔还没算完时序就崩了。MRAM 完全没有这些问题写入就是普通 SPI 传输没有编程时间概念寿命几乎可以认为是无限的。价格上 MRAM 确实比同容量 NOR Flash 贵但工业设备对物料成本的敏感度远低于消费电子。一块电路板因为存储故障返修差旅、停工、人工成本加起来足够买几百颗 MRAM。这个逻辑想清楚了选型就没什么好纠结的。2. 看透 MR25H40CDF4Mbit 的 SPI MRAM 到底提供哪些能力2.1 容量结构与电气参数速查MR25H40CDF 是 Everspin 的串行 MRAM容量 4Mbit换算下来是 512KB。它内部按 64 字节为一页全片一共 8192 页。和 EEPROM 类似的页缓冲结构但写入时不需要发页编程指令直接发 WRITE 命令连续输入最多 64 字节即可。供电范围 2.7~3.6V和 PIC18F45K80 的 3.3V 电源轨完美匹配。SPI 时钟最高支持 40MHz工业级温度范围能覆盖绝大多数现场环境的 -40~85°C。这里有个容易搞错的点MRAM 虽然叫RAM但它不是易失的掉电后数据不会丢。你可以把它理解为掉电不丢数据的 SRAM写入速度比 EEPROM 和 Flash 快好几个数量级。同时它也不需要像 SRAM 那样靠电池备份省掉了电池和充放电管理电路。2.2 六个控制引脚的正确用法MR25H40CDF 是标准 SOP-8 封装引脚非常少但每一个都有讲究引脚方向作用注意事项CS#输入片选低有效所有命令都以 CS# 下降沿开始上升沿结束SCK输入SPI 时钟最高 40MHz空闲状态由 SPI 模式决定SI输入串行数据输入MCU 的 SDO 接这里SO输出串行数据输出MCU 的 SDI 接这里WP#输入硬件写保护低电平时禁止写使能 WREN不能悬空HOLD#输入总线暂停低电平时暂停通讯不能悬空工程中最常见的坑就是 WP# 和 HOLD# 悬空。MRAM 内部没有在这两个引脚上做上拉悬空会导致引脚电平受噪声干扰随机漂移表现为写操作偶发失败。最稳妥的做法是用 10kΩ 电阻把两个引脚都上拉到 VDD。如果确实想用硬件写保护再把 WP# 接到 GPIO 上控制平时拉高需要锁存数据时拉低。2.3 指令集比 Flash 简单得多MR25H40CDF 的指令集和普通 SPI EEPROM/Flash 类似但没有擦除指令也不需要发 0xD8 之类的扇区擦除命令。常用指令就这几个指令操作码功能WREN0x06写使能设置 WEL 锁存位WRDI0x04写禁止清除 WEL 锁存位RDSR0x05读状态寄存器WRSR0x01写状态寄存器用于配置块保护READ0x03从指定地址连续读取WRITE0x02从指定地址连续写入最长 64 字节写数据前必须发 WREN这和 EEPROM 一样。但有一个本质区别MRAM 的数据写入不需要等内部编程周期。WRITE 指令最后一个字节在 CS# 上升沿锁存后数据已经完成写入。因此驱动里不需要像 Flash 那样轮询 WIP 位有些资料把状态寄存器 bit0 标作 WIP但对 MRAM 来说它没有实际意义轮询结果永远是完成状态也不需要等待毫秒级的 tW。这个特性在做掉电保存时非常关键后面我会专门算时间账。3. 硬件接线与 MSSP 配置从引脚映射到 SPI 模式 0 的匹配3.1 一张接线表搞定硬件连接PIC18F45K80 自带 MSSP 模块可以配置成 SPI 主模式。我的接线方案是这样的MR25H40CDF 引脚接 PIC18F45K80 引脚说明CS#RC2普通 GPIO软件控制片选不使用硬件 SS 引脚SCKRC3SCK1SPI 时钟输出SIRC5SDO1主发从收SORC4SDI1主收从发WP#VDD经 10kΩ不启用硬件写保护HOLD#VDD经 10kΩ不启用暂停功能VDD3.3V靠近引脚放 100nF 去耦电容VSSGND与 MCU 共地CS# 用普通 GPIO 而不是 MSSP 的 SS 引脚这是刻意为之。硬件 SS 引脚在某些模式下会自动控制片选反而容易和软件逻辑打架。用 GPIO 软件拉低拉高时序完全可控这是嵌入式项目里最稳的做法。我把 CS# 放在 RC2 上接线时注意不要和邻近引脚短路这个位置在 PCB 布局上走线最短。3.2 为什么 SPI Mode 0 最合适MR25H40CDF 数据手册明确支持 SPI Mode 0 和 Mode 3也就是 CPOL0/CPHA0 或 CPOL1/CPHA1。这两个模式都是上升沿锁存输入数据下降沿更新输出数据和这颗芯片的内部逻辑完全匹配。我习惯用 Mode 0也就是空闲时 SCK 为低数据在 SCK 上升沿被锁存。这样 MRAM 输出数据时主控可以在 SCK 下降沿采样时序比较接近常见 SPI 存储器的默认行为。如果非要用 Mode 3也不是不行但要注意 PIC 的 MSSP 配置里 CKP 和 CKE 的位定义和教科书上略有差异容易搞混。用 MCC 或 MPLAB Code Configurator 生成初始化代码时直接选 SPI Mode 0自动生成的寄存器配置最不容易出错。3.3 MSSP 初始化代码与分频选择我的系统里 PIC18F45K80 跑在 64MHz16MHz 晶振 4 倍 PLLMSSP 主模式最快可以到 Fosc/4也就是 16MHz SPI 时钟。MRAM 支持 40MHz实际上限不在 MRAM而在 MCU 的外设分频。 64MHz 下选择 Fosc/4 就是 16MHz一页 64 字节的传输时间约为 (68 字节 × 8 位) / 16MHz ≈ 34μs完全够用。如果对信号完整性有顾虑可以降一档用 Fosc/16 也就是 4MHz普通杜邦线连接也能稳定工作。初始化代码片段如下void MRAM_SPI_Init(void) { // MSSP1 作为 SPI 主机Fosc/4CKP0SPI Mode 0 SSP1CON1 0x24; // SSPEN1, SSPM0100 (Master Fosc/4), CKP0 SSP1STAT 0x40; // SMP0, CKE1对应 SPI Mode 0 TRISCbits.TRISC2 0; // CS# 输出 TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入 }在使用 MPLAB Code Configurator 时我建议直接生成 SPI 外设配置再把 CS# 单独作为 GPIO 管理。MCC 生成的初始化代码里会自动处理寄存器位比自己手算 CKP/CKE 组合省心很多。需要注意如果同时使用看门狗或低功耗休眠唤醒后要重新确认 MSSP 模块仍处于使能状态避免 SPI 静默失效。4. 驱动实现从底层收发字节到页写入的跨页处理4.1 底层单字节收发函数SPI 是同步全双工通信读和写其实是同一个动作主控发一个字节同时从机返回一个字节。PIC18F45K80 的 MSSP 模块用一个 SSP1BUF 寄存器完成收发代码非常简单uint8_t SPI1_ExchangeByte(uint8_t byte) { while (SSP1STATbits.BF); // 等上次接收完成BF 置 1 表示 SSP1BUF 有数据 SSP1BUF byte; // 写入发送数据 while (!SSP1STATbits.BF); // 等待本次传输完成 return SSP1BUF; // 读取接收数据同时清除 BF }如果 SPI 总线速度很快发送频率很高偶尔会遇到写冲突标志 WCOL。出现 WCOL 说明上次传输还没结束就往 SSP1BUF 里写新数据驱动里可以在发送前清一下 WCOL或者像我一样在调试阶段打印一次告警生产代码里保持这个函数足够简单即可。4.2 写使能和状态查询MRAM 写入前必须发 WREN 指令CS# 从低到高的过程会把 WEL 锁存位置 1。每次成功写入后WEL 位会自动清零所以连续写多个页时每个页写入前都要重新发一次 WREN。这是很多从 EEPROM 转过来的工程师会忽略的细节。void MRAM_WriteEnable(void) { MRAM_CS_L(); SPI1_ExchangeByte(0x06); // WREN MRAM_CS_H(); } uint8_t MRAM_ReadStatus(void) { uint8_t sr; MRAM_CS_L(); SPI1_ExchangeByte(0x05); // RDSR sr SPI1_ExchangeByte(0x00); MRAM_CS_H(); return sr; }初始化时我会读一次状态寄存器确认块保护位处于默认状态。如果读到保护位不为 0就发 WREN WRSR(0x01) 指令把保护位清掉。这个检查在量产时很有必要某些批次的芯片默认状态可能存在差异。4.3 页写入的完整代码MR25H40CDF 的 WRITE 指令格式是0x02 24 位地址 数据。地址分三字节发送A23:A19 实际用不到但仍然要发完整三字节高字节填 0 即可。下面这段代码处理了页边界是工程上最关键的部分void MRAM_Write_Bytes(uint32_t addr, const uint8_t *data, uint16_t len) { uint16_t cnt; uint16_t i; while (len 0) { // 当前页剩余可写字节数 cnt 64 - (addr 0x3F); if (cnt len) cnt (uint16_t)len; MRAM_WriteEnable(); MRAM_CS_L(); SPI1_ExchangeByte(0x02); // WRITE SPI1_ExchangeByte((uint8_t)(addr 16)); // A23:A16 SPI1_ExchangeByte((uint8_t)(addr 8)); // A15:A8 SPI1_ExchangeByte((uint8_t)(addr)); // A7:A0 for (i 0; i cnt; i) { SPI1_ExchangeByte(data[i]); } MRAM_CS_H(); // 上升沿锁存本次写入 addr cnt; data cnt; len - cnt; } }每次 WRITE 最多 64 字节如果 len 超过当前页剩余空间就必须拆成多次写。MRAM 的地址计数器在页内是连续递增的到了页尾不会自动翻到下一页而是回卷到页首。如果你向一个包含 64 字节的页尾部写入超过剩余长度的数据多余部分会被写到页首覆盖旧数据这是和 Flash 完全不同的行为必须用上面的代码主动切页。4.4 连续读也要注意页边界吗READ 指令的地址也会在页内递增超过页尾同样回卷。因此需要跨页读取一段连续数据时也要按页边界拆分否则会读到不该读的数据。读取代码和写入代码思路一致void MRAM_Read_Bytes(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t cnt; uint16_t i; while (len 0) { cnt 64 - (addr 0x3F); if (cnt len) cnt (uint16_t)len; MRAM_CS_L(); SPI1_ExchangeByte(0x03); // READ SPI1_ExchangeByte((uint8_t)(addr 16)); SPI1_ExchangeByte((uint8_t)(addr 8)); SPI1_ExchangeByte((uint8_t)(addr)); for (i 0; i cnt; i) { data[i] SPI1_ExchangeByte(0x00); // 发空字节收从机数据 } MRAM_CS_H(); addr cnt; data cnt; len - cnt; } }这段代码没做任何长度上限检查如果 len 超过 64KBuint16_t 会溢出。实际项目里我会把长度参数也改成 uint32_t或者在外面限制单次读写的最大长度。这里为了示例清晰没有展开工程代码一定要加上边界保护。5. 工业场景里的存储策略掉电保存、日志槽与 CRC 校验5.1 掉电保存的时间预算MRAM 的写入速度是救命稻草工业现场最经典的应用场景是掉电时保存当前运行状态。系统从检测到掉电到主电源彻底跌落之间通常有几十毫秒到几百毫秒的时间窗口具体取决于电源电容大小和负载电流。传统 EEPROM 在 5ms 一个字节的速度下几十毫秒只能存十几个字节还要撞上页写和内部编程时间常常存不完。MRAM 完全不同一次 64 字节的页写在 8MHz SPI 下只要 80μs 左右就算加上写使能和函数调用开销写 10 页也才 1ms 的量级。这个时间在掉电窗口里几乎可以忽略不计真正要优化的是 MCU 检测掉电的速度和进入保存流程的软件开销。我的做法是把需要保存的运行参数始终缓存在 RAM 里掉电中断到来后直接批量写 MRAM。PIC18F45K80 的 BOR 掉电复位检测可以配置成中断触发也可以在电源入口放一个分压比较器拉低外部中断引脚。保存完成后再让 MCU 进入休眠或等待复位。要注意保存过程尽量关中断避免嵌套干扰 SPI 时序特别是严禁在写 MRAM 过程中再进掉电中断。5.2 64 字节日志槽一行一页天然对齐连续数据记录场景里MRAM 的 64 字节页是个非常趁手的设计。把一条日志正好设计成 64 字节一条记录占一页地址天然对齐不需要处理跨页。我的日志结构体长这样typedef struct { uint32_t seq; // 序列号单调递增 uint16_t crc; // CRC16校验 payload uint8_t flags; // 状态标志 uint8_t payload[57]; // 实际业务数据 } LOG_ENTRY;结构体总共 64 字节和 MRAM 页大小严格对齐。写入时先填好 payload 和 crc再一次页写落盘。读取时先从页头读 8 个字节拿到 seq 和 crc如果 CRC 不对说明这条日志是写了一半断电产生的脏数据直接丢弃回退到上一条有效记录。这个设计解决了掉电一致性问题。MRAM 单页写入本身是原子的64 字节在一次 CS# 上升沿锁存不会出现半个页的问题。但如果你一次写多条记录写了一半掉电就需要靠 seq 和 CRC 判断哪条记录是完整的。seq 单调递增还有一个好处启动时读取日志最新的有效记录就是 seq 最大的那条不需要维护额外索引。5.3 参数镜像双槽备份与版本校验运行参数比日志更关键我会采用双槽镜像策略。同一份参数写两个地址区每个区开头放固定魔数、参数版本号、参数数据和 CRC。写入时先写 A 区再写 B 区掉电如果打断在 A 区写入B 区还是完整的旧版本上电后比较两个区的 CRC 和版本号选择有效的那份加载。typedef struct { uint32_t magic; // 固定魔数比如 0xA5A5A5A5 uint8_t version; // 参数版本号 uint8_t reserved[3]; uint8_t data[52]; // 业务参数 uint16_t crc; // CRC16 } PARA_ENTRY; // 总共 64 字节这两个槽分别放在 MRAM 地址空间的首尾区域相距越远越好。上电初始化流程很简单读 A 区校验 CRC再读 B 区校验 CRC。如果 A 区和 B 区都有效取版本号新的只有一个有效就用有效的那个两个都无效恢复出厂默认值。这套逻辑在数据记录设备里我用了很多年简单可靠没有竞态问题。6. 实测表现与踩坑记录三件小事差点让我返工6.1 8MHz SPI 下的实测时序量级我在开发板上用 8MHz SPI 时钟做过一轮简单压测。单页 64 字节写入包括 WREN、写命令、三字节地址和数据传输实测单次页写大约 75~80μs。整片 512KB 全部写一遍按 8192 页计算不超过 0.7 秒。这个速度对工业数据记录来说已经快到不构成瓶颈了。连续读的速度更高8MHz 下理论峰值就是 1MByte/s去掉循环和地址开销实际读完整片约 0.55 秒。对比一下一片 W25Q64 在 8MHz 下如果每次写前都要先擦除扇区哪怕擦除时间按最好的 40ms 算整片擦一遍都要好几秒再加上页编程时间根本不在一个量级。MRAM 在频繁写、要求可靠的场景里优势是碾压性的。6.2 坑一WP# 悬空导致写使能偶发失效第一次调通驱动后我做高温老化测试发现系统运行几个小时后偶发出现写入失败上位机读取到的参数还是旧值。排查了 SPI 时序、电源纹波、代码逻辑都没找到原因。后来用示波器勾 WP# 引脚发现这个脚的电平在随机跳动芯片把写使能指令给禁掉了。问题出在原理图上当时为了省两个电阻WP# 和 HOLD# 直接悬空以为芯片内部有上拉。MR25H40CDF 的 WP# 和 HOLD# 内部没有上拉悬空状态下 CMOS 输入电平不稳定。我给两个引脚各加了一颗 10kΩ 上拉电阻到 3.3V之后再也没有出现过偶发写失败。这个问题让我养成了一个习惯任何芯片的输入控制引脚手册没写内部上拉一律外部接电阻处理不能想当然。6.3 坑二上电瞬间 CS# 低电平引起的伪操作另一个问题是冷启动时偶尔出现启动失败后来定位到是 MCU 的 GPIO 在上电瞬间输出短暂低电平导致 CS# 被拉低MRAM 收到一段随机数据当作命令执行。MRAM 没有命令保护任何符合指令格式的非法访问都可能造成状态寄存器变化。解决方法是把 CS# 所在的 GPIO 在上电期间配置为高阻输入等 MSSP 初始化和 GPIO 方向配置完成后再设为输出低。或者在硬件上给 CS# 加一颗 10kΩ 上拉电阻到 VDD保证 MCU 没初始化前 CS# 处于高电平。我两个方案都用了双保险。这个坑在 Flash 上也存在但 MRAM 的写使能逻辑更宽松更容易被伪命令触发。6.4 坑三从 NOR Flash 驱动的老代码里带过来先擦后写最后一个坑是我自己折腾出来的。因为项目前期用了 Flash驱动里保留了扇区擦除的调用后来换成 MRAM这部分代码没有及时清干净。第一次往 MRAM 写数据时程序先执行了擦除指令 0xD8MRAM 虽然不认识这条指令但因为它处于片选选中状态数据线高电平持续了一段时间MRAM 把后面的命令字节解析得乱七八糟导致整块数据写毁。排查的时候我对比了 Flash 和 MRAM 的指令集才意识到多此一举。MRAM 写入前不需要擦除任何内容每个 bit 都可以独立从 0 变 1 或从 1 变 0不存在 Flash 的只能把 1 擦成 0再写 0 进去的限制。从 Flash 迁移到 MRAM 的驱动时务必把擦除逻辑、页编程等待逻辑全部删掉只保留 READ、WRITE、WREN、RDSR 这几条核心指令。6.5 一点测试建议量产前我建议至少做三类测试一是高温 85°C 下连续写擦除 24 小时确认 MRAM 在极限温度下 SPI 时序余量足够二是随机掉电测试用继电器随机切断电源每次上电检查参数镜像的 CRC 能力和日志的连续性三是全地址读写测试写 0x55、0xAA、0x00、0xFF 四种特征值确认没有地址线短路或数据线交叉。这三轮测试跑完存储子系统基本可以放心了。最后提一句我个人现在的做法MRAM 并不是要取代 Flash只把它放在最合适的位置。运行参数、校准表、高频运行日志放 MR25H40CDF固件 OTA 镜像、离线历史包这些大块数据我还是交给 NOR Flash两边用同一套读写接口上层应用完全不用关心底层是什么介质。这种混合存储的思路让整机在存储事故上几乎没再出过问题。如果你也是做工业控制器或者嵌入式数据记录设备建议从一个小项目开始先把 MRAM 的驱动和 CRC 校验流程跑通之后的收益会很明显。
返回列表