1. 工业控制器存储需求拆解与方案选型逻辑
工业控制器和消费类电子产品在数据存储上的诉求完全是两码事。消费类产品丢了数据顶多用户骂两句,工业控制器丢了数据可能导致产线停机、设备损坏甚至安全事故。我在做这块方案的时候,第一步永远是先把数据按"重要程度"和"变更频率"两个维度分类,而不是上来就选芯片。
1.1 工业场景下数据的三层分类
实际项目里,工业控制器要存的数据大致可以分成三类:
第一类是"命根子"数据:设备序列号、出厂校准参数、电机参数辨识结果、PID整定参数、通信地址配置。这类数据的特点是写入次数极少(可能出厂写一次,之后几年都不动),但绝对不能丢,丢了设备就变成砖头。这类数据量通常很小,几十字节到几KB。
第二类是"高频变更"数据:运行时长累计、故障记录、生产计数、关键工艺参数的最近设定值。这类数据的特点是写入频繁(可能每分钟甚至每秒都在更新),但单次数据量也不大,通常几KB到几十KB。掉电丢最后一次可以接受,但不能丢历史累计。
第三类是"过程数据":运行日志、波形采样记录、故障前后的现场快照、配方文件。这类数据量可能很大(几十MB到几个GB),写入频率中等,但需要大容量存储和文件系统管理。
这三类数据如果都用同一种介质存,要么成本爆炸,要么可靠性不够,要么寿命撑不住。所以分级存储不是"为了显得高级",而是被需求逼出来的必然选择。
1.2 为什么是EEPROM + NOR Flash + SD卡这个组合
先说EEPROM。很多人觉得EEPROM容量小、速度慢,是不是该淘汰了?恰恰相反,在工业场景里EEPROM的地位反而很稳固。原因在于它的擦写寿命通常能做到100万次以上(比如ST的M24系列标称400万次),而且支持字节级擦写,不需要像Flash那样先擦一个扇区再写。对于那种"每次断电前要保存几个参数"的场景,EEPROM是最省心的选择。你不需要管理擦除块,不需要考虑磨损均衡,直接按字节写就行。
NOR Flash的定位在中间层。它的容量比EEPROM大得多(常见几MB到几十MB),擦写寿命通常在10万次左右,支持扇区擦除和页写入。它适合存那些"中等容量、中等频率"的数据,比如故障记录、配方参数、历史统计数据。NOR的特点是支持XIP(就地执行),读取速度快,随机访问性能好,这也是为什么很多工业控制器用它存代码和关键数据。
SD卡负责大容量存储。它的优势是容量大、单位成本低、可插拔更换。但SD卡的可靠性在工业环境下是短板——消费级SD卡的工作温度范围窄,擦写寿命参差不齐,而且文件系统在突然断电时容易损坏。所以SD卡只适合存"丢了不影响设备运行"的过程数据,而且必须做好文件系统的掉电保护。
这个三层架构的核心逻辑是:用最可靠的介质存最关键的数据,用最便宜的介质存最不关键的数据,各司其职,成本和安全兼顾。
1.3 STM32和FPGA在这个架构里的分工
这个方案里STM32和FPGA的分工很明确。STM32作为主控,负责文件系统管理、通信协议处理、业务逻辑运算,它自带EEPROM接口(I2C或SPI)、NOR Flash接口(QSPI或FSMC)、SD卡接口(SDIO或SPI),软件生态成熟,开发效率高。
FPGA的角色是"数据搬运工"和"时序保障者"。在高速数据采集场景下,ADC采样率可能到几MSPS甚至几十MSPS,STM32的CPU根本来不及处理每一个采样点。这时候FPGA负责把高速数据流缓存到内部FIFO或外挂的SRAM里,再以STM32能接受的速率通过FSMC或SPI接口传给STM32。另外,FPGA还能做掉电检测的硬件逻辑——一旦检测到电源跌落,立即触发紧急保存流程,这个响应速度是软件做不到的。
两者配合的关键在于接口设计和数据流控制。我见过不少项目在这里翻车:FPGA往STM32送数据太快,STM32来不及取,数据就丢了;或者STM32在写Flash的时候总线被占用,FPGA的数据没地方放。这些都需要在架构设计阶段就考虑清楚。
2. 三种存储介质的核心原理与关键参数
选型不能只看容量和价格,得把每种介质的脾气摸透。下面我把这三种介质在工业控制器里最需要关注的特性拆开讲。
2.1 EEPROM的字节级擦写与寿命管理
EEPROM的全称是电可擦可编程只读存储器,它的存储单元结构决定了它支持字节级擦写。每个存储单元由两个晶体管组成(浮栅晶体管+选择晶体管),擦写时通过隧穿效应注入或移除电荷。这个过程对氧化层有损耗,所以有擦写寿命限制。
工业级EEPROM的擦写寿命通常在100万到400万次之间。听起来很多,但如果你每秒写一次,100万次也就11.6天就用完了。所以关键数据绝对不能高频写入。我的做法是:在RAM里维护一份参数副本,只有检测到参数真正变化时才写EEPROM,而且写入前做比较,值没变就不写。
另外,EEPROM的写入周期比较长,典型值5ms左右。这意味着你不能在中断里直接写EEPROM,否则会阻塞其他任务。正确的做法是标记"待保存"标志,在主循环的空闲时间片里执行写入。
还有一个容易忽略的点:EEPROM的I2C接口速率。标准模式100kHz,快速模式400kHz,高速模式1MHz。如果你用100kHz,写一个32字节的页需要的时间是:起始位+设备地址+字地址+32字节数据+停止位,大约(1+1+1+32+1)*9/100000 ≈ 3.2ms,再加上内部写入周期5ms,总共8ms左右。这个时间在实时性要求高的系统里必须考虑进去。
2.2 NOR Flash的扇区擦除与磨损均衡
NOR Flash的存储单元是浮栅MOS管,读取速度快,支持随机访问,但写入前必须先擦除,而且擦除的最小单位是扇区(通常4KB),写入的最小单位是页(通常256字节)。这个特性决定了你不能像EEPROM那样随意改写某个字节。
NOR Flash的擦写寿命通常是10万次(每个扇区)。如果你有一个扇区用来存运行时长,每分钟更新一次,那10万次就是69天。所以必须做磨损均衡——把数据分散到多个扇区轮流写,或者用"追加写+定期整理"的方式。
我在项目里常用的磨损均衡策略是"双扇区交替+版本号"。具体做法是:分配两个扇区A和B,每次写入时写到当前非活动扇区,写入成功后更新活动扇区标记。这样每个扇区的擦写次数减半。如果数据量允许,可以扩展到4个或8个扇区轮换,寿命成倍提升。
NOR Flash的另一个关键参数是擦除时间。扇区擦除典型值几十毫秒到几百毫秒,块擦除可能到1秒。这个时间内Flash总线是被占用的,如果代码在Flash里运行(XIP模式),CPU会暂停。所以擦除操作要么放在RAM里执行,要么在擦除期间确保没有关键中断需要响应。
2.3 SD卡的文件系统与掉电保护
SD卡本身只是块设备,上面要跑文件系统(通常是FAT32或exFAT)才能被PC识别。但文件系统在突然断电时非常脆弱——FAT表可能只写了一半,目录项可能指向不存在的簇,整个卡可能变成RAW格式。
工业场景下SD卡的掉电保护有几个层次:
硬件层:在SD卡电源上加大容量电容(比如1000uF),掉电时能给卡提供几十毫秒的供电,让它完成当前写入操作。同时用电压检测芯片监控电源,一旦低于阈值立即触发中断。
文件系统层:不要用标准的FAT库直接写,而是用"日志式"写入。每次写文件时先写到一个临时文件,写完并flush后再重命名为正式文件。这样即使掉电,最多丢临时文件,正式文件不会损坏。
应用层:关键数据不要只存SD卡,要在NOR Flash里留一份最近的副本。SD卡损坏后可以从NOR恢复。
SD卡还有一个坑是"内部寄存器锁死"。有些卡在异常掉电后会进入锁定状态,需要发送特定的命令序列才能解锁。这个在调试阶段很折磨人,建议在驱动里加上卡初始化的重试机制,检测到初始化失败时尝试发送CMD0和CMD1复位。
3. 硬件电路设计与接口分配实操
硬件设计这块,我踩过的坑比软件还多。下面按三种介质分别说。
3.1 EEPROM的I2C电路与地址配置
EEPROM通常用I2C接口,两根线SDA和SCL,加上写保护引脚WP。电路设计要点:
- 上拉电阻:I2C是开漏输出,必须加上拉。阻值根据总线电容和速率选,通常4.7kΩ(100kHz)到2.2kΩ(400kHz)。总线电容超过200pF时要减小阻值。
- 地址引脚:A0/A1/A2三个引脚决定设备地址的低3位,可以挂8片同型号EEPROM。如果只用一片,全部接地即可。
- 写保护:WP接高电平禁止写入,接低电平允许写入。工业场景建议用GPIO控制,正常运行时拉高防止误写,需要保存时拉低。
- 去耦电容:每片EEPROM的VCC引脚旁边放0.1uF陶瓷电容,距离越近越好。
我遇到过一个问题:I2C总线上挂了EEPROM和另一个传感器,传感器偶尔会把SDA拉低导致总线死锁。解决办法是在SDA上加一个总线恢复电路,或者用I2C多路复用器隔离。
3.2 NOR Flash的QSPI接口与PCB布局
NOR Flash现在主流是QSPI接口,四根数据线IO0-IO3,加上CLK和CS。STM32的QSPI外设可以直接驱动,也可以映射到内存地址空间做XIP。
PCB布局要点:
- 等长走线:QSPI的CLK和四根数据线要做等长,误差控制在5mil以内。速率越高要求越严,104MHz以上建议控制在2mil。
- 阻抗匹配:单端阻抗50Ω,差分不用考虑因为QSPI不是差分。
- 远离干扰源:不要和DC-DC电源、电机驱动线平行走线,至少间隔3倍线宽。
- CS信号:CS走线不要和其他信号交叉,必要时包地处理。
STM32的QSPI配置里有个容易搞错的参数:dummy cycles。不同型号的NOR Flash在发送读命令后需要不同数量的dummy周期才能输出数据,这个值必须查Flash的数据手册。设错了读出来的全是0xFF或随机值。
3.3 SD卡的SDIO接口与电源设计
SD卡支持SDIO和SPI两种模式。SDIO是4位并行,速度快(高速模式50MHz,理论25MB/s),但引脚多;SPI模式只用4根线,速度慢但简单。工业控制器建议用SDIO,因为要存大量数据。
SDIO电路要点:
- 上拉电阻:CMD和DAT0-3都需要上拉,典型值10kΩ-50kΩ。注意SDIO规范要求上拉电阻在卡检测和初始化阶段有效,高速传输时可以断开以减小功耗。
- 电源切换:SD卡支持1.8V和3.3V两种电压,初始化时用3.3V,协商后可以切到1.8V提高速度。需要用一个MOS管做电源切换。
- 卡检测:CD引脚用来检测卡插入,可以接GPIO,也可以接地(如果卡是焊死的)。
- ESD保护:SD卡座是暴露在外的,必须加ESD保护器件,否则人体静电可能打坏SDIO控制器。
电源设计上,SD卡的瞬时电流可能达到200mA,而且写入时电流波动很大。建议单独用一颗LDO给SD卡供电,输入输出都加足够的电容(输入10uF,输出22uF以上)。
4. 分级存储的软件架构与数据流设计
硬件搭好了,软件架构才是决定系统稳定性的关键。我一般把存储软件分成三层:驱动层、管理层、应用层。
4.1 驱动层的抽象与统一接口
驱动层负责直接操作硬件,但对外提供统一的接口。这样上层不用关心数据存在哪里,只需要调用storage_write(id, buf, len)和storage_read(id, buf, len)。
驱动层要处理的事情包括:
- EEPROM驱动:I2C读写、页边界处理、写周期等待、ACK超时重试。
- NOR Flash驱动:QSPI命令封装、扇区擦除、页写入、状态寄存器轮询、坏块管理(虽然NOR坏块少,但工业级要检查)。
- SD卡驱动:SDIO初始化、CMD/ACMD命令序列、块读写、DMA传输、错误恢复。
统一接口的设计要点是错误码标准化。我定义了一套错误码:0表示成功,-1表示硬件错误,-2表示超时,-3表示校验失败,-4表示空间不足。上层根据错误码决定重试还是上报。
4.2 管理层的磨损均衡与掉电保护
管理层是分级存储的核心。它要决定数据存在哪个介质、什么时候迁移、怎么保证一致性。
磨损均衡:对NOR Flash,我用"日志式追加+定期整理"的策略。每个数据项有一个固定ID,写入时追加到当前活动扇区的末尾,同时记录版本号。读取时扫描所有扇区,找版本号最高的那条记录。当活动扇区写满后,切换到下一个扇区,并把有效数据整理到新扇区。
掉电保护:关键是"原子写"。对于跨多个存储单元的数据,要么全部写入成功,要么全部不写。实现方法是用"影子页":先写影子页,写完后更新一个"有效标志",再写正式页。掉电后检查有效标志,如果影子页有效但正式页无效,就从影子页恢复。
数据校验:每个数据块后面加CRC32校验。读取时先校验,校验失败则尝试从备份恢复。EEPROM和NOR Flash各存一份关键参数,互为备份。
4.3 应用层的数据分类与存储策略
应用层根据数据类型选择存储介质:
| 数据类型 | 存储介质 | 写入频率 | 保护策略 |
|---|---|---|---|
| 设备序列号 | EEPROM | 出厂一次 | 写保护+CRC |
| 校准参数 | EEPROM | 极少 | 双备份+CRC |
| 运行时长 | NOR Flash | 每分钟 | 磨损均衡+掉电保护 |
| 故障记录 | NOR Flash | 事件触发 | 环形缓冲+CRC |
| 配方文件 | NOR Flash | 用户修改 | 双扇区交替 |
| 运行日志 | SD卡 | 持续 | 日志式写入+定期flush |
| 波形记录 | SD卡 | 触发采集 | 临时文件+重命名 |
这个策略的核心是:关键数据双备份,高频数据做均衡,大容量数据做日志。
5. 实操过程与关键代码实现
下面把几个关键环节的代码实现讲清楚。代码基于STM32 HAL库,FPGA部分用Verilog描述。
5.1 EEPROM读写与参数保存
EEPROM用I2C接口,STM32的HAL库提供了HAL_I2C_Mem_Write和HAL_I2C_Mem_Read函数。但直接用这两个函数有几个坑:一是写周期内不能响应I2C请求,二是页边界会自动回卷。
#define EEPROM_ADDR 0xA0 #define EEPROM_PAGE_SIZE 32 typedef struct { uint32_t magic; uint16_t version; float kp, ki, kd; uint32_t crc; } ParamBlock; HAL_StatusTypeDef eeprom_write_params(ParamBlock *param) { param->magic = 0x50415241; // "PARA" param->crc = crc32((uint8_t*)param, sizeof(ParamBlock) - 4); uint8_t *p = (uint8_t*)param; uint16_t addr = 0; uint16_t remaining = sizeof(ParamBlock); while (remaining > 0) { uint16_t page_remain = EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); uint16_t write_len = (remaining < page_remain) ? remaining : page_remain; if (HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR, addr, I2C_MEMADD_SIZE_16BIT, p, write_len, 100) != HAL_OK) { return HAL_ERROR; } HAL_Delay(6); // 等待EEPROM内部写周期 addr += write_len; p += write_len; remaining -= write_len; } return HAL_OK; }这段代码的关键点是按页写入和写周期等待。EEPROM的页大小通常是32字节或64字节,跨页写入会回卷覆盖,必须分页处理。写周期等待不能省,否则下一次写入会被NACK。
5.2 NOR Flash的扇区擦除与数据追加
NOR Flash用QSPI接口,STM32的QSPI外设支持内存映射模式,但擦除和写入必须用间接模式。
#define NOR_SECTOR_SIZE 4096 #define NOR_PAGE_SIZE 256 // 扇区擦除 HAL_StatusTypeDef nor_erase_sector(uint32_t addr) { QSPI_CommandTypeDef cmd = {0}; cmd.Instruction = 0x20; // Sector Erase cmd.Address = addr; cmd.AddressSize = QSPI_ADDRESS_24_BITS; cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; cmd.AddressMode = QSPI_ADDRESS_1_LINE; cmd.DataMode = QSPI_DATA_NONE; cmd.DummyCycles = 0; if (HAL_QSPI_Command(&hqspi, &cmd, 1000) != HAL_OK) return HAL_ERROR; // 等待擦除完成 QSPI_AutoPollingTypeDef poll = {0}; poll.MatchValue = 0x00; poll.Mask = 0x01; poll.MatchMode = QSPI_MATCH_MODE_AND; poll.StatusBytesSize = 1; poll.Interval = 0x10; poll.AutomaticStop = QSPI_AUTOMATIC_STOP_ENABLE; cmd.Instruction = 0x05; // Read Status Register cmd.DataMode = QSPI_DATA_1_LINE; cmd.NbData = 1; return HAL_QSPI_AutoPolling(&hqspi, &cmd, &poll, 5000); }擦除时间典型值几百毫秒,AutoPolling会自动轮询状态寄存器,比手动延时可靠。
5.3 SD卡的日志式写入与掉电恢复
SD卡用FatFs文件系统,但标准FatFs在掉电时容易损坏。我的做法是加一层日志:
// 日志式写入:先写临时文件,再重命名 FRESULT sd_log_write(const char *filename, const uint8_t *data, uint32_t len) { char temp_name[64]; snprintf(temp_name, sizeof(temp_name), "%s.tmp", filename); FIL fil; FRESULT res = f_open(&fil, temp_name, FA_CREATE_ALWAYS | FA_WRITE); if (res != FR_OK) return res; UINT written; res = f_write(&fil, data, len, &written); if (res != FR_OK || written != len) { f_close(&fil); f_unlink(temp_name); return FR_DISK_ERR; } res = f_sync(&fil); // 强制flush到卡 f_close(&fil); if (res != FR_OK) return res; // 重命名,这一步是原子的 f_unlink(filename); // 删除旧文件 return f_rename(temp_name, filename); }f_sync是关键,它确保数据真正写到SD卡而不是停在缓存里。重命名操作在FAT文件系统里是原子的,要么成功要么失败,不会出现半个文件。
5.4 FPGA与STM32的数据交接
FPGA采集高速数据后,通过FSMC接口传给STM32。FSMC可以映射成SRAM时序,STM32直接读地址就能取数据。
// FPGA侧:双口RAM作为数据缓冲 module data_buffer ( input wire clk_adc, // ADC采样时钟 input wire clk_stm32, // STM32 FSMC时钟 input wire [15:0] adc_data, input wire adc_valid, output reg [15:0] fsmc_data, input wire [10:0] fsmc_addr, output reg data_ready ); reg [15:0] ram [0:2047]; reg [10:0] wr_ptr = 0; reg [10:0] rd_ptr = 0; reg [10:0] count = 0; // ADC写入 always @(posedge clk_adc) begin if (adc_valid) begin ram[wr_ptr] <= adc_data; wr_ptr <= wr_ptr + 1; if (count < 2048) count <= count + 1; end end // STM32读取 always @(posedge clk_stm32) begin fsmc_data <= ram[fsmc_addr]; data_ready <= (count > 0); end endmodule这个双口RAM的深度是2048,ADC以1MSPS写入,STM32以100kSPS读取,缓冲能撑20ms,足够STM32处理其他任务。
6. 常见问题与排查技巧实录
这部分是我这些年踩坑攒下来的经验,很多是文档里不会写的。
6.1 EEPROM读写失败排查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 全部读0xFF | 设备地址错误 | 用逻辑分析仪抓I2C波形,确认地址 |
| 偶尔NACK | 上拉电阻过大 | 减小到2.2kΩ,检查总线电容 |
| 写入后读回不对 | 写周期未等待 | 增加5-10ms延时 |
| 跨页写入覆盖 | 未处理页边界 | 按页拆分写入 |
| 总线死锁 | 从设备拉低SDA | 加总线恢复电路或复位从设备 |
我遇到最坑的一次是EEPROM的WP引脚悬空,导致写入时好时坏。后来改成GPIO控制,正常运行时拉高,保存时拉低,问题解决。
6.2 NOR Flash数据丢失排查
NOR Flash最常见的问题是擦除不彻底和写入未对齐。擦除不彻底表现为读出来的数据有些位是0有些是1,这是因为擦除时间不够或电压不足。写入未对齐表现为数据错位,这是因为NOR Flash要求按页写入,跨页会回卷。
还有一个隐蔽的问题是QSPI dummy cycles设置错误。不同厂家的Flash在读命令后需要的dummy周期不同,比如Winbond的W25Q系列在104MHz下需要4个dummy周期,而Micron的某些型号需要6个。设少了读出来是0xFF,设多了数据错位。
6.3 SD卡无法识别与文件系统损坏
SD卡的问题分两类:卡本身的问题和文件系统的问题。
卡本身的问题:
- 初始化失败:检查电源电压是否稳定,CMD线是否有上拉,时钟频率是否在400kHz以下(初始化阶段)。
- 识别为MMC卡:有些卡兼容MMC协议,需要在ACMD41里设置HCS位强制SD模式。
- 容量识别错误:SDHC和SDXC的容量计算方式不同,SDHC用块地址,SDXC用字节地址。
文件系统的问题:
- 卡变成RAW:用
f_mkfs重新格式化,但数据会丢。预防方法是加掉电保护。 - 文件写入后长度为0:
f_sync没调用,数据还在缓存里。 - 目录项损坏:用
f_check检查文件系统一致性。
6.4 FPGA与STM32通信异常
FPGA和STM32之间的FSMC通信出问题,通常表现为读到的数据全是0或全是0xFF。排查步骤:
- 用示波器看FSMC的片选、读使能、地址线是否有波形。
- 检查FPGA侧的时序是否满足FSMC的建立/保持时间要求。
- 确认双口RAM的读写时钟域是否正确,跨时钟域要加同步器。
- 检查地址映射是否一致,STM32的FSMC地址和FPGA的RAM地址要对齐。
我遇到过一次FPGA的RAM输出没加三态门,导致FSMC总线上多个设备同时驱动,数据冲突。加上assign fsmc_data = (cs && !wr) ? ram_data : 16'hzzzz;后解决。
7. 分级存储方案的扩展与优化方向
这套方案在实际项目里跑了一年多,整体稳定。但有几个地方还可以优化。
7.1 增加FRAM作为EEPROM的替代
FRAM(铁电存储器)的擦写寿命是10的12次方以上,写入速度是EEPROM的几百倍,而且不需要写周期等待。如果成本允许,用FRAM替代EEPROM可以彻底解决寿命和速度问题。缺点是容量小(通常几KB到几MB),价格比EEPROM贵。
7.2 NOR Flash的坏块管理与ECC
虽然NOR Flash的坏块率比NAND低得多,但工业级应用还是要做坏块管理。可以在每个扇区的头部存一个坏块标记,启动时扫描一遍。另外,对关键数据加ECC校验(比如SECDED),可以纠正单比特错误,检测双比特错误。
7.3 SD卡的磨损均衡与健康监测
SD卡内部有FTL(闪存转换层)做磨损均衡,但消费级卡的FTL算法不透明,寿命不可控。工业级SD卡会提供健康状态寄存器,可以读取剩余寿命。如果卡不支持,可以在应用层做写入量统计,达到阈值时提示更换。
7.4 数据压缩与加密
对于波形记录这类数据,可以在写入前做压缩(比如LZ4或ZSTD),减少SD卡写入量,延长寿命。对于敏感数据,可以加AES加密,防止被读取。
7.5 双SD卡冗余
对可靠性要求极高的场景,可以用双SD卡做镜像。主卡写入的同时,数据通过FPGA或STM32的另一个SDIO接口写到备卡。主卡故障时切换到备卡,数据不丢。
这套分级存储方案的核心思想是让合适的介质干合适的事。EEPROM管关键参数,NOR Flash管高频记录,SD卡管大容量数据,FPGA管高速缓冲。每个环节都有冗余和校验,掉电也不会丢关键数据。实际部署时,建议先在实验室做掉电测试——用继电器反复切断电源,观察数据是否完整。这个测试能暴露90%以上的问题。