简介:面向小梅哥Zynq开发板使用者的简易示波器完整工程,以FPGA+ARM双核架构为核心,灵活运用Zynq SoC的可编程逻辑与处理系统,适合电子设计、嵌入式系统及数字信号处理方向的开发者对照学习。工程包共752个文件、约8.68MB,以c/h源码、VHDL/Verilog硬件描述文件为主,并包含Xilinx工程配置(xci/xml)、Makefile、链接脚本及可生成的BOOT.bin镜像,覆盖从硬件逻辑到ARM端应用的全流程。项目实现ADC128S采集、FPGA实时采样、ARM数据处理与LCD波形显示,可直观理解采样控制、PS-PL协同、AXI总线交互等关键技术;完整目录结构便于检索与二次开发。已有1685人学习,适合希望深入掌握Zynq开发板软硬件协同设计的工程师与学生。 有一段时间,我手里的示波器总是在最关键的时候掉链子。印象最深的是调一块SPI接口的传感器板,老模拟示波器触发不稳定,波形在屏幕上左右乱跑,根本没法定时序。朋友一句玩笑话"你手上那块Zynq开发板不就能干这活吗",让我真动了手。花了两周时间,把基于小梅哥Zynq开发板的简易自制示波器跑通了,ADC采样、DMA搬运、LCD显示整条链路全部打通,源码也整理成了可以直接打开的Vivado和Vitis工程。
这套方案到底能干什么?简单说,它利用Zynq片内自带的XADC模数转换器完成最高1MSPS的双通道采样,把波形数据通过AXI DMA搬运到DDR3内存,再在LCD屏幕上实时绘制出来。配合相应的源码,你可以观察10kHz到200kHz内的常见信号,测量幅度、周期、占空比这些基础参数,也可以直接拿来当逻辑分析的前端采集设备。对刚入门FPGA、想把开发板真正用起来的读者来说,这是一个能同时练到PL逻辑、PS驱动、AXI总线协议和显示驱动四个层面的练手项目。
下面我把整个方案的选型过程、链路设计、源码结构、实测数据和踩坑记录完整过一遍。
1. 为什么用Zynq做示波器:三条路线的对比结论
1.1 MCU方案、纯FPGA方案和Zynq方案的本质差异
做自制示波器,常见路线有三条:用高性能MCU、用纯FPGA、用Zynq这种ARM加FPGA的异构平台。这三条路我都简单试过或者研究过,最终落在Zynq上,不是因为Zynq算力最强,而是因为它的分工方式最适合示波器这种"既要实时采集、又要复杂交互"的应用。
先看MCU方案。STM32F4系列内部自带12位ADC,采样率能做到2.4MSPS,理论上够看几十kHz的信号。真做起示波器才体会到问题:MCU的ADC需要用定时器精确触发,否则采样间隔抖动会让波形在水平方向扭来扭去;同时MCU的DMA只有固定的几个通道,要同时兼顾采样、按键扫描、LCD刷新和数据处理,反而处处受制。更麻烦的是,想要更好的信号调理电路,MCU方案的外围模拟电路一点都不少,还把自己的代码逻辑搞得特别复杂。
再看纯FPGA方案。FPGA在采样这一侧确实很强,你可以用并行ADC、高速LVDS接口,甚至直接做1GSPS的等效采样。但问题出在另一边:波形显示和人机交互。纯FPGA做LCD驱动需要自己写RGB时序,做触控、做菜单、做数据处理都只能用状态机硬怼,一个简单的"垂直缩放"功能可能就得写几百行Verilog。很多用纯FPGA做示波器的人最后都会被显示逻辑折磨得想放弃。
Zynq的优势在于把这两件事拆开了。PL端负责采样控制、FIFO缓冲、DMA数据通路,这些都是FPGA最擅长的高速并行逻辑;PS端则跑着ARM Cortex-A9处理器,用C语言处理波形数据、管理LCD显示、响应按键输入。两边通过AXI总线连接,数据流的吞吐和交互的灵活度都能兼顾。说白了,Zynq方案是让专业的人做专业的事,这套架构和我后来看的商用示波器内部设计思路是一致的——前端高速采集加后端嵌入式处理器。
1.2 小梅哥开发板上能直接利用的资源
选择小梅哥这块开发板,是因为它把Zynq方案所必需的外设基础都铺好了。板上用的是Zynq-7020,双核Cortex-A9处理器加上Artix-7架构的可编程逻辑,芯片内部集成了XADC模块,不需要外接ADC芯片就能开始采集实验。DDR3内存、RGB LCD接口、按键、LED、UART转USB这些也都是现成的,基本不需要额外飞线。
最关键的是片内XADC。这颗模数转换器虽然在专业示波器面前显得寒酸——12位分辨率、1MSPS采样率——但它解决了两个实际问题:一是降低了入门门槛,不需要一上来就面对并行ADC的布线、时序、数据对齐问题;二是它和PL端通过内部接口直连,可以在Block Design里直接拖出来用,配置起来非常简单。用这颗内置XADC跑通整条数据链路之后,再想升级外置的高速ADC,PL端的FIFO和DMA逻辑基本不用重写,只需要换采样接口,这个过渡成本非常低。
我还特意确认了板子上的LCD接口类型。小梅哥的这块板子用的是RGB888并口LCD,分辨率通常是800x480或者480x272,通过PL端VDMA控制刷屏。这意味着显示部分可以走"VDMA从DDR取图数据→LCD控制器→屏幕"这条标准通路,和采集链路里的AXI DMA复用同一个数据搬运思路,软件架构非常对称。
2. 采样链路设计:XADC、FIFO和AXI DMA的分工
2.1 XADC的工作机制与配置要点
XADC是Zynq内部带的一个双通道12位逐次逼近型ADC,采样率最高1MSPS。它除了可以测量外部模拟输入信号,还能监控芯片自身的温度和供电电压,这也是Zynq自带的一个"隐藏福利"。XADC在内部以16位数据格式输出,其中高12位是有效采样值,低4位是通道标识信息,这样在数据流里就能区分出当前数据来自哪个通道。
配置XADC时,有两点必须提前想清楚。第一是采样率,XADC内部有一个可编程的采样时钟分频器,默认工作在最高速率。实际使用时我建议先固定一个速率,比如1MSPS,然后通过外部输入信号的频率来反推每个周期有多少个采样点,这样调试起来逻辑清晰。第二是采样模式,XADC支持单通道、双通道交替、连续扫描等模式。做示波器的话,用双通道但是按单通道连续采集的方式最省事,每个通道的数据会带上不同的通道标签,PS端在数据处理时按标签拆分即可。
这里有一个很容易搞混的细节:XADC输出的数据率是固定的1MSPS,但AXI DMA的突发传输单位是4KB或者更大,这两者之间天然存在速率不匹配。如果不做缓冲直接连DMA,高概率会出现数据丢失或者DMA传输提前结束的情况。这就是为什么必须在XADC和DMA之间放一个FIFO,做数据缓存和速率转换。
2.2 FIFO缓冲解决的是速率匹配问题
FIFO在这里承担的角色,可以理解成一个"水龙头和桶之间的接水盆"。XADC一直在慢慢流数据,但DMA是攒一批搬一批。如果没有接水盆,水流太细,DMA这个"大桶"根本接不满就端走了,或者干脆一直等着,整体传输效率很低。有了FIFO,XADC的数据先持续灌进FIFO,等FIFO里的数据攒够了,再一次性通知DMA搬走,这样DMA每次突发传输都是满载状态,效率高得多。
具体在Vivado里,XADC IP核自带一个AXI4-Stream接口,可以直接接到FIFO Generator IP的写侧,FIFO读侧再接AXI DMA的S2MM通道。FIFO深度我实测下来选4096比较合适,对应1MSPS的采样率下大约4毫秒的数据量。如果将来换外置ADC,采样率提升到几十MSPS,FIFO深度需要加大到8192甚至16384,同时要注意FIFO的读写时钟域隔离——XADC输出的采样时钟和AXI DMA侧的时钟通常不是一个来源,必须用独立的读写时钟。
2.3 DMA搬运与中断:数据进DDR的正确姿势
采样数据进入DDR3这一步,用的是AXI DMA IP核的S2MM通道,也就是Stream转Memory-Mapped方向。DMA的配置不算复杂,但有一个地址对齐的要求必须遵守:缓冲区地址必须至少4字节对齐,否则DMA传输会报错或者数据错位。我在代码里直接给接收缓冲区加上了64字节对齐属性,既满足了DMA要求,也为后续做Cache操作留了余地。
DMA传输完成之后会触发中断。我踩过的一个比较典型的坑是:在中断回调函数里直接去处理波形数据、刷新屏幕,结果发现屏幕刷新一次还没画完,下一次DMA中断又来了,数据被覆盖,整个界面卡死。正确的做法是中断回调里只置一个标志位,把耗时操作全部交给主循环或者独立显示线程去处理。数据缓冲区也要做成轮流使用的多块结构,DMA在写当前块的时候,显示逻辑读上一块,两块互不干扰。
3. 源码工程拆解:PL端连接关系与PS端关键代码
3.1 Vivado里的IP连接关系
整个硬件工程在Vivado中是通过Block Design搭建的,核心IP有五个:ZYNQ7 Processing System(PS端)、XADC、FIFO Generator、AXI DMA、AXI GPIO(用于按键和LED控制),另外加上一个VDMA和LCD控制器IP用于屏幕显示。
连接关系是一条典型的采集数据流:XADC的AXI4-Stream输出接到FIFO的写侧,FIFO的读侧接到AXI DMA的S2MM通道,AXI DMA再通过AXI Interconnect连到PS端的DDR控制器。整个过程不需要手写任何Verilog做数据通路控制,唯一需要写的是对外部信号调理电路的控制逻辑。这里补充一点:小梅哥板子把XADC的外部输入引脚引到了扩展排针上,实测输入电压范围是0到1V,超过这个范围就需要先经过电阻分压或者运放衰减,否则可能会损坏XADC输入级。
3.2 PS端XADC初始化与DMA传输代码
PS端代码我用的是Vitis里的Xilinx SDK开发,用C语言写。工程分两块,一块负责初始化XADC并启动采样,另一块负责配置DMA并接收数据。先贴XADC初始化这一段:
#include "xadcps.h" #define XADC_DEVICE_ID XPAR_XADCPS_0_DEVICE_ID static XAdcPs xadc_inst; int xadc_init(void) { XAdcPs_Config *cfg; cfg = XAdcPs_LookupConfig(XADC_DEVICE_ID); if (cfg == NULL) return -1; XAdcPs_CfgInitialize(&xadc_inst, cfg, cfg->BaseAddress); // 清除告警使能,只保留用户自定义采样 XAdcPs_SetAlarmEnables(&xadc_inst, 0); // 设置连续转换模式,固定让XADC不停采样 XAdcPs_SetSequencerMode(&xadc_inst, XADCPS_SEQ_MODE_CONTINPASS); XAdcPs_SetStartOfConversionMode(&xadc_inst, XADCPS_STARTCONV_CONTINUOUS); return 0; }这段代码做的事情是:找到XADC设备,初始化驱动,然后把转换模式设为连续扫描。注意SEQ_MODE_CONTINPASS表示所有通道都开启连续转换,XADC内部会自动轮询所有使能的通道。
DMA这一侧代码如下:
#include "xaxidma.h" #define DMA_DEVICE_ID XPAR_AXIDMA_0_DEVICE_ID #define BUF_SIZE (8192 * 2) // 8192个采样点,每个点16位 static XAxiDma axi_dma; static u32 rx_buffer[8192] __attribute__((aligned(64))); int dma_init(void) { XAxiDma_Config *cfg; cfg = XAxiDma_LookupConfig(DMA_DEVICE_ID); if (cfg == NULL) return -1; XAxiDma_CfgInitialize(&axi_dma, cfg); XAxiDma_IntrDisable(&axi_dma, XAXIDMA_IRQ_ALL_MASK); return XAxiDma_SimpleTransfer(&axi_dma, (u32)rx_buffer, BUF_SIZE, XAXIDMA_DEVICE_TO_DMA); }这里的关键是XAxiDma_SimpleTransfer函数,它告诉DMA把数据从PL侧的Stream接口搬到内存地址rx_buffer。每次传输8192个采样点,对应1MSPS下大约8毫秒的数据窗口。aligned(64)是为了满足地址对齐要求,实测使用过程中,DMA的S2MM通道对地址对齐要求比较高,不满足时会出现数据头错位几个字节的诡异现象。
3.3 分配多个缓冲区,采集和显示互不等待
我把DDR里划分了8个采样缓冲区,每个缓冲区能存8192个采样点,构成一个环形队列。DMA按顺序往缓冲区里写数据,写完一个就触发一次完成中断,中断里把当前缓冲区的索引交给显示任务。显示任务读取的是上一帧数据,这样DMA在写新数据的时候,显示线程读老数据,两条流水线并行,屏幕刷新率可以稳定在25帧左右。
为什么坚持用多缓冲而不是单缓冲?因为单缓冲的方案下,DMA一旦覆盖数据,显示线程刚读一半就会读到新老混叠的波形,画出来就是撕裂的。多缓冲的成本只是多占用一部分DDR空间,而Zynq上的DDR3容量完全够用,这个性价比非常高。实际源码里,缓冲区切换逻辑只用了约30行代码,是整个工程里收益最高的一段设计。
4. 显示链路与触发逻辑:把波形画上屏幕的最后一公里
4.1 从ADC原始值到屏幕坐标的映射
采样数据进入DDR后,下一步是把这些原始值变成屏幕上看得懂的水平方向时间轴和垂直方向电压轴。直接拿原始数据往屏幕上画是行不通的,因为ADC原始值是12位的整数,范围0到4095,而LCD的垂直分辨率只有480像素,必须做一个映射。
垂直方向的映射公式很简单:screen_y = (adc_value - v_min) * (screen_height - 1) / (v_max - v_min),其中v_min和v_max是这次采集窗口内的最小值和最大值。这样波形会自动占满整个垂直范围,看小信号的时候非常方便。如果想要固定的垂直灵敏度,比如每格代表100mV,就需要提前用一个标定值把ADC原始值换算成电压值,再用电压值换算成像素位置,实现起来也不难。
水平方向的映射就要分情况了。如果采样点数小于等于屏幕宽度,直接一个点对应一个像素即可;如果采样点数大于屏幕宽度,比如800个像素宽度却有8192个采样点,就必须做抽点或者包络显示。直接隔N个点取一个的做法会漏掉毛刺,所以我采用的是包络显示:把一段区间内的最大值和最小值各取一个点,在同一列上画一条竖线,这样既保住了整体轮廓,又不会漏掉窄脉冲。
4.2 软件触发和硬件触发的取舍
示波器如果没有触发,波形在屏幕上就是不断滚动的,看不稳定。我第一版只做了自由滚动模式,实测下来发现方波和正弦波勉强能看,但要看某个特定位置的上升沿就特别费劲。后来加了软件触发:读取完缓冲区之后,先在一段数据里搜索第一个时间戳:当采样值从低于触发电平跨越到高于触发电平的位置,把这一点作为起始点开始显示。
软件触发的实现代价很小,只花了几十行C代码,效果却非常明显。对于几十kHz的中低频信号,软件触发已经足够稳定。但它的局限也很明显:如果信号噪声大,可能触发点跳来跳去;如果信号频率太高,一个缓冲区里可能有很多个上升沿,触发点选取需要额外做过滤。硬件触发的思路是在PL端加一个比较器逻辑,当采样值跨过设定阈值时才让FIFO开始记录数据,这样每次显示的波形相位都是严格对齐的。硬件触发改造我已经在源码里留了接口,后续可以直接把触发信号接进PL逻辑,做真正的边沿触发。
4.3 用VDMA刷LCD,闪屏问题是这样解决的
LCD刷新是这个项目里最容易做砸的部分。最初我把波形一帧一帧直接写到LCD寄存器里,一旦数据量稍微大点,屏幕就会闪烁或者上半屏、下半屏撕裂。后来我改成了标准的VDMA方案:在PL端例化一个VDMA IP,把DDR里的一块图像缓存区映射到LCD控制器,VDMA会自动按帧率把图像数据刷到屏幕上。PS端的显示代码只需要往这块图像缓存区里写像素数据,不需要管LCD的时序细节。
在这个基础上,我用的是双缓冲显示。DSDR里的图像缓存区分配两块,显示线程往当前帧缓存区里画波形,画完之后把VDMA的起始地址切换到新缓存区,同时让VDMA去扫描另一块。这样切换速度极快,肉眼完全感觉不到闪烁。实测下来帧率能稳定到25fps,拖动波形或缩放幅度时的实时性都够用。
5. 实测效果与调优记录:五个值得提前避开的坑
5.1 用函数发生器验证不同频率信号的实测表现
我把这套示波器接到一台信号发生器上做了一组实测,结果如下:
| 输入信号 | 频率 | 每周期采样点 | 实测表现 |
|---|---|---|---|
| 方波 | 10kHz | 100点 | 波形清晰,上升沿可见,占空比测量准确 |
| 方波 | 50kHz | 20点 | 能测量周期和占空比,边沿略显粗糙 |
| 正弦波 | 100kHz | 10点 | 幅值和频率测量准确,波形接近正弦 |
| 正弦波 | 200kHz | 5点 | 波形明显锯齿化,只建议做包络观察 |
| 三角波 | 20kHz | 50点 | 线性度良好,能准确测出对称度 |
从结果看,1MSPS的采样率理论最高能看500kHz的信号,但要波形看得舒服、参数测得准,实用上限基本在200kHz以内。这个结论对XADC方案的定位很有参考价值——它做低频、低速协议、电源纹波的观察完全没问题,但别指望它去测高速通信信号。
5.2 五个典型问题与解决过程
排错的过程其实是这个项目里最值钱的部分,我把遇到的五个主要问题按排查链路整理如下。
第一个问题是采样数据全零。排查过程从XADC寄存器读值开始,发现转换完成标志一直没有置位,最后定位到是XADC的复位信号没有正确释放。在Vivado里检查XADC IP的复位连接,确认复位信号由PS端控制并在初始化时置高,问题解决。
第二个问题是DMA搬运来的数据有错位。每帧数据的前几个字节总是对不上,排查后发现是缓冲区地址没有做对齐处理。DMA要求物理地址4字节对齐,我用__attribute__((aligned(64)))声明后,问题立即消失。
第三个问题是波形在屏幕上不断跳动。最初以为是触发不稳定,排查后确认是软件触发逻辑没有做"边沿方向判定"。信号噪声导致同一个方向反复跨越阈值,后来在触发逻辑里加了一个滞回窗口,低于低阈值才算下降沿、高于高阈值才算上升沿,波动就消失了。
第四个问题是LCD刷新有明显撕裂。排查后发现是单缓冲导致读写冲突,改成VDMA双缓冲后解决。这里需要注意的是,VDMA切换缓冲区的时序要放在垂直消隐期间,否则仍会出现轻微撕裂。
第五个问题是显示任务占用了大量CPU资源。用性能分析工具一看,耗时主要集中在坐标变换里的浮点运算上。我把垂直缩放的浮点计算改成整数运算和查表方式,CPU占用直接降了一半,帧率反而提升了。
5.3 这套源码接下来还能怎么扩展
硬件链路完全打通之后,扩展方向其实非常多。如果想把采样率提上去,最直接的办法是在PL端外接一片高速并行ADC,比如AD9288或者ADS828,把原来接XADC的FIFO换成接ADC的接口逻辑,DMA和显示部分完全不用动。想做成频谱分析的话,PL端可以例化FFT IP核,把时域数据在硬件上先做FFT变换,再通过DMA搬运到PS端绘图。
另外,Zynq的PS端是可以跑Linux的,如果你不太喜欢跑裸机程序,可以把这套裸机显示代码改成Linux下的应用程序,通过UIO或FPGA Manager的方式访问PL端的DMA设备。这样整个系统就能具备网络通信、文件存储的能力,波形数据可以直接通过TCP传到上位机,变成一个真正好用的实验室仪器。
我做这套东西最大的体会是,示波器的难点从来不在某一个环节,而在整条链路——模拟前端、采样、缓存、搬运、处理、显示的每个环节单独看都不难,串起来就很容易出问题。如果你也想在Zynq开发板上做点有意思的东西,这套源码会是一个很好的起点。
本文还有配套的精品资源,点击获取