每日熱議!VGA接口原理與Verilog實現編程案例解析

2023-07-01 09:19:58 來源:FPGA之家

一、 軟件平臺與硬件平臺


(資料圖片僅供參考)

軟件平臺:

1、操作系統:Windows-8.1

2、開發套件:ISE14.7

3、仿真工具:ModelSim-10.4-SE

硬件平臺:

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針。其引腳編號圖如下圖所示:

其中每個管腳的詳細定義如下表所示

管腳名稱定義
1RED紅基色(75Ω,0.7Vp-p)
2GREEN綠基色(75Ω,0.7Vp-p)
3BLUE藍基色(75Ω,0.7Vp-p)
4ID2地址碼(顯示器標識位2)
5GND
6RGND紅色地
7GGND綠色地
8BGND藍色地
9KEY保留
10SGND同步信號地
11ID0地址碼(顯示器標識位0)
12ID1地址碼(顯示器標識位1)
13HSYNC行同步信號
14VSYNC場同步信號
15ID3地址碼(顯示器標識位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 ornegedge 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    ;        endend*/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的完整代碼如下:

clearclc% 利用imread函數把圖片轉化為一個三維矩陣image_array = imread("333.jpg");% 利用size函數把圖片矩陣的三個維度大小計算出來% 第一維為圖片的高度,第二維為圖片的寬度,第三維為圖片的RGB分量[height,width,z]=size(image_array);   % 128*128*3% imshow(image_array); % 顯示圖片red   = image_array(:,:,1); % 提取紅色分量,數據類型為uint8green = image_array(:,:,2); % 提取綠色分量,數據類型為uint8blue  = 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-bitfor i = 1:height*width    rgb(i) = bitshift(bitshift(r(i),-3),11) + bitshift(bitshift(g(i),-2),5) + bitshift(bitshift(b(i),-3),0);endfid = 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          endrom_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         endassign 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               endend      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時序的理解。

編輯:黃飛

標簽:

上一篇:基于SVPWM以及實際MCU定時器輸出脈沖的中心對齊模式
下一篇:最后一頁