VGA自1986年发布距今已有34年了。
不少朋友实际上还在使用VGA接口的显示器,很多显卡仍然提供VGA输出端子供用户使用。尽管现在使用的VGA标准不是当年的那个,但是其顽强的生命力还是可见一斑。
这是Wikipedia上当今的一部分图像标准,需要注意的是VGA及其所有衍生标准都是4:3画幅。
VGA中有两个非常重要的信号:水平同步信号(HS)与垂直同步(VS),有些教程里面叫做行同步和场同步。
这两个同步信号给VGA提供精准的时间参考,使得显示器能够依照设置的分辨率显示图形。
需要注意的是无论是水平同步信号还是垂直同步信号,都是负脉冲。
对于每一个VGA分辨率,同步信号各个部分持续的长度都不同,这个长度可以查阅这个网站
本次实验笔者使用的是1280×1024@60Hz的信号,查阅上述网站可知
水平同步 | 时长(像素) | 垂直同步 | 时长(线) |
---|---|---|---|
前沿 A | 48 | 前沿 A | 1 |
同步脉冲 B | 112 | 同步脉冲 B | 2 |
后沿 C | 248 | 后沿 C | 38 |
有效数据 D | 1280 | 有效数据 D | 1024 |
这里最重要的是水平同步和垂直同步的关系。
在回答这个问题之前,需要联系VGA标准出现的时代。
那时候显示器主要是CRT显示器。CRT显示器是依靠阴极射线工作的,也就是高速电子流。
当高速电子流扫过显示器表面时,会点亮对应的像素点。通常而电子束的轨迹类似这样。
为了点亮整个屏幕,电子束需要在水平偏转系统和垂直偏转系统的共同协调下在极短的时间一行一行扫过整个屏幕。
电子束的运动方式如下:从荧光屏的一段开始水平扫到另一端,然后回到起始端继续扫下一行。
因此,在一整个水平同步周期内电子束扫满一行,一整个水平同步周期构成了一个单位垂直同步周期(线)。这就好比计数,记满一行进一,进的这个一就加在了垂直同步上。
分辨率:1280×1024 @ 60Hz
像素时钟频率(像素带宽):108.00Mhz
水平同步 | 时长(像素) | 时长(微秒) | 垂直同步 | 时长(线) | 时长(毫秒) |
---|---|---|---|---|---|
前沿 A | 48 | 0.4444 | 前沿 A | 1 | 0.01562 |
同步脉冲 B | 112 | 1.0370 | 同步脉冲 B | 2 | 0.04689 |
后沿 C | 248 | 2.2963 | 后沿 C | 38 | 0.5939 |
有效数据 D | 1280 | 11.8519 | 有效数据 D | 1024 | 16.0047 |
总时长A+B+C+D | 1688 | 15.6296 | 总时长A+B+C+D | 1066 | 16.6612 |
这个表格再这里又出现了一次。
我们来对这个表格进行详细分析:
这一次增加了一个新的概念:像素时钟。像素时钟是一个像素点点亮的时间的倒数。在1280×1024 @ 60Hz这个分辨率下,一个像素点亮的时长约为9.259纳秒,这也是水平同步的单位时长。
我们可以试着计算水平同步中前沿的时长:
T H A = 48 ( p i x e l ) × 9.259 ( n s / p i x e l ) = 444.43 n s = 0.44443 μ s T_{HA}=48(pixel)\times9.259(ns/pixel)=444.43ns=0.44443\mu s THA=48(pixel)×9.259(ns/pixel)=444.43ns=0.44443μs
这与表格中A的时长对应。
在上面我们说到,电子束扫过一整行后向下垂直偏转一点继续扫描下一行,也就是说一整个水平同步周期应该是一个单位的垂直同步周期,我们还是来计算一下垂直同步的前沿
T V A = 1 ( l i n e ) × ( T H A + T H B + T H C + T H D ) = 15.6296 n s = 0.01562 m s T_{VA} = 1(line)\times(T_{HA}+T_{HB}+T_{HC}+T_{HD})=15.6296ns = 0.01562ms TVA=1(line)×(THA+THB+THC+THD)=15.6296ns=0.01562ms
这与表中垂直同步A对应。
到此我们掌握了VGA同步周期的一般规律。
VGA采用的是模拟信号传输,模拟电压的大小代表了像素的亮度。为了将FPGA的数字信号变成模拟信号,一般而言都会使用一个DAC。
无论这个DAC是一个专用芯片还是一个权电阻网络,都应该注意的是:在VGA适配器的内部有一个75Ω的端接电阻。
这个端接电阻起到了将电流信号转换为电压信号的作用。因为一般而言,电流型输出的DAC容易做到如此高的带宽。
由于VGA使用了RGB565的格式,我们只需要最高6位的DAC就可以完成VGA的驱动,因此价格低廉的权电阻网络是一个非常不错的选择!
上图就是一个使用贴片电阻构成的权电阻网络DAC,使用了若干个1K和2K的贴片排阻,有兴趣的读者可以推导一下。
注意VGA表示信号的模拟电压在0~0.7V之间。
数据是要结合水平同步周期和垂直同步周期来分析的。
我们注意到在两个同步周期的D段是有效数据,只有这部分数据可以被显示在上面。那么在其他阶段可不可以给VGA提供信号呢?答案是不可以。
在早期显示器中,显示后沿包含了电子束水平扫回的路径,这时候是一定不可以给VGA加上电压信号的,否则在扫回的路径上会有一次点亮荧光粉,造成显示不清楚,为了避免这个现象需要强制让电压信号为0,这一过程叫消隐。
在显示前沿的时候,电子束并未打在屏幕涂油荧光粉的部分,这时候加入信号,电子流将打在显像管的管壁上,虽然没有什么损伤,总归是不好的,也应该避免。
然而线代显示器几乎都是液晶显示器,并不存在什么电子束,应该来说在D之外的时间加上信号无伤大雅。实际上这会造成显示器画面偏色、画面移位等问题!
这一点在编程的时候一定要注意!
现代的显示器都有相位补偿的功能。当信号传输距离较长时,有时候水平同步与垂直同步信号的有效段与真实数据不能完全匹配,这就造成了画面上下偏移或者是左右偏移。大部分显示器都是可以自动调节的,这个在OSD菜单中叫做“相位调节”
在具有自动相位调节功能的显示器上,可能会错误地识别某些不合时宜的数据信号,导致显示出现波纹状抖动,或者图像整体偏移。
首先定义了几个参数,这些都与表格中的一致。
module vga(
input clk,
input nrst,
input [15:0] data,//RGB565
output [10:0] x,
output [10:0] y,
output wire vga_hs,
output wire vga_vs,
output [15:0] vga
);
parameter h_visible = 1280;
parameter h_front = 48;
parameter h_sync = 112;
parameter h_back = 248;
parameter h_whole = 1688;
parameter v_visible = 1024;
parameter v_front = 1;
parameter v_sync = 3;
parameter v_back = 38;
parameter v_whole = 1066;
由于VGA的时序逻辑非常简单,这里信号的处理都采用了连续赋值的形式。
assign vga_hs = (hcount >= 11'd0 && hcount < h_sync) ? 1'b0 : 1'b1;
assign vga_vs = (vcount >= 20'd0 && vcount < v_sync) ? 1'b0 : 1'b1;
assign vga_hen = (hcount >= h_sync + h_back && hcount < h_sync + h_back + h_visible) ? 1'b1 : 1'b0; //水平有效信号
assign vga_ven = (vcount >= v_sync + v_back && vcount < v_sync + v_back + v_visible) ? 1'b1 : 1'b0; //垂直有效信号
assign x = vga_hen ? hcount - h_sync - h_back : 11'b0; //水平坐标
assign y = vga_ven ? vcount - v_sync - v_back : 11'b0; //垂直坐标
assign vga = vga_hen & vga_ven ? data : 16'b0; //信号
注意最后一句
assign vga = vga_hen & vga_ven ? data : 16'b0; //信号
当水平有效和垂直有效的时候才加上信号,否则为0。
接下来时两个同步信号的产生,这两个信号在同一个always块中产生,因为他们是进位的关系,很容易用优先编码器实现。
reg [10:0] hcount;
reg [10:0] vcount;
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
hcount <= 11'b0;
vcount <= 11'b0;
end
else begin
if(hcount < h_whole)
hcount <= hcount + 1'b1;
else begin
hcount <= 11'b0;
if(vcount < v_whole)
vcount <= vcount + 1'b1;
else
vcount <= 11'b0;
end
end
end
module vga_test(
input clk,
input nrst,
input [10:0] x,
input [10:0] y,
output reg [15:0] data
);
parameter red = 16'b11111_000000_00000;
parameter green = 16'b00000_111111_00000;
parameter blue = 16'b00000_000000_11111;
parameter purple = 16'hf81f;
parameter yellow = 16'hffe0;
parameter cyan = 16'h07ff;
parameter orange = 16'hfc00;
parameter white = 16'hffff;
parameter bar_wide = 11'd1280 / 11'd8;
always @(posedge clk or negedge nrst) begin
if(!nrst) begin
data <= 16'b0;
end
else begin
if(x < bar_wide * 11'd1)
data <= red;
else if(x < bar_wide * 11'd2)
data <= green;
else if(x < bar_wide * 11'd3)
data <= blue;
else if(x < bar_wide * 11'd4)
data <= purple;
else if(x < bar_wide * 11'd5)
data <= yellow;
else if(x < bar_wide * 11'd6)
data <= cyan;
else if(x < bar_wide * 11'd7)
data <= orange;
else
data <= white;
end
end
endmodule
在测试模块中根据坐标会生成一些彩条。
最后在顶层模块中对这两个模块进行例化
module vga_top(
input clk,
input nrst,
output [15:0]vga,
output vga_hs,
output vga_vs
);
wire [15:0] data;
wire [10:0] x;
wire [10:0] y;
wire clk_108M;
wire locked;
vga vga_inst(
.clk(clk_108M),
.nrst(locked&nrst),
.data(data),//RGB565
.x(x),
.y(y),
.vga_hs(vga_hs),
.vga_vs(vga_vs),
.vga(vga)
);
vga_test vga_test_inst(
.clk(clk_108M),
.nrst(locked&nrst),
.x(x),
.y(y),
.data(data)
);
pll_108M pll_108M_inst (
.areset (!nrst),
.inclk0 (clk),
.c0 (clk_108M),
.locked (locked)
);
endmodule
这里为了产生108兆的像素时钟使用了一个PLL。
显示器正确显示了图像
打开OSD菜单,可以看到
设置的1280 × 1024 @ 60Hz被正确识别,至此VGA显示实验成功。