以下内容来自正点原子的FPGA开发指南,觉得讲的不错,就搬了过来。
一、VGA简介
传送门:VGA介绍
二、实验内容
使用开拓者开发板上的VGA接口在显示器的屏幕中心位置显示彩色图片。显示分辨率为640*480,刷新速率为60hz,图片的大小为100 * 100。
三、程序设计
图 21.4.1是根据本章实验任务画出的系统框图。其中,时钟分频模块负责产生像素时钟,VGA驱动模块产生行场同步信号及像素点的纵横坐标,VGA显示模块输出图像数据,ROM用于存 储需要显示的图片。
VGA显示模块中的ROM是通过例化IP核来实现的只读存储器,它使用FPGA的片上存储资源。我们需要使用保存有图片数据的mif文件来初始化ROM IP核,不过存储的不是原始数据,而是由RGB转换成的RGB565格式的数据,也即一个像素不再对应3个8位数据,而是对应一个16位的数据。
由系统框图可知,FPGA部分包括四个模块:
- 顶层模块(vga_colorbar)
- 时钟分频模块(vga_pll)
- VGA显示模块(vga_display)
- VGA驱动模块(vga_driver)。
其中在顶层模块中完成对另外三个模块的例化。
各模块端口及信号连接如图 21.4.5所示:
时钟分频模块(vga_pll)通过调用锁相环(PLL)IP核来实现。根据实验任务要求的分辨率及刷新速率,本次实验中VGA显示用到的像素时钟为25.175Mhz,因为分辨率不是很高,我们可以设置锁相环IP核让其输出25Mhz的时钟作为像素时钟。
VGA驱动模块(vga_driver)在像素时钟的驱动下,根据VGA时序的参数输出行同步(vga_hs)、场同步(vga_vs)信号。同时VGA驱动模块还需要输出像素点的纵横坐标,供VGA显示模块(vga_display)调用,以绘制图片。
顶层模块的代码如下:
module vga_rom_pic(
input sys_clk, //系统时钟
input sys_rst_n, //复位信号
//VGA接口
output vga_hs, //行同步信号
output vga_vs, //场同步信号
output [15:0] vga_rgb //红绿蓝三原色输出
);
//wire define
wire vga_clk_w; //PLL分频得到25Mhz时钟
wire locked_w; //PLL输出稳定信号
wire rst_n_w; //内部复位信号
wire [15:0] pixel_data_w; //像素点数据
wire [ 9:0] pixel_xpos_w; //像素点横坐标
wire [ 9:0] pixel_ypos_w; //像素点纵坐标
//*****************************************************
//** main code
//*****************************************************
//待PLL输出稳定之后,停止复位
assign rst_n_w = sys_rst_n && locked_w;
vga_pll u_vga_pll( //时钟分频模块
.inclk0 (sys_clk),
.areset (~sys_rst_n),
.c0 (vga_clk_w), //VGA时钟 25M
.locked (locked_w)
);
vga_driver u_vga_driver(
.vga_clk (vga_clk_w),
.sys_rst_n (rst_n_w),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.vga_rgb (vga_rgb),
.pixel_data (pixel_data_w),
.pixel_xpos (pixel_xpos_w),
.pixel_ypos (pixel_ypos_w)
);
vga_display u_vga_display(
.vga_clk (vga_clk_w),
.sys_rst_n (rst_n_w),
.pixel_xpos (pixel_xpos_w),
.pixel_ypos (pixel_ypos_w),
.pixel_data (pixel_data_w)
);
endmodule
顶层模块中主要完成对其余模块的例化,需要注意的是在利用IP核进行时钟分频时,系统上电复位后PLL输出的25Mhz时钟需要经过一段时间才能到达稳定状态。在PLL输出稳定后,标志信号locked拉高(第29行)。
由于VGA驱动模块及显示模块均由PLL输出的像素时钟驱动,因此在PLL输出稳定之前,其余模块应保持复位状态。如程序中第22行所示,通过将系统复位信号sys_rst_n和PLL输出稳定标志信号locked进行“与”操作,得到内部复位信号rst_n_w。将该信号作为VGA驱动模块及显示模块的复位信号,可避免由于系统复位后像素时钟不稳定造成的VGA时序错误。
VGA驱动模块的代码如下所示:
module vga_driver(
input vga_clk, //VGA驱动时钟
input sys_rst_n, //复位信号
//VGA接口
output vga_hs, //行同步信号
output vga_vs, //场同步信号
output [15:0] vga_rgb, //红绿蓝三原色输出
input [15:0] pixel_data, //像素点数据
output [ 9:0] pixel_xpos, //像素点横坐标
output [ 9:0] pixel_ypos //像素点纵坐标
);
//parameter define
parameter H_SYNC = 10'd96; //行同步
parameter H_BACK = 10'd48; //行显示后沿
parameter H_DISP = 10'd640; //行有效数据
parameter H_FRONT = 10'd16; //行显示前沿
parameter H_TOTAL = 10'd800; //行扫描周期
parameter V_SYNC = 10'd2; //场同步
parameter V_BACK = 10'd33; //场显示后沿
parameter V_DISP = 10'd480; //场有效数据
parameter V_FRONT = 10'd10; //场显示前沿
parameter V_TOTAL = 10'd525; //场扫描周期
//reg define
reg [9:0] cnt_h;
reg [9:0] cnt_v;
//wire define
wire vga_en;
wire data_req;
//*****************************************************
//** main code
//*****************************************************
//VGA行场同步信号
assign vga_hs = (cnt_h <= H_SYNC - 1'b1) ? 1'b0 : 1'b1;
assign vga_vs = (cnt_v <= V_SYNC - 1'b1) ? 1'b0 : 1'b1;
//使能RGB565数据输出
assign vga_en = (((cnt_h >= H_SYNC+H_BACK) && (cnt_h < H_SYNC+H_BACK+H_DISP))
&&((cnt_v >= V_SYNC+V_BACK) && (cnt_v < V_SYNC+V_BACK+V_DISP)))
? 1'b1 : 1'b0;
//RGB565数据输出
assign vga_rgb = vga_en ? pixel_data : 16'd0;
//提前一个周期请求像素点颜色数据输入
assign data_req = (((cnt_h >= H_SYNC+H_BACK-1'b1) && (cnt_h < H_SYNC+H_BACK+H_DISP-1'b1))
&& ((cnt_v >= V_SYNC+V_BACK) && (cnt_v < V_SYNC+V_BACK+V_DISP)))
? 1'b1 : 1'b0;
//像素点坐标
assign pixel_xpos = data_req ? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 10'd0;
assign pixel_ypos = data_req ? (cnt_v - (V_SYNC + V_BACK - 1'b1)) : 10'd0;
//行计数器对像素时钟计数
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
cnt_h <= 10'd0;
else begin
if(cnt_h < H_TOTAL - 1'b1)
cnt_h <= cnt_h + 1'b1;
else
cnt_h <= 10'd0;
end
end
//场计数器对行计数
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
cnt_v <= 10'd0;
else if(cnt_h == H_TOTAL - 1'b1) begin
if(cnt_v < V_TOTAL - 1'b1)
cnt_v <= cnt_v + 1'b1;
else
cnt_v <= 10'd0;
end
end
endmodule
程序中第14至25行通过变量声明定义了分辨率为640*480、刷新速率为60hz时VGA时序中的各个参数。
程序第59至69行通过行计数器cnt_h对像素时钟计数,计满一个行扫描周期后清零并重新开始计数。程序第71至81行通过场计数器cnt_v对行进行计数,即扫描完一行后cnt_v加1,计满一个场扫描周期后清零并重新开始计数。
将行场计数器的值与VGA时序中的参数作比较,我们就可以判断行场同步信号何时处于低电平同步状态,以及何时输出RGB565格式的图像数据(38~48行)。程序50至57行输出当前像素点的纵横坐标值,由于坐标输出后下一个时钟周期才能接收到像素点的颜色数据,因此数据请求信号data_req比数据输出使能信号vga_en提前一个时钟周期。
VGA显示模块的代码如下:
module vga_display(
input vga_clk, //VGA驱动时钟
input sys_rst_n, //复位信号
input [ 9:0] pixel_xpos, //像素点横坐标
input [ 9:0] pixel_ypos, //像素点纵坐标
output [15:0] pixel_data //像素点数据
);
//parameter define
parameter H_DISP = 10'd640; //分辨率——行
parameter V_DISP = 10'd480; //分辨率——列
localparam POS_X = 10'd270; //图片区域起始点横坐标
localparam POS_Y = 10'd190; //图片区域起始点纵坐标
localparam WIDTH = 10'd100; //图片区域宽度
localparam HEIGHT = 10'd100; //图片区域高度
localparam TOTAL = 14'd10000; //图案区域总像素数
localparam BLACK = 16'b00000_000000_00000; //屏幕背景色
//reg define
wire rom_rd_en; //读ROM使能信号
reg [13:0] rom_addr; //读ROM地址
reg rom_valid; //读ROM数据有效信号
//wire define
wire [15:0] rom_data; //ROM输出数据
//*****************************************************
//** main code
//*****************************************************
//从ROM中读出的图像数据有效时,将其输出显示
assign pixel_data = rom_valid ? rom_data : BLACK;
//当前像素点坐标位于图案显示区域内时,读ROM使能信号拉高
assign rom_rd_en = (pixel_xpos >= POS_X) && (pixel_xpos < POS_X + WIDTH)
&& (pixel_ypos >= POS_Y) && (pixel_ypos < POS_Y + HEIGHT)
? 1'b1 : 1'b0;
//控制读地址
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
rom_addr <= 14'd0;
end
else if(rom_rd_en) begin
if(rom_addr < TOTAL - 1'b1)
rom_addr <= rom_addr + 1'b1; //每次读ROM操作后,读地址加1
else
rom_addr <= 1'b0; //读到ROM末地址后,从首地址重新开始读操作
end
else
rom_addr <= rom_addr;
end
//从发出读使能到ROM输出有效数据存在一个时钟周期的延时
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
rom_valid <= 1'b0;
else
rom_valid <= rom_rd_en;
end
//通过调用IP核来例化ROM
pic_rom pic_rom_inst(
.clock (vga_clk),
.address (rom_addr),
.rden (rom_rd_en),
.q (rom_data)
);
endmodule
代码中14至19行声明了一系列的变量,方便大家修改图片的大小、在屏幕上显示的位置等,其中图片显示的位置由图片显示区域左上角的纵横坐标来指定。
由于图片存储在ROM中,因此VGA显示模块的主要任务就是控制ROM的读使能及读地址,从而在合适的时间段将ROM中的图片数据读出并显示。代码的36至39行判断当前像素点的纵横坐 标,当其位于图片显示区域时将ROM读使能信号rom_rd_en拉高。第41至54行在读操作过程中将读地址依次累加,从而将图片数据顺序读出;当读到未地址后读地址清零,重新从ROM中图像的第一个像素点数据开始读取。
读ROM的过程中,从发出读使能到ROM输出有效数据存在一个时钟周期的延时,因此ROM数 据有效信号rom_valid需要由rom_rd_en延迟一个时钟周期,如程序第56至62行所示。