把 MR25H40CDF 和 PIC18F4455 放在一起做数据存储,是我去年帮客户做工业参数记录仪时定下来的方案。前者是 Everspin 一颗 4Mbit 的串行 MRAM,后者是 Microchip 的老牌 USB 单片机,组合到一起后,掉电不丢、无限次写入、免擦除,正好对上了工业现场最头疼的那几个存储痛点:数据要存得住、写得快、不能因为频繁擦写把介质写废掉。这篇文章我把从选型、接线、SPI 读写代码到调试时踩过的坑,完整走一遍,给正在做嵌入式存储相关的朋友做个参考。
1. 方案选型:为什么 MRAM 能当成"不丢数据的 SRAM"用
1.1 传统存储介质在工业现场的尴尬
做嵌入式的人对存储方案都不陌生,最常见的就是 EEPROM、NOR Flash,外加 SRAM 加电池。但真上了工业设备后,这几个方案各有各的难受:
- EEPROM 容量小还行,大容量 SPI EEPROM 又贵又少见,而且写寿命也就一百万次左右,如果系统每秒钟记一次日志,用不了几天就摸到寿命上限。
- NOR Flash 容量大,但写入之前必须先擦除,擦除按扇区来,擦写寿命一样有上限。设备掉电瞬间如果正赶上擦除,数据一致性就很麻烦,还容易把扇区搞坏。
- SRAM 加电池这种方式,速度确实快,但电池总有没电的一天,维护成本高,工业环境高温下电池可靠性也差。
所以工业应用里,我一直想要一种"写起来像 RAM、断电又不丢"的东西。MRAM 正好就是这么个角色。MR25H40CDF 是 4Mbit 的串行 MRAM,容量 512KB,SPI 接口,原理是磁隧道结阻态来存储数据,不是电荷存储,所以不存在擦除动作,也没有擦写寿命上限。写入速度跟 SRAM 一个量级,数据保存年限还能到几十年,工作温度范围也能覆盖工业级。
1.2 为什么选中 PIC18F4455 做主控
PIC18F4455 属于 Microchip 8 位 PIC,带硬件 SPI(MSSP 模块)和 USB 外设,这在工业小系统里很实用。
选它的理由很简单:
- 我这边既有代码是基于 PIC18 的,客户产线上一堆老设备也是 PIC,工具链成熟,供货周期稳定,不需要冒险换平台。
- 自带硬件 SPI 模块,读写 MRAM 不用软件模拟时序,CPU 占用低,实时性更好。
- 带 USB,数据记录仪做出来后,上位机可以直接拉数据,省掉额外的 USB 转串口芯片。
- 工业环境里大家对 PIC 的稳定性有共识,抗干扰能力和看门狗、掉电复位这类外设在数据安全场景下很重要。
1.3 和 EEPROM、FRAM、电池 SRAM 的对比
为了说清楚选型逻辑,我把当时对比的几个方案整理成了一张表,数据指标是各厂家中等偏上的典型值。
| 项目 | SPI EEPROM | NOR Flash | SPI FRAM | SRAM + 电池 | MR25H40CDF |
|---|---|---|---|---|---|
| 写前擦除 | 不需要 | 需要 | 不需要 | 不需要 | 不需要 |
| 写寿命 | 约 100 万次 | 约 10 万次 | 约 1 亿次 | 无限 | 近乎无限 |
| 典型访问速度 | 慢 | 慢 | 中 | 快 | 快 |
| 掉电保存 | 可以 | 可以 | 可以 | 依赖电池 | 可以 |
| 工业温度范围 | 有 | 有 | 有 | 受限 | 有 |
| 数据一致性 | 一般 | 需要坏块管理 | 较好 | 一般 | 较好 |
从这个表能看出来,MRAM 在"频繁写入 + 掉电保存 + 数据一致性好"这个象限里几乎没有对手。成本确实比 EEPROM 高一点,但工业设备上数据可靠性优先,一块板子省几块钱结果现场故障返修,得不偿失。
2. 硬件连接与通信链路搭建
2.1 引脚怎么接
PIC18F4455 的 MSSP 模块在 SPI 主模式下由三个引脚组成:SCK、SDO、SDI。SCK 在 RC3,SDO 在 RC5,SDI 在 RC4。除了这仨,还要用普通 GPIO 拉住 MRAM 的片选 CS,我这边用的是 RC1。
接线表如下:
| MR25H40CDF 引脚 | PIC18F4455 引脚 | 说明 |
|---|---|---|
| CS | RC1(GPIO 控制) | 片选,低电平有效 |
| SCK | RC3 | SPI 时钟 |
| SDI(MOSI) | RC5(SDO) | 主发从收 |
| SDO(MISO) | RC4(SDI) | 主收从发 |
| WP | 3.3V | 写保护,高电平允许正常写 |
| HOLD | 3.3V | 禁用 HOLD 功能,保持上拉 |
| VCC | 3.3V | 电源 |
| GND | GND | 地 |
有一点要特别注意:WP 和 HOLD 这两个引脚不能悬空。悬空的时候引脚电平不确定,可能偶发地把写保护打开,或者进入 HOLD 状态,导致数据写不进去。直接接到 VCC 是最省事的做法,如果系统里需要硬件写保护,可以把 WP 接到一个 GPIO 口,不用写的时候拉高,需要彻底禁止写的时候拉低。
2.2 3.3V 和 5V 的电平匹配问题
MR25H40CDF 的工作电压是 2.7V 到 3.6V,这是一颗纯 3.3V 器件。PIC18F4455 本身可以跑 5V 也可以跑 3.3V,但如果 MCU 用 5V,SPI 高电平直接进 MRAM 的引脚,长期使用有风险,还需要电平转换电路,白白增加复杂度。
我的做法是让整个系统统一跑 3.3V,PIC18F4455 用 3.3V 供电,系统时钟降档运行。这样做的好处是 SPI 电平完全匹配,不用加转换芯片,布线也简单。如果项目里已经有成熟的 5V 电源平台,就必须加双向电平转换芯片,或者至少在 SPI 输出线上串联 1kΩ 左右的电阻做电平限制,但这个方法只适合低速场景,不推荐作为量产方案。
MCU 供电纹波也要关注。MRAM 这种芯片对电源稳定性要求不算苛刻,但 SPI 通信如果电源噪声太大,数据采样容易出错。VCC 引脚附近放一个 0.1μF 陶瓷电容,再加一个 4.7μF/10μF 的钽电容或电解电容,这是基本操作。
2.3 SPI 模式选择和时钟频率
MR25H40CDF 支持 SPI Mode 0 和 Mode 3,PIC18F4455 的 MSSP 很容易配置成这两个模式。我习惯用 Mode 0:空闲时钟为低,数据在上升沿采样。这个模式在大多数 SPI 器件上都通用,后面换 Flash 或者其他传感器也不用重新改配置。
时钟频率不能盲目拉高。MRAM 本身支持到 40MHz,但 PIC18F4455 在 3.3V 供电下最高系统时钟有限制,而且板上走线、接插件、线束长度都会影响信号质量。我最终把 SPI 时钟配置成 Fosc/16,以系统时钟 24MHz 计算,SPI 速率是 1.5MHz。这个速度在数据记录场景下完全够用,还留足了信号裕量,哪怕客户把线束加长到 20 厘米也不会出问题。
2.4 MSSP 模块寄存器初始化
初始化代码直接用寄存器写,不用 MCC 自动生成,原因就一个:寄存器逻辑非常固定,自己写更可控,出了问题也容易排查。
#include <xc.h> #define MRAM_CS_LOW() (LATC1 = 0) #define MRAM_CS_HIGH() (LATC1 = 1) #define MRAM_CS_TRIS (TRISC1 = 0) void spi_init(void) { // SPI Master 模式,时钟 = Fosc/16,空闲时钟低 = Mode 0 SSP1CON1 = 0x03; // 0b00000011:Master,Fosc/16,CKP=0 SSP1STAT = 0x00; // SMP=0,CKE=0,Mode 0 TRISC3 = 0; // SCK 输出 TRISC4 = 1; // SDI 输入 TRISC5 = 0; // SDO 输出 MRAM_CS_TRIS; MRAM_CS_HIGH(); SSP1CON1bits.SSPEN = 1; // 使能 MSSP } uint8_t spi_xfer(uint8_t d) { SSP1BUF = d; while (!SSP1STATbits.BF); // 等待接收完成 return SSP1BUF; }这段代码是整套读写功能的基础,后面所有 MRAM 操作都靠这个spi_xfer函数完成。
3. 数据存储与读取的核心实现
3.1 MR25H40CDF 指令速查
这颗 MRAM 的指令集跟 SPI Flash 很像,上手非常快。常用指令就下面几条:
| 指令 | 操作码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| READ | 0x03 | 读数据 |
| FAST_READ | 0x0B | 快速读数据 |
| WRITE | 0x02 | 写数据 |
| SLEEP | 0xB9 | 进入睡眠 |
| WAKE | 0xAB | 唤醒,也可用于读 ID |
跟 Flash 最大的区别就是不需要擦除指令,也没有页编程的概念。每次写之前只需要发 WREN,然后直接发 WRITE 加地址加数据,写完就生效,不用等待,下一条命令马上就能发。
状态寄存器里最低位是 WEL(写使能锁存位)。发完 WREN 后,这个位会变成 1;完成一次 WRITE 或者收到 WRDI 后,它会自动清零。严谨一点的流程可以在每次写入后读一次状态寄存器,确认 WEL 已经清零,再去做别的事。
3.2 读写函数的完整实现
写使能动作必须单独一个片选周期。这一点特别容易踩坑,我刚开始写的时候图省事,把 WREN 和 WRITE 放在同一个片选周期里发,结果数据根本写不进去。MRAM 的命令格式明确要求 WREN 是一个独立的事务,先拉低 CS,发 0x06,拉高 CS,然后再开始下一个写事务。
void mram_write_enable(void) { MRAM_CS_LOW(); spi_xfer(0x06); MRAM_CS_HIGH(); } void mram_write_disable(void) { MRAM_CS_LOW(); spi_xfer(0x04); MRAM_CS_HIGH(); } uint8_t mram_read_status(void) { uint8_t st; MRAM_CS_LOW(); spi_xfer(0x05); st = spi_xfer(0x00); MRAM_CS_HIGH(); return st; }读操作就不需要写使能,直接拉低片选,发 READ 指令、3 字节地址,然后连续读数据。MR25H40CDF 是 512KB 容量,地址范围从 0x00000 到 0x7FFFF,用 24 位地址格式发送,但实际真正用到的只有低 19 位,高 5 位传 0 就行。
void mram_read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); spi_xfer(0x03); spi_xfer((addr >> 16) & 0xFF); spi_xfer((addr >> 8) & 0xFF); spi_xfer(addr & 0xFF); while (len--) { *buf++ = spi_xfer(0x00); } MRAM_CS_HIGH(); }写操作麻烦在要先发 WREN,然后再发 WRITE 指令加地址加数据,整个过程注意片选信号:
void mram_write(uint32_t addr, uint8_t *buf, uint16_t len) { mram_write_enable(); MRAM_CS_LOW(); spi_xfer(0x02); spi_xfer((addr >> 16) & 0xFF); spi_xfer((addr >> 8) & 0xFF); spi_xfer(addr & 0xFF); while (len--) { spi_xfer(*buf++); } MRAM_CS_HIGH(); }写完想确认是否真正落盘了,可以加一个写后读回校验:
uint8_t mram_verify(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t tmp; while (len--) { mram_read(addr, &tmp, 1); if (tmp != *buf++) { return 1; } addr++; } return 0; }这个校验函数在调试阶段特别有用。MRAM 写进去不会像 Flash 那样有时序等待,读完立即可以看到结果,所以做写后读回验证的成本很低,现场排查问题的时候能省很多时间。
3.3 工业数据写入策略:不能让掉电把数据搞乱
MRAM 的硬件特性决定了它不会因为掉电丢数据,但程序层面的数据一致性还是得靠设计。工业设备掉电是常态,写数据写到一半的时候掉电,可能导致记录里的某些块是旧数据、某些块是新数据。
我在这套方案里做了一个很简单的双区备份机制:
- 把整个 512KB 空间分成 A 区、B 区、指针区三个部分。
- A 区写最新数据,B 区写之前的数据,指针区保存当前有效区域的序号。
- 真正写入时,先写数据区,数据区写完再更新指针区。
- 上电启动后,先读指针区,确定当前有效区域,再只读有效区域里的数据。
这样掉电发生时最坏情况是数据区写了新数据但指针没更新,就还当作旧数据用,不会出现新旧数据混在一起的情况。MRAM 写数据不需要擦除,所以这种固定地址反复更新的做法完全可行,这也是我选它而不是选 Flash 的一个重要原因。
数据帧本身也建议加保护字段。我的记录帧格式是:
| 偏移 | 长度 | 内容 |
|---|---|---|
| 0 | 2 | 帧头 0xAA 0x55 |
| 2 | 2 | 数据长度 |
| 4 | 2 | 记录序号 |
| 6 | N | 有效数据 |
| 6 + N | 2 | CRC16 校验 |
读取的时候先查帧头,再校验数据长度和 CRC,全部通过才认为这条记录有效。别嫌 CRC 麻烦,工业现场电机启动、继电器吸合、变频器干扰,这种瞬时噪声随时可能打到 SPI 线上,没有校验位你根本不知道哪条数据已经被污染了。
3.4 一次实际读写例程
下面这段例程演示了怎么把一条 32 字节的设备状态记录写入 MRAM,再读回来验证:
typedef struct { uint16_t seq; uint16_t temperature; uint16_t pressure; uint32_t fault_flags; uint8_t status; } device_record_t; void save_record(uint32_t base_addr, device_record_t *rec) { uint8_t buf[32]; // 将结构体按固定格式打包进 buf,这里省略具体封装代码 buf[0] = 0xAA; buf[1] = 0x55; buf[2] = sizeof(device_record_t) >> 8; buf[3] = sizeof(device_record_t) & 0xFF; buf[4] = rec->seq >> 8; buf[5] = rec->seq & 0xFF; // ... 填充数据字段和 CRC mram_write(base_addr, buf, sizeof(buf)); if (mram_verify(base_addr, buf, sizeof(buf)) == 0) { // 写入成功 } else { // 写入失败,做异常处理 } }按 1.5MHz SPI 速率算,写一条 32 字节的记录,加上指令和地址开销一共不到 40 个字节,耗时大约 0.2ms。即便一秒钟存 100 条记录,CPU 占用率也非常低,完全不影响其他控制逻辑。
4. 工业场景避坑指南与问题排查实录
4.1 掉电保护:要防的不是 MRAM,而是 MCU
MRAM 不会因为掉电丢数据,但 MCU 掉电过程中如果程序跑飞,可能会向 MRAM 发出乱七八糟的写命令。这才是真正的风险源。
我遇到过一个问题:设备断电瞬间,MRAM 里某个固定区域的数据整体变成 0xFF。排查了很久,最后定位到是 MCU 在掉电过程中,GPIO 电平不稳,片选信号出现了一次短脉冲,SPI 时钟线上也出现毛刺,MRAM 被误写了一次。
解决方案分三层:
- 硬件上加掉电检测电路,检测到电源跌落时,立刻把 MRAM 的片选拉高,切断通信路径。
- 初始化时把 GPIO 的电平状态设置好,绝对不能让 CS 在 MCU 上电过程中处于低电平。
- 启动看门狗,并且看门狗超时后要重新初始化 MSSP 模块和 GPIO,避免程序跑飞后一直以错误状态操作外部设备。
4.2 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 读出来全是 0xFF | CS 没真正拉低、接线错误、MRAM 供电异常 | 用示波器看 CS 和 SCK 波形,检查 VCC |
| 能写一次,后面写不进 | 每次写之前漏发 WREN | 确认每个写事务都以 WREN 开始 |
| 数据错位 | SPI 时钟太快、Mode 配置不一致 | 降低 SPI 时钟,确认 Mode 0 或 Mode 3 |
| 数据某几个位偶尔跳变 | SPI 线受干扰、地线连接不佳 | 加串阻、缩短走线、检查接地、加 CRC |
| 掉电后固定区域数据被改 | 上电/掉电过程中 CS 毛刺 | CS 加 10kΩ 上拉,掉电检测控制 CS |
| 写指令执行了但数据没变化 | WP 引脚被拉低,写保护生效 | WP 接 3.3V 或改由 MCU 控制 |
4.3 调试阶段最好用的三板斧
第一,逻辑分析仪比示波器好用。SPI 信号是低速数字信号,用逻辑分析仪抓一下 CS、SCK、SDO、SDI 四根线的时序,一眼就能看出片选是不是有毛刺、数据位是不是多了一个或少了一个。我调试 SPI 器件基本不用示波器,除非要查信号完整性问题。
第二,写一个固定数据的自检函数。上电后在 MRAM 里写一串 0x5A、0xA5 交替的测试数据,再读回来比较。如果这步过了,说明基本通信链路没问题;没过,优先检查接线和 SPI 模式,别急着改应用逻辑。
第三,所有读写函数都留一个返回值。工业产品不比其他应用,写失败必须能通过异常上报机制反映出来,不能静默失败。哪怕只是简单的写后回读校验,也能帮你拦掉一大批现场隐患。
4.4 关于 PCB 布线和信号质量的一点经验
如果这套方案用在 PCB 上,MRAM 尽量靠近 MCU 放,SPI 走线不要穿过电机驱动、继电器、开关电源这些强干扰区域。SCK 和 SDO 上加 33Ω 串联电阻,成本和面积增加很小,但对振铃抑制效果明显。CS 引脚建议加 10kΩ 上拉到 VCC,防止上电瞬间误触发。
如果设备要过 EMC 测试,还应该在 SPI 线上预留对地的滤波电容位置,但注意电容不能太大,否则会把信号边沿搞钝。一般预留 10pF 到 22pF 就够用了,真测试超标再根据实际情况调整。
4.5 一点个人体会
这个项目做完之后,我对 MRAM 的定位有了新的认识:它并不是要全面替代 Flash 或者 EEPROM,而是在"数据要频繁更新"和"掉电不能丢"这两个条件同时成立的时候,它是最省心的答案。PIC18F4455 虽然老,但它的 MSSP 模块配合 MRAM 这种指令简单的介质,整个驱动代码不超过一百行,稳定性和可维护性都很好。
以后再遇到类似需求,我会先问自己一个问题:这个数据是不是经常要改?如果答案是肯定的,那就别纠结 Flash 的擦写均衡了,直接上 MRAM,后面能少掉一大半存储相关的坑。