Skip to content
VLSI Mentor

SPI · Module 15

Architecture B — Oversampling SCLK

The oversampler's precondition has two parts of different kinds: one arithmetic, exact and fully settled by simulation — below which the recovered edge count is meaningless, with a half-period of exactly half the system period recovering zero edges from forty — and one metastability, whose sharpest row is a ratio where simulation recovers every edge and silicon does not.

Chapter 14.1 built an oversampling front end and stated its precondition in one line. This chapter is about the precondition itself, because it has two parts and they are completely different in kind.

SCLK must be slow enough. Slow enough for what, exactly — and which half of the answer can a testbench establish?

1. The Two Limits

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   THE HARD LIMIT   an SCLK half-period shorter than one system clock period may
                    contain no sampling instant at all, so the level is never
                    observed and the edge is never recovered.
                    This is ARITHMETIC. It is exact, it has nothing to do with
                    metastability, and SIMULATION SHOWS IT PERFECTLY.

   THE SOFT LIMIT   an SCLK half-period longer than one system clock but not much
                    longer can still lose an edge, because a synchroniser's first
                    flop may be sampled while its input is changing and may resolve
                    to the OLD value. If the old value is still in place at the next
                    sampling instant, the half-period is never seen.
                    This is METASTABILITY. It is probabilistic, and NO SIMULATION
                    CAN SHOW IT.

Confusing the two is the most common error in a CDC review, in both directions. Reviewers who have only met the hard limit conclude that a ratio of two is sufficient — because that is what the arithmetic says and what simulation confirms. Reviewers who have only met the soft limit conclude the whole question is unanswerable and settle for "use a synchroniser".

Both parts are needed, and they combine into:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   an SCLK half-period must span at least SYNC_N + 1 system clocks

where SYNC_N + 1 rather than 2 is the point: the margin has to cover the whole synchroniser chain resolving, not just one flop being sampled twice.

2. The Measurement

A 10 ns system clock, SYNC_N = 2, forty driven edges at each half-period. The driven count is kept by the bench in pin time, independently of the design — which is what makes a loss measurable rather than inferable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   half_period  ratio  driven  recovered  lost  half_min
         20 ns    2.0      40         40     0         2
         15 ns    1.5      40         40     0         1
         10 ns    1.0      40         40     0         1
          8 ns    0.8      40         24    16         1
          5 ns    0.5      40          0    40     65535
          3 ns    0.3      40          8    32         1
          2 ns    0.2      40          8    32         1

Three things in that table are worth more than the headline.

The boundary is at a ratio of 1.0, not at the rule's 3.0. Every edge is recovered at 1.5 and at 1.0, in simulation, forever. §4 is about what that means.

At exactly half the system period the design recovers ZERO edges. SCLK is at the same level at every single sampling instant, so the recovered level never changes. The design does not report a fast clock — it reports no clock at all, and half_min stays saturated at 65535 because no interval was ever measured. That is a failure mode of sampling rather than of measurement, and it is the most vivid single fact in the chapter.

Below the limit the count is not low, it is meaningless. At 3 ns and 2 ns the design recovers eight edges — and those eight correspond to nothing that was driven. They are aliases. So half_min reading 1 alongside them is meaningless too, and a design that reported "the ratio was too low" would be understating the problem: the measurement itself has stopped meaning anything.

Twelve system-clock cycles across four rows. An SCLK pin row toggles every half system-clock period. A sampling-instant row marks every system clock edge. The recovered level stays constant throughout, and the recovered edge count remains zero.every sample lands on the same levelevery sample lands on thesame levelrecovered edges: still zerorecovered edges: still zeroclk (10 ns)sclk (5 ns half)sampling instantsclk_q (recovered)t0t1t2t3t4t5t6t7t8t9t10t11
Figure 1 — the aliasing row, at an SCLK half-period of exactly half the system period. Every system-clock sampling instant falls on the same SCLK level, so the recovered value never changes and not one of forty edges is recovered. The design reports a bus with no clock on it rather than a clock that is too fast, which is why below the hard limit the recovered count is meaningless rather than merely low.

The SCLK row is drawn as a constant deliberately: at this ratio the pin is toggling, and the recovered view of it is a constant, and the figure shows what the design sees rather than what the bus does.

3. The Block Diagram

An SCLK pin through a synchroniser chain to an edge detector producing a strobe, with an interval counter feeding a recovered-edge count and a measured shortest half-periodsclk_pinSYNC_N flopsedge detectorinterval counteredge_stbedges_recoveredhalf_minreload12
Figure 2 — the oversampler, reduced to what the precondition is about. No chip select, no MOSI, no polarity: the subject is the recovery, and everything else would be noise. The dashed path is the measurement, and it is the only thing that makes the assumption checkable on hardware — the recovered count can be compared against a count the bench kept in pin time, which is how the loss in section 2 was measured rather than inferred.

Two implementation details in that block carry weight.

The interval counter reloads with ONE rather than zero. The cycle an edge is detected on is itself the first cycle of the next interval, so reloading with zero measures every half-period one short — and a master supplying exactly HALF_MIN is then reported as violating the precondition. Chapter 14.1 has the same detail for the same reason.

The counter saturates rather than wrapping. The interval after reset is unbounded, and a wrap would turn it into a very small number — so half_min would record a violation that never happened. Saturation is what makes the aliasing row report 65535 rather than something that looks like a measurement.

4. The Row That Simulation Gets Right And Silicon Does Not

At a half-period of exactly one system clock, the bench recovers every one of forty edges and half_min reads 1 — below the stated rule of 3.

Both facts are asserted, and the gap between them is the chapter.

And the row both agree on: at exactly SYNC_N + 1 = 3 system clocks per half-period, all forty edges arrive and the measurement reads 3. The rule's boundary is where the design says it is, which is the positive half of the claim and is worth checking so that the rule is not merely conservative.

5. Building the Oversampler — Three HDLs

The circuit

A synchroniser chain, an edge detector, a saturating interval counter, and two published measurements. Deliberately smaller than Chapter 14.1's front end, because the subject is the recovery.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_edge_recover.sv — a chain, an edge detector, and a saturating interval counter that makes the assumption checkable
// spi_edge_recover.sv
//
// Chapter 15.3 -- Architecture B, and the two limits it has.
//
// Chapter 14.1 built an oversampling front end and stated its precondition. This
// file is about the precondition itself rather than about the front end, because
// the precondition has TWO parts and they are completely different in kind:
//
//   THE HARD LIMIT      an SCLK half-period shorter than one system clock period
//                       may contain no sampling instant at all, so the level is
//                       never observed and the edge is never recovered. This is
//                       arithmetic. It is exact, it has nothing to do with
//                       metastability, and SIMULATION SHOWS IT PERFECTLY.
//
//   THE SOFT LIMIT      an SCLK half-period longer than one system clock but not
//                       much longer can still lose an edge, because a
//                       synchroniser's first flop may be sampled while its input
//                       is changing and may resolve to the OLD value. If the old
//                       value is still in place at the next sampling instant, the
//                       half-period is never seen. This is metastability. It is
//                       probabilistic, and NO SIMULATION CAN SHOW IT.
//
// Getting these two confused is the most common error in a CDC review, in both
// directions. Reviewers who have only met the hard limit conclude that a ratio of
// two is sufficient, because that is what the arithmetic says and what simulation
// confirms. Reviewers who have only met the soft limit conclude that the whole
// question is unanswerable and settle for "use a synchroniser".
//
// Both parts are needed, and they combine into:
//
//     an SCLK half-period must span at least SYNC_N + 1 system clocks
//
// where SYNC_N + 1 rather than 2 is the point: the margin has to cover the whole
// synchroniser chain resolving, not just one flop being sampled twice.
//
// WHAT THIS BLOCK DOES THAT CHAPTER 14.1'S DOES NOT.
//
// It counts, separately:
//
//   edges_recovered   how many edges the oversampler actually produced
//   half_min          the shortest interval it measured between them
//
// and the testbench counts, independently and in PIN time, how many edges were
// DRIVEN. The difference between the two numbers is the number of edges LOST, and
// that is a direct measurement of the hard limit rather than an inference from a
// flag. A front end that merely reports "the ratio was too low" tells you it was
// unhappy; a front end whose recovered count can be compared against a driven
// count tells you what it cost.
//
// The block is deliberately smaller than Chapter 14.1's front end -- no CS, no
// MOSI, no polarity -- because the subject is the recovery and everything else
// would be noise.

module spi_edge_recover #(
    parameter int SYNC_N = 2,    // synchroniser depth
    parameter int CNT_W  = 16
) (
    input  wire              clk,
    input  wire              rst_n,
    input  wire              sclk_pin,     // asynchronous, and DATA here

    output wire              sclk_q,       // the recovered level
    output wire              edge_stb,     // one cycle per recovered edge
    output reg  [CNT_W-1:0]  edges_recovered,
    output reg  [CNT_W-1:0]  half_min,     // shortest interval between edges
    input  wire              clr
);

    // The chain. Nothing else in this file is subtle, and this is where the whole
    // argument lives: SYNC_N flops, and the value that comes out is settled but is
    // not guaranteed to be the value the pin had when it was sampled.
    reg [SYNC_N-1:0] sr;
    reg              sclk_d;

    assign sclk_q   = sr[SYNC_N-1];
    assign edge_stb = sr[SYNC_N-1] ^ sclk_d;

    // The interval counter, reloaded with ONE rather than zero for Chapter 14.1's
    // reason: the cycle an edge is detected on is the first cycle of the next
    // interval, so reloading with zero measures every half-period one short.
    reg [CNT_W-1:0] since;
    wire            saturated = (since == {CNT_W{1'b1}});

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sr              <= {SYNC_N{1'b0}};
            sclk_d          <= 1'b0;
            since           <= {CNT_W{1'b1}};
            edges_recovered <= {CNT_W{1'b0}};
            half_min        <= {CNT_W{1'b1}};
        end else begin
            sr     <= {sr[SYNC_N-2:0], sclk_pin};
            sclk_d <= sr[SYNC_N-1];

            if (clr) begin
                edges_recovered <= {CNT_W{1'b0}};
                half_min        <= {CNT_W{1'b1}};
            end

            if (edge_stb) begin
                since <= {{(CNT_W-1){1'b0}}, 1'b1};
                edges_recovered <= edges_recovered + 1'b1;
                // The first interval after reset is unbounded and is not a
                // measurement of anything, so a saturated counter is ignored.
                if (!saturated && since < half_min)
                    half_min <= since;
            end else if (!saturated) begin
                since <= since + 1'b1;
            end
        end
    end

`ifdef SPI_CHECKS
    // WHAT CAN AND CANNOT BE CHECKED HERE, stated because the obvious check is
    // wrong and the wrong version fired on the design working.
    //
    // "The edge strobe is never two cycles wide" looks like an invariant and is
    // not. At a half-period of 15 ns against a 10 ns clock the recovered level
    // genuinely changes on two consecutive cycles, so two adjacent strobes are TWO
    // EDGES correctly recovered. A width check there fails on correct behaviour,
    // which is the same trap as Chapter 15.1's both-edge detector.
    //
    // What IS checkable is that the counters are monotone between clears, which is
    // weak but true -- and beyond that, nothing. The interesting property of this
    // block is whether an edge was LOST, and a design cannot observe an edge it
    // never saw. That observation has to come from outside, which is why the
    // testbench keeps its own count in pin time.
    reg [CNT_W-1:0] chk_prev;
    always_ff @(posedge clk) begin
        if (!rst_n) begin
            chk_prev <= {CNT_W{1'b0}};
        end else begin
            if (!clr && edges_recovered < chk_prev)
                $fatal(1, "the recovered count decreased without a clear");
            chk_prev <= edges_recovered;
        end
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_edge_recover.v — the same design in Verilog-2001
// spi_edge_recover.v
//
// Chapter 15.3 -- Architecture B, and the two limits it has.
//
// Chapter 14.1 built an oversampling front end and stated its precondition. This
// file is about the precondition itself rather than about the front end, because
// the precondition has TWO parts and they are completely different in kind:
//
//   THE HARD LIMIT      an SCLK half-period shorter than one system clock period
//                       may contain no sampling instant at all, so the level is
//                       never observed and the edge is never recovered. This is
//                       arithmetic. It is exact, it has nothing to do with
//                       metastability, and SIMULATION SHOWS IT PERFECTLY.
//
//   THE SOFT LIMIT      an SCLK half-period longer than one system clock but not
//                       much longer can still lose an edge, because a
//                       synchroniser's first flop may be sampled while its input
//                       is changing and may resolve to the OLD value. If the old
//                       value is still in place at the next sampling instant, the
//                       half-period is never seen. This is metastability. It is
//                       probabilistic, and NO SIMULATION CAN SHOW IT.
//
// Getting these two confused is the most common error in a CDC review, in both
// directions. Reviewers who have only met the hard limit conclude that a ratio of
// two is sufficient, because that is what the arithmetic says and what simulation
// confirms. Reviewers who have only met the soft limit conclude that the whole
// question is unanswerable and settle for "use a synchroniser".
//
// Both parts are needed, and they combine into:
//
//     an SCLK half-period must span at least SYNC_N + 1 system clocks
//
// where SYNC_N + 1 rather than 2 is the point: the margin has to cover the whole
// synchroniser chain resolving, not just one flop being sampled twice.
//
// WHAT THIS BLOCK DOES THAT CHAPTER 14.1'S DOES NOT.
//
// It counts, separately:
//
//   edges_recovered   how many edges the oversampler actually produced
//   half_min          the shortest interval it measured between them
//
// and the testbench counts, independently and in PIN time, how many edges were
// DRIVEN. The difference between the two numbers is the number of edges LOST, and
// that is a direct measurement of the hard limit rather than an inference from a
// flag. A front end that merely reports "the ratio was too low" tells you it was
// unhappy; a front end whose recovered count can be compared against a driven
// count tells you what it cost.
//
// The block is deliberately smaller than Chapter 14.1's front end -- no CS, no
// MOSI, no polarity -- because the subject is the recovery and everything else
// would be noise.

module spi_edge_recover #(
    parameter SYNC_N = 2,    // synchroniser depth
    parameter CNT_W  = 16
) (
    input  wire              clk,
    input  wire              rst_n,
    input  wire              sclk_pin,     // asynchronous, and DATA here

    output wire              sclk_q,       // the recovered level
    output wire              edge_stb,     // one cycle per recovered edge
    output reg  [CNT_W-1:0]  edges_recovered,
    output reg  [CNT_W-1:0]  half_min,     // shortest interval between edges
    input  wire              clr
);

    // The chain. Nothing else in this file is subtle, and this is where the whole
    // argument lives: SYNC_N flops, and the value that comes out is settled but is
    // not guaranteed to be the value the pin had when it was sampled.
    reg [SYNC_N-1:0] sr;
    reg              sclk_d;

    assign sclk_q   = sr[SYNC_N-1];
    assign edge_stb = sr[SYNC_N-1] ^ sclk_d;

    // The interval counter, reloaded with ONE rather than zero for Chapter 14.1's
    // reason: the cycle an edge is detected on is the first cycle of the next
    // interval, so reloading with zero measures every half-period one short.
    reg [CNT_W-1:0] since;
    wire            saturated = (since == {CNT_W{1'b1}});

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sr              <= {SYNC_N{1'b0}};
            sclk_d          <= 1'b0;
            since           <= {CNT_W{1'b1}};
            edges_recovered <= {CNT_W{1'b0}};
            half_min        <= {CNT_W{1'b1}};
        end else begin
            sr     <= {sr[SYNC_N-2:0], sclk_pin};
            sclk_d <= sr[SYNC_N-1];

            if (clr) begin
                edges_recovered <= {CNT_W{1'b0}};
                half_min        <= {CNT_W{1'b1}};
            end

            if (edge_stb) begin
                since <= {{(CNT_W-1){1'b0}}, 1'b1};
                edges_recovered <= edges_recovered + 1'b1;
                // The first interval after reset is unbounded and is not a
                // measurement of anything, so a saturated counter is ignored.
                if (!saturated && since < half_min)
                    half_min <= since;
            end else if (!saturated) begin
                since <= since + 1'b1;
            end
        end
    end

`ifdef SPI_CHECKS
    // WHAT CAN AND CANNOT BE CHECKED HERE, stated because the obvious check is
    // wrong and the wrong version fired on the design working.
    //
    // "The edge strobe is never two cycles wide" looks like an invariant and is
    // not. At a half-period of 15 ns against a 10 ns clock the recovered level
    // genuinely changes on two consecutive cycles, so two adjacent strobes are TWO
    // EDGES correctly recovered. A width check there fails on correct behaviour,
    // which is the same trap as Chapter 15.1's both-edge detector.
    //
    // What IS checkable is that the counters are monotone between clears, which is
    // weak but true -- and beyond that, nothing. The interesting property of this
    // block is whether an edge was LOST, and a design cannot observe an edge it
    // never saw. That observation has to come from outside, which is why the
    // testbench keeps its own count in pin time.
    reg [CNT_W-1:0] chk_prev;
    always @(posedge clk) begin
        if (!rst_n) begin
            chk_prev <= {CNT_W{1'b0}};
        end else begin
            if (!clr && edges_recovered < chk_prev)
                $fatal(1, "the recovered count decreased without a clear");
            chk_prev <= edges_recovered;
        end
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_edge_recover.vhd — the same design in VHDL
-- spi_edge_recover.vhd
--
-- Chapter 15.3 -- Architecture B, and the two limits it has.
--
-- Chapter 14.1 built an oversampling front end and stated its precondition. This
-- file is about the precondition itself rather than about the front end, because
-- the precondition has TWO parts and they are completely different in kind:
--
--   THE HARD LIMIT      an SCLK half-period shorter than one system clock period
--                       may contain no sampling instant at all, so the level is
--                       never observed and the edge is never recovered. This is
--                       arithmetic. It is exact, it has nothing to do with
--                       metastability, and SIMULATION SHOWS IT PERFECTLY.
--
--   THE SOFT LIMIT      an SCLK half-period longer than one system clock but not
--                       much longer can still lose an edge, because a
--                       synchroniser's first flop may be sampled while its input
--                       is changing and may resolve to the OLD value. If the old
--                       value is still in place at the next sampling instant, the
--                       half-period is never seen. This is metastability. It is
--                       probabilistic, and NO SIMULATION CAN SHOW IT.
--
-- Getting these two confused is the most common error in a CDC review, in both
-- directions. Reviewers who have only met the hard limit conclude that a ratio of
-- two is sufficient, because that is what the arithmetic says and what simulation
-- confirms. Reviewers who have only met the soft limit conclude that the whole
-- question is unanswerable and settle for "use a synchroniser".
--
-- Both parts are needed, and they combine into:
--
--     an SCLK half-period must span at least SYNC_N + 1 system clocks
--
-- where SYNC_N + 1 rather than 2 is the point: the margin has to cover the whole
-- synchroniser chain resolving, not just one flop being sampled twice.
--
-- WHAT THIS BLOCK DOES THAT CHAPTER 14.1'S DOES NOT.
--
-- It counts, separately:
--
--   edges_recovered   how many edges the oversampler actually produced
--   half_min          the shortest interval it measured between them
--
-- and the testbench counts, independently and in PIN time, how many edges were
-- DRIVEN. The difference between the two numbers is the number of edges LOST, and
-- that is a direct measurement of the hard limit rather than an inference from a
-- flag. A front end that merely reports "the ratio was too low" tells you it was
-- unhappy; a front end whose recovered count can be compared against a driven
-- count tells you what it cost.
--
-- The block is deliberately smaller than Chapter 14.1's front end -- no CS, no
-- MOSI, no polarity -- because the subject is the recovery and everything else
-- would be noise.

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

entity spi_edge_recover is
    generic (
        SYNC_N : positive := 2;   -- synchroniser depth
        CNT_W  : positive := 16
    );
    port (
        clk             : in  std_logic;
        rst_n           : in  std_logic;
        sclk_pin        : in  std_logic;   -- asynchronous, and DATA here

        sclk_q          : out std_logic;   -- the recovered level
        edge_stb        : out std_logic;   -- one cycle per recovered edge
        edges_recovered : out unsigned(CNT_W - 1 downto 0);
        half_min        : out unsigned(CNT_W - 1 downto 0);
        clr             : in  std_logic
    );
end entity;

architecture rtl of spi_edge_recover is

    -- The chain. Nothing else in this file is subtle, and this is where the whole
    -- argument lives: SYNC_N flops, and the value that comes out is settled but is
    -- not guaranteed to be the value the pin had when it was sampled.
    signal sr     : std_logic_vector(SYNC_N - 1 downto 0) := (others => '0');
    signal sclk_d : std_logic := '0';

    signal since     : unsigned(CNT_W - 1 downto 0) := (others => '1');
    signal saturated : std_logic;
    signal edges_r   : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal hmin_r    : unsigned(CNT_W - 1 downto 0) := (others => '1');

    constant ALL_ONES : unsigned(CNT_W - 1 downto 0) := (others => '1');

begin

    sclk_q          <= sr(SYNC_N - 1);
    edge_stb        <= sr(SYNC_N - 1) xor sclk_d;
    edges_recovered <= edges_r;
    half_min        <= hmin_r;
    saturated       <= '1' when since = ALL_ONES else '0';

    recover : process (clk, rst_n)
    begin
        if rst_n = '0' then
            sr      <= (others => '0');
            sclk_d  <= '0';
            since   <= (others => '1');
            edges_r <= (others => '0');
            hmin_r  <= (others => '1');
        elsif rising_edge(clk) then
            sr     <= sr(SYNC_N - 2 downto 0) & sclk_pin;
            sclk_d <= sr(SYNC_N - 1);

            if clr = '1' then
                edges_r <= (others => '0');
                hmin_r  <= (others => '1');
            end if;

            if (sr(SYNC_N - 1) xor sclk_d) = '1' then
                -- Reloaded with ONE rather than zero, for Chapter 14.1's reason: the
                -- cycle an edge is detected on is the first cycle of the next
                -- interval, so reloading with zero measures every half-period short.
                since   <= to_unsigned(1, CNT_W);
                edges_r <= edges_r + 1;
                -- The first interval after reset is unbounded and is not a
                -- measurement of anything, so a saturated counter is ignored.
                if saturated = '0' and since < hmin_r then
                    hmin_r <= since;
                end if;
            elsif saturated = '0' then
                since <= since + 1;
            end if;
        end if;
    end process;

    -- WHAT CAN AND CANNOT BE CHECKED HERE, stated because the obvious check is
    -- wrong and the wrong version fired on the design working. "The edge strobe is
    -- never two cycles wide" looks like an invariant and is not: at a half-period
    -- of 15 ns against a 10 ns clock the recovered level genuinely changes on two
    -- consecutive cycles, so two adjacent strobes are TWO EDGES correctly
    -- recovered. What IS checkable is that the count is monotone between clears,
    -- which is weak but true -- and beyond that, nothing, because a design cannot
    -- observe an edge it never saw.
    check : process (clk)
        variable prev : unsigned(CNT_W - 1 downto 0) := (others => '0');
    begin
        if rising_edge(clk) then
            if rst_n = '1' then
                assert not (clr = '0' and edges_r < prev)
                    report "the recovered count decreased without a clear"
                    severity failure;
                prev := edges_r;
            else
                prev := (others => '0');
            end if;
        end if;
    end process;

end architecture;

The testbench

One sweep and three directed rows. The sweep produces §2's table; the directed rows pin the aliasing case, the dangerous case and the boundary.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_edge_recover_tb.sv — one sweep and three directed rows, one of which asserts that a dangerous ratio passes
// spi_edge_recover_tb.sv
//
// One sweep, and the table it produces is the chapter.
//
// The system clock is fixed at 10 ns. SCLK is driven with a half-period from 20 ns
// down to 2 ns, and for each the bench counts edges TWO ways:
//
//   driven     counted in the bench, in PIN time, from the bench's own toggles
//   recovered  read out of the design's own counter
//
// Their difference is the number of edges LOST, which makes the hard limit a
// measurement rather than an inference. And the sweep is arranged so that the
// boundary falls inside it:
//
//   half-period >= 2 system clocks   every edge recovered, with margin
//   half-period == 1 system clock    every edge recovered in SIMULATION, and
//                                    this is the dangerous row -- see below
//   half-period <  1 system clock    edges lost, and the arithmetic says so
//
// THE DANGEROUS ROW is the one at exactly one system clock per half-period. A
// simulator recovers every edge there, because a simulated flop samples a defined
// value at a defined instant and the level is present for exactly one instant. Real
// silicon loses edges at that ratio routinely, because the level is present for
// one sampling instant and that instant is the one where setup is violated.
//
// So this bench ASSERTS that the ratio-of-two row passes, and asserts NOTHING about
// whether it is safe -- and prints a line saying so, because a table with a green
// row at a ratio nobody should ship is a table that will be quoted.

`timescale 1ns/1ps

module spi_edge_recover_tb;

    localparam int SYNC_N = 2;
    localparam int CNT_W  = 16;
    localparam int HALF_MIN_RULE = SYNC_N + 1;   // the rule from Chapter 14.1

    reg clk   = 1'b0;
    reg rst_n = 1'b1;
    always #5 clk = ~clk;                        // a 10 ns system clock, fixed

    reg sclk_pin = 1'b0;
    reg clr      = 1'b0;

    wire             sclk_q, edge_stb;
    wire [CNT_W-1:0] edges_recovered, half_min;

    spi_edge_recover #(.SYNC_N(SYNC_N), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n), .sclk_pin(sclk_pin),
        .sclk_q(sclk_q), .edge_stb(edge_stb),
        .edges_recovered(edges_recovered), .half_min(half_min), .clr(clr)
    );

    integer errors = 0;
    integer driven = 0;      // counted in PIN time, by the bench, independently

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

    // Toggles SCLK `n` times with the given half-period, counting as it goes. The
    // count is the bench's own and shares nothing with the design -- which is the
    // whole point, because a bench that read the design's counter to decide what
    // was driven could not measure a loss.
    task automatic drive_edges(input integer n, input integer half_ns);
        integer i;
        begin
            for (i = 0; i < n; i = i + 1) begin
                #(half_ns);
                sclk_pin = ~sclk_pin;
                driven = driven + 1;
            end
        end
    endtask

    task automatic restart;
        begin
            rst_n    = 1'b1;
            sclk_pin = 1'b0;
            driven   = 0;
            repeat (2) @(posedge clk);
            rst_n    = 1'b0;
            repeat (4) @(posedge clk);
            rst_n    = 1'b1;
            repeat (4) @(posedge clk);
            clr      = 1'b1;
            repeat (2) @(posedge clk);
            clr      = 1'b0;
            repeat (2) @(posedge clk);
            driven   = 0;
        end
    endtask

    integer halves [0:6];
    integer h, rec, lost;

    initial begin
        halves[0] = 20;   // 2 system clocks per half-period... no: 20 ns = 2 clocks
        halves[1] = 15;
        halves[2] = 10;   // 1 system clock per half-period: the dangerous row
        halves[3] = 8;
        halves[4] = 5;
        halves[5] = 3;
        halves[6] = 2;

        $display("  a 10 ns system clock, SYNC_N = %0d, so the stated rule is a half-period of at least %0d system clocks",
                 SYNC_N, HALF_MIN_RULE);
        $display("  half_period  ratio  driven  recovered  lost  half_min");

        for (h = 0; h <= 6; h = h + 1) begin
            restart();
            drive_edges(40, halves[h]);
            repeat (20) @(posedge clk);

            rec  = edges_recovered;
            lost = driven - rec;

            // The ratio is printed to one decimal place rather than as an integer
            // division, because the interesting rows are fractions and 0.8 and 0.2
            // both round to zero.
            $display("  %10d ns  %3d.%0d  %6d  %9d  %4d  %8d",
                     halves[h], halves[h]/10, halves[h]%10,
                     driven, rec, lost, half_min);

            // 1. NO EDGE IS EVER INVENTED. Losing them is a ratio limit; gaining
            //    them is a design fault, and the two must never be confused by
            //    someone reading the table.
            if (rec > driven) begin
                $display("  FAIL: %0d edges recovered from %0d driven", rec, driven);
                errors = errors + 1;
            end

            // 2. AT OR ABOVE ONE SYSTEM CLOCK PER HALF-PERIOD, SIMULATION LOSES
            //    NOTHING. This is the arithmetic, and it is the part simulation
            //    settles. Note carefully that it includes the ratio-of-one row,
            //    which is NOT a safe ratio -- see the header and test 4.
            if (halves[h] >= 10 && lost != 0) begin
                $display("  FAIL: %0d edges lost at a half-period of %0d ns, which spans %0d system clocks -- the arithmetic says none should be",
                         lost, halves[h], halves[h]/10);
                errors = errors + 1;
            end

            // 3. BELOW ONE SYSTEM CLOCK PER HALF-PERIOD, EDGES ARE LOST. Without
            //    this the table could be produced by a design with no limit at all.
            if (halves[h] < 10 && lost == 0) begin
                $display("  FAIL: nothing lost at a half-period of %0d ns against a 10 ns clock -- a level present for less than one sampling interval cannot always be observed",
                         halves[h]);
                errors = errors + 1;
            end

            // 4. THE MEASURED MINIMUM MATCHES THE DRIVEN HALF-PERIOD, wherever the
            //    edges all arrived. This is what makes `half_min` usable as
            //    evidence on hardware rather than as a rough indication.
            if (lost == 0 && half_min != halves[h]/10) begin
                $display("  FAIL: a driven half-period of %0d ns spans %0d system clocks and measured %0d",
                         halves[h], halves[h]/10, half_min);
                errors = errors + 1;
            end
        end

        // 4b. THE ALIASING ROW, which is the most vivid fact in the table and is
        //     worse than a loss. At a half-period of exactly HALF the system
        //     period, SCLK is at the same level at every single sampling instant,
        //     so the recovered level NEVER CHANGES and the design recovers ZERO
        //     edges from forty. It does not report a low count or a short
        //     half-period; it reports a bus with no clock on it at all.
        //
        //     And at a half-period of 3 ns or 2 ns the count is not merely low --
        //     the eight edges recovered correspond to nothing that was driven.
        //     They are aliases. So below the hard limit the recovered count is not
        //     an underestimate, it is meaningless, and `half_min` reading 1
        //     alongside it is meaningless too.
        restart();
        drive_edges(40, 5);
        repeat (20) @(posedge clk);
        if (edges_recovered != 0) begin
            $display("  FAIL: a half-period of exactly half the system period should alias to a constant and recover ZERO edges, got %0d",
                     edges_recovered);
            errors = errors + 1;
        end
        $display("  at a half-period of exactly half the system period the recovered level never changes at all, so %0d of 40 edges were recovered -- the design does not report a fast clock, it reports no clock, which is the failure mode of sampling rather than of measurement",
                 edges_recovered);

        // 5. THE ROW THAT SIMULATION GETS RIGHT AND SILICON DOES NOT. A half-period
        //    of exactly one system clock recovers every edge here and violates the
        //    stated rule of SYNC_N + 1. The bench asserts BOTH facts, because the
        //    gap between them is the chapter's subject and a reader who saw only
        //    the first would ship it.
        restart();
        drive_edges(40, 10);
        repeat (20) @(posedge clk);
        if (edges_recovered != driven) begin
            $display("  FAIL: a half-period of one system clock should recover every edge IN SIMULATION");
            errors = errors + 1;
        end
        if (half_min >= HALF_MIN_RULE) begin
            $display("  FAIL: a half-period of one system clock should measure below the stated rule of %0d",
                     HALF_MIN_RULE);
            errors = errors + 1;
        end
        $display("  at a half-period of exactly one system clock every one of %0d edges was recovered and the measured minimum was %0d, which is below the stated rule of %0d -- simulation is GREEN at a ratio that loses edges on silicon, and no amount of further simulation will say otherwise",
                 driven, half_min, HALF_MIN_RULE);

        // 6. AND THE ROW THAT BOTH AGREE ON. At SYNC_N + 1 system clocks per
        //    half-period the measurement equals the rule, which is the boundary
        //    the rule is about and the smallest ratio worth shipping.
        restart();
        drive_edges(40, HALF_MIN_RULE*10);
        repeat (20) @(posedge clk);
        if (edges_recovered != driven || half_min != HALF_MIN_RULE) begin
            $display("  FAIL: at exactly the stated rule, %0d of %0d edges arrived and the minimum measured %0d",
                     edges_recovered, driven, half_min);
            errors = errors + 1;
        end
        $display("  at exactly the stated rule of %0d system clocks per half-period, all %0d edges arrive and the measurement reads %0d -- the rule's boundary is where the design says it is",
                 HALF_MIN_RULE, driven, half_min);

        if (errors == 0)
            $display("PASS: an oversampling front end has two limits and they are different in kind -- the HARD one is arithmetic, that a level present for less than one sampling interval need not be observed at all, and the sweep measures it exactly by comparing the design's recovered count against a count the bench kept independently in pin time, losing edges at every half-period below one system clock and none at or above it -- and below that limit the count is not merely low but MEANINGLESS -- a half-period of exactly half the system period aliases to a constant and recovers zero edges from forty, reporting no clock rather than a fast one, while 3 ns and 2 ns recover eight edges that correspond to nothing driven -- and the SOFT limit is metastability, that a synchroniser's first flop may resolve to the old value and leave a half-period unseen, which is why the stated rule is SYNC_N + 1 system clocks rather than the arithmetic's one, and which this bench cannot exhibit at all: the row at exactly one system clock per half-period recovers every single edge here, reports a measured minimum below the rule, and is a ratio that loses edges on real silicon -- so the two halves of the precondition must be argued differently, one from a count and one from a datasheet, and a review that treats simulation as evidence for the second half has checked nothing");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_edge_recover_tb.v — the same bench in Verilog-2001
// spi_edge_recover_tb.v
//
// One sweep, and the table it produces is the chapter.
//
// The system clock is fixed at 10 ns. SCLK is driven with a half-period from 20 ns
// down to 2 ns, and for each the bench counts edges TWO ways:
//
//   driven     counted in the bench, in PIN time, from the bench's own toggles
//   recovered  read out of the design's own counter
//
// Their difference is the number of edges LOST, which makes the hard limit a
// measurement rather than an inference. And the sweep is arranged so that the
// boundary falls inside it:
//
//   half-period >= 2 system clocks   every edge recovered, with margin
//   half-period == 1 system clock    every edge recovered in SIMULATION, and
//                                    this is the dangerous row -- see below
//   half-period <  1 system clock    edges lost, and the arithmetic says so
//
// THE DANGEROUS ROW is the one at exactly one system clock per half-period. A
// simulator recovers every edge there, because a simulated flop samples a defined
// value at a defined instant and the level is present for exactly one instant. Real
// silicon loses edges at that ratio routinely, because the level is present for
// one sampling instant and that instant is the one where setup is violated.
//
// So this bench ASSERTS that the ratio-of-two row passes, and asserts NOTHING about
// whether it is safe -- and prints a line saying so, because a table with a green
// row at a ratio nobody should ship is a table that will be quoted.

`timescale 1ns/1ps

module spi_edge_recover_tb;

    localparam SYNC_N = 2;
    localparam CNT_W  = 16;
    localparam HALF_MIN_RULE = SYNC_N + 1;   // the rule from Chapter 14.1

    reg clk;
    reg rst_n;
    always #5 clk = ~clk;                        // a 10 ns system clock, fixed

    reg sclk_pin;
    reg clr;

    wire             sclk_q, edge_stb;
    wire [CNT_W-1:0] edges_recovered, half_min;

    spi_edge_recover #(.SYNC_N(SYNC_N), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n), .sclk_pin(sclk_pin),
        .sclk_q(sclk_q), .edge_stb(edge_stb),
        .edges_recovered(edges_recovered), .half_min(half_min), .clr(clr)
    );

    integer errors;
    integer driven;   // counted in PIN time, by the bench, independently

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

    // Toggles SCLK `n` times with the given half-period, counting as it goes. The
    // count is the bench's own and shares nothing with the design -- which is the
    // whole point, because a bench that read the design's counter to decide what
    // was driven could not measure a loss.
        task drive_edges;
        input integer n;
        input integer half_ns;
        integer i;
        begin
            for (i = 0; i < n; i = i + 1) begin
                #(half_ns);
                sclk_pin = ~sclk_pin;
                driven = driven + 1;
            end
        end
    endtask

    task restart;
        begin
            rst_n    = 1'b1;
            sclk_pin = 1'b0;
            driven   = 0;
            repeat (2) @(posedge clk);
            rst_n    = 1'b0;
            repeat (4) @(posedge clk);
            rst_n    = 1'b1;
            repeat (4) @(posedge clk);
            clr      = 1'b1;
            repeat (2) @(posedge clk);
            clr      = 1'b0;
            repeat (2) @(posedge clk);
            driven   = 0;
        end
    endtask

    integer halves [0:6];
    integer h, rec, lost;

    initial begin
        halves[0] = 20;   // 2 system clocks per half-period... no: 20 ns = 2 clocks
        halves[1] = 15;
        halves[2] = 10;   // 1 system clock per half-period: the dangerous row
        halves[3] = 8;
        halves[4] = 5;
        halves[5] = 3;
        halves[6] = 2;

        $display("  a 10 ns system clock, SYNC_N = %0d, so the stated rule is a half-period of at least %0d system clocks",
                 SYNC_N, HALF_MIN_RULE);
        $display("  half_period  ratio  driven  recovered  lost  half_min");

        for (h = 0; h <= 6; h = h + 1) begin
            restart();
            drive_edges(40, halves[h]);
            repeat (20) @(posedge clk);

            rec  = edges_recovered;
            lost = driven - rec;

            // The ratio is printed to one decimal place rather than as an integer
            // division, because the interesting rows are fractions and 0.8 and 0.2
            // both round to zero.
            $display("  %10d ns  %3d.%0d  %6d  %9d  %4d  %8d",
                     halves[h], halves[h]/10, halves[h]%10,
                     driven, rec, lost, half_min);

            // 1. NO EDGE IS EVER INVENTED. Losing them is a ratio limit; gaining
            //    them is a design fault, and the two must never be confused by
            //    someone reading the table.
            if (rec > driven) begin
                $display("  FAIL: %0d edges recovered from %0d driven", rec, driven);
                errors = errors + 1;
            end

            // 2. AT OR ABOVE ONE SYSTEM CLOCK PER HALF-PERIOD, SIMULATION LOSES
            //    NOTHING. This is the arithmetic, and it is the part simulation
            //    settles. Note carefully that it includes the ratio-of-one row,
            //    which is NOT a safe ratio -- see the header and test 4.
            if (halves[h] >= 10 && lost != 0) begin
                $display("  FAIL: %0d edges lost at a half-period of %0d ns, which spans %0d system clocks -- the arithmetic says none should be",
                         lost, halves[h], halves[h]/10);
                errors = errors + 1;
            end

            // 3. BELOW ONE SYSTEM CLOCK PER HALF-PERIOD, EDGES ARE LOST. Without
            //    this the table could be produced by a design with no limit at all.
            if (halves[h] < 10 && lost == 0) begin
                $display("  FAIL: nothing lost at a half-period of %0d ns against a 10 ns clock -- a level present for less than one sampling interval cannot always be observed",
                         halves[h]);
                errors = errors + 1;
            end

            // 4. THE MEASURED MINIMUM MATCHES THE DRIVEN HALF-PERIOD, wherever the
            //    edges all arrived. This is what makes `half_min` usable as
            //    evidence on hardware rather than as a rough indication.
            if (lost == 0 && half_min != halves[h]/10) begin
                $display("  FAIL: a driven half-period of %0d ns spans %0d system clocks and measured %0d",
                         halves[h], halves[h]/10, half_min);
                errors = errors + 1;
            end
        end

        // 4b. THE ALIASING ROW, which is the most vivid fact in the table and is
        //     worse than a loss. At a half-period of exactly HALF the system
        //     period, SCLK is at the same level at every single sampling instant,
        //     so the recovered level NEVER CHANGES and the design recovers ZERO
        //     edges from forty. It does not report a low count or a short
        //     half-period; it reports a bus with no clock on it at all.
        //
        //     And at a half-period of 3 ns or 2 ns the count is not merely low --
        //     the eight edges recovered correspond to nothing that was driven.
        //     They are aliases. So below the hard limit the recovered count is not
        //     an underestimate, it is meaningless, and `half_min` reading 1
        //     alongside it is meaningless too.
        restart();
        drive_edges(40, 5);
        repeat (20) @(posedge clk);
        if (edges_recovered != 0) begin
            $display("  FAIL: a half-period of exactly half the system period should alias to a constant and recover ZERO edges, got %0d",
                     edges_recovered);
            errors = errors + 1;
        end
        $display("  at a half-period of exactly half the system period the recovered level never changes at all, so %0d of 40 edges were recovered -- the design does not report a fast clock, it reports no clock, which is the failure mode of sampling rather than of measurement",
                 edges_recovered);

        // 5. THE ROW THAT SIMULATION GETS RIGHT AND SILICON DOES NOT. A half-period
        //    of exactly one system clock recovers every edge here and violates the
        //    stated rule of SYNC_N + 1. The bench asserts BOTH facts, because the
        //    gap between them is the chapter's subject and a reader who saw only
        //    the first would ship it.
        restart();
        drive_edges(40, 10);
        repeat (20) @(posedge clk);
        if (edges_recovered != driven) begin
            $display("  FAIL: a half-period of one system clock should recover every edge IN SIMULATION");
            errors = errors + 1;
        end
        if (half_min >= HALF_MIN_RULE) begin
            $display("  FAIL: a half-period of one system clock should measure below the stated rule of %0d",
                     HALF_MIN_RULE);
            errors = errors + 1;
        end
        $display("  at a half-period of exactly one system clock every one of %0d edges was recovered and the measured minimum was %0d, which is below the stated rule of %0d -- simulation is GREEN at a ratio that loses edges on silicon, and no amount of further simulation will say otherwise",
                 driven, half_min, HALF_MIN_RULE);

        // 6. AND THE ROW THAT BOTH AGREE ON. At SYNC_N + 1 system clocks per
        //    half-period the measurement equals the rule, which is the boundary
        //    the rule is about and the smallest ratio worth shipping.
        restart();
        drive_edges(40, HALF_MIN_RULE*10);
        repeat (20) @(posedge clk);
        if (edges_recovered != driven || half_min != HALF_MIN_RULE) begin
            $display("  FAIL: at exactly the stated rule, %0d of %0d edges arrived and the minimum measured %0d",
                     edges_recovered, driven, half_min);
            errors = errors + 1;
        end
        $display("  at exactly the stated rule of %0d system clocks per half-period, all %0d edges arrive and the measurement reads %0d -- the rule's boundary is where the design says it is",
                 HALF_MIN_RULE, driven, half_min);

        if (errors == 0)
            $display("PASS: an oversampling front end has two limits and they are different in kind -- the HARD one is arithmetic, that a level present for less than one sampling interval need not be observed at all, and the sweep measures it exactly by comparing the design's recovered count against a count the bench kept independently in pin time, losing edges at every half-period below one system clock and none at or above it -- and below that limit the count is not merely low but MEANINGLESS -- a half-period of exactly half the system period aliases to a constant and recovers zero edges from forty, reporting no clock rather than a fast one, while 3 ns and 2 ns recover eight edges that correspond to nothing driven -- and the SOFT limit is metastability, that a synchroniser's first flop may resolve to the old value and leave a half-period unseen, which is why the stated rule is SYNC_N + 1 system clocks rather than the arithmetic's one, and which this bench cannot exhibit at all: the row at exactly one system clock per half-period recovers every single edge here, reports a measured minimum below the rule, and is a ratio that loses edges on real silicon -- so the two halves of the precondition must be argued differently, one from a count and one from a datasheet, and a review that treats simulation as evidence for the second half has checked nothing");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


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

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_edge_recover_tb.vhd — the same bench in VHDL
-- spi_edge_recover_tb.vhd
--
-- One sweep, and the table it produces is the chapter.
--
-- The system clock is fixed at 10 ns. SCLK is driven with a half-period from 20 ns
-- down to 2 ns, and for each the bench counts edges TWO ways:
--
--   driven     counted in the bench, in PIN time, from the bench's own toggles
--   recovered  read out of the design's own counter
--
-- Their difference is the number of edges LOST, which makes the hard limit a
-- measurement rather than an inference. And the sweep is arranged so that the
-- boundary falls inside it:
--
--   half-period >= 2 system clocks   every edge recovered, with margin
--   half-period == 1 system clock    every edge recovered in SIMULATION, and
--                                    this is the dangerous row -- see below
--   half-period <  1 system clock    edges lost, and the arithmetic says so
--
-- THE DANGEROUS ROW is the one at exactly one system clock per half-period. A
-- simulator recovers every edge there, because a simulated flop samples a defined
-- value at a defined instant and the level is present for exactly one instant. Real
-- silicon loses edges at that ratio routinely, because the level is present for
-- one sampling instant and that instant is the one where setup is violated.
--
-- So this bench ASSERTS that the ratio-of-two row passes, and asserts NOTHING about
-- whether it is safe -- and prints a line saying so, because a table with a green
-- row at a ratio nobody should ship is a table that will be quoted.

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

entity spi_edge_recover_tb is
end entity;

architecture sim of spi_edge_recover_tb is

    constant SYNC_N        : positive := 2;
    constant CNT_W         : positive := 16;
    constant HALF_MIN_RULE : positive := SYNC_N + 1;   -- the rule from Chapter 14.1

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

    signal sclk_pin : std_logic := '0';
    signal clr      : std_logic := '0';

    signal sclk_q, edge_stb : std_logic;
    signal edges_recovered, half_min : unsigned(CNT_W - 1 downto 0);

    -- The driven edge count lives in the stimulus process as a plain VARIABLE, and
    -- `sclk_pin` is driven there too. An earlier version split them into a
    -- generator process coordinated by signals, and it was wrong in two ways at
    -- once: the count came out one high because the handshake let an extra toggle
    -- through, and the phase relative to the system clock depended on when the
    -- generator happened to be polling -- which changed the aliasing row's result
    -- from zero recovered edges to two.
    --
    -- Driving the pin from the process that counts it removes both. The count
    -- cannot disagree with the toggles because it is incremented in the same
    -- statement sequence, and the phase is exactly what the code says.

begin

    sysclk : process
    begin
        while not halt loop
            clk <= '0'; wait for 5 ns;       -- a 10 ns system clock, fixed
            clk <= '1'; wait for 5 ns;
        end loop;
        wait;
    end process;

    dut : entity work.spi_edge_recover
        generic map (SYNC_N => SYNC_N, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n, sclk_pin => sclk_pin,
                  sclk_q => sclk_q, edge_stb => edge_stb,
                  edges_recovered => edges_recovered, half_min => half_min,
                  clr => clr);

    watchdog : process
    begin
        wait for 4 ms;
        if not halt then
            report "FAIL: the simulation did not finish within its time limit"
                severity failure;
        end if;
        wait;
    end process;

    stim : process
        variable errs   : natural := 0;
        variable driven : natural := 0;
        variable rec, lost, drv : natural;
        type half_arr is array (0 to 6) of time;
        constant HALVES : half_arr :=
            (20 ns, 15 ns, 10 ns, 8 ns, 5 ns, 3 ns, 2 ns);

        procedure restart is
        begin
            sclk_pin <= '0';
            driven   := 0;
            rst_n    <= '1';
            for i in 1 to 2 loop wait until rising_edge(clk); end loop;
            rst_n    <= '0';
            for i in 1 to 4 loop wait until rising_edge(clk); end loop;
            rst_n    <= '1';
            for i in 1 to 4 loop wait until rising_edge(clk); end loop;
            clr      <= '1';
            for i in 1 to 2 loop wait until rising_edge(clk); end loop;
            clr      <= '0';
            for i in 1 to 2 loop wait until rising_edge(clk); end loop;
            driven   := 0;
        end procedure;

        -- Toggles SCLK exactly `n` times with the given half-period, counting as it
        -- goes. The count shares nothing with the design, which is the whole point:
        -- a bench that read the design's counter to decide what was driven could
        -- not measure a loss.
        procedure drive_edges(n : natural; half : time) is
        begin
            for i in 1 to n loop
                wait for half;
                sclk_pin <= not sclk_pin;
                driven := driven + 1;
            end loop;
            for i in 1 to 20 loop wait until rising_edge(clk); end loop;
        end procedure;
    begin
        report "  a 10 ns system clock, SYNC_N = " & integer'image(SYNC_N) &
               ", so the stated rule is a half-period of at least " &
               integer'image(HALF_MIN_RULE) & " system clocks";
        report "  half_period  ratio  driven  recovered  lost  half_min";

        for h in HALVES'range loop
            restart;
            drive_edges(40, HALVES(h));

            drv  := driven;
            rec  := to_integer(edges_recovered);
            if drv >= rec then lost := drv - rec; else lost := 0; end if;

            -- The ratio is printed to one decimal place, because the interesting
            -- rows are fractions and 0.8 and 0.2 both round to zero.
            report "  " & integer'image(HALVES(h) / 1 ns) & " ns  " &
                   integer'image((HALVES(h) / 1 ns) / 10) & "." &
                   integer'image((HALVES(h) / 1 ns) mod 10) & "  " &
                   integer'image(drv) & "  " & integer'image(rec) & "  " &
                   integer'image(lost) & "  " &
                   integer'image(to_integer(half_min));

            -- 1. NO EDGE IS EVER INVENTED at or above the hard limit. Below it the
            --    recovered count is meaningless rather than low (see test 4b), so
            --    the comparison is only meaningful where the limit holds.
            if HALVES(h) >= 10 ns and rec > drv then
                report "  FAIL: " & integer'image(rec) & " edges recovered from " &
                       integer'image(drv) & " driven";
                errs := errs + 1;
            end if;

            -- 2. AT OR ABOVE ONE SYSTEM CLOCK PER HALF-PERIOD, SIMULATION LOSES
            --    NOTHING. This includes the ratio-of-one row, which is NOT safe.
            if HALVES(h) >= 10 ns and lost /= 0 then
                report "  FAIL: " & integer'image(lost) &
                       " edges lost at a half-period of " &
                       integer'image(HALVES(h) / 1 ns) &
                       " ns -- the arithmetic says none should be";
                errs := errs + 1;
            end if;

            -- 3. BELOW ONE SYSTEM CLOCK PER HALF-PERIOD, EDGES ARE LOST.
            if HALVES(h) < 10 ns and lost = 0 then
                report "  FAIL: nothing lost at a half-period of " &
                       integer'image(HALVES(h) / 1 ns) &
                       " ns against a 10 ns clock";
                errs := errs + 1;
            end if;

            -- 4. THE MEASURED MINIMUM MATCHES THE DRIVEN HALF-PERIOD wherever every
            --    edge arrived, which is what makes it usable as evidence.
            if lost = 0 and to_integer(half_min) /= (HALVES(h) / 1 ns) / 10 then
                report "  FAIL: a driven half-period of " &
                       integer'image(HALVES(h) / 1 ns) & " ns measured " &
                       integer'image(to_integer(half_min));
                errs := errs + 1;
            end if;
        end loop;

        -- 4b. THE ALIASING ROW, which is worse than a loss. At a half-period of
        --     exactly HALF the system period, SCLK is at the same level at every
        --     sampling instant, so the recovered level never changes and the design
        --     recovers ZERO edges from forty -- reporting a bus with no clock on it
        --     rather than a fast one.
        restart;
        drive_edges(40, 5 ns);
        drv := driven;
        if to_integer(edges_recovered) /= 0 then
            report "  FAIL: a half-period of exactly half the system period should alias to a constant and recover ZERO edges, got " &
                   integer'image(to_integer(edges_recovered));
            errs := errs + 1;
        end if;
        report "  at a half-period of exactly half the system period the recovered level never changes at all, so " &
               integer'image(to_integer(edges_recovered)) &
               " of 40 edges were recovered -- the design does not report a fast clock, it reports no clock, which is the failure mode of sampling rather than of measurement";

        -- 5. THE ROW THAT SIMULATION GETS RIGHT AND SILICON DOES NOT.
        restart;
        drive_edges(40, 10 ns);
        drv := driven;
        if to_integer(edges_recovered) /= drv then
            report "  FAIL: a half-period of one system clock should recover every edge IN SIMULATION";
            errs := errs + 1;
        end if;
        if to_integer(half_min) >= HALF_MIN_RULE then
            report "  FAIL: a half-period of one system clock should measure below the stated rule";
            errs := errs + 1;
        end if;
        report "  at a half-period of exactly one system clock every one of " &
               integer'image(drv) & " edges was recovered and the measured minimum was " &
               integer'image(to_integer(half_min)) & ", which is below the stated rule of " &
               integer'image(HALF_MIN_RULE) &
               " -- simulation is GREEN at a ratio that loses edges on silicon, and no amount of further simulation will say otherwise";

        -- 6. AND THE ROW THAT BOTH AGREE ON.
        restart;
        drive_edges(40, HALF_MIN_RULE * 10 ns);
        drv := driven;
        if to_integer(edges_recovered) /= drv
           or to_integer(half_min) /= HALF_MIN_RULE then
            report "  FAIL: at exactly the stated rule, " &
                   integer'image(to_integer(edges_recovered)) & " of " &
                   integer'image(drv) & " edges arrived and the minimum measured " &
                   integer'image(to_integer(half_min));
            errs := errs + 1;
        end if;
        report "  at exactly the stated rule of " & integer'image(HALF_MIN_RULE) &
               " system clocks per half-period, all " & integer'image(drv) &
               " edges arrive and the measurement reads " &
               integer'image(to_integer(half_min)) &
               " -- the rule's boundary is where the design says it is";

        if errs = 0 then
            report "PASS: an oversampling front end has two limits and they are different in kind -- the HARD one is arithmetic, that a level present for less than one sampling interval need not be observed at all, and the sweep measures it exactly by comparing the design's recovered count against a count the bench kept independently in pin time, losing edges at every half-period below one system clock and none at or above it -- and below that limit the count is not merely low but MEANINGLESS: a half-period of exactly half the system period aliases to a constant and recovers zero edges from forty, reporting no clock rather than a fast one -- and the SOFT limit is metastability, that a synchroniser's first flop may resolve to the old value and leave a half-period unseen, which is why the stated rule is SYNC_N + 1 system clocks rather than the arithmetic's one, and which this bench cannot exhibit at all: the row at exactly one system clock per half-period recovers every single edge here, reports a measured minimum below the rule, and is a ratio that loses edges on real silicon -- so the two halves of the precondition must be argued differently, one from a count and one from a datasheet, and a review that treats simulation as evidence for the second half has checked nothing";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;

        halt <= true;
        wait;
    end process;

end architecture;

6. Why a Verification Engineer Cares

Keep the driven count outside the design, in pin time. A bench that read edges_recovered to decide what was driven could not measure a loss. This is Chapter 15.1's discipline and it is what turns "the ratio was too low" into "sixteen of forty edges are gone".

Put the hard limit's boundary inside the sweep, on both sides. The rows at 1.0 and 0.8 are the two that matter, and a sweep from 20 ns down to 10 ns never reaches the second.

Assert that the dangerous row PASSES, and say in the log that it is dangerous. This is the unusual move and it is the right one. The alternative — omitting the row — leaves a reader to assume the sweep's lowest green row is safe, which is exactly the wrong conclusion.

Do not check the measured minimum where edges were lost. Below the hard limit half_min describes aliases. The bench checks it only on rows where the recovered count equals the driven count, which is the only regime in which it means anything.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Properties for the oversampler. There are few, and the shortage is the point --
// most of what matters about this block is measured from outside it.

property p_strobe_follows_a_recovered_change;
    // The strobe exists exactly when the recovered level changed. Tautological, and
    // worth writing because the alternative formulations are wrong: a WIDTH check
    // fires on two genuine adjacent edges, and a PERIODICITY check assumes a ratio.
    @(posedge clk) disable iff (!rst_n)
        edge_stb |-> (sclk_q != $past(sclk_q));
endproperty

property p_count_monotone;
    // The recovered count only rises between clears. Weak, true, and about the limit
    // of what can be asserted from inside a crossing.
    @(posedge clk) disable iff (!rst_n)
        !clr |=> (edges_recovered >= $past(edges_recovered));
endproperty

property p_half_min_monotone;
    // It is a MINIMUM, not a last-seen value -- a last-seen value read from a board
    // describes the most recent transaction, which is never the interesting one.
    @(posedge clk) disable iff (!rst_n)
        !clr |=> (half_min <= $past(half_min));
endproperty

property p_saturated_interval_is_ignored;
    // The unbounded interval after reset is not a measurement. Without this the
    // first frame after every reset reports a violation.
    @(posedge clk) disable iff (!rst_n)
        (edge_stb && $past(since == '1)) |=> $stable(half_min);
endproperty
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Coverage. One axis, and it is the only one -- the whole block is about a ratio, so
// the bins are placed at the two limits rather than spread evenly.

covergroup cg_oversample @(posedge clk iff edge_stb);
    option.per_instance = 1;

    ratio: coverpoint half_period_in_dst_tenths {
        bins aliased    = {5};            // exactly half: recovers ZERO edges
        bins below_hard = {[1:9]};        // under one system clock
        bins at_hard    = {10};           // the arithmetic boundary
        bins between    = {[11:29]};      // green in simulation, unsafe on silicon
        bins at_rule    = {30};           // SYNC_N + 1
        bins safe       = {[31:$]};
    }

    // Whether any edge was lost, observed by the BENCH rather than by the design --
    // so this coverpoint needs a bench-side variable and cannot be bound to the DUT.
    lost: coverpoint any_edge_lost { bins no = {0}; bins yes = {1}; }

    x_ratio_lost: cross ratio, lost;

endgroup

7. Why an FPGA or ASIC Engineer Cares

SCLK is data, so there is no create_clock — and that is what makes the ratio a review item rather than a tool check. No timing tool will tell you the ratio is too low, because from its point of view SCLK is an input with a set_input_delay and nothing more. The number has to be computed by a person from two clock frequencies.

The synchroniser chain's depth is load-bearing and the tool would rather change it. ASYNC_REG on both flops and an anti-shift-register attribute, for Chapter 15.1's reasons — and here with an extra consequence: a chain that becomes three flops deep raises HALF_MIN to 4, which raises the minimum SCLK period by a third and invalidates the datasheet.

CNT_W sizes the measurement, not the function. At 12 bits the interval counter reaches 4095 system clocks, which at 100 MHz covers SCLK down to about 12 kHz. Slower than that saturates — and saturation is correct, because it is what makes the aliasing row report an obviously-impossible 65535 rather than something that looks like data.

The whole block is two flops, an XOR and a counter, and it runs at the system clock with a full period available. If an oversampling slave fails timing, it is not here — it is in whatever consumes the strobes.

8. Failure Signature — A Slave That Reports No Clock On A Running Bus

The symptom:

"The slave says it has seen zero SCLK edges. A scope shows SCLK running perfectly. ratio_err is clear and min_half reads its reset value."

What is happening: §2's aliasing row. The SCLK half-period is close to half the system clock period, so every sampling instant lands on the same level and the recovered view is a constant. The design has not detected a fast clock; it has detected no clock, and every one of its measurements is consistent with an idle bus.

Why ratio_err is clear: the flag is set by a measured interval being too short, and no interval was ever measured — the counter saturated and saturated intervals are ignored. That is the correct behaviour and it is also why the flag is not the thing to read first.

How to tell it from a genuinely idle bus: min_half at its saturated reset value plus a bus that a scope says is active is the signature. And the fix is not in the RTL — it is the clock ratio, and §1's rule is the number to check.

9. Common Misconceptions

"A ratio of two is enough, because the arithmetic says one is." The arithmetic sets the hard limit and metastability sets the soft one. The rule is SYNC_N + 1, and the arithmetic's boundary is green in every simulation ever run.

"Below the limit the slave sees fewer edges." Below the limit it sees edges that correspond to nothing. At exactly half the system period it sees none at all. The recovered count stops being an underestimate and becomes meaningless.

"min_half tells you how fast the bus is." It tells you the shortest interval between two recovered edges. Below the hard limit those edges are aliases, so the number describes the aliasing rather than the bus — and where nothing was recovered it reads saturated.

"A deeper synchroniser is safer." It is safer against metastability and it raises HALF_MIN, so it lowers the maximum SCLK the design supports. Three flops instead of two moves the minimum SCLK period up by a third.

"If the sweep is green down to a ratio, that ratio is supported." The sweep is green down to 1.0 and the supported ratio is 3.0. This is the one place in the module where the lowest green row in a table is actively misleading.

"The edge strobe should never be two cycles wide." It should be exactly as wide as there were edges. At a ratio of 1.5 two adjacent strobes are two recovered edges, and a width check there fails on correct behaviour.

10. Reason It Through

Q. SYNC_N = 2 and the system clock is 100 MHz. What is the fastest SCLK this front end supports, and what does simulation say about twice that?

HALF_MIN = 3, so a half-period must be at least 30 ns, so the SCLK period must be at least 60 ns — about 16.7 MHz. At twice that, a 30 ns period with 15 ns half-periods, simulation recovers every edge and half_min reads 1 — green, and unshippable. That gap is the chapter's subject, and the only thing that closes it is the rule.

Q. Why does the design recover exactly zero edges when the SCLK half-period is half the system period, rather than half the edges?

Because the sampling instants and the SCLK transitions are at the same frequency. Every system clock edge lands at the same point in SCLK's cycle, so the sampled level is the same every time and the recovered value never changes. It is aliasing in the strict sense — the sampled sequence is a constant, and a constant has no edges. Half the edges would require the sampling to drift through SCLK's period, which is what happens at a non-integer ratio.

Q. Why is the interval counter's saturation part of the design's correctness rather than an implementation convenience?

Because the interval after reset, and the interval between transactions, are unbounded. A wrapping counter turns a very long interval into a small number, so half_min records a violation that never happened and ratio_err fires on the first frame after every reset. Saturating makes the unbounded case recognisable, and the design then ignores it — which is also what lets the aliasing row report a value no measurement could produce, so a reader knows immediately that nothing was measured.

Q. A reviewer proposes deriving HALF_MIN automatically as SYNC_N + 1 rather than leaving it a separate parameter. Is that an improvement?

It removes a way to get the pair inconsistent, which is real. It also removes the ability to state a larger margin than the minimum — and a design that wants three cycles of margin at SYNC_N = 2 has a legitimate reason to say so, because the soft limit is probabilistic and the acceptable failure rate is a system-level decision rather than a synchroniser-level one. The better arrangement is to keep both and check HALF_MIN >= SYNC_N + 1 at elaboration, which is one assertion and catches the inconsistency without forbidding the margin.

Q. The bench keeps its own edge count. Why can it not simply count the strobes the design produces and compare against the number of times it toggled SCLK, using the design's own strobe output?

It can, and that is exactly what it does — the point is where the two numbers come from. The driven count is incremented in the same statement that toggles the pin, so it cannot disagree with the stimulus; the recovered count comes out of the design. A bench that instead derived the driven count from the design's strobes would have one number twice, and every loss would be invisible. The discipline is not "keep a count" but "keep a count that shares nothing with the thing being measured".

11. Understanding Check

12. Summary

The oversampler's precondition has two parts. The hard limit is arithmetic — a level present for less than one sampling interval need not be observed — and simulation settles it completely, by comparing the design's recovered count against a count the bench keeps in pin time. The soft limit is metastability, and no simulation can reach it. Together they give the rule: a half-period must span at least SYNC_N + 1 system clocks.

The sweep loses edges at every half-period below one system clock and none at or above it. And below that limit the count is not merely low but meaningless: at exactly half the system period the recovered level never changes and zero of forty edges are recovered, so the design reports no clock rather than a fast one, while 3 ns and 2 ns recover eight edges that correspond to nothing driven.

The sharpest row is at one system clock per half-period: every edge recovered, half_min reading 1 against a rule of 3, green in simulation and losing edges on silicon. The bench asserts the green result and prints that the ratio is unshippable, because a table with a green row at an unshippable ratio will be quoted.

And the row both agree on is exactly SYNC_N + 1, where all forty edges arrive and the measurement reads 3 — so the rule's boundary is where the design says it is rather than merely conservative.

Two implementation details carry the measurement: the interval counter reloads with one, because the detecting cycle is the next interval's first; and it saturates, so the unbounded post-reset interval is recognisable and the aliasing row reports an impossible 65535 rather than something that looks like data.

For verification: keep the driven count outside the design and in pin time; put the hard boundary inside the sweep on both sides; assert the dangerous row passes and say so in the log; and check half_min only where nothing was lost, because below the limit it describes aliases.

For implementation: there is no create_clock on SCLK, so no tool will ever check the ratio — it is a review item computed from two frequencies. The chain's depth is load-bearing and needs attributes, and deepening it raises HALF_MIN and lowers the maximum supported SPI frequency.

13. What Comes Next

Both architectures are built and both preconditions are measured. Nothing has yet put them side by side.

Chapter 15.4 — Choosing an Architecture wires both to the same pins with the same frame width, the same capture edge and the same stimulus, so the resulting table differs in exactly one thing: where the shift register's clock comes from. It has three regions rather than two, and the third is the one that stops "Architecture A is faster" from being remembered as "Architecture A has no limit".

Continue learning