1. 程式人生 > >VGA系列之一:VGA顯示網路圖片

VGA系列之一:VGA顯示網路圖片

一休哥是在讀研究生的時候開始正式接觸FPGA的,之所以這麼說呢,是因為之前本科參加電賽的時候也學過一點FPGA的知識,可惜學習週期太短導致那次電賽慘敗。可能世上就是有這麼巧的事,剛上研究生的第一天,老闆就給了我一塊FPGA板,讓我自己玩去,從此就踏上了這條不歸路。
好了,閒話不多說,接下來我們來講講如何用FPGA實現VGA顯示網路圖片。這裡我們先提出幾個問題,通過解決這幾個問題,從而實現工程效果。
1、 如何用FPGA實現VGA顯示
2、 網路圖片和VGA顯示有何區別
3、 VGA如何顯示圖片

1、 如何用FPGA實現VGA顯示

首先,我們需要清楚一個概念,VGA是一個外設模組,VGA模組有兩個介面,一個介面用於連線FPGA,另一個用於連線VGA線,VGA線的另一頭我們常常會與顯示器相連。連線VGA線的介面一般都是一個標準VGA介面的母口。而與FPGA相連的介面有很多種,在這裡,我介紹其中的一種。


這裡寫圖片描述

上圖就是VGA模組的硬體電路,可以看到,在圖片的左邊就是與FPGA相連的介面訊號,一共有29個訊號,我們可以大致分成三類

  • 恆定不變的訊號 (VGA_BLANK,VGA_SYN)
  • 時序控制訊號(VGA_CLK,VGA_HS,VGA_VS)
  • 資料訊號(VGA_R[0-7],VGA_G[0-7],VGA_B[0-7])

在使用VGA顯示時我們需要將VGA_BLANK預設置1,VGA_SYNC預設置0。VGA_CLK是VGA顯示的主時鐘,它的頻率決定了VGA顯示的解析度。VGA_HS是VGA的水平同步訊號,它決定了VGA顯示的寬度。VGA_VS是VGA的垂直同步訊號,它決定了VGA顯示的高度。資料訊號中R、G、B三種顏色的小大都是8位,所以這個VGA模組顯示的顏色深度為24位,也就是說VGA顯示的是24位真彩色,顯示的質量非常高。
剛才提到過VGA顯示的解析度、寬度和高度。我們需要知道,VGA也是一個視訊傳輸標準,所以VGA的解析度也就是視訊的解析度。我們通過檢視視訊格式手冊

可以知道VGA的解析度有哪些。在這裡,我選擇了一個最常見的解析度640*[email protected],這裡的640、480表示水平和垂直方向的畫素點個數,也就表示VGA輸出了一個60Hz的640*480的視訊訊號。
選擇了VGA的解析度之後,我們就可以開始著手編寫程式了嗎?其實不然,我們還不知道VGA顯示的時序,這點我們也可以檢視視訊格式手冊
這裡寫圖片描述

在上圖中,我們可以看到VGA_HS(HSYNC)訊號是一個週期訊號,在一個週期內,VGA_HS的低電平時間為96個VGA_CLK訊號週期,高電平時間為704個VGA_CLK訊號週期。VGA的資料訊號在VGA_HS高電平的第49個VGA_CLK訊號週期開始有效,一直持續到VGA_HS高電平的第688個VGA_CLK訊號週期。VGA_VS(VSYNC)

訊號也是一個週期訊號,在一個週期內,VGA_VS的低電平時間為2個VGA_HS訊號週期,高電平時間為523個VGA_HS訊號週期。VGA的資料訊號在VGA_VS高電平的第34個VGA_HS訊號週期開始有效,一直持續到高電平的第513個VGA_HS訊號週期。為了更直觀的表達這一時序,我們用下面這個圖表示。
這裡寫圖片描述

總的來說,其實VGA的顯示時序就是一個逐行掃描的過程,每完成一個行掃描(即VGA_HS訊號經過一個週期),則開始掃描下一行。只有當VGA_HS和VGA_VS同時有效,VGA資料訊號才有效。
講完VGA的顯示時序後,我們還需要計算出VGA_CLK訊號的頻率。我們需要按照這個公式計算:HS_total×VS_total×60Hz。
其中,HS_total為VGA_HS訊號的一個週期內包含的VGA_CLK訊號週期個數,VS_total為VGA_VS訊號的一個週期內包含的VGA_HS訊號週期個數,解析度640*[email protected]時,HS_total為800,VS_total為525。
因此VGA_CLK訊號的頻率為800×525×60Hz=25.2MHz
接下來,我們開始編寫VGA的顯示時序程式碼,我們用兩個計數器hsync_cnt和vsync_cnt來實現。部分程式碼如下:

/* 時序邏輯,用來給hsync_cnt暫存器賦值 */
always @ (posedge CLK_VGA or negedge RST_N)
begin
    if(!RST_N)                                  
        hsync_cnt <= 16'b0;             
    else
        hsync_cnt <= hsync_cnt_n;           
end

/* 組合邏輯,水平掃描計數器,在啟動標誌拉高後再計數,每個時鐘迴圈遞增 */
always @ (*)
begin
    if(hsync_cnt == `HSYNC_D - 16'h1)
        hsync_cnt_n = 16'b0;                    
    else
        hsync_cnt_n = hsync_cnt + 1'b1; 
end

/* 時序邏輯,用來給vsync_cnt暫存器賦值 */
always @ (posedge CLK_VGA or negedge RST_N)
begin
    if(!RST_N)                                  
        vsync_cnt <= 16'b0;                 
    else
        vsync_cnt <= vsync_cnt_n;           
end

/* 組合邏輯,垂直掃描計數器,每次水平掃描計數器計滿時迴圈遞增 */
always @ (*)
begin
    if((vsync_cnt == `VSYNC_R - 16'h1) && (hsync_cnt == `HSYNC_D - 16'h1))
        vsync_cnt_n = 16'b0;                    
    else if(hsync_cnt == `HSYNC_D - 16'h1)      
        vsync_cnt_n = vsync_cnt + 1'b1; 
    else
        vsync_cnt_n = vsync_cnt;            
end

/* 時序邏輯,用來給VGA_HSYNC暫存器賦值 */
always @ (posedge CLK_VGA or negedge RST_N)
begin
    if(!RST_N)                                  
        VGA_HSYNC <= 1'b0;                          
    else
        VGA_HSYNC <= VGA_HSYNC_N;                       
end

/* 組合邏輯,在B C D區間拉高水平掃描訊號 */
always @ (*)
begin   
    if(hsync_cnt == `HSYNC_A - 16'h1)               
        VGA_HSYNC_N = 1'b1;                     
    else if(hsync_cnt == `HSYNC_D - 16'h1)
        VGA_HSYNC_N = 1'b0;
    else
        VGA_HSYNC_N = VGA_HSYNC;
end

/* 時序邏輯,用來給VGA_VSYNC暫存器賦值 */
always @ (posedge CLK_VGA or negedge RST_N)
begin
    if(!RST_N)                                  
        VGA_VSYNC <= 1'b0;                          
    else
        VGA_VSYNC <= VGA_VSYNC_N;                       
end

/* 組合邏輯,在P Q R區間拉高垂直掃描訊號 */
always @ (*)
begin   
    if(vsync_cnt == `VSYNC_O - 16'h1 && hsync_cnt == `HSYNC_D - 16'h1)              
        VGA_VSYNC_N = 1'b1;                     
    else if((vsync_cnt == `VSYNC_R - 16'h1) && (hsync_cnt == `HSYNC_D - 16'h1))
        VGA_VSYNC_N = 1'b0;
    else
        VGA_VSYNC_N = VGA_VSYNC;
end

2、 網路圖片和VGA顯示有何區別

在第一個問題中,我們知道了VGA顯示解析度為640*480,顏色深度為24位真彩色。但是,網路圖片一般不會完全符合這兩個引數,因此,我們需要藉助一個軟體工具轉換一下。
1、 首先,我們在網上隨意找到一副圖片。
這裡寫圖片描述
這裡寫圖片描述

2、 可以看到這個圖片的引數為解析度為700*718,顏色深度為8位。我們用軟體Image2Lcd開啟該圖片並設定相應引數,可以發現我們得到一張解析度為174*179,顏色深度為8位的bmp圖片。由於我使用的FPGA晶片的片記憶體儲器資源較少,而為了將生成的mif檔案順利匯入Rom IP核中,我們需要壓縮圖片。這裡為什麼要轉換成bmp圖片呢,因為bmp格式是非壓縮的,資料格式比較簡單容易處理,方便我們將這個圖片存取FPGA中。
這裡寫圖片描述

3、 我們知道FPGA不能直接讀取圖片,我們要將圖片轉換成mif檔案,存入FPGA的rom中。因此,這裡我們將製作一個包含圖片全部有效資料的mif檔案。這裡,我們要使用另一個常用的工具MATLAB,用m語言來實現。程式碼如下:

clear;  
clc;  
n=31146;%174*179  
mat = imread('tu1.bmp');%讀取.bmp檔案
mat = double(mat);
fid=fopen('bmp_data.mif','w');%開啟待寫入的.mif檔案  
fprintf(fid,'WIDTH=8;\n');%寫入儲存位寬8位  
fprintf(fid,'DEPTH=31146;\n');%寫入儲存深度31146
fprintf(fid,'ADDRESS_RADIX=UNS;\n');%寫入地址型別為無符號整型  
fprintf(fid,'DATA_RADIX=HEX;');%寫入資料型別為無符號整型   
fprintf(fid,'CONTENT BEGIN\n');%起始內容  
for i=0:n-1  
    x = mod(i,174)+1;  %174為bmp圖片的水平解析度
    y = fix(i/174)+1;  
    k = mat(y,x);
fprintf(fid,'\t%d:%x;\n',i,k);  
end  
fprintf(fid,'END;\n');  
fclose(fid);%關閉檔案 

3、 VGA如何顯示圖片

解決了上述兩個問題之後,終於可以顯示圖片了。
首先,我們呼叫一個Rom IP核,由於我們顯示的bmp圖片解析度為174*179,顏色深度為8位,所以我們設定Rom IP核如下圖所示。
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
如圖中所示,我們設定了一個32768*8bit大小的Rom,並且使輸出q受時鐘控制,加入mif檔案。
VGA顯示圖片的程式碼也十分簡單,我們以VGA顯示有效區設定了vga_x和vga_y座標變數,定義了兩個訊號用來控制產生Rom表的地址訊號,與VGA顯示的資料訊號。由於bmp圖的深度為8bit,所以我們按照332來分配紅綠藍三色資料,並將末尾置1。具體程式碼如下:

assign vga_x = hsync_cnt - `HSYNC_B;
assign vga_y = vsync_cnt - `VSYNC_P;

Rom                     Rom_init
(
    .clock          (CLK_25M            ),
    .address            (bmp_rom_add    ),
    .q                  (bmp_rom_data   )
);

//組合電路,用於生成圖片位置訊號
assign  bmp_add = (vga_x >= `BMP1_X - 8'h3) && (vga_x < `BMP1_X + `BMP1_W - 8'h3) && (vga_y >= `BMP1_Y) && (vga_y < `BMP1_Y + `BMP1_H);     
//組合電路,用於生成圖片使能訊號
assign  bmp_en = (vga_x >= `BMP1_X) && (vga_x < `BMP1_X + `BMP1_W) && (vga_y >= `BMP1_Y) && (vga_y < `BMP1_Y + `BMP1_H);    

//時序電路,用來給bmp_rom_add暫存器賦值
always @ (posedge CLK_25M or negedge RST_N)
begin
    if(!RST_N)
        bmp_rom_add <= 1'h0;
    else
        bmp_rom_add <= bmp_rom_add_n;
end

//組合電路,用於生成bmp_rom_add
always @ (*)
begin
    if((vga_x == `BMP1_X - 8'h3) && (vga_y == `BMP1_Y) && bmp_add)
        bmp_rom_add_n = 1'h0;
    else if(bmp_add)
        bmp_rom_add_n = bmp_rom_add + 1'b1;
    else
        bmp_rom_add_n = bmp_rom_add;
end


/* 時序電路,用來給VGA_DATA暫存器賦值 */
always @ (posedge CLK_25M or negedge RST_N)
begin
    if(!RST_N)
        VGA_DATA <= 1'b0;
    else
        VGA_DATA <= VGA_DATA_N;
end

/* 組合電路,用來生成VGA_DATA */
always @ (*)
begin
    if(bmp_en)
        VGA_DATA_N = {bmp_rom_data[7:5],5'b11111,bmp_rom_data[4:2],5'b11111,bmp_rom_data[1:0],6'bb111111}; 
    else if(hsync_cnt > `HSYNC_B && hsync_cnt <= `HSYNC_B + 16'd128 && vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)
        VGA_DATA_N = 24'hFF0000;
    else if(hsync_cnt > `HSYNC_B + 16'd128 && hsync_cnt <= `HSYNC_B + 16'd256 && vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)
        VGA_DATA_N = 24'hFFFF00;
    else if(hsync_cnt > `HSYNC_B + 16'd256 && hsync_cnt <= `HSYNC_B + 16'd384 && vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)
        VGA_DATA_N = 24'h00FF00;
    else if(hsync_cnt > `HSYNC_B + 16'd384 && hsync_cnt <= `HSYNC_B + 16'd512 && vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)
        VGA_DATA_N = 24'h00FFFF;
    else if(hsync_cnt > `HSYNC_B + 16'd512 && hsync_cnt <= `HSYNC_B + 16'd640 && vsync_cnt > `VSYNC_P && vsync_cnt < `VSYNC_Q)
        VGA_DATA_N = 24'h0000FF;
    else
        VGA_DATA_N = 24'd0;
end

大家可能會比較疑惑,為什麼用來控制產生Rom表的地址訊號bmp_add會比控制VGA顯示圖片的訊號bmp_en 提前三個時鐘 。那是因為我們在讀取Rom表資料時存在延時,經過我們signaltap採集後發現,原本作為地址70的輸出資料FF比地址70 慢兩個時鐘 ,即bmp_rom_data的輸出會比bmp_rom_add延遲兩個時鐘,而VGA_DATA又比bmp_rom_data 延遲1個時鐘 ,因此bmp_add訊號需要比bmp_en 提前三個時鐘
這裡寫圖片描述
最後,奉上一張效果圖。
這裡寫圖片描述