实验二十六:VGA模块
VGA这家伙也算孽缘之一,从《建模篇》那时候开始便一路缠着笔者。《建模篇》之际,学习主要针对像素,帧,颜色等VGA的简单概念。《时序篇》之际,笔者便开始摸索VGA的时序。《整合篇》之际,笔者尝试控制VGA的时序。如今《驱动篇I》的内容返回VGA的本题,也就是图像方面的故事。
此刻,澎湃之情不容怠慢,请怒笔者不再回忆往事,失忆者请复习《Verilog HDL那些事儿》,笔者虽然也想直奔主题 ... 可是在此之前,笔者必须补足那些不易注意的细节
。俗语有云,细节是关键的藏身之地,那些不起眼的小细节,往往都是左右大局的关键,学习也是如此。不过,实验二十六究竟存在多少影响成败的小细节呢?请竖起耳朵,让笔者慢慢告诉读者 ...
图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个时钟沿的长度。
图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 );
怎么样,读者是不是稍微顺眼一点?
图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 处理。
图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。话题继续之前,笔者再稍微补充一下细节内容。
图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:
图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的分辨率。
图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份内容而已。
图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)
理解完毕这些准备知识以后,我们便可以开始建模了。
图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
图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
图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
图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控制模块的核心操作。该控制模块采用流水操作,一边不断发送读取地址,一边不断等待图像信息读出,一边不断输出图像信息。
图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
详细的内容请读者自己看着办吧。
图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也不能经过综合器初始化内容。不管怎么样,实验二十六已经完成任务,以后的问题以后再说吧。