第 4 章 · 共 6 章
Virtual Sequencer 与 Virtual Sequence
一个什么都不驱动的 uvm_sequencer,从一个地方协调 axi_agent 和 irq_agent——uvm_event/uvm_event_pool 作为跨 agent 的同步原语,以及 BVALID 和 irq 之间一个真实存在的同一时钟边沿竞争,靠实测仿真时间戳抓出来,而不只是纸面推理。
第 2、3 两章搭出的两个 agent 从来没有真正对过话:axi_agent 负责驱动,irq_agent 一直只是被动观察,而到目前为止每一个 sequence 都只在一个 sequencer 上跑。uvm 第 10 章明确点名过这个缺口——"把多个 sequence 分层组合"是更大的环境会遇到的真实问题。这一章把它补上:一个 sequence,协调两个 agent,用的是一个 virtual sequencer——一个什么都没挂的 uvm_sequencer。
为什么叫"virtual"——一个没有东西可驱动的 sequencer
axi_sequencer(第 2 章)存在的意义就是通过 seq_item_port/seq_item_export 跟 axi_driver 配对——它产出的每一个 item 最终都会到达一个真正拿它做事的 driver。virtual sequencer 是一个从来不接任何 driver 的 uvm_sequencer:
class regfile_vsqr extends uvm_sequencer;
`uvm_component_utils(regfile_vsqr)
axi_sequencer axi_sqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass它持有的是真正的 sequencer 的句柄(axi_sqr,在下面 regfile_env 的 connect_phase 里接好)。跑在 regfile_vsqr "上"的 sequence 从来不会对 regfile_vsqr 自己调用 start_item/finish_item——那样的话另一头根本没有东西能接住它。它面向的是明确指定的 axi_sqr,而这正是这一章唯一需要的新 API:start_item 接受一个可选的第三个参数,指定实际要用哪个 sequencer,覆盖掉 sequence 自己默认的那个。
跨越 agent:uvm_event 与 uvm_event_pool
从 virtual sequence 驱动 axi_agent,只是熟悉的调用多加了一个参数。跟 irq_agent 协调则是另一回事:irq_agent 压根没有 sequencer(第 3 章),所以没有对应的 start_item 可用——virtual sequence 需要知道的是monitor 什么时候观察到了什么,而不是给它发一条命令。
uvm_event 就是 UVM 给出的答案:一个可以被一段代码 trigger()、被另一段代码用 wait_trigger() 阻塞等待的会合对象。uvm_event_pool::get_global(name) 会把同一个单例事件交给任何用同一个名字字符串调用它的地方——不需要在压根不在同一条组件树分支上的两个组件之间,通过 connect_phase 手动接一条线。irq_monitor(第 3 章)只增加了一处——run_phase 里其余部分都没变:
task run_phase(uvm_phase phase);
bit last = 1'b0;
uvm_event irq_event = uvm_event_pool::get_global("irq_event");
forever begin
@(vif.cb);
if (vif.cb.irq !== last) begin
irq_txn txn = irq_txn::type_id::create("txn");
txn.level = vif.cb.irq;
ap.write(txn);
irq_event.trigger(txn);
last = vif.cb.irq;
end
end
endtaskap.write(txn)(第 3 章)和 irq_event.trigger(txn) 是同一次观察的两种不同消费方式,服务于两个不同目的:analysis_port 广播给任意数量松耦合的观察者(未来的 scoreboard、覆盖率收集器——没有谁必须在听);uvm_event 是给恰好一段需要阻塞等到这件事发生的代码用的紧耦合会合点。trigger(txn) 把 transaction 本身当作触发数据传出去,这样从 wait_trigger() 醒来的一方就能检查到底是哪个变化发生了。
一个真实存在的竞争,靠实测时间抓出来
写 virtual sequence 的 body 时,直觉的写法是:发完四次 DATA 写,再调用 irq_event.wait_trigger()。这是错的,而且不是什么纸面上才成立的微妙理由——它错在一件可以直接测出来的事情上。axil_regfile 的 always_ff 块更新 count_reg(irq 就是从它算出来的)和拉高 s_axi_bvalid,对这次写来说是在同一个带时钟的 always 块里完成的,所以它们在完全相同的时钟边沿变化。直接给 DUT 加探针能证实这一点:
t=175000 BVALID rose
t=175000 IRQ rose
t=205000 BVALID rose
t=205000 IRQ fell
第四次 DATA 写的响应和 irq 的拉高,落在完全相同的仿真时刻;IRQ_CLR 那次写和 irq 的拉低同样如此。这意味着 driver 的 item_done()(解锁 sequence 的 finish_item())和 monitor 的 irq_event.trigger() 调用,是两个被同一个仿真时间点唤醒的独立进程——SystemVerilog 并不保证它们谁先跑。如果 monitor 的进程碰巧先跑,它就会在 virtual sequence 还没来得及调用 wait_trigger() 之前,把事件触发并清空——sequence 就会永远卡在等一个已经发生过的触发上。
修法是让"等待"的注册发生在事件有可能触发之前,而不是之后——把那次写和等待 fork 到一起,而不是顺序执行:
fork
write(8'h08, 32'hDD); // 越过 IRQ_THRESHOLD 的那次写
irq_event.wait_trigger();
join两个分支在同一个仿真时刻同时开始,那时写操作和触发都还没发生——所以不管仿真器碰巧先调度这两个独立进程里的哪一个,共享时钟边沿到来的时候,wait_trigger() 早已经注册好在等了。这和 `uvm_info 的 objection 那套材料、以及这个系列自己讲过的 AW/W 独立性规则,本质上是同一类排序隐患换了个形式出现:两件事发生在同一个仿真时刻,就需要一个明确的顺序保证,否则就是没有保证。
regfile_vsqr 与 regfile_virtual_seq
class regfile_virtual_seq extends uvm_sequence;
`uvm_object_utils(regfile_virtual_seq)
`uvm_declare_p_sequencer(regfile_vsqr)
function new(string name = "regfile_virtual_seq");
super.new(name);
endfunction
task write(bit [7:0] addr, bit [31:0] data);
axi_txn req = axi_txn::type_id::create("req");
start_item(req, -1, p_sequencer.axi_sqr);
req.is_write = 1'b1;
req.addr = addr;
req.wdata = data;
finish_item(req);
endtask
task body();
uvm_event irq_event = uvm_event_pool::get_global("irq_event");
irq_txn ev_txn;
write(8'h00, 32'h1); // CTRL: ENABLE=1
write(8'h08, 32'hAA); // DATA #1
write(8'h08, 32'hBB); // DATA #2
write(8'h08, 32'hCC); // DATA #3
fork
write(8'h08, 32'hDD); // DATA #4 -> 越过 IRQ_THRESHOLD
irq_event.wait_trigger();
join
if ($cast(ev_txn, irq_event.get_trigger_data()) && ev_txn.level == 1'b1)
`uvm_info("VSEQ", "confirmed: irq asserted right after the write that crossed IRQ_THRESHOLD", UVM_LOW)
else
`uvm_error("VSEQ", "expected irq to assert after the 4th DATA write")
fork
write(8'h00, 32'h3); // CTRL: ENABLE=1, IRQ_CLR=1
irq_event.wait_trigger();
join
if ($cast(ev_txn, irq_event.get_trigger_data()) && ev_txn.level == 1'b0)
`uvm_info("VSEQ", "confirmed: irq deasserted right after IRQ_CLR", UVM_LOW)
else
`uvm_error("VSEQ", "expected irq to deassert after IRQ_CLR")
endtask
endclassuvm_declare_p_sequencer(regfile_vsqr) 是这一章加进词汇表的唯一一个宏——它声明一个带类型的 p_sequencer 句柄,这样 body() 就能直接访问 p_sequencer.axi_sqr,不需要手动 $cast。其余的一切——start_item/finish_item、uvm_object_utils、工厂的 type_id::create()——都是 uvm 第 2、6 章的东西,没有变化。在每个同步点用 get_trigger_data() 的 level 去核对 sequence 的预期,不只是记个日志——它几乎不花额外代码就做了一次真正的正确性检查,跟 uvm 第 9 章 scoreboard 那种"发现不一致"的直觉是同一回事,只是这次是内联写的,不是单独一个组件。
接进 regfile_env 和 test
regfile_env(第 3 章)在已有的两个 agent 之外,多了一个 virtual sequencer,在 connect_phase 里接到真正的 sequencer 上:
class regfile_env extends uvm_env;
`uvm_component_utils(regfile_env)
axi_agent axi_agt;
irq_agent irq_agt;
irq_watcher irqw;
regfile_vsqr vsqr;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
axi_agt = axi_agent::type_id::create("axi_agt", this);
irq_agt = irq_agent::type_id::create("irq_agt", this);
irqw = irq_watcher::type_id::create("irqw", this);
vsqr = regfile_vsqr::type_id::create("vsqr", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
irq_agt.mon.ap.connect(irqw.imp);
vsqr.axi_sqr = axi_agt.sqr;
endfunction
endclasstest 现在跑的是 regfile_virtual_seq 在 env.vsqr 上,而不是 axi_basic_seq 直接跑在 env.axi_agt.sqr 上:
task run_phase(uvm_phase phase);
regfile_virtual_seq seq = regfile_virtual_seq::type_id::create("seq");
phase.raise_objection(this);
seq.start(env.vsqr);
phase.drop_objection(this);
endtasktop-level module、axil_regfile、axi4lite_if、irq_if 都没有变化——和第 3 章一样的 DUT、一样的接口、一样的 config_db 交接。现在运行它,会从 virtual sequence 自己打印出两条确认信息,而不只是 irq_watcher 的两条观察——这套环境不再只是让两个 agent 并排跑,而是真正从一个地方协调它们了。
小结
- virtual sequencer 是一个没接任何 driver 的
uvm_sequencer——它存在的唯一目的是持有 virtual sequence 需要用到的那些真正 sequencer 的句柄。 start_item(req, -1, sequencer)的第三个参数是这一章唯一需要的新 API——发 item 这件事的其余部分和uvm第 2、6、7 章完全一样。uvm_event/uvm_event_pool::get_global(name)是 UVM 跨组件的会合原语——monitortrigger()它,sequencewait_trigger()它,不需要在不同分支的组件之间做任何connect_phase接线。- driver 察觉
BVALID和 monitor 察觉irq之间确实存在一个竞争,因为两者在 DUT 里是同一个时钟边沿驱动的——这是靠给实际仿真加探针确认的,不是假设出来的。修法:把触发那次写和等待fork到一起,让等待方在触发有可能发生之前就已经注册好。 - 在每个同步点检查
get_trigger_data()是否符合预期,是一次几乎零成本的真正正确性检查——和uvm第 9 章 scoreboard 的直觉一样,只是内联写的。
为什么在 write(8'h08, 32'hDD) 返回之后紧接着调用 irq_event.wait_trigger() 是不可靠的,而不是把两者 fork 到一起?
regfile_vsqr 的 axi_sqr 字段是什么,它的值是怎么来的?
UVM 类库里哪个方法会把同一个全局共享的 uvm_event,返回给任何用同一个名字字符串调用它的地方?