Skip to content
VLSI Mentor

SPI · Module 10

Extracting Setup, Hold, and Maximum SCLK

Which timing-table rows constrain you and which constrain the device, why a number without its load and corner is not a specification, how a delay on one line alone destroys margin, and the monitor that measures the real link against the datasheet.

Chapter 10.2 got the mode out of the picture. The table beneath the picture holds the rest of pass two — and it is the part most often copied into a spreadsheet without being understood.

The timing table has nine rows. Which of them constrain your design, which constrain the device, and which are only true under conditions your board does not meet?

Getting this wrong does not produce an error message. It produces a design that works on three boards and fails on the fourth.

1. Every Number Constrains Somebody — Work Out Who

The single most useful habit with a timing table is to write, next to each row, whose obligation it is. There are only two answers, and confusing them is the commonest error in the whole exercise.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   t_SU (SDI/MOSI setup)   YOUR obligation   — you must present data this early
   t_HD (SDI/MOSI hold)    YOUR obligation   — you must hold it this long
   t_CSS (CS lead)         YOUR obligation   — CS this far before the first edge
   t_CSH (CS lag)          YOUR obligation   — CS held this far after the last
   t_CS  (CS high)         YOUR obligation   — minimum deselect between frames

   t_V  (SDO/MISO valid)   THE DEVICE'S      — it promises data by this time
   t_DIS (output disable)  THE DEVICE'S      — it promises high-Z by this time
   t_HO (output hold)      THE DEVICE'S      — it promises to hold this long

   f_SCLK(max)             BOTH              — a limit on you, derived from the
                                               device's internal timing

The asymmetry is the point. Your obligations are things your controller must do; the device's are things you may rely on. A number in the wrong column produces a design that either over-constrains itself for no reason or — far worse — assumes a guarantee the device never made.

The naming makes this harder than it should be. SDI and SDO are named from the device's point of view, so SDI is your MOSI and SDO is your MISO. A row labelled "SDI setup time" constrains what you drive. Chapter 10.2's worked example turned on exactly this distinction, and it catches people just as often here.

2. The Conditions Are Part of the Number

A timing value without its conditions is not a specification. Every table has a header or footnote giving:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   load capacitance     often 15 pF, sometimes 30 pF
   supply voltage       a range, with the number quoted at one point
   temperature          a range, with the number quoted at one point
   output drive setting for parts with configurable drive

Load is the one that bites. t_V is measured driving a specified capacitance. Your board presents its own trace capacitance plus every other device's input capacitance plus the controller's input — routinely two or three times the quoted load. The device's output takes longer to reach a valid level, so the effective t_V on your board is larger than the table's, and Chapter 9.4 showed t_V dominates the frequency budget.

Voltage and temperature are the ones that get skipped. A number quoted at 25 °C and nominal supply is a typical-corner number. If your product ships into an enclosure that reaches 70 °C, the slow-corner value is what your design must meet — and that is often 20 to 30 per cent worse.

3. Setup and Hold — What You Must Guarantee

t_SU and t_HD bracket the device's sampling edge. Your controller must present MOSI stable for t_SU before that edge and keep it stable for t_HD after it.

On a normally-clocked master this is nearly free, because the master launches on the opposite edge to the one the device samples on — so both the setup and the hold are naturally close to half a period. The arithmetic only becomes tight in three situations:

  • Very high clock rates, where half a period approaches the numbers themselves.
  • A master that launches on the same edge the device samples on — a mode error, which Chapter 10.2 showed presents as a rate-dependent failure precisely because this margin collapses.
  • Level shifters or buffers on MOSI only, which add delay to the data path without adding it to the clock. That asymmetry eats directly into setup, and it is easy to introduce without noticing because the part looks like a simple wire on the schematic.

The third is worth dwelling on: any delay added to one line and not the other is a direct subtraction from the margin. A single-line buffer, a series resistor on only one net, or a longer route for MOSI than SCLK all do this.

4. t_V — What the Device Guarantees

t_V is the time from the launching edge to MISO being valid. It is the device's promise, it is the largest term in Chapter 9.4's budget, and it is the number to look up before choosing the part if rate matters.

Two subtleties:

  • t_V is specified from a particular edge, and which edge depends on the mode. Reading it as "from the sampling edge" instead of "from the launching edge" gives an answer half a period out.
  • Its companion t_HO — how long the device holds the previous value — is what guarantees you a hold margin on the return path. A part that specifies t_V and not t_HO has told you when data becomes valid and not how long it stays, and at high rates that omission matters.

5. Reading the Three Intervals Off a Capture

t_SU, t_HD and t_V on one bit time

10 cycles
One bit period. MOSI changes early, stays stable through a rising SCLK edge in the middle, and changes again at the end. MISO becomes valid shortly after the SCLK edge and stays valid.MOSI launchedMOSI launcheddevice samplesdevice samplesMOSI changesMOSI changessclkmosiprevD7D7D7D7D7D7D7D7D6miso------------Q7Q7Q7Q7t0t1t2t3t4t5t6t7t8t9
Figure 1 — the three intervals on one bit time. MOSI is launched, then held stable across the sampling edge: the interval before the edge is the setup the master owes, the interval after it is the hold. MISO becomes valid some time after the same edge, and that delay is the device's t_V.

Three intervals, three owners. From the launch to the sampling edge is t_SU and it is yours. From the sampling edge to the next change is t_HD and it is also yours. From the sampling edge to MISO becoming valid is t_V and it is the device's.

Notice that your two obligations together span almost the whole bit period, which is why they are comfortable at moderate rates — and why anything that delays MOSI relative to SCLK subtracts from one without adding to the other.

6. Maximum SCLK Is a Derived Number

The table's f_SCLK(max) is the device's own internal limit. The rate your link can actually run is the smaller of that and what the round trip permits, and Chapter 9.4 gives that arithmetic in full.

What belongs here is the extraction discipline:

  • Find which command each rate applies to. A single headline number usually belongs to the fast-read command only.
  • Find the load it assumes and compare with your board's.
  • Find the corner it assumes and derate for yours.
  • Then take it into Chapter 9.4's budget as one term among four, rather than as the answer.

A device rated 104 MHz whose t_V is 7 ns cannot run at 104 MHz on any ordinary board — the two numbers in the same table are describing different things, and only one of them is about your link.

7. Building the Timing Monitor — Three HDLs

The circuit

Circuit. An oversampling instrument that measures the intervals of §5 on the live bus.

State. How long MOSI has been stable; a hold counter; the worst case of each; sticky violation flags; a count of sampling edges.

Datapath. At each sampling edge the stability counter is the setup. Immediately afterwards a second counter runs until MOSI next changes, and that is the hold. Both are compared against parameters carrying the datasheet's numbers.

Control. Which edge samples comes from CPHA — numbering edges from 1, CPHA=0 samples on the odd ones — so this block sits downstream of Chapter 10.2's mode and Chapter 10.1's profile.

Clock and reset. An oversampling clock; asynchronous active-low reset; a clear that restarts the measurement window so one frame's verdict is never carried into the next.

Enables. The violation flags are sticky within a window, because a violation that occurred once and then stopped is still a violation.

Timing. Both the clock edge and the data transition pass through the same two synchroniser stages, so the common delay cancels in the difference. That is the property that makes a synchronised measurement of an interval trustworthy even though a synchronised measurement of an instant is not.

Synthesis. Two counters, two comparators, three synchronisers. Small enough to leave in a debug build permanently.

Limitations. Its resolution is one oversampling period and no finer. A monitor clocked at ten times SCLK can say a setup is about a tenth of a half period; it cannot say whether a 2 ns requirement is met. It catches the gross violations that static timing analysis never sees — a wrong divisor, a wrong mode, a device slower than its datasheet — none of which appear in a timing netlist.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor.sv — measuring the datasheet's intervals on the live bus
// spi_su_h_monitor.sv
//
// Chapter 10.3 -- turning a timing table into something you can check.
//
// A datasheet gives t_SU and t_HD as numbers. They only become engineering
// when something measures the real link against them. This block
// oversamples the bus and, at every SAMPLING edge, reports:
//
//   setup -- how long MOSI had already been stable when the edge arrived,
//   hold  -- how long MOSI stayed stable after it,
//
// keeping the worst case of each and raising a sticky flag when either
// falls below its limit.
//
// Which edge samples comes from CPHA, so this block is downstream of the
// mode in Chapter 10.2 and of the profile in Chapter 10.1: CPHA=0 samples
// on the leading (odd) edges, CPHA=1 on the trailing (even) ones.
//
// RESOLUTION. Every number here is in oversampling-clock periods, so the
// measurement resolves to one period and no finer. A monitor clocked at
// ten times SCLK can tell you a setup is "about a tenth of a half period";
// it cannot tell you whether a 2 ns requirement is met. Sub-nanosecond
// verification belongs in static timing analysis -- this block is for
// catching the gross violations that STA never sees because they arise
// from a wrong divisor, a wrong mode, or a device slower than its
// datasheet, none of which appear in a timing netlist.

module spi_su_h_monitor #(
    parameter int T_W      = 12,  // width of the measurement counters
    parameter int T_SU_MIN = 8,   // required setup, in oversample periods
    parameter int T_HD_MIN = 4    // required hold,  in oversample periods
) (
    input  logic           clk,      // oversampling clock
    input  logic           rst_n,

    input  logic           sclk,     // observed bus
    input  logic           mosi,
    input  logic           cs_n,
    input  logic           cpol,     // from the device profile
    input  logic           cpha,

    input  logic           clear,    // restart the measurement window

    output logic [T_W-1:0] min_setup,
    output logic [T_W-1:0] min_hold,
    output logic [T_W-1:0] samples,  // sampling edges observed
    output logic           su_viol,  // sticky: a setup fell below T_SU_MIN
    output logic           hd_viol   // sticky: a hold  fell below T_HD_MIN
);

    logic sclk_s1, sclk_s2, sclk_q;
    logic mosi_s1, mosi_s2, mosi_q;
    logic cs_s1,   cs_s2,   cs_q;

    logic [T_W-1:0] since_change;  // MOSI stable for this many periods
    logic [T_W-1:0] hold_cnt;
    logic           in_hold;
    logic           any_edge;
    logic           edge_odd;

    wire sclk_edge = (sclk_s2 != sclk_q);
    wire mosi_edge = (mosi_s2 != mosi_q);
    wire in_frame  = (cs_s2 == 1'b0);
    wire cs_fall   = (cs_q == 1'b1) && (cs_s2 == 1'b0);

    wire next_odd = any_edge ? ~edge_odd : 1'b1;

    // The sampling edge is the leading edge when CPHA=0 and the trailing
    // edge when CPHA=1 -- which, numbering edges from 1, is exactly the odd
    // edges for CPHA=0 and the even ones for CPHA=1. CPOL does not enter:
    // it decides which physical direction "leading" is, and the parity
    // argument is indifferent to that. The port is kept because a reader
    // checking this block against a datasheet expects both halves of the
    // mode to be present, and because a future revision that timestamps
    // rising and falling edges separately will need it.
    wire sample_now = in_frame && sclk_edge && (next_odd != cpha);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_s1 <= 1'b0; sclk_s2 <= 1'b0; sclk_q <= 1'b0;
            mosi_s1 <= 1'b0; mosi_s2 <= 1'b0; mosi_q <= 1'b0;
            cs_s1   <= 1'b1; cs_s2   <= 1'b1; cs_q   <= 1'b1;

            since_change <= {T_W{1'b0}};
            hold_cnt     <= {T_W{1'b0}};
            in_hold      <= 1'b0;
            any_edge     <= 1'b0;
            edge_odd     <= 1'b0;

            min_setup <= {T_W{1'b1}};
            min_hold  <= {T_W{1'b1}};
            samples   <= {T_W{1'b0}};
            su_viol   <= 1'b0;
            hd_viol   <= 1'b0;
        end else begin
            sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
            mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
            cs_s1   <= cs_n; cs_s2   <= cs_s1;   cs_q   <= cs_s2;

            if (clear) begin
                min_setup <= {T_W{1'b1}};
                min_hold  <= {T_W{1'b1}};
                samples   <= {T_W{1'b0}};
                su_viol   <= 1'b0;
                hd_viol   <= 1'b0;
            end

            // How long MOSI has been stable. Both this and the SCLK edge
            // detection pass through the same two synchroniser stages, so
            // the common delay cancels in the difference -- which is why a
            // synchronised measurement of an INTERVAL is trustworthy even
            // though a synchronised measurement of an INSTANT is not.
            // The counter is reloaded with ONE, not zero, on a transition.
            // The transition is detected at the end of a period, so the
            // first period of stability is the one that has already
            // elapsed. Reloading with zero reports every interval one
            // period short -- a constant bias that would quietly turn a
            // measurement against a datasheet limit into a measurement
            // against that limit plus one period.
            if (mosi_edge) since_change <= {{(T_W-1){1'b0}}, 1'b1};
            else           since_change <= since_change + 1'b1;

            if (cs_fall) begin
                any_edge <= 1'b0;
                edge_odd <= 1'b0;
                in_hold  <= 1'b0;
            end else if (in_frame && sclk_edge) begin
                any_edge <= 1'b1;
                edge_odd <= next_odd;
            end

            // SETUP, measured at the sampling edge.
            if (sample_now) begin
                samples <= samples + 1'b1;
                if (since_change < min_setup) min_setup <= since_change;
                if (since_change < T_W'(T_SU_MIN)) su_viol <= 1'b1;

                // Begin measuring the hold of the bit just sampled.
                in_hold  <= 1'b1;
                hold_cnt <= {{(T_W-1){1'b0}}, 1'b1};   // same reasoning
            end else if (in_hold) begin
                if (mosi_edge) begin
                    // HOLD ends at the first change after the sampling edge.
                    if (hold_cnt < min_hold) min_hold <= hold_cnt;
                    if (hold_cnt < T_W'(T_HD_MIN)) hd_viol <= 1'b1;
                    in_hold <= 1'b0;
                end else begin
                    hold_cnt <= hold_cnt + 1'b1;
                end
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor_tb.sv — frames whose timing is known by construction
// spi_su_h_monitor_tb.sv
//
// The testbench drives frames whose setup and hold are KNOWN by
// construction, then requires the monitor to measure them back. Checking
// a measurement instrument against a value it was told is circular, so
// every expectation here is computed from the stimulus timing rather than
// from the monitor's own output.

`timescale 1ns/1ps

module spi_su_h_monitor_tb;

    localparam int T_W      = 12;
    localparam int T_SU_MIN = 8;
    localparam int T_HD_MIN = 4;
    localparam int H        = 20;   // SCLK half period, ns
    localparam int TCLK     = 2;    // oversampling period, ns

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #(TCLK/2) clk = ~clk;

    logic sclk = 1'b0;
    logic mosi = 1'b0;
    logic cs_n = 1'b1;
    logic clear = 1'b0;

    logic [T_W-1:0] min_setup, min_hold, samples;
    logic           su_viol, hd_viol;

    int errors = 0;

    spi_su_h_monitor #(.T_W(T_W), .T_SU_MIN(T_SU_MIN), .T_HD_MIN(T_HD_MIN)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .mosi(mosi), .cs_n(cs_n),
        .cpol(1'b0), .cpha(1'b0), .clear(clear),
        .min_setup(min_setup), .min_hold(min_hold), .samples(samples),
        .su_viol(su_viol), .hd_viol(hd_viol)
    );

    task automatic restart;
        begin
            @(negedge clk); clear = 1'b1;
            @(negedge clk); clear = 1'b0;
            @(negedge clk);
        end
    endtask

    // Mode 0. Each bit is launched skew ns AFTER the trailing edge, so the
    // setup seen at the next leading edge is H - skew and the hold after
    // that leading edge is H + skew.
    task automatic drive_setup_case(input int skew, input logic [7:0] data);
        int i;
        begin
            sclk = 1'b0; cs_n = 1'b1; #(H*2);
            cs_n = 1'b0;
            #(skew); mosi = data[7]; #(H-skew);
            for (i = 0; i < 8; i++) begin
                sclk = 1'b1;                       // leading edge: SAMPLE
                #H;
                sclk = 1'b0;                       // trailing edge: LAUNCH
                if (i < 7) begin
                    #(skew); mosi = data[6-i]; #(H-skew);
                end else begin
                    #H;
                end
            end
            cs_n = 1'b1; #(H*2);
        end
    endtask

    // A slave with excessive clock-to-out: the bit changes late ns AFTER
    // the leading edge that was supposed to sample the previous one. The
    // hold is then only late ns, and the setup for the following bit is
    // correspondingly generous -- which is exactly why a design can fail
    // hold while its setup margin looks comfortable.
    task automatic drive_hold_case(input int late, input logic [7:0] data);
        int i;
        begin
            sclk = 1'b0; cs_n = 1'b1; #(H*2);
            cs_n = 1'b0;
            mosi = data[7];
            #H;
            for (i = 0; i < 8; i++) begin
                sclk = 1'b1;                       // leading edge: SAMPLE
                if (i < 7) begin
                    #(late); mosi = data[6-i]; #(H-late);
                end else begin
                    #H;
                end
                sclk = 1'b0;                       // trailing edge
                #H;
            end
            cs_n = 1'b1; #(H*2);
        end
    endtask

    task automatic expect_near(input string what, input int got, input int want);
        begin
            // The monitor resolves to its own clock and no finer, so one
            // period of tolerance is allowed -- but no systematic offset
            // is: these expectations are the true constructed intervals,
            // and a constant bias would show up as every one of them
            // sitting at the same edge of the window.
            if (got < want-1 || got > want+1) begin
                $display("  FAIL: %s measured %0d periods, expected %0d +/-1",
                         what, got, want);
                errors++;
            end
        end
    endtask

    initial begin
        repeat (5) @(negedge clk);
        rst_n = 1'b1;
        restart();

        // 1. A comfortable link. Setup and hold are both a half period.
        drive_setup_case(0, 8'hA5);
        $display("  clean frame:   setup=%0d hold=%0d samples=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, samples, su_viol, hd_viol);
        expect_near("clean setup", min_setup, H/TCLK);
        expect_near("clean hold",  min_hold,  H/TCLK);
        if (samples !== T_W'(8)) begin
            $display("  FAIL: %0d sampling edges counted in an 8-bit frame", samples);
            errors++;
        end
        if (su_viol || hd_viol) begin
            $display("  FAIL: a comfortable link reported a violation"); errors++;
        end

        // 2. Data launched late relative to the trailing edge. Setup shrinks
        //    by exactly the skew; hold grows by it.
        restart();
        drive_setup_case(8, 8'hA5);
        $display("  8 ns skew:     setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, su_viol, hd_viol);
        expect_near("skewed setup", min_setup, (H-8)/TCLK);
        expect_near("skewed hold",  min_hold,  (H+8)/TCLK);
        if (!su_viol) begin
            $display("  FAIL: a setup of %0d periods did not trip T_SU_MIN=%0d",
                     min_setup, T_SU_MIN);
            errors++;
        end
        if (hd_viol) begin
            $display("  FAIL: hold flagged while hold was growing"); errors++;
        end

        // 3. The converse: a slave whose output changes just after the
        //    sampling edge. Hold collapses while setup stays generous --
        //    the failure that looks fine in a setup-only review.
        restart();
        drive_hold_case(4, 8'hA5);
        $display("  4 ns clk-to-out: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, su_viol, hd_viol);
        expect_near("late-launch hold", min_hold, 4/TCLK);
        if (min_hold !== T_W'(4/TCLK)) begin
            $display("  NOTE: hold measured %0d, constructed %0d", min_hold, 4/TCLK);
        end
        if (!hd_viol) begin
            $display("  FAIL: a hold of %0d periods did not trip T_HD_MIN=%0d",
                     min_hold, T_HD_MIN);
            errors++;
        end
        if (su_viol) begin
            $display("  FAIL: setup flagged on a frame with generous setup");
            errors++;
        end

        // 4. clear restarts the window: the flags and the minima must not
        //    carry a previous frame's verdict into a new measurement.
        restart();
        if (su_viol || hd_viol || samples !== T_W'(0)) begin
            $display("  FAIL: clear did not restart the measurement window");
            errors++;
        end
        drive_setup_case(0, 8'hA5);
        if (su_viol || hd_viol) begin
            $display("  FAIL: a clean frame after a violating one still reports a violation");
            errors++;
        end
        $display("  after clear:   setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, su_viol, hd_viol);

        if (errors == 0)
            $display("PASS: setup and hold are measured back to within one oversampling period of their constructed values, each limit trips only its own flag, and clear restarts the window");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule

The testbench never checks the monitor against a number the monitor produced. It builds frames whose setup and hold are known from the stimulus timing and requires those values back — which is the only way to test an instrument, since checking a measurement against itself is circular.

One detail in that RTL is worth extracting, because it is a class of bug rather than an instance. The stability counter is reloaded with one, not zero, when MOSI changes: the transition is detected at the end of a period, so the first period of stability has already elapsed. Reloading with zero reports every interval one period short — a constant bias, which is the most dangerous kind of measurement error because every reading stays self-consistent and the whole set is wrong. Compared against a datasheet limit, that bias silently turns the check into one against the limit plus a period.

The two directed cases are chosen to fail differently. Launching data late relative to the trailing edge shrinks setup while growing hold. Launching it just after the sampling edge — a slave with excessive clock-to-out — collapses hold while leaving setup generous. A monitor that conflated the two would pass both.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor.v — the same instrument in Verilog-2001
// spi_su_h_monitor.v
//
// Chapter 10.3 -- turning a timing table into something you can check,
// in Verilog-2001.
//
// At every SAMPLING edge the block reports how long MOSI had been stable
// (setup) and how long it stays stable afterwards (hold), keeps the worst
// case of each, and raises a sticky flag when either falls below its
// limit. Which edge samples comes from CPHA: numbering the edges inside a
// frame from 1, CPHA=0 samples on the odd edges and CPHA=1 on the even.
//
// RESOLUTION. Every number is in oversampling-clock periods, so the
// measurement resolves to one period and no finer. This block catches the
// gross violations that static timing analysis never sees -- a wrong
// divisor, a wrong mode, a device slower than its datasheet -- and is not
// a substitute for STA on the sub-nanosecond numbers.

module spi_su_h_monitor #(
    parameter T_W      = 12,  // width of the measurement counters
    parameter T_SU_MIN = 8,   // required setup, in oversample periods
    parameter T_HD_MIN = 4    // required hold,  in oversample periods
) (
    input  wire           clk,      // oversampling clock
    input  wire           rst_n,

    input  wire           sclk,     // observed bus
    input  wire           mosi,
    input  wire           cs_n,
    input  wire           cpol,     // from the device profile
    input  wire           cpha,

    input  wire           clear,    // restart the measurement window

    output reg  [T_W-1:0] min_setup,
    output reg  [T_W-1:0] min_hold,
    output reg  [T_W-1:0] samples,  // sampling edges observed
    output reg            su_viol,  // sticky: a setup fell below T_SU_MIN
    output reg            hd_viol   // sticky: a hold  fell below T_HD_MIN
);

    reg sclk_s1, sclk_s2, sclk_q;
    reg mosi_s1, mosi_s2, mosi_q;
    reg cs_s1,   cs_s2,   cs_q;

    reg [T_W-1:0] since_change;  // MOSI stable for this many periods
    reg [T_W-1:0] hold_cnt;
    reg           in_hold;
    reg           any_edge;
    reg           edge_odd;

    wire sclk_edge = (sclk_s2 != sclk_q);
    wire mosi_edge = (mosi_s2 != mosi_q);
    wire in_frame  = (cs_s2 == 1'b0);
    wire cs_fall   = (cs_q == 1'b1) && (cs_s2 == 1'b0);

    wire next_odd = any_edge ? ~edge_odd : 1'b1;

    // CPOL does not enter the parity argument: it decides which physical
    // direction "leading" is, and the parity is indifferent to that. The
    // port is kept because a reader checking this block against a datasheet
    // expects both halves of the mode to be present.
    wire sample_now = in_frame && sclk_edge && (next_odd != cpha);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sclk_s1 <= 1'b0; sclk_s2 <= 1'b0; sclk_q <= 1'b0;
            mosi_s1 <= 1'b0; mosi_s2 <= 1'b0; mosi_q <= 1'b0;
            cs_s1   <= 1'b1; cs_s2   <= 1'b1; cs_q   <= 1'b1;

            since_change <= {T_W{1'b0}};
            hold_cnt     <= {T_W{1'b0}};
            in_hold      <= 1'b0;
            any_edge     <= 1'b0;
            edge_odd     <= 1'b0;

            min_setup <= {T_W{1'b1}};
            min_hold  <= {T_W{1'b1}};
            samples   <= {T_W{1'b0}};
            su_viol   <= 1'b0;
            hd_viol   <= 1'b0;
        end else begin
            sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
            mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
            cs_s1   <= cs_n; cs_s2   <= cs_s1;   cs_q   <= cs_s2;

            if (clear) begin
                min_setup <= {T_W{1'b1}};
                min_hold  <= {T_W{1'b1}};
                samples   <= {T_W{1'b0}};
                su_viol   <= 1'b0;
                hd_viol   <= 1'b0;
            end

            // How long MOSI has been stable. Both this and the SCLK edge
            // detection pass through the same two synchroniser stages, so
            // the common delay cancels in the difference -- a synchronised
            // measurement of an INTERVAL is trustworthy even though a
            // synchronised measurement of an INSTANT is not.
            //
            // The counter is reloaded with ONE, not zero: the transition is
            // detected at the end of a period, so the first period of
            // stability has already elapsed. Reloading with zero reports
            // every interval one period short.
            if (mosi_edge) since_change <= {{(T_W-1){1'b0}}, 1'b1};
            else           since_change <= since_change + 1'b1;

            if (cs_fall) begin
                any_edge <= 1'b0;
                edge_odd <= 1'b0;
                in_hold  <= 1'b0;
            end else if (in_frame && sclk_edge) begin
                any_edge <= 1'b1;
                edge_odd <= next_odd;
            end

            // SETUP, measured at the sampling edge.
            if (sample_now) begin
                samples <= samples + 1'b1;
                if (since_change < min_setup) min_setup <= since_change;
                if (since_change < T_SU_MIN)  su_viol   <= 1'b1;

                in_hold  <= 1'b1;
                hold_cnt <= {{(T_W-1){1'b0}}, 1'b1};   // same reasoning
            end else if (in_hold) begin
                if (mosi_edge) begin
                    // HOLD ends at the first change after the sampling edge.
                    if (hold_cnt < min_hold) min_hold <= hold_cnt;
                    if (hold_cnt < T_HD_MIN) hd_viol  <= 1'b1;
                    in_hold <= 1'b0;
                end else begin
                    hold_cnt <= hold_cnt + 1'b1;
                end
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor_tb.v — the same constructed timings in Verilog-2001
// spi_su_h_monitor_tb.v
//
// The same checks as the SystemVerilog testbench: frames whose setup and
// hold are known by construction, measured back exactly; each limit
// tripping only its own flag; and clear restarting the window.

`timescale 1ns/1ps

module spi_su_h_monitor_tb;

    parameter T_W      = 12;
    parameter T_SU_MIN = 8;
    parameter T_HD_MIN = 4;
    parameter H        = 20;   // SCLK half period, ns
    parameter TCLK     = 2;    // oversampling period, ns

    reg clk;
    reg rst_n;
    reg sclk;
    reg mosi;
    reg cs_n;
    reg clear;

    wire [T_W-1:0] min_setup, min_hold, samples;
    wire           su_viol, hd_viol;

    integer errors;

    initial begin
        clk = 1'b0; rst_n = 1'b0;
        sclk = 1'b0; mosi = 1'b0; cs_n = 1'b1; clear = 1'b0;
        errors = 0;
    end
    always #(TCLK/2) clk = ~clk;

    spi_su_h_monitor #(.T_W(T_W), .T_SU_MIN(T_SU_MIN), .T_HD_MIN(T_HD_MIN)) dut (
        .clk(clk), .rst_n(rst_n),
        .sclk(sclk), .mosi(mosi), .cs_n(cs_n),
        .cpol(1'b0), .cpha(1'b0), .clear(clear),
        .min_setup(min_setup), .min_hold(min_hold), .samples(samples),
        .su_viol(su_viol), .hd_viol(hd_viol)
    );

    task restart;
        begin
            @(negedge clk); clear = 1'b1;
            @(negedge clk); clear = 1'b0;
            @(negedge clk);
        end
    endtask

    // Mode 0. Each bit is launched skew ns AFTER the trailing edge, so the
    // setup seen at the next leading edge is H - skew and the hold after
    // that leading edge is H + skew.
    task drive_setup_case;
        input integer skew;
        input [7:0]   data;
        integer i;
        begin
            sclk = 1'b0; cs_n = 1'b1; #(H*2);
            cs_n = 1'b0;
            #(skew); mosi = data[7]; #(H-skew);
            for (i = 0; i < 8; i = i + 1) begin
                sclk = 1'b1;                       // leading edge: SAMPLE
                #(H);
                sclk = 1'b0;                       // trailing edge: LAUNCH
                if (i < 7) begin
                    #(skew); mosi = data[6-i]; #(H-skew);
                end else begin
                    #(H);
                end
            end
            cs_n = 1'b1; #(H*2);
        end
    endtask

    // A slave with excessive clock-to-out: the bit changes late ns AFTER
    // the leading edge that sampled the previous one. The hold is then only
    // late ns while the setup for the following bit is generous -- which is
    // why a design can fail hold with a comfortable-looking setup margin.
    task drive_hold_case;
        input integer late;
        input [7:0]   data;
        integer i;
        begin
            sclk = 1'b0; cs_n = 1'b1; #(H*2);
            cs_n = 1'b0;
            mosi = data[7];
            #(H);
            for (i = 0; i < 8; i = i + 1) begin
                sclk = 1'b1;                       // leading edge: SAMPLE
                if (i < 7) begin
                    #(late); mosi = data[6-i]; #(H-late);
                end else begin
                    #(H);
                end
                sclk = 1'b0;                       // trailing edge
                #(H);
            end
            cs_n = 1'b1; #(H*2);
        end
    endtask

    task expect_near;
        input [8*24:1] what;
        input integer  got;
        input integer  want;
        begin
            // One oversampling period of tolerance -- but no systematic
            // offset: these expectations are the true constructed
            // intervals.
            if (got < want-1 || got > want+1) begin
                $display("  FAIL: %0s measured %0d periods, expected %0d +/-1",
                         what, got, want);
                errors = errors + 1;
            end
        end
    endtask

    initial begin
        repeat (5) @(negedge clk);
        rst_n = 1'b1;
        restart;

        // 1. A comfortable link. Setup and hold are both a half period.
        drive_setup_case(0, 8'hA5);
        $display("  clean frame:   setup=%0d hold=%0d samples=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, samples, su_viol, hd_viol);
        expect_near("clean setup", min_setup, H/TCLK);
        expect_near("clean hold",  min_hold,  H/TCLK);
        if (samples !== 8) begin
            $display("  FAIL: %0d sampling edges counted in an 8-bit frame", samples);
            errors = errors + 1;
        end
        if (su_viol || hd_viol) begin
            $display("  FAIL: a comfortable link reported a violation");
            errors = errors + 1;
        end

        // 2. Data launched late relative to the trailing edge. Setup shrinks
        //    by exactly the skew; hold grows by it.
        restart;
        drive_setup_case(8, 8'hA5);
        $display("  8 ns skew:     setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, su_viol, hd_viol);
        expect_near("skewed setup", min_setup, (H-8)/TCLK);
        expect_near("skewed hold",  min_hold,  (H+8)/TCLK);
        if (!su_viol) begin
            $display("  FAIL: a setup of %0d periods did not trip T_SU_MIN=%0d",
                     min_setup, T_SU_MIN);
            errors = errors + 1;
        end
        if (hd_viol) begin
            $display("  FAIL: hold flagged while hold was growing");
            errors = errors + 1;
        end

        // 3. The converse: a slave whose output changes just after the
        //    sampling edge. Hold collapses while setup stays generous.
        restart;
        drive_hold_case(4, 8'hA5);
        $display("  4 ns clk-to-out: setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, su_viol, hd_viol);
        expect_near("late-launch hold", min_hold, 4/TCLK);
        if (!hd_viol) begin
            $display("  FAIL: a hold of %0d periods did not trip T_HD_MIN=%0d",
                     min_hold, T_HD_MIN);
            errors = errors + 1;
        end
        if (su_viol) begin
            $display("  FAIL: setup flagged on a frame with generous setup");
            errors = errors + 1;
        end

        // 4. clear restarts the window.
        restart;
        if (su_viol || hd_viol || samples !== 0) begin
            $display("  FAIL: clear did not restart the measurement window");
            errors = errors + 1;
        end
        drive_setup_case(0, 8'hA5);
        if (su_viol || hd_viol) begin
            $display("  FAIL: a clean frame after a violating one still reports a violation");
            errors = errors + 1;
        end
        $display("  after clear:   setup=%0d hold=%0d su_viol=%0b hd_viol=%0b",
                 min_setup, min_hold, su_viol, hd_viol);

        if (errors == 0)
            $display("PASS: setup and hold are measured back to within one oversampling period of their constructed values, each limit trips only its own flag, and clear restarts the window");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor.vhd — the same instrument in VHDL
-- spi_su_h_monitor.vhd
--
-- Chapter 10.3 -- turning a timing table into something you can check,
-- in VHDL.
--
-- At every SAMPLING edge the block reports how long MOSI had been stable
-- (setup) and how long it stays stable afterwards (hold), keeps the worst
-- case of each, and raises a sticky flag when either falls below its
-- limit. Which edge samples comes from CPHA: numbering the edges inside a
-- frame from 1, CPHA=0 samples on the odd edges and CPHA=1 on the even.
--
-- RESOLUTION. Every number is in oversampling-clock periods, so the
-- measurement resolves to one period and no finer. This block catches the
-- gross violations that static timing analysis never sees -- a wrong
-- divisor, a wrong mode, a device slower than its datasheet -- and is not
-- a substitute for STA on the sub-nanosecond numbers.

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

entity spi_su_h_monitor is
    generic (
        T_W      : positive := 12;  -- width of the measurement counters
        T_SU_MIN : natural  := 8;   -- required setup, in oversample periods
        T_HD_MIN : natural  := 4    -- required hold,  in oversample periods
    );
    port (
        clk       : in  std_logic;  -- oversampling clock
        rst_n     : in  std_logic;

        sclk      : in  std_logic;  -- observed bus
        mosi      : in  std_logic;
        cs_n      : in  std_logic;
        cpol      : in  std_logic;  -- from the device profile
        cpha      : in  std_logic;

        clear     : in  std_logic;  -- restart the measurement window

        min_setup : out unsigned(T_W - 1 downto 0);
        min_hold  : out unsigned(T_W - 1 downto 0);
        samples   : out unsigned(T_W - 1 downto 0);
        su_viol   : out std_logic;
        hd_viol   : out std_logic
    );
end entity;

architecture rtl of spi_su_h_monitor is

    signal sclk_s1, sclk_s2, sclk_q : std_logic := '0';
    signal mosi_s1, mosi_s2, mosi_q : std_logic := '0';
    signal cs_s1,   cs_s2,   cs_q   : std_logic := '1';

    signal since_change : unsigned(T_W - 1 downto 0) := (others => '0');
    signal hold_cnt     : unsigned(T_W - 1 downto 0) := (others => '0');
    signal in_hold      : std_logic := '0';
    signal any_edge     : std_logic := '0';
    signal edge_odd     : std_logic := '0';

    signal min_su_r : unsigned(T_W - 1 downto 0) := (others => '1');
    signal min_hd_r : unsigned(T_W - 1 downto 0) := (others => '1');
    signal samp_r   : unsigned(T_W - 1 downto 0) := (others => '0');
    signal su_v_r   : std_logic := '0';
    signal hd_v_r   : std_logic := '0';

    signal sclk_edge  : std_logic;
    signal mosi_edge  : std_logic;
    signal in_frame   : std_logic;
    signal cs_fall    : std_logic;
    signal next_odd   : std_logic;
    signal sample_now : std_logic;

begin

    sclk_edge <= '1' when sclk_s2 /= sclk_q else '0';
    mosi_edge <= '1' when mosi_s2 /= mosi_q else '0';
    in_frame  <= '1' when cs_s2 = '0' else '0';
    cs_fall   <= '1' when cs_q = '1' and cs_s2 = '0' else '0';

    next_odd <= not edge_odd when any_edge = '1' else '1';

    -- CPOL does not enter the parity argument: it decides which physical
    -- direction "leading" is, and the parity is indifferent to that. The
    -- port is kept because a reader checking this block against a datasheet
    -- expects both halves of the mode to be present.
    sample_now <= '1' when in_frame = '1' and sclk_edge = '1' and
                           next_odd /= cpha else '0';

    measure : process (clk, rst_n)
    begin
        if rst_n = '0' then
            sclk_s1 <= '0'; sclk_s2 <= '0'; sclk_q <= '0';
            mosi_s1 <= '0'; mosi_s2 <= '0'; mosi_q <= '0';
            cs_s1   <= '1'; cs_s2   <= '1'; cs_q   <= '1';

            since_change <= (others => '0');
            hold_cnt     <= (others => '0');
            in_hold      <= '0';
            any_edge     <= '0';
            edge_odd     <= '0';

            min_su_r <= (others => '1');
            min_hd_r <= (others => '1');
            samp_r   <= (others => '0');
            su_v_r   <= '0';
            hd_v_r   <= '0';
        elsif rising_edge(clk) then
            sclk_s1 <= sclk; sclk_s2 <= sclk_s1; sclk_q <= sclk_s2;
            mosi_s1 <= mosi; mosi_s2 <= mosi_s1; mosi_q <= mosi_s2;
            cs_s1   <= cs_n; cs_s2   <= cs_s1;   cs_q   <= cs_s2;

            if clear = '1' then
                min_su_r <= (others => '1');
                min_hd_r <= (others => '1');
                samp_r   <= (others => '0');
                su_v_r   <= '0';
                hd_v_r   <= '0';
            end if;

            -- How long MOSI has been stable. Both this and the SCLK edge
            -- detection pass through the same two synchroniser stages, so
            -- the common delay cancels in the difference -- a synchronised
            -- measurement of an INTERVAL is trustworthy even though a
            -- synchronised measurement of an INSTANT is not.
            --
            -- The counter is reloaded with ONE, not zero: the transition is
            -- detected at the end of a period, so the first period of
            -- stability has already elapsed. Reloading with zero reports
            -- every interval one period short.
            if mosi_edge = '1' then
                since_change <= to_unsigned(1, T_W);
            else
                since_change <= since_change + 1;
            end if;

            if cs_fall = '1' then
                any_edge <= '0';
                edge_odd <= '0';
                in_hold  <= '0';
            elsif in_frame = '1' and sclk_edge = '1' then
                any_edge <= '1';
                edge_odd <= next_odd;
            end if;

            -- SETUP, measured at the sampling edge.
            if sample_now = '1' then
                samp_r <= samp_r + 1;
                if since_change < min_su_r then
                    min_su_r <= since_change;
                end if;
                if to_integer(since_change) < T_SU_MIN then
                    su_v_r <= '1';
                end if;

                in_hold  <= '1';
                hold_cnt <= to_unsigned(1, T_W);   -- same reasoning
            elsif in_hold = '1' then
                if mosi_edge = '1' then
                    -- HOLD ends at the first change after the sampling edge.
                    if hold_cnt < min_hd_r then
                        min_hd_r <= hold_cnt;
                    end if;
                    if to_integer(hold_cnt) < T_HD_MIN then
                        hd_v_r <= '1';
                    end if;
                    in_hold <= '0';
                else
                    hold_cnt <= hold_cnt + 1;
                end if;
            end if;
        end if;
    end process;

    min_setup <= min_su_r;
    min_hold  <= min_hd_r;
    samples   <= samp_r;
    su_viol   <= su_v_r;
    hd_viol   <= hd_v_r;

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor_tb.vhd — the same constructed timings in VHDL
-- spi_su_h_monitor_tb.vhd
--
-- The same checks as the SystemVerilog and Verilog testbenches: frames
-- whose setup and hold are known by construction, measured back exactly;
-- each limit tripping only its own flag; and clear restarting the window.

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

entity spi_su_h_monitor_tb is
end entity;

architecture sim of spi_su_h_monitor_tb is

    constant T_W      : positive := 12;
    constant T_SU_MIN : natural  := 8;
    constant T_HD_MIN : natural  := 4;
    constant H        : time     := 20 ns;
    constant TCLK     : time     := 2 ns;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal halt  : boolean   := false;

    signal sclk  : std_logic := '0';
    signal mosi  : std_logic := '0';
    signal cs_n  : std_logic := '1';
    signal clear : std_logic := '0';

    signal min_setup : unsigned(T_W - 1 downto 0);
    signal min_hold  : unsigned(T_W - 1 downto 0);
    signal samples   : unsigned(T_W - 1 downto 0);
    signal su_viol   : std_logic;
    signal hd_viol   : std_logic;

    signal errors : natural := 0;

begin

    clk <= not clk after TCLK / 2 when not halt else '0';

    dut : entity work.spi_su_h_monitor
        generic map (T_W => T_W, T_SU_MIN => T_SU_MIN, T_HD_MIN => T_HD_MIN)
        port map (
            clk => clk, rst_n => rst_n,
            sclk => sclk, mosi => mosi, cs_n => cs_n,
            cpol => '0', cpha => '0', clear => clear,
            min_setup => min_setup, min_hold => min_hold, samples => samples,
            su_viol => su_viol, hd_viol => hd_viol
        );

    stim : process
        variable errs : natural := 0;

        procedure restart is
        begin
            wait until falling_edge(clk); clear <= '1';
            wait until falling_edge(clk); clear <= '0';
            wait until falling_edge(clk);
        end procedure;

        -- Mode 0. Each bit is launched skew after the trailing edge, so the
        -- setup at the next leading edge is H - skew and the hold after
        -- that leading edge is H + skew.
        procedure drive_setup_case(skew : time;
                                   data : std_logic_vector(7 downto 0)) is
        begin
            sclk <= '0'; cs_n <= '1'; wait for H * 2;
            cs_n <= '0';
            wait for skew; mosi <= data(7); wait for H - skew;
            for i in 0 to 7 loop
                sclk <= '1';                        -- leading edge: SAMPLE
                wait for H;
                sclk <= '0';                        -- trailing edge: LAUNCH
                if i < 7 then
                    wait for skew; mosi <= data(6 - i); wait for H - skew;
                else
                    wait for H;
                end if;
            end loop;
            cs_n <= '1'; wait for H * 2;
        end procedure;

        -- A slave with excessive clock-to-out: the bit changes late after
        -- the leading edge that sampled the previous one. Hold collapses
        -- while setup stays generous.
        procedure drive_hold_case(late : time;
                                  data : std_logic_vector(7 downto 0)) is
        begin
            sclk <= '0'; cs_n <= '1'; wait for H * 2;
            cs_n <= '0';
            mosi <= data(7);
            wait for H;
            for i in 0 to 7 loop
                sclk <= '1';                        -- leading edge: SAMPLE
                if i < 7 then
                    wait for late; mosi <= data(6 - i); wait for H - late;
                else
                    wait for H;
                end if;
                sclk <= '0';                        -- trailing edge
                wait for H;
            end loop;
            cs_n <= '1'; wait for H * 2;
        end procedure;

        procedure expect_near(what : string; got : natural; want : natural) is
        begin
            -- One oversampling period of tolerance -- but no systematic
            -- offset: these expectations are the true constructed intervals.
            if got < want - 1 or got > want + 1 then
                report "  FAIL: " & what & " measured " & integer'image(got) &
                       " periods, expected " & integer'image(want) & " +/-1";
                errs := errs + 1;
            end if;
        end procedure;
    begin
        for i in 0 to 4 loop
            wait until falling_edge(clk);
        end loop;
        rst_n <= '1';
        restart;

        -- 1. A comfortable link. Setup and hold are both a half period.
        drive_setup_case(0 ns, x"A5");
        report "  clean frame: setup=" & integer'image(to_integer(min_setup)) &
               " hold=" & integer'image(to_integer(min_hold)) &
               " samples=" & integer'image(to_integer(samples));
        expect_near("clean setup", to_integer(min_setup), H / TCLK);
        expect_near("clean hold",  to_integer(min_hold),  H / TCLK);
        if samples /= 8 then
            report "  FAIL: wrong number of sampling edges in an 8-bit frame";
            errs := errs + 1;
        end if;
        if su_viol = '1' or hd_viol = '1' then
            report "  FAIL: a comfortable link reported a violation";
            errs := errs + 1;
        end if;

        -- 2. Data launched late relative to the trailing edge.
        restart;
        drive_setup_case(8 ns, x"A5");
        report "  8 ns skew: setup=" & integer'image(to_integer(min_setup)) &
               " hold=" & integer'image(to_integer(min_hold));
        expect_near("skewed setup", to_integer(min_setup), (H - 8 ns) / TCLK);
        expect_near("skewed hold",  to_integer(min_hold),  (H + 8 ns) / TCLK);
        if su_viol /= '1' then
            report "  FAIL: a short setup did not trip T_SU_MIN";
            errs := errs + 1;
        end if;
        if hd_viol = '1' then
            report "  FAIL: hold flagged while hold was growing";
            errs := errs + 1;
        end if;

        -- 3. The converse: hold collapses, setup stays generous.
        restart;
        drive_hold_case(4 ns, x"A5");
        report "  4 ns clk-to-out: setup=" &
               integer'image(to_integer(min_setup)) &
               " hold=" & integer'image(to_integer(min_hold));
        expect_near("late-launch hold", to_integer(min_hold), 4 ns / TCLK);
        if hd_viol /= '1' then
            report "  FAIL: a short hold did not trip T_HD_MIN";
            errs := errs + 1;
        end if;
        if su_viol = '1' then
            report "  FAIL: setup flagged on a frame with generous setup";
            errs := errs + 1;
        end if;

        -- 4. clear restarts the window.
        restart;
        if su_viol = '1' or hd_viol = '1' or samples /= 0 then
            report "  FAIL: clear did not restart the measurement window";
            errs := errs + 1;
        end if;
        drive_setup_case(0 ns, x"A5");
        if su_viol = '1' or hd_viol = '1' then
            report "  FAIL: a clean frame after a violating one still reports a violation";
            errs := errs + 1;
        end if;
        report "  after clear: setup=" & integer'image(to_integer(min_setup)) &
               " hold=" & integer'image(to_integer(min_hold));

        errors <= errs;
        if errs = 0 then
            report "PASS: setup and hold are measured back to within one oversampling period of their constructed values, each limit trips only its own flag, and clear restarts the window";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

Parity

All three implement the same monitor: identical ports and generics, two-stage synchronisers, the setup taken as the stability count at the sampling edge, the hold measured to the next transition, sticky per-window violation flags, and a clear that restarts the window. All three testbenches construct the same four frames and measure back the same values exactly — ten periods of setup and hold on a clean link, six and fourteen with an eight-nanosecond launch skew, and a hold of two with a four-nanosecond clock-to-out.

8. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_su_h_monitor.sva — what an instrument must promise
   // 1. NO BIAS. A known interval must measure back as itself. This is
   //    the property that catches a counter reloaded with the wrong
   //    starting value -- an error that keeps every reading
   //    self-consistent while making the whole set wrong.
   a_unbiased : assert property (
       @(posedge clk) disable iff (!rst_n)
           (sample_now && constructed_setup_valid)
               |-> (since_change == constructed_setup))
       else $error("the measured setup does not equal the constructed one");

   // 2. INDEPENDENCE. Each limit trips only its own flag. A frame with a
   //    short setup and a generous hold must not raise the hold flag --
   //    otherwise the instrument cannot tell you which end to fix.
   a_flags_independent : assert property (
       @(posedge clk) disable iff (!rst_n)
           (hd_viol && !$past(hd_viol)) |->
               ($past(hold_cnt) < T_HD_MIN))
       else $error("the hold flag was raised by something other than a hold");

   // 3. The reported minimum really is a minimum -- it never rises within
   //    a window, only falls.
   a_min_monotone : assert property (
       @(posedge clk) disable iff (!rst_n || clear)
           (min_setup <= $past(min_setup)))
       else $error("the reported minimum setup increased within a window");

   // 4. clear restarts everything. A verdict carried over from a previous
   //    frame is worse than no verdict, because it looks current.
   a_clear_restarts : assert property (
       @(posedge clk) disable iff (!rst_n)
           (clear) |=> (!su_viol && !hd_viol && samples == 0))
       else $error("clear did not restart the measurement window");

Property 1 is the one that transfers beyond SPI. Any measurement block must be validated against constructed inputs, never against its own output or against another measurement of the same thing. A systematic bias is invisible to every self-consistency check — all the readings agree with each other, and all of them are wrong by the same amount.

Property 3 encodes something easy to get wrong in the RTL: min_setup must be initialised to the maximum representable value, not to zero, or the first comparison can never lower it and the block reports zero for every window.

Coverage should target the relationship to the limit, not the raw values:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_timing_cg.sv — margins, not measurements
   covergroup spi_timing_cg @(posedge clk iff sample_now);
       // Distance from the limit is what matters. A suite whose setups are
       // all comfortable never demonstrates that the flag can fire.
       cp_su_margin : coverpoint su_margin_class {
           bins violating    = {SU_BELOW};
           bins exactly_at   = {SU_EQUAL};     // must NOT flag
           bins one_over     = {SU_PLUS_ONE};  // must NOT flag
           bins comfortable  = {SU_AMPLE};
       }

       cp_hd_margin : coverpoint hd_margin_class {
           bins violating    = {HD_BELOW};
           bins exactly_at   = {HD_EQUAL};     // must NOT flag
           bins one_over     = {HD_PLUS_ONE};
           bins comfortable  = {HD_AMPLE};
       }

       // The two failures are independent and a suite must produce each
       // ALONE, or it cannot show the flags are not simply tied together.
       cp_combo : coverpoint viol_combo {
           bins neither   = {V_NONE};
           bins setup_only = {V_SU};
           bins hold_only  = {V_HD};
           bins both       = {V_BOTH};
       }
   endgroup

cp_combo is the coverpoint that earns its place. Setup-only and hold-only must each be reachable, because a monitor whose two flags are accidentally driven by the same condition passes every test that violates both at once.

9. Why an FPGA or ASIC Engineer Cares

Constrain MOSI and SCLK together, not separately. The margin is a difference of delays, so what matters is their relationship. A constraint on each line independently can be met while the skew between them is unacceptable.

Treat any single-line delay as a direct margin loss. A buffer, a level shifter, or a series resistor on one net and not the others subtracts from setup without giving anything back. If a level shifter is needed, put it on all the lines so the delay is common-mode.

Derate t_V for your actual load. The datasheet's number assumes its own capacitance. Board capacitance is routinely two or three times that, and t_V is the dominant term in the frequency budget.

Leave the monitor instantiated on debug builds. It answers, in hardware, whether the link meets the datasheet — which otherwise needs a scope, a working trigger, and someone available to look at it.

Do not use this monitor in place of static timing analysis. It resolves to its own clock period. The two tools see different faults: STA sees the nanoseconds inside your device, and the monitor sees the wrong divisor, the wrong mode and the slower-than-specified part, none of which are in any netlist.

10. Failure Signature — A Design That Meets Timing and Still Fails

Symptom. The FPGA build reports positive slack on every SPI path. The constraints were written carefully and reviewed. The link nevertheless produces occasional wrong bytes on MOSI writes — the device acknowledges but stores the wrong value, roughly once in several thousand transfers.

What "positive slack" establishes. Everything inside the FPGA meets its constraints. So the fault is either outside the device, or inside something the constraints never described.

Plausible mechanisms.

  • The constraint describes the wrong relationship. An output delay constraint written against the wrong clock edge — or against SCLK's launch edge when the device samples on the other one — can be met perfectly while the actual margin is half a period out.
  • A delay on one line only. A level shifter, buffer or series resistor on MOSI but not SCLK is outside the FPGA and therefore outside the timing report entirely.
  • The device's t_SU was taken from the wrong row — the SDO row rather than the SDI row, which is the naming trap of §1.
  • The board's load pushing the real t_V beyond the quoted figure, which affects reads rather than writes and so does not fit a MOSI-write symptom.
  • Crosstalk from SCLK into MOSI, which fits an occasional, non-reproducible error better than any of the above.

The discriminating observation. The rarity is the clue. A constraint error or a wrong table row is a constant offset and would corrupt a large fraction of transfers, not one in thousands. One in thousands means the margin is almost adequate and something is varying — noise, crosstalk, or a data pattern dependence.

So capture the failing transfers and look for a pattern correlation: if the errors cluster on bytes where MOSI changes on many consecutive bits, crosstalk or an edge-rate effect is indicated. If they are uniformly distributed across patterns, look for something time-varying instead.

Then measure with the monitor of §7 rather than reasoning further: it reports the worst setup and hold actually observed over millions of transfers, which is precisely the statistic this symptom needs and precisely what a scope triggered by hand cannot give you.

The fix depends on which it was, and the point of the monitor is to tell you before you start changing things. But the common root — a delay element on one line and not the others — is fixed by making the delay common-mode rather than by tightening anything.

Why the investigation goes wrong. Because "timing is met" is treated as a conclusion rather than a scope-limited statement. The tool reports on the paths it was given, inside the device it was given. Everything past the pin — the shifter, the resistor, the trace, the other device's capacitance — was never in the model.

11. Common Misconceptions

12. Reason It Through

Work this before reading the answer.

A device's table gives t_SU = 5 ns, t_HD = 5 ns, t_V = 12 ns at 30 pF, and f_SCLK(max) = 50 MHz. Your board presents about 60 pF on MISO and needs a level shifter on every line, adding 3 ns each way. Your controller's clock-to-out is 2 ns and its input setup is 2 ns; traces contribute 0.5 ns each way.

What rate can you actually run, and which single change would help most?

First, decide which numbers constrain what. t_SU and t_HD are your obligations on the outbound path; t_V is the device's promise on the return path; f_SCLK(max) is the device's internal limit. The rate is limited by the round trip, so t_V is the term that matters.

Derate t_V for the real load. The table quotes 12 ns at 30 pF and your board presents roughly double. Output delay grows with load; a reasonable working assumption is that doubling the capacitance adds meaningfully to the transition — take t_V as about 16 ns rather than 12. (The exact scaling is part-specific; the discipline is to derate rather than to use the quoted figure.)

Now build Chapter 9.4's budget, remembering the level shifter is in the path twice — once outbound with the clock, once inbound with the data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   controller clock-to-out ....  2.0 ns
   level shifter (out) ........  3.0 ns
   trace out ..................  0.5 ns
   device t_V (derated) ....... 16.0 ns
   level shifter (back) .......  3.0 ns
   trace back .................  0.5 ns
   controller input setup .....  2.0 ns
   ──────────────────────────────────
   total ...................... 27.0 ns  →  T ≥ 54 ns  →  f_max ≈ 18.5 MHz

So about 18 MHz, against a device rated 50. The device's own limit never binds — it is nearly three times the achievable rate.

Then apply divisor granularity. With an 80 MHz system clock the options near 18 MHz are 20 MHz (divisor 4) and 16 MHz (divisor 5). Round down: 16 MHz.

Which single change helps most? Look at the budget's terms. t_V is 16 of 27 ns. The level shifters together are 6. The traces are 1.

  • Removing the level shifters — a device at the controller's voltage — saves 6 ns and lifts the ceiling to about 24 MHz.
  • Halving the board capacitance on MISO recovers most of the 4 ns derate and lifts it to about 21 MHz.
  • Choosing a part with a faster t_V is worth more than both: a device specifying 6 ns instead of 12 removes about 10 ns from the budget and roughly doubles the ceiling.

And the change that beats all of them is not in the budget at all: sampling a half cycle later turns the constraint from T/2 ≥ 27 ns into T ≥ 27 ns and doubles the achievable rate to about 37 MHz, at the cost of one flip-flop. When the round trip is the binding limit, changing the structure of the constraint beats improving any term within it.

The general lesson. Work the budget before choosing the part, derate for your own conditions, and check whether the binding constraint can be restructured before trying to optimise its terms.

13. Understanding Check

14. Summary

A timing table is mostly a list of your obligations, not the device's. Write the owner beside every row: setup, hold and the chip-select times are things you must do; the output valid, disable and hold times are promises you may rely on. SDI and SDO are named from the device's point of view, so an SDI row constrains what you drive.

Conditions are part of every number. Load, supply, temperature and drive setting all qualify the value, and the quoted load is routinely a fraction of what your board presents.

Setup and hold are nearly free on a correctly clocked master, because launch and sample are on opposite edges — so the margin only tightens at high rates, under a mode error, or when a delay is added to one line and not the others, which subtracts directly.

t_V is the device's promise and the dominant term in the frequency budget, which is why it is the number to check before choosing the part.

f_SCLK(max) is a derived limit, conditional on a command, a load and a corner, and it is one of four terms rather than the answer.

In hardware the table becomes a monitor that measures the real intervals — with the common synchroniser delay cancelling in the difference, and the counters reloaded so that no constant bias creeps into readings that would otherwise look perfectly self-consistent. It resolves to its own clock and catches what static timing analysis structurally cannot: the wrong divisor, the wrong mode, the slower-than-specified part.

15. What Comes Next

Pass two is finished: the mode is known, the rate is budgeted, and the link can be measured against the numbers the vendor published.

Chapter 10.4 — Command Encoding and Register Access begins pass three. Command tables pack a read/write bit, sometimes an auto-increment bit and a register address into a single byte — and which bit is which, and which polarity means read, differ between parts in ways that make a driver silently perform writes when it believes it is reading. The chapter turns the convention into parameters and builds the codec that encodes and decodes it, in all three HDLs.

Continue learning