一、 软件平台与硬件平台

  软件平台:

    1、操作系统:Windows-8.1

    2、开发套件:ISE14.7

    3、仿真工具:ModelSim-10.4-SE

  硬件平台:

    1、 FPGA型号:Xilinx公司的XC6SLX45-2CSG324

    2、 VGA接口

    3、 液晶显示器

二、 原理介绍

  VGA(Video Graphics Array)即视频图形阵列,是IBM在1987年推出的使用模拟信号的一种视频传输标准,在当时具有分辨率高、显示速率快、颜色丰富等优点,在彩色显示器领域得到了广泛的应用。这个标准对于现今的个人电脑市场已经十分过时。即使如此,VGA仍然是最多制造商所共同支持的一个标准,个人电脑在加载自己的特殊驱动程序之前,都必须支持VGA的标准。

VGA接口实物图如下图所示

  左边带针的叫VGA公头,右边带槽的叫VGA母头。

  VGA接口的特点:

  1、 VGA接口不支持热插拔

  VGA接口跟HDMI接口一样是不支持热插拔的。热插拔是指一般带电状态下对于接插件的插入或是拔除,并不只是针对有电源接口或者带供电的接口的接插件,而是所有。在运行状态时,插拔会产生耦合电流,电流不稳造成硬件烧坏,导致笔记本的接口端的保护受到冲击。就像U盘不能再一个时间段多次在一个端口插拔使用一样。各种电器的外露端子都会有金属的部分,它们都是要求接地的,但是不同的电器之间的地并不一定相同,比如一台DVD的地和一台电视机的接地都是相对于本身系统而言。

  当端子插入时,首先要建立共同的地来对传输的信号作参考,这就要依靠端子和传输线上的金属部分了,金属部分接地同时也是对信号的屏蔽和保护。两个地相接触一瞬间,会有很高的尖峰脉冲产生,这种脉冲如果不加以滤除可能会直达芯片并将其损坏。另外还有一种是ESD,即静电损坏,这种更难以避免,因为在电子产品上,只能去防护,ESD的持续时间会更短US级别。所以正规的电子产品对于金属端子的接地有比较高的要求,同时在信号线上增加ESD防护器件来避免热插拔的损坏。但实际上很多厂家为了节省成本而偷工减料,或者是对热插拔的防护意识不够导致设计不合理,使得用户会出现热插拔损坏电器的现象产生。

  2、  VGA不能传输音频

  因为视频是VGA信号,而音频信号不是,所以VGA不能传输音频,只能传输视频。相信这就是为什么这几年极度的需求创新转换器的原因。VGA不支持音频传输也是给很多消费者带来烦恼,这最好的办法其实就是购买一款转换器,VGA转HDMI或者HDMI转VGA,达到视频传输的同时还支持音频信号的输出,一举两得。但是不要只想着转换器的输入与输出成问题,同时想想音频输出口,3.5mm是音频输出信号的重要连接线。购买时可以考虑想转换器有没有带3.5mm的音频输出口,然后另外购买一条音频线。

  3、 VGA接口是一种D型接口,上面共有15针孔,分成三排,每排五个。 其中比较重要的是3根RGB彩色分量信号和2根扫描同步信号HSYNC和VSYNC针。其引脚编号图如下图所示:

  其中每个管脚的详细定义如下表所示

管脚

名称

定义

1

RED

红基色(75Ω,0.7Vp-p)

2

GREEN

绿基色(75Ω,0.7Vp-p)

3

BLUE

蓝基色(75Ω,0.7Vp-p)

4

ID2

地址码(显示器标识位2)

5

GND

6

RGND

红色地

7

GGND

绿色地

8

BGND

蓝色地

9

KEY

保留

10

SGND

同步信号地

11

ID0

地址码(显示器标识位0)

12

ID1

地址码(显示器标识位1)

13

HSYNC

行同步信号

14

VSYNC

场同步信号

15

ID3

地址码(显示器标识位3)

  VGA接口时序详解

  VGA 显示器扫描方式从屏幕左上角一点开始,从左向右逐点扫描,每扫描完一行,电子束回到屏幕的左边下一行的起始位置,在这期间,CRT 对电子束进行消隐,每行结束时,用行同步信号进行同步;当扫描完所有的行,形成一帧,用场同步信号进行场同步,并使扫描回到屏幕左上方,同时进行场消隐,开始下一帧。完成一行扫描的时间称为水平扫描时间,其倒数称为行频率;完成一帧(整屏)扫描的时间称为垂直扫描时间,其倒数称为场频率,即屏幕的刷新频率,常见的有 60Hz,75Hz 等等,但标准的 VGA 显示的场频 60Hz。其扫描示意图如下图所示

  在对VGA扫描方式有一个直观的感受以后接下来在看一看VGA接口的详细时序与各个参数的定义。VGA的详细时序如下图所示:

  总的来说,VGA的时序主要包括行时序与场时序两个部分。

  其中行时序主要包括:行同步(Hor Sync) 、行消隐(Hor Back Porch) 、行视频有效(Hor Active Video)和行前肩(Hor Front Porch)这四个参数,行时序的时序图如下图所示

而场时序主要包括:场同步(Ver Sync) 、场消隐(Ver Back Porch) 、场视频有效(Ver Active Video)和场前肩(Ver Front Porch)这四个参数,场时序的时序图如下图所示

  需要注意的有三点:

  1、行时序是以”像素”为单位的, 场时序是以”行”为单位的。

  2、VGA 工业标准显示模式要求:行同步,场同步都为负极性,即同步脉冲要求是负脉冲。

  3、VGA 行时序对行同步时间、 消隐时间、 行视频有效时间和行前肩时间有特定的规范, 场时序也是如此。 常用VGA 分辨率时序参数如下表所示

  其中:

  Pixel Clock = (Screen Refresh Frequency)*(Hor Active Video + Hor Front Porch + Hor Synv Pulse + Hor Back Porch)* (Ver Active Video + Ver Front Porch + Ver Synv Pulse + Ver Back Porch)

  以640x480,60Hz这种分辨率格式来说,25.175MHz = 25175000Hz = 60*(640 + 16 + 96 + 48)*(480 + 11 + 2 + 31) = 60 * 800 * 525

三、 目标任务

  1、编写VGA驱动代码,并用ModelSim对时序进行仿真,然后下载到开发板中使屏幕产生彩色条纹

  2、在上个任务的基础上,把一张存在ROM里面的图片数据显示到显示器上

四、 设计思路与Verilog代码编写

4.1、 VGA驱动模块的接口定义与整体设计

  Verilog编写的VGA模块除了Red,Green,Blue三基色、行同步HS以及场同步VS以外还要包括时钟、复位信号。其框图如下所示

  其中:

  I_clk是系统时钟;

  I_rst_n是系统复位;

  O_hs是行同步信号;

  O_vs是场同步信号;

  O_red是红色分量;

  O_green是绿色分量;

  O_blue是蓝色分量;

  上面的模块框图中没有看到测试数据(彩条或者图片)的输入端口,原因是由于VGA的逻辑比较简单,所以我准备把发送测试图案(彩条或者图片)的逻辑也直接集成到vga_driver模块中,这样可能更加方便理解。但是对于实际一个比较复杂的项目来说,最好还是把各个模块独立开来,这样更加方便二次移植。在写代码之前,先了解一个关于图片的分辨率与位深度的知识点。

4.2、 图片的分辨率、图片的尺寸与位深度

  图片的分辨率指图像中存储的信息量,是每英寸图像内有多少个像素点,它决定了位图图像细节的精细程度。描述分辨率的单位有:dpi(dots per inch)点每英寸、lpi(line per inch)线每英寸和ppi(pixel per inch)像素每英寸。

  图片的尺寸是指一幅图片长度和宽度各占多少像素,我们平常说的一张640×480的图片指的就是这张图片的长度有640个像素点,宽度有480个像素点

  位深度是指图片的每个像素是用多少位(bit)来表示的。比如黑白二色的图像是数字图像中最简单的一种,它只有黑、白两种颜色,也就是说它的每个像素只有1位颜色,位深度是1,用2的零次幂来表示;考虑到位深度平均分给R, G, B和Alpha,而只有RGB可以相互组合成颜色。所以4位颜色的图,它的位深度是4,只有2的4次幂种颜色,即16种颜色或16种灰度等级 )。8位颜色的图,位深度就是8,用2的8次幂表示,它含有256种颜色 ( 或256种灰度等级 )。24位颜色可称之为真彩色,位深度是24,它能组合成2的24次幂种颜色,即:16777216种颜色 ( 或称千万种颜色 ),超过了人眼能够分辨的颜色数量。当我们用24位来记录颜色时,实际上是以2^(8×3),即红、绿、蓝 ( RGB ) 三基色各以2的8次幂,256种颜色而存在的,三色组合就形成一千六百万种颜色。除了上面这几种情况以外,有的图片的位深度是16位,其中红基色占5位,绿基色占6位,蓝基色占5位,他们一共可以组成2^16中颜色。

  下面以这张图片为例来说明如何在电脑上查看图片的大小,分辨率以及位深度等信息。

  在电脑上用选中图片以后,然后鼠标右键在菜单中点击属性,然后在详细信息选项卡中就能查看图片的各个详细信息了,上面这张图片的信息如下图所示

  由上面的信息可知这张图片的大小为128*128。水平分辨率与垂直分辨率为96dpi(dots per inch),位深度为24-bit。

4.3、 原理图分析

  在写代码之前,先来分析一下我的开发板的VGA接口原理图。由于FPGA输出的RGB数据为数字信号,而VGA接口的RGB数据为模拟信号,所以需要一个数模转换器把FPGA输出的数字信号转化为VGA接口的模拟RGB数据输出。一般情况下,为了保证输出数据的保真度,都会使用一个专用的数模转换芯片(比如ADV7123)来实现这个数模转换的功能,但是在我的开发板上为了简单起见,设计了一个电阻匹配网络来实现这个数模转换的功能,FPGA输出的RGB三基色数字信号一共占16-bit,其中Red分量占5-bit,Green分量占6-bit,Blue分量占5-bit。下面是VGA接口部分的原理图

4.4、 vga_driver模块显示彩条Verilog代码编写

  有了上面的基础之后就可以开始着手编写代码,现在在回过头去看行时序与场时序,其实可以发现VGA的时序真的是非常简单。

  对行时序来说,只需要定义一个计数器,当计数器在像素时钟的作用下计满一行的总点数后清零,然后利用assign语句在计数值为Hor Sync期间把行时序信号拉低产生一个低脉冲就可以了。场时序与行时序非常类似,当行计数器计满一行了场计数器才加1,当计满一场的时间后,计数值清零,然后利用assign语句在Ver Sync期间把场时序信号拉低产生一个低脉冲就OK了。

  有了行时序与场时序以后,接下来就是在Hor Active Video和Ver Active Video均有效的期间往Red,Green,Blue三个分量送数据,数据就会在在屏幕上显示出来了。而Hor Active Video有效的期间正是行计数器的计数值在大于(Hor Sync + Hor Back Porch),小于(Hor Sync + Hor Back Porch + Hor Active Video)的时候,而Ver Active Video有效的期间正是场计数器的计数值在大于(Ver Sync + Ver Back Porch),小于(Ver Sync + Ver Back Porch + Ver Active Video)的时候,所以在代码里面可以利用assign语句产生一个激活标志,当激活标志为高的时候给Red,Green,Blue三个分量送数据,数据就会在屏幕显示出来了。

  下面以分辨率为640x480为例来编写vga_driver的代码,由前面的分辨率时序参数表可知,640x480分辨率的像素时钟为25.175Hz,但实际并不需要这么精确的时钟频率,我们取25MHz就可以了,我的开发板的时钟频率为50MHz,所以只需要简单的写一个二分频逻辑就可以得到这个像素时钟了。如果你想显示其他分辨率的图片,比如800x600分辨率的时钟频率是40MHz,这时候就需要用FPGA内部的Clocking Wizard IP核来得到这个40MHz的时钟,Clocking Wizard IP核内部回调用FPGA的PLL资源对输入频率进行处理来得到想要的输出频率。

  下面是VGA接口产生彩条的完整代码:

module vga_driver
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统复位
output reg [:] O_red , // VGA红色分量
output reg [:] O_green , // VGA绿色分量
output reg [:] O_blue , // VGA蓝色分量
output O_hs , // VGA行同步信号
output O_vs // VGA场同步信号
); // 分辨率为640*480时行时序各个参数定义
parameter C_H_SYNC_PULSE = ,
C_H_BACK_PORCH = ,
C_H_ACTIVE_TIME = ,
C_H_FRONT_PORCH = ,
C_H_LINE_PERIOD = ; // 分辨率为640*480时场时序各个参数定义
parameter C_V_SYNC_PULSE = ,
C_V_BACK_PORCH = ,
C_V_ACTIVE_TIME = ,
C_V_FRONT_PORCH = ,
C_V_FRAME_PERIOD = ; parameter C_COLOR_BAR_WIDTH = C_H_ACTIVE_TIME / ; reg [:] R_h_cnt ; // 行时序计数器
reg [:] R_v_cnt ; // 列时序计数器
reg R_clk_25M ; wire W_active_flag ; // 激活标志,当这个信号为1时RGB的数据可以显示在屏幕上 //////////////////////////////////////////////////////////////////
//功能: 产生25MHz的像素时钟
//////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_clk_25M <= 'b0 ;
else
R_clk_25M <= ~R_clk_25M ;
end
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// 功能:产生行时序
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_h_cnt <= 'd0 ;
else if(R_h_cnt == C_H_LINE_PERIOD - 'b1)
R_h_cnt <= 'd0 ;
else
R_h_cnt <= R_h_cnt + 'b1 ;
end assign O_hs = (R_h_cnt < C_H_SYNC_PULSE) ? 'b0 : 1'b1 ;
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// 功能:产生场时序
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_v_cnt <= 'd0 ;
else if(R_v_cnt == C_V_FRAME_PERIOD - 'b1)
R_v_cnt <= 'd0 ;
else if(R_h_cnt == C_H_LINE_PERIOD - 'b1)
R_v_cnt <= R_v_cnt + 'b1 ;
else
R_v_cnt <= R_v_cnt ;
end assign O_vs = (R_v_cnt < C_V_SYNC_PULSE) ? 'b0 : 1'b1 ;
////////////////////////////////////////////////////////////////// assign W_active_flag = (R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH )) &&
(R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_ACTIVE_TIME)) &&
(R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH )) &&
(R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_ACTIVE_TIME)) ; //////////////////////////////////////////////////////////////////
// 功能:把显示器屏幕分成8个纵列,每个纵列的宽度是80
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
begin
O_red <= 'b00000 ;
O_green <= 'b000000 ;
O_blue <= 'b00000 ;
end
else if(W_active_flag)
begin
if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH)) // 红色彩条
begin
O_red <= 'b11111 ; // 红色彩条把红色分量全部给1,绿色和蓝色给0
O_green <= 'b000000 ;
O_blue <= 'b00000 ;
end
else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*)) // 绿色彩条
begin
O_red <= 'b00000 ;
O_green <= 'b111111 ; // 绿色彩条把绿色分量全部给1,红色和蓝色分量给0
O_blue <= 'b00000 ;
end
else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*)) // 蓝色彩条
begin
O_red <= 'b00000 ;
O_green <= 'b000000 ;
O_blue <= 'b11111 ; // 蓝色彩条把蓝色分量全部给1,红色和绿分量给0
end
else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*)) // 白色彩条
begin
O_red <= 'b11111 ; // 白色彩条是有红绿蓝三基色混合而成
O_green <= 'b111111 ; // 所以白色彩条要把红绿蓝三个分量全部给1
O_blue <= 'b11111 ;
end
else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*)) // 黑色彩条
begin
O_red <= 'b00000 ; // 黑色彩条就是把红绿蓝所有分量全部给0
O_green <= 'b000000 ;
O_blue <= 'b00000 ;
end
else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*)) // 黄色彩条
begin
O_red <= 'b11111 ; // 黄色彩条是有红绿两种颜色混合而成
O_green <= 'b111111 ; // 所以黄色彩条要把红绿两个分量给1
O_blue <= 'b00000 ; // 蓝色分量给0
end
else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*)) // 紫色彩条
begin
O_red <= 'b11111 ; // 紫色彩条是有红蓝两种颜色混合而成
O_green <= 'b000000 ; // 所以紫色彩条要把红蓝两个分量给1
O_blue <= 'b11111 ; // 绿色分量给0
end
else // 青色彩条
begin
O_red <= 'b00000 ; // 青色彩条是由蓝绿两种颜色混合而成
O_green <= 'b111111 ; // 所以青色彩条要把蓝绿两个分量给1
O_blue <= 'b11111 ; // 红色分量给0
end
end
else
begin
O_red <= 'b00000 ;
O_green <= 'b000000 ;
O_blue <= 'b00000 ;
end
end
/*
////////////////////////////////////////////////////////////////
// 功能:产生黑白相间的格子图案
////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
begin
O_red <= 5'b00000 ;
O_green <= 6'b000000 ;
O_blue <= 5'b00000 ;
end
else if(W_active_flag)
begin
if((R_h_cnt[4]==1'b1) ^ (R_v_cnt[4]==1'b1))
begin
O_red <= 5'b00000 ;
O_green <= 6'b000000 ;
O_blue <= 5'b00000 ;
end
else
begin
O_red <= 5'b11111 ;
O_green <= 6'b111111 ;
O_blue <= 5'b11111 ;
end
end
else
begin
O_red <= 5'b00000 ;
O_green <= 6'b000000 ;
O_blue <= 5'b00000 ;
end
end
*/ endmodule

  整个代码的编写思路与上面的分析基本一致。除了显示彩条以外,还可以显示黑白格子,代码在上面也已经给出。更多更有趣的图案大家可以自己多试一下。在下载到开发板之前先用ModelSim对整个逻辑进行一下仿真,在测试激励文件里面只需要给时钟和复位加上激励就可以了,非常简单,我就不贴激励文件的代码了,下图是ModelSim仿真图

  具体的时序细节大家可以自己仿的试一下,整个时序都是没问题的。

  下面是代码下载到我的开发板中显示器显示的彩条图案

4.5、 如何把一张图片转化为存放在ROM中的.coe文件

  显示完彩条以后,接下来就是把一张图片显示在液晶显示器上。完成这个任务的第一步就是要把一张图片转化为ROM可以导入的.coe文件,第二部就是把ROM中的图片数据在有效区域输出出来,这样就可以在图片上显示一张图片了。由于我的FPGA型号为XC6SLX45-2CSG324,它内部的BRAM资源十分有限,无法存储一张完整640*480分辨率的图片数据,所以接下来的实验我会把上文那张128x128的圣诞老人的图片显示在液晶显示器上。如果你的FPGA内部资源足够的话,你可以以我这个例子作为参考把你想显示的图片显示出来。

为了把图片转化为.coe文件存放在ROM,这时可以利用Matlab软件先把图片转化为一个三维矩阵,然后把三维矩阵中的Red,Green,Blue分量的颜色数据提取出来按照5-bit Red,6-bit Green, 5-bit Blue的格式进行拼接,最后写入到.coe文件中,Matlab的完整代码如下:

clear
clc % 利用imread函数把图片转化为一个三维矩阵
image_array = imread('333.jpg'); % 利用size函数把图片矩阵的三个维度大小计算出来
% 第一维为图片的高度,第二维为图片的宽度,第三维为图片的RGB分量
[height,width,z]=size(image_array); % ** % imshow(image_array); % 显示图片 red = image_array(:,:,); % 提取红色分量,数据类型为uint8
green = image_array(:,:,); % 提取绿色分量,数据类型为uint8
blue = image_array(:,:,); % 提取蓝色分量,数据类型为uint8 % 把上面得到了各个分量重组成一个1维矩阵,由于reshape函数重组矩阵的
% 时候是按照列进行重组的,所以重组前需要先把各个分量矩阵进行转置以
% 后在重组
% 利用reshape重组完毕以后,由于后面需要对数据拼接,所以为了避免溢出
% 这里把uint8类型的数据扩大为uint32类型
r = uint32(reshape(red' , 1 ,height*width));
g = uint32(reshape(green' , 1 ,height*width));
b = uint32(reshape(blue' , 1 ,height*width)); % 初始化要写入.coe文件中的RGB颜色矩阵
rgb=zeros(,height*width); % 因为导入的图片是24-bit真彩色图片,每个像素占用24-bit,其中RGB分别占用8-bit
% 而我这里需要的是16-bit,其中R为5-bit,G为6-bit,B为5-bit,所以需要在这里对
% 24-bit的数据进行重组与拼接
% bitshift()函数的作用是对数据进行移位操作,其中第一个参数是要进行移位的数据,第二个参数为负数表示向右移,为
% 正数表示向左移,更详细的用法直接在Matlab命令窗口输入 doc bitshift 进行查看
% 所以这里对红色分量先右移3位取出高5位,然后左移11位作为ROM中RGB数据的第15-bit到第11-bit
% 对绿色分量先右移2位取出高6位,然后左移5位作为ROM中RGB数据的第10-bit到第5-bit
% 对蓝色分量先右移3位取出高5位,然后左移0位作为ROM中RGB数据的第4-bit到第0-bit
for i = :height*width
rgb(i) = bitshift(bitshift(r(i),-),) + bitshift(bitshift(g(i),-),) + bitshift(bitshift(b(i),-),);
end fid = fopen( 'image.coe', 'w+' ); % .coe文件的最前面一行必须为这个字符串,其中16表示16进制
fprintf( fid, 'memory_initialization_radix=16;\n'); % .coe文件的第二行必须为这个字符串
fprintf( fid, 'memory_initialization_vector =\n'); % 把rgb数据的前 height*width-1 个数据写入.coe文件中,每个数据之间用逗号隔开
fprintf( fid, '%x,\n',rgb(:end-)); % 把rgb数据的最后一个数据写入.coe文件中,并用分号结尾
fprintf( fid, '%x;',rgb(end)); fclose( fid ); % 关闭文件指针

  基本上每行我都做了详细的注释,最后我在强调几点:

  1、运行这段代码的时候必须把图片文件和Matlab的.m文件放在同一目录。imread()函数的参数是一个字符串,这个字符串是图片的名字。如果图片与Matlab的.m文件不再同一个目录,则imread()里面需要填写图片的绝对路径。

  2、在Matlab对矩阵进行转置只需要在矩阵的右边打一个单引号就可以了

  3、Matlab执行循环的效率非常低,所以我在数据进行重组与拼接之前把数据用reshape函数转化为1维矩阵并用uint32函数扩大了数据的范围,这样的好处是重组拼接的时候只需要一个for循环就ok。如果你觉得用reshape和uint32这两个函数对数据进行预处理太麻烦了,那么你可以使用一个二维for循环进行处理对128*128的矩阵的行和列分别处理。因为这里数据量不算大,所以用单个for循环和二维for循环区别不太大。

  4、如果你的开发板硬件支持显示24-bit真彩色图片,那么上面的代码中你就不需要对RGB分量分别右移进行截取操作,直接左移位并相加(拼接)就可以了

4.6、 vga_driver模块显示图片Verilog代码编写

  有了之前显示彩条的基础以后,这个实验就非常简单了,只要在把显示彩条的逻辑修改为从ROM读数据就可以了。而ROM的配置过程如下所示:

  1、选择Block Memory Generator

  2、选择类型为Single Port ROM

  3、选择ROM的Read Width为16,Read Depth为16384(128*128=16384)

  4、导入用Matlab生成的.coe图片文件

  5、其他的所有参数我都保持默认,如果你需要有其他特殊需求的话可以按需修改。

  配置好ROM就可以修改发送RGB数据的逻辑了,完整的代码如下:

module vga_driver
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统复位
output reg [:] O_red , // VGA红色分量
output reg [:] O_green , // VGA绿色分量
output reg [:] O_blue , // VGA蓝色分量
output O_hs , // VGA行同步信号
output O_vs // VGA场同步信号
); // 分辨率为640*480时行时序各个参数定义
parameter C_H_SYNC_PULSE = ,
C_H_BACK_PORCH = ,
C_H_ACTIVE_TIME = ,
C_H_FRONT_PORCH = ,
C_H_LINE_PERIOD = ; // 分辨率为640*480时场时序各个参数定义
parameter C_V_SYNC_PULSE = ,
C_V_BACK_PORCH = ,
C_V_ACTIVE_TIME = ,
C_V_FRONT_PORCH = ,
C_V_FRAME_PERIOD = ; parameter C_IMAGE_WIDTH = ,
C_IMAGE_HEIGHT = ,
C_IMAGE_PIX_NUM = ; reg [:] R_h_cnt ; // 行时序计数器
reg [:] R_v_cnt ; // 列时序计数器
reg R_clk_25M ; // 25MHz的像素时钟
reg [:] R_rom_addr ; // ROM的地址
wire [:] W_rom_data ; // ROM中存储的数据 wire W_active_flag ; // 激活标志,当这个信号为1时RGB的数据可以显示在屏幕上 //////////////////////////////////////////////////////////////////
//功能: 产生25MHz的像素时钟
//////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_clk_25M <= 'b0 ;
else
R_clk_25M <= ~R_clk_25M ;
end
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// 功能:产生行时序
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_h_cnt <= 'd0 ;
else if(R_h_cnt == C_H_LINE_PERIOD - 'b1)
R_h_cnt <= 'd0 ;
else
R_h_cnt <= R_h_cnt + 'b1 ;
end assign O_hs = (R_h_cnt < C_H_SYNC_PULSE) ? 'b0 : 1'b1 ;
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// 功能:产生场时序
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_v_cnt <= 'd0 ;
else if(R_v_cnt == C_V_FRAME_PERIOD - 'b1)
R_v_cnt <= 'd0 ;
else if(R_h_cnt == C_H_LINE_PERIOD - 'b1)
R_v_cnt <= R_v_cnt + 'b1 ;
else
R_v_cnt <= R_v_cnt ;
end assign O_vs = (R_v_cnt < C_V_SYNC_PULSE) ? 'b0 : 1'b1 ;
////////////////////////////////////////////////////////////////// // 产生有效区域标志,当这个信号为高时往RGB送的数据才会显示到屏幕上
assign W_active_flag = (R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH )) &&
(R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_ACTIVE_TIME )) &&
(R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH )) &&
(R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_ACTIVE_TIME )) ; //////////////////////////////////////////////////////////////////
// 功能:把ROM里面的图片数据输出
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_rom_addr <= 'd0 ;
else if(W_active_flag)
begin
if(R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH ) &&
R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_IMAGE_WIDTH - 'b1) &&
R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH ) &&
R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_IMAGE_HEIGHT - 'b1) )
begin
O_red <= W_rom_data[:] ; // 红色分量
O_green <= W_rom_data[:] ; // 绿色分量
O_blue <= W_rom_data[:] ; // 蓝色分量
if(R_rom_addr == C_IMAGE_PIX_NUM - 'b1)
R_rom_addr <= 'd0 ;
else
R_rom_addr <= R_rom_addr + 'b1 ;
end
else
begin
O_red <= 'd0 ;
O_green <= 'd0 ;
O_blue <= 'd0 ;
R_rom_addr <= R_rom_addr ;
end
end
else
begin
O_red <= 'd0 ;
O_green <= 'd0 ;
O_blue <= 'd0 ;
R_rom_addr <= R_rom_addr ;
end
end rom_image U_rom_image (
.clka(R_clk_25M), // input clka
.addra(R_rom_addr), // input [13 : 0] addra
.douta(W_rom_data) // output [15 : 0] douta
); endmodule

  绑定管脚生成bit文件以后下载到开发板中液晶显示器上就出现了圣诞老人的图片

  

  至此,整个VGA的实验全部完成。

五、 进一步思考

5.1、 如何修改屏幕背景为其他颜色(比如蓝色)

  上面已经可以让一张图片正常在屏幕上显示出来了,而图片以外的区域是黑色的,如果我们想把图片以外的区域改成蓝色的其实很简单,只需要图片以外区域的RGB分量的Red,Green分量给0,Blue分量给1就可以了

5.2、如何让128*128圣诞老人的图片在屏幕中间

  上图的圣诞老人图片显示在屏幕的左上角,如果我们想把他显示在图片中间位置只需要在

       if(R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_OFFSET                          )  &&
R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_OFFSET + C_IMAGE_WIDTH - 'b1 ) &&
R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_OFFSET ) &&
R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_OFFSET + C_IMAGE_HEIGHT - 'b1 ) )

  这个判断语句中加上一个固定的C_H_OFFSET与C_V_OFFSET偏移分量就可以了,当然这个偏移分量不能太大,否则会使图片跑到屏幕外面

5.3、 如何让128*128圣诞老人的图片在屏幕上动起来,并且碰到显示器边框以后就反弹

  上面已经知道修改图片的位置的方法以后,我们可以把图片的行和列的偏移量设置成一个reg变量,然后在每一帧结束以后修改这个偏移量就可以了,而场脉冲的下降沿可以看做每一帧结束的标志位,所以为了实现这个功能需要写一个检测场脉冲下降沿的逻辑。

  至于碰到屏幕以后产生“反弹”的效果实际上就是一个状态机,这个状态机一共有四个状态:

  状态2’b00:图片向右下方移动

  状态2’b01:图片向右上方移动

  状态2’b10:图片向左下方移动

  状态2’b11:图片向左上方移动

  这个状态机很容易就抽象出来了,但关键是状态的跳变一定要理清楚了,下面这个状态机的状态跳变图

  上面这个状态转换图清晰的描述了图片在屏幕上运动的状态切换图,其中每次状态切换都是在场脉冲的下降沿触发,这样才能保证图片在运动过程中不会闪烁。

  完整的代码如下:

module vga_driver
(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统复位
output reg [:] O_red , // VGA红色分量
output reg [:] O_green , // VGA绿色分量
output reg [:] O_blue , // VGA蓝色分量
output O_hs , // VGA行同步信号
output O_vs // VGA场同步信号
); // 分辨率为640*480时行时序各个参数定义
parameter C_H_SYNC_PULSE = ,
C_H_BACK_PORCH = ,
C_H_ACTIVE_TIME = ,
C_H_FRONT_PORCH = ,
C_H_LINE_PERIOD = ; // 分辨率为640*480时场时序各个参数定义
parameter C_V_SYNC_PULSE = ,
C_V_BACK_PORCH = ,
C_V_ACTIVE_TIME = ,
C_V_FRONT_PORCH = ,
C_V_FRAME_PERIOD = ; parameter C_IMAGE_WIDTH = ,
C_IMAGE_HEIGHT = ,
C_IMAGE_PIX_NUM = ; reg [:] R_h_cnt ; // 行时序计数器
reg [:] R_v_cnt ; // 列时序计数器
reg R_clk_25M ;
reg [:] R_rom_addr ; // ROM的地址
wire [:] W_rom_data ; // ROM中存储的数据 reg [:] R_h_pos ; // 图片在屏幕上显示的水平位置,当它为0时,图片贴紧屏幕的左边沿
reg [:] R_v_pos ; // 图片在屏幕上显示的垂直位置,当它为0时,图片贴紧屏幕的上边沿 reg R_vs_reg1 ;
reg R_vs_reg2 ;
wire W_vs_neg ; // 场脉冲下降沿标志
reg [:] R_state ; wire W_active_flag ; // 激活标志,当这个信号为1时RGB的数据可以显示在屏幕上 //////////////////////////////////////////////////////////////////
//功能: 产生25MHz的像素时钟
//////////////////////////////////////////////////////////////////
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_clk_25M <= 'b0 ;
else
R_clk_25M <= ~R_clk_25M ;
end
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// 功能:产生行时序
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_h_cnt <= 'd0 ;
else if(R_h_cnt == C_H_LINE_PERIOD - 'b1)
R_h_cnt <= 'd0 ;
else
R_h_cnt <= R_h_cnt + 'b1 ;
end assign O_hs = (R_h_cnt < C_H_SYNC_PULSE) ? 'b0 : 1'b1 ;
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// 功能:产生场时序
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_v_cnt <= 'd0 ;
else if(R_v_cnt == C_V_FRAME_PERIOD - 'b1)
R_v_cnt <= 'd0 ;
else if(R_h_cnt == C_H_LINE_PERIOD - 'b1)
R_v_cnt <= R_v_cnt + 'b1 ;
else
R_v_cnt <= R_v_cnt ;
end assign O_vs = (R_v_cnt < C_V_SYNC_PULSE) ? 'b0 : 1'b1 ;
////////////////////////////////////////////////////////////////// assign W_active_flag = (R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH )) &&
(R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_ACTIVE_TIME )) &&
(R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH )) &&
(R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_ACTIVE_TIME )) ; //////////////////////////////////////////////////////////////////
// 功能:把ROM中的图片数据显示到屏幕上
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_rom_addr <= 'd0 ;
else if(W_active_flag)
begin
if(R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH + R_h_pos ) &&
R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + R_h_pos + C_IMAGE_WIDTH - 'b1) &&
R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH + R_v_pos ) &&
R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + R_v_pos + C_IMAGE_HEIGHT - 'b1) )
begin
O_red <= W_rom_data[:] ;
O_green <= W_rom_data[:] ;
O_blue <= W_rom_data[:] ;
if(R_rom_addr == C_IMAGE_PIX_NUM - 'b1)
R_rom_addr <= 'd0 ;
else
R_rom_addr <= R_rom_addr + 'b1 ;
end
else
begin
O_red <= 'd0 ;
O_green <= 'd0 ;
O_blue <= 'd0 ;
R_rom_addr <= R_rom_addr ;
end
end
else
begin
O_red <= 'd0 ;
O_green <= 'd0 ;
O_blue <= 'd0 ;
R_rom_addr <= R_rom_addr ;
end
end //////////////////////////////////////////////////////////////////
// 功能:产生场脉冲的下降沿标志,在这个标志用来修改R_h_pos和R_v_pos
// 两个参数,从而改变图片的位置
//////////////////////////////////////////////////////////////////
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_vs_reg1 <= 'b0 ;
R_vs_reg2 <= 'b0 ;
end
else
begin
R_vs_reg1 <= O_vs ;
R_vs_reg2 <= R_vs_reg1 ;
end
end assign W_vs_neg = ~R_vs_reg1 & R_vs_reg2 ; //////////////////////////////////////////////////////////////////
// 功能:使图片移动的状态机
//////////////////////////////////////////////////////////////////
always@(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_h_pos <= 'd0 ;
R_v_pos <= 'd0 ;
R_state <= 'b00 ;
end
else if(W_vs_neg)
begin
case(R_state)
'b00: // 图片往右下方移动
begin
R_h_pos <= R_h_pos + ;
R_v_pos <= R_v_pos + ;
if(R_h_pos + C_IMAGE_WIDTH == C_H_ACTIVE_TIME) // 如果碰到右边框
R_state <= 'b10 ;
else if((R_v_pos + C_IMAGE_HEIGHT) == C_V_ACTIVE_TIME) // 如果碰到下边框
R_state <= 'b01 ;
end
'b01: // 图片往右上方移动
begin
R_h_pos <= R_h_pos + ;
R_v_pos <= R_v_pos - ;
if(R_h_pos + C_IMAGE_WIDTH == C_H_ACTIVE_TIME) // 如果碰到右边框
R_state <= 'b11 ;
else if(R_v_pos == ) // 如果碰到上边框
R_state <= 'b00 ;
end
'b10: // 图片往左下方移动
begin
R_h_pos <= R_h_pos - ;
R_v_pos <= R_v_pos + ;
if(R_h_pos == ) // 如果碰到左边框
R_state <= 'b00 ;
else if(R_v_pos + C_IMAGE_HEIGHT == C_V_ACTIVE_TIME) // 如果碰到下边框
R_state <= 'b11 ;
end
'b11: // 图片往左上方移动
begin
R_h_pos <= R_h_pos - ;
R_v_pos <= R_v_pos - ;
if(R_h_pos == ) // 如果碰到上边框
R_state <= 'b01 ;
else if(R_v_pos == ) // 如果碰到左边框
R_state <= 'b10 ;
end
default:R_state <= 'b00 ;
endcase
end
end rom_image U_rom_image (
.clka(R_clk_25M), // input clka
.addra(R_rom_addr), // input [13 : 0] addra
.douta(W_rom_data) // output [15 : 0] douta
); endmodule

 

  代码里面碰到左边框和碰到上边框的判决方式为 if(R_h_pos == 1) 和if(R_h_pos == 1),我之所以设置为1而不设置为0是因为我发现设置为0的时候当图片碰到左边框和上边框会闪烁一下,具体原因大家自己分析一下。

六、 总结

VGA的时序是非常适合初学者入门的,相对于其他接口时序来说,VGA时序确实是最简单的,所以初学者最好能自己把代码从头到尾敲一遍,然后用ModelSim仿一下,看一看中间变量的波形以加深对VGA时序的理解。

【接口时序】7、VGA接口原理与Verilog实现的更多相关文章

  1. 【接口时序】1、软件与Verilog基本格式规范说明

    一. 说明 以前总是没有记录的习惯,导致遇到问题时总得重新回忆与摸索,大大降低了学习效率,从今天开始决定改掉这个坏毛病,认真记录自己的Verilog学习之路,希望自己能一直坚持下去. 二. 软件资源与 ...

  2. 【接口时序】6、IIC总线的原理与Verilog实现

    一. 软件平台与硬件平台 软件平台: 1.操作系统:Windows-8.1 2.开发套件:ISE14.7 3.仿真工具:ModelSim-10.4-SE .ChipScope 硬件平台: 1. FPG ...

  3. 【接口时序】4、SPI总线的原理与Verilog实现

    一. 软件平台与硬件平台 软件平台: 1.操作系统:Windows-8.1 2.开发套件:ISE14.7 3.仿真工具:ModelSim-10.4-SE 硬件平台: 1. FPGA型号:Xilinx公 ...

  4. 【接口时序】2、Verilog实现流水灯及与C语言的对比

    一. 软件平台与硬件平台 软件平台: 1.操作系统:Windows-8.1 2.开发套件:ISE14.7 3.仿真工具:ModelSim-10.4-SE 硬件平台: 1.FPGA型号:XC6SLX45 ...

  5. VGA接口时序约束

    SF-VGA模块板载VGA显示器DA转换驱动芯片AVD7123,FPGA通过OUPLLN连接器驱动ADV7123芯片产生供给VGA显示器的色彩以及同步信号.SF-CY3核心模块与SF-VGA子模块连接 ...

  6. 基于FPGA的VGA接口使用

    前言 什么是VGA? VGA(视频图形阵列)是IBM公司制定的一种视频数据传输标准. 接口信号主要有5个:R(Red),G(Green),B(Blue),HS(Horizontal synchroni ...

  7. 【原创】FPGA开发手记(二) VGA接口

    以下内容均以Xilinx的Nexys3作为开发板 1.VGA接口介绍 首先,先看电路图(3*5为例): 标准VGA一共15个接口,但是实际应用的接口信号只用五个:HSYNC,行同步信号:VSYNC,场 ...

  8. 小米盒子连接老式电脑显示器(VGA接口)

    家里闲置一台老式显示器,只有VGA接口,无HDMI高清接口; 小米盒子上有三个输出接口: 一个HDMI高清接口:HDMI接口输出的有音频信号和视频信号,现在买的电视机一般都有HDMI高清接口: 一个A ...

  9. 一文读懂HDMI和VGA接口针脚定义

    一文读懂HDMI和VGA接口针脚定义 摘自:http://www.elecfans.com/yuanqijian/jiekou/20180423666604.html   HDMI概述 HDMI是高清 ...

随机推荐

  1. CSS样式学习-3、轮廓、伪类/元素、display-flex布局

    一.轮廓 outline绘制于元素周围的一条线,位于边框边缘外围. 属性规定元素轮廓的样式.颜色.宽度. outline-width轮廓宽度,属性:thin细轮廓.medium中等(默认值).thic ...

  2. 网站加入QQ在线客服

    <!-- qq客服 --> <div class="QQFloat" > <div class="qq"> <div ...

  3. selenium下打开Chrome报错解决

    错误如下: [22516:20196:0704/024642.979:ERROR:install_util.cc(597)] Unable to read registry value HKLM\SO ...

  4. spring proxy-target-class

    <tx:annotation-driven transaction-manager="transactionManager"                          ...

  5. WMS专业术语&系统功能操作培训

    逻辑层:公司.分部.地区物理层:仓库.1个仓库只能属于思维的1个地区.命名规则:SCPRD_WMWHSE1货主:纳思达多货主:1个仓库可以配置1个或多个货主SKU:物料代码(stock keeping ...

  6. PHP 实现单链表

    数据结构是计算机存储.组织数据的方式,结构不同那么数据的检索方式和效率都不一样, 常用的数据结构有  数组 .栈 .队列 .链表 .树.堆 今天讲下单链表,单链表是一种链式存取的数据结构, 跟顺序链表 ...

  7. 图片利用 new Image()预加载原理 和懒加载的实现原理

    二:预加载和懒加载的区别 预加载与懒加载,我们经常经常用到,这些技术不仅仅限于图片加载,我们今天讨论的是图片加载: 图片预加载:顾名思义,图片预加载就是在网页全部加载之前,提前加载图片.当用户需要查看 ...

  8. 19. pt-query-digest

    慢查询参数 slow_query_log=1slow_query_log_file=/mysql3306/log/slow.log 记录的是查询语句,而非管理语句.除非启用 los_slow_admi ...

  9. LibreOJ #6014. 「网络流 24 题」最长 k 可重区间集

    #6014. 「网络流 24 题」最长 k 可重区间集 内存限制:256 MiB时间限制:1000 ms标准输入输出 题目类型:传统评测方式:文本比较 上传者: 匿名 提交提交记录统计讨论测试数据   ...

  10. Git的SSH-key生成、导入及使用

    Git主要使用4种协议传输数据:本地协议,SSH协议,Git协议和HTTP/S协议. SSH协议是最为常用的一种,正式介绍SSH之前,简要说明一下其它协议. 本地协议(file://) 本地协议的优点 ...