interface
interface实现driver的部分功能
在之前所有例子中,interface的定义都非常简单,只是单纯地定义几个logic类型变量而已.
interface my_if(input clk, input rst_n);
logic [7:0] data;
logic valid;
endinterface
但实际上interface能做的事情远不止如此。在interface中可以定义任务与函数。除此之外还可以在interface中使用always语句和initial语句。
在现代高速数据接口中,如USB3.0、1394b、Serial ATA、PCI Express、HDMI、DisplayPort,数据都是以串行的方式传输的。以传输一个8bit的数据为例,出于多种原因的考虑,这些串行传输的数据并不是简单地将这8bit从bit0到bit7轮流发送出去,而是要经过一定的编码,如8b10b编码,这种编码技术将8bit的数据以10bit来表示,从而可以增加数据传输的可靠性。
从8bit到10bit的转换有专门的算法完成。通常来说可以在driver中完成这种转换,并将串行的数据驱动到接口上:
task my_driver::drive_one_pkt(my_transaction tr);
byte unsigned data_q[];
bit[9:0] data10b_q[];
int data_size;
data_size = tr.pack_bytes(data_q) / 8;
data10b_q = new[data_size];
for(int i = 0; i < data_size; i++)
data10b_q[i] = encode_8b10b(data_q[i]);
for ( int i = 0; i < data_size; i++ ) begin
@(posedge vif.p_clk);
for(int j = 0; j < 10; j++) begin
@(posedge vif.s_clk);
vif.sdata <= data10b_q[i][j];
end
end
endtask
上述代码中p_clk为并行的时钟,而s_clk为串行的时钟,后者是前者的10倍频率。
这些事情完全可以在interface中完成。由于8b10b转换的动作适用于任意要驱动的数据,即这是一个“always”的动作,因此可以在interface中使用always语句:
interface my_if(input p_clk, input s_clk, input rst_n);
logic sdata;
logic[7:0] data_8b;
logic[9:0] data_10b;
always@(posedge p_clk) begin
data_10b <= encode_8b10b(data_8b);
end
always@(posedge p_clk) begin
for(int i = 0; i < 10; i++) begin
@(posedge s_clk);
sdata <= data_10b[i];
end
end
endinterface
相应的,数据在driver中可以只驱动到interface的并行接口上即可:
task my_driver::drive_one_pkt(my_transaction tr);
byte unsigned data_q[];
int data_size;
data_size = tr.pack_bytes(data_q) / 8;
for ( int i = 0; i < data_size; i++ ) begin
@(posedge vif.p_clk);
vif.data_8b <= data_q[i];
end
endtask
除了在interface中使用always语句外,类似assign等语句也都可以在interface中使用:
interface my_if(input p_clk, input s_clk, input rst_n);
assign data_10b = (err_8b10b ? data_10b_wrong : data_10b_right);
…
endinterface
在interface中还可以实例化其他interface,如上例由于8b10b转换是一个比较独立的功能,可以将它们放在一个interface中:
interface if_8b10b();
function bit[9:0] encode(bit[7:0] data_8b);
…
endfunction
function bit[7:0] decode(bit[9:0] data_10b);
…
endfunction
endinterface
然后在interface中实例化这个新的interface并调用其中的函数:
interface my_if(input p_clk, input s_clk, input rst_n);
…
if_8b10b encode_if();
always@(posedge p_clk) begin
data_10b <= encode_if.encode(data_8b);
end
…
endinterface
这个新加入的interface与DUT根本没有任何接触,它只是为了提高代码的可重用性,单纯起到一个封装的作用。在项目中可以实例化这个interface用于编码,在其他项目中也可实例化它用于解码。
interface可代替driver做很多事情,但并不能代替driver做所有的事情。interface只适用于做一些低层次的转换,如上述的8b10b转换、曼彻斯特编码等。这些转换动作与transaction完全无关。
使用interface代替driver的第一个好处是可以让driver从底层繁杂的数据处理中解脱出来,更加专注于处理高层数据。第二个好处是有更多数据出现在interface中,这会对调试起到很大的帮助。
前面interface内sdata、data10b、data8b的信号是在波形文件中有记录的,因此可以使用查看波形的软件查看其中的信号。如果8b10b编码的工作是在driver中完成的,即interface中只有data10b或sdata,那么最后的波形文件中一般不会有data8b的信息(除非根据仿真工具做某些特殊复杂的设置,否则driver中的变量很难记录在波形文件中),这会增加调试的难度。这种调试既包括对RTL的调试,也包括driver的调试。
不过,当使用interface完成这些转换后,如果想构造这些转换异常的测试用例则稍显麻烦。如构造8b10b转换的错误,需要在interface中加入标志位err_8b10b,根据标志位的数据决定向数据线上发送何种数据。
而如果这种转换是在driver完成的,有两种选择,一是在正常的driver中加入异常driver的处理代码;二是重新编写一个全新的异常driver,将原来的driver使用factory机制重载掉。
无论是哪种方式都能实现其目的。相比来说,在interface上实现转换能够更有助于调试,这一优势完全可以弥补其劣势。
可变时钟
有时在验证平台中需要频率变化的时钟。可变时钟有三种:
- 在不同测试用例之间时钟频率不同,但在同一测试用例中保持不变。如HDMI协议中,其图像的时钟信号就根据发送(接收)图像的分辨率的变化而变化。当不同测试用例测试不同分辨率的图像时,就需要在不同测试用例中设置不同时钟频率。
- 在同一个测试用例中存在时钟频率变换的情况。芯片上的时钟是由PLL产生的。但PLL并不是一开始就会产生稳定的时钟,而是会有一段过渡期,在过渡期内其时钟频率是一直变化的。有时不关心这段过渡期时,而只关心过渡期前和过渡期后的时钟频率。
- 可变时钟和第二种很像,但它既关心过渡期前后的时钟也关心PLL在过渡期的行为。为了模仿这段过渡期内频率对芯片的影响,就需要一个可变时钟模型。此外,正常工作时理论上PLL会输出稳定的时钟,但实际使用中PLL的时钟频率总是在某个范围内以某种方式(如正弦)变化,如设置为27M的时钟可能在26.9M~27.1M变换。为了模仿这种变化也需要一个可变时钟模型。
在通常的验证平台中时钟都是在top_tb中实现:
initial begin
clk = 0;
forever begin
#100 clk = ~clk;
end
end
这种时钟都是固定的。在传统的实现方式中,如果要实现第一种可变时钟,可以将上述模块独立成一个文件:
`ifndef TEST_CLK
`define TEST_CLK
initial begin
clk = 0;
forever begin
#100 clk = ~clk;
end
end
`endif
然后将上述文件通过inlude的方式包含在top_tb中:
module top_tb();
…
`include "test_clk.sv"
…
endmodule
当需要可变时钟时只需重新编写一个test_clk.v文件即可。这种方式是Verilog搭建的验证平台中经常用到的做法。
要实现第一种可变的时钟,也可使用config_db在测试用例中设置时钟周期:
function void my_case0::build_phase(uvm_phase phase);
…
uvm_config_db#(real)::set(this, "", "clk_half_period", 200.0);
endfunction
在top_tb中使用config_db::get得到设置的周期:
initial begin
static real clk_half_period = 100.0;
clk = 0;
#1;
if(uvm_config_db#(real)::get(uvm_root::get(), "uvm_test_top",
"clk_half_period", clk_half_period)
`uvm_info("top_tb", $sformatf("clk_half_period is %0f",
clk_half_per iod), UVM_MEDIUM)
forever begin
#(clk_half_period*1.0ns) clk = ~clk;
end
end
在这种设置的方式中是非直线的获取。my_case0中的config_db::set看起来比较奇怪:这是一个设置给自己的参数。但真正使用这个参数是在top_tb中而不是在my_case0中。由于config_db::set在0时刻执行,如果config_db::get也在0时刻执行,则可能无法得到设置的数值,所以在top_tb中,在config_db::get前有1个时间单位的延迟。这种生成可变时钟的方式只适用于第一种可变时钟。
对于第二种可变时钟,可使用如下方式:
initial begin
static real clk_half_period = 100.0;
clk = 0;
fork
forever begin
uvm_config_db#(real)::wait_modified(uvm_root::get(),
"uvm_test_top", "clk_half_period");
void'(uvm_config_db#(real)::get(uvm_root::get(),
"uvm_test_top", " clk_half_period", clk_half_period));
`uvm_info("top_tb", $sformatf("clk_half_period is %0f",
clk_half_p eriod), UVM_MEDIUM)
end
forever begin
#(clk_half_period*1.0ns) clk = ~clk;
end
join
end
在测试用例中可以随着时间的变换而设置不同的时钟:
task my_case0::main_phase(uvm_phase phase);
#100000;
uvm_config_db#(real)::set(this, "", "clk_half_period", 200.0);
#100000;
uvm_config_db#(real)::set(this, "", "clk_half_period", 150.0);
endtask
但使用这种config_db的方式很难实现第三种可变时钟。要实现第三种时钟,可专门编写一个时钟接口:
interface clk_if();
logic clk;
endinterface
在top_tb中实例化这个接口,并在需要时钟的地方以如下的方式引用:
clk_if cif();
…
dut my_dut(.clk(cif.clk),
.rst_n(rst_n),
…
为可变时钟从uvm_component派生一个类:
class clk_model extends uvm_component;
`uvm_component_utils(clk_model)
virtual clk_if vif;
real half_period = 100.0;
…
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if(!uvm_config_db#(virtual clk_if)::get(this, "", "vif", vif))
`uvm_fatal("clk_model", "must set interface for vif")
void'(uvm_config_db#(real)::get(this, "", "half_period", half_period));
`uvm_info("clk_model",$sformatf("clk_half_period is %0f",half_period),UVM_MEDIUM)
endfunction
virtual task run_phase(uvm_phase phase);
vif.clk = 0;
forever begin
#(half_period*1.0ns) vif.clk = ~vif.clk;
end
endtask
endclass
在env中实例化此类:
class my_env extends uvm_env;
…
clk_model clk_sys;
…
virtual function void build_phase(uvm_phase phase);
…
clk_sys = clk_model::type_id::create("clk_sys", this);
…
endfunction
…
endclass
在这种使用方式中时钟接口被封装在了一个component中。在需要新时钟模型时只需从clk_model派生一个新的类,然后在新的类中实现时钟模型。使用factory机制的重载功能将clk_model用新的类重载掉。通过这种方式,可以将时钟设置为任意想要的行为。