Skip to content
VLSI Mentor

SPI · Module 17

A Reusable UVM SPI Agent

Three arbitration policies deliver identical items and identical functional coverage, and completely different traffic: round robin switches between sequences 23 times and strict priority once. A coverage report cannot tell you which one ran, so it cannot tell you whether two sequences ever overlapped.

Module 16 built four components and packaged them. This chapter assembles the whole environment, and then measures the one piece of it whose behaviour surprises people most.

Three arbitration policies. Identical items, identical coverage, and traffic so different that a concurrency bug is reachable under one and unreachable under another.

1. The Environment

uvm_testAgent(active)UVM_ACTIVESequenceruvm_sequencerDriveruvm_driverMonitoruvm_monitorvifvirtual interface · bridges testbench to DUTScoreboarduvm_scoreboardPredictorexpectedComparatoractual ≟ expecteduvm_analysis_impwrite(txn)DUTDesign Under TestRTL — Verilog / SystemVerilogseq_itemDriveto DUTSamplefrom DUTanalysis_portwrite(txn)drive (Driver → vif → DUT)sample (DUT → vif → Monitor)analysis (TLM)TLM seq_item
Figure 1 — the environment this module has been building, in its UVM shape. The sequencer arbitrates between sequences and hands one item at a time to the driver; the driver drives the pins through the virtual interface with the sampling discipline of Chapter 16.3; the monitor observes the same interface and reconstructs transactions from pins alone (Chapter 16.5); and its analysis port feeds the scoreboard, whose prediction comes from the independent reference model of Chapter 16.6. The agent is the boundary that makes all of it one line to instantiate — and the passive form of it, from Chapter 16.7, contains only the monitor.

Every block in that picture has already been built and measured. What is new here is the box that has not been examined yet.

2. What A Sequencer Promises

A sequencer stands between N sequences and one driver, and it makes exactly one promise:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   within a sequence, items are delivered in the order the sequence produced them

It promises nothing about the order across sequences. That is not a gap in the specification — it is the whole point. The interleaving is chosen by an arbitration policy, and changing the policy changes which concurrency bugs a test can reach without changing a single item.

Two sequences feeding two per-sequence queues, an arbiter selecting between them under one of three policies, a single driver handshake, and counters for items delivered and longest consecutive waitsequence 0sequence 1queue 0queue 1policyarbiterdrivercounters12
Figure 2 — the sequencer as two queues and one decision. Each sequence has its own queue, which is what keeps the promise: a queue preserves order within a sequence whatever the arbiter does. The policy decides whose turn it is and the queue decides which item, so a policy change can never reorder a sequence — and the starvation counter is an output rather than something a test has to infer.

3. Three Policies, One Set Of Items

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ARB_RR        round robin. Each sequence with an item pending gets the next
                 grant in turn.
   ARB_PRIO      strict priority. Sequence 0 wins whenever it has an item pending.
   ARB_WEIGHTED  sequence 0 wins three grants in four.

4. The Measurement

Twelve items from each sequence, both queues full before the first grant — because an interleaving experiment in which one sequence arrives late measures the arrival times rather than the policy.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   policy            from s0   from s1   order errors   switches   s1 longest wait
   round robin            12        12              0         23            1
   strict priority        12        12              0          1           12
   weighted 3:1           12        12              0          7            3

Zero order errors under all three. That is the promise, and it holds because the policy decides whose turn it is while the per-sequence queue decides which item.

All three delivered every item. So every functional coverage number is identical across the three runs — which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped.

And the traffic is completely different. Round robin switched between the streams 23 times; strict priority switched once. Under priority, sequence 1's items all arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   "we ran both sequences"  and  "we ran both sequences together"
   are different claims

5. Building It — Three HDLs

The sequencer is the part of a UVM environment that can be built and measured in a plain simulator, and this is that build. The class form of the whole agent is in section 7.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_sequencer.sv — the sequencer, with two per-sequence queues and three arbitration policies
// spi_sequencer.sv
//
// Chapter 17.5 -- the agent's sequencer, which is the part of a UVM environment that can be built
// and measured in a plain simulator, and the part whose behaviour surprises people most.
//
// WHAT A SEQUENCER IS FOR.
//
// A sequence produces items in an order that means something -- configure, then a burst, then a
// read-back. A driver consumes one item at a time. A sequencer stands between N sequences and one
// driver, and it makes exactly ONE promise:
//
//     within a sequence, items are delivered in the order the sequence produced them
//
// It promises nothing at all about the order ACROSS sequences, and that is not a gap in the
// specification. It is the whole point: the interleaving is chosen by an arbitration policy, and
// changing the policy changes which concurrency bugs a test can reach without changing a single
// item.
//
// THREE POLICIES, because the differences between them are invisible in a coverage report.
//
//   ARB_RR        round robin. Each sequence with an item pending gets the next grant in turn.
//   ARB_PRIO      strict priority. Sequence 0 wins whenever it has an item pending.
//   ARB_WEIGHTED  sequence 0 wins three grants in four.
//
// All three deliver the same items. All three produce identical functional coverage. They differ
// only in INTERLEAVING -- and a design bug that needs two sequences to overlap is reachable under
// one policy and unreachable under another, which is why "we ran both sequences" is not the same
// statement as "we ran both sequences together".
//
// AND STARVATION IS A FIRST-CLASS OUTPUT HERE.
//
// Strict priority starves the lower sequence for as long as the higher one has work. That is
// correct behaviour and it is also the most common way a test does less than its author thinks:
// the low-priority sequence's items all arrive after the high-priority sequence has finished, so
// the two never overlap and every interleaving bug is unreachable. A sequencer that does not
// COUNT this has no way to tell the author.

`timescale 1ns/1ps

module spi_sequencer #(
    parameter int NSEQ  = 2,
    parameter int DEPTH = 16,
    parameter int DW    = 32,
    parameter int CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire [1:0]        arb,        // 0 = round robin, 1 = strict priority, 2 = weighted

    // Sequence 0 and sequence 1 push items. Each has its own queue, which is what keeps the
    // one promise: a queue preserves order within a sequence whatever the arbiter does.
    input  wire              s0_push,
    input  wire [DW-1:0]     s0_item,
    input  wire              s1_push,
    input  wire [DW-1:0]     s1_item,
    output wire              s0_full,
    output wire              s1_full,

    // The driver side: one item at a time.
    input  wire              drv_ready,
    output reg               drv_valid,
    output reg  [DW-1:0]     drv_item,
    output reg               drv_from,   // which sequence the item came from

    output reg  [CNT_W-1:0]  n_from_s0,
    output reg  [CNT_W-1:0]  n_from_s1,
    // THE LONGEST RUN of consecutive grants sequence 1 waited through while it had an item
    // pending -- not the total.
    //
    // The total is the wrong measure and the difference is instructive. With both queues full,
    // EVERY grant to sequence 0 is a grant sequence 1 did not get, so the total is the length of
    // sequence 0 under every policy including round robin. The number that distinguishes the
    // policies is how many in a ROW: one under round robin, three under a 3:1 weighting, and all
    // of sequence 0 under strict priority. A starvation metric that cannot tell round robin from
    // strict priority is measuring queue occupancy, not starvation.
    output reg  [CNT_W-1:0]  s1_max_wait,
    output reg  [CNT_W-1:0]  s0_max_wait
);

    localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;

    reg [DW-1:0] q0 [0:DEPTH-1];
    reg [DW-1:0] q1 [0:DEPTH-1];
    reg [CNT_W-1:0] h0, t0, d0;
    reg [CNT_W-1:0] h1, t1, d1;

    assign s0_full = (d0 == DEPTH[CNT_W-1:0]);
    assign s1_full = (d1 == DEPTH[CNT_W-1:0]);

    wire have0 = (d0 != {CNT_W{1'b0}});
    wire have1 = (d1 != {CNT_W{1'b0}});

    reg last_grant;      // for round robin
    reg [1:0] w_phase;   // for the weighted policy: three of four grants to sequence 0

    // The current run of consecutive grants each sequence has been passed over for.
    reg [CNT_W-1:0] s0_run, s1_run;

    // The arbitration decision, as one expression per policy. Nothing about the QUEUES appears
    // here, which is the separation that matters: the policy decides WHOSE turn it is and the
    // queues decide WHICH item, so a policy change can never reorder a sequence.
    wire pick0 =
        (arb == ARB_PRIO)     ? have0 :
        (arb == ARB_WEIGHTED) ? (have0 && (w_phase != 2'd3 || !have1)) :
        /* ARB_RR */            (have0 && (!have1 || last_grant == 1'b1));

    wire grant = have0 | have1;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            h0 <= {CNT_W{1'b0}}; t0 <= {CNT_W{1'b0}}; d0 <= {CNT_W{1'b0}};
            h1 <= {CNT_W{1'b0}}; t1 <= {CNT_W{1'b0}}; d1 <= {CNT_W{1'b0}};
            drv_valid  <= 1'b0;
            drv_item   <= {DW{1'b0}};
            drv_from   <= 1'b0;
            n_from_s0  <= {CNT_W{1'b0}};
            n_from_s1  <= {CNT_W{1'b0}};
            s1_max_wait <= {CNT_W{1'b0}};
            s0_max_wait <= {CNT_W{1'b0}};
            s1_run      <= {CNT_W{1'b0}};
            s0_run      <= {CNT_W{1'b0}};
            last_grant <= 1'b1;
            w_phase    <= 2'd0;
        end else begin
            drv_valid <= 1'b0;

            if (s0_push && !s0_full) begin
                q0[t0] <= s0_item;
                t0 <= (t0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t0 + 1'b1;
                d0 <= d0 + 1'b1;
            end
            if (s1_push && !s1_full) begin
                q1[t1] <= s1_item;
                t1 <= (t1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t1 + 1'b1;
                d1 <= d1 + 1'b1;
            end

            if (drv_ready && grant) begin
                if (pick0) begin
                    drv_item  <= q0[h0];
                    drv_from  <= 1'b0;
                    drv_valid <= 1'b1;
                    h0 <= (h0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h0 + 1'b1;
                    d0 <= (s0_push && !s0_full) ? d0 : d0 - 1'b1;
                    n_from_s0  <= n_from_s0 + 1'b1;
                    last_grant <= 1'b0;
                    // STARVATION IS COUNTED AT THE GRANT, not inferred afterwards. Sequence 1 had
                    // an item pending and did not get this grant, so its run grows; sequence 0
                    // just got one, so its run ends.
                    if (have1) begin
                        s1_run <= s1_run + 1'b1;
                        if (s1_run + 1'b1 > s1_max_wait) s1_max_wait <= s1_run + 1'b1;
                    end
                    s0_run <= {CNT_W{1'b0}};
                end else begin
                    drv_item  <= q1[h1];
                    drv_from  <= 1'b1;
                    drv_valid <= 1'b1;
                    h1 <= (h1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h1 + 1'b1;
                    d1 <= (s1_push && !s1_full) ? d1 : d1 - 1'b1;
                    n_from_s1  <= n_from_s1 + 1'b1;
                    last_grant <= 1'b1;
                    if (have0) begin
                        s0_run <= s0_run + 1'b1;
                        if (s0_run + 1'b1 > s0_max_wait) s0_max_wait <= s0_run + 1'b1;
                    end
                    s1_run <= {CNT_W{1'b0}};
                end
                w_phase <= (w_phase == 2'd3) ? 2'd0 : w_phase + 2'd1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sequencer.v — the same design in Verilog-2001
// spi_sequencer.v
//
// Chapter 17.5 -- the agent's sequencer, which is the part of a UVM environment that can be built
// and measured in a plain simulator, and the part whose behaviour surprises people most.
//
// WHAT A SEQUENCER IS FOR.
//
// A sequence produces items in an order that means something -- configure, then a burst, then a
// read-back. A driver consumes one item at a time. A sequencer stands between N sequences and one
// driver, and it makes exactly ONE promise:
//
//     within a sequence, items are delivered in the order the sequence produced them
//
// It promises nothing at all about the order ACROSS sequences, and that is not a gap in the
// specification. It is the whole point: the interleaving is chosen by an arbitration policy, and
// changing the policy changes which concurrency bugs a test can reach without changing a single
// item.
//
// THREE POLICIES, because the differences between them are invisible in a coverage report.
//
//   ARB_RR        round robin. Each sequence with an item pending gets the next grant in turn.
//   ARB_PRIO      strict priority. Sequence 0 wins whenever it has an item pending.
//   ARB_WEIGHTED  sequence 0 wins three grants in four.
//
// All three deliver the same items. All three produce identical functional coverage. They differ
// only in INTERLEAVING -- and a design bug that needs two sequences to overlap is reachable under
// one policy and unreachable under another, which is why "we ran both sequences" is not the same
// statement as "we ran both sequences together".
//
// AND STARVATION IS A FIRST-CLASS OUTPUT HERE.
//
// Strict priority starves the lower sequence for as long as the higher one has work. That is
// correct behaviour and it is also the most common way a test does less than its author thinks:
// the low-priority sequence's items all arrive after the high-priority sequence has finished, so
// the two never overlap and every interleaving bug is unreachable. A sequencer that does not
// COUNT this has no way to tell the author.

`timescale 1ns/1ps

module spi_sequencer #(
    parameter NSEQ  = 2,
    parameter DEPTH = 16,
    parameter DW    = 32,
    parameter CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire [1:0]        arb,        // 0 = round robin, 1 = strict priority, 2 = weighted

    // Sequence 0 and sequence 1 push items. Each has its own queue, which is what keeps the
    // one promise: a queue preserves order within a sequence whatever the arbiter does.
    input  wire              s0_push,
    input  wire [DW-1:0]     s0_item,
    input  wire              s1_push,
    input  wire [DW-1:0]     s1_item,
    output wire              s0_full,
    output wire              s1_full,

    // The driver side: one item at a time.
    input  wire              drv_ready,
    output reg               drv_valid,
    output reg  [DW-1:0]     drv_item,
    output reg               drv_from,   // which sequence the item came from

    output reg  [CNT_W-1:0]  n_from_s0,
    output reg  [CNT_W-1:0]  n_from_s1,
    // THE LONGEST RUN of consecutive grants sequence 1 waited through while it had an item
    // pending -- not the total.
    //
    // The total is the wrong measure and the difference is instructive. With both queues full,
    // EVERY grant to sequence 0 is a grant sequence 1 did not get, so the total is the length of
    // sequence 0 under every policy including round robin. The number that distinguishes the
    // policies is how many in a ROW: one under round robin, three under a 3:1 weighting, and all
    // of sequence 0 under strict priority. A starvation metric that cannot tell round robin from
    // strict priority is measuring queue occupancy, not starvation.
    output reg  [CNT_W-1:0]  s1_max_wait,
    output reg  [CNT_W-1:0]  s0_max_wait
);

    localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;

    reg [DW-1:0] q0 [0:DEPTH-1];
    reg [DW-1:0] q1 [0:DEPTH-1];
    reg [CNT_W-1:0] h0, t0, d0;
    reg [CNT_W-1:0] h1, t1, d1;

    assign s0_full = (d0 == DEPTH[CNT_W-1:0]);
    assign s1_full = (d1 == DEPTH[CNT_W-1:0]);

    wire have0 = (d0 != {CNT_W{1'b0}});
    wire have1 = (d1 != {CNT_W{1'b0}});

    reg last_grant;      // for round robin
    reg [1:0] w_phase;   // for the weighted policy: three of four grants to sequence 0

    // The current run of consecutive grants each sequence has been passed over for.
    reg [CNT_W-1:0] s0_run, s1_run;

    // The arbitration decision, as one expression per policy. Nothing about the QUEUES appears
    // here, which is the separation that matters: the policy decides WHOSE turn it is and the
    // queues decide WHICH item, so a policy change can never reorder a sequence.
    wire pick0 =
        (arb == ARB_PRIO)     ? have0 :
        (arb == ARB_WEIGHTED) ? (have0 && (w_phase != 2'd3 || !have1)) :
        /* ARB_RR */            (have0 && (!have1 || last_grant == 1'b1));

    wire grant = have0 | have1;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            h0 <= {CNT_W{1'b0}}; t0 <= {CNT_W{1'b0}}; d0 <= {CNT_W{1'b0}};
            h1 <= {CNT_W{1'b0}}; t1 <= {CNT_W{1'b0}}; d1 <= {CNT_W{1'b0}};
            drv_valid  <= 1'b0;
            drv_item   <= {DW{1'b0}};
            drv_from   <= 1'b0;
            n_from_s0  <= {CNT_W{1'b0}};
            n_from_s1  <= {CNT_W{1'b0}};
            s1_max_wait <= {CNT_W{1'b0}};
            s0_max_wait <= {CNT_W{1'b0}};
            s1_run      <= {CNT_W{1'b0}};
            s0_run      <= {CNT_W{1'b0}};
            last_grant <= 1'b1;
            w_phase    <= 2'd0;
        end else begin
            drv_valid <= 1'b0;

            if (s0_push && !s0_full) begin
                q0[t0] <= s0_item;
                t0 <= (t0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t0 + 1'b1;
                d0 <= d0 + 1'b1;
            end
            if (s1_push && !s1_full) begin
                q1[t1] <= s1_item;
                t1 <= (t1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : t1 + 1'b1;
                d1 <= d1 + 1'b1;
            end

            if (drv_ready && grant) begin
                if (pick0) begin
                    drv_item  <= q0[h0];
                    drv_from  <= 1'b0;
                    drv_valid <= 1'b1;
                    h0 <= (h0 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h0 + 1'b1;
                    d0 <= (s0_push && !s0_full) ? d0 : d0 - 1'b1;
                    n_from_s0  <= n_from_s0 + 1'b1;
                    last_grant <= 1'b0;
                    // STARVATION IS COUNTED AT THE GRANT, not inferred afterwards. Sequence 1 had
                    // an item pending and did not get this grant, so its run grows; sequence 0
                    // just got one, so its run ends.
                    if (have1) begin
                        s1_run <= s1_run + 1'b1;
                        if (s1_run + 1'b1 > s1_max_wait) s1_max_wait <= s1_run + 1'b1;
                    end
                    s0_run <= {CNT_W{1'b0}};
                end else begin
                    drv_item  <= q1[h1];
                    drv_from  <= 1'b1;
                    drv_valid <= 1'b1;
                    h1 <= (h1 == DEPTH[CNT_W-1:0] - 1'b1) ? {CNT_W{1'b0}} : h1 + 1'b1;
                    d1 <= (s1_push && !s1_full) ? d1 : d1 - 1'b1;
                    n_from_s1  <= n_from_s1 + 1'b1;
                    last_grant <= 1'b1;
                    if (have0) begin
                        s0_run <= s0_run + 1'b1;
                        if (s0_run + 1'b1 > s0_max_wait) s0_max_wait <= s0_run + 1'b1;
                    end
                    s1_run <= {CNT_W{1'b0}};
                end
                w_phase <= (w_phase == 2'd3) ? 2'd0 : w_phase + 2'd1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sequencer.vhd — the same design in VHDL
-- spi_sequencer.vhd
--
-- Chapter 17.5 -- the agent's sequencer, which is the part of a UVM environment that can be built
-- and measured in a plain simulator, and the part whose behaviour surprises people most.
--
-- WHAT A SEQUENCER IS FOR.
--
-- A sequence produces items in an order that means something -- configure, then a burst, then a
-- read-back. A driver consumes one item at a time. A sequencer stands between N sequences and one
-- driver, and it makes exactly ONE promise:
--
--     within a sequence, items are delivered in the order the sequence produced them
--
-- It promises nothing at all about the order ACROSS sequences, and that is not a gap in the
-- specification. It is the whole point: the interleaving is chosen by an arbitration policy, and
-- changing the policy changes which concurrency bugs a test can reach without changing a single
-- item.
--
-- THREE POLICIES, because the differences between them are invisible in a coverage report.
--
--   ARB_RR        round robin. Each sequence with an item pending gets the next grant in turn.
--   ARB_PRIO      strict priority. Sequence 0 wins whenever it has an item pending.
--   ARB_WEIGHTED  sequence 0 wins three grants in four.
--
-- All three deliver the same items. All three produce identical functional coverage. They differ
-- only in INTERLEAVING -- and a design bug that needs two sequences to overlap is reachable under
-- one policy and unreachable under another, which is why "we ran both sequences" is not the same
-- statement as "we ran both sequences together".
--
-- AND STARVATION IS A FIRST-CLASS OUTPUT HERE.
--
-- Strict priority starves the lower sequence for as long as the higher one has work. That is
-- correct behaviour and it is also the most common way a test does less than its author thinks:
-- the low-priority sequence's items all arrive after the high-priority sequence has finished, so
-- the two never overlap and every interleaving bug is unreachable. A sequencer that does not
-- COUNT this has no way to tell the author.

--
-- WHAT VHDL ADDS HERE: the arbitration policy is an ENUMERATION, so a policy added to the type
-- without a branch in the decision is a case-statement error at analysis time rather than a run in
-- which the new policy silently behaves like the default. That is the same argument Chapter 16.4
-- made about its fault codes and Chapter 17.4 about its constraint sets, and it is the third time
-- it has been worth the type declaration.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package spi_seqr_pkg is
    -- A policy added here without a branch in the arbiter below is an analysis error.
    type arb_t is (ARB_RR, ARB_PRIO, ARB_WEIGHTED);
end package spi_seqr_pkg;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_seqr_pkg.all;

entity spi_sequencer is
    generic (
        DEPTH : positive := 16;
        DW    : positive := 32
    );
    port (
        clk         : in  std_logic;
        rst_n       : in  std_logic;

        arb         : in  arb_t;

        -- Sequence 0 and sequence 1 push items. Each has its own queue, which is what keeps the
        -- one promise: a queue preserves order within a sequence whatever the arbiter does.
        s0_push     : in  std_logic;
        s0_item     : in  std_logic_vector(DW - 1 downto 0);
        s1_push     : in  std_logic;
        s1_item     : in  std_logic_vector(DW - 1 downto 0);
        s0_full     : out std_logic;
        s1_full     : out std_logic;

        -- The driver side: one item at a time.
        drv_ready   : in  std_logic;
        drv_valid   : out std_logic;
        drv_item    : out std_logic_vector(DW - 1 downto 0);
        drv_from    : out natural;

        n_from_s0   : out natural;
        n_from_s1   : out natural;
        -- THE LONGEST RUN of consecutive grants each sequence waited through while it had an item
        -- pending -- not the total. With both queues full, every grant to sequence 0 is a grant
        -- sequence 1 did not get, so the TOTAL is the length of sequence 0 under every policy
        -- including round robin. The number that distinguishes the policies is how many in a ROW.
        s1_max_wait : out natural;
        s0_max_wait : out natural
    );
end entity spi_sequencer;

architecture rtl of spi_sequencer is

    type q_t is array (0 to DEPTH - 1) of std_logic_vector(DW - 1 downto 0);

    signal dv_r : std_logic := '0';
    signal di_r : std_logic_vector(DW - 1 downto 0) := (others => '0');
    signal df_r : natural := 0;
    signal n0_r, n1_r : natural := 0;
    signal w0_r, w1_r : natural := 0;
    signal f0_r, f1_r : std_logic := '0';

begin

    drv_valid   <= dv_r;
    drv_item    <= di_r;
    drv_from    <= df_r;
    n_from_s0   <= n0_r;
    n_from_s1   <= n1_r;
    s0_max_wait <= w0_r;
    s1_max_wait <= w1_r;
    s0_full     <= f0_r;
    s1_full     <= f1_r;

    process (clk, rst_n) is
        variable q0, q1         : q_t := (others => (others => '0'));
        variable h0, t0, d0     : natural := 0;
        variable h1, t1, d1     : natural := 0;
        variable have0, have1   : boolean;
        variable pick0          : boolean;
        variable last_grant     : natural := 1;
        variable w_phase        : natural := 0;
        variable r0, r1         : natural := 0;
        variable pushed0, pushed1 : boolean;
    begin
        if rst_n = '0' then
            h0 := 0; t0 := 0; d0 := 0;
            h1 := 0; t1 := 0; d1 := 0;
            last_grant := 1;
            w_phase    := 0;
            r0 := 0; r1 := 0;
            dv_r <= '0';
            di_r <= (others => '0');
            df_r <= 0;
            n0_r <= 0; n1_r <= 0;
            w0_r <= 0; w1_r <= 0;

        elsif rising_edge(clk) then
            dv_r <= '0';

            pushed0 := (s0_push = '1') and (d0 < DEPTH);
            pushed1 := (s1_push = '1') and (d1 < DEPTH);
            if pushed0 then
                q0(t0) := s0_item;
                if t0 = DEPTH - 1 then t0 := 0; else t0 := t0 + 1; end if;
                d0 := d0 + 1;
            end if;
            if pushed1 then
                q1(t1) := s1_item;
                if t1 = DEPTH - 1 then t1 := 0; else t1 := t1 + 1; end if;
                d1 := d1 + 1;
            end if;

            have0 := d0 > 0;
            have1 := d1 > 0;

            -- The arbitration decision, one branch per policy. Nothing about the QUEUES appears
            -- here, which is the separation that matters: the policy decides WHOSE turn it is and
            -- the queues decide WHICH item, so a policy change can never reorder a sequence.
            case arb is
                when ARB_PRIO     => pick0 := have0;
                when ARB_WEIGHTED => pick0 := have0 and ((w_phase /= 3) or not have1);
                when ARB_RR       => pick0 := have0 and ((not have1) or (last_grant = 1));
            end case;

            if drv_ready = '1' and (have0 or have1) then
                if pick0 then
                    di_r <= q0(h0);
                    df_r <= 0;
                    dv_r <= '1';
                    if h0 = DEPTH - 1 then h0 := 0; else h0 := h0 + 1; end if;
                    d0 := d0 - 1;
                    n0_r <= n0_r + 1;
                    last_grant := 0;
                    -- STARVATION IS COUNTED AT THE GRANT, not inferred afterwards. Sequence 1 had
                    -- an item pending and did not get this grant, so its run grows; sequence 0
                    -- just got one, so its run ends.
                    if have1 then
                        r1 := r1 + 1;
                        if r1 > w1_r then w1_r <= r1; end if;
                    end if;
                    r0 := 0;
                else
                    di_r <= q1(h1);
                    df_r <= 1;
                    dv_r <= '1';
                    if h1 = DEPTH - 1 then h1 := 0; else h1 := h1 + 1; end if;
                    d1 := d1 - 1;
                    n1_r <= n1_r + 1;
                    last_grant := 1;
                    if have0 then
                        r0 := r0 + 1;
                        if r0 > w0_r then w0_r <= r0; end if;
                    end if;
                    r1 := 0;
                end if;
                if w_phase = 3 then w_phase := 0; else w_phase := w_phase + 1; end if;
            end if;

            if d0 >= DEPTH then f0_r <= '1'; else f0_r <= '0'; end if;
            if d1 >= DEPTH then f1_r <= '1'; else f1_r <= '0'; end if;
        end if;
    end process;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_sequencer_tb.sv — three policies, identical items and coverage, and completely different interleaving
// spi_sequencer_tb.sv
//
// TWO SEQUENCES, ONE DRIVER, THREE ARBITRATION POLICIES, AND FOUR MEASUREMENTS.
//
// Each sequence pushes a numbered stream -- sequence 0 sends 0x0000, 0x0001, ... and sequence 1
// sends 0x1000, 0x1001, ... -- so the delivered stream can be checked for two completely
// different properties at once:
//
//     ORDER      within each sequence, the numbers must arrive ascending with no gaps.
//     OVERLAP    across the sequences, how the two streams interleave.
//
//   1. ORDER IS PRESERVED UNDER EVERY POLICY. Zero order violations in all three, which is the
//      sequencer's one promise. A policy that could reorder a sequence would make every directed
//      sequence in the suite meaningless, and the property is cheap to check and almost never is.
//
//   2. ALL THREE POLICIES DELIVER THE SAME ITEMS. Identical counts from both sequences. So every
//      functional coverage number is identical across the three runs -- which is the measurement
//      that matters, because it means a coverage report cannot tell you which policy ran.
//
//   3. THE INTERLEAVING IS COMPLETELY DIFFERENT. Measured as the number of times the delivered
//      stream switches from one sequence to the other. Round robin switches on almost every item;
//      strict priority switches once. Same items, same coverage, and a concurrency bug that needs
//      the two streams to overlap is reachable under one and unreachable under the other.
//
//   4. STARVATION IS COUNTED, NOT INFERRED. Under strict priority, sequence 1 waits through every
//      one of sequence 0's grants. The number is reported by the sequencer itself, because a test
//      whose second sequence only ran after the first one finished has done less than its author
//      believes and nothing else in the report says so.

`timescale 1ns/1ps

module spi_sequencer_tb;

    localparam int DW    = 32;
    localparam int CNT_W = 16;
    localparam int NITEM = 12;     // items pushed by each sequence

    localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;

    reg clk = 1'b0;
    always #5 clk = ~clk;
    reg rst_n = 1'b1;

    reg  [1:0]    arb = ARB_RR;
    reg           s0_push = 1'b0, s1_push = 1'b0;
    reg  [DW-1:0] s0_item = {DW{1'b0}}, s1_item = {DW{1'b0}};
    wire          s0_full, s1_full;

    reg           drv_ready = 1'b1;
    wire          drv_valid, drv_from;
    wire [DW-1:0] drv_item;
    wire [CNT_W-1:0] n_from_s0, n_from_s1, s0_max_wait, s1_max_wait;

    spi_sequencer #(.NSEQ(2), .DEPTH(16), .DW(DW), .CNT_W(CNT_W)) u_s (
        .clk(clk), .rst_n(rst_n), .arb(arb),
        .s0_push(s0_push), .s0_item(s0_item), .s1_push(s1_push), .s1_item(s1_item),
        .s0_full(s0_full), .s1_full(s1_full),
        .drv_ready(drv_ready), .drv_valid(drv_valid), .drv_item(drv_item), .drv_from(drv_from),
        .n_from_s0(n_from_s0), .n_from_s1(n_from_s1),
        .s1_max_wait(s1_max_wait), .s0_max_wait(s0_max_wait)
    );

    integer errors = 0;

    initial begin
        #400_000;
        $display("FAIL: the simulation did not finish within its time limit");
        $finish;
    end

    // ------------------------------------------------------------------
    // The observer of the delivered stream. It checks ORDER and counts SWITCHES, which are the
    // two independent properties of an interleaving.
    // ------------------------------------------------------------------
    integer exp0, exp1, order_bad, switches, delivered;
    reg     last_from;
    reg     seen_any;

    always @(posedge clk) if (rst_n && drv_valid) begin
        delivered = delivered + 1;
        if (drv_from == 1'b0) begin
            if (drv_item !== exp0[DW-1:0]) order_bad = order_bad + 1;
            exp0 = exp0 + 1;
        end else begin
            if (drv_item !== exp1[DW-1:0]) order_bad = order_bad + 1;
            exp1 = exp1 + 1;
        end
        if (seen_any && drv_from !== last_from) switches = switches + 1;
        last_from = drv_from;
        seen_any  = 1'b1;
    end

    task automatic reset_all;
        begin
            @(negedge clk);
            rst_n = 1'b1;
            @(negedge clk);
            rst_n = 1'b0;
            repeat (4) @(negedge clk);
            rst_n = 1'b1;
            repeat (2) @(negedge clk);
            exp0 = 0; exp1 = 32'h1000; order_bad = 0; switches = 0; delivered = 0;
            seen_any = 1'b0; last_from = 1'b0;
        end
    endtask

    // Pushes both sequences' items as fast as the queues accept them, then lets the driver drain.
    // Both sequences are made ready BEFORE any grant, because an interleaving experiment in which
    // one sequence arrives late measures the arrival times rather than the policy.
    task automatic run_policy(input [1:0] a);
        integer i;
        begin
            arb = a;
            reset_all();
            drv_ready = 1'b0;               // hold the driver off while both queues fill
            for (i = 0; i < NITEM; i = i + 1) begin
                @(negedge clk);
                s0_push = 1'b1; s0_item = i[DW-1:0];
                s1_push = 1'b1; s1_item = 32'h1000 + i[DW-1:0];
                @(negedge clk);
                s0_push = 1'b0; s1_push = 1'b0;
            end
            drv_ready = 1'b1;
            // Drain, with a bound: a sequencer that stops granting must not hang the bench.
            for (i = 0; i < 200 && delivered < 2*NITEM; i = i + 1) @(negedge clk);
            repeat (4) @(negedge clk);
        end
    endtask

    integer rr_s0, rr_s1, rr_sw, rr_bad, rr_starv;
    integer pr_s0, pr_s1, pr_sw, pr_bad, pr_starv;
    integer wt_s0, wt_s1, wt_sw, wt_bad, wt_starv;
    integer pr_first_s1_at;

    initial begin
        // ============================================================
        // Round robin.
        // ============================================================
        run_policy(ARB_RR);
        rr_s0 = n_from_s0; rr_s1 = n_from_s1; rr_sw = switches; rr_bad = order_bad;
        rr_starv = s1_max_wait;

        // ============================================================
        // Strict priority.
        // ============================================================
        run_policy(ARB_PRIO);
        pr_s0 = n_from_s0; pr_s1 = n_from_s1; pr_sw = switches; pr_bad = order_bad;
        pr_starv = s1_max_wait;

        // ============================================================
        // Weighted.
        // ============================================================
        run_policy(ARB_WEIGHTED);
        wt_s0 = n_from_s0; wt_s1 = n_from_s1; wt_sw = switches; wt_bad = order_bad;
        wt_starv = s1_max_wait;

        $display("  %0d items pushed by each sequence, both queues full before the first grant",
                 NITEM);
        $display("  policy            from s0   from s1   order errors   switches   s1 longest wait");
        $display("  round robin       %7d   %7d   %12d   %8d   %10d", rr_s0, rr_s1, rr_bad, rr_sw, rr_starv);
        $display("  strict priority   %7d   %7d   %12d   %8d   %10d", pr_s0, pr_s1, pr_bad, pr_sw, pr_starv);
        $display("  weighted 3:1      %7d   %7d   %12d   %8d   %10d", wt_s0, wt_s1, wt_bad, wt_sw, wt_starv);

        // ---- 1. order ----
        if (rr_bad != 0 || pr_bad != 0 || wt_bad != 0) begin
            $display("  FAIL: a policy reordered a sequence (%0d, %0d, %0d order errors)",
                     rr_bad, pr_bad, wt_bad);
            errors = errors + 1;
        end
        $display("    1. zero order errors under all three policies. That is the sequencer's ONE promise -- items within a sequence arrive in the order the sequence produced them -- and it is what makes a directed sequence mean anything. The policy decides whose turn it is; the per-sequence queue decides which item, so no policy change can reorder a sequence");

        // ---- 2. same items ----
        if (rr_s0 != NITEM || rr_s1 != NITEM || pr_s0 != NITEM || pr_s1 != NITEM
            || wt_s0 != NITEM || wt_s1 != NITEM) begin
            $display("  FAIL: the policies did not all deliver every item");
            errors = errors + 1;
        end
        $display("    2. all three policies delivered every one of the %0d items from both sequences. So every functional coverage number is IDENTICAL across the three runs -- which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped",
                 NITEM);

        // ---- 3. different interleaving ----
        if (pr_sw >= rr_sw) begin
            $display("  FAIL: strict priority switched between sequences at least as often as round robin (%0d vs %0d), so the interleaving experiment measured nothing",
                     pr_sw, rr_sw);
            errors = errors + 1;
        end
        if (pr_sw != 1) begin
            $display("  FAIL: strict priority switched %0d times; with both queues full it should switch exactly once, when sequence 0 runs out",
                     pr_sw);
            errors = errors + 1;
        end
        $display("    3. round robin switched between the two streams %0d times; strict priority switched %0d. Same items, same coverage, and completely different traffic: under priority, sequence 1's items ALL arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable. `we ran both sequences` and `we ran both sequences together` are different claims",
                 rr_sw, pr_sw);

        // ---- 4. starvation counted ----
        if (rr_starv != 1) begin
            $display("  FAIL: under round robin sequence 1's longest wait should be a single grant, not %0d",
                     rr_starv);
            errors = errors + 1;
        end
        if (pr_starv != NITEM) begin
            $display("  FAIL: under strict priority sequence 1 should have waited through all %0d of sequence 0's grants in a row, not %0d",
                     NITEM, pr_starv);
            errors = errors + 1;
        end
        if (wt_starv <= rr_starv || wt_starv >= pr_starv) begin
            $display("  FAIL: the weighted policy's longest wait (%0d) should sit between round robin's (%0d) and priority's (%0d)",
                     wt_starv, rr_starv, pr_starv);
            errors = errors + 1;
        end
        $display("    4. sequence 1's LONGEST consecutive wait was %0d grant under round robin, %0d under weighted 3:1, and %0d under strict priority -- all of sequence 0. Note that the TOTAL number of grants it was passed over for is %0d under every policy, because with both queues full every grant to sequence 0 is one sequence 1 did not get. A starvation metric that counts the total cannot tell round robin from strict priority; it is measuring queue occupancy",
                 rr_starv, wt_starv, pr_starv, NITEM);

        if (errors == 0)
            $display("PASS: a sequencer makes exactly one promise -- that items within a sequence are delivered in the order the sequence produced them -- and it deliberately promises nothing about the order across sequences, because that is what an arbitration policy is for. Measured with %0d items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all %0d items from both sequences, so every functional coverage number is IDENTICAL across the three runs and a coverage report cannot tell you which policy ran. What differs is the INTERLEAVING -- round robin switched between the streams %0d times and strict priority switched %0d, because under priority sequence 1's items all arrive after sequence 0 has finished. The two never overlap, and any design bug that needs them to overlap is unreachable, which is why `we ran both sequences` and `we ran both sequences together` are different claims. And starvation is COUNTED rather than inferred, as the LONGEST consecutive wait rather than the total: %0d grant under round robin, %0d under weighted, and all %0d of sequence 0's under priority. The total would have been %0d under every policy -- with both queues full, every grant to sequence 0 is one sequence 1 did not get -- so a starvation metric built on the total cannot tell round robin from strict priority and is really measuring queue occupancy. The sequencer reports the number itself, because nothing else in a suite's output distinguishes a test that overlapped its sequences from one that queued them",
                     NITEM, NITEM, rr_sw, pr_sw, rr_starv, wt_starv, pr_starv, NITEM);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sequencer_tb.v — the same bench in Verilog-2001
// spi_sequencer_tb.v
//
// TWO SEQUENCES, ONE DRIVER, THREE ARBITRATION POLICIES, AND FOUR MEASUREMENTS.
//
// Each sequence pushes a numbered stream -- sequence 0 sends 0x0000, 0x0001, ... and sequence 1
// sends 0x1000, 0x1001, ... -- so the delivered stream can be checked for two completely
// different properties at once:
//
//     ORDER      within each sequence, the numbers must arrive ascending with no gaps.
//     OVERLAP    across the sequences, how the two streams interleave.
//
//   1. ORDER IS PRESERVED UNDER EVERY POLICY. Zero order violations in all three, which is the
//      sequencer's one promise. A policy that could reorder a sequence would make every directed
//      sequence in the suite meaningless, and the property is cheap to check and almost never is.
//
//   2. ALL THREE POLICIES DELIVER THE SAME ITEMS. Identical counts from both sequences. So every
//      functional coverage number is identical across the three runs -- which is the measurement
//      that matters, because it means a coverage report cannot tell you which policy ran.
//
//   3. THE INTERLEAVING IS COMPLETELY DIFFERENT. Measured as the number of times the delivered
//      stream switches from one sequence to the other. Round robin switches on almost every item;
//      strict priority switches once. Same items, same coverage, and a concurrency bug that needs
//      the two streams to overlap is reachable under one and unreachable under the other.
//
//   4. STARVATION IS COUNTED, NOT INFERRED. Under strict priority, sequence 1 waits through every
//      one of sequence 0's grants. The number is reported by the sequencer itself, because a test
//      whose second sequence only ran after the first one finished has done less than its author
//      believes and nothing else in the report says so.

`timescale 1ns/1ps

module spi_sequencer_tb;

    localparam DW    = 32;
    localparam CNT_W = 16;
    localparam NITEM = 12;     // items pushed by each sequence

    localparam [1:0] ARB_RR = 2'd0, ARB_PRIO = 2'd1, ARB_WEIGHTED = 2'd2;

    reg clk;
    always #5 clk = ~clk;
    reg rst_n;

    reg  [1:0]    arb;
    reg           s0_push, s1_push;
    reg  [DW-1:0] s0_item, s1_item;
    wire          s0_full, s1_full;

    reg           drv_ready;
    wire          drv_valid, drv_from;
    wire [DW-1:0] drv_item;
    wire [CNT_W-1:0] n_from_s0, n_from_s1, s0_max_wait, s1_max_wait;

    spi_sequencer #(.NSEQ(2), .DEPTH(16), .DW(DW), .CNT_W(CNT_W)) u_s (
        .clk(clk), .rst_n(rst_n), .arb(arb),
        .s0_push(s0_push), .s0_item(s0_item), .s1_push(s1_push), .s1_item(s1_item),
        .s0_full(s0_full), .s1_full(s1_full),
        .drv_ready(drv_ready), .drv_valid(drv_valid), .drv_item(drv_item), .drv_from(drv_from),
        .n_from_s0(n_from_s0), .n_from_s1(n_from_s1),
        .s1_max_wait(s1_max_wait), .s0_max_wait(s0_max_wait)
    );

    integer errors;

    initial begin
        #400_000;
        $display("FAIL: the simulation did not finish within its time limit");
        $finish;
    end

    // ------------------------------------------------------------------
    // The observer of the delivered stream. It checks ORDER and counts SWITCHES, which are the
    // two independent properties of an interleaving.
    // ------------------------------------------------------------------
    integer exp0, exp1, order_bad, switches, delivered;
    reg     last_from;
    reg     seen_any;

    always @(posedge clk) if (rst_n && drv_valid) begin
        delivered = delivered + 1;
        if (drv_from == 1'b0) begin
            if (drv_item !== exp0[DW-1:0]) order_bad = order_bad + 1;
            exp0 = exp0 + 1;
        end else begin
            if (drv_item !== exp1[DW-1:0]) order_bad = order_bad + 1;
            exp1 = exp1 + 1;
        end
        if (seen_any && drv_from !== last_from) switches = switches + 1;
        last_from = drv_from;
        seen_any  = 1'b1;
    end

    task reset_all;
        begin
            @(negedge clk);
            rst_n = 1'b1;
            @(negedge clk);
            rst_n = 1'b0;
            repeat (4) @(negedge clk);
            rst_n = 1'b1;
            repeat (2) @(negedge clk);
            exp0 = 0; exp1 = 32'h1000; order_bad = 0; switches = 0; delivered = 0;
            seen_any = 1'b0; last_from = 1'b0;
        end
    endtask

    // Pushes both sequences' items as fast as the queues accept them, then lets the driver drain.
    // Both sequences are made ready BEFORE any grant, because an interleaving experiment in which
    // one sequence arrives late measures the arrival times rather than the policy.
        task run_policy;
        input [1:0] a;
        integer i;
        begin
            arb = a;
            reset_all();
            drv_ready = 1'b0;               // hold the driver off while both queues fill
            for (i = 0; i < NITEM; i = i + 1) begin
                @(negedge clk);
                s0_push = 1'b1; s0_item = i[DW-1:0];
                s1_push = 1'b1; s1_item = 32'h1000 + i[DW-1:0];
                @(negedge clk);
                s0_push = 1'b0; s1_push = 1'b0;
            end
            drv_ready = 1'b1;
            // Drain, with a bound: a sequencer that stops granting must not hang the bench.
            for (i = 0; i < 200 && delivered < 2*NITEM; i = i + 1) @(negedge clk);
            repeat (4) @(negedge clk);
        end
    endtask

    integer rr_s0, rr_s1, rr_sw, rr_bad, rr_starv;
    integer pr_s0, pr_s1, pr_sw, pr_bad, pr_starv;
    integer wt_s0, wt_s1, wt_sw, wt_bad, wt_starv;
    integer pr_first_s1_at;

    initial begin
        // ============================================================
        // Round robin.
        // ============================================================
        run_policy(ARB_RR);
        rr_s0 = n_from_s0; rr_s1 = n_from_s1; rr_sw = switches; rr_bad = order_bad;
        rr_starv = s1_max_wait;

        // ============================================================
        // Strict priority.
        // ============================================================
        run_policy(ARB_PRIO);
        pr_s0 = n_from_s0; pr_s1 = n_from_s1; pr_sw = switches; pr_bad = order_bad;
        pr_starv = s1_max_wait;

        // ============================================================
        // Weighted.
        // ============================================================
        run_policy(ARB_WEIGHTED);
        wt_s0 = n_from_s0; wt_s1 = n_from_s1; wt_sw = switches; wt_bad = order_bad;
        wt_starv = s1_max_wait;

        $display("  %0d items pushed by each sequence, both queues full before the first grant",
                 NITEM);
        $display("  policy            from s0   from s1   order errors   switches   s1 longest wait");
        $display("  round robin       %7d   %7d   %12d   %8d   %10d", rr_s0, rr_s1, rr_bad, rr_sw, rr_starv);
        $display("  strict priority   %7d   %7d   %12d   %8d   %10d", pr_s0, pr_s1, pr_bad, pr_sw, pr_starv);
        $display("  weighted 3:1      %7d   %7d   %12d   %8d   %10d", wt_s0, wt_s1, wt_bad, wt_sw, wt_starv);

        // ---- 1. order ----
        if (rr_bad != 0 || pr_bad != 0 || wt_bad != 0) begin
            $display("  FAIL: a policy reordered a sequence (%0d, %0d, %0d order errors)",
                     rr_bad, pr_bad, wt_bad);
            errors = errors + 1;
        end
        $display("    1. zero order errors under all three policies. That is the sequencer's ONE promise -- items within a sequence arrive in the order the sequence produced them -- and it is what makes a directed sequence mean anything. The policy decides whose turn it is; the per-sequence queue decides which item, so no policy change can reorder a sequence");

        // ---- 2. same items ----
        if (rr_s0 != NITEM || rr_s1 != NITEM || pr_s0 != NITEM || pr_s1 != NITEM
            || wt_s0 != NITEM || wt_s1 != NITEM) begin
            $display("  FAIL: the policies did not all deliver every item");
            errors = errors + 1;
        end
        $display("    2. all three policies delivered every one of the %0d items from both sequences. So every functional coverage number is IDENTICAL across the three runs -- which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped",
                 NITEM);

        // ---- 3. different interleaving ----
        if (pr_sw >= rr_sw) begin
            $display("  FAIL: strict priority switched between sequences at least as often as round robin (%0d vs %0d), so the interleaving experiment measured nothing",
                     pr_sw, rr_sw);
            errors = errors + 1;
        end
        if (pr_sw != 1) begin
            $display("  FAIL: strict priority switched %0d times; with both queues full it should switch exactly once, when sequence 0 runs out",
                     pr_sw);
            errors = errors + 1;
        end
        $display("    3. round robin switched between the two streams %0d times; strict priority switched %0d. Same items, same coverage, and completely different traffic: under priority, sequence 1's items ALL arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable. `we ran both sequences` and `we ran both sequences together` are different claims",
                 rr_sw, pr_sw);

        // ---- 4. starvation counted ----
        if (rr_starv != 1) begin
            $display("  FAIL: under round robin sequence 1's longest wait should be a single grant, not %0d",
                     rr_starv);
            errors = errors + 1;
        end
        if (pr_starv != NITEM) begin
            $display("  FAIL: under strict priority sequence 1 should have waited through all %0d of sequence 0's grants in a row, not %0d",
                     NITEM, pr_starv);
            errors = errors + 1;
        end
        if (wt_starv <= rr_starv || wt_starv >= pr_starv) begin
            $display("  FAIL: the weighted policy's longest wait (%0d) should sit between round robin's (%0d) and priority's (%0d)",
                     wt_starv, rr_starv, pr_starv);
            errors = errors + 1;
        end
        $display("    4. sequence 1's LONGEST consecutive wait was %0d grant under round robin, %0d under weighted 3:1, and %0d under strict priority -- all of sequence 0. Note that the TOTAL number of grants it was passed over for is %0d under every policy, because with both queues full every grant to sequence 0 is one sequence 1 did not get. A starvation metric that counts the total cannot tell round robin from strict priority; it is measuring queue occupancy",
                 rr_starv, wt_starv, pr_starv, NITEM);

        if (errors == 0)
            $display("PASS: a sequencer makes exactly one promise -- that items within a sequence are delivered in the order the sequence produced them -- and it deliberately promises nothing about the order across sequences, because that is what an arbitration policy is for. Measured with %0d items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all %0d items from both sequences, so every functional coverage number is IDENTICAL across the three runs and a coverage report cannot tell you which policy ran. What differs is the INTERLEAVING -- round robin switched between the streams %0d times and strict priority switched %0d, because under priority sequence 1's items all arrive after sequence 0 has finished. The two never overlap, and any design bug that needs them to overlap is unreachable, which is why `we ran both sequences` and `we ran both sequences together` are different claims. And starvation is COUNTED rather than inferred, as the LONGEST consecutive wait rather than the total: %0d grant under round robin, %0d under weighted, and all %0d of sequence 0's under priority. The total would have been %0d under every policy -- with both queues full, every grant to sequence 0 is one sequence 1 did not get -- so a starvation metric built on the total cannot tell round robin from strict priority and is really measuring queue occupancy. The sequencer reports the number itself, because nothing else in a suite's output distinguishes a test that overlapped its sequences from one that queued them",
                     NITEM, NITEM, rr_sw, pr_sw, rr_starv, wt_starv, pr_starv, NITEM);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        s0_push = 1'b0;
        s1_push = 1'b0;
        s0_item = {DW{1'b0}};
        s1_item = {DW{1'b0}};
        clk = 1'b0;
        rst_n = 1'b1;
        arb = ARB_RR;
        drv_ready = 1'b1;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_sequencer_tb.vhd — the same bench in VHDL
-- spi_sequencer_tb.vhd
--
-- TWO SEQUENCES, ONE DRIVER, THREE ARBITRATION POLICIES, AND FOUR MEASUREMENTS.
--
-- Each sequence pushes a numbered stream -- sequence 0 sends 0x0000, 0x0001, ... and sequence 1
-- sends 0x1000, 0x1001, ... -- so the delivered stream can be checked for two completely
-- different properties at once:
--
--     ORDER      within each sequence, the numbers must arrive ascending with no gaps.
--     OVERLAP    across the sequences, how the two streams interleave.
--
--   1. ORDER IS PRESERVED UNDER EVERY POLICY. Zero order violations in all three, which is the
--      sequencer's one promise. A policy that could reorder a sequence would make every directed
--      sequence in the suite meaningless, and the property is cheap to check and almost never is.
--
--   2. ALL THREE POLICIES DELIVER THE SAME ITEMS. Identical counts from both sequences. So every
--      functional coverage number is identical across the three runs -- which is the measurement
--      that matters, because it means a coverage report cannot tell you which policy ran.
--
--   3. THE INTERLEAVING IS COMPLETELY DIFFERENT. Measured as the number of times the delivered
--      stream switches from one sequence to the other. Round robin switches on almost every item;
--      strict priority switches once. Same items, same coverage, and a concurrency bug that needs
--      the two streams to overlap is reachable under one and unreachable under the other.
--
--   4. STARVATION IS COUNTED, NOT INFERRED. Under strict priority, sequence 1 waits through every
--      one of sequence 0's grants. The number is reported by the sequencer itself, because a test
--      whose second sequence only ran after the first one finished has done less than its author
--      believes and nothing else in the report says so.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_seqr_pkg.all;

entity spi_sequencer_tb is
end entity spi_sequencer_tb;

architecture tb of spi_sequencer_tb is

    constant DW     : positive := 32;
    constant NITEM  : natural  := 12;     -- items pushed by each sequence
    constant HALF_T : time     := 5 ns;

    signal clk      : std_logic := '0';
    signal rst_n    : std_logic := '1';
    signal done_sim : boolean   := false;

    signal arb     : arb_t     := ARB_RR;
    signal s0_push : std_logic := '0';
    signal s1_push : std_logic := '0';
    signal s0_item : std_logic_vector(DW - 1 downto 0) := (others => '0');
    signal s1_item : std_logic_vector(DW - 1 downto 0) := (others => '0');
    signal s0_full, s1_full : std_logic;

    signal drv_ready : std_logic := '1';
    signal drv_valid : std_logic;
    signal drv_item  : std_logic_vector(DW - 1 downto 0);
    signal drv_from  : natural;
    signal n_from_s0, n_from_s1, s0_max_wait, s1_max_wait : natural;

    -- The observer of the delivered stream. It checks ORDER and counts SWITCHES, which are the
    -- two independent properties of an interleaving.
    signal obs_clr   : boolean := false;
    signal order_bad : natural := 0;
    signal switches  : natural := 0;
    signal delivered : natural := 0;

    signal errors : integer := 0;

begin

    clk_gen : process is
    begin
        while not done_sim loop
            wait for HALF_T;
            clk <= not clk;
        end loop;
        wait;
    end process clk_gen;

    u_s : entity work.spi_sequencer
        generic map (DEPTH => 16, DW => DW)
        port map (clk => clk, rst_n => rst_n, arb => arb,
                  s0_push => s0_push, s0_item => s0_item,
                  s1_push => s1_push, s1_item => s1_item,
                  s0_full => s0_full, s1_full => s1_full,
                  drv_ready => drv_ready, drv_valid => drv_valid,
                  drv_item => drv_item, drv_from => drv_from,
                  n_from_s0 => n_from_s0, n_from_s1 => n_from_s1,
                  s1_max_wait => s1_max_wait, s0_max_wait => s0_max_wait);

    observe : process (clk) is
        variable exp0, exp1 : natural := 0;
        variable last_from  : natural := 0;
        variable seen_any   : boolean := false;
    begin
        if rising_edge(clk) then
            if obs_clr then
                exp0 := 0; exp1 := 16#1000#;
                last_from := 0; seen_any := false;
                order_bad <= 0; switches <= 0; delivered <= 0;
            elsif rst_n = '1' and drv_valid = '1' then
                delivered <= delivered + 1;
                if drv_from = 0 then
                    if to_integer(unsigned(drv_item)) /= exp0 then
                        order_bad <= order_bad + 1;
                    end if;
                    exp0 := exp0 + 1;
                else
                    if to_integer(unsigned(drv_item)) /= exp1 then
                        order_bad <= order_bad + 1;
                    end if;
                    exp1 := exp1 + 1;
                end if;
                if seen_any and drv_from /= last_from then
                    switches <= switches + 1;
                end if;
                last_from := drv_from;
                seen_any  := true;
            end if;
        end if;
    end process observe;

    main : process is

        procedure idle_n (n : natural) is
        begin
            for i in 1 to n loop wait until falling_edge(clk); end loop;
        end procedure idle_n;

        procedure reset_all is
        begin
            wait until falling_edge(clk);
            rst_n <= '1';
            wait until falling_edge(clk);
            rst_n <= '0';
            idle_n(4);
            obs_clr <= true;
            wait until falling_edge(clk);
            obs_clr <= false;
            rst_n <= '1';
            idle_n(2);
        end procedure reset_all;

        -- Pushes both sequences' items as fast as the queues accept them, then lets the driver
        -- drain. Both sequences are made ready BEFORE any grant, because an interleaving
        -- experiment in which one sequence arrives late measures the arrival times rather than
        -- the policy.
        procedure run_policy (a : arb_t) is
        begin
            arb <= a;
            reset_all;
            drv_ready <= '0';              -- hold the driver off while both queues fill
            for i in 0 to NITEM - 1 loop
                wait until falling_edge(clk);
                s0_push <= '1'; s0_item <= std_logic_vector(to_unsigned(i, DW));
                s1_push <= '1'; s1_item <= std_logic_vector(to_unsigned(16#1000# + i, DW));
                wait until falling_edge(clk);
                s0_push <= '0'; s1_push <= '0';
            end loop;
            drv_ready <= '1';
            -- Drain, with a bound: a sequencer that stops granting must not hang the bench.
            for i in 1 to 200 loop
                exit when delivered >= 2 * NITEM;
                wait until falling_edge(clk);
            end loop;
            idle_n(4);
        end procedure run_policy;

        variable rr_s0, rr_s1, rr_sw, rr_bad, rr_wait : natural;
        variable pr_s0, pr_s1, pr_sw, pr_bad, pr_wait : natural;
        variable wt_s0, wt_s1, wt_sw, wt_bad, wt_wait : natural;

    begin
        run_policy(ARB_RR);
        rr_s0 := n_from_s0; rr_s1 := n_from_s1; rr_sw := switches;
        rr_bad := order_bad; rr_wait := s1_max_wait;

        run_policy(ARB_PRIO);
        pr_s0 := n_from_s0; pr_s1 := n_from_s1; pr_sw := switches;
        pr_bad := order_bad; pr_wait := s1_max_wait;

        run_policy(ARB_WEIGHTED);
        wt_s0 := n_from_s0; wt_s1 := n_from_s1; wt_sw := switches;
        wt_bad := order_bad; wt_wait := s1_max_wait;

        report "  " & integer'image(NITEM) &
               " items pushed by each sequence, both queues full before the first grant";
        report "  policy            from s0   from s1   order errors   switches   s1 longest wait";
        report "  round robin       " & integer'image(rr_s0) & "   " & integer'image(rr_s1) &
               "   " & integer'image(rr_bad) & "   " & integer'image(rr_sw) & "   " &
               integer'image(rr_wait);
        report "  strict priority   " & integer'image(pr_s0) & "   " & integer'image(pr_s1) &
               "   " & integer'image(pr_bad) & "   " & integer'image(pr_sw) & "   " &
               integer'image(pr_wait);
        report "  weighted 3:1      " & integer'image(wt_s0) & "   " & integer'image(wt_s1) &
               "   " & integer'image(wt_bad) & "   " & integer'image(wt_sw) & "   " &
               integer'image(wt_wait);

        if rr_bad /= 0 or pr_bad /= 0 or wt_bad /= 0 then
            report "  FAIL: a policy reordered a sequence";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    1. zero order errors under all three policies. That is the sequencer's ONE promise -- items within a sequence arrive in the order the sequence produced them -- and it is what makes a directed sequence mean anything. The policy decides whose turn it is; the per-sequence queue decides which item, so no policy change can reorder a sequence";

        if rr_s0 /= NITEM or rr_s1 /= NITEM or pr_s0 /= NITEM or pr_s1 /= NITEM
           or wt_s0 /= NITEM or wt_s1 /= NITEM then
            report "  FAIL: the policies did not all deliver every item";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    2. all three policies delivered every one of the " & integer'image(NITEM) &
               " items from both sequences. So every functional coverage number is IDENTICAL across the three runs -- which is the point: a coverage report cannot tell you which policy ran, and therefore cannot tell you whether the two sequences ever overlapped";

        if pr_sw >= rr_sw then
            report "  FAIL: strict priority switched between sequences at least as often as round robin, so the interleaving experiment measured nothing";
            errors <= errors + 1; wait for 1 ns;
        end if;
        if pr_sw /= 1 then
            report "  FAIL: strict priority switched " & integer'image(pr_sw) &
                   " times; with both queues full it should switch exactly once, when sequence 0 runs out";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    3. round robin switched between the two streams " & integer'image(rr_sw) &
               " times; strict priority switched " & integer'image(pr_sw) &
               ". Same items, same coverage, and completely different traffic: under priority, sequence 1's items ALL arrive after sequence 0 has finished, so the two never overlap and any design bug that needs them to overlap is unreachable. `we ran both sequences` and `we ran both sequences together` are different claims";

        if rr_wait /= 1 then
            report "  FAIL: under round robin sequence 1's longest wait should be a single grant, not " &
                   integer'image(rr_wait);
            errors <= errors + 1; wait for 1 ns;
        end if;
        if pr_wait /= NITEM then
            report "  FAIL: under strict priority sequence 1 should have waited through all " &
                   integer'image(NITEM) & " of sequence 0's grants in a row, not " &
                   integer'image(pr_wait);
            errors <= errors + 1; wait for 1 ns;
        end if;
        if wt_wait <= rr_wait or wt_wait >= pr_wait then
            report "  FAIL: the weighted policy's longest wait should sit between round robin's and priority's";
            errors <= errors + 1; wait for 1 ns;
        end if;
        report "    4. sequence 1's LONGEST consecutive wait was " & integer'image(rr_wait) &
               " grant under round robin, " & integer'image(wt_wait) & " under weighted 3:1, and " &
               integer'image(pr_wait) &
               " under strict priority -- all of sequence 0. Note that the TOTAL number of grants it was passed over for is " &
               integer'image(NITEM) &
               " under every policy, because with both queues full every grant to sequence 0 is one sequence 1 did not get. A starvation metric that counts the total cannot tell round robin from strict priority; it is measuring queue occupancy";

        wait for 1 ns;
        if errors = 0 then
            report "PASS: a sequencer makes exactly one promise -- that items within a sequence are delivered in the order the sequence produced them -- and it deliberately promises nothing about the order across sequences, because that is what an arbitration policy is for. Measured with " &
                   integer'image(NITEM) &
                   " items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all " &
                   integer'image(NITEM) &
                   " items from both sequences, so every functional coverage number is IDENTICAL across the three runs and a coverage report cannot tell you which policy ran. What differs is the INTERLEAVING -- round robin switched between the streams " &
                   integer'image(rr_sw) & " times and strict priority switched " &
                   integer'image(pr_sw) &
                   ", because under priority sequence 1's items all arrive after sequence 0 has finished. The two never overlap, and any design bug that needs them to overlap is unreachable, which is why `we ran both sequences` and `we ran both sequences together` are different claims. And starvation is COUNTED rather than inferred, as the LONGEST consecutive wait rather than the total: " &
                   integer'image(rr_wait) & " grant under round robin, " & integer'image(wt_wait) &
                   " under weighted, and all " & integer'image(pr_wait) &
                   " of sequence 0's under priority. The total would have been " &
                   integer'image(NITEM) &
                   " under every policy -- with both queues full, every grant to sequence 0 is one sequence 1 did not get -- so a starvation metric built on the total cannot tell round robin from strict priority and is really measuring queue occupancy"
                severity note;
        else
            report "FAIL: " & integer'image(errors) & " error(s)" severity error;
        end if;

        done_sim <= true;
        wait for 100 ns;
        std.env.stop;
    end process main;

end architecture tb;

6. Where The Pieces Go

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   AGENT          driver + sequencer + monitor + the configuration they share,
                  and the knowledge of how all of it connects. Active or passive,
                  and Chapter 16.7 measured why that must be structural.

   ENV            one agent per interface, plus the scoreboard and the coverage
                  model. Both subscribe to a MONITOR's analysis port -- never to
                  a sequencer's, because a transaction that was generated is not
                  a transaction that reached the pins.

   TEST           selects the sequences, the arbitration policy and the
                  configuration. Everything above it is reusable; the test is
                  where the choices live.

The connection that decides whether the environment is honest is the one from the passive agent's monitor to the scoreboard. A suite whose observations come from the active agent's monitor has the driver and the observer sharing a component instance, and the far end of the bus is unobserved.

7. The Whole Agent, As Reviewed Code

Icarus cannot run UVM — Chapter 16.3's toolchain note sets out what it does and does not implement. This is the class form of everything the module has built.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// THE SEQUENCER. Almost always the default, and the two lines worth writing are
// the arbitration mode and the reason for it -- because section 4 measured that
// the choice changes the traffic without changing the coverage.
// ---------------------------------------------------------------------------
class spi_sequencer extends uvm_sequencer #(spi_item);
  `uvm_component_utils(spi_sequencer)
  function new(string n, uvm_component p); super.new(n, p); endfunction
endclass

// ---------------------------------------------------------------------------
// THE AGENT. `is_active` decides whether the driver and sequencer are CONSTRUCTED,
// not whether they are used -- Chapter 16.7's measurement, in its UVM spelling.
// ---------------------------------------------------------------------------
class spi_agent extends uvm_agent;
  `uvm_component_utils(spi_agent)

  spi_cfg           cfg;
  spi_monitor       monitor;     // both roles
  spi_master_driver driver;      // active only
  spi_sequencer     sequencer;   // active only

  function new(string n, uvm_component p); super.new(n, p); endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(spi_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "no configuration object for the SPI agent")

    monitor = spi_monitor::type_id::create("monitor", this);

    if (get_is_active() == UVM_ACTIVE) begin
      sequencer = spi_sequencer::type_id::create("sequencer", this);
      driver    = spi_master_driver::type_id::create("driver", this);
    end
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    if (get_is_active() == UVM_ACTIVE)
      driver.seq_item_port.connect(sequencer.seq_item_export);
  endfunction
endclass

// ---------------------------------------------------------------------------
// THE ENVIRONMENT. One active agent, one PASSIVE agent at the far end of the bus,
// and both analysis consumers fed from the passive one's monitor.
// ---------------------------------------------------------------------------
class spi_env extends uvm_env;
  `uvm_component_utils(spi_env)

  spi_agent      master_agent;
  spi_agent      observer_agent;
  spi_ref_model  model;
  spi_scoreboard sb;
  spi_cov        cov;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    uvm_config_db#(uvm_active_passive_enum)::set(this, "master_agent",   "is_active", UVM_ACTIVE);
    uvm_config_db#(uvm_active_passive_enum)::set(this, "observer_agent", "is_active", UVM_PASSIVE);
    master_agent   = spi_agent::type_id::create("master_agent", this);
    observer_agent = spi_agent::type_id::create("observer_agent", this);
    model          = spi_ref_model::type_id::create("model", this);
    sb             = spi_scoreboard::type_id::create("sb", this);
    cov            = spi_cov::type_id::create("cov", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    // The PREDICTION comes from the sequencer's traffic and the OBSERVATION from a monitor --
    // never both from the same place, which is Chapter 16.6's whole argument.
    master_agent.sequencer.ap.connect(model.analysis_export);
    model.pred_ap.connect(sb.pred_fifo.analysis_export);
    // Observations and coverage from the PASSIVE agent, at the far end of the bus.
    observer_agent.monitor.ap.connect(sb.obs_fifo.analysis_export);
    observer_agent.monitor.ap.connect(cov.analysis_export);
  endfunction
endclass

// ---------------------------------------------------------------------------
// A VIRTUAL SEQUENCE running two sequences CONCURRENTLY on one sequencer, which is
// the arrangement section 4 measured. The arbitration mode is set here because it
// is a property of the TEST rather than of the sequencer.
// ---------------------------------------------------------------------------
class spi_overlap_vseq extends uvm_sequence;
  `uvm_object_utils(spi_overlap_vseq)
  spi_sequencer seqr;

  task body();
    spi_width_walk_seq  a = spi_width_walk_seq::type_id::create("a");
    spi_boundary_seq    b = spi_boundary_seq::type_id::create("b");

    // ROUND ROBIN, chosen deliberately: under strict priority the second sequence's items would
    // all arrive after the first had finished, the two would never overlap, and every bug that
    // needs them to overlap would be unreachable -- with identical coverage either way.
    seqr.set_arbitration(UVM_SEQ_ARB_RANDOM);

    fork
      a.start(seqr);
      b.start(seqr);
    join
  endtask
endclass

// ---------------------------------------------------------------------------
// THE TEST. Everything above is reusable; this is where the choices live.
// ---------------------------------------------------------------------------
class spi_overlap_test extends uvm_test;
  `uvm_component_utils(spi_overlap_test)
  spi_env env;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = spi_env::type_id::create("env", this);
  endfunction

  task run_phase(uvm_phase phase);
    spi_overlap_vseq vseq = spi_overlap_vseq::type_id::create("vseq");
    phase.raise_objection(this);
    vseq.seqr = env.master_agent.sequencer;
    vseq.start(null);
    // Drain, so that the last transaction reaches the monitor and the scoreboard before
    // `check_phase` asks whether anything is outstanding -- Chapter 16.6's emptiness check needs
    // the pipeline to have emptied, and an objection dropped too early makes that check fire on a
    // transaction that was merely still in flight.
    phase.phase_done.set_drain_time(this, 200ns);
    phase.drop_objection(this);
  endtask
endclass

8. Why a Verification Engineer Cares

Because "we ran both sequences" is the claim most often made and least often true in the form that matters.

The practical consequence is a rule about what a test must record. An arbitration policy is not recoverable from a coverage report — all three policies here produced identical coverage — so the policy and the reason for it belong in the test as an explicit line. A test that does not set it has whatever the default is, which for two sequences started together is round robin, which is the good case — until somebody sets a priority to express importance and removes the overlap without noticing.

The second is about fairness metrics generally: a sum cannot measure a run. The total wait was identical under all three policies here and the longest consecutive wait separated them completely. The same mistake shows up in latency reports, arbiter reviews and bus-utilisation numbers.

9. Why an FPGA or ASIC Engineer Cares

Because the arbitration policy in your testbench decides which of your design's concurrency bugs is reachable, and you are the one who gets the bug report.

A slave whose registers are written by one sequence and read by another is exercised completely differently under round robin and under strict priority — and the strict case, which reads as the more controlled test, is the one in which the two never overlap. If your design has a read-during-write hazard, a priority-arbitrated test cannot find it, and the coverage report will look the same as the one that could.

And the same argument applies to your own arbiters. A round of grants that looks fair in a total is the shape in which a low-priority requester waits through a burst of thirty — and a latency report built on averages, like the starvation metric this chapter had to correct, will say nothing about it.

10. Failure Signature — Two Sequences That Never Met

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a concurrency bug -- a read during a write, a reconfiguration
                    during a transfer -- escapes to the lab. The suite has both
                    sequences and runs them in the same test. Coverage is closed.

   What happened    the test set a sequence priority, so the sequencer granted
                    every item of the higher-priority sequence first. The second
                    sequence's items all arrived after the first had finished.
                    The two never overlapped.

   What would have  a longest-consecutive-wait metric per sequence, or reading the
   caught it        arbitration mode the test set. Coverage cannot help: all
                    policies deliver identical items and identical bins.

   The tell         the test names a priority. A priority is a statement about
                    IMPORTANCE and the sequencer reads it as a statement about
                    ORDER -- and strict ordering is the same thing as no overlap.

11. Common Misconceptions

"A sequencer decides the order of items." It decides the order across sequences. Within a sequence the order is the sequence's, preserved by a per-sequence queue, and no policy can change it.

"Running both sequences in one test means they overlapped." Under strict priority the second sequence's items all arrive after the first has finished. Same test, same items, same coverage, no overlap.

"Coverage will show whether the sequences interleaved." It will not. All three policies here delivered identical items and produced identical bins. Interleaving is not a coverage property.

"Setting a priority makes a sequence more important." It makes the sequencer grant it first, exhaustively. The effect on the test is the removal of overlap, which is usually the opposite of what was intended.

"Starvation is the number of grants a sequence missed." That total is identical under round robin and strict priority when both queues are full — it measures queue occupancy. Starvation is the longest consecutive run of missed grants.

"The agent is a convenience wrapper." It is the object that holds the knowledge of how the components connect, which is what makes a second instance on a second bus one line. And its active/passive form is a structural guarantee rather than a mode — Chapter 16.7 measured what happens when it is not.

12. Reason It Through

All three policies produced identical coverage. What does that tell you about what coverage measures?

That it measures which transactions occurred, not how they were arranged in time. Interleaving, concurrency and overlap are properties of the ordering across streams, and no bin in a functional coverage model is indexed by them — so a report can be closed while an entire class of concurrency bug is unreachable.

Both queues are filled before the first grant. Why does the experiment need that?

Because otherwise the interleaving is decided by when the items arrived rather than by the policy. With one sequence arriving late, even round robin produces a sequential-looking stream, and the measurement would be about the bench's push timing.

Why is the total number of missed grants a bad starvation metric here?

With both queues full, every grant to sequence 0 is a grant sequence 1 did not get — so the total is the length of sequence 0 under every policy, round robin included. It is measuring queue occupancy. The longest consecutive run separates the policies: 1, 3, and 12.

A test sets a sequence priority to express that one sequence matters more. What has it actually done?

Told the sequencer to grant that sequence exhaustively before the other, which removes the overlap between them. If the design has a hazard that needs the two to interleave, the test can no longer reach it — and nothing in the coverage report changes.

Where should a scoreboard's observations come from in a two-agent environment, and why?

From the passive agent's monitor, at the far end of the bus. Taking them from the active agent's monitor means the driver and the observer share a component instance and the other end of the bus is never watched — which is Chapter 16.5's peeking problem arriving through the environment's wiring rather than through a configuration field.

13. Understanding Check

14. Summary

An agent is the object that knows how a driver, a sequencer, a monitor and their shared configuration connect, so that a second instance on a second bus costs one line — and the piece of it worth measuring is the sequencer. It makes exactly one promise, that items within a sequence arrive in the order the sequence produced them, and it deliberately promises nothing across sequences because that is what an arbitration policy decides. Measured with twelve items from each of two sequences and both queues full before the first grant: zero order errors under all three policies, so the promise holds; every policy delivered all twelve items from both sequences, so every functional coverage number is identical across the three runs and a coverage report cannot tell you which policy ran. What differs is the interleaving — round robin switched between the streams 23 times and strict priority once — so under priority sequence 1's items all arrive after sequence 0 has finished, the two never overlap, and any bug needing them to overlap is unreachable. And starvation is counted as the longest consecutive wait rather than the total: 1 grant under round robin, 3 under weighted, and all 12 under priority. The total would have been 12 under every policy, so a metric built on it measures queue occupancy rather than fairness.

15. What Comes Next

The environment is complete and reusable. Chapter 17.6 points it at the traffic that breaks designs — including three cases that are perfectly legal and must produce no report at all.

Continue learning