Skip to content
VLSI Mentor

SPI · Module 4

Command, Address, and Data Phases

How a device layers a transaction onto a raw byte stream: why the opcode decides the shape of everything after it, how a slave tracks phases with no phase marker, and the sequencer that requires in three HDLs.

Chapter 4.2 fixed how many bits a word carries and Chapter 4.3 fixed which end goes first. Both were about a single word. Real devices are not operated one word at a time.

A flash read is an opcode, then an address, then a data burst. The bus provides no marker distinguishing them — so how does the device know which is which?

The answer is that it counts, and the thing it counts from is chip select. That single fact explains the structure of nearly every SPI device command set, the shape of the RTL in §6, and the failure mode in §10.

1. A Transaction Is Several Words in One Frame

Chapter 4.2 §3 established that CS delimits a transfer. Devices use that frame to carry a whole transaction: several words, each with a different role, in one continuous CS assertion.

The conventional decomposition has four kinds of phase:

  • Command — an opcode naming the operation. Usually one byte.
  • Address — where the operation applies. One to four bytes, or absent.
  • Dummy — turnaround time before the device can answer (Chapter 4.5).
  • Data — the payload, in either direction, often of unbounded length.

Not every transaction has all four, and the order is a device convention rather than a rule. What is universal is that they arrive back to back inside one frame, with nothing between them.

2. There Is No Phase Marker

This is the chapter's load-bearing fact and it is worth stating starkly.

Nothing on an SPI bus indicates a phase boundary. There is no framing bit between the opcode and the address, no delimiter before the data, no length field, and no signal that changes meaning. SCLK toggles identically in every phase. MOSI carries bits identically in every phase. CS stays asserted throughout.

So a device distinguishes phases by exactly one mechanism: it counts bytes since CS asserted. Byte 1 is the opcode because it is the first byte. Bytes 2–4 are the address because they are the second through fourth. There is no other information available.

A direct consequence: the count cannot be repaired mid-frame. There is no resynchronisation point, no marker to search for, and no error signal. Once the two ends disagree about which byte is which, they disagree until CS deasserts.

3. A Read, Step by Step

A sequence diagram of an SPI read transaction between a master and a device. The master asserts chip select, sends an opcode byte, then three address bytes. The device then returns two data bytes. Finally the master releases chip select.A read transaction: opcode, address, then datamasterdeviceCS asserts — frameopens0x03 — opcode0x12 — address[23:16]0x34 — address[15:8]0x56 — address [7:0]data byte 0data byte 1CS releases — framecloses
Figure 1 — a three-byte-address read, as an exchange. Each solid arrow is one byte the master clocks out; each dashed arrow is a byte the device returns. The device drives nothing meaningful until the address is complete, because until then it does not know what to fetch. Every one of these arrows is eight identical SCLK edges — only the position in the sequence distinguishes them.

The diagram shows the logical exchange. Remember from Chapter 1.4 that SPI is full duplex, so bits move in both directions during every one of those arrows — during the address phase the device is still shifting something onto MISO, and the master is obliged to ignore it. The arrows show which direction carries meaning, which is a device-level notion the bus knows nothing about.

4. The Phases on the Wire

One frame, three phases, no marker between them

8 cycles
Byte-time lanes for an SPI read. MOSI carries opcode 0x03 then address bytes 0x12, 0x34 and 0x56. MISO then carries two data bytes. A phase lane shows the device's internal state moving from command to address to data, and returning to idle when chip select releases.one transactionone transactiondevice can answerdevice can answercs_nmosi--0x030x120x340x56------miso----------D0D1--phaseidleCMDADDRADDRADDRDATADATAidlet0t1t2t3t4t5t6t7
Figure 2 — the same transaction as byte-time lanes. The lower lane is the device's internal phase state; it is drawn here because it explains the other lanes, but it is not a signal and does not exist on the bus. MISO carries nothing meaningful until the address is complete.

Cover the bottom lane and the figure becomes what an observer actually sees: a frame containing seven byte times, with bits in both directions and nothing to say where one phase ends and the next begins.

5. The Opcode Decides the Shape

Here is the part that makes the sequencer more than a counter.

Different commands have different shapes. A read carries an address and then data. A status read carries no address and goes straight to data. A write-enable carries neither — it is one byte, and the transaction is over.

So the device cannot know how to frame byte 2 until it has decoded byte 1. The opcode is not merely the first field; it is the field that determines what all the remaining fields are. Using a representative command set:

OpcodeOperationShape
0x03Readopcode → 3 address bytes → data out
0x05Read statusopcode → data out
0x06Write enableopcode only

This is why a slave's command decoder and its phase sequencer are the same piece of logic rather than two: the decode result is the next-state function.

6. Building the Phase Sequencer — Three HDLs

The circuit

Circuit. A small finite state machine sitting above Chapter 4.2's byte engine, consuming one byte_done strobe per received byte.

State. Which phase the transaction is in, the latched opcode, an accumulating address register, and a counter of address bytes received.

Datapath. The address register shifts in a byte at a time, most significant byte first — the byte-level analogue of Chapter 4.3's bit ordering, and an independent choice from it.

Control. cs_n forces the IDLE state unconditionally. Within a frame, byte_done advances the machine; the transition out of CMD is a function of the received opcode, which is §5 expressed as logic.

Clock. The system clock, as throughout — byte_done is a single-cycle strobe, not a clock.

Reset. Asynchronous, active-low, to IDLE with a cleared command and address.

Enables. Nothing advances except on byte_done, so the FSM is stationary between bytes.

Timing. The phase output is registered and therefore valid from the cycle after the byte that caused the transition — which matters for anything gating a data path on in_data.

Synthesis. Two state bits, eight command bits, 8 × ADDR_BYTES address bits, a small counter, and a comparator against the opcode constants. No latches: every case branch either assigns or explicitly holds.

Limitations. One address length and a three-command decode, which is the teaching scope. It has no dummy phase — that is Chapter 4.5 — and no write path or memory array, which belong to Module 14.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_seq.sv — the sequencer that turns byte position into meaning
// spi_phase_seq.sv — the slave-side phase sequencer.
//
// SPI puts no phase marker on the wire. A device knows whether the byte it
// just received was a command, an address or data for exactly one reason:
// it has been COUNTING BYTES since CS asserted. This FSM is that counting,
// and CS deassertion is the only thing that resynchronises it.
//
// Note that the command itself determines the shape of the rest of the
// transaction, so the FSM must decode byte 1 before it can interpret byte 2.
module spi_phase_seq #(
    parameter int ADDR_BYTES = 3
) (
    input  logic                     clk,
    input  logic                     rst_n,
    input  logic                     cs_n,
    input  logic                     byte_done,   // one pulse per received byte
    input  logic [7:0]               rx_byte,
    output logic [1:0]               phase,       // see the encoding below
    output logic [7:0]               cmd,
    output logic [8*ADDR_BYTES-1:0]  addr,
    output logic                     in_data
);
    // Phase encoding. IDLE is "CS deasserted": there is no transaction.
    localparam logic [1:0] PH_IDLE = 2'd0,
                           PH_CMD  = 2'd1,
                           PH_ADDR = 2'd2,
                           PH_DATA = 2'd3;

    // A representative command set. These opcodes are widespread conventions
    // in serial flash, NOT part of SPI -- another device may use the same
    // values for entirely different operations.
    localparam logic [7:0] CMD_READ        = 8'h03,  // address, then data
                           CMD_READ_STATUS = 8'h05,  // no address, then data
                           CMD_WRITE_EN    = 8'h06;  // no address, no data

    localparam int CW = (ADDR_BYTES > 1) ? $clog2(ADDR_BYTES) : 1;
    logic [CW-1:0] addr_cnt;

    // Does this opcode carry an address? Decoding byte 1 is what tells the
    // device how to frame bytes 2..n -- there is nothing on the wire to say.
    function automatic logic has_addr(input logic [7:0] opcode);
        return (opcode == CMD_READ);
    endfunction

    function automatic logic has_data(input logic [7:0] opcode);
        return (opcode == CMD_READ) || (opcode == CMD_READ_STATUS);
    endfunction

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            phase    <= PH_IDLE;
            cmd      <= 8'h00;
            addr     <= '0;
            addr_cnt <= '0;
        end else if (cs_n) begin
            // CS deasserted: the ONLY resynchronising event this FSM has.
            phase    <= PH_IDLE;
            addr_cnt <= '0;
        end else begin
            case (phase)
                PH_IDLE: begin
                    // CS is asserted; the first byte of the frame is a command.
                    phase    <= PH_CMD;
                    addr_cnt <= '0;
                end

                PH_CMD: if (byte_done) begin
                    cmd <= rx_byte;
                    if (has_addr(rx_byte)) begin
                        phase    <= PH_ADDR;
                        addr_cnt <= '0;
                    end else if (has_data(rx_byte)) begin
                        phase <= PH_DATA;
                    end
                    // else: command complete, stay in CMD and ignore the rest
                    // of the frame until CS rises.
                end

                PH_ADDR: if (byte_done) begin
                    addr <= {addr[8*ADDR_BYTES-9:0], rx_byte};   // MSB-first
                    if (addr_cnt == CW'(ADDR_BYTES - 1)) phase <= PH_DATA;
                    else                                 addr_cnt <= addr_cnt + 1'b1;
                end

                PH_DATA: ;   // remain until CS rises

                default: phase <= PH_IDLE;
            endcase
        end
    end

    assign in_data = (phase == PH_DATA);
endmodule

Two details are worth pausing on.

PH_IDLE transitions to PH_CMD with no byte_done. The frame is open as soon as CS asserts, so the machine must already be expecting a command before the first byte arrives. Waiting for a strobe here would consume the opcode as though it were an address byte.

The default branch returns to IDLE. With a two-bit state and four used encodings it is formally unreachable, and it is there so that a state corrupted by an upset resolves to a safe state rather than an undefined one. That is a deliberate choice rather than dead code, and on an FPGA targeting a radiation environment it is a requirement.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_seq_tb.sv — three transaction shapes, plus a frame aborted mid-address
// spi_phase_seq_tb.sv — three transaction shapes, plus a frame aborted mid-address.
`timescale 1ns/1ps
module spi_phase_seq_tb;
    logic clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    localparam int AB = 3;
    logic cs_n = 1, byte_done = 0;
    logic [7:0] rx_byte = 8'h00;
    logic [1:0] phase; logic [7:0] cmd; logic [8*AB-1:0] addr; logic in_data;

    spi_phase_seq #(.ADDR_BYTES(AB)) dut (.clk, .rst_n, .cs_n, .byte_done, .rx_byte,
                                          .phase, .cmd, .addr, .in_data);

    localparam logic [1:0] PH_IDLE = 2'd0, PH_CMD = 2'd1, PH_ADDR = 2'd2, PH_DATA = 2'd3;
    int errors = 0;
    task automatic chk(input string what, input int got, input int exp);
        if (got !== exp) begin $display("FAIL %s: got 0x%0h exp 0x%0h", what, got, exp); errors++; end
    endtask

    task automatic send_byte(input logic [7:0] b);
        rx_byte = b; @(negedge clk); byte_done = 1; @(negedge clk); byte_done = 0; @(negedge clk);
    endtask
    task automatic open_frame();  cs_n = 0; @(negedge clk); @(negedge clk); endtask
    task automatic close_frame(); cs_n = 1; @(negedge clk); @(negedge clk); endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
        chk("phase after reset", phase, PH_IDLE);

        // --- READ 0x03: command, three address bytes, then data ---
        open_frame();
        chk("phase at frame open", phase, PH_CMD);
        send_byte(8'h03);
        chk("cmd latched",         cmd,   8'h03);
        chk("phase after opcode",  phase, PH_ADDR);
        send_byte(8'h12); chk("phase addr byte 1", phase, PH_ADDR);
        send_byte(8'h34); chk("phase addr byte 2", phase, PH_ADDR);
        send_byte(8'h56);
        chk("phase after last addr byte", phase, PH_DATA);
        chk("addr accumulated", addr, 24'h123456);
        chk("in_data asserted", in_data, 1);
        send_byte(8'hAA);   // a data byte: phase must not move
        chk("phase stays DATA", phase, PH_DATA);
        close_frame();
        chk("phase after CS rise", phase, PH_IDLE);
        chk("in_data deasserted", in_data, 0);

        // --- READ STATUS 0x05: no address phase at all ---
        open_frame();
        send_byte(8'h05);
        chk("status: cmd",   cmd,   8'h05);
        chk("status: phase", phase, PH_DATA);
        close_frame();

        // --- WRITE ENABLE 0x06: no address, no data. Trailing bytes ignored. ---
        open_frame();
        send_byte(8'h06);
        chk("wren: cmd",   cmd,   8'h06);
        chk("wren: phase", phase, PH_CMD);
        send_byte(8'hFF);
        chk("wren: trailing byte ignored", phase, PH_CMD);
        close_frame();

        // --- Aborted frame: CS rises mid-address. The FSM must resynchronise
        //     so the NEXT frame is interpreted from its own first byte. ---
        open_frame();
        send_byte(8'h03);
        send_byte(8'hDE);            // one address byte, then abort
        chk("abort: mid-address", phase, PH_ADDR);
        close_frame();
        chk("abort: back to IDLE", phase, PH_IDLE);

        open_frame();
        send_byte(8'h05);            // a fresh, different command
        chk("recovery: cmd",   cmd,   8'h05);
        chk("recovery: phase", phase, PH_DATA);
        close_frame();

        if (errors == 0)
            $display("PASS: the opcode selects the transaction shape; CS resynchronises the sequencer after an aborted frame");
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

    initial begin #200000; $display("FAIL: watchdog timeout"); $finish; end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_seq.v — the same sequencer in Verilog-2001
// spi_phase_seq.v — the same phase sequencer in Verilog-2001.
module spi_phase_seq #(
    parameter ADDR_BYTES = 3
) (
    input  wire                     clk,
    input  wire                     rst_n,
    input  wire                     cs_n,
    input  wire                     byte_done,
    input  wire [7:0]               rx_byte,
    output reg  [1:0]               phase,
    output reg  [7:0]               cmd,
    output reg  [8*ADDR_BYTES-1:0]  addr,
    output wire                     in_data
);
    localparam PH_IDLE = 2'd0,
               PH_CMD  = 2'd1,
               PH_ADDR = 2'd2,
               PH_DATA = 2'd3;

    // Widespread serial-flash conventions, not SPI definitions.
    localparam CMD_READ        = 8'h03,
               CMD_READ_STATUS = 8'h05,
               CMD_WRITE_EN    = 8'h06;

    function integer clogb2;
        input integer value;
        integer v;
        begin
            v = value - 1;
            for (clogb2 = 0; v > 0; clogb2 = clogb2 + 1) v = v >> 1;
        end
    endfunction

    localparam CW = (ADDR_BYTES > 1) ? clogb2(ADDR_BYTES) : 1;
    reg [CW-1:0] addr_cnt;

    // Decoding byte 1 is what tells the device how to frame bytes 2..n.
    function has_addr;
        input [7:0] opcode;
        begin
            has_addr = (opcode == CMD_READ);
        end
    endfunction

    function has_data;
        input [7:0] opcode;
        begin
            has_data = (opcode == CMD_READ) || (opcode == CMD_READ_STATUS);
        end
    endfunction

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            phase    <= PH_IDLE;
            cmd      <= 8'h00;
            addr     <= {(8*ADDR_BYTES){1'b0}};
            addr_cnt <= {CW{1'b0}};
        end else if (cs_n) begin
            phase    <= PH_IDLE;
            addr_cnt <= {CW{1'b0}};
        end else begin
            case (phase)
                PH_IDLE: begin
                    phase    <= PH_CMD;
                    addr_cnt <= {CW{1'b0}};
                end

                PH_CMD: if (byte_done) begin
                    cmd <= rx_byte;
                    if (has_addr(rx_byte)) begin
                        phase    <= PH_ADDR;
                        addr_cnt <= {CW{1'b0}};
                    end else if (has_data(rx_byte)) begin
                        phase <= PH_DATA;
                    end
                end

                PH_ADDR: if (byte_done) begin
                    addr <= {addr[8*ADDR_BYTES-9:0], rx_byte};
                    if (addr_cnt == (ADDR_BYTES - 1)) phase <= PH_DATA;
                    else                              addr_cnt <= addr_cnt + 1'b1;
                end

                PH_DATA: ;

                default: phase <= PH_IDLE;
            endcase
        end
    end

    assign in_data = (phase == PH_DATA);
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_seq_tb.v — the same checks in Verilog-2001
// spi_phase_seq_tb.v — the same checks in Verilog-2001.
`timescale 1ns/1ps
module spi_phase_seq_tb;
    reg clk = 0, rst_n = 0;
    always #5 clk = ~clk;

    parameter AB = 3;
    reg cs_n = 1, byte_done = 0;
    reg [7:0] rx_byte = 8'h00;
    wire [1:0] phase; wire [7:0] cmd; wire [8*AB-1:0] addr; wire in_data;

    spi_phase_seq #(.ADDR_BYTES(AB)) dut (
        .clk(clk), .rst_n(rst_n), .cs_n(cs_n), .byte_done(byte_done), .rx_byte(rx_byte),
        .phase(phase), .cmd(cmd), .addr(addr), .in_data(in_data));

    localparam PH_IDLE = 2'd0, PH_CMD = 2'd1, PH_ADDR = 2'd2, PH_DATA = 2'd3;
    integer errors = 0;

    task chk;
        input [80*8-1:0] what;
        input [31:0] got, exp;
        begin
            if (got !== exp) begin
                $display("FAIL %0s: got 0x%0h exp 0x%0h", what, got, exp);
                errors = errors + 1;
            end
        end
    endtask

    task send_byte;
        input [7:0] b;
        begin
            rx_byte = b; @(negedge clk); byte_done = 1; @(negedge clk); byte_done = 0; @(negedge clk);
        end
    endtask
    task open_frame;  begin cs_n = 0; @(negedge clk); @(negedge clk); end endtask
    task close_frame; begin cs_n = 1; @(negedge clk); @(negedge clk); end endtask

    initial begin
        repeat (3) @(negedge clk); rst_n = 1; @(negedge clk);
        chk("phase after reset", phase, PH_IDLE);

        open_frame();
        chk("phase at frame open", phase, PH_CMD);
        send_byte(8'h03);
        chk("cmd latched",        cmd,   8'h03);
        chk("phase after opcode", phase, PH_ADDR);
        send_byte(8'h12); chk("phase addr byte 1", phase, PH_ADDR);
        send_byte(8'h34); chk("phase addr byte 2", phase, PH_ADDR);
        send_byte(8'h56);
        chk("phase after last addr byte", phase, PH_DATA);
        chk("addr accumulated", addr, 24'h123456);
        chk("in_data asserted", in_data, 1);
        send_byte(8'hAA);
        chk("phase stays DATA", phase, PH_DATA);
        close_frame();
        chk("phase after CS rise", phase, PH_IDLE);
        chk("in_data deasserted", in_data, 0);

        open_frame();
        send_byte(8'h05);
        chk("status: cmd",   cmd,   8'h05);
        chk("status: phase", phase, PH_DATA);
        close_frame();

        open_frame();
        send_byte(8'h06);
        chk("wren: cmd",   cmd,   8'h06);
        chk("wren: phase", phase, PH_CMD);
        send_byte(8'hFF);
        chk("wren: trailing byte ignored", phase, PH_CMD);
        close_frame();

        open_frame();
        send_byte(8'h03);
        send_byte(8'hDE);
        chk("abort: mid-address", phase, PH_ADDR);
        close_frame();
        chk("abort: back to IDLE", phase, PH_IDLE);

        open_frame();
        send_byte(8'h05);
        chk("recovery: cmd",   cmd,   8'h05);
        chk("recovery: phase", phase, PH_DATA);
        close_frame();

        if (errors == 0)
            $display("PASS: the opcode selects the transaction shape; CS resynchronises the sequencer after an aborted frame");
        else
            $display("FAILED with %0d error(s)", errors);
        $finish;
    end

    initial begin #200000; $display("FAIL: watchdog timeout"); $finish; end
endmodule

VHDL expresses this design best of the three, because an enumerated state type lets the states be named things rather than encodings. Note that the output vector is produced by an explicit encode function: the encoding is a property of the interface, not of the state machine, and keeping them separate is what allows a synthesis tool to choose its own internal encoding freely.

Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_seq.vhd — the same sequencer in VHDL, with an enumerated state type
-- spi_phase_seq.vhd — the same phase sequencer in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_phase_seq is
    generic (
        ADDR_BYTES : positive := 3
    );
    port (
        clk       : in  std_logic;
        rst_n     : in  std_logic;
        cs_n      : in  std_logic;
        byte_done : in  std_logic;
        rx_byte   : in  std_logic_vector(7 downto 0);
        phase     : out std_logic_vector(1 downto 0);
        cmd       : out std_logic_vector(7 downto 0);
        addr      : out std_logic_vector(8 * ADDR_BYTES - 1 downto 0);
        in_data   : out std_logic
    );
end entity spi_phase_seq;

architecture rtl of spi_phase_seq is
    -- An enumerated state type says what the states ARE; the output vector is
    -- an encoding of it, kept separate so the encoding is not the state.
    type phase_t is (PH_IDLE, PH_CMD, PH_ADDR, PH_DATA);
    signal state : phase_t;

    -- Widespread serial-flash conventions, not SPI definitions.
    constant CMD_READ        : std_logic_vector(7 downto 0) := x"03";
    constant CMD_READ_STATUS : std_logic_vector(7 downto 0) := x"05";
    constant CMD_WRITE_EN    : std_logic_vector(7 downto 0) := x"06";

    signal addr_cnt : integer range 0 to ADDR_BYTES - 1;
    signal addr_r   : std_logic_vector(8 * ADDR_BYTES - 1 downto 0);

    -- Decoding byte 1 is what tells the device how to frame bytes 2..n.
    function has_addr (opcode : std_logic_vector(7 downto 0)) return boolean is
    begin
        return opcode = CMD_READ;
    end function;

    function has_data (opcode : std_logic_vector(7 downto 0)) return boolean is
    begin
        return (opcode = CMD_READ) or (opcode = CMD_READ_STATUS);
    end function;

    function encode (s : phase_t) return std_logic_vector is
    begin
        case s is
            when PH_IDLE => return "00";
            when PH_CMD  => return "01";
            when PH_ADDR => return "10";
            when PH_DATA => return "11";
        end case;
    end function;
begin

    process (clk, rst_n) is
    begin
        if rst_n = '0' then
            state    <= PH_IDLE;
            cmd      <= (others => '0');
            addr_r   <= (others => '0');
            addr_cnt <= 0;
        elsif rising_edge(clk) then
            if cs_n = '1' then
                -- CS deasserted: the ONLY resynchronising event this FSM has.
                state    <= PH_IDLE;
                addr_cnt <= 0;
            else
                case state is
                    when PH_IDLE =>
                        state    <= PH_CMD;
                        addr_cnt <= 0;

                    when PH_CMD =>
                        if byte_done = '1' then
                            cmd <= rx_byte;
                            if has_addr(rx_byte) then
                                state    <= PH_ADDR;
                                addr_cnt <= 0;
                            elsif has_data(rx_byte) then
                                state <= PH_DATA;
                            end if;
                        end if;

                    when PH_ADDR =>
                        if byte_done = '1' then
                            addr_r <= addr_r(8 * ADDR_BYTES - 9 downto 0) & rx_byte;
                            if addr_cnt = ADDR_BYTES - 1 then
                                state <= PH_DATA;
                            else
                                addr_cnt <= addr_cnt + 1;
                            end if;
                        end if;

                    when PH_DATA =>
                        null;   -- remain until CS rises
                end case;
            end if;
        end if;
    end process;

    phase   <= encode(state);
    addr    <= addr_r;
    in_data <= '1' when state = PH_DATA else '0';

end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_seq_tb.vhd — the same checks in VHDL
-- spi_phase_seq_tb.vhd — the same checks in VHDL.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity spi_phase_seq_tb is
end entity spi_phase_seq_tb;

architecture tb of spi_phase_seq_tb is
    constant AB : positive := 3;

    signal clk       : std_logic := '0';
    signal rst_n     : std_logic := '0';
    signal cs_n      : std_logic := '1';
    signal byte_done : std_logic := '0';
    signal rx_byte   : std_logic_vector(7 downto 0) := (others => '0');
    signal halt      : boolean := false;

    signal phase   : std_logic_vector(1 downto 0);
    signal cmd     : std_logic_vector(7 downto 0);
    signal addr    : std_logic_vector(8 * AB - 1 downto 0);
    signal in_data : std_logic;

    signal errors : natural := 0;

    constant PH_IDLE : std_logic_vector(1 downto 0) := "00";
    constant PH_CMD  : std_logic_vector(1 downto 0) := "01";
    constant PH_ADDR : std_logic_vector(1 downto 0) := "10";
    constant PH_DATA : std_logic_vector(1 downto 0) := "11";
begin

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

    dut : entity work.spi_phase_seq
        generic map (ADDR_BYTES => AB)
        port map (clk => clk, rst_n => rst_n, cs_n => cs_n, byte_done => byte_done,
                  rx_byte => rx_byte, phase => phase, cmd => cmd, addr => addr,
                  in_data => in_data);

    stim : process is
        procedure chk (what : string; got, exp : std_logic_vector) is
        begin
            if got /= exp then
                report "FAIL " & what & ": got 0x" & to_hstring(got)
                    & " exp 0x" & to_hstring(exp) severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure chk_bit (what : string; got, exp : std_logic) is
        begin
            if got /= exp then
                report "FAIL " & what severity error;
                errors <= errors + 1;
            end if;
        end procedure;

        procedure send_byte (b : std_logic_vector(7 downto 0)) is
        begin
            rx_byte <= b;
            wait until falling_edge(clk); byte_done <= '1';
            wait until falling_edge(clk); byte_done <= '0';
            wait until falling_edge(clk);
        end procedure;

        procedure open_frame is
        begin
            cs_n <= '0';
            wait until falling_edge(clk);
            wait until falling_edge(clk);
        end procedure;

        procedure close_frame is
        begin
            cs_n <= '1';
            wait until falling_edge(clk);
            wait until falling_edge(clk);
        end procedure;
    begin
        for i in 0 to 2 loop
            wait until falling_edge(clk);
        end loop;
        rst_n <= '1';
        wait until falling_edge(clk);
        chk("phase after reset", phase, PH_IDLE);

        open_frame;
        chk("phase at frame open", phase, PH_CMD);
        send_byte(x"03");
        chk("cmd latched",        cmd,   x"03");
        chk("phase after opcode", phase, PH_ADDR);
        send_byte(x"12"); chk("phase addr byte 1", phase, PH_ADDR);
        send_byte(x"34"); chk("phase addr byte 2", phase, PH_ADDR);
        send_byte(x"56");
        chk("phase after last addr byte", phase, PH_DATA);
        chk("addr accumulated", addr, x"123456");
        chk_bit("in_data asserted", in_data, '1');
        send_byte(x"AA");
        chk("phase stays DATA", phase, PH_DATA);
        close_frame;
        chk("phase after CS rise", phase, PH_IDLE);
        chk_bit("in_data deasserted", in_data, '0');

        open_frame;
        send_byte(x"05");
        chk("status: cmd",   cmd,   x"05");
        chk("status: phase", phase, PH_DATA);
        close_frame;

        open_frame;
        send_byte(x"06");
        chk("wren: cmd",   cmd,   x"06");
        chk("wren: phase", phase, PH_CMD);
        send_byte(x"FF");
        chk("wren: trailing byte ignored", phase, PH_CMD);
        close_frame;

        open_frame;
        send_byte(x"03");
        send_byte(x"DE");
        chk("abort: mid-address", phase, PH_ADDR);
        close_frame;
        chk("abort: back to IDLE", phase, PH_IDLE);

        open_frame;
        send_byte(x"05");
        chk("recovery: cmd",   cmd,   x"05");
        chk("recovery: phase", phase, PH_DATA);
        close_frame;

        wait until falling_edge(clk);
        if errors = 0 then
            report "PASS: the opcode selects the transaction shape; CS resynchronises "
                 & "the sequencer after an aborted frame" severity note;
        else
            report "FAILED with " & integer'image(errors) & " error(s)" severity error;
        end if;
        halt <= true;
        wait;
    end process;

    watchdog : process is
    begin
        wait for 200 us;
        if not halt then
            report "FAIL: watchdog timeout" severity failure;
        end if;
        wait;
    end process;

end architecture tb;

Parity

All three describe the same machine: identical ports, asynchronous active-low reset to IDLE, cs_n forcing IDLE with priority over every other transition, IDLE advancing to CMD without a strobe, the CMD exit selected by decoding the received opcode, an address register shifting in most-significant byte first, and in_data a combinational decode of the DATA state. All three testbenches run the same four scenarios — a full read, a status read with no address, a write-enable whose trailing bytes are ignored, and a frame aborted mid-address followed by a different command.

7. Why CS Is the Only Resynchronisation

The aborted-frame test above is the most important one in the testbench, and it tests §2's consequence directly.

When CS rises, the sequencer returns to IDLE unconditionally — not "if the transaction was complete", not "unless an address was partially received". Any partial state is discarded. That is not defensive programming; it is the only correct behaviour, because a partially-received transaction carries no information about how much of it was valid.

This gives SPI a recovery property worth appreciating. A device that has become confused — wrong phase, wrong count, half an address — is restored to a known state by a single CS deassertion. There is no reset sequence, no timeout, no escape character. Toggling CS resynchronises any SPI slave, which is why "drop CS and retry" is the universal first move in SPI recovery, and why a driver should never leave CS asserted across an error path.

The cost is the flip side of the same coin: within a frame there is no recovery at all. The two ends either agree or they do not, and nothing in the frame can tell them which.

8. Why a Verification Engineer Cares

The phase sequence is a temporal property, which makes it exactly what assertions are for:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_legal.sva — phases advance only in legal order, and CS always resets them
   // Phase order is a DEVICE property, so this checker belongs to the device
   // model, not to a reusable SPI protocol checker (Chapter 4.1 §7).

   // 1. CS deassertion must return the sequencer to IDLE -- the recovery
   //    property of §7, which the whole error strategy depends on.
   property p_cs_resyncs;
       @(posedge clk) disable iff (!rst_n)
           cs_n |=> (phase == PH_IDLE);
   endproperty
   a_cs_resyncs : assert property (p_cs_resyncs)
       else $error("CS deasserted but phase did not return to IDLE");

   // 2. A phase may only change on a completed byte (or on CS). Catches a
   //    sequencer advancing on something other than byte_done.
   property p_phase_stable_between_bytes;
       @(posedge clk) disable iff (!rst_n || cs_n)
           !byte_done |=> $stable(phase);
   endproperty
   a_phase_stable : assert property (p_phase_stable_between_bytes)
       else $error("phase changed without a completed byte");

   // 3. ADDR is reachable only from CMD -- never entered spontaneously.
   property p_addr_entered_from_cmd;
       @(posedge clk) disable iff (!rst_n)
           $rose(phase == PH_ADDR) |-> $past(phase) == PH_CMD;
   endproperty
   a_addr_from_cmd : assert property (p_addr_entered_from_cmd)
       else $error("entered ADDR from an illegal predecessor");

What these prove. That the sequencer's transitions obey the intended state graph and that CS resynchronises it. What they do not prove. That the phase structure matches the device being modelled — that is a datasheet fact — nor that the master is sending the right number of bytes. A slave sequencer and a master can each be internally consistent and still disagree, which is §10's failure.

Coverage should target transaction shapes rather than opcodes:

Azvya Education Pvt. Ltd.VLSI Mentor
spi_phase_cg.sv — cover the shapes, and the aborts
   covergroup spi_phase_cg @(posedge cs_rose);   // sample at frame close
       // Opcodes are a large, mostly uninteresting space. The SHAPES are
       // small and are what the sequencer actually implements.
       cp_shape : coverpoint txn_shape {
           bins cmd_only        = {SHAPE_CMD};
           bins cmd_data        = {SHAPE_CMD_DATA};
           bins cmd_addr_data   = {SHAPE_CMD_ADDR_DATA};
       }

       // §7: aborting in each phase is the state space that matters, because
       // an abort is the only mid-transaction event the device can see.
       cp_abort_phase : coverpoint phase_at_cs_rise {
           bins in_cmd  = {PH_CMD};
           bins in_addr = {PH_ADDR};
           bins in_data = {PH_DATA};
       }

       // A short frame in the ADDR phase is the specific case that leaves a
       // partial address behind -- cross it with the shape that has one.
       x_shape_abort : cross cp_shape, cp_abort_phase;

       // Address boundaries: a byte-shifting address register is where
       // off-by-one and endian errors live.
       cp_addr : coverpoint rx_addr {
           bins zero    = {0};
           bins low     = {[1:255]};
           bins crosses = {24'h00FFFF, 24'h00FF00, 24'hFFFFFF};
       }
   endgroup

The cp_abort_phase coverpoint is the one most often missing. Every environment tests complete transactions; comparatively few deliberately drop CS during an address phase, which is precisely the case where a partial address is sitting in a register and the next frame's correctness depends on it being discarded.

9. Why an FPGA or ASIC Engineer Cares

The FSM is trivially small and the address register is not. Two state bits cost nothing. A 32-bit address register plus its shift path is real logic, and on a device supporting both 3-byte and 4-byte addressing it must be sized for the maximum with the count becoming a configuration input rather than a constant — the same runtime-versus-elaboration trade Chapter 4.2 §8 described for width.

The phase output is registered, and timing depends on it. in_data is a decode of a registered state, so anything it gates — a memory read enable, an output multiplexer, a tri-state control — sees it one cycle after the byte that caused the transition. On a slave that must present data promptly after the address (Chapter 2.6), that cycle is part of the latency budget, and it is a common source of "the first data byte is wrong" on the first spin.

Do not clock this FSM with SCLK. It is tempting, since the FSM advances once per byte and bytes are defined by SCLK. But SCLK is an input driven by an external master: it stops between transactions, is not free-running, and on an FPGA it is not on a global clock network unless explicitly routed there. Driving a state machine from it creates a gated clock domain that starts and stops with an external device. The strobe-based architecture used throughout this track keeps everything in one always-running domain — the reasoning Chapter 2.3 established, and the subject of Module 15.

10. Failure Signature — The Opcode Works and the Address Is Nonsense

Symptom. A flash read returns data from the wrong location. The device clearly received and recognised the command — it responded, and status reads work correctly — but the address it acted on is not the one requested. The error is reproducible and identical every time.

Why "status reads work" is the key evidence. A status read is a one-byte command with no address phase. That it works proves the mode, bit order, framing and command decode are all correct. The fault is therefore specific to transactions that have an address phase — which eliminates most of the hypothesis space in a single observation.

Plausible mechanisms.

  • Address length disagreement. The device expects 4 address bytes and the master sends 3, or the reverse. Common on larger flash parts that support both, where the mode is a configuration register.
  • Byte order within the address. The master sends least-significant byte first into a device that shifts most-significant byte first — the byte-level analogue of Chapter 4.3, and an entirely separate setting from bit order.
  • An off-by-one byte count in the master, so the address is shifted by one byte position and the first data byte is consumed as address.
  • A missing dummy phase (Chapter 4.5), which misaligns the data rather than the address — distinguishable, since here the address is wrong.

The discriminating observation. Compare the returned address against the requested one numerically. The relationship identifies the mechanism directly: a byte-reversed value indicates byte ordering; a value shifted by eight or sixteen bit positions indicates a length or count disagreement; and a value equal to the requested address with one byte of the payload appended indicates an off-by-one.

That comparison costs nothing and is far more informative than a capture, because each mechanism produces a structurally distinct arithmetic relationship rather than merely a wrong number.

Why the investigation goes wrong. Because the returned data is usually inspected as data — checked against the expected contents, found to be wrong, and treated as a data-path problem. The address is the corrupted field, and it is visible only if you work out which address the returned data actually came from. Reading the returned bytes as a location rather than a value is what turns this from a long investigation into a two-minute one.

11. Common Misconceptions

12. Reason It Through

Work this before reading the answer.

A 256 Mbit flash part is being brought up. Status reads work perfectly. Reads from address 0x000000 return correct data. Reads from any higher address return data from a location that is close to but not equal to the requested one — and specifically, requesting 0x012345 returns the contents of 0x2345XX.

What is wrong?

Read the relationship, not the values. Requesting 0x012345 and receiving the contents of 0x2345XX means the device's address register ended up holding the requested address shifted left by one byte, with an unknown byte occupying the bottom position. That is the entire diagnosis available in one step, and it points at a byte-count disagreement rather than anything to do with data.

Which direction is the disagreement? The device shifted in one byte more than the master intended to supply as address. So the device expects a 4-byte address while the master is sending 3, and the first byte the master intended as data was consumed as the fourth address byte — which is where the unknown low byte came from.

Why does 0x000000 work? Because shifting zero left by a byte is still zero — near enough. The top three bytes are zero either way, and the only difference is the spurious low byte, which lands within the same region and returns plausible-looking data. This is the same phenomenon as Chapter 4.3 §5: a symmetric test value hides an asymmetric bug, and address zero is the most commonly chosen first test in existence.

Why 256 Mbit specifically? 256 Mbit is 32 Mbyte, which needs 25 address bits — just over the 24 that three bytes provide. Parts at and above this capacity therefore support 4-byte addressing, commonly with a configuration register or a distinct set of opcodes selecting between 3-byte and 4-byte mode, and frequently with a power-on default that is not what the driver assumes. The capacity is the clue that the two modes exist at all.

What confirms it? Send the read with four address bytes instead of three and see whether the correct data returns. That is one line of driver change and it either confirms or eliminates the hypothesis immediately — considerably faster than a capture, because the capture would show exactly the bytes the master intended to send, all of which are individually correct.

The general lesson. When an address comes back wrong, the arithmetic relationship between requested and actual names the mechanism: a shift means a count disagreement, a reversal means byte ordering, an increment means an off-by-one. And testing from address zero will hide a byte-count bug every time.

13. Understanding Check

14. Summary

Devices build transactions out of several words inside one CS assertion: a command, optionally an address, optionally a dummy phase, and data. The decomposition is a device convention, and SPI knows nothing about it.

Nothing on the bus marks a phase boundary. SCLK, MOSI and CS are identical in every phase, so a device distinguishes them by counting bytes since CS asserted. The phase structure lives entirely as a pair of counters inside the endpoints, which is why a miscount is not detectable as an error — it is simply a different, internally consistent reading of the same bits.

The opcode decides the shape of everything after it, so the command decoder and the phase sequencer are one piece of logic: the decode result is the next-state function. In RTL this is a small FSM above the byte engine, notable for advancing to the command phase without waiting for a strobe, and for treating CS deassertion as an unconditional return to IDLE.

That unconditional reset gives SPI its recovery property: one CS toggle resynchronises any slave, with no reset sequence or timeout. The same fact means there is no recovery within a frame at all.

For verification, phase order is a temporal property and belongs in assertions, while coverage should target transaction shapes and — the bin most often missing — the phase an aborted frame lands in, since that is the only mid-transaction event a device can observe.

When an address comes back wrong, the arithmetic relationship between requested and actual names the mechanism: a byte shift means a count disagreement, a reversal means byte ordering, an increment means an off-by-one.

15. What Comes Next

Figure 1 showed the device answering immediately after the final address byte. Real devices frequently cannot: an internal array access takes time, and on a shared data line the bus itself must turn around. Chapter 4.5 — Dummy Phases and Read Latency closes the module with the phase that exists purely to buy time — why it is measured in clock cycles rather than bytes, how it is extended as clock frequency rises, what it costs in throughput, and the off-by-one it produces when a driver and a device disagree about its length.

Continue learning