Skip to content
VLSI Mentor

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

A diagram of a UVM scoreboard architecture with in-order matching. On one side a predictor, fed the same stimulus description that the driver received, produces an expected transaction stream. On the other side a monitor observes the interface and produces an observed transaction stream. Both streams arrive at the scoreboard through separate analysis exports. The scoreboard holds the expected stream in a queue and, as each observed transaction arrives, pops the oldest expected one and compares them field by field, reporting any difference with the name of the field that differed.monitorobserved (actual)predictor / refmodelexpected (golden)actual FIFOexpected FIFOcomparehead vs headpass / mismatchactual txnexpected txnpoppop12
Figure 1 — the scoreboard receiving from two sources. The predictor is fed the same stimulus description the driver was given, and the monitor reports what actually appeared on the wire. The scoreboard's only job is to pair them up; it contains no knowledge of frame format, because a third implementation of the specification would leave a disagreement with no arbiter.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass

2. The Coverage Collector

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass
A diagram of the functional coverage closure loop. Randomised stimulus is generated and driven at the design; the monitor observes the resulting transactions and the coverage collector samples them into bins. The coverage report is then read for holes rather than for a percentage. Each unfilled bin names a condition the stimulus never reached, which becomes a directed test written specifically to reach it. That directed stimulus is added to the suite and the loop repeats until the remaining holes are ones that have been deliberately excluded with a stated reason.yesrerunnoRun regressionMeasure coverageCoverage holes?Write targeted sequenceCoverage closed
Figure 2 — coverage closure as a loop rather than a number. An unfilled bin is a task: it names a condition the stimulus never reached, and the response is a directed test, not more random seeds.

Chapter 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass

An 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 late

Nothing 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  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;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 failures

5. 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.

ComponentWhat it is in UVMSVVerilog-2001VHDL-2008Chapter
uart_seq_arbuvm_sequencer✅✅✅16.3
uart_analysis_busuvm_analysis_port✅✅✅16.4
uart_objectionuvm_objection✅✅✅16.5
SuiteChecksVerilog-2001SystemVerilogVHDL-2008
uart_seq_arb2020 / 020 / 020 / 0
uart_analysis_bus1717 / 017 / 017 / 0
uart_objection2222 / 022 / 022 / 0
Total per language5959 / 059 / 059 / 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:

#ComponentDefect installedKilled by
M1sequencerfixed priority — the pointer never moves3
M2sequenceritem released without waiting for item_done8
M3sequencergrant held instead of pulsed6
M4sequencerpointer set to pick, not pick + 13
M5analysisonly subscriber 0 receives the write7
M6analysisone subscriber receives a stale item2
M7analysisthe producer's count is not incremented3
M8objectiondrop does not decrement9
M9objectionall_dropped ignores the drain timesurvived, then killed
M10objectionever_zero not qualified by having been raised2
M11objectionraise and drop in the same cycle increments1
M12objectionan unmatched drop is not reported1

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

Where this fits

Part of the UART curriculum.