项目梳理
MCDF功能描述
从上图的MCDF结构来看主要可以分为如下几个部分:
- 上行数据的通道从端(Channel Slave),负责接收上行数据,并且存储到其FIFO中。
- 仲裁器(Arbiter)可以选择从不同的FIFO中读取数据,进而将数据进一步传送至整形器(formatter)。
- 整形器(Formatter)将数据按照一定的接口时序送出至下行接收端。
- 控制寄存器(Control Registers)有专用的寄存器读写接口,负责接收命令并且对MCDF的功能做出修改。
接口描述
1、系统信号接口
CLK(0):时钟信号。
RSTN(0):复位信号,低位有效。
2、通道从端接口
CHx_DATA(31:0):通道数据输入。
CHx_VALID(0):通道数据有效标志信号,高位有效。
CHx_READY(0):通道数据接收信号,高位表示接收成功。
3、整形器接口
FMT_CHID(1:0):整形数据包的通道ID号。
FMT_LENGTH(4:0):整形数据包长度信号。
FMT_REQ(0):整形数据包发送请求。
FMT_GRANT(0):整形数据包被允许发送的接受标示。
FMT_DATA(31:0):数据输出端口。
FMT_START(0):数据包起始标示。
FMT_END(0):数据包结束标示。
4、控制寄存器接口
CMD(1:0):寄存器读写命令。
CMD_ADDR(7:0):寄存器地址。
CMD_DATA_IN(31:0):寄存器写入数据。
CMD_DATA_OUT(31:0):寄存器读出数据。
接口时序
通道从端接口时序
当valid为高时,表示要写入数据。如果该时钟周期ready为高,则表示已经将数据写入;如果该时钟周期ready为低,则需要等到ready为高的时钟周期才可以将数据写入。
整形器接口时序
- 整形器发送数据是按照数据包的形式发送的,可以选择数据包的长度有4、8、16和32。整形器必须完整发送某一个通道的数据包后,才可以转而准备发送下一个数据包,在发送数据包期间,fmt_chid和fmt_length应该保持不变,直到数据包发送完毕。
- 在整形器准备发送数据包时,首先应该将fmt_req置为高,同时等待接收端的fmt_grant。当fmt_grant变为高时,应该在下一个周期将fmt_req置为低。fmt_start也必须在接收到fmt_grant高有效的下一个时钟被置为高,且需要维持一个时钟周期。在fmt_start被置为高有效的同一个周期,数据也开始传送,数据之间不允许有空闲周期,即应该连续发送数据,直到发送完最后一个数据时,fmt_end也应当被置为高并保持一个时钟周期。
- 相邻的数据包之间应该至少有一个时钟周期的空闲,即fmt_end从高位被拉低以后,至少需要经过一个时钟周期,fmt_req才可以被再次置为高。
控制寄存器接口时序
在控制寄存器接口上,需要在每一个时钟解析cmd。当cmd为写指令时,需要把数据cmd_data_in写入到cmd_addr对应的寄存器中;当cmd为读指令时,即需要从cmd_addr对应的寄存器中读取数据,并在下一个周期,将数据驱动至cmd_data_out接口。
寄存器描述
1、地址0x00 通道1控制寄存器 32bits 读写寄存器
bit(0):通道使能信号。1为打开,0位关闭。复位值为1。 bit(2:1):优先级。0为最高,3为最低。复位值为3。
bit(5:3):数据包长度,解码对应表为, 0对应长度4,1对应长度8,2对应长度16,3对应长度32,其它数值(4-7)均暂时对应长度32。复位值为0。
bit(31:6):保留位,无法写入。复位值为0。
2、地址0x04 通道2控制寄存器 32bits 读写寄存器
同通道1控制寄存器描述。
3、地址0x08 通道3控制寄存器 32bits 读写寄存器
同通道1控制寄存器描述。
4、地址0x10 通道1状态寄存器 32bits 只读寄存器
bit(7:0):上行数据从端FIFO的可写余量,同FIFO的数据余量保持同步变化。复位值为FIFO的深度数。
bit(31:8):保留位,复位值为0。
5、地址0x14 通道2状态寄存器 32bits 只读寄存器
同通道1状态寄存器描述。
6、地址0x18 通道3状态寄存器 32bits 只读寄存器
同通道1状态寄存器描述。
实验3-1:SystemVerilog的真正引入
问1:generator中的run方法规定了执行次数,initiator方法未规定执行次数,test类中的如何finish()?
答:agent封装类中的run方法在调用chnl_generator和chnl_initiator的run方法时,用的是fork-join_any块,即只要数据发送完run方法结束
1 | fork |
问2:如何保证generator生成一个,initiator发送一个?
答:通过mailbox建立的握手行为,initiator执行get完才能写,generator执行完get才能进入下一次生成数据的循环
问3:如何通过指令控制仿真环境
重启仿真:”restart”
生成随机数(改变随机种子):
“vsim -novopt -solvefaildebug -sv_seed 0 work.tb1”
“vsim -novopt -solvefaildebug -sv seed random work.tb1”,每次
- 参数solvefaildebug:随机化失败时会提示错误信息
- 参数0:随机种子编号,通过改变随机种子即可以改变每次生成的随机值
添加参数并通过参数判断是否执行语句:
“+TESTNAME=testname”
文件结构
包chnl_pck
class chnl_trans:定义激励数据的格式(数据包间隔,数据间隔,通道id,数据包id)
初始化:记录创建了多少个对象
constraint cstr:
通过约束+randomize()定义数据的初始值(被generator调用)
function chnl_trans clone():
对实例化对象进行复制!!(无需通过new+参数初始化)
class chnl_initiator:构建和dut、generator相连的初始化模型(驱动器)
初始化:命名
- 属性变量只要interface(沟通dut)和name,还有interface
set_interface(virtual chnl_intf intf):
传入实际的interface,给interface句柄赋值
driver():
从generator、get数据,然后执行chnl_write(input chnl_trans t)将数据发给dut,因为write方法内有时钟和reday信号进行阻塞,所以必须等到write方法执行完后才能用mailbox的put,这样generator就被mailbox的get阻塞,无法进行下一次的数据发送
通过mailbox和generator进行数据交换,先调用mailbox的get方法,从generator获取数据
chnl_write(input chnl_trans t):
和dut进行数据交流
身为驱动类的必备功能,给dut传数据
chnl_idle():
将valid和data置0,结合数据包间隔,数据间隔,给数据传输添加空窗期
run():
执行driver()
class chnl_generator:生成数据并发送给initiator
初始化:(1)在此处对和initiatot交流的mailbox进行实例化;(2)对数据包id,通道id和ntrans赋初值;
- 属性变量包括:通道id,数据包id,对chnl_trans的随机化对象进行约束;ntrans决定数据包的发送次数;
send_trans():
对应initiator的driver()方法
- 此处实例化chnl_trans的req和rsp两个对象,req随机化成功( this.pkt_id++;),发送给mailbox(也就是initiator),initiator克隆一份后将rsp中的rsp变量修改为1,rsp传回generator后通过rsp属性值判断,作为此次发送是否成功的断言!!!失败则打印
run():
执行send_trans()
class chnl_agent:对chnl_generator和chnl_initiator进行封装,将对外接口进一步优化
- 初始化:传入chnl_generator和chnl_initiator需要的参数
- set_interface(virtual chnl_intf vif):给chnl_initiator的set_interface方法传入参数
- run():将generator和initiator的run方法进行封装,同时用句柄赋值的方式,让generator和initiator的mailbox指向同一个地址
chnl_root_test:最顶层封装,tb文件直接调用的对象,创建3个agent,调用run和set_interface方法
- 初始化:创建3个agent
tb文件
interface:
1
2
3
4
5
6
7
8
9
10
11interface chnl_intf(input clk, input rstn);
logic [31:0] ch_data;
logic ch_valid;
logic ch_ready;
logic [ 5:0] ch_margin;
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
output ch_data, ch_valid;
input ch_ready, ch_margin;
endclocking
endinterfacemodule:
dut的输入输出连接
分配时钟和复位信号
启动test(导包,声明接口,声明句柄并实例化,调用run方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
module tb1;
logic clk;
logic rstn;
logic [31:0] mcdt_data;
logic mcdt_val;
logic [ 1:0] mcdt_id;
mcdt dut(
.clk_i (clk )
,.rstn_i (rstn )
,.ch0_data_i (chnl0_if.ch_data )
,.ch0_valid_i (chnl0_if.ch_valid )
,.ch0_ready_o (chnl0_if.ch_ready )
,.ch0_margin_o(chnl0_if.ch_margin )
,.ch1_data_i (chnl1_if.ch_data )
,.ch1_valid_i (chnl1_if.ch_valid )
,.ch1_ready_o (chnl1_if.ch_ready )
,.ch1_margin_o(chnl1_if.ch_margin )
,.ch2_data_i (chnl2_if.ch_data )
,.ch2_valid_i (chnl2_if.ch_valid )
,.ch2_ready_o (chnl2_if.ch_ready )
,.ch2_margin_o(chnl2_if.ch_margin )
,.mcdt_data_o (mcdt_data )
,.mcdt_val_o (mcdt_val )
,.mcdt_id_o (mcdt_id )
);
// clock generation
initial begin
clk <= 0;
forever begin
#5 clk <= !clk;
end
end
// reset trigger
initial begin
#10 rstn <= 0;
repeat(10) @(posedge clk);
rstn <= 1;
end
实验3-2:generator提取
爲了更方便的控制generator生成数据,将generator从agent中拿出,直接由test控制
文件变化
vsim+TESTNAME = chnl_burst_test work.tb2
包chnl_pck
chnl_generator:
属性变量连原本由chnl_trans控制的data_size,pkt_nidles也加入了进来,目的也是让test可以完全掌握数据生成
通过一层层的约束
注意:为什么随机化里面用的是local::而不用this?
local指的是generator 的实例对象,如果在这里使用this,指的是调用randomsize方法的数据实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23rand int pkt_id = -1;
rand int ch_id = -1;
rand int data_nidles = -1;
rand int pkt_nidles = -1;
rand int data_size = -1;
rand int ntrans = 10;
constraint cstr{
soft ch_id == -1;
soft pkt_id == -1;
soft data_size == -1;
soft data_nidles == -1;
soft pkt_nidles == -1;
soft ntrans == 10;
}
task send_trans();
chnl_trans req, rsp;
req = new();
assert(req.randomize with {local::ch_id >= 0 -> ch_id == local::ch_id;
local::pkt_id >= 0 -> pkt_id == local::pkt_id;
local::data_nidles >= 0 -> data_nidles == local::data_nidles;
local::pkt_nidles >= 0 -> pkt_nidles == local::pkt_nidles;
local::data_size >0 -> data.size() == local::data_size;
})class chnl_agent:将generator摘除
class chnl_root_test:变化最大
初始化:agent和generator分别创建3个
因为generator从agent中提出,建立initiator和generator的mailbox连接的任务交由test完成
1
2
3
4
5
6
7
8
9
10
11
12function new(string name = "chnl_root_test");
foreach(agent[i]) begin
this.agent[i] = new($sformatf("chnl_agent%0d",i));
this.gen[i] = new();
// USER TODO 2.1
// Connect the mailboxes handles of gen[i] and agent[i].init
agent[i].init.req_mb = gen[i].req_mb;
agent[i].init.rsp_mb = gen[i].rsp_mb;
end
this.name = name;
$display("%s instantiate objects", this.name);
endfunctiondo_config():对创建的3个generator实例进行随机化(赋予数据),对比3-1中在generator中的随机化,明显对外暴露了更多接口可以自定义(只需在test中修改)
1
2assert(gen[0].randomize() with {ntrans==100; data_nidles==0; pkt_nidles==1; data_size==8;})
assert(req.randomize with {ch_id == local::ch_id; pkt_id == local::pkt_id;})run():同样因为gen和init的分离,run方法如何执行和结束也是一个问题
1
2
3
4
5
6
7
8
9
10
11this.do_config();
fork
agent[0].run();
agent[1].run();
agent[2].run();
join_none
fork
gen[0].run();
gen[1].run();
gen[2].run();
join
实验3-3:增加新组件monitor和checker
monitor和checker干了嘛?
monitor: 分为chnl_monitor监控输入和mcdt_monitor监控输出
如何保证采集数有效:就是允许数据传输的条件
1
2
3
4
5
6
7
8
9//chnl
@(posedge intf.clk iff (intf.mon_ck.ch_valid==='b1 && intf.mon_ck.ch_ready==='b1));
m.data = intf.mon_ck.ch_data;
mon_mb.put(m);
//mcdt
@(posedge intf.clk iff intf.mon_ck.mcdt_val==='b1);
m.data = intf.mon_ck.mcdt_data;
m.id = intf.mon_ck.mcdt_id;
mon_mb.put(m);monitor的run方法:就是为了将采集数据放入mailbox
checker:run方法就是从monitor发送的mailbox中获取数据,比较chnl的mailbox和mcdt的mailbox的数据是否一致
问1:chnl_monitor和mcdt_monitor的数据对比如何保证数据的顺序是对应的?
两者的mailbox保证了上下的数据都是进到队列里面,然后从头部拿出比较,一方为空则比较,或者说checker的run就无法执行
问2:不用$finish()了,如何结束的进程
结束整个进程:test自定义
run_stop_callback()
方法。方法内部通过获取3把钥匙进行阻塞,每有一个gen执行完发送,put一个钥匙在整个文件的最开始建立旗语,
semaphore run_stop_flags = new();
在generator中每有一个发送了ntrans个数据包截止的时候put进去一个钥匙,当三个gen都执行完,run_stop_flags获得三把钥匙,执行$finish(),归根结底还是用的finishfull_test中结束generator:先看一下full_test中给gen赋值的随机化约束:ntrans是不定值
1
2assert(gen[0].randomize() with {ntrans inside {[1000:2000]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[0] randomization failure!");
问3:mailbox有几个,在哪些组件间建立连接
答:共有6个
4个被checker和monitor持有,3个是agent的monitor和checker之间建立的连接,1个是mcdt的monitor和checker的连接
2个被gen和init持有,用于两者的握手协议
注意: 可以看到monitor的数据
mon_data_t
声明非常简单,其和checker的数据传输单纯就是一个存储的队列同样在root_test中建立连接
1
2
3
4
5
6
7
8
9foreach(agents[i]) begin
this.agents[i] = new($sformatf("chnl_agent%0d",i));
this.gen[i] = new();
// USER TODO 2.1
// Connect the mailboxes handles of gen[i] and agents[i].init
this.agents[i].init.req_mb = this.gen[i].req_mb;
this.agents[i].init.rsp_mb = this.gen[i].rsp_mb;
this.agents[i].mon.mon_mb = this.chker.in_mbs[i];
end
新加入文件结构
包chnl_pack
typedef struct packed:
低配版的chnl_trans,作为monitor里的数据容器
class chnl_monitor:获取dut输入,发送给checker
属性:name,interface,mailbox(未实例化)
set_interface:传入接口
mon_trans:发mail给ckecker
@(posedge intf.clk iff (intf.mon_ck.ch_valid==='b1 && intf.mon_ck.ch_ready==='b1))
保证在数据传输的时候进行采样;
并且将数据线上的数据装盒,放入mailbox
class mcdt_monitor:获取dut输出,发送给checker
class chnl_agent更新:将chnl_monitor加入,run方法中加入monitor.run();
class chnl_checker:
- 属性:name,error_count,cmp_count,mailbox
- 初始化:对邮箱实例化,错误和完成计数初始化
- 注意:chnl_monitor有3个,需创建3个mailbox与之对应,监控3个chnl
- do_compare():这里的阻塞
chnl_root_test:作为所有测试类的基类
功能:
- 初始化:实例化各部分组件,连接mailbox(generator和driver的连接,monitor和checker的连接)
- 虚方法(do_config):执行随机化,可以通过断言判断随机化是否成功(在子类中实现)
chnl_basic_test:实现随机化的config方法
1
2
3
4
5
6
7
8
9
10
11virtual function void do_config();
super.do_config();
assert(gen[0].randomize() with {ntrans==100; data_nidles==0; pkt_nidles==1; data_size==8;})
else $fatal("[RNDFAIL] gen[0] randomization failure!");
assert(gen[1].randomize() with {ntrans==50; data_nidles inside {[1:2]}; pkt_nidles inside {[3:5]}; data_size==6;})
else $fatal("[RNDFAIL] gen[1] randomization failure!");
assert(gen[2].randomize() with {ntrans==80; data_nidles inside {[0:1]}; pkt_nidles inside {[1:2]}; data_size==32;})
else $fatal("[RNDFAIL] gen[2] randomization failure!");
endfunctionchnl_burst_test:随机化存在差别,
1
2
3
4
5
6
7
8
9virtual function void do_config();
super.do_config();
assert(gen[0].randomize() with {ntrans inside {[80:100]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[0] randomization failure!");
assert(gen[1].randomize() with {ntrans inside {[80:100]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[1] randomization failure!");
assert(gen[2].randomize() with {ntrans inside {[80:100]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[2] randomization failure!");
endfunctionchnl_fifo_full_test:大数据量冲击
1
2
3
4
5
6
7
8
9virtual function void do_config();
super.do_config();
assert(gen[0].randomize() with {ntrans inside {[1000:2000]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[0] randomization failure!");
assert(gen[1].randomize() with {ntrans inside {[1000:2000]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[1] randomization failure!");
assert(gen[2].randomize() with {ntrans inside {[1000:2000]}; data_nidles==0; pkt_nidles==1; data_size inside {8, 16, 32};})
else $fatal("[RNDFAIL] gen[2] randomization failure!");
endfunction
tb文件
优化:通过外部指令决定执行那个类
核心知识点:
- 字符串索引的关联数组:类似哈希表,不过是将key变成了数组下标,实现字符串到数组元素的映射
- 将3个实例化的test模块放入数组,通过命令键入的字符串取出,用父类test接收,决定执行那个test的方法!!!
1 | import chnl_pkg3::*; |
实验4:新增结构
问1:param_def.v
如图:将reg,formatter拿入考量
文件结构
param_def.v
通过宏('define
)对部分参数进行定义,硬件部分使用
- reg输入、输出:
- 地址位宽
- 数据位宽
- 3个命令(对应2位2进制)
- 6个寄存器地址
- 6个寄存器编号
- chnl:(输入reg的状态寄存器)
- slave_fifo的余量位宽:`define FIFO_MARGIN_WIDTH
- 优先级:(输出,给arbiter)
- 数据包长度:(输出,给formatter)
1 |
1. 包chnl_pack(基层包,无需导包)
变化:结构更加规范:
- class mcdt_monitor,class chnl_checker还有test均从该包拿出;
- 仅保留和chnl相关的功能,即生成激励gen,发送数据driver(initiator改名),监控数据monitor;
- driver存在些许改变,增加reset方法对复位信号做出响应
interface
和硬件交流的信号
1 | interface chnl_intf(input clk, input rstn); |
transcation
driver需要的数据:只有data是被硬件需要的,其他的ch_id,pkt_id是所在通道和包的信息,data_nidles,pkt_nidles是对间隔时间的配置
这里的clone()和sprint()方法,在UVM中可以通过自动化域的方法取代
1 | class chnl_trans; |
driver
核心功能:
从generator接收激励:mailbox,do_driver();
对接口进行驱动:do_driver{mailbox.get(REQ);chnl_write();mailbox.put(RSP)}
将mailbox中拿出的trans类型的req,把数据放到接口上,vaild信号置为1;然后等待ready信号来就可以执行idle把数据关闭了
复位:独立的监控线程,一旦检测复位信号,vailid和data信号拉低
1 | class chnl_driver; |
generator
成员变量和transcation基本相同,无data数组,改为data.size;
generator的随机化约束只是为了使其内部随机化无法生效,需要外部控制
核心功能:send_trans(),实例化trans类型变量req,对req做transcation的随机化,然后将req传入mailbox,发送给driver!!
1 | class chnl_generator; |
monitor
定义monitor传输的数据类型,此时没有trans那么复杂了,因为只需要验证数据的一致性
1 | typedef struct packed { |
monitor组件
同样需要传入接口,因为要从接口上拿数据
核心方法:mon_trans()
当vaild和ready信号都有效,即真正进行数据传输时,采集interface上的时钟块中的data!!!
1 | class chnl_monitor; |
agent
整个chnl_pkg的顶层封装!!!实例化driver,monitor
核心方法run():调用driver和monitor的run()方法,两者并行执行
1 | class chnl_agent; |
2. package rpt_pkg(report工具包)
梳理下功能,然后再看下在test中是如何被调用的
枚举类型:
- 信息类型
- 通过权重对输出的信息进行过滤
- 通过action选择选择执行步骤
1
2
3typedef enum {INFO, WARNING, ERROR, FATAL} report_t;
typedef enum {LOW, MEDIUM, HIGH, TOP} severity_t;
typedef enum {LOG, STOP, EXIT} action_t;设置初始值:
- 权重为low时,表示最低一级的消息都被记录;后期验证次数多了之后,功能逐渐完善,就可以将过滤等级调高
- 文件名,将日志导出
- 对各条信息的输出次数进行计数
1
2
3
4
5
6static severity_t svrt = LOW;
static string logname = "report.log";
static int info_count = 0;
static int warning_count = 0;
static int error_count = 0;
static int fatal_count = 0;最关键的一点:信息收集方法
rpt_msg()
,在do_report()
方法中被调用,看下调用时传入参数的含义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36function void rpt_msg(string src, string i, report_t r=INFO, severity_t s=LOW, action_t a=LOG);
integer logf;
string msg;
case(r)
INFO: info_count++;
WARNING: warning_count++;
ERROR: error_count++;
FATAL: fatal_count++;
endcase
if(s >= svrt) begin
msg = $sformatf("@%0t [%s] %s : %s", $time, r, src, i);
logf = $fopen(logname, "a+");
$display(msg);
$fwrite(logf, $sformatf("%s\n", msg));
$fclose(logf);
if(a == STOP) begin
$stop();
end
else if(a == EXIT) begin
$finish();
end
end
endfunction
function void do_report();
string s;
s = "\n---------------------------------------------------------------\n";
s = {s, "REPORT SUMMARY\n"};
s = {s, $sformatf("info count: %0d \n", info_count)};
s = {s, $sformatf("warning count: %0d \n", warning_count)};
s = {s, $sformatf("error count: %0d \n", error_count)};
s = {s, $sformatf("fatal count: %0d \n", fatal_count)};
s = {s, "---------------------------------------------------------------\n"};
rpt_msg("[REPORT]", s, rpt_pkg::INFO, rpt_pkg::TOP);
endfunctionsrc:提前声明的默认值,实际使用时传入执行者(调用他的类,如checker或agent等)
1
rpt_pkg::rpt_msg($sformatf("[%s]",this.name), s, rpt_pkg::INFO, rpt_pkg::TOP);
当然,也可以传入一下象征意义的字符串,如”[CMPFAIL]”,”[CMPSUCD]”等
i:见do_report的s,对各类型的信息进行计数;
也可以传入自己定义的字符串,如checker中的report;
report_t r:通过case语句,给相应的信息类型计数++
severity_t s:筛选等级,和定义好的svrt进行比较,比它大才能执行后面的打印信息,导出日志等操作
action_t:直接默认就好,此时只执行导出日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function void do_report();
string s;
s = "\n---------------------------------------------------------------\n";
s = {s, "CHECKER SUMMARY \n"};
s = {s, $sformatf("total comparison count: %0d \n", this.total_count)};
foreach(this.chnl_count[i]) s = {s, $sformatf(" channel[%0d] comparison count: %0d \n", i, this.chnl_count[i])};
s = {s, $sformatf("total error count: %0d \n", this.err_count)};
foreach(this.chnl_mbs[i]) begin
if(this.chnl_mbs[i].num() != 0)
s = {s, $sformatf("WARNING:: chnl_mbs[%0d] is not empty! size = %0d \n", i, this.chnl_mbs[i].num())};
end
if(this.fmt_mb.num() != 0)
s = {s, $sformatf("WARNING:: fmt_mb is not empty! size = %0d \n", this.fmt_mb.num())};
s = {s, "---------------------------------------------------------------\n"};
rpt_pkg::rpt_msg($sformatf("[%s]",this.name), s, rpt_pkg::INFO, rpt_pkg::TOP);
endfunction
3. package reg_pkg(导包:`include “param_def.v”)
组件结构和chnl_pkg很像
问:
1.为什么do_writer方法中,传入的trans中cmd == `READ,也就是执行READ时,要等待两个下降沿再获取数据???
答:上升沿来,将命令cmd和地址addr放到interface上;下一个上升沿来,寄存器reg获取到cmd和addr,将内部数据放到interface上;之后的下降沿来,就可以把interface上的data拿走了
interface
cmd:读、写或等待命令
cmd_addr:寄存器地址
cmd_data_s2m:写入读写寄存器的配置信息
cmd_data_m2s:从读寄存器中读出salve_fifo余量
1 | interface reg_intf(input clk, input rstn); |
transcation
不需要id,idle和重复产生激励,所以只生成需要的信号
注意随机化中:对约束增加了关系限制,不允许不合理的激励传入,否则报错
1 | class reg_trans; |
driver
核心方法:do_drive()——reg_write()+mailbox传激励
- 写方法,将地址,命令,数据放在接口上
- 读方法,将地址,命令,放在接口上,下个时钟的下降沿从接口上拿数据(
t.data <= intf.cmd_data_s2m;
)
不需要等待ready
generator是在时钟下降沿判断ready信号;
reg是在第二个时钟下降沿采集寄存器中的数据;读命令的时候要把读到的寄存器状态放入rsp
1 | class reg_driver; |
generator
核心方法:send_trans(),通chnl的send_trans
区别:cmd是read的时候,driver会把rsp中的数据改为读到的数据再发给generator,generator的组件实例可以将其获取到
1 | class reg_generator; |
monitor
核心方法:mon_trans():将接口上的cmd,addr,data都放入(reg_trans)m,然后将m放入mailbox
区别于chnl,没有定义monitor的数据类型,直接使用reg_trans
chnl:时钟上升沿+valid===1+ready===1
reg:时钟上升沿+复位(==1)+intf.mon.clk != IDLE
1 | class reg_monitor; |
agent
省略
4. package fmt_pkg
interface
忽然变复杂,信号有些多,理清输入输出!!
1 | interface fmt_intf(input clk, input rstn); |
enum
1 | typedef enum {SHORT_FIFO, MED_FIFO, LONG_FIFO, ULTRA_FIFO} fmt_fifo_t; |
本质上对fmt的驱动,就是模拟一个fifo,这里对模拟的fifo进行自定义的范围
transcation
只关心length,data,通道id,接口中的响应信号在driver中见
不需要对这3这进行随机化,本身就是接收数据的容器,不用产生激励,只需要对容器多变变形状
多了一个比较方法,因为最后checker中还是对数据包的对比,采用fmt_trans的类型,所以直接在这里定义比较方法
1 | class fmt_trans; |
driver
用mailbox充当接收数据的fifo,定义fifo的宽度(32位),定义fifo的深度(fifo_bound),定义fifo的数据消耗速度(data_consum_peroid,1代表一个时钟周期就消耗掉一个data),以上变量可进行随机化。
fifo_bound:对应trans的fmt_fifo_t fifo;
data_consum_peroid:对应trans的fmt_bandwidth_t bandwidth;
核心方法:do_run()
this.do_receive():forever监控线程,条件满足时将数据放入fifo
@(posedge intf.fmt_req)
:fmt提出发送数据forever判断:fifo深度的余量是否大于length,小于则一直循环,大于往下执行(给fmt一个grant==1)
一旦检测到start上升沿,开始数据传输(length个下降沿,将接口上的数据放到fifo中),同时将grant拉低(fork-join_none)
this.do_config():同样是forever监控线程,对自建的fifo进行揉捏
- generator可以对宽度和深度进行随机化,再通过mailbox发送到driver
this.do_consume():try_get()从自建fifo中取数,取数的速度由随机方法定义
repeat($urandom_range(1, this.data_consum_peroid)) @(posedge intf.clk);
将buffer中的信号导出(减小this.fifo.num(),这样才有空放新数):do_consume()
- 消耗和接收是并行的 ,所以消耗的速度不一定和接收速度一样就得是1个周期完成,消耗的慢接收就不执行呗
1 | class fmt_driver; |
generator
这里的激励就不是数据了,就是对buffer的配置
核心方法:send_trans()
把fifo的配置信息(fifo,bandwidth)发送给driver,driver用do_config接收,对自建fifo进行揉捏
1 | class fmt_generator; |
monitor
核心方法:mon_trans()
采样时机:@(posedge intf.mon_ck.fmt_start); 要把接口上的(ch_id,length,data)都放进fmt_trans,data是length个时钟上升沿,将接口上的data放入m的data数组
1 | class fmt_monitor; |
agent
省略
5. package mcdf_pkg(顶层包)
核心就是checker的实现,和实验3相比,checker多了对寄存器信息的处理
问:
如何将寄存器信息用到数据的比对上?
如上图所示,构建mcdf硬件参考模型(模仿硬件功能),参考模型通过reg_mb信箱获取寄存器内容,do_reg_update()实现对寄存器内容的更新,do_packet()实现对数据的打包。
通过寄存器数据中的length,可以将chnl_mb中的数据放到length长度数组中,封装成fmt_trans格式,调用fmt_trans的compare与fmt_mb中的数据报进行对比
如何保证寄存器模型中的数据指令和数据输入刚好对应?寄存器数据由外部写入,真正数据传输的时候对应寄存器的数据如何得到?
三个寄存器模型如何代表dut中的6个寄存器?
RW和R的低位都是0,4,8。通过reg[3:2]就可以把各个寄存器区分
interface
1 | interface mcdf_intf(input clk, input rstn); |
enum+reg_packet
mcdf_reg_t
除了读写寄存器的6位,还有代表读寄存器的8位信号,相当于一个RW寄存器和一个R寄存器集成了一个寄存器模型
1 | typedef struct packed { |
mcdf_refmod
问:优先级怎么没用到??
在checker中搞一个类似fmt的模型,结合寄存器中的配置信息,对三个通道采集到的数据进行打包,之后再和formattor的输出进行比较
此时传入的接口只用到了复位信号
核心方法:run():同样被阻塞,需要等待monitor在mailbox中放值
do_reg_update():forever监控寄存器配置,有就把它get到,把配置信息放入refmod的对应reg数组中
开始的get方法:意味着reg_monitor没检测到数据,该方法被阻塞
从reg_mb中获取寄存器数据,并将其保存下来;地址位对应寄存器模型数组的index
对于读写寄存器:数据的每一位的功能都赋值到寄存器中;
对于读寄存器:将后8位slave_fifo的余量获取到
do_packet():通过reg数组中获取的配置信息,模仿formattor的打包行为,对3个chnl获取的数据进行打包(3个reg分别对应3个chnl),通过reg的id获取到length
数据包中的data数组的长度就为length!!
开始的peek()方法:意味着chnl_monitor没检测到数据,该方法被阻塞
this.get_field_value(id, RW_LEN)==0,对应ot.length = 4;
this.get_field_value(id, RW_LEN)==1,对应ot.length = 8;以此类推至32
get_field_value(int id, mcdf_field_t f):
把寄存器模型中的数据放到枚举类型里面,方便调用
1 | class mcdf_refmod; |
mcdf_checker
接口传入的目的:见下面各check方法
mailbox的创建:
- 沟通monitor:3个chnl(左连3个monitor的mon_mb,右连3个refmod的in_mbs[3]),1个reg(左连monitor,右连refmod),1个fmt(连接monitor,refmod没有)
- 沟通refmod:exp_mbs[3],无需创建,连接out_mbs[3];
核心方法:
new()
- 实例mailbox和refmod,建立邮箱连接
run()
do_channel_disable_check(int id):
用了mcdf的接口,通道的chnl_en信号为0时,如果valid和ready信号仍能为高,打印错误信息
do_arbiter_priority_check();get_slave_id_with_prio()
用了arb的接口,通过优先级获取id,并通过id判断通道是否有效
比较fmt输出和slave输入:do_compare()
声明俩fmt_trans句柄,一个从fmt_mb里拿,一个从exp_mbs里拿,如上图所示;
然后调用fmt_trans本身的比较方法,注意打印信息
1 | class mcdf_checker; |
mcdf_env
把agent,checker融会贯通(chnl_agt,reg_agt,fmt_agt),功能覆盖率收集
核心方法:
new()
实例化组价,建立mailbox连接
run()
所有组件并行run()
1 | class mcdf_env; |
base_test
组件都封好了,env实例化,执行就好
核心方法:
new():
gen的创建,和与env中各个agent的driver的连接
对日志输出文件进行初始化
1
2rpt_pkg::logname = {this.name, "_check.log"};
rpt_pkg::clean_log();run():
fork-join_none:env.run()
每个agent都等待gen的激励才能执行,激励的配置方法在base_test的继承类中实现
两个值是否相同的比较:diff_value(),用于比较寄存器中写入的数据是否正确
reg的三种命令:都是生成激励(随机化)并发送(start()方法),void’()表示此时不返回值
每次执行完写方法后都执行一次读方法获取寄存器的值,依次来判断寄存器的数据读写是否正确
1 | class mcdf_base_test; |
mcdf_data_consistence_basic_test
无需重复定义run方法,只需要把gen的start执行
3个读写reg的gen配置:固定写入命令;并将写入的命令读出,检查是否正确输入
1 | class mcdf_data_consistence_basic_test extends mcdf_base_test; |
mcdf_full_random_test
3个读写reg的gen配置:随机写入命令;并将写入的命令读出,检查是否正确输入
1 | class mcdf_full_random_test extends mcdf_base_test; |
验证文件
1 |
|
实验5:功能覆盖率
coverage工具类
定义在最顶层的test类中
问1:为什么bins的定义要对bin的范围进行拆分,如cmd_addr的采样点的bins定义?
答:如果将所有bin集中在一个bins,则每次有一个数命中即全部命中。拆分之后的覆盖率则需要每个都命中才行
问2:“type_option.weight = 0”声明在coverpoint里的意义何在?
答:即该coverpoint在covergroup中的权重为0,不参与covergroup的最终覆盖率计算,不会生成bin;这也是为了方便cross覆盖率的定义
1. 寄存器读写覆盖
要求读写命令和地址交叉覆盖,此时才能准确的验证读写是否正确
1 | covergroup cg_mcdf_reg_write_read; |
2. 寄存器非法指令测试
包括给非法寄存器地址的写和读;给合法读写寄存器地址写入非法数据;对合法读寄存器写入数据(无所谓非法不非法)
1 | covergroup cg_mcdf_reg_illegal_access; |
3. 通道关闭测试
1 | covergroup cg_channel_disable; |
覆盖率收集合并分析
仿真
1 | vsim -i -classdebug -solvefaildebug -coverage -coverstore ./mti_covdb -testname mcdf_full_random_test -sv_seed random +TESTNAME=mcdf_full_random_test -l mcdf_full_random_test.log work.tb |
- -classdebug,这是为了提供更多的 SV 类调试功能
- -solvefaildebug,这是为了在 SV 随机化失败之后有更多的信息提供出来
- -sv_seed 0,暂时给固定的随机种子 0 ,-sv_seed random则每次的随机种子也是随机生成
- +TESTNAME=mcdf_full_random_test ,这是指定仿真选择的调试
- -| mcdf_full_random _test.log,这是让仿真的记录保存在特定的测试文件名称中
- -coverage:会在仿真时产生代码覆盖率数据,功能覆盖率数据则默认会生成,与此选项无关。
- -coverstore COVERAGE_ STORAGE_ PATH:这个命令是用来在仿真在最后结束时,生成覆盖率数据并且存储到 COVERAGE_ STORAGE_ PATH。你可以自己制定COVERAGE_STORAGE_ PATH,但需要注意路径名中不要包含中文字符。
- -testname. TESTNAME:这个选项是你需要添加本次仿真的 test 名称,你可以使用同+TESTNAME 选项一样的 test 名称。
这样在仿真结束后,将在COVERAGE_STORAGE_PATH 下产生一个覆盖率数据文件”{TESTNAME}_{SV_SEED}.data” 。由于仿真时我们传入的种子是随机值,因此我们每次提交测试,在测试结束后都将产生一个独一无的覆盖率数据。
下图为3个测试用例的运行结果,endstimulation后才会在文件夹中显示data文件
合并覆盖率
运行不同的仿真,或者运行同一个 test,它们都会生成独一无二的数据库。之前统一在 COVERAGE_STORAGE_PATH 下面生成的 xxx.data 覆盖率数据可以通过命令进行合并。
在 Questasim 的仿真窗口中敲入命令:
1 | vcover merge -out ./mti_covdb/merged_coverage.ucdb ./mti_covdb |
注意这里的路径也就是上面.data文件的默认生成路径
这个命令即是将之前产生的若干个 xxx.data 的覆盖率合并在一起,生成一个合并在一起的覆盖率文件。所以,在测试前期提交的测试越多,那么理论上覆盖率的增长也就越明显。
第一次运行full_random_test:
第二次运行full_random_test:
第一次运行mcdf_data_consistence_basic_test:
第二次运行mcdf_data_consistence_basic_test:
执行合并命令后生成ucdb文件:
合并命令后:覆盖率提升,运行次数太少~
接下来,你可以点击 File -> Open 来打开这个合并后的 UCDB 覆盖率数据库(注意选择文件类型 UCDB 就可以看到这个文件了)。打开这个数据库之后,可以发现合并后的数据库要比之前单独提交的任何一个测试在仿真结束时的该次覆盖率都要高。
可以在 covergroups 窗口栏中查看功能覆盖率,也可以在 Analysis 窗口中查看代码覆盖率:
分析覆盖率
可以依旧使用 Questasim 来打开 UCDB 利用工具来查看覆盖率,或者更直观的方式是在打开当前覆盖率数据库的同时,生成 HTML 报告。选择 Tools -> Coverage
Report-> HTML,按照下图所示进行勾选:
之后Questasim 就会生成一份详尽的 HTML 覆盖率文档
UVM-实验1
1.1 工厂的注册、创建和覆盖机制
总结:
1.实例创建:和new相比,通过仓库进行实例创建时,name无法传进去;
object类型实例创建
1 | class object_create extends top; |
执行仿真命令
1 | vsim -novopt -classdebug +UVM_TESTNAME=object_create work.factory_mechanism |
输出结果
component实例创建
1 | class component_create extends top; |
仿真命令:
1 | vsim -novopt -classdebug +UVM_TESTNAME=component_create work.factory_mechanism |
输出结果:
object覆盖类型
1 | class object_override extends object_create; |
仿真语句:
1 | vsim -novopt -classdebug +UVM_TESTNAME=object_override work.factory_mechanism |
结果:
问:trans类型不是被覆盖了吗,执行结果为什么还会创建trans?
答:badtrans的new方法中,首先执行了父类的new方法:执行步骤为:
- object_override的build_phase先执行类型覆盖,此时trans的工厂实例创建方法全部变成bantrans来创建!!!
- 执行父类object_create的的build_phase,进行trans(实际上是bad_trans)的工厂实例化
- badtrans的工厂实例化实际上还是执行badtrans的new方法,此时首先调用父类的new方法,完成trans的实例创建
component覆盖类型
1 | class component_override extends component_create; |
仿真结果:
和object的覆盖方式类似
1.2 域自动化及UVM常用方法
使用域自动化的宏方法
1 |
uvm_object::compare()
方法
比较t1
和t2
是否相等
1 | trans t1, t2; |
全局控制对象
uvm_default_comparer
(uvm_comparer
类型)是默认的UVM全局比较器,show_max可以设置最大比较结果。
1 | uvm_default_comparer.show_max = 10; |
此时可以将自动化域中的所有变量比较完,并通过回调函数do_compare打印错误信息
回调函数
执行compare()
函数会自动调用do_compare()
这个回调函数
1 | function bit do_compare(uvm_object rhs, uvm_comparer comparer); |
uvm_object::print()
方法及uvm_object_copy()
方法
1 | if(!is_equal) |
执行命令:
1 | vsim -novopt -classdebug +UVM_TESTNAME=object_methods_test work.object_methods |
1.3 phase机制
phase机制使得验证环境从组建、连接、执行,得以分阶段执行,按照层次结构和phase顺序严格执行,继而避免一些依赖关系,也使得可以正确地将不同的代码放置到不同的phase块中。
1 | class comp2 extends uvm_component; |
comp3类
1 | class comp3 extends uvm_component; |
comp1
类
comp1
中声明了comp2
、comp3
作为成员变量,并且在build_phase
中做了例化
1 | class comp1 extends uvm_component; |
phase_order_test
类
创建验证结构,uvm_test
一定是顶层结构,而uvm_test
的引擎是uvm_root
。phase_order_test
中包含了comp1 c1
,并且在build_phase
中也创建了c1
1 | class phase_order_test extends uvm_test; |
执行命令
1 | vsim -novopt -classdebug +UVM_TESTNAME=phase_order_test work.phase_order |
仿真结果
总结:
- comp2和comp3和同级,comp1中实例化了comp2和comp3,最顶层的test类实例化了comp1
- build_phase:test-c1-c2-c3
- connect_phase:c2-c3-c1-test
- run_phase:c2-c3-c1-test(test挂起,进入c1-c2-c3,但是comp组件的run-phase组件都没挂起,直接退出)
- main_phase:test
- report_phase: c2-c3-c1-test
build_phase
、connect_phase
、run_phase
、report_phase
都是按照自顶向下或者自底向上的顺序依次执行的。
run_phase
是9种phase唯一的task,与run_phase
平行执行的是12个分支phase,所以在0时刻,run_phase
和reset_phase
是并行的,但是要等到reset_phase
在执行完成之后,run_phase
才能执行完。继而进入下一个extract_phase
阶段。reset_phase
和main_phase
之间却是有先后顺序的,先执行reset_phase
在1000时刻结束,然后main_phase
才开始执行,在2000时刻结束。所以整个仿真最后耗时Time=2us。
1.4 config机制
实现接口从uvm_config
模块到验证环境中的传递,使得c1
和c2
可以得到接口,并且检查接口是否最终得到。
接口uvm_config_if
和对象uvm_config
1 | interface uvm_config_if; |
1 | class config_obj extends uvm_object; |
实现配置对象config_obj
从uvm_config_test
到c1
和c2
的传递。
底层comp2和次底层comp1类
1 | class comp2 extends uvm_component; |
comp1
类,在comp1
中包含一个comp2
对象
1 | class comp1 extends uvm_component; |
顶层test类
uvm_config
模块,顶层test模块,实现所有object及单一变量的传递
1 | class uvm_config_test extends uvm_test; |
uvm_config模块
1 | module uvm_config; |
总结
接口都是在initial块中就set到config_db中,object和单一变脸都是在test中进行set(注意test类15行中*的用法)
对于调用get方法获取接口或对象的类,其实例化必须发生在config_db的set之后
注意传递interface时参数的固定写法!!! 通过
.*
可以同时代表comp1和comp2两个类1
2
3
4
5
6
7
8
9uvm_config_db#(virtual uvm_config_if)::set(uvm_root::get(), "uvm_test_top.*", "vif", if0);
run_test(""); // empty test name
uvm_config_db#(config_obj)::set(this, "*", "cfg", cfg); //将config_obj通过config_db传递给c1和c2
uvm_config_db#(int)::set(this, "c1", "var1", 10); //c1.var1和c2.var2的变量设置。
uvm_config_db#(int)::set(this, "c1.c2", "var2", 20);
c1 = comp1::type_id::create("c1", this);
执行仿真命令
1 | vsim -novopt -classdebug +UVM_TESTNAME=uvm_config_test work.uvm_config |
仿真结果
1.5 message管理
1 | function void build_phase(uvm_phase phase); |
无错时的打印信息:
5.1.使用冗余度进行过滤的方法:set_report_verbosity_level_hier()
在uvm_message_test::build_phase()
中屏蔽所有层次的消息,也就是不允许有任何uvm_message_test
及其以下组件的消息在仿真时打印出来。
1 | set_report_verbosity_level_hier(UVM_NONE); |
执行命令:
1 | vsim -novopt -classdebug +UVM_TESTNAME=uvm_message_test work.uvm_message |
结果:
很明显,只有设置冗余度之前的消息代码进行了执行
5.2 使用id+冗余度进行过滤:set_report_id_verbosity_hier(“CREATE”, UVM_NONE)
1 | uvm_root::get().set_report_id_verbosity_hier("CREATE", UVM_NONE); |
结果:
5.3 在指定的phase中添加过滤器
在end_of_elaboration_phase
中使用set_report_id_verbosity_level_hier()
来过滤ID的消息,可以使得uvm_message_test
的[BUILD]顺利执行完,继而执行完c1
和c2
的[BUILD]。
1 | function void end_of_elaboration_phase(uvm_phase phase); |
仿真结果:
5.4 如何屏蔽最顶层initial块中的info:
所有的打印消息,无论屏蔽与否,都会将config_obj的[CREATE]消息以及uvm_message模块里面的[TOPTB]消息打印出来。
可以使用消息过滤方法uvm_root::get()来获取最顶层的(即uvm_message_test的顶层),来控制过滤[CREATE]、[TOPTB]消息。
1 | initial begin |
总结
过滤前执行的打印信息照常打印,过滤后执行的打印信息受过滤器管控
UVM-实验2
1.1 验证组件和层次构建
首先将各个package中的SV组件替换为UVM组件
实现组件对应原则:
- SV的transaction类对应uvm_sequence_item
- SV的driver类对应uvm_driver
- SV的generator类对应uvm_sequence + uvm_sequencer
- SV的monitor对应uvm_monitor
- SV的agent对应uvm_agent
- SV的env对应uvm_env
- SV的checker对应uvm_scoreboard
- SV的reference model和coverage model均对应uvm_component
- SV的test对应uvm_test
在遵循上面的对应原则的过程中,在进行类的转换时,需要注意:
SV的上述类均需要继承于其对应的UVM类
在类定义过程中,一定需要使用
'uvm_component_utils()
或者'uvm_object_utils()
完成类的注册。在使用上述工厂注册宏的时候,会伴随着“域声明自动化”,一般而言,将sequence item类定义时,应当伴随着域声明,即利用
'uvm_object_utils_begin
和
'uvm_object_utils_end
完成。这是由于对于sequence item对象的拷贝、比较和打印等操作比较多,因此在定义sequence item类时,最好需要完成域的自动化声明。一定要注意构建函数new()的声明方式,uvm_component的构建函数有两个参数
new(string name, uvm_component parent)
,而uvm_object的构建函数只有一个参数new(string name)
。在组件之间的层次关系构建中,依然按照之前SV组件的层次关系,只需要在不同的phase阶段完成组件的例化和连接。
1.2 UVM文件结构
chnl_trans
类
相比于SV验证模块代码,成员变量没有发生什么变化,但是省略掉了clone()
和sprint()
这两个方法,因为UVM做了类的注册以及域的自动化的声明,可以使用UVM核心基类的克隆、打印、比较等一些常见方法。
1 | class chnl_trans extends uvm_sequence_item; |
chnl_driver
类
- 继承于uvm_driver是一个参数类,所以得
uvm_driver #(chnl_trans)
。而SV中的run()转换成了UVM中run_phase(uvm_phase phase)
。 do_driver()
方法里面void'($cast(rsp, req.clone()))
,是因为UVM核心基类的克隆方法返回的是uvm_object类型,所以需要把父类的句柄转换为子类的句柄,而req.clone()
这个父类句柄指向的是一个子类的对象,所以能转换成功。chnl_write()
方法中把SV中的$display替换成了UVM中的'uvm_info()
。- 全部的打印方法都由UVM的宏来完成!!
1 | // |
chnl_generator
类
- 相比于SV使用构建函数
new()
来创建对象,send_trans()
方法中使用req = chnl_trans::type_id::create("req")
来创建对象。 - 打印消息使用了UVM中的
'uvm_info()
。
1 | // channel generator and to be replaced by sequence + sequencer later |
chnl_monitor
类
将SV中的run()
替换成了UVM中的
run_phase(uvm_phase phase)
以及使用UVM中的'uvm_info()
打印消息。
1 | // channel monitor |
chnl_agent类
- SV验证结构中例化和连接都发生在构建函数new()里面,而UVM中例化是在build_phase()方法中,并且通过create()来例化创建对象。
- SV验证结构中run()需要调用子一级的run()方法,而在UVM中不需要手动去调用子一级的run_phase(),因为run_phase是按照层次来执行的,是由uvm_root来安排的,会自动调用。
1 | // channel agent |
reg_pkg.sv文件、fmt_pkg.sv文件、mcdf_pkg.sv文件修改的地方相似。
1.3 mcdf
测试的开始和结束
UVM验证环境测试的开始、环境构建的过程、连接以及结束的控制。
tb.sv
通过uvm_config_db完成了各个接口从TB(硬件一侧)到验证环境mcdf_env(软件一侧)的传递。实现了以往SV函数的剥离,即UVM不需要深入到目标组件一侧,调用其set_interface()即可完成传递。这种传递方式有赖于config_db的数据存储和层次传递特性。而在mcdf_env中,暂时保留了mcdf_env的set_interface()以及各个子组件的set_interface()函数。仅修改了TB与mcdf_env之间的接口传递,其实可以移除所有的set_interface()函数,完全使用uvm_config_db的set和get方法,从而使得mcdf_env与其各个子组件之间也实现“层次剥离”,这样也就进一步促进了组件之间的独立性。
1 | initial begin |
通过调用run_test()函数即完成了test的选择、例化和开始测试。可以在代码中指定UVM test,或者通过 +UVM_TESTNAME=mytest在仿真选项中灵活传递test名。在run_test()执行中,它会初始化objection机制,即查看objection有没有挂起的地方,因此在test或者generator中必须至少有一处地方使用phase.raise_objection()来挂起仿真,避免仿真退出,而在仿真需要结束时,使用phase.drop_objection()来允许仿真可以退出。同时run_test()可以创建uvm_test组件,及其以下的各层组件群,并且可以调用phase控制方法,按照所有phase顺序执行。在UVM中,将对象的例化放置在build_phase中,而将对象的连接放置在connect_phase中。
UVM-实验3
问:为啥有时候用tlm端口,有时候用tlm通信管道,这两者在执行效率上有什么差距吗
TLM通信的引入
1. TLM单向通信和多向通信
在之前的monitor到checker的通信,以及checker与reference model之间的通信,都是通过mailbox以及在上层进行其句柄的传递实现的。这次实验则使用TLM端口进行通信,做逐步的通信元素和方法的替换。
agent
将agent.monitor
中的用来与checker
中的mailbox
通信的mon_mb
句柄替换为对应的uvm_blocking_put_port
类型。
以chnl的mailbox为例
1 | uvm_blocking_put_port #(mon_data_t) mon_bp_port; |
mcdf_pkg
在checker中声明与monitor通信的imp端口类型,以及reference model通信的imp端口类型,由于checker与多个monitor以及reference model通信,是典型的多方向通信类型,因此需要使用多端口通信的宏声明方法。在使用了宏声明端口类型之后,再在checker中声明其句柄,并且完成例化。
- 3个monitor(port)
- 1个reg(port)
- 1个fmt(port)
- 3个refmod的chnl(port)
1 | //宏声明端口 |
根据声明的import
端口类型,实现其对应的方法。(同样依赖mailbox作为)
1 | //实现put方法 |
在mcdf_refmod
中声明用来与mcdf_checker
中的`import连接的端口,并且完成例化。完成声明和例化后,使用TLM端口呼叫方法。
1 | //声明端口 |
在mcdf_env
的connect_phase()
阶段,完成monitor
的TLM port与mcdf_checker
的TLM import的连接。
1 | chnl_agts[0].monitor.mon_bp_port.connect(chker.chnl0_bp_imp); |
在mcdf_checker的
connect_phase()阶段,完成
refmod的TLM port与
mcdf_checker`的TLM import的连接。
1 | refmod.in_bgpk_ports[0].connect(chnl0_bgpk_imp); |
refmod中端口的使用
1 | task do_reg_update(); |
2. TLM通信管道
TLM通信的优点:
- 通信函数可以定制化,例如可以定制put()/get()/peek()的内容和参数,这比mailbox的通信更加灵活。
- 将组件实现了完全的隔离,通过层次化的TLM端口连接,可以很好地避免直接将不同层次的数据缓存对象的句柄进行“空传递”。
如果使用TLM端口,并且不用实现具体的put()/get()/peek()方法,可以使用uvm_tlm_fifo类。
将原本在mcdf_refmod中的out_mb替换成uvm_tlm_fifo类型,并且完成例化以及对应的变量名替换。
1 | uvm_tlm_fifo #(fmt_trans) out_tlm_fifos[3]; |
将原本在mcdf_checker
中的exp_mbs[3]
的邮箱句柄数组,替换为uvm_blocking_get_port
类型句柄数组,并且做相应的例化以及变量名替换。
1 | uvm_blocking_get_port #(fmt_trans) exp_bg_ports[3]; |
在mcdf_checker
中,完成mcdf_checker
中的TLM port端口到mcdf_refmod
中的uvm_tlm_fifo
自带的blocking_get_export
端口的连接。
1 | foreach(exp_bg_ports[i]) begin |
3. UVM回调类
逻辑:
原先mcdf_base_test类被子类mcdf_data_consistence_basic_test、mcdf_full_random_test类继承,不同的类激励生成的方式不同,依此来尽可能的完成覆盖率收集
增加回调类:
相当于把产生激励的几个方法(do_data(),do_reg(),do_fmt())抽离出来,放入回调类
在test类中进行绑定,测试类mcdf_base_test和回调类cb_mcdf_base
测试类中的这几个方法,要插入回调类的相应方法,也就是执行回调方法!!!
此时测试类mcdf_base_test和回调类cb_mcdf_base 都没有实现上述方法,同样需要回调类的子类实现
值得注意的是:对测试类中用来执行的方法(run_phase中的do_data(),do_reg(),do_fmt()等),或者说要插入回调类的方法,必须要声明为虚方法,因为所有的子测试类虽然实际上都是在调用base_test类的run_phase,但执行的还得是其本身插入的回调类相应方法!!!
这也是为啥在cb_mcdf_data_consistence_basic_test中,是父类的base_test类添加子类的回调类cb_mcdf_data_consistence_basic
1
2cb = cb_mcdf_data_consistence_basic::type_id::create("cb"); //创建回调函数
uvm_callbacks#(mcdf_base_test)::add(this, cb); //添加回调函数测试类的子类mcdf_data_consistence_basic_test、mcdf_full_random_test类都可以定义对应的回调类(继承自base_test的回调类cb_mcdf_base ),实现(do_data(),do_reg(),do_fmt())
此时的测试类就只需要把对应的回调类添加一下就好了,别的都不用管
mcdf_data_consistence_basic_test的执行路径:
- 执行build_phase,添加自己的回调类
- 本身没有run_phase,执行父类base_test的run_phase,父类run_phase中的方法(do_data(),do_reg(),do_fmt())又会找到回调类中的相应方法执行
在uvm_callback类中,预先定义需要的虚方法。(定义)
1 | typedef class mcdf_base_test; |
使用callback
对应的宏,完成目标uvm_test
类型与目标uvm_callback
类型的关联。(绑定)
1 |
在目标uvm_test
类型指定的方法中,完成uvm_callback
的方法回调指定。(插入)
1 | // do register configuration |
完整的mcdf_base_test类:
- 17行:该类和回调类的绑定
- 97,103,109:插入回调方法,也就是test类的方法执行时其实是进入回调类中的方法
1 | // MCDF base test |
实现uvm_callback
和对应test
类的定义(添加)
build_phase中创建和添加回调类,需要注意这里语法的使用
1 | cb = cb_mcdf_data_consistence_basic::type_id::create("cb"); //创建回调函数 |
问:为啥这里的是mcdf_base_test,而不是实际的测试类
因为前面目标uvm_test
类型与目标uvm_callback
类型建立了绑定关系,这里传入的test类型还是父类的test
当test被执行时,首先执行父类test中的run_phase
1 | class cb_mcdf_data_consistence_basic extends cb_mcdf_base; //回调类 |
UVM-实验4
在UVM入门实验3中,实现了monitor
、reference model
与checker
之间的通信是通过TLM端口或者TLM FIFO来完成,相较于之前的mailbox
句柄连接,更加容易定制,也使得组件的独立性提高。
本次实验需要实现:
- 将产生transaction并且发送至driver的generator组件,拆分为sequence与sequencer。
- 在拆分的基础上,实现底层的sequence。
- 完成sequencer与driver的连接和通信工作。
- 构建顶层的virtual sequencer。
- 将原有的mcdf_base_test拆分为mcdf_base_virtual_sequence与mcdf_base_test,前者发挥产生序列的工作,后者只完成挂载序列的工作。
- 将原有的mcdf_data_consistence_basic_test和mcdf_full_random_test继续拆分为对应的virtual sequence和轻量化的顶层test。
- 通过实验4可以将generator、driver与test的关系最终移植为sequence、sequencer、driver和test的关系。
1. generator到序列化的改建(reg_pkg)
trans
trans:继承uvm_sequence_item,变量添加到自动化域(随机化内容和原来相同)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class reg_trans extends uvm_sequence_item;
rand bit[7:0] addr;
rand bit[1:0] cmd;
rand bit[31:0] data;
bit rsp;
constraint cstr {
soft cmd inside {
soft addr inside {
addr[7:4]==0 && cmd==
soft addr[7:5]==0;
addr[4]==1 -> soft cmd ==
};
function new (string name = "reg_trans");
super.new(name);
endfunction
endclass
sequence
激励产生交由sequence实现
和原有的generator方法的明显区别:
最大的区别:generator作为component,在顶层test中创建,随机化和给driver发送激励;
sequence作为object,在自身的body方法中生成激励和调用发送方法,其可以声明在模块的包中,在顶层包中通过virtual sequence进行同一管理(连接和挂载)
继承uvm_sequence,是参数化类,需要传入实例化的item类型
mailbox被取消,核心方法send_trans极大的简化
- 一个`uvm_do_with(req,{“约束”}),即可实现item的随机化创建+挂载
- 一个get_response()方法就可以获取到来自driver的相应rsp-item(前提是deriver返回rsp,两者相匹配)
- reg的激励较为特殊,其要实现3种命令模式的分别创建,依此来进行寄存器的读写功能检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52class reg_base_sequence extends uvm_sequence #(reg_trans);
rand bit[7:0] addr = -1;
rand bit[1:0] cmd = -1;
rand bit[31:0] data = -1;
constraint cstr{
soft addr == -1;
soft cmd == -1;
soft data == -1;
}
function new (string name = "reg_base_sequence");
super.new(name);
endfunction
task body();
send_trans();
endtask
// generate transaction and put into local mailbox
task send_trans();
reg_trans req, rsp;
local::cmd >= 0 -> cmd == local::cmd;
local::data >= 0 -> data == local::data;
})
get_response(rsp);
if(req.cmd ==
this.data = rsp.data;
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
function void post_randomize();
string s;
s = {s, "AFTER RANDOMIZATION \n"};
s = {s, "=======================================\n"};
s = {s, "reg_base_sequence object content is as below: \n"};
s = {s, super.sprint()};
s = {s, "=======================================\n"};
endfunction
endclass: reg_base_sequenceidle_reg_sequence
1
2
3
4
5
6
7
8
9
10
11class idle_reg_sequence extends reg_base_sequence;
constraint cstr{
addr == 0;
cmd ==
data == 0;
}
function new (string name = "idle_reg_sequence");
super.new(name);
endfunction
endclass: idle_reg_sequencewrite_reg_sequence
1
2
3
4
5
6
7
8
9class write_reg_sequence extends reg_base_sequence;
constraint cstr{
cmd ==
}
function new (string name = "write_reg_sequence");
super.new(name);
endfunction
endclass: write_reg_sequenceread_reg_sequence
1
2
3
4
5
6
7
8
9class read_reg_sequence extends reg_base_sequence;
constraint cstr{
cmd ==
}
function new (string name = "read_reg_sequence");
super.new(name);
endfunction
endclass: read_reg_sequence
sequencer
sequencer的声明和定义都很简单,同样是参数化类,需要添加传入item类型
1 | class reg_sequencer extends uvm_sequencer #(reg_trans); |
driver
对应的核心方法do_drive(),通过seq_item_port的方法来获取和返回item;
参数化类,传入item类型
1 | class reg_driver extends uvm_driver #(reg_trans); |
agent
这个就很简单了,sequencer,driver,monitor的创建,以及sequencer和driver的连接
1 | // register agent |
2. 移除generator的踪迹(mcdf_pkg)
在mcdf_base_test
中移除generator
的声明、创建以及和driver
之间的连接。
移除generator的声明句柄,创建,generator和driver的mailbox的连接
不再调用原有的do_reg,do_data,do_fmt方法,激励的产生和发送全部由序列化完成!!!
后面再看一下子类的
run_top_virtual_sequence();
方法如何实现
1 | class mcdf_base_test extends uvm_test; |
3. virtual sequence
既然是对所有sequence的统一管理,就必然牵扯到几点
- 所有底层sequence的创建,并挂载到virtual sequencer的相应sequencer上
- 挂载:其实就可以理解为底层sequence的body()方法的执行!!
base_virtual_sequence
主要是对所有virtual_sequence的统一性的配置:
所有的sequence句柄
传入virtual_sequencer句柄:
`uvm_declare_p_sequencer(mcdf_virtual_sequencer)
body方法:对底层的sequence进行创建和挂载(可以看到底层的sequence中item的挂载都不用传入sequencer,就等着在这里将sequence挂载到sequencer上呢!!!)
这里传入的都是空的虚方法,具体方法在子类实现!!见下面的
mcdf_data_consistence_basic_virtual_sequence
1 | class mcdf_base_virtual_sequence extends uvm_sequence; |
mcdf_data_consistence_basic_virtual_sequence
牵扯到具体的测试类就需要给以确切的激励随机化目标了!!!
do_reg:对3个寄存器依次进行配置
因为对于读命令的sequence,获取到的driver返回的data值会更新对象的data值,可以直接通过对象调用
do_fmt:对模拟fifo的boundwidth和消化速度进行配置
do_data:一个sequence的3个随机化实例挂载到3个sequencer上
每一次做相应配置,都需要调用宏`uvm_do_on_with,创建sequence并进行随机化,将sequence挂载到virtual sequencer上
1 | class mcdf_data_consistence_basic_virtual_sequence extends mcdf_base_virtual_sequence; |
4. virtual sequencer+env
定义virtual sequencer,很简单,只需要把所有组件中的virtual sequencer进行囊括
在顶层env中,也就是大环境中,需要把virtual sequencer和每一个组件中的sequencer连接起来
sequencer定义
1 | class mcdf_virtual_sequencer extends uvm_sequencer; |
env中建立句柄连接sequencer实例
可以看到,env中的属性只有所有的agent和virtual sequencer,build_phase中进行实例化,connect_phase中建立连接
1 | class mcdf_env extends uvm_env; |
5. 重构test
最直观的改变:
将已经被从uvm_base_test移植到reg_pkg中的方法idle_reg()、write_reg()和read_reg()从uvm_base_test中移除。
由此uvm_base_test变为了单纯的容器,激励的生成场景都在virtual sequence中完成,在它内部主要由mcdf_env、mcdf_config配置对象以及被用来挂载的顶层sequence构成。
env中底层组件的run_phase执行时,把每个driver都启动起来,最后回到顶层的run_phase执行this.run_top_virtual_sequence();
重点需要思考的问题:程序最终如何终止?
除run_phase外所有内容都是0时刻执行,这个问题其实也就是问run_phase啥时候终止;
- 已进入run_phase后,直接通过
phase.raise_objection(this);
挂起,然后执行底层run_phase,注意,这里底层的run_phase都是在0时刻启动,然后都会被do_driver()方法中的seq_item_port.get_next_item(req);
卡住,同样0时刻也意味着此时的this.run_top_virtual_sequence();
开始执行 - 然后就是sequence,sequencer,driver之间的数据交换。
然后又有了新问题:按理说sequence数据发送结束后不就执行phase.drop_objection(this)
了吗,那我driver和monitor怎么办??
和前面sv的实验中一样,sequence是要接收driver反馈的RSP的
get_response(rsp);
!!!所以只有driver把sequence给它的最后一个数据发送出去以后,返回rsp给sequence,且sequence接收到这个rsp,激励发送才是结束!!!!而monitor是检测有效数据传输,比如chnl的driver必须等到ready信号才算完成一次数据传输,进入idle阶段,所以monitor在driver刚把数据驱动到接口上就进行了采集
- 综上,sequence一定是最后一个执行结束的!!!
1 | class mcdf_base_test extends uvm_test; |
来看下子类中的方法this.run_top_virtual_sequence();
是怎么实现的
其实没啥花里胡哨,就是将virtual sequence挂载到virtual sequencer上,run_phase启动的时候所有的sequence也就启动了
注意上面sequence的执行顺序并非并行,一定是先do_reg,再do_formatter,再do_data
1 | class mcdf_data_consistence_basic_test extends mcdf_base_test; |
UVM_实验5
从寄存器模型开始,再重新对整个系统做一次梳理
mcdf_rgm_pkg
3个reg相关的类,全是object
1. reg类
正好回忆一下寄存器模型的内容
- 声明寄存器域,对reg的功能进行拆解(rand)
- 考虑寄存器模型的覆盖率收集,可以直接在reg中生成covergroup和对应方法(sample(),sample_values())
- 构造方法:调用父类构造方法,传入:名称,位数,覆盖率类型;设置域的覆盖率采集;如果设置了,就可以实例化covergroup
- build方法:寄存器每个域的创建(typeid+create创建,不需要注册)
读写寄存器ctrl_reg
1 | class ctrl_reg extends uvm_reg; |
状态寄存器stat_reg
1 | class stat_reg extends uvm_reg; |
2. reg_block类
把6个寄存器都放进来,此时每个寄存器也声明为rand
构造方法:同样调用父类构造方法,传入name,UVM_NO_COVERAGE(覆盖率不能重复收集)
build方法:
寄存器配置三部曲:创建+配置+调用寄存器的build方法
常见必须的map:map = create_map(“map”, ‘h0, 4, UVM_LITTLE_ENDIAN);
在map中添加rgm中的寄存器,每个寄存器的偏移地址,寄存器的读写类型等
给所有的reg绑定hdl路径
切记,把模型锁住:lock_model()
1 | class mcdf_rgm extends uvm_reg_block; |
3. reg_adapter类
可以在uvm中找到adapter的实现
mcdf_bus_driver
在读数时会将读回的数据填入到RSP
并返回至sequencer
,因此需要在adapter
中使能provides_responses
。
reg2bus(const ref uvm_reg_bus_op rw):
需要先创建reg_trans,然后调用create方法创建实例对象
用uvm_reg_bus_op的数值对创建的trans进行非阻塞赋值,最后返回reg_trans
bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw)
- 因为传入的参数类型是uvm_sequence_item,需要先进行类型转化,为reg_trans
- 和上面的方法类似进行映射,需要注意的是
rw.status = UVM_IS_OK;
1 | class reg2mcdf_adapter extends uvm_reg_adapter; |
mcdf_env
相比较实验4中的env,毫无疑问的多了点东西,就是多了的这些东西,才让后面寄存器模型的read,write方法的直接使用而不用挂载,
注意这里的uvm_reg_predictor,是参数类,同样需要传入reg_trans
问:为啥要定义和绑定predictor呢?
答:显示自动预测的需要
1 | mcdf_rgm rgm; |
再来看看这几个东西是如何创建的
fgm调用build()方法,关系到reg和map的创建和配置,最终还关系到reg_field的创建和配置
1 | rgm = mcdf_rgm::type_id::create("rgm", this); |
因为这里创建了predictor,就不需要调用rgm.map.set_auto_predict();
了
这几个东西的连接也是重头戏
想象一下结构图,这里的连接就是以结构图为原型
- adapter的集成:rgm.map、reg_agt.sequencer、adapter;以适配器为中心进行连接
- predictor的集成:
1 | rgm.map.set_sequencer(reg_agt.sequencer, adapter); |
1 | // MCDF top environment |
寄存器模型的使用
- 在
connect
阶段实现register block
句柄的传递。 - 将
mcdf_data_consistence_basic_virtual_sequence
原有的由总线sequence
实现的寄存器读写,改为由寄存器模型操作的寄存器读写方式。 - 将
mcdf_full_random_virtual_sequence
原有的由总线sequence
实现的寄存器读写,改为由寄存器模型预先设置寄存器值,再统一做总线寄存器更新的方式,并且由后门读取的方式取得寄存器值。
1.virtual sequence获取rgm句柄,只需要mcdf_base_virtual_sequence进行获取
问:为啥让virtual sequence获取rgm句柄?
因为reg的激励环境配置也是在sequence中做的,就是把原先通过总线进行寄存器访问的方式改为了访问寄存器模型了
看一下base_sequence做的少许修改:
- 声明reg_block的句柄rgm;
- 在body任务中,给句柄赋值p_sequencer.rgm,因为在body中定义,所以起码得等到执行到test的run_phase时才能真正将rgm给到virtual sequence
再看一下virtual sequencer又是如何得到rgm实例的
- 在virtual sequencer中声明rgm句柄
- 在上面的mcdf_env的build_phase中,创建rgm实例,并在connect_phase中将rgm实例赋值给virtual sequencer
1 | class mcdf_base_virtual_sequence extends uvm_sequence; |
2.将mcdf_data_consistence_basic_virtual_sequence
原有的由总线sequence
实现的寄存器读写,由寄存器模型操作的寄存器读写方式
直接调用寄存器模型的写方法,通过wr_val对目的寄存器的状态值进行修改
调用寄存器模型的读方法,获取到寄存器的状态值,输出rd_val
这两种方法默认情况下都是前门访问,从总线访问过去的
1 | task do_reg(); |
3.将mcdf_full_random_virtual_sequence
原有的由总线sequence
实现的寄存器读写,改为由寄存器模型预先设置寄存器值,再统一做总线寄存器更新的方式,并且由后门读取的方式取得寄存器值。
其实就是和2的东西做点区分,增加一点练习
reg_block(reg)的reset方法,给所有寄存器模型复位
通过set设置寄存器模型中的value
调用update更新dut寄存器的状态
不在通过将读到的值和写入的值进行比较来检查,这里用reg的mirror方法进行检查,注意这里的参数传递
如果dut寄存器状态值和软件模型的镜像值不同,会打印结果
1
rgm.chnl0_ctrl_reg.mirror(status, UVM_CHECK, UVM_BACKDOOR);
也可以使用前门访问
问:为啥不直接调用rgm的mirror方法
答:有状态寄存器,镜像值和实际值很可能不一样
1 | task do_reg(); |
寄存器内建序列的应用
和上面的sequence大同小异,就是将body方法封装起来了而已,挂载的目的其实就是为了执行其内部的body方法!!!
在mcdf_reg_builtin_virtual_sequence
类中,使用uvm_reg_hw_reset_seq
,uvm_reg_bit_bash_seq
和uvm_reg_access_seq
对MCDF寄存器模块展开全面测试。
几个内建序列的意思
测试过程:
每一次都要进行硬件的复位和软件模型的复位
reg_rst_seq.model = rgm;
reg_rst_seq.start(p_sequencer.reg_sqr);给内建序列传入寄存器模型reg_block(==
rgm = p_sequencer.rgm;
),并且挂载到reg_sequencer上(就是为了让内建sequence的body方法能够执行)最终在test类中virtual sequence挂载到virtual sequencer的过程其实就对应了body的执行
1 | task do_reg(); |
仿真命令
1 | vsim -novopt -sv_seed random -classdebug +UVM_TESTNAME=mcdf_reg_builtin_test -l assertion.log work.tb |
文件结构
仿真结果
内建序列的反馈信息
由于uvm_reg_bit_bash_seq的结果太多了,就只把reg_access_seq的执行结果拿出来看一看,上面还有uvm_reg_hw_reset_seq,uvm_reg_bit_bash_seq
三者按顺序执行~
- 很直观的一点:装载到reg_agt.sequencer的reg内建序列是执行主体,和我推测的:在挂载时执行内建序列的body方法一样
把uvm_reg_hw_reset_seq的执行结果也拿出来看一下
很明显,每个addr的reg进行了轮询
- driver发送读信号
- 软件模型获取当前寄存器的复位值
- predictor的显示预测值
- monitor的监测值
内建序列的功能覆盖率
以ctrl_reg的len域为例:
可以看出:3位的len [2:0]分配了8个bin,被覆盖到的bin只有0,1,2,4
因为看上面的打印信息就可以看出,只有uvm_reg_bit_bash_seq 在对读写寄存器寄存器的每一位做读写,且一次只有1位为1:
000,001,010,100