寫過 verilog 硬件代碼的同學應該都知道 DUT 會包含很多寄存器,它們是模塊間交互的接口,其用途大致可以分為兩類:
a. 通過讀出寄存器當前的值獲取 DUT 當前的狀態,該類寄存器稱為狀態寄存器;
b. 通過對寄存器進行配置,可以使得 DUT 工作在一定模式下,該類寄存器稱為配置寄存器。
在驗證過程中,寄存器的驗證是最新開始的,只有保證寄存器的配置正確,才能使得硬件之間的“交互”正確。在驗證寄存器配置是否正確的過程中,我們需要頻繁的對 DUT 內部的寄存器進行讀寫操作,如 reference model 需要獲取指定 reg 的參數值,在驗證平台中我們獲取 DUT 內部寄存器的值的方式主要有兩種:
前門訪問(FRONTDOOR):啟動 sequence 產生待操作寄存器的讀寫控制和地址,在 driver 中通過總線(Bus)驅動至 DUT,並在 monitor 中采集 Bus 輸出數據,該方式需要消耗仿真時間 ;
后門訪問(BACKDOOR):在仿真環境中通過 DUT 實例名進行點操作,直接訪問 DUT 內部的寄存器,該方式的缺點是,點操作需要通過絕對路徑操作,如果寄存器數量龐大,會導致驗證環境臃腫繁雜,容易出錯。
因為上述操作的不利因素,才導致寄存器模型 ( RAL Model ) 產生。
1. 什么是寄存器模型
RAL Model 對應於 DUT 中的寄存器,RAL Model 中有 DUT 每一個 寄存器的副本,它是真實硬件寄存器在軟件環境中的行為模型;硬件寄存器的一個或多個 bit 的拼接稱為一個域 ( field );多一個 field 形成一個 reg;多個 reg 構成一個塊 ( block )。uvm library 已經為我們定義好了上述幾個概念,我們在使用時只需繼承即可。
uvm_reg_field:這是寄存器模型中的最小單位。
uvm_reg:它比 uvm_reg_field 高一個級別,但是依然是比較小的單位。一個寄存器中至少包含一個 uvm_reg_field。
uvm_reg_block:它是一個比較大的單位,在其中可以加入許多的 uvm_reg,也可以加入其他的 uvm_reg_block。一個寄存器模型中至少包含一個 uvm_reg_block。
uvm_reg_map:每個寄存器在加入寄存器模型時都有其地址,uvm_reg_map 就是存儲這些地址,並將其轉換成可以訪問的物理地址(因為加入寄存器模型中的寄存器地址一般都是偏移地址,而不是絕對地址)。當寄存器模型使用前門訪問方式來實現讀或寫操作時,uvm_reg_map 就會將地址轉換成絕對地址,啟動一個讀或寫的 sequence,並將讀或寫的結果返回。在每個 reg_block 內部,至少有一個(通常也只有一個)uvm_reg_map。
如下圖所示,RAL Model 中包含 MEM 和 block,它們分別用於對 DUT 中的寄存器和 memory 進行建模,其行為和硬件行為保持一致(其實是盡可能保持一致),ADDR MAP 用於實現訪問寄存器的相對地址和絕對地址的轉換。

寄存器模型注意有以下優勢:
a.方便對 DUT 中寄存器進行讀寫;
b.在軟件仿真時,可以不耗時的獲取寄存器的值(直接從 RAL Model 中獲取);
c.可以很方便的正對寄存器的 coverage 驗證點的收集。
如果有了寄存器模型,那么寄存器訪問過程就可以簡化為:
RAL Model
task my_model::main_phase(uvm_phase phase);
…
reg_model.version.read(status, value, UVM_FRONTDOOR);
reg_model.version.write(status, value, UVM_FRONTDOOR);
…
endtask
只要一條語句就可以實現上述復雜的過程。像啟動 sequence 及將讀取結果返回這些事情,都會由寄存器模型來自動完成。在沒有寄存器模型之前,只能啟動 sequence 通過前門(FRONTDOOR)訪問的方式來讀取寄存器,局限較大,在 scoreboard(或者其他 component )中難以控制。而有了寄存器模型之后,scoreboard 只與寄存器模型打交道,無論是發送讀的指令還是獲取讀操作的返回值,都可以由寄存器模型完成。有了寄存器模型后,可以在任何耗費時間的phase中使用寄存器模型以前門訪問或后門(BACKDOOR)訪問的方式來讀取寄存器的值,同時還能在某些不耗費時間的 phase(如 check_phase)中使用后門訪問的方式來讀取寄存器的值。
2. 寄存器模型實例
假設有如下的 DUT:
DUT
module dut (clk, data, addr, we_n, cs);
input clk;
inout[15:0] data;
input[15:0] addr;
input we_n;
input cs;
reg [16:0] version;
initial begin
version <= 16'h0000;
end
endmodule
這個 DUT 比較的簡單,它只有一個寄存器 version
, 我們依據上述基礎理論為其建造 RAL model。首先要從 uvm_reg 派生一個通用的寄存器類:
reg uvm_reg
class my_reg extends uvm_reg;
rand uvm_reg_field data;
`uvm_object_utils(my_reg)
virtual function void build();
data = uvm_reg_field::type_id::create("data");
//parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, individually accessible
data.configure(this, 16, 0, "RW", 1, 0, 1, 1, 0);
endfunction
function new(input string name="my_reg");
//parameter: name, size, has_coverage
super.new(name, 16, UVM_NO_COVERAGE);
endfunction
endclass
data.configure 的 9 個parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, individually accessible
參數一,是此域的父輩,也就是此域位於哪個寄存器中,即是 this;
參數二,是此域的寬度;
參數三,是此域的最低位在整個寄存器的位置,從0開始計數;
參數四,表示此字段的存取方式;
參數五,表示是否是易失的(volatile),這個參數一般不會使用;
參數六,表示此域上電復位后的默認值;
參數七,表示此域時都有復位;
參數八,表示這個域是否可以隨機化;
參數九,表示這個域是否可以單獨存取。
定義好了此通用寄存器后,我們需要在一個由 reg_block 派生的類中把其實例化:
reg uvm_reg_block
class my_regmodel extends uvm_reg_block;
rand my_reg version;
function void build();
default_map = create_map("default_map", 0, 2, UVM_LITTLE_ENDIAN, 0);
version = my_reg::type_id::create("version", , get_full_name());
version.configure(this, null, "version");
version.build();
default_map.add_reg(version, 16'h47, "RW");
endfunction
`uvm_object_utils(my_regmodel)
function new(input string name="my_regmodel");
super.new(name, UVM_NO_COVERAGE);
endfunction
endclass
reg block中也有 build 函數,在其中要做如下事情:
- 調用
create_map
函數完成default_map
的實例化,default_map = create_map(“default_map”, 0, 2, UVM_LITTLE_ENDIAN, 0)
;
第一個參數,表示名字;
第二個參數,表示該 reg block 的基地址;
第三個參數,表示寄存器所映射到的總線的寬度(單位是 byte,不是 bit );
第四個參數,表示大小端模式;
第五個參數,表示該寄存器能否按 byte 尋址。
- 完成每個寄存器的
build
及configure
操作,uvm_reg
的configure
函數原型:function void configure ( uvm_reg_block blk_parent, uvm_reg_file regfile_parent = null, string hdl_path = "" )
;
第一個參數,表示所在 reg block 的指針;
第二個參數,表示 reg_file 指針;
第三個參數,表示寄存器后面訪問路徑 - string 類型。
- 把每個寄存器加入到
default_map
中,uvm_reg_map
存有各個寄存器的地址信息,default_map.add_reg (version, 16'h47, "RW")
;
第一個參數,表示要添加的寄存器名;
第二個參數,表示地址;
第三個參數,表示寄存器的讀寫屬性。
到此為止,一個簡單的 register model 已經完成。
3. 寄存器模型應用
3.1寄存器模型讀寫操作
a.讀操作
reg read
extern virtual task read(output uvm_status_e status,
output uvm_reg_data_t value,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0
);
b.寫操作
reg read
extern virtual task write(output uvm_status_e status,
input uvm_reg_data_t value,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0
);
read/write
有多個參數,常用的是其前三個參數:
- 輸出型,
uvm_status_e
型的變量,表示讀/寫操作是否成功; - 輸出/入型, 讀/寫取的數值;
- 讀/寫取的方式,可選 UVM_FRONTDOOR 和 UVM_BACKDOOR。

register model 的 FRONTDOOR 方式工作流程如上圖所示,其中左圖為讀操作,右圖為寫操作。無論是讀或寫,RAL model 在調用 read/write
函數時都會通過啟動一個 sequence
(用戶不可見) 產生一個 uvm_reg_bus_op
的變量,此變量中存儲着操作類型(讀還是寫),操作的地址,如果是寫操作,還會有要寫入的數據。此變量中的信息要經過一個轉換器( adapter
)轉換之后,交給 bus_sequencer,之后 bus_sequencer 交給 bus_driver,bus_driver 實現最終的 FRONTDOOR 讀寫操作。因此,必須要定義好一個轉換器。
adapter
class adapter extends uvm_reg_adapter;
string tID = get_type_name();
`uvm_object_utils(my_adapter)
function new(string name="my_adapter");
super.new(name);
endfunction : new
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
bus_transaction tr;
tr = new("tr");
tr.addr = rw.addr;
tr.bus_op = (rw.kind == UVM_READ) BUS_RD: BUS_WR;
if (tr.bus_op == BUS_WR)
tr.wr_data = rw.data;
return tr;
endfunction : reg2bus
function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
bus_transaction tr;
if(!$cast(tr, bus_item)) begin
`uvm_fatal(tID,"Provided bus_item is not of the correct type. Expecting bus_trans action")
return;
end
rw.kind = (tr.bus_op == BUS_RD) UVM_READ : UVM_WRITE;
rw.addr = tr.addr;
rw.byte_en = 'h3;
rw.data = (tr.bus_op == BUS_RD) tr.rd_data : tr.wr_data;
rw.status = UVM_IS_OK;
endfunction : bus2reg
endclass : adapter
一個轉換器要定義好兩個函數,一是 reg2bus,其作用為將寄存器模型通過 sequence 發出的 uvm_reg_bus_op
型的變量轉換成 bus_sequencer
能夠接受的形式,二是 bus2reg,其作用為當監測到總線上有操作時,它將收集來的 transaction 轉換成寄存器模型能夠接受的形式,以便寄存器模型能夠更新相應的寄存器的值。
說到這里,不得不考慮寄存器模型發起的讀操作的數值是如何返回給寄存器模型的?由於總線的特殊性,bus_driver
在驅動總線進行讀操作時,它也能順便獲取要讀的數值,如果它將此值放入從 bus_sequencer
獲得的 bus_transaction
中時,那么bus_transaction
中就會有讀取的值,此值經過 adapter 的 bus2reg 函數的傳遞,最終被寄存器模型獲取,這個過程如上圖讀操作虛線所示。
寄存器模型的讀/寫過程類似,現在以讀操作為例,其完成流程為:
a.參考模型調用寄存器模型的讀任務。
b.寄存器模型產生 sequence,並產生 uvm_reg_item:rw。
c.產生 driver 能夠接受的transaction:bus_req=adapter.reg2bus(rw)。
d.把 bus_req 交給bus_sequencer。
e.driver 得到 bus_req 后驅動它,得到讀取的值,並將讀取值放入 bus_req 中,調用 item_done。
f.寄存器模型調用 adapter.bus2reg(bus_req,rw)將 bus_req 中的讀取值傳遞給 rw。
g.將 rw 中的讀數據返回參考模型。
3.2 寄存器模型例化
一般在 env 或者 test 中加入 RALmodel :
base_test
class base_test extends uvm_test;
my_env env;
my_vsqr v_sqr;
reg_model rm;
adapter reg_sqr_adapter;
…
endclass
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
v_sqr = my_vsqr::type_id::create("v_sqr", this);
rm = reg_model::type_id::create("rm", this);
rm.configure(null, "");
rm.build();
rm.lock_model();
rm.reset();
rm.set_hdl_path_root("top_tb.my_dut");
reg_sqr_adapter = new("reg_sqr_adapter");
env.p_rm = this.rm;
endfunction
function void base_test::connect_phase(uvm_phase phase);
super.connect_phase(phase);
v_sqr.p_my_sqr = env.i_agt.sqr;
v_sqr.p_bus_sqr = env.bus_agt.sqr;
v_sqr.p_rm = this.rm;
rm.default_map.set_sequencer(env.bus_agt.sqr, reg_sqr_adapter);
rm.default_map.set_auto_predict(1);
endfunction
要將一個寄存器模型集成到 base_test 中,那么至少需要在 base_test 中定義兩個成員變量,一是 reg_model,另外一個就是 adapter 。將所有用到的類在 build_phase 中實例化。在實例化后 reg_model 還要做四件事:
第一是調用 configure 函數,其第一個參數是parent block,由於是最頂層的reg_block,因此填寫null,第二個參數是后門訪問路徑,這里傳入一個空的字符串。
第二是調用 build 函數,將所有的寄存器實例化。
第三是調用 lock_model 函數,調用此函數后,reg_model 中就不能再加入新的寄存器了。
第四是調用 reset 函數,如果不調用此函數,那么reg_model中所有寄存器的值都是0,調用此函數后,所有寄存器的值都將變為設置的復位值。
寄存器模型的前門訪問操作最終都將由 uvm_reg_map 完成,因此在 connect_phase 中,需要將轉換器和 bus_sequencer 通過 set_sequencer 函數告知 reg_model 的 default_map,並將 default_map 設置為自動預測狀態。
3.3 寄存器模型應用
寄存器模型定義好后,可以在 reference model 和 sequence,因為 uvm_component 和 uvm_object 的區別,其在 uvm_component 中的應用和在 uvm_object 類中存在些微差異。
- refencer model 場景使用
reference model 為 component 類型,為了使用在 base_test 中例化的 reg model ,需要在參考模型中定義一個寄存器模型的指針,並在env 的 connect_phase 中實現指針賦值:
base_test
class my_model extends uvm_component;
…
reg_model p_rm;
…
endclass
function void my_env::connect_phase(uvm_phase phase);
…
mdl.p_rm = this.p_rm;
endfunction
read/write實例如下:
my_model 應用
task my_model::main_phase(uvm_phase phase);
my_transaction tr;
my_transaction new_tr;
uvm_status_e status;
uvm_reg_data_t value;
super.main_phase(phase);
p_rm.version.read(status, value, UVM_FRONTDOOR);
while(1) begin
port.get(tr);
new_tr = new("new_tr");
new_tr.copy(tr);
//`uvm_info("my_model", "get one transaction, copy and print it:", UV M_LOW)
//new_tr.print();
if(value)
invert_tr(new_tr);
ap.write(new_tr);
end
endtask
- sequence 場景使用
reg model 的使用,關鍵在於獲取 reg model 的實例,在 sequence 為了獲取一個指向 reg model 的指針,我們可以借助於 sequencer。實例如下:
my_model 應用
class case0_cfg_vseq extends uvm_sequence;
…
virtual task body();
…
p_sequencer.p_rm.version.read(status, value, UVM_FRONTDOOR);
…
p_sequencer.p_rm.version.write(status, value, UVM_FRONTDOOR);
…
endtask
endclass