本连载学习效果:不仅看能懂代码,还能知道每一行代码怎么写,怎么设计。
第十二章 VGA显示图片
本文的文档编号:001700000023
本文档没有对应的视频教程
1、至简原理与应用配套的案例和PPT讲解
2本案例要实现的效果是通过VGA,以640*480的分辨率在显示器中心显示120*55的图片图片数据存在ROM IP核内。步骤性教学;
3、这是Altera和Xilinx入门学习案例文档
1 项目背景
1.1 FPGA存储器
目前大多数FPGA都有内嵌的块RAM(Block RAM),可以将其灵活地配置成单端口RAM(DPRAM,Single Port RAM)、双端口RAM(DPRAM,Double Ports RAM)、伪双端口RAM(Pseudo DPRAM)、CAM(Content Addressable Memory)、FIFO等常用存储结构。FPGA中其实并没有专用的ROM硬件资源,实现ROM的思路是对RAM赋予初值,并保持该初值。
Altera的器件内部提供了各种存储器模块(RAM、ROM或双口RAM),可以在设计中使用MegaWizard Plug-In Manager,执行【Tools】|【MegaWizard Plug-In Manager】菜单命令来创建所需要的存储器模块。也可以使用Altera 提供的宏功能模块LPM_ROM来创建存储器模块。 每个ROM模块有CLOCK(时钟)、address(地址)这两个输入信号和一个q(值)输出信号。 ROM在每个时钟上升沿取出由地址信号所指定的存储单元中的值并输出。ROM内的值通过加载MIF (Memory Initialization File,存储器初始化文件)文件来实现。
当在设计中使用了器件内部的存储器模块时,需要对存储器模块进行初始化。在Quartus Ⅱ中,可以使用两种格式的存储器初始化文件:Intel Hex格式(.hex)或Altera存储器初始化格式(.mif)的文件。 MIF文件是Altera存储器类器件初始化的专用文件格式,文件内容为地址与值的对应表,规定了存储器单元的初始值。
如果将要存储于ROM中的内容比较少或者很有规律,可以执行【File】|【New…】菜单命令,创建MIF文件并编辑其内容。如果已经有BMP格式的图片,则可以使用我们提供的BmpToMif这个软件,从现有的BMP格式图片生成MIF文件。其使用非常简单,注意要适当调整原图片的大小,这可以通过各种图形编辑软件修改,如Windows自带的画图程序、Photoshop等。BmpToMif软件的功能有:
① 将bmp图片转为mif文件:将黑白图片转换为单色mif文件;将彩色图片转换为三色mif文件。
② 将二进制文件转为mif文件,如将中英文点阵字库转换为mif文件。
ROM IP核的生成方法[此处应该加上ROM的介绍,以及MIF文件的介绍。]
1.2 图片变成MIF文件的方法
1.我们需要用到Img2Lcd软件,此软件可自行下载,无需安装即可使用。
2 设计目标
通过VGA连接线,将显示器和教学板的VGA接口相连。连接示意图如下。
分辨率 |
行/列 |
同步脉冲 |
显示后沿 |
显示区域 |
显示前沿 |
帧长 |
单位 |
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 |
行 |
图像的内容是:在屏幕的中央显示一个明德扬的LOGO。明德扬LOGO的大小是120*60像素。除了图片之外的显示区域,则显示白色。上板效果图如下图所示(显示器不同,显示效果也会有差别,请注意)。
1 2 |
|
ROM的每一个像素,按RGB565的方式保存,也就是[15:11]表示红基色,[10:5]表示绿基色,[4:0]表示蓝基色。
3 设计实现
3.1 顶层接口
新建目录:D:mdy_bookpicture_new_borad。在该目录中,新建一个名为picture_new_borad.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 |
1 2 3 4 5 6 7 |
module picture_new_borad ( clk , rst_n , lcd_hs , lcd_vs , lcd_rgb ); |
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来产生了。
整个工程的结构图如下。
PLL的生成方式过程,请看本案例的综合工程和上板一节的内容。
3.3 VGA驱动模块设计
3.3.1 接口信号
在目录:D:mdy_book picture_new_borad中,建立一个rectangle.v文件,并用GVIM打开,开始编写代码。
我们新建一个GVIM文件,并且将文件保存为vga_driver.v。
我们先分析功能。要控制显示器,让其产生红色,也就是让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 ); |
1 2 3 4 5 |
input clk ; input rst_n ; output hys ; output vys ; output [15:0] lcd_rgb ; |
3.3.2 信号设计
我们先设计场同步信号hys,VGA时序中的场同步信号,其时序图如下:
分辨率 |
行/列 |
同步脉冲 |
显示后沿 |
显示区域 |
显示前沿 |
帧长 |
单位 |
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 |
行 |
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; |
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 |
将时间信号填入图中,更新后的时序图如下:
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; |
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 |
显示区域:(h_cnt >=(96+48)&& h_cnt <(96+48+640)),并且(v_cnt>=(2+33) && v_cnt<(2+33+480))
图片区域:(h_cnt >=(96+48+320-60)&& h_cnt <(96+48+320+60)),并且(v_cnt>=(2+33+240-27) && v_cnt<(2+33+240+28))
白色区域:在显示区域中,非图片区域的,就是白色区域。
非显示区域:显示区域之外的,就是非显示区域。
我们可以设计几个信号来表示这些区域。显示区域用valid_area=1表示,图片区域用rom_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 |
非显示区域(valid_area=0),lcd_rgb输出“16’b0”;
显示区域(valid_area)中的图片区域(rom_area=1),lcd_rgb输出为图片的像素值。但问题来了,图片的像素值哪里来?来自于ROM,我们怎么使用ROM?将这个ROM例化,下面是例化的代码。
代码中,address、clock和q是rom1内部的信号,而rom_addr、clk和rom_data是VGA驱动模块的信号。上面的例化意思是,将rom1的信号address连到VGA驱动模块的rom_addr信号上;将rom1的信号clock连到VGA驱动模块的clk信号上;将rom1的信号q连到VGA驱动模块的rom_data信号上。想象一下,现在要在一台电视机内部要安装一个电路板。这个电路板的自己命名的接口有address、clock和q。这台电视机里面有三种线,分别是rom_addr,clk和rom_data。我们把rom_addr插到电路板address接口上,把clk插到电路板的clock接口上,把rom_data插到电路板的q接口上,这样就完成了安装。
我们知道,通过控制ROM的地址,就能让ROM输出对应地址的数据。ROM输出的信号是rom_data。所以,显示区域(valid_area)中的图片区域(rom_area=1),lcd_rgb输出为图片的像素值,也就是rom_data的值,即lcd_rgb=rom_data。至于如何控制地址rom_addr,先不考虑,继续完成lcd_rgb信号设计。
显示区域(valid_area)中的非图片区域(rom_area=0),lcd_rgb输出白色16’h11111_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(rom_area)begin lcd_rgb <= rom_data; end else begin lcd_rgb <= 16'b11111_111111_11111; end end else begin lcd_rgb <= 0; end end |
但我们要注意到ROM的时序,rom_data会比rom_addr滞后一个时钟的。这会导致什么问题呢?
遇到此种问题时,我们就需要调整时序。其中一种方法是是调整rom_addr,让它提前一个时钟产生,而其他信号都保持不变。更新后的波形如下图。
所以rom_addr所对应的代码如下:
1 2 3 |
always@(*)begin rom_addr = (h_cnt-96-48-320+60-1) + 120*(v_cnt-2-33-240+27)。 end |
3.3.3 信号定义
接下来定义信号类型。
h_cnt是用always产生的信号,因此类型为reg。h_cnt计数的最大值为800,需要用10根线表示,即位宽是10位。因此代码如下:
1 |
reg [9:0] h_cnt ; |
1 2 |
wire add_h_cnt; wire end_h_cnt; |
1 |
reg [9:0] v_cnt ; |
1 2 |
wire add_v_cnt; wire end_v_cnt; |
1 |
reg [15:0] lcd_rgb; |
1 2 |
reg hys ; reg vys ; |
distance是用always方式设计的,因此类型为reg。其位宽为20位,需要20根线表示。因此代码如下:
valid_area和rom_area是用always方式设计的,因此类型为reg。并且其值是0或1,用一根线表示即可。因此代码如下:
1 2 |
reg valid_area ; reg rom_area; |
rom_addr是用always方式设计的,因此类型为reg。其表示范围是0~6599,需要位宽为13位,需要13根线表示。因此代码如下:
1 |
reg [12:0] distance ; |
rom_data是例化模块的输出,不是用always方式设计的,因此类型为wire。其位宽为16位,需要16根线表示。因此代码如下:
1 |
reg [15:0] rom_data ; |
所以整个模块的代码如下
1 2 |
|
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 |
wire lcd_hs ; wire lcd_vs ; |
lcd_rgb是在例化文件中,因此类型为wire。它的位宽是16位的,用16根线表示即可。因此代码如下:
1 |
wire [15:0] lcd_rgb ; |
至此,整个代码的设计工作已经完成。下一步是新建工程和上板查看现象。
4 综合工程和上板
4.1 新建工程
1.首先在d盘中创建名为“picture_new_borad”的工程文件夹,将写的代码命名为“vga_drive.v”,顶层模块名为“vga_drive”,例化文件命名为“vga_exec7.v”。图片生成的数据文件名为“1.mif”,“vga_pll.v”为时钟分频ip核,由我们提供。
4.2 生成PLL IP核
新建工程后,就要生成PLL IP核。本节的PLL生成过程,与案例“VGA显示颜色”第四点综合工程和上板中的PLL内容一致,注意其中的地址有不同。
1.在界面右侧IP Catalog处搜索ROM。
1.在“Project Navigator”下选中要编译的文件,点击上方工具栏中“Start Compilation”编译按钮(蓝色三角形)。
4.5 配置管脚
器件 |
信号线 |
信号线 |
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 |
配置完成后,关闭Pin Planner,软件自动会保存管脚配置信息。
4.6 再次综合
出现上面的界面,就说明编译综合成功。
图中,下载器接入电脑USB接口,电源接入电源,vga线连接显示器,然后摁下电源开关,看到开发板灯亮。
4.8 上板
1.双击Tasks一栏中”Program Device”。