第6章 UVM中的sequence
UVM 的 sequence 机制:从 driver 中剥离激励产生、sequence 的启动与仲裁、各类宏的实现、p_sequencer、嵌套与 virtual sequence、在 sequence 中使用 config_db、response 反馈机制,以及 sequence library
6.1 sequence基础
6.1.1 从driver中剥离激励产生功能
最开始,激励最初产生在 driver 中,后来产生在 sequence 中。为什么会有这个过程呢?
最开始时,driver 的 main_phase 是这样的:
清单 6-1
systemverilog
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
phase.raise_objection(this);
for(int i = 0; i < 10; i++) begin
tr = new;
assert(tr.randomize);
drive_one_pkt(tr);
end
phase.drop_objection(this);
endtask
如果只是施加上述一种激励,这样是可以的。但当要对 DUT 施加不同的激励时,那应该怎么办呢?上述代码中是施加了正确的包,而下一次测试中要在第 9 个 transaction 中加入 CRC 错误的包,那么可以这么写:
清单 6-2
systemverilog
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
phase.raise_objection(this);
for(int i = 0; i < 10; i++) begin
tr = new;
if(i == 8)
assert(tr.randomize with {tr.crc_err == 1;});
else
assert(tr.randomize);
drive_one_pkt(tr);
end
phase.drop_objection(this);
endtask
这就相当于将整个 main_phase 重新写了一遍。如果现在有了新的需求,需要再测一个超长包,则需要再次改写 main_phase,也就是说,每多测一种情况,就要多改写一次 main_phase。如果经常改写某个任务或者函数,那么就很容易将之前对的地方改错。所以说,这种方法是不可取的,因为它的可扩展性太差,会经常带来错误。
仔细观察 main_phase,其实只有从 tr=new 语句至 drive_one_pkt 之间的语句在变。有没有什么方法可以将这些语句从 main_phase 中独立出来呢?最好的方法就是在不同的测试用例中决定这几行语句的内容。这种想法中已经包含了"激励的产生与驱动的分离"这个观点。drive_one_pkt 是驱动,这是 driver 应该做的事情,但是像"产生什么样的包、如何产生"等这些事情应该从 driver 中独立出去。
要实现上述目标,可以使用一个函数来实现:
清单 6-3
systemverilog
function void gen_pkt(ref my_transaction tr);
tr = new;
assert(tr.randomize);
endfunction
task my_driver::main_phase(uvm_phase phase);
my_transaction tr;
bit send_crc_err = 0;
phase.raise_objection(this);
for(int i = 0; i < 10; i++) begin
gen_pkt(tr);
drive_one_pkt(tr);
end
phase.drop_objection(this);
endtask
如上所示,可以定义一个产生正常包的 gen_pkt 函数,但是如何定义一个 CRC 错误包的函数呢?难道像下面这样吗?
清单 6-4
systemverilog
function void gen_pkt(ref my_transaction tr);
tr = new;
assert(tr.randomize with {crc_err == 1;});
endfunction
这样带来的一个最大的问题就是 gen_pkt 函数的重复定义,显然这样是不允许的。为了避免重复定义,有两种策略:第一种是使用虚函数。将代码清单 6-3 中的 gen_pkt 定义为 virtual 类型,然后在建造 CRC 错误的测试用例时,从 my_driver 派生一个新的 crc_err_driver,并重载 gen_pkt 函数。但是这样新的问题又出现了,如何在这个测试用例中实例化这个新的 driver 呢?似乎只能重新定义一个 my_agent ,为了实例化这个新的 agent,又只能重新定义一个 my_env。这种解决方式显然是不可取的。第二种解决方式是使定义的函数的名字是不一样的,但是在 driver 的 main_phase 中又无法执行这种具有不同名字的函数。
这是一个相当难的问题,单纯用 SystemVerilog 提供的一些接口是根本无法实现的。UVM 为了解决这个问题,引入了 sequence 机制,在解决的过程中还使用了 factory 机制、config 机制。使用 sequence 机制之后,在不同的测试用例中,将不同的 sequence 设置成 sequencer 的 main_phase 的 default_sequence。当 sequencer 执行到 main_phase 时,发现有 default_sequence,那么它就启动 sequence。
仔细回想上面的过程,sequencer 启动 sequence 并执行的过程就相当于之前的 gen_pkt,只是调用的位置从 driver 变到 sequencer。sequencer 将 sequence 产生的 transaction 交给 driver,这其实与在 driver 里面调用 gen_pkt 没有本质的区别。
6.1.2 sequence的启动与执行
当完成一个 sequence 的定义后,可以使用 start 任务将其启动:
清单 6-5
systemverilog
my_sequence my_seq;
my_seq = my_sequence::type_id::create("my_seq");
my_seq.start(sequencer);
除了直接启动之外,还可以使用 default_sequence 启动。事实上 default_sequence 会调用 start 任务,它有两种调用方式,其中一种是前文已经介绍过的:
清单 6-6
systemverilog
uvm_config_db#(uvm_object_wrapper)::set(this,
"env.i_agt.sqr.main_phase",
"default_sequence",
case0_sequence::type_id::get());
另外一种方式是先实例化要启动的 sequence,之后再将其通过 default_sequence 启动:
清单 6-7
systemverilog
文件:src/ch6/section6.1/6.1.2/my_case0.sv
41 function void my_case0::build_phase(uvm_phase phase);
42 case0_sequence cseq;
43 super.build_phase(phase);
44
45 cseq = new("cseq");
46 uvm_config_db#(uvm_sequence_base)::set(this,
47 "env.i_agt.sqr.main_phase",
48 "default_sequence",
49 cseq);
50 endfunction
当一个 sequence 启动后会自动执行 sequence 的 body 任务。其实,除了 body 外,还会自动调用 sequence 的 pre_body 与 post_body:
清单 6-8
systemverilog
文件:src/ch6/section6.1/6.1.2/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
10 virtual task pre_body();
11 `uvm_info("sequence0", "pre_body is called!!!", UVM_LOW)
12 endtask
13
14 virtual task post_body();
15 `uvm_info("sequence0", "post_body is called!!!", UVM_LOW)
16 endtask
17
18 virtual task body();
...
21 #100;
22 `uvm_info("sequence0", "body is called!!!", UVM_LOW)
...
25 endtask
26
27 `uvm_object_utils(case0_sequence)
28 endclass
上述的 sequence 在执行时,会打印出:
text
# UVM_INFO my_case0.sv(11) @ 0: uvm_test_top.env.i_agt.sqr@@cseq [sequence0] pre_body is called!!!
# UVM_INFO my_case0.sv(22) @ 100000: uvm_test_top.env.i_agt.sqr@@cseq [sequence0] body is called!!!
# UVM_INFO my_case0.sv(15) @ 100000: uvm_test_top.env.i_agt.sqr@@cseq [sequence0] post_body is called!!!
6.2 sequence的仲裁机制
6.2.1 在同一sequencer上启动多个sequence
在前文所有的例子中,同一时刻,在同一 sequencer 上只启动了一个 sequence。事实上,UVM 支持同一时刻在同一 sequencer 上启动多个 sequence。
在 my_sequencer 上同时启动了两个 sequence:sequence1 和 sequence2,代码如下所示:
清单 6-9
systemverilog
文件:src/ch6/section6.2/6.2.1/no_pri/my_case0.sv
57 task my_case0::main_phase(uvm_phase phase);
58 sequence0 seq0;
59 sequence1 seq1;
60
61 seq0 = new("seq0");
62 seq0.starting_phase = phase;
63 seq1 = new("seq1");
64 seq1.starting_phase = phase;
65 fork
66 seq0.start(env.i_agt.sqr);
67 seq1.start(env.i_agt.sqr);
68 join
69 endtask
其中 sequence0 的定义为:
清单 6-10
systemverilog
文件:src/ch6/section6.2/6.2.1/no_pri/my_case0.sv
3 class sequence0 extends uvm_sequence #(my_transaction);
...
10 virtual task body();
...
13 repeat (5) begin
14 `uvm_do(m_trans)
15 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
16 end
17 #100;
...
20 endtask
21
22 `uvm_object_utils(sequence0)
23 endclass
sequence1 的定义为:
清单 6-11
systemverilog
文件:src/ch6/section6.2/6.2.1/no_pri/my_case0.sv
25 class sequence1 extends uvm_sequence #(my_transaction);
...
32 virtual task body();
...
35 repeat (5) begin
36 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
37 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
38 end
39 #100;
...
42 endtask
43
44 `uvm_object_utils(sequence1)
45 endclass
运行如上代码后,会显示两个 sequence 交替产生 transaction:
text
# UVM_INFO my_case0.sv(15) @ 85900: uvm_test_top.env.i_agt.sqr@@seq0 [sequence0] send one transaction
# UVM_INFO my_case0.sv(37) @ 112500: uvm_test_top.env.i_agt.sqr@@seq1 [sequence1] send one transaction
# UVM_INFO my_case0.sv(15) @ 149300: uvm_test_top.env.i_agt.sqr@@seq0 [sequence0] send one transaction
# UVM_INFO my_case0.sv(37) @ 200500: uvm_test_top.env.i_agt.sqr@@seq1 [sequence1] send one transaction
# UVM_INFO my_case0.sv(15) @ 380700: uvm_test_top.env.i_agt.sqr@@seq0 [sequence0] send one transaction
# UVM_INFO my_case0.sv(37) @ 436500: uvm_test_top.env.i_agt.sqr@@seq1 [sequence1] send one transaction
sequencer 根据什么选择使用哪个 sequence 的 transaction 呢?这是 UVM 的 sequence 机制中的仲裁问题。对于 transaction 来说,存在优先级的概念,通常来说,优先级越高越容易被选中。当使用 uvm_do 或者 uvm_do_with 宏时,产生的 transaction 的优先级是默认的优先级,即 -1。可以通过 uvm_do_pri 及 uvm_do_pri_with 改变所产生的 transaction 的优先级:
清单 6-12
systemverilog
文件:src/ch6/section6.2/6.2.1/item_pri/my_case0.sv
3 class sequence0 extends uvm_sequence #(my_transaction);
...
10 virtual task body();
...
13 repeat (5) begin
14 `uvm_do_pri(m_trans, 100)
15 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
16 end
17 #100;
...
20 endtask
...
23 endclass
24
25 class sequence1 extends uvm_sequence #(my_transaction);
...
32 virtual task body();
...
35 repeat (5) begin
36 `uvm_do_pri_with(m_trans, 200, {m_trans.pload.size < 500;})
37 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
38 end
...
42 endtask
...
45 endclass
uvm_do_pri 与 uvm_do_pri_with 的第二个参数是优先级,这个数值必须是一个大于等于 -1 的整数。数字越大,优先级越高。
由于 sequence1 中 transaction 的优先级较高,所以按照预期,先选择 sequence1 产生的 transaction。当 sequence1 的 transaction 全部生成完毕后,再产生 sequence0 的 transaction。但是运行上述代码,发现并没有如预期的那样,而是 sequence0 与 sequence1 交替产生 transaction。这是因为 sequencer 的仲裁算法有很多种:
清单 6-13
systemverilog
来源:UVM 源代码
SEQ_ARB_FIFO,
SEQ_ARB_WEIGHTED,
SEQ_ARB_RANDOM,
SEQ_ARB_STRICT_FIFO,
SEQ_ARB_STRICT_RANDOM,
SEQ_ARB_USER
在默认情况下 sequencer 的仲裁算法是 SEQ_ARB_FIFO。它会严格遵循先入先出的顺序,而不会考虑优先级。SEQ_ARB_WEIGHTED 是加权的仲裁;SEQ_ARB_RANDOM 是完全随机选择;SEQ_ARB_STRICT_FIFO 是严格按照优先级的,当有多个同一优先级的 sequence 时,按照先入先出的顺序选择;SEQ_ARB_STRICT_RANDOM 是严格按照优先级的,当有多个同一优先级的 sequence 时,随机从最高优先级中选择;SEQ_ARB_USER 则是用户可以自定义一种新的仲裁算法。
因此,若想使优先级起作用,应该设置仲裁算法为 SEQ_ARB_STRICT_FIFO 或者 SEQ_ARB_STRICT_RANDOM:
清单 6-14
systemverilog
文件:src/ch6/section6.2/6.2.1/item_pri/my_case0.sv
57 task my_case0::main_phase(uvm_phase phase);
...
65 env.i_agt.sqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
66 fork
67 seq0.start(env.i_agt.sqr);
68 seq1.start(env.i_agt.sqr);
69 join
70 endtask
经过如上的设置后,会发现直到 sequence1 发送完 transaction 后,sequence0 才开始发送。
除 transaction 有优先级外,sequence 也有优先级的概念。可以在 sequence 启动时指定其优先级:
清单 6-15
systemverilog
文件:src/ch6/section6.2/6.2.1/sequence_pri/my_case0.sv
57 task my_case0::main_phase(uvm_phase phase);
...
65 env.i_agt.sqr.set_arbitration(SEQ_ARB_STRICT_FIFO);
66 fork
67 seq0.start(env.i_agt.sqr, null, 100);
68 seq1.start(env.i_agt.sqr, null, 200);
69 join
70 endtask
start 任务的第一个参数是 sequencer,第二个参数是 parent sequence,可以设置为 null,第三个参数是优先级,如果不指定则此值为 -1,它同样不能设置为一个小于 -1 的数字。
使用代码清单 6-10 中的 sequence0 和代码清单 6-11 中的 sequence1,即不在 uvm_do 系列宏中指定优先级。运行上述代码,会发现 sequence1 中的 transaction 完全发送完后才发送 sequence0 中的 transaction。所以,对 sequence 设置优先级的本质即设置其内产生的 transaction 的优先级。
6.2.2 sequencer的lock操作
当多个 sequence 在一个 sequencer 上同时启动时,每个 sequence 产生出的 transaction 都需要参与 sequencer 的仲裁。那么考虑这样一种情况,某个 sequence 比较奇特,一旦它要执行,那么它所有的 transaction 必须连续地交给 driver,如果中间夹杂着其他 sequence 的 transaction,就会发生错误。要解决这个问题,可以对此 sequence 赋予较高的优先级。
但是假如有其他 sequence 有更高的优先级呢?所以这种解决方法并不科学。在 UVM 中可以使用 lock 操作来解决这个问题。
所谓 lock,就是 sequence 向 sequencer 发送一个请求,这个请求与其他 sequence 发送 transaction 的请求一同被放入 sequencer 的仲裁队列中。当其前面的所有请求被处理完毕后,sequencer 就开始响应这个 lock 请求,此后 sequencer 会一直连续发送此 sequence 的 transaction,直到 unlock 操作被调用。从效果上看,此 sequencer 的所有权并没有被所有的 sequence 共享,而是被申请 lock 操作的 sequence 独占了。一个使用 lock 操作的 sequence 为:
清单 6-16
systemverilog
文件:src/ch6/section6.2/6.2.2/one_lock/my_case0.sv
25 class sequence1 extends uvm_sequence #(my_transaction);
...
32 virtual task body();
...
35 repeat (3) begin
36 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
37 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
38 end
39 lock();
40 `uvm_info("sequence1", "locked the sequencer ", UVM_MEDIUM)
41 repeat (4) begin
42 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
43 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
44 end
45 `uvm_info("sequence1", "unlocked the sequencer ", UVM_MEDIUM)
46 unlock();
47 repeat (3) begin
48 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
49 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
50 end
...
54 endtask
...
57 endclass
将此 sequence1 与代码清单 6-10 中的 sequence0 使用代码清单 6-9 的方式在 env.i_agt.sqr 上启动,会发现:在 lock 语句前,sequence0 和 sequence1 交替产生 transaction;在 lock 语句后,一直发送 sequence1 的 transaction,直到 unlock 语句被调用后,sequence0 和 sequence1 又开始交替产生 transaction。
如果两个 sequence 都试图使用 lock 任务来获取 sequencer 的所有权则会如何呢?答案是先获得所有权的 sequence 在执行完毕后才会将所有权交还给另外一个 sequence。
清单 6-17
systemverilog
文件:src/ch6/section6.2/6.2.2/dual_lock/my_case0.sv
3 class sequence0 extends uvm_sequence #(my_transaction);
...
10 virtual task body();
...
13 repeat (2) begin
14 `uvm_do(m_trans)
15 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
16 end
17 lock();
18 repeat (5) begin
19 `uvm_do(m_trans)
20 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
21 end
22 unlock();
23 repeat (2) begin
24 `uvm_do(m_trans)
25 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
26 end
27 #100;
...
30 endtask
31
32 `uvm_object_utils(sequence0)
33 endclass
将上述 sequence0 与代码清单 6-16 中的 sequence1 同时在 env.i_agt.sqr 上启动,会发现 sequence0 先获得 sequencer 的所有权,在 unlock 函数被调用前,一直发送 sequence0 的 transaction。在 unlock 被调用后,sequence1 获得 sequencer 的所有权,之后一直发送 sequence1 的 transaction,直到 unlock 函数被调用。
6.2.3 sequencer的grab操作
与 lock 操作一样,grab 操作也用于暂时拥有 sequencer 的所有权,只是 grab 操作比 lock 操作优先级更高。lock 请求是被插入 sequencer 仲裁队列的最后面,等到它时,它前面的仲裁请求都已经结束了。grab 请求则被放入 sequencer 仲裁队列的最前面,它几乎是一发出就拥有了 sequencer 的所有权:
清单 6-18
systemverilog
文件:src/ch6/section6.2/6.2.3/my_case0.sv
25 class sequence1 extends uvm_sequence #(my_transaction);
...
32 virtual task body();
...
35 repeat (3) begin
36 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
37 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
38 end
39 grab();
40 `uvm_info("sequence1", "grab the sequencer ", UVM_MEDIUM)
41 repeat (4) begin
42 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
43 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
44 end
45 `uvm_info("sequence1", "ungrab the sequencer ", UVM_MEDIUM)
46 ungrab();
47 repeat (3) begin
48 `uvm_do_with(m_trans, {m_trans.pload.size < 500;})
49 `uvm_info("sequence1", "send one transaction", UVM_MEDIUM)
50 end
...
54 endtask
55
56 `uvm_object_utils(sequence1)
57 endclass
如果两个 sequence 同时试图使用 grab 任务获取 sequencer 的所有权将会如何呢?这种情况与两个 sequence 同时试图调用 lock 函数一样,在先获得所有权的 sequence 执行完毕后才会将所有权交还给另外一个试图所有权的 sequence。
如果一个 sequence 在使用 grab 任务获取 sequencer 的所有权前,另外一个 sequence 已经使用 lock 任务获得了 sequencer 的所有权则会如何呢?答案是 grab 任务会一直等待 lock 的释放。grab 任务还是比较讲文明的,虽然它会插队,但是绝不会打断别人正在进行的事情。
6.2.4 sequence的有效性
当有多个 sequence 同时在一个 sequencer 上启动时,所有的 sequence 都参与仲裁,根据算法决定哪个 sequence 发送 transaction。仲裁算法是由 sequencer 决定的,sequence 除了可以在优先级上进行设置外,对仲裁的结果无能为力。
通过 lock 任务和 grab 任务,sequence 可以独占 sequencer,强行使 sequencer 发送自己产生的 transaction。同样的,UVM 也提供措施使 sequence 可以在一定时间内不参与仲裁,即令此 sequence 失效。
sequencer 在仲裁时,会查看 sequence 的 is_relevant 函数的返回结果。如果为 1,说明此 sequence 有效,否则无效。因此可以通过重载 is_relevant 函数来使 sequence 失效:
清单 6-19
systemverilog
文件:src/ch6/section6.2/6.2.4/is_relevant/my_case0.sv
3 class sequence0 extends uvm_sequence #(my_transaction);
4 my_transaction m_trans;
5 int num;
6 bit has_delayed;
...
14 virtual function bit is_relevant();
15 if((num >= 3)&&(!has_delayed)) return 0;
16 else return 1;
17 endfunction
18
19 virtual task body();
...
22 fork
23 repeat (10) begin
24 num++;
25 `uvm_do(m_trans)
26 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
27 end
28 while(1) begin
29 if(!has_delayed) begin
30 if(num >= 3) begin
31 `uvm_info("sequence0", "begin to delay", UVM_MEDIUM)
32 #500000;
33 has_delayed = 1'b1;
34 `uvm_info("sequence0", "end delay", UVM_MEDIUM)
35 break;
36 end
37 else
38 #1000;
39 end
40 end
41 join
...
45 endtask
...
48 endclass
这个 sequence 在发送了 3 个 transaction 后开始变为无效,延时 500000 时间单位后又开始有效。将此 sequence 与代码清单 6-11 中的 sequence1 同时启动,会发现在失效前 sequence0 和 sequence1 交替发送 transaction;而在失效的 500000 时间单位内,只有 sequence1 发送 transaction;当 sequence0 重新变有效后,sequence0 和 sequence1 又开始交替发送 transaction。从某种程度上来说,is_relevant 与 grab 任务和 lock 任务是完全相反的。通过设置 is_relevant,可以使 sequence 主动放弃 sequencer 的使用权,而 grab 任务和 lock 任务则强占 sequencer 的所有权。
除了 is_relevant 外,sequence 中还有一个任务 wait_for_relevant 也与 sequence 的有效性相关:
清单 6-20
systemverilog
文件:src/ch6/section6.2/6.2.4/wait_for_relevant/my_case0.sv
3 class sequence0 extends uvm_sequence #(my_transaction);
...
14 virtual function bit is_relevant();
15 if((num >= 3)&&(!has_delayed)) return 0;
16 else return 1;
17 endfunction
18
19 virtual task wait_for_relevant();
20 #10000;
21 has_delayed = 1;
22 endtask
23
24 virtual task body();
...
27 repeat (10) begin
28 num++;
29 `uvm_do(m_trans)
30 `uvm_info("sequence0", "send one transaction", UVM_MEDIUM)
31 end
...
35 endtask
...
38 endclass
当 sequencer 发现在其上启动的所有 sequence 都无效时,此时会调用 wait_for_relevant 并等待 sequence 变有效。当此 sequence 与代码清单 6-11 中的 sequence1 同时启动,并发送了 3 个 transaction 后,sequence0 变为无效。此后 sequencer 一直发送 sequence1 的 transaction,直到全部的 transaction 都发送完毕。此时,sequencer 发现 sequence0 无效,会调用其 wait_for_relevant。换言之,sequence0 失效是自己控制的,但是重新变得有效却是受其他 sequence 的控制。如果其他 sequence 永远不结束,那么 sequence0 将永远处于失效状态。这里与代码清单 6-19 中例子的区别是,代码清单 6-19 例子中 sequence0 并不是等待着 sequence1 的 transaction 全部发送完毕,而是自己主动控制着自己何时有效何时无效。
在 wait_for_relevant 中,必须将使 sequence 无效的条件清除。在代码清单 6-20 中,假如 wait_for_relevant 只是如下定义:
清单 6-21
systemverilog
virtual task wait_for_relevant();
#10000;
endtask
那么当 wait_for_relevant 返回后,sequencer 会继续调用 sequence0 的 is_relevant,发现依然是无效状态,则继续调用 wait_for_relevant。系统会处于死循环的状态。
在代码清单 6-19 的例子中,通过控制延时(500000)单位时间来使 sequence0 重新变得有效。假如在这段时间内,sequence1 的 transaction 发送完毕后,而 sequence0 中又没有重载 wait_for_relevant 任务,那么将会给出如下错误提示:
text
UVM_FATAL @ 1166700: uvm_test_top.env.i_agt.sqr@@seq0 [RELMSM]
is_relevant()was implemented without defining wait_for_relevant()
因此,is_relevant 与 wait_for_relevant 一般应成对重载,不能只重载其中的一个。代码清单 6-19 的例子中没有重载 wait_for_relevant,是因为巧妙地设置了延时,可以保证不会调用到 wait_for_relevant。读者在使用时应该重载 wait_for_relevant 这个任务。
6.3 sequence相关宏及其实现
6.3.1 uvm_do系列宏
uvm_do 系列宏主要有以下 8 个:
清单 6-22
systemverilog
来源:UVM 源代码
`uvm_do(SEQ_OR_ITEM)
`uvm_do_pri(SEQ_OR_ITEM, PRIORITY)
`uvm_do_with(SEQ_OR_ITEM, CONSTRAINTS)
`uvm_do_pri_with(SEQ_OR_ITEM, PRIORITY, CONSTRAINTS)
`uvm_do_on(SEQ_OR_ITEM, SEQR)
`uvm_do_on_pri(SEQ_OR_ITEM, SEQR, PRIORITY)
`uvm_do_on_with(SEQ_OR_ITEM, SEQR, CONSTRAINTS)
`uvm_do_on_pri_with(SEQ_OR_ITEM, SEQR, PRIORITY, CONSTRAINTS)
其中 uvm_do、uvm_do_with、uvm_do_pri、uvm_do_pri_with 在前面已经提到过了,这里只介绍另外 4 个。
uvm_do_on 用于显式地指定使用哪个 sequencer 发送此 transaction。它有两个参数,第一个是 transaction 的指针,第二个是 sequencer 的指针。当在 sequence 中使用 uvm_do 等宏时,其默认的 sequencer 就是此 sequence 启动时为其指定的 sequencer,sequence 将这个 sequencer 的指针放在其成员变量 m_sequencer 中。事实上,uvm_do 等价于:
清单 6-23
systemverilog
`uvm_do_on(tr, this.m_sequencer)
在这里看起来指定使用哪个 sequencer 似乎并没有用,它的真正作用要在 6.5 节 virtual sequence 中得到体现。
uvm_do_on_pri,它有三个参数,第一个参数是 transaction 的指针,第二个是 sequencer 的指针,第三个是优先级:
清单 6-24
systemverilog
`uvm_do_on(tr, this, 100)
uvm_do_on_with,它有三个参数,第一个参数是 transaction 的指针,第二个是 sequencer 的指针,第三个是约束:
清单 6-25
systemverilog
`uvm_do_on_with(tr, this, {tr.pload.size == 100;})
uvm_do_on_pri_with,它有四个参数,是所有 uvm_do 宏中参数最多的一个。第一个参数是 transaction 的指针,第二个是 sequencer 的指针,第三个是优先级,第四个是约束:
清单 6-26
systemverilog
`uvm_do_on_pri_with(tr, this, 100, {tr.pload.size == 100;})
uvm_do 系列的其他七个宏其实都是用 uvm_do_on_pri_with 宏来实现的。如 uvm_do 宏:
清单 6-27
systemverilog
来源:UVM 源代码
`define uvm_do(SEQ_OR_ITEM) \
`uvm_do_on_pri_with(SEQ_OR_ITEM, m_sequencer, -1, {})
6.3.2 uvm_create与uvm_send
除了使用 uvm_do 宏产生 transaction,还可以使用 uvm_create 宏与 uvm_send 宏来产生:
清单 6-28
systemverilog
文件:src/ch6/section6.3/6.3.2/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
10 virtual task body();
11 int num = 0;
12 int p_sz;
...
15 repeat (10) begin
16 num++;
17 `uvm_create(m_trans)
18 assert(m_trans.randomize());
19 p_sz = m_trans.pload.size();
20 {m_trans.pload[p_sz - 4],
21 m_trans.pload[p_sz - 3],
22 m_trans.pload[p_sz - 2],
23 m_trans.pload[p_sz - 1]}
24 = num;
25 `uvm_send(m_trans)
26 end
...
30 endtask
...
33 endclass
uvm_create 宏的作用是实例化 transaction。当一个 transaction 被实例化后,可以对其做更多的处理,处理完毕后使用 uvm_send 宏发送出去。这种使用方式比 uvm_do 系列宏更加灵活。如在上例中,就将 pload 的最后 4 个 byte 替换为此 transaction 的序号。
事实上,在上述的代码中,也完全可以不使用 uvm_create 宏,而直接调用 new 进行实例化:
清单 6-29
systemverilog
virtual task body();
...
m_trans = new("m_trans");
assert(m_trans.randomize());
p_sz = m_trans.pload.size();
{m_trans.pload[p_sz - 4],
m_trans.pload[p_sz - 3],
m_trans.pload[p_sz - 2],
m_trans.pload[p_sz - 1]}
= num;
`uvm_send(m_trans)
...
endtask
除了 uvm_send 外,还有 uvm_send_pri 宏,它的作用是在将 transaction 交给 sequencer 时设定优先级:
清单 6-30
systemverilog
virtual task body();
...
m_trans = new("m_trans");
assert(m_trans.randomize());
p_sz = m_trans.pload.size();
{m_trans.pload[p_sz - 4],
m_trans.pload[p_sz - 3],
m_trans.pload[p_sz - 2],
m_trans.pload[p_sz - 1]}
= num;
`uvm_send_pri(m_trans, 200)
...
endtask
6.3.3 uvm_rand_send系列宏
uvm_rand_send 系列宏有如下几个:
清单 6-31
systemverilog
来源:UVM 源代码
`uvm_rand_send(SEQ_OR_ITEM)
`uvm_rand_send_pri(SEQ_OR_ITEM, PRIORITY)
`uvm_rand_send_with(SEQ_OR_ITEM, CONSTRAINTS)
`uvm_rand_send_pri_with(SEQ_OR_ITEM, PRIORITY, CONSTRAINTS)
uvm_rand_send 宏与 uvm_send 宏类似,唯一的区别是它会对 transaction 进行随机化。这个宏使用的前提是 transaction 已经被分配了空间,换言之,即已经实例化了:
清单 6-32
systemverilog
m_trans = new("m_trans");
`uvm_rand_send(m_trans)
uvm_rand_send_pri 宏用于指定 transaction 的优先级。它有两个参数,第一个是 transaction 的指针,第二个是优先级:
清单 6-33
systemverilog
m_trans = new("m_trans");
`uvm_rand_send_pri(m_trans, 100)
uvm_rand_send_with 宏,用于指定使用随机化时的约束,它有两个参数,第一个是 transaction 的指针,第二个是约束:
清单 6-34
systemverilog
m_trans = new("m_trans");
`uvm_rand_send_with(m_trans, {m_trans.pload.size == 100;})
uvm_rand_send_pri_with 宏,用于指定优先级和约束,它有三个参数,第一个是 transaction 的指针,第二个是优先级,第三个是约束:
清单 6-35
systemverilog
m_trans = new("m_trans");
`uvm_rand_send_pri_with(m_trans, 100, {m_trans.pload.size == 100;})
uvm_rand_send 系列宏及 uvm_send 系列宏的意义主要在于,如果一个 transaction 占用的内存比较大,那么很可能希望前后两次发送的 transaction 都使用同一块内存,只是其中的内容可以不同,这样比较节省内存。
6.3.4 start_item与finish_item
在前面的章节中一直使用宏来产生 transaction。宏隐藏了细节,方便了用户的使用,但是也给用户带来了困扰:宏到底做了什么事情?
不使用宏产生 transaction 的方式要依赖于两个任务:start_item 和 finish_item。在使用这两个任务前,必须要先实例化 transaction 后才可以调用这两个任务:
清单 6-36
systemverilog
tr = new("tr");
start_item(tr);
finish_item(tr);
完整使用如上两个任务构建的一个 sequence 如下:
清单 6-37
systemverilog
virtual task body();
repeat(10) begin
tr = new("tr");
start_item(tr);
finish_item(tr);
end
endtask
上述代码中并没有对 tr 进行随机化。可以在 transaction 实例化后、finish_item 调用前对其进行随机化:
清单 6-38
systemverilog
文件:src/ch6/section6.3/6.3.4/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
9 virtual task body();
...
13 repeat (10) begin
14 tr = new("tr");
15 assert(tr.randomize() with {tr.pload.size == 200;});
16 start_item(tr);
17 finish_item(tr);
18 end
...
22 endtask
...
25 endclass
上述 assert 语句也可以放在 start_item 之后、finish_item 之前。uvm_do 系列宏其实是将下述动作封装在了一个宏中:
清单 6-39
systemverilog
virtual task body();
...
tr = new("tr");
start_item(tr);
assert(tr.randomize() with {tr.pload.size() == 200;});
finish_item(tr);
...
endtask
如果要指定 transaction 的优先级,那么要在调用 start_item 和 finish_item 时都要加入优先级参数:
清单 6-40
systemverilog
virtual task body();
...
start_item(tr, 100);
finish_item(tr, 100);
...
endtask
如果不指定优先级参数,默认的优先级为 -1。
6.3.5 pre_do、mid_do与post_do
uvm_do 宏封装了从 transaction 实例化到发送的一系列操作,封装的越多,则其灵活性越差。为了增加 uvm_do 系列宏的功能,UVM 提供了三个接口:pre_do、mid_do 与 post_do。
pre_do 是一个任务,在 start_item 中被调用,它是 start_item 返回前执行的最后一行代码,在它执行完毕后才对 transaction 进行随机化。mid_do 是一个函数,位于 finish_item 的最开始。在执行完此函数后,finish_item 才进行其他操作。post_do 也是一个函数,也位于 finish_item 中,它是 finish_item 返回前执行的最后一行代码。它们的执行顺序大致为:
清单 6-41
systemverilog
sequencer.wait_for_grant(prior) (task) \
start_item \
parent_seq.pre_do(1) (task) /
\
\
`uvm_do* macros \
parent_seq.mid_do(item) (func) \
/ \
sequencer.send_request(item) (func) \
finish_item / \
sequencer.wait_for_item_done() (task) /
parent_seq.post_do(item) (func) /
wait_for_grant、send_request 及 wait_for_item_done 都是 UVM 内部的一些接口。
这三个接口函数/任务的使用示例如下:
清单 6-42
systemverilog
文件:src/ch6/section6.3/6.3.5/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
4 my_transaction m_trans;
5 int num;
...
11 virtual task pre_do(bit is_item);
12 #100;
13 `uvm_info("sequence0", "this is pre_do", UVM_MEDIUM)
14 endtask
15
16 virtual function void mid_do(uvm_sequence_item this_item);
17 my_transaction tr;
18 int p_sz;
19 `uvm_info("sequence0", "this is mid_do", UVM_MEDIUM)
20 void'($cast(tr, this_item));
21 p_sz = tr.pload.size();
22 {tr.pload[p_sz - 4],
23 tr.pload[p_sz - 3],
24 tr.pload[p_sz - 2],
25 tr.pload[p_sz - 1]} = num;
26 tr.crc = tr.calc_crc();
27 tr.print();
28 endfunction
29
30 virtual function void post_do(uvm_sequence_item this_item);
31 `uvm_info("sequence0", "this is post_do", UVM_MEDIUM)
32 endfunction
33
34 virtual task body();
...
37 repeat (10) begin
38 num++;
39 `uvm_do(m_trans)
40 end
...
44 endtask
...
47 endclass
pre_do 有一个参数,此参数用于表明 uvm_do 宏是在对一个 transaction 还是在对一个 sequence 进行操作,关于这一点请参考 6.4.1 节。mid_do 和 post_do 的两个参数是正在操作的 sequence 或者 item 的指针,但是其类型是 uvm_sequence_item 类型。通过 cast 可以转换成目标类型(示例中为 my_transaction)。
6.4 sequence进阶应用
6.4.1 嵌套的sequence
假设一个产生 CRC 错误包的 sequence 如下:
清单 6-43
systemverilog
文件:src/ch6/section6.4/6.4.1/start/my_case0.sv
4 class crc_seq extends uvm_sequence#(my_transaction);
...
10 virtual task body();
11 my_transaction tr;
12 `uvm_do_with(tr, {tr.crc_err == 1;
13 tr.dmac == 48'h980F;})
14 endtask
15 endclass
另外一个产生长包的 sequence 如下:
清单 6-44
systemverilog
文件:src/ch6/section6.4/6.4.1/start/my_case0.sv
17 class long_seq extends uvm_sequence#(my_transaction);
...
23 virtual task body();
24 my_transaction tr;
25 `uvm_do_with(tr, {tr.crc_err == 0;
26 tr.pload.size() == 1500;
27 tr.dmac == 48'hF675;})
28 endtask
29 endclass
现在要写一个新的 sequence,它可以交替产生上面的两种包。那么在新的 sequence 里面可以这样写:
清单 6-45
systemverilog
class case0_sequence extends uvm_sequence #(my_transaction);
virtual task body();
my_transaction tr;
repeat (10) begin
`uvm_do_with(tr, {tr.crc_err == 1;
tr.dmac == 48'h980F;})
`uvm_do_with(tr, {tr.crc_err == 0;
tr.pload.size() == 1500;
tr.dmac == 48'hF675;})
end
endtask
endclass
似乎这样写起来显得特别麻烦。产生的两种不同的包中,第一个约束条件有两个,第二个约束条件有三个。但是假如约束条件有十个呢?如果整个验证平台中有 30 个测试用例都用到这样的两种包,那就要在这 30 个测试用例的 sequence 中加入这些代码,这是一件相当恐怖的事情,而且特别容易出错。既然已经定义好 crc_seq 和 long_seq,那么有没有简单的方法呢?答案是肯定的。在一个 sequence 的 body 中,除了可以使用 uvm_do 宏产生 transaction 外,其实还可以启动其他的 sequence,即一个 sequence 内启动另外一个 sequence,这就是嵌套的 sequence:
清单 6-46
systemverilog
文件:src/ch6/section6.4/6.4.1/start/my_case0.sv
31 class case0_sequence extends uvm_sequence #(my_transaction);
...
37 virtual task body();
38 crc_seq cseq;
39 long_seq lseq;
...
42 repeat (10) begin
43 cseq = new("cseq");
44 cseq.start(m_sequencer);
45 lseq = new("lseq");
46 lseq.start(m_sequencer);
47 end
...
51 endtask
...
54 endclass
直接在新的 sequence 的 body 中调用定义好的 sequence,从而实现 sequence 的重用。这个功能是非常强大的。在上面代码中,m_sequencer 是 case0_sequence 在启动后所使用的 sequencer 的指针。但通常来说并不用这么麻烦,可以使用 uvm_do 宏来完成这些事情:
清单 6-47
systemverilog
文件:src/ch6/section6.4/6.4.1/uvm_do/my_case0.sv
31 class case0_sequence extends uvm_sequence #(my_transaction);
...
38 virtual task body();
39 crc_seq cseq;
40 long_seq lseq;
...
43 repeat (10) begin
44 `uvm_do(cseq)
45 `uvm_do(lseq)
46 end
...
50 endtask
...
53 endclass
uvm_do 系列宏中,其第一个参数除了可以是 transaction 的指针外,还可以是某个 sequence 的指针。当第一个参数是 transaction 时,它如 6.3.4 节代码清单 6-39 中所示,调用 start_item 和 finish_item;当第一个参数是 sequence 时,它调用此 sequence 的 start 任务。
除了 uvm_do 宏外,前面介绍的 uvm_send 宏、uvm_rand_send 宏、uvm_create 宏,其第一个参数都可以是 sequence 的指针。唯一例外的是 start_item 与 finish_item,这两个任务的参数必须是 transaction 的指针。
6.4.2 在sequence中使用rand类型变量
在 transaction 的定义中,通常使用 rand 来对变量进行修饰,说明在调用 randomize 时要对此字段进行随机化。其实在 sequence 中也可以使用 rand 修饰符。有如下的 sequence,它有成员变量 ldmac:
清单 6-48
systemverilog
文件:src/ch6/section6.4/6.4.2/rand/my_case0.sv
4 class long_seq extends uvm_sequence#(my_transaction);
5 rand bit[47:0] ldmac;
...
11 virtual task body();
12 my_transaction tr;
13 `uvm_do_with(tr, {tr.crc_err == 0;
14 tr.pload.size() == 1500;
15 tr.dmac == ldmac;})
16 tr.print();
17 endtask
18 endclass
这个 sequence 可以作为底层的 sequence 被顶层的 sequence 调用:
清单 6-49
systemverilog
文件:src/ch6/section6.4/6.4.2/rand/my_case0.sv
20 class case0_sequence extends uvm_sequence #(my_transaction);
...
27 virtual task body();
28 long_seq lseq;
...
31 repeat (10) begin
32 `uvm_do_with(lseq, {lseq.ldmac == 48'hFFFF;})
33 end
...
37 endtask
...
40 endclass
sequence 里可以添加任意多的 rand 修饰符,用以规范它产生的 transaction。sequence 与 transaction 都可以调用 randomize 进行随机化,都可以有 rand 修饰符的成员变量,从某种程度上来说,二者的界限比较模糊。这也就是为什么 uvm_do 系列宏可以接受 sequence 作为其参数的原因。
在 sequence 中定义 rand 类型变量时,要注意变量的命名。很多人习惯于变量的名字和 transaction 中相应字段的名字一致:
清单 6-50
systemverilog
文件:src/ch6/section6.4/6.4.2/name/my_case0.sv
4 class long_seq extends uvm_sequence#(my_transaction);
5 rand bit[47:0] dmac;
...
11 virtual task body();
12 my_transaction tr;
13 `uvm_do_with(tr, {tr.crc_err == 0;
14 tr.pload.size() == 1500;
15 tr.dmac == dmac;})
16 tr.print();
17 endtask
18 endclass
在 case0_sequence 中启动上述 sequence,并将 dmac 地址约束为 48'hFFFF,此时将会发现产生的 transaction 的 dmac 并不是 48'hFFFF,而是一个随机值!这是因为,当运行到上述代码的第 15 行时,编译器会首先去 my_transaction 寻找 dmac,如果找到了,就不再继续寻找。换言之,上述代码第 13 到第 15 行等价于:
清单 6-51
systemverilog
`uvm_do_with(tr, {tr.crc_err == 0;
tr.pload.size() == 1500;
tr.dmac == tr.dmac;})
long_seq 中的 dmac 并没有起到作用。所以,在 sequence 中定义 rand 类型变量以向产生的 transaction 传递约束时,变量的名字一定要与 transaction 中相应字段的名字不同。
6.4.3 transaction类型的匹配
一个 sequencer 只能产生一种类型的 transaction,一个 sequence 如果要想在此 sequencer 上启动,那么其所产生的 transaction 的类型必须是这种 transaction 或者派生自这种 transaction。
如果一个 sequence 中产生的 transaction 的类型不是此种 transaction,那么将会报错:
清单 6-52
systemverilog
class case0_sequence extends uvm_sequence #(my_transaction);
your_transaction y_trans;
virtual task body();
repeat (10) begin
`uvm_do(y_trans)
end
endtask
endclass
嵌套 sequence 的前提是,在嵌套里面的所有 sequence 产生的 transaction 都可以被同一个 sequencer 所接受。
那么有没有办法将两个截然不同的 transaction 交给同一个 sequencer 呢?可以,只是需要将 sequencer 和 driver 能够接受的数据类型设置为 uvm_sequence_item:
清单 6-53
systemverilog
class my_sequencer extends uvm_sequencer #(uvm_sequence_item);
class my_driver extends uvm_driver#(uvm_sequence_item);
在 sequence 中可以交替发送 my_transaction 和 your_transaction:
清单 6-54
systemverilog
文件:src/ch6/section6.4/6.4.3/my_case0.sv
12 class case0_sequence extends uvm_sequence;
13 my_transaction m_trans;
14 your_transaction y_trans;
...
20 virtual task body();
...
23 repeat (10) begin
24 `uvm_do(m_trans)
25 `uvm_do(y_trans)
26 end
...
30 endtask
31
32 `uvm_object_utils(case0_sequence)
33 endclass
这样带来的问题是,由于 driver 中接收的数据类型是 uvm_sequence_item,如果它要使用 my_transaction 或者 your_transaction 中的成员变量,必须使用 cast 转换:
清单 6-55
systemverilog
文件:src/ch6/section6.4/6.4.3/my_driver.sv
24 task my_driver::main_phase(uvm_phase phase);
25 my_transaction m_tr;
26 your_transaction y_tr;
...
31 while(1) begin
32 seq_item_port.get_next_item(req);
33 if($cast(m_tr, req)) begin
34 drive_my_transaction(m_tr);
35 `uvm_info("driver", "receive a transaction whose type is my_transaction", UVM_MEDIUM)
36 end
37 else if($cast(y_tr, req)) begin
38 drive_your_transaction(y_tr);
39 `uvm_info("driver", "receive a transaction whose type is your_transaction", UVM_MEDIUM)
40 end
41 else begin
42 `uvm_error("driver", "receive a transaction whose type is unknown")
43 end
44 seq_item_port.item_done();
45 end
46 endtask
6.4.4 p_sequencer的使用
考虑如下一种情况,在 sequencer 中存在如下成员变量:
清单 6-56
systemverilog
文件:src/ch6/section6.4/6.4.4/my_sequencer.sv
4 class my_sequencer extends uvm_sequencer #(my_transaction);
5 bit[47:0] dmac;
6 bit[47:0] smac;
...
12 virtual function void build_phase(uvm_phase phase);
13 super.build_phase(phase);
14 void'(uvm_config_db#(bit[47:0])::get(this, "", "dmac", dmac));
15 void'(uvm_config_db#(bit[47:0])::get(this, "", "smac", smac));
16 endfunction
17
18 `uvm_component_utils(my_sequencer)
19 endclass
在其 build_phase 中,使用 config_db::get 得到这两个成员变量的值。之后 sequence 在发送 transaction 时,必须将目的地址设置为 dmac,源地址设置为 smac。现在的问题是,如何在 sequence 的 body 中得到这两个变量的值呢?
在 6.4.1 节中介绍嵌套的 sequence 时,引入了 m_sequencer 这个属于每个 sequence 的成员变量,但是如果直接使用 m_sequencer 得到这两个变量的值:
清单 6-57
systemverilog
virtual task body();
...
repeat (10) begin
`uvm_do_with(m_trans, {m_trans.dmac == m_sequencer.dmac;
m_trans.smac == m_sequencer.smac;})
end
...
endtask
如上写法会引起编译错误。其根源在于 m_sequencer 是 uvm_sequencer_base(uvm_sequencer 的基类)类型的,而不是 my_sequencer 类型的。m_sequencer 的原型为:
清单 6-58
systemverilog
来源:UVM 源代码
protected uvm_sequencer_base m_sequencer;
但是由于 case0_sequence 在 my_sequencer 上启动,其中的 m_sequencer 本质上是 my_sequencer 类型的,所以可以在 my_sequence 中通过 cast 转换将 m_sequencer 转换成 my_sequencer 类型,并引用其中的 dmac 和 smac:
清单 6-59
systemverilog
virtual task body();
my_sequencer x_sequencer;
...
$cast(x_sequencer, m_sequencer);
repeat (10) begin
`uvm_do_with(m_trans, {m_trans.dmac == x_sequencer.dmac;
m_trans.smac == x_sequencer.smac;})
end
...
endtask
上述过程稍显麻烦。在实际的验证平台中,用到 sequencer 中成员变量的情况非常多。UVM 考虑到这种情况,内建了一个宏:uvm_declare_p_sequencer(SEQUENCER)。这个宏的本质是声明了一个 SEQUENCER 类型的成员变量,如在定义 sequence 时,使用此宏声明 sequencer 的类型:
清单 6-60
systemverilog
文件:src/ch6/section6.4/6.4.4/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
4 my_transaction m_trans;
5 `uvm_object_utils(case0_sequence)
6 `uvm_declare_p_sequencer(my_sequencer)
...
24 endclass
则相当于声明了如下的成员变量:
清单 6-61
systemverilog
class case0_sequence extends uvm_sequence #(my_transaction);
my_sequencer p_sequencer;
...
endclass
UVM 之后会自动将 m_sequencer 通过 cast 转换成 p_sequencer。这个过程在 pre_body() 之前就完成了。因此在 sequence 中可以直接使用成员变量 p_sequencer 来引用 dmac 和 smac:
清单 6-62
systemverilog
文件:src/ch6/section6.4/6.4.4/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
12 virtual task body();
...
15 repeat (10) begin
16 `uvm_do_with(m_trans, {m_trans.dmac == p_sequencer.dmac;
17 m_trans.smac == p_sequencer.smac;})
18 end
...
22 endtask
23
24 endclass
6.4.5 sequence的派生与继承
sequence 作为一个类,是可以从其中派生其他 sequence 的:
清单 6-63
systemverilog
文件:src/ch6/section6.4/6.4.5/my_case0.sv
4 class base_sequence extends uvm_sequence #(my_transaction);
5 `uvm_object_utils(base_sequence)
6 `uvm_declare_p_sequencer(my_sequencer)
7 function new(string name= "base_sequence");
8 super.new(name);
9 endfunction
10 //define some common function and task
11 endclass
12
13 class case0_sequence extends base_sequence;
...
31 endclass
由于在同一个项目中各 sequence 都是类似的,所以可以将很多公用的函数或者任务写在 base sequence 中,其他 sequence 都从此 sequence 派生。
普通的 sequence 这样使用没有任何问题,但对于那些使用了 uvm_declare_p_sequencer 声明 p_sequencer 的 base sequence,在派生的 sequence 中是否也要调用此宏声明 p_sequencer?这个问题的答案是否定的,因为 uvm_declare_p_sequencer 的实质是在 base sequence 中声明了一个成员变量 p_sequencer。当其他的 sequence 从其派生时,p_sequencer 依然是新的 sequence 的成员变量,所以无须再声明一次了。
当然了,如果再声明一次,系统也并不会报错:
清单 6-64
systemverilog
class base_sequence extends uvm_sequence #(my_transaction);
`uvm_object_utils(base_sequence)
`uvm_declare_p_sequencer(my_sequencer)
...
endclass
class case0_sequence extends base_sequence;
`uvm_object_utils(case0_sequence)
`uvm_declare_p_sequencer(my_sequencer)
...
endclass
虽然这相当于连续声明了两个成员变量 p_sequencer,但是由于这两个成员变量一个是属于父类的,一个是属于子类的,所以并不会出错。
6.5 virtual sequence的使用
6.5.1 带双路输入输出端口的DUT
在本书以前所有的例子中,使用的 DUT 几乎都是基于 2.2.1 节中所示的最简单的 DUT。为了说明 virtual sequence,本节引入附录 B 的代码 B-1 所示的 DUT。这个 DUT 相当于在 2.2.1 节所示的 DUT 的基础上增加了一组数据口,这组新的数据口与原先的数据口功能完全一样。新的数据端口增加后,由于这组新的数据端口与原先的一模一样,所以可以在 test 中再额外实例化一个 my_env:
清单 6-65
systemverilog
文件:src/ch6/section6.5/6.5.1/base_test.sv
4 class base_test extends uvm_test;
5
6 my_env env0;
7 my_env env1;
...
16 endclass
17
18
19 function void base_test::build_phase(uvm_phase phase);
20 super.build_phase(phase);
21 env0 = my_env::type_id::create("env0", this);
22 env1 = my_env::type_id::create("env1", this);
23 endfunction
在 top_tb 中做相应更改,多增加一组 my_if,并通过 config_db 将其设置为新的 env 中的 driver 和 monitor:
清单 6-66
systemverilog
文件:src/ch6/section6.5/6.5.1/top_tb.sv
17 module top_tb;
...
22 my_if input_if0(clk, rst_n);
23 my_if input_if1(clk, rst_n);
24 my_if output_if0(clk, rst_n);
25 my_if output_if1(clk, rst_n);
26
27 dut my_dut(.clk(clk),
28 .rst_n(rst_n),
29 .rxd0(input_if0.data),
30 .rx_dv0(input_if0.valid),
31 .rxd1(input_if1.data),
32 .rx_dv1(input_if1.valid),
33 .txd0(output_if0.data),
34 .tx_en0(output_if0.valid),
35 .txd1(output_if1.data),
36 .tx_en1(output_if1.valid));
...
55 initial begin
56 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env0.i_agt.drv", "vif", input_if0);
57 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env0.i_agt.mon","vif", input_if0);
58 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env0.o_agt.mon", "vif", output_if0);
59 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env1.i_agt.drv", "vif", input_if1);
60 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env1.i_agt.mon", "vif", input_if1);
61 uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env1.o_agt.mon", "vif", output_if1);
62 end
63
64 endmodule
通过在测试用例中设置两个 default_sequence,可以分别向两个数据端口施加激励:
清单 6-67
systemverilog
文件:src/ch6/section6.5/6.5.1/my_case0.sv
36 function void my_case0::build_phase(uvm_phase phase);
37 super.build_phase(phase);
38
39 uvm_config_db#(uvm_object_wrapper)::set(this,
40 "env0.i_agt.sqr.main_phase",
41 "default_sequence",
42 case0_sequence::type_id::get());
43 uvm_config_db#(uvm_object_wrapper)::set(this,
44 "env1.i_agt.sqr.main_phase",
45 "default_sequence",
46 case0_sequence::type_id::get());
47 endfunction
6.5.2 sequence之间的简单同步
在这个新的验证平台中有两个 driver,它们原本是完全等价的,但是出于某些原因的考虑,如 DUT 要求 driver0 必须先发送一个最大长度的包,在此基础上 driver1 才可以发送包。这是一个 sequence 之间同步的过程,一种很自然的想法是,将这个同步的过程使用一个全局的事件来完成:
清单 6-68
systemverilog
文件:src/ch6/section6.5/6.5.2/my_case0.sv
3 event send_over;//global event
4 class drv0_seq extends uvm_sequence #(my_transaction);
...
12 virtual task body();
...
15 `uvm_do_with(m_trans, {m_trans.pload.size == 1500;})
16 ->send_over;
17 repeat (10) begin
18 `uvm_do(m_trans)
19 `uvm_info("drv0_seq", "send one transaction", UVM_MEDIUM)
20 end
...
24 endtask
25 endclass
26
27 class drv1_seq extends uvm_sequence #(my_transaction);
...
35 virtual task body();
...
38 @send_over;
39 repeat (10) begin
40 `uvm_do(m_trans)
41 `uvm_info("drv1_seq", "send one transaction", UVM_MEDIUM)
42 end
...
46 endtask
47 endclass
之后,通过 uvm_config_db 的方式分别将这两个 sequence 作为 env0.i_agt.sqr 和 env1.i_agt.sqr 的 default_sequence:
清单 6-69
systemverilog
文件:src/ch6/section6.5/6.5.2/my_case0.sv
60 function void my_case0::build_phase(uvm_phase phase);
61 super.build_phase(phase);
62
63 uvm_config_db#(uvm_object_wrapper)::set(this,
64 "env0.i_agt.sqr.main_phase",
65 "default_sequence",
66 drv0_seq::type_id::get());
67 uvm_config_db#(uvm_object_wrapper)::set(this,
68 "env1.i_agt.sqr.main_phase",
69 "default_sequence",
70 drv1_seq::type_id::get());
71 endfunction
当进入到 main_phase 时,这两个 sequence 会同步启动,但是由于 drv1_seq 要等待 send_over 事件的到来,所以它并不会马上产生 transaction,而 drv0_seq 则会直接产生 transaction。当 drv0_seq 发送完一个最长包后,send_over 事件被触发,于是 drv1_seq 开始产生 transaction。
6.5.3 sequence之间的复杂同步
上节中解决同步的方法看起来非常简单、实用。不过这里有两个问题,第一个问题是使用了一个全局的事件 send_over。全局变量对于初写代码的人来说是非常受欢迎的,但是几乎所有的老师及书本中都会这么说:除非有必要,否则尽量不要使用全局变量。使用全局变量的主要问题即它是全局可见的,本来只是打算在 drv0_seq 和 drv1_seq 中使用这个全局变量,但是假如其他的某个 sequence 也不小心使用了这个全局变量,在 drv0_seq 触发 send_over 事件之前,这个 sequence 已经触发了此事件,这是不允许的。所以应该尽量避免全局变量的使用。
第二个问题是上面只是实现了一次同步,如果是有多次同步怎么办?如 sequence A 要先执行,之后是 B,B 执行后才能是 C,C 执行后才能是 D,D 执行后才能是 E。这依然可以使用上面的全局方法解决,只是这会显得相当笨拙。
实现 sequence 之间同步的最好的方式就是使用 virtual sequence。从字面上理解,即虚拟的 sequence。虚拟的意思就是它根本就不发送 transaction,它只是控制其他的 sequence,起统一调度的作用。
如图 6-1 所示,为了使用 virtual sequence,一般需要有一个 virtual sequencer。virtual sequencer 里面包含指向其他真实 sequencer 的指针:
清单 6-70
systemverilog
文件:src/ch6/section6.5/6.5.3/uvm_do_on/my_vsqr.sv
4 class my_vsqr extends uvm_sequencer;
5
6 my_sequencer p_sqr0;
7 my_sequencer p_sqr1;
...
14 endclass
图6-1(图例):virtual sequence 示意图------展示 virtual sequencer 中保存指向各真实 sequencer(p_sqr0、p_sqr1)的指针,并由 virtual sequence 统一调度、控制各真实 sequence 向对应 sequencer 发送 transaction 的结构关系。
在 base_test 中,实例化 vsqr,并将相应的 sequencer 赋值给 vsqr 中的 sequencer 的指针:
清单 6-71
systemverilog
文件:src/ch6/section6.5/6.5.3/uvm_do_on/base_test.sv
4 class base_test extends uvm_test;
5
6 my_env env0;
7 my_env env1;
8 my_vsqr v_sqr;
...
18 endclass
19
20
21 function void base_test::build_phase(uvm_phase phase);
22 super.build_phase(phase);
23 env0 = my_env::type_id::create("env0", this);
24 env1 = my_env::type_id::create("env1", this);
25 v_sqr = my_vsqr::type_id::create("v_sqr", this);
26 endfunction
27
28 function void base_test::connect_phase(uvm_phase phase);
29 v_sqr.p_sqr0 = env0.i_agt.sqr;
30 v_sqr.p_sqr1 = env1.i_agt.sqr;
31 endfunction
在 virtual sequence 中则可以使用 uvm_do_on 系列宏来发送 transaction:
清单 6-72
systemverilog
文件:src/ch6/section6.5/6.5.3/uvm_do_on/my_case0.sv
35 class case0_vseq extends uvm_sequence;
36 `uvm_object_utils(case0_vseq)
37 `uvm_declare_p_sequencer(my_vsqr)
...
42 virtual task body();
43 my_transaction tr;
44 drv0_seq seq0;
45 drv1_seq seq1;
...
48 `uvm_do_on_with(tr, p_sequencer.p_sqr0, {tr.pload.size == 1500;})
49 `uvm_info("vseq", "send one longest packet on p_sequencer.p_sqr0", UVM_MEDIUM)
50 fork
51 `uvm_do_on(seq0, p_sequencer.p_sqr0);
52 `uvm_do_on(seq1, p_sequencer.p_sqr1);
53 join
...
57 endtask
58 endclass
在 6.3.1 节介绍 uvm_do_on 宏时,读者对其用处感到非常迷茫,现在终于找到答案了。virtual sequence 是 uvm_do_on 宏用得最多的地方。
在 case0_vseq 中,先使用 uvm_do_on_with 在 p_sequencer.sqr0 上发送一个最长包,当其发送完毕后,再启动 drv0_seq 和 drv1_seq。这里的 drv0_seq 和 drv1_seq 非常简单,两者之间不需要为同步做任何事情:
清单 6-73
systemverilog
文件:src/ch6/section6.5/6.5.3/uvm_do_on/my_case0.sv
3 class drv0_seq extends uvm_sequence #(my_transaction);
...
11 virtual task body();
12 repeat (10) begin
13 `uvm_do(m_trans)
14 `uvm_info("drv0_seq", "send one transaction", UVM_MEDIUM)
15 end
16 endtask
17 endclass
18
19 class drv1_seq extends uvm_sequence #(my_transaction);
...
27 virtual task body();
28 repeat (10) begin
29 `uvm_do(m_trans)
30 `uvm_info("drv1_seq", "send one transaction", UVM_MEDIUM)
31 end
32 endtask
33 endclass
在使用 uvm_do_on 宏的情况下,虽然 seq0 是在 case0_vseq 中启动,但是它最终会被交给 p_sequencer.p_sqr0,也即 env0.i_agt.sqr 而不是 v_sqr。这个就是 virtual sequence 和 virtual sequencer 中 virtual 的来源。它们各自并不产生 transaction,而只是控制其他的 sequence 为相应的 sequencer 产生 transaction。virtual sequence 和 virtual sequencer 只是起一个调度的作用。由于根本不直接产生 transaction,所以 virtual sequence 和 virtual sequencer 在定义时根本无需指明要发送的 transaction 数据类型。
如果不使用 uvm_do_on 宏,那么也可以手工启动 sequence,其效果完全一样。手工启动 sequence 的一个优势是可以向其中传递一些值:
清单 6-74
systemverilog
文件:src/ch6/section6.5/6.5.3/start/my_case0.sv
3 class read_file_seq extends uvm_sequence #(my_transaction);
4 my_transaction m_trans;
5 string file_name;
...
19 endclass
...
37 class case0_vseq extends uvm_sequence;
...
44 virtual task body();
45 my_transaction tr;
46 read_file_seq seq0;
47 drv1_seq seq1;
...
50 `uvm_do_on_with(tr, p_sequencer.p_sqr0, {tr.pload.size == 1500;})
51 `uvm_info("vseq", "send one longest packet on p_sequencer.p_sqr0", UVM_MEDIUM)
52 seq0 = new("seq0");
53 seq0.file_name = "data.txt";
54 seq1 = new("seq1");
55 fork
56 seq0.start(p_sequencer.p_sqr0);
57 seq1.start(p_sequencer.p_sqr1);
58 join
...
62 endtask
63 endclass
在 read_file_seq 中,需要一个字符串的文件名字,在手工启动时可以指定文件名字,但是 uvm_do 系列宏无法实现这个功能,因为 string 类型变量前不能使用 rand 修饰符。这就是手工启动 sequence 的优势。
在 case0_vseq 的定义中,一般都要使用 uvm_declare_p_sequencer 宏。这个在前文已经讲述过了,通过它可以引用 sequencer 的成员变量。
回顾一下,为了解决 sequence 的同步,之前使用 send_over 这个全局变量的方式来解决。那么在 virtual sequence 中是如何解决的呢?事实上这个问题在 virtual sequence 中根本就不是个问题。由于 virtual sequence 的 body 是顺序执行,所以只需要先产生一个最长的包,产生完毕后再将其他的 sequence 启动起来,没有必要去刻意地同步。这只是 virtual sequence 强大的调度功能的一个小小的体现。
virtual sequence 的使用可以减少 config_db 语句的使用。由于 config_db::set 函数的第二个路径参数是字符串,非常容易出错,所以减少 config_db 语句的使用可以降低出错的概率。在上节中,使用了两个 uvm_config_db 语句将两个 sequence 送给了相应的 sequencer 作为 default_sequence。假如验证平台中的 sequencer 有多个,如 10 个,那么就需要写 10 个 uvm_config_db 语句,这是一件很令人厌烦的事情。使用 virtual sequence 后可以将这 10 句只压缩成一句:
清单 6-75
systemverilog
文件:src/ch6/section6.5/6.5.3/uvm_do_on/my_case0.sv
70 function void my_case0::build_phase(uvm_phase phase);
...
73 uvm_config_db#(uvm_object_wrapper)::set(this,
74 "v_sqr.main_phase",
75 "default_sequence",
76 case0_vseq::type_id::get());
77 endfunction
virtual sequence 作为一种特殊的 sequence,也可以在其中启动其他的 virtual sequence:
清单 6-76
systemverilog
文件:src/ch6/section6.5/6.5.3/multi_vseq/my_case0.sv
55 class case0_vseq extends uvm_sequence;
...
62 virtual task body();
63 cfg_vseq cvseq;
...
66 `uvm_do(cvseq)
...
70 endtask
71 endclass
其中 cfg_vseq 是另外一个已经定义好的 virtual sequence。
6.5.4 仅在virtual sequence中控制objection
在 sequence 中可以使用 starting_phase 来控制验证平台的关闭。除了手工启动 sequence 时为 starting_phase 赋值外,只有将此 sequence 作为 sequencer 的某动态运行 phase 的 default_sequence 时,其 starting_phase 才不为 null。如果将某 sequence 作为 uvm_do 宏的参数,那么此 sequence 中的 starting_phase 是为 null 的。在此 sequence 中使用 starting_phase.raise_objection 是没有任何用处的:
清单 6-77
systemverilog
文件:src/ch6/section6.5/6.5.3/my_case0.sv
3 class drv0_seq extends uvm_sequence #(my_transaction);
...
11 virtual task body();
12 if(starting_phase != null) begin
13 starting_phase.raise_objection(this);
14 `uvm_info("drv0_seq", "raise objection", UVM_MEDIUM)
15 end
16 else begin
17 `uvm_info("drv0_seq", "starting_phase is null, can't raise objection", UVM_MEDIUM)
18 end
...
29 endtask
30 endclass
31
32 class case0_vseq extends uvm_sequence;
...
39 virtual task body();
40 drv0_seq seq0;
41 if(starting_phase != null)
42 starting_phase.raise_objection(this);
43 `uvm_do_on(seq0, p_sequencer.p_sqr0);
44 #100;
45 if(starting_phase != null)
46 starting_phase.drop_objection(this);
47 endtask
48 endclass
运行上述代码,会发现 drv0_seq 中的 starting_phase 为 null,从而不会对 objection 进行操作。
若使 drv0_seq 中的 starting_phase 不为 null 其实比较容易解决,只要将父 sequence 的 starting_phase 赋值给子 sequence 的 starting_phase 即可。只是可惜 uvm_do 系列宏并不提供 starting_phase 的传递功能。5.2.3 节中提过要么在 scoreboard 中控制 objection,要么在 sequence 中控制。关于在 sequence 中控制 objection,在没有 virtual sequence 之前,这没有什么疑问。但是当 virtual sequence 存在时,尤其是 virtual sequence 中又可以启动其他的 virtual sequence 时,有三个地方可以控制 objection:一是普通的 sequence,二是中间层的 virtual sequence(如代码清单 6-76 中的 cfg_vseq),三是最顶层的 virtual sequence(代码清单 6-76 中的 case0_vseq)。那么应该在何处控制 objection 来最终控制验证平台的关闭呢?
一般来说,只在最顶层的 virtual sequence 中控制 objection。因为 virtual sequence 是起统一调度作用的,这种统一调度不只体现在 transaction 上,也应该体现在 objection 的控制上。在验证平台中使用 objection 时,经常会出现没有按照预期结束仿真的情况。这种情况下就需要层层地查找哪里有 objection 被提起了,哪里有 objection 被撤销了。虽然可以通过 5.2.5 节提及的 objection 调试手段来辅助进行,但它终归是一件比较麻烦的事情。如果大家约定俗成都只在最顶层的 virtual sequence 中控制 objection,那么在遇到这样的问题时,只查找最顶层的 virtual sequence 即可,从而大大提高效率。
6.5.5 在sequence中慎用fork join_none
将 6.5.1 节中的 DUT 的数据口扩展为 4 路,那么相应的验证平台中也要有 4 个完全相同的 driver、sequencer。那么 my_vsqr 可以这样定义:
清单 6-78
systemverilog
文件:src/ch6/section6.5/6.5.5/my_vsqr.sv
4 class my_vsqr extends uvm_sequencer;
5
6 my_sequencer p_sqr[4];
...
12 `uvm_component_utils(my_vsqr)
13 endclass
当 DUT 上电复位后,需要 4 个 my_driver 同时发送数据。在 virtual sequence 中可以使用 fork 来启动 4 个 sequence:
清单 6-79
systemverilog
class case0_vseq extends uvm_sequence;
virtual task body();
drv_seq dseq[4];
for(int i = 0; i < 4; i++)
fork
automatic int j = i;
uvm_do_on(dseq[j], p_sequencer.p_sqr[j]);
join_none
endtask
endclass
这里使用了 join_none,由于 join_none 的特性,系统并不等 fork 起来的进程结束就进入下一次的 for 循环,因此上面的 for 循环的展开后如下:
清单 6-80
systemverilog
class case0_vseq extends uvm_sequence;
virtual task body();
drv_seq dseq[4];
fork
uvm_do_on(dseq[0], p_sequencer.p_sqr[0]);
join_none
fork
uvm_do_on(dseq[1], p_sequencer.p_sqr[1]);
join_none
fork
uvm_do_on(dseq[2], p_sequencer.p_sqr[2]);
join_none
fork
uvm_do_on(dseq[3], p_sequencer.p_sqr[3]);
join_none
endtask
endclass
这样会有什么问题?
当 sequence 启动后会自动执行它的 body 任务。当 body 执行完成时,那么这个 sequence 就相当于已经完成了其使命,已经结束了。如果使用 fork join_none,由于 join_none 的特性,当使用 uvm_do_on 宏将四个 dseq 分别放在四个 p_sqr 上执行时,系统会新启动 4 个进程,但是并不等待这 4 个 mseq 执行完毕就直接返回了。返回之后就到了 endtask,此时系统认为这个 sequence 已经执行完成了。执行完成之后,系统将会清理这个 sequence 之前占据的内存空间,"杀死"掉由其启动的进程,于是这 4 个启动的 dseq 还没有完成就直接被"杀死"掉了。也就是说,看似分别往 4 个 p_sqr 分别丢了一个 sequence,但是事实上这个 sequence 根本没有执行。这是关键所在!
要避免这个问题有多种方法,一是使用 wait fork 语句:
清单 6-81
systemverilog
文件:src/ch6/section6.5/6.5.5/my_case0.sv
19 class case0_vseq extends uvm_sequence;
...
26 virtual task body();
27 my_transaction tr;
28 drv_seq dseq[4];
...
31 for(int i = 0; i < 4; i++)
32 fork
33 automatic int j = i;
34 `uvm_do_on(dseq[j], p_sequencer.p_sqr[j]);
35 join_none
36 wait fork;
...
40 endtask
41 endclass
wait fork 语句将会等待前面被 fork 起来的进程执行完毕。
另外一种方法是使用 fork join:
清单 6-82
systemverilog
class case0_vseq extends uvm_sequence;
virtual task body();
drv_seq dseq[4];
fork
uvm_do_on(dseq[0], p_sequencer.p_sqr[0]);
uvm_do_on(dseq[1], p_sequencer.p_sqr[1]);
uvm_do_on(dseq[2], p_sequencer.p_sqr[2]);
uvm_do_on(dseq[3], p_sequencer.p_sqr[3]);
join
endtask
endclass
只是这样就无法使用 for 循环了。
6.6 在sequence中使用config_db
6.6.1 在sequence中获取参数
在 3.5 节中介绍 config_db 机制时,set 函数的目标都是一个 component,或者说,之前所有获取参数的操作都是在一个 component 中进行的。sequence 机制是 UVM 中最强大的机制之一,config_db 机制也对 sequence 机制提供了支持,可以在 sequence 中获取参数。
能够调用 config_db::get 的前提是已经进行了设置。sequence 本身是一个 uvm_object,它无法像 uvm_component 那样出现在 UVM 树中,从而很难确定在对其进行设置时的第二个路径参数。所以在 sequence 中使用 config_db::get 函数得到参数的最大障碍是路径问题。
在 UVM 中使用 get_full_name() 可以得到一个 component 的完整路径,同样的,此函数也可以在一个 sequence 中被调用,尝试着在一个 sequence 的 body 中调用此函数,并打印出返回值,其结果大体如下:
text
uvm_test_top.env.i_agt.sqr.case0_sequence
这个路径是由两个部分组成:此 sequence 的 sequencer 的路径,及实例化此 sequence 时传递的名字。因此,可以使用如下的方式为一个 sequence 传递参数:
清单 6-83
systemverilog
文件:src/ch6/section6.6/6.6.1/my_case0.sv
43 function void my_case0::build_phase(uvm_phase phase);
...
46 uvm_config_db#(int)::set(this, "env.i_agt.sqr.*", "count", 9);
...
52 endfunction
set 函数的第二个路径参数里面出现了通配符,这是因为 sequence 在实例化时名字一般是不固定的,而且有时是未知的(比如使用 default_sequence 启动的 sequence 的名字就是未知的),所以使用通配符。
在 sequence 中以如下的方式调用 config_db::get 函数:
清单 6-84
systemverilog
文件:src/ch6/section6.6/6.6.1/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
11 virtual task pre_body();
12 if(uvm_config_db#(int)::get(null, get_full_name(), "count", count))
13 `uvm_info("seq0", $sformatf("get count value %0d via config_db", count), UVM_MEDIUM)
14 else
15 `uvm_error("seq0", "can't get count value!")
16 endtask
...
30 endclass
这里需要引起关注的是第一个参数。在 get 函数原型中,第一个参数必须是一个 component,而 sequence 不是一个 component,所以这里不能使用 this 指针,只能使用 null 或者 uvm_root::get()。前文已经提过,当使用 null 时,UVM 会自动将其替换为 uvm_root::get(),再加上第二个参数 get_full_name(),就可以完整地得到此 sequence 的路径,从而得到参数。
6.6.2 在sequence中设置参数
与获取参数相比,在 sequence 中使用 config_db::set 设置参数就比较简单。有了在 top_tb 中设置 virtual interface 的经验,读者在这里可以使用类似的方式为 UVM 树中的任意结点传递参数:
清单 6-85
systemverilog
文件:src/ch6/section6.6/6.6.2/component/my_case0.sv
33 class case0_vseq extends uvm_sequence;
...
40 virtual task body();
...
46 fork
47 `uvm_do_on(seq0, p_sequencer.p_sqr0);
48 `uvm_do_on(seq1, p_sequencer.p_sqr1);
49 begin
50 #10000;
51 uvm_config_db#(bit)::set(uvm_root::get(), "uvm_test_top.env0.scb", "cmp_en", 0);
52 #10000;
53 uvm_config_db#(bit)::set(uvm_root::get(), "uvm_test_top.env0.scb", "cmp_en", 1);
54 end
55 join
...
59 endtask
60 endclass
上例中是向 scoreboard 中传递了一个 cmp_en 的参数。除了向 component 中传递参数外,也可以向 sequence 中传递参数:
清单 6-86
systemverilog
文件:src/ch6/section6.6/6.6.2/sequence/my_case0.sv
3 class drv0_seq extends uvm_sequence #(my_transaction);
4 my_transaction m_trans;
5 bit first_start;
6 `uvm_object_utils(drv0_seq)
7
8 function new(string name= "drv0_seq");
9 super.new(name);
10 first_start = 1;
11 endfunction
12
13 virtual task body();
14 void'(uvm_config_db#(bit)::get(uvm_root::get(), get_full_name(), "first_start", first_start));
15 if(first_start)
16 `uvm_info("drv0_seq", "this is the first start of the sequence", UVM_MEDIUM)
17 else
18 `uvm_info("drv0_seq", "this is not the first start of the sequence",UVM_MEDIUM)
19 uvm_config_db#(bit)::set(uvm_root::get(), "uvm_test_top.v_sqr.*", "first_start", 0);
...
23 endtask
24 endclass
这个 sequence 向自己传递了一个参数:first_start。在一次仿真中,当此 sequence 第一次启动时,其 first_start 值为 1;当后面再次启动时,其 first_start 为 0。根据 first_start 值的不同,可以在 body 中有不同的行为。
这里需要注意的是,由于此 sequence 在 virtual sequence 中被启动,所以其 get_full_name 的结果应该是 uvm_test_top.v_sqr.*,而不是 uvm_test_top.env0.i_agt.sqr.*,所以在设置时,第二个参数应该是前者。
6.6.3 wait_modified的使用
在上一节的例子中,向 scoreboard 传递了一个 cmp_en 的参数,scoreboard 可以根据此参数决定是否对收到的 transaction 进行检查。在做一些异常用例测试的时候,经常用到这种方式。但是关键是如何在 scoreboard 中获取这个参数。
在前面的章节中,scoreboard 都是在 build_phase 中调用 get 函数,并且调用的前提是参数已经被设置过。一个 sequence 是在 task phase 中运行的,当其设置一个参数的时候,其时间往往是不固定的。
针对这种不固定的设置参数的方式,UVM 中提供了 wait_modified 任务,它的参数有三个,与 config_db::get 的前三个参数完全一样。当它检测到第三个参数的值被更新过后,它就返回,否则一直等待在那里。其调用方式如下:
清单 6-87
systemverilog
文件:src/ch6/section6.6/6.6.3/component/my_scoreboard.sv
24 task my_scoreboard::main_phase(uvm_phase phase);
...
30 fork
31 while(1) begin
32 uvm_config_db#(bit)::wait_modified(this, "", "cmp_en");
33 void'(uvm_config_db#(bit)::get(this, "", "cmp_en", cmp_en));
34 `uvm_info("my_scoreboard", $sformatf("cmp_en value modified, the new value is %0d", cmp_en), UVM_LOW)
35 end
...
62 join
63 endtask
在上述代码中,wait_modified 与 main_phase 中的其他进程在同一时刻被 fork 起来,当检测到参数值被设置后,立刻调用 config_db::get 得到新的参数。其他进程可以根据新的参数值决定后续的比对策略。
与 get 函数一样,除了可以在一个 component 中使用外,还可以在一个 sequence 中调用 wait_modified 任务:
清单 6-88
systemverilog
文件:src/ch6/section6.6/6.6.3/sequence/my_case0.sv
3 class drv0_seq extends uvm_sequence #(my_transaction);
...
11 virtual task body();
12 bit send_en = 1;
13 fork
14 while(1) begin
15 uvm_config_db#(bit)::wait_modified(null, get_full_name(), "send_en");
16 void'(uvm_config_db#(bit)::get(null, get_full_name, "send_en", send_en));
17 `uvm_info("drv0_seq", $sformatf("send_en value modified, the new value is %0d", send_en), UVM_LOW)
18 end
19 join_none
...
23 endtask
24 endclass
6.7 response的使用
6.7.1 put_response与get_response
sequence 机制提供了一种 sequence → sequencer → driver 的单向数据传输机制。但是在复杂的验证平台中,sequence 需要根据 driver 对 transaction 的反应来决定接下来要发送的 transaction,换言之,sequence 需要得到 driver 的一个反馈。sequence 机制提供对这种反馈的支持,它允许 driver 将一个 response 返回给 sequence。
如果需要使用 response,那么在 sequence 中需要使用 get_response 任务:
清单 6-89
systemverilog
文件:src/ch6/section6.7/6.7.1/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
10 virtual task body();
...
13 repeat (10) begin
14 `uvm_do(m_trans)
15 get_response(rsp);
16 `uvm_info("seq", "get one response", UVM_MEDIUM)
17 rsp.print();
18 end
...
22 endtask
23
24 `uvm_object_utils(case0_sequence)
25 endclass
在 driver 中,则需要使用 put_response 任务:
清单 6-90
systemverilog
文件:src/ch6/section6.7/6.7.1/my_driver.sv
22 task my_driver::main_phase(uvm_phase phase);
...
27 while(1) begin
28 seq_item_port.get_next_item(req);
29 drive_one_pkt(req);
30 rsp = new("rsp");
31 rsp.set_id_info(req);
32 seq_item_port.put_response(rsp);
33 seq_item_port.item_done();
34 end
35 endtask
这里的关键是设置 set_id_info 函数,它将 req 的 id 等信息复制到 rsp 中。由于可能存在多个 sequence 在同一个 sequencer 上启动的情况,只有设置了 rsp 的 id 等信息,sequencer 才知道将 response 返回给哪个 sequence。
除了使用 put_response 外,UVM 还支持直接将 response 作为 item_done 的参数:
清单 6-91
systemverilog
while(1) begin
seq_item_port.get_next_item(req);
drive_one_pkt(req);
rsp = new("rsp");
rsp.set_id_info(req);
seq_item_port.item_done(rsp);
end
6.7.2 response的数量问题
通常来说,一个 transaction 对应一个 response,但是事实上,UVM 也支持一个 transaction 对应多个 response 的情况,在这种情况下,在 sequence 中需要多次调用 get_response,而在 driver 中,需要多次调用 put_response:
清单 6-92
systemverilog
task my_driver::main_phase(uvm_phase phase);
while(1) begin
seq_item_port.get_next_item(req);
drive_one_pkt(req);
rsp = new("rsp");
rsp.set_id_info(req);
seq_item_port.put_response(rsp);
seq_item_port.put_response(rsp);
seq_item_port.item_done();
end
endtask
class case0_sequence extends uvm_sequence #(my_transaction);
virtual task body();
repeat (10) begin
`uvm_do(m_trans)
get_response(rsp);
rsp.print();
get_response(rsp);
rsp.print();
end
endtask
endclass
当存在多个 response 时,将 response 作为 item_done 参数的方式就不适用了。由于一个 transaction 只能对应一个 item_done,所以使用多次 item_done(rsp) 是会出错的。
response 机制的原理是 driver 将 rsp 推送给 sequencer,而 sequencer 内部维持一个队列,当有新的 response 进入时,就推入此队列。但是此队列的大小并不是无限制的,在默认情况下,其大小为 8。当队列中有 8 个 response 时,如果 driver 再次向此队列推送新的 response,UVM 就会给出如下错误提示:
text
UVM_ERROR @ 1753500000: uvm_test_top.env.i_agt.sqr@@case0_sequence [uvm_test_top.env.i_agt.sqr.case0_sequence]
Response queue overflow, response was dropped
因此,如果在 driver 中每个 transaction 后都发送一个 response,而 sequence 又没能及时 get_response,sequencer 中的 response 队列就存在溢出的风险。
6.7.3 response handler与另类的response
前面讲述的 get_response 和 put_response 是一一对应的。当在 sequence 中启动 get_response 时,进程就会阻塞在那里,一直到 response_queue 中被放入新的记录。如果 driver 能够马上将 response 通过 put_response 的方式传回 sequence,那么 sequence 被阻塞的进程就会得到释放,可以接着发送下一个 transaction 给 driver。但是假如 driver 需要延时较长的一段时间才能将 transaction 传回,在此期间,driver 希望能够继续从 sequence 得到新的 transaction 并驱动它,但是由于 sequence 被阻塞在了那里,根本不可能发出新的 transaction。
发生上述情况的主要原因为 sequence 中发送 transaction 与 get_response 是在同一个进程中执行的,假如将二者分离开来,在不同的进程中运行将会得到不同的结果。在这种情况下需要使用 response_handler:
清单 6-93
systemverilog
文件:src/ch6/section6.7/6.7.3/rsp_handler/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
10 virtual task pre_body();
11 use_response_handler(1);
12 endtask
13
14 virtual function void response_handler(uvm_sequence_item response);
15 if(!$cast(rsp, response))
16 `uvm_error("seq", "can't cast")
17 else begin
18 `uvm_info("seq", "get one response", UVM_MEDIUM)
19 rsp.print();
20 end
21 endfunction
22
23 virtual task body();
24 if(starting_phase != null)
25 starting_phase.raise_objection(this);
26 repeat (10) begin
27 `uvm_do(m_trans)
28 end
29 #100;
30 if(starting_phase != null)
31 starting_phase.drop_objection(this);
32 endtask
33
34 `uvm_object_utils(case0_sequence)
35 endclass
由于 response handler 功能默认是关闭的,所以要使用 response_handler,首先需要调用 use_response_handler 函数,打开 sequence 的 response handler 功能。
当打开 response handler 功能后,用户需要重载虚函数 response_handler。此函数的参数是一个 uvm_sequence_item 类型的指针,需要首先将其通过 cast 转换变成 my_transaction 类型,之后就可以根据 rsp 的值来决定后续 sequence 的行为。
无论是 put/get_response 或者 response_handler,都是新建了一个 transaction,并将其返回给 sequence。事实上,当一个 uvm_do 语句执行完毕后,其第一个参数并不是一个空指针,而是指向刚刚被送给 driver 的 transaction。利用这一点,可以实现一种另类的 response:
清单 6-94
systemverilog
文件:src/ch6/section6.7/6.7.3/smart/my_driver.sv
22 task my_driver::main_phase(uvm_phase phase);
...
27 while(1) begin
28 seq_item_port.get_next_item(req);
29 drive_one_pkt(req);
30 req.frm_drv = "this is information from driver";
31 seq_item_port.item_done();
32 end
33 endtask
driver 中向 req 中的成员变量赋值,而 sequence 则检测这个值:
清单 6-95
systemverilog
文件:src/ch6/section6.7/6.7.3/smart/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction);
...
10 virtual task body();
...
13 repeat (10) begin
14 `uvm_do(m_trans)
15 `uvm_info("seq", $sformatf("get information from driver: %0s", m_trans.frm_drv), UVM_MEDIUM)
16 end
...
20 endtask
21
22 `uvm_object_utils(case0_sequence)
23 endclass
这种另类的 response 在很多总线的 driver 中用到。读者可以参考 7.1.1 节的内容。
6.7.4 rsp与req类型不同
前面所有的例子中,response 的类型都与 req 的类型完全相同。UVM 也支持 response 与 req 类型不同的情况。
uvm_driver、uvm_sequencer 与 uvm_sequence 的原型分别是:
清单 6-96
systemverilog
来源:UVM 源代码
class uvm_driver #(type REQ=uvm_sequence_item,
type RSP=REQ) extends uvm_component;
class uvm_sequencer #(type REQ=uvm_sequence_item,
RSP=REQ)
extends uvm_sequencer_param_base #(REQ, RSP);
virtual class uvm_sequence #(type REQ = uvm_sequence_item,
type RSP = REQ)
extends uvm_sequence_base;
在前面章节的例子中只向它们传递了一个参数,因此 response 与 req 的类型是一样的。如果要使用不同类型的 rsp 与 req,那么 driver、sequencer 与 sequence 在定义时都要传入两个参数:
清单 6-97
systemverilog
class my_driver extends uvm_driver#(my_transaction, your_transaction);
class my_sequencer extends uvm_sequencer #(my_transaction, your_transaction);
class case0_sequence extends uvm_sequence #(my_transaction, your_transaction);
之后,可以使用 put_response 来发送 response:
清单 6-98
systemverilog
文件:src/ch6/section6.7/6.7.4/my_driver.sv
22 task my_driver::main_phase(uvm_phase phase);
...
27 while(1) begin
28 seq_item_port.get_next_item(req);
29 drive_one_pkt(req);
30 rsp = new("rsp");
31 rsp.set_id_info(req);
32 rsp.information = "driver information";
33 seq_item_port.put_response(rsp);
34 seq_item_port.item_done();
35 end
36 endtask
使用 get_response 来接收 response:
清单 6-99
systemverilog
文件:src/ch6/section6.7/6.7.4/my_case0.sv
3 class case0_sequence extends uvm_sequence #(my_transaction, your_transaction);
...
10 virtual task body();
...
13 repeat (10) begin
14 `uvm_do(m_trans)
15 get_response(rsp);
16 `uvm_info("seq", $sformatf("response information is: %0s", rsp.information), UVM_MEDIUM)
17 end
...
21 endtask
22
23 `uvm_object_utils(case0_sequence)
24 endclass
除了 put/get_response 外,也可以使用 response handler,这与 req 及 rsp 类型相同时完全一样。
6.8 sequence library
6.8.1 随机选择sequence
所谓 sequence library,就是一系列 sequence 的集合。sequence_library 类的原型为:
清单 6-100
systemverilog
来源:UVM 源代码
class uvm_sequence_library #(type REQ=uvm_sequence_item,RSP=REQ) extends uvm_sequence #(REQ,RSP);
由上述代码可以看出 sequence library 派生自 uvm_sequence,从本质上说它是一个 sequence。它根据特定的算法随机选择注册在其中的一些 sequence,并在 body 中执行这些 sequence。
一个 sequence library 的定义如下:
清单 6-101
systemverilog
文件:src/ch6/section6.8/6.8.1/my_case0.sv
4 class simple_seq_library extends uvm_sequence_library#(my_transaction);
5 function new(string name= "simple_seq_library");
6 super.new(name);
7 init_sequence_library();
8 endfunction
9
10 `uvm_object_utils(simple_seq_library)
11 `uvm_sequence_library_utils(simple_seq_library)
12
13 endclass
在定义 sequence library 时有三点要特别注意:一是从 uvm_sequence 派生时要指明此 sequence library 所产生的 transaction 类型,这点与普通的 sequence 相同;二是在其 new 函数中要调用 init_sequence_library,否则其内部的候选 sequence 队列就是空的;三是要调用 uvm_sequence_library_utils 注册。
一个 sequence library 在定义之后,如果没有其他的 sequence 注册到其中,是没有任何意义的。一个 sequence 在定义时使用宏 uvm_add_to_seq_lib 来将其加入某个 sequence library 中:
清单 6-102
systemverilog
文件:src/ch6/section6.8/6.8.1/my_case0.sv
15 class seq0 extends uvm_sequence#(my_transaction);
...
20 `uvm_object_utils(seq0)
21 `uvm_add_to_seq_lib(seq0, simple_seq_library)
22 virtual task body();
23 repeat(10) begin
24 `uvm_do(req)
25 `uvm_info("seq0", "this is seq0", UVM_MEDIUM)
26 end
27 endtask
28 endclass
uvm_add_to_seq_lib 有两个参数,第一个是此 sequence 的名字,第二个是要加入的 sequence library 的名字。一个 sequence 可以加入多个不同的 sequence library 中:
清单 6-103
systemverilog
class seq0 extends uvm_sequence#(my_transaction);
`uvm_object_utils(seq0)
`uvm_add_to_seq_lib(seq0, simple_seq_library)
`uvm_add_to_seq_lib(seq0, hard_seq_library)
virtual task body();
repeat(10) begin
`uvm_do(req)
`uvm_info("seq0", "this is seq0", UVM_MEDIUM)
end
endtask
endclass
同样的,可以有多个 sequence 加入同一 sequence library 中:
清单 6-104
systemverilog
文件:src/ch6/section6.8/6.8.1/my_case0.sv
30 class seq1 extends uvm_sequence#(my_transaction);
...
35 `uvm_object_utils(seq1)
36 `uvm_add_to_seq_lib(seq1, simple_seq_library)
37 virtual task body();
38 repeat(10) begin
39 `uvm_do(req)
40 `uvm_info("seq1", "this is seq1", UVM_MEDIUM)
41 end
42 endtask
43 endclass
当 sequence 与 sequence library 定义好后,可以将 sequence library 作为 sequencer 的 default_sequence:
清单 6-105
systemverilog
文件:src/ch6/section6.8/6.8.1/my_case0.sv
85 function void my_case0::build_phase(uvm_phase phase);
86 super.build_phase(phase);
87
88 uvm_config_db#(uvm_object_wrapper)::set(this,
89 "env.i_agt.sqr.main_phase",
90 "default_sequence",
91 simple_seq_library::type_id::get());
92 endfunction
执行上述代码,将发现 UVM 会随机从加入 simple_seq_library 的 sequence 中选择几个,并顺序启动它们。
6.8.2 控制选择算法
在上节中,sequence library 随机从其 sequence 队列中选择几个执行。这是由其变量 selection_mode 决定的,这个变量的定义为:
清单 6-106
systemverilog
来源:UVM 源代码
uvm_sequence_lib_mode selection_mode;
uvm_sequence_lib_mode 是一个枚举类型,共有四个值:
清单 6-107
systemverilog
来源:UVM 源代码
typedef enum
{
UVM_SEQ_LIB_RAND,
UVM_SEQ_LIB_RANDC,
UVM_SEQ_LIB_ITEM,
UVM_SEQ_LIB_USER
} uvm_sequence_lib_mode;
UVM_SEQ_LIB_RAND 就是完全的随机,上节中的例子使用的就是这种算法。
UVM_SEQ_LIB_RANDC 就是将加入其中的 sequence 随机排一个顺序,然后按照此顺序执行。这可以保证每个 sequence 执行一遍,在所有的 sequence 被执行完一遍之前,不会有 sequence 被执行第二次,其配置方式如下:
清单 6-108
systemverilog
文件:src/ch6/section6.8/6.8.2/randc/my_case0.sv
85 function void my_case0::build_phase(uvm_phase phase);
...
92 uvm_config_db#(uvm_sequence_lib_mode)::set(this,
93 "env.i_agt.sqr.main_phase",
94 "default_sequence.selection_mode",
95 UVM_SEQ_LIB_RANDC);
96 endfunction
UVM_SEQ_LIB_ITEM 的意思是 sequence library 并不执行其 sequence 队列中的 sequence,而是自己产生 transaction。换言之,sequence library 在此种情况下就是一个普通的 sequence,只是其产生的 transaction 除了定义时施加的约束外,没有任何额外的约束。
UVM_SEQ_LIB_USER 是用户自定义选择的算法。此时需要用户重载 select_sequence 参数:
清单 6-109
systemverilog
文件:src/ch6/section6.8/6.8.2/user/my_case0.sv
4 class simple_seq_library extends uvm_sequence_library#(my_transaction);
5 function new(string name= "simple_seq_library");
6 super.new(name);
7 init_sequence_library();
8 endfunction
9
10 `uvm_object_utils(simple_seq_library)
11 `uvm_sequence_library_utils(simple_seq_library)
12
13 virtual function int unsigned select_sequence(int unsigned max);
14 static int unsigned index[$];
15 static bit inited;
16 int value;
17 if(!inited) begin
18 for(int i = 0; i <= max; i++) begin
19 if((sequences[i].get_type_name() == "seq0") ||
20 (sequences[i].get_type_name() == "seq1") ||
21 (sequences[i].get_type_name() == "seq3"))
22 index.push_back(i);
23 end
24 inited = 1;
25 end
26 value = $urandom_range(0, index.size() - 1);
27 return index[value];
28 endfunction
29 endclass
假设有 4 个 sequence 加入了 sequence library 中:seq0、seq1、seq2 和 seq3。现在由于各种原因,不想使用 seq2 了。上述代码的 select_sequence 第一次被调用时初始化 index 队列,把 seq0、seq1 和 seq3 在 sequences 中的索引号存入其中。之后,从 index 中随机选择一个值返回,相当于是从 seq0、seq1 和 seq3 随机选一个执行。sequences 是 sequence library 中存放候选 sequence 的队列。select_sequence 会传入一个参数 max,select_sequence 函数必须返回一个介于 0 到 max 之间的数值。如果 sequences 队列的大小为 4,那么传入的 max 的数值是 3,而不是 4。
6.8.3 控制执行次数
在 6.8.1 节及 6.8.2 节中,执行的次数都是 10 次,这是由 sequence library 内部的两个变量控制的:
清单 6-110
systemverilog
来源:UVM 源代码
int unsigned min_random_count=10;
int unsigned max_random_count=10;
sequence library 会在 min_random_count 和 max_random_count 之间随意选择一个数来作为执行次数。这里只能选择 10。当 selection_mode 为 UVM_SEQ_LIB_ITEM 时,将会产生 10 个 item;为其他模式时,将会顺序启动 10 个 sequence。可以设置这两个值为其他值来改变迭代次数:
清单 6-111
systemverilog
文件:src/ch6/section6.8/6.8.3/my_case0.sv
85 function void my_case0::build_phase(uvm_phase phase);
...
88 uvm_config_db#(uvm_object_wrapper)::set(this,
89 "env.i_agt.sqr.main_phase",
90 "default_sequence",
91 simple_seq_library::type_id::get());
92 uvm_config_db#(uvm_sequence_lib_mode)::set(this,
93 "env.i_agt.sqr.main_phase",
94 "default_sequence.selection_mode",
95 UVM_SEQ_LIB_ITEM);
96 uvm_config_db#(int unsigned)::set(this,
97 "env.i_agt.sqr.main_phase",
98 "default_sequence.min_random_count",
99 5);
100 uvm_config_db#(int unsigned)::set(this,
101 "env.i_agt.sqr.main_phase",
102 "default_sequence.max_random_count",
103 20);
104 endfunction
上述设置将会产生最多 20 个,最少 5 个 transaction。
6.8.4 使用sequence_library_cfg
在代码清单 6-111 中使用 3 个 config_db 设置迭代次数和选择算法稍显麻烦。UVM 提供了一个类 uvm_sequence_library_cfg 来对 sequence library 进行配置。它一共有三个成员变量:
清单 6-112
systemverilog
来源:UVM 源代码
class uvm_sequence_library_cfg extends uvm_object;
`uvm_object_utils(uvm_sequence_library_cfg)
uvm_sequence_lib_mode selection_mode;
int unsigned min_random_count;
int unsigned max_random_count;
...
endclass
通过配置如上三个成员变量,并将其传递给 sequence library 就可对 sequence library 进行配置:
清单 6-113
systemverilog
文件:src/ch6/section6.8/6.8.4/cfg/my_case0.sv
85 function void my_case0::build_phase(uvm_phase phase);
86 uvm_sequence_library_cfg cfg;
87 super.build_phase(phase);
88
89 cfg = new("cfg", UVM_SEQ_LIB_RANDC, 5, 20);
90
91 uvm_config_db#(uvm_object_wrapper)::set(this,
92 "env.i_agt.sqr.main_phase",
93 "default_sequence",
94 simple_seq_library::type_id::get());
95 uvm_config_db#(uvm_sequence_library_cfg)::set(this,
96 "env.i_agt.sqr.main_phase",
97 "default_sequence.config",
98 cfg);
99 endfunction
除了使用专门的 cfg 外,还有一种简单的配置方法是使用代码清单 6-7 的方式启动 sequence,在对 sequence library 进行实例化后,对其中的变量进行赋值:
清单 6-114
systemverilog
文件:src/ch6/section6.8/6.8.4/start/my_case0.sv
85 function void my_case0::build_phase(uvm_phase phase);
86 simple_seq_library seq_lib;
87 super.build_phase(phase);
88
89 seq_lib = new("seq_lib");
90 seq_lib.selection_mode = UVM_SEQ_LIB_RANDC;
91 seq_lib.min_random_count = 10;
92 seq_lib.max_random_count = 15;
93 uvm_config_db#(uvm_sequence_base)::set(this,
94 "env.i_agt.sqr.main_phase",
95 "default_sequence",
96 seq_lib);
97 endfunction
sequence 机制:
- 激励产生与驱动分离 :从 driver 中剥离
gen_pkt的思想演进到 sequence 机制,是 sequence 存在的根本理由;sequencer 启动 sequence 并向 driver 交付 transaction,等价于在 driver 内调用gen_pkt。 - 仲裁与独占 :同一 sequencer 可并行启动多个 sequence,靠 transaction/sequence 优先级 + 仲裁算法(
SEQ_ARB_FIFO/WEIGHTED/RANDOM/STRICT_FIFO/STRICT_RANDOM/USER)裁决;lock/grab实现独占,is_relevant/wait_for_relevant实现主动失效。 - 宏的本质 :
uvm_do系列 8 个宏最终都归结为uvm_do_on_pri_with;uvm_create+uvm_send、uvm_rand_send、以及最底层的start_item/finish_item提供了从"最省事"到"最灵活"的不同层次;pre_do/mid_do/post_do在宏内部的关键节点插入钩子。 - 进阶 :嵌套 sequence 实现重用;sequence 内可用
rand变量(注意命名不要与 transaction 字段同名);sequencer 接受类型须匹配,uvm_sequence_item可混发多类型;p_sequencer/m_sequencer与uvm_declare_p_sequencer解决跨类访问 sequencer 成员;base sequence 派生时p_sequencer无需重复声明。 - virtual sequence :用 virtual sequencer 持有各真实 sequencer 指针,由 virtual sequence 统一调度、同步多个数据通道,并集中控制 objection、减少
config_db语句量;务必注意fork...join_none会令父 sequence 提前结束而"杀死"子进程,须用wait fork或fork...join等待。 - config_db 与 sequence :sequence 通过
get_full_name()配合uvm_config_db::get(null, ...)取参,用uvm_config_db::set(uvm_root::get(), ...)设参,wait_modified用于 task phase 中监听参数动态变化。 - response 反馈 :
put/get_response(含直接item_done(rsp))、多 response 与队列溢出风险、response_handler解耦收发进程、以及"借 req 回写字段"的另类 response;并支持 rsp 与 req 类型不同(需双参数化 driver/sequencer/sequence)。 - sequence library :将多个 sequence 注册为一个库,按
UVM_SEQ_LIB_RAND/RANDC/ITEM/USER算法随机选择执行,通过min/max_random_count、selection_mode或uvm_sequence_library_cfg配置执行次数与策略。