
做数据采集这件事很多团队都绕不开一个经典需求把ADC采到的模拟信号实时送到电脑上一边看波形一边存数据。早期我都是用STM32加串口搞定但一旦采样率超过几百kSPS、通道数变成两路以上、或者现场环境让电脑离设备几十米远串口这套方案就捉襟见肘了。换成FPGA加以太网是迟早的事。这篇东西我按自己实际做过的一套系统来写FPGA控制双通道ADC以1MSPS采样数据经异步FIFO缓冲在FPGA内部用状态机封装成UDP包通过RGMII接口送给PHY芯片最终从RJ45网口发到PC上位机用C#写的程序实时显示波形并落盘保存。整个过程涉及到了ADC接口时序、跨时钟域处理、精简TCP/IP协议栈实现、PHY芯片调试、上位机解析与显示踩过不少坑正好整理出来给准备做类似数据采集系统、或者刚把FPGA接进网络通信的朋友做个参考。1. 系统整体设计思路先想清楚再动工1.1 需求拆解采样率、传输距离、实时性三个指标定方案任何一个采样传输系统第一件事不是选芯片而是把指标定死。我这次的需求比较典型两路模拟输入采样率1MSPS分辨率12bit数据要实时传到上位机显示波形的同时要能连续存储半小时以上。电脑和采集设备之间隔着大概二十米现场还会有电机启停这种干扰。这套指标提炼出来就是三个核心问题。采样率决定ADC型号和FPGA资源消耗距离决定必须走以太网而不是USB线直连实时性决定协议层面要牺牲一部分可靠性换速度。很多人上来就想到用TCP觉得可靠但TCP握手重传机制在嵌入式实现里非常麻烦而且1MSPS下数据量并不大UDP加校验加序号完全够用。选型之前把这三个问题想明白后面所有工作都会顺很多。1.2 方案选型从串口升级到FPGA加以太网的理由为什么不用STM32内置MAC加PHY的方案我也考虑过STM32F407这些芯片本身就带MAC外挂一个PHY芯片就能跑以太网。但问题在采样这一侧1MSPS采样率听起来不高可是ADC的数据是连续不断的STM32需要在采样中断、DMA搬运、以太网协议处理之间来回切换CPU占用率一高采样时序就容易抖波形显示出来就有毛刺。FPGA的优势是天然并行采样、缓冲、组包、发送各干各的互不干扰。还有一条路是用FPGA软核比如MicroBlaze或Nios II在软核里跑lwIP协议栈。这个方案开发效率高但入门门槛其实更高因为你要同时掌握软核配置、AXI总线、中断机制出问题不好排查。我最终选的是纯Verilog状态机实现精简UDP协议栈理由很简单UDP头加IP头加MAC头总共就42个字节用状态机发送完全可控代码量不大而且每一帧从组装到发出都是确定性的这一点对实时采集系统特别重要我可以精确知道数据从ADC到网口的延迟。1.3 整机数据流和各模块分工系统整体分成四个大模块。第一个是ADC采样控制模块负责给ADC提供采样时钟、读取转换结果第二个是数据缓冲模块核心是一个异步FIFO解决采样时钟域和逻辑时钟域之间的数据交换第三个是以太网发送模块内部包含MAC、IP、UDP协议的状态机第四个是上位机软件负责UDP接收、解析、波形绘制和存储。数据流走向是这样的模拟信号进入ADCFPGA按设定速率启动转换并读出12bit数据拼接成字节后写入异步FIFO以太网发送模块检测到FIFO里攒够一帧数据量就按UDP/IP协议格式组帧通过RGMII接口送到PHY芯片PHY完成编码和线路驱动从网口发出去PC端上位机绑定了固定端口收到UDP包后解析出采样数据和序号更新显示波形。这个链路里最容易出问题的点不在FPGA逻辑本身而在跨越时钟域的那一下和以太网PHY的时序配置这两块我后面详细说。2. ADC采样链路实战从接口选型到数据可靠落地2.1 ADC接口怎么选SPI、并行还是LVDSADC和FPGA之间的接口方式直接决定了采样控制模块的复杂性。三种常见接口我有过实际对比。SPI接口最简单几根信号线就能搞定但SPI协议一帧一帧串行读取适合采样率不高几百kSPS以内且不要求严格同步的场景。并行CMOS接口是12根数据线加一个转换时钟每个时钟周期出一个数据吞吐高但引脚占用也多适合中速场合。LVDS接口速度最快、抗干扰最好但要处理SerDes解串FPGA内部资源消耗大市面上高速ADC基本都用这种。我这次用的是并行CMOS接口12bit数据线加一个采样时钟输出1MSPS下毫无压力。如果你选SPI接口的ADC要注意SPI时钟频率必须大于采样率乘以位宽比如1MSPS采样率、12bit的数据SPI时钟至少得12MHz以上留余量建议跑20MHz。2.2 采样时钟和FPGA时钟管理容易被忽视的细节ADC采样时钟的质量直接决定采样精度这一点做高速采集的人体会最深。我用MMCM把板载50MHz晶振倍频到100MHz作为系统逻辑时钟同时分频得到1MHz采样时钟送给ADC。这个做法本身没问题但要注意两件事。第一MMCM输出的时钟抖动会叠加到ADC采样时钟上。对12bit ADC来说理论上采样时钟抖动需要控制在几十皮秒以内FPGA内部MMCM的抖动指标通常能满足但如果你从外部引脚直接引时钟给ADC务必在PCB上做包地处理别让数字信号干扰到采样时钟。第二ADC转换结果在采样时钟边沿变化而FPGA内部逻辑跑100MHz采样时钟是1MHz两者完全异步。直接把ADC数据线接到FPGA逻辑里采大概率隔一段时间就采到一个中间电平转换过程中的毛刺值。正确做法是先用IOB寄存器输入输出块寄存器在采样时钟沿把数据打一拍再做跨时钟域处理。这一拍非常关键它是FPGA工具链能正确约束时序的基础。2.3 异步FIFO深度怎么算实例演示ADC产生数据是连续的但以太网发送是突发性的攒够一帧就发一帧几百字节发完再攒下一帧。两个环节之间的节奏完全不一样必须用异步FIFO做缓冲。FIFO深度怎么算公式其实很简单最大写入速率减读取速率乘以最大突发持续时间。我给的参数是这样算的系统逻辑时钟100MHzFIFO读侧每个时钟周期最多读1字节读带宽接近100MB/s远大于写入带宽2MB/s1MSPS乘2通道乘2字节。UDP负载我定在每次发送1024字节按2MB/s的速率攒满这一帧需要约0.5ms。FIFO最坏情况下要存下这0.5ms内积累的所有数据加余量也就是2MB/s乘以0.5ms等于1KB。我再给三倍余量最终选了4KB深度的异步FIFO实际跑下来从来没有溢出过。2.4 数据编码处理偏移二进制转二进制补码并行CMOS接口的ADC输出通常是偏移二进制码全零对应负满量程中间值对应0V全一对应正满量程。而FPGA内做运算和上位机显示波形时带符号数用起来顺手得多。转换方法很简单最高位取反。12bit数据把最高位翻转一下就变成二进制补码直接可作为有符号数使用。assign signed_data {~adc_data[11], adc_data[10:0]};这一行代码花了我当时大概十分钟但后面省了很多事。上位机收到数据后不用再纠结偏移量直接按Int16解析就行。3. 以太网发送链路从MAC到UDP的全流程实现3.1 RGMII接口和PHY芯片选型讲究不少FPGA和PHY芯片之间的接口我选了RGMII4根数据线加时钟DDR双沿采样速率1000Mbps。相比GMII的8位数据线加125MHz时钟RGMII省了一半引脚布线压力小。选PHY芯片时要留意两件事一是MDIO管理接口必须可配置方便读写PHY寄存器二是RGMII的时钟延时模式最好支持内部延时省掉PCB上绕等长线加的延时。芯片我选了瑞昱的RTL8211系列资料多、价格适中、10/100/1000M自适应。这里有个最常见的坑RGMII发送时钟和数据线之间要求约2ns的相位差如果PHY不支持内部延时PCB上必须把数据线比时钟线绕长约4毫米。我在第一版板子上就吃了这个亏时钟和数据边沿完全对齐结果PHY状态机偶尔能通偶尔不通后来通过配置PHY寄存器0x0014打开内部延时选项才彻底解决。3.2 状态机实现精简UDP协议栈代码思路全拆解很多朋友一听到“在FPGA里实现协议栈”就觉得高不可攀其实对于固定接收端、固定发送端的应用根本不需要完整协议栈。我要做的就是把UDP包、IP包、以太网帧的头部拼起来加上数据载荷按序发送出去即可。发送端总共只有四种帧ARP请求的应答帧如果上位机需要先Ping通设备、UDP数据帧其他一律不管。以UDP数据帧为例帧结构是这样的前导码7字节加帧起始定界符1字节、目的MAC地址6字节、源MAC地址6字节、以太网类型0x0800IPv4、IP头20字节、UDP头8字节、负载数据、帧校验序列4字节。其中帧校验序列如果PHY芯片支持可以自动附加RTL8211在配置后可以开启这个功能减轻FPGA负担。我用一个状态机按顺序把这些字节发送出去。核心思路是建立一个发送缓冲区头部字段提前算好填进去发送时按字节计数依次输出。头部里需要我计算的只有IP总长度、UDP长度和校验和。IP校验和是16位反码求和UDP校验和如果暂不处理可以填0但在局域网内最好还是算上否则部分路由器会丢弃。// UDP发送状态机简化示意 localparam S_IDLE 3d0; localparam S_PREAMBLE 3d1; localparam S_MAC 3d2; localparam S_IP 3d3; localparam S_UDP 3d4; localparam S_DATA 3d5; localparam S_END 3d6; always (posedge clk) begin case (state) S_IDLE: if (fifo_empty 1b0 tx_busy 1b0) begin state S_PREAMBLE; byte_cnt 0; end S_PREAMBLE: if (byte_cnt 7) begin state S_MAC; byte_cnt 0; end else byte_cnt byte_cnt 1; // 其余状态按同样模式推进 S_END: state S_IDLE; endcase end3.3 上位机通信协议设计帧格式和包序号数据能不能稳定传送关键在上位机和FPGA之间约定好帧格式。第一次做这系统的人容易犯的错误是直接把原始采样数据丢到UDP负载里上位机根本不知道这一包数据从哪开始、中间有没有丢包。我设计的帧格式如下帧头0xAA55占2字节、数据长度占2字节、包序号占2字节、通道标识占1字节、时间戳占4字节、采样数据N字节、CRC16校验占2字节。包序号是整个设计里最值钱的一个字段。上位机通过比对连续两包的序号差能立刻知道中间丢了几个包。这个信息对判断系统稳定性太重要了。实测里我跑了一个小时百万包级别只出现过一次序号跳变后来定位到是FIFO读使能偶尔因时序违规丢了一个读脉冲。没有序号字段这种问题几乎是隐形的。3.4 带宽怎么算先心里有数1MSPS采样率、2通道、12bit量化用2字节存储数据量是1M乘以2乘以2等于4MB/s也就是32Mbps。加头部封装开销约3%总带宽需求33Mbps左右。这个量级百兆以太网都稳稳够用千兆更是杀鸡用牛刀。这个计算看似简单但一定要做在前面我见过一个项目非要用千兆加万兆交换机结果瓶颈根本不在带宽而在上位机接收缓冲没调好。4. 上位机实现C# UDP接收、解析和波形实时显示4.1 上位机框架怎么选C#依然是效率最优解上位机可选的路子很多LabVIEW拖拽式开发上手快但部署要装运行时程序集臃肿Python加matplotlib灵活但实时性一般数据量一大界面容易卡顿C#加WinForms在工业数据采集领域用得最久性能足够、部署方便、和硬件打交道时遇到问题的资料最全。我做这套系统选了C#开发环境VS2019目标框架用.NET Framework 4.5。顺带回答一个经常被问的问题VS2019开发的C#上位机源代码能不能用VS2015打开答案是只要项目文件里的目标框架版本不高于VS2015自带的.NET Framework版本最高4.6.2就完全可以打开。具体操作是把.csproj文件里的TargetFrameworkVersion改成4.5或4.6.2再把解决方案文件和项目文件的格式版本改成老版本兼容的格式通常是Visual Studio 2013或2015的格式VS2015打开编译基本无障碍。4.2 C#上位机三块核心代码不能省第一块是UDP接收。Socket默认缓冲区可能偏小紧接着就要处理丢包所以必须调大接收缓冲。我直接设到了4MB实测1MSPS双通道数据流的丢包率能到零。UDP接收用异步方式避免界面线程阻塞。udpClient new UdpClient(LISTEN_PORT); udpClient.Client.ReceiveBufferSize 4 * 1024 * 1024; udpClient.BeginReceive(ReceiveCallback, null);第二块是数据解析。收到字节数组后按帧格式逐字段解析重点检查帧头、包序号是否连续、CRC校验是否正确。数据区按小端序转成short数组放入一个环形缓冲区供绘图线程取用。第三块是波形绘制。WinForms的Chart控件双缓冲处理1万点级别没问题但在1MSPS下要显示全貌必须做抽稀。我的做法是固定显示10万个点数据超过后按比例抽稀把所有数据分成10万个区间每个区间取最大值和最小值把这两个点都画出来。这样波形不管是看细节还是看整体轮廓都不会失真。4.3 联调技巧先看通不通再看准不准联调阶段最容易让人崩溃的是数据乱码和波形对不上。我的顺序是这样的先用网口调试工具代替自写上位机把数据存成二进制文件再用Python脚本解析打印前几十个字节确认FPGA发的协议解析正确后才开始写C#显示界面。这样一旦波形显示有问题可以立刻排除是协议问题还是显示问题。5. 调试实录从网口不通到稳定跑十天5.1 分步排障链路通不通逐一确认整个系统调试我排障顺序是固定套路。第一步看PHY链路层连接好网线后用命令查看本地连接是否已连接任何一端灯不亮先查网线和PHY芯片配置。第二步看FPGA是否发出了数据帧用抓包工具抓网卡上的流量能看到发出的UDP包就说明FPGA到PHY的链路是通的。第三步验数据内容把收到的包按协议解析确认帧头、长度、序号、校验都正确。走到这一步剩下的只是显示层面的优化。5.2 踩坑记录那些让人抓狂的离奇问题第一坑PHY配置寄存器写入不生效。排查很久发现是MDIO时序的输出延时不够PHY芯片在时钟上升沿要求数据保持至少10ns我的状态机在时钟沿同时撤销了数据线导致寄存器写入偶发失败。解决办法是先改数据再拉时钟或者把MDIO时钟频率降到2.5MHz以下。第二坑FIFO读数据偶发少一个字节。这是典型的跨时钟域亚稳态问题异步FIFO的读侧信号和写侧信号之间没有做同步。换成Xilinx原生的异步FIFO IP后这个问题消失教训是异步FIFO这种成熟模块自己写代码踩坑的概率远大于省掉的许可费。第三坑上位机长时间运行后内存持续增长。定位到是Chart控件内部缓存没有清理每帧数据都往Series里塞。解决方法是限制显示点数超过上限就从头覆盖同时定时调用Invalidate强制刷新。第四坑RGMII时钟延时没配置时数据偶发错位波形看起来像是“信号被压缩了一半”。这个就是前面说的2ns相位差问题配置PHY内部延时后彻底消失。5.3 性能实测数据和使用建议系统稳定后我做了一组实测24小时连续运行以1MSPS双通道12bit采集UDP每帧1024字节负载帧率约4000帧每秒总收包数约3.5亿丢包数为0CPU占用率在上位机约12%内存占用稳定在500MB以内。整套系统的瓶颈完全不在FPGA和网络而在上位机的磁盘写入速度所以我建议存储时用SSD或者采用分段写文件的方式每10分钟一个文件避免单个文件过大导致读写缓慢。如果你要对这套系统做进一步扩展方向其实很明确一是把通道数扩到8路以上FPGA这种架构天然适合多通道扩展只要FIFO带宽够其他逻辑几乎不用动二是加触发功能在FPGA里做电平触发和边沿触发满足瞬态信号采集需求三是考虑使用时间戳同步多台采集设备拼接成更大的同步采集系统。这些都是在现有架构上迭代可以完成的不需要推翻重来。