1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18F46K20
1.1 一个真实的需求场景
前阵子接了个工业数据采集终端的活儿,客户的要求很朴素:设备装在配电柜里,每天要记录几百条运行参数,断电不能丢,现场电磁环境还特别脏。一开始我下意识想上 SPI Flash,比如 W25Q 系列,便宜、容量大、资料多,几乎是嵌入式存储的默认答案。但仔细一算账,问题就来了。
这个终端每 200ms 就要写一次数据,一天下来是 43 万次写操作。SPI Flash 的擦写寿命通常在 10 万次左右,就算做了磨损均衡,一块 8MB 的 Flash 也撑不了太久,而且 Flash 写入前必须先擦除整个扇区,写入延迟动辄几毫秒到几十毫秒,在掉电瞬间极容易丢数据。客户还要求“写入即持久”,不能有缓存窗口。这时候 Flash 的短板就暴露得很彻底了。
于是我把目光转向了 MRAM。MR25H40CDF 是 Everspin 家的 4Mbit SPI 磁阻随机存储器,它的核心卖点正好戳中这个场景:写入不需要擦除、字节级随机写、写入延迟纳秒级、擦写寿命理论上无限(官方标称 10^15 次以上)、断电即保存。说白了,它用起来像 SRAM,但断电不丢数据,这就很香了。
主控这边选 PIC18F46K20,理由也很实在。这颗 8 位 MCU 是 Microchip 的经典款,64KB Flash、3936 字节 RAM、64KB 寻址空间,自带 MSSP 模块可以硬件跑 SPI,工作温度覆盖工业级 -40 到 +85 摄氏度,抗干扰能力在 8 位机里属于第一梯队。关键是它便宜、好买、开发工具链成熟,用 XC8 编译器加 MPLAB X 就能干活,不需要上 Linux 那套重装备。对于这种数据量不大、实时性要求高、成本敏感的工业小终端,PIC18F46K20 配 MR25H40CDF 是一对很务实的组合。
1.2 为什么不用 EEPROM 或者 FRAM
有朋友会问,那为啥不用 EEPROM 或者 FRAM?我确实都评估过。EEPROM 的问题是写入速度太慢,字节写典型 5ms,页写也要 3ms 左右,而且寿命同样只有百万次量级,高频写入场景下依然会磨损。FRAM 倒是快、寿命也高,但容量普遍偏小、价格偏高,而且很多型号的 SPI 时序在高频下对 PCB 布线要求更苛刻。MRAM 在速度、寿命、容量、成本之间找到了一个比较舒服的平衡点,尤其是 4Mbit 这个容量,对于存参数、日志、配置表来说刚刚好。
还有一个容易被忽略的点:MR25H40CDF 的 SPI 接口支持最高 40MHz 时钟,而 PIC18F46K20 的 MSSP 模块在 64MHz 系统时钟下最高能跑到 16MHz SPI 速率。虽然跑不满 MRAM 的极限,但 16MHz 已经足够让读写操作在微秒级完成,对实时性帮助巨大。这个速率匹配关系在选型时就要算清楚,不然买回来发现 MCU 拖后腿就尴尬了。
1.3 整体架构长什么样
整个方案的结构其实很清晰:PIC18F46K20 作为主控,通过 MSSP 模块的 SPI 接口连接 MR25H40CDF,另外用几个普通 GPIO 做片选和写保护控制。MRAM 负责存储掉电不能丢的关键数据,MCU 内部的 3936 字节 RAM 作为运行时缓存,Flash 用来放程序代码和常量表。数据流是这样的:传感器采集到数据后先写进 MCU 的 RAM 缓冲区,攒够一批或者到达定时点,再通过 SPI 批量写入 MRAM。读取的时候反过来,从 MRAM 读回 RAM 再处理。
这个架构的好处是分工明确,MRAM 只干它最擅长的事——高频、可靠、掉电安全的存储,不承担程序存储的职责。PIC18F46K20 的 64KB Flash 放代码绰绰有余,不需要外部程序存储器,省了一颗芯片和一堆布线。对于工业现场这种“稳定压倒一切”的场景,越简单的架构越不容易出问题。
2. 硬件连接与 SPI 时序细节:别在布线上栽跟头
2.1 引脚连接与片选策略
MR25H40CDF 是标准的 8 脚 SOIC 封装,引脚定义很清爽:VDD、VSS、SCK、SI、SO、CS、WP、HOLD。和 PIC18F46K20 连接时,SCK 接 RC3,SI 接 RC5,SO 接 RC4,这三个是 MSSP 模块的固定 SPI 引脚,不能随便换。CS 片选我接在 RA5 上,用普通 GPIO 控制。WP 写保护和 HOLD 保持引脚我接在 RA4 和 RA3 上,也当普通 IO 用。
这里有个细节值得说:MR25H40CDF 的 CS 必须由主机控制,而且每次 SPI 事务开始前拉低、结束后拉高。有些朋友图省事想用硬件片选自动管理,但 PIC18F46K20 的 MSSP 模块在 SPI 主模式下并不自动控制 CS,还是得手动操作 GPIO。手动片选虽然多几行代码,但控制更灵活,尤其是需要在一次 CS 有效期内连续发送命令加地址加数据的时候,手动模式反而更顺手。
WP 引脚我建议也接上 GPIO 控制,不要直接接地或者接 VDD。为什么?因为 MRAM 的写保护和 Flash 不一样,它是通过状态寄存器控制的,WP 引脚只是硬件层面的额外保护。在调试阶段,你可能需要临时解除保护来写数据,如果 WP 直接焊死接地,那就只能靠软件改状态寄存器,少了一层保险。接 GPIO 的话,软件和硬件双重控制,调试和量产都方便。
2.2 SPI 模式选择:Mode 0 还是 Mode 3
MR25H40CDF 支持 SPI Mode 0(CPOL=0, CPHA=0)和 Mode 3(CPOL=1, CPHA=1)。这两个模式的区别在于时钟空闲电平和数据采样边沿。Mode 0 下 SCK 空闲为低,数据在上升沿采样;Mode 3 下 SCK 空闲为高,数据在下降沿采样。
我实测下来,PIC18F46K20 的 MSSP 模块在 Mode 0 下最稳。配置寄存器 SSPCON1 的 SSPM 位设为 0b0010 就是 SPI 主模式,CKP 位控制时钟极性,设 0 就是空闲低电平。配合 SSPSTAT 的 CKE 位设 0,表示数据在时钟上升沿从主动输出、下降沿输入采样,正好对应 Mode 0。这个配置在 16MHz 时钟下跑了几万次读写没出过错。
注意:如果你用 Mode 3,CKP 要设 1,CKE 也要相应调整。但 Mode 3 在高频下对 PCB 走线的等长要求更高,因为下降沿采样对时钟抖动更敏感。工业现场电磁干扰大,能选 Mode 0 就别折腾 Mode 3。
2.3 PCB 布线的几个坑
SPI 总线在低速下随便拉线都能跑,但到了 16MHz 就不是闹着玩的了。我踩过的坑主要有三个。
第一个是走线太长。第一版板子为了布局好看,把 MRAM 放在了离 MCU 大概 8cm 的位置,SCK 走线绕了个弯。结果 16MHz 下读写偶尔出错,示波器一看,SCK 上升沿有明显的振铃和过冲。后来把 MRAM 挪到离 MCU 3cm 以内,走线尽量短直,问题就消失了。经验是 SPI 时钟线最好控制在 5cm 以内,超过这个距离就要考虑加串联匹配电阻。
第二个是地线处理。MRAM 的 VSS 和 MCU 的 VSS 必须用粗短线连到同一个地平面,不能各自走细线再汇合。我见过一个案例,MRAM 的地线走了 0.2mm 的细线,结果写入电流一大,地电位就跳,数据偶尔写错。换成 0.5mm 以上的铺铜连接后彻底解决。
第三个是去耦电容。MR25H40CDF 的 VDD 引脚旁边必须放一个 0.1uF 的陶瓷电容,越近越好,最好在 2mm 以内。这个电容不是摆设,MRAM 写入瞬间的电流尖峰全靠它来扛。我试过省掉这个电容,结果就是随机写入失败,查了半天才找到原因。
2.4 上电时序与初始化
MR25H40CDF 的上电要求不复杂,VDD 上升到 2.7V 以上后,等待 1ms 左右就可以发命令了。但 PIC18F46K20 的上电复位时间可能更长,所以实际代码里我在初始化 SPI 之前先加了个 10ms 的延时,确保 MRAM 完全就绪。
初始化流程是这样的:先配置 MCU 的时钟和 IO 方向,然后初始化 MSSP 模块为 SPI 主模式,设置时钟速率,最后拉高 CS 和 WP 引脚,让 MRAM 处于待命状态。这里有个顺序问题:CS 必须在上电后保持高电平,否则 MRAM 可能误进入某种状态。我在代码里把 CS 引脚初始化为输出高电平,放在所有初始化之前,这样最保险。
3. 核心驱动实现:从状态寄存器到读写函数
3.1 MR25H40CDF 的命令集
MR25H40CDF 的命令集很精简,常用的就那么几条。读数据是 0x03,写数据是 0x02,读状态寄存器是 0x05,写状态寄存器是 0x01,写使能是 0x06,写禁止是 0x04。另外还有读标识、写保护相关的一些命令,但日常读写用不到。
每条命令的格式都是:先拉低 CS,发送命令字节,然后跟地址(读写的地址是 24 位,因为 4Mbit 等于 512KB,需要 19 位地址,但 MRAM 用 24 位地址格式),最后是数据。读操作时,地址发完后 MRAM 会在 SCK 的驱动下逐字节输出数据;写操作时,地址发完后主机逐字节发送数据。
这里要特别注意地址格式。MR25H40CDF 的地址是 24 位的,高 5 位是无关位,实际有效的是低 19 位。发送的时候要按高字节、中字节、低字节的顺序发。我一开始没注意,把地址当 16 位发,结果只能访问前 64KB,后面的地址全乱套了。后来查数据手册才发现是 24 位地址,改过来就正常了。
3.2 状态寄存器与写保护
MR25H40CDF 的状态寄存器只有 8 位,但每一位都有用。Bit 7 是状态寄存器写保护位,Bit 6 到 Bit 2 是块保护位,Bit 1 是写使能锁存位,Bit 0 是就绪位。块保护位可以设置哪些地址区域被保护,防止误写。
我的做法是:上电后先读一次状态寄存器,确认就绪位为 1,然后根据应用需求设置块保护。比如配置参数区需要频繁写,就不保护;固件版本区只读,就保护起来。设置完块保护后,再发写使能命令,才能进行写操作。每次写操作前都要发一次写使能,写完后写使能自动清除,这是 MRAM 的安全机制,防止意外写入。
提示:写使能命令和实际的写操作之间不能插入其他命令,否则写使能会失效。我在代码里把这两个操作封装在一个函数里,中间不干别的,确保万无一失。
3.3 底层字节收发函数
PIC18F46K20 的 MSSP 模块收发字节很简单,往 SSPBUF 寄存器写一个字节,然后等 SSPIF 标志位置起,再从 SSPBUF 读回来就是收到的字节。但这里有个坑:如果只发不收,也要读一次 SSPBUF 来清除标志位,否则下一次发送会出错。
我封装的 SPI 收发函数是这样的:先写 SSPBUF,然后 while 循环等 SSPIF,然后读 SSPBUF 返回。这个函数同时完成了发送和接收,对于 MRAM 这种全双工 SPI 设备正好合适。读数据的时候,发送的是哑元字节(比如 0xFF),接收到的才是 MRAM 返回的数据。
代码大概长这样:
unsigned char spi_transfer(unsigned char data) { SSPBUF = data; while (!SSPIF); SSPIF = 0; return SSPBUF; }这个函数是整个驱动的基础,所有 MRAM 操作都建立在它之上。实测在 16MHz SPI 时钟下,一次字节传输大概 0.5us,效率很高。
3.4 页写与连续读的实现
MR25H40CDF 支持连续读写,没有页边界限制。也就是说,你发一个读命令加起始地址,然后可以一直读下去,地址会自动递增,直到 CS 拉高。写操作也一样,发一个写命令加起始地址,然后连续写多个字节,地址自动递增。
这个特性非常好用,因为不需要像 Flash 那样考虑页对齐和跨页问题。我实现了一个mram_write_buf函数,参数是起始地址、数据指针和长度,内部就是拉低 CS、发写使能、发写命令、发 24 位地址、循环发数据、拉高 CS。读函数类似,只是把发数据换成收数据。
这里有个优化技巧:如果连续写多个字节,可以在一次 CS 有效期内完成,不需要每个字节都拉低拉高 CS。这样能显著提高吞吐率。我实测过,批量写 256 字节,一次 CS 完成比每字节一次 CS 快了将近 3 倍。
4. 数据可靠性与掉电保护:工业场景的必修课
4.1 掉电检测与紧急写入
工业现场最怕的就是突然断电。虽然 MRAM 本身写入即持久,但如果 MCU 在发送写命令的过程中断电,数据可能只写了一半。所以掉电检测电路是必须的。
我的做法是在电源输入端加一个电压比较器,当 VDD 降到 4.5V 以下时,比较器输出一个中断信号给 PIC18F46K20 的 INT 引脚。MCU 收到中断后,立即停止正常任务,把 RAM 里还没写入 MRAM 的关键数据紧急写进去。由于 MRAM 写入速度极快,几百字节的数据在几毫秒内就能写完,而电源端的电容能提供足够的维持时间。
这个方案的关键是电容容量要算够。假设系统正常电流 20mA,掉电后需要维持 10ms,电压从 5V 降到 4.0V,那么电容 C = I × t / ΔV = 20mA × 10ms / 1V = 200uF。实际选型时我留了一倍余量,用了 470uF,确保万无一失。
4.2 数据校验与恢复
光有掉电保护还不够,数据写入后还得能验证对错。我在每个数据块后面加了一个 CRC16 校验值,读取的时候先算 CRC 再比对,不一致就说明数据损坏,触发恢复流程。
恢复流程是这样的:MRAM 里维护两个数据区,A 区和 B 区,交替写入。每次写入时先写 A 区,CRC 校验通过后再写 B 区作为备份。读取时如果 A 区 CRC 错误,就自动切到 B 区。如果两个区都错,那就只能上报故障了。这个双区备份方案牺牲了一半容量,但对于关键参数来说,可靠性比容量重要得多。
4.3 写入均衡的考虑
虽然 MRAM 寿命极高,理论上不需要磨损均衡,但在极端高频写入的场景下,我还是建议做一个简单的地址轮转。比如日志存储,不要每次都写同一个地址,而是按块轮转,写满一圈再从头覆盖。这样能把写入压力分散到整个存储空间,进一步延长寿命。
实现起来很简单:维护一个写指针,每次写完递增,到达末尾归零。读取的时候按时间戳或者序号来定位。这个逻辑不复杂,但能有效避免局部过热导致的早期失效。
5. 常见问题与排查实录
5.1 读写失败问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读回全 0xFF | CS 未拉低或时序错误 | 示波器看 CS 和 SCK | 检查 CS 控制代码,确保拉低后才发时钟 |
| 读回全 0x00 | SO 线未接或 MRAM 未供电 | 万用表测 VDD 和 SO 通断 | 检查供电和焊接 |
| 写入后读回不变 | 写使能未发或 WP 保护 | 读状态寄存器确认 | 发写使能命令,检查 WP 引脚电平 |
| 偶尔写入错误 | SPI 时钟太快或布线差 | 降低时钟测试 | 降速到 8MHz,优化布线 |
| 地址错乱 | 地址位数错误 | 检查地址发送代码 | 确认发送 24 位地址 |
| 上电后无响应 | 上电延时不够 | 增加延时测试 | 上电后延时 10ms 再初始化 |
5.2 几个我踩过的坑
第一个坑是 CS 时序。我一开始用硬件 SPI 的自动 CS 功能,结果发现 PIC18F46K20 的 MSSP 在连续传输时 CS 不会自动拉高,导致多个命令连在一起被 MRAM 当成一个长命令。后来改成手动 GPIO 控制 CS,每个事务前后明确拉低拉高,问题解决。
第二个坑是写使能丢失。有一次我在发完写使能后,中间插了一个读状态寄存器的操作,结果写操作失败。查手册才知道,写使能命令后必须紧接着写操作,中间不能有其他命令。这个细节手册里写得很清楚,但容易忽略。
第三个坑是电源纹波。工业现场的 24V 转 5V 电源纹波比较大,导致 MRAM 偶尔写入错误。后来在 MRAM 的 VDD 引脚旁边加了一个 10uF 的钽电容并联 0.1uF 陶瓷电容,纹波从 200mV 降到 50mV 以内,问题消失。
5.3 调试工具与技巧
调试 SPI 设备,示波器是必备的。我一般会同时抓 CS、SCK、SI、SO 四根线,看时序是否匹配。逻辑分析仪更好用,能直接解码 SPI 协议,看到发送的命令和地址对不对。如果没有这些设备,也可以用 MCU 的串口打印调试信息,把读回的状态寄存器值打出来,间接判断问题。
还有一个技巧:先写一个最简单的测试程序,只做一件事——读 MRAM 的标识寄存器。MR25H40CDF 有一个读标识命令(0x9F),会返回厂商 ID 和设备 ID。如果这个能读对,说明硬件连接和基本时序没问题,再往下查软件逻辑。如果读不对,那就是硬件或者底层 SPI 配置的问题。这个分步排查法能省很多时间。
6. 性能实测与优化建议
6.1 读写速度实测数据
我在 16MHz SPI 时钟下做了批量读写测试,结果如下:
| 操作 | 数据量 | 耗时 | 等效速率 |
|---|---|---|---|
| 单字节写 | 1 字节 | 约 2us | 500KB/s |
| 批量写 | 256 字节 | 约 140us | 1.8MB/s |
| 单字节读 | 1 字节 | 约 1.5us | 666KB/s |
| 批量读 | 256 字节 | 约 135us | 1.9MB/s |
批量操作的效率明显高于单字节操作,因为省去了 CS 拉低拉高和命令地址的开销。所以在实际应用中,尽量攒一批数据再写,不要每来一个字节就写一次。
6.2 降低功耗的技巧
MRAM 的写入电流比读取大,典型写入电流 15mA,读取只有 5mA。在电池供电的场景下,要尽量减少写入次数。我的做法是加大 RAM 缓冲区,攒够 512 字节再写一次,这样写入次数降到原来的 1/512,功耗大幅下降。
另外,MRAM 在 CS 高电平时进入待机模式,电流只有几微安。所以在不操作的时候,确保 CS 保持高电平,能省不少电。
6.3 代码优化建议
驱动代码里,SPI 收发函数是调用最频繁的,优化它能提升整体性能。我试过用汇编重写这个函数,把等待 SSPIF 的循环展开,速度能提升 20% 左右。不过对于大多数应用,C 语言版本已经够用了。
另一个优化点是地址计算。如果每次读写都要算 24 位地址,开销不小。我建议把常用地址预计算好,存在变量里,用的时候直接取,避免重复计算。
7. 这个方案还能怎么扩展
这套 MRAM 加 PIC18F46K20 的组合,除了存参数和日志,还能干不少事。比如做数据黑匣子,把设备的运行状态实时记录到 MRAM 里,出故障时读出来分析。MRAM 的无限寿命意味着你可以每秒写几十次,连续写几年都不用担心磨损。
还可以做配置存储,把设备的校准参数、网络配置、用户设置都存在 MRAM 里,掉电不丢,上电即用。相比 EEPROM,MRAM 的写入速度快了几个数量级,修改配置时用户几乎感觉不到延迟。
如果数据量再大一些,可以多片 MRAM 级联,用不同的 CS 引脚选择。PIC18F46K20 的 IO 足够多,挂四五片 MRAM 没问题,容量能扩展到 2MB 以上,对于大多数工业终端来说绰绰有余。
我个人在实际操作中的体会是,MRAM 这种器件最大的价值不在于它有多快或者多大,而在于它把“非易失”和“随机读写”这两个原本矛盾的属性统一在了一起。用惯了 Flash 的人第一次用 MRAM,会有一种“原来存储可以这么简单”的感慨。当然,它的价格确实比 Flash 高,但在那些写入频繁、掉电不能丢数据、维护成本高的场景下,多花的这点钱很快就能从可靠性和维护成本上省回来。选型的时候别只看器件单价,要把整个生命周期的成本算进去,这样才不会因小失大。