UVM Agent 组件职责:Driver、Monitor、Sequencer 到底谁干什么
刚接触 UVM(Universal Verification Methodology,通用验证方法学)的人,打开任何一个参考环境都会撞见同一个三件套:driver、monitor、sequencer,外面再包一层 agent 壳。问题是教科书把它们一口气全摆出来,却不先说清边界——结果最常见的初学事故就是「把协议时序写进了 monitor」「在 driver 里做结果比对」这类角色错位。这篇是 UVM 验证平台架构导读 的下钻篇:那一篇讲整个平台分几层,这一篇只聚焦 agent 内部,把「谁产生数据、谁驱动、谁观察、谁仲裁」四个问题逐个钉死。更细的仲裁算法和 item 握手细节在 uvm-sequence-sequencer-arbitration 与 uvm-driver-sequence-item-flow 里单独展开,本文不重复。
本文要回答的问题
- driver、monitor、sequencer 各自的职责边界画在哪里,一句话怎么记住?
- 它们之间用什么端口连、数据沿哪条路径流动?
- sequence、sequencer、driver 之间的一次完整「取牌—打牌」是怎么握手的?
- active agent 和 passive agent 差在哪,什么时候把 driver 和 sequencer 关掉?
- 哪些活儿不属于这三件套(scoreboard、coverage 该放哪)?
一、为什么一个 Agent 要拆成三件
先用一个类比把分工立住。把验证平台想成一家餐厅的出餐口:
- sequencer(序列器)是调度台:它不动手做菜,只负责从一堆「订单」(sequence)里按仲裁规则挑出下一个要做的菜,把订单(sequence item)递给厨师。
- driver(驱动器)是厨师:拿到抽象的订单(一笔事务,比如「向地址 0x1000 写 32 位数据」),把它翻译成灶台动作——对 DUT(Design Under Test,被测设计)的接口信号一拍一拍地拉高压低,直到这笔事务真正在总线上发生。
- monitor(监视器)是站在出餐口的品控:它绝不碰锅铲,只盯着总线信号看,把看到的波形重新拼回抽象事务,广播给任何关心的人(scoreboard、coverage 收集器)。
拆开的原因很实际:三者的变化频率完全不同。协议时序变了,只改 driver;想换一个随机激励策略,只换 sequence,driver 一行不动;想加一个覆盖率收集器,monitor 的广播口天然支持多订阅者。如果把它们揉成一个巨型组件,任何一处改动都会牵连另外两方。
读这张卡抓住一条主线:同一个 sequence item 会被三个组件各看一次,但视角完全不同。sequencer 看到的是「待派发的数据包」;driver 看到的是「待翻译的时序说明书」;monitor 看到的是「事后要重建的事实记录」。颜色也对应这个语义:紫是调度与规划,橙是动手执行,绿是旁路支撑。下文每一节就是把卡上的一栏展开。
二、组件之间怎么连:端口与数据流
知道了各自干什么,下一步是看它们在一个 agent 内部怎么接线。UVM 里组件之间不走全局变量,全走 TLM(Transaction Level Modeling,事务级建模)端口——一种类型化的「插座配插头」机制,编译期就能抓出接错线。
读这张图抓住两条颜色线:紫色—橙色—红色是激励前向路径(sequence 出题、sequencer 派发、driver 翻译成信号、打进 DUT),红色—绿色—灰色是观测回流路径(DUT 引脚被 monitor 采样、重建成事务、广播给订阅者)。注意 monitor 与 driver 之间没有直连线——这是边界的关键:观测结果绝不反馈给激励源,否则平台就有了隐藏耦合,激励会随着观测结果「自适应」,那是 scoreboard 层面才允许发生的闭环。
连线上标注的两类端口值得记住,因为实际代码里所有的连接都是它们的实例:
| 连接 | 端口类型 | 语义 |
|---|---|---|
| sequencer → driver | seq_item_pull_port / seq_item_pull_export | 点对点,一对一绑定,阻塞式取件 |
| monitor → 订阅者 | uvm_analysis_port / uvm_analysis_imp | 广播,一对多,write() 非阻塞 |
这两类端口的语义差异也暗示了组件角色:driver 和 sequencer 是强耦合的一对(一个 driver 只能挂一个 sequencer,反之亦然),因为驱动动作必须有唯一的发令者——两个 sequencer 同时往一个 driver 塞件,等价于两个调度台指挥同一个厨师,总线时序必然打架;monitor 和它的听众是松耦合的扇出,因为「看到了什么」这个事实对多少人广播都不影响事实本身,加一个订阅者不需要 monitor 改任何代码。TLM 端口自身的连接语义细节(port/export/imp 三级)在 TLM 端口与 export 机制 里有专篇,analysis 广播的多订阅玩法见 uvm-analysis-port-multi-subscriber。
还有一个图上没画但工程里天天用的细节:driver 和 monitor 拿到的都是同一条 virtual interface(虚接口,指向真实 SystemVerilog interface 的类句柄)。区别在于 monitor 只对它做读采样,driver 只对它做写驱动——同一个物理接口、两种访问方向,这也是它们能在 active/passive 之间平滑切换的物理基础。
三、一次「取牌—打牌」的完整握手
driver 和 sequencer 之间不是简单的「sequencer 推一个 item 过去」,而是一套带流控的拉模式握手:driver 主动要、sequencer 才给、driver 做完了才交还。这套握手保证了 driver 永远不会被塞爆,也保证了一个 item 在彻底打完之前不会被丢弃。

读这张时序图抓住两个「会差点」:第一个是 finish_item 与 get_next_item 的会合——sequence 侧发完件会阻塞在 finish_item,driver 侧取件会阻塞在 get_next_item,两边都到齐了事务才真正易手,这个设计让激励生成速度天然被 DUT 消化速度限流;第二个是 item_done 之前的整段时间里,sequencer 把这个 item 视为「在途」,别的 sequence 即使拿到仲裁授权,它的 item 也得排队等 driver 空闲。新手最常写的 bug 是漏掉 item_done——sequence 永远阻塞在 finish_item,仿真挂死且没有报错信息,症状与 uvm objection 不撤导致的挂死 很像,排查时先查这条配对是否完整。item 生命周期更细的拆解(含 get_response 的响应队列)见 uvm-driver-sequence-item-flow。
还要知道这套握手存在一个变体:get_next_item / item_done 是阻塞式单件流水,driver 同一时刻只在处理一笔事务。对流水线型协议(发出请求后不等响应就可以发下一笔,比如 AXI 的 outstanding 交易),可以改用 try_next_item 加响应队列的非阻塞玩法:driver 先把请求打上总线,响应回来后再通过 put_response 回填给 sequence。握手语义变了,但「sequencer 派发、driver 驱动」的分工没变——需要乱序响应处理能力的场景见 uvm-pipelined-driver-ooo-response。
四、Sequencer 的仲裁:多个 sequence 抢一个 driver
sequencer 名字里虽然有「sequence」,但它不产生数据——数据全是 sequence 对象随机化出来的。sequencer 唯一不可替代的活儿是仲裁:一个 agent 上可能同时跑着主激励 sequence、错误注入 sequence、背景流量 sequence,它们都要往同一个 driver 发件,谁先谁后由 sequencer 的仲裁算法决定。
| 仲裁模式 | 语义 | 典型用途 |
|---|---|---|
SEQ_ARB_FIFO(默认) | 先来先服务 | 大多数单激励场景,行为最好预测 |
SEQ_ARB_WEIGHTED | 按优先级加权 | 主流量 + 偶发错误注入混合 |
SEQ_ARB_RANDOM | 随机挑 | 想打散顺序暴露时序相关 bug |
SEQ_ARB_STRICT_FIFO | 高优先级绝对优先,同级 FIFO | 复位/中断类高优先级激励插队 |
SEQ_ARB_USER | 用户自定义函数 | 特殊协议需求,极少用 |
仲裁只发生在「多个 sequence 同时有件待发」的时刻;单 sequence 场景下 sequencer 退化成一个透明的传递管道。需要独占总线做一段原子操作(比如连发 8 笔不能被插队的配置写)时,sequence 可以用 lock()/grab() 把 sequencer 临时占住,这套独占机制与仲裁的交互在 uvm-sequencer-lock-grab-exclusive 里单独讲。还有一个初学高频混淆点:sequence 里用的 m_sequencer 和 virtual sequence 里的 p_sequencer 不是同一个东西,差别见 m_sequencer 与 p_sequencer 的区别。
五、Active 与 Passive:同一只 Agent 的两种开法
理解了三个组件的职责后,agent 外壳存在的意义就清楚了:它是一个开关与装配清单。UVM 约定 agent 有一个 is_active 配置位:
| 模式 | sequencer | driver | monitor | 用在哪 |
|---|---|---|---|---|
| active | 建 | 建 | 建 | 本方需要主动激励 DUT 的接口 |
| passive | 不建 | 不建 | 建 | 只观测:系统级环境里复用别处的激励 |
注意表里 monitor 永远在列。这正是 monitor 被设计成「只读、不驱动」的回报——它天然可以在任何模式下复用,一个 block 级 active agent 原封不动搬到子系统级切成 passive,monitor 广播出去的数据流一模一样,scoreboard 和 coverage 无需任何改动。这个模式切换的完整实现(含 config_db 怎么把 is_active 传下去)见 UVM active/passive agent 切换,agent 封装作为复用模式更广义的设计讨论见 sv-agent-encapsulation-pattern。
判断「这个接口该配 active 还是 passive」的口诀只有一句:这个接口的信号由本环境产生,还是由别人产生? 本环境产生就 active(需要 driver 把信号打进去),别人产生(比如 SoC 级环境里另一个 master 已经在驱动这条总线)就 passive(只留 monitor 做观测和检查)。
六、边界之外:哪些活儿不归三件套
把边界讲完整,还要说清楚三件套不干什么。最常见的角色错位有三类:
- 结果比对不归 monitor 管。monitor 只负责「如实重建事实」,比对「期望值与实际值」是 scoreboard 的活(实现上是一个带 analysis imp 的订阅者,参考 uvm-scoreboard-impl-queue)。在 monitor 里写
if (actual != expected) uvm_error等于把品控升级成了判官,换一套期望值就得改 monitor,复用性立刻报废。 - 协议合法性检查可以放 monitor,但不强制。一些团队把「信号组合非法就报警告」放在 monitor 里,这是被接受的实践(monitor 是唯一逐拍看全信号的地方);但端到端的「写出去的数据读回来对不对」绝不在 monitor。更系统化的协议断言通常走 SVA 绑定,见 SVA 断言实践。
- 覆盖率采样不在 driver。功能覆盖率收集器是一个 uvm_subscriber,挂在 monitor 的 analysis port 上被动采样,见 uvm-subscriber-coverage-collector。driver 管的是「把事做成」,不关心「做到了哪一类」。
对应到代码骨架,一个干净的 agent 大致长这样(示意,非完整可编译版):
class my_agent extends uvm_agent;
`uvm_component_utils(my_agent)
my_sequencer sqr; // 仅 active 模式创建
my_driver drv; // 仅 active 模式创建
my_monitor mon; // 两种模式都创建
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = my_monitor::type_id::create("mon", this);
if (is_active == UVM_ACTIVE) begin
sqr = my_sequencer::type_id::create("sqr", this);
drv = my_driver ::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
if (is_active == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass骨架里两个值得多看一眼的点:monitor 的创建无条件执行,driver/sequencer 的创建被 is_active 守卫——这就是上一节那张表的代码形态;connect_phase 里只有一条连线,因为 monitor 的 analysis port 是向外部(env 层的 scoreboard/coverage)开放的,agent 内部不需要连它。build 与 connect 分属两个 phase 的原因(为什么不能在 build 里顺手连线)在 UVM 组件与 phase 机制 里有完整解释:build 自顶向下创建组件,connect 自底向上接线,两层顺序保证了连线时所有端口都已存在。
最后再给一个自查清单,写完或 review 一个 agent 时逐条过一遍:driver 里有没有出现记分/比对的 uvm_error?monitor 里有没有对信号赋值的语句?sequence 里有没有 item_done 的配对遗漏?monitor 的广播口是不是 analysis port 而不是直接句柄回调?四条全过,职责边界就是干净的。
这一篇在知识体系里的位置
本文是 UVM 验证平台架构导读 的组件级下钻,属于 chip-design/beginner 入门线;前置基础是 SystemVerilog 测试平台基础 与 UVM 组件与 phase 机制。顺着本文的边界往深走,四条互链支线分别是:握手细节 uvm-driver-sequence-item-flow、仲裁与独占 uvm-sequence-sequencer-arbitration、模式切换 uvm-agent-active-passive、以及 sequence 与 driver 在完整环境里的串接 uvm-sequence-driver-scoreboard。
一句话总结
UVM Agent 组件职责一句话就够:sequence 产生数据、sequencer 只仲裁派发、driver 把事务翻译成信号时序打进 DUT、monitor 只读信号重建事务并广播——记住「发牌的不碰信号、打牌的不看结果、看牌的绝不动手」,agent 的边界就不会画错,active/passive 不过是把前两者按开关关掉而已。