嵌入式行业里,非易失存储一直是躲不开的话题。工业设备跑着跑着突然断电,数据不能丢,配置不能乱。常用的 EEPROM 擦写次数有限,Flash 要整块擦除,而在频繁记录数据的场景里这两种都让人头疼。我最近在一台工业数据采集终端上,把 MR25H40CDF 这颗 4Mbit SPI MRAM 和 PIC18LF45K50 这颗 8 位 MCU 搭在一起,用来存储和读取运行数据。整套方案做下来,稳定性和速度都超出了预期。这篇就围绕这个组合,把硬件连接、寄存器配置、读写流程和踩过的坑一次说清楚,适合正在做工业数据记录、设备参数存储的嵌入式工程师参考。
1. 存储选型:MR25H40CDF 凭什么挤掉 Flash 和 EEPROM
1.1 工业设备存数据,先算寿命账
做工业设备的人都知道,存储器件选型不能只看“能存多少”。很多产品批量出货后,真正的噩梦常常来自存储寿命。
一块常规 SPI NOR Flash,标称擦写次数大概在 10 万次到 100 万次之间,而且必须先擦除再写。如果设备每秒钟记录一次温度、振动、电压这类工况数据,一天就是 86400 次写入。按 100 万次寿命算,撑不过半个月。EEPROM 虽然便宜、接口简单,寿命通常也只有 100 万次左右,同样撑不住高频写入。就算用外部铁电存储器 FRAM,容量和成本也是问题。
MRAM 的优势正好砸在这些痛点上。标称写入耐力达到 10 的 15 次方级别,基本上可以当作“无限次写”来用。写之前不用擦除,一个地址反复写也不会衰减。配合工业级温度范围和掉电保持能力,非常适合做长期运行日志、关键参数备份和故障记录。MR25H40CDF 作为 Everspin 的 4Mbit SPI MRAM,容量 512KB,在嵌入式场景里正好卡在“需要数据记录但不至于用大容量 Flash”的位置上。
1.2 MR25H40CDF 的关键参数和优势
先列出我实际使用中比较关心的几个点。
- 容量:4Mbit,也就是 512KByte。工业数据记录、设备运行参数、电表事件记录,这个容量根本用不完,还能做多份冗余。
- 接口:标准 SPI,四线制:SCK、SI、SO、CS,外加 WP 和 HOLD。PIC18LF45K50 自带 MSSP 硬件 SPI,不需要软件模拟,占用引脚很少。
- 供电:3.3V,和 PIC18LF45K50 同一个电压域,不需要电平转换。
- 写入速度:SPI MRAM 写入是“即时写”,指令带地址带数据,CS 拉高就完成,不需要像 Flash 那样等页擦除。我实际把 SPI 时钟配置到 4MHz 左右,写入 256 字节数据耗时远远小于同等容量 Flash。
- 数据保持:MRAM 掉电后数据保持能力按年算,手册给出的保持年限很充裕,工业产品完全够用。
- 封装后缀:CDF 这类后缀一般对应封装形式、温度等级和包装方式。拿货的时候我习惯直接找代理核对数据手册,避免买错工业级还是商业级。
对比常用的存储介质,MRAM 的特点一句话总结:它把 SRAM 的写入速度和 Flash 的非易失性结合到了一起,代价是价格比 Flash 高,但在工业品里这钱花得值。
1.3 PIC18LF45K50 和 MRAM 是搭子
PIC18LF45K50 是 Microchip 的 8 位 MCU,主频可以跑到 48MHz,带 USB 接口、多个定时器、DMA,还有 nanoWatt XLP 低功耗技术。我选它的原因是项目里正好需要一个 3.3V 供电、引脚不算太少的 MCU,同时要方便现场用 USB 导出数据。PIC18LF45K50 的 MSSP 模块就是用来做 SPI 的,硬件收发缓冲、时钟分频这些都有,比完全用 GPIO 模拟 SPI 省心太多。
和 MRAM 搭配时,两边都是 3.3V 电平,SPI 模式也兼容,硬件上几乎不需要额外处理。更重要的是,PIC18LF45K50 的 SPI 时钟可以分到比较低,方便我在调试阶段用几 kHz 到几 MHz 的时钟测试 MRAM 的响应。等稳定后再把时钟升上去,避免一上来就吃高频信号的亏。
2. 硬件连接:SPI 总线上的几个容易栽的细节
2.1 引脚连接表
我用的连接方式比较常规。CS 没有接到 MSSP 的硬件从选择引脚上,而是用普通 GPIO 控制,这样灵活性最好。下面是连接表。
| MR25H40CDF 引脚 | PIC18LF45K50 引脚 | 电平要求 |
|---|---|---|
| SI | SDO1 | MOSI 数据输入 |
| SO | SDI1 | MISO 数据输出 |
| SCK | SCK1 | SPI 时钟 |
| CS | 任意 GPIO,如 RB2 | 低有效片选 |
| WP | 3.3V | 拉高,允许状态寄存器写入 |
| HOLD | 3.3V | 拉高,禁用 HOLD 暂停功能 |
| VDD | 3.3V | 供电 |
| VSS | GND | 共地 |
注意,不同芯片的 MSSP 引脚定义可能不同,比如 SDO1、SDI1 在封装上可能是换成其他编号,所以画原理图之前一定要打开 PIC18LF45K50 的数据手册和封装图核对一遍。
2.2 WP、HOLD、CS 的“防呆”处理
这三根线看着简单,处理不好能坑你一整天。
WP 是写保护引脚,低电平时禁止通过 WRSR 指令修改状态寄存器。如果 WP 悬空或者不小心被拉到低电平,你会发现硬件连接看起来完全正确,但状态寄存器写不进去,某些读保护配置改不了。我在工业环境下干脆用一个 10k 电阻把 WP 上拉至 3.3V,确保默认状态下不锁状态寄存器。
HOLD 引脚的作用是暂停 SPI 通信。正常工作时要把它拉高。很多人默认 HOLD 悬空也能跑,但在有强干扰的现场,悬空引脚很容易被感应的噪声拉低,导致通信到一半 MRAM “暂停”,数据就错乱了。所以同样要上拉。
CS 也要做上拉处理。MCU 上电瞬间,GPIO 默认可能是高阻态,如果 CS 没有外部上拉,MRAM 可能被误选通,收到 MCU 引脚电平稳定前的乱码。加一个 10k 上拉后,CS 默认高电平,只有 MCU 主动拉低时才选中器件。
2.3 供电和 PCB 布局
MRAM 虽然是存储芯片,但它本质上是随机存取存储器结构,翻转速度很快,对电源噪声比普通 Flash 敏感一些。我在 MRAM 的 VDD 引脚旁边放了 0.1uF 陶瓷电容和 10uF 钽电容,两个电容尽量贴近芯片引脚。如果板子上空间允许,MCU 的电源也单独去耦。
SPI 信号在低频时随便飞线都能跑,但工业现场不是实验室。SCK、SI、SO、CS 这几根线如果走线太长,串扰和信号边沿退化就会显现出来。我一般把 SPI 信号线走在一起,尽量保持等长,地和电源不要夹在中间。SCK 信号单独包地,能有效减少对其他线的干扰。如果 MCU 和 MRAM 不在一块板上,距离超过 10cm,建议加 SPI 隔离或至少加缓冲器,不要直接用长线拖到主板上。
3. 初始化配置与读写流程
3.1 指令集速查
MR25H40CDF 的指令集和普通 SPI Flash 很像,下面的表是我在固件里用到的几条关键指令。
| 指令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写状态寄存器前必须使用 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读设备 ID |
地址是 24 位,对应 512KB 地址空间。因为 4Mbit 只用到 0x000000 到 0x07FFFF,所以高字节的高位实际会被忽略。但发送地址时仍然要按 3 字节发送,顺序是高位在前、低位在后。
3.2 SPI 初始化:手写还是用 MCC
我个人建议直接用 MPLAB X 的 MCC 生成 SPI 初始化,省事、出错少。关键点是把 MSSP1 配置成主模式,选择 SPI Mode 0,也就是空闲时钟为低、数据在第一个边沿采样。MRAM 手册支持的 SPI 模式一般包括 Mode 0 和 Mode 3,Mode 0 最通用,和 PIC 的 MSSP 配合也最顺。
MCC 生成后,初始化函数大概长这样:
void spi1Initialize(void) { // 使用 MSSP1,主模式,SPI Mode 0 // 时钟分频按实际主频调整 SSP1CON1 = 0x22; // SPI 使能,主模式,分频系数对应 64 分频 SSP1STAT = 0x40; // 采样点设置:主模式下数据在中间采样 TRISC &= 0xFB; // 设置 SCK 输出,具体引脚按实际原理图调整 TRISB |= 0x10; // 设置 SDI 输入 }上面是示意代码,具体寄存器值受主频、引脚映射影响,直接照抄不一定能跑。正确做法是在 MCC 里选择主频和 SPI 时钟,让工具算出分频系数,再检查生成的寄存器值。初始化完成之后,我习惯先发送 RDSR 指令读状态寄存器。如果读到 0xFF 或者 0x00 以外的数值,说明通信链路基本通了;如果一直是 0xFF,先查接线和 SPI 模式。
3.3 读数据流程
读取流程很简单,核心是先拉低 CS,发送 READ 指令,接着发 3 字节地址,然后连续读数据。读完后拉高 CS。
实际代码里,我用一个通用函数完成“发送一个字节并获得返回值”,这样既能发送指令,也能在接收模式下用空字节产生时钟。
uint8_t MRAM_SendByte(uint8_t byte) { SSP1BUF = byte; while (!SSP1STATbits.BF); return SSP1BUF; // 读取接收字节,同时清除 BF 标志 } void MRAM_ReadBytes(uint32_t addr, uint8_t *buf, uint32_t len) { MRAM_CS_LOW(); MRAM_SendByte(0x03); // READ MRAM_SendByte((uint8_t)(addr >> 16)); MRAM_SendByte((uint8_t)(addr >> 8)); MRAM_SendByte((uint8_t)addr); for (uint32_t i = 0; i < len; i++) { buf[i] = MRAM_SendByte(0xFF); // 发哑字节,收数据 } MRAM_CS_HIGH(); }注意 MSSP 的 BF 标志表示接收缓冲器已满,其实也代表一个字节传输完成。发送每个字节前,确保上一字节带来的接收数据已经被读走,否则容易置 SSPOV 溢出标志,通信就会乱掉。上面把发送和收数合并成一个函数,就解决了这个问题。
3.4 写数据流程
写数据分两步走。第一步是发 WREN 写使能,第二步是发 WRITE 指令。对 MRAM 本身来说,它的存储单元写入不一定强制依赖 WREN,但写状态寄存器之前必须 WREN。我为了保险,写数据前也会发一次 WREN,实测没有副作用,反而能避免一些从 Flash 移植过来时的逻辑问题。
void MRAM_WriteEnable(void) { MRAM_CS_LOW(); MRAM_SendByte(0x06); // WREN MRAM_CS_HIGH(); } void MRAM_WriteBytes(uint32_t addr, const uint8_t *buf, uint32_t len) { MRAM_WriteEnable(); MRAM_CS_LOW(); MRAM_SendByte(0x02); // WRITE MRAM_SendByte((uint8_t)(addr >> 16)); MRAM_SendByte((uint8_t)(addr >> 8)); MRAM_SendByte((uint8_t)addr); for (uint32_t i = 0; i < len; i++) { MRAM_SendByte(buf[i]); } MRAM_CS_HIGH(); }这里要特别注意 CS 的时序。CS 必须先拉低,再发 WREN,发完立刻拉高。WREN 的 CS 高电平结束动作就是“锁存写使能”的那一刻,如果提前拉高或者漏了不是高电平结束,写使能可能不生效。WRITE 操作必须从 CS 拉低开始,一直到所有数据发送完,CS 才能拉高。中途 CS 意外弹起,写入会立即终止,前面的字节可能白写。
写入长度方面,MRAM 没有 Flash 那种 256 字节页限制,理论上可以连续发多个字节直到 CS 拉高。但我习惯把一次写入长度控制在 512 字节以内,方便和读回校验的缓冲区对应,也符合实际应用的分包需求。
3.5 校验和冗余存储
工业数据最怕读到被破坏的内容。写寄存器、写配置参数的时候,我通常不是单纯写入,而是加一层保护。
简单做法是:在地址空间里划分几个区域,比如主配置区、备份配置区、运行日志区。每个区域开头放几个字节的“魔数”,比如 0xA5 0x5A,后面跟着数据,最后放 4 字节 CRC32。读取时先判断魔数对不对,再算 CRC,如果不对就切换到备份区。如果备份区也损坏,才恢复出厂默认值。
这样虽然多花了几百字节存储空间,但能显著降低设备在强干扰环境下出现配置丢失的概率。MRAM 本身不容易丢数据,但外部总线上的噪声可能导致写入错误数据,加入 CRC 后至少能识别出来。
4. 实战排查:我踩过的坑和验证方法
4.1 SPI 模式不对,读回来全是 0xFF
第一次调这块板子的时候,我配置完 SPI,调用 RDSR,结果读回来的数据一直是 0xFF。查接线、查供电、查 CS 电平,都没问题。后来用示波器抓 SCK 和数据线,才发现 SCK 空闲电平是高还是低和 MRAM 的预期不一致。
SPI 模式需要 MRAM 和 MCU 匹配。MRAM 支持 Mode 0 和 Mode 3,但 PIC 的 MSSP 如果配置成 Mode 1 或 Mode 2,数据采样边沿就会错开,读取结果自然不对。解决办法是在 MCC 里直接把 SPI Mode 设成 Mode 0,重新生成代码,问题立刻消失。以后再遇到通信不工作,我第一个动作就是用示波器看 SCK 空闲电平和 MOSI 数据变化时刻,不要先盲目改分频或换芯片。
4.2 写入无效,先看 WP 和地址
另一个常见问题是写数据后读回来还是旧值。有次我发现写状态寄存器不生效,查了 WP 引脚,发现原理图把它接到了 GND,这就等于把状态寄存器写保护锁死了。后来改成上拉,问题解决。
还有个隐蔽的坑是地址溢出。MR25H40CDF 只有 512KB 空间,如果用 24 位地址访问超过 0x07FFFF 的地方,地址位会回绕到低地址,造成“写到 A 地址却出现在 B 地址”的假象。我在写日志循环队列时,对地址做了取模处理,才避免这个问题。
写入无效时,请按这个顺序排查:CS 时序是否完整、WREN 是否发出且 CS 高电平结束、WP 是否正确拉高、地址是否越界、电压是否稳定。不要一上来就觉得芯片坏了。
4.3 上电掉电验证,不能只测读写
只调通读写命令还不算完,工业设备必须做掉电验证。我的测试方法是用一个交流接触器控制工装电源,让 MCU 在写入过程中随机断电,循环几百次,然后上电检查数据是否完整。因为 MRAM 写入是即时的,只要 WRITE 指令完整发送完,数据就落住了,整体表现比 Flash 好很多。
但要注意,MCU 掉电瞬间如果电源跌落导致 SPI 信号抖动,MRAM 收了一部分乱字节,也会把错误数据写进地址。解决思路是加电源检测芯片,当电压掉到阈值以下时,MCU 第一时间拉高 CS 并进入复位,切断正在进行的 SPI 事务。同时结合应用层 CRC,把这种极端情况兜住。
另外,新出厂的 MRAM 芯片内部数据可能是随机的,首次烧录前最好全片写一次 0x00 或固定模式,避免把杂散数据当成有效记录。
4.4 工业现场抗干扰的几个有效措施
现场走线和实验室不同,我带设备到车间里跑过一段,发现偶尔会出现读到的数据跳变。后来做了几件事,问题基本绝迹。
第一,SPI 线全部压在 PCB 内层,不跨分割区,SCK 优先级最高。第二,MCU 和 MRAM 的电源分别加滤波,MRAM 靠近电源入口处加磁珠。第三,如果 MRAM 和主控距离接近,我给 CS 加了一个 100 欧姆串联电阻,降低边沿过冲。第四,固件里所有读取操作都加了重试,连续读两次结果一致才采用。
还有一个容易被忽略的点:如果板子上有电机、继电器这类干扰源,务必在继电器线圈两端加 RC 吸收,触点必要时加压敏电阻。不然干扰顺着电源和地线传到 SPI 总线,芯片本身再稳也没用。
最后说一点个人体会。MR25H40CDF 和 PIC18LF45K50 这个组合,真正爽的地方不是某一个指标,而是“省心”。MRAM 不用管理等擦除时间,不用频繁搬家磨损均衡,SPI 流程简单到几乎像操作 SRAM;PIC18LF45K50 的 MSSP 又正好把这个流程接得非常顺。后来我再做类似的数据记录产品,存储介质首选还是 MRAM,只有当成本压力特别大时才会考虑退回去用高寿命 EEPROM 加磨损均衡。如果手里有现成的 PIC 平台,强烈建议拿这套组合先做一个最小系统试试,你会回来感谢那颗不用等写周期的 MRAM。