1. 项目缘起与方案选型
1.1 为什么要在工业场景里折腾 MRAM 和 MKV58 这套组合
工业现场的数据存储有个很尴尬的处境:用 EEPROM 吧,写入速度慢、擦写寿命有限,频繁记录日志很快就写废了;用 Flash 吧,写之前得先擦一整块,掉电瞬间还容易丢数据;用带电池的 SRAM 吧,电池在高温高湿环境下两三年就鼓包漏液,维护成本高得离谱。我前几年做过一个电力监测终端,现场环境夏天机柜内温度能到 70 度,后备电池撑不过第二个夏天,返修率一直压不下来。
后来接触到 MRAM(磁性随机存储器),这东西的特性正好卡在工业需求的点上:非易失、写入不需要擦除、字节级随机写入、读写速度接近 SRAM、擦写寿命几乎无限(10^14 次以上量级)、宽温工作。Everspin 的 MR25H40CDF 就是一颗 4Mbit 的 SPI 接口 MRAM,8 脚 SOIC 封装,直接替换同封装的 SPI EEPROM 或 Flash 就能用,硬件上几乎不用改板。
主控这边选的是 NXP 的 MKV58F1M0VLQ24,Kinetis V 系列,Cortex-M7 内核,240MHz 主频,带浮点单元,LQFP-144 封装,1MB Flash、256KB SRAM。选它主要看中两点:一是 M7 的算力足够跑电机控制或数据采集算法,二是它的 FlexSPI 和 LPSPI 外设配置灵活,跟 MRAM 对接很顺。这套组合我实际用在了一个工业数据记录仪项目上,连续跑了两年多,没出过存储相关的故障,所以把踩过的坑和配置细节整理出来。
这篇文章适合谁看:正在做工业数据采集、需要高可靠非易失存储的嵌入式工程师;想从 EEPROM/Flash 迁移到 MRAM 但不确定怎么下手的硬件软件开发者;以及用 Kinetis V 系列做项目、需要 SPI 外设配置参考的人。下面我会从硬件连接、SPI 时序配置、读写驱动、掉电保护策略几个层面,把整套方案拆开讲。
1.2 MR25H40CDF 的关键参数与选型逻辑
先把这颗 MRAM 的核心参数摆出来,选型时对着看:
| 参数项 | 数值 | 说明 |
|---|---|---|
| 容量 | 4 Mbit (512K x 8) | 够存几万条日志记录 |
| 接口 | SPI,最高 40MHz | 支持 Mode 0 和 Mode 3 |
| 供电电压 | 2.7V ~ 3.6V | 典型 3.3V |
| 工作温度 | -40°C ~ +85°C | 工业级 |
| 写入寿命 | 10^14 次/字节 | 基本可以忽略寿命问题 |
| 数据保持 | 20 年 | 无需电池 |
| 写入时间 | 无延迟,字节级 | 不像 Flash 要等擦除 |
| 封装 | 8-SOIC | 兼容标准 SPI EEPROM 脚位 |
选它的核心理由是写入无延迟。Flash 写一个字节要先擦一个扇区(通常 4KB),擦除时间几毫秒到几十毫秒,这期间如果掉电,整个扇区的数据都可能丢。MRAM 是直接覆盖写,写一个字节就是写一个字节,没有擦除步骤,掉电时最多丢正在写的那一个字节,配合合理的写入策略,数据完整性有保障。
另一个隐性优势是不需要磨损均衡。用 Flash 存日志,你得写一套磨损均衡算法,把写入分散到不同扇区,否则某个扇区先写坏。MRAM 寿命 10^14 次,假设每秒写一次,一年 3150 万次,要写 300 多万年才到寿命上限,磨损均衡这套逻辑可以直接砍掉,代码复杂度大幅下降。
1.3 MKV58F1M0VLQ24 的 SPI 外设资源分配
MKV58 有几个 SPI 相关的模块,得先理清楚用哪个:
- LPSPI:低功耗 SPI,MKV58 上有多个实例,支持主机/从机模式,FIFO 深度 4 级,最高时钟受总线频率限制。
- FlexSPI:主要给外部 Flash 用的,支持 Quad/Octal 模式,接 MRAM 有点大材小用,而且配置复杂。
- DSPI:传统 SPI,功能全但寄存器多。
我选的是LPSPI0,原因很简单:MRAM 只需要标准 SPI 模式(单线 MOSI/MISO),40MHz 时钟,LPSPI 完全够用,而且它的 FIFO 和中断机制比 DSPI 清爽,代码好维护。FlexSPI 留给以后可能扩展的外部程序 Flash。
引脚分配上,LPSPI0 我用的是:
- PTC4 → LPSPI0_SCK
- PTC5 → LPSPI0_SIN (MISO)
- PTC6 → LPSPI0_SOUT (MOSI)
- PTC7 → LPSPI0_PCS0 (片选,硬件片选)
这里有个细节:MRAM 的片选我用了硬件片选(PCS0),没用软件 GPIO 控制。原因是 40MHz 时钟下,软件控制片选的时序抖动太大,容易在片选拉低和第一个时钟沿之间产生不确定的延迟,导致命令解析错误。硬件片选由 LPSPI 模块自动控制,时序精确。如果你的板子片选脚没接到 LPSPI 的 PCS 上,那就只能用软件片选,但建议把时钟降到 10MHz 以下留余量。
2. 硬件连接与信号完整性要点
2.1 原理图连接与去耦电容布置
MR25H40CDF 的 8 个引脚定义很标准:
| 引脚 | 名称 | 连接 |
|---|---|---|
| 1 | CS | 主控 PCS0 |
| 2 | SO | 主控 MISO |
| 3 | WP | 接 VCC(不用的写保护) |
| 4 | VSS | GND |
| 5 | SI | 主控 MOSI |
| 6 | SCK | 主控 SCK |
| 7 | HOLD | 接 VCC |
| 8 | VCC | 3.3V |
WP 和 HOLD 这两个脚在标准 SPI 模式下不用,直接拉高到 VCC。注意别悬空,悬空的话引脚电平不确定,可能意外进入写保护或保持状态,读出来的数据全是 0xFF,这个坑我踩过,查了半天以为是驱动问题。
去耦电容这块,MRAM 的 VCC 脚旁边必须放一个 0.1uF 的陶瓷电容,紧贴引脚,距离不超过 2mm。另外在电源入口再放一个 1uF 的。为什么这么讲究?MRAM 在写入瞬间的电流会有尖峰,虽然平均电流不大,但尖峰如果被电源纹波带下去,可能导致写入失败。工业现场电源本来就脏,去耦做不好,偶发的写入错误很难排查。
2.2 PCB 走线与 SPI 信号完整性
40MHz 的 SPI 时钟,在 PCB 上已经算高速信号了,走线不能随便拉。我的经验是:
- SCK 走线尽量短,最好控制在 5cm 以内,超过的话要考虑串联端接电阻(22Ω~33Ω)。
- SCK 和 MOSI 不要平行走太长,避免串扰。如果必须平行,中间加地线隔离。
- MISO 是 MRAM 输出,主控输入,这条线的驱动能力由 MRAM 决定,走线长了上升沿会变缓,读数据可能出错。
- 地平面要完整,SPI 信号线下面最好有连续的地平面,提供回流路径。
我第一版板子 SCK 走了 8cm,还没做端接,结果 40MHz 下读 MRAM 的 ID 都读不对,降到 20MHz 才正常。后来改板把 SCK 缩短到 3cm,加了 33Ω 串联电阻,40MHz 稳定跑。所以如果你发现高速下通信不稳定,先别怀疑代码,拿示波器看 SCK 和 MISO 的波形,过冲和振铃一眼就能看出来。
提示:调试 SPI 通信时,示波器看 SCK 上升沿是否有过冲、MISO 数据建立时间是否足够,比盯着代码看效率高得多。
2.3 电源时序与上电复位
MRAM 和主控的上电时序要注意。MKV58 的 IO 电压是 3.3V,MRAM 也是 3.3V,理论上可以同时上电。但如果主控先上电、MRAM 后上电,主控在 MRAM 还没准备好时就发命令,MRAM 不会响应,读回来全是 0xFF。
稳妥的做法是在初始化代码里加一个延时,等 MRAM 上电稳定后再操作。MR25H40CDF 的上电时间(VCC 上升到 2.7V 后到可操作)典型值是 1ms 以内,我在初始化里加了 5ms 延时,留足余量。另外 MRAM 上电后不需要任何初始化命令,直接就能读写,这点比 Flash 省事,Flash 还得发一堆配置命令。
3. LPSPI 驱动配置与 MRAM 读写实现
3.1 LPSPI 时钟配置与波特率计算
MKV58 的 LPSPI 时钟源来自总线时钟,假设总线时钟 60MHz,要得到 40MHz 的 SPI 时钟,分频系数怎么算?
LPSPI 的波特率公式是:
SPI_CLK = LPSPI_CLK / (SCKDIV + 2)其中 SCKDIV 是分频寄存器 TCR 里的 SCKDIV 字段,取值 0~255。
要得到 40MHz:
SCKDIV + 2 = 60MHz / 40MHz = 1.51.5 不是整数,所以 40MHz 得不到精确值。取 SCKDIV = 0,SPI_CLK = 60/2 = 30MHz;取 SCKDIV = 1,SPI_CLK = 60/3 = 20MHz。
这里有个取舍:MRAM 支持 40MHz,但主控分频出不来正好 40MHz,那就用 30MHz。实测 30MHz 下 MRAM 读写完全稳定,没必要为了凑 40MHz 去调总线时钟。如果你非要用 40MHz,可以把 LPSPI 时钟源切到 PLL 分频出来的更高频率,但会增加功耗和配置复杂度,工业场景下 30MHz 的吞吐量已经够用了。
配置代码大致这样:
// LPSPI0 时钟配置,假设 LPSPI_CLK = 60MHz LPSPI0->TCR = LPSPI_TCR_PRESC(0) | // 预分频 1 LPSPI_TCR_SCKDIV(0); // 分频 0,SPI_CLK = 60/2 = 30MHz // 配置为 SPI Mode 0 (CPOL=0, CPHA=0) LPSPI0->TCR |= LPSPI_TCR_CPOL(0) | LPSPI_TCR_CPHA(0); // 主机模式,8 位传输 LPSPI0->TCR |= LPSPI_TCR_FRAMESZ(7); // 8 位 = FRAMESZ 7MR25H40CDF 支持 Mode 0 和 Mode 3,我用的是 Mode 0(CPOL=0,CPHA=0),即时钟空闲低电平,数据在上升沿采样。这个模式最通用,跟大多数 SPI 设备一致。
3.2 MRAM 命令集与读写时序
MR25H40CDF 的命令集很精简,常用的就几条:
| 命令 | 编码 | 功能 |
|---|---|---|
| WREN | 0x06 | 写使能 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| WRITE | 0x02 | 写数据 |
| RDID | 0x9F | 读设备 ID |
读数据的时序:拉低 CS → 发 0x03 → 发 3 字节地址(24 位,因为 4Mbit = 512KB,需要 19 位地址,用 3 字节覆盖)→ 连续读数据 → 拉高 CS。
写数据的时序:拉低 CS → 发 0x06(WREN)→ 拉高 CS → 拉低 CS → 发 0x02 → 发 3 字节地址 → 发数据 → 拉高 CS。
注意 WREN 是一个独立的事务,必须先发 WREN 再发 WRITE,中间 CS 要拉高一次。我见过有人把 WREN 和 WRITE 放在同一个 CS 低电平周期里,结果写不进去,因为 MRAM 要求 WREN 命令结束后 CS 有个上升沿来锁存写使能状态。
3.3 底层读写函数实现
先封装两个基础函数:发一个字节和收一个字节。LPSPI 的收发是同一个数据寄存器,写进去就发,读出来就收。
uint8_t LPSPI_TransferByte(uint8_t data) { // 等待发送 FIFO 有空位 while (!(LPSPI0->SR & LPSPI_SR_TDF_MASK)); LPSPI0->TDR = data; // 等待接收 FIFO 有数据 while (!(LPSPI0->SR & LPSPI_SR_RDF_MASK)); return (uint8_t)LPSPI0->RDR; }基于这个,实现 MRAM 读:
void MRAM_Read(uint32_t addr, uint8_t *buf, uint32_t len) { CS_LOW(); LPSPI_TransferByte(0x03); // READ 命令 LPSPI_TransferByte((addr >> 16) & 0xFF); // 地址高字节 LPSPI_TransferByte((addr >> 8) & 0xFF); LPSPI_TransferByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { buf[i] = LPSPI_TransferByte(0x00); // 发 dummy 收数据 } CS_HIGH(); }MRAM 写:
void MRAM_Write(uint32_t addr, const uint8_t *buf, uint32_t len) { // 先发写使能 CS_LOW(); LPSPI_TransferByte(0x06); // WREN CS_HIGH(); // 再发写命令和数据 CS_LOW(); LPSPI_TransferByte(0x02); // WRITE LPSPI_TransferByte((addr >> 16) & 0xFF); LPSPI_TransferByte((addr >> 8) & 0xFF); LPSPI_TransferByte(addr & 0xFF); for (uint32_t i = 0; i < len; i++) { LPSPI_TransferByte(buf[i]); } CS_HIGH(); }这里 CS_LOW() 和 CS_HIGH() 如果是硬件片选,其实不用手动控制,LPSPI 在传输开始和结束时自动拉低拉高 PCS。但为了代码清晰,也为了兼容软件片选的场景,我习惯显式写出来。如果用硬件片选,这两个宏定义为空即可,实际片选由 LPSPI 的 TCR 里 PCS 字段控制。
3.4 读设备 ID 验证通信
上电后第一件事是读设备 ID,确认通信正常。MR25H40CDF 的 RDID 命令返回 1 字节的制造商 ID 和 2 字节的设备 ID。制造商 ID 是 0xEverspin 的编码,设备 ID 对应容量。
uint8_t MRAM_ReadID(void) { uint8_t id; CS_LOW(); LPSPI_TransferByte(0x9F); // RDID id = LPSPI_TransferByte(0x00); LPSPI_TransferByte(0x00); LPSPI_TransferByte(0x00); CS_HIGH(); return id; }如果读回来是 0x00 或 0xFF,说明通信有问题。0xFF 通常是 MISO 一直高,可能是 MRAM 没上电或片选没拉低;0x00 可能是 MISO 一直低或时钟没出来。用示波器抓一下 SCK 和 MISO,基本能定位。
4. 数据存储策略与掉电保护
4.1 日志存储的环形缓冲区设计
MRAM 容量 512KB,存工业日志怎么组织?我用的是环形缓冲区加索引的方式。
把 MRAM 分成三个区域:
- 头部区(0x00000 ~ 0x000FF):存元数据,包括写指针、读指针、记录总数、魔数、校验和。
- 数据区(0x00100 ~ 0x7FFFF):存实际记录,每条记录固定长度,比如 64 字节。
- 备份头部区(0x80000 ~ 0x800FF):头部区的镜像,防止头部写坏。
每条记录 64 字节,数据区约 512KB - 256B ≈ 511.7KB,能存约 8000 条记录。写满后覆盖最旧的记录,形成环形。
头部区的写指针每次写记录后更新。这里的关键是:先写数据,再更新指针。如果先更新指针再写数据,掉电时指针指向了一条没写完的记录,读出来是脏数据。先写数据再更新指针,最坏情况是掉电时数据写完了但指针没更新,这条记录下次会被覆盖,但不会读到脏数据。
4.2 掉电检测与写入原子性
工业现场掉电是常态,掉电保护必须做。MKV58 有内置的电源检测模块,可以配置一个低压中断,当 VCC 降到阈值以下时触发中断,在中断里把关键数据刷进 MRAM。
但这里有个时间窗口问题:从检测到掉电到电源彻底跌到 MRAM 无法工作,可能只有几毫秒。这期间要完成:进入中断、保存现场、写 MRAM。如果数据量大,几毫秒不够。
我的做法是双缓冲加原子标志:
- 正常运行时,数据先写到 SRAM 的缓冲区。
- 每积累一定量(比如 1KB)或每隔固定时间(比如 100ms),把缓冲区刷到 MRAM。
- 刷的时候先写一个"写入中"标志到 MRAM 的特定地址,写完数据后把标志改成"写入完成"。
- 掉电中断里只做一件事:如果缓冲区还有未刷的数据,立即刷一次。
这样即使掉电时正在刷数据,重启后读标志位就知道上次刷写是否完成,没完成的话那条记录丢弃,不影响其他数据。
MRAM 的字节级写入特性在这里帮了大忙:写标志位和写数据是独立的字节操作,不需要擦除整个扇区,所以"写入中"标志的更新不会影响数据区的内容。如果用 Flash,写标志位要先擦扇区,整个扇区的数据都没了,根本没法做这种细粒度的原子操作。
4.3 数据校验与坏块管理
虽然 MRAM 寿命几乎无限,但工业环境的电磁干扰可能导致位翻转。每条记录我加了 CRC16 校验,读出来先校验,校验不过就标记为坏记录跳过。
CRC16 用查表法实现,速度快:
static const uint16_t crc16_table[256] = { ... }; // 标准 CRC16-CCITT 表 uint16_t CRC16_Calc(const uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) { crc = (crc << 8) ^ crc16_table[((crc >> 8) ^ data[i]) & 0xFF]; } return crc; }坏块管理方面,MRAM 不像 NAND Flash 有出厂坏块,但为了保险,我在头部区维护了一个坏记录计数,如果连续读到多条 CRC 错误的记录,就把这个区域标记为可疑,后续写入跳过。这个机制在实际项目中触发过一次,后来查出来是电源纹波导致的写入干扰,加了去耦电容后就没再出现。
5. 实操调试与常见问题排查
5.1 通信失败的分层排查法
SPI 通信调不通,别一上来就改代码,按层次排查效率最高:
| 层级 | 检查项 | 工具 |
|---|---|---|
| 硬件层 | 供电是否 3.3V,WP/HOLD 是否拉高 | 万用表 |
| 连接层 | SCK/MOSI/MISO/CS 是否接对,有无虚焊 | 万用表通断档 |
| 信号层 | SCK 有无波形,频率是否正确 | 示波器 |
| 协议层 | 命令和地址字节是否正确 | 逻辑分析仪 |
| 驱动层 | LPSPI 寄存器配置是否正确 | 调试器读寄存器 |
我遇到最多的问题是片选时序。软件片选时,如果 CS 拉低后没有延时就直接发时钟,MRAM 可能还没识别到片选有效,第一个字节就丢了。解决方法是 CS 拉低后加几个 NOP 延时,或者直接用硬件片选。
另一个常见问题是时钟极性配错。Mode 0 和 Mode 3 的差别在时钟空闲电平,配错了数据采样点就错位,读出来全是乱码。MRAM 支持这两种模式,但主控配置必须和实际接线一致。如果不确定,两种模式都试一下,能读出正确 ID 的那个就是对的。
5.2 写入失败的典型原因
写入失败比读取失败更难查,因为读回来可能是旧数据,看起来像"写没生效"。常见原因:
- WREN 没发或发错:写之前必须发 0x06,且 CS 要有上升沿。用逻辑分析仪抓一下写时序,看 WREN 和 WRITE 之间 CS 有没有拉高。
- 地址越界:MRAM 只有 512KB,地址超过 0x7FFFF 会回绕到 0,写到了不该写的地方。检查地址计算有没有溢出。
- 写保护引脚:WP 脚如果没拉高,MRAM 处于写保护状态,写命令被忽略。这个最隐蔽,因为读正常,就是写不进去。
- 电源跌落:写入瞬间电流尖峰导致 VCC 跌落,写入失败。示波器看 VCC 在写操作时有没有毛刺。
提示:调试写入问题时,写完立即读回来对比,比隔一段时间再读更容易定位。因为如果写失败,立即读能读到旧数据或 0xFF,而隔一段时间读可能被其他操作覆盖,现象就乱了。
5.3 性能实测与优化
实测数据(LPSPI 30MHz,MKV58 240MHz):
| 操作 | 耗时 | 吞吐量 |
|---|---|---|
| 单字节读 | 约 1.2us | — |
| 单字节写 | 约 2.5us | — |
| 256 字节连续读 | 约 72us | 3.5 MB/s |
| 256 字节连续写 | 约 75us | 3.4 MB/s |
| 1KB 连续读 | 约 280us | 3.6 MB/s |
连续读写比单字节操作效率高很多,因为省去了命令和地址的开销。所以存日志时尽量批量写,别一条一条写。我最初是一条记录写一次,100ms 写一条,CPU 开销可以忽略;后来改成 1KB 批量写,同样的数据量写入次数少了,整体功耗也降了。
如果要进一步提升吞吐量,可以启用 LPSPI 的 DMA 模式,让 DMA 搬运数据,CPU 去干别的。不过对于日志存储这种低频场景,DMA 的复杂度不值得,中断模式或轮询就够了。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 读 ID 返回 0xFF | MISO 悬空或 MRAM 未上电 | 检查供电和 MISO 连接 |
| 读 ID 返回 0x00 | SCK 无输出或片选未拉低 | 检查 LPSPI 时钟配置和片选 |
| 读数据错位 | SPI 模式配错 | 切换 Mode 0/Mode 3 测试 |
| 写入不生效 | WREN 未发或 WP 未拉高 | 检查写时序和 WP 引脚 |
| 偶发数据错误 | 电源纹波或信号完整性问题 | 加去耦电容,缩短走线 |
| 高速下不稳定 | SCK 过冲或走线过长 | 降频或加端接电阻 |
| 掉电后数据丢失 | 未做掉电保护 | 加掉电检测和双缓冲 |
6. 几个容易被忽略的实操细节
6.1 MRAM 不需要擦除但要注意写入边界
MRAM 虽然字节级写入,但如果你一次写跨越了页边界(MR25H40CDF 的页大小是 256 字节),内部逻辑会分多次写,中间如果掉电,可能只写了一部分。所以批量写的时候,尽量让每次写的范围不跨页,或者跨页时做好记录,知道写到哪了。
我的做法是记录长度固定 64 字节,地址按 64 字节对齐,这样永远不会跨页。简单粗暴但有效。
6.2 初始化顺序影响稳定性
MKV58 的初始化顺序我调整过好几次,最终稳定的顺序是:
- 系统时钟配置(先配 PLL,再配总线分频)
- GPIO 配置(片选脚先配成输出高,避免上电瞬间误触发)
- LPSPI 配置(时钟、模式、帧格式)
- 延时 5ms 等 MRAM 上电稳定
- 读 ID 验证通信
- 读头部区,校验魔数,决定是初始化还是加载已有数据
第 2 步很关键:片选脚在上电时如果默认是低电平,MRAM 会以为主控在跟它通信,可能误解析噪声为命令。先把片选配成输出高,再配 LPSPI,能避免这个问题。
6.3 温度对 MRAM 的影响
MRAM 的工作温度范围是 -40°C ~ +85°C,但极端温度下写入电流会变化。我在 -30°C 的冷库环境测试过,读写正常,但写入时间比常温略长(微秒级差异,不影响功能)。高温 +85°C 下连续写入,MRAM 表面温度会升高,但因为有去耦和合理的 PCB 散热,实测芯片温度在 90°C 左右,还在范围内。
如果你的应用环境超过 85°C,得选更高温度等级的型号,或者加散热措施。工业场景一般不会超,但如果是车载或户外设备,要留意。
6.4 代码可移植性设计
这套驱动我后来移植到了 STM32 和 ESP32 上,因为把硬件相关部分抽象成了几个宏:
#define CS_LOW() (LPSPI0->TCR |= LPSPI_TCR_PCS(0)) // 硬件片选 #define CS_HIGH() (LPSPI0->TCR &= ~LPSPI_TCR_PCS(0)) #define SPI_XFER(b) LPSPI_TransferByte(b)换平台时只改这三个宏和 LPSPI_TransferByte 的实现,上层 MRAM 读写逻辑完全不用动。这个抽象在项目后期换主控时省了大量时间,建议一开始就这么设计。
7. 写在最后的一点个人体会
这套 MRAM 加 MKV58 的方案,我从第一版调试到现在稳定运行,前后改了三四版硬件、重构过两次驱动。最大的体会是:存储方案的可靠性,一半在芯片选型,一半在软件策略。MRAM 本身很皮实,但如果你写入策略没做好,掉电照样丢数据;反过来,即使芯片有各种保护机制,软件不配合也白搭。
另外,调试 SPI 设备时,逻辑分析仪比示波器更好用。示波器看波形质量,逻辑分析仪看协议内容,两个配合,基本没有查不出来的问题。我用的是一款几十块钱的 8 通道逻辑分析仪,抓 SPI 时序足够了,比盯着代码猜效率高太多。
如果你也在做工业数据存储,建议先用 MRAM 打个样,把读写和掉电保护跑通,再根据实际数据量决定要不要上文件系统。对于大多数日志记录场景,环形缓冲区加 CRC 校验就够了,上文件系统反而增加复杂度和不确定性。