第 2 章 · 共 6 章
AXI4-Lite Driver、Monitor 与 Agent
把 axil_regfile 包进真正的 UVM:一个 axi_txn sequence item、一个终于要等 READY 而不是组合驱动的 driver、一个从两个独立通道重建事务的 monitor,两者一起打包成 axi_agent——直接复用 UVM 基础系列的 seq_item_port/analysis_port 机制,因为这里真正新的东西只有握手本身。
第 1 章是手动驱动 axil_regfile 的——两个纯任务(axi_write/axi_read),直接从 initial 块里调用。这一章把完全相同的驱动逻辑包进 UVM:一个 sequence item、一个 driver、一个 monitor,外加一个 uvm_agent——全部用 uvm 第 8 章已经教过的机制搭出来(seq_item_port、analysis_port、一个 virtual interface 句柄)。UVM 这套管道本身没有任何新东西。真正新的是管道另一头连着什么:一次真正的握手,第一次能让 driver 等待——mux2 是组合逻辑,uvm 第 8 章的 driver 从来没有哪次需要停下来等一个 ready 信号。
axi_txn:一个 item,两个方向
一种事务类型同时覆盖读和写,用 is_write 区分——沿用 uvm 第 8 章给 mux_transaction 加 y 字段时定下的先例,让一个 transaction 既能携带驱动出去的内容,也能携带 DUT 产生的内容,axi_txn 同时携带 sequence 填的字段(addr、wdata)和事务完成后 driver 填回来的字段(rdata、resp):
class axi_txn extends uvm_sequence_item;
rand bit is_write;
rand bit [7:0] addr;
rand bit [31:0] wdata;
bit [31:0] rdata;
bit [1:0] resp;
`uvm_object_utils(axi_txn)
function new(string name = "axi_txn");
super.new(name);
endfunction
function void do_copy(uvm_object rhs);
axi_txn rhs_;
if (!$cast(rhs_, rhs)) `uvm_fatal("AXI_TXN", "do_copy: cast failed")
super.do_copy(rhs);
is_write = rhs_.is_write;
addr = rhs_.addr;
wdata = rhs_.wdata;
rdata = rhs_.rdata;
resp = rhs_.resp;
endfunction
function bit do_compare(uvm_object rhs, uvm_comparer comparer);
axi_txn rhs_;
if (!$cast(rhs_, rhs)) return 0;
return (is_write == rhs_.is_write) && (addr == rhs_.addr) &&
(wdata == rhs_.wdata) && (rdata == rhs_.rdata) && (resp == rhs_.resp);
endfunction
function string convert2string();
return is_write ?
$sformatf("WRITE addr=0x%0h wdata=0x%0h resp=%0d", addr, wdata, resp) :
$sformatf("READ addr=0x%0h rdata=0x%0h resp=%0d", addr, rdata, resp);
endfunction
endclass手写的 do_copy/do_compare/convert2string,uvm_object_utils——完全是 uvm 第 4 章的写法,这里也没有新东西。
Driver:第一次真正遇到反压(backpressure)
driver 的 drive() 任务直接照搬第 1 章的 axi_write/axi_read 任务——同样的握手、同样通过 clocking block 访问,只是现在活在一个类方法里,而不是一个裸的 initial 块:
class axi_driver extends uvm_driver #(axi_txn);
`uvm_component_utils(axi_driver)
virtual axi4lite_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 axi4lite_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("AXI_DRV", "virtual interface not set")
endfunction
task run_phase(uvm_phase phase);
// axi4lite_if.tb_mp 只暴露了 clocking block(第 1 章),没有直接暴露
// aresetn——driver 没法自己感知复位,所以只是等过 top-level testbench
// 用的那个固定复位窗口。这和第 1 章手动驱动的 testbench 用的是同一个
// 简化,只是现在藏在类里面了,这里明说一下。
repeat (4) @(vif.cb);
vif.cb.awvalid <= 1'b0;
vif.cb.wvalid <= 1'b0;
vif.cb.bready <= 1'b1;
vif.cb.arvalid <= 1'b0;
vif.cb.rready <= 1'b1;
forever begin
seq_item_port.get_next_item(req);
drive(req);
seq_item_port.item_done();
end
endtask
task drive(axi_txn txn);
if (txn.is_write) begin
vif.cb.awaddr <= txn.addr;
vif.cb.awvalid <= 1'b1;
vif.cb.wdata <= txn.wdata;
vif.cb.wstrb <= 4'hF;
vif.cb.wvalid <= 1'b1;
@(vif.cb);
while (!(vif.cb.awready && vif.cb.wready)) @(vif.cb);
vif.cb.awvalid <= 1'b0;
vif.cb.wvalid <= 1'b0;
while (!vif.cb.bvalid) @(vif.cb);
txn.resp = vif.cb.bresp;
end else begin
vif.cb.araddr <= txn.addr;
vif.cb.arvalid <= 1'b1;
@(vif.cb);
while (!vif.cb.arready) @(vif.cb);
vif.cb.arvalid <= 1'b0;
while (!vif.cb.rvalid) @(vif.cb);
txn.rdata = vif.cb.rdata;
txn.resp = vif.cb.rresp;
end
endtask
endclassget_next_item/item_done 正是 uvm 第 7 章预告、第 8 章完整展开的那套 sequencer/driver 双向协议——req 是 uvm_driver #(axi_txn) 自带的句柄,由 get_next_item 填好。真正新的地方是 drive() 里那两个 while 循环:mux2 是组合逻辑,uvm 第 8 章的 driver 从来没有遇到过"已经拉高了某个信号,还得原地等 DUT 跟上"这种情况。这里,axil_regfile 可以(而且按 spec 是允许的)把 awready/wready/arready 拉低任意多个周期——driver 必须写得能容忍这一点,而不是想当然地假设一个周期总够用。
Monitor:从两个独立通道重建事务
monitor 什么都不驱动——它观察和 driver 相同的 vif.cb(复用 driver 的 tb_mp 视图,而不是单独搞一个只读视图,这和 uvm 第 8 章给 mux2_if 的 monitor 标注过的简化是同一个道理),把完成的事务通过 analysis_port 报告出去:
class axi_monitor extends uvm_monitor;
`uvm_component_utils(axi_monitor)
virtual axi4lite_if.tb_mp vif;
uvm_analysis_port #(axi_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 axi4lite_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("AXI_MON", "virtual interface not set")
endfunction
task run_phase(uvm_phase phase);
fork
watch_write();
watch_read();
join
endtask
task watch_write();
bit [7:0] addr_q;
bit [31:0] data_q;
forever begin
@(vif.cb);
if (vif.cb.awvalid && vif.cb.awready) addr_q = vif.cb.awaddr;
if (vif.cb.wvalid && vif.cb.wready) data_q = vif.cb.wdata;
if (vif.cb.bvalid && vif.cb.bready) begin
axi_txn txn = axi_txn::type_id::create("txn");
txn.is_write = 1'b1;
txn.addr = addr_q;
txn.wdata = data_q;
txn.resp = vif.cb.bresp;
ap.write(txn);
end
end
endtask
task watch_read();
bit [7:0] addr_q;
forever begin
@(vif.cb);
if (vif.cb.arvalid && vif.cb.arready) addr_q = vif.cb.araddr;
if (vif.cb.rvalid && vif.cb.rready) begin
axi_txn txn = axi_txn::type_id::create("txn");
txn.is_write = 1'b0;
txn.addr = addr_q;
txn.rdata = vif.cb.rdata;
txn.resp = vif.cb.rresp;
ap.write(txn);
end
end
endtask
endclassfork ... join 让 watch_write() 和 watch_read() 作为两个独立并发的进程运行——这正是 systemverilog-basics 第 14 章教过的构造,这次真正派上了用场:写通道和读通道确实需要各自独立的观察者,因为它们可能同时都处在事务进行中。注意 watch_write() 比 axil_regfile 自己(第 1 章)的写接受逻辑要简单——DUT 得决定自己是不是准备好接受新的 AW/W(这正是 aw_have/w_have 和那些 ready 信号存在的原因),但 monitor 只需要记住它看到的最新地址和数据,然后跟下一次 BVALID 配对——由于一次只有一笔事务在途(第 1 章的 DUT 设计),到 BVALID 出现的时候,最近一次捕获的 addr_q/data_q 必然就是对的。
打包成 axi_agent
class axi_sequencer extends uvm_sequencer #(axi_txn);
`uvm_component_utils(axi_sequencer)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
endclass
class axi_agent extends uvm_agent;
`uvm_component_utils(axi_agent)
axi_sequencer sqr;
axi_driver drv;
axi_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 = axi_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = axi_sequencer::type_id::create("sqr", this);
drv = axi_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (get_is_active() == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass这正是 uvm 第 8 章介绍过的 uvm_agent 写法:get_is_active() == UVM_ACTIVE 决定 sequencer/driver 到底建不建——只要换个配置,同一个 agent 就能复用成一个纯被动、只负责观察的角色,第 3 章会把这个特性真正用在 irq_agent 上。
一个 sequence 和一个 test:同样的场景,现在由 UVM 驱动
class axi_basic_seq extends uvm_sequence #(axi_txn);
`uvm_object_utils(axi_basic_seq)
function new(string name = "axi_basic_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);
req.is_write = 1'b1;
req.addr = addr;
req.wdata = data;
finish_item(req);
endtask
task read(bit [7:0] addr);
axi_txn req = axi_txn::type_id::create("req");
start_item(req);
req.is_write = 1'b0;
req.addr = addr;
finish_item(req);
endtask
task body();
write(8'h00, 32'h1); // CTRL: ENABLE=1
write(8'h08, 32'hAA); // DATA write #1
write(8'h08, 32'hBB); // DATA write #2
write(8'h08, 32'hCC); // DATA write #3
write(8'h08, 32'hDD); // DATA write #4 -> COUNT=4 == IRQ_THRESHOLD
read(8'h0C); // COUNT
read(8'h04); // STATUS -- expect bit0=1
write(8'h00, 32'h3); // CTRL: ENABLE=1, IRQ_CLR=1
read(8'h04); // STATUS -- expect bit0=0 again
read(8'h10); // unmapped -> DECERR
endtask
endclass
class axi_smoke_test extends uvm_test;
`uvm_component_utils(axi_smoke_test)
axi_agent agt;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt = axi_agent::type_id::create("agt", 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(agt.sqr);
phase.drop_objection(this);
endtask
endclassstart_item/finish_item(uvm 第 7 章)、工厂的 type_id::create()(uvm 第 6 章)、把 objection 包在 test 的阻塞调用 seq.start(sqr) 外面(uvm 第 7 章自己定下的"一旦 run_phase 不再是手写循环,objection 就从 driver 挪到 test"这个决定)——这个 sequence 和 test 完全是用读者已经掌握的材料拼出来的。沿用第 1 章的 axil_regfile 和 axi4lite_if(原样不动),top-level module 走的是 uvm 第 3 章那套 config_db 交接的写法:
`include "uvm_macros.svh"
import uvm_pkg::*;
module tb_top;
logic aclk;
logic irq;
axi4lite_if axi_if (.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)
);
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);
run_test("axi_smoke_test");
end
endmodule这一章的例子需要一个既支持 UVM、又完整实现 clocking block 的仿真器——Icarus Verilog 两条都不满足。在 EDA Playground 上选 Aldec Riviera-PRO(免费,不需要你自己的授权——和第 1 章、SV 第 13 章指向的是同一个替代方案);它一次满足这两个要求。
小结
axi_txn同时携带 sequence 提出的请求(addr/wdata)和事务完成后 driver 观察到的结果(rdata/resp)——和uvm第 8 章给mux_transaction加y字段是同一种"一个 transaction 同时携带两个方向"的写法。- driver 的
drive()任务本质上就是第 1 章手写的那两个任务,没有实质改变,只是现在由get_next_item/item_done调用,而不是裸的initial块。 - 真正新的东西是反压:
axil_regfile可以把READY拉低任意多个周期,driver 必须写得能等它——mux2从来不需要这个。 - monitor 重建已完成事务所需的记账比 DUT 接受它们所需的要少——只需要记住最新的地址/数据,跟下一次响应配对,因为一次只有一笔事务在途。
axi_agent就是uvm第 8 章那套uvm_agent写法——get_is_active()决定 sequencer/driver 到底存不存在,为第 3 章被动的irq_agent做好了铺垫。- 这一章的例子既需要支持 UVM 的仿真器,也需要完整支持 clocking block 的仿真器——Icarus Verilog 两个都没有;用 EDA Playground 上的 Aldec Riviera-PRO。
为什么 axi_driver 的 drive() 任务需要一个等待 awready/wready 的 while 循环,而 uvm 第 8 章 mux2 的 driver 从来不需要类似的东西?
axi_monitor 的 watch_write() 为什么比第 1 章 axil_regfile 自己的写接受逻辑简单,尽管两者重建的是同一个 AW/W 独立性?
axi_txn 的哪个字段用来区分一次读事务和一次写事务?