不知道能不能更新完。嘛,就隨便寫寫玩玩吧。
一只狸無聊的時候對Verilog的業余描述筆記:以《Verilog數字系統設計教程》第三版·夏宇聞為基礎。
剛初學幾周,很多地方理解不透。不過學Verilog前學C確實會很有幫助,再理解一點點編譯原理,有種自頂向下的快感。有些地方渲染有點奇怪,改了一些,不知道有沒有漏的。
Verilog模塊
Verilog HDL行為描述語言作為一種結構化和過程性的語言,其語法結構非常適合於算法級和RTL級的模型設計。
在C語言中我們有函數,在Verilog中我們有模塊。“模塊”(block)是Verilog的基本設計單元,每個模塊由 module
和 endmodule
聲明,描述了模塊的接口和功能。每個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
模塊可以被引用,在引用模塊時其端口可以用以下兩種方法連接:
- 在引用時,嚴格按照模塊定義的端口順序來連接,而不用標明原模塊定義時規定的端口名;
- 在引用時用“.”符號,標明原模塊定義時的端口名。
兩種引用的示例如下:
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模塊就是如此。
內部信號說明
在模塊內用到的和端口有關的 wire
和 reg
類型變量的聲明。示例如下:
wire 變量名;
wire [信號位寬-1:0] 變量名;
reg 變量名;
reg [信號位寬-1:0] 變量名;
功能定義
功能定義決定了這個模塊的邏輯功能。一共有三種方法可在模塊中產生邏輯:
- 用“assign”聲明語句,如
assign a = b & c;
,用於描述組合邏輯; - 用實例元件,如
add #2 u1(q, a, b);
,這創建了一個雙輸入與門實例,延時為兩個單位時間,注意實例名必須唯一; - 用“always”塊,它既可描述組合邏輯,又可描述時序邏輯。
Verilog數據類型及其常量和變量
Verilog HDL中總共有19種數據類型,數據類型是用來表示數字電路硬件中的數據儲存和傳送元素的。4種基本數據類型是: reg
型、 wire
型、 interger
型和 parameter
型。其他數據類型有 large
、 medium
、 scalared
、 time
、 small
、 tri
、 trio
、 tril
、 triand
、 trior
、 trireg
、 vectored
、 wand
、 wor
。這14種數據類型除了 time
以外都與基本邏輯單元建庫有關,與系統設計沒有很大關系。
常量
數字
整型常量具有下面4種進制表示形式:二進制(b或B)、十進制(d或D)、十六進制(h或H)、八進制(o或O)。
整數表達方式有以下3種:
- <位寬><進制><數字>,這是一種全面的描述方式;
- <進制><數字>,數字的位寬將采用默認位寬(由具體機器系統決定,但至少32位);
- <數字>,在這種描述形式中,進制將采用默認十進制。
示例如下:
8'hff //位寬為8十六進制整數
x和z分別代表不定值和高阻值,注意 wire
和 reg
均為四態(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。常用的網絡數據類型包括 wire
和 tri
。
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中運算符所帶的操作數是不同的,按其所帶的操作數個數可分為三種:
- 單目運算符(unary operator):可以帶一個操作數,操作數放在運算符的右邊;
- 雙目運算符(binary operator):可以帶兩個操作數,操作數放在運算符的兩邊;
- 三目運算符(ternary operator):可以帶三個操作數,這三個操作數用三目運算符分隔開。
其所用運算符和C語言非常相像。
算數運算符
在Verilog HDL中,算數運算符又稱二進制運算符,列如下:
- +(雙目:加法運算符,單目:正值運算符)
- -(雙目:減法運算符,單目:負值運算符)
- *(雙目:乘法運算符)
- /(雙目:除法運算符,整數除法結果值要略去小數部分)
- %(雙目:模運算符,要求兩側均為整型數據,符號位采用第一個操作數的符號位)
關系運算符
- < (雙目:小於)
- > (雙目:大於)
- <= (雙目:小於等於)
- >= (雙目:大於等於)
在進行關系運算時,如果聲明的關系為真(true),則返回值是0,反之(false)為1;如果某個操作數的值不定,則返回值為不定值。所有關系運算符的優先級相同,且均低於算數運算符。
邏輯運算符
Verilog HDL中具有3種邏輯運算符:
- && (雙目:邏輯與)
- || (雙目:邏輯或)
- ! (單目:邏輯非)
其中“&&”和“||”的優先級低於關系運算符,“!”的優先級高於關系運算符。
條件運算符
和C語言一樣,為三目運算符“?:”,用法參考C語言即可。
等式運算符
在Verilog HDL中具有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 | 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中有兩種移位運算符:
- << (雙目:左移位運算符)
- >> (雙目:右移位運算符)
這兩種移位運算符都用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位進行運算,一直運算到最后一位得出答案。下面給出幾個示例:
a = 4’b1010;
&a //為1’b0
|a //為1’b1
^a //為1’b0
優先級
優先級由高到低排列如下:
!
~
*
/
%
+
-
<<
>>
<
<=
>
>=
==
!=
===
!==
&
^
~^
|
&&
||
?:
語句和塊
賦值語句
Verilog HDL中,信號具有兩種賦值方式:
- 非阻塞(Non_Blocking)賦值方式(如b<=a;)
⑴ 在語句塊中,上面語句所賦的值不能立即就為下面的語句所用;
⑵ 塊結束后才能完成這次賦值操作,而所賦的變量值是上一次賦值得到的;
⑶ 在編寫可綜合的時序邏輯模塊時,這是最常用的賦值方法。 - 阻塞(blocking)賦值方式(如b=a;)
⑴ 在賦值語句執行完后,塊才結束;
⑵ b的值在賦值語句執行完后立即就改變的;
⑶ 在時序邏輯中使用時,可能會產生意想不到的結果。
上面是書上的描述,在時序邏輯電路中,或者在always塊中,我們通常都是使用非阻塞的。阻塞的賦值方式更像是用導線之間連接起來的形式,它們是同時發生的;非阻塞的賦值方式似乎更像是接續進行的。
塊語句
塊語句通常用來將兩條或多條語句組合在一起,使其在結構上看更像一條語句,某種程度上很像C語言的大括號(大括號事實上已經被用作位拼接了對吧)。塊語句有兩種:一種是begin_end語句,通常用來表示順序執行的語句,用它來標識的塊稱為順序塊;另一種是fork_join語句,通常用來標識並並行執行的語句,用它來表示的塊稱為並行塊。
順序塊
順序塊有以下特點:
- 塊內的語句是按順序執行的,即只有上面一條語句執行完成后下面的語句才能執行;
- 每條語句的延遲時間是相對於前一條語句的仿真時間而言的;
- 直到最后一條語句執行完,程序流程控制才跳出該語句塊。
順序塊的格式如下:
begin
語句1;
語句2;
...
語句n;
end
或
begin: 塊名
塊內聲明語句
語句1;
語句2;
...
語句n;
end
語句中,塊名即該塊的名字,一個標識名;塊內聲明語句可以是參數聲明語句、reg型變量聲明語句、integer型變量聲明語句和real型變量聲明語句。
並行塊
並行塊有以下特點:
- 塊內的語句是同時執行的,即程序流程控制一進入到該並行塊,塊內則開始並行地執行;
- 塊內每條語句的延遲時間是相對於程序流程控制進入到塊內的仿真時間的;
- 延遲時間是用來給賦值語句提供執行時序的;
- 當按時間時序排序在最后的語句執行完后或一個disable語句執行時,程序流程控制跳出該程序塊。
並行塊的格式如下:
fork
語句1;
語句2;
...
語句n;
join
或
fork: 塊名
塊內聲明語句
語句1;
語句2;
...
語句n;
join
語句中,塊名即該塊的名字,一個標識名;塊內聲明語句可以是參數聲明語句、reg型變量聲明語句、integer型變量聲明語句、real型變量聲明語句、time型變量聲明語句和時間(event)說明語句。
塊名
在Verilog HDL語言中,可以給每個塊取一個名字,只需將名字加在關鍵詞begin或fork后面即可。這樣做的原因有以下幾點:
- 可以在塊內定義局部變量,即只在塊內使用的變量;
- 可以允許塊被其他語句調用,如disable語句;
- 在Verilog HDL語言中,所有的變量都是靜態的,即所有的變量都只有一個唯一的存儲地址,因此進入或跳出塊並不影響存儲在變量內的值。
基於以上原因,塊名提供了一個在任何仿真時刻確認變量值的方法。
延遲控制
這里先引入Verilog HDL中的延遲控制語句。“#”是延遲控制的關鍵字符,延遲語句用於對各條語句的執行時間進行控制,從而快速滿足用戶對時序的需求。的延遲控制語句格式共有兩種:
- #延遲時間 行為語句;
- #延遲時間;
其中延遲時間可以是直接指定的延遲時間量,並以多少個仿真時間單位的形式給出,可以是常量數字,也可以是變量或表達式。例子在后面就會看到。
起始時間和結束時間
在並行塊和順序塊中都有一個起始時間和結束時間的概念。
對於順序塊,起始時間就是第一條語句開始執行的時間,結束時間就是最后一條語句執行完的時間。
而對於並行塊來說,起始時間對於塊內所有的語句是相同的,即程序流程控制進入塊的時間,其結束時間是按時間排序在最后的語句執行結束的時間(當然這說的是指定了語句開始執行的延遲時間的情況下,如果並沒有,那應該就是語句執行時間最長的那句執行結束的時間了吧[待考證]。有趣的是在這之前並沒有講關於語句延遲時間的問題,但是所給的示例程序中卻已經出現了)。
例子
這里引用了書上的兩個例子如下:
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