SystemVerilog基本語法總結(上)
在總結SV的語法之前,先分享一些關於SV的筆試題目,這樣更顯得具有針對性的總結。
數據類型
l 合並數組和非合並數組
1)合並數組:
存儲方式是連續的,中間沒有閑置空間。例如,32bit的寄存器,可以看成是4個8bit的數據,或者也可以看成是1個32bit的數據。
表示方法:數組大小和位,必須在變量名前指定,數組大小必須是[msb:lsb] ;如 Bit [3:0] [7:0] bytes ;
2)二維數組和合並數組識別:
合並數組: bit [3:0] [7:0] arrys; 大小在變量名前面放得,且降序
二維數組: int arrays[0:7] [0:3] ; 大小在變量名后面放得,可降序可升序
位寬在變量名前面,用於識別合並和非合並數組,位寬在后面,用於識別數組中元素個數。
3)非合並數組
一般仿真器存放數組元素時使用32bit的字邊界,byte、shortint、int都放在一個字中。非合並數組:字的地位存放變量,高位不用。
表示方法:Bit [7:0] bytes;
4)合並數組和非合並數組的選擇
(1)當需要以字節或字為單位對存儲單元操作。
(2)當需要等待數組中變化的,則必須使用合並數組。
例如測試平台需要通過存儲器數據的變化來喚醒,需要用到@,@只能用於標量或者合並數組。
Bit[3:0] [7:0] barray[3] ; 表示合並數組,合並數組中有3個元素,每個元素時8bit,4個元素可以組成合並數組
可以使用barry[0]作敏感信號。
l 動態數組
隨機事物不確定大小。
使用方法:數組在開始是空的,同時使用new[]來分配空間,在new[n]指定元素的個數。
int dyn[];
dyn = new[5]; //分配5個元素空間
dyn.delete() ; //釋放空間
l 隊列
在隊列中增加或刪除元素比較方便。
l 關聯數組
當你需要建立一個超大容量的數組。關聯數組,存放稀疏矩陣中的值。
表示方法:采用在方括號中放置數據類型的形式聲明:
Bit[63:0] assoc[bit[63:0]];
l 常量:
1 ) Verilog 推薦使用文本宏。
好處:全局作用范圍,且可以用於位段或類型定義
缺點:當需要局部常量時,可能引起沖突。
2 ) Parameter
作用范圍僅限於單個module
3 ) Systemverilog:
參數可以在多個模塊里共同使用,可以用typedef 代替單調乏味的宏。
過程語句
l 可以在for循環中定義變量,作用范圍僅在循環內部
for(int i=0;i<10;i++)
array[i] =i;
l 任務、函數及void函數
1 ) 區別:(這個任務和函數的區別也是面試經常會被問到的基礎,所以請參考本網站Verilog HDL 任務和函數的區別與聯系)
Verilog中task 和function最重要的區別是:task可以消耗時間而function不能。函數中不能使用#100的延時或@的阻塞語句,也不能調用任務;
Systemverilog中函數可以調用任務,但只能在fork join_none生成的線程中。
2 ) 使用:
如果有一個不消耗時間的systemverilog任務,應該把它定義成void函數;這樣它可以被任何函數或任務調用。
從最大靈活性角度考慮,所有用於調用的子程序都應該被定義成函數而非任務,以便被任何其它任務或函數調用。(因為定義成任務,函數調用任務很有限制)
l 類靜態變量
作用:
1 ) 類的靜態變量,可以被這個類的對象實例所共享。
當你想使用全局變量的時候,應該先想到創建一個類的靜態變量
靜態變量在聲明的時候初始化。
2 ) 類的每一個實例都需要從同一個對象獲取信息。
l 靜態方法
作用:當靜態變量很多的時候,操作它們的代碼是一個很大的程序,可以用在類中創建一個靜態方法讀寫靜態變量,但是靜態方法不能讀寫非靜態變量。
l ref高級的參數類型
ref 參數傳遞為引用而不是復制。ref比 input 、output、inout更好用。
Function void print_checksum(const ref bit [31:0] a[ ]);
1 )也可以不用ref進行數組參數傳遞,這時數組會被復制到堆棧區,代價很高。
2 )用帶ref 進行數組參數傳遞,僅僅是引用,不需要復制;向子程序傳遞數組時,應盡量使用ref以獲得最佳性能,如果不希望子程序改變數組的值,可以使用const ref。
3 )ref參數,用ref 傳遞變量;可以在任務里修改變量而且,修改結果對調用它的函數可見,相對於指針的功能。
l return語句
增加了return語句。task任務由於發現了錯誤而需要提前返回,如果不這樣,那么任務中剩下的語句就必須被放到一個else條件語句中。體會下
task load_array(int len. Ref int array[ ]);
If(len<0) begin
$display(“Bad len”);
return;
//任務中其它代碼
endtask
l 局部數據存儲 automatic作用
Verilog中由於任務中局部變量會使靜態存儲區,當在多個地方調用同一個任務時,不同線程之間會竄用這些局部變量。
Systemverilog中,module和program塊中,缺省使用靜態存儲;如果想使用自動存儲,需加入automatic關鍵詞。
測試平台
l Interface
背景 :
一個信號可能連接幾個設計層次,如果增加一個信號,必須在多個文件中定義和連接。接口可以解決這些問題。
好處:
如果希望在接口中增加一個信號,不需要改變其他模塊,如TOP模塊。
使用方法:
(1)接口中去掉信號的方向類型;
(2)DUT 和測試平台中,信號列表中采用接口名,例化一個名字
注意:
因為去掉了方向類型,接口中不需要考慮方向信號,簡單的接口,可以看做是一組雙向信號的集合。這些信號使用logic類型[d1] 。
雙向信號為何可以使用logic呢?
這里的雙向,只是概念上的雙向,不想verilog中databus多驅動的雙向。
雙向信號如何做接口?
(1)仲裁器的簡單接口
Interface arb_if( input bit clk);
logic [1:0] grant,request;
logic rst;
endinterface
DUT 使用接口:
module arb(arb_if arbif);
always @(posedge arbif.clk or negedge arbif.rst)
…
endmodule
(2)DUT 不采用接口,測試平台中使用接口(推薦)
DUT 中源代碼不需要修改,只需要再top中,將接口連接到端口上。
module top;
bit clk;
always #2 clk =~clk;
arb_if arbif(clk);
arb_port al(.grant(arbif.grant),
.request(arbif.grant),
.rst(arbif.rst),
.clk(arbif.clk)
);
test t1(arbif);
endmodule
n. Modport
背景:
端口的連接方式包含了方向信息,編譯器依次來檢查連續錯誤;接口使用無信號的連接方式。Modport將接口中信號分組並指定方向。
例子:
l 在總線設計中使用modport
並非接口中每個信號都必須連接。Data總線接口中就解決不了,個人覺得?
因為data是一個雙驅動
l 時鍾塊
作用:
一旦定義了時鍾塊,測試平台就可以采用@arbif.cb等待時鍾,而不需要描述確切的時鍾信號和邊沿,即使改變了時鍾塊中的時鍾或邊沿,也不需要修改測試代碼
應用:
將測試平台中的信號,都放在clocking 中,並指定方向(以測試平台為參考的方向)。並且在modprot test(clocking cb,
最完整的接口:
Interface arb_if(input bit clk);
logic[1:0] grant,request;
logic rst;
clocking cb @(posedge clk);
output request;
input grant;
endclocking
modport test (clocking cb,
output rst);
modport dut (input clk, request,rst,
output grant);
endinterface
變化:將request 和grant移動到時鍾塊中去了,test中沒有使用了。
l 接口中的雙向信號
Interface master_if(input bit clk); //在類中為了,不使用有符號數,常用bit[]定義變量
wire [7:0] data;
clocking cb@(posedge clk);
inout data;
endclocking
modport TEST(clocking cb);
endinterface
program test(master_if mif);
initial begin
mif.cb.data <= ‘z;
@mif.cb;
$display(mif.cb.data); //總線中讀數據
@mif.cb;
mif.cb.data <= 8’h5a; //驅動總線
@mif.cb;
mif.cb.data <= ‘z; //釋放總線
注:
(1)interface 列表中clk 采用的是input bit clk;為什么要用bit?
(2)時鍾塊 clocking cb 中,一般將testbench中需要的信號,方向指定在這里;
而在modprot 指定test信號方向的時候,采用clocking cb。
(3)interface中信號,不一定都用logic,也可采用wire(雙驅動);systemverilog
中如果采用C代碼的風格(參數列表中方向和類型寫一起),必須采用logic類型
(4)現在的風格,DUT 沒才用clocking cb ,測試平台和DUT的時鍾如何統一?
l 激勵時序
DUT和測試平台之間時序必須密切配合。
l 測試平台和設計間的競爭狀態
好的風格:
使用非阻塞賦值可以減少競爭。
systemverilog驗證中initial 中都采用<= 賦值,而等待延遲采用@arbif.cb等待一個周期來實現。
而verilog中采用的風格時,initial 中采用 =阻塞賦值,沿時可以采用#2,等實現。
因此時鍾發生器,只能放在module 中,而不能放在program中
l program中不能使用always塊
測試平台可以使用initial 但不能使用always,使用always 模塊不能正常工作。
原因:測試平台的執行過程是進過初始化、驅動和響應等步驟后結束仿真。
如果確實需要一個always塊,可以使用initial forever 來完成。比如:在產生時鍾時。
類
l 類中static變量
背景:
如果一個變量需要被其他對象所共享,如果沒有OPP,就需要創建全局變量,這樣會污染全局名字空間,導致你想定義局部變量,但變量對每個人都是可見的。
1)作用:
類中static變量,將被這個類的所有實例(對象)所共享,使用范圍僅限於這個類。
例:class transaction;
static int count=0;
int id;
endclass
trasaction tr1,tr2;
Id不是靜態變量,所以每個trasaction對象都有自己的id;count 是靜態變量,所有對象只有一個count變量。
如何用?
當你打算創建一個全局變量的時候,首先考慮創建一個類的靜態變量。
2)static變量的引用
句柄或類名加::
static 變量的初始化
static變量通常在聲明時初始化。不能在構造函數中初始化,因為每一個新的對象都會調用構造函數。
l 靜態句柄:
背景:當類的每一個對象,都需要從同一個對象(另一個類)中獲取信息的時候。如果定義成非靜態句柄,則每個對象都會有一份copy,造成內存浪費。
l 靜態方法
背景:
當使用更多靜態變量的時候,操作他們的代碼會很長。
作用:
可以在類中創建一個靜態方法用於讀寫靜態變量。
注:systemverilog不允許,靜態方法讀寫非靜態變量。
l 類之外的方法
背景:解決類太長的問題。類最好控制在一頁內,如果方法很都很長。
l This
背景:如果在類很深的底層作用域,卻想引用類一級的對象。在構造函數中最常見。
作用:this指向類一級變量
l 如何做類,類做多大?
上限:類不能太大
當類中存在多處相同的代碼,你需要將這段代碼做成當前類的一個成員函數或父類的成員函數。
下限:類不能太小
類太小,增加了層次。
方法:如果一個小類只被例化了一次,可以將它合並到父類中去。
l 動態對象
概念區分:方法中修改對象 和修改句柄
修改對象——將對象的變量重新賦值。
修改句柄——在任務中new()對象。
1) 當你將對象傳遞給方法
背景:句柄,new()后變成對象,在將其作為參數傳遞給方法。
實質和作用:
傳遞的是句柄。這個方法可以讀取對象中的值;也以改變對象中的值
2) 修改標量變量的值
背景:在方法的參數中,前面加ref;(用ref傳遞,ref傳遞的是變量的地址)。
作用:
方法可以修改變量的值,並將修改的值,傳遞給主程序。
引申:
方法可以改變對象,即使沒有使用ref 修飾句柄。
因為傳遞的是句柄,句柄是地址。不要將句柄和對象混為一談,如果傳遞的是對象,對象是單向的,那方法以外也不能傳遞回來。可以這樣理解吧。
讀寫對象中的值:
例:
Task transmit(Transcation t);
cbbus.rx_data <= t.data;
t.stats.startT = $time; //在任務中,改變了對象
endtask
trancation t;
initilal beign
t = new();
t.addr = 42;
transmit(t);
end
既然傳遞的是句柄,那數據就沒傳過去,如何讀取值?
答:主程序中new()創建了一個對象,而句柄是指向對象的指針,傳遞的是句柄,transmit中也指向了對象,所以transmit中可以讀寫對象。
3) 在任務中修改句柄
背景:
在方法中,參數為句柄,前面加ref。
作用:
可以在方法中new()對象,並將初始化放在方法中;在主程序中僅僅調用。
注意:正確的事物發生器,參數是帶ref的句柄
function void create(ref transaction tr)
endfunction
方法的參數是句柄,句柄前有ref 和沒ref的差別:
沒ref,在方法中不能new()該句柄的對象,因為沒ref,句柄是不能傳遞到主程序的; 有ref,可以在方法中new()該句柄的對象。
原因:沒ref傳遞的是句柄,不能修改句柄,有ref,傳遞的是句柄的地址,可以修改句柄。
例子:
function void create( Transcation tr)
tr = new(); 不正確
tr.addr = 42;
endfunction
Transcation t;
initial begin
Create(t);
$diasplay (t.addr);
end
l 程序中修改對象
背景:
應該在循環中,new()多個對象,而不是先new()對象再循環發送事物。
作用:
創建多個對象
正確產生器,創建多個對象:
Task generator (int n);
Transaction t;
Repeat(n) begin
t=new();
t.addr =$random();
transmit(t);
endtask
將new()放在循環內,這樣創建了許多對象。
l 對象的復制
目的:防止對象的方法修改原始對象的值。或在一個發生器中保留約束。
分兩種情況,類中不包含其他類的句柄和包含
方法:
1) 使用new復制一個對象——簡易復制(shallow copy)
Transaction src,dst;
src = new() //
dst = new src //復制
局限:
如果類中包含一個指向另外一個類的句柄,那么只有最高一級的對象被new復制,下層的對象都不會被復制。
會出現意想不到的錯誤。
當前類中變量和句柄被復制,這樣兩個對象,都有指向另外一個類的對象statistic(會帶來意想不到的錯誤),但是statistic沒有被復制。如果其中一個transaction對象,修改了statistic對象值,會影響到另一個transaction看到static的值。
2) 簡單的復制函數
如何實現:
Copy函數一般放在類內部,函數名為該類的一個句柄,copy函數中new()對象。
局限:
類中不包含其他類。
3) 深層的復制函數 ——深層copy
目的:解決類中包含另外一個類,copy帶來的問題。
實現:
在copy函數中,將調用另一個類的copy函數,賦值給該句柄;同時需要為statistic類和層次結構中每一個類增加一個copy()方法;copy函數的ID域也要保持一致,copy函數,copy本類,所以ID也要++.
Copy.stats = stats.copy();
Id =count++;
約束
l 約束塊中,只能包含表達式,不能賦值。
1)dist權重分布
dist帶有一個值的列表及相應的權重,中間用:= 或 :/分開。值或權重可以是常量或變量。權重的和不必是100.
:= 表示范圍內,每一個值的權重是相同的;
表示范圍內,權重要均勻分布
2)Inside
產生一個值的集合,在值的集合中取隨機值時,機會相等。
3)在集合中使用數組
l 條件約束
Systemverilog支持兩種關系操作 –>和if—else
—>可產生和case效果類似的語句塊,可以用於枚舉類型的表達式。
l 雙向約束
l 控制多個約束塊
作用:可以打開或關閉某個約束
可以使用內建的Handle.constraint.constraint_mode()打開或關閉。
l 內嵌約束
背景:很多測試只會在代碼的一個地方隨機化對象,但是約束越來越復雜時,
Systemverilog可以使用randomized with 來增加額外的約束,這和在類里增加的約束是等效的。
l Pre_randomize 和post_randomize函數
有時候需要再調用randomize()之前或之后立即執行一些操作。
隨機化前:設置類里的一些非隨機變量(如上下限、權重),
隨機化后:計算數據的誤差矯正值。
l 約束的技巧
1) 約束中使用變量
2) 使用非隨機值
如果一套約束在已產生了幾乎所有想要的激勵向量,但還缺少幾種。
可以使用rand_mode把這些變量設置為非隨機變量。
l 數組約束
Systemverilog可以用foreach對數組中的每一個元素進行約束。
線程及線程間的通信
l 測試平台使用許多並發執行的線程。測試平台隸屬於程序塊。
Systemverilog引入兩種新的創建線程的方法—fork…join_none和fork…join_any
1) 使用fork…join_none來產生線程
在調度其內部語句時,父線程繼續執行。
2) 使用fork…join_any實現線程同步
在調度塊內語句,當第一個語句執行完,父線程才繼續執行。
l 動態線程
Systemverilog中可以動態創建線程。
用法:
fork…join_none放在了任務中,而不是包含兩個線程。
原因:
主程序中有連個線程:發送和檢測線程。但是不能同時啟動,發送事物后,才能檢測,否則還未產生數據,就開始檢測;但是檢測又不能阻塞下一次發送事物的線程。所以fork…join_none 放在了檢測task 任務(后作用的線程中)中,
例:測試平台產生隨機事物並發送到DUT中,DUT把事物返回到測試平台。測試平台必須等到事物完成,但同時不希望停止隨機事物的發送。
program automatic test(bus_ifc.Tbbus);
task check_trans(Transaction tr);
fork
begin
wait(bus.cb.addr == tr.addr);
end
join_none
endtask
initial begin
repreat(10) begin
tr= new();
assert.(tr.randomize());
//把事物發送到DUT中
Transmit(tr);
//等待DUT的回復
check_trans(tr);
end
#100;
end
endprogram
l 並發線程中務必使用自動變量來保持數值。
l #0 延遲,使得當前線程必須等到fork…join_none語句中產生的線程執行完后,才得以運行。
l 停止線程
停止單個線程
使用fork …join_any 后加disable。
3) 停止多個線程
Disable fork 能停止從當前線程中衍生出來得所有子線程。
應該使用fork …join 把目標代碼包含起來,以限制Disable fork的作用范圍