實驗二十六: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也不能經過綜合器初始化內容。不管怎么樣,實驗二十六已經完成任務,以后的問題以后再說吧。