SPI · Module 14
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.
Module 13 built a master. Every chapter in it began from the same comfortable place: the master owns the clock, so it decides when each edge happens, and every other decision is downstream of a decision it already made.
A slave has none of that. SCLK, CS and MOSI all arrive from another device, generated by another clock, with no defined relationship to this one. Nothing in the slave decides when anything happens.
A slave has no control over when the next SCLK edge arrives. What clock should its flip-flops use?
There are exactly two answers, and the choice is not a matter of taste. It determines what the rest of Module 14 is about, which failures are possible, which are impossible, and — this is the part that surprises people — which of the design's assumptions can be tested in simulation at all.
1. The Two Architectures
Architecture A — clock the shift logic on SCLK. Treat SCLK as a real clock. The shift register, the bit counter and the capture logic are all clocked by it. The system-side interface then becomes the foreign domain, and every signal crossing from the shift logic to the system needs a clock-domain crossing.
Architecture B — clock everything on the system clock and recover SCLK's edges. SCLK is just an input. It goes through a synchroniser, an edge detector produces a one-cycle strobe per SCLK transition, and all the shift logic runs on the system clock, advancing when a strobe says to.
A clocked on SCLK B clocked on the system clock
------------------------------------ ------------------------------------
no ratio requirement at all SCLK must be slower than the clock
SCLK is a clock: needs a clock tree, SCLK is data: no tree, no constraint
a constraint, and a skew budget beyond an input delay
every system-side signal crosses a nothing crosses a domain
domain boundary
works at SCLK faster than the fails, silently, if SCLK is too fast
system clock
the FIFO or handshake at the the whole design is one domain and
boundary is the hard part the front end is the hard partNeither is wrong. Architecture A is what you build when SCLK may be faster than the system clock — a 50 MHz SPI flash interface on a 25 MHz control domain has no choice — and Chapter 15.4 builds it properly and compares the two with numbers.
This module takes B. Not because it is better, but because the far more common case is a slave whose system clock is much faster than SCLK, and because B has one property that matters for a teaching sequence: it has one hard requirement, it can be stated in a single line, and everything else in Module 14 follows from it without further assumptions.
2. The Block Diagram
The shape is worth a sentence. In the master (Chapter 13.2) the interesting boundary was control versus datapath, because the master decides everything and the only question is how often it decides. Here the interesting boundary is pin versus recovered: exactly one block touches a pin, and every other block in Module 14 works in recovered time, where SCLK is not a clock but a pair of strobes.
That is why this chapter comes first and why it is short on structure and long on one idea. Get the front end wrong and every later chapter inherits the error, dressed up as something else.
3. The Precondition, and Why It Cannot Be Simulated
State it precisely:
Each SCLK half-period must last at least
HALF_MINsystem clocks, whereHALF_MINis at leastSYNC_N + 1.
The usual explanation for a requirement like this is "resolution" or "margin", and both are wrong. The real reason is that an edge can be lost entirely.
A synchroniser's first flop can be sampled while its input is changing. When that happens the flop goes metastable and resolves to either the new value or the old one — that is what metastability means, and a synchroniser's job is only to make sure that whichever it resolves to, it has finished resolving before anything downstream looks. It does not guarantee which value you get.
At a comfortable ratio, resolving to the old value costs one system clock of extra latency on that edge and nothing else: the next sample gets the new value, the edge is detected one cycle late, and nothing downstream can tell. At a tight ratio it is different. If the half-period is only as long as the synchroniser chain, the old value is still propagating when the next sample arrives, and an entire half-period is never seen. The edge is not late. It is gone. The bit counter is one behind from that point on, and every remaining bit of the frame lands in the wrong position.
Here is the part that matters for how you verify it:
This is a general habit worth taking from the chapter: when a design rests on an assumption that verification cannot reach, make the design report the assumption's inputs. It costs a counter and a comparator, and it converts an unfalsifiable claim into a measurement.
4. Why MOSI Goes Through Exactly As Many Flops As SCLK
This is the part that is most often got wrong, and the usual explanation for getting it right is also wrong, so it is worth being careful.
MOSI is not synchronised to avoid metastability in the way a control signal is. A wrong bit is a wrong bit whether it arrived metastable or not; nothing downstream of MOSI does anything dangerous with an undefined value. MOSI is synchronised to be delay-matched with the SCLK path.
Write S(t) and M(t) for the pin values at cycle t, and take SYNC_N = 2:
sclk_q(t) = S(t-2) mosi_q(t) = M(t-2)The edge detector compares sclk_q(t) against sclk_q(t-1). So a pin edge that happened between t-3 and t-2 is detected at cycle t — and at cycle t, mosi_q holds M(t-2), which is MOSI at the moment that edge happened. Delay-matched, and therefore the right bit.
Now put MOSI through one flop instead of two. The sampled value is M(t-1): one system clock newer than the edge.
matched (2 and 2) mismatched (2 and 1)
------------------------------------ ------------------------------------
edge from t-3..t-2 detected at t same
mosi_q(t) = M(t-2) mosi_q(t) = M(t-1)
= MOSI when the edge happened = MOSI one cycle after the edge
correct at every ratio correct only while the master still
holds the old bit one cycle laterAt a comfortable ratio the master is still holding the old bit one cycle later, so it works. At a tight ratio, or against a master that releases MOSI promptly, M(t-1) is the next bit, and the slave reads every byte shifted by one position — intermittently, as a function of the frequency ratio and the master's output timing, which is about the hardest failure mode there is to attribute to its cause.
How much the mismatch actually costs
MOSI changes only on launch edges, so it is stable for a full SCLK period with the capture edge in the middle: half a period of setup and half a period of hold. A depth mismatch of D cycles spends D of that budget — and at HALF_MIN = 3, one stage is a third of it.
That third was not spare. It belonged to the board, to the master's output-valid time, and to this flop's own setup requirement. A design that spends a third of its data-valid window on a synchroniser asymmetry has spent it on nothing at all.
And a master is not obliged to hold MOSI for a half-period. Datasheets specify an output hold of a few nanoseconds. A master that changes MOSI shortly after the capture edge is entirely legal — and against that master the mismatched design reads the next bit rather than the current one at every ratio, not just tight ones. §6's testbench drives exactly that master and shows the one-stage mismatch failing where the matched path does not.
5. What The Front End Publishes, And Why Each Item Exists
sclk_q, cs_active, mosi_q the recovered pin values, delay-matched
edge_a_stb LEADING edge: SCLK left its idle level
edge_b_stb TRAILING edge: SCLK returned to idle
cs_assert_stb, cs_deassert_stb the transaction boundary, one cycle each
min_half the shortest interval between two edges
ratio_err sticky: a DATA half-period was too shortTwo of these need justifying.
edge_a_stb and edge_b_stb are defined against the configured idle level, not against rising and falling. edge_a_stb means "SCLK left idle" and edge_b_stb means "SCLK returned to idle". Under CPOL = 0 that is rising and falling; under CPOL = 1 it is falling and rising. Consuming CPOL here rather than downstream means every later block sees the same strobe sequence in every polarity, and Chapter 14.6 then has only CPHA to handle — one bit instead of two, and a mode table with two rows instead of four.
min_half and ratio_err deliberately disagree with each other, and §6 is where that matters. min_half is the true shortest interval the front end ever saw, including the last one of a transaction. ratio_err is gated on intervals that were followed by more clocking. The distinction was not in the first version of this design; it came out of Chapter 14.10, where this front end met a real master, and it is explained where it was found.
6. Building the Front End — Three HDLs
The circuit
Three synchroniser chains of identical depth, an edge detector on the recovered SCLK defined against cpol, a CS edge detector, and an interval counter that measures the time between recovered edges. The counter saturates rather than wrapping, and it reloads with one rather than zero — the cycle on which an edge is detected is itself the first cycle of the next interval, so reloading with zero measures every half-period one short and reports a master supplying exactly HALF_MIN as violating the precondition.
// spi_slave_frontend.sv
//
// Chapter 14.1 -- the clocking assumption, made concrete.
//
// A master owns its clock. A slave does not: SCLK, CS and MOSI all arrive from
// another device, on another clock, with no defined relationship to this one.
// Every decision in Module 14 follows from what is done about that, and there
// are only two answers:
//
// A clock the shift logic ON SCLK, and treat the system side as the
// foreign domain
// B clock everything on the system clock and RECOVER SCLK's edges from it
//
// This module takes B, and this file is where that choice lives. Chapter 15
// builds A and compares them properly; what matters here is that B has a
// PRECONDITION, and a design that does not state it has not made a choice, it
// has made an assumption.
//
// THE PRECONDITION, AND WHY IT IS ABOUT EDGES BEING LOST.
//
// SCLK must be slow enough that each half-period lasts at least HALF_MIN system
// clocks. The reason is not resolution and it is not margin: it is that a
// synchroniser's first flop can be sampled while its input is changing, and the
// value it then resolves to may be the OLD one. At a comfortable ratio that
// costs one cycle of edge latency and nothing else. At a tight ratio the old
// value is still in place when the next sample arrives, and an entire SCLK
// half-period is never seen -- the edge is not late, it is GONE, and the frame
// slips by a bit from there on.
//
// That failure cannot be simulated, because metastability is not modelled: a
// simulator's flop always captures a defined value. So the precondition is a
// stated assumption rather than a tested property -- which is exactly why this
// block MEASURES the ratio and reports it, so the assumption is checkable on
// hardware even though it is unreachable in a testbench.
//
// WHY MOSI GOES THROUGH THE SAME NUMBER OF FLOPS AS SCLK.
//
// This is the part that is usually got wrong, and it is worth being precise
// because the usual explanation is also wrong.
//
// MOSI is not synchronised to avoid metastability in the way a control signal
// is. It is synchronised to be DELAY-MATCHED with the SCLK path. Write S(t)
// and M(t) for the pin values at cycle t, and take SYNC_N = 2:
//
// sclk_q(t) = S(t-2) mosi_q(t) = M(t-2)
//
// The edge detector compares sclk_q(t) against sclk_q(t-1), so a pin edge
// between t-3 and t-2 is detected at cycle t -- and at cycle t, mosi_q holds
// M(t-2), which is MOSI at the moment the edge happened. Delay-matched, and
// therefore the right bit.
//
// Put MOSI through ONE flop instead of two and the sampled value is M(t-1):
// one system clock NEWER than the edge. At a comfortable ratio that is still
// inside the master's hold window and it works. At a tight ratio it is the
// NEXT bit, and the slave reads every byte shifted by one -- intermittently,
// as a function of the frequency ratio, which is the hardest possible thing to
// attribute.
//
// So the two synchroniser depths are not independent choices. They are one
// choice, applied twice.
//
// HOW MUCH THE MISMATCH ACTUALLY COSTS. MOSI changes only on launch edges, so
// it is stable for a full SCLK period with the capture edge in the middle: a
// half-period of setup and a half-period of hold. A depth mismatch of D cycles
// spends D of those, and at HALF_MIN = 3 one stage is a third of the budget --
// which is a third that belonged to the BOARD, to the master's output-valid
// time and to this flop's own setup. A design that spends it on a synchroniser
// asymmetry has spent it on nothing.
//
// And a master is not obliged to hold MOSI for a half-period. Datasheets
// specify an output hold of a few nanoseconds, so a master that changes MOSI
// shortly after the capture edge is legal -- and against that master the
// mismatched design reads the NEXT bit rather than the current one, at every
// ratio. Section 6's testbench drives exactly that master and shows a
// one-stage mismatch failing where the matched path does not.
//
// AND WHY THE RATIO PRECONDITION IS ABOUT CORRECTNESS, NOT MARGIN.
//
// The second flop of a synchroniser gives a settled value. It gives the RIGHT
// value only if the input was stable long enough that "the value" is
// unambiguous -- and MOSI is stable for one SCLK half-period. So the ratio
// requirement is what turns "a settled bit" into "the correct bit", and it is
// the same requirement that keeps two recovered edges out of one cycle.
// One precondition, two consequences, and this block measures it.
module spi_slave_frontend #(
parameter int SYNC_N = 2, // synchroniser depth, the SAME for all pins
// Named HALF_MIN rather than MIN_HALF because VHDL identifiers are
// case-insensitive, so a generic MIN_HALF and an output port min_half
// would be the same name -- and the VHDL would compile, with the
// generic silently shadowed. The three languages keep one name.
parameter int HALF_MIN = 3, // system clocks per SCLK half-period
parameter int CNT_W = 12 // width of the interval measurement
) (
input wire clk,
input wire rst_n,
input wire cpol, // this slave's configured idle level
// --- raw pins, asynchronous to clk ----------------------------------
input wire sclk_pin,
input wire cs_n_pin,
input wire mosi_pin,
// --- recovered, delay-matched ---------------------------------------
output wire sclk_q, // SCLK as this design sees it
output wire cs_active, // chip select, synchronised
output wire mosi_q, // MOSI, matched to the edge strobes
output wire edge_a_stb, // LEADING edge: SCLK left idle
output wire edge_b_stb, // TRAILING edge: SCLK returned
output wire cs_assert_stb,
output wire cs_deassert_stb,
// --- the precondition, measured -------------------------------------
output reg [CNT_W-1:0] min_half, // shortest interval observed, ALL of them
output reg ratio_err, // sticky: a DATA half below HALF_MIN
input wire clr_flags
);
// Three synchroniser chains of identical depth. Declared as one array so a
// future edit cannot lengthen one and not the others -- which is the whole
// failure this file's header is about.
reg [SYNC_N-1:0] sclk_sr;
reg [SYNC_N-1:0] cs_n_sr;
reg [SYNC_N-1:0] mosi_sr;
// One extra stage on SCLK alone, for edge detection. This is NOT part of
// the synchroniser: it is the previous-value register, and it is why the
// strobes appear one cycle after `sclk_q` moves.
reg sclk_d;
assign sclk_q = sclk_sr[SYNC_N-1];
assign cs_active = ~cs_n_sr[SYNC_N-1];
assign mosi_q = mosi_sr[SYNC_N-1];
// LEADING and TRAILING, not rising and falling. Defined against the
// configured idle level, exactly as the master's divider defines them
// (Chapter 13.4), so that CPOL is consumed here and nothing downstream
// sees a voltage direction.
wire away_now = (sclk_q != cpol);
wire away_prev = (sclk_d != cpol);
assign edge_a_stb = away_now & ~away_prev;
assign edge_b_stb = ~away_now & away_prev;
reg cs_active_d;
assign cs_assert_stb = cs_active & ~cs_active_d;
assign cs_deassert_stb = ~cs_active & cs_active_d;
// --- the ratio measurement -------------------------------------------
// Cycles since the last recovered edge. The interval before the FIRST edge
// is unbounded, so the counter saturates rather than wrapping -- a wrap
// would make a very long interval look like a very short one and flag the
// first frame after reset, which is the bug this comment exists to prevent
// (the same one as the master's requirements monitor, Chapter 13.1).
reg [CNT_W-1:0] since_edge;
wire any_edge = edge_a_stb | edge_b_stb;
// The interval that most recently closed, held back one edge. An interval is a
// HALF-PERIOD only once another edge has followed it; until then it might be the
// last interval of the transaction, and the last interval is a different thing.
//
// This distinction is not fussiness, it is a real property of real masters and
// Chapter 14.10 measured it. A master clocking CPHA=0 captures on leading edges
// and then returns SCLK to idle -- and it has no reason to wait a full half-period
// before doing so, because that closing edge carries no data. Chapter 13's master
// takes exactly that liberty: its closing edge arrives a fixed two cycles after
// the last capture edge, at every divisor.
//
// So `ratio_err` is gated on intervals that were followed by more clocking, which
// are exactly the intervals a capture depends on. `min_half` is NOT gated: it
// reports the true shortest interval including the closing one, so the closing
// edge stays visible in the measurement even though it does not trip the flag.
// A flag and a number that deliberately disagree, and a reason on the record.
//
// The cost, stated because it is real: in CPHA=1 the final capture IS the closing
// edge, so the one interval excluded there is a data interval. Its edge is still
// recovered -- exclusion costs margin checking on one half-period, not data.
reg [CNT_W-1:0] pend;
reg pend_valid;
wire saturated = (since_edge == {CNT_W{1'b1}});
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_sr <= {SYNC_N{1'b0}};
cs_n_sr <= {SYNC_N{1'b1}}; // deselected, not selected
mosi_sr <= {SYNC_N{1'b0}};
sclk_d <= 1'b0;
cs_active_d <= 1'b0;
since_edge <= {CNT_W{1'b1}}; // "a very long time", saturated
min_half <= {CNT_W{1'b1}};
ratio_err <= 1'b0;
pend <= {CNT_W{1'b1}};
pend_valid <= 1'b0;
end else begin
sclk_sr <= {sclk_sr[SYNC_N-2:0], sclk_pin};
cs_n_sr <= {cs_n_sr[SYNC_N-2:0], cs_n_pin};
mosi_sr <= {mosi_sr[SYNC_N-2:0], mosi_pin};
sclk_d <= sclk_q;
cs_active_d <= cs_active;
if (clr_flags) begin
ratio_err <= 1'b0;
min_half <= {CNT_W{1'b1}};
end
if (any_edge) begin
// The interval that just closed. Reloaded with ONE, not zero,
// because the cycle the edge is detected on is itself the
// first cycle of the next interval -- reloading with zero
// measures every half-period one short, so a master supplying
// exactly HALF_MIN is reported as violating the precondition.
since_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
// The first interval after reset is not a measurement of
// anything, so a saturated counter is ignored.
if (!saturated) begin
if (since_edge < min_half)
min_half <= since_edge;
// Another edge has arrived, so the interval held back is now
// known to have been followed by more clocking -- a half-period,
// and one a capture depended on.
if (pend_valid && pend < HALF_MIN)
ratio_err <= 1'b1;
pend <= since_edge;
pend_valid <= 1'b1;
end else begin
// The interval before the FIRST edge of a transaction is the
// lead, not a half-period. Chapter 14.4 measures that one.
pend_valid <= 1'b0;
end
end else if (!saturated) begin
since_edge <= since_edge + 1'b1;
end
// The transaction is over and no edge followed the interval held back, so
// it was the closing interval and it is discarded. This is the one line
// that makes `ratio_err` a statement about data rather than about the bus.
if (cs_deassert_stb)
pend_valid <= 1'b0;
end
end
`ifdef SPI_CHECKS
// Two recovered edges in one cycle is the failure the precondition exists
// to prevent, and it is not representable: the design would simply not see
// one of them. Stated here so a reviewer knows it was considered.
always_ff @(posedge clk) if (rst_n) begin
if (edge_a_stb && edge_b_stb)
$fatal(1, "both edge strobes on one cycle");
end
`endif
endmodule// spi_slave_frontend.v
//
// Chapter 14.1 -- the clocking assumption, made concrete.
//
// A master owns its clock. A slave does not: SCLK, CS and MOSI all arrive from
// another device, on another clock, with no defined relationship to this one.
// Every decision in Module 14 follows from what is done about that, and there
// are only two answers:
//
// A clock the shift logic ON SCLK, and treat the system side as the
// foreign domain
// B clock everything on the system clock and RECOVER SCLK's edges from it
//
// This module takes B, and this file is where that choice lives. Chapter 15
// builds A and compares them properly; what matters here is that B has a
// PRECONDITION, and a design that does not state it has not made a choice, it
// has made an assumption.
//
// THE PRECONDITION, AND WHY IT IS ABOUT EDGES BEING LOST.
//
// SCLK must be slow enough that each half-period lasts at least HALF_MIN system
// clocks. The reason is not resolution and it is not margin: it is that a
// synchroniser's first flop can be sampled while its input is changing, and the
// value it then resolves to may be the OLD one. At a comfortable ratio that
// costs one cycle of edge latency and nothing else. At a tight ratio the old
// value is still in place when the next sample arrives, and an entire SCLK
// half-period is never seen -- the edge is not late, it is GONE, and the frame
// slips by a bit from there on.
//
// That failure cannot be simulated, because metastability is not modelled: a
// simulator's flop always captures a defined value. So the precondition is a
// stated assumption rather than a tested property -- which is exactly why this
// block MEASURES the ratio and reports it, so the assumption is checkable on
// hardware even though it is unreachable in a testbench.
//
// WHY MOSI GOES THROUGH THE SAME NUMBER OF FLOPS AS SCLK.
//
// This is the part that is usually got wrong, and it is worth being precise
// because the usual explanation is also wrong.
//
// MOSI is not synchronised to avoid metastability in the way a control signal
// is. It is synchronised to be DELAY-MATCHED with the SCLK path. Write S(t)
// and M(t) for the pin values at cycle t, and take SYNC_N = 2:
//
// sclk_q(t) = S(t-2) mosi_q(t) = M(t-2)
//
// The edge detector compares sclk_q(t) against sclk_q(t-1), so a pin edge
// between t-3 and t-2 is detected at cycle t -- and at cycle t, mosi_q holds
// M(t-2), which is MOSI at the moment the edge happened. Delay-matched, and
// therefore the right bit.
//
// Put MOSI through ONE flop instead of two and the sampled value is M(t-1):
// one system clock NEWER than the edge. At a comfortable ratio that is still
// inside the master's hold window and it works. At a tight ratio it is the
// NEXT bit, and the slave reads every byte shifted by one -- intermittently,
// as a function of the frequency ratio, which is the hardest possible thing to
// attribute.
//
// So the two synchroniser depths are not independent choices. They are one
// choice, applied twice.
//
// HOW MUCH THE MISMATCH ACTUALLY COSTS. MOSI changes only on launch edges, so
// it is stable for a full SCLK period with the capture edge in the middle: a
// half-period of setup and a half-period of hold. A depth mismatch of D cycles
// spends D of those, and at HALF_MIN = 3 one stage is a third of the budget --
// which is a third that belonged to the BOARD, to the master's output-valid
// time and to this flop's own setup. A design that spends it on a synchroniser
// asymmetry has spent it on nothing.
//
// And a master is not obliged to hold MOSI for a half-period. Datasheets
// specify an output hold of a few nanoseconds, so a master that changes MOSI
// shortly after the capture edge is legal -- and against that master the
// mismatched design reads the NEXT bit rather than the current one, at every
// ratio. Section 6's testbench drives exactly that master and shows a
// one-stage mismatch failing where the matched path does not.
//
// AND WHY THE RATIO PRECONDITION IS ABOUT CORRECTNESS, NOT MARGIN.
//
// The second flop of a synchroniser gives a settled value. It gives the RIGHT
// value only if the input was stable long enough that "the value" is
// unambiguous -- and MOSI is stable for one SCLK half-period. So the ratio
// requirement is what turns "a settled bit" into "the correct bit", and it is
// the same requirement that keeps two recovered edges out of one cycle.
// One precondition, two consequences, and this block measures it.
module spi_slave_frontend #(
parameter SYNC_N = 2, // synchroniser depth, the SAME for all pins
// Named HALF_MIN rather than MIN_HALF because VHDL identifiers are
// case-insensitive, so a generic MIN_HALF and an output port min_half
// would be the same name -- and the VHDL would compile, with the
// generic silently shadowed. The three languages keep one name.
parameter HALF_MIN = 3, // system clocks per SCLK half-period
parameter CNT_W = 12 // width of the interval measurement
) (
input wire clk,
input wire rst_n,
input wire cpol, // this slave's configured idle level
// --- raw pins, asynchronous to clk ----------------------------------
input wire sclk_pin,
input wire cs_n_pin,
input wire mosi_pin,
// --- recovered, delay-matched ---------------------------------------
output wire sclk_q, // SCLK as this design sees it
output wire cs_active, // chip select, synchronised
output wire mosi_q, // MOSI, matched to the edge strobes
output wire edge_a_stb, // LEADING edge: SCLK left idle
output wire edge_b_stb, // TRAILING edge: SCLK returned
output wire cs_assert_stb,
output wire cs_deassert_stb,
// --- the precondition, measured -------------------------------------
output reg [CNT_W-1:0] min_half, // shortest interval observed, ALL of them
output reg ratio_err, // sticky: a DATA half below HALF_MIN
input wire clr_flags
);
// Three synchroniser chains of identical depth. Declared as one array so a
// future edit cannot lengthen one and not the others -- which is the whole
// failure this file's header is about.
reg [SYNC_N-1:0] sclk_sr;
reg [SYNC_N-1:0] cs_n_sr;
reg [SYNC_N-1:0] mosi_sr;
// One extra stage on SCLK alone, for edge detection. This is NOT part of
// the synchroniser: it is the previous-value register, and it is why the
// strobes appear one cycle after `sclk_q` moves.
reg sclk_d;
assign sclk_q = sclk_sr[SYNC_N-1];
assign cs_active = ~cs_n_sr[SYNC_N-1];
assign mosi_q = mosi_sr[SYNC_N-1];
// LEADING and TRAILING, not rising and falling. Defined against the
// configured idle level, exactly as the master's divider defines them
// (Chapter 13.4), so that CPOL is consumed here and nothing downstream
// sees a voltage direction.
wire away_now = (sclk_q != cpol);
wire away_prev = (sclk_d != cpol);
assign edge_a_stb = away_now & ~away_prev;
assign edge_b_stb = ~away_now & away_prev;
reg cs_active_d;
assign cs_assert_stb = cs_active & ~cs_active_d;
assign cs_deassert_stb = ~cs_active & cs_active_d;
// --- the ratio measurement -------------------------------------------
// Cycles since the last recovered edge. The interval before the FIRST edge
// is unbounded, so the counter saturates rather than wrapping -- a wrap
// would make a very long interval look like a very short one and flag the
// first frame after reset, which is the bug this comment exists to prevent
// (the same one as the master's requirements monitor, Chapter 13.1).
reg [CNT_W-1:0] since_edge;
wire any_edge = edge_a_stb | edge_b_stb;
// The interval that most recently closed, held back one edge. An interval is a
// HALF-PERIOD only once another edge has followed it; until then it might be the
// last interval of the transaction, and the last interval is a different thing.
//
// This distinction is not fussiness, it is a real property of real masters and
// Chapter 14.10 measured it. A master clocking CPHA=0 captures on leading edges
// and then returns SCLK to idle -- and it has no reason to wait a full half-period
// before doing so, because that closing edge carries no data. Chapter 13's master
// takes exactly that liberty: its closing edge arrives a fixed two cycles after
// the last capture edge, at every divisor.
//
// So `ratio_err` is gated on intervals that were followed by more clocking, which
// are exactly the intervals a capture depends on. `min_half` is NOT gated: it
// reports the true shortest interval including the closing one, so the closing
// edge stays visible in the measurement even though it does not trip the flag.
// A flag and a number that deliberately disagree, and a reason on the record.
//
// The cost, stated because it is real: in CPHA=1 the final capture IS the closing
// edge, so the one interval excluded there is a data interval. Its edge is still
// recovered -- exclusion costs margin checking on one half-period, not data.
reg [CNT_W-1:0] pend;
reg pend_valid;
wire saturated = (since_edge == {CNT_W{1'b1}});
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sclk_sr <= {SYNC_N{1'b0}};
cs_n_sr <= {SYNC_N{1'b1}}; // deselected, not selected
mosi_sr <= {SYNC_N{1'b0}};
sclk_d <= 1'b0;
cs_active_d <= 1'b0;
since_edge <= {CNT_W{1'b1}}; // "a very long time", saturated
min_half <= {CNT_W{1'b1}};
ratio_err <= 1'b0;
pend <= {CNT_W{1'b1}};
pend_valid <= 1'b0;
end else begin
sclk_sr <= {sclk_sr[SYNC_N-2:0], sclk_pin};
cs_n_sr <= {cs_n_sr[SYNC_N-2:0], cs_n_pin};
mosi_sr <= {mosi_sr[SYNC_N-2:0], mosi_pin};
sclk_d <= sclk_q;
cs_active_d <= cs_active;
if (clr_flags) begin
ratio_err <= 1'b0;
min_half <= {CNT_W{1'b1}};
end
if (any_edge) begin
// The interval that just closed. Reloaded with ONE, not zero,
// because the cycle the edge is detected on is itself the
// first cycle of the next interval -- reloading with zero
// measures every half-period one short, so a master supplying
// exactly HALF_MIN is reported as violating the precondition.
since_edge <= {{(CNT_W-1){1'b0}}, 1'b1};
// The first interval after reset is not a measurement of
// anything, so a saturated counter is ignored.
if (!saturated) begin
if (since_edge < min_half)
min_half <= since_edge;
// Another edge has arrived, so the interval held back is now
// known to have been followed by more clocking -- a half-period,
// and one a capture depended on.
if (pend_valid && pend < HALF_MIN)
ratio_err <= 1'b1;
pend <= since_edge;
pend_valid <= 1'b1;
end else begin
// The interval before the FIRST edge of a transaction is the
// lead, not a half-period. Chapter 14.4 measures that one.
pend_valid <= 1'b0;
end
end else if (!saturated) begin
since_edge <= since_edge + 1'b1;
end
// The transaction is over and no edge followed the interval held back, so
// it was the closing interval and it is discarded. This is the one line
// that makes `ratio_err` a statement about data rather than about the bus.
if (cs_deassert_stb)
pend_valid <= 1'b0;
end
end
`ifdef SPI_CHECKS
// Two recovered edges in one cycle is the failure the precondition exists
// to prevent, and it is not representable: the design would simply not see
// one of them. Stated here so a reviewer knows it was considered.
always @(posedge clk) if (rst_n) begin
if (edge_a_stb && edge_b_stb)
$fatal(1, "both edge strobes on one cycle");
end
`endif
endmodule-- spi_slave_frontend.vhd
--
-- Chapter 14.1 -- the clocking assumption, made concrete.
--
-- A master owns its clock. A slave does not: SCLK, CS and MOSI all arrive from
-- another device, on another clock, with no defined relationship to this one.
-- Every decision in Module 14 follows from what is done about that, and there
-- are only two answers:
--
-- A clock the shift logic ON SCLK, and treat the system side as foreign
-- B clock everything on the system clock and RECOVER SCLK's edges from it
--
-- This module takes B. Chapter 15 builds A and compares them properly; what
-- matters here is that B has a PRECONDITION, and a design that does not state
-- it has not made a choice, it has made an assumption.
--
-- THE PRECONDITION, AND WHY IT IS ABOUT EDGES BEING LOST.
--
-- SCLK must be slow enough that each half-period lasts at least HALF_MIN system
-- clocks. The reason is not resolution and it is not margin: a synchroniser's
-- first flop can be sampled while its input is changing, and the value it then
-- resolves to may be the OLD one. At a comfortable ratio that costs one cycle of
-- edge latency and nothing else. At a tight ratio the old value is still in
-- place when the next sample arrives, and an entire SCLK half-period is never
-- seen -- the edge is not late, it is GONE, and the frame slips by a bit from
-- there on.
--
-- That failure cannot be simulated, because metastability is not modelled: a
-- simulator's flop always captures a defined value. So the precondition is a
-- stated assumption rather than a tested property -- which is exactly why this
-- block MEASURES the ratio and reports it, so the assumption is checkable on
-- hardware even though it is unreachable in a testbench.
--
-- WHY MOSI GOES THROUGH THE SAME NUMBER OF FLOPS AS SCLK.
--
-- MOSI is not synchronised to avoid metastability in the way a control signal
-- is. It is synchronised to be DELAY-MATCHED with the SCLK path. Write S(t) and
-- M(t) for the pin values at cycle t, and take SYNC_N = 2:
--
-- sclk_q(t) = S(t-2) mosi_q(t) = M(t-2)
--
-- The edge detector compares sclk_q(t) against sclk_q(t-1), so a pin edge
-- between t-3 and t-2 is detected at cycle t -- and at cycle t, mosi_q holds
-- M(t-2), which is MOSI at the moment the edge happened.
--
-- Put MOSI through ONE flop and the sampled value is M(t-1): one system clock
-- NEWER than the edge. So the two synchroniser depths are not independent
-- choices; they are one choice, applied twice.
--
-- HOW MUCH THE MISMATCH COSTS. MOSI changes only on launch edges, so it is
-- stable for a full SCLK period with the capture edge in the middle: a
-- half-period of setup and a half-period of hold. A depth mismatch of D cycles
-- spends D of those, and at HALF_MIN = 3 one stage is a third of a budget that
-- belonged to the BOARD, to the master's output-valid time and to this flop's
-- own setup. And a master is not obliged to hold MOSI for a half-period --
-- datasheets specify a few nanoseconds -- so against a master that releases MOSI
-- shortly after the capture edge the mismatched design reads the NEXT bit, at
-- every ratio.
--
-- The generic is HALF_MIN rather than MIN_HALF because VHDL identifiers are
-- case-insensitive: a generic MIN_HALF and an output port min_half would be the
-- same name, and it would compile with the generic silently shadowed.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_frontend is
generic (
SYNC_N : positive := 2; -- synchroniser depth, the SAME for all pins
HALF_MIN : positive := 3; -- system clocks per SCLK half-period
CNT_W : positive := 12 -- width of the interval measurement
);
port (
clk : in std_logic;
rst_n : in std_logic;
cpol : in std_logic; -- this slave's configured idle level
-- raw pins, asynchronous to clk
sclk_pin : in std_logic;
cs_n_pin : in std_logic;
mosi_pin : in std_logic;
-- recovered, delay-matched
sclk_q : out std_logic;
cs_active : out std_logic;
mosi_q : out std_logic;
edge_a_stb : out std_logic; -- LEADING edge: SCLK left idle
edge_b_stb : out std_logic; -- TRAILING edge: SCLK returned
cs_assert_stb : out std_logic;
cs_deassert_stb : out std_logic;
-- the precondition, measured
min_half : out unsigned(CNT_W - 1 downto 0); -- ALL intervals
ratio_err : out std_logic; -- DATA halves only
clr_flags : in std_logic
);
end entity;
architecture rtl of spi_slave_frontend is
-- Three synchroniser chains of identical depth.
signal sclk_sr : std_logic_vector(SYNC_N - 1 downto 0) := (others => '0');
signal cs_n_sr : std_logic_vector(SYNC_N - 1 downto 0) := (others => '1');
signal mosi_sr : std_logic_vector(SYNC_N - 1 downto 0) := (others => '0');
-- One extra stage on SCLK alone, for edge detection. NOT part of the
-- synchroniser: it is the previous-value register, and it is why the strobes
-- appear one cycle after `sclk_q` moves.
signal sclk_d : std_logic := '0';
signal sclk_i, cs_act_i, mosi_i : std_logic;
signal cs_act_d : std_logic := '0';
signal away_now, away_prev : std_logic;
signal edge_a_i, edge_b_i, any_edge : std_logic;
signal since_edge : unsigned(CNT_W - 1 downto 0) := (others => '1');
signal min_half_r : unsigned(CNT_W - 1 downto 0) := (others => '1');
signal ratio_r : std_logic := '0';
signal saturated : std_logic;
-- The interval that most recently closed, held back one edge. An interval is a
-- HALF-PERIOD only once another edge has followed it; until then it might be the
-- last interval of the transaction, and the last interval is a different thing.
--
-- This distinction is not fussiness, it is a real property of real masters and
-- Chapter 14.10 measured it. A master clocking CPHA=0 captures on leading edges
-- and then returns SCLK to idle -- and it has no reason to wait a full half-period
-- before doing so, because that closing edge carries no data. Chapter 13's master
-- takes exactly that liberty: its closing edge arrives a fixed two cycles after
-- the last capture edge, at every divisor.
--
-- So `ratio_err` is gated on intervals that were followed by more clocking, which
-- are exactly the intervals a capture depends on. `min_half` is NOT gated: it
-- reports the true shortest interval including the closing one, so the closing
-- edge stays visible in the measurement even though it does not trip the flag.
-- A flag and a number that deliberately disagree, and a reason on the record.
--
-- The cost, stated because it is real: in CPHA=1 the final capture IS the closing
-- edge, so the one interval excluded there is a data interval. Its edge is still
-- recovered -- exclusion costs margin checking on one half-period, not data.
signal pend : unsigned(CNT_W - 1 downto 0) := (others => '1');
signal pend_valid : std_logic := '0';
constant ALL_ONES : unsigned(CNT_W - 1 downto 0) := (others => '1');
begin
sclk_i <= sclk_sr(SYNC_N - 1);
cs_act_i <= not cs_n_sr(SYNC_N - 1);
mosi_i <= mosi_sr(SYNC_N - 1);
sclk_q <= sclk_i;
cs_active <= cs_act_i;
mosi_q <= mosi_i;
-- LEADING and TRAILING, not rising and falling. Defined against the
-- configured idle level, exactly as the master's divider defines them
-- (Chapter 13.4), so CPOL is consumed here and nothing downstream sees a
-- voltage direction.
away_now <= '1' when sclk_i /= cpol else '0';
away_prev <= '1' when sclk_d /= cpol else '0';
edge_a_i <= away_now and (not away_prev);
edge_b_i <= (not away_now) and away_prev;
any_edge <= edge_a_i or edge_b_i;
edge_a_stb <= edge_a_i;
edge_b_stb <= edge_b_i;
cs_assert_stb <= cs_act_i and (not cs_act_d);
cs_deassert_stb <= (not cs_act_i) and cs_act_d;
saturated <= '1' when since_edge = ALL_ONES else '0';
min_half <= min_half_r;
ratio_err <= ratio_r;
sync : process (clk, rst_n)
begin
if rst_n = '0' then
sclk_sr <= (others => '0');
cs_n_sr <= (others => '1'); -- deselected, not selected
mosi_sr <= (others => '0');
sclk_d <= '0';
cs_act_d <= '0';
since_edge <= (others => '1'); -- "a very long time", saturated
min_half_r <= (others => '1');
pend <= (others => '1');
pend_valid <= '0';
ratio_r <= '0';
elsif rising_edge(clk) then
if SYNC_N = 1 then
sclk_sr(0) <= sclk_pin;
cs_n_sr(0) <= cs_n_pin;
mosi_sr(0) <= mosi_pin;
else
sclk_sr <= sclk_sr(SYNC_N - 2 downto 0) & sclk_pin;
cs_n_sr <= cs_n_sr(SYNC_N - 2 downto 0) & cs_n_pin;
mosi_sr <= mosi_sr(SYNC_N - 2 downto 0) & mosi_pin;
end if;
sclk_d <= sclk_i;
cs_act_d <= cs_act_i;
if clr_flags = '1' then
ratio_r <= '0';
min_half_r <= (others => '1');
end if;
if any_edge = '1' then
-- Reloaded with ONE, not zero: the cycle the edge is detected on
-- is itself the first cycle of the next interval, so reloading
-- with zero measures every half-period one short and a master
-- supplying exactly HALF_MIN is reported as violating it.
since_edge <= to_unsigned(1, CNT_W);
-- The first interval after reset is not a measurement of
-- anything, so a saturated counter is ignored. A wrap instead of
-- a saturate would make a very long interval look very short and
-- flag the first frame after reset.
if saturated = '0' then
if since_edge < min_half_r then
min_half_r <= since_edge;
end if;
-- Another edge has arrived, so the interval held back is now
-- known to have been followed by more clocking -- a half-period,
-- and one a capture depended on.
if pend_valid = '1' and to_integer(pend) < HALF_MIN then
ratio_r <= '1';
end if;
pend <= since_edge;
pend_valid <= '1';
else
-- The interval before the FIRST edge of a transaction is the
-- lead, not a half-period. Chapter 14.4 measures that one.
pend_valid <= '0';
end if;
elsif saturated = '0' then
since_edge <= since_edge + 1;
end if;
-- The transaction is over and no edge followed the interval held back, so
-- it was the closing interval and it is discarded. This is the one line
-- that makes `ratio_err` a statement about data rather than about the bus.
if (not cs_act_i) = '1' and cs_act_d = '1' then
pend_valid <= '0';
end if;
end if;
end process;
-- Two recovered edges in one cycle is the failure the precondition exists to
-- prevent, and it is not representable: the design would simply not see one
-- of them. Stated here so a reviewer knows it was considered.
check : process (clk)
begin
if rising_edge(clk) and rst_n = '1' then
assert not (edge_a_i = '1' and edge_b_i = '1')
report "both edge strobes on one cycle" severity failure;
end if;
end process;
end architecture;The testbench
The bench has to do five things, and the fifth is the one worth reading.
- Edge recovery at many ratios. Every driven half-period must produce exactly one recovered edge, at ratios from 2 to 16 and under both polarities. A count, not a spot check.
- The sequence is polarity-independent.
edge_a_stbthenedge_b_stb, in that order, identically underCPOL = 0andCPOL = 1— which is the property that lets 14.6 ignore CPOL. - The measurement is exact.
min_halfmust equal the driven half-period at every ratio, and the unbounded interval after reset must be excluded rather than counted as a very short one. - The precondition is reported.
HALF_MIN - 1sets the flag and latches it; exactlyHALF_MINdoes not. The off-by-one in the reload value is what test 3 and test 4 jointly pin down. - The delay-matching experiment. Two samplers run side by side on the same stimulus: the matched one, and a deliberately mismatched one-flop version. Against a generous master — MOSI held for a full SCLK period — they agree completely, which is the point: the bug is invisible against a well-behaved master. Then the bench drives a master whose MOSI hold is a single system clock, which is legal, and the mismatched sampler reads 45 of 96 bits wrong while the matched one reads all 96 correctly.
Test 5 is the one that would have caught this in a real project, and it is worth noticing why: it required writing a legal but unhelpful master. A bench that only ever drives a generous master proves the mismatched design correct.
// spi_slave_frontend_tb.sv
//
// The pins are driven cycle by cycle on the system clock, so the frequency
// ratio is an exact integer the test controls rather than a consequence of two
// independent clock generators. That matters: the whole subject is what happens
// at particular ratios, and a testbench that cannot name the ratio cannot test
// it.
//
// The central experiment is the DELAY-MATCHING one. Two samplers watch the same
// MOSI pin: the design's, which is SYNC_N flops deep and therefore matched to
// the SCLK path, and a deliberately WRONG one a single flop deep. Both sample
// at the design's own capture strobe. Against a generous master -- MOSI held
// for a full SCLK period -- the two agree, which is why the mistake survives
// review. Against a master that releases MOSI shortly after the capture edge,
// which every datasheet permits, the shallow sampler reads the NEXT bit.
`timescale 1ns/1ps
module spi_slave_frontend_tb;
localparam int SYNC_N = 2;
localparam int HALF_MIN = 3;
localparam int CNT_W = 12;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
logic cpol = 1'b0;
logic sclk_pin = 1'b0;
logic cs_n_pin = 1'b1;
logic mosi_pin = 1'b0;
logic clr_flags = 1'b0;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb;
wire cs_assert_stb, cs_deassert_stb;
wire [CNT_W-1:0] min_half;
wire ratio_err;
spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(HALF_MIN), .CNT_W(CNT_W))
dut (
.clk(clk), .rst_n(rst_n),
.cpol(cpol),
.sclk_pin(sclk_pin), .cs_n_pin(cs_n_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q), .cs_active(cs_active), .mosi_q(mosi_q),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.min_half(min_half), .ratio_err(ratio_err), .clr_flags(clr_flags)
);
// --- the deliberately WRONG sampler -----------------------------------
// One flop instead of SYNC_N. Everything else identical, including which
// strobe it samples on, so the only difference under test is the depth.
logic mosi_1flop;
always_ff @(posedge clk) mosi_1flop <= mosi_pin;
// --- monitors ----------------------------------------------------------
logic cpha_now = 1'b0;
wire cap_stb = cpha_now ? edge_b_stb : edge_a_stb;
integer n_a, n_b, n_cap, n_assert, n_deassert;
integer both_edges; // must stay zero at every legal ratio
integer matched_bad, shallow_bad;
// The expectation is an INDEXED WORD, not a live signal. A live expected-bit
// driven in pin time and compared in recovered time is off by the
// synchroniser latency, which produces false failures at exactly the tight
// ratios the test exists to exercise -- and a "fix" that delays the
// expectation by hand bakes the latency into the test.
logic [31:0] exp_word;
integer exp_len;
integer cap_idx;
// Recording is armed only once the pins have settled. Changing `cpol`
// between frames moves the DUT's notion of "away from idle" without SCLK
// moving at all, which is a recovered edge -- real, and not part of any
// frame.
logic rec_arm = 1'b0;
integer ev_n;
integer ev_code [0:255]; // 1 = leading, 2 = trailing
always_ff @(posedge clk) begin
if (rst_n && rec_arm) begin
if (edge_a_stb && edge_b_stb) both_edges <= both_edges + 1;
if (edge_a_stb) begin
n_a <= n_a + 1;
if (ev_n < 256) ev_code[ev_n] <= 1;
ev_n <= ev_n + 1;
end
if (edge_b_stb) begin
n_b <= n_b + 1;
if (ev_n < 256) ev_code[ev_n] <= 2;
ev_n <= ev_n + 1;
end
if (cs_assert_stb) begin
n_assert <= n_assert + 1;
cap_idx <= 0;
end
if (cs_deassert_stb) n_deassert <= n_deassert + 1;
if (cap_stb) begin
n_cap <= n_cap + 1;
if (cap_idx < exp_len) begin
if (mosi_q !== exp_word[exp_len-1-cap_idx])
matched_bad <= matched_bad + 1;
if (mosi_1flop !== exp_word[exp_len-1-cap_idx])
shallow_bad <= shallow_bad + 1;
end
cap_idx <= cap_idx + 1;
end
end
end
integer errors = 0;
task automatic clear_counts;
begin
n_a = 0; n_b = 0; n_cap = 0; n_assert = 0; n_deassert = 0;
matched_bad = 0; shallow_bad = 0; ev_n = 0;
end
endtask
task automatic adv(input integer n);
begin
repeat (n) @(negedge clk);
end
endtask
// A behavioural master driven on the system clock, so `half` is exact.
//
// `tight_hold` models a master whose MOSI hold after the capture edge is a
// few nanoseconds rather than a half-period -- which is what datasheets
// actually specify, and which is the case that separates a matched sampler
// from a shallow one.
task automatic drive_frame(input integer half, input bit pol, input bit pha,
input integer nbits, input [31:0] data,
input bit tight_hold);
integer i;
begin
// Settle the pins at the new polarity BEFORE arming the recorder.
rec_arm = 1'b0;
cpol = pol;
cpha_now = pha;
sclk_pin = pol;
cs_n_pin = 1'b1;
adv(6);
exp_word = data;
exp_len = nbits;
rec_arm = 1'b1;
cs_n_pin = 1'b0;
if (!pha)
// CPHA=0 captures on the FIRST leading edge, so the first bit
// must be on the wire before any edge exists.
mosi_pin = data[nbits-1];
adv(4); // the master's lead time
for (i = 0; i < nbits; i = i + 1) begin
// ---- leading edge ----
sclk_pin = ~pol;
if (pha) begin
// CPHA=1 launches here; the capture is a half-period later.
mosi_pin = data[nbits-1-i];
adv(half);
end else if (tight_hold && i < nbits - 1) begin
// This IS the capture edge. A sloppy master lets MOSI go one
// system clock later -- legal by any datasheet's hold spec.
adv(1);
mosi_pin = data[nbits-2-i];
adv(half - 1);
end else begin
adv(half);
end
// ---- trailing edge ----
sclk_pin = pol;
if (!pha) begin
if (!tight_hold && i < nbits - 1)
mosi_pin = data[nbits-2-i];
adv(half);
end else if (tight_hold) begin
// This IS the capture edge in CPHA=1.
adv(1);
if (i < nbits - 1) mosi_pin = data[nbits-2-i];
adv(half - 1);
end else begin
adv(half);
end
end
adv(4); // the master's lag
cs_n_pin = 1'b1;
adv(6); // and its gap
rec_arm = 1'b0;
end
endtask
// A bare clock train whose CLOSING interval is deliberately short, which is what
// a real master produces: it has no reason to hold SCLK away from idle for a full
// half-period after the last capture, because that closing edge carries no data.
// No data is checked here -- the recorder stays disarmed -- because the subject is
// the measurement and the flag, not the bits.
task automatic drive_train_short_close(input integer half, input integer close,
input integer nbits);
integer i;
begin
rec_arm = 1'b0;
cpol = 1'b0;
cpha_now = 1'b0;
sclk_pin = 1'b0;
cs_n_pin = 1'b1;
adv(6);
cs_n_pin = 1'b0;
adv(4);
for (i = 0; i < nbits; i = i + 1) begin
sclk_pin = 1'b1;
// The LAST return to idle comes early, and only that one, so it is
// the interval CLOSED BY the final edge that is short.
adv((i == nbits - 1) ? close : half);
sclk_pin = 1'b0;
adv(half);
end
adv(4);
cs_n_pin = 1'b1;
adv(6);
end
endtask
integer halves [0:5];
integer h, p, k, seed, bad;
logic [31:0] pat;
initial begin
clear_counts();
both_edges = 0;
exp_word = 32'h0; exp_len = 0; cap_idx = 0;
halves[0] = 2; halves[1] = 3; halves[2] = 4;
halves[3] = 6; halves[4] = 8; halves[5] = 16;
seed = 32'h51A5_E001;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. EDGE RECOVERY. Every driven half-period must produce exactly one
// recovered edge, at every ratio and both polarities.
for (h = 0; h < 6; h = h + 1)
for (p = 0; p <= 1; p = p + 1) begin
clear_counts();
drive_frame(halves[h], p[0], 1'b0, 8, 32'hA5, 1'b0);
if (n_a != 8 || n_b != 8) begin
$display(" FAIL: half=%0d cpol=%0d recovered %0d leading and %0d trailing edges, expected 8 and 8",
halves[h], p, n_a, n_b);
errors = errors + 1;
end
if (n_assert != 1 || n_deassert != 1) begin
$display(" FAIL: half=%0d cpol=%0d saw %0d asserts and %0d deasserts",
halves[h], p, n_assert, n_deassert);
errors = errors + 1;
end
if (matched_bad != 0) begin
$display(" FAIL: half=%0d cpol=%0d matched sampler read %0d wrong bits",
halves[h], p, matched_bad);
errors = errors + 1;
end
end
$display(" edge recovery: 12 (half, polarity) pairs, 8 leading and 8 trailing edges each, one assert and one deassert");
// 2. THE EDGE SEQUENCE IS THE SAME UNDER BOTH POLARITIES. Leading means
// away from idle, so inverting CPOL inverts the pin and reorders
// nothing -- the same claim the master's divider makes.
clear_counts();
drive_frame(4, 1'b0, 1'b0, 8, 32'h3C, 1'b0);
bad = ev_n;
for (k = 0; k < ev_n; k = k + 1)
if (ev_code[k] != ((k % 2 == 0) ? 1 : 2)) begin
$display(" FAIL: CPOL=0 event %0d was %0d", k, ev_code[k]);
errors = errors + 1;
end
clear_counts();
drive_frame(4, 1'b1, 1'b0, 8, 32'h3C, 1'b0);
if (ev_n != bad) begin
$display(" FAIL: CPOL=1 produced %0d events where CPOL=0 produced %0d",
ev_n, bad);
errors = errors + 1;
end
for (k = 0; k < ev_n; k = k + 1)
if (ev_code[k] != ((k % 2 == 0) ? 1 : 2)) begin
$display(" FAIL: CPOL=1 event %0d was %0d", k, ev_code[k]);
errors = errors + 1;
end
$display(" both polarities produced %0d events in strict leading-then-trailing order", ev_n);
// 3. THE RATIO MEASUREMENT. `min_half` must equal the half-period that
// was actually driven, and the first interval after reset -- which is
// unbounded -- must not be counted as a measurement.
for (h = 0; h < 6; h = h + 1) begin
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
clear_counts();
drive_frame(halves[h], 1'b0, 1'b0, 8, 32'h5A, 1'b0);
if (min_half != halves[h]) begin
$display(" FAIL: driving half=%0d measured min_half=%0d",
halves[h], min_half);
errors = errors + 1;
end
end
$display(" the measured shortest half-period matched the driven one at every ratio from 2 to 16");
// 4. THE PRECONDITION IS REPORTED. Below HALF_MIN the flag must set; at
// and above it must not.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_frame(HALF_MIN, 1'b0, 1'b0, 8, 32'hFF, 1'b0);
if (ratio_err) begin
$display(" FAIL: a half-period of exactly %0d was reported as too fast",
HALF_MIN);
errors = errors + 1;
end
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_frame(HALF_MIN - 1, 1'b0, 1'b0, 8, 32'hFF, 1'b0);
if (!ratio_err) begin
$display(" FAIL: a half-period of %0d was not reported", HALF_MIN - 1);
errors = errors + 1;
end
if (min_half != HALF_MIN - 1) begin
$display(" FAIL: the too-fast half measured %0d, expected %0d",
min_half, HALF_MIN - 1);
errors = errors + 1;
end
// And it is sticky: a subsequent legal frame must not clear it.
drive_frame(8, 1'b0, 1'b0, 8, 32'hFF, 1'b0);
if (!ratio_err) begin
$display(" FAIL: a legal frame cleared the ratio report");
errors = errors + 1;
end
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
adv(2);
if (ratio_err) begin
$display(" FAIL: the ratio report did not clear on command");
errors = errors + 1;
end
$display(" a half-period of %0d is accepted, %0d is reported and latched, and the report clears only on command",
HALF_MIN, HALF_MIN - 1);
// 4b. THE CLOSING INTERVAL IS MEASURED BUT NOT JUDGED, which is the one place
// the flag and the measurement are meant to disagree. A master that clocks
// legally and then returns SCLK to idle early has violated nothing a
// capture depended on -- the closing edge carries no data in CPHA=0 -- so
// the flag must stay clear while the number must still show what happened.
//
// This is not a hypothetical. Chapter 14.10 wired this slave to the master
// of Chapter 13.11 and measured a closing interval of exactly two cycles at
// every divisor, because that master ends a transaction on its final
// capture. A front end that judged every interval would have reported a
// fault on every transaction that ever worked.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_train_short_close(8, HALF_MIN - 1, 8);
if (ratio_err) begin
$display(" FAIL: a short CLOSING interval was reported as a ratio violation, which would fire on every transaction a real master ever sends");
errors = errors + 1;
end
if (min_half != HALF_MIN - 1) begin
$display(" FAIL: the short closing interval measured %0d, expected %0d -- it must remain visible in the measurement even though it does not trip the flag",
min_half, HALF_MIN - 1);
errors = errors + 1;
end
$display(" a clock train of half-period 8 that returns SCLK to idle after only %0d cycles: the flag stays clear because no capture depended on that interval, and the measurement still reports %0d -- the number tells the truth and the flag makes a judgement, and they are allowed to differ",
HALF_MIN - 1, min_half);
// 4c. AND A SHORT INTERIOR INTERVAL IS STILL REPORTED, so 4b removed one
// interval from the judgement and not the judgement itself.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_train_short_close(HALF_MIN - 1, 8, 8);
if (!ratio_err) begin
$display(" FAIL: a train with short INTERIOR half-periods and a generous closing one was not reported");
errors = errors + 1;
end
$display(" the same train with the shortness moved INSIDE -- interior halves of %0d and a generous closing interval -- is reported, so excluding the closing interval cost exactly one interval of judgement and nothing more",
HALF_MIN - 1);
// 5. THE DELAY-MATCHING EXPERIMENT.
// (a) a generous master -- MOSI held for a full SCLK period. Both
// samplers agree, which is why a shallow MOSI path survives
// review.
clear_counts();
for (h = 0; h < 6; h = h + 1)
for (p = 0; p <= 1; p = p + 1) begin
seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
pat = seed & 32'hFF;
bad = matched_bad;
drive_frame(halves[h], 1'b0, p[0], 8, pat, 1'b0);
if (matched_bad != bad)
$display(" (generous, half=%0d cpha=%0d: %0d wrong)",
halves[h], p, matched_bad - bad);
end
if (matched_bad != 0 || shallow_bad != 0) begin
$display(" FAIL: against a generous master the matched sampler missed %0d bits and the shallow one %0d",
matched_bad, shallow_bad);
errors = errors + 1;
end
$display(" generous master, %0d captures: both samplers correct -- the shallow one is indistinguishable here",
n_cap);
// (b) a master whose MOSI hold is one system clock. Legal by any
// datasheet, and the shallow sampler now reads the next bit.
clear_counts();
for (h = 0; h < 6; h = h + 1)
for (p = 0; p <= 1; p = p + 1) begin
seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
pat = seed & 32'hFF;
bad = matched_bad;
drive_frame(halves[h], 1'b0, p[0], 8, pat, 1'b1);
if (matched_bad != bad)
$display(" (short hold, half=%0d cpha=%0d: %0d wrong)",
halves[h], p, matched_bad - bad);
end
if (matched_bad != 0) begin
$display(" FAIL: against a short-hold master the matched sampler missed %0d bits",
matched_bad);
errors = errors + 1;
end
if (shallow_bad == 0) begin
$display(" FAIL: the shallow sampler read every bit correctly -- the experiment proves nothing");
errors = errors + 1;
end
$display(" short-hold master, %0d captures: matched sampler %0d wrong, shallow sampler %0d wrong",
n_cap, matched_bad, shallow_bad);
// 6. NO TWO RECOVERED EDGES IN ONE CYCLE, across the whole run.
if (both_edges != 0) begin
$display(" FAIL: %0d cycles carried both edge strobes", both_edges);
errors = errors + 1;
end
$display(" across the whole run no cycle carried both edge strobes");
if (errors == 0)
$display("PASS: SCLK, CS and MOSI are recovered through synchroniser chains of identical depth, so every recovered edge arrives delay-matched with the MOSI value that accompanied it -- every driven half-period produces exactly one recovered edge at ratios from 2 to 16 system clocks and under both polarities, the recovered sequence is strictly leading-then-trailing and identical under both polarities because leading is defined against the configured idle level, the measured shortest half-period matches the driven one exactly at every ratio with the unbounded interval after reset correctly excluded, a half-period below the stated precondition is reported and latched while one exactly at it is accepted, the CLOSING interval of a transaction is measured but not judged -- because no capture depends on it and a real master does return SCLK to idle early, as Chapter 14.10 measured -- while the same shortness moved one interval inside is still reported, and against a master whose MOSI hold is a single system clock the matched sampler reads every bit correctly while a one-flop sampler -- indistinguishable against a generous master -- reads %0d of %0d wrong", shallow_bad, n_cap);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_slave_frontend_tb.v
//
// The pins are driven cycle by cycle on the system clock, so the frequency
// ratio is an exact integer the test controls rather than a consequence of two
// independent clock generators. That matters: the whole subject is what happens
// at particular ratios, and a testbench that cannot name the ratio cannot test
// it.
//
// The central experiment is the DELAY-MATCHING one. Two samplers watch the same
// MOSI pin: the design's, which is SYNC_N flops deep and therefore matched to
// the SCLK path, and a deliberately WRONG one a single flop deep. Both sample
// at the design's own capture strobe. Against a generous master -- MOSI held
// for a full SCLK period -- the two agree, which is why the mistake survives
// review. Against a master that releases MOSI shortly after the capture edge,
// which every datasheet permits, the shallow sampler reads the NEXT bit.
`timescale 1ns/1ps
module spi_slave_frontend_tb;
localparam SYNC_N = 2;
localparam HALF_MIN = 3;
localparam CNT_W = 12;
reg clk;
reg rst_n;
always #5 clk = ~clk;
reg cpol;
reg sclk_pin;
reg cs_n_pin;
reg mosi_pin;
reg clr_flags;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb;
wire cs_assert_stb, cs_deassert_stb;
wire [CNT_W-1:0] min_half;
wire ratio_err;
spi_slave_frontend #(.SYNC_N(SYNC_N), .HALF_MIN(HALF_MIN), .CNT_W(CNT_W))
dut (
.clk(clk), .rst_n(rst_n),
.cpol(cpol),
.sclk_pin(sclk_pin), .cs_n_pin(cs_n_pin), .mosi_pin(mosi_pin),
.sclk_q(sclk_q), .cs_active(cs_active), .mosi_q(mosi_q),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.min_half(min_half), .ratio_err(ratio_err), .clr_flags(clr_flags)
);
// --- the deliberately WRONG sampler -----------------------------------
// One flop instead of SYNC_N. Everything else identical, including which
// strobe it samples on, so the only difference under test is the depth.
reg mosi_1flop;
always @(posedge clk) mosi_1flop <= mosi_pin;
// --- monitors ----------------------------------------------------------
reg cpha_now;
wire cap_stb = cpha_now ? edge_b_stb : edge_a_stb;
integer n_a, n_b, n_cap, n_assert, n_deassert;
integer both_edges; // must stay zero at every legal ratio
integer matched_bad, shallow_bad;
// The expectation is an INDEXED WORD, not a live signal. A live expected-bit
// driven in pin time and compared in recovered time is off by the
// synchroniser latency, which produces false failures at exactly the tight
// ratios the test exists to exercise -- and a "fix" that delays the
// expectation by hand bakes the latency into the test.
reg [31:0] exp_word;
integer exp_len;
integer cap_idx;
// Recording is armed only once the pins have settled. Changing `cpol`
// between frames moves the DUT's notion of "away from idle" without SCLK
// moving at all, which is a recovered edge -- real, and not part of any
// frame.
reg rec_arm;
integer ev_n;
integer ev_code [0:255]; // 1 = leading, 2 = trailing
always @(posedge clk) begin
if (rst_n && rec_arm) begin
if (edge_a_stb && edge_b_stb) both_edges <= both_edges + 1;
if (edge_a_stb) begin
n_a <= n_a + 1;
if (ev_n < 256) ev_code[ev_n] <= 1;
ev_n <= ev_n + 1;
end
if (edge_b_stb) begin
n_b <= n_b + 1;
if (ev_n < 256) ev_code[ev_n] <= 2;
ev_n <= ev_n + 1;
end
if (cs_assert_stb) begin
n_assert <= n_assert + 1;
cap_idx <= 0;
end
if (cs_deassert_stb) n_deassert <= n_deassert + 1;
if (cap_stb) begin
n_cap <= n_cap + 1;
if (cap_idx < exp_len) begin
if (mosi_q !== exp_word[exp_len-1-cap_idx])
matched_bad <= matched_bad + 1;
if (mosi_1flop !== exp_word[exp_len-1-cap_idx])
shallow_bad <= shallow_bad + 1;
end
cap_idx <= cap_idx + 1;
end
end
end
integer errors;
task clear_counts;
begin
n_a = 0; n_b = 0; n_cap = 0; n_assert = 0; n_deassert = 0;
matched_bad = 0; shallow_bad = 0; ev_n = 0;
end
endtask
task adv;
input integer n;
begin
repeat (n) @(negedge clk);
end
endtask
// A behavioural master driven on the system clock, so `half` is exact.
//
// `tight_hold` models a master whose MOSI hold after the capture edge is a
// few nanoseconds rather than a half-period -- which is what datasheets
// actually specify, and which is the case that separates a matched sampler
// from a shallow one.
task drive_frame;
input integer half;
input pol;
input pha;
input integer nbits;
input [31:0] data;
input tight_hold;
integer i;
begin
// Settle the pins at the new polarity BEFORE arming the recorder.
rec_arm = 1'b0;
cpol = pol;
cpha_now = pha;
sclk_pin = pol;
cs_n_pin = 1'b1;
adv(6);
exp_word = data;
exp_len = nbits;
rec_arm = 1'b1;
cs_n_pin = 1'b0;
if (!pha)
// CPHA=0 captures on the FIRST leading edge, so the first bit
// must be on the wire before any edge exists.
mosi_pin = data[nbits-1];
adv(4); // the master's lead time
for (i = 0; i < nbits; i = i + 1) begin
// ---- leading edge ----
sclk_pin = ~pol;
if (pha) begin
// CPHA=1 launches here; the capture is a half-period later.
mosi_pin = data[nbits-1-i];
adv(half);
end else if (tight_hold && i < nbits - 1) begin
// This IS the capture edge. A sloppy master lets MOSI go one
// system clock later -- legal by any datasheet's hold spec.
adv(1);
mosi_pin = data[nbits-2-i];
adv(half - 1);
end else begin
adv(half);
end
// ---- trailing edge ----
sclk_pin = pol;
if (!pha) begin
if (!tight_hold && i < nbits - 1)
mosi_pin = data[nbits-2-i];
adv(half);
end else if (tight_hold) begin
// This IS the capture edge in CPHA=1.
adv(1);
if (i < nbits - 1) mosi_pin = data[nbits-2-i];
adv(half - 1);
end else begin
adv(half);
end
end
adv(4); // the master's lag
cs_n_pin = 1'b1;
adv(6); // and its gap
rec_arm = 1'b0;
end
endtask
// A bare clock train whose CLOSING interval is deliberately short, which is what
// a real master produces: it has no reason to hold SCLK away from idle for a full
// half-period after the last capture, because that closing edge carries no data.
// No data is checked here -- the recorder stays disarmed -- because the subject is
// the measurement and the flag, not the bits.
task drive_train_short_close;
input integer half;
input integer close;
input integer nbits;
integer i;
begin
rec_arm = 1'b0;
cpol = 1'b0;
cpha_now = 1'b0;
sclk_pin = 1'b0;
cs_n_pin = 1'b1;
adv(6);
cs_n_pin = 1'b0;
adv(4);
for (i = 0; i < nbits; i = i + 1) begin
sclk_pin = 1'b1;
// The LAST return to idle comes early, and only that one, so it is
// the interval CLOSED BY the final edge that is short.
adv((i == nbits - 1) ? close : half);
sclk_pin = 1'b0;
adv(half);
end
adv(4);
cs_n_pin = 1'b1;
adv(6);
end
endtask
integer halves [0:5];
integer h, p, k, seed, bad;
reg [31:0] pat;
initial begin
clear_counts();
both_edges = 0;
exp_word = 32'h0; exp_len = 0; cap_idx = 0;
halves[0] = 2; halves[1] = 3; halves[2] = 4;
halves[3] = 6; halves[4] = 8; halves[5] = 16;
seed = 32'h51A5_E001;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. EDGE RECOVERY. Every driven half-period must produce exactly one
// recovered edge, at every ratio and both polarities.
for (h = 0; h < 6; h = h + 1)
for (p = 0; p <= 1; p = p + 1) begin
clear_counts();
drive_frame(halves[h], p[0], 1'b0, 8, 32'hA5, 1'b0);
if (n_a != 8 || n_b != 8) begin
$display(" FAIL: half=%0d cpol=%0d recovered %0d leading and %0d trailing edges, expected 8 and 8",
halves[h], p, n_a, n_b);
errors = errors + 1;
end
if (n_assert != 1 || n_deassert != 1) begin
$display(" FAIL: half=%0d cpol=%0d saw %0d asserts and %0d deasserts",
halves[h], p, n_assert, n_deassert);
errors = errors + 1;
end
if (matched_bad != 0) begin
$display(" FAIL: half=%0d cpol=%0d matched sampler read %0d wrong bits",
halves[h], p, matched_bad);
errors = errors + 1;
end
end
$display(" edge recovery: 12 (half, polarity) pairs, 8 leading and 8 trailing edges each, one assert and one deassert");
// 2. THE EDGE SEQUENCE IS THE SAME UNDER BOTH POLARITIES. Leading means
// away from idle, so inverting CPOL inverts the pin and reorders
// nothing -- the same claim the master's divider makes.
clear_counts();
drive_frame(4, 1'b0, 1'b0, 8, 32'h3C, 1'b0);
bad = ev_n;
for (k = 0; k < ev_n; k = k + 1)
if (ev_code[k] != ((k % 2 == 0) ? 1 : 2)) begin
$display(" FAIL: CPOL=0 event %0d was %0d", k, ev_code[k]);
errors = errors + 1;
end
clear_counts();
drive_frame(4, 1'b1, 1'b0, 8, 32'h3C, 1'b0);
if (ev_n != bad) begin
$display(" FAIL: CPOL=1 produced %0d events where CPOL=0 produced %0d",
ev_n, bad);
errors = errors + 1;
end
for (k = 0; k < ev_n; k = k + 1)
if (ev_code[k] != ((k % 2 == 0) ? 1 : 2)) begin
$display(" FAIL: CPOL=1 event %0d was %0d", k, ev_code[k]);
errors = errors + 1;
end
$display(" both polarities produced %0d events in strict leading-then-trailing order", ev_n);
// 3. THE RATIO MEASUREMENT. `min_half` must equal the half-period that
// was actually driven, and the first interval after reset -- which is
// unbounded -- must not be counted as a measurement.
for (h = 0; h < 6; h = h + 1) begin
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
clear_counts();
drive_frame(halves[h], 1'b0, 1'b0, 8, 32'h5A, 1'b0);
if (min_half != halves[h]) begin
$display(" FAIL: driving half=%0d measured min_half=%0d",
halves[h], min_half);
errors = errors + 1;
end
end
$display(" the measured shortest half-period matched the driven one at every ratio from 2 to 16");
// 4. THE PRECONDITION IS REPORTED. Below HALF_MIN the flag must set; at
// and above it must not.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_frame(HALF_MIN, 1'b0, 1'b0, 8, 32'hFF, 1'b0);
if (ratio_err) begin
$display(" FAIL: a half-period of exactly %0d was reported as too fast",
HALF_MIN);
errors = errors + 1;
end
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_frame(HALF_MIN - 1, 1'b0, 1'b0, 8, 32'hFF, 1'b0);
if (!ratio_err) begin
$display(" FAIL: a half-period of %0d was not reported", HALF_MIN - 1);
errors = errors + 1;
end
if (min_half != HALF_MIN - 1) begin
$display(" FAIL: the too-fast half measured %0d, expected %0d",
min_half, HALF_MIN - 1);
errors = errors + 1;
end
// And it is sticky: a subsequent legal frame must not clear it.
drive_frame(8, 1'b0, 1'b0, 8, 32'hFF, 1'b0);
if (!ratio_err) begin
$display(" FAIL: a legal frame cleared the ratio report");
errors = errors + 1;
end
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
adv(2);
if (ratio_err) begin
$display(" FAIL: the ratio report did not clear on command");
errors = errors + 1;
end
$display(" a half-period of %0d is accepted, %0d is reported and latched, and the report clears only on command",
HALF_MIN, HALF_MIN - 1);
// 4b. THE CLOSING INTERVAL IS MEASURED BUT NOT JUDGED, which is the one place
// the flag and the measurement are meant to disagree. A master that clocks
// legally and then returns SCLK to idle early has violated nothing a
// capture depended on -- the closing edge carries no data in CPHA=0 -- so
// the flag must stay clear while the number must still show what happened.
//
// This is not a hypothetical. Chapter 14.10 wired this slave to the master
// of Chapter 13.11 and measured a closing interval of exactly two cycles at
// every divisor, because that master ends a transaction on its final
// capture. A front end that judged every interval would have reported a
// fault on every transaction that ever worked.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_train_short_close(8, HALF_MIN - 1, 8);
if (ratio_err) begin
$display(" FAIL: a short CLOSING interval was reported as a ratio violation, which would fire on every transaction a real master ever sends");
errors = errors + 1;
end
if (min_half != HALF_MIN - 1) begin
$display(" FAIL: the short closing interval measured %0d, expected %0d -- it must remain visible in the measurement even though it does not trip the flag",
min_half, HALF_MIN - 1);
errors = errors + 1;
end
$display(" a clock train of half-period 8 that returns SCLK to idle after only %0d cycles: the flag stays clear because no capture depended on that interval, and the measurement still reports %0d -- the number tells the truth and the flag makes a judgement, and they are allowed to differ",
HALF_MIN - 1, min_half);
// 4c. AND A SHORT INTERIOR INTERVAL IS STILL REPORTED, so 4b removed one
// interval from the judgement and not the judgement itself.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_train_short_close(HALF_MIN - 1, 8, 8);
if (!ratio_err) begin
$display(" FAIL: a train with short INTERIOR half-periods and a generous closing one was not reported");
errors = errors + 1;
end
$display(" the same train with the shortness moved INSIDE -- interior halves of %0d and a generous closing interval -- is reported, so excluding the closing interval cost exactly one interval of judgement and nothing more",
HALF_MIN - 1);
// 5. THE DELAY-MATCHING EXPERIMENT.
// (a) a generous master -- MOSI held for a full SCLK period. Both
// samplers agree, which is why a shallow MOSI path survives
// review.
clear_counts();
for (h = 0; h < 6; h = h + 1)
for (p = 0; p <= 1; p = p + 1) begin
seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
pat = seed & 32'hFF;
bad = matched_bad;
drive_frame(halves[h], 1'b0, p[0], 8, pat, 1'b0);
if (matched_bad != bad)
$display(" (generous, half=%0d cpha=%0d: %0d wrong)",
halves[h], p, matched_bad - bad);
end
if (matched_bad != 0 || shallow_bad != 0) begin
$display(" FAIL: against a generous master the matched sampler missed %0d bits and the shallow one %0d",
matched_bad, shallow_bad);
errors = errors + 1;
end
$display(" generous master, %0d captures: both samplers correct -- the shallow one is indistinguishable here",
n_cap);
// (b) a master whose MOSI hold is one system clock. Legal by any
// datasheet, and the shallow sampler now reads the next bit.
clear_counts();
for (h = 0; h < 6; h = h + 1)
for (p = 0; p <= 1; p = p + 1) begin
seed = (seed * 32'h0019_660D) + 32'h3C6E_F35F;
pat = seed & 32'hFF;
bad = matched_bad;
drive_frame(halves[h], 1'b0, p[0], 8, pat, 1'b1);
if (matched_bad != bad)
$display(" (short hold, half=%0d cpha=%0d: %0d wrong)",
halves[h], p, matched_bad - bad);
end
if (matched_bad != 0) begin
$display(" FAIL: against a short-hold master the matched sampler missed %0d bits",
matched_bad);
errors = errors + 1;
end
if (shallow_bad == 0) begin
$display(" FAIL: the shallow sampler read every bit correctly -- the experiment proves nothing");
errors = errors + 1;
end
$display(" short-hold master, %0d captures: matched sampler %0d wrong, shallow sampler %0d wrong",
n_cap, matched_bad, shallow_bad);
// 6. NO TWO RECOVERED EDGES IN ONE CYCLE, across the whole run.
if (both_edges != 0) begin
$display(" FAIL: %0d cycles carried both edge strobes", both_edges);
errors = errors + 1;
end
$display(" across the whole run no cycle carried both edge strobes");
if (errors == 0)
$display("PASS: SCLK, CS and MOSI are recovered through synchroniser chains of identical depth, so every recovered edge arrives delay-matched with the MOSI value that accompanied it -- every driven half-period produces exactly one recovered edge at ratios from 2 to 16 system clocks and under both polarities, the recovered sequence is strictly leading-then-trailing and identical under both polarities because leading is defined against the configured idle level, the measured shortest half-period matches the driven one exactly at every ratio with the unbounded interval after reset correctly excluded, a half-period below the stated precondition is reported and latched while one exactly at it is accepted, the CLOSING interval of a transaction is measured but not judged -- because no capture depends on it and a real master does return SCLK to idle early, as Chapter 14.10 measured -- while the same shortness moved one interval inside is still reported, and against a master whose MOSI hold is a single system clock the matched sampler reads every bit correctly while a one-flop sampler -- indistinguishable against a generous master -- reads %0d of %0d wrong", shallow_bad, n_cap);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b0;
cpol = 1'b0;
sclk_pin = 1'b0;
cs_n_pin = 1'b1;
mosi_pin = 1'b0;
clr_flags = 1'b0;
cpha_now = 1'b0;
rec_arm = 1'b0;
errors = 0;
end
endmodule-- spi_slave_frontend_tb.vhd
--
-- The pins are driven cycle by cycle on the system clock, so the frequency ratio
-- is an exact integer the test controls rather than a consequence of two
-- independent clock generators. The whole subject is what happens at particular
-- ratios, and a testbench that cannot name the ratio cannot test it.
--
-- The central experiment is the DELAY-MATCHING one. Two samplers watch the same
-- MOSI pin: the design's, SYNC_N flops deep and therefore matched to the SCLK
-- path, and a deliberately WRONG one a single flop deep. Both sample at the
-- design's own capture strobe. Against a generous master -- MOSI held for a full
-- SCLK period -- they agree, which is why the mistake survives review. Against a
-- master that releases MOSI shortly after the capture edge, which every
-- datasheet permits, the shallow sampler reads the NEXT bit.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_frontend_tb is
end entity;
architecture sim of spi_slave_frontend_tb is
constant SYNC_N : positive := 2;
constant HALF_MIN : positive := 3;
constant CNT_W : positive := 12;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal halt : boolean := false;
signal cpol : std_logic := '0';
signal sclk_pin : std_logic := '0';
signal cs_n_pin : std_logic := '1';
signal mosi_pin : std_logic := '0';
signal clr_flags : std_logic := '0';
signal sclk_q, cs_active, mosi_q : std_logic;
signal edge_a_stb, edge_b_stb : std_logic;
signal cs_assert_stb, cs_deassert_stb : std_logic;
signal min_half : unsigned(CNT_W - 1 downto 0);
signal ratio_err : std_logic;
-- the deliberately WRONG sampler: one flop instead of SYNC_N
signal mosi_1flop : std_logic := '0';
signal cpha_now : std_logic := '0';
signal cap_stb : std_logic;
signal n_a, n_b, n_cap, n_assert, n_deassert : natural := 0;
signal both_edges : natural := 0;
signal matched_bad : natural := 0;
signal shallow_bad : natural := 0;
-- The expectation is an INDEXED WORD, not a live signal. A live expected-bit
-- driven in pin time and compared in recovered time is off by the
-- synchroniser latency, which produces false failures at exactly the tight
-- ratios the test exists to exercise.
signal exp_word : std_logic_vector(31 downto 0) := (others => '0');
signal exp_len : natural := 0;
signal cap_idx : natural := 0;
-- Recording is armed only once the pins have settled. Changing `cpol`
-- between frames moves the DUT's notion of "away from idle" without SCLK
-- moving at all, which is a recovered edge -- real, and not part of a frame.
signal rec_arm : std_logic := '0';
signal clr_stb : std_logic := '0';
signal ev_code : natural := 0;
type nat_vec is array (0 to 255) of natural;
signal ev_seq : nat_vec := (others => 0);
signal ev_n : natural := 0;
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
dut : entity work.spi_slave_frontend
generic map (SYNC_N => SYNC_N, HALF_MIN => HALF_MIN, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, cpol => cpol,
sclk_pin => sclk_pin, cs_n_pin => cs_n_pin,
mosi_pin => mosi_pin,
sclk_q => sclk_q, cs_active => cs_active, mosi_q => mosi_q,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
cs_assert_stb => cs_assert_stb,
cs_deassert_stb => cs_deassert_stb,
min_half => min_half, ratio_err => ratio_err,
clr_flags => clr_flags);
shallow : process (clk)
begin
if rising_edge(clk) then
mosi_1flop <= mosi_pin;
end if;
end process;
cap_stb <= edge_b_stb when cpha_now = '1' else edge_a_stb;
monitor : process (clk)
variable want : std_logic;
begin
if rising_edge(clk) then
if clr_stb = '1' then
n_a <= 0; n_b <= 0; n_cap <= 0;
n_assert <= 0; n_deassert <= 0;
matched_bad <= 0; shallow_bad <= 0; ev_n <= 0;
elsif rst_n = '1' and rec_arm = '1' then
if edge_a_stb = '1' and edge_b_stb = '1' then
both_edges <= both_edges + 1;
end if;
if edge_a_stb = '1' then
n_a <= n_a + 1;
if ev_n < 256 then ev_seq(ev_n) <= 1; end if;
ev_n <= ev_n + 1;
end if;
if edge_b_stb = '1' then
n_b <= n_b + 1;
if ev_n < 256 then ev_seq(ev_n) <= 2; end if;
ev_n <= ev_n + 1;
end if;
if cs_assert_stb = '1' then
n_assert <= n_assert + 1;
cap_idx <= 0;
end if;
if cs_deassert_stb = '1' then
n_deassert <= n_deassert + 1;
end if;
if cap_stb = '1' then
n_cap <= n_cap + 1;
if cap_idx < exp_len then
want := exp_word(exp_len - 1 - cap_idx);
if mosi_q /= want then
matched_bad <= matched_bad + 1;
end if;
if mosi_1flop /= want then
shallow_bad <= shallow_bad + 1;
end if;
end if;
cap_idx <= cap_idx + 1;
end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable seed : unsigned(31 downto 0) := x"51A5E001";
variable pat : std_logic_vector(31 downto 0);
variable bad : natural;
variable ref_n : natural;
procedure adv(n : natural) is
begin
for k in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
procedure clear_counts is
begin
clr_stb <= '1';
wait until falling_edge(clk);
clr_stb <= '0';
end procedure;
procedure next_rand(variable v : out std_logic_vector(31 downto 0)) is
begin
seed := resize(seed * x"0019660D", 32) + x"3C6EF35F";
v := std_logic_vector(seed);
end procedure;
-- A behavioural master driven on the system clock, so `half` is exact.
-- `tight_hold` models a master whose MOSI hold after the capture edge is
-- a few nanoseconds rather than a half-period.
-- A bare clock train whose CLOSING interval is deliberately short, which is
-- what a real master produces: it has no reason to hold SCLK away from idle
-- for a full half-period after the last capture, because that closing edge
-- carries no data. No data is checked here -- the recorder stays disarmed --
-- because the subject is the measurement and the flag, not the bits.
procedure drive_train_short_close(half : natural; close : natural;
nbits : natural) is
begin
rec_arm <= '0';
cpol <= '0';
cpha_now <= '0';
sclk_pin <= '0';
cs_n_pin <= '1';
adv(6);
cs_n_pin <= '0';
adv(4);
for i in 0 to nbits - 1 loop
sclk_pin <= '1';
-- The LAST return to idle comes early, and only that one, so it is
-- the interval CLOSED BY the final edge that is short.
if i = nbits - 1 then
adv(close);
else
adv(half);
end if;
sclk_pin <= '0';
adv(half);
end loop;
adv(4);
cs_n_pin <= '1';
adv(6);
end procedure;
procedure drive_frame(half : natural; pol : std_logic; pha : std_logic;
nbits : natural;
data : std_logic_vector(31 downto 0);
tight_hold : boolean) is
begin
-- Settle the pins at the new polarity BEFORE arming the recorder.
rec_arm <= '0';
cpol <= pol;
cpha_now <= pha;
sclk_pin <= pol;
cs_n_pin <= '1';
adv(6);
exp_word <= data;
exp_len <= nbits;
rec_arm <= '1';
cs_n_pin <= '0';
if pha = '0' then
-- CPHA=0 captures on the FIRST leading edge, so the first bit
-- must be on the wire before any edge exists.
mosi_pin <= data(nbits - 1);
end if;
adv(4); -- the master's lead time
for i in 0 to nbits - 1 loop
-- leading edge
sclk_pin <= not pol;
if pha = '1' then
-- CPHA=1 launches here; the capture is a half-period later.
mosi_pin <= data(nbits - 1 - i);
adv(half);
elsif tight_hold and i < nbits - 1 then
-- This IS the capture edge. A sloppy master lets MOSI go one
-- system clock later.
adv(1);
mosi_pin <= data(nbits - 2 - i);
adv(half - 1);
else
adv(half);
end if;
-- trailing edge
sclk_pin <= pol;
if pha = '0' then
if (not tight_hold) and i < nbits - 1 then
mosi_pin <= data(nbits - 2 - i);
end if;
adv(half);
elsif tight_hold then
-- This IS the capture edge in CPHA=1.
adv(1);
if i < nbits - 1 then
mosi_pin <= data(nbits - 2 - i);
end if;
adv(half - 1);
else
adv(half);
end if;
end loop;
adv(4); -- the master's lag
cs_n_pin <= '1';
adv(6); -- and its gap
rec_arm <= '0';
end procedure;
type int_vec is array (natural range <>) of natural;
constant HALVES : int_vec(0 to 5) := (2, 3, 4, 6, 8, 16);
constant ZERO32 : std_logic_vector(31 downto 0) := (others => '0');
variable pol_v, pha_v : std_logic;
begin
adv(3);
rst_n <= '1';
adv(2);
-- 1. EDGE RECOVERY at every ratio and both polarities.
for h in HALVES'range loop
for p in 0 to 1 loop
if p = 1 then pol_v := '1'; else pol_v := '0'; end if;
clear_counts;
drive_frame(HALVES(h), pol_v, '0', 8, x"000000A5", false);
if n_a /= 8 or n_b /= 8 then
report " FAIL: half=" & integer'image(HALVES(h)) &
" recovered " & integer'image(n_a) & " leading and " &
integer'image(n_b) & " trailing edges, expected 8/8";
errs := errs + 1;
end if;
if n_assert /= 1 or n_deassert /= 1 then
report " FAIL: half=" & integer'image(HALVES(h)) &
" saw " & integer'image(n_assert) & " asserts and " &
integer'image(n_deassert) & " deasserts";
errs := errs + 1;
end if;
if matched_bad /= 0 then
report " FAIL: half=" & integer'image(HALVES(h)) &
" matched sampler read " &
integer'image(matched_bad) & " wrong bits";
errs := errs + 1;
end if;
end loop;
end loop;
report " edge recovery: 12 (half, polarity) pairs, 8 leading and 8 trailing edges each, one assert and one deassert";
-- 2. THE EDGE SEQUENCE IS THE SAME UNDER BOTH POLARITIES.
clear_counts;
drive_frame(4, '0', '0', 8, x"0000003C", false);
ref_n := ev_n;
for k in 0 to ev_n - 1 loop
if (k mod 2 = 0 and ev_seq(k) /= 1) or
(k mod 2 = 1 and ev_seq(k) /= 2) then
report " FAIL: CPOL=0 event " & integer'image(k) & " was " &
integer'image(ev_seq(k));
errs := errs + 1;
end if;
end loop;
clear_counts;
drive_frame(4, '1', '0', 8, x"0000003C", false);
if ev_n /= ref_n then
report " FAIL: CPOL=1 produced " & integer'image(ev_n) &
" events where CPOL=0 produced " & integer'image(ref_n);
errs := errs + 1;
end if;
for k in 0 to ev_n - 1 loop
if (k mod 2 = 0 and ev_seq(k) /= 1) or
(k mod 2 = 1 and ev_seq(k) /= 2) then
report " FAIL: CPOL=1 event " & integer'image(k) & " was " &
integer'image(ev_seq(k));
errs := errs + 1;
end if;
end loop;
report " both polarities produced " & integer'image(ev_n) &
" events in strict leading-then-trailing order";
-- 3. THE RATIO MEASUREMENT.
for h in HALVES'range loop
clr_flags <= '1'; adv(1); clr_flags <= '0';
clear_counts;
drive_frame(HALVES(h), '0', '0', 8, x"0000005A", false);
if to_integer(min_half) /= HALVES(h) then
report " FAIL: driving half=" & integer'image(HALVES(h)) &
" measured min_half=" &
integer'image(to_integer(min_half));
errs := errs + 1;
end if;
end loop;
report " the measured shortest half-period matched the driven one at every ratio from 2 to 16";
-- 4. THE PRECONDITION IS REPORTED.
clr_flags <= '1'; adv(1); clr_flags <= '0';
drive_frame(HALF_MIN, '0', '0', 8, x"000000FF", false);
if ratio_err = '1' then
report " FAIL: a half-period of exactly " &
integer'image(HALF_MIN) & " was reported as too fast";
errs := errs + 1;
end if;
clr_flags <= '1'; adv(1); clr_flags <= '0';
drive_frame(HALF_MIN - 1, '0', '0', 8, x"000000FF", false);
if ratio_err /= '1' then
report " FAIL: a half-period of " & integer'image(HALF_MIN - 1) &
" was not reported";
errs := errs + 1;
end if;
if to_integer(min_half) /= HALF_MIN - 1 then
report " FAIL: the too-fast half measured " &
integer'image(to_integer(min_half));
errs := errs + 1;
end if;
drive_frame(8, '0', '0', 8, x"000000FF", false);
if ratio_err /= '1' then
report " FAIL: a legal frame cleared the ratio report";
errs := errs + 1;
end if;
clr_flags <= '1'; adv(1); clr_flags <= '0';
adv(2);
if ratio_err = '1' then
report " FAIL: the ratio report did not clear on command";
errs := errs + 1;
end if;
report " a half-period of " & integer'image(HALF_MIN) &
" is accepted, " & integer'image(HALF_MIN - 1) &
" is reported and latched, and the report clears only on command";
-- 4b. THE CLOSING INTERVAL IS MEASURED BUT NOT JUDGED, which is the one place
-- the flag and the measurement are meant to disagree. A master that clocks
-- legally and then returns SCLK to idle early has violated nothing a
-- capture depended on -- the closing edge carries no data in CPHA=0 -- so
-- the flag must stay clear while the number must still show what happened.
--
-- This is not a hypothetical. Chapter 14.10 wired this slave to the master
-- of Chapter 13.11 and measured a closing interval of exactly two cycles
-- at every divisor, because that master ends a transaction on its final
-- capture. A front end that judged every interval would have reported a
-- fault on every transaction that ever worked.
clr_flags <= '1'; adv(1); clr_flags <= '0';
drive_train_short_close(8, HALF_MIN - 1, 8);
if ratio_err = '1' then
report " FAIL: a short CLOSING interval was reported as a ratio violation, which would fire on every transaction a real master ever sends";
errs := errs + 1;
end if;
if to_integer(min_half) /= HALF_MIN - 1 then
report " FAIL: the short closing interval measured " &
integer'image(to_integer(min_half)) & ", expected " &
integer'image(HALF_MIN - 1) &
" -- it must remain visible in the measurement even though it does not trip the flag";
errs := errs + 1;
end if;
report " a clock train of half-period 8 that returns SCLK to idle after only " &
integer'image(HALF_MIN - 1) &
" cycles: the flag stays clear because no capture depended on that interval, and the measurement still reports " &
integer'image(to_integer(min_half)) &
" -- the number tells the truth and the flag makes a judgement, and they are allowed to differ";
-- 4c. AND A SHORT INTERIOR INTERVAL IS STILL REPORTED, so 4b removed one
-- interval from the judgement and not the judgement itself.
clr_flags <= '1'; adv(1); clr_flags <= '0';
drive_train_short_close(HALF_MIN - 1, 8, 8);
if ratio_err /= '1' then
report " FAIL: a train with short INTERIOR half-periods and a generous closing one was not reported";
errs := errs + 1;
end if;
report " the same train with the shortness moved INSIDE -- interior halves of " &
integer'image(HALF_MIN - 1) &
" and a generous closing interval -- is reported, so excluding the closing interval cost exactly one interval of judgement and nothing more";
-- 5a. THE DELAY-MATCHING EXPERIMENT: a generous master.
clear_counts;
for h in HALVES'range loop
for p in 0 to 1 loop
if p = 1 then pha_v := '1'; else pha_v := '0'; end if;
next_rand(pat);
pat := pat and x"000000FF";
drive_frame(HALVES(h), '0', pha_v, 8, pat, false);
end loop;
end loop;
if matched_bad /= 0 or shallow_bad /= 0 then
report " FAIL: against a generous master the matched sampler missed " &
integer'image(matched_bad) & " bits and the shallow one " &
integer'image(shallow_bad);
errs := errs + 1;
end if;
report " generous master, " & integer'image(n_cap) &
" captures: both samplers correct -- the shallow one is indistinguishable here";
-- 5b. A master whose MOSI hold is one system clock.
clear_counts;
for h in HALVES'range loop
for p in 0 to 1 loop
if p = 1 then pha_v := '1'; else pha_v := '0'; end if;
next_rand(pat);
pat := pat and x"000000FF";
drive_frame(HALVES(h), '0', pha_v, 8, pat, true);
end loop;
end loop;
if matched_bad /= 0 then
report " FAIL: against a short-hold master the matched sampler missed " &
integer'image(matched_bad) & " bits";
errs := errs + 1;
end if;
if shallow_bad = 0 then
report " FAIL: the shallow sampler read every bit correctly -- the experiment proves nothing";
errs := errs + 1;
end if;
report " short-hold master, " & integer'image(n_cap) &
" captures: matched sampler " & integer'image(matched_bad) &
" wrong, shallow sampler " & integer'image(shallow_bad) & " wrong";
-- 6. NO TWO RECOVERED EDGES IN ONE CYCLE, across the whole run.
if both_edges /= 0 then
report " FAIL: " & integer'image(both_edges) &
" cycles carried both edge strobes";
errs := errs + 1;
end if;
report " across the whole run no cycle carried both edge strobes";
errors <= errs;
if errs = 0 then
report "PASS: SCLK, CS and MOSI are recovered through synchroniser chains of identical depth, so every recovered edge arrives delay-matched with the MOSI value that accompanied it -- every driven half-period produces exactly one recovered edge at ratios from 2 to 16 system clocks and under both polarities, the recovered sequence is strictly leading-then-trailing and identical under both polarities because leading is defined against the configured idle level, the measured shortest half-period matches the driven one exactly at every ratio with the unbounded interval after reset correctly excluded, a half-period below the stated precondition is reported and latched while one exactly at it is accepted, the CLOSING interval of a transaction is measured but not judged -- because no capture depends on it and a real master does return SCLK to idle early, as Chapter 14.10 measured -- while the same shortness moved one interval inside is still reported, and against a master whose MOSI hold is a single system clock the matched sampler reads every bit correctly while a one-flop sampler -- indistinguishable against a generous master -- reads " & integer'image(shallow_bad) & " of " & integer'image(n_cap) & " wrong";
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
The front end is where a slave testbench either becomes trustworthy or becomes a second implementation of the design.
The property to check is edge conservation, not edge timing. "Every driven half-period produces exactly one recovered edge" is a counting property and it is checkable for a whole run in two counters. "The edge appears SYNC_N cycles later" is a timing property, it is fragile, and it re-implements the design in the checker. Check the first.
The delay-matching property needs a second sampler, not an assertion. There is no assertion that says "MOSI is delay-matched with SCLK", because the statement is about a relationship between two latencies and nothing in the design names either. What does work is the structural experiment in test 5: build the wrong thing next to the right thing, drive both, and assert they disagree under a legal stimulus. A checker that can fail only when the design is wrong is worth more than an assertion that restates the design.
The stimulus needs a hostile master, and "hostile" here means "legal". The most valuable single stimulus in this bench is a master whose MOSI hold is one system clock. It violates nothing. It is what a fast master with a short output-hold specification does. And it separates the correct design from the plausible one, which a generous master cannot.
// Edge conservation, as an SVA-style property over a whole run rather than a
// cycle. Written as a bind-time check because it is about counts, not events.
//
// NOTE: Icarus Verilog rejects SVA entirely, so the simulated benches above use
// counters and explicit comparisons. These properties are what you would write
// in a simulator that supports them, and they state the same three things.
property p_one_strobe_per_edge;
// No cycle carries both recovered edges. This is the failure the ratio
// precondition exists to prevent, and it is the only part of that
// precondition a simulation can check at all.
@(posedge clk) disable iff (!rst_n)
not (edge_a_stb && edge_b_stb);
endproperty
property p_strobes_alternate;
// Leading is always followed by trailing, never by another leading. This is
// the property that makes CPOL invisible downstream.
@(posedge clk) disable iff (!rst_n)
edge_a_stb |=> (!edge_a_stb throughout (edge_b_stb[->1]));
endproperty
property p_ratio_sticky;
// A reported violation never un-reports itself. A flag that clears on the
// next good frame is a flag that reads clear on every board that has the
// problem intermittently.
@(posedge clk) disable iff (!rst_n)
ratio_err && !clr_flags |=> ratio_err;
endproperty
property p_min_half_monotone;
// The measurement only ever decreases between clears -- it is a minimum,
// not a last-seen value. A last-seen value read from a board tells you
// about the most recent transaction, which is never the interesting one.
@(posedge clk) disable iff (!rst_n)
!clr_flags |=> (min_half <= $past(min_half));
endproperty// Coverage. The axis that matters is the RATIO, and the reason it matters is
// that the design's one precondition is about it -- so a suite that runs at one
// ratio has tested the precondition zero times.
covergroup cg_frontend @(posedge clk);
option.per_instance = 1;
// Bins chosen around HALF_MIN rather than spread evenly. The interesting
// ratios are the boundary and its immediate neighbours; 16 is there to
// confirm nothing depends on the ratio being small.
ratio: coverpoint min_half {
bins below = {[1:2]}; // precondition violated
bins at = {3}; // exactly HALF_MIN
bins just_ok = {4}; // one cycle of margin
bins roomy = {[5:8]};
bins wide = {[9:$]};
}
// Polarity must be crossed with ratio, because the edge definition depends
// on CPOL and the measurement does not.
pol: coverpoint cpol { bins low = {0}; bins high = {1}; }
// MOSI hold, in system clocks. The single-cycle bin is the one that
// separates a matched front end from a mismatched one, and a suite without
// it cannot tell them apart.
hold: coverpoint mosi_hold_cycles {
bins one = {1};
bins two = {2};
bins half = {[3:8]};
bins full = {[9:$]};
}
x_ratio_pol: cross ratio, pol;
x_ratio_hold: cross ratio, hold;
endgroup8. Why an FPGA or ASIC Engineer Cares
SCLK is a data input here, not a clock, and that is the whole point of architecture B. There is no clock tree for it, no generated-clock constraint, no skew budget. What it needs instead is an input delay constraint on three pins, which is Chapter 15.7's subject and cannot be written from inside the RTL. A design review that finds no create_clock on SCLK and no set_input_delay on the pins has found the real problem.
The three synchroniser chains must not be optimised. They are three shift registers whose only purpose is delay, and a synthesis tool that recognises "a chain of flops with no logic between them" as a candidate for retiming or SRL inference will happily change the depth. On Xilinx that means ASYNC_REG on the first two flops of each chain and SHREG_EXTRACT = "no"; on Intel, ALTERA_ATTRIBUTE with -name SYNCHRONIZER_IDENTIFICATION. The attribute matters twice over: it stops the optimisation, and it tells timing analysis to treat the first flop's input as an asynchronous arrival rather than reporting a violated setup path forever.
Equal depth is a lint rule, not a timing rule. No static timing tool will tell you that MOSI has one flop and SCLK has two. The paths are both short and both pass. The property is structural, and the places it is enforceable are a review, a lint rule that counts flops per input, and — most reliably — one parameter used three times, which is what the RTL above does.
The interval counter is the only wide arithmetic in the block and it runs at the system clock with a full cycle available, so it is never the critical path. CNT_W = 12 gives a measurement range of 4095 system clocks, which at a 100 MHz system clock covers SCLK down to about 12 kHz. Below that the counter saturates, which is why saturating rather than wrapping matters: a wrap makes a very slow clock look like a very fast one and flags the first frame after reset.
9. Failure Signature — A Slave That Works On The Bench And Fails On A Faster Master
The symptom, as it arrives:
"It works with our test board at 1 MHz. On the production host it reads garbage at 8 MHz — but not always, and a scope on MOSI and SCLK looks fine."
What is happening: MOSI has one synchroniser flop and SCLK has two. At 1 MHz the master holds each bit for 50 system clocks, so sampling one cycle late is harmless. At 8 MHz with a master whose output hold is short, one cycle late is the next bit, and the slave reads every byte shifted by one.
Why it looks like noise rather than a design error:
- A scope shows a clean bus, because the bus is clean.
- The failure depends on the ratio of two clocks and on the master's output hold, so it changes with temperature, with silicon, and between two hosts that are nominally identical.
- Simulation passes at every ratio, because simulation has no metastability and because the bench's master holds MOSI generously.
- The received data is often nearly right — a byte shifted by one bit is not obviously garbage in a protocol with a status register full of zeros.
How to find it in five minutes: read min_half. If it is at or below HALF_MIN, the precondition is violated and the architecture is wrong for this system, not the front end. If it is comfortable, the ratio is fine and the fault is the depth mismatch — count the flops on each input. The measurement separates the two cases, which is the entire reason it exists.
10. Common Misconceptions
"MOSI is an input, so it needs a synchroniser to avoid metastability." It needs the synchroniser to be delay-matched with SCLK. Nothing downstream of MOSI does anything dangerous with an undefined value — a wrong bit is a wrong bit. Getting the reason right is what tells you the depth must match rather than merely be at least two.
"Two flops is enough for MOSI because two flops is enough for anything." Two flops is enough for settling. The number that matters is not two, it is the same as SCLK's. If SYNC_N were 3, MOSI would need 3.
"A slave that recovers edges has no timing requirements on SCLK." It has exactly one, and it is the only unsimulatable requirement in the whole module. A slave with no stated ratio requirement has an unstated one.
"The ratio requirement is about having enough resolution to see the edge." It is about the edge being lost. One system clock per half-period would be plenty of resolution if flops were ideal; the requirement exists because the first flop of a synchroniser can resolve to the old value, and at a tight ratio the old value is still there when the next sample arrives.
"If simulation passes at a 1:1 ratio, the design handles a 1:1 ratio." Simulation always passes at 1:1, because simulated flops never go metastable. This is the one place in the module where a green regression means nothing at all.
"A sticky error flag is worse than a live one, because you cannot tell when it happened." For an intermittent fault the opposite is true. A live flag reads clear whenever you happen to look, which is always. The sticky flag plus the measured minimum tells you both that it happened and how bad it got; clr_flags gives you the "when" if you need it.
11. Reason It Through
Q. SYNC_N = 2 and HALF_MIN = 3. The master drives SCLK with a half-period of exactly 3 system clocks and holds MOSI for a full SCLK period. Is the design correct, and is it safe?
Correct, and not safe. Every edge is recovered and every bit is the right bit, because MOSI is delay-matched and stable for a half-period either side of the capture. But there is no margin: min_half will read 3, equal to HALF_MIN, and the precondition is satisfied with nothing to spare. A master 10% faster, or a synchroniser one stage deeper after someone hardens a different input, breaks it. The design should be reported as at its limit, which is why the bench asserts that exactly HALF_MIN is accepted and HALF_MIN - 1 is flagged — the boundary is a design decision and it has to be pinned down in both directions.
Q. Someone proposes removing MOSI's synchroniser entirely and sampling the raw pin at the edge strobe, arguing that MOSI is stable for a half-period so metastability cannot occur. What breaks?
The timing relationship. The edge strobe arrives SYNC_N cycles after the pin edge, so sampling the raw pin at that moment reads MOSI SYNC_N cycles after the edge — which is the mismatch of §4 made worse, not better. The argument about stability is sound and irrelevant: the question is not whether the sampled value is settled but whether it is the value that accompanied the edge. Removing flops from MOSI moves the sample later, exactly as adding flops to SCLK would.
Q. The bench drives a legal master and the mismatched sampler reads 45 of 96 bits wrong. Why 45 rather than 96?
Because a wrong bit is only observably wrong when the next bit differs from it. Sampling one cycle late reads M(t-1) instead of M(t-2); when those two are equal — which is about half the time for random data — the mismatched sampler gets the right answer by luck. That is what makes the bug so hard to attribute in the field: it corrupts roughly half of the bits that change, so the data is wrong in a way that looks like noise rather than a shift.
Q. Why does the interval counter reload with 1 rather than 0, and what would the symptom of reloading with 0 be?
The cycle on which an edge is detected is itself the first cycle of the next interval. Reload with 0 and every measured half-period is one short, so a master supplying exactly HALF_MIN measures HALF_MIN - 1 and is reported as violating the precondition. The symptom is a design that flags every master, including correct ones — and the natural "fix" is to relax the comparison to < instead of <=, which hides the off-by-one and simultaneously stops the flag firing for a master that really is one cycle too fast.
Q. A reviewer asks why ratio_err and min_half are allowed to disagree — the flag clear while the number is below HALF_MIN. What is the answer?
That the flag is a statement about data and the number is a statement about the bus. ratio_err is gated on intervals that were followed by more clocking, which are exactly the intervals a capture depended on; the interval closed by a transaction's last edge is excluded, because in CPHA = 0 that edge carries no data — it is only SCLK returning to idle, and a real master has no reason to wait a full half-period before doing it. Chapter 14.10 measured exactly that against the master of Module 13. A front end that judged every interval would flag every transaction that ever worked, and a flag that is always set is a flag nobody reads.
12. Understanding Check
13. Summary
A slave does not own its clock, and there are two architectures available. Clock on SCLK and the system boundary becomes a clock-domain crossing; clock on the system clock and recover SCLK's edges and the front end becomes the hard part. Module 14 takes the second because the common case is a system clock much faster than SCLK.
That choice has one precondition: each SCLK half-period must last at least HALF_MIN system clocks, with HALF_MIN at least SYNC_N + 1. The reason is not resolution or margin — it is that a synchroniser's first flop may resolve to the old value, and at a tight ratio the old value is still in place when the next sample arrives, so an entire half-period is never seen.
That failure cannot be simulated, because simulated flops do not go metastable. So the design measures the ratio it is given and publishes both a sticky flag and the measured minimum, turning an unfalsifiable assumption into a number an integrator can read off a board.
MOSI passes through exactly as many flops as SCLK, and the reason is delay-matching rather than metastability. Equal depth makes mosi_q at the strobe equal to MOSI at the edge. One flop fewer makes it one cycle newer, which is the next bit against a master with a short output hold — a failure that is invisible against a generous master and is therefore invisible to a bench that only drives one.
The strobes are leading and trailing rather than rising and falling, which consumes CPOL here and leaves Chapter 14.6 with one bit instead of two.
For verification: check edge conservation rather than edge timing; prove delay-matching with a second, deliberately wrong sampler rather than an assertion; and make the hostile stimulus a legal one — a master whose MOSI hold is a single system clock. In UVM, the monitor samples pins in pin time and the scoreboard compares by index into the transaction, because recovered time lags pin time by SYNC_N.
For implementation: SCLK is data, not a clock — no clock tree, no generated clock, but three input-delay constraints and synchroniser attributes that stop the tool changing the depth the design depends on. Equal depth is a lint rule, not a timing rule, and the RTL's defence is one parameter used three times.
14. What Comes Next
The pins are recovered and the edges are strobes. Nothing yet knows when a transaction begins or ends.
Chapter 14.2 — CS Detection and Transaction Boundaries builds the block that turns a chip-select edge into a transaction, counts the edges inside it, and classifies how it ended. The interesting part is the classification: a transaction that ended on a frame boundary and one that ended mid-frame need different handling, and the slave has to decide which happened using nothing but a count — with no multiplier and no divider, because the frame width is a run-time value.
Continue learning
Related tutorials
- Related topic
Why an SPI Slave Is a Clock-Domain Problem
An externally generated SCLK turns a shift register into a CDC question, and there are only four ways to carry an event across: a level, a synchronised level, a toggle, and a handshake. Each is limited by something different, no scheme creates bandwidth, and the difference that matters most is the one no simulation can show.
- Related topic
Synchronizers and Their Limits
A two-flop synchroniser gives a settled value and nothing else — not which value, not a pulse, and not a bus. Measured with the per-bit skew routing produces, independent synchronisers on an 8-bit counter observe values the source never held, and are still wrong when the source is slowed. Gray coding observes zero; data-plus-a-flag fails too unless the source holds still.
- Related topic
Back-to-Back Transactions and Inter-Frame Gap
How soon chip select may fall again after it rises: the minimum deselect time, why a device needs it, what the gap costs in throughput, and the hardware enforcement that keeps software from violating it.
- 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.
