第 6 章 · 共 6 章
综合实战:搭建高阶验证环境
把五章积累下来的类整理进一个 package,加一个从来不清中断的第二场景——证明 COUNT 越过阈值之后还能继续计数,同时 irq 保持电平、monitor 不会重复上报,再用 uvm 第 10 章那套 factory override 的手法,不碰 run_phase,就能在命令行里切换它和第 5 章的场景。
regfile_env的每一块拼图都已经就位——axi_agent 和 irq_agent(第 2、3 章)、协调它们的 virtual sequencer(第 4 章)、寄存器模型(第 5 章)。和 uvm 第 10 章一样,这一章更接近纯粹的组装,而不是引入新概念:把一切整理进一个 package,加一个真正从不同角度考验这套环境的第二场景,然后不改动任何已经跑通的东西,就能在命令行里切换这两个场景。
把一切整理进一个 package
跟 SV 第 16 章定下、uvm 第 10 章沿用的是同一个原则:一个类一个文件,用 `include 汇进一个 package,top module 只 import 一次。
`include "uvm_macros.svh"
package regfile_uvm_pkg;
import uvm_pkg::*;
`include "axil_regfile.sv"
`include "axi4lite_if.sv"
`include "irq_if.sv"
`include "axi_txn.sv"
`include "axi_driver.sv"
`include "axi_monitor.sv"
`include "axi_sequencer.sv"
`include "axi_agent.sv"
`include "axi_basic_seq.sv"
`include "irq_txn.sv"
`include "irq_monitor.sv"
`include "irq_agent.sv"
`include "irq_watcher.sv"
`include "ctrl_reg.sv"
`include "status_reg.sv"
`include "data_reg.sv"
`include "count_reg.sv"
`include "regfile_reg_block.sv"
`include "axi_reg_adapter.sv"
`include "regfile_vsqr.sv"
`include "regfile_env.sv"
`include "regfile_virtual_seq.sv"
`include "regfile_no_clear_seq.sv"
`include "axi_smoke_test.sv"
`include "regfile_no_clear_test.sv"
endpackage二十二个文件,横跨五章——而且每一个要么从第一次出现到现在原封未动,要么正好只多了后面某一章明确说过的那一处添加(irq_monitor 在第 4 章多了一行,regfile_vsqr/regfile_env 每一章各自多一个字段)。没有哪一处需要回头重写前面某一章的设计。
第二个场景:从来不清中断
第 5 章的场景每次确认中断拉高之后,紧接着就清掉它。这一章的新 sequence 故意反着来——反复写过阈值,从不清除,检查寄存器模型和中断在持续拉高的状态下表现是不是完全正确:
class regfile_no_clear_seq extends regfile_virtual_seq;
`uvm_object_utils(regfile_no_clear_seq)
function new(string name = "regfile_no_clear_seq");
super.new(name);
endfunction
task body();
uvm_event irq_event = uvm_event_pool::get_global("irq_event");
irq_txn ev_txn;
uvm_status_e status;
uvm_reg_data_t rdata;
regfile_reg_block regmodel = p_sequencer.regmodel;
write(8'h00, 32'h1); // CTRL: ENABLE=1
write(8'h08, 32'hAA); // #1
write(8'h08, 32'hBB); // #2
write(8'h08, 32'hCC); // #3
fork
write(8'h08, 32'hDD); // #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 at write #4", UVM_LOW)
else
`uvm_error("VSEQ", "expected irq to assert at write #4")
// 再过阈值写两次,全程没有任何 IRQ_CLR——irq 应该全程保持拉高,
// 而且既然 irq_monitor 只在信号发生*变化*时才上报(第 3 章),
// 这两次写都不应该再产生第二次 ASSERTED 事件。
write(8'h08, 32'hEE); // #5
write(8'h08, 32'hFF); // #6
regmodel.COUNT.read(status, rdata, UVM_FRONTDOOR, regmodel.default_map, this);
if (rdata == 32'd6)
`uvm_info("VSEQ", $sformatf("confirmed: COUNT kept incrementing past the threshold to %0d, while irq stayed asserted (and silent) the whole time", rdata), UVM_LOW)
else
`uvm_error("VSEQ", $sformatf("expected COUNT=6 after 6 enabled DATA writes with no clear, got %0d", rdata))
endtask
endclassextends regfile_virtual_seq,而不是直接继承 uvm_sequence——原因和 uvm 第 10 章 my_sequence_exhaustive extends my_sequence 一样:继承下来的 write() 辅助任务和 `uvm_declare_p_sequencer 原样复用,只有 body() 变了。拿这个场景跑一遍真实的 DUT,能精确证实两件事:COUNT 确实到了 6,而且整个 sequence 期间只触发了一次 irq 事件——第 5、6 次写发生的时候 irq 已经拉高了,所以 irq_monitor 的 vif.cb.irq !== last 检查(第 3 章)再也没看到值有变化可以上报。
第二个 test,不碰 run_phase
axi_smoke_test(第 2 到 5 章)完全没有变化。新 test 换的是到底跑哪一个 sequence 类,用的正是 uvm 第 6、10 章已经教过的那套机制:
class regfile_no_clear_test extends axi_smoke_test;
`uvm_component_utils(regfile_no_clear_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
regfile_virtual_seq::type_id::set_type_override(regfile_no_clear_seq::get_type());
super.build_phase(phase);
endfunction
endclassaxi_smoke_test 的 run_phase 通过 type_id::create(...) 建一个 regfile_virtual_seq 然后跑它——一字不改,原样继承。在 build_phase 里、那次 create() 调用真正执行之前设下的 factory override,才是让它实际构造出一个 regfile_no_clear_seq 的原因——和 uvm 第 10 章给 exhaustive_test 用的是完全相同的手法,只是这次换成了替换一个 virtual sequence 的类型,而不是一个普通 sequence 的类型。
跑起来这两个场景
module tb_top;
// ... 和第 1-5 章一样的 DUT、axi4lite_if、irq_if、config_db 交接 ...
initial begin
uvm_config_db#(virtual axi4lite_if.tb_mp)::set(null, "*", "vif", axi_if);
uvm_config_db#(virtual irq_if.mon_mp)::set(null, "*", "vif", irq_intf);
run_test();
end
endmodule不带参数的 run_test()(uvm 第 10 章)会从 +UVM_TESTNAME 读取要构建哪个 test:
+UVM_TESTNAME=axi_smoke_test // 第 5 章的场景:拉高,然后清除
+UVM_TESTNAME=regfile_no_clear_test // 这一章的场景:拉高,保持拉高,继续计数
同一份编译出来的环境,同一个 DUT,同一个 package——到底跑哪个场景是一个命令行参数,不是一次源码修改。
这个系列没有覆盖的
这套环境协调了两个 agent,通过 RAL 驱动了一个真实的寄存器映射,还跨越了一个被动观察者做了同步——确实比单 agent 的环境能做的多。但它检查的仍然只是这个系列手写出来的这两个场景。这里没有任何东西衡量这两个场景是不是够了——COUNT 每一个可达的值、每一种响应码、写和读之间每一种可能的先后顺序,是不是真的都被跑到过——也没有任何系统性的手段去找出一个这两个场景恰好都没触发的 bug。这正是 dv-methodology 明确的范围:覆盖率驱动的判断力(衡量并引导彻底性,而不只是检查两个手选的场景)和调试直觉(一旦这些检查里的某一个真的报错了,怎么系统地把它追出来)。和之前每一个 capstone 交接的方式一样:这里的一切都会被复用,没有任何东西被丢弃,下一个系列会把现在还在手工决定的东西标准化。
小结
- 二十二个文件,一个基于
`include的 package——和 SV 第 16 章、uvm第 10 章同一套组织原则,每个文件要么从引入它的那一章起原封未动,要么正好只多了那一章明确说过的那一处添加。 regfile_no_clear_seq extends regfile_virtual_seq复用继承来的write()辅助方法和p_sequencer声明,只改body()——和uvm第 10 章my_sequence_exhaustive extends my_sequence是同一种写法。- 在真实 DUT 上验证过:六次使能的
DATA写、全程不清除,COUNT会到6,整个过程只触发一次irq事件——irq_monitor(第 3 章)只在变化时才上报,意味着越过阈值之后的写不会产生重复事件。 regfile_no_clear_test extends axi_smoke_test,只重写了build_phase,在继承来的run_phase的create()调用真正跑之前装好 factory override——run_phase本身完全没碰,和uvm第 6、10 章教过的手法一模一样。run_test()配合+UVM_TESTNAME,在同一份编译好的 package 里,从命令行切换第 5 章的场景和这一章的场景。- 这个系列检查的是两个手选的场景;它从来没衡量过这样够不够,也没有系统性的手段去追一个这两个场景都没碰到的 bug。那是
dv-methodology的工作。
为什么 regfile_no_clear_seq 继承的是 regfile_virtual_seq,而不是直接继承 uvm_sequence?
拿 regfile_no_clear_seq 跑真实的 DUT,irq_monitor 总共会上报几次 irq 事件,为什么?
不带任何参数调用哪个系统任务/函数,会从 +UVM_TESTNAME 命令行 plusarg 读取要构建哪个 test?