FPGA筆記之verilog語言(基礎語法篇)

FPGA筆記之verilog語言(基礎語法篇)

寫在前面:
verilogHDL語言是面向硬件的語言,換句話說,就是用語言的形式來描述硬件線路。因此與C語言等軟件語言不同,如果想要在實際的電路中實現,那麼在進行verilog語言編寫時,就需要提前有個硬件電路的構思和想法,同時,在編寫verilog語言時,應該採用可綜合的語句和結構。

1. verilog 的基礎結構

1.1 verilog設計的基本單元——module

在數字電路中,我們常常把一些複雜的電路或者具有特定功能的電路封裝起來作爲一個模塊使用。以後在運用這種模塊化的封裝時,我們只需要知道:1.模塊的輸入是什麼;2.模塊的輸出是什麼;3.什麼樣的輸入對應什麼樣的輸出。而中間輸入是經過什麼樣的電路轉化爲輸出就不是我們在使用時需要特別重視的問題。當很多個這樣的模塊相互組合,就能構成一個系統,解決一些複雜的問題。verilog語言的基礎結構就是基於這種思想。verilog中最基本的模塊是module,就可以看做是一個封裝好的模塊,我們用verilog來寫很多個基本模塊,然後再用verilog描述多個模塊之間的接線方式等,將多個模塊組合得到一個系統。
那麼一個module應該具有哪些要素呢?首先對於一個module,我們應該設計好其各個I/O,以及每個I/O的性質,用於與模塊外部的信號相聯繫,讓使用者知道如何連線。其次,作爲開發者,我們需要自己設計模塊內部的線路來實現所需要的功能。因此需要對模塊內部出現的變量進行聲明,同時通過語句、代碼塊等實現模塊的功能。綜上所述,我們把一個module分成以下五個部分:

  1. 模塊名
  2. 端口定義
  3. I/O說明
  4. 內部信號的聲明
  5. 模塊功能實現
    例:
/////////////////////////////////////////////////////////////////
//module    模塊名 (端口1,端口2,端口3);
//I/O說明 
//內部信號說明
//模塊功能實現
//endmodule
////////////////////////////////////////////////////////////////

////////////////////////////////////
//module    模塊名 (端口1,端口2,端口3);
////////////////////////////////////
module      FreDevider    (                  
                                        Clock,
                                        rst,
                                        Clkout
                                        );
                                        
                                        
////////////////////////////////////
//I/O說明 
////////////////////////////////////
input Clock;
input rst;
output Clkout;


////////////////////////////////////
//內部信號說明
////////////////////////////////////
reg Clkout;


////////////////////////////////////
//模塊功能實現
////////////////////////////////////
    always@(posedge Clock or posedge rst)
    begin
        if(rst)
            Clkout<=0;
        else
            Clkout<=~Clkout;
    end


////////////////////////////////////
//endmodule
////////////////////////////////////
endmodule


1.2 module的使用

當我們已經寫好了一種類型的module,那我們在使用的時候,就可以直接調用module。使用方法是 模塊名+實例名+端口聲明+信號聲明。一個模塊可以定義多個實例。
例如:使用上述已經寫好的module

//已經定義過一個叫FreDevider的module
FreDevider    uut1(                                            //模塊名:FreDevider    實例名:uut1
                  .Clock(clock_signal),           // 端口聲明: .端口名     信號聲明:  (信號名)
                  .rst(rst_signal),
                  .Clkout(clkout_signal)
                  );                                

這裏涉及到了之前module定義和變量聲明等問題,首先module的定義決定了模塊名和端口名,其次module中的信號的處理方式決定了這裏的信號名是reg還是wire型。信號可以看做是一個大的模塊的輸入輸出或中間變量。

1.3 I/O的說明

I/O的類型共有3類:input,output,inout
前兩個比較好理解,分別是輸入和輸出
而inout則是雙向端口,既可以當做輸入,也可以當做輸出,這種雙向端口具有雙向傳輸的能力,比較節約端口,同時適合作爲總線等需要但是要保證在某一時刻,其只進行某一方向的傳輸,也就是說,應避免兩個方向由需要傳輸這種情況。因此在實際工程實例中,我們在比較底層的模塊編寫時,是比較少的用inout端口,而是在較高一層的模塊,通過控制信號來實現inout端口的使用。同時inout端口在實際使用時還有許多問題,對於初學者,先理解整個架構,再對細節補充,所以這部分內容放在高級語法篇講述。
I/O說明的形式:
I/O的說明可以放在端口定義後,也可以在端口定義時同時說明。例:

module   FreDevider    (                  
                       input  Clock,
                       input  rst,
                       output  Clkout
                       );

同時,也可以在說明I/O口的時候,同時說明信號的位數
例:

module    FreDevider    (                  
                        input  Clock,
                        input  rst,
                        input    [15:0]     x;      //16位輸入
                        output  [31:0]     y;      //32位輸出
                        );
                                        

1.3 內部信號的聲明

如果說端口的定義連接了模塊與外部世界,是一個模塊世界中可進可出的若干個大門,那麼信號就是一個個行人。要實現模塊的功能,就要對各個信號進行處理和變換。有些信號是外部輸入的,有些信號是要輸出給外部的,還有些信號是信號變換的中間變量。就像行人有些是外面進來的,有些是要去往外面世界的,有些是住在模塊世界裏的。這些參與到模塊功能實現的所有信號,都要對其信號的屬性進行規定,就像每個行人都有各自的身份。總而言之,分清楚端口和信號的區別,端口是固定的,信號是變化的。端口是門,信號是人。
信號的屬性有reg、wire、paramater三種種,其中reg又稱爲寄存器型,wire又稱爲線性,每個信號都要定義其屬性,但是對於模塊的輸入信號,其屬性必須不是reg型,一般爲wire型。又因爲對於沒有聲明的信號,其默認爲wire型,因此在定義時,我們只需要定義輸出信號的類型和中間變量的類型即可。

reg   a;
wire  b;

1.4 模塊功能的實現

對於一個硬件電路來說,已經有了模塊名,端口和端口名,信號與信號屬性,剩下的就是通過硬件電路來實現各個信號之間的邏輯功能。這比部分的知識就和我們在大學時期學的數電的知識聯繫緊密,通常可以分爲沒有時間,只有邏輯轉換的邏輯電路和有時間和狀態轉換的時序邏輯電路兩種形式,再加上通過調用已經模塊化的實例元件來參與更高一級的模塊設計。所以在一個模塊功能的實現方法中,通常有三種類型:

  1. 用assign聲明語句
    assign語句用於驅動線網型的變量,聲明語句右邊表達式的變量是敏感信號,當右邊的值發生改變時,立即計算左邊的結果,並進行表達。也就是說,當輸入變化時,輸出也隨之變化。這種特性就像是數字電路里的組合邏輯電路。
assign   a_not=~a;
assign   c=a&b;
  1. 採用實例化的元件
    採用實例的元件方法已經在前面講過,除了採用module的實例化元件以外,還可以採用IP核的形式來實現。
  2. 採用always語句塊
    always語句塊既能描述組合邏輯電路,又能描述時序邏輯電路。與assign不同的是,always語句後面的觸發條件是持續敏感,也就是每時每刻都在執行或者判斷的。後面也會更加詳細的區別assign和always語句塊。
//生成時鐘信號
always #5 clk=~clk;

//組合邏輯電路:二選一多路器
reg c_out;
always @(a_in or b_in or sel)
    if(sel)
        c_out=a_in;
    else
        c_out=b_in;
 
 //時序邏輯電路:二分頻模塊
 reg d;
 always @(posedge clk or posedge rst)
 begin
     if(rst)
         d<=1'b0;
     else
         d=~d;

1.5 第一章學習筆記

  1. verilog是一門描述硬件電路的語言,在語言結構上和C語言有相同的地方,所以學習起來比較容易,但是verilog終究還是要最終用硬件的方法去實現,因此在編程時,不能空想內部的邏輯,而是通過電路圖、時序圖等方式,將功能先用硬件的方法表示出來,然後再用軟件的語言來實現。
  2. verilog的使用離不開對數字電路基礎知識的掌握,所以在進行verilog的學習的同時,應該建立在對數字電路基礎知識的學習之上。語言是描述思想的工具,思想纔是解決問題的關鍵。

2. verilog 的數據類型

verilog的數據類型主要分爲四類,整數型、實數型、字符串型、參數型,這四種類型都可以在編程中出現,但是在實際硬件層面綜合時,卻並不能保證我們所寫的數據類型能夠被表達。因爲在實際的電路中,無論是哪種類型的數據,最後都要轉化爲1、0、X、Z(X、Z分別是未知態和高阻態)這四種信號狀態。因此掌握verilog的數據類型,最根本的就是掌握各種數據類型在實際電路中的表達。

2.1 整數型

2.1.1 數制

在verilog中,整數有四種數制,分別是
十進制:代號d/D
二進制:代號b/B
八進制:代號o/O
十六進制:代號h/H
一個整數對於進制的不同表達,只是數的表達形式不同,但是數字本身代表的數學含義是相同的。而在硬件電路中,所有的數字都是通過二進制來進行表達的。因此在編程過程中,對於數字的數學表示,我們可以採用不同的進制,但是對於數字的物理實現,我們一定是採用二進制的方式來進行硬件上的實現。因此在實際編程過程中出現的數字,我們應該從二進制的表達中理解其變化的規律。
這種思想可以在verilog的數字表達中得到體現,一般來說,我們通過以下的方式來表示一個整數:
&lt;&gt;&lt;&gt;&lt;&gt;&lt;&gt;&lt;位寬&gt;&lt;&#x27;&gt;&lt;進制&gt;&lt;數字&gt;
舉例說明:

8'd23                                          //位寬8  十進制  數字23
8'b00010111                                    //位寬8  二進制  數字23  
8'o27                                          //位寬8  八進制  數字23
8'h17                                          //位寬8  十六進制  數字23

對於任何一個數字,無論其後面是什麼進制,什麼數字,最後都要轉化爲指定位寬的二進制數字來表達。因此有可能在進製表達時,出現實際的二進制數字和我們編程所寫的二進制數字不同。或者其他非標準的表達方法。下面我們說明幾種常見的情況:

  1. 位寬大於實際數字二進制的寬度
    位寬大於實際二進制的寬度時,有四種可能:
    1.二進制數字最高位爲1,高位部分用0補全
    2.二進制數字最高位爲0,高位部分用0補全
    3.二進制數字最高位爲X,高位部分用X補全
    4.二進制數字最高位爲Z,高位部分用Z補全
    例如:
6'b101      //實際上是  000101
6'b001      //實際上是  000001
6'bx11      //實際上是  xxxx11
6'bz11      //實際上是  zzzz11
  1. 位寬小於實際二進制的寬度
    當位寬小於實際二進制寬度時,一般情況下是將超過的位數捨去,只保留給定位寬內的數,例如:
4'b101011        //實際上是  1011
  1. 位寬省略不寫
    一般情況下,位寬省略不寫,默認爲是32位寬的數,或者根據操作系統和編程軟件的設定來定義。例如:
'b10111     //位寬爲32的二進制數 代表數字23
  1. 數制位寬都省略不寫
    這種情況下,默認爲數字是32位寬的十進制數。例如
23          //位寬爲32的十進制數 代表數字23
  1. 下劃線表達法
    當需要表達的數字位數較多時,爲了書寫和觀察方便,常常使用下劃線符號來輔助表達。採用下劃線將相鄰若干個數字分隔開,使其表達更清晰。但是注意下劃線只能出現在數字中,不能出現在位寬和進制中,並且數字的第一位不能是下劃線。例如:
16'b0100_1011_1101_0110                      //正確
16'b_1110_1101_0101_0001                     //錯誤

2.1.2 負數

負數在verilog中的表達是很簡單的,直接在數字的前面添加符號就表示負數。但是在實際的二進制編碼中,負數是通過其正數的補碼的形式進行實現的。

-8'd23      //就是-23,但是在二進制代碼中 是用8'b11101001來表示

此時對於一個8位的二進制數,其第一位爲符號位,後7位才爲有效的數字位。
由此可見,一個二進制,當首位爲1時,有可能表示的是一個正數,也有可能表示的是一個負數,那麼在實際硬件電路中,如何區分這兩種情況呢?
一般來說,對於硬件中的信號,硬件處理時是默認爲正整數的,因此當我們輸入一個負值,實際上產生的是我們輸入的值的正數部分的補碼所表示的正值(這裏有點繞,距離來說,我輸入-8’d23,得到的是23的補碼8’b11101001,這個補碼被硬件默認是正數,即8’d233)。那麼我們在進行二進制的數據的變換時,就應該回歸其硬件中的表達,然後在此基礎上通過自己的程序來實現當爲負數時的轉化。
例如:我之前寫過的粗插補算法

		    if(x_rough[15]==0)                //當插補數據爲正數時,將粗插補數據進行四分,並將餘數添加到第一個精插補結果中
	            begin
	                lx[1:0]=x_rough[1:0];
		            x4=(x_rough>>2);
		            x3=(x_rough>>2);
		            x2=(x_rough>>2);
		            x1=((x_rough>>2)+lx);
		        end
	       else                              //當插補數據爲負數時,將插補數據求補碼,進行四分等操作後,再將結果求補碼得到負數(對絕對值進行操作)
	            begin
	                x_complement[15:0]=(~(x_rough[15:0]-8'b0000000000000001));
	                lx[1:0]=x_complement[1:0];
		            x4=((~(x_complement>>2))+8'b0000000000000001);
		            x3=((~(x_complement>>2))+8'b0000000000000001);
	            	x2=((~(x_complement>>2))+8'b0000000000000001);
		            x1=((~((x_complement>>2)+lx))+8'b0000000000000001);

這裏就是根據最高位是1和0來決定進行什麼樣的數據處理。總而言之,我們要明白,數據在硬件中的儲存是默認爲正整數的二進制儲存方式,但是如何取理解這個數,卻是使用這個數據的人自己規定的。這種思想不僅在處理負數時是這樣的,在碰到小數、字符等都是這麼一個思想。0和1的世界是死的,但是解讀他們的規則卻是活的。

2.1.3 X和Z

我們都知道0和1代表的含義,但是對於verilog語言來說,還有X和Z兩種獨立於0和1之外的狀態。這兩種狀態分別是未知態和高阻態。
未知態X是指,無法確定此時信號的狀態是1還是0,但是能確定信號是有狀態的,不是1就是0,且這個狀態是能夠影響到與其相連的後續電路的,當我們用電錶測量時,其值可能是1,可能是0,取決於被測當時硬件電路的當前狀態。而高阻態Z是指,當前的信號狀態既不是1,也不是0,而是沒有狀態,或者可以認爲是斷開,即此時信號的狀態已經無法再影響到後續的電路。
而在實際電路中,某一時刻時只有1、0、高阻態三種狀態。
儘管X和Z表示的狀態不是傳統的1和0,但是X和Z也能參與到二進制的邏輯運算中來。在邏輯運算中,X和Z滿足如下的規律:

0 && X = 0 ;
1 && X = X ;
0 || X = X ;
1 || X = 1 ;

0 && Z = 0 ;
1 && Z = X ;
0 || Z = X ;
1 || Z = 1 ;

2.2 實數型和字符型

實數型和字符型放在一起,因爲這兩種數據的表達有相似的地方。那就是實數型或者字符型的數據可以在verilog語言中出現,但是卻不能通過硬件表達出來。而是常常出現在命令、顯示等非硬件參與的操作中。這與上面的整數的表達方式和意義是不同的。

2.2.1 實數

實數可以用十進制來表示,也可以用科學計數法來表示,例如:

12          //表示12
23e-3       //表示0.0023
23E4        //表示230000

雖然我們可以在verilog中寫出這些數據,但是並不是所有我們寫出來的數據都能在硬件中表達。在實際的仿真過程中,所有的科學計數的表達例如23e-3或者23E4,在硬件中都是0,而硬件中也不能直接表達小數。也就是直接在verilog中寫出來的小數,會被硬件識別爲0。
但是小數或者指數的表達,可以通過使用者自己規定來獲得。例如我們常聽說的浮點數等,就是通過一些算法轉換,來將二進制的代碼表達成帶有小數或者指數的數字。

3. verilog 的變量類型

4. verilog 的運算符

5. verilog 的語句塊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章