工业嵌入式系统的数据存储和数据读取,在很长一段时间里被我当作最没有技术含量的部分。直到做产线数据采集终端时,接连遇到掉电丢最后一条日志、Flash 反复擦写后数据错乱、现场维护人员抱怨记录读不出来等状况,我才真正把注意力放到存储介质选型上。最终定下的方案是 Everspin 的 MR25H40CDF 这颗 4Mbit SPI MRAM,搭配 TM4C129EKCPDT 控制器,把整个存储链路重新设计了一遍。这篇内容就围绕这个组合展开,覆盖芯片选型逻辑、硬件连接、驱动实现、掉电安全策略以及实际踩坑记录,给同样在工业嵌入式场景里做数据记录的朋友一个可参考的完整方案。
1. 工业现场的数据保存痛点:相比 Flash 与 EEPROM,MRAM 到底赢在哪
1.1 掉电、寿命与延迟:三个数据的真实代价
工业设备里的数据存储,和消费电子完全不是一回事。消费场景写坏了 Flash,顶多手机重启一下;工业现场写坏了存储,轻则丢一批生产参数,重则整条产线停机等维修。我之前在几个项目里用过 SPI NOR Flash 和 SPI EEPROM,各有各的麻烦。
先看 SPI NOR Flash。它便宜、容量大,但有三座大山:
- 写入前必须先擦除,擦除以扇区或块为单位,一个 4KB 扇区擦除时间常常要几十到上百毫秒;
- 典型写擦寿命在 10 万次左右,实际工业高温环境下还要打折扣,频繁记录日志时要做损耗均衡;
- 掉电瞬间如果正好在做擦除,这一块数据可能整块报废,后面再写也会出错。
再看 SPI EEPROM。它支持按字节改写,不需要先擦除,写寿命普遍能到 100 万次以上,听起来不错。但容量普遍偏小,常见的 256Kbit 甚至 64Kbit,存几百条带时间戳的报警记录就满了。而且 EEPROM 写入有固定编程时间,比如 5ms 左右,尽管内部不会因为掉电写坏单字节,但整体吞吐量被压得很低,日志类应用很难撑起来。
那 MRAM 呢?MR25H40CDF 是 Everspin 的串行 MRAM,存储单元靠磁阻状态保存数据,而不是像 Flash 那样靠浮栅电荷。电荷会泄漏、隧道氧化层会磨损,磁阻状态本质上没有这些限制,所以它天然具备三个工业场景最看重的特性:写入无需擦除、读写寿命理论无限、掉电后数据能保持 20 年以上。
1.2 选 MR25H40CDF 的具体理由
当时在我面前其实有几种选择:换更大容量的 SPI NOR Flash、用铁电存储器 FRAM、或者上 MRAM。最后选 MR25H40CDF,主要是这几个原因叠加:
- 4Mbit 容量对“关键参数 + 运行日志 + 报警记录”这个量级刚好,512KB 比常见 EEPROM 大得多,又不像大容量 NOR Flash 那样需要复杂文件系统;
- 它用标准 SPI 接口,主控端只要 3 根信号线加一根片选,TM4C129EKCPDT 的 SSI 外设直接就能驱动;
- 最高 SPI 时钟 40MHz,写入和读取几乎对称,不存在“读快写慢”的尴尬;
- 工业级温度范围,-40℃ 到 105℃,适应产线环境和户外机柜;
- 同地址重复改写不会产生坏块,这意味着我可以直接在 MRAM 上做环形日志缓冲,完全不用像 Flash 一样纠结磨损均衡。
如果你只是存几 KB 配置参数,EEPROM 甚至内置 Flash 模拟 EEPROM 都能对付。但要记录几百K 的时序日志、频繁覆盖、还要求掉电不乱,MRAM 的优势是实打实的。
2. TM4C129EKCPDT 平台搭建:SSI、GPIO 与电源时序的匹配细节
2.1 为什么这颗 MCU 适合做记录设备宿主
TM4C129EKCPDT 是 TI Tiva C 系列的旗舰型号之一,ARM Cortex-M4F 内核,主频 120MHz,带 FPU,256KB SRAM,1MB Flash。对于一台产线数据采集终端来说,这个配置很宽裕:采集端要跑传感器滤波算法,通信端要处理以太网或串口协议,存储端还要搬运日志数据,一颗 MCU 能全部扛下来,不用外挂协处理器。
我更看重的是它的存储相关外设:
- 4 个 SSI 模块,可以一个接 MRAM,一个接 SPI 传感器,一个接显示屏,互不干扰;
- SSI 支持 8/16 位帧格式,MRAM 标准 SPI 命令正好用 8 位帧;
- 内置 µDMA 控制器,SSI 收发可以走 DMA,批量读写日志时 CPU 几乎不用参与字节搬运;
- 有 BOR 掉电检测中断,可以用作数据保存的最后一刻触发信号。
2.2 SSI 初始化:一个容易被硬件片选坑到的点
TM4C129 的 SSI 功能很丰富,但它的硬件 FSS 片选信号有一个特点:在连续多字节传输时,FSS 电平行为由硬件管理,不一定符合你的应用需求。MRAM 的 READ、WRITE 命令都是“命令 + 3 字节地址 + 数据”的连续帧传输,我要求 CS 在整个帧内保持低电平,帧结束才拉高。
最省心的方案是:片选引脚不用 SSI 的 FSS,而是单独挑一个 GPIO 手动控制。SCK、TX、RX 仍走 SSI 外设,CS 完全由软件控制,想拉低就拉低,想拉高就拉高。这样逻辑清晰,也避免硬件 FSS 在 FIFO 空/满时产生意外电平跳变。
下面是基于 TivaWare 的 SSI 初始化代码骨架,注意引脚分配要按你自己的板子调整:
#include "tm4c1294ncpdt.h" #include "driverlib/sysctl.h" #include "driverlib/gpio.h" #include "driverlib/ssi.h" #define MRAM_CS_PERIPH SYSCTL_PERIPH_GPION #define MRAM_CS_PORT GPIO_PORTN_BASE #define MRAM_CS_PIN GPIO_PIN_4 void mram_spi_init(void) { // 使能 SSI0 和 GPIO 时钟 SysCtlPeripheralEnable(SYSCTL_PERIPH_SSI0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA); // 按实际引脚调整 SysCtlPeripheralEnable(MRAM_CS_PERIPH); // 把 PA2/PA3/PA4/PA5 配置为 SSI0 引脚(示例,实际按你的原理图改) GPIOPinConfigure(GPIO_PA2_SSI0CLK); GPIOPinConfigure(GPIO_PA3_SSI0FSS); GPIOPinConfigure(GPIO_PA4_SSI0RX); GPIOPinConfigure(GPIO_PA5_SSI0TX); GPIOPinTypeSSI(GPIO_PORTA_BASE, GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5); // CS 脚做普通 GPIO 输出,上电默认拉高 GPIOPinTypeGPIOOutput(MRAM_CS_PORT, MRAM_CS_PIN); GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); // SSI0 配置为主模式,MOTO 模式 0,8 位帧,20MHz SSIConfigSetExpClk(SSI0_BASE, SysCtlClockGet(FALSE), SSI_FRF_MOTO_MODE_0, SSI_MODE_MASTER, 20000000, 8); SSIEnable(SSI0_BASE); }MR25H40CDF 支持 SPI Mode 0 和 Mode 3,我习惯用 Mode 0,也就是 CPOL=0、CPHA=0。SSIConfigSetExpClk 这个函数会把模式、帧格式、时钟频率一次配好,比较省事。
2.3 电源与复位:掉电监控其实比想象中更关键
MR25H40CDF 工作电压范围是 2.7V 到 3.6V,典型 3.3V。MCU 端也是 3.3V,供电简单。但工业现场的 3.3V 往往不是一路干净直流,电机启停、接触器吸合都可能造成电压跌落。MRAM 在电压低于 VDD 下限时,写操作结果是不确定的,这时必须让 MCU 感知到并停止写入。
TM4C129 自带 BOR 检测,可以在电压降到预设阈值时触发中断或复位。我在初始化里把 BOR 中断打开,并且把它的中断优先级设成最高,保证掉电瞬间能抢先执行“紧急保存”代码:
// 使能 BOR 中断 IntEnable(INT_BOR); IntPrioritySet(INT_BOR, 0x00); // 最高优先级另外在 PCB 上,我会在靠近 MCU 电源脚放一个 TLV809 或同类复位芯片,电压低于阈值时直接拉复位。这一层硬件保险比单纯靠软件更可靠,毕竟 MCU 内部检测再快,也只是在一个已经恶化的电源环境里工作。
3. 硬件连接与 PCB 布局:DFN-8 封装、HOLD# 和 WP# 引脚的处理经验
3.1 引脚连接表
MR25H40CDF 是 8 引脚 DFN 封装,引脚不多,但每个引脚都有说头。下面是我这边的连接方式:
| MR25H40CDF 引脚 | 功能 | 连接到 TM4C129EKCPDT |
|---|---|---|
| CS# | 片选 | GPIO 输出,10kΩ 上拉到 3.3V |
| SCK | SPI 时钟 | SSI0CLK |
| SI | MOSI | SSI0TX |
| SO | MISO | SSI0RX |
| WP# | 写保护 | 10kΩ 上拉到 3.3V |
| HOLD# | 暂停传输 | 10kΩ 上拉到 3.3V |
| VDD | 电源 | 3.3V,0.1µF 去耦电容 |
| VSS | 地 | GND |
WP# 和 HOLD# 这两个引脚非常关键。WP# 拉低会禁止写状态寄存器,如果你初始化阶段要配置块保护,就需要能控制它;量产稳定后我一般直接接上拉到 VDD,让写保护功能不碍事。HOLD# 则必须接上拉,悬空是最危险的。
3.2 HOLD# 引脚悬空带来的随机故障
这个小节我单独拿出来讲,是因为它坑过我一次,而且表现非常隐蔽。HOLD# 是暂停传输控制脚,低电平有效。当它被拉低时,MRAM 会停止响应 SPI 总线上的数据,SCK 上继续来的时钟都被忽略,当前传输暂停,直到 HOLD# 恢复高电平才接着之前的位置继续。
问题在于:如果 HOLD# 悬空,它就是一个高阻浮空节点。旁边 PCB 走线上的电磁干扰、串扰噪声都可能让这个引脚的电压随机跌到低电平。表现就是批量读日志时,偶尔一块数据里缺了几个字节,或者后续数据整体错位,而且复现概率很低。用示波器抓 CS、SCK、SO 都是正常的,找半天才发现是 HOLD# 在作怪。
所以我的原则是:HOLD# 和 WP# 都接 10kΩ 上拉,上电瞬间引脚电平必须是确定的,不能等 MCU 初始化。
3.3 DFN 封装的焊接与走线
DFN-8 封装底部有一个散热焊盘,这在 QFN/DFN 封装里很正常,但手工焊接时容易出问题。如果你让 PCB 工厂用钢网刷锡膏,底部焊盘锡膏厚了,回流焊时芯片会被顶起来一点点,四周引脚就可能虚焊,冷启动读数据就会出各种怪象。硬件设计时要注意钢网开孔,让底部焊盘的锡量可控,或者干脆在散热焊盘上打几个过孔,把多余锡引走。
PCB 布局上,SCK 走线尽量短,而且 SI、SO 两侧最好有地平面包着,减少耦合噪声。DFN 封装下面没法走线,所以引脚扇出要提前规划,我一般会在 MRAM 附近留一组测试点,方便贴片后用示波器直接量 CS、SCK、SO。
4. 驱动实现:命令时序、状态寄存器与 DMA 批量读写
4.1 MRAM 命令集与基本时序
MR25H40CDF 的命令集和 SPI NOR Flash 有相似之处,但逻辑完全不同。它不需要擦除,没有页编程限制。下面是我用到的几个命令:
| 命令 | 操作码 | 说明 |
|---|---|---|
| WREN | 0x06 | 写使能,WRITE 和 WRSR 前必须发 |
| WRDI | 0x04 | 写禁止 |
| RDSR | 0x05 | 读状态寄存器 |
| WRSR | 0x01 | 写状态寄存器 |
| READ | 0x03 | 读数据 |
| FAST_READ | 0x0B | 快速读,带 1 个 dummy 字节 |
| WRITE | 0x02 | 写数据,无需擦除 |
所有命令都是 MSB 优先。地址固定发 3 字节,MR25H40CDF 实际容量只需要 19 位地址,高字节直接填 0 就行。
4.2 核心读写函数代码
读写函数的关键点是控制好 CS 的时序:拉低片选后,把命令、地址、数据按顺序发完,再拉高片选。中途不能有 GPIO 抖动,否则 MRAM 会把后面的数据当成新命令。
static void mram_cs_low(void) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, 0); } static void mram_cs_high(void) { GPIOPinWrite(MRAM_CS_PORT, MRAM_CS_PIN, MRAM_CS_PIN); } static uint8_t spi_xfer(uint8_t data) { uint32_t rx_data; SSIDataPut(SSI0_BASE, data); while (SSIBusy(SSI0_BASE)) { } SSIDataGet(SSI0_BASE, &rx_data); return (uint8_t)rx_data; } void mram_write_enable(void) { mram_cs_low(); spi_xfer(0x06); // WREN mram_cs_high(); } uint8_t mram_read_status(void) { uint8_t status; mram_cs_low(); spi_xfer(0x05); // RDSR status = spi_xfer(0xFF); mram_cs_high(); return status; } void mram_wait_busy(void) { while (mram_read_status() & 0x01) { } // WIP 位 } void mram_read_bytes(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t i; mram_cs_low(); spi_xfer(0x03); // READ spi_xfer((addr >> 16) & 0xFF); spi_xfer((addr >> 8) & 0xFF); spi_xfer(addr & 0xFF); for (i = 0; i < len; i++) { buf[i] = spi_xfer(0xFF); } mram_cs_high(); } void mram_write_bytes(uint32_t addr, const uint8_t *buf, uint32_t len) { uint32_t i; mram_write_enable(); mram_cs_low(); spi_xfer(0x02); // WRITE spi_xfer((addr >> 16) & 0xFF); spi_xfer((addr >> 8) & 0xFF); spi_xfer(addr & 0xFF); for (i = 0; i < len; i++) { spi_xfer(buf[i]); } mram_cs_high(); mram_wait_busy(); }这段代码里,MRAM 写的优势已经体现出来了:mram_write_bytes不需要先擦除,不需要检查地址是否对齐,不需要处理坏块,直接把缓冲内容怼进去就好。对于工业日志记录,这比维护一套 Flash 文件系统舒服太多。
4.3 用 µDMA 把吞吐量拉上去
如果只是偶尔写几个字节,轮询式 SPI 就够了。但记录高频运行数据时,比如每毫秒一组 16 字节波形,CPU 如果一直在字节搬运里打转,就没时间处理中断和控制逻辑了。TM4C129 的 µDMA 可以分担这部分工作。
思路是:预先准备一块内存作为发送缓冲区,里面放好“命令字节 + 3 字节地址 + 填充字节”,然后启动 SSI TX 和 SSI RX 两条 DMA 通道。TX DMA 负责发命令和填充字节,RX DMA 同步接收数据到接收缓冲区。一轮传输结束触发中断,CPU 只处理结束标志。
// 伪代码示意,具体寄存器配置按 TivaWare 的 uDMA 接口来 uint8_t tx_buf[256 + 4] = { 0x03, addr[2], addr[1], addr[0] }; // READ 命令 + 地址 uint8_t rx_buf[256]; uDMAChannelAssign(UDMA_CH21_SSI0RX); // 按实际分配 uDMAChannelAssign(UDMA_CH22_SSI0TX); uDMAChannelControlSet(UDMA_CH22_SSI0TX | UDMA_PRI_SELECT, UDMA_SIZE_8 | UDMA_SRC_INC_8 | UDMA_DST_INC_NONE | UDMA_ARB_4); uDMAChannelTransferSet(UDMA_CH22_SSI0TX | UDMA_PRI_SELECT, UDMA_MODE_BASIC, tx_buf, (void *)(SSI0_BASE + SSI_O_DR), 260); uDMAChannelTransferSet(UDMA_CH21_SSI0RX | UDMA_PRI_SELECT, UDMA_MODE_BASIC, (void *)(SSI0_BASE + SSI_O_DR), rx_buf, 256); uDMAChannelEnable(UDMA_CH21_SSI0RX | UDMA_CH22_SSI0TX);实际测试下来,DMA 模式下读 512 字节只需要微秒级完成,CPU 占用几乎为零。这在大批量日志回传、或者需要实时响应外部事件时非常关键。
5. 掉电安全与数据完整性:从写序列到 CRC 校验的设计
5.1 掉电保存目标的时序预算
工业设备最怕的是突然断电。控制器还在运行,外设电源可能已经掉了,或者反过来。MRAM 的写操作本质上没有编程延迟,一个字节的写入时间就是 SPI 传输那点时间,所以掉电窗口内能写入的数据量非常可观。
以我的终端为例,3.3V 掉电后,TM4C129 的 BOR 触发,到电源彻底不能工作,实际能争取到 2ms 到 5ms 的窗口。在 20MHz SPI 时钟下写 512 字节只需要大约 2.3ms 总线上时间。也就是说,在 BOR 中断里完成“把关键状态打包成记录 + 写入 MRAM + 拉高片选”是可行的。注意不要在 ISR 里做复杂运算,最好平时就把记录结构体准备好,掉电时直接拷贝。
5.2 记录格式与环形缓冲设计
MRAM 既然不怕覆盖写,我就可以放心用环形日志。每条记录格式固定如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| magic | 2 字节 | 固定 0x5A5A,用于标记记录头 |
| length | 2 字节 | 本条记录负载长度 |
| seq | 4 字节 | 序号,递增 |
| payload | N 字节 | 实际日志内容 |
| crc16 | 2 字节 | 对整个记录做 CRC 校验 |
环形缓冲区起始地址从 0x00000 开始,写完到最后一条就往回绕,直接覆盖最早的记录。由于 MRAM 没有擦除寿命问题,这种设计不需要考虑 Flash 那种“擦一块写一块”的浪费。而且因为读写是字节级的,绕回时不需要对齐扇区,很灵活。
我为项目保留了一整块参数区专门放设备配置、校准系数这类低频写数据,参数区写在一个固定地址,每次更新用双缓存交替策略:先写副本 A,校验通过后再写副本 B。恢复时优先用校验通过的那份副本。这样即使掉电正好落在写副本 A 的过程中,副本 B 依然完整可用。
5.3 系统恢复流程与半条记录处理
上电后不能直接相信 MRAM 里的最后一个地址,因为掉电可能发生在写记录的过程中。我的恢复流程是:
- 读日志区头部一个独立槽位里的 write_index 字段;
- 根据 write_index 定位最新记录首地址,读整条记录;
- 检查 magic 和 CRC16,如果两个都通过,说明上一条写完了;
- 如果 magic 不对或 CRC 失败,说明最后一条写了一半,回退到 seq - 1 的位置继续读,并丢弃这条半成品。
由于 MRAM 写入单字节已经是原子操作,半条记录最多是某个字节停留在旧值,不太可能出现“字节里混着新旧数据”的情况。CRC 能帮我准确判断该不该丢弃。这个策略比“写日志前先擦除再写”的老方案简单得多,也快得多。
6. 实测性能与容量规划:4Mbit 到底能存多少条记录
6.1 实测读写吞吐
我这里用 20MHz SPI 时钟、DMA 方式做了简单测速,数据如下:
| 操作 | 实测结果 |
|---|---|
| 连续读 512KB | 约 1.9MB/s,CPU 占用接近 0 |
| 连续写 512KB | 约 1.8MB/s,写后无需擦除 |
| 单次写 64B 记录 | 约 35µs(含 WREN 和状态检查) |
| 单次读 64B 记录 | 约 32µs |
如果 SPI 时钟提高到 40MHz,吞吐还能翻倍接近 4MB/s,但实际要看 PCB 走线质量。对记录设备来说,1.8MB/s 已经绰绰有余:一秒写 200 条 1KB 的带波形数据都毫不费力。而 SPI NOR Flash 同样 1KB 数据要先擦除再写,常常要几十毫秒,这个差距不是一点半点。
6.2 容量规划示例
4Mbit 等于 512KB,很多工程师第一反应是“这么小能存啥”。如果你跑的是大数据量采集,那确实不够,但我这里存的是事件型日志和状态型参数,容量规划要按记录条数算:
- 64 字节事件记录,含时间戳、报警码、参数快照:512KB / 64 = 8192 条;
- 1 分钟记录一条,可以覆盖 5.7 天;
- 10 秒记录一条,可以覆盖 22.7 小时;
- 256 字节带波形数据的记录,可以存 2048 条,对一次故障前后的波形抓取足够。
如果你觉得不够,Everspin 同系列还有 8Mbit、16Mbit 等更大容量的型号,驱动代码只需要改容量边界和地址掩码,接口不变。这就体现出 SPI MRAM 在工程上的一种好处:后期扩容成本很低。
6.3 给量产产品的小建议
虽然 MRAM 理论上不会磨损,但焊接不良、静电损伤、超压写入仍然可能导致个别存储单元异常。我在量产测试时会做两遍扫描:先把整片写成 0x5A,再读回比对;然后写成 0xA5,再读回比对。整个过程在 20MHz 下跑一遍 512KB 也就几百毫秒,能发现绝大部分硬件问题。
7. 踩坑记录:从读回全 FF 到随机数据错误的全链路排查
7.1 CS# 毛刺上电误触发
第一次打样回来,上电就发现 MRAM 里数据不对,读出来是一堆随机命令造成的乱码。查了一上午,最后发现 CS# 引脚在上电瞬间的默认电平有问题。
MCU 的 GPIO 在复位期间默认是高阻输入,外部如果没有上拉或下拉,这个引脚电平由内部弱上拉/外部电路决定。偏偏我把 CS# 接到了 GPIO 上,复位期间 GPIO 没初始化,引脚电位不确定;一旦它被内部下拉或外部噪声拉低,同时 SCK 线上有毛刺脉冲,MRAM 就可能误把毛刺当成 SPI 时钟,接收了一个随机命令,把状态寄存器或数据区改掉。
解决方法是 CS# 引脚外部并联 10kΩ 上拉电阻到 3.3V,确保上电期间 CS# 恒定在高电平,并且软件初始化时先把 CS GPIO 配置成输出高,再去初始化 SSI 外设。
7.2 SPI 模式配错读到全 FF
另一个常见现象是 MRAM 读出来全是 0xFF。数据全 FF 一般就是 SPI 采样沿不对,或者根本没有收到有效数据。MRAM 支持 Mode 0 和 Mode 3,我一开始图省事照抄了某个 Flash 例程,配置成了 SPO=0、SPH=1,结果读状态寄存器都是 0xFF。
用示波器看波形最直观:Mode 0 下数据是在 SCK 的上升沿被主控锁存,如果配置成 Mode 1,锁存沿差了一个相位,SO 上的数据就全错位了。把 SSI 配置改成 SSI_FRF_MOTO_MODE_0 后问题消失。建议新焊板子回来,第一件事先发 RDSR 命令,读一下状态寄存器是不是 0x00,如果不是,优先怀疑 SPI 模式配置。
7.3 HOLD# 悬空导致批量读随机出错
这个问题我在前面硬件部分提过,这里展开一下排查过程。现象是批量读 512 字节时,偶发出现中间某几个字节变成 0x00,而且位置不固定;单字节读已经完全正常。一开始怀疑是 SPI 时序余量不足,尝试降低时钟到 5MHz,问题依旧。又以为是 DMA 配置错了,换回轮询方式,仍然偶发。折腾到第二块板子,拿示波器长时间挂测 HOLD# 引脚,才发现它偶尔会被拉到 0.5V 以下。原因:HOLD# 引脚悬空,旁边有一条 24V 继电器驱动线,继电器吸合时产生瞬态干扰,HOLD# 被耦合拉低,MRAM 暂停响应,数据就缺了一段。补上 10kΩ 上拉后,再也没出现过。
7.4 DFN 假焊导致冷启动读数据异常
还有一块板子,常温工作一切正常,断电隔一段时间再开,读出来的第一条记录偶尔是 0xFF。排查了很久,最后手轻轻压在 MR25H40CDF 封装上再读,数据恢复。拆下来重新显微镜检查,发现 SO 引脚假焊,接触电阻时大时小。冷启动时芯片和 PCB 温度低,焊点轻微分离;工作一段时间后温度升高,金属膨胀又压回去了。这个属于 DFN 封装的经典坑,解决办法是检查钢网开窗、加强回流焊温度曲线管控,量产前最好加一道光学检测。
7.5 掉电只写了一半:用 magic 和 CRC 识别残次记录
最后这个算是设计上的坑。早期版本我把日志记录头设计得过于简单,没有 magic 和长度字段,掉电后恢复时发现最新记录的时间戳是旧的、内容却是新写入的,判断逻辑直接懵了。后来统一加上 magic、length、seq、crc16 四件套,任何一条记录在读出来时都能做完整校验,不合法就回退。现在 RUN 状态已经是:掉电写入最多丢最后一条,绝不会污染之前的数据。
这套 MR25H40CDF 与 TM4C129EKCPDT 的组合,我在产线终端上已经跑了两个多月。最大的体会是:写日志终于可以像写 SRAM 一样随意了,不用再琢磨擦除策略、磨损均衡、掉电坏块这些事。数据读取也干净,按地址遍历或者环形覆盖都行,整条链路从硬件到驱动到恢复策略是自洽的。如果后续有更大的日志需求,我会考虑把 16Mbit 的 MRAM 换上,或者用 DMA 把吞吐推到接近 4MB/s,再往这个接口上加一层带时间索引的检索逻辑。工业存储这一块,稳定和简单往往比花哨更重要。