SPI · Module 8
Slave Output Enable and MISO Tri-State
When a slave may drive MISO and when it must let go: the three independent reasons to stop, why release must be faster than assert, and the output-enable controller that makes the dead gap structural.
Chapter 8.2 established that MISO is the one SPI signal that is a shared medium rather than fan-out: every device has an output on it, and the net works only because at most one of them is enabled at any instant.
What enforces "at most one"? Not the master, which cannot see the drivers — each slave enforces it locally, with no knowledge of the others.
That is the interesting constraint. There is no arbiter, no bus request, and no way for one device to know whether another is driving. The property has to emerge from every device independently obeying a rule.
1. The Rule, and Why It Is Hard
The rule is simple to state: drive MISO only while selected and only during the part of the transaction that returns data.
What makes it hard is that it must hold between devices with no coordination. Device B takes the bus when its select asserts; device A must already have let go. Nothing tells A that B is coming, and nothing tells B that A has finished. Both are obeying a local rule and the bus is safe only if the rules compose.
They compose if — and only if — every device releases faster than any device asserts. That single inequality is what §3 builds and what makes the arrangement work without arbitration.
2. Three Independent Reasons to Stop Driving
A slave's enable must fall for any of three reasons, and they are genuinely independent — a design that handles one and not the others has a real gap.
Deselection. CS rises. Everything stops, including driving. This is the common case and the one everybody implements.
Leaving the data phase. The transaction has moved past the part that returns data — some devices return a fixed-length response and then continue to be clocked (Chapter 5.3 §1 in reverse). Continuing to drive past the response is not harmful to this transaction but extends the window in which a handover could collide.
An early release request. The device deliberately lets go before the frame ends, to widen the turnaround before the next device is selected (Chapter 8.4). This is the one most often omitted, and it is the only one that is a choice rather than a consequence.
The §6 controller treats all three uniformly: each is a term in a single "may I drive" expression, and any of them going false removes the enable in the same cycle.
3. Release Must Be Faster Than Assert
Here is the mechanism, and it is worth understanding precisely because it looks like a trick until it does not.
The enable is registered once into an intermediate signal, and the output is that register ANDed with the live permission:
oe_pre <= may_drive (registered: one cycle)
miso_oe <= oe_pre && may_drive (registered: one more cycle)Turning on must propagate through oe_pre, so it takes two cycles. Turning off kills the AND term immediately, so miso_oe follows in one. The asymmetry is structural rather than a timing coincidence, and it does not depend on which of §2's three reasons caused the release.
The consequence at the bus level: if device A's select deasserts and device B's asserts on the same edge, A's driver is gone one cycle later and B's arrives two cycles later. There is a full cycle in which nobody drives — and that gap exists without either device knowing the other exists.
The §6 testbench measures this rather than asserting it, and reports 2 cycles to assert against 1 to release on every path.
4. The Enable in Time
Slow to take hold, quick to let go
10 cycles5. Who Owns the Net
The pull-up is doing more work than it appears. Without it, "nobody driving" is an undefined floating level rather than a defined one — which means a master cannot distinguish no response from a response of zeros, the ambiguity Chapter 6.5 §2 identified as the main obstacle to reading a capture.
6. Building the Output-Enable Controller — Three HDLs
The circuit
Circuit. One combinational permission term, one intermediate register, one output register, and a latch for the early-release request.
State. oe_pre, the released latch, and the output enable itself.
Datapath. None — this module gates a driver, it does not carry data.
Control. The may_drive term collects all three of §2's reasons. Every term in it is a reason to stop, so any going false removes the enable in the same cycle — which is what makes release one-cycle on every path.
Clock and reset. The system clock; asynchronous active-low reset with the enable off, because a device driving out of reset fights whatever else is on the net before software has configured anything.
Enables. The released latch clears on deselection, so a device that let go early can drive again in the next frame. Within a frame it stays released — re-entering the data phase does not undo a deliberate release, and the testbench checks exactly that.
Timing. Assert 2 cycles, release 1, measured rather than assumed.
Synthesis. Three flip-flops and a handful of gates. The miso_oe register should be placed in the pad's output-enable register so that data and enable reach the pin by matched paths — §8.
Limitations. This gates the enable only; the data path and the fetch are Chapter 6.1's. It also says nothing about the pad's analogue turn-off time, which is the subject of Chapter 8.4.
// spi_oe_ctrl.sv — the slave's MISO output-enable controller.
//
// Two devices sharing MISO must never drive it at the same time. The window
// in which that can happen is the handover: one slave releasing while the
// next asserts (Chapter 8.4). This module makes the window impossible to
// close from the wrong side by an asymmetry that is worth naming:
//
// ASSERTING takes two cycles. RELEASING takes one.
//
// The enable is registered once into `oe_pre` and then ANDed with the LIVE
// condition. Turning on must propagate through the register, so it is slow;
// turning off kills the AND immediately, so it is fast. Every slave on the
// bus therefore lets go faster than any slave can take hold, and the dead
// gap between them is structural rather than a timing coincidence.
module spi_oe_ctrl (
input logic clk,
input logic rst_n,
input logic cs_n, // synchronised select, active low (Chapter 5.2)
input logic data_phase, // level: this device owns the response
input logic release_req, // pulse: let go early (e.g. last bit launched)
output logic miso_oe // to the pad's output enable
);
logic oe_pre;
logic released;
// The live permission to drive. Every term here is a reason to STOP, so
// any of them going false removes the enable in the same cycle.
//
// `release_req` appears here as well as in the latch below. Without it,
// an early release would take two cycles -- one for the latch, one for
// the enable -- which is the same as ASSERT, and the whole "release is
// faster" guarantee would be lost on exactly the path built to use it.
logic may_drive;
assign may_drive = (!cs_n) && data_phase && (!released) && (!release_req);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
oe_pre <= 1'b0;
released <= 1'b0;
miso_oe <= 1'b0;
end else begin
// Deselection clears the early-release latch, so the next frame
// starts from a clean state (Chapter 4.4 §7's resynchronisation).
if (cs_n) released <= 1'b0;
else if (release_req) released <= 1'b1;
oe_pre <= may_drive;
// The AND is the whole design: assert waits for oe_pre, release
// does not.
miso_oe <= oe_pre && may_drive;
end
end
endmoduleThe appearance of release_req in both the combinational term and the latch is the detail that makes the design work. Latching alone would make an early release take two cycles — one for the latch, one for the enable — which is the same as assert, losing the guarantee on precisely the path built to exploit it. The latch provides persistence; the combinational term provides speed.
// spi_oe_ctrl_tb.sv — measure the assert and release latencies, and prove
// release is strictly faster than assert.
`timescale 1ns/1ps
module spi_oe_ctrl_tb;
logic clk = 0, rst_n = 0;
always #5 clk = ~clk;
logic cs_n = 1, data_phase = 0, release_req = 0;
logic miso_oe;
spi_oe_ctrl dut (.clk, .rst_n, .cs_n, .data_phase, .release_req, .miso_oe);
int errors = 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
// Count clock edges from a stimulus change until miso_oe follows.
task automatic measure_assert(output int cycles);
cycles = 0;
@(negedge clk); cs_n = 0; data_phase = 1;
while (!miso_oe && cycles < 20) begin
@(posedge clk); #1; cycles++;
end
endtask
task automatic measure_release_via(input string which, output int cycles);
cycles = 0;
@(negedge clk);
if (which == "cs") cs_n = 1;
else if (which == "phase") data_phase = 0;
else release_req = 1;
while (miso_oe && cycles < 20) begin
@(posedge clk); #1; cycles++;
end
release_req = 0;
endtask
int t_assert, t_cs, t_phase, t_req;
initial begin
repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
chk("reset: not driving", miso_oe, 0);
// --- assert ---
measure_assert(t_assert);
$display(" assert latency: %0d cycles", t_assert);
chk("asserted", miso_oe, 1);
// --- release by deselect ---
measure_release_via("cs", t_cs);
$display(" release via deselect: %0d cycles", t_cs);
chk("released", miso_oe, 0);
// --- re-assert, then release by leaving the data phase ---
data_phase = 0; @(negedge clk);
measure_assert(t_assert);
measure_release_via("phase", t_phase);
$display(" release via phase exit: %0d cycles", t_phase);
chk("released", miso_oe, 0);
// --- re-assert, then early release request ---
cs_n = 1; data_phase = 0; repeat (2) @(negedge clk);
measure_assert(t_assert);
measure_release_via("req", t_req);
$display(" release via early request: %0d cycles", t_req);
chk("released", miso_oe, 0);
// --- THE PROPERTY: every release path is faster than assert ---
if (!(t_cs < t_assert && t_phase < t_assert && t_req < t_assert)) begin
$display("FAIL: a release path was not faster than assert (a=%0d cs=%0d ph=%0d rq=%0d)",
t_assert, t_cs, t_phase, t_req);
errors++;
end
// --- the early-release latch must clear on deselect, so the NEXT
// frame can drive again ---
cs_n = 1; data_phase = 0; repeat (3) @(negedge clk);
measure_assert(t_assert);
chk("re-drives after deselect clears the latch", miso_oe, 1);
// --- and must NOT drive while still released within the same frame ---
@(negedge clk); release_req = 1;
@(negedge clk); release_req = 0;
repeat (4) @(negedge clk);
chk("stays released within the frame", miso_oe, 0);
data_phase = 0; @(negedge clk); data_phase = 1;
repeat (4) @(negedge clk);
chk("re-entering the data phase does not undo the release", miso_oe, 0);
cs_n = 1; data_phase = 0; repeat (3) @(negedge clk);
chk("deselected: not driving", miso_oe, 0);
if (errors == 0)
$display("PASS: the enable asserts in %0d cycles and releases in 1 by every path, so every slave lets go faster than any slave can take hold", t_assert);
else
$display("FAILED with %0d error(s)", errors);
$finish;
end
initial begin #500000; $display("FAIL: watchdog timeout"); $finish; end
endmoduleThat testbench does not assert a latency; it measures each one and then asserts the relationship — that every release path is strictly faster than assert. Checking the relationship rather than the numbers means the test survives a design change that alters both, and fails only if the property that matters is lost.
// spi_oe_ctrl.v — the same output-enable controller in Verilog-2001.
module spi_oe_ctrl (
input wire clk,
input wire rst_n,
input wire cs_n, // synchronised select, active low
input wire data_phase, // level: this device owns the response
input wire release_req, // pulse: let go early
output reg miso_oe
);
reg oe_pre;
reg released;
wire may_drive;
// Every term is a reason to STOP, so any going false removes the enable
// in the same cycle. `release_req` appears here as well as in the latch:
// without it an early release would cost two cycles, the same as assert,
// losing the guarantee on the very path built to use it.
assign may_drive = (!cs_n) && data_phase && (!released) && (!release_req);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
oe_pre <= 1'b0;
released <= 1'b0;
miso_oe <= 1'b0;
end else begin
// Deselection clears the latch so the next frame starts clean.
if (cs_n) released <= 1'b0;
else if (release_req) released <= 1'b1;
oe_pre <= may_drive;
// The AND is the whole design: assert waits for oe_pre,
// release does not.
miso_oe <= oe_pre && may_drive;
end
end
endmodule// spi_oe_ctrl_tb.v — the same latency measurements in Verilog-2001.
`timescale 1ns/1ps
module spi_oe_ctrl_tb;
reg clk = 0, rst_n = 0;
always #5 clk = ~clk;
reg cs_n = 1, data_phase = 0, release_req = 0;
wire miso_oe;
spi_oe_ctrl dut (.clk(clk), .rst_n(rst_n), .cs_n(cs_n),
.data_phase(data_phase), .release_req(release_req),
.miso_oe(miso_oe));
integer errors = 0, t_assert = 0, t_cs = 0, t_phase = 0, t_req = 0, cyc = 0;
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
task measure_assert;
begin
cyc = 0;
@(negedge clk); cs_n = 0; data_phase = 1;
while (!miso_oe && cyc < 20) begin
@(posedge clk); #1; cyc = cyc + 1;
end
t_assert = cyc;
end
endtask
task measure_release_cs;
begin
cyc = 0; @(negedge clk); cs_n = 1;
while (miso_oe && cyc < 20) begin @(posedge clk); #1; cyc = cyc + 1; end
t_cs = cyc;
end
endtask
task measure_release_phase;
begin
cyc = 0; @(negedge clk); data_phase = 0;
while (miso_oe && cyc < 20) begin @(posedge clk); #1; cyc = cyc + 1; end
t_phase = cyc;
end
endtask
task measure_release_req;
begin
cyc = 0; @(negedge clk); release_req = 1;
while (miso_oe && cyc < 20) begin @(posedge clk); #1; cyc = cyc + 1; end
t_req = cyc; release_req = 0;
end
endtask
initial begin
repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
chk("reset: not driving", miso_oe, 0);
measure_assert;
$display(" assert latency: %0d cycles", t_assert);
chk("asserted", miso_oe, 1);
measure_release_cs;
$display(" release via deselect: %0d cycles", t_cs);
chk("released", miso_oe, 0);
data_phase = 0; @(negedge clk);
measure_assert;
measure_release_phase;
$display(" release via phase exit: %0d cycles", t_phase);
chk("released", miso_oe, 0);
cs_n = 1; data_phase = 0; repeat (2) @(negedge clk);
measure_assert;
measure_release_req;
$display(" release via early request: %0d cycles", t_req);
chk("released", miso_oe, 0);
if (!(t_cs < t_assert && t_phase < t_assert && t_req < t_assert)) begin
$display("FAIL: a release path was not faster than assert (a=%0d cs=%0d ph=%0d rq=%0d)",
t_assert, t_cs, t_phase, t_req);
errors = errors + 1;
end
cs_n = 1; data_phase = 0; repeat (3) @(negedge clk);
measure_assert;
chk("re-drives after deselect clears the latch", miso_oe, 1);
@(negedge clk); release_req = 1;
@(negedge clk); release_req = 0;
repeat (4) @(negedge clk);
chk("stays released within the frame", miso_oe, 0);
data_phase = 0; @(negedge clk); data_phase = 1;
repeat (4) @(negedge clk);
chk("re-entering the data phase does not undo the release", miso_oe, 0);
cs_n = 1; data_phase = 0; repeat (3) @(negedge clk);
chk("deselected: not driving", miso_oe, 0);
if (errors == 0)
$display("PASS: the enable asserts in %0d cycles and releases in 1 by every path, so every slave lets go faster than any slave can take hold", t_assert);
else
$display("FAILED with %0d error(s)", errors);
$finish;
end
initial begin #500000; $display("FAIL: watchdog timeout"); $finish; end
endmodule-- spi_oe_ctrl.vhd — the same output-enable controller in VHDL.
library ieee;
use ieee.std_logic_1164.all;
entity spi_oe_ctrl is
port (
clk : in std_logic;
rst_n : in std_logic;
cs_n : in std_logic; -- synchronised select, active low
data_phase : in std_logic; -- level: this device owns the response
release_req : in std_logic; -- pulse: let go early
miso_oe : out std_logic
);
end entity spi_oe_ctrl;
architecture rtl of spi_oe_ctrl is
signal oe_pre : std_logic;
signal released : std_logic;
signal may_drive : std_logic;
begin
-- Every term is a reason to STOP, so any going false removes the enable
-- in the same cycle. release_req appears here as well as in the latch:
-- without it an early release would cost two cycles, the same as assert,
-- losing the guarantee on the very path built to use it.
may_drive <= '1' when cs_n = '0' and data_phase = '1'
and released = '0' and release_req = '0'
else '0';
process (clk, rst_n) is
begin
if rst_n = '0' then
oe_pre <= '0';
released <= '0';
miso_oe <= '0';
elsif rising_edge(clk) then
-- Deselection clears the latch so the next frame starts clean.
if cs_n = '1' then
released <= '0';
elsif release_req = '1' then
released <= '1';
end if;
oe_pre <= may_drive;
-- The AND is the whole design: assert waits for oe_pre,
-- release does not.
miso_oe <= oe_pre and may_drive;
end if;
end process;
end architecture rtl;-- spi_oe_ctrl_tb.vhd — the same latency measurements in VHDL.
library ieee;
use ieee.std_logic_1164.all;
entity spi_oe_ctrl_tb is
end entity spi_oe_ctrl_tb;
architecture tb of spi_oe_ctrl_tb is
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal cs_n : std_logic := '1';
signal data_phase : std_logic := '0';
signal release_req : std_logic := '0';
signal miso_oe : std_logic;
signal halt : boolean := false;
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
dut : entity work.spi_oe_ctrl
port map (clk => clk, rst_n => rst_n, cs_n => cs_n,
data_phase => data_phase, release_req => release_req,
miso_oe => miso_oe);
stim : process is
variable t_assert, t_cs, t_phase, t_req, cyc : natural;
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 measure_assert (variable t : out natural) is
begin
cyc := 0;
wait until falling_edge(clk);
cs_n <= '0'; data_phase <= '1';
while miso_oe /= '1' and cyc < 20 loop
wait until rising_edge(clk);
wait for 1 ns;
cyc := cyc + 1;
end loop;
t := cyc;
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("reset: not driving", miso_oe, '0');
measure_assert(t_assert);
report " assert latency: " & integer'image(t_assert) & " cycles"
severity note;
chk_b("asserted", miso_oe, '1');
-- release via deselect
cyc := 0;
wait until falling_edge(clk); cs_n <= '1';
while miso_oe = '1' and cyc < 20 loop
wait until rising_edge(clk); wait for 1 ns; cyc := cyc + 1;
end loop;
t_cs := cyc;
report " release via deselect: " & integer'image(t_cs) & " cycles"
severity note;
chk_b("released", miso_oe, '0');
-- release via leaving the data phase
data_phase <= '0';
wait until falling_edge(clk);
measure_assert(t_assert);
cyc := 0;
wait until falling_edge(clk); data_phase <= '0';
while miso_oe = '1' and cyc < 20 loop
wait until rising_edge(clk); wait for 1 ns; cyc := cyc + 1;
end loop;
t_phase := cyc;
report " release via phase exit: " & integer'image(t_phase) & " cycles"
severity note;
chk_b("released", miso_oe, '0');
-- release via early request
cs_n <= '1'; data_phase <= '0';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
measure_assert(t_assert);
cyc := 0;
wait until falling_edge(clk); release_req <= '1';
while miso_oe = '1' and cyc < 20 loop
wait until rising_edge(clk); wait for 1 ns; cyc := cyc + 1;
end loop;
t_req := cyc; release_req <= '0';
report " release via early request: " & integer'image(t_req) & " cycles"
severity note;
chk_b("released", miso_oe, '0');
if not (t_cs < t_assert and t_phase < t_assert and t_req < t_assert) then
report "FAIL: a release path was not faster than assert" severity error;
errors <= errors + 1;
end if;
-- the latch clears on deselect
cs_n <= '1'; data_phase <= '0';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
measure_assert(t_assert);
chk_b("re-drives after deselect clears the latch", miso_oe, '1');
-- and holds within the frame
wait until falling_edge(clk); release_req <= '1';
wait until falling_edge(clk); release_req <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
chk_b("stays released within the frame", miso_oe, '0');
data_phase <= '0';
wait until falling_edge(clk);
data_phase <= '1';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
chk_b("re-entering the data phase does not undo the release", miso_oe, '0');
cs_n <= '1'; data_phase <= '0';
for i in 0 to 2 loop wait until falling_edge(clk); end loop;
chk_b("deselected: not driving", miso_oe, '0');
if errors = 0 then
report "PASS: the enable asserts in " & integer'image(t_assert)
& " cycles and releases in 1 by every path, so every slave lets go "
& "faster than any slave can take hold" 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 hardware: identical ports, asynchronous active-low reset with the enable off, a single may_drive term carrying all three release reasons plus the live request, an intermediate register ANDed with that term, and a released latch cleared by deselection. All three testbenches measure the assert latency and all three release latencies and report the same values — 2 cycles to assert, 1 to release on every path.
7. Why a Verification Engineer Cares
The properties are about exclusion and about latency:
// 1. The enable implies every permission. Any single term failing must
// already have removed it.
a_enable_implies_permission : assert property (
@(posedge clk) disable iff (!rst_n)
miso_oe |-> (!cs_n && data_phase && !released))
else $error("driving MISO without permission");
// 2. Release is at most one cycle from any cause. This is THE property:
// bus safety across devices depends on it, and it is checked here
// rather than inferred from the structure.
a_release_is_one_cycle : assert property (
@(posedge clk) disable iff (!rst_n)
$fell(may_drive) |=> !miso_oe)
else $error("enable persisted more than a cycle after losing permission");
// 3. Out of reset, not driving. A device enabled out of reset fights the
// bus before any software has run.
a_reset_not_driving : assert property (
@(posedge clk) $rose(rst_n) |-> !miso_oe)
else $error("driving MISO immediately after reset");
// 4. Cross-device exclusion, at the environment level. This cannot be
// checked inside one slave -- it needs the whole bus.
a_one_driver : assert property (
@(posedge clk) disable iff (!rst_n)
$countones({slave0_oe, slave1_oe, slave2_oe}) <= 1)
else $error("more than one slave driving MISO");Property 4 is the one that matters most and the one a per-slave testbench cannot contain — it is a statement about the bus, so it belongs to the environment. That is the structural reason Chapter 8.2 §6 insisted a multi-slave environment is not optional: the most important assertion in this module has no home in a single-device testbench.
What these prove. That one device obeys the local rule, and — with property 4 — that the composition holds. What they cannot prove is the analogue reality: an enable deasserting on time in RTL says nothing about how long the pad takes to reach high impedance, which is Chapter 8.4's subject and is measurable only on hardware or in a timing model.
Coverage must reach all three release paths:
covergroup spi_oe_cg @(negedge miso_oe);
// WHY the enable fell. Most suites only ever exercise deselection,
// leaving two of the three paths untested.
cp_release_cause : coverpoint release_cause {
bins deselect = {CAUSE_CS};
bins phase_exit = {CAUSE_PHASE};
bins early_req = {CAUSE_REQ}; // usually missing
}
// How close the next device's assertion followed. The tight cases are
// where the asymmetry actually earns its keep.
cp_gap_to_next : coverpoint cycles_until_next_oe {
bins immediate = {[0:1]}; // the dangerous case
bins close = {[2:8]};
bins distant = {[9:$]};
}
x_cause_gap : cross cp_release_cause, cp_gap_to_next;
endgroup8. Why an FPGA or ASIC Engineer Cares
Put the enable in the pad's output-enable register. Most FPGA I/O blocks provide one alongside the data register. If the enable reaches the pad by fabric routing while the data comes from an IOB register, the two skew — the driver can turn on before the data is valid, or release after it should have gone high impedance. Both produce marginal, temperature-dependent contention that simulation cannot show.
The AND must not be optimised into the register. Synthesis may be tempted to retime oe_pre && may_drive back into the oe_pre flop, collapsing the two stages and destroying the asymmetry. If the tool does that, the release becomes as slow as the assert and the bus-safety argument evaporates. A dont_touch or a retiming constraint on this path is cheap insurance, and it is worth a comment in the code saying why.
The pad's disable time is a real number. A pad may take a nanosecond or more to reach high impedance after its enable deasserts. On a shared net that time is part of the turnaround budget, and the one-cycle RTL advantage buys real margin only if a cycle is longer than the pad's turn-off.
Never drive MISO from a non-tri-state output. It sounds obvious and it happens: an output declared as a plain register instead of a tri-state buffer drives continuously, and the design works perfectly as long as there is one device on the bus. It fails the moment a second is added, which is Chapter 8.2 §8's failure and the reason single-device bring-up proves nothing about bus behaviour.
9. Failure Signature — Corruption Only on the First Byte After Switching Devices
Symptom. A multi-device bus works when each device is accessed repeatedly. When the software alternates between two devices, the first byte of each transaction is corrupted and every byte after it is correct. Accessing the same device twice in a row is always clean.
What "only after switching" establishes. The fault is in the handover, not in either device. Each device is individually correct — repeated access proves that — and the corruption appears exactly when the net changes owner. That immediately localises it to the output-enable timing rather than to mode, framing or data.
Plausible mechanisms.
- The outgoing device's release is too slow, overlapping the incoming device's assertion. The first byte is sampled while both drive, so it is an undefined blend.
- The outgoing device's pad turn-off is slow even though its RTL enable was prompt — the analogue version of the same thing.
- The master provides no turnaround interval, switching selects on adjacent cycles (Chapter 8.4).
- One device fails to release at all and is only masked when the other is not driving.
The discriminating observation. Insert a deliberate idle interval between the two transactions — deselect everything for a few microseconds before selecting the next device. If the corruption disappears, it is a turnaround problem and the fix is to guarantee that interval in hardware. If it persists, one device is not releasing at all, and the next step is to check its enable with the bus otherwise idle.
That experiment separates a timing overlap from a static failure to release, and it is a software change rather than an instrument.
Why the investigation goes wrong. Because each device works in isolation, so both are presumed correct and the bus is presumed simple. The corruption is in the interval between two devices' transactions, which is nobody's transaction and therefore nobody's responsibility — and it does not appear in any single-device test, no matter how thorough.
10. Common Misconceptions
11. Reason It Through
Work this before reading the answer.
A slave is implemented on an FPGA. Its MISO output enable is correct in RTL, verified, and measured at one system clock from any release cause. The system clock is 10 MHz. The board has two such slaves, and the master switches between them with a two-SCLK-cycle gap at 20 MHz SCLK.
Is the bus safe?
Work in real time, not cycles — that is where this goes wrong.
The slave's release takes one system clock. At 10 MHz that is 100 ns.
The master's gap is two SCLK cycles at 20 MHz, which is 100 ns.
They are equal, which means there is no margin at all. The outgoing slave's enable falls exactly as the incoming slave's select is asserted, and any variation — clock skew between the two slaves' oscillators, a slightly different pad turn-off, temperature — pushes them into overlap.
And it is worse than equal, because two things have been left out.
The incoming slave's assert is not instantaneous either. It takes two of its system clocks, so 200 ns. That actually helps — the incoming driver arrives later than the gap implies — and it is the asymmetry doing its job. But it is the outgoing side that must finish in time, and 100 ns of release against 100 ns of gap is exactly break-even.
The pads are not in the RTL. The one-cycle release is when the internal enable falls. The pad then takes its own turn-off time — often a nanosecond or two, but on a heavily loaded net with a pull-up it can be longer, because the net must be pulled to its idle level by a weak resistor rather than driven there.
So the honest answer is: not safe, and only marginally so — which is the worst kind. It will work on the bench and fail on some units, at some temperatures, which is the signature of a design sitting on its boundary rather than one that is broken.
The fixes, in order of preference.
Raise the slave's system clock. At 50 MHz the release is 20 ns against a 100 ns gap — a five-fold margin, and it costs nothing if the fabric can take it. This is the same conclusion as Chapter 5.2 §12: the ratio between the slave's clock and the SPI timing is the real parameter, and a slow interface clock makes every timing property marginal at once.
Lengthen the master's turnaround. Four SCLK cycles instead of two doubles the margin, costs a little throughput, and is a one-line configuration change if the master supports it — which is what Chapter 8.4 builds.
Use the early-release path. The slave lets go before the frame ends rather than at deselection, widening the gap by however many bit times it gives up. Costs nothing electrically and requires the protocol to tolerate it.
The general lesson. A cycle-count guarantee is meaningless until it is converted to time and compared against the other side's time. "One cycle" sounds fast and is 100 ns on a slow clock — and the bus does not care about cycles.
12. Understanding Check
13. Summary
MISO is a shared medium with no arbitration. Each slave obeys a local rule — drive only while selected and only during its response — and the bus is safe only because those rules compose.
They compose because of an inequality: every device releases faster than any device asserts. That is achieved structurally, by registering the permission and ANDing the register with the live permission, giving two cycles to assert and one to release. No device needs to know another exists.
There are three independent reasons to stop driving — deselection, leaving the data phase, and a deliberate early release — and all three must be terms in the same permission expression so that any of them releases in one cycle. The early-release request must appear in both the combinational term and the latch: the term gives speed, the latch gives persistence.
The net needs a pull-up, without which "nobody driving" is undefined and no response cannot be distinguished from a response of zeros.
For verification, the per-slave properties are exclusion and latency, but the property that matters most — at most one driver on the net — is a statement about the bus and has no home in a single-slave testbench. Coverage must reach all three release causes, of which the early request is the one usually missing.
On hardware, the enable belongs in the pad's output-enable register alongside the data so the two cannot skew, and the AND must be protected from retiming that would collapse the asymmetry. And a cycle-count guarantee means nothing until it is converted to time: one cycle is 100 ns on a 10 MHz clock, and the bus does not care about cycles.
14. What Comes Next
This chapter made one device let go quickly. Chapter 8.4 — Bus Turnaround and the Contention Window examines the interval that creates: how long the gap between one driver releasing and the next asserting actually is, what fills it, why the pads' analogue behaviour makes it shorter than the RTL suggests, and the master-side guard that enforces a dead interval when — and only when — the bus genuinely changes hands.
Continue learning
Related tutorials
- Related topic
Mode 0 (CPOL=0, CPHA=0)
The most widely used SPI mode, and the one carrying a real implementation problem: why CPHA=0 forces the first bit onto the line before any clock edge, and the first-bit launch path in Verilog, SystemVerilog and VHDL.
- Related topic
Bit Ordering — MSB-First and LSB-First
Which end of the shift register goes out first, the two multiplexers that make the order configurable in three HDLs, and why a bit-order bug is perfectly deterministic and yet invisible on certain data.
- Related topic
MOSI Data Flow and CS Framing
What the master drives on MOSI through every region of a transfer, what the two chip-select edges bracket, and why an asynchronous CS must cross into the slave's clock domain before any edge is derived from it.
- Related topic
Anatomy of a Read Transaction
The four phases of a device read, why a read cannot be a write reversed, when the slave takes and releases MISO, the obligation to have the first data bit valid before any edge can launch it, and the slave read datapath in three HDLs.
