1. 项目缘起与方案选型:为什么是 MR25H40CDF 加 PIC18F96J94
1.1 一个真实的需求场景
前阵子接了个工业数据采集终端的活儿,客户的要求很朴素:设备要在产线上连续跑,断电不能丢数据,写入要快,寿命要长,最好十年内别让我换存储芯片。听起来简单,但真选起型来,坑一个接一个。
传统方案无非几条路。EEPROM 写入慢、寿命有限,频繁写个几万次就开始提心吊胆;NOR Flash 擦除块大、写入前要擦、擦写寿命也就十万次量级,而且写入过程中掉电容易丢整块数据;铁电存储器 FRAM 倒是快、寿命也高,但容量小、价格贵,做大缓冲区不划算。这时候 MRAM 就进入了视野——MR25H40CDF这颗 4Mbit 的磁性随机存储器,SPI 接口,读写速度能到 40MHz,几乎无限次擦写,掉电数据不丢,而且不需要像 Flash 那样先擦后写。
主控这边选PIC18F96J94,理由也很实在。这颗 8 位单片机自带硬件 SPI 模块、64KB Flash、接近 4KB RAM,工业温度范围,外设够用,开发工具链成熟,最关键的是它的 SPI 模块支持多种时钟极性和相位组合,跟 MRAM 的时序匹配起来很省心。对于这种中等复杂度、对成本敏感、又要长期稳定运行的工业设备,PIC18F96J94 是个很稳的选择。
1.2 MRAM 到底比 Flash 和 EEPROM 强在哪
很多人第一次听说 MRAM 会懵,这里用生活化的方式讲清楚。你可以把 Flash 想象成一块黑板,写字之前必须先把整块黑板擦干净,擦的时候粉尘飞扬(写入放大),而且擦多了黑板就磨薄了(寿命有限)。EEPROM 像是一本便签本,可以一页一页撕下来重写,但撕的速度慢,撕多了本子也会散架。
MRAM 则像是一块磁性白板,每个存储单元是一个微小的磁隧道结,通过改变磁化方向来记录 0 和 1。写的时候直接翻转磁矩,不需要擦除步骤,写入速度是纳秒级,擦写次数理论上超过 10 的 15 次方,基本可以认为无限。掉电后磁化方向保持,数据自然不丢。这就是为什么工业场景里,MRAM 越来越受欢迎。
具体到 MR25H40CDF,几个关键参数值得记住:
| 参数项 | 数值 | 实际意义 |
|---|---|---|
| 容量 | 4Mbit(512KB) | 足够做数据缓冲和参数存储 |
| 接口 | SPI,最高 40MHz | 读写速度快,接线简单 |
| 供电 | 2.7V ~ 3.6V | 跟 3.3V 系统直接对接 |
| 擦写寿命 | 近乎无限 | 高频写入场景无压力 |
| 数据保持 | 20 年以上 | 工业设备生命周期内无忧 |
| 工作温度 | -40°C ~ 85°C | 工业级,产线环境没问题 |
1.3 为什么不用并口 MRAM 或串行 Flash
有人会问,并口 MRAM 速度更快,为什么不用?答案很简单:引脚太多。PIC18F96J94 的 IO 资源有限,并口 MRAM 动辄需要 20 多根地址线加数据线,布线复杂,PCB 面积大,成本也上去了。SPI 接口只需要 4 根线(CS、SCK、MOSI、MISO),对于 512KB 这个量级的存储需求,40MHz 的 SPI 带宽完全够用。
至于串行 Flash,前面说了,擦除块和写入寿命是硬伤。工业设备经常需要记录运行日志、保存传感器校准参数、缓存突发数据,这些操作可能每秒都在发生,Flash 根本扛不住。MRAM 的“直接覆盖写”特性,让软件逻辑也简单很多,不需要维护擦除块管理、磨损均衡这些复杂机制。
提示:选型时不要只看单价。MRAM 单颗价格确实比同容量 Flash 高,但算上寿命、维护成本、软件复杂度、故障率,综合成本往往更低。我做过一个粗略估算,在每天写入 10 万次的场景下,Flash 方案三年内需要更换存储芯片的概率超过 60%,而 MRAM 方案基本为零。
2. 硬件连接与 SPI 时序:把线接对是第一步
2.1 引脚连接与硬件设计要点
MR25H40CDF 是 8 引脚封装,实际用到的信号线就 4 根,加上电源和地,接线非常清爽。跟 PIC18F96J94 对接时,我用的引脚分配如下:
| MR25H40CDF 引脚 | PIC18F96J94 引脚 | 功能说明 |
|---|---|---|
| CS | RC2(可配置为普通 IO) | 片选,低电平有效 |
| SCK | RC3(SCK) | SPI 时钟 |
| SI(MOSI) | RC5(SDO) | 主出从入 |
| SO(MISO) | RC4(SDI) | 主入从出 |
| VCC | 3.3V | 电源 |
| VSS | GND | 地 |
| WP | 接 VCC 或悬空 | 写保护,不需要时拉高 |
| HOLD | 接 VCC 或悬空 | 保持,不需要时拉高 |
这里有个细节要注意:PIC18F96J94 的 SPI 模块引脚是复用的,RC3、RC4、RC5 分别对应 SCK、SDI、SDO。配置的时候要把 TRISC 的对应位设好,SCK 和 SDO 是输出,SDI 是输入。CS 我单独用 RC2 做软件片选,这样灵活,可以在一条总线上挂多个 SPI 设备。
PCB 布线方面,SPI 信号线尽量短,尤其是 SCK 时钟线,走线要远离模拟信号和电源开关节点。如果线长超过 10cm,建议在 SCK 上串一个 22Ω 到 33Ω 的电阻做阻抗匹配,减少过冲和振铃。MRAM 的电源引脚旁边放一个 0.1μF 的陶瓷电容,越近越好,这是老生常谈但真的管用。
2.2 SPI 模式选择:CPOL 和 CPHA 怎么定
SPI 有四种模式,由 CPOL(时钟极性)和 CPHA(时钟相位)组合决定。MR25H40CDF 支持模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我一般用模式 0,因为这是最常用的,大多数 SPI 设备都支持,调试起来也方便。
模式 0 的时序特征是:时钟空闲时为低电平,数据在时钟上升沿采样,下降沿变化。用逻辑分析仪抓波形的时候,你会看到 CS 拉低后,第一个 SCK 上升沿之前,MOSI 上的数据就已经稳定了。这个细节很关键,如果主控配置不对,第一个 bit 容易丢。
PIC18F96J94 的 SSPCON1 寄存器里,CKP 位控制时钟极性,CKE 位控制时钟相位。配置成模式 0 的话,CKP=0,CKE=0。具体寄存器设置我后面会给出完整代码。
2.3 上电初始化和写保护处理
MR25H40CDF 上电后需要一小段时间稳定,数据手册上写的是 10ms 以内。我一般在上电初始化时加 20ms 延时,保险一点。WP 引脚如果不用硬件写保护,直接拉高到 VCC。HOLD 引脚同理,拉高或者悬空都行,但悬空容易受干扰,建议还是拉高。
有个坑我踩过:有一次 WP 引脚悬空,结果在电机启停的瞬间,MRAM 偶尔会进入写保护状态,数据写不进去。后来把 WP 和 HOLD 都硬拉到 VCC,问题消失。所以别偷懒,该拉的拉,该接的接。
注意:MRAM 虽然写入不需要擦除,但写操作仍然需要一定时间。MR25H40CDF 的单次写入周期典型值是 10ns 量级,但 SPI 传输本身需要时间。在 40MHz 时钟下,传输一个字节需要 0.2μs,加上命令和地址开销,写一个字节大约 1μs。这个速度已经比 EEPROM 快了几个数量级。
3. 软件驱动实现:从底层字节读写到数据管理
3.1 SPI 底层驱动代码
先上 PIC18F96J94 的 SPI 初始化代码。我用的是 MCC 生成的框架,但核心寄存器配置是自己写的,这样更可控。
// SPI 初始化,模式 0,时钟 Fosc/4 void SPI_Init(void) { TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC4 = 1; // SDI 输入 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC2 = 0; // CS 输出 CS = 1; // 片选拉高,空闲状态 SSPCON1 = 0x00; // 先复位 SSP 模块 SSPSTAT = 0x00; // 模式 0:CKE=0 SSPCON1 = 0x20; // SSPEN=1,CKP=0,Fosc/4 // 实际时钟 = 64MHz / 4 = 16MHz,如果需要 40MHz 得用 PLL }这里解释一下时钟。PIC18F96J94 最高跑 64MHz(用 PLL 倍频),SPI 时钟是 Fosc 的分频。SSPCON1 的低四位控制分频比:0000 是 Fosc/4,0001 是 Fosc/16,0010 是 Fosc/64。如果系统时钟 64MHz,Fosc/4 就是 16MHz。MRAM 支持 40MHz,但 16MHz 对大多数应用已经绰绰有余。如果要更快,可以改分频比,但要注意 PCB 布线质量。
字节收发函数:
// SPI 收发一个字节 uint8_t SPI_ExchangeByte(uint8_t data) { SSPBUF = data; // 写入发送缓冲 while(!SSPIF); // 等待传输完成 SSPIF = 0; // 清除中断标志 return SSPBUF; // 读取接收数据 }这个函数是阻塞式的,简单可靠。如果系统对实时性要求高,可以用中断方式,但 SPI 传输本身就快,阻塞几十微秒对大多数应用没影响。
3.2 MRAM 命令集与读写操作
MR25H40CDF 的命令集很精简,常用的就几条:
| 命令 | 十六进制码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
写数据之前必须先发 WREN 命令,这是很多新手容易忘的。MRAM 不像 RAM 那样直接写,它有个写使能锁存机制,每次写操作前都要重新使能。
写一个字节的流程:
// 向指定地址写一个字节 void MRAM_WriteByte(uint32_t addr, uint8_t 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; // 等待写入完成,实际 MRAM 很快,但保险起见 while(MRAM_IsBusy()); }读操作更简单,不需要 WREN:
// 从指定地址读一个字节 uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; CS = 0; SPI_ExchangeByte(0x03); // READ SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); data = SPI_ExchangeByte(0xFF); // 发送哑元,读取数据 CS = 1; return data; }注意读的时候要发一个哑元字节(0xFF 或任意值)来产生时钟,才能把数据移进来。这是 SPI 的标准操作,但初学者经常在这里卡住。
3.3 页写与连续读的优化
单字节读写虽然简单,但效率低。MR25H40CDF 支持页写和连续读,一次可以传输多个字节。页大小是 256 字节,写的时候不能跨页,否则会回卷到页首覆盖数据。
连续读就自由多了,给个起始地址,然后一直发时钟,数据就源源不断地出来,地址自动递增,直到 CS 拉高。这个特性非常适合批量读取日志或参数块。
// 连续读取多个字节 void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { CS = 0; SPI_ExchangeByte(0x03); SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for(uint16_t i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0xFF); } CS = 1; }页写函数要处理跨页的情况,我一般在上层做地址对齐,保证每次写入不跨页。如果数据长度超过页边界,就拆成多次写。
// 页写,注意不要跨页 void MRAM_WritePage(uint32_t addr, uint8_t *buf, uint8_t len) { // 确保不跨页 uint8_t page_offset = addr & 0xFF; if(page_offset + len > 256) { len = 256 - page_offset; } 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); for(uint8_t i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS = 1; while(MRAM_IsBusy()); }3.4 状态寄存器与忙判断
MRAM 的状态寄存器只有一位有用,就是 WIP(Write In Progress)。写操作进行时 WIP 为 1,完成后为 0。虽然 MRAM 写入很快,但在高频写入场景下,还是判断一下比较稳妥。
// 判断 MRAM 是否忙 uint8_t MRAM_IsBusy(void) { uint8_t status; CS = 0; SPI_ExchangeByte(0x05); // RDSR status = SPI_ExchangeByte(0xFF); CS = 1; return (status & 0x01); }实测下来,MRAM 的写入周期极短,大多数情况下这个函数返回 0 的速度比调用开销还快。但为了代码的健壮性,保留这个判断没坏处。
4. 数据管理策略:让存储真正服务于工业应用
4.1 存储空间划分与地址规划
512KB 的空间说大不大,说小不小,规划不好后期会很痛苦。我的做法是按功能分区,每个区域有固定的起始地址和大小,用宏定义管理。
| 区域名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 系统参数区 | 0x00000 | 4KB | 设备配置、校准系数 |
| 运行日志区 | 0x01000 | 256KB | 循环记录运行状态 |
| 数据缓存区 | 0x41000 | 252KB | 突发数据缓冲 |
| 备份区 | 0x7F000 | 4KB | 关键参数备份 |
地址规划的原则是:关键数据放前面,大块数据放后面,留出备份区。系统参数区用双备份加校验,确保不会因为单点故障丢失配置。
4.2 循环日志的实现与磨损均衡
虽然 MRAM 寿命近乎无限,但良好的日志管理仍然重要。我实现了一个简单的循环日志:维护一个写指针,每次写新记录时指针递增,写到区域末尾就回卷到开头。每条记录包含时间戳、数据长度、数据和 CRC 校验。
typedef struct { uint32_t timestamp; uint16_t length; uint8_t data[32]; uint16_t crc; } LogRecord; uint32_t log_write_ptr = LOG_START_ADDR; void Log_Write(uint8_t *data, uint16_t len) { LogRecord rec; rec.timestamp = GetSystemTick(); rec.length = len; memcpy(rec.data, data, len); rec.crc = CalculateCRC16((uint8_t*)&rec, sizeof(rec) - 2); // 检查是否超出区域 if(log_write_ptr + sizeof(LogRecord) > LOG_END_ADDR) { log_write_ptr = LOG_START_ADDR; } MRAM_WritePage(log_write_ptr, (uint8_t*)&rec, sizeof(LogRecord)); log_write_ptr += sizeof(LogRecord); }读取日志时,从当前写指针往前找,遇到 CRC 正确的记录就解析。这样即使掉电,最多丢失最后一条不完整的记录,前面的数据都是安全的。
4.3 掉电保护与数据完整性
工业现场掉电是常态,数据完整性必须保证。我的策略是:关键数据写两份,地址不同,每份带 CRC。读取时两份都读,CRC 都对的取最新,只有一份对的用那份,两份都不对就报错。
typedef struct { uint32_t magic; // 标识字,用于判断是否初始化过 uint8_t config[64]; uint16_t crc; } ConfigBlock; ConfigBlock cfg_a, cfg_b; void Config_Save(void) { cfg_a.magic = 0xAA55AA55; cfg_a.crc = CalculateCRC16((uint8_t*)&cfg_a, sizeof(cfg_a) - 2); MRAM_WritePage(CFG_A_ADDR, (uint8_t*)&cfg_a, sizeof(cfg_a)); cfg_b = cfg_a; cfg_b.magic = 0x55AA55AA; cfg_b.crc = CalculateCRC16((uint8_t*)&cfg_b, sizeof(cfg_b) - 2); MRAM_WritePage(CFG_B_ADDR, (uint8_t*)&cfg_b, sizeof(cfg_b)); }这种双备份加 CRC 的方式,在我做的多个工业项目里都验证过,可靠性很高。即使写入过程中掉电,至少有一份是完整的。
实操心得:CRC 校验用 CRC16-CCITT 就够了,计算快,检错能力强。不要用简单的累加和,那个检错能力太弱,工业环境里电磁干扰大,累加和容易漏检。
5. 调试与问题排查:那些文档里不会写的事
5.1 SPI 通信失败的常见原因
调试 SPI 最怕的就是没反应。我总结了一个排查顺序,按这个来基本能定位问题:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无数据 | CS 没拉低、电源没接、SPI 没使能 | 万用表测电压,逻辑分析仪抓 CS 和 SCK |
| 读出来全是 0xFF | MISO 没接、从机没供电、模式不对 | 检查接线,确认 CPOL/CPHA |
| 读出来全是 0x00 | MISO 被拉低、从机损坏 | 断开从机,测 MISO 是否浮空 |
| 数据偶尔错 | 时钟太快、布线太长、干扰 | 降低 SPI 时钟,加匹配电阻 |
| 写入不成功 | 忘了 WREN、WP 引脚状态不对 | 检查写使能流程,测 WP 电压 |
逻辑分析仪是调试 SPI 的神器,几百块一个的就行。抓一下波形,CS、SCK、MOSI、MISO 四根线一目了然,比盲猜快多了。
5.2 写入数据丢失的排查思路
有一次客户反馈,设备运行几天后参数丢了。我过去排查,发现是写入时没有判断忙状态,连续写多个字节时,前一个写操作还没完成,后一个就开始了,导致数据错乱。虽然 MRAM 写入快,但在 SPI 时钟较慢的情况下,写入周期相对传输时间不可忽略。
解决办法很简单:每次写操作后加忙判断,或者加一个小延时。我后来统一改成写完后调用MRAM_IsBusy()等待,问题再没出现过。
另一个坑是页写跨页。有次写 300 字节的数据,没做跨页处理,结果写到页尾后回卷到页首,把前面的数据覆盖了。这个 bug 隐蔽性强,因为读出来的数据看起来是“对”的,只是位置错了。后来我在页写函数里强制检查跨页,超过就拆分。
5.3 电源与干扰问题的处理
工业现场电磁环境复杂,MRAM 虽然抗干扰能力不错,但电源不稳照样出问题。我遇到过电机启停时 MRAM 读写异常的情况,后来在电源引脚加了 TVS 管和磁珠,问题解决。
PCB 布局上,MRAM 尽量远离继电器、接触器、变频器这些干扰源。如果实在避不开,用屏蔽罩或者把 MRAM 放在板子另一面。SPI 信号线包地处理,时钟线尤其要注意。
注意:MRAM 的数据保持能力虽然强,但强磁场环境仍然可能影响数据。如果设备附近有大型电磁铁或电机,建议加磁屏蔽,或者定期做数据校验和重写。
5.4 常见问题速查表
| 问题 | 排查步骤 | 解决方案 |
|---|---|---|
| 读不到 ID | 测电源、测 CS、抓 SPI 波形 | 检查接线和模式配置 |
| 写入后读出错 | 检查 WREN、检查忙判断 | 加忙等待,确认写使能 |
| 数据随机翻转 | 检查电源纹波、检查干扰源 | 加滤波电容,远离干扰源 |
| 通信距离短 | 检查线长、检查匹配电阻 | 缩短线长,加串阻 |
| 高温下不稳定 | 检查温度范围、检查电源 | 确认工业级器件,加强散热 |
6. 性能实测与优化建议
6.1 读写速度实测数据
我用逻辑分析仪和示波器实测了一组数据,SPI 时钟 16MHz,单字节读写和批量读写的耗时如下:
| 操作类型 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节写 | 1 字节 | 约 1.2μs | 0.83 MB/s |
| 单字节读 | 1 字节 | 约 0.9μs | 1.1 MB/s |
| 页写 | 256 字节 | 约 140μs | 1.8 MB/s |
| 连续读 | 256 字节 | 约 130μs | 1.9 MB/s |
这个速度对于工业数据采集完全够用。如果换成 40MHz 时钟,理论速度还能翻倍多,但对 PCB 布线要求更高,需要评估实际需求。
6.2 降低功耗的技巧
MRAM 的待机功耗很低,但 SPI 时钟频繁翻转也会耗电。如果设备是电池供电,可以在不读写时把 CS 拉高,SPI 模块关掉,需要时再初始化。PIC18F96J94 的 SSPEN 位可以关闭 SPI 模块,进一步省电。
另外,批量读写比单字节读写效率高,单位数据的功耗更低。所以能批量就批量,别一个字节一个字节地磨。
6.3 长期运行的可靠性验证
我做过一个连续写入测试,每秒写 1000 次,每次 32 字节,跑了 30 天,累计写入约 26 亿次,数据零丢失。这个测试虽然不能完全代表实际工况,但至少说明 MRAM 的寿命和稳定性是靠谱的。
实际部署时,建议加一个看门狗和定期自检。自检内容包括:读关键参数区、校验 CRC、检查日志区完整性。发现问题及时上报,别等设备彻底挂了才发现。
7. 项目扩展与个人体会
这套 MRAM 加 PIC18F96J94 的方案,后来我又用在了几个类似的项目上,比如智能电表的数据记录、工业网关的配置存储、医疗设备的参数保存。每次稍微调整一下地址规划和数据结构就能复用,省了不少事。
如果后续要扩展,有几个方向可以考虑。一是换更大容量的 MRAM,比如 16Mbit 的型号,软件几乎不用改,只是地址范围变大。二是加文件系统,把 MRAM 当成一个小型磁盘来管理,适合存储非结构化数据。三是做双机冗余,两颗 MRAM 互为备份,进一步提高可靠性。
我个人在实际操作中的体会是:选对存储介质,能省掉后面一大半的麻烦。MRAM 虽然单价高一点,但它的“直接写、无限写、掉电不丢”这三个特性,在工业场景里太值了。PIC18F96J94 这颗老牌单片机,虽然性能不算强,但胜在稳定、外设全、工具链成熟,跟 MRAM 搭配起来,做中小型工业设备的数据存储,是个很实在的组合。
最后分享一个小技巧:调试 SPI 的时候,先把时钟降到 1MHz 以下,确认通信正常后再逐步提高。这样能快速区分是时序问题还是信号完整性问题。等高速下也稳定了,再优化代码效率。别一上来就飙高频,出了问题很难定位。