寄存器模型的应用场景
如何检查寄存器模型
在了解了寄存器模型的常规方法之后, 我们需要考虑如何利用这些方法来检查寄存器, 以及协助检查硬件设计逻辑和比对数据。 要知道, 在软件实现硬件驱动和固件层时, 也会实现类似寄存器模型镜像值的方法, 即在寄存器配置的底层函数中同时声明一些全局的影子寄存器 (shadow register)。 这些影子寄存器的功能就是暂存当时写入寄存器的值, 而在后期使用时, 如果这些寄存器是非易失的 (non-volatile), 那么便可以省略读取寄存器的步骤,转而使用影子寄存器的值, 这么做的好处在于响应更迅速, 而不再通过若干个时钟周期的总线发起请求和等待响应,但另一方面, 这么做的前提与我们测试寄存器模型的目的是一样的, 即寄存器的写入值可以准确地反映到硬件中的寄存器。
利用寄存器模型的另一个场景是, 在对硬件数据通路做数据比对时,需要及时地知道当时的硬件配置状况, 利用寄存器模型的镜像值可以实现实时读取, 而不需要从前门访问。也许读者会有别的选择,为什么不从后门访问呢?亳无疑问,后门访问也可以在零时刻内完成,只是这么做会省略检查寄存器的步骤, 即假设寄存器模型的镜像值同硬件中的寄存器真实值保持一致, 而这一假设存在验证风险。 所以只有这么做, 才能为后期软件开发时使用影子寄存器扫清可能的硬件缺陷。
寄存器模型不但可以用来检查硬件寄存器,也可以用来配合 scoreboard 实时检查 DUT 的功能。用来检查寄存器时,结合上一节的常规方法和例码,我们总结出下面几种可行的方式:
• 从前门写,并且从前门读。这种方式最为常见,但无法检查地址是否正确映射, 下面的前门与后门混合操作的方式可以保证地址的映射检查。
• 从前门写,再从后门读。 即利用 write()实现前门写, 再使用 read()或 peek()从后门读。
• 从后门写, 再从前门读。 即利用 write()或 poke()从后门写, 再利用 read()从前门读。
• 对于一些状态寄存器(硬件自身信号会驱动更新其实际值), 先用 peek()来获取(并且会调用 predict()方法来更新镜像值), 再调用 mirror()方法来从前门访问并且与之前更新的镜像值比较。
• 上面的这些方法,在寄存器模型的内建序列中都已经实现。与内建序列相比,自建序列可以更灵活,更贴近需求,但需要消耗更多的人力;内建序列使用简单,是全自动化的方式,但考虑到一些特殊的寄存器,在使用内建序列测试前,用户需要对一些寄存器设定 “禁止域名 ” 来将其排除在特定的寄存器检查范围以外。
在配合 scoreboard 实施检查 DUT 的功能时, 需要注意如下几点:
• 无论是将寄存器模型通过 config_db 进行层次化配置,还是间接通过封装在配置对象 (configuration object) 中的寄存器模型句柄, 都需要 scoreboard 可以索引到寄存器模型。
• 在读取寄存器或寄存器域的值时,用户需要加以区分。不少初学者默认 uvm_reg 类中应该对应有类似 value 的成员来表征其对应硬件寄存器的值,然而并没有。 要知道, uvm_reg 并不是寄存器模型的最小切分单元, uvm_reg_ field 才是。所以, uvm_reg 可以理解为 uvm_reg_field 的容器, 一个 uvm_reg 可以包含多个顺序排列的uvm _reg_ field。在取值时,用户可以使用 uvm_reg _ field 的成员 value 直接访问,但笔者更建议使用 uvm_reg 类和 uvm_reg_field 类都具备的接口函数 get_mirrored_ value()。
功能覆盖率的实现
在测试寄存器以及设计的某些功能配置模式时, 需要统计测试过的配置情况。就 MCDF寄存器模型来看,除了测试寄存器本身, 还要考虑在不同的配嚣模式下设计的数据处理、 仲裁等功能是否正确, 所以我们需要放置功能覆盖率 covergroup 在寄存器模型中。 由于寄存器描述文件的结构化, 我们可以通过扩充寄存器模型生成器 (register model generator) 的功能, 使得生成的寄存器模型自动包含各个寄存器域的功能覆盖率。UVM 的寄存器模型已经内置了一些方法用来使能对应的 covergroup, 同时在调用 write()或 read() 方法时自动调用covergroup: :sample()来完成功能覆盖率收集。接下来我们给出两种可供选择的方式来实现寄存器功能覆盖率收集。
1. 内部自动收集模式
如果寄存器模型生成器可以一并生成 covergroup 和对应的方法, 我们就可以考虑是否例化这些 covergroup 以及何时收集这些数据。从例码中摘出的 ctrl_reg 寄存器扩充的定义部分来看, value_cg 是用来收集寄存器中所有的域(包含 reserved 只读区域), 而要例化 value_cg以及何时采集数据, 我们需要在实现过程中考虑下面几点:
• covergroup 在此模式下可以自动生成, 在使能的情况下可以在每次 read()、 write()方法后调用。从例化时的内存消耗以及每次采集时的内存消耗, 从上百个寄存器内置的covergroup 联动的情况出发, 是否例化、 是否使能采样数据都需要考虑。这里给出的建议是, 在验证前期,可以不例化 covergroup, 保证更好的资源利用;在验证后期需要采集功能覆盖率时, 再考虑例化、 使能采样。
• 上面讲到的例化,在 ctrl_reg 的构建函数中,通过 has_coverage()来判断是否需要例化的。该方法会查询成员 ctrl_reg::m_has_cover, 是否具备特定的覆盖率类型, 而该成员在例化时, 已经赋予了初值 UVM_CVR_ALL, 即包含所有覆盖率类型, 因此, value_cg 可以例化。
• 在新扩充的 sample()和 sample_values()两个方法时, 用户也需要注意。sample()可以理解为 read()、 write()方法的回调函数, 用户需要填充该方法,使得可以保证自动采样数据。sample_ values()是供用户外部调用的方法,一些特定事件触发时, 例如中断、 复位等场景, 用户可以在外部通过监听具体事件来调用该方法, 即 ctrl_reg::sampel_ values()。
• 在 sample_values()方法中, 可以通过 get_coverage()方法判断是否允许进行覆盖率采样。用户可能容易将 has_coverage()与 get_coverage()等方法混淆, 就这两个方法而言,前者指是否具备对应的 covergroup, 后者指是否允许使用对应的 covergroup 进行采样。
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg}
uvm_reg_field reserved;
rand uvm_reg_field pkt_len;
rand uvm_reg_field prio_level;
rand uvm_reg_field chnl_en;
covergroup value_cg;
option.per_instance = 1;
reserved: coverpoint reserved.value[25:0];
pkt_len: coverpoint pkt_len.value[2:0];
prio_level: coverpoint prio_level.value[l:0);
chnl_en: coverpoint chnl_en.value[0:0);
endgroup
function new(string name = "ctrl_reg");
super.new(name, 32, UVM CVR ALL};
set_coverage(UVM CVR FIELD VALS);
if(has_coverage(UVM_CVR_FIELD_VALS)) begin
value_cg = new();
end
endfunction
virtual function build();
reserved = uvm_reg_field::type_id::create("reserved");
pkg_len= uvm_reg_field::type_id::create("pkg_len");
prio_level = uvm_reg_field::type_id::create("prio_level")
chnl_en = uvm_reg_field::type_id::create("chnl_en");
reserved.configure(this, 26, 6, "RO", 0, 26'h0, 1, 0, 0);
pkt_len.configure(this, 3, 3, "RW" , 0, 3'h0, 1, 1, 0);
prio_level.configure(this, 2, 1, "RW" , 0, 2'h3, 1, 1, 0);
chnl_en.configure(this, 1, 0, "RW" , 0, 1'hl, 1, 1, 0} ;
endfunction
function void sample(
uvm_reg_data_t data, uvm_reg_data_t byte_en,
bit is_read,
uvm_reg_map map);
super.sample(data, byte_en, is_read, map);
sample_values (};
endfunction
function void sample_values();
super. sample_values (};
if (get_coverage(UVM CVR FIELD VALS)) begin
value_cg.sample(};
end
endfunction
endclass
借助于寄存器描述文件的良好格式, 寄存器模型生成器通过模板的形式来生成可以控制例化和采样的寄存器模型。 用户在开发寄存器模型生成器时, 也考虑是否通过上述参考的变量来控制, 或者在生成时导入一些特定的编译导向 (compiler directive), 例如'ifndef 的语句块来判断是否需要将寄存器的 covergroup 进行编译。 这些方法的目的都是为了更灵活的选择是否需要例化 covergroup 并采样数据, 以此来保证仿真性能。
2.事件触发外部收集模式
自动收集覆盖率的形式不够灵活, 而且不是很贴合实际场景。 不灵活的地方在于, 它默认会采样所有的域, 包括那些保留域 (reserved field), 又或者对某一个域为 2 位时, 它会自动分配 bin_0、 bin_1、 bin_2 和 bin_3 来对应4个可能的值,殊不知可能 val_3 是违法的 (illegal bin), 又或者采样上述的状态寄存器中的域 fifo_avail[7:0], 那么是否要采集从0 到 63 的所有可能值才能保证此域的完备性呢?另外, 不贴合实际场景的地方在于, 它不够 “ 智能 ”,无法组合出更有意义的运用场景, 例如在实际场景下, 我们需要考虑3个通道是否同时使能、 同时关闭、 又或者有的使能有的关闭这些组合情形呢? 3个通道在使能时, 是否考虑到了不同优先级、 相同优先级、 又或者两个通道相同优先级、 一个通道不同优先级的情况呢?这些场景, 我们更应该在后期测试时考虑是否覆盖到。 因此笔者建议, 更贴合实际的、 可作为覆盖率验收标准的 covergroup 定义还当采取自定义的形式, 一方面来限定感兴趣的域和值、 一方面来指定感兴趣的采样事件, 即使用合适的事件来触发采样, 通过这种方式, 最后可以完成寄存器功能覆盖率的验证完备性标准。
下面我们就之前的例码, 自定义一个覆盖率类, 其中嵌入了对应的 covergroup, 以及指定的采样事件。这段例码中, 定义了类 mcdf_coverage, 继承于 uvm_subscriber, 这么做的便利在于准备了一副 “ 耳朵 ” 来订阅从其他地方传来的信息。这里的信息稍后来自于mcdf _bus_ monitor, 接收信息的方式也将通过 mcdf_bus_ monitor 的 uvm_ analysis_port 发往 mcdf _ coverage 的 uvm_ analysis_ export 。 在 mcdf_coverage: :reg_ value_ cg 的覆盖率定义中, 不但指定了对各个寄存器感兴趣的域和值范围,也将各个相关的 coverpoint 进行 cross 组合,构成更复杂的场景实现要求。在覆盖率的采集事件中, 我们利用了 mcdf_bus_ monitor 监听到的前门访问读写事件来作为触发事件, 对数据进行采样。 当然, 采样方式也可以通过内置其他的同步组件来扩展, 例如利用 uvm_event 在中断等特殊事件发生时进行采样。
class mcdf_coverage extends uvm_subscriber #(mcdf_bus_trans);
mcdf_rgm rgm;
`uvm_component_utils(mcdf_coverage)
covergroup reg_value_cg;
option.per_instance = 1;
CHOLEN: coverpoint rgm.chnl0_ctrl_reg.pkt_len.value[2:0] {bins len[) = {0, 1, 2, 3, [4:7));}
CHOPRI: coverpoint rgm.chnl0_ctrl_reg.prio_level.value[l:0];
CHOCEN: coverpoint rgm.chnl0_ctrl_reg.chnl_en.value[0:0);
CHlLEN: coverpoint rgm.chnll_ctrl_reg.pkt_len.value[2:0] {bins len[) =(0, 1, 2, 3, [4:7]};}
CHlPRI: coverpoint rgm.chnll_ctrl_reg.prio_level.value[l:0];
CHlCEN: coverpoint rgm.chnll_ctrl_reg.chnl_en.value[0:0];
CH2LEN: coverpoint rgm.chn12_ctrl_reg.pkt_len.value[2:0] {bins len[]={O, 1, 2, 3, (4:7]);)
CH2PRI: coverpoint rgm.chn12_ctrl_reg.prio_level.value[l:0];
CH2CEN: coverpoint rgm.chnl2_ctrl_reg.chnl_en.value[0:0];
CHOAVL: coverpoint rgm.chnl0_stat_reg.fifo_avail.value[7:0] {bins
avail[] = {0, 1, [2:7), [8:55], [56:61), 62, 63);]
CHlAVL: coverpoint rgm.chnll_stat_reg.fifo_avail.value[7:0] {bins
avail(]= {0, 1, [2:7), [8:55], [56:61], 62, 63);)
CH2AVL: coverpoint rgm.chn12_stat_reg.fifo_avail.value[7:0] {bins
avail[]= {0, 1, [2:7), [8:55], [56:61], 62, 63);)
LEN_COMB: cross CHOLEN, CHlLEN, CH2LEN;
PRI_COMB: cross CHOPRI, CHlPRI, CH2PRI;
CEN_COMB: cross CHOCEN, CHlCEN, CH2CEN;
AVL_COMB: cross CHOAVL, CHlAVL, CH2AVL;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
reg_value_cg = new();
endfunction
function void build_phase(uvm_phase phase);
if(!uvm_config_db#(mcdf_rgm)::get(this, "", "rgm", rgm)) begin
`uvm_info("GETRGM", "no top-down RGM handle is assigned", UVM_LOW) end
endfunction
function void write(T t);
reg_value_cg. sample();
endfunction
endclass