1. 程式人生 > >SV元件實現篇之六:比較器和參考模型

SV元件實現篇之六:比較器和參考模型

本文轉自:http://www.eetop.cn/blog/html/28/1561828-2316831.html

在同之前的verifier梅、尤、董完成了slave、arbiter和registers的模組驗證之後,我們需要看看最後一位verifier婁是如何完成arbiter驗證的。Verifier婁也依照之前的驗證步驟,給出了arbiter的驗證框圖:

在實現了stimulator和monitor之後,就進入了資料比對和功能檢查的環節了。在開始考慮實現checker之前,我們先來看看,如果要完成formatter的功能驗證,需要考慮的功能點檢查,即驗證計劃是什麼?

  1. 復位檢查。檢查在復位以後,formatter各個輸出訊號的值。考慮到verifier婁是一位新手的情況下,他儘量將formatter作為一個黑盒去檢查,而很少關注其內部的訊號狀態。
  2. 資料完整性。即資料的輸入到輸出,沒有資料發生丟失,而且資料按照協議要求進行傳輸。
  3. 功能配置情況。即在暫存器功能配置下,formatter可以將來自不同slave的資料打包為不同長度的資料包。
  4. 協議檢查。從arbiter到formatter,以及formatter到外部下行資料段的兩個協議部分是否符合時序要求。

 

上面的驗證功能點檢查,從列出的功能點,到考慮給出的激勵場景,都是互相對應的。而面對更復雜的設計,要列出的功能點檢查項要更多呢!那麼,我們就上面要檢查的功能點,來看看verifier婁準備如何實現它的checker。實際中,我們可以就要檢查的場景,分為異常檢查、常規檢查和協議檢查:

  • 異常檢查:由於某一異常事件觸發,進而使得設計做出的響應。
  • 常規檢查:指的是設計在正常工作狀態下的表現,一般可以伴隨長時間的穩定工作。
  • 時序檢查:檢查DUT的內部或者外部訊號之間的時序是否符合設計要求。

 

接下來,我們將上面的功能點檢查,與上面三種檢查種類進行劃分的話,它們的類屬關係是,復位檢查由於是復位訊號的事件使得設計發生相應的變化,我們可以視為異常檢查;資料完整性和功能配置情況都是在formatter穩定工作時的檢查,可以歸類為常規檢查;協議檢查則因為特別注重時序性的要求,而應當歸類為時序檢查。而就這種檢查的場景而言,實現功能檢查的方式也不相同:異常檢查主要就事件觸發來檢查設計的響應;而常規檢查則通過檢視設計的配置,來判斷設計的工作狀態是否符合預期;對於時序檢查,則會通過捕捉訊號間的時序關係來進行檢查。對於這三種檢查方式,本文也將依次詳細論述。

 

 

異常檢查

在設計異常的時候,通過設計外部或者內部的異常訊號,我們可以檢查設計的響應(也分內部和外部)是否正確。如果檢查得更細緻,我們還應該考慮:

  • 異常事件的觸發是否合理,符合設計要求。
  • 異常事件的處理是否正確。
  • 異常事件恢復的條件在滿足之後,設計是否能夠再次回到穩定的工作狀態。

 

下面的示例程式碼就是用來檢查“設計復位”的。通過這個例子,讀者可以知道,如何定義一些事件、已經如何針對事件作出響應和如何對事件檢查的功能做出控制。

class fmt_ini_mon;
event reset_e;
task run();
fork
mon_reset();
join_none
endtask

task mon_reset();
forever begin
@(negedge vif.rstn);
-> reset_e;
end
endtask
endclass


 

首先來看看fmt_ini_mon,在其內部定義了成員事件reset_e,通過方法mon_reset()來捕捉復位事件,進而觸發reset_e。那麼這個reset_e用來通知誰呢?我們在本文最後的程式碼全覽中可以看到,這個事件是用來通知比較器fmt_checker。在fmt_checker中,它對reset_e這個事件時刻保持關注,一旦等到了事件被觸發,它便會執行功能檢查chk_rst()。在下面的程式碼中,fmt_checker宣告的事件reset_e是等同於fmt_ini_mon中的同名事件,而這兩個事件的等價傳遞發生在了頂層的testbench環境中。而類似的,關於虛擬介面在chk_rst()中的引用也需要首先保證它們在頂層環境被連線到真正的物理介面上。在事件chk_rst()的檢查中,會檢查formatter的所有輸出介面,且要求在復位觸發時,輸出訊號被置為期望的值,否則通過系統函式$error()報錯。

class fmt_checker;
bit en_chk_rst=1;
event reset_e;

virtual interface fmt_ini_if ini_vif;
virtual interface fmt_rsp_if rsp_vif;

task run();
fork
chk_rst();
join_none
endtask

task chk_rst();
forever begin
@reset_e;
if(en_chk_rst == 1) begin
if(ini_vif.f2a_ack !== 0
|| rsp_vif.fmt_chid !== 0
|| rsp_vif.fmt_length !== 0
|| rsp_vif.fmt_req !== 0
|| rsp_vif.fmt_data !== 0
|| rsp_vif.fmt_start !== 0
|| rsp_vif.fmt_end !== 0)
$error("fmt_checker:: reset value is not correct!");
end
end
endtask
endclass


 

常規檢查

對於常規檢查而言,它是一個長期監測並且時刻檢查的行為,這就要求觀察三個方面:

  • DUT的配置情況
  • DUT的輸入資料
  • DUT的輸出資料

 

對於這三方面的資料監視和最終的比較,這部分提出了兩種可行的解決的方式:

  • 拆分檢查:是將DUT要檢查的功能點有機的剝離開,並且就每個功能點做獨立的檢查。
  • 整體檢查:是將DUT的所有輸入包括配置情況作為“參考模型”的輸入,而參考模型會按照設計要求,將最終期望的資料輸出。

 

下面是對兩種檢查方式的具體實現程式碼。

 

拆分檢查

class fmt_ini_trans;
bit [ 1:0] id;
bit [31:0] data;
int length;

static function int dec_length(int l);
int len;
case(l)
0: len = 4;
1: len = 8;
2: len = 16;
3: len = 32;
default len = 32;
endcase
return len;
endfunction
endclass

class fmt_ini_mon;
fmt_ini_trans trans;
mailbox #(fmt_ini_trans) ini_mb;
virtual interface fmt_ini_if vif;

task run();
fork
forever begin
mon_trans();
put_trans();
end
join_none
endtask

task mon_trans();
forever begin
@(posedge vif.clk iff vif.rstn);
if(vif.mon.a2f_val === 1 && vif.mon.f2a_ack === 1) begin
trans = new();
case(vif.mon.a2f_id)
0: trans.length = trans.dec_length(vif.mon.slv0_len);
1: trans.length = trans.dec_length(vif.mon.slv1_len);
2: trans.length = trans.dec_length(vif.mon.slv2_len);
3: $error("fmt_ini_mon:: a2f_id value is not as expected");
endcase

trans.id = vif.mon.a2f_id;
trans.data = vif.mon.a2f_dat;
break;
end
end
endtask

task put_trans();
ini_mb.put(trans);
endtask
endclass

上面的fmt_ini_mon會在每次有效的資料輸入時採集好資料,裝入新建立的資料物件trans。這裡我們會要求每次建立新的物件引來裝入新的資料。fmt_ini_mon通過mon_trans()和put_trans()兩個成員方法可以時刻監視和傳輸捕捉到的有效資料包。由於輸入資料沒有包的首和尾,因此定義的資料傳輸類fmt_ini_trans的資料成員也只裝載每一個時鐘週期的取樣。另外需要注意的是,由於從暫存器register到formatter傳遞的包長配置訊號slvX_len需要解碼,因此我們在fmt_ini_trans中定義瞭解碼的方法。而該方法dec_length()被宣告為了靜態方法是由於其它外部的函式也需要單獨呼叫該方法(並不需要例化物件)。

class fmt_rsp_trans;
bit [ 1:0] id;
bit [31:0] data_q[$];
int length;

function bit compare(fmt_rsp_trans t);
if(id != t.id
|| length != t.length
|| data_q.size() != t.data_q.size())
return 0;
foreach(data_q[i]) begin
if(data_q[i] != t.data_q[i])
return 0;
end
return 1;
endfunction
endclass

class fmt_rsp_mon;
fmt_rsp_trans trans;
event req_trans_e;
mailbox #(fmt_rsp_trans) rsp_mb;
virtual interface fmt_rsp_if vif;

task run();
fork
forever begin
mon_trans();
put_trans();
end
mon_req_trans();
join_none
endtask

task mon_trans();
forever begin
@(posedge vif.clk iff vif.rstn);
if(vif.mon.fmt_start === 1) begin
trans = new();
trans.length = vif.mon.fmt_length;
trans.id = vif.mon.fmt_chid;
repeat(trans.length) begin
trans.data_q.push_back(vif.mon.fmt_data);
@(posedge vif.clk);
end
break;
end
end
endtask

task put_trans();
rsp_mb.put(trans);
endtask

task mon_req_trans();
forever begin
@(posedge vif.mon.fmt_req iff vif.rstn);
-> req_trans_e;
end
endtask
endclass


 

從fmt_rsp_mon的定義來看,主要做了兩件事情。

 

首先,它同fmt_ini_mon類似的是,也捕捉了輸出的資料,通過觀察vif.mon.fmt_start訊號來捕捉包首,進而逐次取樣資料。在這裡,因為formatter輸出協議有明顯的包的含義,所以,我們將輸出資料包fmt_rsp_trans定義為內部包含多個數據的類,這方便與fmt_rsp_mon可以將一個完整的資料包存入其中。

 

其次,fmt_rsp_mon也單獨捕捉了一個事件,即資料包要傳送時的請求事件。當vif.mon_fmt_req從0跳轉到1時,我們將這個資料包請求傳送的事件捕捉下來,觸發事件req_trans_e。而該事件又等價於在fmt_checker中同名的事件req_trans_e。fmt_checker也將利用這個事件,檢查功能配置是否生效。

 

有一些讀者在參考上面fmt_rsp_mon::mon_trans時對於資料取樣為什麼直接從vif.mon.fmt_start上升沿開始而不是從vif.mon.fmt_req開始可能有疑慮,因為按照協議的要求fmt_req需要先等待fmt_grant,再而才能出發fmt_start。那麼這個時序部分的檢查,在fmt_rsp_mon::mon_trans()採集資料包時可以省略嗎?為什麼?

 

答案是,我們當然很關注邊界訊號協議時序的檢查,而這部分的檢查我們則會交給第三部分的檢查,即“時序檢查”,在接下來的示例程式碼中讀者可以看到。所以,我們在fmt_rsp_mon::mon_trans()中省略了檢查,而使得程式碼變得更為清爽,不是嗎?

 

此外,對於formatter輸出端的資料型別fmt_rsp_trans定義中,也添加了其用來做資料比較的方法fmt_rsp_trans(fmt_rsp_trans t),這個方法會在後面的fmt_checker中用來比較兩部分的資料,即實際取樣到的輸出資料和期望的輸出資料進行比較,如果兩個資料物件相同則返回1,否則返回0。

 

在定義了上面兩個monitor即fmt_ini_mon和fmt_rsp_mon之後,將由fmt_checker做最終的功能檢查:

class fmt_checker;
bit en_chk_data=1;
bit en_chk_len=1;

virtual interface fmt_ini_if ini_vif;
virtual interface fmt_rsp_if rsp_vif;
event req_trans_e;
mailbox #(fmt_ini_trans) ini_mb;
mailbox #(fmt_rsp_trans) rsp_mb;
mailbox #(fmt_rsp_trans) exp_mb;
function new();
ini_mb = new();
rsp_mb = new();
exp_mb = new();
endfunction

task run();
fork
ini2rsp_fmt();
chk_data();
chk_len();
join_none
endtask

task chk_data();
fmt_rsp_trans exp, rsp;
forever begin
wait(en_chk_data == 1);
fork
rsp_mb.get(rsp);
exp_mb.get(exp);
join
if(rsp.compare(exp) == 1)
$display("fmt_checker:: data compared succeeded!");
else
$error("fmt_checker:: data compared failed!");
end
endtask

task ini2rsp_fmt();
fmt_ini_trans s;
fmt_rsp_trans t;
forever begin
ini_mb.get(s);
t = new();
t.id = s.id;
t.length = s.length;
t.data_q.push_back(s.data);
repeat(t.length - 1) begin
ini_mb.get(s);
if(t.id != s.id)
$error("fmt_checker:: data input id is changed!");
t.data_q.push_back(s.data);
end
exp_mb.put(t);
end
endtask

task chk_len();
int length;
forever begin
@req_trans_e;
if(en_chk_len == 1) begin
case(rsp_vif.mon.fmt_chid)
0: length = fmt_ini_trans::dec_length(ini_vif.mon.slv0_len);
1: length = fmt_ini_trans::dec_length(ini_vif.mon.slv1_len);
2: length = fmt_ini_trans::dec_length(ini_vif.mon.slv2_len);
default: $error("fmt_checker:: id value is unexpected");
endcase
if(length != rsp_vif.mon.fmt_length)
$error("fmt_checker:: output length is not as the value configured");
end
end
endtask
endclass

 

在fmt_checker中聲明瞭用來使能檢查功能的控制訊號en_chk_data和en_chk_len,這便於後期在頂層環境的集中控制。除了宣告虛介面ini_vif和rsp_vif以外,fmt_checker還宣告並且例化了三個用來裝載資料的mailbox:ini_mb、rsp_mb和exp_mb。

 

由於從ini_mb收集到的資料型別是從formatter輸入端採集到了fmt_ini_trans型別,而rsp_mb是從formatter輸出端採集到的資料型別fmt_rsp_trans,這兩種資料型別有不小的差別,前者儲存單週期的資料,後者儲存多個週期的資料包。如果要對這兩種資料包做比較,我們需要將它們統一到同樣地格式,這裡fmt_checker選擇了利用fmt_checker::ini2rsp_fmt()方法,將從ini_mb得到的資料利用其所含的資訊包裝成為fmt_rsp_trans型別,進而再儲存到第三個mailbox exp_mb。

 

因為,我們認為exp_mb儲存的資料是期望得到的資料,因為其內部的資料是我們通過formatter資料打包的邏輯組裝而成的,並非是直接取樣得到的資料。而rsp_mb中儲存的取樣到的輸出資料,同exp_mb的資料成員均為fmt_rsp_trans,這就使得比較這兩個FIFO之中的資料變得容易多了。fmt_checker::chk_data()做的就是先從這兩個mailbox中取到資料物件,進而利用fmt_rsp_trans::comapre()做資料比較,將資料比對結果報告出來。

 

所以,對於功能檢查點“資料完整性”的檢查就可以通過上面兩個方法chk_data()和ini2rsp_fmt()完成。那麼,再來看看,對於“功能配置情況”應該如何檢查呢?

 

fmt_checker::chk_len()通過等待事件req_trans_e來觀察formatter在何時發起一個新的資料包。而檢查registers到formatter配置是否成功就在於formatter輸入端slvX_len的配置,是否可以控制輸出包的長度。因此,通過比對slvX_len與fmt_length是否匹配,就可以完成“功能配置的檢查“。

 

因此,上面的檢查方式,是將兩個不同的功能點檢查獨立到不同的檢查方法中完成,互相不影響,並且有獨立的使能訊號en_chk_data和en_chk_len可以控制這兩種檢查。

 

整體檢查(參考模型)

除了獨立拆分的檢查方法之外,我們也可以按照硬體設計描述文件(埠、協議、狀態機)來建立一個參考模型(reference model)。而模型的細緻程度也依賴於checker的要求,檢查得越細緻,對參考模型的準確程度也要求越高。在日常中,除了verifier會自己寫這樣一個參考模型之外,有時候從系統工程師那裡也可能得到了虛擬原型(virtual prototype)或者演算法模型(algorithm model),該原型也是對於設計更具象的要求。對於虛擬原型和演算法模型在驗證環境的嵌入,我們也會在本書後面的章節中介紹。在這裡,我們先討論如何實現一個輕鬆一點,維護較少易複用的參考模型吧。

 

在整體檢查的這段示例程式碼中,首先將參考模型定義為一個類fmt_refmod,它承擔的任務就是將從fmt_ini_mon監測到的fmt_ini_trans資料通過自定義的邏輯轉換為fmt_rsp_trans,因此它內部分別需要兩個mailbox ini_mb和exp_mb。對於它做資料打包的成員方法fmt_refmod::ini2rsp_fmt(),有興趣的讀者可以發現,這個方法實際上是之前fmt_checker::ini2rsp_fmt()和fmt_checker::chk_len()的整合版本。因為它用來做資料打包長度的依據變為根據即將要發包時的暫存器配置數值,而不是之前從fmt_ini_trans::length得來。這就將檢查資料包長度也同資料打包功能合併在了一起,而到了後期進行資料比對時,如果發生錯誤,那麼可以想到的是,有可能是資料包長度的功能錯誤,也可能是本身formatter輸入端資料採集的功能錯誤。

 

那麼,這樣一個完整的參考模型fmt_refmod一旦完成之後,就可以方便地整合到fmt_checker中。在上面的fmt_checker中除了需要例化fmt_checker::refmod之外,也需要注意新添加了方法fmt_checker::connect()。這一方法是用來對fmt_checker內部的元件同自身相連線的,而該方法的呼叫需要在稍後展示的外部formatter_tb中完成。通過這一方法使得fmt_checker::refmod中的虛介面和事件得到賦值,同時也將fmt_checker::refmod的mailbox賦值給fmt_checker中宣告的mailbox指標。

 

1494348931122

1495468297999