【读书笔记】详细解析基于FPGA的VGA控制器显示字符程序
0赞【主题】:详细解析基于FPGA的VGA控制器显示字符程序
【作者】:LinCoding
【时间】:2016.11.30
VGA大家一定不陌生,本篇文章就详细解析下基于FPGA的VGA控制器显示字符程序,适用于硬件为R-2R电阻匹配网络和ADV7123的系统。
效果如下图所示。重点是VGA控制器和字符ROM的寻址。
(修改自CrzayBingo的源码程序)
下图是整个系统的框图,字符数据存储在ROM中,由lcd_display取出相应的数据,然后交给lcd_driver去驱动ADV7123。
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的正中间,以使得数据最为稳定。如以下仿真图所示:
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,由框图可以看到:
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,这又是为什么呢?
同理,问题出在模块的沟通上。
由于这次我们需要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时显示所需数据。