UVM实战 卷I学习笔记13——UVM高级应用(1)


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上实现转换能够更有助于调试,这一优势完全可以弥补其劣势。

可变时钟

有时在验证平台中需要频率变化的时钟。可变时钟有三种:

  1. 在不同测试用例之间时钟频率不同,但在同一测试用例中保持不变。如HDMI协议中,其图像的时钟信号就根据发送(接收)图像的分辨率的变化而变化。当不同测试用例测试不同分辨率的图像时,就需要在不同测试用例中设置不同时钟频率。
  2. 在同一个测试用例中存在时钟频率变换的情况。芯片上的时钟是由PLL产生的。但PLL并不是一开始就会产生稳定的时钟,而是会有一段过渡期,在过渡期内其时钟频率是一直变化的。有时不关心这段过渡期时,而只关心过渡期前和过渡期后的时钟频率。
  3. 可变时钟和第二种很像,但它既关心过渡期前后的时钟也关心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用新的类重载掉。通过这种方式,可以将时钟设置为任意想要的行为。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值