ARTICLE DETAIL

资讯详情

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

工业存储新方案:MRAM替换SPI Flash的可靠读写设计与PIC18实现

工业存储新方案:MRAM替换SPI Flash的可靠读写设计与PIC18实现 之前有朋友问我工业设备里做数据存储为什么老方案用 SPI Flash 总会在运行一两年后出问题其实答案就一句话——绝大多数 Flash 的擦写寿命和掉电保护能力撑不起工业现场那种高频、不规律、说断就断的写入场景。后来我在一版新方案里把存储介质换成 MR25H40CDF主控搭 PIC18LF45K40把“存储和读取数据”这条完整链路从头到尾调通跑了小半年的高低温循环和频繁断电测试现在把这套做法的原理、电路、代码和踩过的坑整理成文。这篇内容适合正在做工业控制器、数据记录仪、电池供电设备或者被 Flash 磨损问题折磨过的嵌入式工程师参考。1. 这个组合真正解决的是“反复可靠地存”不是“能存”1.1 工业存储场景下 Flash 的账面短板先说清楚问题从哪来。工业设备里最常见的存储芯片无非三类EEPROM、SPI NOR Flash、以及带电池的 SRAM。EEPROM 接口简单但容量普遍不大几百字节到几兆位写寿命虽然在数据手册里写着几十万次实际在高温和电压波动下会严重缩水SPI NOR Flash 容量大、便宜但架构上有三个原生缺点擦除块机制Flash 写入前必须先擦除擦除以扇区通常 4KB 或 64KB为单位。你只想改一个字节也得触发整个扇区的擦写流程这对频繁小数据记录非常不友好。写寿命有限消费级 NOR Flash 标称 10 万次擦写工业级也大多在 10 万次量级。按设备每小时记录 10 条数据算一年就是接近 9 万次一块芯片一年就到寿命边缘。掉电损坏风险如果正在擦除或编程时系统断电轻则当前扇区数据错乱重则造成坏块。要处理这种情况软件层必须引入掉电恢复、坏块管理和磨损均衡代码复杂度直线上升。带电池的 SRAM 倒是读写寿命无限但电池本身在工业高温环境里就是损耗件换成可充电电容保持时间又有限而且掉电后数据保持时间和系统断电时长之间永远是笔算不清的账。1.2 MRAM 用物理特性绕开了这些问题MR25H40CDF 是 Everspin 的 4Mbit512KBSPI 接口 MRAM核心存储单元是磁隧道结利用磁阻效应存储数据而不是靠电荷。这个物理机制带来几个 Flash 完全给不了的好处写入不需要擦除任何字节地址可以随时改写写 0 和写 1 对称没有“先擦后写”的概念。擦写耐久标称到 1e14 次工程上基本等于无限次。以前总担心“这块 Flash 还剩多少寿命”用 MRAM 之后彻底不用想这事了。掉电后数据自动保持不需要电池保持时间按年来算工业级温度下标称可到几十年的数据保持能力。写入操作本身在物理层就是即时完成的没有内部编程等待时间CS 拉高即代表该次写入生效不需要查询忙状态。用一句话总结MRAM 在软件接口上长得像 SPI Flash但行为上更像 SRAM只是这个 SRAM 断电不掉数。1.3 主控为什么选 PIC18LF45K40PIC18LF45K40 是 Microchip 的 8 位低功耗 MCU40/44 引脚封装工作电压 1.8V 到 3.6V。选它有几个具体考虑带 MSSP 硬件 SPI 外设走 SPI 存储芯片最忌讳用 GPIO 模拟时序。K40 系列内置 MSSP 模块配好的移位寄存器和中断标志配合硬件 CS 控制能省掉大量 CPU 开销。外围引脚选择PPSK40 系列的每个数字外设输入/输出都可以重映射到大部分 IO 引脚。画 PCB 的时候可以先布线再分配功能不用为了迁就固定引脚把板子走成蜘蛛网。独立于内核的外设CIP比如窗口看门狗、互补波形发生器、数学加速器这些。工业项目里电源异常检测、定时唤醒这类需求用片内外设做比外部加芯片更省成本。LF 后缀的低功耗能力虽然 MR25H40CDF 最低工作电压在 2.7V 附近LF 版本的 1.8V 能力不能完全发挥但系统里还有其他 1.8V 传感器时LF 版本给了电源设计更大的余量MRAM 的 3.3V 电源轨独立供给即可。2. 硬件设计引脚、电源和那些容易忽略的上下拉2.1 MR25H40CDF 引脚逐个看这款 MRAM 是标准 8 引脚 SPI 器件引脚功能如下表引脚名称功能硬件处理建议1CS#片选低有效接 MCU IO建议加 10k 上拉2SCKSPI 时钟接 MCU SCK3SI数据输入MOSI接 MCU SDO4SO数据输出MISO接 MCU SDI5WP#写保护低有效不用时必须接 VDD6VSS地直接接系统地7HOLD#保持低有效不用时必须接 VDD8VDD电源 3.3V就近去耦这里特别强调 WP# 和 HOLD#。很多第一次用 MRAM 的人以为这两个引脚和 Flash 一样不接也能跑实际上 HOLD# 一旦悬空总线上的噪声耦合到该引脚时芯片会暂停当前传输表现为通信偶发挂死。WP# 悬空虽然不至于让读写失效但在噪声较大的现场状态寄存器存在被意外修改的隐患。我的建议非常简单粗暴两个引脚各用 10k 电阻上拉到 VDD一劳永逸。2.2 与 PIC18LF45K40 的接线PPS 重映射的灵活性PIC18LF45K40 的 MSSP1 引脚通过 PPS 单元映射到物理引脚。我实际采用的接线是信号MCU 引脚说明CS#RC2普通 GPIO推挽输出SCKRC3映射 MSSP1_SCKSDORC4映射 MSSP1_SDOSDIRC5映射 MSSP1_SDI输入PPS 的好处是如果 RC3/RC4/RC5 被板载调试器或其他功能占用可以改成 RA、RB 端口任意引脚。接线灵活性带来的代价是软件必须完成 PPS 锁定序列这个后面代码部分细说。2.3 电源和 PCB 层面的工业习惯MR25H40CDF 的 VDD 范围是 2.7V 到 3.6V和 PIC18LF45K40 的 3.3V 供电轨可以直接共用一个 LDO。PCB 上有三个细节是我反复踩过的芯片电源引脚附近放 0.1uF 陶瓷电容靠近引脚同一电源域再加 4.7uF 钽电容做低频段去耦。别只放一个 0.1uF工业脉冲群测试时容易复现偶发读写失败。SPI 时钟和数据线上串联 33 欧姆电阻靠近发送端放置抑制走线反射。PCB 走线超过 5cm 时SCK 线最好做包地处理避免和相邻的高速信号耦合。CS# 上拉到 VDD 的电阻在 MCU 复位期间尤其重要。MCU 处于复位状态时 IO 可能是高阻如果没有这个上拉MRAM 的 CS# 电平不确定可能进入非预期的状态寄存器写入模式。3. 软件驱动从 SPI 初始化到连续读写3.1 MR25H40CDF 的指令集与访问模型MR25H40CDF 的 SPI 指令集非常精简最常用的只有 6 条指令操作码功能WREN0x06设置状态寄存器中的 WEL 位写使能WRDI0x04清除 WEL 位禁止写RDSR0x05读状态寄存器WRSR0x01写状态寄存器一般不直接操作READ0x03读数据地址 3 字节可连续读WRITE0x02写数据地址 3 字节可连续写和 SPI Flash 最大的区别在 WRITE 指令的执行模型。Flash 的 WRITE 通常是以页为单位写完还要轮询 BUSY 位MR25H40CDF 则是从指定地址开始连续写入任意长度直到 CS# 拉高为止没有页大小限制、没有擦除等待、也不存在“当前扇区是否被占用”的概念。我从 Flash 迁移过来时代码里那套“页写入函数 页剩余空间判断 扇区擦除调度”的框架直接被砍掉一半。每次写操作前必须发 WREN 指令否则 WRITE 不生效这是 SPI 存储类设备的通用安全机制。即使系统刚上电、状态寄存器默认值未初始化也要先发 WREN养成习惯。3.2 SPI 初始化与 PPS 配置使用 MPLAB X XC8 开发下面是 SPI 初始化的核心代码。注意 PPS 配置的部分K40 系列有锁定机制必须先解锁才能改映射void MRAM_SPI_Init(void) { // 1. 配置 CS 引脚为普通推挽输出默认拉高 TRISCbits.TRISC2 0; LATCbits.LATC2 1; // 2. 配置 SPI 引脚方向SCK/SDO 输出SDI 输入 TRISCbits.TRISC3 0; TRISCbits.TRISC4 0; TRISCbits.TRISC5 1; // 3. 解锁 PPS 寄存器 PPSLOCK 0x55; PPSLOCK 0xAA; PPSLOCKbits.LOCK 0; // 4. 输出映射SCK 和 SDO 信号分配到 RC3/RC4 RC3PPS 0x17; // MSSP1_SCK 的选择码具体值按数据手册 PPS 表确认 RC4PPS 0x15; // MSSP1_SDO 的选择码 // 5. 输入映射SDI 信号从 RC5 输入 SDI1PPS 0x0D; // RC5 对应的输入引脚编号 // 6. 重新锁定 PPS PPSLOCK 0x55; PPSLOCK 0xAA; PPSLOCKbits.LOCK 1; // 7. 配置 MSSP1 为 SPI MasterSPI Mode 0 SSP1CON1bits.SSPEN 0; // 先关模块再配置 SSP1CON1bits.CKP 0; // 时钟空闲电平为低 SSP1CON1bits.CKE 1; // 数据在 SCK 上升沿移位 SSP1STATbits.SMP 1; // 数据在周期末采样 SSP1CON1bits.SSPM 0b0010; // SPI Master, Fosc/64 SSP1ADD 0; SSP1CON1bits.SSPEN 1; // 重新使能模块 }PPS 寄存器的选择码在不同型号之间并不一样上面代码里的 0x17、0x15、0x0D 需要以 PIC18LF45K40 的数据手册 PPS 映射表为准。如果不确定直接在 MPLAB Code ConfiguratorMCC里把引脚连线点出来让工具自动生成这部分配置是更稳妥的做法。我用 MCC 对照过自己手写的配置确认无误后才固化到工程里。3.3 写使能、连续写与连续读下面是三个核心函数。首先是基础的 SPI 收发和写使能uint8_t SPI1_Transfer(uint8_t data) { SSP1BUF data; while (!SSP1STATbits.BF); return SSP1BUF; } void MRAM_WriteEnable(void) { MRAM_CS_LOW(); SPI1_Transfer(0x06); // WREN MRAM_CS_HIGH(); } uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LOW(); SPI1_Transfer(0x05); // RDSR status SPI1_Transfer(0x00); MRAM_CS_HIGH(); return status; }连续写入函数。MRAM 支持任意长度连续写因此函数参数里的 len 可以用到 512KB 全地址空间void MRAM_WriteBytes(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); SPI1_Transfer(0x02); // WRITE SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); for (uint16_t i 0; i len; i) { SPI1_Transfer(buf[i]); } MRAM_CS_HIGH(); }连续读取函数读取过程中不需要写使能void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); SPI1_Transfer(0x03); // READ SPI1_Transfer((addr 16) 0xFF); SPI1_Transfer((addr 8) 0xFF); SPI1_Transfer(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] SPI1_Transfer(0x00); } MRAM_CS_HIGH(); }底层的 SSP1BUF 读写只有两条逻辑线发送时往 SSP1BUF 里填数据等待 BF 位置位表示移位完成接收时 BF 位置位后从 SSP1BUF 里取回数据。BF 标志会同时反映发送完成和接收缓冲可用处理得当就是高效的 8 位全双工传输。3.4 关于“写完立刻拉高 CS”的时序细节这是我从 Flash 迁移到 MRAM 后踩的第一个坑。在 Flash 的例程代码里一个常见的写法是SPI1_Transfer(last_byte); CS_HIGH();在 Flash 上这么写也许能跑但在 MRAM 上很危险。因为 MSSP1 的发送是双缓冲机制SPI1_Transfer 函数返回时最后一个字节可能还在移位寄存器的最末段并没有真正完整地送到 SO 引脚。此时立刻拉高 CS#MRAM 会认为该字节无效结果就是写入的数据最后一个字节总是丢失而且不是每次必现和当时的 SPI 时钟相位、软件调度延迟都有关。正确做法是在拉高 CS# 之前额外确认发送完成。用下面这种写法MRAM_CS_LOW(); SPI1_Transfer(cmd); // ... for (uint16_t i 0; i len; i) { SPI1_Transfer(buf[i]); } while (!SSP1STATbits.BF); // 确保最后一个字节真正移出 while (!SSP1IF); // 确保中断标志反映传输完成 MRAM_CS_HIGH();如果开了 MSSP1 中断这里要特别注意先清 SSP1IF 再拉高 CS。实际我在调试时发现单靠 BF 也有极小概率不够建议 BF 和 SSP1IF 两个条件同时满足再操作 CS。这个细节花了我大半天时间做二进制比对才定位到规律写出来给大家省点工时。4. 为“铁打的数据”设计存储格式与掉电策略4.1 存储布局三个区互不干扰MRAM 有 512KB 空间规划得当是完全够用的。我在项目里把空间分成三块区域地址范围用途配置区0x00000 - 0x0FFFF参数、校准值、设备配置日志区0x10000 - 0x7FFFF循环记录运行数据索引区0x1F000 - 0x1FFFF实际日志区尾部记录最新写入位置配置区的数据特点是改写不频繁、单次数据量小掉电时绝不能出现半新半旧的状态。日志区则是高频写入每一条记录都独立完整。索引区因为 MRAM 支持随机字节写完全可以只在日志区尾部保留若干个最新位置标记每次写记录前更新一次。和 Flash 方案的一个典型差异是我不需要为日志区预留一整块“擦除临时缓冲”。Flash 写日志前如果空间不够必须先把最老的扇区擦掉擦除期间掉电会导致整段数据不可用MRAM 直接在当前地址继续写写到空间尽头就绕回起始地址没有擦除动作也就没有擦除中途掉电的窗口。4.2 记录格式与 CRC 校验再可靠的介质也不能假设总线永远干净。工业现场的电机启停、变频器干扰都可能让 SPI 数据在传输过程中悄然出错。所以我在每条日志前定义了固定头末尾带校验[2字节 长度][1字节 类型][4字节 序列号][N字节 负载][2字节 CRC16][1字节 尾标记 0x7E]读取流程就是从索引区拿到最新写入位置往前逐条解析先验证尾标记再算 CRCCRC 通过才把这条记录交给业务层。如果 CRC 错说明写入时掉电或者现场干扰破坏了尾部直接丢弃这条记录即可不影响前面记录的有效性。CRC16 的实现不在这里展开常见查表法性能足够。一个值得推荐的增加可靠性的操作是写后回读写完后立刻读回关键字段长度、序列号、CRC 高位和缓冲区比较。MRAM 本身数据翻转概率极低这个步骤防的不是 MRAM 而是总线路由上的串扰和接触不良。实测写后回读能把异常数据检出率从“事后 CRC 发现”提前到“当场定位总线问题”排障效率高很多。4.3 掉电时刻BOR、提前预警和写入收尾MRAM 掉电不丢数据但这不意味着掉电时可以随便中断一次写操作。每次写操作由若干 SPI 时钟周期组成如果写到一半供电掉到低于芯片工作电压正在进行的这一次写操作可能只写入了一部分字节该条记录呈现“半新半旧”状态。好在有 CRC 垫底这种记录会被识别为坏记录并丢弃。为了让“坏记录”出现的概率更低我做了两级掉电应对使用 PIC18LF45K40 的欠压复位BOR配置 BOR 电压阈值在 2.8V 左右。当电源降到阈值时MCU 先进入复位状态停止一切操作。这保证了掉电期间不会继续发后续字节避免污染下一条记录。外部掉电检测进入提前落地如果系统里有比较器或 ADC 监控电源轨可以在 3.0V 时触发高优先级中断中断里做两件事把当前正在写的记录循环写完尤其是一个事务的最后几条日志然后尽快把最新写位置索引更新到索引区。从检测到电压开始下降到 MCU 彻底失去工作能力之间大约有几百微秒到几毫秒足够完成一条 128 字节记录的收尾。这个策略的逻辑是MRAM 保证介质层不坏BOR 保证主控不乱写CRC 保证业务层能识别坏记录三层配合才能称为完整的存储可靠性方案。4.4 磨损均衡这次真的不用做我听到最多的问题是MRAM 写寿命虽然长是不是仍然需要磨损均衡算法这个问题要算笔账。MR25H40CDF 标称耐久 1e14 次写周期。什么叫“一次写周期”一次连续写操作CS 拉低到拉高之间的整段写入算一次不是每字节算一次。假设最极端的场景每 10 毫秒写一条 512 字节的日志每天 8 小时满载运行一年约 10 亿次写周期。1e14 除以 1e9 等于 10 万年的理论寿命。哪怕数据手册指标按最保守打一个 0.1 的折扣依然远超设备本体寿命。磨损均衡算法解决的是“Flash 擦写寿命不足 擦除粒度过大”的问题。MRAM 连擦除都没有均衡算法里的“把热数据挪到不同块”就失去了意义。我最终在代码里删掉了原有 Flash 方案的均衡层这省掉了大量状态维护逻辑和启动时的块扫描时间。5. 实测记录与实战避坑5.1 读写实测连续循环与异常注入板子打样回来我在室内做了三天连续读写测试四组数据测试项条件结果全空间写读回 1 次512KB8MHz SPI0 错误字节单字节地址随机写读 1 万次地址随机每 10ms 一次0 错误日志区循环写 24 小时每 100ms 写 128B0 错误索引区更新正常模拟写一半掉电 500 次写入中随机切断 VDD无坏块坏记录被 CRC 正确丢弃第五项需要在电源路径里加电子开关和控制逻辑500 次掉电测试后重新上电扫描日志区最坏情况只丢失了最后一条记录其他历史数据全部完好。这套数据在很大程度上给了我上量产的信心。5.2 三个很隐蔽的坑和排查过程第一个是 HOLD# 悬空导致的偶发挂死。现象是设备运行几个小时到几天不等SPI 通信随机卡死复位后恢复。最初怀疑是 MCU 程序跑飞抓调试日志发现卡死时程序还在正常执行只是 MRAM 不响应。最后用示波器抓 HOLD# 引脚发现它在电机启动瞬间有毛刺拉到低电平。拉高后故障彻底消失。排查教训低频 SPI 芯片的“控制引脚”在工业现场同样要按可能受干扰来设计。第二个是 PPS 解锁序列写错表现为 SCK 波形正常、SDO 也有输出但 SDI 永远读到 0xFF。原因是输入映射 SDI1PPS 没有配置到位MSSP1 实际使用的输入源还是默认引脚。K40 系列的 PPS 配置有输出映射和输入映射两组只配置输出不配置输入等于一半没接。排查方法是用调试器读所有 PPS 寄存器和你预想的值逐一比对很快就能定位。第三个是“最后一个字节丢失”就是前面 3.4 节写的那个问题。这个坑最阴的地方在于它不是必现的也和写入数据内容有关只有当最后一个字节的 bit 7 和 bit 0 组合让 MSSP 提前产生 BF 标志时才出错。排查时把写入内容改成全 0xAA、0x55复现率瞬间提升才成功定位到 CS 拉高时序上。5.3 项目上线前建议做的核验清单基于这套方案整理几条上线前核验项状态寄存器 WEL 位是否在每次写操作前确认置位没有置位就进入连续写数据会被零写入。CS# 引脚是否在 MCU 复位期间保持高电平建议硬件上拉如果只在软件初始化时拉高复位瞬间要想想有没有风险。掉电策略是否验证过“写入中途断电”而不是“空闲时断电”两种场景差距很大。写后回读是否覆盖了长度、CRC、地址这三个字段回读全负载会增加 1 倍写入时间但 8MHz 下写 128 字节回读一次也才几十微秒。索引区的写次数是否单独预估过如果每次写日志都同时更新索引索引区会承受和日志区相当的写压力建议索引更新频率降低比如每写 10 条日志更新一次。项目落地后我最大的感受是原来维护 Flash 那套坏块表、均衡计数、掉电整理任务的代码在 MRAM 方案里全部可以删掉整个存储模块的代码量少了将近一半逻辑反而更清晰。遇到下一个需要“反复可靠地存”的场景我会优先考虑这套组合。最后说个细节写记录前拉低 CS 之前先看一眼状态寄存器的 WEL 位再决定要不要补发 WREN这个习惯帮我挡掉过好几次因为初始化顺序变更引入的诡异问题留着不亏。
返回列表