第 3 章 · 共 6 章
第二个接口:irq Monitor 与多 Agent 环境
给 axil_regfile 的 irq 输出配一个最小化的独立接口,搭一个纯被动的 irq_agent 去观察它(完全没有 driver——这个信号上根本没有 testbench 能驱动的东西),再把它和 axi_agent 一起装进一个 uvm_env:这正是 uvm 第 10 章留下的“协调不止一个 agent”这个缺口的直接兑现。
uvm 第 10 章结尾点名了单 agent 环境做不到的事:"协调不止一个 agent……真实的 DUT 通常不止一个接口。"axil_regfile 从第 1 章起就有这个第二接口——irq——但到目前为止它一直只是一根裸线,在一个普通的 initial 块里直接读、或者用 $display 打印。这一章给它配上和 axi4lite_if 一样的待遇:自己的 interface、自己的 monitor,以及在一个真正的多 agent 环境里的一席之地。
irq_if:一个最小化的被动接口
irq 从头到尾只需要被读——这个系列里从来没有谁会去驱动它,因为它是 DUT 自己的输出。这让接口本身简化到几乎没什么可写的:一个信号,一个只有输入侧的 clocking block,完全不需要配置输出偏移:
interface irq_if (input logic aclk);
logic irq;
clocking cb @(posedge aclk);
default input #1step;
input irq;
endclocking
modport dut_mp (input aclk, output irq);
modport mon_mp (clocking cb);
endinterface和 axi4lite_if(SV 第 13 章)用的是同一套 input #1step 采样保证——拿到的是时钟边沿前一刻已经稳定的值,这样 monitor 读 irq 的时候,永远不会跟 DUT 自己在同一个边沿的寄存器更新赛跑。dut_mp 从 DUT 一侧把 irq 声明成输出(axil_regfile 就是从这里驱动它的);mon_mp 是 axi4lite_if 的 tb_mp 的镜像——一个只暴露 clocking block 的 modport,只不过这次是给一个只读不写的组件用的。
irq_txn 与 irq_monitor:报告变化沿,而不是电平
观察一个电平信号的 monitor 有两种选择:每个周期都报告一次当前值,或者只在值变化时才报告。每周期都报告会让 analysis port 被一连串"还在拉高"的重复事件淹没;只报告变化沿才是真正有用的——"irq 刚刚拉高了"是一个有意义的事件,"irq 已经拉高了一千个周期"只是噪声。
class irq_txn extends uvm_sequence_item;
rand bit level;
`uvm_object_utils(irq_txn)
function new(string name = "irq_txn");
super.new(name);
endfunction
function void do_copy(uvm_object rhs);
irq_txn rhs_;
if (!$cast(rhs_, rhs)) `uvm_fatal("IRQ_TXN", "do_copy: cast failed")
super.do_copy(rhs);
level = rhs_.level;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
irq_txn rhs_;
if (!$cast(rhs_, rhs)) return 0;
return (level == rhs_.level);
endfunction
function string convert2string();
return $sformatf("IRQ %s", level ? "ASSERTED" : "DEASSERTED");
endfunction
endclass
class irq_monitor extends uvm_monitor;
`uvm_component_utils(irq_monitor)
virtual irq_if.mon_mp vif;
uvm_analysis_port #(irq_txn) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual irq_if.mon_mp)::get(this, "", "vif", vif))
`uvm_fatal("IRQ_MON", "virtual interface not set")
endfunction
task run_phase(uvm_phase phase);
bit last = 1'b0;
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);
last = vif.cb.irq;
end
end
endtask
endclassirq_txn 是一个 uvm_sequence_item,尽管从来没有谁会真的去 sequence 它——它用的是和 axi_txn 同一条基类链(uvm 第 4 章的 uvm_object → uvm_transaction → uvm_sequence_item),这里纯粹是拿它当一个方便、经过工厂注册、能配合 analysis port 使用的数据容器。uvm_analysis_port/ap.write() 完全是第 2 章 axi_monitor 的写法,原封不动。
irq_agent:为什么没有 active 分支
第 2 章的 axi_agent 用 get_is_active() == UVM_ACTIVE 来决定要不要建 sequencer 和 driver——同一个类,配置不同就能两种都跑。irq_agent 根本不需要做这个选择:
class irq_agent extends uvm_agent;
`uvm_component_utils(irq_agent)
irq_monitor mon;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = irq_monitor::type_id::create("mon", this);
endfunction
endclass这里没有 sqr/drv 字段,没有 get_is_active() 判断,压根就没有 active 分支——不是这个 agent 忘了支持,而是因为 irq 这个接口上根本没有 testbench 能去驱动的东西。axi_agent 确实可能被配成被动模式跑(比如某个 test 只想观察 AXI 流量,不想生成它),因为它两个分支都建好了;irq_agent 在结构上就做不到,因为驱动 irq 的永远只有 DUT 自己。uvm 第 8 章的 active/passive 之分,说的是同一个接口,不同的 test 可能想做出不同的选择——而这里是接口本身就取消了这个选择。
regfile_env:两个 agent 归到同一个父节点下
class irq_watcher extends uvm_component;
`uvm_component_utils(irq_watcher)
uvm_analysis_imp #(irq_txn, irq_watcher) imp;
function new(string name, uvm_component parent);
super.new(name, parent);
imp = new("imp", this);
endfunction
function void write(irq_txn txn);
`uvm_info("IRQ_WATCH", txn.convert2string(), UVM_LOW)
endfunction
endclass
class regfile_env extends uvm_env;
`uvm_component_utils(regfile_env)
axi_agent axi_agt;
irq_agent irq_agt;
irq_watcher irqw;
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);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
irq_agt.mon.ap.connect(irqw.imp);
endfunction
endclassuvm_env 打包不止一个 agent 完全是 uvm 第 9 章的写法(那里打包的是一个 agent 加一个 scoreboard);irq_watcher 的 uvm_analysis_imp #(irq_txn, irq_watcher) + write(),正是第 9 章 scoreboard 用来真正接收 monitor 广播内容的那套接收端机制,不只是发送端。这里没有任何新的 UVM 表面——就是第 9 章的同两个机制,套用在第二个 agent 上,而不是 scoreboard 上。
看它跑起来
沿用第 2 章的 axi_txn/axi_driver/axi_monitor/axi_sequencer/axi_agent/axi_basic_seq(原样不动),test 现在建的是 regfile_env,而不是一个裸的 axi_agent:
class axi_smoke_test extends uvm_test;
`uvm_component_utils(axi_smoke_test)
regfile_env env;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = regfile_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
axi_basic_seq seq = axi_basic_seq::type_id::create("seq");
phase.raise_objection(this);
seq.start(env.axi_agt.sqr);
phase.drop_objection(this);
endtask
endclasstop-level module 加上 irq_if,把它接到 DUT 的 irq 端口上,而不是一根裸的 logic,再通过 config_db 多发布一个 virtual interface——同样的调用,只是换了个类型参数,不会冲突,因为 uvm_config_db#(virtual axi4lite_if.tb_mp) 和 uvm_config_db#(virtual irq_if.mon_mp) 是同一个类的两种不同参数化实例,各自有各自的查找表:
`include "uvm_macros.svh"
import uvm_pkg::*;
module tb_top;
logic aclk;
axi4lite_if axi_if (.aclk(aclk));
irq_if irq_intf (.aclk(aclk));
axil_regfile #(.IRQ_THRESHOLD(4)) dut (
.s_axi_aclk (axi_if.aclk),
.s_axi_aresetn (axi_if.aresetn),
.s_axi_awaddr (axi_if.awaddr),
.s_axi_awprot (axi_if.awprot),
.s_axi_awvalid (axi_if.awvalid),
.s_axi_awready (axi_if.awready),
.s_axi_wdata (axi_if.wdata),
.s_axi_wstrb (axi_if.wstrb),
.s_axi_wvalid (axi_if.wvalid),
.s_axi_wready (axi_if.wready),
.s_axi_bresp (axi_if.bresp),
.s_axi_bvalid (axi_if.bvalid),
.s_axi_bready (axi_if.bready),
.s_axi_araddr (axi_if.araddr),
.s_axi_arprot (axi_if.arprot),
.s_axi_arvalid (axi_if.arvalid),
.s_axi_arready (axi_if.arready),
.s_axi_rdata (axi_if.rdata),
.s_axi_rresp (axi_if.rresp),
.s_axi_rvalid (axi_if.rvalid),
.s_axi_rready (axi_if.rready),
.irq (irq_intf.irq)
);
initial aclk = 1'b0;
always #5 aclk = ~aclk;
initial begin
axi_if.aresetn = 1'b0;
repeat (3) @(posedge aclk);
axi_if.aresetn = 1'b1;
end
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("axi_smoke_test");
end
endmodule现在跑 axi_basic_seq 那套场景,会打印出两行这一章之前从没出现过的 uvm_info,都来自 irq_watcher——第四次 DATA 写之后紧跟着 IRQ ASSERTED,CTRL.IRQ_CLR 那次写之后紧跟着 IRQ DEASSERTED——还是同一套环境,现在真正在观察第二个接口了,而不只是驱动第一个。
小结
irq_if就是把axi4lite_if的写法精简到一个纯被观察信号所需要的最小限度:一个 clocking block,只有输入,没有输出偏移需要配置。irq_monitor报告的是变化沿,不是电平——vif.cb.irq !== last把一千个相同的周期,变成每次拉高/拉低各自恰好一个有意义的事件。irq_agent完全没有 active 分支,跟axi_agent不一样——不是遗漏,而是irq这个信号除了 DUT 之外没人会驱动的直接结果。regfile_env把两个 agent 打包进同一个uvm_env,是uvm第 9 章的写法;irq_watcher的uvm_analysis_imp/write()就是第 9 章 scoreboard 用过的同一套接收端机制,这次用在第二个 agent 上。- 这正是
uvm第 10 章那个明确点名的缺口的直接兑现:一个真正协调不止一个 agent 的环境,这是这个网站历史上第一次。
为什么 irq_agent 完全没有 active 分支,而 axi_agent 的 build_phase 明确检查了 get_is_active()?
为什么 irq_monitor 要检查 `vif.cb.irq !== last`,而不是每次 clocking event 都广播一个 irq_txn?
regfile_env 继承自哪个 UVM 基类?uvm 第 9 章用同一个基类把一个 agent 和一个 scoreboard打包在一起。