ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于MRAM的工业数据存储:PIC18驱动MR25H40CDF设计与实现

基于MRAM的工业数据存储:PIC18驱动MR25H40CDF设计与实现 去年做一个工业设备的数据记录模块起初选了最常见的 SPI NOR Flash结果客户现场频繁写运行日志不到一个月就出现参数区读写异常。排查到最后问题出在 Flash 的擦写寿命和页擦写机制上。后来我把存储颗粒换成 Everspin 的 MR25H40CDF主控还是原来的 PIC18F96J65只改了一百多行驱动代码问题彻底消失设备已经稳定跑了半年多。这篇文章想把 MR25H40CDF 和 PIC18F96J65 这套组合的完整用法、设计思路以及调试时踩过的坑完整记录下来。如果你正在做工业仪表、嵌入式数据记录或者受够了 Flash 擦写寿命和磨损均衡带来的麻烦这篇内容应该能帮你少走不少弯路。1. 为什么是这个组合工业存储场景的需求拆解1.1 工业现场的数据写入频率才是选型的分水岭先说一个很多人忽略的现实工业设备里最常见的存储需求不是“存程序”而是“频繁记录小数据”。运行日志、告警事件、累计运行时间、电表读数、温湿度采样这类数据的特点是单条很小可能就 16 字节到 128 字节但写入频率很高有的设备每 5 秒写一次。如果用的是普通 SPI NOR Flash问题就来了。NOR Flash 的最小擦除单位是扇区通常 4KB最小写入单位是页通常 256 字节。你要更新一条 32 字节的日志Flash 没有办法直接覆盖必须先把整个扇区读出来在缓存里修改对应的 32 字节然后把整个扇区擦除再重新写回去。这就是所谓的写放大。我算过一笔账假设一项参数每 10 秒更新一次一天就是 8640 次一个 4KB 扇区要容纳 128 条 32 字节记录按 10 万次擦写寿命算对应大约 1280 万次写入看起来好像有几年但工业现场还有高温、电压波动、ESD 干扰实际寿命远低于理想值。而且一旦掉电发生在擦除后、写入前的窗口期整个扇区的数据会全部丢失。EEPROM 呢容量普遍偏小写速度慢虽然支持字节写但频繁写入同样有寿命限制通常只有 100 万次左右。FRAM 能无限写入但大容量型号少、价格高、生态不如 SPI 类产品成熟。所以在这个场景下MRAM 是最直接的正解。1.2 MRAM 不需要擦除的随机字节写到底意味着什么MR25H40CDF 是 Everspin 的串行 SPI MRAM容量 4Mbit也就是 512KB3.3V 供电工业温度范围SPI 接口最高到 40MHz 级别。MRAM 的存储单元是一个磁隧道结MTJ用两个磁性层的相对方向来表示 0 和 1读的时候是纯电阻读取不改变状态写的时候用电流产生的磁场翻转磁性层的方向。这个物理结构和 Flash 有本质区别。Flash 靠浮栅电荷存储写之前必须先擦除把电荷全部清空MRAM 每个 bit 是独立翻转的所以它天生支持随机字节写不需要块擦除不需要页对齐没有写放大概念。你要写哪个地址直接发一条写指令带上地址和数据就行写入一个字节和写入连续 1KB 的成本几乎一样。数据保持也不依赖电荷。Flash 掉电后靠浮栅里的电荷维持状态电荷会缓慢泄漏MRAM 的磁状态是物理稳定结构掉电后数据保持 20 年以上标称读写次数基本等同于无限这在工业场景里是非常省心的特性——你不必在设计阶段给磨损均衡留出巨大的复杂度。1.3 PIC18F96J65 在整套系统里的角色定位PIC18F96J65 是 Microchip 的 8 位 MCU3.3V 逻辑内置 MSSP 外设支持 SPI 主模式片上还有充裕的 Flash 程序空间、RAM 和中断资源。对于中小型工业控制设备用 8 位 MCU 加外部存储的方案成本比直接上 ARM 加 eMMC 低得多而且 MPLAB X 加 XC8 的工具链非常成熟产品生命周期长工业客户也认可。在这套系统里PIC18F96J65 负责采集传感器数据、执行控制逻辑、和人机交互通信MR25H40CDF 只负责一件事把需要持久化保存的数据可靠地存下来。MCU 内部 EEPROM 可以作为参数备份但容量有限写入次数也有限不适合做高频日志外部 MRAM 把存储容量和工作负载从 MCU 内部剥离互不干扰。MRAM 的读写时序又比 EEPROM 简单MCU 代码里甚至不需要实现坏块管理或擦写均衡整个驱动层几十行函数就够。2. 硬件连接与电平细节搭好地基才能谈读写2.1 引脚映射与最小电路MR25H40CDF 是标准的 8 引脚封装引脚定义和普通 SPI NOR Flash 基本兼容从上到下分别是 CS#、SCK、SI、SO、WP#、VSS、VDD、HOLD#。和 PIC18F96J65 接线时最容易被绕晕的是 SI 和 SO 的方向问题。从 MCU 的角度看MSSP 的 SDO 是主输出引脚必须接到 MRAM 的 SIMSSP 的 SDI 是主输入引脚必须接到 MRAM 的 SO。如果按直觉把“MCU 的 SDO”和“MRAM 的 SO”接在一起那就完全反了读出来永远是 0xFF 或者 0x00。MR25H40CDF 引脚连接到 PIC18F96J65说明CS#GPIO普通 IO 口即可片选低电平有效空闲拉高SCKMSSP 的 SCKSPI 时钟SIMSSP 的 SDO主发从收SOMSSP 的 SDI主收从发VDD3.3V 电源必须加 100nF 去耦电容VSSGND低阻抗接地WP#拉高或接 GPIO写保护低有效HOLD#拉高暂停传输低有效不用时必须上拉最小系统里WP# 可以直接接 VDD但如果想在生产模式下物理禁止写入固件参数区建议把它接到一个 GPIO 上平时拉高执行关键参数锁定时拉低。HOLD# 绝对不能悬空因为这个脚一旦受干扰拉低MRAM 会暂停当前传输但 SPI 信号线还在跑很容易出现写了一半、数据错位的诡异现象。最简单可靠的做法是 HOLD# 直接接 VDD或者经 10kΩ 电阻上拉。2.2 电源、电平和 PCB 布局的实战经验MR25H40CDF 的供电范围是 2.7V 到 3.6V必须使用 3.3V 电源不能接 5V。PIC18F96J65 同样是 3.3V 逻辑两者可以直接相连不需要电平转换。如果板子上还有 5V 的传感器或外设务必检查信号线有没有跨电压域否则轻则读写异常重则烧毁引脚。电源去耦方面VDD 引脚旁边放一颗 100nF 的 X7R 陶瓷电容位置尽量贴近芯片另外在板级电源入口放一颗 10µF 钽电容或瓷片电容。MRAM 写操作本身电流冲击不大但高频 SPI 切换时会有瞬态电流去耦不到位会导致信号波形过冲进而产生误码。SCK 线如果走线比较长可以在 MCU 输出端串联 22Ω 到 33Ω 的电阻这个办法在抑制反射上很有效实测能明显减少工业环境下的误码率。PCB 布局上SPI 四根线都应该避免跨分割地平面SCK 和数据线之间不要平行走太长CS 线上更要注意不要让 CS 信号在 SCK 之前产生毛刺。CS 毛刺会导致 MRAM 误判指令开始最常见的结果就是写操作被静默丢弃。2.3 掉电场景的后备电容估算工业设备经常直接断开电源而恰恰是掉电瞬间我们最想保存现场数据。靠着 MRAM 写入速度快的优势检测到掉电后再写几百字节是完全来得及的但前提是 MCU 和 MRAM 在写入完成前还有电。掉电检测触发后MCU 需要继续工作一段时间这段时间由 VDD 上的储能电容维持。估算很简单假设系统在掉电瞬间还有 20mA 电流消耗我们希望维持 2ms 时间允许电压从 3.3V 跌落到 2.8V跌落 0.5V。根据电容放电公式C I × T / ΔV 0.02A × 0.002s / 0.5V 80µF考虑到电解电容容量误差和低温性能衰减实际选 220µF 到 470µF 比较稳妥。如果是大电流系统或者需要在掉电后写更多数据可以再把电容加大或者单独用超级电容给 MRAM 和 RTC 供电。注意工业温度范围下超级电容的容量会大幅下降一定要按最低工作温度下的容量做设计不能拿常温标称值算。3. 软件实现PIC18 的 MSSP 驱动 MR25H40CDF 完整过程3.1 MSSP 初始化的正确姿势PIC18F96J65 的 MSSP 外设配置不复杂但位定义容易记混。MR25H40CDF 支持 SPI Mode 0 和 Mode 3我习惯用 Mode 0也就是 CPOL0、CPHA0时钟空闲为低数据在第一个边沿采样。对应到 Microchip MSSP 的寄存器就是 CKP0、CKE1。初始化代码大致如下具体端口编号根据自己的 PCB 接线改#define MRAM_CS_TRIS TRISBbits.TRISB0 #define MRAM_CS_LAT LATBbits.LATB0 void spi_master_init(void) { MRAM_CS_TRIS 0; // CS 输出 MRAM_CS_LAT 1; // CS 空闲为高 TRISBbits.TRISB1 0; // SCK 输出 TRISBbits.TRISB2 0; // SDO 输出 TRISBbits.TRISB3 1; // SDI 输入 // SPI 主模式Fosc/16Mode 0 SSPSTAT 0b01000000; // CKE 1 SSPCON1 0b00100001; // SSPEN 1, CKP 0, SSPM 0001 } uint8_t spi_transfer(uint8_t dat) { SSPBUF dat; while (!SSPSTATbits.BF) { } return SSPBUF; }第一次调驱动时建议先用 Fosc/64 甚至 Fosc/16 的慢时钟跑通确认硬件接线没问题后再往上提频率。MR25H40CDF 可以跑 40MHz但 PIC18 的 MSSP 外设本身吞吐有限而且 SPI 走线、电平质量都会限制实际可用频率稳定压倒一切。3.2 MR25H40CDF 指令集和状态寄存器速查MRAM 的指令集和普通 SPI Flash 高度相似这也是很多人第一次上手觉得特别顺手的原因。最常用的指令如下指令命令码功能WREN0x06写使能必须在写操作前执行WRDI0x04写禁用RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据指令后跟 24 位地址FREAD0x0B快速读地址后加 8 个 dummy 周期WRITE0x02写数据指令后跟 24 位地址和数据SLEEP0xB9进入睡眠模式WAKE0xAB唤醒状态寄存器里有两个位最重要。bit0 是 WEL写使能锁存位每次执行成功的 WRITE 或 WRSR 后WEL 会自动清零bit7 是 SRP状态寄存器保护位当 SRP1 且 WP# 引脚为低时状态寄存器本身也无法改写。这里的关键机制是每次写操作前必须先执行一次 WREN把 WEL 置 1然后再执行 WRITE写完后 WEL 自动回到 0。这个设计是为了防误写——即使总线上出现随机干扰只要没有写使能锁存MRAM 不会接受任何写入指令。3.3 最精简的读写函数实现读一个字节的函数如下uint8_t mram_read_byte(uint32_t addr) { uint8_t val; MRAM_CS_LAT 0; spi_transfer(0x03); // READ 指令 spi_transfer((addr 16) 0xFF); // 地址高 8 位 spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); val spi_transfer(0x00); // 发送空数据产生时钟 MRAM_CS_LAT 1; return val; }写一个字节稍微多一步 WRENvoid mram_write_byte(uint32_t addr, uint8_t val) { MRAM_CS_LAT 0; spi_transfer(0x06); // WREN MRAM_CS_LAT 1; MRAM_CS_LAT 0; spi_transfer(0x02); // WRITE spi_transfer((addr 16) 0xFF); spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); spi_transfer(val); MRAM_CS_LAT 1; }连续读写一块数据时只需要在开头执行一次 WREN然后 CS 保持低电平连续发送指令、地址、数据字节全部传完再拉高 CS。MRAM 没有 Flash 那种页缓冲区机制所以写连续多字节不需要考虑页边界也不存在“写到页尾必须回头”的问题。代码可以这样void mram_write_buf(uint32_t addr, const uint8_t *buf, uint16_t len) { MRAM_CS_LAT 0; spi_transfer(0x06); MRAM_CS_LAT 1; MRAM_CS_LAT 0; spi_transfer(0x02); spi_transfer((addr 16) 0xFF); spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); while (len--) { spi_transfer(*buf); } MRAM_CS_LAT 1; }读多字节同理发送完地址后连续调用 spi_transfer(0x00) 直到收满长度再把 CS 拉高。3.4 512KB 的地址规划和跨边界回绕问题MR25H40CDF 的地址是 24 位范围从 0x000000 到 0x07FFFF一共 512KB。应用层最好提前规划好空间不要让日志、参数、告警事件混在一起。我习惯的分区如下地址范围用途0x000000 - 0x000FFF设备参数区 A0x001000 - 0x001FFF设备参数区 B备份0x002000 - 0x007FFF运行日志环形缓冲区0x008000 - 0x00BFFF告警事件区0x00C000 - 0x07FFFF预留区 / 固件升级暂存需要特别提防的是地址自增回绕。MRAM 的连续读写在地址到达 0x07FFFF 后如果继续会自动回到 0x000000。如果环形日志刚好写到容量末尾而记录长度超出了剩余空间就会出现“前半段在末尾后半段覆盖了开头”的情况。解决方案很简单在写入前判断剩余空间不够就把指针回绕到起始地址而不是依赖硬件自动回绕。4. 数据可靠性掉电保护、校验与存储布局设计4.1 工业现场数据损坏的三种典型来源很多人以为换成 MRAM 就一劳永逸了但实际工业现场数据出错更多是系统性问题。第一种是电压跌落和电源纹波MCU 和 MRAM 同时供电异常时写入的 SPI 帧可能不完整。第二种是 SPI 信号受传导干扰导致传输数据错位比如电机启停瞬间SCK 线上耦合的噪声会让 MRAM 把指令码识别错。第三种是程序跑飞后对存储区域的非法写入尤其是上电瞬间 GPIO 状态不确定CS 和其他引脚出现毛刺MRAM 可能被误选中并接收总线上的垃圾数据。MRAM 的无限写寿命能解决 Flash 寿命问题但解决不了上面这几类工程问题。可靠性设计必须靠外围电路加软件策略一起做。4.2 掉电检测与关键数据的紧急写入掉电瞬间想保存数据关键是检测要早、写入要快。PIC18F96J65 这类 J 系列 MCU 大多有片内低电压检测模块也有一个更可靠的做法外接电压监测芯片阈值选在 2.9V 或 3.0V低于阈值立即给 MCU 的外部中断引脚一个低电平。片内 LVD 配置代码因型号而异大致思路是这样// 使能低压检测中断具体寄存器位以数据手册为准 HLVDCONbits.HLVDEN 1; INTCONbits.PEIE 1; INTCONbits.GIE 1;在掉电中断里我会把当前运行状态打包成一个结构体直接写入 MRAM 的紧急存储区。MRAM 写一帧数据非常快一条记录加上校验字段通常一百字节以内只需要几毫秒。注意不要在中断里做太多事更不要调用那些带延时等待的函数否则电压已经跌到 MRAM 工作范围以下怎么写都没用了。这也是为什么我建议外接电压监测器而不是完全依赖片内 LVD监测器阈值和响应速度都更可控。4.3 CRC 校验策略不能省MRAM 本身不丢数据但数据从 MCU 到 MRAM 的传输链路可能出错。因此每条记录都要带校验字段推荐用 CRC-16/CCITT。按位实现的 CRC 函数不长适合 PIC18 这种 8 位 MCUuint16_t crc16_ccitt(uint16_t crc, const uint8_t *data, uint16_t len) { while (len--) { crc ^ (*data) 8; for (int i 0; i 8; i) { if (crc 0x8000) { crc (crc 1) ^ 0x1021; } else { crc 1; } } } return crc; }写记录时把 CRC 放在记录末尾读回来先算 CRC 再比对。启动时只加载校验通过的参数区如果两个备份区的 CRC 都失败再进入出厂默认值模式并发出报警。别用简单的异或和或求和校验工业环境下经常出现全 0xFF 或全 0x00 的干扰模式简单校验很容易碰巧通过。4.4 关于寿命与写入次数的实测认知MR25H40CDF 标称读写次数极高数据保持 20 年以上。做样机时我还是做了几组实测随机地址连续写入 5000 次然后全量读回比对全部一致在 -40 到 85 温度循环下重复写入读取没出现一例误码模拟掉电 1000 次每次掉电前写入紧急记录上电后校验全部通过。不过有一点要提醒MRAM 不需要磨损均衡不代表可以不检查存储链路。我建议在每次上电自检时读一遍关键参数区的 CRC如果发现 CRC 异常尝试用备份区恢复。如果条件允许每隔半年或设备大修时把参数区重写一遍这不是因为 MRAM 会丢数据而是为了验证 MCU、SPI 总线、焊点、电源整个链路的可靠性。5. 调试中的坑从波形到现象的完整排障链路5.1 读出全是 0xFF先别怀疑芯片这是最经典的现象。第一反应往往是“MRAM 坏了”但 90% 的情况是 SPI 通信根本没建立起来。我的排查顺序是固定的。先用逻辑分析仪抓 RDSR 指令的完整帧。如果波形上 CS、SCK、SI 都有动作但 MRAM 始终不回数据先量 VDD 有没有 3.3V再量 WP# 和 HOLD# 是不是被拉低了然后确认 SI 和 SO 有没有接反。这几个问题用万用表就能查完。如果波形上 CS 在 SCK 脉冲结束前就提前拉高了说明 CS 时序有问题多半是 CS 用了普通 GPIO 但初始化顺序不对导致 CS 和 SCK 没有建立足够的时间关系。可以把 SPI 时钟降到 1MHz 再试排除高速信号带来的干扰。5.2 WEL 没置位导致的“写入静默失败”更隐蔽的问题是写入操作看起来执行了但读回来还是旧数据。读状态寄存器会发现 WEL 位是 0说明写操作根本没被接受。最常见的原因是代码把 WREN 和 WRITE 放在了同一个 CS 帧里。正确流程必须是两个独立的片选周期先 CS 拉低、发 0x06、CS 拉高完成写使能然后再 CS 拉低、发 0x02、地址、数据、CS 拉高。如果 WREN 之后没有让 CS 拉高一次MRAM 不会锁存 WEL后面的 WRITE 指令就会被忽略。另一个常见原因是连续写了多块数据第一块写完后 WEL 已经自动清零第二块写之前忘了重新 WREN。这类问题在自测时偶尔复现很容易被当成“偶发故障”误判。5.3 HOLD# 引脚悬空造成的“灵异现象”我调试时遇到过一种现象长时间读写偶发出错逻辑分析仪看到 MRAM 的 SO 数据线在传输中途无端停住随后又恢复时间没有任何规律。最后发现是 HOLD# 悬空。HOLD# 低电平有效悬空状态下只要周围有电磁干扰把这个脚拉低MRAM 就会暂停与主机的通信而 CS 和 SCK 还在正常拉高拉低于是整帧数据在中间断了一截。解决办法就是把 HOLD# 直接接 VDD或者串一个 10kΩ 电阻上拉。不要依赖芯片内部上拉MRAM 的 HOLD# 不是强上拉扛不住工业现场的干扰。5.4 WP# 与 SRP 位的状态寄存器写保护如果想通过 WRSR 修改状态寄存器遇到的另一个坑是 SRP 位保护。当状态寄存器里的 SRP1 且 WP# 引脚为低电平时WRSR 指令会被拒绝更麻烦的是你没法通过 WRSR 把 SRP 清掉因为此时状态寄存器已经处于保护状态。唯一的办法是把 WP# 拉到高电平再重新执行 WRSR。所以调试阶段我建议把 WP# 暂时直接接 VDD等整套功能跑通了再决定要不要用一个 GPIO 来控制它用做生产模式下的物理写保护。5.5 用逻辑分析仪和读写脚本快速定位问题层最后分享一个调试方法论遇到问题先分层。如果 MRAM 读写异常先判断是物理层、协议层还是应用层。物理层查电平、接线、波形协议层用逻辑分析仪抓指令帧核对每个字节是否和期望一致应用层则是看代码逻辑有没有越界、回绕、写使能遗漏。有条件的话可以把 MR25H40CDF 插到一块独立的 SPI 测试板上用 PC 或树莓派跑一段随机读写脚本一秒内写入几百组随机数再读回来比对。如果外部测试板没问题那就是原板硬件或 PIC 驱动代码的问题能把排查范围缩小一半以上。这套方法帮我排除过不止一次“芯片突然坏了”的假象。6. 方案扩展环形日志、参数备份与更高速场景6.1 基于 MRAM 的环形日志比 Flash 时代简单一个数量级日志记录是这类型存储最典型的应用。用 Flash 做环形日志时需要考虑扇区周转、擦写均衡、掉电后扇区头尾识别复杂度很高。用 MRAM 做环形日志本质上只需要维护两个指针写指针 head 和读指针 tail。每条日志固定长度比如 64 字节记录格式是2 字节长度、4 字节时间戳、56 字节数据、2 字节 CRC。写入前判断 head 到空闲区尾部是否放得下一条记录放不下就回绕到环形区起始地址并更新环形区头部的元数据。整个环形区的管理代码不超过五十行而且不需要特殊处理“擦除”这个过程因为每个地址都可以直接覆盖写。6.2 双备份参数区与启动自检对于设备参数这类关键数据我会用双备份加激活标志的方案。参数区 A 和参数区 B 内容冗余另有一个固定地址的激活标志。写入流程是先在非激活区写入新参数并校验校验通过后把激活标志更新为指向该区下次启动时优先加载激活区参数。这样即使写入过程中掉电丢失的至多是备份区的数据激活区还保留着上一份有效参数。启动加载时的流程是读激活标志 → 加载对应区 → CRC 校验 → 校验失败则加载另一区。MRAM 的优势在这里体现得很充分整个双备份切换不需要处理扇区擦除可以在几个字节的地址空间里完成原子切换。6.3 从串行 MRAM 到更大容量和并行接口的升级路径如果日志量特别大512KB 不够用可以向上换 MR25H64CDF 这类更大容量的串行 MRAM指令集和驱动代码几乎不用改只需要把地址组织和分区规划调整一下。如果追求更高读写吞吐并行接口 MRAM 是另一个方向8 位或 16 位总线可以直接挂在 MCU 总线或 FPGA 上但驱动复杂度和 PCB 布局难度都会上升。在 PIC18 这类没有 SPI DMA 的 MCU 上串行 MRAM 的吞吐瓶颈主要在逐字节等待 BF 标志这个过程。每传一个字节要轮询一次200 字节的写入要轮询 200 次。如果以后换到带 DMA 的 MCUSPI 吞吐可以明显提升MRAM 本身 40MHz 的时钟能力足够支撑大部分工业应用。我自己做完这个项目后的体会是存储选型不是选一颗“能存数据”的芯片而是选一种匹配自己写入模型的方案。如果你每次只写几十字节、一天成百上千次、还要求掉电不丢MRAM 的简单可靠能省掉非常多时间和精力。这套驱动和设计思路我后来复用到了好几个项目里每次改动的只有引脚映射和分区表核心读写逻辑几乎原封不动。
返回列表