工业控制器里那些该存不该存的数据,这几年我没少跟它较劲。掉电丢参数、Flash写坏、SD卡文件系统崩了,都是在现场最容易挨骂的故障。后来我把存储体系从"一个存储芯片扛所有"改成了 STM32+FPGA 分级存储,用 EEPROM、NOR Flash、SD 卡各管一摊,才算真正根治了大半的存储痛点。这篇是"硬件篇"系列第 12 篇,聊的就是这套分级方案怎么设计、三种介质怎么分工、底层驱动有哪些必须注意的细节。打算做工控产品、参与设备存储设计、或者只是想把 STM32 的数据存储搞扎实的,这篇应该能给你省下不少弯路。
先说清楚一个背景:我这里讲的"工业控制器",是典型的双处理器架构——一颗 STM32 负责逻辑控制、通信和系统管理,一颗 FPGA 负责高速采集、实时波形生成的硬实时任务。这种组合在运动控制、电力电子、边缘计算网关里都很常见。数据存储在这种系统里不是一个存字那么简单,它牵扯到掉电可靠性、写入寿命、读取速度、脱机分析和日志追溯,东西很杂,但思路理清楚之后,其实就一句话:把不同性格的数据,交给不同性格的存储介质。
1. 为什么要分级存储:先把需求盘明白
1.1 工业控制器的数据其实分成好几类性格
很多刚入行的朋友拿到项目,第一反应是"直接挂一个 SD 卡,省事"。我一开始也这么干过,后来被现实教育了。工业控制器的数据不是铁板一块,至少能分成四类,而且每一类的脾气完全不一样。
第一类是运行参数类,比如 PID 值、电流环增益、编码器零位、校准补偿系数、通信地址和使能开关。这类数据的特点是量特别小,可能总共也就几十到几百字节,但是改动频率很低,可能设备装完调一次参数就一年不动了。它对可靠性要求极高,因为在现场换一台设备重新做整定,代价比买一颗芯片大得多。而且这类数据必须支持"字节级"独立修改——我只想改一个 P 值,不想把整片数据都重写一遍。
第二类是程序和配置类,比如 STM32 的固件镜像、FPGA 的比特流文件、字库、表格、启动参数。这类数据的特点是量大,几百 KB 到几十 MB,读取频率经常发生,但平时几乎不写入,只有在出厂烧录和远程升级的时候才动。它还需要一个关键能力:可以被 XIP(片上执行)或者在启动阶段快速映射到内存,缩短上电时间。
第三类是日志和追溯类,比如故障记录、历史报警、温度趋势曲线、运行时长统计、操作记录。这类数据最大的特点是持续追加写入,量大且频繁,而且往往需要定期导出来做数据分析,设备返修时也要能拿到最后一刻的运行轨迹来定位问题。
第四类是临时和缓冲类,比如采集过程中未处理完的原始数据、上位机下发的临时指令。这类数据只在运行期间有意义,掉电丢了无所谓,但是写入速度要够快,不能拖累主流程。
一眼就能看出来,一张 SD 卡去扛第一类和第二类任务,纯属杀鸡用牛刀还杀不好。反过来,EEPROM 去扛第三类,容量和寿命都不够。所以"分级"不是花架子,是需求本身逼出来的。
1.2 "一张卡存所有"为什么行不通
我见过不少方案把全部分区都放在 SD 卡上,标称支持日志、参数、升级包。听着很美好,实际用起来问题一个接一个。
先说寿命。SD 卡的 NAND Flash 块擦写次数普遍在几百到几千次这个量级(质量好一点的工业级卡能到几万次),而运行参数这种高频改写的数据,如果每次开机都把校准值刷一遍,一张卡几个月就开始出现坏块。NOR Flash 的寿命能到十万次级别,EEPROM 更是百万次级别,这才是参数类数据的应许之地。
然后是掉电一致性。SD 卡的写入涉及文件系统元数据更新、FAT 表修改、数据块搬移,中间任何一步被断电打断,轻则文件丢失,重则整个文件系统挂掉,量产设备返修率直接飙上去。参数存在 EEPROM 里就不一样,页编程本身具备原子性,只要写周期不被打断,数据就是完整的。
还有启动依赖的问题。工业控制器上电到进入运行状态,往往限定在几百毫秒内。如果所有东西都在 SD 卡上,而 SD 卡又处于异常状态,连"从备份恢复参数"这个自救动作都做不了。EEPROM 和 NOR Flash 挂在 SPI/I2C 总线上一叫就应,SD 卡的初始化流程却得走漫长的命令协商、扇区分区解析、文件系统挂载,速度和可靠性完全不在一个维度上。
所以这套方案的核心逻辑就八个字:各司其职,按性格分配。参数给 EEPROM,程序和字库给 NOR Flash,日志和批量数据给 SD 卡,临时缓存掉电不管。既保证了可靠性,又把容量、速度和寿命用到了刀刃上。
2. 存储介质选型:三兄弟的长板和短板
2.1 EEPROM:小而可靠,参数数据的最佳归宿
EEPROM 的存储原理我在系列前面讲过一点,这里再浓缩一下:浮栅晶体管中电荷的有无代表 0 和 1,编程和擦除过程本质是让电子通过隧穿效应进入或离开浮栅。它的最大特点是支持字节级擦写,想改哪个字节就改哪个字节,不用先擦一整个扇区。
最适合 EEPROM 的接口就是 I2C。典型芯片是 AT24C256、AT24C512,容量 256 KBits(32KB)到 512 KBits。I2C 只需要两根线(SCL、SDA),不占用宝贵的外设资源,硬件布线也省心。工业环境下的 I2C 总线要注意上拉电阻取值,通常 4.7k 到 10k 之间,总线电容大时选小一点,走线长时注意信号完整。我习惯在 PCB 上给 SCL/SDA 串接百欧姆级别的限流保护电阻,防止热插拔和静电损伤引脚。
EEPROM 有一个必须尊重的特性:写周期时间。标准型号在 5V 供电下的页写周期大概是 5ms 左右,页大小通常 32 或 64 字节;跨页写要多算一次写周期。很多新手踩的坑是连续写多个字节,却只等待了一次写周期,导致后半段数据丢失或错位。正确做法是:要么逐页发送,每页之间等待 ACK 轮询或延时;要么用状态机管理写操作,别用阻塞延时死等,因为 5ms 在控制环路里已经是天文数字了。
磨损均衡对 EEPROM 来说虽然不紧迫(百万次级别),但也不意味着可以乱来。频繁变化的参数,比如运行小时数,每次开机都写,一颗芯片用十年大约写入几千次,距离寿命极限还很远;但如果你把"系统当前状态"这种每秒刷新一次的变量扔进 EEPROM,一天就 86400 次写入,估计一个月就报废了。高频变化的量,请放在 RAM 或最后落到 SD 卡,别为难 EEPROM。
2.2 NOR Flash:可执行、启动快,程序与镜像的最佳宿主
NOR Flash 在工业控制器里的地位不可替代,因为它支持字节/半字读取,还能直接映射到地址空间做 XIP 执行。这意味着 CPU 可以从 NOR Flash 直接取指运行,不用先把整个镜像搬到 RAM。对启动时间要求高的场合,这一条就是硬需求。再加上它的随机读性能远好于 NAND,启动阶段的体验完全不在一个层级。
商用典型型号是 W25Q128(128 Mbit,16MB)和 W25Q256。接口是 SPI,双线/四线模式还能通过 QPI 拉高读取速度。FPGA 侧读取镜像时对这种快速随机读特别友好,因为 FPGA 本身就可以用 SPI 主模式去拉数据流,甚至做成串行流式加载,边读边初始化。
但 NOR Flash 的写入逻辑必须门儿清:它可以"位编程"(把 1 变 0),但擦除是反过来的(把 0 变 1),而且只能按扇区擦除。W25Q128 的扇区大小多为 4KB,块大小 64KB。写入前如果目标区域已经存在旧数据,必须先整块擦除,否则写入结果就是逻辑垃圾桶。这套"先擦后写"的时序,对 STM32 来说是标配回调,对 FPGA 来设计控制器时则要自己实现状态机。
寿命方面,NOR Flash 标称擦写次数通常是一万到十万次。如果整片存的是 FPGA 镜像,平时不写,只有升级时才擦写,那寿命根本不是问题。但如果有任何设计把有状态的数据放在 NOR 里且频繁覆盖,就一定要做磨损均衡,常见做法是多个槽位轮换写入,记录当前活跃槽的索引。我在固件升级的设计里直接做了 A/B 双分区,还保留了出厂备份区,虽然多耗了一些容量,但换回来的是现场刷机失败时永远有一条退路。
2.3 SD 卡:容量和便携是真香,复杂度和风险也是真的
SD 卡的优势不用多说:单片容量从 512MB 到 1TB 随便选,可拔插,数据能被现场工程师直接带走分析,还能用文件系统组织目录。这对日志、趋势、截图、波形这类批量数据是"正道"。
SD 卡有两条路可选:SPI 模式和 SDIO 模式。SPI 模式兼容性好,几乎所有 MCU 都有 SPI 外设,代码也简单,但时钟频率上限低,普通卡跑到 25MHz 就很勉强了,而且 SPI 模式下部分 4 位模式的高级特性(CRC 等)被禁用,速度约等于阉割版。SDIO 模式可以跑 4 位并行,标准 SD 卡能到 50MB/s 左右,适合塞大量日志时使用。STM32F4 系列的 SDIO 外设支持 4 位总线,DMA 搬运之后 CPU 占用很小,是我在量产方案里的选择。
文件系统方面,在 STM32 生态里就是 FatFs 的天下。它的好处是免移植,资源占用小,但带来一个严肃问题:FAT16/FAT32 对突然断电的容错能力非常弱。文件分配表更新、目录项修改、数据区写入这三个动作只要断在中间,就可能导致整个簇链断裂甚至根目录损坏。所以我的策略是:日志文件不要老保持打开状态,每写满一定大小就同步刷新;更重要的是加"日志前后双区"或者"当前日志+备份日志"轮换机制,即便当前文件损坏,系统也能自动落入备份文件。
SD 卡还有个容易被忽略的坑:兼容性。市面上的卡虽然 SD 标准统一,但不同厂商对时序底线、初始化流程、命令响应事件的处理仍然有差异。量产时一定要做"卡片兼容性矩阵测试",把主流品牌的卡都插上去跑一遍,用同一个固件完成块读写、掉电、反复挂载的测试。我甚至有同事在产品发布前把测试脚本跑在 30 种不同的卡上,这在现场能少收一半售后工单。
| 存储介质 | EEPROM | NOR Flash | SD 卡 |
|---|---|---|---|
| 容量量级 | KB 级别 | 1~64MB | 数百 MB 以上 |
| 写入粒度 | 字节级 | 按页编程/按扇区擦除 | 扇区/块级 |
| 擦写寿命 | 100 万次级别 | 10 万次级别 | 千到数万次(取决于卡) |
| 典型接口 | I2C | SPI/QSPI | SDIO/SPI |
| 掉电一致性 | 优秀(原子写周期) | 需要先擦后写的中断处理 | 弱,FAT 易损坏 |
| 核心用途 | 运行参数、校准数据 | 固件、FPGA 镜像、字库 | 日志、趋势、批量数据 |
3. STM32+FPGA 分级存储架构设计
3.1 FPGA 为什么也要掺和进存储这件事里
很多做纯 STM32 方案的人会问:FPGA 不是管高速逻辑的吗,存储凭什么也要它参与?这里得从一个现实场景出发。以我为某个伺服驱动控制器做的存储方案为例,FPGA 负责编码器信号解码、相电流采集和 PWM 生成,它内部的采样数据以几百 K 到几 MHz 的频率持续产生。这些原始数据如果全部经由 STM32 转发,光中断和 DMA 的调度压力就够把主控吃透了。
所以架构上的第一原则是:FPGA 只负责"产生数据"和"预处理",不直接做复杂的文件系统操作,而是把经过整理的数据块通过某种高速通道交给 STM32 或直接给存储控制器。这套方案里我让 FPGA 把编码器原始数据和波形片段先写入一片 SRAM 或 FIFO 缓存区,攒够一包之后通过并行总线或 SPI DMA 发送给 STM32,再由 STM32 决定是存卡还是存 Flash。这样既发挥了 FPGA 高速采集的硬件优势,又把"哪个数据去哪个介质"这种策略问题集中到了 STM32 一个点上,维护逻辑特别清爽。
反过来,FPGA 的配置流如果每次都靠上位机写 SPI,上电时序就没法保证。我在板级设计里把 FPGA 的镜像也放进了 NOR Flash 的独立分区,上电时 STM32 通过 SPI 把 bitstream 完整加载到 FPGA,加载完成后释放配置引脚并等待握手。这个过程中 NOR Flash 的随机读能力和 FPGA 的流式加载能力天然契合,完全不用把数据读到 RAM 再慢慢推,直接把读取和配置拼接成一个流水线。
3.2 总线与信号划分:谁管数据流,谁管控制流
存储系统的总线划分直接决定调试效率和数据流的畅通性。我的方案在总线层做了明确的职责分工:EEPROM 和 NOR Flash 挂在 STM32 的 I2C1 与 SPI2 上,SD 卡挂在 SDIO 1 上,FPGA 则通过一个专用的并行总线(一次 16 位,片选加读写脉冲)和 STM32 的 FSMC 接口对接。这里需要提一个经验点:不要把 SD 卡和 NOR Flash 挂在同一个 SPI 总线上,因为两者都是有大量 DMA 传输的块设备,一块数据传送期间总线被占住,另一块就得干等,遇到时序敏感的写入操作很容易超时。
信号划分上,每个存储器件都有自己独立的片选信号,尽量用 GPIO 输出而不是挂在同一个译码器后面。因为现场总会出现"我需要单独重置某一个器件的片选状态"的情况,用独立 GPIO 调试起来最直观。数据线和时钟线建议按不同的阻抗分组走线,避免信号串扰,尤其是 SDIO 的四条数据线在高速模式下必须做到等长和阻抗连续。我踩过的一个坑是 SDIO 数据线在 PCB 上绕行太长,时钟跑到 48MHz 以上时读数据开始随机出错,后来通过收短线长和串接 22Ω 电阻才稳定下来。
还有一个必须提前设计的是电源域和掉电监测。EEPROM 这种小容量器件直接挂在 3.3V 主电源域没问题;NOR Flash 如果是大容量(16MB)并且频繁进行擦写,瞬时电流可以到几十毫安,要给它加一个低阻抗的电源路径,必要时并联 10μF 陶瓷电容。SD 卡在高负载写入时电流尖峰更明显,尤其是一些质量不是太好的卡,瞬间大电流甚至会把 MCU 的 ADC 参考电压拉下去,所以 SD 卡电源用单独的 LDO 或 DCDC 供电是最优解。
3.3 掉电保护策略:最关键的一环
工业控制器存储方案的灵魂其实是掉电保护,因为设备永远不知道下一秒会不会被分闸。我的设计里有一套三级掉电保护流程,这套流程反复打磨过,现在很稳定。
第一级是电源监测。STM32 和 FPGA 共同监测一个精确的输入电压阈值点,一旦电压跌落到我设定的阈值(比如主电源 24V 跌到 18V),立刻进入紧急模式。此时 CPU 停止所有非关键任务,仅保留必要的外设。
第二级是储能缓冲。在电源输入端放一个足够大的储能电容组(快速充放电的电解电容 + 钽电容混合),保证从检测到掉电到系统安全关闭还有一个 5~10ms 的窗口期。这个窗口期足够把关键的运行参数按"需要保存的紧急队列"写入 EEPROM,或者把最后一段日志 flush 到 SD 卡。这个窗口期计算方式是:C × ΔU / I 必须大于目标延时的电荷需求,比如 200μF 电容从 24V 跌到 18V 能提供的能量,足以让一个 100mA 的系统维持约 12ms。
第三级是逻辑预案。在掉电的情况下,不写 SD 卡的大文件,避免文件系统损坏;只在 EEPROM 里记录"掉电发生"标志和掉电时刻,并在下次上电时,根据这个标志决定是否做 SD 卡一致性恢复。这套逻辑的顺序性,代码里是用一个中断服务函数 + 一个状态机保证的,任何一步没完成都会把状态停留在掉电序列上,不会进入假死。
4. 实操实现:三种介质的驱动要点与踩坑记录
4.1 EEPROM 的 I2C 读写细节与页内写保护
EEPROM 的读写框架本身并不难,难的是细节。以 AT24C256 为例,一个重要的参数是页大小(常见 32 字节)。I2C 支持一次写多个字节,但连续写不能跨页,否则数据会回卷到本页开头,覆盖掉已经写入的数据。这是最容易踩的坑。我写过一个通用接口,把"待写数据长度"和"当前页剩余空间"做比较,如果剩余空间不足就分成多次写操作,每次写满一页再等待写周期结束。代码骨架大致如下:
uint8_t EEPROM_WritePage(uint16_t addr, uint8_t *buf, uint16_t len) { // 检查地址越界 if (addr + len > EEPROM_SIZE) return FAIL; // 先处理页对齐部分 while (len > 0) { uint16_t pageSize = 32 - (addr % 32); // 本页剩余空间 uint16_t chunk = (len > pageSize) ? pageSize : len; // 发起 I2C 写操作,chunk <= 32 HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, addr, I2C_MEMADD_SIZE_16BIT, buf, chunk, timeout); // 等待写周期结束:轮询 ACK 直到器件不再回应 NACK while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDR, 10, 50) != HAL_OK); addr += chunk; buf += chunk; len -= chunk; } return OK; }写周期等待那一步,网上很多代码直接延时 5ms。实际项目里如果频繁写 EEPROM,比如保存参数时一次写 25 个字段,5ms 乘 25 就是 125ms,用户感知非常明显。更好的做法是使用 ACK 轮询:在写周期结束后我发送一个虚拟的读地址指令,如果器件回应 ACK,说明写周期完成;如果回应 NACK,说明还在内部擦写,继续轮询。这样可以减少不必要的延时。
另一个细节是读操作时的"幽灵读"问题。I2C 从机地址最高位决定是读还是写,AT24C256 的 7 位地址是 0x50 到 0x57(由 A0~A2 引脚决定),最后一位是 R/W 标志。很多新手犯的错是直接写一个 8 位地址字节,把读写标志混进去。正确做法是在 HAL 库的HAL_I2C_Mem_Read里传入 7 位地址和 16 位寄存器地址,库函数会自动补 R/W 位。还有,EEPROM 的上电时序很短,但写入序列要求器件供电稳定,在量产测试里我加过一道"写入后回读校验"逻辑,确认 ADDR 和 DATA 都能准确回读,确保没有把参数写进空气里。
4.2 NOR Flash 的 SPI 编程、擦除与 A/B 分区升级
NOR Flash 的驱动核心是掌握它的指令集。以 W25Q128 为例,常用指令包括:0x06 写使能、0x20 扇区擦除、0x02 页编程、0x03 读数据、0x05 读状态寄存器。所有写操作之前必须先发 0x06 使能 WEL 位,写完再查询状态寄存器直到 BUSY 位清零。这个规矩不能破,否则写指令会被硬件直接忽略。
页编程的"页"通常是 256 字节。和 EEPROM 一样,如果一次传输超过页边界,会在这个页内回卷命中本页开头的空间,覆盖原本数据。所以同样需要拆分写入。扇区擦除是 4KB,块擦除是 64KB,选择哪个粒度取决于要更新的数据规模。升级固件时如果只改一个固件分区,因为固件镜像动辄几百 KB,通常按 64KB 块擦除更高效。
升级这块我要重点说一下 A/B 分区和 magic 字设计。我的方案里,NOR Flash 的地址布局大致如下:
0x000000 - 0x07FFFF A 分区:主要固件(512KB) 0x080000 - 0x0FFFFF B 分区:升级候选固件(512KB) 0x100000 - 0x13FFFF FPGA 镜像 A(256KB) 0x140000 - 0x17FFFF FPGA 镜像 B(256KB) 0x180000 - 0x1FFFFF 字库/表格/预留每个分区头部预留 32 字节存放"分区描述符",包含版本号、CRC32 校验和、镜像有效标志(magic 字)。每次启动时,STM32 在 Bootloader 阶段扫描分区描述符,优先选择有效标志且版本最新的分区,然后把 NOR Flash 里的固件搬运到 SRAM 或外部 RAM 运行。这种双备份结构让升级变成"先写 B,成功后置位 magic,再重启进 B",任何一次掉电升级中断都不会让系统变成砖头。
NOR Flash 的擦除时间很长(扇区擦除典型值 45ms,块擦除 150ms 以上),在擦除期间必须确保芯片的 CS 始终拉低且时钟稳定。我在可靠性测试里故意在擦除期间的任意时刻拉掉电源,结果部分扇区进入了只读半擦状态,后续写进去的数据校验不过。最后的设计是三个分区互相备份,遇到擦坏的分区直接放弃并使用另一个分区,这让我对 NOR Flash 不那么提心吊胆了。另外注意,FPGA 需要的配置文件比较特殊,它要求整段流式加载。我在初始加载序列里对 NOR Flash 的读取做全速 DMA,配合 STM32 的 SPI 主模式,能在一秒内把 2MB 的 bitstream 全量灌给 FPGA,比传统逐字节搬运快了近一个数量级。
4.3 SD 卡的 SDIO 模式接入与 FatFs 文件系统集成
SD 卡接入的主角是 STM32 的 SDIO 外设和 FatFs 中间件。先说硬件初始化。SDIO 外设配置成 4 位宽总线、48MHz 时钟,并使用 DMA 搬运,这比用 SPI 模式快得多。STM32CubeMX 生成的初始化代码里有个常见坑:默认的时钟分频值是 2 分频,在 48MHz 的 APB2 下实际 SDIO_CK 时钟是 24MHz,而很多普通卡此时还稳定,但高容量卡或者旧卡可能会出现通信失败。正确的策略是:初始识别阶段使用低速时钟(400kHz),完成卡片识别并通过CMD6切换电压和总线宽度后再切到高速模式。这个两阶段速度切换,是 STM32 跟 SD 卡稳定握手的基本功。
我踩过最久的坑是"SD 卡内部寄存器锁死"。现象是卡片初始化成功后,执行写操作返回超时,重新插拔后又能恢复。查到最后是 FatFs 写入库文件时在断电或热插拔瞬间破坏了卡内部的状态寄存器。工业现场,操作工常常不耐烦直接把卡从读卡器上拔下来,所以这个问题其实比想象中频繁。解决办法分两层:硬件上,SD 卡座加一个锁卡开关,系统用 GPIO 检测到开关打开时立刻禁止写操作;软件上,FatFs 每次写完文件后立即f_sync(),不依赖系统最终卸载来保证数据落盘。
FatFs 挂载失败的经典问题,多半是文件系统类型不匹配。FatFs 默认支持 FAT12/16/32 和 exFAT(需要打开宏_USE_EXFAT)。如果你拿一张刚格式化 NTFS 的卡,FatFs 直接返回FR_NO_FILESYSTEM。项目里的处理方式是:上电尝试 FatFs 挂载三种常见类型,如果全部失败,就把卡重新格式化(这其实是极其粗暴但有效的现场自愈手段)。另外,一张卡格式化后第一次挂载太慢,多半是因为扇区对齐问题,建议格式化为 4096 字节分配单元,FAT 表位置和簇大小对 FatFs 都叫天然友好。
SD 卡日志写入的另一个关键是缓冲和定时 flush。不要每产生一条日志就f_write打开文件一次,这种操作极伤寿命和速度。我的方案是:日志数据在 RAM 里攒到 4KB 左右,一次性追加写入文件,然后f_sync。这样写的频率降到足够低,卡片的写入放大也最小化。日志文件使用"当日日期.RUN"这样的命名,并在文件头写入起始时间戳。上位机或现场工程师拿到卡后直接从文件名和文件头就能快速定位时间段,省去了大海捞针的烦恼。
5. 常见问题排查与避坑速查表
下面这张表是我这几年在工控项目里积累的典型问题集,都是真金白银的现场事故换来的经验。如果你在调试过程中碰到类似症状,可以按这个顺序排查,大概率能少走弯路。
| 现象 | 可能原因 | 排查步骤与解决思路 |
|---|---|---|
| EEPROM 写入后回读乱码 | 跨页写入未拆分、地址位溢出、写周期等待不足 | 检查写入函数是否按页拆分;确认 16 位地址格式是否正确;写周期改为 ACK 轮询而非固定延时 |
| EEPROM 有时写不进去 | 器件地址被 A0~A2 拨码开关影响、I2C 总线上拉偏弱 | 用示波器观察 SCL/SDA 信号;确认从机地址是否和硬件拨码一致;上拉电阻改为 4.7k 并测量总线电容 |
| NOR Flash 擦除后写入校验失败 | 擦除扇区不完整、写使能未置位、SPI 时钟过快导致时序违例 | 读状态寄存器确认 BUSY 位;先发 0x06 再写;降低 SPI 时钟到 10MHz 做交叉验证 |
| NOR Flash 启动时读取卡住 | 芯片进入 Deep Power-down 模式或 QPI 模式未退出 | 发送 0xAB 唤醒,然后 0x66 + 0x99 切换回标准 SPI 模式;确认 Bootloader 初始化时序正确 |
| SD 卡初始化失败或读到乱数据 | 时钟初始过高、电压切换不匹配、部分卡只支持 SPI | 初始阶段切到 400kHz 低速识别;检查命令 CMD8/ACMD41 响应;用逻辑分析仪抓取命令序列比对 SD 规范 |
| 掉电后文件损坏 | 写入期间掉电、FAT 表未及时同步 | 打开 FatFs 的_FS_MIN_AMT和f_sync;采用双日志文件轮换;增加电源监测在掉电前执行同步 |
| 不同品牌 SD 卡兼容性差 | 各厂商对命令响应时序容忍度不同 | 建立兼容性测试矩阵,覆盖主流品牌;必要时放宽命令超时时间,避免严格 1s 超时误杀慢卡 |
| 混合挂载多个存储设备时系统卡顿 | 多个 DMA 通道争抢总线、中断优先级冲突 | 对 DMA 和中断设置合理优先级,日志写入用低优先级 DMA 或定时任务;避免两个块设备同时启动传输 |
关于 I2C 总线还有一个容易被忽略的现场问题:总线上的从设备如果供电异常,会把 SCL/SDA 拉死,导致整条总线上所有设备都"失踪"。排查方法是逐个断开从设备电源,用示波器看总线释放情况。遇到这种问题,我会在硬件设计上给 EEPROM 加一个与主控一致的上电时序,并在软件初始化时做一次总线恢复序列(比如发送 9 个时钟脉冲释放总线),很多"莫名其妙"的 EEPROM 丢失问题其实就这么化解的。
另外,SD 卡在写入时如果系统刚好发生看门狗复位,复位后的代码如果立刻访问 SD 卡,会再次触发故障,形成循环。我的建议是:看门狗复位后让系统进入一段"存储冷却期",先做 50ms 的电源稳定等待,再尝试挂载文件系统。这一招在现场特别有效,因为很多复位是电源瞬间跌落引起的,马上干活反而又撞上不稳定的电源窗口。
6. 最后的体会和几个容易忽略的小习惯
这套 STM32+FPGA 分级存储方案做下来,最深的感受是:存储设计不只是找个芯片焊上去,更是对设备全生命周期的一次预判。我见过太多项目把存储方案定得很随意,然后靠售后工程师在野外用热风枪换芯片来"补课"。提前把数据类型分清楚,把介质性格摸透,很多问题在设计阶段就被拦住了。
几个小习惯我再啰嗦一遍。第一,所有参数写入 EEPROM 前,先备份当前值,写入完成后回读对比,不一致就把备份写回去,这能让"参数丢失"事故不太可能静默发生。第二,NOR Flash 的每个分区描述符里一定要有 CRC 和 magic 字,别舍不得那几十字节空间,启动时哪怕只多花 0.1 秒的校验,换来的都是升级安全性。第三,SD 卡日志文件名里带日期和序号,并给每个文件写一个 32 字节的文件头,后期做现场溯源能省无数功夫。最后一个提醒,量产前把你打算支持的存储器件列表固化,写入固件版本号里,现场如果换了不同批次介质,至少能快速定位是兼容性差异而不是程序 bug。
如果后面继续做这个系列,我打算专门聊 FPGA 直接驱动存储器的场景,比如数据采集密度太高时让 FPGA 绕过 STM32 直接对 NOR Flash 做突发写入,或者用 FPGA 的并行接口搞定高速 SD 卡协议。总之存储这条线里能挖的东西还很多,先把这三个介质玩明白,博文里写的每一行你都能在板子上跑起来验证一遍,那这套方案就算学到手了。