FPGA多通道同步数据采集系统设计与Verilog实现
发布时间:2026/9/8 22:01:26 作者:尧图编辑部 阅读量:1,286

简介基于FPGA的多通道数据采集系统Verilog设计资源面向电子工程、嵌入式系统开发者及FPGA学习者聚焦8通道16位AD采集与UART串口传输的完整实现方案。资源包共112个文件压缩包大小2.64MB包含Verilog源文件.v、Quartus工程配置.qpf/.qsf、仿真波形.vwf、约束文件.sdc及综合报告.rpt等覆盖从代码编写、功能仿真到FPGA下载调试的典型流程。已有698人学习下载。用户可从中获取模块化Verilog代码、UART时序设计、AD转换控制逻辑及测试平台脚本有助于理解多通道并行采样、同步处理与串行通信协议的具体落地压缩包内还包含设计文档与运行截图便于对照验证。整体适合希望以工程案例切入数字系统设计的开发者参考。 手头这个项目做下来前前后后花了大概两个月时间。最初接到需求的时候对方只提了一个大概方向要用FPGA做一套多通道数据采集系统通道数不能少采样率要够用数据要能存下来还要能传走。听起来不算复杂但真正把架构定下来、把Verilog代码一层层写下去才发现里面坑不少。这篇就当作一次完整复盘把从方案选型到模块划分从仿真验证到板级调试中间的关键细节和踩坑记录都梳理一遍给准备做同类项目的朋友一个参考。1. 项目整体设计思路与方案选型1.1 为什么多通道采集要选FPGA而不是MCU很多第一次接触多通道采集的人会先问STM32不是也有多路ADC吗为什么非要上FPGA这问题确实值得先说清楚。MCU的ADC哪怕有十几个通道本质上还是通过模拟多路开关轮流切换去采每个通道之间天然存在时间差。低速场景无所谓比如采温度、采电池电压毫秒级的时间差根本不敏感。但如果是振动信号、电流波形、超声回波这类需要多通道同步对齐的场景通道间哪怕差几十微秒后续做FFT或者互相关分析的时候就会引入明显相位误差。FPGA的解法不一样。它可以把多片ADC同时挂上去每个ADC独立并行工作同一时刻对所有通道完成采样。这个“真同步”是FPGA方案最核心的价值。再加上FPGA本身擅长流水线处理和并行运算数据进来之后可以一边缓存一边做抽取、滤波、格式转换能分担上位机的预处理压力。我这次做的系统就是8通道同步采样单通道采样率1MSPS位深16bit数据进来之后先做有限脉冲响应滤波再打包上传。这套流程如果全部丢给MCU光采数据就够呛更别提滤波和组包。1.2 系统整体架构与关键指标先给出这套系统的完整链路传感器信号 - 信号调理电路 - ADC - FPGA - DDR3缓存 - 以太网 - 上位机。FPGA在中间承担所有时序控制、数据搬移和接口协议是整个系统的核心。指标方面对方给的初版需求比较模糊后来逐步敲定成下面这组参数基本能覆盖绝大多数工业数据采集场景参数项设计值通道数8通道同步采样单通道采样率最高1MSPS可配置分频ADC位深16bit数据缓存板载DDR3容量1GB突发读写上传接口千兆以太网UDP自定义协议FPGA主频200MHzDDR3用户接口域功耗预算整板≤5W为什么要定1MSPS这是权衡过的再往上走比如做到5MSPS以上ADC和信号调理成本会明显上涨而且上位机做实时存储的压力也大。1MSPS配合16bit精度跑大多数工业现场数据采集都够。DDR3的作用是做一个大缓冲池避免采样过程因为网络拥塞而丢数。1.3 FPGA选型与器件对比FPGA选型这步很多人觉得不复杂实际上决策点不少。核心看三样逻辑资源够不够、IO数量和电气标准对不对得上、有没有硬核存储控制器。这次项目我对比过三个候选Intel Cyclone V系列、Xilinx Artix-7系列、国产高云系列。Cyclone V在成本和片上资源均衡性上都不错但DDR3硬核配置略麻烦Artix-7的资料和生态最好DDR3控制器IP成熟但当时供货周期偏长高云那边性价比高但高速以太网MAC需要自己多花时间调。最后出于项目周期考虑选了Xilinx Artix-7因为它的DDR3和以太网方案我都有现成参考踩坑成本低。选型建议就一条别只看逻辑资源表务必把你要用的接口IP一个个确认清楚比如DDR3控制器、Ethernet MAC、PCIe硬核确认在选定器件上有足够授权和参考设计再动手。我见过不少人资源算好了后来发现需要的IP在新封装上不支持被迫改板那个代价是真不小。2. 核心模块划分与Verilog细节实现2.1 系统时钟与复位设计千万别图省事多通道采集系统里同时存在多个时钟域ADC采样时钟域、FPGA内部用户逻辑时钟域、DDR3接口时钟域、以太网时钟域。每个时钟域单独管理互相之间必须经过同步处理。刚开始做这类项目时容易犯的错误是图省事直接用同一个时钟生成复位给所有模块结果就是异步复位在时钟沿附近释放时出现亚稳态整套逻辑偶尔“抽风”却查不出来。这里采用的是一套常规但稳妥的做法外部50MHz晶振进入FPGA后通过PLL生成三路时钟100MHz给ADC控制逻辑、200MHz给DDR3用户接口、125MHz给以太网。复位全部用“异步复位、同步释放”的标准结构。跨时钟域信号一律走异步FIFO或者两级寄存器同步不允许裸信号直接跨域。我这次为了省资源一度想在跨域信号上只打两级寄存器就算了直到有一次采样数据和帧同步信号出现随机错位排查半天才意识到是跨域时序问题。从那之后老老实实按FIFO来。异步复位同步释放的标准写法如下这个结构建议直接背下来module reset_sync ( input wire clk, input wire rst_n, output wire rst_out_n ); reg rst_r1 1b0; reg rst_r2 1b0; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_r1 1b0; rst_r2 1b0; end else begin rst_r1 1b1; rst_r2 rst_r1; end end assign rst_out_n rst_r2; endmodule2.2 ADC接口逻辑SPI时序状态机这次选的是8通道同步采样ADC接口方式是标准SPI实际是类SPI的四线制。每个转换周期ADC把8个通道的数据在SCLK驱动下依次移出。FPGA要做的就是精准控制CONVST信号启动转换然后产生SCLK把数据全部读回来。SPI读时序用一个状态机来管比较清晰。状态依次是IDLE - START_CONV - WAIT_CONV - READ_DATA - PARSE - IDLE。其中WAIT_CONV要严格遵守ADC手册里的转换时间不能提前去读数据否则读回来的全是无效电平。这个等待时间在代码里最好做成参数可配因为不同批次ADC的转换时间上限略有差异做成常量后期调起来很痛苦。parameter IDLE 3d0; parameter START_CONV 3d1; parameter WAIT_CONV 3d2; parameter READ_DATA 3d3; parameter PARSE 3d4; // READ_DATA 状态下的核心逻辑 reg [3:0] bit_cnt; reg [15:0] shift_reg [0:7]; always (posedge sclk or negedge rst_n) begin if (!rst_n) begin bit_cnt 0; end else if (state READ_DATA) begin if (bit_cnt 16) begin shift_reg[channel][15-bit_cnt] sdata_in each channel; bit_cnt bit_cnt 1b1; end else begin bit_cnt 0; end end end需要注意的是SPI读回来的数据每一位都在SCLK的上升沿采样。写这段逻辑前一定要查清楚ADC手册里对SCLK高电平宽度和建立保持时间的要求。2.3 跨时钟域与异步FIFO深度计算ADC采样时钟域是1MHzDDR3用户接口时钟域是200MHz两者差了两个数量级。直接跨过去肯定不行必须插一个异步FIFO。FIFO在这里的作用不只是隔离时钟域更是担任一个缓冲池吸收突发写入和突发读出的速率差距。FIFO深度不是拍脑袋定的需要算。最坏情况是ADC连续不停写而DDR3那边因为带宽被其他模块占用而延迟响应。设ADC单次突发写N个采样点每个采样点2字节8通道就是16字节。DDR3读侧在极端情况下可能延迟K个时钟周期才能响应一次读请求。留给FIFO的余量要考虑完整。我给这套系统算的深度是4096 * 16bit。算法是这样ADC最大连续写入时间 4096个采样点 / 1MSPS ≈ 4ms这段时间内写入的数据量是4096 * 16bit而DDR3侧只要在4ms内完成至少一次批量搬移就能避免溢出。实际上DDR3的搬移速度远快于此所以4096深度已经留了很大余量。后来实测满载跑了一整天FIFO水位最高只到过60%左右。2.4 DDR3读写控制与状态机设计DDR3控制是整个系统里最难啃的骨头。当然Xilinx提供了MIG IP核底层物理层和时序训练这些都不用自己搞但用户接口那边依然有一套自己的协议写数据需要先发写命令和写地址再以固定节拍吐数据读数据则是发出读命令后要等若干周期才能拿到读数据。这中间的关系如果搞不清楚写出来的逻辑会各种“碰壁”。我这边DDR3读写控制用一个主状态机来调度初始状态等待用户逻辑发送读/写请求。收到写请求后跳转到写命令状态输出写地址、写长度然后把FIFO里的数据按MIG接口时序搬过去。写完后跳回空闲继续等待。读请求类似读出的数据直接进以太网发送FIFO。刚开始调的时候闹过一个低级但典型的笑话我把写命令发出后立刻开始往写数据总线上灌数据结果MIG一直报写数据握手错误。后来仔细看文档发现MIG的数据写使能信号wr_data_en需要等写命令有效之后的若干周期才拉高这个时序对不上数据就丢了。这类问题没有捷径只能一个信号一个信号对着时序图抠。3. 仿真验证与在线调试环境搭建3.1 Testbench设计模拟一个ADC行为模型写TestbenchTB的时候不要只是给几个简单信号就了事最好连一个完整的ADC行为模型一起写上。所谓行为模型就是在仿真里模拟真实ADC的工作方式收到CONVST拉高后等待一个转换时间然后开始按SCLK把你的测试数据一位一位吐出来。这样才能在仿真阶段验证FPGA控制逻辑是否和真实器件兼容。比如模拟一个正弦波数据源的ADC模型可以用$sin系统函数生成理想正弦值再转换成正负的补码数据输出。这一步很重要后续做数据滤波或FFT处理的算法验证都得依赖它。等模型调通了再换真实硬件问题就只剩下板级信号质量而逻辑层面的问题基本都在仿真里解决完了。3.2 仿真数据把结果保存成文件做FPGA开发仿真数据不落地等于白跑。跑一次仿真波形上肉眼看几个周期还行要分析几百上千个采样数据的规律肉眼根本看不出问题。我的习惯是直接把关键数据用$fwrite写进文本文件然后再用脚本或Matlab做后处理。integer file_handle; initial begin file_handle $fopen(adc_data.txt, w); end always (posedge clk) begin if (valid_out) begin $fwrite(file_handle, %d %d\n, ch0_data, ch1_data); end end写成文本之后用Excel或者Python随便画个图波形长什么样立刻清清楚楚比在波形窗口里放大滚动方便多了。这套工作流强烈建议早点建立起来。3.3 ILA在线调试别一上来就抓全部信号上了板子免不了要在线调试。Xilinx环境里就是ILA把它看成是一个可嵌入FPGA的逻辑分析仪。使用上有几个经验值得说第一每次只抓和核心问题相关的信号别把什么信号都拖进去因为ILA的存储深度有限抓太多信号等于每个都抓不久很快就截断了第二要设置合适的触发条件比如用FIFO的满信号做触发就能看到“满”之前和之后各发生了什么分析溢出的根因很有用。第三触发位置建议放在中间不要放在开头保证能看到触发前和触发后各一半的数据。ILA我最终用来排查了一个“周期性数据错误”的问题。现象是数据流每间隔一段时间就出现连续几十个错误采样点用ILA观察后发现每当DDR3那边执行一次刷新操作自动刷新恰好冲突了ADC写入FIFO的时机造成了数据覆盖。这个在纯仿真阶段很难暴露因为在仿真里DDR3刷新模型往往被简化掉了。换到板子上用ILA观察实际问题原因几分钟就定位到了。4. 常见问题与排查技巧实录4.1 通道间数据串位现象第3通道的数据偶尔出现在第4通道的位置上。第一次遇到时排查了很久从寄存器配置到数据链路查了一遍最后才发现问题出在ADC的帧同步信号上。ADC会在转换完成通过一个数据帧同步引脚给出标志信号FPGA靠这个信号来确认一帧数据的边界。由于这个信号是从ADC时钟域过来的跨到FPGA逻辑时钟域的时候没有做充分同步导致偶尔出现一个周期的抖动帧边界就被认错了一位。解决办法很简单对这个帧同步信号做打拍同步同时要求ADC侧设置延迟输出保证边界稳定后再采样。4.2 高速率采集时数据随机丢点这个问题的特征是采样率低的时候一切正常一调到1MSPS就丢数。最终追踪到原因是ADC逻辑的读状态在切换时有一个分支没有覆盖到位宽变更的情况。ADC实际输出在某个配置下是16bit但我的状态机在解析时按照默认8bit处理了半个周期导致偶尔跳过了一个有效数据。这种问题光看波形很难发现把仿真数据存成文件逐点比对后立刻暴露了。解决办法是状态机的分支条件重新对照手册梳理了一遍把所有位宽配置都覆盖到位。4.3 时序约束不足导致的随机器件问题“随机”问题大概率是时序问题。有一段时间系统运行完全不规律可能跑半小时没问题也可能刚上电几分钟就出错。用ILA抓了半天也看不出明确规律。后来综合实现后在Vivado的时序报告里发现了几个时序违例的路径位置刚好在ADC接口逻辑上。原因是外部ADC数据变化相对于FPGA内部时钟有setup/hold裕量不足当时直接用默认约束跑了没单独给ADC接口的输入延迟做约束。解决办法就是写XDC约束显式声明input delay和output delay然后重新实现时序违例清零问题消失。说到XDC给还在学习的朋友一个建议约束不是随便写写就行的。它背后是芯片物理实现里信号到达时间的概念理解这个需要一点数字电路时序基础。我刚开始学的时候也是一头雾水直到自己遇到过一两次由于约束不对导致的诡异问题才真正体会到这个环节的意义。现在每到一个新项目我会把接口时序约束当成和RTL编码同等重要的事情。下面是当时用到的ADC输入约束片段可以直接参考# 假设外部ADC数据在sclk下降沿有效 set_input_delay -clock [get_clocks sclk] -max 2.0 \ [get_ports {adc_sdata[*]}] set_input_delay -clock [get_clocks sclk] -min 0.5 \ [get_ports {adc_sdata[*]}]4.4 寄存器配置不上数据全是零系统上电后FPGA需要通过类I2C接口对ADC芯片内部寄存器做初始化配置比如设置采样率、选择通道范围、调整增益。有一版硬件跑起来之后所有通道读数全是0用ILA抓配置接口波形才发现SCLK的频率太高了超出ADC所允许的I2C最大时钟限制。解决方式很简单把配置接口的分频系数调大把配置时钟从1MHz降到200kHz问题迎刃而解。这个教训是新接一个外部器件时接口速率一定要仔细读手册上限不能想当然按“感觉应该行”来设置。4.5 上位机收到的数据包校验失败系统联调时另一个高频问题出现在以太网数据包上。FPGA组包时计算校验和上位机解包时校验不过概率还不低。后来把组包逻辑和抓包工具Wireshark的显示放一起比对发现是FPGA里校验和的计算范围比协议要求的多算了一个字节。改完之后就稳了。这类问题往往不是什么高深原理导致的就是定义不严谨两边对约定的执行差了一个字节。建议在做这类通信协议时先在文档里写清楚每一字节的偏移和意义FPGA和上位机都按这个表实现比事后联调时互相猜要省力得多。5. 关于这个项目后续还能怎么扩展整块系统做完之后我在想后续可以扩展的方向。比较明确的有三个第一是增加通道数量目前8通道的架构如果换成更高密度的FPGA加到16通道甚至32通道问题不大因为通道数增加主要是并行复制ADC接口逻辑DDR3带宽还能撑得住第二是增加板载数据处理能力比如在FPGA里做实时FFT、峰值检测、异常告警这样就不需要把所有原始数据都传回上位机处理适合对带宽敏感的场景第三是做同步触发功能多个采集设备之间通过外部触发信号实现多机同步采集这类需求在多通道阵列测量中很常见。关于功耗和成本控制如果后续要压缩成本可以考虑砍掉DDR3缓存换成更大深度的片上FIFO但这种方式只适合采样率不高、实时性要求也不高的场景。因为片上存储毕竟有限一旦上位机响应慢一点数据就可能丢掉。做这个项目最大的体会是FPGA多通道采集系统从表面上看起来只是“把ADC的数据读进来”这么简单但真正把系统做稳定考验的是对时钟域管理、异步处理、时序约束、存储调度这些底层细节的理解。每个环节都可能让整个系统“看起来在跑实际上却有隐患”。希望这篇文章里记录的这些设计思路和踩坑经验能帮你少走几步弯路。本文还有配套的精品资源点击获取