SPI · Module 14
The System-Side Interface
A slave cannot ask the master to wait and cannot choose when its buffer changes, so it needs two mechanisms and neither is a buffer: a configured transmit default that makes an underrun recognisable at the master, and a sequence lock that makes a torn read detectable rather than merely unlikely.
The master's register interface (Chapter 13.11) has a comfortable property: the master decides when transfers happen, so software is never late in a way the hardware cannot absorb. It can stall the wire — Chapter 13.9 measures exactly that — and nothing is lost.
A slave has neither option.
IT CANNOT ASK THE MASTER TO WAIT
There is no flow control in SPI. If the master clocks out a second word before
software has supplied one, the slave drives SOMETHING -- and the only question
is whether that something is defined.
IT CANNOT CHOOSE WHEN THE RECEIVE BUFFER CHANGES
A word arrives when the master sends it, which may be in the middle of software
reading the previous ones. A reader walking four words can get the first two from
one transaction and the last two from the next, with nothing in the data to say so.So this block is two mechanisms, one per problem, and neither is a buffer.
Software has loaded nothing and the master clocks out a word. What goes on the wire?
1. The Transmit Side: A Defined Default
When a word is taken with nothing loaded, the slave sends tx_default — a configured value, not the previous word and not whatever happens to be in a register. The three options are not equivalent, and the difference is entirely about where the investigation goes:
send the PREVIOUS word an underrun looks like a master that read the same
location twice -- a plausible thing to have happened,
which sends the investigation to the driver's addressing
send whatever is in the the failure depends on history, so it is not
register reproducible, and an intermittent non-reproducible
data fault is the most expensive kind there is
send a DEFINED value conventionally 0x00 or 0xFF, matching what an absent
device looks like -- so the underrun is recognisable
at the master, which is the only place it can be
acted onThe third is the only one where the symptom points at the cause. And tx_underrun is sticky, because the master's read has already happened by the time software could notice — a live flag would read clear whenever software looked.
2. The Receive Side: A Sequence Number, Not A Lock
The obvious fix for a torn read is to stop the buffer changing while software reads it.
A slave cannot. Stopping it means dropping the word the master is sending — which converts a detectable problem into a silent loss. That trade is always wrong: a torn read that software can notice and retry is strictly better than a missing word that nothing can notice.
So the buffer is never held. Instead, every transaction increments rx_seq:
software reads rx_seq
software reads the words
software reads rx_seq again
if it changed, the read was torn -- discard and repeatThat is a seqlock, and it is the standard answer whenever a reader cannot make a writer wait — which is exactly the position software is in here.
Two details make it work rather than nearly work
The sequence number must change on the FIRST word, not at the end of the transaction.
A reader that samples rx_seq, then reads words while a new transaction's first word lands, then samples rx_seq again, must see a different value. If the counter advanced only at the end of the transaction, that reader sees the same number twice and concludes its read was clean — while holding one word from the new transaction. The tear it is trying to detect is invisible precisely in the case the mechanism exists for.
It must be wide enough that it cannot wrap within one read.
Eight bits at any plausible transaction rate is far more than enough. The cost of getting this wrong is a tear reported as clean, which is worse than having no mechanism at all — because software would then trust it.
An overflow flag completes the picture: the buffer is finite, so a transaction longer than it is a loss, and a loss must be reported for the same reason an underrun must.
3. The Block Diagram
4. The Holding Register Frees Immediately
One timing decision worth isolating: the transmit holding register is freed on word_taken_stb, which fires at the start of the word being transmitted rather than at its end.
That gives software a whole word period to supply the next value — at 8 bits and a divisor of 8, that is 64 system clocks rather than the handful it would get if the register were freed at the word's last bit. It is the difference between an interface that a polling driver can keep up with and one that needs an interrupt per byte.
The cost is that the slave commits to a value one word early, so a write that lands after word_taken_stb applies to the following word. That is the correct trade and it is the same shape as Chapter 13.2's configuration snapshot: freeze early, publish what you froze, and report when a write was too late.
5. Building the System Interface — Three HDLs
The circuit
A depth-limited receive buffer read by index, a sequence counter advanced by the first word of each transaction, a one-word transmit holding register with a configured default, and two sticky flags. The buffer read is combinational from sys_rd_idx — a window into the buffer rather than a pointer the slave advances, which is why software has to ask for word zero to read word zero.
// spi_slave_regs.sv
//
// Chapter 14.9 -- the system side, and the two things a slave cannot do.
//
// A master's register interface (Chapter 13.11) has a comfortable property: the master
// decides when transfers happen, so software is never late in a way the hardware cannot
// absorb. It can stall the wire (Chapter 13.9 measures exactly that) and nothing is
// lost.
//
// A slave has neither option.
//
// IT CANNOT ASK THE MASTER TO WAIT. There is no flow control in SPI. If the master
// clocks out a second word before software has supplied one, the slave drives
// SOMETHING -- and the only question is whether that something is defined.
//
// IT CANNOT CHOOSE WHEN THE RECEIVE BUFFER CHANGES. A word arrives when the master
// sends it, which may be in the middle of software reading the previous ones. A
// reader that walks four words can have the first two from one transaction and the
// last two from the next, with nothing in the data to say so.
//
// So this block is two mechanisms, one per problem, and neither is a buffer.
//
// THE TRANSMIT SIDE: A DEFINED DEFAULT, AND A REPORT.
//
// When a word is taken with nothing loaded, the slave sends `tx_default` -- a
// configured value, not the previous word and not whatever is in a register. That
// matters more than it sounds:
//
// * sending the PREVIOUS word makes an underrun look like a master that read the same
// location twice, which is a plausible thing to have happened and sends the
// investigation to the driver's addressing;
// * sending whatever is in the register makes the failure depend on history, so it is
// not reproducible;
// * sending a defined value -- conventionally 0x00 or 0xFF, matching what an absent
// device looks like -- makes the underrun recognisable at the master, which is the
// only place it can be acted on.
//
// And `tx_underrun` is sticky, because the master's read has already happened by the
// time software could notice.
//
// THE RECEIVE SIDE: A SEQUENCE NUMBER, NOT A LOCK.
//
// The obvious fix for a torn read is to stop the buffer changing while software reads
// it. A slave cannot: stopping it means dropping the word the master is sending, which
// converts a detectable problem into a silent loss.
//
// So the buffer is never held, and instead every transaction increments `rx_seq`.
// Software reads the sequence number, reads the words, reads the sequence number again,
// and if it changed the read was torn and must be repeated. That is a seqlock, and it
// is the standard answer whenever a reader cannot make a writer wait -- which is
// exactly the position software is in here.
//
// Two details make it work rather than nearly work:
//
// THE SEQUENCE NUMBER MUST CHANGE ON THE FIRST WORD, not at the end of the
// transaction. A reader that samples it after the first word of a new transaction has
// landed must see a different value, or the tear it is trying to detect is invisible.
//
// IT MUST BE WIDE ENOUGH THAT IT CANNOT WRAP WITHIN ONE READ. Eight bits at any
// plausible transaction rate is far more than enough, and the cost of getting this
// wrong is a tear that is reported as clean -- which is worse than no mechanism,
// because software would then trust it.
//
// An overflow flag completes the picture: the buffer is finite, so a transaction longer
// than it is a loss, and a loss must be reported for the same reason an underrun must.
module spi_slave_regs #(
parameter int MAX_W = 32,
parameter int DEPTH = 4, // received words held
parameter int PTR_W = 2, // ceil(log2(DEPTH)), supplied not derived
parameter int SEQ_W = 8
) (
input wire clk,
input wire rst_n,
// --- from the receive path (14.7) -------------------------------------
input wire txn_start_stb,
input wire [MAX_W-1:0] word_data,
input wire word_valid_stb,
// --- from the transmit path (14.4) ------------------------------------
input wire word_taken_stb,
// --- the system side --------------------------------------------------
input wire [PTR_W-1:0] sys_rd_idx, // which held word to read
output wire [MAX_W-1:0] sys_rd_data,
output wire [PTR_W:0] sys_rd_count, // words held in this transaction
output wire [SEQ_W-1:0] sys_rd_seq, // changes on every first word
input wire sys_tx_wr,
input wire [MAX_W-1:0] sys_tx_data,
output wire sys_tx_ready, // the holding register is empty
input wire [MAX_W-1:0] tx_default, // what an underrun sends
// --- to the transmit path ---------------------------------------------
output wire [MAX_W-1:0] tx_data,
// --- status -----------------------------------------------------------
output reg tx_underrun, // sticky: a word was taken unloaded
output reg rx_overflow, // sticky: a word arrived with no room
input wire clr_flags
);
// --- the receive buffer -------------------------------------------------
reg [MAX_W-1:0] buf_mem [0:DEPTH-1];
reg [PTR_W:0] fill;
reg [SEQ_W-1:0] seq;
assign sys_rd_data = buf_mem[sys_rd_idx];
assign sys_rd_count = fill;
assign sys_rd_seq = seq;
// --- the transmit holding register --------------------------------------
reg [MAX_W-1:0] tx_hold;
reg tx_loaded;
assign sys_tx_ready = ~tx_loaded;
// The default is presented when nothing is loaded, so the value on the wire is
// defined at every instant -- there is no window in which the slave is driving
// "whatever was there".
assign tx_data = tx_loaded ? tx_hold : tx_default;
integer i;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
fill <= {(PTR_W+1){1'b0}};
seq <= {SEQ_W{1'b0}};
tx_hold <= {MAX_W{1'b0}};
tx_loaded <= 1'b0;
tx_underrun <= 1'b0;
rx_overflow <= 1'b0;
for (i = 0; i < DEPTH; i = i + 1)
buf_mem[i] <= {MAX_W{1'b0}};
end else begin
if (clr_flags) begin
tx_underrun <= 1'b0;
rx_overflow <= 1'b0;
end
// --- receive ------------------------------------------------------
// The fill count is cleared at the transaction START, not at the first
// word. A reader that sampled the count between the start and the first
// word would otherwise see the PREVIOUS transaction's count alongside the
// new transaction's sequence number, which is the tear it is trying to
// detect wearing the disguise of a clean read.
// Written as if/else-if because the two cannot coincide: the transaction
// start is the first cycle of the transaction and the first word is at
// least `len` captures later. Writing it as two independent `if`s invites a
// reader to work out what happens when they overlap, and the answer is
// "they cannot" -- which the structure should say rather than a comment.
if (txn_start_stb) begin
fill <= {(PTR_W+1){1'b0}};
end else if (word_valid_stb) begin
if (fill < DEPTH) begin
buf_mem[fill[PTR_W-1:0]] <= word_data;
fill <= fill + {{PTR_W{1'b0}}, 1'b1};
end else begin
rx_overflow <= 1'b1;
end
// The sequence number advances on the FIRST word of a transaction, not
// at its end: a reader that samples it after the first new word has
// landed must see a different value, or the tear is invisible.
if (fill == {(PTR_W+1){1'b0}})
seq <= seq + 1'b1;
end
// --- transmit -----------------------------------------------------
if (sys_tx_wr && !tx_loaded) begin
tx_hold <= sys_tx_data;
tx_loaded <= 1'b1;
end
if (word_taken_stb) begin
if (tx_loaded) begin
// Consumed. The holding register is freed immediately so software
// has the whole of the next word's time to refill it -- the same
// argument as the master's shadow register (Chapter 13.9), and the
// same one line.
if (!(sys_tx_wr && !tx_loaded))
tx_loaded <= 1'b0;
end else begin
// Nothing was loaded, so `tx_default` went out. Reported, because
// the master has already read it.
tx_underrun <= 1'b1;
end
end
end
end
`ifdef SPI_CHECKS
always_ff @(posedge clk) if (rst_n) begin
if (fill > DEPTH)
$fatal(1, "the receive fill count exceeded the buffer");
end
`endif
endmodule// spi_slave_regs.v
//
// Chapter 14.9 -- the system side, and the two things a slave cannot do.
//
// A master's register interface (Chapter 13.11) has a comfortable property: the master
// decides when transfers happen, so software is never late in a way the hardware cannot
// absorb. It can stall the wire (Chapter 13.9 measures exactly that) and nothing is
// lost.
//
// A slave has neither option.
//
// IT CANNOT ASK THE MASTER TO WAIT. There is no flow control in SPI. If the master
// clocks out a second word before software has supplied one, the slave drives
// SOMETHING -- and the only question is whether that something is defined.
//
// IT CANNOT CHOOSE WHEN THE RECEIVE BUFFER CHANGES. A word arrives when the master
// sends it, which may be in the middle of software reading the previous ones. A
// reader that walks four words can have the first two from one transaction and the
// last two from the next, with nothing in the data to say so.
//
// So this block is two mechanisms, one per problem, and neither is a buffer.
//
// THE TRANSMIT SIDE: A DEFINED DEFAULT, AND A REPORT.
//
// When a word is taken with nothing loaded, the slave sends `tx_default` -- a
// configured value, not the previous word and not whatever is in a register. That
// matters more than it sounds:
//
// * sending the PREVIOUS word makes an underrun look like a master that read the same
// location twice, which is a plausible thing to have happened and sends the
// investigation to the driver's addressing;
// * sending whatever is in the register makes the failure depend on history, so it is
// not reproducible;
// * sending a defined value -- conventionally 0x00 or 0xFF, matching what an absent
// device looks like -- makes the underrun recognisable at the master, which is the
// only place it can be acted on.
//
// And `tx_underrun` is sticky, because the master's read has already happened by the
// time software could notice.
//
// THE RECEIVE SIDE: A SEQUENCE NUMBER, NOT A LOCK.
//
// The obvious fix for a torn read is to stop the buffer changing while software reads
// it. A slave cannot: stopping it means dropping the word the master is sending, which
// converts a detectable problem into a silent loss.
//
// So the buffer is never held, and instead every transaction increments `rx_seq`.
// Software reads the sequence number, reads the words, reads the sequence number again,
// and if it changed the read was torn and must be repeated. That is a seqlock, and it
// is the standard answer whenever a reader cannot make a writer wait -- which is
// exactly the position software is in here.
//
// Two details make it work rather than nearly work:
//
// THE SEQUENCE NUMBER MUST CHANGE ON THE FIRST WORD, not at the end of the
// transaction. A reader that samples it after the first word of a new transaction has
// landed must see a different value, or the tear it is trying to detect is invisible.
//
// IT MUST BE WIDE ENOUGH THAT IT CANNOT WRAP WITHIN ONE READ. Eight bits at any
// plausible transaction rate is far more than enough, and the cost of getting this
// wrong is a tear that is reported as clean -- which is worse than no mechanism,
// because software would then trust it.
//
// An overflow flag completes the picture: the buffer is finite, so a transaction longer
// than it is a loss, and a loss must be reported for the same reason an underrun must.
module spi_slave_regs #(
parameter MAX_W = 32,
parameter DEPTH = 4, // received words held
parameter PTR_W = 2, // ceil(log2(DEPTH)), supplied not derived
parameter SEQ_W = 8
) (
input wire clk,
input wire rst_n,
// --- from the receive path (14.7) -------------------------------------
input wire txn_start_stb,
input wire [MAX_W-1:0] word_data,
input wire word_valid_stb,
// --- from the transmit path (14.4) ------------------------------------
input wire word_taken_stb,
// --- the system side --------------------------------------------------
input wire [PTR_W-1:0] sys_rd_idx, // which held word to read
output wire [MAX_W-1:0] sys_rd_data,
output wire [PTR_W:0] sys_rd_count, // words held in this transaction
output wire [SEQ_W-1:0] sys_rd_seq, // changes on every first word
input wire sys_tx_wr,
input wire [MAX_W-1:0] sys_tx_data,
output wire sys_tx_ready, // the holding register is empty
input wire [MAX_W-1:0] tx_default, // what an underrun sends
// --- to the transmit path ---------------------------------------------
output wire [MAX_W-1:0] tx_data,
// --- status -----------------------------------------------------------
output reg tx_underrun, // sticky: a word was taken unloaded
output reg rx_overflow, // sticky: a word arrived with no room
input wire clr_flags
);
// --- the receive buffer -------------------------------------------------
reg [MAX_W-1:0] buf_mem [0:DEPTH-1];
reg [PTR_W:0] fill;
reg [SEQ_W-1:0] seq;
assign sys_rd_data = buf_mem[sys_rd_idx];
assign sys_rd_count = fill;
assign sys_rd_seq = seq;
// --- the transmit holding register --------------------------------------
reg [MAX_W-1:0] tx_hold;
reg tx_loaded;
assign sys_tx_ready = ~tx_loaded;
// The default is presented when nothing is loaded, so the value on the wire is
// defined at every instant -- there is no window in which the slave is driving
// "whatever was there".
assign tx_data = tx_loaded ? tx_hold : tx_default;
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
fill <= {(PTR_W+1){1'b0}};
seq <= {SEQ_W{1'b0}};
tx_hold <= {MAX_W{1'b0}};
tx_loaded <= 1'b0;
tx_underrun <= 1'b0;
rx_overflow <= 1'b0;
for (i = 0; i < DEPTH; i = i + 1)
buf_mem[i] <= {MAX_W{1'b0}};
end else begin
if (clr_flags) begin
tx_underrun <= 1'b0;
rx_overflow <= 1'b0;
end
// --- receive ------------------------------------------------------
// The fill count is cleared at the transaction START, not at the first
// word. A reader that sampled the count between the start and the first
// word would otherwise see the PREVIOUS transaction's count alongside the
// new transaction's sequence number, which is the tear it is trying to
// detect wearing the disguise of a clean read.
// Written as if/else-if because the two cannot coincide: the transaction
// start is the first cycle of the transaction and the first word is at
// least `len` captures later. Writing it as two independent `if`s invites a
// reader to work out what happens when they overlap, and the answer is
// "they cannot" -- which the structure should say rather than a comment.
if (txn_start_stb) begin
fill <= {(PTR_W+1){1'b0}};
end else if (word_valid_stb) begin
if (fill < DEPTH) begin
buf_mem[fill[PTR_W-1:0]] <= word_data;
fill <= fill + {{PTR_W{1'b0}}, 1'b1};
end else begin
rx_overflow <= 1'b1;
end
// The sequence number advances on the FIRST word of a transaction, not
// at its end: a reader that samples it after the first new word has
// landed must see a different value, or the tear is invisible.
if (fill == {(PTR_W+1){1'b0}})
seq <= seq + 1'b1;
end
// --- transmit -----------------------------------------------------
if (sys_tx_wr && !tx_loaded) begin
tx_hold <= sys_tx_data;
tx_loaded <= 1'b1;
end
if (word_taken_stb) begin
if (tx_loaded) begin
// Consumed. The holding register is freed immediately so software
// has the whole of the next word's time to refill it -- the same
// argument as the master's shadow register (Chapter 13.9), and the
// same one line.
if (!(sys_tx_wr && !tx_loaded))
tx_loaded <= 1'b0;
end else begin
// Nothing was loaded, so `tx_default` went out. Reported, because
// the master has already read it.
tx_underrun <= 1'b1;
end
end
end
end
`ifdef SPI_CHECKS
always @(posedge clk) if (rst_n) begin
if (fill > DEPTH)
$fatal(1, "the receive fill count exceeded the buffer");
end
`endif
endmodule-- spi_slave_regs.vhd
--
-- Chapter 14.9 -- the system side, and the two things a slave cannot do.
--
-- A master's register interface (Chapter 13.11) has a comfortable property: the master
-- decides when transfers happen, so software is never late in a way the hardware cannot
-- absorb. It can stall the wire (Chapter 13.9 measures exactly that) and nothing is
-- lost.
--
-- A slave has neither option.
--
-- IT CANNOT ASK THE MASTER TO WAIT. There is no flow control in SPI. If the master
-- clocks out a second word before software has supplied one, the slave drives
-- SOMETHING -- and the only question is whether that something is defined.
--
-- IT CANNOT CHOOSE WHEN THE RECEIVE BUFFER CHANGES. A word arrives when the master
-- sends it, which may be in the middle of software reading the previous ones. A
-- reader that walks four words can have the first two from one transaction and the
-- last two from the next, with nothing in the data to say so.
--
-- So this block is two mechanisms, one per problem, and neither is a buffer.
--
-- THE TRANSMIT SIDE: A DEFINED DEFAULT, AND A REPORT.
--
-- When a word is taken with nothing loaded, the slave sends `tx_default` -- a
-- configured value, not the previous word and not whatever is in a register. That
-- matters more than it sounds:
--
-- * sending the PREVIOUS word makes an underrun look like a master that read the same
-- location twice, which is a plausible thing to have happened and sends the
-- investigation to the driver's addressing;
-- * sending whatever is in the register makes the failure depend on history, so it is
-- not reproducible;
-- * sending a defined value -- conventionally 0x00 or 0xFF, matching what an absent
-- device looks like -- makes the underrun recognisable at the master, which is the
-- only place it can be acted on.
--
-- And `tx_underrun` is sticky, because the master's read has already happened by the
-- time software could notice.
--
-- THE RECEIVE SIDE: A SEQUENCE NUMBER, NOT A LOCK.
--
-- The obvious fix for a torn read is to stop the buffer changing while software reads
-- it. A slave cannot: stopping it means dropping the word the master is sending, which
-- converts a detectable problem into a silent loss.
--
-- So the buffer is never held, and instead every transaction increments `rx_seq`.
-- Software reads the sequence number, reads the words, reads the sequence number again,
-- and if it changed the read was torn and must be repeated. That is a seqlock, and it
-- is the standard answer whenever a reader cannot make a writer wait -- which is
-- exactly the position software is in here.
--
-- Two details make it work rather than nearly work:
--
-- THE SEQUENCE NUMBER MUST CHANGE ON THE FIRST WORD, not at the end of the
-- transaction. A reader that samples it after the first word of a new transaction has
-- landed must see a different value, or the tear it is trying to detect is invisible.
--
-- IT MUST BE WIDE ENOUGH THAT IT CANNOT WRAP WITHIN ONE READ. Eight bits at any
-- plausible transaction rate is far more than enough, and the cost of getting this
-- wrong is a tear that is reported as clean -- which is worse than no mechanism,
-- because software would then trust it.
--
-- An overflow flag completes the picture: the buffer is finite, so a transaction longer
-- than it is a loss, and a loss must be reported for the same reason an underrun must.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_regs is
generic (
MAX_W : positive := 32;
DEPTH : positive := 4; -- received words held
PTR_W : positive := 2; -- ceil(log2(DEPTH)), supplied not derived
SEQ_W : positive := 8
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- from the receive path (14.7)
txn_start_stb : in std_logic;
word_data : in std_logic_vector(MAX_W - 1 downto 0);
word_valid_stb : in std_logic;
-- from the transmit path (14.4)
word_taken_stb : in std_logic;
-- the system side
sys_rd_idx : in unsigned(PTR_W - 1 downto 0);
sys_rd_data : out std_logic_vector(MAX_W - 1 downto 0);
sys_rd_count : out unsigned(PTR_W downto 0);
sys_rd_seq : out unsigned(SEQ_W - 1 downto 0);
sys_tx_wr : in std_logic;
sys_tx_data : in std_logic_vector(MAX_W - 1 downto 0);
sys_tx_ready : out std_logic;
tx_default : in std_logic_vector(MAX_W - 1 downto 0);
-- to the transmit path
tx_data : out std_logic_vector(MAX_W - 1 downto 0);
-- status
tx_underrun : out std_logic;
rx_overflow : out std_logic;
clr_flags : in std_logic
);
end entity;
architecture rtl of spi_slave_regs is
type buf_t is array (0 to DEPTH - 1) of std_logic_vector(MAX_W - 1 downto 0);
signal buf_mem : buf_t := (others => (others => '0'));
signal fill : unsigned(PTR_W downto 0) := (others => '0');
signal seq : unsigned(SEQ_W - 1 downto 0) := (others => '0');
signal tx_hold : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
signal tx_loaded : std_logic := '0';
signal under_r : std_logic := '0';
signal over_r : std_logic := '0';
begin
sys_rd_data <= buf_mem(to_integer(sys_rd_idx));
sys_rd_count <= fill;
sys_rd_seq <= seq;
sys_tx_ready <= not tx_loaded;
tx_underrun <= under_r;
rx_overflow <= over_r;
-- The default is presented when nothing is loaded, so the value on the wire is
-- defined at every instant -- there is no window in which the slave is driving
-- "whatever was there".
tx_data <= tx_hold when tx_loaded = '1' else tx_default;
regs : process (clk, rst_n)
begin
if rst_n = '0' then
fill <= (others => '0');
seq <= (others => '0');
tx_hold <= (others => '0');
tx_loaded <= '0';
under_r <= '0';
over_r <= '0';
buf_mem <= (others => (others => '0'));
elsif rising_edge(clk) then
if clr_flags = '1' then
under_r <= '0';
over_r <= '0';
end if;
-- Written as if/elsif because the two cannot coincide: the transaction start
-- is the first cycle of the transaction and the first word is at least `len`
-- captures later. Writing it as two independent ifs invites a reader to work
-- out what happens when they overlap, and the answer is "they cannot".
if txn_start_stb = '1' then
fill <= (others => '0');
elsif word_valid_stb = '1' then
if to_integer(fill) < DEPTH then
buf_mem(to_integer(fill)) <= word_data;
fill <= fill + 1;
else
over_r <= '1';
end if;
-- The sequence number advances on the FIRST word of a transaction, not
-- at its end: a reader that samples it after the first new word has
-- landed must see a different value, or the tear is invisible.
if fill = 0 then
seq <= seq + 1;
end if;
end if;
if sys_tx_wr = '1' and tx_loaded = '0' then
tx_hold <= sys_tx_data;
tx_loaded <= '1';
end if;
if word_taken_stb = '1' then
if tx_loaded = '1' then
-- Consumed. The holding register is freed immediately so software
-- has the whole of the next word's time to refill it -- the same
-- argument as the master's shadow register (Chapter 13.9).
if not (sys_tx_wr = '1' and tx_loaded = '0') then
tx_loaded <= '0';
end if;
else
-- Nothing was loaded, so `tx_default` went out. Reported, because
-- the master has already read it.
under_r <= '1';
end if;
end if;
end if;
end process;
check : process (clk)
begin
if rising_edge(clk) and rst_n = '1' then
assert to_integer(fill) <= DEPTH
report "the receive fill count exceeded the buffer" severity failure;
end if;
end process;
end architecture;The testbench
Nine tests. Tests 5, 6 and 7 are the seqlock's proof, and 7 is the one that pins down §2's first-word rule.
- The transmit path. Two words written by software appear on MISO in order.
- An underrun. Nothing loaded, so the default goes out — checked against the default specifically, not merely against "not the previous word".
- The default is configurable, which is what makes it a decision rather than a constant.
- The receive buffer. Three words in one transaction, readable by index, with the count correct.
- A clean read is not reported as torn. The sequence number must not change during a read that had no transaction in it — a mechanism that reports every read as torn is as useless as one that reports none.
- A torn read is detected. The same read with a transaction landing in the middle of it, and the sequence number differs.
- The sequence number moves on the first word, not at the end. The bench lands exactly one word of a new transaction during a read and asserts the number changed — which fails against a design that increments at the transaction boundary and passes test 6 anyway.
- Overflow. A transaction longer than the buffer loses words, and the loss is reported.
- The holding register frees immediately, so software has a whole word's time — measured, not assumed.
// spi_slave_regs_tb.sv
//
// The whole slave runs behind this block, so the transmit words really do go out on MOSI
// and the received words really do arrive from it -- which matters, because both
// mechanisms under test are about the RELATIONSHIP between software's timing and the
// master's, and a testbench that poked the block directly would be choosing both.
//
// The torn-read experiment is the interesting one. Software reads the sequence number,
// reads a word, and reads the sequence number again -- and the test arranges for a
// transaction to land in between, so the sequence number MUST have changed. Then it does
// the same read entirely between transactions and requires that it did not. A mechanism
// that reported every read as torn would pass the first half.
`timescale 1ns/1ps
module spi_slave_regs_tb;
localparam int MAX_W = 32;
localparam int LEN_W = 6;
localparam int CNT_W = 12;
localparam int DEPTH = 4;
localparam int PTR_W = 2;
localparam int SEQ_W = 8;
logic clk = 1'b0;
logic rst_n = 1'b0;
always #5 clk = ~clk;
logic cpol = 1'b0;
logic cpha = 1'b0;
logic lsb_first = 1'b0;
logic clr_flags = 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, 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)
);
logic [LEN_W-1:0] len = 6'd8;
wire txn_active, txn_start_stb, txn_end_stb, txn_report_stb;
wire [CNT_W-1:0] edges_in_txn, frames_in_txn;
wire txn_clean, txn_trunc, txn_empty;
wire [2:0] cs_state;
spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(CNT_W)) u_cs (
.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(cs_state)
);
wire cap_stb, launch_stb, preload_stb, cpol_mismatch, phase_suspect;
wire [3:0] trunc_run;
spi_slave_mode #(.SUSPECT_N(3), .CNT_W(4)) u_mode (
.clk(clk), .rst_n(rst_n), .cpol(cpol), .cpha(cpha),
.sclk_q(sclk_q), .mosi_q(mosi_q), .cs_assert_stb(cs_assert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_report_stb(txn_report_stb), .txn_clean(txn_clean),
.txn_trunc(txn_trunc),
.cap_stb(cap_stb), .launch_stb(launch_stb), .preload_stb(preload_stb),
.cpol_mismatch(cpol_mismatch), .phase_suspect(phase_suspect),
.moved_run(), .trunc_run(trunc_run), .clr_flags(1'b0)
);
wire [MAX_W-1:0] rx_data, rx_partial_sr;
wire rx_valid_stb;
wire [LEN_W-1:0] bit_idx;
wire [CNT_W-1:0] words_in_txn;
spi_slave_rx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_rx (
.clk(clk), .rst_n(rst_n),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.cap_stb(cap_stb), .mosi_q(mosi_q),
.len(len), .lsb_first(lsb_first),
.rx_data(rx_data), .rx_valid_stb(rx_valid_stb),
.bit_idx(bit_idx), .words_in_txn(words_in_txn),
.rx_partial_sr(rx_partial_sr)
);
wire [MAX_W-1:0] word_data, partial_data;
wire word_valid_stb, partial_valid_stb;
wire [CNT_W-1:0] words_complete;
wire [LEN_W-1:0] partial_bits;
wire len_err, aborted;
wire [1:0] frame_state;
spi_slave_frame #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_frame (
.clk(clk), .rst_n(rst_n), .len(len),
.txn_start_stb(txn_start_stb), .txn_report_stb(txn_report_stb),
.txn_trunc(txn_trunc), .txn_empty(txn_empty),
.rx_data(rx_data), .rx_valid_stb(rx_valid_stb), .bit_idx(bit_idx),
.rx_partial_sr(rx_partial_sr),
.word_data(word_data), .word_valid_stb(word_valid_stb),
.words_complete(words_complete),
.partial_data(partial_data), .partial_bits(partial_bits),
.partial_valid_stb(partial_valid_stb),
.len_err(len_err), .aborted(aborted), .state_id(frame_state),
.clr_flags(1'b0)
);
// --- 14.9, the block under test -----------------------------------------
logic [PTR_W-1:0] sys_rd_idx = 2'd0;
logic sys_tx_wr = 1'b0;
logic [MAX_W-1:0] sys_tx_data = 32'h0;
logic [MAX_W-1:0] tx_default = 32'hFF;
wire [MAX_W-1:0] sys_rd_data;
wire [PTR_W:0] sys_rd_count;
wire [SEQ_W-1:0] sys_rd_seq;
wire sys_tx_ready;
wire [MAX_W-1:0] tx_data;
wire tx_underrun, rx_overflow;
wire word_taken_stb;
spi_slave_regs #(.MAX_W(MAX_W), .DEPTH(DEPTH), .PTR_W(PTR_W), .SEQ_W(SEQ_W))
dut (
.clk(clk), .rst_n(rst_n),
.txn_start_stb(txn_start_stb),
.word_data(word_data), .word_valid_stb(word_valid_stb),
.word_taken_stb(word_taken_stb),
.sys_rd_idx(sys_rd_idx), .sys_rd_data(sys_rd_data),
.sys_rd_count(sys_rd_count), .sys_rd_seq(sys_rd_seq),
.sys_tx_wr(sys_tx_wr), .sys_tx_data(sys_tx_data),
.sys_tx_ready(sys_tx_ready), .tx_default(tx_default),
.tx_data(tx_data),
.tx_underrun(tx_underrun), .rx_overflow(rx_overflow),
.clr_flags(clr_flags)
);
// --- the transmit path, so the words really go out ----------------------
wire miso_val;
wire [LEN_W-1:0] bits_driven;
wire [7:0] lead_seen, half_seen;
wire lead_short, half_short;
spi_slave_tx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .LEAD_MIN(3), .HALF_MIN(3),
.CNT_W(8)) u_tx (
.clk(clk), .rst_n(rst_n),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.preload_stb(preload_stb), .launch_stb(launch_stb), .cap_stb(cap_stb),
.len(len), .lsb_first(lsb_first), .tx_data(tx_data),
.miso(miso_val),
.word_taken_stb(word_taken_stb), .bits_driven(bits_driven),
.lead_seen(lead_seen), .lead_short(lead_short),
.half_seen(half_seen), .half_short(half_short), .clr_flags(1'b0)
);
integer errors = 0;
// A WATCHDOG. Every wait in this testbench is bounded, but a watchdog is the only
// thing that catches a hang in a wait somebody adds later -- and a simulation that
// never terminates reports nothing, which is strictly worse than a failure.
initial begin
#2_000_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
task automatic adv(input integer n);
begin repeat (n) @(negedge clk); end
endtask
// The master's side: drives `nwords` words of a known pattern and samples MISO at
// each capture edge.
integer got_bits [0:63];
integer got_n;
task automatic drive_txn(input integer nwords, input integer half,
input [63:0] pattern);
integer i;
begin
cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1;
adv(10);
got_n = 0;
cs_n_pin = 1'b0;
adv(6);
for (i = 0; i < nwords * 8; i = i + 1) begin
mosi_pin = pattern[63 - i];
adv(2);
got_bits[got_n] = miso_val;
got_n = got_n + 1;
sclk_pin = 1'b1;
adv(half);
sclk_pin = 1'b0;
adv(half > 2 ? half - 2 : 1);
end
adv(4);
cs_n_pin = 1'b1;
adv(10);
sclk_pin = 1'b0;
adv(8);
end
endtask
// BOUNDED, like every wait in this module. An unbounded wait for a ready signal
// turns a design bug into a simulation that never terminates, and a simulation that
// never terminates reports nothing at all -- which is strictly worse than a failure.
task automatic sys_write(input [MAX_W-1:0] v);
integer guard;
begin
guard = 4000;
while (!sys_tx_ready && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the transmit holding register never became free");
errors = errors + 1;
end
sys_tx_data = v;
sys_tx_wr = 1'b1;
@(negedge clk);
sys_tx_wr = 1'b0;
end
endtask
function automatic integer miso_word(input integer w);
integer i, v;
begin
v = 0;
for (i = 0; i < 8; i = i + 1)
v = (v << 1) | (got_bits[w*8 + i] & 1);
miso_word = v;
end
endfunction
integer k, seq_a, seq_b, cnt_a, bad;
logic [63:0] pat;
initial begin
got_n = 0;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. THE TRANSMIT PATH. Two words written by software must appear on MISO, and
// no underrun must be reported.
sys_write(32'h5A);
pat = 64'h11_22_00_00_00_00_00_00;
drive_txn(1, 4, pat);
if (miso_word(0) != 8'h5A) begin
$display(" FAIL: software wrote 5a and the master read %02h",
miso_word(0));
errors = errors + 1;
end
if (tx_underrun) begin
$display(" FAIL: a loaded word was reported as an underrun");
errors = errors + 1;
end
$display(" software wrote 5a and the master read 5a, with no underrun reported");
// 2. AN UNDERRUN. Nothing loaded, so the DEFAULT must go out -- not the previous
// word, which would look like a master that read the same place twice.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_txn(1, 4, pat);
if (miso_word(0) != 8'hFF) begin
$display(" FAIL: an underrun sent %02h, expected the default ff",
miso_word(0));
errors = errors + 1;
end
if (!tx_underrun) begin
$display(" FAIL: an underrun was not reported");
errors = errors + 1;
end
$display(" with nothing loaded the master read the configured default ff rather than the previous word 5a, and the underrun is reported");
// 3. THE DEFAULT IS CONFIGURABLE, which is what makes it a decision rather than
// an accident.
tx_default = 32'h00;
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_txn(1, 4, pat);
if (miso_word(0) != 8'h00) begin
$display(" FAIL: with a default of 00 an underrun sent %02h",
miso_word(0));
errors = errors + 1;
end
tx_default = 32'hFF;
$display(" changing the default to 00 changes what an underrun sends, so the value is a decision rather than whatever was left in a register");
// 4. THE RECEIVE BUFFER. Three words in one transaction, readable by index with
// a count that says how many there are.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
// ONE write, not two. The holding register is one word deep and stays occupied
// until a word is taken, so a second write before the transaction starts has
// nowhere to go -- which is the interface working, not a limitation to route
// around. This test is about the receive buffer, so one word is enough and the
// remaining words underrun harmlessly.
sys_write(32'hAA);
pat = 64'h11_22_33_00_00_00_00_00;
drive_txn(3, 4, pat);
if (sys_rd_count != 3) begin
$display(" FAIL: three words gave a count of %0d", sys_rd_count);
errors = errors + 1;
end
bad = 0;
for (k = 0; k < 3; k = k + 1) begin
sys_rd_idx = k[PTR_W-1:0];
adv(1);
if (sys_rd_data[7:0] !== ((pat >> (56 - 8*k)) & 8'hFF)) bad = bad + 1;
end
if (bad != 0) begin
$display(" FAIL: %0d of 3 held words read back wrongly", bad);
errors = errors + 1;
end
$display(" three words in one transaction are held, counted and readable by index");
// 5. A CLEAN READ IS NOT REPORTED AS TORN. The sequence number must not change
// across a read that happens entirely between transactions -- a mechanism
// that flagged everything would pass the next test and be useless.
seq_a = sys_rd_seq;
for (k = 0; k < 3; k = k + 1) begin
sys_rd_idx = k[PTR_W-1:0];
adv(2);
end
seq_b = sys_rd_seq;
if (seq_a != seq_b) begin
$display(" FAIL: a read between transactions changed the sequence number from %0d to %0d",
seq_a, seq_b);
errors = errors + 1;
end
$display(" a read taken entirely between transactions leaves the sequence number unchanged at %0d",
seq_b);
// 6. A TORN READ IS DETECTED. The same read with a transaction landing in the
// middle of it MUST show a different sequence number.
seq_a = sys_rd_seq;
sys_rd_idx = 2'd0;
adv(2);
sys_write(32'hCC);
pat = 64'h44_55_00_00_00_00_00_00;
drive_txn(2, 4, pat); // lands in the middle of software's read
sys_rd_idx = 2'd1;
adv(2);
seq_b = sys_rd_seq;
if (seq_a == seq_b) begin
$display(" FAIL: a read spanning a transaction left the sequence number at %0d",
seq_b);
errors = errors + 1;
end
$display(" the same read with a transaction landing inside it shows the sequence number moving from %0d to %0d, so the tear is detectable",
seq_a, seq_b);
// 7. THE SEQUENCE NUMBER MOVES ON THE FIRST WORD, not at the end -- which is
// what makes a tear detectable before the transaction has even finished.
seq_a = sys_rd_seq;
cs_n_pin = 1'b0; adv(6);
for (k = 0; k < 8; k = k + 1) begin
mosi_pin = k[0];
adv(2); sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(2);
end
adv(4);
seq_b = sys_rd_seq;
if (seq_a == seq_b) begin
$display(" FAIL: the sequence number had not moved after the first word arrived");
errors = errors + 1;
end
cs_n_pin = 1'b1; adv(10); sclk_pin = 1'b0; adv(8);
$display(" the sequence number moves as soon as the first word of a transaction lands, not when the transaction ends");
// 8. OVERFLOW. A transaction longer than the buffer is a loss, and a loss is
// reported -- a slave cannot ask the master to slow down.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
pat = 64'h01_02_03_04_05_06_00_00;
drive_txn(6, 4, pat);
if (!rx_overflow) begin
$display(" FAIL: six words into a four-deep buffer was not reported as an overflow");
errors = errors + 1;
end
if (sys_rd_count != DEPTH) begin
$display(" FAIL: after an overflow the count is %0d, expected %0d",
sys_rd_count, DEPTH);
errors = errors + 1;
end
// And the words that DID fit are the FIRST four, not the last four -- dropping
// the newest is the right policy, because the oldest are the ones a command
// interpreter has already begun acting on.
bad = 0;
for (k = 0; k < DEPTH; k = k + 1) begin
sys_rd_idx = k[PTR_W-1:0];
adv(1);
if (sys_rd_data[7:0] !== ((pat >> (56 - 8*k)) & 8'hFF)) bad = bad + 1;
end
if (bad != 0) begin
$display(" FAIL: after an overflow %0d of the first %0d words were lost",
bad, DEPTH);
errors = errors + 1;
end
$display(" six words into a four-deep buffer reports an overflow, holds a count of four, and keeps the FIRST four rather than the last");
// 9. THE HOLDING REGISTER FREES IMMEDIATELY, so software has a whole word's time
// to refill -- the same argument as the master's shadow register.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
sys_write(32'h12);
pat = 64'h00_00_00_00_00_00_00_00;
cs_n_pin = 1'b0; adv(6);
k = 0;
for (k = 0; k < 8; k = k + 1) begin
adv(2); sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(2);
end
// The first word has been taken; the register must already be free.
if (!sys_tx_ready) begin
$display(" FAIL: the holding register was still occupied after its word was taken");
errors = errors + 1;
end
sys_write(32'h34);
for (k = 0; k < 8; k = k + 1) begin
adv(2); sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(2);
end
adv(4);
cs_n_pin = 1'b1; adv(10); sclk_pin = 1'b0; adv(8);
if (tx_underrun) begin
$display(" FAIL: refilling within the next word's time still reported an underrun");
errors = errors + 1;
end
$display(" the holding register frees as soon as its word is taken, so software refilling within the next word's time causes no underrun");
if (errors == 0)
$display("PASS: a slave has neither of the master's options -- it cannot ask the master to wait and it cannot choose when its receive buffer changes -- so the transmit side answers with a CONFIGURABLE DEFAULT and a sticky report, which makes an underrun recognisable at the master rather than looking like a repeated read, and the receive side answers with a SEQUENCE NUMBER rather than a lock, because holding the buffer would drop the word the master is sending and convert a detectable problem into a silent loss -- a read taken entirely between transactions leaves the sequence unchanged while the same read with a transaction landing inside it shows it moving, the sequence advances on the FIRST word rather than at the end so a tear is detectable before the transaction finishes, an overflow is reported and keeps the oldest words because those are the ones a command interpreter has already acted on, and the holding register frees the moment its word is taken so software has a whole word's time to refill");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_slave_regs_tb.v
//
// The whole slave runs behind this block, so the transmit words really do go out on MOSI
// and the received words really do arrive from it -- which matters, because both
// mechanisms under test are about the RELATIONSHIP between software's timing and the
// master's, and a testbench that poked the block directly would be choosing both.
//
// The torn-read experiment is the interesting one. Software reads the sequence number,
// reads a word, and reads the sequence number again -- and the test arranges for a
// transaction to land in between, so the sequence number MUST have changed. Then it does
// the same read entirely between transactions and requires that it did not. A mechanism
// that reported every read as torn would pass the first half.
`timescale 1ns/1ps
module spi_slave_regs_tb;
localparam MAX_W = 32;
localparam LEN_W = 6;
localparam CNT_W = 12;
localparam DEPTH = 4;
localparam PTR_W = 2;
localparam SEQ_W = 8;
reg clk;
reg rst_n;
always #5 clk = ~clk;
reg cpol;
reg cpha;
reg lsb_first;
reg clr_flags;
reg sclk_pin;
reg cs_n_pin;
reg mosi_pin;
wire sclk_q, cs_active, mosi_q;
wire edge_a_stb, edge_b_stb, 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)
);
reg [LEN_W-1:0] len;
wire txn_active, txn_start_stb, txn_end_stb, txn_report_stb;
wire [CNT_W-1:0] edges_in_txn, frames_in_txn;
wire txn_clean, txn_trunc, txn_empty;
wire [2:0] cs_state;
spi_slave_cs #(.LEN_W(LEN_W), .CNT_W(CNT_W)) u_cs (
.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(cs_state)
);
wire cap_stb, launch_stb, preload_stb, cpol_mismatch, phase_suspect;
wire [3:0] trunc_run;
spi_slave_mode #(.SUSPECT_N(3), .CNT_W(4)) u_mode (
.clk(clk), .rst_n(rst_n), .cpol(cpol), .cpha(cpha),
.sclk_q(sclk_q), .mosi_q(mosi_q), .cs_assert_stb(cs_assert_stb),
.edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.txn_report_stb(txn_report_stb), .txn_clean(txn_clean),
.txn_trunc(txn_trunc),
.cap_stb(cap_stb), .launch_stb(launch_stb), .preload_stb(preload_stb),
.cpol_mismatch(cpol_mismatch), .phase_suspect(phase_suspect),
.moved_run(), .trunc_run(trunc_run), .clr_flags(1'b0)
);
wire [MAX_W-1:0] rx_data, rx_partial_sr;
wire rx_valid_stb;
wire [LEN_W-1:0] bit_idx;
wire [CNT_W-1:0] words_in_txn;
spi_slave_rx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_rx (
.clk(clk), .rst_n(rst_n),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.cap_stb(cap_stb), .mosi_q(mosi_q),
.len(len), .lsb_first(lsb_first),
.rx_data(rx_data), .rx_valid_stb(rx_valid_stb),
.bit_idx(bit_idx), .words_in_txn(words_in_txn),
.rx_partial_sr(rx_partial_sr)
);
wire [MAX_W-1:0] word_data, partial_data;
wire word_valid_stb, partial_valid_stb;
wire [CNT_W-1:0] words_complete;
wire [LEN_W-1:0] partial_bits;
wire len_err, aborted;
wire [1:0] frame_state;
spi_slave_frame #(.MAX_W(MAX_W), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_frame (
.clk(clk), .rst_n(rst_n), .len(len),
.txn_start_stb(txn_start_stb), .txn_report_stb(txn_report_stb),
.txn_trunc(txn_trunc), .txn_empty(txn_empty),
.rx_data(rx_data), .rx_valid_stb(rx_valid_stb), .bit_idx(bit_idx),
.rx_partial_sr(rx_partial_sr),
.word_data(word_data), .word_valid_stb(word_valid_stb),
.words_complete(words_complete),
.partial_data(partial_data), .partial_bits(partial_bits),
.partial_valid_stb(partial_valid_stb),
.len_err(len_err), .aborted(aborted), .state_id(frame_state),
.clr_flags(1'b0)
);
// --- 14.9, the block under test -----------------------------------------
reg [PTR_W-1:0] sys_rd_idx;
reg sys_tx_wr;
reg [MAX_W-1:0] sys_tx_data;
reg [MAX_W-1:0] tx_default;
wire [MAX_W-1:0] sys_rd_data;
wire [PTR_W:0] sys_rd_count;
wire [SEQ_W-1:0] sys_rd_seq;
wire sys_tx_ready;
wire [MAX_W-1:0] tx_data;
wire tx_underrun, rx_overflow;
wire word_taken_stb;
spi_slave_regs #(.MAX_W(MAX_W), .DEPTH(DEPTH), .PTR_W(PTR_W), .SEQ_W(SEQ_W))
dut (
.clk(clk), .rst_n(rst_n),
.txn_start_stb(txn_start_stb),
.word_data(word_data), .word_valid_stb(word_valid_stb),
.word_taken_stb(word_taken_stb),
.sys_rd_idx(sys_rd_idx), .sys_rd_data(sys_rd_data),
.sys_rd_count(sys_rd_count), .sys_rd_seq(sys_rd_seq),
.sys_tx_wr(sys_tx_wr), .sys_tx_data(sys_tx_data),
.sys_tx_ready(sys_tx_ready), .tx_default(tx_default),
.tx_data(tx_data),
.tx_underrun(tx_underrun), .rx_overflow(rx_overflow),
.clr_flags(clr_flags)
);
// --- the transmit path, so the words really go out ----------------------
wire miso_val;
wire [LEN_W-1:0] bits_driven;
wire [7:0] lead_seen, half_seen;
wire lead_short, half_short;
spi_slave_tx #(.MAX_W(MAX_W), .LEN_W(LEN_W), .LEAD_MIN(3), .HALF_MIN(3),
.CNT_W(8)) u_tx (
.clk(clk), .rst_n(rst_n),
.txn_active(txn_active), .txn_start_stb(txn_start_stb),
.preload_stb(preload_stb), .launch_stb(launch_stb), .cap_stb(cap_stb),
.len(len), .lsb_first(lsb_first), .tx_data(tx_data),
.miso(miso_val),
.word_taken_stb(word_taken_stb), .bits_driven(bits_driven),
.lead_seen(lead_seen), .lead_short(lead_short),
.half_seen(half_seen), .half_short(half_short), .clr_flags(1'b0)
);
integer errors;
// A WATCHDOG. Every wait in this testbench is bounded, but a watchdog is the only
// thing that catches a hang in a wait somebody adds later -- and a simulation that
// never terminates reports nothing, which is strictly worse than a failure.
initial begin
#2_000_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
task adv;
input integer n;
begin repeat (n) @(negedge clk); end
endtask
// The master's side: drives `nwords` words of a known pattern and samples MISO at
// each capture edge.
integer got_bits [0:63];
integer got_n;
task drive_txn;
input integer nwords;
input integer half;
input [63:0] pattern;
integer i;
begin
cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1;
adv(10);
got_n = 0;
cs_n_pin = 1'b0;
adv(6);
for (i = 0; i < nwords * 8; i = i + 1) begin
mosi_pin = pattern[63 - i];
adv(2);
got_bits[got_n] = miso_val;
got_n = got_n + 1;
sclk_pin = 1'b1;
adv(half);
sclk_pin = 1'b0;
adv(half > 2 ? half - 2 : 1);
end
adv(4);
cs_n_pin = 1'b1;
adv(10);
sclk_pin = 1'b0;
adv(8);
end
endtask
// BOUNDED, like every wait in this module. An unbounded wait for a ready signal
// turns a design bug into a simulation that never terminates, and a simulation that
// never terminates reports nothing at all -- which is strictly worse than a failure.
task sys_write;
input [MAX_W-1:0] v;
integer guard;
begin
guard = 4000;
while (!sys_tx_ready && guard > 0) begin
@(negedge clk);
guard = guard - 1;
end
if (guard == 0) begin
$display(" FAIL: the transmit holding register never became free");
errors = errors + 1;
end
sys_tx_data = v;
sys_tx_wr = 1'b1;
@(negedge clk);
sys_tx_wr = 1'b0;
end
endtask
function integer miso_word;
input integer w;
integer i, v;
begin
v = 0;
for (i = 0; i < 8; i = i + 1)
v = (v << 1) | (got_bits[w*8 + i] & 1);
miso_word = v;
end
endfunction
integer k, seq_a, seq_b, cnt_a, bad;
reg [63:0] pat;
initial begin
got_n = 0;
adv(3);
rst_n = 1'b1;
adv(2);
// 1. THE TRANSMIT PATH. Two words written by software must appear on MISO, and
// no underrun must be reported.
sys_write(32'h5A);
pat = 64'h11_22_00_00_00_00_00_00;
drive_txn(1, 4, pat);
if (miso_word(0) != 8'h5A) begin
$display(" FAIL: software wrote 5a and the master read %02h",
miso_word(0));
errors = errors + 1;
end
if (tx_underrun) begin
$display(" FAIL: a loaded word was reported as an underrun");
errors = errors + 1;
end
$display(" software wrote 5a and the master read 5a, with no underrun reported");
// 2. AN UNDERRUN. Nothing loaded, so the DEFAULT must go out -- not the previous
// word, which would look like a master that read the same place twice.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_txn(1, 4, pat);
if (miso_word(0) != 8'hFF) begin
$display(" FAIL: an underrun sent %02h, expected the default ff",
miso_word(0));
errors = errors + 1;
end
if (!tx_underrun) begin
$display(" FAIL: an underrun was not reported");
errors = errors + 1;
end
$display(" with nothing loaded the master read the configured default ff rather than the previous word 5a, and the underrun is reported");
// 3. THE DEFAULT IS CONFIGURABLE, which is what makes it a decision rather than
// an accident.
tx_default = 32'h00;
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
drive_txn(1, 4, pat);
if (miso_word(0) != 8'h00) begin
$display(" FAIL: with a default of 00 an underrun sent %02h",
miso_word(0));
errors = errors + 1;
end
tx_default = 32'hFF;
$display(" changing the default to 00 changes what an underrun sends, so the value is a decision rather than whatever was left in a register");
// 4. THE RECEIVE BUFFER. Three words in one transaction, readable by index with
// a count that says how many there are.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
// ONE write, not two. The holding register is one word deep and stays occupied
// until a word is taken, so a second write before the transaction starts has
// nowhere to go -- which is the interface working, not a limitation to route
// around. This test is about the receive buffer, so one word is enough and the
// remaining words underrun harmlessly.
sys_write(32'hAA);
pat = 64'h11_22_33_00_00_00_00_00;
drive_txn(3, 4, pat);
if (sys_rd_count != 3) begin
$display(" FAIL: three words gave a count of %0d", sys_rd_count);
errors = errors + 1;
end
bad = 0;
for (k = 0; k < 3; k = k + 1) begin
sys_rd_idx = k[PTR_W-1:0];
adv(1);
if (sys_rd_data[7:0] !== ((pat >> (56 - 8*k)) & 8'hFF)) bad = bad + 1;
end
if (bad != 0) begin
$display(" FAIL: %0d of 3 held words read back wrongly", bad);
errors = errors + 1;
end
$display(" three words in one transaction are held, counted and readable by index");
// 5. A CLEAN READ IS NOT REPORTED AS TORN. The sequence number must not change
// across a read that happens entirely between transactions -- a mechanism
// that flagged everything would pass the next test and be useless.
seq_a = sys_rd_seq;
for (k = 0; k < 3; k = k + 1) begin
sys_rd_idx = k[PTR_W-1:0];
adv(2);
end
seq_b = sys_rd_seq;
if (seq_a != seq_b) begin
$display(" FAIL: a read between transactions changed the sequence number from %0d to %0d",
seq_a, seq_b);
errors = errors + 1;
end
$display(" a read taken entirely between transactions leaves the sequence number unchanged at %0d",
seq_b);
// 6. A TORN READ IS DETECTED. The same read with a transaction landing in the
// middle of it MUST show a different sequence number.
seq_a = sys_rd_seq;
sys_rd_idx = 2'd0;
adv(2);
sys_write(32'hCC);
pat = 64'h44_55_00_00_00_00_00_00;
drive_txn(2, 4, pat); // lands in the middle of software's read
sys_rd_idx = 2'd1;
adv(2);
seq_b = sys_rd_seq;
if (seq_a == seq_b) begin
$display(" FAIL: a read spanning a transaction left the sequence number at %0d",
seq_b);
errors = errors + 1;
end
$display(" the same read with a transaction landing inside it shows the sequence number moving from %0d to %0d, so the tear is detectable",
seq_a, seq_b);
// 7. THE SEQUENCE NUMBER MOVES ON THE FIRST WORD, not at the end -- which is
// what makes a tear detectable before the transaction has even finished.
seq_a = sys_rd_seq;
cs_n_pin = 1'b0; adv(6);
for (k = 0; k < 8; k = k + 1) begin
mosi_pin = k[0];
adv(2); sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(2);
end
adv(4);
seq_b = sys_rd_seq;
if (seq_a == seq_b) begin
$display(" FAIL: the sequence number had not moved after the first word arrived");
errors = errors + 1;
end
cs_n_pin = 1'b1; adv(10); sclk_pin = 1'b0; adv(8);
$display(" the sequence number moves as soon as the first word of a transaction lands, not when the transaction ends");
// 8. OVERFLOW. A transaction longer than the buffer is a loss, and a loss is
// reported -- a slave cannot ask the master to slow down.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
pat = 64'h01_02_03_04_05_06_00_00;
drive_txn(6, 4, pat);
if (!rx_overflow) begin
$display(" FAIL: six words into a four-deep buffer was not reported as an overflow");
errors = errors + 1;
end
if (sys_rd_count != DEPTH) begin
$display(" FAIL: after an overflow the count is %0d, expected %0d",
sys_rd_count, DEPTH);
errors = errors + 1;
end
// And the words that DID fit are the FIRST four, not the last four -- dropping
// the newest is the right policy, because the oldest are the ones a command
// interpreter has already begun acting on.
bad = 0;
for (k = 0; k < DEPTH; k = k + 1) begin
sys_rd_idx = k[PTR_W-1:0];
adv(1);
if (sys_rd_data[7:0] !== ((pat >> (56 - 8*k)) & 8'hFF)) bad = bad + 1;
end
if (bad != 0) begin
$display(" FAIL: after an overflow %0d of the first %0d words were lost",
bad, DEPTH);
errors = errors + 1;
end
$display(" six words into a four-deep buffer reports an overflow, holds a count of four, and keeps the FIRST four rather than the last");
// 9. THE HOLDING REGISTER FREES IMMEDIATELY, so software has a whole word's time
// to refill -- the same argument as the master's shadow register.
clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
sys_write(32'h12);
pat = 64'h00_00_00_00_00_00_00_00;
cs_n_pin = 1'b0; adv(6);
k = 0;
for (k = 0; k < 8; k = k + 1) begin
adv(2); sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(2);
end
// The first word has been taken; the register must already be free.
if (!sys_tx_ready) begin
$display(" FAIL: the holding register was still occupied after its word was taken");
errors = errors + 1;
end
sys_write(32'h34);
for (k = 0; k < 8; k = k + 1) begin
adv(2); sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(2);
end
adv(4);
cs_n_pin = 1'b1; adv(10); sclk_pin = 1'b0; adv(8);
if (tx_underrun) begin
$display(" FAIL: refilling within the next word's time still reported an underrun");
errors = errors + 1;
end
$display(" the holding register frees as soon as its word is taken, so software refilling within the next word's time causes no underrun");
if (errors == 0)
$display("PASS: a slave has neither of the master's options -- it cannot ask the master to wait and it cannot choose when its receive buffer changes -- so the transmit side answers with a CONFIGURABLE DEFAULT and a sticky report, which makes an underrun recognisable at the master rather than looking like a repeated read, and the receive side answers with a SEQUENCE NUMBER rather than a lock, because holding the buffer would drop the word the master is sending and convert a detectable problem into a silent loss -- a read taken entirely between transactions leaves the sequence unchanged while the same read with a transaction landing inside it shows it moving, the sequence advances on the FIRST word rather than at the end so a tear is detectable before the transaction finishes, an overflow is reported and keeps the oldest words because those are the ones a command interpreter has already acted on, and the holding register frees the moment its word is taken so software has a whole word's time to refill");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
clk = 1'b0;
rst_n = 1'b0;
cpol = 1'b0;
cpha = 1'b0;
lsb_first = 1'b0;
clr_flags = 1'b0;
sclk_pin = 1'b0;
cs_n_pin = 1'b1;
mosi_pin = 1'b0;
len = 6'd8;
sys_rd_idx = 2'd0;
sys_tx_wr = 1'b0;
sys_tx_data = 32'h0;
tx_default = 32'hFF;
errors = 0;
end
endmodule-- spi_slave_regs_tb.vhd
--
-- The whole slave runs behind this block, so the transmit words really do go out on MOSI
-- and the received words really do arrive from it -- which matters, because both
-- mechanisms under test are about the RELATIONSHIP between software's timing and the
-- master's, and a testbench that poked the block directly would be choosing both.
--
-- The torn-read experiment is the interesting one. Software reads the sequence number,
-- reads a word, and reads the sequence number again -- and the test arranges for a
-- transaction to land in between, so the sequence number MUST have changed. Then it does
-- the same read entirely between transactions and requires that it did not. A mechanism
-- that reported every read as torn would pass the first half.
--
-- Every wait is bounded and there is a watchdog, because a simulation that never
-- terminates reports nothing at all -- which is strictly worse than a failure.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_slave_regs_tb is
end entity;
architecture sim of spi_slave_regs_tb is
constant MAX_W : positive := 32;
constant LEN_W : positive := 6;
constant CNT_W : positive := 12;
constant DEPTH : positive := 4;
constant PTR_W : positive := 2;
constant SEQ_W : positive := 8;
type bit_array is array (0 to 63) of std_logic;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
-- Two separate stop signals, ORed into the clock. `halt` is driven by the stimulus
-- and `wd_halt` by the watchdog: VHDL resolves multiple drivers rather than letting
-- either win, so one signal written from both processes has no defined value at all.
signal halt : boolean := false;
signal wd_halt : boolean := false;
signal stop : boolean;
signal cpol : std_logic := '0';
signal cpha : std_logic := '0';
signal lsb_first : std_logic := '0';
signal clr_flags : 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, 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, txn_report_stb : std_logic;
signal edges_in_txn, frames_in_txn : unsigned(CNT_W - 1 downto 0);
signal txn_clean, txn_trunc, txn_empty : std_logic;
signal cs_state : unsigned(2 downto 0);
signal cap_stb, launch_stb, preload_stb : std_logic;
signal cpol_mismatch, phase_suspect : std_logic;
signal trunc_run : unsigned(3 downto 0);
signal mode_moved_run : unsigned(3 downto 0);
signal rx_data, rx_partial_sr : std_logic_vector(MAX_W - 1 downto 0);
signal rx_valid_stb : std_logic;
signal bit_idx : unsigned(LEN_W - 1 downto 0);
signal words_in_txn : unsigned(CNT_W - 1 downto 0);
signal word_data, partial_data : std_logic_vector(MAX_W - 1 downto 0);
signal word_valid_stb, partial_valid_stb : std_logic;
signal words_complete : unsigned(CNT_W - 1 downto 0);
signal partial_bits : unsigned(LEN_W - 1 downto 0);
signal len_err, aborted : std_logic;
signal frame_state : unsigned(1 downto 0);
signal sys_rd_idx : unsigned(PTR_W - 1 downto 0) := (others => '0');
signal sys_tx_wr : std_logic := '0';
signal sys_tx_data : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
signal tx_default : std_logic_vector(MAX_W - 1 downto 0) := x"000000FF";
signal sys_rd_data : std_logic_vector(MAX_W - 1 downto 0);
signal sys_rd_count : unsigned(PTR_W downto 0);
signal sys_rd_seq : unsigned(SEQ_W - 1 downto 0);
signal sys_tx_ready : std_logic;
signal tx_data : std_logic_vector(MAX_W - 1 downto 0);
signal tx_underrun, rx_overflow : std_logic;
signal miso_val : std_logic;
signal word_taken_stb : std_logic;
signal bits_driven : unsigned(LEN_W - 1 downto 0);
signal lead_seen, half_seen : unsigned(7 downto 0);
signal lead_short, half_short : std_logic;
signal got_bits : bit_array := (others => '0');
signal got_n : natural := 0;
signal samp_stb : std_logic := '0';
signal samp_val : std_logic := '0';
signal clr_got : std_logic := '0';
signal errors : natural := 0;
function hex2(v : std_logic_vector(7 downto 0)) return string is
constant DIGITS : string(1 to 16) := "0123456789abcdef";
variable r : string(1 to 2);
begin
r(1) := DIGITS(to_integer(unsigned(v(7 downto 4))) + 1);
r(2) := DIGITS(to_integer(unsigned(v(3 downto 0))) + 1);
return r;
end function;
begin
stop <= halt or wd_halt;
clk <= not clk after 5 ns when not stop else '0';
-- A WATCHDOG. Every wait is bounded, but a watchdog is the only thing that catches a
-- hang in a wait somebody adds later.
watchdog : process
begin
wait for 2 ms;
-- Guarded: stopping the clock does not stop simulation TIME, so a watchdog that
-- reported unconditionally would print a failure after every successful run.
if not halt then
report "FAIL: the simulation did not finish within its time limit"
severity error;
wd_halt <= true;
end if;
wait;
end process;
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');
u_cs : 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, txn_report_stb => txn_report_stb,
state_id => cs_state);
u_mode : entity work.spi_slave_mode
generic map (SUSPECT_N => 3, CNT_W => 4)
port map (clk => clk, rst_n => rst_n, cpol => cpol, cpha => cpha,
sclk_q => sclk_q, mosi_q => mosi_q,
cs_assert_stb => cs_assert_stb,
edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
txn_active => txn_active, txn_start_stb => txn_start_stb,
txn_report_stb => txn_report_stb, txn_clean => txn_clean,
txn_trunc => txn_trunc,
cap_stb => cap_stb, launch_stb => launch_stb,
preload_stb => preload_stb,
cpol_mismatch => cpol_mismatch, phase_suspect => phase_suspect,
moved_run => mode_moved_run, trunc_run => trunc_run, clr_flags => '0');
u_rx : entity work.spi_slave_rx
generic map (MAX_W => MAX_W, LEN_W => LEN_W, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n,
txn_active => txn_active, txn_start_stb => txn_start_stb,
cap_stb => cap_stb, mosi_q => mosi_q,
len => len, lsb_first => lsb_first,
rx_data => rx_data, rx_valid_stb => rx_valid_stb,
bit_idx => bit_idx, words_in_txn => words_in_txn,
rx_partial_sr => rx_partial_sr);
u_frame : entity work.spi_slave_frame
generic map (MAX_W => MAX_W, LEN_W => LEN_W, CNT_W => CNT_W)
port map (clk => clk, rst_n => rst_n, len => len,
txn_start_stb => txn_start_stb, txn_report_stb => txn_report_stb,
txn_trunc => txn_trunc, txn_empty => txn_empty,
rx_data => rx_data, rx_valid_stb => rx_valid_stb,
bit_idx => bit_idx, rx_partial_sr => rx_partial_sr,
word_data => word_data, word_valid_stb => word_valid_stb,
words_complete => words_complete,
partial_data => partial_data, partial_bits => partial_bits,
partial_valid_stb => partial_valid_stb,
len_err => len_err, aborted => aborted,
state_id => frame_state, clr_flags => '0');
dut : entity work.spi_slave_regs
generic map (MAX_W => MAX_W, DEPTH => DEPTH, PTR_W => PTR_W, SEQ_W => SEQ_W)
port map (clk => clk, rst_n => rst_n,
txn_start_stb => txn_start_stb,
word_data => word_data, word_valid_stb => word_valid_stb,
word_taken_stb => word_taken_stb,
sys_rd_idx => sys_rd_idx, sys_rd_data => sys_rd_data,
sys_rd_count => sys_rd_count, sys_rd_seq => sys_rd_seq,
sys_tx_wr => sys_tx_wr, sys_tx_data => sys_tx_data,
sys_tx_ready => sys_tx_ready, tx_default => tx_default,
tx_data => tx_data,
tx_underrun => tx_underrun, rx_overflow => rx_overflow,
clr_flags => clr_flags);
u_tx : entity work.spi_slave_tx
generic map (MAX_W => MAX_W, LEN_W => LEN_W, LEAD_MIN => 3, HALF_MIN => 3,
CNT_W => 8)
port map (clk => clk, rst_n => rst_n,
txn_active => txn_active, txn_start_stb => txn_start_stb,
preload_stb => preload_stb, launch_stb => launch_stb,
cap_stb => cap_stb,
len => len, lsb_first => lsb_first, tx_data => tx_data,
miso => miso_val,
word_taken_stb => word_taken_stb, bits_driven => bits_driven,
lead_seen => lead_seen, lead_short => lead_short,
half_seen => half_seen, half_short => half_short,
clr_flags => '0');
sampler : process (clk)
begin
if rising_edge(clk) then
if clr_got = '1' then
got_n <= 0;
elsif samp_stb = '1' then
if got_n < 64 then
got_bits(got_n) <= samp_val;
end if;
got_n <= got_n + 1;
end if;
end if;
end process;
stim : process
variable errs : natural := 0;
variable pat : std_logic_vector(63 downto 0);
variable seq_a, seq_b, bad, guard : natural;
variable w : std_logic_vector(7 downto 0);
procedure adv(n : natural) is
begin
for j in 1 to n loop wait until falling_edge(clk); end loop;
end procedure;
procedure arm_sample is
begin
samp_val <= miso_val;
samp_stb <= '1';
end procedure;
procedure disarm_sample is
begin
samp_stb <= '0';
end procedure;
-- BOUNDED, like every wait here.
procedure sys_write(v : std_logic_vector(MAX_W - 1 downto 0)) is
begin
guard := 4000;
while sys_tx_ready = '0' and guard > 0 loop
wait until falling_edge(clk);
guard := guard - 1;
end loop;
if guard = 0 then
report " FAIL: the transmit holding register never became free";
errs := errs + 1;
end if;
sys_tx_data <= v;
sys_tx_wr <= '1';
wait until falling_edge(clk);
sys_tx_wr <= '0';
end procedure;
procedure drive_txn(nwords : natural; half : natural;
pattern : std_logic_vector(63 downto 0)) is
begin
cpol <= '0'; sclk_pin <= '0'; cs_n_pin <= '1';
adv(10);
clr_got <= '1';
wait until falling_edge(clk);
clr_got <= '0';
cs_n_pin <= '0';
adv(6);
for i in 0 to nwords * 8 - 1 loop
mosi_pin <= pattern(63 - i);
adv(2);
arm_sample;
sclk_pin <= '1';
adv(1);
disarm_sample;
if half > 1 then adv(half - 1); end if;
sclk_pin <= '0';
if half > 2 then adv(half - 2); else adv(1); end if;
end loop;
adv(4);
cs_n_pin <= '1';
adv(10);
sclk_pin <= '0';
adv(8);
end procedure;
impure function miso_word(w : natural) return std_logic_vector is
variable v : std_logic_vector(7 downto 0) := (others => '0');
begin
for i in 0 to 7 loop
v := v(6 downto 0) & got_bits(w * 8 + i);
end loop;
return v;
end function;
begin
adv(3);
rst_n <= '1';
adv(2);
-- 1. THE TRANSMIT PATH.
sys_write(x"0000005A");
pat := x"1122000000000000";
drive_txn(1, 4, pat);
if miso_word(0) /= x"5A" then
report " FAIL: software wrote 5a and the master read " &
hex2(miso_word(0));
errs := errs + 1;
end if;
if tx_underrun = '1' then
report " FAIL: a loaded word was reported as an underrun";
errs := errs + 1;
end if;
report " software wrote 5a and the master read 5a, with no underrun reported";
-- 2. AN UNDERRUN sends the DEFAULT, not the previous word.
clr_flags <= '1'; adv(1); clr_flags <= '0';
drive_txn(1, 4, pat);
if miso_word(0) /= x"FF" then
report " FAIL: an underrun sent " & hex2(miso_word(0)) &
", expected the default ff";
errs := errs + 1;
end if;
if tx_underrun /= '1' then
report " FAIL: an underrun was not reported";
errs := errs + 1;
end if;
report " with nothing loaded the master read the configured default ff rather than the previous word 5a, and the underrun is reported";
-- 3. THE DEFAULT IS CONFIGURABLE.
tx_default <= x"00000000";
clr_flags <= '1'; adv(1); clr_flags <= '0';
drive_txn(1, 4, pat);
if miso_word(0) /= x"00" then
report " FAIL: with a default of 00 an underrun sent " &
hex2(miso_word(0));
errs := errs + 1;
end if;
tx_default <= x"000000FF";
report " changing the default to 00 changes what an underrun sends, so the value is a decision rather than whatever was left in a register";
-- 4. THE RECEIVE BUFFER. ONE write, because the holding register is one word
-- deep and stays occupied until a word is taken -- which is the interface
-- working, not a limitation to route around.
clr_flags <= '1'; adv(1); clr_flags <= '0';
sys_write(x"000000AA");
pat := x"1122330000000000";
drive_txn(3, 4, pat);
if to_integer(sys_rd_count) /= 3 then
report " FAIL: three words gave a count of " &
integer'image(to_integer(sys_rd_count));
errs := errs + 1;
end if;
bad := 0;
for k in 0 to 2 loop
sys_rd_idx <= to_unsigned(k, PTR_W);
adv(1);
w := pat(63 - 8 * k downto 56 - 8 * k);
if sys_rd_data(7 downto 0) /= w then bad := bad + 1; end if;
end loop;
if bad /= 0 then
report " FAIL: " & integer'image(bad) &
" of 3 held words read back wrongly";
errs := errs + 1;
end if;
report " three words in one transaction are held, counted and readable by index";
-- 5. A CLEAN READ IS NOT REPORTED AS TORN.
seq_a := to_integer(sys_rd_seq);
for k in 0 to 2 loop
sys_rd_idx <= to_unsigned(k, PTR_W);
adv(2);
end loop;
seq_b := to_integer(sys_rd_seq);
if seq_a /= seq_b then
report " FAIL: a read between transactions changed the sequence number";
errs := errs + 1;
end if;
report " a read taken entirely between transactions leaves the sequence number unchanged at " &
integer'image(seq_b);
-- 6. A TORN READ IS DETECTED.
seq_a := to_integer(sys_rd_seq);
sys_rd_idx <= to_unsigned(0, PTR_W);
adv(2);
sys_write(x"000000CC");
pat := x"4455000000000000";
drive_txn(2, 4, pat);
sys_rd_idx <= to_unsigned(1, PTR_W);
adv(2);
seq_b := to_integer(sys_rd_seq);
if seq_a = seq_b then
report " FAIL: a read spanning a transaction left the sequence number unchanged";
errs := errs + 1;
end if;
report " the same read with a transaction landing inside it shows the sequence number moving from " &
integer'image(seq_a) & " to " & integer'image(seq_b) &
", so the tear is detectable";
-- 7. THE SEQUENCE NUMBER MOVES ON THE FIRST WORD.
seq_a := to_integer(sys_rd_seq);
cs_n_pin <= '0'; adv(6);
for k in 0 to 7 loop
if (k mod 2) = 0 then mosi_pin <= '0'; else mosi_pin <= '1'; end if;
adv(2); sclk_pin <= '1'; adv(4); sclk_pin <= '0'; adv(2);
end loop;
adv(4);
seq_b := to_integer(sys_rd_seq);
if seq_a = seq_b then
report " FAIL: the sequence number had not moved after the first word arrived";
errs := errs + 1;
end if;
cs_n_pin <= '1'; adv(10); sclk_pin <= '0'; adv(8);
report " the sequence number moves as soon as the first word of a transaction lands, not when the transaction ends";
-- 8. OVERFLOW, and the OLDEST words survive.
clr_flags <= '1'; adv(1); clr_flags <= '0';
pat := x"0102030405060000";
drive_txn(6, 4, pat);
if rx_overflow /= '1' then
report " FAIL: six words into a four-deep buffer was not reported as an overflow";
errs := errs + 1;
end if;
if to_integer(sys_rd_count) /= DEPTH then
report " FAIL: after an overflow the count is " &
integer'image(to_integer(sys_rd_count));
errs := errs + 1;
end if;
bad := 0;
for k in 0 to DEPTH - 1 loop
sys_rd_idx <= to_unsigned(k, PTR_W);
adv(1);
w := pat(63 - 8 * k downto 56 - 8 * k);
if sys_rd_data(7 downto 0) /= w then bad := bad + 1; end if;
end loop;
if bad /= 0 then
report " FAIL: after an overflow " & integer'image(bad) &
" of the first words were lost";
errs := errs + 1;
end if;
report " six words into a four-deep buffer reports an overflow, holds a count of four, and keeps the FIRST four rather than the last";
-- 9. THE HOLDING REGISTER FREES IMMEDIATELY.
clr_flags <= '1'; adv(1); clr_flags <= '0';
sys_write(x"00000012");
cs_n_pin <= '0'; adv(6);
for k in 0 to 7 loop
adv(2); sclk_pin <= '1'; adv(4); sclk_pin <= '0'; adv(2);
end loop;
if sys_tx_ready /= '1' then
report " FAIL: the holding register was still occupied after its word was taken";
errs := errs + 1;
end if;
sys_write(x"00000034");
for k in 0 to 7 loop
adv(2); sclk_pin <= '1'; adv(4); sclk_pin <= '0'; adv(2);
end loop;
adv(4);
cs_n_pin <= '1'; adv(10); sclk_pin <= '0'; adv(8);
if tx_underrun = '1' then
report " FAIL: refilling within the next word's time still reported an underrun";
errs := errs + 1;
end if;
report " the holding register frees as soon as its word is taken, so software refilling within the next word's time causes no underrun";
errors <= errs;
if errs = 0 then
report "PASS: a slave has neither of the master's options -- it cannot ask the master to wait and it cannot choose when its receive buffer changes -- so the transmit side answers with a CONFIGURABLE DEFAULT and a sticky report, which makes an underrun recognisable at the master rather than looking like a repeated read, and the receive side answers with a SEQUENCE NUMBER rather than a lock, because holding the buffer would drop the word the master is sending and convert a detectable problem into a silent loss -- a read taken entirely between transactions leaves the sequence unchanged while the same read with a transaction landing inside it shows it moving, the sequence advances on the FIRST word rather than at the end so a tear is detectable before the transaction finishes, an overflow is reported and keeps the oldest words because those are the ones a command interpreter has already acted on, and the holding register frees the moment its word is taken so software has a whole word's time to refill";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;6. Why a Verification Engineer Cares
Test 7 is the one that distinguishes a working seqlock from a plausible one, and it is the one a bench does not naturally contain. Test 6 — land a whole transaction mid-read — passes against a design that increments at the transaction end, because by the time the reader re-samples, the transaction has finished. The failing case needs exactly one word of a new transaction to land inside the read window, which requires the bench to control timing at word granularity rather than at transaction granularity.
A tearing check needs both directions. Test 5 asserts that a clean read is not reported torn. Without it, a design whose sequence number incremented every cycle would pass test 6 and be useless. The pair of tests is what makes the mechanism usable; either alone permits a degenerate design.
The underrun test must check the value, not just the flag. Asserting tx_underrun fires is not enough — the whole argument of §1 is about which value goes out, because that is what the master sees and the flag is not visible to the master at all.
Bound every wait. §5's callout is a bench-quality point rather than a design point, and it generalises: a testbench that can hang has a failure mode with no diagnostic. A bounded wait that fails reports what it was waiting for.
// Properties for the system-side interface.
property p_seq_advances_on_first_word;
// Section 2's rule, stated directly: the first word of a transaction moves
// the sequence number. A design that moves it at the transaction end
// satisfies a weaker property and misleads software.
@(posedge clk) disable iff (!rst_n)
(word_valid_stb && first_word_of_txn) |=> (sys_rd_seq != $past(sys_rd_seq));
endproperty
property p_seq_stable_without_words;
// And it moves for no other reason. A counter that ticks on anything else
// reports every read as torn, which is as useless as reporting none.
@(posedge clk) disable iff (!rst_n)
!word_valid_stb |=> $stable(sys_rd_seq);
endproperty
property p_default_on_underrun;
// The value matters, not just the flag -- the master sees the value and
// cannot see the flag.
@(posedge clk) disable iff (!rst_n)
(word_taken_stb && !loaded) |=> (tx_data == tx_default);
endproperty
property p_underrun_sticky;
// The master's read has already happened by the time software could look.
@(posedge clk) disable iff (!rst_n)
tx_underrun && !clr_flags |=> tx_underrun;
endproperty
property p_buffer_never_held;
// A word is never refused in order to protect a reader. This is the trade
// in section 2: a detectable tear beats a silent loss, so the write path
// has no back-pressure at all.
@(posedge clk) disable iff (!rst_n)
word_valid_stb |=> (sys_rd_count > 0);
endproperty
property p_hold_freed_at_take;
// Section 4: software gets a whole word period, so the register frees at the
// START of the transmitted word rather than at its end.
@(posedge clk) disable iff (!rst_n)
word_taken_stb |=> sys_tx_ready;
endproperty// Coverage. The axis that matters is WHERE a word lands relative to a software
// read, because that is what the seqlock is about -- and it is an axis most
// benches do not have at all.
covergroup cg_regs @(posedge clk iff sys_read_done);
option.per_instance = 1;
// Where the arriving word fell relative to the read window. "first_of_txn
// inside" is test 7's case and the one that separates a working seqlock
// from a plausible one.
landing: coverpoint word_landing_vs_read {
bins none = {0}; // a clean read
bins before = {1};
bins first_of_txn_in = {2}; // the case that matters
bins mid_txn_in = {3};
bins whole_txn_in = {4};
bins after = {5};
}
// How full the buffer was, because overflow is a different report from a
// tear and the two can coincide.
fill: coverpoint sys_rd_count {
bins empty = {0};
bins some = {[1:3]};
bins full = {4};
}
// Whether software supplied a transmit word in time. The underrun bin has to
// be reached deliberately; no random stimulus produces it reliably.
supply: coverpoint tx_supply_state {
bins in_time = {0};
bins late = {1}; // underrun
bins never = {2}; // default for a whole transaction
}
x_landing_fill: cross landing, fill;
x_supply_fill: cross supply, fill;
endgroup7. Why an FPGA or ASIC Engineer Cares
The buffer is a register file read combinationally by sys_rd_idx, not a FIFO. That is a deliberate choice: software reads words by index and can re-read them, which a FIFO's destructive read forbids — and re-reading is exactly what the seqlock's retry requires. A FIFO here would make the retry impossible, because the first attempt would have consumed the data.
DEPTH and PTR_W are separate parameters and PTR_W is supplied rather than derived. Deriving it needs $clog2, which puts the module outside Verilog-2001. Supplying it means a mismatch is possible, so the pair is checked once at the top level rather than defended in every module — the same discipline as SYNC_N in Chapter 14.8.
The combinational read path is DEPTH-to-1 at MAX_W bits wide. At DEPTH = 4 and MAX_W = 32 that is a 32-bit 4-to-1 mux, which is nothing. At DEPTH = 16 it is worth registering the output and accepting one cycle of read latency — and that is a change software must know about, because a driver that reads the index and the data in the same bus cycle would then read stale data.
The sequence counter is the one piece of state a clock-domain crossing would complicate. If the register bus moved to a different clock, sys_rd_seq would have to cross it — and a multi-bit counter crossing a domain needs either a Gray encoding or a handshake, because a binary counter sampled mid-increment can read a value it never held. That would break the seqlock in the silent-false-negative way §2 warns about, so it is the first thing to look at if this interface ever moves domains.
8. Failure Signature — Occasional Impossible Sensor Readings
The symptom:
"Once every few thousand reads we get a temperature of 4000 degrees. The sensor is fine, the checksum over the payload passes, and the driver is single-threaded."
What is happening: a torn read. The driver walked four words; a new transaction's first word landed after it read word 0 and before it read word 1. Words 1 to 3 are from the new transaction, word 0 from the old one — and the payload checksum passes because both halves are internally valid and the checksum was computed over the wrong grouping.
Why the checksum does not help: it validates that the bytes were received correctly, which they were. Tearing is not a corruption of bytes; it is a mis-grouping of correct bytes, and no per-byte or per-payload integrity check can see it. That is the specific reason the seqlock is needed rather than a checksum.
How to confirm and fix: read sys_rd_seq before and after the walk. If it differs on the failing reads, that is the mechanism, and the driver's fix is to retry rather than to trust. If a design has no sequence number, the confirming experiment is to read the words twice in quick succession and compare — a tear will usually not repeat, and a genuine sensor fault will.
9. Common Misconceptions
"A slave should hold its receive buffer while software reads it." It cannot: holding means dropping the word the master is sending, which converts a detectable tear into a silent loss. The buffer is never held, and the reader detects tearing instead.
"An underrun should send the previous word — at least it is valid data." It is valid data that makes the underrun look like a master reading the same location twice, which is plausible enough to send the investigation to the driver's addressing. A configured default matching the board's idle state makes the symptom point at the cause.
"The sequence number can be incremented at the end of a transaction — that is when the transaction is complete." Then a reader that samples it after the first word of a new transaction has landed sees no change and concludes its read was clean, holding one word from each transaction. The tear is invisible exactly in the case the mechanism exists for.
"A narrow sequence counter is fine because reads are short." The cost of a wrap within one read is a tear reported as clean, which is worse than no mechanism because software trusts it. Eight bits is cheap and the failure mode of being too narrow is not recoverable by anything downstream.
"A payload checksum makes a seqlock unnecessary." A checksum validates bytes; tearing mis-groups correct bytes. Both halves of a torn read are internally valid, so no integrity check over the payload can see it.
"The receive buffer should be a FIFO — that is what buffers are." A FIFO's destructive read forbids the retry the seqlock depends on. An indexed register file lets software read, detect a tear, and read again.
10. Reason It Through
Q. Software reads rx_seq as 7, reads four words, and reads rx_seq as 7 again. What does it know?
That no word arrived during the read, so all four words are from the same transaction. Note what it does not know: whether that transaction was clean. txn_trunc and edges_in_txn from Chapter 14.2 are separate observations, and this is why Chapter 14.3 argued the verdict must be readable alongside the words — the seqlock establishes that the words belong together, not that they are correct.
Q. SEQ_W = 2. Software reads the sequence number, is preempted for long enough that four transactions complete, and reads it again. What happens?
It reads the same value, concludes the read was clean, and is wrong — four transactions wrapped a two-bit counter exactly. This is §2's width requirement and it is why the failure mode is a false negative rather than a false positive: the mechanism does not fail loudly, it fails silently and with software's confidence behind it.
Q. Why is the holding register freed at the start of the transmitted word rather than at its end, and what does software give up?
To give software a whole word period — 64 system clocks at 8 bits and a divisor of 8 — rather than a handful of cycles, which is the difference between a polling driver working and needing an interrupt per byte. What software gives up is the ability to change its mind: a write landing after word_taken_stb applies to the following word, so the value is committed one word early. That is the same freeze-early trade as Chapter 13.2's configuration snapshot.
Q. A reviewer proposes replacing the sequence number with a "read in progress" bit that software sets and clears, which the slave uses to defer buffer updates. What is wrong with it?
Deferring means the arriving word has nowhere to go, so it is dropped — the silent loss §2 rules out. And it introduces a worse failure: software that crashes between setting and clearing the bit leaves the slave deferring forever, so the device stops receiving entirely and the cause is a flag in a register nobody is looking at. A reader-side mechanism cannot be given authority over a writer that must not be blocked.
Q. DEPTH = 16 and someone registers the buffer read to shorten the combinational path. What must change in software, and why is it a compatibility break?
The read becomes one cycle late, so a driver that writes sys_rd_idx and reads sys_rd_data in the same bus access gets the previous index's data. Every read must now be a two-step: set the index, then read. It is a compatibility break because the register map has not changed and nothing in it announces the new latency — the same registers, the same widths, and a different protocol for using them, which is the kind of change that gets discovered by a driver that worked on the previous silicon revision.
11. Understanding Check
12. Summary
A slave cannot ask the master to wait and cannot choose when its receive buffer changes. So the system side is two mechanisms, and neither is a buffer.
On the transmit side, an underrun sends a configured default — not the previous word, which makes the underrun look like a duplicated read, and not whatever is in a register, which makes it non-reproducible. The default is configurable because what an absent device looks like depends on the board's pull, and matching it means the driver's existing absent-device handling is the handling an underrun needs. tx_underrun is sticky, because the master's read has already happened.
On the receive side, the buffer is never held — holding it would drop the arriving word, converting a detectable tear into a silent loss. Instead a seqlock: software reads the sequence number, the words, and the sequence number again.
Two details are correctness requirements rather than refinements. The number must advance on the first word, or a reader holding one word from a new transaction sees no change. And it must be wide enough not to wrap within a read, because the failure mode is a tear reported as clean — and a mechanism that can produce a false negative is worse than no mechanism, because software trusts it.
The holding register frees at the start of the transmitted word, giving software a whole word period rather than a few cycles, at the cost of committing the value one word early — the same freeze-early trade as Chapter 13.2's configuration snapshot.
For verification: build the test where exactly one word lands inside a read, because the whole-transaction version passes against a broken design; check tearing in both directions, since a counter that ticks constantly passes the positive test; check the underrun's value and not just its flag, because the master sees the value and cannot see the flag; and bound every wait, because a bench that hangs reports nothing.
For implementation: an indexed register file rather than a FIFO, because the retry needs a non-destructive read; PTR_W supplied rather than derived, with the match checked at the top level; and the sequence counter is the one piece of state that a clock-domain crossing would break in the silent way.
13. What Comes Next
Nine blocks exist and each has been verified against pin-level requirements rather than against the others. None of them has ever met a real master.
Chapter 14.10 — Synthesizable Slave Architecture and RTL Review assembles all nine and wires them to the master of Chapter 13.11, byte for byte, with nothing in between. They interoperate on the first attempt — because Module 14 derived three numbers a master must respect and Module 13 made all three programmable.
And then integration finds two things neither module's own bench could have found: that the master truncates the last half-period of every transaction at every divisor, which forced a real distinction into Chapter 14.1's ratio monitor, and that a real master's MOSI appears one cycle after the edge that launched it, which forced Chapter 14.6's phase detector to become a window. Both are in the chapter, with the reasoning that produced them.
Continue learning
Related tutorials
- Related topic
Command Encoding and Register Access
How a command byte packs direction, auto-increment and a register address, why the polarity of the read/write bit differs between parts and silently turns reads into destructive writes, and the codec that encodes and decodes any convention.
- Related topic
Slave Microarchitecture and Clocking Assumptions
The two architectures available to a slave that does not own its clock, the one precondition the chosen architecture rests on and why no simulation can test it, why MOSI must pass through exactly as many flops as SCLK, and a delay-matched front end verified in three HDLs.
- Related topic
CS Detection and Transaction Boundaries
Chip select is SPI's only framing and therefore its only resynchronisation point: why counters must reset on assert, how to classify a transaction with a running remainder instead of a divider, why a CPHA mismatch is invisible to the edge count, and a transaction detector verified in three HDLs.
- Related topic
MOSI Capture and Bit Counting
Why the bit counter takes its zero from chip select and nothing else, why a one-bit slip produces well-formed words at the wrong offsets, the complete taxonomy of which configuration mismatches a slave can detect, and a receive path verified in three HDLs across 210 words.
