SPI · Module 14
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.
A master knows a transfer has started because it decided to start one. A slave is told by a pin — and that pin is the only thing that tells it.
There is no preamble in SPI. No sync word, no start bit, no idle pattern, no framing byte. The entire framing of the protocol is chip select went low. Chapter 14.1 recovered that edge; this chapter turns it into a transaction, and then asks the question that decides how the rest of the module handles errors:
The slave counted 130 SCLK edges under one chip select, and the frame width is 8 bits. What should it do?
The answer everyone reaches for is "report an error". The better answer is to notice that 130 is a more useful thing to say than "error", and to work out what the slave can and cannot conclude from it.
1. The Consequence of Having Only One Framing Signal
Because CS is the only framing, it is also the only resynchronisation point. This follows immediately and it shapes everything downstream.
Suppose the slave miscounts a single edge — it lost one, or saw one that was not there. Where in the data stream can it notice and recover? Nowhere. There is no pattern to lock back onto, no parity, no delimiter, no illegal symbol. The slave is wrong for the remainder of the assertion, and the only thing that fixes it is chip select going high and coming back down.
Two design rules fall straight out of that.
Every counter in the slave is reset by CS assert, never by reaching a count. A bit counter that wraps at len and keeps going has decided that its own arithmetic defines the frame boundary. If it is ever wrong, it stays wrong, and it stays wrong confidently — producing well-formed words at the wrong offsets. A counter reset by the assert has one authority for where frames begin, and that authority is the pin.
A long transaction is strictly riskier than several short ones. The window in which a single lost edge can corrupt data is the whole assertion. Eight separate one-byte transactions each risk one byte; one eight-byte transaction risks eight. That is a real cost of the burst mode Chapter 13.7 provides on the master side, and it is worth knowing which side of the trade a system is on: bursts buy throughput with the size of the blast radius.
2. What The Block Publishes
txn_active a transaction is open; everything downstream gates on it
txn_start_stb one cycle, at the assert
txn_end_stb one cycle, at the deassert
edges_in_txn how many SCLK edges arrived, counted live
frames_in_txn how many whole frames those edges make
txn_clean it ended on a frame boundary: a non-zero whole number
txn_trunc it did not
txn_empty chip select asserted and no edge arrived at all
txn_report_stb one cycle, when the three classifications are VALIDTwo of these need arguing for.
edges_in_txn is the one that earns its keep. A slave that reports only "wrong" turns every integration problem into a guess. A slave that reports "130 edges arrived where 128 were expected" has named the fault — and the two most common integration faults in SPI are a driver sending the wrong number of bytes and a lost edge, both of which are visible in that number and in nothing else, because both produce plausible-looking data.
txn_report_stb exists separately from txn_end_stb, and that is not redundancy. txn_end_stb fires on the cycle the transaction ends — which is one cycle before the classification is computed. A consumer that samples txn_clean at txn_end_stb reads the previous transaction's verdict. The symptom is a diagnosis that is always one transaction behind, which in a downstream block with a threshold looks exactly like a threshold that is off by one. That is the same stale-level trap as the master's frame_done in Chapter 13.9, and the fix here is the same: the strobe that says "the verdict is ready" is a different signal from the one that says "the transaction ended".
3. The State Machine
Three states, and the third exists only to hold a value for one cycle.
edges_in_txn counts in ACTIVE. frames_in_txn and the running remainder are maintained in ACTIVE too. The classification is assigned on the transition out of REPORT, and txn_report_stb is asserted for exactly the cycle on which it is valid.
4. Classifying Without a Divider
A frame is len bits, and every bit takes two SCLK edges, so a frame boundary falls every 2 × len edges. The question the slave has to answer at the deassert is:
Is
edges_in_txna non-zero whole multiple of2 × len?
The obvious implementation is a modulo, and it is the wrong one. len is a run-time value — the frame width is configurable — so edges % (2*len) is a division by a variable, and edges - frames * edges_per_frame is a multiplication by one. Either synthesises to real arithmetic: a divider is out of the question in a block this small, and a multiplier is an absurd amount of logic to answer a yes/no question.
The answer is a running remainder:
in_frame edges into the CURRENT frame, 0 .. 2*len-1
edges_per_frame 2 * len, which is len << 1 -- a shift, not a multiply
on each edge:
if in_frame == edges_per_frame - 1 -> in_frame <= 0, frames++
else -> in_frame <= in_frame + 1
at the deassert:
txn_clean = (frames != 0) && (in_frame == 0)
txn_trunc = (frames != 0 || edges != 0) && (in_frame != 0)
txn_empty = (edges == 0)One comparator, one incrementer, one shift. The remainder is maintained incrementally, so the "modulo" never has to be computed — it is always already there.
This is worth generalising: when a design needs x mod k and x only ever increments, maintain the remainder instead of computing it. The incremental version is a comparator and the computed version is a divider, and the two are equivalent only because x moves by one.
5. What The Edge Count Cannot Detect
This section exists because the first version of this design got it wrong, and the wrong version is more plausible than the right one.
The tempting claim is that a CPHA mismatch truncates every transaction. The argument goes: the master deasserts promptly after its own last capture, which under CPHA=1 is a trailing edge, and a CPHA=0 slave is still waiting for the leading edge that would finish its Nth bit — so every transaction ends one bit short, and txn_trunc fires.
It is wrong, and the arithmetic is simple enough to check:
a CPHA=1 master sending N bits produces 2N edges
a CPHA=0 slave captures on leading edges,
and there are exactly N of them in 2N N bits captured
-> a whole number of frames, on every transaction, in both phasesThe count agrees. txn_clean fires. Truncation never happens. A design that relied on this to diagnose a phase mismatch would report nothing at all — and worse, would report clean, actively asserting that the transaction was fine.
So the honest statement of this block's diagnostic reach is:
the edge count FINDS the edge count is BLIND TO
---------------------------- --------------------------------------
a driver sending the wrong a CPHA mismatch, which sends a whole
number of bytes number of frames
a lost edge a bit-order mismatch, which sends the
a genuine abort mid-frame right bits in the wrong order
an empty select pulseA phase mismatch needs a completely different observation, and Chapter 14.6 makes it: not a count, but the fact that MOSI is in motion at the instant the slave samples it. That chapter's testbench drives a mismatched master, confirms the transaction is classified clean by this block, and diagnoses it anyway.
6. Building the Transaction Detector — Three HDLs
The circuit
Three states, a live edge counter, a frame counter, and a running remainder. The classification is combinational from those counters and is registered into flags on the way out of REPORT, so a consumer has exactly one cycle on which the verdict is both valid and announced.
// spi_slave_cs.sv
//
// Chapter 14.2 -- the transaction boundary, and why it is the only one.
//
// A master knows when a transfer starts because it decided to start one. A slave
// is told, by a pin, and that pin is the ONLY thing that tells it. There is no
// preamble, no sync word, no start bit and no idle pattern: the entire framing
// of SPI is "chip select went low".
//
// THE CONSEQUENCE THAT SHAPES THE REST OF THE MODULE.
//
// Because CS is the only framing, it is also the only RESYNCHRONISATION point. A
// slave that miscounts one edge is wrong for the remainder of the assertion and
// has no way to recover inside it -- there is nothing in the data stream to lock
// back onto. So:
//
// * every counter in the slave is reset by CS ASSERT, never by reaching a
// count. A bit counter that wraps at `len` and keeps going has decided that
// its own arithmetic defines the frame boundary, and if it is ever wrong it
// stays wrong.
// * a longer transaction is strictly riskier than several short ones, because
// the window in which a single lost edge can corrupt data is the whole
// assertion. That is the cost of the burst mode Chapter 13.7 provides.
//
// WHAT THIS BLOCK PUBLISHES, AND WHY EACH IS NEEDED.
//
// txn_active a transaction is open. Everything downstream gates on this.
// edges_in_txn how many SCLK edges arrived, counted live.
// txn_clean it ended on a frame boundary: a non-zero whole number of
// frames.
// txn_trunc it did not.
// txn_empty chip select asserted and no edge arrived at all.
//
// `edges_in_txn` is the one that earns its keep. A slave that reports only
// "wrong" turns every integration problem into a guess; a slave that reports
// "130 edges arrived where 128 were expected" has named the fault. And the two
// most common integration faults in SPI -- a CPHA mismatch and a driver sending
// the wrong number of bytes -- are both visible in that number and in nothing
// else, because both produce plausible-looking data.
//
// WHAT THIS CANNOT DETECT, STATED HERE BECAUSE IT IS TEMPTING TO THINK IT CAN.
//
// A CPHA mismatch does NOT show up in the edge count. The plausible argument is
// that the master deasserts after its own last capture and leaves this slave
// waiting for one more edge -- and it is wrong: a CPHA=1 master sending N bits
// produces 2N edges, and a CPHA=0 slave takes the N leading ones. A whole number
// of frames, every time, on both phases. The count agrees and `txn_clean` fires.
//
// So the edge count finds a driver sending the wrong number of bytes, a lost edge
// and a genuine abort -- and is blind to a phase mismatch, which needs a different
// observation entirely (Chapter 14.6 makes it).
//
// ON CHIP-SELECT GLITCHES. A pulse shorter than the synchroniser depth may not
// be seen at all, and that is the right answer: a master that produces one is
// broken and there is nothing useful to do about it. A pulse that IS seen
// produces a transaction with no edges, and that must be REPORTED rather than
// quietly ignored -- an unexplained `txn_empty` on a working system is a signal
// integrity problem on the select line, and it is the only evidence of it the
// slave can offer.
module spi_slave_cs #(
parameter int LEN_W = 6, // width of the frame-length field
parameter int CNT_W = 12 // width of the edge counter
) (
input wire clk,
input wire rst_n,
// --- from the front end of 14.1 --------------------------------------
input wire cs_assert_stb,
input wire cs_deassert_stb,
input wire edge_a_stb,
input wire edge_b_stb,
// --- configuration ---------------------------------------------------
input wire [LEN_W-1:0] len, // bits per frame, 1 .. MAX
// --- the transaction --------------------------------------------------
output wire txn_active,
output wire txn_start_stb,
output wire txn_end_stb,
output wire [CNT_W-1:0] edges_in_txn, // live during, held after
output wire [CNT_W-1:0] frames_in_txn,
output reg txn_clean, // ended on a frame boundary
output reg txn_trunc, // ended mid-frame
output reg txn_empty, // asserted, and no edge arrived
// One cycle, coincident with the three flags above being VALID. `txn_end_stb`
// fires a cycle earlier -- on entry to the reporting state -- so a consumer
// that samples the classification there reads the PREVIOUS transaction's
// verdict. That is the same stale-level trap as the master's `frame_done`
// (Chapter 13.9), and it is why the strobe that says "the verdict is ready" is
// separate from the one that says "the transaction ended".
output reg txn_report_stb,
output wire [2:0] state_id // for waveform capture
);
localparam [1:0] S_IDLE = 2'd0, // deselected
S_ACTIVE = 2'd1, // a transaction is open
S_REPORT = 2'd2; // one cycle: classify what just ended
reg [1:0] state;
reg [CNT_W-1:0] edges;
reg [CNT_W-1:0] frames;
reg [LEN_W:0] in_frame; // edges into the CURRENT frame, a remainder
reg start_r, end_r, report_r;
wire any_edge = edge_a_stb | edge_b_stb;
// Two edges make one bit, so a frame is 2 * len edges. Zero-extended by
// assignment and then shifted, which avoids a replication whose width could
// reach zero for some parameter combination -- and that is not legal.
wire [LEN_W:0] len_ext = {1'b0, len};
wire [LEN_W:0] edges_per_frame = len_ext << 1;
assign txn_active = (state == S_ACTIVE);
assign txn_start_stb = start_r;
assign txn_end_stb = end_r;
assign edges_in_txn = edges;
assign frames_in_txn = frames;
assign state_id = {1'b0, state};
// `report_r` is set on the cycle the classification is assigned, so it is high
// on the following cycle -- exactly when the flags are valid.
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) txn_report_stb <= 1'b0;
else txn_report_stb <= report_r;
// `in_frame` is a RUNNING REMAINDER, not `edges` modulo the frame size.
// Computing the remainder would need a divide, and computing it as
// `edges - frames * edges_per_frame` would need a multiply -- either of
// which is real hardware on a block whose job is to count to eight.
// Zero means the count is exactly on a frame boundary.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
edges <= {CNT_W{1'b0}};
frames <= {CNT_W{1'b0}};
in_frame <= {(LEN_W+1){1'b0}};
start_r <= 1'b0;
end_r <= 1'b0;
report_r <= 1'b0;
txn_clean <= 1'b0;
txn_trunc <= 1'b0;
txn_empty <= 1'b0;
end else begin
start_r <= 1'b0;
end_r <= 1'b0;
report_r <= 1'b0;
case (state)
S_IDLE: begin
if (cs_assert_stb) begin
// Every counter is cleared HERE. This is the design
// decision the whole chapter is about: the frame
// boundary is defined by the select pin, and nothing
// the slave counts is allowed to redefine it.
edges <= {CNT_W{1'b0}};
frames <= {CNT_W{1'b0}};
in_frame <= {(LEN_W+1){1'b0}};
start_r <= 1'b1;
state <= S_ACTIVE;
end
end
S_ACTIVE: begin
if (any_edge) begin
edges <= edges + 1'b1;
// A frame completes on the edge that brings the running
// remainder back to zero.
if (in_frame == (edges_per_frame - 1'b1)) begin
in_frame <= {(LEN_W+1){1'b0}};
frames <= frames + 1'b1;
end else begin
in_frame <= in_frame + 1'b1;
end
end
// A deassert on the SAME cycle as an edge is possible and is
// handled by letting the edge count first: the edge arrived
// before the select released, so it is part of the
// transaction. Ordering it the other way loses the final
// bit of every transaction whose master deasserts promptly.
if (cs_deassert_stb) begin
end_r <= 1'b1;
state <= S_REPORT;
end
end
S_REPORT: begin
// `edges` and `frames` are final here, so the classification
// is a comparison rather than a prediction. One cycle of
// latency buys a report that cannot be wrong about a
// transaction still in progress.
txn_empty <= (edges == {CNT_W{1'b0}});
txn_clean <= (edges != {CNT_W{1'b0}}) &&
(in_frame == {(LEN_W+1){1'b0}});
txn_trunc <= (edges != {CNT_W{1'b0}}) &&
(in_frame != {(LEN_W+1){1'b0}});
report_r <= 1'b1;
state <= S_IDLE;
end
default: state <= S_IDLE;
endcase
end
end
`ifdef SPI_CHECKS
always_ff @(posedge clk) if (rst_n) begin
if (txn_clean && txn_trunc)
$fatal(1, "a transaction classified both clean and truncated");
if (edge_a_stb && edge_b_stb)
$fatal(1, "both edge strobes on one cycle");
end
`endif
endmodule// spi_slave_cs.v
//
// Chapter 14.2 -- the transaction boundary, and why it is the only one.
//
// A master knows when a transfer starts because it decided to start one. A slave
// is told, by a pin, and that pin is the ONLY thing that tells it. There is no
// preamble, no sync word, no start bit and no idle pattern: the entire framing
// of SPI is "chip select went low".
//
// THE CONSEQUENCE THAT SHAPES THE REST OF THE MODULE.
//
// Because CS is the only framing, it is also the only RESYNCHRONISATION point. A
// slave that miscounts one edge is wrong for the remainder of the assertion and
// has no way to recover inside it -- there is nothing in the data stream to lock
// back onto. So:
//
// * every counter in the slave is reset by CS ASSERT, never by reaching a
// count. A bit counter that wraps at `len` and keeps going has decided that
// its own arithmetic defines the frame boundary, and if it is ever wrong it
// stays wrong.
// * a longer transaction is strictly riskier than several short ones, because
// the window in which a single lost edge can corrupt data is the whole
// assertion. That is the cost of the burst mode Chapter 13.7 provides.
//
// WHAT THIS BLOCK PUBLISHES, AND WHY EACH IS NEEDED.
//
// txn_active a transaction is open. Everything downstream gates on this.
// edges_in_txn how many SCLK edges arrived, counted live.
// txn_clean it ended on a frame boundary: a non-zero whole number of
// frames.
// txn_trunc it did not.
// txn_empty chip select asserted and no edge arrived at all.
//
// `edges_in_txn` is the one that earns its keep. A slave that reports only
// "wrong" turns every integration problem into a guess; a slave that reports
// "130 edges arrived where 128 were expected" has named the fault. And the two
// most common integration faults in SPI -- a CPHA mismatch and a driver sending
// the wrong number of bytes -- are both visible in that number and in nothing
// else, because both produce plausible-looking data.
//
// WHAT THIS CANNOT DETECT, STATED HERE BECAUSE IT IS TEMPTING TO THINK IT CAN.
//
// A CPHA mismatch does NOT show up in the edge count. The plausible argument is
// that the master deasserts after its own last capture and leaves this slave
// waiting for one more edge -- and it is wrong: a CPHA=1 master sending N bits
// produces 2N edges, and a CPHA=0 slave takes the N leading ones. A whole number
// of frames, every time, on both phases. The count agrees and `txn_clean` fires.
//
// So the edge count finds a driver sending the wrong number of bytes, a lost edge
// and a genuine abort -- and is blind to a phase mismatch, which needs a different
// observation entirely (Chapter 14.6 makes it).
//
// ON CHIP-SELECT GLITCHES. A pulse shorter than the synchroniser depth may not
// be seen at all, and that is the right answer: a master that produces one is
// broken and there is nothing useful to do about it. A pulse that IS seen
// produces a transaction with no edges, and that must be REPORTED rather than
// quietly ignored -- an unexplained `txn_empty` on a working system is a signal
// integrity problem on the select line, and it is the only evidence of it the
// slave can offer.
module spi_slave_cs #(
parameter LEN_W = 6, // width of the frame-length field
parameter CNT_W = 12 // width of the edge counter
) (
input wire clk,
input wire rst_n,
// --- from the front end of 14.1 --------------------------------------
input wire cs_assert_stb,
input wire cs_deassert_stb,
input wire edge_a_stb,
input wire edge_b_stb,
// --- configuration ---------------------------------------------------
input wire [LEN_W-1:0] len, // bits per frame, 1 .. MAX
// --- the transaction --------------------------------------------------
output wire txn_active,
output wire txn_start_stb,
output wire txn_end_stb,
output wire [CNT_W-1:0] edges_in_txn, // live during, held after
output wire [CNT_W-1:0] frames_in_txn,
output reg txn_clean, // ended on a frame boundary
output reg txn_trunc, // ended mid-frame
output reg txn_empty, // asserted, and no edge arrived
// One cycle, coincident with the three flags above being VALID. `txn_end_stb`
// fires a cycle earlier -- on entry to the reporting state -- so a consumer
// that samples the classification there reads the PREVIOUS transaction's
// verdict. That is the same stale-level trap as the master's `frame_done`
// (Chapter 13.9), and it is why the strobe that says "the verdict is ready" is
// separate from the one that says "the transaction ended".
output reg txn_report_stb,
output wire [2:0] state_id // for waveform capture
);
localparam [1:0] S_IDLE = 2'd0, // deselected
S_ACTIVE = 2'd1, // a transaction is open
S_REPORT = 2'd2; // one cycle: classify what just ended
reg [1:0] state;
reg [CNT_W-1:0] edges;
reg [CNT_W-1:0] frames;
reg [LEN_W:0] in_frame; // edges into the CURRENT frame, a remainder
reg start_r, end_r, report_r;
wire any_edge = edge_a_stb | edge_b_stb;
// Two edges make one bit, so a frame is 2 * len edges. Zero-extended by
// assignment and then shifted, which avoids a replication whose width could
// reach zero for some parameter combination -- and that is not legal.
wire [LEN_W:0] len_ext = {1'b0, len};
wire [LEN_W:0] edges_per_frame = len_ext << 1;
assign txn_active = (state == S_ACTIVE);
assign txn_start_stb = start_r;
assign txn_end_stb = end_r;
assign edges_in_txn = edges;
assign frames_in_txn = frames;
assign state_id = {1'b0, state};
// `report_r` is set on the cycle the classification is assigned, so it is high
// on the following cycle -- exactly when the flags are valid.
always @(posedge clk or negedge rst_n)
if (!rst_n) txn_report_stb <= 1'b0;
else txn_report_stb <= report_r;
// `in_frame` is a RUNNING REMAINDER, not `edges` modulo the frame size.
// Computing the remainder would need a divide, and computing it as
// `edges - frames * edges_per_frame` would need a multiply -- either of
// which is real hardware on a block whose job is to count to eight.
// Zero means the count is exactly on a frame boundary.
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
edges <= {CNT_W{1'b0}};
frames <= {CNT_W{1'b0}};
in_frame <= {(LEN_W+1){1'b0}};
start_r <= 1'b0;
end_r <= 1'b0;
report_r <= 1'b0;
txn_clean <= 1'b0;
txn_trunc <= 1'b0;
txn_empty <= 1'b0;
end else begin
start_r <= 1'b0;
end_r <= 1'b0;
report_r <= 1'b0;
case (state)
S_IDLE: begin
if (cs_assert_stb) begin
// Every counter is cleared HERE. This is the design
// decision the whole chapter is about: the frame
// boundary is defined by the select pin, and nothing
// the slave counts is allowed to redefine it.
edges <= {CNT_W{1'b0}};
frames <= {CNT_W{1'b0}};
in_frame <= {(LEN_W+1){1'b0}};
start_r <= 1'b1;
state <= S_ACTIVE;
end
end
S_ACTIVE: begin
if (any_edge) begin
edges <= edges + 1'b1;
// A frame completes on the edge that brings the running
// remainder back to zero.
if (in_frame == (edges_per_frame - 1'b1)) begin
in_frame <= {(LEN_W+1){1'b0}};
frames <= frames + 1'b1;
end else begin
in_frame <= in_frame + 1'b1;
end
end
// A deassert on the SAME cycle as an edge is possible and is
// handled by letting the edge count first: the edge arrived
// before the select released, so it is part of the
// transaction. Ordering it the other way loses the final
// bit of every transaction whose master deasserts promptly.
if (cs_deassert_stb) begin
end_r <= 1'b1;
state <= S_REPORT;
end
end
S_REPORT: begin
// `edges` and `frames` are final here, so the classification
// is a comparison rather than a prediction. One cycle of
// latency buys a report that cannot be wrong about a
// transaction still in progress.
txn_empty <= (edges == {CNT_W{1'b0}});
txn_clean <= (edges != {CNT_W{1'b0}}) &&
(in_frame == {(LEN_W+1){1'b0}});
txn_trunc <= (edges != {CNT_W{1'b0}}) &&
(in_frame != {(LEN_W+1){1'b0}});
report_r <= 1'b1;
state <= S_IDLE;
end
default: state <= S_IDLE;
endcase
end
end
`ifdef SPI_CHECKS
always @(posedge clk) if (rst_n) begin
if (txn_clean && txn_trunc)
$fatal(1, "a transaction classified both clean and truncated");
if (edge_a_stb && edge_b_stb)
$fatal(1, "both edge strobes on one cycle");
end
`endif
endmodule-- spi_slave_cs.vhd
--
-- Chapter 14.2 -- the transaction boundary, and why it is the only one.
--
-- A master knows when a transfer starts because it decided to start one. A slave
-- is told, by a pin, and that pin is the ONLY thing that tells it. There is no
-- preamble, no sync word, no start bit and no idle pattern: the entire framing of
-- SPI is "chip select went low".
--
-- THE CONSEQUENCE THAT SHAPES THE REST OF THE MODULE.
--
-- Because CS is the only framing, it is also the only RESYNCHRONISATION point. A
-- slave that miscounts one edge is wrong for the remainder of the assertion and
-- has no way to recover inside it -- there is nothing in the data stream to lock
-- back onto. So:
--
-- * every counter in the slave is reset by CS ASSERT, never by reaching a
-- count. A bit counter that wraps at `len` and keeps going has decided that
-- its own arithmetic defines the frame boundary, and if it is ever wrong it
-- stays wrong.
-- * a longer transaction is strictly riskier than several short ones, because
-- the window in which a single lost edge can corrupt data is the whole
-- assertion. That is the cost of the burst mode Chapter 13.7 provides.
--
-- WHAT THIS BLOCK PUBLISHES.
--
-- txn_active a transaction is open. Everything downstream gates on this.
-- edges_in_txn how many SCLK edges arrived, counted live.
-- txn_clean it ended on a frame boundary: a non-zero whole number.
-- txn_trunc it did not.
-- txn_empty chip select asserted and no edge arrived at all.
--
-- `edges_in_txn` is the one that earns its keep. A slave that reports only
-- "wrong" turns every integration problem into a guess; one that reports "130
-- edges arrived where 128 were expected" has named the fault. And the two most
-- common integration faults in SPI -- a CPHA mismatch and a driver sending the
-- wrong number of bytes -- are both visible in that number and nothing else,
-- because both produce plausible-looking data.
--
-- WHAT THIS CANNOT DETECT, STATED HERE BECAUSE IT IS TEMPTING TO THINK IT CAN.
--
-- A CPHA mismatch does NOT show up in the edge count. The plausible argument is that the
-- master deasserts after its own last capture and leaves this slave waiting for one more
-- edge -- and it is wrong: a CPHA=1 master sending N bits produces 2N edges, and a CPHA=0
-- slave takes the N leading ones. A whole number of frames, every time, on both phases.
-- The count agrees and `txn_clean` fires.
--
-- So the edge count finds a driver sending the wrong number of bytes, a lost edge and a
-- genuine abort -- and is blind to a phase mismatch, which needs a different observation
-- entirely (Chapter 14.6 makes it).
--
-- ON CHIP-SELECT GLITCHES. A pulse shorter than the synchroniser depth may not be
-- seen at all, and that is the right answer: a master that produces one is broken.
-- A pulse that IS seen produces a transaction with no edges, and that must be
-- REPORTED rather than quietly ignored -- an unexplained `txn_empty` on a working
-- system is a signal-integrity problem on the select line, and it is the only
-- evidence of it the slave can offer.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_cs is
generic (
LEN_W : positive := 6; -- width of the frame-length field
CNT_W : positive := 12 -- width of the edge counter
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- from the front end of 14.1
cs_assert_stb : in std_logic;
cs_deassert_stb : in std_logic;
edge_a_stb : in std_logic;
edge_b_stb : in std_logic;
-- configuration
len : in unsigned(LEN_W - 1 downto 0);
-- the transaction
txn_active : out std_logic;
txn_start_stb : out std_logic;
txn_end_stb : out std_logic;
edges_in_txn : out unsigned(CNT_W - 1 downto 0);
frames_in_txn : out unsigned(CNT_W - 1 downto 0);
txn_clean : out std_logic; -- ended on a frame boundary
txn_trunc : out std_logic; -- ended mid-frame
txn_empty : out std_logic; -- asserted, and no edge arrived
-- One cycle, coincident with the three flags above being VALID.
-- `txn_end_stb` fires a cycle earlier -- on entry to the reporting state --
-- so a consumer that samples the classification there reads the PREVIOUS
-- transaction's verdict. That is the same stale-level trap as the master's
-- `frame_done` (Chapter 13.9), and it is why the strobe that says "the
-- verdict is ready" is separate from the one that says "the transaction
-- ended".
txn_report_stb : out std_logic;
state_id : out unsigned(2 downto 0)
);
end entity;
architecture rtl of spi_slave_cs is
constant S_IDLE : unsigned(1 downto 0) := "00"; -- deselected
constant S_ACTIVE : unsigned(1 downto 0) := "01"; -- a transaction is open
constant S_REPORT : unsigned(1 downto 0) := "10"; -- classify what ended
signal state : unsigned(1 downto 0) := S_IDLE;
signal edges : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal frames : unsigned(CNT_W - 1 downto 0) := (others => '0');
-- `in_frame` is a RUNNING REMAINDER, not `edges` modulo the frame size.
-- Computing the remainder would need a divide, and computing it as
-- `edges - frames * edges_per_frame` would need a multiply -- either of which
-- is real hardware on a block whose job is to count to eight.
signal in_frame : unsigned(LEN_W downto 0) := (others => '0');
signal start_r : std_logic := '0';
signal end_r : std_logic := '0';
signal report_r : std_logic := '0';
signal report_q : std_logic := '0';
signal clean_r : std_logic := '0';
signal trunc_r : std_logic := '0';
signal empty_r : std_logic := '0';
signal any_edge : std_logic;
signal edges_per_frame : unsigned(LEN_W downto 0);
begin
any_edge <= edge_a_stb or edge_b_stb;
-- Two edges make one bit, so a frame is 2 * len edges.
edges_per_frame <= shift_left(resize(len, LEN_W + 1), 1);
txn_active <= '1' when state = S_ACTIVE else '0';
txn_start_stb <= start_r;
txn_end_stb <= end_r;
edges_in_txn <= edges;
frames_in_txn <= frames;
txn_clean <= clean_r;
txn_trunc <= trunc_r;
txn_empty <= empty_r;
-- `report_r` is set on the cycle the classification is assigned, so `report_q`
-- is high exactly when the flags are valid.
txn_report_stb <= report_q;
state_id <= resize(state, 3);
fsm : process (clk, rst_n)
begin
if rst_n = '0' then
state <= S_IDLE;
edges <= (others => '0');
frames <= (others => '0');
in_frame <= (others => '0');
start_r <= '0';
end_r <= '0';
report_r <= '0';
report_q <= '0';
clean_r <= '0';
trunc_r <= '0';
empty_r <= '0';
elsif rising_edge(clk) then
start_r <= '0';
end_r <= '0';
report_q <= report_r;
report_r <= '0';
case to_integer(state) is
when 0 => -- S_IDLE
if cs_assert_stb = '1' then
-- Every counter is cleared HERE. This is the design
-- decision the whole chapter is about: the frame
-- boundary is defined by the select pin, and nothing the
-- slave counts is allowed to redefine it.
edges <= (others => '0');
frames <= (others => '0');
in_frame <= (others => '0');
start_r <= '1';
state <= S_ACTIVE;
end if;
when 1 => -- S_ACTIVE
if any_edge = '1' then
edges <= edges + 1;
-- A frame completes on the edge that brings the running
-- remainder back to zero.
if in_frame = (edges_per_frame - 1) then
in_frame <= (others => '0');
frames <= frames + 1;
else
in_frame <= in_frame + 1;
end if;
end if;
-- A deassert on the SAME cycle as an edge is possible and is
-- handled by letting the edge count first: the edge arrived
-- before the select released, so it is part of the
-- transaction. Ordering it the other way loses the final bit
-- of every transaction whose master deasserts promptly.
if cs_deassert_stb = '1' then
end_r <= '1';
state <= S_REPORT;
end if;
when 2 => -- S_REPORT
-- `edges` and `frames` are final here, so the classification
-- is a comparison rather than a prediction. One cycle of
-- latency buys a report that cannot be wrong about a
-- transaction still in progress.
if edges = 0 then
empty_r <= '1';
clean_r <= '0';
trunc_r <= '0';
elsif in_frame = 0 then
empty_r <= '0';
clean_r <= '1';
trunc_r <= '0';
else
empty_r <= '0';
clean_r <= '0';
trunc_r <= '1';
end if;
report_r <= '1';
state <= S_IDLE;
when others =>
state <= S_IDLE;
end case;
end if;
end process;
check : process (clk)
begin
if rising_edge(clk) and rst_n = '1' then
assert not (clean_r = '1' and trunc_r = '1')
report "a transaction classified both clean and truncated"
severity failure;
assert not (edge_a_stb = '1' and edge_b_stb = '1')
report "both edge strobes on one cycle" severity failure;
end if;
end process;
end architecture;The testbench
Eight tests, and three of them are about cases that are easy to get wrong for reasons that have nothing to do with the design.
- Whole frames. 16, 32 and 64 edges at
len = 8classify clean with 1, 2 and 4 frames. - One edge too many. 17 edges is not "two frames and a bit" — it is truncated, and
edges_in_txnsays 17 so the integrator can see what happened. - One edge too few. 15 edges. This is the shape a lost edge produces, and the shape a genuine abort produces, and the block cannot tell them apart — which is stated rather than papered over.
- A single edge, and no edge at all. Both are transactions. One is truncated and one is empty, and the difference matters because empty means something specific about the board.
- A prompt deassert. The select releases on the cycle after the final edge, which is the tightest legal case, and the classification must still be right.
- Frame widths. The boundary is
2 × lenedges, so changing the width moves it. Widths of 4, 8, 12 and 16 each get a clean case and a truncated one. - A repeated phase-mismatch transaction. The point of this test is negative: it drives the pattern a CPHA=1 master produces and asserts that this block classifies it clean, because that is what it does and a later chapter depends on knowing so.
- The continuous properties. No cycle carries both
txn_start_stbandtxn_end_stb;txn_activeis true for exactly the cycles between them; the three classification flags are mutually exclusive and exactly one is set attxn_report_stb.
// spi_slave_cs_tb.sv
//
// Driven through the real front end of Chapter 14.1, so the transaction
// boundaries under test are the RECOVERED ones -- with the synchroniser latency
// and the deassert-on-the-same-cycle-as-an-edge case included, which is where
// the interesting failures are.
//
// The sweep drives transactions of every shape the classification has to
// separate: whole numbers of frames, frames plus a stray edge, frames minus one
// edge, a single edge, and no edge at all. And it drives the CPHA-mismatch
// shape explicitly -- a transaction one edge short of a whole number of frames,
// repeated -- because that is what a phase mismatch looks like from here and it
// is the reason this block reports rather than merely counts.
`timescale 1ns/1ps
module spi_slave_cs_tb;
localparam int LEN_W = 6;
localparam int CNT_W = 12;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
// --- the front end of 14.1 --------------------------------------------
logic cpol = 1'b0;
logic sclk_pin = 1'b0;
logic cs_n_pin = 1'b1;
logic mosi_pin = 1'b0;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb;
wire cs_assert_stb, cs_deassert_stb;
wire [11:0] min_half;
wire ratio_err;
spi_slave_frontend #(.SYNC_N(2), .HALF_MIN(3), .CNT_W(12)) u_fe (
.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(1'b0)
);
// --- 14.2, the block under test ---------------------------------------
logic [LEN_W-1:0] len = 6'd8;
wire txn_active, txn_start_stb, txn_end_stb;
wire [CNT_W-1:0] edges_in_txn, frames_in_txn;
wire txn_clean, txn_trunc, txn_empty;
wire [2:0] state_id;
spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.len(len),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_end_stb(txn_end_stb),
.edges_in_txn(edges_in_txn), .frames_in_txn(frames_in_txn),
.txn_clean(txn_clean), .txn_trunc(txn_trunc), .txn_empty(txn_empty),
.txn_report_stb(txn_report_stb),
.state_id(state_id)
);
// --- monitors ----------------------------------------------------------
integer n_start, n_end;
integer idle_edges_seen; // edges that arrived while deselected
integer idle_edges_counted; // ... and were wrongly counted: must stay zero
integer both_flags; // clean and truncated together
integer active_no_cs; // active more than one cycle past the deassert
logic [CNT_W-1:0] edges_q;
logic cs_active_q;
always_ff @(posedge clk) begin
if (rst_n) begin
edges_q <= edges_in_txn;
cs_active_q <= cs_active;
if (txn_start_stb) n_start <= n_start + 1;
if (txn_end_stb) n_end <= n_end + 1;
// An SCLK edge with the select released is legal and must be
// IGNORED, not absent -- so what is checked is that the edge count
// did not move, rather than that no edge occurred.
if ((edge_a_stb || edge_b_stb) && !cs_active)
idle_edges_seen <= idle_edges_seen + 1;
if (!cs_active && !cs_active_q && edges_in_txn !== edges_q)
idle_edges_counted <= idle_edges_counted + 1;
if (txn_clean && txn_trunc) both_flags <= both_flags + 1;
// The machine needs one cycle to leave ACTIVE after the recovered
// deassert, so a single cycle of overlap is the design and not a
// fault. More than one would be.
if (txn_active && !cs_active && !cs_active_q)
active_no_cs <= active_no_cs + 1;
end
end
integer errors = 0;
task automatic adv(input integer n);
begin repeat (n) @(negedge clk); end
endtask
// Drives exactly `n_edges` SCLK edges inside one chip-select assertion, then
// deasserts. Driving EDGES rather than bits is what lets the test produce a
// transaction that is not a whole number of frames -- which is the case the
// classification exists for and which a bit-oriented driver cannot express.
//
// `prompt` deasserts on the cycle immediately after the last edge, which is
// the same-cycle case at the recovered end of the front end.
task automatic drive_edges(input integer n_edges, input bit pol,
input integer half, input bit prompt);
integer i;
begin
cpol = pol;
sclk_pin = pol;
cs_n_pin = 1'b1;
adv(8);
cs_n_pin = 1'b0;
adv(4);
for (i = 0; i < n_edges; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? ~pol : pol;
adv(half);
end
if (!prompt) adv(4);
cs_n_pin = 1'b1;
// An ODD number of edges leaves SCLK away from its idle level, and
// returning it is itself an edge. So the return happens only after
// the deassert has been recovered -- otherwise the driver delivers
// one more edge than it asked for, and every odd count in this test
// becomes an even one. (The deselected edge that results is not a
// problem: an SCLK edge with the select released must be ignored,
// and checking that it IS ignored is one of the properties below.)
adv(8);
sclk_pin = pol;
adv(8);
end
endtask
task automatic expect_txn(input integer want_edges, input integer want_frames,
input bit want_clean, input bit want_trunc,
input bit want_empty, input string tag);
begin
if (edges_in_txn != want_edges) begin
$display(" FAIL: %0s counted %0d edges, expected %0d",
tag, edges_in_txn, want_edges);
errors = errors + 1;
end
if (frames_in_txn != want_frames) begin
$display(" FAIL: %0s counted %0d frames, expected %0d",
tag, frames_in_txn, want_frames);
errors = errors + 1;
end
if (txn_clean !== want_clean || txn_trunc !== want_trunc ||
txn_empty !== want_empty) begin
$display(" FAIL: %0s classified clean=%0b trunc=%0b empty=%0b, expected %0b/%0b/%0b",
tag, txn_clean, txn_trunc, txn_empty,
want_clean, want_trunc, want_empty);
errors = errors + 1;
end
end
endtask
integer k, p, nf, bad;
initial begin
n_start = 0; n_end = 0; both_flags = 0;
idle_edges_seen = 0; idle_edges_counted = 0; active_no_cs = 0;
edges_q = 0; cs_active_q = 0;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. WHOLE FRAMES. One, two and four frames of eight bits: 16, 32 and 64
// edges, and each must classify as clean.
for (nf = 1; nf <= 4; nf = nf + 1) begin
drive_edges(nf * 16, 1'b0, 4, 1'b0);
expect_txn(nf * 16, nf, 1'b1, 1'b0, 1'b0, "whole frames");
end
$display(" whole transactions of 1, 2, 3 and 4 eight-bit frames all classified clean");
// 2. ONE EDGE TOO MANY. 17 edges is two frames' worth of nothing: a
// slave that only counted frames would report one frame and be right,
// and would say nothing about the stray edge.
drive_edges(17, 1'b0, 4, 1'b0);
expect_txn(17, 1, 1'b0, 1'b1, 1'b0, "one edge too many");
$display(" 17 edges: one complete frame and a stray edge -- reported truncated, with the edge count naming the surplus");
// 3. ONE EDGE TOO FEW. This is the CPHA-mismatch shape, and it is the
// reason this block reports rather than merely counts.
drive_edges(15, 1'b0, 4, 1'b0);
expect_txn(15, 0, 1'b0, 1'b1, 1'b0, "one edge too few");
$display(" 15 edges: no frame completed at all -- the signature a phase mismatch produces on every transaction");
// 4. A SINGLE EDGE, and NO edge. Both are transactions; only one of them
// is empty.
drive_edges(1, 1'b0, 4, 1'b0);
expect_txn(1, 0, 1'b0, 1'b1, 1'b0, "a single edge");
drive_edges(0, 1'b0, 4, 1'b0);
expect_txn(0, 0, 1'b0, 1'b0, 1'b1, "no edges");
$display(" a one-edge transaction is truncated; a zero-edge one is empty, and the two are distinguished");
// 5. A PROMPT DEASSERT. The select releases on the cycle after the final
// edge, so the deassert and the edge can be recovered on the same
// cycle. The final edge must still be counted -- it happened before
// the release -- and ordering it the other way loses the last bit of
// every transaction whose master is not sluggish.
for (p = 0; p <= 1; p = p + 1) begin
drive_edges(16, p[0], 4, 1'b1);
expect_txn(16, 1, 1'b1, 1'b0, 1'b0, "prompt deassert");
drive_edges(16, p[0], 2, 1'b1);
expect_txn(16, 1, 1'b1, 1'b0, 1'b0, "prompt deassert, tight ratio");
end
$display(" a deassert immediately after the final edge still counts that edge, at both polarities and at a tight ratio");
// 6. FRAME WIDTHS. The boundary is 2*len edges, so a width change moves
// what counts as clean without any other change.
len = 6'd4;
adv(2);
drive_edges(8, 1'b0, 4, 1'b0);
expect_txn(8, 1, 1'b1, 1'b0, 1'b0, "len=4, 8 edges");
drive_edges(16, 1'b0, 4, 1'b0);
expect_txn(16, 2, 1'b1, 1'b0, 1'b0, "len=4, 16 edges");
len = 6'd1;
adv(2);
drive_edges(2, 1'b0, 4, 1'b0);
expect_txn(2, 1, 1'b1, 1'b0, 1'b0, "len=1, 2 edges");
drive_edges(1, 1'b0, 4, 1'b0);
expect_txn(1, 0, 1'b0, 1'b1, 1'b0, "len=1, 1 edge");
len = 6'd32;
adv(2);
drive_edges(64, 1'b0, 4, 1'b0);
expect_txn(64, 1, 1'b1, 1'b0, 1'b0, "len=32, 64 edges");
len = 6'd8;
adv(2);
$display(" widths of 1, 4, 8 and 32 bits each move the frame boundary to 2*len edges with no other change");
// 7. A REPEATED PHASE-MISMATCH TRANSACTION. The point is that the
// signature is CONSISTENT: every transaction truncated by exactly one
// edge, which is what distinguishes a mismatch from a glitch.
bad = 0;
for (k = 0; k < 6; k = k + 1) begin
drive_edges(16 * (k + 1) - 1, 1'b0, 4, 1'b0);
if (!txn_trunc) bad = bad + 1;
if (frames_in_txn != k) bad = bad + 1;
end
if (bad != 0) begin
$display(" FAIL: the repeated phase-mismatch shape misclassified %0d times", bad);
errors = errors + 1;
end
$display(" six transactions each one edge short: all truncated, each completing one frame fewer than a matched slave would");
// 8. THE CONTINUOUS PROPERTIES.
if (n_start != n_end) begin
$display(" FAIL: %0d starts and %0d ends", n_start, n_end);
errors = errors + 1;
end
if (idle_edges_counted != 0) begin
$display(" FAIL: %0d edges arriving while deselected were counted",
idle_edges_counted);
errors = errors + 1;
end
if (idle_edges_seen == 0) begin
$display(" FAIL: no edge ever arrived while deselected -- that property is untested");
errors = errors + 1;
end
if (both_flags != 0) begin
$display(" FAIL: %0d cycles reported clean and truncated together",
both_flags);
errors = errors + 1;
end
if (active_no_cs != 0) begin
$display(" FAIL: %0d cycles stayed active more than one cycle past the deassert",
active_no_cs);
errors = errors + 1;
end
$display(" %0d transactions, %0d starts and %0d ends; %0d edges arrived while deselected and none was counted; never two classifications at once",
n_start, n_start, n_end, idle_edges_seen);
if (errors == 0)
$display("PASS: the transaction boundary is taken from the select pin alone and every counter is cleared by it, so nothing the slave counts can redefine where a frame ends -- whole transactions of one to four frames classify clean while one edge too many and one edge too few are both reported as truncated with an edge count that names the discrepancy, a single-edge transaction is distinguished from an empty one, a deassert on the cycle after the final edge still counts that edge at both polarities and at a tight ratio, widths of 1, 4, 8 and 32 bits move the boundary with no other change, six consecutive transactions each one edge short produce the consistent signature a phase mismatch leaves, and across %0d transactions the %0d SCLK edges that arrived while deselected were all ignored and no transaction was ever classified two ways at once",
n_start, idle_edges_seen);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_slave_cs_tb.v
//
// Driven through the real front end of Chapter 14.1, so the transaction
// boundaries under test are the RECOVERED ones -- with the synchroniser latency
// and the deassert-on-the-same-cycle-as-an-edge case included, which is where
// the interesting failures are.
//
// The sweep drives transactions of every shape the classification has to
// separate: whole numbers of frames, frames plus a stray edge, frames minus one
// edge, a single edge, and no edge at all. And it drives the CPHA-mismatch
// shape explicitly -- a transaction one edge short of a whole number of frames,
// repeated -- because that is what a phase mismatch looks like from here and it
// is the reason this block reports rather than merely counts.
`timescale 1ns/1ps
module spi_slave_cs_tb;
localparam LEN_W = 6;
localparam CNT_W = 12;
reg clk;
reg rst_n;
always #5 clk = ~clk;
// --- the front end of 14.1 --------------------------------------------
reg cpol;
reg sclk_pin;
reg cs_n_pin;
reg mosi_pin;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb;
wire cs_assert_stb, cs_deassert_stb;
wire [11:0] min_half;
wire ratio_err;
spi_slave_frontend #(.SYNC_N(2), .HALF_MIN(3), .CNT_W(12)) u_fe (
.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(1'b0)
);
// --- 14.2, the block under test ---------------------------------------
reg [LEN_W-1:0] len;
wire txn_active, txn_start_stb, txn_end_stb;
wire [CNT_W-1:0] edges_in_txn, frames_in_txn;
wire txn_clean, txn_trunc, txn_empty;
wire [2:0] state_id;
spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(CNT_W)) dut (
.clk(clk), .rst_n(rst_n),
.cs_assert_stb(cs_assert_stb), .cs_deassert_stb(cs_deassert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.len(len),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_end_stb(txn_end_stb),
.edges_in_txn(edges_in_txn), .frames_in_txn(frames_in_txn),
.txn_clean(txn_clean), .txn_trunc(txn_trunc), .txn_empty(txn_empty),
.txn_report_stb(txn_report_stb),
.state_id(state_id)
);
// --- monitors ----------------------------------------------------------
integer n_start, n_end;
integer idle_edges_seen; // edges that arrived while deselected
integer idle_edges_counted; // ... and were wrongly counted: must stay zero
integer both_flags; // clean and truncated together
integer active_no_cs; // active more than one cycle past the deassert
reg [CNT_W-1:0] edges_q;
reg cs_active_q;
always @(posedge clk) begin
if (rst_n) begin
edges_q <= edges_in_txn;
cs_active_q <= cs_active;
if (txn_start_stb) n_start <= n_start + 1;
if (txn_end_stb) n_end <= n_end + 1;
// An SCLK edge with the select released is legal and must be
// IGNORED, not absent -- so what is checked is that the edge count
// did not move, rather than that no edge occurred.
if ((edge_a_stb || edge_b_stb) && !cs_active)
idle_edges_seen <= idle_edges_seen + 1;
if (!cs_active && !cs_active_q && edges_in_txn !== edges_q)
idle_edges_counted <= idle_edges_counted + 1;
if (txn_clean && txn_trunc) both_flags <= both_flags + 1;
// The machine needs one cycle to leave ACTIVE after the recovered
// deassert, so a single cycle of overlap is the design and not a
// fault. More than one would be.
if (txn_active && !cs_active && !cs_active_q)
active_no_cs <= active_no_cs + 1;
end
end
integer errors;
task adv;
input integer n;
begin repeat (n) @(negedge clk); end
endtask
// Drives exactly `n_edges` SCLK edges inside one chip-select assertion, then
// deasserts. Driving EDGES rather than bits is what lets the test produce a
// transaction that is not a whole number of frames -- which is the case the
// classification exists for and which a bit-oriented driver cannot express.
//
// `prompt` deasserts on the cycle immediately after the last edge, which is
// the same-cycle case at the recovered end of the front end.
task drive_edges;
input integer n_edges;
input pol;
input integer half;
input prompt;
integer i;
begin
cpol = pol;
sclk_pin = pol;
cs_n_pin = 1'b1;
adv(8);
cs_n_pin = 1'b0;
adv(4);
for (i = 0; i < n_edges; i = i + 1) begin
sclk_pin = (i % 2 == 0) ? ~pol : pol;
adv(half);
end
if (!prompt) adv(4);
cs_n_pin = 1'b1;
// An ODD number of edges leaves SCLK away from its idle level, and
// returning it is itself an edge. So the return happens only after
// the deassert has been recovered -- otherwise the driver delivers
// one more edge than it asked for, and every odd count in this test
// becomes an even one. (The deselected edge that results is not a
// problem: an SCLK edge with the select released must be ignored,
// and checking that it IS ignored is one of the properties below.)
adv(8);
sclk_pin = pol;
adv(8);
end
endtask
task expect_txn;
input integer want_edges;
input integer want_frames;
input want_clean;
input want_trunc;
input want_empty;
input [8*40:1] tag;
begin
if (edges_in_txn != want_edges) begin
$display(" FAIL: %0s counted %0d edges, expected %0d",
tag, edges_in_txn, want_edges);
errors = errors + 1;
end
if (frames_in_txn != want_frames) begin
$display(" FAIL: %0s counted %0d frames, expected %0d",
tag, frames_in_txn, want_frames);
errors = errors + 1;
end
if (txn_clean !== want_clean || txn_trunc !== want_trunc ||
txn_empty !== want_empty) begin
$display(" FAIL: %0s classified clean=%0b trunc=%0b empty=%0b, expected %0b/%0b/%0b",
tag, txn_clean, txn_trunc, txn_empty,
want_clean, want_trunc, want_empty);
errors = errors + 1;
end
end
endtask
integer k, p, nf, bad;
initial begin
n_start = 0; n_end = 0; both_flags = 0;
idle_edges_seen = 0; idle_edges_counted = 0; active_no_cs = 0;
edges_q = 0; cs_active_q = 0;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. WHOLE FRAMES. One, two and four frames of eight bits: 16, 32 and 64
// edges, and each must classify as clean.
for (nf = 1; nf <= 4; nf = nf + 1) begin
drive_edges(nf * 16, 1'b0, 4, 1'b0);
expect_txn(nf * 16, nf, 1'b1, 1'b0, 1'b0, "whole frames");
end
$display(" whole transactions of 1, 2, 3 and 4 eight-bit frames all classified clean");
// 2. ONE EDGE TOO MANY. 17 edges is two frames' worth of nothing: a
// slave that only counted frames would report one frame and be right,
// and would say nothing about the stray edge.
drive_edges(17, 1'b0, 4, 1'b0);
expect_txn(17, 1, 1'b0, 1'b1, 1'b0, "one edge too many");
$display(" 17 edges: one complete frame and a stray edge -- reported truncated, with the edge count naming the surplus");
// 3. ONE EDGE TOO FEW. This is the CPHA-mismatch shape, and it is the
// reason this block reports rather than merely counts.
drive_edges(15, 1'b0, 4, 1'b0);
expect_txn(15, 0, 1'b0, 1'b1, 1'b0, "one edge too few");
$display(" 15 edges: no frame completed at all -- the signature a phase mismatch produces on every transaction");
// 4. A SINGLE EDGE, and NO edge. Both are transactions; only one of them
// is empty.
drive_edges(1, 1'b0, 4, 1'b0);
expect_txn(1, 0, 1'b0, 1'b1, 1'b0, "a single edge");
drive_edges(0, 1'b0, 4, 1'b0);
expect_txn(0, 0, 1'b0, 1'b0, 1'b1, "no edges");
$display(" a one-edge transaction is truncated; a zero-edge one is empty, and the two are distinguished");
// 5. A PROMPT DEASSERT. The select releases on the cycle after the final
// edge, so the deassert and the edge can be recovered on the same
// cycle. The final edge must still be counted -- it happened before
// the release -- and ordering it the other way loses the last bit of
// every transaction whose master is not sluggish.
for (p = 0; p <= 1; p = p + 1) begin
drive_edges(16, p[0], 4, 1'b1);
expect_txn(16, 1, 1'b1, 1'b0, 1'b0, "prompt deassert");
drive_edges(16, p[0], 2, 1'b1);
expect_txn(16, 1, 1'b1, 1'b0, 1'b0, "prompt deassert, tight ratio");
end
$display(" a deassert immediately after the final edge still counts that edge, at both polarities and at a tight ratio");
// 6. FRAME WIDTHS. The boundary is 2*len edges, so a width change moves
// what counts as clean without any other change.
len = 6'd4;
adv(2);
drive_edges(8, 1'b0, 4, 1'b0);
expect_txn(8, 1, 1'b1, 1'b0, 1'b0, "len=4, 8 edges");
drive_edges(16, 1'b0, 4, 1'b0);
expect_txn(16, 2, 1'b1, 1'b0, 1'b0, "len=4, 16 edges");
len = 6'd1;
adv(2);
drive_edges(2, 1'b0, 4, 1'b0);
expect_txn(2, 1, 1'b1, 1'b0, 1'b0, "len=1, 2 edges");
drive_edges(1, 1'b0, 4, 1'b0);
expect_txn(1, 0, 1'b0, 1'b1, 1'b0, "len=1, 1 edge");
len = 6'd32;
adv(2);
drive_edges(64, 1'b0, 4, 1'b0);
expect_txn(64, 1, 1'b1, 1'b0, 1'b0, "len=32, 64 edges");
len = 6'd8;
adv(2);
$display(" widths of 1, 4, 8 and 32 bits each move the frame boundary to 2*len edges with no other change");
// 7. A REPEATED PHASE-MISMATCH TRANSACTION. The point is that the
// signature is CONSISTENT: every transaction truncated by exactly one
// edge, which is what distinguishes a mismatch from a glitch.
bad = 0;
for (k = 0; k < 6; k = k + 1) begin
drive_edges(16 * (k + 1) - 1, 1'b0, 4, 1'b0);
if (!txn_trunc) bad = bad + 1;
if (frames_in_txn != k) bad = bad + 1;
end
if (bad != 0) begin
$display(" FAIL: the repeated phase-mismatch shape misclassified %0d times", bad);
errors = errors + 1;
end
$display(" six transactions each one edge short: all truncated, each completing one frame fewer than a matched slave would");
// 8. THE CONTINUOUS PROPERTIES.
if (n_start != n_end) begin
$display(" FAIL: %0d starts and %0d ends", n_start, n_end);
errors = errors + 1;
end
if (idle_edges_counted != 0) begin
$display(" FAIL: %0d edges arriving while deselected were counted",
idle_edges_counted);
errors = errors + 1;
end
if (idle_edges_seen == 0) begin
$display(" FAIL: no edge ever arrived while deselected -- that property is untested");
errors = errors + 1;
end
if (both_flags != 0) begin
$display(" FAIL: %0d cycles reported clean and truncated together",
both_flags);
errors = errors + 1;
end
if (active_no_cs != 0) begin
$display(" FAIL: %0d cycles stayed active more than one cycle past the deassert",
active_no_cs);
errors = errors + 1;
end
$display(" %0d transactions, %0d starts and %0d ends; %0d edges arrived while deselected and none was counted; never two classifications at once",
n_start, n_start, n_end, idle_edges_seen);
if (errors == 0)
$display("PASS: the transaction boundary is taken from the select pin alone and every counter is cleared by it, so nothing the slave counts can redefine where a frame ends -- whole transactions of one to four frames classify clean while one edge too many and one edge too few are both reported as truncated with an edge count that names the discrepancy, a single-edge transaction is distinguished from an empty one, a deassert on the cycle after the final edge still counts that edge at both polarities and at a tight ratio, widths of 1, 4, 8 and 32 bits move the boundary with no other change, six consecutive transactions each one edge short produce the consistent signature a phase mismatch leaves, and across %0d transactions the %0d SCLK edges that arrived while deselected were all ignored and no transaction was ever classified two ways at once",
n_start, idle_edges_seen);
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;
len = 6'd8;
errors = 0;
end
endmodule-- spi_slave_cs_tb.vhd
--
-- Driven through the real front end of Chapter 14.1, so the transaction
-- boundaries under test are the RECOVERED ones -- with the synchroniser latency
-- and the deassert-on-the-same-cycle-as-an-edge case included, which is where the
-- interesting failures are.
--
-- The sweep drives transactions of every shape the classification has to
-- separate: whole numbers of frames, frames plus a stray edge, frames minus one
-- edge, a single edge, and no edge at all. And it drives the CPHA-mismatch shape
-- explicitly -- a transaction one edge short of a whole number of frames,
-- repeated -- because that is what a phase mismatch looks like from here.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_cs_tb is
end entity;
architecture sim of spi_slave_cs_tb is
constant LEN_W : positive := 6;
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 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(11 downto 0);
signal ratio_err : std_logic;
signal len : unsigned(LEN_W - 1 downto 0) := to_unsigned(8, LEN_W);
signal txn_active, txn_start_stb, txn_end_stb : std_logic;
signal edges_in_txn, frames_in_txn : unsigned(CNT_W - 1 downto 0);
signal txn_clean, txn_trunc, txn_empty, txn_report_stb : std_logic;
signal state_id : unsigned(2 downto 0);
signal n_start, n_end : natural := 0;
signal idle_edges_seen, idle_edges_counted : natural := 0;
signal both_flags, active_no_cs : natural := 0;
signal edges_q : unsigned(CNT_W - 1 downto 0) := (others => '0');
signal cs_active_q : std_logic := '0';
signal errors : natural := 0;
begin
clk <= not clk after 5 ns when not halt else '0';
u_fe : entity work.spi_slave_frontend
generic map (SYNC_N => 2, HALF_MIN => 3, CNT_W => 12)
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 => '0');
dut : entity work.spi_slave_cs
generic map (LEN_W => LEN_W, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
cs_assert_stb => cs_assert_stb,
cs_deassert_stb => cs_deassert_stb,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
len => len,
txn_active => txn_active, txn_start_stb => txn_start_stb,
txn_end_stb => txn_end_stb,
edges_in_txn => edges_in_txn, frames_in_txn => frames_in_txn,
txn_clean => txn_clean, txn_trunc => txn_trunc,
txn_empty => txn_empty, state_id => state_id);
monitor : process (clk)
begin
if rising_edge(clk) then
if rst_n = '1' then
edges_q <= edges_in_txn;
cs_active_q <= cs_active;
if txn_start_stb = '1' then n_start <= n_start + 1; end if;
if txn_end_stb = '1' then n_end <= n_end + 1; end if;
-- An SCLK edge with the select released is legal and must be
-- IGNORED, not absent -- so what is checked is that the edge
-- count did not move, rather than that no edge occurred.
if (edge_a_stb = '1' or edge_b_stb = '1') and cs_active = '0' then
idle_edges_seen <= idle_edges_seen + 1;
end if;
if cs_active = '0' and cs_active_q = '0' and
edges_in_txn /= edges_q then
idle_edges_counted <= idle_edges_counted + 1;
end if;
if txn_clean = '1' and txn_trunc = '1' then
both_flags <= both_flags + 1;
end if;
-- The machine needs one cycle to leave ACTIVE after the recovered
-- deassert, so a single cycle of overlap is the design and not a
-- fault. More than one would be.
if txn_active = '1' and cs_active = '0' and cs_active_q = '0' then
active_no_cs <= active_no_cs + 1;
end if;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable bad : natural;
procedure adv(n : natural) is
begin
for k in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
-- Drives exactly `n_edges` SCLK edges inside one assertion. Driving EDGES
-- rather than bits is what lets the test produce a transaction that is not
-- a whole number of frames -- the case the classification exists for and
-- which a bit-oriented driver cannot express.
procedure drive_edges(n_edges : natural; pol : std_logic;
half : natural; prompt : boolean) is
begin
cpol <= pol;
sclk_pin <= pol;
cs_n_pin <= '1';
adv(8);
cs_n_pin <= '0';
adv(4);
for i in 0 to n_edges - 1 loop
if (i mod 2) = 0 then
sclk_pin <= not pol;
else
sclk_pin <= pol;
end if;
adv(half);
end loop;
if not prompt then adv(4); end if;
cs_n_pin <= '1';
-- An ODD number of edges leaves SCLK away from its idle level, and
-- returning it is itself an edge. So the return happens only after
-- the deassert has been recovered -- otherwise the driver delivers
-- one more edge than it asked for. The deselected edge that results
-- is not a problem: an SCLK edge with the select released must be
-- ignored, and checking that it IS ignored is one of the properties.
adv(8);
sclk_pin <= pol;
adv(8);
end procedure;
procedure expect_txn(want_edges : natural; want_frames : natural;
want_clean : std_logic; want_trunc : std_logic;
want_empty : std_logic; tag : string) is
begin
if to_integer(edges_in_txn) /= want_edges then
report " FAIL: " & tag & " counted " &
integer'image(to_integer(edges_in_txn)) &
" edges, expected " & integer'image(want_edges);
errs := errs + 1;
end if;
if to_integer(frames_in_txn) /= want_frames then
report " FAIL: " & tag & " counted " &
integer'image(to_integer(frames_in_txn)) &
" frames, expected " & integer'image(want_frames);
errs := errs + 1;
end if;
if txn_clean /= want_clean or txn_trunc /= want_trunc or
txn_empty /= want_empty then
report " FAIL: " & tag & " classified wrongly";
errs := errs + 1;
end if;
end procedure;
variable pol_v : std_logic;
begin
adv(3);
rst_n <= '1';
adv(2);
-- 1. WHOLE FRAMES.
for nf in 1 to 4 loop
drive_edges(nf * 16, '0', 4, false);
expect_txn(nf * 16, nf, '1', '0', '0', "whole frames");
end loop;
report " whole transactions of 1, 2, 3 and 4 eight-bit frames all classified clean";
-- 2. ONE EDGE TOO MANY.
drive_edges(17, '0', 4, false);
expect_txn(17, 1, '0', '1', '0', "one edge too many");
report " 17 edges: one complete frame and a stray edge -- reported truncated, with the edge count naming the surplus";
-- 3. ONE EDGE TOO FEW -- the CPHA-mismatch shape.
drive_edges(15, '0', 4, false);
expect_txn(15, 0, '0', '1', '0', "one edge too few");
report " 15 edges: no frame completed at all -- the signature a phase mismatch produces on every transaction";
-- 4. A SINGLE EDGE, and NO edge.
drive_edges(1, '0', 4, false);
expect_txn(1, 0, '0', '1', '0', "a single edge");
drive_edges(0, '0', 4, false);
expect_txn(0, 0, '0', '0', '1', "no edges");
report " a one-edge transaction is truncated; a zero-edge one is empty, and the two are distinguished";
-- 5. A PROMPT DEASSERT.
for p in 0 to 1 loop
if p = 1 then pol_v := '1'; else pol_v := '0'; end if;
drive_edges(16, pol_v, 4, true);
expect_txn(16, 1, '1', '0', '0', "prompt deassert");
drive_edges(16, pol_v, 2, true);
expect_txn(16, 1, '1', '0', '0', "prompt deassert, tight ratio");
end loop;
report " a deassert immediately after the final edge still counts that edge, at both polarities and at a tight ratio";
-- 6. FRAME WIDTHS.
len <= to_unsigned(4, LEN_W);
adv(2);
drive_edges(8, '0', 4, false);
expect_txn(8, 1, '1', '0', '0', "len=4, 8 edges");
drive_edges(16, '0', 4, false);
expect_txn(16, 2, '1', '0', '0', "len=4, 16 edges");
len <= to_unsigned(1, LEN_W);
adv(2);
drive_edges(2, '0', 4, false);
expect_txn(2, 1, '1', '0', '0', "len=1, 2 edges");
drive_edges(1, '0', 4, false);
expect_txn(1, 0, '0', '1', '0', "len=1, 1 edge");
len <= to_unsigned(32, LEN_W);
adv(2);
drive_edges(64, '0', 4, false);
expect_txn(64, 1, '1', '0', '0', "len=32, 64 edges");
len <= to_unsigned(8, LEN_W);
adv(2);
report " widths of 1, 4, 8 and 32 bits each move the frame boundary to 2*len edges with no other change";
-- 7. A REPEATED PHASE-MISMATCH TRANSACTION.
bad := 0;
for k in 0 to 5 loop
drive_edges(16 * (k + 1) - 1, '0', 4, false);
if txn_trunc /= '1' then bad := bad + 1; end if;
if to_integer(frames_in_txn) /= k then bad := bad + 1; end if;
end loop;
if bad /= 0 then
report " FAIL: the repeated phase-mismatch shape misclassified " &
integer'image(bad) & " times";
errs := errs + 1;
end if;
report " six transactions each one edge short: all truncated, each completing one frame fewer than a matched slave would";
-- 8. THE CONTINUOUS PROPERTIES.
if n_start /= n_end then
report " FAIL: " & integer'image(n_start) & " starts and " &
integer'image(n_end) & " ends";
errs := errs + 1;
end if;
if idle_edges_counted /= 0 then
report " FAIL: " & integer'image(idle_edges_counted) &
" edges arriving while deselected were counted";
errs := errs + 1;
end if;
if idle_edges_seen = 0 then
report " FAIL: no edge ever arrived while deselected -- that property is untested";
errs := errs + 1;
end if;
if both_flags /= 0 then
report " FAIL: " & integer'image(both_flags) &
" cycles reported clean and truncated together";
errs := errs + 1;
end if;
if active_no_cs /= 0 then
report " FAIL: " & integer'image(active_no_cs) &
" cycles stayed active more than one cycle past the deassert";
errs := errs + 1;
end if;
report " " & integer'image(n_start) & " transactions, " &
integer'image(n_start) & " starts and " & integer'image(n_end) &
" ends; " & integer'image(idle_edges_seen) &
" edges arrived while deselected and none was counted; never two classifications at once";
errors <= errs;
if errs = 0 then
report "PASS: the transaction boundary is taken from the select pin alone and every counter is cleared by it, so nothing the slave counts can redefine where a frame ends -- whole transactions of one to four frames classify clean while one edge too many and one edge too few are both reported as truncated with an edge count that names the discrepancy, a single-edge transaction is distinguished from an empty one, a deassert on the cycle after the final edge still counts that edge at both polarities and at a tight ratio, widths of 1, 4, 8 and 32 bits move the boundary with no other change, six consecutive transactions each one edge short produce the consistent signature a phase mismatch leaves, and across " & integer'image(n_start) & " transactions the " & integer'image(idle_edges_seen) & " SCLK edges that arrived while deselected were all ignored and no transaction was ever classified two ways at once";
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 interesting properties here are all about boundaries, and boundaries are where benches are laziest. A bench that always deasserts a comfortable number of cycles after the last edge never tests the prompt deassert of test 5, and a prompt deassert is what a fast master actually does.
The classification flags need a mutual-exclusion check, not three separate checks. txn_clean, txn_trunc and txn_empty partition the outcome space. Checking each one against its own expected condition lets two of them be true at once if both conditions are subtly wrong; checking that exactly one is set at txn_report_stb catches that, and it is one line.
Test 7 is the pattern worth stealing: a test whose assertion is that nothing is reported. It drives a genuinely broken configuration and asserts the block says clean, because that is the truth about this block's reach. A suite without it will grow, later, an assertion that a phase mismatch produces txn_trunc — and that assertion will fail against correct hardware, and someone will "fix" the hardware.
// Properties for the transaction detector. Written as SVA; the simulated benches
// use explicit counters because Icarus Verilog rejects SVA outright.
property p_start_end_exclusive;
// The two boundary strobes can never coincide: a transaction takes at least
// one cycle. If they can coincide, a zero-length transaction exists and
// every downstream counter has a case nobody wrote.
@(posedge clk) disable iff (!rst_n)
not (txn_start_stb && txn_end_stb);
endproperty
property p_active_matches_strobes;
// txn_active rises with the start strobe and falls with the end strobe, and
// nothing else moves it. This is the property every downstream gate assumes.
@(posedge clk) disable iff (!rst_n)
txn_start_stb |=> txn_active throughout (txn_end_stb[->1]);
endproperty
property p_one_classification;
// Exactly one of the three, and only when the verdict is announced.
@(posedge clk) disable iff (!rst_n)
txn_report_stb |-> $onehot({txn_clean, txn_trunc, txn_empty});
endproperty
property p_no_verdict_without_report;
// The flags do not move except at the report strobe, which is what makes it
// safe for a consumer to sample them there and nowhere else.
@(posedge clk) disable iff (!rst_n)
!txn_report_stb |=> $stable({txn_clean, txn_trunc, txn_empty});
endproperty
property p_counters_reset_on_assert;
// Every counter resets on the assert. This is the rule from section 1, and
// it is checkable directly: one cycle after a start, both counters are zero.
@(posedge clk) disable iff (!rst_n)
txn_start_stb |=> (edges_in_txn == 0 && frames_in_txn == 0);
endproperty
property p_edges_monotone_in_txn;
// Within a transaction the edge count only increases. A count that ever
// decreases means something other than the assert reset it.
@(posedge clk) disable iff (!rst_n)
(txn_active && !txn_start_stb) |=> (edges_in_txn >= $past(edges_in_txn));
endproperty// Coverage. The axes are the edge count's RELATIONSHIP to the frame boundary and
// the frame width, because the block's whole job is that relationship.
covergroup cg_txn @(posedge clk iff txn_report_stb);
option.per_instance = 1;
// Not "how many edges" but "how many edges relative to a frame boundary",
// which is the only thing the classification depends on.
offset: coverpoint edge_offset_from_boundary {
bins exact = {0}; // clean
bins one_over = {1}; // a spurious edge, or a bench error
bins one_under = {-1}; // a lost edge, or an abort, or CPHA noise
bins mid = {[2:$]}; // a genuine mid-frame abort
bins under = {[$:-2]};
}
frames: coverpoint frames_in_txn {
bins none = {0}; // empty or sub-frame
bins one = {1};
bins few = {[2:4]};
bins burst = {[5:$]};
}
// The width must be crossed with the offset, because the boundary arithmetic
// is the only place width appears -- a suite that only ever runs at 8 bits
// has tested one row of a table with LEN_W rows.
width: coverpoint len {
bins w4 = {4}; bins w8 = {8}; bins w12 = {12};
bins w16 = {16}; bins w32 = {32};
}
verdict: coverpoint {txn_clean, txn_trunc, txn_empty} {
bins clean = {3'b100};
bins trunc = {3'b010};
bins empty = {3'b001};
}
x_offset_width: cross offset, width;
x_verdict_frames: cross verdict, frames;
endgroup8. Why an FPGA or ASIC Engineer Cares
There is no arithmetic here worth worrying about, and that is the result of a decision rather than luck. The running remainder replaced a modulo by a run-time value. Had the design computed edges % (2*len) at the deassert, the block would contain a divider — a variable-width one, on a path with one cycle to complete it, in a block whose entire remaining logic is three flops and a comparator.
CNT_W sets the longest transaction that can be measured, not the longest that works. At CNT_W = 12 the counter reaches 4095 edges, which at 8 bits per frame is 255 frames under one select. A longer transaction still works — the classification uses the remainder, not the total — but edges_in_txn saturates or wraps and the diagnostic value is lost. If a system does 1 KB bursts, the counter needs to be wide enough to say so.
The state register is two bits and state_id is published three bits wide. That is deliberate: state_id exists for waveform capture and for the debug register of Chapter 14.9, and a padded encoding means adding a state later does not change the width of a field software already reads.
Nothing in this block is on a timing-critical path. It runs at the system clock, it is fed by strobes, and its widest combinational path is the remainder comparator. If a slave fails timing, it is not here — which is worth knowing because the block looks like the busy one.
9. Failure Signature — A Slave That Is Fine Until Software Changes The Frame Width
The symptom:
"Works perfectly at 8 bits. We changed one command to 16-bit frames and now that command reports a truncated transaction every time — but the data is correct."
What is happening: the master's frame width was changed and the slave's was not, or vice versa. The slave's len says 8, the master is sending 16-bit frames, and 32 edges is a whole number of 8-bit frames — so this block says clean, and the receive path assembles two 8-bit words instead of one 16-bit word. The data is "correct" in the sense that every bit arrived; it is wrong in the sense that it is cut in the wrong place.
The interesting version is the reverse: slave len is 16 and the master sends one 8-bit frame. That is 16 edges, in_frame ends at 16 of 32, and the slave reports truncated — correctly, and unhelpfully, because the fault is a width disagreement rather than a truncation.
How to tell them apart in one read: edges_in_txn and frames_in_txn together. 32 edges with 4 frames at len = 8 when software expected one 16-bit frame is a width disagreement, not an abort. This is why the numbers are published: the flag says something is wrong, and the numbers say what.
10. Common Misconceptions
"A bit counter should wrap at the frame width — that is what a frame boundary means." It is what a frame boundary means when the counter is right. A counter that wraps on its own arithmetic has made itself the authority on frame boundaries, and an authority that can be wrong and cannot notice is worse than no authority. CS assert is the only thing that can reset it correctly, because CS assert is the only framing the protocol has.
"A CPHA mismatch shows up as a truncated transaction." It does not. A CPHA=1 master sending N bits produces 2N edges and a CPHA=0 slave takes the N leading ones — a whole number of frames, every time. The count agrees and this block says clean. Chapter 14.6 diagnoses it from something else entirely.
"txn_end_stb is the cycle to sample the verdict on." It is one cycle early. The verdict is computed from counters that are still being read on that cycle, and it is announced by txn_report_stb on the next. Sampling at the end strobe gives the previous transaction's verdict, which looks like an off-by-one threshold in whatever consumes it.
"An empty chip-select pulse is a no-op and can be ignored." It is the only evidence the slave can give of a signal-integrity problem on the select line. Reporting it costs a flag; ignoring it costs a lab session.
"Bursts are strictly better than separate transactions because they amortise the select overhead." They amortise the overhead and they extend the blast radius of a single lost edge from one frame to the whole assertion. Which is better depends on whether the system is throughput-limited or reliability-limited, and the point is that it is a trade rather than an improvement.
"The classification needs a modulo, so the block needs a divider." It needs the remainder, and the remainder can be maintained incrementally because the edge count only ever increments by one. A comparator replaces a divider, and the two are equivalent for exactly this reason.
11. Reason It Through
Q. len = 8 and a transaction carries 130 edges. What does the block report, and what are the two most likely causes?
txn_trunc, with edges_in_txn = 130 and frames_in_txn = 8. Eight whole frames used 128 edges and two are left over, so the remainder is non-zero. The two likely causes are a master that sent 130 edges because its bit counter is configured for something other than 8-bit frames, and a bench or driver that toggled SCLK twice while deselected in a way the slave counted — which is why test 8 checks that an edge outside a transaction does not count. Both are visible in the number and neither is visible in the flag.
Q. Why is txn_trunc asserted for a transaction with a single edge, rather than txn_empty?
Because something arrived. txn_empty means the select asserted and no edge came at all, which is a statement about the select line; a single edge means the clock line carried something, which is a statement about a transaction that did not finish. They point at different faults, and merging them would lose the distinction that makes txn_empty worth publishing.
Q. A reviewer proposes deleting S_REPORT and computing the classification combinationally in S_IDLE, arguing it saves a state. What breaks?
Nothing about the values — they would be correct. What breaks is the announcement. Without REPORT there is no cycle that means "the verdict for the transaction that just ended is valid now", so a consumer has to either sample at txn_end_stb (one cycle early, giving the previous verdict) or watch for a change in the flags (which never happens when two consecutive transactions have the same verdict). The state exists to produce a strobe, and the strobe exists because a level cannot say "again".
Q. The running remainder compares against edges_per_frame - 1 rather than resetting when it reaches edges_per_frame. Why does that distinction matter?
Because the reset has to happen on the edge that completes the frame, not on the one after it. Comparing against edges_per_frame and then zeroing means the remainder holds a full frame's worth for one edge period, during which a deassert would classify the transaction as truncated when it ended exactly on a boundary. The comparison is against edges_per_frame - 1 so that the last edge of a frame both increments the frame count and zeroes the remainder in the same cycle.
Q. A system uses 1 KB bursts at 8-bit frames. What must change in this block, and what must not?
CNT_W must grow: 1024 bytes is 16384 edges, which needs 15 bits, and at CNT_W = 12 the counter saturates or wraps and edges_in_txn stops being diagnostic. What must not change is the classification logic — it reads the remainder, which is bounded by 2 × len regardless of transaction length, so txn_clean and txn_trunc stay correct even when the total count has overflowed. That is a useful property to know deliberately rather than by accident: the verdict is width-independent and only the diagnostic needs the bits.
12. Understanding Check
13. Summary
Chip select is the only framing SPI has, and therefore the only resynchronisation point. Two rules follow: every counter resets on the assert rather than on reaching a count, because a counter that defines its own frame boundary stays wrong once it is wrong; and a long transaction is strictly riskier than several short ones, because the blast radius of a single lost edge is the whole assertion.
The block publishes a boundary, a live edge count, a frame count, and a three-way classification. The count is what turns an integration problem from a guess into a fact — "130 edges where 128 were expected" names a fault that no flag can.
txn_report_stb is separate from txn_end_stb because the verdict is not valid on the cycle the transaction ends. Sampling at the end strobe reads the previous verdict, and that appears downstream as an off-by-one threshold.
Classification needs a running remainder, not a modulo. len is a run-time value, so edges % (2*len) is a division by a variable; maintaining the remainder incrementally replaces a divider with a comparator, and the substitution is exact because the count only moves by one.
txn_empty must be reported. It is the only evidence a slave can offer of a signal-integrity problem on the select line.
And the honest limit: the edge count finds a wrong byte count, a lost edge and a genuine abort. It is blind to a CPHA mismatch, which sends a whole number of frames and is classified clean — and blind to a bit-order mismatch, which sends the right bits in the wrong order. Stating that here is what makes Chapter 14.6 necessary rather than optional.
For verification: check the classification flags as a partition rather than individually; randomise the deassert timing, because a prompt deassert is the tight case a comfortable bench never reaches; and keep the test whose assertion is that a broken configuration reports clean, because a suite that later asserts otherwise will fail against correct hardware.
For implementation: no arithmetic worth constraining, CNT_W sized by the longest burst worth diagnosing rather than the longest that works, and a padded state_id so that adding a state later does not change a field software reads.
14. What Comes Next
The transaction boundary exists and the edges inside it are counted. Nothing yet looks at MOSI.
Chapter 14.3 — MOSI Capture and Bit Counting builds the receive path: capture the delay-matched MOSI on the capture strobe, assemble bits into words of the configured width and order, and announce each word exactly once. The interesting part is a one-cycle question — whether the word-valid strobe fires on the cycle of the final capture or the cycle after it — and the answer is forced by something that has nothing to do with the receive path at all.
Continue learning
Related tutorials
- Related topic
Command, Address, and Data Phases
How a device layers a transaction onto a raw byte stream: why the opcode decides the shape of everything after it, how a slave tracks phases with no phase marker, and the sequencer that requires in three HDLs.
- Related topic
Command-Then-Read Sequences
Why every SPI read is a write first, why request and response must share one CS frame, what the master drives once its half is done, what the request costs in bus time, and the master read sequencer in three HDLs.
- Related topic
The Master Control FSM
The five states an SPI master transfer passes through, why the fifth differs from the first by exactly one behaviour, why outputs must be decoded from state alone, and the failure that disappears the moment you add a print statement.
- Related topic
Slave Microarchitecture and Clocking Assumptions
The two architectures available to a slave that does not own its clock, the one precondition the chosen architecture rests on and why no simulation can test it, why MOSI must pass through exactly as many flops as SCLK, and a delay-matched front end verified in three HDLs.
