资讯动态

FPGA示波器自制指南:从采样到VGA显示的完整实现

发布时间:2026/10/13 20:12:23 来源:尧图企业网站定制
简介基于FPGA的VGA示波器设计是一套围绕FPGA与VGA接口实现的完整工程资源面向电子设计竞赛参赛者、FPGA入门学习者以及需要自制测试仪器的硬件工程师。项目以FPGA为控制核心通过编程生成正弦、方波、三角、锯齿四种波形幅度与频率均可动态调整并借助VGA接口完成数字信号到显示器像素的映射、时序控制与平滑滤波内容覆盖波形发生器、参数调整、坐标计算、显示驱动和模块化设计等关键环节。压缩包共318个文件主要包含FPGA源码v、tdf、Quartus工程与配置文件qpf、qsf、qdb、qip、仿真波形与后端时序文件sdo、vo、wlf、mif初始化数据以及可下载的sof配置文件等整体大小18.95MB目录结构清晰。当前已有277人参与学习可直接参考这些文件还原项目设计流程或提取其中VGA接口驱动、任意波形生成和参数动态调节的实现思路用于课程设计、毕业设计或实际仪器开发。1. 打开第一台FPGA示波器入门带宽够用先把信号“看见”再说你手头需要看一个1kHz方波、一条I²C总线、一块电源芯片的输出纹波买台式示波器预算不够用逻辑分析仪又看不见模拟量这时最划算的路线不是省吃俭用而是自己写一个FPGA示波器。用一块入门级FPGA开发板加一块并行ADC把采样数据缓存到内部RAM再通过VGA接口实时显示波形——这个方案做出来的数字示波器带宽做到20MHz左右、采样率做到100MSPS左右对应三五千元成品仪器的入门水平成本只有其中的零头。更重要的是做完这一套你对时序逻辑、跨时钟域、存储带宽、接口协议的理解比翻十遍课本都扎实。这篇文章适合已经会写基本Verilog、手头有一块开发板和VGA显示器的工程师照着做一路踩坑一路排最终拿到一台能触发、能测频、能看波形的桌面仪器。2. 为什么这个方案用FPGA而不是单片机先把带宽账算明白2.1 单片机和ARM卡在哪不是CPU频率不够是“实时”两个字先看一个常见误区有人拿主频200MHz的ARM Cortex-M4去采10MHz正弦波ADC采样率标称5MSPS按奈奎斯特采样定理2.5MHz以下信号“理论上”能采。但实际一接信号源波形完全失真。原因不是ADC位数不够而是采样不均匀CPU要响应中断、搬运数据、刷新屏幕每次采样的时间间隔抖动达到几十纳秒甚至几百纳秒这种抖动在频域表现为相位噪声直接拉高波形本底噪声让正弦波变成一条粗线。我做这个方案的时候同样踩过这个坑后来想明白一个道理示波器前端必须有一个不依赖CPU的“硬实时”采样链路。FPGA的优势就在这儿——ADC的转换时钟直接由FPGA引脚输出采样时刻由硬件时钟沿锁定抖动在皮秒级。采样数据进来之后要么进RAM要么进FIFO全程没有“等待”这个动作。这正是示波器这类设备必须用FPGA或ASIC来实现的原因单片机的CPU再快也只是“事后处理快”采样瞬间的确定性做不到。2.2 采样率与带宽的关系200MSPS为什么只能标40MHz带宽示波器带宽的定义是正弦波幅度衰减到实际幅度的70.7%时对应的频率不是“能看多少频率的信号”。一个标称40MHz带宽的示波器测40MHz正弦波幅度已经衰减了3dB测100MHz方波可能已经只剩一条带毛刺的基线。原因是方波的频谱分量延伸很远基频只是其中能量最大的那一根。工程上常用的换算关系有三档采样率除以5到8得到“靠谱带宽”除以10得到“能看个大概”的带宽除以25得到“能准确测量幅度”的带宽。上面说的200MSPS采样率除以5是40MHz这就是为什么多数200MSPS示波器标称带宽40MHz——不是硬件做不到更高是标高了用户会拿去测100MHz信号测出来幅度不对就投诉。同样道理ADC的模拟输入带宽如果设计成200MHz那前端电路本身也会引入衰减。做这个FPGA示波器项目时我把目标定在10MHz以内就收手了原因很现实ADC选型容易前端放大电路不必做超高速运放PCB布局布线压力小一个量级而10MHz带宽已经能覆盖大部分日常调试场景。2.3 主控选型与系统级面板设计入门级FPGA够不够用先列一个最小系统需要的东西再谈选型。这个系统由五块组成ADC前端电路、ADC芯片、FPGA主控、VGA接口、输入面板按键或旋钮。下面是一个参考配置表模块选型参考关键参数备注ADC8bit 并行ADC30~50MSPS量化位数、ENOB、输入范围ENOB有6.5bit以上即可FPGA入门级逻辑单元5k~15kBRAM容量需至少够一帧波形内部RAM不够就外挂SRAM时钟板上50MHz晶振 PLL倍频像素时钟25.175MHz、采样时钟30MHzPLL产生不用外部时钟源VGA15针D-Sub接口RGB三路用3bit~8bit电阻网络分压不用DAC也行电阻分压前端两个运放做偏置与驱动带宽≥50MHz增益1~2关键看共模范围入门级FPGA逻辑单元一般在5k到15k之间这个项目主要资源消耗是VGA时序控制器大约占200~400个逻辑单元采样时钟域控制大约占100个RAM深度2k×8bit大约占几个BRAM块整体资源占用非常低。唯一要提前确认的是BRAM容量如果一屏要存2048个采样点每点8bit就是16kbit入门级FPGA一般都有几百kbit的BRAM足够。我见过有人一上来就外挂SRAM芯片结果逻辑复杂度翻倍实际上内部RAM不够时优先减小存储深度而不是外挂芯片——示波器又不是要存几十秒的波形。3. VGA时序是第一个大坑行场参数和像素时钟差一点都不行3.1 VGA协议的最小模型三路颜色与两个同步信号VGA接口的本质就五个信号三路模拟颜色R/G/B和两个同步信号行同步HS、场同步VS。行同步信号标志着一行像素扫描的开始和结束场同步信号标志着一帧画面的开始和结束。显示器内部有晶体振荡器它只做一件事严格跟随行同步信号的节奏逐行移动电子束再跟随场同步信号逐帧换行。因此FPGA要做的只有两件事精确输出HS和VS脉冲以及在可见区域里按行列坐标送出对应的颜色数据。分辨率的每一个参数都有行业标准不能自己拍脑袋。640×48060Hz是VGA最基础、对FPGA最友好的模式像素时钟只要25.175MHz而这个频率在FPGA的PLL里很容易倍频出来几乎不需要外部时钟芯片。注意一个问题VGA时序里的大多数“后沿”“前沿”数值因分辨率不同而不同。我见过有人把800×600的行参数套在640×480上用结果是画面整体偏移、左右有宽黑边、显示内容被压缩——时序这东西严格到每一个像素没有“差不多”的余地。3.2 640x48060Hz完整时序参数表收藏这一张就够下面的参数是VESA标准的640×48060Hz非隔行模式FPGA工程里直接按这个表写计数器项目像素/行数说明可见行宽640显示有效像素行前沿16行同步脉冲之前的空白行同步脉冲96HS拉低的时间行后沿48行同步脉冲之后的空白整行长度800640169648可见帧高480显示有效行数帧前沿10场同步脉冲之前的空白行场同步脉冲2VS拉低的时间单位行帧后沿33场同步脉冲之后的空白行整帧长度52548010233像素时钟25.175MHz上述参数全部以该时钟为基准HS极性负极性低电平有效VS极性负极性低电平有效这里有个容易写错的地方HS脉冲宽度96个像素时钟VS脉冲宽度2行意思是场同步期间正好跳过2个完整的行周期。这两者之间存在“行同步包含在场同步里”的嵌套关系编码实现时一般用两个计数器一个像素计数器0~799负责行内定位一个行计数器0~524负责帧内定位行计数器在像素计数器归零时加一这样逻辑最清晰。3.3 像素时钟的选择PLL精确到25.175MHz还是用25MHz凑合FPGA开发板上的晶振一般是50MHz用PLL倍频到25.175MHz是可以做到的但有些入门级FPGA的PLL频率步进不够精细分频系数组合后只能得到25MHz或25.25MHz。这时有两种选择。第一种是用25MHz将就代价是每行800个像素时钟里少了7个显示器会从场同步信号推算行扫描频率画面会出现在轻微的水平偏移同时每行右侧有几像素的裁切肉眼几乎看不出来。第二种是用PLL的“分数分频”能力配置出精确的25.175MHz如果FPGA不支持分数分频就放弃。我一般会直接选25MHz同时把行后沿从48改成55、行前沿从16改成9让整个行周期仍然是800个时钟这样显示器侧的时序完全和标准一致只是把“多出来的7个像素”安排进了消隐区。这种做法比死磕25.175MHz要省事得多而且不影响显示效果。另外注意像素时钟的抖动直接影响画面稳定性PLL电源引脚上加0.1μF和10μF并联去耦电容是必须的操作不是可选优化项。3.4 行场同步的Verilog骨架两个计数器就够了VGA控制器的核心代码不长但要注意计数器置位逻辑下面给出一个最简骨架// VGA 640x48060Hz 时序控制器骨架 // 时钟: pixel_clk (25MHz) reg [9:0] h_cnt 0; // 行像素计数器 0~799 reg [9:0] v_cnt 0; // 帧行计数器 0~524 wire h_valid (h_cnt 640); // 行内可见区 wire v_valid (v_cnt 480); // 帧内可见区 wire valid h_valid v_valid; // 像素有效标志 // 行计数: 像素计满800归零, 同时行计数器加1 always (posedge pixel_clk) begin if (h_cnt 799) begin h_cnt 0; if (v_cnt 524) v_cnt 0; else v_cnt v_cnt 1b1; end else begin h_cnt h_cnt 1b1; end end // 同步输出: 负极性 assign hsync (h_cnt 64016) (h_cnt 6401696) ? 1b0 : 1b1; assign vsync (v_cnt 48010) (v_cnt 480102) ? 1b0 : 1b1;这段代码把HS的下降沿放在第656个像素时钟持续96个时钟后恢复高电平VS下降沿放在第490行持续2行后恢复。注意v_cnt的最大值524是从“48010233525行”减一得到的因为计数器从0开始。写代码时最容易犯的错是把“总共525行”直接当成最大值525结果帧有526行显示器的帧率就偏了。4. 从采样到显示的核心链路RAM缓存与双口读写是成败关键4.1 模块划分先切成四个独立的部分再动手整个工程不复杂但不分模块直接写一个顶层文件的话综合后的时序约束会非常乱。常见做法是分四个模块采样控制模块负责接收ADC数据和产生写地址触发模块负责判断是否满足触发条件、是否重置写地址存储模块用FPGA内部的真双口RAM写时钟接采样时钟、读时钟接像素时钟显示控制模块产生行列坐标并映射成读地址和RGB颜色。四个模块之间用简单的握手信号连接不要搞复杂的总线协议。这种划分的核心原因是“跨时钟域”——采样时钟是30MHz像素时钟是25MHz两者频率不同、相位不同如果直接把采样数据用像素时钟去读会出现数据冲突和显示错位。RAM必须用带独立读写时钟的真双口RAM写端口时钟接adc_clk读端口时钟接pixel_clk两边地址独立读写互不干扰。这是整个项目最关键的技术决策没有之一。4.2 采样存储写地址生成与写使能的完整逻辑写地址的逻辑要回答一个问题每次采样到来时数据写到哪个RAM地址。未触发状态下数据按顺序从0写到2047再循环像流水一样不断刷新这就是“滚动模式”触发之后写地址清零重写这就是“触发模式”。两种模式靠写使能和地址重置信号切换。下面这段代码示意触发模式的核心逻辑// 采样写入逻辑, 时钟为adc_clk(30MHz) // 设定触发条件: 信号上升沿跨过阈值 reg trigger_armed; reg last_above; reg [10:0] wr_addr; reg wr_en; always (posedge adc_clk) begin // 电平检测: 当前采样值高于阈值 且 上一个时钟低于阈值 上升沿 if (adc_data 8d128 last_above 1b0) begin trigger_armed 1b1; end if (trigger_armed) begin wr_addr 11d0; // 触发点写入RAM起始地址 trigger_armed 1b0; end else if (wr_addr 2047) begin wr_addr wr_addr 1b1; end wr_en (wr_addr 2047); // 写满2048点后停止 last_above (adc_data 8d128); // 延迟一拍用于边沿检测 end注意这段代码里的last_above寄存器必须在每次时钟沿都刷新它的作用是保存“上一个时钟周期信号是否在阈值之上”这个历史状态。触发判断必须用last_above不能用adc_data当前值否则边界条件会有1个周期的不确定性。阈值8d128是把ADC的量程中点当作触发点具体数值应跟ADC的输入偏置配合调整比如输入信号被偏置在128附近那么128就是零点。另外wr_addr在触发瞬间被赋值为0但wr_en信号是组合产生的可能比wr_addr早一拍同一时钟沿上addr已经归零但wr_en仍有效这就是一个典型的竞争条件。实际工程中我会把wr_en改成寄存器并且把它置为无效上面代码是示意逻辑真做工程时要在wr_en路径上加一拍否则触发后RAM的0地址会被多写一拍数据。4.3 VGA读取显示行列坐标到RAM地址的转换关系显示侧的任务是当扫描到屏幕上第X列、第Y行时找到RAM中对应的采样点把该点的幅度转换为对应的Y坐标再决定这一像素要不要点亮。假设RAM深度2048、显示横向使用640像素那一个采样点对4列像素640÷2048≈0.3实际做法是取前512个采样点对应640列按列号右移一位得到地址。常见做法是每2个像素列对应1个采样点这样水平方向正好显示1024个点超出屏幕宽度的部分忽略。确定“点亮哪个像素”的映射是另一个关键采集的数据是8bit无符号数0对应波形底部255对应波形顶部屏幕垂直方向有480行需要把数据范围压缩到屏幕中部留出上下边框。下面这段代码是显示映射的常见写法// VGA显示映射, 时钟为pixel_clk // 计算当前像素对应的RAM读地址 wire [9:0] ram_addr h_cnt[9:1]; // 每2个像素读1次RAM, 取640点 // RAM读出的8bit数据转换成屏幕Y坐标: 数据范围0~255映射到屏幕Y80~400 wire [8:0] wave_y 400 - {1b0, ram_data[7:0] 1}; // 幅度大则Y小, 符合屏幕坐标 // 当前像素位置是否落在波形线上: 行坐标与波形Y坐标相等时点亮 wire wave_on (v_cnt wave_y) valid;这里有个值得注意的细节wave_y的计算中使用了减法而ram_data的高位在前也就是说采样值越大屏幕上的Y坐标越小对应“屏幕上波形上移”。这是常规示波器的方向约定。如果你发现波形上下颠倒就把减数与被减数交换位置不用动其他逻辑。4.4 滚动模式与触发模式的共存方案很多第一次做示波器的人只在“滚动模式”下看到了波形但两个信号频率接近时波形会左右游走无法稳定观察。稳定的关键是触发模式。实现时不能把滚动和触发做成两个独立模块而应该用同一个写RAM逻辑只是写地址的产生方式不同。滚动模式写地址一直递增触发模式写地址在触发条件满足时清零并写满一次后暂停。一个实用的中间做法是“预触发”持续滚动写入RAM当触发条件到达时停止更新RAM并冻结当前内容这样触发点其实位于RAM中间画面左侧是触发前的波形右侧是触发后的波形。预触发深度为总RAM深度的一半。这种做法能观察到触发点之前的信号形态对定位故障非常有帮助。工程实现上它只是把“清零写地址”改成“写满后禁止写”其他逻辑完全复用。一个需要特别提醒的坑RAM的读写端口虽然独立但WRITE_FIRST、READ_FIRST、NO_CHANGE三种模式要选对。默认的WRITE_FIRST模式在同地址写时会把新数据同时输出到读端口显示就会随机出现“亮点毛刺”。示波器场景应该配置为NO_CHANGE模式或者让读写地址永远错开——我一般是把显示读取的分频因子做成略大于写侧的采样深度从根源上不让两边访问同一地址区间。5. FPGA示波器踩坑与排查从黑屏到波形毛刺的5条血泪记录5.1 黑屏只有行场不同步同步极性搞反了现象接上VGA显示器屏幕提示“无信号”或满屏粗斜纹偶尔能抖出一帧画面。原因640×48060Hz的HS和VS都是负极性即“低电平有效”。新手容易照抄别人的代码却忽略极性定义或者直接把800×600的正极性参数搬过来用显示器无法识别同步信号。解决在代码里检查hsync和vsync的默认电平。负极性时无同步区域保持高电平同步脉冲拉低如果写成平时低电平、脉冲拉高就把极性位取反。最快的验证方法是下载一个只输出纯色块不依赖任何RAM数据的测试工程如果纯色画面正常时序就没问题此时再接上波形显示逻辑黑屏问题一定在数据通路。5.2 画面整体右移或左移消隐区的参数配错了现象波形显示正常但整幅波形不在屏幕中心左侧空出一段黑边右侧波形被截断一部分。原因把640×480的时序参数不完整地抄进代码——只写了可见区域和脉冲宽度漏了前沿/后沿或者数值抄错。解决用511行中的“行消隐总宽度160”来倒推行同步占96、前沿16、后沿48如果屏幕偏右即内容被挤到右边说明前沿参数设大了后沿参数设小了偏左则反过来。调整时只改后沿数值让“前沿同步后沿”总和保持160不变画面即可回到中心。前端时间参数影响的是“一行内容从哪里开始和结束”肉眼判断方向后微调这个值最直接。5.3 波形在屏幕上有毛刺、有叠影RAM的同步写读模式没配现象波形主轮廓正确但上面叠加了一层随机的亮点或短线看起来像第二道影子。用手按开发板时毛刺还会变。原因读写地址发生竞争。WRITE_FIRST模式下写端口同一地址写入新数据时读端口也看到新数据此刻VGA正在读“新写进”的那一个点但屏幕上对应的像素已经扫过了于是出现错位亮点。同时开发板内部逻辑电源纹波干扰了读端口的地址建立时间表现为随机毛刺。解决把RAM配置成NO_CHANGE模式或者让读写地址区间彻底分离。我是把写地址映射到RAM的前2048个单元读地址永远走另一片地址区间通过地址重映射错开。另一种思路是加大显示存储深度到4K把写地址限制在偶地址、读地址限制在奇地址两边永远不碰同一个物理单元。这个办法的代价是多耗一倍BRAM但对显示稳定性的提升非常明显。5.4 采样值全0或全满量程ADC的共模输入范围被压死了现象波形只有一条直线贴在屏幕底部全0或顶部全满信号源输入怎么调都不动。原因ADC的输入范围一般不支持负压比如单端AD转换器的输入通常要求在0~Vref之间。但信号源的输出是双极性的例如±1V的正弦波如果不加偏置直接进ADC负半周被钳位到0正半周可能超过Vref被削顶。更隐蔽的情形是输入信号幅值很小共模电压又不在ADC允许范围内ADC把直流分量都读成了0。解决在前端加一级运算放大器把双极性信号抬升到ADC量程中点。例如Vref2V时把输入信号偏置到1V的中点再做±0.8V摆幅。运放用±5V供电单电源供电的方案要选能“轨到轨”输出的型号。调试时先断开信号源用万用表量ADC输入引脚的直流电压确认在量程中点附近再接入信号。这条验证路径比看波形快得多。5.5 波形本身正常但背景噪声大电源与地的处理不干净现象没有输入信号时屏幕上已经不是一条细线而是一条宽约2~3格的粗带时长久了还会缓慢上下漂移。原因VGA的模拟地、ADC的模拟地和FPGA的数字地在开发板上一般是共地的采样时钟的开关噪声通过地通路径耦合进ADC输入等效为示波器本底噪声。如果前端放大了100倍这个噪声会放大得很可观。解决如果开发板是两层板优先把采样时钟频率降低一档例如从30MHz降到24MHz带宽损失可以接受同时把ADC模拟供电引脚上的去耦电容从0.1μF换成1μF加0.01μF并联噪声改善立竿见影。如果板子上ADC和VGA接口在物理上离得很远走线会形成一个大环路此时可以用“一点接地”的方式在ADC附近把模拟地单独分出一块用0欧电阻或磁珠连接到数字地平面。6. 让示波器真正能用用1kHz方波做全链路校准与时基验证做到前几章这一步示波器能显示波特形了但能不能拿它换掉一台成品示波器来做调试就得看一件事时基准不准幅度标不标。任何一个测试工具必须要有可追溯的校准流程。我的习惯是第一步先接标准的1kHz方波信号源周期为1ms如果时基档位设的是“每格1ms”那么屏幕10格宽的显示区内应当恰好看到一个完整周期左右对齐。如果差一点就调整显示读取侧的分频系数。示波器的时基本质上就是“水平方向每格代表多少时间”它由两个因素决定采样时钟频率和每格显示的采样点数。比如采样时钟30MHz屏幕横向10格一格显示512个采样点那么一格时间就是512/30MHz≈17μs。校准公式是一格时间 采样点数 / 采样时钟频率。如果你发现一格实际是17.5μs而示波器标称17μs说明采样时钟不是30MHz而是29.8MHz——这时去检查PLL配置而不是改代码。幅度校准同样重要输入标准1Vpp方波屏幕上波形峰峰之间占的格数乘以垂直灵敏度比如每格0.2V应该等于1V如果不符调整前端运放增益不要靠软件补偿硬撑。实际验证我还有一套更实用的做法直接把开发板上某个GPIO引出写一个分频逻辑输出一个占空比50%、频率精确等于1kHz的方波把这个输出接到示波器的输入端自测。这样不用外部信号发生器也能完成90%的校准验证——因为那个GPIO方波来自FPGA同一个晶振相位严格同步不会产生两个独立时钟源之间的相对漂移。等自测通过再接外部信号源做最终标定。最后建议你一定要把这个“最小可用版本”先跑通不要一上来就加光标测量、波形存储、FFT这些功能。示波器最难的部分永远不是“功能多”而是“显示稳定、读数可信”。把基础校准做完再考虑给项目加一个频率计数功能——最直接的做法是用先前那根1kHz自检方波做对照数一帧内波形周期数乘以帧率就得到输入信号的频率精度足以判断电路“大概坏了没有”。这台设备的价值不在于对标几百块的成品而在于你完全知道它内部每一个环节是怎么工作的出问题手到擒来这比产品本身值钱得多。希望这篇笔记能帮你把第一台FPGA示波器在实验桌上点亮少走几条弯路。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑