做工业控制器的硬件设计,存储这件事看着不起眼,却最容易在量产和现场调试阶段翻车。我自己做过好几轮控制器的硬件迭代,早期方案里把参数直接存进MCU片内Flash,日志往SD卡里堆,结果批量烧录时发现参数区被反复擦写搞坏了,SD卡又因为频繁插拔和掉电导致文件系统损坏。后来才逐步收敛成一套相对固定的做法:EEPROM负责参数与关键配置,NOR Flash负责引导与关键数据备份,SD卡专做大容量日志与历史记录。这篇是硬件篇第十二篇,我把基于STM32+FPGA这套架构下的分级存储方案完整拆开讲一遍,包括选型逻辑、接口设计、掉电保护,还有调试时踩过的一些坑。
这套方案适合正在做工业控制器、边缘网关、通信测试终端这类设备的朋友参考。不管你用STM32还是其他MCU,只要涉及“参数不能丢、日志要能查、掉电不能坏”,分级存储的思路都通用。下面从需求拆解开始,一步步讲清楚。
1. 为什么工业控制器需要分级存储?先想清楚存什么
1.1 不同类型数据对存储介质的诉求完全不同
以一台典型的工业控制器为例,设备里的数据大概分三类。
第一类是运行参数和配置:设备编号、通信地址、PID参数、传感器校准系数、报警阈值等。这类数据的特点是单条体积小,几字节到几十字节;改动频率低,可能一天也就改几次;但一旦掉电丢失,整个设备等于废了,现场重新标定要花大量人工。
第二类是运行日志与事件记录:温度曲线、压力采样、报警记录、开关机状态变化。这类数据是持续产生的,一个采样周期可能就要追加几条记录,一天下来从几百KB到几十MB都有可能。它们要求能连续写入,最好还能被上位机直接读取分析。
第三类是批量数据与固件升级:采集到的完整波形文件、配方表、固件镜像。这类数据体积大,几百MB到几个GB很常见,需要文件化管理,方便工程师通过U盘或网络拷贝出来。
问题在于,这三类数据对存储介质的要求是互相矛盾的。参数数据要求单字节随便改,擦写次数越多越好;日志数据要求大容量、持续追加、掉电后尽量不损坏;大容量数据则要求有文件系统、读取方便。
如果全塞进一块Flash芯片里,Flash的擦写寿命和按扇区擦除的特性会折磨死人;如果全塞进SD卡,嵌入式环境里挂文件系统、掉电恢复、兼容性这些问题会一个接一个冒出来。所以工业控制器几乎都是分级的:每类数据选最合适的载体,而不是一块存储打天下。
1.2 用一张“收纳图”理解分级存储架构
打个比方。家里的收纳逻辑通常是:钥匙和常用药放在床头柜抽屉里,随手就能拿到;经常看的书放在书架上,翻开就能读;不常用的旧资料打包放进储物间或地下室。
存储介质也是这个道理。EEPROM就是那个床头柜抽屉——容量不大,但拿取最快、最灵活;NOR Flash是书架——容量中等,读起来很快,适合放需要“上电就能跑”的内容;SD卡就是储物间——容量大,方便存取整箱整袋的东西,但需要一套“登记管理”系统(文件系统),操作起来也更重。
对应到控制器里,这套分级架构通常长这样:
- 第一级(L1)参数存储:EEPROM,容量KB级,按字节改写,寿命在100万次左右,存设备参数、校准数据、故障标志。
- 第二级(L2)关键代码与备份存储:NOR Flash,容量MB级,按扇区擦除,支持片内执行,存Bootloader、应用程序,以及参数双备份。
- 第三级(L3)大容量记录存储:SD卡,容量GB级,带FAT文件系统,存运行日志、历史曲线、升级包。
有人会问:SD卡又大又便宜,为什么不能把参数也存SD卡?这个我后面会展开,简单说就是:SD卡挂在文件系统上,每次读写都要挂载、打开文件、定位、写入、关闭,复杂度和故障率都远超直接线性访问的EEPROM。
2. 三种存储介质的核心特性与选型对比
2.1 EEPROM:按字节改,参数配置的不二选择
EEPROM的全称是Electrically Erasable Programmable Read-Only Memory,中文常叫“电可擦除可编程只读存储器”。它的核心优势是可以按字节擦写,写一个字节和写一整页的代价差不多,擦写寿命普遍在10万到100万次,比Flash高一个数量级。
工业控制器里最常用的是I2C接口的AT24C系列,比如AT24C02(2Kb)、AT24C16(16Kb)、AT24C32(32Kb)。这些芯片封装小(SOIC-8很常见)、外围电路极简(两个上拉电阻就能跑),价格也就几毛钱到两块钱。
这里有个关键参数容易被人忽略:写周期(Write Cycle Time)。I2C EEPROM写完一个字节(或写完一页)之后,芯片内部还要把数据真正“固化”到浮栅里,这个过程通常需要5ms左右。在这个时间内,芯片不响应任何I2C命令。如果你连续写,必须老老实实等这5ms,或者通过轮询ACK的方式检测写周期结束。很多数据飞掉的问题,根源就是没管这个写周期。
另外,EEPROM支持页写(Page Write)。以AT24C32为例,页大小是32字节,一次可以连续写32字节,只要不跨页边界。批量修改参数时,用页写比逐字节写快得多。
还有一个容易被忽略的细节:如果MCU用内部Flash模拟EEPROM,虽然省了外部芯片,但MCU片内Flash的擦写寿命通常只有1万到10万次,在频繁改参数的工业设备上,几年就可能磨穿。独立的EEPROM芯片寿命更高,而且坏了还能单独换,不用动主控,所以在量产设备里我一直坚持外挂独立EEPROM。
2.2 NOR Flash:掉电不丢、启动引导的硬汉
NOR Flash的特点是读速度快、可以按字节随机读取,甚至支持片内执行(XIP),也就是说CPU可以直接在NOR Flash上跑程序,不用先拷到RAM里。但是它的写入很麻烦:写之前必须先擦除,而擦除的最小单位是扇区(常见4KB或64KB),擦除一片4KB扇区通常要几十到几百毫秒。
工业控制器里最常用的是SPI接口NOR Flash,比如Winbond W25Q64(8MB)、W25Q128(16MB)。它的主要用途有三个:
第一,存Bootloader和应用程序。单片机复位后第一条指令从NOR Flash读出来,不需要初始化SD卡或文件系统,上电即启动。工业设备对启动时间有要求,用NOR Flash最直接。第二,存FPGA配置文件。FPGA是SRAM架构,掉电配置就丢失,上电时需要从配置芯片加载比特流。有些方案用专用配置芯片,但用SPI NOR Flash配合FPGA的从串模式(Slave SelectMAP或SPI Flash配置模式)也很常见。第三,做关键数据的备份区。比如EEPROM里的参数在写入前先备份一份到NOR Flash,万一EEPROM损坏,设备还能从备份恢复。
为什么SD卡都这么普及了,NOR Flash还是没被淘汰?因为NOR Flash是线性可寻址的,不需要文件系统,上电就能读,做启动引导不可替代。而且NOR Flash的可靠性比SD卡高得多,没有机械结构,不怕震动和拔插。
我做项目时,NOR Flash一般选W25Q128这类16MB容量的,分出几个固定区域:Bootloader、App主程序、FPGA配置文件、参数备份区、日志暂存区。后面会给出具体的地址规划。
2.3 SD卡:大容量日志和文件管理的现实担当
SD卡的优势不用多说:容量大、便宜、可拆卸、自带文件系统支持。接上FATFS这类开源文件系统库之后,日志和采集数据可以直接写成CSV或二进制文件,拔下来插电脑就能看,对现场工程师极其友好。
但SD卡也有几个“性格”要先摸清。第一,SD卡是块设备,读写最小单位是扇区(一般512字节),每次写一个字节也要先读改写整个扇区,频繁小写入对寿命和性能都很不友好。所以日志系统必须做缓冲,攒够一块再整块写。第二,SD卡的分区表、FAT表、目录项在掉电时容易损坏,尤其写文件写了一半突然断电,轻则丢文件,重则整卡的文件系统打不开。第三,SD卡的擦写寿命虽然也有磨损均衡机制,但廉价卡的质量参差不齐,工业环境下必须做兼容性测试。
工业控制器里用SD卡,接口一般选SDIO 4-bit模式,速度比SPI模式快得多。STM32F4系列自带SDIO外设,配合DMA,连续写速度能到几MB/s,满足绝大多数日志场景。如果板上空间紧张或者对速度要求不高,也可以退而用SPI模式接线,但性能会明显下降。
FatFS是目前嵌入式里最常用的文件系统库,免费、可裁剪、代码量不大。移植时只需要实现disk_initialize、disk_read、disk_write、disk_ioctl这几个底层函数,然后把FF_USE_LFN打开支持长文件名,把FF_FS_EXFAT打开支持exFAT(大容量卡用)。这个后面实操部分细说。
2.4 选型对照表与容量测算方法
先给一张我常用的选型对照表,方便快速决策:
| 存储介质 | 接口 | 最小操作单位 | 典型擦写寿命 | 典型容量 | 是否需要文件系统 | 主要用途 |
|---|---|---|---|---|---|---|
| EEPROM | I2C | 字节/页 | 10万~100万次 | 2Kb~256Kb | 不需要 | 参数、校准数据、故障标志 |
| NOR Flash | SPI/QSPI/并口 | 扇区擦除+页编程 | 1万~10万次 | 1MB~64MB | 不需要 | Bootloader、App、FPGA配置、备份 |
| SD卡 | SDIO/SPI | 扇区(512B) | 依卡质量,几万~几十万次 | 数GB~数TB | FAT/exFAT | 运行日志、历史数据、升级包 |
容量怎么算?我给你一个例子。一台设备每秒记录10个模拟量点位,每个点位4字节(float),再带一个4字节时间戳,单条记录就是44字节。每秒写10条,一分钟才26.4KB,一天大约38MB。一张32GB的SD卡,理论能存800多天。但实际不能这么算,因为日志文件按天切分会有文件头和目录项开销,SD卡实际可用容量还要打折。所以我的经验是:按估算需求的三倍配置SD卡容量,给文件系统开销和长期磨损留余量。
参数区的容量测算就简单了。假设设备有200个参数,每个参数4字节,再加一些字符串配置,总共也不到2KB。用一片AT24C16(2KB)或AT24C32(4KB)绰绰有余。厂家批量采购时,2KB和4KB的价差可能就几毛钱,所以在参数区设计上我倾向于买大不买小,省得固件后期加需求时容量不够用。
3. STM32+FPGA的存储架构分工与设计
3.1 为什么是双芯片,而不是STM32单机搞定
先说清楚STM32和FPGA在这套架构里分别扮演什么角色。一般来说,FPGA负责高速、并行、确定性强的数据采集与预处理,STM32负责协议解析、存储管理、通信等事务性工作。
存储系统也一样。FPGA本身没有文件系统的概念,你让它在SD卡上跑FATFS,属于硬给自己找麻烦。FPGA更擅长的是把多路ADC的采样数据以精确的时序写入内部FIFO或外部SRAM,做一个高速暂存;而SD卡、EEPROM、NOR Flash这些存储的驱动和管理,交给STM32更顺手。
为什么不让FPGA把数据直接写SD卡?因为SD卡的写时序受文件系统管理和缓存策略影响,实时性和确定性都不好。FPGA在采集过程中一旦写SD卡被阻塞,高速数据就丢了。所以正确的分工是:FPGA先把采集数据暂存在自己的FIFO里,STM32按照自己的节奏把FIFO里的数据搬出来,再写入SD卡。FPGA的数据采集和STM32的数据落盘被解耦了,谁慢都不会丢数据。
当然也有人会说,STM32单芯片也能干这些活。确实,如果采样率不高、路数少,比如几十kHz的单路ADC,STM32靠DMA也能应付。但一旦通道数多(比如八路同步采样)、采样率高(几Msps以上)或者要求采样时序严格同步,STM32就吃力了。加一片FPGA后,存储的压力反而减轻了。这也是工业控制器里“MCU+FPGA”组合越来越常见的原因。
3.2 总线拓扑与资源分配具体方案
这里给出一个我在项目里验证过的具体方案,主控用STM32F407,FPGA用中低端规模(比如Spartan-6或Artix-7级别的),存储外设分布如下:
- EEPROM:挂在STM32的I2C1上,地址引脚A0/A1/A2全部接地,7位从机地址0x50。存设备参数和校准系数。
- NOR Flash:挂在STM32的SPI2上,片选用PB12。16MB空间里分区分块,做Bootloader、App、FPGA配置、参数备份和日志暂存。
- SD卡:挂在STM32的SDIO接口上,4-bit模式。D0-D3、CLK、CMD分别接到PC8-PC11、PC12、PD2。跑FatFS,存按天切分的日志文件。
- FPGA采集数据:通过一个并口(比如8位或16位数据总线+握手信号)连接STM32的FSMC接口,FPGA内部FIFO满半时产生中断,STM32用DMA把数据搬进内存缓冲。
这张拓扑图里,EEPROM和NOR Flash是独立的,SD卡也是独立的,三者互不干扰。哪怕SD卡坏了、拔了,参数读写和程序运行都不受影响——这是工业设备的基本要求:存储外设不能互相拖后腿。
如果你的项目里FPGA也需要一片NOR Flash做配置存储,有个做法是把NOR Flash挂在FPGA和STM32之间共享。FPGA上电时先占用NOR Flash加载配置,配置完成后释放总线,STM32接管。这样省了一片Flash,但电路上需要做总线切换,而且切换逻辑要小心,搞不好会两边同时访问。我一般建议宁可多放一片便宜的Flash,也别省这个事。
3.3 地址空间与读写权限规划
存储系统的地址规划,就是给每类数据找固定位置,防止互相覆盖。我以最常见的情况为例:EEPROM用AT24C32(4KB),NOR Flash用W25Q128(16MB),SD卡不规划固定地址、由文件系统管理。
EEPROM的4KB空间规划:
| 地址范围 | 用途 | 说明 |
|---|---|---|
| 0x0000 - 0x00FF | 设备参数区 | PID参数、通信地址、阈值等 |
| 0x0100 - 0x01FF | 校准数据区 | 传感器零点、满量程校准系数 |
| 0x0200 - 0x02FF | 报警与故障标志 | 最近一次报警代码、时间戳 |
| 0x0300 - 0x03FF | 掉电保存缓冲区 | 掉电前需要紧急保存的变量 |
| 0x0400 - 0x0FFF | 保留区 | 固件版本升级后的扩展预留 |
NOR Flash的16MB空间规划:
| 地址范围 | 区域 | 说明 |
|---|---|---|
| 0x000000 - 0x0FFFFF(1MB) | Bootloader | 上电引导程序,启动时优先执行 |
| 0x100000 - 0x2FFFFF(2MB) | 应用程序区 | App主程序,支持在线升级 |
| 0x300000 - 0x3FFFFF(1MB) | FPGA配置区 | FPGA比特流文件,上电自动加载 |
| 0x400000 - 0x4FFFFF(1MB) | 参数备份区 | EEPROM参数的双份备份 |
| 0x500000 - 0xFFFFFF(11MB) | 日志暂存区 | 掉电前紧急数据暂存、历史事件备份 |
为什么启动代码要放NOR Flash而不是SD卡?因为这涉及上电引导顺序:SD卡要初始化、要挂载文件系统,期间要靠主控固件撑着,而主控固件本身就需要一个可靠的启动源头。NOR Flash上电即可读,CPU复位后直接跳进去执行,没有依赖链。这个特点叫做“零依赖启动”,在工业设备里是底线要求。
参数为什么要双备份?因为EEPROM写入过程中如果掉电,可能写到一半,校验和不过,数据就废了。双备份方案是:写参数时,先把新值写入备份区(NOR Flash),写入成功后再更新EEPROM主区;上电时读EEPROM主区,如果校验失败就用NOR Flash里的备份恢复。这样即使掉电,也有最后一道保险。
4. 关键实现细节:从时序到代码
4.1 EEPROM的I2C读写要点:别忽略写周期
先看一段最基础的I2C EEPROM写操作,基于STM32 HAL库:
uint8_t params[32]; uint16_t addr = 0x0000; // 一次性写32字节到EEPROM(AT24C32页大小为32字节,刚好一页) HAL_I2C_Mem_Write(&hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_16BIT, params, 32, 100); // 关键:写完必须等待写周期完成 HAL_Delay(10);注意几个点。第一,AT24C32是16位地址,所以I2C_MEMADD_SIZE_16BIT必须带上,如果只用8位地址模式,地址超过0xFF就会出现错乱,读出来全是FF。第二,0xA0是7位地址0x50左移一位的结果,HAL库里的器件地址参数是8位格式,所以要用0xA0而不是0x50,这个搞错的人不在少数。
写周期等待有两种方式。一种是上面代码里的死等延时,简单可靠,但浪费时间。另一种更优雅的是写完后立刻发一个重复起始条件读器件地址的低字节,如果芯片返回NACK,说明写周期还没结束;返回ACK,说明写完了。实际用下来,死等5-10ms完全够用,而且代码更直观。
页写有一个边界陷阱。AT24C32页大小32字节,如果你从地址0x0010开始连续写40字节,写到第32字节时地址会回卷到页首,把前面写的数据覆盖掉。解决方法是:写数据前先检查起始地址到页边界的距离,超过页边界就拆成多次页写。这个坑我踩过一次,排查了整整一个下午。
另外一个非常重要的寿命保护策略:不要每次上电都写EEPROM。很多现场问题就是这样埋下的——启动时把参数从EEPROM读出来,又把同样的值写回去,一天开机几十次,一年就是上万次擦写。正确的做法是:启动时只读不写,只有参数真正变化时才写入,并且写入前先比较新旧值,不同才写。
4.2 NOR Flash的SPI操作与扇区管理
以W25Q128为例,它的几个核心命令要背下来:
- 写使能:0x06
- 读状态寄存器1:0x05
- 读数据:0x03
- 页编程:0x02(一次最多写256字节)
- 扇区擦除:0x20(擦除4KB)
- 块擦除:0xD8(擦除64KB)
NOR Flash的操作规则一句话概括:先擦后写。写数据前对应的扇区必须是全0xFF状态,否则数据写不进去。
扇区擦除的标准流程如下:
uint8_t SPI_Flash_ReadStatus(void) { uint8_t status; CS_LOW(); SPI_SendByte(0x05); status = SPI_RecvByte(); CS_HIGH(); return status & 0x01; // WIP位,1表示忙 } void SPI_Flash_EraseSector(uint32_t addr) { // 1. 写使能 CS_LOW(); SPI_SendByte(0x06); CS_HIGH(); // 2. 发送扇区擦除命令和24位地址 CS_LOW(); SPI_SendByte(0x20); SPI_SendByte((addr >> 16) & 0xFF); SPI_SendByte((addr >> 8) & 0xFF); SPI_SendByte(addr & 0xFF); CS_HIGH(); // 3. 等待擦除完成,一般几百毫秒 while (SPI_Flash_ReadStatus() == 1); }有个细节要注意:写使能(0x06)必须在CS拉低时发送字节后再拉高,命令和地址之间不能再插别的操作。有些工程师把写使能和擦除命令合并成一次CS低电平周期,就会出现擦除无效、状态寄存器WEL位(写使能锁存)一直为0的情况。这是SPI Flash比较容易踩的时序坑。
另一个工业场景必须面对的问题:磨损均衡。日志暂存区如果每次都写同一个扇区,寿命会很快耗尽。我的做法是做一个4KB扇区的轮换表:把日志暂存区划成64个扇区,用一个固定扇区记录当前写到哪个索引,写完一个扇区就指针加一,循环使用。这样单个扇区的擦除频率降到原来的六十四分之一,整个区域的寿命大幅延长。
NOR Flash还支持QSPI模式,把时钟提到104MHz,读取性能能提升好几倍。STM32F4/F7系列自带QSPI外设,代码层面比用普通SPI麻烦一些,但数据量大的时候值得用。如果是做运动控制或高速采集的控制器,日志数据量大的话建议直接上QSPI。
4.3 SD卡的FATFS移植与缓冲设计
SD卡这部分,硬件连接确认无误后,主要工作集中在FatFS的移植和日志写入策略上。
移植FatFS时,几个配置宏直接决定后面用起来顺不顺手:
#define FF_USE_LFN 1 // 支持长文件名,默认是0,只支持8.3短名 #define FF_LFN_UNICODE 0 // 用ANSI/OEM编码,中文文件名能正常 #define FF_FS_EXFAT 1 // 支持exFAT,大容量卡必须开 #define FF_USE_MKFS 1 // 支持格式化,调试时很有用 #define FF_USE_STRFUNC 1 // 支持f_printf,写日志方便 #define FF_FS_RPATH 1 // 支持相对路径和chdir挂载和打开文件:
FATFS fs; FIL log_file; FRESULT res; res = f_mount(&fs, "", 1); // 挂载SD卡 if (res == FR_OK) { res = f_open(&log_file, "0:/LOG/20260713.CSV", FA_OPEN_APPEND | FA_WRITE); }日志文件建议按天切分,文件名里带日期,比如20260713.CSV。这样单个文件不会无限膨胀,现场查问题时直接按日期找文件,用户体验好得多。
然后是关键的写入缓冲策略。前面说过,SD卡每次写一个扇区(512字节),如果来一条数据写一次,频繁的小写入会让SD卡寿命消耗得非常快。我在STM32侧维护一块4KB的RAM缓冲区:
- 采集数据由FPGA中断触发,STM32通过DMA把数据从FPGA FIFO搬到RAM缓冲。
- 缓冲区的数据量达到4KB时,一次性调用
f_write写入SD卡。 - 每次写入后刷新文件缓存
f_sync——但注意不能每条都刷,否则性能崩掉。
实际经验是:写4KB缓冲后再sync,速度既不会太慢,掉电丢数据的范围也可控。每秒产生的数据如果只有几十KB,那最多丢不到4KB的数据,可接受。
这里要纠正一个常见误区:FatFS的f_write并不是直接写物理扇区,它有自己的扇区缓存和簇分配逻辑。f_sync才负责把文件系统的缓存数据真正落到SD卡。所以很多工程师发现文件丢了,是因为只调用了f_write,没调f_sync就断电了。正确姿势是:关键数据点或固定周期内调用f_sync,给文件系统一个“落盘”的机会。
CRC校验不能省。SD卡在工业环境里偶尔会出传输错误(走线过长、电磁干扰导致数据线毛刺),虽然FatFS本身不带扇区校验,但我在每条日志记录末尾加了16位CRC,读取时可以快速判断这条记录是否损坏。多花几个字节的存储,换来的排查效率提升非常值得。
4.4 掉电保护:工业现场最容易翻车的环节
工业设备最怕的不是稳定运行时的故障,而是突然掉电。掉电瞬间,参数可能写到一半,日志文件还开着没关闭,NOR Flash擦除到一半——这些问题积累下来,轻则数据损坏,重则设备起不来。
硬件层面,我常用的方案有三道保险:
第一道,电源掉电检测。用STM32内置的可编程电压检测器(PVD),在VDD掉到设定阈值以下时触发中断。阈值要选在电压已经低于稳定工作区,但还没跌到芯片复位电压之间的窗口里——比如3V左右触发,此时芯片还能正常工作几毫秒到几十毫秒。
第二道,供电延寿。在电源输入端加个大电容或超级电容,让掉电后系统还能维持几百毫秒的工作时间。这个时间足够完成“参数写入EEPROM、f_sync关闭文件、NOR Flash擦除完成”这些收尾动作。具体电容容量怎么算,一个经验值:系统掉电前需要100ms,平均电流100mA,5V供电,需要的能量约0.05J。用公式C=2E/U^2估算,约需4700uF的大电容。实际测试时可以根据波形调整。
第三道,掉电处理流程。PVD中断只做一件事:置一个“掉电标志”,然后回主循环。主循环检测到标志后,按优先级执行收尾动作:
- 停止FPGA采集,防止新数据进来打乱状态。
- 把当前参数副本写入EEPROM,并写一条掉电记录。
- 调用
f_sync关闭当前SD卡日志文件。 - 往NOR Flash写一个掉电标志,方便下次上电判断是否发生过异常断电。
为什么要费劲做这一套?因为工业现场的数据对于事后故障分析太重要了。几百毫秒的延寿时间里完成收尾,就能保证下次上电时设备状态是可控的。这比任何花哨的功能都实在。
5. 调试实录:常见问题与排查手段
5.1 EEPROM数据飞掉、I2C死锁怎么办
先讲一个典型的“掉数据”案例。某次现场测试,设备运行几天后,PID参数偶尔变成默认值,排查代码逻辑没发现任何问题。后来用逻辑分析仪抓I2C波形,发现设备在启动瞬间,EEPROM写周期内又发起了一次读操作,导致读回来的数据全FF。原代码里写了EEPROM之后没有等待写周期结束,直接读回来校验,当时测试数据恰好读得快,问题没暴露,上线后才显现。
排查方向一:地址和时序。I2C速率先降到100kHz,写周期延时加到10ms。如果问题消失,说明时序余量不足。排查方向二:总线死锁。I2C总线上SDA被拉低,常见原因是某个器件时钟拉伸或异常导致总线状态机卡死。解决办法是写一个总线恢复函数:把SCL翻转9次,让从机释放SDA,然后发一个STOP条件重新初始化。
排查方向三:器件地址冲突。板上如果挂了两片I2C EEPROM且地址引脚都设成相同地址,读出来的就是两份数据的混合体。挂在同一个I2C总线上的设备地址不能重复,这个在设计阶段就要核对。
给一张排查速查表:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 读回数据全0xFF | 地址不对、器件没焊好、写周期内读取 | 示波器抓I2C波形、检查器件地址 |
| HAL_I2C卡住返回BUSY | 总线被外部器件占用或死锁 | 总线恢复函数、重新初始化 |
| 参数隔几天丢一次 | 写周期未等待、电源噪声 | 加延时、PVD掉电保护、加CRC校验 |
| 写入不生效 | WP引脚被拉高 | 量WP引脚电压,确认硬件上拉/跳线 |
5.2 NOR Flash擦除失败与坏块处理
NOR Flash的擦除失败,最常见原因有两个。一个是写使能没生效。写使能命令必须单独发,而且要确保CS在正确时序下拉低再拉高。很多时候是代码里CS控制时序不对,导致WEL位(写使能锁存)一直是0。排查时读状态寄存器,看到WEL位为0,就知道问题出在哪一步了。
另一个是擦除时间不够。有些工程师擦除后不等待WIP位清除就继续下一个操作,下一个操作发出去,Flash还在忙,命令全部被忽略。所以擦除后必须轮询状态寄存器,直到WIP位清零再继续。这是死规则,没有例外。
NOR Flash没有像NAND那样的坏块管理机制,这让我在工业项目里很头疼。我的做法是:软件层面做坏块标记和轮换。扇区擦除后,读回所有字节验证是否为0xFF。如果不是,连续擦拭三次仍失败,就在这个扇区的块头写一个“BAD”标记,后续读写绕过它。启动时扫描所有扇区,建立一张可用扇区表。这套逻辑不复杂,但能显著延长Flash的实际使用寿命。
5.3 SD卡挂载失败与文件损坏的排查
SD卡“挂不上”是排障榜第一名。最常见的硬件原因:卡槽的Card Detect引脚没接上拉,导致卡插进去系统检测不到。或者卡的电源引脚没做滤波,供电纹波过大导致卡上电初始化失败。
排查时不要上来就翻代码,先做三步检查:
第一步,量硬件。用万用表量卡槽引脚电压,确认CD引脚在插卡后从高变低(或从低变高)。检查VCC电压是否稳定在3.3V。
第二步,抓初始化时序。用示波器或逻辑分析仪抓SDIO接口的CMD和CLK波形,确认上电初始化命令序列是否完整。SD卡初始化会依次发CMD0、CMD8、ACMD41、CMD2、CMD3、CMD7。如果中间某条命令没有响应,问题大概率在电平或时序匹配上。
第三步,换卡验证。治兼容性问题最简单的方法就是换一张不同品牌的卡。SD卡市场型号繁杂,工业产品一定要做兼容卡列表测试。我们项目里测试过十几种卡,把不稳定的几个型号直接加进黑名单,现场就少了很多莫名奇妙的投诉。
文件损坏的问题分两种。一种是FAT表损坏,整个卡都打不开。这种一般发生在写文件过程中突然掉电,没有f_sync。对这种问题,除了程序里加掉电保护,还要给FatFS开启掉电前刷盘机制——定时f_sync,或者每条关键记录写完后f_sync。另一种是单个文件损坏,打开文件后内容读到一半报错。我见过最多的原因是:文件系统断电前没有正确关闭,导致目录项和FAT链不一致。恢复手段是把损坏的文件删掉,重新建一个新文件,日志数据从头开始积累。
5.4 调试存储子系统时的通用排查顺序
最后把多年调试经验浓缩成一个检查清单,按顺序做,可以省下大量无头绪的时间:
- 先看电源和地。存储芯片对电源纹波敏感,3.3V纹波超过50mV就会出各种诡异问题。用示波器量芯片电源引脚,纹波大就先解决电源。
- 降低速度。I2C降到100kHz、SPI降到1MHz、SDIO降到25MHz。速度降低后问题消失,就是时序余量不够,后面再逐步提速找上限。
- 单项验证。把业务逻辑全部去掉,只写一个存储测试工程。单独操作EEPROM、NOR Flash、SD卡,确认每个器件在最小系统下都能正常工作。
- 加CRC和版本号。每个存储块头部写一个数据块版本号加16位CRC。出错时能迅速判断是数据被改过、还是地址读错、还是链路干扰。
- 压力测试。持续写入48小时,穿插100次断电重启。这种测试能暴露绝大多数存储系统的问题,比在现网设备上赌运气强得多。
这么多年做下来,我的体会是:存储系统设计最怕的不是方案本身的复杂性,而是“看起来够用”的侥幸心理。EEPROM、NOR Flash、SD卡各自的特性差异很大,如果不能量体裁衣,总会在某个意想不到的时刻爆发问题。这套分级存储方案在我们几个控制器项目里已经稳定跑了挺长时间,至少到目前为止,存储相关的返修记录寥寥无几。
最后再分享一个小技巧:不管用哪种介质,给每条记录都加上16位CRC和帧类型标志。在调试现场遇到数据异常时,你能立刻判断是“链路传输错误”还是“写入逻辑Bug”,这个区分能帮你在排查时省下成倍的时间。存储系统设计没有太多玄学,把每类数据的访问习惯摸清楚、把最坏情况预案做好,剩下的就是时间验证的事。