ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MRAM与PIC18组合:工业控制器高频数据存储方案

MRAM与PIC18组合:工业控制器高频数据存储方案 前阵子接了一个工业控制器项目需要在主控里保存校准系数、配置参数和运行日志。现场条件不友好设备随时可能断电日志写入频率很高而且程序要靠历史数据判断设备状态。同事一开始用的是 SPI EEPROM用到后期开始担心寿命换 NOR Flash又绕不开擦除、块对齐和写等待。最后我把目光放到了 Everspin 的 MR25H40CDF 串行 MRAM 上配合一颗 Microchip 的 PIC18F96J94 做主控整套存储与读取方案才算真正稳定下来。MR25H40CDF 是一颗 4Mbit 的 SPI 接口 MRAM最大的特点是无磨损限制、写入不需要擦除也不存在等待非常适合工业现场无规律的高频数据记录PIC18F96J94 自带 MSSP 串行外设、资源充足成本也能压在合理范围。这篇文章我把关键点一次讲透从硬件接线到底层驱动再到掉电一致性和日志区设计适合正在做嵌入式存储、数据采集或设备状态记录的开发者参考。1. 为什么这个组合适合工业数据存储选型逻辑1.1 MR25H40CDF 到底是一颗什么样的芯片MR25H40CDF 不是普通 EEPROM也不是 Flash它属于 MRAM存储单元用的是磁隧道结技术。磁性状态一旦写入即使断电也不会丢不需要电池备份也没有 Flash 那种“必须按页编程、写之前先擦除”的规则。容量上MR25H40CDF 是 4Mbit按 8bit 组织就是 512K x 8也就是 512KB。供电范围典型是 3.3V接口是标准 SPI支持 SPI Mode 0 和 Mode 3串行时钟可以跑到几十 MHz具体速率要看封装和温度档位。DF 后缀一般代表封装形式和温度等级工业现场使用要选温度范围够宽的批次。真正让它在工业场景里脱颖而出的是三点写入寿命极高。EEPROM 一般十万到百万次写NOR Flash 往往只有十万次级别MRAM 在正常使用中基本不用考虑磨损。免擦除、免等待。SPI 时钟发完一个字节数据就已经固化了不像 NOR Flash 要等上几个毫秒甚至几十毫秒的页编程时间。掉电保存。不需要超级电容、不需要电池写进去就是非易失的。简单说MRAM 在软件眼里很像一片“掉电不丢的 SRAM”读写逻辑非常简单。1.2 PIC18F96J94 在这套方案里扮演什么角色PIC18F96J94 是 Microchip 的 8 位 PIC18 系列属于 J94 这个产品家族。它的定位不是做大量数据处理的高端处理器而是在工业控制里干“采集、通信、控制”这些脏活累活。在这套方案里PIC18F96J94 主要做三件事通过 ADC、UART、Modbus 等接口采集现场数据把数据整理之后通过 SPI 写入 MR25H40CDF上电之后从 MRAM 读回配置和日志继续执行控制逻辑。J94 系列内部集成有 MSSP 模块可以配置成 SPI 主模式直接和 MRAM 对接。代码里不需要用 GPIO 模拟 SPI时序稳定性和 CPU 占用都会好很多。再加上 PIC18 的抗干扰能力和成熟生态很多工业板卡仍会选这类 MCU 做主控而不是一上来就用高性能应用处理器。1.3 为什么不用 EEPROM、FRAM 或 NOR Flash很多朋友一听“非易失存储”第一反应就是 I2C EEPROM 或者 SPI NOR Flash。但在我们的场景里它们各有各的别扭。对比项MR25H40CDF串行 EEPROMFRAMSPI NOR Flash写入寿命极高几乎不考虑磨损10^5 ~ 10^6 次10^10 ~ 10^12 次10^4 ~ 10^5 次写入方式按字节/连续写免擦除按字节写按字节写按页写需先擦除写入等待命令发完即完成一般 3~5ms命令发完即完成页编程几 ms擦除更慢典型密度4Mbit 常见几 Kbit 到 1Mbit几百 Kbit 居多4Mbit 以上很常见软件复杂度低低低高要管块、页、坏块如果只是保存几个配置字EEPROM 够用。如果做高频无规律的数据记录EEPROM 寿命和写等待就是瓶颈。NOR Flash 容量大但管理负担重掉电一致性、磨损均衡、块擦除都得处理不适合高频率小数据量的日志场景。FRAM 写入寿命也不错但大容量型号不多成本和供货也不如 MRAM 稳定。所以结论很简单频繁写、不定时写、还要保证掉电不丢首选 MRAM。MR25H40CDF 的容量和 SPI 接口又刚好适合 8 位 MCU 方案和 PIC18F96J94 这种带 MSSP 的芯片组合起来很顺。2. 硬件接线让 MR25H40CDF 和 PIC18F96J94 可靠通信2.1 引脚映射与最小系统连接MR25H40CDF 虽然叫 MRAM但引脚逻辑和普通 SPI Flash 很像。核心引脚是这些VDD、VSS电源和地CS#片选低电平有效SCKSPI 时钟SI主出从入也就是主控发出去的数据线SO主入从出主控接收 MRAM 数据的线WP#写保护引脚低电平有效HOLD#暂停通信引脚低电平有效。连接 PIC18F96J94 时原则是“主控的 SDO 接 MRAM 的 SI主控的 SDI 接 MRAM 的 SO”。不要接反接反之后读出来的数据永远是 0xFF 或者 0x00。我手上的板子使用 MSSP1 外设引脚分配大概如下主控功能连接到 MR25H40CDFGPIOCSCS#SCKSCKSDOSISDISO3.3VVDDGNDVSSCS 必须用普通 GPIO 手动拉低拉高不能图省事直接接到地。因为每条 SPI 命令都以 CS 的下降沿开始、上升沿结束CS 一直拉低会让 MRAM 无法区分命令边界。我习惯用宏定义把引脚隔离出来方便移植#define MRAM_CS_LAT LATBbits.LATB2 #define MRAM_CS_TRIS TRISBbits.TRISB2 #define MRAM_SCK_TRIS TRISBbits.TRISB3 #define MRAM_SDO_TRIS TRISBbits.TRISB4 #define MRAM_SDI_TRIS TRISBbits.TRISB5不同封装的复用引脚不同实际用哪几个脚一定要去查 PIC18F96J94 数据手册里的引脚复用表。这里只是示例不是所有型号都默认长这样。2.2 电源去耦与地平面布置MRAM 本身功耗不大但在掉电瞬间和频繁读写时电源纹波会有变化。如果电源处理不好偶发读写错误会非常难查。我在这颗 MRAM 的 VDD 附近放了两颗电容一颗 0.1uF 陶瓷电容做高频去耦一颗 4.7uF 到 10uF 的电容做低频储能。注意电容要尽量靠近 VDD 引脚走线先过电容再到芯片不要绕到板子另一边再绕回来。地平面也要尽量完整。SPI 虽然不像高速差分总线那么挑剔但时钟到几十 MHz 之后回路面积过大会引入噪声。工业现场往往有变频器、继电器、电机干扰源非常多地线乱弹很容易造成 SPI 误码。如果 MCU 和 MRAM 分属两块板子比如主板和存储板通过排线连接我会建议在 SPI 信号线上加 22~33Ω 的串联电阻抑制振铃。长度超过 10cm 的话优先降低 SPI 时钟频率而不是一味追求高速。2.3 HOLD、WP 与 CS 的上拉策略HOLD# 引脚如果浮空工业现场的噪声一旦把它拉低MRAM 会立刻“冻结”当前通信后续时钟全部忽略。主机侧可能一直等不到数据逻辑分析仪又看不出明显问题。这是最容易忽视的坑。处理办法很简单HOLD# 不用就通过 4.7k 或 10k 电阻上拉到 3.3V。WP# 如果不动态改写保护位直接接到 3.3V如果想在程序里控制写保护就接到 MCU 的 GPIO。CS# 建议同时加一个 10k 上拉避免 MCU 上电初始阶段 IO 状态不定时MRAM 被误选中。另外要注意电平匹配。MR25H40CDF 工作电压是 3.3V 左右如果 PIC18F96J94 工作在 5V就必须做电平转换或者在确认数据手册允许的情况下使用开漏加外部上拉的方案。不要直接硬接长期超出绝对最大额定值会让芯片内部损伤。3. PIC18F96J94 驱动MSSP 配置与读写命令封装3.1 MSSP 主模式初始化先解决时钟极性和相位PIC18F96J94 的 MSSP 配置成 SPI 主模式之后最重要的两个参数是时钟极性和相位。MR25H40CDF 支持 SPI Mode 0 和 Mode 3我这里统一用 Mode 0也就是空闲时 SCK 为低电平数据在第一个时钟沿被采样。Microchip 的 MSSP 寄存器里控制这两个参数的是 CKP 和 CKE很多人会在这里反直觉地出错SPI Mode 0CKP 0CKE 1SPI Mode 3CKP 1CKE 0。不要看到 CKE1 就以为是上升沿采样必须结合 CKP 一起看。第一次调驱动的时候我就在这里吃过亏。初始化代码大致如下void mram_spi_init(void) { // CS 默认拉高防止上电乱选中 MRAM_CS_LAT 1; MRAM_CS_TRIS 0; // 引脚方向SCK、SDO 输出SDI 输入 MRAM_SCK_TRIS 0; MRAM_SDO_TRIS 0; MRAM_SDI_TRIS 1; // MSSP1SPI 主模式时钟 Fosc/64 // SSPEN1, CKP0, SSPM0010 SSP1CON1 0b00100010; SSP1STATbits.CKE 1; // SPI Mode 0 }如果系统时钟是 32MHzSPI 时钟就是 32MHz / 64 500kHz。这个速度对 MRAM 来说很慢但绝对稳先跑通功能再提速。3.2 收发一个字节的基础函数SPI 是同步全双工协议主控每发送一个字节的同时也会从 SO 线上收到一个字节。所以无论你是单纯发命令还是单纯读数据底层函数本质都是同一个uint8_t mram_spi_xfer(uint8_t out) { SSP1BUF out; while (!SSP1STATbits.BF); return SSP1BUF; }代码里用 BF 位判断接收缓冲是否有效比靠延时靠谱。注意如果上一次读 SSP1BUF 之后没有清标志或者后续中断没有处理最好先读一次清掉缓冲再开始下一次传输。实际项目中我不会让这个函数无限等下去。SPI 受干扰时BF 可能一直不变卡死在 while 里会导致整个主控看门狗超时。后面的章节会给带超时版本的写法。3.3 命令层WREN / RDSR / READ / WRITEMR25H40CDF 的 SPI 命令风格和常见 SPI Flash 接近但不需要擦除命令。我用的核心命令如下命令操作码说明WREN0x06写使能写入前必须先执行WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03按地址读数据WRITE0x02按地址写数据写使能的时序一定要单独用 CS 拉高拉低包起来void mram_write_enable(void) { MRAM_CS_LAT 0; mram_spi_xfer(0x06); MRAM_CS_LAT 1; }读状态寄存器uint8_t mram_read_status(void) { uint8_t st 0; MRAM_CS_LAT 0; mram_spi_xfer(0x05); st mram_spi_xfer(0x00); MRAM_CS_LAT 1; return st; }读任意长度数据MR25H40CDF 是 512KB地址需要 19bit所以发送三个地址字节void mram_read(uint32_t addr, uint8_t *buf, uint32_t len) { if ((addr len) 0x80000U) len 0x80000U - addr; MRAM_CS_LAT 0; mram_spi_xfer(0x03); mram_spi_xfer((addr 16) 0xFF); mram_spi_xfer((addr 8) 0xFF); mram_spi_xfer(addr 0xFF); while (len--) *buf mram_spi_xfer(0x00); MRAM_CS_LAT 1; }写入数据类似void mram_write(uint32_t addr, const uint8_t *buf, uint32_t len) { if ((addr len) 0x80000U) len 0x80000U - addr; mram_write_enable(); MRAM_CS_LAT 0; mram_spi_xfer(0x02); mram_spi_xfer((addr 16) 0xFF); mram_spi_xfer((addr 8) 0xFF); mram_spi_xfer(addr 0xFF); while (len--) mram_spi_xfer(*buf); MRAM_CS_LAT 1; }MRAM 写操作不需要像 NOR Flash 那样等页编程完成CS 拉高之后数据就已经是新的了。所以从软件角度看这套命令封装比 Flash 简单很多。3.4 逻辑分析仪验证时序驱动写完后先不要直接上大数据量我习惯做一步最基础的验证往地址 0x1000 写一个字节 0x5A再读回来看看对不对。uint8_t test 0x5A; mram_write(0x1000, test, 1); test 0; mram_read(0x1000, test, 1); if (test ! 0x5A) // 通信或接线有问题如果失败了接上逻辑分析仪抓 CS、SCK、SI、SO 四条线。重点看第一条 WREN 命令CS 拉低发送 0x06CS 拉高。不满足这个分离时序MRAM 不会接受后续写命令。我当时抓到的最典型错误是代码里为了让“写使能 写数据”连续执行CS 全程没有拉起来MRAM 把 0x06 当成了普通数据写操作自然失败。这个问题在逻辑分析仪上一眼就能看出来。4. 存储架构设计配置、日志和掉电一致性4.1 配置区多副本 序号 CRCMRAM 虽然写入可靠但任何存储方案都不能假设“断电一定发生在两次操作之间”。比如正在写一个 128 字节配置块写到第 80 字节时突然断电这个块就是残缺的。我采用的办法是保留多个配置副本每个副本带上单调递增的序号和 CRC 校验。启动时逐个读取选择“序号最大且 CRC 校验通过”的副本作为有效配置。一次典型的启动流程如下读取副本 A、B、C对每个副本做魔数检查 CRC 校验从通过校验的副本中选择序号最大的一个如果最高序号副本的数据异常则回退到次新副本并把异常副本标记为待修复正常运行时更新配置时同时刷新所有副本。因为 MRAM 不需要擦除也不担心磨损所以更新多个副本的成本很低。选择最高序号而不是最新时间是为了避免各个副本的时间戳不一致。4.2 日志区固定长度记录 环形缓冲工业现场常需要保存最近一段时间的数据比如故障前后的 ADC 采样值、温度值、开关状态。MRAM 容量有 512KB不适合存超大文件但保存几千到几万条日志很合适。我推荐的布局是环形缓冲区MRAM 日志区起始地址 [8 字节头部][记录 0][记录 1][记录 2] ... [记录 N-1]头部可以保存魔数、当前写入索引、缓冲区总长度、当前封装版本号。每条记录尽量用固定长度比如 64 字节。固定长度最大的好处是计算位置方便record_addr LOG_BASE 8 (write_index * RECORD_SIZE);写入顺序很关键。我先写记录数据本身再更新头部中的 write_index。这样即使写入过程中断电头部指向的还是上一条有效记录已崩溃的半条记录会在下一次启动时被跳过或覆盖。4.3 CRC 校验与脏数据识别工业记录里我常用 Modbus CRC16代码简单、计算量也小。每一条记录除了业务数据再追加 2 字节 CRC。读日志时如果 CRC 错误只跳过这条不中断后续记录。配置区比日志区更严格。配置块结构大概是这样魔数 4 字节 序号 4 字节 业务数据 N 字节 CRC16 2 字节写入时我习惯先写魔数和业务数据最后写 CRC。校验时如果发现魔数正确但 CRC 错误说明数据写了一半系统应该回退到上一版。因为 MRAM 免擦除这种“多副本 校验”的实现成本非常低。4.4 实测性能与瓶颈把 SPI 时钟从 500kHz 提到 Fosc/4 之后单片机 32MHz 主频下 SPI 时钟为 8MHzMRAM 本身完全可以承受。实测下来连续写 32KB 数据大约 35ms连续读 512KB 大约 0.55s具体数值会受中断占用和 CRC 计算影响但比 EEPROM 快了一个量级。对日志应用来说即使每秒钟产生 64 字节记录一天也不过 5.5MB512KB 的 MRAM 不可能存一天。所以方案上我会做分层高频数据缓存在 RAM 里每 30 秒或 1 分钟批量写一次 MRAM。这样既可以保证掉电不丢太多也不至于频繁搬运数据。工业场景里瓶颈往往不再存储芯片而在主控的采样频率和通信速度。MRAM 只是把原来“能不能写、会不会磨损”的问题直接消掉了。5. 调试中踩过的坑从全 0xFF 到写保护5.1 现象一读回来全是 0xFF用 mram_read 读配置区返回缓冲区里全是 0xFF这是 SPI 存储器最常见的问题。原因不是 MRAM 坏了而是时钟相位和主控不匹配SO 上采不到有效数据。MR25H40CDF 支持 Mode 0 和 Mode 3。如果你 MSSP 配置成了 Mode 1 或 Mode 2读状态寄存器也会得到 0xFF。先确认 CKP 和 CKE 的组合再查 SO、SI 有没有接反最后用逻辑分析仪看时钟边沿和数据有没有对齐。排查顺序建议是检查 SCK 空闲电平检查数据变化沿和采样沿检查 CS 是否在每个命令之间正常拉高检查 SO 是否确实到了主控 SDI 引脚。5.2 现象二执行了 WRITE但数据没进去写操作返回正常但读回地址内容还是 0xFF十有八九是漏了 WREN。MR25H40CDF 和大多数 SPI 非易失存储一样默认状态是写禁止。WRITE 命令前必须发 WREN把状态寄存器里的 WEL 位置 1。而且 WREN 必须是一条独立的命令有自己的 CS 低高周期。我这边踩过的坑是为了省时间把 WREN 和 WRITE 放在同一个 CS 低电平周期里连续发 0x06 再发 0x02。MRAM 不会把它理解为“先写使能再写”而是把它当成一个普通数据流。正确的做法是CS 拉低 - 发送 0x06 - CS 拉高 CS 拉低 - 发送 0x02 地址 数据 - CS 拉高每次写操作之前都调用 mram_write_enable最省心。5.3 现象三写入“成功”但指定区域没变还有一种更难查的情况写入操作正常WEL 也设置了但目标区域始终是原始值。这时要去看状态寄存器里的块保护位。MR25H40CDF 的状态寄存器里有 BP0、BP1、WPEN 等保护位。如果误操作把保护位设成了非 0那么受保护区间会变成只读WRITE 命令在这个区域不会生效而且没有任何错误标志告诉主机“写入被拒绝”。我当时就是调试程序时不小心执行了一次 WRSR把一个区域保护了结果所有写测试都失败。解决方法是先读状态寄存器uint8_t st mram_read_status(); if ((st 0x0C) ! 0) // BP1、BP0 中有任意一个置位 { // 需要解除保护 mram_write_enable(); // 发送 WRSR0x00 表示不保护任何区域 MRAM_CS_LAT 0; mram_spi_xfer(0x01); mram_spi_xfer(0x00); MRAM_CS_LAT 1; }注意如果 WP# 引脚被拉低WRSR 可能也会被忽略所以调试时 WP# 要先接高。5.4 现象四工业现场偶发卡在死循环前面的坑都解决后样机在实验室一切正常上到现场却偶发死机。后来发现是底层 spi_xfer 的 while 等待没有超时SPI 线上一个毛刺导致 BF 位始终不变程序就卡死了。这是嵌入式里非常危险的事情。任何通信等待都要带超时不能单纯 while 死等。我把底层函数改成了uint8_t mram_spi_xfer_timeout(uint8_t out, uint16_t timeout) { SSP1BUF out; while (!SSP1STATbits.BF) { if (timeout-- 0) return 0xFF; // 超时 } return SSP1BUF; }超时之后可以重试也可以上报错误让主控层处理。加了这个机制之后现场再也没有出现过“永久卡死”的问题。另外HOLD# 引脚的上拉和电源去耦一定要做好。我曾经为了省一个电阻把 HOLD# 悬空结果在继电器吸合的瞬间日志偶发漏写。后来补上 10k 上拉问题消失。写保护、超时、SPI 模式、HOLD 上拉这四件事看着都不起眼但每一件都足以让整套存储方案在工业现场翻车。MRAM 本身足够稳剩下的稳定性要靠主控侧驱动和 PCB 细节来兜底。
返回列表