1. 程式人生 > >SV元件實現篇之四:監測器的取樣

SV元件實現篇之四:監測器的取樣

當verifier梅在實現slave channel驗證環境的時候,她也繪製了一幅slave channel驗證結構圖:

verifier梅借鑑了verifier董在驗證模組registers時的方法,為DUT slave channel建立了2個stimulator,即上行傳送資料的initiator和下行接收資料的responder。在實現了這兩個元件之後,由initiator傳送的激勵,經過slave的資料通路可以最終送出至responder。在stimulator實現之後,就需要將必要的訊號採集下來,以用作日後的資料比較。

就之前的介紹,slave的initiator和responder應該採取各自的interface,即ini_if和rsp_if,而從不同介面上可以分別採集到送入slave的資料和從slave送出的資料。因此,我們傾向於也分別實現兩個內部結構類似的monitor,即ini_monitor和rsp_monitor(別怕麻煩,將monitor按照介面和功能分離為兩個的優勢在後面的驗證整合中會顯現)。待monitor採集完資料之後,我們也準備將其進一步傳送至checker。

對於monitor的功能而言,它的核心部分就是從interface做資料取樣(sampling)和打包(packaging)送給checker。我們在之前的《SV的環境構建篇之四:程式和模組》中已經提到了,設計部分(硬體)和驗證部分(軟體)之間可能存在競爭的現象,除了program可以消除競爭之外,我們也可以通過interface的clocking塊來消除競爭。所以,我們先在這裡引入interface clocking塊的介紹,再利用clocking來實現monitor的資料取樣功能。

interface clocking介紹

在之前《SV的環境構建篇之三:介面》中,可以看到硬體和軟體世界的連線可以通過靈活的interface來連線,也可以通過modport來進一步限定訊號傳輸的方向,避免埠連線的錯誤。同時,我們也可以在介面中宣告clocking(時序塊)來通過顯式地指出時鐘訊號,用其來對訊號做訊號的同步和取樣。clocking塊基於時鐘週期來對訊號在進行驅動或者取樣的方式,使得testbench不需要再關注於如何準確及時地對訊號傳輸或者取樣,也消除了訊號競爭的問題。通過clocking,testbench可以利用其進行:

  • 事件的同步
  • 輸入的取樣
  • 輸出的驅動

我們來看看,一個典型的clocking定義:

clocking bus @(posedge clock1);
default input #10ns output #2ns;
input data, ready, enable = top.mem1.enable;
output negedge ack;
input #1step addr;
endclocking

在上面這個例子中,第一行定義了一個clocking塊bus,由clock1的上升沿來驅動。第二行指出了在clocking塊中所有的訊號,預設情況下會在clocking事件(clock1上升沿)的前10ns來對其進行輸入取樣,在其事件的後2ns對其進行輸出驅動。下一行是聲明瞭要對其取樣的三個輸入訊號,data,ready和指向top.mem1.enable的enable訊號,這三個訊號作為輸入,它們的取樣時間即採用了預設輸入事件(clock1上升沿前的10ns)。第四行則聲明瞭要驅動的ack訊號,而驅動該訊號的時間則是時鐘clock1的下降沿,即覆蓋了原有的預設輸出事件(clock1上升沿後的2ns)。接下來的addr,也採用了自身定義的取樣事件,即clock1上升沿前的1step。這裡的1step會使得采樣發生在clock1上升沿的上一個time slot的postponed區域,即可以保證取樣到的資料是上一個時鐘週期的資料。

從上面這個例子可以看到,關於定義clocking塊需要注意的幾個地方:

  • clocking塊不但可以定義在interface中,也可以定義在module和program中。
  • clocking中列舉的訊號不是自己定義的,而是應該由interface或者其它宣告clocking的模組定義的。
  • clocking在宣告完名字之後,應該伴隨著定義預設的取樣事件,即“default input/output event”。如果沒有定義,則會採用預設的在clocking取樣事件前的1step對輸入進行取樣,在取樣事件後的#0對輸入進行驅動。
  • 除了定義預設的取樣和驅動事件,也可以在其後定義訊號方向時,用新的取樣事件對預設取樣和驅動事件做覆蓋。

這裡,我們再深入一些,討論關於所謂定義取樣和驅動事件的規定,例如,上面例子中第二行:

default input #10ns output #2ns;

相對的取樣事件是如何定義的。

從這張關於clocking事件的說明圖能夠得知,輸入訊號的取樣,會在關於時鐘事件(clock1上升沿)前的10ns做取樣,所以,規定的“input clocking_skew”中clocking skew(時鐘偏移量)採取的是負值,即相對clocking事件的前10ns;而輸出驅動則採用的是正值,會在clocking事件後的2ns時刻做輸出驅動。

所以,從上面的圖中可以發現,這種在時鐘事件前後的取樣或者驅動方式可以有效地避免競爭的情況,因為可以通過在不同的time slot中做訊號的取樣或驅動。例如input #1step,可以使得在上一個時鐘postponed區域做時鐘取樣,而#1ps,利用一個很小的輸出延遲,使得輸出可以在clocking事件(時鐘變化沿)後的time slot內發生變化。這樣就使得在clocking塊中羅列的訊號事件不會造成競爭的情況。

利用clocking事件同步

關於clocking功能的第一特性,使用起來同一般@或者wait事件方式沒有明顯差別,只是需要注意新增訊號所定義的clocking塊名稱。為了使得讀者對於包括clocking事件同步在內的clocking三個功能特性有更好的體會,我們拿出下面這個例子來看看:

module clocking1;
bit vld;
bit grt;
bit clk;

clocking ck @(posedge clk);
default input #3ns output #3ns;
input vld;
output grt;
endclocking

initial forever #5ns clk <= !clk;


initial begin: drv_vld
$display("$%0t vld initial value is %d", $time, vld);
#3ns vld = 1; $display("$%0t vld is assigned %d", $time, vld);
#10ns vld = 0; $display("$%0t vld is assigned %d", $time, vld);
#8ns vld = 1; $display("$%0t vld is assigned %d", $time, vld);
end

initial forever
@ck $display("$%0t vld is sampled as %d at sampling time $%0t", $time, vld, $time);
initial forever
@ck $display("$%0t ck.vld is sampled as %d at sampling time $%0t", $time, ck.vld, $time-3);
endmodule

無論是上面利用@ck(即@(posedge clk))還是@ck.vld,可以肯定的一點是,這些事件發生的時刻都是在clk的上升沿,因為無論是clocking塊本身,還是通過ck取樣的vld或者驅動的grant,都是基於時鐘上升沿的。所以,我們仍然可以通過使用@或者wait等耗時的操作符來基於訊號變化的事件進行同步。

利用clocking取樣資料

依然還是上面的clocking1的例子,其中雖然在clocking1::drv_vld中並沒有在clk時鐘沿驅動vld,但取樣時,仍然還是基於時鐘沿和取樣偏移量進行基於固定取樣時刻的資料取樣;如果沒有利用clocking塊進行取樣,那麼取樣只會基於時鐘沿(沒有疊加取樣偏移量)進行。

從上面的例子輸出結果可以看到:

# $0 vld initial value is 0
# $3 vld is assigned 1
# $5 ck.vld is sampled as 0 at sampling time $2
# $5 vld is sampled as 1 at sampling time $5
# $13 vld is assigned 0
# $15 vld is sampled as 0 at sampling time $15
# $15 ck.vld is sampled as 1 at sampling time $12
# $21 vld is assigned 1
# $25 ck.vld is sampled as 1 at sampling time $22
# $25 vld is sampled as 1 at sampling time $25

vld的驅動和取樣,也可以從波形圖例項中有更直觀的體現:

從波形圖上也可以看到,如果只是基於clk上升沿,那麼取樣的結果同基於clk上升沿疊加取樣偏移量的結果是不相同的。如果,我們將取樣偏移量抽象為物理中的建立時間(setup time),則其要求更明確為時鐘上升沿之前的某一段時間內,資料不能有變化,否則資料取樣會有不一樣的結果;如果只是基於時鐘上升沿取樣,那麼上圖中的“取樣vld”則表示了取樣出的結果。而無論是“取樣ck.vld”還是“取樣vld”,值得變化均發生在了時鐘上升沿,只是背後的取樣點不同罷了。

利用clocking產生激勵

同樣地,如果取樣clocking塊的驅動,也可以實現一種類似於“物理保持時間”的驅動方式,實現了驅動時鐘沿疊加驅動偏移量的延遲效果。例如下面的例子:

module clocking2;
bit vld;
bit grt;
bit clk;

clocking ck @(posedge clk);
default input #3ns output #3ns;
input vld;
output grt;
endclocking

initial forever #5ns clk <= !clk;

initial begin: drv_grt
$display("$%0t grt initial value is %d", $time, grt);
@ck ck.grt <= 1; $display("$%0t grt is assigned 1", $time);
@ck ck.grt <= 0; $display("$%0t grt is assigned 0", $time);
@ck ck.grt <= 1; $display("$%0t grt is assigned 1", $time);
end

initial forever
@grt $display("$%0t grt is driven as %d", $time, grt);
endmodule

上面的例子中,對於ck塊中的grt賦值,都是通過clocking塊ck.grt來引用進而賦值的。所以,雖然賦值的時間點都是在clk上升沿觸發的,而真正grt值發生變化的時刻都是在clk上升沿疊加驅動偏移量的時間點。關於這一點,可以從下面的輸出結果以及驅動時序波形圖中印證。

輸出結果:

# $0 grt initial value is 0
# $5 grt is assigned 1
# $8 grt is driven as 1
# $15 grt is assigned 0
# $18 grt is driven as 0
# $25 grt is assigned 1
# $28 grt is driven as 1

monitor的取樣功能

在介紹完了clocking塊的三種屬性之後,verifier梅就準備利用這一特性來實現slave monitor的資料取樣。由於slave的輸入輸出端資料協議簡單,因此,我們準備將採集到的資料都存入到一個統一的資料格式中:

class slv_trans; // 資料包定義
logic [31:0] data;
endclass

接下來,我們引入作為連線stimulator與monitor的interface,slv_ini_if和slv_rsp_if:



interface slv_ini_if(
input rstn,
input clk
);
logic valid;
logic [31:0] data;
logic ready;

slv_trans mon_fifo[$]; // 資料儲存FIFO

clocking ck_mon @(posedge clk); // 取樣clocking
default input #1step output #1ps;
input valid;
input data;
input ready;
endclocking

function void put_trans(slv_trans t); // 寫入FIFO
mon_fifo.push_back(t);
$display("slv_ini_if::mon_fifo size is %0d", mon_fifo.size());
endfunction
endinterface

interface slv_rsp_if(
input rstn,
input clk
);
logic req;
logic [31:0] data;
logic ack;

slv_trans mon_fifo[$];

clocking ck_mon @(posedge clk);
default input #1step output #1ps;
input req;
input data;
input ack;
endclocking

function void put_trans(slv_trans t);
mon_fifo.push_back(t);
$display("slv_rsp_if::mon_fifo size is %0d", mon_fifo.size());
endfunction
endinterface

從上面定義的兩個介面來看,verifier梅利用了clocking塊的取樣特性,在其中分別聲明瞭用來取樣的clocking和訊號列表。同時,它們各自提供了一個開放的方法和FIFO(用佇列來實現),用來儲存採集到的資料。

再來看看兩個monitor,即slv_ini_mon和slv_rsp_mon是如何定義的:

class slv_ini_mon;

slv_trans trans;
virtual interface slv_ini_if vif;

task run(); // 運轉方法
forever begin
mon_trans();
put_trans();
end
endtask

task mon_trans(); // 採集有效資料
forever begin
@(posedge vif.clk iff vif.rstn);
if(vif.ck_mon.valid === 1 && vif.ck_mon.ready === 1) begin
trans = new();
trans.data = vif.ck_mon.data;
break;
end
end
endtask

function void put_trans(); // 將資料寫入介面中的FIFO
vif.put_trans(trans);
endfunction
endclass

class slv_rsp_mon;
slv_trans trans;
virtual interface slv_rsp_if vif;

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

task mon_trans();
forever begin
@(posedge vif.clk iff vif.rstn);
if(vif.ck_mon.req === 1 && vif.ck_mon.ack === 1) begin
trans = new();
trans.data = vif.ck_mon.data;
break;
end
end
endtask

function void put_trans();
vif.put_trans(trans);
endfunction
endclass

上述兩個monitor提供的方法相同,均為:

  • run:讓monitor運轉起來,保持時刻監視和取樣資料。
  • mon_trans:每次收集一個有效資料。
  • put_trans:將有效資料通過虛介面vif存入介面中的FIFO。

再來看看兩個monitor提供的成員變數,分別是自己對應的虛介面和一個只有宣告,並沒有在構建函式中例化的控制代碼trans。

這裡的mon_trans在採集完有效的一次資料寫入後,通過clocking塊準確地將取樣到的資料先先入到“臨時建立”的物件中,該物件的控制代碼為trans,進而通過break退出方法。接下來,將trans的值(新建立物件的控制代碼)寫入到介面的FIFO中,如此往復。

這裡,讀者需要注意的是,通過每次觸發有效的資料寫入事件,來建立新的物件trans = new(),進而將有效資料寫入到trans指向的新物件中,再到隨後通過vif.put_trans(trans)將每次控制代碼的值寫入介面的FIFO中,這整個過程當中,兩個monitor都在不斷地建立物件,那麼在每次更改trans值(從舊物件指向了新的物件),之前物件是否還存在呢?畢竟,讀者可能疑惑,trans畢竟不再指向舊物件了。

答案是,之前建立的物件仍然存在,因為FIFO中每個元素仍然在引用之前建立的每一個物件,根據SV空間自動回收機制的用法,只有我們再銷燬了FIFO即這個佇列之後,那麼之前建立的物件由於環境中再沒有其它控制代碼的引用,這樣所有之前建立的物件才會被回收。例如,我們可以通過佇列的delete()刪除整個佇列,進而銷燬其元素指向的物件。

從上面這個上意圖來看,隨著有效資料取樣發生e1 -> e2 -> e3 -> e4 -> ...,每次都會建立新的物件,而且trans也會指向最想的物件,同時之前建立的所有物件的控制代碼都存入vif.mon_fifo中。注意,這裡存入的是物件的控制代碼,而不是物件本身。以後,其它的元件例如checker,也可以通過介面內FIFO的控制代碼元素來索引到它們分別指向的物件。

上面的這個資料打包寫入的方式,是將interface內定義的佇列作為資料快取中間站,而後,一旦checker建立好以後,也可以將interface的指標傳遞給checker,進而為checker提供get_trans()的方法。這樣,checker即可以從各個interface中得到採集到的資料。

實際上,取樣好的資料應該快取到什麼地方,選擇有很多,除了可以存放到interface中,也可以考慮存放到monitor或者checker內部,而後通過monitor與checker之間的直接對話來實現。這也就是下一節我們會引出的,元件之間的主要對話方式,或者程序之間同步的方法。

最後,我們再來看看,上面定義的interface和monitor在頂層testbench的例化:

module slave_tb;

logic rstn;
logic clk;
logic ch_valid;
logic [31:0] ch_data;
logic ch_ready;
logic req;
logic [31:0] data;
logic ack;

slave dut(...); // DUT slave例化
// 介面例化
slv_ini_if ini_if(.rstn(rstn), .clk(clk));
slv_rsp_if rsp_if(.rstn(rstn), .clk(clk));

//monitor
slv_ini_mon ini_mon;
slv_rsp_mon rsp_mon;

// 介面連線
assign ini_if.valid = ch_valid;
assign ini_if.data = ch_data;
assign ini_if.ready = ch_ready;
assign rsp_if.req = req;
assign rsp_if.ack = ack;
assign rsp_if.data = data;

initial begin
// 建立monitor物件
ini_mon = new();
rsp_mon = new();
// 連線虛介面
ini_mon.vif = ini_if;
rsp_mon.vif = rsp_if;
// 令monitor物件開始工作
fork
ini_mon.run();
rsp_mon.run();
join
end

... // test場景建立和激勵生成
endmodule

至此,monitor通過clocking塊做資料取樣並且打包寫入快取的方式接介紹完了。目前,我們暫時可以將資料快取至介面內的快取,而在我們下一節學習了《元件間的通訊》之後,我們就可以用程序間通訊的三種類型來做更靈活的嘗試了。