☰
FPGA多通道数据采集系统:千兆以太网UDP传输与上位机联调
2026/10/16 6:00:12 网站建设 项目流程

做一个多通道的数据采集系统,从硬件搭起来到最终上位机稳定显示、数据不丢,中间要踩的坑比想象中多得多。这篇是这个系列的最后一部分,前两篇分别讲了硬件选型与原理图设计、FPGA内部逻辑与ADC驱动,到了这一篇,重点放在把整个链路真正跑通:以太网数据传输、上位机软件、系统联调,以及那些不实际跑起来根本发现不了的问题。项目代号暂定为“某数据采集系统”,如果你正打算做类似的高精度多通道采集设备,这篇的经验可以直接抄作业。

基于FPGA的数据采集系统(三)终

1. 最终版系统架构与整体链路回顾

在进入联调细节之前,先把整个系统的最终形态交代清楚。很多做采集的朋友容易陷入一个误区:觉得ADC选得够好、FPGA逻辑写完了,系统就差不多了。实际上数据采集系统的复杂度是沿着链路往下走的——ADC只是源头,后面的存储、传输、解析、显示任何一个环节掉链子,整个系统都是废的。

我们这个项目最终定型的方案是:8通道同步采样,16位分辨率,最高采样率1MSPS,模拟输入范围正负10V,信号经过前端调理电路(衰减、滤波、保护)之后进入ADC。ADC通过LVDS接口将数据送入FPGA,FPGA内部完成串并转换、数据对齐、FIFO缓存和UDP协议栈封装,最终通过千兆以太网物理层芯片(PHY)输出到PC,由上位机软件完成波形显示、参数分析和数据存储。

整条数据链路是:

模拟信号 → 调理电路 → ADC(8×16bit@1MSPS) → LVDS → FPGA(串并转换/对齐/缓存/组包) → RGMII → PHY → RJ45 → 上位机

很多人会问:为什么数据率并不高(8×16bit×1MSPS = 128Mbps),不在FPGA内部加个大缓存,等采完再统一上传?其实这个系统要求的是长时间连续采集,不是抓一段波形就停。按16MB/s的持续流速,10分钟就是9.6GB数据,FPGA内部哪怕全部Block RAM都用上也存不了多少,所以必须实时往上传。这也是选择千兆以太网的根本原因:持续吞吐够高,协议实现比PCIe简单,驱动兼容性又好。

整个三篇系列的分工是:第一篇解决“信号怎么进来”,第二篇解决“数据在FPGA内部怎么流动”,这一篇解决“数据怎么出去、怎么展示、怎么不丢”。从系统角度看,每一层都是完整的闭环——硬件定了,逻辑定了,传输定了,上层显示也定了,这才叫一个能交付的项目。

2. 数据传输通道:千兆以太网UDP的落地

2.1 传输方案选型:为什么是UDP

数据采集系统的传输方案,直接决定了整个系统的实时性表现和开发工作量。我一开始考虑过USB 3.0,后来放弃了。原因很现实:FPGA上实现USB 3.0协议需要额外的高速收发器和复杂的协议栈,通常还得买第三方IP,成本不低;而且PC端写驱动就是个大坑——要么自己写WDM驱动,要么用通用驱动,通用驱动对自定义传输模式的适配很痛苦,调试周期按周算。

换成千兆以太网之后,一切变得清爽很多。FPGA厂商普遍提供成熟的PHY芯片支持方案,RGMII接口电平简单、引脚少,FPGA内部只需要自己实现一个精简的UDP协议栈。UDP本身是无连接的,FPGA侧不需要维护状态机,发完就完事,这对实时数据流来说反而是优势。

有人说TCP更可靠,丢了能重传,但数据采集场景恰恰最怕重传。实时数据流是“新数据一直在产生”的,重传旧数据只会造成乱序和延迟累积。在采集系统里,“宁可丢一帧,不能早一帧”——只要上位机能识别丢帧并做标记,就比TCP重传导致的时间线错乱更容易处理。以太网链路本身误码率极低,局域网直连环境下实测一整天几乎不会丢,所以UDP在这个场景里是合理选择。

2.2 FPGA侧UDP协议栈的轻量化实现

FPGA里做UDP协议栈,听起来高大上,拆开了其实就几层:MAC层(以太网帧)、IP层、UDP层。我们不需要支持ARP之外的网络管理功能,也不需要路由和分片,所以代码量比很多人想象的要少。

MAC层负责把数据封装成以太网帧:目的MAC、源MAC、以太网类型(0x0800表示IPv4)。IP层添加IP头:版本、首部长度、总长度、标识、TTL、协议号(17表示UDP)、源IP、目的IP,然后是校验和。UDP层更简单:源端口、目的端口、长度、校验和(可选,IPv4下UDP校验和可以为0,但我会建议算上它,因为调试时多一层校验就能多排除一类问题)。

这里有个工程上的重要决定:组帧用状态机逐字节发送,还是用DMA方式从FIFO搬数据?我们的数据流是持续高速的,逐字节发送会占用大量的逻辑资源且容易成为瓶颈。最终采用的是“整包搬运”思路:FPGA内部的数据FIFO攒够一帧数据(比如1472字节,正好是标准以太网MTU下的UDP最大载荷),就触发一次DMA式的读取,由发送状态机按固定时序把数据从FIFO中读出,拼上协议头,生成完整的以太网帧从RGMII接口发出去。

帧格式按照IEEE 802.3标准排布,每个字段都是固定的。协议头部分每次发送都一样(除了IP头中的总长度字段),所以可以预先存成固定模板,发送时直接拼装。这样做速度快,逻辑也简单。CRC校验交给PHY芯片或者RGMII下游的MAC控制器处理就可以,我们在数据链路层不去重复做。

2.3 跨时钟域处理与FIFO设计

FPGA内部其实有四个时钟域,这是很多新手容易忽略的:

  • ADC采样时钟域(1MHz)
  • 数据处理时钟域(50MHz,用于对齐与组帧)
  • RGMII发送时钟域(125MHz)
  • 配置时钟域(SPI配置ADC用)

ADC把数据送进来的时候,是自己在采样时钟边沿发出的,FPGA侧必须用同一个时钟去采集,这就形成了ADC时钟域。信号进入FPGA之后,要通过异步FIFO把数据从1MHz时钟域同步到50MHz系统时钟域,之后在50MHz时钟域完成组帧和打包,再经过另一个异步FIFO送到125MHz的以太网发送时钟域。

这里最容易出问题的不是FIFO本身,而是FIFO的空满信号。异步FIFO的空满判断需要格雷码跨时钟域传递,如果格雷码同步没做好,FIFO的状态判断会出错,要么提前报满丢数据,要么明明空了还在读。这是老生常谈的坑,但每次联调遇到数据莫名丢失,我都会先回头检查FIFO这部分——十次有八次问题就出在这。

FIFO深度的设计也有讲究。我最初规划的是每通道一帧数据128个采样点,8通道合计128×8×16bit = 16Kb,加上协议头,一帧UDP数据约8KB多点,配置一个16KB的异步FIFO就能缓存两帧。这个深度在持续流模式下够用,因为发送端和接收端都是连续工作的。但如果上位机偶尔卡顿,16KB大约只能缓冲1毫秒的数据,这就太浅了。后来我把FIFO加深到64KB,实测上位机即使卡顿到3~5毫秒也能撑住,丢帧率显著下降。FIFO深度不是越大越好,太大浪费Block RAM资源,太小承受不了抖动,要根据上位机的响应特性来折中。

3. 上位机软件:从“能收到数据”到“真正好用”

3.1 上位机架构:生产者—消费者模型

上位机如果只用一条Socket同步收数据再直接画图,100%会卡死。因为网络到达的数据是急促的瓶颈型流量——一帧数据忽然灌进来,然后短暂空白,下一帧又灌进来。UI线程如果直接去接收,界面连拖动窗口的响应都没了。

我采用的是经典的生产者—消费者模型,分成三个线程:

  • 接收线程:创建UdpClient,绑定网卡的某个端口,循环异步接收数据,收到后原样塞入ConcurrentQueue
  • 解析线程:从队列里取出数据,校验帧头(自定义的包头包含帧序号、通道数、采样点数、时间戳),去掉协议头,按通道拆分采样点,放入各通道的环形缓冲
  • 显示线程:定时从环形缓冲读取最新数据,增量绘制波形,更新统计信息

接收线程的优先级要高于显示线程,但接收线程内部不能做任何复杂处理,只做“搬运”。解析线程和显示线程之间通过环形缓冲解耦。这里的环形缓冲设计是个小技巧:使用定长数组加读写游标,写线程只更新写游标,读线程只更新读游标,游标通过原子操作更新,不需要锁。这样比用锁加队列的效率高很多,连续跑一晚上也不会因为锁竞争导致CPU占用上升。

3.2 实时波形显示与交互体验

显示这块,我用的是基于WPF(Windows Presentation Foundation)的自绘方案。有朋友建议直接用第三方图表控件,但我对第三方控件一直有些担心:数据量一大容易卡,授权也麻烦,自定义程度不够。自绘其实不复杂,核心就是Polyline加上点的动态更新。

显示上的一个关键设计是“增量绘制”而非全量重绘。每帧波形到达后,只更新画布上新增的那一段折线,而不是每次清空重画所有数据。这样300ms内积累的数据一次性更新,视觉上既流畅,CPU占用又低。最初版本是全量重绘,8通道×每通道4096点,刷新率才10fps,优化后30fps毫无压力,CPU占用从25%降到了9%左右。

显示帧率也要设置上限,我把它控制在30fps,也就是大约每33毫秒刷新一次。帧率再高,人眼其实已经感受不到流畅度提升了,反而白白消耗CPU。真要说“实时性”,刷一次的最新数据延迟取决于接收缓冲区排队时间,实际测试链路总延迟大约在10~30毫秒,这对绝大多数工业监测场景完全够用。

3.3 数据存储:二进制格式与双缓冲落盘

采集系统只显示不存数据等于没做,回放、离线分析都靠存储文件。我选择的是自定义二进制格式,开头是文件头(16字节,包含文件魔数、版本号、通道数、采样率、采样深度),接着是按固定长度分块的通道数据。为什么不存CSV?8通道1MSPS持续采集,1个小时就是57.6GB的数据点,存文本格式磁盘根本扛不住,而且解析也慢。二进制格式写起来是内存搬移的速度,分析时按偏移量随机访问也很方便。

落盘部分用了双缓冲机制:一块内存缓冲区写满之后,切换另一块继续写,同时把已写满的块异步写入磁盘。为什么需要双缓冲?因为磁盘写入是有延迟抖动的,如果同步写,缓冲区写满时必须等磁盘完成,链路就会出现短暂停顿,影响接收连续性。实际使用中发现,SATA固态盘持续写入带宽在2GB/s以上,完全不是瓶颈,但系统调度和IO队列偶尔会造成毫秒级延迟,双缓冲把这种抖动吸收掉了。

还有一个小细节:文件命名里带上了起始时间戳和文件内的帧序号范围。万一程序崩溃或者拔线,也能通过文件名定位异常时间段,排查问题会方便很多。

4. 系统联调实录:三个让人头秃的问题

4.1 问题一:LVDS通道数据错位

第一轮硬件联调,FPGA配置好ADC之后,通过内部逻辑分析仪(类似嵌入式逻辑分析仪)观察数据。发现8通道的数据波形中,有几个通道的信号完全乱了:输入一个干净的正弦波,观察到的却是类似阶梯的乱码,而且不同通道错乱程度还不一样。

排查过程是这样的:先确认ADC配置寄存器读写正常,排除SPI配置问题;再用示波器量LVDS差分对的眼图,发现信号幅度正常但建立保持时间很紧张。问题定位在FPGA内部的输入延时单元:LVDS数据线和时钟线的物理长度有差异,采样时钟采到的是数据跳变沿附近的电平,导致某些位采样不稳定。

解决方法是使用FPGA的输入延时单元(IDELAY)对每条数据线做逐位微调。我写了一个训练状态机,在数据链路空闲时向ADC发送特定的伪随机测试码,FPGA端尝试不同的延时tap值,直到所有位都与期望测试序列匹配时锁定配置。这个过程类似于高速串行通信的字对齐(word alignment),跑通之后把参数固化在配置里。实测最佳tap值范围很窄,而且与温度变化有关。后来我在设计里加了上电自动训练流程,每次启动时重新对齐,不同板子、不同温度环境下都能稳定工作。

这个问题的核心教训:LVDS不是简单的连线就能用,源同步接口的时序边界必须做训练和余量验证,否则就是“实验室能用,现场不定什么时候就抽风”。

4.2 问题二:UDP偶发丢包

系统跑起来之后,上位机发现偶发丢帧,大概每几百帧丢1~2帧。一开始怀疑FPGA发送端FIFO溢出,但用逻辑分析仪抓了发送计数,发现FPGA侧每一帧都正常发出去了,帧序号是连续的,说明丢帧发生在网络或PC端。

进一步排查PC端。用抓包工具观察网卡接收,发现其实数据包到达了网卡,但应用程序的Socket接收队列中出现了空洞——这是典型的接收缓冲区溢出。默认的操作系统接收缓冲区在千兆网突发流量下偏小,UDP没有流控,一旦上位机解析线程稍微卡顿,内核的接收缓冲被写满,数据包直接被丢弃。

处理方法是三板斧:

  • 把Socket接收缓冲区调大到64MB(通过设置Socket.ReceiveBufferSize)
  • 关闭网卡的节能以太网、绿色以太网以及中断节流合并
  • 让接收线程循环内不做任何阻塞操作,只往队列塞数据

改完之后,用满负荷8通道1MSPS跑了一整晚(12小时),帧序号连续性验证零丢失,CPU占用仅11%。

这里还有个小细节容易被忽略:如果用了网卡绑定的虚拟化功能、交换机又开启了巨型帧,每帧数据量不合适也会导致分片或丢包。我们最终在PC端和FPGA端都启用了9000字节巨型帧,UDP单帧载荷提升到8948字节,帧数减少了近6倍,发送端负担降低,接收端中断次数也减少,丢包率进一步下降。但启用巨型帧必须链路两端的网卡和交换机都支持,否则会出现无法通信的尴尬局面。

4.3 问题三:上位机界面卡顿与假死

第三个问题表现得很直接:数据接收一切正常,但界面窗口拖动时明显掉帧,波形刷新有时候会停顿半秒。

只有分清“接收线程卡”和“UI线程卡”才能对症。日志显示接收线程一直正常,数据都在队列里,问题是UI线程来不及渲染。原因是我最初在绘图时对8通道的2万多个采样点做了全量重绘,每次都要遍历重建图形对象,高分辨率下GPU和CPU都被拖累。

最终优化:

  • 改为增量绘制,只有新增数据才画新的折线段
  • 波形显示区域设置屏幕分辨率上限,超过上限先做抽稀再绘制
  • 显示线程休眠时间改为动态调整,如果当前帧渲染耗时超过33毫秒,自动降到15fps保证界面响应
  • 把用于波形绘制的一段数据固定为最新4096点,早期数据通过缩放查看,避免一次性画百万级点

这部分做完之后,开启8通道满载运行,界面依然流畅,拖动、缩放、切换通道都没有明显延迟。做采集系统的朋友值得在UI渲染上多花些功夫,硬件都做好了,软件卡顿会让整个系统显得非常业余。

4.4 整机实测指标

联调全部完成后,我做了一轮完整性能验收,核心数据如下:

指标项实测结果
通道数8通道同步采样
分辨率/采样率16bit / 最高1MSPS
数据传输方式千兆以太网UDP/巨型帧
持续吞吐8通道满载16MB/s,实测12小时0丢帧
端到端延迟信号输入到上位机显示约20ms
PC端资源占用4核CPU占用约11%,内存占用约400MB
输入电压范围正负10V(带过压保护)

这个结果达到了项目立项时的预期,甚至有富裕。实际上持续吞吐只用了千兆带宽的一成多,后续如果需要扩展通道数或者提高采样率,链路还有很大的升级空间。

5. 关键参数优化与调优思路

5.1 采样率、FIFO深度和带宽的匹配

系统设计时最容易犯的错是只算ADC采样率,不把整条链路吞吐算清楚。对于连续采集场景,任何一个环节的处理能力低于源端数据率,最终都会表现为丢数据。我这里给一个可以照用的计算框架:

源端数据率 = 通道数 × 采样率 × 位宽

本系统是8×1M×16bit = 128Mbps = 16MB/s。对应到以太网链路,还要附加协议开销。标准以太网帧1500字节,有效载荷1472字节,开销约1.9%;开了9000字节巨型帧后,开销降到0.6%左右。所以实际需要的链路带宽约16MB/s × 1.006 ≈ 16.1MB/s,远低于千兆以太网理论极限的118MB/s有效吞吐。链路余量非常充足。

FIFO深度的匹配则是看“系统能忍受多长时间的停顿”。假设FPGA发送端的FIFO是64KB,那么当上位机或者网络短暂拥堵时,FPGA内部的FIFO还能缓冲约4毫秒的数据(64KB ÷ 16MB/s = 4ms)。如果上位机停顿超过4毫秒,数据就无法挽回地丢失。理解了这一点,就能根据上位机软件的质量来选择FIFO深度:软件越不稳定,FIFO就要做得越深。这也是我最终把FIFO从16KB加深到64KB的根本原因。

5.2 以太网发送调度:成帧间隔与突发

粗调之后还有一个隐藏的优化点:UDP帧的发送调度。如果FIFO一有数据就立刻组帧发送,数据会变成小包高频突发,对接收端的中断处理和缓冲都是压力。更好的策略是“攒包”——等FIFO中的数据积累到接近MTU上限时再组帧发送。这样帧数更少、开销更低、接收端中断更少,整体带宽利用率更高。

我在FPGA里做了一个简单的攒包状态机:当FIFO中的数据量达到阈值(比如8500字节,配合巨型帧)或者超时定时器到点(比如10毫秒)时,才触发一次发送。超时定时器是为了应对低采样率下的实时性——如果采样率只有1kSPS,攒满8500字节需要好几十秒,显然不能等,这时候靠超时机制保证最低的帧发送频率和实时性。

这个设计与协议栈的流量控制类似,实现不复杂,但对系统性能的影响立竿见影。实测攒包后同样的数据流,每秒产生的以太网中断从大约1100次降到了150次以内。

5.3 上位机接收缓冲的细节调优

Socket接收缓冲区设置成64MB后,丢包问题基本解决,但这里有个副作用:过大缓冲会增大延迟。假设接收缓冲里积压了32MB数据,那么最新数据要等队列里旧数据先被取出才能被应用读到,延迟会达到秒级,完全违背实时监测的需求。

所以调优是一个平衡:缓冲区不能大到延迟不可控,又不能小到容易被瞬时流量冲垮。在16MB/s数据率下,我最终选择了8MB的Socket接收缓冲,对应约500毫秒的缓冲容忍度,同时将应用层队列长度限制在6400帧。如果应用层队列超过这个限制,说明解析线程或者存储IO出现了问题,这时宁可标记丢帧,也不要让数据无限积压导致延迟飙升。

这个策略本质是“显式丢帧,保证新鲜度”:数据采集系统要的是最近时刻的准确数据,而不是可能几分钟前的历史数据。收到旧帧如果有分析需求,直接从磁盘回放即可,实时链路保证新鲜才是第一位的。

6. 常见问题速查与避坑清单

这节整理联调过程中遇到的典型问题,按表现、原因、排查方法和解决方案列成速查表,方便后续维护时快速对照:

现象可能原因排查手法对策
波形出现阶梯状乱码LVDS位对齐错误逻辑分析仪观察数据、调整IDELAY tap增加上电训练/字对齐流程
偶发丢帧但FPGA发送正常PC接收缓冲溢出抓包对比帧序号加大Socket缓冲区、关闭网卡节能
上位机界面卡顿UI线程全量重绘分离线程统计耗时增量绘制、限制刷新率
采样值整体偏移ADC参考电压不准万用表测基准电压使用精密基准源、校准偏移
偶发数据毛刺电源噪声耦合示波器量电源纹波加强去耦、改善PCB走线
长时间运行温度漂移IDELAY随温度变化监控误码率变化定期重训练或加温控补偿

再补充几条独家的工程经验:

  • 每块板卡上电时都执行一次LVDS自动对齐训练,不要手工写死tap值。不同板卡由于PCB工艺偏差,最佳采样点可能差好几个tap,写死了换板就出问题。
  • UDP帧头里加上帧序号和时间戳不要省。帧序号是排查丢包的唯一可靠依据,时间戳用于端到端延迟分析和多通道时间对齐,这两个字段占不了几字节,价值却非常大。
  • 联调时不要一上来就满速率跑。先把采样率降到100kSPS验证通路正确,再逐步提高,否则数据错乱了都不知道是哪一环造成的。
  • 上位机的解析模块要能独立模拟FPGA发送(回放文件),这样才能分开调显示和调网络,否则同一个bug还得两头猜。

7. 后续扩展与个人体会

系统现阶段功能完整,但继续往里走还有很多可以做的方向。比如加入触发采集模式,FPGA内部实现电压阈值触发和预触发缓存,这样捕捉异常波形时不用存几GB的无用数据。又比如把上位机的数据处理从软件搬到GPU上做FFT和频谱分析,实时性会再上一个台阶。还考虑过把存储格式向通用格式对齐,这样采集出来的数据可以直接进主流分析软件,减少后期转换的麻烦。

但坦白说,在动手扩展之前,我会先把系统做成“文档完整”状态。硬件设计中的时序约束、时序报告、原理图签名,FPGA代码里的关键状态机说明,上位机通信协议的版本管理——这些我在项目初期都不太重视,到了调试后期才发现没文档寸步难行。有一次改了帧格式,忘了同步更新上位机的解析代码,联调浪费了一整天。从那之后我养成了习惯:每次改协议,必定先把文档改了再改代码。

这个项目从零到跑通,前后大概花了三个月。硬件设计两周,FPGA逻辑四周,上位机三周,剩下时间全部砸在联调和优化上。让我重新做一次的话,硬件和逻辑部分的估时基本不变,但上位机我会上来就按“高并发流式处理”的思路写,而不是先写个能跑的版本再去优化——很多架构问题是后期改不动的。

最后再分享一个小技巧:联调阶段一定要留一个可读的操作日志系统,FPGA侧的帧计数、丢帧计数、FIFO空满标志,上位机侧的接收队列长度、处理耗时、丢帧计数,这些要能一边跑一边刷新出来。这个项目最终能顺利交付,很大程度上取决于我能同时看到链路两端的状态指标,任何异常都能快速定位到具体环节。数据采集这种系统,黑盒子状态下调试无异于盲人摸象,把自己变成透明盒子才是最快的排查路径。

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

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

立即咨询