01-Verilog基本語法元素

八衛門狸發表於2021-04-28

不知道能不能更新完,畢竟我們學校計院對硬體向來不太重視,現在對競賽也不咋地重視了,也不加分,也沒啥用。嘛,就隨便寫寫玩玩吧。

一隻狸無聊的時候對Verilog的業餘描述筆記:以《Verilog數字系統設計教程》第三版·夏宇聞為基礎。

剛初學幾周,很多地方理解不透。不過學Verilog前學C確實會很有幫助,再理解一點點編譯原理,有種自頂向下的快感。有些地方渲染有點奇怪,改了一些,不知道有沒有漏的。

Verilog模組

Verilog HDL行為描述語言作為一種結構化和過程性的語言,其語法結構非常適合於演算法級和RTL級的模型設計。

在C語言中我們有函式,在Verilog中我們有模組。“模組”(block)是Verilog的基本設計單元,每個模組由 moduleendmodule 宣告,描述了模組的介面和功能。每個Verilog程式都包括4個主要部分:埠定義、I/O說明、內部訊號宣告、功能定義。

埠定義

我們可以通過下面這個簡單的3位加法器簡單理解Verilog的模組:

module adder(
	input [2:0] a,
	input [2:0] b,
	input cin,
	output cout,
	output [2:0] sum
);	//埠宣告語句

	assign {cout, sum} = a + b + cin;
endmodule

模組可以被引用,在引用模組時其埠可以用以下兩種方法連線:

  1. 在引用時,嚴格按照模組定義的埠順序來連線,而不用標明原模組定義時規定的埠名;
  2. 在引用時用“.”符號,標明原模組定義時的埠名。

兩種引用的示例如下:

adder myAdder0(myA, myB, myCin, myCout, mySum);
adder myAdder1(.a(myA), .b(myB), .cin(myCin), .cout(myCout), .sum(mySum));

模組內容

模組內容包括I/O說明、內部訊號宣告和功能定義。

I/O說明

I/O包括輸入口、輸出口和輸入/輸出口,均示例如下:

//輸入口
input 埠名;
input [訊號位寬-1:0] 埠名;
//輸出口
output 埠名;
output [訊號位寬-1:0] 埠名;
//輸入輸出口
inout 埠名;
inout [訊號位寬-1:0] 埠名;

I/O說明也可以寫在埠宣告語句裡,值得注意的是前面的adder模組就是如此。

內部訊號說明

在模組內用到的和埠有關的 wirereg 型別變數的宣告。示例如下:

wire 變數名;
wire [訊號位寬-1:0] 變數名;
reg 變數名;
reg [訊號位寬-1:0] 變數名;

功能定義

功能定義決定了這個模組的邏輯功能。一共有三種方法可在模組中產生邏輯:

  1. 用“assign”宣告語句,如 assign a = b & c; ,用於描述組合邏輯;
  2. 用例項元件,如 add #2 u1(q, a, b); ,這建立了一個雙輸入與門例項,延時為兩個單位時間,注意例項名必須唯一;
  3. 用“always”塊,它既可描述組合邏輯,又可描述時序邏輯。

Verilog資料型別及其常量和變數

Verilog HDL中總共有19種資料型別,資料型別是用來表示數位電路硬體中的資料儲存和傳送元素的。4種基本資料型別是: reg 型、 wire 型、 interger 型和 parameter 型。其他資料型別有 largemediumscalaredtimesmalltritriotriltriandtriortriregvectoredwandwor 。這14種資料型別除了 time 以外都與基本邏輯單元建庫有關,與系統設計沒有很大關係。

常量

數字

整型常量具有下面4種進製表示形式:二進位制(b或B)、十進位制(d或D)、十六進位制(h或H)、八進位制(o或O)。

整數表達方式有以下3種:

  1. <位寬><進位制><數字>,這是一種全面的描述方式;
  2. <進位制><數字>,數字的位寬將採用預設位寬(由具體機器系統決定,但至少32位);
  3. <數字>,在這種描述形式中,進位制將採用預設十進位制。

示例如下:

8'hff	//位寬為8十六進位制整數

x和z分別代表不定值和高阻值,注意 wirereg 均為四態(0,1,x,z)的資料型別。使用和整數一樣,示例如下:

8'h4x	//其低4位值為不定值

負數的表示只需要在表示式前加一個負號,注意這個負號必須寫在最前面。示例如下:

-8'hff	//位寬為8十六進位制補碼
8'h-ff	//這是錯誤的

我們可以使用下劃線來提高數字的可讀性,但是它只能被使用在數字之間,不能被使用在位寬和進位制處。示例如下:

8'b1111_1111	//位寬為8十六進位制補碼
8'b_1111_1111	//這是錯誤的

當常量不說明位數時,預設是32位;字串則每個字母用8位的ASCII值表示。下面列出幾個示例:

10=32'd10=32'b1010
-1=-32'd1=32'hFFFFFFFF
`BX=32'bX=32'bXXXX···X
"AB"=16'b01000001_01000010=16'h4142

引數(parameter)型

在Verilog HDL中用 parameter 來定義一個符號常量,即定義一個識別符號代表一個常量。其格式如下:

parameter 引數名1=表示式, 引數名2=表示式, ... , 引數名n=表示式;

注意表示式必須是一個常數表示式,且只能包含數字和先前定義過的引數。示例如下:

parameter msb = 7;
parameter e = 25, f = 29;
parameter r = 3.1;
parameter delay = (r + f) / 2;

引數型常數經常用於定義延遲時間和變數寬度。在模組或例項引用時,可通過引數傳遞改變在被引用模組或例項中已定義的引數。

變數

變數是在程式執行過程中可以改變的量。網路資料型別表示結構實體(例如門)之間的物理連線。網路型別的變數不能儲值,且必須受到驅動器(例如門或連續複製語句,assign)的驅動,否則該變數就是高阻的,即值為z。常用的網路資料型別包括 wiretri

wire型

wire型資料常用於表示以 assign 關鍵字指定的組合邏輯訊號。Verilog程式塊中輸出、輸出訊號型別預設時自動定義為wire型。wire型訊號可以用做任何方程式的輸入,也可以用作“assign”語句或例項元件的輸出。其定義示例如下:

wire a;
wire [7:0] b;
wire [4:0] c, d;

reg型

暫存器時資料儲存單元的抽象,暫存器(register)資料型別的關鍵字是reg。通過賦值語句可以改變暫存器儲存的值,其作用與改變觸發器儲存的值相當。

reg型別的資料預設初始值為不定值x,它可以賦正值,也可以賦負值,但當一個reg型資料是一個表示式的運算元時,它的值被當作無符號的值,即正值(對於4位的暫存器,-1會被認為是+15)。

注意reg型只表示被定義的訊號將用在“always”語句塊內,,儘管其常常是暫存器或觸發器的輸出,但並不一定總是這樣。

memory型

Verilog HDL通過對reg型變數建立陣列來對儲存器建模,可以描述RAM型儲存器、ROM儲存器和reg檔案。陣列中的每一個單元通過一個陣列索引進行定址。在Verilog中沒有多維陣列,memory型資料是通過擴充套件reg型資料的地址範圍來生成的。其格式如下:

reg[n-1:0] 儲存器名[m-1:0];
reg[n-1:0] 儲存器名[m:1];

注意:對儲存器進行地址索引的表示式必須是常數表示式。

在這裡,reg[n-1:0]定義了儲存器中每一個儲存單元的大小,即該儲存單元是一個n位的暫存器;儲存器名後的[m-1:0]或[m:1]則定義了該儲存器中有多少個這樣的暫存器。另外,在同一個資料型別宣告語句中,可以同時定義儲存器型資料和reg型資料,示例如下:

parameter wordsize = 16, memsize = 256;
reg[wordsize-1:0] mem[memsize-1:0], writereg, readreg;

儘管memory型資料和reg型資料的定義格式很相似,但要注意其不同之處。一個由n個一位暫存器構成的儲存器組是不同於一個n位的暫存器的,一個完整的儲存器組不能在一條賦值語句中賦值,見下例:

reg [n-1:0] rega;	//一個n位的暫存器
reg mema [n-1:0];	//一個由n個1位暫存器構成的儲存器組

rega = 0;	//賦值
mema = 0;	//這是非法的
mema[3] = 0;	//合法賦值

如果想對memory中的儲存單元進行讀寫操作,必須指定該單元在儲存器中的地址。進行定址的地址索引可以是表示式,而表示式的值可以取決於電路中其他的暫存器的值。

運算子

Verilog HDL中運算子所帶的運算元是不同的,按其所帶的運算元個數可分為三種:

  1. 單目運算子(unary operator):可以帶一個運算元,運算元放在運算子的右邊;
  2. 雙目運算子(binary operator):可以帶兩個運算元,運算元放在運算子的兩邊;
  3. 三目運算子(ternary operator):可以帶三個運算元,這三個運算元用三目運算子分隔開。

其所用運算子和C語言非常相像。

算數運算子

在Verilog HDL中,算數運算子又稱二進位制運算子,列如下:

  1. +(雙目:加法運算子,單目:正值運算子)
  2. -(雙目:減法運算子,單目:負值運算子)
  3. *(雙目:乘法運算子)
  4. /(雙目:除法運算子,整數除法結果值要略去小數部分)
  5. %(雙目:模運算子,要求兩側均為整型資料,符號位採用第一個運算元的符號位)

關係運算子

  1. < (雙目:小於)
  2. > (雙目:大於)
  3. <= (雙目:小於等於)
  4. >= (雙目:大於等於)

在進行關係運算時,如果宣告的關係為真(true),則返回值是0,反之(false)為1;如果某個運算元的值不定,則返回值為不定值。所有關係運算子的優先順序相同,且均低於算數運算子。

邏輯運算子

Verilog HDL中具有3種邏輯運算子:

  1. && (雙目:邏輯與)
  2. || (雙目:邏輯或)
  3. ! (單目:邏輯非)

其中“&&”和“||”的優先順序低於關係運算子,“!”的優先順序高於關係運算子。

條件運算子

和C語言一樣,為三目運算子“?:”,用法參考C語言即可。

等式運算子

在Verilog HDL中具有4種等式運算子:

  1. == (雙目:等於)
  2. != (雙目:不等於)
  3. === (雙目:等於)
  4. !== (雙目:不等於)

這四個等式運算子的優先順序是相同的。

其中“==”和“!==”為邏輯等式運算子,當運算元的某些位可能是不定值x和高阻值z時,其結果可能為不定值x。而“===”和“!==”對不定值x和高阻值z也進行比較,“===”只有在兩個運算元完全一致時結果才為1,它們常常用於case表示式的判別,所以又被稱為“case等式運算子”。下面列出了“==”與“===”的真值表:

=== 0 1 x z
0 1 0 0 0
1 0 1 0 0
x 0 0 1 0
z 0 0 0 1
== 0 1 x z
0 1 0 x x
1 0 1 x x
x x x x x
z x x x x

位運算子

注意硬體電路中有4種狀態值,即0、1、x、z。對於不同長度,系統會自動將兩者按右端對齊,位數少的運算元會在相應的高位用0填滿。

  1. ~(單目:取反)
  2. &(雙目:按位與)
  3. |(雙目:按位或)
  4. ^(雙目:按位異或)
  5. ^~(雙目:按位同或/異或非)

運算規則如下:

~ 結果
1 0
0 1
x x
& 0 1 x
0 0 0 0
1 0 1 x
x 0 x x
| 0 1 x
0 0 1 x
1 1 1 1
x x 1 x
^ 0 1 x
0 0 1 x
1 1 0 x
x x x x
^~ 0 1 x
0 1 0 x
1 0 1 x
x x x x

移位運算子

Verilog HDL中有兩種移位運算子:

  1. << (雙目:左移位運算子)
  2. >> (雙目:右移位運算子)

這兩種移位運算子都用0來填補移出的空位。

位拼接運算子

Verilog中有一個特殊的運算子:位拼接運算子(Concatation)“{}”。用這個運算子可以將兩個或多個訊號的某幾位拼接起來進行運算操作。使用方法如下:

{訊號1的某幾位, 訊號2的某幾位, 訊號3的某幾位, ... , 訊號n的某幾位}

注意每個訊號必須指明位寬。

位拼接運算子也可以用重複法簡化,示例如下:

{4{w}}	//等同於{w, w, w, w}

也可以巢狀:

{b, {4{a, b}}}	//等同於{b, a, b, a, b, a, b, a, b}

縮減運算子

縮減運算子(reduction operator)一共有三種:

  1. & (單目:與)
  2. | (單目:或)
  3. ^ (單目:非)

書上寫的是“縮減運算子……也有與、或、非運算”,但是我瞅著這個非不是本來就是單目的嘛,最後查到應該是“^”,那不就是異或來著,具體是啥待考證……

縮減運算子是在訊號的每個位之間進行運算:第一步先將運算元的第1位和第2位進行運算,然後將結果和第3位進行運算,一直運算到最後一位得出答案。下面給出幾個示例:

a = 4’b1010;
&a //為1’b0
|a //為1’b1
^a //為1’b0

優先順序

優先順序由高到低排列如下:

  1. ! ~
  2. * / %
  3. + -
  4. << >>
  5. < <= > >=
  6. == != === !==
  7. &
  8. ^ ~^
  9. |
  10. &&
  11. ||
  12. ?:

語句和塊

賦值語句

Verilog HDL中,訊號具有兩種賦值方式:

  1. 非阻塞(Non_Blocking)賦值方式(如b<=a;)
    ⑴ 在語句塊中,上面語句所賦的值不能立即就為下面的語句所用;
    ⑵ 塊結束後才能完成這次賦值操作,而所賦的變數值是上一次賦值得到的;
    ⑶ 在編寫可綜合的時序邏輯模組時,這是最常用的賦值方法。
  2. 阻塞(blocking)賦值方式(如b=a;)
    ⑴ 在賦值語句執行完後,塊才結束;
    ⑵ b的值在賦值語句執行完後立即就改變的;
    ⑶ 在時序邏輯中使用時,可能會產生意想不到的結果。

上面是書上的描述,在時序邏輯電路中,或者在always塊中,我們通常都是使用非阻塞的。阻塞的賦值方式更像是用導線之間連線起來的形式,它們是同時發生的;非阻塞的賦值方式似乎更像是接續進行的。

塊語句

塊語句通常用來將兩條或多條語句組合在一起,使其在結構上看更像一條語句,某種程度上很像C語言的大括號(大括號事實上已經被用作位拼接了對吧)。塊語句有兩種:一種是begin_end語句,通常用來表示順序執行的語句,用它來標識的塊稱為順序塊;另一種是fork_join語句,通常用來標識並並行執行的語句,用它來表示的塊稱為並行塊。

順序塊

順序塊有以下特點:

  1. 塊內的語句是按順序執行的,即只有上面一條語句執行完成後下面的語句才能執行;
  2. 每條語句的延遲時間是相對於前一條語句的模擬時間而言的;
  3. 直到最後一條語句執行完,程式流程控制才跳出該語句塊。

順序塊的格式如下:

begin
	語句1;
	語句2;
	...
	語句n;
end

begin: 塊名
	塊內宣告語句
	語句1;
	語句2;
	...
	語句n;
end

語句中,塊名即該塊的名字,一個標識名;塊內宣告語句可以是引數宣告語句、reg型變數宣告語句、integer型變數宣告語句和real型變數宣告語句。

並行塊

並行塊有以下特點:

  1. 塊內的語句是同時執行的,即程式流程控制一進入到該並行塊,塊內則開始並行地執行;
  2. 塊內每條語句的延遲時間是相對於程式流程控制進入到塊內的模擬時間的;
  3. 延遲時間是用來給賦值語句提供執行時序的;
  4. 當按時間時序排序在最後的語句執行完後或一個disable語句執行時,程式流程控制跳出該程式塊。

並行塊的格式如下:

fork
	語句1;
	語句2;
	...
	語句n;
join

fork: 塊名
	塊內宣告語句
	語句1;
	語句2;
	...
	語句n;
join

語句中,塊名即該塊的名字,一個標識名;塊內宣告語句可以是引數宣告語句、reg型變數宣告語句、integer型變數宣告語句、real型變數宣告語句、time型變數宣告語句和時間(event)說明語句。

塊名

在Verilog HDL語言中,可以給每個塊取一個名字,只需將名字加在關鍵詞begin或fork後面即可。這樣做的原因有以下幾點:

  1. 可以在塊內定義區域性變數,即只在塊內使用的變數;
  2. 可以允許塊被其他語句呼叫,如disable語句;
  3. 在Verilog HDL語言中,所有的變數都是靜態的,即所有的變數都只有一個唯一的儲存地址,因此進入或跳出塊並不影響儲存在變數內的值。

基於以上原因,塊名提供了一個在任何模擬時刻確認變數值的方法。

延遲控制

這裡先引入Verilog HDL中的延遲控制語句。“#”是延遲控制的關鍵字元,延遲語句用於對各條語句的執行時間進行控制,從而快速滿足使用者對時序的需求。的延遲控制語句格式共有兩種:

  1. #延遲時間 行為語句;
  2. #延遲時間;

其中延遲時間可以是直接指定的延遲時間量,並以多少個模擬時間單位的形式給出,可以是常量數字,也可以是變數或表示式。例子在後面就會看到。

起始時間和結束時間

在並行塊和順序塊中都有一個起始時間和結束時間的概念。

對於順序塊,起始時間就是第一條語句開始執行的時間,結束時間就是最後一條語句執行完的時間。

而對於並行塊來說,起始時間對於塊內所有的語句是相同的,即程式流程控制進入塊的時間,其結束時間是按時間排序在最後的語句執行結束的時間(當然這說的是指定了語句開始執行的延遲時間的情況下,如果並沒有,那應該就是語句執行時間最長的那句執行結束的時間了吧[待考證]。有趣的是在這之前並沒有講關於語句延遲時間的問題,但是所給的示例程式中卻已經出現了)。

例子

這裡引用了書上的兩個例子如下:

parameter d = 50;	//宣告d是一個引數
reg [7:0] r;		//宣告r是一個8位的暫存器變數
begin				//由一系列延遲產生的波形
	#d r = 'h35;
	#d r = 'hE2;
	#d r = 'h00;
	#d r = 'hF7;
	#d -> end_wave;	//表示觸發時間end_wave使其翻轉
end

fork
	#50 r = 'h35;
	#100 r = 'hE2;
	#150 r = 'h00;
	#200 r = 'hF7;
	#250 -> end_wave;	//表示觸發時間end_wave使其翻轉
end

其中的begin_end塊和fork_end塊的效果是一樣的,雖然我們暫時還不知道這個end_wave在幹啥,但是這並不影響我們對並行塊和順序塊的理解。

by SDUST weilinfox