官方论坛
官方淘宝
官方博客
微信公众号
点击联系吴工 点击联系周老师

2.11 VGA显示圆

发布时间:2021-08-22   作者:admin 浏览量:

本节的文档编号:001700000022

需要看对应的视频,请点击视频编号:001700000443

1、至简原理与应用配套的案例和PPT讲解
2、本设计需要通过VGA连接线将显示器和开发板进行连接,FPGA在连接成功后产生640*480分辨率,刷新频率为60Hz的VGA时序,使得显示器产生图像其中图像中间有一个直径为100像素的圆。圆内显示颜色为绿色,圆外显示颜色是白色。步骤性教学;

3、这是Altera和Xilinx入门学习案例文档

1项目背景

在一个平面内,一动点以一定点为中心,以一定长度为距离旋转一周所形成的封闭曲线叫做圆。圆有无数个点。

在同一平面内,到定点的距离等于定长的点的集合叫做圆。圆可以表示为集合{M||MO|=r},圆的标准方程是(x - a)2+ (y - b)2= r2。其中,o是圆心,r是半径。

图399

在平面直角坐标系中,以点O(a,b)为圆心,以r为半径的圆的标准方程是(x-a)2+(y-b)2=r2。

特别地,以原点为圆心,半径为r(r>0)的圆的标准方程为x2+y2=r2。


2设计目标

通过VGA连接线,将显示器和教学板的VGA接口相连。连接示意图如下。

图400

分辨率

行/列

同步脉冲

显示后沿

显示区域

显示前沿

帧长

单位

640*480

/60Hz

96

48

640

16

800

基准时钟

2

33

480

10

525

800*600

/72Hz

120

64

800

56

1040

基准时钟

6

23

600

37

666

800*600

/60Hz

128

88

800

40

1056

基准时钟

4

23

600

1

628

1024*768

/60Hz

136

160

1024

24

1344

基准时钟

6

29

768

3

806

然后FPGA产生640*480分辨率(使用上表中的第一种分辨率),让显示器产生显示一幅图像。提示:显示器一般都会自适应功能,无须设置就能识别不同分辨率的图像。

图像的内容是:在屏幕的中央显示一个直径为100个像素的圆,圆内的颜色为绿色,圆外的颜色是白色,如下图所示。

图401

上板效果图如下图所示(显示器不同,显示效果也会有差别,请注意)。

图402


3设计实现

3.1顶层接口

新建目录:D:mdy_book ga_exec1。在该目录中,新建一个名为vga_exec1.v的文件,并用GVIM打开,开始编写代码。

我们要实现的功能,概括起来就是FPGA产生VGA时序,即控制VGA_R4~R0、VGA_G5~G0、VGA_B4~B0、VGA_HSYNC和VGA_VSYNC,让显示器显示红色。其中,VGA_HSYNC和VGA_VSYNC,FPGA可根据时序产生高低电平。而颜色数据,由于是固定的红色,FPGA也能自己产生,不需要外部输入图像的数据。那么我们的FPGA工程,可以定义输出信号hys表示行同步,用输出信号vys表示场同步,定义一个16位的信号lcd_rgb,其中lcd_rgb[15:11]表示VGA_R4~0,、lcd_rgb[10:5]表示VGA_G5~0,、lcd_rgb[4:0]表示VGA_B4~0。

我们还需要时钟信号和复位信号来进行工程控制。

综上所述,我们这个工程需要五个信号,时钟clk,复位rst_n,场同步信号vys、行同步信号hys和RGB输出信号lcd_rgb。

器件

电阻网络转换后

信号线

信号线

FPGA管脚

FPGA工程信号

CN1

VGA_RED

VGA_R4

E11

lcd_rgb[15]

VGA_R3

C10

lcd_rgb[14]

VGA_R2

D10

lcd_rgb[13]

VGA_R1

E9

lcd_rgb[12]

VGA_R0

E10

lcd_rgb[11]

VGA_GREEN

VGA_G5

D15

lcd_rgb[10]

VGA_G4

C17

lcd_rgb[9]

VGA_G3

C19

lcd_rgb[8]

VGA_G2

E12

lcd_rgb[7]

VGA_G1

C13

lcd_rgb[6]

VGA_G0

E15

lcd_rgb[5]

VGA_BLUE

VGA_B4

D13

lcd_rgb[4]

VGA_B3

E13

lcd_rgb[3]

VGA_B2

D17

lcd_rgb[2]

VGA_B1

E16

lcd_rgb[1]

VGA_B0

C15

lcd_rgb[0]

VGA_HSYNC

VGA_HSYNC

C20

hys

VGA_VSYNC

VGA_VSYNC

D20

vys

X1


SYS_CLK

G1

clk

K1


SYS_RST

AB12

rst_n

将module的名称定义为vga_exec1。并且我们已经知道该模块有五个信号:clk、rst_n、lcd_hs、lcd_vs和lcd_rgb。为此,代码如下:

1

2

3

4

5

6

7

module vga_exec1(

clk      ,

rst_n    ,

lcd_hs   ,

lcd_vs   ,

lcd_rgb

);

其中clk、rst_n是输入信号,lcd_hs、lcd_vs和lcd_rgb是输出信号,其中clk、rst_n、lcd_hs、lcd_vs的值是0或者1,一根线即可,lcd_rgb为16位位宽的,根据这些信息,我们补充输入输出端口定义。代码如下:

1

2

3

4

5

input                   clk           ;

input                   rst_n         ;

output                  lcd_hs        ;

output                  lcd_vs        ;

output  [15:0]          lcd_rgb       ;


3.2架构设计

需要注意的是,输入进来的时钟clk是50MHz,而从分辨率参数表可知道,行单位的基准时钟是25 MHz。为此我们需要根据50MHz来产生一个25 MHz的时钟,然后再用于产生VGA时序。

为了得到这个25M时钟,我们需要一个PLL。PLL可以认为是FPGA内的一个硬核,它的功能是根据输入的时钟,产生一个或多个倍频和分频后的输出时钟,同时可以调整这些输出时钟的相位、占空比等。

例如,输入进来是50M时钟,如果我需要一个100M时钟,那么从逻辑上、代码上是不可能产生的,我们就必须用到PLL来产生了。

整个工程的结构图如下。

图403

PLL的生成方式过程,请看本案例的综合工程和上板一节的内容。


3.3VGA驱动模块设计

3.3.1接口信号

在目录:D:mdy_book ga_exec1中,建立一个vga_driver.v文件,并用GVIM打开,开始编写代码。

我们先分析功能。要控制显示器,让其产生红色,也就是让FPGA控制VGA_R0~4、VGA_G0~5、VGA_B0~4、VGA_VSYNC和VGA_HSYNC信号。那么VGA驱动模块,可以定义输出信号hys表示行同步,用输出信号vys表示场同步,定义一个16位的信号lcd_rgb,其中lcd_rgb[15:11]表示VGA_R4~0,、lcd_rgb[10:5]表示VGA_G5~0,、lcd_rgb[4:0]表示VGA_B4~0。

同时该模块的工作时钟为25M,同时需要一个复位信号。

综上所述,我们这个模块需要五个信号,25M时钟clk,复位rst_n,场同步信号vys、行同步信号hys和RGB输出信号lcd_rgb。

将module的名称定义为vga_driver。并且我们已经知道该模块有五个信号:clk、rst_n、hys、vys和lcd_rgb。为此,代码如下:

1

2

3

4

5

6

7

module vga_driver(

clk,

rst_n,

hys,

vys,

lcd_rgb

);

其中clk、rst_n是输入信号,hys、vys和lcd_rgb是输出信号,其中clk、rst_n、hys、vys的值是0或者1,一根线即可,lcd_rgb为16位位宽的,根据这些信息,我们补充输入输出端口定义。代码如下:

1

2

3

4

5

input                  clk      ;

input                  rst_n    ;

output                 hys      ;

output                 vys      ;

output [15:0]          lcd_rgb  ;


3.3.2信号设计

我们先设计场同步信号hys,VGA时序中的场同步信号,其时序图如下:

图404

hys就是一个周期性地高低变化的脉冲。我们使用的是下表中的第一种分辨率,也就是同步脉冲a的时间是96个时钟周期,而显示后沿b是48个时钟周期,显示时序c是640个时钟周期,显示前沿是16个时钟周期,一共是800个时钟周期。

分辨率

行/列

同步脉冲

显示后沿

显示区域

显示前沿

帧长

单位

640*480

/60Hz

96

48

640

16

800

基准时钟

2

33

480

10

525

800*600

/72Hz

120

64

800

56

1040

基准时钟

6

23

600

37

666

800*600

/60Hz

128

88

800

40

1056

基准时钟

4

23

600

1

628

1024*768

/60Hz

136

160

1024

24

1344

基准时钟

6

29

768

3

806

将时间信号填入图中,更新后的时序图如下:

图405

很显然,我们需要1个计数器来产生这个时序,我们将该计数器命名为h_cnt。由于hys是不停地产生的,那么h_cnt就是不停地计数,每个时钟都要计数器,所以认为该计数器的加1条件为“1”,可写成:assign add_h_cnt = 1。从上图可知,该计数器的周期是800。综上所述,该计数器的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

always @(posedge clk or negedge rst_n)begin

if(!rst_n)begin

h_cnt <= 0;

end

else if(add_h_cnt)begin

if(end_h_cnt)

h_cnt <= 0;

else

h_cnt <= h_cnt + 1;

end

end

assign add_h_cnt = 1;

assign end_h_cnt = add_h_cnt && h_cnt== 800 - 1;

有了计数器h_cnt,那么hys信号就有了对齐的对象。从时序图可以发现, hys有两个变化点,一个是h_cnt数到96个时,由0变1;另一个是当h_cnt数到800个时,由1变0。所以,场同步信号的代码如下:

1

2

3

4

5

6

7

8

9

10

11

always@(posedge clk or negedge rst_n)begin

if(!rst_n)begin

hys <= 0;

end

else if(add_h_cnt && h_cnt == 96 -1)begin

hys <= 1'b1;

end

else if(end_h_cnt)begin

hys <= 1'b0;

end

end

接下来设计vys信号。该信号的时序图如下所示。

图406

vys就是一个周期性地高低变化的脉冲。我们使用的是表中的第一种分辨率,查询表可知,同步脉冲a的时间是2行的时间,而显示后沿b是33行,显示时序c是480行,显示前沿是10行,一共是525行。其中,一“行”结束,也就是h_cnt数完了。

将时间信号填入图中,更新后的时序图如下:

图407

很显然,我们还需要1个计数器来产生这个时序,我们将该计数器命名为v_cnt。该计数器是用来数有多少行的,所以加1条件就是一行结束,即end_h_cnt,可写成:assign add_v_cnt = end_h_cnt。从上图可知,该计数器的周期是525。综上所述,该计数器的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

always @(posedge clk or negedge rst_n)begin

if(!rst_n)begin

v_cnt <= 0;

end

else if(add_v_cnt)begin

if(end_v_cnt)

v_cnt <= 0;

else

v_cnt <= v_cnt + 1;

end

end

assign add_v_cnt = end_h_cnt;

assign end_v_cnt = add_v_cnt && v_cnt== 525 - 1;

有了计数器v_cnt,那么vys信号就有了对齐的对象。从时序图可以发现, vys有两个变化点,一个是v_cnt数到2个时,由0变1;另一个是当h_cnt数到525个时,由1变0。所以,场同步信号的代码如下:

1

2

3

4

5

6

7

8

9

10

11

always  @(posedge clk or negedge rst_n)begin

if(!rst_n)begin

vys <= 1'b0;

end

else if(add_v_cnt && v_cnt == 2 - 1)begin

vys <= 1'b1;

end

else if(end_v_cnt)begin

vys <= 1'b0;

end

end

最后我们还有一个信号需要设计,那就是lcd_rgb信号。

图408

我们在显示器中一共要显示两种颜色:绿色和白色。lcd_rgb等于16’b00000_111111_00000时表示绿色;lcd_rgb等于16’b11111_111111_11111时表示白色。还要注意的是,在非显示区域,lcd_rgb的值要为0,才能正确显示。我们现在要仔细区分,在什么时候分别输出上面的值。

图像分成两部分:圆内和圆外。圆外显示白色,圆内显示绿色。所以我们进一步分析,如何知道是圆内还是圆外呢?我们需要用到圆的定义公式:(x-a)2+(y-b)2=r2。自然地,当(x-a)2+(y-b)22的区域是圆内,(x-a)2+(y-b)2>=r2的区域就是圆外。

圆的原理和公式是初中的知识,非常简单,我们可以认为这个公式就是一个算法,关键的是我们如何使用FPGA来实现这个算法。常听到有人说,学FPGA工具是很简单的,算法才是最重要的。对于这一点,我不敢苟同。

有读者一听到算法,就觉得很高大上。其实算法就是解决某个问题的数学公式,简单的就是一个加法运算,例如求和、求平均数等,再复杂一些就是FFT等。要创造、发明、改进一套算法很难,这些真是需要一些数学功底很好的高材生才能做的事,需要天赋,训练也训练不出来。我们更多地找到算法、读懂算法、实现算法、解决问题。我们学习FPGA,学习怎么把各种算法用FPGA实现,这是比较实现可行的目标(当然,这是笔者基于自身水平而自定的目标哈,天外有天,人外有人,厉害的人物要忽略这建议)。

现在我们要用FPGA实现算法(x-a)2+(y-b)22,我们应该如何考虑呢?我们重要的是先搞清楚公式里各个元素是什么,在FPGA里面怎么表示。

图409

首先看r。r是圆的半径,根据功能要求,我们显示的圆的直径是100个像素,所以r的值应该为50。

其次看a和b。a和b是圆心的坐标。我们要在屏幕的中心显示一个圆,中间位置就是行列的中间值,所以a的值为640/2=320,b的值为480/2=240。

最后我们再来看x和y。x和y是现时屏幕上显示的坐标。在屏幕中x的范围是0~639,y的范围是0~479,最左上角的值为(0,0),最右下角的坐标是(639,479)。现在FPGA的信号中没有x和y,但有相似的信号h_cnt和v_cnt,我们想办法将h_cnt和v_cnt表示x和y。

考虑到VGA时序中的同步脉冲和显示前沿的因素,当h_cnt=96+48,v_cnt=2+33时,才表示x=0和y=0。所以用h_cnt和v_cnt来表示x和y,则有x = h_cnt-96-48,y=v_cnt-2-33。

用distance表示距离的平方,则有distance = (x-a)*(x-a) + (y-b)*(y-b) = (h_cnt-96-48 -320) *(h_cnt-96-48 -320) +(v_cnt-2-33 -240) *(v_cnt-2-33 -240)

1

2

3

4

always  @(*)begin

distance   = ((h_cnt - 96 - 48 - 320) * (h_cnt - 96 - 48 - 320)) + ((v_cnt - 2 - 33 - 240) * (v_cnt - 2 - 33 - 240));

end

如果distance小于r2=(50)2=2500 ,说明在圆内,否则在圆外。

显示区域:(h_cnt >=(96+48)&& h_cnt <(96+48+640)),并且(v_cnt>=(2+33) && v_cnt<(2+33+480))

绿色区域:distance < 2500

白色区域:在显示区域中,非绿色区域的,就是白色区域。

非显示区域:显示区域之外的,就是非显示区域。

我们可以设计几个信号来表示这些区域。显示区域用valid_area=1表示,红色区域用red_area=1表示,绿色区域用green_area=1表示。可得到代码如下:

1

2

3

4

5

6

7

8

always  @(*)begin

green_area = distance < 2500 ;

end


always  @(*)begin

valid_area = h_cnt >=(96+48) && h_cnt <(96+48+640) && v_cnt >=(2+33) && v_cnt < (2+33+480);

end

有了green_area和valid_area后,设计lcd_rgb就好办了。

非显示区域(valid_area=0),lcd_rgb输出“16’b0”;

显示区域(valid_area)中的绿色区域(green_area=1),lcd_rgb输出“16’b00000_111111_00000”;

显示区域(valid_area)中的非绿色区域(green_area=0),lcd_rgb输出“16’b11111_111111_11111”。

则可以写出代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

always  @(posedge clk or negedge rst_n)begin

if(rst_n==1'b0)begin

lcd_rgb <= 16'h0;

end

else if(valid_area)begin

if(green_area)begin

lcd_rgb <= 16'b00000_111111_00000;

end

else begin

lcd_rgb <= 16'b11111_111111_11111;

end

end

else begin

lcd_rgb <= 0;

end

end

此次,主体程序已经完成。接下来是将module补充完整。


3.3.3信号定义

接下来定义信号类型。

h_cnt是用always产生的信号,因此类型为reg。h_cnt计数的最大值为800,需要用10根线表示,即位宽是10位。因此代码如下:

1

reg    [9:0]           h_cnt  ;

add_h_cnt和end_h_cnt都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1个线表示即可。因此代码如下:

1

2

wire                   add_h_cnt;

wire                   end_h_cnt;

v_cnt是用always产生的信号,因此类型为reg。v_cnt计数的最大值为525,需要用10根线表示,即位宽是10位。因此代码如下:

1

reg    [9:0]           v_cnt  ;

add_v_cnt和end_v_cnt都是用assign方式设计的,因此类型为wire。并且其值是0或者1,1根线表示即可。因此代码如下:

1

2

wire                   add_v_cnt;

wire                   end_v_cnt;

lcd_rgb是用always方式设计的,因此类型为reg。并且它的位宽是16位,16根线表示即可。因此代码如下:

1

reg    [15:0]          lcd_rgb;

hys和vys是用always方式设计的,因此类型为reg。并且其值是0或1,需要1根线表示即可。因此代码如下:

1

2

reg                    hys    ;

reg                    vys    ;

distance是用always方式设计的,因此类型为reg。其位宽为20位,需要20根线表示。因此代码如下:

1

reg  [19:0]            distance   ;

valid_area和green_area是用always方式设计的,因此类型为reg。并且其值是0或1,用一根线表示即可。因此代码如下:

1

reg                    valid_area ;

reg                    green_area;


3.4顶层模块设计

3.4.1例化子模块

例化PLL IP核的代码

1

2

3

4

vga_pll   module_1(

.inclk0     (clk    ),

.c0         (clk_0  )

);

例化驱动模块的代码

1

2

3

4

5

6

7

color module_6(

.clk        (clk_0  ),

.rst_n      (rst_n  ),

.hys        (lcd_hs ),

.vys        (lcd_vs ),

.lcd_rgb    (lcd_rgb)

);


3.4.2信号定义

clk_0是在例化文件中,因此类型为wire。并且其值是0或1,用一根线表示即可。因此代码如下:

1

wire                    clk_0         ;

lcd_sh和lcd_vs是在例化文件中,因此类型为wire。并且其值是0或1,用一根线表示即可。因此代码如下:

1

2

wirelcd_hs        ;

wire                  lcd_vs        ;

lcd_rgb是在例化文件中,因此类型为wire。它的位宽是16位的,用16根线表示即可。因此代码如下:

1

wire[15:0]lcd_rgb       ;

至此,整个代码的设计工作已经完成。下一步是新建工程和上板查看现象。


4综合与上板

4.1新建工程

1.首先在d盘中创建名为“vga_exec1”的工程文件夹,将写的代码命名为“vga_exec1.v”,顶层模块名为“vga_exec1”,例化文件命名为“vga_driver.v”。

图410

图411

图412

2.然后打开QuartusⅡ,点击File下拉列表中的New Project Wzard...新建工程选项。

图413

3.在出现的界面中直接点击最下方的“Next”。

图414

4.之后出现的是工程文件夹、工程名、顶层模块名设置界面。按照之前的命名进行填写,第一栏选择工程文件夹“vga_exec1”,第二栏选择工程文件“vga_exec1.v”,最后一栏选择顶层模块名“vga_exec1”,然后点击”Next”,在出现的界面选择empty project。

图415

图416

5.之后是文件添加界面。在上方一栏中添加之前写的”vga_driver.v和vga_exec1.v”文件和生成的“my_pll.v”,点击右侧的“Add”按钮,之后文件还会出现在大方框中,之后点击“Next”。

图417

6.器件型号选择界面。在“Device family”处选择CycloneⅣE,在“Available devices”处选择EP4CE15F23C8,然后点击“Next”。

图418

7.EDA工具界面。该页面用默认的就行,直接点击最下方“Next”。

图419

8.之后出现的界面是我们前面的设置的总结,确认没有错误后点击“Finish”。

图420


4.2生成PLL IP核

新建工程后,就要生成PLL IP核。本节的PLL生成过程,与案例“VGA显示颜色”第四点综合工程和上板中的PLL内容一致,注意其中的地址有不同。


4.3综合

1.新建工程步骤完成后,就会出现以下界面。在“Project Navigator”下选中要编译的文件,点击上方工具栏中“Start Compilation”编译按钮(蓝色三角形)。

图421

2.编译成功后会出现以下界面,点击“OK”。

图422


4.4配置管脚

图423

在菜单栏中,选中Assignments,然后选择Pin Planner,就会弹出配置管脚的窗口。

图 424

在配置窗口最下方中的location一列,参考下表中最右两列配置好FPGA管脚。

器件

信号线

信号线

FPGA管脚

内部信号

U6,U7

SEG_E

SEG0

Y6

seg_ment[2]

SEG_DP

SEG1

W6

未用到

SEG_G

SEG2

Y7

seg_ment[0]

SEG_F

SEG3

W7

seg_ment[1]

SEG_D

SEG4

P3

seg_ment[3]

SEG_C

SEG5

P4

seg_ment[4]

SEG_B

SEG6

R5

seg_ment[5]

SEG_A

SEG7

T3

seg_ment[6]

DIG1

DIG_EN1

T4

seg_sel[0]

DIG2

DIG_EN2

V4

seg_sel[1]

DIG3

DIG_EN3

V3

seg_sel[2]

DIG4

DIG_EN4

Y3

seg_sel[3]

DIG5

DIG_EN5

Y8

seg_sel[4]

DIG6

DIG_EN6

W8

seg_sel[5]

DIG7

DIG_EN7

W10

seg_sel[6]

DIG8

DIG_EN8

Y10

seg_sel[7]

X1


SYS_CLK

G1

clk

K1


SYS_RST

AB12

rst_n

注意注意:与其他案例不同的是,VGA案例中所有管脚的电平必须选用为LVCMOS3.3,而不能是default。如下图所示。

配置完成后,关闭Pin Planner,软件自动会保存管脚配置信息。


4.5再次综合

图425

在菜单栏中,选中Processing,然后选择Start Compilation,再次对整个工程进行编译和综合。

图426

出现上面的界面,就说明编译综合成功。


4.6连接开发板

图中,下载器接入电脑USB接口,电源接入电源,vga线连接显示器,然后摁下电源开关,看到开发板灯亮。

图427


4.7上板

1.双击Tasks一栏中”Program Device”。

图 428

2.会出现如下界面,点击add file添加.sof文件,在右侧点击“Start”,会在上方的“Progress”处显示进度。

图429

3.进度条中提示成功后,即可在显示器上观察到相应的现象。





   拓展阅读