1. 项目缘起与方案选型思考
工业现场的数据存储有个很尴尬的处境:用 EEPROM 吧,写入速度慢得让人着急,擦写次数也就百万次级别,高频采集场景下没几个月就写废了;用 SRAM 加后备电池吧,电池没电数据就全丢,维护成本高得离谱;用 Flash 吧,写之前得先擦一个扇区,掉电时机不对还会丢数据。我前两年做一个工业数据采集终端的项目,就卡在这个存储环节上,后来选定了 MRAM 方案,主控用的是 PIC18LF4680,存储芯片是 MR25H40CDF,跑下来效果确实稳。
先说说这两颗芯片各自是什么定位。MR25H40CDF是 Everspin 出的 4Mbit 串行 MRAM(磁阻随机存储器),SPI 接口,40MHz 时钟,最关键是它具备 SRAM 级别的写入速度,同时又是非易失的,掉电数据不丢,读写寿命号称无限次。PIC18LF4680是 Microchip 的一款 8 位单片机,自带 MSSP 模块可以硬件跑 SPI,工作电压范围宽,工业温度等级也有,非常适合做这种中小规模的数据采集和存储控制。
为什么选 MRAM 而不是继续用 Flash 或者 FRAM?这里面的取舍值得展开说。Flash 的问题在于写入粒度,你哪怕只改一个字节,也得把整个扇区读到 RAM 里、改完再擦除整个扇区写回去,这个过程耗时且掉电风险大。FRAM 虽然写入快、寿命也高,但容量普遍偏小,而且价格按容量算下来比 MRAM 还贵,供货也不如 MRAM 稳定。MRAM 的写入是直接覆盖,不需要擦除,字节级写入,40MHz SPI 下理论带宽能到 5MB/s,对于工业采集这种每秒几百到几千个采样点的场景完全够用。
从系统架构上看,这个组合解决的核心问题是:在无电池、无超级电容的条件下,实现高频次、掉电安全的数据记录。适合的场景包括工业设备运行日志、电力监测终端、车载黑匣子、医疗设备参数记录等。适合阅读这篇文章的读者,应该是有一定单片机基础、了解 SPI 通信、正在做嵌入式数据存储方案选型的工程师,或者正在学习嵌入式外设驱动的学生。
注意:MRAM 虽然写入寿命近乎无限,但并不意味着可以无限制地高频写入而不考虑功耗和总线占用,实际设计中仍需做写入缓冲和批量提交策略。
2. 硬件连接与 SPI 总线设计要点
2.1 引脚连接与硬件片选
MR25H40CDF 是 8 引脚封装,SPI 四线制加片选。和 PIC18LF4680 连接时,我用的引脚分配是这样的:
| MR25H40CDF 引脚 | 功能 | PIC18LF4680 引脚 | 说明 |
|---|---|---|---|
| CS | 片选 | RC2 | 普通 GPIO 控制 |
| SO | 数据输出 | RC4(SDI) | MSSP 输入 |
| SI | 数据输入 | RC5(SDO) | MSSP 输出 |
| SCK | 时钟 | RC3(SCK) | MSSP 时钟 |
| VDD | 电源 | 3.3V | 需加 0.1uF 去耦 |
| VSS | 地 | GND | 就近接地 |
| WP | 写保护 | 接 VDD 或 GPIO | 建议 GPIO 可控 |
| HOLD | 保持 | 接 VDD | 不用时拉高 |
这里有个细节很多人会忽略:WP 和 HOLD 引脚不能悬空。WP 悬空时芯片内部状态不确定,可能导致写入被意外禁止或者意外使能。我的做法是 WP 接到一个空闲 GPIO 上,软件里可以主动控制写保护,调试阶段拉高允许写入,量产固件里可以根据需要拉低保护关键数据区。HOLD 引脚如果不用就老老实实拉到 VDD,悬空会引入噪声,在工业电磁环境下尤其容易出问题。
2.2 硬件片选还是软件片选
热搜词里有人问 SPI 硬件片选和软件片选的区别,这里结合项目实际说一下。PIC18LF4680 的 MSSP 模块在 SPI 主模式下,片选信号是需要软件控制的,它不像有些 ARM 芯片那样有硬件 NSS 自动管理。所以在这个项目里,片选就是普通 GPIO,拉低选中,拉高释放。
那硬件片选和软件片选到底差在哪?硬件片选是 SPI 外设自动在传输前后拉低拉高 CS,时序精准,适合高速连续传输;软件片选是手动控制 GPIO,灵活但需要程序员保证时序正确。在这个项目里,因为只有一颗 SPI 从设备,软件片选完全够用,而且更灵活——比如我可以在两次传输之间插入延时,或者在不传输时把 CS 拉高让芯片进入低功耗状态。
实操心得:软件片选时,CS 拉低到第一个 SCK 上升沿之间要留至少 5ns 的建立时间,MR25H40CDF 手册里写的是 CS 建立时间最小 5ns,PIC18 在 40MHz 下指令周期 100ns,实际上只要在拉低 CS 后执行一条 NOP 再开始传输就足够了。
2.3 电源与去耦设计
MRAM 的写入电流比读取电流大不少,写入瞬间的电流尖峰如果电源去耦没做好,会导致电压跌落,轻则写入失败,重则芯片复位。我在 PCB 上的做法是:VDD 引脚旁边放一个 0.1uF 的陶瓷电容,距离引脚不超过 2mm,再在电源入口处放一个 10uF 的钽电容做储能。如果是工业环境,电源线上再串一个磁珠抑制高频噪声。
地线处理上,MRAM 的 VSS 要和单片机的 GND 单点连接,避免数字地和模拟地混在一起形成地环路。SPI 信号线如果走线超过 10cm,建议在 SCK 和 SI 上串 22 欧姆的电阻做阻抗匹配,减少反射。
3. SPI 通信协议与寄存器操作详解
3.1 MR25H40CDF 的指令集
MR25H40CDF 的指令集不复杂,常用的就几条:
| 指令名称 | 指令码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写入前必须先发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
状态寄存器里主要关注 WEL 位(写使能锁存)和 WIP 位(写进行中)。WEL 位在每次 WREN 后置 1,写入完成后自动清零。WIP 位在写入过程中为 1,写入完成后为 0。
这里有个容易踩的坑:每次写入操作前都必须发 WREN 指令,哪怕你连续写多个地址,只要中间没有 CS 拉高再拉低,WREN 只发一次就行。但如果 CS 拉高了,WEL 位就会自动清零,下次写入前必须重新发 WREN。我一开始没注意这个细节,调试时发现偶尔写入失败,后来用逻辑分析仪抓波形才发现是 CS 操作后忘了重新使能写。
3.2 PIC18LF4680 的 MSSP 初始化
PIC18LF4680 的 MSSP 模块配置成 SPI 主模式,关键寄存器是 SSPCON1、SSPSTAT、SSPADD。初始化代码如下:
void SPI_Init(void) { TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC4 = 1; // SDI 输入 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC2 = 0; // CS 输出 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SPI Master Fosc/4 SSPSTAT = 0x40; // CKE=1, 数据在时钟上升沿采样 SSPADD = 0; // 主模式下不用 CS = 1; // 初始释放片选 }SSPCON1 的 bit4 是 CKP,决定空闲时时钟极性;SSPSTAT 的 bit6 是 CKE,决定数据在哪个时钟边沿采样。MR25H40CDF 支持 SPI 模式 0 和模式 3,我用的是模式 0(CPOL=0,CPHA=0),也就是空闲时 SCK 为低,数据在上升沿采样。这个配置下 CKP=0,CKE=1。
时钟频率方面,SSPCON1 的低四位是 SSPM,设成 0b0000 就是 Fosc/4。PIC18LF4680 我用的是 10MHz 晶振,Fosc/4 就是 2.5MHz,MR25H40CDF 最高支持 40MHz,所以还有很大余量。如果想跑更快,可以把 SSPM 设成 0b0001(Fosc/16)或者用 PLL 倍频,但工业环境下我不建议跑太高,2.5MHz 到 5MHz 足够稳定。
3.3 字节读写函数实现
先看写一个字节的底层函数:
unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; while(!PIR1bits.SSPIF); // 等待传输完成 PIR1bits.SSPIF = 0; return SSPBUF; }这个函数同时完成发送和接收,SPI 是全双工的,发一个字节的同时也会收一个字节。读 MRAM 时,先发指令和地址,然后继续发 dummy 字节来产生时钟,接收到的就是数据。
写数据的完整流程:
void MRAM_WriteByte(unsigned long addr, unsigned char data) { CS = 0; SPI_ExchangeByte(0x06); // WREN CS = 1; CS = 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); SPI_ExchangeByte(data); CS = 1; // 等待写入完成 while(MRAM_IsBusy()); }读数据的流程:
unsigned char MRAM_ReadByte(unsigned long addr) { unsigned char data; CS = 0; SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); data = SPI_ExchangeByte(0x00); // dummy 字节产生时钟 CS = 1; return data; }注意:MR25H40CDF 是 4Mbit,也就是 512KB,地址线需要 19 位,所以地址要发 3 个字节。很多人用 24 位地址习惯了,这里 3 字节地址的高 5 位是无效的,但发 0 就行,不影响。
4. 数据存储策略与掉电保护设计
4.1 环形缓冲区设计
工业采集场景下,数据是持续产生的,但存储空间有限,不可能无限写入。我的做法是在 MRAM 里划出一块区域做环形缓冲区,用两个指针管理:写指针和读指针。写指针指向下一个要写入的位置,读指针指向下一个要读取的位置。当写指针追上读指针时,说明缓冲区满了,这时候要么覆盖最老的数据,要么停止写入等待读取。
环形缓冲区的元数据(读写指针、缓冲区大小、魔数)存在 MRAM 的固定地址,每次上电时先读取这些元数据来恢复状态。因为 MRAM 是非易失的,掉电后指针不会丢,重新上电后可以接着上次的位置继续写。
typedef struct { unsigned long write_ptr; unsigned long read_ptr; unsigned long buf_start; unsigned long buf_size; unsigned long magic; } RingBufferMeta; RingBufferMeta meta; void RingBuffer_Init(void) { MRAM_ReadBuffer(META_ADDR, (unsigned char*)&meta, sizeof(meta)); if(meta.magic != 0xA5A5A5A5) { // 首次使用,初始化元数据 meta.write_ptr = BUF_START; meta.read_ptr = BUF_START; meta.buf_start = BUF_START; meta.buf_size = BUF_SIZE; meta.magic = 0xA5A5A5A5; MRAM_WriteBuffer(META_ADDR, (unsigned char*)&meta, sizeof(meta)); } }4.2 掉电检测与紧急写入
虽然 MRAM 写入速度快,掉电时数据不会丢,但有个前提是写入操作已经完成。如果掉电发生在 SPI 传输过程中,数据可能只写了一半。所以掉电检测电路还是必要的。
我的做法是用一个分压电阻网络监测电源电压,接到 PIC18LF4680 的 ADC 引脚上。正常工作时 ADC 每 10ms 采样一次,当检测到电压低于阈值(比如 3.0V)时,立即触发中断,在中断里把当前缓冲区里未提交的数据紧急写入 MRAM。因为 MRAM 写入不需要擦除,一个字节写入在 2.5MHz SPI 下大概 10us 左右,写 100 个字节也就 1ms,完全来得及。
实操心得:掉电检测的阈值要留足余量。我一开始设的 2.8V,结果发现电源从 3.3V 掉到 2.8V 的时间只有几百微秒,根本来不及写完。后来改成 3.0V 触发,并且把去耦电容加大到 100uF,争取到了 5ms 左右的缓冲时间,这才稳了。
4.3 数据校验与恢复
工业现场电磁干扰大,SPI 通信偶尔会出现位翻转。我在每个数据块后面加了一个 CRC16 校验,读取时先校验再使用。如果校验失败,尝试重新读取,连续三次失败就标记该块数据损坏,跳过继续读下一块。
CRC16 用查表法实现,速度快,占用空间小。PIC18LF4680 的 Flash 有 64KB,放一张 512 字节的 CRC 表绰绰有余。
unsigned int CRC16_Calculate(unsigned char *data, unsigned int len) { unsigned int crc = 0xFFFF; while(len--) { crc = (crc >> 8) ^ crc16_table[(crc ^ *data++) & 0xFF]; } return crc; }5. 性能实测与优化经验
5.1 读写速度实测数据
我在实验室用逻辑分析仪抓了实际波形,测出来的数据如下:
| 操作类型 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节写入 | 1 字节 | 约 12us | 83KB/s |
| 连续写入 | 256 字节 | 约 1.2ms | 213KB/s |
| 单字节读取 | 1 字节 | 约 8us | 125KB/s |
| 连续读取 | 256 字节 | 约 0.9ms | 284KB/s |
连续读写比单字节快很多,因为指令和地址只发一次,后面就是连续的数据流。所以实际使用中我尽量用批量读写,减少指令开销。
5.2 优化技巧
第一个优化是减少 CS 翻转次数。每次 CS 拉高再拉低,MRAM 内部需要重新同步,而且 WREN 也要重发。连续写入时,我在一次 CS 有效期内写完所有数据,中间不释放片选。
第二个优化是用 DMA 或者中断驱动 SPI。PIC18LF4680 没有 DMA,但可以用中断驱动。不过对于 2.5MHz 的 SPI 来说,中断开销反而可能拖慢速度,我实测下来还是轮询方式最快。如果换成带 DMA 的 ARM 芯片,那又是另一回事了。
第三个优化是写入缓冲。采集到的数据先存在 RAM 缓冲区里,攒够 64 字节或者每隔 100ms 批量写入一次 MRAM。这样既减少了 SPI 传输次数,又降低了功耗。
注意:批量写入时如果掉电,缓冲区里未写入的数据会丢。所以掉电检测中断里必须把缓冲区数据紧急刷入 MRAM,这个逻辑不能省。
5.3 功耗考量
MR25H40CDF 的写入电流典型值 15mA,读取电流 8mA,待机电流 100uA。对于电池供电的设备,功耗是个大问题。我的做法是:不写入时把 CS 拉高,MRAM 自动进入待机模式;采集间隔较长时,甚至可以把 PIC18LF4680 也进入睡眠模式,用定时器唤醒。
实测下来,每秒写入一次、每次写 64 字节的场景下,平均功耗大概 2mA 左右,用 2000mAh 的锂电池可以跑 40 天以上。
6. 常见问题排查与避坑指南
6.1 写入失败排查流程
写入失败是最常见的问题,排查思路如下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全写不进去 | WP 引脚悬空或拉低 | 测量 WP 引脚电压 |
| 偶尔写入失败 | CS 时序不对 | 逻辑分析仪抓 CS 和 SCK 波形 |
| 写入后读出来是 0xFF | WREN 没发或 WEL 未置位 | 读状态寄存器确认 WEL 位 |
| 写入后数据错位 | 地址字节顺序错误 | 确认地址是 24 位还是 16 位 |
| 高速写入失败 | SCK 频率超过芯片上限 | 降低 SPI 时钟 |
6.2 我踩过的几个坑
第一个坑是WP 引脚悬空。一开始我觉得 WP 不用就不接,结果芯片内部状态不确定,有时候能写有时候不能写,折腾了半天才发现是 WP 的问题。后来老老实实接到 GPIO 上,问题解决。
第二个坑是CS 拉高后立即拉低。MRAM 在 CS 拉高后需要一定时间完成内部操作,如果立即拉低开始下一次传输,芯片可能还没准备好。我在两次传输之间加了 1us 的延时,问题消失。
第三个坑是电源去耦不足。写入时电流尖峰导致电压跌落,PIC18 复位,整个系统重启。后来在 MRAM 的 VDD 引脚旁边加了 0.1uF 和 10uF 并联去耦,问题解决。
第四个坑是SPI 模式配置错误。MR25H40CDF 支持模式 0 和模式 3,我一开始配成了模式 1,结果读出来的数据全是乱的。后来查手册确认是模式 0,改过来就正常了。
6.3 调试工具推荐
逻辑分析仪是必备的,我用的是 Saleae 的 8 通道版本,抓 SPI 波形非常方便。没有逻辑分析仪的话,至少要用示波器看 SCK 和 CS 的时序。软件层面,可以在代码里加调试打印,把关键寄存器的值通过 UART 输出到串口助手,帮助定位问题。
实操心得:调试 SPI 时,先把 SCK 频率降到最低(比如 100kHz),确认通信正常后再逐步提高。这样可以把时序问题和逻辑问题分开排查,效率高很多。
7. 方案扩展与个人体会
这套 MRAM 加 PIC18LF4680 的方案,我已经在三个项目里用过了,最长的已经连续运行两年多,没有出现过数据丢失。后来有项目需要更大容量,我换成了 MR25H40CDF 的升级型号 MR25H256,容量翻倍,引脚兼容,代码几乎不用改。
如果要做更高速的采集,PIC18LF4680 的 SPI 可能成为瓶颈,可以考虑换成带 DMA 的 32 位 MCU,比如 STM32 系列,SPI 可以跑到 20MHz 以上,配合 MRAM 的 40MHz 上限,带宽能到 2.5MB/s,应付音频采集都够了。
最后分享一个小技巧:MRAM 的存储单元在出厂时可能不是全 0 或全 1,首次使用前建议先做一次全片擦除(其实就是全写 0),这样后续的读写测试才有基准。另外,MRAM 对磁场敏感,虽然芯片内部有屏蔽层,但尽量不要把芯片放在大电流走线或者变压器旁边,否则可能影响数据保持特性。