
去年有个同学跑来找我说想做一个能看波形又能看频谱的小仪器预算不高要求也很直白能把常见的正弦波、方波看清能粗略看出信号的频率成分。我给他推荐了FPGA路线然后一起搭了一个基于FPGA的简易示波器、频谱仪。做完复盘这其实是个特别适合拿来学FPGA的项目因为它的数据链路非常典型高速采集、跨时钟域处理、存储控制、显示驱动、数字信号处理FFT等于把FPGA的看家本领全过了一遍。这类项目我应该算有点发言权。这几年帮人评估过不少“用单片机做示波器”“用ARM加高速ADC做采集卡”的方案最后大多绕回到FPGA上。原因不复杂示波器和频谱仪本质上是数据率密集的设备单片机在这个场景下要么算力不够要么时序不可控而FPGA的并行性和确定性在这里是天然优势。这篇文章就把整个项目的设计思路、关键模块、调试记录和踩过的坑完完整整记下来适合已经会一点Verilog、想动手做硬核项目的朋友每一步的关键取舍我都尽量说清楚能帮你少走不少弯路。1. 整体设计思路这个项目到底在做什么1.1 功能定位与性能目标先回答一个特别容易想当然的问题这个“简易示波器”到底要做到什么程度真实的商用示波器动辄上千兆采样率、十几位ADC还带高级触发、协议解码显然不是一块入门级FPGA加几百块预算能解决的问题。所以这类项目的第一步不是画大饼而是把指标砍到“够用且能实现”的档位。我们当时定下的目标是模拟带宽约25MHz级别可以清晰观察几百kHz到十几MHz的信号采样率双通道各50Msps单通道模式下最高100Msps存储深度每通道采样4096点也就是4K深度触发方式上升沿触发触发电平可调带简易预触发频谱分析4096点FFT50Msps采样率下频率分辨率约12kHz显示1024x600 LCD同时显示波形和频谱带简单游标读数这个指标组合的妙处在于50Msps的数据率正好卡在“不用跑高速SerDes”的位置普通并行ADC加FPGA普通IO就能搞定4096点FFT在入门级FPGA上利用IP核资源完全跑得动12kHz左右的频率分辨率对音频信号、通信基带信号、开关电源纹波分析已经非常够用。1.2 为什么是FPGA而不是MCU或纯上位机可能有读者会问采集数据发给电脑用上位机做FFT和显示不是更简单吗确实更简单但那严格说就不叫“基于FPGA的仪器”了。我坚持把处理放在FPGA内部有三个核心理由。第一是时序确定性。示波器的核心要求是“采样时刻准确”而MCU跑操作系统时中断响应抖动几个微秒很常见对几十MHz的信号来说就是几十上百个采样点的偏差波形看起来完全失真。FPGA里的采集逻辑是纯硬件状态机每个时钟周期做什么、在哪一拍把ADC数据打进来全部是确定的。第二是数据吞吐率。50Msps、10位宽度单通道数据率就是500Mbps双通道1Gbps。这个速率用串口或USB2.0实时传出去是不现实的必须在本地完成存储和处理。FPGA内部存储带宽和并行能力在这种数据率下毫无压力MCU则是必然瓶颈。第三是采集与显示的实时配合。示波器最忌讳“波形跟不上旋钮操作”FPGA可以做到采集、触发、绘制一气呵成操作响应和真实仪器接近。如果所有数据都先传回上位机再做延迟会非常明显。1.3 系统三大链路把整个系统拆开看其实是三条独立又相互配合的数据链路采集链路信号调理 → ADC → 跨时钟域握手 → 采集双口RAM触发链路电平比较 → 同步处理 → 触发状态机 → 控制采集启停显示链路波形RAM → 读地址生成 → 坐标映射 → LCD控制器听起来不复杂但把这三条链路在工程上同时跑对要处理的问题就多了ADC采样时钟抖动会直接影响波形噪声触发沿的亚稳态会让波形来回跳动FFT不加窗会出现频谱泄漏。每个坑我都在后面单独说明这些都是实际调试中真实消耗过时间的点。2. 硬件平台搭建与选型思路2.1 FPGA选型资源怎么估很多初学者会问到底需要多大容量的FPGA我给的保守经验是逻辑资源不低于1万级片上RAM不小于500Kbit最好有可用的硬件乘法器或DSP单元。如果打算用厂商的FFT IP核还要确认手里的开发工具版本支持对应器件。我们用的是一块入门级开发板上面那颗FPGA资源中等偏低做这个项目刚好够用片内RAM用来做采样缓存和显存DSP资源做FFT蝶形运算通用IO接ADC数据线和LCD控制信号。实测下来采集、FFT、显示三个大模块合起来逻辑资源占用大约80%RAM占用接近90%属于“稍微紧张但能放下”的状态。这也说明如果后续想加功能最好选资源再高一档的芯片别把余量卡太死。2.2 ADC与采样时钟ADC是整个示波器的“眼珠子”选型时重点看采样率、分辨率、输入带宽和量化噪声。我们用的是一款8位并行ADC采样率50~100Msps并行接口控制简单价格也不高。位数上8位偏少量化噪声相对明显但对“简易”定位完全够用如果预算允许10位或12位会让波形细腻很多代价是数据位宽增加、FFT计算量变大。采样时钟的质量容易被忽略。能用FPGA内部PLL输出就尽量用PLL不要用普通IO随便分频产生采样时钟。PLL输出的抖动通常在皮秒级普通IO分频的抖动可能到百皮秒级对50MHz采样率来说后者会在波形上叠加明显的高频噪声成分。这个差异在示波器屏幕上看就是“波形上有一层毛”和“波形干净”的区别。2.3 信号调理前端不是把信号直接怼进ADC这块是新手最爱跳过的部分但恰恰是最影响实测效果的一环。直接把信号接到ADC引脚会遇到三个问题信号幅度可能超过ADC量程导致削顶、输入阻抗不匹配导致波形反射失真、输入电压超过ADC供电范围可能烧芯片。应对方案是做三级结构高阻缓冲跟随器 → 电阻分压衰减网络 → 限幅保护。我们做了两档衰减1:1和1:10实测中已经覆盖了从毫伏级到几伏级的大部分测试场景。关键点是前端运放的单位增益带宽要足够否则高频信号幅度会先被“削”掉一截屏幕上看到的波形幅度比实际偏小甚至高频成分直接被吞掉。注意信号调理前端是整个系统里最容易“高不成低不就”的地方。性能做得太好成本和调试复杂度飙升性能太差后端数字部分做得再好也是白搭。这个项目定位是简易所以前端只要保证关键指标不拖后腿就行别过度设计。2.4 显示方案选择显示部分有两个方向直接驱动LCD屏或者输出VGA信号外接显示器。我们选的是LCD直驱屏幕分辨率1024x600FPGA内置显示控制器数据写入显存区域再按扫描顺序读出。实测刷新率能稳定在60Hz左右波形滚动平滑没有明显撕裂感。如果手里只有VGA显示器也可以走VGA方案生成行场同步时序把波形RAM数据按坐标映射成RGB信号。VGA的好处是不挑屏幕、逻辑更经典坏处是老式显示器体积大而且不方便做触屏交互。两者核心逻辑完全一样坐标映射模块可以直接复用所以一开始选了哪条路问题都不大。3. 核心逻辑设计波形采集链路3.1 采集控制状态机采集逻辑是整个示波器的地基。状态机通常分四步空闲IDLE→ 等待触发ARMED→ 采样存储CAPTURE→ 处理完成DONE。最需要想清楚的不是“怎么存”而是“什么时候开始存、存到哪里、什么时候停”。为了做预触发我们采用了类似环形缓冲区的思路双口RAM始终在循环写入同时保留最近N个点的地址信息。触发信号到来后继续写入M个点再停止这样缓冲区里既有触发点之前的波形也有触发点之后的过程屏幕能完整显示触发前后的变化。这个“循环写触发后停”的流程是一个典型状态机Verilog里用几个状态和计数器就能实现关键在于状态转移条件要写干净避免组合逻辑毛刺把状态机带飞。3.2 触发电平、同步与亚稳态处理触发逻辑最典型的坑是“比较器输出作为异步信号直接进状态机”。ADC输出的数据本身与时钟对齐但你要判断“采样值是否越过了设定电平”这个判断天然是异步的而且可能刚好发生在时钟沿附近形成亚稳态。我踩过的坑是触发沿偶尔多触发一拍波形在屏幕上偶尔左右跳一个点。后来在比较结果后面加了两级同步寄存器再把同步后的上升沿标志送给状态机问题就消失了。不要小看这个细节示波器这类仪器对确定性极其敏感多一拍的抖动在观感上就是波形在跳动看起来非常不专业。3.3 存储深度与数据帧结构我们把每帧存4096点这是一个兼顾分辨率和资源的折中。点数太少波形细节不够点数太多比如16384点片内RAM压力明显变大而屏幕横向只有1024列点多了一样要做抽值价值有限。另外4096正好是2的12次方对FFT模块特别友好。帧结构上我习惯把前16个地址用来存元数据比如采样率档位、触发电平、触发位置、本帧总点数后面4080个地址存有效波形数据。这样显示模块或后续的数据上报功能读取时不需要额外通道传递参数直接从RAM按帧读出就行。这个习惯在做“数据帧快照”类功能时特别实用。3.4 坐标映射与波形绘制优化显示波形时最容易出现的现象是“锯齿感”和“细节丢失”。波形RAM读出的数据是原始ADC量化值要显示到屏幕上必须先做坐标映射纵向把ADC量程等比映射到屏幕高度横向把4096点中的某一段映射到1024个像素列。一开始我直接“每4个采样点抽一个点”来显示结果波形锯齿感非常强方波的跳变沿时有时无。后来改成“最值保持”方式每个像素列对应的4个采样点里取最大值和最小值把从min到max的整根垂直短线画出来。这样方波边沿、窄毛刺都能完整可见显示效果一下子就对了。这个思路本质是用横向分辨率换纵向信息量对示波器这种“看细节”的仪器来说非常值得。4. 频谱分析链路从FFT到屏幕4.1 FFT的FPGA实现IP核还是手写频谱仪功能的实现核心是FFT。FPGA里做FFT无非两条路调用厂商IP核或者自己用Verilog写基2蝶形运算单元。我的建议很直接第一版用IP核跑通全部流程之后再考虑要不要手写。IP核的好处是时序收敛、资源优化都替你处理过了你需要做的是配好点数、数据位宽和旋转因子位宽。我们用4096点复数FFT输入位宽12位输出位宽16位在100MHz工作时钟下跑完一次FFT大约需要几十微秒完全满足实时性。自己手写FFT当然可以但第一版就手写会同时面对逻辑bug和数据格式bug两类问题排错成本陡增。这里有个特别容易翻车的点IP核输入一般要求补码有符号数ADC输出是无符号二进制数两者之间要先做一次转换。很多人漏掉这一步FFT结果出来是乱的第一反应以为IP核坏了其实只是符号位没处理好。这种问题很“隐形”因为波形显示时你根本感觉不到数据格式差异一进FFT就原形毕露。4.2 窗函数频谱不拖尾的关键做频谱分析不处理窗函数基本等于白做。直接对原始数据做FFT相当于给信号加了矩形窗频域会以sinc函数的形状向外泄漏一个本应在300kHz处干净的谱线会拖出一大片裙边把旁边的弱小信号完全淹没。这也是实测中频谱图看起来“脏”的头号原因。我们用的是汉宁窗它比矩形窗的旁瓣衰减好很多实现成本也低。具体做法是在FFT之前对每个采样点乘一个预存好的窗系数系数可以事先算好放进ROMFPGA里做一次乘法即可。加入汉宁窗之后频谱图的底噪大约降了10dB以上视觉效果和使用价值都上了一个档次。如果信号本身是瞬态脉冲类窗的选择还要更讲究但对常规连续波信号汉宁窗是性价比之王。4.3 频谱取模、对数压缩与屏幕映射FFT的输出是复数显示时需要取幅度也就是计算平方根。FPGA里直接开方比较浪费资源我用的方法是查表法或CORDIC近似。对这种简易项目直接用近似公式也能接受误差控制在正负1dB以内肉眼完全分辨不出来。频谱动态范围很大如果线性映射到屏幕弱小信号会直接沉底看不见。因此需要做对数压缩把幅度换算成dB值再按屏幕高度映射。工程上我把dB结果算成8位值直接送显存这样底部噪声显示为一条平线峰值信号在满刻度附近看起来很有“仪器感”。界面上再标注当前频率范围和参考电平读数价值就出来了。4.4 时间分辨率与频率分辨率的取舍FFT点数固定为4096时采样率越高频率分辨率越粗。50Msps下分辨率约为12kHz如果想观察更低频率的信号就得降低采样率。所以频谱功能必须配合采样率档位切换来用不能所有场景都跑最高采样率。我们做了三档切换50Msps对应0到25MHz频段、5Msps对应0到2.5MHz、500Ksps对应0到250kHz。实测在低频档下1kHz的音频信号在频谱图上能清楚分辨这与商用频谱仪“看低频段要调小RBW分辨率带宽”的思路是一致的。这种分档设计让一颗固定点数的FFT覆盖了更大范围的应用场景逻辑上只是多几个时钟分频档位收益却非常明显。5. 实测过程与调试实录5.1 第一轮上板正弦波、方波、调幅信号把工程烧进板子先用信号源输出1MHz正弦波屏幕上出现漂亮的波形频谱图上有一根干净谱线第一次点亮的时候还是挺有成就感的。换方波测试问题立刻来了方波的频谱应该是一系列奇次谐波递减的谱线但屏幕上除了基波谐波几乎看不见底噪反而很高。排查后发现问题出在采样率档位没切换。方波本身包含丰富的高频成分但最高采样率档位下2MHz方波的谐波超出了FFT显示窗口的可见范围看起来自然是“只有一根线”。把采样率降下来再看奇次谐波一根根清清楚楚。这个经历让我意识到功能本身没问题难的是让用户理解“当前显示窗口到底能看到什么”所以后来界面上加了频率范围刻度标注。5.2 触发不稳定与波形跳动正弦波显示稳定但换任意波形后波形经常左右移动无法稳定显示。最初怀疑是触发电平设置太高导致无法触发调低之后依然跳动。后来用示波器直接看触发比较器输出发现信号本身在触发电平附近有高频毛刺导致触发沿不唯一。解决办法分两步在模拟前端加了一个简单的低通滤波把毛刺滤掉同时在数字侧加了“触发确认窗口”电平跨过阈值并且保持超过几个采样周期才认为是一次有效触发。这个数字防抖的思路比单纯提高触发电平可靠得多因为信号叠加噪声时任何固定电平都可能产生多次误触发。5.3 频谱图拖尾问题的最终解决前面提到加窗解决了大部分频谱泄漏但还有一个顽固现象某些谱线附近总有一个不对称的小包。查了很久最后定位到跨时钟域丢点采集时钟和FFT处理时钟不同数据从采集FIFO读到FFT模块时最初的握手逻辑没有处理“背靠背读空”的情况导致偶尔几个采样点重复或丢失。频谱上表现为信号被周期性破坏自然产生杂散分量。修复方法比较经典把FIFO的读使能逻辑改为“当fifo计数大于设定阈值时才开始读”先让数据进入稳定状态再消费。这类问题在高速数据采集里其实很常见但原理图和时序图看着都对实际跑起来就是会偶发丢数只有频谱图这种高灵敏度的检波工具才能把它暴露出来。这也反过来说明频谱仪功能不只是用来测信号的它自己也是很好的调试工具。5.4 显示刷新与采集的衔接最后遇到的是界面流畅度问题波形刷新率只有十几帧画面卡顿感明显。查了瓶颈问题出在LCD显存写入上。波形数据和频谱数据是两个独立模块生成的如果都直接写同一块显存就会出现写冲突谁先谁后不确定整体帧率自然被拉低。我把显存改成双缓冲当前帧显示A缓冲区时新数据写入B缓冲区下一帧从B读取、同时写A。这个乒乓操作几乎是所有实时显示系统的标准解法成本只是多占用一块RAM换来的是刷新率从十几帧提升到接近50帧肉眼完全看不出卡顿。6. 常见问题速查与避坑清单6.1 高频问题对照表整理一份我在调试中遇到的最典型问题和排查思路方便你直接对照处理现象可能原因排查与处理波形锯齿感极强坐标映射只做了抽点未做峰值保持像素列内取最大最小值绘制垂直短线波形左右跳动触发比较输出亚稳态或防抖不足比较结果加两级同步加触发确认窗口频谱底噪高、谱线宽未加窗函数FFT前乘汉宁窗系数ROM频谱出现不对称杂散跨时钟域采样丢点或重复点检查采集FIFO读写握手设置读前阈值波形幅度整体偏小模拟前端带宽不足或阻抗不匹配检查运放带宽匹配前端阻抗刷新率低、画面撕裂单缓冲显存读写冲突改双缓冲高频时波形模糊ADC输入带宽不够或采样时钟抖动大换更高带宽ADC用PLL生成采样时钟触发点总是偏后预触发深度设置过小加大环形缓冲区增加触发后写点数6.2 信号完整性的几个隐藏雷区这个项目里真正影响成败的很多时候不是逻辑代码而是电路板走线和电源处理。ADC数据线长度不一致会导致数据错位波形上出现随机毛刺采样时钟和地线处理不好底噪直接抬升。如果你是在开发板上做尽量把ADC模块靠近FPGA的IO区数字信号线和时钟线不要平行走太长距离。电源方面ADC的模拟电源和FPGA的数字电源最好分开供给哪怕中间只加一个磁珠也能明显降低开关噪声串入采样结果的程度。实测直接把两个电源短接时波形底噪大概抬了1到2个LSB量级不大但信号幅度很小时就是明显的雪花底噪非常影响观感。6.3 资源优化与后续扩展方向整个工程合起来逻辑占用大约80%后续想加功能的话我建议优先做这几件低成本高收益的事把FFT输入数据位宽和旋转因子位宽调低一档频谱精度损失很小但逻辑资源能省出一大块坐标映射里的乘法改成移位加法近似释放DSP资源给后面的峰峰值测量或自适应触发加一个简易串口数据上报功能把原始波形数据吐给上位机这样示波器同时也能当数据采集器用想再进一步可以加数字滤波、平均值采样、李萨如图形。李萨如图形其实只比普通波形多一个“把两通道数据分别映射到X和Y轴”的改动逻辑非常简单但做出来的效果相当直观展示或面试时非常加分。这个方向上的扩展基本不会碰到架构级重写也侧面证明了当初把系统拆成“采集、触发、显示”三大链路的架构是正确的。收尾的一点个人体会做这个项目最大的收获并不是“从此有了一台自制示波器”而是通过它把FPGA开发里最容易被忽略的几道坎都踩了一遍异步信号同步、跨时钟域、数据格式转换、显示时序协调。这些知识在写任何高速数据通路时都通用。如果只给后来者一个建议我会说不要一上来就追求漂亮界面和高级功能先保证“正弦波显示不跳、频谱图干净不拖尾、方波谐波能看到”这三个基础目标再谈别的。这三个目标背后对应的正是触发处理、窗函数和采样链路可靠性这三件核心事。这三件事做扎实了这个项目就成了八成功力。最后分享一个小技巧调试频谱仪时不要只用单一频率的纯正弦波测试多试试方波、锯齿波、调幅信号甚至两路信号叠加。这些“脏”信号才是真正能暴露仪器缺陷的试金石。我那次的方波谐波丢失问题就是用方波测出来的用正弦波测可能永远发现不了。