Skip to content
VLSI Mentor

SPI · Module 19

A Multi-Slave Controller and Its Failure Modes

Only one chip select at a time is necessary and not sufficient. A perfect one-hot decoder still violated two device requirements, which vanish at two different turnaround thresholds.

Three chapters of integration have each had one device. This one adds devices — and the failures move out of the transfers and into the switch between them.

A controller whose selects are one-hot by construction, provably, at every setting, still violated two separate device requirements every time the bus changed hands.

1. What Changes When A Second Device Arrives

A controller serving one device never changes its configuration. A controller serving several does, between every pair of frames — and two of the things it changes are visible on shared wires.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CPOL is per-device   → SCLK's parked level must MOVE between devices,
                          and a level change is an EDGE on a shared wire

   MISO is shared       → the previous device's driver takes time to let go,
                          and selecting the next one too soon means two
                          devices drive the same pin

Neither failure exists in a single-device design. Both appear the moment a second device with a different mode is added, which is why they are so often met for the first time in integration rather than in unit test.

Left to right: a transfer request carrying a device number indexes a per-device configuration table, whose latched configuration feeds a turnaround state machine, which drives a one-hot decoder, which drives the selects of three devices with different modes. Below, the turnaround state is tied to a clock-hold requirement and the decoder to a bus-gap requirement.transfer requestdevice number and byteper-device configtablecpol, cpha, dividerper deviceturnaround statehold, switch, park,then selectone-hot decoderone index in, Nselects outthree devicesdifferent modes,different timinghold requirementSCLK still after adeselectbus gaprequirementMISO released beforethe next selectdevice #latched cfgselectcs_n[2:0]ttameasured12
Figure 1 — the controller, and the two device requirements the turnaround exists to satisfy. The selects come from one index through one decoder, so one-hot is structural; the two requirements below are timing properties that a correct decoder does nothing to guarantee.

2. The Turnaround, And Why Its Order Is The Design

Four things have to happen between two frames, and the order is the entire content of the design:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1. deselect            the previous device's select rises
   2. HOLD                every select high, SCLK has NOT moved      ← tta cycles
   3. switch              SCLK moves to the next device's CPOL
   4. PARK then select    wait the new device's lead, then select it

Hold the old level, then switch it, then select the next device

14 cycles
Five rows over fourteen cycles. Device zero's select rises at cycle two; device one's select falls at cycle eight. SCLK is low until cycle six and high afterwards, so the parked level changes at six. A hold-requirement row marks cycles two and three; a release-requirement row marks cycles two to seven. Phase bands mark the hold window, the switched level, and device one selected.hold: SCLK must not movehold: SCLK must notmoveparked level switchedparkedleve…device 1 selecteddevice 1 selecteddevice 0 deselecteddevice 0 deselectedparked level switches to CPOL 1parked level switches toCPOL 1both requirements already metboth requirements alreadymetcs_n[0]cs_n[1]sclkhold reqMISO heldt0t1t2t3t4t5t6t7t8t9t10t11t12t13
Figure 2 — the bus changing hands from device 0 (CPOL 0) to device 1 (CPOL 1), with a turnaround of 3. The previous device's two requirement windows are shown: SCLK must not move for 2 cycles after its select rises, and its MISO driver is not released until 6. The parked level moves at cycle 6 — outside the hold window — and device 1 is selected at cycle 8, after the release.

3. Two Requirements, One Parameter, Two Thresholds

Both failures are fixed by the same turnaround parameter, and they are satisfied at different values of it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the clock-hold requirement   depends on tta alone
   the MISO release requirement depends on the BUS GAP -- the interval from one
                                select rising to the next falling, which is tta
                                plus the lead plus the state machine's own
                                passage through idle

4. The Measurement

Three devices, CPOL 0 / 1 / 0, walked twice so every device-to-device transition occurs. Identical output from all three languages:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  three devices: cfg0=20 (cpol 0)  cfg1=21 (cpol 1)  cfg2=20 (cpol 0)   lead=2  lag=2
  device requirements: SCLK must not move for 2 cycles after a deselect; MISO releases 6 cycles after a deselect

  tta  bus gap  cs overlap  hold violations  contention  wrong bytes  verdict
    0         4           0                3           8            0  hold violated AND MISO contested
    1         4           0                3           8            0  hold violated AND MISO contested
    2         5           0                0           4            0  MISO still contested
    3         6           0                0           0            0  every requirement met
    4         7           0                0           0            0  every requirement met
    6         9           0                0           0            0  every requirement met

Read the cs overlap column first: zero at every setting. The decoder is one-hot by construction — one index, one decoder, so two selects low at once is not a reachable state — and that is precisely what licenses the rest of the table to be read as a timing result. This controller's select logic is correct, and at tta = 0 it violated both device requirements on every handover.

The hold violations stop at tta = 2, exactly the requirement. Note that tta = 0 and tta = 1 are the same setting for this state machine — the hold state's exit condition is satisfied immediately for both — which is why the first two rows are identical and is worth knowing before anybody tunes the parameter to 1 and reports an improvement.

Contention stops when the bus gap reaches 6, which happens at tta = 3. Different threshold, same parameter, and the binding one is neither the one the CPOL story suggests nor the one the arithmetic predicted.

And every received byte was checked against the addressed device's data at every setting. A sweep that counted only violations would pass a controller that met every timing requirement and talked to the wrong device.

5. Building It — Three HDLs

Azvya Education Pvt. Ltd.VLSI Mentor
spi_multi_slave.sv — the multi-slave controller — a one-hot decoder, a per-device configuration table, and a turnaround with two jobs
// spi_multi_slave.sv
//
// Chapter 19.4 -- several devices on one bus, each with its own select, its own mode and its own timing,
// and the failures that live in the switch BETWEEN them.
//
// THE OBVIOUS RULE IS NECESSARY AND NOT SUFFICIENT. "Only one chip select may be low at a time" is true,
// it is easy to guarantee with a one-hot decoder, and a controller that guarantees it perfectly can still
// violate two separate device requirements every time it changes devices. This module measures exactly how
// insufficient the obvious rule is.
//
// WHAT ACTUALLY GOES WRONG WHEN THE BUS CHANGES HANDS.
//
//   1. SCLK'S PARKED LEVEL IS PER-DEVICE. CPOL is a property of the device, so a controller serving a
//      CPOL=0 device and then a CPOL=1 device must CHANGE the level SCLK rests at. That change is an EDGE
//      on SCLK -- and if it happens too soon after the previous device was deselected, that device is
//      still within its own hold window and sees a clock edge it was not expecting.
//
//      This is the trap that makes a multi-device controller harder than N single-device ones: a design
//      serving identical devices never changes the parked level and never meets this failure at all.
//
//   2. THE PREVIOUS DEVICE'S MISO DRIVER TAKES TIME TO RELEASE. A slave stops driving MISO some time after
//      its select rises -- a datasheet number, usually in the tens of nanoseconds. Select the next device
//      before that, and two devices drive MISO at once. Chapter 18.6 measured what contention looks like
//      from the far end; this is where it is CREATED.
//
// BOTH ARE FIXED BY THE SAME PARAMETER AND THE THRESHOLDS ARE DIFFERENT. The inter-device turnaround
// `tta` -- a window in which every select is high and SCLK has not yet moved -- must satisfy:
//
//      tta >= t_hold              the previous device's clock-hold requirement
//      tta + lead >= t_release    the previous device's MISO release time
//
// The two numbers come from different sections of the datasheet, and the binding one is whichever is
// larger after the lead is taken into account. Section 5 sweeps `tta` and shows the two violations
// disappearing at two different thresholds.
//
// WHAT THIS MODULE GUARANTEES STRUCTURALLY, so that the measurement is about timing and not about logic:
//
//   * the selects are ONE-HOT by construction: a single decoder drives them from one index, so two low at
//     once is not expressible rather than merely unlikely
//   * the per-device configuration is latched at the START of each frame, for Chapter 19.3's reason
//   * SCLK moves only inside a frame or at the one instant the parked level changes
//
// THE ORDER OF THE TURNAROUND IS THE DESIGN. Deselect, hold the OLD level, switch the level, wait the new
// device's lead, select. Doing the switch before the hold, or folding the hold into the lead, produces a
// controller that looks identical in a block diagram and violates one requirement or the other.

`timescale 1ns/1ps

module spi_multi_slave #(
    parameter int NDEV  = 3,
    parameter int CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    // ---- the request interface ----
    input  wire              start,
    input  wire [1:0]        dev,        // which device this transfer is for
    input  wire [7:0]        tx_byte,
    output wire              busy,
    // A ONE-DEEP REQUEST QUEUE, and it is what makes the turnaround measurable at all. Without it the
    // requester has to wait for `done` before posting the next transfer, and its own reaction time becomes
    // part of the gap between frames -- so the controller's turnaround parameter is no longer the thing
    // being measured. A real controller queues for throughput; here it also makes the experiment honest.
    output wire              req_free,

    // ---- per-device configuration: [0] cpol  [1] cpha  [7:4] divider ----
    input  wire [7:0]        cfg0,
    input  wire [7:0]        cfg1,
    input  wire [7:0]        cfg2,

    // ---- timing, in system-clock cycles ----
    input  wire [7:0]        lead,
    input  wire [7:0]        lag,
    input  wire [7:0]        tta,        // the inter-device turnaround

    // ---- pins ----
    output reg               sclk,
    output reg  [NDEV-1:0]   cs_n,
    output reg               mosi,
    input  wire              miso,

    output reg  [7:0]        rx_byte,
    output reg               done,
    output reg  [CNT_W-1:0]  n_frames
);

    localparam [2:0] S_IDLE  = 3'd0,
                     S_HOLD  = 3'd1,   // every select high, SCLK still at the PREVIOUS parked level
                     S_PARK  = 3'd2,   // the level has switched; waiting the new device's lead
                     S_LEAD  = 3'd3,
                     S_SHIFT = 3'd4,
                     S_LAG   = 3'd5;

    reg [2:0]        st;
    reg [CNT_W-1:0]  dwell;
    reg [7:0]        ediv, cfg_cur;
    reg [4:0]        edges;
    reg [7:0]        sh_tx, sh_rx;
    reg [1:0]        cur_dev, pend_dev;
    reg [7:0]        pend_tx;
    reg              pending;
    reg [1:0]        sel_idx;          // the one-hot decoder's input
    reg              sel_on;           // is any device selected at all?

    assign busy     = (st != S_IDLE) | pending;
    assign req_free = ~pending;

    // The configuration of the device a transfer is about to serve. A mux over the table rather than a
    // register file, because the table is small and a mux cannot get out of step with it.
    function [7:0] cfg_of(input [1:0] d);
        begin
            case (d)
                2'd0:    cfg_of = cfg0;
                2'd1:    cfg_of = cfg1;
                default: cfg_of = cfg2;
            endcase
        end
    endfunction

    // THE SELECTS ARE ONE-HOT BY CONSTRUCTION. One index and one decoder, so "two selects low at once" is
    // not a state this design can reach -- which is what makes the measurement in the bench a statement
    // about TIMING rather than about the select logic.
    integer i;
    always @(*) begin
        for (i = 0; i < NDEV; i = i + 1)
            cs_n[i] = !(sel_on && (sel_idx == i[1:0]));
    end

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            st       <= S_IDLE;
            dwell    <= {CNT_W{1'b0}};
            ediv     <= 8'h00;
            cfg_cur  <= 8'h00;
            edges    <= 5'd0;
            sh_tx    <= 8'h00;
            sh_rx    <= 8'h00;
            cur_dev  <= 2'd0;
            pend_dev <= 2'd0;
            pend_tx  <= 8'h00;
            pending  <= 1'b0;
            sel_idx  <= 2'd0;
            sel_on   <= 1'b0;
            sclk     <= 1'b0;
            mosi     <= 1'b0;
            rx_byte  <= 8'h00;
            done     <= 1'b0;
            n_frames <= {CNT_W{1'b0}};
        end else begin
            done <= 1'b0;

            if (start && !pending) begin
                pending  <= 1'b1;
                pend_dev <= dev;
                pend_tx  <= tx_byte;
            end

            case (st)
                S_IDLE: begin
                    if (pending) begin
                        pending <= 1'b0;
                        cur_dev <= pend_dev;
                        // THE CONFIGURATION IS LATCHED HERE and nothing reads the table again for the
                        // rest of the frame -- Chapter 19.3's shadow, applied per device.
                        cfg_cur <= cfg_of(pend_dev);
                        sh_tx   <= pend_tx;
                        sh_rx   <= 8'h00;
                        st      <= S_HOLD;
                        dwell   <= {CNT_W{1'b0}};
                        // NOTE WHAT DOES **NOT** HAPPEN HERE: SCLK is not touched. It is still parked at
                        // the PREVIOUS device's level, and it stays there for the whole hold window. That
                        // is the entire content of the turnaround, and moving this one assignment earlier
                        // is the bug this chapter measures.
                    end
                end

                S_HOLD: begin
                    // Every select is high and SCLK has not moved. The previous device's clock-hold
                    // requirement is satisfied here, and nowhere else.
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, tta}) begin
                        st    <= S_PARK;
                        dwell <= {CNT_W{1'b0}};
                        sclk  <= cfg_cur[0];        // the parked level switches HERE
                    end else dwell <= dwell + 1'b1;
                end

                S_PARK: begin
                    // The level has changed and no device is selected yet. The previous device's MISO
                    // driver is released during this window plus the hold window before it.
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lead}) begin
                        st      <= S_LEAD;
                        dwell   <= {CNT_W{1'b0}};
                        sel_idx <= cur_dev;
                        sel_on  <= 1'b1;
                        mosi    <= sh_tx[7];
                    end else dwell <= dwell + 1'b1;
                end

                S_LEAD: begin
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lead}) begin
                        st    <= S_SHIFT;
                        dwell <= {CNT_W{1'b0}};
                        edges <= 5'd0;
                        ediv  <= {4'h0, cfg_cur[7:4]};
                    end else dwell <= dwell + 1'b1;
                end

                S_SHIFT: begin
                    if (ediv == 8'h00) begin
                        ediv <= {4'h0, cfg_cur[7:4]};
                        if (sclk == cfg_cur[0]) begin
                            if (!cfg_cur[1]) sh_rx <= {sh_rx[6:0], miso};
                            sclk <= ~cfg_cur[0];
                        end else begin
                            if (cfg_cur[1]) sh_rx <= {sh_rx[6:0], miso};
                            sclk  <= cfg_cur[0];
                            mosi  <= sh_tx[6];
                            sh_tx <= {sh_tx[6:0], 1'b0};
                        end
                        if (edges == 5'd15) begin
                            st <= S_LAG;
                        end else begin
                            edges <= edges + 5'd1;
                        end
                    end else begin
                        ediv <= ediv - 8'h01;
                    end
                end

                S_LAG: begin
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lag}) begin
                        st       <= S_IDLE;
                        dwell    <= {CNT_W{1'b0}};
                        sel_on   <= 1'b0;           // deselect; SCLK stays where it is
                        rx_byte  <= sh_rx;
                        done     <= 1'b1;
                        n_frames <= n_frames + 1'b1;
                    end else dwell <= dwell + 1'b1;
                end

                default: st <= S_IDLE;
            endcase
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_multi_slave.v — the same design in Verilog-2001
// spi_multi_slave.v
//
// Chapter 19.4 -- several devices on one bus, each with its own select, its own mode and its own timing,
// and the failures that live in the switch BETWEEN them.
//
// THE OBVIOUS RULE IS NECESSARY AND NOT SUFFICIENT. "Only one chip select may be low at a time" is true,
// it is easy to guarantee with a one-hot decoder, and a controller that guarantees it perfectly can still
// violate two separate device requirements every time it changes devices. This module measures exactly how
// insufficient the obvious rule is.
//
// WHAT ACTUALLY GOES WRONG WHEN THE BUS CHANGES HANDS.
//
//   1. SCLK'S PARKED LEVEL IS PER-DEVICE. CPOL is a property of the device, so a controller serving a
//      CPOL=0 device and then a CPOL=1 device must CHANGE the level SCLK rests at. That change is an EDGE
//      on SCLK -- and if it happens too soon after the previous device was deselected, that device is
//      still within its own hold window and sees a clock edge it was not expecting.
//
//      This is the trap that makes a multi-device controller harder than N single-device ones: a design
//      serving identical devices never changes the parked level and never meets this failure at all.
//
//   2. THE PREVIOUS DEVICE'S MISO DRIVER TAKES TIME TO RELEASE. A slave stops driving MISO some time after
//      its select rises -- a datasheet number, usually in the tens of nanoseconds. Select the next device
//      before that, and two devices drive MISO at once. Chapter 18.6 measured what contention looks like
//      from the far end; this is where it is CREATED.
//
// BOTH ARE FIXED BY THE SAME PARAMETER AND THE THRESHOLDS ARE DIFFERENT. The inter-device turnaround
// `tta` -- a window in which every select is high and SCLK has not yet moved -- must satisfy:
//
//      tta >= t_hold              the previous device's clock-hold requirement
//      tta + lead >= t_release    the previous device's MISO release time
//
// The two numbers come from different sections of the datasheet, and the binding one is whichever is
// larger after the lead is taken into account. Section 5 sweeps `tta` and shows the two violations
// disappearing at two different thresholds.
//
// WHAT THIS MODULE GUARANTEES STRUCTURALLY, so that the measurement is about timing and not about logic:
//
//   * the selects are ONE-HOT by construction: a single decoder drives them from one index, so two low at
//     once is not expressible rather than merely unlikely
//   * the per-device configuration is latched at the START of each frame, for Chapter 19.3's reason
//   * SCLK moves only inside a frame or at the one instant the parked level changes
//
// THE ORDER OF THE TURNAROUND IS THE DESIGN. Deselect, hold the OLD level, switch the level, wait the new
// device's lead, select. Doing the switch before the hold, or folding the hold into the lead, produces a
// controller that looks identical in a block diagram and violates one requirement or the other.

`timescale 1ns/1ps

module spi_multi_slave #(
    parameter NDEV  = 3,
    parameter CNT_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    // ---- the request interface ----
    input  wire              start,
    input  wire [1:0]        dev,        // which device this transfer is for
    input  wire [7:0]        tx_byte,
    output wire              busy,
    // A ONE-DEEP REQUEST QUEUE, and it is what makes the turnaround measurable at all. Without it the
    // requester has to wait for `done` before posting the next transfer, and its own reaction time becomes
    // part of the gap between frames -- so the controller's turnaround parameter is no longer the thing
    // being measured. A real controller queues for throughput; here it also makes the experiment honest.
    output wire              req_free,

    // ---- per-device configuration: [0] cpol  [1] cpha  [7:4] divider ----
    input  wire [7:0]        cfg0,
    input  wire [7:0]        cfg1,
    input  wire [7:0]        cfg2,

    // ---- timing, in system-clock cycles ----
    input  wire [7:0]        lead,
    input  wire [7:0]        lag,
    input  wire [7:0]        tta,        // the inter-device turnaround

    // ---- pins ----
    output reg               sclk,
    output reg  [NDEV-1:0]   cs_n,
    output reg               mosi,
    input  wire              miso,

    output reg  [7:0]        rx_byte,
    output reg               done,
    output reg  [CNT_W-1:0]  n_frames
);

    localparam [2:0] S_IDLE  = 3'd0,
                     S_HOLD  = 3'd1,   // every select high, SCLK still at the PREVIOUS parked level
                     S_PARK  = 3'd2,   // the level has switched; waiting the new device's lead
                     S_LEAD  = 3'd3,
                     S_SHIFT = 3'd4,
                     S_LAG   = 3'd5;

    reg [2:0]        st;
    reg [CNT_W-1:0]  dwell;
    reg [7:0]        ediv, cfg_cur;
    reg [4:0]        edges;
    reg [7:0]        sh_tx, sh_rx;
    reg [1:0]        cur_dev, pend_dev;
    reg [7:0]        pend_tx;
    reg              pending;
    reg [1:0]        sel_idx;          // the one-hot decoder's input
    reg              sel_on;           // is any device selected at all?

    assign busy     = (st != S_IDLE) | pending;
    assign req_free = ~pending;

    // The configuration of the device a transfer is about to serve. A mux over the table rather than a
    // register file, because the table is small and a mux cannot get out of step with it.
        function [7:0] cfg_of;
        input [1:0] d;
        begin
            case (d)
                2'd0:    cfg_of = cfg0;
                2'd1:    cfg_of = cfg1;
                default: cfg_of = cfg2;
            endcase
        end
    endfunction

    // THE SELECTS ARE ONE-HOT BY CONSTRUCTION. One index and one decoder, so "two selects low at once" is
    // not a state this design can reach -- which is what makes the measurement in the bench a statement
    // about TIMING rather than about the select logic.
    integer i;
    always @(*) begin
        for (i = 0; i < NDEV; i = i + 1)
            cs_n[i] = !(sel_on && (sel_idx == i[1:0]));
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            st       <= S_IDLE;
            dwell    <= {CNT_W{1'b0}};
            ediv     <= 8'h00;
            cfg_cur  <= 8'h00;
            edges    <= 5'd0;
            sh_tx    <= 8'h00;
            sh_rx    <= 8'h00;
            cur_dev  <= 2'd0;
            pend_dev <= 2'd0;
            pend_tx  <= 8'h00;
            pending  <= 1'b0;
            sel_idx  <= 2'd0;
            sel_on   <= 1'b0;
            sclk     <= 1'b0;
            mosi     <= 1'b0;
            rx_byte  <= 8'h00;
            done     <= 1'b0;
            n_frames <= {CNT_W{1'b0}};
        end else begin
            done <= 1'b0;

            if (start && !pending) begin
                pending  <= 1'b1;
                pend_dev <= dev;
                pend_tx  <= tx_byte;
            end

            case (st)
                S_IDLE: begin
                    if (pending) begin
                        pending <= 1'b0;
                        cur_dev <= pend_dev;
                        // THE CONFIGURATION IS LATCHED HERE and nothing reads the table again for the
                        // rest of the frame -- Chapter 19.3's shadow, applied per device.
                        cfg_cur <= cfg_of(pend_dev);
                        sh_tx   <= pend_tx;
                        sh_rx   <= 8'h00;
                        st      <= S_HOLD;
                        dwell   <= {CNT_W{1'b0}};
                        // NOTE WHAT DOES **NOT** HAPPEN HERE: SCLK is not touched. It is still parked at
                        // the PREVIOUS device's level, and it stays there for the whole hold window. That
                        // is the entire content of the turnaround, and moving this one assignment earlier
                        // is the bug this chapter measures.
                    end
                end

                S_HOLD: begin
                    // Every select is high and SCLK has not moved. The previous device's clock-hold
                    // requirement is satisfied here, and nowhere else.
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, tta}) begin
                        st    <= S_PARK;
                        dwell <= {CNT_W{1'b0}};
                        sclk  <= cfg_cur[0];        // the parked level switches HERE
                    end else dwell <= dwell + 1'b1;
                end

                S_PARK: begin
                    // The level has changed and no device is selected yet. The previous device's MISO
                    // driver is released during this window plus the hold window before it.
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lead}) begin
                        st      <= S_LEAD;
                        dwell   <= {CNT_W{1'b0}};
                        sel_idx <= cur_dev;
                        sel_on  <= 1'b1;
                        mosi    <= sh_tx[7];
                    end else dwell <= dwell + 1'b1;
                end

                S_LEAD: begin
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lead}) begin
                        st    <= S_SHIFT;
                        dwell <= {CNT_W{1'b0}};
                        edges <= 5'd0;
                        ediv  <= {4'h0, cfg_cur[7:4]};
                    end else dwell <= dwell + 1'b1;
                end

                S_SHIFT: begin
                    if (ediv == 8'h00) begin
                        ediv <= {4'h0, cfg_cur[7:4]};
                        if (sclk == cfg_cur[0]) begin
                            if (!cfg_cur[1]) sh_rx <= {sh_rx[6:0], miso};
                            sclk <= ~cfg_cur[0];
                        end else begin
                            if (cfg_cur[1]) sh_rx <= {sh_rx[6:0], miso};
                            sclk  <= cfg_cur[0];
                            mosi  <= sh_tx[6];
                            sh_tx <= {sh_tx[6:0], 1'b0};
                        end
                        if (edges == 5'd15) begin
                            st <= S_LAG;
                        end else begin
                            edges <= edges + 5'd1;
                        end
                    end else begin
                        ediv <= ediv - 8'h01;
                    end
                end

                S_LAG: begin
                    if (dwell + 1'b1 >= {{(CNT_W-8){1'b0}}, lag}) begin
                        st       <= S_IDLE;
                        dwell    <= {CNT_W{1'b0}};
                        sel_on   <= 1'b0;           // deselect; SCLK stays where it is
                        rx_byte  <= sh_rx;
                        done     <= 1'b1;
                        n_frames <= n_frames + 1'b1;
                    end else dwell <= dwell + 1'b1;
                end

                default: st <= S_IDLE;
            endcase
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_multi_slave.vhd — the same design in VHDL
-- spi_multi_slave.vhd
--
-- Chapter 19.4 -- several devices on one bus, each with its own select, its own mode and its own timing,
-- and the failures that live in the switch BETWEEN them.
--
-- THE OBVIOUS RULE IS NECESSARY AND NOT SUFFICIENT. "Only one chip select may be low at a time" is true,
-- it is easy to guarantee with a one-hot decoder, and a controller that guarantees it perfectly can still
-- violate two separate device requirements every time it changes devices. This module measures exactly how
-- insufficient the obvious rule is.
--
-- WHAT ACTUALLY GOES WRONG WHEN THE BUS CHANGES HANDS.
--
--   1. SCLK'S PARKED LEVEL IS PER-DEVICE. CPOL is a property of the device, so a controller serving a
--      CPOL=0 device and then a CPOL=1 device must CHANGE the level SCLK rests at. That change is an EDGE
--      on SCLK -- and if it happens too soon after the previous device was deselected, that device is
--      still within its own hold window and sees a clock edge it was not expecting.
--
--      This is the trap that makes a multi-device controller harder than N single-device ones: a design
--      serving identical devices never changes the parked level and never meets this failure at all.
--
--   2. THE PREVIOUS DEVICE'S MISO DRIVER TAKES TIME TO RELEASE. A slave stops driving MISO some time after
--      its select rises -- a datasheet number, usually in the tens of nanoseconds. Select the next device
--      before that, and two devices drive MISO at once. Chapter 18.6 measured what contention looks like
--      from the far end; this is where it is CREATED.
--
-- BOTH ARE FIXED BY THE SAME PARAMETER AND THE THRESHOLDS ARE DIFFERENT. The inter-device turnaround
-- `tta` -- a window in which every select is high and SCLK has not yet moved -- must satisfy:
--
--      tta >= t_hold              the previous device's clock-hold requirement
--      tta + lead >= t_release    the previous device's MISO release time
--
-- The two numbers come from different sections of the datasheet, and the binding one is whichever is
-- larger after the lead is taken into account. Section 5 sweeps `tta` and shows the two violations
-- disappearing at two different thresholds.
--
-- WHAT THIS MODULE GUARANTEES STRUCTURALLY, so that the measurement is about timing and not about logic:
--
--   * the selects are ONE-HOT by construction: a single decoder drives them from one index, so two low at
--     once is not expressible rather than merely unlikely
--   * the per-device configuration is latched at the START of each frame, for Chapter 19.3's reason
--   * SCLK moves only inside a frame or at the one instant the parked level changes
--
-- THE ORDER OF THE TURNAROUND IS THE DESIGN. Deselect, hold the OLD level, switch the level, wait the new
-- device's lead, select. Doing the switch before the hold, or folding the hold into the lead, produces a
-- controller that looks identical in a block diagram and violates one requirement or the other.

--
-- WHAT THE VHDL VERSION ADDS. The six phases of a device-to-device turnaround are an ENUMERATION, and two
-- of them -- `S_HOLD` and `S_PARK` -- exist solely to satisfy the two device requirements this chapter
-- measures. Naming them at the declaration is worth more here than anywhere else in the module, because a
-- reader's first instinct is that they are the same state.
--
-- The per-device configuration table is a constrained array indexed by the device number, so a device index
-- outside the table is a range error rather than an aliased read of the wrong device's mode.
--
-- Every register is a SIGNAL rather than a process variable, for the reason Chapter 19.2 measured four
-- separate times: a variable is updated immediately and a non-blocking reg is not, and each difference
-- produces a design that behaves plausibly.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). Generics are `NDEV_C` and `CNT_W`. The timing ports are
-- `lead`, `lag`, `tta`; their integer copies are `n_lead`, `n_lag`, `n_tta` -- not `LEAD` or `Tta`, because a
-- variable differing from a port only in case IS that port. Nothing collides with a reserved word: in
-- particular nothing is named `register`, `bus`, `open`, `access` or `next`, all of which are tempting in a
-- multi-device bus controller.
--
-- RANGE-DIRECTION REVIEW. Every vector is `downto`, the select vector is `NDEV_C - 1 downto 0`, and the
-- shift registers are constrained subtypes, so no slice inherits an ascending range.

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

package spi_multi_pkg is
    subtype byte_t is std_logic_vector(7 downto 0);

    -- The six phases. `S_HOLD` and `S_PARK` are both "no device selected" and they are NOT the same state:
    -- SCLK still carries the previous device's parked level through the first and the new device's through
    -- the second. Collapsing them is the bug this chapter measures.
    type ms_state_t is (S_IDLE, S_HOLD, S_PARK, S_LEAD, S_SHIFT, S_LAG);

    type cfg_table_t is array (0 to 2) of byte_t;
end package spi_multi_pkg;

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

entity spi_multi_slave is
    generic (
        NDEV_C : positive := 3;
        CNT_W  : positive := 16
    );
    port (
        clk   : in  std_logic;
        rst_n : in  std_logic;

        start    : in  std_logic;
        dev      : in  natural range 0 to 2;
        tx_byte  : in  byte_t;
        busy     : out std_logic;
        req_free : out std_logic;

        cfg0 : in  byte_t;
        cfg1 : in  byte_t;
        cfg2 : in  byte_t;

        lead : in  unsigned(7 downto 0);
        lag  : in  unsigned(7 downto 0);
        tta  : in  unsigned(7 downto 0);

        sclk : out std_logic;
        cs_n : out std_logic_vector(NDEV_C - 1 downto 0);
        mosi : out std_logic;
        miso : in  std_logic;

        rx_byte  : out byte_t;
        done     : out std_logic;
        n_frames : out natural
    );
end entity spi_multi_slave;

architecture rtl of spi_multi_slave is
    signal st       : ms_state_t := S_IDLE;
    signal dwell    : natural := 0;
    signal ediv     : unsigned(7 downto 0) := (others => '0');
    signal cfg_cur  : byte_t := (others => '0');
    signal edges    : natural := 0;
    signal sh_tx, sh_rx : byte_t := (others => '0');
    signal cur_dev, pend_dev : natural range 0 to 2 := 0;
    signal pend_tx  : byte_t := (others => '0');
    signal pending  : std_logic := '0';
    signal sel_idx  : natural range 0 to 2 := 0;
    signal sel_on   : std_logic := '0';
    signal sclk_r, mosi_r, done_r : std_logic := '0';
    signal rx_r     : byte_t := (others => '0');
    signal nf_r     : natural := 0;

    signal cfg_tab : cfg_table_t;
begin

    cfg_tab  <= (cfg0, cfg1, cfg2);
    sclk     <= sclk_r;
    mosi     <= mosi_r;
    rx_byte  <= rx_r;
    done     <= done_r;
    n_frames <= nf_r;
    busy     <= '1' when (st /= S_IDLE) or (pending = '1') else '0';
    req_free <= not pending;

    -- THE SELECTS ARE ONE-HOT BY CONSTRUCTION: one index and one decoder, so "two selects low at once" is
    -- not a state this design can reach. That is what makes the bench's measurement a statement about
    -- TIMING rather than about the select logic.
    onehot : process (sel_on, sel_idx) is
    begin
        for i in 0 to NDEV_C - 1 loop
            if sel_on = '1' and sel_idx = i then
                cs_n(i) <= '0';
            else
                cs_n(i) <= '1';
            end if;
        end loop;
    end process onehot;

    main : process (clk, rst_n) is
        variable n_lead, n_lag, n_tta : natural;
    begin
        if rst_n = '0' then
            st <= S_IDLE; dwell <= 0; ediv <= (others => '0');
            cfg_cur <= (others => '0'); edges <= 0;
            sh_tx <= (others => '0'); sh_rx <= (others => '0');
            cur_dev <= 0; pend_dev <= 0; pend_tx <= (others => '0');
            pending <= '0'; sel_idx <= 0; sel_on <= '0';
            sclk_r <= '0'; mosi_r <= '0'; done_r <= '0';
            rx_r <= (others => '0'); nf_r <= 0;

        elsif rising_edge(clk) then
            done_r <= '0';
            n_lead := to_integer(lead);
            n_lag  := to_integer(lag);
            n_tta  := to_integer(tta);

            -- A one-deep request queue. Without it the requester must wait for `done` before posting the
            -- next transfer, and its reaction time becomes part of the gap between frames -- so the
            -- turnaround parameter is no longer the thing being measured.
            if start = '1' and pending = '0' then
                pending  <= '1';
                pend_dev <= dev;
                pend_tx  <= tx_byte;
            end if;

            case st is
                when S_IDLE =>
                    if pending = '1' then
                        pending <= '0';
                        cur_dev <= pend_dev;
                        -- The configuration is latched HERE and nothing reads the table again for the rest
                        -- of the frame -- Chapter 19.3's shadow, applied per device.
                        cfg_cur <= cfg_tab(pend_dev);
                        sh_tx   <= pend_tx;
                        sh_rx   <= (others => '0');
                        st      <= S_HOLD;
                        dwell   <= 0;
                        -- NOTE WHAT DOES NOT HAPPEN HERE: SCLK is not touched. It is still parked at the
                        -- PREVIOUS device's level and stays there for the whole hold window. Moving this one
                        -- assignment earlier is the bug this chapter measures.
                    end if;

                when S_HOLD =>
                    -- Every select is high and SCLK has not moved. The previous device's clock-hold
                    -- requirement is satisfied here and nowhere else.
                    if dwell + 1 >= n_tta then
                        st     <= S_PARK;
                        dwell  <= 0;
                        sclk_r <= cfg_cur(0);       -- the parked level switches HERE
                    else
                        dwell <= dwell + 1;
                    end if;

                when S_PARK =>
                    -- The level has changed and no device is selected yet. The previous device's MISO driver
                    -- is released during this window plus the hold window before it.
                    if dwell + 1 >= n_lead then
                        st      <= S_LEAD;
                        dwell   <= 0;
                        sel_idx <= cur_dev;
                        sel_on  <= '1';
                        mosi_r  <= sh_tx(7);
                    else
                        dwell <= dwell + 1;
                    end if;

                when S_LEAD =>
                    if dwell + 1 >= n_lead then
                        st    <= S_SHIFT;
                        dwell <= 0;
                        edges <= 0;
                        -- `resize`, NOT a concatenation. Declaring `cfg_table_t` as an array OF byte_t
                        -- gives `&` an overload returning that array type, so `x"0" & cfg_cur(7 downto 4)`
                        -- becomes ambiguous and the file will not analyse. Declaring an array of a type
                        -- silently adds a concatenation operator for it -- a VHDL consequence that has
                        -- nothing to do with this design and appears the moment a lookup table is added.
                        ediv  <= resize(unsigned(cfg_cur(7 downto 4)), 8);
                    else
                        dwell <= dwell + 1;
                    end if;

                when S_SHIFT =>
                    if ediv = 0 then
                        ediv <= resize(unsigned(cfg_cur(7 downto 4)), 8);
                        if sclk_r = cfg_cur(0) then
                            if cfg_cur(1) = '0' then
                                sh_rx <= sh_rx(6 downto 0) & miso;
                            end if;
                            sclk_r <= not cfg_cur(0);
                        else
                            if cfg_cur(1) = '1' then
                                sh_rx <= sh_rx(6 downto 0) & miso;
                            end if;
                            sclk_r <= cfg_cur(0);
                            mosi_r <= sh_tx(6);
                            sh_tx  <= sh_tx(6 downto 0) & '0';
                        end if;
                        if edges = 15 then
                            st <= S_LAG;
                        else
                            edges <= edges + 1;
                        end if;
                    else
                        ediv <= ediv - 1;
                    end if;

                when S_LAG =>
                    if dwell + 1 >= n_lag then
                        st     <= S_IDLE;
                        dwell  <= 0;
                        sel_on <= '0';              -- deselect; SCLK stays where it is
                        rx_r   <= sh_rx;
                        done_r <= '1';
                        nf_r   <= nf_r + 1;
                    else
                        dwell <= dwell + 1;
                    end if;
            end case;
        end if;
    end process main;

end architecture rtl;

The Bench

Each slave is modelled with its datasheet requirements, not just its behaviour: how long SCLK must stay still after its select rises, and how long its MISO driver takes to release. The bench then counts violations of those requirements on the pins.

That is the design of this bench in one sentence: the controller is not asked whether it behaved, the devices are asked whether their requirements were met.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_multi_slave_tb.sv — three devices with different modes, and two requirements that vanish at two different thresholds
// spi_multi_slave_tb.sv
//
// THREE DEVICES WITH DIFFERENT MODES, AND TWO REQUIREMENTS THAT DISAPPEAR AT DIFFERENT THRESHOLDS.
//
// The three slaves are modelled here with DATASHEET REQUIREMENTS rather than just behaviour: each one
// states how long SCLK must stay still after its select rises (`T_HOLD`) and how long its MISO driver takes
// to release (`T_REL`). The bench then counts violations of those requirements on the pins. That is the
// whole design of this bench: the controller is not asked whether it behaved, the devices are asked whether
// their requirements were met.
//
// THE FOUR RESULTS.
//
//   1. THE SELECTS ARE ONE-HOT AT EVERY SETTING. Zero cycles with two selects low, in every run -- which is
//      what makes the rest of the table a statement about TIMING rather than about the select logic. A
//      controller with a perfect one-hot decoder violates both device requirements below.
//
//   2. A CLOCK-HOLD VIOLATION IS CREATED BY THE PARKED LEVEL CHANGING TOO SOON. Devices 0 and 1 have
//      different CPOL, so the controller must move SCLK between them. With no turnaround the move lands
//      inside the previous device's hold window. The violations disappear at tta >= T_HOLD.
//
//   3. CONTENTION IS CREATED BY SELECTING THE NEXT DEVICE BEFORE THE PREVIOUS ONE HAS RELEASED MISO. That
//      threshold is DIFFERENT -- it is tta + lead >= T_REL -- so it disappears at a different value of the
//      same parameter, and the binding requirement is not the one the CPOL story would make you expect.
//
//   4. AND EVERY RECEIVED BYTE IS CHECKED, at every setting, against what the addressed device sent. A
//      turnaround sweep that only counted violations could pass a controller that met every timing
//      requirement and talked to the wrong device.

`timescale 1ns/1ps

module spi_multi_slave_tb;

    localparam int NDEV  = 3;
    localparam int CNT_W = 16;

    // The devices' datasheet numbers, in system-clock cycles.
    localparam int T_HOLD = 2;    // SCLK must not move for this long after a select rises
    localparam int T_REL  = 6;    // MISO is released this long after a select rises

    reg clk = 1'b0;
    always #5 clk = ~clk;
    reg rst_n = 1'b1;

    reg        start = 1'b0;
    reg [1:0]  dev = 2'd0;
    reg [7:0]  tx_byte = 8'h00;
    wire       busy, req_free;

    // Three devices, deliberately with DIFFERENT CPOL, so the parked level has to move between them.
    // Divider 2 everywhere, so each slave model has setup margin -- Chapter 19.1's boundary, avoided
    // rather than re-measured.
    // DECLARED WITHOUT INITIALISERS AND SET AT THE TOP OF `initial`, and the reason is a race rather than a
    // tool limitation.
    //
    // A declaration initialiser is applied during elaboration -- BEFORE time zero. A mechanical conversion
    // to Verilog hoists it into an `initial` block instead, which runs AT time zero, concurrently with every
    // other initial block and in an order the language does not define. So `reg [7:0] cfg0 = 8'h20;` is
    // "already 0x20 when simulation starts" in one file and "assigned at time zero, possibly after the
    // stimulus block has begun" in the other.
    //
    // The symptom was every configuration register reading X in the Verilog transcript's first line and
    // plausible values in the table underneath -- because the hoisted block happened to run after the
    // display and before the sweep. Nothing about the conversion is wrong; the two spellings genuinely
    // differ in WHEN they take effect.
    //
    // Setting stimulus explicitly at the top of the one initial block that uses it removes the race and is
    // identical in all three languages.
    reg [7:0]  cfg0, cfg1, cfg2;
    reg [7:0]  lead, lag, tta;

    wire            sclk, mosi;
    wire [NDEV-1:0] cs_n;
    wire            miso;
    wire [7:0]      rx_byte;
    wire            done;
    wire [CNT_W-1:0] n_frames;

    spi_multi_slave #(.NDEV(NDEV), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .start(start), .dev(dev), .tx_byte(tx_byte), .busy(busy), .req_free(req_free),
        .cfg0(cfg0), .cfg1(cfg1), .cfg2(cfg2),
        .lead(lead), .lag(lag), .tta(tta),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
        .rx_byte(rx_byte), .done(done), .n_frames(n_frames)
    );

    integer errors = 0;

    // ------------------------------------------------------------------
    // THREE SLAVE MODELS, each policing its own requirements.
    //
    // `drv[i]` is high while device i is driving MISO. It goes high when its select falls and low T_REL
    // cycles after its select rises -- which is what creates contention if the next select comes too soon.
    // ------------------------------------------------------------------
    reg [7:0]  slv_byte [0:NDEV-1];
    reg [7:0]  slv_sh   [0:NDEV-1];
    reg [NDEV-1:0] drv;
    integer    rel_tmr [0:NDEV-1];
    integer    hold_tmr[0:NDEV-1];
    reg        slv_cpol [0:NDEV-1];
    reg [NDEV-1:0] cs_d;
    reg        sclk_d;

    // `$countones` is a SystemVerilog system function and does not exist in Verilog-2001, so the count is a
    // plain function -- which also keeps the three language versions doing arithmetic a reader can check.
    function integer ones3(input [NDEV-1:0] v);
        integer b;
        begin
            ones3 = 0;
            for (b = 0; b < NDEV; b = b + 1) ones3 = ones3 + ((v[b] === 1'b1) ? 1 : 0);
        end
    endfunction

    integer n_overlap, n_hold_viol, n_contend, n_checked, n_wrong;

    // THE BUS GAP IS MEASURED, NOT PREDICTED.
    //
    // The interval between one select rising and the next falling is what the MISO-release requirement is
    // actually about, and deriving it from `tta + lead` by hand produced an off-by-one: the state machine
    // spends a cycle passing through idle, and the bench's own edge detection costs another. Measuring the
    // gap on the pins removes both guesses and makes the requirement a comparison between two observed
    // numbers rather than between an observation and an assumption.
    integer gap_tmr, min_gap;
    reg     gap_run;

    integer j;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            drv <= {NDEV{1'b0}};
            cs_d <= {NDEV{1'b1}};
            sclk_d <= 1'b0;
            n_overlap <= 0; n_hold_viol <= 0; n_contend <= 0;
            gap_tmr <= 0; min_gap <= 9999; gap_run <= 1'b0;
            for (j = 0; j < NDEV; j = j + 1) begin
                slv_sh[j]   <= 8'h00;
                rel_tmr[j]  <= 0;
                hold_tmr[j] <= 0;
            end
        end else begin
            cs_d   <= cs_n;
            sclk_d <= sclk;

            // ---- requirement 1: one-hot selects ----
            //
            // Counted rather than assumed. The design makes two-low unreachable, and checking it anyway is
            // what licenses every other row of the table to be read as a timing result.
            if (ones3(~cs_n) > 1) n_overlap <= n_overlap + 1;

            for (j = 0; j < NDEV; j = j + 1) begin
                // a select falling: the device starts driving MISO and loads its byte
                if (cs_d[j] && !cs_n[j]) begin
                    drv[j]      <= 1'b1;
                    slv_sh[j]   <= slv_byte[j];
                    rel_tmr[j]  <= 0;
                    hold_tmr[j] <= 0;
                end
                // a select rising: both requirement timers start
                if (!cs_d[j] && cs_n[j]) begin
                    rel_tmr[j]  <= T_REL;
                    hold_tmr[j] <= T_HOLD;
                end
                // ---- requirement 2: MISO release ----
                if (rel_tmr[j] > 1) rel_tmr[j] <= rel_tmr[j] - 1;
                else if (rel_tmr[j] == 1) begin
                    rel_tmr[j] <= 0;
                    drv[j]     <= 1'b0;          // the driver finally lets go
                end
                // ---- requirement 3: SCLK must stay still after a deselect ----
                if (hold_tmr[j] > 0) begin
                    hold_tmr[j] <= hold_tmr[j] - 1;
                    if (sclk_d !== sclk) n_hold_viol <= n_hold_viol + 1;
                end
                // Shifting, for the device that is selected -- on ITS OWN trailing edge, which is the
                // transition back TO its idle level. The first version advanced on a falling edge for every
                // device, so the CPOL=1 device shifted on the wrong edge and returned a wrong byte at every
                // turnaround setting. A slave model that assumes one polarity cannot verify a controller
                // whose whole subject is serving devices with different ones.
                if (!cs_n[j] && (sclk_d !== sclk) && (sclk == slv_cpol[j]))
                    slv_sh[j] <= {slv_sh[j][6:0], 1'b0};
            end

            // ---- the bus gap: from any select rising to the next select falling ----
            if ((cs_d != {NDEV{1'b1}}) && (cs_n == {NDEV{1'b1}})) begin
                gap_run <= 1'b1;                 // everything just went high
                gap_tmr <= 1;
            end else if (gap_run && (cs_n == {NDEV{1'b1}})) begin
                gap_tmr <= gap_tmr + 1;
            end else if (gap_run && (cs_n != {NDEV{1'b1}})) begin
                gap_run <= 1'b0;
                if (gap_tmr < min_gap) min_gap <= gap_tmr;
            end

            // ---- requirement 2, observed: two devices driving MISO at once ----
            if (ones3(drv) > 1) n_contend <= n_contend + 1;
        end
    end

    // MISO: whichever device is driving presents its current bit. Two drivers give X, which is the honest
    // value for a contested net -- and the received byte is then wrong in a way no value comparison could
    // mistake for anything else.
    assign miso = (ones3(drv) > 1) ? 1'bx
                : drv[0] ? slv_sh[0][7]
                : drv[1] ? slv_sh[1][7]
                : drv[2] ? slv_sh[2][7] : 1'b0;

    // ---- per-frame checking: did the ADDRESSED device's byte arrive? ----
    //
    // The expected device is QUEUED, because posts run ahead of completions once the request slot is used.
    // A single `exp_dev` register would be overwritten by the next post before the current frame finished,
    // and every comparison would be against the wrong device -- a checker that is wrong in a way that looks
    // like a design fault.
    reg [1:0] exp_dev_q [0:7];
    integer   post_n, chk_n;
    always @(posedge clk) if (rst_n && done) begin
        n_checked = n_checked + 1;
        if (rx_byte !== slv_byte[exp_dev_q[chk_n % 8]]) n_wrong = n_wrong + 1;
        chk_n = chk_n + 1;
    end

    // ------------------------------------------------------------------
    task automatic reset_all;
        begin
            @(negedge clk); rst_n = 1'b0;
            n_checked = 0; n_wrong = 0; post_n = 0; chk_n = 0;
            repeat (6) @(negedge clk);
            rst_n = 1'b1;
            repeat (3) @(negedge clk);
        end
    endtask

    integer guard;

    // POST a transfer as soon as the request slot is free -- which is while the previous frame is still
    // running. The gap between frames is then entirely the controller's turnaround, which is the parameter
    // under test. Waiting for `done` first would add the requester's own latency to every gap and the sweep
    // would measure the bench.
    task automatic post(input [1:0] d, input [7:0] b);
        begin
            guard = 0;
            while (!req_free && guard < 4000) begin @(negedge clk); guard = guard + 1; end
            if (guard >= 4000) begin
                $display("  FAIL: the request slot never freed for device %0d", d);
                errors = errors + 1;
            end
            exp_dev_q[post_n % 8] = d;
            post_n = post_n + 1;
            dev = d; tx_byte = b; start = 1'b1;
            @(negedge clk); start = 1'b0;
        end
    endtask

    task automatic drain;
        begin
            guard = 0;
            while (busy && guard < 8000) begin @(negedge clk); guard = guard + 1; end
            if (guard >= 8000) begin
                $display("  FAIL: the controller never drained");
                errors = errors + 1;
            end
        end
    endtask

    integer k, mutations;
    integer TTAS [0:5];
    integer r_ov [0:5], r_hv[0:5], r_ct[0:5], r_wr[0:5], r_gap[0:5];
    integer first_hold_ok, first_ct_ok;

    initial begin
        cfg0 = 8'h20;              // cpol 0, cpha 0, divider 2
        cfg1 = 8'h21;              // cpol 1, cpha 0, divider 2
        cfg2 = 8'h20;              // cpol 0, cpha 0, divider 2
        lead = 8'd2; lag = 8'd2; tta = 8'd0;
        mutations = 0; n_checked = 0; n_wrong = 0; post_n = 0; chk_n = 0;
        slv_byte[0] = 8'hA5; slv_byte[1] = 8'h3C; slv_byte[2] = 8'h96;
        // Each model polices its own mode, taken from the same table the controller uses.
        slv_cpol[0] = cfg0[0]; slv_cpol[1] = cfg1[0]; slv_cpol[2] = cfg2[0];
        TTAS[0] = 0; TTAS[1] = 1; TTAS[2] = 2; TTAS[3] = 3; TTAS[4] = 4; TTAS[5] = 6;

        $display("  three devices: cfg0=%02h (cpol 0)  cfg1=%02h (cpol 1)  cfg2=%02h (cpol 0)   lead=%0d  lag=%0d",
                 cfg0, cfg1, cfg2, lead, lag);
        $display("  device requirements: SCLK must not move for %0d cycles after a deselect; MISO releases %0d cycles after a deselect",
                 T_HOLD, T_REL);
        $display("");
        $display("  tta  bus gap  cs overlap  hold violations  contention  wrong bytes  verdict");

        for (k = 0; k < 6; k = k + 1) begin
            tta = TTAS[k][7:0];
            reset_all;
            // Walk the three devices twice, so every device-to-device transition occurs -- including the
            // 0->1 and 1->2 changes where the parked level moves.
            post(2'd0, 8'h11);
            post(2'd1, 8'h22);
            post(2'd2, 8'h33);
            post(2'd0, 8'h44);
            post(2'd1, 8'h55);
            drain;
            repeat (40) @(negedge clk);
            r_ov[k] = n_overlap; r_hv[k] = n_hold_viol; r_ct[k] = n_contend; r_wr[k] = n_wrong;
            r_gap[k] = min_gap;
            $display("  %3d  %8d  %10d  %15d  %10d  %11d  %0s",
                     tta, r_gap[k], r_ov[k], r_hv[k], r_ct[k], r_wr[k],
                     (r_hv[k] == 0 && r_ct[k] == 0 && r_wr[k] == 0) ? "every requirement met"
                     : (r_ct[k] != 0 && r_hv[k] != 0) ? "hold violated AND MISO contested"
                     : (r_ct[k] != 0) ? "MISO still contested"
                                      : "hold still violated");
            if (r_ov[k] != 0) begin
                $display("  FAIL: %0d cycles with more than one select low at tta=%0d; the decoder is not one-hot",
                         r_ov[k], tta);
                errors = errors + 1;
            end
            if (n_checked == 0) begin
                $display("  FAIL: no frames completed at tta=%0d", tta);
                errors = errors + 1;
            end
        end

        // Find the two thresholds from the measurement rather than asserting them.
        first_hold_ok = -1; first_ct_ok = -1;
        for (k = 0; k < 6; k = k + 1) begin
            if (first_hold_ok < 0 && r_hv[k] == 0) first_hold_ok = TTAS[k];
            if (first_ct_ok   < 0 && r_ct[k] == 0) first_ct_ok   = TTAS[k];
        end

        if (first_hold_ok != T_HOLD) begin
            $display("  FAIL: hold violations stopped at tta=%0d where the requirement is %0d",
                     first_hold_ok, T_HOLD);
            errors = errors + 1;
        end
        // THE REQUIREMENT IS STATED AGAINST THE MEASURED GAP, so there is no hand-derived off-by-one to get
        // wrong: contention must be absent exactly when the observed bus gap reaches the device's release
        // time, and present when it does not.
        for (k = 0; k < 6; k = k + 1) begin
            if ((r_gap[k] >= T_REL) && (r_ct[k] != 0)) begin
                $display("  FAIL: at tta=%0d the bus gap was %0d cycles -- at least the %0d-cycle release time -- and contention was still reported %0d times",
                         TTAS[k], r_gap[k], T_REL, r_ct[k]);
                errors = errors + 1;
            end
            if ((r_gap[k] < T_REL) && (r_ct[k] == 0)) begin
                $display("  FAIL: at tta=%0d the bus gap was only %0d cycles against a %0d-cycle release time, and no contention was reported -- the detector is not working",
                         TTAS[k], r_gap[k], T_REL);
                errors = errors + 1;
            end
        end
        if (first_hold_ok == first_ct_ok) begin
            $display("  FAIL: both requirements were satisfied at the same turnaround, so the chapter's claim that the thresholds differ was not exercised");
            errors = errors + 1;
        end
        if (r_wr[5] != 0) begin
            $display("  FAIL: %0d wrong bytes at the largest turnaround, where every requirement is met",
                     r_wr[5]);
            errors = errors + 1;
        end

        $display("");
        $display("    1. the selects were ONE-HOT in every run -- zero cycles with two low, at every turnaround from %0d to %0d. That is what licenses the rest of the table to be read as a timing result: this controller's decoder is correct by construction, and it still violated two separate device requirements at the smaller settings. `Only one chip select at a time` is necessary and it is not sufficient",
                 TTAS[0], TTAS[5]);
        $display("    2. the CLOCK-HOLD violations came from the parked level moving too soon. Devices 0 and 1 have different CPOL, so SCLK must change level between them, and with no turnaround that change landed inside the previous device's hold window -- %0d violations at tta=0, and zero from tta=%0d, exactly the requirement. A controller serving identical devices never changes the parked level and never meets this failure, which is why it arrives only when a second device is added",
                 r_hv[0], first_hold_ok);
        $display("    3. the CONTENTION had a DIFFERENT threshold, and it is stated against the MEASURED bus gap rather than a hand-derived one. The previous device releases MISO %0d cycles after its deselect; the observed gap between one select rising and the next falling went %0d, %0d, %0d, %0d, %0d, %0d cycles as tta went %0d to %0d, and contention was present exactly while that gap was below %0d -- vanishing at tta=%0d, not at tta=%0d where the clock-hold requirement was already satisfied. Deriving the gap by hand as tta + lead gave an answer one cycle out, because the state machine spends a cycle passing through idle: the two requirements come from different sections of the datasheet, they are fixed by the same parameter, and the BINDING one is neither the one the CPOL story suggests nor the one arithmetic-on-paper predicts",
                 T_REL, r_gap[0], r_gap[1], r_gap[2], r_gap[3], r_gap[4], r_gap[5],
                 TTAS[0], TTAS[5], T_REL, first_ct_ok, first_hold_ok);
        $display("    4. and every received byte was checked against the ADDRESSED device's data at every setting. At tta=%0d, where both requirements are met, %0d frames were checked with %0d wrong. A turnaround sweep that counted only violations would pass a controller that met every timing requirement and talked to the wrong device",
                 TTAS[5], n_checked, r_wr[5]);

        // ---- BENCH INTEGRITY ----
        // Two deliberately wrong expectations, and proof the contention detector can fire at all.
        if (first_hold_ok != 99) mutations = mutations + 1;
        if (r_ct[0] != 0)        mutations = mutations + 1;
        if (mutations != 2) begin
            $display("  FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
            errors = errors + 1;
        end
        if (r_hv[0] == 0) begin
            $display("  FAIL: the hold-violation detector never fired, so its zero readings prove nothing");
            errors = errors + 1;
        end
        if (r_ct[0] == 0) begin
            $display("  FAIL: the contention detector never fired, so its zero readings prove nothing");
            errors = errors + 1;
        end

        if (errors == 0) begin
            $display("");
            $display("    and the bench proved itself: two deliberately wrong expectations mismatched, BOTH violation detectors were observed firing at tta=0 -- so their zero readings at the larger settings mean something -- and every device model polices its own datasheet requirement rather than reporting the controller's opinion of itself");
            $display("PASS: on a shared bus the failures live in the switch BETWEEN devices, and `only one chip select at a time` is necessary and not sufficient. The selects were one-hot in every run, at every turnaround, and the controller still violated two separate device requirements at the smaller settings. A per-device CPOL means SCLK's parked level must MOVE between devices, and with no turnaround that move landed inside the previous device's clock-hold window: %0d violations at tta=0, zero from tta=%0d. The previous device's MISO driver takes %0d cycles to release, and the next select falls tta + lead afterwards, so contention has a DIFFERENT threshold -- it was present exactly while the MEASURED bus gap was below that release time, and stopped at tta=%0d rather than %0d. Two requirements from different sections of the datasheet, fixed by the same parameter, with the binding one neither the one the CPOL story suggests nor the one hand-derived arithmetic predicts -- deriving the gap as tta + lead was a cycle out, because the state machine spends a cycle passing through idle. And every received byte was checked against the addressed device's data at every setting, because a sweep that counts only violations would pass a controller that met every timing requirement and talked to the wrong device",
                     r_hv[0], first_hold_ok, T_REL, first_ct_ok, first_hold_ok);
        end else begin
            $display("FAIL: %0d error(s)", errors);
        end
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_multi_slave_tb.v — the same bench in Verilog-2001
// spi_multi_slave_tb.v
//
// THREE DEVICES WITH DIFFERENT MODES, AND TWO REQUIREMENTS THAT DISAPPEAR AT DIFFERENT THRESHOLDS.
//
// The three slaves are modelled here with DATASHEET REQUIREMENTS rather than just behaviour: each one
// states how long SCLK must stay still after its select rises (`T_HOLD`) and how long its MISO driver takes
// to release (`T_REL`). The bench then counts violations of those requirements on the pins. That is the
// whole design of this bench: the controller is not asked whether it behaved, the devices are asked whether
// their requirements were met.
//
// THE FOUR RESULTS.
//
//   1. THE SELECTS ARE ONE-HOT AT EVERY SETTING. Zero cycles with two selects low, in every run -- which is
//      what makes the rest of the table a statement about TIMING rather than about the select logic. A
//      controller with a perfect one-hot decoder violates both device requirements below.
//
//   2. A CLOCK-HOLD VIOLATION IS CREATED BY THE PARKED LEVEL CHANGING TOO SOON. Devices 0 and 1 have
//      different CPOL, so the controller must move SCLK between them. With no turnaround the move lands
//      inside the previous device's hold window. The violations disappear at tta >= T_HOLD.
//
//   3. CONTENTION IS CREATED BY SELECTING THE NEXT DEVICE BEFORE THE PREVIOUS ONE HAS RELEASED MISO. That
//      threshold is DIFFERENT -- it is tta + lead >= T_REL -- so it disappears at a different value of the
//      same parameter, and the binding requirement is not the one the CPOL story would make you expect.
//
//   4. AND EVERY RECEIVED BYTE IS CHECKED, at every setting, against what the addressed device sent. A
//      turnaround sweep that only counted violations could pass a controller that met every timing
//      requirement and talked to the wrong device.

`timescale 1ns/1ps

module spi_multi_slave_tb;

    localparam NDEV  = 3;
    localparam CNT_W = 16;

    // The devices' datasheet numbers, in system-clock cycles.
    localparam T_HOLD = 2;    // SCLK must not move for this long after a select rises
    localparam T_REL  = 6;    // MISO is released this long after a select rises

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

    reg        start;
    reg [1:0]  dev;
    reg [7:0]  tx_byte;
    wire       busy, req_free;

    // Three devices, deliberately with DIFFERENT CPOL, so the parked level has to move between them.
    // Divider 2 everywhere, so each slave model has setup margin -- Chapter 19.1's boundary, avoided
    // rather than re-measured.
    // DECLARED WITHOUT INITIALISERS AND SET AT THE TOP OF `initial`, and the reason is a race rather than a
    // tool limitation.
    //
    // A declaration initialiser is applied during elaboration -- BEFORE time zero. A mechanical conversion
    // to Verilog hoists it into an `initial` block instead, which runs AT time zero, concurrently with every
    // other initial block and in an order the language does not define. So `reg [7:0] cfg0 = 8'h20;` is
    // "already 0x20 when simulation starts" in one file and "assigned at time zero, possibly after the
    // stimulus block has begun" in the other.
    //
    // The symptom was every configuration register reading X in the Verilog transcript's first line and
    // plausible values in the table underneath -- because the hoisted block happened to run after the
    // display and before the sweep. Nothing about the conversion is wrong; the two spellings genuinely
    // differ in WHEN they take effect.
    //
    // Setting stimulus explicitly at the top of the one initial block that uses it removes the race and is
    // identical in all three languages.
    reg [7:0]  cfg0, cfg1, cfg2;
    reg [7:0]  lead, lag, tta;

    wire            sclk, mosi;
    wire [NDEV-1:0] cs_n;
    wire            miso;
    wire [7:0]      rx_byte;
    wire            done;
    wire [CNT_W-1:0] n_frames;

    spi_multi_slave #(.NDEV(NDEV), .CNT_W(CNT_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .start(start), .dev(dev), .tx_byte(tx_byte), .busy(busy), .req_free(req_free),
        .cfg0(cfg0), .cfg1(cfg1), .cfg2(cfg2),
        .lead(lead), .lag(lag), .tta(tta),
        .sclk(sclk), .cs_n(cs_n), .mosi(mosi), .miso(miso),
        .rx_byte(rx_byte), .done(done), .n_frames(n_frames)
    );

    integer errors;

    // ------------------------------------------------------------------
    // THREE SLAVE MODELS, each policing its own requirements.
    //
    // `drv[i]` is high while device i is driving MISO. It goes high when its select falls and low T_REL
    // cycles after its select rises -- which is what creates contention if the next select comes too soon.
    // ------------------------------------------------------------------
    reg [7:0]  slv_byte [0:NDEV-1];
    reg [7:0]  slv_sh   [0:NDEV-1];
    reg [NDEV-1:0] drv;
    integer    rel_tmr [0:NDEV-1];
    integer    hold_tmr[0:NDEV-1];
    reg        slv_cpol [0:NDEV-1];
    reg [NDEV-1:0] cs_d;
    reg        sclk_d;

    // `$countones` is a SystemVerilog system function and does not exist in Verilog-2001, so the count is a
    // plain function -- which also keeps the three language versions doing arithmetic a reader can check.
        function integer ones3;
        input [NDEV-1:0] v;
        integer b;
        begin
            ones3 = 0;
            for (b = 0; b < NDEV; b = b + 1) ones3 = ones3 + ((v[b] === 1'b1) ? 1 : 0);
        end
    endfunction

    integer n_overlap, n_hold_viol, n_contend, n_checked, n_wrong;

    // THE BUS GAP IS MEASURED, NOT PREDICTED.
    //
    // The interval between one select rising and the next falling is what the MISO-release requirement is
    // actually about, and deriving it from `tta + lead` by hand produced an off-by-one: the state machine
    // spends a cycle passing through idle, and the bench's own edge detection costs another. Measuring the
    // gap on the pins removes both guesses and makes the requirement a comparison between two observed
    // numbers rather than between an observation and an assumption.
    integer gap_tmr, min_gap;
    reg     gap_run;

    integer j;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            drv <= {NDEV{1'b0}};
            cs_d <= {NDEV{1'b1}};
            sclk_d <= 1'b0;
            n_overlap <= 0; n_hold_viol <= 0; n_contend <= 0;
            gap_tmr <= 0; min_gap <= 9999; gap_run <= 1'b0;
            for (j = 0; j < NDEV; j = j + 1) begin
                slv_sh[j]   <= 8'h00;
                rel_tmr[j]  <= 0;
                hold_tmr[j] <= 0;
            end
        end else begin
            cs_d   <= cs_n;
            sclk_d <= sclk;

            // ---- requirement 1: one-hot selects ----
            //
            // Counted rather than assumed. The design makes two-low unreachable, and checking it anyway is
            // what licenses every other row of the table to be read as a timing result.
            if (ones3(~cs_n) > 1) n_overlap <= n_overlap + 1;

            for (j = 0; j < NDEV; j = j + 1) begin
                // a select falling: the device starts driving MISO and loads its byte
                if (cs_d[j] && !cs_n[j]) begin
                    drv[j]      <= 1'b1;
                    slv_sh[j]   <= slv_byte[j];
                    rel_tmr[j]  <= 0;
                    hold_tmr[j] <= 0;
                end
                // a select rising: both requirement timers start
                if (!cs_d[j] && cs_n[j]) begin
                    rel_tmr[j]  <= T_REL;
                    hold_tmr[j] <= T_HOLD;
                end
                // ---- requirement 2: MISO release ----
                if (rel_tmr[j] > 1) rel_tmr[j] <= rel_tmr[j] - 1;
                else if (rel_tmr[j] == 1) begin
                    rel_tmr[j] <= 0;
                    drv[j]     <= 1'b0;          // the driver finally lets go
                end
                // ---- requirement 3: SCLK must stay still after a deselect ----
                if (hold_tmr[j] > 0) begin
                    hold_tmr[j] <= hold_tmr[j] - 1;
                    if (sclk_d !== sclk) n_hold_viol <= n_hold_viol + 1;
                end
                // Shifting, for the device that is selected -- on ITS OWN trailing edge, which is the
                // transition back TO its idle level. The first version advanced on a falling edge for every
                // device, so the CPOL=1 device shifted on the wrong edge and returned a wrong byte at every
                // turnaround setting. A slave model that assumes one polarity cannot verify a controller
                // whose whole subject is serving devices with different ones.
                if (!cs_n[j] && (sclk_d !== sclk) && (sclk == slv_cpol[j]))
                    slv_sh[j] <= {slv_sh[j][6:0], 1'b0};
            end

            // ---- the bus gap: from any select rising to the next select falling ----
            if ((cs_d != {NDEV{1'b1}}) && (cs_n == {NDEV{1'b1}})) begin
                gap_run <= 1'b1;                 // everything just went high
                gap_tmr <= 1;
            end else if (gap_run && (cs_n == {NDEV{1'b1}})) begin
                gap_tmr <= gap_tmr + 1;
            end else if (gap_run && (cs_n != {NDEV{1'b1}})) begin
                gap_run <= 1'b0;
                if (gap_tmr < min_gap) min_gap <= gap_tmr;
            end

            // ---- requirement 2, observed: two devices driving MISO at once ----
            if (ones3(drv) > 1) n_contend <= n_contend + 1;
        end
    end

    // MISO: whichever device is driving presents its current bit. Two drivers give X, which is the honest
    // value for a contested net -- and the received byte is then wrong in a way no value comparison could
    // mistake for anything else.
    assign miso = (ones3(drv) > 1) ? 1'bx
                : drv[0] ? slv_sh[0][7]
                : drv[1] ? slv_sh[1][7]
                : drv[2] ? slv_sh[2][7] : 1'b0;

    // ---- per-frame checking: did the ADDRESSED device's byte arrive? ----
    //
    // The expected device is QUEUED, because posts run ahead of completions once the request slot is used.
    // A single `exp_dev` register would be overwritten by the next post before the current frame finished,
    // and every comparison would be against the wrong device -- a checker that is wrong in a way that looks
    // like a design fault.
    reg [1:0] exp_dev_q [0:7];
    integer   post_n, chk_n;
    always @(posedge clk) if (rst_n && done) begin
        n_checked = n_checked + 1;
        if (rx_byte !== slv_byte[exp_dev_q[chk_n % 8]]) n_wrong = n_wrong + 1;
        chk_n = chk_n + 1;
    end

    // ------------------------------------------------------------------
    task reset_all;
        begin
            @(negedge clk); rst_n = 1'b0;
            n_checked = 0; n_wrong = 0; post_n = 0; chk_n = 0;
            repeat (6) @(negedge clk);
            rst_n = 1'b1;
            repeat (3) @(negedge clk);
        end
    endtask

    integer guard;

    // POST a transfer as soon as the request slot is free -- which is while the previous frame is still
    // running. The gap between frames is then entirely the controller's turnaround, which is the parameter
    // under test. Waiting for `done` first would add the requester's own latency to every gap and the sweep
    // would measure the bench.
        task post;
        input [1:0] d;
        input [7:0] b;
        begin
            guard = 0;
            while (!req_free && guard < 4000) begin @(negedge clk); guard = guard + 1; end
            if (guard >= 4000) begin
                $display("  FAIL: the request slot never freed for device %0d", d);
                errors = errors + 1;
            end
            exp_dev_q[post_n % 8] = d;
            post_n = post_n + 1;
            dev = d; tx_byte = b; start = 1'b1;
            @(negedge clk); start = 1'b0;
        end
    endtask

    task drain;
        begin
            guard = 0;
            while (busy && guard < 8000) begin @(negedge clk); guard = guard + 1; end
            if (guard >= 8000) begin
                $display("  FAIL: the controller never drained");
                errors = errors + 1;
            end
        end
    endtask

    integer k, mutations;
    integer TTAS [0:5];
    integer r_ov [0:5], r_hv[0:5], r_ct[0:5], r_wr[0:5], r_gap[0:5];
    integer first_hold_ok, first_ct_ok;

    initial begin
        cfg0 = 8'h20;              // cpol 0, cpha 0, divider 2
        cfg1 = 8'h21;              // cpol 1, cpha 0, divider 2
        cfg2 = 8'h20;              // cpol 0, cpha 0, divider 2
        lead = 8'd2; lag = 8'd2; tta = 8'd0;
        mutations = 0; n_checked = 0; n_wrong = 0; post_n = 0; chk_n = 0;
        slv_byte[0] = 8'hA5; slv_byte[1] = 8'h3C; slv_byte[2] = 8'h96;
        // Each model polices its own mode, taken from the same table the controller uses.
        slv_cpol[0] = cfg0[0]; slv_cpol[1] = cfg1[0]; slv_cpol[2] = cfg2[0];
        TTAS[0] = 0; TTAS[1] = 1; TTAS[2] = 2; TTAS[3] = 3; TTAS[4] = 4; TTAS[5] = 6;

        $display("  three devices: cfg0=%02h (cpol 0)  cfg1=%02h (cpol 1)  cfg2=%02h (cpol 0)   lead=%0d  lag=%0d",
                 cfg0, cfg1, cfg2, lead, lag);
        $display("  device requirements: SCLK must not move for %0d cycles after a deselect; MISO releases %0d cycles after a deselect",
                 T_HOLD, T_REL);
        $display("");
        $display("  tta  bus gap  cs overlap  hold violations  contention  wrong bytes  verdict");

        for (k = 0; k < 6; k = k + 1) begin
            tta = TTAS[k][7:0];
            reset_all;
            // Walk the three devices twice, so every device-to-device transition occurs -- including the
            // 0->1 and 1->2 changes where the parked level moves.
            post(2'd0, 8'h11);
            post(2'd1, 8'h22);
            post(2'd2, 8'h33);
            post(2'd0, 8'h44);
            post(2'd1, 8'h55);
            drain;
            repeat (40) @(negedge clk);
            r_ov[k] = n_overlap; r_hv[k] = n_hold_viol; r_ct[k] = n_contend; r_wr[k] = n_wrong;
            r_gap[k] = min_gap;
            $display("  %3d  %8d  %10d  %15d  %10d  %11d  %0s",
                     tta, r_gap[k], r_ov[k], r_hv[k], r_ct[k], r_wr[k],
                     (r_hv[k] == 0 && r_ct[k] == 0 && r_wr[k] == 0) ? "every requirement met"
                     : (r_ct[k] != 0 && r_hv[k] != 0) ? "hold violated AND MISO contested"
                     : (r_ct[k] != 0) ? "MISO still contested"
                                      : "hold still violated");
            if (r_ov[k] != 0) begin
                $display("  FAIL: %0d cycles with more than one select low at tta=%0d; the decoder is not one-hot",
                         r_ov[k], tta);
                errors = errors + 1;
            end
            if (n_checked == 0) begin
                $display("  FAIL: no frames completed at tta=%0d", tta);
                errors = errors + 1;
            end
        end

        // Find the two thresholds from the measurement rather than asserting them.
        first_hold_ok = -1; first_ct_ok = -1;
        for (k = 0; k < 6; k = k + 1) begin
            if (first_hold_ok < 0 && r_hv[k] == 0) first_hold_ok = TTAS[k];
            if (first_ct_ok   < 0 && r_ct[k] == 0) first_ct_ok   = TTAS[k];
        end

        if (first_hold_ok != T_HOLD) begin
            $display("  FAIL: hold violations stopped at tta=%0d where the requirement is %0d",
                     first_hold_ok, T_HOLD);
            errors = errors + 1;
        end
        // THE REQUIREMENT IS STATED AGAINST THE MEASURED GAP, so there is no hand-derived off-by-one to get
        // wrong: contention must be absent exactly when the observed bus gap reaches the device's release
        // time, and present when it does not.
        for (k = 0; k < 6; k = k + 1) begin
            if ((r_gap[k] >= T_REL) && (r_ct[k] != 0)) begin
                $display("  FAIL: at tta=%0d the bus gap was %0d cycles -- at least the %0d-cycle release time -- and contention was still reported %0d times",
                         TTAS[k], r_gap[k], T_REL, r_ct[k]);
                errors = errors + 1;
            end
            if ((r_gap[k] < T_REL) && (r_ct[k] == 0)) begin
                $display("  FAIL: at tta=%0d the bus gap was only %0d cycles against a %0d-cycle release time, and no contention was reported -- the detector is not working",
                         TTAS[k], r_gap[k], T_REL);
                errors = errors + 1;
            end
        end
        if (first_hold_ok == first_ct_ok) begin
            $display("  FAIL: both requirements were satisfied at the same turnaround, so the chapter's claim that the thresholds differ was not exercised");
            errors = errors + 1;
        end
        if (r_wr[5] != 0) begin
            $display("  FAIL: %0d wrong bytes at the largest turnaround, where every requirement is met",
                     r_wr[5]);
            errors = errors + 1;
        end

        $display("");
        $display("    1. the selects were ONE-HOT in every run -- zero cycles with two low, at every turnaround from %0d to %0d. That is what licenses the rest of the table to be read as a timing result: this controller's decoder is correct by construction, and it still violated two separate device requirements at the smaller settings. `Only one chip select at a time` is necessary and it is not sufficient",
                 TTAS[0], TTAS[5]);
        $display("    2. the CLOCK-HOLD violations came from the parked level moving too soon. Devices 0 and 1 have different CPOL, so SCLK must change level between them, and with no turnaround that change landed inside the previous device's hold window -- %0d violations at tta=0, and zero from tta=%0d, exactly the requirement. A controller serving identical devices never changes the parked level and never meets this failure, which is why it arrives only when a second device is added",
                 r_hv[0], first_hold_ok);
        $display("    3. the CONTENTION had a DIFFERENT threshold, and it is stated against the MEASURED bus gap rather than a hand-derived one. The previous device releases MISO %0d cycles after its deselect; the observed gap between one select rising and the next falling went %0d, %0d, %0d, %0d, %0d, %0d cycles as tta went %0d to %0d, and contention was present exactly while that gap was below %0d -- vanishing at tta=%0d, not at tta=%0d where the clock-hold requirement was already satisfied. Deriving the gap by hand as tta + lead gave an answer one cycle out, because the state machine spends a cycle passing through idle: the two requirements come from different sections of the datasheet, they are fixed by the same parameter, and the BINDING one is neither the one the CPOL story suggests nor the one arithmetic-on-paper predicts",
                 T_REL, r_gap[0], r_gap[1], r_gap[2], r_gap[3], r_gap[4], r_gap[5],
                 TTAS[0], TTAS[5], T_REL, first_ct_ok, first_hold_ok);
        $display("    4. and every received byte was checked against the ADDRESSED device's data at every setting. At tta=%0d, where both requirements are met, %0d frames were checked with %0d wrong. A turnaround sweep that counted only violations would pass a controller that met every timing requirement and talked to the wrong device",
                 TTAS[5], n_checked, r_wr[5]);

        // ---- BENCH INTEGRITY ----
        // Two deliberately wrong expectations, and proof the contention detector can fire at all.
        if (first_hold_ok != 99) mutations = mutations + 1;
        if (r_ct[0] != 0)        mutations = mutations + 1;
        if (mutations != 2) begin
            $display("  FAIL: a deliberately wrong expectation did not mismatch (%0d of 2)", mutations);
            errors = errors + 1;
        end
        if (r_hv[0] == 0) begin
            $display("  FAIL: the hold-violation detector never fired, so its zero readings prove nothing");
            errors = errors + 1;
        end
        if (r_ct[0] == 0) begin
            $display("  FAIL: the contention detector never fired, so its zero readings prove nothing");
            errors = errors + 1;
        end

        if (errors == 0) begin
            $display("");
            $display("    and the bench proved itself: two deliberately wrong expectations mismatched, BOTH violation detectors were observed firing at tta=0 -- so their zero readings at the larger settings mean something -- and every device model polices its own datasheet requirement rather than reporting the controller's opinion of itself");
            $display("PASS: on a shared bus the failures live in the switch BETWEEN devices, and `only one chip select at a time` is necessary and not sufficient. The selects were one-hot in every run, at every turnaround, and the controller still violated two separate device requirements at the smaller settings. A per-device CPOL means SCLK's parked level must MOVE between devices, and with no turnaround that move landed inside the previous device's clock-hold window: %0d violations at tta=0, zero from tta=%0d. The previous device's MISO driver takes %0d cycles to release, and the next select falls tta + lead afterwards, so contention has a DIFFERENT threshold -- it was present exactly while the MEASURED bus gap was below that release time, and stopped at tta=%0d rather than %0d. Two requirements from different sections of the datasheet, fixed by the same parameter, with the binding one neither the one the CPOL story suggests nor the one hand-derived arithmetic predicts -- deriving the gap as tta + lead was a cycle out, because the state machine spends a cycle passing through idle. And every received byte was checked against the addressed device's data at every setting, because a sweep that counts only violations would pass a controller that met every timing requirement and talked to the wrong device",
                     r_hv[0], first_hold_ok, T_REL, first_ct_ok, first_hold_ok);
        end else begin
            $display("FAIL: %0d error(s)", errors);
        end
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b1;
        start = 1'b0;
        dev = 2'd0;
        tx_byte = 8'h00;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_multi_slave_tb.vhd — the same bench in VHDL
-- spi_multi_slave_tb.vhd
--
-- THREE DEVICES WITH DIFFERENT MODES, AND TWO REQUIREMENTS THAT DISAPPEAR AT DIFFERENT THRESHOLDS.
--
-- The three slaves are modelled here with DATASHEET REQUIREMENTS rather than just behaviour: each states how
-- long SCLK must stay still after its select rises (`T_HOLD_C`) and how long its MISO driver takes to release
-- (`T_REL_C`). The bench then counts violations of those requirements on the pins. That is the whole design
-- of this bench: the controller is not asked whether it behaved, the devices are asked whether their
-- requirements were met.
--
-- The same four results as the other two languages, with the same numbers.
--
-- MISO IS A RESOLVED SIGNAL WITH THREE DRIVERS, which is the one place this version is a better model than
-- the others. Each slave drives its own bit or 'Z', and contention produces 'X' through the language's own
-- resolution rather than through a hand-placed value -- so the X appears exactly where two devices genuinely
-- drive at once and nowhere else.
--
-- IDENTIFIER REVIEW (VHDL IS CASE-INSENSITIVE). `NDEV_C`, `T_HOLD_C`, `T_REL_C` and the `s_` prefixed drive
-- signals; nothing differs from anything else by case alone, and nothing collides with a reserved word.
--
-- RANGE DIRECTION: every vector is `downto`; every subprogram formal is constrained.

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.spi_multi_pkg.all;

entity spi_multi_slave_tb is
end entity spi_multi_slave_tb;

architecture tb of spi_multi_slave_tb is

    constant NDEV_C   : positive := 3;
    constant T_HOLD_C : natural  := 2;   -- SCLK must not move for this long after a select rises
    constant T_REL_C  : natural  := 6;   -- MISO is released this long after a select rises

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '1';
    signal run   : boolean   := true;

    signal s_start   : std_logic := '0';
    signal s_dev     : natural range 0 to 2 := 0;
    signal s_tx      : byte_t := (others => '0');
    signal busy, req_free : std_logic;

    -- Three devices, deliberately with DIFFERENT CPOL, so the parked level has to move between them.
    -- Divider 2 everywhere, so each slave model has setup margin -- Chapter 19.1's boundary, avoided rather
    -- than re-measured.
    signal s_cfg0 : byte_t := x"20";     -- cpol 0, cpha 0, divider 2
    signal s_cfg1 : byte_t := x"21";     -- cpol 1, cpha 0, divider 2
    signal s_cfg2 : byte_t := x"20";     -- cpol 0, cpha 0, divider 2

    signal s_lead : unsigned(7 downto 0) := to_unsigned(2, 8);
    signal s_lag  : unsigned(7 downto 0) := to_unsigned(2, 8);
    signal s_tta  : unsigned(7 downto 0) := to_unsigned(0, 8);

    signal sclk, mosi : std_logic;
    signal cs_n : std_logic_vector(NDEV_C - 1 downto 0);
    signal miso : std_logic;
    signal rx_byte : byte_t;
    signal done : std_logic;
    signal n_frames : natural;

    type byte_arr is array (natural range <>) of byte_t;
    type nat_arr  is array (natural range <>) of natural;

    signal slv_byte : byte_arr(0 to 2) := (x"A5", x"3C", x"96");
    signal slv_cpol : std_logic_vector(2 downto 0) := "001";
    signal slv_sh   : byte_arr(0 to 2) := (others => (others => '0'));
    signal drv      : std_logic_vector(2 downto 0) := (others => '0');

    signal n_overlap, n_hold_viol, n_contend : natural := 0;
    signal n_checked, n_wrong : natural := 0;
    signal min_gap : natural := 9999;

    -- THE REQUIREMENT TIMERS ARE SIGNALS, not process variables.
    --
    -- Each is loaded when a select rises and decremented every cycle, and the SystemVerilog and Verilog
    -- versions hold them in reg arrays assigned non-blockingly -- so a load takes effect AFTER the edge and
    -- the same cycle's decrement does not see it. As process variables the load is visible immediately, the
    -- decrement that follows in the same invocation consumes a cycle of the window, and both detectors
    -- measured a window one cycle too short: the hold detector reported zero violations where the other two
    -- languages reported three, and contention read four instead of eight.
    --
    -- Nothing about the design changed. The BENCH measured a different requirement, which is the more
    -- dangerous direction -- a device model that under-reports its own requirement passes a controller that
    -- violates it.
    signal rel_tmr, hold_tmr : nat_arr(0 to 2) := (others => 0);

    signal cs_d   : std_logic_vector(NDEV_C - 1 downto 0) := (others => '1');
    signal sclk_d : std_logic := '0';

    signal post_n, chk_n : natural := 0;
    signal exp_dev_q : nat_arr(0 to 7) := (others => 0);

    function i2s (v : integer; w : natural) return string is
        constant S : string          := integer'image(v);
        constant P : string(1 to 40) := (others => ' ');
    begin
        if S'length >= w then return S; end if;
        return P(1 to w - S'length) & S;
    end function i2s;

    function hex2 (v : byte_t) return string is
        constant D : string := "0123456789abcdef";
        variable u : natural := to_integer(unsigned(v));
        variable r : string(1 to 2);
    begin
        r(1) := D(u / 16 + 1);
        r(2) := D(u mod 16 + 1);
        return r;
    end function hex2;

    function ones3 (v : std_logic_vector) return natural is
        variable n : natural := 0;
    begin
        for i in v'range loop
            if v(i) = '1' then n := n + 1; end if;
        end loop;
        return n;
    end function ones3;

begin

    clk_gen : process is
    begin
        while run loop
            clk <= '0'; wait for 5 ns;
            clk <= '1'; wait for 5 ns;
        end loop;
        wait;
    end process clk_gen;

    dut : entity work.spi_multi_slave
        generic map (NDEV_C => NDEV_C, CNT_W => 16)
        port map (
            clk => clk, rst_n => rst_n,
            start => s_start, dev => s_dev, tx_byte => s_tx, busy => busy, req_free => req_free,
            cfg0 => s_cfg0, cfg1 => s_cfg1, cfg2 => s_cfg2,
            lead => s_lead, lag => s_lag, tta => s_tta,
            sclk => sclk, cs_n => cs_n, mosi => mosi, miso => miso,
            rx_byte => rx_byte, done => done, n_frames => n_frames
        );

    -- MISO: THREE DRIVERS ON ONE RESOLVED SIGNAL. Each device drives its bit or 'Z', and two driving at once
    -- resolves to 'X' through the language rather than through a hand-placed value.
    miso_drv : for i in 0 to NDEV_C - 1 generate
        miso <= slv_sh(i)(7) when drv(i) = '1' else 'Z';
    end generate miso_drv;
    -- A weak pull-down so a bus nobody drives reads as 0 rather than 'Z'.
    miso <= 'L';

    dly : process (clk, rst_n) is
    begin
        if rst_n = '0' then
            cs_d <= (others => '1'); sclk_d <= '0';
        elsif rising_edge(clk) then
            cs_d   <= cs_n;
            sclk_d <= sclk;
        end if;
    end process dly;

    -- THE THREE SLAVE MODELS, each policing its own requirements.
    slaves : process (clk, rst_n) is
        variable gap_tmr : natural;
        variable gap_run : boolean;
    begin
        if rst_n = '0' then
            drv <= (others => '0');
            for j in 0 to NDEV_C - 1 loop
                slv_sh(j)   <= (others => '0');
                rel_tmr(j)  <= 0;
                hold_tmr(j) <= 0;
            end loop;
            n_overlap <= 0; n_hold_viol <= 0; n_contend <= 0;
            min_gap <= 9999;
            gap_tmr := 0; gap_run := false;

        elsif rising_edge(clk) then
            -- requirement 1: one-hot selects. Counted rather than assumed -- the design makes two-low
            -- unreachable, and checking it anyway is what licenses every other row to be read as timing.
            if ones3(not cs_n) > 1 then n_overlap <= n_overlap + 1; end if;

            for j in 0 to NDEV_C - 1 loop
                if cs_d(j) = '1' and cs_n(j) = '0' then
                    drv(j)     <= '1';
                    slv_sh(j)   <= slv_byte(j);
                    rel_tmr(j)  <= 0;
                    hold_tmr(j) <= 0;
                end if;
                if cs_d(j) = '0' and cs_n(j) = '1' then
                    rel_tmr(j)  <= T_REL_C;
                    hold_tmr(j) <= T_HOLD_C;
                end if;
                -- requirement 2: MISO release
                if rel_tmr(j) > 1 then
                    rel_tmr(j) <= rel_tmr(j) - 1;
                elsif rel_tmr(j) = 1 then
                    rel_tmr(j) <= 0;
                    drv(j) <= '0';                 -- the driver finally lets go
                end if;
                -- requirement 3: SCLK must stay still after a deselect
                if hold_tmr(j) > 0 then
                    hold_tmr(j) <= hold_tmr(j) - 1;
                    if sclk_d /= sclk then n_hold_viol <= n_hold_viol + 1; end if;
                end if;
                -- shifting, for the device that is selected -- on ITS OWN trailing edge, which is the
                -- transition back TO its idle level. A model that assumes one polarity cannot verify a
                -- controller whose subject is serving devices with different ones.
                if cs_n(j) = '0' and sclk_d /= sclk and sclk = slv_cpol(j) then
                    slv_sh(j) <= slv_sh(j)(6 downto 0) & '0';
                end if;
            end loop;

            -- the bus gap: from any select rising to the next select falling. MEASURED, because deriving it
            -- as tta + lead is one cycle out -- the state machine spends a cycle passing through idle.
            if cs_d /= (cs_d'range => '1') and cs_n = (cs_n'range => '1') then
                gap_run := true;
                gap_tmr := 1;
            elsif gap_run and cs_n = (cs_n'range => '1') then
                gap_tmr := gap_tmr + 1;
            elsif gap_run and cs_n /= (cs_n'range => '1') then
                gap_run := false;
                if gap_tmr < min_gap then min_gap <= gap_tmr; end if;
            end if;

            -- requirement 2, observed: two devices driving MISO at once
            if ones3(drv) > 1 then n_contend <= n_contend + 1; end if;
        end if;
    end process slaves;

    -- Per-frame checking. The expected device is QUEUED, because posts run ahead of completions once the
    -- request slot is used: a single register would be overwritten by the next post before the current frame
    -- finished, and every comparison would be against the wrong device.
    chk : process (clk, rst_n) is
    begin
        if rst_n = '0' then
            n_checked <= 0; n_wrong <= 0; chk_n <= 0;
        elsif rising_edge(clk) then
            if done = '1' then
                n_checked <= n_checked + 1;
                if rx_byte /= slv_byte(exp_dev_q(chk_n mod 8)) then
                    n_wrong <= n_wrong + 1;
                end if;
                chk_n <= chk_n + 1;
            end if;
        end if;
    end process chk;

    stim : process is

        variable e, mutations, guard : natural := 0;
        constant TTAS_C : nat_arr(0 to 5) := (0, 1, 2, 3, 4, 6);
        variable r_ov, r_hv, r_ct, r_wr, r_gap : nat_arr(0 to 5);
        variable first_hold_ok, first_ct_ok : integer := -1;
        variable ln : line;

        procedure reset_all is
        begin
            wait until falling_edge(clk);
            rst_n <= '0';
            post_n <= 0;
            for i in 1 to 6 loop wait until falling_edge(clk); end loop;
            rst_n <= '1';
            for i in 1 to 3 loop wait until falling_edge(clk); end loop;
        end procedure reset_all;

        -- POST a transfer as soon as the request slot is free -- which is while the previous frame is still
        -- running. The gap between frames is then entirely the controller's turnaround.
        procedure post (d : natural; b : byte_t) is
        begin
            guard := 0;
            while req_free = '0' and guard < 4000 loop
                wait until falling_edge(clk); guard := guard + 1;
            end loop;
            if guard >= 4000 then
                write(ln, string'("  FAIL: the request slot never freed"));
                writeline(output, ln); e := e + 1;
            end if;
            exp_dev_q(post_n mod 8) <= d;
            post_n <= post_n + 1;
            s_dev <= d; s_tx <= b; s_start <= '1';
            wait until falling_edge(clk);
            s_start <= '0';
        end procedure post;

        procedure drain is
        begin
            guard := 0;
            while busy = '1' and guard < 8000 loop
                wait until falling_edge(clk); guard := guard + 1;
            end loop;
            if guard >= 8000 then
                write(ln, string'("  FAIL: the controller never drained"));
                writeline(output, ln); e := e + 1;
            end if;
        end procedure drain;

    begin
        write(ln, string'("  three devices: cfg0=") & hex2(s_cfg0) & string'(" (cpol 0)  cfg1=")
                  & hex2(s_cfg1) & string'(" (cpol 1)  cfg2=") & hex2(s_cfg2)
                  & string'(" (cpol 0)   lead=2  lag=2"));
        writeline(output, ln);
        write(ln, string'("  device requirements: SCLK must not move for ") & i2s(T_HOLD_C, 1)
                  & string'(" cycles after a deselect; MISO releases ") & i2s(T_REL_C, 1)
                  & string'(" cycles after a deselect"));
        writeline(output, ln);
        write(ln, string'(""));
        writeline(output, ln);
        write(ln, string'("  tta  bus gap  cs overlap  hold violations  contention  wrong bytes  verdict"));
        writeline(output, ln);

        for k in 0 to 5 loop
            s_tta <= to_unsigned(TTAS_C(k), 8);
            reset_all;
            -- Walk the three devices twice, so every device-to-device transition occurs -- including the
            -- 0->1 and 1->2 changes where the parked level moves.
            post(0, x"11");
            post(1, x"22");
            post(2, x"33");
            post(0, x"44");
            post(1, x"55");
            drain;
            for i in 1 to 40 loop wait until falling_edge(clk); end loop;
            r_ov(k) := n_overlap; r_hv(k) := n_hold_viol; r_ct(k) := n_contend;
            r_wr(k) := n_wrong;   r_gap(k) := min_gap;
            write(ln, string'("  ") & i2s(TTAS_C(k), 3) & string'("  ") & i2s(r_gap(k), 8)
                      & string'("  ") & i2s(r_ov(k), 10) & string'("  ") & i2s(r_hv(k), 15)
                      & string'("  ") & i2s(r_ct(k), 10) & string'("  ") & i2s(r_wr(k), 11)
                      & string'("  "));
            if r_hv(k) = 0 and r_ct(k) = 0 and r_wr(k) = 0 then
                write(ln, string'("every requirement met"));
            elsif r_ct(k) /= 0 and r_hv(k) /= 0 then
                write(ln, string'("hold violated AND MISO contested"));
            elsif r_ct(k) /= 0 then
                write(ln, string'("MISO still contested"));
            else
                write(ln, string'("hold still violated"));
            end if;
            writeline(output, ln);
            if r_ov(k) /= 0 then
                write(ln, string'("  FAIL: cycles with more than one select low; the decoder is not one-hot"));
                writeline(output, ln); e := e + 1;
            end if;
            if n_checked = 0 then
                write(ln, string'("  FAIL: no frames completed"));
                writeline(output, ln); e := e + 1;
            end if;
        end loop;

        for k in 0 to 5 loop
            if first_hold_ok < 0 and r_hv(k) = 0 then first_hold_ok := TTAS_C(k); end if;
            if first_ct_ok   < 0 and r_ct(k) = 0 then first_ct_ok   := TTAS_C(k); end if;
        end loop;

        if first_hold_ok /= T_HOLD_C then
            write(ln, string'("  FAIL: hold violations stopped at tta=") & i2s(first_hold_ok, 1)
                      & string'(" where the requirement is ") & i2s(T_HOLD_C, 1));
            writeline(output, ln); e := e + 1;
        end if;
        -- The requirement is stated against the MEASURED gap, so there is no hand-derived off-by-one.
        for k in 0 to 5 loop
            if r_gap(k) >= T_REL_C and r_ct(k) /= 0 then
                write(ln, string'("  FAIL: the bus gap reached the release time and contention was still reported"));
                writeline(output, ln); e := e + 1;
            end if;
            if r_gap(k) < T_REL_C and r_ct(k) = 0 then
                write(ln, string'("  FAIL: the bus gap was below the release time and no contention was reported -- the detector is not working"));
                writeline(output, ln); e := e + 1;
            end if;
        end loop;
        if first_hold_ok = first_ct_ok then
            write(ln, string'("  FAIL: both requirements were satisfied at the same turnaround, so the chapter's claim that the thresholds differ was not exercised"));
            writeline(output, ln); e := e + 1;
        end if;
        if r_wr(5) /= 0 then
            write(ln, string'("  FAIL: wrong bytes at the largest turnaround, where every requirement is met"));
            writeline(output, ln); e := e + 1;
        end if;

        write(ln, string'(""));
        writeline(output, ln);
        write(ln, string'("    1. the selects were ONE-HOT in every run -- zero cycles with two low, at every turnaround from ")
                  & i2s(TTAS_C(0), 1) & string'(" to ") & i2s(TTAS_C(5), 1)
                  & string'(". That is what licenses the rest of the table to be read as a timing result: this controller's decoder is correct by construction, and it still violated two separate device requirements at the smaller settings. `Only one chip select at a time` is necessary and it is not sufficient"));
        writeline(output, ln);
        write(ln, string'("    2. the CLOCK-HOLD violations came from the parked level moving too soon. Devices 0 and 1 have different CPOL, so SCLK must change level between them, and with no turnaround that change landed inside the previous device's hold window -- ")
                  & i2s(r_hv(0), 1) & string'(" violations at tta=0, and zero from tta=")
                  & i2s(first_hold_ok, 1)
                  & string'(", exactly the requirement. A controller serving identical devices never changes the parked level and never meets this failure, which is why it arrives only when a second device is added"));
        writeline(output, ln);
        write(ln, string'("    3. the CONTENTION had a DIFFERENT threshold, and it is stated against the MEASURED bus gap rather than a hand-derived one. The previous device releases MISO ")
                  & i2s(T_REL_C, 1)
                  & string'(" cycles after its deselect; the observed gap between one select rising and the next falling went ")
                  & i2s(r_gap(0), 1) & string'(", ") & i2s(r_gap(1), 1) & string'(", ")
                  & i2s(r_gap(2), 1) & string'(", ") & i2s(r_gap(3), 1) & string'(", ")
                  & i2s(r_gap(4), 1) & string'(", ") & i2s(r_gap(5), 1)
                  & string'(" cycles as tta went ") & i2s(TTAS_C(0), 1) & string'(" to ")
                  & i2s(TTAS_C(5), 1) & string'(", and contention was present exactly while that gap was below ")
                  & i2s(T_REL_C, 1) & string'(" -- vanishing at tta=") & i2s(first_ct_ok, 1)
                  & string'(", not at tta=") & i2s(first_hold_ok, 1)
                  & string'(" where the clock-hold requirement was already satisfied. Deriving the gap by hand as tta + lead gave an answer one cycle out, because the state machine spends a cycle passing through idle: the two requirements come from different sections of the datasheet, they are fixed by the same parameter, and the BINDING one is neither the one the CPOL story suggests nor the one arithmetic-on-paper predicts"));
        writeline(output, ln);
        write(ln, string'("    4. and every received byte was checked against the ADDRESSED device's data at every setting. At tta=")
                  & i2s(TTAS_C(5), 1) & string'(", where both requirements are met, ") & i2s(n_checked, 1)
                  & string'(" frames were checked with ") & i2s(r_wr(5), 1)
                  & string'(" wrong. A turnaround sweep that counted only violations would pass a controller that met every timing requirement and talked to the wrong device"));
        writeline(output, ln);

        -- ---- BENCH INTEGRITY ----
        if first_hold_ok /= 99 then mutations := mutations + 1; end if;
        if r_ct(0) /= 0         then mutations := mutations + 1; end if;
        if mutations /= 2 then
            write(ln, string'("  FAIL: a deliberately wrong expectation did not mismatch (")
                      & i2s(mutations, 1) & string'(" of 2)"));
            writeline(output, ln); e := e + 1;
        end if;
        if r_hv(0) = 0 then
            write(ln, string'("  FAIL: the hold-violation detector never fired, so its zero readings prove nothing"));
            writeline(output, ln); e := e + 1;
        end if;
        if r_ct(0) = 0 then
            write(ln, string'("  FAIL: the contention detector never fired, so its zero readings prove nothing"));
            writeline(output, ln); e := e + 1;
        end if;

        if e = 0 then
            write(ln, string'(""));
            writeline(output, ln);
            write(ln, string'("    and the bench proved itself: two deliberately wrong expectations mismatched, BOTH violation detectors were observed firing at tta=0 -- so their zero readings at the larger settings mean something -- and every device model polices its own datasheet requirement rather than reporting the controller's opinion of itself"));
            writeline(output, ln);
            write(ln, string'("PASS: on a shared bus the failures live in the switch BETWEEN devices, and `only one chip select at a time` is necessary and not sufficient. The selects were one-hot in every run, at every turnaround, and the controller still violated two separate device requirements at the smaller settings. A per-device CPOL means SCLK's parked level must MOVE between devices, and with no turnaround that move landed inside the previous device's clock-hold window: ")
                      & i2s(r_hv(0), 1) & string'(" violations at tta=0, zero from tta=")
                      & i2s(first_hold_ok, 1) & string'(". The previous device's MISO driver takes ")
                      & i2s(T_REL_C, 1)
                      & string'(" cycles to release, and the next select falls tta + lead afterwards, so contention has a DIFFERENT threshold -- it was present exactly while the MEASURED bus gap was below that release time, and stopped at tta=")
                      & i2s(first_ct_ok, 1) & string'(" rather than ") & i2s(first_hold_ok, 1)
                      & string'(". Two requirements from different sections of the datasheet, fixed by the same parameter, with the binding one neither the one the CPOL story suggests nor the one hand-derived arithmetic predicts -- deriving the gap as tta + lead was a cycle out, because the state machine spends a cycle passing through idle. And every received byte was checked against the addressed device's data at every setting, because a sweep that counts only violations would pass a controller that met every timing requirement and talked to the wrong device"));
            writeline(output, ln);
        else
            write(ln, string'("FAIL: ") & i2s(e, 1) & string'(" error(s)"));
            writeline(output, ln);
        end if;

        run <= false;
        wait;
    end process stim;

end architecture tb;

6. The Assertions This Design Deserves

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   at most one select is low at any time
   SCLK does not change while every select is high AND the hold window is open
   the latched configuration does not change between a frame's start and its end
   the interval between one select rising and the next falling is at least the
     largest release time among the devices

7. Coverage For A Shared Bus

The interesting coverage is not each device was accessed. It is each ordered pair of devices, because the failure lives in the transition:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   3 devices → 9 ordered pairs, of which 6 are device changes
   of those 6, the ones that change CPOL are the ones that move the parked level

8. FPGA And ASIC Implementation

The selects are ordinary outputs and should be registered straight to their pads, like SCLK. What matters more is that they are not generated from separate comparators per device: N comparators can each be correct and still overlap during a decode transition, whereas one index through one decoder cannot. On an FPGA that is also the cheaper structure.

The turnaround parameter is a timing budget, not a tuning knob. Its correct value is max(hold time, release time − lead) over every device fitted, computed from datasheets in the units of your system clock. A value found by reducing it until the link stops working is a value that fails at temperature, and — worse — a value that fails when somebody fits a different part in the same footprint.

On an ASIC, the per-device configuration table is small enough to be flops rather than memory, and it should be software-writable: the devices on the board are not known when the chip is made. And the turnaround must be software-writable for the same reason. A controller with a hard-coded turnaround is a controller that constrains which parts can be fitted, which is not a decision the RTL should be making.

9. Failure Signature — "The Flash Works Until We Talk To The ADC"

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   Symptom     a serial flash on a shared bus returns corrupt data, but only
               in the build that also polls an ADC on the same bus
   Checked     the flash's own frames on a scope -- correct, every time
   Checked     the ADC's frames -- correct, every time
   Concluded   a bus loading or signal-integrity problem
   Actioned    series resistors, then a slower SCLK for both devices
   Result      improved but not fixed
   Actual      the ADC used mode 3 and the flash mode 0, so the controller
               changed SCLK's parked level between them -- inside the flash's
               clock-hold window, on every handover
   Found       by an engineer who captured the gap BETWEEN frames instead of
               the frames

Both devices' frames were correct because the fault is not in a frame. It is in the interval between two frames, which is exactly the part of a capture that gets trimmed away as uninteresting — and the trigger that finds it is a deselect rather than a select.

The slower SCLK helped for a reason that misleads: it lengthened the whole schedule including the turnaround, which reduced the violation rate without addressing it. A change that improves a symptom without touching its cause is the most expensive kind of progress, and this module has now met it three times — the mode sweep in Chapter 18.2, the rate reduction in Chapter 18.6, and this.

10. Common Misconceptions

MisconceptionWhat is actually true
One-hot selects are the multi-slave requirementThey are necessary and insufficient; two timing requirements remain
A shared bus fails because of loadingIt fails in the handover long before it fails electrically
Changing CPOL between devices is a configuration detailIt is an edge on a shared wire, inside the previous device's hold window
MISO is released when the select risesIt is released some datasheet time after the select rises
The turnaround can be tuned until the link worksIts value is max(hold, release − lead) from datasheets, or it fails at temperature
Walking the devices in order covers the transitionsA fixed order covers a third of the ordered pairs
A correct frame means a correct transferBoth devices' frames were perfect in section 9

11. Reason It Through

12. Understanding Check

13. Summary

On a shared bus the failures live in the switch between devices, and only one chip select at a time is necessary and not sufficient. The selects were one-hot in every run at every turnaround — the decoder is one index through one decoder, so two low at once is unreachable — and the controller still violated two separate device requirements at the smaller settings.

A per-device CPOL means SCLK's parked level must move between devices, and with no turnaround that move landed inside the previous device's clock-hold window: 3 violations at tta = 0, zero from tta = 2. The previous device's MISO driver takes 6 cycles to release, and the next select falls a measured gap afterwards, so contention has a different threshold — present exactly while the measured bus gap was below the release time, and stopping at tta = 3 rather than 2.

Two requirements from different sections of the datasheet, fixed by the same parameter, with the binding one neither the one the CPOL story suggests nor the one hand-derived arithmetic predicts — deriving the gap as tta + lead was a cycle out, because the state machine spends a cycle passing through idle. That is why the requirement is stated against a measured interval.

Three of this chapter's defects were in the bench, and all three made a correct controller look broken or a broken one look correct: a requester that waited for done made its own latency part of every gap and reported zero hold violations at every setting; a slave model that advanced on a falling edge for every device returned wrong bytes for the CPOL 1 part; and requirement timers held in VHDL process variables measured a window one cycle too short, so a device model under-reported its own requirement. A device model that is less configurable than the controller silently tests one mode and reports on all of them.

14. Where This Track Goes Next

Module 19 has taken SPI from an isolated block to a subsystem: a streaming converter under a hard deadline, an event-driven sensor whose notification can expire, a register-mapped controller with software in the loop, and a shared bus whose failures live between the frames. Each chapter reused the timing, crossing and RTL reasoning the earlier modules built, and each one found its faults by measuring a requirement rather than by inspecting a design.

Module 20 is the capstone: one configurable SPI controller carried from a written specification through microarchitecture, RTL, constraints and CDC, a checking layer, coverage closure, deliberate bug injection, and a final design review — the whole track assembled into a single deliverable that has to survive being questioned.

Continue learning