一、 軟件平臺與硬件平臺
軟件平臺:
1、操作系統:Windows-8.1
2、開發套件:ISE14.7
硬件平臺:
1、 FPGA型號:Xilinx公司的XC6SLX45-2CSG324
2、 VGA接口
3、 液晶顯示器
二、 原理介紹
VGA(Video Graphics Array)即視頻圖形陣列,是IBM在1987年推出的使用模擬信號的一種視頻傳輸標準,在當時具有分辨率高、顯示速率快、顏色豐富等優點,在彩色顯示器領域得到了廣泛的應用。這個標準對于現今的個人電腦市場已經十分過時。即使如此,VGA仍然是最多制造商所共同支持的一個標準,個人電腦在加載自己的特殊驅動程序之前,都必須支持VGA的標準。
VGA接口實物所示
?
左邊帶針的叫VGA公頭,右邊帶槽的叫VGA母頭。
VGA接口的特點:
1、 VGA接口不支持熱插拔
VGA接口跟HDMI接口一樣是不支持熱插拔的。熱插拔是指一般帶電狀態下對于接插件的插入或是拔除,并不只是針對有電源接口或者帶供電的接口的接插件,而是所有。在運行狀態時,插拔會產生耦合電流,電流不穩造成硬件燒壞,導致筆記本的接口端的保護受到沖擊。就像U盤不能再一個時間段多次在一個端口插拔使用一樣。各種電器的外露端子都會有金屬的部分,它們都是要求接地的,但是不同的電器之間的地并不一定相同,比如一臺DVD的地和一臺電視機的接地都是相對于本身系統而言。
當端子插入時,首先要建立共同的地來對傳輸的信號作參考,這就要依靠端子和傳輸線上的金屬部分了,金屬部分接地同時也是對信號的屏蔽和保護。兩個地相接觸一瞬間,會有很高的尖峰脈沖產生,這種脈沖如果不加以濾除可能會直達芯片并將其損壞。另外還有一種是ESD,即靜電損壞,這種更難以避免,因為在電子產品上,只能去防護,ESD的持續時間會更短US級別。所以正規的電子產品對于金屬端子的接地有比較高的要求,同時在信號線上增加ESD防護器件來避免熱插拔的損壞。但實際上很多廠家為了節省成本而偷工減料,或者是對熱插拔的防護意識不夠導致設計不合理,使得用戶會出現熱插拔損壞電器的現象產生。
2、 ?VGA不能傳輸音頻
因為視頻是VGA信號,而音頻信號不是,所以VGA不能傳輸音頻,只能傳輸視頻。相信這就是為什么這幾年極度的需求創新轉換器的原因。VGA不支持音頻傳輸也是給很多消費者帶來煩惱,這最好的辦法其實就是購買一款轉換器,VGA轉HDMI或者HDMI轉VGA,達到視頻傳輸的同時還支持音頻信號的輸出,一舉兩得。但是不要只想著轉換器的輸入與輸出成問題,同時想想音頻輸出口,3.5mm是音頻輸出信號的重要連接線。購買時可以考慮想轉換器有沒有帶3.5mm的音頻輸出口,然后另外購買一條音頻線。
3、 VGA接口是一種D型接口,上面共有15針孔,分成三排,每排五個。其中比較重要的是3根RGB彩色分量信號和2根掃描同步信號HSYNC和VSYNC針。其引腳編號圖如下圖所示:
其中每個管腳的詳細定義如下表所示
管腳 | 名稱 | 定義 |
1 | RED | 紅基色(75Ω,0.7Vp-p) |
2 | GREEN | 綠基色(75Ω,0.7Vp-p) |
3 | BLUE | 藍基色(75Ω,0.7Vp-p) |
4 | ID2 | 地址碼(顯示器標識位2) |
5 | GND | 地 |
6 | RGND | 紅色地 |
7 | GGND | 綠色地 |
8 | BGND | 藍色地 |
9 | KEY | 保留 |
10 | SGND | 同步信號地 |
11 | ID0 | 地址碼(顯示器標識位0) |
12 | ID1 | 地址碼(顯示器標識位1) |
13 | HSYNC | 行同步信號 |
14 | VSYNC | 場同步信號 |
15 | ID3 | 地址碼(顯示器標識位3) |
?
VGA接口時序詳解
VGA 顯示器掃描方式從屏幕左上角一點開始,從左向右逐點掃描,每掃描完一行,電子束回到屏幕的左邊下一行的起始位置,在這期間,CRT 對電子束進行消隱,每行結束時,用行同步信號進行同步;當掃描完所有的行,形成一幀,用場同步信號進行場同步,并使掃描回到屏幕左上方,同時進行場消隱,開始下一幀。完成一行掃描的時間稱為水平掃描時間,其倒數稱為行頻率;完成一幀(整屏)掃描的時間稱為垂直掃描時間,其倒數稱為場頻率,即屏幕的刷新頻率,常見的有 60Hz,75Hz 等等,但標準的 VGA 顯示的場頻 60Hz。其掃描示意圖如下圖所示
在對VGA掃描方式有一個直觀的感受以后接下來在看一看VGA接口的詳細時序與各個參數的定義。VGA的詳細時序如下圖所示:
總的來說,VGA的時序主要包括行時序與場時序兩個部分。
其中行時序主要包括:行同步(Hor Sync) 、行消隱(Hor Back Porch) 、行視頻有效(Hor Active Video)和行前肩(Hor Front Porch)這四個參數,行時序的時序圖如下圖所示
而場時序主要包括:場同步(Ver Sync) 、場消隱(Ver Back Porch) 、場視頻有效(Ver Active Video)和場前肩(Ver Front Porch)這四個參數,場時序的時序圖如下圖所示
需要注意的有三點:
1、行時序是以”像素”為單位的, 場時序是以”行”為單位的。
2、VGA 工業標準顯示模式要求:行同步,場同步都為負極性,即同步脈沖要求是負脈沖。
3、VGA 行時序對行同步時間、 消隱時間、 行視頻有效時間和行前肩時間有特定的規范, 場時序也是如此。常用VGA 分辨率時序參數如下表所示
其中:
Pixel Clock = (Screen Refresh Frequency)*(Hor Active Video + Hor Front Porch + Hor Synv Pulse + Hor Back Porch)* (Ver Active Video + Ver Front Porch + Ver Synv Pulse + Ver Back Porch)
以640x480,60Hz這種分辨率格式來說,25.175MHz = 25175000Hz = 60*(640 + 16 + 96 + 48)*(480 + 11 + 2 + 31) = 60 * 800 * 525
三、 目標任務
1、編寫VGA驅動代碼,并用ModelSim對時序進行仿真,然后下載到開發板中使屏幕產生彩色條紋
2、在上個任務的基礎上,把一張存在ROM里面的圖片數據顯示到顯示器上
四、 設計思路與Verilog代碼編寫
4.1、 VGA驅動模塊的接口定義與整體設計
Verilog編寫的VGA模塊除了Red,Green,Blue三基色、行同步HS以及場同步VS以外還要包括時鐘、復位信號。其框圖如下所示
其中:
I_clk是系統時鐘;
I_rst_n是系統復位;
O_hs是行同步信號;
O_vs是場同步信號;
O_red是紅色分量;
O_green是綠色分量;
O_blue是藍色分量;
上面的模塊框圖中沒有看到測試數據(彩條或者圖片)的輸入端口,原因是由于VGA的邏輯比較簡單,所以我準備把發送測試圖案(彩條或者圖片)的邏輯也直接集成到vga_driver模塊中,這樣可能更加方便理解。但是對于實際一個比較復雜的項目來說,最好還是把各個模塊獨立開來,這樣更加方便二次移植。在寫代碼之前,先了解一個關于圖片的分辨率與位深度的知識點。
4.2、 圖片的分辨率、圖片的尺寸與位深度
圖片的分辨率指圖像中存儲的信息量,是每英寸圖像內有多少個像素點,它決定了位圖圖像細節的精細程度。描述分辨率的單位有:dpi(dots per inch)點每英寸、lpi(line per inch)線每英寸和ppi(pixel per inch)像素每英寸。
圖片的尺寸是指一幅圖片長度和寬度各占多少像素,我們平常說的一張640×480的圖片指的就是這張圖片的長度有640個像素點,寬度有480個像素點
位深度是指圖片的每個像素是用多少位(bit)來表示的。比如黑白二色的圖像是數字圖像中最簡單的一種,它只有黑、白兩種顏色,也就是說它的每個像素只有1位顏色,位深度是1,用2的零次冪來表示;考慮到位深度平均分給R, G, B和Alpha,而只有RGB可以相互組合成顏色。所以4位顏色的圖,它的位深度是4,只有2的4次冪種顏色,即16種顏色或16種灰度等級 )。8位顏色的圖,位深度就是8,用2的8次冪表示,它含有256種顏色 ( 或256種灰度等級 )。24位顏色可稱之為真彩色,位深度是24,它能組合成2的24次冪種顏色,即:16777216種顏色 ( 或稱千萬種顏色 ),超過了人眼能夠分辨的顏色數量。當我們用24位來記錄顏色時,實際上是以2^(8×3),即紅、綠、藍 ( RGB )?三基色各以2的8次冪,256種顏色而存在的,三色組合就形成一千六百萬種顏色。除了上面這幾種情況以外,有的圖片的位深度是16位,其中紅基色占5位,綠基色占6位,藍基色占5位,他們一共可以組成2^16中顏色。
在電腦上用選中圖片以后,然后鼠標右鍵在菜單中點擊屬性,然后在詳細信息選項卡中就能查看圖片的各個詳細信息了,上面這張圖片的信息如下圖所示
由上面的信息可知這張圖片的大小為128*128。水平分辨率與垂直分辨率為96dpi(dots per inch),位深度為24-bit。
4.3、 原理圖分析
在寫代碼之前,先來分析一下我的開發板的VGA接口原理圖。由于FPGA輸出的RGB數據為數字信號,而VGA接口的RGB數據為模擬信號,所以需要一個數模轉換器把FPGA輸出的數字信號轉化為VGA接口的模擬RGB數據輸出。一般情況下,為了保證輸出數據的保真度,都會使用一個專用的數模轉換芯片(比如ADV7123)來實現這個數模轉換的功能,但是在我的開發板上為了簡單起見,設計了一個電阻匹配網絡來實現這個數模轉換的功能,FPGA輸出的RGB三基色數字信號一共占16-bit,其中Red分量占5-bit,Green分量占6-bit,Blue分量占5-bit。下面是VGA接口部分的原理圖
4.4、 vga_driver模塊顯示彩條Verilog代碼編寫
有了上面的基礎之后就可以開始著手編寫代碼,現在在回過頭去看行時序與場時序,其實可以發現VGA的時序真的是非常簡單。
對行時序來說,只需要定義一個計數器,當計數器在像素時鐘的作用下計滿一行的總點數后清零,然后利用assign語句在計數值為Hor Sync期間把行時序信號拉低產生一個低脈沖就可以了。場時序與行時序非常類似,當行計數器計滿一行了場計數器才加1,當計滿一場的時間后,計數值清零,然后利用assign語句在Ver Sync期間把場時序信號拉低產生一個低脈沖就OK了。
有了行時序與場時序以后,接下來就是在Hor Active Video和Ver Active Video均有效的期間往Red,Green,Blue三個分量送數據,數據就會在在屏幕上顯示出來了。而Hor Active Video有效的期間正是行計數器的計數值在大于(Hor Sync + Hor Back Porch),小于(Hor Sync + Hor Back Porch + Hor Active Video)的時候,而Ver Active Video有效的期間正是場計數器的計數值在大于(Ver Sync + Ver Back Porch),小于(Ver Sync + Ver Back Porch + Ver Active Video)的時候,所以在代碼里面可以利用assign語句產生一個激活標志,當激活標志為高的時候給Red,Green,Blue三個分量送數據,數據就會在屏幕顯示出來了。
下面以分辨率為640x480為例來編寫vga_driver的代碼,由前面的分辨率時序參數表可知,640x480分辨率的像素時鐘為25.175Hz,但實際并不需要這么精確的時鐘頻率,我們取25MHz就可以了,我的開發板的時鐘頻率為50MHz,所以只需要簡單的寫一個二分頻邏輯就可以得到這個像素時鐘了。如果你想顯示其他分辨率的圖片,比如800x600分辨率的時鐘頻率是40MHz,這時候就需要用FPGA內部的Clocking Wizard IP核來得到這個40MHz的時鐘,Clocking Wizard IP核內部回調用FPGA的PLL資源對輸入頻率進行處理來得到想要的輸出頻率。
下面是VGA接口產生彩條的完整代碼
module vga_driver ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統復位 output reg [4:0] O_red , // VGA紅色分量 output reg [5:0] O_green , // VGA綠色分量 output reg [4:0] O_blue , // VGA藍色分量 output O_hs , // VGA行同步信號 output O_vs // VGA場同步信號 ); // 分辨率為640*480時行時序各個參數定義 parameter C_H_SYNC_PULSE = 96 , C_H_BACK_PORCH = 48 , C_H_ACTIVE_TIME = 640 , C_H_FRONT_PORCH = 16 , C_H_LINE_PERIOD = 800 ; // 分辨率為640*480時場時序各個參數定義 parameter C_V_SYNC_PULSE = 2 , C_V_BACK_PORCH = 33 , C_V_ACTIVE_TIME = 480 , C_V_FRONT_PORCH = 10 , C_V_FRAME_PERIOD = 525 ; parameter C_COLOR_BAR_WIDTH = C_H_ACTIVE_TIME / 8 ; reg [11:0] R_h_cnt ; // 行時序計數器 reg [11:0] R_v_cnt ; // 列時序計數器 reg R_clk_25M ; wire W_active_flag ; // 激活標志,當這個信號為1時RGB的數據可以顯示在屏幕上 ////////////////////////////////////////////////////////////////// //功能:產生25MHz的像素時鐘 ////////////////////////////////////////////////////////////////// always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_clk_25M <= 1'b0 ; else R_clk_25M <= ~R_clk_25M ; end ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // 功能:產生行時序 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_h_cnt <= 12'd0 ; else if(R_h_cnt == C_H_LINE_PERIOD - 1'b1) R_h_cnt <= 12'd0 ; else R_h_cnt <= R_h_cnt + 1'b1 ; end assign O_hs = (R_h_cnt < C_H_SYNC_PULSE) ? 1'b0 : 1'b1 ; ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // 功能:產生場時序 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_v_cnt <= 12'd0 ; else if(R_v_cnt == C_V_FRAME_PERIOD - 1'b1) R_v_cnt <= 12'd0 ; else if(R_h_cnt == C_H_LINE_PERIOD - 1'b1) R_v_cnt <= R_v_cnt + 1'b1 ; else R_v_cnt <= R_v_cnt ; end assign O_vs = (R_v_cnt < C_V_SYNC_PULSE) ? 1'b0 : 1'b1 ; ////////////////////////////////////////////////////////////////// assign W_active_flag = (R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH )) && (R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_ACTIVE_TIME)) && (R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH )) && (R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_ACTIVE_TIME)) ; ////////////////////////////////////////////////////////////////// // 功能:把顯示器屏幕分成8個縱列,每個縱列的寬度是80 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) begin O_red <= 5'b00000 ; O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end else if(W_active_flag) begin if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH)) // 紅色彩條 begin O_red <= 5'b11111 ; // 紅色彩條把紅色分量全部給1,綠色和藍色給0 O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*2)) // 綠色彩條 begin O_red <= 5'b00000 ; O_green <= 6'b111111 ; // 綠色彩條把綠色分量全部給1,紅色和藍色分量給0 O_blue <= 5'b00000 ; end else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*3)) // 藍色彩條 begin O_red <= 5'b00000 ; O_green <= 6'b000000 ; O_blue <= 5'b11111 ; // 藍色彩條把藍色分量全部給1,紅色和綠分量給0 end else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*4)) // 白色彩條 begin O_red <= 5'b11111 ; // 白色彩條是有紅綠藍三基色混合而成 O_green <= 6'b111111 ; // 所以白色彩條要把紅綠藍三個分量全部給1 O_blue <= 5'b11111 ; end else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*5)) // 黑色彩條 begin O_red <= 5'b00000 ; // 黑色彩條就是把紅綠藍所有分量全部給0 O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*6)) // 黃色彩條 begin O_red <= 5'b11111 ; // 黃色彩條是有紅綠兩種顏色混合而成 O_green <= 6'b111111 ; // 所以黃色彩條要把紅綠兩個分量給1 O_blue <= 5'b00000 ; // 藍色分量給0 end else if(R_h_cnt < (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_COLOR_BAR_WIDTH*7)) // 紫色彩條 begin O_red <= 5'b11111 ; // 紫色彩條是有紅藍兩種顏色混合而成 O_green <= 6'b000000 ; // 所以紫色彩條要把紅藍兩個分量給1 O_blue <= 5'b11111 ; // 綠色分量給0 end else // 青色彩條 begin O_red <= 5'b00000 ; // 青色彩條是由藍綠兩種顏色混合而成 O_green <= 6'b111111 ; // 所以青色彩條要把藍綠兩個分量給1 O_blue <= 5'b11111 ; // 紅色分量給0 end end else begin O_red <= 5'b00000 ; O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end end /* //////////////////////////////////////////////////////////////// // 功能:產生黑白相間的格子圖案 //////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) begin O_red <= 5'b00000 ; O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end else if(W_active_flag) begin if((R_h_cnt[4]==1'b1) ^ (R_v_cnt[4]==1'b1)) begin O_red <= 5'b00000 ; O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end else begin O_red <= 5'b11111 ; O_green <= 6'b111111 ; O_blue <= 5'b11111 ; end end else begin O_red <= 5'b00000 ; O_green <= 6'b000000 ; O_blue <= 5'b00000 ; end end */ endmodule
整個代碼的編寫思路與上面的分析基本一致。除了顯示彩條以外,還可以顯示黑白格子,代碼在上面也已經給出。更多更有趣的圖案大家可以自己多試一下。在下載到開發板之前先用ModelSim對整個邏輯進行一下仿真,在測試激勵文件里面只需要給時鐘和復位加上激勵就可以了,非常簡單,我就不貼激勵文件的代碼了,下圖是ModelSim仿真圖
具體的時序細節大家可以自己仿的試一下,整個時序都是沒問題的。
下面是代碼下載到我的開發板中顯示器顯示的彩條圖案
4.5、 如何把一張圖片轉化為存放在ROM中的.coe文件
顯示完彩條以后,接下來就是把一張圖片顯示在液晶顯示器上。完成這個任務的第一步就是要把一張圖片轉化為ROM可以導入的.coe文件,第二部就是把ROM中的圖片數據在有效區域輸出出來,這樣就可以在圖片上顯示一張圖片了。由于我的FPGA型號為XC6SLX45-2CSG324,它內部的BRAM資源十分有限,無法存儲一張完整640*480分辨率的圖片數據,所以接下來的實驗我會把上文那張128x128的圣誕老人的圖片顯示在液晶顯示器上。如果你的FPGA內部資源足夠的話,你可以以我這個例子作為參考把你想顯示的圖片顯示出來。
為了把圖片轉化為.coe文件存放在ROM,這時可以利用Matlab軟件先把圖片轉化為一個三維矩陣,然后把三維矩陣中的Red,Green,Blue分量的顏色數據提取出來按照5-bit Red,6-bit Green, 5-bit Blue的格式進行拼接,最后寫入到.coe文件中,Matlab的完整代碼如下:
clear clc % 利用imread函數把圖片轉化為一個三維矩陣 image_array = imread('333.jpg'); % 利用size函數把圖片矩陣的三個維度大小計算出來 % 第一維為圖片的高度,第二維為圖片的寬度,第三維為圖片的RGB分量 [height,width,z]=size(image_array); % 128*128*3 % imshow(image_array); % 顯示圖片 red = image_array(:,:,1); % 提取紅色分量,數據類型為uint8 green = image_array(:,:,2); % 提取綠色分量,數據類型為uint8 blue = image_array(:,:,3); % 提取藍色分量,數據類型為uint8 % 把上面得到了各個分量重組成一個1維矩陣,由于reshape函數重組矩陣的 % 時候是按照列進行重組的,所以重組前需要先把各個分量矩陣進行轉置以 % 后在重組 % 利用reshape重組完畢以后,由于后面需要對數據拼接,所以為了避免溢出 % 這里把uint8類型的數據擴大為uint32類型 r = uint32(reshape(red' , 1 ,height*width)); g = uint32(reshape(green' , 1 ,height*width)); b = uint32(reshape(blue' , 1 ,height*width)); % 初始化要寫入.coe文件中的RGB顏色矩陣 rgb=zeros(1,height*width); % 因為導入的圖片是24-bit真彩色圖片,每個像素占用24-bit,其中RGB分別占用8-bit % 而我這里需要的是16-bit,其中R為5-bit,G為6-bit,B為5-bit,所以需要在這里對 % 24-bit的數據進行重組與拼接 % bitshift()函數的作用是對數據進行移位操作,其中第一個參數是要進行移位的數據,第二個參數為負數表示向右移,為 % 正數表示向左移,更詳細的用法直接在Matlab命令窗口輸入 doc bitshift 進行查看 % 所以這里對紅色分量先右移3位取出高5位,然后左移11位作為ROM中RGB數據的第15-bit到第11-bit % 對綠色分量先右移2位取出高6位,然后左移5位作為ROM中RGB數據的第10-bit到第5-bit % 對藍色分量先右移3位取出高5位,然后左移0位作為ROM中RGB數據的第4-bit到第0-bit for i = 1:height*width rgb(i) = bitshift(bitshift(r(i),-3),11) + bitshift(bitshift(g(i),-2),5) + bitshift(bitshift(b(i),-3),0); end fid = fopen( 'image.coe', 'w+' ); % .coe文件的最前面一行必須為這個字符串,其中16表示16進制 fprintf( fid, 'memory_initialization_radix=16; '); % .coe文件的第二行必須為這個字符串 fprintf( fid, 'memory_initialization_vector = '); % 把rgb數據的前 height*width-1 個數據寫入.coe文件中,每個數據之間用逗號隔開 fprintf( fid, '%x, ',rgb(1:end-1)); % 把rgb數據的最后一個數據寫入.coe文件中,并用分號結尾 fprintf( fid, '%x;',rgb(end)); fclose( fid ); % 關閉文件指針
基本上每行我都做了詳細的注釋,最后我在強調幾點:
1、運行這段代碼的時候必須把圖片文件和Matlab的.m文件放在同一目錄。imread()函數的參數是一個字符串,這個字符串是圖片的名字。如果圖片與Matlab的.m文件不再同一個目錄,則imread()里面需要填寫圖片的絕對路徑。
2、在Matlab對矩陣進行轉置只需要在矩陣的右邊打一個單引號就可以了
3、Matlab執行循環的效率非常低,所以我在數據進行重組與拼接之前把數據用reshape函數轉化為1維矩陣并用uint32函數擴大了數據的范圍,這樣的好處是重組拼接的時候只需要一個for循環就ok。如果你覺得用reshape和uint32這兩個函數對數據進行預處理太麻煩了,那么你可以使用一個二維for循環進行處理對128*128的矩陣的行和列分別處理。因為這里數據量不算大,所以用單個for循環和二維for循環區別不太大。
4、如果你的開發板硬件支持顯示24-bit真彩色圖片,那么上面的代碼中你就不需要對RGB分量分別右移進行截取操作,直接左移位并相加(拼接)就可以了
4.6、 vga_driver模塊顯示圖片Verilog代碼編寫
有了之前顯示彩條的基礎以后,這個實驗就非常簡單了,只要在把顯示彩條的邏輯修改為從ROM讀數據就可以了。而ROM的配置過程如下所示:
1、選擇Block Memory Generator
2、選擇類型為Single Port ROM
3、選擇ROM的Read Width為16,Read Depth為16384(128*128=16384)
4、導入用Matlab生成的.coe圖片文件
5、其他的所有參數我都保持默認,如果你需要有其他特殊需求的話可以按需修改。
配置好ROM就可以修改發送RGB數據的邏輯了,完整的代碼如下:
module vga_driver ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統復位 output reg [4:0] O_red , // VGA紅色分量 output reg [5:0] O_green , // VGA綠色分量 output reg [4:0] O_blue , // VGA藍色分量 output O_hs , // VGA行同步信號 output O_vs // VGA場同步信號 ); // 分辨率為640*480時行時序各個參數定義 parameter C_H_SYNC_PULSE = 96 , C_H_BACK_PORCH = 48 , C_H_ACTIVE_TIME = 640 , C_H_FRONT_PORCH = 16 , C_H_LINE_PERIOD = 800 ; // 分辨率為640*480時場時序各個參數定義 parameter C_V_SYNC_PULSE = 2 , C_V_BACK_PORCH = 33 , C_V_ACTIVE_TIME = 480 , C_V_FRONT_PORCH = 10 , C_V_FRAME_PERIOD = 525 ; parameter C_IMAGE_WIDTH = 128 , C_IMAGE_HEIGHT = 128 , C_IMAGE_PIX_NUM = 16384 ; reg [11:0] R_h_cnt ; // 行時序計數器 reg [11:0] R_v_cnt ; // 列時序計數器 reg R_clk_25M ; // 25MHz的像素時鐘 reg [13:0] R_rom_addr ; // ROM的地址 wire [15:0] W_rom_data ; // ROM中存儲的數據 wire W_active_flag ; // 激活標志,當這個信號為1時RGB的數據可以顯示在屏幕上 ////////////////////////////////////////////////////////////////// //功能:產生25MHz的像素時鐘 ////////////////////////////////////////////////////////////////// always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_clk_25M <= 1'b0 ; else R_clk_25M <= ~R_clk_25M ; end ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // 功能:產生行時序 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_h_cnt <= 12'd0 ; else if(R_h_cnt == C_H_LINE_PERIOD - 1'b1) R_h_cnt <= 12'd0 ; else R_h_cnt <= R_h_cnt + 1'b1 ; end assign O_hs = (R_h_cnt < C_H_SYNC_PULSE) ? 1'b0 : 1'b1 ; ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // 功能:產生場時序 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_v_cnt <= 12'd0 ; else if(R_v_cnt == C_V_FRAME_PERIOD - 1'b1) R_v_cnt <= 12'd0 ; else if(R_h_cnt == C_H_LINE_PERIOD - 1'b1) R_v_cnt <= R_v_cnt + 1'b1 ; else R_v_cnt <= R_v_cnt ; end assign O_vs = (R_v_cnt < C_V_SYNC_PULSE) ? 1'b0 : 1'b1 ; ////////////////////////////////////////////////////////////////// // 產生有效區域標志,當這個信號為高時往RGB送的數據才會顯示到屏幕上 assign W_active_flag = (R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH )) && (R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_ACTIVE_TIME )) && (R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH )) && (R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_ACTIVE_TIME )) ; ////////////////////////////////////////////////////////////////// // 功能:把ROM里面的圖片數據輸出 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_rom_addr <= 14'd0 ; else if(W_active_flag) begin if(R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH ) && R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_IMAGE_WIDTH - 1'b1) && R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH ) && R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_IMAGE_HEIGHT - 1'b1) ) begin O_red <= W_rom_data[15:11] ; // 紅色分量 O_green <= W_rom_data[10:5] ; // 綠色分量 O_blue <= W_rom_data[4:0] ; // 藍色分量 if(R_rom_addr == C_IMAGE_PIX_NUM - 1'b1) R_rom_addr <= 14'd0 ; else R_rom_addr <= R_rom_addr + 1'b1 ; end else begin O_red <= 5'd0 ; O_green <= 6'd0 ; O_blue <= 5'd0 ; R_rom_addr <= R_rom_addr ; end end else begin O_red <= 5'd0 ; O_green <= 6'd0 ; O_blue <= 5'd0 ; R_rom_addr <= R_rom_addr ; end end rom_image U_rom_image ( .clka(R_clk_25M), // input clka .addra(R_rom_addr), // input [13 : 0] addra .douta(W_rom_data) // output [15 : 0] douta ); endmodule
綁定管腳生成bit文件以后下載到開發板中液晶顯示器上就出現了圣誕老人的圖片
至此,整個VGA的實驗全部完成。
五、 進一步思考
5.1、 如何修改屏幕背景為其他顏色(比如藍色)
上面已經可以讓一張圖片正常在屏幕上顯示出來了,而圖片以外的區域是黑色的,如果我們想把圖片以外的區域改成藍色的其實很簡單,只需要圖片以外區域的RGB分量的Red,Green分量給0,Blue分量給1就可以了
5.2、如何讓128*128圣誕老人的圖片在屏幕中間
上圖的圣誕老人圖片顯示在屏幕的左上角,如果我們想把他顯示在圖片中間位置只需要在
?
?
if(R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_OFFSET ) && R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_OFFSET + C_IMAGE_WIDTH - 1'b1 ) && R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_OFFSET ) && R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_OFFSET + C_IMAGE_HEIGHT - 1'b1 ) )
?
?
這個判斷語句中加上一個固定的C_H_OFFSET與C_V_OFFSET偏移分量就可以了,當然這個偏移分量不能太大,否則會使圖片跑到屏幕外面
5.3、 如何讓128*128圣誕老人的圖片在屏幕上動起來,并且碰到顯示器邊框以后就反彈
上面已經知道修改圖片的位置的方法以后,我們可以把圖片的行和列的偏移量設置成一個reg變量,然后在每一幀結束以后修改這個偏移量就可以了,而場脈沖的下降沿可以看做每一幀結束的標志位,所以為了實現這個功能需要寫一個檢測場脈沖下降沿的邏輯。
至于碰到屏幕以后產生“反彈”的效果實際上就是一個狀態機,這個狀態機一共有四個狀態:
狀態2’b00:圖片向右下方移動
狀態2’b01:圖片向右上方移動
狀態2’b10:圖片向左下方移動
狀態2’b11:圖片向左上方移動
這個狀態機很容易就抽象出來了,但關鍵是狀態的跳變一定要理清楚了,下面這個狀態機的狀態跳變圖
上面這個狀態轉換圖清晰的描述了圖片在屏幕上運動的狀態切換圖,其中每次狀態切換都是在場脈沖的下降沿觸發,這樣才能保證圖片在運動過程中不會閃爍。
完整的代碼如下:
module vga_driver ( input I_clk , // 系統50MHz時鐘 input I_rst_n , // 系統復位 output reg [4:0] O_red , // VGA紅色分量 output reg [5:0] O_green , // VGA綠色分量 output reg [4:0] O_blue , // VGA藍色分量 output O_hs , // VGA行同步信號 output O_vs // VGA場同步信號 ); // 分辨率為640*480時行時序各個參數定義 parameter C_H_SYNC_PULSE = 96 , C_H_BACK_PORCH = 48 , C_H_ACTIVE_TIME = 640 , C_H_FRONT_PORCH = 16 , C_H_LINE_PERIOD = 800 ; // 分辨率為640*480時場時序各個參數定義 parameter C_V_SYNC_PULSE = 2 , C_V_BACK_PORCH = 33 , C_V_ACTIVE_TIME = 480 , C_V_FRONT_PORCH = 10 , C_V_FRAME_PERIOD = 525 ; parameter C_IMAGE_WIDTH = 128 , C_IMAGE_HEIGHT = 128 , C_IMAGE_PIX_NUM = 16384 ; reg [11:0] R_h_cnt ; // 行時序計數器 reg [11:0] R_v_cnt ; // 列時序計數器 reg R_clk_25M ; reg [13:0] R_rom_addr ; // ROM的地址 wire [15:0] W_rom_data ; // ROM中存儲的數據 reg [11:0] R_h_pos ; // 圖片在屏幕上顯示的水平位置,當它為0時,圖片貼緊屏幕的左邊沿 reg [11:0] R_v_pos ; // 圖片在屏幕上顯示的垂直位置,當它為0時,圖片貼緊屏幕的上邊沿 reg R_vs_reg1 ; reg R_vs_reg2 ; wire W_vs_neg ; // 場脈沖下降沿標志 reg [1:0] R_state ; wire W_active_flag ; // 激活標志,當這個信號為1時RGB的數據可以顯示在屏幕上 ////////////////////////////////////////////////////////////////// //功能:產生25MHz的像素時鐘 ////////////////////////////////////////////////////////////////// always @(posedge I_clk or negedge I_rst_n) begin if(!I_rst_n) R_clk_25M <= 1'b0 ; else R_clk_25M <= ~R_clk_25M ; end ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // 功能:產生行時序 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_h_cnt <= 12'd0 ; else if(R_h_cnt == C_H_LINE_PERIOD - 1'b1) R_h_cnt <= 12'd0 ; else R_h_cnt <= R_h_cnt + 1'b1 ; end assign O_hs = (R_h_cnt < C_H_SYNC_PULSE) ? 1'b0 : 1'b1 ; ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// // 功能:產生場時序 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_v_cnt <= 12'd0 ; else if(R_v_cnt == C_V_FRAME_PERIOD - 1'b1) R_v_cnt <= 12'd0 ; else if(R_h_cnt == C_H_LINE_PERIOD - 1'b1) R_v_cnt <= R_v_cnt + 1'b1 ; else R_v_cnt <= R_v_cnt ; end assign O_vs = (R_v_cnt < C_V_SYNC_PULSE) ? 1'b0 : 1'b1 ; ////////////////////////////////////////////////////////////////// assign W_active_flag = (R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH )) && (R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + C_H_ACTIVE_TIME )) && (R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH )) && (R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + C_V_ACTIVE_TIME )) ; ////////////////////////////////////////////////////////////////// // 功能:把ROM中的圖片數據顯示到屏幕上 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) R_rom_addr <= 14'd0 ; else if(W_active_flag) begin if(R_h_cnt >= (C_H_SYNC_PULSE + C_H_BACK_PORCH + R_h_pos ) && R_h_cnt <= (C_H_SYNC_PULSE + C_H_BACK_PORCH + R_h_pos + C_IMAGE_WIDTH - 1'b1) && R_v_cnt >= (C_V_SYNC_PULSE + C_V_BACK_PORCH + R_v_pos ) && R_v_cnt <= (C_V_SYNC_PULSE + C_V_BACK_PORCH + R_v_pos + C_IMAGE_HEIGHT - 1'b1) ) begin O_red <= W_rom_data[15:11] ; O_green <= W_rom_data[10:5] ; O_blue <= W_rom_data[4:0] ; if(R_rom_addr == C_IMAGE_PIX_NUM - 1'b1) R_rom_addr <= 14'd0 ; else R_rom_addr <= R_rom_addr + 1'b1 ; end else begin O_red <= 5'd0 ; O_green <= 6'd0 ; O_blue <= 5'd0 ; R_rom_addr <= R_rom_addr ; end end else begin O_red <= 5'd0 ; O_green <= 6'd0 ; O_blue <= 5'd0 ; R_rom_addr <= R_rom_addr ; end end ////////////////////////////////////////////////////////////////// // 功能:產生場脈沖的下降沿標志,在這個標志用來修改R_h_pos和R_v_pos // 兩個參數,從而改變圖片的位置 ////////////////////////////////////////////////////////////////// always @(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) begin R_vs_reg1 <= 1'b0 ; R_vs_reg2 <= 1'b0 ; end else begin R_vs_reg1 <= O_vs ; R_vs_reg2 <= R_vs_reg1 ; end end assign W_vs_neg = ~R_vs_reg1 & R_vs_reg2 ; ////////////////////////////////////////////////////////////////// // 功能:使圖片移動的狀態機 ////////////////////////////////////////////////////////////////// always@(posedge R_clk_25M or negedge I_rst_n) begin if(!I_rst_n) begin R_h_pos <= 12'd0 ; R_v_pos <= 12'd0 ; R_state <= 2'b00 ; end else if(W_vs_neg) begin case(R_state) 2'b00: // 圖片往右下方移動 begin R_h_pos <= R_h_pos + 1 ; R_v_pos <= R_v_pos + 1 ; if(R_h_pos + C_IMAGE_WIDTH == C_H_ACTIVE_TIME) // 如果碰到右邊框 R_state <= 2'b10 ; else if((R_v_pos + C_IMAGE_HEIGHT) == C_V_ACTIVE_TIME) // 如果碰到下邊框 R_state <= 2'b01 ; end 2'b01: // 圖片往右上方移動 begin R_h_pos <= R_h_pos + 1 ; R_v_pos <= R_v_pos - 1 ; if(R_h_pos + C_IMAGE_WIDTH == C_H_ACTIVE_TIME) // 如果碰到右邊框 R_state <= 2'b11 ; else if(R_v_pos == 1) // 如果碰到上邊框 R_state <= 2'b00 ; end 2'b10: // 圖片往左下方移動 begin R_h_pos <= R_h_pos - 1 ; R_v_pos <= R_v_pos + 1 ; if(R_h_pos == 1) // 如果碰到左邊框 R_state <= 2'b00 ; else if(R_v_pos + C_IMAGE_HEIGHT == C_V_ACTIVE_TIME) // 如果碰到下邊框 R_state <= 2'b11 ; end 2'b11: // 圖片往左上方移動 begin R_h_pos <= R_h_pos - 1 ; R_v_pos <= R_v_pos - 1 ; if(R_h_pos == 1) // 如果碰到上邊框 R_state <= 2'b01 ; else if(R_v_pos == 1) // 如果碰到左邊框 R_state <= 2'b10 ; end default:R_state <= 2'b00 ; endcase end end rom_image U_rom_image ( .clka(R_clk_25M), // input clka .addra(R_rom_addr), // input [13 : 0] addra .douta(W_rom_data) // output [15 : 0] douta ); endmodule
代碼里面碰到左邊框和碰到上邊框的判決方式為?if(R_h_pos == 1)?和if(R_h_pos == 1),我之所以設置為1而不設置為0是因為我發現設置為0的時候當圖片碰到左邊框和上邊框會閃爍一下,具體原因大家自己分析一下。
六、 總結
VGA的時序是非常適合初學者入門的,相對于其他接口時序來說,VGA時序確實是最簡單的,所以初學者最好能自己把代碼從頭到尾敲一遍,然后用ModelSim仿一下,看一看中間變量的波形以加深對VGA時序的理解。
編輯:黃飛
?
評論
查看更多