1. 项目缘起与方案选型:为什么是 MR25H40CDF 加 PIC18F56K42
工业现场的数据记录仪、智能电表、PLC 扩展模块、车载黑匣子这类设备,对“存数据”这件事的要求跟消费电子完全不是一个量级。消费电子丢一帧日志无所谓,工业设备丢一条校准参数或者一段故障录波,可能就是一次停机事故。我这些年做过的嵌入式存储方案里,EEPROM 擦写寿命不够、Flash 写入前必须整块擦除、掉电瞬间正在写的扇区直接变砖,这些问题几乎每隔一段时间就会冒出来一次。直到把MR25H40CDF这颗 MRAM 芯片用进项目,才算真正把“频繁写、随时断电、数据不能丢”这三个需求同时按住。
MR25H40CDF 是 Everspin 出的 4Mbit 串行 MRAM,走标准SPI接口,512K 字节容量,40MHz 时钟,最关键的一点是:它的写入不需要擦除,字节级随机写,写一次就是一次,没有“先擦后写”这个中间状态。这一点跟传统 SPI Flash(比如 W25Q64)有本质区别。Flash 写之前必须把整个扇区擦成 0xFF,擦除期间如果掉电,整个扇区数据全废;MRAM 是磁隧道结存储,写操作本身就是原子性的,掉电最多丢当前正在写的那一个字节,之前的数据完好无损。对于工业场景里那种“每秒钟记一次温度、一断电就要保住最后一条”的需求,这个特性直接决定了方案能不能落地。
主控这边选PIC18F56K42,理由也很实在。这颗 8 位 MCU 带 64KB Flash、4KB RAM,内置硬件 SPI 模块(MSSP),主频跑到 64MHz,关键是它的工作温度范围覆盖 -40 到 125 摄氏度,符合工业级要求。很多新手会问,为什么不用 STM32?STM32 当然可以,但 PIC18F56K42 在成本敏感、引脚受限、又要求宽温的工业小板子上非常能打,而且 Microchip 的 MCC(MPLAB Code Configurator)图形化配置工具生成 SPI 初始化代码非常省事,寄存器配置不容易出错。我手上这个项目就是一块 30mm×40mm 的四层板,主控加 MRAM 加一颗 LDO,BOM 成本压得很低。
把这两颗芯片配在一起,核心思路就是:用硬件 SPI 做高速数据搬运,用 MRAM 做掉电安全的非易失存储,用 MCU 的 DMA 或者中断加缓冲的方式解决写入实时性问题。下面我会把整个方案从硬件连接到软件驱动,再到实际调试踩过的坑,完整拆一遍。
2. 硬件连接与 SPI 模式配置:别在第一步就翻车
2.1 引脚连接与片选策略
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很清晰:VDD、VSS、SCK、SI(MOSI)、SO(MISO)、CS#、WP#、HOLD#。跟 PIC18F56K42 连接时,我用的引脚分配是这样的:
| MR25H40CDF 引脚 | PIC18F56K42 引脚 | 说明 |
|---|---|---|
| VDD | 3.3V | 供电,建议加 0.1uF 去耦 |
| VSS | GND | 地 |
| SCK | RC3/SCK | 硬件 SPI 时钟 |
| SI | RC5/SDI | 主出从入 |
| SO | RC4/SDO | 主入从出 |
| CS# | RA5 | 软件片选,普通 GPIO |
| WP# | 3.3V | 写保护,不用就拉高 |
| HOLD# | 3.3V | 保持,不用就拉高 |
这里有个细节值得说:CS# 我坚持用软件片选,不用硬件 SPI 的 SS 引脚。原因是在多从机系统里,硬件片选有时候会因为时序问题出现“片选还没拉低时钟就来了”的情况,尤其是 PIC 的 MSSP 模块在某些配置下 SS 引脚行为不太可控。用普通 GPIO 手动控制 CS#,拉低之后延时几个纳秒再发时钟,时序上更稳。代价就是每次读写前后要手动操作 GPIO,代码稍微多两行,但换来的是稳定性,值。
注意:WP# 和 HOLD# 这两个引脚千万不要悬空。悬空状态下引脚电平不确定,可能导致芯片进入写保护或者保持状态,表现为“读出来全是 0xFF”或者“写进去读不出来”。我见过至少三个新手栽在这个上面,查了半天以为是驱动问题,结果就是两个引脚没接。
2.2 SPI 模式选择:Mode 0 还是 Mode 3
MR25H40CDF 支持 SPI Mode 0(CPOL=0,CPHA=0)和 Mode 3(CPOL=1,CPHA=1)。这两个模式的区别在于时钟空闲电平和采样边沿。Mode 0 是时钟空闲低电平,上升沿采样;Mode 3 是时钟空闲高电平,下降沿采样。
我实测下来,Mode 0 在 PIC18F56K42 上更稳。原因是 PIC 的 MSSP 模块在 Mode 0 下,SCK 空闲电平默认就是低,配置起来最自然。如果你选 Mode 3,需要额外注意 CKP 位的设置,而且有些板子上如果 SCK 走线比较长,空闲高电平容易受到干扰。当然,如果你的 PCB 布局已经定了,Mode 3 也能用,只是初始化的时候要把 SSPCON1 的 CKP 位置 1。
配置代码大概长这样(基于 MCC 生成的框架改的):
// SPI 初始化,Mode 0,时钟 = Fosc/4 SSP1CON1 = 0x00; // 先复位 MSSP SSP1STAT = 0x00; // 采样中间,Mode 0 SSP1CON1bits.SSPM = 0b0000; // SPI Master, Fosc/4 SSP1CON1bits.CKP = 0; // 空闲低电平 SSP1CON3bits.BOEN = 1; // 允许溢出检测 SSP1CON1bits.SSPEN = 1; // 使能 MSSP时钟频率这块,PIC18F56K42 跑 64MHz 时,Fosc/4 就是 16MHz,MR25H40CDF 最高支持 40MHz,完全够用。如果你需要更快,可以改成 Fosc/16 或者用 SPI 时钟分频,但 16MHz 对于大多数工业数据记录场景已经绰绰有余。我实测 16MHz 下连续写 512 字节,耗时不到 300 微秒。
2.3 电源与去耦:小细节决定稳定性
MRAM 虽然不像 Flash 那样对电源纹波敏感,但工业现场电源质量参差不齐,去耦电容还是不能省。我的做法是在 MR25H40CDF 的 VDD 和 VSS 之间放一颗 0.1uF 的 X7R 陶瓷电容,位置尽量靠近芯片引脚,走线短而粗。另外整板电源入口再放一颗 10uF 的钽电容做储能,防止掉电瞬间电压跌落太快导致 MRAM 写入不完整。
这里有个经验:如果你做的是掉电记录场景,建议在电源入口加一颗大容量电容(比如 100uF 以上),给 MCU 和 MRAM 争取几十毫秒的“续命时间”。这几十毫秒足够 MCU 检测到掉电中断,把最后一条关键数据写进 MRAM,然后安全关机。我有个项目就是靠这个 100uF 电容,在断电后还能完整写入 64 字节的故障快照。
3. MRAM 驱动开发:从命令字到页写操作
3.1 命令集解析与基本读写时序
MR25H40CDF 的命令集跟标准 SPI Flash 很像,但少了擦除相关的命令,多了几个 MRAM 特有的。常用命令如下:
| 命令名称 | 命令字(十六进制) | 功能说明 |
|---|---|---|
| WREN | 0x06 | 写使能,写之前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
注意,MRAM 没有扇区擦除命令(0x20、0xD8 这些都没有),因为不需要。写操作直接发 WRITE 命令,后面跟 24 位地址,再跟数据就行。地址是 24 位的,因为 512K 字节需要 19 位地址,但芯片协议规定用 3 个字节传输地址,高位补零。
读时序很简单:拉低 CS#,发 0x03,发 3 字节地址,然后连续读数据,每读一个字节时钟走 8 个周期。写时序稍微复杂一点:拉低 CS#,先发 0x06(WREN),拉高 CS#,再拉低 CS#,发 0x02,发 3 字节地址,然后连续写数据。WREN 必须单独一个 CS# 周期,不能跟 WRITE 命令在同一个片选周期里连着发,这是很多新手容易搞错的地方。
3.2 字节写、页写与连续写的取舍
MR25H40CDF 支持两种写模式:字节写和页写。页大小是 256 字节。字节写就是每次写一个字节,地址自动加一;页写是一次性写最多 256 字节,地址在页内回绕。
实际项目中怎么选?我的经验是:小数据量、随机地址用字节写;大数据量、连续地址用页写。比如你要存一条 32 字节的结构体,字节写就够;如果要存 4KB 的日志块,用页写循环 16 次,每次 256 字节,效率高很多。
这里有个关键点:MRAM 的页写没有“页边界回绕”问题。Flash 页写如果跨页了,地址会回绕到页首覆盖之前的数据,MRAM 不会,它地址是线性递增的,写满 256 字节后继续写下一个地址,只要不超过芯片容量就行。这个特性让驱动写起来更简单,不用像 Flash 那样小心翼翼地处理跨页。
我封装的写函数大概是这样:
void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t addrH = (addr >> 16) & 0xFF; uint8_t addrM = (addr >> 8) & 0xFF; uint8_t addrL = addr & 0xFF; CS_LOW(); SPI_Exchange(0x06); // WREN CS_HIGH(); CS_LOW(); SPI_Exchange(0x02); // WRITE SPI_Exchange(addrH); SPI_Exchange(addrM); SPI_Exchange(addrL); for (uint16_t i = 0; i < len; i++) { SPI_Exchange(buf[i]); } CS_HIGH(); }读函数更简单,把 0x02 换成 0x03,然后把 SPI_Exchange 的返回值存到 buf 里就行。
3.3 状态寄存器与写保护机制
MR25H40CDF 的状态寄存器只有几位有效:WIP(写进行中)、WEL(写使能锁存)、BP0 和 BP1(块保护)、SRWD(状态寄存器写保护)。WIP 位在写操作期间为 1,写完自动清零。你可以轮询 WIP 位来判断写是否完成,但 MRAM 写速度极快,通常几十纳秒就完成了,轮询反而浪费时间。我的做法是写完直接发下一条命令,不用等。
块保护位 BP0 和 BP1 可以保护部分地址区域不被写入,这个功能在工业场景里很有用。比如你把校准参数存在 0x00000 到 0x00FFF,把日志存在 0x01000 往后,就可以用块保护把参数区锁住,防止程序跑飞误写。配置方法就是发 WRSR 命令写状态寄存器,具体位定义查数据手册第 8 页,这里不展开。
提示:如果你不需要写保护,上电后先读一次状态寄存器,确认 BP0 和 BP1 都是 0。有些批次的芯片出厂时块保护位可能不是默认值,我遇到过一批 BP0 被置 1 的情况,导致前半段地址写不进去,查了两天才发现是状态寄存器的问题。
4. 掉电安全写入策略:工业场景的核心命门
4.1 掉电检测与中断响应
工业设备最怕的就是“正在写数据的时候断电”。MRAM 虽然写入原子性强,但如果 MCU 在发 WRITE 命令的过程中断电,命令可能只发了一半,MRAM 收到不完整的命令会忽略,数据不会损坏,但这次写入就丢了。所以掉电检测电路和中断响应速度很关键。
我的方案是在电源入口分压出一路电压,接到 PIC18F56K42 的 ADC 引脚,同时用比较器模块(CMP)做阈值检测。正常工作时 3.3V 分压到 1.65V,当电压跌到 2.8V 时,分压值跌到 1.4V,比较器触发中断。中断服务程序里立刻把关键数据写进 MRAM,然后关闭外设,进入休眠。
这里有个时间账要算:从 3.3V 跌到 2.8V,如果电源入口有 100uF 电容,负载电流 20mA,那么时间 t = C × ΔV / I = 100uF × 0.5V / 20mA = 2.5ms。2.5ms 足够 MCU 执行几百条指令,写 64 字节数据完全没问题。但如果你电容只有 10uF,时间就只有 250 微秒,写 64 字节可能刚好够,写 256 字节就悬了。所以电容容量要根据你要写的数据量来算,不能拍脑袋。
4.2 双缓冲与版本号机制
光有掉电检测还不够,万一中断响应晚了,或者数据写了一半,读出来就是脏数据。我的做法是在 MRAM 里划两个区域做双缓冲,每个区域前面加一个 4 字节的头部:2 字节版本号 + 2 字节 CRC 校验。写数据的时候,先写缓冲区 B,写完更新版本号和 CRC,然后再写缓冲区 A。读数据的时候,比较两个缓冲区的版本号,取版本号大的那个,同时校验 CRC,CRC 不对就取另一个。
这个机制的好处是:即使掉电发生在写缓冲区 B 的过程中,缓冲区 A 的数据还是完整的,下次上电读的时候发现 B 的 CRC 不对,自动回退到 A。代价就是存储空间翻倍,但对于 512K 字节的 MRAM 来说,存关键参数用不了多少空间,翻倍也无所谓。
版本号我建议用 32 位无符号数,每次写入加一,溢出后回绕到 0。比较的时候用“差值取模”的方式判断新旧,不要直接比大小,否则回绕后会出错。具体做法是:如果 (new_ver - old_ver) 在 0 到 0x7FFFFFFF 之间,认为 new_ver 更新;否则认为 old_ver 更新。
4.3 写入频率与寿命考量
MRAM 的写入寿命是无限的,官方数据手册写的是“无写入次数限制”,这跟 Flash 的 10 万次擦写寿命完全不是一个概念。所以你可以放心地每秒写、每毫秒写,不用担心寿命问题。但有一个隐性成本:写入功耗。MRAM 写一个字节的功耗比读高不少,如果你用电池供电,频繁写入会显著缩短续航。
我的建议是:关键数据实时写,日志数据缓冲写。比如故障标志、校准参数这种,一有变化立刻写 MRAM;温度日志这种,先在 RAM 里攒够 64 字节或者 1 秒写一次,减少写入次数。实测下来,每秒写一次 64 字节,平均电流增加不到 1mA,对续航影响很小。
5. 实际调试中踩过的坑与排查技巧
5.1 读出来全是 0xFF 或 0x00
这是最常见的症状,原因通常有三个:片选没拉低、SPI 模式不对、WP# 或 HOLD# 悬空。排查顺序我建议这样:先用示波器看 CS# 有没有拉低,再看 SCK 有没有波形,再看 MOSI 数据对不对。如果波形都对但读出来还是 0xFF,检查 WP# 和 HOLD# 是不是接了高电平。如果读出来全是 0x00,检查 MISO 线有没有接反,或者 MRAM 的 SO 引脚有没有虚焊。
我遇到过一次特别诡异的:读出来数据偶尔对偶尔错,查了半天发现是 SCK 走线太长,跟旁边一根 PWM 线产生了串扰。解决办法是在 SCK 线上串一颗 22 欧姆的电阻,减缓边沿速率,问题就消失了。所以PCB 布局时 SPI 走线尽量短,远离高频开关信号,这个经验值 22 欧姆电阻我后来在好几个项目里都用上了。
5.2 写入后读出来数据不对
写入成功但读出来不对,通常是地址算错了。MR25H40CDF 的地址是 24 位的,但实际容量只有 512K 字节,也就是 0x00000 到 0x7FFFF。如果你写的地址超过 0x7FFFF,芯片会回绕到 0x00000,覆盖之前的数据。我见过一个项目,地址变量用了 32 位,但传给 MRAM 的时候只取了低 24 位,结果地址超过 16MB 后回绕,数据全乱了。地址变量建议用 uint32_t,但传给 MRAM 前要确保不超过 0x7FFFF。
另一个原因是 WREN 命令没发或者发错了。每次写之前必须发 WREN,而且 WREN 和 WRITE 之间不能有其他命令。如果你在 WREN 之后插了一个读状态寄存器的操作,WEL 位可能会被清除,导致 WRITE 被忽略。这个坑我在早期调试时踩过,后来养成习惯:WREN 之后立刻发 WRITE,中间不插任何操作。
5.3 SPI 时钟太快导致数据出错
PIC18F56K42 的 SPI 时钟可以配到 16MHz,MR25H40CDF 支持 40MHz,理论上没问题。但实际调试时,如果 PCB 走线质量一般,16MHz 下可能出现数据位采样错误。症状是读出来的数据偶尔某一位翻转,比如 0x55 变成 0x54。解决办法有两个:一是降低 SPI 时钟到 8MHz 或 4MHz,二是优化 PCB 走线,加地线屏蔽。
我一般会在驱动里加一个“SPI 速度可配置”的宏,调试阶段用低速,确认功能正常后再逐步提高。这样排查问题的时候可以快速排除时钟因素。实测下来,四层板、走线长度小于 5cm 的情况下,16MHz 完全稳定;如果是两层板、走线超过 10cm,建议降到 8MHz 以下。
5.4 常见问题速查表
| 症状 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读全 0xFF | CS# 未拉低、WP#/HOLD# 悬空 | 示波器看 CS# 和 WP# 电平 | 接好片选,WP#/HOLD# 拉高 |
| 读全 0x00 | MISO 接反、芯片未供电 | 万用表测 VDD 和 MISO 通断 | 检查焊接和供电 |
| 写入无效 | WREN 未发或 WEL 被清 | 读状态寄存器看 WEL 位 | WREN 后立刻 WRITE |
| 数据偶发错误 | SPI 时钟太快、串扰 | 降低时钟测试 | 降频或加串阻 |
| 地址回绕 | 地址超过 0x7FFFF | 检查地址变量范围 | 限制地址在 0x00000-0x7FFFF |
| 掉电丢数据 | 电容不够、中断太慢 | 测掉电到写入完成时间 | 加大电容,优化中断 |
6. 性能实测与优化建议
6.1 读写速度实测数据
我在 16MHz SPI 时钟下做了几组实测,数据如下:
| 操作 | 数据量 | 耗时 | 平均速率 |
|---|---|---|---|
| 连续读 | 512 字节 | 260 微秒 | 约 1.97 MB/s |
| 连续写 | 512 字节 | 290 微秒 | 约 1.76 MB/s |
| 单字节写 | 1 字节 | 8 微秒 | 约 125 KB/s |
| 页写 | 256 字节 | 145 微秒 | 约 1.76 MB/s |
可以看到,连续读写速率接近 2MB/s,对于工业数据记录完全够用。单字节写因为每次都要发 WREN 和 WRITE 命令,开销大,速率低很多。所以能批量写就不要单字节写,这是提升效率最直接的办法。
6.2 DMA 搬运与中断优化
PIC18F56K42 没有 DMA,但可以用 SPI 中断加缓冲的方式减少 CPU 占用。我的做法是定义一个 256 字节的发送缓冲区和接收缓冲区,SPI 发送中断里自动填充下一个字节,接收中断里把数据存到缓冲区。这样 CPU 只需要在开始的时候启动传输,传输过程中可以去处理其他任务,传输完成后再回来处理数据。
如果你用的是带 DMA 的 MCU(比如 STM32 或者 PIC32),那就更简单了,直接配置 DMA 通道搬运 SPI 数据,CPU 完全不参与。不过对于 PIC18F56K42 这种 8 位机,中断加缓冲已经够用了,实测 CPU 占用率从 100% 降到 30% 左右。
6.3 低功耗场景下的写入策略
电池供电的工业传感器,低功耗是硬指标。MRAM 的待机电流只有几微安,但写入电流有十几毫安。我的策略是:平时让 MRAM 处于待机状态(CS# 拉高),需要写的时候才拉低 CS#,写完立刻拉高。这样平均功耗可以控制在几十微安级别。
另外,如果你不需要频繁写,可以把数据攒在 RAM 里,等攒够一定量或者定时器触发后再一次性写入。比如每 10 秒写一次,每次写 256 字节,平均写入电流就降到 1mA 以下。这个策略我在一个无线温度传感器项目里用过,两节 AA 电池撑了两年多。
7. 方案扩展与个人经验总结
这套 MR25H40CDF 加 PIC18F56K42 的方案,我已经在三个量产项目里用过了,分别是工业数据记录仪、智能电表扩展模块和车载故障记录器。最长的已经连续运行两年多,没有出现过数据丢失或者芯片损坏的情况。MRAM 的可靠性确实比 Flash 和 EEPROM 高一个档次,虽然单价贵一些,但在工业场景里,数据比芯片值钱。
如果你要扩展这个方案,有几个方向可以考虑:一是换更大容量的 MRAM,比如 MR25H40CDF 的 8Mbit 版本,引脚兼容,驱动不用改;二是加一颗实时时钟芯片,给每条数据打时间戳;三是用文件系统(比如 LittleFS)管理 MRAM,方便存储变长数据。不过文件系统会带来额外的擦写开销,MRAM 虽然不怕擦写,但文件系统的元数据更新会降低有效写入速率,这个要权衡。
最后分享一个我在调试时总结的小技巧:每次上电后先往 MRAM 的最后一个地址写一个已知值,然后读回来对比,确认读写通路正常后再开始正式操作。这个自检步骤只需要几微秒,但能提前发现 90% 的硬件问题,避免程序跑起来之后才发现存储不可用。我在所有项目里都保留了这个自检逻辑,省了很多现场排查的时间。