ARTICLE DETAIL

资讯详情

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

MRAM+PIC32工业存储实战:SPI驱动、掉电保存与记录方案

MRAM+PIC32工业存储实战:SPI驱动、掉电保存与记录方案 做嵌入式工业设备固件的这些年最让我头疼的从来不是算法而是那一堆必须断电保存的现场数据。设备参数、运行日志、故障记录在设备几十年生命周期里天天被写入普通NOR Flash那点擦写寿命根本扛不住掉电瞬间丢数据更是家常便饭。后来在一条电机状态采集产线上我直接上了Everspin的MR25H40CDF4Mbit串行MRAM配Microchip的PIC32MX695F512L才算是把存储和读取数据这件事彻底理顺了。这篇就把从硬件接线、SPI驱动、存储协议到现场排障的完整做法记录下来给正在做工业嵌入式存储的朋友一个可以直接抄作业的参考。这套方案适合下面这些人要做设备运行历史记录、故障黑匣子、参数掉电保存又嫌Flash寿命不够、U盘方案太大、电池SRAM要维护的人手里正好有PIC32MX695F512L这类MIPS核心MCU想接一颗MRAM做高速无痕写入的人还有纯粹想了解SPI MRAM到底怎么用、和普通NOR Flash驱动差在哪的人。看完之后你会发现MRAM的驱动比Flash还简单难的是把存储策略想明白。1. 为什么是MRAM PIC32这套组合解决了什么问题1.1 工业现场存储的真实痛点先说说我之前用NOR Flash踩过的坑。工业设备上最喜欢用SPI NOR Flash存参数几块钱一片容量大熟悉的人也多。但NOR Flash的毛病非常明确擦写寿命普遍在10万次左右高端一些的也就100万次量级而且写入前必须先擦除擦除按4KB甚至64KB的扇区来擦一次几十毫秒要是设备每天写几十条日志两三年下来Flash就可能出现坏块。现场设备又不能动不动就返厂换Flash所以我后来一看到频繁写入断电保存的需求第一反应就是换存储介质。另一个常见方案是带电池的SRAM比如DS1230这种。优点是读写不限次数、速度快缺点是需要电池维持数据高温环境下电池寿命缩水几年就得换一次而且电池本身在工业环境下也算一个隐患。还有铁电存储器FRAM读写也快寿命也长但容量做不大价格偏高4Mbit的FRAM比MRAM贵不少。那为什么MRAM能把这些问题全盖掉MRAM的中文叫磁阻随机存储器原理是利用磁隧道结的磁化状态来保存数据不是靠电荷所以它天生没有擦写次数限制也不用担心断电丢电荷。我用的这颗MR25H40CDF数据保持标称20年以上读写耐久性可以到10的14次方次这意味着你可以把它当成一个纯内存来用整天整夜地写完全不用考虑寿命损耗。1.2 MR25H40CDF这颗芯片到底强在哪先看容量和接口。MR25H40CDF容量是4Mbit换算下来512KB对于存参数、存运行记录、存故障日志这种场景完全够用。接口是标准SPI支持Mode 0和Mode 3时钟最高能跑到40MHz以上我实际只跑10MHz留了足够裕量。供电3.3V工业级温度范围封装是DFN8占板面积非常小。再看读写行为。这是MRAM和Flash最本质的区别MRAM不需要擦除写入时直接把目标地址的内容覆盖成新数据写入速度就是SPI传输速度本身没有先擦后写的等待时间。读操作和写操作的时序几乎一样简单指令集也和SPI NOR Flash非常像有WREN写使能、WRDI写禁止、RDSR读状态寄存器、READ读数据、WRITE写数据。这就带来一个好处如果你以前写过SPI Flash驱动改造成MRAM驱动只需要删掉擦除逻辑再把写函数里的页缓冲处理去掉基本就通了。还有一个容易被忽略的优点MRAM写入是即时生效的没有内部编程忙状态。也就是说你写完最后一个字节拉高CS数据就已经落稳了不需要轮询WIP位等几十毫秒。这点对掉电保护特别友好因为写入窗口被压缩到极短数据被突然断电打断的概率大幅降低。1.3 PIC32MX695F512L为什么合适做主控PIC32MX695F512L是Microchip的MIPS M4K内核主频80MHz512KB Flash、128KB RAM在工业控制里算一个皮实耐用的主力型号。它自带多个SPI、UART、I2C模块其中SPI模块的时钟可以自由配置从几十kHz到几十MHz都能出适配MRAM的高速特性绰绰有余。选它还有一个现实原因工业现场往往不只是存储前面还要通过Modbus或OPC UA协议去读PLC、传感器、数控机床的运行状态后面还要通过UART/以太网把历史数据吐给上位机。PIC32MX695F512L的外设数量和引脚数量都够用一片MCU把采集、存储、上报全包了不用额外挂协处理器。而且Microchip的编译工具链成熟MCC插件不用寄存器直接操作反而更可控因为存储驱动这种代码越底层越不容易出幺蛾子。1.4 整条方案的长相不画图我用文字描述一下整体结构。PIC32MX695F512L作为SPI主机通过四条线连到MR25H40CDFSCK、SDI、SDO、CS。主机以大约10MHz的频率访问MRAM。应用层采集现场数据比如通过Modbus拉回一台电机的电流、温度、转速组装成固定长度的记录帧调用存储模块写入MRAM。系统重启后存储模块扫描MRAM里的记录区把最后N条记录读出来通过UART或以太网传给上位机做故障分析。整条链路最核心的就是存储模块下面逐步展开。2. 硬件连接把MRAM挂到PIC32的SPI上2.1 引脚和接线别接错了首先确认引脚定义。MR25H40CDF的标准SPI引脚是SCK时钟、SDI数据输入主设备视角的MOSI、SDO数据输出主设备视角的MISO、CS片选低有效。另外还有HOLD和WP两个功能引脚。PIC32MX695F512L这边我用SPI1模块主机模式下的SCK1、SDO1、SDI1分别接到MRAM的SCK、SDI、SDO。CS不一定要用硬件片选我建议直接用任意一个GPIO引脚来控制CS这样时序控制最精确。注意SDI和SDO千万不要接反接反了的表现就是写进去的数据能读出来但全反位或者根本读不出来。我在调试时见过有人把SDO接到SDI上结果读回来的全是0xFF还以为是芯片坏了。连接线上导线长度尽量控制在5到10厘米以内。MRAM的IO翻转速度很快线拖太长时在10MHz下波形边缘可能产生过冲和振铃轻则偶发误码重则完全通讯不上。如果你非要用杜邦线飞线调试我建议把SPI时钟先降到1MHz验证逻辑没问题后再升到10MHz。2.2 电源、HOLD和WP引脚的处理电源这块没什么玄学MRAM的VDD接3.3V每个电源引脚旁边放一只0.1uF的陶瓷电容电容尽量靠近芯片电源脚。工业环境如果干扰大我还会在VDD入口串一颗10欧姆电阻再并一只10uF电容组成一个简单的低通滤波能压掉不少高频毛刺。HOLD引脚是暂停通讯用的这个功能在普通应用里用不上但必须处理好。如果HOLD被拉低芯片会暂停SPI通讯不管时钟怎么翻转都忽略表现就是数据读到一半突然全卡住或者CS拉低后无响应。我的处理办法是把HOLD直接上拉到3.3V绝对不让它浮空。WP是写保护引脚拉低后会禁止写状态寄存器但不会禁止写入主存储阵列。一般应用里我也直接上拉但要记住如果你后续要改状态寄存器WP必须为高。2.3 上电前我习惯做的三项检查这里分享一个经验不要一上电就急着写代码先把硬件检查做透。第一量一下MRAM的VDD对地电阻如果接近短路多半是芯片焊反或贴歪了。第二确认四根SPI线没有接错尤其是SDI和SDO用万用表蜂鸣档顺着线两端量一遍避免PCB上丝印误人。第三用示波器看CS和SCK引脚在模块复位后是不是正常高电平和低频方波如果CS上电就一直是低那MCU固件可能把它配置成输出了错误电平。这三项检查看起来基础但能让你后面调驱动时少掉一半头发。我见过太多人拿着一个接反线的板子一遍遍查代码查驱动最后才发现是硬件问题。3. PIC32MX695F512L侧SPI驱动实现3.1 先把SPI1主模式初始化写对PIC32MX695F512L的SPI模块是寄存器控制的不用库也能很容易配好。我最常用的写法是直接操作SPI1CON、SPI1STAT、SPI1BRG和SPI1BUF四个寄存器。初始化代码如下// 假设外设时钟FPB 40MHz // SCK频率 FPB / (2 * (SPI1BRG 1)) // 想要10MHzSPI1BRG 40 / (2*10) - 1 1 void spi1_init_mram(void) { SPI1CON 0; // 先关闭模块 SPI1STAT 0; SPI1CONbits.MSTEN 1; // 主模式 SPI1CONbits.CKP 0; // SPI Mode 0空闲时SCK为低 SPI1CONbits.CKE 1; // 数据在SCK上升沿采样配合CKP0就是Mode 0 SPI1CONbits.MODE16 0; // 8位数据格式 SPI1CONbits.MODE32 0; SPI1BRG 1; // 10MHz见上面的公式 SPI1STATbits.SPIROV 0; // 清溢出标志 SPI1CONbits.ON 1; // 启动 }这里的SPI模式很关键。MR25H40CDF支持Mode 0和Mode 3我自己习惯用Mode 0也就是CPOL0、CPHA0。在Microchip的寄存器里CKP0且CKE1对应的就是Mode 0。如果你习惯用Mode 3则需要CKP1、CKE1。选择哪个模式没有对错但对采样沿的理解必须一致否则数据会错位半个周期。通行的做法是先把SPI1BRG设成一个大值比如99用低速把链路跑通确认读ID或读状态正常后再把频率提上来。我最终定的10MHz是经过抗干扰测试的如果你板子布线很随意建议先降到2MHz看看稳定性。3.2 底层收发函数一个就够SPI是同步全双工协议发一个字节同时收到一个字节。所以底层只要写一个函数uint8_t spi1_exchange(uint8_t tx) { SPI1STATbits.SPIROV 0; // 清溢出 while (!SPI1STATbits.SPITBE); // 等待发送缓冲空 SPI1BUF tx; // 写入要发的内容 while (!SPI1STATbits.SPIRBF); // 等待接收缓冲满 return SPI1BUF; // 读回数据 }这里提醒一句很多PIC32启动时会偶发SPI溢出就是SPIROV置1如果不清除后续接收可能错位。所以我在每次传输前清一次溢出标志这个习惯一直保留着。CS的控制独立于SPI模块我用两个宏#define MRAM_CS_L() LATAbits.LATA0 0 #define MRAM_CS_H() LATAbits.LATA0 1CS拉低表示选中芯片拉高表示释放。封装所有MRAM操作时必须保证CS低电平的窗口完全覆盖命令字地址数据整个传输过程这是后面所有调试的基础。3.3 封装MRAM的基础命令MRAM的指令集和SPI NOR Flash很像但要注意它没有读ID的标准指令具体能不能用0x9F要查手册我用得最多的只有WREN、WRDI、RDSR、READ、WRITE。写使能和读状态最简单void mram_write_enable(void) { MRAM_CS_L(); spi1_exchange(0x06); // WREN MRAM_CS_H(); } uint8_t mram_read_status(void) { uint8_t st; MRAM_CS_L(); spi1_exchange(0x05); // RDSR st spi1_exchange(0x00); // 时钟出数据 MRAM_CS_H(); return st; }注意WREN执行完之后CS必须先拉高再拉低下一次操作才能生效。这是标准的SPI时序要求如果你连续拉低CS发WREN再发WRITE芯片会认为WRITE被吞掉了。这个现象在逻辑分析仪上会看得特别清楚两条命令之间没有CS高脉冲。执行写操作时一定要先WREN然后再发WRITE命令否则芯片直接忽略写入。下面是把任意长度数据写到指定地址的函数void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { mram_write_enable(); MRAM_CS_L(); spi1_exchange(0x02); // WRITE spi1_exchange((addr 16) 0xFF); spi1_exchange((addr 8) 0xFF); spi1_exchange(addr 0xFF); for (uint32_t i 0; i len; i) { spi1_exchange(buf[i]); } MRAM_CS_H(); }读函数和写函数几乎对称void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_L(); spi1_exchange(0x03); // READ spi1_exchange((addr 16) 0xFF); spi1_exchange((addr 8) 0xFF); spi1_exchange(addr 0xFF); for (uint32_t i 0; i len; i) { buf[i] spi1_exchange(0x00); } MRAM_CS_H(); }看到没有没有擦除操作也没有页大小限制更不用等忙状态。因为MRAM写入是字节级覆盖突发写可以一直写到地址末尾然后自动回绕到0。这个特性后面设计记录存储时会带来巨大便利。3.4 先跑通自检别急着上应用我第一次拿到新板子不会直接跑业务代码而是先做一个存储模块自检。流程很简单在地址0x00000处写入一串特征值0xA5、0x5A、0x3C、0xC3然后读回来比对再把CS引脚拉高拉低几次重新读确认数据依然正确。这一步通过后再全片扫描一遍读写一致性。扫描方法先写0x55再读回再写0xAA再读回如果都通过驱动的正确性就有底了。我建议把这个自检函数留在固件里通过UART命令手动触发以后现场怀疑存储异常时直接远程触发自检比拆机换芯片靠谱得多。4. 数据怎么存、怎么读完整协议与实现4.1 数据帧格式先想清楚再动手驱动层通了之后真正的业务设计才开始。工业现场的数据存储不能像写草稿一样随手写要有明确的帧结构方便崩溃后扫描、恢复、定位。我设计一个固定长度的记录帧32字节。#define RECORD_SIZE 32 #define RECORD_MAGIC 0xA55A typedef struct { uint16_t magic; // 帧头固定0xA55A uint16_t len; // 有效数据长度 uint32_t timestamp; // 时间戳 uint16_t crc16; // 校验 uint8_t data[24]; // 业务数据 } record_t;帧头的作用是快速判断这个位置是不是一条有效记录。时间戳是现场排查故障的关键我用设备主时钟的秒计数即使掉电也能通过外部RTC恢复。CRC16我建议覆盖从magic到data的全部字节这样能检测到帧内容被篡改或部分写入。业务data区可以放Modbus读回来的电流、温度、转速等字段也可以放设备告警码。固定长度的好处是地址计算极其简单一条记录占32字节第N条记录的起始地址就是 BASE_ADDR N * 32不需要链表不需要索引表任何一个地址上都能独立判断这里是不是一条完整记录。4.2 写一条记录从内存到MRAM的关键一步我规划的存储区布局是这样MRAM起始地址0x00000开始偏移0x100放一个元信息区保存最新的记录序号、记录总数、存储区初始化标记0x00100之后是记录区容量约510KB可以放大约16320条记录。实际容量按RECORD_SIZE和总容量算一下心里有数。写一条记录的流程如下void storage_append_record(const record_t *rec) { uint32_t addr STORAGE_BASE (g_next_index * RECORD_SIZE); // 先写数据帧最后一个字节不写 mram_write_bytes(addr, (const uint8_t *)rec, RECORD_SIZE - 1); // 最后写帧尾的有效性标记二阶段提交 uint8_t valid 0x01; mram_write_bytes(addr RECORD_SIZE - 1, valid, 1); // 更新元信息区的当前索引 g_next_index (g_next_index 1) % MAX_RECORD_COUNT; mram_write_bytes(META_INDEX_ADDR, (const uint8_t *)g_next_index, sizeof(g_next_index)); }这里有个重要的设计思想二阶段提交。先写整条记录最后再写有效性标记字节。如果掉电发生在数据写到一半时最后那个标记字节是0x00重启后扫描程序就能判断这条记录不完整从而丢弃。因为MRAM没有擦除等待窗口两阶段提交的失败概率极低比Flash方案稳得多。当然g_next_index建议在系统启动时从元信息区恢复而不是每次开机从0开始。恢复逻辑很简单读META_INDEX_ADDR处的值如果数值范围合法就恢复不合法就扫描整个记录区找最后的有效记录。4.3 读数据从MRAM找历史并不难读取的逻辑比写入更依赖帧结构。我通常会实现一个游标式读取函数让上层可以逐条读取记录uint8_t storage_read_record(uint32_t seq, record_t *rec) { uint32_t addr STORAGE_BASE (seq * RECORD_SIZE); mram_read_bytes(addr, (uint8_t *)rec, RECORD_SIZE); if (rec-magic ! RECORD_MAGIC) { return 0; // 不是有效记录 } uint16_t crc crc16_compute((const uint8_t *)rec, RECORD_SIZE - 2); if (crc ! rec-crc16) { return 0; // 校验失败 } return 1; }上层遍历时从元信息区拿到当前索引从当前索引往回数N条逐条打印或上传。因为MRAM读操作不消耗寿命所以你可以放心反复读它一千遍一万遍。关于输出接口我一般通过UART直接打印成文本一条记录一行配合上位机脚本解析。你完全可以写成JSON格式通过以太网或者串口对接上位机。数据来源如果是Modbus和OPC UA那这一步就是把原始协议数据封装进data区解析工作交给上位机MCU只负责存取职责单一不容易乱。4.4 存储边界和回绕最容易写错的地方MRAM的地址空间是线性的突发写到0x7FFFF之后会自动回绕到0。这个特性听起来方便但如果你一条记录恰好跨越边界读的时候还按线性地址读就会读到错误数据。所以我的存储区规划始终遵循一个原则任何一条记录都不能跨越存储区边界。具体做法是在append时判断if ((addr RECORD_SIZE) MRAM_SIZE) { // 直接回绕到存储区起始位置 g_next_index 0; addr STORAGE_BASE; }这种回绕式的环形记录区让MRAM可以被当成一个永不磨损的环形缓冲。多年运行下来它会覆盖旧数据保留最近几千条记录用来做故障现场还原非常合适。5. 现场调试验证还有那些坑5.1 写入后读回全0xFF先从模式怀疑起这个现象我遇到最多。第一次上电读出来的全是0xFF写什么都写不进去。碰上这种情况先别怀疑芯片先依次检查四件事第一CS是不是真的拉低了示波器直接量CS引脚在操作期间的波形。第二SPI模式对不对如果CKP和CKE组合错了芯片也可能认为数据全是无效的。第三WP引脚是否被拉低虽然WP不保护主存储区但某些状态组合下还是可能影响操作。第四写使能有没有发RDSR读回来如果WEL位为0说明WREN没生效或WREN和WRITE之间CS没有拉高。我建议把这四个检查项做成一个固定的排查顺序比盲改代码快得多。5.2 SPI线序错误SDO和SDI接反还有一种很隐蔽的问题命令发出去了状态寄存器也能读但读出来的数据全是一些规律性的错误值。如果逻辑分析仪显示命令波形是对的、MISO上也有数据但数据位像是左右镜像或者错位那多半是SDO和SDI接反。SDI对主设备而言是输出对从设备是输入SDO对主设备是输入对从设备是输出。很多初学者看到SD两个字母就想当然地对接这个坑我印象很深。5.3 CS低电平窗口不对导致命令被吞CS波形如果和SCK没有完全对齐最典型的就是WREN和WRITE之间没有插入CS高脉冲芯片会直接不吃后面的WRITE。正确时序是CS拉低发WRENCS拉高CS拉低发WRITE地址数据CS拉高。不要自作聪明地把WREN的CS高电平省掉。同样READ命令的CS窗口如果提前拉高数据可能会少读一个字节后续所有记录的位置全部错位。我调试时喜欢用逻辑分析仪抓一段CS和SCK的波形肉眼确认每个命令之间都有完整的CS高脉冲这一步能省很多猜测时间。5.4 关于耐久性和数据保持的实测结论我用自动写循环跑过MRAM的耐久性测试每秒写一条32字节记录连续跑了一个星期大约写了60万条记录再检查读写一致性没有出现任何错误。60万次对MRAM来说连毛毛雨都不算按这个速率跑几十年完全没问题。这就是我在项目里敢直接做环形覆盖写的原因换作NOR Flash这种跑法一周就能把芯片擦写寿命耗尽。数据保持这块我没有办法现场等20年但按照Everspin的官方标称在工业级温度范围内数据保持超过20年。实际项目里我也做过高温老化后重新上电读数据数据完好。对工业设备来说这个指标已经足够安心。5.5 常见问题速查表现象最可能的原因排查/解决读回全是0xFFSPI模式错误/CS未拉低/芯片未供电先查CS波形再核对CKP/CKE组合只有部分字节正确SDO和SDI接反用万用表按引脚定义逐一核对线序写完立即读偶尔不对SPI溢出标志未清除每次传输前清SPIROVWREN不生效WREN和WRITE之间CS没有拉高检查时序保证两条命令间有CS高脉冲读到的记录帧头对但CRC错记录跨地址回绕边界append前判断是否越过MRAM_SIZE强制回绕高速率下偶发错误导线过长/电源纹波大降到2MHz验证加0.1uF和10uF电容存储区数据乱套未正确恢复g_next_index启动时从META_INDEX_ADDR恢复索引我个人在实际操作中体会最深的一条是驱动代码其实半小时就能写完真正决定这个存储系统靠不靠谱的是记录格式和掉电恢复策略。MRAM把能不能写坏这个物理层面的担忧彻底清除了平台就把精力都放在数据怎么组织才不会乱上。最后再分享一个小技巧。如果你的MCU支持多路SPI我建议给MRAM单独分一路SPI不要和SD卡、显示屏、CAN控制器混用。原因很简单MRAM是高速高可靠性数据存储其他外设一旦在总线上拉起长时序或者被中断打断都可能影响存储操作的连贯性。给它一个独占的SPI总线你会发现写驱动和排障都清爽很多。后续如果你想扩展还可以在这个框架上做多区存储比如参数区用MRAM、大块历史数据用SD卡两者互不干扰这也是工业设备常用的混合存储架构。
返回列表