1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18
1.1 一个真实场景引出的需求
前阵子接了个工业数据采集终端的活儿,设备装在配电柜里,环境温度夏天能到 70 度,冬天户外柜内能到零下 30 度。需求说起来简单:每隔 100ms 采一组传感器数据,累计运行时间、故障记录、校准参数这些关键信息必须掉电不丢,而且设备寿命要求十年以上,期间不允许换电池。
一开始想用 FRAM,贵得离谱;想用 EEPROM,写入寿命和速度都扛不住 100ms 一次的频率;想用带电池的 SRAM,电池在高温下两年就鼓包了。后来翻资料翻到 Everspin 的 MR25H40CDF,4Mbit 的 SPI MRAM,无限次擦写、掉电即存、不需要电池,工作温度覆盖工业级甚至部分车规范围。主控选了 PIC18F55K42,原因很简单:这颗片子自带 SPI 外设、DMA、充足的中断资源和 5V 容忍 IO,在工业现场抗干扰能力比 3.3V 单电源的 ARM 更让人放心,而且 Microchip 的生态对长期供货有保障。
这个组合实测下来非常稳,所以把整个存储和读取的完整实现过程整理出来,给做工业嵌入式存储的朋友一个可直接抄作业的参考。
1.2 MR25H40CDF 到底是个什么东西
MR25H40CDF 是 Everspin 出的 4Mbit(512K x 8)串行 MRAM。MRAM 的全称是磁性随机存储器,它的存储单元是磁性隧道结(MTJ),靠电子自旋方向存储数据,而不是靠电荷。这个物理原理决定了它几个非常关键的特性:
- 写入不需要擦除:不像 Flash 和 EEPROM 要先擦后写,MRAM 直接覆盖写,字节级随机写入,没有块的概念。
- 写入寿命近乎无限:官方标称 10^14 次以上,实际上你按 100ms 写一次算,十年也就 3×10^9 次,连零头都不到。
- 写入速度极快:SPI 时钟最高 40MHz,没有写入等待周期,写一个字节和读一个字节时间一样。
- 掉电数据保持:数据靠磁性状态保持,不需要供电,官方标称 20 年数据保持。
- 宽温工作:工业级 -40 到 85 度,部分型号到 105 度。
对比一下常见非易失存储的差异,选型时一目了然:
| 特性 | MR25H40CDF (MRAM) | 典型 SPI Flash (W25Q64) | 典型 SPI EEPROM (25LC256) |
|---|---|---|---|
| 容量 | 512KB | 8MB | 32KB |
| 写入前擦除 | 不需要 | 必须按扇区擦 | 不需要 |
| 擦写寿命 | 10^14 次 | 10 万次 | 100 万次 |
| 写入速度 | 与读同速 | 慢,有等待 | 慢,有等待 |
| 字节级写入 | 支持 | 不支持 | 支持 |
| 掉电保持 | 20 年 | 20 年 | 200 年 |
| 单价 | 高 | 低 | 中 |
贵是真贵,但工业场景里数据可靠性比几块钱的 BOM 成本重要得多。我踩过的坑是:曾经用 Flash 存运行日志,结果设备频繁掉电导致擦写次数迅速耗尽,现场返修换芯片,售后成本远超当初省下的那点物料钱。
1.3 PIC18F55K42 的 SPI 资源盘点
PIC18F55K42 是 Microchip 的 8 位增强型单片机,主频 64MHz,自带两个 MSSP 模块(SPI/I2C 可配置),还有独立的 DMA 控制器。用它驱动 MR25H40CDF 有几个天然优势:
- 硬件 SPI 速率可配:通过 SSPxCON1 的 SSPxCON0 寄存器设置时钟极性、相位和分频,最高能到 16MHz(Fosc/4)。
- DMA 支持:可以把 SPI 收发和内存搬运解耦,CPU 不用死等,特别适合连续读写大块数据。
- 5V 容忍 IO:MR25H40CDF 是 3.3V 供电,但 PIC 的 IO 可以容忍 5V 输入,电平匹配简单,不需要额外电平转换芯片。
- 充足的中断向量:SPI 收发中断、DMA 完成中断、定时器中断可以各司其职,实时性有保障。
选 PIC18F55K42 而不是更便宜的 PIC18F45K42,主要是看中了 55K42 的更大 RAM(4KB)和更多外设,做数据缓冲和 DMA 描述符管理更从容。
2. 硬件连接与底层时序:把线接对是第一步
2.1 引脚连接与 PCB 布局要点
MR25H40CDF 是 8 引脚 DFN 或 SOIC 封装,引脚定义如下:
| 引脚 | 名称 | 功能 | 接 PIC18F55K42 |
|---|---|---|---|
| 1 | CS | 片选,低有效 | RC0(软件片选) |
| 2 | SO | 数据输出(MISO) | RC4(SDI) |
| 3 | WP | 写保护,低有效 | 接 3.3V 或 IO 控制 |
| 4 | VSS | 地 | GND |
| 5 | SI | 数据输入(MOSI) | RC5(SDO) |
| 6 | SCK | 时钟 | RC3(SCK) |
| 7 | HOLD | 保持,低有效 | 接 3.3V |
| 8 | VDD | 电源 | 3.3V |
这里有几个实操中容易翻车的点:
注意:WP 和 HOLD 引脚不要悬空。我见过有人把 WP 悬空,结果上电瞬间被干扰拉低,芯片进入写保护状态,怎么调都写不进去。稳妥做法是 WP 和 HOLD 都通过 10K 上拉到 3.3V,如果不需要动态写保护,WP 直接接 3.3V 也行。
PCB 布局上,SPI 走线尽量短,SCK 和 MOSI 远离模拟信号线。MRAM 的电源引脚旁边必须放 0.1uF 陶瓷电容,紧贴芯片,再加一个 1uF 的钽电容做低频滤波。地平面要完整,不要被其他信号割裂。如果设备工作在强电磁环境,SCK 线上可以串一个 22 欧姆的电阻做阻尼,抑制过冲。
2.2 SPI 模式选择与时钟计算
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。我一般用模式 0,因为 PIC 的 MSSP 默认配置就是模式 0,省事。
时钟频率计算:PIC18F55K42 的系统时钟 Fosc 设为 64MHz,SPI 时钟源是 Fosc/4 = 16MHz,再通过 SSPxCON1 的 SSPxADD 分频。如果 SSPxADD = 0,SPI 时钟就是 16MHz。MR25H40CDF 最高支持 40MHz,所以 16MHz 完全没问题。
但实际调试时我发现,16MHz 在长走线或劣质排线上容易出错,后来降到 8MHz(SSPxADD = 1)就非常稳。工业现场建议留余量,不要跑极限。
2.3 底层读写时序的代码实现
MR25H40CDF 的指令集很简洁,核心就几条:
| 指令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
注意 MRAM 和 EEPROM 不同,它没有页的概念,WRITE 指令后面跟 24 位地址,然后可以连续写任意多个字节,地址自动递增,直到 CS 拉高。这比 EEPROM 的页写方便太多,不用算页边界。
下面是基于 PIC18F55K42 的 SPI 底层驱动代码,用 MCC 生成的 MSSP 初始化基础上手写:
#include <xc.h> #include "mcc_generated_files/spi1.h" #define MRAM_CS_LAT() (LATC0 = 0) #define MRAM_CS_HIGH() (LATC0 = 1) // 写使能 void MRAM_WriteEnable(void) { MRAM_CS_LAT(); SPI1_ExchangeByte(0x06); MRAM_CS_HIGH(); } // 读状态寄存器 uint8_t MRAM_ReadStatus(void) { uint8_t status; MRAM_CS_LAT(); SPI1_ExchangeByte(0x05); status = SPI1_ExchangeByte(0x00); MRAM_CS_HIGH(); return status; } // 写一个字节到指定地址 void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_LAT(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((uint8_t)(addr >> 16)); SPI1_ExchangeByte((uint8_t)(addr >> 8)); SPI1_ExchangeByte((uint8_t)(addr)); SPI1_ExchangeByte(data); MRAM_CS_HIGH(); } // 从指定地址读一个字节 uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t data; MRAM_CS_LAT(); SPI1_ExchangeByte(0x03); SPI1_ExchangeByte((uint8_t)(addr >> 16)); SPI1_ExchangeByte((uint8_t)(addr >> 8)); SPI1_ExchangeByte((uint8_t)(addr)); data = SPI1_ExchangeByte(0x00); MRAM_CS_HIGH(); return data; }这段代码里有个细节:MRAM_WriteEnable 之后必须紧跟写操作,中间不能插入其他 SPI 事务,否则写使能会失效。我在调试时曾经在 WREN 和 WRITE 之间加了一句读状态的调试代码,结果写不进去,查了半天才发现是这个原因。
3. 数据存储架构设计:怎么组织 512KB 空间
3.1 分区规划与地址映射
512KB 看着不大,但存运行日志、参数、故障记录绰绰有余。我的分区方案如下:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| 参数区 | 0x00000 | 4KB | 校准参数、设备ID、配置 |
| 运行统计区 | 0x01000 | 4KB | 累计运行时间、启停次数 |
| 故障记录区 | 0x02000 | 60KB | 循环记录最近 1000 条故障 |
| 数据日志区 | 0x11000 | 448KB | 循环记录传感器数据 |
| 备份区 | 0x7F000 | 4KB | 参数区镜像,带校验 |
参数区用双备份加 CRC 校验,写入时先写备份区,校验通过再写主区,防止写过程中掉电导致参数损坏。这个思路借鉴了汽车电子里 NVM 的常见做法。
3.2 循环日志的读写指针管理
数据日志区用循环队列,头指针和尾指针存在运行统计区里。每写一条记录,头指针前进,如果追上尾指针就覆盖最旧数据。关键点是头尾指针的更新必须原子化,否则掉电时可能指针和数据不一致。
我的做法是:每条记录末尾带一个 16 位 CRC,读取时先校验 CRC,不通过就认为这条记录无效,跳过。这样即使掉电导致某条记录写了一半,也不会影响整体数据的可用性。
typedef struct { uint32_t timestamp; uint16_t sensor1; uint16_t sensor2; uint8_t status; uint16_t crc; } LogRecord_t; #define LOG_RECORD_SIZE sizeof(LogRecord_t) #define LOG_AREA_START 0x11000 #define LOG_AREA_SIZE 0x6F000 #define LOG_MAX_RECORDS (LOG_AREA_SIZE / LOG_RECORD_SIZE)按这个结构,每条记录 11 字节,448KB 能存约 4 万条。100ms 一条的话,能存约 66 分钟。如果嫌短,可以降低采样频率或者压缩数据。实际项目中我把传感器数据做了差分压缩,存储时间延长到了 8 小时。
3.3 掉电保护与写入原子性
MRAM 本身写入是原子的,但多条数据的逻辑事务不是。比如更新参数时,要同时改参数值和 CRC,如果写完参数值掉电,CRC 还是旧的,下次上电校验就失败。
解决方案是双缓冲加事务标志:
- 在参数区头部设一个 4 字节的事务标志区。
- 写参数前,先把标志写成 0xAA55AA55 表示“写入中”。
- 写完参数和 CRC 后,把标志改成 0x55AA55AA 表示“写入完成”。
- 上电时检查标志,如果是“写入中”,说明上次写了一半,自动回滚到备份区。
这个机制实测能扛住任意时刻的掉电,参数不会丢也不会错。
4. 完整读写流程与 DMA 加速实战
4.1 单字节读写与批量读写的取舍
单字节读写适合参数更新这种低频操作,代码简单,不容易出错。但数据日志区如果一条一条写,SPI 事务开销太大。假设一条记录 11 字节,单字节写要 11 次 CS 拉低拉高,每次都有指令和地址开销,实际有效数据率很低。
批量写就一次 CS 拉低,发 WRITE 指令加起始地址,然后连续发 N 个字节,地址自动递增。MR25H40CDF 没有页边界限制,一次可以写满整个芯片(只要你能等)。实测 16MHz SPI 下,连续写 1KB 数据大约 520 微秒,效率比单字节写高一个数量级。
void MRAM_WriteBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_WriteEnable(); MRAM_CS_LAT(); SPI1_ExchangeByte(0x02); SPI1_ExchangeByte((uint8_t)(addr >> 16)); SPI1_ExchangeByte((uint8_t)(addr >> 8)); SPI1_ExchangeByte((uint8_t)(addr)); for (uint16_t i = 0; i < len; i++) { SPI1_ExchangeByte(buf[i]); } MRAM_CS_HIGH(); }4.2 用 DMA 解放 CPU
PIC18F55K42 的 DMA 控制器可以配置成 SPI 发送和接收。配置步骤:
- 设置 DMA 源地址为内存缓冲区,目标地址为 SPI1TXB 寄存器。
- 设置传输长度为要发送的字节数。
- 触发源选择 SPI1TX 空中断或软件触发。
- 启动 DMA,CPU 去处理其他任务,DMA 完成后产生中断。
接收方向类似,源地址为 SPI1RXB,目标地址为内存缓冲区。注意 SPI 是全双工,发送的同时会接收,如果只发不收,接收数据丢弃即可,但 DMA 接收通道要关掉或者指向一个 dummy 变量。
DMA 配置的坑在于:PIC 的 DMA 和外设中断优先级要协调好,如果 SPI 中断优先级高于 DMA 完成中断,可能出现数据还没搬完就进中断的情况。我的做法是把 DMA 完成中断设为高优先级,SPI 中断设为低优先级,实测稳定。
4.3 实测性能数据
用逻辑分析仪抓了一下实际波形,整理成表格:
| 操作 | SPI 时钟 | 数据量 | 耗时 | 有效速率 |
|---|---|---|---|---|
| 单字节写 | 16MHz | 1B | 5.2us | 192KB/s |
| 批量写 | 16MHz | 1KB | 520us | 1.97MB/s |
| 批量读 | 16MHz | 1KB | 515us | 1.99MB/s |
| DMA 批量写 | 16MHz | 4KB | 2.06ms | 1.99MB/s |
| DMA 批量读 | 16MHz | 4KB | 2.05ms | 2.0MB/s |
可以看到批量操作能把有效速率拉到接近 2MB/s,对于 512KB 的芯片来说,全片读写也就 0.25 秒左右。DMA 相比 CPU 轮询的优势不在速率,而在于 CPU 占用率从 100% 降到接近 0,可以同时处理传感器采集和通信任务。
5. 常见问题与排查技巧实录
5.1 读写失败问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读出来全是 0xFF | CS 没拉低或时序不对 | 逻辑分析仪抓 CS 和 SCK | 检查 CS 引脚配置,确认拉低时机 |
| 写不进去 | WP 引脚被拉低 | 万用表测 WP 电压 | WP 接 3.3V 或上拉 |
| 数据偶尔出错 | SPI 时钟太快或走线太长 | 降低时钟测试 | 降速到 8MHz,加阻尼电阻 |
| 上电后参数丢失 | 掉电时写入未完成 | 检查事务标志 | 实现双缓冲事务机制 |
| 读出的数据错位 | 地址字节序搞反 | 对照手册检查地址发送顺序 | 高字节在前,24 位地址 |
| DMA 传输不完成 | 触发源配置错误 | 查 DMA 状态寄存器 | 确认触发源和通道使能 |
5.2 几个让我熬夜的坑
第一个坑是 CS 时序。PIC 的 MSSP 在配置成 SPI 主模式时,CS 引脚如果配置成 SSPxSS 功能,硬件会自动控制。但我用的是普通 IO 做软件片选,结果初始化时忘了把 RC0 设为输出,导致 CS 一直是高阻态,MRAM 根本不响应。后来养成习惯,所有片选引脚初始化时先设输出高电平,再配置 SPI。
第二个坑是 SPI 模式不匹配。MR25H40CDF 支持模式 0 和模式 3,我一开始用模式 0,但 PIC 的 CKP 位设错了,实际输出的是模式 1,结果读出来的数据整体偏移一位。用逻辑分析仪对比 SCK 和 MOSI 的相位才找到问题。教训是:SPI 调试一定要有逻辑分析仪,光靠串口打印是查不出时序问题的。
第三个坑是电源纹波。工业现场 24V 转 3.3V 的 DC-DC 纹波比较大,MRAM 在写入瞬间电流突增,导致电源跌落,写入失败。后来在 3.3V 输出端加了 100uF 电解电容和 0.1uF 陶瓷电容组合,问题解决。所以电源设计不能省,尤其是 MRAM 这种写入电流比静态电流大不少的器件。
5.3 提高可靠性的几个实操心得
- 上电自检:每次上电先读参数区 CRC,不通过就从备份区恢复。再写一个测试模式到日志区末尾,读回验证,确认 SPI 通路正常。
- 写入后回读:关键参数写入后立即回读比对,不一致就重写,最多重试三次。MRAM 写入失败概率极低,但工业环境什么都有可能。
- 温度监控:如果设备工作温度接近 MRAM 的极限,建议加温度传感器,超温时降低写入频率或报警。
- ESD 防护:SPI 走线如果引出到接插件,一定要加 TVS 管,工业现场静电打坏 SPI 引脚的案例不少。
6. 这个方案还能怎么扩展
如果你也在做类似的工业存储项目,这套 MRAM 加 PIC 的组合可以直接复用。后续如果想扩展,有几个方向:
一是把 PIC18F55K42 换成带以太网或 CAN 的型号,把存储数据直接上传到上位机。二是增加文件系统层,把 MRAM 模拟成 FAT 格式,方便 PC 端直接读取。三是用双 MRAM 做冗余,主备同时写,可靠性再上一个台阶。
我个人在实际操作中的体会是:MRAM 的价格虽然高,但在工业场景里,它省掉的电池、省掉的擦写管理逻辑、省掉的售后返修,综合成本反而更低。选型时不要只看 BOM 单价,要把全生命周期成本算进去。另外,SPI 调试千万别省逻辑分析仪的钱,一个几百块的分析仪能帮你省下几十个小时的调试时间。