拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MRAM+8位MCU:工业设备掉电不丢数据的存储设计实践

MRAM+8位MCU:工业设备掉电不丢数据的存储设计实践

做工业设备存储的人,多少都被“频繁掉电丢数据”这件事折磨过。我前阵子做一台北数据传输终端,要记录电机运行累计时长、报警事件和出厂参数,原来用 SPI NOR Flash,写到十万次左右就开始出现扇区磨损,更麻烦的是现场突然断电,Flash 正擦到一半,整块记录直接报废。后来我把存储芯片换成 Everspin 的 MR25H40CDF,主控用 Microchip PIC18F47K42,这套组合在工业场景里表现相当稳。这篇文章把从选型、电路设计到驱动代码和掉电处理的完整过程整理出来,重点聊聊为什么 MRAM 适合这类嵌入式应用,以及实际调试中那些容易踩的坑。如果你也在做数据记录仪、参数备份、计量校准或任何“频繁写+不能丢”的嵌入式项目,这篇应该能帮你少走不少弯路。

1. 为什么是“MRAM + 8位MCU”:从一次现场掉电事故说起

1.1 MR25H40CDF 到底解决了什么问题

MR25H40CDF 是 Everspin 的 4Mb 串行 SPI MRAM,容量折算下来是 512KB。它和 Flash、EEPROM 最大的区别在存储原理:MRAM 利用磁隧道结的磁阻状态来保存数据,写入是直接改变磁性状态,不需要像 Flash 那样先擦除再编程,也不需要像 EEPROM 那样靠电荷泵慢慢憋电压。所以它既能像 SRAM 一样按字节随便写,又能在断电后把数据留住。

当初换芯片的原因很简单:原来的 NOR Flash 写入一个页要经历“擦除 + 编程”两段长窗口,现场电一断正好打在擦除中间,轻则当前扇区数据错乱,重则整块记录区报废。MRAM 的写操作只需要一条 SPI 写指令的时间,芯片内部是直接翻转存储单元,没有“先擦后写”的中间态,掉电窗口被压缩到了单个 SPI 传输的微秒级。再加上它的写耐久标称大约 10^14 次,对“每秒写一条日志”这种玩法来说,基本可以无视磨损问题。

对比一下更能说明问题:

特性MR25H40CDF(MRAM)SPI NOR Flash串行 EEPROM
写入前是否擦除不需要需要不需要
单字节/小页写入耗时SPI 传输时间,微秒级页编程毫秒级毫秒级
写耐久典型值约 10^14 次约 10^4~10^5 次约 10^6 次
掉电中途写入风险极低高中等
容量定位512KB大容量为主小容量为主

对嵌入式架构师来说,选存储芯片不能只看容量和价格,还要看“写入模式”和“环境风险”。工业现场最贵的是维护工单,一块存储芯片因为掉电写坏导致设备停机,省下的那几毛钱成本根本不值。

1.2 PIC18F47K42 在这个组合里扮演什么角色

PIC18F47K42 是 Microchip 的 8 位增强型中端 MCU,片上有 128KB Flash、8KB RAM,集成了 MSSP(SPI/I2C 模块)、ADC、PWM、比较器、定时器这些常用外设。对于数据采集终端、设备状态监测、计量控制这类逻辑不算特别复杂的工业板,8 位机完全扛得住,没必要一上来就上 Cortex-M。

我选它的另一个原因是整个板子想控制在 3.3V 单电源,PIC18F47K42 工作电压范围宽,和 3.3V 的 MRAM 可以直连,不用做电平转换。K42 的 MSSP 模块支持 SPI 主模式,SCK 可以跑到系统时钟的几分之一,带 MRAM 这种 40MHz 级别的芯片绰绰有余。实际工程里如果引脚不够用,K42 还有多个 MSSP 模块,MRAM 挂 MSSP1,别的 SPI 设备挂 MSSP2,互不干扰,这在多器件 SPI 总线上比共用一条总线省心得多。

另外,PIC18F-K42 系列可以用 MPLAB X 自带的 MCC 图形化配置工具生成外设初始化代码,对不熟悉寄存器细节的人很友好。底层生成好之后,MRAM 驱动只需要围绕 SPI 收发和片选控制写,工作量不大。

2. 硬件连接:这些引脚和电源细节千万别想当然

2.1 引脚功能与最小接线图

MR25H40CDF 的功能引脚主要是 CS#、SCK、SI、SO、VCC、GND、WP#、HOLD#,DFN8 封装。它的 SPI 接口是标准四线制,SI 接 MCU 的 SDO,SO 接 MCU 的 SDI,CS# 必须用普通 GPIO 手动控制,这一点后面会专门说。

我板子上的实际接法:

存储芯片引脚MCU 连接说明
CS#RD0(普通 GPIO)10k 上拉,默认高
SCKRC3(MSSP1 SCK)SPI 时钟
SIRC5(MSSP1 SDO)MCU 输出到存储芯片
SORC4(MSSP1 SDI)存储芯片输出到 MCU
WP#VCC10k 上拉
HOLD#VCC10k 上拉,不用必须拉高
VCC3.3V就近 100nF 去耦
GNDGND实心铺地

实际调试时最容易被忽略的是 HOLD#。HOLD# 在低电平时会让芯片“暂停”当前通信,但不复位内部状态。如果这个引脚悬空,上电瞬间电平不确定,SPI 传输经常出现第一个字节错位,表现是读 ID 偶尔正常偶尔乱码。所以 HOLD# 和 WP# 都必须明确接高,不能因为“不用”就空着。

WP# 的功能是写保护输入,严格来说它要不要接高取决于状态寄存器里的保护位设置。最简单的做法是把它上拉到 VCC,让硬件层面不限制写操作,再用软件控制写流程,后面驱动部分会展开。

2.2 电平、退耦、布局:工业板不能只求“能跑”

很多人在嵌入式开发板上用 5V 供电的 MCU 直连 3.3V 的存储芯片,靠着芯片引脚容忍 5V 输入就硬跑。MR25H40CDF 的电源范围是 2.7V 到 3.6V,如果 MCU 跑 5V,它的 SDO 输出高电平最高接近 5V,对 3.3V 器件来说这不是可靠的数字接口设计。最稳的方案是整套系统统一用 3.3V,PIC18F47K42 在这个电压下完全能工作,MRAM 也不需要额外的电平转换芯片。

电源去耦方面,VCC 和 GND 之间放一个 100nF 陶瓷电容,位置尽量靠近芯片,这是所有数字 IC 的基本要求。如果 PCB 上还有 DC-DC 或者大电流 PWM 负载,建议在 MRAM 的电源入口再加一个 10μF 钽电容或大容值 MLCC,防止负载切换时 VCC 跌落。

PCB 布局上有一个我踩过的坑:第一次画板图省事,把 MRAM 放在 DC-DC 电感旁边,读 ID 在实验室正常,一到现场带载就偶尔出错。后来用示波器看 SCK 线,发现电感辐射在时钟线上叠加了不小的噪声尖刺。解决办法是把存储芯片挪到板边,SPI 信号线远离电感,并且每条线串联 22Ω 电阻,既能抑制振铃,又能挡一点 ESD。工业品不能只看原理图能不能工作,还要看现场环境的真实干扰。

3. SPI 通信配置:K42 的 MSSP 和 MRAM 指令集怎么对上

3.1 MSSP 寄存器配置的几条核心规则

PIC18F47K42 的 MSSP 模块配置不算复杂,但有几个地方容易绕晕。首先是 SPI 模式,MR25H40CDF 支持模式 0 和模式 3,也就是说时钟空闲电平和采样边沿有两种合法组合。我习惯用模式 3,配置 CKP=1、CKE=0,这样时钟空闲时为高,数据在上升沿变化、下降沿采样,抗干扰能力比模式 0 略好一点。

其次是片选 CS#,MSSP 的硬件 SS 引脚不是给 MRAM 用的。K42 的 MSSP 如果启用硬件片选,会在每字节传输之间自动翻转 CS,而 MRAM 要求“一次完整的指令周期内 CS# 必须持续拉低”,中间一旦拉高,芯片就会认为当前命令结束。所以 CS# 必须用普通 GPIO 手动控制,发完整个指令再拉高。

初始化代码可以这样写,在 MPLAB X + XC8 环境下直接改:

#define MRAM_CS_LAT LATDbits.LATD0 #define MRAM_CS_TRIS TRISDbits.TRISD0 void MRAM_Init(void) { // CS 引脚:RD0,输出,默认高电平 MRAM_CS_TRIS = 0; MRAM_CS_LAT = 1; // MSSP1 引脚:RC3=SCK 输出,RC5=SDO 输出,RC4=SDI 输入 TRISCbits.TRISC3 = 0; TRISCbits.TRISC5 = 0; TRISCbits.TRISC4 = 1; ANSELC = 0x00; // 关闭模拟功能 // MSSP1 中断标志清零 PIR1bits.SSP1IF = 0; // SPI 模式 3:CKP=1,CKE=0 SSP1STATbits.SMP = 1; // 输入数据在周期末尾采样 SSP1STATbits.CKE = 0; SSP1CON1bits.CKP = 1; // 时钟空闲为高 SSP1CON1bits.SSPEN = 1; // 使能 MSSP1 SSP1CON1bits.SSPM = 0b0000; // SPI 主模式,时钟源为 FOSC/4 SSP1ADD = 15; // 分频,64MHz 下得到约 1MHz SCK }

这里的 SSP1ADD=15 对应 SCK 约 1MHz。MSSP 主模式下的时钟公式通常是 FOSC / (4 × (SSP1ADD+1)),64MHz 系统时钟除下来就是 1MHz。先把时钟降到 1MHz 调通通信,再用示波器确认波形干净,之后逐步提高分频,这样排查硬件问题会容易很多。

3.2 指令集与片选时序:最容易被忽略的部分

MR25H40CDF 的指令集和 SPI NOR Flash 很接近,常用指令如下:

指令操作码功能
WREN0x06写使能
WRDI0x04写禁止
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据
RDID0x9F读器件 ID
SLEEP0xB9进入睡眠模式
WAKE0xAB唤醒

地址格式是三个字节,高字节在前。MR25H40CDF 是 4Mb 容量,地址范围 0x00000 到 0x7FFFF,实际用到 19 位,三字节地址里最高字节只需要关心最低几位,其余位填 0 即可。

片选时序有三个重点。第一,WREN 是独立命令,必须单独拉低 CS#、发 0x06、拉高 CS#,之后才能发 WRITE 指令。如果图省事把 WREN 和 WRITE 放在同一个 CS# 低电平周期里连续发送,芯片不会执行写入。第二,READ 和 WRITE 命令在 CS# 拉低后都必须完整发送“操作码 + 3 字节地址 + 数据段”,中途抬 CS# 会导致指令被抛弃。第三,连续读模式下,只要 CS# 保持低电平,地址会持续自增直到 0x7FFFF 后回绕到 0x00000;一旦 CS# 拉高,内部地址状态复位,下次通信从头开始。

4. 驱动代码:从单字节到连续读写的落地实现

4.1 底层 SPI 收发和单字节读写

驱动的基础是一个 SPI 收发函数,同时发送一个字节并接收一个字节。用查询方式写最简单:

uint8_t SPI_Transfer(uint8_t tx) { SSP1BUF = tx; while (!PIR1bits.SSP1IF); // 等待发送完成 PIR1bits.SSP1IF = 0; return SSP1BUF; }

然后封装 MRAM 的基础操作。写数据之前必须先写使能:

void MRAM_WriteEnable(void) { MRAM_CS_LAT = 0; SPI_Transfer(0x06); // WREN MRAM_CS_LAT = 1; } void MRAM_WriteByte(uint32_t addr, uint8_t data) { MRAM_WriteEnable(); MRAM_CS_LAT = 0; SPI_Transfer(0x02); // WRITE SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); SPI_Transfer(data); MRAM_CS_LAT = 1; } uint8_t MRAM_ReadByte(uint32_t addr) { uint8_t val; MRAM_CS_LAT = 0; SPI_Transfer(0x03); // READ SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); val = SPI_Transfer(0x00); MRAM_CS_LAT = 1; return val; }

这里有个容易被忽略的细节:MRAM 写入前必须 WREN,但读操作不需要 WREN。我见过有人对着手册写读函数也先写 0x06,这不会造成硬件错误,但多一次片选周期,浪费时间。写使能只针对写指令和状态寄存器写指令。

每写完一个字节后,MRAM 内部会立刻完成存储操作,不需要像 Flash 那样轮询等待。这是我用这个芯片最爽的地方:写日志时可以连续发多个写操作,每个操作之间几乎零等待,数据吞吐完全取决于 SPI 时钟。

4.2 状态寄存器与写入完成判断

状态寄存器通过 RDSR 读取:

uint8_t MRAM_ReadStatus(void) { uint8_t sr; MRAM_CS_LAT = 0; SPI_Transfer(0x05); // RDSR sr = SPI_Transfer(0x00); MRAM_CS_LAT = 1; return sr; }

状态寄存器最低位是 WIP(写进行中)。对 Flash 来说,发完写命令之后必须轮询 WIP 直到清零,否则数据可能没写完。MR25H40CDF 的 WIP 位在数据写完之后通常读出来就是 0,因为它的存储单元写入是瞬时完成的。我在驱动里仍然保留 WIP 检测,不是为了等写入,而是为了确认通信链路正常。

如果发现 WIP 一直为 1,或者读出来的状态寄存器值完全不变,就要检查是不是 SPI 模式配错了。这时候可以顺便看一下 WEL 位,也就是写使能锁存位。执行完 WREN 后 WEL 变为 1,执行完 WRITE 后 WEL 自动清零。调试时可以在每次写入后打印这几位,能快速判断命令是否真的被执行。

状态寄存器的高位还有块保护位,可以配置成把指定区域设为只读。这个功能适合用来锁关键区,比如工厂校准参数写完之后就禁止再改。但要注意,MRAM 的块保护含义和具体位组合必须以 MR25H40C 数据手册为准,不同批次和型号可能略有差异。我第一个版本直接照抄网上某个 FRAM 的设置值,结果把整个阵列保护住了,后面想取消保护还得先理解保护位逻辑,折腾了好半天。建议前期调试阶段不要启用块保护,先用软件层面的写流程控制,等到功能稳定后再考虑。

4.3 连续读与页写:日志存储的基本功

日志类应用很少只读一个字节,要么是连续读出一大段历史记录,要么是批量写入新数据。READ 指令天然支持连续读,只要 CS# 保持低电平,地址自动递增:

void MRAM_ReadBuffer(uint32_t addr, uint8_t *buf, uint16_t len) { MRAM_CS_LAT = 0; SPI_Transfer(0x03); SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); while (len--) { *buf++ = SPI_Transfer(0x00); } MRAM_CS_LAT = 1; }

写操作要特别注意页边界。MRAM 的 WRITE 指令在同一个 CS# 低电平周期里可以连续写入多个字节,但地址自增到页边界之后会回绕到页首,继续写就会覆盖当前页开头的数据。稳妥的做法是按 256 字节的页来切分,每次写入不超过当前页剩余长度:

uint16_t MRAM_WriteBuffer(uint32_t addr, const uint8_t *buf, uint16_t len) { uint16_t chunk; while (len) { chunk = 256 - (addr % 256); // 当前页剩余长度 if (chunk > len) chunk = len; MRAM_WriteEnable(); MRAM_CS_LAT = 0; SPI_Transfer(0x02); SPI_Transfer((addr >> 16) & 0xFF); SPI_Transfer((addr >> 8) & 0xFF); SPI_Transfer(addr & 0xFF); for (uint16_t i = 0; i < chunk; i++) { SPI_Transfer(buf[i]); } MRAM_CS_LAT = 1; addr += chunk; buf += chunk; len -= chunk; } return 0; }

这里把页大小保守定为 256 字节,即使芯片实际允许 512 字节页,这么写也永远安全。如果数据手册确认支持更大的页突发,可以再针对性优化。

日志环形缓冲区的设计也建议按页对齐来分区。比如整片 512KB,我划成 128 个 4KB 区域,每个区域写满后跳到下一个,配合页写函数,写入效率高,而且启动扫描时能快速定位最新记录位置。

5. 掉电保护与数据完整性:工业现场最容易翻车的地方

5.1 WP# 引脚和状态寄存器保护的正确打开方式

WP# 引脚和状态寄存器里的 WPEN、块保护位是配合使用的。很多人以为把 WP# 拉低就能保护整个阵列,实际不一定。MRAM 的 WP# 只有在状态寄存器中使能了相关保护位之后,才会禁止写操作;如果状态寄存器没有开启保护,WP# 拉低并不影响阵列写入。

所以我的建议分两种情况。如果只是普通数据记录,不需要硬件级只读保护,WP# 直接接 VCC,用软件流程控制写入,MRAM 本身必须“WREN + WRITE”两条指令才能写,已经比并行 RAM 安全得多。如果是参数区、校准区这种一旦写坏就麻烦的数据,可以在写完参数后通过 WRSR 把对应区域设为块保护,再把 WP# 拉低作为第二道锁。

块保护的配置要小心,写状态寄存器本身也需要先 WREN。假设要把某块区域锁住,流程是:

  1. 执行 WREN。
  2. 拉低 CS#,发 WRSR(0x01),写入新的状态寄存器值。
  3. 拉高 CS#。
  4. 把 WP# 由高拉低,强化保护。

这里最容易踩的坑是:有人不先 WREN 就发 WRSR,结果状态寄存器里旧值没变,还以为是芯片坏了。我在项目里吃过这个亏,花了大半个下午才发现是命令顺序问题。

5.2 利用掉电检测把最后状态“抢救”进 MRAM

MRAM 写入速度快,不等于不需要掉电策略。工业现场断电经常毫无预兆,如果当前正好在写一条很长的记录,掉电瞬间可能只写了前半段。重启后如果直接使用这条半截记录,数据就是错的。

针对这种情况,我的做法是两层防护。第一层是利用 MCU 的电源监测机制,在电压开始跌落但 MCU 还能正常工作的短暂窗口里,把最重要的状态紧急写入 MRAM 顶部的保留区。PIC18F47K42 可以用 ADC 对 VDD 分压后持续采样,也可以直接用内部的低电压检测相关模块,触发后进入中断。中断里只做一件事:把当前累计值、最后一条报警序号、下一次要写入的日志指针写入 MRAM 固定地址。这整个过程就是几十字节的 SPI 写操作,在 1MHz SCK 下大约几百微秒,完全赶得及。

第二层是软件层面的“先写后确认”。我说的确认不是写完再读,而是把记录设计成自描述的帧结构:

  • 帧头使用固定魔数,比如 0xA5 0x5A。
  • 帧头后面跟着记录长度、记录序号、CRC16。
  • 最后再放一个结束魔数。

读取时先检查帧头和 CRC,CRC 不对就丢弃这一条。这样即使掉电把一条记录写了一半,顶多损失这一条,不会殃及整片日志。

5.3 校验与双备份:怎么写才不会丢数据

对于“当前状态”这类数据,单靠 CRC 丢弃还不够。比如设备的累计运行时间,掉电时写坏了一条,重启后读到旧的正确记录,但期间可能已经运行了几十分钟没记上。所以关键状态必须“双备份交替写”。

具体方案:存储区里划出两个固定块 A 和 B,每个块里都存一份完整的“状态快照”,快照里带序号。启动时先读 A,再读 B,比较序号,取序号较新且 CRC 通过的那一份。每次要更新状态时,看上一次写的是 A 还是 B,这次就写另一个块。这样即使一次写入过程被掉电打断,另一块里仍然有完整的历史数据,最多丢失本次新增量,不会出现完全没有可用状态的情况。

CRC16 的查表实现网上很多,关键是要统一多项式。设备里所有读写校验都用同一个 CRC 函数,避免出现“写入用多项式 A,读取用多项式 B”的低级错误。我在早期版本里就犯过一次,写入端和读取端抄了不同来源的查表代码,结果数据明明没坏,却总是校验失败。

另外,启动时可以在存储区开头写一个“启动标记”,正常关机流程里写一个“正常关闭标记”。下次上电先看关闭标记,如果没读到,说明上次是异常掉电,日志扫描逻辑要进入更严格的 CRC 检查模式。这个标记本身可以用双备份,防止掉电时正好打在标记写入上。

6. 实测数据与调试避坑经验

6.1 读写速度实测:比 Flash 快的不只是一点点

在我这块测试板上,PIC18F47K42 系统时钟 64MHz,SCK 配置为 8MHz,用查询方式跑 MR25H40CDF。连续读 512KB 数据的纯传输时间约 0.5 秒左右,连续写同样接近这个量级。如果换成擦除加编程的 NOR Flash,同样容量先擦后写,耗时通常要几秒甚至几十秒,具体看芯片型号和编程方式。MRAM 的优势在频繁小数据写入时更明显,因为它不需要等待页擦除,也不需要每写一页就轮询忙状态。

实际吞吐没有跑满 8MHz 线速率,主要开销在 MSSP 中断标志查询和 CS# 翻转上。8 位 MCU 用查询方式,每个字节都要等 SSP1IF,差不多能有 6Mbit/s 左右的有效吞吐。对工业日志记录来说这个速度完全够用。如果追求更高吞吐,可以考虑把读写函数改成中断或 DMA 方式,但驱动复杂度会明显上升,不见得值得。

我还在同样环境下对比过写 1MB 数据的累计耗时,MRAM 大约 1 秒多,EEPROM 按单字节 3ms 算的话要将近一个小时。差距就是这么夸张。这也是为什么“频繁写小数据”的场景里,MRAM 几乎是无可替代的选择。

6.2 高低温环境下的验证结果

我把整块板子放进温箱,从 -30°C 到 85°C 做循环,每个温度点停留一小时,期间每 5 秒写一条包含时间戳和随机数的记录,总共跑三个循环。结果读出的数据逐条校验全部通过,没有出现单 bit 翻转。

比较需要注意的反而是 PCB 焊接问题。MR25H40CDF 是 DFN8 封装,底部有散热焊盘,手工焊的时候容易虚焊或焊锡桥接。我曾经遇到过一块板子在常温下完全正常,放进温箱低温区就开始偶发读 ID 失败,最后查出来是 SO 引脚虚焊,温度变化导致引脚接触不良。如果调试时遇到“冷的时候出错、热的时候正常”,优先怀疑焊点和连接器,而不是芯片本身。

另外,即使 MRAM 抗磁场能力比传统磁存储器件强很多,仍然不建议把强磁铁直接贴在芯片表面测试。我在实验室里顺手拿一块钕磁铁靠近芯片试过,几毫米距离内读写偶尔会出现通讯错误,移开后又恢复正常。工业现场的电机、电磁铁如果紧贴着芯片,理论上也可能有影响,布局时留一点空间最稳妥。

6.3 三个费了我最多时间的 CS 时序坑

第一个坑是 WREN 和 WRITE 被放在同一个 CS# 周期里。有一次我把代码精简成了“拉低 CS、连续发 0x06 0x02 地址数据、拉高 CS”,看起来没问题,因为很多 SPI NOR Flash 的例程就是这么写的,但 MR25H40CDF 不认。它要求 WREN 必须单独占用一个 CS# 周期,WREN 结束拉高后,下一次拉低 CS# 再发 WRITE 才有效。这个坑的排查过程很折磨人,因为读操作完全正常,只有写不进去,很容易误判成“WP# 有问题”或者“芯片坏了”。

第二个坑是 CS# 拉低后没有给时钟留出稳定时间。用 GPIO 模拟 CS 时,CS# 刚拉低立刻发第一个字节,如果此时 SCK 空闲电平还没稳定,MRAM 可能把第一个时钟沿当成杂散信号。解决办法很简单,CS# 拉低后加几个 NOP 延时,或者先确保 SCK 处于正确的空闲电平再操作 CS#。硬件上再加一个 10k 下拉/上拉电阻也可以。

第三个坑是连续读不拉高 CS#。调试时我为了图方便,读完一段数据后直接改地址继续读,发现后面的数据总是隔一段就重复。原因就是 CS# 一直处于低电平,MRAM 内部地址计数器继续自增回绕,而我发的地址只在当前 CS# 周期开始时有效。正确做法是每次读操作结束必须拉高 CS#,下一次操作重新从片选开始。

调试 MRAM 驱动的第一件事不是写读写函数,而是先读器件 ID。发 0x9F,连续读 9 个字节,检查返回的前几个字节是否符合 MR25H40C 系列的 ID。只要能稳定读出固定 ID,说明供电、引脚、SPI 配置全部正确,之后再做数据读写,排错范围一下就缩小了。

最后分享一个小技巧:在板子上把 CS# 引到测试点,每次调试新功能前用示波器抓一下片选波形,确认每个命令周期都有干净的拉高拉低。MRAM 的时序坑大多出在片选边界上,波形一抓,很多“偶发错误”立刻现形。这套组合我用到现在,最大的体会就是:MRAM 把存储的可靠性问题从“芯片磨损”变成了“软件时序问题”,后者虽然也要调,但至少是可复现、可定位的。

返回列表