UVM 报告机制:uvm_info/uvm_warning/error/fatal 与 verbosity 完全指南
更新时间:2026-09-24。本文是「集成电路大类 · 芯片设计」进阶层(intermediate) 验证方法学的一篇。跑过 UVM 组件与 phase 的 hello world 之后,日志问题马上就来:driver、monitor、scoreboard 同时向屏幕吐字,一次回归产生几万行日志,真正的 mismatch 被埋在中间。UVM(Universal Verification Methodology,通用验证方法学)的答案是一套集中式报告机制:所有打印走
uvm_info/uvm_warning/uvm_error/uvm_fatal四个宏,每条消息带 ID、严重级和冗余等级,框架统一决定「打不打、怎么处理、要不要退出」,并在仿真收尾时汇总计数。这一篇把这套机制从宏调用一路拆到退出计数。
本文要回答的问题
- 四个报告宏各自什么语义?为什么 error 出现后仿真还在继续,fatal 却立刻结束?
- verbosity 等级 UVM_NONE/LOW/MEDIUM/HIGH/FULL/DEBUG 是怎么过滤消息的?阈值设在哪一级?
+UVM_VERBOSITY和set_report_verbosity_level有什么区别,设置在组件树里如何继承?- 不想让某条 warning 打印、或者想把某个 ID 的 warning 升级成 error,怎么做?
- report_phase 的计数表如何决定用例成败?error 太多时怎样提前终止而不是傻跑完?
一、四个报告宏:一条消息的三个属性
UVM 报告体系里,每条消息有三个必备属性:ID(字符串,标明来源,便于 grep 和过滤)、严重级 severity(消息的性质)、冗余等级 verbosity(只对 info 有意义,标明这条信息有多「啰嗦」)。四个宏对应四种严重级:
`uvm_info("CFG", $sformatf("addr=0x%0h data=0x%0h", a, d), UVM_HIGH)
`uvm_warning("BUS", "handshake timeout, issuing retry")
`uvm_error("SB", $sformatf("mismatch @0x%0h exp=0x%0h got=0x%0h", addr, e, g))
`uvm_fatal("DRV", "virtual interface not set, cannot drive")四种严重级的默认处置差别很大,选级别的本质是回答「这条消息出现后,仿真还有没有意义继续」:
| 严重级 | 典型用途 | 默认 action | 是否计数 | 对仿真的影响 |
|---|---|---|---|---|
UVM_INFO | 进度、配置、事务细节 | DISPLAY | 否 | 无,受 verbosity 过滤 |
UVM_WARNING | 异常但已自恢复(重试、兜底取值) | DISPLAY | 计 warning 数 | 继续跑 |
UVM_ERROR | 功能错误、checker 不匹配 | DISPLAY | 计 error 数 | 默认继续,便于一次暴露多个错 |
UVM_FATAL | 环境性致命错误、无法继续 | DISPLAY + EXIT | 计 fatal 数 | 立即结束仿真 |
设计上有个刻意的安排:error 默认不终止仿真。一个用例里 DUT 可能错十次,若第一次就退出,你只能看到第一处现场;让仿真继续,scoreboard 会把十处 mismatch 连同地址全报出来,修复后一次回归验证全部消失。fatal 则相反——virtual interface 没拿到、config 对象为 null 这类问题属于环境本身坏了,再跑一百万拍也不会产生任何有效激励,立刻退出最省时间。fatal 消息要带足上下文(哪个组件、缺什么资源),否则退出后只剩一句干巴巴的报错。
认识一下报告最终上屏的长相。UVM 默认的一行报告包含固定的几个字段,读懂格式后看日志就能直接定位,不必再回头翻代码:
UVM_ERROR @ 128500: uvm_test_top.env.agt.sb [SB/DATA] mismatch @0x1004 exp=0x00000001 got=0x00000000
UVM_INFO @ 90000: uvm_test_top.env.agt.drv [DRV] drove beat addr=0x2000@ 后面的数字是报告发生时的仿真时间,这里是 128500 个时间单位——同一个错误,能从日志里直接读出它在激励序列的哪个时刻出现;随后是发出报告的组件全路径名,从 uvm_test_top 一路写到具体组件,多实例环境(总线上挂了四个 agent)靠它区分是哪一个实例报的;方括号里是 ID,最后才是消息正文。warning 和 fatal 行会额外冠以明显的标记前缀,在日志里翻页时一眼可辨。这四个字段都是框架自动填的,你写宏时只需提供 ID 和正文,这也是「走宏」相对手写打印的隐性收益:格式全站统一,grep、着色、日志解析工具都能直接消费。
二、verbosity:一条消息从宏调用到屏幕要过几道关
verbosity(冗余度)是一个整数阈值,等级从低到高排列:
UVM_NONE(0) < UVM_LOW(100) < UVM_MEDIUM(200) < UVM_HIGH(300) < UVM_FULL(400) < UVM_DEBUG(500)语义要从「比较」角度理解:每条 uvm_info 自带一个等级,组件当前设了一个阈值;消息等级不高于阈值才打印。阈值默认是 UVM_MEDIUM,所以默认能看到 LOW 和 MEDIUM,HIGH 以上全部静默。注意 UVM_NONE 是「无论阈值多低都要打印」的等级,而 warning/error/fatal 不受 verbosity 过滤——错误永远有资格上屏。
下面这张图画出一条报告从代码里的宏调用开始,经过的全部关卡:
读图时抓住「两道关、一个出口」:第一道是 verbosity 关卡,等级不够的 info 在这一步被静默丢弃,连计数都不进;第二道是 severity/ID 过滤器,查用户是否给这个严重级或这个 ID 改过动作;最后所有处置归结到 action——打印、静默、计数、退出是四个可以组合的独立动作,而不是互斥选项。即使 fatal 触发 EXIT,UVM 也会先把报告汇总打出来再结束,所以退出不等于没有结论。
等级怎么选有个经验分法:LOW 放每个用例只出现一两次的关键信息(用例名、寄存器配置结果、用例结束的统计),MEDIUM 放阶段性摘要(发出的事务总数、覆盖率事件),HIGH 放逐条事务(每个 beat、每次传输的地址数据),DEBUG 放最细的内部状态(逐信号、逐周期的握手细节)。判断标准很实际:这条信息在回归失败定位时多久会用到一次?一个月用一次的细节,就不该在每天的默认日志里刷屏。
verbosity 等级的使用约定也值得列成表,它直接对应调试时的排查动作:
| 等级 | 数值 | 放什么内容 | 典型打开场景 |
|---|---|---|---|
| UVM_NONE | 0 | 极少用,强制可见的 info | 需要无视任何阈值的横幅信息 |
| UVM_LOW | 100 | 用例名、配置结果、收尾统计 | 默认(回归日志的主干) |
| UVM_MEDIUM | 200 | 阶段摘要、激励约束结果 | 默认阈值,排查激励构成时 |
| UVM_HIGH | 300 | 逐事务、逐 beat 的地址数据 | 定位协议级 mismatch |
| UVM_FULL | 400 | 组帧细节、状态机迁移 | 怀疑 DUT 内部行为时 |
| UVM_DEBUG | 500 | 逐周期信号、握手电平 | 最后的现场级排查 |
一个容易混淆的点:阈值是全局整数,自定义时也可以直接传数字(如 set_report_verbosity_level(350)),含义是「显示所有等级不超过 350 的 info」,于是 HIGH 能看到、FULL 看不到。命令行还支持不重新编译就按组件精确开关,语法是 +uvm_set_verbosity=<comp>,<id>,<verbosity>,<phase>,例如 +uvm_set_verbosity=uvm_test_top.env.agt.mon,MON,DEBUG,run_phase 表示 monitor 在 run_phase 期间打开 DEBUG。它和 +UVM_VERBOSITY 的区别是:后者一刀切改全局基线,前者只动指定路径、指定 ID,且可以限定生效的 phase,调试完毕撤参数即可,不留任何代码痕迹。
三、设置 verbosity:命令行、组件句柄与层级继承
阈值有两条设置途径。命令行加参数影响全局,不用重新编译:
+UVM_VERBOSITY=UVM_HIGH
+UVM_VERBOSITY=DEBUG # 等价于 UVM_DEBUG代码里用组件方法精确控制,而且可以带 recurse 参数一口气覆盖整棵子树:
// 只调当前组件
env.agent.monitor.set_report_verbosity_level(UVM_DEBUG);
// 调 env 及其所有子孙组件
env.set_report_verbosity_level_hier(UVM_HIGH);
// 连 ID 一起限定:只打开 scoreboard 的 SB/DATA 类细节
sb.set_report_id_verbosity("SB/DATA", UVM_DEBUG);下图是一棵典型环境树和继承关系。子组件不单独设置时,沿树向上找到的第一个显式设置就是有效值;注意 verbosity 继承只发生在「设置动作」时(hier 版本逐级下发),不是运行时动态查询,所以事后新建的组件拿不到之前的下发值,这是真实踩过的时序坑。
top: "uvm_test_top\n(默认 UVM_MEDIUM)"
env: "env\nset_..._hier(UVM_HIGH)\n下发覆盖整棵子树"
agent: "agent\n继承 env:UVM_HIGH"
drv: "driver\n继承:UVM_HIGH"
mon: "monitor\n单独设 UVM_DEBUG\n注:hier 下发发生在调用瞬间,之后才创建的组件不受影响"
sb: "scoreboard\n继承:UVM_HIGH"
top -> env
env -> agent
env -> sb
agent -> drv
agent -> mon这张图解释了调试时最常用的一个手法:回归默认阈值保持 MEDIUM,日志干净;定位某个失败用例时只给怀疑的子树(比如 monitor)临时开 DEBUG,定位完撤掉,避免「全量开 HIGH 然后在十万行里找人」。
设置时机同样有讲究。UVM 的组件构建分阶段发生:test 的 build_phase 里先建 env,env 的 build_phase 再建 agent 和各组件,层层向下。如果你在 env 的 build_phase 开头调用 hier 版本下发阈值,此刻 agent 和 monitor 还没被 new 出来,下发不到它们身上——这正是图下注释说的时序坑。稳妥写法有两种:一是在 test 的 build_phase 末尾(此时 super.build_phase 和子组件构建都已完成)再做 hier 下发;二是把设置放到 connect_phase 或 end_of_elaboration_phase,那时整棵组件树已经成型,一次下发覆盖全部现存组件。反过来,如果某个组件希望无论环境怎么配都保持自己的阈值(比如 checker 永远打印关键结论),就在它自己的 build_phase 里设置自身,就近配置的优先级最清晰。
class base_test extends uvm_test;
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
// 放在 create 之后:子树已存在,hier 下发才能到达每个组件
env.set_report_verbosity_level_hier(UVM_HIGH);
endfunction
endclass另外,命令行参数的优先级高于代码里的默认设置:回归平台固定在代码里配好常规阈值,个人调试时临时加 +UVM_VERBOSITY 覆盖,两条路径互不打架,这也是 CI(持续集成)环境推荐的分工。
四、severity action:抑制、升级与退出计数
action 不止默认那一套,每个组件、每种严重级、每个 ID 都可以单独改配,常用方法分三档粒度:
// 1) 抑制:某类已知无害的 warning 不再打印
drv.set_report_severity_id_action(UVM_WARNING, "BUS/RETRY", UVM_NO_ACTION);
// 2) 升级:把某 ID 的 warning 当 error 计数
agt.set_report_severity_id_action(UVM_WARNING, "CFG/FALLBACK", UVM_DISPLAY | UVM_COUNT);
// 3) 全局:让 error 达到上限就退出
set_report_max_quit_count(10); // 0 表示不限制(默认)
set_report_severity_action(UVM_ERROR, UVM_DISPLAY | UVM_COUNT | UVM_EXIT);action 的取值是位掩码,可以或起来:UVM_NO_ACTION(什么都不做)、UVM_DISPLAY(打印)、UVM_COUNT(计入计数器)、UVM_EXIT(触发退出)、UVM_STOP(停在调试器,类似 $stop)。抑制某条消息的正解是 NO_ACTION,而不是去注释掉那行宏——宏保留着,回归换个配置还能重新打开。
顺带厘清两个容易混的动作:UVM_EXIT 是让仿真彻底结束、返回 shell,适合无人值守的批量回归;UVM_STOP 只是把仿真器挂起在交互式命令行,等人上去看波形、查变量,适合本地交互式调试。波形还没dump、信号没接上的自动化环境里误用 STOP,整批回归会挂在那等人敲回车,是真实出现过的流水线事故。另外 EXIT 的退出路径仍受 phase 机制约束——它不是凭空杀掉进程,UVM 会先走完报告汇总、执行收尾动作再返回,所以「error 太多提前退出」和「正常结束」拿到的信息形态是一样的,区别只在汇总段里那句退出原因。
max quit count 解决第一节留下的问题:error 默认继续跑能暴露更多错误,但 DUT 彻底跑飞后可能连续报出几万个同源 error,日志被撑爆、仿真时间白白浪费。set_report_max_quit_count(10) 的语义是计数型消息累计到 10 条就收尾退出,前十个现场足够定位,又不会傻跑到最后。注意计数对象是「带 COUNT 动作的报告」,默认配置下就是 error 与 fatal 的合计。
仿真收尾时 report_phase 打印每张报告服务器的计数表,形如「WARNING : 3 ERROR : 2 FATAL : 1」。回归脚本判成败看的就是这张表(或进程返回值),不是「仿真有没有正常结束」——这正是为什么用 $display 直接打印错误是坏习惯:它绕开了计数,用例失败了回归系统却判通过。
真实日志里的汇总段大致长这样,每个发出过报告的组件各占一张小表:
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO : 47
UVM_WARNING : 3
UVM_ERROR : 2
UVM_FATAL : 0
** Report counts by id
["SB/DATA"] 2
["BUS/RETRY"] 3除了按严重级汇总,框架还按 ID 汇总,这就是规范 ID 的另一重回报:用例失败后不用翻全文,看 ID 计数就知道两处分错都出自 scoreboard 的数据比对,三条警告全是总线重试——分流给 RTL 工程师还是验证工程师,结论直接写在表里。配了 max quit count 时,汇总段还会注明「因达到退出上限而结束」,提醒你这不是用例自然跑完,后续 error 没有继续收集。
还有一个常被忽略的出口定制:默认报告只进仿真控制台,而长回归的控制台日志动辄上 GB。可以给报告服务器挂自定义 handler,把 error 以上的消息单独写一份小文件、或转发到 socket 供实时看板消费,普通 info 仍走控制台。消息的产生(宏)和消息的去向(handler)由此解耦,改去向时一行验证代码都不用动。
五、ID 命名与防刷屏实践
ID 是这套机制里最容易被写废、也最值得投入的一维。推荐用层级化短词,全环境保持一张约定表:
| ID | 归属 | 放什么 |
|---|---|---|
DRV | driver | 管脚驱动、复位释放 |
MON | monitor | 总线观测到的事务 |
SB/DATA、SB/PROTO | scoreboard | 数据比对 / 协议序比对 |
CFG | 配置过程 | 寄存器模型、config_db 取值 |
SEQ | sequence | 激励编排与约束结果 |
斜杠分出子类后,可以按 ID 精确调 verbosity 或改 action(如只抑制 SB/PROTO 的告警而保留数据比对),grep 时也能一把锁定来源。反过来,空 ID、随手写的 "err"、每条消息 ID 都不同,等于主动放弃过滤和统计能力。
ID 约定表要在项目启动时就定,并在 code review 里守住:新加一个组件,先登记它使用的 ID,而不是写到哪算哪。配套的小纪律是同一类事件全环境复用同一个 ID——所有「总线超时重试」都叫 BUS/RETRY,这样前面演示的按 ID 抑制和汇总才有意义;如果 driver 叫 RETRY、monitor 叫 MON_RETRY、scoreboard 又叫 TIMEOUT,同一件事在计数表里散成三行,抑制时要改三个地方,迟早漏一个。ID 命名表可以和类名约定、寄存器前缀一起放进验证环境的 README,新人照着表写,日志风格从第一天就是统一的。
其余高频坑一并列出:
- 所有 info 都用 UVM_LOW:默认日志爆炸,真正的 error 被淹没,等于没有分级;反过来走向另一个极端——关键结论塞进 DEBUG,回归失败后默认日志里什么都看不到,同样是事故;
- 在
uvm_info里做重计算:被 verbosity 过滤掉的消息,其参数表达式仍会求值,$sformatf拼接大数组、调用sprint打印整个对象是实打实的仿真开销。极细日志可以先判uvm_report_enabled(UVM_HIGH)再拼装,把成本也关进阈值后面; - fatal 不带上下文:至少写清组件全名、地址、期望值与实际值;
- 用
$display/$write打错误:绕开计数与汇总,回归无法自动判失败;\uvm_error 内部就是走报告服务器,没有任何理由在验证组件里保留裸打印; - 只看仿真是否结束就报通过:必须检查 report_phase 计数或退出码;
- warning 长期放任不清理:warning 不影响通过判定,于是越积越多,某天一条真正重要的 warning 混在两百条噪声里无人看见。健康的回归应当对 warning 数也设趋势红线,新增 warning 需要说明理由,确认为无害的再用 NO_ACTION 显式抑制——抑制动作本身就是一次评审记录。
这一篇在知识体系里的位置
本篇属于芯片设计进阶层的 UVM 验证方法学,回答「验证环境怎么说话、回归系统怎么听」。向前读,组件、phase 与报告服务器的挂载关系见 UVM 组件与 phase,产生报告最多的三个组件见 UVM sequence、driver 与 scoreboard;横向配套,组件树的层次替换与 UVM factory 机制同源,scoreboard 报告与对拍方法见 UVM scoreboard 与 trace 对齐,仿真何时结束由 UVM phase objection 机制决定;向后读,本篇的计数表是 回归管理与失败分流 的输入,低功耗仿真下的报告差异见 低功耗 UVM 验证。
总结
UVM 用 uvm_info/uvm_warning/uvm_error/uvm_fatal 四个宏统一全部打印,每条消息带 ID、严重级和 verbosity 三个属性:info 按「消息等级不高于阈值」被 UVM_LOW/MEDIUM/HIGH/DEBUG 过滤,阈值可由 +UVM_VERBOSITY 全局给定或 set_report_verbosity_level(_hier) 沿组件树下发;warning/error/fatal 不受 verbosity 限制,error 默认计数继续跑以暴露全部现场、fatal 默认 DISPLAY+EXIT 立即收尾;用 set_report_severity_id_action 可按 ID 抑制或升级告警,用 set_report_max_quit_count 让错误累计到上限提前终止,最终 report_phase 的计数表(而非仿真是否结束)决定用例成败——实践关键就是 ID 层级化、细节日志拉高 verbosity、一切错误走宏而非 $display,别让日志噪声掩盖真正的问题。