文章目录
前言
前段时间用开发板玩了玩VGA,显示了一些彩条、汉字、图片等,后期想用VGA做一些复杂点的东西,比如贪吃蛇游戏等,暂且先总结一下这几个显示小实验,其中详细一下谈图片显示的部分,用到了片内的ROM_IP核
---------此篇文章主要内容:FPGA-VGA学习笔记
一、VGA是什么
VGA(Video Graphics Array):视频图像阵列,一种使用模拟信号进行视频传输的标准。
- 接口定义
VGA接口定义如下:
引脚定义 | 引脚名称 | 功能说明 |
---|---|---|
Pin1 | RED | 红色 |
Pin2 | GREEN | 绿色 |
Pin3 | BLUE | 蓝色 |
Pin4 | ID2 | 地址码2 |
Pin5 | GND | 行同步地 |
Pin6 | RGND | 红色地 |
Pin7 | GGND | 绿色地 |
Pin8 | BGND | 蓝色地 |
Pin9 | KEY | 保留 |
Pin10 | GND | 场同步地 |
Pin11 | ID0 | 地址码0 |
Pin12 | ID1 | 地址码1 |
Pin13 | HSYNC | 行同步 |
Pin14 | VSYNC | 场同步 |
Pin15 | ID3 | 地址码3 |
注: 在引脚1、2、3上输出视频的三原色,是一个模拟信号,信号的电压变化范围在0-0.714V之间,可将数字信号进行数模转换,以模拟信号输出;引脚13、14可接TTL电平,即可以直接连接数字信号输出。
二、VGA–工作流程
1、VGA扫描方式
扫描从屏幕的左上方开始,从左到右,从上到下进行扫描,每扫描完一行,电子束都回到屏幕的左边下一行的起始位置,在这期间,CRT对电子束进行消隐。每行结束时,用行同步信号进行同步;扫描完所有行(整屏一帧图像),用场同步信号进行场同步,并使扫描回到屏幕的左上方,同时进行场消隐,预备下一场的扫描。
2、VGA标准时序
一帧图像的显示不仅包括有效数据段,还要包括同步阶段、显示后沿、显示前沿等。
- 行同步时序
parameter H_SYNC = 10'd96; // 行同步
parameter H_BACK = 10'd48; // 行显示后沿
parameter H_DISP = 10'd640; // 行有效数据
parameter H_FRONT = 10'd16; // 行显示前沿
parameter H_TOTAL = 10'd800; // 行扫描周期
- 场同步时序
parameter V_SYNC = 10'd2; // 场同步
parameter V_BACK = 10'd33; // 场显示后沿
parameter V_DISP = 10'd480; // 场有效数据
parameter V_FRONT = 10'd10; // 场显示前沿
parameter V_TOTAL = 10'd525; // 场扫描周期
3、VGA常用分辨率
- 以640x480@60分辨率为例:
640表示每行640个像素点,480表示480行,60表示VGA显示每秒刷新60次(每秒显示60帧图像),25.175表示VGA的驱动时钟为25.175MHz。
VGA工业标准所要求的频率为:
名称 | 频率 |
---|---|
时钟频率 | 25.175MHz |
行频率 | 31469Hz |
场频率 | 59.94Hz |
VGA工业标准显示模式要求:行同步、场同步都为负极性,即同步脉冲通过要求是负脉冲。
其中,25.175MHz是这样计算的:800x525x59.94=25.175MHz,计算中包括了行消隐和场消隐。
当我们采用25MHz时钟驱动VGA时:显示器的刷新频率为25MHz/(800x525)=59.52Hz,接近工业标准场频率59.94Hz。
注:我们使用 25MHz 的时钟代替 25.175MHz 的时钟,不会对实验造成影响。
4、VGA工作流程小结
代码设计中,VGA在每一帧图像显示时,可用两个计数器进行计数(行、场扫描计数器),行计数器的驱动时钟为25MHz(可由PLL_IP核分频得到),场计数器的驱动时钟为行计数的溢出信号。计数器同时控制行、场同步信号输出,并在适当的时候送出像素点数据,就可以了显示相应的图像。
其实,简单来说,就是在显示的有效区内,根据VGA时钟(采用25MHz),按照从左到右,由上至下的扫描顺序,在适当的VGA时钟周期依次填充每个像素点的颜色数据值。
三、FPGA–VGA硬件设计
VGA显示实验基于DE2-115开发板,其VGA硬件部分如下:
DE2-115开发板提供了一个24位高质量VGA视频输出模块,该模块使用专用是DAC芯片ADV7123,实现24位动态色彩输出。
- ADV7123部分引脚介绍:
引脚 | 描述 |
---|---|
VGA_R[0-7] | 红色信号 |
VGA_G[0-7] | 绿色信号 |
VGA_B[0-7] | 蓝色信号 |
VGA_CLK | VGA驱动时钟 |
VGA_SYNC_N | 同步控制信号输入,当SYNC为低电平时,关闭IRE电流源,该信号仅允许在行消隐期间被设置。如果绿色通道不需要同步信号,可以将该信号接到低电平(这里不需要,直接接低电平) |
VGA_BLANK_N | 消隐控制信号,当该信号为0时,所有的RGB输入数据将被忽略,因此如果希望正常输出VGA信号,则要保证在数据区,BLANK信号为高电平 |
另外,FPGA芯片与VGA接口直接相连了行同步信号与场同步信号。
引脚 | 描述 |
---|---|
VGA_ HS | 行同步信号 |
VGA_ VS | 场同步信号 |
四、FPGA–VGA程序设计
1、VGA驱动模块
VGA驱动模块主要完成:VGA时序的控制、输出扫描到当前像素点的X、Y坐标、输入要显示的像素点数据,并驱动VGA进行图像显示。(采用RGB888格式)
- 此模块的输入输出接口:
input vga_clk_25, // VGA驱动时钟---25MHz
input rst_n, // 复位信号(低有效)
input [23:0] pixel_data, // RGB888要显示的像素点数据
output [23:0] vga_rgb, // RGB888红绿蓝三原色输出
output vga_hs, // 行同步信号
output vga_vs, // 场同步信号
output vga_blank, // 场消隐信号
output [ 9:0] pixel_xpos, // 像素点横坐标
output [ 9:0] pixel_ypos // 像素点纵坐标
-行、场同步信号:
// H_cnt 为行计数器; H_SYNC = 10'd96;
// V_cnt 为场计数器; V_SYNC = 10'd2;
assign vga_hs = (H_cnt >= H_SYNC) ? 1'b1 : 1'b0; // 每个行周期起始时:VGA_HS为96个(0-95)低电平VGA时钟周期
assign vga_vs = (V_cnt >= V_SYNC) ? 1'b1 : 1'b0; // 每个场周期起始时:VGA_VS为2个(0-1)低电平的行溢出周期
-行、场计数器:
// 行计数器,其中---H_TOTAL = 10'd800;->行扫描周期
always @(posedge vga_clk_25 or negedge rst_n)
begin
if(!rst_n) begin
H_cnt <= 10'd0;
end
else begin
if(H_cnt == H_TOTAL - 10'd1)
H_cnt <= 10'd0;
else
H_cnt <= H_cnt + 10'd1;
end
end
// 场计数器,其中---V_TOTAL = 10'd525;->场扫描周期
always @(posedge vga_clk_25 or negedge rst_n)
begin
if(!rst_n) begin
V_cnt <= 10'd0;
end
else if(H_cnt == H_TOTAL - 10'd1) begin
if(V_cnt == V_TOTAL - 10'd1)
V_cnt <= 10'd0;
else
V_cnt <= V_cnt + 10'd1;
end
else
V_cnt <= V_cnt;
end
-在图像有效的显示区,输出有效像素数据:
// vga_en在图像有效显示区为1,在消隐期间为0
assign vga_en = (((H_cnt >= (H_SYNC+H_BACK)) && (H_cnt < (H_TOTAL-H_FRONT)))
&& ((V_cnt >= (V_SYNC+V_BACK)) && (V_cnt < (V_TOTAL-V_FRONT))))
? 1'b1 : 1'b0;
assign vga_blank = vga_en; // 有效区时,消隐控制信号置1,正常输出VGA信号;其余,置0,忽略RGB输入的数据
assign vga_rgb = vga_en ? pixel_data : 24'd0; // 有效显示区,输出像素数据
-返回像素点坐标值:
// 这里对于每行的X坐标信号提前了一个VGA时钟周期发出申请,为其他模块在判断使用X、Y坐标,并送出像素点数据时提供了一个VGA时钟周期的缓冲
assign pixel_req = (((H_cnt >= (H_SYNC+H_BACK-10'd1)) && (H_cnt < (H_TOTAL-H_FRONT-10'd1)))
&& ((V_cnt >= (V_SYNC+V_BACK)) && (V_cnt < (V_TOTAL-V_FRONT))))
? 1'b1 : 1'b0;
assign pixel_xpos = pixel_req ? (H_cnt - (H_SYNC+H_BACK-10'd1)) : 10'd0;
assign pixel_ypos = pixel_req ? (V_cnt - (V_SYNC+V_BACK)) : 10'd0;
2、VGA驱动模块的Modelsim仿真
-Testbench设计:
`timescale 1 ns/ 1 ns
module VGA_Driver_tb();
// 输入变量
reg vga_clk_25;
reg rst_n;
reg [23:0] pixel_data;
// 输出变量
wire vga_blank;
wire vga_hs;
wire [23:0] vga_rgb;
wire vga_vs;
wire [ 9:0] pixel_xpos;
wire [ 9:0] pixel_ypos;
// 接收到指定显示区范围使能信号
wire recv_en;
localparam WHITE = 24'b11111111_11111111_11111111; // RGB888 白色
localparam BLANK = 24'b00000000_00000000_00000000; // RGB888 黑色
localparam RED = 24'b11111111_00000000_00000000; // RGB888 红色
localparam GREEN = 24'b00000000_11111111_00000000; // RGB888 绿色
localparam BLUE = 24'b00000000_00000000_11111111; // RGB888 蓝色
VGA_Driver u_VGA_Driver(
.vga_clk_25 (vga_clk_25),
.rst_n (rst_n ),
.pixel_data (pixel_data),
.vga_rgb (vga_rgb ),
.vga_hs (vga_hs ),
.vga_vs (vga_vs ),
.vga_blank (vga_blank ),
.pixel_xpos (pixel_xpos),
.pixel_ypos (pixel_ypos)
);
initial
begin
vga_clk_25 = 1'b0;
rst_n = 1'b0;
#80 rst_n = 1'b1;
#100000000 $stop;
end
always #20 vga_clk_25 = ~vga_clk_25;
// 设置0-9行,每行1-9像素点位置为有效显示区
assign recv_en = ( ((pixel_xpos >= 10'd1) && (pixel_xpos < 10'd10))
&& ((pixel_ypos >= 10'd0) && (pixel_ypos < 10'd10)) )
? 1'b1 : 1'b0;
// 在显示有效区中,进行有效像素点输出
always @(posedge vga_clk_25 or negedge rst_n)
begin
if(!rst_n)
pixel_data <= BLANK;
else begin
if(recv_en)
pixel_data <= WHITE;
else
pixel_data <= GREEN;
end
end
-modelsim仿真波形:
可观察到,在Testbench中收到X、Y坐标后,经过一个VGA时钟周期的后,才输出了有效的像素点数据,故在VGA驱动模块中提前一个VGA时钟周期送出坐标值(主要是X坐标),方便其他模块捕捉坐标值处理并将像素数据回传给VGA驱动模块,在图像有效区进行像素点坐标对应显示。
简单来说,比如模块A要在坐标X=10,Y=23,这个像素点位置进行颜色显示。若模块A用always语句判断已经到了这个像素点的坐标位置,即(pixel_xpos >= 10'd10) && (pixel_xpos >= 10'd23)
, 直接进行像素数据输出,则正好对应驱动模块要输出图像位置(10,23)的像素点数据。
注意:如果模块A全都使用assign语句进行坐标位置的判断和像素数据输出,则将不存在一个VGA时钟周期的处理时间,在VGA驱动模块设计中,可去除提前一个VGA时钟周期的坐标申请。即
// 去除了提前一个VGA时钟周期发起坐标申请
assign pixel_req = (((H_cnt >= (H_SYNC+H_BACK)) && (H_cnt < (H_TOTAL-H_FRONT)))
&& ((V_cnt >= (V_SYNC+V_BACK)) && (V_cnt < (V_TOTAL-V_FRONT))))
? 1'b1 : 1'b0;
assign pixel_xpos = pixel_req ? (H_cnt - (H_SYNC+H_BACK)) : 10'd0;
assign pixel_ypos = pixel_req ? (V_cnt - (V_SYNC+V_BACK)) : 10'd0;
五、FPGA–VGA图片显示实验
- 任务:使用开发板通过VGA接口在显示屏上显示一个100x100大小的图片
- 步骤:首先将图片数据初始化到FPGA内嵌的ROM中,然后在选定的显示区处读取ROM中的数据,送到VGA驱动模块进行显示
1、ROM_IP核的创建
- 此次图显实验创建一个单端口ROM_IP核,并创建一个读使能信号rden
输出位宽:使用RGB888格式,故位宽选择24bits
存储深度:要显示图片为100x100,即共需存储10000个像素点数据,选择10000以上即可,在此选择16384words
存储类型:保持默认Auto
时钟类型:单时钟
创建一个读使能信号,且输出无需寄存
把图片文件转换成mif格式,放入ROM的初始化中
注意:若要对ROM_IP核进行Modelsim仿真,则要把mif文件放到工程文件目录下,否则仿真时ROM的输出将会一直是0
2、图片显示模块代码设计
- 在此模块中例化刚才创建的ROM_IP核
pic_rom u_pic_rom(
.clock (vga_clk_25),
.address (rom_addr ), // 读ROM地址
.rden (rom_rd_en ), // 读ROM使能信号
.q (rom_data ) // ROM输出数据
);
- 扫描到图片显示区时,拉高读使能信号
// START_X ->图片区域起始横坐标; START_Y ->图片区域起始纵坐标
// PHOTO_H ->待显图片的水平宽度; PHOTO_V ->待显图片的垂直高度
assign rom_rd_en = ( ((pixel_xpos >= START_X) && (pixel_xpos < START_X + PHOTO_H))
&& ((pixel_ypos >= START_Y) && (pixel_ypos < START_Y + PHOTO_V)) )
? 1'b1 : 1'b0;
- 控制ROM读数据的地址
// 读地址范围0-9999; PHOTO = 14'd10000; ->图片区域总像素数
always @(posedge vga_clk_25 or negedge rst_n)
begin
if(!rst_n)
rom_addr <= 14'd0;
else begin
if(rom_rd_en) begin
if(rom_addr == PHOTO - 1'b1)
rom_addr <= 14'd0; // 读到ROM有效末地址后,从首地址重新开始读操作
else
rom_addr <= rom_addr + 14'd1; // 每次ROM数据后,读地址+1
end
else
rom_addr <= rom_addr;
end
end
- 对读使能信号延时一个VGA时钟周期
// 由于对ROM写入读地址和读使能后,数据会在下一个VGA时钟周期输出
// 故此对读使能信号也延迟一个VGA时钟周期,使rom_valid与ROM有效数据输出的时钟保持一致
always @(posedge vga_clk_25 or negedge rst_n)
begin
if(!rst_n)
rom_valid <= 1'b0;
else
rom_valid <= rom_rd_en;
end
- 在图片显示区,把ROM中数据进行输出,VGA驱动模块接收此数据进行图片显示
// 在图片显示区,进行ROM数据输出,显示屏背景色为白色
// pixel_data ->24位像素数据,送到VGA驱动模块进行图像显示
assign pixel_data = rom_valid ? rom_data : WHITE;
总结
此文大致总结了本人使用DE2-115开发板进行的FPGA—VGA显示实验,全部笔记内容均为纯手打(包括上一篇浅谈串口也都是纯手工码字,每篇基本都花费了整下午+整晚上的时间,累啊!),其中也包括了个人的一些理解及总结,若文中有描述有误的地方,欢迎留言指正。