☰
MRAM+PIC18实现工业级掉电存储:SPI驱动与数据保护实践
2026/10/4 19:23:07 网站建设 项目流程

做工业设备的人,基本都跟存储打过交道。跑产线的工控板、电表、充电桩、医疗仪器、采集器,都逃不开一个问题:把参数、日志、运行状态存下来,系统掉电重启之后还不能丢。以前大家习惯用EEPROM或者NOR Flash,做久了你会发现这两类器件在工业场景里都有点“别扭”:写得太慢、寿命有限、掉电时正在擦写还容易损坏数据。

我这边最近一个项目正好碰到这个需求,最后选了MR25H40CDF 与 PIC18F86J10这套组合:一颗 Everspin 的 4Mbit SPI MRAM,加一颗 Microchip 的 8 位 MCU,把“存储和读取数据”这件事做得很干净。这篇文章就把这套方案的选型逻辑、SPI 时序、PIC18 侧的驱动实现、工业现场的掉电保护设计和排错经验完整拆开讲一遍。适合正在做工业控制板、独立数据记录器,或者想从裸机驱动过渡到嵌入式 Linux 下 SPI 存储的人参考。

1. 为什么要用 MRAM + PIC18 这个组合做工业存储

1.1 传统 EEPROM 和 NOR Flash 的四个坑

先说清楚我为什么不想再用传统的非易失存储。EEPROM 和 NOR Flash 在消费类产品里没问题,但放到工业设备上,短板非常明显:

  • 擦写寿命有限。普通 SPI EEPROM 擦写寿命大概 100 万次,NOR Flash 更是只有 10 万次左右。看着不少,但工业设备一旦做数据记录,比如每 5 秒写一条运行状态,算下来一年就是 600 多万次写入。EEPROM 在这种场景下几个月就报废。
  • 写入慢,擦除更慢。NOR Flash 写入前必须先擦除块,一个 4KB 扇区擦除经常要几百毫秒。系统掉电的瞬间,如果你正在等擦除,数据基本救不回来。
  • 页对齐和坏块问题。NOR Flash 有页的概念,写数据要按页规划,跨越页边界还得特殊处理,管理成本不低。
  • 掉电损坏逻辑复杂。在写操作过程中断电,这一块数据可能处于半擦半写的状态。为了防这个,得做双备份、校验、回滚,软件工作量翻倍。

这些问题不是没有解决方案,但方案基本都是“用软件兜底”。于是我开始考虑有没有硬件层面直接规避掉这些问题的存储介质。

1.2 MRAM 为什么是“工业掉电存储”的答案

MRAM 这个英文全称是 Magnetoresistive Random Access Memory,磁阻随机存取存储器。原理上和 EEPROM/Flash 完全不同:数据不是靠电荷存储,而是靠磁性隧道结(MTJ)中自由层的磁化方向来记录。写数据时,通过电流改变磁化方向,物理上不存在“擦除-写入”这种两阶段操作,也不存在电荷泄漏的问题。

这带来的实际好处有三个:

  • 寿命基本无限。MRAM 的擦写耐久度在 10^12 次以上,很多资料直接写“unlimited”,你不需要考虑磨损均衡(wear leveling)这件事。
  • 写入极快。写一个字节的物理时间在纳秒级,实际速度瓶颈完全在 SPI 接口上。更重要的是,写入不需要等待擦除,没有“先擦后写”的延迟窗口。
  • 读写对称。读和写的速度一致,不像 NOR Flash 那样读得快、写得慢,程序逻辑可以做得非常简单。

我用一个生活化的类比:NOR Flash 像一块需要先擦掉整片黑板才能重新写的粉笔板,而 MRAM 像一块磁性白板,写字就是改变磁极方向,写多少次都不磨损。MR25H40CDF就是这次用的那颗 SPI 接口 MRAM,容量 4Mbit,也就是 512KB,工业级温度范围,引脚和命令风格跟普通 SPI NOR Flash 高度相似,迁移成本很低。

1.3 PIC18F86J10 在组合中的角色

PIC18F86J10 是 Microchip 的 8 位 PIC18 系列单片机,工作电压范围宽,带硬件 MSSP 模块,可以直接跑 SPI 主模式。在这套组合里,我把它定位成“存储管理从机”:对上,通过 UART、CAN 或者 Modbus 接收主控下发的数据;对下,通过 SPI 操作 MR25H40CDF,负责把数据可靠地写进去、读出来。

为什么不直接塞给一颗 Cortex-M 内核的单片机?很多工业场景其实不需要那么高的主频,反而看重功耗、启动时间和长期供货稳定性。PIC18F86J10 在 3.3V 下就能跑,外部电路简单,XC8 编译器用起来也顺手,对小批量、长生命周期的工业产品非常合适。另外,它的 MSSP 模块是硬 SPI,不是靠 GPIO 模拟的,时钟极性、采样沿都可以配,驱动 MRAM 这种对时序有要求的器件更省心。

1.4 这个组合实际能做什么

我用下来,最典型的应用场景有这几类:

  • 设备参数存储:把 IP 地址、通信波特率、校准系数、开关配置存进 MRAM,掉电不丢,上电直接读。
  • 事件日志记录:故障码、报警时间、操作记录,持续追加写入,第二天回来还能分析问题。
  • 高频数据采集缓存:比如电表每秒记录一次瞬时功率,MRAM 可以扛住这个写入频率。
  • 固件升级引导备份:把升级包暂存到 MRAM,校验通过再刷进主 Flash,避免升级断电变砖。

这些场景的共同要求是:写入频繁、掉电不能坏、读取要快、软件逻辑要简单。MR25H40CDF 加 PIC18F86J10正好命中。

2. MR25H40CDF 的核心特性与 SPI 命令集拆解

2.1 关键参数速览

拿到一颗新芯片,我习惯先列一张参数表,把重要的数据手册信息钉在眼前。MR25H40CDF 的关键参数如下:

参数数值说明
容量4Mbit512K x 8bit,地址范围 0x000000 ~ 0x07FFFF
接口SPI支持标准 SPI、双倍速率读等
工作电压2.7V ~ 3.6V3.3V 系统直接供电
工作温度-40°C ~ +85°C工业级(具体以订购后缀为准)
数据保持20 年以上掉电后数据不丢
擦写寿命10^12 次以上不需要做磨损均衡
最大时钟40MHz 级别实际使用建议降额,见后文

这颗芯片的逻辑组织结构是线性的字节数组,地址用 3 字节表示。跟 NOR Flash 最大的不同就是没有任何页、扇区、块的概念,写任何一个地址都是直接写,不需要先擦除。这是我选择它的核心原因之一。

2.2 引脚与最小系统连接

MR25H40CDF 常见的小封装是 8 引脚,引脚功能跟 SPI NOR Flash 很像,但有两个脚要特别注意。

引脚名方向说明
CS#输入片选,低有效。整个命令期间必须保持低电平
SCK输入SPI 时钟
SI输入MOSI,数据输入
SO输出MISO,数据输出
WP#输入写保护,低电平有效
HOLD#输入暂停通信,低电平有效
VCC电源2.7V~3.6V,建议并 100nF 和 10uF 电容
GND地接地

WP# 和 HOLD# 这两个脚是最容易踩坑的地方。WP 拉低时,状态寄存器里的 BP 位会被锁住,写使能命令虽然能发,但写操作不生效。HOLD 拉低时芯片会忽略 SPI 时钟,如果你悬空没处理,有时候干扰会导致芯片莫名卡住。我建议:WP# 直接上拉到 VCC,HOLD# 也上拉到 VCC,除非你真的要用硬件写保护和暂停功能,否则不要给它们悬空的机会。

上电时序方面的经验:SCK 在上电期间要保持确定电平,不能让引脚悬空产生随机跳变,否则芯片可能误判成一个命令。我习惯在 SCK、SI、CS# 上都加 10kΩ 下拉电阻(SCK、SI 下拉,CS# 上拉),这样上电默认就是“未选中、无时钟”的稳定状态。

2.3 读、写、状态操作时序

MRAM 的时序逻辑和 SPI NOR Flash 非常像,命令格式是同一个路子。下面用最常用的三个操作举例。

读数据(0x03),时序是:

  1. 拉低 CS#。
  2. 发送 0x03(读命令)。
  3. 发送 3 字节地址,高位在前。
  4. 持续发送空字节(0x00),SCK 每来一个时钟,SO 上就输出一个字节数据。
  5. 读完最后一个字节后拉高 CS#。

写数据(0x02),时序是:

  1. 拉低 CS#,发送 0x06(写使能 WREN),拉高 CS#。
  2. 再次拉低 CS#。
  3. 发送 0x02(写命令)。
  4. 发送 3 字节地址。
  5. 发送 1 到 N 个数据字节。
  6. 拉高 CS#。

这里的关键是写使能和写数据必须分成两个 CS 周期:WREN 命令自己一个低电平窗口,结束要求拉高一次,然后才能发 WRITE。如果图省事把 0x06 和 0x02 连续发,芯片不会理你,这个细节在我看过的不少代码里都出过错。

读状态寄存器(0x05),时序是:拉低 CS#,发送 0x05,然后读一个字节,拉高 CS#。状态寄存器里主要是 WEL(写使能锁存)和 BP(块保护)位。MRAM 由于写操作瞬时完成,一般不需要像 NOR Flash 那样死等 WIP 位清零,但读一下状态确认 WEL 已经置位,依然是排查问题的好习惯。

2.4 命令集速查表

我把 MR25H40CDF 常用的命令整理成一张表,写驱动的时候对照着看:

命令字节码操作说明
WREN0x06写使能,写任何数据前必须执行
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器(配置 BP 保护位)
READ0x03普通读,读速度低但兼容性好
FAST_READ0x0B快速读,多一个 dummy 字节
WRITE0x02写数据
RDID0x9F读 JEDEC ID,用于识别器件
SLEEP0xB9进入休眠模式
WAKE0xAB唤醒

RDID 这个命令做上电自检特别好用。主控可以先发 0x9F,读回厂商 ID 和器件 ID,确认 SPI 通路、接线、供电都正常了,再继续后续操作。这相当于给驱动加了一个“自检握手”,能省下很多排除硬件故障的时间。

2.5 和 NOR Flash 兼容但不要照搬

因为 MR25H40CDF 的命令集看起来跟 SPI NOR Flash 很像,很多人直接把 Flash 驱动拿过来改改,但有两个坑要注意:

  • 不要死等 WIP。NOR Flash 写完要反复查状态寄存器的 WIP 位,MRAM 写是瞬时的,你查半天发现 WIP 永远是 0,但代码逻辑如果写成“等待 WIP 直到超时”,某些实现会在超时后报错。正确做法是写完直接进行下一步,或者只做一次 WEL 检查。
  • 页边界规则不同。NOR Flash 写命令受页大小限制,跨页要拆成多次命令;MRAM 没有页边界,连续写多少字节都行,地址会线性递增。当然,为了数据组织方便,我还是建议分批写,比如每批 256 字节,但这纯粹是软件层面的选择,不是硬件限制。

3. PIC18F86J10 驱动实现:从寄存器到可用的 API

3.1 硬件接线设计

我这边用的是 3.3V 供电,PIC18F86J10 和 MR25H40CDF 共用一个电源,逻辑电平天然匹配,不需要电平转换芯片。具体接线参考这张表:

PIC18F86J10 引脚MR25H40CDF 引脚说明
VDD(3.3V)VCC电源,并联 100nF + 10uF 电容
VSSGND共地
SCK 输出SCKSPI 时钟,项目里串一个 22Ω 电阻
SDO 输出SIMOSI 方向:MCU -> MRAM
SDI 输入SOMISO 方向:MRAM -> MCU
任意 GPIOCS#用普通 GPIO 控制,不要用 MSSP 自带的 SS
3.3V 上拉WP#直接拉高
3.3V 上拉HOLD#直接拉高

SCK 上串 22Ω 的小电阻是我做 EMC 测试攒下来的经验。SPI 时钟上升到沿太陡,辐射会变强,串一个电阻能抑制振铃,代价是信号边沿稍微变缓,对几 MHz 的速率完全没影响。

3.2 MSSP SPI 主模式初始化

PIC18F86J10 的 MSSP 模块配置 SPI 主模式,核心寄存器是 SSPCON1 和 SSPSTAT。下面的代码基于 MPLAB XC8,寄存器名直接对应 PIC18F86J10 的数据手册。

#include <xc.h> #define F_OSC 32000000UL void spi_init(void) { // 1. 先配置引脚方向:SCK、SDO 为输出,SDI 为输入,CS 为输出 // 具体 TRIS 位请按照实际硬件原理图修改 TRIS_SCK = 0; // SCK 输出 TRIS_SDO = 0; // MOSI 输出 TRIS_SDI = 1; // MISO 输入 TRIS_CS = 0; // CS 输出 MRAM_CS = 1; // 片选默认拉高,不选中器件 // 2. 配置 MSSP 为 SPI 主模式,Mode 0 // CKP=0, CKE=0 -> 时钟空闲为低,数据在上升沿采样 // SMP=1 -> 输入采样在数据输出周期的中间,保证稳定 SSPSTAT = 0x40; // SMP=1, CKE=0 SSPCON1 = 0x20; // SSPEN=1, CKP=0, SSPM=0000 主模式 FOSC/4 // 3. 配置 SPI 时钟分频 // 实际 SPI 时钟 = FOSC / (4 * (SSPADD + 1)) // FOSC=32MHz, SSPADD=3 -> 2MHz SSPADD = 3; }

注意SSPCON1的最低 4 位是主模式选择,0x20对应的是SSPEN=1且主模式时钟 FOSC/4。如果你需要更慢的时钟,可以修改SSPADD,或者把SSPCON1的低 4 位改成其他主模式,这一点在数据手册的 MSSP 章节写得很详细。

3.3 最底层的 SPI 读写字节函数

MSSP 模块发送和接收共用SSPBUF寄存器。每次发送一个字节的同时,SCK 会驱动 MISO 采样,所以“读一个字节”本质上就是“发送一个空字节并读取返回”。

unsigned char spi_write_read(unsigned char data) { SSPBUF = data; // 写入数据,启动传输 while (!PIR1bits.SSPIF); // 等待传输完成标志置位 PIR1bits.SSPIF = 0; // 软件清除标志位 return SSPBUF; // 返回接收到的数据 }

这个函数是整个驱动的地基,后面所有 MRAM 操作都建立在它上面。有一点要提醒:MSSP 的 FIFO 很浅,不要在 SSPIF 没置位前连续写 SSPBUF,否则会覆盖上一次传输。这也是为什么我用while循环死等,而不是丢进去就不管。

3.4 MRAM 读写驱动封装

有了底层函数,封装 MRAM 的读、写、写使能就很简单了。

#define MRAM_CS LATCbits.LATC0 // 请按实际硬件修改 void mram_cs_low(void) { MRAM_CS = 0; } void mram_cs_high(void) { MRAM_CS = 1; } void mram_write_enable(void) { mram_cs_low(); spi_write_read(0x06); // WREN mram_cs_high(); } unsigned char mram_read_byte(unsigned long addr) { unsigned char val; mram_cs_low(); spi_write_read(0x03); // READ spi_write_read((addr >> 16) & 0xFF); // 地址高字节 spi_write_read((addr >> 8) & 0xFF); // 地址中字节 spi_write_read(addr & 0xFF); // 地址低字节 val = spi_write_read(0x00); // 读一个字节 mram_cs_high(); return val; } void mram_write_byte(unsigned long addr, unsigned char data) { mram_write_enable(); // 写数据前必须 WREN mram_cs_low(); spi_write_read(0x02); // WRITE spi_write_read((addr >> 16) & 0xFF); spi_write_read((addr >> 8) & 0xFF); spi_write_read(addr & 0xFF); spi_write_read(data); mram_cs_high(); }

如果要做连续多字节读写,就把读和写的部分放到循环里,CS# 在整个操作期间保持低电平,不要每读一个字节都重新拉一次 CS。比如写 64 字节:

void mram_write_buffer(unsigned long addr, unsigned char *buf, unsigned int len) { unsigned int i; mram_write_enable(); mram_cs_low(); spi_write_read(0x02); spi_write_read((addr >> 16) & 0xFF); spi_write_read((addr >> 8) & 0xFF); spi_write_read(addr & 0xFF); for (i = 0; i < len; i++) { spi_write_read(buf[i]); } mram_cs_high(); }

这里有一个细节:地址是按字节递增的。如果写入长度导致地址跨过 0x07FFFF,会回卷到 0x000000。工业应用里我会在驱动外面做边界检查,不允许写入越界。

3.5 数据存储 API 与使用示例

驱动层做好之后,我还会在应用层封装几个带“语义”的接口,而不是让业务代码直接调用mram_write_byte。比如:

typedef struct { unsigned int magic; // 固定魔数,用于识别数据块 unsigned int version; // 数据结构版本号 unsigned long timestamp; // 时间戳 unsigned int crc; // CRC16 校验 unsigned char data[64]; // 业务数据 } app_block_t;

写一个块的操作是:填结构体,算 CRC,然后调用mram_write_buffer。读一个块的操作是:读回来,检查 magic 和 CRC,通过才返回数据,不通过就报错。这一步极其重要,它把存储层的可靠性问题在应用层做了兜底。

4. 工业场景的数据完整性设计与异常处理

4.1 掉电瞬间保存数据:不只是靠 MRAM 快写

MRAM 写入速度快,不代表系统设计可以偷懒。掉电保存的关键在于:你要在电压降到 MCU 无法工作之前,完成“检测掉电 -> 写数据 -> 数据落盘”整个链条。

我的做法分三部分:

  • 电源上加大容量储能电容。用一个大一点的电解电容或者超级电容,保证掉电后系统还能维持几十毫秒的工作时间。具体容量取决于你要写多少数据。写 64 字节 SPI 数据,算上开销也就几百微秒,但 MCU 检测、中断响应、CRC 计算都需要时间,我一般留 20ms 以上余量。
  • 用电源监测芯片或 MCU 的 LVD(低电压检测)模块。当电压掉到阈值(比如 3.0V),立即触发中断。在中断服务函数里,把最重要的参数写进 MRAM。
  • 只保存关键数据。掉电时间窗口有限,不要指望把整个运行缓存都写进去。我的原则是:核心参数必须存,日志数据能丢就丢,等下次上电再补。

实际测试中,我甚至故意在写数据的瞬间断电,反复做几百次,MRAM 里面的数据从来没有出现半截损坏的情况。这一点换成 NOR Flash,大概率会出问题。

4.2 分区管理与磨损无关

因为 MRAM 没有磨损问题,分区设计可以做得比 Flash 简单得多。我用三个区:

  • 0x000000 ~ 0x00FFFF:系统配置区,保存参数和校准数据。
  • 0x010000 ~ 0x03FFFF:运行数据区,保存计数值、累计量、实时状态。
  • 0x040000 ~ 0x07FFFF:日志区,按环形队列追加事件记录。

环形队列的写法和 Flash 的环形队列写法很像,但少了两件事:不需要先擦除新区域,不需要考虑磨损均衡。每次写日志直接在尾部追加就行,写满后回绕到起始位置覆盖最老的记录。这个逻辑在 MRAM 上写起来非常直接。

4.3 数据校验方案

校验是数据完整性的最后一道防线。我推荐用 CRC16,查表方式实现速度很快。下面这个函数是常见的 CRC16-CCITT 变体,用在日志块校验上足够了:

unsigned int crc16_update(unsigned int crc, unsigned char byte) { unsigned int i; crc ^= byte; for (i = 0; i < 8; i++) { if (crc & 1) crc = (crc >> 1) ^ 0x8408; else crc >>= 1; } return crc; }

每次写数据块时,把 CRC 值放到结构体尾部;读取时重新计算整个块,和存储的 CRC 比对。一旦不一致,就说明这块数据已经不可信,应该走备份回滚逻辑。我还习惯在块头部放一个固定魔数,比如0xA5A5。上电时先读魔数,不是魔数说明这个块从来没被初始化过,直接跳过,避免把随机数据当有效数据解析。

4.4 双备份与回滚

工业设备讲究“不能因为存储错误导致整机趴窝”,所以我用双备份方案:关键配置区写两份,一份在主区,一份在备份区。写入顺序是:先写备份区,再写主区。读取时优先读主区,如果主区 CRC 校验失败,自动读备份区,并在主区重新写入备份区的内容。

为什么先写备份区而不是先写主区?如果先写主区,写到一半掉电,主区坏了,备份区还是旧数据,但你不知道主区坏了,下次启动读到坏主区就可能出错。反过来,先写备份区,即使写备份时掉电,主区依然是完整且最新的数据,系统可以正常跑。这个“先写备份、再写主区”的顺序,是一个几乎零成本的保险,强烈建议照做。

5. 常见问题与排错速查

驱动写好后,真正调板子的过程总会遇到几个典型问题。我把踩过的坑整理成一个速查表,按现象 -> 原因 -> 处理方式排列。

5.1 读回全是 0xFF 或者全是 0x00

优先怀疑硬件连接和 SPI 配置:

  • WP# 没有上拉。如果 WP# 被拉低或悬空,芯片可能进入保护状态,写不进去,读出来保持出厂值 0xFF。解决:WP# 接 VCC。
  • SCK 和 SI 接反。检查原理图,确认 MCU 的 SDO 接 MRAM 的 SI,MCU 的 SDI 接 MRAM 的 SO。
  • SPI 模式不匹配。MR25H40CDF 支持 Mode 0 和 Mode 3,确认 PIC18 的 CKP/CKE 配置对应了其中一种。如果时钟极性和采样沿不对,读回来的数据全是乱码或者 0。
  • CS# 引脚悬空或电平不稳。CS# 必须由上拉电阻拉到高,操作时拉低。悬空时芯片可能反复进命令状态。

5.2 写进去读不出来

最常见的原因是写使能命令没发成功。WREN(0x06)必须在 WRITE(0x02)之前的独立 CS 周期发送,两个命令之间 CS# 要拉高一次。如果拉低 CS# 后连续发 0x06 0x02,芯片会忽略后续命令。

另外检查 SI/SO 是否接反。有些开发板丝印不清晰,SI 和 SO 接反之后,读命令写不进去,但读操作因为 MCU 只发命令不关心返回,反而可能读出全 0。

5.3 系统复位后数据丢失

这种问题很隐蔽。我遇到过一次,排查了很久才发现是上电瞬间 MCU 的 GPIO 输出不确定,CS# 被拉低了一段不短的时间,MRAM 误以为收到了一个命令,恰好在那个窗口里执行了非预期的操作。

解决方法是给 CS# 加上拉电阻,同时把 CS 对应的 GPIO 在初始化早期就设为高电平,再配置其他外设。还有一次是 HOLD# 引脚没接,悬空状态下受到干扰,芯片进入了 HOLD 状态,SPI 时钟被忽略,写进去的数据实际上没有生效。HOLD# 直接接 VCC 就能避免。

5.4 高低温下偶发读写错误

工业环境要求 -40°C 到 +85°C,低温和高温下 SPI 信号质量会变差。我处理过几次高低温测试失败,靠三招解决:

  • 降低 SPI 时钟。从 8MHz 降到 2MHz,信号完整性压力明显减小。MRAM 本身很快,2MHz 也远远跑不满它,但对 PCB 走线和连接器容错率大大提升。
  • SCK 上串电阻。抑制边沿振铃,减少过冲。
  • 缩短飞线长度。工业设备里如果 MCU 和 MRAM 分板连接,尽量用双排针短接,不要用几十厘米的杜邦线飞线。

5.5 用 JEDEC ID 做上电自检

驱动里加一个自检函数:上电后发 0x9F 读 3 字节 JEDEC ID,如果读出的值和厂商给出的一致,再继续初始化;不一致就报存储故障。这比任何调试手段都直观,可以快速区分“硬件没接好”和“存储芯片坏了”。

int mram_self_test(void) { unsigned char id[3]; mram_cs_low(); spi_write_read(0x9F); // RDID id[0] = spi_write_read(0x00); id[1] = spi_write_read(0x00); id[2] = spi_write_read(0x00); mram_cs_high(); if (id[0] == 0x56 && id[1] == 0x00 && id[2] == 0x00) // 设备具体 ID 以手册为准 return 1; return 0; }

注意:这里具体的厂商 ID 和器件 ID 值,请以你拿到的数据手册为准,不同批次型号可能有差异。自检的重点是“读回来的是不是合理值”,而不是死记某个固定数。

6. 扩展:从裸机移植到嵌入式 Linux 环境

6.1 什么时候需要把 MRAM 接到 Linux 上

有些工业设备的主控不是单片机,而是一块跑嵌入式 Linux 的板子,比如 RTU、边缘网关、工控机。这时候 MR25H40CDF 可以直接挂到 CPU 的 SPI 控制器上,在 Linux 用户态通过spidev接口读写,完全不需要写内核驱动。这个做法对于产品原型验证和小批量设备来说开发效率很高。

嵌入式 Linux 下操作 SPI 设备的关键是设备树配置。简单来说,你要在设备树里把 SPI 控制器的片选、时钟频率、极性、模式配好,系统启动后会生成一个/dev/spidevX.Y节点,应用层直接对这个节点做open、ioctl、read、write就行。

6.2 用 spidev 读写 MRAM 的最小示例

下面这段 C 代码是在 Linux 用户态操作 MRAM 的骨架,思路和裸机驱动完全一样,只是把 SPI 收发换成了 ioctl。

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/spi/spidev.h> static int spi_fd; void spi_open(const char *dev) { spi_fd = open(dev, O_RDWR); if (spi_fd < 0) { perror("open"); return; } unsigned char mode = SPI_MODE_0; unsigned char bits = 8; unsigned int speed = 2000000; // 2MHz ioctl(spi_fd, SPI_IOC_WR_MODE, &mode); ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &bits); ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); } void spi_transfer(unsigned char *tx, unsigned char *rx, unsigned int len) { struct spi_ioc_transfer tr = { .tx_buf = (unsigned long)tx, .rx_buf = (unsigned long)rx, .len = len, .speed_hz = 2000000, .bits_per_word = 8, }; if (ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr) < 0) { perror("spi_transfer"); } } unsigned char mram_read_byte(unsigned long addr) { unsigned char tx[5] = {0x03, addr >> 16, addr >> 8, addr, 0x00}; unsigned char rx[5] = {0}; spi_transfer(tx, rx, 5); return rx[4]; }

这里要特别说明:spidev的每次SPI_IOC_MESSAGE传输,内核会自己管理 CS 片选,会在传输开始拉低 CS、传输结束后拉高。所以写数据之前需要分两次 ioctl:第一次发 WREN,第二次发 WRITE+地址+数据。这一点和裸机驱动里“两个 CS 周期”的概念完全相同。

Linux 下也可以用现成的spidev_test工具手动验证:

# 读 JEDEC ID spidev_test -D /dev/spidev0.0 -s 2000000 -p "\x9F\x00\x00\x00" # 写一个字节到地址 0x000000(先写使能) spidev_test -D /dev/spidev0.0 -s 2000000 -p "\x06" spidev_test -D /dev/spidev0.0 -s 2000000 -p "\x02\x00\x00\x00\xAA" # 读回来验证 spidev_test -D /dev/spidev0.0 -s 2000000 -p "\x03\x00\x00\x00\x00"

不过说实话,spidev_test的输出格式读大块数据不太方便,实际开发我还是建议直接写一个几十行的 C 小程序,把循环读、解析、CRC 校验都放进去,效率高得多。

6.3 Linux 应用层的数据管理思路

不管用裸机还是 Linux,数据管理的核心逻辑是一样的:分区块、带魔数、带 CRC、双备份顺序写。在 Linux 上做可以更奢侈一点,因为应用层有文件系统,也可以把 MRAM 映射成一个裸块设备,但我的建议是别轻易在上面格式化文件系统,尤其是数据记录频繁的场景。MRAM 虽然寿命长,但文件系统本身的开销和日志事务会让简单的问题复杂化。工业场景里,直接用自定义的块结构管理 MRAM,往往比套一层文件系统更稳定、更可控。

我在实际使用中也试过用 Linux 的mtd驱动去管理 MRAM,发现那套面向 NAND/NOR 的坏块、擦除、磨损均衡逻辑在 MRAM 上反而是累赘。MRAM 的价值就在于“简单、直接、可靠”,上层逻辑越贴近这一本质越好。

7. 我的一点个人经验

这套 MR25H40CDF + PIC18F86J10 的方案,最终在我们的设备上跑了近一年的连续写入,没有因为存储问题出过一次故障。相比之前用 NOR Flash 的方案,代码量少了很多,以前要处理擦除块、页对齐、磨损均衡、掉电恢复这些事,现在统统不需要了。省下来的时间,我花在了更值得的地方,比如数据校验和异常上报。

如果让我给第一次用 MRAM 的人一个建议,那就一句话:先把 JEDEC ID 自检函数写好,再往下做。只要 ID 读得对,后面所有问题都变得清晰;ID 读不对,先把硬件查明白再写业务代码,这是我在现场调试换来的教训。

最后分享一个很实用的小技巧:如果现场没有逻辑分析仪,可以用 MCU 的 GPIO 模拟一个“软示波器”,在 SPI 读写的关键时刻翻转一个空引脚,用另一块板子或者机顶盒抓波形。配合串口打印十六进制数据,排查 SPI 通信问题比想象中有效。这个土办法帮我解决过好几起“看起来像芯片问题,其实是我代码问题”的乌龙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询