ARTICLE DETAIL

资讯详情

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

嵌入式存储选型:PIC18F46K22 外挂 SPI MRAM 解决掉电与寿命问题

嵌入式存储选型:PIC18F46K22 外挂 SPI MRAM 解决掉电与寿命问题 干嵌入式项目时间长了你会发现真正让你半夜跑现场的往往不是算法而是数据存没存住。产品跑着跑着参数漂了、日志丢了、上电起不来十有八九都出在存储设计上。最近我手里一台工业控制板正好把存储部分换成了 PIC18F46K22 外挂 MR25H40CDF 的组合解决了频繁写参数的寿命问题也让设备出故障后有足够长的日志可以倒查。MR25H40CDF 是 Everspin 的 4Mbit SPI MRAM容量算下来 512KB接口和普通 SPI Flash 几乎一样但内部机制完全不同。这篇是我从选型、接线、驱动到掉电保护折腾一遍之后的记录如果你也在做工业设备、仪器仪表、运动控制这类需要连续记录数据的嵌入式系统内容可以直接参考。1. 选这颗 MRAM 之前我到底在纠结什么1.1 存参数和存日志是两种完全不同的脾气很多嵌入式开发刚接触存储时习惯性看容量和价格先把 SPI Flash 或者片上 EEPROM 挂上跑完功能测试发现没什么问题就交付了。但产品进到工业现场问题会以另一种方式暴露出来要么是设备每天保存几十条运行记录EEPROM 的擦写寿命撑不了几个月要么是突然断电后正在写的参数区损坏设备上电就进入异常状态。存储选型不能只按“能存多少”来选。工业应用里更关键的两个维度是写次数和写模式。EEPROM 虽然随机读写方便但擦写寿命通常在十万到一百万次量级写一页数据还要先擦除掉电窗口内很容易把正在擦除的块写坏。普通 SPI Flash 容量大、价格低但页大小、扇区擦除、擦写均衡、磨损管理这些事全要 MCU 处理频繁小数据写入时效率很低。FRAM 也很能打但容量普遍偏小部分型号的高温特性和供货情况并不理想。MR25H40CDF 这颗 MRAM 走的是另一条路线它是磁阻随机存储器本质上没有电荷存储不需要“先擦除再写入”。写入寿命规格在十的十二次方次以上几乎可以把它当成“掉电不丢的 SRAM”来用。这个特性对我的项目意义很大参数区可以每次上电都存一遍日志区可以每条记录都直接写不用再做复杂的磨损均衡。1.2 MR25H40CDF 规格书里最值钱的几行字我第一次看 MR25H40CDF 数据手册时除了确认接口是 SPI最关注三个参数工作电压、时钟频率、数据保持时间。工作电压 3.3V和 PIC18F46K22 在 3.3V 供电下很好匹配不用额外的电平转换。SPI 时钟最高 40MHzPIC18F46K22 的 MSSP 主机模式跑到 16MHz 没压力实际系统瓶颈在 MCU 这边。数据保持时间规格上一般标 20 年以上掉电保存不用靠外部电池。另外它支持 SPI Mode 0 和 Mode 3特别适合和大多数 MCU 的 SPI 主机对接。你不需要为它定制特殊的脉冲宽度只要按标准 SPI 时序把 CS、SCK、SI/SO 拉好就行。4Mbit 换算成字节是 524288 字节。对工业控制板来说这个空间做好分区后非常宽裕参数区用 64KB日志区用 448KB剩余区域放索引和状态标记。如果设备每 10 秒写一条 64 字节的运行记录能存几百小时的数据足够故障回溯。1.3 MRAM 给系统设计带来的简化用了 MRAM 之后软件架构可以明显简化。普通 SPI Flash 驱动里写之前要检查块是否已擦除写中要处理页边界掉电恢复后还要扫描哪些块是无效的。MRAM 不需要这些驱动里只需要三条主要指令WREN、WRITE、READ。这不是说 MRAM 万能它也有局限容量相比大容量 Flash 没有优势单价也高一些。所以我的选型原则是小容量、高可靠、频繁写的场景优先考虑 MRAM大容量、低成本的存档类数据继续用 SPI Flash。工业设备里的“关键数据存储区”和“文件型数据区”分开设计比全部塞进一种芯片要稳得多。2. 硬件连接4 根线就能把数据接到 MRAM 里2.1 PIC18F46K22 与 MR25H40CDF 的引脚映射MR25H40CDF 是标准 SPI 从设备引脚不多CS、SCK、SI、SO、VDD、VSS、WP、HOLD。PIC18F46K22 的 MSSP1 模块正好提供 SCK1、SDI1、SDO1 这三根线。我项目里的接法如下MRAM 引脚功能接 PIC18F46K22CS片选低有效任意 GPIO建议用普通 IO 控制SCK时钟SCK1 引脚SI数据输入SDO1 引脚SO数据输出SDI1 引脚VDD3.3V 电源3.3V就近放 100nF 陶瓷电容VSS地公共地WP写保护低有效拉高到 VDD不用做写保护HOLD暂停通信低有效拉高到 VDD不用此功能CS 不一定要用 MSSP 的硬件片选我习惯用普通 GPIO 控制。原因有两个MRAM 写命令要求 CS 先拉低、拉高、再拉低用普通 GPIO 更好控时序多个 SPI 设备挂在同一条总线上时软件片选也更灵活。接线时有两点要注意WP 和 HOLD 不能悬空。工业环境有干扰悬空引脚可能耦合噪声导致误触发写保护或总线暂停。最简单可靠的做法是直接接 VDD。如果 PIC18F46K22 供电 5V而 MRAM 供电 3.3VSI、SCK、CS 这三根输出线必须做电平转换不能用 5V 高电平直接怼进去。我项目里 MCU 和 MRAM 都统一用 3.3V少了一层麻烦。2.2 电源和 PCB 上的几个细节MR25H40CDF 的工作电压范围是 3.3V 系统写入瞬间电流会比读取时大一些但整体功耗仍然很低。工业板上我建议做好三点VDD 引脚旁放 100nF 和 10uF 电容不要省。SPI 走线尽量短SCK、SI、SO、CS 四根线长度接近避免时钟和数据到达时间差太大。CS 线上加一个 10kΩ 上拉电阻防止 MCU 复位期间 CS 处于不确定状态误把噪声当片选。我第一次打样时没在 CS 加上拉MCU 上电复位瞬间 MRAM 偶尔会被写入一串乱码。加上拉后问题消失。这个不是芯片缺陷是 MCU 复位时 IO 状态不确定造成的工业板上一定要防住。3. PIC18F46K22 驱动代码从 SPI 初始化到连续读写3.1 MSSP1 初始化配置PIC18F46K22 的 SPI 主机模式用 MSSP1 模块。以 XC8 编译器为例初始化很简单关键是 CKP、CKE 要和 MRAM 的 SPI Mode 0 对上。#define MRAM_CS_TRIS TRISBbits.TRISB2 #define MRAM_CS LATBbits.LATB2 void SPI1_Init(void) { // CS 引脚配置为输出初始拉高 MRAM_CS_TRIS 0; MRAM_CS 1; // SCK1、SDO1 为输出SDI1 为输入 TRISCbits.TRISC3 0; // SCK1 TRISCbits.TRISC5 0; // SDO1 TRISCbits.TRISC4 1; // SDI1 // 先关闭 MSSP1配置完成后再打开 SSP1CON1bits.SSPEN 0; // SPI Mode 0空闲时 SCK 为低上升沿采样下降沿改变数据 SSP1CON1bits.CKP 0; SSP1STATbits.CKE 0; // 主机模式时钟 Fosc/4 SSP1CON1bits.SSPM 0b0000; // 使能 MSSP1 SSP1CON1bits.SSPEN 1; }如果你的主时钟是 64MHzSPI 时钟就是 16MHz。实测这个速率下MR25H40CDF 在常规 PCB 走线上很稳。如果板子走线较长或者现场干扰比较大可以把 SSPM 改成 0b0010 用 Fosc/16写日志慢一点没关系稳定性优先。3.2 字节收发基础函数MSSP 接收和发送是同一个过程写 SSP1BUF 启动传输传输结束后读取 SSP1BUF既能发送一个字节也能收到一个字节。发送 0x00 时循环读取 SO 上返回的数据。uint8_t SPI1_Exchange(uint8_t data) { PIR1bits.SSP1IF 0; SSP1BUF data; while (!PIR1bits.SSP1IF) { // 等待传输完成 } return SSP1BUF; }这里有个经验一定要先清中断标志再写 SSP1BUF。如果不清可能读到上一次遗留的标志位导致第一个字节直接跳过等待时序乱掉。另外MSSP 的接收缓冲区只有一个字节深度连续读数据时不要试图“先读后发”造成数据积压按实际需求逐字节交换就行。3.3 读操作0x03 指令与连续读取MR25H40CDF 的读操作很简单CS 拉低发送 0x03再发送 3 字节地址之后每一个无效字节的发送都会被当成一个读取周期SO 会持续输出地址递增的数据。我写了一个通用块读取函数方便把参数区和日志区整体读出来。#define MRAM_CMD_READ 0x03 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_WREN 0x06 #define MRAM_CMD_RDSR 0x05 uint16_t MRAM_ReadBlock(uint32_t addr, uint8_t *buf, uint16_t len) { if (addr len 0x80000u) { return 0; } MRAM_CS 0; SPI1_Exchange(MRAM_CMD_READ); SPI1_Exchange((addr 16) 0xFF); SPI1_Exchange((addr 8) 0xFF); SPI1_Exchange(addr 0xFF); while (len 0) { *buf SPI1_Exchange(0x00); buf; len--; } MRAM_CS 1; return 1; }读操作不需要 WREN也不需要担心磨损所以在调试时反复读、反复比对没关系。地址范围 0x000000 到 0x07FFFF只要是 512KB 地址空间内都可以直接访问。3.4 写操作WREN、0x02 与总线时序MRAM 写操作和 EEPROM 类似也需要先写使能。区别在于 MRAM 不需要先擦除直接覆盖写入即可。很多新手第一次写驱动在一个 CS 低电平期间连续发 WREN 和 WRITE这是不对的。正确流程CS 拉低发送 WREN然后 CS 拉高。这个上升沿把写使能锁存器置位。CS 再次拉低发送 WRITE接着发 3 字节地址和数据。写完所有数据后CS 拉高。uint16_t MRAM_WriteBlock(uint32_t addr, const uint8_t *buf, uint16_t len) { if (addr len 0x80000u) { return 0; } // 第 1 步写使能 MRAM_CS 0; SPI1_Exchange(MRAM_CMD_WREN); MRAM_CS 1; // 第 2 步写数据 MRAM_CS 0; SPI1_Exchange(MRAM_CMD_WRITE); SPI1_Exchange((addr 16) 0xFF); SPI1_Exchange((addr 8) 0xFF); SPI1_Exchange(addr 0xFF); while (len 0) { SPI1_Exchange(*buf); buf; len--; } MRAM_CS 1; return 1; }3.5 状态寄存器与忙检测MRAM 写一个字节不需要经历 EEPROM 那种毫秒级擦写时间。写入内部完成后状态寄存器里有一个写进行标志位可以读。稳妥做法是写完后用 RDSR 读状态等待忙标志清零。uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS 0; SPI1_Exchange(MRAM_CMD_RDSR); status SPI1_Exchange(0x00); MRAM_CS 1; return status; }具体忙标志的位定义不同批次型号会有细节差别我建议以你手里那颗芯片数据手册为准。实际项目中我甚至会在写完关键参数后把数据重新读回来比对而不是只依赖状态寄存器。MRAM 本身就快回读验证多花的时间完全可以接受。4. 工业场景的数据布局把 512KB 用出条理4.1 分区设计参数区、日志区、索引区芯片再可靠软件逻辑乱也一样会出问题。512KB 空间虽然不算小但在工业产品里不能随便划拉。我习惯分成三个区域区域地址范围大小用途参数区 A0x00000 - 0x0FFFF64KB当前参数、配方、校准值日志区0x10000 - 0x7FCFF约 448KB运行记录、报警记录、事件记录索引区0x7FD00 - 0x7FFFF768B日志写指针、启动标记、数据版本参数区为什么预留 64KB不是为了存 64KB 参数而是为了做双备份。工业设备里参数写坏导致设备停线是最常见的事故。我在参数区内部再分 A/B 两份每份前面放一个版本号和 CRC 校验值。上电时先读版本号版本号正常就加载版本号异常就切换到备份区同时报警提示参数被恢复。日志区用环形结构每一条记录固定长度比如 64 字节。记录内部包含标志字、时间戳、数据类型、数据正文、CRC16。标志字固定为 0xA5A5防止读到半写的垃圾数据时误判成有效记录。4.2 写日志时顺序比随机重要MRAM 虽然随机写性能很强但工业日志还是建议顺序写。原因不是硬件限制而是便于掉电恢复和检索。每次写日志前先从索引区读取当前写指针然后到日志区对应位置写入一条记录写完后把写指针更新到下一个位置。如果日志区满了就把写指针回绕到起始位置覆盖最旧的数据。这个看起来很简单但要防止“记录写了一半指针已经更新”的问题。实际逻辑应该反过来先写数据记录到日志区。读回数据校验 CRC。校验通过后再更新索引区中的写指针。如果中途掉电数据记录可能没写完但指针还是旧的下次启动时依然从旧位置补写。这种方式牺牲了一点速度换来的是真正的事务一致性。工业设备日志不允许出现“数据没写进去指针却已经跳过去”的丢数据情况。4.3 把 MRAM 当“掉电不丢的 SRAM”来设计我在项目里有一个很直接的体会不要过度设计 MRAM 驱动。普通 Flash 驱动里的垃圾回收、块擦除、磨损均衡在这里都不需要。你越往上加复杂逻辑越容易引入隐藏 bug。正确做法是把它看作大号 SRAM写数据时直接覆盖读数据时直接寻址。系统里只剩几个大状态量需要额外保护比如日志写指针和启动标志。只要这些关键状态做好双备份整个存储方案就非常稳。例如索引区的写指针我会用三个副本。三个副本一致才认为有效不一致时取多数一致的版本并把这个情况记入故障日志。MRAM 写不会频繁把状态写坏但工业电磁环境里一切皆有可能多几份副本成本很低可靠性收益却很高。5. 现场跑起来后的几个坑与排查方法5.1 CS 毛刺导致的莫名写入第一次做样机时我遇到过 MRAM 里偶尔多出一些 0x00 的情况。排查代码看不出问题后来用示波器抓 CS 引脚发现 MCU 复位瞬间 CS 上有一串低电平毛刺。MRAM 误以为主机在发指令把总线上不确定的数据写进去了。解决办法是在 CS 线上加 10kΩ 上拉电阻并在代码里做两层保护一是所有写函数开头检查写使能状态二是在系统初始化阶段先把 MRAM 的 WP 相关逻辑检查一遍确保主程序没有跑起来之前不会误写。此外把所有外部 Flash、MRAM、传感器这类 SPI 从设备的片选都统一处理空闲时必须为高主控复位期间禁止任意 IO 浮空。5.2 SPI 模式不对读出来全是 0xFF 或乱跳MR25H40CDF 支持 Mode 0 和 Mode 3。如果读回来第一个字节正确、后续字节错乱往往是 CPOL 或 CPHA 配置不匹配。PIC18F46K22 的 MSSP 配置里CKE 位决定数据在哪个边沿变化。调试时不要只看手册直接写一个最简单的读 ID 或读状态函数用示波器量 SCK 和 SO 的相位关系。如果发现数据变化沿和采样沿重合就把 CKE 取反重新测。正常 Mode 0 下SCK 空闲为低数据在 SCK 下降沿后改变在上升沿被主机采样。5.3 SPI 时钟太快导致长线数据错位现场设备经常有几十厘米长的排线SCK 到 MRAM 的传播延迟和 SO 到 MCU 的传播延迟不一定相等。我把 SPI 时钟从 16MHz 降到 8MHz 后原来偶发一字节错误的问题消失了。如果产品已经定型、无法改 PCB 布局可以这样排查用示波器同时抓 SCK 和 SO看数据是否在采样沿稳定建立。在 SO 上串 33Ω 电阻抑制振铃。把 SCK 到 MRAM 的走线尽量缩短或者改为 Fosc/16 速率。工业现场线束较长时高速 SPI 不一定是最优解。稳定的数据比漂亮的理论速率重要得多。5.4 写保护引脚悬空惹的祸还有一次客户反馈设备偶尔写不进参数排查到最后发现是 WP 引脚焊盘虚焊造成内部逻辑在写保护和正常状态之间跳变。写不进去不是 MRAM 坏了而是 WP 被拉到了接近低电平的位置。WP 和 HOLD 这类控制引脚原则就是不用就明确接高电平不要只依靠软件配置。焊接后最好做一次 AOI 或通断测试尤其是小封装芯片引脚虚焊很难从外观看出问题。5.5 常见问题速查表现象可能原因处理方法写不进去读回全是旧值WREN 时序不对或 WP 引脚异常核对 CS 上升沿检查 WP 是否接 VDD数据偶发错乱SPI 时钟过快或走线过长降速到 Fosc/16检查 SO 波形读数据全 0xFFSPI Mode 配置错误或 CS 悬空检查 CKP/CKE确认 CS 空闲为高复位后参数损坏CS 毛刺或电源不稳CS 加上拉加电压监控复位管理部分扇区无法写入混用了 SPI Flash 驱动逻辑确认没做扇区擦除直接覆盖写写入后立即掉电丢数据数据在 VDD 低于规格时写用电压监控芯片控制 CS或延迟复位6. 复盘与一点个人体会这套 PIC18F46K22 加 MR25H40CDF 的方案我后来在很多项目里沿用。最大的收获不是省了代码而是整个存储模块的复杂度降了下来。原来用 SPI Flash 时驱动代码里一半逻辑在处理擦除、磨损、页大小现在这些全都不需要了。留给上层的是非常干净的读写接口数据安全则通过分区、双备份、CRC 来保证。有一点必须在选型前想清楚MRAM 的容量和价格决定了它不适合当大容量文件系统。如果把 4Mbit 当成放图片、固件升级包、大文件缓存的仓库会觉得很贵、很挤。但如果把它定位成“高可靠关键数据的保险柜”配合外部大容量 Flash 做归档存储这个组合在工业和高端嵌入式应用里非常能打。另外建议在驱动层提前留一个“读写模式调试开关”。开发阶段打开回读校验和状态寄存器打印可以省下大量现场排查时间量产后关闭或用串口维护口查看不影响性能。我吃过一次亏就是没留调试接口现场偶发读回错误时只能不断烧录程序看现象效率很低。MR25H40CDF 这种 SPI MRAM 在嵌入式里不算什么新潮技术但真正用过之后你会发现它把存储设计里的“低频次、高可靠”和“高频次、不怕磨损”这两个需求统一在了一颗芯片里。如果你的产品也卡在日志写坏、掉电丢参数这些老问题上可以认真考虑换一套这样的组合试试。
返回列表