基于VMM1.2验证方法学的微码引擎验证环境


林 罕lin.han@nationz.com.cn

国民技术股份有限公司

摘要

目前,使用SystemVerilog语言,并结合Synopsys的VMM验证方法学搭建验证平台,已经成为业界的广泛标准。利用SystemVerilog面向对象的封装,继承,多态的特性,再加上VMM1.2提供的强大的类库,使得验证具有很好的结构性和重用性,缩短了验证平台搭建的时间,提高了效率。并且利用SystemVerilog约束性随机,建模自动比对和功能覆盖率收集的功能,能够大大提高验证的质量。
本文主要提出了一种基于SystemVerilog的VMM1.2验证方法学的32位微码引擎的验证环境,采用VMM1.2类库。这个微码引擎是由公司自己开发,自行定义指令集的一个小型CPU。这个验证平台采用基于VMM类库进行的微码引擎建模,抽象微码引擎的MCU指令,并且通过约束产生随机指令,另外,功能覆盖率模型能够收集覆盖率,达到验证目标。

Abstract

Nowadays, using SystemVerilog language and Synopsys VMM methodology to build a verifi cation platform has become the industry wide standard. Using SystemVerilog object-oriented encapsulation, inheritance, polymorphism features, coupled with VMM provides powerful library, making the testbench structural and reusability, reducing time to build verifi cation platform,improving efficiency. Binding testbench use SystemVerilog constraint-randomize, auto-checking, collecting coverage can greatly improve the quality of verifi cation.
Here deliver a VMM verification environment on SystemVerilog. In this environment, verified an 32-bit MicroEngine which developed by NationZ itself. This verification platform used VMM class to modeling MicroEngine, abstracting MicroEngine instruction, use random Generator to generate constraint -randomize instruction. In addition, the functional coverage model is able to collect coverage, achieve the verifi cation target.

1. 简介

随着芯片规模越来越大,复杂度越来越高,在芯片的整个设计和验证中,验证占用了大部分的时间和精力,为了达到验证所要求的各项指标,验证工程师需要付出极大的努力,在验证的过程中,有三个问题是需要解决的,首先,如何发出激励,并且激励易于控制和随机,并且能保证,激励是能够覆盖模块所需要的功能。其次,如何保证DUT的响应是正确的,如何做到自动比对,不需要人为的对每条测励进行判断。最后,如何提高验证平台的重用性,使得验证组件可以在同一项目的不同阶段和不同项目中去重用。
Synopsys VMM1.2验证方法学提供了基于SystemVerilog的验证方法,包括了带约束的随机场景发生器,层次化的验证结构,对所有测励通用的验证平台以及功能覆盖率为指标的验证流程。充分利用了SystemVerilog的面向对象的封装,继承,多态的特点,并且开发出一套验证类库,使得用户能够充分利用这套类库,迅速的建立易于控制的,高度自动化的,可重用的验证平台。
第二章将会介绍待测的DUT,即公司自研的微码引擎。第三章将会介绍整个验证环境的架构和流程。第四章将会介绍环境中各个组件如何实现。包括参考模型,随机指令如何灌入,以及如何做自动比对和功能覆盖率收集。最后一章将会总结整个验证的流程。

2. DUT简介

这个验证环境验证的DUT,是公司自研的32位微码引擎,基本功能如下:
• 采用哈佛结构,程序指令存储和数据存储分开。指令存储的地址位宽为20bit,支持寻址空间为0×00000~0xFFFFF。数据存储的地址位宽为16bit,支持寻址空间为0×0000 ~0xFFFF。指令存储的数据总线位宽32bit,数据存储的数据总线位宽为32bit。
• 数据总线访问支持ready机制。
• 程序总线访问支持ready机制。
• 支持的指令包括:空操作、跳转、算术运算、逻辑运算、循环移位、算术移位、逻辑移位、数据转移和数据装载。
• 跳转指令在1个时钟周期内完成;外部特殊寄存器访问1个周期完成;外部memory访问,写1个周期完成,读2个周期完成;乘法指令17个周期完成;除法指令33个周期完成;其它指令1个周期完成。
• 支持中断功能。(不支持中断嵌套)
• 支持abort机制。中断有效时会打断当前未完成的外部访问操作。中断有效时会打断当前未完成的取值操作。中断有效时乘法和除法未执行完成将被打断。中断完成后会返回打断之前的地址执行。
• 跳转指令要支持绝对跳转(指令后面直接携带目标地址)、相对跳转(目标地址由寄存器和偏移地址相加)、位检测跳转、比较判断跳转。
• 支持软件debug功能。
• 支持设置和清除断点。
以下是微码引擎简要模块框图。

图 1 – 微码引擎内部结构

3. 基于VMM1.2 类库的MCU验证平台

微码引擎的验证环境是指用于运行芯片顶层功能验证的各个组件(component)的集合,它充分利用了VMM1.2 类库,包括约束性随机的指令生成,自动比对机制,覆盖率收集,断言检查时序等组件,如图2所示:

图 2 –微码引擎验证平台架构

• testcase: 测试用例,包括对环境的配置和DUT的配置。
• int_gen: 中断事物发生器。用于产生Microine的中断事物。
• debug_gen: 调试场景发生器,用于产生调试事物场景。
• micro_model: Microine的参考模型,这个模型是由Instruction ,Microine,MicroDecode三个类组成的。Instruction类是参数化类,对于不同的微码项目是可重用的。只需要修改参数。
• micro_scb: 记分板,主要完成参考模型和DUT的自动比对。
• int_driver: 时序发生器,产生中断的时序。
• micro_mon: 监视器,用于监控DUT的运行结果。
• instr_ram : 指令RAM模型。用于模拟Microine指令存储器行为。
• data_ram: 数据RAM模型。用于模拟Microine数据存储器行为。
• Assertion : 根据DUT的内部信号,判断指令的执行周期。中断相应的时序要求是否符合。
平台中的模块,都是基于VMM1.2的类库开发的,在接下来的一章,将详细介绍主要组件的开发。

4. 各个组件的功能及其开发

4.1 简介

以VMM1.2为基础,实现一个验证32位微码引擎的平台,这个平台可以随机生成一系列指令,也可以通过用户指定汇编指令,进行定向测试,上一章已经讲了平台的基本框图和各个组件的简介。这一章主要是对各个组件进行详细的描述。

4.2 指令事物以及Atomic Generator

在VMM验证环境中,事物一般从vmm_data中派生或者从vmm_object派生,vmm_data提供了一些宏,用户可以定义这些宏,从而自动展开出事物的方法。节省代码量,使得代码重用性更高,更为简洁。
微码引擎的指令有两种指令格式,一种是二字节指令,一种是四字节指令,部分指令编码如图三所示:

图 2 –微码引擎部分指令集

指令的前八位是操作码,对于这个微码,具体原则是,一个字的存储地址,只能是两条2-bytes指令或一条2-bytes指令和一条remove指令,或一条4-bytes指令。第一个半字不能为remove指令。

4.2.1 随机指令操作码事物

随机指令操作码主要作用是将各个操作码约束成有效的指令或标号。将32bit的字分成两个操作码,那么对于二字节指令,这个操作码就决定了整条指令,对于四字节指令,是由两个操作码组成的,并且第二个操作码的类型必须是DATA_ADDR_LABLE或者BYTE_EN_LABLE。
指令操作码事物包括以下数据成员:
rand opcode_type_e opcode_type; :操作码类型
rand kind_e kind; :具体指令类型
rand bit[15:0] instr; :操作码的值
rand bit[19:0] data_addr; :数据或地址
rand bit[3:0] rn; :rn的标号
rand bit[3:0] rm; :rm的标号
rand bit[3:0] byte_enable; :byte_enalbe的标号
rand bit[4:0] bit_num; :比特位
指令操作码类型分为:TWO_BYTE_INSTR,FOUR_BYTE_INSTR_1,FOUR_BYTE_INSTR_2,LABLE,分别表示二字节指令,标号是地址或数据的四字节指令,标号是ByteEnable的四字节指令。通过一个枚举类型来表示:
typedef enum {
TWO_BYTE_INSTR,
FOUR_BYTE_INSTR_1,
FOUR_BYTE_INSTR_2,
LABLE
} opcode_type_e;
对各种指令的操作码用枚举类型来表示:
typedef enum {
NOP,
RETI,
ADD_R0_Rm,
JMPR_Rel_Rm,
DJNZ_Rm_Rel,
BJZ_Rel,
CMP_Rn_Rm,
………
REMOVAL,
DATA_ADDR_LABLE,
BYTE_EN_LABLE
} kind_e;
其中,表示如果指令是二字节指令,第二个字节可能是数据地址或者Byte Enable,这两种类型分别用DATA_ADDR_LABLE,BYTE_EN_LABLE来表示。
对于指令约束主要来自指令格式,首先约束的是rm和rn的值,对于微码指令,rm和rn的值只能是r0~r9
constraint only_10_register_valid{
rn inside {[0:9]};
rm inside {[0:9]};
}
然后对byte_enable进行约束,byte_enale只支持4’b0001,4’b0010,4’b0100,4’b1000,4’b0011,4’b1100,
4’b1111这其中格式。
constraint byte_enable_valid{
byte_enable inside {1, 2, 3, 4, 8, 12, 15};
}
接着对指令类型进行约束
constraint opcode_type_valid{
(opcode_type==TWO_BYTE_INSTR) -> kind inside { NOP, RETI, ADD_R0_Rm,

REMOVAL };
(opcode_type==FOUR_BYTE_INSTR_1) -> kind inside{ AND_R0_Mem,

ORR_R0_Mem, XOR_R0_Mem};
(opcode_type==FOUR_BYTE_INSTR_2) -> kind inside{ MOV_Rn_iRm_B, MOV_iRn_Rm_B };
(opcode_type==LABLE) -> kind inside { DATA_ADDR_LABLE, BYTE_EN_LABLE };
}
最后对指令的值进行约束
constraint instr_value{
(kind == NOP ) -> (instr[15:0] == 16′h0000);
(kind == RETI ) -> (instr[15:0] == 16′h0100);
(kind == ADD_R0_Rm ) -> (instr[15:0] == {12′h080,rm_1});
(kind == SUB_R0_Rm ) -> (instr[15:0] == {12′h090,rm_1});
(kind == INC_Rm ) -> (instr[15:0] == {12′h0a0,rm_1});
(kind == DEC_Rm ) -> (instr[15:0] == {12′h0b0,rm_1});
(kind == LSR_R0_Rm ) -> (instr[15:0] == {12′h190,rm_1});
(kind == ROL_R0_Rm ) -> (instr[15:0] == {12′h1a0,rm_1});
(kind == DATA_ADDR_LABLE) -> (instr[15:0] == data_addr[15:0]);

(kind == BYTE_EN_LABLE ) -> (instr[15:0] == {byte_enable,12′h0});
}

4.2.2 随机指令槽事物

对于这个微码,具体原则是,一个字的存储地址,只能是两条2-bytes指令或一条2-bytes指令和一条remove指令,或一条4-bytes指令。第一个半字不能为remove指令。随机指令槽事物,就是对微码这个原则进行约束。
指令槽事物只包括一个数据成员rand opcode op[2]表示指令槽的两个操作码,首先约束第一个字节必须不能是LABLE
constraint slot0_only_good {
!(op[0].opcode_type inside {opcode::LABLE});
}
其次,约束指令槽的第二个字节不能是四字节指令的前两个字节
constraint slot1_only_good {
!(op[1].opcode_type inside {opcode::FOUR_BYTE_INSTR_1, opcode::FOUR_BYTE_INSTR_2});
}
最后约束前两个字节和后两个字节的关系,首先,如果前两个字节是二字节指令,那么后两个字节必须是二字节指令。其次,指令的第一个字节不能是REMOVAL指令,最后,如果指令是四字节指令,那么指令是FOUR_BYTE_INSTR_1类型,那么后两个字节必须是DATA_ADDR_LABLE,如果指令是FOUR_BYTE_INSTR_2类型,那么后两个字节必须是BYTE_EN_LABLE。
constraint slot0_slot1_valid {
(op[0].opcode_type == opcode::TWO_BYTE_INSTR) -> (op[1].opcode_type ==
opcode::TWO_BYTE_INSTR);
op[0].kind != opcode::REMOVAL;
(op[0].opcode_type == opcode::FOUR_BYTE_INSTR_1) -> (op[1].kind ==
opcode::DATA_ADDR_LABLE);
(op[0].opcode_type == opcode::FOUR_BYTE_INSTR_2) -> (op[1].kind == opcode::BYTE_EN_LABLE);
}

4.3 中断事物及中断产生器

中断事物是用于控制高电平和低电平的时间,可以通过约束这两个时间的范围,从而达到对中断的密度进行控制。中断事物包含两个数据成员int_cycle,idle_cycle,分别是中断电平持续时间,中断间断时间。
中断产生器,是将中断的事物随机化之后,放入channel中去,driver将从channel中获取这个事物,将其转换成具体的时序。中断产生器,直接使用vmm提供的宏,就可以实现,只需要一行代码:
`vmm_atomic_gen(int_trans, “Interrupt transaction generator”)

4.4 中断驱动器(IntDriver)

中断驱动器,是将中断产生器产生的随机中断事物,转换成具体的时序,在VMM1.2中,Driver是从vmm_xactor中派生的。
数据成员:
int_trans_channel in_chan; :传送int_trans事物的通道
virtual micro_if sigs; :与DUT相连的接口
方法:
extern function new (string inst, vmm_unit parent=null); :构造函数
extern virtual protected task main(); :主任务,将接受到的事物转换成具体时序

4.5 参考模型(RM)

RM是一个行为级功能模型,它主要模拟的微码引擎指令执行和中断处理的功能。它是由以下几个类组成的:
Instruction :这个是由vmm_object派生的类,主要模拟各种指令的执行过程。这是一个参数化类,可重用在不同项目不同位宽的微码引擎项目,只需要通过不同的参数传递就可以。
Memory:这个是由vmm_object派生的类,主要提供参考模型memory初始化,读写,配置的函数。这是一个参数化类,ROM和RAM的大小可以通过参数传递配置,可重用到16位平台和32位平台的微码引擎。
MicroDecode:这个是由vmm_object派生的类,是八位微码引擎的译码模块,这个类是不可重用的,因为不同的微码引擎拥有不同的指令格式和译码方式。
micro_model:这个是从vmm_xactor派生的类。这个是微码引擎模型的顶层,它提供了微码引擎基本的寄存器和基本操作函数。通过调用这个类的start_xactor函数,就可以模拟指令的运行过程。
参考模型的顶层是由vmm_xactor派生的原因是,参考模型属于环境的一部分,从vmm_xactor派生,可以通过vmm1.2的inplicit phase,环境直接启动参考模型,其次,vmm_xactor提供了callbacks的类,可以通过这些callbacks类,对参考模型进行控制和执行结果进行监控,从而不用修改参考模型的代码,达到可重用的目的。
task micro_model::main();
super.main();
`vmm_trace(this.log, $psprintf(”%m”));
count = 0;
reset();
while(count < instrcount || instrcount <= 0)begin
string instr;
string result;
run_type = NORMAL;
`vmm_callback(micro_model_callbacks,before_run(this));
ir = get_ir(pc);
count ++;
ins = microDecode.decode(ir[15:0]);
pc_inc2();
ins.run();
instr = ins.to_string;
result = ins.show_result();
`vmm_note(this.log,$psprintf("%s\n%s",instr,result));
`vmm_callback(micro_model_callbacks,after_run(this));
end
this.notify.indicate(this.DONE);
endtask
由于DUT是带时序的模型,而参考模型是不带时序的,验证环境期待的是,DUT执行一条指令,通知RM执行一条指令,当DUT遇到中断,则通知RM执行中断,这样就可以达到RM和DUT同步和中断验证的目的。DUT中断和执行指令,是通过vmm_notify来实现的。
task micro_int_callbacks::before_run(micro_model micro);
// Monitor has detect MicroEngine DUT has run an instruction
mon.notify.wait_for(mon.RUN_ONE_INSTR);
`vmm_note(log,$psprintf("DUT has run an instruction at %d",$time));
// Monitor has detect MicroEngine DUT has go into interrupt routine
if(mon.notify.is_on(mon.BREAK_INT)) begin
// Set MicroEngine Model interrupt run bit
micro.run_type = INT_BREAK;
// Reset Monitor INT notify, wait next interrupt
mon.notify.reset(mon.BREAK_INT);
end
endtask
DUT通过Monitor得到微码的执行结果,要达到跟参考模型自动比对,自然也需要得到参考模型的执行结果。可以通过扩展micro_model_callbacks类,来达到窥探参考模型执行结果的目的。

4.6 接收事物以及监控器

DUT执行一条指令之后,需要将执行的结果抽象成事物,这样才利于比对和调试,接收事物就是将DUT执行指令的结果抽象成具
体的事物。数据成员包括:
bit [31:0] rn[8]; :通用寄存器
bit [31:0] rn_shadow[8]; :shadow状态下的通用寄存器
bit [31:0] pc; :程序指针
bit [31:0] sp; :堆栈指针
bit [31:0] sr; :程序状态寄存器
bit [31:0] sr_shadow; :shadow状态下程序状态寄存器
bit [31:0] source1; :源1域
bit [31:0] source2; :源2域
bit [31:0] dest; :目的域
bit [31:0] mem_data; :存储器数据
bit int_fl ag; :中断标识
bit [31:0] retipc; :中断返回地址
opcode::kind_e kind; :指令类型
而监控器,就是监控DUT的时序,将其抽象成接受事物的格式。并且将这个事物放入channel,在scoreboard跟参考模型执行的结果进行自动比对。

4.7 自动比对Scoreboard

Scoreboard收集的是DUT和RM的运行结果,进行自动比对,根据比对结果,打印各种信息,根据各种信息判断DUT是否正确。
Scoreboard是从vmm_xactor派生的,它可以通过implicit phase的方式,自动被环境调用。

4.8 功能覆盖模型

对于微码这样的DUT,代码覆盖率是不能够代表验证的充分性,必须开发一些覆盖率模型,来保证DUT验证的充分性。例如:
单指令的功能覆盖点,这个功能覆盖点,是用于保证微码执行过所有的指令。
cp_op_type : coverpoint opcode_kind{
bins op_nop = {opcode::NOP};
bins op_reti = {opcode::RETI};
bins op_add_r0_rm = {opcode::ADD_R0_Rm};
bins op_sub_r0_rm = {opcode::SUB_R0_Rm};
bins op_inc_rm = {opcode::INC_Rm};
bins op_dec_rm = {opcode::DEC_Rm};

}
指令翻转功能覆盖点:由于微码是分为取指单元和执行单元两个过程,所以,必须保证所有指令的翻转组合都覆盖到。
cp_ir_cross : coverpoint opcode_kind{
bins op_cross[] = ([opcode::NOP : opcode::INTERRUPT] =>
[opcode::NOP : opcode::INTERRUPT]);
}
对于微码这样的DUT,我们不仅仅要考虑各条指令是否执行到,而且要充分考虑到他们排列组合的情况,需要考虑到各种状态位与指令的交叉情况,跳转地址的覆盖情况等等。SystemVerilog提供的各种覆盖率模型,使得我们可以充分的对这些情况进行建模,检验验证的充分性。

4.9 构造测试场景

SystemVerilog可以通过约束的方式,构造出各种测试场景,这些测试场景可以通过队列的方式,定义测试场景,通过SystemVerilog的foreach语句,可以对队列的每个元素进行约束。

4.9.1 约束的场景

对于微码的验证,有时候,我们需要只关注算术操作,就可以通过将场景约束成只有算术运算指令。
class arith_only_scn extends common_instruction_scenario;
int SCN_ARITH_ONLY;
constraint arith_only_scn_valid {
if (scenario_kind == SCN_ARITH_ONLY) {
repeated == 0;
length inside {15:30};
foreach (items[i]) {
foreach (items[i].op[j]) {
items[i].op[j].opcode_type == opcode::COMPUTE;
}}}}
function new ();
super.new();
this.SCN_ARITH_ONLY = defi ne_scenario (“SCN_ARITH_ONLY”, 30);
endfunction
endclass

4.9.2 定向的场景

对于我们的微码验证,有时候,还可以定义一些完全定向的场景,用于测试用户希望测试的定向指令。这样,就可以定义一些定向的场景。
class init_regs_scn extends instruction_stream_scenario;
int SCN_INIT_REGS;
constraint init_regs_scn_valid {
if (scenario_kind == SCN_INIT_REGS) {
length == 0;
repeated == 0;
}
}
function void new();
super.new();
this.SCN_INIT_REGS = defi ne_scenario (“SCN_INIT_REGS”, 24);
endfunction
function int unsigned init_regs_scenario_t (instruction_channel ch);
int i, status;
this.items = new[24];
for ( i = 0; i < 24; i++ ) begin
this.items[i] = new;
status = this.items[i].randomize() with {
op[0].kind == opcode::JMP; op[0].rd == 2*i;
op[1].kind == opcode::JMPZ; op[1].rd == 2*i+1;
};
end
this.length = this.items.size();
foreach (this.items[i]) begin
if (ch != null) ch.put_t (this.items[i]);
end
endfunction
virtual task apply (instruction_channel ch);
if (scenario_kind == this.SCN_INIT_REGS) begin
this.init_regs_scenario_t (ch);
end
endtask
endclass

5. 环境及其流程

VMM1.2的环境是从vmm_group派生的,在VMM1.2中,环境的流程是采用Implicitly Phase。是分成三个time_line,分别是:pre_test/ top_test/ post_test。各个阶段都是自动调用的,无需显式声明,VMM1.2环境的执行流程如图3所示:

图4- VMM1.2 验证流程

对于微码验证,主要是定义了这几个阶段:
build_ph() :构造环境的各个组件:Monitor,Driver,MicroineModel,Callbacks类的创建和注册。各个组件的channel。
connect_ph() :将各个组件根据配置信息,通过channel连接起来。连接Monitor与Scoreboard,Rm和Scoreboard,
Generator和Driver等。
confi gure_test() :随机化Confi g类,或者通过vmm_opts改变配置 。
start_of_sim ():这个阶段,将配置信息配置Testbensh的组件。
reset_ph():这个阶段,产生DUT的复位信号。
start_ph(): 这个阶段启动各个组件,由于是implicit phase,所以各个组件是自动启动的。无需显式调用。
shutdown_ph(): 等待Monitor和Rm的通知信息,如果这两个组件都通知DONE信息,指示测试结束。
对于微码的指令,一旦开始运行,尤其是随机指令,执行指令不是顺序的,有时候就会进入死循环,所以,环境希望进入死循环
一定时间,就重新reset,重新灌入随机化的指令,继续验证。
对于vmm1.2提供了timeline使得用户可以方便的在top_test阶段对环境的流程进行跳转。从而达到这个验证目的:
vmm_timeline t1;
initial begin
vmm_opts::set_int(”%*:int_drv_set”,0);
env = new(”env”);
t1 = vmm_simulation::get_top_timeline();
vmm_simulation::list();
repeat(100)begin
t1.run_phase(”cleanup”);
t1.reset_to_phase(”confi gure_test”);
end
通过timeline提供的run_phase和reset_to_phase的机制,可以使得环境回到原来的阶段,再次初始化和reset,重新验证,不用停止仿真。

6. 总结

本文实现了一个验证微码的验证环境,在这个环境,可以支持随机的和定向C代码的指令,通过参考模型进行自动比对,利用功能覆盖模型,对验证质量进行把关。充分利用了VMM1.2提供的implicit phase,timeline,callbacks等机制,使得验证环境的控制方便灵活,重用性高。