UVM domain phase jump:多 domain 独立相位、jump 跳转语义与跳回坑全解
更新时间:2026-09-28。在 UVM 组件与 phase 里,我们认识了 build/connect/run 这一串标准相位;在 UVM phase 与 objection 结束测试 里,我们学会了用 objection 拖住单个相位。默认情况下所有组件都挂在同一个
uvm_domain(即 common domain)里同步推进,但多时钟、多电源域、多复位源的 DUT 往往要求「一部分组件跑自己的相位节奏」。这一篇专门拆开 domain 与 phase jump:domain 怎么分、相位怎么跳、什么时候跳会把你带进坑里。
本文要回答的问题
- domain 到底是什么,它和 component 的相位推进有什么关系?
- 为什么一个 testbench 里可能需要多个 domain 并行跑相位?
uvm_phase::jump()的语义是什么,forward 跳和 backward 跳差别在哪?- 哪些相位之间允许跳,跳回 build_phase 会发生什么?
- 复位重激励场景里,怎么用 domain + jump 优雅地重启一段激励而不污染全局 objection?
一、domain:相位推进的分组单位
UVM 的相位调度器按 domain 分组推进。一个 domain 就是一组共享同一相位序列的 component 集合:同一个 domain 里的所有组件,build_phase 一起进、run_phase 一起跑、extract_phase 一起收。不同 domain 之间相位完全独立——A domain 可以还在 run,B domain 已经走到 check,互不等待。
仿真启动时,UVM 只创建了一个默认 domain,叫 uvm_domain(也常叫 common domain),里面包含 build/connect/end_of_elaboration/start_of_simulation 这一串 build-time phase,加上 reset/configure/main/shutdown 这 12 个 runtime phase,再往后是 extract/check/report/final。所有没显式换 domain 的组件,都默认挂在这一个域里。

上图是绝大多数 testbench 的默认形态:一个 domain 装下所有组件。这个形态对单时钟、单复位、激励一条主线的 DUT 足够用。问题出在「两条主线节奏不同」的场合——比如一个 SoC 里 CPU 域和 IO 域各有自己的 reset 序列,CPU 复位时 IO 侧不该跟着回 build;又比如低功耗验证里某个电源域下电再上电,对应 agent 要重走 configure,而其他域保持 main 不动。
新增 domain 的写法很直接:
uvm_domain io_domain;
initial begin
io_domain = new("io_domain");
// 把 io_agent 整棵子树挪进新 domain
uvm_top.find("*.env.io_agent").set_domain(io_domain);
endset_domain 会把该组件及其所有子孙一起搬走。搬家之后,io_agent 那一支的相位时序就不再跟 common domain 走,两个调度器各自推进。
二、domain 的相位序列:独立但同构
每个 domain 自带一份完整的相位链表。新建 uvm_domain 时,UVM 会克隆一套标准相位给它——build-time 那串、runtime 那 12 个、收尾那串,一份不少。两个 domain 的区别只在推进时机,不在相位内容。

这张图想强调两点。第一,两个 domain 的 build_phase 是并行起跑的:仿真 0 时刻,common 和 io 两个 domain 都从 build 开始往下走,io_agent 的 build 并不会因为换了 domain 而推迟。第二,runtime 相位之间的同步靠 objection 和 jump,不靠「domain 之间对齐」——UVM 从不等待两个 domain 到达同一个相位。
这意味着跨 domain 的数据通路(比如 common domain 的 scoreboard 订阅 io_domain 的 monitor)不会自动获得相位对齐保证:io 侧可能已经在 extract,common 侧还在 main 里 get。这类通路的收尾要靠 UVM phase 与 objection 结束测试 里讲的 drain_time 与显式握手,不能指望相位边界。
还有一个容易忽略的点:uvm_domain::get_common_domain() 拿到的是那个唯一的默认域。如果你想让某棵子树回到默认节奏,调用 set_domain(uvm_domain::get_common_domain()) 即可,不必保留原引用。
三、domain 的常见划分维度
什么时候值得新开一个 domain?工程上有三个主流划分维度,按 DUT 结构对号入座即可。
| 划分维度 | 典型信号 | 切分收益 |
|---|---|---|
| 按接口/协议 | 一个 env 挂了 AXI、UART、JTAG 三类 agent,各自激励独立 | 某接口出错 jump 回 reset 不影响其他接口跑长激励 |
| 按电源域 | UPF 里有可独立上下电的 power domain | 下电域的 agent 可在 reset/configure 间往返,常开域连续采样覆盖率 |
| 按复位域 | DUT 有多个异步 reset 源,各自可独立触发 | 局部复位只重启对应 agent 的相位,其他复位域保持 main |
反面情况也要认清:如果所有 agent 共享同一个时钟复位、激励是一条强耦合的主线(比如 driver 发一笔、scoreboard 必须马上比一笔才能决定下一笔),切 domain 只会带来额外的同步负担,收益为零。切分的判据一句话——这棵子树的相位推进是否需要独立被 jump、被 objection、被结束?三个答案有一个是「是」,就值得切。
另外,domain 切分粒度按 agent 还是按 env 的子树,取决于 jump 时需要重启的最小单元。低功耗场景里常以「电源域对应的 agent 簇」为单位:一个 power domain 下面挂的几个接口 agent 一起进新 domain,上电时整簇一起回 configure。粒度切太细(每个 monitor 一个 domain)会让 objection 管理和同步逻辑爆炸,切太粗则退化成 common domain。
四、phase jump:在 runtime 相位间跳转
uvm_phase::jump(target) 是当前 domain 内「强制改变推进方向」的接口。调用之后,当前 domain 的调度器会放弃原先「一路向后」的计划,转而跳到 target 相位继续执行。jump 只对 runtime phase(run_phase 及其 12 个兄弟)有效,对 build/connect 这类 function 相位不允许跳——原因后面讲。
jump 分两个方向:
- forward jump:目标是当前相位之后的相位。比如 main_phase 里跳去 shutdown_phase,相当于「提前收尾」。
- backward jump:目标是当前相位之前的相位。比如 main_phase 里跳回 reset_phase,相当于「再复位一轮」。

图上两条虚线是 jump 的两类典型用法。backward jump 到 reset 是低功耗和错误注入验证的常规武器:跑到一半发现需要重复位 DUT,不必结束仿真,直接跳回 reset_phase 重走一遍复位序列即可。forward jump 到 shutdown 则用于「检测到致命错误,跳过后续激励,优雅收尾」。
jump 的关键语义有三条。第一,jump 只影响当前 domain:common domain 的 main 跳回 reset,io_domain 完全无感,继续按自己的节奏走。这正是 domain 分离的价值——你可以只重启出问题的那一支。第二,jump 会清掉当前相位的所有 objection:调度器离开当前相位时,不再等还没落下的 objection,所有挂在当前相位的 task 会被 kill(等效于 phase 结束时 kill 掉未完成任务)。第三,跳到目标相位后从相位入口重新开始:跳回 reset_phase,意味着 reset_phase 的所有 task(包括挂在它上面的 sequence)会被重新启动,phase_started 回调会再触发一次。
从调度器视角看,jump 做的事情是改写「下一个相位是谁」这根指针。正常情况下每个相位的后继在 domain 建立时就排好了(pre_reset 的后面是 reset,reset 后面是 post_reset),jump 相当于把当前相位的后继临时改写成 target,同时把 target 原本的前驱关系搁置——目标相位执行完之后,仍然按它自己固有的后继继续往下走,而不是跳回 jump 的发起点。也就是说 jump 是「单程插入」,不是「调用-返回」。跳回 reset 重跑一轮后,相位流会再次经过 configure、main 一路向前,不会记着「我当初是从 main 跳过来的」。理解这一点,后面「激励被重放两遍」的现象就不难解释了:那是相位流自然走到 main 的结果,不是 bug。
五、jump 的合法目标:runtime only
并非所有相位都能作为 jump 目标。UVM 的硬约束是:源和目标都必须是 runtime phase,也就是 uvm_task_phase 的子类(run、pre_reset、reset、post_reset、pre_configure、configure、post_configure、pre_main、main、post_main、pre_shutdown、shutdown、post_shutdown 这 13 个)。
| 相位类别 | 例子 | 能否 jump 进入 | 能否 jump 离开 |
|---|---|---|---|
| build-time(function) | build、connect、end_of_elaboration、start_of_simulation | 不能 | 不能 |
| runtime(task) | run、reset、configure、main、shutdown 等 13 个 | 能 | 能 |
| 收尾(function) | extract、check、report、final | 不能 | 不能 |
为什么 function 相位不能跳?因为 function 相位在 0 仿真时间内顺序执行完,不存在「停在 build_phase 里等 jump」的窗口;跳进去也没有意义——build_phase 的使命是建树,树建完就翻页,跳回去重新建树会让已经建好的 component 句柄变成孤儿,config_db 里的资源引用也会错乱。这条边界是 UVM 在类型层面就焊死的:uvm_phase::jump 内部会检查 phase_type,非 task phase 直接报 fatal。
一个实战推论:想重复位 DUT,不要试图跳回 build,跳回 reset_phase 才是正道。 reset 之前的 pre_reset 也可以作为目标,用来在复位前再做一轮准备。如果激励序列挂在 main_phase 上(最常见),跳回 reset 之后,main_phase 会重新被调度,原本挂在 main 的 default sequence 会再启一遍——这一点设计用例时要心里有数,避免激励被意外重放两遍。
还有一个关于「跳回 main」的限制值得一提。12 个 runtime 兄弟相位与 run_phase 是并行关系——run_phase 从 pre_reset 起跑那一刻起就一直活着,直到 post_shutdown 结束。因此 jump 的目标集合是这 12 个兄弟加上 run 本身,但你无法在兄弟相位之间「横向插队到 run」来改变 run 的行为;run_phase 的结束由它自己的 objection 决定,跟兄弟相位的 jump 无关。实践中这意味着:如果激励线程写在 run_phase 里而不是 main_phase,jump 回 reset 不会重启 run 里的线程——它根本没被打断,还在原地跑。想让激励跟随 jump 重启,就把激励挂在 main_phase(或其他兄弟相位)上,而不是 run_phase。
六、jump 与 objection、跨 domain 同步的配合
jump 清 objection 的行为只在「被跳出的那个相位」生效,需要把三种角色分开看。第一,当前相位里本组件持有的 objection:调度器离开相位时直接作废,不会泄漏到目标相位。第二,当前相位里其他组件持有的 objection:同样被无视——这正是 jump 的强制性所在,哪怕某个 agent 还没 drop,jump 也照跳不误,该 agent 挂在相位上的 task 被 kill。第三,其他 domain 的 objection:完全不受影响,各自相位独立计票。
由此引出一条使用纪律:jump 之前,发起方最好显式 phase.drop_objection 把自己在当前相位的 objection 清干净。虽然调度器会兜底,但显式 drop 能让日志里的 objection 计数干净,也方便 debug 时区分「自然结束」与「被 jump 截断」。
domain 之间没有内建的「等待对方到达某相位」机制,但 runtime 相位对象本身提供了同步钩子:每个 phase 单例上有 wait_for_state(),可以阻塞等待某个相位到达指定状态(如 UVM_PHASE_STARTED、UVM_PHASE_ENDED)。想在 common domain 里等 io_domain 的 reset 走完,写法是:
task common_driver::main_phase(uvm_phase phase);
uvm_phase io_reset = uvm_reset_phase::get();
// 注意:wait_for_state 只观察全局相位状态,
// 多 domain 下要配合软同步(event / uvm_event)判定是哪个 domain 的实例
io_reset.wait_for_state(UVM_PHASE_ENDED);
start_traffic();
endtask这里有个诚实的提醒:wait_for_state 观察的是相位单例的全局状态,当多个 domain 都包含同名 runtime 相位时,它无法区分「是哪个 domain 的 reset 结束了」。可靠做法是用 uvm_event_pool 或组件间 event 做软同步:io_domain 的 agent 在 reset_phase 末尾 trigger 一个全局 event,common 侧 wait 这个 event。这是多 domain testbench 里最常用的对齐手段,比赌相位时序稳健得多。
七、多 domain + jump 的典型场景:局部复位
把 domain 和 jump 组合起来,最常见的落地是「局部复位」:DUT 的某个子域需要中途复位重激励,其他子域保持运行。
class io_agent extends uvm_agent;
// ... 常规 build/connect ...
endclass
class my_test extends uvm_test;
uvm_domain io_dom;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
io_dom = new("io_dom");
endfunction
function void connect_phase(uvm_phase phase);
env.io_agt.set_domain(io_dom); // 把 io_agent 挪出 common
endfunction
endclass
// 在 io_domain 的 main_phase 里,检测到错误就跳回 reset
task io_driver::main_phase(uvm_phase phase);
forever begin
drive_one();
if (error_detected) begin
`uvm_info("JUMP", "local error, jump back to reset", UVM_LOW)
phase.jump(uvm_reset_phase::get());
end
end
endtask
这张图里虚线是关键:common domain 的 main 与 io_domain 的 main 并行推进,io 侧 jump 回 reset 时,common 侧完全不察觉。这正是低功耗验证中「某电源域下电再上电」的建模方式——把该域的 agent 放进独立 domain,上下电用 jump 在 reset/configure 之间往返,其他域的 scoreboard 与覆盖率采样保持连续。
配套的 objection 策略也要跟着调:common domain 的 main_phase objection 不该被 io 侧的局部 jump 卡住。实践中常让 io_domain 自己管理 objection,common 侧只关心全局主激励是否完成;或者在 io 侧 jump 前先 drop 干净自己的 objection,避免 kill 时残留。
这个例子里还有一处值得停下来想:jump 的发起点放在哪个组件里最合适。放在 driver 里简单直接,但 driver 通常只看得见自己那一路信号,「要不要整支 agent 复位」往往是一个更高层的决策——错误可能由 scoreboard 检出、由 coverage 的异常 bins 暴露、或者由 test 层的看门狗定时触发。更稳的结构是把 jump 决策上收到 env 或 test:底层组件只负责上报「我这边出错了」(通过 uvm_event 或 analysis 通路),env 层拿到 io_domain 的相位句柄后统一发起 phase.jump。这样 driver 不需要 import domain 相关类型,复用到单 domain 的简单项目里也不用改代码。需要拿到相位句柄时,注意 phase 参数是 uvm_phase 基类,jump 方法就在基类上,直接调用即可,不需要向下转型。
八、jump 的常见坑
- 跳回 build_phase:类型层面就报错。需要重建组件就重启仿真,或者改用 factory override 在 build 期就定好形态。
- jump 后 sequence 被重放:挂在 main_phase 的 default sequence 在 backward jump 后会重启。如果不想重放,把 sequence 挂到 pre_main 或者由 test 显式 start 而不是 default_sequence。
- 跨 domain 的 scoreboard 漏收尾:io_domain 已经 extract,common 侧 scoreboard 还在 get。要靠 drain_time 加显式「io 侧事务计数清零」检查兜底。
- jump 时 objection 残留:jump 会 kill 当前相位的 task,但如果 objection 是别的相位持有的,行为可能出乎意料。稳妥做法是 jump 前显式 drop。
- 把 set_domain 写在 build_phase 里:
set_domain要在所有 component 建好之后调用才覆盖整棵子树,放在 connect_phase 或 end_of_elaboration_phase 更安全;写在 test 的 build_phase 里时,目标 agent 可能还没创建。 - runtime 相位名记错:UVM 的 runtime phase 是 12 个兄弟加 run_phase 本体共 13 个,jump 目标必须是其中真实存在的单例,用
uvm_reset_phase::get()这种类型方法取,不要自己 new。 - 多 domain 下的 timeout:
uvm_top.set_timeout是全局的,跟 domain 无关;某个 domain 卡死会一直拖到全局 timeout 才暴露。局部超时要在各 domain 的 phase 内自己埋。 - 用 wait_for_state 做多 domain 对齐:相位单例的状态是全局共享的,分不清是哪个 domain 的实例到达;多 domain 场景一律改用 uvm_event 软同步。
- jump 放进 forever 循环但忘了 break:jump 之后当前 task 被 kill,但如果发起 jump 的代码在 kill 之前又循环触发了第二次 jump,日志里会出现连续跳转,相位轨迹完全看不懂。jump 发起后应立即 return 或 break,把控制权交还给调度器。
- forward jump 跨过必要相位:从 main 直接跳到 shutdown 看似省事,但 post_main、pre_shutdown 里可能挂着 drain 逻辑(等 DUT 排空流水线)。跨相位跳转前确认被跨过的相位上没有不可或缺的收尾任务,否则最后一笔事务的比对会被静默丢掉。
- 在 function 相位回调里调 jump:jump 只改变调度器的 phase 链表走向,必须在 task 相位的执行上下文中发起才有意义;在 phase_started 这类 function 回调里调用,行为依赖仿真器实现,不要依赖它。
- domain 切分后忘记同步 config:子树搬进新 domain 不影响 config_db 查找,但如果你的配置下发依赖相位顺序(比如在 common domain 的 configure_phase 里 set、期望 io 侧同相位 get),切分后两边的 configure 不再对齐,配置可能没生效激励就开跑了。配置统一在 build/connect 期下发,避开 runtime 相位时序。
这一篇在知识体系里的位置
- 向上承接:UVM 组件与 phase 给出相位骨架,本篇解释多 domain 与跳转。
- 横向相关:UVM phase 与 objection 结束测试 讲单个相位的 objection 语义,与本篇的 jump 清 objection 行为互相印证。
- 落地延伸:低功耗 UVM 验证 与 UPF 端到端低功耗验证 里大量用到 domain 切分与 reset jump。
- 复位细节:复位与初始化验证 给出复位序列本身的检查要点,本篇解决「什么时候把整支 agent 拉回复位」的调度问题。
总结
UVM domain phase jump 把相位调度从「全班同步走」拆成「分组各走各的」:domain 是相位推进的分组单位,set_domain 把子树搬进独立域后节奏互不干扰;uvm_phase::jump 只允许在 13 个 runtime phase 之间跳转,forward 用于提前收尾、backward 用于局部重激励,jump 会 kill 当前相位任务并清掉 objection——用好这组机制,低功耗上下电、局部复位、错误注入重放都能在不重启仿真的前提下优雅落地。