简介:面向FPGA入门与嵌入式进阶开发者,这份完整的Altera Quartus工程演示了如何用FPGA驱动RGB接口TFT液晶屏并输出彩条,覆盖像素时钟、行/列同步(HSync/VSync)生成、RGB并行数据发送等核心时序逻辑,工程以Altera器件为平台。资源共126个文件,约3.43MB,包含7个Verilog源码文件、QSF/QPF工程配置、SOF比特流,以及编译过程生成的cdb/hdb等数据库文件,并附txt/readme/md说明文档;Verilog源码为主逻辑,QSF/QPF用于重建工程,SOF可直接下载验证,说明文档帮助上手排错。已有1720人浏览学习。开发者可据此快速搭建RGB LCD驱动环境,复用彩条显示模块,理解HSync/VSync与像素时钟的配合关系;工程中的帧缓冲、时钟生成及调试思路也可迁移到其他显示方案。1. 先看这个 RAR 到底救了什么:FPGA 调屏与 RGB 彩条第一次拿到带 TFT 屏的板子,最怕的不是代码编译不过,而是屏幕亮起来全是花屏或者白屏。到底是复位没拉对,还是像素时钟差了几兆?又或者只是 RGB 数据线松了一根?这时候硬猜会把人逼疯。我习惯的做法是先跑一遍 fpga 彩条工程:让屏幕按标准顺序显示白、黄、青、绿、品红、红、蓝、黑八条竖条纹,眼睛扫一眼就能判断链路状态。tft_rgb_colorbar 这个 rar 就是这样一个基于 Verilog 的测试工程,面向想快速验证 TFT 面板与 FPGA 之间接线、时序参数和颜色编码的开发者。不管你是刚接触 FPGA 的新手,还是调屏调到头秃的老手,这套固定彩条都比对着逻辑分析仪盲猜高效得多。2. 系统设计:从像素时钟到 RGB 输出的完整链路2.1 拿到 .rar 先看这四类文件:工程骨架比我之前想的清爽解压 tft_rgb_colorbar.rar 之后,里面不是散落一堆 .v 文件,而是按 RTL、约束、仿真、文档分好的标准工程结构。我见过太多丢三落四的 demo,这个包里该有的东西基本都齐了:tft_rgb_colorbar/ ├── rtl/ │ ├── tft_rgb_colorbar_top.v # 顶层模块 │ ├── clk_gen.v # 像素时钟生成(PLL 包装) │ ├── tft_timing_gen.v # 行场同步、DE 与像素坐标生成 │ └── colorbar_gen.v # 彩条颜色生成 ├── con/ │ └── tft_colorbar.xdc # 引脚约束和时序约束 ├── sim/ │ ├── tb_tft_rgb_colorbar.v # 仿真顶层 │ └── tb_run.tcl # 一键跑仿真脚本 └── README.md # 使用说明和屏参记录这套结构的好处是职责分离:clk_gen只管产生像素时钟,tft_timing_gen只负责把时序跑对,colorbar_gen只根据坐标给像素上色,顶层就做实例化和连线。改屏参数时不用翻整个工程,改颜色时也不怕碰坏时序。如果你之前拿到过一个几百行全堆在 top 里的 fpga 工程,再看这个会觉得清爽不少。2.2 屏幕时序参数:H_ACTIVE、H_FP、H_SYNC 这些数字从哪来所有调屏的血泪经验最后都会指向同一个源头:屏幕数据手册里的时序图。tft_rgb_colorbar 工程里预设的是一块常见的 800x480 RGB TFT,像素时钟大约 33.3MHz,刷新率约 60Hz。我习惯先把这些参数抄到 README 里,再写进代码,避免下次翻车。参数值说明H_ACTIVE800每行有效像素数H_SYNC128行同步脉冲宽度H_BP88行后肩H_FP40行前肩V_ACTIVE480有效行数V_SYNC2帧同步脉冲宽度V_BP30帧后肩V_FP13帧前肩H_TOTAL1056行总周期,等于 128 + 88 + 800 + 40V_TOTAL525帧总行数,等于 2 + 30 + 480 + 13把这些数代进去算一下:33.3MHz 除以 1056 再除以 525,正好约等于 60Hz,这个刷新率是合理的。如果你的屏是 480x272 或者 1280x800,不要照抄,而应以规格书 tming 参数为准。工程里这些参数都做成了 parameter,改起来不用动逻辑。2.3 顶层模块的信号约定:所有线头都汇到 tft_rgb_colorbar_top顶层模块的角色就是一个接线盒。输入只有外部晶振时钟和复位,剩下的信号都输出到 TFT 排针。下面是精简后的顶层结构:module tft_rgb_colorbar_top #( parameter H_ACTIVE = 800, parameter H_SYNC = 128, parameter H_BP = 88, parameter H_FP = 40, parameter V_ACTIVE = 480, parameter V_SYNC = 2, parameter V_BP = 30, parameter V_FP = 13 )( input wire clk_ref, input wire rst_n, output wire pclk, output wire hsync, output wire vsync, output wire de, output wire [7:0] data_r, output wire [7:0] data_g, output wire [7:0] data_b, output wire lcd_bl ); wire clk_pixel; wire locked; wire [11:0] hcnt, vcnt; clk_gen u_clk_gen( .clk_ref (clk_ref), .rst_n (rst_n), .clk_out (clk_pixel), .locked (locked) ); assign pclk = clk_pixel; tft_timing_gen #( .H_ACTIVE(H_ACTIVE), .H_SYNC(H_SYNC), .H_BP(H_BP), .H_FP(H_FP), .V_ACTIVE(V_ACTIVE), .V_SYNC(V_SYNC), .V_BP(V_BP), .V_FP(V_FP) ) u_timing( .pclk (clk_pixel), .rst_n (rst_n & locked), .hsync (hsync), .vsync (vsync), .de (de), .hcnt (hcnt), .vcnt (vcnt) ); colorbar_gen u_colorbar( .pclk (clk_pixel), .rst_n (rst_n & locked), .de (de), .hcnt (hcnt), .vcnt (vcnt), .data_r(data_r), .data_g(data_g), .data_b(data_b) ); assign lcd_bl = 1'b1; endmodule注意rst_n & locked这个细节:外部复位虽然拉起来了,但如果 FPGA 内部的时钟 IP 还没锁定,从clk_gen出来的时钟可能是不完整的,直接驱动时序模块会产生奇怪的毛刺。加上locked是为了让时序模块在时钟稳定之后再开始跑。lcd_bl是背光使能,调试阶段直接拉高是最省事的做法;有些屏的背光引脚是低有效,这一步要看原理图。3. 核心模块拆解:彩色条纹是怎么从坐标算出来的3.1 时序生成:让像素坐标规规整整走完一帧TFT 显示的物理过程可以理解成一条扫描线从左上角走到右下角。tft_timing_gen.v里的核心就是两个计数器:hcnt计数当前行内的像素序号,vcnt计数当前是第几行。每次hcnt走到H_TOTAL - 1时回 0,同时vcnt加 1;vcnt走到V_TOTAL - 1时整帧结束,回到第 1 行。这段逻辑我抽出来看:localparam H_START = H_SYNC + H_BP; localparam H_END = H_START + H_ACTIVE - 1; localparam V_START = V_SYNC + V_BP; localparam V_END = V_START + V_ACTIVE - 1; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin hcnt <= 0; vcnt <= 0; end else begin if (hcnt == H_TOTAL - 1) begin hcnt <= 0; if (vcnt == V_TOTAL - 1) vcnt <= 0; else vcnt <= vcnt + 1; end else begin hcnt <= hcnt + 1; end end end assign de = (hcnt >= H_START) && (hcnt <= H_END) && (vcnt >= V_START) && (vcnt <= V_END); assign hsync = (hcnt < H_SYNC) ? 1'b0 : 1'b1; assign vsync = (vcnt < V_SYNC) ? 1'b0 : 1'b1;这里H_START等于同步脉宽加后肩,即有效区从同步结束后的第 216 个像素开始,持续 800 个像素,到 1015 结束。de信号就是有效显示窗口,之后第 5 章二次开发时,你会频繁用到它。可能有人会问为什么直接用de而不用hsync/vsync去锁数据。因为现在很多 RGB TFT 都支持 DE 同步模式,DE 有效时送出颜色数据,DE 无效时送 0,就能避免花屏;同步模式反而对时序要求更严格,极性一错就滚屏。这个工程默认以 DE 为主,行场同步只是按照屏规约拉出来,属于双兼容做法。3.2 彩条生成:用坐标区间给每个像素上色有了坐标之后,彩条生成就是纯粹的翻译工作。标准彩条从左到右依次是白、黄、青、绿、品红、红、蓝、黑,800 像素分成 8 段,每段 100 像素。colorbar_gen.v里不需要复杂的除法,直接比较横坐标落在哪个区间:module colorbar_gen #( parameter H_START = 216, parameter H_ACTIVE = 800 )( input wire pclk, input wire rst_n, input wire de, input wire [11:0] hcnt, input wire [11:0] vcnt, output reg [7:0] data_r, output reg [7:0] data_g, output reg [7:0] data_b ); wire [11:0] h_act = hcnt - H_START; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'h00; end else if (!de) begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'h00; end else if (h_act < 100) begin data_r <= 8'hFF; data_g <= 8'hFF; data_b <= 8'hFF; // 白 end else if (h_act < 200) begin data_r <= 8'hFF; data_g <= 8'hFF; data_b <= 8'h00; // 黄 end else if (h_act < 300) begin data_r <= 8'h00; data_g <= 8'hFF; data_b <= 8'hFF; // 青 end else if (h_act < 400) begin data_r <= 8'h00; data_g <= 8'hFF; data_b <= 8'h00; // 绿 end else if (h_act < 500) begin data_r <= 8'hFF; data_g <= 8'h00; data_b <= 8'hFF; // 品红 end else if (h_act < 600) begin data_r <= 8'hFF; data_g <= 8'h00; data_b <= 8'h00; // 红 end else if (h_act < 700) begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'hFF; // 蓝 end else begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'h00; // 黑 end end endmodule这段代码里最容易漏掉的是!de时的置零分支。有些屏在有效区外会采样 RGB 数据,如果不主动送 0,残留上一段颜色,屏幕边缘会出现一条彩色尾巴。h_act是用hcnt减H_START得到的有效横坐标,范围是 0 到 799;注意它不能直接在de为低时使用,因为此时hcnt小于H_START,相减会得到一个非常大的数,所以一定要先把de拿来兜底。vcnt在这个模块里没有参与颜色计算,因为竖条纹只和横坐标有关;如果你想做横向渐变条,再把vcnt用上。3.3 仿真:用几行 testbench 检查一行帧时序仿真不是走形式,仿真过了至少能证明两个坐标计数器的行为是对的。这个包里的sim/tb_tft_rgb_colorbar.v写法很朴素:module tb_tft_rgb_colorbar; reg clk_ref = 0; reg rst_n = 0; wire pclk, hsync, vsync, de; wire [7:0] data_r, data_g, data_b; always #10 clk_ref = ~clk_ref; tft_rgb_colorbar_top dut( .clk_ref (clk_ref), .rst_n (rst_n), .pclk (pclk), .hsync (hsync), .vsync (vsync), .de (de), .data_r (data_r), .data_g (data_g), .data_b (data_b), .lcd_bl () // 仿真时悬空 ); initial begin rst_n = 0; #100 rst_n = 1; #50_000_000 $finish; end always @(posedge pclk) begin if (hcnt == 0) $display("Frame: vsync=%b hsync=%b vcnt=%d", vsync, hsync, vcnt); end endmodule跑仿真时重点关注几个点:hsync的低脉冲宽度要在H_SYNC个像素时钟内;de连续高电平的周期数应当是H_ACTIVE,也就是 800;vcnt到 524 之后跳回 0,完成一帧。如果在波形上看到de只高了 799 拍或者 hsync 不对称,先回 3.1 检查参数是不是被覆盖了。仿真看到的data_r/data_g/data_b在de为高时会按彩条顺序变化,这就代表数据链路正确。4. 避坑指南:TFT 彩条上板最常见的五个翻车现场4.1 现象一:屏一直白屏,彩条根本没出来白屏大概率意味着背光亮了,但没有有效数据进入屏。我遇到第一次调这块屏时,复位和时钟都拉对了,综合也过了,屏幕仍然全白。原因出在clk_gen的locked没有接进时序模块的复位,外部复位拉高后时序模块立刻开跑,而当时的像素时钟还在漂,产生了大量无效跳变,导致 de 从未正确拉高。解决方法是像 2.3 里那样写rst_n & locked,并且加一个仿真 log,打印locked值确认时钟 IP 没有报错。上板前可以用板上 LED 引一个 status,看到灯亮再收工,否则就是时钟问题优先排查。4.2 现象二:颜色顺序错乱,红绿蓝互换屏幕能显示条纹,但白色变成了绿色,或者原本红色变蓝,这种问题基本不是 FPGA 逻辑的问题,而是 RGB 数据线接错或者位序映射不对。这个工程输出的是 RGB888 三个 8 位通道,但很多屏的驱动板实际只引出了 18 位,需要丢弃低位。如果接口定义里 RGB 顺序是 BGR,数据必须交叉接线。解决方法是先别改代码,拿放大镜对着转接板原理图看,确认数据标号;如果硬件固定不能换线,就在colorbar_gen的端口赋值上做一次手工交叉映射,即把红色通道接到屏的蓝色引脚,其他通道同理,让逻辑适配物理接线。4.3 现象三:画面横滚或上下跳,图像像被撕裂横滚最常见的直接原因是行同步极性反了。TFT 屏规格书里如果写HSYNC: Negative,而工程里默认低有效,代码会输出hcnt < H_SYNC拉低,应该没问题;但如果规格书是正极性,你需要把那句改为hcnt >= H_SYNC时拉低。还有一种情况是同时接入了 DE 模式和行场同步模式,屏内部优先选了一个与外部冲突的信号,导致画面整体偏移。解决方法是临时把de直接拉低,只保留 hsync/vsync 跑一帧,看是否滚屏;如果滚屏,说明 DE 模式的输出和同步冲突,需要检查约束文件里是不是把 DE 引脚误接到了同步脚。4.4 现象四:屏幕明显闪烁,刷新率掉到 45Hz 以下800x480 跑到 33.3MHz 是 60Hz,如果实际像素时钟只有 25MHz,刷新率就会掉到 45Hz 左右,肉眼可见闪烁。有时综合工具会把clk_gen的参数记错,生成 27MHz 或 24MHz 的时钟,flash 里被告警过但你忽略了。解决方法是不要只看倍频系数,直接把pclk引到一个测试脚,用示波器或者频率计量一下,确认是 33.3MHz。更稳妥的办法是在 testbench 里用仿真测量两帧间隔,打印出刷新率,和屏规格书对一下再上板。4.5 现象五:上板后亮度极暗,只有边角有隐约图案这个现象背后十有八九是背光没有完全打开。很多 RGB TFT 的背光使能引脚是开漏输出,外部必须上拉,而 FPGA 的 IO 默认弱下拉;代码里lcd_bl = 1'b1虽然写了,但引脚约束可能没加PULLUP,相当于悬空。还有一个坑是背光信号别和某个低频时钟共用一个管脚,否则背光会被周期性关断,亮度看起来忽明忽暗。解决方法是暂时把背光引脚绑到一个固定高电平,换一个专用 IO,并加上拉约束;如果还是暗,查背光电源是否只有 3.3V,而屏需要 5V。5. 二次开发:把静态彩条改成可滚动动画5.1 让彩条动起来:一个偏移量搞定静态彩条验证完链路之后,很多人想让它看起来更像“动态”一点。其实只需要在colorbar_gen里增加一个每帧递增的偏移量,再用它替代h_act参与颜色区间判断。按这个思路可以写一个简单的滚动彩条:reg [9:0] offset = 0; always @(posedge pclk or negedge rst_n) begin if (!rst_n) offset <= 0; else if (de && hcnt == 1015) offset <= offset + 1; // 每行有效数据结束后偏移加一,速度快 end wire [11:0] h_shift = (h_act + offset >= H_ACTIVE) ? h_act + offset - H_ACTIVE : h_act + offset;用h_shift替换掉原本h_act所在的比较判断,屏幕上的条纹就会以每行 1 个像素的速度滚动。因为偏移量最大 799 加最大坐标 799 不会超过 1600,做一次减法就能回绕。想控制速度就把加一的条件从每行改成每帧一次,也就是检测vcnt到边界时再加。最后说个我自己的坏习惯:以前拿到新屏总想跳过彩条,直接跑自己的界面代码,结果经常分不清是软件问题还是硬件问题,白白耗掉一整天。从那以后我每次换屏或改板子,第一件事就是先把 tft_rgb_colorbar 跑一遍,确认时钟、同步、RGB 数据线全通再碰业务逻辑。这个习惯帮我省下的时间远比想象中多,希望帮到你。本文还有配套的精品资源,点击获取
简介:面向FPGA入门与嵌入式进阶开发者,这份完整的Altera Quartus工程演示了如何用FPGA驱动RGB接口TFT液晶屏并输出彩条,覆盖像素时钟、行/列同步(HSync/VSync)生成、RGB并行数据发送等核心时序逻辑,工程以Altera器件为平台。资源共126个文件,约3.43MB,包含7个Verilog源码文件、QSF/QPF工程配置、SOF比特流,以及编译过程生成的cdb/hdb等数据库文件,并附txt/readme/md说明文档;Verilog源码为主逻辑,QSF/QPF用于重建工程,SOF可直接下载验证,说明文档帮助上手排错。已有1720人浏览学习。开发者可据此快速搭建RGB LCD驱动环境,复用彩条显示模块,理解HSync/VSync与像素时钟的配合关系;工程中的帧缓冲、时钟生成及调试思路也可迁移到其他显示方案。1. 先看这个 RAR 到底救了什么:FPGA 调屏与 RGB 彩条第一次拿到带 TFT 屏的板子,最怕的不是代码编译不过,而是屏幕亮起来全是花屏或者白屏。到底是复位没拉对,还是像素时钟差了几兆?又或者只是 RGB 数据线松了一根?这时候硬猜会把人逼疯。我习惯的做法是先跑一遍 fpga 彩条工程:让屏幕按标准顺序显示白、黄、青、绿、品红、红、蓝、黑八条竖条纹,眼睛扫一眼就能判断链路状态。tft_rgb_colorbar 这个 rar 就是这样一个基于 Verilog 的测试工程,面向想快速验证 TFT 面板与 FPGA 之间接线、时序参数和颜色编码的开发者。不管你是刚接触 FPGA 的新手,还是调屏调到头秃的老手,这套固定彩条都比对着逻辑分析仪盲猜高效得多。2. 系统设计:从像素时钟到 RGB 输出的完整链路2.1 拿到 .rar 先看这四类文件:工程骨架比我之前想的清爽解压 tft_rgb_colorbar.rar 之后,里面不是散落一堆 .v 文件,而是按 RTL、约束、仿真、文档分好的标准工程结构。我见过太多丢三落四的 demo,这个包里该有的东西基本都齐了:tft_rgb_colorbar/ ├── rtl/ │ ├── tft_rgb_colorbar_top.v # 顶层模块 │ ├── clk_gen.v # 像素时钟生成(PLL 包装) │ ├── tft_timing_gen.v # 行场同步、DE 与像素坐标生成 │ └── colorbar_gen.v # 彩条颜色生成 ├── con/ │ └── tft_colorbar.xdc # 引脚约束和时序约束 ├── sim/ │ ├── tb_tft_rgb_colorbar.v # 仿真顶层 │ └── tb_run.tcl # 一键跑仿真脚本 └── README.md # 使用说明和屏参记录这套结构的好处是职责分离:clk_gen只管产生像素时钟,tft_timing_gen只负责把时序跑对,colorbar_gen只根据坐标给像素上色,顶层就做实例化和连线。改屏参数时不用翻整个工程,改颜色时也不怕碰坏时序。如果你之前拿到过一个几百行全堆在 top 里的 fpga 工程,再看这个会觉得清爽不少。2.2 屏幕时序参数:H_ACTIVE、H_FP、H_SYNC 这些数字从哪来所有调屏的血泪经验最后都会指向同一个源头:屏幕数据手册里的时序图。tft_rgb_colorbar 工程里预设的是一块常见的 800x480 RGB TFT,像素时钟大约 33.3MHz,刷新率约 60Hz。我习惯先把这些参数抄到 README 里,再写进代码,避免下次翻车。参数值说明H_ACTIVE800每行有效像素数H_SYNC128行同步脉冲宽度H_BP88行后肩H_FP40行前肩V_ACTIVE480有效行数V_SYNC2帧同步脉冲宽度V_BP30帧后肩V_FP13帧前肩H_TOTAL1056行总周期,等于 128 + 88 + 800 + 40V_TOTAL525帧总行数,等于 2 + 30 + 480 + 13把这些数代进去算一下:33.3MHz 除以 1056 再除以 525,正好约等于 60Hz,这个刷新率是合理的。如果你的屏是 480x272 或者 1280x800,不要照抄,而应以规格书 tming 参数为准。工程里这些参数都做成了 parameter,改起来不用动逻辑。2.3 顶层模块的信号约定:所有线头都汇到 tft_rgb_colorbar_top顶层模块的角色就是一个接线盒。输入只有外部晶振时钟和复位,剩下的信号都输出到 TFT 排针。下面是精简后的顶层结构:module tft_rgb_colorbar_top #( parameter H_ACTIVE = 800, parameter H_SYNC = 128, parameter H_BP = 88, parameter H_FP = 40, parameter V_ACTIVE = 480, parameter V_SYNC = 2, parameter V_BP = 30, parameter V_FP = 13 )( input wire clk_ref, input wire rst_n, output wire pclk, output wire hsync, output wire vsync, output wire de, output wire [7:0] data_r, output wire [7:0] data_g, output wire [7:0] data_b, output wire lcd_bl ); wire clk_pixel; wire locked; wire [11:0] hcnt, vcnt; clk_gen u_clk_gen( .clk_ref (clk_ref), .rst_n (rst_n), .clk_out (clk_pixel), .locked (locked) ); assign pclk = clk_pixel; tft_timing_gen #( .H_ACTIVE(H_ACTIVE), .H_SYNC(H_SYNC), .H_BP(H_BP), .H_FP(H_FP), .V_ACTIVE(V_ACTIVE), .V_SYNC(V_SYNC), .V_BP(V_BP), .V_FP(V_FP) ) u_timing( .pclk (clk_pixel), .rst_n (rst_n & locked), .hsync (hsync), .vsync (vsync), .de (de), .hcnt (hcnt), .vcnt (vcnt) ); colorbar_gen u_colorbar( .pclk (clk_pixel), .rst_n (rst_n & locked), .de (de), .hcnt (hcnt), .vcnt (vcnt), .data_r(data_r), .data_g(data_g), .data_b(data_b) ); assign lcd_bl = 1'b1; endmodule注意rst_n & locked这个细节:外部复位虽然拉起来了,但如果 FPGA 内部的时钟 IP 还没锁定,从clk_gen出来的时钟可能是不完整的,直接驱动时序模块会产生奇怪的毛刺。加上locked是为了让时序模块在时钟稳定之后再开始跑。lcd_bl是背光使能,调试阶段直接拉高是最省事的做法;有些屏的背光引脚是低有效,这一步要看原理图。3. 核心模块拆解:彩色条纹是怎么从坐标算出来的3.1 时序生成:让像素坐标规规整整走完一帧TFT 显示的物理过程可以理解成一条扫描线从左上角走到右下角。tft_timing_gen.v里的核心就是两个计数器:hcnt计数当前行内的像素序号,vcnt计数当前是第几行。每次hcnt走到H_TOTAL - 1时回 0,同时vcnt加 1;vcnt走到V_TOTAL - 1时整帧结束,回到第 1 行。这段逻辑我抽出来看:localparam H_START = H_SYNC + H_BP; localparam H_END = H_START + H_ACTIVE - 1; localparam V_START = V_SYNC + V_BP; localparam V_END = V_START + V_ACTIVE - 1; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin hcnt <= 0; vcnt <= 0; end else begin if (hcnt == H_TOTAL - 1) begin hcnt <= 0; if (vcnt == V_TOTAL - 1) vcnt <= 0; else vcnt <= vcnt + 1; end else begin hcnt <= hcnt + 1; end end end assign de = (hcnt >= H_START) && (hcnt <= H_END) && (vcnt >= V_START) && (vcnt <= V_END); assign hsync = (hcnt < H_SYNC) ? 1'b0 : 1'b1; assign vsync = (vcnt < V_SYNC) ? 1'b0 : 1'b1;这里H_START等于同步脉宽加后肩,即有效区从同步结束后的第 216 个像素开始,持续 800 个像素,到 1015 结束。de信号就是有效显示窗口,之后第 5 章二次开发时,你会频繁用到它。可能有人会问为什么直接用de而不用hsync/vsync去锁数据。因为现在很多 RGB TFT 都支持 DE 同步模式,DE 有效时送出颜色数据,DE 无效时送 0,就能避免花屏;同步模式反而对时序要求更严格,极性一错就滚屏。这个工程默认以 DE 为主,行场同步只是按照屏规约拉出来,属于双兼容做法。3.2 彩条生成:用坐标区间给每个像素上色有了坐标之后,彩条生成就是纯粹的翻译工作。标准彩条从左到右依次是白、黄、青、绿、品红、红、蓝、黑,800 像素分成 8 段,每段 100 像素。colorbar_gen.v里不需要复杂的除法,直接比较横坐标落在哪个区间:module colorbar_gen #( parameter H_START = 216, parameter H_ACTIVE = 800 )( input wire pclk, input wire rst_n, input wire de, input wire [11:0] hcnt, input wire [11:0] vcnt, output reg [7:0] data_r, output reg [7:0] data_g, output reg [7:0] data_b ); wire [11:0] h_act = hcnt - H_START; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'h00; end else if (!de) begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'h00; end else if (h_act < 100) begin data_r <= 8'hFF; data_g <= 8'hFF; data_b <= 8'hFF; // 白 end else if (h_act < 200) begin data_r <= 8'hFF; data_g <= 8'hFF; data_b <= 8'h00; // 黄 end else if (h_act < 300) begin data_r <= 8'h00; data_g <= 8'hFF; data_b <= 8'hFF; // 青 end else if (h_act < 400) begin data_r <= 8'h00; data_g <= 8'hFF; data_b <= 8'h00; // 绿 end else if (h_act < 500) begin data_r <= 8'hFF; data_g <= 8'h00; data_b <= 8'hFF; // 品红 end else if (h_act < 600) begin data_r <= 8'hFF; data_g <= 8'h00; data_b <= 8'h00; // 红 end else if (h_act < 700) begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'hFF; // 蓝 end else begin data_r <= 8'h00; data_g <= 8'h00; data_b <= 8'h00; // 黑 end end endmodule这段代码里最容易漏掉的是!de时的置零分支。有些屏在有效区外会采样 RGB 数据,如果不主动送 0,残留上一段颜色,屏幕边缘会出现一条彩色尾巴。h_act是用hcnt减H_START得到的有效横坐标,范围是 0 到 799;注意它不能直接在de为低时使用,因为此时hcnt小于H_START,相减会得到一个非常大的数,所以一定要先把de拿来兜底。vcnt在这个模块里没有参与颜色计算,因为竖条纹只和横坐标有关;如果你想做横向渐变条,再把vcnt用上。3.3 仿真:用几行 testbench 检查一行帧时序仿真不是走形式,仿真过了至少能证明两个坐标计数器的行为是对的。这个包里的sim/tb_tft_rgb_colorbar.v写法很朴素:module tb_tft_rgb_colorbar; reg clk_ref = 0; reg rst_n = 0; wire pclk, hsync, vsync, de; wire [7:0] data_r, data_g, data_b; always #10 clk_ref = ~clk_ref; tft_rgb_colorbar_top dut( .clk_ref (clk_ref), .rst_n (rst_n), .pclk (pclk), .hsync (hsync), .vsync (vsync), .de (de), .data_r (data_r), .data_g (data_g), .data_b (data_b), .lcd_bl () // 仿真时悬空 ); initial begin rst_n = 0; #100 rst_n = 1; #50_000_000 $finish; end always @(posedge pclk) begin if (hcnt == 0) $display("Frame: vsync=%b hsync=%b vcnt=%d", vsync, hsync, vcnt); end endmodule跑仿真时重点关注几个点:hsync的低脉冲宽度要在H_SYNC个像素时钟内;de连续高电平的周期数应当是H_ACTIVE,也就是 800;vcnt到 524 之后跳回 0,完成一帧。如果在波形上看到de只高了 799 拍或者 hsync 不对称,先回 3.1 检查参数是不是被覆盖了。仿真看到的data_r/data_g/data_b在de为高时会按彩条顺序变化,这就代表数据链路正确。4. 避坑指南:TFT 彩条上板最常见的五个翻车现场4.1 现象一:屏一直白屏,彩条根本没出来白屏大概率意味着背光亮了,但没有有效数据进入屏。我遇到第一次调这块屏时,复位和时钟都拉对了,综合也过了,屏幕仍然全白。原因出在clk_gen的locked没有接进时序模块的复位,外部复位拉高后时序模块立刻开跑,而当时的像素时钟还在漂,产生了大量无效跳变,导致 de 从未正确拉高。解决方法是像 2.3 里那样写rst_n & locked,并且加一个仿真 log,打印locked值确认时钟 IP 没有报错。上板前可以用板上 LED 引一个 status,看到灯亮再收工,否则就是时钟问题优先排查。4.2 现象二:颜色顺序错乱,红绿蓝互换屏幕能显示条纹,但白色变成了绿色,或者原本红色变蓝,这种问题基本不是 FPGA 逻辑的问题,而是 RGB 数据线接错或者位序映射不对。这个工程输出的是 RGB888 三个 8 位通道,但很多屏的驱动板实际只引出了 18 位,需要丢弃低位。如果接口定义里 RGB 顺序是 BGR,数据必须交叉接线。解决方法是先别改代码,拿放大镜对着转接板原理图看,确认数据标号;如果硬件固定不能换线,就在colorbar_gen的端口赋值上做一次手工交叉映射,即把红色通道接到屏的蓝色引脚,其他通道同理,让逻辑适配物理接线。4.3 现象三:画面横滚或上下跳,图像像被撕裂横滚最常见的直接原因是行同步极性反了。TFT 屏规格书里如果写HSYNC: Negative,而工程里默认低有效,代码会输出hcnt < H_SYNC拉低,应该没问题;但如果规格书是正极性,你需要把那句改为hcnt >= H_SYNC时拉低。还有一种情况是同时接入了 DE 模式和行场同步模式,屏内部优先选了一个与外部冲突的信号,导致画面整体偏移。解决方法是临时把de直接拉低,只保留 hsync/vsync 跑一帧,看是否滚屏;如果滚屏,说明 DE 模式的输出和同步冲突,需要检查约束文件里是不是把 DE 引脚误接到了同步脚。4.4 现象四:屏幕明显闪烁,刷新率掉到 45Hz 以下800x480 跑到 33.3MHz 是 60Hz,如果实际像素时钟只有 25MHz,刷新率就会掉到 45Hz 左右,肉眼可见闪烁。有时综合工具会把clk_gen的参数记错,生成 27MHz 或 24MHz 的时钟,flash 里被告警过但你忽略了。解决方法是不要只看倍频系数,直接把pclk引到一个测试脚,用示波器或者频率计量一下,确认是 33.3MHz。更稳妥的办法是在 testbench 里用仿真测量两帧间隔,打印出刷新率,和屏规格书对一下再上板。4.5 现象五:上板后亮度极暗,只有边角有隐约图案这个现象背后十有八九是背光没有完全打开。很多 RGB TFT 的背光使能引脚是开漏输出,外部必须上拉,而 FPGA 的 IO 默认弱下拉;代码里lcd_bl = 1'b1虽然写了,但引脚约束可能没加PULLUP,相当于悬空。还有一个坑是背光信号别和某个低频时钟共用一个管脚,否则背光会被周期性关断,亮度看起来忽明忽暗。解决方法是暂时把背光引脚绑到一个固定高电平,换一个专用 IO,并加上拉约束;如果还是暗,查背光电源是否只有 3.3V,而屏需要 5V。5. 二次开发:把静态彩条改成可滚动动画5.1 让彩条动起来:一个偏移量搞定静态彩条验证完链路之后,很多人想让它看起来更像“动态”一点。其实只需要在colorbar_gen里增加一个每帧递增的偏移量,再用它替代h_act参与颜色区间判断。按这个思路可以写一个简单的滚动彩条:reg [9:0] offset = 0; always @(posedge pclk or negedge rst_n) begin if (!rst_n) offset <= 0; else if (de && hcnt == 1015) offset <= offset + 1; // 每行有效数据结束后偏移加一,速度快 end wire [11:0] h_shift = (h_act + offset >= H_ACTIVE) ? h_act + offset - H_ACTIVE : h_act + offset;用h_shift替换掉原本h_act所在的比较判断,屏幕上的条纹就会以每行 1 个像素的速度滚动。因为偏移量最大 799 加最大坐标 799 不会超过 1600,做一次减法就能回绕。想控制速度就把加一的条件从每行改成每帧一次,也就是检测vcnt到边界时再加。最后说个我自己的坏习惯:以前拿到新屏总想跳过彩条,直接跑自己的界面代码,结果经常分不清是软件问题还是硬件问题,白白耗掉一整天。从那以后我每次换屏或改板子,第一件事就是先把 tft_rgb_colorbar 跑一遍,确认时钟、同步、RGB 数据线全通再碰业务逻辑。这个习惯帮我省下的时间远比想象中多,希望帮到你。本文还有配套的精品资源,点击获取