A new revision of a third-party PCIe VIP prints a protocol error that its vendor calls harmless, and 300 tests turn red overnight. Someone adds a report catcher that demotes every error whose ID contains MISMATCH, "until the vendor fixes it". Four months later a real data-path bug ships to silicon. Its scoreboard error had the ID SB_MISMATCH, and every one of them was demoted to an info by a catcher nobody remembered.
A report catcher is the most powerful reporting tool UVM has, and the most dangerous. This lesson shows exactly where it sits in a message's path, what it can change, how the bench waives one known issue narrowly, and the discipline that keeps a waiver from becoming a false pass.
Every message travels from the component that reported it to the global report server, which counts it. A report catcher is a customs officer on that road. It can open the parcel, read the label (severity, ID, message, the component that sent it), change the label, and then either wave it on (THROW) or confiscate it (CAUGHT). The server only ever sees what the officers let through, labelled the way they left it. That is why a catcher can turn a failing test into a passing one, and why each catcher must be as narrow as a court order.
Catchers are callbacks: uvm_report_catcher derives from uvm_callback, and you register one on a report object with uvm_report_cb::add(). The object you register it on decides whose messages it sees.
Three consequences follow from that order, and all three were confirmed in a scratch run:
A catcher sees the severity after any override. An SB_EMPTY error overridden to info on a component reaches that component's catcher already as UVM_INFO.
Changing the severity changes the action. When the message still carries its old severity's default action and the catcher does not call set_action(), the 1800.2 library replaces it with the default action of the new severity. An error demoted to info loses UVM_COUNT; a warning promoted to error gains it.
`CAUGHT` makes a message vanish from the log and from the counts. A demotion with THROW leaves evidence; prefer it.
dma_waiver_test reproduces a known bench limitation (war story 9): the DMA ignores a KEY write while it is busy, but the scoreboard takes the new key at once, so the remaining words of that job are reported as mismatches. The limitation is tracked as ticket DMA-142. The catcher waives only that:
// snippet: from the course DMA bench
// A narrow waiver: demotes one message ID whose text matches one pattern, tags
// it with the tracking ticket, and counts what it waived.
class dma_waiver_catcher extends uvm_report_catcher;
string waive_id;
string pattern = "*";
string ticket;
int unsigned waived;
function new(string name = "dma_waiver_catcher");
super.new(name);
endfunction
function action_e catch();
if (get_severity() == UVM_ERROR && get_id() == waive_id && uvm_is_match(pattern, get_message())) begin
set_severity(UVM_INFO);
set_message({"[WAIVED ", ticket, "] ", get_message()});
waived++;
end
return THROW;
endfunction
endclassOne severity, one ID, one pattern. It acts only on UVM_ERROR with ID SB_MISMATCH whose text matches pattern (uvm_is_match is glob matching).
It tags the message with the ticket, so the waived lines are still in the log and searchable.
It counts what it waived, so the test can report the number.
It always returns `THROW`. Nothing disappears.
The test attaches it to the scoreboard only, after the env exists, and reports the count:
// snippet: from dma_waiver_test in the course DMA bench (dma_tb.sv)
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
catcher = new("catcher");
catcher.waive_id = "SB_MISMATCH";
catcher.ticket = "DMA-142";
uvm_report_cb::add(env.sb, catcher); // this component only
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("WAIVER", $sformatf("%0d message(s) waived under %s", catcher.waived, catcher.ticket), UVM_LOW)
endfunctionuvm_report_cb::add(env.sb, c) registers the catcher on that one report object. It sees messages reported by env.sb, not by its children: callbacks on a report object are not hierarchical. uvm_report_cb::add(null, c) registers it type-wide, for every report object in the run. The scratch run attached a demoting catcher to a component p and a promoting catcher type-wide:
// snippet: scratch run (sync_tb.sv), in the test's end_of_elaboration_phase
demote_catcher d = new("d"); // demotes VIP_X errors to warnings
promote_catcher pr = new("pr"); // promotes PROTO_TIMEOUT warnings to errors
uvm_report_cb::add(p, d); // p only, not p.c
uvm_report_cb::add(null, pr); // every report object
p.set_report_severity_id_override(UVM_ERROR, "SB_EMPTY", UVM_INFO);scratch run: +UVM_TESTNAME=catcher_test
UVM_ERROR sync_tb.sv(152) @ 10000: uvm_test_top.p.c [VIP_X] child error
UVM_WARNING sync_tb.sv(167) @ 20000: uvm_test_top.p [VIP_X] [DEMOTED] parent error
UVM_ERROR sync_tb.sv(168) @ 20000: uvm_test_top.p [PROTO_TIMEOUT] [PROMOTED] no response in 64 cycles
UVM_INFO sync_tb.sv(169) @ 20000: uvm_test_top.p [SB_EMPTY] overridden then caught
--- UVM Report catcher Summary ---
Number of demoted UVM_ERROR reports : 1
Number of caught UVM_ERROR reports : 0The child's VIP_X error was not demoted: the catcher on p never saw it.
Promotion is the underused direction. Many VIPs report protocol violations as warnings, which never fail a test; a type-wide promoting catcher for exactly those IDs makes them count.
The library prints a catcher summary of demoted and caught messages. A regression that reads it can flag runs where catchers did unexpected work.
A waiver is a promise that a known message is harmless, for a reason, for a while. Write all five parts of that promise into the code:
Field | In the bench | Why |
|---|---|---|
Narrow ID |
| An exact ID, never a substring or a glob over IDs. |
Narrow text |
| Narrow it to the known signature, for example the address range of the known issue. The demo is deliberately wide: every |
One component |
| Never type-wide for a demotion. |
Ticket |
| The waived line is searchable and someone owns closing it. |
Expiry and expected count | not in the bench; add them | A waiver must end, and must fail loudly when it waives too much or nothing at all. |
The last row is what turns a catcher from a muzzle into a contract. A derived waiver can check both at the end of the run:
// snippet: a waiver with an expected count and an expiry
class dma_waiver_rule extends dma_waiver_catcher;
int unsigned max_waived = 8; // the known issue never produces more
int unsigned expires_rev = 7; // RTL revision that fixes DMA-142
function new(string name = "dma_waiver_rule");
super.new(name);
endfunction
// Called from the test's report_phase.
function void audit();
int unsigned rtl_rev;
if ($value$plusargs("RTL_REV=%d", rtl_rev) && rtl_rev >= expires_rev)
`uvm_error("WAIVER_EXPIRED", $sformatf("%s should be fixed in rev %0d; remove the waiver", ticket, expires_rev))
if (waived > max_waived)
`uvm_error("WAIVER_EXCESS", $sformatf("%s waived %0d messages, more than the known %0d", ticket, waived, max_waived))
if (waived == 0)
`uvm_warning("WAIVER_UNUSED", $sformatf("%s waived nothing in this test", ticket))
endfunction
endclassLarge teams often move waivers out of the simulation entirely, into the regression tool, which matches failure signatures against a reviewed waiver list after the run. That keeps every message intact in the log. In-simulation catchers remain the right tool for a VIP's known noise, where the message must not count at all.
+UVM_TESTNAME=dma_waiver_test
UVM_INFO dma_tb.sv(396) @ 375000: uvm_test_top.env.sb [SB_MISMATCH] [WAIVED DMA-142] expected 4bce257b, got b4312584
UVM_INFO dma_tb.sv(396) @ 395000: uvm_test_top.env.sb [SB_MISMATCH] [WAIVED DMA-142] expected 98357889, got 67ca7876
UVM_INFO dma_tb.sv(396) @ 435000: uvm_test_top.env.sb [SB_MISMATCH] [WAIVED DMA-142] expected 089dd98a, got f762d975
UVM_INFO dma_tb.sv(396) @ 475000: uvm_test_top.env.sb [SB_MISMATCH] [WAIVED DMA-142] expected 2d9cf6a9, got d263f656
UVM_INFO dma_tb.sv(396) @ 525000: uvm_test_top.env.sb [SB_MISMATCH] [WAIVED DMA-142] expected 511e14a1, got aee1145e
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 3 words matched, 5 mismatched
UVM_INFO dma_tb.sv(1899) @ 655000: uvm_test_top [WAIVER] 5 message(s) waived under DMA-142
UVM_ERROR : 0
UVM_FATAL : 0Five SB_MISMATCH lines, each tagged [WAIVED DMA-142], all at UVM_INFO. The evidence is in the log; the server did not count them as errors.
"3 words matched, 5 mismatched" is the scoreboard's own report and still tells the truth.
"5 message(s) waived under DMA-142" is the count a reviewer compares with the ticket. If it ever says 6, something new is being waived.
The bench's dma_false_pass_test is the opening story in miniature. It is the early-end bug (the test drops its objection before any data moves), after someone "quietened" the scoreboard's end-of-test errors with two unconditional severity overrides. The scoreboard says plainly that it checked nothing, and the verdict says PASS:
+UVM_TESTNAME=dma_false_pass_test
UVM_INFO dma_tb.sv(407) @ 155000: uvm_test_top.env.sb [SB_EMPTY] no words were compared: the test checked nothing
UVM_INFO dma_tb.sv(411) @ 155000: uvm_test_top.env.sb [SB] 0 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0
TEST VERDICT: PASS (UVM_ERROR=0, UVM_FATAL=0)No catcher was even needed: a severity override is a catcher with no conditions. The fix is not to ban demotions but to make every one of them carry the five fields above, and to keep SB_EMPTY-style "I checked nothing" errors off every waiver list. The full diagnosis of this failure, and how to detect it in a regression, is in the false pass playbook.
uvm_report_cb::add(env, c) is supposed to demote an error from env.agt.mon. It does nothing. Why, and what are the two fixes?Catchers registered on a report object see only that object's own messages, not its children's. Either register it on env.agt.mon, or register it type-wide (null) and check get_client().get_full_name() inside catch() so it still acts on one component only.
CAUGHT for a VIP's known error. Six months later a regression on a new VIP revision is green, yet the bug the error reported is real. What made it invisible, and what would have kept the evidence?CAUGHT removes the message from the log and from every count, so nothing showed it was still firing. A demotion with THROW and a ticket tag keeps the line in the log, and an expected-count audit would have flagged the change.
SB_EMPTY error that a severity override already turned into an info?Severity overrides are applied by the component's report handler before the message reaches any catcher. The catcher sees an info; the error no longer exists anywhere in the path.
Next: catchers decide what each message means. The report server decides what the whole run means, and it must say so even when the run dies: Custom Report Servers and the Verdict Line.