mirrored、 desired 和 actual value
在应用寄存器模型时, 除了利用它的寄存器信息, 还可以利用它来跟踪寄存器的值。 跟踪寄存器的值, 一方面是建立 mirrored value, 另一方面是为建立 desired value。寄存器模型中的每一个寄存器都应该有两个值,一个是镜像值(mirrored value),一个是期望值 (desired value) 。 期望值是先利用寄存器模型修改软件对象值, 而后利用该值更新硬件值; 镜像值是表示当前硬件的已知状态值。 镜像值往往由模型预测给出, 关于预测,在前门访问时通过观察总线或在后门访问时通过自动预测等方式来给出镜像值。 然而, 镜像值有可能与硬件实际值 (hardware actual value)不一致,例如状态寄存器的镜像值就无法与硬件实际值保持同步更新。 另外, 如果其他访问寄存器的通路修改了寄存器, 那么可能由于那一路总线没有被监测, 导致寄存器的镜像值未得到及时更新。
prediction的分类
UVM 提供了两种用来跟踪寄存器值的方式, 我们将其分为自动预测 (auto prediction)和显式预测 (explicit)。 如果读者想使用自动预测的方式,还需要调用函数 uvm_reg_map: :set_auto_predict () 。两种预测方式的显著差别在于, 显式预测对寄存器数值预测更为准确, 我们可以通过下面对两种模式的分析得出具体原因。
自动预测 (auto prediction) :如果读者没有在环境中集成独立的 predictor, 而是利用寄存器的操作来自动记录每一次寄存器的读写数值, 并在后台自动调用 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 连接。
关于 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;
`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);
mcdf2reg_predictor.map = rgm.map;
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 三个类提供的用于访问寄存器的方法。
uvm_reg_ sequence 提供的方法(均是针对寄存器对象的, 而不是寄存器块或寄存器域)如下表。
结合mirrored value、 desired value 和 actual value, 我们需要理解这4种方法在调用时, 三种数值的变化时序关系:
• 对于前门访问的 read()和 write(), 在总线事务完成时, 镜像值和期望值才会更新为与总线上相同的值, 这种预测方式是显式预测。
• 对于 peek()和 poke(), 以及后门访问模式下的 read()和 write(), 由于不通过总线, 默认采取自动预测的方式, 因此在方法调用返回后, 镜像值和期望值也相应修改。
关于 reset()和 get_reset()的用法, 下面也给出部分例码。 例如硬件在复位触发时, 会将内部寄存器值复位, 而寄存器模型在捕捉到复位事件时, 为了保持同硬件行为一致, 也应当对其复位。 这里注意的是, 复位的对象是寄存器模型, 而不是硬件。
@(negedge p_sequencer.vif.rstn);
rgm. reset(); / / register block reset for mirrored value and desired valuergm.chnl0_ctrl_reg.reset(); // register level reset
rgm.chnl0_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.chnl0_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.chnl0_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. chnlO_ctrl_reg. randomize());
rgm.chnlO ctrl reg.pkt len.set('h3);
rgm.chnlO ctrl reg. update (status, UVM FRONTDOOR, .parent (this)); void'(rgm.chnll_ctrl_reg.randomize());
rgm. chnl0_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。
• 在 adapter 实现中, 也需要考虑到存储模型 BURST 访问的情形, 实现 4 种访问类型(uvm _access_ e) 的转换,即UVM_READ、UVM_WRITE、 UVM_BURS_READ 和UVM_BURST_ WRITE。对于 UVM_READ 和 UVM_WRITE 的桥接, 已经在寄存器模型访问中实现, 而 UVM_BURST_READ 和 UVM_BURST_WRITE的转换,往往需要考虑写入的数据长度, 例如长度是否是 4、 8、 16 或其他。 比如 AHB 总线, 支持连续 4 个、8 个、 16 个数据的读写 (INCR4 、 INCR8、 INCR16), 但是如果数据长度不是这些固定长度时, adapter 还需要自己处理来实现 INCR 的连续访问方式。
• 此外还需要考虑不同总线的其他控制参数, 例如 AHB 支持 WRAP 模式, AXI 支持out-of-order 模式等, 如果想要将更多的总线控制封装在 adapter 的桥接功能里, 需要将更多的配置作为扩展配置, 在调用访问方法时作为扩展信息类,传入到形式参数uvm_object_extension。待传入后, adapter 将可以在桥接方法中抽取出扩展信息类,作为更准确的协议访问的限定依据。
• 对于更为复杂的 BURST 形式, 如果需要实现更多的协议配置要求, 那么笔者推荐直接在总线 UVC 层面去驱动。这样做的灵活性更大, 且更能充分全面的测试存储接口的协议层完备性。因此, 验证师在为存储模型访问实现 adapter 方法时, 需要考虑的是, uvm_mem 层面的方法应该尽员便捷、必要的参数应该少量, 以便于使用和维护; 而另一方面, 如果要首先测试存储接口协议, 则应该在总线 UVC 的层面上完成更充分的验证。
内建sequences
不少有经验的 UVM 用户可能会忽略 UVM 针对寄存器模型内建的(built-in) 一些sequence, 实际上, 将这些自建的序列作为验证项目开始前的健康检查必选项, 对整个项目的平稳运行会有不小的贡献。 这是因为, 在项目的开始阶段, 设计内部的逻辑尚不稳定, 验证师要跟上设计的进度, 可以展开验证的部分无外乎是系统控制信号(时钟、复位、 电源)和寄存器的验证。在项目早期 , 寄存器模型的验证可以为后期各个功能点验证打下良好的基础。 比如, 通过内建的寄存器序列可以实现完善的寄存器复位值检查, 又比如检查读写寄存器的读写功能是否正常等。
不过有一些寄存器即便可以测试, 也建议将其作为例外而过滤出去, 例如一些重要的系统控制信号(时钟、复位、电源), 当写入某些值以后, 会使得系统全部或局部复位、时钟也可能被关闭 , 这就可能阻碍寄存器的下一步检查。 所以 UVM 提供了一些特殊域, 用来禁止sequence 检查这些寄存器或存储。接下来,我们从下表来分别浏览整理出的寄存器和存储相关的自建sequence 。
寄存器模型内建序列
存储器模型内建序列
接下来我们给出一段例码,来演示如何利用内建序列完成MCDF寄存器测试一开始的健康检查。 下面的例码分别添加了 uvm_reg_ hw _reset_ seq、 uvm_reg_ bit_ bash_ seq和 uvm _reg_ access_ seq 来测试寄存器模型, 从代码的整洁性来看, 用户并不需要额外再添加什么, 这种使用方式非常方便, 且又能完成寄存器的大规模集成测试。
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 release
@(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;
`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. chnlO stat reg. get full name () } ,
NO REG ACCESS TEST", 1);
uvm_resource_db# (bit)::set ({ "REG: : ", this. chnll 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", l);
endfunction
endclass