SPI · Module 14
MISO Output Enable and Release
Asserting late costs a bit; releasing late puts two output stages on one wire. Why the enable is a register rather than a fast combinational path, how the gap requirement depends on the neighbouring device, why two identical slaves never contend, and an output stage verified in three HDLs against a slow and a fast peer.
MISO is the one pin a slave drives, and on a bus with more than one slave it is shared. That gives the slave two obligations, and they are not symmetric:
ASSERT LATE the master reads nothing for a while. Recoverable: it reads
the wrong first bit, which Chapter 14.4 measures and reports.
RELEASE LATE two devices drive the same wire. Not recoverable by anyone:
the contending value is neither slave's, both output stages
source current into each other, and on a long enough bus it
is a reliability problem as well as a data one.Everything else in Module 14 is about data. This chapter is about the one failure that damages hardware, and the asymmetry is why the release gets all the attention.
Chip select goes high. How quickly must the slave stop driving MISO — and who decides?
The second half of that question has a surprising answer: not this slave, and not the master either.
1. The Enable Is A Register
oe <= cs_active // cs_active is already SYNC_N behind the pinOne line, and the pin-to-release latency is therefore SYNC_N + 1 system clocks. That is not a design choice that could have been made differently: the pin is the only information the slave has about being deselected, and it arrives through the synchroniser of Chapter 14.1.
What is a choice is the alternative someone will propose the first time they see that number: derive the enable combinationally from the chip-select pin, and release in nanoseconds instead of cycles.
2. Reset Must Release The Pin
The reset condition on oe is not a formality, and it is the one place in the slave where an asynchronous reset is genuinely mandatory rather than conventional.
A slave held in reset while driving a shared bus makes every other device on that bus unusable. Not degraded — unusable, because MISO is held at whatever the output stage defaults to and no other slave's response can be read. That is a system-level failure caused by one device being held in a state that looks, from the inside, like the safest possible state.
So the ordering matters: oe must go low because of reset, asynchronously, without waiting for a clock — because a board in reset may not have a clock yet. The bench checks this before reset is removed, which is an unusual thing for a bench to do and the only way to check it at all.
3. The Third Requirement, And Why It Depends On The Neighbour
Chapter 14.4 produced two requirements from the synchroniser depth: a lead of SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third — and it is the only one of the three that is not a property of this slave alone.
GAP >= (this slave's release latency) - (the next device's assert latency)Read that subtraction carefully, because it has a consequence that is genuinely surprising:
Two identical oversampling slaves never contend, at any gap. The one being selected is exactly as slow to start driving as the one being deselected is to stop. The subtraction is zero, and the requirement evaporates. A board populated with two of this design can use a gap of one cycle and nothing bad happens.
Put this slave next to a device that drives immediately — an ASIC with no oversampling, or anything clocked on SCLK — and the subtraction stops being free. The neighbour's assert latency is approximately zero, so:
GAP >= SYNC_N + 14. The Slave Cannot Detect Contention
This is the third thing in Module 14 that is invisible from inside the slave, after the late first bit (Chapter 14.4) and the lost edge (Chapter 14.1). It is worth collecting them, because the pattern is the point:
invisible to the slave visible to
---------------------------- ------------------------------------
a lost SCLK edge (14.1) nothing in simulation; a board, via
the measured ratio
a late first bit (14.4) a pin-level observer; the slave sees
only the interval, which cancels
bus contention (14.5) a bus-level observer onlyFor contention specifically: the slave is driving. What it would read back is its own value fighting someone else's, and a CMOS output stage wins or loses by drive strength rather than by arbitration. There is no state in which the slave can conclude "someone else is driving" — it can only conclude "the wire is at the value I am driving", which is what it expects.
What the slave can measure is its own CS-high time. So a back-to-back pair of transactions to this device is checkable — gap_seen reports the interval and gap_short fires if it was below GAP_MIN. A gap between this slave and a neighbour is not checkable by either of them, because neither one observes the other's select line.
That distinction is worth stating precisely because it bounds what the flag means: gap_short clear does not mean there was no contention. It means there was no contention caused by this slave being reselected too quickly.
5. The Block Diagram
6. Building the Output Stage — Three HDLs
The circuit
One enable register, one tri-state assignment, and a gap counter. The tri-state is the only place in the slave that produces a value other than 0 or 1, and the enable is the only signal whose asynchronous reset is a system-level requirement rather than a convention.
// spi_slave_oe.sv
//
// Chapter 14.5 -- driving MISO only while selected, and releasing it in time.
//
// MISO is the one pin a slave drives, and on a bus with more than one slave it is
// shared. So there are two obligations, and they are not symmetric:
//
// ASSERT LATE and the master reads nothing for a while. Recoverable: it reads
// the wrong first bit, which Chapter 14.4 measures and reports.
//
// RELEASE LATE and two devices drive the same wire. Not recoverable by anyone:
// the contending value is neither slave's, both output stages source current
// into each other, and on a long enough bus it is a reliability problem as well
// as a data one.
//
// So the release is the side that gets the attention.
//
// THE ENABLE IS A REGISTER, AND IT IS DERIVED FROM THE SYNCHRONISED SELECT.
//
// oe <= cs_active // cs_active is already SYNC_N behind the pin
//
// which means the pin-to-release latency is SYNC_N + 1 system clocks. That is not
// a design choice that could be made differently: the pin is the only information
// the slave has, and it arrives through the synchroniser.
//
// What IS a choice is the alternative someone will propose -- deriving the enable
// combinationally from the chip-select pin to release faster. Do not:
//
// * a glitch on the select line then DRIVES THE BUS, which is the failure the
// enable exists to prevent;
// * the enable is an output-stage control, so a combinational path from an
// asynchronous input to it is a path from a pin to a pin with no flop in it,
// which static timing analysis has nothing to say about;
// * and it buys SYNC_N cycles on a release that the master's inter-transaction
// gap already covers, if the gap is specified -- which is what §4 is about.
//
// THE THIRD REQUIREMENT ON THE MASTER, AND WHY IT DEPENDS ON THE NEIGHBOUR.
//
// Chapter 14.4 produced two requirements from the synchroniser depth: a lead of
// SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third, and it is the
// only one of the three that is not a property of this slave alone:
//
// GAP >= (this slave's release latency) - (the next device's assert latency)
//
// Two identical oversampling slaves never contend, at any gap, because the one
// being selected is as slow to start driving as the one being deselected is to
// stop. Put this slave next to a device that drives immediately -- an ASIC with no
// oversampling, or anything clocked on SCLK -- and the subtraction stops being
// free: the requirement becomes GAP >= SYNC_N + 1.
//
// That is why a datasheet specifies a minimum CS-high time rather than leaving it
// to the master's judgement. The master cannot compute it, because it depends on
// two devices it does not know the internals of.
//
// AND THE SLAVE CANNOT DETECT CONTENTION. It is driving; what it reads back is its
// own value fighting someone else's, and a CMOS output stage wins or loses by
// drive strength rather than by arbitration. Only a bus-level observer sees it --
// which is the third thing in this module that is invisible from inside the slave,
// after the late first bit (14.4) and the lost edge (14.1). What the slave CAN
// measure is its own CS-high time, so a back-to-back pair of transactions to THIS
// device is checkable; a neighbour's is not.
module spi_slave_oe #(
parameter int GAP_MIN = 3, // recovered cycles of CS-high this slave needs
parameter int CNT_W = 8
) (
input wire clk,
input wire rst_n,
// --- from the front end of 14.1 --------------------------------------
input wire cs_active,
input wire cs_assert_stb,
input wire cs_deassert_stb,
// --- from 14.4 --------------------------------------------------------
input wire miso_val,
// --- the pin ----------------------------------------------------------
output wire oe,
output wire miso_pad, // driven, or released to high impedance
// --- status -----------------------------------------------------------
output reg [CNT_W-1:0] gap_seen, // this slave's own CS-high time
output reg gap_short, // sticky: below GAP_MIN
output reg [CNT_W-1:0] rel_cycles, // measured release latency
input wire clr_flags
);
reg oe_r;
reg cs_active_d;
assign oe = oe_r;
// The tri-state. One expression, and the only place in the slave where a pin
// is not unconditionally driven.
assign miso_pad = oe_r ? miso_val : 1'bz;
reg [CNT_W-1:0] since_release; // cycles since `oe` dropped
reg [CNT_W-1:0] since_deassert; // cycles since the select was seen to rise
reg armed; // a release has happened and not been measured
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset releases the pin, asynchronously. A slave held in reset while
// still driving a shared bus is the same failure as a master holding
// a select line low in reset (Chapter 13.10), from the other side --
// and here the consequence is that no other device on the bus can be
// used at all until this one comes out of reset.
oe_r <= 1'b0;
cs_active_d <= 1'b0;
since_release <= {CNT_W{1'b0}};
since_deassert <= {CNT_W{1'b1}};
armed <= 1'b0;
gap_seen <= {CNT_W{1'b1}};
gap_short <= 1'b0;
rel_cycles <= {CNT_W{1'b0}};
end else begin
// THE enable. One register, fed by the synchronised select and by
// nothing else -- no combinational term, no bypass, no fast path.
oe_r <= cs_active;
cs_active_d <= cs_active;
if (clr_flags)
gap_short <= 1'b0;
// --- the measured release latency ------------------------------
// Counted from the synchronised select falling to the enable
// dropping. In a correct design it is the constant 1, and publishing
// it means a reviewer reads the number rather than deriving it from
// the synchroniser depth -- and a gate-level run that changes it says
// so instead of failing somewhere else.
if (cs_active_d && !cs_active) begin
since_release <= {{(CNT_W-1){1'b0}}, 1'b1};
armed <= 1'b1;
end else if (armed && !oe_r) begin
rel_cycles <= since_release;
armed <= 1'b0;
end else if (since_release != {CNT_W{1'b1}}) begin
since_release <= since_release + 1'b1;
end
// --- this slave's own CS-high time -----------------------------
// Measured in recovered cycles, so the synchroniser latency is at
// both ends and cancels. It says nothing about a NEIGHBOUR's gap,
// which is the case that actually causes contention -- and that
// asymmetry is the point of the header.
if (cs_deassert_stb) begin
since_deassert <= {{(CNT_W-1){1'b0}}, 1'b1};
end else if (cs_assert_stb) begin
if (since_deassert != {CNT_W{1'b1}}) begin
gap_seen <= since_deassert;
if (since_deassert < GAP_MIN)
gap_short <= 1'b1;
end
end else if (since_deassert != {CNT_W{1'b1}}) begin
since_deassert <= since_deassert + 1'b1;
end
end
end
endmodule// spi_slave_oe.v
//
// Chapter 14.5 -- driving MISO only while selected, and releasing it in time.
//
// MISO is the one pin a slave drives, and on a bus with more than one slave it is
// shared. So there are two obligations, and they are not symmetric:
//
// ASSERT LATE and the master reads nothing for a while. Recoverable: it reads
// the wrong first bit, which Chapter 14.4 measures and reports.
//
// RELEASE LATE and two devices drive the same wire. Not recoverable by anyone:
// the contending value is neither slave's, both output stages source current
// into each other, and on a long enough bus it is a reliability problem as well
// as a data one.
//
// So the release is the side that gets the attention.
//
// THE ENABLE IS A REGISTER, AND IT IS DERIVED FROM THE SYNCHRONISED SELECT.
//
// oe <= cs_active // cs_active is already SYNC_N behind the pin
//
// which means the pin-to-release latency is SYNC_N + 1 system clocks. That is not
// a design choice that could be made differently: the pin is the only information
// the slave has, and it arrives through the synchroniser.
//
// What IS a choice is the alternative someone will propose -- deriving the enable
// combinationally from the chip-select pin to release faster. Do not:
//
// * a glitch on the select line then DRIVES THE BUS, which is the failure the
// enable exists to prevent;
// * the enable is an output-stage control, so a combinational path from an
// asynchronous input to it is a path from a pin to a pin with no flop in it,
// which static timing analysis has nothing to say about;
// * and it buys SYNC_N cycles on a release that the master's inter-transaction
// gap already covers, if the gap is specified -- which is what §4 is about.
//
// THE THIRD REQUIREMENT ON THE MASTER, AND WHY IT DEPENDS ON THE NEIGHBOUR.
//
// Chapter 14.4 produced two requirements from the synchroniser depth: a lead of
// SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third, and it is the
// only one of the three that is not a property of this slave alone:
//
// GAP >= (this slave's release latency) - (the next device's assert latency)
//
// Two identical oversampling slaves never contend, at any gap, because the one
// being selected is as slow to start driving as the one being deselected is to
// stop. Put this slave next to a device that drives immediately -- an ASIC with no
// oversampling, or anything clocked on SCLK -- and the subtraction stops being
// free: the requirement becomes GAP >= SYNC_N + 1.
//
// That is why a datasheet specifies a minimum CS-high time rather than leaving it
// to the master's judgement. The master cannot compute it, because it depends on
// two devices it does not know the internals of.
//
// AND THE SLAVE CANNOT DETECT CONTENTION. It is driving; what it reads back is its
// own value fighting someone else's, and a CMOS output stage wins or loses by
// drive strength rather than by arbitration. Only a bus-level observer sees it --
// which is the third thing in this module that is invisible from inside the slave,
// after the late first bit (14.4) and the lost edge (14.1). What the slave CAN
// measure is its own CS-high time, so a back-to-back pair of transactions to THIS
// device is checkable; a neighbour's is not.
module spi_slave_oe #(
parameter GAP_MIN = 3, // recovered cycles of CS-high this slave needs
parameter CNT_W = 8
) (
input wire clk,
input wire rst_n,
// --- from the front end of 14.1 --------------------------------------
input wire cs_active,
input wire cs_assert_stb,
input wire cs_deassert_stb,
// --- from 14.4 --------------------------------------------------------
input wire miso_val,
// --- the pin ----------------------------------------------------------
output wire oe,
output wire miso_pad, // driven, or released to high impedance
// --- status -----------------------------------------------------------
output reg [CNT_W-1:0] gap_seen, // this slave's own CS-high time
output reg gap_short, // sticky: below GAP_MIN
output reg [CNT_W-1:0] rel_cycles, // measured release latency
input wire clr_flags
);
reg oe_r;
reg cs_active_d;
assign oe = oe_r;
// The tri-state. One expression, and the only place in the slave where a pin
// is not unconditionally driven.
assign miso_pad = oe_r ? miso_val : 1'bz;
reg [CNT_W-1:0] since_release; // cycles since `oe` dropped
reg [CNT_W-1:0] since_deassert; // cycles since the select was seen to rise
reg armed; // a release has happened and not been measured
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// Reset releases the pin, asynchronously. A slave held in reset while
// still driving a shared bus is the same failure as a master holding
// a select line low in reset (Chapter 13.10), from the other side --
// and here the consequence is that no other device on the bus can be
// used at all until this one comes out of reset.
oe_r <= 1'b0;
cs_active_d <= 1'b0;
since_release <= {CNT_W{1'b0}};
since_deassert <= {CNT_W{1'b1}};
armed <= 1'b0;
gap_seen <= {CNT_W{1'b1}};
gap_short <= 1'b0;
rel_cycles <= {CNT_W{1'b0}};
end else begin
// THE enable. One register, fed by the synchronised select and by
// nothing else -- no combinational term, no bypass, no fast path.
oe_r <= cs_active;
cs_active_d <= cs_active;
if (clr_flags)
gap_short <= 1'b0;
// --- the measured release latency ------------------------------
// Counted from the synchronised select falling to the enable
// dropping. In a correct design it is the constant 1, and publishing
// it means a reviewer reads the number rather than deriving it from
// the synchroniser depth -- and a gate-level run that changes it says
// so instead of failing somewhere else.
if (cs_active_d && !cs_active) begin
since_release <= {{(CNT_W-1){1'b0}}, 1'b1};
armed <= 1'b1;
end else if (armed && !oe_r) begin
rel_cycles <= since_release;
armed <= 1'b0;
end else if (since_release != {CNT_W{1'b1}}) begin
since_release <= since_release + 1'b1;
end
// --- this slave's own CS-high time -----------------------------
// Measured in recovered cycles, so the synchroniser latency is at
// both ends and cancels. It says nothing about a NEIGHBOUR's gap,
// which is the case that actually causes contention -- and that
// asymmetry is the point of the header.
if (cs_deassert_stb) begin
since_deassert <= {{(CNT_W-1){1'b0}}, 1'b1};
end else if (cs_assert_stb) begin
if (since_deassert != {CNT_W{1'b1}}) begin
gap_seen <= since_deassert;
if (since_deassert < GAP_MIN)
gap_short <= 1'b1;
end
end else if (since_deassert != {CNT_W{1'b1}}) begin
since_deassert <= since_deassert + 1'b1;
end
end
end
endmodule-- spi_slave_oe.vhd
--
-- Chapter 14.5 -- driving MISO only while selected, and releasing it in time.
--
-- MISO is the one pin a slave drives, and on a bus with more than one slave it is
-- shared. So there are two obligations, and they are not symmetric:
--
-- ASSERT LATE and the master reads nothing for a while. Recoverable: it reads the
-- wrong first bit, which Chapter 14.4 measures and reports.
--
-- RELEASE LATE and two devices drive the same wire. Not recoverable by anyone:
-- the contending value is neither slave's, both output stages source current into
-- each other, and on a long enough bus it is a reliability problem as well as a
-- data one.
--
-- So the release is the side that gets the attention.
--
-- THE ENABLE IS A REGISTER, AND IT IS DERIVED FROM THE SYNCHRONISED SELECT.
--
-- oe <= cs_active -- cs_active is already SYNC_N behind the pin
--
-- which means the pin-to-release latency is SYNC_N + 1 system clocks. That is not a
-- design choice that could be made differently: the pin is the only information the
-- slave has, and it arrives through the synchroniser.
--
-- What IS a choice is the alternative someone will propose -- deriving the enable
-- combinationally from the chip-select pin to release faster. Do not:
--
-- * a glitch on the select line then DRIVES THE BUS, which is the failure the
-- enable exists to prevent;
-- * the enable is an output-stage control, so a combinational path from an
-- asynchronous input to it is a path from a pin to a pin with no flop in it,
-- which static timing analysis has nothing to say about;
-- * and it buys SYNC_N cycles on a release that the master's inter-transaction
-- gap already covers, if the gap is specified.
--
-- THE THIRD REQUIREMENT ON THE MASTER, AND WHY IT DEPENDS ON THE NEIGHBOUR.
--
-- Chapter 14.4 produced two requirements from the synchroniser depth: a lead of
-- SYNC_N + 2 and a half-period of SYNC_N + 1. This is the third, and it is the only
-- one of the three that is not a property of this slave alone:
--
-- GAP >= (this slave's release latency) - (the next device's assert latency)
--
-- Two identical oversampling slaves never contend, at any gap, because the one being
-- selected is as slow to start driving as the one being deselected is to stop. Put
-- this slave next to a device that drives immediately -- an ASIC with no
-- oversampling, or anything clocked on SCLK -- and the subtraction stops being free:
-- the requirement becomes GAP >= SYNC_N + 1.
--
-- That is why a datasheet specifies a minimum CS-high time rather than leaving it to
-- the master's judgement. The master cannot compute it, because it depends on two
-- devices it does not know the internals of.
--
-- AND THE SLAVE CANNOT DETECT CONTENTION. It is driving; what it reads back is its
-- own value fighting someone else's, and a CMOS output stage wins or loses by drive
-- strength rather than by arbitration. Only a bus-level observer sees it -- the third
-- thing in this module invisible from inside the slave, after the late first bit
-- (14.4) and the lost edge (14.1). What the slave CAN measure is its own CS-high
-- time, so a back-to-back pair of transactions to THIS device is checkable; a
-- neighbour's is not.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_oe is
generic (
GAP_MIN : positive := 3; -- recovered cycles of CS-high this slave needs
CNT_W : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- from the front end of 14.1
cs_active : in std_logic;
cs_assert_stb : in std_logic;
cs_deassert_stb : in std_logic;
-- from 14.4
miso_val : in std_logic;
-- the pin
oe : out std_logic;
miso_pad : out std_logic; -- driven, or released to high impedance
-- status
gap_seen : out unsigned(CNT_W - 1 downto 0);
gap_short : out std_logic;
rel_cycles : out unsigned(CNT_W - 1 downto 0);
clr_flags : in std_logic
);
end entity;
architecture rtl of spi_slave_oe is
signal oe_r : std_logic := '0';
signal cs_active_d : std_logic := '0';
signal since_release : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal since_deassert : unsigned(CNT_W - 1 downto 0) := (others => '1');
signal armed : std_logic := '0';
signal gap_seen_r : unsigned(CNT_W - 1 downto 0) := (others => '1');
signal gap_short_r : std_logic := '0';
signal rel_cycles_r : unsigned(CNT_W - 1 downto 0) := (others => '0');
constant ALL_ONES : unsigned(CNT_W - 1 downto 0) := (others => '1');
begin
oe <= oe_r;
gap_seen <= gap_seen_r;
gap_short <= gap_short_r;
rel_cycles <= rel_cycles_r;
-- The tri-state. One expression, and the only place in the slave where a pin is
-- not unconditionally driven.
miso_pad <= miso_val when oe_r = '1' else 'Z';
enable : process (clk, rst_n)
begin
if rst_n = '0' then
-- Reset releases the pin, asynchronously. A slave held in reset while
-- still driving a shared bus is the same failure as a master holding a
-- select line low in reset (Chapter 13.10), from the other side -- and
-- here the consequence is that no other device on the bus can be used
-- at all until this one comes out of reset.
oe_r <= '0';
cs_active_d <= '0';
since_release <= (others => '0');
since_deassert <= (others => '1');
armed <= '0';
gap_seen_r <= (others => '1');
gap_short_r <= '0';
rel_cycles_r <= (others => '0');
elsif rising_edge(clk) then
-- THE enable. One register, fed by the synchronised select and by
-- nothing else -- no combinational term, no bypass, no fast path.
oe_r <= cs_active;
cs_active_d <= cs_active;
if clr_flags = '1' then
gap_short_r <= '0';
end if;
-- the measured release latency: from the synchronised select falling to
-- the enable dropping. In a correct design it is the constant 1, and
-- publishing it means a reviewer reads the number rather than deriving
-- it -- and a gate-level run that changes it says so.
if cs_active_d = '1' and cs_active = '0' then
since_release <= to_unsigned(1, CNT_W);
armed <= '1';
elsif armed = '1' and oe_r = '0' then
rel_cycles_r <= since_release;
armed <= '0';
elsif since_release /= ALL_ONES then
since_release <= since_release + 1;
end if;
-- this slave's own CS-high time, in recovered cycles so the latency is
-- at both ends and cancels. It says nothing about a NEIGHBOUR's gap,
-- which is the case that actually causes contention.
if cs_deassert_stb = '1' then
since_deassert <= to_unsigned(1, CNT_W);
elsif cs_assert_stb = '1' then
if since_deassert /= ALL_ONES then
gap_seen_r <= since_deassert;
if to_integer(since_deassert) < GAP_MIN then
gap_short_r <= '1';
end if;
end if;
elsif since_deassert /= ALL_ONES then
since_deassert <= since_deassert + 1;
end if;
end if;
end process;
end architecture;The testbench
Six tests. Tests 3 and 4 are the pair that makes the chapter's point, and they are the same experiment with one parameter changed.
- Reset releases the pin. Checked before reset is removed, because a slave that drives during reset makes every other device on the bus unusable and that is not observable after reset has gone.
- Deselected means released, and the latency is published.
SYNC_N + 1cycles, measured rather than asserted. - Two identical slaves never contend, at any gap. Two instances of this design, the second selected the cycle after the first is deselected, at gaps from 1 upward — and there is never a cycle on which both drive. This is the surprising half of §3 and it is checked rather than argued.
- A fast peer. The same experiment with a peer that drives immediately. Now the gap matters, and the bench finds the boundary: contention occurs for gaps below
SYNC_N + 1and not at or above it. The boundary is the number, and it is measured rather than assumed. - The slave's own CS-high time is measurable and a neighbour's is not. Back-to-back transactions to this device set
gap_short; the identical situation between this device and a peer does not, and the bench asserts that too — because the limit of the flag is part of its specification. - The release latency is visible as driven-while-deselected cycles, which is the observation test 2 turns into a number.
// spi_slave_oe_tb.sv
//
// This testbench builds a BUS, not a block. Two devices share one MISO net, and
// what is measured is whether the net ever carries a contending value -- which is
// the only place the failure exists, because neither device can see it.
//
// Two neighbours are used, and the difference between them is the chapter's whole
// argument:
//
// an IDENTICAL oversampling slave, which is as slow to start driving as this one
// is to stop, so the two never contend at any inter-transaction gap;
//
// a FAST peer that drives the instant its select pin falls -- an ASIC with no
// oversampling, or anything clocked on SCLK -- against which the gap requirement
// stops being free and has to be measured.
//
// The gap sweep then calibrates the number: it finds the shortest gap at which the
// net is clean, and requires it to equal the release latency the slave publishes.
`timescale 1ns/1ps
module spi_slave_oe_tb;
localparam int SYNC_N = 2;
localparam int GAP_MIN = 3;
localparam int CNT_W = 8;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
// --- the shared bus -----------------------------------------------------
wire miso_bus;
// --- device A: the slave under test -------------------------------------
logic cs_a_pin = 1'b1;
logic sclk_pin = 1'b0;
logic mosi_pin = 1'b0;
logic clr_flags = 1'b0;
logic miso_val_a = 1'b1; // a constant 1, so contention with a 0 is visible
wire sclk_q_a, cs_active_a, mosi_q_a;
wire edge_a_a, edge_b_a, cs_as_a, cs_de_a;
wire [11:0] min_half_a;
wire ratio_err_a;
spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_a (
.clk(clk), .rst_n(rst_n), .cpol(1'b0),
.sclk_pin(sclk_pin), .cs_n_pin(cs_a_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q_a), .cs_active(cs_active_a), .mosi_q(mosi_q_a),
.edge_a_stb(edge_a_a), .edge_b_stb(edge_b_a),
.cs_assert_stb(cs_as_a), .cs_deassert_stb(cs_de_a),
.min_half(min_half_a), .ratio_err(ratio_err_a), .clr_flags(1'b0)
);
wire oe_a;
wire [CNT_W-1:0] gap_seen, rel_cycles;
wire gap_short;
spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.cs_active(cs_active_a), .cs_assert_stb(cs_as_a),
.cs_deassert_stb(cs_de_a),
.miso_val(miso_val_a),
.oe(oe_a), .miso_pad(miso_bus),
.gap_seen(gap_seen), .gap_short(gap_short), .rel_cycles(rel_cycles),
.clr_flags(clr_flags)
);
// --- device B, in two flavours ------------------------------------------
logic cs_b_pin = 1'b1;
logic b_is_fast = 1'b0; // 0 = an identical slave, 1 = a zero-latency peer
logic miso_val_b = 1'b0; // a constant 0: contention with A's 1 gives X
// B as an identical oversampling slave.
wire cs_active_b, sclk_q_b, mosi_q_b;
wire edge_a_b, edge_b_b, cs_as_b, cs_de_b;
wire [11:0] min_half_b;
wire ratio_err_b;
spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_b (
.clk(clk), .rst_n(rst_n), .cpol(1'b0),
.sclk_pin(sclk_pin), .cs_n_pin(cs_b_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q_b), .cs_active(cs_active_b), .mosi_q(mosi_q_b),
.edge_a_stb(edge_a_b), .edge_b_stb(edge_b_b),
.cs_assert_stb(cs_as_b), .cs_deassert_stb(cs_de_b),
.min_half(min_half_b), .ratio_err(ratio_err_b), .clr_flags(1'b0)
);
wire oe_b_slow;
wire [CNT_W-1:0] gap_seen_b, rel_cycles_b;
wire gap_short_b;
wire pad_b_slow;
spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) u_b_slow (
.clk(clk), .rst_n(rst_n),
.cs_active(cs_active_b), .cs_assert_stb(cs_as_b),
.cs_deassert_stb(cs_de_b),
.miso_val(miso_val_b),
.oe(oe_b_slow), .miso_pad(pad_b_slow),
.gap_seen(gap_seen_b), .gap_short(gap_short_b),
.rel_cycles(rel_cycles_b), .clr_flags(1'b0)
);
// B as a FAST peer: it drives the instant its select pin falls, with no
// synchroniser at all. This is not a straw man -- a device clocked on SCLK, or
// one built in a technology where the select pin reaches the output enable
// through combinational logic, behaves exactly like this.
wire pad_b_fast = (cs_b_pin == 1'b0) ? miso_val_b : 1'bz;
// Only one flavour drives at a time.
assign miso_bus = b_is_fast ? pad_b_fast : pad_b_slow;
// --- the bus monitor ----------------------------------------------------
integer contentions, driven_cycles, z_cycles;
integer a_driving_deselected;
always_ff @(posedge clk) begin
if (rst_n) begin
if (miso_bus === 1'bx) contentions <= contentions + 1;
else if (miso_bus === 1'bz) z_cycles <= z_cycles + 1;
else driven_cycles <= driven_cycles + 1;
if (oe_a && cs_a_pin) a_driving_deselected <= a_driving_deselected + 1;
end
end
integer errors = 0;
task automatic adv(input integer n);
begin repeat (n) @(negedge clk); end
endtask
task automatic clear_bus_stats;
begin
contentions = 0; driven_cycles = 0; z_cycles = 0;
end
endtask
// A transaction to A followed, after `gap` cycles of both selects high, by a
// transaction to B. `gap` is the master's t_CSD in system clocks.
task automatic a_then_b(input integer gap, input integer nbits,
input integer half);
integer i;
begin
cs_a_pin = 1'b1; cs_b_pin = 1'b1; sclk_pin = 1'b0;
adv(10);
cs_a_pin = 1'b0;
adv(4);
for (i = 0; i < nbits * 2; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
adv(half);
end
adv(4);
cs_a_pin = 1'b1;
adv(gap);
cs_b_pin = 1'b0;
adv(4);
for (i = 0; i < nbits * 2; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
adv(half);
end
adv(4);
cs_b_pin = 1'b1;
adv(10);
end
endtask
integer gap, k, boundary, found;
initial begin
clear_bus_stats();
a_driving_deselected = 0;
adv(3);
// 1. RESET RELEASES THE PIN. Checked before reset is removed, because a
// slave that drives a shared bus while held in reset makes every other
// device on that bus unusable.
if (oe_a !== 1'b0) begin
$display(" FAIL: the enable was asserted while in reset");
errors = errors + 1;
end
if (miso_bus !== 1'bz) begin
$display(" FAIL: the bus was driven while both devices were in reset");
errors = errors + 1;
end
$display(" in reset: the enable is low and the shared bus is released");
rst_n = 1'b1;
adv(4);
// 2. DESELECTED MEANS RELEASED, and the latency is published rather than
// implied.
if (oe_a !== 1'b0) begin
$display(" FAIL: deselected, the enable is still asserted");
errors = errors + 1;
end
cs_a_pin = 1'b0;
adv(6);
if (oe_a !== 1'b1) begin
$display(" FAIL: selected, the enable never asserted");
errors = errors + 1;
end
cs_a_pin = 1'b1;
adv(6);
if (oe_a !== 1'b0) begin
$display(" FAIL: deselected again, the enable did not drop");
errors = errors + 1;
end
if (rel_cycles != 1) begin
$display(" FAIL: the published release latency is %0d, expected 1 cycle past the synchroniser",
rel_cycles);
errors = errors + 1;
end
$display(" the enable follows the synchronised select and publishes a release latency of %0d cycle past it, so the pin-to-release time is %0d system clocks",
rel_cycles, SYNC_N + rel_cycles);
// 3. TWO IDENTICAL SLAVES NEVER CONTEND, at any gap. The one being
// selected is as slow to start as the one being deselected is to stop,
// so the subtraction in the header comes out negative.
b_is_fast = 1'b0;
clear_bus_stats();
for (gap = 0; gap <= 6; gap = gap + 1)
a_then_b(gap, 4, 4);
if (contentions != 0) begin
$display(" FAIL: two identical slaves contended on %0d cycles",
contentions);
errors = errors + 1;
end
if (driven_cycles == 0) begin
$display(" FAIL: the bus was never driven -- the monitor proves nothing");
errors = errors + 1;
end
$display(" two identical oversampling slaves, gaps from 0 to 6: %0d cycles driven, %0d released, and no contending cycle at any gap",
driven_cycles, z_cycles);
// 4. A FAST PEER. Now the gap matters, and the boundary is the number.
b_is_fast = 1'b1;
boundary = 99;
found = 0;
for (gap = 0; gap <= 6; gap = gap + 1) begin
clear_bus_stats();
a_then_b(gap, 4, 4);
$display(" gap=%0d: %0d contending cycles", gap, contentions);
if (contentions == 0 && gap < boundary) boundary = gap;
if (contentions != 0) found = 1;
end
if (!found) begin
$display(" FAIL: a zero-latency peer never contended -- the experiment proves nothing");
errors = errors + 1;
end
if (boundary != SYNC_N + 1) begin
$display(" FAIL: the bus became clean at a gap of %0d, expected %0d",
boundary, SYNC_N + 1);
errors = errors + 1;
end
$display(" a zero-latency peer contends for every gap below %0d and never at or above it, which is exactly this slave's pin-to-release time",
boundary);
// 5. THE SLAVE'S OWN CS-HIGH TIME is measurable, and a neighbour's is not.
b_is_fast = 1'b0;
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
// Two transactions to THIS slave, with a generous gap.
cs_a_pin = 1'b1; adv(10);
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
if (gap_short) begin
$display(" FAIL: a generous CS-high time was reported as short");
errors = errors + 1;
end
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
// And again with a gap below the requirement.
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(1);
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
if (!gap_short) begin
$display(" FAIL: a one-cycle CS-high time was not reported");
errors = errors + 1;
end
$display(" a generous CS-high time to this slave is accepted and a one-cycle one is reported, with the measured value published as %0d cycles",
gap_seen);
// 6. THE RELEASE LATENCY IS VISIBLE AS DRIVEN-WHILE-DESELECTED CYCLES, and
// it is non-zero BY DESIGN. Reporting zero here would mean the enable
// had a combinational path from the pin, which is the thing the chapter
// says not to build.
if (a_driving_deselected == 0) begin
$display(" FAIL: the enable never outlived the select pin -- that implies a combinational path from the pin to the output enable");
errors = errors + 1;
end
$display(" the enable outlived the deselected pin on %0d cycles across the run, which is the registered release the chapter argues for rather than a fault",
a_driving_deselected);
if (errors == 0)
$display("PASS: the output enable is a single register fed by the synchronised select and by nothing else, so the pin-to-release latency is SYNC_N plus one system clocks and the enable is published rather than implied -- reset releases the shared bus asynchronously, two identical oversampling slaves never contend at any inter-transaction gap because the device being selected is as slow to start driving as the one being deselected is to stop, and a zero-latency neighbour contends for every gap below %0d and never at or above it, which is precisely this slave's release time and therefore the minimum CS-high time it requires of a master it shares a bus with -- the slave's own CS-high time is measurable and reported while a neighbour's is not, and the enable outliving the deselected pin is the registered behaviour the chapter argues for rather than a fault, because the alternative is a combinational path from an asynchronous pin to an output stage that no timing tool can constrain",
SYNC_N + 1);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_slave_oe_tb.v
//
// This testbench builds a BUS, not a block. Two devices share one MISO net, and
// what is measured is whether the net ever carries a contending value -- which is
// the only place the failure exists, because neither device can see it.
//
// Two neighbours are used, and the difference between them is the chapter's whole
// argument:
//
// an IDENTICAL oversampling slave, which is as slow to start driving as this one
// is to stop, so the two never contend at any inter-transaction gap;
//
// a FAST peer that drives the instant its select pin falls -- an ASIC with no
// oversampling, or anything clocked on SCLK -- against which the gap requirement
// stops being free and has to be measured.
//
// The gap sweep then calibrates the number: it finds the shortest gap at which the
// net is clean, and requires it to equal the release latency the slave publishes.
`timescale 1ns/1ps
module spi_slave_oe_tb;
localparam SYNC_N = 2;
localparam GAP_MIN = 3;
localparam CNT_W = 8;
reg clk;
reg rst_n;
always #5 clk = ~clk;
// --- the shared bus -----------------------------------------------------
wire miso_bus;
// --- device A: the slave under test -------------------------------------
reg cs_a_pin;
reg sclk_pin;
reg mosi_pin;
reg clr_flags;
reg miso_val_a; // a constant 1, so contention with a 0 is visible
wire sclk_q_a, cs_active_a, mosi_q_a;
wire edge_a_a, edge_b_a, cs_as_a, cs_de_a;
wire [11:0] min_half_a;
wire ratio_err_a;
spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_a (
.clk(clk), .rst_n(rst_n), .cpol(1'b0),
.sclk_pin(sclk_pin), .cs_n_pin(cs_a_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q_a), .cs_active(cs_active_a), .mosi_q(mosi_q_a),
.edge_a_stb(edge_a_a), .edge_b_stb(edge_b_a),
.cs_assert_stb(cs_as_a), .cs_deassert_stb(cs_de_a),
.min_half(min_half_a), .ratio_err(ratio_err_a), .clr_flags(1'b0)
);
wire oe_a;
wire [CNT_W-1:0] gap_seen, rel_cycles;
wire gap_short;
spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.cs_active(cs_active_a), .cs_assert_stb(cs_as_a),
.cs_deassert_stb(cs_de_a),
.miso_val(miso_val_a),
.oe(oe_a), .miso_pad(miso_bus),
.gap_seen(gap_seen), .gap_short(gap_short), .rel_cycles(rel_cycles),
.clr_flags(clr_flags)
);
// --- device B, in two flavours ------------------------------------------
reg cs_b_pin;
reg b_is_fast; // 0 = an identical slave, 1 = a zero-latency peer
reg miso_val_b; // a constant 0: contention with A's 1 gives X
// B as an identical oversampling slave.
wire cs_active_b, sclk_q_b, mosi_q_b;
wire edge_a_b, edge_b_b, cs_as_b, cs_de_b;
wire [11:0] min_half_b;
wire ratio_err_b;
spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(3), .CNT_W(12)) u_fe_b (
.clk(clk), .rst_n(rst_n), .cpol(1'b0),
.sclk_pin(sclk_pin), .cs_n_pin(cs_b_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q_b), .cs_active(cs_active_b), .mosi_q(mosi_q_b),
.edge_a_stb(edge_a_b), .edge_b_stb(edge_b_b),
.cs_assert_stb(cs_as_b), .cs_deassert_stb(cs_de_b),
.min_half(min_half_b), .ratio_err(ratio_err_b), .clr_flags(1'b0)
);
wire oe_b_slow;
wire [CNT_W-1:0] gap_seen_b, rel_cycles_b;
wire gap_short_b;
wire pad_b_slow;
spi_slave_oe #(.GAP_MIN(GAP_MIN), .CNT_W(CNT_W)) u_b_slow (
.clk(clk), .rst_n(rst_n),
.cs_active(cs_active_b), .cs_assert_stb(cs_as_b),
.cs_deassert_stb(cs_de_b),
.miso_val(miso_val_b),
.oe(oe_b_slow), .miso_pad(pad_b_slow),
.gap_seen(gap_seen_b), .gap_short(gap_short_b),
.rel_cycles(rel_cycles_b), .clr_flags(1'b0)
);
// B as a FAST peer: it drives the instant its select pin falls, with no
// synchroniser at all. This is not a straw man -- a device clocked on SCLK, or
// one built in a technology where the select pin reaches the output enable
// through combinational logic, behaves exactly like this.
wire pad_b_fast = (cs_b_pin == 1'b0) ? miso_val_b : 1'bz;
// Only one flavour drives at a time.
assign miso_bus = b_is_fast ? pad_b_fast : pad_b_slow;
// --- the bus monitor ----------------------------------------------------
integer contentions, driven_cycles, z_cycles;
integer a_driving_deselected;
always @(posedge clk) begin
if (rst_n) begin
if (miso_bus === 1'bx) contentions <= contentions + 1;
else if (miso_bus === 1'bz) z_cycles <= z_cycles + 1;
else driven_cycles <= driven_cycles + 1;
if (oe_a && cs_a_pin) a_driving_deselected <= a_driving_deselected + 1;
end
end
integer errors;
task adv;
input integer n;
begin repeat (n) @(negedge clk); end
endtask
task clear_bus_stats;
begin
contentions = 0; driven_cycles = 0; z_cycles = 0;
end
endtask
// A transaction to A followed, after `gap` cycles of both selects high, by a
// transaction to B. `gap` is the master's t_CSD in system clocks.
task a_then_b;
input integer gap;
input integer nbits;
input integer half;
integer i;
begin
cs_a_pin = 1'b1; cs_b_pin = 1'b1; sclk_pin = 1'b0;
adv(10);
cs_a_pin = 1'b0;
adv(4);
for (i = 0; i < nbits * 2; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
adv(half);
end
adv(4);
cs_a_pin = 1'b1;
adv(gap);
cs_b_pin = 1'b0;
adv(4);
for (i = 0; i < nbits * 2; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? 1'b1 : 1'b0;
adv(half);
end
adv(4);
cs_b_pin = 1'b1;
adv(10);
end
endtask
integer gap, k, boundary, found;
initial begin
clear_bus_stats();
a_driving_deselected = 0;
adv(3);
// 1. RESET RELEASES THE PIN. Checked before reset is removed, because a
// slave that drives a shared bus while held in reset makes every other
// device on that bus unusable.
if (oe_a !== 1'b0) begin
$display(" FAIL: the enable was asserted while in reset");
errors = errors + 1;
end
if (miso_bus !== 1'bz) begin
$display(" FAIL: the bus was driven while both devices were in reset");
errors = errors + 1;
end
$display(" in reset: the enable is low and the shared bus is released");
rst_n = 1'b1;
adv(4);
// 2. DESELECTED MEANS RELEASED, and the latency is published rather than
// implied.
if (oe_a !== 1'b0) begin
$display(" FAIL: deselected, the enable is still asserted");
errors = errors + 1;
end
cs_a_pin = 1'b0;
adv(6);
if (oe_a !== 1'b1) begin
$display(" FAIL: selected, the enable never asserted");
errors = errors + 1;
end
cs_a_pin = 1'b1;
adv(6);
if (oe_a !== 1'b0) begin
$display(" FAIL: deselected again, the enable did not drop");
errors = errors + 1;
end
if (rel_cycles != 1) begin
$display(" FAIL: the published release latency is %0d, expected 1 cycle past the synchroniser",
rel_cycles);
errors = errors + 1;
end
$display(" the enable follows the synchronised select and publishes a release latency of %0d cycle past it, so the pin-to-release time is %0d system clocks",
rel_cycles, SYNC_N + rel_cycles);
// 3. TWO IDENTICAL SLAVES NEVER CONTEND, at any gap. The one being
// selected is as slow to start as the one being deselected is to stop,
// so the subtraction in the header comes out negative.
b_is_fast = 1'b0;
clear_bus_stats();
for (gap = 0; gap <= 6; gap = gap + 1)
a_then_b(gap, 4, 4);
if (contentions != 0) begin
$display(" FAIL: two identical slaves contended on %0d cycles",
contentions);
errors = errors + 1;
end
if (driven_cycles == 0) begin
$display(" FAIL: the bus was never driven -- the monitor proves nothing");
errors = errors + 1;
end
$display(" two identical oversampling slaves, gaps from 0 to 6: %0d cycles driven, %0d released, and no contending cycle at any gap",
driven_cycles, z_cycles);
// 4. A FAST PEER. Now the gap matters, and the boundary is the number.
b_is_fast = 1'b1;
boundary = 99;
found = 0;
for (gap = 0; gap <= 6; gap = gap + 1) begin
clear_bus_stats();
a_then_b(gap, 4, 4);
$display(" gap=%0d: %0d contending cycles", gap, contentions);
if (contentions == 0 && gap < boundary) boundary = gap;
if (contentions != 0) found = 1;
end
if (!found) begin
$display(" FAIL: a zero-latency peer never contended -- the experiment proves nothing");
errors = errors + 1;
end
if (boundary != SYNC_N + 1) begin
$display(" FAIL: the bus became clean at a gap of %0d, expected %0d",
boundary, SYNC_N + 1);
errors = errors + 1;
end
$display(" a zero-latency peer contends for every gap below %0d and never at or above it, which is exactly this slave's pin-to-release time",
boundary);
// 5. THE SLAVE'S OWN CS-HIGH TIME is measurable, and a neighbour's is not.
b_is_fast = 1'b0;
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
// Two transactions to THIS slave, with a generous gap.
cs_a_pin = 1'b1; adv(10);
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
if (gap_short) begin
$display(" FAIL: a generous CS-high time was reported as short");
errors = errors + 1;
end
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
// And again with a gap below the requirement.
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(1);
cs_a_pin = 1'b0; adv(8); cs_a_pin = 1'b1; adv(10);
if (!gap_short) begin
$display(" FAIL: a one-cycle CS-high time was not reported");
errors = errors + 1;
end
$display(" a generous CS-high time to this slave is accepted and a one-cycle one is reported, with the measured value published as %0d cycles",
gap_seen);
// 6. THE RELEASE LATENCY IS VISIBLE AS DRIVEN-WHILE-DESELECTED CYCLES, and
// it is non-zero BY DESIGN. Reporting zero here would mean the enable
// had a combinational path from the pin, which is the thing the chapter
// says not to build.
if (a_driving_deselected == 0) begin
$display(" FAIL: the enable never outlived the select pin -- that implies a combinational path from the pin to the output enable");
errors = errors + 1;
end
$display(" the enable outlived the deselected pin on %0d cycles across the run, which is the registered release the chapter argues for rather than a fault",
a_driving_deselected);
if (errors == 0)
$display("PASS: the output enable is a single register fed by the synchronised select and by nothing else, so the pin-to-release latency is SYNC_N plus one system clocks and the enable is published rather than implied -- reset releases the shared bus asynchronously, two identical oversampling slaves never contend at any inter-transaction gap because the device being selected is as slow to start driving as the one being deselected is to stop, and a zero-latency neighbour contends for every gap below %0d and never at or above it, which is precisely this slave's release time and therefore the minimum CS-high time it requires of a master it shares a bus with -- the slave's own CS-high time is measurable and reported while a neighbour's is not, and the enable outliving the deselected pin is the registered behaviour the chapter argues for rather than a fault, because the alternative is a combinational path from an asynchronous pin to an output stage that no timing tool can constrain",
SYNC_N + 1);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b0;
cs_a_pin = 1'b1;
sclk_pin = 1'b0;
mosi_pin = 1'b0;
clr_flags = 1'b0;
miso_val_a = 1'b1;
cs_b_pin = 1'b1;
b_is_fast = 1'b0;
miso_val_b = 1'b0;
errors = 0;
end
endmodule-- spi_slave_oe_tb.vhd
--
-- This testbench builds a BUS, not a block. Two devices share one MISO net, and what
-- is measured is whether the net ever carries a contending value -- which is the only
-- place the failure exists, because neither device can see it.
--
-- Two neighbours are used, and the difference between them is the chapter's whole
-- argument:
--
-- an IDENTICAL oversampling slave, which is as slow to start driving as this one is
-- to stop, so the two never contend at any inter-transaction gap;
--
-- a FAST peer that drives the instant its select pin falls -- an ASIC with no
-- oversampling, or anything clocked on SCLK -- against which the gap requirement
-- stops being free and has to be measured.
--
-- The gap sweep then calibrates the number: it finds the shortest gap at which the
-- net is clean, and requires it to equal the release latency the slave publishes.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_oe_tb is
end entity;
architecture sim of spi_slave_oe_tb is
constant SYNC_N : positive := 2;
constant GAP_MIN : positive := 3;
constant CNT_W : positive := 8;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
-- the shared bus
signal miso_bus : std_logic;
-- device A: the slave under test
signal cs_a_pin : std_logic := '1';
signal sclk_pin : std_logic := '0';
signal mosi_pin : std_logic := '0';
signal clr_flags : std_logic := '0';
signal miso_val_a : std_logic := '1'; -- a constant 1, so a 0 contends visibly
signal sclk_q_a, cs_active_a, mosi_q_a : std_logic;
signal edge_a_a, edge_b_a, cs_as_a, cs_de_a : std_logic;
signal min_half_a : unsigned(11 downto 0);
signal ratio_err_a : std_logic;
signal oe_a : std_logic;
signal gap_seen, rel_cycles : unsigned(CNT_W - 1 downto 0);
signal gap_short : std_logic;
-- device B, in two flavours
signal cs_b_pin : std_logic := '1';
signal b_is_fast : boolean := false;
signal miso_val_b : std_logic := '0'; -- a constant 0: contention gives X
signal sclk_q_b, cs_active_b, mosi_q_b : std_logic;
signal edge_a_b, edge_b_b, cs_as_b, cs_de_b : std_logic;
signal min_half_b : unsigned(11 downto 0);
signal ratio_err_b : std_logic;
signal oe_b_slow : std_logic;
signal gap_seen_b, rel_cycles_b : unsigned(CNT_W - 1 downto 0);
signal gap_short_b : std_logic;
signal pad_b_slow : std_logic;
signal pad_b_fast : std_logic;
signal contentions, driven_cycles, z_cycles : natural := 0;
signal a_driving_deselected : natural := 0;
signal clr_stats : std_logic := '0';
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
u_fe_a : entity work.spi_slave_frontend
generic map (SYNC_N => SYNC_N, HALF_MIN => 3, CNT_W => 12)
port map (clk => clk, rst_n => rst_n, cpol => '0',
sclk_pin => sclk_pin, cs_n_pin => cs_a_pin,
mosi_pin => mosi_pin,
sclk_q => sclk_q_a, cs_active => cs_active_a,
mosi_q => mosi_q_a,
edge_a_stb => edge_a_a, edge_b_stb => edge_b_a,
cs_assert_stb => cs_as_a, cs_deassert_stb => cs_de_a,
min_half => min_half_a, ratio_err => ratio_err_a,
clr_flags => '0');
dut : entity work.spi_slave_oe
generic map (GAP_MIN => GAP_MIN, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
cs_active => cs_active_a, cs_assert_stb => cs_as_a,
cs_deassert_stb => cs_de_a,
miso_val => miso_val_a,
oe => oe_a, miso_pad => miso_bus,
gap_seen => gap_seen, gap_short => gap_short,
rel_cycles => rel_cycles, clr_flags => clr_flags);
u_fe_b : entity work.spi_slave_frontend
generic map (SYNC_N => SYNC_N, HALF_MIN => 3, CNT_W => 12)
port map (clk => clk, rst_n => rst_n, cpol => '0',
sclk_pin => sclk_pin, cs_n_pin => cs_b_pin,
mosi_pin => mosi_pin,
sclk_q => sclk_q_b, cs_active => cs_active_b,
mosi_q => mosi_q_b,
edge_a_stb => edge_a_b, edge_b_stb => edge_b_b,
cs_assert_stb => cs_as_b, cs_deassert_stb => cs_de_b,
min_half => min_half_b, ratio_err => ratio_err_b,
clr_flags => '0');
u_b_slow : entity work.spi_slave_oe
generic map (GAP_MIN => GAP_MIN, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
cs_active => cs_active_b, cs_assert_stb => cs_as_b,
cs_deassert_stb => cs_de_b,
miso_val => miso_val_b,
oe => oe_b_slow, miso_pad => pad_b_slow,
gap_seen => gap_seen_b, gap_short => gap_short_b,
rel_cycles => rel_cycles_b, clr_flags => '0');
-- B as a FAST peer: it drives the instant its select pin falls, with no
-- synchroniser at all. Not a straw man -- a device clocked on SCLK, or one where
-- the select pin reaches the output enable combinationally, behaves like this.
-- Only one flavour drives at a time, and the net is resolved so a genuine
-- conflict shows as 'X'.
--
-- Written as a flat selection rather than a nested conditional expression:
-- VHDL's `when ... else` is not an expression, so it cannot appear inside one.
pad_b_fast <= miso_val_b when (b_is_fast and cs_b_pin = '0') else 'Z';
miso_bus <= pad_b_fast when b_is_fast else pad_b_slow;
monitor : process (clk)
begin
if rising_edge(clk) then
if clr_stats = '1' then
contentions <= 0;
driven_cycles <= 0;
z_cycles <= 0;
elsif rst_n = '1' then
if miso_bus = 'X' then
contentions <= contentions + 1;
elsif miso_bus = 'Z' then
z_cycles <= z_cycles + 1;
else
driven_cycles <= driven_cycles + 1;
end if;
if oe_a = '1' and cs_a_pin = '1' then
a_driving_deselected <= a_driving_deselected + 1;
end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable boundary : integer;
variable found : boolean;
procedure adv(n : natural) is
begin
for k in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
procedure clear_bus_stats is
begin
clr_stats <= '1';
wait until falling_edge(clk);
clr_stats <= '0';
end procedure;
-- A transaction to A followed, after `gap` cycles of both selects high, by a
-- transaction to B. `gap` is the master's t_CSD in system clocks.
procedure a_then_b(gap : natural; nbits : natural; half : natural) is
begin
cs_a_pin <= '1'; cs_b_pin <= '1'; sclk_pin <= '0';
adv(10);
cs_a_pin <= '0';
adv(4);
for i in 0 to nbits * 2 - 1 loop
if (i mod 2) = 0 then sclk_pin <= '1'; else sclk_pin <= '0'; end if;
adv(half);
end loop;
adv(4);
cs_a_pin <= '1';
if gap > 0 then adv(gap); end if;
cs_b_pin <= '0';
adv(4);
for i in 0 to nbits * 2 - 1 loop
if (i mod 2) = 0 then sclk_pin <= '1'; else sclk_pin <= '0'; end if;
adv(half);
end loop;
adv(4);
cs_b_pin <= '1';
adv(10);
end procedure;
begin
adv(3);
-- 1. RESET RELEASES THE PIN.
if oe_a /= '0' then
report " FAIL: the enable was asserted while in reset";
errs := errs + 1;
end if;
if miso_bus /= 'Z' then
report " FAIL: the bus was driven while both devices were in reset";
errs := errs + 1;
end if;
report " in reset: the enable is low and the shared bus is released";
rst_n <= '1';
adv(4);
-- 2. DESELECTED MEANS RELEASED, and the latency is published.
if oe_a /= '0' then
report " FAIL: deselected, the enable is still asserted";
errs := errs + 1;
end if;
cs_a_pin <= '0';
adv(6);
if oe_a /= '1' then
report " FAIL: selected, the enable never asserted";
errs := errs + 1;
end if;
cs_a_pin <= '1';
adv(6);
if oe_a /= '0' then
report " FAIL: deselected again, the enable did not drop";
errs := errs + 1;
end if;
if to_integer(rel_cycles) /= 1 then
report " FAIL: the published release latency is " &
integer'image(to_integer(rel_cycles)) & ", expected 1";
errs := errs + 1;
end if;
report " the enable follows the synchronised select and publishes a release latency of " &
integer'image(to_integer(rel_cycles)) &
" cycle past it, so the pin-to-release time is " &
integer'image(SYNC_N + to_integer(rel_cycles)) & " system clocks";
-- 3. TWO IDENTICAL SLAVES NEVER CONTEND, at any gap.
b_is_fast <= false;
clear_bus_stats;
for gap in 0 to 6 loop
a_then_b(gap, 4, 4);
end loop;
if contentions /= 0 then
report " FAIL: two identical slaves contended on " &
integer'image(contentions) & " cycles";
errs := errs + 1;
end if;
if driven_cycles = 0 then
report " FAIL: the bus was never driven -- the monitor proves nothing";
errs := errs + 1;
end if;
report " two identical oversampling slaves, gaps from 0 to 6: " &
integer'image(driven_cycles) & " cycles driven, " &
integer'image(z_cycles) &
" released, and no contending cycle at any gap";
-- 4. A FAST PEER. Now the gap matters, and the boundary is the number.
b_is_fast <= true;
boundary := 99;
found := false;
for gap in 0 to 6 loop
clear_bus_stats;
a_then_b(gap, 4, 4);
report " gap=" & integer'image(gap) & ": " &
integer'image(contentions) & " contending cycles";
if contentions = 0 and gap < boundary then boundary := gap; end if;
if contentions /= 0 then found := true; end if;
end loop;
if not found then
report " FAIL: a zero-latency peer never contended -- the experiment proves nothing";
errs := errs + 1;
end if;
if boundary /= SYNC_N + 1 then
report " FAIL: the bus became clean at a gap of " &
integer'image(boundary) & ", expected " &
integer'image(SYNC_N + 1);
errs := errs + 1;
end if;
report " a zero-latency peer contends for every gap below " &
integer'image(boundary) &
" and never at or above it, which is exactly this slave's pin-to-release time";
-- 5. THE SLAVE'S OWN CS-HIGH TIME is measurable, and a neighbour's is not.
b_is_fast <= false;
clr_flags <= '1'; adv(1); clr_flags <= '0';
cs_a_pin <= '1'; adv(10);
cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(10);
cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(10);
if gap_short = '1' then
report " FAIL: a generous CS-high time was reported as short";
errs := errs + 1;
end if;
clr_flags <= '1'; adv(1); clr_flags <= '0';
cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(1);
cs_a_pin <= '0'; adv(8); cs_a_pin <= '1'; adv(10);
if gap_short /= '1' then
report " FAIL: a one-cycle CS-high time was not reported";
errs := errs + 1;
end if;
report " a generous CS-high time to this slave is accepted and a one-cycle one is reported, with the measured value published as " &
integer'image(to_integer(gap_seen)) & " cycles";
-- 6. THE RELEASE LATENCY IS VISIBLE AS DRIVEN-WHILE-DESELECTED CYCLES, and
-- it is non-zero BY DESIGN.
if a_driving_deselected = 0 then
report " FAIL: the enable never outlived the select pin -- that implies a combinational path from the pin to the output enable";
errs := errs + 1;
end if;
report " the enable outlived the deselected pin on " &
integer'image(a_driving_deselected) &
" cycles across the run, which is the registered release the chapter argues for rather than a fault";
errors <= errs;
if errs = 0 then
report "PASS: the output enable is a single register fed by the synchronised select and by nothing else, so the pin-to-release latency is SYNC_N plus one system clocks and the enable is published rather than implied -- reset releases the shared bus asynchronously, two identical oversampling slaves never contend at any inter-transaction gap because the device being selected is as slow to start driving as the one being deselected is to stop, and a zero-latency neighbour contends for every gap below " & integer'image(SYNC_N + 1) & " and never at or above it, which is precisely this slave's release time and therefore the minimum CS-high time it requires of a master it shares a bus with -- the slave's own CS-high time is measurable and reported while a neighbour's is not, and the enable outliving the deselected pin is the registered behaviour the chapter argues for rather than a fault, because the alternative is a combinational path from an asynchronous pin to an output stage that no timing tool can constrain";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;7. Why a Verification Engineer Cares
Contention needs two instances, not one instance and an assertion. There is no property of a single slave that says "no contention". The property is about a wire with two drivers, so the bench must instantiate two things and check the wire — and the second thing has to be parameterisable between "identical to this slave" and "drives immediately", because those are the two cases with different answers.
Test 1 runs before reset is removed, which most benches structurally cannot do. A bench whose stimulus process begins with reset; #100; rst_n = 1; has already lost the ability to check anything about the reset state. The check has to be an assertion that is live from time zero, or a stimulus process that inspects the DUT before releasing reset.
Check the boundary of the flag's meaning, not just the flag. Test 5 asserts that gap_short does not fire for a peer-to-this-slave gap, which sounds like testing that a feature is missing. It is testing that the flag means what the datasheet will say it means: this device was reselected too quickly, not there was contention. A flag whose documented meaning is broader than its implementation is worse than no flag.
Tri-state needs a !== comparison, not !=. A bench comparing miso_pad != 1'bz silently succeeds for x as well. This is the one place in the module where four-state comparison is load-bearing, and in VHDL it is the one place 'Z' appears at all.
// Properties for the output stage. The first is the one that matters, and it is
// about a wire rather than about a block -- which is why it needs the second
// driver that test 3 and 4 provide.
property p_never_drive_while_deselected;
// The core obligation. Note it is stated against the REGISTERED enable and
// the synchronised select, because those are the only two things that exist
// in the same time frame.
@(posedge clk) disable iff (!rst_n)
!cs_active |=> !oe;
endproperty
property p_released_in_reset;
// Not inside a disable iff, deliberately: this property is about the reset
// state itself, so disabling it during reset would remove its only content.
@(posedge clk)
!rst_n |-> !oe;
endproperty
property p_pad_z_when_disabled;
// The pad is high-impedance exactly when the enable is low. A four-state
// comparison, because === is the only operator that distinguishes z from x.
@(posedge clk) disable iff (!rst_n)
!oe |-> (miso_pad === 1'bz);
endproperty
property p_pad_driven_when_enabled;
// And the converse: enabled means driving a real value, never x. An x here
// means miso_val is undefined, which is a transmit-path fault surfacing on
// the one pin where it becomes a board problem.
@(posedge clk) disable iff (!rst_n)
oe |-> (miso_pad === 1'b0 || miso_pad === 1'b1);
endproperty
property p_gap_measured_at_assert;
// The measurement is taken at the assert and held, not continuously
// updated -- a continuously updated value reports the current idle time
// rather than the gap that preceded the last transaction.
@(posedge clk) disable iff (!rst_n)
(!cs_assert_stb && !clr_flags) |=> $stable(gap_seen);
endproperty// Coverage. The axis is the GAP, and the second axis is the peer -- because the
// requirement is a function of the neighbour and a suite with one peer type has
// tested one of the two answers.
covergroup cg_oe @(posedge clk iff cs_assert_stb);
option.per_instance = 1;
gap: coverpoint gap_seen {
bins one = {1};
bins below = {2}; // below SYNC_N + 1 at SYNC_N = 2
bins exact = {3}; // exactly SYNC_N + 1
bins margin = {4, 5};
bins roomy = {[6:$]};
}
// The peer's assert latency is what makes the requirement bite, so it is a
// first-class coverage axis rather than a testbench detail.
peer: coverpoint peer_assert_latency {
bins immediate = {0}; // an ASIC, or SCLK-clocked
bins one_cycle = {1};
bins matched = {[2:3]}; // another oversampling slave
bins slower = {[4:$]};
}
// Whether contention actually occurred, observed on the wire by the bench.
// Crossed with the gap, this is the calibration: the boundary must fall at
// exactly one place.
contended: coverpoint saw_both_driving { bins no = {0}; bins yes = {1}; }
x_gap_peer: cross gap, peer;
x_gap_contended: cross gap, contended;
endgroup8. Why an FPGA or ASIC Engineer Cares
The tri-state is an I/O-buffer primitive, and where it lives matters. On an FPGA the oe signal drives the T input of an OBUFT, and the tool will infer that from the conditional assignment — but only if the conditional assignment is at the top level of the design, driving a port. A tri-state buried three levels down gets synthesised as a mux with a constant, or rejected, depending on the tool. That is why this block is instantiated at the top of Chapter 14.10's hierarchy and its output goes straight to a port.
Internal tri-state does not exist on most modern FPGAs. If a design needs the MISO value internally as well as on the pin — for a loopback test, say — it must take miso_val and oe separately rather than reading back the pad. Reading the pad works in simulation and infers a bidirectional buffer with a feedback path in synthesis, which is a different circuit.
oe should have its own ASYNC_REG treatment even though it is not a synchroniser. It is fed by cs_active, which is the output of one, and the register that gates a shared bus is worth placing close to the pad and worth keeping out of a retiming pass. A tool that decides to retime oe a cycle later has lengthened the release latency without telling anyone, and the effect is a contention window on a board with a fast peer.
The release latency is a number for the datasheet, and it is SYNC_N + 1 system clocks — not nanoseconds. That is an awkward thing to publish, because it is a function of the customer's clock, so the honest datasheet entry is a formula plus a worked example at a nominal frequency. A datasheet that states a fixed nanosecond figure has quietly assumed a system clock.
9. Failure Signature — A Two-Slave Board Where One Slave Corrupts The Other
The symptom:
"Each device works perfectly when it is the only one being addressed. With both in use, the first byte read from device B is wrong whenever the previous transaction addressed device A. Swapping the order makes the problem move."
What is happening: device A is this slave, with a release latency of SYNC_N + 1. Device B drives immediately. The master's gap is one cycle. For SYNC_N cycles after device B is selected, both output stages are driving MISO, and the value the master reads during that window is neither device's — so B's first bit is wrong, and only when A was the previous target.
Why it moves when the order is swapped: because the requirement is asymmetric. A-then-B contends; B-then-A does not, because B releases immediately and A is slow to start. A fault that appears in one ordering and not the other is almost always a release-versus-assert latency mismatch, and that asymmetry is the fastest way to recognise it.
How to confirm without an oscilloscope: increase the master's inter-transaction gap and see whether it goes away at exactly SYNC_N + 1. If it does, the fault is this, and the fix is the gap rather than either device. gap_seen on device A will not report the problem — §4 explains why, and knowing that in advance saves an afternoon of doubting the instrument.
10. Common Misconceptions
"The output enable should release as fast as possible, so derive it from the pin." That makes a glitch on the select line drive the bus — the exact failure the enable exists to prevent — and creates a pin-to-pin combinational path that static timing analysis cannot see. The register is not a compromise; it is the mechanism.
"Release latency is a slave-internal concern." It is a board-level concern, because it interacts with the next device's assert latency. The same slave is correct with a one-cycle gap beside a copy of itself and incorrect beside an ASIC.
"If the slave cannot detect contention it cannot be held responsible for it." The slave is responsible for publishing its release latency, which is what lets an integrator compute the gap. That is the whole of what it can do, and a design that does not document the number has failed at the part it could have done.
"gap_short clear means there was no contention." It means this device was not reselected too quickly. Contention between this device and a neighbour is invisible to both of them.
"Two slaves on a bus always need a gap." Two identical oversampling slaves need none, at any gap, because the subtraction in §3 is zero. The requirement appears only when the devices differ.
"Reset releasing the pin is obvious and needs no test." It needs a test that runs before reset is removed, which most bench structures cannot do. And the failure it prevents is not a slave failure — it is every other device on the bus becoming unreadable, which is the largest blast radius of anything in this module.
11. Reason It Through
Q. Two instances of this slave, SYNC_N = 2, and the master uses a gap of one cycle. Is there contention?
No, at any gap. The slave being deselected stops driving SYNC_N + 1 = 3 cycles after its select rises; the slave being selected starts driving 3 cycles after its select falls. If the second select falls one cycle after the first rises, the second starts driving at cycle 4 and the first stopped at cycle 3. The subtraction in §3 is zero and the gap is irrelevant — which is why test 3 sweeps the gap from 1 and finds no contention anywhere.
Q. The same board, but device B is an ASIC that drives within a nanosecond of its select falling. What is the minimum gap, and what happens at one less than it?
SYNC_N + 1 = 3 cycles. At a gap of 2, device B starts driving essentially immediately while device A is still driving for one more cycle — one cycle of contention, during which the master reads a value that is neither device's. One cycle is enough to corrupt B's first bit under CPHA=0, because that is exactly when the master samples it.
Q. Why is it correct for the enable to be derived from the ungated chip select, when Chapter 14.8 gates the select for everything else?
Because the gate exists to keep a transaction begun during reset out of the receive path, and it has nothing to say about the output stage. A slave that was stranded by a reset mid-transaction must still release MISO properly and must still not drive while deselected — those obligations do not depend on whether the transaction is being processed. Wiring the enable to the gated select would mean a stranded slave released MISO late, which is the one failure the enable exists to prevent, in exchange for nothing.
Q. A reviewer proposes publishing oe as an output so that a bus monitor can observe contention. Is that reasonable?
Yes, and it is what Chapter 14.10 does internally. Contention is not observable from the pins — the wire carries one value whichever devices are driving it — so the only way a monitor can see it is by watching each device's enable. Publishing oe costs a pin on a test build or nothing at all on an internal net, and it is the difference between a bench that can check the property and one that cannot.
Q. MISO is pulled up by a resistor on the board, and someone argues that makes contention harmless. What is wrong with that?
The pull-up defines the wire's value when nobody drives it, which is a different situation. When two output stages drive opposite values, the pull-up is irrelevant — it is a weak device in parallel with two strong ones, and the current flows between the two drivers rather than through it. What the pull-up does help with is the released state, where it prevents the input of every listening device from floating; that is a real and separate reason to have one.
12. Understanding Check
13. Summary
MISO is shared, so the slave has two asymmetric obligations. Asserting late costs a bit and is measurable. Releasing late puts two output stages on one wire, and that is a hardware problem rather than a data one — which is why the release gets the attention.
The enable is one register fed by the synchronised select, giving a release latency of SYNC_N + 1. Deriving it combinationally from the pin would make a glitch on chip select drive the bus, create a pin-to-pin path invisible to timing analysis, and buy cycles the specified gap already covers.
Reset must release the pin asynchronously. A slave driving during reset makes every other device on the bus unreadable — the largest blast radius in the module — and a board in reset may have no clock.
The third requirement on the master is GAP >= (release latency) - (the neighbour's assert latency), and it is the only one that is not a property of this slave alone. Two identical oversampling slaves never contend at any gap. A neighbour that drives immediately makes the requirement SYNC_N + 1. A master cannot compute this, which is why a datasheet specifies a minimum CS-high time and the master's gap is programmable.
Contention is invisible to the slave — the third such thing in the module, after the lost edge and the late first bit. What the slave can measure is its own CS-high time, so gap_short means this device was reselected too quickly, not there was no contention, and the bench checks that limit explicitly.
The enable uses the ungated select, because the reset gate exists to protect the receive path and using it here would delay the release for nothing.
For verification: contention needs two instances and a configurable peer latency, because the answer changes with the neighbour; the reset check must run before reset is removed; and tri-state comparisons need !== rather than !=.
For implementation: the tri-state must be at the top level driving a port for OBUFT inference; internal tri-state does not exist on modern FPGAs, so a loopback must take miso_val and oe separately; and oe is worth keeping out of a retiming pass, because a tool that moves it a cycle later has silently lengthened the release.
14. What Comes Next
The slave receives, transmits, and releases the bus. It has been assuming all along that it and the master agree about which edge does what.
Chapter 14.6 — CPOL/CPHA Handling in a Slave builds the mode logic, and it is the chapter where the module's one genuinely wrong first answer had to be replaced. A polarity mismatch is caught from a level before a single bit moves. A phase mismatch is not caught by counting edges — a mismatched pair exchanges a whole number of frames, every time — and the observation that does catch it is physical rather than arithmetical. A bit-order mismatch is not caught at all, and the chapter argues that reporting nothing is the correct engineering answer.
Continue learning
Related tutorials
- Related topic
Slave Microarchitecture and Clocking Assumptions
The two architectures available to a slave that does not own its clock, the one precondition the chosen architecture rests on and why no simulation can test it, why MOSI must pass through exactly as many flops as SCLK, and a delay-matched front end verified in three HDLs.
- Related topic
CS Detection and Transaction Boundaries
Chip select is SPI's only framing and therefore its only resynchronisation point: why counters must reset on assert, how to classify a transaction with a running remainder instead of a divider, why a CPHA mismatch is invisible to the edge count, and a transaction detector verified in three HDLs.
- Related topic
MOSI Capture and Bit Counting
Why the bit counter takes its zero from chip select and nothing else, why a one-bit slip produces well-formed words at the wrong offsets, the complete taxonomy of which configuration mismatches a slave can detect, and a receive path verified in three HDLs across 210 words.
- Related topic
MISO Generation and Launch Timing
Under CPHA=0 the master samples MISO on the very first SCLK edge, so the slave must drive before any clock has moved: deriving the lead a master must provide, why the slave cannot see a late first bit but can measure the interval exactly, why it cannot suppress its final launch, and a transmit path verified in three HDLs.
