1. 项目缘起与方案选型思路
工业现场的数据记录仪、PLC 扩展模块、智能电表这些设备有一个共同的痛点:频繁掉电。市电波动、设备急停、人为拉闸,随时可能把正在写入的关键参数打断。传统方案用 EEPROM 或者带后备电池的 SRAM 来扛这个问题,但 EEPROM 写入寿命只有百万次量级,擦写还慢;电池方案又要考虑电池寿命、低温失效、运输合规一堆麻烦事。我这两年做的一个工业采集终端项目,就卡在这个环节上,最后选定了MR25H40CDF 这颗 MRAM(磁性随机存储器)搭配PIC18F45K40单片机来做数据存储与读取,实测下来稳定性非常满意。
先说清楚这套组合到底解决什么问题。MR25H40CDF 是一颗 4Mbit(512KB)容量的 SPI 接口 MRAM,它的核心特性是写入无延迟、近乎无限次擦写、掉电数据不丢。跟 Flash 比,它不需要先擦后写,字节级随机写入,写一个字节和写一整页耗时几乎一样;跟 EEPROM 比,它没有擦写寿命焦虑,写入次数在 10^14 量级,基本可以认为写不坏。PIC18F45K40 是 Microchip 的 8 位单片机,自带 MSSP 模块可以配置成硬件 SPI,主频 64MHz,内置 EUSART、ADC、比较器等外设,工业温度范围版本很常见,价格也友好。两者通过 SPI 总线连接,MCU 做主机,MRAM 做从机,构成一个极简但极其可靠的非易失存储子系统。
为什么不用片内 EEPROM?PIC18F45K40 本身有 1KB 的片内 EEPROM,小数据量确实够用。但项目里要存的是带时间戳的采样记录,一天几千条,还要支持历史数据回读,1KB 根本不够,而且片内 EEPROM 写入会阻塞 CPU,擦写寿命也就十万次级别,长期高频写入迟早出问题。外挂 MRAM 之后,存储容量、写入速度、寿命三个维度同时解决,代价只是多一根 SPI 总线和几个 IO 口。
为什么不用 SD 卡或者 SPI Flash?SD 卡文件系统复杂,工业现场震动、湿度、掉电都会导致文件系统损坏,维护成本高。SPI Flash(比如 W25Q 系列)容量大、便宜,但写入前必须擦除整个扇区,掉电时正在擦除的扇区数据全丢,而且擦写寿命只有十万次,做高频数据记录很快就写废了。MRAM 的字节级写入和掉电原子性,恰好补上了这个短板。所以这套方案的核心定位就是:中等容量、高频写入、掉电绝对不能丢数据的工业嵌入式存储场景。
适合谁来参考这篇内容?如果你正在做工业数据采集、设备参数存储、黑匣子记录、或者任何需要“写不坏、掉电不丢”的嵌入式项目,这套组合值得认真考虑。下面我会从硬件连接、SPI 配置、读写驱动、掉电保护策略到实际踩坑,完整拆一遍。
2. MR25H40CDF 与 PIC18F45K40 核心细节解析
2.1 MR25H40CDF 的关键特性与引脚定义
MR25H40CDF 是 Everspin 的 4Mbit MRAM,采用 8 引脚 SOIC 封装,工作电压 2.7V 到 3.6V,SPI 模式支持 0 和 3,最高时钟频率 40MHz。它的存储阵列是 512K × 8 位,也就是 524288 个字节,地址线需要 19 位,所以读写命令里地址字段是 3 个字节(24 位,高 5 位忽略)。这一点跟常见的 24 系列 EEPROM 用 2 字节地址不同,写驱动的时候地址拼接千万别搞错,否则会写到错误的页去。
引脚方面,8 个脚分别是:CS(片选,低有效)、SO(从机输出,即 MISO)、SI(从机输入,即 MOSI)、SCK(时钟)、VDD、VSS,还有两个 NC 脚。注意 MR25H40CDF 没有 HOLD 和 WP 引脚,这意味着它不需要像 Flash 那样通过 WP 引脚做硬件写保护,写保护完全靠软件指令或者状态寄存器。这个设计简化了硬件连线,但也意味着软件上要自己保证不会误写。
状态寄存器里有一个WEL(Write Enable Latch)位,每次写操作之前必须先发 WREN(0x06)指令把 WEL 置 1,写完成后 WEL 自动清零。如果 WREN 没发或者发失败,后续的 WRITE 指令会被直接忽略,数据写不进去,而且不会报错。这是新手最容易踩的坑:读回来发现数据没变,查半天以为是 SPI 时序问题,其实是 WREN 没生效。我一般会在 WRITE 之后立刻读状态寄存器确认 WEL 已经清零,作为写入完成的间接标志。
2.2 PIC18F45K40 的 MSSP 模块与 SPI 主机配置
PIC18F45K40 的 MSSP 模块可以工作在 SPI 或 I2C 模式,做 SPI 主机时关键寄存器是 SSP1CON1、SSP1STAT、SSP1ADD。配置成 SPI 主机的基本步骤是:先设置 SSP1STAT 的 CKE 位和 SSP1CON1 的 CKP 位来确定 SPI 模式,然后设置 SSP1CON1 的 SSPM 位为 0b0000 或 0b0010 选择时钟频率,最后把 SSPEN 置 1 使能模块。MR25H40CDF 支持模式 0 和模式 3,我一般用模式 0,即 CKP=0、CKE=0,空闲时时钟低电平,数据在上升沿采样。
时钟频率的选择要算一下。PIC18F45K40 的 SPI 时钟来自系统时钟 Fosc,SSPM=0b0000 时是 Fosc/4,SSPM=0b0010 时是 Fosc/64。假设系统时钟 64MHz,Fosc/4 就是 16MHz,MR25H40CDF 最高支持 40MHz,16MHz 完全在范围内。但实际布线如果比较长,或者走线经过噪声环境,建议降到 Fosc/16 或 Fosc/64,先保证通信稳定再谈速度。我在一块四层板上用 16MHz 跑得很稳,但在一块两层的实验板上 16MHz 偶尔会读到 0xFF,降到 4MHz 就正常了,所以速度不是越高越好,要看板子。
片选信号我强烈建议用硬件片选,也就是用 MCU 的一个普通 GPIO 手动拉低拉高,而不是依赖 MSSP 模块的 SS 引脚自动控制。原因很简单:MR25H40CDF 的指令序列是“拉低 CS → 发指令 → 发地址 → 发/收数据 → 拉高 CS”,整个过程必须在一个 CS 低电平周期内完成。如果用自动 SS,中间一旦被中断打断,CS 可能被意外释放,MRAM 会认为指令结束,后续数据就全乱了。手动控制 GPIO 虽然多写两行代码,但时序完全可控,这是工业项目里更稳妥的做法。
2.3 SPI 通信协议与指令集要点
MR25H40CDF 的指令集很精简,常用的就几条:WREN(0x06)写使能、WRDI(0x04)写禁止、RDSR(0x05)读状态寄存器、WRSR(0x01)写状态寄存器、READ(0x03)读数据、WRITE(0x02)写数据。没有扇区擦除指令,因为 MRAM 不需要擦除,这是它跟 Flash 最大的区别。写数据时直接发 WRITE 指令加 3 字节地址,然后连续发数据字节,地址自动递增,可以一直写到 512KB 末尾。
读数据用 READ 指令,同样 3 字节地址,然后连续读,SO 引脚上会依次输出数据。这里有个细节:MRAM 的读是非破坏性的,读操作不会改变存储内容,也不需要刷新,所以可以放心地频繁读。另外,MRAM 的写入是原子性的,一个字节的写入要么完成要么没完成,不会出现写了一半的中间状态,这对掉电保护极其重要。
时序参数方面,CS 拉低到第一个 SCK 上升沿需要至少 5ns 的建立时间,SCK 周期最小 25ns(对应 40MHz),CS 拉高到下一次拉低需要至少 10ns。这些参数在 16MHz 下都有充足余量,不用特别担心。真正需要注意的是CS 拉高之后要等一段时间再发下一条指令,因为 MRAM 内部需要时间完成写入。虽然 MRAM 写入很快,但状态寄存器里的 WIP(Write In Progress)位还是建议查一下,或者至少延时几个微秒,确保上一次写入彻底完成。
3. 硬件连接与实操过程详解
3.1 硬件连线与去耦电容布置
先给一张接线对照表,这是最基础的,接错了后面全白搭。
| PIC18F45K40 引脚 | 方向 | MR25H40CDF 引脚 | 说明 |
|---|---|---|---|
| RC3(SCK) | 输出 | SCK(脚 6) | SPI 时钟 |
| RC5(SDO) | 输出 | SI(脚 5) | 主机输出从机输入 |
| RC4(SDI) | 输入 | SO(脚 2) | 主机输入从机输出 |
| RB0(GPIO) | 输出 | CS(脚 1) | 手动片选,低有效 |
| 3.3V | 电源 | VDD(脚 8) | 供电 |
| GND | 地 | VSS(脚 4) | 共地 |
注意 PIC18F45K40 的 SDO 是 RC5,SDI 是 RC4,别跟其他型号搞混。MR25H40CDF 的 SI 接 MCU 的 SDO,SO 接 MCU 的 SDI,这是标准 SPI 交叉接法。CS 我用了 RB0,你也可以用任何空闲 GPIO,只要在代码里对应改掉就行。
去耦电容这块必须强调。MR25H40CDF 的 VDD 和 VSS 之间要放一个0.1μF 陶瓷电容,尽量靠近芯片引脚,走线越短越好。如果电源纹波比较大,再并一个 1μF 或 10μF 的钽电容。我遇到过一块板子因为去耦电容离得太远,SPI 读数据偶尔出错,换了位置立刻就好。工业现场电源噪声大,这个电容不是可选项,是必选项。另外,SPI 的四根信号线如果走线超过 10cm,建议在 SCK 和 CS 上串 22Ω 到 33Ω 的电阻做阻抗匹配,减少反射。
3.2 SPI 初始化代码与参数计算
下面这段是 PIC18F45K40 的 SPI 主机初始化代码,用 XC8 编译器,直接可以抄。
#include <xc.h> #define CS_PIN LATBbits.LATB0 #define CS_TRIS TRISBbits.TRISB0 void SPI_Init(void) { // 配置 CS 为输出,默认拉高 CS_TRIS = 0; CS_PIN = 1; // 配置 SCK、SDO 为输出,SDI 为输入 TRISCbits.TRISC3 = 0; // SCK TRISCbits.TRISC5 = 0; // SDO TRISCbits.TRISC4 = 1; // SDI // MSSP1 配置为 SPI 主机,模式 0 SSP1STAT = 0x00; // CKE=0,SMP=0(模式 0) SSP1CON1 = 0x00; // 先清零 SSP1CON1bits.CKP = 0; // 空闲时钟低 SSP1CON1bits.SSPM = 0b0000; // Fosc/4 SSP1CON1bits.SSPEN = 1; // 使能 MSSP // 清空接收缓冲 volatile unsigned char dummy = SSP1BUF; (void)dummy; }时钟频率的计算:假设系统时钟配置为 64MHz,SSPM=0b0000 对应 Fosc/4,即 16MHz。SPI 时钟周期 62.5ns,MR25H40CDF 最小 SCK 周期 25ns,满足要求。如果系统时钟是 16MHz,Fosc/4 就是 4MHz,更保守,也完全够用。我一般先用 4MHz 调通,再逐步往上提,确认稳定后再定最终频率。
SPI 收发一个字节的函数也很简单,核心是写 SSP1BUF 然后等 BF 位(SSP1STATbits.BF)置 1,再读 SSP1BUF。
unsigned char SPI_ExchangeByte(unsigned char data) { SSP1BUF = data; while (!SSP1STATbits.BF); // 等待收发完成 return SSP1BUF; }这个函数是阻塞式的,在工业项目里如果 SPI 速度够快,阻塞几微秒完全可以接受。如果不想阻塞,可以用中断方式,但代码复杂度会上升,对于 MRAM 这种高速器件,阻塞式反而更简单可靠。
3.3 MRAM 读写驱动实现
写数据之前必须先发 WREN,这是铁律。下面这个函数封装了 WREN 加 WRITE 的完整流程。
void MRAM_WriteEnable(void) { CS_PIN = 0; SPI_ExchangeByte(0x06); // WREN CS_PIN = 1; } void MRAM_Write(uint32_t addr, unsigned char *buf, uint16_t len) { MRAM_WriteEnable(); CS_PIN = 0; SPI_ExchangeByte(0x02); // WRITE 指令 SPI_ExchangeByte((addr >> 16) & 0xFF); // 地址高字节 SPI_ExchangeByte((addr >> 8) & 0xFF); // 地址中字节 SPI_ExchangeByte(addr & 0xFF); // 地址低字节 for (uint16_t i = 0; i < len; i++) { SPI_ExchangeByte(buf[i]); } CS_PIN = 1; // 等待写入完成,MRAM 很快,延时 1us 足够 __delay_us(1); }读数据更简单,不需要 WREN。
void MRAM_Read(uint32_t addr, unsigned char *buf, uint16_t len) { CS_PIN = 0; SPI_ExchangeByte(0x03); // READ 指令 SPI_ExchangeByte((addr >> 16) & 0xFF); SPI_ExchangeByte((addr >> 8) & 0xFF); SPI_ExchangeByte(addr & 0xFF); for (uint16_t i = 0; i < len; i++) { buf[i] = SPI_ExchangeByte(0x00); // 发 dummy 收数据 } CS_PIN = 1; }地址拼接这里再强调一次:MR25H40CDF 是 19 位地址,但指令格式要求发 3 个字节,高 5 位是无关位,一般填 0。所以(addr >> 16) & 0xFF取的是地址的第 16 到 23 位,对于 512KB 空间,最高位到第 18 位,第 19 到 23 位始终是 0。如果你不小心把地址算错,比如用了 2 字节地址格式,那读出来的数据会完全错位,而且很难查。
3.4 掉电保护与数据完整性策略
MRAM 本身掉电不丢数据,但正在写入的那一瞬间掉电怎么办?MRAM 的写入是原子性的,一个字节的写入要么完成要么没完成,不会出现半写状态。但如果你一次写多个字节,比如写一条 32 字节的记录,写到第 16 个字节时掉电,那前 16 个字节写进去了,后 16 个没写,这条记录就不完整了。所以数据完整性要靠软件层面的双缓冲或日志结构来保证。
我的做法是:把存储区分成两个区域,一个叫数据区,一个叫索引区。数据区按固定大小的记录块顺序写入,每条记录带一个 CRC 校验和。索引区存当前写入位置和记录计数。每次写新记录时,先写数据区,CRC 算好一起写进去,然后再更新索引区。如果掉电发生在写数据区时,索引区还是旧值,上电后从索引区指向的位置重新写就行,旧记录被覆盖,不会影响已确认的数据。如果掉电发生在更新索引区时,因为索引区只有一个字节或几个字节,MRAM 的原子性保证它要么是旧值要么是新值,不会中间态。
还有一种更简单的做法:用双份存储加标志位。关键参数存两份,每份带一个有效标志。写的时候先写第二份,再写第一份,读的时候优先读第一份,如果第一份 CRC 错就读第二份。这样即使写第一份时掉电,第二份还是好的。这个方法代码简单,适合参数不多的情况。
注意:MRAM 虽然写入快,但也不要在中断里频繁写。SPI 通信本身需要时间,如果在高优先级中断里写 MRAM,可能阻塞其他中断,导致系统实时性下降。建议把写操作放到主循环或者低优先级任务里,用标志位触发。
4. 常见问题与排查技巧实录
4.1 SPI 通信失败排查速查表
| 现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 读回全是 0xFF | CS 没拉低或接线错 | 用示波器看 CS 和 SCK | 检查 CS 引脚定义和接线 |
| 读回全是 0x00 | MISO 没接或 MRAM 没供电 | 万用表测 VDD 电压 | 检查供电和 MISO 连线 |
| 数据偶尔错位 | SPI 时钟太快或走线太长 | 降低时钟频率测试 | 降速或加阻抗匹配电阻 |
| 写入后读回没变 | WREN 没生效 | 读状态寄存器 WEL 位 | 确保 WREN 在 WRITE 之前发送 |
| 地址错乱 | 地址字节数不对 | 检查地址拼接代码 | 确认用 3 字节地址格式 |
| 上电后数据丢失 | 去耦电容不足或电源跌落 | 示波器看 VDD 纹波 | 加 0.1μF 和 10μF 电容 |
这张表是我实际调试中总结出来的,基本上覆盖了 90% 以上的问题。其中“写入后读回没变”是最常见的,新手往往以为是 SPI 时序问题,其实十有八九是 WREN 没发或者发完 WREN 之后 CS 拉高太早,导致 WREN 没被 MRAM 识别。WREN 是一个独立指令,发完之后必须拉高 CS 才能生效,然后再拉低 CS 发 WRITE。如果你把 WREN 和 WRITE 放在同一个 CS 低电平周期里,MRAM 是不认的。
4.2 实操心得与避坑经验
第一个坑:CS 拉高之后不要立刻发下一条指令。MRAM 在 CS 上升沿之后需要一点时间完成内部操作,虽然 datasheet 上写的是纳秒级,但实际板子上如果电源不稳,这个时间会变长。我的做法是在 CS 拉高之后加一个__delay_us(1),1 微秒对系统性能几乎没影响,但能避免很多偶发问题。
第二个坑:不要用 SPI 自动片选。前面提过,这里再强调一次。PIC 的 MSSP 模块有 SS 引脚,可以自动控制片选,但那个片选在字节之间会跳动,不适合 MRAM 这种需要连续 CS 低电平的器件。手动 GPIO 控制虽然多写代码,但时序完全可控。
第三个坑:写入数据量大的时候要分块。虽然 MRAM 支持连续写直到地址末尾,但如果你一次写几 KB,中间如果被中断打断,CS 可能被意外拉高,导致写入中断。我的做法是每 256 字节分一块,每块写完拉高 CS,再重新发 WREN 和 WRITE。这样即使中间被打断,最多丢一块,不会影响整个数据区。
第四个坑:上电初始化要加延时。MRAM 上电后需要一段时间稳定,datasheet 上写的是 1ms 左右。我一般在上电后延时 10ms 再初始化 SPI 和访问 MRAM,确保电源稳定。这个延时在工业设备上完全不是问题,但能避免上电瞬间的误操作。
4.3 性能实测与优化建议
我在这套组合上做过实测:SPI 时钟 16MHz,写 256 字节耗时约 130μs,读 256 字节耗时约 128μs。换算下来写入速度约 1.9MB/s,读取速度约 2MB/s。这个速度对于工业数据记录完全够用,一天记录 10 万条 32 字节的记录,总数据量 3.2MB,写入时间加起来不到 2 秒,分散到全天更是可以忽略不计。
如果要进一步优化,可以把 SPI 时钟提到 20MHz 甚至更高,但要看板子布线质量。另外,读操作可以用 DMA 或者中断方式,减少 CPU 占用。不过对于大多数工业应用,阻塞式读写已经足够,没必要过度优化。
功耗方面,MR25H40CDF 的待机电流典型值 1mA 左右,写入电流约 10mA,读取电流约 5mA。对于电池供电的设备,可以在不访问时把 CS 拉高,MRAM 会自动进入低功耗待机模式。PIC18F45K40 本身也有多种低功耗模式,配合起来可以做到整体功耗很低。
5. 方案扩展与个人体会
这套 MRAM 加 PIC 的组合,除了做数据记录,还可以扩展成参数存储区、黑匣子、固件配置区。比如把设备的校准参数、网络配置、运行日志都放在 MRAM 里,掉电不丢,写入无寿命焦虑,维护起来非常省心。如果容量不够,MR25H40CDF 是 512KB,还有更大容量的 MRAM 型号可以选,接口和指令基本兼容,换芯片只需要改地址位宽。
我在实际使用中发现,MRAM 最大的价值不是速度快,而是让软件设计变简单了。用 Flash 的时候,你要考虑擦除块大小、写前擦除、磨损均衡、掉电保护,代码复杂且容易出 bug。用 MRAM 之后,写就是写,读就是读,不需要擦除,不需要均衡,掉电原子性天然保证,软件层面只需要处理多字节记录的完整性问题。这种简化带来的开发效率提升和后期维护成本降低,远比芯片本身的差价有价值。
最后分享一个小技巧:如果你不确定 MRAM 是否正常工作,可以先写一个字节到地址 0x00000,然后读回来对比。如果读回来一致,再写一个不同的值到地址 0x7FFFF(最后一个字节),再读回来。这两个地址覆盖了地址空间的最高位和最低位,能快速验证地址线是否全部正常。这个方法比写一大段数据再对比要快得多,调试初期非常实用。