工业控制器分级存储方案:STM32+FPGA与EEPROM/NOR/SD卡选型设计

工业控制器这类设备有个很现实的问题:它不像消费电子那样可以随便"重启大法"糊弄过去,一旦上了产线,参数丢了、日志断了、配方没了,轻则停机重调,重则整批产品报废。所以存储方案的设计,从一开始就不是"能存就行",而是要在掉电可靠性、写入寿命、容量成本、读写速度这几个互相拉扯的指标里找一个平衡点。我这些年做过的板子里,单靠一颗EEPROM打天下的方案基本都活不过两年,而全上SD卡又会在振动和掉电场景下翻车。比较稳妥的做法,是用STM32做管理核心、FPGA做高速数据通道,再配一套EEPROM、NOR Flash、SD卡三级分层的存储架构,让不同性质的数据各归其位。这篇就围绕这套分级存储方案,把选型逻辑、硬件连接、分区规划、驱动要点和实测踩过的坑一次讲透,适合正在做工业控制器、边缘网关、通信测试终端这类项目的硬件和嵌入式工程师参考。

1. 为什么工业控制器必须做分级存储

1.1 单一存储介质在工业场景下的三种死法

先说清楚为什么不能只用一种存储。我见过太多项目在实验室跑得好好的,一到现场就出问题,归根结底是没搞清楚工业环境和办公环境的差别。

第一种死法是掉电写坏。EEPROM和NOR Flash在写入过程中如果突然断电,正在写的那个扇区或页可能处于半写状态,数据直接变成乱码。工业现场电网波动、急停断电都是家常便饭,如果关键参数只存在一颗没有掉电保护机制的存储里,早晚出事。

第二种死法是写入寿命耗尽。NOR Flash的擦写次数通常在10万次量级,SD卡里的NAND更惨,消费级卡可能只有几百到几千次擦写寿命。如果拿SD卡去存每秒都在更新的运行计数或者高频日志,几个月就能把某些块写废。而EEPROM虽然单字节可以擦写百万次,但容量小、速度慢,扛不住大数据量。

第三种死法是环境应力。SD卡是插拔式结构,金手指在振动环境下会接触不良,工业现场的高低温、粉尘、湿度也会加速氧化。我有个做通信测试终端的朋友,设备在机房跑了一年,SD卡座簧片疲劳,接触电阻变大,读写开始随机报错,查了整整两周才定位到是卡座的问题。

所以分级存储的本质,是按数据的"性格"分配住所:变化慢但要求绝对可靠的关键参数,住EEPROM;需要频繁读写、容量中等的配置和日志,住NOR Flash;大容量、低频访问的历史数据和文件,住SD卡。

1.2 三类数据的生命周期差异决定了分层逻辑

把数据按生命周期拆开看,分层逻辑就非常清晰了。

第一类是"出生即定型"的静态数据,比如设备序列号、硬件版本、校准系数、出厂标定值。这类数据写入次数极少,可能整个生命周期就写几次,但要求几十年不丢,掉电绝对不能损坏。EEPROM是天然的选择,它支持字节级擦写,不需要整页擦除,写入功耗低,数据保持时间通常标称40年以上。

第二类是"周期性变化"的配置与运行数据,比如PID参数、通信地址、报警阈值、运行小时数、故障记录。这类数据可能每天写几次到几百次,容量需求在几十KB到几MB之间。NOR Flash的扇区结构适合做这种"块状更新",配合磨损均衡和掉电保护,能撑很久。

第三类是"海量且低频读取"的历史数据,比如波形采样记录、批量测试结果、固件升级包、导出报表。这类数据动辄几十MB到几GB,写入频率不高但容量要求大,SD卡(或eMMC)是唯一在成本上能接受的选择。

这里有个容易被忽略的点:数据的"写入频率"和"读取频率"要分开评估。有些数据写得少但读得极频繁,比如校准系数每次上电都要读,这种放EEPROM没问题,因为EEPROM读操作几乎不损耗寿命。但如果是频繁读写的日志索引,就要考虑放到RAM里做缓存,定期刷到Flash,减少实际擦写次数。

1.3 STM32+FPGA分工:谁管存储,谁管数据流

这套架构里,STM32和FPGA的分工不是随便定的,而是由两者的能力边界决定的。

STM32的优势在于控制逻辑、协议栈、文件系统和事务管理。它有成熟的HAL库、FatFs文件系统支持、丰富的通信外设,适合做"存储管家"——负责决定什么数据写到哪一级、什么时候触发刷写、掉电时如何保护。它的短板是并行处理能力弱,面对高速ADC采样流或者多通道数据汇聚时容易成为瓶颈。

FPGA的优势在于并行数据采集、实时预处理和高速接口。它可以同时挂多路ADC、做实时滤波和打包,然后通过高速总线把数据推给STM32。但FPGA本身不擅长做复杂的文件系统和事务管理,让它直接管SD卡的文件分配表会非常痛苦。

所以合理的分工是:FPGA负责"生产数据",STM32负责"归档数据"。FPGA把采集到的原始数据做初步打包,通过SPI或FSMC总线传给STM32,STM32再根据数据类型决定是缓存、写NOR Flash还是落盘到SD卡。EEPROM则通常直接挂在STM32的I2C总线上,由STM32独占管理,FPGA一般不直接碰它,避免总线竞争。

这个分工带来的一个实际好处是:存储策略的调整只需要改STM32的固件,不用动FPGA逻辑。工业项目后期需求变更很常见,这种解耦能省下大量重新综合FPGA的时间。

2. 三种存储介质的硬件选型与电路要点

2.1 EEPROM:I2C接口的细节决定可靠性

EEPROM选型看起来简单,其实坑不少。常用的型号比如AT24C系列、CAT24系列,容量从2Kbit到2Mbit都有。工业场景我一般推荐选带写保护引脚(WP)和独立地址引脚的型号,方便做硬件级写保护和多片级联。

接口上,I2C是最常见的选择,两根线(SCL/SDA)就能挂多片。但这里有几个硬件细节必须注意:

上拉电阻的取值。I2C是开漏输出,必须加上拉。阻值不是随便选4.7K就完事,要根据总线电容和速率算。标准模式100kHz下,总线电容不超过400pF时,4.7K通常没问题;但如果走线长、挂的设备多,电容上去了,就得减小阻值到2.2K甚至1K。我遇到过一块板子I2C偶尔通信失败,最后发现是上拉用了10K,上升沿太缓,在长走线下被干扰误判成起始条件。

地址冲突。AT24C系列用A0/A1/A2三个引脚决定低三位地址,最多挂8片。如果项目里同时有EEPROM、温度传感器、RTC都走I2C,一定要提前把地址表列出来,避免撞车。我习惯在原理图上直接标注每颗器件的7位地址,评审时一眼就能看出冲突。

写保护策略。WP引脚拉高时EEPROM进入只读,这个引脚不要直接接地了事。我的做法是接到STM32的一个GPIO上,正常运行时拉低允许写,设备进入"出厂锁定"模式或者检测到异常时拉高保护。这样即使固件跑飞,也不会误写关键标定数据。

去耦电容。每片EEPROM的VCC引脚旁边必须放0.1uF陶瓷电容,位置尽量靠近引脚。EEPROM在写入瞬间电流会有尖峰,去耦不好会导致写入失败或者数据错误。

2.2 NOR Flash:SPI NOR的容量与扇区规划

NOR Flash分并行和SPI两种。并行NOR速度快但引脚多,现在工业控制器里更常见的是SPI NOR,比如W25Q系列、S25FL系列,容量从1MB到128MB都有。

选型时重点看三个参数:扇区大小、擦写寿命、是否支持4字节地址。

扇区大小通常是4KB,有些型号支持更小的1KB子扇区。扇区越小,做磨损均衡时粒度越细,但管理开销越大。我一般选4KB扇区的标准型号,配合STM32的Flash抽象层做管理。

擦写寿命方面,SPI NOR通常标称10万次。这个数字看着不少,但如果某个扇区被频繁更新,比如每秒写一次运行状态,一天就是86400次,一天多就废了。所以必须做磨损均衡,不能盯着一个地址死写。

容量超过16MB的NOR Flash会用到4字节地址模式,需要发专门的进入命令(比如W25Q系列的0xB7命令)。如果固件里没处理这个,访问高地址区就会出错。这个坑我在一个32MB的项目里踩过,读出来的数据全是0xFF,查了半天才发现是地址模式没切。

电路上,SPI NOR的CS片选、CLK、MOSI、MISO四根线要尽量等长,CLK走线避免跨分割。如果和SD卡共用SPI总线,一定要用独立的片选,并且在切换设备时确保前一个设备的CS已经拉高,否则会出现两个设备同时驱动MISO的冲突。

2.3 SD卡:工业级与消费级的真实差距

SD卡这块我要多说几句,因为它是整个方案里最容易出问题的一环。

首先是卡的选择。消费级SD卡(比如手机里用的那种)在工业环境下寿命和可靠性都不够。工业级SD卡(比如Swissbit、Apacer、ATP的工业系列)用的是SLC或pSLC NAND,擦写寿命能到几万次,工作温度范围宽,还带掉电保护电容。价格贵好几倍,但该花还得花。我见过用消费级卡做数据记录仪,半年换了三张卡,最后算下来比直接买工业卡还贵。

其次是接口模式。SD卡支持SPI模式和SDIO模式。SPI模式接线简单(4根线),但速度慢,实测也就几MB/s;SDIO模式用4位数据线,速度能到20MB/s以上,适合大数据量场景。STM32的SDIO外设很成熟,我一般优先用SDIO。但要注意SDIO的CLK、CMD、D0-D3这几根线要严格等长,并且加22-33欧姆的串联匹配电阻,抑制反射。

第三是卡座和供电。卡座要选带金属屏蔽罩和自锁结构的,插拔力要够。供电方面,SD卡在写入瞬间电流能到100mA以上,电源要能扛住这个瞬态,VDD引脚旁边放10uF+0.1uF组合电容。另外SD卡的检测引脚(CD)最好接到STM32的GPIO上,这样固件能感知卡的插拔,避免在卡拔出时还在写导致文件系统损坏。

第四是掉电保护。这是SD卡最脆弱的地方。如果正在写文件时断电,FAT表可能损坏,整个卡都读不出来。解决办法有两个:一是用带掉电保护的工业卡,卡内部有电容能在断电后完成当前写操作;二是在STM32侧做写缓存+定期刷盘,把频繁的小写入攒成大批量写入,减少文件系统元数据的更新次数,同时在检测到掉电信号时立即停止写入并刷盘。

2.4 三种介质的硬件对比与选型决策表

把三种介质的特性拉平了对比,选型时就有据可依:

特性EEPROMSPI NOR FlashSD卡(工业级)
典型容量2Kbit~2Mbit1MB~128MB4GB~64GB
接口I2C/SPISPI/QSPISDIO/SPI
擦写粒度字节4KB扇区块(内部管理)
擦写寿命100万次/字节10万次/扇区1万~3万次/块
写入速度慢(ms级)中(百us级)快(MB/s级)
数据保持40年+20年10年(工业级)
掉电风险低(单字节)中(扇区)高(FAT表)
成本低中高
适用数据标定/序列号配置/日志历史/文件

这张表不是让你照搬,而是帮你建立判断框架。比如你的项目只需要存几百字节的标定值,那EEPROM就够了,没必要上NOR Flash;如果需要存几MB的波形数据又要频繁更新,NOR Flash比SD卡更合适;如果是要存几GB的历史记录,那只能上SD卡,但必须做好掉电保护。

3. 存储分区规划:把数据放到对的地方

3.1 按数据性质划分存储区域的实操方法

分区规划的核心原则是:同类数据集中管理,不同类数据物理隔离。我通常把整个存储空间划成这么几块:

EEPROM区:只放最关键的、写入次数最少的、掉电绝对不能丢的数据。具体包括设备序列号(16字节)、硬件版本(4字节)、出厂日期(4字节)、校准系数(每个通道4字节浮点,假设8通道就是32字节)、通信参数(地址、波特率等,16字节)。总共不到100字节,用一颗2Kbit(256字节)的EEPROM就够,留足余量。

NOR Flash区:分成三个子区。第一个是配置区,存用户可修改的参数,比如PID参数、报警阈值、工作模式,这部分做双备份+CRC校验,写入时先写备份区再写主区,确保任何时候至少有一份完整数据。第二个是日志区,用环形缓冲区结构,存故障记录和运行事件,每条记录带时间戳和CRC。第三个是固件备份区,存一份当前固件的备份,用于升级失败时回滚。

SD卡区:用FatFs文件系统,分成几个目录。/config存导出的配置文件,/log存详细运行日志,/data存采样数据文件,/update存固件升级包。文件按日期或批次命名,方便检索。

这里有个经验:不要把关键配置只存在SD卡上。SD卡是可插拔的,用户可能拔走,也可能损坏。关键配置必须在EEPROM或NOR Flash里有一份,SD卡上的只是副本或导出文件。

3.2 地址映射与STM32内存视图的对应关系

在STM32的固件里,要把这些物理存储映射成统一的逻辑地址空间,方便上层调用。我的做法是定义一个存储抽象层(SAL),用枚举区分存储类型,用偏移量定位具体数据。

typedef enum { STORAGE_EEPROM = 0, STORAGE_NOR_FLASH, STORAGE_SD_CARD } storage_type_t; typedef struct { storage_type_t type; uint32_t offset; uint32_t length; uint16_t crc; } storage_item_t;

然后为每类数据定义一个描述符,比如:

#define ITEM_SERIAL_NUM {STORAGE_EEPROM, 0x0000, 16, 0} #define ITEM_CALIB_COEF {STORAGE_EEPROM, 0x0010, 32, 0} #define ITEM_PID_PARAM {STORAGE_NOR_FLASH, 0x1000, 64, 0} #define ITEM_FAULT_LOG {STORAGE_NOR_FLASH, 0x2000, 4096, 0}

上层读写时只需要传描述符,SAL层负责调用对应的底层驱动。这样以后换存储芯片或者调整分区,只改描述符就行,业务代码不动。

对于NOR Flash,还要在STM32的内存里维护一个逻辑地址到物理地址的映射表,因为做了磨损均衡后,同一个逻辑地址可能对应不同的物理扇区。这个映射表本身也要存到Flash里,并且做双备份。

3.3 掉电保护的分级策略

掉电保护不是一句"加个电容"就完事,要分级设计。

第一级:EEPROM的字节级保护。EEPROM单字节写入时间约5ms,如果在这5ms内断电,该字节可能写坏。但EEPROM通常有内部电荷泵和写完成检测,实际风险较低。为了保险,关键数据写两份,读的时候比对,不一致就用备份恢复。

第二级:NOR Flash的扇区级保护。写一个4KB扇区需要先擦除(约50ms)再写入(约1ms)。擦除过程中断电,整个扇区数据丢失。所以配置区必须双备份,写的时候先擦备份扇区、写新数据、校验通过后再擦主扇区、写新数据。这样任何时刻至少有一个扇区是完整的。

第三级:SD卡的文件系统级保护。这是最复杂的。我的策略是:在STM32的RAM里开一个写缓存(比如8KB),所有小写入先攒着,攒满或者超过一定时间(比如5秒)再一次性写入SD卡。同时监测掉电信号(通过ADC监测输入电压或者用比较器产生中断),一旦检测到电压跌落,立即停止接受新数据,把缓存里的数据刷盘,然后关闭文件。这样能把文件系统损坏的概率降到很低。

注意:掉电检测的阈值要留足余量。比如系统正常供电5V,SD卡写入时电流突增会导致电压瞬间跌到4.5V,如果阈值设成4.5V就会误触发。我一般设成4.0V触发,并且加100ms的消抖。

4. STM32侧的驱动实现与关键代码

4.1 I2C读写EEPROM的时序陷阱

STM32的HAL库提供了I2C的读写函数,但直接用HAL_I2C_Mem_Write有时候会遇到问题。最常见的是写周期等待。EEPROM在收到写命令后,内部需要5ms左右完成写入,这期间它不会响应任何I2C命令。如果紧接着发下一个写命令,会收到NACK。

正确的做法是:每次写操作后,用"应答轮询"(Acknowledge Polling)等待EEPROM就绪。具体就是反复发起始条件+设备地址,直到收到ACK为止。

void eeprom_wait_ready(uint16_t dev_addr) { uint32_t timeout = 10000; while (timeout--) { if (HAL_I2C_IsDeviceReady(&hi2c1, dev_addr, 1, 1) == HAL_OK) { return; } } // 超时处理 }

另一个坑是页写入边界。EEPROM通常按页组织,比如AT24C256每页64字节。如果一次写入跨越页边界,地址会回卷到页首,覆盖之前的数据。所以写多字节时要么分页写,要么确保不跨页。

void eeprom_write_buffer(uint16_t mem_addr, uint8_t *data, uint16_t len) { uint16_t page_size = 64; while (len > 0) { uint16_t page_offset = mem_addr % page_size; uint16_t write_len = page_size - page_offset; if (write_len > len) write_len = len; HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, mem_addr, I2C_MEMADD_SIZE_16BIT, data, write_len, 100); eeprom_wait_ready(EEPROM_ADDR); mem_addr += write_len; data += write_len; len -= write_len; } }

4.2 SPI NOR Flash的扇区擦除与磨损均衡

SPI NOR的驱动比EEPROM复杂,因为写之前必须先擦除整个扇区。基本流程是:写使能→擦除扇区→等待擦除完成→写使能→页编程→等待写入完成。

void nor_flash_write(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t sector_addr = addr & ~(0xFFF); // 4KB对齐 nor_flash_sector_erase(sector_addr); nor_flash_page_program(addr, data, len); }

但这样直接写有个问题:如果只想改扇区里的几个字节,擦除会丢掉其他数据。所以要先读出整个扇区到RAM,修改后再整体写回。这就是"读-改-写"模式。

磨损均衡方面,我用的是一种简单的日志式映射:把NOR Flash的日志区当成一个环形缓冲区,每次写新记录就追加到末尾,写满后擦除最旧的扇区。同时维护一个索引表,记录每条有效记录的位置。这样每个扇区的擦写次数大致均匀。

typedef struct { uint32_t write_ptr; // 当前写入位置 uint32_t sector_count; // 总扇区数 uint32_t erase_count[MAX_SECTORS]; // 每个扇区的擦除次数 } nor_flash_log_t;

擦除时优先选择擦除次数最少的扇区,实现简单的均衡。

4.3 SD卡FatFs的挂载、写入与掉电安全

STM32上用FatFs挂载SD卡,基本流程是:初始化SDIO→挂载文件系统→打开/创建文件→写入→关闭。但工业场景下要加很多保护。

首先是挂载失败的处理。SD卡可能没插、接触不良、或者文件系统损坏。挂载失败时不要死循环重试,要上报错误并降级到只用NOR Flash。我见过固件在SD卡挂载失败时卡死,导致整个设备无法启动。

FRESULT fr = f_mount(&fs, "0:", 1); if (fr != FR_OK) { // 记录错误,降级运行 system_status.sd_available = 0; return; } system_status.sd_available = 1;

其次是写入的原子性。FatFs的f_write不是原子的,写大文件时如果断电,文件可能不完整。我的做法是:先写临时文件(比如data.tmp),写完后关闭,再用f_rename改成正式文件名。这样即使断电,最多丢一个临时文件,正式文件不受影响。

f_open(&file, "0:/data.tmp", FA_WRITE | FA_CREATE_ALWAYS); f_write(&file, buffer, len, &bw); f_sync(&file); // 强制刷盘 f_close(&file); f_rename("0:/data.tmp", "0:/data.csv");

f_sync很关键,它把缓存数据真正写到卡上。但频繁调用f_sync会降低性能,所以要权衡。我的策略是每写1MB或者每5秒调用一次。

第三是掉电时的紧急处理。在掉电中断里,不能做复杂的文件操作,只能做最简单的动作:设置一个标志,让主循环尽快调用f_sync和f_close。如果时间实在来不及,至少保证文件系统元数据不损坏。

4.4 三种介质统一读写接口的封装

为了让上层代码不关心底层是哪种存储,我封装了一套统一接口:

int storage_read(storage_item_t *item, uint8_t *buf, uint32_t len); int storage_write(storage_item_t *item, uint8_t *buf, uint32_t len); int storage_erase(storage_item_t *item);

内部根据item->type分发到对应的驱动。同时加一层CRC校验,读出来的数据先验CRC,不通过就尝试从备份恢复。

int storage_read_with_crc(storage_item_t *item, uint8_t *buf) { storage_read(item, buf, item->length); uint16_t crc = crc16(buf, item->length); if (crc != item->crc) { // 尝试从备份读取 return storage_read_backup(item, buf); } return 0; }

这层封装看起来多写了几百行代码,但后期维护和移植的时候能省下大量时间。特别是当项目从STM32F4换到STM32H7,或者从SPI NOR换成QSPI NOR时,只改底层驱动,上层不动。

5. FPGA与STM32的数据交接设计

5.1 为什么不让FPGA直接写存储

前面提到FPGA负责生产数据、STM32负责归档,这里展开说说为什么不让FPGA直接写SD卡或NOR Flash。

技术上FPGA当然可以直接驱动SD卡(有现成的SPI控制器IP)或者NOR Flash。但问题在于文件系统。SD卡上要建FAT文件系统才能被PC读取,而FAT的目录项更新、簇链分配、空闲簇管理这些逻辑,用FPGA的状态机实现极其繁琐,而且一旦掉电,恢复逻辑几乎没法做。NOR Flash的磨损均衡和坏块管理同理。

另外,多主机访问同一存储会带来竞争问题。如果FPGA和STM32都能写NOR Flash,就需要仲裁机制,增加复杂度和出错概率。

所以让STM32做唯一的"存储管理者",FPGA只负责把数据通过FIFO或双口RAM交给STM32,是最清晰的分工。

5.2 SPI/FSMC总线的数据吞吐与缓冲设计

FPGA和STM32之间的数据通道,常见的有SPI和FSMC(或FMC)两种。

SPI方式接线简单,但速度受限。STM32的SPI最高能到几十MHz,实际有效吞吐在几MB/s到十几MB/s。适合数据率不高的场景,比如每秒几KB的传感器数据。

FSMC/FMC方式是并行总线,16位数据宽度下,配合DMA能到几十MB/s。适合高速ADC采样数据的传输。但FSMC接线多(地址线、数据线、控制线加起来十几根),PCB布线要等长,成本高一些。

不管用哪种,缓冲设计都是关键。FPGA侧要有一个FIFO,STM32侧要有一个环形缓冲区。FPGA采集到数据先写入FIFO,STM32通过中断或DMA把FIFO数据搬到自己的环形缓冲区,然后再慢慢处理(写NOR Flash或SD卡)。这样能吸收突发数据,避免因为存储写入慢而丢数据。

// STM32侧环形缓冲区 #define RING_BUF_SIZE 8192 typedef struct { uint8_t buf[RING_BUF_SIZE]; volatile uint32_t head; volatile uint32_t tail; } ring_buf_t; void ring_buf_push(ring_buf_t *rb, uint8_t *data, uint32_t len) { for (uint32_t i = 0; i < len; i++) { uint32_t next = (rb->head + 1) % RING_BUF_SIZE; if (next == rb->tail) { // 缓冲区满,丢弃或覆盖最旧数据 rb->tail = (rb->tail + 1) % RING_BUF_SIZE; } rb->buf[rb->head] = data[i]; rb->head = next; } }

5.3 数据打包格式与校验机制

FPGA传给STM32的数据不能是裸流,要打包成帧,带帧头、长度、类型、CRC。这样STM32才能正确解析和归档。

我用的帧格式是这样的:

字段长度说明
帧头2字节0xAA55
类型1字节0x01=ADC数据,0x02=状态,0x03=事件
长度2字节数据段长度
序号4字节递增,用于检测丢帧
数据N字节实际载荷
CRC162字节从类型到数据的校验

STM32收到帧后先验帧头和CRC,通过后再根据类型分发。序号用来检测是否有丢帧,如果发现序号不连续,记录一条错误日志。

这个打包逻辑放在FPGA里用状态机实现,资源消耗不大,但能极大提升数据可靠性。我实测过,加了CRC和序号后,数据完整性问题基本绝迹。

6. 实测踩坑与排查实录

6.1 SD卡写入随机失败的完整排查链路

这个坑我印象很深。一块工业控制器,SD卡写入偶尔失败,概率大概百分之一,但一旦失败就丢一批数据。排查过程如下:

第一步,确认是硬件还是软件问题。换了一张卡,问题依旧;换了一块板子,问题依旧。初步判断是软件或设计问题,不是单板故障。

第二步,抓波形。用示波器看SDIO的CLK和CMD线,发现失败时CLK上有明显的过冲和振铃。这说明信号完整性有问题。

第三步,查PCB。SDIO的CLK走线没有做阻抗控制,而且和一路PWM信号平行走了很长一段,存在串扰。PWM信号跳变时耦合到CLK上,导致SD卡采样错误。

第四步,验证。把PWM信号改道,远离SDIO走线,同时在CLK上串了22欧姆电阻。重新测试,连续写入10GB数据无一次失败。

这个案例的教训是:SDIO是高速信号,PCB设计不能当普通GPIO对待。CLK要包地,走线要短,远离干扰源,串联匹配电阻不能省。

6.2 NOR Flash擦除后数据全FF的地址模式问题

另一个坑是32MB的NOR Flash,访问高地址区(超过16MB)时读出来全是0xFF。一开始以为是芯片坏了,换了一片还是这样。

后来查数据手册才发现,超过16MB地址需要进入4字节地址模式。默认是3字节地址模式,只能访问前16MB。发0xB7命令进入4字节模式后,问题解决。

但这个命令有个细节:进入4字节模式后,所有的读写命令都要用4字节地址,包括读状态寄存器。如果固件里有的地方用3字节有的用4字节,就会乱套。我的做法是在初始化时统一进入4字节模式,然后所有操作都用4字节地址,不再切回3字节。

6.3 EEPROM写入后立即读取读到旧值的时序问题

这个坑比较隐蔽。写完EEPROM后立即读,有时候读到的是旧值。原因是EEPROM的写周期还没完成,读操作被内部逻辑延迟或者返回了旧数据。

解决办法就是前面说的应答轮询。每次写操作后,必须等到EEPROM响应ACK了才能进行下一次操作。我一开始图省事,写完后延时10ms了事,但批量写入时10ms不够,而且浪费时间。改成应答轮询后,既可靠又快。

6.4 掉电瞬间文件系统损坏的恢复方案

有一次现场断电,SD卡里的文件系统损坏,PC都读不出来。虽然数据不是特别关键,但暴露了掉电保护的不足。

后来我加了两个措施:一是掉电检测中断,电压低于4.0V时立即触发,主循环优先处理刷盘;二是文件系统自检,每次上电时检查SD卡能否挂载,不能挂载就尝试用f_mkfs重新格式化(会丢数据,但至少设备能继续运行),同时把损坏的卡标记出来提示用户更换。

另外,对于特别关键的数据,我在NOR Flash里也存一份副本。SD卡坏了,至少NOR Flash里的配置和最近日志还在。

7. 几个提升可靠性的工程习惯

7.1 双备份与CRC校验的落地细节

双备份不是简单写两份,而是要交叉验证。我的做法是:主备份和从备份各带一个CRC,读取时两份都读出来,CRC都通过且数据一致,才认为有效;如果一份CRC错,用另一份恢复;如果两份都错,上报严重故障。

写入时也有讲究:先写从备份,校验通过后再写主备份。这样任何时刻至少有一份是完整的。如果写从备份时断电,主备份还是好的;如果写主备份时断电,从备份是新的,主备份是旧的,读取时以从备份为准。

7.2 存储寿命的监控与预警

EEPROM和NOR Flash的擦写次数是有限的,虽然做了磨损均衡,但还是要监控。我在每个扇区的头部留几个字节记录擦除次数,每次擦除前先读出计数、加一、写回。当某个扇区的擦除次数接近标称值的80%时,上报预警,提示维护人员准备更换。

SD卡的健康状态可以通过读CSD寄存器和查询卡的状态来粗略评估,但更实用的是监控写入错误率。如果连续出现写入失败,就标记该卡为"不健康",建议更换。

7.3 固件升级时的存储回滚设计

固件升级是存储方案的一个典型应用场景。我的设计是:新固件先下载到SD卡的/update目录,校验CRC和签名,通过后复制到NOR Flash的备份区,然后STM32从备份区读取并写入自己的程序Flash。如果升级过程中断电,下次上电时bootloader检测到升级标志未清除,就从NOR Flash的备份区重新升级。如果备份区也损坏,就回滚到旧固件。

这个流程的关键是升级标志和固件备份都要存在掉电安全的存储里。升级标志我存在EEPROM里(带双备份),固件备份存在NOR Flash里(带CRC)。这样即使升级失败,设备也不会变砖。

7.4 生产测试阶段的存储自检流程

产品出厂前,存储部分必须做完整自检。我的自检流程包括:

  1. EEPROM自检:写入测试模式(0x55、0xAA交替),读回比对,然后恢复出厂数据。
  2. NOR Flash自检:擦除一个测试扇区,写入递增数据,读回比对,再擦除。
  3. SD卡自检:创建测试文件,写入1MB数据,读回比对,删除文件。
  4. 掉电模拟:在写入过程中人为断电,重新上电后检查数据完整性。

这个自检流程跑一遍大概几十秒,但能筛掉大部分存储相关的早期故障。我见过不少板子因为EEPROM虚焊或者SD卡座接触不良,在实验室没问题,到了现场才暴露。产线自检能把这类问题拦在出厂前。

存储方案这东西,设计的时候多花一天,调试的时候能省一周,现场运行的时候能省一年。工业控制器的存储不是"能存就行",而是要在可靠性、寿命、成本、速度之间找到那个平衡点。EEPROM管关键参数、NOR Flash管配置日志、SD卡管历史数据,STM32做管家、FPGA做数据源,这套分级架构我在多个项目里验证过,只要硬件细节做到位、掉电保护做扎实,跑个五年八年问题不大。