Skip to content
VLSI Mentor

SPI · Module 13

Synthesizable Master Architecture and RTL Review

Seven blocks assembled behind the register interface software actually sees, the six rules that make it synthesisable, the review checklist that would gate it, and the bug a pin-level slave model found that no unit test could.

Ten chapters, seven blocks, every one verified alone. This chapter assembles them, gives software a way to drive the result, and closes with the review that would gate it.

The top level of a well-partitioned design should contain almost no logic. Why is that a useful test rather than an aesthetic preference?

Because a top level with interesting logic in it is a top level where a decision was made in the wrong place. Every judgement worth arguing about was made in Chapters 13.1 through 13.10, and the only thing left is wiring plus a register file.

1. The Assembly

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   13.4  spi_clkdiv_strobe   SCLK, and the two edge strobes
   13.5  spi_mode_edges      which edge launches, which captures
   13.6  spi_shift_datapath  the two shift registers
   13.8  spi_width_order     width and bit order, wrapping 13.6
   13.7  spi_cs_ctrl         chip select, lead, lag, gap, and abort
   13.9  spi_multibyte       the shadow register, streaming, overrun
   13.10 spi_abort_ctl       abort policy and what was lost

Plus the register file, which is the only thing in this file that is not an instance.

The complete SPI master. A register interface with seven addresses feeds a configuration snapshot, a transmit queue write, a receive read and a command register. The configuration reaches the clock divider, the mode logic, the width and order wrapper and the chip-select controller. The divider produces edge strobes for the mode logic, which produces launch and capture strobes for the datapath. The chip-select controller gates the divider and drives the select pins. The streaming controller holds a shadow word and a received word. The abort policy gates the request and pulses the controller. Three output pins and one input pin.Register file7 addresses, all softwareseesConfig snapshot13.2 — stable for the frameStreaming13.9 — shadow and overrunAbort policy13.10 — decide and reportCS control13.7 — the only pin ownerClock divider13.4 — SCLK and two strobesMode logic13.5 — launch or captureWidth and order13.8 — boundary transformShift datapath13.6 — two registersSCLKa registered outputMOSI / MISOa flop out, one flop inCS pinsone low, everwritescommanddiv, cpolcpha, lenlen, ordertiming, selrequestgated req, abortenableedge A, edge Blaunch, captureshaped word12
Figure 1 — the complete master. The transmit queue and the received word are written and read through the register file, alongside the configuration. Software touches only the register file; nothing in the SCLK-rate column reads it, because everything arrives through the configuration snapshot of Chapter 13.2. The abort policy gates the streaming controller's request and pulses the chip-select controller, which is the only block that drives a select pin. MISO enters one flop and goes nowhere else.

2. The Register Map

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   0x00  CTRL     [0] cpol  [1] cpha  [2] lsb_first
                  [15:8] div      SCLK period in system clocks, >= 2
                  [21:16] len     bits per frame, 1 .. MAX_W
                  [25:24] sel     which slave
   0x04  TIMING   [7:0] lead   [15:8] lag   [23:16] gap    (system clocks)
   0x08  TXDATA   write: queue a word, transaction CONTINUES after it
   0x0C  TXLAST   write: queue a word, transaction ENDS after it
   0x10  RXDATA   read:  the received word; reading POPS it
   0x14  STATUS   read only
   0x18  CMD      write: [0] abort   [1] clear the sticky flags

Three decisions in that map are worth defending.

Two addresses for one queue. TXDATA and TXLAST write the same shadow and differ only in the end-of-transaction flag. The alternatives are worse: a flag bit inside the data word costs a bit of the data width, and at MAX_W = 32 there is none to spare; a separate "this is the last one" register written before the data is two bus cycles per word and a race if an interrupt lands between them. An address bit is free.

There is a second argument that matters more in practice. A driver that forgets to set a flag bit leaves the transaction open and the slave selected — and forgetting a bit is easy. Forgetting to use a different address for the last word is a different kind of mistake, and the two addresses make the intent visible at the call site rather than in a bitfield.

STATUS is read-only and complete. Every error the design can detect appears there: not just the ones a driver is expected to handle, but the ones that mean the driver is wrong — a divisor below two, a frame width the datapath cannot hold, a slave that is not fitted. A design that silently absorbs those is a design whose bring-up consists of guessing.

Reset defaults make the master inert, not merely defined. Mode 0, the slowest legal divisor, one byte, and generous timing. A reset default that happens to be a fast clock is a reset default that can violate a slave's timing before software has executed a single instruction — and the first thing many drivers do is issue a read to identify the device.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   CTRL    0x0008_FF00      mode 0, div 255, 8 bits, slave 0
   TIMING  0x0010_1010      16 cycles of lead, lag and gap

3. The Six Rules That Make It Synthesisable

One clock. clk, everywhere. Nothing is clocked on SCLK, which is what the edge strobes of Chapter 13.4 exist to make possible. One clock means no generated-clock constraints, no clock-domain crossings, and no synchronisers.

One reset, asynchronous, on every flop that drives a pin. Required rather than preferred, for the reason Chapter 13.10 gives: a reset asserted while the clock is stopped must still release chip select.

No latches. Every combinational block assigns every output on every path. The read mux assigns 32'h0 first and then overwrites, so the default arm and every incomplete case are covered by construction rather than by inspection.

No gated or generated clocks. SCLK is a registered output that happens to look like a clock. Not clk & en, which needs a clock-gating cell and a constraint; not counter[N], which becomes a clock tree.

No combinational path from miso to any output pin. MISO is an asynchronous input from another device, so it is sampled into a flop and used nowhere else.

No initial blocks, no delays, no $ calls outside simulation guards. The assertions in the RTL are inside an ifdef the testbench defines and synthesis does not.

4. A Complete Transaction

Twenty-one cycles across six rows showing a register-driven transaction. A write row marks writes to the control, timing and transmit registers. A chip-select row goes low once and stays low across all eight frames. An SCLK row runs in bursts separated by the inter-frame hold. A transmit-ready row toggles as the shadow empties and refills. A busy row is high for the whole transaction. A receive-ready row pulses once per frame.CTRL and TIMING written onceCTRL and TIMING writtenoncefirst TXDATA write: busy risesfirst TXDATA write: busyrisesTXLAST: the transaction will endTXLAST: the transactionwill endreg_wrCTTDDDDDDDDDDDDDDLLLLcs_nsclktx_readybusyrx_readyt0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15t16t17t18t19t20
Figure 2 — a flash READ driven entirely through the register interface. Software writes CTRL and TIMING once, then queues eight words — seven to TXDATA and the last to TXLAST — reading RXDATA between them. One chip-select assertion covers the command, the three address bytes and the four data bytes; the status register's busy bit is what software polls, and the transaction's end is a select that rises.

5. Building the Master — Three HDLs

The circuit

Seven instances and a register file. Two details in the register file are worth naming:

abort_pend converts a register write into a control signal. The abort controller wants a level held until it has acknowledged; software writes a bit once. Latching it here is the whole of the difference, and doing it anywhere else would require a driver to poll the status register fast enough to keep a bit asserted — which is not a thing software can promise.

status is assembled by field rather than by concatenation. A concatenation has to be exactly 32 bits wide and the padding depends on the parameters, so a parameter change turns into a silently misaligned status register — and with LEN_W at its default the padding expressions come out zero bits wide, which is not even legal. Indexed part-selects into a zeroed word cannot drift.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top.sv — seven instances, one register file, and no decisions
// spi_master_top.sv
//
// Chapter 13.11 -- the whole master, and the interface software actually sees.
//
// Seven blocks, wired together, plus the one thing none of them has: a way for
// software to drive it. This file is deliberately almost all structure. Every
// design decision worth arguing about was made in Chapters 13.1 through 13.10,
// and a top level that has interesting logic in it is a top level where a
// decision was made in the wrong place.
//
//     13.4  spi_clkdiv_strobe   SCLK, and the two edge strobes
//     13.5  spi_mode_edges      which edge launches, which captures
//     13.6  spi_shift_datapath  the two shift registers
//     13.8  spi_width_order     width and bit order, wrapping 13.6
//     13.7  spi_cs_ctrl         chip select, lead, lag, gap, and abort
//     13.9  spi_multibyte       the shadow register, streaming, overrun
//     13.10 spi_abort_ctl       abort policy and what was lost
//
// THE REGISTER MAP.
//
//   0x00  CTRL     [0] cpol  [1] cpha  [2] lsb_first
//                  [15:8] div (SCLK period in clocks, >= 2)
//                  [21:16] len (bits per frame, 1..MAX_W)
//                  [25:24] sel (which slave)
//   0x04  TIMING   [7:0] lead   [15:8] lag   [23:16] gap   (system clocks)
//   0x08  TXDATA   write: queue a word, transaction CONTINUES after it
//   0x0C  TXLAST   write: queue a word, transaction ENDS after it
//   0x10  RXDATA   read:  the received word; reading POPS it
//   0x14  STATUS   read only
//   0x18  CMD      write: [0] abort   [1] clear the sticky flags
//
// TWO ADDRESSES FOR ONE FIFO. `TXDATA` and `TXLAST` write the same queue and
// differ only in the end-of-transaction flag. The alternative -- a flag bit
// inside the data word -- costs a bit of the data width, and at MAX_W = 32 there
// is no bit to spare. The alternative after that -- a separate "this is the
// last one" register written before the data -- is two bus cycles per word and
// a race if an interrupt lands between them. An address bit is free.
//
// STATUS IS READ-ONLY AND COMPLETE. Every error the design can detect appears
// here: not just the ones a driver is expected to handle, but the ones that mean
// the DRIVER is wrong -- a divisor below two, a frame width the datapath cannot
// hold, a slave that is not fitted. A design that silently absorbs those is a
// design whose bring-up consists of guessing.
//
// WHAT MAKES IT SYNTHESISABLE.
//
//   - one clock, `clk`, everywhere; nothing is clocked on SCLK;
//   - one reset, `rst_n`, asynchronously asserted and released on every flop
//     that drives a pin;
//   - no latches: every `always_comb` equivalent is a continuous assignment
//     with every branch assigned;
//   - no gated or generated clocks: SCLK is a REGISTERED OUTPUT (Chapter 13.4),
//     not a clock this design uses;
//   - no combinational path from `miso` to any output pin: MISO is sampled into
//     a flop and nothing else;
//   - no initial blocks, no delays, no `$` calls outside simulation guards.
//
// The last two are the ones that get missed. A master that muxes MISO onto MOSI
// combinationally -- a "loopback mode", say -- has just built a path from an
// asynchronous input to an output with no flop in it, and no timing tool will
// tell you what to constrain.

module spi_master_top #(
    parameter int MAX_W = 32,
    parameter int LEN_W = 6,
    parameter int DIV_W = 8,
    parameter int CNT_W = 8,
    parameter int N_CS  = 4,
    parameter int SEL_W = 2
) (
    input  wire               clk,
    input  wire               rst_n,

    // --- register interface ---------------------------------------------
    input  wire [4:0]         reg_addr,
    input  wire               reg_wr,
    input  wire               reg_rd,
    input  wire [31:0]        reg_wdata,
    output wire [31:0]        reg_rdata,

    // --- SPI pins -------------------------------------------------------
    output wire               sclk,
    output wire               mosi,
    input  wire               miso,
    output wire [N_CS-1:0]    cs_n
);

    localparam [4:0] A_CTRL   = 5'h00,
                     A_TIMING = 5'h04,
                     A_TXDATA = 5'h08,
                     A_TXLAST = 5'h0C,
                     A_RXDATA = 5'h10,
                     A_STATUS = 5'h14,
                     A_CMD    = 5'h18;

    // --- configuration registers -----------------------------------------
    reg               cfg_cpol;
    reg               cfg_cpha;
    reg               cfg_lsb;
    reg [DIV_W-1:0]   cfg_div;
    reg [LEN_W-1:0]   cfg_len;
    reg [SEL_W-1:0]   cfg_sel;
    reg [CNT_W-1:0]   cfg_lead;
    reg [CNT_W-1:0]   cfg_lag;
    reg [CNT_W-1:0]   cfg_gap;

    // --- decoded strobes --------------------------------------------------
    wire wr        = reg_wr;
    wire wr_txdata = wr && (reg_addr == A_TXDATA);
    wire wr_txlast = wr && (reg_addr == A_TXLAST);
    wire wr_cmd    = wr && (reg_addr == A_CMD);
    wire rd_rxdata = reg_rd && (reg_addr == A_RXDATA);

    wire tx_push   = wr_txdata | wr_txlast;
    wire tx_last   = wr_txlast;
    wire clr_flags = wr_cmd && reg_wdata[1];
    wire cmd_abort = wr_cmd && reg_wdata[0];

    // An abort is a COMMAND, written once, but the abort controller wants a
    // level held until it has acknowledged. Latching it here is the whole of
    // the difference between a register write and a control signal, and doing
    // it anywhere else means a driver has to poll the status register fast
    // enough to keep a bit asserted -- which is not a thing software can
    // promise.
    reg abort_pend;

    // --- the engine -------------------------------------------------------
    wire              edge_a_stb, edge_b_stb, bit_done, div_err;
    wire [DIV_W-1:0]  half_a, half_b;
    wire              shift_en, start_stb, busy, sel_err;
    wire [2:0]        cs_state;
    wire              preload_stb, launch_stb, capture_stb, frame_done;
    wire [LEN_W-1:0]  bit_idx;
    wire [MAX_W-1:0]  rx_word;
    wire              rx_valid_stb, len_err;
    wire [MAX_W-1:0]  tx_data;
    wire              load_stb, stalled;
    wire              req_raw, hold_raw, tx_ready, rx_ready, rx_overrun;
    wire [MAX_W-1:0]  rx_rdata;
    wire              req_gated, abort_out;
    wire              aborting, aborted, rx_trunc, flush, abort_ack;
    wire [LEN_W-1:0]  abort_bits;

    spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
        .clk(clk), .rst_n(rst_n),
        .en(shift_en), .div(cfg_div), .cpol(cfg_cpol),
        .sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .bit_done(bit_done), .half_a(half_a), .half_b(half_b),
        .div_err(div_err)
    );

    spi_mode_edges #(.LEN_W(LEN_W)) u_mode (
        .clk(clk), .rst_n(rst_n),
        .cpha(cfg_cpha), .len(cfg_len),
        .active(shift_en), .start_stb(start_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
    );

    spi_width_order #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_data (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_data), .len(cfg_len), .lsb_first(cfg_lsb),
        .load_stb(load_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb),
        .miso(miso), .mosi(mosi),
        .rx_data(rx_word), .rx_valid_stb(rx_valid_stb), .len_err(len_err)
    );

    spi_cs_ctrl #(.N_CS(N_CS), .SEL_W(SEL_W), .CNT_W(CNT_W)) u_cs (
        .clk(clk), .rst_n(rst_n),
        .req(req_gated), .hold(hold_raw), .sel(cfg_sel),
        .lead_cyc(cfg_lead), .lag_cyc(cfg_lag), .gap_cyc(cfg_gap),
        .core_done(frame_done), .abort(abort_out),
        .cs_n(cs_n), .shift_en(shift_en), .start_stb(start_stb),
        .busy(busy), .sel_err(sel_err), .state_id(cs_state)
    );

    spi_multibyte #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_stream (
        .clk(clk), .rst_n(rst_n),
        .tx_push(tx_push), .tx_wdata(reg_wdata[MAX_W-1:0]), .tx_last(tx_last),
        .tx_ready(tx_ready),
        .flush(flush), .clr_flags(clr_flags),
        .rx_pop(rd_rxdata), .rx_rdata(rx_rdata), .rx_ready(rx_ready),
        .rx_overrun(rx_overrun),
        .core_busy(busy), .shift_en(shift_en), .start_stb(start_stb),
        .rx_valid_stb(rx_valid_stb), .rx_word(rx_word),
        .req(req_raw), .hold(hold_raw), .tx_data(tx_data),
        .load_stb(load_stb), .stalled(stalled)
    );

    spi_abort_ctl #(.LEN_W(LEN_W)) u_abort (
        .clk(clk), .rst_n(rst_n),
        .abort_req(abort_pend), .clr_flags(clr_flags),
        .busy(busy), .shift_en(shift_en), .frame_done(frame_done),
        .bit_idx(bit_idx), .len(cfg_len),
        .req_in(req_raw),
        .req_out(req_gated), .abort_out(abort_out),
        .aborting(aborting), .aborted(aborted), .abort_bits(abort_bits),
        .rx_trunc(rx_trunc), .flush(flush), .abort_ack(abort_ack)
    );

    // --- status -----------------------------------------------------------
    // Assembled by field rather than by concatenation. A concatenation has to
    // be exactly 32 bits wide and the padding depends on the parameters, so a
    // parameter change turns into a silently misaligned status register -- and
    // with LEN_W at its default the padding expressions come out zero bits
    // wide, which is not even legal. Indexed part-selects into a zeroed word
    // cannot drift.
    reg [31:0] status;
    always @(*) begin
        status              = 32'h0;
        status[0]           = tx_ready;
        status[1]           = rx_ready;
        status[2]           = busy;
        status[3]           = stalled;
        status[4]           = rx_overrun;
        status[5]           = rx_trunc;
        status[6]           = aborted;
        status[7]           = sel_err;
        status[8]           = len_err;
        status[9]           = div_err;
        status[10]          = aborting;
        status[16 +: LEN_W] = abort_bits;
    end

    // --- read mux ---------------------------------------------------------
    // Purely combinational, every branch assigned, no latch. RXDATA is the only
    // read with a side effect, and that side effect is driven by `reg_rd`
    // above rather than by this mux -- a read data path that also generates
    // control is a read data path nobody can safely add a register to.
    reg [31:0] rdata_mux;
    always @(*) begin
        rdata_mux = 32'h0;
        case (reg_addr)
            A_CTRL: begin
                rdata_mux[0]           = cfg_cpol;
                rdata_mux[1]           = cfg_cpha;
                rdata_mux[2]           = cfg_lsb;
                rdata_mux[8  +: DIV_W] = cfg_div;
                rdata_mux[16 +: LEN_W] = cfg_len;
                rdata_mux[24 +: SEL_W] = cfg_sel;
            end
            A_TIMING: begin
                rdata_mux[0  +: CNT_W] = cfg_lead;
                rdata_mux[8  +: CNT_W] = cfg_lag;
                rdata_mux[16 +: CNT_W] = cfg_gap;
            end
            A_RXDATA: rdata_mux[MAX_W-1:0] = rx_rdata;
            A_STATUS: rdata_mux            = status;
            default:  rdata_mux            = 32'h0;
        endcase
    end
    assign reg_rdata = rdata_mux;

    // --- register writes --------------------------------------------------
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset values chosen so the master is INERT, not merely defined:
            // mode 0, the slowest legal divisor, one byte, generous timing.
            // A reset default that happens to be a fast clock is a reset
            // default that can violate a slave's timing before software has
            // run a single instruction.
            cfg_cpol   <= 1'b0;
            cfg_cpha   <= 1'b0;
            cfg_lsb    <= 1'b0;
            cfg_div    <= 255;
            cfg_len    <= 8;
            cfg_sel    <= {SEL_W{1'b0}};
            cfg_lead   <= 16;
            cfg_lag    <= 16;
            cfg_gap    <= 16;
            abort_pend <= 1'b0;
        end else begin
            if (wr) begin
                case (reg_addr)
                    A_CTRL: begin
                        cfg_cpol <= reg_wdata[0];
                        cfg_cpha <= reg_wdata[1];
                        cfg_lsb  <= reg_wdata[2];
                        cfg_div  <= reg_wdata[8 +: DIV_W];
                        cfg_len  <= reg_wdata[16 +: LEN_W];
                        cfg_sel  <= reg_wdata[24 +: SEL_W];
                    end
                    A_TIMING: begin
                        cfg_lead <= reg_wdata[0  +: CNT_W];
                        cfg_lag  <= reg_wdata[8  +: CNT_W];
                        cfg_gap  <= reg_wdata[16 +: CNT_W];
                    end
                    default: ;   // TXDATA / TXLAST / CMD act through strobes
                endcase
            end

            // Set by the command, cleared by the acknowledgement. Ordered so a
            // command arriving on the same cycle as an acknowledgement is not
            // lost -- the write wins, because a dropped abort is an abort the
            // driver believes happened.
            if (abort_ack)
                abort_pend <= 1'b0;
            if (cmd_abort)
                abort_pend <= 1'b1;
        end
    end

    // NOTE ON CONFIGURATION WHILE BUSY. The registers here accept writes at any
    // time, and the datapath does NOT resample them mid-transfer: Chapter 13.2's
    // latch takes a snapshot at the start of each frame. So a write during a
    // transfer changes the NEXT frame and never corrupts the one in flight.
    // That is a deliberate division of labour -- the register file stays a
    // register file, and exactly one block owns the question of when
    // configuration takes effect.

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top_tb.sv — driven only through the registers, checked by a pin-level slave
// spi_master_top_tb.sv
//
// Every stimulus in this testbench goes through the REGISTER INTERFACE. There
// is no poking of internal signals anywhere, because the interface is what is
// being verified: a master whose blocks are all individually correct and whose
// register map is wrong is a master nobody can write a driver for.
//
// The closing test is a real flash transaction -- READ (0x03), three address
// bytes, four data bytes, one chip select -- driven exactly as software would
// drive it, and checked against a behavioural flash that only answers if the
// command and address arrived correctly under a single unbroken assertion. It
// is the whole track's argument in one transfer: Chapters 1 to 12 explained what
// the wire has to look like, and this is a master that produces it.

`timescale 1ns/1ps

module spi_master_top_tb;

    localparam int MAX_W = 32;
    localparam int LEN_W = 6;
    localparam int DIV_W = 8;
    localparam int CNT_W = 8;
    localparam int N_CS  = 4;
    localparam int SEL_W = 2;

    localparam [4:0] A_CTRL   = 5'h00,
                     A_TIMING = 5'h04,
                     A_TXDATA = 5'h08,
                     A_TXLAST = 5'h0C,
                     A_RXDATA = 5'h10,
                     A_STATUS = 5'h14,
                     A_CMD    = 5'h18;

    localparam int S_TXRDY   = 0, S_RXRDY  = 1, S_BUSY   = 2, S_STALL  = 3,
                   S_OVR     = 4, S_TRUNC  = 5, S_ABORTD = 6, S_SELERR = 7,
                   S_LENERR  = 8, S_DIVERR = 9, S_ABTING = 10;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    always #5 clk = ~clk;

    logic [4:0]  reg_addr  = 5'h0;
    logic        reg_wr    = 1'b0;
    logic        reg_rd    = 1'b0;
    logic [31:0] reg_wdata = 32'h0;
    wire  [31:0] reg_rdata;

    wire              sclk, mosi;
    wire [N_CS-1:0]   cs_n;
    wire              miso;

    spi_master_top #(.MAX_W(MAX_W), .LEN_W(LEN_W), .DIV_W(DIV_W),
                     .CNT_W(CNT_W), .N_CS(N_CS), .SEL_W(SEL_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .reg_addr(reg_addr), .reg_wr(reg_wr), .reg_rd(reg_rd),
        .reg_wdata(reg_wdata), .reg_rdata(reg_rdata),
        .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n)
    );

    // ---------------------------------------------------------------------
    // A behavioural SPI slave that works off the PINS ONLY. It recovers bit
    // boundaries from SCLK edges and word boundaries from chip select, exactly
    // as a real device does, so nothing it reports depends on the master's
    // internal strobes being right.
    // ---------------------------------------------------------------------
    logic cs_q, sclk_q;
    logic [7:0] sl_rx_sr;
    integer     sl_bits;
    logic [7:0] sl_rx_q [0:63];
    integer     sl_rx_n;
    logic [7:0] sl_tx_q [0:63];
    integer     sl_tx_n;
    logic [7:0] sl_tx_sr;
    integer     sl_tx_bits;
    logic       sl_miso_r;
    logic       sl_cpol, sl_cpha;

    assign miso = sl_miso_r;

    wire sl_cs = ~cs_n[0] | ~cs_n[1] | ~cs_n[2] | ~cs_n[3];

    // The capture edge for this mode is the LEADING edge when CPHA=0 and the
    // TRAILING edge when CPHA=1, which in terms of the raw pin is:
    wire sl_lead_edge  = (sclk != sclk_q) && (sclk != sl_cpol);
    wire sl_trail_edge = (sclk != sclk_q) && (sclk == sl_cpol);
    wire sl_cap_edge   = sl_cpha ? sl_trail_edge : sl_lead_edge;
    wire sl_drv_edge   = sl_cpha ? sl_lead_edge  : sl_trail_edge;

    always @(posedge clk) begin
        sclk_q <= sclk;
        cs_q   <= sl_cs;

        if (sl_cs && !cs_q) begin
            // Chip select has just asserted: a new transaction. With CPHA=0
            // the slave must already have its top bit on the pin, because the
            // first capture is the first edge and there is no earlier moment.
            // With CPHA=1 it must NOT -- the first leading edge is when both
            // sides drive, and a slave that pre-drives there is one bit ahead
            // for the whole transaction.
            sl_rx_sr <= 8'h0;
            sl_bits  <= 0;
            sl_tx_n  <= sl_tx_n + 1;
            if (!sl_cpha) begin
                sl_tx_sr   <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
                sl_miso_r  <= sl_tx_q[sl_tx_n][7];
                sl_tx_bits <= 1;
            end else begin
                sl_tx_sr   <= sl_tx_q[sl_tx_n];
                sl_tx_bits <= 0;
            end
        end else if (sl_cs) begin
            if (sl_cap_edge) begin
                sl_rx_sr <= {sl_rx_sr[6:0], mosi};
                if (sl_bits == 7) begin
                    sl_rx_q[sl_rx_n] <= {sl_rx_sr[6:0], mosi};
                    sl_rx_n          <= sl_rx_n + 1;
                    sl_bits          <= 0;
                end else begin
                    sl_bits <= sl_bits + 1;
                end
            end
            if (sl_drv_edge) begin
                if (sl_tx_bits == 8) begin
                    // Word boundary: fetch the next byte to send. Only reached
                    // with CPHA=0, where the first bit of each byte goes out
                    // half a period early; with CPHA=1 every bit including the
                    // first is driven on a leading edge, so the counter never
                    // gets here.
                    sl_tx_sr   <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
                    sl_miso_r  <= sl_tx_q[sl_tx_n][7];
                    sl_tx_bits <= 1;
                    sl_tx_n    <= sl_tx_n + 1;
                end else begin
                    sl_miso_r  <= sl_tx_sr[7];
                    sl_tx_sr   <= {sl_tx_sr[6:0], 1'b0};
                    sl_tx_bits <= sl_tx_bits + 1;
                end
            end
        end
    end

    // ---------------------------------------------------------------------
    // bus helpers
    // ---------------------------------------------------------------------
    integer errors = 0;

    task automatic bus_write(input [4:0] a, input [31:0] d);
        begin
            @(negedge clk);
            reg_addr = a; reg_wdata = d; reg_wr = 1'b1;
            @(negedge clk);
            reg_wr = 1'b0;
        end
    endtask

    logic [31:0] bus_q;
    task automatic bus_read(input [4:0] a);
        begin
            @(negedge clk);
            reg_addr = a; reg_rd = 1'b1;
            #1 bus_q = reg_rdata;
            @(negedge clk);
            reg_rd = 1'b0;
        end
    endtask

    // A status read that does NOT pop anything, used for polling.
    task automatic poll_status;
        begin
            @(negedge clk);
            reg_addr = A_STATUS; reg_rd = 1'b1;
            #1 bus_q = reg_rdata;
            @(negedge clk);
            reg_rd = 1'b0;
        end
    endtask

    task automatic configure(input integer dv, input bit pol, input bit pha,
                             input bit lsb, input integer nbits,
                             input integer slave,
                             input integer lead, input integer lag,
                             input integer gap);
        begin
            bus_write(A_CTRL, (pol ? 32'h1 : 32'h0) |
                              (pha ? 32'h2 : 32'h0) |
                              (lsb ? 32'h4 : 32'h0) |
                              ((dv    & 32'hFF) << 8)  |
                              ((nbits & 32'h3F) << 16) |
                              ((slave & 32'h3)  << 24));
            bus_write(A_TIMING, (lead & 32'hFF) |
                                ((lag & 32'hFF) << 8) |
                                ((gap & 32'hFF) << 16));
            sl_cpol = pol; sl_cpha = pha;
        end
    endtask

    task automatic send(input [31:0] d, input bit last);
        integer guard;
        begin
            guard = 8000;
            poll_status();
            while (!bus_q[S_TXRDY] && guard > 0) begin
                poll_status();
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: the transmit queue never became ready");
                errors = errors + 1;
            end
            bus_write(last ? A_TXLAST : A_TXDATA, d);
        end
    endtask

    integer got_n;
    logic [31:0] got_q [0:63];

    task automatic collect;
        begin
            poll_status();
            while (bus_q[S_RXRDY]) begin
                bus_read(A_RXDATA);
                got_q[got_n] = bus_q;
                got_n        = got_n + 1;
                poll_status();
            end
        end
    endtask

    // Waits for the transfer to START and only then for it to finish. Waiting
    // only for `busy` to fall is the classic driver bug: a request takes the
    // programmed lead time to turn into a busy engine, so a poll issued
    // immediately after the queue write sees an idle master and concludes the
    // transfer is over before it has begun. Every check downstream then reads
    // stale data and blames the hardware.
    task automatic wait_done;
        integer guard;
        begin
            guard = 4000;
            poll_status();
            while (!bus_q[S_BUSY] && guard > 0) begin
                collect();
                poll_status();
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: the master never became busy after a queue write");
                errors = errors + 1;
            end
            guard = 20000;
            poll_status();
            while (bus_q[S_BUSY] && guard > 0) begin
                collect();
                poll_status();
                guard = guard - 1;
            end
            repeat (6) @(negedge clk);
            collect();
        end
    endtask

    // Discards anything left in the receive register from an earlier test, so
    // one test's leftovers cannot be counted as the next test's first word.
    task automatic flush_rx;
        begin
            collect();
            got_n = 0;
        end
    endtask

    integer i, k, bad, base;
    logic [31:0] st;

    initial begin
        sl_rx_n = 0; sl_tx_n = 0; sl_bits = 0; sl_tx_bits = 0;
        sl_rx_sr = 8'h0; sl_tx_sr = 8'h0; sl_miso_r = 1'b0;
        sl_cpol = 1'b0; sl_cpha = 1'b0;
        got_n = 0;
        for (i = 0; i < 64; i = i + 1) sl_tx_q[i] = 8'h00;

        repeat (3) @(negedge clk);
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        // 1. RESET DEFAULTS. The master must come up inert and READABLE: a
        //    register file whose reset value cannot be read back is a register
        //    file nobody can debug.
        bus_read(A_CTRL);
        if (bus_q[7:0] !== 8'h00 || bus_q[15:8] !== 8'd255 ||
            bus_q[21:16] !== 6'd8) begin
            $display("  FAIL: CTRL came out of reset as %08h", bus_q);
            errors = errors + 1;
        end
        bus_read(A_TIMING);
        if (bus_q[7:0] !== 8'd16 || bus_q[15:8] !== 8'd16 ||
            bus_q[23:16] !== 8'd16) begin
            $display("  FAIL: TIMING came out of reset as %08h", bus_q);
            errors = errors + 1;
        end
        poll_status();
        if (!bus_q[S_TXRDY] || bus_q[S_BUSY] || bus_q[S_RXRDY]) begin
            $display("  FAIL: STATUS out of reset was %08h", bus_q);
            errors = errors + 1;
        end
        if (cs_n !== {N_CS{1'b1}}) begin
            $display("  FAIL: reset left a chip select asserted");
            errors = errors + 1;
        end
        $display("  out of reset: mode 0, divisor 255, eight bits, 16-cycle windows, all selects released, queue ready, not busy");

        // 2. READ-BACK OF EVERY WRITABLE FIELD. A field that cannot be read
        //    back is a field that will be programmed wrong for a year.
        configure(6, 1'b1, 1'b1, 1'b1, 13, 2, 7, 9, 11);
        bus_read(A_CTRL);
        if (bus_q[0] !== 1'b1 || bus_q[1] !== 1'b1 || bus_q[2] !== 1'b1 ||
            bus_q[15:8] !== 8'd6 || bus_q[21:16] !== 6'd13 ||
            bus_q[25:24] !== 2'd2) begin
            $display("  FAIL: CTRL read back %08h after programming", bus_q);
            errors = errors + 1;
        end
        bus_read(A_TIMING);
        if (bus_q[7:0] !== 8'd7 || bus_q[15:8] !== 8'd9 ||
            bus_q[23:16] !== 8'd11) begin
            $display("  FAIL: TIMING read back %08h after programming", bus_q);
            errors = errors + 1;
        end
        $display("  every writable field read back exactly as written");

        // 3. THE ERROR FLAGS MEAN WHAT THEY SAY. Each is provoked on its own.
        configure(1, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4);   // divisor below two
        poll_status();
        if (!bus_q[S_DIVERR]) begin
            $display("  FAIL: a divisor of 1 did not raise the divisor error");
            errors = errors + 1;
        end
        configure(4, 1'b0, 1'b0, 1'b0, 33, 0, 4, 4, 4);  // width above 32
        poll_status();
        if (!bus_q[S_LENERR]) begin
            $display("  FAIL: a 33-bit frame did not raise the width error");
            errors = errors + 1;
        end
        configure(4, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4);
        poll_status();
        if (bus_q[S_DIVERR] || bus_q[S_LENERR]) begin
            $display("  FAIL: a legal configuration still reports an error: %08h",
                     bus_q);
            errors = errors + 1;
        end
        $display("  divisor 1 and width 33 each reported on their own, and a legal setting reports neither");

        // 4. A COMPLETE TRANSACTION, all four modes, checked on the pins by a
        //    slave that only sees pins.
        for (k = 0; k < 4; k = k + 1) begin
            sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
            sl_tx_q[0] = 8'hDE; sl_tx_q[1] = 8'hAD;
            sl_tx_q[2] = 8'hBE; sl_tx_q[3] = 8'hEF;
            configure(4, k[0], k[1], 1'b0, 8, 0, 4, 4, 4);
            repeat (4) @(negedge clk);
            flush_rx();
            // Collected BETWEEN sends, because the receive side is one word
            // deep (Chapter 13.9): a driver that queues every word first and
            // only then starts reading loses all but the last one, and the
            // overrun flag says so.
            send(32'h12, 1'b0);  collect();
            send(32'h34, 1'b0);  collect();
            send(32'h56, 1'b0);  collect();
            send(32'h78, 1'b1);  collect();
            wait_done();

            if (sl_rx_n != 4) begin
                $display("  FAIL: mode %0d -- the slave saw %0d of 4 bytes",
                         k, sl_rx_n);
                errors = errors + 1;
            end else if (sl_rx_q[0] !== 8'h12 || sl_rx_q[1] !== 8'h34 ||
                         sl_rx_q[2] !== 8'h56 || sl_rx_q[3] !== 8'h78) begin
                $display("  FAIL: mode %0d -- the slave saw %02h %02h %02h %02h",
                         k, sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
                errors = errors + 1;
            end
            if (got_n != 4) begin
                $display("  FAIL: mode %0d -- software read %0d of 4 words",
                         k, got_n);
                errors = errors + 1;
            end else if (got_q[0][7:0] !== 8'hDE || got_q[1][7:0] !== 8'hAD ||
                         got_q[2][7:0] !== 8'hBE || got_q[3][7:0] !== 8'hEF) begin
                $display("  FAIL: mode %0d -- software read %02h %02h %02h %02h",
                         k, got_q[0][7:0], got_q[1][7:0], got_q[2][7:0],
                         got_q[3][7:0]);
                errors = errors + 1;
            end
        end
        $display("  all four modes: the slave received 12 34 56 78 and software read back de ad be ef, over one chip select each");

        // 5. LSB-FIRST, ON THE WIRE. The slave assembles MSB-first, so an
        //    LSB-first master must make it see the bit-reversed byte -- which is
        //    the only way to prove the setting reached the pins.
        sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
        configure(4, 1'b0, 1'b0, 1'b1, 8, 0, 4, 4, 4);
        repeat (4) @(negedge clk);
        flush_rx();
        send(32'h8D, 1'b1);
        wait_done();
        if (sl_rx_n != 1 || sl_rx_q[0] !== 8'hB1) begin
            $display("  FAIL: LSB-first 0x8d -- the slave recorded %0d bytes, the first being %02h, expected 1 and b1",
                     sl_rx_n, sl_rx_q[0]);
            errors = errors + 1;
        end
        $display("  LSB-first: software sent 8d and the MSB-first slave saw b1 -- the reversal happened on the wire");

        // 6. ABORT THROUGH THE COMMAND REGISTER, mid-transfer, and recovery.
        sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
        configure(8, 1'b0, 1'b0, 1'b0, 8, 0, 4, 6, 6);
        repeat (4) @(negedge clk);
        send(32'hAA, 1'b0);  collect();
        send(32'h55, 1'b0);
        // Let it get properly into the first frame.
        repeat (40) @(negedge clk);
        bus_write(A_CMD, 32'h1);
        i = 4000;
        poll_status();
        while (bus_q[S_BUSY] && i > 0) begin
            poll_status();
            i = i - 1;
        end
        repeat (10) @(negedge clk);
        poll_status();
        st = bus_q;
        if (!st[S_ABORTD]) begin
            $display("  FAIL: an abort written to CMD did not set the abort flag");
            errors = errors + 1;
        end
        if (!st[S_TRUNC]) begin
            $display("  FAIL: an abort mid-byte did not report a truncated word");
            errors = errors + 1;
        end
        // A truncated word that got nowhere is a contradiction, and it is
        // exactly what a request serviced twice produces: the second pass finds
        // the bus idle and zeroes the count while the flag stays set.
        if (st[S_TRUNC] && st[21:16] == 6'd0) begin
            $display("  FAIL: a truncated word was reported as having got 0 bits out");
            errors = errors + 1;
        end
        if (st[S_BUSY]) begin
            $display("  FAIL: the master was still busy long after the abort");
            errors = errors + 1;
        end
        if (cs_n !== {N_CS{1'b1}}) begin
            $display("  FAIL: the abort left a chip select asserted");
            errors = errors + 1;
        end
        $display("  abort written to CMD: flagged, the partial word reported as truncated (%0d bits made it), every select released",
                 st[21:16]);

        // The flags must clear on command, and only on command.
        poll_status();
        if (!bus_q[S_ABORTD] || !bus_q[S_TRUNC]) begin
            $display("  FAIL: the sticky flags cleared themselves on a status read");
            errors = errors + 1;
        end
        bus_write(A_CMD, 32'h2);
        repeat (2) @(negedge clk);
        poll_status();
        if (bus_q[S_ABORTD] || bus_q[S_TRUNC]) begin
            $display("  FAIL: writing the clear bit left %08h", bus_q);
            errors = errors + 1;
        end
        $display("  the sticky flags survive a status read and clear only on an explicit command");

        // 7. THE FLASH TRANSACTION. READ (0x03), a 24-bit address and four data
        //    bytes, under one chip select, driven the way a driver drives it.
        //    The flash answers only if the command and address arrived intact.
        sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
        // The flash's response, positioned so it lands in the data phase.
        sl_tx_q[0] = 8'hFF; sl_tx_q[1] = 8'hFF;
        sl_tx_q[2] = 8'hFF; sl_tx_q[3] = 8'hFF;
        sl_tx_q[4] = 8'hC0; sl_tx_q[5] = 8'hFF;
        sl_tx_q[6] = 8'hEE; sl_tx_q[7] = 8'h01;
        configure(4, 1'b0, 1'b0, 1'b0, 8, 1, 5, 5, 8);
        repeat (4) @(negedge clk);
        flush_rx();
        send(32'h03, 1'b0);  collect();   // READ
        send(32'h01, 1'b0);  collect();   // address 0x012345
        send(32'h23, 1'b0);  collect();
        send(32'h45, 1'b0);  collect();
        send(32'h00, 1'b0);  collect();   // four bytes to clock the data out
        send(32'h00, 1'b0);  collect();
        send(32'h00, 1'b0);  collect();
        send(32'h00, 1'b1);  collect();
        wait_done();

        if (sl_rx_n != 8) begin
            $display("  FAIL: the flash transaction delivered %0d of 8 bytes",
                     sl_rx_n);
            errors = errors + 1;
        end else if (sl_rx_q[0] !== 8'h03 || sl_rx_q[1] !== 8'h01 ||
                     sl_rx_q[2] !== 8'h23 || sl_rx_q[3] !== 8'h45) begin
            $display("  FAIL: the flash saw command %02h address %02h%02h%02h",
                     sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
            errors = errors + 1;
        end
        if (got_n != 8) begin
            $display("  FAIL: software read %0d of 8 words from the flash read",
                     got_n);
            errors = errors + 1;
        end else if (got_q[4][7:0] !== 8'hC0 || got_q[5][7:0] !== 8'hFF ||
                     got_q[6][7:0] !== 8'hEE || got_q[7][7:0] !== 8'h01) begin
            $display("  FAIL: the data phase returned %02h %02h %02h %02h",
                     got_q[4][7:0], got_q[5][7:0], got_q[6][7:0],
                     got_q[7][7:0]);
            errors = errors + 1;
        end
        poll_status();
        if (bus_q[S_OVR] || bus_q[S_TRUNC] || bus_q[S_ABORTD] ||
            bus_q[S_SELERR] || bus_q[S_LENERR] || bus_q[S_DIVERR]) begin
            $display("  FAIL: the flash transaction finished with status %08h",
                     bus_q);
            errors = errors + 1;
        end
        $display("  flash READ 0x03 of address 012345 on slave 1: command and address arrived intact under one chip select, and the four data bytes came back as c0 ff ee 01 with every error flag clear");

        if (errors == 0)
            $display("PASS: the complete master is driven entirely through its register interface -- it comes out of reset inert and fully readable, every writable field reads back as written, and each of the divisor, width and slave errors is reported on its own while a legal configuration reports none -- four-byte transactions in all four modes deliver the right bytes to a slave that only sees pins and return the right words to software, LSB-first genuinely reverses the bits on the wire, an abort written to the command register stops the transfer, flags the truncated word with its partial bit count, releases every chip select and leaves the engine able to run the next transfer, the sticky flags survive a status read and clear only on command, and a flash READ of command plus three address bytes plus four data bytes completes under a single unbroken chip select with every error flag clear");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top.v — the same master in Verilog-2001
// spi_master_top.v
//
// Chapter 13.11 -- the whole master, and the interface software actually sees.
//
// Seven blocks, wired together, plus the one thing none of them has: a way for
// software to drive it. This file is deliberately almost all structure. Every
// design decision worth arguing about was made in Chapters 13.1 through 13.10,
// and a top level that has interesting logic in it is a top level where a
// decision was made in the wrong place.
//
//     13.4  spi_clkdiv_strobe   SCLK, and the two edge strobes
//     13.5  spi_mode_edges      which edge launches, which captures
//     13.6  spi_shift_datapath  the two shift registers
//     13.8  spi_width_order     width and bit order, wrapping 13.6
//     13.7  spi_cs_ctrl         chip select, lead, lag, gap, and abort
//     13.9  spi_multibyte       the shadow register, streaming, overrun
//     13.10 spi_abort_ctl       abort policy and what was lost
//
// THE REGISTER MAP.
//
//   0x00  CTRL     [0] cpol  [1] cpha  [2] lsb_first
//                  [15:8] div (SCLK period in clocks, >= 2)
//                  [21:16] len (bits per frame, 1..MAX_W)
//                  [25:24] sel (which slave)
//   0x04  TIMING   [7:0] lead   [15:8] lag   [23:16] gap   (system clocks)
//   0x08  TXDATA   write: queue a word, transaction CONTINUES after it
//   0x0C  TXLAST   write: queue a word, transaction ENDS after it
//   0x10  RXDATA   read:  the received word; reading POPS it
//   0x14  STATUS   read only
//   0x18  CMD      write: [0] abort   [1] clear the sticky flags
//
// TWO ADDRESSES FOR ONE FIFO. `TXDATA` and `TXLAST` write the same queue and
// differ only in the end-of-transaction flag. The alternative -- a flag bit
// inside the data word -- costs a bit of the data width, and at MAX_W = 32 there
// is no bit to spare. The alternative after that -- a separate "this is the
// last one" register written before the data -- is two bus cycles per word and
// a race if an interrupt lands between them. An address bit is free.
//
// STATUS IS READ-ONLY AND COMPLETE. Every error the design can detect appears
// here: not just the ones a driver is expected to handle, but the ones that mean
// the DRIVER is wrong -- a divisor below two, a frame width the datapath cannot
// hold, a slave that is not fitted. A design that silently absorbs those is a
// design whose bring-up consists of guessing.
//
// WHAT MAKES IT SYNTHESISABLE.
//
//   - one clock, `clk`, everywhere; nothing is clocked on SCLK;
//   - one reset, `rst_n`, asynchronously asserted and released on every flop
//     that drives a pin;
//   - no latches: every `always_comb` equivalent is a continuous assignment
//     with every branch assigned;
//   - no gated or generated clocks: SCLK is a REGISTERED OUTPUT (Chapter 13.4),
//     not a clock this design uses;
//   - no combinational path from `miso` to any output pin: MISO is sampled into
//     a flop and nothing else;
//   - no initial blocks, no delays, no `$` calls outside simulation guards.
//
// The last two are the ones that get missed. A master that muxes MISO onto MOSI
// combinationally -- a "loopback mode", say -- has just built a path from an
// asynchronous input to an output with no flop in it, and no timing tool will
// tell you what to constrain.

module spi_master_top #(
    parameter MAX_W = 32,
    parameter LEN_W = 6,
    parameter DIV_W = 8,
    parameter CNT_W = 8,
    parameter N_CS  = 4,
    parameter SEL_W = 2
) (
    input  wire               clk,
    input  wire               rst_n,

    // --- register interface ---------------------------------------------
    input  wire [4:0]         reg_addr,
    input  wire               reg_wr,
    input  wire               reg_rd,
    input  wire [31:0]        reg_wdata,
    output wire [31:0]        reg_rdata,

    // --- SPI pins -------------------------------------------------------
    output wire               sclk,
    output wire               mosi,
    input  wire               miso,
    output wire [N_CS-1:0]    cs_n
);

    localparam [4:0] A_CTRL   = 5'h00,
                     A_TIMING = 5'h04,
                     A_TXDATA = 5'h08,
                     A_TXLAST = 5'h0C,
                     A_RXDATA = 5'h10,
                     A_STATUS = 5'h14,
                     A_CMD    = 5'h18;

    // --- configuration registers -----------------------------------------
    reg               cfg_cpol;
    reg               cfg_cpha;
    reg               cfg_lsb;
    reg [DIV_W-1:0]   cfg_div;
    reg [LEN_W-1:0]   cfg_len;
    reg [SEL_W-1:0]   cfg_sel;
    reg [CNT_W-1:0]   cfg_lead;
    reg [CNT_W-1:0]   cfg_lag;
    reg [CNT_W-1:0]   cfg_gap;

    // --- decoded strobes --------------------------------------------------
    wire wr        = reg_wr;
    wire wr_txdata = wr && (reg_addr == A_TXDATA);
    wire wr_txlast = wr && (reg_addr == A_TXLAST);
    wire wr_cmd    = wr && (reg_addr == A_CMD);
    wire rd_rxdata = reg_rd && (reg_addr == A_RXDATA);

    wire tx_push   = wr_txdata | wr_txlast;
    wire tx_last   = wr_txlast;
    wire clr_flags = wr_cmd && reg_wdata[1];
    wire cmd_abort = wr_cmd && reg_wdata[0];

    // An abort is a COMMAND, written once, but the abort controller wants a
    // level held until it has acknowledged. Latching it here is the whole of
    // the difference between a register write and a control signal, and doing
    // it anywhere else means a driver has to poll the status register fast
    // enough to keep a bit asserted -- which is not a thing software can
    // promise.
    reg abort_pend;

    // --- the engine -------------------------------------------------------
    wire              edge_a_stb, edge_b_stb, bit_done, div_err;
    wire [DIV_W-1:0]  half_a, half_b;
    wire              shift_en, start_stb, busy, sel_err;
    wire [2:0]        cs_state;
    wire              preload_stb, launch_stb, capture_stb, frame_done;
    wire [LEN_W-1:0]  bit_idx;
    wire [MAX_W-1:0]  rx_word;
    wire              rx_valid_stb, len_err;
    wire [MAX_W-1:0]  tx_data;
    wire              load_stb, stalled;
    wire              req_raw, hold_raw, tx_ready, rx_ready, rx_overrun;
    wire [MAX_W-1:0]  rx_rdata;
    wire              req_gated, abort_out;
    wire              aborting, aborted, rx_trunc, flush, abort_ack;
    wire [LEN_W-1:0]  abort_bits;

    spi_clkdiv_strobe #(.DIV_W(DIV_W)) u_div (
        .clk(clk), .rst_n(rst_n),
        .en(shift_en), .div(cfg_div), .cpol(cfg_cpol),
        .sclk(sclk), .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .bit_done(bit_done), .half_a(half_a), .half_b(half_b),
        .div_err(div_err)
    );

    spi_mode_edges #(.LEN_W(LEN_W)) u_mode (
        .clk(clk), .rst_n(rst_n),
        .cpha(cfg_cpha), .len(cfg_len),
        .active(shift_en), .start_stb(start_stb),
        .edge_a_stb(edge_a_stb), .edge_b_stb(edge_b_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb), .bit_idx(bit_idx), .frame_done(frame_done)
    );

    spi_width_order #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_data (
        .clk(clk), .rst_n(rst_n),
        .tx_data(tx_data), .len(cfg_len), .lsb_first(cfg_lsb),
        .load_stb(load_stb),
        .preload_stb(preload_stb), .launch_stb(launch_stb),
        .capture_stb(capture_stb),
        .miso(miso), .mosi(mosi),
        .rx_data(rx_word), .rx_valid_stb(rx_valid_stb), .len_err(len_err)
    );

    spi_cs_ctrl #(.N_CS(N_CS), .SEL_W(SEL_W), .CNT_W(CNT_W)) u_cs (
        .clk(clk), .rst_n(rst_n),
        .req(req_gated), .hold(hold_raw), .sel(cfg_sel),
        .lead_cyc(cfg_lead), .lag_cyc(cfg_lag), .gap_cyc(cfg_gap),
        .core_done(frame_done), .abort(abort_out),
        .cs_n(cs_n), .shift_en(shift_en), .start_stb(start_stb),
        .busy(busy), .sel_err(sel_err), .state_id(cs_state)
    );

    spi_multibyte #(.MAX_W(MAX_W), .LEN_W(LEN_W)) u_stream (
        .clk(clk), .rst_n(rst_n),
        .tx_push(tx_push), .tx_wdata(reg_wdata[MAX_W-1:0]), .tx_last(tx_last),
        .tx_ready(tx_ready),
        .flush(flush), .clr_flags(clr_flags),
        .rx_pop(rd_rxdata), .rx_rdata(rx_rdata), .rx_ready(rx_ready),
        .rx_overrun(rx_overrun),
        .core_busy(busy), .shift_en(shift_en), .start_stb(start_stb),
        .rx_valid_stb(rx_valid_stb), .rx_word(rx_word),
        .req(req_raw), .hold(hold_raw), .tx_data(tx_data),
        .load_stb(load_stb), .stalled(stalled)
    );

    spi_abort_ctl #(.LEN_W(LEN_W)) u_abort (
        .clk(clk), .rst_n(rst_n),
        .abort_req(abort_pend), .clr_flags(clr_flags),
        .busy(busy), .shift_en(shift_en), .frame_done(frame_done),
        .bit_idx(bit_idx), .len(cfg_len),
        .req_in(req_raw),
        .req_out(req_gated), .abort_out(abort_out),
        .aborting(aborting), .aborted(aborted), .abort_bits(abort_bits),
        .rx_trunc(rx_trunc), .flush(flush), .abort_ack(abort_ack)
    );

    // --- status -----------------------------------------------------------
    // Assembled by field rather than by concatenation. A concatenation has to
    // be exactly 32 bits wide and the padding depends on the parameters, so a
    // parameter change turns into a silently misaligned status register -- and
    // with LEN_W at its default the padding expressions come out zero bits
    // wide, which is not even legal. Indexed part-selects into a zeroed word
    // cannot drift.
    reg [31:0] status;
    always @(*) begin
        status              = 32'h0;
        status[0]           = tx_ready;
        status[1]           = rx_ready;
        status[2]           = busy;
        status[3]           = stalled;
        status[4]           = rx_overrun;
        status[5]           = rx_trunc;
        status[6]           = aborted;
        status[7]           = sel_err;
        status[8]           = len_err;
        status[9]           = div_err;
        status[10]          = aborting;
        status[16 +: LEN_W] = abort_bits;
    end

    // --- read mux ---------------------------------------------------------
    // Purely combinational, every branch assigned, no latch. RXDATA is the only
    // read with a side effect, and that side effect is driven by `reg_rd`
    // above rather than by this mux -- a read data path that also generates
    // control is a read data path nobody can safely add a register to.
    reg [31:0] rdata_mux;
    always @(*) begin
        rdata_mux = 32'h0;
        case (reg_addr)
            A_CTRL: begin
                rdata_mux[0]           = cfg_cpol;
                rdata_mux[1]           = cfg_cpha;
                rdata_mux[2]           = cfg_lsb;
                rdata_mux[8  +: DIV_W] = cfg_div;
                rdata_mux[16 +: LEN_W] = cfg_len;
                rdata_mux[24 +: SEL_W] = cfg_sel;
            end
            A_TIMING: begin
                rdata_mux[0  +: CNT_W] = cfg_lead;
                rdata_mux[8  +: CNT_W] = cfg_lag;
                rdata_mux[16 +: CNT_W] = cfg_gap;
            end
            A_RXDATA: rdata_mux[MAX_W-1:0] = rx_rdata;
            A_STATUS: rdata_mux            = status;
            default:  rdata_mux            = 32'h0;
        endcase
    end
    assign reg_rdata = rdata_mux;

    // --- register writes --------------------------------------------------
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset values chosen so the master is INERT, not merely defined:
            // mode 0, the slowest legal divisor, one byte, generous timing.
            // A reset default that happens to be a fast clock is a reset
            // default that can violate a slave's timing before software has
            // run a single instruction.
            cfg_cpol   <= 1'b0;
            cfg_cpha   <= 1'b0;
            cfg_lsb    <= 1'b0;
            cfg_div    <= 255;
            cfg_len    <= 8;
            cfg_sel    <= {SEL_W{1'b0}};
            cfg_lead   <= 16;
            cfg_lag    <= 16;
            cfg_gap    <= 16;
            abort_pend <= 1'b0;
        end else begin
            if (wr) begin
                case (reg_addr)
                    A_CTRL: begin
                        cfg_cpol <= reg_wdata[0];
                        cfg_cpha <= reg_wdata[1];
                        cfg_lsb  <= reg_wdata[2];
                        cfg_div  <= reg_wdata[8 +: DIV_W];
                        cfg_len  <= reg_wdata[16 +: LEN_W];
                        cfg_sel  <= reg_wdata[24 +: SEL_W];
                    end
                    A_TIMING: begin
                        cfg_lead <= reg_wdata[0  +: CNT_W];
                        cfg_lag  <= reg_wdata[8  +: CNT_W];
                        cfg_gap  <= reg_wdata[16 +: CNT_W];
                    end
                    default: ;   // TXDATA / TXLAST / CMD act through strobes
                endcase
            end

            // Set by the command, cleared by the acknowledgement. Ordered so a
            // command arriving on the same cycle as an acknowledgement is not
            // lost -- the write wins, because a dropped abort is an abort the
            // driver believes happened.
            if (abort_ack)
                abort_pend <= 1'b0;
            if (cmd_abort)
                abort_pend <= 1'b1;
        end
    end

    // NOTE ON CONFIGURATION WHILE BUSY. The registers here accept writes at any
    // time, and the datapath does NOT resample them mid-transfer: Chapter 13.2's
    // latch takes a snapshot at the start of each frame. So a write during a
    // transfer changes the NEXT frame and never corrupts the one in flight.
    // That is a deliberate division of labour -- the register file stays a
    // register file, and exactly one block owns the question of when
    // configuration takes effect.

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top_tb.v — the same register-driven tests in Verilog-2001
// spi_master_top_tb.v
//
// Every stimulus in this testbench goes through the REGISTER INTERFACE. There
// is no poking of internal signals anywhere, because the interface is what is
// being verified: a master whose blocks are all individually correct and whose
// register map is wrong is a master nobody can write a driver for.
//
// The closing test is a real flash transaction -- READ (0x03), three address
// bytes, four data bytes, one chip select -- driven exactly as software would
// drive it, and checked against a behavioural flash that only answers if the
// command and address arrived correctly under a single unbroken assertion. It
// is the whole track's argument in one transfer: Chapters 1 to 12 explained what
// the wire has to look like, and this is a master that produces it.

`timescale 1ns/1ps

module spi_master_top_tb;

    localparam MAX_W = 32;
    localparam LEN_W = 6;
    localparam DIV_W = 8;
    localparam CNT_W = 8;
    localparam N_CS  = 4;
    localparam SEL_W = 2;

    localparam [4:0] A_CTRL   = 5'h00,
                     A_TIMING = 5'h04,
                     A_TXDATA = 5'h08,
                     A_TXLAST = 5'h0C,
                     A_RXDATA = 5'h10,
                     A_STATUS = 5'h14,
                     A_CMD    = 5'h18;

    localparam S_TXRDY   = 0, S_RXRDY  = 1, S_BUSY   = 2, S_STALL  = 3,
                   S_OVR     = 4, S_TRUNC  = 5, S_ABORTD = 6, S_SELERR = 7,
                   S_LENERR  = 8, S_DIVERR = 9, S_ABTING = 10;

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

    reg [4:0]  reg_addr;
    reg        reg_wr;
    reg        reg_rd;
    reg [31:0] reg_wdata;
    wire  [31:0] reg_rdata;

    wire              sclk, mosi;
    wire [N_CS-1:0]   cs_n;
    wire              miso;

    spi_master_top #(.MAX_W(MAX_W), .LEN_W(LEN_W), .DIV_W(DIV_W),
                     .CNT_W(CNT_W), .N_CS(N_CS), .SEL_W(SEL_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .reg_addr(reg_addr), .reg_wr(reg_wr), .reg_rd(reg_rd),
        .reg_wdata(reg_wdata), .reg_rdata(reg_rdata),
        .sclk(sclk), .mosi(mosi), .miso(miso), .cs_n(cs_n)
    );

    // ---------------------------------------------------------------------
    // A behavioural SPI slave that works off the PINS ONLY. It recovers bit
    // boundaries from SCLK edges and word boundaries from chip select, exactly
    // as a real device does, so nothing it reports depends on the master's
    // internal strobes being right.
    // ---------------------------------------------------------------------
    reg cs_q, sclk_q;
    reg [7:0] sl_rx_sr;
    integer     sl_bits;
    reg [7:0] sl_rx_q [0:63];
    integer     sl_rx_n;
    reg [7:0] sl_tx_q [0:63];
    integer     sl_tx_n;
    reg [7:0] sl_tx_sr;
    integer     sl_tx_bits;
    reg       sl_miso_r;
    reg       sl_cpol, sl_cpha;

    assign miso = sl_miso_r;

    wire sl_cs = ~cs_n[0] | ~cs_n[1] | ~cs_n[2] | ~cs_n[3];

    // The capture edge for this mode is the LEADING edge when CPHA=0 and the
    // TRAILING edge when CPHA=1, which in terms of the raw pin is:
    wire sl_lead_edge  = (sclk != sclk_q) && (sclk != sl_cpol);
    wire sl_trail_edge = (sclk != sclk_q) && (sclk == sl_cpol);
    wire sl_cap_edge   = sl_cpha ? sl_trail_edge : sl_lead_edge;
    wire sl_drv_edge   = sl_cpha ? sl_lead_edge  : sl_trail_edge;

    always @(posedge clk) begin
        sclk_q <= sclk;
        cs_q   <= sl_cs;

        if (sl_cs && !cs_q) begin
            // Chip select has just asserted: a new transaction. With CPHA=0
            // the slave must already have its top bit on the pin, because the
            // first capture is the first edge and there is no earlier moment.
            // With CPHA=1 it must NOT -- the first leading edge is when both
            // sides drive, and a slave that pre-drives there is one bit ahead
            // for the whole transaction.
            sl_rx_sr <= 8'h0;
            sl_bits  <= 0;
            sl_tx_n  <= sl_tx_n + 1;
            if (!sl_cpha) begin
                sl_tx_sr   <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
                sl_miso_r  <= sl_tx_q[sl_tx_n][7];
                sl_tx_bits <= 1;
            end else begin
                sl_tx_sr   <= sl_tx_q[sl_tx_n];
                sl_tx_bits <= 0;
            end
        end else if (sl_cs) begin
            if (sl_cap_edge) begin
                sl_rx_sr <= {sl_rx_sr[6:0], mosi};
                if (sl_bits == 7) begin
                    sl_rx_q[sl_rx_n] <= {sl_rx_sr[6:0], mosi};
                    sl_rx_n          <= sl_rx_n + 1;
                    sl_bits          <= 0;
                end else begin
                    sl_bits <= sl_bits + 1;
                end
            end
            if (sl_drv_edge) begin
                if (sl_tx_bits == 8) begin
                    // Word boundary: fetch the next byte to send. Only reached
                    // with CPHA=0, where the first bit of each byte goes out
                    // half a period early; with CPHA=1 every bit including the
                    // first is driven on a leading edge, so the counter never
                    // gets here.
                    sl_tx_sr   <= {sl_tx_q[sl_tx_n][6:0], 1'b0};
                    sl_miso_r  <= sl_tx_q[sl_tx_n][7];
                    sl_tx_bits <= 1;
                    sl_tx_n    <= sl_tx_n + 1;
                end else begin
                    sl_miso_r  <= sl_tx_sr[7];
                    sl_tx_sr   <= {sl_tx_sr[6:0], 1'b0};
                    sl_tx_bits <= sl_tx_bits + 1;
                end
            end
        end
    end

    // ---------------------------------------------------------------------
    // bus helpers
    // ---------------------------------------------------------------------
    integer errors;

        task bus_write;
        input [4:0] a;
        input [31:0] d;
        begin
            @(negedge clk);
            reg_addr = a; reg_wdata = d; reg_wr = 1'b1;
            @(negedge clk);
            reg_wr = 1'b0;
        end
    endtask

    reg [31:0] bus_q;
        task bus_read;
        input [4:0] a;
        begin
            @(negedge clk);
            reg_addr = a; reg_rd = 1'b1;
            #1 bus_q = reg_rdata;
            @(negedge clk);
            reg_rd = 1'b0;
        end
    endtask

    // A status read that does NOT pop anything, used for polling.
    task poll_status;
        begin
            @(negedge clk);
            reg_addr = A_STATUS; reg_rd = 1'b1;
            #1 bus_q = reg_rdata;
            @(negedge clk);
            reg_rd = 1'b0;
        end
    endtask

        task configure;
        input integer dv;
        input pol;
        input pha;
        input lsb;
        input integer nbits;
        input integer slave;
        input integer lead;
        input integer lag;
        input integer gap;
        begin
            bus_write(A_CTRL, (pol ? 32'h1 : 32'h0) |
                              (pha ? 32'h2 : 32'h0) |
                              (lsb ? 32'h4 : 32'h0) |
                              ((dv    & 32'hFF) << 8)  |
                              ((nbits & 32'h3F) << 16) |
                              ((slave & 32'h3)  << 24));
            bus_write(A_TIMING, (lead & 32'hFF) |
                                ((lag & 32'hFF) << 8) |
                                ((gap & 32'hFF) << 16));
            sl_cpol = pol; sl_cpha = pha;
        end
    endtask

        task send;
        input [31:0] d;
        input last;
        integer guard;
        begin
            guard = 8000;
            poll_status();
            while (!bus_q[S_TXRDY] && guard > 0) begin
                poll_status();
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: the transmit queue never became ready");
                errors = errors + 1;
            end
            bus_write(last ? A_TXLAST : A_TXDATA, d);
        end
    endtask

    integer got_n;
    reg [31:0] got_q [0:63];

    task collect;
        begin
            poll_status();
            while (bus_q[S_RXRDY]) begin
                bus_read(A_RXDATA);
                got_q[got_n] = bus_q;
                got_n        = got_n + 1;
                poll_status();
            end
        end
    endtask

    // Waits for the transfer to START and only then for it to finish. Waiting
    // only for `busy` to fall is the classic driver bug: a request takes the
    // programmed lead time to turn into a busy engine, so a poll issued
    // immediately after the queue write sees an idle master and concludes the
    // transfer is over before it has begun. Every check downstream then reads
    // stale data and blames the hardware.
    task wait_done;
        integer guard;
        begin
            guard = 4000;
            poll_status();
            while (!bus_q[S_BUSY] && guard > 0) begin
                collect();
                poll_status();
                guard = guard - 1;
            end
            if (guard == 0) begin
                $display("  FAIL: the master never became busy after a queue write");
                errors = errors + 1;
            end
            guard = 20000;
            poll_status();
            while (bus_q[S_BUSY] && guard > 0) begin
                collect();
                poll_status();
                guard = guard - 1;
            end
            repeat (6) @(negedge clk);
            collect();
        end
    endtask

    // Discards anything left in the receive register from an earlier test, so
    // one test's leftovers cannot be counted as the next test's first word.
    task flush_rx;
        begin
            collect();
            got_n = 0;
        end
    endtask

    integer i, k, bad, base;
    reg [31:0] st;

    initial begin
        sl_rx_n = 0; sl_tx_n = 0; sl_bits = 0; sl_tx_bits = 0;
        sl_rx_sr = 8'h0; sl_tx_sr = 8'h0; sl_miso_r = 1'b0;
        sl_cpol = 1'b0; sl_cpha = 1'b0;
        got_n = 0;
        for (i = 0; i < 64; i = i + 1) sl_tx_q[i] = 8'h00;

        repeat (3) @(negedge clk);
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        // 1. RESET DEFAULTS. The master must come up inert and READABLE: a
        //    register file whose reset value cannot be read back is a register
        //    file nobody can debug.
        bus_read(A_CTRL);
        if (bus_q[7:0] !== 8'h00 || bus_q[15:8] !== 8'd255 ||
            bus_q[21:16] !== 6'd8) begin
            $display("  FAIL: CTRL came out of reset as %08h", bus_q);
            errors = errors + 1;
        end
        bus_read(A_TIMING);
        if (bus_q[7:0] !== 8'd16 || bus_q[15:8] !== 8'd16 ||
            bus_q[23:16] !== 8'd16) begin
            $display("  FAIL: TIMING came out of reset as %08h", bus_q);
            errors = errors + 1;
        end
        poll_status();
        if (!bus_q[S_TXRDY] || bus_q[S_BUSY] || bus_q[S_RXRDY]) begin
            $display("  FAIL: STATUS out of reset was %08h", bus_q);
            errors = errors + 1;
        end
        if (cs_n !== {N_CS{1'b1}}) begin
            $display("  FAIL: reset left a chip select asserted");
            errors = errors + 1;
        end
        $display("  out of reset: mode 0, divisor 255, eight bits, 16-cycle windows, all selects released, queue ready, not busy");

        // 2. READ-BACK OF EVERY WRITABLE FIELD. A field that cannot be read
        //    back is a field that will be programmed wrong for a year.
        configure(6, 1'b1, 1'b1, 1'b1, 13, 2, 7, 9, 11);
        bus_read(A_CTRL);
        if (bus_q[0] !== 1'b1 || bus_q[1] !== 1'b1 || bus_q[2] !== 1'b1 ||
            bus_q[15:8] !== 8'd6 || bus_q[21:16] !== 6'd13 ||
            bus_q[25:24] !== 2'd2) begin
            $display("  FAIL: CTRL read back %08h after programming", bus_q);
            errors = errors + 1;
        end
        bus_read(A_TIMING);
        if (bus_q[7:0] !== 8'd7 || bus_q[15:8] !== 8'd9 ||
            bus_q[23:16] !== 8'd11) begin
            $display("  FAIL: TIMING read back %08h after programming", bus_q);
            errors = errors + 1;
        end
        $display("  every writable field read back exactly as written");

        // 3. THE ERROR FLAGS MEAN WHAT THEY SAY. Each is provoked on its own.
        configure(1, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4);   // divisor below two
        poll_status();
        if (!bus_q[S_DIVERR]) begin
            $display("  FAIL: a divisor of 1 did not raise the divisor error");
            errors = errors + 1;
        end
        configure(4, 1'b0, 1'b0, 1'b0, 33, 0, 4, 4, 4);  // width above 32
        poll_status();
        if (!bus_q[S_LENERR]) begin
            $display("  FAIL: a 33-bit frame did not raise the width error");
            errors = errors + 1;
        end
        configure(4, 1'b0, 1'b0, 1'b0, 8, 0, 4, 4, 4);
        poll_status();
        if (bus_q[S_DIVERR] || bus_q[S_LENERR]) begin
            $display("  FAIL: a legal configuration still reports an error: %08h",
                     bus_q);
            errors = errors + 1;
        end
        $display("  divisor 1 and width 33 each reported on their own, and a legal setting reports neither");

        // 4. A COMPLETE TRANSACTION, all four modes, checked on the pins by a
        //    slave that only sees pins.
        for (k = 0; k < 4; k = k + 1) begin
            sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
            sl_tx_q[0] = 8'hDE; sl_tx_q[1] = 8'hAD;
            sl_tx_q[2] = 8'hBE; sl_tx_q[3] = 8'hEF;
            configure(4, k[0], k[1], 1'b0, 8, 0, 4, 4, 4);
            repeat (4) @(negedge clk);
            flush_rx();
            // Collected BETWEEN sends, because the receive side is one word
            // deep (Chapter 13.9): a driver that queues every word first and
            // only then starts reading loses all but the last one, and the
            // overrun flag says so.
            send(32'h12, 1'b0);  collect();
            send(32'h34, 1'b0);  collect();
            send(32'h56, 1'b0);  collect();
            send(32'h78, 1'b1);  collect();
            wait_done();

            if (sl_rx_n != 4) begin
                $display("  FAIL: mode %0d -- the slave saw %0d of 4 bytes",
                         k, sl_rx_n);
                errors = errors + 1;
            end else if (sl_rx_q[0] !== 8'h12 || sl_rx_q[1] !== 8'h34 ||
                         sl_rx_q[2] !== 8'h56 || sl_rx_q[3] !== 8'h78) begin
                $display("  FAIL: mode %0d -- the slave saw %02h %02h %02h %02h",
                         k, sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
                errors = errors + 1;
            end
            if (got_n != 4) begin
                $display("  FAIL: mode %0d -- software read %0d of 4 words",
                         k, got_n);
                errors = errors + 1;
            end else if (got_q[0][7:0] !== 8'hDE || got_q[1][7:0] !== 8'hAD ||
                         got_q[2][7:0] !== 8'hBE || got_q[3][7:0] !== 8'hEF) begin
                $display("  FAIL: mode %0d -- software read %02h %02h %02h %02h",
                         k, got_q[0][7:0], got_q[1][7:0], got_q[2][7:0],
                         got_q[3][7:0]);
                errors = errors + 1;
            end
        end
        $display("  all four modes: the slave received 12 34 56 78 and software read back de ad be ef, over one chip select each");

        // 5. LSB-FIRST, ON THE WIRE. The slave assembles MSB-first, so an
        //    LSB-first master must make it see the bit-reversed byte -- which is
        //    the only way to prove the setting reached the pins.
        sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
        configure(4, 1'b0, 1'b0, 1'b1, 8, 0, 4, 4, 4);
        repeat (4) @(negedge clk);
        flush_rx();
        send(32'h8D, 1'b1);
        wait_done();
        if (sl_rx_n != 1 || sl_rx_q[0] !== 8'hB1) begin
            $display("  FAIL: LSB-first 0x8d -- the slave recorded %0d bytes, the first being %02h, expected 1 and b1",
                     sl_rx_n, sl_rx_q[0]);
            errors = errors + 1;
        end
        $display("  LSB-first: software sent 8d and the MSB-first slave saw b1 -- the reversal happened on the wire");

        // 6. ABORT THROUGH THE COMMAND REGISTER, mid-transfer, and recovery.
        sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
        configure(8, 1'b0, 1'b0, 1'b0, 8, 0, 4, 6, 6);
        repeat (4) @(negedge clk);
        send(32'hAA, 1'b0);  collect();
        send(32'h55, 1'b0);
        // Let it get properly into the first frame.
        repeat (40) @(negedge clk);
        bus_write(A_CMD, 32'h1);
        i = 4000;
        poll_status();
        while (bus_q[S_BUSY] && i > 0) begin
            poll_status();
            i = i - 1;
        end
        repeat (10) @(negedge clk);
        poll_status();
        st = bus_q;
        if (!st[S_ABORTD]) begin
            $display("  FAIL: an abort written to CMD did not set the abort flag");
            errors = errors + 1;
        end
        if (!st[S_TRUNC]) begin
            $display("  FAIL: an abort mid-byte did not report a truncated word");
            errors = errors + 1;
        end
        // A truncated word that got nowhere is a contradiction, and it is
        // exactly what a request serviced twice produces: the second pass finds
        // the bus idle and zeroes the count while the flag stays set.
        if (st[S_TRUNC] && st[21:16] == 6'd0) begin
            $display("  FAIL: a truncated word was reported as having got 0 bits out");
            errors = errors + 1;
        end
        if (st[S_BUSY]) begin
            $display("  FAIL: the master was still busy long after the abort");
            errors = errors + 1;
        end
        if (cs_n !== {N_CS{1'b1}}) begin
            $display("  FAIL: the abort left a chip select asserted");
            errors = errors + 1;
        end
        $display("  abort written to CMD: flagged, the partial word reported as truncated (%0d bits made it), every select released",
                 st[21:16]);

        // The flags must clear on command, and only on command.
        poll_status();
        if (!bus_q[S_ABORTD] || !bus_q[S_TRUNC]) begin
            $display("  FAIL: the sticky flags cleared themselves on a status read");
            errors = errors + 1;
        end
        bus_write(A_CMD, 32'h2);
        repeat (2) @(negedge clk);
        poll_status();
        if (bus_q[S_ABORTD] || bus_q[S_TRUNC]) begin
            $display("  FAIL: writing the clear bit left %08h", bus_q);
            errors = errors + 1;
        end
        $display("  the sticky flags survive a status read and clear only on an explicit command");

        // 7. THE FLASH TRANSACTION. READ (0x03), a 24-bit address and four data
        //    bytes, under one chip select, driven the way a driver drives it.
        //    The flash answers only if the command and address arrived intact.
        sl_rx_n = 0; sl_tx_n = 0; got_n = 0;
        // The flash's response, positioned so it lands in the data phase.
        sl_tx_q[0] = 8'hFF; sl_tx_q[1] = 8'hFF;
        sl_tx_q[2] = 8'hFF; sl_tx_q[3] = 8'hFF;
        sl_tx_q[4] = 8'hC0; sl_tx_q[5] = 8'hFF;
        sl_tx_q[6] = 8'hEE; sl_tx_q[7] = 8'h01;
        configure(4, 1'b0, 1'b0, 1'b0, 8, 1, 5, 5, 8);
        repeat (4) @(negedge clk);
        flush_rx();
        send(32'h03, 1'b0);  collect();   // READ
        send(32'h01, 1'b0);  collect();   // address 0x012345
        send(32'h23, 1'b0);  collect();
        send(32'h45, 1'b0);  collect();
        send(32'h00, 1'b0);  collect();   // four bytes to clock the data out
        send(32'h00, 1'b0);  collect();
        send(32'h00, 1'b0);  collect();
        send(32'h00, 1'b1);  collect();
        wait_done();

        if (sl_rx_n != 8) begin
            $display("  FAIL: the flash transaction delivered %0d of 8 bytes",
                     sl_rx_n);
            errors = errors + 1;
        end else if (sl_rx_q[0] !== 8'h03 || sl_rx_q[1] !== 8'h01 ||
                     sl_rx_q[2] !== 8'h23 || sl_rx_q[3] !== 8'h45) begin
            $display("  FAIL: the flash saw command %02h address %02h%02h%02h",
                     sl_rx_q[0], sl_rx_q[1], sl_rx_q[2], sl_rx_q[3]);
            errors = errors + 1;
        end
        if (got_n != 8) begin
            $display("  FAIL: software read %0d of 8 words from the flash read",
                     got_n);
            errors = errors + 1;
        end else if (got_q[4][7:0] !== 8'hC0 || got_q[5][7:0] !== 8'hFF ||
                     got_q[6][7:0] !== 8'hEE || got_q[7][7:0] !== 8'h01) begin
            $display("  FAIL: the data phase returned %02h %02h %02h %02h",
                     got_q[4][7:0], got_q[5][7:0], got_q[6][7:0],
                     got_q[7][7:0]);
            errors = errors + 1;
        end
        poll_status();
        if (bus_q[S_OVR] || bus_q[S_TRUNC] || bus_q[S_ABORTD] ||
            bus_q[S_SELERR] || bus_q[S_LENERR] || bus_q[S_DIVERR]) begin
            $display("  FAIL: the flash transaction finished with status %08h",
                     bus_q);
            errors = errors + 1;
        end
        $display("  flash READ 0x03 of address 012345 on slave 1: command and address arrived intact under one chip select, and the four data bytes came back as c0 ff ee 01 with every error flag clear");

        if (errors == 0)
            $display("PASS: the complete master is driven entirely through its register interface -- it comes out of reset inert and fully readable, every writable field reads back as written, and each of the divisor, width and slave errors is reported on its own while a legal configuration reports none -- four-byte transactions in all four modes deliver the right bytes to a slave that only sees pins and return the right words to software, LSB-first genuinely reverses the bits on the wire, an abort written to the command register stops the transfer, flags the truncated word with its partial bit count, releases every chip select and leaves the engine able to run the next transfer, the sticky flags survive a status read and clear only on command, and a flash READ of command plus three address bytes plus four data bytes completes under a single unbroken chip select with every error flag clear");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        reg_addr = 5'h0;
        reg_wr = 1'b0;
        reg_rd = 1'b0;
        reg_wdata = 32'h0;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top.vhd — the same master in VHDL
-- spi_master_top.vhd
--
-- Chapter 13.11 -- the whole master, and the interface software actually sees.
--
-- Seven blocks, wired together, plus the one thing none of them has: a way for
-- software to drive it. This file is deliberately almost all structure. Every
-- design decision worth arguing about was made in Chapters 13.1 through 13.10,
-- and a top level that has interesting logic in it is a top level where a
-- decision was made in the wrong place.
--
--     13.4  spi_clkdiv_strobe   SCLK, and the two edge strobes
--     13.5  spi_mode_edges      which edge launches, which captures
--     13.6  spi_shift_datapath  the two shift registers
--     13.8  spi_width_order     width and bit order, wrapping 13.6
--     13.7  spi_cs_ctrl         chip select, lead, lag, gap, and abort
--     13.9  spi_multibyte       the shadow register, streaming, overrun
--     13.10 spi_abort_ctl       abort policy and what was lost
--
-- THE REGISTER MAP.
--
--   0x00  CTRL     [0] cpol  [1] cpha  [2] lsb_first
--                  [15:8] div (SCLK period in clocks, >= 2)
--                  [21:16] len (bits per frame, 1..MAX_W)
--                  [25:24] sel (which slave)
--   0x04  TIMING   [7:0] lead   [15:8] lag   [23:16] gap   (system clocks)
--   0x08  TXDATA   write: queue a word, transaction CONTINUES after it
--   0x0C  TXLAST   write: queue a word, transaction ENDS after it
--   0x10  RXDATA   read:  the received word; reading POPS it
--   0x14  STATUS   read only
--   0x18  CMD      write: [0] abort   [1] clear the sticky flags
--
-- TWO ADDRESSES FOR ONE FIFO. `TXDATA` and `TXLAST` write the same queue and
-- differ only in the end-of-transaction flag. A flag bit inside the data word
-- costs a bit of the data width, and at MAX_W = 32 there is none to spare. A
-- separate "this is the last one" register written first is two bus cycles per
-- word and a race if an interrupt lands between them. An address bit is free.
--
-- STATUS IS READ-ONLY AND COMPLETE. Every error the design can detect appears
-- here: not just the ones a driver is expected to handle, but the ones that mean
-- the DRIVER is wrong -- a divisor below two, a frame width the datapath cannot
-- hold, a slave that is not fitted. A design that silently absorbs those is a
-- design whose bring-up consists of guessing.
--
-- WHAT MAKES IT SYNTHESISABLE.
--
--   - one clock, `clk`, everywhere; nothing is clocked on SCLK;
--   - one reset, `rst_n`, asynchronous, on every flop that drives a pin;
--   - no latches: every combinational process assigns every output on every
--     path;
--   - no gated or generated clocks: SCLK is a REGISTERED OUTPUT (Chapter 13.4),
--     not a clock this design uses;
--   - no combinational path from `miso` to any output pin: MISO is sampled into
--     a flop and nothing else.
--
-- The last two are the ones that get missed. A master that muxes MISO onto MOSI
-- combinationally -- a "loopback mode", say -- has just built a path from an
-- asynchronous input to an output with no flop in it, and no timing tool will
-- tell you what to constrain.

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

entity spi_master_top is
    generic (
        MAX_W : positive := 32;
        LEN_W : positive := 6;
        DIV_W : positive := 8;
        CNT_W : positive := 8;
        N_CS  : positive := 4;
        SEL_W : positive := 2
    );
    port (
        clk       : in  std_logic;
        rst_n     : in  std_logic;

        -- register interface
        reg_addr  : in  unsigned(4 downto 0);
        reg_wr    : in  std_logic;
        reg_rd    : in  std_logic;
        reg_wdata : in  std_logic_vector(31 downto 0);
        reg_rdata : out std_logic_vector(31 downto 0);

        -- SPI pins
        sclk      : out std_logic;
        mosi      : out std_logic;
        miso      : in  std_logic;
        cs_n      : out std_logic_vector(N_CS - 1 downto 0)
    );
end entity;

architecture rtl of spi_master_top is

    constant A_CTRL   : natural := 16#00#;
    constant A_TIMING : natural := 16#04#;
    constant A_TXDATA : natural := 16#08#;
    constant A_TXLAST : natural := 16#0C#;
    constant A_RXDATA : natural := 16#10#;
    constant A_STATUS : natural := 16#14#;
    constant A_CMD    : natural := 16#18#;

    -- configuration registers
    signal cfg_cpol : std_logic := '0';
    signal cfg_cpha : std_logic := '0';
    signal cfg_lsb  : std_logic := '0';
    signal cfg_div  : unsigned(DIV_W - 1 downto 0) := (others => '0');
    signal cfg_len  : unsigned(LEN_W - 1 downto 0) := (others => '0');
    signal cfg_sel  : unsigned(SEL_W - 1 downto 0) := (others => '0');
    signal cfg_lead : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal cfg_lag  : unsigned(CNT_W - 1 downto 0) := (others => '0');
    signal cfg_gap  : unsigned(CNT_W - 1 downto 0) := (others => '0');

    -- decoded strobes
    signal wr_txdata, wr_txlast, wr_cmd, rd_rxdata : std_logic;
    signal tx_push, tx_last, clr_flags, cmd_abort  : std_logic;

    -- An abort is a COMMAND, written once, but the abort controller wants a
    -- level held until it has acknowledged. Latching it here is the whole of
    -- the difference between a register write and a control signal, and doing it
    -- anywhere else means a driver has to poll the status register fast enough
    -- to keep a bit asserted -- which is not a thing software can promise.
    signal abort_pend : std_logic := '0';

    -- the engine
    signal edge_a_stb, edge_b_stb, bit_done, div_err : std_logic;
    signal half_a, half_b : unsigned(DIV_W - 1 downto 0);
    signal shift_en, start_stb, busy, sel_err : std_logic;
    signal cs_state : unsigned(2 downto 0);
    signal preload_stb, launch_stb, capture_stb, frame_done : std_logic;
    signal bit_idx : unsigned(LEN_W - 1 downto 0);
    signal rx_word : std_logic_vector(MAX_W - 1 downto 0);
    signal rx_valid_stb, len_err : std_logic;
    signal tx_data : std_logic_vector(MAX_W - 1 downto 0);
    signal load_stb, stalled : std_logic;
    signal req_raw, hold_raw, tx_ready, rx_ready, rx_overrun : std_logic;
    signal rx_rdata : std_logic_vector(MAX_W - 1 downto 0);
    signal req_gated, abort_out : std_logic;
    signal aborting, aborted, rx_trunc, flush, abort_ack : std_logic;
    signal abort_bits : unsigned(LEN_W - 1 downto 0);

    signal status : std_logic_vector(31 downto 0);
    signal tx_word : std_logic_vector(MAX_W - 1 downto 0);

begin

    tx_word <= reg_wdata(MAX_W - 1 downto 0);

    wr_txdata <= '1' when reg_wr = '1' and to_integer(reg_addr) = A_TXDATA
                 else '0';
    wr_txlast <= '1' when reg_wr = '1' and to_integer(reg_addr) = A_TXLAST
                 else '0';
    wr_cmd    <= '1' when reg_wr = '1' and to_integer(reg_addr) = A_CMD
                 else '0';
    rd_rxdata <= '1' when reg_rd = '1' and to_integer(reg_addr) = A_RXDATA
                 else '0';

    tx_push   <= wr_txdata or wr_txlast;
    tx_last   <= wr_txlast;
    clr_flags <= wr_cmd and reg_wdata(1);
    cmd_abort <= wr_cmd and reg_wdata(0);

    u_div : entity work.spi_clkdiv_strobe
        generic map (DIV_W => DIV_W)
        port map (clk => clk, rst_n => rst_n,
                  en => shift_en, div => cfg_div, cpol => cfg_cpol,
                  sclk => sclk, edge_a_stb => edge_a_stb,
                  edge_b_stb => edge_b_stb, bit_done => bit_done,
                  half_a => half_a, half_b => half_b, div_err => div_err);

    u_mode : entity work.spi_mode_edges
        generic map (LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  cpha => cfg_cpha, len => cfg_len,
                  active => shift_en, start_stb => start_stb,
                  edge_a_stb => edge_a_stb, edge_b_stb => edge_b_stb,
                  preload_stb => preload_stb, launch_stb => launch_stb,
                  capture_stb => capture_stb, bit_idx => bit_idx,
                  frame_done => frame_done);

    u_data : entity work.spi_width_order
        generic map (MAX_W => MAX_W, LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  tx_data => tx_data, len => cfg_len, lsb_first => cfg_lsb,
                  load_stb => load_stb,
                  preload_stb => preload_stb, launch_stb => launch_stb,
                  capture_stb => capture_stb,
                  miso => miso, mosi => mosi,
                  rx_data => rx_word, rx_valid_stb => rx_valid_stb,
                  len_err => len_err);

    u_cs : entity work.spi_cs_ctrl
        generic map (N_CS => N_CS, SEL_W => SEL_W, CNT_W => CNT_W)
        port map (clk => clk, rst_n => rst_n,
                  req => req_gated, hold => hold_raw, sel => cfg_sel,
                  lead_cyc => cfg_lead, lag_cyc => cfg_lag,
                  gap_cyc => cfg_gap,
                  core_done => frame_done, abort => abort_out,
                  cs_n => cs_n, shift_en => shift_en, start_stb => start_stb,
                  busy => busy, sel_err => sel_err, state_id => cs_state);

    u_stream : entity work.spi_multibyte
        generic map (MAX_W => MAX_W, LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  tx_push => tx_push, tx_wdata => tx_word, tx_last => tx_last,
                  tx_ready => tx_ready,
                  flush => flush, clr_flags => clr_flags,
                  rx_pop => rd_rxdata, rx_rdata => rx_rdata,
                  rx_ready => rx_ready, rx_overrun => rx_overrun,
                  core_busy => busy, shift_en => shift_en,
                  start_stb => start_stb,
                  rx_valid_stb => rx_valid_stb, rx_word => rx_word,
                  req => req_raw, hold => hold_raw, tx_data => tx_data,
                  load_stb => load_stb, stalled => stalled);

    u_abort : entity work.spi_abort_ctl
        generic map (LEN_W => LEN_W)
        port map (clk => clk, rst_n => rst_n,
                  abort_req => abort_pend, clr_flags => clr_flags,
                  busy => busy, shift_en => shift_en, frame_done => frame_done,
                  bit_idx => bit_idx, len => cfg_len,
                  req_in => req_raw,
                  req_out => req_gated, abort_out => abort_out,
                  aborting => aborting, aborted => aborted,
                  abort_bits => abort_bits, rx_trunc => rx_trunc,
                  flush => flush, abort_ack => abort_ack);

    -- Assembled by field rather than by concatenation. A concatenation has to be
    -- exactly 32 bits wide and the padding depends on the generics, so a generic
    -- change turns into a silently misaligned status register.
    status_p : process (tx_ready, rx_ready, busy, stalled, rx_overrun, rx_trunc,
                       aborted, sel_err, len_err, div_err, aborting, abort_bits)
        variable s : std_logic_vector(31 downto 0);
    begin
        s := (others => '0');
        s(0)  := tx_ready;
        s(1)  := rx_ready;
        s(2)  := busy;
        s(3)  := stalled;
        s(4)  := rx_overrun;
        s(5)  := rx_trunc;
        s(6)  := aborted;
        s(7)  := sel_err;
        s(8)  := len_err;
        s(9)  := div_err;
        s(10) := aborting;
        s(16 + LEN_W - 1 downto 16) := std_logic_vector(abort_bits);
        status <= s;
    end process;

    -- Purely combinational, every branch assigned, no latch. RXDATA is the only
    -- read with a side effect, and that side effect is driven by `rd_rxdata`
    -- above rather than by this mux -- a read data path that also generates
    -- control is a read data path nobody can safely add a register to.
    read_p : process (reg_addr, cfg_cpol, cfg_cpha, cfg_lsb, cfg_div, cfg_len,
                      cfg_sel, cfg_lead, cfg_lag, cfg_gap, rx_rdata, status)
        variable d : std_logic_vector(31 downto 0);
    begin
        d := (others => '0');
        case to_integer(reg_addr) is
            when A_CTRL =>
                d(0) := cfg_cpol;
                d(1) := cfg_cpha;
                d(2) := cfg_lsb;
                d(8  + DIV_W - 1 downto 8)  := std_logic_vector(cfg_div);
                d(16 + LEN_W - 1 downto 16) := std_logic_vector(cfg_len);
                d(24 + SEL_W - 1 downto 24) := std_logic_vector(cfg_sel);
            when A_TIMING =>
                d(0  + CNT_W - 1 downto 0)  := std_logic_vector(cfg_lead);
                d(8  + CNT_W - 1 downto 8)  := std_logic_vector(cfg_lag);
                d(16 + CNT_W - 1 downto 16) := std_logic_vector(cfg_gap);
            when A_RXDATA =>
                d(MAX_W - 1 downto 0) := rx_rdata;
            when A_STATUS =>
                d := status;
            when others =>
                d := (others => '0');
        end case;
        reg_rdata <= d;
    end process;

    write_p : process (clk, rst_n)
    begin
        if rst_n = '0' then
            -- Reset values chosen so the master is INERT, not merely defined:
            -- mode 0, the slowest legal divisor, one byte, generous timing. A
            -- reset default that happens to be a fast clock is a reset default
            -- that can violate a slave's timing before software has run a single
            -- instruction.
            cfg_cpol   <= '0';
            cfg_cpha   <= '0';
            cfg_lsb    <= '0';
            cfg_div    <= to_unsigned(255, DIV_W);
            cfg_len    <= to_unsigned(8, LEN_W);
            cfg_sel    <= (others => '0');
            cfg_lead   <= to_unsigned(16, CNT_W);
            cfg_lag    <= to_unsigned(16, CNT_W);
            cfg_gap    <= to_unsigned(16, CNT_W);
            abort_pend <= '0';
        elsif rising_edge(clk) then
            if reg_wr = '1' then
                case to_integer(reg_addr) is
                    when A_CTRL =>
                        cfg_cpol <= reg_wdata(0);
                        cfg_cpha <= reg_wdata(1);
                        cfg_lsb  <= reg_wdata(2);
                        cfg_div  <= unsigned(reg_wdata(8 + DIV_W - 1 downto 8));
                        cfg_len  <= unsigned(reg_wdata(16 + LEN_W - 1
                                                       downto 16));
                        cfg_sel  <= unsigned(reg_wdata(24 + SEL_W - 1
                                                       downto 24));
                    when A_TIMING =>
                        cfg_lead <= unsigned(reg_wdata(0 + CNT_W - 1 downto 0));
                        cfg_lag  <= unsigned(reg_wdata(8 + CNT_W - 1 downto 8));
                        cfg_gap  <= unsigned(reg_wdata(16 + CNT_W - 1
                                                       downto 16));
                    when others =>
                        null;      -- TXDATA / TXLAST / CMD act through strobes
                end case;
            end if;

            -- Set by the command, cleared by the acknowledgement. Ordered so a
            -- command arriving on the same cycle as an acknowledgement is not
            -- lost -- the write wins, because a dropped abort is an abort the
            -- driver believes happened.
            if abort_ack = '1' then
                abort_pend <= '0';
            end if;
            if cmd_abort = '1' then
                abort_pend <= '1';
            end if;
        end if;
    end process;

    -- NOTE ON CONFIGURATION WHILE BUSY. The registers here accept writes at any
    -- time, and the datapath does NOT resample them mid-transfer: Chapter 13.2's
    -- latch takes a snapshot at the start of each frame. So a write during a
    -- transfer changes the NEXT frame and never corrupts the one in flight.
    -- That is a deliberate division of labour -- the register file stays a
    -- register file, and exactly one block owns the question of when
    -- configuration takes effect.

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top_tb.vhd — the same register-driven tests in VHDL
-- spi_master_top_tb.vhd
--
-- Every stimulus in this testbench goes through the REGISTER INTERFACE. There is
-- no poking of internal signals anywhere, because the interface is what is being
-- verified: a master whose blocks are all individually correct and whose register
-- map is wrong is a master nobody can write a driver for.
--
-- The closing test is a real flash transaction -- READ (0x03), three address
-- bytes, four data bytes, one chip select -- driven exactly as software would
-- drive it, and checked against a behavioural flash that only answers if the
-- command and address arrived correctly under a single unbroken assertion.

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

entity spi_master_top_tb is
end entity;

architecture sim of spi_master_top_tb is

    constant MAX_W : positive := 32;
    constant LEN_W : positive := 6;
    constant DIV_W : positive := 8;
    constant CNT_W : positive := 8;
    constant N_CS  : positive := 4;
    constant SEL_W : positive := 2;

    constant A_CTRL   : natural := 16#00#;
    constant A_TIMING : natural := 16#04#;
    constant A_TXDATA : natural := 16#08#;
    constant A_TXLAST : natural := 16#0C#;
    constant A_RXDATA : natural := 16#10#;
    constant A_STATUS : natural := 16#14#;
    constant A_CMD    : natural := 16#18#;

    constant S_TXRDY  : natural := 0;
    constant S_RXRDY  : natural := 1;
    constant S_BUSY   : natural := 2;
    constant S_OVR    : natural := 4;
    constant S_TRUNC  : natural := 5;
    constant S_ABORTD : natural := 6;
    constant S_SELERR : natural := 7;
    constant S_LENERR : natural := 8;
    constant S_DIVERR : natural := 9;

    type byte_array is array (0 to 63) of std_logic_vector(7 downto 0);
    type word_array is array (0 to 63) of std_logic_vector(31 downto 0);

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

    signal reg_addr  : unsigned(4 downto 0) := (others => '0');
    signal reg_wr    : std_logic := '0';
    signal reg_rd    : std_logic := '0';
    signal reg_wdata : std_logic_vector(31 downto 0) := (others => '0');
    signal reg_rdata : std_logic_vector(31 downto 0);

    signal sclk, mosi, miso : std_logic;
    signal cs_n : std_logic_vector(N_CS - 1 downto 0);

    -- A behavioural SPI slave that works off the PINS ONLY. It recovers bit
    -- boundaries from SCLK edges and word boundaries from chip select, exactly
    -- as a real device does.
    signal cs_q, sclk_q : std_logic := '0';
    signal sl_rx_sr : std_logic_vector(7 downto 0) := (others => '0');
    signal sl_bits  : natural := 0;
    signal sl_rx_q  : byte_array := (others => (others => '0'));
    signal sl_rx_n  : natural := 0;
    signal sl_tx_q  : byte_array := (others => (others => '0'));
    signal sl_tx_n  : natural := 0;
    signal sl_tx_sr : std_logic_vector(7 downto 0) := (others => '0');
    signal sl_tx_bits : natural := 0;
    signal sl_miso_r  : std_logic := '0';
    signal sl_cpol, sl_cpha : std_logic := '0';
    signal sl_clr : std_logic := '0';

    signal sl_cs : std_logic;
    signal sl_lead_edge, sl_trail_edge, sl_cap_edge, sl_drv_edge : std_logic;

    signal errors : natural := 0;

    function hex2(v : std_logic_vector(7 downto 0)) return string is
        constant DIGITS : string(1 to 16) := "0123456789abcdef";
        variable r : string(1 to 2);
    begin
        r(1) := DIGITS(to_integer(unsigned(v(7 downto 4))) + 1);
        r(2) := DIGITS(to_integer(unsigned(v(3 downto 0))) + 1);
        return r;
    end function;

    function hex8(v : std_logic_vector(31 downto 0)) return string is
        constant DIGITS : string(1 to 16) := "0123456789abcdef";
        variable u : unsigned(31 downto 0);
        variable r : string(1 to 8);
    begin
        u := unsigned(v);
        for k in 8 downto 1 loop
            r(k) := DIGITS(to_integer(u(3 downto 0)) + 1);
            u := shift_right(u, 4);
        end loop;
        return r;
    end function;

begin

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

    dut : entity work.spi_master_top
        generic map (MAX_W => MAX_W, LEN_W => LEN_W, DIV_W => DIV_W,
                     CNT_W => CNT_W, N_CS => N_CS, SEL_W => SEL_W)
        port map (clk => clk, rst_n => rst_n,
                  reg_addr => reg_addr, reg_wr => reg_wr, reg_rd => reg_rd,
                  reg_wdata => reg_wdata, reg_rdata => reg_rdata,
                  sclk => sclk, mosi => mosi, miso => miso, cs_n => cs_n);

    miso  <= sl_miso_r;
    sl_cs <= '0' when cs_n = (cs_n'range => '1') else '1';

    -- The capture edge is the LEADING edge when CPHA=0 and the TRAILING edge
    -- when CPHA=1, which in terms of the raw pin is:
    sl_lead_edge  <= '1' when sclk /= sclk_q and sclk /= sl_cpol else '0';
    sl_trail_edge <= '1' when sclk /= sclk_q and sclk =  sl_cpol else '0';
    sl_cap_edge   <= sl_trail_edge when sl_cpha = '1' else sl_lead_edge;
    sl_drv_edge   <= sl_lead_edge  when sl_cpha = '1' else sl_trail_edge;

    slave_p : process (clk)
    begin
        if rising_edge(clk) then
            sclk_q <= sclk;
            cs_q   <= sl_cs;

            if sl_clr = '1' then
                sl_rx_n    <= 0;
                sl_tx_n    <= 0;
                sl_bits    <= 0;
                sl_tx_bits <= 0;
                sl_rx_sr   <= (others => '0');
                sl_tx_sr   <= (others => '0');
                sl_miso_r  <= '0';
            elsif sl_cs = '1' and cs_q = '0' then
                -- A new transaction. With CPHA=0 the slave must already have its
                -- top bit on the pin, because the first capture is the first
                -- edge and there is no earlier moment. With CPHA=1 it must NOT
                -- -- the first leading edge is when both sides drive, and a
                -- slave that pre-drives there is one bit ahead for the whole
                -- transaction.
                sl_rx_sr <= (others => '0');
                sl_bits  <= 0;
                sl_tx_n  <= sl_tx_n + 1;
                if sl_cpha = '0' then
                    sl_tx_sr   <= sl_tx_q(sl_tx_n)(6 downto 0) & '0';
                    sl_miso_r  <= sl_tx_q(sl_tx_n)(7);
                    sl_tx_bits <= 1;
                else
                    sl_tx_sr   <= sl_tx_q(sl_tx_n);
                    sl_tx_bits <= 0;
                end if;
            elsif sl_cs = '1' then
                if sl_cap_edge = '1' then
                    sl_rx_sr <= sl_rx_sr(6 downto 0) & mosi;
                    if sl_bits = 7 then
                        sl_rx_q(sl_rx_n) <= sl_rx_sr(6 downto 0) & mosi;
                        sl_rx_n          <= sl_rx_n + 1;
                        sl_bits          <= 0;
                    else
                        sl_bits <= sl_bits + 1;
                    end if;
                end if;
                if sl_drv_edge = '1' then
                    if sl_tx_bits = 8 then
                        -- Word boundary. Only reached with CPHA=0, where the
                        -- first bit of each byte goes out half a period early.
                        sl_tx_sr   <= sl_tx_q(sl_tx_n)(6 downto 0) & '0';
                        sl_miso_r  <= sl_tx_q(sl_tx_n)(7);
                        sl_tx_bits <= 1;
                        sl_tx_n    <= sl_tx_n + 1;
                    else
                        sl_miso_r  <= sl_tx_sr(7);
                        sl_tx_sr   <= sl_tx_sr(6 downto 0) & '0';
                        sl_tx_bits <= sl_tx_bits + 1;
                    end if;
                end if;
            end if;
        end if;
    end process;

    stim : process
        variable errs  : natural := 0;
        variable guard : natural;
        variable bus_q : std_logic_vector(31 downto 0);
        variable got_q : word_array;
        variable got_n : natural := 0;
        variable st    : std_logic_vector(31 downto 0);
        variable w     : std_logic_vector(31 downto 0);

        procedure bus_write(a : natural; d : std_logic_vector(31 downto 0)) is
        begin
            wait until falling_edge(clk);
            reg_addr <= to_unsigned(a, 5); reg_wdata <= d; reg_wr <= '1';
            wait until falling_edge(clk);
            reg_wr <= '0';
        end procedure;

        procedure bus_read(a : natural) is
        begin
            wait until falling_edge(clk);
            reg_addr <= to_unsigned(a, 5); reg_rd <= '1';
            wait for 1 ns;
            bus_q := reg_rdata;
            wait until falling_edge(clk);
            reg_rd <= '0';
        end procedure;

        -- A status read, which pops nothing.
        procedure poll_status is
        begin
            bus_read(A_STATUS);
        end procedure;

        procedure configure(dv : natural; pol : std_logic; pha : std_logic;
                            lsb : std_logic; nbits : natural; slave : natural;
                            lead : natural; lag : natural; gap : natural) is
            variable d : std_logic_vector(31 downto 0);
        begin
            d := (others => '0');
            d(0) := pol; d(1) := pha; d(2) := lsb;
            d(15 downto 8)  := std_logic_vector(to_unsigned(dv, 8));
            d(21 downto 16) := std_logic_vector(to_unsigned(nbits, 6));
            d(25 downto 24) := std_logic_vector(to_unsigned(slave, 2));
            bus_write(A_CTRL, d);
            d := (others => '0');
            d(7 downto 0)   := std_logic_vector(to_unsigned(lead, 8));
            d(15 downto 8)  := std_logic_vector(to_unsigned(lag, 8));
            d(23 downto 16) := std_logic_vector(to_unsigned(gap, 8));
            bus_write(A_TIMING, d);
            sl_cpol <= pol; sl_cpha <= pha;
        end procedure;

        procedure collect is
        begin
            poll_status;
            while bus_q(S_RXRDY) = '1' loop
                bus_read(A_RXDATA);
                got_q(got_n) := bus_q;
                got_n        := got_n + 1;
                poll_status;
            end loop;
        end procedure;

        procedure send(d : std_logic_vector(31 downto 0); last : std_logic) is
        begin
            guard := 8000;
            poll_status;
            while bus_q(S_TXRDY) = '0' and guard > 0 loop
                poll_status;
                guard := guard - 1;
            end loop;
            if guard = 0 then
                report "  FAIL: the transmit queue never became ready";
                errs := errs + 1;
            end if;
            if last = '1' then bus_write(A_TXLAST, d);
            else               bus_write(A_TXDATA, d); end if;
        end procedure;

        -- Waits for the transfer to START and only then for it to finish.
        -- Waiting only for `busy` to fall is the classic driver bug: a request
        -- takes the programmed lead time to turn into a busy engine, so a poll
        -- issued immediately after the queue write sees an idle master and
        -- concludes the transfer is over before it has begun.
        procedure wait_done is
        begin
            guard := 4000;
            poll_status;
            while bus_q(S_BUSY) = '0' and guard > 0 loop
                collect;
                poll_status;
                guard := guard - 1;
            end loop;
            if guard = 0 then
                report "  FAIL: the master never became busy after a queue write";
                errs := errs + 1;
            end if;
            guard := 20000;
            poll_status;
            while bus_q(S_BUSY) = '1' and guard > 0 loop
                collect;
                poll_status;
                guard := guard - 1;
            end loop;
            for j in 1 to 6 loop wait until falling_edge(clk); end loop;
            collect;
        end procedure;

        -- Discards anything left in the receive register from an earlier test.
        procedure flush_rx is
        begin
            collect;
            got_n := 0;
        end procedure;

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

        variable pol_v, pha_v : std_logic;
        variable bad : natural;
        constant ALL_HIGH : std_logic_vector(N_CS - 1 downto 0)
                            := (others => '1');
    begin
        for j in 1 to 3 loop wait until falling_edge(clk); end loop;
        rst_n <= '1';
        for j in 1 to 2 loop wait until falling_edge(clk); end loop;

        -- 1. RESET DEFAULTS. The master must come up inert and READABLE: a
        --    register file whose reset value cannot be read back is a register
        --    file nobody can debug.
        bus_read(A_CTRL);
        if bus_q(7 downto 0) /= x"00" or bus_q(15 downto 8) /= x"FF" or
           bus_q(21 downto 16) /= "001000" then
            report "  FAIL: CTRL came out of reset as " & hex8(bus_q);
            errs := errs + 1;
        end if;
        bus_read(A_TIMING);
        if bus_q(7 downto 0) /= x"10" or bus_q(15 downto 8) /= x"10" or
           bus_q(23 downto 16) /= x"10" then
            report "  FAIL: TIMING came out of reset as " & hex8(bus_q);
            errs := errs + 1;
        end if;
        poll_status;
        if bus_q(S_TXRDY) /= '1' or bus_q(S_BUSY) /= '0' or
           bus_q(S_RXRDY) /= '0' then
            report "  FAIL: STATUS out of reset was " & hex8(bus_q);
            errs := errs + 1;
        end if;
        if cs_n /= ALL_HIGH then
            report "  FAIL: reset left a chip select asserted";
            errs := errs + 1;
        end if;
        report "  out of reset: mode 0, divisor 255, eight bits, 16-cycle windows, all selects released, queue ready, not busy";

        -- 2. READ-BACK OF EVERY WRITABLE FIELD.
        configure(6, '1', '1', '1', 13, 2, 7, 9, 11);
        bus_read(A_CTRL);
        if bus_q(0) /= '1' or bus_q(1) /= '1' or bus_q(2) /= '1' or
           bus_q(15 downto 8) /= x"06" or bus_q(21 downto 16) /= "001101" or
           bus_q(25 downto 24) /= "10" then
            report "  FAIL: CTRL read back " & hex8(bus_q) & " after programming";
            errs := errs + 1;
        end if;
        bus_read(A_TIMING);
        if bus_q(7 downto 0) /= x"07" or bus_q(15 downto 8) /= x"09" or
           bus_q(23 downto 16) /= x"0B" then
            report "  FAIL: TIMING read back " & hex8(bus_q) &
                   " after programming";
            errs := errs + 1;
        end if;
        report "  every writable field read back exactly as written";

        -- 3. THE ERROR FLAGS MEAN WHAT THEY SAY.
        configure(1, '0', '0', '0', 8, 0, 4, 4, 4);        -- divisor below two
        poll_status;
        if bus_q(S_DIVERR) /= '1' then
            report "  FAIL: a divisor of 1 did not raise the divisor error";
            errs := errs + 1;
        end if;
        configure(4, '0', '0', '0', 33, 0, 4, 4, 4);       -- width above 32
        poll_status;
        if bus_q(S_LENERR) /= '1' then
            report "  FAIL: a 33-bit frame did not raise the width error";
            errs := errs + 1;
        end if;
        configure(4, '0', '0', '0', 8, 0, 4, 4, 4);
        poll_status;
        if bus_q(S_DIVERR) = '1' or bus_q(S_LENERR) = '1' then
            report "  FAIL: a legal configuration still reports an error: " &
                   hex8(bus_q);
            errs := errs + 1;
        end if;
        report "  divisor 1 and width 33 each reported on their own, and a legal setting reports neither";

        -- 4. A COMPLETE TRANSACTION, all four modes, checked on the pins by a
        --    slave that only sees pins.
        for k in 0 to 3 loop
            if (k mod 2) = 1 then pol_v := '1'; else pol_v := '0'; end if;
            if k >= 2            then pha_v := '1'; else pha_v := '0'; end if;
            clear_slave;
            sl_tx_q(0) <= x"DE"; sl_tx_q(1) <= x"AD";
            sl_tx_q(2) <= x"BE"; sl_tx_q(3) <= x"EF";
            configure(4, pol_v, pha_v, '0', 8, 0, 4, 4, 4);
            for j in 1 to 4 loop wait until falling_edge(clk); end loop;
            flush_rx;
            -- Collected BETWEEN sends, because the receive side is one word deep
            -- (Chapter 13.9): a driver that queues every word first and only
            -- then starts reading loses all but the last one.
            send(x"00000012", '0'); collect;
            send(x"00000034", '0'); collect;
            send(x"00000056", '0'); collect;
            send(x"00000078", '1'); collect;
            wait_done;

            if sl_rx_n /= 4 then
                report "  FAIL: mode " & integer'image(k) &
                       " -- the slave saw " & integer'image(sl_rx_n) &
                       " of 4 bytes";
                errs := errs + 1;
            elsif sl_rx_q(0) /= x"12" or sl_rx_q(1) /= x"34" or
                  sl_rx_q(2) /= x"56" or sl_rx_q(3) /= x"78" then
                report "  FAIL: mode " & integer'image(k) &
                       " -- the slave saw " & hex2(sl_rx_q(0)) & " " &
                       hex2(sl_rx_q(1)) & " " & hex2(sl_rx_q(2)) & " " &
                       hex2(sl_rx_q(3));
                errs := errs + 1;
            end if;
            if got_n /= 4 then
                report "  FAIL: mode " & integer'image(k) &
                       " -- software read " & integer'image(got_n) &
                       " of 4 words";
                errs := errs + 1;
            elsif got_q(0)(7 downto 0) /= x"DE" or
                  got_q(1)(7 downto 0) /= x"AD" or
                  got_q(2)(7 downto 0) /= x"BE" or
                  got_q(3)(7 downto 0) /= x"EF" then
                report "  FAIL: mode " & integer'image(k) &
                       " -- software read " & hex2(got_q(0)(7 downto 0)) & " " &
                       hex2(got_q(1)(7 downto 0)) & " " &
                       hex2(got_q(2)(7 downto 0)) & " " &
                       hex2(got_q(3)(7 downto 0));
                errs := errs + 1;
            end if;
        end loop;
        report "  all four modes: the slave received 12 34 56 78 and software read back de ad be ef, over one chip select each";

        -- 5. LSB-FIRST, ON THE WIRE. The slave assembles MSB-first, so an
        --    LSB-first master must make it see the bit-reversed byte -- the only
        --    way to prove the setting reached the pins.
        clear_slave;
        configure(4, '0', '0', '1', 8, 0, 4, 4, 4);
        for j in 1 to 4 loop wait until falling_edge(clk); end loop;
        flush_rx;
        send(x"0000008D", '1');
        wait_done;
        if sl_rx_n /= 1 or sl_rx_q(0) /= x"B1" then
            report "  FAIL: LSB-first 0x8d -- the slave recorded " &
                   integer'image(sl_rx_n) & " bytes, the first being " &
                   hex2(sl_rx_q(0)) & ", expected 1 and b1";
            errs := errs + 1;
        end if;
        report "  LSB-first: software sent 8d and the MSB-first slave saw b1 -- the reversal happened on the wire";

        -- 6. ABORT THROUGH THE COMMAND REGISTER, mid-transfer, and recovery.
        clear_slave;
        configure(8, '0', '0', '0', 8, 0, 4, 6, 6);
        for j in 1 to 4 loop wait until falling_edge(clk); end loop;
        flush_rx;
        send(x"000000AA", '0'); collect;
        send(x"00000055", '0');
        for j in 1 to 40 loop wait until falling_edge(clk); end loop;
        bus_write(A_CMD, x"00000001");
        guard := 4000;
        poll_status;
        while bus_q(S_BUSY) = '1' and guard > 0 loop
            poll_status;
            guard := guard - 1;
        end loop;
        for j in 1 to 10 loop wait until falling_edge(clk); end loop;
        poll_status;
        st := bus_q;
        if st(S_ABORTD) /= '1' then
            report "  FAIL: an abort written to CMD did not set the abort flag";
            errs := errs + 1;
        end if;
        if st(S_TRUNC) /= '1' then
            report "  FAIL: an abort mid-byte did not report a truncated word";
            errs := errs + 1;
        end if;
        -- A truncated word that got nowhere is a contradiction, and it is
        -- exactly what a request serviced twice produces.
        if st(S_TRUNC) = '1' and st(21 downto 16) = "000000" then
            report "  FAIL: a truncated word was reported as having got 0 bits out";
            errs := errs + 1;
        end if;
        if st(S_BUSY) = '1' then
            report "  FAIL: the master was still busy long after the abort";
            errs := errs + 1;
        end if;
        if cs_n /= ALL_HIGH then
            report "  FAIL: the abort left a chip select asserted";
            errs := errs + 1;
        end if;
        report "  abort written to CMD: flagged, the partial word reported as truncated (" &
               integer'image(to_integer(unsigned(st(21 downto 16)))) &
               " bits made it), every select released";

        -- The flags must clear on command, and only on command.
        poll_status;
        if bus_q(S_ABORTD) /= '1' or bus_q(S_TRUNC) /= '1' then
            report "  FAIL: the sticky flags cleared themselves on a status read";
            errs := errs + 1;
        end if;
        bus_write(A_CMD, x"00000002");
        for j in 1 to 2 loop wait until falling_edge(clk); end loop;
        poll_status;
        if bus_q(S_ABORTD) = '1' or bus_q(S_TRUNC) = '1' then
            report "  FAIL: writing the clear bit left " & hex8(bus_q);
            errs := errs + 1;
        end if;
        report "  the sticky flags survive a status read and clear only on an explicit command";

        -- 7. THE FLASH TRANSACTION. READ (0x03), a 24-bit address and four data
        --    bytes, under one chip select, driven the way a driver drives it.
        clear_slave;
        sl_tx_q(0) <= x"FF"; sl_tx_q(1) <= x"FF";
        sl_tx_q(2) <= x"FF"; sl_tx_q(3) <= x"FF";
        sl_tx_q(4) <= x"C0"; sl_tx_q(5) <= x"FF";
        sl_tx_q(6) <= x"EE"; sl_tx_q(7) <= x"01";
        configure(4, '0', '0', '0', 8, 1, 5, 5, 8);
        for j in 1 to 4 loop wait until falling_edge(clk); end loop;
        flush_rx;
        send(x"00000003", '0'); collect;     -- READ
        send(x"00000001", '0'); collect;     -- address 0x012345
        send(x"00000023", '0'); collect;
        send(x"00000045", '0'); collect;
        send(x"00000000", '0'); collect;     -- four bytes to clock data out
        send(x"00000000", '0'); collect;
        send(x"00000000", '0'); collect;
        send(x"00000000", '1'); collect;
        wait_done;

        if sl_rx_n /= 8 then
            report "  FAIL: the flash transaction delivered " &
                   integer'image(sl_rx_n) & " of 8 bytes";
            errs := errs + 1;
        elsif sl_rx_q(0) /= x"03" or sl_rx_q(1) /= x"01" or
              sl_rx_q(2) /= x"23" or sl_rx_q(3) /= x"45" then
            report "  FAIL: the flash saw command " & hex2(sl_rx_q(0)) &
                   " address " & hex2(sl_rx_q(1)) & hex2(sl_rx_q(2)) &
                   hex2(sl_rx_q(3));
            errs := errs + 1;
        end if;
        if got_n /= 8 then
            report "  FAIL: software read " & integer'image(got_n) &
                   " of 8 words from the flash read";
            errs := errs + 1;
        elsif got_q(4)(7 downto 0) /= x"C0" or got_q(5)(7 downto 0) /= x"FF" or
              got_q(6)(7 downto 0) /= x"EE" or got_q(7)(7 downto 0) /= x"01" then
            report "  FAIL: the data phase returned " &
                   hex2(got_q(4)(7 downto 0)) & " " &
                   hex2(got_q(5)(7 downto 0)) & " " &
                   hex2(got_q(6)(7 downto 0)) & " " &
                   hex2(got_q(7)(7 downto 0));
            errs := errs + 1;
        end if;
        poll_status;
        if bus_q(S_OVR) = '1' or bus_q(S_TRUNC) = '1' or
           bus_q(S_ABORTD) = '1' or bus_q(S_SELERR) = '1' or
           bus_q(S_LENERR) = '1' or bus_q(S_DIVERR) = '1' then
            report "  FAIL: the flash transaction finished with status " &
                   hex8(bus_q);
            errs := errs + 1;
        end if;
        report "  flash READ 0x03 of address 012345 on slave 1: command and address arrived intact under one chip select, and the four data bytes came back as c0 ff ee 01 with every error flag clear";

        errors <= errs;
        if errs = 0 then
            report "PASS: the complete master is driven entirely through its register interface -- it comes out of reset inert and fully readable, every writable field reads back as written, and each of the divisor, width and slave errors is reported on its own while a legal configuration reports none -- four-byte transactions in all four modes deliver the right bytes to a slave that only sees pins and return the right words to software, LSB-first genuinely reverses the bits on the wire, an abort written to the command register stops the transfer, flags the truncated word with its partial bit count, releases every chip select and leaves the engine able to run the next transfer, the sticky flags survive a status read and clear only on command, and a flash READ of command plus three address bytes plus four data bytes completes under a single unbroken chip select with every error flag clear";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

Parity

All three implementations are driven entirely through the register interface — no internal signal is poked anywhere — and all three: come out of reset inert and fully readable; read back every writable field as written; report the divisor, width and slave errors each on their own while a legal configuration reports none; deliver 12 34 56 78 to a pin-level slave and read back de ad be ef in all four modes; send 0x8D LSB-first and have an MSB-first slave see 0xB1; abort mid-byte with six bits reported as having made it, every select released and the engine still working afterwards; keep the sticky flags across a status read and clear them only on command; and complete a flash READ of 0x03 plus a 24-bit address plus four data bytes under a single unbroken chip select with every error flag clear.

6. Why a Verification Engineer Cares

Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top.sva — the register contract, and the pin invariants
// A top-level assertion set should not restate the blocks' properties: they are
// already bound to the blocks. What belongs here is the REGISTER CONTRACT, which
// no block can state, and the pin invariants that are properties of the assembly
// rather than of any one part.

module spi_master_top_sva #(
    parameter int MAX_W = 32,
    parameter int N_CS  = 4
) (
    input logic              clk,
    input logic              rst_n,
    input logic [4:0]        reg_addr,
    input logic              reg_wr,
    input logic              reg_rd,
    input logic [31:0]       reg_wdata,
    input logic [31:0]       reg_rdata,
    input logic              sclk,
    input logic              mosi,
    input logic              miso,
    input logic [N_CS-1:0]   cs_n
);

    localparam [4:0] A_CTRL = 5'h00, A_TIMING = 5'h04, A_TXDATA = 5'h08,
                     A_TXLAST = 5'h0C, A_RXDATA = 5'h10, A_STATUS = 5'h14,
                     A_CMD = 5'h18;

    default clocking cb @(posedge clk); endclocking
    default disable iff (!rst_n);

    // A write to a configuration register must be readable back. Stated for the
    // whole register rather than field by field, because a field that reads back
    // in the wrong bit position is exactly the failure a concatenated status
    // word produces.
    property p_readback(addr, mask);
        logic [31:0] w;
        (reg_wr && reg_addr == addr, w = reg_wdata & mask) |=>
            ##[1:$] (reg_rd && reg_addr == addr) |-> (reg_rdata & mask) == w;
    endproperty
    a_ctrl_readback:   assert property (p_readback(A_CTRL,   32'h0303_FF07));
    a_timing_readback: assert property (p_readback(A_TIMING, 32'h00FF_FFFF));

    // STATUS is read-only: a write to it must change nothing that is readable.
    a_status_ro: assert property (
        reg_wr && reg_addr == A_STATUS |=> $stable(reg_rdata) || reg_rd
    );

    // A read of RXDATA is the ONLY read with a side effect. Every other read
    // must leave the receive-ready bit alone.
    a_only_rxdata_pops: assert property (
        reg_rd && reg_addr != A_RXDATA |=> $stable(rx_ready)
    );

    // Reset defaults must be INERT: the slowest legal divisor and generous
    // timing. A design that comes up fast can violate a slave before software
    // runs, and this is the only place that can be asserted.
    a_reset_inert: assert property (
        $rose(rst_n) |-> ##1 (cfg_div >= 8'd128) && (cfg_lead >= 8'd8)
    );

    // Pin invariants that belong to the assembly. At most one select low, ever.
    a_one_hot: assert property ($countones(~cs_n) <= 1);

    // No clock edge with nothing selected -- requirement R1 of Chapter 13.1,
    // asserted at the only level where both signals exist as pins.
    a_no_edge_deselected: assert property (
        &cs_n |-> $stable(sclk)
    );

    // MISO must not reach any output pin combinationally. Not expressible as a
    // temporal assertion -- it is a structural property -- so it is stated here
    // as a comment and checked by lint. Worth writing down at the point a
    // reviewer will look for it.
    //   lint: assert no_comb_path(miso -> {mosi, sclk, cs_n})

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_master_top_cg.sv — the configuration space, and what software actually did
// At the top level the coverage question changes. The blocks' internal cases are
// covered by their own models; what needs covering here is the CONFIGURATION
// SPACE as software can reach it, and the register access PATTERNS that a driver
// will actually produce.

covergroup cg_master_top @(posedge clk);

    // The four modes must each carry real traffic, not merely be written.
    mode: coverpoint {cfg_cpol, cfg_cpha} iff (frame_start) {
        bins mode0 = {2'b00};
        bins mode1 = {2'b10};
        bins mode2 = {2'b01};
        bins mode3 = {2'b11};
    }

    // Transaction shapes, named after the things drivers actually do. A suite
    // that only sends single bytes has not tested the master a flash needs.
    shape: coverpoint txn_shape iff (txn_end) {
        bins single_byte    = {1};
        bins cmd_plus_data  = {2};
        bins cmd_addr_data  = {[5:8]};
        bins page_sized     = {[9:$]};
    }

    // Which register was written, and in what order relative to a transaction.
    // A configuration write DURING a transaction is legal and must be seen.
    wr_when: coverpoint {wr_addr, busy} iff (reg_wr) {
        bins ctrl_idle    = {{5'h00, 1'b0}};
        bins ctrl_busy    = {{5'h00, 1'b1}};
        bins timing_idle  = {{5'h04, 1'b0}};
        bins timing_busy  = {{5'h04, 1'b1}};
        bins txdata       = {{5'h08, 1'b0}, {5'h08, 1'b1}};
        bins txlast       = {{5'h0C, 1'b0}, {5'h0C, 1'b1}};
        bins cmd_abort    = {{5'h18, 1'b1}};
        bins cmd_idle     = {{5'h18, 1'b0}};
    }

    // Each error flag must have been raised ALONE. A configuration that trips
    // two at once cannot show that either is wired correctly.
    solo_error: coverpoint status[9:7] iff (|status[9:7]) {
        bins sel_only = {3'b001};
        bins len_only = {3'b010};
        bins div_only = {3'b100};
    }

    // And the clean case, because a status register that always reports
    // something is a status register nobody reads.
    all_clear: coverpoint (status[9:4] == 6'b000000) iff (txn_end) {
        bins clean = {1};
    }

    // Whether the slave model that observed this transaction was PIN-LEVEL.
    // A suite whose every check came from a strobe-driven model cannot find a
    // fault in the strobes, and this coverpoint is what records that.
    observer: coverpoint slave_is_pin_level iff (txn_end) {
        bins pin_level   = {1};
        bins strobe_fed  = {0};
    }

    x_mode_shape:    cross mode, shape;
    x_shape_observer: cross shape, observer;

endgroup

7. Why an FPGA or ASIC Engineer Cares

The whole master is roughly 250 flops. Counting them by block at the defaults:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   13.4  divider            13
   13.5  mode logic         12
   13.6  datapath           68     two 32-bit registers plus counters
   13.8  width and order     0     entirely combinational
   13.7  chip select        20
   13.9  streaming          70     shadow plus receive holding register
   13.10 abort policy       10
   top   register file      50
   ---------------------------
                           243

The two 32-bit shift registers and the two 32-bit holding registers are two-thirds of it, which is worth knowing: a design that only ever needs byte transfers can set MAX_W = 8 and the master drops to about 100 flops without any structural change.

Timing closure has exactly one candidate. shift_en gates the divider and is the only signal in the design on a path that matters at the bit rate. Everything else is once-per-frame. If the master fails timing, that is where to look, and one-hot encoding the chip-select controller's state makes it a flop output rather than a decode.

The combinational blocks are the two barrel shifters, and both are off the per-bit path. The transmit-side alignment and reversal are evaluated on the load cycle; the receive-side reversal on the read. If either ever mattered, the receive side can be registered for free — the received word is stable from rx_valid_stb until the next load.

Lint and CDC are clean by construction rather than by effort. One clock means no CDC report at all. No latches, because every combinational block assigns every output on every path. No unconstrained paths, because MISO enters a flop. Those are the three reports that usually consume a week, and the reason they are empty is the six rules of §3 rather than any cleanup.

8. The RTL Review Checklist

This is the review that would gate the design, in the order a reviewer should ask. Each item names the chapter that answers it, so a "no" has somewhere to go.

Clocking and reset

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] one clock domain, and SCLK is not a clock            13.4
   [ ] nothing clocked on SCLK                              13.4
   [ ] async reset on every flop that drives a pin          13.10
   [ ] reset defaults leave the master inert, not just defined  13.11
   [ ] reset releases chip select with no clock edge         13.10

Pins

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] MOSI is a flop, not a mux off the shift register      13.6
   [ ] MISO enters one flop and reaches no output pin        13.6
   [ ] at most one select asserted, ever                     13.7
   [ ] no SCLK edge with nothing selected                    13.1
   [ ] no clock edge after the select is released, on abort   13.10

Timing intervals

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] lead, lag and gap are programmable and MEASURED        13.7
   [ ] intervals are checked two-sided: not short, not long   13.7
   [ ] counters load with the interval minus one              13.7
   [ ] the select is held across every frame of a transaction 13.7

Configuration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] configuration is snapshotted at frame start            13.2
   [ ] all fields snapshot together, on one condition          13.2
   [ ] a mid-transfer write is accepted and REPORTED           13.2
   [ ] every writable field reads back                         13.11
   [ ] illegal divisor, width and select are reported          13.4 13.8 13.7

Data path

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] two shift registers, not one circulating                13.6
   [ ] the receive register is cleared on load                  13.6
   [ ] bit order is one self-inverse transform at the boundaries 13.8
   [ ] the valid pulse coincides with complete data              13.6
   [ ] a narrow frame leaves zeros above it                      13.6

Modes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] the launch and capture assignment depends on CPHA alone   13.5
   [ ] CPHA=0 preloads, and the preload ADVANCES the register    13.5
   [ ] CPHA=0's final launch is suppressed                       13.5
   [ ] the frame-done level is not asserted on the start cycle   13.5

Streaming and stopping

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] the shadow is freed at load, not at frame completion      13.9
   [ ] a stall is reported and is not treated as an error        13.9
   [ ] an overrun is sticky and clears only on command           13.9
   [ ] the transaction tail is not counted as a stall            13.9
   [ ] the abort request is edge-qualified                        13.10
   [ ] the abort reports a partial bit count, and it is non-zero
       whenever truncation is flagged                             13.10
   [ ] the engine recovers after every abort                      13.10

Verification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   [ ] at least one slave model is PIN-LEVEL and shares nothing
       with the design                                            13.11
   [ ] bit order is checked on the WIRE, not in loopback           13.8
   [ ] both directions are compared against an independent pattern 13.6
   [ ] every error flag has been raised ALONE                       13.1
   [ ] coverage bins margins relative to limits, not absolute values 13.1

9. Failure Signature — A Master That Passes Every Test And Fails Review

Symptom. A master passes its entire regression: all four modes, every width, loopback and independent-slave data checks, abort at every bit position, measured intervals within limits. It is submitted for review and rejected on three items.

This is worth working through because it is the most common outcome of a design that was verified without being reviewed, and none of the three failures is a functional bug.

Item 1: SCLK is counter[2]. It works — the frequency is right and the duty cycle is even at power-of-two divisors. It is a divided clock, so synthesis infers a clock tree, the tool asks for a create_generated_clock constraint, and anything a future engineer clocks on it is in a second domain whose frequency is a runtime parameter. The fix is a registered output, which also makes odd divisors possible — so the reviewer's objection fixes a functional limitation the tests never exercised because the suite only used even divisors.

Item 2: the receive register is read directly by software. It works, as long as software reads between frames. During a frame the register is being shifted, so a read that lands mid-frame returns a partially-shifted word — and in a streaming transaction there is no "between frames" for software to aim at. The tests never caught it because the testbench polled a status bit before reading, which a real driver under interrupt load will not do reliably. The fix is a holding register, which also makes the overrun flag meaningful.

Item 3: cs_n is synchronously reset. It works in every test, because every test has a running clock. The reviewer's objection is about the case the tests cannot contain: a reset asserted when the clock has stopped. The fix is one attribute on one always block.

What the three have in common. All three are cases where the test environment is more forgiving than the real one — even divisors, a polling driver, a running clock — and none of them can be found by a suite that does not deliberately model the environment's hostility. That is what a review is for, and it is why the checklist in §8 is organised by property rather than by block: a property that no test can reach still has to be argued for.

The lesson for a suite. Every item in §8 that a test cannot check is an item that needs a reviewer, and knowing which is which is worth writing down. Three in that list are unreachable by simulation: the reset-with-no-clock case, the no-combinational-path-from-MISO case, and the SCLK-is-not-a-clock case. Everything else in the list is checkable, and everything checkable should be checked rather than reviewed.

10. Common Misconceptions

"A top level with no logic in it is a sign the design was over-partitioned." It is a sign the decisions were made where they belong. The test is whether any block could be replaced independently — and here each can, because each was verified against pins rather than against its neighbours.

"A register map is an implementation detail that can be changed later." It is the only part of the design software depends on, and it is the part that cannot be changed later without changing every driver. The two-address queue and the read-only status register are both decisions that would be expensive to reverse.

"Reset defaults only need to be defined, not safe." The first thing many drivers do is read a device ID, and they do it before configuring anything. A default of the fastest divisor means that read happens at a frequency the slave may not support.

"If the suite passes, the design is done." Three properties in §8 cannot be reached by simulation at all — reset with no clock, no combinational path from MISO, and SCLK not being a clock — and all three are design-review items. A passing suite is necessary and not sufficient, and knowing precisely which properties it cannot reach is more useful than assuming it reaches everything.

"A pin-level slave model is a nice-to-have when an ideal one is easier." It is the only component in the suite capable of finding a fault in the design's own abstractions, and it found one here that eleven unit testbenches missed. It is not a cleaner model — it is a differently founded one, and that is the point.

11. Reason It Through

Why two register addresses for the transmit queue rather than a flag bit?

Because at 32 bits of data there is no bit to spare, because a separate flag register is two bus cycles and a race, and because forgetting to use a different address is a more visible mistake than forgetting to set a bit. The consequence of forgetting is a transaction left open with the slave selected, so making the mistake visible at the call site is worth an address bit.

Why is the status word assembled by field rather than by concatenation?

Because a concatenation must be exactly 32 bits and the padding depends on the parameters, so a parameter change silently misaligns every flag above the changed field. With LEN_W at its default the padding expressions come out zero bits wide, which is not even legal — and indexed part-selects into a zeroed word cannot drift no matter what the parameters do.

What breaks if the register file's outputs feed the SCLK-rate logic directly, with no snapshot?

A configuration write lands mid-frame and changes the bit period, the width or the mode partway through a transfer. The frame's boundaries move, the slave is out of step, and the transfer completes returning plausible garbage. Because the corruption depends on when the write landed relative to the frame, it is not reproducible — which is Chapter 13.2's subject and the reason the snapshot exists.

Which properties in the review checklist can a simulation not check, and why?

Three. Reset with no clock running, because a testbench that stops the clock stops the simulation's notion of time and cannot then observe anything. The absence of a combinational path from MISO to an output, because that is a structural property of the netlist rather than a behaviour. And SCLK not being a clock, because functionally a divided clock and a registered output are indistinguishable — the difference is what the synthesis tool infers. All three are review items, and writing down that they are is more useful than hoping a test covers them.

A design passes every functional test and the reviewer rejects three items, none of which is a functional bug. What do the three have in common?

Each is a case where the test environment is more forgiving than the real one: only even divisors were used, so a divided clock looked fine; the testbench polled before reading, so a directly-read shift register looked fine; every test had a running clock, so a synchronous reset on the select pins looked fine. A suite tests the design against the environment it models, and a review tests the design against the environment it will meet — which is why the checklist is organised by property rather than by block.

12. Understanding Check

13. Summary

The top level is seven instances and a register file, and that is the test of the partition: every decision worth arguing about was made in a block, and logic at the top would mean one was made in the wrong place.

The register map's three defended decisions are two addresses for one queue — no data bit to spare, no two-cycle race, and a mistake that is visible at the call site; a read-only and complete status register, reporting the errors that mean the driver is wrong as well as the ones it is expected to handle; and reset defaults that leave the master inert, because many drivers read a device ID before configuring anything.

Six rules make it synthesisable: one clock with nothing clocked on SCLK, one asynchronous reset on every pin-driving flop, no latches, no gated or generated clocks, no combinational path from MISO to any output, and no simulation constructs outside guards. The last two are the ones that fail review, and both fail because of features added later — a loopback mode muxing MISO to MOSI, or a debug bit driving SCLK.

A pin-level slave model found a bug eleven unit testbenches missed: a runt ninth clock pulse on every CPHA=1 frame, invisible from inside the master because every internal count correctly agreed it was not there. A testbench built from the design's own abstractions cannot find bugs in those abstractions, and the insurance is one model that shares nothing with the design.

The whole master is about 250 flops, two-thirds of them the four 32-bit registers — so a byte-only configuration drops to roughly 100 without structural change. One signal matters for timing: shift_en, which gates the divider. Lint and CDC are empty by construction rather than by cleanup.

The review checklist is organised by property rather than by block, and three of its items cannot be checked by simulation at all: reset with the clock stopped, the absence of a combinational MISO path, and SCLK not being a clock. Knowing which properties a suite cannot reach is more useful than assuming it reaches everything — and the three functional-looking review rejections of §9 all came from the test environment being more forgiving than the real one.

14. What Comes Next

This closes Module 13, and with it the first complete design in the track. Thirteen modules have gone from what SPI is, through what a device expects, to a master that produces it — verified in three languages, reviewed against the properties a simulation cannot reach, and driven the way software will drive it.

What the module did not build is the other end of the wire. A master decides when everything happens; a slave is told, and has to recover every boundary from pins it does not control — no clock of its own, no say in when chip select falls, and a setup time it must meet against an edge that arrives whenever the master feels like it. Every simplification this module enjoyed by owning the clock is a problem there, and the shift in perspective is the point: the next module builds the device that has to survive a master like the one just designed.

Continue learning