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:
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 drivingTwo 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 cycles4. 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:
| Interval | Cause | Applies when |
|---|---|---|
| Deselect time (7.3) | the device's internal reset | every frame boundary |
| Bus turnaround (this chapter) | pad handover on a shared net | only 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
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.
// 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
endmoduleNote 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.
// 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
endmoduleThe 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.
// 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// 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-- 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;-- 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
// 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:
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;
endgroup8. 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:
τ = 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
Related tutorials
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
- Related topic
Extracting Setup, Hold, and Maximum SCLK
Which timing-table rows constrain you and which constrain the device, why a number without its load and corner is not a specification, how a delay on one line alone destroys margin, and the monitor that measures the real link against the datasheet.
- Related topic
Lane Widths per Phase and Direction Changes
Reading the 1-1-4 / 1-4-4 / 4-4-4 notation properly, the direction it does not state, why a multi-lane read reverses four wires at once, and why that is the second reason a quad read needs dummy cycles.
- Related topic
Chip-Select Generation
Chip select is a state machine, not a wire: the three ways deriving it from a busy signal fails, why the between-frames pause and the between-transactions pause are opposites, and why a select for a slave that is not fitted must be refused.
