ARTICLE DETAIL

资讯详情

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

SPI MRAM替代EEPROM:PIC18F46K20工业掉电保存实践

SPI MRAM替代EEPROM:PIC18F46K20工业掉电保存实践 MR25H40CDF 这块 4Mbit 的 SPI MRAM配上 PIC18F46K20 这颗 8 位单片机是我最近一个工业项目里最能打的组合。项目本身不复杂一台现场仪表需要记录运行参数、故障日志还要在掉电瞬间保存现场快照要求断电后数据不丢同时能扛住长期反复写入。最开始我用的 I2C EEPROM跑了一段时间就出问题了——现场频繁断电把标称 100 万次擦写寿命的存储芯片直接磨穿了设备重启后参数全部乱掉。后来把存储芯片换成 MR25H40CDF驱动在 PIC18F46K20 的 MSSP 模块上重写才算彻底根治。这篇文章就把这套方案从选型、电路、驱动到数据完整性设计完整拆开讲一遍适合正在做工业控制器、仪器仪表、设备日志存储或者单纯好奇 MRAM 到底怎么用的嵌入式开发者参考。1. 从一颗被写穿的 EEPROM 说起为什么最终选了 MR25H40CDF1.1 现场设备的一次数据丢失故障那台仪表的功能很简单每隔一段时间记录一组运行数据一旦检测到外部断电要在主控芯片彻底掉电前把这组关键参数写进非易失存储。用 I2C EEPROM 的时候设备在实验室里一切正常可一到客户现场就频繁出现“上电后参数全乱”的故障。一开始我怀疑是电源问题又怀疑是 I2C 总线被干扰折腾了一圈最后用编程器把 EEPROM 内容读出来才发现存储区某些字节已经永久性损坏不管写什么进去读出来都是同一个错误值。为什么会这么快写坏算一笔账就清楚了。假如设备每隔 10 秒写一次日志一天下来就是 8640 次一个月接近 26 万次。标称 100 万次擦写寿命的 EEPROM在这种写入频率下撑不过 4 个月。如果日志间隔缩短到 1 秒寿命就只有十几天。更麻烦的是现场环境温度偏高而 EEPROM 的擦写寿命会随温度上升进一步缩水。数据量不大、写入却极其频繁这正好戳中了 EEPROM 和 NOR Flash 的软肋。1.2 三种存储器的本质差异我们平时在单片机项目里用得最多的非易失存储就是这三类串行 EEPROM、SPI NOR Flash 和 MRAM。三者看起来都能“掉电不丢数据”但内部工作原理完全不同直接决定了使用寿命和写入方式。特性I2C EEPROMSPI NOR FlashMRAMMR25H40CDF写入前是否需要擦除不需要必须先擦除整个扇区不需要最小写单位字节扇区擦除后按页编程字节/任意连续区域典型擦写寿命100 万次左右10 万~100 万次实际可视为无限字节级写入时间约 5ms擦除编程数百毫秒由 SPI 时钟决定微秒级掉电数据保持10 年以上10 年以上厂商标称 20 年以上随机改写便利度一般非常麻烦和 SRAM 一样随意说人话就是EEPROM 是“省着写”的器件Flash 是“先擦后写”的器件而 MRAM 本质上是把磁隧道结做成了存储单元写一个字节不需要擦除、不需要等待电荷泵升压写多少遍都不心疼。MR25H40CDF 属于 Everspin 的串行 MRAM 家族4Mbit 容量SPI 接口工作电压 3.3V写入次数标称在 10 的 12 次方以上这个量级在工业现场基本意味着“寿命无限”。1.3 MRAM 的选型边界不过我也得说句公道话MRAM 不是万能的价格明显比同容量的 EEPROM 和 NOR Flash 贵。如果产品里有个配置参数一年才改一次那 EEPROM 足够了没必要多花钱。如果只是存固件、存静态字库NOR Flash 依然是更优解。MRAM 真正适合的场景是高频日志记录、掉电瞬间多次保存、关键参数每次变化都需要入盘、写入失败绝不容忍。4Mbit 的容量对中小型工业设备刚刚好再大的数据量比如音视频、大容量文件存储还是老老实实用 Flash 或 SD 卡。另外有一点要注意MRAM 是靠磁隧道结翻转存储数据的PCB 布局时尽量远离大电流母线、变压器和强磁元件。虽然贴片封装在实际工程里很少被干扰但把存储芯片放在电机驱动器旁边这种事我建议能避免就避免。2. MR25H40CDF 的存储组织与 SPI 指令集固件里必须吃透的细节2.1 容量、寻址与地址边界MR25H40CDF 内部按 512K x 8 组织也就是 512KB地址范围从 0x00000 到 0x7FFFF一共 19 位有效地址。SPI 命令里的地址字段是 3 个字节但最高字节其实只有低 3 位有效也就是 A18~A16其余位建议直接写 0避免不同版本手册理解上的偏差。这颗芯片没有“页”的概念。NOR Flash 通常一页 256 字节写入不能跨页EEPROM 也有页写缓冲超过页大小就容易出问题。MRAM 不一样一次连续写可以从任意地址开始写任意多个字节中间不需要考虑页边界。但这带来一个新的坑地址到达 0x7FFFF 之后会回卷到 0x00000。也就是说如果你在写日志时没做边界检查数据可能会绕过末尾悄悄覆盖掉开头的参数区。后面第 5 章我会专门讲怎么处理。SPI 时钟频率方面MR25H40CDF 工业级版本标称最高 40MHz 级别具体看数据手册对应尾缀而 PIC18F46K20 在 64MHz 系统时钟下SPI 主模式最高也就 16MHz所以根本没有瓶颈。写入一个字节只要 8 个 SPI 时钟周期理论上 16MHz 时钟下写 32 字节数据加命令地址总耗时在几十微秒内这是后面设计掉电保存流程的关键底气。2.2 指令集与写使能机制MR25H40CDF 的 SPI 指令集沿用了标准串行存储器的通用指令风格和 W25Q 系列 NOR Flash 有一部分重合但千万别理所当然地照搬。我最常用的指令如下指令操作码说明WREN0x06写使能任何写操作前必须发送WRDI0x04写禁用RDSR0x05读状态寄存器WRSR0x01写状态寄存器READ0x03读数据FAST_READ0x0B快速读带一个时钟周期的 dummyWRITE0x02写数据SLEEP0xB9进入睡眠模式WAKE0xAB从睡眠模式唤醒写使能机制是这颗芯片最容易踩坑的地方。每次执行 WRITE 或者 WRSR 之前必须先发一条 WREN把 CS 拉低发 0x06再把 CS 拉高。WREN 命令完成后状态寄存器里的 WEL 位会被置 1表示器件允许写入。写完数据或状态寄存器后WEL 位自动清 0。如果你漏发了 WREN直接发 WRITE芯片会静默忽略读出来数据纹丝不动没有任何报错。这种“没反应”是最难排查的故障之一。2.3 从 NOR Flash 迁过来要改掉的习惯如果你之前用过 W25Q 这类 SPI NOR Flash有几个习惯一定要改。第一不要再发扇区擦除命令0x20或整片擦除命令0xC7MRAM 没有擦除操作发了也不认识。第二NOR Flash 写数据前要等 BUSY 位而 MRAM 写入是即时的不需要等待擦除或编程时间读状态寄存器主要是为了看 WEL而不是等忙状态。第三块保护位的含义可能不同别沿用旧代码里的状态寄存器读写逻辑建议以你手里这只批次的数据手册为准。还有一个非常隐蔽的问题MR25H40CDF 支持 SLEEP 指令芯片一旦进入睡眠模式会忽略绝大多数 SPI 命令。如果在软件里不小心发过 0xB9又没有发 0xAB 唤醒后续读写就会像“失联”一样。稳妥的做法是在每次上电初始化时先发一条 WAKE 命令确保器件处于活跃状态这条命令成本极低但能省掉大量前期排查时间。3. 电路连接与电源设计两块芯片之间的可靠握手3.1 引脚连接与电平匹配PIC18F46K20 的 MSSP 模块默认引脚是 RC3/SCK、RC4/SDI、RC5/SDO我把这三根线直接接到 MR25H40CDF 的 SCK、SO、SI 上。很多新手在这里会搞混方向MRAM 的 SI 是芯片输入要接单片机的 SDOMRAM 的 SO 是芯片输出要接单片机的 SDI。接反的结果往往是“写进去了但读不出来”或者干脆全部读到 0xFF。PIC18F46K20 引脚方向MR25H40CDF 引脚说明RC3/SCK输出SCKSPI 时钟RC5/SDO输出SI单片机发给 MRAM 的数据RC4/SDI输入SOMRAM 返回给单片机的数据RD0任意 GPIO输出CS片选低电平有效GPIO 或 10k 上拉—WP写保护低电平禁止写直接接高或 10k 上拉—HOLD暂停传输低电平有效CS 引脚我没有用 MSSP 的硬件从机选择因为工业现场经常需要精细控制片选时序用普通 GPIO 更灵活。WP 和 HOLD 两个引脚必须处理好这是新手最容易忽略的地方。HOLD 引脚如果悬空一旦被噪声拉低SPI 传输会被暂停表现就是数据错位、命令丢失极其诡异。所以 HOLD 要么直接接 VCC要么通过 10k 电阻上拉。WP 引脚如果不做硬件写保护就直接上拉接高如果以后想在生产完成后锁定参数区可以把 WP 接到一个 GPIO 上默认输出高需要禁写时拉低。3.2 电源滤波、掉电检测与 PCB 细节供电方面MR25H40CDF 工作在 3.3VPIC18F46K20 也在 3.3V 下运行不需要电平转换。VDD 引脚旁放一个 0.1uF 陶瓷电容再在电源入口放一个 10uF 钽电容或者电解电容用于吸收 SPI 突发传输时的电流尖峰。SPI 走线如果超过 5cm建议在 SCK、SI、SO 上各串一个 22~33Ω 的电阻用来抑制振铃线长控制在 10cm 以内最佳。掉电检测是整个工业方案里必须认真对待的环节。PIC18F46K20 内部有 BOR 和 HLVD/LVD 模块可以把低压检测阈值设在 2.9V 左右。当 3.3V 电源开始跌落时MCU 先收到高优先级中断此时还有几毫秒的余电可以执行紧急保存。如果只用 BOR电压掉到复位阈值才会触发那时候主控已接近复位边缘保存动作容易被硬件复位打断。我的做法是开启 HLVD 中断在中断里做最精简的保存操作只写关键 32 字节几毫秒绰绰有余。你也可以外接一颗电压监控芯片比如 MCP112-315 之类的效果更直接。4. PIC18F46K20 的 MSSP 驱动与读写函数实现4.1 MSSP 模块寄存器配置PIC18F46K20 工作在 64MHz 系统时钟下时MSSP 主模式可以配置为 Fosc/4也就是 16MHz 的 SPI 时钟完全在 MR25H40CDF 的规格内。不过工业现场如果走线较长我会降速到 4MHz 左右后面第 6 章会细说原因。下面这段初始化代码是我在 MPLAB X XC8 环境里用的目标是 SPI 模式 0也就是 CPOL0、CPHA0这也正是 MR25H40CDF 数据手册默认的时序关系。#define CS_TRIS TRISDbits.TRISD0 #define CS_LAT LATDbits.LATD0 void spi_master_init(void) { TRISCbits.TRISC3 0; // SCK 输出 TRISCbits.TRISC4 1; // SDI 输入 TRISCbits.TRISC5 0; // SDO 输出 CS_TRIS 0; CS_LAT 1; // CS 默认高 SSP1CON1 0x00; // 先关闭 MSSP SSP1STAT 0x40; // SMP0, CKE1配合 CKP0 得到模式 0 SSP1ADD 0x00; // Fosc / (4 * (SSPADD1)) 16MHz SSP1CON1 0x22; // CKP0, SSPEN1, SPI Master, Fosc/4 }有一点要提醒不同版本的头文件里MSSP1 寄存器可能写作 SSP1CON1 或 SSPCON1它们是同一个物理寄存器的不同别名编译报错了就换成另一种写法。初始化顺序也很重要先配置模式和引脚最后再使能 MSSP避免模块在引脚方向还没设好时就开始输出。4.2 字节收发与 BF 位陷阱SPI 是同步全双工协议每发送一个字节的同时就会收到一个字节。MSSP 模块的 BF 位是缓冲区满标志发送完一个字节后 BF 会置 1此时必须读取 SSPBUF 才能把它清掉。很多新手在这里翻车写完一个字节后忘记读缓冲BF 一直被置位下次发送直接卡死。void spi_write_byte(uint8_t d) { SSP1BUF d; while (!SSP1STATbits.BF); (void)SSP1BUF; // 必须读一次清掉 BF 位 } uint8_t spi_read_byte(void) { SSP1BUF 0x00; // 发送 dummy 时钟把数据从 SO 挪进来 while (!SSP1STATbits.BF); return SSP1BUF; }注意读函数里发的 dummy 可以随便写 0x00也可以写 0xFF目的只是产生时钟边沿让从机把数据吐出来。4.3 MRAM 读写 API 完整代码我习惯把 MRAM 的操作封装成四个基础函数写使能、读状态、连续读、连续写。这里给出可直接抄作业的版本。void mram_wren(void) { CS_LAT 0; spi_write_byte(0x06); // WREN CS_LAT 1; } uint8_t mram_read_status(void) { uint8_t st; CS_LAT 0; spi_write_byte(0x05); // RDSR st spi_read_byte(); CS_LAT 1; return st; } void mram_write(uint32_t addr, const uint8_t *buf, uint16_t len) { mram_wren(); // 每次写之前必须先使能 CS_LAT 0; spi_write_byte(0x02); // WRITE spi_write_byte((uint8_t)((addr 16) 0x07)); spi_write_byte((uint8_t)(addr 8)); spi_write_byte((uint8_t)addr); while (len--) { spi_write_byte(*buf); } CS_LAT 1; } void mram_read(uint32_t addr, uint8_t *buf, uint16_t len) { CS_LAT 0; spi_write_byte(0x03); // READ spi_write_byte((uint8_t)((addr 16) 0x07)); spi_write_byte((uint8_t)(addr 8)); spi_write_byte((uint8_t)addr); while (len--) { *buf spi_read_byte(); } CS_LAT 1; }地址为什么要(addr 16) 0x07因为 MR25H40CDF 只有 19 位有效地址最高 5 位应当置 0。如果你直接发完整 24 位地址有些芯片可能不认这算是一个细节坑。上电初始化时建议加一条 WAKE 命令防止器件处于睡眠状态影响首条读写void mram_init(void) { spi_master_init(); CS_LAT 0; spi_write_byte(0xAB); // WAKE CS_LAT 1; }4.4 封装成存储服务层与地址规划驱动函数只是基础真正工程里我不会让业务代码直接调mram_write而是再包一层存储服务。比如storage_save_record(record_id, data, len)负责写入带 CRC 的记录storage_load_record()负责校验并读出上层根本不需要关心底层存储是 MRAM、EEPROM 还是 NOR Flash以后换芯片也只需要改这一层。地址规划我建议固定下来不要随意摆放地址范围用途0x00000 ~ 0x0003F全局配置区A/B 双槽0x00040 ~ 0x000FF设备校准参数0x10000 ~ 0x1FFFF64KB 循环日志区0x7FE00 ~ 0x7FFFF紧急掉电保存区把配置区和日志区隔开防止日志回卷时误伤配置数据。MRAM 不像 Flash日志区写满后可以原地覆盖最旧的数据不需要擦除这种特性做循环日志简直不要太舒服。5. 工业现场的数据完整性设计别因为是 MRAM 就放松5.1 被中断的写命令也有半截问题很多文章宣传 MRAM“写不完不会丢”“无限次写入”这容易让人产生一个错觉掉电随便写都没事。MR25H40CDF 的写入确实很快但它本质上还是通过 SPI 串行接收命令和数据。如果 CS 在命令传输中途被拉高或者电源在某个字节传输到一半时跌落这个字节的最终结果是不确定的——可能没写进去可能写了一半。因为每个 bit 是独立的磁隧道结翻转不能保证一整条多字节命令的原子性。所以我的原则是MRAM 解决了“寿命”和“速度”两大痛点但没有解决“写了一半怎么办”的问题。解决这个问题必须靠软件层的校验和备份机制不能把宝全押在硬件上。5.2 A/B 双槽与 CRC 校验布局对于关键参数我采用经典的双槽交替写入方案。比如配置区用两块 32 字节区域A 槽和 B 槽各存一份完整记录。记录格式固定为4 字节魔数比如 0xA5A5A5A5、2 字节版本号、若干字节数据、4 字节 CRC32。写入流程是先读 A、B 两槽找出当前版本较新且 CRC 校验通过的那一块下次写入时写另一块。上电读取时同样先验魔数和 CRC哪个完整且版本新就加载哪个。这样做有两个好处一是任何时刻至少有一份完整数据掉电打断写入最多损坏当前槽旧槽依然可用二是避免频繁写同一块区域虽然 MRAM 寿命无限但双槽能进一步降低任何潜在风险。代码上只多花不到 300 字节工程上却把“写一半”的风险压到了几乎为零这笔投资非常划算。5.3 紧急断电保存流程掉电保存流程我用 HLVD 中断来实现。流程大致是HLVD 检测到电压降到阈值以下触发高优先级中断中断入口立即关闭其他中断防止保存过程被打断把要保存的现场数据通过mram_write()写入紧急掉电保存区写完后清中断标志然后进入死循环等待系统掉电复位或者直接触发软件复位。32 字节数据加 4 字节命令地址在 16MHz SPI 时钟下写一轮大概也就几十微秒而电源从 3.3V 跌到 MCU 复位电压通常有数毫秒的余量时间上非常充裕。需要特别注意的是掉电中断里千万不要调用复杂的文件系统或者带互斥锁的服务函数直接操作最底层的写函数即可越简单越可靠。5.4 地址边界与环形日志环形日志实现起来很简单但有个边界必须处理。MR25H40CDF 的地址回卷发生在 0x7FFFF 到 0x00000如果日志区设在 0x10000 ~ 0x1FFFF从尾部写回头部时不能只靠指针自增得拆成两段写先写从当前写指针到区域末尾的数据再写从区域开头到剩余长度的数据。用位运算配合 2 的幂次区域大小可以简化取模操作但工程上我更建议写一个显式拆分的函数出问题时容易排查。6. 实测踩坑排查从全 0xFF 到偶发数据错6.1 读回清一色 0xFF第一次上板我满心期待读回数据结果读出来全是 0xFF写入似乎完全不起作用。排查链路是这样的先用万用表确认 SCK、SI、SO 引脚没有接反再确认 RC3/RC4/RC5 的复用功能配置是否正确。然后拿逻辑分析仪抓 CS、SCK、SI 三根线发现命令和地址确实在发但 SO 上始终没有有效数据返回。这里最典型的原因是 SPI 模式不匹配。MRAM 数据手册默认时序是模式 0也就是 SCK 空闲为低、数据在第一个边沿被采样。如果 MSSP 的 CKP 和 CKE 配置不对读回来的就是无效数据很可能全 0xFF 或者全 0x00。最可靠的办法是用示波器对比 SCK 空闲电平和数据线的变化边沿和手册时序图逐项核对不要只看网上代码。6.2 WP 悬空导致写入被静默忽略解决了模式问题后读写都正常了但放到另一块板上又出现“写入没效果”的怪现象。写入函数返回正常读回来却是旧数据。排查到最后发现那块板上 WP 引脚悬空了。WP 低电平时芯片禁止写入悬空状态下引脚电位受干扰影响可能在写操作那一瞬间被拉低于是整条 WRITE 命令被静默忽略。给 WP 加上 10k 上拉电阻后问题再也没有出现过。这个坑非常隐蔽因为芯片没有任何状态位告诉你“写入被拒绝了”唯一能根据的状态就是 RDSR 的 WEL 位。调试时可以读一下 WEL如果发送 WREN 之后 WEL 一直不置位大概率就是 WP 或者供电有问题。6.3 高速长线上的偶发比特错16MHz 时钟在短距离贴板布局完全没问题但我的测试板通过 20cm 排线连接时出现了偶发的 1 bit 数据错误。用示波器看 SCK 和 SI 波形上升沿有明显振铃信号过冲接近 1V。这导致接收端偶尔采到了错误电平。解决办法有三个我全做了SPI 时钟从 16MHz 降到 4MHzSPI 线上串联 33Ω 电阻抑制振铃尽量缩短排线长度。工业产品不要盲目追求高速稳定压倒一切。降速后波特率寄存器按公式反过来算SSPADD 1 Fosc / (4 * 期望频率)64MHz 下要得到 4MHzSSPADD 设为 3 即可。6.4 与 MRAM 特性相关的两个隐藏坑最后一个坑是上电后首条命令偶发“失联”。原因是芯片可能处于睡眠状态0xAB 的 WAKE 命令必须在初始化时发送否则 SLEEP 状态下命令会被忽略。我后来把 WAKE 固定放在mram_init()第一行这个问题就消失了。另一个隐藏坑是地址回卷。代码里如果用write_ptr len而不判断是否越过 0x7FFFF日志数据会悄悄跑到 0x00000 覆盖配置区。我踩过一次后养成了习惯每次 ESP 日志写完后检查写指针是否越界越界就拆分写。MRAM 确实不需要像 NOR Flash 那样管理坏块和擦除边界但地址边界的管理反而是从 Flash 迁移过来时最容易忽略的点。这套方案在项目里跑了一年多再没有出现过写穿、参数丢失或者掉电数据损坏的情况。如果让我总结最核心的体会那就是 MRAM 改变了存储策略的思维方式EEPROM 要省着写、Flash 要先擦后写而 MRAM 可以当成一块永不丢失的 SRAM 随意写。但无论底层换什么芯片关键数据带的 CRC 校验和 A/B 双槽备份永远值得保留。以后再遇到“频繁掉电高频写入”的需求我大概率还是会第一时间拿起 MR25H40CDF 这类 SPI MRAM而不是继续跟 EEPROM 的寿命较劲。
返回列表