简介:面向FPGA学习者的AD9238数据采集与VGA波形显示例程包,基于Cyclone IV E系列EP4CE6F17C8器件与Quartus 17.1环境,适合正在钻研FPGA与高速ADC接口、视频显示驱动的开发者。压缩包共167个文件,约4.91MB,核心为Verilog源码(v/tdf)、Quartus工程配置(qsf/qpf/qip)、时序约束(sdc)、编译报告(rpt/logdb)及PDF/readme说明文档,目录结构完整清晰。已有1125人学习下载。程序含顶层模块、AD9238双通道采样、地址缓冲、VGA时序生成与波形绘制等模块,并具备网格背景及双通道叠加显示能力。读者可直接打开Quartus工程运行与仿真,也可参照源码梳理ADC采样时钟、数据对齐和VGA同步信号的设计思路,对数字示波器、信号采集显示类项目具有直接参考价值。
1. 一张 640×480 的屏幕,怎么把 AD9238 的采样点画成波形
做 FPGA 数据采集的人迟早会碰到这类需求:ADC 以几十 MHz 采样,FPGA 把数据接住,然后直接在 VGA 显示器上画出实时波形,不经过上位机。标题里的 AD9238 是一片 12 位、双通道、最高 65 MSPS 的并行 ADC,而 VGA 显示的核心难点不在「画点」,在于两个时钟域之间的数据搬运和时序对齐。这个例程正好把 FPGA 入门到进阶的几个关键点串在了一起:并行 ADC 接口时序、跨时钟域存储、VGA 时序生成和 Quartus 工程的引脚约束。适合刚学完 Verilog 语法、想用一块开发板做出一个能看得见摸得着的采集系统的读者;也适合正在做 FPGA 图像处理或仪器类项目、需要快速搭一个「采样-缓存-显示」验证链路的人。下文所有代码和参数都按可在 Quartus Prime 里直接建工程复现的粒度来写。
2. AD9238 数据采集:并行接口时序与 Verilog 采样代码
2.1 AD9238 并行输出接口的时序特征
AD9238 是典型的 CMOS 并行输出 ADC,12 位数据总线 D11~D0,双通道共享控制引脚,输出数据在编码时钟(ENC)的驱动下更新。手册里最重要的一个参数是输出数据延迟 tOD,它表示 ENC 边沿到数据总线稳定之间的时间,通常在几纳秒到十几纳秒。如果把 FPGA 采样时钟和 ADC 的 ENC 直接连在一起,然后在同一个时钟上升沿去采数据,很容易采到总线切换过程中的中间态。
常见的做法不是把 ADC 时钟原封不动接进逻辑,而是通过 PLL 生成一个相位偏移后的采样时钟。相位偏移量可以参考 tOD 和 PCB 走线延迟来定,工程上常用 -2 ns 到 -3 ns 的延时,或者直接用时钟下降沿采样,让数据在上升沿附近处于稳定区。下面这张表是接口设计时需要对照确认的几个参数。
| 参数 | 含义 | 对 FPGA 设计的影响 |
|---|---|---|
| 分辨率 | 12 bit | 数据总线位宽,存储和显示时需要做位宽裁剪 |
| 通道数 | 2 | 双通道可并行采集,本例程先做单通道 |
| 最高采样率 | 65 MSPS | 时序裕量按采样时钟周期计算,65 MHz 下周期约 15.4 ns |
| tOD | 输出数据延迟 | 决定 FPGA 采样时钟的相位偏移量 |
| 输出格式 | 偏移二进制 | 波形显示前要转成有符号数或做中心偏移 |
2.2 采样模块的 Verilog 实现
先写最核心的 ADC 数据采集模块,接口包含采样时钟、复位、12 位数据输入和输出给后级缓存的数据与写使能。这里用打两拍的方式降低亚稳态风险,数据在采样时钟域内同步。
module ad9238_capture #( parameter DATA_WIDTH = 12 )( input wire rst_n, input wire adc_clk, // PLL 输出,相对 ENC 做相位偏移 input wire [DATA_WIDTH-1:0] adc_d, // AD9238 D11..D0 output reg [DATA_WIDTH-1:0] adc_data, // 同步后数据 output reg adc_valid // 数据有效标志 ); reg [DATA_WIDTH-1:0] data_sync1; reg [DATA_WIDTH-1:0] data_sync2; always @(posedge adc_clk or negedge rst_n) begin if (!rst_n) begin data_sync1 <= 12'd0; data_sync2 <= 12'd0; adc_data <= 12'd0; adc_valid <= 1'b0; end else begin data_sync1 <= adc_d; data_sync2 <= data_sync1; adc_data <= data_sync2; adc_valid <= 1'b1; end end endmodule代码逻辑分两层:第一级寄存器直接采 ADC 总线,第二级寄存器再次同步,最后输出的 adc_data 是与 adc_clk 同域的信号。adc_valid 在每个时钟周期拉高,表示数据有效;如果不希望每个周期都采集,可以在这个模块外部用采样使能信号门控它。需要调整的地方是 adc_clk 的来源。在 Quartus 里用 ALTPLL 生成采样时钟时,把 PLL 输出相移设置为 -2 ns 左右,具体值要结合 ModelSim 仿真观察 adc_d 与 adc_clk 的建立关系。
2.3 偏移二进制转有符号数与位宽裁剪
AD9238 默认输出偏移二进制格式,即输入为 0 V 时输出接近中间码 2048,满量程时接近 4095。直接把这 12 位数据拿去显示,波形会整体偏上,看起来像一条贴在屏幕上沿的平线。显示前先做一个转换:减掉 2048 得到有符号数,再根据屏幕纵向分辨率决定取哪些位。
wire signed [11:0] adc_signed; assign adc_signed = $signed({1'b0, adc_data}) - 12'sd2048; wire [7:0] display_data; assign display_data = adc_signed[11:4]; // 取高 8 位,压缩到 256 级转换逻辑放在数据进入缓存之前还是之后,取决于后级显示算法的位数要求。这里取了高 8 位,因为 640×480 的 VGA 屏幕纵向只有 480 行,12 位原始分辨率在像素比较算法里用不到,反而会增加存储宽度。如果想要更高纵向精度,可以保留 10 位,在比较时做乘法缩放,代价是缓存位宽和比较器面积变大。
3. 跨时钟域存储:双口 RAM 乒乓缓存与读写速率匹配
3.1 为什么 ADC 采集时钟和 VGA 像素时钟必须解耦
VGA 显示需要独立的像素时钟,比如 640×480@60 Hz 约 25.175 MHz,而 ADC 采样时钟可能是 25 MHz、40 MHz 或 50 MHz。两个时钟即使频率标称相同,也来自不同 PLL 输出,存在相位差和微小频差,不能直接共用一个时钟域。如果把采集数据直接接到 VGA 显示逻辑,会出现两种情况:显示端读到正在更新的数据,导致波形中间出现撕裂;或者采样端写入过快,缓存溢出丢点。
解耦手段只有两个方向:异步 FIFO,或者双口 RAM 加乒乓切换。异步 FIFO 适合连续不断的数据流,但需要处理空满标志的格雷码同步,逻辑复杂。对单帧 640 个采样点的波形显示,双口 RAM 乒乓是更直观的方案:一块 RAM 被 ADC 写入时,另一块被 VGA 读取,写满后交换角色,读写互不干扰。
3.2 乒乓控制逻辑与双口 RAM 实例化
双口 RAM 的写口由采样模块控制,读口由 VGA 显示模块控制。关键在「什么时候采满 640 个点」和「什么时候切换缓冲区」。下面用一个简化版乒乓控制器说明开关逻辑。
module waveform_pingpong #( parameter ADDR_WIDTH = 10, // 1024 深度,实际使用 640 parameter DATA_WIDTH = 8 )( input wire rst_n, input wire wr_clk, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, input wire rd_clk, input wire [ADDR_WIDTH-1:0] rd_addr, output wire [DATA_WIDTH-1:0] rd_data, output reg buf_ready // 当前缓冲区可读 ); wire [DATA_WIDTH-1:0] ram0_dout; wire [DATA_WIDTH-1:0] ram1_dout; // 写地址计数器,采满 640 点后停 reg [ADDR_WIDTH-1:0] wr_addr; reg [9:0] sample_cnt; reg write_buf_sel; always @(posedge wr_clk or negedge rst_n) begin if (!rst_n) begin sample_cnt <= 10'd0; wr_addr <= 10'd0; write_buf_sel <= 1'b0; end else if (wr_en) begin if (sample_cnt == 10'd640) begin sample_cnt <= 10'd0; write_buf_sel <= ~write_buf_sel; wr_addr <= 10'd0; end else begin sample_cnt <= sample_cnt + 1'b1; wr_addr <= wr_addr + 1'b1; end end end // 读端根据 write_buf_sel 的取反选择另一块 RAM wire read_buf_sel = ~write_buf_sel; assign rd_data = read_buf_sel ? ram1_dout : ram0_dout; // 双口 RAM 实例化 dual_port_ram u_ram0 ( .addr_a (wr_addr), .clk_a (wr_clk), .din_a (wr_data), .we_a (wr_en && !write_buf_sel), .addr_b (rd_addr), .clk_b (rd_clk), .dout_b (ram0_dout) ); dual_port_ram u_ram1 ( .addr_a (wr_addr), .clk_a (wr_clk), .din_a (wr_data), .we_a (wr_en && write_buf_sel), .addr_b (rd_addr), .clk_b (rd_clk), .dout_b (ram1_dout) ); endmodule这个例子用 write_buf_sel 区分写侧正在使用哪块 RAM,读侧固定读另一块。sample_cnt 到 640 时完成一次波形采集,write_buf_sel 翻转。读侧不需要等「整帧写完」才切换,因为读侧在 VGA 消隐期才会产生新的读地址,天然有时间窗口。实际工程中建议把 buf_ready 拉高放在切换完成之后,VGA 显示逻辑只在 buf_ready 为高时开始读。
表格对比三种缓存方案,便于按项目选型:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 单口 RAM 共享 | 采集率远低于显示率 | 资源最省 | 读写不能同时,容易闪烁 |
| 双口 RAM 乒乓 | 波形刷新显示 | 读写完全并行,无撕裂 | 双倍存储资源 |
| 异步 FIFO | 连续高速数据流 | 天然处理速率抖动 | 空满标志设计复杂,调试成本高 |
3.3 读写速率不匹配时的节流策略
VGA 一帧只有约 16.6 ms,而 ADC 在这个时间内可能采了 40 万个点。乒乓缓存不会消除这种数量级差异,必须在采样入口做节流。最直接的方法:在每个 VGA 帧同步信号到来后,只采集 640 个点,采满后关闭 wr_en,等待下一帧。这样显示的波形是上一帧时刻的实时快照,帧率等于 VGA 刷新率,逻辑上也最简单。
另一种做法是「滑动窗口」,ADC 持续写入,VGA 读侧每次从某个偏移地址开始读,实现波形水平滚动。这需要把双口 RAM 的写地址设计成循环回绕,并在读地址上叠加偏移量。对理解 AD9238 连续采样场景有帮助,但调试难度比快照方式高一个级别,建议先把乒乓快照跑通再扩展。
4. VGA 时序与波形绘制:行场同步参数与像素比较逻辑
4.1 VGA 行场同步参数与状态机
VGA 显示的核心是严格按照时序产生行同步 HSYNC、场同步 VSYNC 和有效显示区间信号。以最常用的 640×480@60 Hz 为例,像素时钟约 25.175 MHz,实际工程中如果 FPGA 开发板没有这个频率的晶振,可以用 25 MHz 近似,显示几何上略有偏差但可接受。整个行周期 800 个像素时钟,其中有效显示 640 个;整个帧周期 525 行,其中有效显示 480 行。
| 参数 | 数值 | 说明 |
|---|---|---|
| 像素时钟 | 25.175 MHz | 工程可用 25 MHz 替代 |
| 行同步脉冲 | 96 像素 | HSYNC 低电平宽度 |
| 行前肩 | 16 像素 | 同步脉冲后到有效数据 |
| 行后肩 | 48 像素 | 有效数据后到同步脉冲 |
| 行周期 | 800 像素 | 总计 |
| 场同步脉冲 | 2 行 | VSYNC 低电平宽度 |
| 场前肩 | 10 行 | |
| 场后肩 | 33 行 | |
| 帧周期 | 525 行 |
状态机可以用计数器实现,不需要写成复杂状态跳转。
module vga_timing #( parameter H_ACTIVE = 640, parameter H_FP = 16, parameter H_SYNC = 96, parameter H_BP = 48, parameter V_ACTIVE = 480, parameter V_FP = 10, parameter V_SYNC = 2, parameter V_BP = 33 )( input wire clk, input wire rst_n, output reg hsync, output reg vsync, output reg video_active, output reg [9:0] pixel_x, output reg [9:0] pixel_y ); reg [9:0] h_cnt; reg [9:0] v_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin h_cnt <= 10'd0; v_cnt <= 10'd0; end else begin if (h_cnt == H_ACTIVE + H_FP + H_SYNC + H_BP - 1) begin h_cnt <= 10'd0; if (v_cnt == V_ACTIVE + V_FP + V_SYNC + V_BP - 1) v_cnt <= 10'd0; else v_cnt <= v_cnt + 1'b1; end else begin h_cnt <= h_cnt + 1'b1; end end end always @(*) begin hsync = ~(h_cnt >= H_ACTIVE + H_FP && h_cnt < H_ACTIVE + H_FP + H_SYNC); vsync = ~(v_cnt >= V_ACTIVE + V_FP && v_cnt < V_ACTIVE + V_FP + V_SYNC); video_active = (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE); pixel_x = h_cnt; pixel_y = v_cnt; end endmodulepixel_x 和 pixel_y 在有效区内从 0 开始递增,可直接作为波形缓存的读地址来源。VGA 接口针脚定义中 HSYNC、VSYNC 通常为 TTL 电平,RGB 各通道按颜色位宽输出。FPGA 开发板上往往通过排针或 VGA 母座引出,接线时注意对照原理图核对同步信号极性。
4.2 像素比较法绘制波形
波形绘制的核心算法可以用一个比较器完成:从双口 RAM 读出一个采样点,它的值代表这条波形在屏幕纵向上应该出现的位置;当前扫描行 pixel_y 恰好等于这个值时,输出波形像素颜色,否则输出背景色。
wire [9:0] wave_y; wire [7:0] wave_data; ram_rd #( .DATA_WIDTH (10) ) u_ram_rd ( .rd_clk (clk), .rd_addr (pixel_x), .rd_data (wave_y) ); reg [11:0] rgb; wire wave_hit = (pixel_y == wave_y); always @(*) begin if (!video_active) begin rgb = 12'h000; // 消隐区输出黑色 end else if (wave_hit) begin rgb = 12'h0F0; // 波形亮绿色 end else begin rgb = 12'h00F; // 背景蓝色 end endpixel_x 作为读地址在 VGA 有效区逐列递增,等价于把 640 个采样点映射到屏幕横轴。wave_y 是缓存的 12 位数据裁剪后的高 8 位,取值范围 0~255,而屏幕纵向有 480 行,因此直接比较会把波形压缩在屏幕上半部分。要占满屏幕,可以把 wave_y 乘以一个缩放系数,或者把 480 行分成上下两半,让波形围绕中心线摆动。常用做法是做一个带偏移的映射:pixel_y + wave_y - 128后再比较。
4.3 显示算法的两个常见误用
第一个误用是在消隐区做地址递增。pixel_x 在行前肩、行同步、行后肩区间仍然在变化,但这些区域 video_active 为低,不应该读取缓存或输出颜色。设计 RAM 读地址时必须用 video_active 门控,否则波形会出现在屏幕可见区域之外,或导致行显示错位。
第二个误用是对 RGB 位宽不匹配不管不顾。640×480@60 的 VGA 常用 12 位色深,RGB 各 4 位;有些开发板是 16 位或 24 位色深。代码里的 12'h0F0 是按 4:4:4 写的,如果板子是 5:6:5 或 8:8:8,需要按实际位宽展开颜色值,否则显示颜色会发暗或偏色。
5. Quartus 工程调试:Signaltap 验证数据对齐与常见花屏修复
5.1 SignalTap II 抓取 ADC 输入与写地址
Quartus Prime 里调试这类跨时钟域问题,最有效的工具是 SignalTap II Logic Analyzer。在 .qsf 工程里添加 SignalTap 文件,把 adc_clk 设为采样时钟,添加观察信号:adc_d、adc_data、adc_valid、wr_addr、write_buf_sel、h_cnt、v_cnt。触发条件推荐用写地址等于 0,这样能抓到一次完整波形采集过程,确认 ADC 数据在写入缓存第一个点时的数值是否符合预期。
采样深度不用太大,2048 足够覆盖 640 个采样点加前后上下文。触发位置设置在中间,可以看到写地址翻转前的完整过程。如果在 SignalTap 里看到 adc_valid 连续拉高、wr_addr 按顺序递增,说明采集链路工作正常。如果 adc_data 在低几位出现跳动,优先检查 PLL 相位偏移量。
5.2 VGA 花屏、闪烁与数据错位的排查顺序
花屏先分两类:如果是整个屏幕颜色错乱但不闪,通常是 VGA 行场参数或引脚约束错误;如果波形区域闪烁而背景稳定,问题在乒乓缓存切换。
| 现象 | 常见原因 | 检查点 |
|---|---|---|
| 屏幕整体偏移 | hsync/vsync 极性反 | 检查 VGA 驱动对同步信号的有效电平 |
| 波形向上或向下跳变 | 缓存切换时读到半帧数据 | buf_ready 判断时序,切换后同步一个周期 |
| 波形有毛刺 | ADC 采样建立时间不足 | PLL 相移加大,或改下降沿采样 |
| 画面闪动 | 读写块切换过快 | 确认 sample_cnt 640 计数完整,检查 wr_en 节流 |
数据错位还有一个隐蔽来源:AD9238 输出数据总线如果布线较长,各 bit 之间延迟不一致,高位和低位可能出现在不同时钟周期。这时需要检查 PCB 走线等长,或者用 IDDR 原语把 ADC 时钟的双沿都用上,在数据眼图中间采样。
5.3 复位释放与显示锁定的一个小技巧
复位释放和缓存切换同时发生时,可能出现第一帧读出全零的情况。常见做法是让 VGA 显示逻辑等待垂直同步信号到来后再开始扫描,同时给乒乓切换做一个两拍同步,确保写侧切换和读侧选择不在同一时刻竞争。
reg buf_sel_sync1, buf_sel_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin buf_sel_sync1 <= 1'b0; buf_sel_sync2 <= 1'b0; end else begin buf_sel_sync1 <= write_buf_sel; buf_sel_sync2 <= buf_sel_sync1; end end这个两拍同步器把写侧的写缓冲区选择信号同步到 VGA 像素时钟域,避免读侧用异步信号直接作为 RAM 选择时出现竞争冒险。加上这个逻辑后,剩下要调的就是 VGA 显示的刷新率和波形纵向缩放系数了。
本文还有配套的精品资源,点击获取