LinCoding

【读书笔记】详细解析基于FPGA的VGA控制器显示字符程序

0
阅读(4992)

【主题】:详细解析基于FPGA的VGA控制器显示字符程序

【作者】:LinCoding

【时间】:2016.11.30

  VGA大家一定不陌生,本篇文章就详细解析下基于FPGA的VGA控制器显示字符程序,适用于硬件为R-2R电阻匹配网络和ADV7123的系统。

效果如下图所示。重点是VGA控制器和字符ROM的寻址。

(修改自CrzayBingo的源码程序)

blob.png

       下图是整个系统的框图,字符数据存储在ROM中,由lcd_display取出相应的数据,然后交给lcd_driver去驱动ADV7123。

blob.png

        1、先说lcd_driver

module lcd_driver
(
	input			clk,		//global clock
	input			rst_n,		//global reset

	input	[23:0]	lcd_data,
	//ADV7123 HardWare
	output			lcd_dclk,	//ADV7123 clock
	output			lcd_blank,
	output			lcd_sync,
	output			lcd_hs,		//horizontal signal
	output			lcd_vs,		//vertical signal
	output	[23:0]	lcd_rgb,	
	
	output			lcd_en,		//valid area enable signal
	output			lcd_request,
	output	[10:0]	        lcd_xpos,
	output	[10:0]	        lcd_ypos
);

 第一部分是输入输出定义,不解释。

//-----------------------------------
//h_sync counter
reg	[10:0]	hcnt;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		hcnt	<= 11'd0;
	else if ( hcnt < `H_TOTAL )
		hcnt	<= hcnt + 1'b1;
	else
		hcnt	<= 11'd1;
end
assign	lcd_hs	= ( hcnt <= `H_SYNC ) ? 1'b0 : 1'b1;

//-----------------------------------
//v_sync counter
reg	[10:0]	vcnt;
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		vcnt	<= 11'd0;
	else if ( hcnt == `H_TOTAL )
		vcnt 	<= ( vcnt < `V_TOTAL ) ? vcnt + 1'b1 : 11'd1;
	else
		vcnt	<= vcnt;
end
assign	lcd_vs	= ( vcnt <= `V_SYNC ) ? 1'b0 : 1'b1;

  第二部分定义了行扫描和场扫描的计数器,用于输出符合VGA时序的行信号和场信号。其中的宏定义均可在lcd_para.h或者lcd_para.v中先定义好,方便移植。

assign	lcd_dclk	= ~ clk;
assign	lcd_blank	= lcd_hs & lcd_vs;
assign	lcd_sync	= 1'b0;

 第三部分定义了ADV7123硬件所需的信号,lcd_dclk是ADC7123的时钟信号,这里赋值为~ clk,原因是可以使ADV7123的lcd_dclk的上升沿出现在lcd_hs和lcd_vs的正中间,以使得数据最为稳定。如以下仿真图所示:

blob.png

localparam	COMPENSATE	= 1'b1;
localparam	H_AHEAD		= 1'b1;

assign	lcd_en		= ( hcnt >= `H_SYNC + `H_BACK + COMPENSATE && hcnt < `H_SYNC + `H_BACK +     `H_DISP + COMPENSATE ) &&  ( vcnt >= `V_SYNC + `V_BACK + COMPENSATE && vcnt < `V_SYNC + `V_BACK + `V_DISP + COMPENSATE ) ? 1'b1 : 1'b0;

assign	lcd_rgb		= lcd_en ? lcd_data : 24'd0;

assign	lcd_request	= ( hcnt >= `H_SYNC + `H_BACK + COMPENSATE - H_AHEAD && hcnt < `H_SYNC + `H_BACK + `H_DISP + COMPENSATE - H_AHEAD ) && ( vcnt >= `V_SYNC + `V_BACK + COMPENSATE && vcnt < `V_SYNC + `V_BACK + `V_DISP + COMPENSATE ) ? 1'b1 : 1'b0;

assign	lcd_xpos = lcd_request ? ( hcnt - ( `H_SYNC + `H_BACK + COMPENSATE - H_AHEAD ) ) : 11'd0;	
assign	lcd_ypos = lcd_request ? ( vcnt - ( `V_SYNC + `V_BACK + COMPENSATE ) ) : 11'd0;

 第四部分则是整个lcd_driver最为重要的部分

1、lcd_en稍微好理解一点,就是在VGA的lcd_hs和lcd_vs数据有效区域内置为高,以显示当前数据的有效区,至于为什么加了一个COMPENSATE,这是根据仿真看出来的,不加的话就正好少了一个数。

2、lcd_rgb呢,就是在数据有效区内输出有效数据,否则的话就输出0。

3、lcd_request其实和lcd_en很类似,只是hcnt中减去了H_AHEAD,这又是为什呢?

我们知道模块与模块的连接,如果都为时序逻辑的话,则沟通需要一个clk,由框图可以看到:

blob.png

  lcd_request信号是由lcd_driver模块发送给lcd_display模块的,由于都为时序逻辑,则沟通需要一个clk,但是为了使得lcd_data与lcd_hs、lcd_vs同步,则lcd_request提前了一个clk发给lcd_display模块,这时,lcd_display模块接收到lcd_driver模块发来的lcd_request后将lcd_data发送给lcd_driver模块,当lcd_data到达lcd_driver模块时正好与lcd_hs、lcd_vs同步,这样的话就保证了数据和扫描同步进行。

4、lcd_xpos信号的原理也同上,提前了一个clk信号。

这里大家可能有疑惑,为什么只提前lcd_xpos,而不提前lcd_ypos?

这是因为vcnt是由hcnt决定的,也就是当hcnt计数满H_TOTAL后vcnt才计一个数,因此,只需提前lcd_xpos即可。

  2、再说lcd_display

module lcd_display
(
	input				clk,	//global clock
	input				rst_n,	//global rst_n
	
	input				lcd_request,
	input		[10:0]	        lcd_xpos,
	input		[10:0]	        lcd_ypos,
	
	output	reg	[23:0]	        lcd_data
);

   第一部分还是输入输出定义。

`define	COMPENSATE	9'd1
//-------------------------------------
wire		valid_area1	= ( ( lcd_xpos >= 10'd64 && lcd_xpos < 10'd576 ) && 
				    ( lcd_ypos >= 10'd128&& lcd_ypos < 10'd192 ) ) ? 1'b1 : 1'b0;
wire	[8:0]	rom_addr1	= lcd_xpos[8:0] - ( 9'd64 - `COMPENSATE );	
wire	[63:0]	rom_data1;				
helloworld u_helloworld
(	
	.clock		(clk),
	.address	(rom_addr1),
	.q		(rom_data1)
);

//-------------------------------------
wire		valid_area2	= ( ( lcd_xpos >= 10'd64 && lcd_xpos < 10'd576 ) && 
				    ( lcd_ypos >= 10'd256&& lcd_ypos < 10'd320 ) ) ? 1'b1 : 1'b0;
wire	[8:0]	rom_addr2	= lcd_xpos[8:0] - ( 9'd64 - `COMPENSATE );	
wire	[63:0]	rom_data2;
LinCoding u_LinCoding
(	
	.clock		(clk),
	.address	(rom_addr2),
	.q		(rom_data2)
);		

 第二部分是关于ROM寻址和ROM输出数据,有一点需要注意。

rom_addr1和romaddr2中又出现了COMPENSATE,这又是为什么呢?

同理,问题出在模块的沟通上。

blob.png

 由于这次我们需要lcd_display模块给ROM发地址,然后ROM返回lcd_data给lcd_display模块,然后lcd_display模块再将数据发送给lcd_driver模块,而lcd_display和lcd_driver模块进行lcd_xpos和lcd_data沟通时使用的是组合逻辑,因此沟通时间可忽略,时间仅仅浪费在了ROM读取数据的一个clk和lcd_display本身一个always块中,因此,要提前2个clk,而lcd_driver中已经将lcd_xpos提前了1个clk,所以,这里我们只需再提前1个clk即可。

 原本,我们设想是的当lcd_xpos为64时,显示ROM中的0个数据,但是由于模块的沟通消耗了2个clk,那么我们就必须在lcd_xpos为62时,就向ROM要第0个数据,使得ROM给lcd_display发出lcd_data时,lcd_xpos变为了63,而lcd_display本身的always块又浪费了1个clk,当lcd_display给lcd_driver发送lcd_data时,此时的lcd_xpos变为了64,数据正好同步输出,达到我们的预期。

//-------------------------------------
always @ ( posedge clk or negedge rst_n )
begin
	if ( ! rst_n )
		lcd_data	<= `BLACK;
	else
		if ( lcd_request && valid_area1 )
			if ( rom_data1[6'd63-lcd_ypos[5:0]] == 1'b1 )
				lcd_data	<= `WHITE;
			else
				lcd_data	<= `BLUE;
		else if ( lcd_request && valid_area2 )
			if ( rom_data2[6'd63-lcd_ypos[5:0]] == 1'b1 )
				lcd_data	<= `WHITE;
			else
				lcd_data	<= `BLUE;
		else	
			lcd_data	<= `BLACK;
end

   最后一个部分就是输出数据显示在VGA上了,由于我们取字模时,使用了:阴码、逐列式,所以在程序需在rom数据为1时显示所需数据。