简介一份基于小梅哥 Zynq 开发板的简易自制示波器源码面向嵌入式系统、FPGA 编程与数字信号处理学习者展示了如何利用 ARM 处理器与可编程逻辑协同工作完成模拟信号采集、数据缓存、处理以及 LCD 实时波形显示。该工程覆盖 ADC128S 采集驱动、采样控制、FPGA 逻辑设计、PS 端应用程序等关键环节适合熟悉 Verilog/VHDL 和 C/C 的开发者也可作为课程设计或项目实践的完整参考。压缩包共包含 752 个文件整体大小约 8.68MB文件类型涵盖 .c/.h 软件源码、.v/.vhd 硬件描述、.xci/.xml 工程配置、.bit/.bin 配置文件以及 .elf 可执行程序等目录结构完整便于对照学习和二次开发。目前已有 1685 人学习下载。借助这套源码读者可以深入理解 Zynq 平台软硬件协同开发流程、AXI 接口的使用方法、PL 与 PS 之间的数据交互方式并掌握从 ADC 采样到 LCD 显示的完整链路设计思路。资源中还包含大量 SDK 工程与配置文件对想自制示波器或学习高速采集系统的开发者来说具有直接参考价值。 最近在小梅哥Zynq开发板上基于AD9288做了一台简易示波器整个工程从硬件电路到PL逻辑再到PS端波形显示全部源码都在调通之后感觉这项目特别适合拿来练手。示波器本身是嵌入式开发最常用的调试工具与其天天用别人的不如自己动手搓一台。这篇文章我把整条链路拆开来讲为什么用Zynq而不是单片机和纯FPGA采样率带宽这些指标怎么取舍PL端触发和缓存怎么写PS端怎么把数据变成波形以及我在调试过程中踩过的那些坑。如果你是FPGA入门不久、想搞明白ARMFPGA怎么协作或者单纯想给自己做一个低频调试利器这篇内容应该能帮你省不少时间。1. 整体方案为什么选Zynq做示波器指标怎么取舍1.1 为什么Zynq是这个场景的最优解我记得最早想自己做示波器的时候第一个跳出来的方案是STM32。STM32虽然开发简单、资料多但内置ADC一般也就几Msps做到十几Msps已经是少数高速型号而且ADC采样和波形刷新还是串行处理的实时性差。用STM32做一个测音频级别的示波器还可以一旦想测MHz级别的信号就很吃力。换成纯FPGA方案之后采样速度问题解决了但新问题又来了波形怎么显示按键怎么扫描数据和上位机怎么通信这些控制逻辑用Verilog写起来极其痛苦每加一个功能都要重新综合实现调试效率太低了。Zynq这个架构可以说就是冲着这个矛盾来的。它内部有一个双核ARM Cortex-A9处理器PS端和一片可编程逻辑PL端两边通过AXI总线高速互联。示波器这种应用天然适合分工合作PL端负责高速数据采样、触发判断、FIFO缓存这些对时序敏感的操作PS端跑C程序负责从FIFO读数据、换算成电压值、绘制波形、响应按键和触摸屏。用我自己的话说就是FPGA干粗活ARM干细活两边各干各擅长的。1.2 采样率、带宽、存储深度这些核心指标怎么取舍示波器的关键参数无外乎四个采样率、带宽、存储深度、触发方式。做之前先把这些指标定下来后面代码和设备选型才不会跑偏。先说采样率和带宽。根据奈奎斯特采样定理采样率至少是信号最高频率的两倍才能不混叠但那只是理论上限。工程上我一般按5到10倍来取比如要稳定观察10MHz的正弦波ADC采样率最好做到50Msps以上。我用的是双通道8bit的AD9288单通道最高100Msps只开一个通道时采样率可以跑到100Msps对测量1MHz到20MHz以内的信号完全够用做入门级仪器很合适。然后是存储深度也就是一屏能存多少个采样点。这个决定了你看到的时间窗口有多宽。我用FPGA内部的BRAM做FIFO缓存深度设成2048点也就是每触发一次采2048个点。在100Msps采样率下2048个点就是大约20微秒的时间窗口看高频信号够了。如果想看更长时间的波形可以把采样率降下来后续也可以把数据搬到DDR里面做深度存储那就是后话。触发这块我做了最基础的上升沿和下降沿触发支持通过寄存器设置触发电平阈值。为什么触发很重要如果没有触发波形会乱滚因为每次采集的起点都不一样。有了触发之后每次等待信号跨过设定阈值再开始采集这样波形就能稳定显示。这部分逻辑放在PL端做速度极快这是Zynq做示波器相比纯ARM方案的一个大优势。2. 硬件细节模拟前端、ADC与PCB上的几个关键决策2.1 模拟输入通道和偏置抬升电路示波器测的是外部信号但ADC的输入范围是固定的直接接进去肯定不行前端必须做好调理。我的做法分三步阻抗匹配、衰减、偏置抬升。阻抗匹配上输入用了标准的50欧姆BNC座并联一个50欧电阻到地这样和外界的同轴电缆阻抗匹配能最大限度减少信号反射。衰减部分做了一档简单的1x/10x切换通过跳线帽或者继电器实现10x档在测大电压信号时用避免前端运放饱和。最关键的其实是偏置抬升。ADC的输入范围是0到2V而我们要测的交流信号是双极性的比如正负1V的正弦波。直接输入的话负半周完全落在ADC量程之外根本采不到。解决方案是在运放跟随器后面加一个1V的直流偏置把信号整体抬高1V让正负1V的信号变成0到2V正好填满ADC量程。我把这个1V偏置用精密基准源产生实测很稳定。这里提醒一下前端运放一定要选高速型号比如AD8031或者OPA836这类增益带宽积要远大于你关心的信号频率否则放大器本身就把信号衰减了。我踩过一次坑最初手边只有一个LM358测试方波的时候上升沿明显变缓用频率计一看带宽不到1MHz换掉之后波形立刻正常了。2.2 ADC选型、采样时钟和电源处理ADC我是直接用了开发板上板载的AD9288。这颗芯片是双通道8bit、最高100Msps的并行ADC在入门级示波器项目里很常见淘宝二手也便宜资料好找。它需要外部提供采样时钟我用PL端的MMCM/PLL从开发板50MHz晶振生成100MHz时钟再用这个时钟给ADC和FPGA采数逻辑同步工作。这里有个工程细节ADC的采样时钟质量直接影响采样结果。我当时在PCB上引线比较随意时钟线走了很长一段才到ADC引脚结果采样数据偶尔出现跳动。后来在时序约束里把采样时钟路径单独约束同时在ADC时钟引脚附近加了一个电阻做串阻端接问题基本解决。如果你直接用开发板这些都不用管板厂已经处理好了。电源方面模拟电路和数字电路尽量分开供电。开发板通常已经做了模拟地和数字地的分割但如果你自己画板子一定注意在ADC芯片下方用磁珠或0欧电阻把模拟电源和数字电源连接起来避免数字开关噪声通过电源耦合进模拟前端。另外ADC的参考电压要稳定最好是独立LDO供电不要和FPGA的IO电源混在一起否则测出来的波形会有周期性毛刺。3. 核心源码PL侧采样逻辑与PS侧波形显示的实现思路3.1 PL侧触发判断、FIFO缓存与跨时钟域处理PL端是整个示波器的核心它的工作流程可以概括成一句话等触发、采数据、存FIFO、通知ARM来取。我先定义了一个触发状态机。系统复位后进入等待状态然后持续比较ADC数据是否跨过设定阈值。以上升沿触发为例如果上一个采样点小于阈值而当前采样点大于等于阈值说明信号正在上升穿越触发电平这时触发标志拉高开始采集。下降沿触发逻辑完全对称只是把比较方向反过来。触发逻辑在Verilog里写起来很直接核心代码大致这样// 触发检测上升沿触发 wire trigger_rise (adc_data_d1 thresh) (adc_data thresh); wire trigger_fall (adc_data_d1 thresh) (adc_data thresh); always (posedge clk) begin if (!rst_n) state IDLE; else begin case (state) IDLE: begin if (trig_mode RISE ? trigger_rise : trigger_fall) state ACQUIRE; end ACQUIRE: begin if (fifo_wr_count DEPTH - 1) state DONE; end DONE: state IDLE; endcase end end触发之后ADC数据连续写入FIFO写满2048个点就停止同时拉高中断信号给ARM。这里特别要注意的是跨时钟域问题ADC采样率是100MHz但ARM读取数据用的AXI总线时钟可能也是100MHz或者更高两个时钟虽然同源但相位和频率都不完全一样直接用同步FIFO很容易丢数据。我用的方案是异步FIFO写时钟接ADC采样时钟读时钟接AXI时钟这样两边互不干扰数据在读写速率不匹配的情况下也不会丢点或重复。3.2 PS端寄存器读写、数据换算与波形绘制PS端的程序我分为三个部分AXI-Lite寄存器读写、FIFO数据读取、波形绘制。为了尽量简单我没有用AXI DMA而是直接映射了一组寄存器ARM通过这组寄存器控制PL端的触发方式、阈值设置以及读取FIFO数据。这种方式带宽不高但对于2048点×8bit的数据量来说读取一次也就几十微秒刷新率够用而且代码直观适合学习和调试。寄存器规划如下地址偏移0x00是控制寄存器bit0使能采集bit1选择上升沿或下降沿触发0x04是触发电平寄存器写入8bit阈值0x08是状态寄存器PL端采满数据后会置位bit00x10到0x1FFF是数据读取区ARM按顺序读取2048个地址每个地址返回一个字节的ADC采样值。波形绘制是把数据变成图像的关键一步。ADC输出的是8bit二进制数范围0到255但并不是直接拿来画图要先换算回电压值。换算公式很简单float voltage (adc_value / 255.0f) * VREF - V_OFFSET; // VREF是ADC满量程2.0VV_OFFSET是偏置1.0V float input_voltage voltage / attenuation; // attenuation为1或10取决于前端档位其中V_OFFSET就是我在硬件部分提到的1V直流偏置attenuation则是1x/10x档位。算出来真实电压之后再映射到LCD屏幕的Y坐标。我用的屏幕是800×480的RGB LCD横轴0到799对应2048个采样点中按比例抽样的数据纵轴中心线对应0V位置正电压向上负电压向下。画波形时用两条扫描线交替擦除绘制避免闪烁。界面部分我还加了简易的网格背景和电压/时间刻度显示虽然不如商业示波器那么花哨但已经能直观读幅度和周期。按键做了四个触发方式切换、阈值加减、时基调节、暂停刷新。这些都用轮询方式实现裸机跑没有上操作系统代码量不大但五脏俱全。4. 调试实录波形毛刺、触发乱跳等常见问题的排查4.1 波形毛刺和错位问题多半出在跨时钟域或FIFO空满信号第一次把整个工程跑起来的时候我输入一个1kHz的正弦波屏幕上显示的波形大致形状是对的但边缘全是毛刺而且每过几个点就有一个异常跳变。我当时第一个反应是ADC坏了后来用示波器直接量ADC输出引脚发现数据引脚在采样时钟沿附近有抖动FPGA在这个不稳定窗口里采到了不确定的值。这个问题的根子是数据和时钟的时序关系没有约束好。我当时的解决办法分两步先把ADC数据输入引脚在FPGA内部打一拍再使用确保采到的是稳定值然后在XDC约束文件里给采样时钟专门建了时钟约束把ADC数据线的input delay也算进去。打完补丁之后重新生成比特流毛刺基本消失。另外FIFO的读写指针跨时钟域也可能导致读取到空数据或者重复数据。检查方法很简单在PL端拉出FIFO的empty和full信号用ILA观察时序看看ARM读数据的时候FIFO是不是已经被写满了。如果ARM读得太早会读出大量同一地址的重复数据波形就会变成很多条水平线。4.2 触发不稳定、波形左右乱跳检查阈值和迟滞第二个典型问题是触发不稳定。在低噪声信号下基本正常但一旦信号稍微带点噪声波形就在触发点附近抖来抖去看起来像水平方向在呼吸。这是典型的噪声误触发信号在阈值附近上下抖动可能一个时钟周期内来回穿越了好几次触发点每次都不一样。解决方法是给比较器加迟滞。也就是说把触发逻辑从单一阈值改成两个阈值触发开始需要信号从低于low_thresh穿越到高于high_thresh而复位等待则需要信号跌落到low_thresh以下。两个阈值之间拉开几个LSB的差距噪声就没办法引起反复触发了。这个改动代码量很少但效果立竿见影波形立刻稳定下来。还有一种情况是阈值设置超出信号范围比如输入信号是1Vpp的正弦波偏置1V但阈值设成了2V那么信号永远达不到阈值示波器就一直在等待状态屏幕上是一条直线。遇到这种情况不要急着查代码先算一下你输入的信号范围和阈值是否匹配。4.3 信号完整性问题电源纹波和地弹造成的周期性干扰有时候你会看到波形上叠加了一串很规律的毛刺频率恰好和开发板上的某个时钟频率一致。这时候就要怀疑是不是电源或者地的干扰。我自己就遇到过波形上每隔一段就有一个很小的尖刺用频谱分析一查频率正好是板上某个DDR时钟的倍频。这种问题属于信号完整性问题不是逻辑代码能完全解决的。我能给的建议是模拟前端和ADC尽量远离FPGA的高速数字接口采样时钟线的包地做好如果怀疑电源噪声用LDO单独给模拟部分供电并在靠近ADC电源引脚的地方并联一组0.1uF和10uF的电容。如果你用的是现成开发板先确认供电跳线和电容是否完整开发板设计通常没问题但电源状态要检查。4.4 XDC时序约束和综合策略的几个坑很多初学者写PL代码不看时序约束这在低速逻辑里问题不大但示波器项目里有时序要求的信号太多了采样时钟、ADC数据总线、FIFO读写指针、AXI接口全都要约束。不作约束的话实现结果随缘可能这一次综合成功下次改一行代码就时序违例。我建议在XDC里至少做这几件事用create_clock声明采样时钟和AXI时钟的频率用set_input_delay约束ADC数据引脚对跨时钟域的同步器加set_async_reg约束避免工具把它们当成普通寄存器优化。另外综合策略上把Optimization Strategy设为Performance而不是Area毕竟示波器更看时序而不是省资源。这些约束看上去麻烦但调完一次之后整套工程都能复用一劳永逸。5. 后续还能怎么扩展从简易示波器到桌面级小仪器5.1 Linux动态加载FPGA这个调试技巧强烈推荐整个示波器工程调通之后我又试了一种更舒服的开发流程在Zynq上跑Linux然后在系统运行中动态加载FPGA比特流。这种方式在调试PL逻辑时特别方便——传统方式下每次修改Verilog代码都要重新编译整个工程、重新烧写启动镜像光启动Linux就要一两分钟。而用动态加载的方法Linux系统启动后随时可以用工具把新的bit文件加载到PL端几秒钟就能完成一次迭代。具体就是在Linux内核配置里打开FPGA管理器支持然后在系统起来之后调用fpgautil把bitstream加载进去。需要注意的一点是PL端的地址空间映射和中断号要在设备树里提前声明好否则动态加载之后PS端访问不到PL的寄存器。这种玩法特别适合像我这样频繁改PL逻辑的调试场景也让你对Zynq的ARMFPGA可重构特性有更直观的理解。5.2 示波器的功能扩展还能往哪些方向走这个项目做完基础功能之后还可以加很多实用扩展。硬件上可以加一个更高位数的ADC比如12bit的AD9226分辨率提升几个数量级也可以加可控增益放大器PGA来实现更精细的垂直档位调节。软件上可以做FFT频谱显示Zynq的ARM跑1024点FFT只需要几十微秒实时频谱完全可行还可以做自动测量比如自动计算峰峰值、频率、上升时间这些功能对商业示波器来说是标配但自己做出来成就感完全不同。另外如果做出来了别忘了顺手给板子加一个网口或者USB传输功能把采集到的波形数据上传到电脑上保存分析配合Python的matplotlib画图也是很实用的玩法。把基础仪器做成可联网的小设备后续能玩的花样非常多。做这个项目的过程中我最深的感受是真正难的不是写代码而是把采样—存储—显示这条链路里的所有细节都想明白。每一个看似简单的功能背后都藏着硬件、逻辑、软件三个层面的协作。遇到波形不对的时候先用排除法确定问题出在哪一层——先用信号发生器给一个规律信号然后用ILA看FPGA内部数据再逐级排除到前端电路效率比瞎猜高得多。还有一个小技巧如果你手头有现成的示波器记得用它来调试你自制的示波器两个示波器对照着看排查问题会轻松很多。项目源码我整理在工程目录里了如果你也要在小梅哥Zynq开发板上复现建议从PL端采样逻辑开始逐个模块仿真验证然后再整套上板调这样能少走不少弯路。本文还有配套的精品资源点击获取