1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 和 PIC
做过工业数据采集或者嵌入式存储的朋友应该都有体会,选存储介质这件事,往往比选主控还让人头疼。EEPROM 擦写寿命撑不住高频写入,Flash 又得整块擦除、写入前还得搬来搬去,掉电那一瞬间如果正好在写,数据说没就没。我手头这个项目就是典型的工业现场需求:设备要在 -40℃ 到 85℃ 的宽温区间里长期运行,采集到的标定参数、运行日志、故障快照需要频繁更新,而且现场断电是家常便饭,根本不给文件系统留缓冲时间。
这个背景下,我把目光投向了MR25H40CDF这颗 4Mbit 的 SPI MRAM。MRAM 的物理机制是靠磁性隧道结存储数据,写入过程不涉及电荷的充放电,所以它有几个特别对工业场景胃口的特性:写入前不需要擦除、字节级随机写入、写入速度接近 SRAM、擦写寿命理论上无限、掉电后数据能保持二十年以上。说白了,它把 EEPROM 的字节可写和 Flash 的容量、SRAM 的速度揉到了一起。
主控这边选的是PIC18LF47K42,Microchip 的 8 位机,带硬件 MSSP 模块可以跑 SPI,低功耗版本 LF 系列在 3.3V 下工作,和 MR25H40CDF 的供电区间正好匹配。有人会问为什么不上 STM32 或者 ESP32,原因很实际:这个项目对成本敏感,而且 PIC18 的外设配置直接、代码量小、长期供货稳定,工业客户最看重的就是十年后还能买到同型号芯片。PIC18LF47K42 的 MSSP 支持 SPI 主模式,最高时钟可以到系统时钟的四分之一,跑 MRAM 的 40MHz 上限绰绰有余。
提示:MRAM 虽然写入寿命长,但并不意味着可以无脑高频写。工业场景里仍然建议做写入合并和缓存,减少 SPI 总线占用,给其他外设留出时序余量。
1.2 整体架构长什么样
整个数据存储链路其实不复杂,但每一环都有讲究。PIC18LF47K42 作为 SPI 主机,通过 MSSP1 模块连接 MR25H40CDF,片选信号用普通 GPIO 控制。MRAM 的 HOLD 和 WP 引脚在这个项目里都拉高,因为我们不需要硬件写保护,写保护逻辑放在固件层做,这样更灵活。电源部分用了一颗 3.3V 的 LDO,MRAM 和主控共用同一路电源,避免上电时序不一致导致 SPI 通信异常。
数据流是这样的:传感器数据先进入 PIC18 的 RAM 缓冲区,固件按固定格式打包成记录,每条记录 32 字节,攒够 8 条也就是 256 字节后,一次性通过 SPI 写入 MRAM 的连续地址。读取的时候反过来,按记录索引算出地址,直接读对应长度的数据。这种设计的好处是减少了 SPI 事务次数,每次事务都有片选拉低、命令字节、地址字节、数据字节、片选拉高的开销,批量写能把这部分开销摊薄。
地址空间规划上,4Mbit 等于 512KB,我把它分成几个区:0x00000 到 0x00FFF 放设备标定参数,0x01000 到 0x0FFFF 放运行日志,0x10000 到 0x1FFFF 放故障快照,剩下的留作扩展。每个区之间留了保护间隔,防止越界写把关键参数覆盖掉。这个分区方案不是拍脑袋定的,是根据实际数据量和更新频率估算出来的,标定参数可能几个月才改一次,日志每天写几千条,故障快照只在异常时触发。
2. 硬件连接与 SPI 时序细节
2.1 引脚连接与电平匹配
MR25H40CDF 是 8 引脚 SOIC 封装,引脚定义很标准:CS、SO、SI、SCK、VDD、VSS、WP、HOLD。和 PIC18LF47K42 连接的时候,我用了 MSSP1 的默认引脚组,SCK 接 RC3,SDI 接 RC4,SDO 接 RC5,片选单独用 RA5 控制。这里有个细节要注意,PIC18 的 MSSP 引脚有多个映射选项,用哪个组取决于你其他外设的分配,我选这组是因为它不和 UART、I2C 冲突。
电平方面,两边都是 3.3V,直接连就行,不需要电平转换。但如果你用的是 5V 的 PIC18 型号,那就得加电平转换芯片或者用电阻分压,MRAM 的 IO 耐压不超过 VDD+0.3V,直接怼 5V 会烧。PCB 布局上,SPI 的四根线尽量走等长,SCK 作为时钟线要远离模拟信号,片选线如果走线长,建议加一个 100nF 的滤波电容靠近 MRAM 的 CS 引脚,防止毛刺误触发。
注意:MRAM 的 WP 和 HOLD 引脚内部有弱上拉,但工业环境建议外部再加 10k 上拉到 VDD,避免悬空导致意外进入写保护或保持状态。
2.2 SPI 模式与时钟配置
MR25H40CDF 支持 SPI 模式 0 和模式 3,也就是 CPOL=0/CPHA=0 或者 CPOL=1/CPHA=1。我选的是模式 0,因为 PIC18 的 MSSP 在模式 0 下配置最简单,SCK 空闲低电平,数据在上升沿采样。配置寄存器的时候,SSP1CON1 的 SSPM 位设成 0b0010 就是 SPI 主模式,CKP 位设 0 对应空闲低电平,CKE 位设 0 对应上升沿采样。
时钟频率的计算要看 PIC18LF47K42 的系统时钟。我用的外部晶振是 16MHz,经过 PLL 倍频到 64MHz 作为系统时钟,MSSP 的时钟源是系统时钟的四分之一,也就是 16MHz。这个频率远低于 MRAM 的 40MHz 上限,所以时序上很宽裕。实际测下来,16MHz 下 SPI 的波形很干净,上升沿和下降沿都在 5ns 以内,没有过冲。
如果你要把时钟拉到更高,比如 32MHz,那就得注意 PCB 走线的阻抗匹配和 MRAM 的建立保持时间。MR25H40CDF 的数据手册里写了,SCK 高电平和低电平的最小时间都是 10ns,对应最高 50MHz,但实际能跑多快取决于你的板子。我建议先用低速调通,再逐步提高,用示波器看波形,如果发现数据出错就降频。
2.3 片选控制与事务边界
SPI 通信里片选是事务的边界,每次读写都要拉低片选、发命令、发地址、传数据、拉高片选。MRAM 的片选建立时间和保持时间分别是 5ns 和 5ns,PIC18 的 GPIO 翻转速度足够快,但如果你在中断里操作片选,要注意中断延迟可能引入额外的等待。我的做法是把片选控制封装成宏,在 SPI 传输前后调用,确保每次事务的片选波形干净。
有个坑我踩过:早期调试的时候,片选拉高后立刻又拉低发下一条命令,结果 MRAM 没来得及完成内部状态机复位,导致第二条命令被忽略。后来在片选拉高后加了 100ns 的延时,问题就消失了。这个延时不是 MRAM 手册要求的,而是实际测试中发现的边界情况,说明手册参数是底线,实际应用要留余量。
3. 固件实现与核心代码拆解
3.1 SPI 底层驱动封装
底层驱动我写了三个函数:SPI_Init、SPI_Transfer、SPI_CS。SPI_Init 负责配置 MSSP1 的寄存器,SPI_Transfer 负责发送一个字节并返回接收到的字节,SPI_CS 负责控制片选。这三个函数是整个存储操作的基础,必须保证稳定可靠。
void SPI_Init(void) { TRISC3 = 0; // SCK 输出 TRISC4 = 1; // SDI 输入 TRISC5 = 0; // SDO 输出 TRISA5 = 0; // CS 输出 CS_HIGH(); SSP1STAT = 0x40; // CKE=0, 上升沿采样 SSP1CON1 = 0x02; // SPI 主模式, CKP=0 SSP1CON3 = 0x00; // 无中断 SSP1ADD = 0x03; // 时钟分频, 实际频率 = Fosc/4/(SSP1ADD+1) } uint8_t SPI_Transfer(uint8_t data) { SSP1BUF = data; while (!SSP1STATbits.BF); return SSP1BUF; }这里 SSP1ADD 的值决定了 SPI 时钟频率。PIC18LF47K42 的 MSSP 时钟源是 Fosc/4,再经过 SSP1ADD 分频。我设成 3,实际频率就是 64MHz/4/4 = 4MHz。这个频率下 MRAM 读写完全没问题,而且功耗低。如果你要提速,把 SSP1ADD 改小就行,但要注意最小不能小于 0,否则时钟会超过 MRAM 上限。
3.2 MRAM 读写命令实现
MR25H40CDF 的命令集很简洁,读数据是 0x03,写数据是 0x02,读状态寄存器是 0x05,写状态寄存器是 0x01。写操作之前不需要发写使能命令,这是 MRAM 和 EEPROM、Flash 最大的区别。读操作就是发 0x03,然后发 24 位地址,接着连续读数据,地址会自动递增。
void MRAM_Write(uint32_t addr, uint8_t *buf, uint16_t len) { 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]); } CS_HIGH(); __delay_us(1); } void MRAM_Read(uint32_t addr, uint8_t *buf, uint16_t len) { 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); } CS_HIGH(); }写函数里最后加了一个 1 微秒的延时,这是给 MRAM 内部写周期留时间。虽然 MRAM 写入很快,但片选拉高后内部状态机需要一点时间复位,这个延时是实测出来的,不加的话连续写有时候会丢数据。读函数不需要延时,因为读操作不改变内部状态。
3.3 数据记录格式与索引管理
数据记录我定义成 32 字节的结构体,包含时间戳、数据类型、数据负载和 CRC 校验。时间戳用 4 字节的 Unix 时间,数据类型 1 字节,数据负载 24 字节,CRC 2 字节,剩下 1 字节保留。这个格式的好处是每条记录自包含,读取的时候不需要依赖其他记录就能解析。
typedef struct { uint32_t timestamp; uint8_t type; uint8_t payload[24]; uint16_t crc; uint8_t reserved; } Record_t;索引管理用了一个简单的环形缓冲区策略。日志区从 0x01000 开始,每条记录 32 字节,总共可以存 0x0F000/32 = 1920 条。写指针存在 MRAM 的固定地址里,每次写入后更新。读的时候从写指针往前推,读最近 N 条。这种设计不需要文件系统,代码量小,可靠性高。
提示:写指针的更新要和数据写入放在同一个事务里,或者用双备份加 CRC 校验,防止更新写指针时掉电导致索引错乱。
4. 掉电保护与数据完整性
4.1 掉电检测与紧急写入
工业现场掉电是常态,所以掉电保护必须做。我的方案是用 PIC18 的 LVD 模块检测电源电压,当电压低于 3.0V 时触发中断,在中断里把 RAM 中未写入的数据紧急写入 MRAM。MRAM 的写入速度是纳秒级,从检测到掉电到完成写入,整个过程可以在几百微秒内完成,而电源的滤波电容能撑住几毫秒,时间绰绰有余。
LVD 的配置要注意阈值选择。PIC18LF47K42 的 LVD 有多个阈值可选,我选的是 3.0V,因为 LDO 的输出是 3.3V,留了 0.3V 的余量。如果阈值设得太低,比如 2.5V,可能电容已经撑不住了才触发中断;设得太高,比如 3.2V,正常波动就会误触发。3.0V 是实测下来最稳的。
4.2 CRC 校验与数据恢复
每条记录都有 CRC16 校验,读取的时候先算 CRC,和存储的值比对,不一致就标记为损坏。如果损坏的是关键参数,就从备份区恢复;如果是日志,就跳过。备份区我用了双备份加版本号的方式,两个备份区交替写,版本号大的有效。这样即使一个备份区在写入时掉电损坏,另一个还能用。
CRC 的计算我用的是查表法,提前生成 256 项的 CRC16 表存在 Flash 里,运行时直接查表,速度比逐位计算快很多。对于 32 字节的记录,查表法大概几十个时钟周期就能算完,对主循环的影响可以忽略。
4.3 写入均衡与寿命管理
虽然 MRAM 的擦写寿命理论上无限,但工业客户还是希望看到寿命管理策略。我的做法是日志区采用环形写入,写指针循环递增,这样每个地址的写入次数大致均匀。标定参数区因为更新频率低,直接原地覆盖,但每次写入前先读出来比对,如果数据没变就不写,减少不必要的写入。
这个策略的实际效果是,即使设备每天写 10 万条记录,十年下来每个地址的写入次数也就几百万次,远低于 MRAM 的 10^15 次写入寿命。所以从工程角度,MRAM 在这个场景下是“寿命过剩”的,但客户看到你有管理策略,心里会更踏实。
5. 调试踩坑与问题排查
5.1 SPI 通信失败排查流程
调试 SPI 最常见的问题就是读出来全是 0xFF 或者 0x00。我的排查顺序是这样的:先看片选有没有正常拉低拉高,用示波器或者逻辑分析仪抓波形;再看 SCK 有没有输出,频率对不对;然后看 MOSI 有没有数据,数据是不是你发的;最后看 MISO 有没有回应。这个顺序能覆盖 90% 的问题。
有一次我遇到读出来全是 0xFF,查了半天发现是 MISO 线虚焊,MRAM 根本没把数据送出来。还有一次是 SCK 频率太高,MRAM 采样跟不上,降频就好了。所以调试的时候,先用低速、简单的命令,确认链路通了再逐步加复杂度。
5.2 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 读出全 0xFF | MISO 未连接或 MRAM 未供电 | 检查硬件连接和电源 |
| 读出全 0x00 | 片选未拉低或命令错误 | 检查片选波形和命令字节 |
| 写入后读出不一致 | 写延时不足或时序违规 | 增加片选拉高后的延时 |
| 偶发数据错误 | 电源波动或干扰 | 加滤波电容,降低 SPI 频率 |
| 掉电后数据丢失 | 掉电检测阈值不当 | 调整 LVD 阈值,增加电容 |
5.3 实操心得与避坑建议
第一个心得是,MRAM 的写入虽然不需要擦除,但并不意味着可以随意乱写。每次写入都会消耗一定的能量,如果频繁写同一地址,虽然不会坏,但功耗会上去。工业设备对功耗敏感的话,还是要做写入合并。
第二个心得是,SPI 的片选控制尽量用硬件 GPIO,不要用软件模拟。软件模拟的片选在中断频繁的时候容易抖动,导致 MRAM 误判事务边界。我早期用软件模拟,后来改成硬件 GPIO 就稳了。
第三个心得是,MRAM 的 WP 和 HOLD 引脚不要悬空。虽然内部有弱上拉,但工业环境的干扰可能让它们误触发。我见过一个案例,HOLD 引脚被干扰拉低,导致 SPI 通信间歇性中断,查了两天才找到原因。
注意:调试 SPI 的时候,逻辑分析仪比示波器好用,因为能同时看多根线的时序关系。如果手头没有逻辑分析仪,用示波器的双通道也能凑合,但效率低很多。
6. 性能实测与优化方向
6.1 读写速度实测数据
在 4MHz SPI 时钟下,我实测了连续读写 256 字节的耗时。写入耗时约 520 微秒,读取耗时约 510 微秒。这个时间包括了片选、命令、地址和数据传输的全部开销。如果把 SPI 时钟提高到 16MHz,耗时可以降到 130 微秒左右。对于工业数据采集,这个速度完全够用,因为传感器采样率通常也就几十赫兹。
功耗方面,MRAM 在写入时的电流约 5mA,读取时约 3mA,待机时不到 10 微安。PIC18LF47K42 在 64MHz 下运行电流约 8mA,加上 MRAM 的功耗,整个存储子系统的功耗在 15mA 以内。对于电池供电的设备,可以通过降低 SPI 频率和增加休眠时间来进一步优化。
6.2 后续扩展思路
这个项目目前只用了 MRAM 的存储功能,其实 MR25H40CDF 还有一些特性可以挖掘。比如它的状态寄存器可以配置写保护区域,把标定参数区保护起来,防止固件 bug 误写。还有它的 HOLD 功能,可以在多主机场景下用来暂停通信,虽然这个项目用不到,但以后扩展多主控架构的时候可以用上。
软件层面,可以加一个简单的日志检索功能,按时间戳范围查找记录。因为记录是定长的,检索就是二分查找,速度很快。还可以加数据压缩,把重复的日志合并存储,提高存储利用率。这些扩展都不难,关键是先把基础功能做稳。
6.3 选型对比与适用场景
| 特性 | MR25H40CDF (MRAM) | EEPROM | NOR Flash |
|---|---|---|---|
| 写入前擦除 | 不需要 | 不需要 | 需要 |
| 写入速度 | 纳秒级 | 毫秒级 | 毫秒级 |
| 擦写寿命 | 10^15 | 10^6 | 10^5 |
| 字节可写 | 是 | 是 | 否 |
| 成本 | 高 | 低 | 中 |
| 适用场景 | 高频写入、掉电敏感 | 低频配置存储 | 大容量代码存储 |
从表里能看出来,MRAM 的优势在于高频写入和掉电保护,劣势是成本高。所以它适合用在对数据完整性要求高、写入频繁的场景,比如工业日志、汽车黑匣子、医疗设备参数记录。如果只是存个配置参数,一年改不了几次,那 EEPROM 就够了,没必要上 MRAM。
我个人在实际操作中的体会是,MRAM 这颗芯片用起来确实省心,不用管擦除、不用管寿命、不用管掉电,代码写起来也简单。但它的成本摆在那里,选型的时候还是要看具体需求。如果项目对成本极度敏感,那还是老老实实用 EEPROM 加掉电检测电容的方案。如果预算允许,MRAM 带来的可靠性提升是值得的。