《SystemVerilog驗證-測試平臺編寫指南》學習 - 第3章 過程語句和子程序
做設計驗證的大部分代碼在任務和函數裏。SystemVerilog增加了許多改進使它更接近C語言,從而使代碼編寫更容易。
3.1 過程語句
SystemVerilog從C和C++引入了很多操作符和語句。你可以在 for 循環中定義循環變量,它的作用範圍僅限於循環內部,從而有助於避免一些代碼漏洞。自動遞增符”++“和自動遞減符”--“既可以作爲前綴也可以作爲後綴。如果在begin和fork語句中使用標識符,那麼在相對應的end和join語句中可以放置相同的符號,這使得程序塊的收尾匹配更加容易。你也可以把標識符放在SystemVerilog的其他結束語句裏,例如 endmodule、endtask、endfunction 以及其他語句。如下例所示:
// 例3.1 新的過程語句和操作符
initial
begin :example
integer array [10], sum, j ;
// 在 for 語句中聲明 i
for (int i = 0; i < 10; i++) // 定義i,i 遞增
array [i] = i ;
// 把數組裏的元素相加
sum = array [9] ;
j = 8 ;
do // do ... while 循環
sum += array[j] ; // 累加
while (j--) ; // 判斷 j=0 是否成立
$display ("Sum = %4d", sum) ; // %4d 指定寬度
end : example
SystemVerilog爲循環功能增加了兩個新語句。
循環功能新增語句 | 功能 |
---|---|
continue | 用於在循環中跳過本輪循環剩下的語句而直接進入下一輪循環 |
break | 用於終止並跳出循環 |
//例3.2 在讀取文件時使用break和continue
initial begin
bit [127:0] cmd ;
int file , c ;
file = $fopen ("commands.txt", "r") ;
while (!$feof(file)) begin // $feof() 當讀到文件末尾時(eof)爲非0,否則爲0
c = $fscanf (file, "%s", cmd) ; // $fscanf() 一行一行讀取,成功返回 1
case (cmd)
"" : continue ; // 空行 - 跳過本輪循環
"done" : break ; // Done - 終止並跳出循環
...
endcase // case (cmd)
end
$fclose (file) ;
end
3.2 任務、函數以及void函數
在Verilog中,任務task和函數function之間有很明顯的區別,其中最重要的是:
- 任務可以消耗時間而函數不能。函數裏面不能帶有諸如 #100 的時延語句或諸如 @(posedge clock) 、wait(ready) 的阻塞語句。
- 函數不能調用任務。
- 另外,Verilog中的函數必須有返回值,並且返回值必須被使用,例如用到賦值語句中。
SystemVerilog對這條限制稍有放寬,允許函數調用任務,但只能在由 fork...join_none 語句生成的線程中調用。
注:如果你有一個不消耗時間的SystemVerilog任務,你應該把它定義成void函數,這種函數沒有返回值。這樣它就能被任何任務或函數所調用了。從最大靈活性的角度考慮,所有用於調試的子程序都應該定義成void函數而非任務,以便以被任何其他任務或函數所調用。如下例所示:
// 例3.3 用於調試的void函數
function void print_state (...) ;
$display ("@%0t: state = %s", $time, cur_state.name()) ;
endfunction
在SystemVerilog中,如果你想調用函數並且忽略它的返回值,可以使用 void 進行結果轉換,如下例所示。有些仿真器,如VCS,允許你在不使用上述void語法的情況下忽略返回值。
// 例3.4 忽略函數的返回值
void '($fscanf (file, "%d", i)) ;
3.3 任務和函數概述
一般情況下,不帶參數的子程序在定義或調用時並不需要帶括號()。
在SystemVerilog中,begin...end塊變成可選的了,而在Verilog-1995中則對單行以外的子程序都是必須的。如下例所示,task/endtask和function/endfunction的關鍵詞已經足以定義這些子程序的邊界了。
// 例3.5 不帶 begin...end 的簡單任務
task multiple_lines ;
$display ("First line") ;
$display ("Second line") ;
endtask : multiple_lines
3.4 子程序參數
SystemVerilog對子程序的很多改進使參數聲明變得更加方便,同時也擴展了參數的傳遞方式。
3.4.1 C語言風格的子程序參數
// 例3.6 Verilog-1995的子程序參數
task mytask2 ;
output [31:0] x ;
reg [31:0] x ;
input y ;
...
endtask
// 例3.7 C語言風格的子程序參數
task mytask1 (output logic [31:0] x,
input logic y) ;
...
endtask
3.4.2 參數的方向
在子程序參數方面還可以有更多的便捷。因爲缺省的類型和方向是“logic 輸入”,所以在聲明類似參數時可不必重複。下例採用SystemVerilog的數據類型但以Verilog-1995的風格編寫的一個子程序頭。
// 例3.8 帶Verilog風格的繁冗的子程序參數
task T3
input a, b ;
logic a, b ;
output [15:0] u, v ;
bit [15:0] u, v ;
...
endtask
// 例3.9 帶缺省類型的子程序參數
task T3 (a, b, output bit [15:0] u, v) ;
儘管這種間接的編程方式,但是不建議使用這種方式,這種方式可能會使代碼滋生一些細小而難以發現的漏洞。所以建議對所有子程序的聲明都帶上類型和方向。
3.4.3 高級的參數類型
Verilog對參數的處理方式很簡單:
在子程序的開頭把input和inout的值複製給本地變量,在子程序退出時則複製output和inout的值。除了標量以外,沒有任何把存儲器傳遞給Verilog子程序的辦法。
在SystemVerilog中,參數的傳遞方式可以指定爲引用而不是複製。這種 ref 參數類型比input、output或inout更好用。首先,你現在可以吧數組傳遞給子程序。
// 例3.10 使用ref和const傳遞數組
function void print_checksum (const ref bit [31:0] a []) ;
bit [31:0] checksum = 0 ;
for (int i = 0; i < a.size(); i++)
checksum ^= a[i] ;
$display ("The array checksum is %0d", checksum) ;
endfunction
SystemVerilog允許不帶ref進行數組參數的傳遞,這時數組會被複制到堆棧區裏。這種操作的代價很高,除非是對特別小的數組。
SystemVerilog的語言參考手冊(LRM)規定了ref參數只能被用於帶自動存儲的子程序中。如果你對子程序或模塊指明瞭automatic屬性,則整個子程序內部都是自動存儲的。
上例中使用了const修飾符,雖然數組變量 a 指向了調用程序中的數組,但子程序不能修改數組的值。如果你試圖改變數組的值,編譯器將報錯。向子程序傳遞參數時應儘量使用ref以獲取最佳性能。如果你不希望子程序改變數組的值,可以使用 const ref 類型。這種情況下,編譯器會進行檢查以確保數組不被子程序修改。
ref參數的第二個好處是在任務裏可以修改變量而且修改結果對調用它的函數隨時可見。
// 例3.11 在多線程間使用 ref
task bus_read (input logic [31:0] addr,
ref logic [31:0] data) ;
// 請求總線並驅動地址
bus.request = 1'b1 ;
@(posedge bus.grant) bus.addr = addr ;
// 等待來自存儲器的數據
@(posedge bus.enable) data = bus.data ;
// 釋放總線並等待許可
bus.request = 1'b0 ;
@(posedge bus.grant) ;
endtask
logic [31:0] addr, data ;
initial
fork
bus_read (addr, data) ;
thread2 : begin
@data ; // 只要數據變化時即可觸發
$display ("Read %h from bus", data) ;
end
join
3.4.4 參數的缺省值
當測試程序越來越複雜時,你可能希望在不破壞原有代碼的情況下增加額外的控制。在SystemVerilog中,可以爲參數指定一個缺省值,如果在調用時不指名參數,則使用缺省值。
// 例3.12 帶缺省值的函數
function void print_checksum (ref bit [31:0] a [] ,
input bit [31:0] low = 0,
input int high = -1) ;
bit [31:0] checksum = 0 ;
if (high == -1 || high >= a.size())
high = a.size() - 1 ;
for (int i = low; i <= high; i++)
checksum += a[i] ;
$display ("The array checksum is %0d", checksum) ;
endfunction
// 例3.13 使用參數的缺省值
print_checksum (a) ; // a[0:size()-1] 中所有元素的校驗和 -- 缺省情況
print_checksum (a, 2, 4) ; // a[2:4]中所有元素的校驗和
print_checksum (a, 1) ; // 從1開始
print_checksum (a,,2) ; // a[0:2]中所有元素的校驗和
print_checksum () ; // 編譯錯誤,a沒有缺省值
使用-1(或其它任何越界值)作爲缺省值,對於獲知調用時是否有指定值,不失爲一種好方法。
3.4.5 採用名字進行參數傳遞
在SystemVerilog的語言參數手冊(LRM)中,任務或函數的參數有時也被稱爲端口“port”,就跟模塊的接口一樣。所以可以採用端口名關聯法進行參數傳遞。
// 例3.14 採用名字進行參數傳遞
task many (input int a = 1, b = 2, c = 3, d = 4) ;
$display ("%0d %0d %0d %0d", a, b, c, d) ;
endtask
initial begin
many (6, 7, 8, 9) ; // 6 7 8 9 指定所有值
many () ; // 1 2 3 4 使用缺省值
many (.c(5)) ; // 1 2 5 4 只指定c,其它使用缺省值
many (, 6, .d(8)) ; // 1 6 3 8 混合方式
end
3.4.6 常見代碼錯誤
在編寫代碼時最容易犯的錯誤就是,你往往會忘記,在缺省的情況下參數的類型是與其前一個參數相同的,而第一個參數的缺省類型是單比特輸入。
// 例3.15 原始的任務頭
task sticky (int a, b) ;
// 例3.16 加入額外數組參數的任務頭
task sticky (ref int array[50] ,
int a, b) ; // 這些變量的方向是什麼?
在例3.16中,a和b的參數類型是什麼呢?它們在方向上實際採用的是與前一個參數一致的 ref 類型。對簡單的int變量使用ref通常並無必要,但編譯器不會對此作出任何反應,連警告都沒有,所以你不會意識到正在使用一個錯誤的方向類型。
如果在子程序中使用了非缺省輸入類型的參數,應該明確指明所有參數的方向,如下所示:
// 例3.17 加入額外數組參數的任務頭
task sticky (ref int array[50] ,
input int a, b) ; // 明確指定方向
3.5 子程序的返回
Verilog中子程序的結束方式比較簡單:當你執行完子程序的最後一條語句,程序就會返回到調用子程序的代碼上。此外函數還會返回一個值,該值賦給與函數同名的變量。
3.5.1 返回(return)語句
SystemVerilog增加了return語句,使子程序中的流程控制變得更方便。下例的任務由於發現錯誤而需要提前返回。如果不這樣做,那麼任務中剩下的部分就必須放到一個else條件語句中,從而使得代碼變得不規整,可讀性也降低了。
// 例3.18 在任務中用return返回
task load_array (int len, ref int array []) ;
if (len <= 0) begin
$display ("Bad len") ;
return ;
end
// 任務中其餘的代碼
...
endtask
// 例3.19 在函數中使用return返回
function bit transmit (...) ;
// 發送處理
...
return ~ifc.cb.error ; // 返回狀態:0=error
endfunction
3.5.2 從函數中返回一個數組
Verilog的子程序只能返回一個簡單值,例如比特、整數或是向量。如果你想計算並返回一個數組,那就不是一件容易的事情了。在SystemVerilog中,函數可以採用多種方式返回一個數組。
第一種方式是定義一個數組類型,然後在函數的聲明中使用該類型。
// 例3.20 使用typedef從函數中返回一個數組
typedef int fixed_array5 [5] ;
fixed_array5 f5 ;
function fixed_array5 init (int start) ;
foreach (init[i])
init[i] = i + start ;
endfunction
initial begin
f5 = init (5)
foreach (f5[i])
$display ("f5[%0d] = %0d", i, f5[i]) ;
end
上述代碼的一個問題是,函數init創建一個數組,該數組的值被拷貝到數組f5中。如果數組很大,可能會引起性能上的問題。
另一種方式是通過引用來進行數組參數的傳遞。
// 例3.21 把數組作爲ref參數傳遞給函數
function void init (ref int f [5], input int start) ;
foreach (f[i])
f[i] = i + start ;
endfunction
int fa [5] ;
initial begin
init (fa, 5) ;
foreach (fa[i])
$display ("fa[%0d] = %0d", i, fa[i]) ;
end
從函數返回數組的最後一種方式是將數組包裝到一個類中,然後返回對象的句柄。
3.6 局部數據存儲
3.6.1 自動存儲
在Verilog-1995裏,如果你試圖在測試程序裏的多個地方調用同一個任務,由於任務裏的局部變量會使用共享的靜態存儲區,所以不同的線程之間會竄用這些局部變量。在Verilog-2001裏,可以指定任務、函數和模塊使用自動存儲,從而迫使仿真器使用堆棧區存儲局部變量。
在SystemVerilog中,模塊(module)和program塊中的子程序缺省情況下仍然使用靜態存儲。如果要使用自動存儲,則必須在程序語句中加入automatic關鍵詞。
// 例3.22 在program塊中指定自動存儲方式
program automatic test ; // program不可包含always/UDP/module/interface/program
task wait_for_mem (input [31:0] addr, expect_data,
output success) ;
while (bus.addr !== addr)
@(bus.addr) ;
success = (bus.data == expect_data) ;
endtask
...
endprogram
因爲參數addr和expect_data在每次調用時都使用不同的存儲空間,所以對這個任務同時進行多次調用是沒有問題的。但如果沒有修飾符automatic,由於第一次調用的任務處於等待狀態,所以對wait_for_mem的第二次調用會覆蓋它的兩個參數。
3.6.2 變量的初始化
當你試圖在聲明中初始化局部變量時,類似的問題也會出現,因爲局部變量實際上在仿真開始前就被賦了初值。常規的解決辦法是避免在變量聲明中賦予除常數以外的任何值。
// 例3.23 靜態初始化的漏洞
program initialization ;
task check_bus ;
repeat (5) @(posedge clock) ;
if (bus_cmd == 'READ) begin
//何時對 local_addr 賦初值?
logic [7:0] local_addr = addr << 2 ; //有漏洞
$display ("Local Addr = %h", local_addr) ;
end
endtask
endprogram
存在的漏洞是,變量local_addr是靜態分配的,所以實際上在仿真的一開始它就有初始值,而不是等到進入begin...end塊才進行初始化。同樣地,解決辦法是把程序塊聲明爲automatic:
// 例3.24 修復靜態初始化的漏洞:使用automatic
program automatic initialization ; // 漏洞修復
...
endprogram
此外,你如果不在聲明中初始化變量,那這個漏洞可以避免,只是這種方式不太好記住,尤其習慣了C語言的程序員。下例給雙一種較爲可取的編碼風格,用於分離聲明和初始化。
// 例3.25 修復靜態初始化的漏洞:把聲明和初始化拆開
logic [7:0] local_addr ;
local_addr = addr << 2 ; // 漏洞
3.7 時間值
SystemVerilog有幾種新結構使你可以非常明確地在你的系統中指明時間值。
3.7.1 時間單位和精度
當你依賴語句 `timescale 時,在編譯文件時就必須按照適當的順序以確保所有的時延都採用適宜的量和精度。timeunit 和 timeprecision 聲明語句可以明確地位每個模塊指明時間值,從而避免模糊不清。注意,如果你使用這些語句代替 `timescale ,則必須把它們放到每個帶有時延的模塊裏。
3.7.2 時間參數
SystemVerilog允許使用數值和單位來明確指定一個時間值。 $timeformat的四個參數分別是時間標度、小數點後的數據精度、時間值之後的後綴字符串、顯示數值的最小寬度。
// 例3.26 時間參數和$timeformat
module timing ;
timeunit 1ns ;
timeprecision 1ps ;
initial begin
$timeformat(-9, 3, "ns", 8) ;
#1 $display ("%t", $realtime) ; // 1.000ns
#2ns $display ("%t", $realtime) ; // 3.000ns
#0.1ns $display ("%t", $realtime) ; // 3.100ns
#41ps $display ("%t", $realtime) ; // 3.141ns
end
endmodule
3.7.3 時間和變量
你可以把時間值存放到變量裏,並在計算和延時中使用它們。根據當前的時間量程和精度,時間值會被縮放或舍入。time 類型的變量不能保存小數時延,因爲它們是64bit的整數,所以時延的小數部分會被舍入。如果你不希望這樣,你應該採用 real 變量。
// 例3.27 時間變量及舍入
`timescale 1ps/1ps
module ps;
initial begin
real rdelay = 800fs ; // 以0.800存儲
time tdelay = 800fs ; // 舍入後得到 1
$timeformat (-15, 0, "fs", 5) ;
#rdelay ; // 時延後得到 1ps
$display ("%t", rdelay) ; // "800fs"
#tdelay ; // 再次延時 1ps
$display ("%t", tdelay) ; // "1000fs"
end
endmodule
系統任務 $time的返回值時一個根據所在模塊的時間精度要求進行舍入的整數,不帶小數部分,而 $realtime 的返回值則是一個帶小數部分的完整實數。