拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA视频测试图案发生器vtpgZero:从零实现与上板调试

FPGA视频测试图案发生器vtpgZero:从零实现与上板调试

1. 视频测试图案发生器到底是个什么东西

1.1 从一块黑屏说起

搞过FPGA视频方向的人大概都经历过这样的场景:板子焊好、供电正常、时钟锁定,显示器却一片漆黑,或者花屏闪烁。你盯着屏幕,心里犯嘀咕——是时序参数配错了?是像素时钟频率不对?还是数据位映射反了?这时候如果手头有一个能输出标准测试图案的模块,问题定位速度至少快三倍。

视频测试图案发生器,英文叫Video Test Pattern Generator,简称TPG,干的就是这件事。它本质上是一个纯逻辑电路,按照你设定的分辨率和时序标准,源源不断地产生带有特定图案的像素数据流,同时输出配套的同步信号(HSYNC、VSYNC、DE)。你可以把它理解成一个“虚拟摄像头”——它不采集任何真实画面,但输出的信号格式和真实视频源完全一致,后级电路根本分不出来。

vtpgZero这个开源项目,就是把这个东西用Verilog HDL完整实现了一遍,目标平台是FPGA。它的定位很明确:轻量、可配置、易集成。你不需要外挂DDR、不需要软核CPU、不需要任何IP授权,只要把几个.v文件拖进工程,给一个像素时钟,它就能开始工作。

1.2 为什么不用现成的IP核

Xilinx和Intel(原Altera)都提供视频测试图案的IP核,功能也挺全。但实际项目里用它们有几个绕不开的麻烦。

第一是授权和移植性问题。厂商IP通常绑定特定器件系列,你从Artix换到Cyclone,或者从高端的Kintex降到低端的Spartan,IP核可能就得重新生成甚至重新买license。开源方案没有这个顾虑,纯RTL代码,任何支持Verilog的FPGA都能综合。

第二是资源开销。厂商IP为了通用性,往往塞了一堆你用不到的功能——动态配置接口、AXI总线、中断输出等等。vtpgZero的设计哲学完全相反:只做一件事,做到极致。核心代码量很小,综合后占用的LUT和寄存器数量在低端FPGA上也毫无压力。

第三是可定制性。开源代码意味着你可以直接改。想加一个自定义图案?改几十行代码的事。想调整颜色条的顺序?改一个case分支就行。用厂商IP的话,你得看几百页的文档,还不一定能找到对应的配置寄存器。

1.3 谁适合看这个项目

这篇文章面向的读者大概分三类。

第一类是FPGA初学者,正在找练手项目。vtpgZero的代码结构清晰,涉及的核心知识点包括时序生成、状态机设计、ROM初始化、颜色空间转换等,非常适合作为从“点灯”到“做系统”的过渡项目。

第二类是做视频接口开发的工程师。你可能在调HDMI、VGA、MIPI DSI或者LVDS接口,需要一个稳定的信号源来验证链路。vtpgZero可以直接综合到板子上,省去外接信号发生器的麻烦。

第三类是做图像处理算法验证的人。你需要一个可控的、可重复的视频输入来测试你的滤波、边缘检测、缩放等模块。vtpgZero输出的图案是确定性的,每一帧都一样,非常适合做回归测试。

注意:vtpgZero输出的是数字视频信号,要显示在屏幕上还需要后级电路——比如VGA的DAC、HDMI的TMDS编码器、或者MIPI的串化器。它只负责产生像素数据和同步时序,不负责物理层驱动。

2. 核心架构拆解:一个TPG需要哪些模块

2.1 整体数据流

vtpgZero的内部结构可以用一句话概括:一个计数器驱动的有限状态机,配合几个查找表,按时序要求吐出像素。

具体来说,数据流是这样的:像素时钟进来,驱动两个计数器——行计数器和帧计数器。行计数器从0数到H_TOTAL-1,帧计数器从0数到V_TOTAL-1。这两个计数器的值组合起来,唯一确定了当前像素在屏幕上的位置。然后,一个组合逻辑模块根据当前位置判断应该输出什么颜色——是彩条、棋盘格、渐变还是纯色。最后,同步信号生成模块根据计数器的值产生HSYNC、VSYNC和DE。

这个架构的好处是极其确定性。每个像素的值只取决于它的坐标,不依赖任何历史状态。这意味着你可以在任意时刻复位,下一帧立刻恢复正常输出,不会出现画面撕裂或错位。

2.2 时序参数的定义与计算

视频时序是TPG的骨架,必须先把这部分搞清楚。以经典的1080p60为例,标准参数如下:

参数水平方向垂直方向
有效像素19201080
前肩884
同步脉冲445
后肩14836
总计22001125

这些数字不是随便定的,它们来自CTA-861标准。总像素时钟频率 = 2200 × 1125 × 60 = 148.5 MHz。如果你用的FPGA板子上正好有148.5MHz的时钟源,那直接拿来用就行。如果没有,就需要用PLL倍频得到。

在vtpgZero的代码里,这些参数通常定义为localparam或者模块参数,方便不同分辨率之间切换。比如:

localparam H_ACTIVE = 1920; localparam H_FP = 88; localparam H_SYNC = 44; localparam H_BP = 148; localparam H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP; localparam V_ACTIVE = 1080; localparam V_FP = 4; localparam V_SYNC = 5; localparam V_BP = 36; localparam V_TOTAL = V_ACTIVE + V_FP + V_SYNC + V_BP;

这里有个容易踩的坑:不同来源的时序标准对前肩和后肩的定义可能不一样。有的文档把前肩叫“front porch”,后肩叫“back porch”,有的则用“left border”和“right border”。名字不重要,重要的是搞清楚每个阶段持续多少个像素时钟。我的建议是直接画一张时序图,把HSYNC的上升沿、下降沿和DE的起止位置标清楚,然后对着图写代码,比看文字描述靠谱得多。

2.3 同步信号生成逻辑

同步信号的生成逻辑其实很简单,就是拿计数器跟阈值比较。以HSYNC为例:

assign hsync = ~((h_cnt >= H_ACTIVE + H_FP) && (h_cnt < H_ACTIVE + H_FP + H_SYNC));

这行代码的意思是:当行计数器落在同步脉冲区间内时,HSYNC拉低(大多数标准是低电平有效),其余时间拉高。VSYNC同理,只不过用的是帧计数器。

DE信号更简单,它只在有效像素区间内为高:

assign de = (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE);

看起来平平无奇,但这里有个细节值得注意:DE的时序必须和像素数据严格对齐。如果你在DE为高的时候输出的像素数据有延迟,画面就会偏移。在vtpgZero里,像素数据的生成是组合逻辑,和DE同拍输出,所以不存在这个问题。但如果你在后面加了流水线寄存器,就必须同步延迟DE信号,否则就会出现经典的“画面右移几个像素”现象。

2.4 图案生成模块

vtpgZero支持多种测试图案,每种图案的生成逻辑各不相同。最经典的是八色彩条,从上到下或从左到右排列白、黄、青、绿、品红、红、蓝、黑八种颜色。生成逻辑就是一个多路选择器:

always @(*) begin case (bar_index) 3'd0: pixel = {8'hFF, 8'hFF, 8'hFF}; // 白 3'd1: pixel = {8'hFF, 8'hFF, 8'h00}; // 黄 3'd2: pixel = {8'h00, 8'hFF, 8'hFF}; // 青 3'd3: pixel = {8'h00, 8'hFF, 8'h00}; // 绿 3'd4: pixel = {8'hFF, 8'h00, 8'hFF}; // 品红 3'd5: pixel = {8'hFF, 8'h00, 8'h00}; // 红 3'd6: pixel = {8'h00, 8'h00, 8'hFF}; // 蓝 3'd7: pixel = {8'h00, 8'h00, 8'h00}; // 黑 endcase end

bar_index的计算方式是 h_cnt / (H_ACTIVE/8),也就是把一行有效像素均分成八段。这里用除法会消耗较多逻辑资源,实际实现时通常用移位和乘法代替,或者直接预计算好每个条带的边界值。

棋盘格图案稍微复杂一点,需要同时考虑行和列的奇偶性。常见做法是把像素坐标的高位做异或:

wire checker = h_cnt[5] ^ v_cnt[5]; assign pixel = checker ? WHITE : BLACK;

这样每个格子的大小就是32×32像素。你可以通过调整取哪一位来改变格子大小——取第6位就是64×64,取第4位就是16×16。

渐变图案的实现方式又不一样。它需要根据像素坐标计算出一个渐变值,通常是线性插值。在FPGA里做插值,最简单的方法是查表。预先在ROM里存好256个渐变值,然后用坐标的高8位作为地址去查。这样既省资源又保证时序。

2.5 颜色空间与位宽处理

vtpgZero默认输出RGB888格式,每个像素24位。但实际项目中你可能需要RGB565、YUV422或者其他格式。这时候就需要一个颜色空间转换模块。

RGB888转RGB565很简单,直接截高位:

assign rgb565 = {rgb888[23:19], rgb888[15:10], rgb888[7:3]};

但RGB转YUV就复杂一些,涉及浮点运算。在FPGA里通常用定点数近似:

// Y = 0.299R + 0.587G + 0.114B // 定点化:乘以256后取整 wire [15:0] y_temp = 77*r + 150*g + 29*b; assign y = y_temp[15:8];

这里的系数77、150、29就是0.299、0.587、0.114乘以256后的近似值。误差在可接受范围内,但如果你做的是广播级应用,可能需要更高精度的系数。

实操心得:颜色空间转换的舍入方式会影响最终画质。直接截断会产生明显的色带,建议加上四舍五入逻辑。具体做法是在截断前加上0.5的偏移量,也就是加128再取高位。

3. 从零搭建:vtpgZero的完整实现流程

3.1 工程目录结构与文件组织

vtpgZero的代码组织遵循一个原则:每个文件只做一件事。典型的目录结构如下:

vtpgZero/ ├── rtl/ │ ├── vtpg_top.v // 顶层模块 │ ├── timing_gen.v // 时序生成 │ ├── pattern_gen.v // 图案生成 │ ├── color_bar.v // 彩条图案 │ ├── checkerboard.v // 棋盘格 │ └── gradient.v // 渐变 ├── sim/ │ ├── tb_vtpg_top.v // 测试平台 │ └── wave.do // 波形配置 ├── constraints/ │ └── vtpg.xdc // 引脚约束 └── doc/ └── timing_diagram.png // 时序图

这种模块化设计的好处是,你想换一种图案,只需要替换pattern_gen.v里的一个子模块,顶层和其他部分完全不用动。测试平台也可以针对每个子模块单独写,定位问题更快。

3.2 顶层模块的接口设计

vtpg_top的端口列表应该尽量简洁。一个典型的接口如下:

module vtpg_top #( parameter H_ACTIVE = 1920, parameter V_ACTIVE = 1080, parameter PATTERN_SEL = 0 )( input wire pixel_clk, input wire rst_n, output wire hsync, output wire vsync, output wire de, output wire [23:0] pixel_data );

这里把分辨率和图案选择做成了参数,综合时就可以确定,不需要运行时配置。如果你确实需要动态切换图案,可以加一个输入端口,但那样会多消耗一些逻辑资源。

rst_n是低电平有效的异步复位。虽然有些FPGA设计规范推荐同步复位,但视频时序生成这种场景下,异步复位更安全——万一时钟没起来,同步复位就永远生效不了。

3.3 时序生成模块的详细实现

timing_gen是整个TPG的心脏。它的核心是两个计数器和一个状态机。状态机其实很简单,只有四个状态:同步、后肩、有效、前肩。但用计数器实现更直接:

always @(posedge pixel_clk or negedge rst_n) begin if (!rst_n) begin h_cnt <= 0; v_cnt <= 0; end else begin if (h_cnt == H_TOTAL - 1) begin h_cnt <= 0; if (v_cnt == V_TOTAL - 1) v_cnt <= 0; else v_cnt <= v_cnt + 1; end else begin h_cnt <= h_cnt + 1; end end end

这段代码看起来简单,但有一个性能隐患:H_TOTAL和V_TOTAL通常是比较大的数(比如2200和1125),比较器会消耗不少LUT。如果你的FPGA资源紧张,可以把比较改成减法计数——从H_TOTAL-1往下数到0。这样只需要判断是否为零,省去了大位宽比较器。

另一个优化点是计数器的位宽。H_TOTAL=2200需要12位,V_TOTAL=1125需要11位。不要图省事直接用32位,那样会浪费大量寄存器。

3.4 图案生成模块的集成

pattern_gen模块接收当前像素坐标,输出对应的颜色。它的内部通常是一个多路选择器,根据PATTERN_SEL选择不同的图案源:

always @(*) begin case (PATTERN_SEL) 0: pixel = color_bar_pixel; 1: pixel = checkerboard_pixel; 2: pixel = gradient_pixel; default: pixel = 24'h000000; endcase end

这里有个时序问题需要注意:如果各个图案生成模块的输出有组合逻辑延迟,多路选择器会把这些延迟累加起来。在1080p60下,像素时钟周期只有6.7ns,组合逻辑路径不能太长。解决办法是在每个图案模块的输出加一级寄存器,这样多路选择器的输入就是干净的寄存器输出,时序更容易满足。

3.5 仿真验证与波形分析

写完了RTL,下一步是仿真。测试平台需要做几件事:产生像素时钟、施加复位、运行足够长的时间让至少两帧画面输出、检查同步信号的时序是否符合标准。

一个实用的技巧是在测试平台里加一个自动检查器,用断言验证HSYNC的周期是否等于H_TOTAL个时钟周期:

property hsync_period_check; @(posedge pixel_clk) disable iff (!rst_n) $rose(hsync) |-> ##[1:$] $rose(hsync); endproperty

不过SystemVerilog断言在有些仿真器里支持不好,更简单的做法是用计数器手动检查:

integer hsync_interval; always @(posedge pixel_clk) begin if (hsync_falling) begin if (hsync_interval != H_TOTAL) $display("ERROR: HSYNC period mismatch!"); hsync_interval <= 0; end else begin hsync_interval <= hsync_interval + 1; end end

波形分析的重点是看DE、HSYNC、VSYNC三者的相对关系。特别是VSYNC的下降沿应该落在垂直同步区间内,而DE在VSYNC有效期间必须为低。这些细节如果搞错了,显示器可能直接不认信号。

常见问题:仿真通过了,上板子却没画面。八成是约束文件写错了。检查引脚分配是否和原理图一致,特别是像素时钟的引脚——很多FPGA的时钟输入引脚是专用的,不能随便分配。

4. 上板调试与常见问题排查

4.1 约束文件的编写要点

约束文件是连接RTL和物理引脚的桥梁。对于视频输出,最关键的是时钟约束和引脚约束。

时钟约束告诉综合工具像素时钟的频率,让它知道该怎么优化时序:

create_clock -period 6.734 -name pixel_clk [get_ports pixel_clk]

6.734ns对应148.5MHz,这是1080p60的像素时钟周期。如果你用的是其他分辨率,记得改这个值。

引脚约束把RTL端口映射到FPGA的实际引脚:

set_property PACKAGE_PIN R4 [get_ports pixel_clk] set_property IOSTANDARD LVCMOS33 [get_ports pixel_clk] set_property PACKAGE_PIN T1 [get_ports {pixel_data[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {pixel_data[0]}]

这里有个坑:不同Bank的IOSTANDARD可能不一样。如果你把LVCMOS33的信号分配到只支持LVCMOS18的Bank,综合会报错。解决办法是查板子的原理图,确认每个引脚所在的Bank和对应的电平标准。

4.2 时序不收敛的典型原因

综合实现之后,如果时序报告显示setup违例,通常有几个原因。

第一是组合逻辑路径太长。比如图案生成模块里用了除法器或者大位宽的乘法器,这些都会拖慢关键路径。解决办法是流水线化——把长路径拆成几级,中间插入寄存器。

第二是扇出过高。比如复位信号rst_n如果直接连到几百个寄存器,布线延迟会很大。解决办法是用寄存器复制,或者改成同步复位。

第三是时钟约束不准确。如果你约束的周期比实际时钟快,工具会过度优化,浪费资源;如果比实际慢,又可能漏掉真正的违例。最可靠的方法是用示波器实测时钟频率,然后按实测值约束。

4.3 画面异常的问题定位

上板之后画面不正常,可以按照以下流程排查:

现象可能原因排查方法
全黑像素时钟没起来用示波器测时钟引脚
全白复位一直有效检查rst_n是否被拉低
花屏数据位映射错误逐位检查引脚约束
画面偏移DE与数据不对齐仿真看波形
颜色不对颜色空间转换错误用纯色图案测试
闪烁帧率不稳定检查时钟抖动

我遇到过最诡异的一次是画面每隔几秒闪一下。查了半天发现是电源纹波太大,导致PLL偶尔失锁。换了个LDO就好了。所以硬件问题有时候比逻辑问题更难查。

4.4 资源占用与优化

vtpgZero在Xilinx Artix-7上的资源占用大致如下:

资源类型使用量占比
LUT约3201.5%
FF约1800.8%
BRAM00%
DSP00%

这个占用率非常低,意味着你可以在同一个FPGA里塞进多个TPG实例,同时输出多路视频。或者把剩余资源用来做图像处理算法。

如果还想进一步优化,可以考虑把图案生成模块的查找表改成分布式RAM,这样能省一些LUT。但说实话,320个LUT已经很少了,没必要过度优化。

4.5 扩展思路:从TPG到完整视频系统

vtpgZero本身只是一个信号源,但你可以把它作为起点,搭建更完整的视频处理系统。

比如加一个图像缩放模块,把1080p的测试图案缩放到720p输出。或者加一个帧缓冲,把TPG的输出存进DDR,然后再读出来做其他处理。再或者加一个HDMI发送模块,直接把TPG的输出编码成TMDS信号送到显示器。

这些扩展模块都可以在vtpgZero的基础上叠加,而不需要修改TPG本身的代码。这就是模块化设计的好处——每个模块只负责一件事,通过标准接口连接。

我个人在实际操作中的体会是,TPG这种看似简单的模块,真正写好并不容易。时序参数差一个像素,画面就可能偏到姥姥家。颜色位序搞反了,红蓝互换,调试半天才发现。所以建议大家在写代码之前,先把时序图仔仔细细画一遍,把每个信号的起止位置标清楚,然后再动手。磨刀不误砍柴工,这句话在FPGA开发里尤其适用。

最后再分享一个小技巧:如果你手头没有示波器,可以用FPGA的ILA(集成逻辑分析仪)来抓时序。把HSYNC、VSYNC、DE和几个像素数据信号连到ILA上,设置合适的触发条件,就能在电脑上看到实际的波形。这个方法比盲猜高效得多,而且不需要额外的硬件设备。

返回列表