1. 为什么 MRAM 在工业嵌入式场景里越来越受关注
1.1 从 EEPROM 和 Flash 的痛点说起
做过工业设备的人大概都有过这样的经历:设备在现场跑了几年,突然某天参数丢失了,返厂一查,EEPROM 的某个扇区写坏了。或者更尴尬的是,掉电那一瞬间正在写数据,结果整块参数区变成了乱码。这类问题在电力监控、工业仪表、医疗设备里特别常见,因为它们的共同特点是——频繁写小数据、随时可能断电、要求十年以上可靠运行。
传统的方案无非两种:EEPROM 和 NOR Flash。EEPROM 字节级可写,但写入速度慢得让人着急,典型字节写入时间在 5ms 到 10ms 量级,而且擦写寿命通常只有 100 万次左右。NOR Flash 速度快、容量大,但必须按扇区擦除,写之前要先擦,擦写寿命一般 10 万次,而且擦除操作耗时长,掉电保护逻辑复杂。两者在“高频小数据写入 + 突发掉电”这个组合场景下,都算不上理想。
MR25H40CDF 这类 MRAM 器件之所以近几年在工业圈被反复提起,核心就在于它把两者的优点捏到了一起:字节级随机读写、纳秒级写入、几乎无限的擦写寿命、天然的非易失性。它不需要擦除就能直接覆盖写,写入过程没有电荷泵升压,也就没有“写一半掉电变砖”的问题。对于用 PIC18F86K22 这类 8 位 MCU 做工业控制的项目来说,这几乎是把数据存储这块的心病直接摘掉了。
1.2 MR25H40CDF 到底是个什么器件
先把这颗芯片的基本盘说清楚。MR25H40CDF 是 Everspin 家的 4Mbit(512KB)SPI 接口 MRAM,工作电压 2.7V 到 3.6V,标准四线 SPI 接口(CS、SCK、MOSI、MISO),支持 SPI 模式 0 和模式 3,最高时钟频率 40MHz。它的存储阵列是磁隧道结(MTJ),靠磁化方向存储数据,所以写入是真正的“物理翻转”,不需要像 Flash 那样先注入电荷再隧穿。
几个关键参数值得单独拎出来:
| 参数 | MR25H40CDF | 典型 EEPROM | 典型 NOR Flash |
|---|---|---|---|
| 接口 | SPI | I2C/SPI | SPI |
| 写入粒度 | 字节 | 字节 | 扇区(4KB) |
| 写入时间 | 无等待(纳秒级) | 5~10ms/字节 | 擦除 50~300ms |
| 擦写寿命 | 10^14 次以上 | 10^6 次 | 10^5 次 |
| 掉电数据保持 | 20 年以上 | 10~40 年 | 10~20 年 |
| 待机电流 | 约 100µA | 1~3µA | 10~50µA |
注意最后一行,MRAM 的待机电流比 EEPROM 高,这是它的代价。所以在纯电池供电、几年不换电池的场合,得权衡一下。但工业设备大多有稳定供电,这点电流通常不是问题。
1.3 PIC18F86K22 为什么适合搭配它
PIC18F86K22 是 Microchip 的 8 位 MCU,128KB Flash、4KB RAM、自带 MSSP 模块(可配置成 SPI 主模式),工作电压 1.8V 到 5.5V。它有几个特点让它和 MR25H40CDF 很搭:一是 MSSP 硬件 SPI 稳定可靠,不用软件模拟时序;二是它本身有比较丰富的定时器、ADC、比较器,适合做工业采集控制;三是 8 位机成本低、生态成熟,很多老项目维护起来方便。
用硬件 SPI 而不是软件模拟,这点在工业环境里很重要。软件模拟 SPI 的时序受中断影响,一旦有高优先级中断插进来,时钟就可能被拉长,虽然 MRAM 对时序容忍度比 Flash 高,但长期看硬件 SPI 的确定性更好,EMC 表现也更稳。
2. 硬件连接与 SPI 时序设计要点
2.1 引脚连接与电平匹配
MR25H40CDF 是 3.3V 器件,PIC18F86K22 可以工作在 3.3V,所以直接对接没问题。如果 MCU 跑 5V,那就必须做电平转换,否则 MRAM 的 IO 会被打坏。连接关系如下:
- PIC18F86K22 的 SCK(RC3)接 MRAM 的 SCK
- SDO(RC5)接 MRAM 的 SI(MOSI)
- SDI(RC4)接 MRAM 的 SO(MISO)
- 任意 GPIO 接 MRAM 的 CS(片选)
- VCC 接 3.3V,GND 共地
这里有个容易踩的坑:MSSP 模块的 SDO/SDI 引脚是固定的,不能随便映射。PIC18F86K22 的 MSSP 在 RC3/RC4/RC5 上,配置前要先把这几个引脚设成数字 IO,关掉对应的模拟功能,否则 SPI 死活不通。我见过有人调了半天以为是时序问题,结果是 ANSEL 寄存器没清。
CS 片选建议用硬件 GPIO 控制,不要用 MSSP 的 SS 自动片选。原因很简单:MRAM 的指令序列里,CS 拉低后要先发命令字节再发地址,整个过程中 CS 必须保持低电平,用软件控制 GPIO 最直观,也方便在两次操作之间插入延时做调试。
2.2 SPI 模式选择与时序参数
MR25H40CDF 支持 SPI 模式 0(CPOL=0, CPHA=0)和模式 3(CPOL=1, CPHA=1)。PIC18F86K22 的 SSPCON1 里 CKP 位控制时钟极性,SSPSTAT 的 CKE 位控制采样边沿。对应关系是:
- 模式 0:CKP=0,数据在时钟上升沿采样,下降沿变化
- 模式 3:CKP=1,数据在时钟下降沿采样,上升沿变化
我一般选模式 0,因为它是绝大多数 SPI 器件的默认模式,调试时用逻辑分析仪看波形最直观。配置代码大致是这样:
// 假设 Fosc = 64MHz, SPI 时钟 = Fosc/(4*(SSPADD+1)) // 想要 10MHz,SSPADD = 64/(4*10) - 1 = 0.6,取 0 得 16MHz // 想要 5MHz,SSPADD = 64/(4*5) - 1 = 2.2,取 2 得 5.33MHz SSP1STAT = 0x00; // 模式0:CKE=0 SSP1CON1 = 0x20; // SSPEN=1, CKP=0, SPI主模式, 时钟=Fosc/4 SSP1ADD = 2; // 分频后约 5.33MHz时钟频率不是越高越好。虽然 MRAM 标称 40MHz,但实际布线如果比较长、或者板子上有干扰源,跑太高会出现偶发读错。工业板子我一般先跑 5MHz 验证功能,稳定后再往上试,10MHz 是个比较稳妥的平衡点。如果走线超过 10cm,建议降到 2MHz 以下,或者加串联电阻做阻抗匹配。
2.3 电源去耦与 PCB 布局
MRAM 虽然写入不靠电荷泵,但高速翻转时瞬态电流还是有的。VCC 引脚旁边必须放 0.1µF 陶瓷电容,紧贴引脚,再并一个 1µF 的做低频滤波。地平面要完整,SPI 四根线尽量等长、远离高频开关节点(比如 DC-DC 的 SW 引脚)。
有个细节很多人忽略:MRAM 的 CS 线要加一个上拉电阻(比如 10kΩ 到 VCC)。因为 MCU 复位期间 GPIO 是高阻态,CS 可能被干扰拉低,导致 MRAM 误以为收到命令。加上拉后,复位期间 CS 保持高电平,器件处于待机,安全得多。
3. 驱动实现:从底层字节读写到上层数据管理
3.1 底层 SPI 收发函数
一切从最基础的字节收发开始。PIC18F86K22 的 MSSP 收发是同一个寄存器 SSP1BUF,写进去启动发送,读出来是接收数据。注意发送和接收是同时进行的,所以读一个字节其实就是发一个 dummy 字节然后取回结果。
unsigned char SPI_ExchangeByte(unsigned char data) { SSP1BUF = data; // 写入待发送数据 while (!PIR1bits.SSP1IF); // 等待传输完成 PIR1bits.SSP1IF = 0; // 清中断标志 return SSP1BUF; // 返回接收到的数据 }这个函数是整个驱动的地基,必须保证它足够快且不被打断。如果系统里有高优先级中断,建议在收发期间短暂关中断,或者把 SPI 操作放在中断优先级最高的任务里。我实测过,如果在 5MHz SPI 下被一个 10µs 的中断打断,读出来的数据大概率是错的,因为 MSSP 的接收缓冲只有一级,中断期间新数据会覆盖旧数据。
3.2 MRAM 命令集与读写时序
MR25H40CDF 的命令集很简洁,常用的就几条:
| 命令 | 编码 | 作用 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
写操作前必须先发 WREN,这是很多新手容易漏的一步。MRAM 上电后默认是写禁止状态,不发 WREN 直接 WRITE,数据写不进去,而且不会报错,读回来还是旧值,特别容易误判成硬件问题。
写一个字节的完整时序是:CS 拉低 → 发 0x06(WREN)→ CS 拉高 → CS 拉低 → 发 0x02(WRITE)→ 发 24 位地址(高字节在前)→ 发数据字节 → CS 拉高。注意 WREN 和 WRITE 之间必须拉高 CS,这是两个独立的事务。
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; }读操作简单些,不需要 WREN:CS 拉低 → 发 0x03 → 发 24 位地址 → 连续读 N 个字节 → CS 拉高。MRAM 支持连续读,地址会自动递增,所以读一整块数据只需要一次事务,效率很高。
3.3 页写与连续写优化
虽然 MRAM 支持字节写,但每次写都要发命令 + 3 字节地址,开销不小。如果要写连续的一段数据,可以用连续写模式:发一次 WRITE 命令和起始地址,然后连续发多个数据字节,地址自动递增。这样写 64 字节只需要一次事务开销,比逐字节写快得多。
不过要注意,连续写跨越 256 字节边界时,有些器件会回卷到页首,MR25H40CDF 的行为是地址正常递增,不会回卷,所以可以放心连续写大块数据。但为了代码可移植,我一般还是按页对齐来组织写入,每 256 字节处理一次。
void MRAM_WritePage(unsigned long addr, unsigned char *buf, unsigned int len) { CS = 0; SPI_ExchangeByte(0x06); CS = 1; CS = 0; SPI_ExchangeByte(0x02); SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (unsigned int i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS = 1; }3.4 数据分区与磨损均衡的取舍
MRAM 寿命高达 10^14 次,理论上你每秒写一次,写几百年都写不坏。所以传统 Flash 那套磨损均衡算法在这里基本可以省掉,这是它最大的工程价值之一——代码复杂度直接下降一个量级。
但省掉磨损均衡不代表可以乱写。我建议还是做简单的分区管理:把 512KB 分成几个区域,比如 0x00000~0x0FFFF 存设备参数,0x10000~0x1FFFF 存运行日志,0x20000~0x2FFFF 存校准数据,剩下留作扩展。分区的好处是数据边界清晰,升级固件时不会误擦参数区,调试时也容易定位。
参数区建议加一个简单的校验结构,比如每个参数块前面放 2 字节 CRC16 和 1 字节版本号。虽然 MRAM 掉电不丢数据,但代码 bug 或者外部干扰还是可能写错值,有校验能第一时间发现。
4. 工业场景下的可靠性设计与实测经验
4.1 掉电保护:MRAM 的真正优势场景
工业设备最怕的就是掉电瞬间数据写一半。用 EEPROM 时,你得在掉电检测中断里抢时间把数据写完,但 EEPROM 写一个字节要 5ms,电容储能根本撑不住。用 Flash 更惨,擦除就要几百毫秒,想都别想。
MRAM 的写入是纳秒级完成的,你发出 WRITE 命令后,数据在几个时钟周期内就物理写入了,不存在“写一半”的中间态。这意味着掉电保护逻辑可以极大简化:检测到掉电 → 立刻写一次关键数据 → 完事。不需要大电容,不需要复杂的备份机制。
我做过一个测试:在 5MHz SPI 下,写 16 字节关键数据(含命令和地址开销)大约需要 40µs。用一颗 100µF 电容,在 3.3V 下掉电到 2.7V 能撑大约 10ms,足够写几百次了。这个余量非常舒服。
4.2 常见问题排查速查表
实际调试中遇到的问题,我整理成了一张表,方便快速定位:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读出来全是 0xFF | CS 没拉低 / SPI 模式错 / 器件没供电 | 用示波器看 CS 和 SCK 波形 |
| 写进去读出来没变 | 漏发 WREN / 写保护引脚被拉低 | 检查 WREN 时序,测 WP 引脚电平 |
| 偶发读错 | SPI 时钟太快 / 走线太长 / 无去耦 | 降时钟到 1MHz 测试,加去耦电容 |
| 上电后数据乱 | CS 复位期间被干扰 / 电源上升慢 | CS 加上拉,检查电源斜率 |
| 连续读越界 | 地址计算溢出 / 长度参数错 | 打印地址和长度,检查边界 |
其中“写进去读出来没变”这个坑我踩过两次。第一次是漏了 WREN,第二次是板子上 WP 引脚被误接到了 GND。MR25H40CDF 有个 WP 引脚,低电平时硬件写保护,如果你不需要这个功能,直接拉到 VCC。很多人照着参考设计抄,没注意 WP 的状态,结果调半天。
4.3 实测性能数据与优化建议
我在一块工业采集板上做过实测,MCU 跑 64MHz,SPI 跑 10MHz,MRAM 用模式 0:
- 单字节写(含 WREN):约 4.8µs
- 64 字节连续写:约 58µs
- 单字节读:约 3.2µs
- 512 字节连续读:约 420µs
这个速度对于工业参数存储、日志记录完全够用。如果嫌慢,可以把 SPI 提到 20MHz,但要注意 PCB 质量。我的建议是:功能验证阶段用 1MHz,稳定后逐步提到 10MHz,超过 10MHz 必须用示波器确认信号完整性。
另外一个小技巧:如果系统里同时有 SPI Flash 和 MRAM,可以共用 SPI 总线,用不同的 CS 区分。但要注意 Flash 的擦除操作会占用总线几百毫秒,期间 MRAM 无法访问。如果 MRAM 上有实时性要求高的数据,建议单独给它一路 SPI,或者把 Flash 操作放到低优先级任务里。
5. 从参数存储到日志系统的完整落地
5.1 参数存储模块的设计
参数存储的核心需求是:上电加载、修改后保存、掉电不丢、版本可升级。我一般设计一个结构体,把设备所有参数打包,前面加头部信息:
typedef struct { unsigned short crc; unsigned char version; unsigned char reserved; // 参数区 float kp, ki, kd; unsigned short sample_rate; unsigned char device_id[8]; // ... 其他参数 } DeviceParams;保存时先算 CRC,再整块写入 MRAM 的参数区。加载时读出来校验 CRC,不通过就用默认值。版本号用于固件升级时判断参数结构是否变化,变了就做迁移或者重置。
这里有个细节:不要每次改一个参数就写一次 MRAM。虽然 MRAM 寿命无限,但频繁写会占用 SPI 总线,影响其他任务。我的做法是在 RAM 里维护一份参数副本,修改时只改 RAM,等到“保存”按钮按下或者定时 5 秒无修改时才真正写入。这样既减少总线占用,也避免误操作。
5.2 运行日志的循环存储
日志系统和参数存储不同,它是持续写入、容量有限、旧数据可覆盖。用 MRAM 做日志特别合适,因为不需要擦除,直接覆盖写就行。
我通常把日志区做成环形缓冲:一个写指针存在 MRAM 固定位置,每次写日志时读指针、写数据、更新指针。日志条目包含时间戳、事件类型、数据负载。读日志时从指针位置往前读,遇到无效条目就停。
void Log_Write(unsigned long timestamp, unsigned char type, unsigned char *data, unsigned char len) { unsigned long ptr = MRAM_ReadLong(LOG_PTR_ADDR); if (ptr < LOG_START || ptr >= LOG_END) ptr = LOG_START; unsigned char buf[16]; buf[0] = (timestamp >> 24) & 0xFF; // ... 打包时间戳、类型、长度、数据 MRAM_WritePage(ptr, buf, 4 + 1 + 1 + len); ptr += 4 + 1 + 1 + len; if (ptr >= LOG_END) ptr = LOG_START; MRAM_WriteLong(LOG_PTR_ADDR, ptr); }注意写指针本身也要存 MRAM,而且要在日志数据写完之后再更新。这样即使更新指针时掉电,最多丢一条日志,不会破坏整个日志区的结构。
5.3 固件升级时的数据保护
工业设备经常需要现场升级固件。升级过程中最怕的就是参数区被误擦。用 MRAM 的好处是,升级流程里根本不需要碰存储区,因为 MRAM 不是代码存储介质,MCU 的 Flash 才是。所以升级时只要保证不调用参数区的写函数就行。
但有个隐患:如果新固件的参数结构变了,旧参数读出来会错位。我的做法是在参数头部放一个 magic number 和版本号,新固件启动时检查,不匹配就加载默认参数并标记需要重新配置。这样即使升级出问题,设备也能启动到安全状态,不会因为参数错乱而失控。
6. 几个容易被忽略的工程细节
6.1 上电初始化顺序
上电时,MRAM 需要一点时间稳定。虽然它的上电时间很短(典型 1ms 以内),但保险起见,我一般在 MCU 初始化完成后延时 10ms 再访问 MRAM。另外,初始化 SPI 之前先把 CS 拉高,避免上电瞬间产生伪命令。
还有一点:MRAM 的状态寄存器上电默认值要确认。有些批次的器件出厂时状态寄存器里的写保护位是置位的,虽然 MR25H40CDF 默认不保护,但如果你换了供应商或者批次,最好读一次状态寄存器确认。读状态用 RDSR 命令,返回值的 bit0 是 WIP(写进行中),bit1 是 WEL(写使能锁存),bit7 是 SRWD(状态寄存器写保护)。
6.2 中断与 SPI 的冲突处理
如果系统里有多个中断源,SPI 操作被中断打断是难免的。我的处理原则是:SPI 事务期间关全局中断,事务结束立刻开。一个 SPI 事务最长也就几十微秒(连续读 512 字节约 420µs),对大多数工业应用来说,这个中断延迟是可以接受的。
如果实在不能关中断,那就用状态机把 SPI 操作拆成非阻塞的,每次中断里推进一个字节。但这样代码复杂度会上升不少,除非有硬性实时要求,否则不推荐。
6.3 温度与长期可靠性
MRAM 的工作温度范围通常是 -40°C 到 +85°C,工业级够用。但要注意,高温下 MRAM 的保持特性会略有下降,虽然 20 年的数据保持是在 85°C 下保证的,但如果你的设备长期在 85°C 以上运行,建议定期刷新关键数据(读出来再写回去)。这个操作对 MRAM 来说毫无压力,因为寿命无限。
另外,强磁场环境要小心。MRAM 靠磁化方向存储,虽然它的磁屏蔽做得不错,但在接近电机的场合,还是建议做一下磁屏蔽或者拉开距离。我见过一个案例,MRAM 放在变频器旁边,数据偶尔翻转,后来加了坡莫合金屏蔽片就解决了。
7. 写在最后的一点个人体会
这套 MR25H40CDF + PIC18F86K22 的组合,我在三个工业项目里用过,最长的已经跑了四年多,现场没有出现过一次数据丢失。相比之前用 EEPROM 的方案,代码量少了大概三分之一,掉电保护电路也简化了很多。唯一的代价是待机电流高一点,但对于有稳定供电的工业设备来说,这点功耗完全可以接受。
如果你正在选型,我的建议是:只要你的场景涉及频繁写小数据、或者对掉电保护有要求,MRAM 值得多花的那点成本。它省下的调试时间和现场故障率,远比芯片本身的差价值钱。至于 SPI 驱动,照着上面的代码框架改改就能用,真正要花心思的是数据分区和校验设计,这部分想清楚了,后面基本不会出问题。