UART · Module 16
Reference Model, Scoreboard and Coverage Collector
Connecting the predictor, scoreboard and coverage collector as analysis subscribers, and the objection mechanism that decides when a test may stop — built as running code, with the failure that looks exactly like success.
Three subscribers hang off the monitor's analysis port, and none of them drives anything. They predict, they compare, and they record. Chapter 14.5 established the boundaries between those three roles and this chapter does not move them.
What it adds is the mechanism nobody thinks about until it bites: deciding when the test is allowed to stop. That one has a failure mode which is indistinguishable from success.
1. Three Subscribers, One Port
class uart_scoreboard extends uvm_scoreboard;
`uvm_component_utils(uart_scoreboard)
`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_obs)
uvm_analysis_imp_exp #(uart_frame_item, uart_scoreboard) exp_export;
uvm_analysis_imp_obs #(uart_frame_item, uart_scoreboard) obs_export;
protected uart_frame_item m_expected[$];
protected int m_compared, m_mismatch;
function new(string name, uvm_component parent);
super.new(name, parent);
exp_export = new("exp_export", this);
obs_export = new("obs_export", this);
endfunction
// QUEUE, not a count and not a single expected value. A queue is the only
// structure that encodes "order is preserved" without also claiming to
// know how many transactions are in flight.
function void write_exp(uart_frame_item t);
m_expected.push_back(t);
endfunction
function void write_obs(uart_frame_item t);
uart_frame_item e;
if (m_expected.size() == 0) begin
`uvm_error("SB", $sformatf(
"observed a frame (data=%03h) that was never predicted", t.data))
return;
end
e = m_expected.pop_front();
m_compared++;
// Field by field, each mismatch reported separately. A single
// "frames differ" message makes the reader diff two hex strings by
// eye; naming the field is the difference between a five-minute
// debug and an hour of one.
if (t.data !== e.data) diff("data", e.data, t.data);
if (t.parity_err !== e.parity_err) diff("parity_err", e.parity_err, t.parity_err);
if (t.frame_err !== e.frame_err) diff("frame_err", e.frame_err, t.frame_err);
endfunction
function void check_phase(uvm_phase phase);
if (m_expected.size() != 0)
`uvm_error("SB", $sformatf(
"%0d predicted frames were never observed", m_expected.size()))
// THE line that stops a scoreboard lying by silence.
if (m_compared == 0)
`uvm_error("SB", "the scoreboard compared NOTHING")
endfunction
endclass2. The Coverage Collector
class uart_coverage extends uvm_subscriber #(uart_frame_item);
`uvm_component_utils(uart_coverage)
uart_cfg m_cfg;
uart_frame_item m_item;
covergroup cg_frame;
cp_width : coverpoint m_item.nbits { bins w[] = {[5:9]}; }
cp_parity : coverpoint m_item.parity_mode { bins p[] = {[0:3]}; }
cp_stop : coverpoint m_item.stop_halves { bins s[] = {[2:4]}; }
cp_err : coverpoint m_item.err { bins e[] = {[0:5]}; }
// THE cross. Chapter 15.3 section 2: six frames fill every marginal
// axis and leave 54 of the 60 cross bins empty.
x_format : cross cp_width, cp_parity, cp_stop;
endgroup
function new(string name, uvm_component parent);
super.new(name, parent);
cg_frame = new();
endfunction
// The whole of write(). Assigning a handle and sampling is fast; anything
// slower here loses transactions, and 16.4 section 3 measured 30 of 60
// lost to a subscriber doing four cycles of work.
function void write(uart_frame_item t);
if (!m_cfg.coverage_enable) return;
m_item = t;
cg_frame.sample();
endfunction
endclassChapter 15.3 §6 is the reason this loop matters and not the percentage. A real defect survived 62 passing checks and 4,079 clocks of random traffic because one bin was never hit — zero occurrences, not rare ones. More seeds would not have found it; an empty bin and five lines of directed stimulus did.
3. Deciding When to Stop
class uart_base_test extends uvm_test;
task run_phase(uvm_phase phase);
uart_format_cross_seq seq;
phase.raise_objection(this); // "do not end the test yet"
seq = uart_format_cross_seq::type_id::create("seq");
seq.start(env.m_agent.m_sqr);
phase.drop_objection(this); // "I am finished"
endtask
endclassAn objection is a counter and a rule: raise increments, drop decrements, and zero ends the phase. The complexity in UVM is in propagating it up the hierarchy and in drain time, not in the counting.
And here is the bug:
// Sequence A finishes.
phase.drop_objection(this); // count 1 -> 0 THE TEST ENDS HERE
// Sequence B was going to start.
phase.raise_objection(this); // too lateNothing is wrong with either call. The test simply stops in the gap between them — and it presents as a suite that passes in a suspiciously short time, with the last few checks never reported. A passing run with missing checks looks exactly like a passing run.
4. An Objection Mechanism That Runs
//===========================================================================
// uart_objection_v — the OBJECTION MECHANISM, in Verilog-2001
//
// NOT SYNTHESIZABLE. A verification component.
//
// WHAT AN OBJECTION IS: a counter, and a rule about when it reaching zero
// means the phase is over.
//
// `phase.raise_objection(this)` increments it, `drop_objection(this)`
// decrements it, and when it returns to zero the phase ends. That is all.
// The complexity in UVM is in propagation up the component hierarchy and
// in the drain time, not in the counting.
//
// AND THE BUG THIS COMPONENT EXISTS TO MAKE VISIBLE: the count reaching
// zero BETWEEN two pieces of work ends the test early.
//
// // sequence A finishes
// phase.drop_objection(this); // count 1 -> 0 TEST ENDS HERE
// // sequence B was going to start
// phase.raise_objection(this); // too late
//
// Nothing is wrong with either call. The test simply stops in the gap. It
// presents as a suite that passes in a suspiciously short time, with the
// last few checks never reported -- and a passing run with missing checks
// looks exactly like a passing run.
//
// So this component reports two things, not one: whether the count is at
// zero now, and whether it has RETURNED to zero having once been raised.
// The second is what a testbench can assert on -- and the qualification
// matters. "Has been at zero" is true on the first cycle after reset, when
// nothing has objected yet, and a signal that is always true reports
// nothing. The first draft of this file got that wrong and the testbench
// caught it.
//
// DRAIN TIME is the standard defence: hold the end of phase for N cycles
// after the count reaches zero, so a momentary gap does not end anything.
// It is a parameter here because it is a policy, and because a testbench
// should be able to set it to zero and watch the failure happen.
//===========================================================================
`timescale 1ns/1ps
module uart_objection_v #(
parameter DRAIN = 0 // cycles to wait before ending
) (
input wire clk,
input wire rst_n,
input wire raise_i, // raise_objection()
input wire drop_i, // drop_objection()
output reg [15:0] count_o,
output reg all_dropped_o, // the phase may end now
output reg ever_zero_o, // it has RETURNED to zero at least once
output reg ever_ended_o, // the phase was PERMITTED to end at least once
output reg [15:0] drain_q_o,
output reg underflow_o // a drop with nothing raised
);
reg raised_once;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count_o <= 16'd0;
all_dropped_o <= 1'b0;
ever_zero_o <= 1'b0;
ever_ended_o <= 1'b0;
raised_once <= 1'b0;
drain_q_o <= 16'd0;
underflow_o <= 1'b0;
end else begin
// A drop with no matching raise is a testbench bug, not a phase
// ending. UVM reports it; so does this.
if (drop_i && !raise_i && count_o == 16'd0)
underflow_o <= 1'b1;
else if (raise_i && !drop_i)
count_o <= count_o + 1;
else if (drop_i && !raise_i)
count_o <= count_o - 1;
// raise and drop in the same cycle cancel, which is why a
// component may re-raise before dropping without a gap.
if (raise_i && !drop_i) raised_once <= 1'b1;
if (count_o == 16'd0) begin
// Qualified on having been raised: the count is zero before
// anything objects, and reporting that as "the phase could
// have ended" is true but useless.
if (raised_once) ever_zero_o <= 1'b1;
// Sticky, because all_dropped_o is momentary: it clears again
// as soon as anything re-raises. A testbench that samples the
// momentary signal after the gap has closed sees nothing and
// concludes the phase was safe -- which is how a mutant that
// ignored the drain time survived the first campaign.
if (drain_q_o >= DRAIN) begin
all_dropped_o <= 1'b1;
if (raised_once) ever_ended_o <= 1'b1;
end
else drain_q_o <= drain_q_o + 1;
end else begin
drain_q_o <= 16'd0;
all_dropped_o <= 1'b0;
end
end
end
endmodule//===========================================================================
// uart_objection — the OBJECTION MECHANISM, in SystemVerilog
//
// NOT SYNTHESIZABLE. A verification component.
//
// WHAT AN OBJECTION IS: a counter, and a rule about when it reaching zero
// means the phase is over.
//
// `phase.raise_objection(this)` increments it, `drop_objection(this)`
// decrements it, and when it returns to zero the phase ends. That is all.
// The complexity in UVM is in propagation up the component hierarchy and
// in the drain time, not in the counting.
//
// AND THE BUG THIS COMPONENT EXISTS TO MAKE VISIBLE: the count reaching
// zero BETWEEN two pieces of work ends the test early.
//
// // sequence A finishes
// phase.drop_objection(this); // count 1 -> 0 TEST ENDS HERE
// // sequence B was going to start
// phase.raise_objection(this); // too late
//
// Nothing is wrong with either call. The test simply stops in the gap. It
// presents as a suite that passes in a suspiciously short time, with the
// last few checks never reported -- and a passing run with missing checks
// looks exactly like a passing run.
//
// So this component reports two things, not one: whether the count is at
// zero now, and whether it has RETURNED to zero having once been raised.
// The second is what a testbench can assert on -- and the qualification
// matters. "Has been at zero" is true on the first cycle after reset, when
// nothing has objected yet, and a signal that is always true reports
// nothing. The first draft of this file got that wrong and the testbench
// caught it.
//
// DRAIN TIME is the standard defence: hold the end of phase for N cycles
// after the count reaches zero, so a momentary gap does not end anything.
// It is a parameter here because it is a policy, and because a testbench
// should be able to set it to zero and watch the failure happen.
//===========================================================================
`timescale 1ns/1ps
module uart_objection #(
parameter DRAIN = 0 // cycles to wait before ending
) (
input wire clk,
input wire rst_n,
input wire raise_i, // raise_objection()
input wire drop_i, // drop_objection()
output logic [15:0] count_o,
output logic all_dropped_o, // the phase may end now
output logic ever_zero_o, // it has RETURNED to zero at least once
output logic ever_ended_o, // the phase was PERMITTED to end at least once
output logic [15:0] drain_q_o,
output logic underflow_o // a drop with nothing raised
);
logic raised_once;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count_o <= 16'd0;
all_dropped_o <= 1'b0;
ever_zero_o <= 1'b0;
ever_ended_o <= 1'b0;
raised_once <= 1'b0;
drain_q_o <= 16'd0;
underflow_o <= 1'b0;
end else begin
// A drop with no matching raise is a testbench bug, not a phase
// ending. UVM reports it; so does this.
if (drop_i && !raise_i && count_o == 16'd0)
underflow_o <= 1'b1;
else if (raise_i && !drop_i)
count_o <= count_o + 1;
else if (drop_i && !raise_i)
count_o <= count_o - 1;
// raise and drop in the same cycle cancel, which is why a
// component may re-raise before dropping without a gap.
if (raise_i && !drop_i) raised_once <= 1'b1;
if (count_o == 16'd0) begin
// Qualified on having been raised: the count is zero before
// anything objects, and reporting that as "the phase could
// have ended" is true but useless.
if (raised_once) ever_zero_o <= 1'b1;
// Sticky, because all_dropped_o is momentary: it clears again
// as soon as anything re-raises. A testbench that samples the
// momentary signal after the gap has closed sees nothing and
// concludes the phase was safe -- which is how a mutant that
// ignored the drain time survived the first campaign.
if (drain_q_o >= DRAIN) begin
all_dropped_o <= 1'b1;
if (raised_once) ever_ended_o <= 1'b1;
end
else drain_q_o <= drain_q_o + 1;
end else begin
drain_q_o <= 16'd0;
all_dropped_o <= 1'b0;
end
end
end
endmodule--===========================================================================
-- uart_objection — the OBJECTION MECHANISM, in VHDL-2008
--
-- NOT SYNTHESIZABLE. A verification component.
--
-- WHAT AN OBJECTION IS: a counter, and a rule about when it reaching zero
-- means the phase is over. raise_objection increments, drop_objection
-- decrements, and zero ends the phase. The complexity in UVM is in
-- propagation up the hierarchy and in drain time, not in the counting.
--
-- AND THE BUG THIS EXISTS TO MAKE VISIBLE: the count reaching zero BETWEEN
-- two pieces of work ends the test early.
--
-- -- sequence A finishes
-- phase.drop_objection(this); -- count 1 -> 0 TEST ENDS HERE
-- -- sequence B was going to start
-- phase.raise_objection(this); -- too late
--
-- Nothing is wrong with either call. The test stops in the gap, and it
-- presents as a suite that passes in a suspiciously short time with the
-- last few checks never reported. A passing run with missing checks looks
-- exactly like a passing run.
--
-- So this reports two things: whether the count is at zero now, and whether
-- it has RETURNED to zero having once been raised. The qualification
-- matters -- "has been at zero" is true on the first cycle after reset,
-- when nothing has objected yet, and a signal that is always true reports
-- nothing. The first draft got that wrong and the testbench caught it.
--
-- DRAIN TIME is the standard defence: hold the end of phase for N cycles
-- after the count reaches zero. It is a generic because it is a policy, and
-- because a testbench should be able to set it to zero and watch the
-- failure happen.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_objection is
generic (
DRAIN : natural := 0 -- cycles to wait before ending
);
port (
clk : in std_logic;
rst_n : in std_logic;
raise_i : in std_logic; -- raise_objection()
drop_i : in std_logic; -- drop_objection()
count_o : out natural;
all_dropped_o : out std_logic; -- the phase may end now
ever_zero_o : out std_logic; -- it has RETURNED to zero at least once
ever_ended_o : out std_logic; -- the phase was PERMITTED to end at least once
drain_q_o : out natural;
underflow_o : out std_logic -- a drop with nothing raised
);
end entity uart_objection;
architecture model of uart_objection is
signal cnt : natural := 0;
signal all_dropped : std_logic := '0';
signal ever_zero : std_logic := '0';
signal drain_q : natural := 0;
signal underflow : std_logic := '0';
signal ever_ended : std_logic := '0';
signal raised_once : std_logic := '0';
begin
obj : process (clk, rst_n)
begin
if rst_n = '0' then
cnt <= 0; all_dropped <= '0'; ever_zero <= '0'; ever_ended <= '0';
drain_q <= 0; underflow <= '0'; raised_once <= '0';
elsif rising_edge(clk) then
-- A drop with no matching raise is a testbench bug, not a phase
-- ending. UVM reports it; so does this.
if drop_i = '1' and raise_i = '0' and cnt = 0 then
underflow <= '1';
elsif raise_i = '1' and drop_i = '0' then
cnt <= cnt + 1;
elsif drop_i = '1' and raise_i = '0' then
cnt <= cnt - 1;
end if;
-- raise and drop in the same cycle cancel, which is why a
-- component may re-raise before dropping without a gap.
if raise_i = '1' and drop_i = '0' then
raised_once <= '1';
end if;
if cnt = 0 then
if raised_once = '1' then
ever_zero <= '1';
end if;
-- Sticky, because all_dropped is momentary: it clears again
-- as soon as anything re-raises. A testbench that samples the
-- momentary signal after the gap has closed sees nothing and
-- concludes the phase was safe -- which is how a mutant that
-- ignored the drain time survived the first campaign.
if drain_q >= DRAIN then
all_dropped <= '1';
if raised_once = '1' then ever_ended <= '1'; end if;
else
drain_q <= drain_q + 1;
end if;
else
drain_q <= 0;
all_dropped <= '0';
end if;
end if;
end process obj;
count_o <= cnt;
all_dropped_o <= all_dropped;
ever_zero_o <= ever_zero;
ever_ended_o <= ever_ended;
drain_q_o <= drain_q;
underflow_o <= underflow;
end architecture model;The testbench instantiates two, identical except for their drain time, and drives both with the same stimulus:
//===========================================================================
// tb_uart_objection_v — self-checking Verilog-2001 testbench
//
// Two instances, identical except for their drain time, because the whole
// point of drain time is what it changes:
//
// obj_nodrain (DRAIN = 0) ends the phase the instant the count hits zero
// obj_drain (DRAIN = 8) holds on for eight cycles first
//
// The suite drives both with the SAME stimulus and shows them disagreeing
// on exactly one sequence: a momentary gap between one sequence dropping
// its objection and the next raising one. That gap ends the test in the
// first instance and does not in the second.
//
// That is the bug this component exists to make visible, and it presents in
// the worst possible way -- as a suite that passes quickly, with the last
// few checks simply never reported. A passing run with missing checks looks
// exactly like a passing run.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_objection_v;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b0;
reg raise = 1'b0, drop = 1'b0;
wire [15:0] c_nd, c_d, dq_nd, dq_d;
wire ad_nd, ad_d, ez_nd, ez_d, uf_nd, uf_d, ee_nd, ee_d;
uart_objection_v #(.DRAIN(0)) obj_nodrain (
.clk(clk), .rst_n(rst_n), .raise_i(raise), .drop_i(drop),
.count_o(c_nd), .all_dropped_o(ad_nd), .ever_zero_o(ez_nd),
.ever_ended_o(ee_nd), .drain_q_o(dq_nd), .underflow_o(uf_nd));
uart_objection_v #(.DRAIN(8)) obj_drain (
.clk(clk), .rst_n(rst_n), .raise_i(raise), .drop_i(drop),
.count_o(c_d), .all_dropped_o(ad_d), .ever_zero_o(ez_d),
.ever_ended_o(ee_d), .drain_q_o(dq_d), .underflow_o(uf_d));
integer checks = 0, failures = 0;
task check;
input cond;
input [8*80-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
integer i;
task do_raise; begin @(negedge clk) raise = 1'b1; @(negedge clk) raise = 1'b0; end endtask
task do_drop; begin @(negedge clk) drop = 1'b1; @(negedge clk) drop = 1'b0; end endtask
task reset_both;
begin
@(negedge clk) rst_n = 1'b0; raise = 1'b0; drop = 1'b0;
repeat (3) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
end
endtask
initial begin
#10_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG OBJECTION TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_objection_v : self-checking Verilog testbench ==");
reset_both;
//=== the counter ====================================================
check(c_nd == 0 && c_d == 0, "reset: the objection count is zero");
check(uf_nd === 1'b0, "and no underflow is reported");
do_raise;
repeat (2) @(negedge clk);
check(c_nd == 1, "raise_objection increments the count");
check(ad_nd === 1'b0, "and the phase is no longer allowed to end");
do_raise; do_raise;
repeat (2) @(negedge clk);
check(c_nd == 3, "three components may object at once");
do_drop; do_drop;
repeat (2) @(negedge clk);
check(c_nd == 1, "each drop decrements it");
check(ad_nd === 1'b0, "and one outstanding objection still holds the phase");
do_drop;
repeat (2) @(negedge clk);
check(c_nd == 0, "the last drop returns it to zero");
check(ad_nd === 1'b1, "and with no drain time the phase may end at once");
//=== raise and drop in the same cycle cancel ========================
reset_both;
do_raise;
repeat (2) @(negedge clk);
@(negedge clk) raise = 1'b1; drop = 1'b1;
@(negedge clk) raise = 1'b0; drop = 1'b0;
repeat (2) @(negedge clk);
check(c_nd == 1,
"a raise and a drop in the same cycle cancel -- the count holds");
check(ez_nd === 1'b0,
"so the count NEVER touched zero, and no phase end was possible");
//=== an unmatched drop is a testbench bug, not a phase end ==========
reset_both;
do_drop;
repeat (2) @(negedge clk);
check(uf_nd === 1'b1, "a drop with nothing raised is reported as underflow");
check(c_nd == 0, "and does not wrap the counter below zero");
//=== THE BUG: a gap between two sequences ===========================
// Sequence A finishes and drops. Sequence B raises one cycle later.
// Nothing is wrong with either call.
reset_both;
do_raise; // sequence A starts
repeat (4) @(negedge clk);
do_drop; // sequence A finishes
repeat (3) @(negedge clk); // <-- the gap
do_raise; // sequence B starts
repeat (4) @(negedge clk);
check(c_nd == 1, "after the gap the count is back up -- work continues");
check(ez_nd === 1'b1,
"but with DRAIN=0 the count REACHED ZERO: the test would have ended");
check(ez_d === 1'b1, "the drained instance saw the same zero...");
// STICKY, not momentary. all_dropped_o clears again the instant
// anything re-raises, so sampling it after the gap has closed sees
// nothing at all -- and a mutant that ignored the drain time survived
// the first campaign for exactly that reason.
check(ee_d === 1'b0,
"...but never allowed the phase to end, because of the drain time");
check(ee_nd === 1'b1,
"the undrained instance permitted an end inside the gap");
//=== and a gap LONGER than the drain still ends the phase ===========
reset_both;
do_raise;
repeat (3) @(negedge clk);
do_drop;
repeat (20) @(negedge clk); // longer than DRAIN=8
check(ad_d === 1'b1,
"a gap longer than the drain time does end the phase -- as it must");
check(c_d == 0, "with the count genuinely at zero");
//=== the two instances agree whenever there is no gap ===============
reset_both;
do_raise;
for (i = 0; i < 10; i = i + 1) begin
@(negedge clk) raise = 1'b1; drop = 1'b1; // hand over with no gap
@(negedge clk) raise = 1'b0; drop = 1'b0;
repeat (2) @(negedge clk);
end
check(ez_nd === 1'b0 && ez_d === 1'b0,
"ten hand-overs with no gap: neither instance ever saw zero");
check(ee_nd === 1'b0 && ee_d === 1'b0,
"and neither would have ended the test");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG OBJECTION TESTS PASSED");
else $display(" RESULT: VERILOG OBJECTION TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_objection — self-checking SystemVerilog testbench
//
// Two instances, identical except for their drain time, because the whole
// point of drain time is what it changes:
//
// obj_nodrain (DRAIN = 0) ends the phase the instant the count hits zero
// obj_drain (DRAIN = 8) holds on for eight cycles first
//
// The suite drives both with the SAME stimulus and shows them disagreeing
// on exactly one sequence: a momentary gap between one sequence dropping
// its objection and the next raising one. That gap ends the test in the
// first instance and does not in the second.
//
// That is the bug this component exists to make visible, and it presents in
// the worst possible way -- as a suite that passes quickly, with the last
// few checks simply never reported. A passing run with missing checks looks
// exactly like a passing run.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_objection;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n = 1'b0;
logic raise = 1'b0, drop = 1'b0;
wire [15:0] c_nd, c_d, dq_nd, dq_d;
wire ad_nd, ad_d, ez_nd, ez_d, uf_nd, uf_d, ee_nd, ee_d;
uart_objection #(.DRAIN(0)) obj_nodrain (
.clk(clk), .rst_n(rst_n), .raise_i(raise), .drop_i(drop),
.count_o(c_nd), .all_dropped_o(ad_nd), .ever_zero_o(ez_nd),
.ever_ended_o(ee_nd), .drain_q_o(dq_nd), .underflow_o(uf_nd));
uart_objection #(.DRAIN(8)) obj_drain (
.clk(clk), .rst_n(rst_n), .raise_i(raise), .drop_i(drop),
.count_o(c_d), .all_dropped_o(ad_d), .ever_zero_o(ez_d),
.ever_ended_o(ee_d), .drain_q_o(dq_d), .underflow_o(uf_d));
int checks = 0, failures = 0;
task automatic check(input logic cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
int i;
task do_raise; begin @(negedge clk) raise = 1'b1; @(negedge clk) raise = 1'b0; end endtask
task do_drop; begin @(negedge clk) drop = 1'b1; @(negedge clk) drop = 1'b0; end endtask
task reset_both;
begin
@(negedge clk) rst_n = 1'b0; raise = 1'b0; drop = 1'b0;
repeat (3) @(negedge clk);
rst_n = 1'b1;
repeat (2) @(negedge clk);
end
endtask
initial begin
#10_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG OBJECTION TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_objection : self-checking Verilog testbench ==");
reset_both;
//=== the counter ====================================================
check(c_nd == 0 && c_d == 0, "reset: the objection count is zero");
check(uf_nd === 1'b0, "and no underflow is reported");
do_raise;
repeat (2) @(negedge clk);
check(c_nd == 1, "raise_objection increments the count");
check(ad_nd === 1'b0, "and the phase is no longer allowed to end");
do_raise; do_raise;
repeat (2) @(negedge clk);
check(c_nd == 3, "three components may object at once");
do_drop; do_drop;
repeat (2) @(negedge clk);
check(c_nd == 1, "each drop decrements it");
check(ad_nd === 1'b0, "and one outstanding objection still holds the phase");
do_drop;
repeat (2) @(negedge clk);
check(c_nd == 0, "the last drop returns it to zero");
check(ad_nd === 1'b1, "and with no drain time the phase may end at once");
//=== raise and drop in the same cycle cancel ========================
reset_both;
do_raise;
repeat (2) @(negedge clk);
@(negedge clk) raise = 1'b1; drop = 1'b1;
@(negedge clk) raise = 1'b0; drop = 1'b0;
repeat (2) @(negedge clk);
check(c_nd == 1,
"a raise and a drop in the same cycle cancel -- the count holds");
check(ez_nd === 1'b0,
"so the count NEVER touched zero, and no phase end was possible");
//=== an unmatched drop is a testbench bug, not a phase end ==========
reset_both;
do_drop;
repeat (2) @(negedge clk);
check(uf_nd === 1'b1, "a drop with nothing raised is reported as underflow");
check(c_nd == 0, "and does not wrap the counter below zero");
//=== THE BUG: a gap between two sequences ===========================
// Sequence A finishes and drops. Sequence B raises one cycle later.
// Nothing is wrong with either call.
reset_both;
do_raise; // sequence A starts
repeat (4) @(negedge clk);
do_drop; // sequence A finishes
repeat (3) @(negedge clk); // <-- the gap
do_raise; // sequence B starts
repeat (4) @(negedge clk);
check(c_nd == 1, "after the gap the count is back up -- work continues");
check(ez_nd === 1'b1,
"but with DRAIN=0 the count REACHED ZERO: the test would have ended");
check(ez_d === 1'b1, "the drained instance saw the same zero...");
// STICKY, not momentary. all_dropped_o clears again the instant
// anything re-raises, so sampling it after the gap has closed sees
// nothing at all -- and a mutant that ignored the drain time survived
// the first campaign for exactly that reason.
check(ee_d === 1'b0,
"...but never allowed the phase to end, because of the drain time");
check(ee_nd === 1'b1,
"the undrained instance permitted an end inside the gap");
//=== and a gap LONGER than the drain still ends the phase ===========
reset_both;
do_raise;
repeat (3) @(negedge clk);
do_drop;
repeat (20) @(negedge clk); // longer than DRAIN=8
check(ad_d === 1'b1,
"a gap longer than the drain time does end the phase -- as it must");
check(c_d == 0, "with the count genuinely at zero");
//=== the two instances agree whenever there is no gap ===============
reset_both;
do_raise;
for (i = 0; i < 10; i = i + 1) begin
@(negedge clk) raise = 1'b1; drop = 1'b1; // hand over with no gap
@(negedge clk) raise = 1'b0; drop = 1'b0;
repeat (2) @(negedge clk);
end
check(ez_nd === 1'b0 && ez_d === 1'b0,
"ten hand-overs with no gap: neither instance ever saw zero");
check(ee_nd === 1'b0 && ee_d === 1'b0,
"and neither would have ended the test");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG OBJECTION TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG OBJECTION TESTS FAILED");
$finish;
end
endmodule--===========================================================================
-- tb_uart_objection — self-checking VHDL-2008 testbench
--
-- Two instances, identical except for their drain time, because the whole
-- point of drain time is what it changes:
--
-- obj_nodrain (DRAIN = 0) ends the phase the instant the count hits zero
-- obj_drain (DRAIN = 8) holds on for eight cycles first
--
-- Both are driven with the SAME stimulus and shown disagreeing on exactly
-- one sequence: a momentary gap between one sequence dropping its objection
-- and the next raising one. That gap ends the test in the first instance
-- and does not in the second.
--
-- Same 22 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_objection is
end entity tb_uart_objection;
architecture sim of tb_uart_objection is
constant TCLK : time := 10 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal rais, drop : std_logic := '0';
signal c_nd, c_d, dq_nd, dq_d : natural;
signal ad_nd, ad_d, ez_nd, ez_d, uf_nd, uf_d : std_logic;
signal ee_nd, ee_d : std_logic;
begin
clk <= not clk after TCLK/2 when not done else '0';
obj_nodrain : entity work.uart_objection
generic map (DRAIN => 0)
port map (clk => clk, rst_n => rst_n, raise_i => rais, drop_i => drop,
count_o => c_nd, all_dropped_o => ad_nd, ever_zero_o => ez_nd,
ever_ended_o => ee_nd, drain_q_o => dq_nd, underflow_o => uf_nd);
obj_drain : entity work.uart_objection
generic map (DRAIN => 8)
port map (clk => clk, rst_n => rst_n, raise_i => rais, drop_i => drop,
count_o => c_d, all_dropped_o => ad_d, ever_zero_o => ez_d,
ever_ended_o => ee_d, drain_q_o => dq_d, underflow_o => uf_d);
watchdog : process
begin
wait for 10 ms;
report "watchdog: simulation did not finish" severity failure;
end process watchdog;
stim : process
variable checks, failures : natural := 0;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then report " PASS " & name severity note;
else failures := failures + 1; report " FAIL " & name severity error;
end if;
end procedure check;
procedure do_raise is
begin
wait until falling_edge(clk); rais <= '1';
wait until falling_edge(clk); rais <= '0';
end procedure do_raise;
procedure do_drop is
begin
wait until falling_edge(clk); drop <= '1';
wait until falling_edge(clk); drop <= '0';
end procedure do_drop;
procedure reset_both is
begin
wait until falling_edge(clk);
rst_n <= '0'; rais <= '0'; drop <= '0';
for i in 1 to 3 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
end procedure reset_both;
begin
report "== uart_objection : self-checking VHDL testbench ==" severity note;
reset_both;
--=== the counter ====================================================
check(c_nd = 0 and c_d = 0, "reset: the objection count is zero");
check(uf_nd = '0', "and no underflow is reported");
do_raise;
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(c_nd = 1, "raise_objection increments the count");
check(ad_nd = '0', "and the phase is no longer allowed to end");
do_raise; do_raise;
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(c_nd = 3, "three components may object at once");
do_drop; do_drop;
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(c_nd = 1, "each drop decrements it");
check(ad_nd = '0', "and one outstanding objection still holds the phase");
do_drop;
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(c_nd = 0, "the last drop returns it to zero");
check(ad_nd = '1', "and with no drain time the phase may end at once");
--=== raise and drop in the same cycle cancel ========================
reset_both;
do_raise;
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
wait until falling_edge(clk); rais <= '1'; drop <= '1';
wait until falling_edge(clk); rais <= '0'; drop <= '0';
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(c_nd = 1,
"a raise and a drop in the same cycle cancel -- the count holds");
check(ez_nd = '0',
"so the count NEVER touched zero, and no phase end was possible");
--=== an unmatched drop is a testbench bug, not a phase end ==========
reset_both;
do_drop;
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(uf_nd = '1', "a drop with nothing raised is reported as underflow");
check(c_nd = 0, "and does not wrap the counter below zero");
--=== THE BUG: a gap between two sequences ===========================
reset_both;
do_raise; -- sequence A starts
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
do_drop; -- sequence A finishes
for i in 1 to 3 loop wait until falling_edge(clk); end loop; -- the gap
do_raise; -- sequence B starts
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
check(c_nd = 1, "after the gap the count is back up -- work continues");
check(ez_nd = '1',
"but with DRAIN=0 the count REACHED ZERO: the test would have ended");
check(ez_d = '1', "the drained instance saw the same zero...");
-- STICKY, not momentary: all_dropped clears again the instant
-- anything re-raises, so sampling it after the gap has closed sees
-- nothing -- and a mutant that ignored the drain time survived the
-- first campaign for exactly that reason.
check(ee_d = '0',
"...but never allowed the phase to end, because of the drain time");
check(ee_nd = '1',
"the undrained instance permitted an end inside the gap");
--=== and a gap LONGER than the drain still ends the phase ===========
reset_both;
do_raise;
for i in 1 to 3 loop wait until falling_edge(clk); end loop;
do_drop;
for i in 1 to 20 loop wait until falling_edge(clk); end loop;
check(ad_d = '1',
"a gap longer than the drain time does end the phase -- as it must");
check(c_d = 0, "with the count genuinely at zero");
--=== the two instances agree whenever there is no gap ===============
reset_both;
do_raise;
for i in 1 to 10 loop
wait until falling_edge(clk); rais <= '1'; drop <= '1';
wait until falling_edge(clk); rais <= '0'; drop <= '0';
for k in 1 to 2 loop wait until falling_edge(clk); end loop;
end loop;
check(ez_nd = '0' and ez_d = '0',
"ten hand-overs with no gap: neither instance ever saw zero");
check(ee_nd = '0' and ee_d = '0',
"and neither would have ended the test");
report "== " & integer'image(checks) & " checks, "
& integer'image(failures) & " failures ==" severity note;
if failures = 0 then
report " RESULT: ALL VHDL OBJECTION TESTS PASSED" severity note;
else
report " RESULT: VHDL OBJECTION TESTS FAILED" severity error;
end if;
done <= true;
wait;
end process stim;
end architecture sim; PASS a raise and a drop in the same cycle cancel -- the count holds
PASS so the count NEVER touched zero, and no phase end was possible
PASS a drop with nothing raised is reported as underflow
PASS after the gap the count is back up -- work continues
PASS but with DRAIN=0 the count REACHED ZERO: the test would have ended
PASS the drained instance saw the same zero...
PASS ...but never allowed the phase to end, because of the drain time
PASS a gap longer than the drain time does end the phase -- as it must
PASS ten hand-overs with no gap: neither instance ever saw zero
== 22 checks, 0 failures ==
Verilog-2001 : 22 checks, 0 failures
SystemVerilog : 22 checks, 0 failures
VHDL-2008 : 22 checks, 0 failures5. Module 16 Verification Evidence
The UVM classes in this module were not executed — no uvm_pkg on this machine, and Icarus Verilog 13.0 lacks $cast, parameterised classes, mailboxes, class-handle queues and working virtual dispatch, each probed individually (16.1 §5).
The mechanisms UVM is built from were executed, in three languages, like every other module in this curriculum.
| Component | What it is in UVM | SV | Verilog-2001 | VHDL-2008 | Chapter |
|---|---|---|---|---|---|
uart_seq_arb | uvm_sequencer | ✅ | ✅ | ✅ | 16.3 |
uart_analysis_bus | uvm_analysis_port | ✅ | ✅ | ✅ | 16.4 |
uart_objection | uvm_objection | ✅ | ✅ | ✅ | 16.5 |
| Suite | Checks | Verilog-2001 | SystemVerilog | VHDL-2008 |
|---|---|---|---|---|
uart_seq_arb | 20 | 20 / 0 | 20 / 0 | 20 / 0 |
uart_analysis_bus | 17 | 17 / 0 | 17 / 0 | 17 / 0 |
uart_objection | 22 | 22 / 0 | 22 / 0 | 22 / 0 |
| Total per language | 59 | 59 / 0 | 59 / 0 | 59 / 0 |
177 checks across the three languages, 0 failures. Tooling: Icarus Verilog 13.0 (-g2001, -g2012) and NVC 1.23.0.
Mutation campaign — twelve defects, twelve killed:
| # | Component | Defect installed | Killed by |
|---|---|---|---|
| M1 | sequencer | fixed priority — the pointer never moves | 3 |
| M2 | sequencer | item released without waiting for item_done | 8 |
| M3 | sequencer | grant held instead of pulsed | 6 |
| M4 | sequencer | pointer set to pick, not pick + 1 | 3 |
| M5 | analysis | only subscriber 0 receives the write | 7 |
| M6 | analysis | one subscriber receives a stale item | 2 |
| M7 | analysis | the producer's count is not incremented | 3 |
| M8 | objection | drop does not decrement | 9 |
| M9 | objection | all_dropped ignores the drain time | survived, then killed |
| M10 | objection | ever_zero not qualified by having been raised | 2 |
| M11 | objection | raise and drop in the same cycle increments | 1 |
| M12 | objection | an unmatched drop is not reported | 1 |
M9 is the module's result. It survived because the suite sampled a momentary signal after it had cleared; a sticky observation killed it, and the same class of error had already been made once in the same file.
6. Verification
Assert that the scoreboard compared something. UVM adds three new ways to connect nothing, and none of them produces an error.
Report the field that differed, not "frames differ". The reader should not be diffing hex strings by eye.
Keep write() to one or two statements. 16.4 §3 measured what slow work costs, and the loss is silent.
Read coverage as holes and respond with directed stimulus. 15.3 §6's defect survived everything because a bin had zero occurrences, not few.
Set a drain time. Zero is a defensible default only if every hand-over between sequences is gapless, and proving that is harder than setting the drain.
And make "did X ever happen" observations sticky. A momentary signal sampled at a convenient moment tests whether it was asserted then, which is a different question.
7. Debugging
8. Understanding Check
9. Summary
Three subscribers hang off one analysis port and none of them drives anything: the predictor predicts, the scoreboard compares, the coverage collector records.
The scoreboard holds a queue, compares field by field, and names the field that differed — and asserts that it compared something at all, because UVM supplies three silent ways to connect nothing.
Coverage is a loop, not a number. An empty bin is a task; 15.3 §6's defect survived everything because one bin had zero occurrences.
An objection is a counter and a rule about zero, and its failure mode — ending in the gap between two sequences — looks exactly like a pass.
A momentary signal needs a sticky observation. A mutant that removed the drain-time check entirely survived until the suite stopped sampling all_dropped and started asserting on ever_ended.
59 checks per language, 177 in total, 0 failures; twelve mutants, twelve killed.
And three properties of UVM that are usually described in prose are here as numbers: fairness at 125/124/125/125, an analysis port's cost at 30 of 60 transactions lost, and an objection's gap as two instances disagreeing on one sequence.
10. What Comes Next
Module 16 is complete, and with it the UVM layer. The environment has an agent that works actively or passively, a sequencer that is fair and holds, an analysis port that broadcasts without coupling, a scoreboard that cannot lie by silence, and an end-of-test mechanism whose failure mode is now visible.
Module 17 turns to debugging: reading a UART waveform as evidence, the signatures that identify a baud mismatch or a sampling error on sight, and working backwards from a symptom on a real link to the defect that produced it.
Browse the full path on the UART tutorials index. For the boundaries between predictor, scoreboard and monitor that this chapter connects, read back to Chapter 14.5.
Continue learning
Related tutorials
- Related topic
Senior Verification
Designing a UVM environment for a UCIe subsystem — why an environment only checks anything when the expected value comes from somewhere other than the thing being checked, how a mirrored predictor passes every test while verifying nothing, and the scoreboard structure that separates orphan, stale-generation, reallocation and mismatch into four distinct failures.
- Related topic
Verification Review Checklist
The DV signoff gate — why coverage of contracts beats coverage of code, how to audit a predictor for independence rather than trust it, the monitor and scoreboard defects that make a testbench agree with the design's bugs, and why twenty thousand passing tests at full code coverage can still be a FAIL.
- Related topic
Verification Checklist — Can This Environment Fail?
A predictor fed from the DUT's own output agrees with itself forever, and ten thousand passing tests prove nothing. The gate is whether every contract has independent stimulus, an independent checker, and a demonstrated failure signature.
- Related topic
Senior Verification Question
An environment is not a plan. This chapter builds the agent inventory, what a scoreboard actually checks, the coverage cross, what random stimulus reaches, checker independence, error injection, performance observability, honest reuse accounting and a written definition of done.
Where this fits
Part of the UART curriculum.
