UVM active passive agent:is_active 配置、组件裁剪与复用
更新时间:2026-09-28。agent 的封装动机——为什么 driver、monitor、sequencer 要打包成一个壳——在 SystemVerilog agent 封装模式 里已经讲过;这一篇只回答壳的一个分支问题:同一个 agent,什么时候要它「干活」,什么时候只要它「看着」。控制下发本身依赖 UVM config_db / resource_db:配置与资源的存取,两篇建议连读。
本文要回答的问题
- UVM active passive agent 到底差在哪?只是少两个组件,还是有更深的语义差别?
is_active这个开关在什么阶段生效,怎么通过 config_db 或配置对象下发?- 一份 agent 代码如何同时服务「主驱 DUT」和「旁路观测」两种角色,裁剪边界画在哪里?
一、两种模式的差别:驱动 vs 观测
UVM 里的 agent 默认包三块:uvm_sequencer(事务源)、uvm_driver(把事务打到接口上)、uvm_monitor(从接口上采事务)。所谓 active / passive 不是两个类,而是同一个 agent 类在例化时的两种形态:
- active(主动):三块全建。sequence 在 sequencer 上跑,driver 把事务转成信号翻转驱动 DUT,monitor 照常采样。agent 是「事务的生产者 + 观察者」。
- passive(被动):只建 monitor,sequencer 和 driver 干脆不例化。agent 只趴在接口上看,不产生任何驱动。它是「纯粹的观察者」。
什么时候用 passive?最典型的场景是 SoC 集成验证:子系统级验证时,某接口的 agent 是 active 的(env 自己造激励);集到 SoC 后,这条接口的激励改由真实上游模块(比如 CPU 或另一个子系统)产生,agent 再驱动就会和真实主设备抢总线。这时把同一个 agent 切成 passive,monitor 继续采事务喂 scoreboard 和覆盖率,一行驱动代码都不用删——因为它根本没被例化。
| 对比项 | active agent | passive agent |
|---|---|---|
| sequencer | 例化 | 不例化 |
| driver | 例化并连 TLM | 不例化 |
| monitor | 例化 | 例化 |
| 对 DUT 的影响 | 驱动信号 | 零影响,只读 |
| 典型场景 | block/subsystem 级自造激励 | SoC 集成、后接在真实主设备下游观测 |
| sequence 能否启动 | 能 | 不能(没有 sequencer) |
这张表要记的是中间两行:模式只裁剪「激励通路」,从不裁剪「观测通路」。monitor 永远在,因为 scoreboard、覆盖率、协议检查器全都挂在它的 analysis port 上。
二、is_active 怎么生效:build 阶段的一次分支
标准写法是在 agent 的 build_phase 里读 is_active,按值决定是否例化激励通路:
class axi_agent extends uvm_agent;
`uvm_component_utils(axi_agent)
uvm_active_passive_enum is_active = UVM_ACTIVE; // 默认值
axi_sequencer sqr;
axi_driver drv;
axi_monitor mon;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 从 config_db 取上层下发的模式;没下发就用默认值
void'(uvm_config_db#(uvm_active_passive_enum)::get(
this, "", "is_active", is_active));
if (is_active == UVM_ACTIVE) begin
sqr = axi_sequencer::type_id::create("sqr", this);
drv = axi_driver ::type_id::create("drv", this);
end
mon = axi_monitor::type_id::create("mon", this); // 两种模式都建
endfunction
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
if (is_active == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass上层(env 或 test)下发开关:
uvm_config_db#(uvm_active_passive_enum)::set(
this, "env.axi_agt", "is_active", UVM_PASSIVE);时序上的硬约束和虚接口下发一样:set 必须早于 agent 的 build_phase,所以放在 test/env 的 build 里、且 env 先于 agent 执行 build,天然满足。connect_phase 里同样要按 is_active 分支——passive 模式下 sqr/drv 句柄是 null,去 connect 会直接空指针崩。
另一个常见变体是不用裸枚举,而是把 is_active 收进 agent 的配置对象(axi_agent_cfg)里,连同位宽、时延参数一起下发:
class axi_agent_cfg extends uvm_object;
uvm_active_passive_enum is_active = UVM_ACTIVE;
int unsigned data_width = 32;
// ...
endclass
// test 层:
cfg = axi_agent_cfg::type_id::create("cfg");
cfg.is_active = UVM_PASSIVE;
uvm_config_db#(axi_agent_cfg)::set(this, "env.axi_agt", "cfg", cfg);项目大了以后推荐后者:config_db 里少挂零散的键,模式切换和其它配置一起走一个对象,改配置不用记一堆字符串键名。注意 uvm_active_passive_enum 是 UVM 预定义枚举,取值只有 UVM_PASSIVE(0) 和 UVM_ACTIVE(1),不要自己再造一个布尔量 is_passive 和枚举并存——两套真值源一旦不同步,build 走枚举、connect 走布尔,模式就劈叉了。全环境只认一个字段,其它地方一律引用它。
三、结构对照:一张图看清裁掉了什么

对照左右两边看:passive 侧不是「半个 agent」,而是「去掉激励通路的 agent」。monitor → scoreboard 的观测链两边完全对称,这正是复用的核心——SoC 集成时把 agent 切到 passive,subsystem 级攒下的 scoreboard 检查逻辑和 covergroup 原封不动继续工作,验证资产零损失。
四、复用案例:从 block 级到 SoC 级的同代码迁徙
讲一个典型的两级复用流程,看 active/passive 切换怎么省工。
block 级(UART 子系统自验):UART DUT 的 RX/TX 各挂一个 UART agent,两边都是 active。env 造随机激励从 RX 打进去,TX 侧 agent 也 active(回环对打),scoreboard 比对收发事务,covergroup 收波特率、校验位、帧错的覆盖率。
SoC 级(UART 集成进芯片):此时 UART 的激励来自真实的 CPU 总线写,UART agent 的 TX 对外引脚接给了 pad 环或另一个真实模块。env 里把同一个 UART agent 类重新例化,test 层只做一件事——把 is_active 下发为 UVM_PASSIVE:
// soc_base_test::build_phase
uvm_config_db#(uvm_active_passive_enum)::set(
this, "env.uart_agt", "is_active", UVM_PASSIVE);切换后,sequencer/driver 不例化,不会和真实主设备抢线;monitor 照旧采事务,block 级写好的 scoreboard 比对逻辑、covergroup、协议检查器全部原样复用。真正的新增工作只剩把 monitor 的 analysis port 重新连到 SoC 级 scoreboard 上。这就是「观测资产跨级复用」的完整闭环。
还有一种 hybrid 用法值得知道:reactive slave。比如 AXI slave agent 看起来是「响应方」,但它要驱动 R/B 通道的响应数据,所以本质上是 active 的——sequencer 上跑一个永远等待请求的 response sequence。它的响应路径和主动 agent 正好反过来:主动 agent 是 sequence 自发产事务往下推;reactive slave 是 monitor 先采到 DUT 发来的请求,经 analysis port 或 request export 递回 sequencer,挂起的 response sequence 收到请求后才生成响应事务交给 driver 打出去。整条链路上 monitor、sequencer、driver 一个都少不了,把它误配成 passive 后,链路断在「没有 sequencer」这一环,DUT 发出的读请求永远等不到数据,仿真挂死。判据:只要 agent 的 driver 会碰任何方向的信号,它就是 active,和它在协议里叫 master 还是 slave 无关。
在回归(regression)矩阵层面,is_active 通常不作为随机维度,而是作为测试层级维度:同一份 env 编一次,block 级回归跑一拨全 active 的 test,SoC 集成回归跑另一拨把部分 agent 切成 passive 的 test,两拨用不同的 test 类名区分开。回归脚本按 test 名分发到对应层级即可,不需要为 passive 场景单开一份 env 代码库。这样做的好处是模式切换的覆盖面天然等于 test 类的覆盖面——每新增一个集成场景,只需新写一个 test 改几个 is_active 下发,agent 和 env 一行不动;坏处是 test 数量会涨,建议在 test 命名里带上层级前缀(比如 blk_* 与 soc_*),回归报表按前缀聚合,哪一层出问题一眼能看出来。
五、组件裁剪的边界与常见坑
裁剪边界画在哪里,有几条经验法则:
- monitor 永远进「不裁」集合。它连 analysis port,是观测资产的载体,任何模式都要建。
- sequencer 和 driver 必须同生同灭。只建 driver 不建 sequencer 没有意义(没人喂事务);只建 sequencer 不建 driver 是无用开销。
- monitor 内部不要假设 driver 存在。有些写法喜欢让 monitor 从 driver 拿配置或状态,passive 模式下这就是个定时 null 指针。
常见坑按「现象 → 根因 → 处理」排成一张表:
| 现象 | 根因 | 处理 |
|---|---|---|
passive 下 seq.start() 报空句柄 | sqr 未例化,test 仍起激励 | sequence 启动由配置门控;passive 模式 test 不起该 agent 的激励 |
| connect_phase 崩溃 | passive 下 connect drv.seq_item_port | connect 也按 is_active 分支,与 build 对称 |
| SoC 上波形一片 X,agent 抢总线 | set/get 键名打错(如 "isActive"),静默回退默认 UVM_ACTIVE | get 返回值加断言,至少 debug 构建不留 void'() |
| 同一接口两侧都驱动 | master/slave agent 都设 active | 同一物理链路只许一侧驱动;另一侧 passive 或 reactive slave |
| DUT 读请求挂死无响应 | reactive slave agent 被误切 passive | 响应方也要驱动信号,按 active 处理 |
| 切模式后覆盖率掉零 | 把 monitor 也一并裁掉了 | monitor 永在「不裁」集合,观测链路不许动 |
| build 走枚举、connect 走布尔,行为劈叉 | 自造 is_passive 布尔与枚举并存 | 全环境单一真值源,只用 is_active 枚举 |
再补一张图,把「配置从 test 流向 agent、裁剪在 build 发生」这条顺序画清楚:

这张图和前面的结构对照图是互补关系:对照图看「静态结果」(两种模式各长什么样),这张看「动态顺序」(谁在什么阶段决定了这个长相)。记住一点——模式的决定权在 test,执行在 agent 的 build,消费在 connect 和 run,三个阶段靠同一个 is_active 值对齐,任何一环读到不一致的真值都会变成上面表里的某一坑。
这一篇在知识体系里的位置
- 封装动机与 agent 内部职责划分见 SystemVerilog agent 封装模式。
is_active的下发通道与 set/get 时序细节见 UVM config_db / resource_db:配置与资源的存取。- 与模式切换配套的「句柄按配置重指」手法见 SystemVerilog 虚接口:动态赋值、重配与环境复用;UVM 组件与 phase 的入门框架见 UVM 组件与 phase 机制。
总结
UVM active passive agent 的差别只有一个 is_active 开关:active 例化 sequencer+driver+monitor 主动驱动 DUT,passive 只留 monitor 纯观测;开关在 build 阶段经 config_db 或配置对象下发,裁剪边界永远是「激励通路可裁、观测通路不动」——守住 monitor 必建、sequencer/driver 同生同灭、connect 按模式分支这三条,同一份 agent 代码就能从 block 级一路复用到 SoC 集成。