1. 这个组合能做什么:MR25H40CDF 与 PIC18F45K50 的应用背景
前一阵在调一块工业采集板,主控是 Microchip 的 PIC18F45K50,数据存储从原来的 SPI EEPROM 换成了 Everspin 的 MR25H40CDF。项目需求很典型:现场设备要记录参数修改、事件日志和累计量,断电不能丢,而且写入频率比普通 EEPROM 敢承受的极限高得多。折腾完回头想,这套 MRAM + 8 位机组合在嵌入式里其实很有代表性,值得把选型、接线、驱动和排障写出来。
先说结论:MR25H40CDF 是一颗 4Mbit 的串行 MRAM,换算过来是 512KB 非易失存储,接口是标准 SPI,指令集和常见 SPI EEPROM 非常接近。PIC18F45K50 则是经典的 40 引脚 8 位单片机,带 USB 全速接口、双 MSSP 模块、12 位 ADC,工业级温度范围,价格也不算贵。这套方案适合做工业数据采集器、环境监控节点、伺服驱动器故障记录这类场景,核心价值就三个字:放心写。你不需要像伺候 Flash 那样去擦除再写,也不用担心写一万次以后某个字节先报废。
嵌入式项目做到中后期,最容易翻车的往往不是算法,而是“数据怎么存才不丢”。很多工程师第一反应是用 SPI EEPROM,一查写寿命 100 万次,勉强够用;再查一下写一个字节要 5ms,要频繁记录波形或者事件时就不太行了。换到 MRAM 之后,这些问题基本消失,但代价是必须自己把驱动、存储布局和掉电恢复逻辑做对。下面我把完整思路和实操过程拆开讲。
1.1 工业记录场景的三个硬指标
工业设备里的数据存储,和开发板上随便存几个数完全不是一回事。我整理了几条硬指标,大家可以拿来自检:
- 写入频率不能成为瓶颈。事件日志、曲线记录、累计量更新,可能出现每秒几十次甚至上百次的写操作。普通 EEPROM 的单字节写周期是毫秒级,Flash 更麻烦,得先擦除整个扇区。MRAM 没有写周期延迟,存储单元本身是随机存储,SPI 发完一帧数据就完成了。
- 断电瞬间的数据要救得回来。设备掉电、看门狗复位、工人直接拉闸,这一类“脏掉电”在工业现场是常态。如果只有最后一条记录丢失,可能只是误差;如果是负载、累计量、校准参数丢掉,那就要出生产事故。MRAM 因为写算法不像 Flash 那样需要电荷泵持续高压,也没有“先擦后写”的窗口,做掉电保护的设计难度低很多。
- 写入寿命要够长。EEPROM 单个字节通常标称 100 万次写,FRAM 能做到 10 亿次以上,MRAM 的卖点则是“实际可视为不限次数”。这一点在做计数器、开机次数统计、磨损均衡设计时会非常省心,因为代码里根本不需要处理损耗。
MR25H40CDF 还有一个容易被忽略的优点:它支持字节级随机写,没有页编程的概念。SPI NOR Flash 写数据前必须先擦除整块,小数据频繁修改会很痛苦;而 MRAM 可以直接往任意地址写任意长度,想改一个字节就改一个字节,驱动逻辑简单一大截。
1.2 MR25H40CDF 到底是什么器件
Everspin 的 MR25H40CDF 属于磁阻随机存取存储器,存储单元不是靠浮栅电荷,而是靠磁性隧道结的磁阻状态记录数据。这听起来很玄,实际用起来其实很简单,就是一颗标准 SPI 从机芯片。CDF 这个后缀在我看来可以拆开理解:C 表示 3.3V 供电版本,D 是 DFN8 小封装,F 是无铅环保工艺。当然,最权威的来源还是官方数据手册,我这里只是给大家一个判断方向。
因为存储原理不同,MR25H40CDF 没有 EEPROM 那种“写周期内必须等待”的状态,也不需要像 Flash 那样维护坏块表。它的特性可以总结为:SPI 接口操作、读写对称、无限次写入、断电保持。工业应用中最划算的用法,就是用一颗便宜的 8 位机去控制它,不需要上 Linux、不需要跑文件系统,就能完成参数保存和日志记录。
1.3 为什么主控选 PIC18F45K50
PIC18F45K50 在 Microchip 产品线里算是“小钢炮”:32KB Flash、256 字节片内 EEPROM、3.7KB 左右 SRAM、48MHz 内部振荡器、全速 USB、双 MSSP、双 EUSART、12 位 ADC。长得不算豪华,但做工业数据记录非常够用。
片内那 256 字节 EEPROM 存个序列号和出厂标志还行,存日志完全不够。外挂一颗 MR25H40CDF 之后,相当于给这颗 8 位机配了一个 512KB 的“超级 EEPROM”。PIC18F45K50 的 MSSP 模块可以直接跑硬件 SPI,主模式时钟最高能到 FOSC/4,也就是 48MHz 下约 12MHz。这个速度用来读 MRAM 已经比很多单片机外挂 EEPROM 快一个数量级了。
另外,PIC18F45K50 支持在 MPLAB X 环境里用 XC8 编译器开发,官方还有 MCC 代码生成器,初始化引脚和 SPI 模块几乎不用手写。这对工程师来说很友好,能把精力集中在数据存储逻辑本身。
2. 整体设计:接口定义、存储布局与一致性方案
存储芯片选好只是第一步,真正决定项目成败的是整体设计。这块我先讲硬件接线,再讲存储空间怎么划分,最后重点说说“怎么保证数据一致性”。很多嵌入式工程师把读写接口调通就以为完事了,结果一掉电就丢数据,问题往往就出在整体布局上。
2.1 最小硬件连接与引脚处理
MR25H40CDF 的 SPI 接口引脚不多:CS、SCK、SI、SO,另外还有 WP 和 HOLD。PIC18F45K50 这边用 MSSP1 模块,接法非常简单,示例布局可以这样:
MCU(PIC18F45K50) MR25H40CDF SCK1 -----------> SCK SDO1 -----------> SI SDI1 <----------- SO RB2 GPIO -----------> CS RB3 GPIO -----------> WP 3.3V -----------> VDD 3.3V -----------> HOLD GND -----------> GND这里有两个引脚最容易踩坑,一个是 WP,一个是 HOLD。WP 是写保护,低电平生效,所以要么直接接 3.3V,要么接一个默认拉高的 GPIO。HOLD 是暂停传输,低电平时芯片会忽略 SCK 边沿,如果悬空,工业现场稍微有点干扰就可能把传输卡住,读出来全是 0xFF。我的习惯是 HOLD 直接接 VDD,不给它任何悬空的机会。
供电和滤波方面,MR25H40CDF 是 3.3V 器件,PIC18F45K50 也在 3.3V 下运行最稳。芯片旁边放一个 0.1uF 陶瓷电容,再并一个 4.7uF 左右的钽电容或者铝电解,足以扛住大部分电源毛刺。如果主板上还有 5V 器件,记得做电平转换,不要赌“上拉一下就行”这种事。
关于引脚复用,建议用 MCC 生成初始化代码。PIC18F45K50 这类器件引脚功能往往和模拟输入、PPS 重映射混在一起,手写很容易漏掉某个 ANSEL 或者复用位。MCC 生成之后,你再把 CS、WP 两个 GPIO 补上就行。
2.2 512KB 存储空间怎么划分
MR25H40CDF 有 512KB,对于纯数据记录来说是“中等偏小”的容量,所以不能浪费,也不应该一上来就套一个完整文件系统。我的建议是按区域划分,简单直接:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00000 - 0x000FF | 设备信息区 | 型号、版本、序列号、出厂标志 |
| 0x00100 - 0x001FF | 参数区 | 双备份存储,带 CRC 和提交标记 |
| 0x00200 - 0x003FF | 累计量区 | 开机次数、流量累计等,高频更新 |
| 0x00400 - 0x7FFFF | 日志区 | 环形记录,按固定长度记录追加 |
这个划分有几个好处。设备信息区基本只读一次,参数区需要支持频繁修改且掉电可恢复,累计量区则要承受超高频写入,日志区单独拉出来方便做环形覆盖。每块区域之间留一点空白地址,主要是为了以后扩展字段时不用大面积搬数据。
日志区如果做成定长记录,恢复 tail 指针特别方便。每一条日志可以是“Magic + 时间戳 + 数据 + CRC”,固定 16 字节或者 32 字节。上电扫描时,只要从日志区头部开始,找到最后一个 Magic 正确且 CRC 正确的记录,就能知道当前写到哪了。这个扫描在 12MHz SPI 下读整个日志区大约几百毫秒,对工业设备开机自检来说完全可接受。
2.3 一致性设计:写数据不是“写一下就完事”
MRAM 虽然写寿命无限,也不怕擦除,但“一个逻辑记录包含多个字段”时,依然可能出现写了一半、掉电在中间的尴尬状态。解决办法不是依赖 MRAM 的可靠性,而是靠软件做事务标记。
拿参数区举例,我会申请两份备份:参数 A 和参数 B,每份都带 CRC。写入流程是先把完整参数写到 A,再写一个 commit 标记;如果中途掉电,commit 标记没写成功,上电时只会读到旧的 A,不会读到半新半旧的参数。下一版如果修改参数到 B,再更新 B 的 commit 标记。这样一个简单的双备份机制,能把掉电窗口从“字节级不确定”收敛到“一次提交操作不成功就回退”。
日志记录也一样。先写数据体,最后单独写一个“提交区”的 Magic 值。上电恢复时看到 Magic 才有资格被当作有效记录,否则就认为这条记录未完成。MRAM 的优势是字节写入很快,这个“先写数据再写标记”的流程在 SPI 时钟 4MHz 以上时只多花几十微秒,工业现场完全扛得住。
3. 实操:从 MPLAB 工程到 MRAM 读写程序
接下来是大家最关心的代码部分。我用的是 MPLAB X IDE + XC8 编译器,下面代码基于 PIC18F45K50 的 MSSP1 模块,MRAM 的 CS 和 WP 分别接在 RB2、RB3。不同板子的引脚可能不一样,宏定义改掉就行。
3.1 初始化 SPI 与 GPIO
初始化最关键的是先把 CS 拉高,再把 MSSP1 配成主模式。MR25H40CDF 支持 SPI Mode 0 和 Mode 3,我习惯用 Mode 0,也就是时钟空闲为低、数据在上升沿采样。初始化代码大致是这样:
#include <xc.h> #define MRAM_CS LATBbits.LATB2 #define MRAM_CS_TRIS TRISBbits.TRISB2 #define MRAM_WP LATBbits.LATB3 #define MRAM_WP_TRIS TRISBbits.TRISB3 void MRAM_Init(void) { // 先停用 MSSP1,避免配置过程中出现多余时钟边沿 SSP1CON1 = 0x00; // CS、WP 配置为输出,默认拉高 MRAM_CS_TRIS = 0; MRAM_WP_TRIS = 0; MRAM_CS = 1; MRAM_WP = 1; // 如果对应引脚有模拟功能,记得关模拟输入 // ANSELB &= ~0x0C; // 按实际引脚调整 // MSSP1 作为 SPI 主模式,时钟 FOSC/4,SPI Mode 0 SSP1STAT = 0x40; // CKE=1,配合 CKP=0 就是 Mode 0 SSP1CON1 = 0x20; // SSPEN=1,CKP=0,SSPM=0000 }这里有一点要提醒:FOSC/4在 48MHz 系统时钟下是 12MHz。MRAM 本身通常能支持更高时钟,但第一次调板我建议先把时钟降下来,比如配置成FOSC/16或者FOSC/64,确认链路没问题再提速。工业现场的长线连接更容易在高速时钟下出问题,后边排障部分会细说。
SPI 单字节传输函数是核心中的核心:
uint8_t SPI1_Transfer(uint8_t dat) { SSP1BUF = dat; while (!SSP1STATbits.BF) { } return SSP1BUF; }很多新手写这个函数容易少等一个中断标志,导致把下一个字节写进缓冲区时,上一个字节还没移位完。只要用了上面这个循环,每发一字节都等接收缓冲满,就能避免数据错位。
3.2 写使能、单字节读写与突发读写
MR25H40CDF 的指令集和普通 SPI EEPROM 很像,写数据前要先发WRITE ENABLE命令,也就是 0x06。养成“每次写事务前都发 WREN”的习惯,不要只在初始化时发一次,这样最稳妥。
单字节写函数可以这样写:
void MRAM_WriteByte(uint32_t addr, uint8_t dat) { // 先发 WREN MRAM_CS = 0; SPI1_Transfer(0x06); MRAM_CS = 1; // 再发写命令和数据 MRAM_CS = 0; SPI1_Transfer(0x02); // WRITE 命令 SPI1_Transfer((addr >> 16) & 0xFF); // 地址高字节在前 SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); SPI1_Transfer(dat); MRAM_CS = 1; }地址是三个字节,而且是大端在前。我犯过最蠢的错误就是把地址高低字节发反,写进 0x000123 的数,实际落到了 0x230001 这种莫名其妙的位置。排错时先看这三位地址序,能省一小时。
读单字节更简单:
uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t dat; MRAM_CS = 0; SPI1_Transfer(0x03); // READ 命令 SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); dat = SPI1_Transfer(0x00); // 随便发一个字节,把数据读回来 MRAM_CS = 1; return dat; }单字节读写适合存参数,但如果要写日志,最好用突发读写。突发写就是在 CS 拉低之后,连续发送多个数据字节,不需要每写一个字节就重新拉高 CS:
void MRAM_WriteBurst(uint32_t addr, const uint8_t *buf, uint16_t len) { MRAM_CS = 0; SPI1_Transfer(0x06); MRAM_CS = 1; MRAM_CS = 0; SPI1_Transfer(0x02); SPI1_Transfer((addr >> 16) & 0xFF); SPI1_Transfer((addr >> 8) & 0xFF); SPI1_Transfer(addr & 0xFF); while (len--) { SPI1_Transfer(*buf++); } MRAM_CS = 1; }突发读同理,命令后面跟着连续读字节。要注意地址到达容量边界之后的行为,不同器件可能回卷,也可能停在边界,最好查一下数据手册再决定要不要在驱动里做地址保护。
3.3 数据校验:CRC16 是低成本安全感
MRAM 本身很可靠,但 SPI 链路、PCB 走线、电源噪声都可能造成偶发错误。工业应用里我不会裸写裸读,至少要在每条日志尾部加 2 字节 CRC16。校验算法不用追求高大上,用 CRC-16/MODBUS 这种反射多项式就够了:
uint16_t CRC16_Update(uint16_t crc, uint8_t b) { crc ^= b; for (uint8_t i = 0; i < 8; i++) { if (crc & 1) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } return crc; }写入时从初始值 0xFFFF 开始,对整条记录逐字节更新 CRC;读回时同样从头算一遍,和记录尾部存的 CRC 比较。如果不等,这条记录就不要用了。这个函数占不了几个字节 Flash,但对整个系统的可靠性提升非常明显。
3.4 日志追加与上电恢复的思路
日志追加可以写成这样一组操作:
- 在 RAM 里组装一条定长记录,包括 Magic、时间戳、业务数据和 CRC16;
- 调用
MRAM_WriteBurst把记录写到日志区当前 tail; - 单独写一个 2 字节 commit 标记,比如固定写成
0x55 0xAA; - 将 tail 指针向前移动一个记录长度,如果越过日志区末尾就回绕到起点。
上电恢复时,从日志区起点开始按定长扫描,检查 Magic 和 CRC。遇到第一条 Mark 不对的记录,说明上一次写到一半掉电了,从这条往后的记录都无效,tail 就设在这里。这样即使最后一条记录写了一半,也不会被当作有效数据。
有人可能觉得先写数据再写 commit 标记有点多余。实际在掉电瞬间,SPI 正在传输的数据可能只写了一部分,如果不加标记,上电扫描会看到一条“Magic 正确但数据残缺”的记录,误判为有效。加 commit 标记之后,判断条件从一条变成两条,安全性完全不一样。
4. 调试现场:我踩过的问题和排查方法
再稳定的方案,到了现场也会出各种奇奇怪怪的问题。这一节我把实际调试中碰到的高频问题整理成速查表,按现象排查比较快。
4.1 读出来全是 0xFF 或 0x00
这是 SPI 调试最容易遇到的现象。读出来全是 0xFF,大概率是引脚悬空、CS 没有正常工作,或者 MRAM 一直处于 HOLD 状态。读出来全是 0x00,通常是 MISO 没接对,或者 SCK 根本没跑起来。
我用逻辑分析仪抓过一波时序,发现最典型的原因是板子上 SCK 被复用到了别的外设引脚,MCC 生成的代码没覆盖到位。所以遇到全 0xFF,先不要怀疑芯片坏了,拿逻辑分析仪量 CS 和 SCK,看看有没有命令帧发出去。再不行直接把 SPI 时钟降到 1MHz,排除高速信号完整性问题。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 全 0xFF | HOLD 悬空、CS 未拉低、SPI 模式不对 | 检查 HOLD/WP 电平,逻辑分析仪看时序 |
| 全 0x00 | MISO 接错、SCK 没输出、芯片供电异常 | 核对引脚映射,量 VDD,示波器看 SCK |
| 数据错位 | SPI Mode 0/3 不匹配、地址字节序错误 | 统一模式,按手册确认地址字节序 |
4.2 写完读回还是旧值
这种问题在 EEPROM 上常见,在 MRAM 上出现时,先怀疑是不是根本没写进去。MR25H40CDF 写数据前必须发 WREN,如果漏了,写命令会被忽略。WP 引脚被拉低也会让整个芯片写保护,这时候读正常,写无效。
还有一个容易被忽略的点:写时序过程中 CS 不能提前拉高。比如你发完地址后,数据字节还没发完就把 CS 拉高了,这次写事务就中止了,芯片不会有任何提示。代码里最好在突发写函数结束前只拉一次 CS,别在中间自作聪明加延时或者拉高再拉低。
更稳的做法是写完立刻回读。调试阶段可以写一个MRAM_WriteThenVerify函数:先写,再读,然后比较。这个验证动作虽然让每次写入多花一点时间,但能让所有“假写入”问题立刻暴露。
4.3 掉电瞬间丢最后一条记录
这是工业项目里真正要命的场景。MRAM 本身不掉电不丢数据,但如果 PIC 在掉电瞬间还在写,而供电已经跌到芯片最低工作电压以下,那么这个写事务的最终状态就不确定了。这不是 MRAM 缺陷,而是系统电源设计问题。
我的做法有两个:一是打开 PIC18F45K50 的欠压复位功能,把 BOR 检测作为“即将掉电”的信号;二是在电源入口放足够大的电容,让 MCU 在 BOR 触发后还能稳定跑几百微秒,把最后一条紧急记录写完。别小看这几百微秒,在 SPI 1MHz 时钟下,写 16 字节大约只需要一百多微秒,完全来得及。
如果现场连电容都不愿加,那就只能在软件层做兜底:写数据之前先算好 CRC,再写 CRC,最后写 commit 标记。掉电后上电扫描时,如果 commit 标记没出现,就放弃这条记录。这样最多丢最后一条,但不会把一条坏记录当真。
4.4 调试工具和习惯建议
我调试这类 SPI 存储设备,必备三样东西:逻辑分析仪、示波器、串口打印。逻辑分析仪抓 SPI 时序,示波器看电源纹波和信号边沿,串口打印则是把 MCU 内部的状态变量实时吐出来。
特别建议把SPI1_Transfer里等待超时的逻辑加进去。比如循环超过 10000 次还没等到 BF 就强制退出并置错误标志,这样至少不会让整个系统卡死在读 MRAM 上。工业设备最忌讳一个外设坏了把整机拖死。
5. 这套方案适合谁,不适合谁
MR25H40CDF 加 PIC18F45K50 的组合,我认为最合适的是中低成本的工业控制板、仪器仪表、环境监测设备和 USB 接口的数据采集模块。这类设备不需要跑操作系统,不需要大容量多媒体存储,但对数据完整性和写入频率有刚性要求。MRAM 在这里的定位就是把 EEPROM 的短板补上,同时不给主控增加负担。
更适合的典型场景包括:
- 设备参数频繁保存,比如触摸屏界面每改一个数就往 MRAM 里写一份备份;
- 故障录波和事件记录,掉电前要把最近几百毫秒的原始数据存下来;
- 累计量、开机次数这类高频计数器,不用考虑磨损均衡;
- 需要替代“电池供电的 SRAM”,省掉换电池的维护工作。
不太适合的场景也明显:如果你要存 4MB 固件升级包,或者存大量图片、录音文件,那应该用 SPI NOR Flash 加文件系统,而不是 MRAM。MRAM 虽然读写快,但单位成本比 Flash 高,大容量数据存储上并不划算。另外如果产品生命周期里写入频率极低,一年都写不了几次,一颗 SPI EEPROM 可能十块钱以内就搞定了,没必要为了“无限寿命”多花几倍成本。按我个人习惯,这种存储方案能不能上板,最核心就看两条:写入频率高不高,断电数据可不可丢。只要占了一条,MRAM 就比 EEPROM 或 Flash 更省心;如果两条都不占,普通 SPI EEPROM 更务实,没必要硬上。
最后再分享一个小技巧。MR25H40CDF 上电后的状态寄存器默认值不一定是你需要的,调试时可以在初始化最后读一次状态字,通过串口打印出来。这样既验证了 SPI 通信,又能确认芯片不是处于意外写保护或 HOLD 状态。把这行测试代码留在工程里,以后现场排障会省很多事。