驱动VGA显示
概述: VGA(Video Graphics Array),视频图形阵列,是一种视频传输标准,具有分辨率高、显示速度快、颜色丰富等优点,不支持音频传输。
说明: FPGA芯片采用了altera的Cyclon IV E系列的“EP4CE10F17C8”,软件环境-Quartus-Ⅱ。
VGA是一种D型接口,采用非对称分布连接方式,共有15针,分三排,每排5个孔。
●VGA接口管脚表:
管脚 | 定义 | 管脚 | 定义 |
---|---|---|---|
1 | 红基色(Red) | 9 | 保留(各家定义不同) |
2 | 绿基色(Green) | 10 | 数字地 |
3 | 蓝基色(Blue) | 11 | 地址码 |
4 | 地址码(ID Bit) | 12 | 地址码 |
5 | 自测试(各家定义不同) | 13 | 行同步 |
6 | 红地 | 14 | 场同步 |
7 | 绿地 | 15 | 地址码(各家定义不同) |
8 | 蓝地 |
在15个管脚中,其中比较重要的是3根RGB彩色分量信号和2根扫描同步信号HSYNC和VSYNC。
●像素点构成: VGA显示器上每一个像素点可以有多种颜色,由三基色信号R、G、B组合构成。如果每个像素点采用3位二进制数表示(R、G、B信号各1位),则总共可以显示2×2×2=8种颜色,每个像素点采用8位二进制数表示(R、G、B信号分别为3、3、2),则总共可以显示8×8×4=256种颜色。
R(bit) | G(bit) | B(bit) | 颜色个数 |
---|---|---|---|
1 | 1 | 1 | 2×2×2=8 |
3 | 3 | 2 | 8×8×4=256 |
要知道VGA显示器是不认识数字信号的,它只认识模拟信号。所谓它只认识模拟信号,即 在它的数据引脚1、2、3(RED、GREEN、BLUE)输入的不是简单的0、1数字信号,而是模拟电压(0V-0.714V)。1、2、3引脚具有不同的电压时,VGA显示器显示不同的颜色。
但是FPGA要想产生模拟信号就需要借助DA,利用DA产生模拟信号,输出至VGA的RED、GREEN、BLUE基色数据线。也有利用电阻网络分流模拟DA实现的。
●VGA各种颜色如何实现的
在VGA接口的1、2、3引脚分别接至以下电压:
RED | GREEN | BLUE | 颜色 |
---|---|---|---|
0.714V | 0V | 0V | 红色 |
0V | 0.714V | 0V | 绿色 |
0V | 0V | 0.714V | 蓝色 |
0V | 0V | 0.357V | 半蓝色 |
0V | 0V | 0V | 黑色 |
●利用电阻网络分流模拟DA
R-2R电阻网络分压原理:
(1)每个像素点采用3位二进制(R(1bit)、G(1bit)、B(1bit))
例如VGA显示器要显示纯红色,则在RED引脚要输入0.714V电压,如果我们VGA_RED信号为1位电路如下图所示,那么(X+75)/3.3=75/0.714,计算得出X=271.6。
将R1阻值选择为271.6Ω,仿真得出VGA_RED引脚输入电压确实为0.714V。
(2)每个像素点采用8位二进制(R(3bit)、G(3bit)、B(2bit))
如果我们VGA_RED信号为3位电路如下图所示,那么(X+75)/3.3=75/0.714,计算得出X=271.6,X是通过三个电阻并联得到的。Rx||2Rx||4Rx=X,得出Rx=475.3Ω。
将R1、R2、R3阻值选择为475.3Ω、950.6Ω、1.9012KΩ,并且将R1、R2、R3都接通至3.3V,得出VGA_RED节点电压为0.714V。
上图的三个3.3V节点接至FPGA的三个输出引脚,就可以通过数字量去控制模拟量了。 例如FPGA三引脚输出数字量101,则对应以下电路(则会对应不同的颜色),VGA_RED节点电压为0.554V:
VGA_GREEN、VGA_BLUE同理,我们就会得到以下完整的电路:
原理很简单,之前说过,每个像素点采用8位二进制数表示(R、G、B信号分别为3、3、2)。观察上图得到以下对应关系:
RGB | R(3bit) | G(3bit) | B(2bit) |
---|---|---|---|
8位二进制 | D7、D6、D5 | D4、D3、D2 | D1、D0 |
对应硬件端口 | VGA_R2、 VGA_R1、VGA_R0 | VGA_G2、VGA_G1、VGA_G0 | VGA_B1、VGA_B0 |
D7-D0分别映射到FPGA的8个IO口,就可以通过这8个IO口的数字量去控制VGA的颜色显示了。
帧时序的四个部分别是:同步脉冲(Sync o)、显示后沿(Back porch p)、显示时序段(Display interval q)和显示前沿(Front porchr)。其中同步脉冲(Sync o)、显示后沿(Back porch p)和显示前沿(Front porch r)是消隐区,RGB信号无效,屏幕不显示数据。显示时序段(Display interval q)是有效数据区。.
o | p | q | r |
---|---|---|---|
同步脉冲(Sync) | 显示后沿(Back porch) | 显示时序段(Display interval) | 显示前沿(Front porch) |
RGB信号无效 | RGB信号无效 | 有效数据区 | RGB信号无效 |
●行时序:
行时序的四个部分分别是:同步脉冲(Sync a)、显示后沿(Back porch b)、显示时序(Display interval c)和显示前沿(Front porchd)。其中同步脉冲(Sync a)、显示后沿(Back porch b)和显示前沿(Front porch d)是消隐区,RGB信号无效,屏幕不显示数据。显示时序段(Display interval c)是有效数据区。
a | b | c | d |
---|---|---|---|
同步脉冲(Sync) | 显示后沿(Back porch) | 显示时序段(Display interval) | 显示前沿(Front porch) |
RGB信号无效 | RGB信号无效 | 有效数据区 | RGB信号无效 |
不同的分辨率,它的时序是不一样的。例如800*600@60Hz的VGA时序:
行时序(HSYNC数据线):
a | b | c | d | e |
---|---|---|---|---|
拉低的128个列像素 | 拉高的88个列像素 | 拉高的800个列像素 | 拉高的40个列像素 | 总共1056个列像素 |
帧时序(VSYNC数据线):
o | p | q | r | s |
---|---|---|---|---|
拉低的4个行像素 | 拉高的23个行像素 | 拉高的600个行像素 | 拉高的1个行像素 | 总共628个行像素 |
时钟频率:
628×1056×60约为40MHz。
重点都在代码里:
//行时序宏定义
`define HSYNC_A 16'd128
`define HSYNC_B 16'd216
`define HSYNC_C 16'd1016
`define HSYNC_D 16'd1056
//列时序宏定义
`define VSYNC_O 16'd4
`define VSYNC_P 16'd27
`define VSYNC_Q 16'd627
`define VSYNC_R 16'd628
//颜色定义
`define RED 8'hE0 //1110_0000(参照上面电路图)
`define GREEN 8'h1C //0001_1100(参照上面电路图)
`define BLUE 8'h03 //0000_0011(参照上面电路图)
`define YELLOW 8'hFC
`define BLACK 8'h00
module VGA
(
//输入
input CLK_50M,
input RST_N,
//输出
output reg VSYNC, //垂直同步端口
output reg HSYNC, //水平同步端口
output reg[7:0] VGA_DATA //数据端口
);
reg[15:0] hsync_cnt; //水平扫描计数器
reg[15:0] vsync_cnt; //垂直扫描计数器
reg vga_data_valid; //RGB数据信号有效区使能信号
//水平扫描(扫描1056个点)
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
hsync_cnt <= 16'd0;
else if(hsync_cnt == `HSYNC_D)
hsync_cnt <= 16'd0;
else
hsync_cnt <= hsync_cnt + 16'd1;
end
//垂直扫描(扫描628个点)
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
vsync_cnt <= 16'd0;
else if((vsync_cnt == `VSYNC_R) && (hsync_cnt == `HSYNC_D))
vsync_cnt <= 16'd0;
else if(hsync_cnt == `HSYNC_D)
vsync_cnt <= vsync_cnt + 16'd1;
else
vsync_cnt <= vsync_cnt;
end
//行时序
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
HSYNC <= 1'b0;
else if(hsync_cnt < `HSYNC_A) //a域为0
HSYNC <= 1'b0;
else
HSYNC <= 1'b1; //其他域为1
end
//列时序
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
VSYNC <= 1'b0;
else if(vsync_cnt < `VSYNC_O) //o域为0
VSYNC <= 1'b0;
else
VSYNC <= 1'b1; //其他域为1
end
//提取显示有效区(q域+c域)
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
vga_data_valid <= 1'b0;
else if((hsync_cnt > `HSYNC_B && hsync_cnt < `HSYNC_C) && (vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)) //数据有效区
vga_data_valid <= 1'b1;
else
vga_data_valid <= 1'b0;
end
//在数据有效区,在将数据送至VGA_RED、VGA_GREEN、VGA_BLUE数据引脚
always@(*)
begin
if(vga_data_valid)
begin
if(vsync_cnt >`VSYNC_P)//显示区
begin
if((hsync_cnt > `HSYNC_B) && (hsync_cnt < `HSYNC_B+10'd300))
VGA_DATA <= `RED; //红色 1110_0000
else if((hsync_cnt > `HSYNC_B+10'd300) && (hsync_cnt < `HSYNC_B+10'd400))
VGA_DATA <= `BLUE; //蓝色 0000_0111
else if((hsync_cnt > `HSYNC_B+10'd400) && (hsync_cnt < `HSYNC_B+10'd500))
VGA_DATA <= `YELLOW; //黄色 1111_1100
else if((hsync_cnt > `HSYNC_B+10'd500) && (hsync_cnt < `HSYNC_B+10'd800))
VGA_DATA <= `GREEN; //绿色 0001_1100
else
VGA_DATA <= `BLACK; //黑色 0000_0000
end
else
VGA_DATA <= `BLACK; //黑色
end
else
VGA_DATA <= `BLACK; //黑色
end
//PLL_IP获取40M时钟
wire CLK_40M;
PLL PLL_inst (
.inclk0 ( CLK_50M ),
.c0 ( CLK_40M )
);
玩过LCD、OLED屏幕的都知道显示字符、数字、汉字,首先要对其取模。我们借助的工具为PCtoLCD2002软件进行字模的提取。
取模格式设置:
生成的数组为:
{
0x00,0x00,0x1F,0x98,0x10,0x84,0x11,0x04,0x11,0x04,0x10,0x88,0x10,0x70,0x00,0x00},/*"5",0*/
{
0x10,0x00,0x1E,0x00,0x11,0xE0,0x00,0x1C,0x00,0x70,0x13,0x80,0x1C,0x00,0x10,0x00},/*"V",1*/
字模是如何对应字符的:
例如:数字‘5’的字模数组为:
{
0x00,0x00,0x1F,0x98,0x10,0x84,0x11,0x04,0x11,0x04,0x10,0x88,0x10,0x70,0x00,0x00},/*"5",0*/
//第一列 第二例 第三列 第四列 第五列 第六列 第七列 第8列
{
0x00,0x00,0x1F,0x98,0x10,0x84,0x11,0x04,0x11,0x04,0x10,0x88,0x10,0x70,0x00,0x00},/*"5",0*/
//每列两个字节(16位),构成8*16大小的字符
将十六进制转换为二进制,并按行列录入至excel观察:
第二例:0x1F98转换为二进制=1111110011000,对应excel_B列,以此类推。
生成的字库放置在存储器里面才能被我们调用,在这里选择了将数据放置置ROM_IP核内。
.mif是配置存储器专用的文件格式,利用QuartusⅡ生成.mif文件:
并将数据录入至.mif文件中:
并将生成的.mif文件关联至创建的ROM IP核中。
创建的ROM IP核如下所示,输入为地址和时钟信号,输出为存储的数据,我们在使用数据时,在ROM的时钟沿给予ROM相应的地址即可在输出端口得到数据。
//行时序宏定义
`define HSYNC_A 16'd128
`define HSYNC_B 16'd216
`define HSYNC_C 16'd1016
`define HSYNC_D 16'd1056
//列时序宏定义
`define VSYNC_O 16'd4
`define VSYNC_P 16'd27
`define VSYNC_Q 16'd627
`define VSYNC_R 16'd628
//颜色定义
`define RED 8'hE0 //1110_0000(参照上面电路图)
`define GREEN 8'h1C //0001_1100(参照上面电路图)
`define BLUE 8'h03 //0000_0011(参照上面电路图)
`define YELLOW 8'hFC
`define BLACK 8'h00
module VGA
(
//输入
input CLK_50M,
input RST_N,
//输出
output reg VSYNC, //垂直同步端口
output reg HSYNC, //水平同步端口
output reg[7:0] VGA_DATA //数据端口
);
reg[15:0] hsync_cnt; //水平扫描计数器
reg[15:0] vsync_cnt; //垂直扫描计数器
reg vga_data_valid; //RGB数据信号有效区使能信号
//水平扫描
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
hsync_cnt <= 16'd0;
else if(hsync_cnt == `HSYNC_D)
hsync_cnt <= 16'd0;
else
hsync_cnt <= hsync_cnt + 16'd1;
end
//垂直扫描
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
vsync_cnt <= 16'd0;
else if((vsync_cnt == `VSYNC_R) && (hsync_cnt == `HSYNC_D))
vsync_cnt <= 16'd0;
else if(hsync_cnt == `HSYNC_D)
vsync_cnt <= vsync_cnt + 16'd1;
else
vsync_cnt <= vsync_cnt;
end
//行时序
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
HSYNC <= 1'b0;
else if(hsync_cnt < `HSYNC_A)
HSYNC <= 1'b0;
else
HSYNC <= 1'b1;
end
//列时序
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
VSYNC <= 1'b0;
else if(vsync_cnt < `VSYNC_O)
VSYNC <= 1'b0;
else
VSYNC <= 1'b1;
end
//提取显示有效区
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
vga_data_valid <= 1'b0;
else if((hsync_cnt > `HSYNC_B && hsync_cnt < `HSYNC_C) && (vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)) //数据有效区
vga_data_valid <= 1'b1;
else
vga_data_valid <= 1'b0;
end
//显示字符
reg [7:0] rom_add;
wire [15:0] rom_data;
wire [15:0] vga_x;
wire [15:0] vga_y;
assign vga_x = hsync_cnt - `HSYNC_B;
assign vga_y = vsync_cnt - `VSYNC_P;
assign display_en = (vga_x >= 10'd98) && (vga_x <= 10'd116) && (vga_y >= 10'd90) && (vga_y <= 10'd106);//开窗
//读取字库
always@(posedge CLK_40M or negedge RST_N)
begin
if(!RST_N)
rom_add <= 8'h0;
else if(display_en)
begin
if(vga_x == 10'd98) //5 x坐标
rom_add <= 8'h0; //看.mif文件中的地址
else if(vga_x == 10'd106) //v x坐标
rom_add <= 8'h08; //看.mif文件中的地址
else
rom_add <= rom_add + 1'b1;
end
else
rom_add <= 8'h0;
end
//显示
always@(*)
begin
if(display_en)
begin
if(rom_data[10'd106 - vga_y]) //从下往上刷(rom_data元素为16的列向量)
VGA_DATA <= `RED; //红色
else
VGA_DATA <= `YELLOW; //黑色
end
else
VGA_DATA <= `BLACK; //黑色
end
//ROM_IP 存放字库数据
ROM ROM_inst (
.address ( rom_add ),
.clock ( CLK_40M ),
.q ( rom_data )
);
//PLL_IP生成VGA需要的40M时钟
wire CLK_40M;
PLL PLL_inst (
.inclk0 ( CLK_50M ),
.c0 ( CLK_40M )
);
endmodule