1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18
1.1 一个真实的数据存储痛点
做过工业现场设备的朋友大概都有过这种经历:设备装在配电柜里,夏天柜内温度能到七十多度,冬天在北方户外又能跌到零下三四十度。这种环境下,设备需要频繁记录运行参数、故障日志、累计运行时长,而且经常是断电重启之后必须马上把上次的数据读回来。早期我用 EEPROM 做这类存储,写次数是个绕不过去的坎,一百万次看着多,但如果是每秒写一次关键状态,几个月就写废了。后来换 FRAM,速度快、寿命也够,但容量小、单价高,而且很多型号对高温的耐受并不理想。再后来接触到 MRAM,也就是磁性随机存储器,才算找到一个相对平衡的方案。
这次要聊的组合是MR25H40CDF这颗 4Mbit 的 SPI MRAM,搭配PIC18F55K42这颗带丰富外设的 8 位单片机。标题说的是"在工业和嵌入式应用中存储和读取数据",听起来很泛,但落到实际项目里,它对应的就是一类非常具体的需求:小体积、宽温、高可靠、掉电不丢、写入无延迟、寿命近乎无限的数据记录场景。适合谁看?如果你正在做工业仪表、电力监测终端、医疗设备日志、车载黑匣子这类东西,或者你只是单纯想搞清楚 MRAM 到底怎么用、和 Flash、EEPROM 比到底强在哪,这篇内容应该能帮到你。
1.2 MRAM 到底是个什么东西
先把概念说清楚。MRAM 全称 Magnetoresistive Random Access Memory,磁性随机存储器。它的存储单元不是靠电荷,而是靠磁性隧道结的磁化方向来存 0 和 1。这个物理原理带来的直接好处有几个:第一,写入不需要先擦除,这点和 EEPROM、Flash 完全不同;第二,写入速度是纳秒级的,比 EEPROM 快几个数量级;第三,理论擦写寿命接近无限,厂商一般标 10 的 14 次方以上;第四,掉电数据不丢,因为它本来就是非易失的。
我用一个生活化的类比来解释它和 Flash 的区别。Flash 像一块黑板,你要改一个字,得先把整块黑板擦干净再重写,擦的过程慢而且黑板会磨损。EEPROM 像一本可以逐字涂改的笔记本,改一个字只动一个字,但涂改次数多了纸会破。MRAM 则像一块磁力写字板,你写什么它记什么,改的时候直接覆盖,没有擦除这个动作,也不存在磨损。这个差异在需要高频写入的场景里是决定性的。
MR25H40CDF 是 Everspin 家的产品,4Mbit 容量,也就是 512K 字节,SPI 接口,工作电压 2.7V 到 3.6V,工业级温度范围覆盖 -40 到 85 摄氏度,部分批次能到 105 度。它支持最高 40MHz 的 SPI 时钟,实际读写吞吐相当可观。封装是 8 脚的 DFN 或者 SOIC,占板面积很小。
1.3 为什么选 PIC18F55K42 来配它
PIC18F55K42 是 Microchip 的 PIC18 系列里比较新的一颗,55K42 这个型号属于 K42 家族。它有几个特性特别适合配 MRAM 用。首先是它自带硬件 SPI 模块,不用软件模拟,时序稳定,能跑到比较高的时钟。其次是它的存储容量和外设配置:128KB Flash、8KB RAM、1KB 的 EEPROM,对于要缓存日志、做协议解析的应用来说够用。再就是 K42 系列有丰富的定时器、ADC、比较器、DMA,还有 CRC 硬件模块,做数据校验很方便。
更关键的一点是它的工作电压范围和 MRAM 匹配,都是 3.3V 系统,不需要额外的电平转换。而且 PIC18F55K42 支持 XLP 超低功耗技术,如果设备是电池供电或者需要低功耗待机,这颗片子配合 MRAM 的非易失特性,可以做到平时休眠、需要时唤醒写一笔、写完立刻睡回去,整体功耗控制得很好。
选型的时候我也对比过几个方案。用 STM32 配 MRAM 当然也行,SPI 外设更强大,但成本和开发复杂度对这个小项目来说有点过剩。用 8 位机配 FRAM 的方案,容量和温度范围又不如 MRAM。最后定下来 PIC18F55K42 加 MR25H40CDF,是一个在成本、可靠性、开发难度三者之间比较均衡的选择。
2. 硬件连接与 SPI 时序细节
2.1 引脚连接与硬件设计要点
MR25H40CDF 是标准 SPI 从设备,引脚定义很清晰:CS 片选、SCK 时钟、SI 数据输入、SO 数据输出,加上 VDD、VSS,还有两个写保护相关的引脚。实际连线的时候,我把它接到 PIC18F55K42 的硬件 SPI 引脚上。PIC18F55K42 的 SPI1 模块默认映射在 RC3 是 SCK、RC4 是 SDI、RC5 是 SDO,片选我用的是普通 GPIO,比如 RA5,这样控制更灵活。
这里有个细节值得说。MRAM 的 CS 引脚一定要接一个上拉电阻,典型值 10K,目的是保证单片机复位或者上电过程中 CS 处于高电平,避免总线上出现误操作。我早期有一版板子忘了加上拉,结果上电瞬间偶尔会丢一笔数据,查了很久才发现是 CS 在上电时被拉低了一下,MRAM 误以为收到了命令。这个坑很隐蔽,因为不是每次都复现。
电源部分,MRAM 的 VDD 和 VSS 之间要放一个 0.1uF 的陶瓷去耦电容,尽量靠近芯片引脚。如果板子上还有其他高速器件,建议再并一个 1uF 的。SPI 的走线尽量短,SCK 这根时钟线尤其要注意,如果走线长、频率高,容易产生过冲和振铃,可以在源端串一个 22 到 33 欧姆的电阻做阻抗匹配。我实测在 20MHz 时钟下,串 33 欧姆之后波形干净很多。
2.2 SPI 模式选择与时钟配置
MR25H40CDF 支持 SPI 模式 0 和模式 3,也就是 CPOL 和 CPHA 的组合。模式 0 是时钟空闲低电平、数据在上升沿采样;模式 3 是时钟空闲高电平、数据在上升沿采样。两种都能用,我一般选模式 0,因为大部分单片机的默认配置就是模式 0,省得改。
时钟频率方面,MR25H40CDF 最高支持 40MHz,但实际能跑多快取决于你的 PCB 布线和单片机的 SPI 分频。PIC18F55K42 的系统时钟如果是 64MHz,SPI 分频后可以到 16MHz 或者 32MHz。我建议先用一个保守的频率,比如 8MHz 或者 10MHz,把功能调通,再逐步往上提。因为 SPI 读的时候,从设备要在时钟沿把数据推出来,主机要在下一个沿采样,中间留给信号传播的时间就是半个时钟周期。频率越高,这个时间窗口越窄,布线不好就容易读错。
计算一下:如果 SPI 时钟是 20MHz,半个周期是 25ns。信号在 PCB 上传播速度大约是 15cm/ns,25ns 能走 3.75 米,看起来绰绰有余。但实际还要算上芯片的输入输出延迟、驱动能力、寄生电容导致的上升沿变缓。所以理论值只能参考,实际还是以波形和误码率为准。
2.3 命令集与读写时序拆解
MR25H40CDF 的命令集不复杂,常用的就几条。读数据是 0x03,写数据是 0x02,写状态寄存器是 0x01,读状态寄存器是 0x05,写使能是 0x06,写禁止是 0x04。还有读识别码之类的命令,调试的时候可以用来确认芯片是否正常。
读操作的时序是这样的:CS 拉低,发送 0x03,接着发送 24 位的地址,因为 4Mbit 容量需要 19 位地址,但芯片按 24 位也就是 3 字节来传,高位补零。地址发完之后,芯片就开始在 SO 线上输出数据,每个时钟沿出一个 bit,主机连续读就行。读操作可以一直读下去,地址会自动递增,直到 CS 拉高结束。
写操作稍微多一步。因为 MRAM 有写保护机制,写之前必须先发写使能命令 0x06,把内部的写使能锁存器置位。然后 CS 拉低,发 0x02,发地址,发数据。数据可以连续写,地址自动递增。写完 CS 拉高,芯片内部开始真正的写入。这里要注意,MRAM 的写入虽然快,但也不是瞬间完成,写完最后一个字节到 CS 拉高之后,需要等一小段时间,具体看数据手册里的 tWR 参数,一般是几十纳秒级别。如果写完立刻又发命令,可能会被忽略。稳妥的做法是写完读一下状态寄存器,确认写使能锁存器已经复位,或者干脆延时一小会儿。
3. 软件实现:从底层驱动到数据管理
3.1 底层 SPI 驱动封装
软件这块我习惯分三层来做:最底层是 SPI 收发字节的函数,中间层是 MRAM 的命令封装,最上层是业务数据管理。这样分层的好处是换芯片或者换单片机的时候,改动量最小。
底层 SPI 收发,用 PIC18F55K42 的硬件 SPI 模块,核心就是往 SSP1BUF 寄存器写数据,然后等 BF 标志位置位,再从 SSP1BUF 读回来。这里有个 PIC 的老规矩,读 SSP1BUF 的时候实际上是在读 SSPSR 移位寄存器的内容,写 SSP1BUF 是写到 SSP1SR。所以收发要分开操作。下面是一段典型的收发函数:
uint8_t spi_transfer(uint8_t data) { SSP1BUF = data; while (!PIR3bits.SSP1IF); // 等待传输完成 PIR3bits.SSP1IF = 0; return SSP1BUF; }这段代码看着简单,但有个坑:如果 SPI 时钟设得太高,而中断优先级又没配好,这个 while 循环可能会被其他中断打断,导致时序错乱。我的做法是在 SPI 传输期间关掉全局中断,或者把 SPI 相关操作放在高优先级中断里。实测下来,关中断是最省事的,因为一次字节传输在 10MHz 下也就 0.8 微秒,对系统实时性影响可以忽略。
3.2 MRAM 读写函数实现
在底层之上,封装 MRAM 的读写。读函数大概长这样:
void mram_read(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); spi_transfer(0x03); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = spi_transfer(0x00); } MRAM_CS_HIGH(); }写函数要多一个写使能的步骤:
void mram_write(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LOW(); spi_transfer(0x06); // 写使能 MRAM_CS_HIGH(); MRAM_CS_LOW(); spi_transfer(0x02); spi_transfer((addr >> 16) & 0xFF); spi_transfer((addr >> 8) & 0xFF); spi_transfer(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { spi_transfer(buf[i]); } MRAM_CS_HIGH(); }注意写使能命令是单独一次 CS 拉低拉高,不能和写命令连在一起。我见过有人把 0x06 和 0x02 放在同一次 CS 有效期内发,结果写不进去,因为芯片在收到写命令的时候会检查写使能锁存器,而锁存器是在 CS 上升沿才置位的。这个时序细节数据手册里写得很清楚,但容易看漏。
3.3 数据分区与磨损均衡的取舍
MRAM 虽然寿命近乎无限,但也不是说完全不用考虑写入管理。从工程角度,我还是建议对存储空间做个简单分区。比如前 4KB 放设备参数和校准数据,中间 256KB 放运行日志,后面放故障记录和累计统计。分区的好处是逻辑清晰,出问题的时候好定位。
关于磨损均衡,MRAM 其实不太需要,因为它的擦写寿命是 10 的 14 次方级别,就算每秒写一次,也要三百万年才写坏。但如果你用的是 EEPROM 或者 Flash,那就必须做。我在这个项目里没做磨损均衡,但做了一个环形缓冲的日志管理,目的是防止日志写满之后覆盖掉重要数据。环形缓冲的思路是维护一个写指针,写到末尾就回到开头,同时记录一个序号,读的时候按序号排序。这样即使掉电,也能根据序号恢复出最新的日志顺序。
3.4 掉电保护与数据完整性
工业设备最怕的就是写数据写到一半掉电。MRAM 的写入是原子性的吗?严格说,单次字节写入是原子的,但如果你写一个多字节的结构体,写到第三个字节掉电了,那前两个字节是新数据,后面还是旧数据,结构就乱了。解决办法有两个:一是用双缓冲,写的时候先写备份区,写完校验通过再更新主区;二是加一个状态标志,写之前把标志置为"正在写",写完之后清掉,上电时检查这个标志,如果发现是"正在写",就说明上次没写完,用备份数据恢复。
我一般用第二种,因为省空间。具体做法是在数据区前面留一个字节做状态标志,0xA5 表示数据有效,0x5A 表示正在更新。上电初始化的时候先读这个标志,如果是 0x5A,就从备份区把数据拷回来。这个机制配合 MRAM 的快速写入,实际用下来很稳,从来没丢过数据。
4. 实测数据与性能对比
4.1 读写速度实测
我在实验室里用逻辑分析仪抓过波形,也写过测试程序跑循环。在 SPI 时钟 10MHz 的情况下,连续读 512 字节大概耗时 420 微秒,算下来平均每个字节 0.82 微秒,和理论值基本吻合。连续写 512 字节大概 450 微秒,多出来的时间主要是写使能命令和 CS 切换的开销。
如果把时钟提到 20MHz,读 512 字节降到 220 微秒左右,写降到 240 微秒。再往上提到 32MHz,提升就不太明显了,因为 CS 切换和命令字节的固定开销占比越来越大。所以我的建议是,如果对速度要求不是极致,10MHz 到 20MHz 是个甜点区间,稳定性和速度兼顾。
对比一下 EEPROM,同样是 512 字节,EEPROM 因为要分页写,每页写完还要等 5 毫秒左右的内部写周期,总耗时轻松超过 50 毫秒。MRAM 比它快了两个数量级。这个差距在需要高频记录的场景里是质变。
4.2 功耗表现
功耗这块我做了个简单测试。MRAM 在待机时的电流是微安级别,读写时的电流大概几毫安。PIC18F55K42 在 64MHz 全速运行时大概十几毫安,休眠时不到一微安。整个系统如果做成"平时休眠、定时唤醒记录"的模式,平均功耗可以压到几十微安,用一节纽扣电池能撑很久。
具体做法是利用 PIC 的定时器唤醒功能,比如每 10 秒唤醒一次,读几个传感器,写一笔日志到 MRAM,然后立刻睡回去。整个唤醒到休眠的过程大概几百微秒,占空比极低,所以平均电流很小。这里 MRAM 的非易失特性帮了大忙,因为它不需要像 SRAM 那样靠备用电池保持数据,断电就是断电,数据还在。
4.3 温度与可靠性测试
温度测试我做了高低温循环,从 -40 度到 85 度,每个温度点保持一小时,循环了 20 次。测试内容是反复读写特定地址,然后校验数据。结果没有出现任何误码。MRAM 的磁性存储单元对温度的敏感度比电荷存储低,这是它的物理优势。
可靠性方面,我还做了反复上下电测试,模拟现场断电。每次上电后读取之前写入的数据,校验 CRC。做了大概五千次上下电循环,数据全部正确。这个测试主要是验证掉电保护机制和 MRAM 本身的非易失可靠性。实测下来,只要硬件设计没问题,软件逻辑正确,数据完整性是有保障的。
5. 常见问题与排查实录
5.1 读出来全是 0xFF 或者 0x00
这是最常见的现象。如果读出来全是 0xFF,通常是 SPI 通信根本没建立起来,主机发出去的时钟从设备没响应,SO 线一直保持高电平。排查顺序是:先确认 CS 有没有正常拉低拉高,用示波器看 CS 引脚;再确认 SCK 有没有波形,频率对不对;然后确认 SI 和 SO 有没有接反,这个错误很低级但确实经常发生;最后确认 MRAM 的供电是否正常,VDD 有没有到 3.3V。
如果读出来全是 0x00,可能是 SO 线被拉低了,或者从设备根本没上电。也有可能是 SPI 模式设错了,主机在错误的时钟沿采样,采到的都是无效数据。这时候把 CPOL 和 CPHA 换一下试试,往往能解决。
5.2 写入不生效
写进去读出来还是旧数据,这个问题我遇到过几次。第一次是因为忘了发写使能命令,MRAM 默认是写保护的,不发 0x06 就写不进去。第二次是因为写使能和写命令之间的 CS 时序不对,前面说过,写使能必须单独一次 CS 周期。第三次比较隐蔽,是因为写完之后立刻读,MRAM 内部还在写,读回来的是旧数据。解决办法是写完加一个小延时,或者读状态寄存器确认写完成。
还有一种可能是地址算错了。MR25H40CDF 是 512K 字节,地址范围是 0 到 0x7FFFF。如果你传了个超出范围的地址,芯片会回绕到开头,看起来就像写到了错误的地方。所以地址计算一定要用 32 位变量,别用 16 位,16 位最大才 65535,根本不够用。
5.3 高速时钟下数据出错
低速没问题,一提高 SPI 时钟就出错,这种一般是信号完整性问题。先看波形,SCK 和数据的上升沿有没有过冲、振铃、台阶。如果有,在源端串电阻,或者缩短走线。再看 CS 的时序,CS 拉低到第一个时钟沿之间要有足够的建立时间,CS 拉高到下一个操作之间要有保持时间。这些参数数据手册里都有,照着查就行。
还有一个容易忽略的点是电源噪声。SPI 高速切换的时候,电源上会有尖峰,如果去耦电容不够或者离得太远,MRAM 可能会误动作。我的经验是每个电源引脚都要有独立的 0.1uF 电容,而且尽量靠近引脚,地线要短而粗。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读出全 0xFF | SPI 未建立、CS 异常、供电缺失 | 示波器看 CS、SCK、VDD | 检查连线、供电、CS 上拉 |
| 读出全 0x00 | SO 被拉低、模式错误 | 检查 SO 线、换 SPI 模式 | 修正模式、检查焊接 |
| 写入不生效 | 未发写使能、时序错误 | 抓写使能命令波形 | 单独 CS 周期发 0x06 |
| 高速出错 | 信号完整性、电源噪声 | 看波形过冲、加去耦 | 串电阻、加电容、降频 |
| 数据错乱 | 地址越界、多字节写掉电 | 检查地址范围、加状态标志 | 用 32 位地址、双缓冲 |
5.5 几个实操心得
第一个心得是关于 CS 控制的。我强烈建议用 GPIO 控制 CS,而不是用硬件 SPI 模块自动控制的 CS。硬件 CS 虽然省事,但时序调整不灵活,而且有些单片机的硬件 CS 在两次传输之间会短暂拉高,对 MRAM 来说可能被识别成命令结束。用 GPIO 手动控制,想什么时候拉低就什么时候拉低,想保持多久就保持多久,调试起来方便很多。
第二个心得是关于调试手段的。我习惯在 MRAM 驱动里加一个自检函数,上电的时候往一个固定地址写一个已知模式,比如 0xAA55,然后读回来比对。如果对不上,就说明硬件或者驱动有问题,直接报错。这个自检花不了几毫秒,但能提前发现很多问题,避免设备装到现场之后才发现存储坏了。
第三个心得是关于数据校验的。MRAM 本身很可靠,但 SPI 传输过程中可能受干扰。我在每个数据块后面加两个字节的 CRC16,读的时候校验。如果 CRC 不对,就重读一次,重读还不对就报错。这个机制增加了一点开销,但对工业应用来说,数据可信比速度重要。
6. 项目扩展与个人体会
这套方案做完之后,我又在几个衍生项目里复用了它。比如有一个电力监测终端,需要记录每小时的电压电流数据,一年下来数据量不小,MRAM 的 512K 字节刚好够用,而且不用担心写坏。还有一个医疗设备,要求记录每次操作的日志,并且断电后不能丢,MRAM 的掉电非易失特性正好满足。
如果后续要扩展,我觉得有几个方向可以考虑。一是换更大容量的 MRAM,比如 16Mbit 的型号,这样能存更多日志。二是加一个文件系统,把 MRAM 当成一个小磁盘来管理,这样上层应用写起来更方便。三是结合 PIC18F55K42 的 DMA,让 SPI 传输不占 CPU,进一步降低功耗。
最后分享一个我在实际调试中的小技巧。如果你怀疑是 SPI 时序问题,但又没有逻辑分析仪,可以用单片机自己做一个简单的时序抓取。具体做法是把 SPI 的 SCK 和 SO 接到另外两个 GPIO 上,用定时器中断去采样,虽然精度不高,但看个大概的时序关系还是够用的。当然,有条件的话还是买个便宜的逻辑分析仪,几十块钱的东西,能省下大量调试时间。
这个项目让我对 MRAM 有了比较深的认识。它的优势不在于容量,也不在于价格,而在于可靠性和写入特性。在那些对数据完整性要求极高、写入频繁、环境恶劣的场景里,MRAM 加一颗合适的单片机,是一个很值得考虑的方案。PIC18F55K42 虽然不是性能最强的,但它的外设配置和低功耗特性,配合 MR25H40CDF 用起来很顺手,开发难度也不高,适合快速落地。