mirro、desired和actual value
我们在应用寄存器模型的时候,除了利用它的寄存器信息,也会利用它来跟踪寄存器的值。寄存器模型中的每一个寄存器,都应该有两个值,一个是镜像值(mirrored value) ,一个是期望值(desired value) 。期望值是先利用寄存器模型修改软件对象值,而后利用该值更新硬件值;镜像值是表示当前硬件的已知状态值。镜像值往往由模型预测给出,即在前门访问时通过观察总线或者在后门访问时通过自动预测等方式来给出镜像值。
镜像值有可能与硬件实际值(actual value)不一致。例如状态寄存器的镜像值就无法与硬件实际值保持同步更新,另外如果其他访问寄存器的通路修改了寄存器,那么可能由于那一路总线没有被监测,因此寄存器的镜像值也无法得到及时更新。接下来我们要讨论寄存器模型的预测方式,与上面的三种值相关,这是因为预测行为会直接影响到如何更新镜像值和期望值。在介绍之前需要区别的是,mirrored value与desired value是寄存器模型的属性,而actual value对应着硬件的真实数值。
prediction的分类
UVM提供了两种用来跟踪寄存器值的方式,我们将其分为自动预测(auto prediction)和显式预测(explicit) 。如果用户想使用自动预测的方式,还需要调用函数uvm_reg_map::set_auto_predict()
。两种预测方式的显著差别在于,显式预测对寄存器数值预测更为准确,我们可以通过下面对两种模式的分析得出具体原因。
自动预测(auto prediction)
如果用户没有在环境中集成独立的preditor,而是利用寄存器的操作来自动记录每一次寄存器的读写数值,并在后台自动调用predict()
方法的话,这种方式被称之为自动预测。这种方式简单有效,然而需要注意,如果出现了其它一些sequence直接在总线层面上对寄存器进行操作(跳过寄存器级别的write()/read()
操作),或者通过其它总线来访问寄存器等这些额外的情况,都无法自动得到寄存器的镜像值和预期值。
显示预测(explicit prediction)
更为可靠的一种方式是在物理总线上通过监视器来捕捉总线事务,并将捕捉到的事务传递给外部例化的predictor,该predictor由UVM参数化类uvm_reg_predictor例化并集成在顶层环境中。
在集成的过程中需要将adapter与map的句柄也一并传递给predictor,同时将monitor采集的事务通过analysis port接入到predictor—侧。这种集成关系可以使得,monitor一旦捕捉到有效事务,会发送给predictor,再由其利用adapter的桥接方法,实现事务信息转换,并将转化后的寄存器模型有关信息更新到map中。
默认情况下,系统将采用显式预测的方式,这就要求集成到环境中的总线UVC monitor需要具备捕捉事务的功能和对应的analysis port,以便于同predictor连接。
//实例
class mcdf_bus_env extends uvm_env ;
mcdf_bus_agent agent;
mcdf_rgm rgm;
reg2mcdf_adapter reg2mcdf ;
uvm_reg_predictor #(mcdf_bus_trans) mcdf2reg_predictor; //predict是参数化类,要指定流向predictor的parent
`uvm_component_utils (mcdf_bus_env)
...
function void build phase (uvm phase phase);
agent = mcdf_bus_agent::type_id::create ("agent", this);
if (!uvm_config_db#(mcdf_rgm)::get(this, "", "rgm", rgm)) begin
`uvm_info("GETRGM", "no top-down RGM handle is assigned", UVM_LOW)
rgm = mcdf_rgm::type_id::create("rgm", this);
'uvm_info("NEWRGM", "created rgm instance locally", UVM_LOW)
end
rgm.build();
reg2mcdf = reg2mcdf_adapter::type_id::create("reg2mcdf");
mcdf2reg predictor = uvm_reg _predictor#(mcdf _bus_trans)::type_id::create("mcdf2reg_predcitor", this); //predict不是object,需要指定parent
mcdf2reg predictor.map = rgm.map; //通过地址索引到对应的寄存器,所以要传递map,adapter
mcdf2reg predictor.adapter = reg2mcdf;
endfunction
function void connect phase(uvm_phase phase);
rgm.map.set_sequencer(agent.sequencer, reg2mcdf);
agent.monitor.ap.connect(mcdf2reg_predictor.bus_in);
endfunction
endclass
uvm_reg的访问方式
uvm_reg_block、uvm_reg、uvm_reg_field提供的用于访问寄存器的方法:
mirror value不能randomize,只有prediction才能修改mirror value。desired value才能randomize。desired value先更新软件model的desired value值,再去更新硬件的actual value值,最后去更新软件的mirror value。
uvm_reg_sequence提供的方法(针对寄存器对象,而非寄存器块或寄存器域):
结合着mirrored value、desired value和actual value,我们需要理解这四种方法在调用时,三种数值的变化时序关系:
- 对于前门访问的read()
和write()
,在总线事务完成时,镜像值和期望值才会更新为与总线上相同的值,这种预测方式是显式预测。
- 对于peek()
和poke()
,以及后门访问模式下的read()
和write()
,由于不通过总线,默认采取自动预测的方式,因此在零时刻方法调用返回后,镜像值和期望值也相应修改。
关于reset()
和get_reset()
的用法,例如硬件在复位触发时,会将内部寄存器值复位,而寄存器模型在捕捉到复位事件时,为了保持同硬件行为一致,也应当对其复位。这里注意的是,复位的对象是寄存器模型,而不是硬件。
@(negedge p_sequencer.vif.rstn);
rgm.reset(); //register block reset for mirrored value and desired value
rgm.chnl0_ctrl_reg.reset(); //register level reset
rgm.chn10_ctrl_reg.pkt_len.reset(); //register field reset
在复位之后,用户也可以通过读取寄存器模型的复位值(与寄存器描述文件一致),与前门访问获取的寄存器复位值进行比较,以此来判断硬件各个寄存器的复位值是否按照寄存器描述去实现。这里的get_reset()
方法指的也是寄存器模型的复位值,而不是硬件。
//register model reset value get and check
rstval = rgm.chnl0_ctrl_reg.get_reset();
rgm.chn10_ctrl_reg.read(status, data, UVM_BACKDOOR, .parent(this));
if(rstval != data)
`uvm_error("RSTERR", "reset value read is not the desired reset value")
mirror()
方法与read()
方法类似,也可以选择前门访问后者后门访问,不同的是mirror()
不会返回读回的数值,但是会将对应的镜像值修改。在修改镜像值之前,用户还可以选择是否将读回的值与模型中的原镜像值进行比较。
下面的例码在更新镜像值之前,首先将读回的值与上一次镜像值做了比对,随后再更新镜像值。譬如,对于配置寄存器,可以采用这种方法来检查上一次的配置是否生效,又或者对于状态寄存器,可以选择只更新镜像值不做比较,这是因为状态寄存器随时可能被硬件内部逻辑修改。
//get register value and check
rgm.chn10_ctrl_reg.mirror(status, UVM_CHECK, UVM_FRONTDOOR, .parent(this));
下面的方法是运用set()
和update()
对寄存器做批量修改。首先set()
方法的对象是寄存器模型自身,通过set()
可以修改期望值,而在寄存器配置时不妨先对其模型随机化,再配置个别寄存器或者域,当寄存器的期望值与镜像值不相同时,可以通过update()
方法来将不相同的寄存器通过前门访问或者后门访问的方式做全部修改。这种set()
和update()
的方式较write()
和poke()
的写寄存器方式更为灵活的是,它可以实现随机化寄存器配置值(先随机化寄存器模型,后将随机值结合某些域的指定值写入到寄存器),继而模拟更多不可预知的寄存器应用场景,另外update()
强大的批量操作寄存器功能使得修改寄存器更为便捷。
//randomize register model, set register/field value and update to hardware actual value
void'(rgm.chnl0_ctrl_reg.randomize());
rgm.chnl0_ctrl_reg.pkt_len.set('h3);
rgm.chnl0_ctrl_reg.update(status, UVM_FRONTDOOR, .parent(this));
void'(rgm.chnl1_ctrl_reg.randomize());
rgm.chnl1_ctrl_reg.set('h22);
rgm.update(status, uVM_FRONTDOOR, .parent(this));
mem与reg的联系和差别
UVM寄存器模型也可以用来对存储建模。uvm_mem类可以用来模拟RW(读写)、RO(只读)和WO (只写)类型的存储,并且可以配置存储模型的数据宽度和地址范围。uvm_mem不同于uvm_reg的地方在于,考虑到物理存储一旦映射到uvm_mem会带来更大的资源消耗,因此uvm_mem并不支持预测和影子存储(shadow storage)功能,即没有镜像值和期望值。uvm_mem可以提供的功能就是利用自带的方法去访问硬件存储,相比于直接利用硬件总线UVC进行访问,这么做的好处在于:
- 类似于寄存器模型访问寄存器,利用存储模型访问硬件存储便于维护和复用。
- 在访问过程中,可以利用模型的地址范围来测试硬件的地址范围是否全部覆盖。
- 由于uvm_mem也同时提供前门访问和后门访问,这使得存储测试可以考虑先通过后门访问预先加载存储内容,而后通过前门访问读取存储内容,继而做数据比对,这样做不但节省时间,同时也在测试方式上保持了前后一致性。同时这种方式相比于传统测试方式(利用系统函数或者仿真器实现存储加载),要在UVM框架中更为统一。
与uvm_reg相比,uvm_mem不但拥有常规的访问方法read()
、write()
、peek()
和poke()
,也提供了burst_read()
和burst_write()
。之所以额外提供这两种方法,不但是为了可以更高速通过总线BURST方式连续存储,也是为了贴合实际访问存储中的场景。要实现BURST访问形式,需要考虑下面这些因素:
- 目前挂载的总线UVC是否支持BURST形式访问,例如APB不能支持BURST访问模式。
- 与read()
、write()
方法相比,burst_read()
和burst_write()
的参数列表中的一项uvm_reg_data_t value[]
采用的是数组形式,不再是单一变量,即表示用户可以传递多个数据。而在后台,这些数据首先需要装载到uvm_reg_item对象中,装载时value数组可以直接写入,另外两个成员需要分别指定为element_kind = UVM_MEM,kind = UVM_BURST_READ
。
内建(built-in)sequences
不少有经验的UVM用户可能会忽略UVM针对寄存器模型内建的一些sequence,实际上如果可以将这些自建的序列作为验证项目一开始的健康检查必选项的话,这对于整个项目的平稳运行会有不小的贡献。这是因为在项目一开始的阶段,设计内部的逻辑还不稳定,对于verifier而言,如果想要跟上设计的进度,可以展开验证的部分无外乎是系统控制信号(时钟、复位、电源)和寄存器的验证。在项目早期,寄存器模型的验证可以为后期各个功能点验证打下良好的基础。比如,通过内建的寄存器或者存储序列可以实现完善的寄存器复位值检查,又比如检查读写寄存器的读写功能是否正常等。
不过有一些寄存器即便可以测试,也建议将其作为例外而过滤出去,例如一些重要的系统控制信号(时钟、复位、电源),当写入某些值以后,会使得系统全部或者局部复位、时钟也可能被关闭,这就可能阻碍寄存器的下一步检查。所以UVM提供了一些特殊域,用来禁止sequence检查这些寄存器或者存储。
寄存器模型内建序列
存储模型内建序列
下面的例码演示如何利用内建序列完成MCDF寄存器测试一开始的健康检查。分别添加了uvm_reg_hw_reset_seq、uvm_reg_bit_bash_seq和uvm_reg access_seq来测试寄存器模型。从代码的整洁性来看,用户并不需要额外再添加什么,这种使用方式非常方便,且又能完成寄存器的大规模集成测试。
寄存器健康检查
//实例
class mcdf_example_seq extends uvm_reg_sequence;
mcdf_rgm rgm;
`uvm_object_utils(mcdf_example_seq)
`uvm_declare_p_sequencer(mcdf_bus_sequencer)
...
task body();
uvm_status_e status;
uvm_reg_data_t data;
uvm_reg_hw_reset_seq reg_rst_seq = new();
uvm_reg_bit_bash_seq reg_bit_bash_seq = new();
uvm_reg_access_seq reg_acc_seq = new();
if(!uvm_config_db#(mcdf_rgm)::get(null,get_full_name(),"rgm", rgm)) begin
`uvm_error("GETRGM", "no top-down RGM handle is assigned")
end
//wait reset asserted and releasee
@(negedge p_sequencer.vif.rstn);
@(posedge p_sequencer.vif.rstn);
`uvm_info( "BLTINSEQ", "register reset sequence started", UVM_LOW)
reg_rst_seq.model = rgm;
reg_rst_seq.start(m_sequencer);
`uvm_info("BLTINSEQ", "register reset sequence finished", UVM_LOW)
`uvm_info("BLTINSEQ", "register bit bash sequence started", UVM_LOW)
//reset hardware register and register model
reg_bit_bash_seq.model = rgm;
reg_bit_bash_seq.start(m_sequencer);
`uvm_info("BLTINSEQ", "register bit bash sequence finished", UVM_LOW)
`uvm_info("BLTINSEQ", "register access sequence started", UVM_LOW)
//reset hardware register and register model
reg_acc_seq.model = rgm;
reg_acc_seq.start(m_sequencer);
`uvm_info("BLTINSEQ", "register access sequence finished", UVM_LOW)
endtask
endclass
对于一些寄存器,如果想将其排除在某些内建序列测试范围之外,用户可以额外添加上面列表中提到的“禁止域名”。由于uvm_reg_block和uvm_reg均是uvm_object类而不是uvm_component类,所以可以使用uvm_resource_db来配置“禁止域名”。
下面的代码摘自mcdf_rgm::build()
方法,这相当于寄存器模型在自己的建立阶段设定了一些属性。当然,uvm_resource_db的配置也可以在更高层指定,只不过考虑到uvm_resource_db不具备层次化的覆盖属性,我们建议只在一个地方进行“禁止域名”的配置。
class mcdf_rgm extends uvm_reg_block;
...
virtual function build();
...
//disable built-in seq attributes
uvm_resource_db#(bit)::set({"REG:: ", this.chnl0_stat_reg.get_full_name()), NO_REG_ACCESs_TEST", 1);
uvm_resource_db#(bit)::set({"REG:: ", this.chnl1_stat_reg.get_full_name()), NO_REG_ACCESs_TEST", 1);
uvm_resource_db#(bit)::set({"REG:: ", this.chnl2_stat_reg.get_full_name()), NO_REG_ACCESs_TEST", 1);
endfunction
endclass