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降到低端的SpartanIP核可能就得重新生成甚至重新买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) 3d0: pixel {8hFF, 8hFF, 8hFF}; // 白 3d1: pixel {8hFF, 8hFF, 8h00}; // 黄 3d2: pixel {8h00, 8hFF, 8hFF}; // 青 3d3: pixel {8h00, 8hFF, 8h00}; // 绿 3d4: pixel {8hFF, 8h00, 8hFF}; // 品红 3d5: pixel {8hFF, 8h00, 8h00}; // 红 3d6: pixel {8h00, 8h00, 8hFF}; // 蓝 3d7: pixel {8h00, 8h00, 8h00}; // 黑 endcase endbar_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_TOTAL2200需要12位V_TOTAL1125需要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 24h000000; 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上设置合适的触发条件就能在电脑上看到实际的波形。这个方法比盲猜高效得多而且不需要额外的硬件设备。