多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PIC18与MRAM组合:实现工业级高可靠SPI数据存储

PIC18与MRAM组合:实现工业级高可靠SPI数据存储 工业现场的数据存储说起来是个老话题但每次真正做到产品里都会踩出新问题。最近在帮朋友调试一台设备的状态记录模块主控用的是Microchip的PIC18LF45K22存储芯片选了Everspin的MR25H40CDF一颗4Mbit的工业级SPI接口MRAM。这个组合不算新奇但如果你既要频繁写报文又要求断电之后数据还在还要能扛住车间里的温差和电源毛刺它确实是个非常能打的方案。这篇文章就围绕这套组合讲清楚为什么挑它们、怎么接线、怎么写驱动以及我在实际调试中掉过的坑。想做个可靠的数据记录单元、需要频繁掉电保存参数、或者准备把老设计里的EEPROM换成更耐写的方案都可以参考这里的思路。1. 方案选型为什么偏偏是这颗MRAM配这颗MCU1.1 工业场景对存储芯片的三个硬性要求先说需求。工业设备里的数据存储不是电脑上存个文件那么简单。比如设备运行日志现场要求每100毫秒记录一条运行状态包含电流、温度、报警码又比如工艺参数表用户可能随时通过触摸屏改配方改完必须立刻保存谁也不想设备断电一次配方就丢了。这类场景会同时提三个硬性要求第一写入次数高一天下来几万次甚至几十万次很正常第二掉电不丢这是底线第三工作温度宽车间里夏天40多度冬天没暖气的地方零下十几度芯片不能掉链子。传统方案里SPI NOR Flash便宜、容量大但致命问题是写入要擦除。磨擦寿命一般标称十万次按高频率写入的日志场景用不了多久就到寿了。EEPROM虽然能按字节写但写速慢容量也小存稍微长一点的报文就捉襟见肘。我早年做过一个仪表项目用SPI Flash存事件记录刚交付时一切正常三个月后现场开始频繁报存储错误。查到最后就是Flash磨损问题只能改软件做均衡还得跑到现场给几十台设备升级固件。那次之后凡是高频写入的场景我都会优先考虑MRAM。1.2 MRAM、NOR Flash、EEPROM的对比MRAM磁阻随机存取存储器把前面两个方案的短板基本都补上了。它用磁阻状态存储数据掉电不丢写入不需要擦除也没有写寿命压力。同样是SPI接口指令集和NOR Flash高度相似但数据手册承诺的写耐久是10^14次量级。下面这个表是我做选型时经常拿出来的对比维度MRAMSPI NOR FlashEEPROM写前擦除不需要必须先擦除扇区可字节写但结构复杂写入速度纳秒级毫秒级毫秒级典型写寿命10^14次约10万次约100万次容量范围几百Kbit到几十MbitMbit到Gbit级别几Kbit到几百Kbit断电保持有有有单位成本偏高低低从表格能看出来MRAM就是为“写得多、要求高可靠”的场景准备的。价格上它确实比Flash贵但如果因为Flash寿命问题导致现场返修那点成本差早就被差旅费淹没了。1.3 PIC18LF45K22的适配性主控这边选PIC18LF45K22有几个很实际的理由。它是PIC18系列的中端型号自带MSSP模块跑SPI主模式轻轻松松数据手册支持2.0V到3.6V范围和MRAM的3.3V电源域干脆统一省了电平转换芯片工业级温度范围能满足大多数设备环境。对很多做运控、仪表、能源监测的老工程师来说这套组合不需要学新架构用MPLAB X XC8就能上手。更关键的一点是PIC18LF45K22脚下带足够的通用GPIO单独拉一个脚做片选很轻松。SPI总线上的从机不多的时候片选用普通IO比硬件SS脚灵活多了后面要扩展第二个存储或者传感器也不会被引脚绑死。有人可能问为什么不用STM32那当然可以但我这里强调的是稳定和简单PIC18LF45K22的SPI模块逻辑非常直白代码里不需要处理DMA、中断优先级这些东西一个循环收发函数就能把读写搞利索对于工业小设备反而更容易维护。2. 硬件连接与引脚细节接线图与三处最容易被忽略的引脚2.1 接线表与GPIO分配MR25H40CDF是8脚封装引脚不算多但每一根都别想当然。以PIC18LF45K22的MSSP1外设为例建议这样接功能PIC18LF45K22引脚MR25H40CDF引脚说明SPI时钟RC3/SCK1SCK时钟线主出从入RC5/SDO1SIMCU输出到MRAM主入从出RC4/SDI1SOMRAM输出到MCU片选RA3任意GPIOCS#低电平有效电源3.3VVDD供电注意去耦地GNDVSS共地写保护3.3V或GPIOWP#接高禁用保护挂起控制3.3VHOLD#必须接高这里我给出的片选是RA3单纯是因为这个脚平时空着。你也可以换成任何没被占用的输出脚但要注意片选脚在MCU复位期间必须保持高电平否则复位瞬间的IO状态抖动可能会误触发MRAM的指令周期。选引脚时尽量避开默认上拉或默认模拟功能的脚。2.2 两个必须处理的引脚HOLD#和WP#第一次画板子的人最容易翻车的地方就是MRAM的HOLD#引脚。这个脚的作用是暂停通信如果直接悬空现场走线长了之后容易被噪声拉低芯片偶尔就“冻结”了表现为主机发指令没人理读出来全是0xFF。我的习惯是直接把它接在3.3V上简单粗暴彻底消除隐患。有些设计为了省电想用HOLD#挂起通信但在工业设备里收益很低没必要为了这点功耗增加故障点。WP#引脚是硬件写保护。接3.3V时写保护不生效SPI指令可以正常改写数据如果你想防止误写可以把它接到一个GPIO上平时拉高只有在需要写入时才拉低。工业现场如果怕干扰导致误写用GPIO控制WP#会更稳代价是多占一个IO脚、多几句驱动代码。不过这里要注意WP#的时序和SPI指令是配合的如果改成GPIO控制务必要先解除WP#再发WREN指令顺序反了数据一样写不进去。HOLD#和WP#的处理不要省这是MRAM能不能在现场稳定跑起来的关键。我记得第一次画这块电路的时候偷懒把HOLD#悬空实验室单板测试完全正常一装到设备上就间歇性失灵查了一整天最后用示波器抓到HOLD#引脚上的噪声毛刺才知道自己被坑了。2.3 电源与PCB布局上的注意点MRAM虽然是存储芯片但它的内部存取机制和SRAM类似电源上瞬间的电流变化比EEPROM要明显。VDD和VSS之间并联0.1μF陶瓷电容尽量靠近芯片引脚放置。如果PCB走线特别长再加一个4.7μF的电解电容做低频滤波。SPI信号线不要拉太长尤其不要让SCK和SO/SI两根线平行走很远不然现场变频器、继电器一动作干扰串进去就出邪门问题。一般建议SPI线上加33Ω到100Ω的串阻靠近主控引脚放能有效抑制振铃。片选CS的布线也要注意尽量短避免上电瞬间的毛刺误触发写入序列。另外MR25H40CDF的工作电压范围和PIC18LF45K22的LF版本都是3.3V友好型。如果主控用5V版本就需要加电平转换芯片或者用电阻分压方案这会让时序变得复杂。我的建议是这种组合统一用3.3V供电省心得多。3. SPI协议与驱动代码模式选择、指令时序和读写实现3.1 指令集与状态寄存器MR25H40CDF的SPI指令和常见的SPI NOR Flash很像没有擦除指令这对从Flash转过来的人是个大利好。常用指令就这几个指令代码说明WREN0x06写使能写命令前必须执行WRDI0x04写禁止RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据WRITE0x02写数据状态寄存器里最重要的两位是WIPBit0和WELBit1。WIP表示内部是否忙WEL表示写使能是否打开。要注意MRAM虽然写入很快但状态寄存器仍然保留了WIP位写命令后最好还是读一下确认芯片完成内部操作。MRAM的读取操作比Flash省心在一点它没有“页读”的概念也没有“连续读跨页必须重新发命令”的限制只要CS一直拉低你就可以沿着地址连续读下去读多少字节都可以读到地址尽头会自动回卷。这对日志读取和固件拷贝之类的操作特别友好。3.2 SPI模式选择Mode 0还是Mode 3MR25H40CDF数据手册支持SPI Mode 0和Mode 3这两种模式在工业级芯片里最常见。Mode 0是空闲时时钟为低、数据在上升沿采样Mode 3是空闲时时钟为高、数据在上升沿采样。只要主从两边配置一致就行没有哪个更优的说法。在PIC18LF45K22的MSSP模块里Mode 0通常这样配对CKP0空闲时钟低、CKE1。如果要用Mode 3把CKP改成1即可。重点不是记配置值而是确认你写的每一条SPI通信时序图跟数据手册上的示例一致否则读回来的数据就像打乱的拼图时好时坏。这里多说一句SPI调试的时候不要总盯着寄存器理论值有条件就上逻辑分析仪抓一下SCK和MOSI的波形。我遇到过有人把Mode 0和Mode 3搞反了改代码改了两天没发现最后抓波形才看出来CS下降沿之后数据位的对齐方式整个是反的。3.3 驱动代码初始化、读写、状态等待下面是我在工程里实际跑过的一套精简驱动用XC8编译器。先做初始化和底层字节收发#include xc.h #define MRAM_CS_TRIS TRISAbits.TRISA3 #define MRAM_CS_LAT LATAbits.LATA3 void spi_master_init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC5 0; // SDO 输出 TRISCbits.TRISC4 1; // SDI 输入 MRAM_CS_TRIS 0; MRAM_CS_LAT 1; // CS 空闲拉高避免上电误触发 SSP1STAT 0x40; // CKE1, SMP0对应 SPI Mode 0 SSP1CON1 0x20; // SSPEN1, CKP0, Master FOSC/4 } uint8_t mram_spi_byte(uint8_t data) { SSP1BUF data; while (!PIR1bits.SSP1IF) {} PIR1bits.SSP1IF 0; return SSP1BUF; }注意初始化里有一行很不起眼但很重要的代码先把CS配置成输出并拉高。如果在SPI模块启动之前CS悬空或者处于低电平MRAM可能把上电噪声当成片选信号执行一个错误的命令序列。这个细节我踩过后面排查汇总里会再提。然后读写状态寄存器和写使能uint8_t mram_read_status(void) { uint8_t status; MRAM_CS_LAT 0; mram_spi_byte(0x05); status mram_spi_byte(0x00); MRAM_CS_LAT 1; return status; } void mram_write_enable(void) { MRAM_CS_LAT 0; mram_spi_byte(0x06); // WREN MRAM_CS_LAT 1; } void mram_wait_busy(void) { while (mram_read_status() 0x01) {} }写使能有个坑WREN和后续的WRITE命令之间CS必须重新拉高一次形成一个完整的命令周期。有的新手图省事把WREN和WRITE连在一个CS低电平里发结果WEL位根本没起效数据写不进去。读取单字节和连续读取uint8_t mram_read_byte(uint32_t addr) { uint8_t val; MRAM_CS_LAT 0; mram_spi_byte(0x03); // READ mram_spi_byte((uint8_t)(addr 16)); mram_spi_byte((uint8_t)(addr 8)); mram_spi_byte((uint8_t)(addr)); val mram_spi_byte(0x00); MRAM_CS_LAT 1; return val; } void mram_read_bytes(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; MRAM_CS_LAT 0; mram_spi_byte(0x03); mram_spi_byte((uint8_t)(addr 16)); mram_spi_byte((uint8_t)(addr 8)); mram_spi_byte((uint8_t)(addr)); for (i 0; i len; i) { buf[i] mram_spi_byte(0x00); } MRAM_CS_LAT 1; }写入函数支持单字节和最多32字节的连续写void mram_write_page(uint32_t addr, uint8_t *buf, uint8_t len) { uint8_t i; if (len 0 || len 32) return; if (addr 0x80000u) return; // 4Mbit 512KB mram_write_enable(); MRAM_CS_LAT 0; mram_spi_byte(0x02); // WRITE mram_spi_byte((uint8_t)(addr 16)); mram_spi_byte((uint8_t)(addr 8)); mram_spi_byte((uint8_t)(addr)); for (i 0; i len; i) { mram_spi_byte(buf[i]); } MRAM_CS_LAT 1; mram_wait_busy(); } void mram_write_byte(uint32_t addr, uint8_t data) { mram_write_page(addr, data, 1); }3.4 写入与读出的时序说明这套驱动的关键就一句话CS低电平代表一次指令周期的开始CS高电平代表结束。READ和WRITE都是先发一个命令字节再发三个地址字节然后根据方向收发数据。地址字节的第3个字节是高字节对512KB容量的MRAM来说最高地址是0x7FFFF超过这个范围的读写在硬件上会环绕回低地址这是新手最容易写炸的地方。很多资料会说MRAM写入速度极快不需要等状态寄存器。但严谨的做法还是写完之后读一下WIP位。我实际测试过绝大多数情况下写完立刻读WIP已经是0但如果在写入期间正好来了中断主循环调度晚了也不影响功能只是多花几微秒而已。留着这个等待逻辑反而能在调试时用来确认每次写操作是否有异常。还有一个细节MR25H40CDF带有32字节的页写缓冲区。你可以连续写32字节也可以一次只写1字节都没有问题。写操作和Flash最大的不同是Flash的连续写不能跨页边界MRAM没有这个限制因为底层不需要页缓存加编程。把MRAM当作一块掉电不丢的SRAM来理解很多时序上的顾虑就消失了。4. 工业应用中的数据读写策略日志、参数和掉电保存4.1 典型场景运行日志环形缓冲如果要在MCU里存环形日志很多人的第一反应是“象征性”写一个缓冲区管理。传统Flash方案里日志老手会做扇区擦除调度、磨损均衡、掉电恢复代码量能写出一本书。换成MRAM之后这些都可以大幅简化。环形缓冲的常见做法是开头固定区域存“头指针”和“尾指针”后面区域顺序写日志条目。每写一条更新指针。对于Flash指针更新频繁会引起擦写寿命问题所以还得专门给指针做损耗均衡。MRAM这里不需要指针区域可以随便写头尾指针每100ms更新一次十年都不带怕的。有同事问我MRAM写入这么快是不是可以把日志缓冲直接做到MRAM里连RAM缓冲都省了理论上可以但我建议还是保留一小段RAM缓冲攒够一批再写。这样做的原因不是MRAM不够快而是减少SPI总线上的交互次数给其他外设留出带宽也让代码结构更清晰。4.2 掉电数据保存与上电恢复MRAM自己不掉电不丢数据但“掉电保存”这四个字真正的难点在MCU侧电压掉下去了程序还能不能及时执行一次完整写入我的经验是掉电保存别指望掉电那一瞬间才动手。真正的工程做法是分两层平时周期性保存关键参数掉电时只需保存极少量紧急数据。如果确实要在掉电瞬间写日志硬件上要在VDD输入端留足够的储能电容让电压从正常降到MCU复位阈值之间有几百微秒到几毫秒的窗口。软件上用PIC的欠压检测机制做中断触发在窗口内完成写操作。写完以后不要等立刻进入低功耗或循环等待复位避免半执行状态。这里有个特别实际的建议掉电保存的代码路径要和正常写路径解耦。不要在掉电中断里调用复杂的文件系统或者多层函数直接操作寄存器把关键数据拼成固定格式一次性写入。我见过太多人想在掉电时调用通用读写函数结果函数还没执行到关键步骤电压已经跌没了。上电恢复时要做一次完整的有效性检查。MRAM虽然掉电不丢但不代表写入的数据一定是完整的如果写入过程中发生了掉电尾部的数据可能有残缺。我的做法是每条记录都带CRC和序号上电时从环形缓冲头部开始扫描丢弃CRC错误或序号不连续的记录这样即使掉电写入中断也不会把损坏的数据当成真实事件报出去。4.3 磨损均衡要不要做很多从Flash转过来的人会惯性问一句MRAM要不要做磨损均衡答案很简单正常设备生命周期内不需要。10^14次写寿命是什么概念按每毫秒写1次算要连续写三千多年。所以那些Flash时代的分区轮换、垃圾回收、掉电映射表全都可以砍掉。这是我推荐MRAM最核心的原因之一不是因为它性能多夸张而是它把软件的复杂度实实在在降下来了。当然不做磨损均衡不代表可以胡来。写保护还是要留的尤其初始化代码里要把关键参数区的写权限管住防住程序跑飞后对MRAM的疯狂乱写。比如把WP#引脚接到GPIO平时上锁只有在固件升级或参数写入时解锁几毫秒这比纯软件靠状态寄存器保险得多。新产品如果前期只用了一半容量我会故意把日志区域分散在低位和高位两个区不是为了磨损均衡而是为了后续升级固件时预留空间万一新增了日志类型不用大改地址映射。5. 调试中踩过的坑与疑难排查现场翻车实录5.1 读回数据全0xFF或偶发失败的经典原因第一次拿到板子上电后读MRAM最容易碰到的现象就是读回来全是0xFF。先说第一个元凶HOLD#悬空。芯片的数据手册上HOLD#是低电平有效的挂起引脚悬空状态下只要板子上有一点噪声芯片就进入挂起状态此时芯片不理睬主机的任何命令SO输出保持高阻读出来自然全是0xFF。处理方式我在硬件章节写过直接接3.3V。第二个元凶是SPI模式不匹配。如果初始化时CKP配错了SCK空闲电平正好相反芯片判读数据的边沿全错读回的数据就是乱的。用示波器对比一下SCK波形和数据手册的时序图一眼就能看出来。第三个原因比较隐蔽读写线序接反。SI和SO如果接反主机发出的命令根本进不了芯片但读的时候可能仍然能读到一串数据因为SO引脚被SDI输入端的内部结构“带动”了。这种问题不是每次都能复现建议焊接前先仔细核对封装引脚顺序DFN封装的引脚排列有时候和SOIC习惯不一样。5.2 写命令执行了但数据不变还有种情况让人更头疼读数据正常写命令也发了但写完之后读回来还是旧数据。先查WREN时序很多工程会犯的错是WREN之后CS没有拉高或者WREN和WRITE之间隔了太多其他操作导致WEL位被自动清除。正确时序是CS拉低、发0x06、CS拉高、CS拉低、发0x02、地址、数据、CS拉高。再查WP#引脚和状态寄存器的BP位。如果状态寄存器里有块保护位被误置位即使WEL正确也会拒绝写入。调试时先读一次状态寄存器看看WIP和WEL到底是什么状态比盲猜快得多。还有一次遇到更奇怪的芯片能写能读但写进去的数据会在断电后变成0xFF。查了半天发现是PCB上把MRAM的供电脚和一个大功率继电器的线圈电源画在了同一个网络继电器吸合瞬间把VDD拉到了2V以下芯片内部的数据被破坏了。这说明MRAM虽然本身可靠性高但供电质量依然是高压故障的根源别让大电流负载和存储芯片共用同一路电源。5.3 排查速查表与现场验证建议我把这段时间遇到的问题整理成一个速查表给大家现场排查用现象可能原因检查与处理读回数据全0xFFHOLD#悬空或SCK极性不对HOLD#接VDD核对CKP/CKE配置写入不生效WREN时序不对或WP#被拉低确认WREN后CS先拉高WP#接3.3V或由GPIO正确控制写入后数据漂移地址越界或SI/SO接反核对地址是否小于0x80000检查接线顺序偶发通信失败电源纹波大或SPI线过长优化去耦电容SPI线串电阻缩短走线高温下偶尔丢数据电源跌落或接口噪声使用示波器抓VDD跌落加储能电容掉电后数据异常写入过程中电源跌落加CRC校验关键数据双写两份现场验证有个实用小技巧写一个自检函数往固定地址交替写0x5A和0xA5每写一次读一次连续跑几千遍如果中途有任何一次不一致说明硬件有问题。这个办法比任何仪器都直观因为它是从软件层验证整条SPI链路和芯片状态的真实验证。如果自检能稳定通过但实际运行还是偶发问题建议在驱动里加一个临时调试模式把每次SPI收发的字节全部通过串口打印出来。跑一次现场故障模拟看日志里最后一次通信在哪个指令上断掉往往比在示波器前蹲半天管用。最后再聊一点个人心得。MR25H40CDF和PIC18LF45K22这套组合我在好几台设备上都跑过最放心的不是它的速度而是省心。你不需要像伺候Flash那样伺候擦除周期和磨损均衡也不需要像EEPROM一样抠容量写代码的时候把它当成一块掉电不丢的SRAM来用就行了。如果你手头正好在纠结日志存储、参数掉电保存或者被SPI NOR Flash的寿命坑过试一次这个组合多半会有一种相见恨晚的感觉。
返回列表