Skip to content
VLSI Mentor

SPI · Module 14

Reset Behaviour and Safe Idle

A slave's reset lands mid-transaction, and the fact it most needs is destroyed by the reset that created the situation because the synchronisers reset to deselected. Recovering that fact exactly from timing instead, why refusing one transaction beats guessing, and a safe-idle gate verified in three HDLs.

A master's reset is synchronous with everything it does, because it generates the clock. A slave's is not: reset comes from its own system and SCLK comes from somebody else's. So reset can — and on a real board, will — be released in the middle of a transaction that is already under way.

Reset releases. Chip select is already low, SCLK is running, bits are arriving. What should the slave do?

The natural answer is "start receiving". It is wrong, and it is wrong in the worst available way.

1. Why Joining In Is The Wrong Answer

The slave has missed an unknown number of bits. It does not know whether this is bit 3 or bit 300. So every word it assembles from here is misaligned — and, from Chapter 14.3, misaligned in a way nothing can detect: the words are the right length and arrive at the right times, so the system receives plausible data from a device that has no idea where it is.

On the transmit side it is worse. The slave will start driving MISO on the next launch edge, in the middle of a word the master is part-way through reading. So the master's word is corrupted too — by a device that has just come out of reset and has, from its own point of view, done nothing wrong.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   join in                              refuse
   ---------------------------------    ------------------------------------
   receives plausible wrong data        receives nothing for one transaction
   corrupts the master's read too       leaves MISO released
   undetectable by either device        reportable, by one flag
   lasts until the master notices       lasts exactly one transaction
     something at a higher layer

2. The Right Answer: Refuse, And Say So

A slave that comes out of reset with chip select already asserted is stranded. It must not capture, must not drive, and must not report anything about the transaction it missed. It waits for chip select to go high — which ends that transaction — and rejoins on the next assert.

That is the safe idle of the chapter's title, and there is a subtlety in the name worth drawing out:

How long being stranded lasts: exactly one transaction. That is the whole cost. The missed transaction is lost and the next one is normal — so a master with a timeout, which is any master worth using, gets correct behaviour on its retry. Refusing costs one transaction; guessing costs an unbounded stretch of plausible wrong data.

What reset must do immediately: release MISO. That is Chapter 14.5's asynchronous reset, and it is required rather than preferred — a slave held in reset while driving a shared bus makes every other device on that bus unusable.

What reset must not do at all: resume. There is no partial state worth keeping. A slave that tried to remember where it was would be remembering a position it has no way to validate.

3. The Hard Part: How The Slave Knows

The obvious test is "at reset release, is chip select already low?" — and it cannot be asked.

The reason is that the slave's own front end is in the same reset. Chapter 14.1's synchroniser chain resets to the deselected value, and it has to: a slave that woke up believing it was selected would be worse than one that woke up believing it was not. So at the instant reset releases, cs_active reads 0 whatever the pin is doing.

The information has been destroyed by the thing that needs it.

What survives is timing

After reset the chain is full of ones — deselected. So:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   if the pin is ALREADY LOW at reset release:
       cs_active rises exactly SYNC_N cycles later, and not before

   if the pin is HIGH and falls afterwards:
       the assert cannot arrive until at least SYNC_N cycles AFTER the fall,
       which is strictly later than SYNC_N cycles after reset release

Therefore:

An assert observed within SYNC_N cycles of reset release means the select was already low when reset released.

And that is exact rather than heuristic: the two cases cannot produce the same arrival cycle. A post-reset counter of SYNC_N + 1 states is the whole mechanism.

4. The Gate Must Refuse The Assert It Is Refusing

This is a small point with a large consequence, and the first version of this block got it wrong.

The natural gate condition is state != S_STRANDED — pass the select through unless we are stranded. But on the cycle the stranding assert arrives, the state machine has not yet entered S_STRANDED; it enters it because of that assert. So the gate passes the very assert it is refusing, and the downstream blocks start a transaction that the gate then blocks the rest of.

The fix is to gate on a signal that is true on that cycle:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   refuse = (state == S_STRANDED) | (stale_window & cs_active)

and to use refuse in all three gates — the level, the assert strobe and the deassert strobe. The stale_window term is true on the cycle the stranding assert arrives, which is precisely the cycle the state test misses.

The general shape: a gate derived from a state that the event itself causes is one cycle late, and the missing cycle is the event.

5. What Reset Also Destroys: The Count

A counter of how many times reset has stranded this slave cannot survive the reset that increments it. It can only ever read zero or one.

So was_stranded is a flag rather than a count, and a system that wants the count has to keep it somewhere outside this reset domain. That is a system-level decision and not something this block can fake — and the testbench keeps the count itself, which is the honest place for it.

It is worth stating because the alternative is a counter that looks useful and is not. A field labelled "stranding events" that reads 1 after fifty resets is worse than a flag labelled "stranded since the last clear", because the first invites arithmetic that the value cannot support.

6. The State Machine

Safe-idle states: IDLE joins a transaction on a normal assert or becomes STRANDED on an assert inside the post-reset stale window; ACTIVE returns to IDLE on deassert; STRANDED leaves only on chip select risingIDLEACTIVESTRANDEDassert, window pastassert, window pastassert in windowassert in windowdeassertdeassertcs risescs risesrefuse everythingrefuse everything
Figure 1 — the safe-idle gate. IDLE and STRANDED both refuse to participate and look identical on the pins; they differ in what they are sensitive to, which is why they are two states. The only exit from STRANDED is chip select rising, because that is the only event that ends the transaction the slave missed — and the transition into it is taken on an assert that arrives inside the stale window, which is the exact test of section 3.
Ten system-clock cycles across six rows. Reset releases on cycle 0 and a stale window covers cycles 0 to 2. In the already-low case the chip-select pin is low throughout and the assert strobe arrives on cycle 2, inside the window. In the falls-later case the pin falls on cycle 2 and the assert arrives on cycle 4, outside it.inside: strandedinside: strandedoutside: normaloutside: normalclkrst_nwindowcs_n: lowassert: lowcs_n: laterassert: latert0t1t2t3t4t5t6t7t8t9
Figure 2 — the two cases the timing test separates, at SYNC_N = 2. Above the divide, the select was already low when reset released, so the synchroniser publishes an assert on cycle 2 — inside the window. Below it, the select falls on cycle 2 and the assert arrives on cycle 4, outside it. No pin behaviour can put an assert inside the window without the select having been low already, which is what makes the test exact.

7. Building the Safe-Idle Gate — Three HDLs

The circuit

Three states, a four-bit saturating post-reset counter, and three gated outputs. The counter is four bits rather than a computed width — it covers any synchroniser depth anyone will build, and it keeps the module inside Verilog-2001, where a width expression would need $clog2.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_safe_idle.sv — three states, a saturating post-reset counter, and a gate that refuses the event that triggers it
// spi_slave_safe_idle.sv
//
// Chapter 14.8 -- reset lands wherever it lands, and the state the slave must refuse
// to be in.
//
// A master's reset is synchronous with everything it does, because it generates the
// clock. A slave's is not: reset comes from its own system and SCLK comes from
// somebody else's, so reset can and will be released in the MIDDLE of a transaction
// that is already under way.
//
// THE QUESTION, AND THE WRONG ANSWER.
//
// Reset releases. Chip select is already low, SCLK is running, bits are arriving. What
// should the slave do?
//
// The natural answer is "start receiving", and it is wrong. The slave has missed an
// unknown number of bits -- it does not know whether this is bit 3 or bit 300 -- so
// every word it assembles from here is misaligned, and worse, it is misaligned in a way
// nothing can detect: the words are the right length and arrive at the right times
// (Chapter 14.3), so the system receives plausible data from a device that has no idea
// where it is.
//
// And on the transmit side it is worse still. The slave will start driving MISO on the
// next launch edge, in the middle of a word the master is part-way through reading, so
// the master's word is corrupted too -- by a device that just came out of reset and
// has, from its own point of view, done nothing wrong.
//
// THE RIGHT ANSWER IS TO REFUSE.
//
// A slave that comes out of reset with chip select already asserted is STRANDED. It
// must not capture, must not drive, and must not report anything. It waits for chip
// select to go HIGH -- which ends the transaction it missed -- and then rejoins on the
// next assert.
//
// That is the "safe idle" of this chapter's title, and it is worth noticing that the
// safe state is not the reset state: reset puts the slave in a state where it is not
// participating, and the stranded state is a second, distinct state where it is not
// participating BECAUSE IT CANNOT KNOW WHERE IT IS. They look identical on the pins and
// they are reached differently, which is the same argument as the master's IDLE and GAP
// (Chapter 13.3): two states with the same outputs and different input sensitivity.
//
// WHAT RESET MUST DO IMMEDIATELY, AND WHAT IT MUST NOT.
//
//   IMMEDIATELY: release MISO. That is Chapter 14.5's asynchronous reset, and it is
//   required rather than preferred -- a slave held in reset while driving a shared bus
//   makes every other device on that bus unusable.
//
//   NOT AT ALL: resume. There is no partial state worth keeping. A slave that tried to
//   remember where it was would be remembering a position it has no way to validate.
//
// HOW LONG BEING STRANDED LASTS. Exactly one transaction, and that is the whole cost:
// the missed transaction is lost, and the next one is normal. A master that retries --
// which any master with a timeout does -- gets correct behaviour on the retry, which is
// why refusing is cheap and guessing is not.
//
// AND NOW THE HARD PART: HOW THE SLAVE KNOWS.
//
// The obvious test is "at reset release, is chip select already low?" It cannot be
// asked, and the reason is that the slave's own front end is in the same reset. The
// synchroniser chain of Chapter 14.1 resets to the DESELECTED value -- it has to, or a
// slave would wake up believing it was selected -- so at the instant reset releases,
// `cs_active` reads 0 whatever the pin is doing. The information has been destroyed by
// the thing that needs it.
//
// What survives is TIMING. After reset the chain is full of ones, so if the pin is
// already low, `cs_active` rises exactly SYNC_N cycles later and not before. If the pin
// is high and falls afterwards, the assert cannot arrive until at least SYNC_N cycles
// after the fall -- which is strictly later. So:
//
//     an assert observed within SYNC_N cycles of reset release means the select
//     was ALREADY LOW when reset released
//
// and that is exact rather than heuristic: the two cases cannot produce the same
// arrival cycle. A post-reset counter of SYNC_N + 1 states is the whole mechanism.
//
// It is worth noticing what kind of inference this is. The slave recovers a fact about
// a signal it could not observe, from WHEN a derived signal arrived -- which is the same
// trick as measuring an interval in recovered cycles so the latency cancels (Chapters
// 14.4 and 14.5), used to recover a value rather than a duration.
//
// ONE MORE THING RESET DESTROYS. A counter of how many times reset has stranded this
// slave cannot survive the reset that increments it, so it can only ever read zero or
// one. `was_stranded` is therefore a flag and not a count, and a system that wants the
// count has to keep it somewhere that is not in this reset domain -- which is a
// system-level decision and not something this block can fake.

module spi_slave_safe_idle #(
    parameter int SYNC_N = 2   // must match the front end's synchroniser depth
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- from the front end of 14.1 ---------------------------------------
    input  wire              cs_active,
    input  wire              cs_assert_stb,
    input  wire              cs_deassert_stb,

    // --- the gated view the rest of the slave sees ------------------------
    output wire              g_cs_active,
    output wire              g_cs_assert_stb,
    output wire              g_cs_deassert_stb,

    // --- status -----------------------------------------------------------
    output wire              participating,
    output wire              stranded,
    // A FLAG, not a count: see the header. A counter of reset events cannot survive
    // the reset that increments it.
    output reg               was_stranded,
    output wire [1:0]        state_id,
    input  wire              clr_flags
);

    localparam [1:0] S_IDLE     = 2'd0,   // deselected, ready to join
                     S_ACTIVE   = 2'd1,   // a transaction this slave saw begin
                     S_STRANDED = 2'd2;   // selected at reset release: refusing

    reg [1:0] state;

    // Cycles since reset released, saturating at SYNC_N + 1. This is the only thing
    // that survives reset and carries information about the pin, so it is the whole
    // mechanism. Four bits rather than a computed width: it covers any synchroniser
    // depth anyone will build, and it keeps the module inside Verilog-2001 where a
    // width expression would need $clog2.
    reg [3:0] post_reset;
    wire stale_window = (post_reset <= SYNC_N);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state        <= S_IDLE;
            post_reset   <= 4'd0;
            was_stranded <= 1'b0;
        end else begin
            if (post_reset <= SYNC_N)
                post_reset <= post_reset + 1'b1;

            if (clr_flags)
                was_stranded <= 1'b0;

            case (state)
                S_IDLE: begin
                    if (cs_assert_stb) begin
                        if (stale_window) begin
                            // An assert this early cannot be a new one: the
                            // synchroniser was full of ones at reset release, so the
                            // only way the select can already have propagated is if it
                            // was low before reset ended. Refuse the transaction.
                            state        <= S_STRANDED;
                            was_stranded <= 1'b1;
                        end else begin
                            state <= S_ACTIVE;
                        end
                    end
                end

                S_ACTIVE: begin
                    if (cs_deassert_stb)
                        state <= S_IDLE;
                end

                S_STRANDED: begin
                    // Waiting for the missed transaction to END. Not for a count, not
                    // for a timeout -- for the select to rise, because that is the only
                    // thing in SPI that means "this transaction is over" (Chapter 14.2).
                    if (!cs_active)
                        state <= S_IDLE;
                end

                default: state <= S_IDLE;
            endcase
        end
    end

    assign participating = (state == S_ACTIVE);
    assign stranded      = (state == S_STRANDED);
    assign state_id      = state;

    // THE GATE, and the term that makes it correct.
    //
    // `refuse` includes the stale window directly rather than relying on the state,
    // because the state does not become STRANDED until the cycle AFTER the stale assert
    // arrives -- and on that cycle the state is still IDLE, so a gate written as
    // `state != S_STRANDED` passes through the very assert it is deciding to refuse.
    // The downstream transaction then starts, and the refusal arrives one cycle too
    // late to prevent anything.
    //
    // Gating on the same expression the state machine decides with is what keeps the
    // two consistent within the cycle.
    wire refuse = (state == S_STRANDED) | (stale_window & cs_active);

    assign g_cs_active       = cs_active & ~refuse;
    assign g_cs_assert_stb   = cs_assert_stb & (state == S_IDLE) & ~refuse;
    assign g_cs_deassert_stb = cs_deassert_stb & (state == S_ACTIVE);

`ifdef SPI_CHECKS
    always_ff @(posedge clk) if (rst_n) begin
        if (g_cs_assert_stb && stranded)
            $fatal(1, "an assert was passed through while stranded");
        if (participating && stranded)
            $fatal(1, "participating and stranded at once");
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_safe_idle.v — the same design in Verilog-2001
// spi_slave_safe_idle.v
//
// Chapter 14.8 -- reset lands wherever it lands, and the state the slave must refuse
// to be in.
//
// A master's reset is synchronous with everything it does, because it generates the
// clock. A slave's is not: reset comes from its own system and SCLK comes from
// somebody else's, so reset can and will be released in the MIDDLE of a transaction
// that is already under way.
//
// THE QUESTION, AND THE WRONG ANSWER.
//
// Reset releases. Chip select is already low, SCLK is running, bits are arriving. What
// should the slave do?
//
// The natural answer is "start receiving", and it is wrong. The slave has missed an
// unknown number of bits -- it does not know whether this is bit 3 or bit 300 -- so
// every word it assembles from here is misaligned, and worse, it is misaligned in a way
// nothing can detect: the words are the right length and arrive at the right times
// (Chapter 14.3), so the system receives plausible data from a device that has no idea
// where it is.
//
// And on the transmit side it is worse still. The slave will start driving MISO on the
// next launch edge, in the middle of a word the master is part-way through reading, so
// the master's word is corrupted too -- by a device that just came out of reset and
// has, from its own point of view, done nothing wrong.
//
// THE RIGHT ANSWER IS TO REFUSE.
//
// A slave that comes out of reset with chip select already asserted is STRANDED. It
// must not capture, must not drive, and must not report anything. It waits for chip
// select to go HIGH -- which ends the transaction it missed -- and then rejoins on the
// next assert.
//
// That is the "safe idle" of this chapter's title, and it is worth noticing that the
// safe state is not the reset state: reset puts the slave in a state where it is not
// participating, and the stranded state is a second, distinct state where it is not
// participating BECAUSE IT CANNOT KNOW WHERE IT IS. They look identical on the pins and
// they are reached differently, which is the same argument as the master's IDLE and GAP
// (Chapter 13.3): two states with the same outputs and different input sensitivity.
//
// WHAT RESET MUST DO IMMEDIATELY, AND WHAT IT MUST NOT.
//
//   IMMEDIATELY: release MISO. That is Chapter 14.5's asynchronous reset, and it is
//   required rather than preferred -- a slave held in reset while driving a shared bus
//   makes every other device on that bus unusable.
//
//   NOT AT ALL: resume. There is no partial state worth keeping. A slave that tried to
//   remember where it was would be remembering a position it has no way to validate.
//
// HOW LONG BEING STRANDED LASTS. Exactly one transaction, and that is the whole cost:
// the missed transaction is lost, and the next one is normal. A master that retries --
// which any master with a timeout does -- gets correct behaviour on the retry, which is
// why refusing is cheap and guessing is not.
//
// AND NOW THE HARD PART: HOW THE SLAVE KNOWS.
//
// The obvious test is "at reset release, is chip select already low?" It cannot be
// asked, and the reason is that the slave's own front end is in the same reset. The
// synchroniser chain of Chapter 14.1 resets to the DESELECTED value -- it has to, or a
// slave would wake up believing it was selected -- so at the instant reset releases,
// `cs_active` reads 0 whatever the pin is doing. The information has been destroyed by
// the thing that needs it.
//
// What survives is TIMING. After reset the chain is full of ones, so if the pin is
// already low, `cs_active` rises exactly SYNC_N cycles later and not before. If the pin
// is high and falls afterwards, the assert cannot arrive until at least SYNC_N cycles
// after the fall -- which is strictly later. So:
//
//     an assert observed within SYNC_N cycles of reset release means the select
//     was ALREADY LOW when reset released
//
// and that is exact rather than heuristic: the two cases cannot produce the same
// arrival cycle. A post-reset counter of SYNC_N + 1 states is the whole mechanism.
//
// It is worth noticing what kind of inference this is. The slave recovers a fact about
// a signal it could not observe, from WHEN a derived signal arrived -- which is the same
// trick as measuring an interval in recovered cycles so the latency cancels (Chapters
// 14.4 and 14.5), used to recover a value rather than a duration.
//
// ONE MORE THING RESET DESTROYS. A counter of how many times reset has stranded this
// slave cannot survive the reset that increments it, so it can only ever read zero or
// one. `was_stranded` is therefore a flag and not a count, and a system that wants the
// count has to keep it somewhere that is not in this reset domain -- which is a
// system-level decision and not something this block can fake.

module spi_slave_safe_idle #(
    parameter SYNC_N = 2   // must match the front end's synchroniser depth
) (
    input  wire              clk,
    input  wire              rst_n,

    // --- from the front end of 14.1 ---------------------------------------
    input  wire              cs_active,
    input  wire              cs_assert_stb,
    input  wire              cs_deassert_stb,

    // --- the gated view the rest of the slave sees ------------------------
    output wire              g_cs_active,
    output wire              g_cs_assert_stb,
    output wire              g_cs_deassert_stb,

    // --- status -----------------------------------------------------------
    output wire              participating,
    output wire              stranded,
    // A FLAG, not a count: see the header. A counter of reset events cannot survive
    // the reset that increments it.
    output reg               was_stranded,
    output wire [1:0]        state_id,
    input  wire              clr_flags
);

    localparam [1:0] S_IDLE     = 2'd0,   // deselected, ready to join
                     S_ACTIVE   = 2'd1,   // a transaction this slave saw begin
                     S_STRANDED = 2'd2;   // selected at reset release: refusing

    reg [1:0] state;

    // Cycles since reset released, saturating at SYNC_N + 1. This is the only thing
    // that survives reset and carries information about the pin, so it is the whole
    // mechanism. Four bits rather than a computed width: it covers any synchroniser
    // depth anyone will build, and it keeps the module inside Verilog-2001 where a
    // width expression would need $clog2.
    reg [3:0] post_reset;
    wire stale_window = (post_reset <= SYNC_N);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state        <= S_IDLE;
            post_reset   <= 4'd0;
            was_stranded <= 1'b0;
        end else begin
            if (post_reset <= SYNC_N)
                post_reset <= post_reset + 1'b1;

            if (clr_flags)
                was_stranded <= 1'b0;

            case (state)
                S_IDLE: begin
                    if (cs_assert_stb) begin
                        if (stale_window) begin
                            // An assert this early cannot be a new one: the
                            // synchroniser was full of ones at reset release, so the
                            // only way the select can already have propagated is if it
                            // was low before reset ended. Refuse the transaction.
                            state        <= S_STRANDED;
                            was_stranded <= 1'b1;
                        end else begin
                            state <= S_ACTIVE;
                        end
                    end
                end

                S_ACTIVE: begin
                    if (cs_deassert_stb)
                        state <= S_IDLE;
                end

                S_STRANDED: begin
                    // Waiting for the missed transaction to END. Not for a count, not
                    // for a timeout -- for the select to rise, because that is the only
                    // thing in SPI that means "this transaction is over" (Chapter 14.2).
                    if (!cs_active)
                        state <= S_IDLE;
                end

                default: state <= S_IDLE;
            endcase
        end
    end

    assign participating = (state == S_ACTIVE);
    assign stranded      = (state == S_STRANDED);
    assign state_id      = state;

    // THE GATE, and the term that makes it correct.
    //
    // `refuse` includes the stale window directly rather than relying on the state,
    // because the state does not become STRANDED until the cycle AFTER the stale assert
    // arrives -- and on that cycle the state is still IDLE, so a gate written as
    // `state != S_STRANDED` passes through the very assert it is deciding to refuse.
    // The downstream transaction then starts, and the refusal arrives one cycle too
    // late to prevent anything.
    //
    // Gating on the same expression the state machine decides with is what keeps the
    // two consistent within the cycle.
    wire refuse = (state == S_STRANDED) | (stale_window & cs_active);

    assign g_cs_active       = cs_active & ~refuse;
    assign g_cs_assert_stb   = cs_assert_stb & (state == S_IDLE) & ~refuse;
    assign g_cs_deassert_stb = cs_deassert_stb & (state == S_ACTIVE);

`ifdef SPI_CHECKS
    always @(posedge clk) if (rst_n) begin
        if (g_cs_assert_stb && stranded)
            $fatal(1, "an assert was passed through while stranded");
        if (participating && stranded)
            $fatal(1, "participating and stranded at once");
    end
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_safe_idle.vhd — the same design in VHDL
-- spi_slave_safe_idle.vhd
--
-- Chapter 14.8 -- reset lands wherever it lands, and the state the slave must refuse
-- to be in.
--
-- A master's reset is synchronous with everything it does, because it generates the
-- clock. A slave's is not: reset comes from its own system and SCLK comes from
-- somebody else's, so reset can and will be released in the MIDDLE of a transaction
-- that is already under way.
--
-- THE QUESTION, AND THE WRONG ANSWER.
--
-- Reset releases. Chip select is already low, SCLK is running, bits are arriving. What
-- should the slave do?
--
-- The natural answer is "start receiving", and it is wrong. The slave has missed an
-- unknown number of bits -- it does not know whether this is bit 3 or bit 300 -- so
-- every word it assembles from here is misaligned, and worse, it is misaligned in a way
-- nothing can detect: the words are the right length and arrive at the right times
-- (Chapter 14.3), so the system receives plausible data from a device that has no idea
-- where it is.
--
-- And on the transmit side it is worse still. The slave will start driving MISO on the
-- next launch edge, in the middle of a word the master is part-way through reading, so
-- the master's word is corrupted too -- by a device that just came out of reset and
-- has, from its own point of view, done nothing wrong.
--
-- THE RIGHT ANSWER IS TO REFUSE.
--
-- A slave that comes out of reset with chip select already asserted is STRANDED. It
-- must not capture, must not drive, and must not report anything. It waits for chip
-- select to go HIGH -- which ends the transaction it missed -- and then rejoins on the
-- next assert.
--
-- That is the "safe idle" of this chapter's title, and it is worth noticing that the
-- safe state is not the reset state: reset puts the slave in a state where it is not
-- participating, and the stranded state is a second, distinct state where it is not
-- participating BECAUSE IT CANNOT KNOW WHERE IT IS. They look identical on the pins and
-- they are reached differently, which is the same argument as the master's IDLE and GAP
-- (Chapter 13.3): two states with the same outputs and different input sensitivity.
--
-- WHAT RESET MUST DO IMMEDIATELY, AND WHAT IT MUST NOT.
--
--   IMMEDIATELY: release MISO. That is Chapter 14.5's asynchronous reset, and it is
--   required rather than preferred -- a slave held in reset while driving a shared bus
--   makes every other device on that bus unusable.
--
--   NOT AT ALL: resume. There is no partial state worth keeping. A slave that tried to
--   remember where it was would be remembering a position it has no way to validate.
--
-- HOW LONG BEING STRANDED LASTS. Exactly one transaction, and that is the whole cost:
-- the missed transaction is lost, and the next one is normal. A master that retries --
-- which any master with a timeout does -- gets correct behaviour on the retry, which is
-- why refusing is cheap and guessing is not.
--
-- AND NOW THE HARD PART: HOW THE SLAVE KNOWS.
--
-- The obvious test is "at reset release, is chip select already low?" It cannot be
-- asked, and the reason is that the slave's own front end is in the same reset. The
-- synchroniser chain of Chapter 14.1 resets to the DESELECTED value -- it has to, or a
-- slave would wake up believing it was selected -- so at the instant reset releases,
-- `cs_active` reads 0 whatever the pin is doing. The information has been destroyed by
-- the thing that needs it.
--
-- What survives is TIMING. After reset the chain is full of ones, so if the pin is
-- already low, `cs_active` rises exactly SYNC_N cycles later and not before. If the pin
-- is high and falls afterwards, the assert cannot arrive until at least SYNC_N cycles
-- after the fall -- which is strictly later. So:
--
--     an assert observed within SYNC_N cycles of reset release means the select
--     was ALREADY LOW when reset released
--
-- and that is exact rather than heuristic: the two cases cannot produce the same
-- arrival cycle. A post-reset counter of SYNC_N + 1 states is the whole mechanism.
--
-- It is worth noticing what kind of inference this is. The slave recovers a fact about
-- a signal it could not observe, from WHEN a derived signal arrived -- which is the same
-- trick as measuring an interval in recovered cycles so the latency cancels (Chapters
-- 14.4 and 14.5), used to recover a value rather than a duration.
--
-- ONE MORE THING RESET DESTROYS. A counter of how many times reset has stranded this
-- slave cannot survive the reset that increments it, so it can only ever read zero or
-- one. `was_stranded` is therefore a flag and not a count, and a system that wants the
-- count has to keep it somewhere that is not in this reset domain -- which is a
-- system-level decision and not something this block can fake.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_slave_safe_idle is
    generic (
        SYNC_N : positive := 2   -- must match the front end's synchroniser depth
    );
    port (
        clk               : in  std_logic;
        rst_n             : in  std_logic;

        -- from the front end of 14.1
        cs_active         : in  std_logic;
        cs_assert_stb     : in  std_logic;
        cs_deassert_stb   : in  std_logic;

        -- the gated view the rest of the slave sees
        g_cs_active       : out std_logic;
        g_cs_assert_stb   : out std_logic;
        g_cs_deassert_stb : out std_logic;

        -- status
        participating     : out std_logic;
        stranded          : out std_logic;
        -- A FLAG, not a count: see the header. A counter of reset events cannot survive
        -- the reset that increments it.
        was_stranded      : out std_logic;
        state_id          : out unsigned(1 downto 0);
        clr_flags         : in  std_logic
    );
end entity;

architecture rtl of spi_slave_safe_idle is

    constant S_IDLE     : unsigned(1 downto 0) := "00";  -- deselected, ready to join
    constant S_ACTIVE   : unsigned(1 downto 0) := "01";  -- a transaction this slave saw
    constant S_STRANDED : unsigned(1 downto 0) := "10";  -- selected at reset: refusing

    signal state : unsigned(1 downto 0) := S_IDLE;

    -- Cycles since reset released, saturating at SYNC_N + 1. This is the only thing
    -- that survives reset and carries information about the pin, so it is the whole
    -- mechanism. Four bits covers any synchroniser depth anyone will build.
    signal post_reset   : unsigned(3 downto 0) := (others => '0');
    signal stale_window : std_logic;
    signal refuse       : std_logic;
    signal was_str_r    : std_logic := '0';

begin

    stale_window <= '1' when to_integer(post_reset) <= SYNC_N else '0';

    -- THE GATE, and the term that makes it correct.
    --
    -- `refuse` includes the stale window directly rather than relying on the state,
    -- because the state does not become STRANDED until the cycle AFTER the stale assert
    -- arrives -- and on that cycle the state is still IDLE, so a gate written as
    -- `state /= S_STRANDED` passes through the very assert it is deciding to refuse.
    -- The downstream transaction then starts, and the refusal arrives one cycle too late
    -- to prevent anything.
    refuse <= '1' when (state = S_STRANDED) or
                       (stale_window = '1' and cs_active = '1') else '0';

    g_cs_active       <= cs_active and (not refuse);
    g_cs_assert_stb   <= cs_assert_stb and (not refuse)
                         when state = S_IDLE else '0';
    g_cs_deassert_stb <= cs_deassert_stb when state = S_ACTIVE else '0';

    participating <= '1' when state = S_ACTIVE   else '0';
    stranded      <= '1' when state = S_STRANDED else '0';
    was_stranded  <= was_str_r;
    state_id      <= state;

    fsm : process (clk, rst_n)
    begin
        if rst_n = '0' then
            state      <= S_IDLE;
            post_reset <= (others => '0');
            was_str_r  <= '0';
        elsif rising_edge(clk) then
            if to_integer(post_reset) <= SYNC_N then
                post_reset <= post_reset + 1;
            end if;

            if clr_flags = '1' then
                was_str_r <= '0';
            end if;

            case to_integer(state) is

                when 0 =>                        -- S_IDLE
                    if cs_assert_stb = '1' then
                        if stale_window = '1' then
                            -- An assert this early cannot be a new one: the
                            -- synchroniser was full of ones at reset release, so the
                            -- only way the select can already have propagated is if it
                            -- was low before reset ended. Refuse the transaction.
                            state     <= S_STRANDED;
                            was_str_r <= '1';
                        else
                            state <= S_ACTIVE;
                        end if;
                    end if;

                when 1 =>                        -- S_ACTIVE
                    if cs_deassert_stb = '1' then
                        state <= S_IDLE;
                    end if;

                when 2 =>                        -- S_STRANDED
                    -- Waiting for the missed transaction to END. Not for a count, not
                    -- for a timeout -- for the select to rise, because that is the only
                    -- thing in SPI that means "this transaction is over".
                    if cs_active = '0' then
                        state <= S_IDLE;
                    end if;

                when others =>
                    state <= S_IDLE;

            end case;
        end if;
    end process;

    check : process (clk)
    begin
        if rising_edge(clk) and rst_n = '1' then
            assert not (g_cs_assert_stb = '1' and stranded = '1')
                report "an assert was passed through while stranded" severity failure;
            assert not (participating = '1' and stranded = '1')
                report "participating and stranded at once" severity failure;
        end if;
    end process;

end architecture;

The testbench

Six tests, and the sixth is the one that reveals §5's limitation rather than working around it.

  1. Reset released while deselected. The next transaction is normal — which is the case that must not be broken by the mechanism that handles the other one.
  2. Reset released mid-transaction. The transaction must be refused entirely: no captures pass, no assert strobe reaches the downstream blocks, and was_stranded is set.
  3. Recovery. After the refused transaction's select rises, the next one is fully normal. The transaction after the interesting one, which is this module's recurring discipline.
  4. The stranded state ends on the select rising and not before. Checked by continuing to clock SCLK while stranded and confirming nothing passes.
  5. And it rejoins properly. The transaction after rejoining is normal, including its first word.
  6. Repeated resets. Each one strands exactly one transaction, and the testbench keeps the count — because the design cannot. This test is where §5's argument is made concrete: the bench asserts was_stranded after each reset and maintains its own tally of how many times that happened.
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_safe_idle_tb.sv — six tests; the bench keeps the count the design structurally cannot
// spi_slave_safe_idle_tb.sv
//
// The property under test is a NEGATIVE one -- that a transaction in progress at reset
// release is never reported -- so the receive chain is instantiated behind the gate and
// what is counted is words that should not exist.
//
// That is deliberate. A test that only checked the state machine's state would pass on a
// design whose gate was wired wrong, and the gate is the part that matters: being in the
// stranded state is worth nothing if the bits get through anyway.
//
// The reset is released at every bit position of a transaction, because "mid-frame" is
// not one moment and a design that special-cases the beginning is easy to write.

`timescale 1ns/1ps

module spi_slave_safe_idle_tb;

    localparam int MAX_W = 32;
    localparam int LEN_W = 6;
    localparam int CNT_W = 12;

    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 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)
    );

    // --- 14.8, the block under test -----------------------------------------
    logic            clr_flags = 1'b0;
    wire             g_cs_active, g_cs_assert_stb, g_cs_deassert_stb;
    wire             participating, stranded, was_stranded;
    wire [1:0]       si_state;

    spi_slave_safe_idle #(.SYNC_N(2)) dut (
        .clk(clk), .rst_n(rst_n),
        .cs_active(cs_active), .cs_assert_stb(cs_assert_stb),
        .cs_deassert_stb(cs_deassert_stb),
        .g_cs_active(g_cs_active), .g_cs_assert_stb(g_cs_assert_stb),
        .g_cs_deassert_stb(g_cs_deassert_stb),
        .participating(participating), .stranded(stranded),
        .was_stranded(was_stranded), .state_id(si_state),
        .clr_flags(clr_flags)
    );

    // --- the receive chain, behind the gate ---------------------------------
    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(g_cs_assert_stb), .cs_deassert_stb(g_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(g_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)
    );

    // --- monitors -----------------------------------------------------------
    integer n_words, n_starts, n_ends;
    // Counted HERE rather than in the DUT, because a counter of reset events cannot
    // survive the reset that increments it. The testbench is not in that reset domain,
    // so it can.
    integer n_strands;
    // A second accumulator that `drive_txn` never clears, so a test can count strand
    // events across several transactions.
    integer n_strands_total;
    logic   stranded_q;
    logic [MAX_W-1:0] last_word;

    always_ff @(posedge clk) begin
        if (stranded && !stranded_q) begin
            n_strands       <= n_strands + 1;
            n_strands_total <= n_strands_total + 1;
        end
        stranded_q <= stranded;
        if (rst_n) begin
            if (rx_valid_stb) begin
                n_words   <= n_words + 1;
                last_word <= rx_data;
            end
            if (txn_start_stb) n_starts <= n_starts + 1;
            if (txn_end_stb)   n_ends   <= n_ends + 1;
        end
    end

    integer errors = 0;

    task automatic adv(input integer n);
        begin repeat (n) @(negedge clk); end
    endtask

    task automatic clear_counts;
        begin n_words = 0; n_starts = 0; n_ends = 0; end
    endtask

    // Drives one whole transaction of `nwords` eight-bit words with a pattern whose
    // first word is 0xA5. `reset_at` is the bit index at which reset is pulsed, or -1
    // for no reset.
    task automatic drive_txn(input integer nwords, input integer half,
                             input integer reset_at);
        integer i, b;
        begin
            cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1;
            adv(10);
            cs_n_pin = 1'b0;
            adv(4);
            for (i = 0; i < nwords * 8; i = i + 1) begin
                if (i == reset_at) begin
                    // Reset pulsed in the MIDDLE of the transaction, asynchronously to
                    // everything the master is doing -- and the counters are cleared
                    // immediately afterwards, because words that COMPLETED before the
                    // reset were genuinely delivered and are not evidence of anything.
                    // Counting them makes a late reset look like a failure to refuse.
                    rst_n = 1'b0;
                    adv(3);
                    rst_n = 1'b1;
                    clear_counts();
                    n_strands = 0;
                    adv(2);
                end
                mosi_pin = (8'hA5 >> (7 - (i % 8))) & 1'b1;
                adv(2);
                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

    integer k, bad;

    initial begin
        clear_counts();
        last_word = 0; n_strands = 0; n_strands_total = 0; stranded_q = 1'b0;

        adv(3);
        rst_n = 1'b1;
        adv(2);

        // 1. RESET RELEASED WHILE DESELECTED. The next transaction is normal -- which
        //    is the baseline the refusal must not break.
        clear_counts();
        drive_txn(2, 4, -1);
        if (n_words != 2 || n_starts != 1) begin
            $display("  FAIL: a normal transaction gave %0d words and %0d starts",
                     n_words, n_starts);
            errors = errors + 1;
        end
        if (n_strands != 0) begin
            $display("  FAIL: a normal transaction was counted as stranded");
            errors = errors + 1;
        end
        $display("  reset released while deselected: the next transaction delivers both words normally and nothing is stranded");

        // 2. RESET RELEASED MID-TRANSACTION. The transaction must be refused
        //    ENTIRELY -- no start, no words, no end -- and counted as stranded.
        for (k = 1; k <= 15; k = k + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            clear_counts();
            n_strands = 0;
            drive_txn(2, 4, k);
            if (n_starts != 0) begin
                $display("  FAIL: reset at bit %0d still reported %0d transaction starts",
                         k, n_starts);
                errors = errors + 1;
            end
            if (n_words != 0) begin
                $display("  FAIL: reset at bit %0d still delivered %0d words",
                         k, n_words);
                errors = errors + 1;
            end
            if (n_ends != 0) begin
                $display("  FAIL: reset at bit %0d still reported %0d transaction ends",
                         k, n_ends);
                errors = errors + 1;
            end
            if (n_strands != 1) begin
                $display("  FAIL: reset at bit %0d stranded %0d transactions",
                         k, n_strands);
                errors = errors + 1;
            end
            if (!was_stranded) begin
                $display("  FAIL: reset at bit %0d did not latch the stranded flag", k);
                errors = errors + 1;
            end
        end
        $display("  reset released at each of fifteen bit positions: the transaction in progress is refused entirely every time -- no start, no word, no end -- and counted exactly once");

        // 3. RECOVERY. After the refused transaction's select rises, the next one is
        //    completely normal. One transaction lost, and only one.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        clear_counts();
        drive_txn(2, 4, 5);          // refused
        drive_txn(2, 4, -1);         // and this one must be perfect
        if (n_starts != 1 || n_words != 2) begin
            $display("  FAIL: after a refused transaction the next gave %0d starts and %0d words",
                     n_starts, n_words);
            errors = errors + 1;
        end
        if (last_word[7:0] !== 8'hA5) begin
            $display("  FAIL: the recovered word was %02h, expected a5",
                     last_word[7:0]);
            errors = errors + 1;
        end
        $display("  the transaction after a refused one is completely normal: one start, both words, and the last word reads a5 -- exactly one transaction is lost");

        // 4. THE STRANDED STATE ENDS ON THE SELECT RISING AND NOT BEFORE. Checked by
        //    keeping the select low for a long time after reset and confirming the
        //    slave stays out.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        clear_counts();
        cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1; adv(10);
        cs_n_pin = 1'b0; adv(6);
        rst_n = 1'b0; adv(3); rst_n = 1'b1;
        // Cleared after the reset for the same reason as inside `drive_txn`: the start
        // that was counted before the reset was a legitimate one.
        clear_counts();
        adv(4);
        if (!stranded) begin
            $display("  FAIL: reset released with the select low did not strand the slave");
            errors = errors + 1;
        end
        // A long run of clocks: none of it may be seen.
        for (k = 0; k < 40; k = k + 1) begin
            sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(4);
        end
        if (!stranded) begin
            $display("  FAIL: the slave rejoined while the select was still low");
            errors = errors + 1;
        end
        if (n_words != 0 || n_starts != 0) begin
            $display("  FAIL: %0d words and %0d starts got through while stranded",
                     n_words, n_starts);
            errors = errors + 1;
        end
        cs_n_pin = 1'b1; adv(8);
        if (stranded) begin
            $display("  FAIL: the slave stayed stranded after the select rose");
            errors = errors + 1;
        end
        adv(8);
        $display("  stranded, the slave ignored forty clock periods and rejoined only when the select rose -- not on a count and not on a timeout");

        // 5. AND IT REJOINS PROPERLY. The transaction after rejoining is normal.
        clear_counts();
        drive_txn(3, 4, -1);
        if (n_starts != 1 || n_words != 3) begin
            $display("  FAIL: after rejoining, a three-word transaction gave %0d starts and %0d words",
                     n_starts, n_words);
            errors = errors + 1;
        end
        $display("  after rejoining, a three-word transaction delivers three words and one start");

        // 6. REPEATED RESETS. Each one strands exactly one transaction, and the count
        //    accumulates rather than sticking at one.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        n_strands_total = 0;
        for (k = 0; k < 4; k = k + 1)
            drive_txn(2, 4, 3 + k);
        if (n_strands_total != 4) begin
            $display("  FAIL: four mid-transaction resets stranded %0d transactions",
                     n_strands_total);
            errors = errors + 1;
        end
        $display("  four consecutive mid-transaction resets strand four transactions, so the count accumulates rather than sticking");

        if (errors == 0)
            $display("PASS: a slave whose reset is released while chip select is already asserted has missed an unknown number of bits, so it refuses the transaction entirely rather than guessing where it is -- verified at each of fifteen bit positions, with no transaction start, no word and no transaction end getting through in any of them, and each refusal counted exactly once -- the refusal ends on the select RISING and not on a count or a timeout, so forty clock periods with the select still low are ignored, and the transaction after a refused one is completely normal with the right word, meaning exactly one transaction is lost and a master that retries gets correct behaviour -- and four consecutive mid-transaction resets strand four transactions, counted outside the reset domain because a counter of reset events cannot survive the reset that increments it -- and the whole inference rests on TIMING rather than on a level, because the slave's own synchroniser resets to deselected and destroys the fact it needs, leaving only the arrival cycle of the assert to recover it from");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_safe_idle_tb.v — the same bench in Verilog-2001
// spi_slave_safe_idle_tb.v
//
// The property under test is a NEGATIVE one -- that a transaction in progress at reset
// release is never reported -- so the receive chain is instantiated behind the gate and
// what is counted is words that should not exist.
//
// That is deliberate. A test that only checked the state machine's state would pass on a
// design whose gate was wired wrong, and the gate is the part that matters: being in the
// stranded state is worth nothing if the bits get through anyway.
//
// The reset is released at every bit position of a transaction, because "mid-frame" is
// not one moment and a design that special-cases the beginning is easy to write.

`timescale 1ns/1ps

module spi_slave_safe_idle_tb;

    localparam MAX_W = 32;
    localparam LEN_W = 6;
    localparam CNT_W = 12;

    reg clk;
    reg rst_n;
    always #5 clk = ~clk;

    reg cpol;
    reg cpha;
    reg lsb_first;

    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)
    );

    // --- 14.8, the block under test -----------------------------------------
    reg            clr_flags;
    wire             g_cs_active, g_cs_assert_stb, g_cs_deassert_stb;
    wire             participating, stranded, was_stranded;
    wire [1:0]       si_state;

    spi_slave_safe_idle #(.SYNC_N(2)) dut (
        .clk(clk), .rst_n(rst_n),
        .cs_active(cs_active), .cs_assert_stb(cs_assert_stb),
        .cs_deassert_stb(cs_deassert_stb),
        .g_cs_active(g_cs_active), .g_cs_assert_stb(g_cs_assert_stb),
        .g_cs_deassert_stb(g_cs_deassert_stb),
        .participating(participating), .stranded(stranded),
        .was_stranded(was_stranded), .state_id(si_state),
        .clr_flags(clr_flags)
    );

    // --- the receive chain, behind the gate ---------------------------------
    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(g_cs_assert_stb), .cs_deassert_stb(g_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(g_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)
    );

    // --- monitors -----------------------------------------------------------
    integer n_words, n_starts, n_ends;
    // Counted HERE rather than in the DUT, because a counter of reset events cannot
    // survive the reset that increments it. The testbench is not in that reset domain,
    // so it can.
    integer n_strands;
    // A second accumulator that `drive_txn` never clears, so a test can count strand
    // events across several transactions.
    integer n_strands_total;
    reg   stranded_q;
    reg [MAX_W-1:0] last_word;

    always @(posedge clk) begin
        if (stranded && !stranded_q) begin
            n_strands       <= n_strands + 1;
            n_strands_total <= n_strands_total + 1;
        end
        stranded_q <= stranded;
        if (rst_n) begin
            if (rx_valid_stb) begin
                n_words   <= n_words + 1;
                last_word <= rx_data;
            end
            if (txn_start_stb) n_starts <= n_starts + 1;
            if (txn_end_stb)   n_ends   <= n_ends + 1;
        end
    end

    integer errors;

        task adv;
        input integer n;
        begin repeat (n) @(negedge clk); end
    endtask

    task clear_counts;
        begin n_words = 0; n_starts = 0; n_ends = 0; end
    endtask

    // Drives one whole transaction of `nwords` eight-bit words with a pattern whose
    // first word is 0xA5. `reset_at` is the bit index at which reset is pulsed, or -1
    // for no reset.
        task drive_txn;
        input integer nwords;
        input integer half;
        input integer reset_at;
        integer i, b;
        begin
            cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1;
            adv(10);
            cs_n_pin = 1'b0;
            adv(4);
            for (i = 0; i < nwords * 8; i = i + 1) begin
                if (i == reset_at) begin
                    // Reset pulsed in the MIDDLE of the transaction, asynchronously to
                    // everything the master is doing -- and the counters are cleared
                    // immediately afterwards, because words that COMPLETED before the
                    // reset were genuinely delivered and are not evidence of anything.
                    // Counting them makes a late reset look like a failure to refuse.
                    rst_n = 1'b0;
                    adv(3);
                    rst_n = 1'b1;
                    clear_counts();
                    n_strands = 0;
                    adv(2);
                end
                mosi_pin = (8'hA5 >> (7 - (i % 8))) & 1'b1;
                adv(2);
                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

    integer k, bad;

    initial begin
        clear_counts();
        last_word = 0; n_strands = 0; n_strands_total = 0; stranded_q = 1'b0;

        adv(3);
        rst_n = 1'b1;
        adv(2);

        // 1. RESET RELEASED WHILE DESELECTED. The next transaction is normal -- which
        //    is the baseline the refusal must not break.
        clear_counts();
        drive_txn(2, 4, -1);
        if (n_words != 2 || n_starts != 1) begin
            $display("  FAIL: a normal transaction gave %0d words and %0d starts",
                     n_words, n_starts);
            errors = errors + 1;
        end
        if (n_strands != 0) begin
            $display("  FAIL: a normal transaction was counted as stranded");
            errors = errors + 1;
        end
        $display("  reset released while deselected: the next transaction delivers both words normally and nothing is stranded");

        // 2. RESET RELEASED MID-TRANSACTION. The transaction must be refused
        //    ENTIRELY -- no start, no words, no end -- and counted as stranded.
        for (k = 1; k <= 15; k = k + 1) begin
            clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
            clear_counts();
            n_strands = 0;
            drive_txn(2, 4, k);
            if (n_starts != 0) begin
                $display("  FAIL: reset at bit %0d still reported %0d transaction starts",
                         k, n_starts);
                errors = errors + 1;
            end
            if (n_words != 0) begin
                $display("  FAIL: reset at bit %0d still delivered %0d words",
                         k, n_words);
                errors = errors + 1;
            end
            if (n_ends != 0) begin
                $display("  FAIL: reset at bit %0d still reported %0d transaction ends",
                         k, n_ends);
                errors = errors + 1;
            end
            if (n_strands != 1) begin
                $display("  FAIL: reset at bit %0d stranded %0d transactions",
                         k, n_strands);
                errors = errors + 1;
            end
            if (!was_stranded) begin
                $display("  FAIL: reset at bit %0d did not latch the stranded flag", k);
                errors = errors + 1;
            end
        end
        $display("  reset released at each of fifteen bit positions: the transaction in progress is refused entirely every time -- no start, no word, no end -- and counted exactly once");

        // 3. RECOVERY. After the refused transaction's select rises, the next one is
        //    completely normal. One transaction lost, and only one.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        clear_counts();
        drive_txn(2, 4, 5);          // refused
        drive_txn(2, 4, -1);         // and this one must be perfect
        if (n_starts != 1 || n_words != 2) begin
            $display("  FAIL: after a refused transaction the next gave %0d starts and %0d words",
                     n_starts, n_words);
            errors = errors + 1;
        end
        if (last_word[7:0] !== 8'hA5) begin
            $display("  FAIL: the recovered word was %02h, expected a5",
                     last_word[7:0]);
            errors = errors + 1;
        end
        $display("  the transaction after a refused one is completely normal: one start, both words, and the last word reads a5 -- exactly one transaction is lost");

        // 4. THE STRANDED STATE ENDS ON THE SELECT RISING AND NOT BEFORE. Checked by
        //    keeping the select low for a long time after reset and confirming the
        //    slave stays out.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        clear_counts();
        cpol = 1'b0; sclk_pin = 1'b0; cs_n_pin = 1'b1; adv(10);
        cs_n_pin = 1'b0; adv(6);
        rst_n = 1'b0; adv(3); rst_n = 1'b1;
        // Cleared after the reset for the same reason as inside `drive_txn`: the start
        // that was counted before the reset was a legitimate one.
        clear_counts();
        adv(4);
        if (!stranded) begin
            $display("  FAIL: reset released with the select low did not strand the slave");
            errors = errors + 1;
        end
        // A long run of clocks: none of it may be seen.
        for (k = 0; k < 40; k = k + 1) begin
            sclk_pin = 1'b1; adv(4); sclk_pin = 1'b0; adv(4);
        end
        if (!stranded) begin
            $display("  FAIL: the slave rejoined while the select was still low");
            errors = errors + 1;
        end
        if (n_words != 0 || n_starts != 0) begin
            $display("  FAIL: %0d words and %0d starts got through while stranded",
                     n_words, n_starts);
            errors = errors + 1;
        end
        cs_n_pin = 1'b1; adv(8);
        if (stranded) begin
            $display("  FAIL: the slave stayed stranded after the select rose");
            errors = errors + 1;
        end
        adv(8);
        $display("  stranded, the slave ignored forty clock periods and rejoined only when the select rose -- not on a count and not on a timeout");

        // 5. AND IT REJOINS PROPERLY. The transaction after rejoining is normal.
        clear_counts();
        drive_txn(3, 4, -1);
        if (n_starts != 1 || n_words != 3) begin
            $display("  FAIL: after rejoining, a three-word transaction gave %0d starts and %0d words",
                     n_starts, n_words);
            errors = errors + 1;
        end
        $display("  after rejoining, a three-word transaction delivers three words and one start");

        // 6. REPEATED RESETS. Each one strands exactly one transaction, and the count
        //    accumulates rather than sticking at one.
        clr_flags = 1'b1; adv(1); clr_flags = 1'b0;
        n_strands_total = 0;
        for (k = 0; k < 4; k = k + 1)
            drive_txn(2, 4, 3 + k);
        if (n_strands_total != 4) begin
            $display("  FAIL: four mid-transaction resets stranded %0d transactions",
                     n_strands_total);
            errors = errors + 1;
        end
        $display("  four consecutive mid-transaction resets strand four transactions, so the count accumulates rather than sticking");

        if (errors == 0)
            $display("PASS: a slave whose reset is released while chip select is already asserted has missed an unknown number of bits, so it refuses the transaction entirely rather than guessing where it is -- verified at each of fifteen bit positions, with no transaction start, no word and no transaction end getting through in any of them, and each refusal counted exactly once -- the refusal ends on the select RISING and not on a count or a timeout, so forty clock periods with the select still low are ignored, and the transaction after a refused one is completely normal with the right word, meaning exactly one transaction is lost and a master that retries gets correct behaviour -- and four consecutive mid-transaction resets strand four transactions, counted outside the reset domain because a counter of reset events cannot survive the reset that increments it -- and the whole inference rests on TIMING rather than on a level, because the slave's own synchroniser resets to deselected and destroys the fact it needs, leaving only the arrival cycle of the assert to recover it from");
        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;
        sclk_pin = 1'b0;
        cs_n_pin = 1'b1;
        mosi_pin = 1'b0;
        clr_flags = 1'b0;
        len = 6'd8;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_slave_safe_idle_tb.vhd — the same bench in VHDL
-- spi_slave_safe_idle_tb.vhd
--
-- The property under test is a NEGATIVE one -- that a transaction in progress at reset
-- release is never reported -- so the receive chain is instantiated behind the gate and
-- what is counted is words that should not exist.
--
-- That is deliberate. A test that only checked the state machine's state would pass on a
-- design whose gate was wired wrong, and the gate is the part that matters: being in the
-- stranded state is worth nothing if the bits get through anyway.
--
-- The reset is released at every bit position of a transaction, because "mid-frame" is
-- not one moment and a design that special-cases the beginning is easy to write.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_slave_safe_idle_tb is
end entity;

architecture sim of spi_slave_safe_idle_tb is

    constant MAX_W : positive := 32;
    constant LEN_W : positive := 6;
    constant CNT_W : positive := 12;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal halt  : boolean   := false;

    signal cpol      : std_logic := '0';
    signal 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 g_cs_active, g_cs_assert_stb, g_cs_deassert_stb : std_logic;
    signal participating, stranded, was_stranded : std_logic;
    signal si_state : unsigned(1 downto 0);

    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 n_words, n_starts, n_ends : natural := 0;
    -- Counted HERE rather than in the DUT, because a counter of reset events cannot
    -- survive the reset that increments it. The testbench is not in that reset domain.
    signal n_strands, n_strands_total : natural := 0;
    signal stranded_q : std_logic := '0';
    signal last_word  : std_logic_vector(MAX_W - 1 downto 0) := (others => '0');
    signal clr_cnt, clr_str, clr_tot : 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

    clk <= not clk after 5 ns when not halt else '0';

    u_fe : entity work.spi_slave_frontend
        generic map (SYNC_N => 2, HALF_MIN => 3, CNT_W => 12)
        port map (clk => clk, rst_n => rst_n, cpol => cpol,
                  sclk_pin => sclk_pin, cs_n_pin => cs_n_pin,
                  mosi_pin => mosi_pin,
                  sclk_q => sclk_q, cs_active => cs_active, mosi_q => mosi_q,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  cs_assert_stb => cs_assert_stb,
                  cs_deassert_stb => cs_deassert_stb,
                  min_half => min_half, ratio_err => ratio_err, clr_flags => '0');

    dut : entity work.spi_slave_safe_idle
        generic map (SYNC_N => 2)
        port map (clk => clk, rst_n => rst_n,
                  cs_active => cs_active, cs_assert_stb => cs_assert_stb,
                  cs_deassert_stb => cs_deassert_stb,
                  g_cs_active => g_cs_active, g_cs_assert_stb => g_cs_assert_stb,
                  g_cs_deassert_stb => g_cs_deassert_stb,
                  participating => participating, stranded => stranded,
                  was_stranded => was_stranded, state_id => si_state,
                  clr_flags => clr_flags);

    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 => g_cs_assert_stb,
                  cs_deassert_stb => g_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 => g_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);

    monitor : process (clk)
    begin
        if rising_edge(clk) then
            if stranded = '1' and stranded_q = '0' then
                if clr_str = '0' then n_strands <= n_strands + 1; end if;
                if clr_tot = '0' then
                    n_strands_total <= n_strands_total + 1;
                end if;
            elsif clr_str = '1' then
                n_strands <= 0;
            end if;
            if clr_tot = '1' then
                n_strands_total <= 0;
            end if;
            stranded_q <= stranded;

            if clr_cnt = '1' then
                n_words <= 0; n_starts <= 0; n_ends <= 0;
            elsif rst_n = '1' then
                if rx_valid_stb = '1' then
                    n_words   <= n_words + 1;
                    last_word <= rx_data;
                end if;
                if txn_start_stb = '1' then n_starts <= n_starts + 1; end if;
                if txn_end_stb   = '1' then n_ends   <= n_ends + 1;   end if;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;

        procedure adv(n : natural) is
        begin
            for j in 1 to n loop wait until falling_edge(clk); end loop;
        end procedure;

        procedure clear_counts is
        begin
            clr_cnt <= '1'; clr_str <= '1';
            wait until falling_edge(clk);
            clr_cnt <= '0'; clr_str <= '0';
        end procedure;

        procedure clear_total is
        begin
            clr_tot <= '1';
            wait until falling_edge(clk);
            clr_tot <= '0';
        end procedure;

        -- Drives one whole transaction of `nwords` eight-bit words of 0xA5.
        -- `reset_at` is the bit index at which reset is pulsed, or a value beyond the
        -- transaction for no reset.
        procedure drive_txn(nwords : natural; half : natural;
                            reset_at : integer) is
            variable b : std_logic;
        begin
            cpol <= '0'; sclk_pin <= '0'; cs_n_pin <= '1';
            adv(10);
            cs_n_pin <= '0';
            adv(4);
            for i in 0 to nwords * 8 - 1 loop
                if i = reset_at then
                    -- Reset pulsed in the MIDDLE of the transaction, asynchronously to
                    -- everything the master is doing -- and the counters are cleared
                    -- immediately afterwards, because words that COMPLETED before the
                    -- reset were genuinely delivered and are not evidence of anything.
                    rst_n <= '0';
                    adv(3);
                    rst_n <= '1';
                    clear_counts;
                    adv(2);
                end if;
                if ((16#A5# / (2 ** (7 - (i mod 8)))) mod 2) = 1 then
                    b := '1';
                else
                    b := '0';
                end if;
                mosi_pin <= b;
                adv(2);
                sclk_pin <= '1';
                adv(half);
                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;
    begin
        adv(3);
        rst_n <= '1';
        adv(2);

        -- 1. RESET RELEASED WHILE DESELECTED.
        clear_counts;
        drive_txn(2, 4, 99);
        if n_words /= 2 or n_starts /= 1 then
            report "  FAIL: a normal transaction gave " & integer'image(n_words) &
                   " words and " & integer'image(n_starts) & " starts";
            errs := errs + 1;
        end if;
        if n_strands /= 0 then
            report "  FAIL: a normal transaction was counted as stranded";
            errs := errs + 1;
        end if;
        report "  reset released while deselected: the next transaction delivers both words normally and nothing is stranded";

        -- 2. RESET RELEASED MID-TRANSACTION.
        for k in 1 to 15 loop
            clr_flags <= '1'; adv(1); clr_flags <= '0';
            clear_counts;
            drive_txn(2, 4, k);
            if n_starts /= 0 then
                report "  FAIL: reset at bit " & integer'image(k) &
                       " still reported " & integer'image(n_starts) & " starts";
                errs := errs + 1;
            end if;
            if n_words /= 0 then
                report "  FAIL: reset at bit " & integer'image(k) &
                       " still delivered " & integer'image(n_words) & " words";
                errs := errs + 1;
            end if;
            if n_ends /= 0 then
                report "  FAIL: reset at bit " & integer'image(k) &
                       " still reported " & integer'image(n_ends) & " ends";
                errs := errs + 1;
            end if;
            if n_strands /= 1 then
                report "  FAIL: reset at bit " & integer'image(k) &
                       " stranded " & integer'image(n_strands) & " transactions";
                errs := errs + 1;
            end if;
            if was_stranded /= '1' then
                report "  FAIL: reset at bit " & integer'image(k) &
                       " did not latch the stranded flag";
                errs := errs + 1;
            end if;
        end loop;
        report "  reset released at each of fifteen bit positions: the transaction in progress is refused entirely every time -- no start, no word, no end -- and counted exactly once";

        -- 3. RECOVERY.
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        clear_counts;
        drive_txn(2, 4, 5);
        drive_txn(2, 4, 99);
        if n_starts /= 1 or n_words /= 2 then
            report "  FAIL: after a refused transaction the next gave " &
                   integer'image(n_starts) & " starts and " &
                   integer'image(n_words) & " words";
            errs := errs + 1;
        end if;
        if last_word(7 downto 0) /= x"A5" then
            report "  FAIL: the recovered word was " &
                   hex2(last_word(7 downto 0)) & ", expected a5";
            errs := errs + 1;
        end if;
        report "  the transaction after a refused one is completely normal: one start, both words, and the last word reads a5 -- exactly one transaction is lost";

        -- 4. THE STRANDED STATE ENDS ON THE SELECT RISING AND NOT BEFORE.
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        clear_counts;
        cpol <= '0'; sclk_pin <= '0'; cs_n_pin <= '1'; adv(10);
        cs_n_pin <= '0'; adv(6);
        rst_n <= '0'; adv(3); rst_n <= '1';
        clear_counts;
        adv(4);
        if stranded /= '1' then
            report "  FAIL: reset released with the select low did not strand the slave";
            errs := errs + 1;
        end if;
        for k in 0 to 39 loop
            sclk_pin <= '1'; adv(4); sclk_pin <= '0'; adv(4);
        end loop;
        if stranded /= '1' then
            report "  FAIL: the slave rejoined while the select was still low";
            errs := errs + 1;
        end if;
        if n_words /= 0 or n_starts /= 0 then
            report "  FAIL: " & integer'image(n_words) & " words and " &
                   integer'image(n_starts) & " starts got through while stranded";
            errs := errs + 1;
        end if;
        cs_n_pin <= '1'; adv(8);
        if stranded = '1' then
            report "  FAIL: the slave stayed stranded after the select rose";
            errs := errs + 1;
        end if;
        adv(8);
        report "  stranded, the slave ignored forty clock periods and rejoined only when the select rose -- not on a count and not on a timeout";

        -- 5. AND IT REJOINS PROPERLY.
        clear_counts;
        drive_txn(3, 4, 99);
        if n_starts /= 1 or n_words /= 3 then
            report "  FAIL: after rejoining, a three-word transaction gave " &
                   integer'image(n_starts) & " starts and " &
                   integer'image(n_words) & " words";
            errs := errs + 1;
        end if;
        report "  after rejoining, a three-word transaction delivers three words and one start";

        -- 6. REPEATED RESETS.
        clr_flags <= '1'; adv(1); clr_flags <= '0';
        clear_total;
        for k in 0 to 3 loop
            drive_txn(2, 4, 3 + k);
        end loop;
        if n_strands_total /= 4 then
            report "  FAIL: four mid-transaction resets stranded " &
                   integer'image(n_strands_total) & " transactions";
            errs := errs + 1;
        end if;
        report "  four consecutive mid-transaction resets strand four transactions, so the count accumulates rather than sticking";

        errors <= errs;
        if errs = 0 then
            report "PASS: a slave whose reset is released while chip select is already asserted has missed an unknown number of bits, so it refuses the transaction entirely rather than guessing where it is -- verified at each of fifteen bit positions, with no transaction start, no word and no transaction end getting through in any of them, and each refusal counted exactly once -- the refusal ends on the select RISING and not on a count or a timeout, so forty clock periods with the select still low are ignored, and the transaction after a refused one is completely normal with the right word, meaning exactly one transaction is lost and a master that retries gets correct behaviour -- and four consecutive mid-transaction resets strand four transactions, counted outside the reset domain because a counter of reset events cannot survive the reset that increments it -- and the whole inference rests on TIMING rather than on a level, because the slave's own synchroniser resets to deselected and destroys the fact it needs, leaving only the arrival cycle of the assert to recover it from";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

8. Why a Verification Engineer Cares

Reset release timing has to be a randomised stimulus axis, not a fixed setup step. Almost every testbench releases reset at a comfortable moment and never again. Here the phase of reset release relative to the transaction is the whole subject, so it needs to be swept: before the assert, inside the stale window, mid-word, and between transactions.

Test 6's structure is the honest way to verify something the design cannot report. The bench keeps the count that the design cannot keep, and asserts the flag each time. A bench that instead checked a design-side counter would be checking a value that is structurally incapable of exceeding one — and would pass whatever it read.

The stranded state needs a negative check over a duration, not at an instant. "Nothing passes while stranded" is a property over the whole stranded interval, and the interesting stimulus is SCLK continuing to run during it. A bench that stops clocking when it strands the slave has tested nothing.

The gate bug of §4 is invisible to a test that only checks the end result. The downstream blocks get one spurious assert and then nothing, so the received data is empty either way. The check that finds it is on the gated strobe itself: g_cs_assert_stb must never fire during a refused transaction, including on its first cycle.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Properties for the safe-idle gate.

property p_nothing_passes_while_stranded;
    // The core obligation, over a duration rather than at an instant.
    @(posedge clk) disable iff (!rst_n)
        stranded |-> (!g_cs_active && !g_cs_assert_stb && !g_cs_deassert_stb);
endproperty

property p_stranding_assert_never_passes;
    // Section 4's bug, stated directly: the assert that CAUSES the stranding is
    // itself refused. A gate derived from the state alone passes this one.
    @(posedge clk) disable iff (!rst_n)
        (cs_assert_stb && stale_window) |-> !g_cs_assert_stb;
endproperty

property p_stranded_only_from_early_assert;
    // And the converse: the slave never strands itself on a legitimate assert.
    // A design that stranded on every assert would be safe and useless.
    @(posedge clk) disable iff (!rst_n)
        $rose(stranded) |-> $past(stale_window);
endproperty

property p_stranded_exits_only_on_deselect;
    // The only exit. Not a timeout, not a count of edges -- the select rising,
    // because that is the only event that ends the transaction that was missed.
    @(posedge clk) disable iff (!rst_n)
        stranded |=> (stranded || $past(cs_deassert_stb) || $past(!cs_active));
endproperty

property p_flag_sticky;
    // was_stranded latches. It is a flag rather than a count for the reason in
    // section 5, and a flag that cleared itself would be unreadable.
    @(posedge clk) disable iff (!rst_n)
        was_stranded && !clr_flags |=> was_stranded;
endproperty

property p_normal_assert_passes_intact;
    // The mechanism must not cost anything in the normal case. Outside the
    // window, the gate is transparent.
    @(posedge clk) disable iff (!rst_n)
        (cs_assert_stb && !stale_window && !stranded) |-> g_cs_assert_stb;
endproperty
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Coverage. The axis is the PHASE OF RESET RELEASE relative to the transaction,
// which in most testbenches is a constant and here is the subject.

covergroup cg_safe_idle @(posedge clk iff $rose(rst_n));
    option.per_instance = 1;

    // Where reset landed. "mid_word" and "between_words" are distinguished
    // because the slave's ignorance is the same in both and it is worth
    // confirming the design does not accidentally treat them differently.
    phase: coverpoint reset_release_phase {
        bins deselected     = {0};   // no transaction in flight
        bins just_before    = {1};   // select falls within SYNC_N cycles after
        bins mid_word       = {2};
        bins between_words  = {3};
        bins last_bit       = {4};   // released on the final capture
    }

    // The gap between reset release and the observed assert, in cycles. This is
    // the mechanism's input, so its boundary must be covered on both sides.
    arrival: coverpoint cycles_release_to_assert {
        bins at_0     = {0};
        bins in_win   = {[1:2]};     // <= SYNC_N: stranded
        bins boundary = {3};         // SYNC_N + 1: NOT stranded
        bins after    = {[4:$]};
    }

    // Whether SCLK was running during the stranded interval, because a bench
    // that stops clocking has not tested the refusal.
    clocking: coverpoint sclk_ran_while_stranded {
        bins quiet   = {0};
        bins running = {1};
    }

    x_phase_arrival:  cross phase, arrival;
    x_phase_clocking: cross phase, clocking;

endgroup

9. Why an FPGA or ASIC Engineer Cares

Reset must be released synchronously even though it is asserted asynchronously. This block's entire mechanism counts cycles from reset release, which requires reset release to be a clean edge on the system clock. A reset that releases asynchronously can release on different flops on different cycles, and then post_reset starts counting from a different moment than the synchroniser chain does — which breaks the exactness of §3's test. The standard reset synchroniser (asynchronous assert, synchronous release) is what makes this block's argument valid, and it belongs at the top of the design rather than here.

post_reset is four bits and saturating, and both choices are deliberate. Four bits covers SYNC_N up to 14, which is more than anyone will build, and it avoids a $clog2 that would put the module outside Verilog-2001. Saturating rather than wrapping matters because a wrap would re-enter the stale window a few cycles later and strand a legitimate transaction.

The three gated outputs are combinational from refuse, so they add one gate of delay to signals that were already registered. That is worth noting only because the alternative — registering the gated versions — would add a cycle to the transaction boundary and change Chapter 14.4's lead requirement from SYNC_N + 2 to SYNC_N + 3. A structural decision in this block would have moved a number in a datasheet.

SYNC_N must match the front end's. It is the same parameter, and passing a different value here silently changes the stale window — too small and the slave joins a transaction it missed, too large and it refuses legitimate ones. There is no assertion that can check the match inside this block; it is a top-level wiring property, and Chapter 14.10 passes one parameter to both.

10. Failure Signature — A Device That Returns Garbage After Every Warm Reset

The symptom:

"After a firmware reset the first transfer returns plausible but wrong data, and so does the next one, until we power-cycle. Cold boot is always fine."

What is happening: reset released mid-transaction, the slave joined in, and it is misaligned by an unknown number of bits. Because Chapter 14.3's counter takes its zero from the chip-select assert, the misalignment should clear at the next assert — so a single wrong transaction would be the expected symptom. Two or more suggests the slave also corrupted the master's read on the transaction it joined, and the master's driver is now out of step at a higher layer.

Why cold boot is fine: at cold boot nothing is driving SCLK, so reset releases while deselected and the slave starts from a known position. Warm reset is the only case where a transaction can be in flight, which is why this bug survives every bench that resets at time zero.

How to confirm: read was_stranded. On a design with this block it is set, the transaction was refused, and the garbage has a different cause. On a design without it, the flag does not exist — and the diagnostic is to assert reset deliberately in the middle of a transfer and see whether the following transfer is clean. If it is not, the slave joined in.

11. Common Misconceptions

"Reset puts the slave in a known state, so it can start receiving." It puts the slave in a known internal state and tells it nothing about the bus. The bus may be mid-transaction, and the slave has no way to find out how far in.

"The slave can check whether chip select is low at reset release." It cannot. Its own synchroniser chain resets to deselected — necessarily, or a slave would wake up believing it was selected — so the level reads 0 whatever the pin is doing. The information is destroyed by the reset that needs it.

"Recovering the fact from timing is a heuristic." It is exact. An assert inside the stale window can only be produced by a select that was already low, because a select that falls after reset release cannot produce an assert until SYNC_N cycles after the fall, which is strictly later. The two cases cannot share an arrival cycle.

"The stranded state is just the reset state." They have identical outputs and different input sensitivity, which makes them two states. Reset is left by a clock edge; stranded is left only by chip select rising. Merging them gives a slave that rejoins mid-transaction.

"Gate on state != STRANDED — that is what the state is for." That passes the very assert that causes the stranding, because the state has not been entered yet on that cycle. The gate needs a term that is true on that cycle, which is the stale window itself.

"was_stranded should be a counter so we can see how often it happens." A counter of reset events cannot survive the reset that increments it. It would read zero or one forever, and a field labelled as a count that cannot count is worse than a flag.

12. Reason It Through

Q. SYNC_N = 2. Reset releases on cycle 0, and cs_assert_stb is observed on cycle 3. Was the select already low?

No. The stale window covers cycles 0 to 2 inclusive — post_reset <= SYNC_N. An assert on cycle 3 is outside it, which means the select fell at or after cycle 1 and this is a legitimate new transaction. Cycle 3 is the boundary, and the bench covers both 2 and 3 for exactly that reason: getting the comparison wrong by one either strands a legitimate transaction or joins a missed one.

Q. Why does the slave wait for chip select to go high rather than counting edges or using a timeout?

Because chip select rising is the only event that definitively ends the transaction it missed. A timeout would have to be longer than the longest legal transaction, which is unbounded — a master may hold the select low for a kilobyte. An edge count would require knowing how many edges the transaction contains, which is the thing the slave does not know. The select rising is the only unambiguous signal, and it is free.

Q. A reviewer suggests that the slave should capture bits while stranded and simply discard them, on the grounds that it is simpler than gating. What breaks?

Nothing in the receive path — but Chapter 14.4's transmit path would launch on those edges, driving MISO into the middle of a word the master is reading. The gating is not primarily about the receive side; it is about not transmitting. And a design that gated only the transmit side would then have Chapter 14.2 reporting a transaction and Chapter 14.9 publishing words from it, which is the plausible-wrong-data outcome the refusal exists to prevent.

Q. Why would registering the three gated outputs change a number in a datasheet?

Because the gated assert strobe is the transaction boundary for every downstream block, and Chapter 14.4's lead requirement is derived by counting cycles from the pin to that boundary and then to MISO. An extra register adds one cycle, making the requirement SYNC_N + 3 — so a master that worked before would now sample MISO one cycle early. A local decision about pipelining inside a gate propagates to a timing parameter an integrator has to respect.

Q. The design is instantiated with SYNC_N = 3 here and SYNC_N = 2 in the front end. What happens, and why will no test find it?

The stale window becomes one cycle too long, so a legitimate assert arriving 3 cycles after reset release is treated as a stranding and a valid transaction is refused. No unit test finds it because each block is correct with respect to its own parameter, and the bug is in the relationship between two instantiations. It is a top-level wiring property, which is why Chapter 14.10 passes one parameter to both and why a review of the top level is the only place it is visible.

13. Understanding Check

14. Summary

A slave's reset is asynchronous to SCLK, so it lands mid-transaction. Joining in is the wrong answer: the slave has missed an unknown number of bits, so it produces plausible undetectable garbage and corrupts the master's read by driving MISO mid-word.

The right answer is to refuse. A slave that comes out of reset selected is stranded: it captures nothing, drives nothing, reports nothing about that transaction, waits for chip select to rise, and rejoins on the next assert. The cost is exactly one transaction, and any master with a timeout gets correct behaviour on its retry.

The safe state is not the reset state. They have identical outputs and different input sensitivity, which makes them two states — the same argument that kept IDLE and GAP apart in Chapter 13.3.

The hard part is that the slave cannot ask whether the select was low, because its own synchronisers reset to deselected. The information is destroyed by the reset that needs it. What survives is timing: an assert within SYNC_N cycles of reset release can only come from a select that was already low, because one falling afterwards cannot produce an assert that early. Exact, not heuristic, and a SYNC_N + 1 state counter is the whole mechanism.

That is the module's third recovery of an unobservable fact from a time rather than a value — and the only one that recovers a value rather than a duration.

The gate must refuse the assert that causes the stranding, which a state != STRANDED test passes because the state is entered because of that assert. The general form: a gate derived from a state the event causes is one cycle late, and the missing cycle is the event.

was_stranded is a flag, because a counter cannot survive the reset that increments it. The count belongs outside this reset domain, and the testbench keeps it.

For verification: make reset release timing a randomised axis rather than a setup step; keep SCLK running during the stranded interval, or the refusal is untested; keep the count in the bench, because the design structurally cannot; and check the gated strobe rather than only the end result, since the §4 bug produces empty data either way.

For implementation: reset must be released synchronously or the cycle-counting argument collapses; post_reset saturates so the window cannot be re-entered; the gates are deliberately combinational, because registering them would change Chapter 14.4's lead requirement from SYNC_N + 2 to SYNC_N + 3; and SYNC_N must be the same parameter as the front end's, which only a top-level review can confirm.

15. What Comes Next

The slave receives, transmits, releases the bus, diagnoses mode mismatches, handles aborts and refuses what it cannot know. Nothing has yet handed any of it to software.

Chapter 14.9 — The System-Side Interface builds that boundary, and its central problem is one that has been implicit since Chapter 14.3: a transaction's words arrive over time, and software reads them whenever it happens to run. A read that lands mid-transaction can see half of one transaction and half of the next — a torn read — and the chapter builds a sequence-lock that makes tearing detectable rather than merely unlikely, along with a transmit default that answers the question of what a slave sends when software has loaded nothing.

Continue learning