news 2026/9/28 5:54:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+FPGA工业控制器分级存储方案:EEPROM/NOR Flash/SD卡设计

做过工业控制器的人都知道,最怕的往往不是算法写不出来,而是设备在现场跑着跑着突然掉电,再上电时参数没了、日志坏了、采集的数据只存了一半。我刚做完的一台基于 STM32+FPGA 的工业通信测试终端就踩了个遍,最后沉淀下来一套 EEPROM/NOR Flash/SD 卡的分级存储方案。这篇是硬件篇的第十二篇,我把整个存储架构、选型逻辑和实际调试过程完整梳理了一遍,希望能给正在做类似 STM32 项目、FPGA 项目或边缘网关设备的朋友一些可直接落地的参考。

这套方案解决的痛点很明确:工业控制器里的数据根本不是同一种东西,配置参数、固件镜像、运行日志、波形记录,它们的容量、写入频率、可靠性要求都不一样。指望用一块 Flash 通吃所有场景,最后一定是配置参数先丢、日志先坏、大容量数据无处安放。下面我从需求分级开始讲,再到每类存储介质的具体设计,最后分享调试现场遇到的那些坑。

1. 分级存储的本质:一次掉电,三种数据的命运完全不同

1.1 先把工业控制器里的数据分成三档再谈存储

很多朋友一上来就问“用多大 Flash”,这其实是没想清楚要存什么。我习惯在设计存储方案前,把控制器里的数据按“容量、频率、可靠性”三个维度先做一个分类表,这个动作比选芯片重要得多。

工业控制器里的数据大体可以分三档。第一档是设备配置参数,包括 PID 参数、IP 地址、设备序列号、校准系数、固件版本标识这一类,特点是容量非常小,整个参数区通常不超过 4KB,写入频率极低,可能只在设备出厂校准或者现场调试时写一次,但可靠性要求极高,掉电、静电、强电磁干扰环境下都不能丢,丢了设备就得返厂。

第二档是固件程序和运行日志。固件程序包括 STM32 的应用程序和 FPGA 的 bitstream 配置文件,容量从几百 KB 到几十 MB 不等。运行日志也不可小觑,一个控制器一天跑下来,事件记录、报警记录、操作记录轻轻松松就是几百 KB。这一档数据的特点是中等容量、中低写入频率,但必须支持反复擦写和可靠的掉电保护。

第三档是采集数据,比如振动波形、电流波形、温度曲线、高速传感器数据。这类数据容量最大,动辄几百 MB 甚至几个 GB,而且往往需要持续高速写入。可靠性要求相对前两档可以适当放宽,因为数据本身带有时间戳和序号,丢一小段不会导致系统崩溃,但不能长期无法写入。

1.2 为什么“一块 Flash 打天下”在工业现场行不通

我在早期设计里曾经想过,用一颗大容量 NOR Flash 把这三种数据都装进去,一个 16MB 的 W25Q128 也才一两块钱,看上去性价比极高。但深入一想就能发现三个致命问题。

第一个问题是擦写寿命的错配。通用 NOR Flash 的擦写次数通常在 10 万次左右,而设备配置参数的写入虽然频率低,却可能在运行中因为远程修改参数而频繁触发。假设一天写 20 次,10 万次只能撑不到 14 年,看起来够用。但问题在于 NOR Flash 最小的擦除单位是 4KB 扇区,参数每次更新都要先把整个扇区读出来、改掉其中几个字节、再整体擦除写回,这种“读改写”操作对扇区的磨损是加倍的。而且如果代码里没有做磨损均衡,每次都往同一个扇区写,这个扇区会提前报废,表现出来就是设备运行几年后参数突然写不进去了。

第二个问题是 NOR Flash 的写放大。EEPROM 支持按字节写入,改一个字节就写一个字节,而 NOR Flash 必须先擦后写,哪怕只改一个字节也得擦掉 4KB 的扇区。这种特性决定了它适合存固件这类“一次性整体写入”的数据,不适合存频繁变化的小参数。

第三个问题是掉电窗口。NOR Flash 在扇区擦除过程中如果掉电,这个扇区的数据完整性基本无法保证,可能变成 0xFF 也可能变成随机值。配置参数一旦写到这种介质上,掉电损坏的概率会大幅度上升。

所以分级不是堆料,而是让每类数据找到最匹配的介质:EEPROM 负责小容量高可靠参数,NOR Flash 负责中等容量固件和日志,SD 卡负责大容量采集数据。三者互相配合,才能覆盖工业现场各种极端情况。

1.3 三级存储的整体拓扑与数据流

这套方案在实际板卡上的连接关系大概是这样的:一颗 I2C 接口的 EEPROM 挂在 STM32 的 I2C1 总线上,负责参数存储;一颗 SPI NOR Flash 挂在 STM32 的 SPI1 总线上,负责固件和日志;SD 卡则使用 STM32 的 SDIO 接口,负责大容量采集数据。FPGA 并不直接连这三颗存储芯片,而是通过 SPI 从机接口或者并行 FIFO 与 STM32 交互,FPGA 的高速采集数据经过预处理后交给 STM32,由 STM32 统一决定落到哪一级存储。

这里有个关键点:FPGA 负责高频数据采集方向采样和预处理,比如 LVDS 接收传感器数据、SPI ADC 采集波形、UART 与外部设备通信,但它不做存储管理。存储管理的职责全部收拢到 STM32 侧,因为 STM32 上跑文件系统、做坏块管理、处理掉电保护都要比 FPGA 灵活得多。FPGA 需要存储服务时,通过一个简单的请求应答协议向 STM32 发起,而不是自己去操作 Flash 芯片。这样的设计从源头上避免了总线冲突,也让我在调试存储问题时只需要盯着一颗主控芯片的逻辑。

数据流大致是:传感器或总线数据先进 FPGA,经过降采样、滤波、格式整理后,通过并行接口或者高速 SPI 送给 STM32,STM32 根据数据类型决定是写 EEPROM、NOR Flash 还是 SD 卡。整套数据流的仲裁和调度逻辑,我放在后面第五章详细说。

2. EEPROM:关键参数的最后防线

2.1 选型要点:容量、页写、擦写寿命一个都不能少

EEPROM 的选型其实很有讲究。工业控制器里最常用的型号是 Microchip 的 AT24C256 或者 ST 的 M24128,容量从 128Kbit 到 512Kbit 不等。我个人的习惯是至少选 256Kbit,也就是 32KB。为什么不用更小的 8KB?因为参数区除了存储用户数据,还要留出双备份区、CRC 校验区、版本号区和磨损均衡的轮换空间,32KB 用起来才从容。

除了容量,还有三个参数必须关注。第一个是页写大小,AT24C256 的页写是 64 字节,一次最多连续写 64 字节,超过了就会回卷覆盖页首,这是个非常经典的坑。第二个是写周期时间 tWR,典型值是 5ms,也就是说每次发起写操作后必须等待 5ms 才能进行下一次操作,很多新手在这里翻车。第三个是擦写寿命,工业级 EEPROM 通常标称 100 万次,比 NOR Flash 的 10 万次高了一个数量级,这也是参数类数据选择 EEPROM 的根本原因。

这里要特别提醒温度范围。工业控制器必须选工业级型号,工作温度范围是 -40℃ 到 +85℃,消费级的是 0℃ 到 +70℃。价格差不了几毛钱,但在户外机柜、高温车间这种环境里,消费级芯片的参数漂移和寿命衰减会非常明显。我见过一台设备因为在高温环境使用消费级 EEPROM,运行半年后配置参数出现随机翻转,查了很久才发现是温度导致存储单元电荷泄漏。

2.2 STM32 侧 I2C 读写 EEPROM 的工程细节

STM32 读写 EEPROM 的关键不是 I2C 协议本身,而是把它封装成可靠的参数存取服务。先简单说时钟树配置:I2C1 挂在 APB1 总线上,我的板子上 APB1 时钟是 42MHz,I2C 时钟配置为 400kHz 快速模式,分频值要根据实际的 APB1 时钟计算。用 STM32CubeMX 配置时直接选 Fast Mode 并填 400000,它会自动算出时序参数,这个不用手算,但要注意 I2C 引脚必须配置为开漏输出并外接上拉电阻,典型值是 4.7kΩ。

实际读写操作我推荐用 HAL 库的HAL_I2C_Mem_Write和HAL_I2C_Mem_Read,这两个函数的第三个参数就是 EEPROM 内部的字节地址,直接绕过了手动组装地址帧的麻烦。但使用中有两个细节必须处理。

第一个细节是必须检查 EEPROM 的写周期。发出写命令后,EEPROM 进入内部写周期,此时它不会响应任何 I2C 请求。正确的做法是发送一个无效的起始条件来探测设备是否忙,如果 EEPROM 返回 ACK 说明写完了,返回 NACK 说明还在忙,要重试。HAL 库函数本身就内置了这种机制,但要注意将I2C_TIMEOUT设得足够长,我一般设为 100ms,而不是默认的 25ms。

第二个细节是页写边界。如果参数结构体长度超过了页写大小,或者跨越了页边界,就必须手动拆分。我的做法是写一个通用的参数写入函数,入参是参数 ID、数据指针、数据长度,函数内部按页边界自动拆分,每写完一页等待 tWR 后再写下一页。

提示:EEPROM 的 I2C 地址不是固定的。AT24C256 的地址引脚 A2、A1、A0 决定器件地址的低三位,默认全接地时是 0x50。如果你的板子上挂了多颗 EEPROM 或者其他 I2C 设备,务必核对地址不冲突。

2.3 掉电保护的 CRC 与冗余存储设计

EEPROM 本身的可靠性不低,但工业现场从来不缺意外。设备正在写参数的瞬间被打雷、被误操作、被远端断电,写了一半的 EEPROM 里可能是一份半新半旧的数据。要应对这种情况,单靠 EEPROM 本身是不够的,必须在软件层做冗余设计。

我目前的参数存储格式是这样的:EEPROM 里划分两个参数区,每区头部是 4 字节的魔法数字(比如 0xA5A5A5A5)、4 字节的版本号递增计数器、2 字节的参数长度、1 字节的参数 CRC8,然后是实际参数数据。写入时先写区 A,写完并校验通过后更新区 B,保证任何时刻至少有一个区是完整的。读取时优先读版本号较高且 CRC 校验通过的区域,如果两区都损坏,才回退到出厂默认参数。

这种设计能覆盖绝大多数掉电场景。假设正在写区 A 时掉电,区 A 的数据可能是坏的,但区 B 还是完整的上一次参数,上电后自然是区 B 生效。等设备恢复工作,下一次写参数时先写区 A,把区 A 修复,再写区 B,两个区重新对齐。整个过程对用户完全透明,参数永远不会因为一次掉电变成“丢失”状态。

磨损均衡在这个设计里也顺带解决了。两个区轮流写,每次参数更新只磨损其中一个区,相比只用一个区,寿命直接翻倍。对于一天写几十次的参数场景,这个设计能让 EEPROM 轻松撑过设备全生命周期。

2.4 FPGA 为什么也要读 EEPROM:Verilog I2C 控制器要点

这套架构里有一个容易被忽视的场景:FPGA 上电后需要尽快拿到一组校准系数才能开始正常采集工作。如果校准系数存在 EEPROM 里,而 FPGA 必须等 STM32 先把参数读出来再通过总线转发给它,这里就有两个问题:一是启动时序变长,二是形成了不必要的耦合。

所以我在 FPGA 侧也预留了一个轻量级 I2C 主机控制器,用来直接读取 EEPROM 中的校准参数区。这个控制器的实现并不复杂,I2C 协议的本质就是一根时钟线 SCL 加一根数据线 SDA 上的状态机跳转。用 Verilog 实现时,核心状态就几个:START 起始、发送器件地址+读位、发送内部字节地址、重复起始、读数据、发送 STOP。

这里只提醒最关键的一点:I2C 的 SDA 在 FPGA 里必须是 inout 类型,并且发送数据时要按位打拍,接收数据时要在 SCL 高电平中间采样。很多初学者会把 SDA 写成单纯的 output,结果就是读回来的数据永远不对。另外强烈建议用现成的开源 I2C 控制器或者厂商 IP,比如 Microchip 的 CoreI2C,不要自己从头写。I2C 时序在高速 400kHz 下对建立时间和保持时间的要求非常细,自己写的控制器往往在实验室正常、在现场高温或长走线条件下就罢工了。

如果只是需求读取 EEPROM,还可以考虑让 STM32 在启动时把校准参数通过 SPI 配置到 FPGA 的寄存器里,这样 FPGA 根本不需要碰 I2C,复杂度最低。只有当 FPGA 需要独立于 STM32 自主读取参数时,才值得在 FPGA 里实现 I2C 主机。

3. NOR Flash:固件和日志的可靠阵地

3.1 为什么是 NOR Flash 而不是 NAND 或 eMMC

接下来是第二档数据,固件镜像和运行日志。这里我选择了 SPI NOR Flash,典型型号是 Winbond 的 W25Q128JVSIQ,16MB 容量,工业级。其实很多人在这个环节会纠结:为什么不用 NAND Flash?不是容量更大更便宜吗?这就涉及工业场景的实际取舍了。

NOR Flash 最大的优势是可靠性。它没有坏块概念,不需要 ECC 校验,随机读取速度极快,支持 XIP(片上执行)。这些特性决定了它非常适合存固件:STM32 上电后可以直接从 NOR Flash 里把 App 代码搬到 RAM 执行,或者某些支持 XIP 的芯片可以直接在 Flash 上跑代码。固件在传输过程中哪怕错一个 bit,在 NOR Flash 上是可察觉的、可以靠 CRC 兜底的,但在 NAND Flash 上,一个 bit 的错误可能直接判读成一个坏块,而且坏块管理逻辑本身就需要相当多的软件资源。

NAND Flash 的优势在大容量和成本,但它需要坏块管理和 ECC,而且在写入时对掉电非常敏感,容易出现数据错乱。在工业控制器这种每台设备都要稳定运行五到十年的场景里,我不会把 NAND 作为主存储,只会在需要超大容量的边缘网关里考虑 eMMC。eMMC 内部已经做了坏块管理和磨损均衡,可以当作一个黑盒使用,但成本也上去了,而且 STM32 的 SDIO 接口驱动 eMMC 芯片时还需要处理启动时序,比驱动 SD 卡复杂不少。

所以在 16MB 到 64MB 这个容量区间,SPI NOR Flash 是最平衡的选择。一颗 W25Q128 成本不到两块钱,不用考虑坏块和 ECC,SPI 接口又对 STM32 极其友好,基本是零门槛接入。

3.2 NOR Flash 的分区规划:A/B 双固件和日志区隔离

选中 NOR Flash 之后,第一件事不是写驱动,而是画分区表。在这个 16MB 的 Flash 里,我划分成了四个区域,顺序和大小如下表所示:

区域起始地址大小内容
Bootloader0x000000256KBSTM32 引导程序,出厂固化
App A0x0400004MBSTM32 应用程序主副本
App B0x4400004MBSTM32 应用程序备份副本
Bitstream0x8400004MBFPGA 配置文件及备份
日志区0xC400003.5MB运行日志、事件记录

A/B 分区是固件升级的安全保障。升级时把新固件写入当前不活动的 App 区,全部写完并通过 CRC 校验后,把启动标志位从 EEPROM 里更新为指向新区,然后复位执行。如果新固件启动失败,Bootloader 检测到启动标志超时,自动回滚到旧区。有了 A/B 双区,远程固件升级失败再也回不去把设备变成砖头的风险。

日志区单独划出来,不跟固件区混在一起,因为日志需要频繁擦写,和固件区的“一次写入不变”是不同的磨损模式。如果混在一个大分区里,日志频繁擦写会提前耗尽相邻扇区的寿命,影响固件的完整性。

3.3 SPI NOR Flash 的写入策略:擦写循环、忙等待与掉电窗口

SPI NOR Flash 的操作逻辑不复杂,但工程细节非常多。W25Q128 的标准操作是:写使能(06h)、扇区擦除(20h)、页编程(02h)、读状态寄存器(05h)。每次写操作前必须先写使能,写完或擦完后轮询状态寄存器的 BUSY 位,直到变为 0 才能进行下一次操作。

这里有一个我在项目早期踩过的坑:很多人会在发送擦除指令后直接用HAL_Delay(100)这种死等方式延时等待。Flash 擦除时间典型值是 50ms 到 400ms,死等虽然简单,但在高速数据采集系统中,这段时间 CPU 完全被占用,中断都被延后处理,会导致数据采集丢帧或者与其他任务产生时序冲突。正确的做法是把 Flash 操作封装成异步任务,发起擦写后立即返回,在定时器回调里轮询 BUSY 状态。STM32 的 SPI 是全双工,轮询 BUSY 时可以用HAL_SPI_Receive接收状态寄存器值,判断 bit0 是否为 1。

掉电窗口问题则更隐蔽。Flash 正在擦写期间掉电,擦除结果是不确定的,可能是全 0xFF,也可能是部分数据损坏。如果正在写日志扇区,最多丢一段日志;但如果正在写固件扇区,整个分区可能全废。为了避免最坏情况,A/B 升级流程里会要求设备处于稳定的供电状态下才允许升级操作,升级期间提高看门狗超时时间,并且每次擦写前把操作类型和扇区号记录到 EEPROM 中。这样即使升级中掉电,上电后 Bootloader 也能从 EEPROM 里读出“正在升级”的状态,发现当前区未完成,自动回滚到另一个完整区。

3.4 FPGA bitstream 的加载方式:谁的活谁来干

NOR Flash 里存的 FPGA bitstream 有一个加载路径的问题需要提前想清楚。常见的方式有两种:第一种是 STM32 上电后通过 FPGA 的配置接口(比如 SelectMAP 或 JTAG)把 bitstream 推给 FPGA;第二种是 FPGA 在上电时主动从 SPI NOR Flash 里读取配置数据,这要求 FPGA 支持配置器件接口,且 Flash 要挂在 FPGA 的专用配置引脚上。

我在这个项目里选择的方式是 STM32 主动配置。原因很简单:bitstream 要通过远程升级来更新,而远程升级的数据源一定在 STM32 侧(要经过网络或者通信总线),让 STM32 把新 bitstream 写入 NOR Flash,再由它通过 SelectMAP 接口加载到 FPGA,整个链路最顺。FPGA 不直接接触 NOR Flash,它只是等待 STM32 的 DONE 信号,配置完成后进入正常工作状态。这样也顺便避开了两个主控同时访问 Flash 的总线仲裁问题。

如果你的 FPGA 必须从 Flash 自加载,那就需要给 FPGA 单独接一颗配置 Flash,或者用带 dual-image 功能的器件把加载和升级的时序分开。不过在我的架构下,STM32 统一管理的方案已经足够可靠,而且调试起来非常方便——修改 bitstream 后只需通过串口下发到 STM32,STM32 写入 Flash 并触发重配置。

4. SD 卡:当数据量到了几百 MB 的级别

4.1 SD 卡不是万能的,该用的时候才用

到了第三档数据,硬盘级的容量需求。以我们这台通信测试终端为例,它需要连续记录对端设备发送来的原始数据帧,按每小时约 200MB 计算,一个 32GB 的 SD 卡可以存一个星期的数据。这个量级如果用 NOR Flash,就算用 64MB 也撑不了几小时,成本还不现实。所以大容量采集数据必须走 SD 卡。

但我必须强调一个反直觉的结论:SD 卡不适合频繁少量写入,反而适合大块连续写入。SD 卡的内部结构决定了它的最小写入单位是扇区,通常是 512 字节,如果每次只写几十字节,SD 卡控制器要频繁进行读改写操作,速度会掉到惨不忍睹,而且对闪存寿命的损耗极大。工业现场的数据往往是一串波形或一段报文,天然适合按大到几 KB 甚至几十 KB 的块来写入,这就和 SD 卡的工作方式非常匹配。

所以在选型上,规规矩矩用 SD 卡之前,要先问一个问题:我的数据是高频小包还是低频大包?如果是高频小包,应该先在 STM32 的 RAM 里拼装缓冲,攒够至少一个扇区再落卡。如果只是偶尔存个配置导出文件,那用 SD 卡纯属杀鸡用牛刀。

4.2 SPI 模式还是 SDIO 模式:接口选择的道道

STM32 驱动 SD 卡有两条路:SPI 模式和 SDIO(SDMMC)模式。这个选择直接影响吞吐上限和代码复杂度。

SPI 模式的优势是代码简单、引脚少,标准 SPI 库直接能用,不用去配置复杂的 SDIO 协议状态机。但速度上限受限于 SPI 时钟,通常只能到 10MHz 到 20MHz 左右,而且它只能做单线传输,实际吞吐能有 2MB/s 就顶天了。如果你的数据采集带宽比较低,比如每秒只有几 KB 到几十 KB 的日志,SPI 模式完全够用,而且调试起来极为方便,逻辑分析仪一挂就能看到完整时序。

SDIO 模式才是大带宽的正确选择。STM32F4 系列及以上都内置了 SDMMC 外设,支持 4bit 并行传输,时钟可以跑到 48MHz 甚至更高,配合 DMA 可以轻松达到 10MB/s 以上的实际写入速度。代价是初始化代码和协议处理要复杂得多,要处理 SD 卡的 R1/R3/R7 响应、CMD 命令编码、数据线方向切换、DMA 缓冲对齐等一堆细节。

我的建议很直接:采样率超过 1MHz 的连续波形记录,老老实实用 SDIO 4bit + DMA。只做低频日志记录,SPI 模式省事。不要想着 SPII 模式还能通过超频撑过去,写入速度不够时数据丢失的锅最终还是你来背。

4.3 文件系统还是裸扇区:这个决定必须提前做

SD 卡的另一个大话题是存储格式。工业控制器里不外乎两种选择:挂 FATFS 文件系统,或者直接按裸扇区记录。

挂文件系统的好处是人人都能理解,数据导出来到 PC 上直接双击能看。FATFS 在 STM32 上跑得非常成熟,四个钩子函数对接 SDIO 底层驱动就能草草跑起来。但致命弱点是掉电保护。FATFS 的文件操作涉及文件分配表(FAT)、文件目录项和数据区三部分,任何一部分在写入过程中掉电都可能导致整个文件系统错乱。设备现场隔三岔五掉一次电,用 FATFS 连续写了几个星期数据,突然某次掉电后整个卡打不开了,这种问题在工业现场是致命的。

我的折中方案是把日志记录分成两个场景。用于快速实时记录的日志区,使用裸扇区的自定义格式:预先在格式化时把所有扇区划分成固定大小的记录块,每个块头部写有块序号、时间戳、CRC 校验。在写入时完全绕过 FATFS 的文件管理,直接按块号连续写,写完一个块更新一个“当前块索引”的头部扇区。掉电后上电,直接从头部扇区读出块索引,跳过半块检查 CRC,找到最后完整写入的位置继续写。这种方案的掉电鲁棒性远高于 FATFS,而且写入速度快得多。

第二种场景是人为操作的数据导出或调试文件,比如导出配置、转存某段波形,这种情况对掉电的容忍度可以稍高一些,用户手动操作时通常会保证供电稳定,所以这些文件可以用 FATFS 管理,保持文件格式的通用性。

两种场景共存在一张卡上,分区表分开。FATFS 管理一个标准 FAT32 分区,裸扇区记录一个自定义分区。这样既保证了实时记录不掉链子,又保留了对外的数据便捷性。

4.4 FATFS 掉电安全的细节处理

既然提到了 FATFS,我把实际操作中积累的几个关键细节一起说。

第一,f_write之后必须主动调用f_sync,而不是等到f_close。f_close只有在文件正常关闭时才刷新缓存,如果掉电发生在 close 之前,文件缓冲区的数据可能全部丢失。工业控制器里很多日志文件是长期打开、持续写入的,不可能频繁 open/close,所以每次写完一批数据后立刻 f_sync 是必须的。我实际测下来,f_sync 的开销大约几百微秒到几毫秒,对于每秒写一次日志的场景完全可接受。

第二,底层 diskio 层的写函数要认真处理扇区对齐。SD 卡在 SPI 模式下最小单位是 512 字节,FATFS 默认扇区大小也是 512,但 DMA 缓冲区和数据数组的地址必须按 4 字节对齐,否则在 F4/H7 系列上 DMA 会直接报错。

第三,SD 卡安装时必须处理“内部寄存器锁死”这种奇葩问题。有些劣质 SD 卡在异常掉电后内部的写保护寄存器和状态寄存器会卡死在错误状态,上电后一直返回 BUSY,导致系统在初始化阶段就卡死。我的处理方式是在 mount 失败时执行一次 CMD0 软复位并重试三次,如果在固件升级模式中卡死,还涉及强制重新初始化。这个问题的具体表现和排查过程,我在后文实测部分会提到。

4.5 SD 卡镜像预制作带来的稳定性提升

项目中后期我引入了一个很好用的做法:在 PC 上预先制作 SD 卡镜像,然后批量烧录,而不是让每台设备在首次上电时自己格式化。

具体流程是:在一台完整的 Linux 工控机上,用dd或专门的镜像工具创建一个 32GB 的 SD 卡镜像,里面预先写入一个干净的 FAT32 文件系统、参数配置文件、日志索引文件,甚至可以把裸扇区日志区的头部索引清零。对镜像做完质量检查后,再用写卡工具批量写入每张 SD 卡。设备第一次上电时只需检查头部识别字符串,确认镜像版本正确就直接进入工作状态。

这样做的好处是避免了在设备上跑格式化逻辑的一致性风险,也杜绝了某些劣质卡在首次格式化时出现坏块导致文件系统不完整的问题。调试阶段可以随时修改镜像的具体内容,比如往里面预置一些测试用的配置文件,极大提升了对板调试的效率。这个手法在批量生产的项目里几乎是必须的,一台一台现场格式化既不现实也容易出错。

5. 分层控制与总线仲裁:STM32 和 FPGA 谁说了算

5.1 为什么不能两边同时怼到同一颗 Flash 上

把三类存储介质都定下来之后,还有一个架构层面的问题要处理:STM32 和 FPGA 之间,存储访问权怎么分配。我见过不少板子的设计是 FPGA 也直接接 NOR Flash、也从 EEPROM 读参数、SD 卡还可能从两个处理器引出两路信号,这种设计在静态分析时好像很灵活,但实际跑起来全是坑。

SPI NOR Flash 是单主设备协议,它不原生支持多主机访问。如果 FPGA 和 STM32 同时向 Flash 发起读操作,两条 SPI 片选信号同时拉低,Flash 的数据线会被两个主机同时驱动,轻则读回的数据是乱的,重则把芯片驱动冲突烧掉。即便你用模拟开关做总线切换,也存在切换间隙数据不确定的问题。

更关键的是逻辑层面的冲突。STM32 正在写日志写到一半,FPGA 如果同时发起读 Flash 的请求,Flash 的写操作和读操作在内部是冲突的,读出的数据完全不可信,而且会让正在进行的页编程产生不确定结果。存储管理的核心原则是单点控制、串行访问,而不是多头管理。

5.2 我的仲裁方案:存储全归 STM32,FPGA 用请求应答

所以我最终的方案是:所有存储芯片物理上只连接 STM32,FPGA 不直接连接任何一颗存储芯片。FPGA 需要存储服务时,通过一个简单的寄存器接口向 STM32 发起请求,STM32 在空闲时依次处理。

具体的交互协议是这样:FPGA 内部有一个 16bit 的任务请求寄存器和一个 16bit 的任务状态寄存器。当 FPGA 需要写一段数据到日志区时,它把“写日志请求”的命令字和数据长度写入请求寄存器,然后将数据通过并口 FIFO 推给 STM32。STM32 在定时器轮询中发现请求寄存器变化,读取命令,处理数据写入,完成后把状态寄存器置为“完成”并返回结果码。整个过程是一个同步握手,避免了任何总线竞争的可能。

时序上也要优先保证实时采集任务。FPGA 的高速采集数据通过 DMA 写入 STM32 的 RAM 双缓冲,采集 DMA 的优先级高于存储任务。存储任务放在后台线程,通过消息队列接收待写入数据块。这样采集不会因为一次长时间的 SD 卡写操作而丢数据。

5.3 一张表理清存储访问权与优先级

最终这套分级存储架构可以用一张表格完整表达:

存储介质接口访问方典型数据写入触发方式优先级
EEPROMI2C1STM32 主 / FPGA 备配置参数、校准系数、启动标志参数变更、启动恢复中
NOR FlashSPI1仅 STM32固件 A/B 区、FPGA bitstream、运行日志固件升级、定时日志低
SD 卡SDMMC仅 STM32采集波形、原始报文、导出文件DMA 累积触发批量写入高

这张表基本就是整个硬件篇第十二篇的核心图纸。你能看到每一类数据都对应唯一的主访问方,而 FPGA 的存储需求全部通过 STM32 间接满足。这套结构牺牲了一点点“并发访问”的灵活性,但换来了极高的可靠性,在工业控制器里,可靠性远比峰值性能重要。

6. 实测中的踩坑记录:从掉电丢参数到 SD 卡锁死

6.1 EEPROM 参数丢失的根因排查:不是芯片不行,是写入流程有漏洞

第一块绊脚石出现在参数写入上。设备在实验室跑了一个星期都正常,但送到现场后开始偶发性出现“参数复位”现象。用户反馈说改完一个参数,第二天上电后参数又变回旧值。第一次怀疑是 EEPROM 质量问题,换了三颗芯片无果。

后来用调试器挂上监控,发现“参数复位”的触发条件很微妙:设备在修改参数后立刻重启。而我们的写入函数在HAL_I2C_Mem_Write返回后没有等待够 tWR,紧接着就去读取校验,这时读到的还是旧值,校验失败后直接走了“参数区损坏”的恢复逻辑,把出厂默认值写回去了。

修复方式就是前面 2.2 节说的:写入完成后必须轮询设备 ACK,确认内部写周期结束,再执行读取校验。而且校验不能只比对一次,要连续读两遍比对一致才判定成功。仔细想想,这个问题本质上不是 EEPROM 的问题,而是我对底层时序的理解不够,把 HAL 库返回的“发送完成”误当成了“写入完成”。

6.2 NOR Flash 日志损坏:扇区擦除与掉电窗口的博弈

第二个坑出在日志写入上。设备连续运行几天后,某次掉电再上电,发现最近两小时的日志文件打不开了。检查 Flash 内容发现,日志区最后一个扇区的数据一半是 0xFF 一半是正常数据,明显是擦除过程中掉电导致的结果。

当时的第一反应是增加写入频率的冗余,把每个日志扇区写完后立刻做双份备份。但这个方案被我自己否掉了,因为日志量太大,双份写入意味着 Flash 磨损翻倍,寿命反而下降。

最终采用的方案是自定义日志格式,不再依赖 FATFS 管理日志区。每个日志块头部写了“魔数+块序号+时间戳+CRC”,每次启动时从头部指针向前扫描,发现最后一个块的 CRC 不合法就跳过整个块,从上一个完整块的末尾继续写。这样最多丢失一个不满的日志块,而绝不污染整体日志结构。代价是日志区不再能通过 PC 直接读取,需要配套一个小工具导出解析,但这个代价换来的是极高的掉电鲁棒性。

6.3 SD 卡写入速度与实时性的平衡

第三个典型的坑是 SD 卡写入速度远低于理论值。SDIO 模式配置好了,理论吞吐应该在 10MB/s 以上,但实测连续写只有 1.5MB/s。排查发现根因是每次f_write只写了 512 字节,而 FATFS 每次写入都要更新 FAT 表和目录项,这三个动作叠加导致写放大和寻道开销巨大。

优化方法是调整内存缓冲和写入块大小。我在 RAM 里开了一个 8KB 的环形缓冲,每当采集数据累积到 8KB 时,一次性f_write出去,然后调用f_sync。实测写入速度从 1.5MB/s 拉升到了 6MB/s 以上,基本满足了波形记录的需求。8KB 这个值是在 DMA 缓冲大小和掉电丢失窗口之间折中的结果,如果掉电,最多丢失最后 8KB 数据,约合 15ms 的波形,完全可接受。

6.4 SD 卡内部寄存器锁死:等于设备挂了

最后记录一个最让现场工程师抓狂的问题:SD 卡内部寄存器锁死。设备在现场运行一段时间后,突然所有涉及 SD 卡的写操作全部卡死,我一直等到看门狗复位才恢复。重新初始化后卡能挂载,但写入卡死的周期内数据全部丢失。

仔细查代码才发现,卡死的原因是在 SDIO 初始化过程中,CMD8命令的响应处理有一个兼容性问题。某些低速或劣质卡在响应CMD8时延迟极长,超过了我的超时时间,驱动直接标记为“卡无效”并进入错误状态。更离谱的是个别卡在长期异常掉电后,内部的写保护寄存器被置位,就算我重新下 CMD0 复位也解不开,必须物理拔卡再插卡。

应对策略分两层。第一层是代码层面,把所有的 SD 命令超时时间放宽,从 100ms 改到 1s,并在初始化失败时做最多三次完整复位重试。第二层是硬件层面,在 SD 卡座的电源引脚上增加一个可控 MOS 开关,当驱动检测到卡死状态时,通过 GPIO 切断 SD 卡电源 100ms 再上电,强制让卡内部完全复位。这套方案实际使用后再没有出现过固件卡死的现象。

注意:实际的 SD 卡电源切断电路需要谨慎设计,不能在上电过程中产生电压毛刺,一般建议用负载开关芯片而不是直接开关 MOS 管,并且在驱动代码里保证切断电源前 SDIO 控制器已经完全释放总线。

7. 最终整理:这套方案在项目里长什么样

如果把这套分级存储方案整体拆开看,它其实是一个典型的 ARM+FPGA 边缘网关的存储模型。EEPROM 守住最基础的配置和启动标志,NOR Flash 负责固件和日志的中转稳定,SD 卡承接大容量采集数据。每级存储都有明确的职责边界、访问方和掉电保护策略,互不干扰又互相补充。

这套架构在调试时给我节省了大量时间。固件升级失败可以无脑回滚,参数写入异常可以双区自愈,日志掉电损坏最多丢最后一段,SD 卡问题也定位到了驱动和电源层面。对一个要在工业现场稳定跑五年的设备来说,这几点比任何炫酷的技术特性都重要。

最后再分享一个基于个人经验的判断:存储方案的设计重点不是“怎么把数据写进去”,而是“写失败时怎么保证系统还能用”。把最坏情况想清楚,把掉电、坏块、卡死、写入中断这些场景全部做进设计里,这个存储架构才算真正立住了。如果你正在做 STM32 项目或 FPGA 项目的存储部分,建议先别急着选芯片,先把数据分档、把掉电时序画出来,再动手写驱动,这样能少走很多弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 5:54:42

微信小程序旅游服务后端:Java Spring Boot与数据库设计实战解析

简介:这份资源是基于微信小程序的旅游服务软件完整项目,后端采用Java与SSM框架开发,配套数据库脚本,面向计算机相关专业毕业设计或初学小程序全栈开发的读者。项目可在IntelliJ IDEA及微信开发者工具中直接导入运行,数…

作者头像 李华
网站建设 2026/9/28 5:54:41

STM32嵌入式设备实现阿拉伯语LCD动态显示的完整方案

1. 项目拆解:阿拉伯语显示到底难在哪里1.1 阿拉伯语书写的三个“反常识”规则如果你在嵌入式设备上显示过中文,大概率会觉得“显示阿拉伯语不就是再做一套字库嘛”。真做起来你会发现,事情远没有这么简单。阿拉伯语有三个完全不同于英文和中文…

作者头像 李华
网站建设 2026/9/28 5:54:34

响应式网站技术新手入门:3个档位报价单拆解,避开改需求拖一周的坑

响应式网站技术新手入门:3个档位报价单拆解,避开改需求拖一周的坑 上周刚跟一家做建材出口的公司谈完,对方老板拿着另一家公司的报价单,指着上面那行“响应式适配费:3000元”问我:“为什么我明明只要改个手机端按钮颜色,对方说技术架构要重构,拖了一周还没动静?这钱到底花在哪了?”…

作者头像 李华
网站建设 2026/9/28 5:54:28

5年老兵揭秘:推广策划书模板从零搭建避坑指南

5年老兵揭秘:推广策划书模板从零搭建避坑指南 域名服务器搞不懂?别慌,这行十年,我见过太多老板被这一关卡住。 其实核心就两点:服务器买错配置,域名没绑解析。 今天把推广策划书模板从零搭建的账算清楚,让你心里有底。 方案类型与适用场景…

作者头像 李华
网站建设 2026/9/28 5:54:21

Keyboard Shortcuts 配置 TaoToken:settings.json 骨架与验证动作

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 5:54:23

网站怎样做301跳转图解步骤避坑指南

网站怎样做301跳转图解步骤避坑指南 找建站公司怕被坑高价?别慌,其实很多“高深”的技术问题,拆开看就是几行代码的事。比如 网站怎样做301跳转 ,这不仅是SEO的基础,更是保护你品牌权重的关键动作。很多小白一听到“301”就头大,觉得需要找专家花大价钱处理,结果交出去一堆费用,最后发现就是改个配置…

作者头像 李华