1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 和 PIC
工业现场的数据记录有个很尴尬的处境:用 EEPROM 吧,写入速度慢得让人抓狂,擦写次数也就百万次级别,高频记录的场景下没几年就写废了;用 SRAM 加后备电池吧,电池在高温高湿环境下三五年就鼓包漏液,维护成本高得离谱;用 NOR Flash 吧,写入前得先擦除整个扇区,掉电保护逻辑复杂,而且擦写寿命同样有限。我前两年做一个电力监测终端项目,设备装在配电柜里,环境温度夏天能到 70 度,客户要求每秒钟记录一次三相电流电压数据,设备寿命至少十年。算一下,每秒一次,一年就是三千多万次写入,十年三亿多次,EEPROM 和 Flash 根本扛不住。
后来接触到MRAM这个技术路线,才算是找到了正解。MRAM 的全称是磁性随机存储器,它的存储单元用的是磁性隧道结,通过电子自旋方向来记录数据,而不是电荷。这个物理机制决定了它几个非常讨喜的特性:写入速度接近 SRAM,纳秒级别;擦写寿命几乎是无限的,官方标称 10 的 14 次方以上;掉电不丢数据,不需要后备电池;工作温度范围宽,工业级和汽车级都有。MR25H40CDF就是 Everspin 家的一款 4Mbit 容量的 SPI MRAM,SOIC-8 封装,引脚和常见的 SPI Flash 兼容,硬件上可以直接替换,软件上改改驱动就能跑起来。
主控这边我选的是PIC18F55K42,Microchip 的 8 位机,带 SPI 外设,工作电压 1.8V 到 5.5V,工业级温度范围 -40 到 125 度,内置 DMA 和丰富的定时器资源。选它有几个考虑:一是 8 位机在工业控制里生态成熟,开发工具链稳定,Microchip 的 MCC 代码生成器能省不少配置时间;二是这颗芯片的 SPI 模块支持多种时钟极性和相位组合,跟 MR25H40CDF 对接很灵活;三是价格适中,供货相对稳定,不像某些 32 位机那样动不动就缺货涨价。当然如果你手头是 STM32 或者 ESP32,SPI 驱动逻辑是相通的,改改寄存器操作就行。
这个组合适合什么人参考呢?做工业数据采集、电力监控、车载记录仪、医疗设备日志存储的嵌入式工程师,尤其是那些被 EEPROM 寿命和 Flash 掉电保护折磨过的朋友。如果你刚开始学嵌入式,想找一个 SPI 外设加非易失存储的练手项目,这个组合也合适,因为 MRAM 的驱动比 Flash 简单得多,没有擦除命令、没有扇区管理、没有写平衡算法,读写就是读写,逻辑干净。
1.2 整体架构怎么搭
整个系统的数据流是这样的:传感器信号经过调理电路进入 PIC18F55K42 的 ADC,MCU 在定时器中断里周期性地采集数据,做简单的滤波和标定,然后通过 SPI 总线把数据写入 MR25H40CDF。MRAM 这边按地址顺序存储,我习惯把它组织成一个环形缓冲区,写指针到了末尾就绕回开头,这样不需要文件系统,也不需要复杂的磨损均衡,因为 MRAM 本身就不怕写坏。读取的时候,上位机通过 UART 或者 CAN 发送读取命令,MCU 从 MRAM 里把数据读出来打包上传。
硬件连接上,MR25H40CDF 是标准的 SPI 从设备,四根线:SCLK、MOSI、MISO、CS。注意它的 CS 片选信号必须由 MCU 的 GPIO 控制,不能用硬件 SPI 的自动片选,因为 MRAM 的时序要求片选在命令字节之前拉低,在最后一个数据字节之后拉高,中间不能有毛刺。我试过用硬件片选,在某些时钟配置下会出现数据错位,后来改成软件控制 GPIO 就稳了。电源方面,MR25H40CDF 支持 2.7V 到 3.6V 供电,PIC18F55K42 可以跑在 3.3V,两者电平匹配,不需要电平转换芯片。去耦电容一定要加,0.1uF 的陶瓷电容紧贴 MRAM 的 VCC 引脚,否则高频写入时电源纹波会导致位翻转。
SPI 时钟频率方面,MR25H40CDF 最高支持 40MHz,但 PIC18F55K42 在 3.3V 下 SPI 时钟最高是 Fosc/4,我用的 64MHz 系统时钟,SPI 就是 16MHz。实测下来 16MHz 完全稳定,误码率为零。如果你追求更高速率,可以换 32 位机,但工业场景下 16MHz 的写入带宽已经绰绰有余了,毕竟传感器采样率通常也就几 kHz。
2. MR25H40CDF 的底层操作与 SPI 时序要点
2.1 命令集其实就那几条
MR25H40CDF 的指令集比 SPI Flash 简单太多了。Flash 你得记一堆命令:读状态寄存器、写使能、扇区擦除、页编程、读数据、写禁止等等,操作之前还得查忙状态。MRAM 这边核心命令就四个:WREN(写使能,0x06)、WRDI(写禁止,0x04)、READ(读数据,0x03)、WRITE(写数据,0x02)。另外还有读状态寄存器和写状态寄存器,但状态寄存器主要用来配置一些保护模式,正常读写用不上。
写操作的流程是:先拉低 CS,发送 WREN 命令字节,拉高 CS;然后再次拉低 CS,发送 WRITE 命令字节,接着发送 24 位地址(因为 4Mbit 容量需要 19 位地址,但协议上按 24 位发送,高 5 位忽略),然后连续发送要写入的数据字节,最后拉高 CS。注意 WREN 和 WRITE 之间必须有一次 CS 的上升沿,这是 MRAM 的锁存机制,跟 Flash 类似。读操作更简单:拉低 CS,发送 READ 命令字节,发送 24 位地址,然后就可以连续读出数据了,地址会自动递增,读到末尾会绕回开头,这个特性在环形缓冲区场景下特别好用。
注意:WREN 命令之后如果 CS 没有拉高再拉低,直接发 WRITE 命令,MRAM 会忽略这次写操作。我一开始图省事想在一个 CS 周期里发完 WREN 和 WRITE,结果数据死活写不进去,查了半天时序图才发现这个问题。
2.2 SPI 模式怎么选
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。这两种模式的区别在于空闲时钟电平和数据采样边沿。模式 0 下,SCLK 空闲为低,数据在上升沿采样;模式 3 下,SCLK 空闲为高,数据在下降沿采样。我一般用模式 0,因为 PIC18F55K42 的 SPI 模块默认配置就是模式 0,省得改。但如果你发现读出来的数据全是 0xFF 或者 0x00,先检查一下 SPI 模式对不对,这是最常见的坑。
PIC18F55K42 的 SPI 配置寄存器是 SPI1CON0、SPI1CON1、SPI1CON2 和 SPI1BAUD。关键位是 SPI1CON0 的 MODE 位,设置为 0 就是模式 0。SPI1BAUD 设置时钟分频,我设成 3,对应 Fosc/4 分频,64MHz 系统时钟下就是 16MHz。SPI1CON1 里要设置 SDI(数据输入)和 SDO(数据输出)的采样沿,模式 0 下 SDI 在上升沿采样,SDO 在下降沿变化。这些配置用 MCC 生成代码的话,图形界面上勾选一下就行,但建议还是手动核对一遍寄存器值,MCC 有时候会生成冗余配置。
2.3 地址管理和数据组织
4Mbit 容量等于 512KB,地址范围 0x000000 到 0x07FFFF。我把它分成几个区域:前 4KB 存设备配置参数和标定系数,中间 500KB 做数据环形缓冲区,最后 8KB 存系统日志和故障记录。配置参数区只在上电初始化时读一次,数据缓冲区按页管理,每页 256 字节,写满一页就更新写指针。因为 MRAM 没有擦除概念,写指针可以直接覆盖旧数据,不需要先擦后写,这是它比 Flash 省事的地方。
环形缓冲区的实现逻辑是这样的:定义一个写地址变量 write_addr,每次写入 N 个字节后,write_addr 加上 N,如果超过缓冲区末尾地址,就绕回到缓冲区起始地址。读的时候,上位机指定起始地址和长度,MCU 从 MRAM 读出数据返回。因为 MRAM 的读操作支持地址自动递增和绕回,所以即使读取跨越了缓冲区末尾,也不需要软件干预,硬件会自动处理。这个特性我在实际项目中验证过,读 512KB 全容量数据,地址从 0x07FFFF 读到 0x000000,数据完全正确,没有断点。
3. PIC18F55K42 端的驱动实现与代码拆解
3.1 底层字节读写函数
先写两个最基础的函数:SPI 单字节交换和 MRAM 片选控制。PIC18F55K42 的 SPI 发送和接收是同一个寄存器操作,写 SPI1TXB 会启动发送,同时接收到的数据会出现在 SPI1RXB 里。所以一个字节交换函数就是:等 SPI1TXB 空,写入要发送的字节,等接收完成,读出 SPI1RXB。代码如下:
uint8_t spi_exchange_byte(uint8_t tx_data) { while (!SPI1STATUSbits.TXBE); // 等待发送缓冲区空 SPI1TXB = tx_data; while (!SPI1STATUSbits.RXBF); // 等待接收缓冲区满 return SPI1RXB; }片选控制就两个宏:
#define MRAM_CS_LOW() (LATCbits.LATC0 = 0) #define MRAM_CS_HIGH() (LATCbits.LATC0 = 1)注意 CS 引脚要配置成数字输出模式,PIC18F55K42 的引脚复用比较多,ANSELC 寄存器要清零对应的位,否则引脚可能处于模拟模式,输出电平不对。
3.2 写使能与写数据函数
写 MRAM 之前必须先发 WREN 命令。这个命令单独一个 CS 周期:
void mram_write_enable(void) { MRAM_CS_LOW(); spi_exchange_byte(0x06); // WREN MRAM_CS_HIGH(); }然后写数据函数接收地址、数据指针和长度:
void mram_write(uint32_t addr, uint8_t *data, uint16_t len) { mram_write_enable(); MRAM_CS_LOW(); spi_exchange_byte(0x02); // WRITE spi_exchange_byte((addr >> 16) & 0xFF); spi_exchange_byte((addr >> 8) & 0xFF); spi_exchange_byte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { spi_exchange_byte(data[i]); } MRAM_CS_HIGH(); }这里有个细节:地址是 24 位的,但 MR25H40CDF 实际只用了低 19 位,高 5 位是无关位。我习惯把地址变量定义成 uint32_t,发送时取低 24 位,这样代码可读性好,以后换更大容量的 MRAM 也不用改。
3.3 读数据函数与连续读优化
读函数跟写函数结构类似,只是不需要 WREN:
void mram_read(uint32_t addr, uint8_t *buffer, uint16_t len) { MRAM_CS_LOW(); spi_exchange_byte(0x03); // READ spi_exchange_byte((addr >> 16) & 0xFF); spi_exchange_byte((addr >> 8) & 0xFF); spi_exchange_byte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buffer[i] = spi_exchange_byte(0x00); } MRAM_CS_HIGH(); }连续读的时候,每次交换发送 0x00 是占位字节,MRAM 会忽略 MOSI 上的数据,只管把内部数据推出来。如果你用 DMA 的话,可以把 SPI 接收和发送都配成 DMA 通道,这样读大块数据时 CPU 占用率极低。我在一个高速采集项目里用 DMA 读 MRAM,1KB 数据读取时间从 520us 降到了 510us,提升不明显,因为瓶颈在 SPI 时钟上,但 CPU 确实解放出来了,可以去处理其他任务。
3.4 环形缓冲区的读写指针管理
环形缓冲区的核心是两个指针:写指针和读指针。写指针由 MCU 在采集中断里更新,读指针由上位机命令更新。为了防止读写冲突,我用了一个简单的互斥机制:写操作在中断里执行,读操作在主循环里执行,读之前先关中断,读完再开中断。因为 MRAM 的读写速度很快,关中断的时间窗口只有几十微秒,对系统实时性影响可以忽略。
写指针更新逻辑:
void buffer_write(uint8_t *data, uint16_t len) { mram_write(write_addr, data, len); write_addr += len; if (write_addr >= BUF_END_ADDR) { write_addr = BUF_START_ADDR; } }读指针类似,但读的时候要判断是否读到了写指针的位置,避免读到还没写入的旧数据。我的做法是在每个数据页的头部加一个 4 字节的时间戳和序列号,上位机解析时根据序列号判断数据的新旧和连续性。
4. 工业现场的数据存储策略与可靠性设计
4.1 掉电保护怎么做
MRAM 本身掉电不丢数据,但 MCU 在掉电瞬间可能正在写数据,如果写到一半断电,这一页数据就不完整了。我的做法是在硬件上加一个掉电检测电路,用比较器监测电源电压,当电压低于 3.0V 时触发 MCU 的外部中断。中断服务程序里立刻停止采集,把当前写指针和状态标志写入 MRAM 的配置区,然后进入低功耗模式。因为 MRAM 写入速度是纳秒级,从检测到掉电到完成关键数据保存,整个过程在 100us 以内,电源上的大电容足够撑住。
提示:掉电检测的阈值要留足余量。我一开始设的 2.9V,结果发现电源纹波偶尔会误触发,后来改成 3.0V 并加了 10ms 的软件消抖,就稳定了。
4.2 数据校验与纠错
工业现场电磁干扰强,SPI 总线虽然短,但也不能完全排除位翻转的可能。我在每个数据页的末尾加了 2 字节的 CRC16 校验,上位机读取时先校验再使用。如果 CRC 不匹配,说明这一页数据在传输或存储过程中出了问题,上位机可以请求重读或者标记为坏页。MRAM 本身的位翻转率极低,官方数据是 10 的负 15 次方以下,但加上 CRC 校验后,系统的数据可靠性又上了一个台阶。
CRC16 的计算我用的是查表法,在 PIC18F55K42 上跑一次 256 字节的 CRC 大约 200us,完全可以接受。如果你对速度要求更高,可以用硬件 CRC 模块,但 PIC18F55K42 没有这个外设,只能用软件实现。
4.3 温度补偿与寿命预估
MRAM 的读写特性对温度不敏感,但 MCU 的 SPI 时钟在极端温度下可能会有轻微频偏。我在 -40 度和 85 度环境下各做了 100 万次读写测试,误码率都是零。寿命方面,MRAM 的擦写次数是 10 的 14 次方,按每秒写一次算,可以写 300 万年,完全不用担心写坏。真正需要注意的是连接器和 PCB 焊点的机械寿命,工业现场振动大,SOIC-8 封装虽然比 BGA 结实,但长期振动下焊点还是可能开裂。我的做法是在 MRAM 芯片周围点红胶固定,PCB 布局时远离板边和安装孔。
5. 实操踩坑记录与常见问题排查
5.1 读出来全是 0xFF 或 0x00
这是最常遇到的问题,原因通常有三个:SPI 模式不对、片选信号没控制好、MRAM 供电异常。排查顺序是先用示波器看 SCLK、MOSI、CS 三根线的波形,确认时钟极性和相位跟 MRAM 要求的一致。然后看 CS 在命令字节期间是否一直保持低电平,中间有没有毛刺。最后用万用表量 MRAM 的 VCC 引脚,确认电压在 2.7V 到 3.6V 之间。我遇到过一回是 CS 引脚被配置成了模拟输入,输出一直是高阻态,MRAM 根本收不到片选信号,改了一下 ANSEL 寄存器就好了。
5.2 写入后读出来数据错位
数据错位通常是地址发送顺序搞反了。MR25H40CDF 的地址是 24 位,先发高 8 位,再发中 8 位,最后发低 8 位。我一开始习惯性地先发低字节,结果写进去的数据全跑到错误的地址上去了。后来统一用(addr >> 16) & 0xFF、(addr >> 8) & 0xFF、addr & 0xFF这个顺序发送,就再也没错过。
5.3 高速写入时偶发数据丢失
SPI 时钟跑到 16MHz 以上时,如果 PCB 走线太长或者没有阻抗匹配,可能会出现数据丢失。我的经验是 SPI 走线尽量短,最好在 5cm 以内,SCLK 和 MOSI 之间加地线隔离,MRAM 的 VCC 去耦电容用 0.1uF 和 1uF 并联。如果还是丢数据,把 SPI 时钟降到 8MHz 试试,工业场景下 8MHz 的写入带宽已经足够覆盖绝大多数传感器采样率了。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读出全 0xFF | SPI 模式错误 | 示波器看 SCLK 空闲电平 | 改为模式 0 或模式 3 |
| 读出全 0x00 | CS 未拉低 | 量 CS 引脚电压 | 检查 GPIO 配置和代码 |
| 数据错位 | 地址字节顺序错误 | 核对地址发送顺序 | 高字节在前发送 |
| 偶发丢数据 | SPI 时钟过高 | 降低时钟测试 | 降频或优化 PCB 走线 |
| 写入无效 | WREN 后 CS 未翻转 | 看 CS 波形 | WREN 和 WRITE 分两个 CS 周期 |
| 电源纹波大 | 去耦电容不足 | 示波器看 VCC 纹波 | 加 0.1uF 和 1uF 电容 |
5.5 几个容易被忽略的细节
第一个细节是 MRAM 的写保护引脚。MR25H40CDF 有一个 WP 引脚,低电平有效,如果悬空或者被拉低,写操作会被硬件禁止。我习惯把它直接接 VCC,软件里也不做写保护配置,因为工业场景下数据写入是核心功能,不需要额外的硬件保护。
第二个细节是 SPI 的 MISO 引脚在上电初始化之前的状态。如果 MISO 被外部电路拉低,MCU 可能会误判为接收到了数据。我的做法是在初始化 SPI 之前,先把 MISO 配置成普通 GPIO 输入,等 SPI 配置完成后再切换回 SPI 功能。
第三个细节是 MRAM 的读写操作不需要等待时间。Flash 写入后需要等 5ms 左右的内部编程时间,MRAM 写完立即就可以读,没有忙状态。所以代码里不需要加延时,也不需要查状态寄存器,写完之后直接读下一地址就行。这个特性在高速数据记录场景下非常关键,省下来的时间可以用于处理更多传感器数据。
6. 性能实测与方案对比
6.1 读写速度实测数据
我用逻辑分析仪抓了 SPI 总线上的波形,统计了不同操作下的耗时。测试条件:SPI 时钟 16MHz,PIC18F55K42 系统时钟 64MHz,MRAM 供电 3.3V,室温 25 度。
| 操作 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节写 | 1 字节 | 2.1us | 476 KB/s |
| 256 字节页写 | 256 字节 | 132us | 1.94 MB/s |
| 单字节读 | 1 字节 | 1.8us | 555 KB/s |
| 256 字节页读 | 256 字节 | 130us | 1.97 MB/s |
| 1KB 连续读 | 1024 字节 | 518us | 1.98 MB/s |
从数据可以看出,连续读写时速率接近 2MB/s,瓶颈在 SPI 时钟上。如果把 SPI 时钟提到 40MHz,理论速率可以到 5MB/s,但 PIC18F55K42 的 SPI 最高只能到 Fosc/4,所以 16MHz 就是这颗 MCU 的极限了。如果你需要更高带宽,可以考虑用带 QSPI 的 32 位机,MR25H40CDF 也支持 Quad SPI 模式,四根数据线并行传输,速率可以翻四倍。
6.2 与 EEPROM 和 Flash 的对比
| 特性 | MR25H40CDF | 典型 EEPROM | 典型 NOR Flash |
|---|---|---|---|
| 写入速度 | 纳秒级 | 毫秒级 | 毫秒级(需擦除) |
| 擦写寿命 | 10^14 次 | 10^6 次 | 10^5 次 |
| 掉电保持 | 20 年以上 | 10 年以上 | 10 年以上 |
| 写入前擦除 | 不需要 | 不需要 | 需要 |
| 待机功耗 | 极低 | 低 | 低 |
| 单位成本 | 较高 | 低 | 低 |
| 接口复杂度 | 简单 | 简单 | 复杂 |
从对比可以看出,MRAM 的优势在于写入速度和寿命,劣势在于成本。4Mbit 的 MRAM 价格大概是同容量 EEPROM 的 5 到 8 倍,Flash 的 3 到 5 倍。但在工业场景下,设备维护成本远高于器件成本,一次现场更换芯片的人工费就够买几十片 MRAM 了。所以这个账要算总拥有成本,不能只看器件单价。
6.3 实际项目中的表现
我在电力监测终端上跑了连续 72 小时的老化测试,每秒写入 128 字节数据,总共写了约 2600 万次。测试结束后读取全部数据,CRC 校验全部通过,没有一页数据损坏。设备在 -20 度到 60 度的温度循环箱里各跑了 24 小时,读写功能正常。振动测试按照 GB/T 2423 标准做了扫频振动,MRAM 焊点没有开裂。这个结果让我对 MRAM 在工业场景下的可靠性有了充分的信心。
7. 扩展思路与后续优化方向
7.1 用 DMA 进一步降低 CPU 占用
PIC18F55K42 有 DMA 控制器,可以配置成 SPI 发送和接收的自动搬运。具体做法是:把要写入的数据放在 RAM 缓冲区,DMA 源地址指向缓冲区,目标地址指向 SPI1TXB,触发源选择 SPI 发送空中断;同时另一个 DMA 通道源地址指向 SPI1RXB,目标地址指向接收缓冲区,触发源选择 SPI 接收满中断。这样 CPU 只需要配置 DMA 描述符和启动传输,剩下的数据搬运全部由 DMA 完成。在 1KB 数据写入的场景下,CPU 占用率从 45% 降到了 5% 以下。
7.2 多片 MRAM 级联扩展容量
4Mbit 如果不够用,可以挂多片 MR25H40CDF 在同一个 SPI 总线上,用不同的 GPIO 做片选。软件上定义一个设备地址表,每次读写前先根据地址范围选择对应的片选引脚。因为 MRAM 的 CS 是低电平有效,多片并联时只要保证同一时刻只有一片的 CS 被拉低就行。我试过挂四片,总容量 2MB,读写逻辑跟单片一样,只是多了一层地址映射。
7.3 结合文件系统做数据管理
如果数据需要按文件组织,可以在 MRAM 上跑一个轻量级文件系统,比如 LittleFS 或者 SPIFFS。这两个文件系统都是为嵌入式 Flash 设计的,但移植到 MRAM 上也很容易,因为 MRAM 的读写接口比 Flash 简单。移植的关键是去掉擦除操作,把擦除函数实现为空函数,其他读写逻辑不变。这样上位机就可以像操作 U 盘一样读取 MRAM 里的数据文件,用户体验更好。
7.4 低功耗场景下的优化
电池供电的设备对功耗敏感。MR25H40CDF 的待机电流只有 10uA 左右,读写电流约 15mA。优化策略是:不读写的时候把 CS 拉高,MRAM 自动进入待机模式;MCU 的 SPI 模块在不使用时关闭时钟;数据采集采用突发模式,攒够一批数据再集中写入,减少 SPI 总线的活跃时间。我实测过,采用突发写入策略后,系统平均功耗从 8mA 降到了 2mA,电池寿命延长了四倍。
这个方案我在多个工业项目上复用过了,从电力监测到车载记录仪,从医疗设备到环境监控,MRAM 加 PIC 的组合表现一直很稳。如果你正在选型数据存储方案,不妨试试这个路线,前期多花点时间调通 SPI 时序,后面就是复制粘贴的活了。