目录
一、VGA 协议
VGA(视频图像阵列 Video Graphics Array)是一种常见的视频显示标准,其核心是通过时序信号控制显示器的扫描过程。VGA时序分为 水平时序 和 垂直时序,分别控制每行和每帧的扫描。其主要信号线为:R、G、B信号、 HSYNC(行同步信号) 、 VSYNC(场同步信号)、GND
需要注意三原颜色的输入为模拟信号,而FPGA输出的信号为数字信号,所以要先驱动VGA必须进行数模转换,可以用DAC或者权电阻电路进行数模转换,我的开发板恰好设计右有权电阻电路,依次直接将数字信号输出到对应引脚即可(由于开发板电路限制,我RGB可输出位宽各4位,即RGB444的标准,精度不达RGB555或RGB565标准,因此输出数据若采用例如RGB565格式需对数据进行取舍,将高四位输出并舍去低位。不过后面我设计VGA控制器时仍然输出16位RGB数据,并在顶层模块进行数据取舍)。

(1)同步时序
行同步时序控制每一行的扫描,HSYNC每产生一个脉冲表示新一行数据开始传输,而行同步时序包括以下部分:
同步脉冲(Sync Pulse) | 同步信号,告诉显示器开始扫描新一行(时序图中为负极性脉冲) |
后沿(Back Porch) | 同步脉冲结束后的一段空白时间 |
左边界(Left Border) | 位于显示区域的左侧,是一段空白区域 |
显示区域(Visible Area) | 实际显示的像素区域(例如640像素) |
右边界(Right Border) | 位于显示区域的右侧,是一段空白区域 |
前沿(Front Porch) | 显示区域结束后的一段空白时间 |
水平总周期 = 同步脉冲 + 前沿 + 左边界 + 显示区域 + 右边界 + 后沿 |

场同步时序控制每一帧的扫描,VSYNC每产生一个脉冲表示新一帧数据开始传输(所有行传输完成表示传输完一帧,下一帧又从第一行开始),其同步时序与行同步一致,不再赘述。

(2)时序参数标准
显示模式对应各自的标准,例如分辨率640x480 刷新率60Hz 要求时钟为 25.175MHZ:水平总周期*垂直总周期*刷新率=800*525*60Hz=25.2MHz ,这个频率非常重要,频率不准会导致图像同步失败导致显示出错,所以最好用稳定准确的时钟源,可以用IP核生成相应频率的时钟(亲身体会,做图像显示时各种出错,最后发现不是代码问题而是时钟频率选的25而不是25.2MHz,差了0.2导致结果出错)。
对于 640x480@60Hz,其同步脉冲持续96个时钟周期,有效图像区域为640个时钟周期,有效图像期间每个时钟周期会扫描一个像素点数据,一行640个像素刚好640个时钟周期扫描完成。
在设计VGA控制器时一定需要根据输出情况进行参数设置。
二、彩条显示
(1)设计要求
1. VGA驱动液晶显示屏,要求分辨率640*480,刷新率60Hz
2. 在屏幕上竖向显示彩条(红 橙 黄 绿 青 蓝 紫 黑 白 灰)
(2)模块设计
1.同步信号的产生时机:可以分别通过两个计数器 h_cnt、v_cnt 进行实现。水平计数器 h_cnt 每个clk时钟就加 1,计满水平总周期后置0重新开始计数,通过判断其值对水平同步信号进行控制;需要注意垂直计数器 v_cnt 每当 h_cnt 记满一轮再加 1,即表示一行数据扫描完成后再开始下一行。
2.像素坐标换算:通过一个data_valid信号判断是否属于有效数据区间,如果data_valid为1则进行坐标换算,x*y范围即为输出分辨率范围640*480。
`timescale 1ns / 1ps
module VGA_ctrl(
input wire clk, //25MHz(根据显示模式大致计算:如640*480@60Hz:水平总周期*垂直总周期*刷新率=800*525*60Hz=25.2MHz)
input wire rst_n, //复位
output wire hsync, //行同步信号
output wire vsync, //场同步信号
output reg [15:0] RGB //RGB565格式数据(Red[15:11] Green[10:5] Blue[4:0])
);
reg [11:0] h_cnt; //水平计数器(行:对应像素点的行位置x)
reg [11:0] v_cnt; //垂直计数器(场:对应像素点的列位置y)
wire data_valid; //数据有效信号(有效数据期间为1)
wire [11:0] pix_x; //像素点x坐标(左上角为原点)
wire [11:0] pix_y; //像素点y坐标
//VGA时序参数(代表各区间持续的时钟周期个数,根据输出分辨率进行设置)
parameter H_SYNC = 12'd96, //水平同步脉冲时间
H_LEFT = 12'd8, //水平左边时间
H_BACK = 12'd40, //水平后沿时间
H_DATA = 12'd640, //水平数据输出时间
H_RIGHT = 12'd8, //水平右边时间
H_FRONT = 12'd8, //水平前沿时间
H_TOTAL = H_SYNC + H_BACK + H_LEFT + H_DATA + H_RIGHT + H_FRONT, //水平总计时间
V_SYNC = 12'd2, //垂直同步脉冲时间
V_LEFT = 12'd8, //垂直左边时间
V_BACK = 12'd25, //垂直后沿时间时间
V_RIGHT = 12'd8, //垂直右边时间
V_DATA = 12'd480, //垂直数据输出
V_FRONT = 12'd2, //垂直前沿时间
V_TOTAL = V_SYNC + V_BACK + V_LEFT + V_DATA + V_RIGHT + V_FRONT; //垂直总计时间
//颜色参数 RGB565格式
localparam RED = 16'hF800,//红
ORANGE = 16'hFC00,//橙
YELLOW = 16'hFFE0,//黄
GREEN = 16'h07E0,//绿
CYAN = 16'h07FF,//青
BLUE = 16'h001F,//蓝
PURPPLE = 16'hF81F,//紫
BLACK = 16'h0000,//黑
WHITE = 16'hFFFF,//白
GRAY = 16'hD69A;//灰
//**********************计数器**********************//
//水平计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
h_cnt <= 12'd0;
else if (h_cnt == H_TOTAL-1)
h_cnt <= 12'd0;
else
h_cnt <= h_cnt + 12'd1;
end
//垂直计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
v_cnt <= 12'd0;
else if (v_cnt == V_TOTAL-1) //一帧结束
v_cnt <= 12'd0;
else if (h_cnt == H_TOTAL-1) //递增条件是一行结束
v_cnt <= v_cnt + 12'd1;
end
//*******************生成同步信号*******************//
assign hsync = (h_cnt < H_SYNC) ? 1'b0 : 1'b1;//输出负脉冲‾\_/‾‾‾‾‾‾‾‾‾‾‾‾\_/‾
assign vsync = (v_cnt < V_SYNC) ? 1'b0 : 1'b1;//输出负脉冲
//assign hsync = (h_cnt < H_SYNC) ? 1'b1 : 1'b0;//输出正脉冲_/‾\____________/‾\_
//assign vsync = (v_cnt < V_SYNC) ? 1'b1 : 1'b0;//输出正脉冲
//*******************换算横纵坐标*******************//
assign data_valid = (h_cnt >= H_SYNC + H_BACK + H_LEFT -1) &&
(v_cnt >= V_SYNC + V_BACK + V_LEFT -1) &&
(h_cnt <= H_SYNC + H_BACK + H_LEFT + H_DATA -1) &&
(v_cnt <= V_SYNC + V_BACK + V_LEFT + V_DATA -1); //水平垂直计数器均处于有效数据区间
assign pix_x = (data_valid) ? (h_cnt - (H_SYNC + H_BACK + H_LEFT -1)) : 12'hFFF; //有效坐标范围为0 ~ H_DATA-1
assign pix_y = (data_valid) ? (v_cnt - (V_SYNC + V_BACK + V_LEFT -1)) : 12'hFFF; //有效坐标范围为0 ~ V_DATA-1
//*******************输出像素数据*******************//
// 生成图像数据
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
RGB <= BLACK;
else if (pix_x <= H_DATA/10*1 -1)//红
RGB <= RED;
else if (pix_x <= H_DATA/10*2 -1)//橙
RGB <= ORANGE;
else if (pix_x <= H_DATA/10*3 -1)//黄
RGB <= YELLOW;
else if (pix_x <= H_DATA/10*4 -1)//绿
RGB <= GREEN;
else if (pix_x <= H_DATA/10*5 -1)//青
RGB <= CYAN;
else if (pix_x <= H_DATA/10*6 -1)//蓝
RGB <= BLUE;
else if (pix_x <= H_DATA/10*7 -1)//紫
RGB <= PURPPLE;
else if (pix_x <= H_DATA/10*8 -1)//黑
RGB <= BLACK;
else if (pix_x <= H_DATA/10*9 -1)//白
RGB <= WHITE;
else if (pix_x <= H_DATA/10*10-1)//灰
RGB <= GRAY;
else
RGB <= BLACK;
end
endmodule
(3)顶层模块
前面提到了我的开发板全电阻网络支持RGB各4位的输出方式,因此需要在顶层模块中对RGB565格式数据进行处理,我这里做了输出三原色高四位的操作,此外还添加了一个时钟IP将FPGA的100MHz系统时钟分频产生一个25.2MHz的时钟供VGA控制器使用。
`timescale 1ns / 1ps
module VGA_top(
input wire clk, //系统时钟100MHz
input wire rst_n, //复位
output wire hsync, //行同步信号
output wire vsync, //场同步信号
output wire[3:0] red, //红色像素分量
output wire[3:0] green, //绿色像素分量
output wire[3:0] blue //蓝色像素分量
);
wire clk_div; //25.2MHz分频时钟线
wire [15:0] RGB; //RGB565格式数据(Red[15:11] Green[10:5] Blue[4:0])
assign red = RGB[15:12]; //截取红色分量高4位
assign green = RGB[10:7]; //截取绿色分量高4位
assign blue = RGB[4:1]; //截取蓝色分量高4位
clk_wiz_0 clk_wiz_0(
.clk_in1 (clk),
.clk_out1 (clk_div)
);
VGA_ctrl VGA_ctrl( //VGA控制器
.clk (clk_div),
.rst_n (rst_n),
.hsync (hsync),
.vsync (vsync),
.RGB (RGB)
);
endmodule
(4)效果
将屏幕纵向划分为10等分,依次显示:红橙黄绿青蓝紫黑白灰。
二、静态图像显示
(1)设计要求
1. VGA驱动液晶显示屏,要求分辨率 640x480,刷新率 60Hz
2. 在屏幕上部分区域显示一个大小 256*160 的图像
3. 图像数据通过初始化存储在RAM中,VGA控制器从RAM读取数据并输出
(2)图像数据
图像数据是以RGB565格式输出给显示屏,所以需要将显示的图片转换为RGB565格式,我这里用到了 BMP2Mif 这么一个小工具,可以将.bmp格式的位图转换为RGB565格式数据。如图所示按以下步骤进行:
将待显示的图片通过电脑画图工具打开,锁定比例重设大小,我这里缩小为256*160,要缩小的原因是因为图像存在 RAM中,而FPGA 的 Block RAM资源有限,使用RAM时需要保证能够存下一整张图片。
另存为BMP格式的位图
用 BMP2Mif 将.bmp格式的位图转换为RGB565格式数据,并以Coe文件类型保存,可供 RAM 进行数据初始化。
转换得到的COE文件如下(图像数据一共 40960个 16位数据):
RAM IP核选16位地址对应深度65535(用了板上30个Block RAM资源,图像数据一共 40960个 16位数据,可以全部存进去)。注意我选的RAM类型为 Simple Dual Port RAM 即双端口RAM,A端口写数据,B端口读数据,但是实际我只用了B端口读数据,A端口不用。
初始化文件选择之前生成的.coe文件
(3)模块设计
VGA控制器代码时序控制的部分没有变化,主要是添加输入数据和输出地址端口,并修改输出像素数据部分的代码逻辑,关键是保证读数据的地址只在要显示区域进行自增,同时数据也需要在该区域进行更新:
`timescale 1ns / 1ps
module VGA_ctrl(
input wire clk, //25MHz(根据显示模式大致计算:如640*480@60Hz:水平总周期*垂直总周期*刷新率=800*525*60Hz=25.2MHz)
input wire rst_n, //复位
input wire [15:0]data_in, //数据输入(数据从RAM读取)
output wire hsync, //行同步信号
output wire vsync, //场同步信号
output reg [15:0]RGB, //RGB565格式数据(Red[15:11] Green[10:5] Blue[4:0])
output reg [15:0]data_addr //数据地址(RAM数据地址)
);
reg [11:0] h_cnt; //水平计数器(行:对应像素点的行位置x)
reg [11:0] v_cnt; //垂直计数器(场:对应像素点的列位置y)
wire data_valid; //数据有效信号(有效数据期间为1)
wire [11:0] pix_x; //像素点x坐标(左上角为原点)
wire [11:0] pix_y; //像素点y坐标
//VGA时序参数(代表各区间持续的时钟周期个数,根据输出分辨率进行设置)
parameter H_SYNC = 12'd96, //水平同步脉冲时间
H_BACK = 12'd40, //水平后沿时间
H_LEFT = 12'd8, //水平左边时间
H_DATA = 12'd640, //水平数据输出时间
H_RIGHT = 12'd8, //水平右边时间
H_FRONT = 12'd8, //水平前沿时间
H_TOTAL = H_SYNC + H_BACK + H_LEFT + H_DATA + H_RIGHT + H_FRONT, //水平总计时间
V_SYNC = 12'd2, //垂直同步脉冲时间
V_BACK = 12'd25, //垂直后沿时间时间
V_LEFT = 12'd8, //垂直左边时间
V_DATA = 12'd480, //垂直数据输出
V_RIGHT = 12'd8, //垂直右边时间
V_FRONT = 12'd2, //垂直前沿时间
V_TOTAL = V_SYNC + V_BACK + V_LEFT + V_DATA + V_RIGHT + V_FRONT; //垂直总计时间
//颜色参数 RGB565格式
localparam RED = 16'hF800,//红
ORANGE = 16'hFC00,//橙
YELLOW = 16'hFFE0,//黄
GREEN = 16'h07E0,//绿
CYAN = 16'h07FF,//青
BLUE = 16'h001F,//蓝
PURPPLE = 16'hF81F,//紫
BLACK = 16'h0000,//黑
WHITE = 16'hFFFF,//白
GRAY = 16'hD69A;//灰
//**********************计数器**********************//
//水平计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
h_cnt <= 12'd0;
else if (h_cnt == H_TOTAL-1) //一行结束
h_cnt <= 12'd0;
else
h_cnt <= h_cnt + 12'd1;
end
//垂直计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
v_cnt <= 12'd0;
else if (v_cnt == V_TOTAL-1) //一帧结束
v_cnt <= 12'd0;
else if (h_cnt == H_TOTAL-1) //递增条件是一行结束
v_cnt <= v_cnt + 12'd1;
end
//*******************生成同步信号*******************//
assign hsync = (h_cnt < H_SYNC) ? 1'b0 : 1'b1;//输出负脉冲‾\_/‾‾‾‾‾‾‾‾‾‾‾‾\_/‾
assign vsync = (v_cnt < V_SYNC) ? 1'b0 : 1'b1;//输出负脉冲
//assign hsync = (h_cnt < H_SYNC) ? 1'b1 : 1'b0;//输出正脉冲_/‾\____________/‾\_
//assign vsync = (v_cnt < V_SYNC) ? 1'b1 : 1'b0;//输出正脉冲
//*******************换算横纵坐标*******************//
assign data_valid = (h_cnt >= H_SYNC + H_BACK + H_LEFT -1) &&
(v_cnt >= V_SYNC + V_BACK + V_LEFT -1) &&
(h_cnt <= H_SYNC + H_BACK + H_LEFT + H_DATA -1) &&
(v_cnt <= V_SYNC + V_BACK + V_LEFT + V_DATA -1); //水平垂直计数器均处于有效数据区间
assign pix_x = (data_valid) ? (h_cnt - (H_SYNC + H_BACK + H_LEFT -1)) : 12'hFFF; //有效坐标范围为0 ~ H_DATA-1
assign pix_y = (data_valid) ? (v_cnt - (V_SYNC + V_BACK + V_LEFT -1)) : 12'hFFF; //有效坐标范围为0 ~ V_DATA-1
//*******************输出像素数据*******************//
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
RGB <= BLACK;
else if (pix_x >= 192 && pix_x < 448 && pix_y >= 160 && pix_y < 320) //显示位置为屏幕中间256*160区域
RGB <= data_in;
else
RGB <= BLACK;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_addr <= 16'h0;
else if (v_cnt == V_TOTAL-1) //一帧结束,从头开始
data_addr <= 16'h0;
else if (pix_x >= 192 && pix_x < 448 && pix_y >= 160 && pix_y < 320)
data_addr <= data_addr + 16'h1;
end
endmodule
(4)顶层模块
例化三个模块:时钟模块、VGA控制器、RAM模块。同时RGB数据输出仍然是截取高位。
`timescale 1ns / 1ps
module VGA_RAM_test_top(
input wire clk, //系统时钟100MHz
input wire rst_n, //复位
output wire hsync, //行同步信号
output wire vsync, //场同步信号
output wire[3:0] red, //红色像素分量
output wire[3:0] green, //绿色像素分量
output wire[3:0] blue //蓝色像素分量
);
wire clk_div; //25.2MHz分频时钟线
wire [15:0] ram_addr; //ram数据地址线
wire [15:0] ram_data; //ram读出数据线
wire [15:0] RGB; //RGB565格式数据(Red[15:11] Green[10:5] Blue[4:0])
assign red = RGB[15:12]; //截取红色分量高4位
assign green = RGB[10:7]; //截取绿色分量高4位
assign blue = RGB[4:1]; //截取蓝色分量高4位
clk_wiz_0 clk_wiz_0(
.clk_out1 (clk_div),
.clk_in1 (clk)
);
VGA_ctrl VGA_ctrl( //VGA控制器
.clk (clk_div),
.rst_n (rst_n),
.data_addr (ram_addr),
.data_in (ram_data),
.hsync (hsync),
.vsync (vsync),
.RGB (RGB)
);
blk_mem_gen_0 blk_mem_gen_0 (
.clka (0),
.wea (0),
.addra (0),
.dina (0),
.clkb (clk_div),
.addrb (ram_addr),
.doutb (ram_data)
);
endmodule