前门访问:指的是通过模拟cpu在总线上发出读指令,进行读写操作。在 这个过程中,仿真时间($time函数得到的时间)是一直往前走的。有了寄存器模型后,可以在任何耗费时间的phase中使用寄存器模型以前门访问或后门(BACKDOOR)访问的方式来读取寄存器的值。
后门访问:从广义上来说,所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操 作都是后门访问操作。不通过总线进行读写操作,而是直接通过层次化的引用来改变寄存器的值。所有后门访问操作都是不消耗仿真时间(即$time打印的时间)而只消耗运行时间的。
在验证平台中离不开寄存器的使用,通过寄存器来控制数据行为。那么在验证平台中如何将寄存器的值传递给参考模型和duv呢?和正常的数据item通过intf有什么区别?为什么多了个寄存器模型?
通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器, DUT可以根据寄存器的值来改变其行为。这组控制端口就是寄存器配置总线。可以通过外挂agent产生的item通过intf传递给duv。总线协议规定了读写操作等,由上图可以好理解写入改变寄存器的值。也可以如in_agent将控制数据传递给intf。那么DUT中寄存器的值应该和refm控制数据的值是保持一致的,都是由外部bus_agent产生写入的。
假如DUT中某个寄存器会根据RTL内部代码行为,不同的状态下改变自己的值,即这个值不是外部agent产生同时写入refm和DUV的,那么refm如何获取DUT中寄存器的值?只能先通过使用bus_driver向总线上发送读指令,并给出要读的寄存器地址来查看一个寄存器的值。要实现这个过程,需要启动一个sequence,这个sequence会发送一个transaction给bus_driver。所以第一个问题是如何在参考 模型的控制下来启动一个sequence以读取寄存器。第二个问题是,sequence读取的寄存器的值如何传递给参考模型。refm可以通过全局变量控制何时读寄存器,不使用全局变量则可以通过config_db结合事件搭配,启动seq读寄存器,同样利用config_db机制将读取到的寄存器值config_db给refm。过程相当麻烦。为了方便使用,UVM多了寄存器模型的功能。启动seq以及读取寄存器的值返回的事情都可以由寄存器模型自动完成。
寄存器模型基本概念,从大到小:
uvm_reg_block:寄存器块,里边可以包含很多uvm_reg和其它的uvm_reg_block,还会有uvm_refg_map。
uvm_reg:寄存器。
uvm_reg_field:寄存器域段,根据不同的bit代表不同的功能,reserved不算域段。为寄存器模型的最小单位。
uvm_reg_map:存储寄存器地址,并将其转换成可以访问的物理地址(因为加入寄存器模型中的寄存器地址一般都是偏移地址,而不是绝对地址)。当寄存器模型使用前门访问方式来实现读或 写操作时,uvm_reg_map就会将地址转换成绝对地址,启动一个读或写的sequence,并将读或写的结果返回。在每个reg_block内 部,至少有一个(通常也只有一个)uvm_reg_map。
建造寄存器模型的步骤:
1)从uvm_reg派生一个类,并在此类(寄存器)中声明寄存区域段。每一个派生自uvm_reg的类都有一个build,但和uvm_component中的build_phase不一样,并不会自动执行,只能手动调用。在寄存器的build中完成寄存器域段的实例化。在实例化后要调用configure函数配置这个域段。在new函数中,要将寄存器的宽度作为参数传递给super.new函数。这里的宽度并不是指这个寄存器的有效宽度,而是指 这个寄存器中总共的位数。build完成的是对寄存器域段的实例化,new函数完成的是寄存器的实例化,寄存器的注册宏用的是uvm_object_utils类型。
class reg extends uvm_reg;
5
6 rand uvm_reg_field reg_data;
7
8 virtual function void build();
9 reg_data = uvm_reg_field::type_id::create("reg_data");
10 // parameter: parent, size, lsb_pos, access, volatile, reset value,
has_reset, is_rand, individually accessible
11 reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
12 endfunction
13
14 `uvm_object_utils(reg)
15
16 function new(input string name="reg");
17 //parameter: name, size, has_coverage
18 super.new(name, 16, UVM_NO_COVERAGE);
19 endfunction
20 endclass
定义好此寄存器后,需要在一个由reg_block派生的类中将其实例化。
22 class reg_model extends uvm_reg_block;
23 rand reg reg1;
24
25 virtual function void build();
26 default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
27
28 reg1 = reg::type_id::create("reg", , get_full_name());
29 reg1.configure(this, null, "");
30 reg1.build();
31 default_map.add_reg(reg1, 'h9, "RW");
32 endfunction
33
34 `uvm_object_utils(reg_model)
35
36 function new(input string name="reg_model");
37 super.new(name, UVM_NO_COVERAGE);
38 endfunction
39
40 endclass
一个uvm_reg_block中一定要对应一个uvm_reg_map,系统已经有一个声明好的default_map,只需要在build中将其实例化。这 个实例化的过程并不是直接调用uvm_reg_map的new函数,而是通过调用uvm_reg_block的create_map来实现,create_map有众多的 参数,其中第一个参数是名字,第二个参数是基地址,第三个参数则是系统总线的宽度,这里的单位是byte而不是bit,第四个参 数是大小端,最后一个参数表示是否能够按照byte寻址。
同uvm_reg派生的类一样,每一个由uvm_reg_block派生的类也要定义一个build函数,一般在此函数中实现所有寄存器的实例化。
随后实例化reg1并调用reg1.configure函数。这个函数的主要功能是指定寄存器进行后门访问操作时的路径。其第一个参数是此寄存器所在uvm_reg_block的指针,这里填写this,第二个参数是reg_file的指针这里暂时填 写null,第三个参数是此寄存器的后门访问路径,这里暂且为空。当调用完configure时,需要手动调用 reg1的build函数,将寄存器中的域实例化。 最后一步则是将此寄存器加入default_map中。uvm_reg_map的作用是存储所有寄存器的地址,因此必须将实例化的寄存器加 入default_map中,否则无法进行前门访问操作。add_reg函数的第一个参数是要加入的寄存器,第二个参数是寄存器的地址,这里 是16’h9,第三个参数是此寄存器的存取方式。 到此为止,一个简单的寄存器模型已经完成。
建立寄存器模型需要从uvm_reg_block类派生类并在此实例化寄存器,需要从uvm_reg派生寄存器类并在类中实例化寄存器域段。并在寄存器块uvm_reg_block的子类中添加地址,添加地址的方式是使用default_map。另外还要调用configure函数对寄存器和寄存器域段加以配置,如读写类型,宽度等。
建立好寄存器模型,还需要将寄存器模型集成到平台上。寄存器模型通过seq访问寄存器,seq会产生一个uvm_reg_bus_op的变量。此变量存储这操作类型、操作地址(写和操作数据)。此变量的信息要经过一个转换器转换成bus_seqr能接收的类型。
UVM提供了一个uvm_reg_adapter类,在其中定义了两个函数,一个是reg2bus,另外一个是bus2reg。寄存器模型通过seq产生的item经reg2bus函数后被bus_seqr接收进而发给bus_drv,bus_drv最终实现总线的读写操作。bus_drv在驱动总线进行读操作时能够顺便获取总线的值再返回给adapter,经bus2reg返回给寄存器模型能够接收的值。通过寄存器模型传递给refm。
寄存器模型可以在所需的位置声明例化,比如env或testbase。此外还需要声明adapter。
声明后需要将其实例化,并调用对象的build函数将其中的寄存器实例化。
还要调用lock_model函数,调用此函数后,reg_model中就不能再加入 新的寄存器了。
第四是调用reset函数,如果不调用此函数,那么reg_model中所有寄存器的值都是0,调用此函数后,所有寄存器 的值都将变为设置的复位值。
寄存器模型的前门访问操作最终都将由uvm_reg_map完成,因此在connect_phase中,需要将转换器和bus_sequencer通过 set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态。寄存器模型在添加寄存器时有设置初始复位值,reg_cfg中也会有reset及相应的初始复位值,在用例中设置reg_cfg中变量的名,reg_seq中获取reg_cfg后,将相应的变量set给寄存器,如若在寄存器模型中寄存器的默认值设置错误,比如寄存器a默认值为1,寄存器模型中设置错误为0,此时再在用例中设置reg_cfg.a=0,在reg_seq中set后就会set为0,此时期望值和镜像值就为一致,不会进行update操作。所谓寄存器模型auto_predict都是平台中寄存器模型的值,不会真正去rtl中读取。镜像值初始也就是寄存器模型设置的默认值。
在用例build_phase中set reg_cfg给reg_seq后,再在connect_phase或configure_phase中把reg_cfg中变量的值改变,reg_seq获取的是改变后的值,虽然reg_cfg已经发送过去,但reg_seq是在main_phase后才使用,因此虽然已经发送过reg_cfg句柄,但仍可以在main_phase前改变reg_cfg的变量的值。
在其它component中使用寄存器模型需要把寄存器模型传递过去,类似于virtual_seqr和seqr之间的关系。寄存器模型提供了read和write函数,可以通过mo_reg_model.mo_reg.mo_reg_field.read/write的方式进行读取或写入。
本文章参考《UVM实战》、《芯片验证漫游指南》等资料整理而成,仅作学习心得交流,如果涉及侵权烦请请告知,我将第一时间处理。