路科验证的个人空间 https://blog.eetop.cn/1561828 [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

SV组件实现篇之一:激励器的驱动

已有 4217 次阅读| 2016-12-25 23:36 |个人分类:验证系统思想|系统分类:芯片设计

从这一章开始,我们将进入验证各个组件的实现部分。对于各个组件介绍的顺序也将遵循于《验证的结构篇》,从激励器、到监测器再到比较器,而牵系到的知识点也会以各个验证师实现的方式来介绍,这么做的目的是为了告诉读者SV的特性出色在什么地方,以及如何思考从0到1的过程。《SV验证组件的实现篇》旨在于通过对比不同的实现方式,做出优劣或者应用场景的判别,从而为读者梳理SV语言的各种特性。

在组件实现的讨论中,我们会主要基于SV基本特性,深入浅出来介绍这些特性的应用。例如,对于类的介绍,我们不会陷入类本身的特种语法参考,而是比较实用类来封装组件同模块来实现组件的异同和优劣,通过这种思考来从根本上认识类。又比如,对于组件之间的通信,我们会介绍进程间主要通信的方式,这些通信特性的方法不在本书的重点,而这些通信的具体应用场景则会更细致的阐明。

通过对SV各种语言特性的思辨性探讨,我们为大家展开的不是一间“武器库”,而是一架“钢琴”。因为作者相信任何一门语言的特性不是与生俱来的,而是为了实现各种目的最终和谐奏鸣的,所以我们期待读者们最终可以谈好这架钢琴,而不是只从武器库里挑选一门新式的大炮用来打一只苍蝇。



stimulator(激励器)作为生成激励的源,究竟应该以一种什么方式来运作?试想一下,之前在Verilog又或者VHDL语言中的testbench,也可以产生出合适的激励,那么为什么另辟蹊径单独细说stimulator的实现呢?为了解释这个问题,我们首先看看老式stimulator的不足:
  • 如果激励接口复杂,那么各项激励之间的协调也较为困难,而激励本身也可能于设计时钟输入采样存在竞争的问题
  • 激励时序难以调整,因为测试方式是平叙的方式写在进程中的。
  • 激励向量固定,因为是直接测试方式。

而我们从这一节开始则分别要解决上述问题,即通过合理的驱动、数据和功能的封装和激励自身的随机化来解决老式stimulator的不足。

在之前我们通过介绍interface来认识利用它来做验证组件与DUT之间的联系,而连接好以后,激励也会通过接口向DUT传递。同时给出符合接口协议的激励,我们也需要遵循接口的时序关系。对于verifier董 来说,他是这么安排他的stimulator和interface的。
在上一篇中verifier董 准备利用上面接口与对应的stimulator连接,我们来看看他是如何实现stimulator stm_ini以及同interface ini_if在testbench中的连线。

激励驱动的方法
与regs_ini_if相连接的stimulator ini_stim,verifier董将其声明为module。

module stm_ini(
input clk,
input rstn,
output [ 1:0] cmd,
output [ 7:0] cmd_addr,
output [31:0] cmd_data_w,
input [31:0] cmd_data_r
);

localparam IDLE = 2'b00;
localparam RD = 2'b01;
localparam WR = 2'b10;

logic [ 1:0] v_cmd;
logic [ 7:0] v_cmd_addr;
logic [31:0] v_cmd_data_w;

assign cmd = v_cmd;
assign cmd_addr = v_cmd_addr;
assign cmd_data_w = v_cmd_data_w;


typedef struct{ // trans 数据类型定义
bit [ 1:0] cmd;
bit [ 7:0] cmd_addr;
bit [31:0] cmd_data_w;
bit [31:0] cmd_data_r;
} trans;

trans ts[3]; // trans固定数组声明和初始化

task op_wr(trans t); // 写指令定义
...
task op_rd(trans t); // 读指令定义
...
task op_idle(); // 空闲指令定义
...
task op_parse(trans t); // 指令类型解析

... // 指令分发即产生激励

endmodule

由于stm_ini在内部声明了多个方法(methods),即op_wr、op_rd、op_idle和op_parse,且它们会对硬件信号做驱动。在深入这些方法之前,需要先声明几个变量v_cmd、v_cmd_addr、v_cmd_data_w,这是因为方法内部的非阻塞赋值只能引用logic类型或者reg类型,而无法直接对stm_ini的端口(wire类型)做赋值

为了使得激励上的数据发送更加清晰,我们又对总线数据类型trans_t做了定义:
typedef struct{
bit [ 1:0] cmd;
bit [ 7:0] cmd_addr;
bit [31:0] cmd_data_w;
bit [31:0] cmd_data_r;
} trans;

这么做的好处在于将每次发送激励的数据集结为一个数据体,无论从数据发送还是后面monitor的数据采集都有帮助。在对新的数据类型定义之后,我们需要声明一个数组并且为其做初始化。

trans ts[3];

initial begin
ts[0].cmd = WR;
ts[0].cmd_addr = 0;
ts[0].cmd_data_w = 32'hFFFF_FFFF;
ts[1].cmd = RD;
ts[1].cmd_addr = 0;
ts[2].cmd = IDLE;
end

上面声明了一个数组ts,其有三个成员,接下来的initial块将为其在仿真开始进行初始化。由于ts[0]是写操作,所以需要为其中三个成员赋初值,而ts[1]是读操作,只需要为两个成员赋初值,作为第三个成员ts[2]因为是空闲操作,因为只需要为其成员cmd赋初值。

在这里的操作命令,我们将其声明为了局部参数(localparam),如我们之前介绍过的,我们还可以通过宏定义(macro)、枚举(enum)或者常量(const)来做类似的声明

接下来我们对各个对应的激励方法做出解释。

首先方法op_wr有一个参数trans t,如果没有标明传递方向,默认是为输入端(input trans t)。该方法从名字可以读出是为写命令的操作,即在时钟的上升沿会将变量t中的t.cmd、t.cmd_addr和t.cmd_data_w分别写入只硬件信号中,最终来触发一次写操作。
task op_wr(trans t);
@(posedge clk);
v_cmd <= t.cmd;
v_cmd_addr <= t.cmd_addr;
v_cmd_data_w <= t.cmd_data_w;
endtask

读方法也是在时钟上升沿将t中的t.cmd和t.cmd_addr写入至硬件信号中。
task op_rd(trans t);
@(posedge clk);
v_cmd <= t.cmd;
v_cmd_addr <= t.cmd_addr;
endtask

空闲操作则无需参数,只需要将v_cmd置为IDLE,其余信号赋值为0。
task op_idle();
@(posedge clk);
v_cmd <= IDLE;
v_cmd_addr <= 0;
v_cmd_data_w <= 0;
endtask

上面的三种方法分别对应着写操作、读操作与空闲操作,那么在调用这些方法之前,我们需要对之前声明了的数组ts内每个成员类型做出解析,根据其成员的操作类型来判断应该调用的操作方法。这里,需要再声明一个方法op_parse,其根据参数的命令类型来判断应该最终调用哪种指令操作方法,如果是无效指令则会通过系统函数$error(MSG)来报告错误。
task op_parse(trans t);
case(t.cmd)
WR: op_wr(t);
RD: op_rd(t);
IDLE: op_idle();
default: $error("Invalid CMD!");
endcase
endtask

通过上面定义的方法,verifier董就可以对其定义的trans数组ts做出逐个解析,并且最终将数据转换为硬件信号加以驱动。在stm_ini最后又声明了一个initial块来产生最终的激励

initial begin: stmgen
@(posedge rstn); // 等待复位释放
foreach(ts[i]) begin // 解析ts数组中每个成员,从ts[0]至ts[2]
op_parse(ts[i]); // 调用解析方法
end
repeat(5) @(posedge clk); // 保证激励发送完毕且DUT做出反馈
$finish(); // 主动结束仿真
end

任务和函数
在上面stm_ini中定义了若干种方法:op_wr、op_rd、op_idle和op_parse,他们在定义时均声明为了task(任务),而不是function(函数)。相比较软件编程语言定义的方法均为函数类型,而非任务类型,我们有必要比较这两种方法类型的异同,使得用户清楚在何种场合定义和调用。

task与function的参数列表中均可以声明多个input(输入)、output(输出)、inout(输入输出)和ref(引用)类型。input即从外部拷贝传入的形式参数,output即由被调用方法产生拷贝传递给外部的形式参数,inout则表示分别在进入方法和退出方法是分别被拷贝了两次,而ref则类似于软件中的指针,在调用方式时不会有任何的拷贝行为,而是直接引用或者修改外部传入的数据对象。

inout和ref类型均可以使得形式参数在方法中内部被调用,并且将结果输出给外部。它们之间的不同是,inout只有在方法结束之后才会传递到外部,而ref可以在方法执行过程中直接修改数据对象无需等待方法执行结束。

在SV中,为了使用数据对象“指针”的便捷,并不会有像C语言中指针使用“*”寻址的方法。像下面的例子中,声明了拷贝函数op_copy,有两个参数,函数将把s中的数据直接拷贝给t。在这里,t被声明为ref类型,在实际使用中使用t = s则表明引用的数据被直接赋值,而用户不需要为何时该使用像C语言中的“*”来寻址,op_copy因此可以直接对外部传递进来的参数t本身操作。

这里需要注意的是,ref一般是对非句柄(类指针)类型的数据对象使用,而在后面介绍介绍了类和句柄之后,方法调用时则只需要传递句柄,无需ref类型声明

function automatic void op_copy(ref trans t, input trans s);
t = s;
endfunction

initial begin
trans s;
trans t;
s.cmd = WR;
s.cmd_addr = 'h10;
s.cmd_data_w = 'h_3F;
op_copy(t, s);
$display(" t.cmd='h%0x \n t.cmd_addr='h%0x \n t.cmd_data_w='h%0x",
t.cmd, t.cmd_addr, t.cmd_data_w);
end

输出结果:
# t.cmd='h2
# t.cmd_addr='h10
# t.cmd_data_w='h3f


除此之外,function还有以下的属性:
  • 默认的数据类型是为logic,例如 input [7:0] addr。
  • 数组可以作为形式参数传递
  • function可以返回或者不返回结果,如果返回即需要用关键词return,如果不返回则应该在声明function时采用void function()
  • 只有数据变量可以在形式参数列表中被声明为ref类型,而线网类型则不能被声明为ref类型,这是为了防止通过ref的方式来修改线网信号值,也可以理解为可以通过输入形式参数采样线网数值,而不能通过ref来直接修改线网数值。具体修改线网信号的方法我们会在接下来stimulator通过interface输出激励的方式来介绍。
  • 在使用ref时,有时候为了保护参数使得只被读取不被写入时,可以通过const的方式来限定ref定义的参数
  • 在声明参数时,可以给入默认数值,例如 input [7:0] addr = 0,同时在调用时如果省略该参数的传递,那么默认值即会被传递给function。

与function相比,task有以下几点不同
  • task无法通过return返回结果,因此只能通过output、inout或者ref的参数来返回。
  • task内可以置入耗时语句,而function则不能。常见的耗时语句包括@event、wait event、# delay等。

通过上面的比较,我们对function和task建议的使用方式是:
  • 对于初学者,傻瓜式用法即全部采用task来定义方法,因为它可以内置常用到的耗时语句。
  • 对于有经验的使用者,请今后对这两种方法类型加以区别,在非耗时方法定义时使用function,在内置耗时语句时使用task,这么做的好处是在遇到了这两种方法定义时,就可以知道function可能运用于纯粹的数字或者逻辑运算,而task则会被运用于需要耗时的信号采样或者驱动场景。
  • 如果要调用function,则function和task均可在内部调用;而如果要调用task,则我们建议使用外部的task来调用,这是因为如果被调用的task内置有耗时语句,则外部调用它的方法类型必须为task。


数据生命周期
在SV中,我们将数据的声明周期分为两类:
  • automatic(动态)
  • static(静态)

如果数据变量被声明为automatic,那么在进入该进程/方法之后,automatic变量会被创建,而在离开该进程/方法之后,automatic变量会被销毁。这同C语言的变量及其作用域的使用说明是一致的。而static变量在仿真开始时即会被创建,而在进程/方法执行过程中,自身不会被销毁,且可以被多个进程/方法所共享。

所以,对于automatic与static两种生命周期的数据类型,最直观的区别就是static在仿真过程中的任何时刻都可以被共享,且不会被销毁,直到仿真结束;而automatic变量则同软件的局部变量一样,在它的作用域生命结束时,它也会被销毁回收存储空间

我们再来看下面这个例子:

function automatic int auto_cnt(input a);
int cnt = 0;
cnt += a;
return cnt;
endfunction

function static int static_cnt(input a);
static int cnt = 0;
cnt += a;
return cnt;
endfunction

function int def_cnt(input a);
static int cnt = 0;
cnt += a;
return cnt;
endfunction

initial begin
$display("@1 auto_cnt = %0d", auto_cnt(1));
$display("@2 auto_cnt = %0d", auto_cnt(1));
$display("@1 static_cnt = %0d", static_cnt(1));
$display("@2 static_cnt = %0d", static_cnt(1));
$display("@1 def_cnt = %0d", def_cnt(1));
$display("@2 def_cnt = %0d", def_cnt(1));
end

输出结果:
# @1 auto_cnt = 1
# @2 auto_cnt = 1
# @1 static_cnt = 1
# @2 static_cnt = 2
# @1 def_cnt = 1
# @2 def_cnt = 2

上面的三个function被定义在了module内,分别被声明为了automatic、static和默认类型。

对于automatic方法,其内部的所有变量默认也是automatic,即伴随automatic方法的声明周期建立和销毁。对于static方法,其内部的所有变量默认也是static类型。对于automatic或者static方法,用户可以对其内部定义的变量做单个声明,使其类型被显式声明为automatic或者static。

对于static变量,用户在声明变量时应该同时对其做初始化,而初始化只会伴随它的生命周期发生一次,并不会随着方法调用被多次初始化。

而上面的的第三种方法def_cnt的默认类型是static,这是因为SV规定:
  • 在module、program、interface、task和function之外声明的变量拥有静态的生命周期,即存在于整个仿真阶段,这同C定义的静态变量一直。
  • 在module、interface和program内部声明,且在task、process或者function外部声明的变量也是static变量,且作用域在该块中。
  • 在module、program和interface中定义的task、function默认都是static类型
  • 在过程块中(task、function、process)定义的变量均跟随它的作用域,即过程块的类型,如果过程块为static,则它们也默认为static,反之亦然。这些变量也可以由用户显式声明为automatic或者static。
  • 为了使得在过程块中声明的变量有统一默认的生命周期,可以在定义module、interface、package或者program时,通过限定词automatic或者static来区分。对于上述程序块默认的生命周期类型为static。

例如下面的代码通过显式声明test,来改变其内部过程块task t的形式参数和内部变量类型为automatic。

program automatic test ;
int i; // 不在过程块中,因此仍然为static类型
task t ( int a ); // t的参数和变量默认类型为automatic
...
endtask
endprogram

通过接口驱动
激励驱动的方法中我们可以看到,激励可以通过module内置的方法来生产,最终通过module端口输出给外部,而在更高层的TB中则依赖于interface来连接DUT与stimulator。这是一种常见的将stimualtor与DUT连接的方法,此外,我们也可以通过virtual interface(虚接口)在stimulator内部直接做采样或者驱动。

我们将之前stimulator stm_ini加以修改,使用virtual interface来实现激励的驱动:

typedef enum bit [1:0] {IDLE=2'b00, RD=2'b01, WR=2'b10} cmd_t;

module stm_ini;

virtual interface regs_ini_if vif;

... // trans 数据类型定义

trans ts[]; // trans 动态数组声明

task op_wr(trans t); // 写指令定义
...
task op_rd(trans t); // 读指令定义
...
task op_idle(); // 空闲指令定义
...
task op_parse(trans t); // 指令类型解析

initial begin: stmgen // 指令分发即产生激励
wait(vif != null);
@(posedge vif.rstn);
foreach(ts[i]) begin
op_parse(ts[i]);
end
repeat(5) @(posedge vif.clk);
$finish();
end
endmodule


module tb;

regs_cr_if crif();
regs_ini_if iniif();

assign iniif.clk = crif.clk;
assign iniif.rstn = crif.rstn;

ctrl_regs dut(
.clk_i(crif.clk),
.rstn_i(crif.rstn),
.cmd_i(iniif.cmd),
.cmd_addr_i(iniif.cmd_addr),
.cmd_data_i(iniif.cmd_data_w),
.cmd_data_o(iniif.cmd_data_r)
);

stm_ini ini();

initial begin: arrini
ini.ts = new[3];
ini.ts[0].cmd = WR;
ini.ts[0].cmd_addr = 0;
ini.ts[0].cmd_data_w = 32'hFFFF_FFFF;
ini.ts[1].cmd = RD;
ini.ts[1].cmd_addr = 0;
ini.ts[2].cmd = IDLE;
end

initial begin: setif // 传递接口
ini.vif = iniif;
end
endmodule

在上面的代码示例中,与之前相比,可以看到下面几处不同点:
  • stm_ini module没有声明任何端口,即激励并不通过端口传递。
  • stm_ini 内部声明了virtual interface类型,即interface的“指针”。这一点很有趣,因为interface就内部构建和应用场景来看都贴近于硬件的“世界”,比如它内部可以声明过程语句块(always/initial),而也只有硬件部分才可以对interface做例化。同时,interface又可以被软件世界所引用,这里就依靠“指针”virtual interface。在声明时,stm_ini并不知道最终例化的regs_ini_if在何处,所以它先假定regs_ini_if在仿真开始时可以被传递到virtual interface vif。
  • 信号驱动的方法例如op_wr直接引用vif,进而驱动regs_ini_if中的各个变量
  • 在TB中,需要在initial setif块中传递iniif给ini.vif,最终完成实体接口tb.iniif到虚接口ini.vif。只有完成了这一传递,才最终可以保证信号驱动任务在被调用之前,虚接口被赋值,可以寻址到实体接口。
  • 在stm_ini的stmgen initial块中一开始先需要等待stm_ini.vif接口得到赋值传递而不是null值。这可以保证在后期调用驱动方法时,不会因为tb.ini.vif悬空无法引用到tb.iniif中的变量而发生运行错误。当然,用户也可以使用“#1ps”给入固定的延时,保证tb.setif在tb.ini.stmgen执行之前完成

测试向量产生
上面示例代码中,与之前通过stm_ini模块端口驱动的例子另外一个不同点是,这次在stm_ini中声明的trans数组不是固定数组,而是动态数组ts[],在其声明时并没有规定其容量大小。这么做的好处是使得数组的大小可以调整,并且将这一任务交给更高层的tb.arrini。在tb.arrini中,首先指定了tb.ini.ts的大小为new[3],其次对其中的单元做初始化赋值。

这里,动态数组的使用和外部初始化使得TB将stimulator的驱动功能和test vector(测试向量)生成的两个任务清晰地剥离开,这么做可以尽量保证stm_ini只完成驱动功能,有一定的封装性。而面对不同的测试场景,用户只需要关心如何生成测试向量。那么可以抽象地理解为,如果用户将每个test case(测试用例)包装为一个方法,用户就可以很方便地在TB中调用这些测试场景。

这种方法使得最终stimualtor、test vector和testbench可以很好的分开来。我们再来看看下面这个实例:

module tests;

task test_wr;
tb.ini.ts = new[2];
tb.ini.ts[0].cmd = WR;
tb.ini.ts[0].cmd_addr = 0;
tb.ini.ts[0].cmd_data_w = 32'h0000_FFFF;
tb.ini.ts[0].cmd = RD;
tb.ini.ts[0].cmd_addr = 0;
endtask

task test_rd;
tb.ini.ts = new[2];
tb.ini.ts[0].cmd = RD;
tb.ini.ts[0].cmd_addr = 'h10;
tb.ini.ts[1].cmd = RD;
tb.ini.ts[1].cmd_addr = 'h14;
endtask
endmodule

module tb;

... // interface例化和连接

ctrl_regs dut(...); // dut例化

stm_ini ini();

tests tts();

initial begin: vecgen
string t;
if($value$plusargs("TEST=%s", t)) begin
$display("+TEST=%s is passed as a test vector", t);
if(t == "test_wr") tts.test_wr();
else if(t == "test_rd") tts.test_rd();
else $warning("+TEST=%s is not a valid test vector", t);
end
else
$warning("+TEST= not found");
#100ns $finish();
end

initial begin:setif
ini.vif = iniif;
end
endmodule

仿真结果:

  • sim +TEST=test_wr
# +TEST=test_wr is passed as a test vector

除了之前已经将stimulator stm_ini分开之外,这个例子也将tests和tb分开。在tests中将不同的test vector以task的形式来定义,在其内部初始化tb.ini.ts动态数组,这里通过绝对路径的引用方式可以在仿真时寻址到该数组。

而在tb.vecgen initial块中通过SV的系统函数$value$plusargs(user_string, variable)来得到仿真时外部传递的test vector指令。例如,如果仿真时传递参数+TEST=test_wr,则会通过tb.vecgen字符串比较,最终选择tb.tts.test_wr()来初始化生成测试向量;如果传递参数没有被识别,或者没有传递+TEST=参数,那么仿真都会利用系统函数$error()来报告错误。

通过上面这种方式,我们便可以将维护的力量主要投入到module tests中,而stm_ini和tb则有了较好的复用性。

仿真结束控制
在之前的例子中,可以发现,无论是stimulator、test vector还是tb都有结束仿真的权利,那么究竟让谁来结束仿真比较合适呢?我们倾向于让test vector来结束仿真,这是由于测试场景可以更具体地控制合适产生激励,也应该知道合适结束仿真。无路是仿真的开始、激励的生成,还是最终仿真的结束,都应该完整地属于测试向量的一部分。

这样就可以让整个测试场景完全交付于独立的测试向量,我们可以通过修改上面的代码,实现这一功能:

module tests;

int fin;

task test_wr;
...
fin = 150;
endtask

task test_rd;
...
fin = 200;
endtask
endmodule

module tb;

...

initial begin: vecgen
string t;
if($value$plusargs("TEST=%s", t)) begin
$display("+TEST=%s is passed as a test vector", t);
if(t == "test_wr") tts.test_wr();
else if(t == "test_rd") tts.test_rd();
else $error("+TEST=%s is not a valid test vector", t);
end

点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 254

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

站长推荐 上一条 /1 下一条

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-5-13 00:42 , Processed in 0.018018 second(s), 13 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部