首页 / 资讯中心 / 文章详情

基于FPGA的简易示波器与频谱仪设计全攻略

基于FPGA的简易示波器与频谱仪设计全攻略 ★ FEATURED ARTICLE
最近有朋友问我能不能用一块入门级的FPGA开发板自己动手做一台能看波形、能看频谱的迷你仪器。他嫌台式示波器太贵又觉得电脑声卡示波器只能看低频音频信号不够过瘾。我想了想这个需求其实很典型——用FPGA做简易示波器和频谱仪算是数字系统设计里综合度很高、也很有意思的一个练手项目。它不光能让你把Verilog/VHDL的知识落到实处还能顺带摸到ADC采样、FIFO缓冲、FFT运算、显示驱动这些嵌入式系统里绕不开的硬骨头。这篇文章就围绕这个项目把从需求拆解到硬件设计再到FPGA内部逻辑实现、系统调试的完整思路捋一遍尽量把我踩过的坑和觉得值得注意的地方都说清楚。1. 需求拆解这个简易示波器到底要做到什么程度先别急着画原理图、写代码。做仪器类项目最大的忌讳就是一上来就想全都要。示波器有几百兆采样率的频谱仪有几十GHz频段的那不是一块几百块钱的开发板能搞定的事。既然是简易就必须明确边界这台设备的核心任务是什么哪些功能必须做哪些功能可以妥协。1.1 核心功能边界与关键指标我给自己定的目标是做一个看得见、留得住、能分析的波形采集与频谱分析装置。具体来说输入信号单通道模拟电压范围0到3.3V或者正负几伏取决于前端调理电路频率范围从几十赫兹到几兆赫兹。采样系统使用FPGA开发板上自带的ADC或者外接一块并行ADC芯片。采样率至少做到20MSPS以上才能比较像样地观察1MHz以内的信号。显示系统使用一块小尺寸TFT液晶屏比如3.2寸、320x240分辨率或者更大一些的RGB接口屏幕直接在屏幕上绘制波形曲线和频谱柱状图。交互方式按键切换示波模式/频谱模式、调节水平时基采样率分档、调节垂直增益、冻结/继续显示。触发功能至少支持上升沿/下降沿触发不然看周期性波形时会觉得画面乱跳。再说说什么可不做不需要双通道不需要差分探头不需要USB通信上位机不需要旋钮编码器调参按键就行也不需要做硬件上的自动量程。把这些多余的期望砍掉之后整个系统复杂度立刻下降一个量级项目才真的可能做出来。1.2 为什么选FPGA而不是单片机或DSP可能有人会问这种东西用一片带ADC的STM32或者用树莓派加音频采集卡也能做啊为什么非要用FPGA关键在于两个字并行和时序。ADC采样出数据之后数据是连续不断往外流的。单片机一般情况下用中断或者DMA搬运数据但处理高频信号时会发现中断太频繁主循环根本来不及响应DMA缓冲区又不够灵活尤其在需要同时对数据进行实时显示和FFT运算的时候CPU的负担会很重。FPGA则天生适合做这种数据流型任务ADC时钟直接作为同步时钟数据进来就立即写入FIFO显示模块可以并行地从FIFO读数据画波形FFT模块可以在另一个时钟域里独立完成运算。整个架构是流水线式的彼此之间不用抢CPU时间。另外一个理由也很实际学习价值。用单片机做示波器核心工作可能在写GUI用FPGA做示波器核心工作在深入理解时序、存储、接口这些数字设计的基本功。1.3 整体数据流架构规划动手之前先在纸上把数据流画出来。我的方案里整个系统分为这几级模拟输入 → 前置调理电路电压缩放、偏置调整、限幅保护 → ADC采样 → FPGA内部采集控制逻辑 → 触发判断与FIFO存储 → 显示控制模块画波形的点阵写入SRAM显存 → TFT屏幕。如果需要频谱显示则在FPGA里再增加一条分支FIFO里攒够一帧数据后送到FFT运算模块得到幅度谱之后再映射成屏幕上柱状图的高度值也写入显存。这个框图一定要在写代码之前定下来否则写Verilog时会越写越乱。后面整个实现都是围绕这条数据流展开的。2. 硬件部分的关键选择ADC、前端调理与电源设计说实话FPGA内部逻辑再漂亮前面模拟信号的链路没处理好示波器看到的也是脏波形。硬件这部分是简易示波器能不能看到真实信号的分水岭。2.1 开发板自带ADC与外置ADC该如何取舍现在不少中高端的FPGA开发板会板载一颗ADC芯片常见的是AD922612位、65MSPS或者类似型号通过并行数据接口和FPGA相连。如果你手里的板子就有这样的ADC建议直接用省事。但要注意看板子的原理图ADC的输入接口是不是SMA同轴接口输入范围是多少有没有运放做前端缓冲这些都会影响后面信号调理电路的设计。如果开发板上没有高速ADC就需要自己买模块。这里有个经验不要只看淘宝页面上标的最高采样率还要注意数据输出的格式。有些廉价ADC模块输出串行SPI接口比如ADS805之类采样率虽然标得高但是串行输出在FPGA里接收时会麻烦不少。做示波器这种场景尽量选并行CMOS输出接口的ADC靠时钟边沿一拍一拍读数据写Verilog时最简单可靠。我自己用一个通用方案项目里的做法选择了某款8位并行ADC芯片最高采样率标称30MSPS接口就是典型的时钟数据线模式。8位的分辨率虽然不高但对于观察波形轮廓、测量幅度和频率已经基本够用再配合前端调理电路把信号幅度调整到ADC满量程附近动态范围表现也还行。2.2 信号调理电路分压、偏置与钳位保护ADC通常只能采集0到3.3V或者0到2V的单极性信号而我们要观察的信号可能是正负对称的交流信号比如正负1V的正弦波。所以前端必须做两件事电平平移和幅度缩放。最简单的方案是用一颗运算放大器搭成加法器电路。先把输入信号按照分压电阻网络衰减到合适的幅度再用运放叠加一个直流偏置把信号的中心电平抬到ADC量程中间。比如要观察-1V到1V的信号ADC的输入范围是0到3.3V那我希望-1V对应0.65V1V对应2.65V也就是先把信号衰减到0.25倍左右然后加上1.65V偏置。这个电路里运放选型要注意增益带宽积。我们要观察几兆赫兹的信号运放的增益带宽积最好在10MHz以上否则高频部分会被衰减显示出来的波形幅度会偏小。我自己用的是一颗通用型运放增益带宽积100MHz左右实测下来满足几兆赫兹范围的需求不成问题。还需要加一个钳位保护电路。防止外部误接高压信号把ADC烧掉。简单做法是在ADC的输入端并联两个背靠背的二极管到电源和地二极管导通电压大概是0.3到0.7V能有效把输入引脚电压钳制在-0.7V ~ VCC0.7V之间。当然这只是防御性的措施信号源电压如果太高还是先手动衰减再接入。2.3 电源与时钟这些细节决定最终效果FPGA开发板一般自带稳压电源但模拟前端电路最好单独供电。别小看这一点——数字器件的开关噪声通过电源耦合到模拟输入端会在屏幕上显示出毛刺。建议模拟部分使用低噪声的LDO供电并且用0欧姆电阻或者磁珠把模拟地和数字地单点连接。采样时钟则直接决定你测出来的频率准不准。ADC的采样时钟最好不要用FPGA内部PLL生成的普通时钟就完事因为PLL输出通常带有一定的抖动对采样精度有影响。但做简易示波器这一个阶段直接用板载晶振透过PLL分频产生采样时钟其实是可以接受的毕竟我们是显示波形不是做精密计量。如果将来想升级再去考虑用专用的低抖动时钟芯片。3. FPGA内部实现从采样到存储的RTL设计作硬件设计的一个小建议先从核心的采样存储链路开始写不碰显示和FFT。因为这条链路是一切的基础——如果采集进来的数据有问题后面做出来的显示和频谱全都不可信。3.1 采样时钟域与数据接收逻辑ADC的数据接口看起来很简单每个采样时钟上升沿数据线上就会更新一次采样结果。所以最直接的代码就是module adc_capture ( input wire clk_adc, // ADC采样时钟 input wire [7:0] adc_data, // ADC并行数据 output reg [7:0] data_out, output reg data_valid ); always (posedge clk_adc) begin data_out adc_data; data_valid 1b1; end endmodule这个模块看起来简单但实际使用时有一个很容易被忽略的问题ADC的数据输出相对于采集时钟存在一个小的延时datasheet里叫tOD所以严格来说我们不应该直接在clk_adc的上升沿去采样数据因为采到的可能正好是数据变化的瞬间。为了稳妥不少设计里会用一个相位偏移一点点的时钟去采集。如果是用FPGA的PLL生成采样时钟可以生成两路时钟一路送给ADC芯片如果ADC需要外部提供时钟另一路相位偏移90度的时钟用于FPGA内部采集数据这样就能保证采到的数据是稳定电平。偏移时钟的实现也很好做在调用PLL IP核时选择输出一个与主时钟有固定相位偏移例如90度的时钟然后专门用于寄存ADC数据。3.2 触发逻辑让波形不再乱飘没有触发功能的示波器显示出来的波形会是叠影因为你每刷新一帧画面时采样的起始点没有固定。触发的核心思想是当输入信号满足某个条件时比如上升沿跨过某个电平阈值才开始把之后的数据写入存储FIFO然后一直采样存储足够的点数存储满后停止把这一帧数据交给显示。触发逻辑的代码也不复杂reg [7:0] adc_d1; wire trigger_flag (adc_d1 trig_level) (adc_data trig_level);这是最简单的上升沿触发条件即上一拍数据低于触发电平当前数据高于或者等于触发电平就认为检测到一个上升沿。检测到之后产生一个触发脉冲用这个脉冲去控制FIFO的写使能和计数器。实际操作中有个体验如果只做触发后马上开存储那你看到的波形大多出现在屏幕偏右的位置。原因是触发位置在波形上显示时应该把触发点放在屏幕左端或者中间。所以存储控制逻辑理想做法是持续不断地把ADC数据写入FIFO但只有在触发条件满足之后才开始计算存储点的个数并在存储了设定点数例如1024点后停止写入。这样加上FIFO本身的预触发深度触发前已经存了多少数据就能做到波形水平位置可调。简易版本也可以不实现预触发直接触发后计数存储屏幕显示从触发点开始的一段波形同样够用。3.3 FIFO存储注意位宽和深度的匹配显示一帧波形我们需要多少数据点这取决于屏幕的水平像素比如320个点。但直接让模拟前端采样频率等于屏幕像素点数是不现实的因为采样率还必须考虑被测信号的带宽。所以通常让采样率远高于显示点数然后在显示之前做抽取。我的选择是FIFO深度4096、位宽8位。这样我可以在一种时基设置下采集4096个点。显示时若屏幕只有320列宽就把4096点分成320组每组取平均值或者抽取代表点画到屏幕上。抽取这个动作可以在FPGA里做也可以简单地把数据先存下来然后在读出来的时候只取其中一部分比如每4个点取一个4096/41024还是大于320再继续二次抽取。FIFO的例化直接调用FPGA厂家的IP核就行。关键在于要留意FIFO的空满信号使用。写满一帧后要等显示模块读完才能开始下一帧写入。否则就会出现脏数据——新数据把没显示完的老数据覆盖了。这种跨时钟域的安排建议是AD采集写FIFO的时钟是采样时钟域而显示模块读FIFO的时钟来自显示控制器的像素时钟完全不同的频率。FIFO天然承担了跨时钟域的角色这也是为什么仪器类设计里到处是FIFO的原因。4. 显示与交互在TFT液晶屏上画出波形显示模块属于看得到的工作量最直观的部分。画波形、画网格、画菜单如果只靠往屏幕一个点一个点地写像素时间根本不够。必须要有一个完整的显存刷屏控制器的思路。4.1 显存机制一个SRAM做画布TFT屏幕常见的驱动方式有两种低速的SPI屏幕和高速的RGB并口屏幕。做示波器我强烈建议RGB接口屏幕。原因很简单——RGB接口屏幕需要FSMC或类似的控制器持续刷新屏幕数据FPGA里可以设计一个屏幕刷新手逻辑周期性从显存比如板载SRAM或者FPGA内的Block RAM搬运像素数据给屏幕同时另有一个画图逻辑波形绘制模块往显存的指定地址写数据。这样刷屏幕和画波形是并行进行的不会互相阻塞。如果用SPI屏幕光刷一帧320x240的图就要传输320*240个像素点而且还需要一个一个字节串行发送即便SPI时钟能跑到40MHz一帧的刷新时间也在几十毫秒量级屏幕会明显感觉到刷新慢在示波器这种需要实时刷新场景下体验很不好。显存的选择可以这样规划如果屏幕分辨率是320x240每像素颜色用16位RGB565那么显存空间为320x240x2字节约150KB。一般FPGA内部Block RAM不够大所以最好用板载SRAM/DRAM芯片。如果用的开发板上有现成的SRAM接口接上之后用Verilog做一个简单的SRAM控制器就行。没有的话也可以用FPGA内部RAM硬核对屏幕做纵向分割刷新但要牺牲分辨率或颜色深度。4.2 波形绘制与网格叠加如何把数据变成曲线波形绘制的关键是给定一组采样值代表电压和该组数据显示的水平坐标向显存写入对应像素点。如果直接把所有坐标点写入屏幕上会出现点状波形不够连续。所以真实绘制波形时每个水平坐标点上根据该坐标点对应的采样值可能经过抽取会亮一个垂直小线段线段的长度和亮度可以表示信号值。这种做法还顺便实现了加粗波形的作用。比如水平像素x处对应的数据值是y_data0~239那么我可以把y_data为中心向上下各扩展1~2个像素把这一列像素全部点亮。这样波形至少有两个像素宽看起来更清晰。网格和文字怎么叠加最简单的方式是在屏幕刷新逻辑中根据当前刷屏的行列地址来判断如果这个像素恰好处于网格线位置就在最终RGB值上叠加灰色如果是文字区域就查询字库ROM得到点阵值来决定颜色。这个过程很机械但代码结构要清晰最好把像素颜色生成写成独立函数块。整个过程对新手来说容易绕晕的地方是坐标变换采样数据是8位0~255而屏幕垂直方向只有240像素需要做一次线性映射。映射关系也不能简单做除法因为FPGA做除法消耗资源大建议提前算好一个缩放系数用乘法器IP核或者用移位加减法近似。4.3 按键控制与时基切换参数寄存器的设计交互部分我用了一个简单的状态机。按一下按键状态迁移到下一个模式示波模式、频谱模式、冻结、调节垂直偏移、调节时基。每种模式下按键加减用来修改对应参数比如时基参数是一个分频值控制ADC的采样时钟是对基础时钟分频多少倍。这些参数用寄存器保存在Verilog里体现为一组reg。注意参数变化时要考虑FIFO和显示链路里的时序同步问题。最简单的处理方式是参数在按键盘产生一个更新脉冲时集中同步更新避免在ADC采样过程中实时改写分频计数器导致波形周期产生毛刺。我实际调试时遇到过这个问题按键瞬间采样率突然变化画面上波形横向压缩了一下后来改成等当前帧采集结束后再更新参数就彻底解决了。5. 频谱分析模式在FPGA里做FFT的那些事频谱仪是示波器功能的自然延伸。当你想观察一个信号里有哪些频率成分时直接把采样数据做FFT就行。FPGA里做FFT各个大厂都有现成的FFT IP核重点不在算法本身而在于怎么正确地把数据送进去、把结果取出来并显示。5.1 数据准备与加窗做FFT之前第一个要注意的是截断效应。如果我们直接对一段有限长度的采样序列做FFT频谱会出现泄漏——主瓣旁边会冒出来一堆假谱线看起来像是旁边有好几个频率成分。为了压低这种泄漏通常需要对时域数据加窗最常用的就是汉宁窗Hanning Window。在FPGA里实现加窗也不复杂提前把窗函数值存到一张ROM表里例如1024个点的窗系数FFT之前把每个采样点乘以对应的窗系数。乘法用FPGA的DSP硬核很快就能搞定。需要注意窗系数的数据格式通常用定点数比如把窗系数乘以256后存为整数FRAME数据也同样缩放最后乘法结果右移8位保持数据范围不溢出。窗函数会造成主瓣变宽、幅度变小但这是善意的小误差换来的是远好看得多的频谱图。对简易频谱仪来说这完全划算。5.2 FFT IP核的使用要点必须处理好ready与valid信号以某常见FPGA厂家的FFT IP核为例它的接口大体包括时钟、复位、数据的s_axis_data_tvalid/tready/tlast以及结果输出通道m_axis_data_tvalid/tdata。刚开始用的时候容易被tready和tvalid信号组合弄晕。我的使用经验是这样的数据源端比如从FIFO读数据要等FFT IP核准备好s_axis_data_tready拉高才向前推进同时要保证每个FFT帧发送时最后一笔数据要拉高s_axis_data_tlast。配置FFT IP时可以设定采用固定点数比如1024点数据流模式选Real实序列这样输入数据只有实部虚部填0。输出是复数形式实部和虚部交织在tdata里。FFT做完之后需要计算幅度谱。幅度 sqrt(实部^2 虚部^2)。FPGA里开平方根可以用Cordic IP核或者更简单的做法——用近似幅值 max(|实部|,|虚部|) 0.4*min(|实部|,|虚部|) 来估算实测误差在几个百分点内对显示来说完全够用。我用的是这个近似法省掉了一个Cordic核的资源开销。5.3 频谱显示与坐标映射得到1024个频点幅度值之后正常只显示前512个点对应0到采样率/2的频率范围。屏幕上横轴表示频率纵轴表示幅度。但这个幅度值的动态范围很大强信号和弱信号可能差几个数量级。直接用线性坐标显示弱频率成分可能就压在横轴上看不见。所以简易频谱仪一般会做对数转换显示dB刻度。对数转换在FPGA里最实用的方法是查表法。提前算好一段幅度范围对应的dB值存到ROM里幅度的索引值作为地址读出来就是dB数据。量程可以用两级表或者分段表覆盖。这种查表法用的资源比直接调用log运算核少得多实时性还好。频谱图的X轴也是把512个点映射到320列宽的屏幕上方法跟波形显示时的抽取逻辑一致Y轴做dB值的线性映射。显示出来的频谱图配合网格视觉上基本就是一台正经频谱仪的样子。6. 整机联调那些容易把人劝退的疑难杂症硬件设计和逻辑设计都完成之后联调阶段才是真正考验耐心的时候。这里记录几个我调试时遇到的实际问题以及对应的排查思路如果有人也在做这类项目很可能也会撞上其中一两个。6.1 波形毛刺多半不是FPGA逻辑的锅第一次上电我用信号发生器输出1kHz正弦波屏幕上显示的波形却带有明显的台阶和毛刺。刚开始怀疑是ADC采集代码写错了后来经过排查发现毛刺频率与工频没有关系反而与信号发生器的输出频率是整数倍关系。这就有点意思了。逐项排查之后问题锁定在电源和地线上信号发生器送来的信号其地参考和开发板的地之间存在一个电压差通过BNC线的屏蔽层形成地环路。接上信号之后地环路把噪声引入了模拟前端。解决办法用隔直电容加电阻网络的方式做信号耦合或者在模拟前端输入端加一个隔离变压器。更简单的临时手段是让信号发生器用浮地输出与开发板共地后再接入。排掉地环路问题之后波形立刻干净了很多。6.2 FFT结果看起来不对称或者完全乱掉频谱模式下如果屏幕显示的全是乱码、噪声先别怀疑FFT IP核坏了。第一件事检查送入FFT的时域数据顺序是否正确。很多FFT核要求输入数据是按自然顺序进去输出是乱序的需要在读取输出时做好位逆序排列或者直接配置IP核选择自然输出顺序如果有这个选项的话。我的实现里一开始没注意到输出顺序配置导致频谱图的高频部分其实对应着低频率点看起来就非常奇怪。第二件事检查数据有没有溢出。FFT计算过程中中间级数据位宽增长很快。如果IP核配置时没有选择自动扩展位宽输出数据发生溢出后幅度值乱跳。正确做法是在配置FFT时选择扩展数据路径位宽或者在输入级先把信号幅度衰减一半预留计算余量。6.3 屏幕闪烁和撕裂感典型的显存读写冲突屏幕刷新过程中如果画波形逻辑正在往显存写入而刷新逻辑正在读同一块显存就会出现撕裂——画面上半部分是旧波形下半部分是新波形中间有一条明显的断层。解决撕裂的经典方案是双缓冲。简单的实现我试下来很有效准备两块显存区A区用于屏幕刷新读取B区用于逻辑画图写入一帧画完之后两个区互换角色。代价是显存需要两倍空间。如果SRAM够大就直接上双缓冲不够的话可以退而求其次画图逻辑只在屏幕刷新的行消隐期间写显存尽量避开读操作。后者实时性差一些但基本也能消除撕裂。6.4 触发抖动为什么同一频率的波形时左时右有一次调试发现信号明明是周期稳定的正弦波一帧帧看过去波形位置有时候会左右错开几个像素。排查了很久发现在于触发条件判断使用了上一拍小于阈值当前大于阈值但ADC的数据在这个阈值附近时存在噪声抖动。阈值刚好在信号上升沿的中间位置时还好但如果信号幅度较小噪声会在阈值附近来回摆动导致触发点不稳定。改进方法是加入触发消抖检测到触发条件之后不立刻结束判断而是等信号连续超过阈值若干个采样周期才算真正触发成功。这样一些短暂的毛刺就不会触发误动作。代价是触发会有几个周期的延迟但对于观察一般周期性信号完全无伤大雅。7. 进阶方向与资源优化思路基础版本的示波器/频谱仪跑通之后其实还有不少有意思的扩展点。如果你的FPGA资源还有富余下面几个方向都值得尝试。7.1 从8位升级到12位数据位宽扩大带来的连锁改动8位ADC虽然够看波形但要是想观察比较微弱的信号细节就力不从心了。想要真实升级体验最直接的方式就是把ADC换成12位芯片。但别觉得只是把8位数据线改成12位就行。数据位宽扩大之后FIFO的位宽同步扩大内存占用翻倍显示映射的除法精度也要相应调整否则屏幕看着没区别。整个数据链路都需要重新梳理一遍位宽和溢出边界所以这个升级我会建议放在基础功能全部稳定之后再动。7.2 硬件旋钮与更细腻的交互改善使用体验用按键调参数确实能用但不顺手。旋钮编码器在FPGA里做也不难编码器的正交信号通过状态机解码为加减脉冲再映射到参数寄存器。旋钮的阻尼感和连续调节的速度会比按键舒服得多。如果你手头有现成的EC11旋钮可以加上试试。7.3 低资源优化如果你的板子逻辑单元比较少有些入门级FPGA芯片逻辑资源有限FFT如果直接上1024点会占掉大半资源导致显存模块没位置放。这时候有几种优化手段FFT点数降到256点分辨率下降但没有质的损失显存换成外部SPI SRAM把内存成本挪给芯片外的器件或者显示刷新率降低比如从60Hz降到30Hz屏幕响应慢一点但依旧可用。这些优化没有标准答案全看自己手里板子的资源表先综合估算一下再决定做多大配置总比写到一半发现地方放不下强。8. 最后一点心里话做这个项目最大的收获不只是拥有一台自己亲手做的简易示波器而是把数字信号处理和数字电路设计这两件事真正串起来了。你在课堂上学的采样定理、FFT、FIFO缓冲到了FPGA里全是活生生的电路行为——一个采样时钟沿过了数据就在寄存器间流动一个FIFO满了真的会有波形画不出来。这种用硬件去触摸数据的感觉跟写软件确实完全不同。如果你准备动手我的建议是不要一上来就追求高指标先把采样→存储→显示这条链路跑通哪怕显示的是乱码也是一个巨大的进步。之后每加一个功能都切一块下来单独验证。这个项目急不得但它值得你慢慢折腾。要说有什么必须提醒的那就是一定要把示波器探头或者信号源的共地问题处理好。我见过太多人卡在屏幕全乱这一步最后发现问题不是逻辑错而是示波器探头的地没接好。其他的就放手去试吧电子这东西烧几个芯片、看了几页datasheet水平就慢慢出来了。
阅读完成 · 觉得有帮助?
咨询建站