基于FPGA的VGA显示综合实验
对前一阶段的VGA显示实验做一个小小的学习总结,具体的实验内容包括:VGA 彩条显示实验、VGA方块移动实验(由此联想到是否可以实现贪吃蛇游戏的设计)、VGA字符显示实验、VGA图片显示实验。
1. VGA 显示原理
- 图示
文章图片
如图所示为RGB-565的VGA接口的实物图,下面介绍一下VGA各个接口的定义。
- 接口定义
文章图片
16个接口中,常用的接口分别是1、2、3、13(行同步)、14(场同步)这五个接口。
- 原理图
众所周知,任何一种颜色,都可以用红、绿、蓝三种元素表示出来,VGA显示便是利用了这一巧妙的原理。
通常情况下,可以使用ADV7123芯片进行数模转换,但在这里,本实验使用了更为简便的电阻权值匹配网络来进行数模转换。图(b)简单讲解了2位权值网络,根据不同的{D1,D0}组合(数字信号),得到不同的电流输出值(介于0-3(Vcc/2R)之间),同理,图(a)中红、绿、蓝各由5位、6位、5位信号来控制(故得名RGB-565)。
同理,可以计算推导,得到红、绿、蓝输出的电流介于0-0.714V之间,例如,当D11-D15全为1时(高电平),RED输出0.714v这时的红色分量占比重最大。
文章图片
- VGA的时序
在说VGA时序之前,先讲一下VGA显示是怎么实现的。以你面前的电脑屏幕为例,在显示时,从电脑屏幕的最左上角开始,进行一行扫描,在扫描的过程中给这一行的每一个像素点赋值(颜色),扫描完一行之后,接着会到下一行的起始位置,再次开始扫描和赋值,直到扫描到电脑屏幕最右下角为止,此时称完成了一帧图像的扫描。每扫描完一行称为行扫描周期,每扫描完一帧图像称为一个场扫描周期。 在行扫描周期中,用行同步时序进行同步,在场扫描周期中用场同步时序进行同步。
文章图片
一个行扫描周期分为a b c d四个阶段,同步和显示前沿均为准备阶段,数据线上不需要有任何数据,有效数据段为显示一行图像的数据,显示后沿也是数据线上无需传输数据。场扫描时序与之类似,不再做具体说明。
文章图片
下面来看一下啊常用的分辨率,我们以640x480@60为例,做一下讲解
文章图片
640表示有640基准时钟周期数(像素数),480表示一帧有480行 ,60表示每秒钟刷新60帧。注意,640不等于行同步周期,因为一个行同步周期还要加上同步、显示前沿、显示后沿,场同步周期同理。
因此,在计算时钟频率时应该使用表达式800x525x60=25.2MHZ这与表格中的时钟频率非常接近。至于为什么要使用25.175MHZ是因为时钟在取用时,有一定的限制很难精确取到25.2MHZ,这里不做深入讲解。
通过FPGA开发板上的VGA接口与电脑VGA接口连接,显示的实验效果如下。
文章图片
- 程序框图和顶层模块图
文章图片
文章图片
实验中,时钟分频模块通过PPL锁相环进行时钟的分频,采用Quartus ii自带的ip核。
VGA驱动模块与显示器相连接(输出有效信息:行同步信号、场同步信号、颜色信息)
```verilog代码块
//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;
//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;
代码块中,设置了一个行技术器cnt_h,场计数器cnt_v,用于时钟计数,另外在模块内部还定义了像素点颜色输入使能和RGB565数据输出使能信号,这一块值得借鉴和学习。这个体现了VGA驱动模块的实质功能:提供数据输出,提供像素点坐标。
//行计数器对像素时钟计数
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
【基于FPGA的VGA显示综合实验】上面时计数器的代码块,没啥好说的,老常规了。
VGA显示模块根据VGA驱动模块给出的像素点坐标,给坐标赋值。本实验要求为将屏幕分成五个不同颜色的竖直区域(见实验效果图)
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
pixel_data <= 16'd0;
else begin
if((pixel_xpos >= 0) && (pixel_xpos <= (H_DISP/5)*1))
pixel_data <= WHITE;
else if((pixel_xpos >= (H_DISP/5)*1) && (pixel_xpos < (H_DISP/5)*2))
pixel_data <= BLACK;
else if((pixel_xpos >= (H_DISP/5)*2) && (pixel_xpos < (H_DISP/5)*3))
pixel_data <= RED;
else if((pixel_xpos >= (H_DISP/5)*3) && (pixel_xpos < (H_DISP/5)*4))
pixel_data <= GREEN;
else
pixel_data <= BLUE;
end
end
各个颜色的RGB565数据代码如下
localparam WHITE= 16'b11111_111111_11111;
//RGB565 白色
localparam BLACK= 16'b00000_000000_00000;
//RGB565 黑色
localparam RED= 16'b11111_000000_00000;
//RGB565 红色
localparam GREEN= 16'b00000_111111_00000;
//RGB565 绿色
localparam BLUE= 16'b00000_000000_11111;
//RGB565 蓝色
3.VGA方块移动实验
文章图片
时钟分频模块和VGA驱动模块同上一个实验,下面来学习一下具体的VGA显示模块Verilog代码块内容。
//reg define
reg [ 9:0] block_x;
//方块左上角横坐标
reg [ 9:0] block_y;
//方块左上角纵坐标
reg [21:0] div_cnt;
//时钟分频计数器
regh_direct;
//方块水平移动方向,1:右移,0:左移
regv_direct;
//方块竖直移动方向,1:向下,0:向上
这个是模块内部定义的参数,方块左上角的横纵坐标表示方块的位置,时钟分频计数器用于控制方块每次移动的的间隔时间,例如,需要每10ms移动一次,则需要计数250000次(时钟频率为25Mhz),h_direct和v_direct用于控制移动的水平和竖直的方向。
//当方块移动到边界时,改变移动方向
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
h_direct <= 1'b1;
//方块初始水平向右移动
v_direct <= 1'b1;
//方块初始竖直向下移动
end
else begin
if(block_x == SIDE_W - 1'b1)//到达左边界时,水平向右
h_direct <= 1'b1;
else//到达右边界时,水平向左
if(block_x == H_DISP - SIDE_W - BLOCK_W)
h_direct <= 1'b0;
else
h_direct <= h_direct;
if(block_y == SIDE_W - 1'b1)//到达上边界时,竖直向下
v_direct <= 1'b1;
else//到达下边界时,竖直向上
if(block_y == V_DISP - SIDE_W - BLOCK_W)
v_direct <= 1'b0;
else
v_direct <= v_direct;
end
end
根据左上角坐标的不同值,判断是否到达边界,当没有在边界时保持原方向
//根据方块移动方向,改变其纵横坐标
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
block_x <= 22'd100;
//方块初始位置横坐标
block_y <= 22'd100;
//方块初始位置纵坐标
end
else if(move_en) begin
if(h_direct)
block_x <= block_x + 1'b1;
//方块向右移动
else
block_x <= block_x - 1'b1;
//方块向左移动if(v_direct)
block_y <= block_y + 1'b1;
//方块向下移动
else
block_y <= block_y - 1'b1;
//方块向上移动
end
else begin
block_x <= block_x;
block_y <= block_y;
end
end
根据方块移动的方向,改变横纵坐标的值。
//给不同的区域绘制不同的颜色
always @(posedge vga_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
pixel_data <= BLACK;
else begin
if((pixel_xpos < SIDE_W) || (pixel_xpos >= H_DISP - SIDE_W)
|| (pixel_ypos < SIDE_W) || (pixel_ypos >= V_DISP - SIDE_W))
pixel_data <= BLUE;
//绘制边框为蓝色
else
if((pixel_xpos >= block_x) && (pixel_xpos < block_x + BLOCK_W)
&& (pixel_ypos >= block_y) && (pixel_ypos < block_y + BLOCK_W))
pixel_data <= BLACK;
//绘制方块为黑色
else
pixel_data <= WHITE;
//绘制背景为白色
end
end
依据坐标,给不同的区域绘制不同的颜色,方块黑色,边框蓝色,背景为白色,如实验效果图所示。
以上便是VGA显示模块的的具体代码内容,本实验设计的巧妙之处便是用方块左上角一个点的坐标来追踪方块的移动,这种转化的思想值得借鉴。考虑是否可以由按键来控制h_direct和v_direct(本实验这两者一开始是默认了向右和向下的方向,由碰到边界来改变方向),只需要增加一些条件即可,这与贪吃蛇的游戏风格有点相似。
4.VGA图片显示实验
本实验是基于ROM(只读存储器)结构的,需要将图片转换为.mif文件,mif文件就是存储器初始化文件,即memory initialization file,用来配置RAM或ROM中的数据。
- 下面介绍两种产生.mif文件的方法。
①利用Quartus自带的mif编辑器生成,其优点是对于小容量RAM可以快速方便的完成mif文件的编辑工作,不需要第三方软件的编辑,缺点是一旦数据量过大,必须要有充分的耐心。如图,根据需要填写每一个点的值
文章图片
②使用.mif转换器(有需要的可以私信我)
文章图片
- 下面来看一下本次实验的系统框图和顶层模块
文章图片
文章图片
可以看到本实验比前两次实验多了一个ROM模块,此模块为VGA显示模块内部定义的模块,ROM由Quartus自带的ip核来给出来,VGA显示模块根据VGA驱动模块提供的像素点坐标,从ROM中读出图片数据,并将图片数据输出给VGA驱动模块。下面介绍一下单端口ROM ip核的使用。
clock为时钟信号、rden为读使能信号(高有效)、address为地址信号,根据给出的地址,输出相应的数据,如果我们需要连续读取ROM内容,可使rden一直拉高。
- 代码实现
与前两个模块不同的是VGA显示模块,下面只介绍一下显示模块的代码
//从ROM中读出的图像数据有效时,将其输出显示
assign pixel_data = https://www.it610.com/article/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;
这是给输出的数据和rom读使能信号赋值,采用assign语句
//控制读地址
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
这两个always块是进行读地址控制和读使能信号的延迟(这个延迟可以自己细品一下),读控制中的total表示图片总的像素点(自己在转换器中设置),注意像素点的内存不能超过FPGA自带的内存量。以我自己的为例,我的是270kb的内存,那我可以选择100x100x16=160000bit<270x1024bit
文章图片
pic_rom pic_rom_inst(
.clock(vga_clk),
.address (rom_addr),
.rden(rom_rd_en),
.q(rom_data)
);
最后这个是ip核的例化
文章图片
在选择ip核时,在File name中添加自己转换的.mif文件即可。
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量