ARTICLE DETAIL

资讯详情

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

基于PIC32与MR25H40CDF的SPI MRAM掉电保存方案设计

基于PIC32与MR25H40CDF的SPI MRAM掉电保存方案设计 这颗 MR25H40CDF 是我最近做工业数据采集节点时用到的 4Mbit SPI MRAM搭配 PIC32MX664F064L 单片机一起工作。项目需求本身不复杂每隔几百毫秒记录一组传感器数据设备断电后重新上电数据必须还在。刚开始原方案用的是 SPI NOR Flash测试下来发现两个问题一是写入寿命在频繁记录时很难估算二是掉电瞬间如果恰好还在做扇区擦除恢复过程非常痛苦。后来换成 MR25H40CDF 这颗磁阻随机存取存储器问题一下子简单很多。如果你也在做环境监控、设备日志记录或者参数掉电保存这篇内容应该能帮你省不少时间。1. 为什么这个地方我会选 MRAM而不是 Flash 或 EEPROM1.1 从一次现场设备掉电开始说之前维护过一台户外监测设备核心逻辑就是把传感器的温湿度、振动、电流值按周期存到外置 Flash。设备偶尔会因为现场供电不稳而掉电重新上电后最怕两件事一批关键日志变成全 0xFF或者文件系统索引坏了导致整段数据无法读取。SPI NOR Flash 的老问题有两个。第一是擦除粒度太大一个扇区通常 4KB改 100 字节数据也常常要先擦掉整个扇区再写入。擦除动作一旦被掉电打断这个扇区就处于不确定状态需要软件做额外恢复。第二是写寿命普通 SPI Flash 标称擦写次数一般 10 万次左右看着不少但设备如果每秒钟写几十个字节寿命消耗速度会远超直觉预期。两个问题叠加就成了“不是不能用而是用起来很费心”。换成 MR25H40CDF 之后写数据不再需要先擦后写。MRAM 的存储单元本身就是靠磁阻状态保存数据的写入就是直接覆盖没有“旧数据先清除”的步骤。这对掉电场景特别友好任何一次写命令只要 SPI 时序完整结束数据就立刻生效不需要等一个长达几十毫秒的擦除窗口。1.2 MRAM 原理和 MR25H40CDF 的关键参数MRAM 全称是 Magnetoresistive Random Access Memory磁阻随机存取存储器。MR25H40CDF 属于 Everspin 的串行 MRAM 产品线容量 4Mbit也就是 512KB。它内部的基本存储单元是磁隧道结通过改变自由层的磁化方向来改变电阻值从而表示 0 或 1。判断状态不需要持续供电所以断电后数据自然保持。这颗芯片的几个关键参数值得留意。接口是标准 SPI最高时钟频率在数据手册标称值很高工程上我一般降额到 10MHz 到 20MHz 使用。供电 3.3V工业级温度范围覆盖 -40℃ 到 85℃量产批次一致性也稳定。写入寿命方面MRAM 的产品逻辑是“几乎无限次写入”数据手册通常标到 10 的 14 次方量级以上实际工程中不需要再做磨损均衡。还有一个被很多人忽略的点MRAM 支持字节级随机写入想改哪个地址就改哪个地址不像 Flash 需要考虑“只能在空白区域写”或者“整块擦除后重写”。这意味着数据管理逻辑可以大幅简化特别是多通道参数、运行模式、报警阈值这类频繁小量更新的数据非常适合直接放 MRAM。1.3 和 Flash、EEPROM 的对比这里有一张表很多工程师选型时会犹豫EEPROM 不也能字节写吗为什么不用 24LC256 这类芯片我直接把三者放在一起对比结论会很清楚。项目SPI NOR FlashEEPROMMRAM (MR25H40CDF)写入前是否要擦除必须按扇区擦除不需要不需要擦写寿命约 10 万次约 100 万次10 的 14 次方量级单次写耗时擦除加编程可达几十毫秒单字节毫秒级SPI 连续时钟字节级微秒左右掉电保持通常 10 年以上通常 10 年以上数据保持能力强适合工业环境随机写要先构造空白区域支持支持模块复杂度需要擦除管理和磨损均衡简单最简单EEPROM 的主要问题是写速度慢而且容量通常不大几 Kbit 到几 Mbit 都有但高位号的价格不便宜。MRAM 的单价虽然比同容量 NOR Flash 高一些但考虑到设计复杂度、故障率和维护成本在关键数据存储场景下反而更划算。尤其是“掉电前最后一批数据必须保住”这种需求MRAM 几乎是为它量身定做的。1.4 什么情况下我不建议上 MRAMMRAM 不是万能的。如果你的产品代码和资源文件需要大量存储比如几百 MB 的固件升级包、音视频素材MRAM 的容量和成本都不合适老老实实用 eMMC 或者 SD 卡。如果只是保存几个序列号、校准参数一年写不了几次普通 EEPROM 也够了没必要增加 BOM 成本。MRAM 最适合的场景是高频写入、掉电敏感、数据量在几百 KB 以内的工业应用。我这次做的数据采集节点正好落在这个区间每条记录 64 字节一共 8192 条全部存在 MRAM 里连文件系统都不需要。2. PIC32MX664F064L 的选型和硬件连接细节2.1 PIC32MX664F064L 这颗芯片到底提供了什么PIC32MX664F064L 是 Microchip 的 MIPS M4K 内核 MCU主频最高 80MHz。选它不只是因为手头有库存更重要的是 100 引脚的封装给了充足的 GPIO 和外设资源同时芯片内部集成了多个 SPI 模块、UART、I2C 和 DMA 控制器。实际项目中这颗 MCU 除了和 MR25H40CDF 通信还要负责采集模拟量、跑 Modbus 协议栈、驱动数码管和按键甚至还要留一路串口给调试。如果 MCU 外设太少方案就得增加专用芯片复杂度会明显上升。而 PIC32MX664F064L 单个芯片把这些事情都接住了硬件上非常干净。另外PIC32MX 系列有 DMA这对 MRAM 的高效读取很有价值。后面我会单独讲 DMA 怎么和 SPI 结合把 CPU 从逐字节搬运中解放出来。2.2 电路连接CS、SCK、SDI、SDO 怎么接MR25H40CDF 是标准 4 线 SPI 器件接线不存在歧义。MCU 作为主机连接方式如下SCK接 PIC32 的 SPI1_SCK提供通信时钟。SDIMRAM 的串行数据输出接 PIC32 的 SPI1_SDIMCU 从这里读数据。SDOMRAM 的串行数据输入接 PIC32 的 SPI1_SDOMCU 从这里发命令和数据。CS接任意 GPIO低电平有效。我把 CS 放在了 PIC32 的一个普通 GPIO 上没有用硬件片选。理由很简单MRAM 的指令流是需要“CS 拉低、发 WREN、CS 拉高、再拉低、发写命令”这种序列的普通 GPIO 控制起来更灵活。电路上还有两个容易被忽略的细节。第一CS、SCK、SDO、SDI 四条线都建议串联一个 22Ω 到 33Ω 的电阻作用是抑制振铃和降低过冲。第二MR25H40CDF 的 VDD 引脚旁边必须放一颗 0.1μF 陶瓷电容并且尽量靠近芯片引脚。MRAM 在写入时瞬间电流变化比普通 CMOS 器件更明显去耦不到位会导致 SPI 波形畸变。2.3 上电时序和抗干扰设计工业设备里 MCU 和 MRAM 通常共用一路 3.3V但上电时序仍然要检查。MCU 的 GPIO 如果在 MRAM 还没供电时就输出高电平电流会通过芯片内部的保护二极管倒灌进 VDD轻则导致 MRAM 逻辑异常重则损伤器件。我现在的做法是MRAM 的 VDD 和 MCU 的 3.3V 在同一个电源域同时上电CS 引脚默认配置为高电平在 SPI 模块初始化之前不让其他 GPIO 主动驱动到 MRAM 相关引脚上。如果实际布局无法做到同时上电更稳妥的办法是在 SPI 数据线上串联 100Ω 电阻把倒灌电流限制在安全范围内。另外工业现场经常有电机、继电器这类强干扰源。MR25H40CDF 本身对磁场有一定耐受能力但我还是建议 PCB 上让 SPI 信号线远离大电流回路必要的时候在 CS、SCK 上加一个小电容滤波。当然电容不能太大否则会把 SPI 边沿拖缓一般 10pF 到 22pF 就够。如果能把 MRAM 放在 MCU 附近线短了抗干扰问题自然少一大半。3. SPI 初始化与 MRAM 数据读写函数实现3.1 SPI 时钟、极性相位怎么配PIC32MX664F064L 的每个 SPI 模块都可以独立配置为 8 位或 16 位数据宽度支持主模式、从模式。MR25H40CDF 支持 SPI Mode 0 和 Mode 3我习惯用 Mode 0空闲时 SCK 为低数据在第一个时钟沿被采样。波特率计算公式是SPIxBRG (FPBK / (2 * 目标SPI时钟)) - 1假设外设总线时钟 PBCLK 是 40MHz目标 SPI 时钟 10MHz那么SPIxBRG (40000000 / (2 * 10000000)) - 1 1如果想让时序更稳比如 PCB 走线较长或者连接器接触电阻大可以把目标改成 5MHz算出来 BRG 就是 3。工业现场我很少用满速MRAM 的接口能力足够瓶颈通常在别的环节没必要把时序裕量压到极限。初始化代码我按 PLIB 风格写了一个最小版本#define SPI1_BRG 1 // PBCLK40MHz, 目标10MHz void mram_spi_init(void) { // 1. 配置CS引脚为输出默认拉高 MRAM_CS_TRIS 0; MRAM_CS_LAT 1; // 2. 复位SPI1并做基本配置 SPI1CON 0; SPI1BRG SPI1_BRG; SPI1CONbits.MSTEN 1; // 主模式 SPI1CONbits.CKP 0; // SCK空闲低电平 SPI1CONbits.CKE 0; // 实测可用的相位组合 SPI1CONbits.SMP 1; // 输入数据在时钟末端采样链路较长时更稳 SPI1CONbits.ON 1; // 使能SPI1 }这里要提醒一句PIC32 的 CKE 位含义和很多 ARM 单片机的 CPHA 并不是逐位对应的。我的经验是不要凭感觉猜直接用逻辑分析仪抓一根波形看数据线变化时刻和时钟采样点是否满足 MRAM 的时序图。上面这份配置在我板子上是稳定的但你换一颗 MCU 或者换一个 SPI 模块时最好重新验证。3.2 MR25H40CDF 的指令集与状态寄存器MR25H40CDF 的指令集很精简实际用到的就几个0x06WREN写使能。每次写数据之前必须先发这个命令。0x04WRDI写禁用平时可以不用。0x02WRITE按地址写数据。0x03READ按地址读数据。0x05RSR读状态寄存器。0x01WRSR写状态寄存器主要用于配置写保护。有一点和 EEPROM 使用者习惯不同SPI MRAM 的状态寄存器里没有传统意义上的“忙标志”位。读状态寄存器能看到的主要是写使能锁存位 WEL 和区域保护位。也就是说主机写完数据后不需要轮询 BUSY只要 CS 拉高这一帧写操作就算完成了。这比管理 NOR Flash 的页编程状态机要简单得多。不过 WEL 位有个细节WREN 命令必须完整结束也就是 CS 从低拉高的那一刻WEL 才真正置位。如果你发送 0x06 后没有拉高 CS而是继续发写命令MRAM 会直接忽略后面的写操作。这是很多人第一次用这颗芯片时遇到的最大坑。3.3 可跨页读写的代码实现MR25H40CDF 的容量是 512KB地址范围从 0x00000 到 0x7FFFF。一次写操作建议控制在 256 字节以内超过 256 字节时应该分帧处理。下面这个写函数能自动处理 256 字节边界回绕#define MRAM_CMD_WREN 0x06 #define MRAM_CMD_WRITE 0x02 #define MRAM_CMD_READ 0x03 void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { while (len 0) { uint32_t chunk 256 - (addr 0xFF); if (chunk len) chunk len; // 1. 发送WREN并完整拉高CS完成锁存 MRAM_CS_LAT 0; spi1_write_byte(MRAM_CMD_WREN); MRAM_CS_LAT 1; // 2. 发送写命令和24位地址 MRAM_CS_LAT 0; spi1_write_byte(MRAM_CMD_WRITE); spi1_write_byte((addr 16) 0xFF); spi1_write_byte((addr 8) 0xFF); spi1_write_byte(addr 0xFF); // 3. 写入数据 for (uint32_t i 0; i chunk; i) { spi1_write_byte(buf[i]); } MRAM_CS_LAT 1; addr chunk; buf chunk; len - chunk; } }读函数更简单同一帧内发完读命令和地址后连续读即可void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LAT 0; spi1_write_byte(MRAM_CMD_READ); spi1_write_byte((addr 16) 0xFF); spi1_write_byte((addr 8) 0xFF); spi1_write_byte(addr 0xFF); while (len--) { *buf spi1_read_byte(0x00); } MRAM_CS_LAT 1; }spi1_write_byte 和 spi1_read_byte 就是最标准的“写 SPI1BUF、等 SPIRBF”阻塞收发。10MHz 时钟下一次读 64 字节大约也就 55 微秒左右对绝大多数工业应用来说完全不是瓶颈。3.4 想提速DMA 怎么接进来如果记录数据量大比如一次要读回 4KB 日志阻塞式逐字节读写会占用 CPU。PIC32MX664F064L 的 DMA 控制器可以配合 SPI1 的接收事件自动把数据搬进内存。核心思路是把 SPI1 的 RX 事件作为 DMA 触发源DMA 目的地址指向要填充的缓冲区每次 SPI 收到一字节就自动触发一次搬运。这样 CPU 只需要在 DMA 中断里检查“结束标志”然后把缓冲区拿去用。我用的 PLIB 风格代码如下void mram_dma_read(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LAT 0; spi1_write_byte(MRAM_CMD_READ); spi1_write_byte((addr 16) 0xFF); spi1_write_byte((addr 8) 0xFF); spi1_write_byte(addr 0xFF); // 用SPI1接收事件触发DMA搬运 DmaChnOpen(0, 0, DMA_OPEN_AUTO); DmaChnSetEventControl(0, DMA_EVENT_START_IRQ_EN | DMA_EVENT_END_IRQ_EN | DMA_EVENT_END_SECTOR); DmaChnSetTRIGEN(0, DMA_EVENT_SPI1_READ); DmaChnSetSourceSize(0, 1); DmaChnSetDestSize(0, 1); DmaChnSetSrcAddr(0, (uint32_t)SPI1BUF); DmaChnSetDstAddr(0, (uint32_t)buf); DmaChnSetTxferCount(0, len); DMA0INTbits.CH0IE 1; // 使能DMA通道0中断 DmaChnEnable(0); // 等待DMA完成中断代码里用一个标志位表示 while (!dma_done_flag); MRAM_CS_LAT 1; }DMA 方式只适合读操作。写操作因为要先发命令和地址再连续发数据用中断或阻塞方式写其实都不慢毕竟 MRAM 写入不需要擦除等待。真正需要 DMA 的还是大批量回读日志这种场景。4. 工业现场的数据管理策略4.1 写一半掉电怎么办原子性和完整性MRAM 解决了“掉电丢数据”的问题但没有解决“写一半断电这帧数据半旧半新”的问题。比如一条记录有 64 字节掉电时刚写到第 40 字节那么最终 MRAM 里可能是前 40 字节新数据、后 24 字节旧数据。这个问题的解法不是靠 MRAM而是靠记录结构设计。我通常用“数据 CRC 提交标志”三段式布局偏移内容长度0记录序号4 字节4数据主体N 字节4NCRC324 字节4N4提交标志1 字节写的时候先写序号、数据、CRC最后再写提交标志。读的时候只有提交标志匹配预期值才认为这条记录完整有效。由于 MRAM 字节写是原子的只要最后一个字节没写成这条记录就会被判定为无效不会出现“半条记录被当成有效数据”的情况。4.2 循环记录方案的两种做法连续记录设备经常要“存满后覆盖最旧数据”这就是环形缓冲。MRAM 不像 Flash 那样需要擦除所以实现环形缓冲非常简单。一种做法是在 MRAM 里固定一个头部地址存放“当前写位置”指针。每次写完一条记录就把写位置指针更新到下一个地址到末尾后回绕到起始地址。这个方案直观但写指针本身也是数据掉电时可能在更新指针途中出问题造成上电后写位置错误。更可靠的做法是序号扫描法每条记录自带 4 字节递增序号。上电时从头扫描整个环形区找到序号最大的那条记录它的下一条地址就是新的写位置。这个扫描过程在 512KB 空间里按 64 字节一条算最多读 8192 条记录以 10MHz SPI 速度扫描大概耗时几毫秒到十几毫秒完全可接受。我最终用的是第二种方案。它的好处是不需要维护额外指针也不需要处理指针更新的原子性问题。只要保证记录本身结构完整就算掉电发生在任意时刻上电后依然能通过序号找到正确位置。4.3 参数存储区怎么做版本管理除了日志记录设备里还有一堆配置参数比如采样周期、报警阈值、设备地址。这些参数的特点是更新频率低但一旦写错就会导致设备行为异常。我用了一个简单的小文件思路每个参数块头部放一个 magic 值和版本号尾部放 CRC。每次修改参数时先写到备份区校验通过后再更新主区。读取时如果主区 CRC 失败就直接读备份区。这个思路在传统 EEPROM 系统里很常见搬到 MRAM 上更轻松因为不需要考虑擦除寿命。typedef struct { uint32_t magic; uint32_t version; uint8_t data[128]; uint32_t crc; } param_block_t;主区和备份区各放一份 param_block_t用不同起始地址。上电时分别校验两块区的 magic、CRC版本号高的获胜。MRAM 的优点在这里体现得很直接更新参数不需要先擦除备份区可以直接覆盖写入整个过程还能保持实时性。4.4 littlefs 该不该上有时读者会问我既然这颗 MRAM 容量有 512KB为什么不直接跑 littlefs这个问题我认真想过。littlefs 的优点是提供标准文件接口方便管理多文件和多目录它的掉电恢复设计也很成熟。但代价是代码量、RAM 占用和运行时开销。对于“固定结构、固定记录长度、读取顺序固定”的工业数据记录我认为裸地址管理更好。裸地址方案没有文件分配表没有目录项没有碎片整理所有逻辑都是直接对地址操作可靠性和实时性都可控。littlefs 更适合多文件、文件名可变、文件大小差异大的通用存储需求。如果你确实希望日志可以按文件导出那也不要直接在 MRAM 上跑通用文件系统。更稳妥的方案是 MCU 侧通过 USB 或者以太网把 MRAM 里的原始记录发给上位机由上位机转换格式。这样 MRAM 这层始终保持简单。5. 常见问题排查与实战避坑5.1 问题速查表做这个项目期间我在调试和现场应用中遇到不少问题整理成一张表方便后续复用。现象可能原因解决办法上电后读回全是 0xFFSPI 模式配置不对或 CS 空闲电平不对检查 CKP/CKE 组合用逻辑分析仪确认写数据后立刻读数据不一致WREN 没有生效CS 拉高时序不完整确认发送 WREN 后 CS 先拉高再拉低写完某个地址相邻地址也变了写长度超过 256 字节地址回绕到低地址按 256 字节边界分帧写高低温试验时读写偶发失败SPI 时钟太高或走线太长时序裕量不足降低 SPI 时钟到 5MHz 或 10MHz掉电后最后几条记录没了记录格式中提交标志和 CRC 策略不完善改为先写数据和 CRC最后写提交标志整机频繁复位后 MRAM 不响应MCU 与 MRAM 上电时序异常IO 倒灌电流同电源域供电或串 100Ω 电阻读状态寄存器 WEL 始终为 0WREN 命令被 SPI 配置错误吞掉检查 SPI 模块是否真的配置为主模式5.2 用逻辑分析仪抓波形的一个实测案例有一次我遇到的故障非常典型代码看起来没问题指令发送顺序也对但写入的数据就是不对读回来全是 0xFF。折腾了一下午最后用逻辑分析仪抓波形发现 WREN 之后我虽然写了 CS 拉高但因为连续操作太快CS 高电平持续时间非常短接近信号线上的毛刺。MRAM 内部状态机根本没识别到完整的 CS 上升沿WEL 位始终为 0写自然不成功。解决方法是拉高 CS 后加一个微秒级延时或者干脆调整代码结构让 WREN 和后续 WRITE 命令之间的 GPIO 操作更明确。这算是一个典型的“程序看起来对但时序不满足器件要求”的例子。从那以后所有 SPI 器件调试我都习惯先抓波形再收工。5.3 实测下来的几条经验第一MRAM 的写入速度会带来一个认知变化调试时不用再像 Flash 那样等待擦除代码里少了大量状态机整个 SPI 写流程变成了“发命令、发数据、拉高 CS”三步逻辑上非常干净。第二SPI 时钟速度不要盲目追求高。10MHz 在大多数 10cm 以内 PCB 走线上很稳但如果你用了杜邦线、转接板或者连接器高频下波形会明显劣化。工业场景多一级可靠性少一分排查成本。第三MR25H40CDF 这类 MRAM 和上位机联调时可以先用一个最简单的回环测试往地址 0x00000 写 32 字节再读出来比对。这个测试能快速排除接线、SPI 模式、供电三方面的问题。如果回环不通过后面任何日志和参数管理都先别谈。我在实际使用中最满意的其实是它让“掉电保存”这个需求变得透明了。做嵌入式时间长了会形成一种习惯每次写关键数据前都要想“万一掉电怎么办”要设计擦除标志、写完成标志、备份区。换成 MRAM 后虽然还需要 CRC 和提交标志来保证多字节一致性但不再需要处理擦除掉电恢复这种复杂状态整个系统的心里负担轻了一大截。最后再分享一个开发期的小技巧刚拿到 MR25H40CDF 时不要急着写应用逻辑。先用一个最小工程把 SPI 初始化、读状态寄存器、写 64 字节、读回来比对这四个功能跑通全程用逻辑分析仪盯住波形。确认基础通路没问题后再往上面加环形缓冲和参数区结构。基础打得稳后面排查问题时你会非常感谢当时多花的这一个小时。
返回列表