Skip to content
VLSI Mentor

SPI · Module 13

Master Microarchitecture

Partitioning an SPI master into blocks that each earn their place: why control and datapath separate along the line of what changes per bit, why configuration must be snapshotted at transfer start, and what a mid-transfer write must do instead of being ignored.

Chapter 13.1 produced six requirements and an instrument that checks them. Nothing has been designed. This chapter makes the first structural decision, and it is the one every later chapter inherits.

Software writes a new divisor to the control register while a transfer is running. What should the hardware do?

There are three answers. Two of them are wrong in ways that are hard to find, and picking between them is not a coding question — it is the microarchitecture.

1. The Partition That Matters

An SPI master has to do six things, and they divide cleanly if you ask the right question about each. The question is not "is this control or data?" — that framing produces arguments. It is:

Does this logic do something different on every bit, or something once per transfer?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   once per transfer                    every bit
   ------------------------------------ --------------------------------------
   decode the request                   shift out the next bit
   snapshot the configuration           shift in the arriving bit
   assert chip select, pay the lead      toggle SCLK
   count the frames of a burst          count the bits
   pay the lag, release chip select
   pay the inter-transaction gap

The left column is control. The right column is datapath. The division is not stylistic: the right column is clocked at the SCLK rate and the left is not, so anything that migrates from left to right lands on the timing-critical path, and anything that migrates the other way gets re-evaluated when it should have been frozen.

That gives the blocks, and each one is a chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   13.4  clock divider        SCLK and two edge strobes
   13.5  mode logic           which edge launches, which captures
   13.6  shift datapath       the two shift registers
   13.8  width and order      frame width and bit order
   13.7  chip-select control   lead, lag, gap, and burst hold
   13.9  streaming            keeping the wire busy between frames
   13.10 abort policy         stopping without leaving the bus illegal
   13.2  configuration latch  when the settings take effect

The last one is this chapter's, and it exists because of the question in the epigraph.

2. The Block Diagram

An SPI master partitioned into control and datapath. A register interface feeds a configuration latch, which snapshots mode, divisor, width, bit order and length at the start of each transfer. The latch feeds both the chip-select controller and the clock divider. The divider produces two edge strobes that feed the mode logic, which produces launch and capture strobes for the shift datapath. The shift datapath drives MOSI and samples MISO. The chip-select controller drives the select pins and gates the divider.Register filesoftware writes here, anytimeConfig latch13.2 — snapshot at framestartCS control13.7 — lead, lag, gap, holdStreaming13.9 — one word of slackAbort policy13.10 — stop, legallyClock divider13.4 — SCLK and two strobesMode logic13.5 — launch or captureShift datapath13.6 — two registersWidth and order13.8 — a boundary transformSCLK pina registered outputCS pinsone low, everMOSI / MISOone flop from each pinwritestimingdiv, cpolcphalen, orderenableedge A, edge Blaunch, captureload12
Figure 1 — the master's blocks and the boundary that matters. The dashed edge from the abort block into chip-select control is how a transfer is abandoned without leaving the bus illegal. Everything to the right of the dashed line runs at the SCLK rate and sees only strobes; everything to the left runs once per transfer and sees the configuration. The latch is the boundary: it is the only block both sides read, and it is the reason nothing on the right can change mid-frame.

Two things in that diagram are worth naming before the rest of the module relies on them.

Nothing in the middle column reads the register file. Everything it needs arrives from the latch, which means a register write cannot reach the SCLK-rate logic at all — not by accident, not by timing, not by a path someone adds later. That is a structural guarantee rather than a discipline, and structural guarantees are the only ones that survive maintenance.

The divider's enable comes from chip-select control, not from the register file. The clock runs when a transfer is in progress and at no other time, and the block that knows whether a transfer is in progress is the one that owns the select pin. An enable derived from a "start" bit in a register would let the clock run before chip select had settled, which is requirement R2 from Chapter 13.1.

3. The Question, and the Three Answers

Software writes a new divisor mid-transfer. The candidates:

Answer A — the datapath reads the registers continuously. Simplest to write, and wrong. The divisor changes mid-frame, so the second half of a bit period is a different length from the first; the frame's bit boundaries move; the slave, which is counting edges, is now out of step. The transfer completes and returns plausible garbage, and because the corruption depends on when the write landed relative to the frame, it is not reproducible.

Answer B — writes are rejected while busy. Safe and defensible, and it pushes a problem into software: the driver must now poll a status bit before every configuration write, and handle the case where the write was refused. It also makes a common pattern awkward — configuring the next transfer while the current one runs is exactly what a driver wants to do, and rejecting it forces the bus to idle between transfers for no hardware reason.

Answer C — the registers accept the write, and the datapath uses a snapshot taken at frame start. The write succeeds, nothing in flight changes, and the new setting takes effect on the next frame. Software gets a register file that behaves like a register file; hardware gets a configuration that is stable for the whole of every frame.

C is the answer, and the block that implements it is one register-width of flops plus a single load condition. The cost is trivial; the value is that there is exactly one place in the design where the question "when does configuration take effect" is answered, and every other block is relieved of it.

4. What "Snapshot" Has To Mean Precisely

The load condition looks obvious and has two traps in it.

It must be start && !busy, not start. A start request arriving while a transfer is already running must not reload the snapshot, or the running transfer's configuration changes underneath it — which is Answer A with extra steps. In a burst (Chapter 13.7) the second and subsequent frames are started while the transaction is still open, and it is precisely those frames that must keep the first frame's settings.

The snapshot must not be partially updated. All five fields — mode, divisor, width, bit order, length — load together or not at all. A design that loads each field when that field's register is written has no snapshot at all; it has five independent races. The condition is one condition and it drives all five loads.

And the change_ignored logic has a trap of its own, which is the one this chapter's testbench exists to catch:

A mid-transfer write must set the flag and change nothing. It is very easy to write the flag logic as "if busy and the request differs, record it" and then — in the same branch, because it reads naturally — also assign the configuration. The result is a design that sets the flag correctly and also corrupts the transfer, which is worse than either mistake alone: the flag says the write was ignored, and it was not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   start && !busy    ->  load all five fields, clear change_ignored
   busy && differs   ->  set change_ignored, assign NOTHING
   otherwise         ->  hold

5. Building the Configuration Latch — Three HDLs

The circuit

The latch takes a requested configuration and a start/busy pair, and publishes the configuration the datapath should use plus the change_ignored flag. It also publishes unstable, which counts cycles on which a published field changed while busy was asserted — a self-check that exists so the testbench can assert the central property directly rather than inferring it.

unstable is worth a note. It is redundant in a correct design: it must be zero always, so it carries no information when the design is right. That is exactly what makes it valuable during development, and it costs one comparator and one counter. In a production build it can be parameterised away; the habit worth keeping is that the property a block exists to guarantee should be observable at the block's own boundary.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch.sv — the snapshot, and the flag that says a write was too late
// spi_cfg_latch.sv
//
// Chapter 13.2 -- where the control/datapath boundary actually sits.
//
// A microarchitecture is a set of decisions about which block knows what.
// This block is the first of those decisions, and it is the one that makes
// every later chapter simpler:
//
//   THE DATAPATH NEVER SEES A CHANGING CONFIGURATION.
//
// Mode, divisor, frame width, bit order and length are latched once, at the
// instant a transfer starts, and held for its whole duration. So the shift
// register (Ch 13.6), the divider (Ch 13.4), the edge logic (Ch 13.5) and
// the chip-select generator (Ch 13.7) can each read their parameter as a
// constant. None of them needs to handle it changing, because it cannot.
//
// That is worth stating as a cost as well as a benefit. The cost is one
// register per parameter -- about thirty flip-flops. The benefit is that
// four blocks lose an entire class of behaviour they would otherwise have
// to implement and verify. Buying simplicity in four places with thirty
// flip-flops in one is the trade this chapter argues for.
//
// A request that changes mid-transfer is IGNORED and REPORTED. Not applied
// late, not applied partially, and not silently dropped -- because software
// writing a new divisor while a transfer runs has made a mistake it needs
// to be told about, and a controller that applied it would produce a frame
// matching no device (Chapter 10.1's atomicity argument, now about time
// rather than about validity).

module spi_cfg_latch #(
    parameter int DIV_W = 8,
    parameter int LEN_W = 16
) (
    input  logic             clk,
    input  logic             rst_n,

    input  logic             start,    // pulse: begin a transfer
    input  logic             busy,     // from the control FSM

    // The requested configuration. May change at any time.
    input  logic [1:0]       req_mode,
    input  logic [DIV_W-1:0] req_div,
    input  logic [5:0]       req_width,
    input  logic             req_lsb_first,
    input  logic [LEN_W-1:0] req_len,

    // The configuration in force. Constant for the whole transfer.
    output logic [1:0]       cfg_mode,
    output logic [DIV_W-1:0] cfg_div,
    output logic [5:0]       cfg_width,
    output logic             cfg_lsb_first,
    output logic [LEN_W-1:0] cfg_len,
    output logic             cfg_valid,

    output logic             change_ignored  // sticky: a change was refused
);

    // Does the request currently differ from what is in force? Computed
    // continuously so a mid-transfer change is detected on the cycle it
    // appears rather than at the end of the transfer.
    wire differs = (req_mode      != cfg_mode)   ||
                   (req_div       != cfg_div)    ||
                   (req_width     != cfg_width)  ||
                   (req_lsb_first != cfg_lsb_first) ||
                   (req_len       != cfg_len);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset loads a safe configuration rather than an arbitrary one:
            // mode 0, the slowest divisor, eight bits, MSB first. The same
            // argument as Chapter 11.2's reset default -- a controller that
            // has not been configured must not be able to run fast or in an
            // exotic mode.
            cfg_mode       <= 2'd0;
            cfg_div        <= {DIV_W{1'b1}};
            cfg_width      <= 6'd8;
            cfg_lsb_first  <= 1'b0;
            cfg_len        <= {{(LEN_W-1){1'b0}}, 1'b1};
            cfg_valid      <= 1'b0;
            change_ignored <= 1'b0;
        end else begin
            if (start && !busy) begin
                // The single instant at which the configuration may change.
                cfg_mode       <= req_mode;
                cfg_div        <= req_div;
                cfg_width      <= req_width;
                cfg_lsb_first  <= req_lsb_first;
                cfg_len        <= req_len;
                cfg_valid      <= 1'b1;
                // A new transfer clears the previous verdict, so software
                // reads a report about the transfer it is looking at.
                change_ignored <= 1'b0;
            end else if (busy && differs) begin
                // Refused, and recorded. Note what is NOT here: any
                // assignment to cfg_*. The refusal is the absence of an
                // action rather than an action of its own, which is why no
                // partial application is possible.
                change_ignored <= 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch_tb.sv — every field rewritten mid-transfer, nothing allowed to move
// spi_cfg_latch_tb.sv
//
// The property under test is STABILITY, and it is checked continuously
// rather than at the end: the configuration must not change on any cycle
// while busy, so the testbench snapshots it when busy rises and compares
// every cycle until busy falls.
//
// Checking only at the end would pass a design that changed the
// configuration mid-transfer and changed it back.

`timescale 1ns/1ps

module spi_cfg_latch_tb;

    localparam int DIV_W = 8;
    localparam int LEN_W = 16;

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

    logic             start = 1'b0;
    logic             busy = 1'b0;
    logic [1:0]       req_mode = 2'd0;
    logic [DIV_W-1:0] req_div = 8'd4;
    logic [5:0]       req_width = 6'd8;
    logic             req_lsb_first = 1'b0;
    logic [LEN_W-1:0] req_len = 16'd1;

    // Driven by the DUT, so declared as nets: a module output cannot drive
    // a variable in Verilog-2001, and using `wire` here keeps the two
    // published sources identical in structure.
    wire [1:0]       cfg_mode;
    wire [DIV_W-1:0] cfg_div;
    wire [5:0]       cfg_width;
    wire             cfg_lsb_first;
    wire [LEN_W-1:0] cfg_len;
    wire             cfg_valid;
    wire             change_ignored;

    int errors = 0;

    spi_cfg_latch #(.DIV_W(DIV_W), .LEN_W(LEN_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .start(start), .busy(busy),
        .req_mode(req_mode), .req_div(req_div), .req_width(req_width),
        .req_lsb_first(req_lsb_first), .req_len(req_len),
        .cfg_mode(cfg_mode), .cfg_div(cfg_div), .cfg_width(cfg_width),
        .cfg_lsb_first(cfg_lsb_first), .cfg_len(cfg_len),
        .cfg_valid(cfg_valid), .change_ignored(change_ignored)
    );

    // Continuous stability checker: snapshot on the first busy cycle, then
    // compare on every subsequent one.
    logic [1:0]       s_mode;
    logic [DIV_W-1:0] s_div;
    logic [5:0]       s_width;
    logic             s_lsb;
    logic [LEN_W-1:0] s_len;
    logic             snapped;
    int               unstable_cycles;

    always_ff @(posedge clk) begin
        if (!rst_n) begin
            snapped         <= 1'b0;
            unstable_cycles <= 0;
        end else if (busy) begin
            if (!snapped) begin
                s_mode <= cfg_mode; s_div <= cfg_div; s_width <= cfg_width;
                s_lsb  <= cfg_lsb_first; s_len <= cfg_len;
                snapped <= 1'b1;
            end else if (cfg_mode !== s_mode || cfg_div !== s_div ||
                         cfg_width !== s_width || cfg_lsb_first !== s_lsb ||
                         cfg_len !== s_len) begin
                unstable_cycles <= unstable_cycles + 1;
            end
        end else begin
            snapped <= 1'b0;
        end
    end

    task automatic request(input int m, input int d, input int w,
                           input bit lsb, input int n);
        begin
            @(negedge clk);
            req_mode = 2'(m); req_div = DIV_W'(d); req_width = 6'(w);
            req_lsb_first = lsb; req_len = LEN_W'(n);
        end
    endtask

    // Begin a transfer of `cycles` busy cycles.
    task automatic run(input int cycles);
        begin
            @(negedge clk);
            start = 1'b1;
            @(negedge clk);
            start = 1'b0;
            busy = 1'b1;
            repeat (cycles) @(negedge clk);
            busy = 1'b0;
            @(negedge clk);
        end
    endtask

    task automatic check_cfg(input string what, input int m, input int d,
                             input int w, input bit lsb, input int n);
        begin
            if (cfg_mode !== 2'(m) || cfg_div !== DIV_W'(d) ||
                cfg_width !== 6'(w) || cfg_lsb_first !== lsb ||
                cfg_len !== LEN_W'(n)) begin
                $display("  FAIL: %s -- cfg is mode=%0d div=%0d width=%0d lsb=%0b len=%0d, wanted %0d/%0d/%0d/%0b/%0d",
                         what, cfg_mode, cfg_div, cfg_width, cfg_lsb_first,
                         cfg_len, m, d, w, lsb, n);
                errors++;
            end
        end
    endtask

    initial begin
        unstable_cycles = 0; snapped = 1'b0;
        repeat (3) @(negedge clk);
        rst_n = 1'b1;
        @(negedge clk);

        // 1. RESET LOADS A SAFE CONFIGURATION, and it is not yet valid --
        //    nothing may transact on a configuration nobody supplied.
        if (cfg_valid) begin
            $display("  FAIL: cfg_valid set out of reset"); errors++;
        end
        if (cfg_div !== {DIV_W{1'b1}} || cfg_mode !== 2'd0 ||
            cfg_width !== 6'd8 || cfg_lsb_first !== 1'b0) begin
            $display("  FAIL: reset did not load the safe configuration (mode=%0d div=%0d width=%0d lsb=%0b)",
                     cfg_mode, cfg_div, cfg_width, cfg_lsb_first);
            errors++;
        end
        $display("  reset: mode=%0d div=%0d width=%0d lsb=%0b, valid=%0b -- safe and not yet usable",
                 cfg_mode, cfg_div, cfg_width, cfg_lsb_first, cfg_valid);

        // 2. A START LATCHES THE WHOLE REQUEST.
        request(3, 4, 16, 1'b1, 32);
        run(10);
        check_cfg("after start", 3, 4, 16, 1'b1, 32);
        if (!cfg_valid) begin
            $display("  FAIL: cfg_valid not set after a start"); errors++;
        end
        if (change_ignored) begin
            $display("  FAIL: a change was reported when none was made");
            errors++;
        end
        $display("  latched: mode=%0d div=%0d width=%0d lsb=%0b len=%0d",
                 cfg_mode, cfg_div, cfg_width, cfg_lsb_first, cfg_len);

        // 3. A CHANGE MID-TRANSFER IS REFUSED AND REPORTED. Every field is
        //    changed, so a design that latched any one of them would fail.
        @(negedge clk);
        start = 1'b1;
        @(negedge clk);
        start = 1'b0;
        busy = 1'b1;
        repeat (3) @(negedge clk);
        req_mode = 2'd1; req_div = 8'd99; req_width = 6'd12;
        req_lsb_first = 1'b0; req_len = 16'd7;
        repeat (5) @(negedge clk);
        check_cfg("during a refused change", 3, 4, 16, 1'b1, 32);
        if (!change_ignored) begin
            $display("  FAIL: a mid-transfer change was not reported"); errors++;
        end
        busy = 1'b0;
        @(negedge clk);
        // And it stays refused after busy drops -- the change is not applied
        // late, which would be the most confusing possible behaviour.
        check_cfg("after busy fell", 3, 4, 16, 1'b1, 32);
        $display("  mid-transfer change: refused, reported, and NOT applied late");

        // 4. THE NEXT START APPLIES THE NEW REQUEST, and clears the report.
        run(6);
        check_cfg("after the next start", 1, 99, 12, 1'b0, 7);
        if (change_ignored) begin
            $display("  FAIL: the report survived a new start"); errors++;
        end
        $display("  next start: mode=%0d div=%0d width=%0d len=%0d, report cleared",
                 cfg_mode, cfg_div, cfg_width, cfg_len);

        // 5. A START WHILE BUSY IS IGNORED by this block -- remembering it is
        //    Chapter 13.10's job, and doing it here would put two owners on
        //    one decision.
        @(negedge clk);
        busy = 1'b1;
        request(2, 7, 4, 1'b1, 3);
        @(negedge clk);
        start = 1'b1;
        @(negedge clk);
        start = 1'b0;
        repeat (3) @(negedge clk);
        check_cfg("start while busy", 1, 99, 12, 1'b0, 7);
        busy = 1'b0;
        @(negedge clk);
        $display("  start while busy: configuration unchanged");

        // 6. THE CONTINUOUS STABILITY CHECK. Nothing above should have
        //    produced a single unstable cycle.
        if (unstable_cycles != 0) begin
            $display("  FAIL: the configuration changed on %0d busy cycles",
                     unstable_cycles);
            errors++;
        end
        $display("  stability: 0 busy cycles on which the configuration changed");

        // 7. A SWEEP. Many transfers, each with a different request and a
        //    change attempted part-way through, all required to be stable.
        for (int k = 0; k < 24; k++) begin
            request(k % 4, (k % 7) + 2, (k % 24) + 1, k[0], (k % 9) + 1);
            @(negedge clk);
            start = 1'b1;
            @(negedge clk);
            start = 1'b0;
            busy = 1'b1;
            repeat (2) @(negedge clk);
            // Attempt a change every time.
            req_div = DIV_W'(200 - k);
            req_mode = 2'((k + 1) % 4);
            repeat (4) @(negedge clk);
            if (!change_ignored) begin
                $display("  FAIL: transfer %0d did not report the change", k);
                errors++;
            end
            busy = 1'b0;
            @(negedge clk);
        end
        if (unstable_cycles != 0) begin
            $display("  FAIL: the sweep produced %0d unstable cycles",
                     unstable_cycles);
            errors++;
        end
        $display("  24 transfers swept: every change refused and reported, 0 unstable cycles");

        // 8. NO CHANGE MEANS NO REPORT. A request that stays identical
        //    through a transfer must not be reported -- otherwise the flag
        //    means nothing, since software usually leaves the request alone.
        request(1, 5, 8, 1'b0, 2);
        run(4);
        @(negedge clk);
        busy = 1'b1;
        repeat (6) @(negedge clk);       // busy, request unchanged
        if (change_ignored) begin
            $display("  FAIL: an unchanged request was reported as a change");
            errors++;
        end
        busy = 1'b0;
        @(negedge clk);
        $display("  unchanged request during a transfer: not reported");

        if (errors == 0)
            $display("PASS: reset loads a safe configuration that is not yet valid, a start latches every field at once, a change attempted mid-transfer is refused and reported and is not applied late, the next start applies it and clears the report, a start while busy changes nothing, an unchanged request is never reported, and across 24 transfers with a change attempted in each there was not one busy cycle on which the configuration in force changed");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch.v — the same latch in Verilog-2001
// spi_cfg_latch.v
//
// Chapter 13.2 -- where the control/datapath boundary actually sits.
//
// A microarchitecture is a set of decisions about which block knows what.
// This block is the first of those decisions, and it is the one that makes
// every later chapter simpler:
//
//   THE DATAPATH NEVER SEES A CHANGING CONFIGURATION.
//
// Mode, divisor, frame width, bit order and length are latched once, at the
// instant a transfer starts, and held for its whole duration. So the shift
// register (Ch 13.6), the divider (Ch 13.4), the edge logic (Ch 13.5) and
// the chip-select generator (Ch 13.7) can each read their parameter as a
// constant. None of them needs to handle it changing, because it cannot.
//
// That is worth stating as a cost as well as a benefit. The cost is one
// register per parameter -- about thirty flip-flops. The benefit is that
// four blocks lose an entire class of behaviour they would otherwise have
// to implement and verify. Buying simplicity in four places with thirty
// flip-flops in one is the trade this chapter argues for.
//
// A request that changes mid-transfer is IGNORED and REPORTED. Not applied
// late, not applied partially, and not silently dropped -- because software
// writing a new divisor while a transfer runs has made a mistake it needs
// to be told about, and a controller that applied it would produce a frame
// matching no device (Chapter 10.1's atomicity argument, now about time
// rather than about validity).

module spi_cfg_latch #(
    parameter DIV_W = 8,
    parameter LEN_W = 16
) (
    input  wire              clk,
    input  wire              rst_n,

    input  wire              start,    // pulse: begin a transfer
    input  wire              busy,     // from the control FSM

    // The requested configuration. May change at any time.
    input  wire  [1:0]       req_mode,
    input  wire  [DIV_W-1:0] req_div,
    input  wire  [5:0]       req_width,
    input  wire              req_lsb_first,
    input  wire  [LEN_W-1:0] req_len,

    // The configuration in force. Constant for the whole transfer.
    output reg   [1:0]       cfg_mode,
    output reg   [DIV_W-1:0] cfg_div,
    output reg   [5:0]       cfg_width,
    output reg               cfg_lsb_first,
    output reg   [LEN_W-1:0] cfg_len,
    output reg               cfg_valid,

    output reg               change_ignored  // sticky: a change was refused
);

    // Does the request currently differ from what is in force? Computed
    // continuously so a mid-transfer change is detected on the cycle it
    // appears rather than at the end of the transfer.
    wire differs = (req_mode      != cfg_mode)   ||
                   (req_div       != cfg_div)    ||
                   (req_width     != cfg_width)  ||
                   (req_lsb_first != cfg_lsb_first) ||
                   (req_len       != cfg_len);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset loads a safe configuration rather than an arbitrary one:
            // mode 0, the slowest divisor, eight bits, MSB first. The same
            // argument as Chapter 11.2's reset default -- a controller that
            // has not been configured must not be able to run fast or in an
            // exotic mode.
            cfg_mode       <= 2'd0;
            cfg_div        <= {DIV_W{1'b1}};
            cfg_width      <= 6'd8;
            cfg_lsb_first  <= 1'b0;
            cfg_len        <= {{(LEN_W-1){1'b0}}, 1'b1};
            cfg_valid      <= 1'b0;
            change_ignored <= 1'b0;
        end else begin
            if (start && !busy) begin
                // The single instant at which the configuration may change.
                cfg_mode       <= req_mode;
                cfg_div        <= req_div;
                cfg_width      <= req_width;
                cfg_lsb_first  <= req_lsb_first;
                cfg_len        <= req_len;
                cfg_valid      <= 1'b1;
                // A new transfer clears the previous verdict, so software
                // reads a report about the transfer it is looking at.
                change_ignored <= 1'b0;
            end else if (busy && differs) begin
                // Refused, and recorded. Note what is NOT here: any
                // assignment to cfg_*. The refusal is the absence of an
                // action rather than an action of its own, which is why no
                // partial application is possible.
                change_ignored <= 1'b1;
            end
        end
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch_tb.v — the same stability checks in Verilog-2001
// spi_cfg_latch_tb.v
//
// The property under test is STABILITY, and it is checked continuously
// rather than at the end: the configuration must not change on any cycle
// while busy, so the testbench snapshots it when busy rises and compares
// every cycle until busy falls.
//
// Checking only at the end would pass a design that changed the
// configuration mid-transfer and changed it back.

`timescale 1ns/1ps

module spi_cfg_latch_tb;

    integer k;

    localparam DIV_W = 8;
    localparam LEN_W = 16;

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

    reg             start;
    reg             busy;
    reg [1:0]       req_mode;
    reg [DIV_W-1:0] req_div;
    reg [5:0]       req_width;
    reg             req_lsb_first;
    reg [LEN_W-1:0] req_len;

    // Driven by the DUT, so declared as nets: a module output cannot drive
    // a variable in Verilog-2001, and using `wire` here keeps the two
    // published sources identical in structure.
    wire [1:0]       cfg_mode;
    wire [DIV_W-1:0] cfg_div;
    wire [5:0]       cfg_width;
    wire             cfg_lsb_first;
    wire [LEN_W-1:0] cfg_len;
    wire             cfg_valid;
    wire             change_ignored;

    integer errors;

    spi_cfg_latch #(.DIV_W(DIV_W), .LEN_W(LEN_W)) dut (
        .clk(clk), .rst_n(rst_n),
        .start(start), .busy(busy),
        .req_mode(req_mode), .req_div(req_div), .req_width(req_width),
        .req_lsb_first(req_lsb_first), .req_len(req_len),
        .cfg_mode(cfg_mode), .cfg_div(cfg_div), .cfg_width(cfg_width),
        .cfg_lsb_first(cfg_lsb_first), .cfg_len(cfg_len),
        .cfg_valid(cfg_valid), .change_ignored(change_ignored)
    );

    // Continuous stability checker: snapshot on the first busy cycle, then
    // compare on every subsequent one.
    reg [1:0]       s_mode;
    reg [DIV_W-1:0] s_div;
    reg [5:0]       s_width;
    reg             s_lsb;
    reg [LEN_W-1:0] s_len;
    reg             snapped;
    integer               unstable_cycles;

    always @(posedge clk) begin
        if (!rst_n) begin
            snapped         <= 1'b0;
            unstable_cycles <= 0;
        end else if (busy) begin
            if (!snapped) begin
                s_mode <= cfg_mode; s_div <= cfg_div; s_width <= cfg_width;
                s_lsb  <= cfg_lsb_first; s_len <= cfg_len;
                snapped <= 1'b1;
            end else if (cfg_mode !== s_mode || cfg_div !== s_div ||
                         cfg_width !== s_width || cfg_lsb_first !== s_lsb ||
                         cfg_len !== s_len) begin
                unstable_cycles <= unstable_cycles + 1;
            end
        end else begin
            snapped <= 1'b0;
        end
    end

        task request;
        input integer m;
        input integer d;
        input integer w;
        input lsb;
        input integer n;
        begin
            @(negedge clk);
            req_mode = (m); req_div = (d); req_width = (w);
            req_lsb_first = lsb; req_len = (n);
        end
    endtask

    // Begin a transfer of `cycles` busy cycles.
        task run;
        input integer cycles;
        begin
            @(negedge clk);
            start = 1'b1;
            @(negedge clk);
            start = 1'b0;
            busy = 1'b1;
            repeat (cycles) @(negedge clk);
            busy = 1'b0;
            @(negedge clk);
        end
    endtask

        task check_cfg;
        input [8*40:1] what;
        input integer m;
        input integer d;
        input integer w;
        input lsb;
        input integer n;
        begin
            if (cfg_mode !== (m) || cfg_div !== (d) ||
                cfg_width !== (w) || cfg_lsb_first !== lsb ||
                cfg_len !== (n)) begin
                $display("  FAIL: %0s -- cfg is mode=%0d div=%0d width=%0d lsb=%0b len=%0d, wanted %0d/%0d/%0d/%0b/%0d",
                         what, cfg_mode, cfg_div, cfg_width, cfg_lsb_first,
                         cfg_len, m, d, w, lsb, n);
                errors = errors + 1;
            end
        end
    endtask

    initial begin
        unstable_cycles = 0; snapped = 1'b0;
        repeat (3) @(negedge clk);
        rst_n = 1'b1;
        @(negedge clk);

        // 1. RESET LOADS A SAFE CONFIGURATION, and it is not yet valid --
        //    nothing may transact on a configuration nobody supplied.
        if (cfg_valid) begin
            $display("  FAIL: cfg_valid set out of reset"); errors = errors + 1;
        end
        if (cfg_div !== {DIV_W{1'b1}} || cfg_mode !== 2'd0 ||
            cfg_width !== 6'd8 || cfg_lsb_first !== 1'b0) begin
            $display("  FAIL: reset did not load the safe configuration (mode=%0d div=%0d width=%0d lsb=%0b)",
                     cfg_mode, cfg_div, cfg_width, cfg_lsb_first);
            errors = errors + 1;
        end
        $display("  reset: mode=%0d div=%0d width=%0d lsb=%0b, valid=%0b -- safe and not yet usable",
                 cfg_mode, cfg_div, cfg_width, cfg_lsb_first, cfg_valid);

        // 2. A START LATCHES THE WHOLE REQUEST.
        request(3, 4, 16, 1'b1, 32);
        run(10);
        check_cfg("after start", 3, 4, 16, 1'b1, 32);
        if (!cfg_valid) begin
            $display("  FAIL: cfg_valid not set after a start"); errors = errors + 1;
        end
        if (change_ignored) begin
            $display("  FAIL: a change was reported when none was made");
            errors = errors + 1;
        end
        $display("  latched: mode=%0d div=%0d width=%0d lsb=%0b len=%0d",
                 cfg_mode, cfg_div, cfg_width, cfg_lsb_first, cfg_len);

        // 3. A CHANGE MID-TRANSFER IS REFUSED AND REPORTED. Every field is
        //    changed, so a design that latched any one of them would fail.
        @(negedge clk);
        start = 1'b1;
        @(negedge clk);
        start = 1'b0;
        busy = 1'b1;
        repeat (3) @(negedge clk);
        req_mode = 2'd1; req_div = 8'd99; req_width = 6'd12;
        req_lsb_first = 1'b0; req_len = 16'd7;
        repeat (5) @(negedge clk);
        check_cfg("during a refused change", 3, 4, 16, 1'b1, 32);
        if (!change_ignored) begin
            $display("  FAIL: a mid-transfer change was not reported"); errors = errors + 1;
        end
        busy = 1'b0;
        @(negedge clk);
        // And it stays refused after busy drops -- the change is not applied
        // late, which would be the most confusing possible behaviour.
        check_cfg("after busy fell", 3, 4, 16, 1'b1, 32);
        $display("  mid-transfer change: refused, reported, and NOT applied late");

        // 4. THE NEXT START APPLIES THE NEW REQUEST, and clears the report.
        run(6);
        check_cfg("after the next start", 1, 99, 12, 1'b0, 7);
        if (change_ignored) begin
            $display("  FAIL: the report survived a new start"); errors = errors + 1;
        end
        $display("  next start: mode=%0d div=%0d width=%0d len=%0d, report cleared",
                 cfg_mode, cfg_div, cfg_width, cfg_len);

        // 5. A START WHILE BUSY IS IGNORED by this block -- remembering it is
        //    Chapter 13.10's job, and doing it here would put two owners on
        //    one decision.
        @(negedge clk);
        busy = 1'b1;
        request(2, 7, 4, 1'b1, 3);
        @(negedge clk);
        start = 1'b1;
        @(negedge clk);
        start = 1'b0;
        repeat (3) @(negedge clk);
        check_cfg("start while busy", 1, 99, 12, 1'b0, 7);
        busy = 1'b0;
        @(negedge clk);
        $display("  start while busy: configuration unchanged");

        // 6. THE CONTINUOUS STABILITY CHECK. Nothing above should have
        //    produced a single unstable cycle.
        if (unstable_cycles != 0) begin
            $display("  FAIL: the configuration changed on %0d busy cycles",
                     unstable_cycles);
            errors = errors + 1;
        end
        $display("  stability: 0 busy cycles on which the configuration changed");

        // 7. A SWEEP. Many transfers, each with a different request and a
        //    change attempted part-way through, all required to be stable.
        for (k = 0; k < 24; k = k + 1) begin
            request(k % 4, (k % 7) + 2, (k % 24) + 1, k[0], (k % 9) + 1);
            @(negedge clk);
            start = 1'b1;
            @(negedge clk);
            start = 1'b0;
            busy = 1'b1;
            repeat (2) @(negedge clk);
            // Attempt a change every time.
            req_div = (200 - k);
            req_mode = ((k + 1) % 4);
            repeat (4) @(negedge clk);
            if (!change_ignored) begin
                $display("  FAIL: transfer %0d did not report the change", k);
                errors = errors + 1;
            end
            busy = 1'b0;
            @(negedge clk);
        end
        if (unstable_cycles != 0) begin
            $display("  FAIL: the sweep produced %0d unstable cycles",
                     unstable_cycles);
            errors = errors + 1;
        end
        $display("  24 transfers swept: every change refused and reported, 0 unstable cycles");

        // 8. NO CHANGE MEANS NO REPORT. A request that stays identical
        //    through a transfer must not be reported -- otherwise the flag
        //    means nothing, since software usually leaves the request alone.
        request(1, 5, 8, 1'b0, 2);
        run(4);
        @(negedge clk);
        busy = 1'b1;
        repeat (6) @(negedge clk);       // busy, request unchanged
        if (change_ignored) begin
            $display("  FAIL: an unchanged request was reported as a change");
            errors = errors + 1;
        end
        busy = 1'b0;
        @(negedge clk);
        $display("  unchanged request during a transfer: not reported");

        if (errors == 0)
            $display("PASS: reset loads a safe configuration that is not yet valid, a start latches every field at once, a change attempted mid-transfer is refused and reported and is not applied late, the next start applies it and clears the report, a start while busy changes nothing, an unchanged request is never reported, and across 24 transfers with a change attempted in each there was not one busy cycle on which the configuration in force changed");
        else
            $display("FAIL: %0d error(s)", errors);
        $finish;
    end


    initial begin
        clk = 1'b0;
        rst_n = 1'b0;
        start = 1'b0;
        busy = 1'b0;
        req_mode = 2'd0;
        req_div = 8'd4;
        req_width = 6'd8;
        req_lsb_first = 1'b0;
        req_len = 16'd1;
        errors = 0;
    end

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch.vhd — the same latch in VHDL
-- spi_cfg_latch.vhd
--
-- Chapter 13.2 -- where the control/datapath boundary actually sits, in VHDL.
--
-- A microarchitecture is a set of decisions about which block knows what.
-- This is the first of them, and it makes every later chapter simpler:
--
--   THE DATAPATH NEVER SEES A CHANGING CONFIGURATION.
--
-- Mode, divisor, frame width, bit order and length are latched once, at the
-- instant a transfer starts, and held for its whole duration. So the shift
-- register, the divider, the edge logic and the chip-select generator can
-- each read their parameter as a constant -- none needs to handle it
-- changing, because it cannot.
--
-- The cost is one register per parameter, about thirty flip-flops. The
-- benefit is that four blocks lose an entire class of behaviour they would
-- otherwise have to implement and verify.
--
-- A request that changes mid-transfer is IGNORED and REPORTED. Not applied
-- late, not applied partially, and not silently dropped.

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

entity spi_cfg_latch is
    generic (
        DIV_W : positive := 8;
        LEN_W : positive := 16
    );
    port (
        clk            : in  std_logic;
        rst_n          : in  std_logic;

        start          : in  std_logic;   -- pulse: begin a transfer
        busy           : in  std_logic;   -- from the control FSM

        -- The requested configuration. May change at any time.
        req_mode       : in  unsigned(1 downto 0);
        req_div        : in  unsigned(DIV_W - 1 downto 0);
        req_width      : in  unsigned(5 downto 0);
        req_lsb_first  : in  std_logic;
        req_len        : in  unsigned(LEN_W - 1 downto 0);

        -- The configuration in force. Constant for the whole transfer.
        cfg_mode       : out unsigned(1 downto 0);
        cfg_div        : out unsigned(DIV_W - 1 downto 0);
        cfg_width      : out unsigned(5 downto 0);
        cfg_lsb_first  : out std_logic;
        cfg_len        : out unsigned(LEN_W - 1 downto 0);
        cfg_valid      : out std_logic;

        change_ignored : out std_logic     -- sticky: a change was refused
    );
end entity;

architecture rtl of spi_cfg_latch is

    -- Named with an r_ prefix rather than after the ports: VHDL is
    -- case-insensitive, so a signal called cfg_mode would be the same
    -- identifier as the port.
    signal r_mode  : unsigned(1 downto 0) := (others => '0');
    signal r_div   : unsigned(DIV_W - 1 downto 0) := (others => '1');
    signal r_width : unsigned(5 downto 0) := to_unsigned(8, 6);
    signal r_lsb   : std_logic := '0';
    signal r_len   : unsigned(LEN_W - 1 downto 0) := to_unsigned(1, LEN_W);
    signal r_valid : std_logic := '0';
    signal r_chg   : std_logic := '0';

    signal differs : std_logic;

begin

    -- Does the request currently differ from what is in force? Computed
    -- continuously so a mid-transfer change is detected on the cycle it
    -- appears rather than at the end of the transfer.
    differs <= '1' when (req_mode /= r_mode) or (req_div /= r_div) or
                        (req_width /= r_width) or (req_lsb_first /= r_lsb) or
                        (req_len /= r_len)
               else '0';

    latch : process (clk, rst_n)
    begin
        if rst_n = '0' then
            -- Reset loads a safe configuration rather than an arbitrary one:
            -- mode 0, the slowest divisor, eight bits, MSB first. A
            -- controller that has not been configured must not be able to
            -- run fast or in an exotic mode.
            r_mode  <= (others => '0');
            r_div   <= (others => '1');
            r_width <= to_unsigned(8, 6);
            r_lsb   <= '0';
            r_len   <= to_unsigned(1, LEN_W);
            r_valid <= '0';
            r_chg   <= '0';
        elsif rising_edge(clk) then
            if start = '1' and busy = '0' then
                -- The single instant at which the configuration may change.
                r_mode  <= req_mode;
                r_div   <= req_div;
                r_width <= req_width;
                r_lsb   <= req_lsb_first;
                r_len   <= req_len;
                r_valid <= '1';
                -- A new transfer clears the previous verdict, so software
                -- reads a report about the transfer it is looking at.
                r_chg   <= '0';
            elsif busy = '1' and differs = '1' then
                -- Refused, and recorded. Note what is NOT here: any
                -- assignment to the configuration registers. The refusal is
                -- the absence of an action rather than an action of its own,
                -- which is why no partial application is possible.
                r_chg <= '1';
            end if;
        end if;
    end process;

    cfg_mode       <= r_mode;
    cfg_div        <= r_div;
    cfg_width      <= r_width;
    cfg_lsb_first  <= r_lsb;
    cfg_len        <= r_len;
    cfg_valid      <= r_valid;
    change_ignored <= r_chg;

end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch_tb.vhd — the same stability checks in VHDL
-- spi_cfg_latch_tb.vhd
--
-- The property under test is STABILITY, and it is checked continuously
-- rather than at the end: the configuration must not change on any cycle
-- while busy, so the testbench snapshots it when busy rises and compares
-- every cycle until busy falls.
--
-- Checking only at the end would pass a design that changed the
-- configuration mid-transfer and changed it back.

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

entity spi_cfg_latch_tb is
end entity;

architecture sim of spi_cfg_latch_tb is

    constant DIV_W : positive := 8;
    constant LEN_W : positive := 16;

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

    signal start         : std_logic := '0';
    signal busy          : std_logic := '0';
    signal req_mode      : unsigned(1 downto 0) := (others => '0');
    signal req_div       : unsigned(DIV_W - 1 downto 0) := to_unsigned(4, DIV_W);
    signal req_width     : unsigned(5 downto 0) := to_unsigned(8, 6);
    signal req_lsb_first : std_logic := '0';
    signal req_len       : unsigned(LEN_W - 1 downto 0) := to_unsigned(1, LEN_W);

    signal cfg_mode       : unsigned(1 downto 0);
    signal cfg_div        : unsigned(DIV_W - 1 downto 0);
    signal cfg_width      : unsigned(5 downto 0);
    signal cfg_lsb_first  : std_logic;
    signal cfg_len        : unsigned(LEN_W - 1 downto 0);
    signal cfg_valid      : std_logic;
    signal change_ignored : std_logic;

    signal unstable_cycles : natural := 0;
    signal errors          : natural := 0;

begin

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

    dut : entity work.spi_cfg_latch
        generic map (DIV_W => DIV_W, LEN_W => LEN_W)
        port map (
            clk => clk, rst_n => rst_n,
            start => start, busy => busy,
            req_mode => req_mode, req_div => req_div, req_width => req_width,
            req_lsb_first => req_lsb_first, req_len => req_len,
            cfg_mode => cfg_mode, cfg_div => cfg_div, cfg_width => cfg_width,
            cfg_lsb_first => cfg_lsb_first, cfg_len => cfg_len,
            cfg_valid => cfg_valid, change_ignored => change_ignored
        );

    -- Continuous stability checker: snapshot on the first busy cycle, then
    -- compare on every subsequent one.
    stability : process (clk)
        variable s_mode  : unsigned(1 downto 0);
        variable s_div   : unsigned(DIV_W - 1 downto 0);
        variable s_width : unsigned(5 downto 0);
        variable s_lsb   : std_logic;
        variable s_len   : unsigned(LEN_W - 1 downto 0);
        variable snapped : boolean := false;
    begin
        if rising_edge(clk) then
            if rst_n = '0' then
                snapped := false;
                unstable_cycles <= 0;
            elsif busy = '1' then
                if not snapped then
                    s_mode := cfg_mode; s_div := cfg_div;
                    s_width := cfg_width; s_lsb := cfg_lsb_first;
                    s_len := cfg_len;
                    snapped := true;
                elsif cfg_mode /= s_mode or cfg_div /= s_div or
                      cfg_width /= s_width or cfg_lsb_first /= s_lsb or
                      cfg_len /= s_len then
                    unstable_cycles <= unstable_cycles + 1;
                end if;
            else
                snapped := false;
            end if;
        end if;
    end process;

    stim : process
        variable errs : natural := 0;

        -- Parameters are NOT named after the generics or ports, because VHDL
        -- is case-insensitive and they would shadow them.
        procedure request(n_mode : natural; n_div : natural; n_width : natural;
                          n_lsb : std_logic; n_len : natural) is
        begin
            wait until falling_edge(clk);
            req_mode      <= to_unsigned(n_mode, 2);
            req_div       <= to_unsigned(n_div, DIV_W);
            req_width     <= to_unsigned(n_width, 6);
            req_lsb_first <= n_lsb;
            req_len       <= to_unsigned(n_len, LEN_W);
        end procedure;

        procedure run(cycles : natural) is
        begin
            wait until falling_edge(clk);
            start <= '1';
            wait until falling_edge(clk);
            start <= '0';
            busy  <= '1';
            for k in 1 to cycles loop
                wait until falling_edge(clk);
            end loop;
            busy <= '0';
            wait until falling_edge(clk);
        end procedure;

        procedure check_cfg(what : string; n_mode : natural; n_div : natural;
                            n_width : natural; n_lsb : std_logic;
                            n_len : natural) is
        begin
            if to_integer(cfg_mode) /= n_mode or
               to_integer(cfg_div) /= n_div or
               to_integer(cfg_width) /= n_width or
               cfg_lsb_first /= n_lsb or
               to_integer(cfg_len) /= n_len then
                report "  FAIL: " & what & " -- the configuration is wrong";
                errs := errs + 1;
            end if;
        end procedure;
    begin
        for k in 0 to 2 loop
            wait until falling_edge(clk);
        end loop;
        rst_n <= '1';
        wait until falling_edge(clk);

        -- 1. RESET LOADS A SAFE CONFIGURATION, not yet valid.
        if cfg_valid = '1' then
            report "  FAIL: cfg_valid set out of reset"; errs := errs + 1;
        end if;
        if cfg_div /= (cfg_div'range => '1') or cfg_mode /= 0 or
           to_integer(cfg_width) /= 8 or cfg_lsb_first /= '0' then
            report "  FAIL: reset did not load the safe configuration";
            errs := errs + 1;
        end if;
        report "  reset: mode=0 div=" & integer'image(to_integer(cfg_div)) &
               " width=8 lsb=0, valid=0 -- safe and not yet usable";

        -- 2. A START LATCHES THE WHOLE REQUEST.
        request(3, 4, 16, '1', 32);
        run(10);
        check_cfg("after start", 3, 4, 16, '1', 32);
        if cfg_valid /= '1' then
            report "  FAIL: cfg_valid not set after a start"; errs := errs + 1;
        end if;
        if change_ignored = '1' then
            report "  FAIL: a change was reported when none was made";
            errs := errs + 1;
        end if;
        report "  latched: mode=3 div=4 width=16 lsb=1 len=32";

        -- 3. A CHANGE MID-TRANSFER IS REFUSED AND REPORTED. Every field is
        --    changed, so a design latching any one of them would fail.
        wait until falling_edge(clk);
        start <= '1';
        wait until falling_edge(clk);
        start <= '0';
        busy  <= '1';
        for k in 1 to 3 loop
            wait until falling_edge(clk);
        end loop;
        req_mode      <= to_unsigned(1, 2);
        req_div       <= to_unsigned(99, DIV_W);
        req_width     <= to_unsigned(12, 6);
        req_lsb_first <= '0';
        req_len       <= to_unsigned(7, LEN_W);
        for k in 1 to 5 loop
            wait until falling_edge(clk);
        end loop;
        check_cfg("during a refused change", 3, 4, 16, '1', 32);
        if change_ignored /= '1' then
            report "  FAIL: a mid-transfer change was not reported";
            errs := errs + 1;
        end if;
        busy <= '0';
        wait until falling_edge(clk);
        -- And it stays refused after busy drops -- not applied late, which
        -- would be the most confusing possible behaviour.
        check_cfg("after busy fell", 3, 4, 16, '1', 32);
        report "  mid-transfer change: refused, reported, and NOT applied late";

        -- 4. THE NEXT START APPLIES THE NEW REQUEST, and clears the report.
        run(6);
        check_cfg("after the next start", 1, 99, 12, '0', 7);
        if change_ignored = '1' then
            report "  FAIL: the report survived a new start"; errs := errs + 1;
        end if;
        report "  next start: mode=1 div=99 width=12 len=7, report cleared";

        -- 5. A START WHILE BUSY IS IGNORED by this block -- remembering it is
        --    Chapter 13.10's job, and doing it here would put two owners on
        --    one decision.
        wait until falling_edge(clk);
        busy <= '1';
        request(2, 7, 4, '1', 3);
        wait until falling_edge(clk);
        start <= '1';
        wait until falling_edge(clk);
        start <= '0';
        for k in 1 to 3 loop
            wait until falling_edge(clk);
        end loop;
        check_cfg("start while busy", 1, 99, 12, '0', 7);
        busy <= '0';
        wait until falling_edge(clk);
        report "  start while busy: configuration unchanged";

        -- 6. THE CONTINUOUS STABILITY CHECK.
        if unstable_cycles /= 0 then
            report "  FAIL: the configuration changed on a busy cycle";
            errs := errs + 1;
        end if;
        report "  stability: 0 busy cycles on which the configuration changed";

        -- 7. A SWEEP. Many transfers, each with a different request and a
        --    change attempted part-way through, all required to be stable.
        for k in 0 to 23 loop
            request(k mod 4, (k mod 7) + 2, (k mod 24) + 1,
                    std_logic'val((k mod 2) + character'pos('0') - 48 + 2),
                    (k mod 9) + 1);
            wait until falling_edge(clk);
            start <= '1';
            wait until falling_edge(clk);
            start <= '0';
            busy  <= '1';
            for j in 1 to 2 loop
                wait until falling_edge(clk);
            end loop;
            req_div  <= to_unsigned(200 - k, DIV_W);
            req_mode <= to_unsigned((k + 1) mod 4, 2);
            for j in 1 to 4 loop
                wait until falling_edge(clk);
            end loop;
            if change_ignored /= '1' then
                report "  FAIL: a swept transfer did not report the change";
                errs := errs + 1;
            end if;
            busy <= '0';
            wait until falling_edge(clk);
        end loop;
        if unstable_cycles /= 0 then
            report "  FAIL: the sweep produced unstable cycles";
            errs := errs + 1;
        end if;
        report "  24 transfers swept: every change refused and reported, 0 unstable cycles";

        -- 8. NO CHANGE MEANS NO REPORT, or the flag means nothing since
        --    software usually leaves the request alone.
        request(1, 5, 8, '0', 2);
        run(4);
        wait until falling_edge(clk);
        busy <= '1';
        for k in 1 to 6 loop
            wait until falling_edge(clk);
        end loop;
        if change_ignored = '1' then
            report "  FAIL: an unchanged request was reported as a change";
            errs := errs + 1;
        end if;
        busy <= '0';
        wait until falling_edge(clk);
        report "  unchanged request during a transfer: not reported";

        errors <= errs;
        if errs = 0 then
            report "PASS: reset loads a safe configuration that is not yet valid, a start latches every field at once, a change attempted mid-transfer is refused and reported and is not applied late, the next start applies it and clears the report, a start while busy changes nothing, an unchanged request is never reported, and across 24 transfers with a change attempted in each there was not one busy cycle on which the configuration in force changed";
        else
            report "FAIL: " & integer'image(errs) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

end architecture;

Parity

All three report zero unstable cycles across twenty-four transfers, including transfers during which every field was rewritten, and all three set change_ignored for exactly the writes that were ignored — not for writes that happened to arrive while busy and requested the value already in force, which is a distinction worth having because a driver that re-writes its configuration defensively before every transfer would otherwise see the flag constantly and learn to ignore it.

6. Why a Verification Engineer Cares

The property is a stability property, which is the shape assertions are best at.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch.sva — configuration is frozen for the life of a frame
// The whole of Chapter 13.2 in four assertions. Note that none of them mentions
// the requested value: the specification is about STABILITY, not about content,
// and an assertion that checked content would have to duplicate the load
// condition and would then agree with the design by construction.

module spi_cfg_latch_sva #(parameter int DIV_W = 8, LEN_W = 6) (
    input logic              clk,
    input logic              rst_n,
    input logic              start,
    input logic              busy,
    input logic [1:0]        cfg_mode,
    input logic [DIV_W-1:0]  cfg_div,
    input logic [LEN_W-1:0]  cfg_len,
    input logic              cfg_lsb_first,
    input logic [2:0]        cfg_width,
    input logic              change_ignored
);

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

    // The published configuration may change ONLY on a cycle that loads it.
    // Everything else follows from this.
    property p_stable_while_busy;
        busy && !(start && !busy) |=>
            $stable(cfg_mode) && $stable(cfg_div) && $stable(cfg_len) &&
            $stable(cfg_lsb_first) && $stable(cfg_width);
    endproperty
    a_stable: assert property (p_stable_while_busy);

    // All five fields move together or not at all. A partial update is five
    // races wearing one name, and this is the only assertion that finds it.
    property p_atomic;
        !$stable(cfg_div) |-> !$stable(cfg_mode) || $stable(cfg_mode);
    endproperty
    // Stated properly: on any cycle where the latch loads, every field takes
    // its requested value; on any other cycle, none of them moves.
    property p_all_or_none;
        (start && !busy) or
        ($stable(cfg_mode) && $stable(cfg_div) && $stable(cfg_len) &&
         $stable(cfg_lsb_first) && $stable(cfg_width));
    endproperty
    a_all_or_none: assert property (p_all_or_none);

    // The flag must be set by an ignored write, and it must be STICKY: a driver
    // that checks it once at the end of a transaction has to see a write that
    // was ignored twenty cycles earlier.
    property p_flag_sticky;
        change_ignored && !(start && !busy) |=> change_ignored;
    endproperty
    a_flag_sticky: assert property (p_flag_sticky);

    // And it must be cleared by the next accepted load, so it describes the
    // transfer in front of you rather than the history of the device.
    property p_flag_clears;
        (start && !busy) |=> !change_ignored;
    endproperty
    a_flag_clears: assert property (p_flag_clears);

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_cfg_latch_cg.sv — which fields were rewritten, and when
// The interesting axis is not the VALUES written -- there are far too many and
// they are all equivalent -- but WHICH FIELDS were rewritten and at what point
// in the transfer. A suite that only ever rewrites the divisor has not tested
// the latch; it has tested one fifth of it.

covergroup cg_cfg_latch @(posedge clk);

    // Which field differed from the one in force, as a bitmask. The cross of
    // this with `when` below is the real coverage goal.
    field: coverpoint differing_fields iff (req_write) {
        bins mode      = {5'b00001};
        bins div       = {5'b00010};
        bins len       = {5'b00100};
        bins order     = {5'b01000};
        bins width     = {5'b10000};
        bins several   = {[5'b00011:5'b11110]};
        bins all_five  = {5'b11111};
    }

    // When the write landed, relative to the transfer. IDLE must take effect;
    // everything else must not.
    when: coverpoint phase iff (req_write) {
        bins idle       = {0};
        bins first_bit  = {1};
        bins mid_frame  = {2};
        bins last_bit   = {3};
        bins inter_frame = {4};
    }

    // A write on the same cycle as the start request: the boundary case, and
    // the one where `start && !busy` differs from `start`.
    bins_on_start: coverpoint (req_write && start) {
        bins simultaneous = {1};
    }

    // And the flag's two outcomes must both occur, including the case where a
    // write arrives while busy but requests what is already in force -- which
    // must NOT set the flag.
    flag: coverpoint {change_ignored, redundant_write} iff (req_write && busy) {
        bins genuinely_ignored = {2'b10};
        bins no_change_wanted  = {2'b00};
    }

    x_field_when: cross field, when;

endgroup

7. Why an FPGA or ASIC Engineer Cares

The partition has consequences that show up in timing closure rather than in simulation.

The SCLK-rate logic is small, and deliberately. Only the divider, the mode logic and the two shift registers run at the bit rate. Everything else — five counters, a state machine, a register file — is once-per-frame logic whose timing is irrelevant at any plausible SCLK. If a master fails timing, the failing path is in a block the partition already isolated, and the fix is local.

The configuration latch breaks a long combinational path that would otherwise exist. Without it, the register file's flops feed the divider's comparators, the width barrel shifter and the mode mux directly. With it, they feed one set of flops. On an FPGA where the register file may be placed far from the SPI logic, that single stage of registers is often the difference between closing and not.

The latch is also where a clock-domain crossing would go, if the register bus were on a different clock. It is already a load-enabled register with a single load condition, so converting it into the destination side of a handshake is a local change — which is not true of a design where the datapath reads the registers directly, because there the crossing is in five places at once.

Cost. The latch is 2 + DIV_W + LEN_W + 1 + 3 flops — 21 at the defaults — plus one equality comparator across the same width and one flag. The unstable counter is development-only. Against a master of a few hundred flops this is noise, and it is the cheapest architectural insurance in the design.

8. Failure Signature — A Transfer That Is Corrupted Only Under Interrupt Load

Symptom. A sensor driver reads a 16-bit value every millisecond. Roughly one reading in ten thousand is wrong — not noise, but a value that looks like the correct value shifted or with the wrong upper byte. The rate rises sharply when an unrelated network interrupt becomes busy and falls to zero when the network is quiet.

What that rules out. A fault that depends on the load of an unrelated interrupt is not an analogue problem, not a sensor problem and not a bit-order problem — all three would be constant. It is a timing coincidence between two pieces of software, which narrows it to a shared resource.

The mechanism. The driver's read routine writes the control register before every transfer, defensively, with the same values every time. The network interrupt occasionally delays that routine so the write lands after the transfer has already started — and the master reads its configuration continuously. The write is idempotent in value, so nothing should change; but the register write takes effect mid-frame, and on this design the divisor register's output goes through a comparator whose result changes for one cycle while the write settles. One bit period is short by a cycle, and the slave's next sample lands in the wrong place.

Why it is one in ten thousand. The write has to land inside the frame, which is a window of a few microseconds in each millisecond, and the network interrupt has to delay it by the right amount. The conditional probability is small and the absolute rate is therefore small — but it is not zero, and it is not going to get better.

How the latch fixes it. With Answer C, a write landing mid-frame changes nothing in flight. The defensive rewrite still succeeds, change_ignored is set, and the transfer completes with the configuration it started with. The driver's behaviour does not have to change at all — which matters, because the driver is not wrong: writing your configuration before use is good practice.

The diagnostic that would have found it faster. change_ignored asserted in a system where no configuration ever changes is an immediate signal that writes are landing inside transfers. That is a one-line check at the end of the read routine, and it points at the interaction rather than at the sensor.

9. Common Misconceptions

"Rejecting writes while busy is safer than latching." It is equally safe and less useful. It forces every driver to poll before writing, it prevents configuring the next transfer during the current one, and it produces an error path that most drivers will not handle correctly. Latching makes the safe behaviour the default.

"A snapshot means the design has two copies of the configuration, which can disagree." They can and must: that is the point. The requested configuration and the in-force configuration are different things, and a design that cannot represent the difference cannot represent "the setting you just wrote takes effect next time".

"change_ignored is an error flag." It is a notification. Writing configuration while a transfer runs is legal and often deliberate. The flag exists so a driver author can tell the difference between "took effect now" and "takes effect next", which is otherwise invisible.

"The partition is a style preference; the same logic will synthesise the same way." It will not, because the partition changes which flops feed which combinational logic. The latch specifically inserts a register stage between the register file and the SCLK-rate logic, and removing it lengthens a real path.

"Control and datapath split along the line between state machines and registers." They split along the line between once-per-transfer and once-per-bit. A counter is a register and belongs on whichever side its counting rate puts it — the bit counter is datapath, the frame counter is control, and they are the same kind of object.

10. Reason It Through

Why must the load condition be start && !busy rather than start?

Because in a burst the second and subsequent frames are started while the transaction is still open and busy is asserted. With the condition on start alone, each of those frames would reload the snapshot from whatever is in the register file at that instant — so a mid-burst configuration write would take effect between two bytes of one transaction, which is Answer A restricted to burst boundaries. That is harder to find than Answer A, not easier, because most tests use single-frame transfers.

What would break if the five fields loaded independently, each on a write to its own register?

There would be no snapshot. A driver that writes mode and then divisor would have a window between the two writes in which the in-force configuration is half old and half new, and a frame starting in that window would use a combination that the driver never asked for. The bug would depend on the instruction timing of two consecutive stores, which is about as reproducible as anything gets.

Why does unstable count cycles rather than being a sticky bit?

Because the count distinguishes a single-cycle glitch from sustained instability, and the two have different causes. One cycle suggests a load condition that is momentarily true — a decode glitch, or a start that is not one cycle wide. Many cycles suggests the published configuration is simply wired to the requested one on some path. A sticky bit would say only that something is wrong.

A design uses Answer B, rejecting writes while busy, and a driver still sees corrupted transfers. Where would you look?

At what "busy" means. If busy is asserted only while SCLK is running, then the lead time, the inter-frame hold and the lag are all windows in which the design believes it is idle and a write will be accepted — and a write accepted during the inter-frame hold of a burst reaches the next frame of a transaction that is still in progress. Answer B is only as good as its definition of busy, and the definition that makes it correct is "a transaction is open", which is a different signal from "the clock is running".

Where would a clock-domain crossing go, if the register bus ran on a different clock from the SPI logic?

At the latch, and only there. The latch is already a load-enabled register with a single load condition, so the crossing becomes: synchronise the request-valid into the SPI domain, and load on the synchronised pulse. The data can cross without synchronisers because it is stable for many cycles before and after the load — a single-bit control crossing plus a stable data bus, which is the cheapest safe crossing there is. In a design where the datapath reads the register file directly, the same conversion needs five crossings and none of them has a stable data window.

11. Understanding Check

12. Summary

The master partitions along one question: does this logic do something different on every bit, or once per transfer? Per-bit logic is datapath and is timing-critical; per-transfer logic is control and is not. The partition is structural, not stylistic — migrating logic across it either lands it on the critical path or lets it be re-evaluated when it should be frozen.

Nothing in the datapath reads the register file. Everything arrives through the configuration latch, which makes "a register write cannot disturb a frame in flight" a property of the structure rather than a discipline someone must remember.

Of the three possible responses to a mid-transfer configuration write, snapshot at frame start is the right one. Applying immediately corrupts the frame; rejecting pushes an error path into software and forbids the useful pattern of configuring the next transfer during the current one; snapshotting does neither and costs about twenty flops.

The load condition is start && !busy, because burst frames start while the transaction is open and must keep the transaction's configuration. All five fields load together or not at all, because independent loads are five races wearing one name.

change_ignored is a notification, not an error, and the branch that sets it must assign nothing — a design that sets the flag and honours the write anyway is worse than one that does neither, because the flag then actively misleads.

For verification the property is stability, not content: the published configuration may change only on a load. Coverage bins which fields were rewritten crossed with when, because a suite that only ever rewrites the divisor has tested one fifth of the latch. And in UVM the same snapshot discipline applies to the configuration object, for the same reason.

For implementation the latch is a register stage on the path from the register file to the SCLK-rate logic, which often decides timing closure on an FPGA — and it is the single place a clock-domain crossing would go if the register bus ever moved to another clock.

13. What Comes Next

The blocks are named and the boundary between them is drawn. The first block to build is the one that decides what happens when.

Chapter 13.3 — The Master Control FSM builds the state machine that sequences a transfer: idle, the lead into the first edge, the shift, the lag out of the last edge, and the gap before the next transaction. Five states, and the interesting part is not that each exists but that one of them differs from another by exactly one behaviour — and that single difference is the whole reason the fifth state cannot be merged into the first.

Continue learning