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

资讯详情

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

PIC18F86K90使用MRAM替代EEPROM:工业仪表高频写入与掉电保存方案

PIC18F86K90使用MRAM替代EEPROM:工业仪表高频写入与掉电保存方案

最近我在整理一个工业仪表的存储子系统,主控是 PIC18F86K90,外挂存储没有继续用 EEPROM,换成了一颗 MR25H40CDF。这颗芯片是 Everspin 的 4Mbit SPI MRAM,工业温度档,SOIC-8 封装,能跟绝大多数 SPI EEPROM 或 FRAM 的封装和命令兼容。项目要求很直接:现场仪表每隔几百毫秒就要记录一条运行数据,还要在断电瞬间把关键状态存下来,不能出现写一半丢数据的情况。原来那颗 256Kbit EEPROM 容量刚够,但按这个写入频次算,每个字节一年就要刷过上百万次,寿命先扛不住,所以必须从存储介质层面换方案。

这篇文章不打算讲太多花哨的理论,重点是把“为什么选 MRAM”“怎么把它接到 PIC18F86K90 上”“固件和日志怎么做”“调试时踩过哪些坑”讲清楚。如果你也在做工业数据记录、设备日志、掉电保存这类功能,可以直接把里边的代码和表格拿来当参考,剩下就是按自己 PCB 上的引脚重新映射一遍。

1. 为什么要动存储:项目真正要解决的事

1.1 项目里到底存什么、读什么

工业仪表的存储需求通常分三层。第一层是长期参数,比如仪表编号、量程、校准系数、通讯地址,这类数据很少写,但必须断电不丢,写一次要管十年。第二层是运行日志,比如每次超限报警、开关量变化、温度曲线,这类数据需要高频写入,而且往往是循环覆盖,存满之后把最旧的那条覆盖掉。第三层是掉电保存,就是检测到电源异常之后,用剩余能量把当前瞬时值、累计量、运行状态写到存储里,下次上电要能完整读回来。

这三个需求放一起,普通 EEPROM 会有点吃力。EEPROM 写入电压高,内部有电荷泵,写一个字节周期要几毫秒,写完了还要等内部擦写完成才能进行下一次写,否则会丢数据。更致命的是它的耐久度,常见串行 EEPROM 标称每字节一百万次擦写,听着不少,但掉电保存场景是“同一地址反复写”,一秒钟写一次,一天的写入次数就逼近九十万次,一百万次寿命几天就耗尽。常规解决办法是加磨损均衡算法,把数据分散到不同地址,可一旦主控本身不够强,或者软件工程师没时间把均衡算法写对,问题会反复出现。

MR25H40CDF 解决的就是这个痛点。它是磁阻式随机存取存储器,写入依靠磁化方向改变,不需要电荷泵,也不存在擦除过程,理论上每个地址都能承受极大量的写循环。对“固定地址反复覆盖写”这种用法,MRAM 几乎是无压力的,软件里不需要做磨损均衡,逻辑大大简化。

1.2 MRAM 和 Flash、EEPROM 的差别

很多工程师一开始容易把 MRAM 和 FRAM 混在一起,但它们底层原理不同。FRAM 靠铁电晶体的极化状态存储数据,MRAM 靠磁隧道结的磁化方向存储数据,共同点是都能随机字节写、快速写、耐写次数高。但 MRAM 在容量密度、工业温度范围和保持时间上通常更稳健一些。

这里我把常见三种存储介质拉了一张对比表,方便选型时直接看差距:

特性串行 EEPROMNOR FlashMRAM(以 MR25H40CDF 为例)
典型容量256Kbit ~ 1Mbit4Mbit ~ 128Mbit4Mbit
写入方式页写,先擦后写必须先擦除扇区/块直接覆盖写
写周期5ms 左右/页扇区擦除 100ms 以上无擦除等待,微秒级
每字节耐久度约 100 万次约 10 万次擦除极高,可认为无磨损
读速度1MHz SPI 为主支持高速 SPI/QSPI支持 SPI 40MHz 级别
运行功耗低高,尤其擦除时电流大低,无电荷泵尖峰
数据保持约 100 年10 年左右20 年以上

从这张表能看出来,MRAM 最大的价值不在容量,而在“写得快”和“写得无限”。工业数据记录最怕的不是容量不够,而是写到一半断电、或者寿命耗尽后数据变得不可靠。MRAM 可以把这块短板直接补掉。

1.3 为什么是 PIC18F86K90 来当主控

有人可能会问,既然要工业存储,为什么不用个更主流的 MCU?项目里选 PIC18F86K90 是有实际原因的。这颗芯片是 Microchip 的 8 位增强型中端 PIC18 家族,80 引脚封装,带片上 LCD 段式液晶驱动,很多温控器、流量计、水气表就是这种形态。它内部集成 MSSP 模块,可以跑 SPI/I²C 主机模式,外接 MRAM 不需要额外软件模拟时序,直接用硬件 SPI 就能把时钟和数据推出去。

另外,PIC18F86K90 的低功耗特性很适合电池供电或 24V 回路供电的工业设备。平时休眠时液晶和传感器可以继续工作,主控几乎不耗电;需要写日志时再唤醒,完成一条 MRAM 写入后立刻回休眠。对于这种“频繁小数据量写入”的业务模型,8 位 MCU 的能效比反而比 32 位 ARM 更有优势。成本也摆在那,一颗成熟 8 位芯片加一颗 MRAM,总物料成本远低于一片入门级 ARM 加配套 EEPROM 的方案。

2. 硬件连接:先把物理层做对

2.1 引脚号跟着谁走

MR25H40CDF 是 8 脚 SOIC 封装,引脚定义和常见 SPI EEPROM 基本一致。第一脚是片选 CS,第二脚是数据输出 SO,第三脚是写保护 WP,第四脚接地,第五脚是数据输入 SI,第六脚是时钟 SCK,第七脚是保持 HOLD,第八脚是电源 VCC。这里特别提醒一句:不同批次、不同后缀的 MRAM,引脚功能顺序要以数据手册为准,我下面这张接线表是基于主流 SPI MRAM 的标准定义。

MRAM 引脚功能接到 PIC18F86K90说明
1 CS片选,低有效任意 GPIO,如 RC0高电平时 SO 输出为高阻
2 SO串行数据输出SDI1主从输入
3 WP写保护,低有效直接接 VCC不使用保护时别悬空
4 VSS地GND共地
5 SI串行数据输入SDO1主从输出
6 SCK串行时钟SCK1时钟信号
7 HOLD保持,低有效直接接 VCC不使用保持功能时接高
8 VCC3.3V 电源3.3V100nF 去耦电容就近

从这张表能看出来,MR25H40CDF 和普通 SPI EEPROM 的引脚极其接近,很多情况下把一颗 25LC256 或者 93C46 拔下来,改一下容量定义,就能换成 MRAM。这也是它在替换现有项目时特别省事的原因。

2.2 电源、去耦和空闲引脚处理

MRAM 工作电压一般在 2.7V 到 3.6V,所以最省心的做法是 MCU 和 MRAM 共用同一条 3.3V 电源轨。PIC18F86K90 本身可以在 3.3V 下跑得很稳,MSSP 的 IO 电平也是 3.3V,不会出现逻辑不匹配。

电源上不要省那两颗电容。VCC 和 GND 之间放一颗 100nF 的陶瓷电容,尽量挨着芯片引脚放;如果 PCB 空间允许,再放一颗 10µF 电解电容做低频储能。MRAM 写入时虽然没有电荷泵,但 SPI 翻转频率高,电源纹波大会造成偶发读写错误。我在现场调试时见过一例奇怪问题:每次写完读回都是好的,但只要把示波器探头一靠近数据线,数据就出错,最后排查下来是电源去耦电容离芯片太远,高速 SPI 翻转时 VCC 上产生了 200mV 左右的跌落,导致芯片内部逻辑异常。

WP 和 HOLD 这两个引脚,默认情况下如果不准备用写保护,就直接接到 VCC,不要悬空。悬空引脚在强电磁干扰环境里很容易被感应成低电平,从而意外进入写保护或者保持模式。特别是 HOLD 拉低时,芯片会忽略 SCK 上的变化,导致 SPI 数据推进失败,读回来的数据错位,这类问题用逻辑分析仪都很难一眼看出来。

2.3 和别的 SPI 设备共享总线时的注意事项

如果同一个 SPI 总线上还挂了 LCD、SD 卡、Flash 等设备,CS 必须分开控制,SCK、SI、SO 可以复用。有一个容易出错的点:多个从机共享 MISO 线时,必须确保任意时刻只能有一个从机片选被拉低,否则两个从机同时把数据输出到同一条线上,轻则读取冲突,重则损坏引脚。

MR25H40CDF 在 CS 为高电平时,SO 引脚会进入高阻态,这个特性很好。但有些 SPI 传感器不具备三态输出能力,如果你的总线上混接了这类设备,就需要在硬件上做隔离,比如给共享 MISO 加一个三态缓冲器,或者把这类设备放到另一条软件模拟的 SPI 总线上。

还有一个容易被忽略的问题是 SPI 总线的空闲电平。MRAM 支持标准 SPI 模式 0 和模式 3,两者都是时钟空闲高或空闲低,但如果总线上既挂了模式 0 的设备,又挂了模式 3 的设备,切换设备时需要临时修改 MSSP 的 CKP 位,否则会在某个设备上出现数据错误。代码里我建议把 MRAM 独立用一组 GPIO 来控制 CS,并把 SPI 时钟参数单独封装,这样切换设备时不会牵连其他驱动。

3. 固件:把 PIC18F86K90 的 SPI 调顺

3.1 MSSP 初始化代码详解

PIC18F86K90 的 MSSP 模块可以配置成 SPI 主模式,这里我用的时钟分频是 Fosc/16,也就是把 SPI 时钟降到几 MHz 量级。工业环境里不需要追求极限速度,稳定优先。

#define MRAM_CS LATCbits.LATC0 #define MRAM_CS_TRIS TRISCbits.TRISC0 void MRAM_SPI_Init(void) { MRAM_CS_TRIS = 0; // CS 输出 MRAM_CS = 1; // 默认不片选 SSP1CON1bits.SSPEN = 0; // 先关闭 SPI,方便配置 SSP1CON1bits.CKP = 0; // 时钟空闲为低 SSP1STATbits.CKE = 1; // 数据改变在时钟下降沿,采样在上升沿 SSP1STATbits.SMP = 1; // 输入采样在数据输出末端 SSP1CON1bits.SSPM = 0b0001;// 主模式,Fosc/16 SSP1CON1bits.SSPEN = 1; // 使能 SPI }

要特别留意的是配置顺序。MSSP 模块一旦使能,CKP 位就不能随便改了,所以一定要先在 SSPEN 为 0 的状态下把极性设置好,最后再打开 SPI。CKE 和 SMP 这两个位的组合决定了 SPI 模式,这里给的是模式 0。如果你的 MRAM 数据手册支持模式 3,还需要把 CKP 改成 1、CKE 改成 0。我在项目初期就是因为照抄了某个例程里的模式 3 配置,导致 MRAM 读出来的数据整体偏移一位,排查了很久才发现是 CKE 设置的问题。

3.2 MRAM 的命令序列与读写函数

MR25H40CDF 的 SPI 命令和普通 SPI EEPROM 几乎一样,最常用的是下面这几个:

指令操作码功能
WREN0x06设置写使能锁存
WRDI0x04清除写使能锁存
RDSR0x05读状态寄存器
WRSR0x01写状态寄存器
READ0x03读数据
WRITE0x02写数据

底层 SPI 字节收发函数可以直接用查询方式实现,不必上中断。一条指令只有几个字节,即使是 1MHz 的 SPI 时钟,单字节传输也就 8µs 左右,完全够用:

void SPI_WriteByte(uint8_t b) { SSP1BUF = b; while (!PIR1bits.SSP1IF) { ; } PIR1bits.SSP1IF = 0; } uint8_t SPI_ReadByte(void) { SSP1BUF = 0x00; // 发一个空字节,同时接收 while (!PIR1bits.SSP1IF) { ; } PIR1bits.SSP1IF = 0; return SSP1BUF; }

接下来说明写使能和写入函数。很多第一次写 MRAM 的工程师会直接把 WRITE 命令打给芯片,结果发现数据没写进去,原因就是忽略了 WREN。MRAM 内部有一个写使能锁存位 WEL,它必须先被 WREN 指令置位,后续 WRITE 或 WRSR 指令才有效。而且 WREN 和 WRITE 之间必须让 CS 拉高再拉低,形成一个完整的指令间隔,不能在同一个 CS 低电平时段内连续发两条指令,否则芯片会把后边的字节当成一条长指令的扩展数据。

void MRAM_EnableWrite(void) { MRAM_CS = 0; SPI_WriteByte(0x06); // WREN MRAM_CS = 1; } void MRAM_WriteBytes(uint32_t addr, const uint8_t *p, uint16_t len) { uint16_t i; if (len == 0) return; MRAM_EnableWrite(); MRAM_CS = 0; SPI_WriteByte(0x02); // WRITE opcode SPI_WriteByte((addr >> 16) & 0xFF); // 地址高字节 SPI_WriteByte((addr >> 8) & 0xFF); SPI_WriteByte(addr & 0xFF); for (i = 0; i < len; i++) { SPI_WriteByte(p[i]); } MRAM_CS = 1; } void MRAM_ReadBytes(uint32_t addr, uint8_t *p, uint16_t len) { uint16_t i; MRAM_CS = 0; SPI_WriteByte(0x03); // READ opcode SPI_WriteByte((addr >> 16) & 0xFF); SPI_WriteByte((addr >> 8) & 0xFF); SPI_WriteByte(addr & 0xFF); for (i = 0; i < len; i++) { p[i] = SPI_ReadByte(); } MRAM_CS = 1; }

MR25H40CDF 的地址是 512KB,也就是 19 位,读写的地址参数用 3 字节发送,最高字节只用到低 3 位。如果你只跳过了最高字节,或者只发两个 8 位地址,数据会被写到完全不同的位置,这种问题就很隐蔽,因为读回来也能读到自己刚写的数据,只是和其他变量的位置撞在一起了。

写完之后如果心里不踏实,可以读一下状态寄存器,确认 WEL 状态,但正常逻辑里不查也行,因为 MRAM 写操作没有忙轮询一说,写完直接就是完成状态:

uint8_t MRAM_ReadStatus(void) { uint8_t s; MRAM_CS = 0; SPI_WriteByte(0x05); // RDSR s = SPI_ReadByte(); MRAM_CS = 1; return s; }

3.3 为什么写 MRAM 不需要“擦除”和“等待”

我见过不少从 Flash 平台转过来的工程师,写代码时顺手就加了一整套块擦除、擦除等待、状态轮询的逻辑。这些在 MRAM 上全部可以删掉。MRAM 的存储单元是通过磁化方向来表示 0 和 1 的,写入时直接把目标位覆盖成新值,不需要先把旧值擦掉,也没有高压电荷泵的建立过程。因此,WRITE 指令发完,数据就已经落在存储区里了,芯片不会产生 NVM 擦除等待时间。

这一点带给实际程序的收益非常大。掉电保存场景里,中断服务程序可以一条 WRITE 指令把关键数据写完,不用等待数毫秒擦除时间,也不用担心擦除中断导致数据损坏。日志循环覆盖场景里同样不需要先擦除整个扇区,直接按固定长度往目标地址写就行。软件里少掉“擦除”这一步,很多边界条件自然就消失了。

当然,MRAM 不是没有缺点。它的容量相对同价位的 NOR Flash 小很多,单位比特成本也高,所以它更适合放“需要频繁写、必须掉电不丢”的核心数据,不适合当大容量文件系统盘用。工业仪表把日志参数区放在 MRAM,把大块历史数据放在 TF 卡或者外部 Flash,这种分工是很常见的。

4. 存储与读取:实现一个工业日志缓冲区

4.1 512KB 怎么分

一颗 MR25H40CDF 有 512KB,地址范围从 0x00000 到 0x7FFFF。容量不算大,但用于参数区和日志区足够。我这里给的划分方式可以拿来直接用:

地址范围大小用途
0x00000 ~ 0x00FFF4KB出厂参数、校准系数、版本信息
0x01000 ~ 0x01FFF4KB掉电瞬间快照,关键运行数据备份
0x02000 ~ 0x7FFFF约 504KB循环日志区

参数区建议用双份备份。比如把 4KB 分成 A/B 两块,写入时先写 A,再写 B,读取时先读 A 并校验,如果 A 出错就用 B。对 MRAM 来说,连续写入本身可靠性很高,双份主要防的是“写一半断电”这种撕裂场景。只要保证读到 A 和 B 的版本号一致,参数就不会出现半新半旧的情况。

掉电快照区平时不写,只有在检测到电源异常才写入。因为地址固定,每次覆盖的都是同一块区域,所以写入量很小,也不会因为寿命问题产生担忧。

4.2 日志环形缓冲的实现

日志区用固定长度的记录格式比较省事。每条记录 64 字节,前 12 字节是管理头,后面 52 字节放业务数据。固定长度能避免需要记录偏移长度和解析复杂头部的麻烦。

#define LOG_REC_SIZE 64u #define LOG_BASE 0x02000u #define LOG_SIZE 0x7E000u typedef struct { uint32_t magic; // 固定魔数,用于识别有效记录 uint16_t crc; // 数据区 CRC uint16_t length; // 数据区实际长度,最大 52 uint32_t seq; // 记录序号,用于排序和恢复 uint8_t data[52]; // 业务数据 } LogRecord;

写入日志时,需要维护一个尾部指针,表示下一次写入的起始地址。每写完一条,尾部指针往后移 64 字节;如果尾部越过日志区末尾,就回绕到 LOG_BASE 重新开始,用新记录覆盖最旧记录。

static uint32_t logTail = LOG_BASE; static uint32_t logSeq = 0; uint16_t Crc16(uint8_t *p, uint16_t len) { // 自己实现或复用现有 CRC 函数 } void Log_Append(uint8_t *data, uint16_t len) { uint8_t raw[LOG_REC_SIZE]; LogRecord *rec = (LogRecord *)raw; if (len > 52) len = 52; rec->magic = 0x4D52414DUL; // 'MRAM' rec->length = len; rec->crc = Crc16(data, len); rec->seq = logSeq++; memset(rec->data, 0, 52); memcpy(rec->data, data, len); if (logTail + LOG_REC_SIZE > LOG_BASE + LOG_SIZE) { logTail = LOG_BASE; } MRAM_WriteBytes(logTail, raw, LOG_REC_SIZE); logTail += LOG_REC_SIZE; }

这里有一个管理技巧要强调:先写日志记录,后更新尾部指针。一些工程师更习惯先改尾部指针,再写记录,这样容易在掉电时出现一种麻烦状态——尾部已经指向了新位置,但实际数据还没写完整,重启后扫描会认为这里有一条记录,CRC 校验一错,反而把整个恢复逻辑搞乱。所以,尾部指针相当于“提交标记”,必须放在日志记录成功写入之后再更新。

如果业务系统里有很多线程或中断同时触达日志区,最好给 Log_Append 加一个简单临界区保护,否则两个调用方同时修改 logTail 和 logSeq,写入位置会错乱。PIC18F86K90 的存储规划其实很朴素,不要让多个上下文并发操作同一个尾部变量是最基础的防错原则。

4.3 读取日志和一致性校验

读取日志的接口不需要考虑擦除,直接按地址把整条记录读回来,然后做三项校验:魔数是否正确,CRC 是否匹配,序号是否连续。只要魔数不对,就说明这一条记录从未被写进去,或者被写了一半;只要 CRC 不对,就说明数据已经损坏或撕裂。

uint8_t Log_Read(uint32_t addr, LogRecord *rec) { uint8_t raw[LOG_REC_SIZE]; MRAM_ReadBytes(addr, raw, LOG_REC_SIZE); memcpy(rec, raw, sizeof(LogRecord)); if (rec->magic != 0x4D52414DUL) return 0; if (rec->crc != Crc16(rec->data, rec->length)) return 0; return 1; }

重启恢复的时候,从日志区起始地址开始,每 64 字节读一条,连续读取并记录最后一个校验通过的记录。由于日志是循环覆盖,新记录会覆盖旧记录,所以启动时并不需要从头扫到尾,而是从上次保存的尾部地址附近开始扫描即可。尾部本身也保存在 MRAM 的管理区里,这样能省掉大段时间。

扫描恢复时有个细节:如果读到连续几条 CRC 错误,不要立即停止,而是继续往后扫几条。因为突然断电可能只损坏当前最后一条记录的前半部分,后续记录可能也受到了影响,但跳过当前地址后,后面地址上的记录很可能是完整的。扫描策略我一般设定为“连续 8 条无效才退出扫描”,这样既能容忍单条撕裂,又能避免在整个空洞区里浪费时间。

4.4 掉电瞬间怎么写关键数据

PIC18F86K90 内部有低电压检测模块,可以把掉电检测做成一个中断。当 VDD 跌到设定阈值以下,CPU 还有几十到几百微秒的响应时间,只要在主电源彻底失效前把这部分时间利用起来,把关键数据写进 MRAM 就够了。

硬件上要给系统留一点掉电缓冲。最简单的方式是在 VDD 和 GND 之间增加一个几百微法的储能电容,让电源跌落在供电检测到异常后还能维持几个毫秒。软件上要尽量缩短保存时间,只写业务真正需要恢复的变量,比如当前温度、累计流量、运行状态、故障代码,不要把所有全局变量都往 MRAM 里倒。

中断里的保存代码大致是这样:

void __interrupt() LowVoltageISR(void) { if (PLVDIF) { PLVDIF = 0; MRAM_WriteBytes(0x01000, (uint8_t *)&sysSnapshot, sizeof(sysSnapshot)); } }

为什么掉电保存要用 MRAM 而不是 Flash?原因很清楚,Flash 断电时做一次页写入或者扇区擦除太慢,很可能写到一半电源就彻底没了,而 MRAM 的 WRITE 指令一旦时钟送完,数据就稳定落在存储单元里,无需担心后续的擦写等待被断电打断。这也是整个方案的核心价值所在。

5. 用公式算一遍:这套方案能扛多久

5.1 写入寿命核算

寿命计算比想象中简单。MR25H40CDF 的容量是 512KB,我们以每 200ms 写一条 64 字节记录为例:

  • 每秒写 5 条,也就是 320B/s。
  • 一天下来写入量是 27.6MB。
  • 日志区按 504KB 算,一天大约循环覆盖 55 次。
  • 一年大约循环覆盖 2 万次。

如果换成 EEPROM,按每字节一百万次寿命计算,覆盖 2 万次根本不算问题。那真正的差异体现在哪?体现在“固定地址高频覆盖”的场景。比如掉电快照区固定在 0x01000 地址,每次掉电都往这里写,一天掉电 10 次就是 10 次写入,一年才 3650 次,EEPROM 也撑得住。但如果是实时计数器,每 10ms 更新一次同一个地址,一天就是 864 万次写入,EEPROM 一天多就报废了。MRAM 的耐久度可以达到 10 的 14 次方以上,在这个量级下,“固定地址反复写”不再需要磨损均衡。

所以,MRAM 带来的不只是寿命数字变大,更是让软件设计从“我该怎么避免反复写某些地址”变成“我直接固定地址写就行”。这价值在工程上比单纯寿命数字更重要。

项目里还要留一个余量认知:MRAM 不是无限寿命,但它的寿命上限远远超过设备本身的使用周期,按 10 年甚至 15 年设计,不需要额外做磨损均衡。工业设备设计时少一个均衡算法,少一次代码混淆,就能少一类 bug 来源。

5.2 数据保持与温度影响

MRAM 的数据保持时间主要受温度影响。工业级 MRAM 在 85℃ 环境下通常能保持 20 年以上,温度越高,保持时间越短,但这个衰减曲线比 Flash 的电荷泄漏要平缓得多。Flash 靠浮栅电荷表示数据,电荷会缓慢泄漏,极端高温下几年甚至几个月就可能丢数据。MRAM 靠磁化方向表示数据,没有电荷泄漏通道,数据保持能力是物理稳定性决定的。

对工业仪表来说,机柜内部温度通常不会持续超过 70℃,所以 20 年的保持时间基本够用。如果你的设备会长期工作在 105℃ 以上的恶劣环境,选型时不仅看保持时间,还要看 MRAM 的最高工作温度等级,不同后缀代表不同温区,项目定型前先对准数据手册。

数据保持和写入次数之间没有直接强耦合,这一点也让我省心。使用 Flash 时,写入次数快到寿命上限后,数据保持能力会快速下降;MRAM 的写入发生多次后,数据保持能力依然由温度决定,和写入历史关系不大。所以现场运行多年的设备,读旧日志时依然能保证完整读出。

5.3 为什么不用电池备份 SRAM 或者 FRAM

电池备份 SRAM 是传统工业控制器里常用的方案。SRAM 速度快,写入无限次,断电时靠电池供电保持数据,听起来很完美。但电池本身是消耗品,工业工程师最头疼的就是电池更换周期。设备装在现场,三年后电池电量耗尽,如果没及时更换,所有关键数据直接清零。MRAM 不需要外接电池,非易失性天然存在,少掉一个维护点。

FRAM 和 MRAM 更像,但 FRAM 的容量密度通常不如 MRAM,而且 FRAM 在高温下数据保持时间相对短一些。这里不是要否定 FRAM,它在某些场景下同样优秀,只是针对“工业仪表 + 频繁掉电保存 + 长期高温柜内运行”这个组合,MRAM 会更合适。选型时把功耗、写入速度、保持时间和温度范围放在同一张表里比较,很快就能得出结论。

6. 调试实录:那些“应该能跑”但没跑起来的瞬间

6.1 读取全是 0xFF 或 0x00

这个现象大概率发生在第一次上电自检时。读出来的全是 0xFF,通常说明 MISO 线上没有数据过来,可能原因有三个:一是 CS 没有正确拉低,二是 MISO 接错引脚,三是 SPI 时钟没有跑起来。

读出来全是 0x00 的情况比较特殊,多半是 CS 引脚被强制拉低,或者 MISO 和 SI 两根线焊反了。MRAM 的 SPI 在芯片不被选中时,SO 是高阻态,如果你用万用表量 MISO 对地阻抗,应该看到高阻;如果看到接近 0Ω,就要查 MISO 引脚是不是和地短路。

还有一种常见原因是 SPI 极性不对。MR25H40CDF 支持模式 0 或模式 3,但如果代码初始化成了模式 2,芯片根本不会正常采样。调试时用逻辑分析仪抓 CS、SCK、SI、SO 四个信号,看 SI 上是否有完整的命令字节,SO 上是否在指令后的高阻期变成有效数据,基本能一眼定位问题。

6.2 状态寄存器 WEL 一直为 0

我遇到过最典型的低级错误就是 WREN 之后 CS 没有拉高,然后紧接着把 WRITE 指令发进去。芯片认为从 CS 拉低开始的所有字节都属于同一条指令,WREN 被当成某条多字节指令的命令码,WRITE 自然没有生效,WEL 也一直是 0。

正确做法是:把 WREN 和 WRITE 拆成两个完整的 SPI 事务。每个事务都以 CS 拉低开始,以 CS 拉高结束。中间不能插入其他命令。检查代码时,把 MRAM_EnableWrite 和 MRAM_WriteBytes 分清楚,确保每次调用 MRAM_WriteBytes 前都单独执行一次 WREN。

另外,如果你给 MRAM 写过状态寄存器,可能把写保护打开了。MR25H40CDF 的状态寄存器里有 WPEN 位,一旦使能硬件写保护,WREN 置位后也可能写不进去。调试时先读 RDSR,看状态寄存器里的保护位是否为 0,如果有保护,执行一次 WRSR 把保护关掉,或者直接检查 WP 引脚是否被意外拉低。

6.3 偶发一条日志损坏

偶发性损坏通常分两种情况。第一种是信号完整性问题,SPI 时钟频率太高、PCB 走线太长、或者干扰源太强,造成某个位被翻转。出现这种情况时,先降频测试,比如把 SPI 时钟从 8MHz 降到 1MHz,如果故障消失,说明是信号质量问题。调整走线、加串联电阻、缩短 SPI 线缆是常规解决方向。

第二种情况是软件上的并发问题。如果在写入过程中有中断改了 SPI 寄存器、或者另一个任务也调用了 MRAM 写函数,日志就会被交错的数据打破。处理方式是在 MRAM 读写函数前关闭相关中断,或者加一个简单的互斥标志,保证同一时间只有一段代码能操作 SPI 总线。

日志读取端的 CRC 校验能发现这种偶发性损坏,但不能防止它发生。完成一条日志写入后,还可以主动读回一次数据,和源缓冲区逐字节比对,发现不一致就重写一次。对 MRAM 来说,重写成本很低,不需要担心额外磨损。

6.4 掉电测试时的“幽灵记录”

掉电测试最容易出现的问题就是日志区出现“幽灵记录”。断电瞬间,记录已经写进去一半,重启后扫描看到魔数有效,但 CRC 错误,这类记录就是半写记录。如果把尾部指针在记录写入前就更新了,那么重启时扫描还会把这条半写记录当成有效位置,后续写入会跳过它,日志区就多出一个永远无法覆盖的坏块。

解决办法就是我之前强调的“先写数据,后更新尾部指针,再写提交标记”。恢复扫描时遇到 CRC 错误,就判定这一条记录无效,并从这个地址继续往后扫描,直到找到下一个完整记录。扫描算法要设置合理的连续失败次数上限,比如连续 8 条失败才放弃,这样能兼容极少数总线上一次多字节帧被中断的极端情况。

6.5 写入后奇怪地“挪位”了

还有一种隐蔽问题:地址字节发错了。MRAM 虽然只需要 19 位地址,但 SPI 地址字段仍是 3 个字节,第 1 字节的高位必须保持 0,第 2、第 3 字节是你实际的地址位。如果把地址参数从 16 位变量简化成 2 字节发送,超过 64KB 的地址区域就会随机落到另一个地址段,读回时发现数据“漂移”了。排查方法很简单,往地址 0x10000 写一串递增数据,再读回来,确认数据停留在 0x10000,而不是跑到 0x00000。

7. 能直接抄的出厂自检函数

7.1 启动自检的思路

生产线上每块板子焊完,最好在出厂前跑一遍 MRAM 自检,避免把虚焊、贴片偏移、MISO 断线等问题漏到用户现场。自检不宜太复杂,用一小块测试地址区,写入固定模式,再读回比较,出错就报故障。

MRAM 自检方式可以选择全部测试或者抽测。全部测试需要遍历所有 512KB,时间稍长;抽测则选几个代表性地址,比如最低地址、最高地址、中间地址。如果考虑到信号完整性和焊盘问题,我建议至少写三种固定模式:全 0x00、全 0xFF、递增序列。这三种模式能暴露大部分位线粘连和地址译码问题。

7.2 自检代码片段与判定逻辑

#define SELF_TEST_ADDR 0x10000u #define TEST_BLOCK_LEN 256u uint8_t MRAM_SelfTest(void) { uint8_t buf[TEST_BLOCK_LEN]; uint16_t i; for (i = 0; i < TEST_BLOCK_LEN; i++) { buf[i] = (uint8_t)i; } MRAM_WriteBytes(SELF_TEST_ADDR, buf, TEST_BLOCK_LEN); MRAM_ReadBytes(SELF_TEST_ADDR, buf, TEST_BLOCK_LEN); for (i = 0; i < TEST_BLOCK_LEN; i++) { if (buf[i] != (uint8_t)i) { return 0; } } return 1; }

这个函数的生产价值在于,它能把“芯片没焊好”“引脚信号反了”“SPI 模式配错”这类问题在产线阶段直接暴露出来。每次复位或上电后,如果这个自检不通过,设备就可以直接停在故障界面,现场维修人员也能节省大量时间。

我调试这个组合时还有一个心得:不要把自检放在所有外设初始化之后再做,放在最前面,越早越好。否则 LCD 驱动、传感器初始化、通讯协议这些代码都跑起来后,环境变量已经变复杂,一旦自检失败反而难判断是不是存储问题。把自检放在主循环开头,用一个小 LED 指示结果,生产线的测试员只要看灯就能知道这块板子能不能进入下一步。

返回列表