SV agent 封装:driver/monitor/sequencer 三合一组件组装与配置全解
更新时间:2026-09-28。在 SystemVerilog TLM 通信 里我们解决了组件之间怎么传事务,在 分层 testbench 架构与复用 里看到了 agent 是分层复用的基本单元。这一篇把 agent 本身拆开:driver、monitor、sequencer 三个组件怎样在一个
uvm_agent里组装、谁和谁 connect、配置怎么进来、对外露出什么端口,最后给出一个能直接搬进项目的手搓 agent 骨架。
本文要回答的问题
- 为什么要把 driver、monitor、sequencer 包进一个 agent,而不是平铺在 env 里?
- agent 的
build_phase和connect_phase各自该写哪些代码,顺序有什么讲究? - active 和 passive 两种模式差在哪,
is_active应该怎么传、在哪一层生效? - monitor 的 analysis_port 和 driver 的 seq_item_port,分别要暴露到 agent 的哪一层?
- 手搓一个协议 agent,最小可用骨架长什么样,常见坑有哪些?
一、为什么需要 agent 这一层封装
写一个接口的验证组件,最少要有三块逻辑:把事务拍成信号时序的 driver、把信号时序拼回事务的 monitor、给 driver 提供激励流的 sequencer。如果这三者直接平铺在 env 里,env 的 connect_phase 会被每个接口的六根连接线淹没,而且一旦第二个项目也要验同一种协议,你得从 env 的代码堆里把这三兄弟连同它们的连线一根根挑出来拷走——漏一根就是 elaboration 报错。
SV agent 封装的意义就在于此:agent 是「一个协议接口的完整验证前端」。对内,它拥有并组装 driver、monitor、sequencer,管它们的创建、连接和配置;对外,它只露出两类东西——一个 virtual interface(驱动和采样都靠它)和几个 TLM 端口(monitor 的事务出口、偶尔还有 driver 的请求口)。env 看到的是一个黑盒:给接口、给配置、connect 几个端口,这个接口的激励与观测能力就齐了。
上图是 SV agent 封装的标准视角:sequencer 到 driver 是 agent 内部唯一一条 TLM 连接(seq_item_port 那一对),driver 和 monitor 共享同一个 virtual interface 但职责正交——一个只写不读,一个只读不写;monitor 的 analysis_port 是 agent 对 env 的主要出口。这个结构对任何协议都成立,AXI、APB、自定义小包协议的区别只在 transaction 字段和 driver/monitor 的时序逻辑里,骨架一字不动。
封装带来的复用收益是实打实的:同一个 agent 类,block 级验证里挂在自己的 env 下,subsystem 级被例化三次,SoC 级被包进子 env 里再透传端口——调用方代码始终只有「create + connect」两行模式。这正是 分层 testbench 架构与复用 能成立的前提:没有 agent 这一层,复用粒度就退化到「拷整个 env」。
二、uvm_agent 基类:is_active 与三件套创建
uvm_agent 本身是个轻量基类,它预置的东西不多,但有一个关键字段:is_active,类型是 uvm_active_passive_enum,取值 UVM_ACTIVE 或 UVM_PASSIVE。语义很直白——active 模式的 agent 会驱动 DUT(创建 sequencer 和 driver),passive 模式只监听(只创建 monitor)。
| 组件 | active 模式 | passive 模式 | 职责 |
|---|---|---|---|
| sequencer | 创建 | 不创建 | 仲裁 sequence,把事务递给 driver |
| driver | 创建 | 不创建 | 事务拍成信号,驱动 interface |
| monitor | 创建 | 创建 | 采样信号拼成事务,analysis 广播 |
为什么要 passive 模式?两个高频场景。一是 SoC 级验证里,某个接口的激励由片上 CPU 或另一个模块真实产生,testbench 只需要 monitor 把总线上的事记下来比对,此时若还创建 driver,两个驱动源抢同一组信号就是仿真事故。二是复用已有 VIP 做纯观测,比如绑在内部探针点上的被动检查。一个设计良好的 agent,两种模式切换只改一个配置值,其他代码零改动。
判断逻辑写在 build_phase 里,标准写法是用 uvm_config_db 把 is_active 从 env 传进来,取不到时回落到默认值:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_sequencer sqr;
my_driver drv;
my_monitor mon;
my_agent_cfg cfg; // 配置对象
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(my_agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "agent 拿不到配置对象")
// monitor 两种模式都要
mon = my_monitor::type_id::create("mon", this);
if (cfg.is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver ::type_id::create("drv", this);
end
endfunction
endclass注意两点:一是创建走 factory 的 type_id::create 而不是 new,给 UVM factory 覆写 留门——项目后期想把 driver 换成错误注入版本,不用改 agent 一行代码。二是 monitor 的创建放在 if 外面,两种模式都要它;很多初学者把整段创建都包进 if (is_active),结果 passive 模式下 monitor 也没了,env 里 connect 一个 null 句柄,elaboration 阶段才炸。
uvm_agent 基类内部其实也维护了一个 is_active 成员,用 get_is_active() 读取,默认值可以通过 config_db 以 is_active 为键直接下发、由基类在 build_phase 里自取。两种做法(塞进自定义 cfg 对象 vs 用基类字段)在项目里只能选一种贯穿始终,混用会出现「env 改了基类字段、agent 却读 cfg 对象」的静默不一致。
三、connect_phase:内部接线与对外端口暴露
agent 的 connect_phase 干两类活:内部三件套之间的接线,以及把内部端口接到 agent 自己声明的对外端口上。
内部接线只有一条是必做的:driver 的 seq_item_port 连到 sequencer 的 seq_item_export。这一对端口的握手(get_next_item / item_done)在 sequence 与 driver/scoreboard 协作 里已经讲透,agent 层只需要把它们接上:
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (cfg.is_active == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
mon.ap.connect(ap); // 内部 port → 对外 export 透传
drv.vif = cfg.vif; // virtual interface 下发
mon.vif = cfg.vif;
endfunction对外端口暴露用的是 SystemVerilog TLM 通信 里的透传规则:agent 声明一个 uvm_analysis_port(analysis 端口特殊,它本身既能发起又能透传,所以直接用 port 即可),在 connect_phase 里把 mon.ap 接上来。env 层 connect 的就是 agt.ap,从头到尾不知道 monitor 叫什么名字。virtual interface 不走 TLM,它是句柄赋值:env 或 test 在 build_phase 之前把真实 interface 塞进 config_db,agent 取出来放到 cfg 里,connect_phase 再分发给 driver 和 monitor——两个组件共用同一条总线视角,时序语义才不会撕裂。
这张图把两条 connect 链画在一起,要点是它们发生在不同组件的 connect_phase 里:agent 内部那条(sqr-drv、mon-ap)写在 agent 的 connect_phase,agent 到 scoreboard 那条写在 env 的 connect_phase。UVM 的 phase 调度保证所有 build_phase 完成后才开始所有 connect_phase,所以跨层 connect 时目标组件一定已存在;反过来,在 build_phase 里 connect 是经典错误——对端可能还是 null。
passive 模式下这张图里 sqr、drv 两个框连同那条内部连线整体消失,ap 到 scoreboard 的链路原样保留。这也是为什么 monitor 的事务出口一定要透传到 agent 边界:env 不该关心 agent 内部是 active 还是 passive,它永远 connect agt.ap。
四、配置怎么进 agent:配置对象模式
一个可复用的 agent,行为差异全靠配置表达:is_active、virtual interface、协议参数(数据位宽、是否支持 burst、超时阈值)、monitor 的采样细节(是否收空包、覆盖率开关)。把这些散字段挨个用 config_db 传递,键名一多就是维护灾难;工程上通行做法是配置对象模式——定义一个 my_agent_cfg 普通类把所有字段打包,test/env 层 new 出来、填好字段、整包塞进 config_db,agent 一次 get 全拿到。
class my_agent_cfg extends uvm_object;
`uvm_object_utils(my_agent_cfg)
uvm_active_passive_enum is_active = UVM_ACTIVE;
virtual my_if vif;
bit has_coverage = 1;
int unsigned max_burst_len = 16;
int unsigned rsp_timeout = 1000;
endclass
// test 层:
function void build_phase(uvm_phase phase);
my_agent_cfg cfg = my_agent_cfg::type_id::create("cfg");
cfg.is_active = UVM_PASSIVE; // SoC 级只监听
cfg.max_burst_len = 32;
if (!uvm_config_db #(virtual my_if)::get(this, "", "vif", cfg.vif))
`uvm_fatal("NOVIF", "顶层没下发 virtual interface")
uvm_config_db #(my_agent_cfg)::set(this, "env.agt", "cfg", cfg);
endfunction配置对象模式有三个细节值得坚持。一是字段全部给合理默认值,新用例只覆盖差异项,定向用例的 build_phase 才能保持三五行的体量。二是 cfg 在 agent 的 build_phase 里 get,早于任何子组件创建——这样 is_active 才能控制三件套是否创建,拿到的 vif 也能在 connect_phase 顺利下发。三是 agent 应该把 cfg 句柄继续透传给子组件(driver、monitor 各自的 config_db set 或直接赋值),而不是由 agent 拆字段逐一下发;driver 想读 max_burst_len 时直接 cfg.max_burst_len,新增配置项不需要在 agent 里加转发代码。
config_db 的路径参数也有讲究:set 时写 "env.agt" 这种相对实例路径,比 "*" 通配精确得多。通配符在多实例场景(同一个 agent 例化三份挂在三条总线上)会把三份配置互相覆盖,查起来极痛苦——每个实例该拿哪份配置,用路径说清楚。关于 config_db 与 resource 机制的更多取舍,可对照 UVM 配置对象与 resource_db。
这张配置流向图的关键在「共享句柄」:test、agent、driver、monitor 手里是同一个 cfg 对象,test 填一次,处处可见。代价是运行期不要再去改它——cfg 本质是 build 时刻的快照,run 阶段改字段,哪些组件已经读过、哪些还没读,就成了靠运气的事。需要运行期可调的行为(比如错误注入开关),应该另走寄存器模型或显式 API,而不是回改 cfg。
五、active/passive 运行形态对照
把两种模式的运行行为并排看,差异比「多两个组件」更深一层。
active 模式下,agent 是激励闭环的一部分:test 启动 sequence,sequence 挂在 agent 内部的 sequencer 上(通过 env.agt.sqr 这个句柄路径引用),事务流经 driver 变成信号,DUT 响应,monitor 采回事务广播出去。passive 模式下,激励闭环根本不存在——DUT 的信号来自别的真实主设备,agent 里只有 monitor 在转,sequencer 句柄是 null,如果哪个 sequence 不小心 start 到 env.agt.sqr 上,空句柄解引用当场 fatal。
对照图读,两条链路下 monitor 到 env 的出口长得一模一样——这正是模式切换不打扰上层的意义。写 env 和 scoreboard 代码时按「永远有个 analysis_port 会广播事务」来设计,agent 是 active 还是 passive 只是 test 层一行配置的事。
工程实践中还有两个常用变体。一是同一接口两 agent:主口挂 active agent 发激励,从口挂 passive agent 专门观测响应,block 级存储类 IP 常这么搭。二是block 级 active、系统级 passive 的复用路径:同一个 agent 类,block 验证时 UVM_ACTIVE 全速打激励,集成到 SoC 环境时切 UVM_PASSIVE 只留观测——block 级攒下的 monitor 协议检查(参考 monitor 与协议检查器)和覆盖率模型在系统级原样生效,一行验证代码不浪费。
六、完整骨架与常见坑
把前面五节拼起来,一个手搓 agent 的最小可用骨架如下,可直接作为新协议 agent 的起点:
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_agent_cfg cfg;
my_sequencer sqr;
my_driver drv;
my_monitor mon;
uvm_analysis_port #(my_txn) ap; // 对外事务出口
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this); // 端口不进 factory,直接 new
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db #(my_agent_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "missing agent cfg")
mon = my_monitor::type_id::create("mon", this);
mon.cfg = cfg;
if (cfg.is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver ::type_id::create("drv", this);
drv.cfg = cfg;
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (cfg.is_active == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
mon.ap.connect(ap);
endfunction
endclass配套的环境侧用法三行:test 里填 cfg 并 set,env 里 agt = my_agent::type_id::create(...),agt.ap.connect(sb.analysis_export)。sequence 启动时写 seq.start(env.agt.sqr)。
常见坑按踩坑频率排:
- passive 下 connect null:
drv.seq_item_port.connect(...)忘了包在if (is_active)里,passive 模式 drv 是 null,elaboration 直接炸。反之把 monitor 创建包进 if 也一样致命。 - 端口在 build_phase 里 new 到一半:
uvm_analysis_port不进 factory,惯例是在new里创建;放进 build_phase 也能跑,但任何想在 build_phase 早期 connect 它的上层代码都会拿到 null。 - vif 忘记下发:agent 拿到了 cfg 却没人把 vif 交给 driver/monitor,run 阶段对 null 句柄解引用,报错位置离根因隔着三层组件。骨架里把 vif 装进 cfg 一起传就是为了收口这条路径。
- 一个 agent 里塞多个协议:图省事把两条相关总线的三件套塞进同一个 agent,结果 is_active 无法分别控制、端口命名越来越长。一个 agent 对应一个协议接口实例,这是封装粒度的底线。
- 绕过 agent 直连内部组件:env 里写
agt.mon.ap.connect(...),等于把 agent 的内部结构钉死在 env 代码里,日后重命名 monitor 就要改所有环境。所有跨层连接一律走 agent 对外端口。 - cfg 运行期被改写:某个用例在 run 阶段改
cfg.max_burst_len想「动态调参」,driver 下一笔事务读到新值、monitor 还按旧值检查,比对结果灵异。运行期行为用专门机制,别碰 cfg。
这一篇在知识体系里的位置
- 向上承接:UVM 组件与 phase 给出组件骨架与 phase 时序,本篇把它落到 agent 这个具体复用单元上。
- 通信机制:SystemVerilog TLM 通信 讲 port/export/imp 与 analysis 通路,agent 的对外端口暴露就是这些规则的直接应用。
- 激励侧深入:sequence 与 driver/scoreboard 协作 展开 sequencer-driver 握手细节,本篇只负责把它们接上线。
- 横向相关:monitor 与协议检查器 讲 monitor 内部实现,UVM 配置对象与 resource_db 展开配置机制选型。
- 落地延伸:分层 testbench 架构与复用 展示多个 agent 如何在 env/subenv 里组织成可复用环境。
总结
SV agent 封装把一个协议接口的 driver、monitor、sequencer 装进一个 uvm_agent:build_phase 按配置对象里的 is_active 决定三件套创建范围,connect_phase 完成内部 seq_item 接线并把 monitor 的 analysis_port 透传为对外唯一出口,virtual interface 随配置对象统一下发——active 驱动、passive 监听两种形态同一套代码,env 只看见端口与配置,这就是跨项目复用的最小完整单元。