Skip to content
VLSI Mentor

SPI · Module 14

MISO Output Enable and Release

Asserting late costs a bit; releasing late puts two output stages on one wire. Why the enable is a register rather than a fast combinational path, how the gap requirement depends on the neighbouring device, why two identical slaves never contend, and an output stage verified in three HDLs against a slow and a fast peer.

MISO is the one pin a slave drives, and on a bus with more than one slave it is shared. That gives the slave two obligations, and they are not symmetric:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ASSERT LATE     the master reads nothing for a while. Recoverable: it reads
                   the wrong first bit, which Chapter 14.4 measures and reports.

   RELEASE LATE    two devices drive the same wire. Not recoverable by anyone:
                   the contending value is neither slave's, both output stages
                   source current into each other, and on a long enough bus it
                   is a reliability problem as well as a data one.

Everything else in Module 14 is about data. This chapter is about the one failure that damages hardware, and the asymmetry is why the release gets all the attention.

Chip select goes high. How quickly must the slave stop driving MISO — and who decides?

The second half of that question has a surprising answer: not this slave, and not the master either.

1. The Enable Is A Register

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   oe <= cs_active          // cs_active is already SYNC_N behind the pin

One line, and the pin-to-release latency is therefore SYNC_N + 1 system clocks. That is not a design choice that could have been made differently: the pin is the only information the slave has about being deselected, and it arrives through the synchroniser of Chapter 14.1.

What is a choice is the alternative someone will propose the first time they see that number: derive the enable combinationally from the chip-select pin, and release in nanoseconds instead of cycles.

2. Reset Must Release The Pin

The reset condition on oe is not a formality, and it is the one place in the slave where an asynchronous reset is genuinely mandatory rather than conventional.

A slave held in reset while driving a shared bus makes every other device on that bus unusable. Not degraded — unusable, because MISO is held at whatever the output stage defaults to and no other slave's response can be read. That is a system-level failure caused by one device being held in a state that looks, from the inside, like the safest possible state.

So the ordering matters: oe must go low because of reset, asynchronously, without waiting for a clock — because a board in reset may not have a clock yet. The bench checks this before reset is removed, which is an unusual thing for a bench to do and the only way to check it at all.

3. The Third Requirement, And Why It Depends On The Neighbour

Chapter 14.4 produced two requirements from the synchroniser depth: a lead of SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third — and it is the only one of the three that is not a property of this slave alone.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   GAP >= (this slave's release latency) - (the next device's assert latency)

Read that subtraction carefully, because it has a consequence that is genuinely surprising:

Two identical oversampling slaves never contend, at any gap. The one being selected is exactly as slow to start driving as the one being deselected is to stop. The subtraction is zero, and the requirement evaporates. A board populated with two of this design can use a gap of one cycle and nothing bad happens.

Put this slave next to a device that drives immediately — an ASIC with no oversampling, or anything clocked on SCLK — and the subtraction stops being free. The neighbour's assert latency is approximately zero, so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   GAP >= SYNC_N + 1
Twelve system-clock cycles across six rows. A clock row runs throughout. This slave's chip select rises on cycle 2 and its output enable falls three cycles later on cycle 5. The peer's chip select falls on cycle 3 and its output enable rises the same cycle. A bus row shows both devices driving on cycles 3 and 4.this slave deselectedthis slave deselectedpeer drives immediatelypeer drives immediatelythis slave finally releasesthis slave finally releasesclkcs_n (this)oe (this)cs_n (peer)oe (peer)miso driverthisthisthisBOTHBOTHpeerpeerpeerpeerpeerpeerpeert0t1t2t3t4t5t6t7t8t9t10t11
Figure 2 — the contention window, at SYNC_N = 2 with a gap of one cycle and a peer that drives immediately. This slave stops driving three cycles after its select rises; the peer starts on the cycle its own select falls. Cycles 3 and 4 have two output stages on one wire, and under CPHA=0 that is exactly when the master samples the peer's first bit. Between two copies of THIS slave the window does not exist at any gap, because both latencies are three.

4. The Slave Cannot Detect Contention

This is the third thing in Module 14 that is invisible from inside the slave, after the late first bit (Chapter 14.4) and the lost edge (Chapter 14.1). It is worth collecting them, because the pattern is the point:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   invisible to the slave          visible to
   ----------------------------    ------------------------------------
   a lost SCLK edge (14.1)         nothing in simulation; a board, via
                                     the measured ratio
   a late first bit (14.4)         a pin-level observer; the slave sees
                                     only the interval, which cancels
   bus contention (14.5)           a bus-level observer only

For contention specifically: the slave is driving. What it would read back is its own value fighting someone else's, and a CMOS output stage wins or loses by drive strength rather than by arbitration. There is no state in which the slave can conclude "someone else is driving" — it can only conclude "the wire is at the value I am driving", which is what it expects.

What the slave can measure is its own CS-high time. So a back-to-back pair of transactions to this device is checkable — gap_seen reports the interval and gap_short fires if it was below GAP_MIN. A gap between this slave and a neighbour is not checkable by either of them, because neither one observes the other's select line.

That distinction is worth stating precisely because it bounds what the flag means: gap_short clear does not mean there was no contention. It means there was no contention caused by this slave being reselected too quickly.

5. The Block Diagram

Output stage: the synchronised chip select registers into an output enable which gates a tri-state driver carrying the transmit value; a gap counter measures deselected cycles and compares them against the minimumcs_activemiso_valcs_deassert_stbrst_noe registergap countertri-state drivercompare at assertmiso_padgap_seen, gap_shortasync clearstartenable12
Figure 1 — the output stage. The enable is one register fed by the synchronised select, and the tri-state is the only place in the slave where a signal has three states rather than two. The dashed path is the measurement: the gap counter runs while deselected and is compared against the minimum at the next assert, which is the only contention-adjacent fact the slave can establish about itself.

6. Building the Output Stage — Three HDLs

The circuit

One enable register, one tri-state assignment, and a gap counter. The tri-state is the only place in the slave that produces a value other than 0 or 1, and the enable is the only signal whose asynchronous reset is a system-level requirement rather than a convention.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_oe.sv — one enable register, one tri-state, and a gap measurement whose limits are part of its specification
// spi_slave_oe.sv
//
// Chapter 14.5 -- driving MISO only while selected, and releasing it in time.
//
// MISO is the one pin a slave drives, and on a bus with more than one slave it is
// shared. So there are two obligations, and they are not symmetric:
//
//   ASSERT LATE and the master reads nothing for a while. Recoverable: it reads
//   the wrong first bit, which Chapter 14.4 measures and reports.
//
//   RELEASE LATE and two devices drive the same wire. Not recoverable by anyone:
//   the contending value is neither slave's, both output stages source current
//   into each other, and on a long enough bus it is a reliability problem as well
//   as a data one.
//
// So the release is the side that gets the attention.
//
// THE ENABLE IS A REGISTER, AND IT IS DERIVED FROM THE SYNCHRONISED SELECT.
//
//     oe <= cs_active          // cs_active is already SYNC_N behind the pin
//
// which means the pin-to-release latency is SYNC_N + 1 system clocks. That is not
// a design choice that could be made differently: the pin is the only information
// the slave has, and it arrives through the synchroniser.
//
// What IS a choice is the alternative someone will propose -- deriving the enable
// combinationally from the chip-select pin to release faster. Do not:
//
//   * a glitch on the select line then DRIVES THE BUS, which is the failure the
//     enable exists to prevent;
//   * the enable is an output-stage control, so a combinational path from an
//     asynchronous input to it is a path from a pin to a pin with no flop in it,
//     which static timing analysis has nothing to say about;
//   * and it buys SYNC_N cycles on a release that the master's inter-transaction
//     gap already covers, if the gap is specified -- which is what §4 is about.
//
// THE THIRD REQUIREMENT ON THE MASTER, AND WHY IT DEPENDS ON THE NEIGHBOUR.
//
// Chapter 14.4 produced two requirements from the synchroniser depth: a lead of
// SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third, and it is the
// only one of the three that is not a property of this slave alone:
//
//     GAP >= (this slave's release latency) - (the next device's assert latency)
//
// Two identical oversampling slaves never contend, at any gap, because the one
// being selected is as slow to start driving as the one being deselected is to
// stop. Put this slave next to a device that drives immediately -- an ASIC with no
// oversampling, or anything clocked on SCLK -- and the subtraction stops being
// free: the requirement becomes GAP >= SYNC_N + 1.
//
// That is why a datasheet specifies a minimum CS-high time rather than leaving it
// to the master's judgement. The master cannot compute it, because it depends on
// two devices it does not know the internals of.
//
// AND THE SLAVE CANNOT DETECT CONTENTION. It is driving; what it reads back is its
// own value fighting someone else's, and a CMOS output stage wins or loses by
// drive strength rather than by arbitration. Only a bus-level observer sees it --
// which is the third thing in this module that is invisible from inside the slave,
// after the late first bit (14.4) and the lost edge (14.1). What the slave CAN
// measure is its own CS-high time, so a back-to-back pair of transactions to THIS
// device is checkable; a neighbour's is not.

module spi_slave_oe #(
    parameter int GAP_MIN = 3,    // recovered cycles of CS-high this slave needs
    parameter int CNT_W   = 8
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- from the front end of 14.1 --------------------------------------
    input  wire              cs_active,
    input  wire              cs_assert_stb,
    input  wire              cs_deassert_stb,

    // --- from 14.4 --------------------------------------------------------
    input  wire              miso_val,

    // --- the pin ----------------------------------------------------------
    output wire              oe,
    output wire              miso_pad,     // driven, or released to high impedance

    // --- status -----------------------------------------------------------
    output reg  [CNT_W-1:0]  gap_seen,     // this slave's own CS-high time
    output reg               gap_short,    // sticky: below GAP_MIN
    output reg  [CNT_W-1:0]  rel_cycles,   // measured release latency
    input  wire              clr_flags
);

    reg oe_r;
    reg cs_active_d;

    assign oe = oe_r;

    // The tri-state. One expression, and the only place in the slave where a pin
    // is not unconditionally driven.
    assign miso_pad = oe_r ? miso_val : 1'bz;

    reg [CNT_W-1:0] since_release;   // cycles since `oe` dropped
    reg [CNT_W-1:0] since_deassert;  // cycles since the select was seen to rise
    reg             armed;           // a release has happened and not been measured

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset releases the pin, asynchronously. A slave held in reset while
            // still driving a shared bus is the same failure as a master holding
            // a select line low in reset (Chapter 13.10), from the other side --
            // and here the consequence is that no other device on the bus can be
            // used at all until this one comes out of reset.
            oe_r           <= 1'b0;
            cs_active_d    <= 1'b0;
            since_release  <= {CNT_W{1'b0}};
            since_deassert <= {CNT_W{1'b1}};
            armed          <= 1'b0;
            gap_seen       <= {CNT_W{1'b1}};
            gap_short      <= 1'b0;
            rel_cycles     <= {CNT_W{1'b0}};
        end else begin
            // THE enable. One register, fed by the synchronised select and by
            // nothing else -- no combinational term, no bypass, no fast path.
            oe_r        <= cs_active;
            cs_active_d <= cs_active;

            if (clr_flags)
                gap_short <= 1'b0;

            // --- the measured release latency ------------------------------
            // Counted from the synchronised select falling to the enable
            // dropping. In a correct design it is the constant 1, and publishing
            // it means a reviewer reads the number rather than deriving it from
            // the synchroniser depth -- and a gate-level run that changes it says
            // so instead of failing somewhere else.
            if (cs_active_d && !cs_active) begin
                since_release <= {{(CNT_W-1){1'b0}}, 1'b1};
                armed         <= 1'b1;
            end else if (armed && !oe_r) begin
                rel_cycles <= since_release;
                armed      <= 1'b0;
            end else if (since_release != {CNT_W{1'b1}}) begin
                since_release <= since_release + 1'b1;
            end

            // --- this slave's own CS-high time -----------------------------
            // Measured in recovered cycles, so the synchroniser latency is at
            // both ends and cancels. It says nothing about a NEIGHBOUR's gap,
            // which is the case that actually causes contention -- and that
            // asymmetry is the point of the header.
            if (cs_deassert_stb) begin
                since_deassert <= {{(CNT_W-1){1'b0}}, 1'b1};
            end else if (cs_assert_stb) begin
                if (since_deassert != {CNT_W{1'b1}}) begin
                    gap_seen <= since_deassert;
                    if (since_deassert < GAP_MIN)
                        gap_short <= 1'b1;
                end
            end else if (since_deassert != {CNT_W{1'b1}}) begin
                since_deassert <= since_deassert + 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_oe.v — the same design in Verilog-2001
// spi_slave_oe.v
//
// Chapter 14.5 -- driving MISO only while selected, and releasing it in time.
//
// MISO is the one pin a slave drives, and on a bus with more than one slave it is
// shared. So there are two obligations, and they are not symmetric:
//
//   ASSERT LATE and the master reads nothing for a while. Recoverable: it reads
//   the wrong first bit, which Chapter 14.4 measures and reports.
//
//   RELEASE LATE and two devices drive the same wire. Not recoverable by anyone:
//   the contending value is neither slave's, both output stages source current
//   into each other, and on a long enough bus it is a reliability problem as well
//   as a data one.
//
// So the release is the side that gets the attention.
//
// THE ENABLE IS A REGISTER, AND IT IS DERIVED FROM THE SYNCHRONISED SELECT.
//
//     oe <= cs_active          // cs_active is already SYNC_N behind the pin
//
// which means the pin-to-release latency is SYNC_N + 1 system clocks. That is not
// a design choice that could be made differently: the pin is the only information
// the slave has, and it arrives through the synchroniser.
//
// What IS a choice is the alternative someone will propose -- deriving the enable
// combinationally from the chip-select pin to release faster. Do not:
//
//   * a glitch on the select line then DRIVES THE BUS, which is the failure the
//     enable exists to prevent;
//   * the enable is an output-stage control, so a combinational path from an
//     asynchronous input to it is a path from a pin to a pin with no flop in it,
//     which static timing analysis has nothing to say about;
//   * and it buys SYNC_N cycles on a release that the master's inter-transaction
//     gap already covers, if the gap is specified -- which is what §4 is about.
//
// THE THIRD REQUIREMENT ON THE MASTER, AND WHY IT DEPENDS ON THE NEIGHBOUR.
//
// Chapter 14.4 produced two requirements from the synchroniser depth: a lead of
// SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third, and it is the
// only one of the three that is not a property of this slave alone:
//
//     GAP >= (this slave's release latency) - (the next device's assert latency)
//
// Two identical oversampling slaves never contend, at any gap, because the one
// being selected is as slow to start driving as the one being deselected is to
// stop. Put this slave next to a device that drives immediately -- an ASIC with no
// oversampling, or anything clocked on SCLK -- and the subtraction stops being
// free: the requirement becomes GAP >= SYNC_N + 1.
//
// That is why a datasheet specifies a minimum CS-high time rather than leaving it
// to the master's judgement. The master cannot compute it, because it depends on
// two devices it does not know the internals of.
//
// AND THE SLAVE CANNOT DETECT CONTENTION. It is driving; what it reads back is its
// own value fighting someone else's, and a CMOS output stage wins or loses by
// drive strength rather than by arbitration. Only a bus-level observer sees it --
// which is the third thing in this module that is invisible from inside the slave,
// after the late first bit (14.4) and the lost edge (14.1). What the slave CAN
// measure is its own CS-high time, so a back-to-back pair of transactions to THIS
// device is checkable; a neighbour's is not.

module spi_slave_oe #(
    parameter GAP_MIN = 3,    // recovered cycles of CS-high this slave needs
    parameter CNT_W   = 8
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- from the front end of 14.1 --------------------------------------
    input  wire              cs_active,
    input  wire              cs_assert_stb,
    input  wire              cs_deassert_stb,

    // --- from 14.4 --------------------------------------------------------
    input  wire              miso_val,

    // --- the pin ----------------------------------------------------------
    output wire              oe,
    output wire              miso_pad,     // driven, or released to high impedance

    // --- status -----------------------------------------------------------
    output reg  [CNT_W-1:0]  gap_seen,     // this slave's own CS-high time
    output reg               gap_short,    // sticky: below GAP_MIN
    output reg  [CNT_W-1:0]  rel_cycles,   // measured release latency
    input  wire              clr_flags
);

    reg oe_r;
    reg cs_active_d;

    assign oe = oe_r;

    // The tri-state. One expression, and the only place in the slave where a pin
    // is not unconditionally driven.
    assign miso_pad = oe_r ? miso_val : 1'bz;

    reg [CNT_W-1:0] since_release;   // cycles since `oe` dropped
    reg [CNT_W-1:0] since_deassert;  // cycles since the select was seen to rise
    reg             armed;           // a release has happened and not been measured

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset releases the pin, asynchronously. A slave held in reset while
            // still driving a shared bus is the same failure as a master holding
            // a select line low in reset (Chapter 13.10), from the other side --
            // and here the consequence is that no other device on the bus can be
            // used at all until this one comes out of reset.
            oe_r           <= 1'b0;
            cs_active_d    <= 1'b0;
            since_release  <= {CNT_W{1'b0}};
            since_deassert <= {CNT_W{1'b1}};
            armed          <= 1'b0;
            gap_seen       <= {CNT_W{1'b1}};
            gap_short      <= 1'b0;
            rel_cycles     <= {CNT_W{1'b0}};
        end else begin
            // THE enable. One register, fed by the synchronised select and by
            // nothing else -- no combinational term, no bypass, no fast path.
            oe_r        <= cs_active;
            cs_active_d <= cs_active;

            if (clr_flags)
                gap_short <= 1'b0;

            // --- the measured release latency ------------------------------
            // Counted from the synchronised select falling to the enable
            // dropping. In a correct design it is the constant 1, and publishing
            // it means a reviewer reads the number rather than deriving it from
            // the synchroniser depth -- and a gate-level run that changes it says
            // so instead of failing somewhere else.
            if (cs_active_d && !cs_active) begin
                since_release <= {{(CNT_W-1){1'b0}}, 1'b1};
                armed         <= 1'b1;
            end else if (armed && !oe_r) begin
                rel_cycles <= since_release;
                armed      <= 1'b0;
            end else if (since_release != {CNT_W{1'b1}}) begin
                since_release <= since_release + 1'b1;
            end

            // --- this slave's own CS-high time -----------------------------
            // Measured in recovered cycles, so the synchroniser latency is at
            // both ends and cancels. It says nothing about a NEIGHBOUR's gap,
            // which is the case that actually causes contention -- and that
            // asymmetry is the point of the header.
            if (cs_deassert_stb) begin
                since_deassert <= {{(CNT_W-1){1'b0}}, 1'b1};
            end else if (cs_assert_stb) begin
                if (since_deassert != {CNT_W{1'b1}}) begin
                    gap_seen <= since_deassert;
                    if (since_deassert < GAP_MIN)
                        gap_short <= 1'b1;
                end
            end else if (since_deassert != {CNT_W{1'b1}}) begin
                since_deassert <= since_deassert + 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_oe.vhd — the same design in VHDL
-- spi_slave_oe.vhd
--
-- Chapter 14.5 -- driving MISO only while selected, and releasing it in time.
--
-- MISO is the one pin a slave drives, and on a bus with more than one slave it is
-- shared. So there are two obligations, and they are not symmetric:
--
--   ASSERT LATE and the master reads nothing for a while. Recoverable: it reads the
--   wrong first bit, which Chapter 14.4 measures and reports.
--
--   RELEASE LATE and two devices drive the same wire. Not recoverable by anyone:
--   the contending value is neither slave's, both output stages source current into
--   each other, and on a long enough bus it is a reliability problem as well as a
--   data one.
--
-- So the release is the side that gets the attention.
--
-- THE ENABLE IS A REGISTER, AND IT IS DERIVED FROM THE SYNCHRONISED SELECT.
--
--     oe <= cs_active          -- cs_active is already SYNC_N behind the pin
--
-- which means the pin-to-release latency is SYNC_N + 1 system clocks. That is not a
-- design choice that could be made differently: the pin is the only information the
-- slave has, and it arrives through the synchroniser.
--
-- What IS a choice is the alternative someone will propose -- deriving the enable
-- combinationally from the chip-select pin to release faster. Do not:
--
--   * a glitch on the select line then DRIVES THE BUS, which is the failure the
--     enable exists to prevent;
--   * the enable is an output-stage control, so a combinational path from an
--     asynchronous input to it is a path from a pin to a pin with no flop in it,
--     which static timing analysis has nothing to say about;
--   * and it buys SYNC_N cycles on a release that the master's inter-transaction
--     gap already covers, if the gap is specified.
--
-- THE THIRD REQUIREMENT ON THE MASTER, AND WHY IT DEPENDS ON THE NEIGHBOUR.
--
-- Chapter 14.4 produced two requirements from the synchroniser depth: a lead of
-- SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third, and it is the only
-- one of the three that is not a property of this slave alone:
--
--     GAP >= (this slave's release latency) - (the next device's assert latency)
--
-- Two identical oversampling slaves never contend, at any gap, because the one being
-- selected is as slow to start driving as the one being deselected is to stop. Put
-- this slave next to a device that drives immediately -- an ASIC with no
-- oversampling, or anything clocked on SCLK -- and the subtraction stops being free:
-- the requirement becomes GAP >= SYNC_N + 1.
--
-- That is why a datasheet specifies a minimum CS-high time rather than leaving it to
-- the master's judgement. The master cannot compute it, because it depends on two
-- devices it does not know the internals of.
--
-- AND THE SLAVE CANNOT DETECT CONTENTION. It is driving; what it reads back is its
-- own value fighting someone else's, and a CMOS output stage wins or loses by drive
-- strength rather than by arbitration. Only a bus-level observer sees it -- the third
-- thing in this module invisible from inside the slave, after the late first bit
-- (14.4) and the lost edge (14.1). What the slave CAN measure is its own CS-high
-- time, so a back-to-back pair of transactions to THIS device is checkable; a
-- neighbour's is not.

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

entity spi_slave_oe is
    generic (
        GAP_MIN : positive := 3;   -- recovered cycles of CS-high this slave needs
        CNT_W   : positive := 8
    );
    port (
        clk             : in  std_logic;
        rst_n           : in  std_logic;

        -- from the front end of 14.1
        cs_active       : in  std_logic;
        cs_assert_stb   : in  std_logic;
        cs_deassert_stb : in  std_logic;

        -- from 14.4
        miso_val        : in  std_logic;

        -- the pin
        oe              : out std_logic;
        miso_pad        : out std_logic;   -- driven, or released to high impedance

        -- status
        gap_seen        : out unsigned(CNT_W - 1 downto 0);
        gap_short       : out std_logic;
        rel_cycles      : out unsigned(CNT_W - 1 downto 0);
        clr_flags       : in  std_logic
    );
end entity;

architecture rtl of spi_slave_oe is

    signal oe_r        : std_logic := '0';
    signal cs_active_d : std_logic := '0';

    signal since_release  : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal since_deassert : unsigned(CNT_W - 1 downto 0) := (others => '1');
    signal armed          : std_logic := '0';
    signal gap_seen_r     : unsigned(CNT_W - 1 downto 0) := (others => '1');
    signal gap_short_r    : std_logic := '0';
    signal rel_cycles_r   : unsigned(CNT_W - 1 downto 0) := (others => '0');

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

begin

    oe         <= oe_r;
    gap_seen   <= gap_seen_r;
    gap_short  <= gap_short_r;
    rel_cycles <= rel_cycles_r;

    -- The tri-state. One expression, and the only place in the slave where a pin is
    -- not unconditionally driven.
    miso_pad <= miso_val when oe_r = '1' else 'Z';

    enable : process (clk, rst_n)
    begin
        if rst_n = '0' then
            -- Reset releases the pin, asynchronously. A slave held in reset while
            -- still driving a shared bus is the same failure as a master holding a
            -- select line low in reset (Chapter 13.10), from the other side -- and
            -- here the consequence is that no other device on the bus can be used
            -- at all until this one comes out of reset.
            oe_r           <= '0';
            cs_active_d    <= '0';
            since_release  <= (others => '0');
            since_deassert <= (others => '1');
            armed          <= '0';
            gap_seen_r     <= (others => '1');
            gap_short_r    <= '0';
            rel_cycles_r   <= (others => '0');
        elsif rising_edge(clk) then
            -- THE enable. One register, fed by the synchronised select and by
            -- nothing else -- no combinational term, no bypass, no fast path.
            oe_r        <= cs_active;
            cs_active_d <= cs_active;

            if clr_flags = '1' then
                gap_short_r <= '0';
            end if;

            -- the measured release latency: from the synchronised select falling to
            -- the enable dropping. In a correct design it is the constant 1, and
            -- publishing it means a reviewer reads the number rather than deriving
            -- it -- and a gate-level run that changes it says so.
            if cs_active_d = '1' and cs_active = '0' then
                since_release <= to_unsigned(1, CNT_W);
                armed         <= '1';
            elsif armed = '1' and oe_r = '0' then
                rel_cycles_r <= since_release;
                armed        <= '0';
            elsif since_release /= ALL_ONES then
                since_release <= since_release + 1;
            end if;

            -- this slave's own CS-high time, in recovered cycles so the latency is
            -- at both ends and cancels. It says nothing about a NEIGHBOUR's gap,
            -- which is the case that actually causes contention.
            if cs_deassert_stb = '1' then
                since_deassert <= to_unsigned(1, CNT_W);
            elsif cs_assert_stb = '1' then
                if since_deassert /= ALL_ONES then
                    gap_seen_r <= since_deassert;
                    if to_integer(since_deassert) < GAP_MIN then
                        gap_short_r <= '1';
                    end if;
                end if;
            elsif since_deassert /= ALL_ONES then
                since_deassert <= since_deassert + 1;
            end if;
        end if;
    end process;

end architecture;

The testbench

Six tests. Tests 3 and 4 are the pair that makes the chapter's point, and they are the same experiment with one parameter changed.

  1. Reset releases the pin. Checked before reset is removed, because a slave that drives during reset makes every other device on the bus unusable and that is not observable after reset has gone.
  2. Deselected means released, and the latency is published. SYNC_N + 1 cycles, measured rather than asserted.
  3. Two identical slaves never contend, at any gap. Two instances of this design, the second selected the cycle after the first is deselected, at gaps from 1 upward — and there is never a cycle on which both drive. This is the surprising half of §3 and it is checked rather than argued.
  4. A fast peer. The same experiment with a peer that drives immediately. Now the gap matters, and the bench finds the boundary: contention occurs for gaps below SYNC_N + 1 and not at or above it. The boundary is the number, and it is measured rather than assumed.
  5. The slave's own CS-high time is measurable and a neighbour's is not. Back-to-back transactions to this device set gap_short; the identical situation between this device and a peer does not, and the bench asserts that too — because the limit of the flag is part of its specification.
  6. The release latency is visible as driven-while-deselected cycles, which is the observation test 2 turns into a number.
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_oe_tb.sv — six tests; two instances and a configurable peer, because contention is a property of a wire
// spi_slave_oe_tb.sv
//
// This testbench builds a BUS, not a block. Two devices share one MISO net, and
// what is measured is whether the net ever carries a contending value -- which is
// the only place the failure exists, because neither device can see it.
//
// Two neighbours are used, and the difference between them is the chapter's whole
// argument:
//
//   an IDENTICAL oversampling slave, which is as slow to start driving as this one
//   is to stop, so the two never contend at any inter-transaction gap;
//
//   a FAST peer that drives the instant its select pin falls -- an ASIC with no
//   oversampling, or anything clocked on SCLK -- against which the gap requirement
//   stops being free and has to be measured.
//
// The gap sweep then calibrates the number: it finds the shortest gap at which the
// net is clean, and requires it to equal the release latency the slave publishes.

`timescale 1ns/1ps

module spi_slave_oe_tb;

    localparam int SYNC_N  = 2;
    localparam int GAP_MIN = 3;
    localparam int CNT_W   = 8;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #5 clk = ~clk;

    // --- the shared bus -----------------------------------------------------
    wire miso_bus;

    // --- device A: the slave under test -------------------------------------
    logic cs_a_pin   = 1'b1;
    logic sclk_pin   = 1'b0;
    logic mosi_pin   = 1'b0;
    logic clr_flags  = 1'b0;
    logic miso_val_a = 1'b1;     // a constant 1, so contention with a 0 is visible

    wire        sclk_q_a, cs_active_a, mosi_q_a;
    wire        edge_a_a, edge_b_a, cs_as_a, cs_de_a;
    wire [11:0] min_half_a;
    wire        ratio_err_a;

    spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_a (
        .clk(clk), .rst_n(rst_n), .cpol(1'b0),
        .sclk_pin(sclk_pin), .cs_n_pin(cs_a_pin), .mosi_pin(mosi_pin),
        .sclk_q(sclk_q_a), .cs_active(cs_active_a), .mosi_q(mosi_q_a),
        .edge_a_stb(edge_a_a), .edge_b_stb(edge_b_a),
        .cs_assert_stb(cs_as_a), .cs_deassert_stb(cs_de_a),
        .min_half(min_half_a), .ratio_err(ratio_err_a), .clr_flags(1'b0)
    );

    wire             oe_a;
    wire [CNT_W-1:0] gap_seen, rel_cycles;
    wire             gap_short;

    spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .cs_active(cs_active_a), .cs_assert_stb(cs_as_a),
        .cs_deassert_stb(cs_de_a),
        .miso_val(miso_val_a),
        .oe(oe_a), .miso_pad(miso_bus),
        .gap_seen(gap_seen), .gap_short(gap_short), .rel_cycles(rel_cycles),
        .clr_flags(clr_flags)
    );

    // --- device B, in two flavours ------------------------------------------
    logic cs_b_pin    = 1'b1;
    logic b_is_fast   = 1'b0;   // 0 = an identical slave, 1 = a zero-latency peer
    logic miso_val_b  = 1'b0;   // a constant 0: contention with A's 1 gives X

    // B as an identical oversampling slave.
    wire        cs_active_b, sclk_q_b, mosi_q_b;
    wire        edge_a_b, edge_b_b, cs_as_b, cs_de_b;
    wire [11:0] min_half_b;
    wire        ratio_err_b;

    spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_b (
        .clk(clk), .rst_n(rst_n), .cpol(1'b0),
        .sclk_pin(sclk_pin), .cs_n_pin(cs_b_pin), .mosi_pin(mosi_pin),
        .sclk_q(sclk_q_b), .cs_active(cs_active_b), .mosi_q(mosi_q_b),
        .edge_a_stb(edge_a_b), .edge_b_stb(edge_b_b),
        .cs_assert_stb(cs_as_b), .cs_deassert_stb(cs_de_b),
        .min_half(min_half_b), .ratio_err(ratio_err_b), .clr_flags(1'b0)
    );

    wire             oe_b_slow;
    wire [CNT_W-1:0] gap_seen_b, rel_cycles_b;
    wire             gap_short_b;
    wire             pad_b_slow;

    spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) u_b_slow (
        .clk(clk), .rst_n(rst_n),
        .cs_active(cs_active_b), .cs_assert_stb(cs_as_b),
        .cs_deassert_stb(cs_de_b),
        .miso_val(miso_val_b),
        .oe(oe_b_slow), .miso_pad(pad_b_slow),
        .gap_seen(gap_seen_b), .gap_short(gap_short_b),
        .rel_cycles(rel_cycles_b), .clr_flags(1'b0)
    );

    // B as a FAST peer: it drives the instant its select pin falls, with no
    // synchroniser at all. This is not a straw man -- a device clocked on SCLK, or
    // one built in a technology where the select pin reaches the output enable
    // through combinational logic, behaves exactly like this.
    wire pad_b_fast = (cs_b_pin == 1'b0) ? miso_val_b : 1'bz;

    // Only one flavour drives at a time.
    assign miso_bus = b_is_fast ? pad_b_fast : pad_b_slow;

    // --- the bus monitor ----------------------------------------------------
    integer contentions, driven_cycles, z_cycles;
    integer a_driving_deselected;

    always_ff @(posedge clk) begin
        if (rst_n) begin
            if (miso_bus === 1'bx) contentions   <= contentions + 1;
            else if (miso_bus === 1'bz) z_cycles <= z_cycles + 1;
            else driven_cycles <= driven_cycles + 1;
            if (oe_a && cs_a_pin) a_driving_deselected <= a_driving_deselected + 1;
        end
    end

    integer errors = 0;

    task automatic adv(input integer n);
        begin repeat (n) @(negedge clk); end
    endtask

    task automatic clear_bus_stats;
        begin
            contentions = 0; driven_cycles = 0; z_cycles = 0;
        end
    endtask

    // A transaction to A followed, after `gap` cycles of both selects high, by a
    // transaction to B. `gap` is the master's t_CSD in system clocks.
    task automatic a_then_b(input integer gap, input integer nbits,
                            input integer half);
        integer i;
        begin
            cs_a_pin = 1'b1; cs_b_pin = 1'b1; sclk_pin = 1'b0;
            adv(10);
            cs_a_pin = 1'b0;
            adv(4);
            for (i = 0; i < nbits * 2; i = i + 1) begin
                sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
                adv(half);
            end
            adv(4);
            cs_a_pin = 1'b1;
            adv(gap);
            cs_b_pin = 1'b0;
            adv(4);
            for (i = 0; i < nbits * 2; i = i + 1) begin
                sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
                adv(half);
            end
            adv(4);
            cs_b_pin = 1'b1;
            adv(10);
        end
    endtask

    integer gap, k, boundary, found;

    initial begin
        clear_bus_stats();
        a_driving_deselected = 0;

        adv(3);
        // 1. RESET RELEASES THE PIN. Checked before reset is removed, because a
        //    slave that drives a shared bus while held in reset makes every other
        //    device on that bus unusable.
        if (oe_a !== 1'b0) begin
            $display("  FAIL: the enable was asserted while in reset");
            errors = errors + 1;
        end
        if (miso_bus !== 1'bz) begin
            $display("  FAIL: the bus was driven while both devices were in reset");
            errors = errors + 1;
        end
        $display("  in reset: the enable is low and the shared bus is released");
        rst_n = 1'b1;
        adv(4);

        // 2. DESELECTED MEANS RELEASED, and the latency is published rather than
        //    implied.
        if (oe_a !== 1'b0) begin
            $display("  FAIL: deselected, the enable is still asserted");
            errors = errors + 1;
        end
        cs_a_pin = 1'b0;
        adv(6);
        if (oe_a !== 1'b1) begin
            $display("  FAIL: selected, the enable never asserted");
            errors = errors + 1;
        end
        cs_a_pin = 1'b1;
        adv(6);
        if (oe_a !== 1'b0) begin
            $display("  FAIL: deselected again, the enable did not drop");
            errors = errors + 1;
        end
        if (rel_cycles != 1) begin
            $display("  FAIL: the published release latency is %0d, expected 1 cycle past the synchroniser",
                     rel_cycles);
            errors = errors + 1;
        end
        $display("  the enable follows the synchronised select and publishes a release latency of %0d cycle past it, so the pin-to-release time is %0d system clocks",
                 rel_cycles, SYNC_N + rel_cycles);

        // 3. TWO IDENTICAL SLAVES NEVER CONTEND, at any gap. The one being
        //    selected is as slow to start as the one being deselected is to stop,
        //    so the subtraction in the header comes out negative.
        b_is_fast = 1'b0;
        clear_bus_stats();
        for (gap = 0; gap <= 6; gap = gap + 1)
            a_then_b(gap, 4, 4);
        if (contentions != 0) begin
            $display("  FAIL: two identical slaves contended on %0d cycles",
                     contentions);
            errors = errors + 1;
        end
        if (driven_cycles == 0) begin
            $display("  FAIL: the bus was never driven -- the monitor proves nothing");
            errors = errors + 1;
        end
        $display("  two identical oversampling slaves, gaps from 0 to 6: %0d cycles driven, %0d released, and no contending cycle at any gap",
                 driven_cycles, z_cycles);

        // 4. A FAST PEER. Now the gap matters, and the boundary is the number.
        b_is_fast = 1'b1;
        boundary  = 99;
        found     = 0;
        for (gap = 0; gap <= 6; gap = gap + 1) begin
            clear_bus_stats();
            a_then_b(gap, 4, 4);
            $display("    gap=%0d: %0d contending cycles", gap, contentions);
            if (contentions == 0 && gap < boundary) boundary = gap;
            if (contentions != 0) found = 1;
        end
        if (!found) begin
            $display("  FAIL: a zero-latency peer never contended -- the experiment proves nothing");
            errors = errors + 1;
        end
        if (boundary != SYNC_N + 1) begin
            $display("  FAIL: the bus became clean at a gap of %0d, expected %0d",
                     boundary, SYNC_N + 1);
            errors = errors + 1;
        end
        $display("  a zero-latency peer contends for every gap below %0d and never at or above it, which is exactly this slave's pin-to-release time",
                 boundary);

        // 5. THE SLAVE'S OWN CS-HIGH TIME is measurable, and a neighbour's is not.
        b_is_fast = 1'b0;
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        // Two transactions to THIS slave, with a generous gap.
        cs_a_pin = 1'b1; adv(10);
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
        if (gap_short) begin
            $display("  FAIL: a generous CS-high time was reported as short");
            errors = errors + 1;
        end
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        // And again with a gap below the requirement.
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(1);
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
        if (!gap_short) begin
            $display("  FAIL: a one-cycle CS-high time was not reported");
            errors = errors + 1;
        end
        $display("  a generous CS-high time to this slave is accepted and a one-cycle one is reported, with the measured value published as %0d cycles",
                 gap_seen);

        // 6. THE RELEASE LATENCY IS VISIBLE AS DRIVEN-WHILE-DESELECTED CYCLES, and
        //    it is non-zero BY DESIGN. Reporting zero here would mean the enable
        //    had a combinational path from the pin, which is the thing the chapter
        //    says not to build.
        if (a_driving_deselected == 0) begin
            $display("  FAIL: the enable never outlived the select pin -- that implies a combinational path from the pin to the output enable");
            errors = errors + 1;
        end
        $display("  the enable outlived the deselected pin on %0d cycles across the run, which is the registered release the chapter argues for rather than a fault",
                 a_driving_deselected);

        if (errors == 0)
            $display("PASS: the output enable is a single register fed by the synchronised select and by nothing else, so the pin-to-release latency is SYNC_N plus one system clocks and the enable is published rather than implied -- reset releases the shared bus asynchronously, two identical oversampling slaves never contend at any inter-transaction gap because the device being selected is as slow to start driving as the one being deselected is to stop, and a zero-latency neighbour contends for every gap below %0d and never at or above it, which is precisely this slave's release time and therefore the minimum CS-high time it requires of a master it shares a bus with -- the slave's own CS-high time is measurable and reported while a neighbour's is not, and the enable outliving the deselected pin is the registered behaviour the chapter argues for rather than a fault, because the alternative is a combinational path from an asynchronous pin to an output stage that no timing tool can constrain",
                     SYNC_N + 1);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_oe_tb.v — the same bench in Verilog-2001
// spi_slave_oe_tb.v
//
// This testbench builds a BUS, not a block. Two devices share one MISO net, and
// what is measured is whether the net ever carries a contending value -- which is
// the only place the failure exists, because neither device can see it.
//
// Two neighbours are used, and the difference between them is the chapter's whole
// argument:
//
//   an IDENTICAL oversampling slave, which is as slow to start driving as this one
//   is to stop, so the two never contend at any inter-transaction gap;
//
//   a FAST peer that drives the instant its select pin falls -- an ASIC with no
//   oversampling, or anything clocked on SCLK -- against which the gap requirement
//   stops being free and has to be measured.
//
// The gap sweep then calibrates the number: it finds the shortest gap at which the
// net is clean, and requires it to equal the release latency the slave publishes.

`timescale 1ns/1ps

module spi_slave_oe_tb;

    localparam SYNC_N  = 2;
    localparam GAP_MIN = 3;
    localparam CNT_W   = 8;

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

    // --- the shared bus -----------------------------------------------------
    wire miso_bus;

    // --- device A: the slave under test -------------------------------------
    reg cs_a_pin;
    reg sclk_pin;
    reg mosi_pin;
    reg clr_flags;
    reg miso_val_a;   // a constant 1, so contention with a 0 is visible

    wire        sclk_q_a, cs_active_a, mosi_q_a;
    wire        edge_a_a, edge_b_a, cs_as_a, cs_de_a;
    wire [11:0] min_half_a;
    wire        ratio_err_a;

    spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_a (
        .clk(clk), .rst_n(rst_n), .cpol(1'b0),
        .sclk_pin(sclk_pin), .cs_n_pin(cs_a_pin), .mosi_pin(mosi_pin),
        .sclk_q(sclk_q_a), .cs_active(cs_active_a), .mosi_q(mosi_q_a),
        .edge_a_stb(edge_a_a), .edge_b_stb(edge_b_a),
        .cs_assert_stb(cs_as_a), .cs_deassert_stb(cs_de_a),
        .min_half(min_half_a), .ratio_err(ratio_err_a), .clr_flags(1'b0)
    );

    wire             oe_a;
    wire [CNT_W-1:0] gap_seen, rel_cycles;
    wire             gap_short;

    spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .cs_active(cs_active_a), .cs_assert_stb(cs_as_a),
        .cs_deassert_stb(cs_de_a),
        .miso_val(miso_val_a),
        .oe(oe_a), .miso_pad(miso_bus),
        .gap_seen(gap_seen), .gap_short(gap_short), .rel_cycles(rel_cycles),
        .clr_flags(clr_flags)
    );

    // --- device B, in two flavours ------------------------------------------
    reg cs_b_pin;
    reg b_is_fast;   // 0 = an identical slave, 1 = a zero-latency peer
    reg miso_val_b;   // a constant 0: contention with A's 1 gives X

    // B as an identical oversampling slave.
    wire        cs_active_b, sclk_q_b, mosi_q_b;
    wire        edge_a_b, edge_b_b, cs_as_b, cs_de_b;
    wire [11:0] min_half_b;
    wire        ratio_err_b;

    spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_b (
        .clk(clk), .rst_n(rst_n), .cpol(1'b0),
        .sclk_pin(sclk_pin), .cs_n_pin(cs_b_pin), .mosi_pin(mosi_pin),
        .sclk_q(sclk_q_b), .cs_active(cs_active_b), .mosi_q(mosi_q_b),
        .edge_a_stb(edge_a_b), .edge_b_stb(edge_b_b),
        .cs_assert_stb(cs_as_b), .cs_deassert_stb(cs_de_b),
        .min_half(min_half_b), .ratio_err(ratio_err_b), .clr_flags(1'b0)
    );

    wire             oe_b_slow;
    wire [CNT_W-1:0] gap_seen_b, rel_cycles_b;
    wire             gap_short_b;
    wire             pad_b_slow;

    spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) u_b_slow (
        .clk(clk), .rst_n(rst_n),
        .cs_active(cs_active_b), .cs_assert_stb(cs_as_b),
        .cs_deassert_stb(cs_de_b),
        .miso_val(miso_val_b),
        .oe(oe_b_slow), .miso_pad(pad_b_slow),
        .gap_seen(gap_seen_b), .gap_short(gap_short_b),
        .rel_cycles(rel_cycles_b), .clr_flags(1'b0)
    );

    // B as a FAST peer: it drives the instant its select pin falls, with no
    // synchroniser at all. This is not a straw man -- a device clocked on SCLK, or
    // one built in a technology where the select pin reaches the output enable
    // through combinational logic, behaves exactly like this.
    wire pad_b_fast = (cs_b_pin == 1'b0) ? miso_val_b : 1'bz;

    // Only one flavour drives at a time.
    assign miso_bus = b_is_fast ? pad_b_fast : pad_b_slow;

    // --- the bus monitor ----------------------------------------------------
    integer contentions, driven_cycles, z_cycles;
    integer a_driving_deselected;

    always @(posedge clk) begin
        if (rst_n) begin
            if (miso_bus === 1'bx) contentions   <= contentions + 1;
            else if (miso_bus === 1'bz) z_cycles <= z_cycles + 1;
            else driven_cycles <= driven_cycles + 1;
            if (oe_a && cs_a_pin) a_driving_deselected <= a_driving_deselected + 1;
        end
    end

    integer errors;

        task adv;
        input integer n;
        begin repeat (n) @(negedge clk); end
    endtask

    task clear_bus_stats;
        begin
            contentions = 0; driven_cycles = 0; z_cycles = 0;
        end
    endtask

    // A transaction to A followed, after `gap` cycles of both selects high, by a
    // transaction to B. `gap` is the master's t_CSD in system clocks.
        task a_then_b;
        input integer gap;
        input integer nbits;
        input integer half;
        integer i;
        begin
            cs_a_pin = 1'b1; cs_b_pin = 1'b1; sclk_pin = 1'b0;
            adv(10);
            cs_a_pin = 1'b0;
            adv(4);
            for (i = 0; i < nbits * 2; i = i + 1) begin
                sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
                adv(half);
            end
            adv(4);
            cs_a_pin = 1'b1;
            adv(gap);
            cs_b_pin = 1'b0;
            adv(4);
            for (i = 0; i < nbits * 2; i = i + 1) begin
                sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
                adv(half);
            end
            adv(4);
            cs_b_pin = 1'b1;
            adv(10);
        end
    endtask

    integer gap, k, boundary, found;

    initial begin
        clear_bus_stats();
        a_driving_deselected = 0;

        adv(3);
        // 1. RESET RELEASES THE PIN. Checked before reset is removed, because a
        //    slave that drives a shared bus while held in reset makes every other
        //    device on that bus unusable.
        if (oe_a !== 1'b0) begin
            $display("  FAIL: the enable was asserted while in reset");
            errors = errors + 1;
        end
        if (miso_bus !== 1'bz) begin
            $display("  FAIL: the bus was driven while both devices were in reset");
            errors = errors + 1;
        end
        $display("  in reset: the enable is low and the shared bus is released");
        rst_n = 1'b1;
        adv(4);

        // 2. DESELECTED MEANS RELEASED, and the latency is published rather than
        //    implied.
        if (oe_a !== 1'b0) begin
            $display("  FAIL: deselected, the enable is still asserted");
            errors = errors + 1;
        end
        cs_a_pin = 1'b0;
        adv(6);
        if (oe_a !== 1'b1) begin
            $display("  FAIL: selected, the enable never asserted");
            errors = errors + 1;
        end
        cs_a_pin = 1'b1;
        adv(6);
        if (oe_a !== 1'b0) begin
            $display("  FAIL: deselected again, the enable did not drop");
            errors = errors + 1;
        end
        if (rel_cycles != 1) begin
            $display("  FAIL: the published release latency is %0d, expected 1 cycle past the synchroniser",
                     rel_cycles);
            errors = errors + 1;
        end
        $display("  the enable follows the synchronised select and publishes a release latency of %0d cycle past it, so the pin-to-release time is %0d system clocks",
                 rel_cycles, SYNC_N + rel_cycles);

        // 3. TWO IDENTICAL SLAVES NEVER CONTEND, at any gap. The one being
        //    selected is as slow to start as the one being deselected is to stop,
        //    so the subtraction in the header comes out negative.
        b_is_fast = 1'b0;
        clear_bus_stats();
        for (gap = 0; gap <= 6; gap = gap + 1)
            a_then_b(gap, 4, 4);
        if (contentions != 0) begin
            $display("  FAIL: two identical slaves contended on %0d cycles",
                     contentions);
            errors = errors + 1;
        end
        if (driven_cycles == 0) begin
            $display("  FAIL: the bus was never driven -- the monitor proves nothing");
            errors = errors + 1;
        end
        $display("  two identical oversampling slaves, gaps from 0 to 6: %0d cycles driven, %0d released, and no contending cycle at any gap",
                 driven_cycles, z_cycles);

        // 4. A FAST PEER. Now the gap matters, and the boundary is the number.
        b_is_fast = 1'b1;
        boundary  = 99;
        found     = 0;
        for (gap = 0; gap <= 6; gap = gap + 1) begin
            clear_bus_stats();
            a_then_b(gap, 4, 4);
            $display("    gap=%0d: %0d contending cycles", gap, contentions);
            if (contentions == 0 && gap < boundary) boundary = gap;
            if (contentions != 0) found = 1;
        end
        if (!found) begin
            $display("  FAIL: a zero-latency peer never contended -- the experiment proves nothing");
            errors = errors + 1;
        end
        if (boundary != SYNC_N + 1) begin
            $display("  FAIL: the bus became clean at a gap of %0d, expected %0d",
                     boundary, SYNC_N + 1);
            errors = errors + 1;
        end
        $display("  a zero-latency peer contends for every gap below %0d and never at or above it, which is exactly this slave's pin-to-release time",
                 boundary);

        // 5. THE SLAVE'S OWN CS-HIGH TIME is measurable, and a neighbour's is not.
        b_is_fast = 1'b0;
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        // Two transactions to THIS slave, with a generous gap.
        cs_a_pin = 1'b1; adv(10);
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
        if (gap_short) begin
            $display("  FAIL: a generous CS-high time was reported as short");
            errors = errors + 1;
        end
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        // And again with a gap below the requirement.
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(1);
        cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
        if (!gap_short) begin
            $display("  FAIL: a one-cycle CS-high time was not reported");
            errors = errors + 1;
        end
        $display("  a generous CS-high time to this slave is accepted and a one-cycle one is reported, with the measured value published as %0d cycles",
                 gap_seen);

        // 6. THE RELEASE LATENCY IS VISIBLE AS DRIVEN-WHILE-DESELECTED CYCLES, and
        //    it is non-zero BY DESIGN. Reporting zero here would mean the enable
        //    had a combinational path from the pin, which is the thing the chapter
        //    says not to build.
        if (a_driving_deselected == 0) begin
            $display("  FAIL: the enable never outlived the select pin -- that implies a combinational path from the pin to the output enable");
            errors = errors + 1;
        end
        $display("  the enable outlived the deselected pin on %0d cycles across the run, which is the registered release the chapter argues for rather than a fault",
                 a_driving_deselected);

        if (errors == 0)
            $display("PASS: the output enable is a single register fed by the synchronised select and by nothing else, so the pin-to-release latency is SYNC_N plus one system clocks and the enable is published rather than implied -- reset releases the shared bus asynchronously, two identical oversampling slaves never contend at any inter-transaction gap because the device being selected is as slow to start driving as the one being deselected is to stop, and a zero-latency neighbour contends for every gap below %0d and never at or above it, which is precisely this slave's release time and therefore the minimum CS-high time it requires of a master it shares a bus with -- the slave's own CS-high time is measurable and reported while a neighbour's is not, and the enable outliving the deselected pin is the registered behaviour the chapter argues for rather than a fault, because the alternative is a combinational path from an asynchronous pin to an output stage that no timing tool can constrain",
                     SYNC_N + 1);
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        cs_a_pin = 1'b1;
        sclk_pin = 1'b0;
        mosi_pin = 1'b0;
        clr_flags = 1'b0;
        miso_val_a = 1'b1;
        cs_b_pin = 1'b1;
        b_is_fast = 1'b0;
        miso_val_b = 1'b0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_oe_tb.vhd — the same bench in VHDL
-- spi_slave_oe_tb.vhd
--
-- This testbench builds a BUS, not a block. Two devices share one MISO net, and what
-- is measured is whether the net ever carries a contending value -- which is the only
-- place the failure exists, because neither device can see it.
--
-- Two neighbours are used, and the difference between them is the chapter's whole
-- argument:
--
--   an IDENTICAL oversampling slave, which is as slow to start driving as this one is
--   to stop, so the two never contend at any inter-transaction gap;
--
--   a FAST peer that drives the instant its select pin falls -- an ASIC with no
--   oversampling, or anything clocked on SCLK -- against which the gap requirement
--   stops being free and has to be measured.
--
-- The gap sweep then calibrates the number: it finds the shortest gap at which the
-- net is clean, and requires it to equal the release latency the slave publishes.

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

entity spi_slave_oe_tb is
end entity;

architecture sim of spi_slave_oe_tb is

    constant SYNC_N  : positive := 2;
    constant GAP_MIN : positive := 3;
    constant CNT_W   : positive := 8;

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

    -- the shared bus
    signal miso_bus : std_logic;

    -- device A: the slave under test
    signal cs_a_pin   : std_logic := '1';
    signal sclk_pin   : std_logic := '0';
    signal mosi_pin   : std_logic := '0';
    signal clr_flags  : std_logic := '0';
    signal miso_val_a : std_logic := '1';   -- a constant 1, so a 0 contends visibly

    signal sclk_q_a, cs_active_a, mosi_q_a : std_logic;
    signal edge_a_a, edge_b_a, cs_as_a, cs_de_a : std_logic;
    signal min_half_a : unsigned(11 downto 0);
    signal ratio_err_a : std_logic;

    signal oe_a : std_logic;
    signal gap_seen, rel_cycles : unsigned(CNT_W - 1 downto 0);
    signal gap_short : std_logic;

    -- device B, in two flavours
    signal cs_b_pin   : std_logic := '1';
    signal b_is_fast  : boolean   := false;
    signal miso_val_b : std_logic := '0';   -- a constant 0: contention gives X

    signal sclk_q_b, cs_active_b, mosi_q_b : std_logic;
    signal edge_a_b, edge_b_b, cs_as_b, cs_de_b : std_logic;
    signal min_half_b : unsigned(11 downto 0);
    signal ratio_err_b : std_logic;
    signal oe_b_slow : std_logic;
    signal gap_seen_b, rel_cycles_b : unsigned(CNT_W - 1 downto 0);
    signal gap_short_b : std_logic;
    signal pad_b_slow  : std_logic;
    signal pad_b_fast  : std_logic;

    signal contentions, driven_cycles, z_cycles : natural := 0;
    signal a_driving_deselected : natural := 0;
    signal clr_stats : std_logic := '0';

    signal errors : natural := 0;

begin

    clk <= not clk after 5 ns when not halt else '0';

    u_fe_a : entity work.spi_slave_frontend
        generic map (SYNC_N => SYNC_N, HALF_MIN => 3, CNT_W => 12)
        port map (clk => clk, rst_n => rst_n, cpol => '0',
                  sclk_pin => sclk_pin, cs_n_pin => cs_a_pin,
                  mosi_pin => mosi_pin,
                  sclk_q => sclk_q_a, cs_active => cs_active_a,
                  mosi_q => mosi_q_a,
                  edge_a_stb => edge_a_a, edge_b_stb => edge_b_a,
                  cs_assert_stb => cs_as_a, cs_deassert_stb => cs_de_a,
                  min_half => min_half_a, ratio_err => ratio_err_a,
                  clr_flags => '0');

    dut : entity work.spi_slave_oe
        generic map (GAP_MIN => GAP_MIN, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n,
                  cs_active => cs_active_a, cs_assert_stb => cs_as_a,
                  cs_deassert_stb => cs_de_a,
                  miso_val => miso_val_a,
                  oe => oe_a, miso_pad => miso_bus,
                  gap_seen => gap_seen, gap_short => gap_short,
                  rel_cycles => rel_cycles, clr_flags => clr_flags);

    u_fe_b : entity work.spi_slave_frontend
        generic map (SYNC_N => SYNC_N, HALF_MIN => 3, CNT_W => 12)
        port map (clk => clk, rst_n => rst_n, cpol => '0',
                  sclk_pin => sclk_pin, cs_n_pin => cs_b_pin,
                  mosi_pin => mosi_pin,
                  sclk_q => sclk_q_b, cs_active => cs_active_b,
                  mosi_q => mosi_q_b,
                  edge_a_stb => edge_a_b, edge_b_stb => edge_b_b,
                  cs_assert_stb => cs_as_b, cs_deassert_stb => cs_de_b,
                  min_half => min_half_b, ratio_err => ratio_err_b,
                  clr_flags => '0');

    u_b_slow : entity work.spi_slave_oe
        generic map (GAP_MIN => GAP_MIN, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n,
                  cs_active => cs_active_b, cs_assert_stb => cs_as_b,
                  cs_deassert_stb => cs_de_b,
                  miso_val => miso_val_b,
                  oe => oe_b_slow, miso_pad => pad_b_slow,
                  gap_seen => gap_seen_b, gap_short => gap_short_b,
                  rel_cycles => rel_cycles_b, clr_flags => '0');

    -- B as a FAST peer: it drives the instant its select pin falls, with no
    -- synchroniser at all. Not a straw man -- a device clocked on SCLK, or one where
    -- the select pin reaches the output enable combinationally, behaves like this.
    -- Only one flavour drives at a time, and the net is resolved so a genuine
    -- conflict shows as 'X'.
    --
    -- Written as a flat selection rather than a nested conditional expression:
    -- VHDL's `when ... else` is not an expression, so it cannot appear inside one.
    pad_b_fast <= miso_val_b when (b_is_fast and cs_b_pin = '0') else 'Z';
    miso_bus   <= pad_b_fast when b_is_fast else pad_b_slow;

    monitor : process (clk)
    begin
        if rising_edge(clk) then
            if clr_stats = '1' then
                contentions   <= 0;
                driven_cycles <= 0;
                z_cycles      <= 0;
            elsif rst_n = '1' then
                if miso_bus = 'X' then
                    contentions <= contentions + 1;
                elsif miso_bus = 'Z' then
                    z_cycles <= z_cycles + 1;
                else
                    driven_cycles <= driven_cycles + 1;
                end if;
                if oe_a = '1' and cs_a_pin = '1' then
                    a_driving_deselected <= a_driving_deselected + 1;
                end if;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;
        variable boundary : integer;
        variable found : boolean;

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

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

        -- A transaction to A followed, after `gap` cycles of both selects high, by a
        -- transaction to B. `gap` is the master's t_CSD in system clocks.
        procedure a_then_b(gap : natural; nbits : natural; half : natural) is
        begin
            cs_a_pin <= '1'; cs_b_pin <= '1'; sclk_pin <= '0';
            adv(10);
            cs_a_pin <= '0';
            adv(4);
            for i in 0 to nbits * 2 - 1 loop
                if (i mod 2) = 0 then sclk_pin <= '1'; else sclk_pin <= '0'; end if;
                adv(half);
            end loop;
            adv(4);
            cs_a_pin <= '1';
            if gap > 0 then adv(gap); end if;
            cs_b_pin <= '0';
            adv(4);
            for i in 0 to nbits * 2 - 1 loop
                if (i mod 2) = 0 then sclk_pin <= '1'; else sclk_pin <= '0'; end if;
                adv(half);
            end loop;
            adv(4);
            cs_b_pin <= '1';
            adv(10);
        end procedure;
    begin
        adv(3);
        -- 1. RESET RELEASES THE PIN.
        if oe_a /= '0' then
            report "  FAIL: the enable was asserted while in reset";
            errs := errs + 1;
        end if;
        if miso_bus /= 'Z' then
            report "  FAIL: the bus was driven while both devices were in reset";
            errs := errs + 1;
        end if;
        report "  in reset: the enable is low and the shared bus is released";
        rst_n <= '1';
        adv(4);

        -- 2. DESELECTED MEANS RELEASED, and the latency is published.
        if oe_a /= '0' then
            report "  FAIL: deselected, the enable is still asserted";
            errs := errs + 1;
        end if;
        cs_a_pin <= '0';
        adv(6);
        if oe_a /= '1' then
            report "  FAIL: selected, the enable never asserted";
            errs := errs + 1;
        end if;
        cs_a_pin <= '1';
        adv(6);
        if oe_a /= '0' then
            report "  FAIL: deselected again, the enable did not drop";
            errs := errs + 1;
        end if;
        if to_integer(rel_cycles) /= 1 then
            report "  FAIL: the published release latency is " &
                   integer'image(to_integer(rel_cycles)) & ", expected 1";
            errs := errs + 1;
        end if;
        report "  the enable follows the synchronised select and publishes a release latency of " &
               integer'image(to_integer(rel_cycles)) &
               " cycle past it, so the pin-to-release time is " &
               integer'image(SYNC_N + to_integer(rel_cycles)) & " system clocks";

        -- 3. TWO IDENTICAL SLAVES NEVER CONTEND, at any gap.
        b_is_fast <= false;
        clear_bus_stats;
        for gap in 0 to 6 loop
            a_then_b(gap, 4, 4);
        end loop;
        if contentions /= 0 then
            report "  FAIL: two identical slaves contended on " &
                   integer'image(contentions) & " cycles";
            errs := errs + 1;
        end if;
        if driven_cycles = 0 then
            report "  FAIL: the bus was never driven -- the monitor proves nothing";
            errs := errs + 1;
        end if;
        report "  two identical oversampling slaves, gaps from 0 to 6: " &
               integer'image(driven_cycles) & " cycles driven, " &
               integer'image(z_cycles) &
               " released, and no contending cycle at any gap";

        -- 4. A FAST PEER. Now the gap matters, and the boundary is the number.
        b_is_fast <= true;
        boundary  := 99;
        found     := false;
        for gap in 0 to 6 loop
            clear_bus_stats;
            a_then_b(gap, 4, 4);
            report "    gap=" & integer'image(gap) & ": " &
                   integer'image(contentions) & " contending cycles";
            if contentions = 0 and gap < boundary then boundary := gap; end if;
            if contentions /= 0 then found := true; end if;
        end loop;
        if not found then
            report "  FAIL: a zero-latency peer never contended -- the experiment proves nothing";
            errs := errs + 1;
        end if;
        if boundary /= SYNC_N + 1 then
            report "  FAIL: the bus became clean at a gap of " &
                   integer'image(boundary) & ", expected " &
                   integer'image(SYNC_N + 1);
            errs := errs + 1;
        end if;
        report "  a zero-latency peer contends for every gap below " &
               integer'image(boundary) &
               " and never at or above it, which is exactly this slave's pin-to-release time";

        -- 5. THE SLAVE'S OWN CS-HIGH TIME is measurable, and a neighbour's is not.
        b_is_fast <= false;
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        cs_a_pin <= '1'; adv(10);
        cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(10);
        cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(10);
        if gap_short = '1' then
            report "  FAIL: a generous CS-high time was reported as short";
            errs := errs + 1;
        end if;
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(1);
        cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(10);
        if gap_short /= '1' then
            report "  FAIL: a one-cycle CS-high time was not reported";
            errs := errs + 1;
        end if;
        report "  a generous CS-high time to this slave is accepted and a one-cycle one is reported, with the measured value published as " &
               integer'image(to_integer(gap_seen)) & " cycles";

        -- 6. THE RELEASE LATENCY IS VISIBLE AS DRIVEN-WHILE-DESELECTED CYCLES, and
        --    it is non-zero BY DESIGN.
        if a_driving_deselected = 0 then
            report "  FAIL: the enable never outlived the select pin -- that implies a combinational path from the pin to the output enable";
            errs := errs + 1;
        end if;
        report "  the enable outlived the deselected pin on " &
               integer'image(a_driving_deselected) &
               " cycles across the run, which is the registered release the chapter argues for rather than a fault";

        errors <= errs;
        if errs = 0 then
            report "PASS: the output enable is a single register fed by the synchronised select and by nothing else, so the pin-to-release latency is SYNC_N plus one system clocks and the enable is published rather than implied -- reset releases the shared bus asynchronously, two identical oversampling slaves never contend at any inter-transaction gap because the device being selected is as slow to start driving as the one being deselected is to stop, and a zero-latency neighbour contends for every gap below " & integer'image(SYNC_N + 1) & " and never at or above it, which is precisely this slave's release time and therefore the minimum CS-high time it requires of a master it shares a bus with -- the slave's own CS-high time is measurable and reported while a neighbour's is not, and the enable outliving the deselected pin is the registered behaviour the chapter argues for rather than a fault, because the alternative is a combinational path from an asynchronous pin to an output stage that no timing tool can constrain";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

7. Why a Verification Engineer Cares

Contention needs two instances, not one instance and an assertion. There is no property of a single slave that says "no contention". The property is about a wire with two drivers, so the bench must instantiate two things and check the wire — and the second thing has to be parameterisable between "identical to this slave" and "drives immediately", because those are the two cases with different answers.

Test 1 runs before reset is removed, which most benches structurally cannot do. A bench whose stimulus process begins with reset; #100; rst_n = 1; has already lost the ability to check anything about the reset state. The check has to be an assertion that is live from time zero, or a stimulus process that inspects the DUT before releasing reset.

Check the boundary of the flag's meaning, not just the flag. Test 5 asserts that gap_short does not fire for a peer-to-this-slave gap, which sounds like testing that a feature is missing. It is testing that the flag means what the datasheet will say it means: this device was reselected too quickly, not there was contention. A flag whose documented meaning is broader than its implementation is worse than no flag.

Tri-state needs a !== comparison, not !=. A bench comparing miso_pad != 1'bz silently succeeds for x as well. This is the one place in the module where four-state comparison is load-bearing, and in VHDL it is the one place 'Z' appears at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Properties for the output stage. The first is the one that matters, and it is
// about a wire rather than about a block -- which is why it needs the second
// driver that test 3 and 4 provide.

property p_never_drive_while_deselected;
    // The core obligation. Note it is stated against the REGISTERED enable and
    // the synchronised select, because those are the only two things that exist
    // in the same time frame.
    @(posedge clk) disable iff (!rst_n)
        !cs_active |=> !oe;
endproperty

property p_released_in_reset;
    // Not inside a disable iff, deliberately: this property is about the reset
    // state itself, so disabling it during reset would remove its only content.
    @(posedge clk)
        !rst_n |-> !oe;
endproperty

property p_pad_z_when_disabled;
    // The pad is high-impedance exactly when the enable is low. A four-state
    // comparison, because === is the only operator that distinguishes z from x.
    @(posedge clk) disable iff (!rst_n)
        !oe |-> (miso_pad === 1'bz);
endproperty

property p_pad_driven_when_enabled;
    // And the converse: enabled means driving a real value, never x. An x here
    // means miso_val is undefined, which is a transmit-path fault surfacing on
    // the one pin where it becomes a board problem.
    @(posedge clk) disable iff (!rst_n)
        oe |-> (miso_pad === 1'b0 || miso_pad === 1'b1);
endproperty

property p_gap_measured_at_assert;
    // The measurement is taken at the assert and held, not continuously
    // updated -- a continuously updated value reports the current idle time
    // rather than the gap that preceded the last transaction.
    @(posedge clk) disable iff (!rst_n)
        (!cs_assert_stb && !clr_flags) |=> $stable(gap_seen);
endproperty
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Coverage. The axis is the GAP, and the second axis is the peer -- because the
// requirement is a function of the neighbour and a suite with one peer type has
// tested one of the two answers.

covergroup cg_oe @(posedge clk iff cs_assert_stb);
    option.per_instance = 1;

    gap: coverpoint gap_seen {
        bins one     = {1};
        bins below   = {2};                   // below SYNC_N + 1 at SYNC_N = 2
        bins exact   = {3};                   // exactly SYNC_N + 1
        bins margin  = {4, 5};
        bins roomy   = {[6:$]};
    }

    // The peer's assert latency is what makes the requirement bite, so it is a
    // first-class coverage axis rather than a testbench detail.
    peer: coverpoint peer_assert_latency {
        bins immediate   = {0};               // an ASIC, or SCLK-clocked
        bins one_cycle   = {1};
        bins matched     = {[2:3]};           // another oversampling slave
        bins slower      = {[4:$]};
    }

    // Whether contention actually occurred, observed on the wire by the bench.
    // Crossed with the gap, this is the calibration: the boundary must fall at
    // exactly one place.
    contended: coverpoint saw_both_driving { bins no = {0}; bins yes = {1}; }

    x_gap_peer:      cross gap, peer;
    x_gap_contended: cross gap, contended;

endgroup

8. Why an FPGA or ASIC Engineer Cares

The tri-state is an I/O-buffer primitive, and where it lives matters. On an FPGA the oe signal drives the T input of an OBUFT, and the tool will infer that from the conditional assignment — but only if the conditional assignment is at the top level of the design, driving a port. A tri-state buried three levels down gets synthesised as a mux with a constant, or rejected, depending on the tool. That is why this block is instantiated at the top of Chapter 14.10's hierarchy and its output goes straight to a port.

Internal tri-state does not exist on most modern FPGAs. If a design needs the MISO value internally as well as on the pin — for a loopback test, say — it must take miso_val and oe separately rather than reading back the pad. Reading the pad works in simulation and infers a bidirectional buffer with a feedback path in synthesis, which is a different circuit.

oe should have its own ASYNC_REG treatment even though it is not a synchroniser. It is fed by cs_active, which is the output of one, and the register that gates a shared bus is worth placing close to the pad and worth keeping out of a retiming pass. A tool that decides to retime oe a cycle later has lengthened the release latency without telling anyone, and the effect is a contention window on a board with a fast peer.

The release latency is a number for the datasheet, and it is SYNC_N + 1 system clocks — not nanoseconds. That is an awkward thing to publish, because it is a function of the customer's clock, so the honest datasheet entry is a formula plus a worked example at a nominal frequency. A datasheet that states a fixed nanosecond figure has quietly assumed a system clock.

9. Failure Signature — A Two-Slave Board Where One Slave Corrupts The Other

The symptom:

"Each device works perfectly when it is the only one being addressed. With both in use, the first byte read from device B is wrong whenever the previous transaction addressed device A. Swapping the order makes the problem move."

What is happening: device A is this slave, with a release latency of SYNC_N + 1. Device B drives immediately. The master's gap is one cycle. For SYNC_N cycles after device B is selected, both output stages are driving MISO, and the value the master reads during that window is neither device's — so B's first bit is wrong, and only when A was the previous target.

Why it moves when the order is swapped: because the requirement is asymmetric. A-then-B contends; B-then-A does not, because B releases immediately and A is slow to start. A fault that appears in one ordering and not the other is almost always a release-versus-assert latency mismatch, and that asymmetry is the fastest way to recognise it.

How to confirm without an oscilloscope: increase the master's inter-transaction gap and see whether it goes away at exactly SYNC_N + 1. If it does, the fault is this, and the fix is the gap rather than either device. gap_seen on device A will not report the problem — §4 explains why, and knowing that in advance saves an afternoon of doubting the instrument.

10. Common Misconceptions

"The output enable should release as fast as possible, so derive it from the pin." That makes a glitch on the select line drive the bus — the exact failure the enable exists to prevent — and creates a pin-to-pin combinational path that static timing analysis cannot see. The register is not a compromise; it is the mechanism.

"Release latency is a slave-internal concern." It is a board-level concern, because it interacts with the next device's assert latency. The same slave is correct with a one-cycle gap beside a copy of itself and incorrect beside an ASIC.

"If the slave cannot detect contention it cannot be held responsible for it." The slave is responsible for publishing its release latency, which is what lets an integrator compute the gap. That is the whole of what it can do, and a design that does not document the number has failed at the part it could have done.

"gap_short clear means there was no contention." It means this device was not reselected too quickly. Contention between this device and a neighbour is invisible to both of them.

"Two slaves on a bus always need a gap." Two identical oversampling slaves need none, at any gap, because the subtraction in §3 is zero. The requirement appears only when the devices differ.

"Reset releasing the pin is obvious and needs no test." It needs a test that runs before reset is removed, which most bench structures cannot do. And the failure it prevents is not a slave failure — it is every other device on the bus becoming unreadable, which is the largest blast radius of anything in this module.

11. Reason It Through

Q. Two instances of this slave, SYNC_N = 2, and the master uses a gap of one cycle. Is there contention?

No, at any gap. The slave being deselected stops driving SYNC_N + 1 = 3 cycles after its select rises; the slave being selected starts driving 3 cycles after its select falls. If the second select falls one cycle after the first rises, the second starts driving at cycle 4 and the first stopped at cycle 3. The subtraction in §3 is zero and the gap is irrelevant — which is why test 3 sweeps the gap from 1 and finds no contention anywhere.

Q. The same board, but device B is an ASIC that drives within a nanosecond of its select falling. What is the minimum gap, and what happens at one less than it?

SYNC_N + 1 = 3 cycles. At a gap of 2, device B starts driving essentially immediately while device A is still driving for one more cycle — one cycle of contention, during which the master reads a value that is neither device's. One cycle is enough to corrupt B's first bit under CPHA=0, because that is exactly when the master samples it.

Q. Why is it correct for the enable to be derived from the ungated chip select, when Chapter 14.8 gates the select for everything else?

Because the gate exists to keep a transaction begun during reset out of the receive path, and it has nothing to say about the output stage. A slave that was stranded by a reset mid-transaction must still release MISO properly and must still not drive while deselected — those obligations do not depend on whether the transaction is being processed. Wiring the enable to the gated select would mean a stranded slave released MISO late, which is the one failure the enable exists to prevent, in exchange for nothing.

Q. A reviewer proposes publishing oe as an output so that a bus monitor can observe contention. Is that reasonable?

Yes, and it is what Chapter 14.10 does internally. Contention is not observable from the pins — the wire carries one value whichever devices are driving it — so the only way a monitor can see it is by watching each device's enable. Publishing oe costs a pin on a test build or nothing at all on an internal net, and it is the difference between a bench that can check the property and one that cannot.

Q. MISO is pulled up by a resistor on the board, and someone argues that makes contention harmless. What is wrong with that?

The pull-up defines the wire's value when nobody drives it, which is a different situation. When two output stages drive opposite values, the pull-up is irrelevant — it is a weak device in parallel with two strong ones, and the current flows between the two drivers rather than through it. What the pull-up does help with is the released state, where it prevents the input of every listening device from floating; that is a real and separate reason to have one.

12. Understanding Check

13. Summary

MISO is shared, so the slave has two asymmetric obligations. Asserting late costs a bit and is measurable. Releasing late puts two output stages on one wire, and that is a hardware problem rather than a data one — which is why the release gets the attention.

The enable is one register fed by the synchronised select, giving a release latency of SYNC_N + 1. Deriving it combinationally from the pin would make a glitch on chip select drive the bus, create a pin-to-pin path invisible to timing analysis, and buy cycles the specified gap already covers.

Reset must release the pin asynchronously. A slave driving during reset makes every other device on the bus unreadable — the largest blast radius in the module — and a board in reset may have no clock.

The third requirement on the master is GAP >= (release latency) - (the neighbour's assert latency), and it is the only one that is not a property of this slave alone. Two identical oversampling slaves never contend at any gap. A neighbour that drives immediately makes the requirement SYNC_N + 1. A master cannot compute this, which is why a datasheet specifies a minimum CS-high time and the master's gap is programmable.

Contention is invisible to the slave — the third such thing in the module, after the lost edge and the late first bit. What the slave can measure is its own CS-high time, so gap_short means this device was reselected too quickly, not there was no contention, and the bench checks that limit explicitly.

The enable uses the ungated select, because the reset gate exists to protect the receive path and using it here would delay the release for nothing.

For verification: contention needs two instances and a configurable peer latency, because the answer changes with the neighbour; the reset check must run before reset is removed; and tri-state comparisons need !== rather than !=.

For implementation: the tri-state must be at the top level driving a port for OBUFT inference; internal tri-state does not exist on modern FPGAs, so a loopback must take miso_val and oe separately; and oe is worth keeping out of a retiming pass, because a tool that moves it a cycle later has silently lengthened the release.

14. What Comes Next

The slave receives, transmits, and releases the bus. It has been assuming all along that it and the master agree about which edge does what.

Chapter 14.6 — CPOL/CPHA Handling in a Slave builds the mode logic, and it is the chapter where the module's one genuinely wrong first answer had to be replaced. A polarity mismatch is caught from a level before a single bit moves. A phase mismatch is not caught by counting edges — a mismatched pair exchanges a whole number of frames, every time — and the observation that does catch it is physical rather than arithmetical. A bit-order mismatch is not caught at all, and the chapter argues that reporting nothing is the correct engineering answer.

Continue learning