VGA原理及图片显示
前言
本文是在浙江大学计算机学院数字逻辑设计课程的大作业的实践过程中总结而出的,当时VGA学的很痛苦,网上的资料也很不全,找到的demo跑起来也都是报错,于是有了写这么一篇文章的想法,希望能对大家有所帮助
学习有感
关于VGA色彩显示以及图片显示部分的内容,尽管有老师和学长学姐们留下的资料,但由于demo的模块性不够明显和独立,描述的也不是很详细,因此跌跌撞撞地也踩了不少坑,这里我希望能够写出一份详尽完备的关于VGA和图片显示的资料,算是对这一阶段数逻大作业的一个总结,同时也希望能够让后来的学弟学妹们少走点弯路
本文提供的内容
本文内容主要分为两部分,关于VGA原理的说明以及给出样例和测试
这里我提供一份能够独立测试图片显示功能的代码样例demo(拿着一整个综合项目跑VGA功能确实很痛苦,这也是我把图像部分单独抽离出来的原因),这样就可以单独针对VGA部分进行调试测验,思路更清晰,问题定位也更准确,更重要的是,ISE综合所花费的时间会更短;
我提供的demo里还包含了一份仿真文件,稍后我会详细介绍该仿真文件的使用,以及如何借助该仿真文件来查看自己是否成功调用取到了IP核里的数据
VGA显示原理
VGA概念
什么是VGA?VGA不是用来显示的那块屏幕,而是用来传输信号的接口。VGA全称是Video Graphics Array,即视频图形阵列,是模拟信号的一种视频传输标准,VGA传输接口实物图如下:

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

VGA显示扫描原理
像素点rgb显示
相信大多数人和我一样,一开始接触VGA的时候,被这一大串复杂的介绍说明给吓蒙了。实际上,操作VGA的过程就是给你一块有横纵坐标范围的区域,区域上的每一个坐标点就是一个像素点,你可以做的事情是给这个像素点特定的rgb色彩,既可以通过自定义rgb,也可以去取图像某个像素点的rgb,这也就提供了你在VGA上画图以及显示图片的能力
理解了这件事情之后,就可以来详细地了解一下VGA的具体扫描显示的原理了
VGA分辨率及理解误区
我们一般使用的屏幕分辨率大小是640*480,也包括我们数逻实验室的板子。分辨率可以理解为像素点的个数,拿实验的板子为例,640*480的规格就是显示屏幕上每行有640个像素点,总共有480行。注意,一件很重要的事情是,虽然你看到的屏幕大小是640*480的,但是它的实际大小并不只有那么点,形象一点就是说,VGA扫描的范围是包含了你能够看到的640*480这一块区域的更大区域,他会在周围一圈你看不到的区域部分进行扫描,因此,我们在处理扫描信号的时候一定要注意只有扫描到有效区域的时候才能把像素点数据传给VGA显示
VGA 显示器扫描方式从屏幕左上角一点开始,从左向右逐点扫描,每扫描完一行,电子束回到屏幕的左边下一行的起始位置,在这期间,CRT 对电子束进行消隐,每行结束时,用行同步信号进行同步;当扫描完所有的行,形成一帧,用场同步信号进行场同步,并使扫描回到屏幕左上方,同时进行场消隐,开始下一帧。完成一行扫描的时间称为水平扫描时间,其倒数称为行频率;完成一帧(整屏)扫描的时间称为垂直扫描时间,其倒数称为场频率,即屏幕的刷新频率。其扫描示意图如下图所示

有效显示区域
关于那一块非有效显示区域,实际上是因为每一行和每一列的扫描区间都是由以下几部分组成:
行扫描:Hor Sync 、Hor Back Porch 、Hor Active Video和Hor Front Porch

Hor Scan Time是一个扫描周期,它会先扫描到Hor Sync、再扫描Hor Back Porch,然后才进入有效显示区Hor Active Video,最后是一段Hor Front Porch;可以看出来,四段区间只有Hor Active Video这一段是能够正常显示图像信息的,也就是屏幕上显示的那一块区间
列扫描也同理

给定参数
看到这里,你肯定会产生和我一样的念头,这么多参数,我怎么知道它的值是多少啊?实际上,根据固定的分辨率大小,比如我们实验室的板子使用的640*480的屏幕大小,这些非有效区间的长度值都是一个固定的常数,因此你只要在扫描的时候把这些常数稍加处理即可:
下图罗列了不同分辨率所对应的各个参数,其中的a、b、c、d、e、f、g、h、i、k的含义,与上面给出的两张图的标注一一对应;我们实验使用的是第一组数据,可以看到行时序参数中的c代表每一行的有效显示区间640,列时序参数中的h代表每一列的有效显示区间480

需要的扫描频率
很重要的一件事,无论是行扫描还是列扫描还是给像素点赋值,我们都需要用一个时钟,这个时钟不是系统时钟!!!,必须要先将50MHZ的时钟分频为25MHZ,然后拿去作为扫描信号的时钟
这一点是特别需要注意的,否则到时候会出现out of range的现象
前期准备:coe文件的生成(详细!)
coe和ip核介绍
我们调用VGA的终极目标是想要能够显示一张图片,在这之前,我们要先对这张图片进行一些预处理,因为我们是通过verilog语言去调用图片的数据的,但是verilog并不知道怎么从一张已有的图片上去取相应的数据
因此我们要先将图片转化成coe文件,coe文件实际上就是按照某种规律将图像每一个像素点的rgb数据都摆放成单独一行(一般我们习惯用16进制的方式来表述rgb数据),然后借助ISE的IP核生成将coe文件转化成ROM,生成的ROM.xco和ROM.v文件就可以被verilog语言所识别调用,最终就能够取到图像的数据了!
24位bmp图像转换为coe文件(附源代码)
为了统一规范,这里我们使用24位真彩色的bmp格式图像进行转换,好处是,bmp的三通道数据特征是非常明显的,易于处理
如何获取24位bmp图像
通过截图工具,一般获得的是.png图像,首先我们右键点击打开方式,用‘ 画图 ’打开png图像,选择文件->另存为->BMP图片

然后bmp格式也有很多种,如单色、16色、24色、256色等,这里我们统一选择24位位图选项,点击保存即可

使用matlab将24位真彩色bmp位图转化为coe文件
由于24位共rgb三个通道,每个通道分到的是8位,占用的资源太多,且老师给的ucf引脚约束对rgb每一个通道只给了4位,因此我们在转化为coe文件的同时,要将每个通道压缩成4位,即整张图像同时转化为12位
压缩图像是一件非常重要的事情,因为经过后续的实践我们发现,板子内存资源有限,无法同时加载多张大图
为了能够对多张图像进行批量处理,我使用matlab写了一个img2coe.m
的函数,只要对bmp图像调用这个函数即可转换成coe文件了,转化过程介绍如下:
先将rgb提取为三个通道,用reshape函数对转置后的矩阵进行重组,对rgb三个分量的数据都右移4位,舍去细节,留下高四位作为最终的数据,然后写入到coe文件中
matlab源代码如下:
function img2coe(path,name)
% 利用imread函数把图片转化为一个三维矩阵
image_array = imread(path);
% 利用size函数把图片矩阵的三个维度大小计算出来
% 第一维为图片的高度,第二维为图片的宽度,第三维为图片的RGB分量
[height,width,z]=size(image_array);
red = image_array(:,:,1); % 提取红色分量,数据类型为uint8
green = image_array(:,:,2); % 提取绿色分量,数据类型为uint8
blue = image_array(:,:,3); % 提取蓝色分量,数据类型为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(1,height*width);
% 因为导入的图片是24-bit真彩色图片,每个像素占用24-bit,其中RGB分别占用8-bit
% 而我这里需要的是12-bit,其中R为4-bit,G为4-bit,B为4-bit,所以需要在这里对24-bit的数据进行重组与拼接
% bitshift()函数的作用是对数据进行移位操作,其中第一个参数是要进行移位的数据,第二个参数为负数表示向右移,为
% 正数表示向左移,更详细的用法直接在Matlab命令窗口输入 doc bitshift 进行查看
% 所以这里对红色分量先右移4位取出高4位,然后左移11位作为ROM中RGB数据的第11-bit到第8-bit
% 对绿色分量先右移4位取出高4位,然后左移5位作为ROM中RGB数据的第7-bit到第4-bit
% 对蓝色分量先右移4位取出高4位,然后左移0位作为ROM中RGB数据的第3-bit到第0-bit
for i = 1:height*width
rgb(i) = bitshift(bitshift(r(i),-3),11) + bitshift(bitshift(g(i),-2),5) + bitshift(bitshift(b(i),-3),0);
end
fid = fopen( name , '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(1:end-1));
% 把rgb数据的最后一个数据写入.coe文件中,并用分号结尾
fprintf( fid, '%x;',rgb(end));
fclose( fid ); % 关闭文件指针
end
调用方式也很简单,把img2coe.m
文件放在和你的bmp文件同一个目录下,然后调用img2coe('文件名.bmp', '目标文件名.coe');
即可
中期准备:coe转换成ROM
经过上面的步骤我们已经得到了coe文件
接下来就是把它转化成verilog能够调用的ROM文件了
- 在ISE工程中,点击右键新建文件,选择IP核生成

- 选择Memories选项中的RAMs&ROMs里的Block Memory Generator

- 接下来会进入这个界面,一路点击next

-
然后进入到这个设置参数的界面
其中width代表你设置的图片数据是几进制,由于之前写coe的时候是用16进制写的,因此这里写16;depth表示图像的大小,即长乘宽,由于我使用的是640*480的图片,因此这里填207200(=640*480)

- 然后进入这个页面,点击load init file,选择你的coe文件即可;值的说明的是,你可以看到我这里出现了未响应的现象,不要以为出什么错了,这是正常的,光是导入个路径就要花半天时间…

- 当你的文件路径成功出现的时候,你就可以点击下方的generate生成了,当然这个generate也要很久很久,一般一张640*480的图片要generate近一个小时左右才能生成

-
生成完毕的标志是:Creating结束,并且在你的目录下出现类似于太阳的这个标记
后期调用与测试
调用方式很简单,从文件夹里把.xco文件导入即可,读取数据使用:
ROM_name m0(
.clka(R_clk_25M), // input clk_25MHZ
.addra(R_rom_addr), // input [18 : 0] address
.douta(W_rom_data) // output [11 : 0] data
);
关于单个测试图片,我提供了一个叫做imageProcess的工程样例,它可以单独测试单张图片的好坏,源代码如下:
top文件
module Top(
input I_clk , // 系统50MHz时钟
input I_rst_n , // 系统复位
output [3:0] O_red , // VGA红色分量
output [3:0] O_green , // VGA绿色分量
output [3:0] O_blue , // VGA蓝色分量
output O_hs , // VGA行同步信号
output O_vs // VGA场同步信号
);
//分频系统时钟,生成25MHZ的时钟
reg R_clk_25M;
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
R_clk_25M <= 1'b0;
else
R_clk_25M <= ~R_clk_25M;
end
reg [11:0] vga_data;//vga颜色显示
wire [9:0] col_addr;//x的值
wire [8:0] row_addr;//y的值
reg [18:0] R_rom_addr; // ROM的地址,必须要比总像素点个数大,否则会出现out of range
wire [11:0] W_rom_data; // ROM中存储的数据,总共存储12位
//调用vga模块输出r、g、b和行同步信号、场同步信号
vga vga_test(.vga_clk(R_clk_25M),.clrn(I_rst_n),.d_in(vga_data),.row_addr(row_addr),.col_addr(col_addr),.r(O_red),.g(O_green),.b(O_blue),.hs(O_hs),.vs(O_vs));
//在要显示的区域显示图片,需要先获取图片地址R_rom_addr
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
R_rom_addr <= 19'd0;
else if(col_addr>=0&&col_addr<=639&&row_addr>=0&&row_addr<=479)
begin //这里的作用是背景图全屏显示
if(R_rom_addr == 307199)
R_rom_addr <= 19'd0;
else
R_rom_addr <= row_addr*40+col_addr;
end
// 这里以(x,y)为图片左上角的坐标为例来显示图片,注意这里(x,y)的范围是x:0~639; y:0~479
// begin
// if(R_rom_addr == imgSize) //imgSize是图像大小-1,即height*width-1
// R_rom_addr <= 19'd0;
// else if(col_addr>=x && col_addr<=x+img_width-1 && row_addr>=y && row_addr <= y+img_height-1)
// R_rom_addr <= R_rom_addr+1'd1;
// end
end
//根据已经拿到的图片像素点地址R_rom_addr取对应的数据R_rom_data
ROM_success success(
.clka(R_clk_25M), // input clk_25MHZ
.addra(R_rom_addr), // input [18 : 0] address
.douta(W_rom_data) // output [11 : 0] data
);
//将整个显示区数据给vga_data,既包括图片显示区,也包括其他区域
always @(posedge R_clk_25M or negedge I_rst_n)
begin
if(!I_rst_n)
vga_data<=12'b0;
else if(col_addr>=0&&col_addr<=639&&row_addr>=0&&row_addr<=479)
vga_data<=W_rom_data[11:0];
else
vga_data<=12'b1;
end
endmodule
vga模块如下:
module vga (vga_clk,clrn,d_in,row_addr,col_addr,rdn,r,g,b,hs,vs); // vgac
input [11:0] d_in; // bbbb_gggg_rrrr, pixel
input vga_clk; // 25MHz
input clrn;
output reg [8:0] row_addr; // pixel ram row address, 480 (512) lines
output reg [9:0] col_addr; // pixel ram col address, 640 (1024) pixels
output reg [3:0] r,g,b; // red, green, blue colors
output reg rdn; // read pixel RAM (active_low)
output reg hs,vs; // horizontal and vertical synchronization
// h_count: VGA horizontal counter (0-799)
reg [9:0] h_count; // VGA horizontal counter (0-799): pixels
always @ (posedge vga_clk or negedge clrn)
begin
if (!clrn)
h_count <= 10'h0;
else if (h_count == 10'd799)
h_count <= 10'h0;
else
h_count <= h_count + 10'h1;
end
// v_count: VGA vertical counter (0-524)
reg [9:0] v_count; // VGA vertical counter (0-524): lines
always @ (posedge vga_clk or negedge clrn)
begin
if (!clrn)
v_count <= 10'h0;
else if(v_count == 10'd524)
v_count <= 10'h0;
else if(h_count == 10'd799)
v_count <= v_count + 10'h1;
else
v_count<=v_count;
end
// signals, will be latched for outputs
wire [8:0] row = v_count - 10'd35; // pixel ram row addr
wire [9:0] col = h_count - 10'd143; // pixel ram col addr
wire h_sync = (h_count > 10'd95); // 96 -> 799
wire v_sync = (v_count > 10'd1); // 2 -> 524
wire read = (h_count > 10'd142) && // 143 -> 782
(h_count < 10'd783) && // 640 pixels
(v_count > 10'd34) && // 35 -> 514
(v_count < 10'd515); // 480 lines
// vga signals
always @ (posedge vga_clk or negedge clrn)
begin
if (!clrn)
begin
row_addr <= 9'b0; // pixel ram row address
col_addr <= 10'b0; // pixel ram col address
rdn <= 1'b1; // read pixel (active low)
hs <= 1'b0; // horizontal synchronization
vs <= 1'b0; // vertical synchronization
r <= 4'b0; // 3-bit red
g <= 4'b0; // 3-bit green
b <= 4'b0; // 2-bit blue
end
else
begin
row_addr <= row[8:0]; // pixel ram row address
col_addr <= col[9:0]; // pixel ram col address
rdn <= ~read; // read pixel (active low)
hs <= h_sync; // horizontal synchronization
vs <= v_sync; // vertical synchronization
r <= rdn ? 4'h0 : d_in[11:8]; // 3-bit red
g <= rdn ? 4'h0 : d_in[7:4]; // 3-bit green
b <= rdn ? 4'h0 : d_in[3:0]; // 2-bit blue
end
end
endmodule
ucf引脚约束如下:
#VGA
NET "O_blue[0]" LOC = T20 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_blue[1]" LOC = R20 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_blue[2]" LOC = T22 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_blue[3]" LOC = T23 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_green[0]" LOC = R22 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_green[1]" LOC = R23 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_green[2]" LOC = T24 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_green[3]" LOC = T25 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_red[0]" LOC = N21 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_red[1]" LOC = N22 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_red[2]" LOC = R21 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_red[3]" LOC = P21 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_hs" LOC = M22 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "O_vs" LOC = M21 | IOSTANDARD = LVCMOS33 | SLEW = FAST ;
NET "I_clk" LOC = AC18 | IOSTANDARD = LVCMOS18;
NET "I_rst_n" LOC = AA10 | IOSTANDARD = LVCMOS15;
通过以上的代码,你就可以单独测试某张图片的效果了!
数据仿真
由于实验室并不是什么时候都开放的,且板子上跑一次时间很长,我们更希望能够借助仿真来看一下程序是否真的读取到了图像数据,因此我设计了一个仿真文件
激励代码如下:
module top_sim;
// Inputs
reg I_clk;
reg I_rst_n;
// Outputs
wire [3:0] O_red;
wire [3:0] O_green;
wire [3:0] O_blue;
wire O_hs;
wire O_vs;
// Instantiate the Unit Under Test (UUT)
Top uut (
.I_rst_n(I_rst_n),
.I_clk(I_clk),
.O_red(O_red),
.O_green(O_green),
.O_blue(O_blue),
.O_hs(O_hs),
.O_vs(O_vs)
);
initial begin
I_rst_n=1; #30;
I_rst_n=0; #30;
I_rst_n=1; #30;
I_rst_n=0; #30;
I_rst_n=1; #30;
I_rst_n=0; #30;
I_rst_n=1; #30;
end
always begin
I_clk =0 ; #2;
I_clk =1 ; #2;
end
endmodule
查看仿真结果:

从该图中,对照着取到的rgb值,可以与图像的rgb值一一对应,说明程序没有问题,成功取到了图像的数据
结语
VGA显示的过程中会遇到很多小bug,有些bug的原因你根本找不出,比如out of range,你只能静下心来慢慢调试仿真,看看数据对不对,再回过头来看看自己写的代码有没有出现问题;到了后来图片显示正常了,结果板子资源不够存放你那么多图片,该怎么办?这时候就要尝试压缩分辨率取解决问题了…
总之,不同的人会遇到不同的问题,写下这篇文章,希望能够给后面的学弟学妹们一个借鉴吧