第 7 章 · 共 10 章
Sequence 与 Sequencer
uvm_sequence 的 body() task,以及和 sequencer 之间 start_item/finish_item 的握手协议;用 SV 第 12 章的方式随机化 transaction;把 driver 改造成第 1 章第一个例子早就用过的 uvm_driver#(REQ)/seq_item_port 形态。
到目前为止,每个 driver 都是自己发明激励——两组写死的 sel/a/b 组合,直接写在 run_phase 里。这正是第 1 章第三条差距:测试不同的场景就得改 driver 本身。这一章开始,激励变成一层独立、可替换的东西:一个 sequence,生成 transaction,交给一个 sequencer,再由它交给 driver。
uvm_sequence:比 uvm_sequence_item 再高一层
第 4 章讲过 uvm_sequence_item——每个 transaction 继承的基类。sequence 是另一个类,高一层,负责生产item:
class my_sequence extends uvm_sequence #(mux_transaction);
`uvm_object_utils(my_sequence)
function new(string name = "my_sequence");
super.new(name);
endfunction
task body();
req = mux_transaction::type_id::create("req");
start_item(req);
if (!req.randomize())
`uvm_error("SEQ", "randomize failed")
finish_item(req);
req = mux_transaction::type_id::create("req");
start_item(req);
if (!req.randomize() with { sel == 1; })
`uvm_error("SEQ", "randomize failed")
finish_item(req);
endtask
endclassuvm_sequence #(mux_transaction) 本身就是一个 uvm_object(第 4 章的材料直接适用——`uvm_object_utils、只需要 name 的构造函数、没有 parent)。它已经声明了一个类型参数化的 req 属性,这也是为什么 body() 可以直接给 req 赋值而不用自己声明它。两次 randomize() 调用正是 SV 第 12 章(randomization-and-constraints)的材料——包括 randomize() with { sel == 1; },和那一章一样的内联约束语法,现在约束的是接下来要驱动的 transaction,而不是一个普通的局部变量。
start_item()/finish_item():握手协议中 sequence 这一侧
start_item(req)/finish_item(req) 不只是簿记——它们是和对面(sequencer,再往后是 driver)对话的一半:
start_item(req)会阻塞,直到 sequencer 准备好接受这个 sequence 的新 item(仲裁——如果同时有不止一个 sequence 在跑,这个系列单 sequence 的例子不需要操心这个)。start_item()和finish_item()之间正是随机化该发生的地方——这时候 item 还没被送到任何地方。finish_item(req)把req送到连接在对面的东西那里,并且阻塞,直到对面发信号说处理完这个 item 了。
最后这一点很重要:body() 的第二次 start_item() 要等到 driver 处理完第一个 item 才会执行。sequence 的节奏完全取决于对面消费 item 的速度。
Driver:uvm_driver#(REQ),不再是纯 uvm_component
握手协议的另一半需要一个真正能"说"这套协议的 driver。改一下 my_driver 的基类:
class my_driver extends uvm_driver #(mux_transaction);
`uvm_component_utils(my_driver)
virtual mux2_if.tb_mp vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("DRV", "no virtual interface set for vif -- check the config_db::set() call in the top module")
endfunction
task run_phase(uvm_phase phase);
forever begin
mux_transaction tr;
seq_item_port.get_next_item(tr);
drive(tr);
seq_item_port.item_done();
end
endtask
task drive(mux_transaction tr);
vif.sel = tr.sel;
vif.a = tr.a;
vif.b = tr.b;
#1;
`uvm_info("DRV", $sformatf("%s -> y=%0b", tr.convert2string(), vif.y), UVM_MEDIUM)
endtask
endclass这正是第 1 章第一个代码例子用过的形态——simple_driver extends uvm_driver #(my_transaction)——现在你知道为什么了:uvm_driver #(REQ, RSP=REQ) 是一个 uvm_component 子类,自带一个已经声明好的 seq_item_port,随时可以连接到 sequencer。build_phase 和 drive() 和第 4、6 章相比一个字都没变——第 6 章的 factory 替换在这里依然完全照常生效,它跟 transaction 从哪来毫无关系。变的是 run_phase:不再驱动两个写死的 transaction,而是变成一个 forever 循环——get_next_item(tr) 阻塞直到某个 sequence 的 finish_item() 交过来一个 item,drive(tr) 做和之前一样的工作,item_done() 正是让那次 finish_item() 调用解除阻塞、让 sequence 得以继续的东西。
把 sequencer 接到 driver 上,以及 objection 去哪了
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_driver drv;
uvm_sequencer #(mux_transaction) sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
drv = my_driver::type_id::create("drv", this);
sqr = uvm_sequencer#(mux_transaction)::type_id::create("sqr", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
task run_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(sqr);
phase.drop_objection(this);
endtask
endclassdrv.seq_item_port.connect(sqr.seq_item_export) 是普通的 connect_phase 工作——第 2 章"connect_phase 自底向上执行"那条结论正是这行代码能安全写在这里的原因:等到 my_test 自己的 connect_phase 运行时,drv 和 sqr 都已经完全构造好了。
注意 objection 挪了位置:它已经完全不在 driver 里了。run_phase 的 forever 循环自己不再有天然的终点——它只是不停地处理 sequencer 交给它的东西,没完没了。唯一知道"激励够了"的,是当初决定要跑这个 sequence 的那个角色,现在是 test。seq.start(sqr) 是一个阻塞调用——它要等到 body() 跑完才会返回——所以把它包在 raise_objection/drop_objection 里,其实还是第 2 章教的那个模式,只不过这次用在了 test 调用的一个 task 上,而不是 driver 跑的一个循环上。
不同的运行,同样的 driver 和 sequencer
要跑不同的激励,my_driver 和 sqr 都不需要改——这正是第 1 章第三条承诺的全部意义。另写一个 sequence 类,写一个不同的 body(),用 seq2.start(sqr) 代替 seq.start(sqr) 来跑,就能通过完全相同的 driver 和 sequencer 驱动出一整套不同的 transaction。
小结
uvm_sequence #(T)是一个uvm_object(第 4 章),它的body()task 通过一个继承来的req属性生产类型为T的 item。start_item(req)/finish_item(req)是和对面(sequencer,再到 driver)的双向握手:finish_item()阻塞直到 driver 发信号说处理完了——这正是让 sequence 的执行节奏被"拍住"的地方。- 在
start_item()和finish_item()之间随机化一个 sequence item,用的正是 SV 第 12 章的randomize()/randomize() with {...}语法。 uvm_driver #(REQ, RSP=REQ)是一个自带seq_item_port的uvm_component——正是第 1 章第一个例子用过的基类。forever循环里的get_next_item()/drive()/item_done()取代了写死的激励,改成由连接上的 sequence 来生产。drv.seq_item_port.connect(sqr.seq_item_export)属于connect_phase,之所以安全,是因为第 2 章的自底向上 connect 顺序保证了两边都已经存在。- 一旦 driver 的
run_phase变成一个没有终点的循环,objection 就从 driver 挪到了 test:seq.start(sqr)阻塞直到 sequence 跑完,把它包在raise_objection/drop_objection里,就是 test 现在用来决定"激励够了"的方式。
在 body() 内部,为什么 finish_item(req) 会阻塞?
为什么 my_driver 现在改成继承 uvm_driver #(mux_transaction),而不是纯 uvm_component?
为什么 objection 从第 3-6 章 driver 的 run_phase 挪到了这一章 test 的 run_phase?
drv.seq_item_port.connect(sqr.seq_item_export); 依赖第 2 章的哪个结论?为什么写在 my_test 的 connect_phase 里是安全的?