本節書摘來自華章社群《uvm實戰》一書中的第2章,第2.3節為驗證平台加入各個元件,作者 張 強,更多章節内容可以通路雲栖社群“華章社群”公衆号檢視
2.3 為驗證平台加入各個元件
2.3.1 加入transaction
在2.2節中,所有的操作都是基于信号級的。從本節開始将引入reference model、monitor、scoreboard等驗證平台的其他元件。在這些元件之間,資訊的傳遞是基于transaction的,是以,本節将先引入transaction的概念。
transaction是一個抽象的概念。一般來說,實體協定中的資料交換都是以幀或者包為機關的,通常在一幀或者一個包中要定義好各項參數,每個包的大小不一樣。很少會有協定是以bit或者byte為機關來進行資料交換的。以以太網為例,每個包的大小至少是64byte。這個包中要包括源位址、目的位址、包的類型、整個包的crc校驗資料等。transaction就是用于模拟這種實際情況,一筆transaction就是一個包。在不同的驗證平台中,會有不同的transaction。一個簡單的transaction的定義如下:
代碼清單 2-23
其中dmac是48bit的以太網目的位址,smac是48bit的以太網源位址,ether_type是以太網類型,pload是其攜帶資料的大小,通過pload_cons限制可以看到,其大小被限制在46~1500byte,crc是前面所有資料的校驗值。由于crc的計算方法稍顯複雜,且其代碼在網絡上随處可見,是以這裡隻是在post_randomize中加了一個空函數calc_crc,有興趣的讀者可以将其補充完整。post_randomize是systemverilog中提供的一個函數,當某個類的執行個體的randomize函數被調用後,post_randomize會緊随其後無條件地被調用。
在transaction定義中,有兩點值得引起注意:一是my_transaction的基類是uvm_sequence_item。在uvm中,所有的transaction都要從uvm_sequence_item派生,隻有從uvm_sequence_item派生的transaction才可以使用後文講述的uvm中強大的sequence機制。二是這裡沒有使用uvm_component_utils宏來實作factory機制,而是使用了uvm_object_utils。從本質上來說,my_transaction與my_driver是有差別的,在整個仿真期間,my_driver是一直存在的,my_transaction不同,它有生命周期。它在仿真的某一時間産生,經過driver驅動,再經過reference model處理,最終由scoreboard比較完成後,其生命周期就結束了。一般來說,這種類都是派生自uvm_object或者uvm_object的派生類,uvm_sequence_item的祖先就是uvm_object。uvm中具有這種特征的類都要使用uvm_object_utils宏來實作。
當完成transaction的定義後,就可以在my_driver中實作基于transaction的驅動:
代碼清單 2-24
在main_phase中,先使用randomize将tr随機化,之後通過drive_one_pkt任務将tr的内容驅動到dut的端口上。在drive_one_pkt中,先将tr中所有的資料壓入隊列data_q中,之後再将data_q中所有的資料彈出并驅動。将tr中的資料壓入隊列data_q中的過程相當于打包成一個byte流的過程。這個過程還可以使用systemverlog提供的流操作符實作。具體請參照systemverilog語言标準ieee std 1800tm—2012(ieee standard for systemverilog—unified hardware design, specification, and verification language)的11.4.14節。
2.3.2 加入env
在驗證平台中加入reference model、scoreboard等之前,思考一個問題:假設這些元件已經定義好了,那麼在驗證平台的什麼位置對它們進行執行個體化呢?在top_tb中使用run_test進行執行個體化顯然是不行的,因為run_test函數雖然強大,但也隻能執行個體化一個執行個體;如果在top_tb中使用2.2.1節中執行個體化driver的方式顯然也不可行,因為run_test相當于在top_tb結構層次之外建立一個新的結構層次,而2.2.1節的方式則是基于top_tb的層次結構,如果基于此進行執行個體化,那麼run_test的引用也就沒有太大的意義了;如果在driver中進行執行個體化則更加不合理。
這個問題的解決方案是引入一個容器類,在這個容器類中執行個體化driver、monitor、reference model和scoreboard等。在調用run_test時,傳遞的參數不再是my_driver,而是這個容器類,即讓uvm自動建立這個容器類的執行個體。在uvm中,這個容器類稱為uvm_env:
代碼清單 2-25
這個new函數有兩個參數,第一個參數是執行個體的名字,第二個則是parent。由于my_driver在uvm_env中執行個體化,是以my_driver的父結點(parent)就是my_env。通過parent的形式,uvm建立起了樹形的組織結構。在這種樹形的組織結構中,由run_test建立的執行個體是樹根(這裡是my_env),并且樹根的名字是固定的,為uvm_test_top,這在前文中已經講述過;在樹根之後會生長出枝葉(這裡隻有my_driver),長出枝葉的過程需要在my_env的build_phase中手動實作。無論是樹根還是樹葉,都必須由uvm_component或者其派生類繼承而來。整棵uvm樹的結構如圖2-3所示。
當加入了my_env後,整個驗證平台中存在兩個build_phase,一個是my_env的,一個是my_driver的。那麼這兩個build_phase按照何種順序執行呢?在uvm的樹形結構中,build_phase的執行遵照從樹根到樹葉的順序,即先執行my_env的build_phase,再執行my_driver的build_phase。當把整棵樹的build_phase都執行完畢後,再執行後面的phase。
my_driver在驗證平台中的層次結構發生了變化,它一躍從樹根變成了樹葉,是以在top_tb中使用config_db機制傳遞virtual my_if時,要改變相應的路徑;同時,run_test的參數也從my_driver變為了my_env:
代碼清單 2-27
2.3.3 加入monitor
驗證平台必須監測dut的行為,隻有知道dut的輸入輸出信号變化之後,才能根據這些信号變化來判定dut的行為是否正确。
驗證平台中實作監測dut行為的元件是monitor。driver負責把transaction級别的資料轉變成dut的端口級别,并驅動給dut,monitor的行為與其相對,用于收集dut的端口資料,并将其轉換成transaction交給後續的元件如reference model、scoreboard等處理。
一個monitor的定義如下:
代碼清單 2-29
有幾點需要注意的是:第一,所有的monitor類應該派生自uvm_monitor;第二,與driver類似,在my_monitor中也需要有一個virtual my_if;第三,uvm_monitor在整個仿真中是一直存在的,是以它是一個component,要使用uvm_component_utils宏注冊;第四,由于monitor需要時刻收集資料,永不停歇,是以在main_phase中使用while(1)循環來實作這一目的。
在查閱collect_one_pkt的代碼時,可以與my_driver的drv_one_pkt對比來看,兩者代碼非常相似。當收集完一個transaction後,通過my_print函數将其列印出來。my_print在my_transaction中定義如下:
代碼清單 2-30
需要引起注意的是這裡執行個體化了兩個monitor,一個用于監測dut的輸入口,一個用于監測dut的輸出口。dut的輸出口設定一個monitor沒有任何疑問,但是在dut的輸入口設定一個monitor有必要嗎?由于transaction是由driver産生并輸出到dut的端口上,是以driver可以直接将其交給後面的reference model。在2.1節所示的框圖中,也是使用這樣的政策。是以是否使用monitor,這個答案仁者見仁,智者見智。這裡還是推薦使用monitor,原因是:第一,在一個大型的項目中,driver根據某一協定發送資料,而monitor根據這種協定收集資料,如果driver和monitor由不同人員實作,那麼可以大大減少其中任何一方對協定了解的錯誤;第二,在後文将會看到,在實作代碼重用時,使用monitor是非常有必要的。
現在,整棵uvm樹的結構如圖2-4所示。

在env中執行個體化monitor後,要在top_tb中使用config_db将input_if和output_if傳遞給兩個monitor:
代碼清單 2-32
*2.3.4 封裝成agent
上一節在驗證平台中加入monitor時,讀者看到了driver和monitor之間的聯系:兩者之間的代碼高度相似。其本質是因為二者處理的是同一種協定,在同樣一套既定的規則下做着不同的事情。由于二者的這種相似性,uvm中通常将二者封裝在一起,成為一個agent。是以,不同的agent就代表了不同的協定。
代碼清單 2-33
這個枚舉變量僅有兩個值:uvm_passive和uvm_active。在uvm_agent中,is_active的值預設為uvm_active,在這種模式下,是需要執行個體化driver的。那麼什麼是uvm_passive模式呢?以本章的dut為例,如圖2-5所示,在輸出端口上不需要驅動任何信号,隻需要監測信号。在這種情況下,端口上是隻需要monitor的,是以driver可以不用執行個體化。
在把driver和monitor封裝成agent後,在env中需要執行個體化agent,而不需要直接執行個體化driver和monitor了:
代碼清單 2-36
完成i_agt和o_agt的聲明後,在my_env的build_phase中對它們進行執行個體化後,需要指定各自的工作模式是active模式還是passive模式。現在,整棵uvm樹變為了如
圖2-6所示形式。
由于agent的加入,driver和monitor的層次結構改變了,在top_tb中使用config_db設定virtual my_if時要注意改變路徑:
代碼清單 2-37
隻是uvm中約定俗成的還是在build_phase中完成執行個體化工作。是以,強烈建議僅在build_phase中完成執行個體化。
2.3.5 加入reference model
在2.1節中講述驗證平台的框圖時曾經說過,reference model用于完成和dut相同的功能。reference model的輸出被scoreboard接收,用于和dut的輸出相比較。dut如果很複雜,那麼reference model也會相當複雜。本章的dut很簡單,是以reference model也相當簡單:
代碼清單 2-41
這裡實作了兩個my_transaction的複制。
完成my_model的定義後,需要将其在my_env中執行個體化。其執行個體化方式與agent、driver相似,這裡不具體列出代碼。在加入my_model後,整棵uvm樹變成了如圖2-7所示的形式。
my_model并不複雜,這其中令人感興趣的是my_transaction的傳遞方式。my_model是從i_agt中得到my_transaction,并把my_transaction傳遞給my_scoreboard。在uvm中,通常使用tlm(transaction level modeling)實作component之間transaction級别的通信。
要實作通信,有兩點是值得考慮的:第一,資料是如何發送的?第二,資料是如何接收的?在uvm的transaction級别的通信中,資料的發送有多種方式,其中一種是使用uvm_analysis_port。在my_monitor中定義如下變量:
代碼清單 2-43
write是uvm_analysis_port的一個内建函數。到此,在my_monitor中需要為transaction通信準備的工作已經全部完成。
uvm的transaction級别通信的資料接收方式也有多種,其中一種就是使用uvm_blocking_get_port。這也是一個參數化的類,其參數是要在其中傳遞的transaction的類型。在my_model的第6行中,定義了一個端口,并在build_phase中對其進行執行個體化。在main_phase中,通過port.get任務來得到從i_agt的monitor中發出的transaction。
在my_monitor和my_model中定義并實作了各自的端口之後,通信的功能并沒有實作,還需要在my_env中使用fifo将兩個端口聯系在一起。在my_env中定義一個fifo,并在build_phase中将其執行個體化:
代碼清單 2-46
fifo的類型是uvm_tlm_analysis_fifo,它本身也是一個參數化的類,其參數是存儲在其中的transaction的類型,這裡是my_transaction。
之後,在connect_phase中将fifo分别與my_monitor中的analysis_port和my_model中的blocking_get_port相連:
代碼清單 2-47
這裡引入了connect_phase。與build_phase及main_phase類似,connect_phase也是uvm内建的一個phase,它在build_phase執行完成之後馬上執行。但是與build_phase不同的是,它的執行順序并不是從樹根到樹葉,而是從樹葉到樹根——先執行driver和monitor的connect_phase,再執行agent的connect_phase,最後執行env的connect_phase。
為什麼這裡需要一個fifo呢?不能直接把my_monitor中的analysis_port和my_model中的blocking_get_port相連嗎?由于analysis_port是非阻塞性質的,ap.write函數調用完成後馬上傳回,不會等待資料被接收。假如當write函數調用時,blocking_get_port正在忙于其他事情,而沒有準備好接收新的資料時,此時被write函數寫入的my_transaction就需要一個暫存的位置,這就是fifo。
在如上的連接配接中,用到了i_agt的一個成員變量ap,它的定義與my_monitor中ap的定義完全一樣:
代碼清單 2-48
與my_monitor中的ap不同的是,不需要對my_agent中的ap進行執行個體化,而隻需要在my_agent的connect_phase中将monitor的值賦給它,換句話說,這相當于是一個指向my_monitor的ap的指針:
代碼清單 2-49
根據前面介紹的connect_phase的執行順序,my_agent的connect_phase的執行順序早于my_env的connect_phase的執行順序,進而可以保證執行到i_agt.ap.connect語句時,i_agt.ap不是一個空指針。
2.3.6 加入scoreboard
在驗證平台中加入了reference model和monitor之後,最後一步是加入scoreboard。my_scoreboard的代碼如下:
代碼清單 2-50
my_scoreboard要比較的資料一是來源于reference model,二是來源于o_agt的monitor。前者通過exp_port擷取,而後者通過act_port擷取。在main_phase中通過fork建立起了兩個程序,一個程序處理exp_port的資料,當收到資料後,把資料放入expect_queue中;另外一個程序處理act_port的資料,這是dut的輸出資料,當收集到這些資料後,從expect_queue中彈出之前從exp_port收到的資料,并調用my_transaction的my_compare函數。采用這種比較處理方式的前提是exp_port要比act_port先收到資料。由于dut處理資料需要延時,而reference model是基于進階語言的處理,一般不需要延時,是以可以保證exp_port的資料在act_port的資料之前到來。
act_port和o_agt的ap的連接配接方式及exp_port和reference model的ap的連接配接方式與2.3.5節講述的i_agt的ap和reference model的端口的連接配接方式類似,這裡不再贅述。
代碼清單2-50中的第38行用到了my_compare函數,這是一個在my_transaction中定義的函數,其原型為:
代碼清單 2-51
它逐字段比較兩個my_transaction,并給出最終的比較結果。
完成my_scoreboard的定義後,也需要在my_env中将其執行個體化。此時,整棵uvm樹變為如圖2-8所示的形式。
2.3.7 加入field_automation機制
在2.3.3節中引入my_mointor時,在my_transaction中加入了my_print函數;在2.3.5節中引入reference model時,加入了my_copy函數;在2.3.6節引入scoreboard時,加入了my_compare函數。上述三個函數雖然各自不同,但是對于不同的transaction來說,都是類似的:它們都需要逐字段地對transaction進行某些操作。
那麼有沒有某種簡單的方法,可以通過定義某些規則自動實作這三個函數呢?答案是肯定的。這就是uvm中的field_automation機制,使用uvm_field系列宏實作:
代碼清單 2-52
這裡使用uvm_object_utils_begin和uvm_object_utils_end來實作my_transaction的factory注冊,在這兩個宏中間,使用uvm_field宏注冊所有字段。uvm_field系列宏随着transaction成員變量的不同而不同,如上面的定義中出現了針對bit類型的uvm_field_int及針對byte類型動态數組的uvm_field_array_int。3.3.1節列出了所有的uvm_field系列宏。
當使用上述宏注冊之後,可以直接調用copy、compare、print等函數,而無需自己定義。這極大地簡化了驗證平台的搭建,提高了效率:
代碼清單 2-53
引入field_automation機制的另外一大好處是簡化了driver和monitor。在2.3.1節及2.3.3節中,my_driver的drv_one_pkt任務和my_monitor的collect_one_pkt任務代碼很長,但是幾乎都是一些重複性的代碼。使用field_automation機制後,drv_one_pkt任務可以簡化為:
代碼清單 2-55
這裡使用unpack_bytes函數将data_q中的byte流轉換成tr中的各個字段。unpack_bytes函數的輸入參數必須是一個動态數組,是以需要先把收集到的、放在data_q中的資料複制到一個動态數組中。由于tr中的pload是一個動态數組,是以需要在調用unpack_bytes之前指定其大小,這樣unpack_bytes函數才能正常工作。