news 2026/9/16 1:30:55

FPGA并行ADDA转换与VGA波形显示:AD9708/AD9280时序分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA并行ADDA转换与VGA波形显示:AD9708/AD9280时序分析

简介:这套FPGA读写AD9708+AD9280的ADDA实验工程,面向数字电路与FPGA学习者,尤其适合正在练习AD/DA驱动时序和VGA波形显示的开发者。工程基于Cyclone IV E系列EP4CE6F17C8器件,使用Quartus 17.1开发,完整覆盖ADC数据采集、DAC波形输出以及VGA显示三个核心环节,代码注释清晰、模块划分明确,可直接下载后打开工程编译验证。压缩包共163个文件,以Verilog源码、Quartus工程配置(.qsf/.qpf)、初始化数据(.mif)、编译报告与PDF文档为主,另含可烧录的.sof文件,包体仅5.33MB,便于快速上手。配套文档对AD9708与AD9280的接口逻辑和实验现象做了说明,能帮助读者理解数模转换中的数据同步与时钟设计思路。目前已有945人学习,适合希望从零搭建ADDA采集与显示链路的FPGA初学者参考。

1. 为什么AD9708+AD9280的ADDA实验值得用VGA来验证

拿到这个名为 an108_adda_vga_test 的 Quartus 工程时,压缩包里一半是编译中间文件,真正值得看的是 Verilog 逻辑:FPGA 同时驱动 AD9280 做模数采集、AD9708 做数模回放,再把采到的数据送到 VGA 显示器画波形。很多做 FPGA 入门的人会卡在并行 ADC/DAC 的时序建立上,不是看不懂代码,而是不知道为什么 AD9280 的 8 位数据会在 VGA 上变成一团噪点。这套逻辑的价值在于,它把 ADC 缓存、DAC 波形发生器和 VGA 显示放在同一个时钟框架里,适合想独立调通 ADDA 读写、又不想上逻辑分析仪的人。文章从头分析接口时序和代码分段,最后给出工程恢复和 SignalTap 调试的完整路径。

2. 读懂AD9708和AD9280的并行时序,再拆顶层信号

打开 top 模块能看到三类信号:ADC 输入、DAC 输出和 VGA 输出。信号命名里 adc0_buf 表示一级缓存,wave0 表示叠加后的波形层,grid 表示网格层。先弄清每个信号到底由谁产生、被谁消费,再谈代码,不然很容易把 adc_clk 和 dac_clk 当成同一个时钟,后面全乱。并行 ADDA 的特点是引脚数多但协议简单,难点就两个:一是采样数据的同步锁存,二是数据在跨时钟域时的缓存写入。

2.1 AD9280:一个时钟沿抓一次8位数据

AD9280 是 8 位并行 ADC,最大采样率在 32 MSPS 级别,编码时钟由 FPGA 直接输出到 ad9280_clk 引脚。手册上 CLK 上升沿启动一次转换,数据经过流水线延迟后出现在 ad9280_data[7:0] 上。因此当前时刻读到的数据并不是当前时刻的模拟输入,而是几个采样周期前的历史值,但只要后续缓存和 VGA 扫描保持连续,这个延迟不会被肉眼察觉。

reg [7:0] ad9280_ff1; reg [7:0] ad9280_ff2; always @(posedge adc_clk or negedge rst_n) begin if (!rst_n) begin ad9280_ff1 <= 8'd0; ad9280_ff2 <= 8'd0; end else begin ad9280_ff1 <= ad9280_data; ad9280_ff2 <= ad9280_ff1; end end

这里打两拍的目的不是消抖,而是把引脚上的并行总线数据同步到 adc_clk 域,降低亚稳态向后续逻辑传播的概率。ad9280_ff1 是第一拍,直接采 ad9280_data;ad9280_ff2 是第二拍,通常作为实际数据参与缓存写入。如果你发现波形上出现随机毛刺,先把这两拍补上,再检查后面 RAM 的写使能有没有用过 ad9280_ff1。对 AD9280 这种并行总线来说,牺牲一个采样周期完全不影响显示效果。

2.2 AD9708:数据与时钟的边沿要对齐

AD9708 是 8 位并行 DAC,最高 125 MSPS,FPGA 侧只要送 8 位数据和时钟。DAC 在 ad9708_clk 上升沿锁存数据,所以数据端必须满足建立时间。工程里 dac_data 来自一个 512 深度的 ROM,ROM 地址在每个 dac_clk 下降沿加 1,这样 ROM 输出经过内部寄存器后,在下一个上升沿仍然稳定,相当于给 DAC 留出了半个时钟周期的建立时间裕量。

always @(negedge dac_clk or negedge rst_n) begin if (!rst_n) rom_addr <= 9'd0; else rom_addr <= rom_addr + 9'd1; end rom u_sin_rom ( .address(rom_addr), .clock(dac_clk), .q(dac_data) );

这里的关键是 ROM 输出天然比地址晚一个 dac_clk 周期,所以地址在下降沿跳变、DAC 上升沿采样时,dac_data 已经稳定。如果你换用组合逻辑实现的查找表,一定要重新做时序分析,否则高速下会出现 DAC 输出频谱变差。这个工程是低速演示,dac_clk 通常和 adc_clk 同频,但相位需要错开,后面 PLL 配置会细说。

2.3 VGA 波形显示:双口 RAM 的读写仲裁

VGA 640x480@60 需要 25 MHz 像素时钟,工程里 video_clk 就是这个频率。AD9280 采样数据按顺序写入深度为 2048 的 RAM,VGA 控制器扫描到有效显示区时,按列地址从同一块 RAM 读出数据,再映射成波形亮度。写入侧时钟是 adc_clk,读出侧时钟是 video_clk,两边频率、相位都不同,所以不能用单口 RAM,必须用双口 RAM。

常见的做法是写侧用一个状态机控制 adc0_buf_wr、adc0_buf_addr、adc0_buf_data,读侧用行内计数器生成 vga_rd_addr。读写地址在逻辑上没有互斥关系,DPRAM 内部会仲裁,但可能会有一行数据前半部分是新采样、后半部分是旧采样的现象,表现为波形在水平方向轻微错位。后面第四章我会给出一个用双帧切换消除这个问题的思路。

2.4 顶层信号怎么对应到硬件引脚

信号名方向位宽在工程里的作用
ad9280_datainput8AD9280 并行采样输出
ad9280_clkoutput1送给 AD9280 的采样时钟
ad9708_dataoutput8送给 AD9708 的并行数据
ad9708_clkoutput1送给 AD9708 的锁存时钟
vga_out_hs / vga_out_vsoutput1 / 1VGA 行场同步
vga_out_r / g / boutput5 / 6 / 5VGA 模拟 RGB 引脚

top 模块里还有 wave0_hs、grid_hs 这些内部信号,它们不是引脚,而是 VGA 时序经过图层叠加后的中间总线。调试 ADDA 实验时,最值得抓的是 adc0_buf_wr、adc0_buf_addr 和 adc0_buf_data 这三个内部信号,它们能直接告诉你 ADC 数据有没有真正进入缓存。引脚约束建议按原理图逐一分配,不要靠 Quartus 自动分配,否则 PIN 位置对不上,板子会毫无反应。

3. Verilog 代码实现:PLL 分频、ADC 缓存与 DAC 波形发生器

顶层代码本身不算长,但数据通路值得拆成三段看:时钟产生、ADC 写入 VGA 读取、DAC 波形回放。这个实验的全部核心在 PLL 配置和双口 RAM 例化上,这两处只要有一处参数不对,VGA 画面就会乱。先看时钟树,再看缓存,最后看叠加输出,顺序不能反。

3.1 PLL 配置:25MHz 像素时钟与 50MHz ADDA 时钟

Quartus 17.1 里通过 IP Catalog 生成 ALTPLL,输入时钟选板载 50 MHz,输出三路:c0 给 VGA 25 MHz,c1 给 ADC 采样时钟 50 MHz,c2 给 DAC 时钟 50 MHz,并且 c2 与 c1 反相 180°。反相的目的是让 AD9708 在采样沿能看到更稳定的数据,因为数据来自 ROM 寄存器,天然落后 dac_clk 一个周期。

pll u_pll ( .inclk0(clk), .c0(video_clk), .c1(adc_clk), .c2(dac_clk), .locked(pll_locked) ); wire rst_n_real = rst_n & pll_locked;

加上 pll_locked 作为复位条件,是为了防止 PLL 未稳定时逻辑就启动,导致 RAM 或 ROM 处于非正常状态。如果你不关心上电时序,直接用 rst_n 也能跑,但遇到 VGA 画面闪烁、波形位置随机漂移时,先检查这里的复位电路。50 MHz 的采样时钟对 AD9280 完全够用,实际上这个工程跑 25 MHz 也看不出差别,频率低一点反而抗干扰更好。

时钟名频率用途相位
video_clk25 MHzVGA 行场扫描和图层叠加
adc_clk50 MHzAD9280 采样和数据写入
dac_clk50 MHzAD9708 数据锁存180°

3.2 ADC 缓存:用双口 RAM 把采样数据跨到像素时钟域

缓存模块我习惯直接例化 altsyncram 或 altdpram,深度 2048,地址位宽 11 位。写端口时钟是 adc_clk,写使能是 adc0_buf_wr,写数据是 ad9280_ff2;读端口时钟是 video_clk,读地址由 VGA 行扫描模块产生。下面代码按 altdpram 原语写,实际工程可以在 IP Catalog 中生成同功能的双端口 RAM。

altdpram adc_ram ( .wrclock(adc_clk), .wren(adc0_buf_wr), .wraddress(adc0_buf_addr), .data(adc0_buf_data), .rdclock(video_clk), .rdaddress(vga_rd_addr), .q(vga_rd_data) );

写侧逻辑上,adc0_buf_wr 应只在有效采样期间拉高。最稳妥的写法是:adc0_buf_addr 从 0 到 2047 循环,每来一个 adc_clk 上升沿,只要数据有效就写一次;当地址到达 2047 后回卷,同时翻转一帧计数器。这样 VGA 读侧每次扫描一行就能读到连续 2048 个采样点。如果读侧地址范围超过 2048,读到的就是重复数据,波形会横向拉伸,所以 vga_rd_addr 必须限制在 10 位以内,剩下高位取行计数器低位借用。

这里最容易犯的错误是写使能长期拉高,导致同一地址被反复改写,波形出现断层。我一般会在 SignalTap 里同时观察 adc0_buf_wr 和 adc0_buf_addr,确保写使能是一个时钟周期脉冲,而不是电平。

3.3 DAC 波形回放:ROM 查找表生成正弦波

AD9708 的数据可以来自 ADC 环回,也可以来自 ROM 查找表。这个工程里用了一个 512 点正弦 ROM,地址位宽 9 位,数据位宽 8 位。每来一个 dac_clk 上升沿,ROM 输出一个正弦码值;地址在 dac_clk 下降沿累加,保证数据在上升沿稳定。生成 ROM 初始化文件可以用 Python 一行循环。

import math with open("sine.mem", "w") as fp: for i in range(512): v = int(127.5 + 127.5 * math.sin(2 * math.pi * i / 512)) fp.write(f"{v:02x}\n")

这段代码把正弦波一个周期量化成 512 个点,数值范围 0x00 到 0xFF,0x80 对应零电平。ROM 在 Verilog 中用$readmemh加载,Quartus 综合时会把它实现为内部存储器块。DAC 输出频率按公式 50 MHz / 512 = 97.7 kHz 计算,这个频率远低于 AD9708 的极限,也低于 AD9280 的实际采样能力,适合直接观察。如果你想要可调频率,只需要用 NCO 累加器替代固定地址加 1,把步进值改成键盘输入即可。

3.4 图层叠加:网格优先还是波形优先

VGA 显示粒子效果来自两层信息的叠加:网格层和波形层。网格层是固定图案,波形层来自 ADC 缓存读出的 vga_rd_data。我常用 select 信号做优先级,网格像素设置为最高优先级,网格位置输出白色,其他位置输出波形颜色,这样波形会被网格切割,便于读数。

assign video_r = grid_pixel ? grid_r : wave_r; assign video_g = grid_pixel ? grid_g : wave_g; assign video_b = grid_pixel ? grid_b : wave_b; assign vga_out_r[4:0] = video_r[7:3]; assign vga_out_g[5:0] = video_g[7:2]; assign vga_out_b[4:0] = video_b[7:3];

8 位颜色截断成 5/6/5 位时,直接扔掉低 3 位或低 2 位。这是最省 LUT 的转换方式,也符合 VGA 电阻网络的实际精度。若你的板子 RGB 顺序是 565,green 的高位分配方式可能不同,具体看原理图里的电阻网络接在哪几个数据脚上。不要在这里做加权平均,VGA 模拟输出本身精度有限,低两位的舍入误差肉眼完全看不出。

4. Quartus 17.1 工程恢复、引脚约束和 SignalTap 调试

这个压缩包里有一堆 .ammdb、.bpm、.cdb 文件,它们都是 Quartus 编译过程留下的数据库,不是源码。拿到手第一步不是打开工程,而是先清理编译中间文件,再干净地重建一次。很多人在这一步走了弯路,双击 .qpf 打开后发现引脚没有分配,或者编译直接报错,就是因为中间文件覆盖了新写入的配置。

4.1 哪些文件保留,哪些可以删除

文件类型作用处理建议
.qpf / .qsf工程定义和配置保留
.v / .sv逻辑源码保留
.sdc时序约束保留
.ammdb / .bpm / .cdb编译中间数据库删除
.sof / .pof下载和固化文件可重新生成
.qdbQuartus 数据库可删除

解压后先删除编译中间文件,让 Quartus 重新做全量分析,避免旧数据库干扰新约束。如果你用的是 Git Bash 或者 Linux 终端,可以这样清理:

cd /d/project/an108_adda_vga_test rm -rf db incremental_db find . \( -name "*.ammdb" -o -name "*.bpm" -o -name "*.cdb" \) -delete

删除后启动 Quartus,选择 File -> Open Project,找到 .qpf 打开。如果压缩包里没有 .qpf,则需要新建一个工程,器件选择 Cyclone IV E 系列 EP4CE6F17C8,顶层模块设为 top,把 zip 内所有 .v 文件添加进去,再手工补上 SDC 约束。编译第一次会慢一些,因为缓存全部重建,但之后的增量编译会恢复正常。

4.2 引脚约束:QSF 和 SDC 一个都不能少

引脚约束写在 .qsf 里,用 QSF 语法。EP4CE6F17C8 是 256 引脚封装,不同开发板把 AD9280、AD9708 挂在不同的 BANK 上。下面是一段约束模板,具体 PIN 编号必须按你手上的原理图改:

set_location_assignment PIN_85 -to clk set_location_assignment PIN_86 -to rst_n set_location_assignment PIN_101 -to ad9280_data[0] set_location_assignment PIN_102 -to ad9280_data[1] set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to ad9280_data[*] set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to ad9708_clk set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to vga_out_hs

IO_STANDARD 用 3.3-V LVTTL 即可,因为 Cyclone IV E 的大多数引脚都在 3.3 V BANK。注意 ad9280_data[7:0] 全部要约束到同一个 BANK,且电压标准一致,否则编译会报 I/O bank mismatch。VGA 同步引脚 HS/VS 是单端输出,不用加差分标准。

时序约束方面,至少要创建两个主时钟:板载输入时钟和 PLL 输出时钟。PLL 输出时钟可以在 SDC 中直接 get_nets,也可以让 Quartus 综合后自动传播。初学者最常犯的错误是完全不写 create_clock,导致信号完整性分析默认按住 1 GHz 评估,时序很难收敛。

create_clock -period 20.000 -name sys_clk [get_ports {clk}] create_clock -period 20.000 -name adc_clk [get_nets {u_pll|c0}]

SDC 文件需要添加到工程的 Timing Settings 里。如果不加,工程能编译,但 Fitter 会随意布线,高速 ADDA 场景下波形可能出现时序违规。低速实验可以跑,但我不建议养成这个习惯。

4.3 SignalTap II:抓内部信号比万用表好用

ADDA 实验调波形时,我只信 SignalTap II。不需要外部逻辑分析仪,也不需要额外接线,直接在下载器里跑。方法是在 Quartus 里创建一个 .stp 文件,添加 adc_clk 作为采样时钟,添加 adc0_buf_wr、adc0_buf_addr、ad9280_ff2 作为观测节点,触发条件设为 adc0_buf_wr 的上升沿。

设置项推荐值目的
Sample depth2048覆盖一整帧波形数据
Trigger positionCenter同时看到触发前后数据
Trigger conditionsadc0_buf_wr == 1捕获写入起始点
Storage qualifieradc0_buf_addr < 2048限制存储区间

编译后打开 Programmer,下载 .sof 到 FPGA。运行 SignalTap,等一段时间如果采集到波形,你会看到 adc0_buf_wr 是一个窄脉冲,adc0_buf_addr 在 0 到 2047 之间循环。如果看到 adc0_buf_wr 长期为 1,说明状态机有问题,采样数据会在同一地址反复覆盖,VGA 显示必然断层。

SignalTap 会占用器件内嵌存储资源,EP4CE6F17C8 只有约 270 Kbit RAM,配置 2048 深度采样完全够用,但不要同时开多组采样,否则会影响原工程的布线。

4.4 常见问题板级排查表

现象可能原因处理方式
VGA 画面全黑PLL 未锁定或 rst_n 无效SignalTap 观察 pll_locked 是否为高
波形固定在屏幕一侧读地址没有按行清零检查 vga_rd_addr 是否在行同步期间被复位
DAC 输出没有信号dac_clk 相位设置错误PLL 中把 dac_clk 改为反相 180°
AD9280 数据全是 0 或 FFADC 输入超出量程检查模拟输入是否偏置到量程中点
ModelSim 仿真报 license 错误仿真器环境变量未生效确认 LM_LICENSE_FILE 指向 license.dat

第一个值得排查的是 PLL,因为整个工程所有时钟都由它派生。第二个是 IO 标准配置,BANK 电压不对会导致采样数据读回来全是 FF。ModelSim license 的问题跟 Verilog 本身无关,但 Quartus 17.1 经常出现,主要是安装时没有设置环境变量,启动图形界面时没有自动加载。

5. 验证 ADDA 数据通路的土办法:从 DAC 自环到 SignalTap 导出 CSV

芯片级验证不需要昂贵的仪器,利用开发板本身的 AD9708 和 AD9280 就能搭一个自环。把 AD9708 的模拟输出经过电阻分压后接到 AD9280 的模拟输入,前提是两者电平范围匹配。如果 AD9708 输出是 0 到 1 V,而 AD9280 输入量程是 0 到 2 V,就直接串联电阻分压;如果板上已有跳线帽,直接短接最省事。信号源就是正弦 ROM,所以你不接外部信号源也能验证通路的完整性。

5.1 环回前必须确认的三个节点

先测 AD9708 数据端,再测 AD9708 时钟端,最后测 AD9280 时钟端。用万用表量 adc_clk 和 dac_clk 的直流电压,如果 PLL 输出正常,50 MHz 时钟用万用表测会显示约 1.65 V 左右的平均值,因为方波占空比 50%。如果平均值接近 0 V 或 3.3 V,说明时钟没有翻转,问题回到 PLL 配置或引脚约束。

环回接线要短。杜邦线超过 10 cm 时,AD9280 的输入会看到明显的振铃,采样数据低几位会跳动。如果必须用长线,串一个 33 欧姆电阻靠近 ADC 引脚,能改善反射。

5.2 用 SignalTap 导出 CSV 并还原波形

SignalTap 采到的数据不只有人肉观测一种用途,可以把整帧数据导出成 CSV,然后用 Python 重新画图验证波形质量。在 SignalTap 界面右键选择“Export”,格式选 CSV,导出后在 Python 里读取 ad9280_ff2 列,画前 512 个采样点:

import csv import matplotlib.pyplot as plt samples = [] with open("adc_capture.csv") as fp: reader = csv.reader(fp) header = next(reader) for row in reader: if len(row) >= 2: samples.append(int(row[1], 16)) plt.plot(samples[:512], linewidth=1) plt.xlabel("sample index") plt.ylabel("adc code") plt.grid(True) plt.show()

这里 row[1] 是 SignalTap 导出表中 ad9280_ff2 所在列,十六进制字符串转成整数。如果环回成功,你会看到一条 97.7 kHz 的正弦波,幅值约 256 格。出现阶梯感是正常的,因为 ADC 是 8 位,量化间隔就是 1 LSB。如果波形峰峰值不到 200 个码值,说明 AD9708 输出幅度没有充分利用 AD9280 的量程,需要调整偏置或放大电路。还有一个技巧:计算相邻两个样本的码值差,连续出现几十次 0 说明采样时钟没有连续翻转,优先检查 adc_clk 引脚上的容性负载。这个数据文件本身就是后续做数字滤波、FFT 分析的基础,不要把 SignalTap 只当成看波形的工具。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 1:28:24

基于TMS320F28335的并联供电系统数字均流控制设计

简介&#xff1a;本资源面向电子设计竞赛参赛者、电源方向学生及DSP开发者&#xff0c;提供一套完整的基于TMS320F28335的开关电源模块并联供电系统设计方案。系统以单端反激电路为功率级主回路&#xff0c;通过DSP采集两路DC模块输出电流并分别控制PWM&#xff0c;实现0&#…

作者头像 李华
网站建设 2026/9/16 1:26:43

AGV无线通信方案对比:从nRF24L01到工业WiFi的选型与部署

AGV这行干久了你会发现&#xff0c;真正让项目从“能跑”变成“跑得稳”的&#xff0c;往往不是底盘、不是电机、甚至不是调度算法&#xff0c;而是那条看不见的无线链路。AGV无线通信听起来是个基础话题&#xff0c;实际上牵扯到整车的控制闭环、多车调度、场地上位机交互&…

作者头像 李华
网站建设 2026/9/16 1:26:32

ComfyUI 接入 MiniMax H3:低显存本地部署与提示词优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 1:26:24

老旧Android工程解析:Camera采集+Socket推流的远程监控

简介&#xff1a;安卓Android源码——基于手机的远程视频监控系统&#xff0c;是一份面向Android初中级开发者的完整工程&#xff0c;演示如何利用手机摄像头实现远程画面的实时采集与查看&#xff0c;可用于毕业设计、课程项目或移动监控方向的技术练手。压缩包共41个文件&…

作者头像 李华
网站建设 2026/9/16 1:26:17

MySQL报错ERROR 1406 Data too long for column ‘name‘排查与修复指南

我上周就在一个线上项目里遇到这个报错&#xff0c;正在跑一个数据导入任务&#xff0c;往user_info表插一条用户自我介绍&#xff0c;结果一句几百字的文本刚塞进去&#xff0c;客户端直接甩出来一串红字&#xff1a;ERROR 1406 (22001): Data truncation: Data too long for …

作者头像 李华
网站建设 2026/9/16 1:26:05

扑克牌目标识别数据集标注全流程:从工具选型到YOLO训练验证

简介&#xff1a;面向目标检测入门与扑克牌识别场景的标注数据集&#xff0c;适合学习YOLO、SSD等检测模型的初学者&#xff0c;也适用于需要自定义扑克牌识别任务的开发者。数据集中图片均来自真实拍摄画面&#xff0c;标注类别涵盖queen、ten、nine、king、jack、ace六种常见…

作者头像 李华