资讯动态

FPGA简易示波器设计:从触发逻辑到异步FIFO的完整Verilog实现

发布时间:2026/9/16 15:48:27 来源:尧图企业网站定制
简介一套基于 FPGA 与 Verilog 的简易数字存储示波器工程适合电子工程、嵌入式及数字系统设计初学者也适合教学实验和原型验证。项目已在 EP2C8Q208C8 上验证覆盖数据采集、存储缓冲、触发控制、显示接口及时序分析等核心模块可帮助读者理解简易示波器的完整设计流程并给出各模块在 Quartus 下的层次组织。资源包为 rar 格式共 757 个文件约 1.73MB以 Quartus 工程中间文件cdb/hdb居多同时包含 Verilog 源码.v、TDF 定义、QSF/QPF 工程设置、sof/pof 配置文件以及 readme 等说明文档目录保留了完整工程构建与综合结果便于对照学习与二次开发。目前已有 901 人学习/浏览。通过源码、工程文件与网表映射等材料读者能够查看采样、存储、触发和显示模块的具体实现并了解 Quartus 环境中从设计输入到布局布线、生成下载文件的完整过程。1. 从“跑马灯之外”看FPGA示波器为什么采样率不是唯一指标在学校实验室或自己的桌面调试台上当你把一个信号发生器输出的方波接到自制“示波器”输入端屏幕上却没有出现预期的边沿时问题往往不出在ADC有多快而在触发逻辑和跨时钟域处理上。FPGA简易示波器这个题目几乎所有做数字IC和FPGA开发的人都动过手要么为了课程设计要么为了给自家板卡加一个便携调试工具。表面上看它不过是“ADC采集数据 显示”但真正把verilog代码写完整、跑通上板的人都知道核心难点在三处采样时钟与显示刷新时钟的异步交互、触发条件的准确捕获、以及FIFO深度和有效存储深度之间的取舍。这篇文章会把从架构拆分到可综合代码的完整路径讲清楚适合已经有基础verilog语法、想认真完成一个FPGA数据采集项目的工程师。2. 从需求到架构FPGA示波器的五级处理链路2.1 先定采集端的时钟和位宽再谈处理逻辑FPGA示波器的第一个关键是决定ADC接口长什么样。多数简易示波器使用ADC0809这类8位并行ADC时钟直接由FPGA的PLL输出驱动。采样率决定了你能看到的信号最高频率根据奈奎斯特准则实际可观测信号频率大约是采样率的十分之一到二十分之一。也就是说50MSPS的采样率看5MHz以内的波形才是实际可用的。位宽则决定了垂直分辨率8位对应256个量化级别显示在屏幕上已经能得到像样的波形曲线。架构上我一般把整个系统拆成五级信号调理 - ADC采样 - 触发检测 - 异步FIFO - 显示刷新信号调理负责把外部信号的电平调整到ADC输入范围通常是0到3.3V或者0到5V这部分是模拟电路的事。ADC采样模块在FPGA里不过是一段同步逻辑难点在触发检测和异步FIFO。触发检测要根据设定的边沿类型判断什么时候开始存储数据而FIFO要解决采样时钟域和显示时钟域不一致的问题。如果跳过FIFO直接做跨时钟域显示端采到的数据会有大量亚稳态波形看起来像噪声。2.2 触发模式决定波形是否稳定边沿触发是常用起点示波器能稳定显示周期性信号本质上靠的是触发。上升沿触发的意思是当信号从低到高跨越阈值电压时开始把ADC数据写入FIFO。这样的话每次存储的起点都对应信号同一位置显示出来波形就能左右稳定。实现上不复杂用两个寄存器把当前采样值和上一拍采样值做比较前一个低于阈值、后一个高于阈值就产生一个trigger信号。reg [7:0] sample_d1; reg trigger_rise; wire [7:0] threshold 8d128; always (posedge clk_adc or negedge rst_n) begin if (!rst_n) begin sample_d1 8d0; trigger_rise 1b0; end else begin sample_d1 adc_data; if (sample_d1 threshold adc_data threshold) trigger_rise 1b1; else trigger_rise 1b0; end end这段代码里clk_adc是ADC采样时钟rst_n是低电平复位的异步清零信号。寄存器的非阻塞赋值保证了sample_d1和adc_data的边沿比较是基于前后两个连续的采样点而不是同一拍内的不确定值。阈值8d128对应ADC中点电压也就是信号电平的一半。收到触发信号后并不会立刻停止采集而是要等FIFO填满预设点数才停止。这段时间差对应触发位置在波形上的左右偏移后文会进一步说明。2.3 存储深度的计算公式采样率乘时间窗FIFO深度等于采样率乘上你需要显示的时间窗口长度。50M采样率要显示10微秒的波形窗口深度就需要500个点。这看起来不多但实际上考虑到需要先采集、后处理、再显示深度往往做到4096或8192。选择深度时有一条经验规则深度是显示水平分辨率的四到八倍。如果显示端是800像素宽那么每像素对应5到10个采样点波形曲线会非常平滑也能做后端的插值显示。采样率时间窗FIFO深度显示分辨率建议50 MSPS10 us500 - 取512128-256像素50 MSPS100 us5000 - 取4096或8192512-1024像素100 MSPS10 us1000 - 取1024256-512像素FIFO深度建议使用2的幂次这样地址回卷只需要做位与操作不需要判断是否到最大值综合后的资源占用会小幅下降。在异步FIFO设计里深度还影响格雷码跨时钟域传递的可靠性稍后会说明为什么格雷码是必选项而不是可选项。3. 核心Verilog模块实现采样、FIFO与控制逻辑3.1 ADC采样接口时序与状态机ADC在FPGA里通常是一个简单的并行接口但数据不是每个时钟周期都有效需要根据ADC芯片手册搭建一个握手机制。以一个典型的8位并行ADC为例接口信号包含转换启动信号、忙信号或数据有效信号、并行数据总线。用状态机来控制采样的节奏而不是直接在每个时钟上升沿读取数据。localparam IDLE 2d0; localparam CONV 2d1; localparam READ 2d2; reg [1:0] state; reg adc_start; reg [7:0] adc_data_reg; always (posedge clk_sys or negedge rst_n) begin if (!rst_n) begin state IDLE; adc_start 1b0; adc_data_reg 8d0; end else begin case (state) IDLE: begin adc_start 1b1; state CONV; end CONV: begin adc_start 1b0; if (adc_busy) begin state READ; end end READ: begin adc_data_reg adc_data; state IDLE; end endcase end end这个状态机里clk_sys是系统时钟不一定是采样时钟。许多实际项目中系统时钟和ADC采样时钟是分开的系统时钟跑更高频率来执行控制逻辑采样时钟则是PLL分频得到的固定速率。状态机从IDLE启动转换检测到ADC的busy信号变高后等待转换完成再在READ状态锁存数据。关键参数是每个状态持续几个时钟周期必须大于ADC芯片手册规定的最小转换时间。如果你使用的是ADC0809这类芯片转换时间大约100微秒那么系统时钟50MHz下每个状态可能需要持续多个周期单纯用状态机跳转是不够的需要引入计数器来延长状态持续时间。很多初学者会直接在这个状态机里写数据存储逻辑这就埋了一个隐患一次采样转换只能得到一个数据点而存储逻辑需要连续的数据流。更合理的做法是把状态机产生的adc_data_reg通过一个简单的握手信号比如data_valid交给FIFO写入端由写侧逻辑负责连续存储。3.2 跨时钟域方案异步FIFO与格雷码采样的关系要理解为什么异步FIFO是FPGA示波器的必需品先看一个数据流关系ADC采样时钟来自PLL显示刷新时钟来自像素时钟或串口波特率时钟二者不仅频率不同相位也完全无关。如果直接用读时钟去采写时钟域的信号在边沿附近会进入亚稳态值可能非0非1也可能在采样后抖动。解决亚稳态的常见做法有两种一种是用两级同步器打两拍适用于单bit跨时钟域信号另一种是使用异步FIFO适用于多位宽数据跨时钟域。而多位宽数据如果直接同步是不能保证所有bit在同一拍到达的。异步FIFO的巧妙之处在于写地址和读地址分别用二进制递增但在跨时钟域传递前转换成格雷码。格雷码相邻两个数值之间只有一位变化这样即使同步器采样到中间状态读出的地址偏差也只有一个单位不会导致数据读写指针错乱。module async_fifo #( parameter DATA_WIDTH 8, parameter ADDR_WIDTH 10 )( input wire wr_clk, input wire wr_rst_n, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, output wire wr_full, input wire rd_clk, input wire rd_rst_n, input wire rd_en, output wire [DATA_WIDTH-1:0] rd_data, output wire rd_empty ); reg [ADDR_WIDTH:0] wr_ptr_gray; reg [ADDR_WIDTH:0] rd_ptr_gray; reg [ADDR_WIDTH:0] wr_ptr_gray_sync1; reg [ADDR_WIDTH:0] wr_ptr_gray_sync2; reg [ADDR_WIDTH:0] rd_ptr_gray_sync1; reg [ADDR_WIDTH:0] rd_ptr_gray_sync2; wire [ADDR_WIDTH-1:0] wr_addr wr_ptr_gray[ADDR_WIDTH-1:0]; wire [ADDR_WIDTH-1:0] rd_addr rd_ptr_gray[ADDR_WIDTH-1:0]; reg [DATA_WIDTH-1:0] mem [0:(1ADDR_WIDTH)-1]; assign wr_full (wr_ptr_gray {~rd_ptr_gray_sync2[ADDR_WIDTH:ADDR_WIDTH-1], rd_ptr_gray_sync2[ADDR_WIDTH-2:0]}); assign rd_empty (rd_ptr_gray wr_ptr_gray_sync2); always (posedge wr_clk or negedge wr_rst_n) begin if (!wr_rst_n) wr_ptr_gray 0; else if (wr_en !wr_full) begin mem[wr_addr] wr_data; wr_ptr_gray wr_ptr_gray (wr_ptr_gray[0] ^ 1); end end always (posedge rd_clk or negedge rd_rst_n) begin if (!rd_rst_n) rd_ptr_gray 0; else if (rd_en !rd_empty) rd_ptr_gray rd_ptr_gray (rd_ptr_gray[0] ^ 1); end always (posedge wr_clk or negedge wr_rst_n) begin if (!wr_rst_n) begin rd_ptr_gray_sync1 0; rd_ptr_gray_sync2 0; end else begin rd_ptr_gray_sync1 rd_ptr_gray; rd_ptr_gray_sync2 rd_ptr_gray_sync1; end end always (posedge rd_clk or negedge rd_rst_n) begin if (!rd_rst_n) begin wr_ptr_gray_sync1 0; wr_ptr_gray_sync2 0; end else begin wr_ptr_gray_sync1 wr_ptr_gray; wr_ptr_gray_sync2 wr_ptr_gray_sync1; end end assign rd_data mem[rd_addr]; endmoduleADDR_WIDTH 10对应1024个存储单元实际工程中根据存储深度的需要改成12或13即可。wr_ptr_gray和rd_ptr_gray的最高位用于指示是否跨越了半圈比较满空状态时考虑最高位可以区分“全满”和“全空”的边界条件。写侧比较满信号时使用同步后的读指针的格雷码并与当前写指针格雷码做比较读侧同理。这段代码省略了读写指针的二进制转格雷码逻辑直接用wr_ptr_gray (wr_ptr_gray[0] ^ 1)这种递增方式来生成格雷码序列需要注意的是标准格雷码计数器和这个表达式的等价性是基于“最低位翻转则进位”的特性。实际工程中建议写成独立的二进制计数加组合逻辑转换可读性更好也方便仿真调试。3.3 读侧与显示刷新如何把采样点映射到扫面线数据读出来之后怎么变成屏幕上的一条波形如果使用VGA接口显示需要把FIFO读出的采样点按坐标映射成一幅位图如果通过串口传到上位机则只需要简单的协议打包。简易示波器工程里VGA直连显示是常见做法省去上位机软件也更容易在板级验证时看到效果。wire [9:0] pixel_x; wire [9:0] pixel_y; wire vga_clk; reg [11:0] vga_data; always (posedge vga_clk) begin if (pixel_y (10d240 - adc_value)) begin vga_data 12hFFF; // 白色点显示波形 end else begin vga_data 12h000; // 黑色背景 end end这里用一个极简单的思路把ADC采样值当作Y坐标偏移量X坐标直接对应FIFO读地址。每一行像素扫描时从FIFO读取一个数据点计算出对应的Y坐标点亮该像素。VGA像素时钟通常是25MHz或40MHz而FIFO读时钟可以直接用像素时钟这样无需额外的时钟域转换读使能信号接像素扫描的列有效信号即可。这种映射方式存在两个问题一是波形看起来是一条线缺少余辉效果二是当采样点比像素少时波形会出现断续。工程上常见的解决手段是把每个采样点扩展成上下几行的垂直点亮或者用简单线性插值填补相邻点的空白。插值逻辑用一个组合逻辑完成代价很小但显示效果提升明显。4. 仿真与上板验证看波形之前先看仿真波形4.1 编写行为级ADC模型检验触发边界条件FPGA工程调试的第一步永远是仿真而不是上板。示波器工程里需要仿真的对象是触发检测和异步FIFO的读写协同。编写一个简单的ADC行为模型来产生已知特征信号比如频率已知的正弦波或带毛刺的方波。module adc_model ( input wire clk, input wire start, output reg [7:0] data, output reg busy ); reg [7:0] phase; reg [7:0] sine_lut [0:255]; initial begin $readmemh(sine_lut.hex, sine_lut); phase 8d0; busy 1b0; data 8d0; end always (posedge clk) begin if (start) begin busy 1b1; end else if (busy) begin busy 1b0; data sine_lut[phase]; phase phase 8d4; end end endmodule$readmemh从外部十六进制文件加载正弦查找表这个文件可以用MATLAB或Python生成也可以手写一小段C代码导出。phase每次加4相当于查表步长为4/256输出的正弦波周期是64个时钟周期。挂上这个模型之后用testbench给触发模块施加一个低到高的信号跳变观察trigger_rise是否出现在正确的时钟沿上以及FIFO写入的起始位置是否与触发点对齐。这一步能把大部分边沿判断的逻辑错误在仿真阶段找出来上板后才不会对着毛刺抓瞎。testbench里需要注意的一点是FIFO复位信号必须比写时钟和读时钟的有效沿早至少一个周期释放否则地址格雷码的初始状态可能不对导致rd_empty无法拉低数据读不出来。你可以打一个计数器在复位释放后等待50个周期再开始对FIFO进行操作。4.2 ModelSim与Vivado仿真中的常用调试命令进行仿真时不必把整个工程的波形全部翻出来看关注几个关键信号足够了trigger_rise、wr_en、fifo_wr_ptr_gray、rd_empty、adc_value。在ModelSim中执行如下命令可以快速检查add wave -hex /testbench/u_adc_model/data add wave -hex /testbench/u_fifo/rd_data add wave /testbench/u_trigger/trigger_rise add wave /testbench/u_fifo/wr_en add wave /testbench/u_fifo/rd_empty run -all examine -hex /testbench/u_fifo/rd_dataadd wave命令把关心的信号加入波形窗口run -all运行到仿真结束examine直接在命令行读取当前时间点的数据值。当你发现rd_empty始终为高、FIFO数据读不出来时检查写侧使能信号wr_en是否在触发之后被正确拉高。常见错误是触发信号只维持了一个时钟周期但FIFO写使能需要持续整个存储窗口需要在触发后用计数器接续拉高写使能。reg wr_en_reg; reg [13:0] write_cnt; always (posedge clk_adc or negedge rst_n) begin if (!rst_n) begin wr_en_reg 1b0; write_cnt 14d0; end else if (trigger_rise) begin wr_en_reg 1b1; write_cnt 14d0; end else if (write_cnt 14d4095) begin wr_en_reg 1b0; end else if (wr_en_reg) begin write_cnt write_cnt 1b1; end end这段代码在触发后把写使能拉高写入4096个点后自动拉低。由此引出一个细节触发点并不在FIFO的最前端而是在中间某个位置。也就是说触发发生前的数据并没有被保存。如果你希望看到信号触发前的状态实际测量中非常重要需要增加预触发逻辑例如让FIFO始终处于写入状态触发后不停止写入而是等FIFO满了再停止并从中取出数据块。这样FIFO里既有触发前的数据也有触发后的数据显示端再做左右偏移动画。这种方案对存储深度的需求会提高到8192或16384。4.3 板上调试用ILA抓片内信号仿真只能验证逻辑的正确性上板后的时序问题需要依靠FPGA厂商自带的逻辑分析仪来抓取Xilinx平台是ILAIntel平台是SignalTap。ILA可以把FPGA内部信号实时采样并上传到PC端查看等效于把一个逻辑分析仪塞进你的示波器工程里。create_debug_core u_ila_0 ila set_property C_DATA_DEPTH 1024 [get_debug_cores u_ila_0] set_property C_TRIGIN_EN false [get_debug_cores u_ila_0] set_property C_TRIGOUT_EN false [get_debug_cores u_ila_0] connect_debug_port u_ila_0/CLK [get_nets clk_adc] connect_debug_port u_ila_0/PROBE0 [get_nets trigger_rise] connect_debug_port u_ila_0/PROBE1 [get_nets adc_data_reg] connect_debug_port u_ila_0/PROBE2 [get_nets wr_en_reg]create_debug_core在Vivado的Tcl控制台里创建一个ILA核C_DATA_DEPTH 1024表示采样深度为1024个时钟周期PROBE0到PROBE2分别连接触发信号、ADC数据和FIFO写使能。上板运行后触发ILA捕获观察的实际时序情况与仿真是否一致。尤其要注意trigger_rise信号是否有毛刺如果毛刺存在说明ADC数据的建立时间可能不满足需要在数据线上增加约束或者把采样时钟沿调整半个周期。5. 三种提升测量精度的工程技巧5.1 用等效时间采样扩展高频信号观察能力当采样率不足以覆盖信号频率时常规做法是提高ADC时钟但受限于FPGA和ADC芯片的速度等级频率总有一个上限。等效时间采样是另一个思路假设信号是严格周期性的每次触发后只采集一个点但在不同触发周期里通过调整采样时钟相位滑动采样点最终把这些点按相位重新排列成一个完整波形。实现上需要一个高精度的相位累加器和一个可变延迟链代码量不大但要注意信号必须真的是周期信号否则还原出的波形是乱码。5.2 用均值滤波降低随机噪声对于高频噪声引起的波形毛刺可以在显示端做多次采集后按点取平均工程上称为滚动平均或滑动窗口滤波。verilog实现滑动窗口滤波的核心是维护一个固定长度的移位寄存器和一个累加器每次进来新数据累加器加新值、减最旧值输出为平均值。reg [7:0] shift_reg [0:15]; reg [11:0] sum; reg [3:0] index; always (posedge clk_disp or negedge rst_n) begin if (!rst_n) begin sum 12d0; index 4d0; end else if (rd_en) begin sum sum rd_data - shift_reg[index]; shift_reg[index] rd_data; index index 1b1; end end assign filtered_data sum[11:4];滤波窗口从数据流的视角看是每16个点求一次平均。sum[11:4]相当于除以16用移位代替除法节省DSP资源。这个技巧适用于信号本身变化比较缓慢、噪声频率高的场景如果信号本身就是高速变化的方法均值滤波会把边沿磨平取舍要按待测信号特征来定。5.3 显示端加余辉效果提升肉眼可读性波形在VGA屏幕上如果只显示一条亮线视觉上会显得单薄也难以看出信号的历史轨迹。简单的数字余辉实现办法是在屏幕刷新中断里读取上一帧的波形数据把当前采样值和历史值做加权合并后写入缓存。代码上不过是一个加法和移位assign display_value (current_value 2) (old_value 2) (old_value 1);这个公式等价于0.25倍当前值 0.75倍历史值每次刷新后旧波形逐渐变暗但并不立刻消失形成类似模拟示波器余辉的视觉残留。权重参数可以按显示刷新率调节余辉时间过长会让快速变化的信号出现拖影过短则失去余辉意义。从调试角度说余辉效果能直观地帮你观察信号抖动范围比盯着单个波形线要高效得多。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价