【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块

实验二十六:VGA模块

VGA这家伙也算孽缘之一,从《建模篇》那时候开始便一路缠着笔者。《建模篇》之际,学习主要针对像素,帧,颜色等VGA的简单概念。《时序篇》之际,笔者便开始摸索VGA的时序。《整合篇》之际,笔者尝试控制VGA的时序。如今《驱动篇I》的内容返回VGA的本题,也就是图像方面的故事。

此刻,澎湃之情不容怠慢,请怒笔者不再回忆往事,失忆者请复习《Verilog HDL那些事儿》,笔者虽然也想直奔主题 ... 可是在此之前,笔者必须补足那些不易注意的细节

。俗语有云,细节是关键的藏身之地,那些不起眼的小细节,往往都是左右大局的关键,学习也是如此。不过,实验二十六究竟存在多少影响成败的小细节呢?请竖起耳朵,让笔者慢慢告诉读者 ...

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第1张图片

图26.1 C1计数的理想时序。

图26.1是一段计数时序,C1扮演计数器,而且时序理想。对此,这段时序一定按照“时间点”在表现。假设笔者想要建立判断,那么判断基准会基于C1的过去值,例如:

if( C1 == 0 );

那么if( C1 == 0 ) 必须在T1才能成立。再假设笔者想要拉长判断的长度,例如T0~T8之间,那么笔者可以这样写:

if( C1 == 0 || C1 == 1 || C1 == 2 || C1 == 3 || C1 == 4 || C1 == 5 || C1 == 6 || C1 == 7 );

如果长度像妈妈一样长气,例如T2~T99。当然,上述方法一定行不通,因为后果不仅累坏自己,而且模块的内容也会沉长臃肿。为此,笔者可以或者这样写:

if(C1 >= 0 && C1 <= 7);

其中,if( C1 >= 0 && C1 <= 7 ) 表示9个时钟沿的长度。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第2张图片

图26.2 长度有AB之别的计数时序。

假设某某长度是固定的家伙,例如图26.2,T0~T3组成4个时钟沿的长度A,而

T3~T8组成5个时钟沿的长度B。为了方便,笔者可以建立常量:

parameter A = 3, B = 5;

如果笔者想把长度A作为有效的判断基准,那么笔者可以这样写:

if( C1 >= 0 && C1 <= A -1 );

代码的用意非常明显,C1 >= 0表示长度的起始,然而 C1 <= A -1 则是长度的结束。

如果长度B也作为判断的基准,同样的写法也适用:

if( C1 >= 2 && C1 <= A + B -1 );

可是,不管怎么看 ... 笔者觉得 C1 >= 2 非常别扭,对此笔者可以这样修改:

if( C1 >= A -1 && C1 < A + B -1 );

怎么样,读者是不是稍微顺眼一点?

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第3张图片

图26.3 长度有CDE之别的计数时序。

假设顽皮的长度作出手势,然后分身成为3个,结果如图26.3所示。T0~T1组成3个时钟沿的长度C,T2~T6组成5个时钟沿的长度D,T6~T8组成3个时钟沿的长度E。为了方便,笔者先做常量声明:

parameter C = 2, D = 4, E = 2。

如果笔者想把长度C作为判断的基准,那么笔者可以这样写:

if( C1 >= 0 && C1 <= C -1 );

如果笔者想把长度D作为判断的基准,那么笔者可以这样写:

if( C1 >= C -1 && C1 <= C + D -1 );

如果笔者想把长度E作为判断的基准,那么笔者可以这样写:

if( C1 >= C + D -1 && C1 <= C + D + E -1 );

理解完毕以后,笔者可以这样总结 ... 由于C1从0开始计数,除非长度的起始地方是从0开始,否则长度的起始地方必须添加 -1 的处理。反之,不管结束地方怎么歇斯底里抵抗,长度的结束地方都必须加上 -1 处理。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第4张图片

图26.4 VGA时序。

图26.4是典型的VGA时序,VGA有5条信号,其中 HSYNC 与 VSYNC 控制显示分辨率还有显示帧。本实验的显示标准选为 1024 × 768 @ 60Hz - 65Mhz,而表26.1就是该显示标准的内容:

表26.1 显示标准 1024 × 768 @ 60Hz - 65Mhz。

信号

A

B

C

D

E

VGA_HSYNC

136

160

1024

24

1344

信号

O

P

Q

R

S

VGA_VSYNC

6

29

768

3

806

如表26.1所示,A段与O段都是拉低的起始段,然后B段与P段是准备段,C段与Q段是数据段,D段与R段都是结束段,至于E段是ABCD段的总和,S段则是OPRQ段的总和。HSYNC有时候也称为列像素或者X,VSYNC则称为行像素或者Y。列像素最小的周期时间(或称像素时钟 Pixel Clock)是 1/65Mhz,行像素最小的周期时间则是 E段 × 1/65Mhz。话题继续之前,笔者再稍微补充一下细节内容。

clip_image010

图26.5 计数例子。

我们先假设 HSYNC长度有8,前3个为拉低的起始段,后8个为拉高的数据段。如图26.5所示,C1总共计数两次,分别是T0~T8还有T8~T16。期间,第一次是无效的计数,所以HSYNC并没有拉低起始段。换之,第二次开始才是有效的计数,因为HSYNC拉低起始段。为此,Verilog可以这样描述:

if( C1 == 7 ) HSYNC <= 1’b0;

else if( C1 == 2 ) HSYNC <= 1’b1;

假设起始段声明为A,而结束段声明为D,E则是A与D的总和,那么内容可以继续修改:

parameter A = 3, D = 5, E = 8;

if( C1 == E -1 ) HSYNC <= 1’b0;

else if( C1 == A -1 ) HSYNC <= 1’b1;

明白以后,我们便可以用Verilog 描述表26.1的内容,结果如代码26.1所示:

1.         parameter SA = 11'd136, SE = 11'd1344;
2.         parameter SO = 11'd6, SS = 11'd806;
3.         
4.          reg [10:0]C1;
5.         reg [9:0]C2;
6.         reg rH,rV;
7.         
8.         always @ ( posedge CLOCK or negedge RESET )
9.             if( !RESET )
10.                  begin
11.                          C1 <= 11'd0;
12.                         C2 <= 10'd0;
13.                         rH <= 1'b1;
14.                         rV <= 1'b1;
15.                    end
16.              else 
17.                  begin
18.                        if( C1 == SE -1 ) rH <= 1'b0; 
19.                         else if( C1 == SA -1  ) rH <= 1'b1;
20.                         
21.                        if( C2 == SS -1 ) rV <= 1'b0;
22.                         else if( C2 == SO -1  ) rV <= 1'b1;
23.                         
24.                         if( C2 == SS -1 ) C2 <= 10'd0;
25.                         else if( C1 == SE -1 ) C2 <= C2 + 1'b1;
26.                         
27.                         if( C1 == SE -1 ) C1 <= 11'd0;
28.                         else C1 <= C1 + 1'b1; 
29.                    end

代码26.1

第1~2行是 A段与E段,O段还有S段的常量声明。第4~6是相关的寄存器声明,第10~14行则是这些寄存器的复位操作。请注意一下第8行的主时钟是65Mhz。第27~28行是针对列像素的计数器C1,计数范围为0~1343。第24~25行是针对行像素的计数器C2,计数范围为0~805。第18~19行用来控制 HSYNC,第21~22行则是用来控制VSYNC。

理解完毕 HSYNC 与 VSYNC信号以后,接下来我们便要学习 RGB 信号。事实上,HSYNC与VSYNC的数据段,其实也是RGB有效的数据段。表26.1基于1024×768 @ 60Hz,所以HSYNC数据段的长度有1024,而VSYNC的数据段的长度则有768。如果VGA拥有显示标准,那么RGB信号也有所谓的颜色标准。根据Photoshop ,常见的颜色如表26.2所示:

表26.2 常见的颜色。

颜色

位宽

黑白

1bit

灰度级

4bit,8bit

RGB

8bit,16bit,24bit,32bit

黑白可谓是最典型的颜色,不禁让笔者想起怀念的小绿人。灰度级正如字面上的意思,意指失去颜色的图像,4bit有16灰度级,8bit则有256灰度级。RGB是电脑专用的颜色标准,从过去发展至今,RGB的颜色分辨率也从4bit发展到32bit。虽说8位RGB是历史悠久的前辈,不过至今它还未退休吔,例如街边的LED招牌。8位RGB也称为索引色。

16位RGB可是人眼比较接近的的颜色标准,也是本实验的主题,其中RGB[15:11]是5位红色,RGB[10:5]是6位绿色,RGB[4:0]则是5位蓝色,16位RGB称为高彩。24位RGB也是目前流行的颜色标准,其中字节0为蓝色,字节1为绿色,字节2为红色,24位称为真彩。32位RGB相较24位RGB则多了一个字节3的通道字节,通道也指透明效果,它称为全彩。

为消除读者对RGB的恐惧,笔者就解剖一下16位RGB:

clip_image012

图26.6 4×1与16位RGB图像。

图26.6是一幅宽为4高为1的16位RGB图像,如果从左至右开始数起,列0为饱和的红色,列1为饱和的绿色,列2为饱和的蓝色,列3为纯黑。如果将其卸甲并且一字排开高位在前的内容,结果如表26.2所示:

表示26.3 4×1与16位RGB图像内容(高位在前)。

列0

列1

列2

列3

饱和红

饱和绿

饱和蓝

16’hF800

16’h07E0

16’h001F

16’h0000

16’b1111_1000_0000_0000

16’b0000_0111_1110_0000

16’b0000_0000_0001_1111

16’b0000_0000_0000_0000

如表26.3所示,我们可以看见列0的红色占据RGB[15:11]的位置,亦即红色有25=32的分辨率。列1的绿色则是占据RGB[10:5]的位置,亦即26= 64的分辨率。至于列2的蓝色占据RGB[4:0]的位置,结果它有25=32的分辨率。

clip_image014

图26.7 4×1与16位RGB图像(红色渐变)。

图26.7也是一幅宽有4高有1的16位RGB图像,不过是红色渐变的图像。如果从左至右开始数起,列0为4/4饱和的红色,列1为3/4饱和的红色,列2为2/4饱和的红色,列3为1/4饱和的红色。笔者随之伸出魔爪将其卸甲,然后一字排开高位在前的内容,结果如表26.4所示:

表示26.4 4×1与16位RGB图像内容(红色渐变与高位在前)。

列0

列1

列2

列3

4/4饱和红

3/4饱和红

2/4饱和红

1/4饱和红

16’hF800

16’hC000

16’h8000

16’h4000

16’b1111_1000_0000_0000

16’b1100_0000_0000_0000

16’b1000_0000_0000_0000

16’b0100_0000_0000_0000

如表26.4所示,我们可以看见列0的红色充斥整座RGB[15:11],列1的红色则是占据其中的14份内容而已。列2更惨,它占据的份儿只有8份而已,而最惨的列3则是只有麻雀眼泪般的4份内容而已。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第5张图片

图26.8 实验用的小可爱。

图26.6是实验二十六所用的图像资源,内容是一群可爱的比卡丘在玩耍。图像大小为128 × 96 × 16 bit,结果容量为:

128 × 96 × 16 bit = 196608 bit

如果储存器的位宽有16位,那么储存器的地址位宽则是:

128 × 96 = 12208

因此,必须建立位宽为 214 的地址信号:

214 = 16384

简单而言,图26.8的X为128还有Y为96,而Y也充当偏移量的角色。为此,正确的地址公式如下所示:

Address = X + ( Y × 128 )

= X + (Y << 7)

理解完毕这些准备知识以后,我们便可以开始建模了。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第6张图片

图26.9 实验二十六(VGA基础模块)的建模图。

图26.9是实验二十六的建模图,其中PLL将50Mhz 的CLOCK增至 65Mhz。VGA储存模块有 128 × 96 × 16 bit 的容量,里边暂存皮卡丘的图像信息。VGA功能模块则是负责1024 × 768 @ 60Hz的显示标准,还有将内部的计数信息反馈给VGA控制模块。VGA控制模块位与中间,它借助 iAddr然后转换成为图像读取的地址,最后再将反馈回来的图像信息发至 VGAD顶层信号。

vga_savemod.v

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第7张图片

图26.10 VGA储存模块的建模图。

图26.10虽是VGA储存模块的建模图,不过内容却是简单ROM模块,具体内容让我们来看代码吧。

1.    module vga_savemod
2.    (
3.         input CLOCK, RESET,
4.         input [13:0]iAddr,
5.         output [15:0]oData
6.    );
7.        (* ramstyle = "no_rw_check , m9k" , ram_init_file = "pikachu_128x96_16_msb.mif" *) 
8.         reg [15:0] RAM[12287:0];
9.         reg [15:0]D1;
10.         
11.         always @ ( posedge CLOCK or negedge RESET )
12.             if( !RESET )
13.                  D1 <= 16'd0;
14.              else 
15.                  D1 <= RAM[ iAddr ];
16.         
17.         assign oData = D1;
18.    
19.    endmodule

以上内容虽然简单,不过还是注意一下第7行的建立声明。其中 no_rw_check是用来告诉综合器无视 read during write 的检测,m9k则声明片上内存用 m9k,至于ram

init file 则是RAM的初始化内容,亦即图26.8。

vga_funcmod.v

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第8张图片

图26.11 VGA功能模块的建模图。

图26.11是VGA功能模块的建模图,左边是反馈给朋友的oAddr,其中[20:10]是X地址,[9:0]则是Y地址。右边是驱动顶层的VGA_HSYNC与 VGA_VSYNC。

1.    module vga_funcmod 
2.    (
3.         input CLOCK, RESET,
4.         output VGA_HSYNC, VGA_VSYNC,
5.         output [20:0]oAddr
6.    );
7.         parameter SA = 11'd136, SE = 11'd1344;
8.         parameter SO = 11'd6, SS = 11'd806;
9.         

以上内容为相关的出入端声明,还有A段,E段,O段还有S段的常量声明。

10.         reg [10:0]C1;
11.         reg [9:0]C2;
12.         reg rH,rV;
13.         
14.         always @ ( posedge CLOCK or negedge RESET )
15.             if( !RESET )
16.                  begin
17.                         C1 <= 11'd0;
18.                         C2 <= 10'd0;
19.                         rH <= 1'b1;
20.                         rV <= 1'b1;
21.                    end
22.              else 

以上内容为相关的寄存器声明,其中C1为HSYNC计数,C2则为VSYNC计数。

23.                  begin
24.                        
25.                         if( C1 == SE -1 ) rH <= 1'b0; 
26.                         else if( C1 == SA -1  ) rH <= 1'b1;
27.                         
28.                         if( C2 == SS -1 ) rV <= 1'b0;
29.                         else if( C2 == SO -1  ) rV <= 1'b1;
30.                         
31.                         if( C2 == SS -1 ) C2 <= 10'd0;
32.                         else if( C1 == SE -1 ) C2 <= C2 + 1'b1;
33.                         
34.                         if( C1 == SE -1 ) C1 <= 11'd0;
35.                         else C1 <= C1 + 1'b1;
36.                         
37.                    end
38.                    

以上内容为 HSYNC 与 VSYNC的驱动过程。第35~36行是计数HSYNC,第32~33行则是计数VYSNC,一个VSYNC为1344个HSYNC。第26~27行是 rH的控制,第29~30行则是 rV的控制。

39.        reg [1:0]B1,B2,B3;
40.         
41.        always @ ( posedge CLOCK or negedge RESET )
42.             if( !RESET )
43.                  {  B3, B2, B1 } <= 6'b11_11_11;
44.              else
45.                  begin
46.                         B1 <= { rH,rV };
47.                         B2 <= B1;
48.                         B3 <= B2;
49.                    end    
50.        

第40~51则是用来延迟 rH与rV的周边操作,期间总共延迟3个时钟。详细内容往后再说。

51.         assign { VGA_HSYNC, VGA_VSYNC } = B3;
52.         assign oAddr = {C1,C2}
53.         
54.    endmodule

以上内容为相关的输出驱动。

vga_ctrlmod.v

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第9张图片

图26.12 VGA控制模块的建模图。

图26.12是VGA控制模块的建模图,而设计思路来源《整合篇》。左边信号连接储存模块,右边信号连接功能模块,北边信号则驱动顶层。具体内容还是让我们来看代码吧:

1.    module vga_ctrlmod
2.    (
3.         input CLOCK, RESET,
4.         output [15:0]VGAD,
5.         output [13:0]oAddr,
6.         input [15:0]iData,
7.         input [20:0]iAddr
8.    );

以上内容为相关的出入端声明。

9.         parameter SA = 11'd136, SB = 11'd160, SC = 11'd1024, SD = 11'd24, SE = 11'd1344;
10.         parameter SO = 11'd6, SP = 11'd29, SQ = 11'd768, SR = 11'd3, SS = 11'd806;
11.         
12.         // Height , width, x-offset and y-offset
13.         parameter XSIZE = 8'd128, YSIZE = 8'd96, XOFF = 10'd0, YOFF = 10'd0; 
14.        
15.         wire isX = ( (iAddr[20:10] >= SA + SB + XOFF -1 ) && ( iAddr[20:10] <= SA + SB + XOFF + XSIZE -1) );
16.         wire isY = ( (iAddr[9:0] >= SO + SP + YOFF -1 ) && ( iAddr[9:0] <= SO + SP + YOFF + YSIZE -1) );
17.         wire isReady = isX & isY;
18.         
19.         wire [31:0] x = iAddr[20:10] - XOFF - SA - SB -1; 
20.         wire [31:0] y = iAddr[9:0] - YOFF - SO - SP -1;
21.         

以上内容为相关的常量声明。第9~10行是针对 1024 × 768 @ 60Hz 显示标准的常量声明。第13行声明图像信息,如大小还有位置。第15行声明有效的列像素,第16行则声明有效的Y像素,至于第17行则是声明图像的有效区域,注意它们都是即时事件。第19~20行用来是计算有效的X与Y,主要用来取得图像的读取地址,注意它们也是即时事件。

22.         reg [31:0]D1;
23.         reg [15:0]rVGAD;
24.         
25.         always @ ( posedge CLOCK or negedge RESET )
26.             if( !RESET )
27.                  begin
28.                        D1 <= 18'd0;
29.                        rVGAD <= 16'd0;
30.                    end
31.                else

以上内容为相关的寄存器声明还有复位操作。

32.                   begin
33.                    
34.                        // step 1 : compute data address and index-n
35.                         if( isReady )
36.                             D1 <= (y << 7) + x; 
37.                         else
38.                             D1 <= 14'd0;
39.                         
40.                         // step 2 : reading data from rom
41.                         // but do-nothing
42.                         
43.                         // step 3 : assign RGB_Sig
44.                         rVGAD <= isReady ? iData : 16'd0;
45.                         
46.                    end
47.                    

以上内容为VGA控制模块的核心操作。该控制模块采用流水操作,一边不断发送读取地址,一边不断等待图像信息读出,一边不断输出图像信息。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第10张图片

图26.13 VGA功能模块的流水操作。

从某种程度来说,实验二十六的VGA基础模块可以看成如图26.13所示。VGA功能模块输出HSYNC与VSYNC的瞬间,它也发送 Addr 给 VGA控制模块。VGA控制模块计算读取地址以后再发送给 VGA储存模块,VGA储存模块随意也将图像信息返回发送给VGA控制模块,最后VGA控制模块再将图像信息驱动至 VGAD。

简单来说,图像信息一共慢HSYNC和VSYNC信号3拍,因此VGA功能模块多了旁路的周边操作,目的是为了同步 HSYNC,VSYNC还有 VGAD之间的延迟。返回话题,有效的读取地址只有 isReady 拉高的时候,如代码行第36所示。同样,有效的图像信息也是 isReady 拉高的时候,如代码行第45所示。

48.        assign VGAD = rVGAD;
49.        assign oAddr = D1[13:0];
50.    
51.    endmodule

以上内容为相关的输出驱动。

vga_basemod.v

该模块是VGA基础模块,至于内部的连线部署请参考图26.9。

1.    module vga_basemod
2.    (
3.         input CLOCK, RESET,
4.         output VGA_HSYNC, VGA_VSYNC,
5.         output [15:0]VGAD
6.    );
7.        wire CLOCK_65M;
8.         
9.        pll_module U1  
10.        (
11.            .inclk0 ( CLOCK ),
12.            .c0 ( CLOCK_65M ) // CLOCK 65Mhz
13.         );
14.         
15.         wire [20:0]AddrU2;
16.         
17.         vga_funcmod U2    // 1024 * 758 @ 60Hz
18.         (
19.           .CLOCK( CLOCK_65M ), 
20.           .RESET( RESET ),
21.           .VGA_HSYNC( VGA_HSYNC ), 
22.           .VGA_VSYNC( VGA_VSYNC ),
23.           .oAddr( AddrU2 ),
24.         );             
25.         
26.         wire [15:0]DataU3;
27.         
28.         vga_savemod U3
29.         (
30.            .CLOCK( CLOCK_65M ),
31.            .RESET( RESET ),
32.            .iAddr( AddrU4 ),
33.            .oData ( DataU3 )
34.        );
35.        
36.         wire [13:0]AddrU4;
37.         
38.         vga_ctrlmod U4  // 128 * 96 * 16bit, X0,Y0
39.         (
40.             .CLOCK( CLOCK_65M ),
41.              .RESET( RESET ),
42.              .VGAD( VGAD ),
43.             .iData( DataU3 ),
44.             .oAddr( AddrU4 ),
45.             .iAddr( AddrU2 ),
46.         );
47.         
48.    endmodule

详细的内容请读者自己看着办吧。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块_第11张图片

图26.14 实验二十六的结果。

综合完毕便下载程序,如果实验成功,分辨率为1024 * 768 的显示器左上角便会出现一幅128 × 96 × 16 bit 的图像显示在 X0与Y0的位置,结果如图26.14所示。

细节一:完整的个体模块

实验二十六的VGA基础模块虽为就绪状态,不过它是一只能力有限的家伙。原因很单纯,因为储存128 × 96 × 16 bit 的图像已经是EP4CE6F17C8 这块FPGA的极限,这家伙的度量很小,所以片上内存也不怎么大。

细节二: 片上内存

笔者曾经说过,片上内存是资粮很棒的储存资源,它不仅访问时间短,自定义强,而且操作也简单,但是肚量是它永远的痛。EP4CE6F17C8 虽然有 476280 bit 的片上内存,不过实际可用的范围只有其中的90%而已。

细节三: 缓冲图像的储存模块

如果片上内存不行,那么SDRAM当然是最好的替代。同样,笔者也曾经说过,SDRAM的优点除了肚量大以外,无论是访问速度还有操作程度也不及片上内存。一旦SDRAM谋朝散位成功,VGA控制模块,VGA储存模块还有VGA功能模块之间就会失去同步性。此外,SDRAM也不能经过综合器初始化内容。不管怎么样,实验二十六已经完成任务,以后的问题以后再说吧。

你可能感兴趣的:(【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验二十六:VGA模块)