Skip to content
VLSI Mentor

SPI · Module 16

The SPI Interface and Clocking Blocks

Three samplers of the same pins, separated by nothing but when the bench writes them. Both recover every value in the right order, so a payload check passes on both and only the cycle attribution moves, which is how a sampling race survives review.

Chapter 16.1's first testbench drove the pins on the same clock edge its monitor sampled them. Every rule whose precondition was inside a transaction reported zero exercises, and the waveform looked perfect.

That is the subject of this chapter. It is usually taught as a keyword — input #1step, output #2ns — and taught that way it is a thing to copy. So this chapter builds the mechanism first, measures the race that exists without it, and only then shows the declarative form.

Two samplers of the same pins recover the same values in the same order, and disagree about which cycle each value belongs to. Which check notices?

1. The Race, Precisely

A testbench that samples a pin on the same edge that another process drives it has no defined answer. Both events are scheduled at the same simulation time, and whether the sample sees the old value or the new one depends on the order the simulator happens to evaluate them.

The dangerous part is not that the answer is arbitrary. It is that the answer is consistent:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   an INCONSISTENT wrong answer    fails immediately, noisily, on the first run
   a CONSISTENT wrong answer       passes for a year, then changes when the tool
                                   is upgraded, a file is reordered, or an
                                   unrelated process is added

A simulator will give the same answer every time for a given source file. That answer is not portable across tools, across versions, or across edits that have nothing to do with the sampling — and a suite built on it looks reliable right up to the day it does not.

2. What a Clocking Block Actually Specifies

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   input  #1step     sample the pin as it was JUST BEFORE the clock edge
   output #2ns       drive the pin a stated time AFTER the clock edge

The input skew is the important half, and it is the half that is hard to build by hand, because "as it was just before the edge" is not the same as either of the two things people substitute for it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   "one cycle earlier"                     -- stable, and a cycle late
   "whatever the pin reads when my
    process happens to run"                -- the race, restated

3. Three Samplers

The experiment builds three samplers of one set of pins and drives those pins two ways.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   RACY      samples `pins` on the clock edge, with no skew. What a bench does
             by accident.
   SKEWED    samples a copy the INTERFACE took shortly before the edge. This is
             `input #1step`, hand-built.
   DELAYED   registers the skewed sampler's output, so it carries the same
             values in the same order one cycle later. The fix people reach for
             first.

And the stimulus:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CARELESS      the bench writes the pins ON the posedge -- the sampling edge.
   DISCIPLINED   the bench writes them on the NEGEDGE, away from it.

4. The Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   stimulus       cycles   racy-vs-skewed   skewed seq errs   racy seq errs
   on negedge         28                0                 0               0
   on posedge         28               24                 0               0

   values driven per stimulus ........ 24
   distinct values the skewed sampler actually saw: 24 / 24
   delayed vs skewed, same cycle ..... 24 of 28   (must differ: it is a cycle behind)
   delayed vs skewed, shifted a cycle. 0 of 28    (must agree: late is not wrong)

Read the second and third columns together, because their combination is the finding.

Driven on the negedge, the racy sampler agreed with the skewed one on every cycle. Sampling on a clock edge is not the defect. Sampling on an edge somebody else is driving is.

Driven on the posedge, they disagreed on 24 of 28 cycles — and both still recovered all 24 values in the right ORDER. That is the shape that lets a race survive review:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a check that compares payloads, or value sets, or a byte stream    PASSES on both
   a check that reasons about WHICH CYCLE a value belongs to          is now a
                                                                     cycle out

The data is not corrupted. The data is misattributed, and every timing check downstream is wrong while the payload check is green. The report will name the DUT.

And the delayed sampler is shifted rather than broken, proved in both directions: zero disagreements with a one-cycle-delayed copy of the skewed sampler, and 24 disagreements with the undelayed one. A bench that cannot separate late from wrong will accept a shifted monitor and then chase the shift through the scoreboard as if it were a design fault.

5. Where the Discipline Lives

The pre-edge copy belongs in the interface, not in any component, and the argument is about inheritance:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   discipline in the INTERFACE     every connected component samples the same
                                   way, and no component has to remember to

   discipline in a COMPONENT       every other component reimplements it, and
                                   reimplements it slightly differently, and the
                                   two then disagree about cycle boundaries for
                                   reasons nobody connects to sampling

And the second thing an interface holds is direction:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
modport drv (output pins, input clk);                  // may drive, may not peek
modport mon (input pins, input pins_pre, input clk);   // may read, cannot drive

modport mon grants inputs only, so a monitor that assigns to a pin does not compile. That matters more than it looks: a monitor with write access is one that can be made to drive by a single mistyped assignment, and the symptom — a DUT that behaves differently when the monitor is connected — is the hardest class of bug to believe. Chapter 16.7 makes the same argument at the agent level and measures what happens when the guarantee is a runtime condition instead.

6. Building It — Three HDLs

Each language answers this problem differently, and for once the differences are substantive rather than syntactic.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_if.sv — the interface, and why the pre-edge copy belongs in it
// spi_if.sv
//
// Chapter 16.3 -- the interface, and why the pre-edge copy belongs in it.
//
// An interface is usually introduced as a bundle of signals that saves typing in port
// lists. It is that, and the more important thing it is is a place to put the SAMPLING
// DISCIPLINE -- so that every component connected to it samples the same way, and no
// component has to remember to.
//
// This interface carries:
//
//   * the pins themselves
//   * `pins_pre`, a copy taken one delta BEFORE each clock edge, which is what
//     `input #1step` provides in a clocking block
//   * modports, so a driver cannot read what it should not and a monitor cannot drive
//
// THE MODPORTS ARE THE PART THAT PREVENTS A WHOLE CLASS OF BUG.
//
// A monitor with write access to the pins is a monitor that can be made to drive them
// by a single mistyped assignment, and the symptom is a DUT that behaves differently
// when the monitor is connected -- which is the hardest kind of bug to believe. A
// `modport` that grants only inputs makes that assignment a compile error.
//
// Chapter 16.7's passive agent is the same argument at the agent level: the passive
// path must be unable to drive, not merely choose not to.

`timescale 1ns/1ps

interface spi_if #(
    parameter int W   = 4,
    // How long after the negedge the pre-edge copy is taken. Must be strictly less
    // than the clock's half period, and large enough that a driver writing on the
    // negedge has finished. Four fifths of the half period is a deliberate choice:
    // close enough to the edge to be honest about what "just before" means, far
    // enough from the negedge that nothing is racing it.
    parameter int PRE = 4
) (input logic clk);

    logic [W-1:0] pins;

    // The pre-edge copy. `#1step` is not available here, so it is built: a process
    // triggered by the NEGEDGE waits PRE time units and then copies the pins, which
    // places the copy PRE units after the negedge and therefore shortly BEFORE the
    // next sampling edge.
    //
    // THE DELAY IS THE POINT, and copying AT the negedge instead would have moved the
    // race rather than removed it: a driver that also writes the pins on the negedge
    // would then be racing this copy, and the interface would be reproducing the exact
    // defect it exists to prevent. Waiting until the pins have settled makes the copy
    // ordered with respect to every negedge driver regardless of evaluation order.
    //
    // This is not identical to `#1step` -- `#1step` samples in the preponed region,
    // arbitrarily close to the edge -- and the difference matters in exactly one case,
    // stated so nobody is surprised by it: a pin that changes in the last (HALF - PRE)
    // units before the edge is seen by `#1step` and not by this copy. A testbench that
    // drives on the negedge, which is the discipline this whole chapter argues for,
    // never produces that case.
    logic [W-1:0] pins_pre;

    always @(negedge clk) begin
        #(PRE);
        pins_pre = pins;
    end

    // A driver may write the pins and read the clock. It may NOT read `pins_pre`,
    // which is a sampling aid and has no meaning to something that drives.
    modport drv (output pins, input clk);

    // A monitor may read everything and write nothing. This is the modport that makes
    // an accidental drive a compile error rather than a mystery.
    modport mon (input pins, input pins_pre, input clk);

endinterface
Azvya Education Pvt. Ltd.VLSI Mentor
spi_if_sampler.sv — three samplers of the same pins
// spi_if_sampler.sv
//
// Chapter 16.3 -- the interface, and the race a clocking block exists to prevent.
//
// THIS CHAPTER HAS AN UNUSUAL SHAPE, and the reason is worth stating first.
//
// A clocking block is a declarative statement about WHEN a testbench samples and
// drives, and the simulator these examples run in does not implement it. So the
// chapter does the next most useful thing: it BUILDS what a clocking block does,
// measures the race that exists without one, and then publishes the clocking-block
// form as reviewed code in section 6 -- where it can be read as the declarative
// spelling of the mechanism that has just been measured.
//
// That is not a workaround. Building the mechanism first is how the feature becomes
// something other than a keyword to copy.
//
// THE RACE, STATED PRECISELY.
//
// A testbench that samples a pin on the same edge that another process drives it has
// no defined answer. Both events are scheduled at the same simulation time, and
// whether the sample sees the old value or the new one depends on the order the
// simulator happens to evaluate them -- which is deterministic for a given tool and
// arbitrary across tools, versions and even unrelated edits to the file.
//
// This is not hypothetical. Chapter 16.1's first testbench drove the pins on the same
// edge its monitor sampled them, and every rule whose precondition was inside a
// transaction reported ZERO exercises while the waveform looked perfect. The fix was
// to drive on the opposite edge, and the whole content of a clocking block is that the
// fix stops being something each task has to remember.
//
// WHAT A CLOCKING BLOCK ACTUALLY SPECIFIES.
//
//     input  #1step    sample the pin as it was JUST BEFORE the clock edge
//     output #2ns      drive the pin a stated time AFTER the clock edge
//
// The first is the important half and it is the half that is hard to build by hand:
// "as it was just before the edge" is not the same as "one cycle earlier", and it is
// not the same as "whatever the pin reads when my process runs".
//
// THIS MODULE BUILDS THREE SAMPLERS OF THE SAME PINS.
//
//     RACY      samples on the clock edge with no skew. This is what a bench does by
//               accident, and the measurement shows what it costs.
//     SKEWED    samples a copy of the pins taken one delta BEFORE the edge, which is
//               what `input #1step` means. Correct, and hand-built.
//     DELAYED   samples one full cycle late. Correct in the sense of being stable,
//               and WRONG in the sense of being a cycle behind -- included because it
//               is the fix people reach for first and it changes what the values mean.
//
// The bench drives the pins ON the clock edge deliberately, which is the one stimulus
// that separates the three.

module spi_if_sampler #(
    parameter int W = 4
) (
    input  wire          clk,
    input  wire          rst_n,

    // The pins, driven by something else at a time this module does not control.
    input  wire [W-1:0]  pins,

    // A copy of the pins as they were BEFORE this edge, provided by the interface
    // rather than sampled here -- which is the whole trick and is why a clocking
    // block has to be part of the interface and not part of the component.
    input  wire [W-1:0]  pins_pre,

    output reg  [W-1:0]  s_racy,
    output reg  [W-1:0]  s_skewed,
    output reg  [W-1:0]  s_delayed
);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            s_racy    <= {W{1'b0}};
            s_skewed  <= {W{1'b0}};
            s_delayed <= {W{1'b0}};
        end else begin
            // RACY: reads `pins` in the same time step the driver writes it. There is
            // no correct answer here and the simulator will give a consistent wrong
            // one, which is worse than an inconsistent one because it looks reliable.
            s_racy    <= pins;

            // SKEWED: reads the pre-edge copy. This is `input #1step`, built out of a
            // signal the interface maintains.
            s_skewed  <= pins_pre;

            // DELAYED: stable, and a cycle behind -- it registers the skewed
            // sampler's OUTPUT rather than the pins, so it carries the same values in
            // the same order, one cycle later. Included because it is the first fix
            // people reach for, and because being a cycle behind changes what every
            // subsequent comparison MEANS: a monitor built this way reports
            // transactions one cycle late and a scoreboard then has to know, which is
            // how an off-by-one enters a suite disguised as a DUT fault.
            s_delayed <= s_skewed;
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_if_sampler.v — the same three samplers in Verilog-2001
// spi_if_sampler.v
//
// Chapter 16.3 -- the interface, and the race a clocking block exists to prevent.
//
// THIS CHAPTER HAS AN UNUSUAL SHAPE, and the reason is worth stating first.
//
// A clocking block is a declarative statement about WHEN a testbench samples and
// drives, and the simulator these examples run in does not implement it. So the
// chapter does the next most useful thing: it BUILDS what a clocking block does,
// measures the race that exists without one, and then publishes the clocking-block
// form as reviewed code in section 6 -- where it can be read as the declarative
// spelling of the mechanism that has just been measured.
//
// That is not a workaround. Building the mechanism first is how the feature becomes
// something other than a keyword to copy.
//
// THE RACE, STATED PRECISELY.
//
// A testbench that samples a pin on the same edge that another process drives it has
// no defined answer. Both events are scheduled at the same simulation time, and
// whether the sample sees the old value or the new one depends on the order the
// simulator happens to evaluate them -- which is deterministic for a given tool and
// arbitrary across tools, versions and even unrelated edits to the file.
//
// This is not hypothetical. Chapter 16.1's first testbench drove the pins on the same
// edge its monitor sampled them, and every rule whose precondition was inside a
// transaction reported ZERO exercises while the waveform looked perfect. The fix was
// to drive on the opposite edge, and the whole content of a clocking block is that the
// fix stops being something each task has to remember.
//
// WHAT A CLOCKING BLOCK ACTUALLY SPECIFIES.
//
//     input  #1step    sample the pin as it was JUST BEFORE the clock edge
//     output #2ns      drive the pin a stated time AFTER the clock edge
//
// The first is the important half and it is the half that is hard to build by hand:
// "as it was just before the edge" is not the same as "one cycle earlier", and it is
// not the same as "whatever the pin reads when my process runs".
//
// THIS MODULE BUILDS THREE SAMPLERS OF THE SAME PINS.
//
//     RACY      samples on the clock edge with no skew. This is what a bench does by
//               accident, and the measurement shows what it costs.
//     SKEWED    samples a copy of the pins taken one delta BEFORE the edge, which is
//               what `input #1step` means. Correct, and hand-built.
//     DELAYED   samples one full cycle late. Correct in the sense of being stable,
//               and WRONG in the sense of being a cycle behind -- included because it
//               is the fix people reach for first and it changes what the values mean.
//
// The bench drives the pins ON the clock edge deliberately, which is the one stimulus
// that separates the three.

module spi_if_sampler #(
    parameter W = 4
) (
    input  wire          clk,
    input  wire          rst_n,

    // The pins, driven by something else at a time this module does not control.
    input  wire [W-1:0]  pins,

    // A copy of the pins as they were BEFORE this edge, provided by the interface
    // rather than sampled here -- which is the whole trick and is why a clocking
    // block has to be part of the interface and not part of the component.
    input  wire [W-1:0]  pins_pre,

    output reg  [W-1:0]  s_racy,
    output reg  [W-1:0]  s_skewed,
    output reg  [W-1:0]  s_delayed
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            s_racy    <= {W{1'b0}};
            s_skewed  <= {W{1'b0}};
            s_delayed <= {W{1'b0}};
        end else begin
            // RACY: reads `pins` in the same time step the driver writes it. There is
            // no correct answer here and the simulator will give a consistent wrong
            // one, which is worse than an inconsistent one because it looks reliable.
            s_racy    <= pins;

            // SKEWED: reads the pre-edge copy. This is `input #1step`, built out of a
            // signal the interface maintains.
            s_skewed  <= pins_pre;

            // DELAYED: stable, and a cycle behind -- it registers the skewed
            // sampler's OUTPUT rather than the pins, so it carries the same values in
            // the same order, one cycle later. Included because it is the first fix
            // people reach for, and because being a cycle behind changes what every
            // subsequent comparison MEANS: a monitor built this way reports
            // transactions one cycle late and a scoreboard then has to know, which is
            // how an off-by-one enters a suite disguised as a DUT fault.
            s_delayed <= s_skewed;
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_if_sampler.vhd — the same three samplers in VHDL, with the bundle as a record
-- spi_if_sampler.vhd
--
-- Chapter 16.3 -- the interface, the race a clocking block exists to prevent, and the
-- one place where VHDL's answer is genuinely different rather than merely differently
-- spelled.
--
-- VHDL SPLITS THE PROBLEM INTO THREE HALVES AND SOLVES TWO OF THEM WITHOUT ASKING.
--
--   BUNDLING      is a RECORD in a package. `spi_pins_t` below travels as one port and
--                 grows a field without touching a single instantiation, which is the
--                 practical half of what an interface buys.
--
--   ENFORCEMENT   is the PORT MODE, and it is free. A port of mode `in` cannot be
--                 assigned, and that is checked by the analyser on every component in
--                 the design. SystemVerilog needs a `modport` to get the same guarantee
--                 across an interface; VHDL has it by default and has always had it.
--
--   TIMING        is the half VHDL does not bundle. There is no construct that says
--                 "sample this record as it was just before the clock edge", so the
--                 pre-edge copy is a signal somebody has to maintain -- the `pins_pre`
--                 field below -- and every component trusts that somebody did.
--
-- AND THE RACE ITSELF IS HARDER TO WRITE HERE, WHICH IS WORTH BEING PRECISE ABOUT.
--
-- A VHDL signal assignment never takes effect in the delta cycle that executes it. So a
-- process that reads a signal on a clock edge sees the value that signal held BEFORE
-- the edge, always, and the careless SystemVerilog defect -- a blocking write to a pin
-- in the same time step a sampler reads it -- has no VHDL spelling. You cannot reach it
-- by forgetting.
--
-- You can still reach it DELIBERATELY, and the testbench does: an assignment written
-- with a delay that lands exactly ON the sampling edge updates the signal in the same
-- delta the clock changes, and a process sensitive to the clock then reads the new
-- value. That produces the identical defect -- a value attributed to the edge on which
-- it arrived, one cycle early -- and it is how the race actually appears in VHDL
-- suites, usually as a pin model written with `after` to mimic an output delay.
--
-- THE THREE SAMPLERS ARE THE SAME THREE.
--
--   RACY      reads the pins field on the edge, with no skew.
--   SKEWED    reads the pre-edge copy. This is `input #1step`, hand-maintained.
--   DELAYED   registers the skewed sampler's output, so it carries the same values in
--             the same order one cycle later -- the fix people reach for first, and one
--             that changes what every downstream comparison MEANS.

library ieee;
use ieee.std_logic_1164.all;

package spi_if_pkg is

    -- Fixed here rather than generic, because a record field cannot be sized by an
    -- entity's generic: the type has to exist before any entity uses it. That is a real
    -- constraint of the bundling-by-record approach and it is the reason a VHDL project
    -- usually fixes its bus widths in a package rather than passing them down.
    constant PIN_W : natural := 4;

    subtype pin_vec_t is std_logic_vector(PIN_W - 1 downto 0);

    -- The bundle. One port instead of two, and a third field added later costs nothing
    -- at any instantiation site.
    type spi_pins_t is record
        pins     : pin_vec_t;   -- the pins themselves
        pins_pre : pin_vec_t;   -- the same pins as they were shortly before this edge
    end record;

end package spi_if_pkg;

library ieee;
use ieee.std_logic_1164.all;
use work.spi_if_pkg.all;

entity spi_if_sampler is
    port (
        clk      : in  std_logic;
        rst_n    : in  std_logic;

        -- Mode `in`, so no statement in this entity can assign to any field of it. That
        -- is the `modport mon` guarantee, obtained from the port list rather than from a
        -- separate declaration, and checked by the analyser.
        bus_in   : in  spi_pins_t;

        s_racy    : out pin_vec_t;
        s_skewed  : out pin_vec_t;
        s_delayed : out pin_vec_t
    );
end entity spi_if_sampler;

architecture rtl of spi_if_sampler is

    signal racy_r    : pin_vec_t := (others => '0');
    signal skewed_r  : pin_vec_t := (others => '0');
    signal delayed_r : pin_vec_t := (others => '0');

begin

    sample : process (clk, rst_n) is
    begin
        if rst_n = '0' then
            racy_r    <= (others => '0');
            skewed_r  <= (others => '0');
            delayed_r <= (others => '0');
        elsif rising_edge(clk) then
            -- RACY. In VHDL this reads the pre-edge value whenever the driver used an
            -- ordinary assignment, because an ordinary assignment cannot land in this
            -- delta. It reads the NEW value only when the driver scheduled the update
            -- to arrive exactly at this time -- which the testbench does on purpose,
            -- because that is the VHDL spelling of the defect.
            racy_r    <= bus_in.pins;

            -- SKEWED. The pre-edge copy, maintained outside this entity, which is the
            -- whole trick: the discipline belongs to the bundle, not to the component.
            skewed_r  <= bus_in.pins_pre;

            -- DELAYED. The same values, one cycle later. Stable, and a cycle behind --
            -- correct in the first sense and wrong in the second, and a monitor built
            -- this way reports every transaction late and a scoreboard then has to know.
            delayed_r <= skewed_r;
        end if;
    end process sample;

    s_racy    <= racy_r;
    s_skewed  <= skewed_r;
    s_delayed <= delayed_r;

end architecture rtl;

The Bench

Azvya Education Pvt. Ltd.VLSI Mentor
spi_if_sampler_tb.sv — one stimulus driven two ways, four measurements
// spi_if_sampler_tb.sv
//
// One stimulus driven two ways, three samplers of the same pins, and four measurements.
//
// THE EXPERIMENT.
//
// The pins carry a strictly increasing sequence, one new value per clock cycle, and the
// only thing that changes between the two halves of this bench is WHEN the bench writes
// them:
//
//   CARELESS      the bench writes the pins ON the posedge -- the same edge the
//                 samplers use. This is what a bench does by accident.
//   DISCIPLINED   the bench writes them on the NEGEDGE, away from the sampling edge.
//
// THE FOUR MEASUREMENTS, and the reason each one is here:
//
//   1. Under the discipline, the skewed sampler recovers the sequence EXACTLY. This is
//      the positive claim: a pre-edge copy is not an approximation of anything, it is
//      the right answer, and it is what `input #1step` buys.
//
//   2. Under the discipline, the racy sampler AGREES with the skewed one on every
//      cycle. The race is not a property of sampling on an edge; it is a property of
//      sampling on an edge somebody else is driving.
//
//   3. Under carelessness, the two DISAGREE. And the shape of the disagreement is the
//      part worth reading twice: both samplers still recover the same VALUES in the
//      same ORDER -- what moves is the cycle each value is attributed to. A checker
//      that compares value sets, or payload bytes, or anything that is not anchored to
//      a cycle, passes on both. Everything that reasons about timing is now a cycle
//      out, and the report will name the DUT.
//
//   4. The delayed sampler is CORRECT and LATE, proven in both directions: it never
//      disagrees with a one-cycle-delayed copy of the skewed sampler, and it always
//      disagrees with the undelayed one. That pair of results is the difference between
//      "broken" and "shifted", and a bench that cannot tell them apart will accept a
//      shifted monitor and then chase the shift through the scoreboard.
//
// The modports are checked by the COMPILER rather than by any of this, and section 4 of
// the chapter says what happens when they are not.

`timescale 1ns/1ps

module spi_if_sampler_tb;

    localparam int W    = 4;
    localparam int HALF = 5;     // clock half period
    localparam int PRE  = 4;     // where in that half period the pre-edge copy lands

    logic clk = 1'b0;
    always #(HALF) clk = ~clk;

    logic rst_n = 1'b1;

    // The interface. The pre-edge copy lives HERE, not in any component, which is the
    // architectural claim of the chapter: a sampling discipline that lives in the
    // interface is one that every connected component inherits, and one that lives in
    // a component is one every other component has to reimplement correctly.
    spi_if #(.W(W), .PRE(PRE)) bus (.clk(clk));

    wire [W-1:0] s_racy, s_skewed, s_delayed;

    spi_if_sampler #(.W(W)) u_s (
        .clk(clk), .rst_n(rst_n),
        .pins(bus.pins), .pins_pre(bus.pins_pre),
        .s_racy(s_racy), .s_skewed(s_skewed), .s_delayed(s_delayed)
    );

    integer errors = 0;

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

    // ------------------------------------------------------------------
    // The recorders.
    //
    // Two kinds, and the distinction between them is the whole of measurement 3.
    //
    // A CHANGE RECORDER logs the sequence of distinct consecutive values a sampler
    // produced. It is alignment-free and it is therefore blind to timing.
    //
    // A CYCLE COMPARATOR logs whether two samplers disagreed on a given cycle. It sees
    // nothing but timing.
    //
    // A race shows up in the second and not in the first, which is exactly why it
    // survives review.
    // ------------------------------------------------------------------

    reg         collect;
    integer     n_sent, n_got_s, n_got_r;
    reg [W-1:0] sent  [0:63];
    reg [W-1:0] got_s [0:63];
    reg [W-1:0] got_r [0:63];
    reg [W-1:0] prev_s, prev_r;

    integer n_cyc, n_rs_diff, n_ds_diff, n_ds_shift_bad;
    reg [W-1:0] skew_d;

    // Every read here happens in the active region of the posedge, so it sees the
    // values the samplers registered on the PREVIOUS edge -- consistently, for all
    // three, which is what makes comparing them meaningful.
    always @(posedge clk) begin
        if (!rst_n) begin
            skew_d <= {W{1'b0}};
        end else begin
            skew_d <= s_skewed;

            if (collect) begin
                // change recorders
                if (s_skewed !== prev_s) begin
                    if (n_got_s < 64) got_s[n_got_s] = s_skewed;
                    n_got_s = n_got_s + 1;
                    prev_s  = s_skewed;
                end
                if (s_racy !== prev_r) begin
                    if (n_got_r < 64) got_r[n_got_r] = s_racy;
                    n_got_r = n_got_r + 1;
                    prev_r  = s_racy;
                end

                // cycle comparators
                n_cyc = n_cyc + 1;
                if (s_racy    !== s_skewed) n_rs_diff      = n_rs_diff + 1;
                if (s_delayed !== s_skewed) n_ds_diff      = n_ds_diff + 1;
                if (s_delayed !== skew_d)   n_ds_shift_bad = n_ds_shift_bad + 1;
            end
        end
    end

    // ------------------------------------------------------------------
    // The stimulus. `on_edge` selects the careless variant.
    // ------------------------------------------------------------------
    task automatic send(input integer n, input integer on_edge);
        integer i;
        begin
            for (i = 0; i < n; i = i + 1) begin
                if (on_edge != 0) @(posedge clk);
                else              @(negedge clk);
                bus.pins = (i % 15) + 1;               // 1..15, never 0, never repeats
                if (n_sent < 64) sent[n_sent] = (i % 15) + 1;
                n_sent = n_sent + 1;
            end
        end
    endtask

    task automatic restart;
        begin
            collect = 1'b0;
            @(negedge clk);
            bus.pins = {W{1'b0}};
            rst_n    = 1'b1;
            repeat (2) @(negedge clk);
            rst_n    = 1'b0;
            repeat (4) @(negedge clk);
            rst_n    = 1'b1;
            repeat (4) @(negedge clk);
            n_sent   = 0; n_got_s = 0; n_got_r = 0;
            prev_s   = {W{1'b0}};
            prev_r   = {W{1'b0}};
            n_cyc    = 0; n_rs_diff = 0; n_ds_diff = 0; n_ds_shift_bad = 0;
        end
    endtask

    // Compares a recorded change sequence against the intended one. Returns the number
    // of positions that differ, counting a length mismatch as a difference per position.
    function automatic integer seq_bad(input integer which);
        integer i, bad, ng;
        begin
            bad = 0;
            ng  = (which == 0) ? n_got_s : n_got_r;
            if (ng != n_sent) bad = bad + ((ng > n_sent) ? (ng - n_sent) : (n_sent - ng));
            for (i = 0; i < n_sent; i = i + 1)
                if (i < ng) begin
                    if (which == 0) begin
                        if (got_s[i] !== sent[i]) bad = bad + 1;
                    end else begin
                        if (got_r[i] !== sent[i]) bad = bad + 1;
                    end
                end
            seq_bad = bad;
        end
    endfunction

    integer d_rs_diff, d_seq_s, d_seq_r, d_cyc, d_ngs;
    integer c_rs_diff, c_seq_s, c_seq_r, c_cyc, c_ngs;
    integer d_ds_diff, d_ds_shift_bad;

    localparam int NVAL = 24;

    initial begin
        // ==================================================================
        // A. THE DISCIPLINE: the bench writes the pins on the NEGEDGE.
        // ==================================================================
        restart();
        collect = 1'b1;
        send(NVAL, 0);
        repeat (4) @(posedge clk);
        collect = 1'b0;
        d_cyc = n_cyc; d_rs_diff = n_rs_diff;
        d_ds_diff = n_ds_diff; d_ds_shift_bad = n_ds_shift_bad;
        d_seq_s = seq_bad(0); d_seq_r = seq_bad(1); d_ngs = n_got_s;

        // ==================================================================
        // B. CARELESSNESS: the bench writes the pins ON the posedge.
        // ==================================================================
        restart();
        collect = 1'b1;
        send(NVAL, 1);
        repeat (4) @(posedge clk);
        collect = 1'b0;
        c_cyc = n_cyc; c_rs_diff = n_rs_diff;
        c_seq_s = seq_bad(0); c_seq_r = seq_bad(1); c_ngs = n_got_s;

        // ==================================================================
        // The table. Two stimuli, the same three samplers.
        // ==================================================================
        $display("  stimulus       cycles   racy-vs-skewed   skewed seq errs   racy seq errs");
        $display("  on negedge   %8d   %14d   %15d   %13d   the discipline",
                 d_cyc, d_rs_diff, d_seq_s, d_seq_r);
        $display("  on posedge   %8d   %14d   %15d   %13d   the accident",
                 c_cyc, c_rs_diff, c_seq_s, c_seq_r);
        $display("  values driven per stimulus ........ %0d", NVAL);
        $display("  distinct values the skewed sampler actually saw: %0d on the negedge stimulus, %0d on the posedge one  (a zero here would make every agreement below vacuous)",
                 d_ngs, c_ngs);
        $display("  delayed vs skewed, same cycle ..... %0d of %0d  (must differ: it is a cycle behind)",
                 d_ds_diff, d_cyc);
        $display("  delayed vs skewed, shifted a cycle. %0d of %0d  (must agree: being late is not being wrong)",
                 d_ds_shift_bad, d_cyc);

        // ------------- measurement 1: sequence recovery -------------
        if (d_seq_s != 0) begin
            $display("  FAIL: under the discipline the skewed sampler mismatched the intended sequence in %0d position(s); a pre-edge copy must recover it exactly",
                     d_seq_s);
            errors = errors + 1;
        end
        $display("    1. the skewed sampler recovered all %0d values in order. A pre-edge copy is not an approximation -- it is the right answer, and it is what `input #1step` buys",
                 NVAL);

        // ------------- measurement 2: no race when nothing races -------------
        //
        // The vacuity guard comes FIRST, because agreement between two observers that
        // both read a constant is not evidence of anything. The VHDL sibling of this
        // bench hit exactly that: a second driver on the pins resolved them to X, both
        // samplers read a constant, and measurement 2 passed for the worst reason there
        // is. Check that something was observed before believing any agreement.
        if (d_ngs != NVAL) begin
            $display("  FAIL: the skewed sampler recorded %0d distinct values where %0d were driven, so every agreement below is vacuous",
                     d_ngs, NVAL);
            errors = errors + 1;
        end
        if (d_rs_diff != 0) begin
            $display("  FAIL: under the discipline the racy sampler disagreed with the skewed one on %0d of %0d cycles; with the stimulus off the sampling edge there is nothing to race",
                     d_rs_diff, d_cyc);
            errors = errors + 1;
        end
        $display("    2. with the stimulus on the NEGEDGE the racy sampler agreed with the skewed one on all %0d cycles. Sampling on a clock edge is not the defect -- sampling on an edge somebody else is driving is",
                 d_cyc);

        // ------------- measurement 3: the race, and its shape -------------
        if (c_rs_diff == 0) begin
            $display("  FAIL: writing the pins ON the posedge produced no disagreement between the racy and skewed samplers, so this stimulus did not create the race and the experiment measured nothing");
            errors = errors + 1;
        end
        $display("    3. with the stimulus ON the posedge they disagreed on %0d of %0d cycles -- and the shape of that disagreement is the part to read twice",
                 c_rs_diff, c_cyc);
        if (c_seq_s != 0 || c_seq_r != 0) begin
            $display("  NOTE: under carelessness the recorded value sequences also differ from the intended one (skewed %0d, racy %0d), which is a stronger failure than the timing shift and is tool-dependent",
                     c_seq_s, c_seq_r);
        end else begin
            $display("       both samplers still recovered all %0d values in the right ORDER. A checker that compares payloads, or value sets, or anything not anchored to a cycle, passes on BOTH. Only the cycle each value is attributed to moved -- and every check that reasons about timing is now a cycle out, and its report will name the DUT",
                     NVAL);
        end

        // ------------- measurement 4: late is not wrong -------------
        if (d_ds_shift_bad != 0) begin
            $display("  FAIL: the delayed sampler disagreed with a one-cycle-delayed copy of the skewed sampler on %0d of %0d cycles; it is meant to be shifted, not broken",
                     d_ds_shift_bad, d_cyc);
            errors = errors + 1;
        end
        if (d_ds_diff == 0) begin
            $display("  FAIL: the delayed sampler never disagreed with the undelayed one, so it is not actually delayed and measurement 4 proved nothing");
            errors = errors + 1;
        end
        $display("    4. the delayed sampler disagreed with the skewed one on %0d of %0d cycles and with a one-cycle-delayed copy of it on %0d. It is SHIFTED, not broken -- and a bench that cannot tell those apart accepts a shifted monitor and then chases the shift through the scoreboard as if it were a DUT fault",
                 d_ds_diff, d_cyc, d_ds_shift_bad);

        // ------------- the compile-time half -------------
        $display("    5. and the modports are enforced by the COMPILER rather than here: `modport mon` grants only inputs, so a monitor that assigns to a pin does not build. That matters because a monitor which CAN drive produces a DUT that behaves differently when the monitor is connected, and that is the hardest class of bug to believe");

        if (errors == 0)
            $display("PASS: a testbench that samples a pin on the same edge another process drives it has no defined answer, and the simulator supplies a consistent one -- worse than an inconsistent one, because it looks reliable and does not survive a tool change. The same three samplers of the same pins separate completely on nothing but WHEN the bench writes: driven on the NEGEDGE, the skewed sampler recovered all values in order and the racy sampler agreed with it on every one of the cycles measured, so sampling on a clock edge is not the defect and sampling on an edge somebody else drives is. Driven ON the posedge they disagreed, and the shape of the disagreement is what lets a race survive review: both samplers still recovered every value in the right ORDER, so a payload check passes on both, and only the cycle each value is attributed to moved -- which silently breaks every check that reasons about timing and points the report at the DUT. The delayed sampler, the fix people reach for first, was proven SHIFTED rather than broken in both directions -- never disagreeing with a one-cycle-delayed copy of the skewed sampler and always disagreeing with the undelayed one -- because a bench that cannot separate `late` from `wrong` will accept a shifted monitor and chase the shift through the scoreboard. The pre-edge copy therefore belongs in the INTERFACE, where every connected component inherits it, rather than in a component, where every other component must reimplement it correctly; and the interface's modports are enforced by the compiler, so a monitor that assigns to a pin does not build at all");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_if_sampler_tb.v — the same bench in Verilog-2001, which has no interface
// spi_if_sampler_tb.v
//
// VERILOG-2001 HAS NO `interface`, AND THAT IS THIS CHAPTER'S LANGUAGE COMPARISON.
//
// Everything measured below is measured identically here, because the MECHANISM -- a
// copy of the pins taken shortly before the sampling edge -- is ordinary Verilog. What
// Verilog-2001 cannot do is put that mechanism somewhere every component inherits it.
// So the pins and the pre-edge copy are declared in the BENCH, and every component that
// wants correct sampling has to be handed `pins_pre` and has to be trusted to use it.
//
// The two consequences are worth naming, because they are the actual argument for the
// SystemVerilog construct rather than a matter of taste:
//
//   * THE DISCIPLINE IS NOT INHERITED. A second bench, or a second monitor in this one,
//     reimplements the copy -- and reimplements it slightly differently, and the two
//     then disagree on cycle boundaries for reasons nobody connects to sampling.
//   * THERE IS NO MODPORT, SO NOTHING IS ENFORCED. `pins` is a plain reg in the bench's
//     scope. A monitor task that assigns to it compiles cleanly, and the symptom is a
//     DUT that behaves differently when the monitor is present.
//
// A Verilog-2001 suite handles both by convention: one file owns the pins, monitors take
// them as inputs only, and code review enforces it. That works, and it is enforced by
// people rather than by the compiler, which is a different quality of guarantee.
//
// One stimulus driven two ways, three samplers of the same pins, and four measurements.
//
// THE EXPERIMENT.
//
// The pins carry a strictly increasing sequence, one new value per clock cycle, and the
// only thing that changes between the two halves of this bench is WHEN the bench writes
// them:
//
//   CARELESS      the bench writes the pins ON the posedge -- the same edge the
//                 samplers use. This is what a bench does by accident.
//   DISCIPLINED   the bench writes them on the NEGEDGE, away from the sampling edge.
//
// THE FOUR MEASUREMENTS, and the reason each one is here:
//
//   1. Under the discipline, the skewed sampler recovers the sequence EXACTLY. This is
//      the positive claim: a pre-edge copy is not an approximation of anything, it is
//      the right answer, and it is what `input #1step` buys.
//
//   2. Under the discipline, the racy sampler AGREES with the skewed one on every
//      cycle. The race is not a property of sampling on an edge; it is a property of
//      sampling on an edge somebody else is driving.
//
//   3. Under carelessness, the two DISAGREE. And the shape of the disagreement is the
//      part worth reading twice: both samplers still recover the same VALUES in the
//      same ORDER -- what moves is the cycle each value is attributed to. A checker
//      that compares value sets, or payload bytes, or anything that is not anchored to
//      a cycle, passes on both. Everything that reasons about timing is now a cycle
//      out, and the report will name the DUT.
//
//   4. The delayed sampler is CORRECT and LATE, proven in both directions: it never
//      disagrees with a one-cycle-delayed copy of the skewed sampler, and it always
//      disagrees with the undelayed one. That pair of results is the difference between
//      "broken" and "shifted", and a bench that cannot tell them apart will accept a
//      shifted monitor and then chase the shift through the scoreboard.
//
// The fifth line of the log is the one Verilog-2001 cannot turn into a measurement: the
// SystemVerilog version's modport makes an accidental drive a build error, and here it
// is only a convention. Section 4 of the chapter says what that costs.

`timescale 1ns/1ps

module spi_if_sampler_tb;

    localparam W    = 4;
    localparam HALF = 5;     // clock half period
    localparam PRE  = 4;     // where in that half period the pre-edge copy lands

    reg clk;
    always #(HALF) clk = ~clk;

    reg rst_n;

    // No interface: the pins and their pre-edge copy are the bench's own signals.
    reg [W-1:0] pins;
    reg [W-1:0] pins_pre;

    // The pre-edge copy, PRE units after the negedge and therefore shortly before the
    // next sampling edge. The delay is what keeps it from racing a negedge driver --
    // copying AT the negedge would move the race rather than remove it.
    always @(negedge clk) begin
        #(PRE);
        pins_pre = pins;
    end

    wire [W-1:0] s_racy, s_skewed, s_delayed;

    spi_if_sampler #(.W(W)) u_s (
        .clk(clk), .rst_n(rst_n),
        .pins(pins), .pins_pre(pins_pre),
        .s_racy(s_racy), .s_skewed(s_skewed), .s_delayed(s_delayed)
    );

    integer errors;

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

    // ------------------------------------------------------------------
    // The recorders.
    //
    // Two kinds, and the distinction between them is the whole of measurement 3.
    //
    // A CHANGE RECORDER logs the sequence of distinct consecutive values a sampler
    // produced. It is alignment-free and it is therefore blind to timing.
    //
    // A CYCLE COMPARATOR logs whether two samplers disagreed on a given cycle. It sees
    // nothing but timing.
    //
    // A race shows up in the second and not in the first, which is exactly why it
    // survives review.
    // ------------------------------------------------------------------

    reg         collect;
    integer     n_sent, n_got_s, n_got_r;
    reg [W-1:0] sent  [0:63];
    reg [W-1:0] got_s [0:63];
    reg [W-1:0] got_r [0:63];
    reg [W-1:0] prev_s, prev_r;

    integer n_cyc, n_rs_diff, n_ds_diff, n_ds_shift_bad;
    reg [W-1:0] skew_d;

    // Every read here happens in the active region of the posedge, so it sees the
    // values the samplers registered on the PREVIOUS edge -- consistently, for all
    // three, which is what makes comparing them meaningful.
    always @(posedge clk) begin
        if (!rst_n) begin
            skew_d <= {W{1'b0}};
        end else begin
            skew_d <= s_skewed;

            if (collect) begin
                // change recorders
                if (s_skewed !== prev_s) begin
                    if (n_got_s < 64) got_s[n_got_s] = s_skewed;
                    n_got_s = n_got_s + 1;
                    prev_s  = s_skewed;
                end
                if (s_racy !== prev_r) begin
                    if (n_got_r < 64) got_r[n_got_r] = s_racy;
                    n_got_r = n_got_r + 1;
                    prev_r  = s_racy;
                end

                // cycle comparators
                n_cyc = n_cyc + 1;
                if (s_racy    !== s_skewed) n_rs_diff      = n_rs_diff + 1;
                if (s_delayed !== s_skewed) n_ds_diff      = n_ds_diff + 1;
                if (s_delayed !== skew_d)   n_ds_shift_bad = n_ds_shift_bad + 1;
            end
        end
    end

    // ------------------------------------------------------------------
    // The stimulus. `on_edge` selects the careless variant.
    // ------------------------------------------------------------------
        task send;
        input integer n;
        input integer on_edge;
        integer i;
        begin
            for (i = 0; i < n; i = i + 1) begin
                if (on_edge != 0) @(posedge clk);
                else              @(negedge clk);
                pins = (i % 15) + 1;               // 1..15, never 0, never repeats
                if (n_sent < 64) sent[n_sent] = (i % 15) + 1;
                n_sent = n_sent + 1;
            end
        end
    endtask

    task restart;
        begin
            collect = 1'b0;
            @(negedge clk);
            pins = {W{1'b0}};
            rst_n    = 1'b1;
            repeat (2) @(negedge clk);
            rst_n    = 1'b0;
            repeat (4) @(negedge clk);
            rst_n    = 1'b1;
            repeat (4) @(negedge clk);
            n_sent   = 0; n_got_s = 0; n_got_r = 0;
            prev_s   = {W{1'b0}};
            prev_r   = {W{1'b0}};
            n_cyc    = 0; n_rs_diff = 0; n_ds_diff = 0; n_ds_shift_bad = 0;
        end
    endtask

    // Compares a recorded change sequence against the intended one. Returns the number
    // of positions that differ, counting a length mismatch as a difference per position.
        function integer seq_bad;
        input integer which;
        integer i, bad, ng;
        begin
            bad = 0;
            ng  = (which == 0) ? n_got_s : n_got_r;
            if (ng != n_sent) bad = bad + ((ng > n_sent) ? (ng - n_sent) : (n_sent - ng));
            for (i = 0; i < n_sent; i = i + 1)
                if (i < ng) begin
                    if (which == 0) begin
                        if (got_s[i] !== sent[i]) bad = bad + 1;
                    end else begin
                        if (got_r[i] !== sent[i]) bad = bad + 1;
                    end
                end
            seq_bad = bad;
        end
    endfunction

    integer d_rs_diff, d_seq_s, d_seq_r, d_cyc, d_ngs;
    integer c_rs_diff, c_seq_s, c_seq_r, c_cyc, c_ngs;
    integer d_ds_diff, d_ds_shift_bad;

    localparam NVAL = 24;

    initial begin
        // ==================================================================
        // A. THE DISCIPLINE: the bench writes the pins on the NEGEDGE.
        // ==================================================================
        restart();
        collect = 1'b1;
        send(NVAL, 0);
        repeat (4) @(posedge clk);
        collect = 1'b0;
        d_cyc = n_cyc; d_rs_diff = n_rs_diff;
        d_ds_diff = n_ds_diff; d_ds_shift_bad = n_ds_shift_bad;
        d_seq_s = seq_bad(0); d_seq_r = seq_bad(1); d_ngs = n_got_s;

        // ==================================================================
        // B. CARELESSNESS: the bench writes the pins ON the posedge.
        // ==================================================================
        restart();
        collect = 1'b1;
        send(NVAL, 1);
        repeat (4) @(posedge clk);
        collect = 1'b0;
        c_cyc = n_cyc; c_rs_diff = n_rs_diff;
        c_seq_s = seq_bad(0); c_seq_r = seq_bad(1); c_ngs = n_got_s;

        // ==================================================================
        // The table. Two stimuli, the same three samplers.
        // ==================================================================
        $display("  stimulus       cycles   racy-vs-skewed   skewed seq errs   racy seq errs");
        $display("  on negedge   %8d   %14d   %15d   %13d   the discipline",
                 d_cyc, d_rs_diff, d_seq_s, d_seq_r);
        $display("  on posedge   %8d   %14d   %15d   %13d   the accident",
                 c_cyc, c_rs_diff, c_seq_s, c_seq_r);
        $display("  values driven per stimulus ........ %0d", NVAL);
        $display("  distinct values the skewed sampler actually saw: %0d on the negedge stimulus, %0d on the posedge one  (a zero here would make every agreement below vacuous)",
                 d_ngs, c_ngs);
        $display("  delayed vs skewed, same cycle ..... %0d of %0d  (must differ: it is a cycle behind)",
                 d_ds_diff, d_cyc);
        $display("  delayed vs skewed, shifted a cycle. %0d of %0d  (must agree: being late is not being wrong)",
                 d_ds_shift_bad, d_cyc);

        // ------------- measurement 1: sequence recovery -------------
        if (d_seq_s != 0) begin
            $display("  FAIL: under the discipline the skewed sampler mismatched the intended sequence in %0d position(s); a pre-edge copy must recover it exactly",
                     d_seq_s);
            errors = errors + 1;
        end
        $display("    1. the skewed sampler recovered all %0d values in order. A pre-edge copy is not an approximation -- it is the right answer, and it is what `input #1step` buys",
                 NVAL);

        // ------------- measurement 2: no race when nothing races -------------
        //
        // The vacuity guard comes FIRST, because agreement between two observers that
        // both read a constant is not evidence of anything. The VHDL sibling of this
        // bench hit exactly that: a second driver on the pins resolved them to X, both
        // samplers read a constant, and measurement 2 passed for the worst reason there
        // is. Check that something was observed before believing any agreement.
        if (d_ngs != NVAL) begin
            $display("  FAIL: the skewed sampler recorded %0d distinct values where %0d were driven, so every agreement below is vacuous",
                     d_ngs, NVAL);
            errors = errors + 1;
        end
        if (d_rs_diff != 0) begin
            $display("  FAIL: under the discipline the racy sampler disagreed with the skewed one on %0d of %0d cycles; with the stimulus off the sampling edge there is nothing to race",
                     d_rs_diff, d_cyc);
            errors = errors + 1;
        end
        $display("    2. with the stimulus on the NEGEDGE the racy sampler agreed with the skewed one on all %0d cycles. Sampling on a clock edge is not the defect -- sampling on an edge somebody else is driving is",
                 d_cyc);

        // ------------- measurement 3: the race, and its shape -------------
        if (c_rs_diff == 0) begin
            $display("  FAIL: writing the pins ON the posedge produced no disagreement between the racy and skewed samplers, so this stimulus did not create the race and the experiment measured nothing");
            errors = errors + 1;
        end
        $display("    3. with the stimulus ON the posedge they disagreed on %0d of %0d cycles -- and the shape of that disagreement is the part to read twice",
                 c_rs_diff, c_cyc);
        if (c_seq_s != 0 || c_seq_r != 0) begin
            $display("  NOTE: under carelessness the recorded value sequences also differ from the intended one (skewed %0d, racy %0d), which is a stronger failure than the timing shift and is tool-dependent",
                     c_seq_s, c_seq_r);
        end else begin
            $display("       both samplers still recovered all %0d values in the right ORDER. A checker that compares payloads, or value sets, or anything not anchored to a cycle, passes on BOTH. Only the cycle each value is attributed to moved -- and every check that reasons about timing is now a cycle out, and its report will name the DUT",
                     NVAL);
        end

        // ------------- measurement 4: late is not wrong -------------
        if (d_ds_shift_bad != 0) begin
            $display("  FAIL: the delayed sampler disagreed with a one-cycle-delayed copy of the skewed sampler on %0d of %0d cycles; it is meant to be shifted, not broken",
                     d_ds_shift_bad, d_cyc);
            errors = errors + 1;
        end
        if (d_ds_diff == 0) begin
            $display("  FAIL: the delayed sampler never disagreed with the undelayed one, so it is not actually delayed and measurement 4 proved nothing");
            errors = errors + 1;
        end
        $display("    4. the delayed sampler disagreed with the skewed one on %0d of %0d cycles and with a one-cycle-delayed copy of it on %0d. It is SHIFTED, not broken -- and a bench that cannot tell those apart accepts a shifted monitor and then chases the shift through the scoreboard as if it were a DUT fault",
                 d_ds_diff, d_cyc, d_ds_shift_bad);

        // ------------- the part Verilog-2001 cannot check -------------
        $display("    5. and there is no modport to check: `pins` is a plain reg in this bench, so a monitor that assigns to it compiles cleanly. The SystemVerilog version makes that a build error. Here it is a convention -- one file owns the pins, monitors take them as inputs -- enforced by people rather than by the compiler, which is a different quality of guarantee");

        if (errors == 0)
            $display("PASS: a testbench that samples a pin on the same edge another process drives it has no defined answer, and the simulator supplies a consistent one -- worse than an inconsistent one, because it looks reliable and does not survive a tool change. The same three samplers of the same pins separate completely on nothing but WHEN the bench writes: driven on the NEGEDGE, the skewed sampler recovered all values in order and the racy sampler agreed with it on every one of the cycles measured, so sampling on a clock edge is not the defect and sampling on an edge somebody else drives is. Driven ON the posedge they disagreed, and the shape of the disagreement is what lets a race survive review: both samplers still recovered every value in the right ORDER, so a payload check passes on both, and only the cycle each value is attributed to moved -- which silently breaks every check that reasons about timing and points the report at the DUT. The delayed sampler, the fix people reach for first, was proven SHIFTED rather than broken in both directions -- never disagreeing with a one-cycle-delayed copy of the skewed sampler and always disagreeing with the undelayed one -- because a bench that cannot separate `late` from `wrong` will accept a shifted monitor and chase the shift through the scoreboard. The pre-edge copy therefore belongs somewhere every connected component inherits it rather than in each component -- which in Verilog-2001 means one file owns the pins and every monitor is handed them as inputs by convention, because the language offers no `interface` to hold the discipline and no `modport` to enforce it; a monitor that assigns to a pin here compiles cleanly, and the symptom is a DUT that behaves differently when the monitor is connected");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b1;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_if_sampler_tb.vhd — the same bench in VHDL, where the accident is unreachable
-- spi_if_sampler_tb.vhd
--
-- One stimulus driven two ways, three samplers of the same pins, four measurements, and
-- one finding that only the VHDL version can make.
--
-- THE EXPERIMENT is the same as the SystemVerilog one. The pins carry a repeating,
-- non-zero, never-immediately-repeating sequence, one new value per clock cycle, and the
-- only thing that changes between the two halves is WHEN the update lands:
--
--   DISCIPLINED   the driver assigns on the FALLING edge. The update lands a delta
--                 later, far from the sampling edge.
--   CARELESS      the driver assigns on the falling edge WITH A DELAY that lands the
--                 update exactly ON the next rising edge.
--
-- THE SECOND FORM IS THE POINT, AND IT IS WHY THE VHDL VERSION IS NOT A TRANSLATION.
--
-- The SystemVerilog defect -- a blocking write to a pin in the same time step a sampler
-- reads it -- has no VHDL spelling, because a VHDL signal assignment cannot take effect
-- in the delta that executes it. A process reading a signal on an edge sees its pre-edge
-- value, always. So the accident is unreachable: you cannot produce this race in VHDL by
-- forgetting to think about it.
--
-- What you CAN do is schedule the update to arrive at the edge, with `after`. Then the
-- clock and the pin change in the same delta, a process sensitive to the clock reads the
-- new value, and the result is the identical defect: a value attributed to the edge on
-- which it arrived, one cycle early. That is not a contrived stimulus -- it is what a
-- pin model written with `after` to mimic an output delay does when the delay happens to
-- equal the remaining half period, and the failure appears when somebody changes the
-- clock frequency.
--
-- THE FOUR MEASUREMENTS:
--
--   1. Under the discipline the skewed sampler recovers the sequence EXACTLY.
--   2. Under the discipline the racy sampler AGREES with it on every cycle.
--   3. With the update landing on the edge they DISAGREE -- and both still recover the
--      same values in the same ORDER, so a payload check passes on both and only the
--      cycle attribution moved.
--   4. The delayed sampler is CORRECT and LATE, proven in both directions.
--
-- Measurement 5 in the SystemVerilog log has no equivalent here because it needs none:
-- `bus_in` is a port of mode `in`, so the analyser already refused every assignment to
-- it before this simulation existed.

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

entity spi_if_sampler_tb is
end entity spi_if_sampler_tb;

architecture tb of spi_if_sampler_tb is

    constant HALF_T : time    := 5 ns;   -- clock half period
    constant PRE_T  : time    := 4 ns;   -- where in that half the pre-edge copy lands
    constant NVAL   : natural := 24;     -- values driven per stimulus

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '1';

    -- The bundle, driven here. Note that the TESTBENCH owns both fields: the pins
    -- because it drives them, and the pre-edge copy because somebody has to maintain it
    -- and VHDL offers no construct that maintains it for you.
    signal bus_s : spi_pins_t := (pins => (others => '0'), pins_pre => (others => '0'));

    signal s_racy, s_skewed, s_delayed : pin_vec_t;

    -- Stimulus control. `on_edge` selects the delayed-assignment variant.
    signal go      : boolean := false;
    signal on_edge : boolean := false;
    signal done    : boolean := false;
    signal collect : boolean := false;

    type int_arr_t is array (0 to 63) of integer;

    -- ------------------------------------------------------------------
    -- VHDL-2008 REQUIRES A SHARED VARIABLE TO HAVE A PROTECTED TYPE, and the reason is
    -- exactly the subject of this chapter one level up: two processes touching the same
    -- unprotected variable have no defined order, which is the same class of defect as
    -- two processes touching the same pin in the same delta. The language refuses the
    -- unprotected form rather than leaving it to review.
    --
    -- So the bookkeeping lives in a protected type, which is a real object: state that
    -- only its own methods can reach, and methods that the simulator serialises. That is
    -- more than the SystemVerilog version has here -- Icarus cannot compile a class in
    -- this position -- and it is the second place in this module where VHDL's answer is
    -- stronger rather than merely different.
    -- ------------------------------------------------------------------
    type recorder_t is protected
        procedure clear;
        procedure push_sent (v : integer);
        procedure push_s    (v : integer);
        procedure push_r    (v : integer);
        procedure tick (rs_ne : boolean; ds_ne : boolean; shift_ne : boolean);
        impure function n_sent       return integer;
        impure function n_gs         return integer;
        impure function n_gr         return integer;
        impure function cyc          return integer;
        impure function rs_diff      return integer;
        impure function ds_diff      return integer;
        impure function ds_shift_bad return integer;
        -- Compares a recorded change sequence against the intended one. A length
        -- mismatch counts once per missing or extra position.
        impure function seq_bad (which : integer) return integer;
    end protected recorder_t;

    type recorder_t is protected body
        variable sent_v  : int_arr_t := (others => -1);
        variable got_s_v : int_arr_t := (others => -1);
        variable got_r_v : int_arr_t := (others => -1);
        variable ns, gs, gr : integer := 0;
        variable c, rs, ds, dsb : integer := 0;

        procedure clear is
        begin
            ns := 0; gs := 0; gr := 0;
            c  := 0; rs := 0; ds := 0; dsb := 0;
        end procedure clear;

        procedure push_sent (v : integer) is
        begin
            if ns < 64 then sent_v(ns) := v; end if;
            ns := ns + 1;
        end procedure push_sent;

        procedure push_s (v : integer) is
        begin
            if gs < 64 then got_s_v(gs) := v; end if;
            gs := gs + 1;
        end procedure push_s;

        procedure push_r (v : integer) is
        begin
            if gr < 64 then got_r_v(gr) := v; end if;
            gr := gr + 1;
        end procedure push_r;

        procedure tick (rs_ne : boolean; ds_ne : boolean; shift_ne : boolean) is
        begin
            c := c + 1;
            if rs_ne    then rs  := rs  + 1; end if;
            if ds_ne    then ds  := ds  + 1; end if;
            if shift_ne then dsb := dsb + 1; end if;
        end procedure tick;

        impure function n_sent       return integer is begin return ns;  end function;
        impure function n_gs         return integer is begin return gs;  end function;
        impure function n_gr         return integer is begin return gr;  end function;
        impure function cyc          return integer is begin return c;   end function;
        impure function rs_diff      return integer is begin return rs;  end function;
        impure function ds_diff      return integer is begin return ds;  end function;
        impure function ds_shift_bad return integer is begin return dsb; end function;

        impure function seq_bad (which : integer) return integer is
            variable bad : integer := 0;
            variable ng  : integer;
        begin
            if which = 0 then ng := gs; else ng := gr; end if;
            if ng > ns then bad := ng - ns;
            elsif ng < ns then bad := ns - ng;
            end if;
            for i in 0 to ns - 1 loop
                if i < ng then
                    if which = 0 then
                        if got_s_v(i) /= sent_v(i) then bad := bad + 1; end if;
                    else
                        if got_r_v(i) /= sent_v(i) then bad := bad + 1; end if;
                    end if;
                end if;
            end loop;
            return bad;
        end function seq_bad;
    end protected body recorder_t;

    shared variable rec : recorder_t;

    signal errors : integer := 0;

    -- Snapshots of each phase, so the table can be printed once at the end.
    signal d_cyc, d_rs, d_ss, d_rr, d_dd, d_dsb : integer := 0;
    signal c_cyc, c_rs, c_ss, c_rr              : integer := 0;
    signal d_ngs, c_ngs                         : integer := 0;

    function pat (i : integer) return integer is
    begin
        return (i mod 15) + 1;           -- 1..15, never 0, never twice in a row
    end function pat;

    function iv (n : integer) return pin_vec_t is
    begin
        return std_logic_vector(to_unsigned(n, PIN_W));
    end function iv;

    function vi (v : pin_vec_t) return integer is
    begin
        return to_integer(unsigned(v));
    end function vi;

begin

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

    -- The pre-edge copy: PRE_T after the falling edge, and therefore shortly before the
    -- next rising one. The delay is what keeps it clear of a driver that assigns on the
    -- falling edge; taking the copy AT the falling edge would read whichever value that
    -- driver's own delta happened to leave behind.
    pre_copy : process is
    begin
        wait until falling_edge(clk);
        wait for PRE_T;
        bus_s.pins_pre <= bus_s.pins;
    end process pre_copy;

    dut : entity work.spi_if_sampler
        port map (
            clk => clk, rst_n => rst_n, bus_in => bus_s,
            s_racy => s_racy, s_skewed => s_skewed, s_delayed => s_delayed
        );

    -- ------------------------------------------------------------------
    -- The recorders. A CHANGE recorder logs the sequence of distinct consecutive values
    -- a sampler produced and is blind to timing; a CYCLE comparator sees nothing but
    -- timing. A race shows up in the second and not the first, which is exactly why it
    -- survives review.
    --
    -- Every read below happens in the first delta of the rising edge, so it sees the
    -- values the sampler registered on the PREVIOUS edge -- consistently for all three,
    -- which is what makes comparing them mean anything.
    -- ------------------------------------------------------------------
    record_proc : process (clk) is
        variable prev_s, prev_r, skew_d : integer := 0;
    begin
        if rising_edge(clk) then
            if rst_n = '0' then
                prev_s := 0; prev_r := 0; skew_d := 0;
            elsif collect then
                if vi(s_skewed) /= prev_s then
                    rec.push_s(vi(s_skewed));
                    prev_s := vi(s_skewed);
                end if;
                if vi(s_racy) /= prev_r then
                    rec.push_r(vi(s_racy));
                    prev_r := vi(s_racy);
                end if;

                rec.tick(s_racy /= s_skewed, s_delayed /= s_skewed, vi(s_delayed) /= skew_d);

                skew_d := vi(s_skewed);
            else
                skew_d := vi(s_skewed);
            end if;
        end if;
    end process record_proc;

    -- ------------------------------------------------------------------
    -- The stimulus.
    -- ------------------------------------------------------------------
    -- THIS PROCESS IS THE ONLY DRIVER OF `bus_s.pins`, and that is not tidiness.
    --
    -- The first version of this bench also cleared the pins from the reset sequence in
    -- `main`. Two processes driving the same field of a record are two drivers on a
    -- resolved signal, `'0'` from one and `'1'` from the other resolve to `'X'`, the
    -- pre-edge copy carried `'X'` forever, and the skewed sampler NEVER CHANGED -- at
    -- which point measurement 2 passed for the worst possible reason: both samplers read
    -- a constant, so of course they agreed. The sequence check is what caught it, and the
    -- lesson is the one this whole chapter is about at a different level: agreement
    -- between two observers means nothing until you have shown that either of them
    -- observed anything.
    --
    -- Returning the pins to idle at the END of a phase, from this same process, gives
    -- them one driver for the whole simulation.
    drive_proc : process is
    begin
        bus_s.pins <= (others => '0');
        loop
            wait until go;
            for i in 0 to NVAL - 1 loop
                wait until falling_edge(clk);
                if on_edge then
                    -- The update is scheduled to land exactly on the next rising edge.
                    -- This is the VHDL spelling of the defect, and the accident it
                    -- models is a pin delay that happens to equal the remaining half
                    -- period.
                    bus_s.pins <= iv(pat(i)) after HALF_T;
                else
                    bus_s.pins <= iv(pat(i));
                end if;
                rec.push_sent(pat(i));
            end loop;
            wait until not go;
            bus_s.pins <= (others => '0');
        end loop;
    end process drive_proc;

    main : process is

        procedure restart is
        begin
            collect <= false;
            go      <= false;
            wait until falling_edge(clk);
            rst_n      <= '1';
            for i in 0 to 1 loop wait until falling_edge(clk); end loop;
            rst_n      <= '0';
            for i in 0 to 3 loop wait until falling_edge(clk); end loop;
            rst_n      <= '1';
            for i in 0 to 3 loop wait until falling_edge(clk); end loop;
            rec.clear;
        end procedure restart;

    begin
        -- ==============================================================
        -- A. THE DISCIPLINE: an ordinary assignment on the falling edge.
        -- ==============================================================
        on_edge <= false;
        restart;
        collect <= true;
        go      <= true;
        for i in 0 to NVAL + 4 loop wait until rising_edge(clk); end loop;
        collect <= false;
        go      <= false;
        d_cyc <= rec.cyc; d_rs <= rec.rs_diff; d_dd <= rec.ds_diff; d_dsb <= rec.ds_shift_bad;
        d_ss  <= rec.seq_bad(0); d_rr <= rec.seq_bad(1); d_ngs <= rec.n_gs;
        wait for 1 ns;

        -- ==============================================================
        -- B. CARELESSNESS: the update lands ON the sampling edge.
        -- ==============================================================
        on_edge <= true;
        restart;
        collect <= true;
        go      <= true;
        for i in 0 to NVAL + 4 loop wait until rising_edge(clk); end loop;
        collect <= false;
        go      <= false;
        c_cyc <= rec.cyc; c_rs <= rec.rs_diff;
        c_ss  <= rec.seq_bad(0); c_rr <= rec.seq_bad(1); c_ngs <= rec.n_gs;
        wait for 1 ns;

        -- ==============================================================
        -- The table.
        -- ==============================================================
        report "  stimulus            cycles   racy-vs-skewed   skewed seq errs   racy seq errs";
        report "  assigned on fall  " & integer'image(d_cyc) & "   " & integer'image(d_rs) &
               "   " & integer'image(d_ss) & "   " & integer'image(d_rr) & "   the discipline";
        report "  lands on the edge " & integer'image(c_cyc) & "   " & integer'image(c_rs) &
               "   " & integer'image(c_ss) & "   " & integer'image(c_rr) & "   the defect";
        report "  values driven per stimulus ........ " & integer'image(NVAL);
        report "  distinct values the skewed sampler actually saw: " & integer'image(d_ngs) &
               " on the falling-edge stimulus, " & integer'image(c_ngs) &
               " on the edge-landing one  (a zero here would make every agreement below vacuous)";
        report "  delayed vs skewed, same cycle ..... " & integer'image(d_dd) & " of " &
               integer'image(d_cyc) & "  (must differ: it is a cycle behind)";
        report "  delayed vs skewed, shifted a cycle. " & integer'image(d_dsb) & " of " &
               integer'image(d_cyc) & "  (must agree: being late is not being wrong)";

        -- ---------- 1. sequence recovery ----------
        if d_ss /= 0 then
            report "  FAIL: under the discipline the skewed sampler mismatched the intended sequence in " &
                   integer'image(d_ss) & " position(s); a pre-edge copy must recover it exactly";
            errors <= errors + 1;
            wait for 1 ns;
        end if;
        report "    1. the skewed sampler recovered all " & integer'image(NVAL) &
               " values in order. A pre-edge copy is not an approximation -- it is the right answer, and it is what `input #1step` declares in SystemVerilog and what a maintained copy means here";

        -- ---------- 2. no race when nothing races ----------
        --
        -- The vacuity guard comes FIRST, because agreement between two observers that
        -- both read a constant is not evidence of anything. An earlier version of this
        -- bench had exactly that failure and it passed measurement 2.
        if d_ngs /= NVAL then
            report "  FAIL: the skewed sampler recorded " & integer'image(d_ngs) &
                   " distinct values where " & integer'image(NVAL) &
                   " were driven, so any agreement between samplers below is vacuous";
            errors <= errors + 1;
            wait for 1 ns;
        end if;
        if d_rs /= 0 then
            report "  FAIL: under the discipline the racy sampler disagreed with the skewed one on " &
                   integer'image(d_rs) & " of " & integer'image(d_cyc) &
                   " cycles; an ordinary VHDL assignment cannot land in the sampling delta, so there is nothing to race";
            errors <= errors + 1;
            wait for 1 ns;
        end if;
        report "    2. with an ordinary assignment the racy sampler agreed with the skewed one on all " &
               integer'image(d_cyc) &
               " cycles -- and in VHDL that is not luck. A signal assignment never takes effect in the delta that executes it, so the SystemVerilog accident has no VHDL spelling: you cannot reach this race by forgetting";

        -- ---------- 3. the race, and its shape ----------
        if c_rs = 0 then
            report "  FAIL: landing the update on the sampling edge produced no disagreement between the racy and skewed samplers, so the stimulus did not create the race and the experiment measured nothing";
            errors <= errors + 1;
            wait for 1 ns;
        end if;
        report "    3. with the update scheduled to land ON the edge they disagreed on " &
               integer'image(c_rs) & " of " & integer'image(c_cyc) &
               " cycles. That is the deliberate form, and it is what a pin model written with `after` produces when the delay equals the remaining half period";
        if c_ss /= 0 or c_rr /= 0 then
            report "  NOTE: the recorded value sequences also differ from the intended one (skewed " &
                   integer'image(c_ss) & ", racy " & integer'image(c_rr) &
                   "), which is a stronger failure than the timing shift";
        else
            report "       and both samplers still recovered all " & integer'image(NVAL) &
                   " values in the right ORDER. A checker that compares payloads, or value sets, or anything not anchored to a cycle, passes on BOTH -- only the cycle each value is attributed to moved, every check that reasons about timing is now a cycle out, and the report will name the DUT";
        end if;

        -- ---------- 4. late is not wrong ----------
        if d_dsb /= 0 then
            report "  FAIL: the delayed sampler disagreed with a one-cycle-delayed copy of the skewed sampler on " &
                   integer'image(d_dsb) & " of " & integer'image(d_cyc) &
                   " cycles; it is meant to be shifted, not broken";
            errors <= errors + 1;
            wait for 1 ns;
        end if;
        if d_dd = 0 then
            report "  FAIL: the delayed sampler never disagreed with the undelayed one, so it is not actually delayed and measurement 4 proved nothing";
            errors <= errors + 1;
            wait for 1 ns;
        end if;
        report "    4. the delayed sampler disagreed with the skewed one on " & integer'image(d_dd) &
               " of " & integer'image(d_cyc) & " cycles and with a one-cycle-delayed copy of it on " &
               integer'image(d_dsb) &
               ". It is SHIFTED, not broken -- and a bench that cannot tell those apart accepts a shifted monitor and then chases the shift through the scoreboard as if it were a DUT fault";

        -- ---------- 5. the half VHDL does not need to measure ----------
        report "    5. and there is nothing to measure about enforcement: `bus_in` is a port of mode `in`, so the ANALYSER refused every assignment to it before this simulation existed. SystemVerilog needs a `modport` to obtain the same guarantee across an interface; VHDL has it from the port list. What VHDL lacks is the timing half -- no construct bundles a sampling discipline with the signals, so `pins_pre` is a field somebody must maintain and every component trusts that somebody did";

        wait for 1 ns;
        if errors = 0 then
            report "PASS: a testbench that samples a pin in the same delta another process updates it has no useful answer, and the three samplers of the same bundle separate on nothing but WHEN the update lands. Driven by an ordinary falling-edge assignment, the skewed sampler recovered every value in order and the racy sampler agreed with it on every cycle measured -- and in VHDL that agreement is structural rather than lucky, because a signal assignment cannot take effect in the delta that executes it, so the SystemVerilog accident has no VHDL spelling and cannot be reached by forgetting. Scheduled with `after` to land exactly on the sampling edge, the same two samplers disagreed, which is the deliberate form and the one a pin model written with an output delay produces when that delay equals the remaining half period; and the shape of the disagreement is what lets it survive review, because both samplers still recovered every value in the right ORDER, so a payload check passes on both and only the cycle attribution moved. The delayed sampler was proven SHIFTED rather than broken in both directions, never disagreeing with a one-cycle-delayed copy of the skewed sampler and always disagreeing with the undelayed one, because a bench that cannot separate late from wrong accepts a shifted monitor and then chases the shift through the scoreboard. VHDL supplies the bundling half as a record in a package and the enforcement half as the `in` port mode, checked by the analyser on every component; the timing half it does not supply, which is precisely the half a clocking block exists for"
                severity note;
        else
            report "FAIL: " & integer'image(errors) & " error(s)" severity error;
        end if;

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

end architecture tb;

7. The Clocking Block, as Reviewed Code

Everything above builds the mechanism. This is the declarative spelling of it, and it should now read as a statement of the thing that was just measured rather than as a keyword to copy.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The same interface, with the discipline DECLARED instead of built. `default
// input #1step` is the pre-edge copy -- the mechanism section 4 measured -- and
// `output #2ns` is the drive skew that keeps the bench off the sampling edge
// without every task having to remember which edge to wait for.
//
// The whole content of a clocking block is that the discipline stops being
// something each task must get right. It is not a new capability; it is the same
// capability, stated once, in the place every component inherits it from.
interface spi_if_cb #(parameter int W = 4) (input logic clk);

  logic [W-1:0] pins;

  clocking cb_mon @(posedge clk);
    default input #1step;          // sample as the pins were JUST BEFORE the edge
    input pins;
  endclocking

  clocking cb_drv @(posedge clk);
    default input #1step output #2ns;   // drive 2ns AFTER the edge, never on it
    output pins;
  endclocking

  // The modports now grant access to the CLOCKING BLOCK rather than to the raw
  // pins, which is the stronger form: a component using `cb` cannot reach the
  // undisciplined signal at all, so the discipline is not merely available, it
  // is the only thing available.
  modport drv (clocking cb_drv);
  modport mon (clocking cb_mon);

endinterface

// A monitor written against it. Note what is absent: no negedge, no pre-edge
// copy, no comment explaining which edge is safe. `@(vif.cb_mon)` waits for the
// clocking event and `vif.cb_mon.pins` is the pre-edge value, by declaration.
class spi_pin_monitor extends uvm_component;
  `uvm_component_utils(spi_pin_monitor)
  virtual spi_if_cb.mon vif;

  task run_phase(uvm_phase phase);
    forever begin
      @(vif.cb_mon);
      collect(vif.cb_mon.pins);    // the pre-edge sample, guaranteed
    end
  endtask
endclass

// And a driver. `vif.cb_drv.pins <= value` is a synchronous drive with the
// declared output skew: it lands 2ns after the edge, so it can never be the
// thing a monitor races.
class spi_pin_driver extends uvm_driver #(spi_item);
  `uvm_component_utils(spi_pin_driver)
  virtual spi_if_cb.drv vif;

  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      @(vif.cb_drv);
      vif.cb_drv.pins <= req.pin_value;   // drive AFTER the edge, by declaration
      seq_item_port.item_done();
    end
  endtask
endclass

8. Why a Verification Engineer Cares

Because this is the bug class that produces a perfect waveform and a zero, and it is the hardest one to find by looking.

When a checker reports zero exercises while the waveform in the viewer shows exactly the traffic you expected, the sampling instant is the first thing to suspect and almost the last thing people check — because the waveform viewer shows the values, which are correct, and not the evaluation order, which is not.

The practical rule that follows: a bench drives on one edge and samples on the other, always, even where it does not seem to matter. The cost is nothing. The benefit is that the whole class of failure becomes unreachable, and a clocking block is the mechanism that makes it a property of the interface rather than a habit every task author has to maintain.

9. Why an FPGA or ASIC Engineer Cares

Because the race in this chapter is a simulation artefact and the one it models is not.

Real silicon has setup and hold windows, and a pin that changes inside the capture window has no defined value either — the same failure, with metastability instead of evaluation order. A bench that samples on the driving edge is a bench that has quietly assumed a zero-width window, which is exactly the assumption Module 15's constraint chapters spent their length dismantling.

And the delayed sampler is worth a second look from a design perspective: registering a signal one more time to make it stable is the correct fix at a clock domain boundary and the wrong one inside a monitor. Same technique, opposite verdict, and the difference is whether anything downstream cares which cycle the value belonged to.

10. Failure Signature — A Perfect Waveform And Zero Exercises

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom          a checker reports 0 violations and 0 exercises. The
                    waveform shows the traffic clearly and correctly. Every
                    value in the viewer is right.

   What happened    the bench drives the pins on the same edge the monitor
                    samples them. The monitor's precondition -- a transition,
                    an interval, a coincidence -- is evaluated against values
                    that are one cycle out of step, so it is never true.

   What would have  driving on the opposite edge, or a clocking block, or the
   caught it        vacuity guard: a check that some observer observed a
                    CHANGING value before believing any agreement between
                    observers.

   The tell         the values are all correct and the ATTRIBUTION is wrong. A
                    payload check passes, a byte-stream comparison passes, and
                    only checks about timing fail -- or, worse, silently never
                    run. If a bug report reads "the data is fine but the timing
                    checks are strange", look at the sampling edge before
                    looking at the design.

11. Common Misconceptions

"An interface is a bundle of signals that saves typing." It is that, and its more important job is holding the sampling discipline and the direction rules so that every connected component inherits both. A bundle without those is a typing convenience; a bundle with them is the reason a second monitor written by somebody else agrees with the first about cycle boundaries.

"Sampling on the clock edge is the bug." Sampling on an edge somebody else is driving is the bug. The measurement shows it directly: with the stimulus moved to the negedge, the racy sampler agreed with the skewed one on every single cycle.

"input #1step means one time unit before the edge." It means the preponed region — conceptually an infinitesimal before the edge, after which no further updates at that time step are visible. It is not a delay you can spell as #1, and the hand-built approximation in this chapter differs from it in precisely one case, which the interface's header states.

"A delayed sample is a safe fix." It is a correct value and a late one, and the lateness is not free: every downstream comparison has to know about the shift. That is how a scoreboard acquires an off-by-one that looks like a design fault, and it is why the bench proves shifted rather than settling for not broken.

"The values were all recovered, so the sampling was fine." Both samplers recovered every value in the right order under the careless stimulus. A payload check passes on both. What differed was which cycle each value was attributed to, and that is invisible to every check that is not anchored to a cycle.

12. Reason It Through

Under the careless stimulus both samplers recovered all 24 values in the right order. Name a check that passes on both and a check that does not.

Passes: a comparison of the received word against the sent word; a byte-stream or payload comparison; a value-set or histogram check. Fails — or silently never fires: any interval measurement, any "did this pin move at the forbidden moment" rule, any check that correlates a pin against a cycle count. All of Chapter 16.1's eight rules are in the second group.

Why does taking the pre-edge copy at the negedge itself not fix the race?

Because a driver that also writes on the negedge is then racing the copy. The race moves from the sampler to the interface and the interface becomes the component that reproduces the defect it exists to prevent. The copy needs to be ordered after every negedge driver, which is what the delay provides.

Why is the vacuity guard needed before believing that the racy and skewed samplers agree?

Because two observers reading a constant agree trivially. A double driver on the pins resolved them to X, both samplers read a constant, and the agreement check passed while nothing was being observed at all. Agreement is evidence only once you have shown that something was observed.

In VHDL the accidental form of this race is unreachable. Does that make VHDL benches immune to the problem?

No. It makes the careless form unreachable — you cannot produce it by forgetting — while the deliberate form is still easy to write: any assignment scheduled with after that lands on the sampling edge reproduces it exactly. A pin model written with an output delay equal to the remaining half period does this, and the symptom appears when the clock frequency changes rather than when the model is written.

13. Understanding Check

14. Summary

A testbench that samples a pin on the same edge another process drives it has no defined answer, and the simulator supplies a consistent one — which is worse than an inconsistent one, because it looks reliable and does not survive a tool change. Three samplers of the same pins separated on nothing but when the bench wrote them: driven on the negedge, the racy and skewed samplers agreed on every cycle, so sampling on a clock edge is not the defect; driven on the posedge they disagreed on 24 of 28 cycles while both still recovered every value in the right order, so a payload check passes on both and only the cycle attribution moved. The delayed sampler was proved shifted rather than broken in both directions. The pre-edge copy therefore belongs in the interface, where every component inherits it, and the modports are enforced by the compiler so that a monitor which assigns to a pin does not build at all. A clocking block is that whole arrangement, declared once instead of built — and the mechanism is worth building first, because a keyword you have watched behave is a different thing from a keyword you have copied.

15. What Comes Next

The sampling discipline is settled. Chapter 16.4 builds the component that owns every timing number in the protocol, and checks it against Chapter 16.1's rule monitor — because a driver cannot check itself.

Continue learning