1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18F86J15
工业现场的数据记录仪、电力监控终端、医疗设备参数存储模块,这类场景有一个共同的痛点:数据写入频繁、掉电随时可能发生、现场环境温度跨度大、设备要求十年以上不换电池。传统方案里,EEPROM 擦写寿命十万次左右,铁电存储器容量小且贵,带后备电池的 SRAM 又要考虑电池老化和更换问题。我最初接手这个项目时,客户的要求很直接——每秒记录一次关键参数,设备寿命内不能丢数据,掉电瞬间正在写的那条记录也不能损坏。
选型阶段我对比了几条路线。SPI NOR Flash 便宜、容量大,但写入前要擦除扇区,写入延迟高,而且擦写寿命通常只有十万次量级,按每秒一次算,单扇区很快就写穿了。EEPROM 字节级写入方便,但同样受限于擦写次数,而且写入周期在毫秒级,频繁写会拖慢主循环。FRAM 读写速度快、寿命近乎无限,但容量普遍偏小,价格也偏高。最后落到MRAM上,具体型号就是MR25H40CDF,4Mbit 容量,SPI 接口,最关键的是它没有擦写寿命的概念——写入次数理论上无限,写入延迟在几十纳秒量级,而且掉电后数据保持二十年以上。
主控这边选PIC18F86J15,理由也很实际。这颗芯片是 Microchip 的 8 位增强型单片机,自带硬件 SPI 模块、64KB Flash、3936 字节 RAM,工作电压 2.0V 到 3.6V,工业级温度范围 -40 到 +85 摄氏度。它的 SPI 模块支持主从模式,波特率可以通过寄存器分频配置,配合 MRAM 的 40MHz 最高时钟完全够用。更重要的是,PIC18 系列在工业现场存量巨大,开发工具链成熟,客户那边的维护工程师也熟悉,后续产线烧录和现场升级都不需要重新培训。
这个组合的核心价值在于:用 MRAM 解决"写不死、掉电不丢"的问题,用 PIC18F86J15 解决"控制简单、成本可控、工业可靠"的问题。两者通过 SPI 总线连接,硬件上只需要四根线(SCK、SDI、SDO、CS),软件上就是标准的 SPI 读写时序。适合谁参考?做工业数据记录、设备参数存储、掉电保护型应用的嵌入式工程师,尤其是那些被 EEPROM 擦写寿命和掉电数据完整性折磨过的朋友。
2. 硬件连接与 SPI 时序细节拆解
2.1 引脚连接与硬件设计要点
MR25H40CDF 是 8 引脚封装,实际用到的信号线不多,但每一根都有讲究。先看连接关系:
| MR25H40CDF 引脚 | 功能 | PIC18F86J15 对应引脚 | 说明 |
|---|---|---|---|
| CS | 片选 | RC2(可配置为普通 IO) | 低电平有效,必须由主控控制 |
| SCK | 时钟 | RC3/SCK | 硬件 SPI 时钟输出 |
| SI | 数据输入 | RC5/SDO | 主出从入 |
| SO | 数据输出 | RC4/SDI | 主入从出 |
| VDD | 电源 | 3.3V | 2.7V 到 3.6V 范围 |
| VSS | 地 | GND | 就近接地 |
| WP | 写保护 | 接 VDD 或 IO 控制 | 低电平禁止写入 |
| HOLD | 保持 | 接 VDD | 低电平暂停通信 |
这里有几个容易踩坑的地方。WP 引脚很多人直接悬空,结果发现写不进去数据,因为悬空电平不确定,芯片可能处于写保护状态。我的做法是接一个 10K 上拉到 VDD,如果应用需要动态写保护,就用一个 IO 口控制,正常写入时拉高,关键配置写完后拉低锁定。HOLD 引脚同理,不用的时候必须拉高,否则 SPI 通信会在中途被暂停,表现为读出来的数据错位或者全 FF。
PCB 布局上,MRAM 尽量靠近主控,SPI 走线控制在 10 厘米以内。SCK 是时钟线,最容易引入干扰,走线要避免和电机驱动、继电器控制线平行。我一般会在 SCK 和 CS 上各串一个 22 欧姆电阻,靠近主控端放置,抑制过冲和振铃。电源引脚旁边放一个 0.1uF 陶瓷电容加一个 1uF 钽电容,MRAM 在写入瞬间电流会有小幅波动,去耦不到位会导致写入失败。
2.2 SPI 模式与波特率配置
MR25H40CDF 支持 SPI 模式 0 和模式 3,也就是 CPOL=0/CPHA=0 或者 CPOL=1/CPHA=1。我选模式 0,因为 PIC18F86J15 的硬件 SPI 在模式 0 下最稳定,时序余量最大。具体来说,空闲时 SCK 为低电平,数据在 SCK 上升沿采样,下降沿输出。
波特率方面,MRAM 最高支持 40MHz,但 PIC18F86J15 在 3.3V 供电、系统时钟 40MHz 的情况下,SPI 时钟通过 SSPADD 寄存器分频。计算公式是:
Fosc / (4 * (SSPADD + 1))
假设系统时钟 Fosc 为 40MHz,想要 10MHz 的 SPI 时钟,SSPADD 应该设为 0,因为 40MHz / 4 = 10MHz。如果系统时钟是 16MHz,想要 4MHz,SSPADD 设为 0 得到 4MHz,设为 1 得到 2MHz。实际项目中我一般跑 5MHz 到 10MHz,太快了在长走线或者干扰环境下容易出错,太慢了影响数据记录吞吐。
配置代码大致如下,用 C 语言基于 Microchip XC8 编译器:
// SPI 初始化,模式 0,时钟 Fosc/4 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 (主模式 Fosc/4) SSPSTAT = 0x00; // SMP=0, CKE=0,模式 0 SSPADD = 0; // 分频值 TRISCbits.TRISC3 = 0; // SCK 输出 TRISCbits.TRISC5 = 0; // SDO 输出 TRISCbits.TRISC4 = 1; // SDI 输入 TRISCbits.TRISC2 = 0; // CS 输出 CS = 1; // 初始拉高这段代码里SSPSTAT的CKE位决定数据在时钟的哪个边沿输出,模式 0 下 CKE=0,SMP=0 表示在数据末尾采样。很多人配置完发现读出来全是 0xFF,八成是模式搞错了,或者 CS 没有正确拉低。
2.3 读写命令集与地址组织
MR25H40CDF 的指令集很精简,常用的就几条:
| 指令名称 | 指令码 | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写入前必须发送 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
地址是 19 位,因为 4Mbit 等于 512KB,需要 19 根地址线。发送地址时要分三个字节:第一个字节的高 3 位是无效的,低 5 位是地址的 A18 到 A14,第二个字节是 A13 到 A6,第三个字节是 A5 到 A0。这个细节很容易搞错,我第一次写驱动的时候就是地址移位没对齐,读出来的数据整体偏移了一个扇区。
读操作的完整时序是:CS 拉低,发送 0x03,发送三个字节地址,然后连续读取数据,每读一个字节 SCK 产生八个脉冲,读完拉高 CS。写操作多一步:先发 WREN(0x06),拉高 CS,再拉低 CS,发 0x02,发地址,发数据,最后拉高 CS。注意 WREN 和 WRITE 之间必须有一次 CS 的上升沿,否则写使能不生效。
3. 驱动实现与数据存储策略
3.1 底层读写函数实现
先写最基础的字节收发函数,这是所有操作的地基:
unsigned char SPI_ExchangeByte(unsigned char data) { SSPBUF = data; while (!SSPIF); // 等待传输完成 SSPIF = 0; return SSPBUF; }这个函数看着简单,但有个隐患:如果 SPI 模块因为故障没有置位 SSPIF,这里会死循环。工业现场我一般加一个超时计数,超过一定次数就返回错误码,让上层决定是否复位 SPI 模块。
读数据函数:
void MRAM_Read(unsigned long addr, unsigned char *buf, unsigned int len) { CS = 0; SPI_ExchangeByte(0x03); SPI_ExchangeByte((addr >> 16) & 0x07); // 只取低 3 位有效 SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (unsigned int i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0x00); } CS = 1; }写数据函数:
void MRAM_Write(unsigned long addr, unsigned char *buf, unsigned int len) { CS = 0; SPI_ExchangeByte(0x06); // WREN CS = 1; CS = 0; SPI_ExchangeByte(0x02); // WRITE SPI_ExchangeByte((addr >> 16) & 0x07); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (unsigned int i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS = 1; // 等待写入完成,MRAM 写入很快,但保险起见查状态 while (MRAM_IsBusy()); }MRAM_IsBusy读状态寄存器的 WIP 位,MRAM 的写入周期极短,通常几十纳秒,实际测试中几乎查不到忙状态,但保留这个检查是良好习惯。
3.2 掉电保护型数据记录结构设计
数据记录不能裸写,要有结构。我设计了一个环形缓冲区加双备份头的方案。存储空间从 0x00000 开始,前 4KB 放元数据,后面是数据区。
元数据区结构:
| 偏移地址 | 长度 | 内容 |
|---|---|---|
| 0x0000 | 4 字节 | 魔数 0x4D52414D("MRAM") |
| 0x0004 | 4 字节 | 写指针,指向下一个写入位置 |
| 0x0008 | 4 字节 | 记录总数 |
| 0x000C | 4 字节 | 元数据 CRC32 |
| 0x0010 | 4 字节 | 备份魔数 |
| 0x0014 | 4 字节 | 备份写指针 |
| 0x0018 | 4 字节 | 备份记录总数 |
| 0x001C | 4 字节 | 备份 CRC32 |
每次写入一条记录,流程是:先写数据区,更新写指针和记录总数,计算 CRC,写入主元数据,再写入备份元数据。掉电如果发生在写主元数据之后、备份之前,上电时主备不一致,以 CRC 正确的那个为准。如果两个都坏了,说明掉电发生在更早的阶段,数据区那条记录可能不完整,通过记录本身的 CRC 校验丢弃即可。
每条记录的结构:
typedef struct { unsigned long timestamp; // 时间戳,秒 unsigned char type; // 记录类型 unsigned char data[27]; // 数据载荷 unsigned short crc; // 记录 CRC16 } LogRecord; // 共 34 字节34 字节对齐到 32 字节边界,每条记录占 64 字节,方便地址计算。512KB 减去 4KB 元数据,大约能存 8000 条记录,环形覆盖。
3.3 写入性能实测与优化
实测数据:SPI 时钟 10MHz 下,写一条 34 字节记录,包括 WREN、地址、数据、状态查询,总耗时约 45 微秒。读一条记录约 38 微秒。每秒写一次,CPU 占用率不到 0.01%,完全无压力。
如果追求更高吞吐,可以去掉每次写前的 WREN,改成上电时发一次 WREN,之后连续写。但这样有风险,如果中间发生干扰导致状态机跑飞,后续写入会全部失败。我的做法是保留每次 WREN,45 微秒的代价换可靠性,值得。
还有一个优化点:MRAM 支持页写吗?严格说 MR25H40CDF 没有页的概念,它是真正的随机访问,任意地址连续写都行,不需要考虑页边界。这是它比 Flash 和 EEPROM 省心的地方,驱动里不用处理跨页拆分。
4. 常见问题排查与实战避坑指南
4.1 读出来全是 0xFF 或 0x00
这是最高频的问题。排查顺序如下:
- CS 有没有拉低。用示波器看 CS 引脚,读操作期间必须是低电平。如果 CS 一直是高,MRAM 根本不响应。
- SPI 模式对不对。模式 0 和模式 3 搞反了,数据采样边沿错位,读出来就是全 FF 或全 00。用逻辑分析仪抓 SCK 和 SI,看数据在哪个边沿变化。
- WP 引脚电平。WP 为低时写入被禁止,但读操作不受影响。如果读都失败,先查 CS 和模式。
- 地址移位错误。19 位地址分三个字节发送,如果第一个字节没有屏蔽高 5 位,地址会错到别的区域,读出来可能是空白区。
我遇到过最隐蔽的一次,是 PCB 上 CS 走线和 SCK 走线挨得太近,SCK 的跳变耦合到 CS 上,导致 CS 在通信中途被干扰出一个窄脉冲,MRAM 状态机复位,后续数据全错。解决办法是 CS 走线包地,或者在 CS 上并一个 100pF 电容到地,滤掉窄脉冲。
4.2 写入后读出来数据不对
写入成功但读出来不一致,常见原因:
- WREN 和 WRITE 之间 CS 没有拉高。MRAM 要求 WREN 之后必须有一个 CS 上升沿来锁存写使能,如果 CS 一直保持低,WRITE 命令会被忽略。
- 写入后立即读取,没有等待。虽然 MRAM 写入极快,但如果 SPI 时钟太快,写入周期还没结束就读,可能读到旧数据。加一个状态查询或者几微秒延时。
- 电源电压偏低。MRAM 写入时电流比读大,如果电源去耦不足,写入瞬间电压跌落,导致写入失败。用示波器看 VDD 引脚,写入时不能有超过 100mV 的跌落。
4.3 掉电后数据丢失或元数据损坏
掉电保护方案再完善,也怕一种情况:掉电发生在写元数据的过程中,主元数据写了一半,CRC 对不上,备份元数据还是旧的,上电后以备份为准,但备份的写指针落后一条记录。这条记录的数据区已经写了,但元数据没更新,相当于丢了一条。
我的处理方式是:数据区写入后,先更新备份元数据,再更新主元数据。这样掉电最坏情况是主元数据旧、备份新,上电时以备份为准,数据不丢。如果掉电发生在更新备份的过程中,备份 CRC 坏,主元数据还是旧的,同样以主为准,数据也不丢。只有主备都坏的情况才会丢,而这种情况概率极低,因为两次写入之间有足够的时间间隔。
另外,元数据区我用了两个独立的 4KB 块,物理地址隔开,避免单点故障。MRAM 本身没有坏块概念,但 PCB 上的干扰可能只影响某个地址段,物理隔离能降低同时损坏的概率。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读全 FF | CS 未拉低 / 模式错误 | 示波器看 CS 和 SCK | 检查 CS 控制,确认 SPI 模式 0 |
| 读全 00 | MISO 未接 / 芯片未供电 | 万用表测 VDD 和 MISO | 检查供电和连线 |
| 写入无效 | WP 为低 / WREN 未生效 | 测 WP 电平,抓 WREN 时序 | WP 上拉,WREN 后加 CS 上升沿 |
| 数据错位 | 地址移位错误 | 对比读写地址 | 屏蔽地址高 5 位 |
| 偶发错误 | 电源干扰 / 走线耦合 | 示波器看电源纹波 | 加去耦电容,CS 包地 |
| 掉电丢数据 | 元数据更新顺序错误 | 模拟掉电测试 | 先备份后主,加 CRC 校验 |
5. 工业场景下的可靠性加固与扩展思路
5.1 温度与长期稳定性考量
MR25H40CDF 的工业级版本支持 -40 到 +85 摄氏度,数据保持 20 年。但在实际现场,温度循环会导致 PCB 应力变化,焊点疲劳。我的经验是,MRAM 芯片底部不要铺大面积铜皮,避免热膨胀系数不匹配导致焊点开裂。如果设备工作在振动环境,芯片四周点红胶固定。
长期稳定性方面,MRAM 的数据保持不需要刷新,也不依赖电荷,所以没有电荷泄漏问题。但要注意,强磁场环境可能影响 MRAM 的磁隧道结状态。虽然 MR25H40CDF 有磁屏蔽设计,但在电机、变压器附近安装时,还是尽量远离强磁场源,或者加一个坡莫合金屏蔽罩。
5.2 与上位机的数据交互设计
设备记录的数据最终要传到上位机。PIC18F86J15 有两个 UART 模块,我用 UART1 做调试输出,UART2 做数据上传。上传协议自定义,帧格式:
| 字节 | 内容 |
|---|---|
| 0 | 帧头 0xAA |
| 1 | 命令码 |
| 2-3 | 数据长度 |
| 4-N | 数据载荷 |
| N+1 | CRC16 低字节 |
| N+2 | CRC16 高字节 |
上位机发命令读取指定范围的记录,下位机从 MRAM 读出后打包上传。如果记录量大,分页传输,每页 512 字节,上位机确认后再传下一页。这个协议简单可靠,现场调试时用串口助手就能手动发命令验证。
5.3 固件升级与参数配置的存储分离
MRAM 除了存记录,还可以存设备参数和固件升级标志。我把存储空间划分成三个区:参数区(0x00000-0x00FFF)、记录区(0x01000-0x7FFFF)、升级区(0x80000-0x7FFFF)。参数区存设备 ID、校准系数、通信地址等,升级区存新固件的标志和校验值。
固件升级流程:上位机把新固件通过 UART 传到升级区,写一个升级标志到参数区,然后复位。Bootloader 启动时检查升级标志,如果有效,就把升级区的固件搬运到程序 Flash,清除标志,跳转到新程序。这个方案利用了 MRAM 掉电不丢的特性,升级过程中断电也不会变砖,重新上电继续升级。
5.4 实操心得与避坑清单
最后分享几条我在这个项目里踩过的坑和总结的经验:
- SPI 时钟先慢后快。调试阶段用 1MHz,确认读写正常后再逐步提高到 10MHz。直接上高频,出了问题很难判断是时序还是硬件。
- CS 控制用宏定义。
#define CS LATCbits.LATC2,方便统一修改,也避免直接操作寄存器时手误。 - 每次写入前读一次状态寄存器。虽然 MRAM 几乎不忙,但这个习惯能提前发现 SPI 通信异常。
- 元数据双备份加 CRC。这是掉电保护的底线,不要省。
- 预留调试接口。PCB 上留出 SCK、SDI、SDO、CS 的测试点,现场排查时直接夹逻辑分析仪。
- 记录结构体加填充字节。对齐到 4 字节边界,读写效率更高,也方便后续扩展字段。
这个方案目前已经在几个工业现场跑了两年多,没有出现过数据丢失。MRAM 的价格虽然比 EEPROM 高,但省掉了电池、省掉了擦写寿命计算、省掉了掉电保护电路,综合成本反而更低。如果你也在做类似的数据记录项目,这套组合值得认真考虑。