第 8 章 · 共 10 章
Driver 与 Monitor:TLM 端口实战
monitor 那种 push 风格的 analysis_port,相对 sequencer 那种 pull 风格 seq_item_port 的区别;为什么它取代了 SV 第 14 章手写的 mailbox;给 mux_transaction 加上一个观测到的输出字段;以及把 sequencer/driver/monitor 打包成一个可配置的 uvm_agent。
第 7 章已经给了 driver 它的 seq_item_port 和最终的 run_phase 形态。第 1 章第四条承诺——可复用的验证 IP,而不是一套定制的 mailbox 握手——还缺的另一半是:一个 monitor,以及它广播观测结果的标准化方式。
两种 TLM 端口形态:pull 与 push
第 7 章的 seq_item_port 是一个 pull:driver 主动调用 get_next_item(),并阻塞直到有东西可用。monitor 的工作不一样——它不消费一个待处理任务队列,只是盯着 DUT,把看到的东西通知给不管有多少个监听者(第 9 章会接上一个 scoreboard;理论上可以同时接不止一个,虽然这个系列只会接一个)。这就是一个 push:uvm_analysis_port#(T),通过 write(T t) 调用把 t 立刻交给每一个连接上的接收方。
这正是 analysis_port 能取代 capstone 里手写的 mailbox(SV 第 14 章)的真正原因,而不只是给同一个东西换了个名字:mailbox 是为恰好一个进程排队等待稍后 get(),一次一个。write() 完全不排队——它立刻同步地调用每一个连接上的接收方自己的 write() 方法,不管当前有零个、一个还是好几个监听者,行为都一样。monitor 不需要知道也不需要关心下游是谁,它只管调用 write(),然后继续。
给 mux_transaction 加上观测到的输出
第 4 章的 mux_transaction 只需要 sel/a/b——driver 要驱动的全部东西。monitor 还需要捕获 DUT 实际产生的结果,所以它多了一个字段:
class mux_transaction extends uvm_sequence_item;
`uvm_object_utils(mux_transaction)
rand bit sel;
rand bit a;
rand bit b;
bit y; // 观测到的 DUT 输出 -- 不随机化,由 monitor 填入
// ... 构造函数不变 ...
function void do_copy(uvm_object rhs);
mux_transaction rhs_;
if (!$cast(rhs_, rhs))
`uvm_fatal("DO_COPY", "rhs is not a mux_transaction")
super.do_copy(rhs);
sel = rhs_.sel;
a = rhs_.a;
b = rhs_.b;
y = rhs_.y;
endfunction
function string convert2string();
return $sformatf("sel=%0b a=%0b b=%0b y=%0b", sel, a, b, y);
endfunction
// do_compare 也只需要加一行一样的比较:&& (y == rhs_.y)
endclassy 不是 rand——任何用来记录 DUT 实际行为的字段都不应该被随机化。
写一个 monitor
class my_monitor extends uvm_monitor;
`uvm_component_utils(my_monitor)
virtual mux2_if.tb_mp vif;
uvm_analysis_port #(mux_transaction) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);
if (!uvm_config_db#(virtual mux2_if.tb_mp)::get(this, "", "vif", vif))
`uvm_fatal("MON", "no virtual interface set for vif -- check the config_db::set() call in the top module")
endfunction
task run_phase(uvm_phase phase);
mux_transaction tr;
forever begin
@(vif.sel, vif.a, vif.b);
#1; // 等组合逻辑稳定下来,和 driver 用的是同一个等待
tr = mux_transaction::type_id::create("tr");
tr.sel = vif.sel;
tr.a = vif.a;
tr.b = vif.b;
tr.y = vif.y;
ap.write(tr);
end
endtask
endclass几点值得注意:
uvm_monitor相比uvm_component没有加任何东西——它纯粹是一个命名习惯,和uvm_test(第 3 章)存在的理由一样。用它而不是纯uvm_component,只是标记这个组件的角色。ap = new("ap", this),不是type_id::create(...)。 TLM 端口不是要经过 factory(第 6 章)的uvm_component/uvm_object子类——它们是普通构造出来的对象,所以用普通的new(...)调用才是正确、地道的构造方式。- monitor 复用了和 driver 完全一样的
virtual mux2_if.tb_mp视图和完全一样的config_db::get()模式——第 5 章顶层 module 里那次带"*"通配符的set(),本来就已经覆盖了任何查询"vif"的组件;monitor 只是又多了一个来查询的组件。 @(vif.sel, vif.a, vif.b)等待这三个信号中任意一个变化,然后#1给组合逻辑留出和 driver 自己的drive()task 一样的稳定时间——之后vif.y就反映了刚才驱动的结果。- 通过
tb_mpmodport 读取vif.sel/vif.a/vif.b是合法的,尽管tb_mp把它们声明成了output——modport 的output方向限制的是谁可以写一个信号,不是谁可以读一个你本来就能写的信号。(一个更精致的 testbench 会给 monitor 一个专属的只读 modport 视图;这个系列的mux2_if没有这么做,因为复用tb_mp更简单,而且同样合法。) ap.write(tr)现在还没有连接任何东西。 这一章结束时还没有任何监听者——那是第 9 章的工作。在一个没有连接任何东西的analysis_port上调用write()不是错误;它只是一次目前没有听众的广播。
打包起来:uvm_agent
第 1 章已经介绍过 uvm_agent——把 sequencer、driver、monitor 打包在一起,可以配置成 active 或 passive。现在三者都已经存在,打包它们几乎是机械性的工作:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_driver drv;
uvm_sequencer #(mux_transaction) sqr;
my_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 = my_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
drv = my_driver::type_id::create("drv", this);
sqr = uvm_sequencer#(mux_transaction)::type_id::create("sqr", 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
endclassuvm_agent 相比 uvm_component 只加了一样东西:一个内置的 is_active 字段(一个枚举类型,默认是 UVM_ACTIVE),这里通过 get_is_active() 读取。monitor 是无条件构造的——观测并不需要驱动。driver 和 sequencer 只有在 agent 处于 active 状态时才会被构造、被连接;一个 passive agent(配置方式和第 5 章讲的 config_db 一样,在 build_phase 运行之前设置 agent 的 is_active 字段)会同时丢掉这两者,只保留 monitor——适合那种应该被观测但绝不应该被驱动的总线一侧。
Test 也变简单了
class my_test extends uvm_test;
`uvm_component_utils(my_test)
my_agent agent;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = my_agent::type_id::create("agent", this);
endfunction
task run_phase(uvm_phase phase);
my_sequence seq;
phase.raise_objection(this);
seq = my_sequence::type_id::create("seq");
seq.start(agent.sqr);
phase.drop_objection(this);
endtask
endclassmy_test 不再单独管理 drv/sqr——my_agent 内部自己负责这部分连接,test 只需要通过它(agent.sqr)去启动一个 sequence。
小结
seq_item_port(第 7 章)是一个 pull:driver 主动请求,阻塞直到有东西可用。uvm_analysis_port#(T)是一个 push:write(t)立刻把t交给每一个连接上的接收方,不排队——这正是它能取代 SV 第 14 章mailbox的真正原因,而不只是换了个名字。- monitor 只观测不驱动:用和 driver 一样的
virtual interface和config_db::get()模式,但只读信号,从不给它们赋值。 uvm_monitor相比uvm_component没有加任何东西——和uvm_test一样,是一个标记组件角色的命名习惯。- TLM 端口用普通的
new(...)构造,不是type_id::create(...)——它们不是注册进 factory 的类型。 uvm_agent打包 sequencer、driver、monitor;get_is_active()(背后是一个内置的is_active字段)决定 driver/sequencer 是否会被构造、被连接,还是这个 agent 只保留 monitor(passive)。
除了名字不同,seq_item_port 的 pull 和 uvm_analysis_port 的 push 之间真正的功能差异是什么?
为什么 monitor 的 ap = new('ap', this); 用 new() 来写,而 my_monitor 本身却是用 my_monitor::type_id::create('mon', this) 构造的?
在 my_agent 的 build_phase 里,为什么 mon 是无条件构造的,而 drv 和 sqr 只有在 get_is_active() == UVM_ACTIVE 时才构造?
在这一章结束时,monitor 每次观测到活动都会调用 mon.ap.write(tr),但 mon.ap 还没连接任何东西。会发生什么?