尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+FPGA工业控制器数据分级存储方案:EEPROM/NOR/SD卡实战

STM32+FPGA工业控制器数据分级存储方案:EEPROM/NOR/SD卡实战 做工业控制器的朋友大概率都纠结过一个问题数据到底该放哪儿尤其是STM32FPGA这种异构方案参数、日志、波形、固件全挤在一起选错存储介质轻则卡顿丢数据重则现场返工。这套分级存储方案核心就一句话——按数据的“性格”分家高频少量走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卡控制器按块突发写入。整个数据路径完全不经过MCUMCU只负责告诉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 8hFF) state CHECK_CRC; CHECK_CRC: if (spi_rx_data[4:0] 5b00101) state FINISH; FINISH: begin write_done 1b1; 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设备如果正在运动过程中突然断电当前位置、当前速度、当前平面的坐标系必须死死记住否则上电后设备不知道自己在哪可能直接撞机。我做的掉电处理顺序是这样的电源监测芯片输出掉电中断一般比主电源降到MCU最低工作电压早5ms以上。STM32立刻把FPGA采集中断停掉停止一切外部通信。将关键状态当前位置坐标、当前配方号、工序状态、实时时钟打包成一段不超过256字节的数据。写入EEPROM。如果一次写不完优先写最小必需集其余写NOR Flash。写完后置一个“掉电标志位”再进入低功耗或死循环等待。为什么不用NOR Flash做这一步因为NOR擦除太慢。W25Q128擦一个扇区大约需要100msEEPROM写一个字节才5ms而且写之前不需要擦。掉电窗口就几毫秒根本等不起一次扇区擦除。所以掉电快照必须优先进EEPROM除非你的板子上设计了足够大的超级电容能把供电拉长到几百毫秒。我实测过用2200uF电容搭配低功耗MCU方案大约能给STM32争取6~8ms的掉电工作时间。如果还想写NOR日志电容得加到10000uF以上成本明显上升。大多数场景下EEPROM写关键姿态已经足够。不过要特别注意掉电后再上电第一件事不是立即恢复运行而是先读掉电标志和EEPROM数据确认坐标有没有“半写”状态。如果CRC校验失败说明掉电那一下太急了EEPROM正在写一半就没了电这时候必须让设备进入手动回零模式而不是自动恢复。4.3 启动自检顺序不可乱分级存储做得再漂亮如果启动时没有一套完整自检现场早晚给你颜色看。我踩过最典型的坑是SD卡没插好开机后系统直接把旧数据当新数据用了还有一次是NOR日志区头部魔数错乱系统扫描时死循环。启动自检建议按下面的顺序来检测EEPROM是否可读校验CRC。失败则载入出厂默认参数并置“参数异常”报警。扫描NOR Flash日志区块头找到有效日志块重建当前写入偏移。如果魔数全错就不要扫描了直接格式化日志区并记录一条日志。检查SD卡是否存在、能否识别容量。用SPI模式的初始化命令逐个进行复位直到收到0x01或重试次数用尽。如果SD卡是文件系统方案检查根目录有没有目标记录文件。如果没有就创建并写一条开机记录。最后恢复该恢复的现场状态。这里有个小诀窍把上电自检结果也写进日志。很多人只会在故障时看日志但开机那几十秒的信息往往最有用。比如“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卡 EEPROMSTM32只做显示和上位机通信。数据流全部走FPGAMCU工作量很轻。纯FPGA方案没有MCU的时候EEPROM用Verilog I2C控制器挂参数SD卡用裸扇区写入配合一个软核跑FatFS做文件管理。选型口诀我总结成一句话参数进EEPROM日志进NOR波形进SD掉电关键量进EEPROM。6.2 最后分享一个习惯这套分级存储方案我做了不少版本迭代最大的体会是存储方案一定要在硬件设计阶段就定下来不要等软件写完了再改。尤其是掉电保护那一环涉及电容容量和PCB布局后补非常痛苦。如果你正在做一个带FPGA的控制器建议先列一张你自己的数据分类表把每种数据的频率、体积、掉电要求填上再对照这篇文章的结构设计基本就不会跑偏。最后再给一个小技巧NOR Flash和SD卡的驱动代码开个硬件抽象层把底层接口统一成ReadSector/WriteSector/EraseBlock。这样就算现场换介质比如把NOR从W25Q128换成MX25L128底层驱动改几行就行上面的日志、固件、文件缓存逻辑完全不用动。我吃过分立维护两套驱动的亏后来统一抽象层维护成本立刻降了一半。存储这件事前期多想一步后面能少踩一路坑。
返回列表