FPGA基础设计(一):VGA显示方法(文字、图形、波形)

概述

  VGA是一种学习FPGA最常见的基础实验。虽然现在的显示屏大多已经采用DVI和HDMI方案,但其实VGA在另一个地方还有应用,那就是大屏的LCD。目前4.3寸以上的TFT基本都是VGA接口,这样在完成一个FPGA系统设计时,选择一个VGA接口的TFT用来显示便是最简单方便的方案。

  现在2017年全国大学生电子设计大赛还有不到一个月,熟练的使用VGA显示各种图形、文字、波形还是很重要的,而不是停留在只能显示彩条的入门实验上。这篇博文便致力于解决这个问题。


VGA显示驱动

  目前常见的电路板上的VGA接口是这样的,单独使用R、G、B三条线控制颜色:

FPGA基础设计(一):VGA显示方法(文字、图形、波形)_第1张图片

  或者是这样的,增加一个电阻网络来使可以控制的颜色更加丰富:

FPGA基础设计(一):VGA显示方法(文字、图形、波形)_第2张图片

  现在应该很少会看到专门使用VGA驱动芯片的了。使用电阻网络已经能获得不错的显示效果。FPGA需要处理的信号有行同步信号HSYNC和场同步信号VSYNC,以及R、G、B三组颜色控制信号。在驱动VGA之前,我们首先要确定自己的显示参数,分辨率及刷新率,比如800*600@60Hz的显示方式其时序参数如下所示:

  不同的分辨率和刷新率有不同的参数,这个数据可以在www.tinyvga.com这个网页中查到。进下来就进行VGA的时序驱动,我的习惯是先将关键性数据用parameter定义出来:

//-------------------------------------------------//
//          扫描参数的设定 640*480 60Hz VGA
//-------------------------------------------------//
	parameter H_SYNC_END   = 96;     //行同步脉冲结束时间
	parameter V_SYNC_END   = 2;      //列同步脉冲结束时间
	parameter H_SYNC_TOTAL = 800;    //行扫描总像素单位
	parameter V_SYNC_TOTAL = 525;    //列扫描总像素单位
	parameter H_SHOW_START = 144;    //显示区行开始像素点
	parameter V_SHOW_START = 35;     //显示区列开始像素点

  VGA的时序驱动部分是相当固定的,只要我们使用VGA,肯定会加入下面这段代码。主要方法是定义两个计数器,一个管理行扫描,一个管理列扫描;当行扫描计数器扫描完行同步脉冲后置高HSYNC信号;同理,当列扫描计数器扫描完列同步脉冲后置高VSYNC信号。

//水平扫描
	always@(posedge clk_25M or negedge RSTn)	
		if(!RSTn) x_cnt <= 'd0;
		else if (x_cnt == H_SYNC_TOTAL) x_cnt <= 'd0;
		else  x_cnt <= x_cnt + 1'b1;
	
	//垂直扫描
	always@(posedge clk_25M or negedge RSTn)	
		if(!RSTn) y_cnt <= 'd0;
		else if (y_cnt == V_SYNC_TOTAL) y_cnt <= 'd0;
		else if (x_cnt == H_SYNC_TOTAL) y_cnt <= y_cnt + 1'b1;
		else y_cnt <= y_cnt;

	//H_SYNC信号
	always@(posedge clk_25M or negedge RSTn)	
		if(!RSTn) hsync <= 'd0;
		else if (x_cnt == 'd0) hsync <= 1'b0;
		else if (x_cnt == H_SYNC_END) hsync <= 1'b1;
		else  hsync <= hsync;
		
	//V_SYNC信号
	always@(posedge clk_25M or negedge RSTn)	
		if(!RSTn) vsync <= 'd0;
		else if (y_cnt == 'd0) vsync <= 1'b0;
		else if (y_cnt == V_SYNC_END) vsync <= 1'b1;
		else  vsync <= vsync;	

  由于VGA需要一个时钟来管理扫描的速度,因此这个时钟大小就应当为扫面面积刷新率,如上例中的640480@60Hz的显示方式需要的VGA时钟大小为80052560=25.2MHz(扫描时不仅包括显示区域,还有同步脉冲、显示前沿和显示后沿,因此整个区域要大于分辨率)。VGA时钟可以用PLL或分频等方法产生,通常要求不会太严苛,上例取整为25MHz即可。

  为了后面的显示方便,一种实用的方法是再定义两个寄存器,专门存储显示区域的坐标,即以可以显示的第一个像素点为坐标(0,0)。

    assign x_pos = x_cnt - H_SHOW_START;
	assign y_pos = y_cnt - V_SHOW_START;

  这样上例中行扫描计数器和列扫描计数器的范围分别为800和525,而显示区域的坐标x_pos和y_pos范围只有640和480。


VGA显示图形、波形、文字

  其实在得到了显示区域的坐标后,我们控制显示图像的方法就是当计数器扫描到指定位置后,为R、G、B三组信号赋值得到对应的图形。以前面写的“FPGA综合系统设计(三):贪吃蛇游戏(VGA+键盘)https://blog.csdn.net/FPGADesigner/article/details/75103652”这个工程中的显示部分代码为例:

always@(posedge clk_25M)
		if (area)  //坐标处于显示分数的区域内,80*80
		begin     
		     case(pop)
		      	0:    color_out <= data0 ? 3'b111 : 3'b000;
                1:    color_out <= data1 ? 3'b111 : 3'b000;
                2:    color_out <= data2 ? 3'b111 : 3'b000;
                3:    color_out <= data3 ? 3'b111 : 3'b000;
                4:    color_out <= data4 ? 3'b111 : 3'b000;
                5:    color_out <= data5 ? 3'b111 : 3'b000;
                6:    color_out <= data6 ? 3'b111 : 3'b000;
                7:    color_out <= data7 ? 3'b111 : 3'b000;
                8:    color_out <= data8 ? 3'b111 : 3'b000;
                9:    color_out <= data9 ? 3'b111 : 3'b000;
                10:   color_out <= data10 ? 3'b111 : 3'b000;
                11:   color_out <= data11 ? 3'b111 : 3'b000;
                12:   color_out <= data12 ? 3'b111 : 3'b000;
               default :  color_out <= 3'b000;
		     endcase
		end
		else       //坐标处于游戏界面的区域内
		begin             
			lox=x_pos[3:0]; //取偏移坐标 
			loy=y_pos[3:0];  
			/* 根据当前扫描到的点是哪一部分输出相应颜色 */
			/*苹果*/
if(x_pos[9:4]==apple_x&&y_pos[9:4]==apple_y) 
				case({loy,lox})					
					8'b0000_0000:color_out=3'b000;
					default:color_out=3'b001;
				endcase
			/*背景*/	
			else if(snake==NONE)
				color_out=3'b000;
			/*墙壁*/	
			else if(snake==WALL)
				color_out=3'b101;
			/*蛇头与蛇身*/
			else if(snake==HEAD|snake==BODY)     
				case({lox,loy})
				     8'b0000_0000:color_out=3'b000;
				     default:color_out=(snake==HEAD)?HEAD_COLOR:BODY_COLOR;
				endcase				
		end

  看这个的设计思路,always里我把整个显示区域划分为if(area)和else两个区域,area是用逻辑判断定义好的一个80*80大小的区域,用来显示分数;else则是显示屏的剩下区域, 用来显示游戏。

  先看if(area)区域,我事先将各个分数以图片的形式存到了ROM中,所有的ROM接的是一组地址线,每个ROM又有不同的数据线。根据当前的游戏分数,我使用case来决定选择哪个ROM中的图形作为当前区域的输出。这个设计思路用处就大了,比如显示电压、电流、频率等如何让其动态变化,这就是一种好的方法。

  再来看else区域,我使用计数器位数之间的关系,来将整个屏幕划分为几个小格子,然后根据格子应当属于哪种游戏元素来决定显示什么颜色,这样整个显示区域就划分为苹果、墙壁、蛇头、蛇身等各个游戏元素。

  其实显示波形的方法和else区域显示的方法基本是一样的。假设我要画一段512个点长的频谱,我就可以选择出行计数器扫描中的512个像素点,每一个像素点作为一个频谱点;用同样的方法,我们把列计数器扫描的像素点按一定比例分配给不同的峰值,这样扫描到指定点时输出颜色,看起来就是一个完整的频谱图了。当然如果觉得图形不够连贯,可以用更多像素点来显示,对中间的像素点插值即可。

你可能感兴趣的:(FPGA)