☰
STM32+FPGA工业控制器数据分级存储方案:EEPROM/NOR/SD卡实战
2026/10/1 1:44:02 网站建设 项目流程

做工业控制器的朋友,大概率都纠结过一个问题:数据到底该放哪儿?尤其是STM32+FPGA这种异构方案,参数、日志、波形、固件全挤在一起,选错存储介质,轻则卡顿丢数据,重则现场返工。这套分级存储方案,核心就一句话——按数据的“性格”分家:高频少量走EEPROM,中等容量带擦写管理走NOR Flash,海量采集走SD卡。这篇硬件篇第12回,我把STM32和FPGA两侧的职责划分、介质选型、掉电处理、以及实际调试中踩过的坑一次性说透。

这套方案的适用对象很明确:正在做运动控制、仪器仪表、边缘网关,或者任何需要“参数不丢、日志可查、波形能回放”的嵌入式产品的工程师。不管是刚入门还是已经调过几块板子的,都能在这里找到可以直接抄的落地细节。

1. 先想清楚一件事:这三类数据不能塞进同一个篮子

1.1 工业控制器里到底躺着哪几类数据

我拆过不少产品的存储需求,最终都会落到以下五类:

  • 配置参数:IP地址、从站号、PID系数、校准值。特点是单条体积小,可能就几十字节,但改动频率低,一年可能改不了几次,不过一旦丢就出大事。
  • 运行日志:报警记录、开关机时间、操作记录。每天几百条,每条几十字节,需要长期保存,最好能导出分析。
  • 工艺/采样数据:振动波形、编码器位置、温度曲线、高速ADC采集。这类最麻烦,量可以冲到每秒几MB甚至几十MB,而且不能丢,丢了波形就没法回放分析。
  • 固件镜像:Bootloader、应用程序、FPGA配置文件。体积从几十KB到几MB不等,要支持升级,但升级频率极低。
  • 关键状态快照:掉电瞬间的当前坐标、当前配方、正在执行的工序号。体积很小,但写入时机极端,必须在掉电前的几毫秒内完成。

这五类数据的写入频率、单次体积、可靠性要求完全不同,根本没可能用同一种介质搞定。我见过一些项目把配置参数和采样波形都塞进一张SD卡,结果SD卡疯狂磨损,几个月就坏。也见过把波形数据往EEPROM里塞的,容量根本不够,数据刚采几秒就写满了。所以第一步不是选芯片,而是先给数据分类。

1.2 三种介质的脾气你要先摸透

EEPROM、NOR Flash、SD卡虽然都能存数据,但内部机理和使用逻辑差别很大,用错就是灾难。

EEPROM(比如AT24C02/AT24C64)是按字节擦写的,寿命标称通常在100万次左右。这货最大的好处是写单字节特别方便,改一个参数不用像Flash那样先整块擦除。但容量一般就几KB到几十KB,速度也慢,I2C接口下写一页要等5ms左右。它非常适合放“改得少、不能丢、要好改”的配置参数。

NOR Flash(比如W25Q64/W25Q128)容量从几百KB到几十MB都有,SPI接口,读速度很快,XIP(片上执行)方便,很多MCU直接在上面跑代码。但NOR的致命弱点是必须按扇区擦除,典型扇区4KB,擦除前要先读出来改完再写回去,而且擦写寿命一般只有10万次。这意味着它不适合频繁小数据写入,只适合放“中量、低频、需要随机读”的数据,比如固件、日志、配方包。

SD卡容量轻松上GB,写速度可以到几十MB每秒,但它本质是Flash加了个控制器,内部有磨损均衡,理论上寿命很长。可SD卡有两个明显毛病:一是怕频繁小写入,每条几百字节地写会拖慢速度,还会把文件系统搞坏;二是掉电风险,写文件到一半断电,FAT表很容易损坏。它适合做“大批量、连续、原始数据”的落盘,也就是采样波形这种场景。

三者真正串在一起用,才能对付工业控制里的各种存储需求。

1.3 一张表说清楚怎么分级

我习惯在项目一开始就画这么一张分级表,直接指导选型和代码结构:

数据类别推荐介质典型容量写入频率关键约束
配置参数EEPROM2KB~32KB低频修改掉电不能丢
运行日志NOR Flash环形区4MB~16MB每分钟几条磨损均衡
固件/配方NOR Flash4MB~16MB极少双备份+校验
高速采样SD卡8GB以上连续大块写掉电保护
掉电快照EEPROM + SRAM几百字节极低频写入时间短

这张表不是凭空定的,背后逻辑是“把写入频率和物理寿命对齐”。SD卡虽然寿命长,但频繁小写会放大磨损,放在这里最亏;EEPROM虽然寿命长,但容量太小,扛不住日志写入量;NOR Flash介于中间,擦写寿命和日志频率刚好匹配。如果日志量特别大,也可以把NOR和SD配合:近期日志写NOR,历史日志批量归档到SD。

2. STM32这一侧:把参数和日志管到字节级

2.1 EEPROM不是拿来频繁“写”的,而是拿来“定参数”的

很多新手拿到EEPROM第一反应是“数据直接存进去”,然后定时器每100ms把当前运行状态往里存一次,过几天发现EEPROM彻底坏了。100ms写一次,一天864000次,就算标称100万次寿命,也就撑一天多。所以EEPROM在所有工业控制器里的定位都应该是“定参数的地方”,不是“记录过程的地方”。

我在STM32代码里通常只允许以下几类操作写EEPROM:

  • 用户修改参数后按“保存”
  • 校准完自动写入
  • 掉电中断里保存关键状态

真正写的时候还有几个细节要特别注意。以AT24C64为例,I2C页写入一次最多写32字节,跨页就得拆两次写。只改一个字节时,我会先读出一整页,改完再页写回去,避免同页其他字节出现莫名其妙的漂移。另外,写完成后最好加一个“读回校验”,也就是重新读出来比对,这一步能发现不少布线不良导致的偶发数据错误。

下面是AT24C64单页写入的示例代码,按设备地址、页地址、数据长度组织好一次写事务:

#define AT24C64_DEV_ADDR 0xA0 #define AT24C64_PAGE_SIZE 32 uint8_t AT24C64_WritePage(uint16_t page_offset, uint8_t *buf, uint8_t len) { uint8_t i; HAL_StatusTypeDef status; if (len > AT24C64_PAGE_SIZE) len = AT24C64_PAGE_SIZE; status = HAL_I2C_Mem_Write(&hi2c1, AT24C64_DEV_ADDR, page_offset, I2C_MEMADD_SIZE_16BIT, buf, len, 100); if (status != HAL_OK) return 1; // 写周期等待,AT24C64 典型 5ms HAL_Delay(5); // 读回校验,确认写进去的数据和预期一致 uint8_t verify[AT24C64_PAGE_SIZE]; HAL_I2C_Mem_Read(&hi2c1, AT24C64_DEV_ADDR, page_offset, I2C_MEMADD_SIZE_16BIT, verify, len, 100); for (i = 0; i < len; i++) { if (verify[i] != buf[i]) return 2; } return 0; }

参数存储我强烈建议做三槽备份加校验。以一组PLC类型参数为例,把30字节的参数分别写入偏移0、64、128三个槽位,每个槽尾部放CRC16校验值。读取时从上到下找“校验通过”的槽,如果后写的槽校验失败,就用前一个槽的值并把警告记进日志。这个设计成本极低,但能避免现场掉电导致参数部分写入损坏的问题。

2.2 NOR Flash的擦写逻辑与磨损均衡

STM32侧的NOR Flash我一般选W25Q系列,SPI接口,4线模式跑起来能稳定到几十MHz。它最大的使用误区是“直接往固定地址写”。因为NOR写之前必须擦除,如果你的日志每5分钟写一条,每10万次就得换一批扇区,否则固定扇区的寿命很快耗尽。

我的日志环形区做法是这样:把4MB划分成128个32KB块,每个块头64字节存魔数(比如0xAA55)、块序号、写入偏移、CRC。启动时扫描块头找到最大的块序号作为当前写块,如果当前块写满就擦下一个块。擦除前先更新块头,防止掉电后不知道当前状态。这样每块实际写满32KB才擦一次,128块轮一圈,寿命比固定地址放大了上百倍。擦除是耗时操作,实测W25Q128擦一个32KB块大约需要100ms左右,所以不能放在中断里执行,我会放到任务循环里,用状态机分步处理。

再补一个经验:NOR Flash写数据之前如果忘了擦除,读出来的数据往往是“老数据和新数据按位AND的结果”,因为Flash写只能把1变0,不能把0变1。遇到这种现象,第一反应不是怀疑芯片坏了,而是回忆这个地址上次是什么时候擦的。调试阶段我见过太多次“写入后读数不对”,最后发现都是同一个块写满了没触发擦除。

对于固件存储,我还习惯留双备份区。A区放当前运行版本,B区放升级临时镜像。启动时Bootloader先检查B区头部有没有“升级待确认”标志,有的话校验整个镜像,再搬到A区,然后跑A区。这样升级失败后还能回滚旧版本,不至于在现场变砖。FPGA配置文件也可以按同样思路放在NOR里,上电时由STM32读出来灌给FPGA。

2.3 STM32这边的推荐分层结构

STM32不是把所有事都干了,它只负责“小数据”和“管理调度”。我一般把代码按下面这层结构组织:

  • 参数层:EEPROM驱动 + 参数结构体映射,提供Load和Save接口
  • 日志层:NOR环形区驱动 + 格式化输出,提供LogWrite和LogExport
  • 固件层:NOR的A/B区管理,提供FirmwareDownload和FirmwareActive
  • 与FPGA通信层:SPI/并口/双口RAM驱动,负责给FPGA下采集命令、接收完成中断

把中低速数据全部留在STM32侧,可以避开FPGA逻辑编译时间长的麻烦事。改参数格式、加日志字段、调整固件升级流程,都在STM32的C代码里改,重新编译下载就行,FPGA那边完全不用动。这对后期维护非常关键。

3. FPGA这一侧:SD卡是另一个世界,别把它当普通外设

3.1 为什么高速数据要绕过STM32直接交给FPGA落盘

先说一个实测结论:STM32F4配合SDIO接口大概能到4MB/s左右,看着还行,但采样数据一旦加上时间戳、加上协议开销,再叠加FATFS文件系统写入的延迟,很容易出现丢数据。更关键的是,STM32的所有外设共享一个总线带宽,SD卡写入和CAN通信、以太网、UI渲染抢带宽,系统实时性很难保证。

FPGA这边就不一样。高速ADC的数据流是天然并行的,FPGA可以用一个异步FIFO或者双口RAM把ADC数据缓冲下来,然后由独立的SD卡控制器按块突发写入。整个数据路径完全不经过MCU,MCU只负责告诉FPGA“开始采”和“采多久”,以及接收FPGA给的“写完第几个块”的中断。这个思路在测量仪器、振动分析、语音采集上非常实用。

我做过一套方案:ADC以10MHz输出16bit数据,理论速度20MB/s。先用一个深度8192*16bit的FIFO削峰,当FIFO半满时,FPGA内部状态机启动SD卡写块操作,每次写512字节。由于写SD卡时ADC还在继续采,FIFO的深度必须大于一次块写入期间ADC新产生的数据量,否则就溢出了。按SPI模式下512字节写入耗时约3ms计算,一次写块期间ADC会多产生约6KB数据,所以16KB深度的FIFO是刚好够用的,再留点余量,我通常会选32KB。

还有一个容易忽略的点:数据缓冲和文件系统缓冲是两回事。很多FPGA方案把FIFO和SD卡的缓冲区混为一谈,写入的时候直接拿FIFO当传地址,结果FIFO空了一半就开始发写命令,导致SD卡数据错乱。我习惯在FIFO后面再接一个固定512字节的写缓冲区,FIFO攒够512字节就搬进缓冲区,然后才启动SD卡块写命令。

3.2 SD卡裸扇区写入、SPI模式与文件系统取舍

很多人第一步就问:FPGA上要不要跑文件系统?我的答案是:看你要不要“拔卡插电脑直接读”。

如果产品是数据记录仪,用户需要把SD卡拔下来插电脑,用工具打开波形文件,那就必须有FAT文件系统。结合FPGA的资源情况,我推荐用软核方案,也就是在FPGA里例化一个NIOS II或者MicroBlaze,跑FatFS的SDIO驱动,CPU负责文件系统逻辑,自定义逻辑负责数据搬运。这个方案的好处是代码移植成本低,出了问题也容易调试。

如果项目只需要持续写一批固定格式的数据,拿到电脑后用专用上位机解析,那我建议直接绕开文件系统。做法是把SD卡先格式化成FAT32,然后在PC上创建一个大文件(比如2GB)并完全填充,再把这个文件的起始扇区地址和长度记录到你的配置区。FPGA只维护一个“当前写入扇区”计数器,到文件末尾就回到文件头继续循环。由于你写入的是已存在的文件数据区,不更新FAT表,所以拔卡掉电都不会破坏文件结构。代价是电脑上看到的文件内容是原始的块数据,需要自己做解析和索引。

还有一种折中的方案:用FPGA裸逻辑维护一个简易媒体分配表,每写完一个连续扇区区间,就把逻辑块号写到卡末尾的一个索引区。PC端工具读取时先扫索引区,再按逻辑块号拼接文件。这比FATFS轻量很多,也比纯裸扇区方便解析。

具体到SPI还是SDIO选择上,FPGA新手我建议先走SPI模式。SDIO的工作频率高、命令状态机复杂、CRC校验也多,调试起来头大;SPI模式的命令格式简单,一个状态机就能搞定初始化、单块读、单块写、多块写,在CLK约10MHz时实际写入速度也能到1MB/s以上,对于大多数采样落盘场景已经够用。如果确实需要几MB/s以上写入,再考虑SDIO加DMA。

3.3 一个可参考的Verilog写块状态机

下面是一个SPI模式下写单个512字节扇区的简化状态机框架,注意这里只展示核心流程,实际工程里还需要加超时计数、CRC7校验和错误重试逻辑。

typedef enum { IDLE, WAIT_FIFO, // 等待FIFO攒够512字节 SEND_CMD24, // 发送CMD24(写单块) SEND_TOKEN, // 发送0xFE起始令牌 SEND_DATA, // 逐个字节发送512字节数据 WAIT_BUSY, // 等待SD卡忙结束 CHECK_CRC, // 读取响应并验证 FINISH // 写完成,发出完成脉冲 } state_t; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: if (fifo_count >= 512) state <= SEND_CMD24; SEND_CMD24: if (spi_tx_done) state <= SEND_TOKEN; SEND_TOKEN: if (spi_tx_done) state <= SEND_DATA; SEND_DATA: if (byte_cnt == 511 && spi_tx_done) state <= WAIT_BUSY; WAIT_BUSY: if (spi_rx_data == 8'hFF) state <= CHECK_CRC; CHECK_CRC: if (spi_rx_data[4:0] == 5'b00101) state <= FINISH; FINISH: begin write_done <= 1'b1; state <= IDLE; end endcase end end

这个状态机有两个细节需要注意。第一,WAIT_BUSY状态的判断条件:SD卡忙时数据线会一直拉低,只有空闲时才释放回高电平,所以退出条件应该是最少连续收到8个0xFF字节,而不是只判断某一个字节。第二,SPI模式下写入前必须发CMD24并等待R1响应为0x00,如果卡没有准备好,R1会返回0x05之类错误值,此时要重发命令而不是强行写数据。

我把帧格式固定为:2字节帧头(0xEB90)、2字节帧长度、4字节时间戳、若干字节通道数据、2字节CRC16。这个格式在PC端解析特别方便,而且可以通过帧头快速找回同步点。

4. 数据分级流转:掉电、启动、异常该怎么衔接

4.1 正常工作时的一条完整数据流

把整台设备串起来看,“分级存储”不是各存各的,而是有一套明确的流转关系。以我做过的一套振动采集控制器为例:

  • 开机后,STM32先从EEPROM加载工作参数,初始化采样通道。
  • 用户通过触摸屏修改采样频率后,STM32先将新参数写入EEPROM,再把采样配置通过SPI发给FPGA。
  • FPGA收到配置后,启动ADC采集,数据经过FIFO缓冲,按块写入SD卡。
  • 每写完1MB数据,FPGA通过中断通知STM32更新“当前采样文件大小”。STM32把它和当前时间写入NOR日志区。
  • 设备异常报警时,STM32把报警类型、时间戳、当前采样块号写入EEPROM和NOR日志,方便事后对齐波形数据和报警时刻。

这套流程的关键点在于STM32不直接碰大数据流,它只做小数据的持久化和状态同步。FPGA也不需要关心配置参数的格式和校验,只负责按指令高速搬数据。两边之间的耦合非常少,后期任何一侧改动都不会连坐另一侧。

4.2 掉电瞬间的黄金时间段怎么用

工业现场最怕的其实是掉电。尤其是伺服控制器、CNC设备,如果正在运动过程中突然断电,当前位置、当前速度、当前平面的坐标系必须死死记住,否则上电后设备不知道自己在哪,可能直接撞机。

我做的掉电处理顺序是这样的:

  1. 电源监测芯片输出掉电中断(一般比主电源降到MCU最低工作电压早5ms以上)。
  2. STM32立刻把FPGA采集中断停掉,停止一切外部通信。
  3. 将关键状态(当前位置坐标、当前配方号、工序状态、实时时钟)打包成一段不超过256字节的数据。
  4. 写入EEPROM。如果一次写不完,优先写最小必需集,其余写NOR Flash。
  5. 写完后置一个“掉电标志位”,再进入低功耗或死循环等待。

为什么不用NOR Flash做这一步?因为NOR擦除太慢。W25Q128擦一个扇区大约需要100ms,EEPROM写一个字节才5ms,而且写之前不需要擦。掉电窗口就几毫秒,根本等不起一次扇区擦除。所以掉电快照必须优先进EEPROM,除非你的板子上设计了足够大的超级电容,能把供电拉长到几百毫秒。

我实测过用2200uF电容搭配低功耗MCU方案,大约能给STM32争取6~8ms的掉电工作时间。如果还想写NOR日志,电容得加到10000uF以上,成本明显上升。大多数场景下,EEPROM写关键姿态已经足够。不过要特别注意:掉电后再上电,第一件事不是立即恢复运行,而是先读掉电标志和EEPROM数据,确认坐标有没有“半写”状态。如果CRC校验失败,说明掉电那一下太急了,EEPROM正在写一半就没了电,这时候必须让设备进入手动回零模式,而不是自动恢复。

4.3 启动自检顺序不可乱

分级存储做得再漂亮,如果启动时没有一套完整自检,现场早晚给你颜色看。我踩过最典型的坑是:SD卡没插好,开机后系统直接把旧数据当新数据用了;还有一次是NOR日志区头部魔数错乱,系统扫描时死循环。

启动自检建议按下面的顺序来:

  1. 检测EEPROM是否可读,校验CRC。失败则载入出厂默认参数,并置“参数异常”报警。
  2. 扫描NOR Flash日志区块头,找到有效日志块,重建当前写入偏移。如果魔数全错,就不要扫描了,直接格式化日志区并记录一条日志。
  3. 检查SD卡是否存在、能否识别容量。用SPI模式的初始化命令逐个进行复位,直到收到0x01或重试次数用尽。
  4. 如果SD卡是文件系统方案,检查根目录有没有目标记录文件。如果没有就创建,并写一条开机记录。
  5. 最后恢复该恢复的现场状态。

这里有个小诀窍:把上电自检结果也写进日志。很多人只会在故障时看日志,但开机那几十秒的信息往往最有用。比如“EEPROM参数CRC失败后回退默认值”什么时候发生的,如果不在日志里,后面排查起来毫无线索。我习惯在参数加载完成、外设初始化完成后各打一条带CRC结果的记录。

5. 实测“踢到铁板”的经验:故障排查与速查

搞这套方案三年多,遇到的故障我整理成了一张速查表,按介质分类,排查起来特别快。

5.1 EEPROM方向:I2C死锁和半写是两大顽固问题

I2C总线死锁是最常见也最隐蔽的:SDA被拉低,所有通信全部卡死。原因通常是从机正在写EEPROM内部周期时主机发起了下一次通信,或者上电时序中总线状态未知。解决方式是总线复位函数里连续产生9个SCL时钟,让从机释放SDA,再发一个STOP条件。调试时最好定期跑一遍总线复位自检。

半写问题前面提过,真的遇到过掉电时正在写参数,然后参数槽既不是旧值也不是新值,而是新旧混合。所以三槽备份不是软弱设计,而是必要的工业保险。

5.2 NOR Flash方向:擦除和磨损不均的坑

擦除期间任务卡死很常见。一次4KB扇区擦除在SPI 50MHz下约需50ms,如果直接在中断服务函数里等,整个系统实时性就崩了。我改用状态机加超时机制,擦除命令发出去之后就切出去干别的,等SPI完成中断再回来。

磨损不均导致块坏也遇到过。环形逻辑看似轮转,但启动时如果固定从块0开始扫描,每次开机都会碰一次块0,长期下来块0先到寿命。我在块头里额外存一个“最近写入时间”的计数器,启动时直接跳到最新块,不要让扫描每次都从头开始。

另外SPI指令集里读状态寄存器(0x05)的轮询也要加超时。有时候Flash芯片内部正在做别的操作,状态寄存器一直返回busy,不加超时死等会卡死整个任务。

5.3 SD卡方向:初始化失败和文件损坏

SD卡问题最多,排查优先级也最高。

一是初始化失败。SPI模式下最常见的原因是上电时序不对:很多卡要求先至少74个时钟周期的拉高,然后发CMD0,再发CMD8确认卡类型。如果这段初始化时序压缩得太狠,一些国产卡直接不认。我的做法是写一个软件延时循环保证100个空闲时钟,再开始CMD0,实测明显提高兼容性。

二是文件系统损坏。这一点几乎所有做过这个方向的人都吃过亏:正写到一半拔卡或掉电,再插到电脑上提示需要格式化。所以我的原则是:能走“固定大文件预分配”就不走逐条FAT更新,能整块写就不逐扇区写。如果确实需要文件系统,那必须加UPS或电容保护,保证掉电时能完整收尾当前写事务。

再补一个操作层面的小技巧:生产部署前,在电脑上把一张SD卡格式化好,再做一个镜像备份。现场换卡时直接用镜像烧录器灌进去,至少保证文件系统结构一致。以前我遇到过现场工程师随手找了一张卡,格式化成exFAT,结果固件里的FatFS不支持,初始化失败后已经录了两个小时的数据全丢了。

我习惯在每次写SD卡的程序里加一个“写状态记录”,也就是把上一次写操作的扇区地址、块数、CRC结果记到NOR日志区。一旦卡上数据出现空洞,先看NOR日志就知道是不是中途掉电导致的,不用一遍遍去解析卡里的原始数据。

6. 选型口诀与个人习惯

6.1 几个常见的量产配置

说了这么多,最后给几套可以直接抄的配置组合,覆盖不同产品场景。

  • 低成本仪表:STM32G0 + AT24C02 + W25Q32,没有SD卡。只保存参数和日志,采样数据通过通信端口回传上位机。
  • 标准控制器:STM32F4 + AT24C64 + W25Q128 + 8GB SD卡,就是这篇文章写的标准三层结构。
  • 高性能采集器:FPGA(比如Artix-7)+ SD卡 + EEPROM,STM32只做显示和上位机通信。数据流全部走FPGA,MCU工作量很轻。
  • 纯FPGA方案:没有MCU的时候,EEPROM用Verilog I2C控制器挂参数,SD卡用裸扇区写入,配合一个软核跑FatFS做文件管理。

选型口诀我总结成一句话:参数进EEPROM,日志进NOR,波形进SD,掉电关键量进EEPROM。

6.2 最后分享一个习惯

这套分级存储方案我做了不少版本迭代,最大的体会是:存储方案一定要在硬件设计阶段就定下来,不要等软件写完了再改。尤其是掉电保护那一环,涉及电容容量和PCB布局,后补非常痛苦。如果你正在做一个带FPGA的控制器,建议先列一张你自己的数据分类表,把每种数据的频率、体积、掉电要求填上,再对照这篇文章的结构设计,基本就不会跑偏。

最后再给一个小技巧:NOR Flash和SD卡的驱动代码开个硬件抽象层,把底层接口统一成ReadSector/WriteSector/EraseBlock。这样就算现场换介质,比如把NOR从W25Q128换成MX25L128,底层驱动改几行就行,上面的日志、固件、文件缓存逻辑完全不用动。我吃过分立维护两套驱动的亏,后来统一抽象层,维护成本立刻降了一半。存储这件事,前期多想一步,后面能少踩一路坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询