VGA原理与FPGA实现
推荐参考小梅哥视频学习
使用FPGA驱动VGA不适合用来显示复杂多变的图像内容,但是却常用于显示实时图像内容,例如显示图像传感器实时采集到的图像。此种方式数据流由图像传感器实时提供,FPGA只需要控制好图像数据流的存储和传输即可,无需主动生成需要绘制的图像数据,所以实现相对简单。
我们来看看生活中常见到的VGA接口:
见识过 VGA 接口的外观,接下来对接口各引脚功能并没有进一步的认识,下
面,我们结合 VGA 接口引脚图和各引脚定义表格,对 VGA 接口各引脚做一下简单介绍:
上图中,引脚标黄的就是我们需要着重掌握的。我们再来看看Digilent公司给的参考图:
最后一句:To understand how these signals should be driven, we need to take a look at how our monitors actually work.不过在这里关于VGA显示器成像原理不管是CRT还是液晶显示器都不做赘述(有需要的可以去官网等地方学习),我们直接从它的时序分析,从而深入理解。
一、VGA时序详解
由于VGA时序的诞生,最早就是为了驱动阴极射线管(CRT)。也因为CRT的物理特性,才有了多个与之相关的物理时间参数,所以为了让大家彻底理解VGA时序中各个参数的物理意义,这里以一张图像在基于CRT结构的显示器上显示为例,介绍显示器完整展现这幅图的各个阶段和每个阶段的物理意义。
该图片来源于:小梅哥《VGA控制器设计与验证》文档。可能在不同的地方看到的有一些出入,只要理解他的含义就好,下图再给出Digilent公司的参考图:
从上面两张图可以看出白底的卡通人物图像(Display Area)就是我们希望显示的图像内容。我们的目的就是要让该图像恰好完全显示在显示器上。而CRT是怎么样实现图像显示的呢? 它是通过对一张图片进行一行一行的扫描,原理如下图:
1.CRT行扫描过程
对于CRT显示器,虽然扫描的时候是按照一行一行的方式进行的,但不是扫描完一行有效数据段之后就立马返回,而是会继续向右扫描一段区域,这个区域称为右边界区域(horizontal right border),该区域已经不在有效的显示范围内,如果从物理结构的角度来说,这一段对应的荧光屏玻璃上就不再有荧光粉了,但是电子枪还在继续向右走,大家可以形象理解为显示器右边的黑边。同理,也有左边界区域(horizontal left border)。可以参考图中Display Area周围的Border(边界)。
那么,电子枪什么时候会到最左侧准备开始新一行图像的扫描呢?当电子枪扫描一行图像到达荧光屏的最右端后,其并不会自动回到最左边准备下一行,而是需要有一个通知信号,通知其回去,这个通知信号就是行同步信号脉冲(horizontal sync pulse)。行同步信号是一个脉冲,当该脉冲出现后,电子枪的指向会在一定时间内从最右侧回到显示屏的最左侧。而这个回去的过程需要耗费一定的时间,这个时间就称为horizontal back porch。这也是这个名词中back的意义所在,即出现行同步信号后,电子枪从显示屏最右侧回到最左侧的时间。
当电子枪扫描过了右侧没有荧光粉的区域后,还没有收到回到最左侧的命令(行同步信号脉冲)之前,电子枪需要关闭以实现消隐,这个消隐的时间段就称为horizontal front porch,直观一点理解就是完成了一行图像的扫描,但还没收到回到最左侧命令之前的一段时间。这也是这个名词中front的意义所在。
2.CRT场扫描过程
理解了行扫描中的"参数"含义理解场扫描中的名词就非常简单了。首先来讲,CRT在扫描一行图像的时候,电子枪的水平位置是保持稳定不变的,而当一行图像扫描完成,开始扫描下一行图像的时候,电子枪的水平位置会向下调整一定的值。因此,我们可以认为,场时序就是在垂直方向上从上往下依次扫描。
其次来说,对于CRT显示器来说,其不是扫描完所有行的图像后就立马返回最上方,而是会继续向下扫描一段区域,这个区域称为下边界区域(vertical bottom border),该区域已经不在有效的显示范围内,如果从物理结构的角度来说,这一段对应的荧光屏玻璃上就不再有荧光粉了,但是电子枪还在继续向下走,大家可以形象理解为显示器下边的黑边。同样的,显示器上边也有这样一段黑边,在开始显示有效数据之前,电子枪扫描到的这段区域同样也是没有荧光粉的,不会显示图像, 这个区域称为上边界区域(vertical top border)。
再来说说,电子枪什么时候会到最上方准备开始新一场图像的扫描。当电子枪扫描一场图像到达荧光屏的最下方后,其并不会自动回到最上边准备下一场,而是需要有一个通知信号,通知其回去,这个通知信号就是场同步信号脉冲(vertical sync pulse)。场同步信号是一个脉冲,当该脉冲出现后,电子枪的指向会在一定时间内从最下方回到显示屏的最上方。而这个回去的过程需要耗费一定的时间,这个时间就称为vertical back porch。这也是这个名词中back的意义所在,即出现场同步信号后,电子枪从显示屏最下方回到最上方的时间。
当电子枪扫描过了下方没有荧光粉的区域后,还没有收到回到最上方的命令(场同步信号脉冲)之前,电子枪需要关闭以实现消隐,这个消隐的时间段就称为vertical front porch,直观一点理解就是完成了一场图像的扫描,但还没收到回到最上方命令之前的一段时间。这也是这个名词中front的意义所在。
3.VGA时序
在上面介绍行、场同步脉冲信号的时候,我们只说了是脉冲信号,但是并未定义脉冲信号的极性,VGA时序标准支持四种极性,但是并不是所有的显示设备都支持这四种极性,本文默认以应用最为广泛的行、场同步信号都为低脉冲进行介绍。所谓行、场同步信号都是低电平,就是说在产生同步脉冲信号的时候,行、场同步信号变为低电平,其他时刻为高电平。注意:Digilent公司提供的参考图中是以高脉冲为例,但是其下开发板并不都是高脉冲。
下图分别为行扫描时序和场扫描时序的示意图。
上述两幅图中,都只给出了时序参数的名称,并没有给出每个参数具体的值是多少。而每个参数具体的值是多少,并不是固定的,而是根据需要扫描的有效图像区域的大小确定的。需要扫描的有效图像区域的大小,一般用分辨率来表示。例如标准VGA时序的分辨率就是640*480个像素点。除了VGA标准,还有很多表示更大或更小分辨率的标准,如QVGA、SVGA、XVGA等等。
4.各常见分辨率时序参数
下表给出了若干个常见分辨率对应的行场时序中各个参数的具体数值,注意,这些参数值中,行相关的参数都是以像素的更新频率,也就是像素时钟作为单位,而场相关的参数,则是以行作为单位。
即使 VGA 显示分辨率相同,但刷新频率不同的话,相关参数也存在差异,如 640x480@60、640x480@75,这两个显示模式虽然具有相同的分辨率,但是 640x480@75 的刷新频率更快,所以像素时钟更快,时序参数也有区别。
下面我们以显示模式 640x480@60、640x480@75 为例,学习一下时钟频率的计算方法。
640x480@60:
行扫描周期:800(像素),场扫描周期:525(行扫描周期) 刷新频率:60Hz
800 * 525 * 60 = 25,200,000 ≈ 25.175MHz (误差忽略不计)
640x480@75:
行扫描周期:840(像素) 场扫描周期:500(行扫描周期) 刷新频率:75Hz
840 * 500 * 75 = 31,500,000 = 31.5MHz
在计算时钟频率时,读者要谨记一点,要使用行扫描周期和场扫描周期的参数进行计算,不能使用有效图像的参数进行计算,虽然在有效图像外的其他阶段图像信息均无效,但图像无效阶段的扫描也花费了扫描时间。
二、640*480分辨率VGA 控制器设计与验证
有关于设计思路呢,我认为从宏观上来说Digilent公司介绍的不错:
尤其是上图框图中的计数器、时钟分频器写的很清楚,当然细节一步步的代码怎么写如果要仔细介绍,就过于冗余了,相信很多人看了时序分析就已经会自己写了,如果不会写,可以随机搜一个代码啃一遍肯定会获益更多。
设计代码 |
module VGA_CTRL(Clk,Reset_n,Data,Data_Req,hcount,vcount,VGA_HS,VGA_VS,VGA_BLK,VGA_RGB);input Clk;input Reset_n;input [23:0]Data;output reg Data_Req; //根据波形调试得到output reg [9:0]hcount; //当前扫描点的有效图片H坐标output reg [9:0]vcount; //当前扫描点的有效图片V坐标 用于test模块output reg VGA_HS;output reg VGA_VS; output reg VGA_BLK; //BLK表示的就是 输出有效图片 信号 高电平有效output reg [23:0]VGA_RGB;// RGB888localparam Hsync_End = 800; //行扫描结束 即行的总时间 单位 像素时钟localparam HS_End = 96; //行同步信号脉冲localparam Vsync_End = 525; //场扫描结束 即场的总时间 单位 行localparam VS_End = 2; //场同步信号脉冲localparam Hdat_Begin = 144; //行输出有校图片 开始localparam Hdat_End = 784; //行输出有校图片 结束localparam Vdat_Begin = 35; //场输出有校图片 开始localparam Vdat_End = 515; //场输出有校图片 结束//行扫描计数器reg [9:0]hcnt;always@(posedge Clk or negedge Reset_n)if(!Reset_n)hcnt <= 0;else if(hcnt >= Hsync_End -1)hcnt <= 0;elsehcnt <= hcnt + 1'b1;//行同步信号always@(posedge Clk)VGA_HS <= (hcnt < HS_End)?0:1; //用的使能是常见的低脉冲有效//场扫描计数器 reg [9:0]vcnt;always@(posedge Clk or negedge Reset_n)if(!Reset_n)vcnt <= 0;else if(hcnt == Hsync_End -1)beginif(vcnt >= Vsync_End -1)vcnt <= 0;elsevcnt <= vcnt + 1'd1;endelsevcnt <= vcnt;//场同步信号 always@(posedge Clk)VGA_VS <= (vcnt < VS_End)?0:1;//BLK表示的就是输出有效图片 部分always@(posedge Clk)Data_Req <= ((hcnt >= Hdat_Begin - 1) && (hcnt < Hdat_End - 1) && (vcnt >= Vdat_Begin) && (vcnt < Vdat_End))?1:0;always@(posedge Clk)VGA_BLK <= Data_Req; always@(posedge Clk)VGA_RGB <= Data_Req? Data:0; always@(posedge Clk)hcount <= Data_Req? hcnt - Hdat_Begin:0; always@(posedge Clk)vcount <= Data_Req? vcnt - Vdat_Begin:0; endmodule
测试代码 |
`timescale 1ns / 1psmodule VGA_CRTL_tb ;reg Clk;reg Reset_n;reg [23:0]Data;wire Data_Req;wire [9:0]hcount;wire [8:0]vcount;wire VGA_HS;wire VGA_VS; wire VGA_BLK;wire [23:0]VGA_RGB;//{R[7:0]、G[7:0]、B[7:0]}VGA_CTRL VGA_CTRL(Clk,Reset_n,Data,Data_Req,hcount,vcount,VGA_HS,VGA_VS,VGA_BLK,VGA_RGB);initial Clk = 1;always #20 Clk = ~Clk;//HS 的变化位置//VS 的变化位置//待显示数据和HS、VS的位置关系initial beginReset_n = 0;#201;Reset_n = 1;#200000000;$stop;endalways@(posedge Clk or negedge Reset_n)if(!Reset_n)Data <= 0;else if(!Data_Req)Data <= Data;elseData <= Data + 1'd1;endmodule
仿真结果 |
仿真结果建议自己跑一下,然后对照时序挨个验证一下,关于这里的代码主要注意两点:
1.Data_Req 和 VGA_BLK
2.hcnt和VGA_HS在仿真结果中可能得理解一下
三、拓展
1.等宽彩条
VGA控制器的像素时钟为25MHz,而很多开发板或者晶振提供的是50MHz的时钟,所以首先大家可以在在vivado下的clocking wizard配置25MHz时钟,这里我配置中没有选择reset和locked。
module VGA_CTRL_test(Clk,Reset_n,VGA_RGB, //有效数据输出VGA_HS, //行同步信号VGA_VS, //场同步信号VGA_BLK, //VGA 场消隐信号VGA_CLK //25MHz);input Clk;input Reset_n;output [23:0] VGA_RGB;output VGA_HS;output VGA_VS;output VGA_BLK;output VGA_CLK;reg disp_data;wire Clk25M;wire Data_Req;wire [9:0] hcount;wire [9:0] vcount;assign VGA_CLK= Clk25M;vga_pll vga_pll(.clk_out1(Clk25M),.clk_in1(Clk));VGA_CTRL VGA_CTRL(.Clk(Clk25M),.Reset_n(Reset_n),.Data(disp_data),.Data_Req(Data_Req),.hcount(hcount),.vcount(vcount),.VGA_HS(VGA_HS),.VGA_VS(VGA_VS),.VGA_BLK(VGA_BLK),.VGA_RGB(VGA_RGB));//定义颜色编码
localparam BLACK = 24'h000000, //黑色BLUE = 24'h0000FF, //蓝色RED = 24'hFF0000, //红色PURPPLE = 24'hFF00FF, //紫色GREEN = 24'h00FF00, //绿色CYAN = 24'h00FFFF, //青色YELLOW = 24'hFFFF00, //黄色WHITE = 24'hFFFFFF; //白色//定义每个像素块的默认显示颜色值
localparam R0_C0 = BLACK, //第0行0列像素块R0_C1 = BLUE, //第0行1列像素块R1_C0 = RED, //第1行0列像素块R1_C1 = PURPPLE,//第1行1列像素块R2_C0 = GREEN, //第2行0列像素块R2_C1 = CYAN, //第2行1列像素块R3_C0 = YELLOW, //第3行0列像素块R3_C1 = WHITE; //第3行1列像素块wire R0_act = vcount >= 0 && vcount < 120; //正在扫描第0行wire R1_act = vcount >= 120 && vcount < 240;//正在扫描第1行wire R2_act = vcount >= 240 && vcount < 360;//正在扫描第2行wire R3_act = vcount >= 360 && vcount < 480;//正在扫描第3行wire C0_act = hcount >= 0 && hcount < 320; //正在扫描第0列wire C1_act = hcount >= 320 && hcount < 640;//正在扫描第1列 wire R0_C0_act=R0_act & C0_act;//第0行0列像素块处于被扫描中标志信号wire R0_C1_act=R0_act & C1_act;//第0行1列像素块处于被扫描中标志信号wire R1_C0_act=R1_act & C0_act;//第1行0列像素块处于被扫描中标志信号wire R1_C1_act=R1_act & C1_act;//第1行1列像素块处于被扫描中标志信号wire R2_C0_act=R2_act & C0_act;//第2行0列像素块处于被扫描中标志信号wire R2_C1_act=R2_act & C1_act;//第2行1列像素块处于被扫描中标志信号wire R3_C0_act=R3_act & C0_act;//第3行0列像素块处于被扫描中标志信号wire R3_C1_act=R3_act & C1_act;//第3行1列像素块处于被扫描中标志信号always@(*)case({R3_C1_act,R3_C0_act,R2_C1_act,R2_C0_act,R1_C1_act,R1_C0_act,R0_C1_act,R0_C0_act})8'b0000_0001:disp_data = R0_C0;8'b0000_0010:disp_data = R0_C1;8'b0000_0100:disp_data = R1_C0;8'b0000_1000:disp_data = R1_C1;8'b0001_0000:disp_data = R2_C0;8'b0010_0000:disp_data = R2_C1;8'b0100_0000:disp_data = R3_C0;8'b1000_0000:disp_data = R3_C1;default:disp_data = R0_C0;endcaseendmodule
2.多分辨率适配型VGA控制器设计
**条件编辑原理:**所谓条件编译,就是当设计中满足某个条件时,将该条件下的一段代码编译进设计中。因此,我们只需要合理设置编译条件,并在对应的编译条件下编写正确的代码,就可以实现条件编译。以一个最简单的例子来说明。
对于一个最简单的16位计数器,我们可以根据不同的需求,设置其计数的最大值,从而来设置其计数一周所占用的时间。假设这个计数最大值用CNT_MAX表示,如果定义了工作在条件一(CASE_A)的情况下,该计数最大值为2000,如果定义了工作在条件二(CASE_B)的情况下,该计数最大值为2500,则该代码可以按照如下方式编写。
`define CASE_A
//`define CASE_B`ifdef CASE_A`define CNT_MAX 2000
`elsif CASE_B`define CNT_MAX 2500
`endif
实际使用时,只需要在代码中定义工作在条件一还是条件二,就能选择让CNT_MAX为对应的值。在Verilog代码中,只需要直接使用CNT_MAX这个参数即可,如下所示。
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)cnt <= 0;
else if(cnt == `CNT_MAX)cnt <= 0;
elsecnt <= cnt + 1'b1;
代码
//`define Resolution_480x272 1 //刷新率为60Hz时像素时钟为9MHz
//`define Resolution_640x480 1 //刷新率为60Hz时像素时钟为25.175MHz
`define Resolution_800x480 1 //刷新率为60Hz时像素时钟为33MHz
//`define Resolution_800x600 1 //刷新率为60Hz时像素时钟为40MHz
//`define Resolution_1024x768 1 //刷新率为60Hz时像素时钟为65MHz
//`define Resolution_1280x720 1 //刷新率为60Hz时像素时钟为74.25MHz
//`define Resolution_1920x1080 1 //刷新率为60Hz时像素时钟为148.5MHz`ifdef Resolution_480x272 `define H_Right_Border 0`define H_Front_Porch 2`define H_Sync_Time 41`define H_Back_Porch 2`define H_Left_Border 0`define H_Data_Time 480`define H_Total_Time 525`define V_Bottom_Border 0`define V_Front_Porch 2`define V_Sync_Time 10`define V_Back_Porch 2`define V_Top_Border 0`define V_Data_Time 272`define V_Total_Time 286`elsif Resolution_640x480`define H_Total_Time 12'd800`define H_Right_Border 12'd8`define H_Front_Porch 12'd8`define H_Sync_Time 12'd96`define H_Data_Time 12'd640`define H_Back_Porch 12'd40`define H_Left_Border 12'd8`define V_Total_Time 12'd525`define V_Bottom_Border 12'd8`define V_Front_Porch 12'd2`define V_Sync_Time 12'd2`define V_Data_Time 12'd480`define V_Back_Porch 12'd25`define V_Top_Border 12'd8`elsif Resolution_800x480`define H_Total_Time 12'd1056`define H_Right_Border 12'd0`define H_Front_Porch 12'd40`define H_Sync_Time 12'd128`define H_Data_Time 12'd800`define H_Back_Porch 12'd88`define H_Left_Border 12'd0`define V_Total_Time 12'd525`define V_Bottom_Border 12'd8`define V_Front_Porch 12'd2`define V_Sync_Time 12'd2`define V_Data_Time 12'd480`define V_Back_Porch 12'd25`define V_Top_Border 12'd8`elsif Resolution_800x600`define H_Total_Time 12'd1056`define H_Right_Border 12'd0`define H_Front_Porch 12'd40`define H_Sync_Time 12'd128`define H_Data_Time 12'd800`define H_Back_Porch 12'd88`define H_Left_Border 12'd0`define V_Total_Time 12'd628`define V_Bottom_Border 12'd0`define V_Front_Porch 12'd1`define V_Sync_Time 12'd4`define V_Data_Time 12'd600`define V_Back_Porch 12'd23`define V_Top_Border 12'd0`elsif Resolution_1024x768`define H_Total_Time 12'd1344`define H_Right_Border 12'd0`define H_Front_Porch 12'd24`define H_Sync_Time 12'd136`define H_Data_Time 12'd1024`define H_Back_Porch 12'd160`define H_Left_Border 12'd0`define V_Total_Time 12'd806`define V_Bottom_Border 12'd0`define V_Front_Porch 12'd3`define V_Sync_Time 12'd6`define V_Data_Time 12'd768`define V_Back_Porch 12'd29`define V_Top_Border 12'd0`elsif Resolution_1280x720`define H_Total_Time 12'd1650`define H_Right_Border 12'd0`define H_Front_Porch 12'd110`define H_Sync_Time 12'd40`define H_Data_Time 12'd1280`define H_Back_Porch 12'd220`define H_Left_Border 12'd0`define V_Total_Time 12'd750`define V_Bottom_Border 12'd0`define V_Front_Porch 12'd5`define V_Sync_Time 12'd5`define V_Data_Time 12'd720`define V_Back_Porch 12'd20`define V_Top_Border 12'd0`elsif Resolution_1920x1080`define H_Total_Time 12'd2200`define H_Right_Border 12'd0`define H_Front_Porch 12'd88`define H_Sync_Time 12'd44`define H_Data_Time 12'd1920`define H_Back_Porch 12'd148`define H_Left_Border 12'd0`define V_Total_Time 12'd1125`define V_Bottom_Border 12'd0`define V_Front_Porch 12'd4`define V_Sync_Time 12'd5`define V_Data_Time 12'd1080`define V_Back_Porch 12'd36`define V_Top_Border 12'd0 `endif
修改VGA_CTRL模块中的hcount和vcount的位宽,以及相关参数:
`include "vga_parameter.v" //注意该命令localparam Hsync_End = `H_Total_Time;localparam HS_End = `H_Sync_Time;localparam Hdat_Begin = `H_Sync_Time + `H_Back_Porch + `H_Left_Border;localparam Hdat_End = `H_Sync_Time + `H_Left_Border + `H_Back_Porch + `H_Data_Time;localparam Vsync_End = `V_Total_Time;localparam VS_End = `V_Sync_Time;localparam Vdat_Begin = `V_Sync_Time + `V_Back_Porch + `V_Top_Border;localparam Vdat_End = `V_Sync_Time + `V_Back_Porch + `V_Top_Border + `V_Data_Time;
参考文献:
小梅哥VGA学习资料
野火FPGA学习资料
添加链接描述Digilent 的 VGA Display Controller