Skip to content
VLSI Mentor

SPI · Module 8

Bus Turnaround and the Contention Window

The overlap between one driver releasing and the next asserting: how long the gap really is once pad turn-off counts, why re-selecting the same device needs none, and the guard that enforces dead time only on a handover.

Chapter 8.3 made every slave let go faster than any slave takes hold, which guarantees a gap. This chapter asks how wide that gap actually is.

Device A has been deselected and device B is about to be selected. Between those two events there is an interval in which the bus should have no driver. How long is it, and what makes it shorter than it looks?

The answer involves a quantity that does not appear anywhere in the RTL, and a distinction — which selections need a turnaround and which do not — that most masters get wrong in the safe direction and some get wrong in the other.

1. The Window, Precisely

Call the moment device A's internal enable falls t₀ and the moment device B's driver becomes active t₁. The bus is safe if, and only if, A's output has reached high impedance before t₁.

Four intervals sit between them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   t₀  A's enable falls (RTL)
       + A's pad turn-off time          ← not in any RTL model
   ───────────────────────────────────
       A is genuinely off the net
       + the master's dead interval     ← the only part you control directly
   ───────────────────────────────────
       B's select asserts
       + B's enable latency (RTL)
       + B's pad turn-on time
   t₁  B is genuinely driving

Two of those four are analogue and appear in no simulation. That is the chapter's first point: the RTL measurement of Chapter 8.3 — one cycle to release, two to assert — is an upper bound on the digital part only, and the real window is narrower at the front and wider at the back than the digital picture suggests.

2. Why the Pad Is Slower Than the Register

A pad's turn-off is not a logic transition; it is a driver's output stage going high impedance and the net then settling to whatever a weak pull-up or the line's own capacitance decides.

The asymmetry matters. When a driver turns on, it pulls the net actively and the edge is fast. When it turns off, nothing pulls the net — it drifts to the pull-up's level at a rate set by R × C, and a few tens of kilohms against a few tens of picofarads is hundreds of nanoseconds.

That has a consequence worth stating plainly: releasing a net is slower than driving it, at the electrical level, even though it is faster at the logic level. Chapter 8.3's asymmetry buys you a cycle of digital margin; the pull-up's RC can spend considerably more than that.

If the incoming driver turns on before the net has settled, no damage occurs — the new driver simply wins, because it is active and the pull-up is weak. The dangerous overlap is two active drivers, which is Chapter 8.5. The net-settling time matters for a different reason: it determines when the master can trust the line, not whether the devices are fighting.

3. The Gap, Drawn

Neither driver, for a deliberate interval

10 cycles
Two chip selects and two output enables during a handover. Slave A's select deasserts and its output enable falls. After an interval with neither enable asserted, slave B's select asserts and its output enable rises.A lets goA lets goB takes overB takes overcs_aoe_acs_boe_bt0t1t2t3t4t5t6t7t8t9
Figure 1 — a handover between two slaves. Device A's select releases and its output enable follows; several cycles later device B is selected and its enable follows. The interval in which neither enable is asserted is the turnaround, and it exists because the master deliberately left it.

4. Only a Handover Needs a Turnaround

This is the distinction the §6 guard implements, and it is worth being precise about because it is a free performance gain.

Selecting a different device hands the bus over. One pad must stop and another must start, so the dead interval is genuinely required.

Re-selecting the same device hands the bus to nobody. The same pad drives before and after. There is no moment when two drivers could overlap, because there is only ever one driver involved — so no turnaround is needed at all.

That does not mean back-to-back frames to one device are unconstrained: Chapter 7.3 established a minimum deselect time, which is a device recovery requirement rather than a bus one. The two intervals have different causes, different values, and apply in different cases:

IntervalCauseApplies when
Deselect time (7.3)the device's internal resetevery frame boundary
Bus turnaround (this chapter)pad handover on a shared netonly when the next device differs

A master that applies the larger of the two unconditionally is correct and slow. One that distinguishes them pays each cost only where it is owed — which on a bus alternating between two devices is a meaningful saving, and on one hammering a single device is a large one.

5. The Ownership Handover

A block diagram showing three states of the MISO net during a handover. First slave A drives the net. Then neither slave drives and a pull-up holds the level. Finally slave B drives the net.Slave A drivesbefore the handoverNobody drivesturnaround — pull-up holdsSlave B drivesafter the handoverBoth drivewhat the gap preventsA releasesB assertsif too earlycontention12
Figure 2 — the same handover as bus ownership. During the turnaround the net has no active driver and is held at its idle level by the pull-up alone. That state is not a failure; it is the interval the whole arrangement depends on.

6. Building the Turnaround Guard — Three HDLs

The circuit

Circuit. A three-state machine with a counter, sitting above Chapter 8.1's select generator and owning sel_en and sel_idx.

State. Ready, active, or counting out a turnaround; plus the elapsed count and the previously-selected index.

Datapath. None.

Control. The handover term — have_last && (req_idx != last_idx) — decides whether a turnaround is required at all, and required is min_turnaround or zero accordingly. That single expression is §4 in logic.

Clock and reset. System clock; asynchronous active-low reset to ready with nothing selected and the counter saturated, so the first frame after reset is not delayed by a turnaround from a frame that never happened.

Enables. Deselection happens on entry to the turnaround state, not on exit. Nothing may be selected while the counter runs — that is what makes it dead time rather than a delay.

Timing. The state machine counts before considering the request, so a pending request cannot shorten the interval.

Synthesis. Two state bits, a TA_W counter and comparator, and an index register. The comparator's second input is required, which is a multiplexer output rather than a constant — the runtime-configurability cost seen throughout this track.

Limitations. It enforces the bus turnaround only. The per-device deselect time of Chapter 7.3 is a separate requirement and, on a design needing both, the two guards compose — or one guard takes the maximum of the two applicable values.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround.sv — dead time when, and only when, the bus changes hands
// spi_bus_turnaround.sv — dead time when MISO changes hands.
//
// Chapter 7.3 enforced a gap between frames to the SAME device, because that
// device needs time to recover. This is a different requirement with a
// different cause: when the next frame targets a DIFFERENT slave, the bus
// itself changes hands, and the outgoing slave's pad must reach high
// impedance before the incoming slave's pad starts driving.
//
// The distinction matters because the two intervals are not the same number
// and are not needed in the same cases. Re-selecting the same device hands
// the bus to nobody -- the same pad drives before and after -- so no
// turnaround is required at all.
module spi_bus_turnaround #(
    parameter int IDX_W = 2,
    parameter int TA_W  = 8
) (
    input  logic             clk,
    input  logic             rst_n,
    input  logic             req,             // level: a frame is wanted
    input  logic [IDX_W-1:0] req_idx,         // which slave
    input  logic [TA_W-1:0]  min_turnaround,  // dead cycles when handing over
    input  logic             frame_done,      // pulse: frame content complete

    output logic             sel_en,          // to the CS generator (Chapter 8.1)
    output logic [IDX_W-1:0] sel_idx,
    output logic             ta_wait,         // level: held off for turnaround
    output logic             busy
);
    typedef enum logic [1:0] { ST_IDLE, ST_ACTIVE, ST_TURN } state_t;

    state_t          state;
    logic [TA_W-1:0] ta_cnt;
    logic [IDX_W-1:0] last_idx;
    logic            have_last;

    // Turnaround is required only when the bus actually changes hands.
    logic handover;
    assign handover = have_last && (req_idx != last_idx);

    logic [TA_W-1:0] required;
    assign required = handover ? min_turnaround : '0;

    logic ta_ok;
    assign ta_ok = (ta_cnt >= required);

    assign ta_wait = req && (state == ST_TURN) && !ta_ok;
    assign busy    = (state == ST_ACTIVE);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state     <= ST_IDLE;
            sel_en    <= 1'b0;
            sel_idx   <= '0;
            ta_cnt    <= '1;          // fully elapsed: the first frame is not delayed
            last_idx  <= '0;
            have_last <= 1'b0;
        end else begin
            case (state)
                ST_IDLE: if (req) begin
                    sel_en  <= 1'b1;
                    sel_idx <= req_idx;
                    state   <= ST_ACTIVE;
                end

                ST_ACTIVE: if (frame_done) begin
                    // Deselect FIRST. Nothing may be selected during the
                    // turnaround -- that is what makes it dead time.
                    sel_en    <= 1'b0;
                    last_idx  <= sel_idx;
                    have_last <= 1'b1;
                    ta_cnt    <= '0;
                    state     <= ST_TURN;
                end

                ST_TURN: begin
                    if (!ta_ok) begin
                        ta_cnt <= ta_cnt + 1'b1;
                    end else if (req) begin
                        sel_en  <= 1'b1;
                        sel_idx <= req_idx;
                        state   <= ST_ACTIVE;
                    end else begin
                        state <= ST_IDLE;
                    end
                end

                default: state <= ST_IDLE;
            endcase
        end
    end
endmodule

Note where have_last matters. Without it, the very first frame after reset would compare req_idx against an uninitialised last_idx and might spuriously demand a turnaround — or, worse, spuriously skip one later. Tracking whether a previous selection exists makes the first frame unambiguous.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround_tb.sv — measuring the dead interval in both cases
// spi_bus_turnaround_tb.sv — measure the dead interval for a handover and
// for a same-slave re-selection, and show they differ.
`timescale 1ns/1ps
module spi_bus_turnaround_tb;
    logic clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    localparam int IW = 2, TW = 8;
    logic req = 0, frame_done = 0;
    logic [IW-1:0] req_idx = '0;
    logic [TW-1:0] min_turnaround = 8'd6;
    logic sel_en, ta_wait, busy;
    logic [IW-1:0] sel_idx;

    spi_bus_turnaround #(.IDX_W(IW), .TA_W(TW)) dut (
        .clk, .rst_n, .req, .req_idx, .min_turnaround, .frame_done,
        .sel_en, .sel_idx, .ta_wait, .busy);

    int errors = 0, frames = 0;
    task automatic chk(input string what, input int g, input int e);
        if (g !== e) begin $display("FAIL %s: got %0d exp %0d", what, g, e); errors++; end
    endtask

    // Measure how long NOTHING is selected between consecutive frames.
    int  dead_len = 0, measured_dead = -1;
    logic sel_en_q = 1'b0, measuring = 1'b0;
    always @(posedge clk) if (rst_n) begin
        sel_en_q <= sel_en;
        if (sel_en && !sel_en_q) frames++;
        if (!sel_en && sel_en_q) begin        // deselected: start measuring
            dead_len  <= 1;
            measuring <= 1'b1;
        end else if (sel_en && !sel_en_q) begin
            measured_dead <= dead_len;
            measuring     <= 1'b0;
        end else if (measuring) begin
            dead_len <= dead_len + 1;
        end
    end

    task automatic run_frame();
        repeat (4) @(negedge clk);
        frame_done = 1; @(negedge clk); frame_done = 0; @(negedge clk);
    endtask

    task automatic wait_sel();
        while (!sel_en) @(negedge clk);
        repeat (2) @(negedge clk);
    endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
        chk("idle: nothing selected", sel_en, 0);

        // --- frame 1 on slave 0 ---
        req = 1; req_idx = 2'd0;
        wait_sel();
        chk("frame 1 open",  frames,  1);
        chk("slave 0",       sel_idx, 0);

        // --- SAME slave again: no handover, so no turnaround required ---
        run_frame();
        wait_sel();
        chk("frame 2 open",           frames,  2);
        chk("still slave 0",          sel_idx, 0);
        $display("  dead time, same slave:  %0d cycles", measured_dead);
        if (measured_dead > 2) begin
            $display("FAIL: same-slave re-selection should not wait for turnaround (got %0d)",
                     measured_dead);
            errors++;
        end

        // --- DIFFERENT slave: the bus changes hands, so dead time applies.
        //     The next target is presented before the frame completes, which
        //     is what a real master does -- the FSM decides whether a
        //     handover is happening at the instant it deselects. ---
        req_idx = 2'd2;
        run_frame();
        repeat (2) @(negedge clk);
        chk("held for turnaround", ta_wait, 1);
        wait_sel();
        chk("frame 3 open", frames,  3);
        chk("slave 2",      sel_idx, 2);
        $display("  dead time, handover:    %0d cycles", measured_dead);
        if (measured_dead < 6) begin
            $display("FAIL: handover dead time was %0d, minimum is 6", measured_dead);
            errors++;
        end

        // --- back to slave 0: another handover ---
        req_idx = 2'd0;
        run_frame();
        wait_sel();
        chk("frame 4 open", frames,  4);
        chk("slave 0 again", sel_idx, 0);
        if (measured_dead < 6) begin
            $display("FAIL: second handover dead time was %0d, minimum is 6", measured_dead);
            errors++;
        end

        req = 0;
        run_frame();
        repeat (10) @(negedge clk);
        chk("finally idle",  sel_en, 0);
        chk("no extra frame", frames, 4);

        if (errors == 0)
            $display("PASS: a handover to a different slave is held for the full turnaround with nothing selected, while re-selecting the same slave needs none");
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

    initial begin #500000; $display("FAIL: watchdog timeout"); $finish; end
endmodule

The testbench measures the interval during which nothing is selected and compares it against the requirement — separately for a same-slave re-selection and for a genuine handover. It reports 1 cycle for the former and 7 for the latter against a minimum of 6, which is §4's distinction demonstrated rather than asserted.

One subtlety the testbench encodes: the next target is presented before the current frame completes, because the guard decides whether a handover is occurring at the instant it deselects. A master that only decides afterwards cannot be given the information in time — which is a real constraint on how a controller's request interface must be shaped.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround.v — the same guard in Verilog-2001
// spi_bus_turnaround.v — the same handover guard in Verilog-2001.
module spi_bus_turnaround #(
    parameter IDX_W = 2,
    parameter TA_W  = 8
) (
    input  wire             clk,
    input  wire             rst_n,
    input  wire             req,
    input  wire [IDX_W-1:0] req_idx,
    input  wire [TA_W-1:0]  min_turnaround,
    input  wire             frame_done,
    output reg              sel_en,
    output reg  [IDX_W-1:0] sel_idx,
    output wire             ta_wait,
    output wire             busy
);
    localparam ST_IDLE   = 2'd0,
               ST_ACTIVE = 2'd1,
               ST_TURN   = 2'd2;

    reg [1:0]       state;
    reg [TA_W-1:0]  ta_cnt;
    reg [IDX_W-1:0] last_idx;
    reg             have_last;

    // Turnaround is required only when the bus actually changes hands.
    wire handover = have_last && (req_idx != last_idx);
    wire [TA_W-1:0] required = handover ? min_turnaround : {TA_W{1'b0}};
    wire ta_ok = (ta_cnt >= required);

    assign ta_wait = req && (state == ST_TURN) && !ta_ok;
    assign busy    = (state == ST_ACTIVE);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state     <= ST_IDLE;
            sel_en    <= 1'b0;
            sel_idx   <= {IDX_W{1'b0}};
            ta_cnt    <= {TA_W{1'b1}};    // fully elapsed
            last_idx  <= {IDX_W{1'b0}};
            have_last <= 1'b0;
        end else begin
            case (state)
                ST_IDLE: if (req) begin
                    sel_en  <= 1'b1;
                    sel_idx <= req_idx;
                    state   <= ST_ACTIVE;
                end

                ST_ACTIVE: if (frame_done) begin
                    // Deselect FIRST: nothing may be selected during the
                    // turnaround, which is what makes it dead time.
                    sel_en    <= 1'b0;
                    last_idx  <= sel_idx;
                    have_last <= 1'b1;
                    ta_cnt    <= {TA_W{1'b0}};
                    state     <= ST_TURN;
                end

                ST_TURN: begin
                    if (!ta_ok) begin
                        ta_cnt <= ta_cnt + 1'b1;
                    end else if (req) begin
                        sel_en  <= 1'b1;
                        sel_idx <= req_idx;
                        state   <= ST_ACTIVE;
                    end else begin
                        state <= ST_IDLE;
                    end
                end

                default: state <= ST_IDLE;
            endcase
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround_tb.v — the same measurements in Verilog-2001
// spi_bus_turnaround_tb.v — the same measurements in Verilog-2001.
`timescale 1ns/1ps
module spi_bus_turnaround_tb;
    reg clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    parameter IW = 2, TW = 8;
    reg req = 0, frame_done = 0;
    reg [IW-1:0] req_idx = 0;
    reg [TW-1:0] min_turnaround = 8'd6;
    wire sel_en, ta_wait, busy;
    wire [IW-1:0] sel_idx;

    spi_bus_turnaround #(.IDX_W(IW), .TA_W(TW)) dut (
        .clk(clk), .rst_n(rst_n), .req(req), .req_idx(req_idx),
        .min_turnaround(min_turnaround), .frame_done(frame_done),
        .sel_en(sel_en), .sel_idx(sel_idx), .ta_wait(ta_wait), .busy(busy));

    integer errors = 0, frames = 0, dead_len = 0, measured_dead = -1;
    reg sel_en_q = 1'b0, measuring = 1'b0;

    task chk;
        input [80*8-1:0] what;
        input [31:0] g, e;
        begin
            if (g !== e) begin
                $display("FAIL %0s: got %0d exp %0d", what, g, e);
                errors = errors + 1;
            end
        end
    endtask

    always @(posedge clk) if (rst_n) begin
        sel_en_q <= sel_en;
        if (sel_en && !sel_en_q) frames = frames + 1;
        if (!sel_en && sel_en_q) begin
            dead_len  <= 1;
            measuring <= 1'b1;
        end else if (sel_en && !sel_en_q) begin
            measured_dead <= dead_len;
            measuring     <= 1'b0;
        end else if (measuring) begin
            dead_len <= dead_len + 1;
        end
    end

    task run_frame;
        begin
            repeat (4) @(negedge clk);
            frame_done = 1; @(negedge clk); frame_done = 0; @(negedge clk);
        end
    endtask

    task wait_sel;
        begin
            while (!sel_en) @(negedge clk);
            repeat (2) @(negedge clk);
        end
    endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
        chk("idle: nothing selected", sel_en, 0);

        req = 1; req_idx = 2'd0;
        wait_sel;
        chk("frame 1 open", frames,  1);
        chk("slave 0",      sel_idx, 0);

        // Same slave: no handover, so no turnaround.
        run_frame;
        wait_sel;
        chk("frame 2 open",  frames,  2);
        chk("still slave 0", sel_idx, 0);
        $display("  dead time, same slave:  %0d cycles", measured_dead);
        if (measured_dead > 2) begin
            $display("FAIL: same-slave re-selection should not wait (got %0d)", measured_dead);
            errors = errors + 1;
        end

        // Different slave: the bus changes hands.
        req_idx = 2'd2;
        run_frame;
        repeat (2) @(negedge clk);
        chk("held for turnaround", ta_wait, 1);
        wait_sel;
        chk("frame 3 open", frames,  3);
        chk("slave 2",      sel_idx, 2);
        $display("  dead time, handover:    %0d cycles", measured_dead);
        if (measured_dead < 6) begin
            $display("FAIL: handover dead time was %0d, minimum is 6", measured_dead);
            errors = errors + 1;
        end

        req_idx = 2'd0;
        run_frame;
        wait_sel;
        chk("frame 4 open",  frames,  4);
        chk("slave 0 again", sel_idx, 0);
        if (measured_dead < 6) begin
            $display("FAIL: second handover dead time was %0d, minimum is 6", measured_dead);
            errors = errors + 1;
        end

        req = 0;
        run_frame;
        repeat (10) @(negedge clk);
        chk("finally idle",   sel_en, 0);
        chk("no extra frame", frames, 4);

        if (errors == 0)
            $display("PASS: a handover to a different slave is held for the full turnaround with nothing selected, while re-selecting the same slave needs none");
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

    initial begin #500000; $display("FAIL: watchdog timeout"); $finish; end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround.vhd — the same guard in VHDL
-- spi_bus_turnaround.vhd — the same handover guard in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_bus_turnaround is
    generic (
        IDX_W : positive := 2;
        TA_W  : positive := 8
    );
    port (
        clk            : in  std_logic;
        rst_n          : in  std_logic;
        req            : in  std_logic;                        -- level
        req_idx        : in  unsigned(IDX_W - 1 downto 0);
        min_turnaround : in  unsigned(TA_W - 1 downto 0);
        frame_done     : in  std_logic;                        -- pulse
        sel_en         : out std_logic;                        -- to the CS generator
        sel_idx        : out unsigned(IDX_W - 1 downto 0);
        ta_wait        : out std_logic;                        -- level
        busy           : out std_logic
    );
end entity spi_bus_turnaround;

architecture rtl of spi_bus_turnaround is
    type state_t is (ST_IDLE, ST_ACTIVE, ST_TURN);
    signal state : state_t;

    signal ta_cnt    : unsigned(TA_W - 1 downto 0);
    signal last_idx  : unsigned(IDX_W - 1 downto 0);
    signal have_last : std_logic;
    signal sel_idx_r : unsigned(IDX_W - 1 downto 0);

    signal handover : std_logic;
    signal required : unsigned(TA_W - 1 downto 0);
    signal ta_ok    : std_logic;
begin

    -- Turnaround is required only when the bus actually changes hands.
    handover <= '1' when have_last = '1' and req_idx /= last_idx else '0';
    required <= min_turnaround when handover = '1' else (others => '0');
    ta_ok    <= '1' when ta_cnt >= required else '0';

    ta_wait <= '1' when req = '1' and state = ST_TURN and ta_ok = '0' else '0';
    busy    <= '1' when state = ST_ACTIVE else '0';
    sel_idx <= sel_idx_r;

    process (clk, rst_n) is
    begin
        if rst_n = '0' then
            state     <= ST_IDLE;
            sel_en    <= '0';
            sel_idx_r <= (others => '0');
            ta_cnt    <= (others => '1');    -- fully elapsed
            last_idx  <= (others => '0');
            have_last <= '0';
        elsif rising_edge(clk) then
            case state is
                when ST_IDLE =>
                    if req = '1' then
                        sel_en    <= '1';
                        sel_idx_r <= req_idx;
                        state     <= ST_ACTIVE;
                    end if;

                when ST_ACTIVE =>
                    if frame_done = '1' then
                        -- Deselect FIRST: nothing may be selected during the
                        -- turnaround, which is what makes it dead time.
                        sel_en    <= '0';
                        last_idx  <= sel_idx_r;
                        have_last <= '1';
                        ta_cnt    <= (others => '0');
                        state     <= ST_TURN;
                    end if;

                when ST_TURN =>
                    if ta_ok = '0' then
                        ta_cnt <= ta_cnt + 1;
                    elsif req = '1' then
                        sel_en    <= '1';
                        sel_idx_r <= req_idx;
                        state     <= ST_ACTIVE;
                    else
                        state <= ST_IDLE;
                    end if;
            end case;
        end if;
    end process;

end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround_tb.vhd — the same measurements in VHDL
-- spi_bus_turnaround_tb.vhd — the same measurements in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_bus_turnaround_tb is
end entity spi_bus_turnaround_tb;

architecture tb of spi_bus_turnaround_tb is
    constant IW : positive := 2;
    constant TW : positive := 8;

    signal clk            : std_logic := '0';
    signal rst_n          : std_logic := '0';
    signal req            : std_logic := '0';
    signal frame_done     : std_logic := '0';
    signal req_idx        : unsigned(IW - 1 downto 0) := (others => '0');
    signal min_turnaround : unsigned(TW - 1 downto 0) := to_unsigned(6, TW);
    signal halt           : boolean := false;

    signal sel_en  : std_logic;
    signal sel_idx : unsigned(IW - 1 downto 0);
    signal ta_wait : std_logic;
    signal busy    : std_logic;

    signal errors        : natural := 0;
    signal frames        : natural := 0;
    signal measured_dead : natural := 0;
begin

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

    dut : entity work.spi_bus_turnaround
        generic map (IDX_W => IW, TA_W => TW)
        port map (clk => clk, rst_n => rst_n, req => req, req_idx => req_idx,
                  min_turnaround => min_turnaround, frame_done => frame_done,
                  sel_en => sel_en, sel_idx => sel_idx, ta_wait => ta_wait,
                  busy => busy);

    observe : process (clk) is
        variable sel_q     : std_logic := '0';
        variable dead_len  : natural   := 0;
        variable measuring : boolean   := false;
    begin
        if rising_edge(clk) and rst_n = '1' then
            if sel_en = '1' and sel_q = '0' then
                frames <= frames + 1;
                if measuring then
                    measured_dead <= dead_len;
                    measuring     := false;
                end if;
            elsif sel_en = '0' and sel_q = '1' then
                dead_len  := 1;
                measuring := true;
            elsif measuring then
                dead_len := dead_len + 1;
            end if;
            sel_q := sel_en;
        end if;
    end process;

    stim : process is
        procedure chk_b (what : string; g, e : std_logic) is
        begin
            if g /= e then
                report "FAIL " & what severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure chk_n (what : string; g, e : natural) is
        begin
            if g /= e then
                report "FAIL " & what & ": got " & integer'image(g)
                    & " exp " & integer'image(e) severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure run_frame is
        begin
            for i in 0 to 3 loop wait until falling_edge(clk); end loop;
            frame_done <= '1';
            wait until falling_edge(clk);
            frame_done <= '0';
            wait until falling_edge(clk);
        end procedure;

        procedure wait_sel is
        begin
            while sel_en = '0' loop
                wait until falling_edge(clk);
            end loop;
            for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        end procedure;
    begin
        for i in 0 to 2 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        wait until falling_edge(clk);
        chk_b("idle: nothing selected", sel_en, '0');

        req <= '1'; req_idx <= to_unsigned(0, IW);
        wait_sel;
        chk_n("frame 1 open", frames, 1);
        chk_n("slave 0",      to_integer(sel_idx), 0);

        -- Same slave: no handover, so no turnaround.
        run_frame;
        wait_sel;
        chk_n("frame 2 open",  frames, 2);
        chk_n("still slave 0", to_integer(sel_idx), 0);
        report "  dead time, same slave:  " & integer'image(measured_dead)
             & " cycles" severity note;
        if measured_dead > 2 then
            report "FAIL: same-slave re-selection should not wait" severity error;
            errors <= errors + 1;
        end if;

        -- Different slave: the bus changes hands.
        req_idx <= to_unsigned(2, IW);
        run_frame;
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        chk_b("held for turnaround", ta_wait, '1');
        wait_sel;
        chk_n("frame 3 open", frames, 3);
        chk_n("slave 2",      to_integer(sel_idx), 2);
        report "  dead time, handover:    " & integer'image(measured_dead)
             & " cycles" severity note;
        if measured_dead < 6 then
            report "FAIL: handover dead time too short" severity error;
            errors <= errors + 1;
        end if;

        req_idx <= to_unsigned(0, IW);
        run_frame;
        wait_sel;
        chk_n("frame 4 open",  frames, 4);
        chk_n("slave 0 again", to_integer(sel_idx), 0);
        if measured_dead < 6 then
            report "FAIL: second handover dead time too short" severity error;
            errors <= errors + 1;
        end if;

        req <= '0';
        run_frame;
        for i in 0 to 9 loop wait until falling_edge(clk); end loop;
        chk_b("finally idle",   sel_en, '0');
        chk_n("no extra frame", frames, 4);

        if errors = 0 then
            report "PASS: a handover to a different slave is held for the full "
                 & "turnaround with nothing selected, while re-selecting the same "
                 & "slave needs none" severity note;
        else
            report "FAILED with " & integer'image(errors) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

    watchdog : process is
    begin
        wait for 500 us;
        if not halt then
            report "FAIL: watchdog timeout" severity failure;
        end if;
        wait;
    end process;

end architecture tb;

Parity

All three implement the same machine: identical ports and generics, asynchronous active-low reset to ready with nothing selected and the counter saturated, deselection on entry to the turnaround state, counting before considering the request, and a requirement of min_turnaround only when the target index differs from the last. All three testbenches report the same measurements — 1 cycle for a same-slave re-selection, 7 for a handover against a minimum of 6.

7. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_bus_turnaround.sva — nothing selected, for long enough, when it matters
   // 1. THE property. Between deselecting one slave and selecting a
   //    DIFFERENT one, nothing may be selected for at least min_turnaround.
   property p_dead_interval;
       @(posedge clk) disable iff (!rst_n)
           ($fell(sel_en), prev_idx = sel_idx)
           |-> ##[1:$] ($rose(sel_en) ##0
                        ((sel_idx == prev_idx) || (dead_cycles >= min_turnaround)));
   endproperty
   a_dead_interval : assert property (p_dead_interval)
       else $error("bus handed over without sufficient dead time");

   // 2. Nothing is selected DURING the turnaround. A guard that merely
   //    delayed the next selection while leaving the previous one asserted
   //    would satisfy a naive timing check and provide no dead time at all.
   a_nothing_selected : assert property (
       @(posedge clk) disable iff (!rst_n) (state == ST_TURN) |-> !sel_en)
       else $error("a slave remained selected during the turnaround");

   // 3. Same-slave re-selection is not delayed. A correctness property in
   //    the performance direction: over-applying the turnaround is safe but
   //    wrong, and nothing else would catch it.
   a_no_needless_wait : assert property (
       @(posedge clk) disable iff (!rst_n)
           ($rose(sel_en) && (sel_idx == last_idx)) |-> (dead_cycles <= 2))
       else $error("turnaround applied to a same-slave re-selection");

Property 3 is unusual and worth keeping: it asserts that the design is not too conservative. Most assertions catch unsafe behaviour; this one catches a safe-but-wrong behaviour that would silently halve throughput on a bus alternating between devices, and that no functional test would ever report as a failure.

What these prove. That the digital dead interval exists, is long enough, and is applied only when needed. What they cannot prove is that min_turnaround covers the pad turn-off — §1's analogue term. A guard provably correct against a number that is too small produces exactly the overlap it exists to prevent.

Coverage must target the handover, not the frames:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_turnaround_cg.sv — the transition is the event
   covergroup spi_turnaround_cg @(posedge sel_en);
       cp_kind : coverpoint (sel_idx == last_idx) {
           bins same_slave = {1};     // no turnaround owed
           bins handover   = {0};     // turnaround owed
       }

       // Margin actually achieved relative to the requirement. `exact` is
       // where an off-by-one in the comparison hides.
       cp_margin : coverpoint (dead_cycles - min_turnaround) iff (sel_idx != last_idx) {
           bins exact       = {0};
           bins tight       = {[1:3]};
           bins comfortable = {[4:$]};
       }

       // Whether a request was already waiting when the interval expired.
       cp_pending : coverpoint req_pending_at_expiry;

       x_margin_pending : cross cp_margin, cp_pending;
   endgroup

8. Why an FPGA or ASIC Engineer Cares

Convert the requirement to time before choosing the constant. min_turnaround is in system-clock cycles, and the requirement is in nanoseconds — set by the slowest pad turn-off on the bus plus margin. Six cycles at 100 MHz is 60 ns; the same constant at 10 MHz is 600 ns, which is merely slow, and at 200 MHz it is 30 ns, which may not cover the pad. Deriving the constant from a nanosecond figure and the clock period at elaboration is the robust form, and it is the same discipline as Chapter 7.3 §7.

The pull-up sets how fast the net recovers, not how fast the driver stops. A stronger pull-up settles the net sooner and costs static current whenever a device drives low. On a bus that idles mostly undriven, a stronger pull-up is often the cheaper fix than a longer turnaround — and on one that spends most of its time driven low, it is the more expensive one.

Do not put the turnaround in software. The same argument as Chapter 7.3 §7: timer granularity is far coarser than the requirement, and a preemptive system can make the delay arbitrarily long rather than reliably short. A counter in the controller removes the requirement from software entirely.

Measure it on hardware once. The digital interval is knowable from the RTL; the total is not. A single scope capture of the MISO net across a handover — looking for the interval in which it is neither driven high nor driven low but drifting — validates the whole budget, and it is the only way to confirm the analogue terms of §1.

9. Failure Signature — Corruption Proportional to How Fast the Software Switches Devices

Symptom. A multi-device bus is reliable under light load. When the software polls several devices in a tight round-robin, data corruption appears — and its rate rises the faster the loop runs. Slowing the loop down makes it disappear. Each device in isolation is perfect.

What the load dependence establishes. The fault is in the interval between transactions, not in any transaction. A slower loop lengthens the gaps between frames, which is the only thing it changes — so whatever is failing is a function of how closely one frame follows another. Combined with each device being individually correct, that localises it to the handover.

Plausible mechanisms.

  • No turnaround is being enforced, so a fast switch overlaps the two drivers (Chapter 8.5).
  • A turnaround exists but is too short for the pad turn-off — §1's analogue terms not covered.
  • The deselect time of Chapter 7.3 is being violated, which has the same load dependence but a different cause and a different fix.
  • The master drives the selects from software GPIOs, so the interval is whatever the scheduler happened to allow.

The discriminating observation. Whether the corruption depends on which pair of devices is involved. A turnaround problem appears only when the loop switches between different devices, and disappears if the loop is changed to access the same device repeatedly at the same rate. A deselect-time problem persists in both cases, because it is a per-device recovery requirement rather than a bus one.

That single experiment — same-device hammering at the same rate — separates the two mechanisms that this module and Chapter 7.3 describe, and it requires no instrument.

Why the investigation goes wrong. Because "it works when slower" reads as a marginal-speed problem, and the response is to reduce SCLK. That does not help, because SCLK is not what changed — the gap between frames is, and it is set by software loop timing rather than by the bus clock. Lowering SCLK lengthens each frame and can even worsen the ratio, which makes the evidence more confusing rather than less.

10. Common Misconceptions

11. Reason It Through

Work this before reading the answer.

Two slaves share a MISO net with a 47 kΩ pull-up and about 30 pF of total net capacitance. Both slaves release their drivers within 20 ns of deselection. The master enforces a 100 ns turnaround between devices.

Is 100 ns enough? What is it actually protecting against?

Separate the two questions, because they have different answers.

What it protects against: two active drivers. That requires slave A's driver to still be on when slave B's driver turns on. A releases within 20 ns, and the master waits 100 ns before selecting B — after which B still needs its own enable latency before driving. So the active-driver overlap is comfortably prevented with roughly a five-fold margin. For contention, 100 ns is ample.

What it does not protect against: the net having settled. Once A stops driving, the net is held only by the pull-up. Its recovery time is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   τ = R × C = 47 kΩ × 30 pF ≈ 1.4 µs

   to settle within one time constant   ≈ 1.4 µs
   to settle to a valid logic level     ≈ 2–3 µs  (2 τ or so)

That is twenty to thirty times longer than the turnaround. So at the instant B is selected, the net is still drifting from whatever A left it at.

Does that matter? Usually not, and the reason is worth understanding: when B's driver turns on, it actively drives the net and wins immediately against a 47 kΩ pull-up. The slow RC recovery only matters while nothing is driving.

So when does it matter? In exactly one case — and it is the case that makes this question worth asking. If the master samples MISO during the turnaround, or during the interval after B is selected but before B's data phase begins, it reads a drifting, undefined level. A master that treats every clocked bit as data will capture noise in those positions, which is why Chapter 6.4's alignment discards everything before the payload rather than merely ignoring it.

The practical conclusions.

The turnaround is sized for driver overlap, not for net settling. 100 ns is the right order for the first and hopeless for the second, and that is fine because settling is not what it is for.

If the net must settle — because something samples it undriven — reduce the RC, not the timing. A 4.7 kΩ pull-up gives τ ≈ 140 ns at the cost of about 0.7 mA whenever a device drives low. That is a real trade and it is the right lever.

And if the pull-up is weak, do not trust an undriven MISO. This is Chapter 6.5 §2 again: on a weakly-pulled net, "undriven" and "driven low" are indistinguishable for microseconds after release.

The general lesson. A shared net has two independent time constants — how fast a driver stops and how fast the net recovers — and they differ by orders of magnitude. Turnaround budgets protect against the first. Anything that depends on the second needs a different fix entirely.

12. Understanding Check

13. Summary

The turnaround is the interval between one pad genuinely stopping and another genuinely starting — not the interval between chip-select edges, which merely bound it. Four terms sit inside: the outgoing enable latency, the outgoing pad's turn-off, the master's deliberate dead interval, and the incoming enable and pad turn-on. Two of those four are analogue and appear in no simulation.

Electrically, releasing a net is slower than driving it: an active driver pulls hard, while a released net drifts to the pull-up's level at an RC rate. That does not usually cause contention — an active driver beats a weak pull-up immediately — but it means an undriven net is not trustworthy for a long time after release.

A turnaround is owed only on a handover. Re-selecting the same device hands the bus to nobody, and the separate per-device deselect time of Chapter 7.3 is a different requirement with a different cause. Applying the larger of the two unconditionally is safe and costs throughput.

In RTL the guard is a small machine that deselects on entry to the turnaround — so the interval is genuinely dead rather than merely delayed — counts before considering the request, and requires dead time only when the target index differs.

For verification, the notable property asserts the design is not too conservative, catching a safe-but-wrong behaviour that halves throughput and that no functional test would report. And the properties cannot cover the pad turn-off, so the constant must be justified in nanoseconds and confirmed once on hardware.

When fast device switching corrupts data, the discriminating experiment is to hammer one device at the same rate: a turnaround problem disappears and a deselect-time problem does not.

14. What Comes Next

This chapter and the last were both about preventing one thing. Chapter 8.5 — MISO Contention and Multiple Slaves Selected examines what happens when the prevention fails: what two active drivers do to the bus electrically, what the master actually samples, why the damage ranges from a wrong bit to a destroyed output stage, and why contention is the one failure in this track that cannot be implemented in RTL at all — only modelled, detected and designed against.

Continue learning