1. 项目缘起与方案选型:为什么是 MRAM 加 PIC18
1.1 一个真实场景引出的存储需求
工业现场的数据记录仪、电力监测终端、PLC 扩展模块这类设备,有一个共同的痛点:它们需要频繁地写入小批量关键数据,比如设备运行参数、故障日志、校准系数、累计运行时长,而且这些设备往往工作在高温、强振动、供电不稳定的环境里。我前几年接手过一个配电柜里的数据采集板项目,客户要求每 200 毫秒记录一次三相电流有效值,掉电后最近 10 分钟的数据不能丢。当时第一版方案用的是 EEPROM,结果现场跑了不到三个月就陆续出现写入失败,拆回来一测,EEPROM 的擦写寿命被高频写入吃光了。
这个教训让我开始认真考虑 MRAM 这类新型非易失存储。MR25H40CDF 就是在这个背景下进入视野的——4Mbit 容量、SPI 接口、号称无限次读写、无需擦除、写入速度接近 SRAM。而主控选 PIC18F86K90,是因为它自带 SPI 模块、引脚资源够用、工业温度等级齐全,而且 8 位机在这个场景下成本可控、代码量小、实时性反而比跑 Linux 的方案更稳。这套组合不是拍脑袋定的,下面把选型逻辑拆开讲。
1.2 MR25H40CDF 到底强在哪
先把这颗芯片的关键参数摆出来,方便你对照自己的项目判断是否合适。
| 参数项 | MR25H40CDF 指标 | 对比 EEPROM | 对比 SPI NOR Flash |
|---|---|---|---|
| 容量 | 4 Mbit(512 KB) | 通常 1K~512Kbit | 常见 1M~128Mbit |
| 接口 | SPI(最高 40 MHz) | I2C/SPI | SPI |
| 写入前是否需擦除 | 不需要 | 不需要 | 必须按扇区擦除 |
| 擦写寿命 | 近乎无限(10^14 次量级) | 约 100 万次 | 约 10 万次 |
| 写入延迟 | 无等待,字节级即时写入 | 有 5ms 级写周期 | 有 ms 级页编程时间 |
| 掉电数据保持 | 20 年以上 | 10 年以上 | 10 年以上 |
| 工作温度 | 工业级 -40~85℃ | 视型号 | 视型号 |
这张表里最值得说的是“写入前不需要擦除”和“字节级即时写入”。传统 Flash 写一个字节,实际流程是:读出整个扇区到 RAM、改一个字节、擦除整个扇区、再写回去。这个过程中如果掉电,整个扇区的数据可能全丢。MRAM 的存储机理是磁性隧道结的磁化方向,写入就是改变磁化方向,天然支持按字节随机写,没有擦除这一步,也就没有“擦除期间掉电丢一片”的风险。对于掉电频繁的工业现场,这一点比容量和速度都重要。
1.3 PIC18F86K90 作为主控的匹配度
PIC18F86K90 是 Microchip 的 8 位增强型单片机,128KB Flash、4KB RAM、自带 MSSP 模块可以配置成 SPI 主模式。选它主要看三点:第一,SPI 时钟可以灵活分频,从 Fosc/4 到 Fosc/64,配合 MRAM 的 40MHz 上限,跑个 10MHz 很轻松;第二,它有充足的中断源和定时器,方便做定时采集加存储的调度;第三,工业级版本温度范围覆盖 -40~125℃,和 MRAM 的 -40~85℃ 能对上。
有人会问,为什么不上 STM32 或者直接上嵌入式 Linux。我的经验是,这种“定时采集 + 关键数据存储 + 简单通信”的任务,8 位机的中断响应确定性反而更好,没有操作系统调度抖动,代码量小到可以全流程审查,出问题好定位。上 Linux 方案,光是根文件系统挂载、驱动适配就要折腾很久,对于批量不大的工业定制板,性价比不划算。
提示:选型时不要只看容量。MRAM 的价值在于“高频写 + 掉电安全”,如果你的项目只是偶尔存个配置参数,EEPROM 或 Flash 完全够用,没必要为 MRAM 的溢价买单。
2. 硬件连接与 SPI 时序要点
2.1 引脚连接与片选策略
MR25H40CDF 是标准 SPI 从设备,引脚包括 SCLK、SI(MOSI)、SO(MISO)、CS#,外加 WP# 和 HOLD# 两个控制脚。和 PIC18F86K90 连接时,我一般这样分配:
- SCLK 接 PIC 的 SCK 引脚(MSSP 模块固定引脚)
- SI 接 SDO,SO 接 SDI
- CS# 接一个普通 GPIO,用软件控制片选
- WP# 和 HOLD# 直接上拉到 VDD,除非你确实需要硬件写保护
这里有个细节值得展开:SPI 的片选到底用硬件还是软件。PIC18F86K90 的 MSSP 模块在从模式下才需要硬件 SS 引脚,做主模式时片选完全由软件控制。我强烈建议用软件片选,原因有两个:一是软件片选可以精确控制 CS 拉低到第一个时钟沿之间的建立时间,MRAM 对 CS 建立时间有要求;二是当总线上挂多个 SPI 设备时,软件片选能避免硬件 SS 冲突。实测下来,用软件片选配合适当的延时,通信稳定性明显更好。
2.2 SPI 模式与时钟极性选择
MR25H40CDF 支持 SPI 模式 0(CPOL=0,CPHA=0)和模式 3(CPOL=1,CPHA=1)。这两个模式的区别在于空闲时时钟是高还是低,以及数据在哪个边沿采样。我一般选模式 0,因为它是绝大多数 SPI 器件的默认模式,调试时用逻辑分析仪抓波形最直观。
配置 PIC18F86K90 的 SSPCON1 寄存器时,关键位是 CKP(时钟极性)和 CKE(时钟边沿选择)。模式 0 对应 CKP=0、CKE=1。这里容易踩的坑是:CKE 位的含义在不同手册里表述不一致,有的写“CKE=1 表示在时钟上升沿发送数据”,实际配置时要对着数据手册的时序图确认。我的做法是先按模式 0 配好,然后用逻辑分析仪抓一次读 ID 的波形,看数据是否在时钟上升沿稳定,不对就翻转 CKP 和 CKE 再试。
2.3 时钟频率与信号完整性
MRAM 标称支持 40MHz,但实际能跑多快取决于你的 PCB 布线和主控能力。PIC18F86K90 在 64MHz 系统时钟下,SPI 最高可以到 Fosc/4 即 16MHz。我一般把 SPI 时钟设在 10MHz 左右,这个频率下普通 FR4 板、几厘米的走线,信号质量都很好,不需要特别做阻抗匹配。
如果你非要把时钟拉高,要注意几点:SCLK 走线尽量短且远离其他高速信号;CS# 和 SCLK 之间不要有太长的平行走线,避免串扰;MISO 线上如果加了上拉电阻,阻值不要太小,否则会拖慢上升沿。我见过一个案例,SPI 跑到 20MHz 时读数据偶尔出错,最后发现是 MISO 走线太长又没做端接,降频到 12MHz 就完全稳定了。工业产品里,稳定性永远优先于那点速度。
注意:MRAM 的 WP# 引脚如果悬空,内部有弱上拉,但为了抗干扰,建议外部再加一个 10K 上拉到 VDD。HOLD# 同理,不用就上拉,不要悬空。
3. 底层驱动实现:从读 ID 到页写入
3.1 先读器件 ID 确认通信正常
任何 SPI 器件上手第一步都是读 ID。MR25H40CDF 的读 ID 命令是 0x9F,连续读 4 个字节会返回厂商 ID 和器件 ID。厂商 ID 是 0xE0,器件 ID 的高字节是 0x22,低字节是 0x08。这一步能同时验证接线、时钟极性、片选逻辑是否正确。
下面是我常用的读 ID 函数,基于 PIC18F86K90 的 MSSP 模块,用轮询方式收发:
unsigned char spi_transfer(unsigned char data) { SSPBUF = data; // 写入发送缓冲 while (!SSPSTATbits.BF); // 等待接收完成 return SSPBUF; // 读回接收数据 } void mram_read_id(unsigned char *id) { MRAM_CS_LOW(); // 片选拉低 spi_transfer(0x9F); // 发送读 ID 命令 id[0] = spi_transfer(0xFF); // 厂商 ID id[1] = spi_transfer(0xFF); // 器件类型 id[2] = spi_transfer(0xFF); // 容量 id[3] = spi_transfer(0xFF); // 保留 MRAM_CS_HIGH(); // 片选拉高 }这段代码里,spi_transfer用 0xFF 作为“空字节”来产生时钟,这是 SPI 读操作的通用套路——主机必须发送数据才能产生时钟,从机才会把数据移出来。while (!SSPSTATbits.BF)是等待接收缓冲满,注意这里不能用while (!SSPIF),因为 SSPIF 是中断标志,轮询方式下用 BF 位更直接。
3.2 写使能与写操作流程
MRAM 的写操作需要先发写使能命令 0x06,然后发写命令 0x02,接着是 3 字节地址(24 位地址,因为 512KB 需要 19 位,但协议用 3 字节对齐),最后是数据。写使能命令发出后,片选拉高,WREN 位就置位了,之后可以连续写多个字节,地址会自动递增。
void mram_write_enable(void) { MRAM_CS_LOW(); spi_transfer(0x06); MRAM_CS_HIGH(); } void mram_write(uint32_t addr, unsigned char *buf, uint16_t len) { mram_write_enable(); 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(); }这里有个关键点:MRAM 的写入是即时的,发完最后一个字节、片选拉高,数据就已经存进去了,不需要像 Flash 那样等待“写完成”状态。这意味着你可以在写完之后立刻读回校验,不用担心器件还在忙。我在实际项目里养成了一个习惯:每次写完关键数据,立刻读回比对,确认无误再更新“数据有效标志”。这个习惯帮我抓到过好几次因为电源波动导致的写入异常。
3.3 读操作与地址对齐
读命令是 0x03,流程和写类似,只是不需要写使能。读操作支持连续读,地址自动递增,可以一次把整块数据读出来。
void mram_read(uint32_t addr, unsigned char *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(0xFF); } MRAM_CS_HIGH(); }地址对齐方面,MRAM 没有页的概念,任意地址都能读写,这比 Flash 方便太多。但为了效率,我一般把数据结构设计成 4 字节对齐,这样读结构体时不会跨边界,代码也更好维护。
3.4 状态寄存器与写保护
MR25H40CDF 有一个状态寄存器,通过 0x05 命令读取,0x01 命令写入。状态寄存器的 bit1 是 WEL(写使能锁存),bit0 是 WIP(写进行中)。虽然 MRAM 写入是即时的,WIP 基本不会置位,但读一下状态寄存器确认 WEL 是否成功置位,是个好习惯。
状态寄存器里还有块保护位(BP0、BP1),可以硬件锁定部分地址区域。如果你的项目有“出厂参数区”需要防止误写,可以用这个功能把高地址段保护起来。配置方法是通过 0x01 命令写状态寄存器,把对应的 BP 位置 1。注意写状态寄存器本身也需要先发写使能命令。
提示:块保护一旦设置,只有发写状态寄存器命令才能解除。调试阶段建议先不设保护,等逻辑跑通再开启,否则容易把自己锁在外面。
4. 数据组织与掉电安全设计
4.1 数据结构布局
512KB 的空间怎么划分,直接决定了后续维护的难易。我在工业项目里一般这样分:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x00000~0x000FF | 设备信息区 | 序列号、固件版本、出厂日期 |
| 0x00100~0x001FF | 校准参数区 | 传感器零点、增益系数 |
| 0x00200~0x00FFF | 配置参数区 | 通信地址、采样周期等 |
| 0x01000~0x7FFFF | 循环日志区 | 按环形缓冲写入运行数据 |
循环日志区用环形缓冲的方式管理:维护一个写指针,每次写满一条记录指针后移,到末尾回绕到起始。这样不需要擦除,旧数据被新数据自然覆盖。MRAM 无限次写入的特性在这里发挥得淋漓尽致,我有个项目日志区每秒写一次,跑了两年多,累计写入超过 6000 万次,芯片依然正常。
4.2 掉电安全的三重保障
工业现场掉电是常态,数据安全必须设计到位。我总结了三层保障:
第一层是写入原子性。每条记录带一个 CRC 校验和,读的时候先校验 CRC,不通过就丢弃。这样即使写入过程中掉电导致记录不完整,也不会被误读。
第二层是双备份。关键参数区存两份,一份主一份备,写入时先写备份再写主,读取时如果主份 CRC 错误就切到备份。两份同时损坏的概率极低。
第三层是掉电检测。PIC18F86K90 有 LVD(低压检测)模块,可以配置成在电压跌落到阈值时触发中断。在中断里立刻停止写入、保存现场,利用电源电容的余电完成最后一次写入。MRAM 写入快,几微秒就能完成,这个时间窗口完全够用。
void __interrupt() isr(void) { if (PIR2bits.LVDIF) { PIR2bits.LVDIF = 0; g_power_fail = 1; // 置掉电标志 mram_write(LOG_ADDR, (unsigned char*)&last_record, sizeof(last_record)); } }这段中断代码要尽量短,只做最关键的保存动作。复杂的逻辑放到主循环里根据g_power_fail标志处理。
4.3 环形缓冲的指针管理
环形缓冲的写指针本身也需要掉电保护。我的做法是把写指针和它的反码一起存,读的时候校验两者是否互补,不互补就用备份指针。指针更新和记录写入的顺序是:先写记录数据,再更新指针。这样即使更新指针时掉电,最多是丢一条记录,不会破坏整个缓冲区的结构。
5. 常见问题排查与实战避坑
5.1 通信失败排查速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读 ID 全 0xFF | MISO 没接好或从机没供电 | 万用表测 MISO 对地电压,检查 VDD |
| 读 ID 全 0x00 | 时钟没输出或片选没拉低 | 逻辑分析仪抓 SCLK 和 CS# |
| 读 ID 偶尔错 | 时钟太快或走线干扰 | 降频到 5MHz 测试,检查走线 |
| 写入后读回不对 | 写使能没成功或地址错 | 读状态寄存器确认 WEL,核对地址 |
| 数据隔一段时间丢失 | 电源波动或未做掉电保护 | 加 LVD 检测,检查电源纹波 |
5.2 几个我踩过的坑
第一个坑是片选时序。早期我用硬件 SS 引脚,发现连续读写时偶尔出错。后来用逻辑分析仪抓波形,发现硬件 SS 在字节间会有毛刺,导致 MRAM 误判命令边界。改成软件片选,在每次操作前后加几个 NOP 延时,问题消失。
第二个坑是SPI 时钟相位。有次换了一批 MRAM 芯片,读 ID 正常但读数据错位。查了半天发现是新批次对 CKE 位的响应和旧批次略有差异,把 SPI 模式从 0 改成 3 就好了。这提醒我,批量生产前一定要用实际物料做完整测试,不能只信样品。
第三个坑是电源去耦。MRAM 在写入瞬间电流会有小波动,如果去耦电容离芯片太远,可能导致写入失败。我的做法是在 MRAM 的 VDD 引脚旁边放一个 0.1uF 加一个 1uF 电容,越近越好。
第四个坑是地址越界。MR25H40CDF 是 512KB,地址范围 0x00000~0x7FFFF。有次代码里地址计算用了 32 位无符号数,结果某个减法操作下溢,写到了 0x80000 以上,读回来全是 0xFF。后来加了地址范围断言,编译期就能发现这类问题。
5.3 调试工具与技巧
逻辑分析仪是调 SPI 的必备工具,没有它基本靠猜。我用的是一款支持 SPI 协议解码的,抓一次波形就能看到命令、地址、数据的完整解析,比对着示波器数时钟沿高效太多。
另外,我会在代码里加一个“自检”函数,上电时自动读 ID、写一个测试模式到保留区、读回校验。这个自检结果通过一个 LED 或串口输出,现场调试时一眼就能看出硬件是否正常。
注意:调试阶段不要把关键数据区和测试区混用。我一般留最后 4KB 作为测试区,自检只在这个区域操作,避免误擦生产数据。
6. 性能实测与场景扩展
6.1 实测写入速度
在 PIC18F86K90 跑 64MHz、SPI 时钟 10MHz 的条件下,我实测连续写入 256 字节耗时约 210 微秒,平均每个字节不到 1 微秒。这个速度对于 200ms 采集一次的场景绰绰有余。读取速度类似,256 字节约 205 微秒。如果 SPI 提到 16MHz,时间还能再缩短约 40%。
对比之前用 EEPROM 的方案,EEPROM 写一个字节要等 5ms 写周期,256 字节要 1.28 秒,根本没法做高频记录。这就是 MRAM 在这个场景下的核心价值。
6.2 这套方案还能用在哪
除了工业数据记录,这套 MRAM 加 PIC18 的组合还适合几类场景:
- 汽车电子:行车记录仪的碰撞前数据缓存,需要掉电瞬间保存
- 医疗设备:便携式监测仪的病人数据记录,要求高可靠性
- 智能仪表:电表水表的事件日志,写入频繁且不能丢
- 工业控制:PLC 的配方参数存储,需要频繁修改且掉电安全
如果你的项目对写入频率要求不高,或者成本极度敏感,那还是老老实实用 EEPROM 或 Flash。技术选型没有绝对的好坏,只有合不合适。
6.3 后续可以优化的方向
如果要把这套方案做得更完善,我会考虑几个方向:一是加一个文件系统层,把裸地址操作封装成文件读写接口,方便上层应用;二是做双 MRAM 冗余,进一步提高可靠性;三是把 SPI 改成 DMA 方式,减少 CPU 占用。不过对于大多数工业场景,当前这套已经足够稳了。
我在实际项目里最大的体会是:存储方案的选择,一定要从“最坏情况”出发。不是问“正常情况下能不能工作”,而是问“掉电、高温、振动的时候数据还在不在”。MRAM 加 PIC18 这套组合,就是我为“最坏情况”准备的答案。